Skip to content

Commit 1db1523

Browse files
committed
fix: harden PowerContext memory contract
1 parent 32bfebc commit 1db1523

17 files changed

Lines changed: 339 additions & 68 deletions

File tree

console/src/locales/en.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2368,7 +2368,7 @@
23682368
"llmAcquireTimeoutGtPauseJitter": "Slot acquire timeout must be greater than rate limit pause + jitter",
23692369
"contextManagementTitle": "Context Management",
23702370
"memoryManagerBackend": "Long-term Memory Management Backend",
2371-
"memoryManagerBackendTooltip": "Backend for the memory manager. Choose remelight (local files), adbpg (cloud database), or none (disable memory).",
2371+
"memoryManagerBackendTooltip": "Backend for the memory manager. Choose remelight (local files), adbpg (cloud database), PowerContext (remote memory service), or none (disable memory).",
23722372
"memoryManagerBackendRestartWarning": "Switching the long-term memory management backend does not support hot reload. Save and restart QwenPaw for changes to take effect.",
23732373
"maxIters": "Max Iterations",
23742374
"maxItersTooltip": "Maximum number of reasoning-acting iterations for ReAct agent",

console/src/locales/id.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2364,7 +2364,7 @@
23642364
"llmAcquireTimeoutGtPauseJitter": "Timeout perolehan slot harus lebih besar dari jeda batas kecepatan + jitter",
23652365
"contextManagementTitle": "Manajemen Konteks",
23662366
"memoryManagerBackend": "Backend Manajemen Memori Jangka Panjang",
2367-
"memoryManagerBackendTooltip": "Backend untuk manajer memori. Pilih remelight (file lokal) atau adbpg (database cloud).",
2367+
"memoryManagerBackendTooltip": "Backend untuk manajer memori. Pilih remelight (file lokal), adbpg (database cloud), PowerContext (layanan memori jarak jauh), atau none (nonaktifkan memori).",
23682368
"memoryManagerBackendRestartWarning": "Mengalihkan backend manajemen memori jangka panjang tidak mendukung hot reload. Simpan dan restart QwenPaw agar perubahan berlaku.",
23692369
"maxIters": "Iterasi Maksimum",
23702370
"maxItersTooltip": "Jumlah maksimum iterasi penalaran-tindakan untuk agen ReAct",

console/src/locales/ja.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1912,7 +1912,7 @@
19121912
"llmAcquireTimeoutGtPauseJitter": "スロット取得タイムアウトはレート制限一時停止とジッターの合計より大きい必要があります",
19131913
"contextManagementTitle": "コンテキスト管理",
19141914
"memoryManagerBackend": "長期記憶管理バックエンド",
1915-
"memoryManagerBackendTooltip": "メモリマネージャーのバックエンドタイプ。remelight(ローカルファイル)または adbpg(クラウドデータベース)から選択できます。",
1915+
"memoryManagerBackendTooltip": "メモリマネージャーのバックエンドタイプ。remelight(ローカルファイル)adbpg(クラウドデータベース)、PowerContext(リモートメモリサービス)、または none(メモリを無効化)から選択できます。",
19161916
"memoryManagerBackendRestartWarning": "長期記憶管理バックエンドの切り替えはホットリロードに対応していません。変更を反映するには保存後に QwenPaw を再起動してください。",
19171917
"maxIters": "最大反復回数",
19181918
"maxItersTooltip": "ReActエージェントの推論・行動サイクルの最大反復回数",

console/src/locales/pt-BR.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2150,7 +2150,7 @@
21502150
"llmAcquireTimeoutGtPauseJitter": "Slot acquire timeout must be greater than rate limit pause + jitter",
21512151
"contextManagementTitle": "Context Management",
21522152
"memoryManagerBackend": "Backend de gerenciamento de memória de longo prazo",
2153-
"memoryManagerBackendTooltip": "Backend para o gerenciador de memória. Escolha remelight (arquivos locais) ou adbpg (banco de dados na nuvem).",
2153+
"memoryManagerBackendTooltip": "Backend para o gerenciador de memória. Escolha remelight (arquivos locais), adbpg (banco de dados na nuvem), PowerContext (serviço remoto de memória) ou none (desativar memória).",
21542154
"memoryManagerBackendRestartWarning": "A troca do backend de gerenciamento de memória de longo prazo não permite recarregamento a quente. Salve e reinicie o QwenPaw para aplicar as alterações.",
21552155
"maxIters": "Max Iterations",
21562156
"maxItersTooltip": "Maximum number of reasoning-acting iterations for ReAct agent",

