superlocalmemory 3.8.14 → 4.0.0

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.
Files changed (211) hide show
  1. package/ATTRIBUTION.md +4 -4
  2. package/CHANGELOG.md +113 -137
  3. package/README.md +65 -63
  4. package/docs/pi-dev-integration.md +1 -1
  5. package/package.json +6 -1
  6. package/plugin/.claude-plugin/plugin.json +1 -1
  7. package/plugin/CLAUDE.md +3 -3
  8. package/plugin/agents/slm-governance-advisor.md +1 -1
  9. package/plugin/agents/slm-loop-runner.md +1 -1
  10. package/plugin/agents/slm-memory-advisor.md +1 -1
  11. package/plugin/agents/slm-optimize-advisor.md +1 -1
  12. package/plugin/requirements.txt +1 -1
  13. package/plugin/skills/slm-cache/SKILL.md +1 -1
  14. package/plugin/skills/slm-compress/SKILL.md +1 -1
  15. package/plugin/skills/slm-governance/SKILL.md +1 -1
  16. package/plugin/skills/slm-graph/SKILL.md +1 -1
  17. package/plugin/skills/slm-loop/SKILL.md +1 -1
  18. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  19. package/plugin/skills/slm-profile/SKILL.md +1 -1
  20. package/plugin/skills/slm-recall/SKILL.md +1 -1
  21. package/plugin/skills/slm-remember/SKILL.md +1 -1
  22. package/plugin/skills/slm-scope/SKILL.md +1 -1
  23. package/plugin/skills/slm-session/SKILL.md +1 -1
  24. package/plugin/skills/slm-status/SKILL.md +1 -1
  25. package/plugin-src/rules/AGENTS.md +1 -1
  26. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  28. package/plugin-src/skills/slm-governance/SKILL.md +248 -0
  29. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  30. package/plugin-src/skills/slm-loop/SKILL.md +99 -0
  31. package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
  32. package/plugin-src/skills/slm-profile/SKILL.md +148 -0
  33. package/plugin-src/skills/slm-recall/SKILL.md +1 -1
  34. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  35. package/plugin-src/skills/slm-scope/SKILL.md +176 -0
  36. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  37. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  38. package/pyproject.toml +11 -4
  39. package/src/superlocalmemory/__init__.py +1 -1
  40. package/src/superlocalmemory/cli/commands.py +125 -11
  41. package/src/superlocalmemory/cli/daemon.py +5 -1
  42. package/src/superlocalmemory/cli/main.py +35 -2
  43. package/src/superlocalmemory/cli/ops_cmd.py +281 -0
  44. package/src/superlocalmemory/cli/setup_wizard.py +1 -1
  45. package/src/superlocalmemory/compliance/audit.py +65 -0
  46. package/src/superlocalmemory/compliance/eu_ai_act.py +27 -57
  47. package/src/superlocalmemory/compliance/gdpr.py +416 -20
  48. package/src/superlocalmemory/compliance/retention.py +74 -22
  49. package/src/superlocalmemory/compliance/scheduler.py +78 -9
  50. package/src/superlocalmemory/core/actor_context.py +166 -0
  51. package/src/superlocalmemory/core/admission.py +549 -0
  52. package/src/superlocalmemory/core/backend_orchestrator.py +23 -10
  53. package/src/superlocalmemory/core/config.py +202 -24
  54. package/src/superlocalmemory/core/consolidation_engine.py +13 -13
  55. package/src/superlocalmemory/core/context_cache.py +28 -0
  56. package/src/superlocalmemory/core/embeddings.py +64 -2
  57. package/src/superlocalmemory/core/engine.py +7 -2
  58. package/src/superlocalmemory/core/engine_ingestion.py +65 -3
  59. package/src/superlocalmemory/core/engine_wiring.py +32 -5
  60. package/src/superlocalmemory/core/ingest_policy.py +38 -0
  61. package/src/superlocalmemory/core/maintenance.py +255 -0
  62. package/src/superlocalmemory/core/modes.py +40 -13
  63. package/src/superlocalmemory/core/mutations.py +437 -44
  64. package/src/superlocalmemory/core/operation_policy.py +92 -0
  65. package/src/superlocalmemory/core/operation_policy_registry.py +542 -0
  66. package/src/superlocalmemory/core/operation_request.py +127 -0
  67. package/src/superlocalmemory/core/ops_remediation.py +542 -0
  68. package/src/superlocalmemory/core/recall_pipeline.py +7 -0
  69. package/src/superlocalmemory/core/remember_runtime.py +202 -4
  70. package/src/superlocalmemory/core/remote_mode.py +20 -5
  71. package/src/superlocalmemory/core/store_pipeline.py +150 -0
  72. package/src/superlocalmemory/core/topic_signature.py +19 -4
  73. package/src/superlocalmemory/core/transactions/__init__.py +78 -0
  74. package/src/superlocalmemory/core/transactions/concrete_owners.py +597 -0
  75. package/src/superlocalmemory/core/transactions/erasure.py +825 -0
  76. package/src/superlocalmemory/core/transactions/manifest.py +255 -0
  77. package/src/superlocalmemory/core/transactions/manifest_key.py +155 -0
  78. package/src/superlocalmemory/core/transactions/obligations.py +272 -0
  79. package/src/superlocalmemory/core/transactions/owners.py +114 -0
  80. package/src/superlocalmemory/core/transactions/reconciler.py +285 -0
  81. package/src/superlocalmemory/core/transactions/service.py +330 -0
  82. package/src/superlocalmemory/core/worker_pool.py +33 -5
  83. package/src/superlocalmemory/encoding/cognitive_consolidator.py +70 -28
  84. package/src/superlocalmemory/encoding/emotional.py +75 -14
  85. package/src/superlocalmemory/encoding/temporal_parser.py +4 -0
  86. package/src/superlocalmemory/evolution/blind_verifier.py +11 -4
  87. package/src/superlocalmemory/evolution/evolution_store.py +244 -4
  88. package/src/superlocalmemory/evolution/llm_dispatch.py +40 -0
  89. package/src/superlocalmemory/evolution/model_selection.py +18 -3
  90. package/src/superlocalmemory/evolution/mutation_generator.py +3 -0
  91. package/src/superlocalmemory/evolution/skill_activator.py +270 -0
  92. package/src/superlocalmemory/evolution/skill_evolver.py +281 -59
  93. package/src/superlocalmemory/evolution/types.py +30 -8
  94. package/src/superlocalmemory/graph/cozo_backend.py +17 -9
  95. package/src/superlocalmemory/hooks/auto_invoker.py +2 -1
  96. package/src/superlocalmemory/hooks/auto_recall.py +64 -30
  97. package/src/superlocalmemory/hooks/codex_assets.py +14 -1
  98. package/src/superlocalmemory/infra/backup.py +434 -7
  99. package/src/superlocalmemory/infra/process_reaper.py +18 -0
  100. package/src/superlocalmemory/infra/self_heal.py +401 -0
  101. package/src/superlocalmemory/learning/feedback.py +52 -9
  102. package/src/superlocalmemory/loops/engine.py +10 -0
  103. package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
  104. package/src/superlocalmemory/mcp/http_transport.py +30 -331
  105. package/src/superlocalmemory/mcp/profiles.py +5 -0
  106. package/src/superlocalmemory/mcp/resources.py +8 -0
  107. package/src/superlocalmemory/mcp/server.py +51 -4
  108. package/src/superlocalmemory/mcp/shared.py +19 -0
  109. package/src/superlocalmemory/mcp/tools_active.py +25 -4
  110. package/src/superlocalmemory/mcp/tools_code_graph.py +26 -18
  111. package/src/superlocalmemory/mcp/tools_context.py +50 -8
  112. package/src/superlocalmemory/mcp/tools_core.py +69 -21
  113. package/src/superlocalmemory/mcp/tools_evolution.py +9 -2
  114. package/src/superlocalmemory/mcp/tools_learning.py +21 -10
  115. package/src/superlocalmemory/mcp/tools_loops.py +29 -18
  116. package/src/superlocalmemory/mcp/tools_mesh.py +8 -0
  117. package/src/superlocalmemory/mcp/tools_ops.py +115 -0
  118. package/src/superlocalmemory/mcp/tools_optimize.py +4 -0
  119. package/src/superlocalmemory/mcp/tools_v28.py +10 -3
  120. package/src/superlocalmemory/mcp/tools_v3.py +34 -14
  121. package/src/superlocalmemory/mcp/tools_v33.py +18 -33
  122. package/src/superlocalmemory/mesh/broker.py +124 -46
  123. package/src/superlocalmemory/mesh/broker_security.py +470 -0
  124. package/src/superlocalmemory/mesh/discovery.py +365 -0
  125. package/src/superlocalmemory/mesh/lock_protocol.py +313 -0
  126. package/src/superlocalmemory/mesh/node_identity.py +97 -0
  127. package/src/superlocalmemory/mesh/outbox_remote.py +429 -0
  128. package/src/superlocalmemory/mesh/remote_sync.py +511 -28
  129. package/src/superlocalmemory/mesh/state_sync.py +286 -0
  130. package/src/superlocalmemory/optimize/config/store.py +45 -0
  131. package/src/superlocalmemory/parameterization/cross_project.py +12 -0
  132. package/src/superlocalmemory/parameterization/prompt_injector.py +13 -11
  133. package/src/superlocalmemory/parameterization/prompt_lifecycle.py +8 -2
  134. package/src/superlocalmemory/parameterization/workflow_miner.py +17 -0
  135. package/src/superlocalmemory/retrieval/ann_index.py +5 -0
  136. package/src/superlocalmemory/retrieval/bm25_channel.py +49 -2
  137. package/src/superlocalmemory/retrieval/engine.py +19 -4
  138. package/src/superlocalmemory/retrieval/fusion.py +4 -1
  139. package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -3
  140. package/src/superlocalmemory/retrieval/remote_reranker.py +47 -22
  141. package/src/superlocalmemory/retrieval/reranker.py +32 -1
  142. package/src/superlocalmemory/retrieval/temporal_channel.py +16 -3
  143. package/src/superlocalmemory/retrieval/temporal_utils.py +107 -0
  144. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +155 -42
  145. package/src/superlocalmemory/retrieval/vector_store.py +214 -8
  146. package/src/superlocalmemory/server/api.py +5 -5
  147. package/src/superlocalmemory/server/egress_policy.py +258 -0
  148. package/src/superlocalmemory/server/rbac_enforce.py +32 -0
  149. package/src/superlocalmemory/server/route_mutations.py +20 -0
  150. package/src/superlocalmemory/server/routes/compliance.py +153 -7
  151. package/src/superlocalmemory/server/routes/data_io.py +43 -2
  152. package/src/superlocalmemory/server/routes/events.py +15 -0
  153. package/src/superlocalmemory/server/routes/memories.py +56 -3
  154. package/src/superlocalmemory/server/routes/mesh.py +82 -1
  155. package/src/superlocalmemory/server/routes/mesh_lock.py +54 -0
  156. package/src/superlocalmemory/server/routes/mesh_state.py +63 -0
  157. package/src/superlocalmemory/server/routes/v3_api.py +50 -27
  158. package/src/superlocalmemory/server/routes/ws.py +86 -0
  159. package/src/superlocalmemory/server/ui.py +6 -6
  160. package/src/superlocalmemory/server/unified_daemon.py +942 -119
  161. package/src/superlocalmemory/storage/_migration_internals.py +568 -0
  162. package/src/superlocalmemory/storage/_schema_version.py +110 -0
  163. package/src/superlocalmemory/storage/database.py +329 -24
  164. package/src/superlocalmemory/storage/embedding_migrator.py +246 -51
  165. package/src/superlocalmemory/storage/erasure_fence.py +45 -0
  166. package/src/superlocalmemory/storage/generation_fence.py +63 -0
  167. package/src/superlocalmemory/storage/migration_runner.py +140 -424
  168. package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +40 -0
  169. package/src/superlocalmemory/storage/migrations/M033_projection_transactions.py +148 -0
  170. package/src/superlocalmemory/storage/migrations/M034_obligation_integrity.py +58 -0
  171. package/src/superlocalmemory/storage/migrations/M035_erasure_receipts.py +113 -0
  172. package/src/superlocalmemory/storage/migrations/M036_vector_row_map.py +107 -0
  173. package/src/superlocalmemory/storage/migrations/M037_manifest_hmac_version.py +162 -0
  174. package/src/superlocalmemory/storage/migrations/{M033_learning_feedback_channel.py → M038_learning_feedback_channel.py} +3 -3
  175. package/src/superlocalmemory/storage/migrations/{M034_scene_fact_members.py → M039_scene_fact_members.py} +15 -5
  176. package/src/superlocalmemory/storage/migrations/__init__.py +4 -4
  177. package/src/superlocalmemory/storage/schema.py +10 -2
  178. package/src/superlocalmemory/storage/write_coordinator.py +125 -0
  179. package/src/superlocalmemory/trust/scorer.py +28 -4
  180. package/src/superlocalmemory/ui/index.html +14 -3
  181. package/src/superlocalmemory/ui/js/auto-settings.js +12 -1
  182. package/src/superlocalmemory/ui/js/brain.js +6 -4
  183. package/src/superlocalmemory/ui/js/compliance.js +66 -12
  184. package/src/superlocalmemory/ui/js/dashboard.js +13 -3
  185. package/src/superlocalmemory/ui/js/feedback.js +8 -2
  186. package/src/superlocalmemory/ui/js/lifecycle.js +7 -1
  187. package/src/superlocalmemory/ui/js/modal.js +272 -5
  188. package/src/superlocalmemory/ui/js/od-backup.js +9 -2
  189. package/src/superlocalmemory/ui/js/od-compliance-ext.js +301 -0
  190. package/src/superlocalmemory/ui/js/od-operations.js +154 -23
  191. package/src/superlocalmemory/ui/js/od-ops-health.js +417 -0
  192. package/src/superlocalmemory/ui/js/od-optimize.js +35 -21
  193. package/src/superlocalmemory/ui/js/od-team.js +9 -2
  194. package/src/superlocalmemory/ui/js/optimize.js +13 -16
  195. package/src/superlocalmemory/ui/js/profiles.js +7 -3
  196. package/src/superlocalmemory/ui/js/settings.js +7 -1
  197. package/src/superlocalmemory/vector/lancedb_backend.py +19 -9
  198. package/src/superlocalmemory/attribution/mathematical_dna.py +0 -235
  199. package/src/superlocalmemory/cli/post_install.py +0 -114
  200. package/src/superlocalmemory/core/clock_monitor.py +0 -45
  201. package/src/superlocalmemory/core/db_pool.py +0 -80
  202. package/src/superlocalmemory/core/error_catalog.py +0 -113
  203. package/src/superlocalmemory/core/loop_watchdog.py +0 -56
  204. package/src/superlocalmemory/core/priority_queue.py +0 -61
  205. package/src/superlocalmemory/core/pruning_engine.py +0 -216
  206. package/src/superlocalmemory/core/queue_dispatcher.py +0 -73
  207. package/src/superlocalmemory/core/slmignore.py +0 -125
  208. package/src/superlocalmemory/infra/heartbeat_monitor.py +0 -140
  209. package/src/superlocalmemory/infra/webhook_dispatcher.py +0 -247
  210. package/src/superlocalmemory/learning/quantization_scheduler.py +0 -320
  211. package/src/superlocalmemory/storage/access_control.py +0 -182
