superlocalmemory 3.8.13 → 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 (212) hide show
  1. package/ATTRIBUTION.md +4 -4
  2. package/CHANGELOG.md +113 -121
  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 +36 -9
  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/scene_builder.py +115 -13
  86. package/src/superlocalmemory/encoding/temporal_parser.py +4 -0
  87. package/src/superlocalmemory/evolution/blind_verifier.py +11 -4
  88. package/src/superlocalmemory/evolution/evolution_store.py +244 -4
  89. package/src/superlocalmemory/evolution/llm_dispatch.py +40 -0
  90. package/src/superlocalmemory/evolution/model_selection.py +18 -3
  91. package/src/superlocalmemory/evolution/mutation_generator.py +3 -0
  92. package/src/superlocalmemory/evolution/skill_activator.py +270 -0
  93. package/src/superlocalmemory/evolution/skill_evolver.py +281 -59
  94. package/src/superlocalmemory/evolution/types.py +30 -8
  95. package/src/superlocalmemory/graph/cozo_backend.py +17 -9
  96. package/src/superlocalmemory/hooks/auto_invoker.py +2 -1
  97. package/src/superlocalmemory/hooks/auto_recall.py +64 -30
  98. package/src/superlocalmemory/hooks/codex_assets.py +14 -1
  99. package/src/superlocalmemory/infra/backup.py +434 -7
  100. package/src/superlocalmemory/infra/process_reaper.py +18 -0
  101. package/src/superlocalmemory/infra/self_heal.py +401 -0
  102. package/src/superlocalmemory/learning/feedback.py +52 -9
  103. package/src/superlocalmemory/loops/engine.py +10 -0
  104. package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
  105. package/src/superlocalmemory/mcp/http_transport.py +30 -331
  106. package/src/superlocalmemory/mcp/profiles.py +5 -0
  107. package/src/superlocalmemory/mcp/resources.py +8 -0
  108. package/src/superlocalmemory/mcp/server.py +51 -4
  109. package/src/superlocalmemory/mcp/shared.py +19 -0
  110. package/src/superlocalmemory/mcp/tools_active.py +25 -4
  111. package/src/superlocalmemory/mcp/tools_code_graph.py +26 -18
  112. package/src/superlocalmemory/mcp/tools_context.py +50 -8
  113. package/src/superlocalmemory/mcp/tools_core.py +69 -21
  114. package/src/superlocalmemory/mcp/tools_evolution.py +9 -2
  115. package/src/superlocalmemory/mcp/tools_learning.py +21 -10
  116. package/src/superlocalmemory/mcp/tools_loops.py +29 -18
  117. package/src/superlocalmemory/mcp/tools_mesh.py +8 -0
  118. package/src/superlocalmemory/mcp/tools_ops.py +115 -0
  119. package/src/superlocalmemory/mcp/tools_optimize.py +4 -0
  120. package/src/superlocalmemory/mcp/tools_v28.py +10 -3
  121. package/src/superlocalmemory/mcp/tools_v3.py +34 -14
  122. package/src/superlocalmemory/mcp/tools_v33.py +18 -33
  123. package/src/superlocalmemory/mesh/broker.py +124 -46
  124. package/src/superlocalmemory/mesh/broker_security.py +470 -0
  125. package/src/superlocalmemory/mesh/discovery.py +365 -0
  126. package/src/superlocalmemory/mesh/lock_protocol.py +313 -0
  127. package/src/superlocalmemory/mesh/node_identity.py +97 -0
  128. package/src/superlocalmemory/mesh/outbox_remote.py +429 -0
  129. package/src/superlocalmemory/mesh/remote_sync.py +511 -28
  130. package/src/superlocalmemory/mesh/state_sync.py +286 -0
  131. package/src/superlocalmemory/optimize/config/store.py +45 -0
  132. package/src/superlocalmemory/parameterization/cross_project.py +12 -0
  133. package/src/superlocalmemory/parameterization/prompt_injector.py +13 -11
  134. package/src/superlocalmemory/parameterization/prompt_lifecycle.py +8 -2
  135. package/src/superlocalmemory/parameterization/workflow_miner.py +17 -0
  136. package/src/superlocalmemory/retrieval/ann_index.py +5 -0
  137. package/src/superlocalmemory/retrieval/bm25_channel.py +49 -2
  138. package/src/superlocalmemory/retrieval/engine.py +19 -4
  139. package/src/superlocalmemory/retrieval/fusion.py +4 -1
  140. package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -3
  141. package/src/superlocalmemory/retrieval/remote_reranker.py +47 -22
  142. package/src/superlocalmemory/retrieval/reranker.py +32 -1
  143. package/src/superlocalmemory/retrieval/temporal_channel.py +16 -3
  144. package/src/superlocalmemory/retrieval/temporal_utils.py +107 -0
  145. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +155 -42
  146. package/src/superlocalmemory/retrieval/vector_store.py +214 -8
  147. package/src/superlocalmemory/server/api.py +5 -5
  148. package/src/superlocalmemory/server/egress_policy.py +258 -0
  149. package/src/superlocalmemory/server/rbac_enforce.py +32 -0
  150. package/src/superlocalmemory/server/route_mutations.py +20 -0
  151. package/src/superlocalmemory/server/routes/compliance.py +153 -7
  152. package/src/superlocalmemory/server/routes/data_io.py +43 -2
  153. package/src/superlocalmemory/server/routes/events.py +15 -0
  154. package/src/superlocalmemory/server/routes/memories.py +56 -3
  155. package/src/superlocalmemory/server/routes/mesh.py +82 -1
  156. package/src/superlocalmemory/server/routes/mesh_lock.py +54 -0
  157. package/src/superlocalmemory/server/routes/mesh_state.py +63 -0
  158. package/src/superlocalmemory/server/routes/v3_api.py +50 -27
  159. package/src/superlocalmemory/server/routes/ws.py +86 -0
  160. package/src/superlocalmemory/server/ui.py +6 -6
  161. package/src/superlocalmemory/server/unified_daemon.py +942 -119
  162. package/src/superlocalmemory/storage/_migration_internals.py +568 -0
  163. package/src/superlocalmemory/storage/_schema_version.py +110 -0
  164. package/src/superlocalmemory/storage/database.py +329 -24
  165. package/src/superlocalmemory/storage/embedding_migrator.py +246 -51
  166. package/src/superlocalmemory/storage/erasure_fence.py +45 -0
  167. package/src/superlocalmemory/storage/generation_fence.py +63 -0
  168. package/src/superlocalmemory/storage/migration_runner.py +140 -417
  169. package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +40 -0
  170. package/src/superlocalmemory/storage/migrations/M033_projection_transactions.py +148 -0
  171. package/src/superlocalmemory/storage/migrations/M034_obligation_integrity.py +58 -0
  172. package/src/superlocalmemory/storage/migrations/M035_erasure_receipts.py +113 -0
  173. package/src/superlocalmemory/storage/migrations/M036_vector_row_map.py +107 -0
  174. package/src/superlocalmemory/storage/migrations/M037_manifest_hmac_version.py +162 -0
  175. package/src/superlocalmemory/storage/migrations/{M033_learning_feedback_channel.py → M038_learning_feedback_channel.py} +3 -3
  176. package/src/superlocalmemory/storage/migrations/M039_scene_fact_members.py +137 -0
  177. package/src/superlocalmemory/storage/migrations/__init__.py +4 -2
  178. package/src/superlocalmemory/storage/schema.py +67 -0
  179. package/src/superlocalmemory/storage/write_coordinator.py +125 -0
  180. package/src/superlocalmemory/trust/scorer.py +28 -4
  181. package/src/superlocalmemory/ui/index.html +14 -3
  182. package/src/superlocalmemory/ui/js/auto-settings.js +12 -1
  183. package/src/superlocalmemory/ui/js/brain.js +6 -4
  184. package/src/superlocalmemory/ui/js/compliance.js +66 -12
  185. package/src/superlocalmemory/ui/js/dashboard.js +13 -3
  186. package/src/superlocalmemory/ui/js/feedback.js +8 -2
  187. package/src/superlocalmemory/ui/js/lifecycle.js +7 -1
  188. package/src/superlocalmemory/ui/js/modal.js +272 -5
  189. package/src/superlocalmemory/ui/js/od-backup.js +9 -2
  190. package/src/superlocalmemory/ui/js/od-compliance-ext.js +301 -0
  191. package/src/superlocalmemory/ui/js/od-operations.js +154 -23
  192. package/src/superlocalmemory/ui/js/od-ops-health.js +417 -0
  193. package/src/superlocalmemory/ui/js/od-optimize.js +35 -21
  194. package/src/superlocalmemory/ui/js/od-team.js +9 -2
  195. package/src/superlocalmemory/ui/js/optimize.js +13 -16
  196. package/src/superlocalmemory/ui/js/profiles.js +7 -3
  197. package/src/superlocalmemory/ui/js/settings.js +7 -1
  198. package/src/superlocalmemory/vector/lancedb_backend.py +19 -9
  199. package/src/superlocalmemory/attribution/mathematical_dna.py +0 -235
  200. package/src/superlocalmemory/cli/post_install.py +0 -114
  201. package/src/superlocalmemory/core/clock_monitor.py +0 -45
  202. package/src/superlocalmemory/core/db_pool.py +0 -80
  203. package/src/superlocalmemory/core/error_catalog.py +0 -113
  204. package/src/superlocalmemory/core/loop_watchdog.py +0 -56
  205. package/src/superlocalmemory/core/priority_queue.py +0 -61
  206. package/src/superlocalmemory/core/pruning_engine.py +0 -216
  207. package/src/superlocalmemory/core/queue_dispatcher.py +0 -73
  208. package/src/superlocalmemory/core/slmignore.py +0 -125
  209. package/src/superlocalmemory/infra/heartbeat_monitor.py +0 -140
  210. package/src/superlocalmemory/infra/webhook_dispatcher.py +0 -247
  211. package/src/superlocalmemory/learning/quantization_scheduler.py +0 -320
  212. package/src/superlocalmemory/storage/access_control.py +0 -182