console/src/locales/ru.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1927,7 +1927,7 @@
19271927
"llmAcquireTimeoutGtPauseJitter": "Таймаут получения слота должен быть больше суммы паузы и джиттера",
19281928
"contextManagementTitle": "Управление контекстом",
19291929
"memoryManagerBackend": "Бэкенд управления долговременной памятью",
1930-
"memoryManagerBackendTooltip": "Тип бэкенда менеджера памяти. Выберите remelight (локальные файлы) или adbpg (облачная база данных).",
1930+
"memoryManagerBackendTooltip": "Тип бэкенда менеджера памяти. Выберите remelight (локальные файлы), adbpg (облачная база данных), PowerContext (удалённый сервис памяти) или none (отключить память).",
19311931
"memoryManagerBackendRestartWarning": "Смена бэкенда управления долговременной памятью не поддерживает горячую перезагрузку. Сохраните и перезапустите QwenPaw для применения изменений.",
19321932
"maxIters": "Макс. итераций",
19331933
"maxItersTooltip": "Максимальное число итераций рассуждение-действие для агента ReAct",

console/src/locales/vi.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1238,7 +1238,7 @@
12381238
"llmAcquireTimeoutGtPauseJitter": "Timeout phải lớn hơn tổng thời gian tạm dừng và jitter",
12391239
"contextManagementTitle": "Quản lý ngữ cảnh",
12401240
"memoryManagerBackend": "Backend quản lý bộ nhớ dài hạn",
1241-
"memoryManagerBackendTooltip": "Chọn remelight (file cục bộ) hoặc adbpg (cơ sở dữ liệu đám mây).",
1241+
"memoryManagerBackendTooltip": "Chọn remelight (tệp cục bộ), adbpg (cơ sở dữ liệu đám mây), PowerContext (dịch vụ bộ nhớ từ xa), hoặc none (tắt bộ nhớ).",
12421242
"memoryManagerBackendRestartWarning": "Đổi backend quản lý bộ nhớ dài hạn yêu cầu khởi động lại QwenPaw.",
12431243
"powercontextMemoryTitle": "Bộ nhớ PowerContext",
12441244
"powercontextConfig": {

console/src/locales/zh.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2226,7 +2226,7 @@
22262226
"llmAcquireTimeoutGtPauseJitter": "槽位获取超时必须大于限流暂停时长与抖动范围之和",
22272227
"contextManagementTitle": "上下文管理",
22282228
"memoryManagerBackend": "长期记忆管理后端",
2229-
"memoryManagerBackendTooltip": "记忆管理器的后端类型,可选 remelight(本地文件)、adbpg(云端数据库)或 none(关闭记忆)",
2229+
"memoryManagerBackendTooltip": "记忆管理器的后端类型,可选 remelight(本地文件)、adbpg(云端数据库)、PowerContext(远程记忆服务)或 none(关闭记忆)",
22302230
"memoryManagerBackendRestartWarning": "切换长期记忆管理后端不支持热更新,保存后需要重启 QwenPaw 才能生效",
22312231
"maxIters": "最大迭代次数",
22322232
"maxItersTooltip": "ReAct 智能体的最大推理-行动迭代次数",

console/src/pages/Agent/Config/components/PowerContextConfigCard.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,18 @@ export function PowerContextConfigCard() {
7474
"max_results",
7575
]}
7676
initialValue={3}
77-
rules={[{ required: true }, { type: "number", min: 1 }]}
77+
rules={[
78+
{ required: true },
79+
{ type: "number", min: 1, max: 50 },
80+
]}
7881
tooltip={t("agentConfig.autoMaxResultsTooltip")}
7982
>
80-
<InputNumber style={{ width: "100%" }} min={1} step={1} />
83+
<InputNumber
84+
style={{ width: "100%" }}
85+
min={1}
86+
max={50}
87+
step={1}
88+
/>
8189
</Form.Item>
8290
</>
8391
),

src/qwenpaw/agents/memory/powercontext_client.py

Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# -*- coding: utf-8 -*-
12
"""Small async client for PowerContext's public memory HTTP contract."""
23

34
from __future__ import annotations
@@ -7,6 +8,31 @@
78

89
import httpx
910