@@ -1,7 +1,7 @@
1
1
  # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
2
  # Licensed under AGPL-3.0-or-later
3
3
 
4
- """M034 — normalized scene/fact membership for bounded assignment.
4
+ """M039 — normalized scene/fact membership for bounded assignment.
5
5
 
6
6
  ``memory_scenes.fact_ids_json`` remains the public compatibility format. This
7
7
  additive projection provides the indexed reverse lookup needed to map nearest
@@ -13,7 +13,7 @@ from __future__ import annotations
13
13
 
14
14
  import sqlite3
15
15
 
16
- NAME = "M034_scene_fact_members"
16
+ NAME = "M039_scene_fact_members"
17
17
  DB_TARGET = "memory"
18
18
 
19
19
  DDL = """
@@ -32,6 +32,10 @@ CREATE TABLE IF NOT EXISTS memory_scenes (
32
32
  FOREIGN KEY (profile_id) REFERENCES profiles(profile_id) ON DELETE CASCADE
33
33
  );
34
34
  CREATE INDEX IF NOT EXISTS idx_scenes_profile ON memory_scenes(profile_id);
35
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_scenes_profile_scene
36
+ ON memory_scenes (profile_id, scene_id);
37
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_facts_profile_fact
38
+ ON atomic_facts (profile_id, fact_id);
35
39
 
36
40
  CREATE TABLE IF NOT EXISTS scene_fact_members (
37
41
  profile_id TEXT NOT NULL,
@@ -39,8 +43,10 @@ CREATE TABLE IF NOT EXISTS scene_fact_members (
39
43
  fact_id TEXT NOT NULL,
40
44
  position INTEGER NOT NULL DEFAULT 0,
41
45
  PRIMARY KEY (scene_id, fact_id),
42
- FOREIGN KEY (scene_id) REFERENCES memory_scenes(scene_id) ON DELETE CASCADE,
43
- FOREIGN KEY (fact_id) REFERENCES atomic_facts(fact_id) ON DELETE CASCADE,
46
+ FOREIGN KEY (profile_id, scene_id)
47
+ REFERENCES memory_scenes(profile_id, scene_id) ON DELETE CASCADE,
48
+ FOREIGN KEY (profile_id, fact_id)
49
+ REFERENCES atomic_facts(profile_id, fact_id) ON DELETE CASCADE,
44
50
  FOREIGN KEY (profile_id) REFERENCES profiles(profile_id) ON DELETE CASCADE
45
51
  );
46
52
 
@@ -102,7 +108,7 @@ def verify(conn: sqlite3.Connection) -> bool:
102
108
  (str(row[0]), str(row[1]))
103
109
  for row in conn.execute(
104
110
  "SELECT name, type FROM sqlite_master "
105
- "WHERE name IN (?, ?, ?, ?, ?)"
111
+ "WHERE name IN (?, ?, ?, ?, ?, ?, ?)"
106
112
  ,
107
113
  (
108
114
  "scene_fact_members",
@@ -110,6 +116,8 @@ def verify(conn: sqlite3.Connection) -> bool:
110
116
  "idx_scene_fact_members_order",
111
117
  "trg_scene_fact_members_insert",
112
118
  "trg_scene_fact_members_update",
119
+ "uq_scenes_profile_scene",
120
+ "uq_facts_profile_fact",
113
121
  ),
114
122
  ).fetchall()
115
123
  }
@@ -119,6 +127,8 @@ def verify(conn: sqlite3.Connection) -> bool:
119
127
  ("idx_scene_fact_members_order", "index"),
120
128
  ("trg_scene_fact_members_insert", "trigger"),
121
129
  ("trg_scene_fact_members_update", "trigger"),
130
+ ("uq_scenes_profile_scene", "index"),
131
+ ("uq_facts_profile_fact", "index"),
122
132
  }
123
133
 
124
134
 
@@ -28,8 +28,8 @@ from . import (
28
28
  M020_model_state_integrity,
29
29
  M029_behavioral_history_indexes,
30
30
  M030_entity_explorer_indexes,
31
- M033_learning_feedback_channel,
32
- M034_scene_fact_members,
31
+ M038_learning_feedback_channel,
32
+ M039_scene_fact_members,
33
33
  )
34
34
 
35
35
  # ---------------------------------------------------------------------------
@@ -83,8 +83,8 @@ __all__ = (
83
83
  "M020_model_state_integrity",
84
84
  "M029_behavioral_history_indexes",
85
85
  "M030_entity_explorer_indexes",
86
- "M033_learning_feedback_channel",
87
- "M034_scene_fact_members",
86
+ "M038_learning_feedback_channel",
87
+ "M039_scene_fact_members",
88
88
  # Legacy re-exports (backward compat):
89
89
  "CURRENT_SCHEMA_VERSION",
90
90
  "get_schema_version",
@@ -180,6 +180,8 @@ CREATE TABLE IF NOT EXISTS atomic_facts (
180
180
  embedding TEXT,
181
181
  fisher_mean TEXT,
182
182
  fisher_variance TEXT,
183
+ -- Tracks how many accesses had Fisher applied; delta-based update in maintenance
184
+ fisher_last_applied_access INTEGER NOT NULL DEFAULT 0,
183
185
 
184
186
  -- Lifecycle
185
187
  lifecycle TEXT NOT NULL DEFAULT 'active'
@@ -453,6 +455,10 @@ CREATE TABLE IF NOT EXISTS memory_scenes (
453
455
 
454
456
  CREATE INDEX IF NOT EXISTS idx_scenes_profile
455
457
  ON memory_scenes (profile_id);
458
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_scenes_profile_scene
459
+ ON memory_scenes (profile_id, scene_id);
460
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_facts_profile_fact
461
+ ON atomic_facts (profile_id, fact_id);
456
462
  """
