nexo-brain 3.1.5 → 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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "3.1.5",
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.5",
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",
@@ -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",
@@ -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 res = await fetch(url, { method: 'DELETE' });
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
- const source = type === 'reminder' ? allReminders : allFollowups;
475
- const item = source.find(i => i.id === id);
476
- if (!item) return;
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, {
@@ -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()