11+
MAX_MEMORY_TEXT_BYTES = 8000
12+
MIN_SEARCH_RESULTS = 1
13+
MAX_SEARCH_RESULTS = 50
14+
15+
16+
def truncate_utf8_text(
17+
text: str,
18+
*,
19+
max_bytes: int = MAX_MEMORY_TEXT_BYTES,
20+
) -> str:
21+
"""Bound text without splitting a UTF-8 code point.
22+
23+
PowerContext accepts at most 8192 normalized UTF-8 bytes. Keep a small
24+
margin so the client remains valid when the server normalizes whitespace.
25+
"""
26+
encoded = text.encode("utf-8")
27+
if len(encoded) <= max_bytes:
28+
return text
29+
return encoded[:max_bytes].decode("utf-8", errors="ignore")
30+
31+
32+
def bound_search_limit(limit: int) -> int:
33+
"""Clamp a caller-provided result count to the PowerContext contract."""
34+
return min(MAX_SEARCH_RESULTS, max(MIN_SEARCH_RESULTS, limit))
35+
1036

1137
@dataclass(frozen=True)
1238
class PowerContextConfig:
@@ -25,14 +51,29 @@ class PowerContextHTTPError(RuntimeError):
2551
"""
2652

2753
def __init__(
28-
self, *, operation: str, response: httpx.Response, token: str = ""
54+
self,
55+
*,
56+
operation: str,
57+
response: httpx.Response,
58+
token: str = "",
2959
) -> None:
3060
self.operation = operation
3161
self.status_code = response.status_code
3262
self.summary = _safe_error_summary(response, token=token)
3363
super().__init__(
3464
f"PowerContext {operation} failed with HTTP {self.status_code}: "
35-
f"{self.summary}"
65+
f"{self.summary}",
66+
)
67+
68+
69+
class PowerContextProtocolError(RuntimeError):
70+
"""Safe error for a successful response that violates the API contract."""
71+
72+
def __init__(self, *, operation: str, summary: str) -> None:
73+
self.operation = operation
74+
self.summary = summary
75+
super().__init__(
76+
f"PowerContext {operation} returned invalid response: {summary}",
3677
)
3778

3879

@@ -70,7 +111,11 @@ def _safe_error_summary(response: httpx.Response, *, token: str = "") -> str:
70111
)
71112
if isinstance(payload, str) and payload.strip():
72113
summary = payload.strip()
73-
return summary.replace(token, "<redacted>")[:300] if token else summary[:300]
114+
return (
115+
summary.replace(token, "<redacted>")[:300]
116+
if token
117+
else summary[:300]
118+
)
74119
return response.reason_phrase or "request failed"
75120

76121

@@ -87,39 +132,64 @@ def __init__(self, config: PowerContextConfig) -> None:
87132
)
88133

89134
async def remember(
90-
self, *, kind: str, text: str, scope_id: str | None = None
135+
self,
136+
*,
137+
kind: str,
138+
text: str,
139+
scope_id: str | None = None,
91140
) -> dict[str, Any]:
92141
response = await self._http.post(
93142
"/v1/memory/remember",
94143
json={
95144
"scope_id": scope_id or self.config.scope_id,
96145
"kind": kind,
97-
"text": text,
146+
"text": truncate_utf8_text(text),
98147
},
99148
)
100149
self._raise_for_status("memory remember", response)
101150
return response.json()
102151

103152
async def search(
104-
self, *, query: str, limit: int = 5, scope_id: str | None = None
153+
self,
154+
*,
155+
query: str,
156+
limit: int = 5,
157+
scope_id: str | None = None,
105158
) -> list[dict[str, Any]]:
106159
response = await self._http.post(
107160
"/v1/memory/search",
108161
json={
109162
"scope_id": scope_id or self.config.scope_id,
110163
"query": query,
111-
"limit": limit,
164+
"limit": bound_search_limit(limit),
112165
},
113166
)
114167
self._raise_for_status("memory search", response)
115168
payload = response.json()
116-
return payload.get("hits", []) if isinstance(payload, dict) else []
169+
if not isinstance(payload, dict):
170+
raise PowerContextProtocolError(
171+
operation="memory search",
172+
summary="response body must be an object",
173+
)
174+
hits = payload.get("hits")
175+
if not isinstance(hits, list):
176+
raise PowerContextProtocolError(
177+
operation="memory search",
178+
summary="response does not contain a hits list",
179+
)
180+
return hits
117181

118182
async def close(self) -> None:
119183
await self._http.aclose()
120184

121-
def _raise_for_status(self, operation: str, response: httpx.Response) -> None:
185+
def _raise_for_status(
186+
self,
187+
operation: str,
188+
response: httpx.Response,
189+
) -> None:
122190
if response.is_error:
123191
raise PowerContextHTTPError(
124-
operation=operation, response=response, token=self.config.token
192+
operation=operation,
193+
response=response,
194+
token=self.config.token,
125195
)

0 commit comments

Comments
 (0)