Spaces:
Sleeping
Sleeping
fix: Hugging Face sync, routing, dashboard, and agent isolation bugs
Browse files- src/app.py +2 -0
- src/functions.py +4 -0
- src/routes/graph.py +3 -49
- src/routes/mcp.py +43 -1
- src/routes/observations.py +28 -4
- start.sh +4 -0
- sync.py +17 -7
- tests/test_properties.py +8 -8
src/app.py
CHANGED
|
@@ -92,6 +92,8 @@ def create_app() -> Flask:
|
|
| 92 |
|
| 93 |
# 4. Flask app + blueprints
|
| 94 |
flask_app = Flask(__name__)
|
|
|
|
|
|
|
| 95 |
from routes import register_blueprints
|
| 96 |
register_blueprints(flask_app)
|
| 97 |
|
|
|
|
| 92 |
|
| 93 |
# 4. Flask app + blueprints
|
| 94 |
flask_app = Flask(__name__)
|
| 95 |
+
from werkzeug.middleware.proxy_fix import ProxyFix
|
| 96 |
+
flask_app.wsgi_app = ProxyFix(flask_app.wsgi_app, x_proto=1, x_host=1, x_port=1, x_prefix=1)
|
| 97 |
from routes import register_blueprints
|
| 98 |
register_blueprints(flask_app)
|
| 99 |
|
src/functions.py
CHANGED
|
@@ -3494,6 +3494,10 @@ def folder_graph_build(kv: StateKV) -> Dict[str, Any]:
|
|
| 3494 |
Edges are deduplicated on (source, target, type).
|
| 3495 |
"""
|
| 3496 |
index_entries = kv.list(KV.folders)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3497 |
|
| 3498 |
# --- Build folder_map and collect obs text per (folder, agent) pair ---
|
| 3499 |
# folder_map: folderPath -> {"folderPath", "agentIds": set, "obsCount", "color"}
|
|
|
|
| 3494 |
Edges are deduplicated on (source, target, type).
|
| 3495 |
"""
|
| 3496 |
index_entries = kv.list(KV.folders)
|
| 3497 |
+
if is_agent_scope_isolated():
|
| 3498 |
+
aid = get_agent_id()
|
| 3499 |
+
if aid:
|
| 3500 |
+
index_entries = [e for e in index_entries if e.get("agentId") == aid]
|
| 3501 |
|
| 3502 |
# --- Build folder_map and collect obs text per (folder, agent) pair ---
|
| 3503 |
# folder_map: folderPath -> {"folderPath", "agentIds": set, "obsCount", "color"}
|
src/routes/graph.py
CHANGED
|
@@ -59,55 +59,9 @@ def api_graph_stats():
|
|
| 59 |
return auth_err
|
| 60 |
|
| 61 |
kv = _get_kv()
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
folders = set()
|
| 66 |
-
concepts_by_folder = {}
|
| 67 |
-
|
| 68 |
-
for s in sessions:
|
| 69 |
-
project = s.get("project", "").strip()
|
| 70 |
-
if project:
|
| 71 |
-
folders.add(project)
|
| 72 |
-
|
| 73 |
-
for m in memories:
|
| 74 |
-
project = m.get("project", "").strip()
|
| 75 |
-
if project:
|
| 76 |
-
folders.add(project)
|
| 77 |
-
concepts = m.get("concepts", [])
|
| 78 |
-
if concepts:
|
| 79 |
-
if project not in concepts_by_folder:
|
| 80 |
-
concepts_by_folder[project] = set()
|
| 81 |
-
for c in concepts:
|
| 82 |
-
if isinstance(c, str):
|
| 83 |
-
concepts_by_folder[project].add(c.lower())
|
| 84 |
-
|
| 85 |
-
node_count = len(folders)
|
| 86 |
-
edge_count = 0
|
| 87 |
-
folder_list = list(folders)
|
| 88 |
-
|
| 89 |
-
for i in range(len(folder_list)):
|
| 90 |
-
for j in range(i + 1, len(folder_list)):
|
| 91 |
-
f1 = folder_list[i]
|
| 92 |
-
f2 = folder_list[j]
|
| 93 |
-
|
| 94 |
-
c1 = concepts_by_folder.get(f1, set())
|
| 95 |
-
c2 = concepts_by_folder.get(f2, set())
|
| 96 |
-
shared = c1.intersection(c2)
|
| 97 |
-
|
| 98 |
-
p1 = [p for p in f1.replace("\\", "/").split("/") if p]
|
| 99 |
-
p2 = [p for p in f2.replace("\\", "/").split("/") if p]
|
| 100 |
-
common_subdirs = 0
|
| 101 |
-
for k in range(min(len(p1), len(p2))):
|
| 102 |
-
if p1[k].lower() == p2[k].lower():
|
| 103 |
-
p_low = p1[k].lower()
|
| 104 |
-
if p_low not in ("c:", "d:", "downloads", "projects", "other projects"):
|
| 105 |
-
common_subdirs += 1
|
| 106 |
-
else:
|
| 107 |
-
break
|
| 108 |
-
|
| 109 |
-
if len(shared) > 0 or common_subdirs > 0:
|
| 110 |
-
edge_count += 1
|
| 111 |
|
| 112 |
return jsonify({"nodes": node_count, "edges": edge_count, "success": True}), 200
|
| 113 |
|
|
|
|
| 59 |
return auth_err
|
| 60 |
|
| 61 |
kv = _get_kv()
|
| 62 |
+
g = functions.folder_graph_build(kv)
|
| 63 |
+
node_count = len(g.get("nodes", []))
|
| 64 |
+
edge_count = len(g.get("edges", []))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
return jsonify({"nodes": node_count, "edges": edge_count, "success": True}), 200
|
| 67 |
|
src/routes/mcp.py
CHANGED
|
@@ -247,6 +247,12 @@ def mcp_tools_call():
|
|
| 247 |
limit = int(args.get("limit") or 10)
|
| 248 |
folder_path = args.get("folderPath")
|
| 249 |
agent_id = args.get("agentId")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
res = functions.folder_search(kv, q, limit, folder_path=folder_path, agent_id=agent_id)
|
| 251 |
text_out = json.dumps(res, indent=2)
|
| 252 |
|
|
@@ -269,6 +275,12 @@ def mcp_tools_call():
|
|
| 269 |
limit = int(args.get("limit") or 10)
|
| 270 |
folder_path = args.get("folderPath")
|
| 271 |
agent_id = args.get("agentId")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
res = functions.folder_search(kv, q, limit, folder_path=folder_path, agent_id=agent_id)
|
| 273 |
text_out = json.dumps(res, indent=2)
|
| 274 |
|
|
@@ -313,6 +325,13 @@ def mcp_tools_call():
|
|
| 313 |
or "agent"
|
| 314 |
)
|
| 315 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
if not text:
|
| 317 |
return jsonify({"error": "text (or content) is required"}), 400
|
| 318 |
|
|
@@ -338,6 +357,14 @@ def mcp_tools_call():
|
|
| 338 |
mem_type = args.get("type") or "fact"
|
| 339 |
concepts = _parse_mcp_list_arg(args.get("concepts"))
|
| 340 |
files = _parse_mcp_list_arg(args.get("files"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
if not content:
|
| 342 |
return jsonify({"error": "content is required"}), 400
|
| 343 |
payload = {
|
|
@@ -357,6 +384,10 @@ def mcp_tools_call():
|
|
| 357 |
key=lambda x: x.get("lastUpdated", ""),
|
| 358 |
reverse=True,
|
| 359 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
text_out = json.dumps(pairs, indent=2)
|
| 361 |
|
| 362 |
elif name == "memory_folder_observations":
|
|
@@ -364,6 +395,10 @@ def mcp_tools_call():
|
|
| 364 |
aid = args.get("agentId", "")
|
| 365 |
if not fp or not aid:
|
| 366 |
return jsonify({"error": "folderPath and agentId are required"}), 400
|
|
|
|
|
|
|
|
|
|
|
|
|
| 367 |
obs = sorted(
|
| 368 |
kv.list(KV.folder_obs(fp, aid)),
|
| 369 |
key=lambda x: x.get("timestamp", ""),
|
|
@@ -372,11 +407,18 @@ def mcp_tools_call():
|
|
| 372 |
text_out = json.dumps(obs, indent=2)
|
| 373 |
|
| 374 |
elif name == "memory_timeline":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 375 |
res = functions.folder_timeline(
|
| 376 |
kv,
|
| 377 |
limit=int(args.get("limit", 100)),
|
| 378 |
folder_path=args.get("folderPath"),
|
| 379 |
-
agent_id=
|
| 380 |
before=args.get("before"),
|
| 381 |
after=args.get("after"),
|
| 382 |
)
|
|
|
|
| 247 |
limit = int(args.get("limit") or 10)
|
| 248 |
folder_path = args.get("folderPath")
|
| 249 |
agent_id = args.get("agentId")
|
| 250 |
+
if functions.is_agent_scope_isolated():
|
| 251 |
+
current_aid = functions.get_agent_id()
|
| 252 |
+
if current_aid:
|
| 253 |
+
if agent_id and agent_id != current_aid:
|
| 254 |
+
return jsonify({"error": "Unauthorized: Agent scope is isolated"}), 403
|
| 255 |
+
agent_id = current_aid
|
| 256 |
res = functions.folder_search(kv, q, limit, folder_path=folder_path, agent_id=agent_id)
|
| 257 |
text_out = json.dumps(res, indent=2)
|
| 258 |
|
|
|
|
| 275 |
limit = int(args.get("limit") or 10)
|
| 276 |
folder_path = args.get("folderPath")
|
| 277 |
agent_id = args.get("agentId")
|
| 278 |
+
if functions.is_agent_scope_isolated():
|
| 279 |
+
current_aid = functions.get_agent_id()
|
| 280 |
+
if current_aid:
|
| 281 |
+
if agent_id and agent_id != current_aid:
|
| 282 |
+
return jsonify({"error": "Unauthorized: Agent scope is isolated"}), 403
|
| 283 |
+
agent_id = current_aid
|
| 284 |
res = functions.folder_search(kv, q, limit, folder_path=folder_path, agent_id=agent_id)
|
| 285 |
text_out = json.dumps(res, indent=2)
|
| 286 |
|
|
|
|
| 325 |
or "agent"
|
| 326 |
)
|
| 327 |
|
| 328 |
+
if functions.is_agent_scope_isolated():
|
| 329 |
+
current_aid = functions.get_agent_id()
|
| 330 |
+
if current_aid:
|
| 331 |
+
if agent_id and agent_id != current_aid:
|
| 332 |
+
return jsonify({"error": "Unauthorized: Agent scope is isolated"}), 403
|
| 333 |
+
agent_id = current_aid
|
| 334 |
+
|
| 335 |
if not text:
|
| 336 |
return jsonify({"error": "text (or content) is required"}), 400
|
| 337 |
|
|
|
|
| 357 |
mem_type = args.get("type") or "fact"
|
| 358 |
concepts = _parse_mcp_list_arg(args.get("concepts"))
|
| 359 |
files = _parse_mcp_list_arg(args.get("files"))
|
| 360 |
+
|
| 361 |
+
if functions.is_agent_scope_isolated():
|
| 362 |
+
current_aid = functions.get_agent_id()
|
| 363 |
+
if current_aid:
|
| 364 |
+
if agent_id and agent_id != current_aid:
|
| 365 |
+
return jsonify({"error": "Unauthorized: Agent scope is isolated"}), 403
|
| 366 |
+
agent_id = current_aid
|
| 367 |
+
|
| 368 |
if not content:
|
| 369 |
return jsonify({"error": "content is required"}), 400
|
| 370 |
payload = {
|
|
|
|
| 384 |
key=lambda x: x.get("lastUpdated", ""),
|
| 385 |
reverse=True,
|
| 386 |
)
|
| 387 |
+
if functions.is_agent_scope_isolated():
|
| 388 |
+
aid = functions.get_agent_id()
|
| 389 |
+
if aid:
|
| 390 |
+
pairs = [p for p in pairs if p.get("agentId") == aid]
|
| 391 |
text_out = json.dumps(pairs, indent=2)
|
| 392 |
|
| 393 |
elif name == "memory_folder_observations":
|
|
|
|
| 395 |
aid = args.get("agentId", "")
|
| 396 |
if not fp or not aid:
|
| 397 |
return jsonify({"error": "folderPath and agentId are required"}), 400
|
| 398 |
+
if functions.is_agent_scope_isolated():
|
| 399 |
+
current_aid = functions.get_agent_id()
|
| 400 |
+
if current_aid and aid != current_aid:
|
| 401 |
+
return jsonify({"error": "Unauthorized: Agent scope is isolated"}), 403
|
| 402 |
obs = sorted(
|
| 403 |
kv.list(KV.folder_obs(fp, aid)),
|
| 404 |
key=lambda x: x.get("timestamp", ""),
|
|
|
|
| 407 |
text_out = json.dumps(obs, indent=2)
|
| 408 |
|
| 409 |
elif name == "memory_timeline":
|
| 410 |
+
request_aid = args.get("agentId")
|
| 411 |
+
if functions.is_agent_scope_isolated():
|
| 412 |
+
current_aid = functions.get_agent_id()
|
| 413 |
+
if current_aid:
|
| 414 |
+
if request_aid and request_aid != current_aid:
|
| 415 |
+
return jsonify({"error": "Unauthorized: Agent scope is isolated"}), 403
|
| 416 |
+
request_aid = current_aid
|
| 417 |
res = functions.folder_timeline(
|
| 418 |
kv,
|
| 419 |
limit=int(args.get("limit", 100)),
|
| 420 |
folder_path=args.get("folderPath"),
|
| 421 |
+
agent_id=request_aid,
|
| 422 |
before=args.get("before"),
|
| 423 |
after=args.get("after"),
|
| 424 |
)
|
src/routes/observations.py
CHANGED
|
@@ -57,7 +57,8 @@ def api_observe():
|
|
| 57 |
body = request.get_json(force=True) or {}
|
| 58 |
|
| 59 |
# Compat shim: accept both folder-based and legacy session-based payloads.
|
| 60 |
-
# Old clients send sessionId/project/cwd
|
|
|
|
| 61 |
folder_path = (
|
| 62 |
body.get("folderPath")
|
| 63 |
or body.get("cwd")
|
|
@@ -72,7 +73,22 @@ def api_observe():
|
|
| 72 |
or os.getenv("AGENT_ID")
|
| 73 |
or "agent"
|
| 74 |
)
|
|
|
|
|
|
|
| 75 |
text = body.get("text") or body.get("content") or ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
payload = {
|
| 78 |
"folderPath": folder_path,
|
|
@@ -89,9 +105,9 @@ def api_observe():
|
|
| 89 |
return jsonify(res), 201
|
| 90 |
except Exception as e:
|
| 91 |
import traceback
|
| 92 |
-
|
| 93 |
-
print(
|
| 94 |
-
return jsonify({"error": str(e), "detail": type(e).__name__}), 400
|
| 95 |
|
| 96 |
|
| 97 |
# ---------------------------------------------------------------------------
|
|
@@ -155,6 +171,10 @@ def api_folders():
|
|
| 155 |
key=lambda x: x.get("lastUpdated", ""),
|
| 156 |
reverse=True,
|
| 157 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
return jsonify({"folders": folders}), 200
|
| 159 |
|
| 160 |
|
|
@@ -171,6 +191,10 @@ def api_folder_observations():
|
|
| 171 |
aid = request.args.get("agentId")
|
| 172 |
if not fp or not aid:
|
| 173 |
return jsonify({"error": "folderPath and agentId are required"}), 400
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
observations = sorted(
|
| 175 |
_get_kv().list(KV.folder_obs(fp, aid)),
|
| 176 |
key=lambda x: x.get("timestamp", ""),
|
|
|
|
| 57 |
body = request.get_json(force=True) or {}
|
| 58 |
|
| 59 |
# Compat shim: accept both folder-based and legacy session-based payloads.
|
| 60 |
+
# Old clients send sessionId/project/cwd + data{tool_input/output}
|
| 61 |
+
# New clients send folderPath/agentId/text
|
| 62 |
folder_path = (
|
| 63 |
body.get("folderPath")
|
| 64 |
or body.get("cwd")
|
|
|
|
| 73 |
or os.getenv("AGENT_ID")
|
| 74 |
or "agent"
|
| 75 |
)
|
| 76 |
+
|
| 77 |
+
# Build text from whichever field the client used
|
| 78 |
text = body.get("text") or body.get("content") or ""
|
| 79 |
+
if not text:
|
| 80 |
+
# Legacy clients put content in a nested 'data' dict
|
| 81 |
+
data = body.get("data")
|
| 82 |
+
if isinstance(data, dict):
|
| 83 |
+
parts = [str(v) for k, v in data.items()
|
| 84 |
+
if v and k in ("tool_input", "tool_output", "prompt",
|
| 85 |
+
"response", "tool_name", "content")]
|
| 86 |
+
text = " | ".join(parts) if parts else str(data)
|
| 87 |
+
elif isinstance(data, str):
|
| 88 |
+
text = data
|
| 89 |
+
if not text:
|
| 90 |
+
# Last resort: use hookType as a minimal marker so we don't 400
|
| 91 |
+
text = body.get("hookType") or "observation"
|
| 92 |
|
| 93 |
payload = {
|
| 94 |
"folderPath": folder_path,
|
|
|
|
| 105 |
return jsonify(res), 201
|
| 106 |
except Exception as e:
|
| 107 |
import traceback
|
| 108 |
+
tb = traceback.format_exc()
|
| 109 |
+
print(f"[observe] 400 — keys={list(body.keys())} {type(e).__name__}: {e}\n{tb}")
|
| 110 |
+
return jsonify({"error": str(e), "detail": type(e).__name__, "keys": list(body.keys()), "tb": tb}), 400
|
| 111 |
|
| 112 |
|
| 113 |
# ---------------------------------------------------------------------------
|
|
|
|
| 171 |
key=lambda x: x.get("lastUpdated", ""),
|
| 172 |
reverse=True,
|
| 173 |
)
|
| 174 |
+
if functions.is_agent_scope_isolated():
|
| 175 |
+
aid = functions.get_agent_id()
|
| 176 |
+
if aid:
|
| 177 |
+
folders = [f for f in folders if f.get("agentId") == aid]
|
| 178 |
return jsonify({"folders": folders}), 200
|
| 179 |
|
| 180 |
|
|
|
|
| 191 |
aid = request.args.get("agentId")
|
| 192 |
if not fp or not aid:
|
| 193 |
return jsonify({"error": "folderPath and agentId are required"}), 400
|
| 194 |
+
if functions.is_agent_scope_isolated():
|
| 195 |
+
current_aid = functions.get_agent_id()
|
| 196 |
+
if current_aid and aid != current_aid:
|
| 197 |
+
return jsonify({"error": "Unauthorized: Agent scope is isolated to another agent"}), 403
|
| 198 |
observations = sorted(
|
| 199 |
_get_kv().list(KV.folder_obs(fp, aid)),
|
| 200 |
key=lambda x: x.get("timestamp", ""),
|
start.sh
CHANGED
|
@@ -60,3 +60,7 @@ export III_REST_PORT=7860
|
|
| 60 |
# Start Flask application in the foreground
|
| 61 |
echo "[start] Starting Flask application on port 7860..."
|
| 62 |
python3 src/app.py
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
# Start Flask application in the foreground
|
| 61 |
echo "[start] Starting Flask application on port 7860..."
|
| 62 |
python3 src/app.py
|
| 63 |
+
|
| 64 |
+
# Perform a final backup on shutdown
|
| 65 |
+
echo "[start] Flask application exited. Performing final backup..."
|
| 66 |
+
python3 /app/sync.py backup
|
sync.py
CHANGED
|
@@ -156,11 +156,28 @@ def _list_repo_prefix(api, prefix):
|
|
| 156 |
return []
|
| 157 |
|
| 158 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
def backup():
|
| 160 |
if not HF_TOKEN:
|
| 161 |
return
|
| 162 |
api = get_api()
|
| 163 |
|
|
|
|
|
|
|
|
|
|
| 164 |
targets = _collect_sync_targets()
|
| 165 |
if not targets:
|
| 166 |
print("[sync] nothing to backup")
|
|
@@ -225,7 +242,6 @@ def backup():
|
|
| 225 |
finally:
|
| 226 |
shutil.rmtree(staging, ignore_errors=True)
|
| 227 |
|
| 228 |
-
|
| 229 |
if __name__ == "__main__":
|
| 230 |
cmd = sys.argv[1] if len(sys.argv) > 1 else "backup"
|
| 231 |
if cmd == "restore":
|
|
@@ -235,9 +251,3 @@ if __name__ == "__main__":
|
|
| 235 |
else:
|
| 236 |
print(f"[sync] unknown command: {cmd}")
|
| 237 |
sys.exit(1)
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
HF_TOKEN = os.environ.get("HF_TOKEN", "")
|
| 241 |
-
REPO_ID = os.environ.get("AGENTMEMORY_DATASET_REPO", "Yash030/agentmemory-python-data")
|
| 242 |
-
DATA_DIR = os.path.expanduser("~/.agentmemory")
|
| 243 |
-
DB_PATH = os.path.join(DATA_DIR, "agentmemory.db")
|
|
|
|
| 156 |
return []
|
| 157 |
|
| 158 |
|
| 159 |
+
def _checkpoint_db():
|
| 160 |
+
"""Checkpoint the SQLite WAL file before backing up to ensure all data is in the main DB file."""
|
| 161 |
+
try:
|
| 162 |
+
if os.path.exists(DB_PATH):
|
| 163 |
+
conn = sqlite3.connect(DB_PATH, check_same_thread=False, timeout=10)
|
| 164 |
+
try:
|
| 165 |
+
conn.execute("PRAGMA wal_checkpoint(TRUNCATE)")
|
| 166 |
+
print("[sync] DB checkpoint complete (WAL merged)")
|
| 167 |
+
finally:
|
| 168 |
+
conn.close()
|
| 169 |
+
except Exception as e:
|
| 170 |
+
print(f"[sync] DB checkpoint failed: {e}")
|
| 171 |
+
|
| 172 |
+
|
| 173 |
def backup():
|
| 174 |
if not HF_TOKEN:
|
| 175 |
return
|
| 176 |
api = get_api()
|
| 177 |
|
| 178 |
+
# Checkpoint WAL changes to main DB file before backup
|
| 179 |
+
_checkpoint_db()
|
| 180 |
+
|
| 181 |
targets = _collect_sync_targets()
|
| 182 |
if not targets:
|
| 183 |
print("[sync] nothing to backup")
|
|
|
|
| 242 |
finally:
|
| 243 |
shutil.rmtree(staging, ignore_errors=True)
|
| 244 |
|
|
|
|
| 245 |
if __name__ == "__main__":
|
| 246 |
cmd = sys.argv[1] if len(sys.argv) > 1 else "backup"
|
| 247 |
if cmd == "restore":
|
|
|
|
| 251 |
else:
|
| 252 |
print(f"[sync] unknown command: {cmd}")
|
| 253 |
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_properties.py
CHANGED
|
@@ -76,7 +76,7 @@ def _safe_text():
|
|
| 76 |
# Two distinct (folderPath, agentId) pairs never share observations.
|
| 77 |
# ---------------------------------------------------------------------------
|
| 78 |
|
| 79 |
-
@settings(max_examples=50)
|
| 80 |
@given(
|
| 81 |
path1=_safe_path(),
|
| 82 |
agent1=_safe_agent(),
|
|
@@ -106,7 +106,7 @@ def test_property_1_pair_isolation(path1, agent1, path2, agent2, text):
|
|
| 106 |
# meta.obsCount == len(kv.list(folder_obs_scope))
|
| 107 |
# ---------------------------------------------------------------------------
|
| 108 |
|
| 109 |
-
@settings(max_examples=50)
|
| 110 |
@given(
|
| 111 |
path=_safe_path(),
|
| 112 |
agent=_safe_agent(),
|
|
@@ -132,7 +132,7 @@ def test_property_2_obs_count_consistency(path, agent, texts):
|
|
| 132 |
# Every written pair has a KV.folders entry.
|
| 133 |
# ---------------------------------------------------------------------------
|
| 134 |
|
| 135 |
-
@settings(max_examples=50)
|
| 136 |
@given(
|
| 137 |
path=_safe_path(),
|
| 138 |
agent=_safe_agent(),
|
|
@@ -160,7 +160,7 @@ def test_property_3_index_coverage(path, agent, text):
|
|
| 160 |
# No stored obs text contains raw secrets after folder_observe().
|
| 161 |
# ---------------------------------------------------------------------------
|
| 162 |
|
| 163 |
-
@settings(max_examples=30)
|
| 164 |
@given(
|
| 165 |
path=_safe_path(),
|
| 166 |
agent=_safe_agent(),
|
|
@@ -190,7 +190,7 @@ def test_property_4_privacy_invariant(path, agent, prefix):
|
|
| 190 |
# folder_timeline() always returns results sorted newest-first.
|
| 191 |
# ---------------------------------------------------------------------------
|
| 192 |
|
| 193 |
-
@settings(max_examples=40)
|
| 194 |
@given(
|
| 195 |
path=_safe_path(),
|
| 196 |
agent=_safe_agent(),
|
|
@@ -220,7 +220,7 @@ def test_property_5_timeline_ordering(path, agent, n):
|
|
| 220 |
# After forget({folderPath, agentId}), all three scopes are empty.
|
| 221 |
# ---------------------------------------------------------------------------
|
| 222 |
|
| 223 |
-
@settings(max_examples=40)
|
| 224 |
@given(
|
| 225 |
path=_safe_path(),
|
| 226 |
agent=_safe_agent(),
|
|
@@ -251,7 +251,7 @@ def test_property_6_forget_completeness(path, agent, texts):
|
|
| 251 |
# Superseded memories have parentId; at least one memory is always latest.
|
| 252 |
# ---------------------------------------------------------------------------
|
| 253 |
|
| 254 |
-
@settings(max_examples=30, suppress_health_check=[HealthCheck.filter_too_much])
|
| 255 |
@given(
|
| 256 |
base_content=st.text(
|
| 257 |
alphabet="abcdefghijklmnopqrstuvwxyz ",
|
|
@@ -292,7 +292,7 @@ def test_property_7_memory_version_uniqueness(base_content, n_variants):
|
|
| 292 |
# normalize(normalize(p)) == normalize(p) for all valid inputs.
|
| 293 |
# ---------------------------------------------------------------------------
|
| 294 |
|
| 295 |
-
@settings(max_examples=100)
|
| 296 |
@given(
|
| 297 |
path=st.text(
|
| 298 |
alphabet="abcdefghijklmnopqrstuvwxyz0123456789/_-.",
|
|
|
|
| 76 |
# Two distinct (folderPath, agentId) pairs never share observations.
|
| 77 |
# ---------------------------------------------------------------------------
|
| 78 |
|
| 79 |
+
@settings(max_examples=50, deadline=None)
|
| 80 |
@given(
|
| 81 |
path1=_safe_path(),
|
| 82 |
agent1=_safe_agent(),
|
|
|
|
| 106 |
# meta.obsCount == len(kv.list(folder_obs_scope))
|
| 107 |
# ---------------------------------------------------------------------------
|
| 108 |
|
| 109 |
+
@settings(max_examples=50, deadline=None)
|
| 110 |
@given(
|
| 111 |
path=_safe_path(),
|
| 112 |
agent=_safe_agent(),
|
|
|
|
| 132 |
# Every written pair has a KV.folders entry.
|
| 133 |
# ---------------------------------------------------------------------------
|
| 134 |
|
| 135 |
+
@settings(max_examples=50, deadline=None)
|
| 136 |
@given(
|
| 137 |
path=_safe_path(),
|
| 138 |
agent=_safe_agent(),
|
|
|
|
| 160 |
# No stored obs text contains raw secrets after folder_observe().
|
| 161 |
# ---------------------------------------------------------------------------
|
| 162 |
|
| 163 |
+
@settings(max_examples=30, deadline=None)
|
| 164 |
@given(
|
| 165 |
path=_safe_path(),
|
| 166 |
agent=_safe_agent(),
|
|
|
|
| 190 |
# folder_timeline() always returns results sorted newest-first.
|
| 191 |
# ---------------------------------------------------------------------------
|
| 192 |
|
| 193 |
+
@settings(max_examples=40, deadline=None)
|
| 194 |
@given(
|
| 195 |
path=_safe_path(),
|
| 196 |
agent=_safe_agent(),
|
|
|
|
| 220 |
# After forget({folderPath, agentId}), all three scopes are empty.
|
| 221 |
# ---------------------------------------------------------------------------
|
| 222 |
|
| 223 |
+
@settings(max_examples=40, deadline=None)
|
| 224 |
@given(
|
| 225 |
path=_safe_path(),
|
| 226 |
agent=_safe_agent(),
|
|
|
|
| 251 |
# Superseded memories have parentId; at least one memory is always latest.
|
| 252 |
# ---------------------------------------------------------------------------
|
| 253 |
|
| 254 |
+
@settings(max_examples=30, suppress_health_check=[HealthCheck.filter_too_much], deadline=None)
|
| 255 |
@given(
|
| 256 |
base_content=st.text(
|
| 257 |
alphabet="abcdefghijklmnopqrstuvwxyz ",
|
|
|
|
| 292 |
# normalize(normalize(p)) == normalize(p) for all valid inputs.
|
| 293 |
# ---------------------------------------------------------------------------
|
| 294 |
|
| 295 |
+
@settings(max_examples=100, deadline=None)
|
| 296 |
@given(
|
| 297 |
path=st.text(
|
| 298 |
alphabet="abcdefghijklmnopqrstuvwxyz0123456789/_-.",
|