457
463
 
458
464
 
@@ -467,8 +473,10 @@ CREATE TABLE IF NOT EXISTS scene_fact_members (
467
473
  fact_id TEXT NOT NULL,
468
474
  position INTEGER NOT NULL DEFAULT 0,
469
475
  PRIMARY KEY (scene_id, fact_id),
470
- FOREIGN KEY (scene_id) REFERENCES memory_scenes(scene_id) ON DELETE CASCADE,
471
- FOREIGN KEY (fact_id) REFERENCES atomic_facts(fact_id) ON DELETE CASCADE,
476
+ FOREIGN KEY (profile_id, scene_id)
477
+ REFERENCES memory_scenes(profile_id, scene_id) ON DELETE CASCADE,
478
+ FOREIGN KEY (profile_id, fact_id)
479
+ REFERENCES atomic_facts(profile_id, fact_id) ON DELETE CASCADE,
472
480
  FOREIGN KEY (profile_id) REFERENCES profiles(profile_id) ON DELETE CASCADE
473
481
  );
474
482
 
@@ -67,6 +67,16 @@ class CommandRejectedError(WriteCoordinatorError):
67
67
  self.error_code = error_code
68
68
 
69
69
 
70
+ class WriterStalledError(WriteCoordinatorError):
71
+ """Raised by submit()/execute() when the worker thread has stalled past STALL_THRESHOLD.
72
+
73
+ This is the circuit-breaker fast-fail: instead of enqueueing behind a dead worker
74
+ (causing every new caller to wait for their per-item timeout), new callers get
75
+ this immediate, actionable error. Admin remediation: check /operations/failed,
76
+ then restart the daemon if the stall persists.
77
+ """
78
+
79
+
70
80
  class Lane(StrEnum):