@@ -0,0 +1,137 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later
3
+
4
+ """M039 — normalized scene/fact membership for bounded assignment.
5
+
6
+ ``memory_scenes.fact_ids_json`` remains the public compatibility format. This
7
+ additive projection provides the indexed reverse lookup needed to map nearest
8
+ fact-vector hits back to candidate scenes without scanning every scene for
9
+ every ingested fact. Triggers keep all existing scene write paths synchronized.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import sqlite3
15
+
16
+ NAME = "M039_scene_fact_members"
17
+ DB_TARGET = "memory"
18
+
19
+ DDL = """
20
+ BEGIN IMMEDIATE;
21
+
22
+ -- Keep the deferred migration independently safe for partial/legacy installs.
23
+ -- On normal daemon startup MemoryEngine has already created this table.
24
+ CREATE TABLE IF NOT EXISTS memory_scenes (
25
+ scene_id TEXT PRIMARY KEY,
26
+ profile_id TEXT NOT NULL DEFAULT 'default',
27
+ theme TEXT NOT NULL DEFAULT '',
28
+ fact_ids_json TEXT NOT NULL DEFAULT '[]',
29
+ entity_ids_json TEXT NOT NULL DEFAULT '[]',
30
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
31
+ last_updated TEXT NOT NULL DEFAULT (datetime('now')),
32
+ FOREIGN KEY (profile_id) REFERENCES profiles(profile_id) ON DELETE CASCADE
33
+ );
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);
39
+
40
+ CREATE TABLE IF NOT EXISTS scene_fact_members (
41
+ profile_id TEXT NOT NULL,
42
+ scene_id TEXT NOT NULL,
43
+ fact_id TEXT NOT NULL,
44
+ position INTEGER NOT NULL DEFAULT 0,
45
+ PRIMARY KEY (scene_id, fact_id),
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,
50
+ FOREIGN KEY (profile_id) REFERENCES profiles(profile_id) ON DELETE CASCADE
51
+ );
52
+
53
+ CREATE INDEX IF NOT EXISTS idx_scene_fact_members_lookup
54
+ ON scene_fact_members (profile_id, fact_id, scene_id);
55
+ CREATE INDEX IF NOT EXISTS idx_scene_fact_members_order
56
+ ON scene_fact_members (scene_id, position);
57
+
58
+ CREATE TRIGGER IF NOT EXISTS trg_scene_fact_members_insert
59
+ AFTER INSERT ON memory_scenes
60
+ BEGIN
61
+ DELETE FROM scene_fact_members WHERE scene_id = NEW.scene_id;
62
+ INSERT OR IGNORE INTO scene_fact_members
63
+ (profile_id, scene_id, fact_id, position)
64
+ SELECT NEW.profile_id, NEW.scene_id, af.fact_id, CAST(member.key AS INTEGER)
65
+ FROM json_each(
66
+ CASE WHEN json_valid(NEW.fact_ids_json)
67
+ THEN NEW.fact_ids_json ELSE '[]' END
68
+ ) AS member
69
+ JOIN atomic_facts AS af
70
+ ON af.fact_id = member.value
71
+ AND af.profile_id = NEW.profile_id;
72
+ END;
73
+
74
+ CREATE TRIGGER IF NOT EXISTS trg_scene_fact_members_update
75
+ AFTER UPDATE OF profile_id, fact_ids_json ON memory_scenes
76
+ BEGIN
77
+ DELETE FROM scene_fact_members WHERE scene_id = NEW.scene_id;
78
+ INSERT OR IGNORE INTO scene_fact_members
79
+ (profile_id, scene_id, fact_id, position)
80
+ SELECT NEW.profile_id, NEW.scene_id, af.fact_id, CAST(member.key AS INTEGER)
81
+ FROM json_each(
82
+ CASE WHEN json_valid(NEW.fact_ids_json)
83
+ THEN NEW.fact_ids_json ELSE '[]' END
84
+ ) AS member
85
+ JOIN atomic_facts AS af
86
+ ON af.fact_id = member.value
87
+ AND af.profile_id = NEW.profile_id;
88
+ END;
89
+
90
+ INSERT OR IGNORE INTO scene_fact_members
91
+ (profile_id, scene_id, fact_id, position)
92
+ SELECT ms.profile_id, ms.scene_id, af.fact_id, CAST(member.key AS INTEGER)
93
+ FROM memory_scenes AS ms
94
+ JOIN json_each(
95
+ CASE WHEN json_valid(ms.fact_ids_json) THEN ms.fact_ids_json ELSE '[]' END
96
+ ) AS member
97
+ JOIN atomic_facts AS af
98
+ ON af.fact_id = member.value
99
+ AND af.profile_id = ms.profile_id;
100
+
101
+ COMMIT;
102
+ """
103
+
104
+
105
+ def verify(conn: sqlite3.Connection) -> bool:
106
+ """Verify the table, covering indexes, and synchronization triggers."""
107
+ objects = {
108
+ (str(row[0]), str(row[1]))
109
+ for row in conn.execute(
110
+ "SELECT name, type FROM sqlite_master "
111
+ "WHERE name IN (?, ?, ?, ?, ?, ?, ?)"
112
+ ,
113
+ (
114
+ "scene_fact_members",
115
+ "idx_scene_fact_members_lookup",
116
+ "idx_scene_fact_members_order",
117
+ "trg_scene_fact_members_insert",
118
+ "trg_scene_fact_members_update",
119
+ "uq_scenes_profile_scene",
120
+ "uq_facts_profile_fact",
121
+ ),
122
+ ).fetchall()
123
+ }
124
+ return objects == {
125
+ ("scene_fact_members", "table"),
126
+ ("idx_scene_fact_members_lookup", "index"),
127
+ ("idx_scene_fact_members_order", "index"),
128
+ ("trg_scene_fact_members_insert", "trigger"),
129
+ ("trg_scene_fact_members_update", "trigger"),
130
+ ("uq_scenes_profile_scene", "index"),
131
+ ("uq_facts_profile_fact", "index"),
132
+ }
133
+
134
+
135
+ def repair(conn: sqlite3.Connection) -> None:
136
+ """Restore an accidentally dropped projection and re-backfill it."""
137
+ conn.executescript(DDL)
@@ -28,7 +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,
31
+ M038_learning_feedback_channel,
32
+ M039_scene_fact_members,
32
33
  )
33
34
 
34
35
  # ---------------------------------------------------------------------------
@@ -82,7 +83,8 @@ __all__ = (
82
83
  "M020_model_state_integrity",
83
84
  "M029_behavioral_history_indexes",
84
85
  "M030_entity_explorer_indexes",
85
- "M033_learning_feedback_channel",
86
+ "M038_learning_feedback_channel",
87
+ "M039_scene_fact_members",
86
88
  # Legacy re-exports (backward compat):
87
89
  "CURRENT_SCHEMA_VERSION",
88
90
  "get_schema_version",
@@ -43,6 +43,7 @@ _TABLES: Final[tuple[str, ...]] = (
43
43
  "entity_aliases",
44
44
  "entity_profiles",
45
45
  "memory_scenes",
46
+ "scene_fact_members",
46
47
  "temporal_events",
47
48
  "graph_edges",
48
49
  "consolidation_log",
@@ -179,6 +180,8 @@ CREATE TABLE IF NOT EXISTS atomic_facts (
179
180
  embedding TEXT,
180
181
  fisher_mean TEXT,
181
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,
182
185
 
183
186
  -- Lifecycle
184
187
  lifecycle TEXT NOT NULL DEFAULT 'active'
@@ -452,6 +455,67 @@ CREATE TABLE IF NOT EXISTS memory_scenes (
452
455
 
453
456
  CREATE INDEX IF NOT EXISTS idx_scenes_profile
454
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);
462
+ """
463
+
464
+
465
+ # ---------------------------------------------------------------------------
466
+ # Normalized scene/fact membership projection (bounded scene assignment)
467
+ # ---------------------------------------------------------------------------
468
+
469
+ _SQL_SCENE_FACT_MEMBERS: Final[str] = """
470
+ CREATE TABLE IF NOT EXISTS scene_fact_members (
471
+ profile_id TEXT NOT NULL,
472
+ scene_id TEXT NOT NULL,
473
+ fact_id TEXT NOT NULL,
474
+ position INTEGER NOT NULL DEFAULT 0,
475
+ PRIMARY KEY (scene_id, fact_id),
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,
480
+ FOREIGN KEY (profile_id) REFERENCES profiles(profile_id) ON DELETE CASCADE
481
+ );
482
+
483
+ CREATE INDEX IF NOT EXISTS idx_scene_fact_members_lookup
484
+ ON scene_fact_members (profile_id, fact_id, scene_id);
485
+ CREATE INDEX IF NOT EXISTS idx_scene_fact_members_order
486
+ ON scene_fact_members (scene_id, position);
487
+
488
+ CREATE TRIGGER IF NOT EXISTS trg_scene_fact_members_insert
489
+ AFTER INSERT ON memory_scenes
490
+ BEGIN
491
+ DELETE FROM scene_fact_members WHERE scene_id = NEW.scene_id;
492
+ INSERT OR IGNORE INTO scene_fact_members
493
+ (profile_id, scene_id, fact_id, position)
494
+ SELECT NEW.profile_id, NEW.scene_id, af.fact_id, CAST(member.key AS INTEGER)
495
+ FROM json_each(
496
+ CASE WHEN json_valid(NEW.fact_ids_json)
497
+ THEN NEW.fact_ids_json ELSE '[]' END
498
+ ) AS member
499
+ JOIN atomic_facts AS af
500
+ ON af.fact_id = member.value
501
+ AND af.profile_id = NEW.profile_id;
502
+ END;
503
+
504
+ CREATE TRIGGER IF NOT EXISTS trg_scene_fact_members_update
505
+ AFTER UPDATE OF profile_id, fact_ids_json ON memory_scenes
506
+ BEGIN
507
+ DELETE FROM scene_fact_members WHERE scene_id = NEW.scene_id;
508
+ INSERT OR IGNORE INTO scene_fact_members
509
+ (profile_id, scene_id, fact_id, position)
510
+ SELECT NEW.profile_id, NEW.scene_id, af.fact_id, CAST(member.key AS INTEGER)
511
+ FROM json_each(
512
+ CASE WHEN json_valid(NEW.fact_ids_json)
513
+ THEN NEW.fact_ids_json ELSE '[]' END
514
+ ) AS member
515
+ JOIN atomic_facts AS af
516
+ ON af.fact_id = member.value
517
+ AND af.profile_id = NEW.profile_id;
518
+ END;
455
519
  """
456
520
 
457
521
 
@@ -803,6 +867,7 @@ _DDL_ORDERED: Final[tuple[str, ...]] = (
803
867
  _SQL_ENTITY_ALIASES,
804
868
  _SQL_ENTITY_PROFILES,
805
869
  _SQL_MEMORY_SCENES,
870
+ _SQL_SCENE_FACT_MEMBERS,
806
871
  _SQL_TEMPORAL_EVENTS,
807
872
  _SQL_GRAPH_EDGES,
808
873
  _SQL_CONSOLIDATION_LOG,
@@ -887,6 +952,8 @@ def drop_all_tables(conn: sqlite3.Connection) -> None:
887
952
  "atomic_facts_fts_insert",
888
953
  "atomic_facts_fts_delete",
889
954
  "atomic_facts_fts_update",
955
+ "trg_scene_fact_members_insert",
956
+ "trg_scene_fact_members_update",
890
957
  ):
891
958
  conn.execute(f"DROP TRIGGER IF EXISTS {trigger}")
892
959
 
@@ -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 {