nexo-brain 3.1.6 → 3.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/src/auto_update.py +2 -0
- package/src/dashboard/app.py +66 -2
- package/src/dashboard/templates/memory.html +102 -0
- package/src/dashboard/templates/operations.html +43 -8
- package/src/db/__init__.py +82 -0
- package/src/db/_hot_context.py +660 -0
- package/src/db/_learnings.py +20 -13
- package/src/db/_reminders.py +245 -2
- package/src/db/_schema.py +50 -0
- package/src/plugins/protocol.py +59 -0
- package/src/server.py +74 -1
- package/src/tools_hot_context.py +163 -0
- package/src/tools_sessions.py +77 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.8",
|
|
4
4
|
"description": "Local cognitive runtime for Claude Code \u2014 persistent memory, overnight learning, doctor diagnostics, personal scripts, recovery-aware jobs, startup preflight, and optional dashboard/power helper.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "NEXO Brain",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.8",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO Brain — Shared brain for AI agents. Persistent memory, semantic RAG, natural forgetting, metacognitive guard, trust scoring, 150+ MCP tools. Works with Claude Code, Codex, Claude Desktop & any MCP client. 100% local, free.",
|
|
6
6
|
"homepage": "https://nexo-brain.com",
|
package/src/auto_update.py
CHANGED
|
@@ -1200,6 +1200,7 @@ def _backup_runtime_tree(dest: Path = NEXO_HOME) -> str:
|
|
|
1200
1200
|
"client_preferences.py", "agent_runner.py", "bootstrap_docs.py",
|
|
1201
1201
|
"hook_guardrails.py", "protocol_settings.py", "public_evolution_queue.py",
|
|
1202
1202
|
"auto_update.py", "tools_sessions.py", "tools_coordination.py",
|
|
1203
|
+
"tools_hot_context.py",
|
|
1203
1204
|
"tools_reminders.py", "tools_reminders_crud.py", "tools_learnings.py",
|
|
1204
1205
|
"tools_credentials.py", "tools_task_history.py", "tools_menu.py",
|
|
1205
1206
|
"cli.py", "script_registry.py", "skills_runtime.py", "user_context.py",
|
|
@@ -1251,6 +1252,7 @@ def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_H
|
|
|
1251
1252
|
"client_preferences.py", "agent_runner.py", "bootstrap_docs.py",
|
|
1252
1253
|
"hook_guardrails.py", "protocol_settings.py", "public_evolution_queue.py",
|
|
1253
1254
|
"auto_update.py", "tools_sessions.py", "tools_coordination.py",
|
|
1255
|
+
"tools_hot_context.py",
|
|
1254
1256
|
"tools_reminders.py", "tools_reminders_crud.py", "tools_learnings.py",
|
|
1255
1257
|
"tools_credentials.py", "tools_task_history.py", "tools_menu.py",
|
|
1256
1258
|
"cli.py", "script_registry.py", "skills_runtime.py", "user_context.py",
|
package/src/dashboard/app.py
CHANGED
|
@@ -106,6 +106,7 @@ class ReminderUpdate(BaseModel):
|
|
|
106
106
|
date: Optional[str] = None
|
|
107
107
|
status: Optional[str] = None
|
|
108
108
|
category: Optional[str] = None
|
|
109
|
+
read_token: Optional[str] = None
|
|
109
110
|
|
|
110
111
|
class FollowupCreate(BaseModel):
|
|
111
112
|
description: str
|
|
@@ -119,10 +120,12 @@ class FollowupUpdate(BaseModel):
|
|
|
119
120
|
status: Optional[str] = None
|
|
120
121
|
verification: Optional[str] = None
|
|
121
122
|
reasoning: Optional[str] = None
|
|
123
|
+
read_token: Optional[str] = None
|
|
122
124
|
|
|
123
125
|
class MoveRequest(BaseModel):
|
|
124
126
|
id: str
|
|
125
127
|
direction: str # "to_followup" | "to_reminder"
|
|
128
|
+
read_token: Optional[str] = None
|
|
126
129
|
|
|
127
130
|
class InboxCreate(BaseModel):
|
|
128
131
|
direction: str # "to_nexo" | "to_user"
|
|
@@ -181,6 +184,22 @@ def _dashboard_status_matches(status: object, requested: str | None) -> bool:
|
|
|
181
184
|
return normalized == requested_key.upper()
|
|
182
185
|
|
|
183
186
|
|
|
187
|
+
def _require_dashboard_item_read(item_type: str, item_id: str, read_token: str | None):
|
|
188
|
+
db = _db()
|
|
189
|
+
ok, message = db.validate_item_read_token(read_token or "", item_type, item_id)
|
|
190
|
+
if ok:
|
|
191
|
+
return None
|
|
192
|
+
prefix = "followup" if item_type == "followup" else "reminder"
|
|
193
|
+
return JSONResponse(
|
|
194
|
+
{
|
|
195
|
+
"error": f"{message} Read /api/{prefix}s/{item_id} first and reuse its read_token.",
|
|
196
|
+
"item_type": item_type,
|
|
197
|
+
"item_id": item_id,
|
|
198
|
+
},
|
|
199
|
+
status_code=409,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
184
203
|
def _latest_periodic_summary(kind: str) -> dict:
|
|
185
204
|
root = _deep_sleep_dir()
|
|
186
205
|
pattern = f"*-{kind}-summary.json"
|
|
@@ -807,6 +826,9 @@ async def api_reminders_update(rid: str, body: ReminderUpdate):
|
|
|
807
826
|
row = db.get_reminder(rid)
|
|
808
827
|
if not row:
|
|
809
828
|
return JSONResponse({"error": f"Reminder {rid} not found"}, status_code=404)
|
|
829
|
+
read_error = _require_dashboard_item_read("reminder", rid, body.read_token)
|
|
830
|
+
if read_error:
|
|
831
|
+
return read_error
|
|
810
832
|
fields = {}
|
|
811
833
|
if body.description is not None:
|
|
812
834
|
fields["description"] = body.description
|
|
@@ -825,12 +847,15 @@ async def api_reminders_update(rid: str, body: ReminderUpdate):
|
|
|
825
847
|
|
|
826
848
|
|
|
827
849
|
@app.delete("/api/reminders/{rid}")
|
|
828
|
-
async def api_reminders_delete(rid: str):
|
|
850
|
+
async def api_reminders_delete(rid: str, read_token: str = Query("", description="Read token from GET /api/reminders/{rid}")):
|
|
829
851
|
"""Soft-delete a reminder."""
|
|
830
852
|
db = _db()
|
|
831
853
|
row = db.get_reminder(rid)
|
|
832
854
|
if not row:
|
|
833
855
|
return JSONResponse({"error": f"Reminder {rid} not found"}, status_code=404)
|
|
856
|
+
read_error = _require_dashboard_item_read("reminder", rid, read_token)
|
|
857
|
+
if read_error:
|
|
858
|
+
return read_error
|
|
834
859
|
db.add_reminder_note(rid, "Soft-deleted from dashboard.", actor="dashboard")
|
|
835
860
|
db.delete_reminder(rid)
|
|
836
861
|
return {"success": True, "deleted_id": rid}
|
|
@@ -902,6 +927,9 @@ async def api_followups_update(fid: str, body: FollowupUpdate):
|
|
|
902
927
|
row = db.get_followup(fid)
|
|
903
928
|
if not row:
|
|
904
929
|
return JSONResponse({"error": f"Followup {fid} not found"}, status_code=404)
|
|
930
|
+
read_error = _require_dashboard_item_read("followup", fid, body.read_token)
|
|
931
|
+
if read_error:
|
|
932
|
+
return read_error
|
|
905
933
|
fields = {}
|
|
906
934
|
if body.description is not None:
|
|
907
935
|
fields["description"] = body.description
|
|
@@ -922,12 +950,15 @@ async def api_followups_update(fid: str, body: FollowupUpdate):
|
|
|
922
950
|
|
|
923
951
|
|
|
924
952
|
@app.delete("/api/followups/{fid}")
|
|
925
|
-
async def api_followups_delete(fid: str):
|
|
953
|
+
async def api_followups_delete(fid: str, read_token: str = Query("", description="Read token from GET /api/followups/{fid}")):
|
|
926
954
|
"""Soft-delete a followup."""
|
|
927
955
|
db = _db()
|
|
928
956
|
row = db.get_followup(fid)
|
|
929
957
|
if not row:
|
|
930
958
|
return JSONResponse({"error": f"Followup {fid} not found"}, status_code=404)
|
|
959
|
+
read_error = _require_dashboard_item_read("followup", fid, read_token)
|
|
960
|
+
if read_error:
|
|
961
|
+
return read_error
|
|
931
962
|
db.add_followup_note(fid, "Soft-deleted from dashboard.", actor="dashboard")
|
|
932
963
|
db.delete_followup(fid)
|
|
933
964
|
return {"success": True, "deleted_id": fid}
|
|
@@ -947,6 +978,9 @@ async def api_ops_move(body: MoveRequest):
|
|
|
947
978
|
item = db.get_reminder(body.id)
|
|
948
979
|
if not item:
|
|
949
980
|
return JSONResponse({"error": f"Reminder {body.id} not found"}, status_code=404)
|
|
981
|
+
read_error = _require_dashboard_item_read("reminder", body.id, body.read_token)
|
|
982
|
+
if read_error:
|
|
983
|
+
return read_error
|
|
950
984
|
fid = _next_followup_id(conn)
|
|
951
985
|
created = db.create_followup(
|
|
952
986
|
fid,
|
|
@@ -965,6 +999,9 @@ async def api_ops_move(body: MoveRequest):
|
|
|
965
999
|
item = db.get_followup(body.id)
|
|
966
1000
|
if not item:
|
|
967
1001
|
return JSONResponse({"error": f"Followup {body.id} not found"}, status_code=404)
|
|
1002
|
+
read_error = _require_dashboard_item_read("followup", body.id, body.read_token)
|
|
1003
|
+
if read_error:
|
|
1004
|
+
return read_error
|
|
968
1005
|
rid = _next_reminder_id(conn)
|
|
969
1006
|
created = db.create_reminder(
|
|
970
1007
|
rid,
|
|
@@ -1388,6 +1425,33 @@ async def api_memory_flow():
|
|
|
1388
1425
|
"stm_recent": stm_recent, "ltm_recent": ltm_recent, "quarantine": quarantine}
|
|
1389
1426
|
|
|
1390
1427
|
|
|
1428
|
+
@app.get("/api/recent-context")
|
|
1429
|
+
async def api_recent_context(
|
|
1430
|
+
query: str = Query("", description="Optional search query for hot context"),
|
|
1431
|
+
hours: int = Query(24, ge=1, le=168, description="How many recent hours to inspect"),
|
|
1432
|
+
limit: int = Query(8, ge=1, le=25, description="Max contexts/events to return"),
|
|
1433
|
+
):
|
|
1434
|
+
"""Expose recent hot context and event timeline for the last N hours."""
|
|
1435
|
+
db = _db()
|
|
1436
|
+
bundle = db.build_pre_action_context(query=query, hours=hours, limit=limit)
|
|
1437
|
+
return {
|
|
1438
|
+
"query": bundle.get("query") or "",
|
|
1439
|
+
"hours": bundle.get("hours") or hours,
|
|
1440
|
+
"has_matches": bool(bundle.get("has_matches")),
|
|
1441
|
+
"counts": {
|
|
1442
|
+
"contexts": len(bundle.get("contexts") or []),
|
|
1443
|
+
"events": len(bundle.get("events") or []),
|
|
1444
|
+
"reminders": len(bundle.get("reminders") or []),
|
|
1445
|
+
"followups": len(bundle.get("followups") or []),
|
|
1446
|
+
},
|
|
1447
|
+
"contexts": bundle.get("contexts") or [],
|
|
1448
|
+
"events": bundle.get("events") or [],
|
|
1449
|
+
"reminders": bundle.get("reminders") or [],
|
|
1450
|
+
"followups": bundle.get("followups") or [],
|
|
1451
|
+
"excerpt": db.format_pre_action_context_bundle(bundle, compact=True) if bundle.get("has_matches") else "No recent context.",
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
|
|
1391
1455
|
# ---------------------------------------------------------------------------
|
|
1392
1456
|
# Dream Journal
|
|
1393
1457
|
# ---------------------------------------------------------------------------
|
|
@@ -126,6 +126,47 @@
|
|
|
126
126
|
</div>
|
|
127
127
|
</div>
|
|
128
128
|
|
|
129
|
+
<!-- Hot Context -->
|
|
130
|
+
<div class="mb-6">
|
|
131
|
+
<div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between mb-3">
|
|
132
|
+
<div>
|
|
133
|
+
<h2 class="text-xs uppercase tracking-wider text-slate-500 font-semibold flex items-center gap-2">
|
|
134
|
+
<span class="w-2 h-2 rounded-full bg-emerald-400 glow-dot" style="color:#34d399"></span>
|
|
135
|
+
Hot Context 24h
|
|
136
|
+
</h2>
|
|
137
|
+
<p class="text-xs text-slate-600 mt-1">Recent operational memory shared across sessions, clients, and channels.</p>
|
|
138
|
+
</div>
|
|
139
|
+
<div class="flex items-center gap-2">
|
|
140
|
+
<input type="text" id="context-query" placeholder="Search recent context..."
|
|
141
|
+
class="bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-xs text-slate-200 placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-emerald-500 w-64"
|
|
142
|
+
onkeydown="if(event.key==='Enter')loadRecentContext()">
|
|
143
|
+
<button onclick="loadRecentContext()" class="px-3 py-1.5 text-xs bg-emerald-600 text-white rounded-lg hover:bg-emerald-500 transition-colors font-medium">Refresh</button>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<div class="grid grid-cols-1 xl:grid-cols-2 gap-5">
|
|
148
|
+
<div class="bg-emerald-500/5 border border-emerald-500/20 rounded-xl p-4">
|
|
149
|
+
<div class="flex items-center justify-between mb-3">
|
|
150
|
+
<div class="text-xs uppercase tracking-wider text-emerald-400 font-semibold">Active Contexts</div>
|
|
151
|
+
<div class="text-[10px] text-slate-500 font-mono" id="hot-context-count">--</div>
|
|
152
|
+
</div>
|
|
153
|
+
<div class="space-y-2" id="hot-context-list">
|
|
154
|
+
<div class="text-xs text-slate-600 text-center py-6">Loading...</div>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<div class="bg-emerald-500/5 border border-emerald-500/20 rounded-xl p-4">
|
|
159
|
+
<div class="flex items-center justify-between mb-3">
|
|
160
|
+
<div class="text-xs uppercase tracking-wider text-emerald-400 font-semibold">Recent Events</div>
|
|
161
|
+
<div class="text-[10px] text-slate-500 font-mono" id="hot-event-count">--</div>
|
|
162
|
+
</div>
|
|
163
|
+
<div class="space-y-2" id="hot-event-list">
|
|
164
|
+
<div class="text-xs text-slate-600 text-center py-6">Loading...</div>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
129
170
|
<!-- Two-column: STM and LTM Recent -->
|
|
130
171
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5 mb-6" id="columns-area">
|
|
131
172
|
<!-- Recent STM -->
|
|
@@ -232,6 +273,65 @@
|
|
|
232
273
|
</div>`;
|
|
233
274
|
}
|
|
234
275
|
|
|
276
|
+
function recentTime(value) {
|
|
277
|
+
if (value === null || value === undefined || value === '') return '--';
|
|
278
|
+
if (typeof value === 'number') return relativeTime(new Date(value * 1000).toISOString());
|
|
279
|
+
if (/^\d+(\.\d+)?$/.test(String(value))) return relativeTime(new Date(parseFloat(value) * 1000).toISOString());
|
|
280
|
+
return relativeTime(value);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function renderHotContextCard(item) {
|
|
284
|
+
const title = item.title || item.context_key || '(untitled)';
|
|
285
|
+
const summary = item.summary || '';
|
|
286
|
+
const state = item.state || 'active';
|
|
287
|
+
const owner = item.owner || '';
|
|
288
|
+
const lastEvent = item.last_event_at ? recentTime(item.last_event_at) : '';
|
|
289
|
+
return `<div class="bg-slate-900/50 border border-emerald-500/15 rounded-xl p-4">
|
|
290
|
+
<div class="flex items-center gap-2 mb-2 flex-wrap">
|
|
291
|
+
<span class="text-[10px] px-1.5 py-0.5 rounded bg-emerald-500/10 text-emerald-400 font-medium">${escapeHtml(state)}</span>
|
|
292
|
+
${owner ? `<span class="text-[10px] px-1.5 py-0.5 rounded bg-slate-800 text-slate-500">${escapeHtml(owner)}</span>` : ''}
|
|
293
|
+
<span class="text-[10px] text-slate-600 ml-auto font-mono">${escapeHtml(lastEvent)}</span>
|
|
294
|
+
</div>
|
|
295
|
+
<div class="text-sm text-slate-200 leading-relaxed">${escapeHtml(title)}</div>
|
|
296
|
+
${summary ? `<div class="text-xs text-slate-400 leading-relaxed mt-2">${escapeHtml(summary)}</div>` : ''}
|
|
297
|
+
<div class="text-[10px] text-slate-600 font-mono mt-2">${escapeHtml(item.context_key || '')}</div>
|
|
298
|
+
</div>`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function renderHotEventCard(event) {
|
|
302
|
+
const title = event.title || event.context_key || '(event)';
|
|
303
|
+
const summary = event.summary || event.body || '';
|
|
304
|
+
const created = event.created_at ? recentTime(event.created_at) : '';
|
|
305
|
+
return `<div class="bg-slate-900/50 border border-emerald-500/15 rounded-xl p-4">
|
|
306
|
+
<div class="flex items-center gap-2 mb-2 flex-wrap">
|
|
307
|
+
<span class="text-[10px] px-1.5 py-0.5 rounded bg-emerald-500/10 text-emerald-400 font-medium">${escapeHtml(event.event_type || 'event')}</span>
|
|
308
|
+
<span class="text-[10px] text-slate-600 ml-auto font-mono">${escapeHtml(created)}</span>
|
|
309
|
+
</div>
|
|
310
|
+
<div class="text-sm text-slate-200 leading-relaxed">${escapeHtml(title)}</div>
|
|
311
|
+
${summary ? `<div class="text-xs text-slate-400 leading-relaxed mt-2 whitespace-pre-wrap">${escapeHtml(summary)}</div>` : ''}
|
|
312
|
+
<div class="text-[10px] text-slate-600 font-mono mt-2">${escapeHtml(event.context_key || '')}</div>
|
|
313
|
+
</div>`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function loadRecentContext() {
|
|
317
|
+
const q = document.getElementById('context-query').value.trim();
|
|
318
|
+
const url = '/api/recent-context?hours=24&limit=8' + (q ? `&query=${encodeURIComponent(q)}` : '');
|
|
319
|
+
const data = await fetchJSON(url);
|
|
320
|
+
if (!data) return;
|
|
321
|
+
|
|
322
|
+
document.getElementById('hot-context-count').textContent = formatNumber((data.counts && data.counts.contexts) || 0);
|
|
323
|
+
document.getElementById('hot-event-count').textContent = formatNumber((data.counts && data.counts.events) || 0);
|
|
324
|
+
|
|
325
|
+
const contexts = data.contexts || [];
|
|
326
|
+
const events = data.events || [];
|
|
327
|
+
document.getElementById('hot-context-list').innerHTML = contexts.length === 0
|
|
328
|
+
? '<div class="text-xs text-slate-600 text-center py-6">No active hot context in the last 24h</div>'
|
|
329
|
+
: contexts.map(renderHotContextCard).join('');
|
|
330
|
+
document.getElementById('hot-event-list').innerHTML = events.length === 0
|
|
331
|
+
? '<div class="text-xs text-slate-600 text-center py-6">No recent events in the last 24h</div>'
|
|
332
|
+
: events.map(renderHotEventCard).join('');
|
|
333
|
+
}
|
|
334
|
+
|
|
235
335
|
async function loadFlow() {
|
|
236
336
|
const data = await fetchJSON('/api/memory/flow');
|
|
237
337
|
if (!data) return;
|
|
@@ -312,7 +412,9 @@
|
|
|
312
412
|
}
|
|
313
413
|
|
|
314
414
|
// Init
|
|
415
|
+
loadRecentContext();
|
|
315
416
|
loadFlow();
|
|
417
|
+
setInterval(loadRecentContext, REFRESH_MS);
|
|
316
418
|
setInterval(loadFlow, REFRESH_MS);
|
|
317
419
|
</script>
|
|
318
420
|
{% endblock %}
|
|
@@ -129,6 +129,7 @@
|
|
|
129
129
|
</div>
|
|
130
130
|
<input type="hidden" name="type" id="modal-type">
|
|
131
131
|
<input type="hidden" name="edit_id" id="modal-edit-id">
|
|
132
|
+
<input type="hidden" name="read_token" id="modal-read-token">
|
|
132
133
|
<div class="flex items-center justify-end gap-2 pt-1">
|
|
133
134
|
<button type="button" onclick="closeModal()" class="text-xs px-3 py-1.5 rounded-lg bg-slate-800 text-slate-400 hover:bg-slate-700 hover:text-slate-200 transition-colors">
|
|
134
135
|
Cancel
|
|
@@ -229,6 +230,24 @@ function opsRelativeDate(dateStr) {
|
|
|
229
230
|
return d.toLocaleDateString('en', { month: 'short', day: 'numeric' });
|
|
230
231
|
}
|
|
231
232
|
|
|
233
|
+
async function fetchItemDetail(id, type) {
|
|
234
|
+
const url = type === 'reminder' ? '/api/reminders/' + id : '/api/followups/' + id;
|
|
235
|
+
const data = await fetchJSON(url);
|
|
236
|
+
if (!data || !data.success) {
|
|
237
|
+
throw new Error((data && (data.error || data.detail)) || 'Failed to read item history');
|
|
238
|
+
}
|
|
239
|
+
return type === 'reminder' ? data.reminder : data.followup;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function fetchReadToken(id, type) {
|
|
243
|
+
const item = await fetchItemDetail(id, type);
|
|
244
|
+
const token = item.read_token;
|
|
245
|
+
if (!token) {
|
|
246
|
+
throw new Error('Missing read token for ' + id);
|
|
247
|
+
}
|
|
248
|
+
return { item, token };
|
|
249
|
+
}
|
|
250
|
+
|
|
232
251
|
// -----------------------------------------------------------------------
|
|
233
252
|
// Grouping
|
|
234
253
|
// -----------------------------------------------------------------------
|
|
@@ -373,11 +392,12 @@ function filterOps() {
|
|
|
373
392
|
// -----------------------------------------------------------------------
|
|
374
393
|
async function completeItem(id, type) {
|
|
375
394
|
try {
|
|
395
|
+
const { token } = await fetchReadToken(id, type);
|
|
376
396
|
const url = type === 'reminder' ? '/api/reminders/' + id : '/api/followups/' + id;
|
|
377
397
|
const res = await fetch(url, {
|
|
378
398
|
method: 'PUT',
|
|
379
399
|
headers: { 'Content-Type': 'application/json' },
|
|
380
|
-
body: JSON.stringify({ status: 'COMPLETED' })
|
|
400
|
+
body: JSON.stringify({ status: 'COMPLETED', read_token: token })
|
|
381
401
|
});
|
|
382
402
|
const data = await res.json();
|
|
383
403
|
if (data.success) {
|
|
@@ -393,10 +413,12 @@ async function completeItem(id, type) {
|
|
|
393
413
|
|
|
394
414
|
async function moveItem(id, direction) {
|
|
395
415
|
try {
|
|
416
|
+
const type = direction === 'to_followup' ? 'reminder' : 'followup';
|
|
417
|
+
const { token } = await fetchReadToken(id, type);
|
|
396
418
|
const res = await fetch('/api/ops/move', {
|
|
397
419
|
method: 'POST',
|
|
398
420
|
headers: { 'Content-Type': 'application/json' },
|
|
399
|
-
body: JSON.stringify({ id, direction })
|
|
421
|
+
body: JSON.stringify({ id, direction, read_token: token })
|
|
400
422
|
});
|
|
401
423
|
const data = await res.json();
|
|
402
424
|
if (data.success) {
|
|
@@ -428,7 +450,8 @@ function deleteItem(id, type) {
|
|
|
428
450
|
pendingConfirmAction = async () => {
|
|
429
451
|
try {
|
|
430
452
|
const url = type === 'reminder' ? '/api/reminders/' + id : '/api/followups/' + id;
|
|
431
|
-
const
|
|
453
|
+
const { token } = await fetchReadToken(id, type);
|
|
454
|
+
const res = await fetch(url + '?read_token=' + encodeURIComponent(token), { method: 'DELETE' });
|
|
432
455
|
const data = await res.json();
|
|
433
456
|
if (data.success) {
|
|
434
457
|
opsToast('Marked ' + id + ' as deleted');
|
|
@@ -462,6 +485,7 @@ function openCreate(type) {
|
|
|
462
485
|
form.reset();
|
|
463
486
|
document.getElementById('modal-type').value = type;
|
|
464
487
|
document.getElementById('modal-edit-id').value = '';
|
|
488
|
+
document.getElementById('modal-read-token').value = '';
|
|
465
489
|
document.getElementById('modal-title').textContent = type === 'reminder' ? 'New Reminder' : 'New Followup';
|
|
466
490
|
document.getElementById('modal-submit-btn').textContent = 'Create';
|
|
467
491
|
document.getElementById('category-group').classList.toggle('hidden', type !== 'reminder');
|
|
@@ -470,17 +494,23 @@ function openCreate(type) {
|
|
|
470
494
|
document.getElementById('ops-modal').classList.remove('hidden');
|
|
471
495
|
}
|
|
472
496
|
|
|
473
|
-
function editItem(id, type) {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
497
|
+
async function editItem(id, type) {
|
|
498
|
+
let item;
|
|
499
|
+
try {
|
|
500
|
+
const detail = await fetchReadToken(id, type);
|
|
501
|
+
item = detail.item;
|
|
502
|
+
document.getElementById('modal-read-token').value = detail.token;
|
|
503
|
+
} catch (err) {
|
|
504
|
+
opsToast('Read history failed: ' + err.message, 'error');
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
478
507
|
const form = document.getElementById('modal-form');
|
|
479
508
|
form.reset();
|
|
480
509
|
document.getElementById('modal-type').value = type;
|
|
481
510
|
document.getElementById('modal-edit-id').value = id;
|
|
482
511
|
document.getElementById('modal-title').textContent = 'Edit ' + id;
|
|
483
512
|
document.getElementById('modal-submit-btn').textContent = 'Save';
|
|
513
|
+
document.getElementById('modal-read-token').value = item.read_token || '';
|
|
484
514
|
document.getElementById('category-group').classList.toggle('hidden', type !== 'reminder');
|
|
485
515
|
document.getElementById('verification-group').classList.toggle('hidden', type !== 'followup');
|
|
486
516
|
document.getElementById('reasoning-group').classList.toggle('hidden', type !== 'followup');
|
|
@@ -500,6 +530,7 @@ function editItem(id, type) {
|
|
|
500
530
|
|
|
501
531
|
function closeModal() {
|
|
502
532
|
document.getElementById('ops-modal').classList.add('hidden');
|
|
533
|
+
document.getElementById('modal-read-token').value = '';
|
|
503
534
|
}
|
|
504
535
|
|
|
505
536
|
async function submitForm(e) {
|
|
@@ -509,6 +540,7 @@ async function submitForm(e) {
|
|
|
509
540
|
const type = fd.get('type');
|
|
510
541
|
const editId = fd.get('edit_id');
|
|
511
542
|
const isEdit = !!editId;
|
|
543
|
+
const readToken = fd.get('read_token');
|
|
512
544
|
|
|
513
545
|
let url, method, body;
|
|
514
546
|
|
|
@@ -530,6 +562,9 @@ async function submitForm(e) {
|
|
|
530
562
|
reasoning: fd.get('reasoning') || null
|
|
531
563
|
};
|
|
532
564
|
}
|
|
565
|
+
if (isEdit) {
|
|
566
|
+
body.read_token = readToken || '';
|
|
567
|
+
}
|
|
533
568
|
|
|
534
569
|
try {
|
|
535
570
|
const res = await fetch(url, {
|
package/src/db/__init__.py
CHANGED
|
@@ -3,8 +3,55 @@
|
|
|
3
3
|
This package replaces the monolithic db.py. All public functions are
|
|
4
4
|
re-exported here for full backwards compatibility:
|
|
5
5
|
from db import get_db, create_learning, ...
|
|
6
|
+
|
|
7
|
+
Important:
|
|
8
|
+
`importlib.reload(db)` must also refresh the concrete submodules. The test
|
|
9
|
+
suite and several runtime repair flows rely on switching database paths or
|
|
10
|
+
runtime roots mid-process. If the package only re-exported functions from
|
|
11
|
+
already-imported submodules, those callables would keep pointing at stale
|
|
12
|
+
module state (especially old `db._core` connection globals).
|
|
6
13
|
"""
|
|
7
14
|
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import importlib
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _load_submodule(name: str):
|
|
22
|
+
"""Import or reload a db submodule and expose it on the package."""
|
|
23
|
+
module = sys.modules.get(name)
|
|
24
|
+
if module is None:
|
|
25
|
+
return importlib.import_module(name)
|
|
26
|
+
return importlib.reload(module)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _module(name: str):
|
|
30
|
+
module = sys.modules.get(name)
|
|
31
|
+
if module is None:
|
|
32
|
+
module = importlib.import_module(name)
|
|
33
|
+
return module
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
_core = _load_submodule("db._core")
|
|
37
|
+
_fts = _load_submodule("db._fts")
|
|
38
|
+
_schema = _load_submodule("db._schema")
|
|
39
|
+
_sessions = _load_submodule("db._sessions")
|
|
40
|
+
_reminders = _load_submodule("db._reminders")
|
|
41
|
+
_learnings = _load_submodule("db._learnings")
|
|
42
|
+
_credentials = _load_submodule("db._credentials")
|
|
43
|
+
_tasks = _load_submodule("db._tasks")
|
|
44
|
+
_entities = _load_submodule("db._entities")
|
|
45
|
+
_episodic = _load_submodule("db._episodic")
|
|
46
|
+
_evolution = _load_submodule("db._evolution")
|
|
47
|
+
_cron_runs = _load_submodule("db._cron_runs")
|
|
48
|
+
_protocol = _load_submodule("db._protocol")
|
|
49
|
+
_workflow = _load_submodule("db._workflow")
|
|
50
|
+
_watchers = _load_submodule("db._watchers")
|
|
51
|
+
_personal_scripts = _load_submodule("db._personal_scripts")
|
|
52
|
+
_skills = _load_submodule("db._skills")
|
|
53
|
+
_hot_context = _load_submodule("db._hot_context")
|
|
54
|
+
|
|
8
55
|
# Core: connection, constants, init, utils
|
|
9
56
|
from db._core import (
|
|
10
57
|
DB_PATH, SESSION_STALE_SECONDS, MESSAGE_TTL_SECONDS, QUESTION_TTL_SECONDS,
|
|
@@ -139,3 +186,38 @@ from db._skills import (
|
|
|
139
186
|
collect_skill_improvement_candidates, materialize_personal_skill_definition,
|
|
140
187
|
get_skill_health_report,
|
|
141
188
|
)
|
|
189
|
+
|
|
190
|
+
# Hot context / recent continuity
|
|
191
|
+
from db._hot_context import (
|
|
192
|
+
DEFAULT_CONTEXT_TTL_HOURS,
|
|
193
|
+
derive_context_key, clamp_ttl_hours,
|
|
194
|
+
cleanup_expired_hot_context,
|
|
195
|
+
remember_hot_context, record_recent_event, capture_context_event,
|
|
196
|
+
get_hot_context, search_hot_context, search_recent_events,
|
|
197
|
+
build_pre_action_context, format_pre_action_context_bundle,
|
|
198
|
+
resolve_hot_context,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def get_db():
|
|
203
|
+
return _module("db._core").get_db()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def close_db():
|
|
207
|
+
return _module("db._core").close_db()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def init_db():
|
|
211
|
+
return _module("db._core").init_db()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def now_epoch():
|
|
215
|
+
return _module("db._core").now_epoch()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def run_migrations():
|
|
219
|
+
return _module("db._schema").run_migrations()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def get_schema_version():
|
|
223
|
+
return _module("db._schema").get_schema_version()
|