71
81
  """Scheduling lanes, ordered to protect foreground memory operations."""
72
82
 
@@ -236,12 +246,18 @@ class WriteCoordinator:
236
246
  this coordinator.
237
247
  """
238
248
 
249
+ #: Default stall threshold in seconds. Well above the per-item 1s deadline.
250
+ #: The watchdog is inert for any item that completes within this window,
251
+ #: preserving healthy-path performance byte-for-byte.
252
+ _DEFAULT_STALL_THRESHOLD_S: float = 30.0
253
+
239
254
  def __init__(
240
255
  self,
241
256
  db_path: str | Path,
242
257
  *,
243
258
  owner_id: str | None = None,
244
259
  max_queue_depth: int = _MAX_QUEUE_DEPTH,
260
+ stall_threshold: float | None = None,
245
261
  ) -> None:
246
262
  if max_queue_depth < 1:
247
263
  raise ValueError("max_queue_depth must be at least one")
@@ -274,6 +290,15 @@ class WriteCoordinator:
274
290
  self._worker_ident: int | None = None
275
291
  self._capability_token = object()
276
292
  self._handlers: dict[CommandKind, CommandHandler] = {}
293
+ # ---- Stall watchdog state (additive — inert on the healthy path) ----
294
+ # Protected by self._condition for thread-safety.
295
+ self._stall_threshold: float = (
296
+ stall_threshold if stall_threshold is not None else self._DEFAULT_STALL_THRESHOLD_S
297
+ )
298
+ self._inflight_started_at: float | None = None
299
+ self._inflight_op_id: str | None = None
300
+ self._writer_stalled: bool = False
301
+ self._stall_logged_once: bool = False
277
302
 
278
303
  @property
279
304
  def db_path(self) -> Path:
@@ -285,6 +310,46 @@ class WriteCoordinator:
285
310
  """Opaque daemon instance identifier recorded in the ownership lease."""
286
311
  return self._owner_id
287
312
 
313
+ @property
314
+ def stall_threshold(self) -> float:
315
+ """Seconds before an in-flight item is considered stalled."""
316
+ return self._stall_threshold
317
+
318
+ @property
319
+ def writer_stalled(self) -> bool:
320
+ """True when the worker thread has exceeded stall_threshold on one item.
321
+
322
+ This is a dynamic check — computed from the current inflight age without
323
+ requiring a submitter to trigger _check_stall(). Safe to poll from any thread.
324
+ """
325
+ with self._condition:
326
+ if self._writer_stalled:
327
+ return True
328
+ started = self._inflight_started_at
329
+ if started is None:
330
+ return False
331
+ return (time.monotonic() - started) > self._stall_threshold
332
+
333
+ def inflight_info(self) -> dict:
334
+ """Return stall health snapshot for /health and get_status.
335
+
336
+ Returns:
337
+ {"stalled": bool, "op_id": str | None, "age_s": float | None}
338
+
339
+ Pure read — never raises.
340
+ """
341
+ with self._condition:
342
+ started = self._inflight_started_at
343
+ stalled = self._writer_stalled
344
+ op_id = self._inflight_op_id
345
+ if started is None:
346
+ return {"stalled": False, "op_id": None, "age_s": None}
347
+ now = time.monotonic()
348
+ age = now - started
349
+ # Dynamic stall check (same logic as writer_stalled property)
350
+ is_stalled = stalled or (age > self._stall_threshold)
351
+ return {"stalled": is_stalled, "op_id": op_id, "age_s": round(age, 2)}
352
+
288
353
  def claim_ownership(self) -> bool:
289
354
  """Claim the cross-platform owner lease without waiting on another daemon."""
290
355
  if _portalocker is None:
@@ -407,6 +472,34 @@ class WriteCoordinator:
407
472
  if context is not None:
408
473
  context.__exit__(None, None, None)
409
474
 
475
+ def _check_stall(self) -> None:
476
+ """Circuit-breaker check: raise WriterStalledError if worker is stalled.
477
+
478
+ Called from submit() and execute() BEFORE enqueueing. Inert on the
479
+ healthy path (started_at is None or within stall_threshold).
480
+ Thread-safe: reads only condition-protected state.
481
+ """
482
+ with self._condition:
483
+ started = self._inflight_started_at
484
+ if started is None:
485
+ return
486
+ age = time.monotonic() - started
487
+ if age > self._stall_threshold:
488
+ if not self._stall_logged_once:
489
+ import logging as _log
490
+ _log.getLogger(__name__).critical(
491
+ "WriteCoordinator: worker thread stalled for %.1fs on op=%s; "
492
+ "new submitters will receive WriterStalledError",
493
+ age,
494
+ self._inflight_op_id,
495
+ )
496
+ self._stall_logged_once = True
497
+ self._writer_stalled = True
498
+ if self._writer_stalled:
499
+ raise WriterStalledError(
500
+ "write subsystem stalled; admin remediation required"
501
+ )
502
+
410
503
  def execute(
411
504
  self,
412
505
  sql: str,
@@ -424,6 +517,7 @@ class WriteCoordinator:
424
517
  raise ValueError("sql must be a non-empty statement")
425
518
  if timeout <= 0:
426
519
  raise ValueError("timeout must be greater than zero")
520
+ self._check_stall() # circuit-breaker (inert if no stall)
427
521
  lane = self._coerce_lane(priority)
428
522
  self.start()
429
523
  item = _Execution(
@@ -467,6 +561,7 @@ class WriteCoordinator:
467
561
  raise TypeError("command must be a WriteCommand")
468
562
  if timeout <= 0:
469
563
  raise ValueError("timeout must be greater than zero")
564
+ self._check_stall() # circuit-breaker (inert if no stall)
470
565
  lane = self._coerce_lane(priority)
471
566
  self.start()
472
567
  item = _Execution(
@@ -569,8 +664,25 @@ class WriteCoordinator:
569
664
  return Lane.FOREGROUND
570
665
 
571
666
  def _execute_item(self, conn: sqlite3.Connection, item: _Execution) -> None:
667
+ # ------------------------------------------------------------------
668
+ # Stall watchdog: record inflight state so _check_stall() can detect
669
+ # a frozen worker. Cleared in the finally block. This guard is
670
+ # strictly ADDITIVE — the item execution path is unchanged.
671
+ # ------------------------------------------------------------------
672
+ op_id: str | None = None
673
+ if item.command is not None:
674
+ op_id = item.command.command_id
675
+ with self._condition:
676
+ self._inflight_started_at = time.monotonic()
677
+ self._inflight_op_id = op_id
678
+
572
679
  if item.cancelled or time.monotonic() >= item.deadline:
573
680
  item.error = WriteDeadlineExceededError("canonical write expired before execution")
681
+ with self._condition:
682
+ self._inflight_started_at = None
683
+ self._inflight_op_id = None
684
+ self._writer_stalled = False
685
+ self._stall_logged_once = False
574
686
  item.completion.set()
575
687
  return
576
688
  remaining = max(0.0, item.deadline - time.monotonic())
@@ -578,6 +690,11 @@ class WriteCoordinator:
578
690
  item.error = WriteDeadlineExceededError(
579
691
  "canonical write expired waiting for its process lock"
580
692
  )
693
+ with self._condition:
694
+ self._inflight_started_at = None
695
+ self._inflight_op_id = None
696
+ self._writer_stalled = False
697
+ self._stall_logged_once = False
581
698
  item.completion.set()
582
699
  return
583
700
  try:
@@ -637,6 +754,13 @@ class WriteCoordinator:
637
754
  item.error.__cause__ = exc
638
755
  finally:
639
756
  self._process_write_lock.release()
757
+ # Clear inflight state and reset stall breaker so next caller can proceed.
758
+ with self._condition:
759
+ self._inflight_started_at = None
760
+ self._inflight_op_id = None
761
+ if self._writer_stalled:
762
+ self._writer_stalled = False
763
+ self._stall_logged_once = False
640
764
  item.completion.set()
641
765
 
642
766
  def _execute_command(self, conn: sqlite3.Connection, command: WriteCommand) -> WriteResult:
@@ -800,4 +924,5 @@ __all__ = [
800
924
  "WriteCoordinatorError",
801
925
  "WriteDeadlineExceededError",
802
926
  "WriteResult",
927
+ "WriterStalledError",
803
928
  ]
@@ -73,10 +73,34 @@ class TrustScorer:
73
73
  return self._compute_trust(alpha, beta)
74
74
 
75
75
  def get_fact_trust(self, fact_id: str, profile_id: str) -> float:
76
- """Get trust for a fact. Inherits source agent trust, modified by
77
- any contradiction evidence recorded directly against the fact."""
78
- alpha, beta = self._get_beta_params("fact", fact_id, profile_id)
79
- return self._compute_trust(alpha, beta)
76
+ """Get trust for a fact.
77
+
78
+ When a direct trust record exists for the fact, use it.
79
+ Otherwise inherit the trust of the agent that created the fact,
80
+ as recorded in the provenance table.
81
+ """
82
+ rows = self._db.execute(
83
+ "SELECT trust_score, evidence_count FROM trust_scores "
84
+ "WHERE target_type = ? AND target_id = ? AND profile_id = ?",
85
+ ("fact", fact_id, profile_id),
86
+ )
87
+ if rows:
88
+ d = dict(rows[0])
89
+ alpha, beta = self._decode_beta(d["trust_score"], d["evidence_count"])
90
+ return self._compute_trust(alpha, beta)
91
+
92
+ # No direct record — inherit from the provenance source agent.
93
+ prov = self._db.execute(
94
+ "SELECT created_by FROM provenance "
95
+ "WHERE fact_id = ? AND profile_id = ?",
96
+ (fact_id, profile_id),
97
+ )
98
+ if prov:
99
+ agent_id = dict(prov[0]).get("created_by", "")
100
+ if agent_id:
101
+ return self.get_agent_trust(agent_id, profile_id)
102
+
103
+ return _DEFAULT_TRUST
80
104
 
81
105
  def get_entity_trust(self, entity_id: str, profile_id: str) -> float:
82
106
  """Convenience: get trust for an entity."""
@@ -49,8 +49,8 @@
49
49
  <input id="q" placeholder="Search memories…">
50
50
  <span class="kbd">⌘K</span>
51
51
  </label>
52
- <span class="badge-local" title="All data stays on this machine. Nothing leaves your device.">
53
- <i class="bi bi-lock-fill" aria-hidden="true"></i> LOCAL ONLY
52
+ <span class="badge-local" title="Local-first memory; active mode and integrations determine network egress.">
53
+ <i class="bi bi-lock-fill" aria-hidden="true"></i> LOCAL-FIRST
54
54
  </span>
55
55
  <button class="btn icon ghost" data-theme-icon aria-label="Toggle theme"></button>
56
56
  </header>
@@ -973,7 +973,10 @@
973
973
  <h6 class="mb-2">Active retention policies</h6>
974
974
  <div class="mb-3" id="compliance-policies-content"><span class="text-muted">Loading...</span></div>
975
975
  <div class="d-flex justify-content-between align-items-center mb-2">
976
- <h6 class="mb-0">Audit trail</h6>
976
+ <div class="d-flex align-items-center gap-2">
977
+ <h6 class="mb-0">Audit trail</h6>
978
+ <span id="cp-chain-integrity"></span>
979
+ </div>
977
980
  <select class="form-select form-select-sm" id="cp-audit-filter" style="width: auto;" data-act-change="load-compliance">
978
981
  <option value="">All events</option>
979
982
  <option value="recall">Recall</option>
@@ -986,6 +989,12 @@
986
989
  </div>
987
990
  <div id="compliance-audit-content"><span class="text-muted">Loading...</span></div>
988
991
  </section>
992
+
993
+ <!-- Admin Operations Health is provided as a self-injected tab by
994
+ ui/js/od-ops-health.js (fetches /operations/failed, KPI strip,
995
+ writer-stall banner, one-click Retry/Reconcile/Cancel). The earlier
996
+ static shell here was unpopulated and carried an inline handler, so it
997
+ was removed in favour of the single functional CSP-clean tab. -->
989
998
  </div>
990
999
 
991
1000
  <!-- Skill Evolution (v3.4.10 — Arsenal Evolution) -->
@@ -1569,6 +1578,8 @@
1569
1578
  <script src="static/js/od-components.js?v=382"></script>
1570
1579
  <script src="static/js/od-health.js?v=382"></script>
1571
1580
  <script src="static/js/od-operations.js?v=380"></script>
1581
+ <script src="static/js/od-compliance-ext.js?v=100"></script>
1582
+ <script src="static/js/od-ops-health.js?v=400"></script>
1572
1583
  <script src="static/js/od-team.js?v=379"></script>
1573
1584
  <script src="static/js/od-graph.js?v=379"></script>
1574
1585
  <script src="static/js/od-memories.js?v=379"></script>
@@ -195,12 +195,23 @@ async function loadModeSettings() {
195
195
 
196
196
  var bannerDetail = document.getElementById('settings-current-detail');
197
197
  if (bannerDetail) {
198
- if (mode === 'a') bannerDetail.textContent = 'Zero cloudEU AI Act compliant';
198
+ if (mode === 'a') bannerDetail.textContent = 'Local inferencelegal classification requires deployment assessment';
199
199
  else if (data.has_key) bannerDetail.textContent = 'API key configured';
200
200
  else if (provider === 'ollama') bannerDetail.textContent = 'No API key needed';
201
201
  else bannerDetail.textContent = 'API key not set';
202
202
  }
203
203
 
204
+ var postureBadge = document.querySelector('.badge-local');
205
+ if (postureBadge) {
206
+ if (mode === 'c') {
207
+ postureBadge.innerHTML = '<i class="bi bi-cloud" aria-hidden="true"></i> CLOUD MODE';
208
+ postureBadge.title = 'Mode C can use configured cloud providers and integrations.';
209
+ } else {
210
+ postureBadge.innerHTML = '<i class="bi bi-lock-fill" aria-hidden="true"></i> LOCAL INFERENCE';
211
+ postureBadge.title = 'Inference is local; mesh, integrations, and backups have separate egress controls.';
212
+ }
213
+ }
214
+
204
215
  var banner = document.getElementById('settings-current-banner');
205
216
  if (banner) {
206
217
  banner.className = mode === 'a' ? 'alert alert-success mb-3' :
@@ -1066,10 +1066,12 @@
1066
1066
  'aria-live': 'polite',
1067
1067
  });
1068
1068
  btn.addEventListener('click', async () => {
1069
- const ok = window.confirm(
1070
- 'Reset all learning data? Memories will be preserved, '
1071
- + 'but learned patterns and ranking signals will be deleted.',
1072
- );
1069
+ const ok = await window.confirmDestructive({
1070
+ title: 'Reset learning data',
1071
+ target: 'All learned patterns and ranking signals',
1072
+ consequence: 'Memories will be preserved, but learned patterns and ranking signals will be deleted.',
1073
+ confirmLabel: 'Reset',
1074
+ });
1073
1075
  if (!ok) return;
1074
1076
  status.textContent = 'Resetting…';
1075
1077
  try {
@@ -10,11 +10,9 @@ async function loadCompliance() {
10
10
  var filterValue = filterEl ? filterEl.value : '';
11
11
 
12
12
  try {
13
- var url = '/api/compliance/status';
14
- if (filterValue) url += '?event_type=' + encodeURIComponent(filterValue);
15
- var response = await fetch(url);
16
- if (!response.ok) throw new Error('HTTP ' + response.status);
17
- var data = await response.json();
13
+ var statusResp = await fetch('/api/compliance/status');
14
+ if (!statusResp.ok) throw new Error('HTTP ' + statusResp.status);
15
+ var data = await statusResp.json();
18
16
  _complianceData = data;
19
17
 
20
18
  if (!data.available) {
@@ -24,20 +22,37 @@ async function loadCompliance() {
24
22
 
25
23
  renderComplianceStats(data);
26
24
  renderCompliancePolicies(data);
27
- renderComplianceAudit(data);
25
+
26
+ // Task C: fetch real audit trail from dedicated endpoint with filters
27
+ var auditUrl = '/api/compliance/audit?limit=100';
28
+ if (filterValue) auditUrl += '&event_type=' + encodeURIComponent(filterValue);
29
+ var auditResp = await fetch(auditUrl);
30
+ var auditData = auditResp.ok ? await auditResp.json() : null;
31
+ renderComplianceAudit(auditData || data);
28
32
 
29
33
  var badge = document.getElementById('compliance-profile-badge');
30
34
  if (badge) badge.textContent = data.active_profile || 'default';
35
+
36
+ // Task C: show chain-integrity indicator
37
+ var chainEl = document.getElementById('cp-chain-integrity');
38
+ if (chainEl && auditData) {
39
+ var ok = auditData.chain_verified !== false;
40
+ chainEl.innerHTML = ok
41
+ ? '<span class="badge bg-success"><i class="bi bi-shield-check"></i> Chain verified</span>'
42
+ : '<span class="badge bg-danger"><i class="bi bi-shield-x"></i> Chain integrity issue</span>';
43
+ }
31
44
  } catch (error) {
32
45
  console.error('Error loading compliance:', error);
33
46
  }
34
47
  }
35
48
 
49
+ // Task D: Fix KPI field reads — /api/compliance/status returns top-level fields,
50
+ // NOT a nested .stats object. audit_events_count, retention_policies (array),
51
+ // abac_policies_count are all top-level.
36
52
  function renderComplianceStats(data) {
37
- var stats = data.stats || {};
38
- animateCounter('cp-audit-count', stats.audit_count || 0);
39
- animateCounter('cp-retention-count', stats.retention_count || 0);
40
- animateCounter('cp-abac-count', stats.abac_count || 0);
53
+ animateCounter('cp-audit-count', data.audit_events_count || 0);
54
+ animateCounter('cp-retention-count', (data.retention_policies || []).length);
55
+ animateCounter('cp-abac-count', data.abac_policies_count || 0);
41
56
  }
42
57
 
43
58
  function renderCompliancePolicies(data) {
@@ -58,7 +73,7 @@ function renderCompliancePolicies(data) {
58
73
  table.className = 'table table-sm table-hover mb-0';
59
74
  var thead = document.createElement('thead');
60
75
  var headRow = document.createElement('tr');
61
- ['Policy Name', 'Retention (days)', 'Category', 'Action', 'Created'].forEach(function(h) {
76
+ ['Policy Name', 'Retention (days)', 'Category', 'Action', 'Created', ''].forEach(function(h) {
62
77
  var th = document.createElement('th');
63
78
  th.textContent = h;
64
79
  headRow.appendChild(th);
@@ -106,16 +121,55 @@ function renderCompliancePolicies(data) {
106
121
  dateCell.textContent = formatDate(pol.created_at || '');
107
122
  row.appendChild(dateCell);
108
123
 
124
+ // Task E: Delete button per row — calls DELETE /api/compliance/retention-policy?name=
125
+ var actCell = document.createElement('td');
126
+ var delBtn = document.createElement('button');
127
+ delBtn.className = 'btn btn-sm btn-outline-danger';
128
+ delBtn.textContent = 'Delete';
129
+ delBtn.addEventListener('click', (function(policyName) {
130
+ return async function() {
131
+ var confirmed = await confirmDestructive({
132
+ title: 'Delete retention policy',
133
+ target: policyName,
134
+ consequence: 'This policy will stop applying to all memories.',
135
+ confirmLabel: 'Delete',
136
+ });
137
+ if (!confirmed) return;
138
+ delBtn.disabled = true;
139
+ try {
140
+ var r = await fetch(
141
+ '/api/compliance/retention-policy?name=' + encodeURIComponent(policyName),
142
+ { method: 'DELETE' }
143
+ );
144
+ var d = await r.json().catch(function() { return {}; });
145
+ if (d.success !== false) {
146
+ showToast('Policy deleted.');
147
+ loadCompliance();
148
+ } else {
149
+ showToast((d.error || 'Delete failed.'));
150
+ delBtn.disabled = false;
151
+ }
152
+ } catch (e) {
153
+ showToast('Network error deleting policy.');
154
+ delBtn.disabled = false;
155
+ }
156
+ };
157
+ }(pol.name || '')));
158
+ actCell.appendChild(delBtn);
159
+ row.appendChild(actCell);
160
+
109
161
  tbody.appendChild(row);
110
162
  }
111
163
  table.appendChild(tbody);
112
164
  container.appendChild(table);
113
165
  }
114
166
 
167
+ // Task C: renderComplianceAudit accepts data from either the status endpoint
168
+ // (recent_audit_events) or the dedicated audit endpoint (events). Tries both.
115
169
  function renderComplianceAudit(data) {
116
170
  var container = document.getElementById('compliance-audit-content');
117
171
  if (!container) return;
118
- var events = data.audit_events || [];
172
+ var events = data.events || data.recent_audit_events || data.audit_events || [];
119
173
  container.textContent = '';
120
174
 
121
175
  if (events.length === 0) {
@@ -354,11 +354,21 @@ async function loadDashboard() {
354
354
  if (dashVer) dashVer.textContent = ver;
355
355
  if (settVer) settVer.textContent = ver;
356
356
 
357
- // OD dashboard subtitle
357
+ // OD dashboard subtitle — locality comes from the mode record via API
358
+ // (data.data_locality_label). Never hardcode a fixed locality claim;
359
+ // Mode C is provider-assisted and must not claim data never leaves.
358
360
  var subtitle = document.getElementById('od-dash-subtitle');
359
361
  if (subtitle && data.mode_name) {
360
- subtitle.textContent = 'Mode ' + data.mode.toUpperCase() + ' · ' + data.mode_name +
361
- ' · local-only · v' + (ver || '?');
362
+ var locality = (data.data_locality_label || '').trim();
363
+ var parts = [
364
+ 'Mode ' + data.mode.toUpperCase(),
365
+ data.mode_name,
366
+ ];
367
+ if (locality) {
368
+ parts.push(locality);
369
+ }
370
+ parts.push('v' + (ver || '?'));
371
+ subtitle.textContent = parts.join(' · ');
362
372
  }
363
373
 
364
374
  // Update mode badge in sidebar (ng-premount hidden element)
@@ -312,8 +312,14 @@ function showPrivacyDetails() {
312
312
  /**
313
313
  * Reset all learning data.
314
314
  */
315
- function resetLearningData() {
316
- if (!confirm('Reset all learning data? Your memories will be preserved.')) return;
315
+ async function resetLearningData() {
316
+ var confirmed = await confirmDestructive({
317
+ title: 'Reset learning data',
318
+ target: 'All learned patterns and ranking signals',
319
+ consequence: 'Your memories will be preserved.',
320
+ confirmLabel: 'Reset',
321
+ });
322
+ if (!confirmed) return;
317
323
 
318
324
  fetch('/api/learning/reset', {method: 'POST'})
319
325
  .then(function(r) { return r.json(); })
@@ -299,7 +299,13 @@ async function compactDryRun() {
299
299
  }
300
300
 
301
301
  async function compactExecute() {
302
- if (!confirm('This will transition memories to lower lifecycle states. Continue?')) return;
302
+ var confirmed = await confirmDestructive({
303
+ title: 'Apply compaction',
304
+ target: 'All eligible memories',
305
+ consequence: 'Transitions memories to lower lifecycle states.',
306
+ confirmLabel: 'Apply',
307
+ });
308
+ if (!confirmed) return;
303
309
  try {
304
310
  var response = await fetch('/api/lifecycle/compact', {
305
311
  method: 'POST',