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,330 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+
4
+ from __future__ import annotations
5
+
6
+ import logging
7
+ import sqlite3
8
+ from collections.abc import Iterable, Mapping
9
+
10
+ from superlocalmemory.core.transactions.manifest import CompletionManifest
11
+ from superlocalmemory.core.transactions.obligations import ObligationLedger
12
+ from superlocalmemory.core.transactions.owners import (
13
+ ObligationKind,
14
+ ObligationState,
15
+ OperationContext,
16
+ ProjectionOwner,
17
+ )
18
+ from superlocalmemory.core.transactions.reconciler import Reconciler
19
+
20
+ logger = logging.getLogger("superlocalmemory.core.transactions.service")
21
+
22
+ MAX_APPLY_ATTEMPTS = 10
23
+
24
+
25
+ class MemoryTransactionService:
26
+ def __init__(
27
+ self,
28
+ owners: Mapping[str, ProjectionOwner] | None = None,
29
+ *,
30
+ ledger: ObligationLedger | None = None,
31
+ reconciler: Reconciler | None = None,
32
+ ) -> None:
33
+ self._owners: dict[str, ProjectionOwner] = dict(owners or {})
34
+ self._ledger = ledger or ObligationLedger()
35
+ self._reconciler = reconciler or Reconciler(self._ledger)
36
+
37
+ @property
38
+ def owners(self) -> Mapping[str, ProjectionOwner]:
39
+ return dict(self._owners)
40
+
41
+ def register(self, owner: ProjectionOwner) -> None:
42
+ self._owners[owner.name] = owner
43
+
44
+ def record(
45
+ self,
46
+ conn: sqlite3.Connection,
47
+ context: OperationContext,
48
+ *,
49
+ owners: Iterable[str] | None = None,
50
+ kind: ObligationKind = ObligationKind.APPLY,
51
+ ) -> None:
52
+ names = list(owners) if owners is not None else list(self._owners)
53
+ self._ledger.record_many(conn, context, names, kind)
54
+
55
+ def apply(
56
+ self, conn: sqlite3.Connection, context: OperationContext,
57
+ ) -> None:
58
+ for obligation in self._ledger.fetch(conn, context.operation_id):
59
+ if obligation.kind is not ObligationKind.APPLY:
60
+ continue
61
+ if (
62
+ obligation.state in (ObligationState.FAILED, ObligationState.APPLIED)
63
+ and obligation.attempts >= MAX_APPLY_ATTEMPTS
64
+ ):
65
+ continue
66
+ self._reconcile_owner(conn, context, obligation)
67
+
68
+ def erase(
69
+ self, conn: sqlite3.Connection, context: OperationContext,
70
+ ) -> None:
71
+ for obligation in self._ledger.fetch(conn, context.operation_id):
72
+ if obligation.kind is not ObligationKind.ERASE:
73
+ continue
74
+ if obligation.state is ObligationState.ERASED:
75
+ continue
76
+ self._erase_one(conn, context, obligation.owner)
77
+
78
+ def compensate(
79
+ self, conn: sqlite3.Connection, context: OperationContext, owner_name: str,
80
+ ) -> None:
81
+ owner = self._owners.get(owner_name)
82
+ if owner is None:
83
+ self._ledger.mark(
84
+ conn, context.operation_id, owner_name, ObligationKind.APPLY,
85
+ ObligationState.FAILED,
86
+ detail={"phase": "compensate", "error": "owner not registered"},
87
+ bump_attempts=True,
88
+ )
89
+ return
90
+ try:
91
+ result = owner.compensate(context)
92
+ except Exception as exc: # noqa: BLE001
93
+ self._ledger.mark(
94
+ conn, context.operation_id, owner_name, ObligationKind.APPLY,
95
+ ObligationState.FAILED,
96
+ detail={"phase": "compensate", "error": _err(exc)},
97
+ bump_attempts=True,
98
+ )
99
+ return
100
+ state = ObligationState.COMPENSATED if result.ok else ObligationState.FAILED
101
+ self._ledger.mark(
102
+ conn, context.operation_id, owner_name, ObligationKind.APPLY, state,
103
+ checksum=result.checksum,
104
+ detail={"phase": "compensate", **dict(result.detail)},
105
+ bump_attempts=True,
106
+ )
107
+
108
+ def reconcile(
109
+ self,
110
+ conn: sqlite3.Connection,
111
+ operation_id: str,
112
+ profile_id: str,
113
+ *,
114
+ canonical_committed: bool | None = None,
115
+ ) -> CompletionManifest:
116
+ return self._reconciler.reconcile(
117
+ conn, operation_id, profile_id,
118
+ canonical_committed=canonical_committed,
119
+ )
120
+
121
+ def run(
122
+ self,
123
+ conn: sqlite3.Connection,
124
+ context: OperationContext,
125
+ *,
126
+ canonical_committed: bool | None = None,
127
+ ) -> CompletionManifest:
128
+ self.apply(conn, context)
129
+ return self.reconcile(
130
+ conn, context.operation_id, context.profile_id,
131
+ canonical_committed=canonical_committed,
132
+ )
133
+
134
+ def reconcile_operation(
135
+ self,
136
+ db: object,
137
+ context: OperationContext,
138
+ *,
139
+ canonical_committed: bool | None = None,
140
+ ) -> CompletionManifest:
141
+ op = context.operation_id
142
+ with db.raw_connection() as conn:
143
+ obligations = self._ledger.fetch(conn, op)
144
+ for obligation in obligations:
145
+ if obligation.kind is not ObligationKind.APPLY:
146
+ continue
147
+ if (
148
+ obligation.state in (ObligationState.FAILED, ObligationState.APPLIED)
149
+ and obligation.attempts >= MAX_APPLY_ATTEMPTS
150
+ ):
151
+ continue
152
+ self._reconcile_owner_unlocked(db, context, obligation)
153
+ with db.raw_connection() as conn:
154
+ return self._reconciler.reconcile(
155
+ conn, op, context.profile_id,
156
+ canonical_committed=canonical_committed,
157
+ )
158
+
159
+ def _reconcile_owner_unlocked(
160
+ self, db: object, context: OperationContext, obligation: object,
161
+ ) -> None:
162
+ op = context.operation_id
163
+ owner_name = obligation.owner
164
+ owner = self._owners.get(owner_name)
165
+ if owner is None:
166
+ with db.raw_connection() as conn:
167
+ self._ledger.mark(
168
+ conn, op, owner_name, ObligationKind.APPLY,
169
+ ObligationState.FAILED,
170
+ detail={"phase": "apply", "error": "owner not registered"},
171
+ bump_attempts=True,
172
+ )
173
+ return
174
+ ok, checksum, detail = self._safe_verify(owner, context)
175
+ if (
176
+ ok
177
+ and obligation.state is ObligationState.VERIFIED
178
+ and obligation.checksum is not None
179
+ and checksum != obligation.checksum
180
+ ):
181
+ ok = False
182
+ detail = {"error": "projection drift: content changed since verification"}
183
+ if not ok:
184
+ applied_ok, a_checksum, a_detail = self._safe_apply(owner, context)
185
+ with db.raw_connection() as conn:
186
+ if applied_ok:
187
+ self._ledger.mark(
188
+ conn, op, owner_name, ObligationKind.APPLY,
189
+ ObligationState.APPLIED, checksum=a_checksum,
190
+ bump_attempts=True,
191
+ )
192
+ else:
193
+ self._ledger.mark(
194
+ conn, op, owner_name, ObligationKind.APPLY,
195
+ ObligationState.FAILED, checksum=a_checksum,
196
+ detail={"phase": "apply", **a_detail}, bump_attempts=True,
197
+ )
198
+ if applied_ok:
199
+ ok, checksum, detail = self._safe_verify(owner, context)
200
+ with db.raw_connection() as conn:
201
+ if ok:
202
+ updated = self._ledger.mark(
203
+ conn, op, owner_name, ObligationKind.APPLY,
204
+ ObligationState.VERIFIED, checksum=checksum,
205
+ bump_verify_attempts=True, set_verified_at=True,
206
+ )
207
+ else:
208
+ updated = self._ledger.mark(
209
+ conn, op, owner_name, ObligationKind.APPLY,
210
+ ObligationState.FAILED, checksum=checksum,
211
+ detail={"phase": "verify", **detail},
212
+ bump_verify_attempts=True,
213
+ )
214
+ if updated == 0:
215
+ logger.warning(
216
+ "obligation %s/%s missing during reconcile mark", op, owner_name,
217
+ )
218
+
219
+ @staticmethod
220
+ def _safe_verify(
221
+ owner: ProjectionOwner, context: OperationContext,
222
+ ) -> tuple[bool, str | None, dict]:
223
+ try:
224
+ result = owner.verify(context)
225
+ except Exception as exc: # noqa: BLE001
226
+ return False, None, {"error": _err(exc)}
227
+ return result.ok, result.checksum, dict(result.detail)
228
+
229
+ @staticmethod
230
+ def _safe_apply(
231
+ owner: ProjectionOwner, context: OperationContext,
232
+ ) -> tuple[bool, str | None, dict]:
233
+ try:
234
+ result = owner.apply(context)
235
+ except Exception as exc: # noqa: BLE001
236
+ return False, None, {"error": _err(exc)}
237
+ return result.ok, result.checksum, dict(result.detail)
238
+
239
+ def verify_manifest(
240
+ self, conn: sqlite3.Connection, operation_id: str,
241
+ ) -> bool:
242
+ return self._reconciler.verify_manifest(conn, operation_id)
243
+
244
+ def fetch_manifest(
245
+ self, conn: sqlite3.Connection, operation_id: str,
246
+ ) -> CompletionManifest | None:
247
+ return self._reconciler.fetch_manifest(conn, operation_id)
248
+
249
+ def _reconcile_owner(
250
+ self, conn: sqlite3.Connection, context: OperationContext, obligation: object,
251
+ ) -> None:
252
+ op = context.operation_id
253
+ owner_name = obligation.owner
254
+ owner = self._owners.get(owner_name)
255
+ if owner is None:
256
+ self._ledger.mark(
257
+ conn, op, owner_name, ObligationKind.APPLY, ObligationState.FAILED,
258
+ detail={"phase": "apply", "error": "owner not registered"},
259
+ bump_attempts=True,
260
+ )
261
+ return
262
+ ok, checksum, detail = self._safe_verify(owner, context)
263
+ if (
264
+ ok
265
+ and obligation.state is ObligationState.VERIFIED
266
+ and obligation.checksum is not None
267
+ and checksum != obligation.checksum
268
+ ):
269
+ ok = False
270
+ detail = {"error": "projection drift: content changed since verification"}
271
+ if not ok:
272
+ applied_ok, a_checksum, a_detail = self._safe_apply(owner, context)
273
+ if applied_ok:
274
+ self._ledger.mark(
275
+ conn, op, owner_name, ObligationKind.APPLY,
276
+ ObligationState.APPLIED, checksum=a_checksum, bump_attempts=True,
277
+ )
278
+ ok, checksum, detail = self._safe_verify(owner, context)
279
+ else:
280
+ self._ledger.mark(
281
+ conn, op, owner_name, ObligationKind.APPLY, ObligationState.FAILED,
282
+ checksum=a_checksum, detail={"phase": "apply", **a_detail},
283
+ bump_attempts=True,
284
+ )
285
+ if ok:
286
+ self._ledger.mark(
287
+ conn, op, owner_name, ObligationKind.APPLY, ObligationState.VERIFIED,
288
+ checksum=checksum, bump_verify_attempts=True, set_verified_at=True,
289
+ )
290
+ else:
291
+ self._ledger.mark(
292
+ conn, op, owner_name, ObligationKind.APPLY, ObligationState.FAILED,
293
+ checksum=checksum, detail={"phase": "verify", **detail},
294
+ bump_verify_attempts=True,
295
+ )
296
+
297
+ def _erase_one(
298
+ self, conn: sqlite3.Connection, context: OperationContext, owner_name: str,
299
+ ) -> None:
300
+ owner = self._owners.get(owner_name)
301
+ op = context.operation_id
302
+ if owner is None:
303
+ self._ledger.mark(
304
+ conn, op, owner_name, ObligationKind.ERASE, ObligationState.FAILED,
305
+ detail={"phase": "erase", "error": "owner not registered"},
306
+ bump_attempts=True,
307
+ )
308
+ return
309
+ try:
310
+ proof = owner.erase(context)
311
+ except Exception as exc: # noqa: BLE001
312
+ self._ledger.mark(
313
+ conn, op, owner_name, ObligationKind.ERASE, ObligationState.FAILED,
314
+ detail={"phase": "erase", "error": _err(exc)}, bump_attempts=True,
315
+ )
316
+ return
317
+ state = ObligationState.ERASED if proof.erased else ObligationState.FAILED
318
+ self._ledger.mark(
319
+ conn, op, owner_name, ObligationKind.ERASE, state,
320
+ checksum=proof.checksum,
321
+ detail={"phase": "erase", **dict(proof.detail)},
322
+ bump_attempts=True,
323
+ )
324
+
325
+
326
+ def _err(exc: BaseException) -> str:
327
+ return f"{type(exc).__name__}: {exc}"[:500]
328
+
329
+
330
+ __all__ = ["MemoryTransactionService"]
@@ -84,10 +84,12 @@ class WorkerPool:
84
84
  so the worker-side engine resolves the configured default — shared
85
85
  memory is opt-in.
86
86
  """
87
- msg = {
87
+ msg: dict = {
88
88
  "cmd": "recall", "query": query, "limit": limit,
89
- "session_id": session_id or "", "fast": bool(fast),
89
+ "session_id": session_id or "",
90
90
  }
91
+ if fast is not None:
92
+ msg["fast"] = bool(fast)
91
93
  if include_global is not None:
92
94
  msg["include_global"] = bool(include_global)
93
95
  if include_shared is not None:
@@ -232,12 +234,38 @@ class WorkerPool:
232
234
  def _readline_with_timeout(stream, timeout_seconds: float) -> str:
233
235
  """Read one line from *stream* with a timeout.
234
236
 
235
- Uses a daemon thread so the call never blocks the main thread
236
- indefinitely. This is the cross-platform replacement for
237
- ``selectors`` which fails on Windows pipes.
237
+ Prefer a deadline-driven selector poll of the stream's file descriptor
238
+ (POSIX pipes). That path never spawns a helper thread, so a hung
239
+ worker cannot leak reader threads or pin the pipe FD across timeouts.
240
+ A thread fallback remains only for streams without a usable fileno
241
+ (unit-test mocks) and for Windows, where selectors cannot wait on
242
+ pipes.
238
243
 
239
244
  Returns the line read, or ``""`` on timeout / error.
240
245
  """
246
+ import selectors
247
+
248
+ timeout_seconds = max(0.0, float(timeout_seconds))
249
+ fd: int | None
250
+ try:
251
+ raw_fd = stream.fileno()
252
+ fd = raw_fd if isinstance(raw_fd, int) else None
253
+ except (AttributeError, OSError, ValueError, TypeError):
254
+ fd = None
255
+
256
+ # Windows select()/selectors only accept sockets, not subprocess pipes.
257
+ if fd is not None and sys.platform != "win32":
258
+ try:
259
+ with selectors.DefaultSelector() as sel:
260
+ sel.register(fd, selectors.EVENT_READ)
261
+ events = sel.select(timeout=timeout_seconds)
262
+ if not events:
263
+ return ""
264
+ line = stream.readline()
265
+ return line if line else ""
266
+ except (OSError, ValueError):
267
+ return ""
268
+
241
269
  result_container: list[str] = []
242
270
  error_container: list[Exception] = []
243
271
 
@@ -716,46 +716,88 @@ class CognitiveConsolidator:
716
716
 
717
717
  Source facts are SOFT-ARCHIVED (HR-04), never deleted.
718
718
  Gist embedding stored at float32 (HR-05).
719
+
720
+ Ordering guarantee: the gist replacement is committed as a live
721
+ atomic fact BEFORE source facts are archived. This ensures there
722
+ is never a window where neither the sources nor the replacement
723
+ are recallable.
719
724
  """
720
725
  block_id = _new_id()
721
726
 
722
- # Generate gist embedding (full float32 precision — HR-05)
727
+ # Generate gist embedding BEFORE the transaction. Embedding generation
728
+ # is a pure CPU/ML computation with no DB side effects; running it outside
729
+ # the transaction avoids holding a write lock during a potentially slow
730
+ # model forward pass. If generation fails, we abort before any write.
731
+ # Note: gist_embedding_rowid is currently always None because vec0
732
+ # insertion requires VectorStore integration (future work). When that
733
+ # integration lands, the rowid write must move INSIDE the transaction
734
+ # below so a rollback removes the embedding row together with the block.
723
735
  gist_embedding_rowid: int | None = None
724
736
  if self._embedder is not None:
725
737
  try:
726
738
  self._embedder.encode(gist.gist_text)
727
- # Note: storing in vec0 requires VectorStore integration.
728
- # For now, the embedding is generated and the block records
729
- # that an embedder was available.
730
739
  except Exception as exc:
731
740
  logger.warning("Gist embedding generation failed: %s", exc)
732
741
 
733
- # Store the consolidated block
734
- self._db.store_ccq_block(
735
- block_id=block_id,
736
- profile_id=profile_id,
737
- content=gist.gist_text,
738
- source_fact_ids=json.dumps(list(cluster.fact_ids)),
739
- gist_embedding_rowid=gist_embedding_rowid,
740
- char_count=len(gist.gist_text),
741
- cluster_id=cluster.cluster_id,
742
- )
743
-
744
- # Archive source facts (HR-04: soft-archive, never delete) through the
745
- # lifecycle invariant writer.
742
+ from superlocalmemory.storage.models import AtomicFact, MemoryRecord
746
743
  from superlocalmemory.core.lifecycle_state import set_fact_lifecycle_zone
747
- set_fact_lifecycle_zone(
748
- self._db, cluster.fact_ids, "archive", profile_id=profile_id,
749
- )
750
- for fact_id in cluster.fact_ids:
751
- # Log access event
752
- self._db.execute(
753
- "INSERT INTO fact_access_log "
754
- "(log_id, fact_id, profile_id, accessed_at, "
755
- " access_type, session_id) "
756
- "VALUES (?, ?, ?, datetime('now'), 'consolidation', 'ccq')",
757
- (_new_id(), fact_id, profile_id),
744
+
745
+ # All four writes are wrapped in a single transaction so a mid-write
746
+ # failure (e.g. store_ccq_block raising) cannot leave an orphan gist
747
+ # AtomicFact with no corresponding ccq_consolidated_blocks row.
748
+ # set_fact_lifecycle_zone is transaction-aware and reuses the active
749
+ # connection rather than opening a nested one.
750
+ with self._db.transaction():
751
+ # --- Store replacement fact FIRST (ordering guarantee) ---
752
+ # The gist is persisted as a live atomic fact so it is immediately
753
+ # recallable. Source archival happens only after this write lands.
754
+ self._db.store_memory(
755
+ MemoryRecord(
756
+ memory_id=block_id,
757
+ profile_id=profile_id,
758
+ content=gist.gist_text,
759
+ session_id="ccq",
760
+ )
758
761
  )
762
+ self._db.store_fact(
763
+ AtomicFact(
764
+ memory_id=block_id,
765
+ profile_id=profile_id,
766
+ content=gist.gist_text,
767
+ canonical_entities=list(gist.key_entities),
768
+ importance=0.8,
769
+ confidence=1.0,
770
+ session_id="ccq",
771
+ )
772
+ )
773
+
774
+ # Store the consolidated block — must follow the gist fact INSERT so
775
+ # the step-1 identify filter (NOT IN ccq_consolidated_blocks) sees the
776
+ # correct state on any immediate re-run.
777
+ self._db.store_ccq_block(
778
+ block_id=block_id,
779
+ profile_id=profile_id,
780
+ content=gist.gist_text,
781
+ source_fact_ids=json.dumps(list(cluster.fact_ids)),
782
+ gist_embedding_rowid=gist_embedding_rowid,
783
+ char_count=len(gist.gist_text),
784
+ cluster_id=cluster.cluster_id,
785
+ )
786
+
787
+ # Archive source facts AFTER replacement is committed (HR-04: soft-archive, never delete)
788
+ set_fact_lifecycle_zone(
789
+ self._db, cluster.fact_ids, "archive", profile_id=profile_id,
790
+ )
791
+ for fact_id in cluster.fact_ids:
792
+ # Log access event
793
+ self._db.execute(
794
+ "INSERT INTO fact_access_log "
795
+ "(log_id, fact_id, profile_id, accessed_at, "
796
+ " access_type, session_id) "
797
+ "VALUES (?, ?, ?, datetime('now'), 'consolidation', 'ccq')",
798
+ (_new_id(), fact_id, profile_id),
799
+ )
800
+
759
801
  return block_id
760
802
 
761
803
  # ------------------------------------------------------------------
@@ -16,28 +16,89 @@ Part of Qualixar | Author: Varun Pratap Bhardwaj
16
16
  from __future__ import annotations
17
17
 
18
18
  import logging
19
- import math
19
+ import os
20
+ import threading
20
21
  from dataclasses import dataclass
21
22
 
22
23
  logger = logging.getLogger(__name__)
23
24
 
24
- _vader_analyzer = None
25
+ # ---------------------------------------------------------------------------
26
+ # Thread-safe, fork-safe VADER singleton
27
+ #
28
+ # Design notes:
29
+ # • _vader_analyzer = None means "not yet initialized for this process" OR
30
+ # "initialized but vaderSentiment is unavailable" (both cases return None
31
+ # from _get_vader, which causes tag_emotion to fall back to the keyword
32
+ # heuristic — the correct, observable behavior in both cases).
33
+ #
34
+ # • _vader_pid tracks which OS process last ran initialization. On fork()
35
+ # (e.g., under pytest-xdist, multiprocessing, or uwsgi) the child inherits
36
+ # the parent's _vader_analyzer pointer but the native SentimentIntensityAnalyzer
37
+ # object may reference memory that is no longer safe in the child's address
38
+ # space. Re-initializing per-PID guarantees each process gets a fresh, safe
39
+ # object constructed in its own address space.
40
+ #
41
+ # • _vader_lock (threading.Lock) prevents the double-initialization race in a
42
+ # multi-threaded caller (e.g., the ingestion thread-pool in store_pipeline.py
43
+ # where tag_emotion is called from multiple MaterializerWorker threads
44
+ # concurrently). Without the lock, Thread A and Thread B can both see
45
+ # _vader_analyzer is None, both enter the import block, and both try to load
46
+ # vaderSentiment's lexicon simultaneously — causing memory corruption when
47
+ # the native C-extension in the same process's numpy/BLAS layer is already
48
+ # running in a third thread (the LanceDB tokio background thread). The lock
49
+ # serializes VADER initialization so only one thread loads the lexicon.
50
+ #
51
+ # • After initialization, reads bypass the lock entirely (fast-path at top of
52
+ # _get_vader). The GIL guarantees atomic reads of _vader_pid and
53
+ # _vader_analyzer in CPython; writing _vader_pid AFTER _vader_analyzer
54
+ # (always, inside the lock) ensures a reader that sees a matching _vader_pid
55
+ # is guaranteed to also see the correctly initialized _vader_analyzer.
56
+ # ---------------------------------------------------------------------------
57
+
58
+ _vader_analyzer = None # None = uninitialized OR unavailable
59
+ _vader_pid: int | None = None # PID in which _vader_analyzer was initialized
60
+ _vader_lock = threading.Lock() # Serializes initialization only; not scoring
25
61
 
26
62
 
27
63
  def _get_vader():
28
- """Lazy-load VADER to avoid import cost on startup."""
29
- global _vader_analyzer
30
- if _vader_analyzer is not None:
64
+ """Lazy-load VADER thread-safe via lock, fork-safe via PID tracking.
65
+
66
+ Returns a SentimentIntensityAnalyzer instance, or None if vaderSentiment
67
+ is not installed. Callers must treat None as "fallback to keyword heuristic."
68
+ """
69
+ global _vader_analyzer, _vader_pid
70
+ current_pid = os.getpid()
71
+
72
+ # Fast-path: already initialized in this process — no lock needed.
73
+ # We read _vader_pid first; if it matches current_pid, _vader_analyzer was
74
+ # written before _vader_pid was set (see ordering in slow-path below),
75
+ # so the value we read is fully initialized.
76
+ if _vader_pid == current_pid:
31
77
  return _vader_analyzer
32
- try:
33
- import warnings
34
- with warnings.catch_warnings():
35
- warnings.filterwarnings("ignore", category=DeprecationWarning, module="vaderSentiment")
36
- from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer
37
- _vader_analyzer = SentimentIntensityAnalyzer()
38
- except ImportError:
39
- logger.warning("vaderSentiment not installed emotional tagging disabled")
40
- _vader_analyzer = None
78
+
79
+ # Slow-path: either first call in this process, or a forked child.
80
+ with _vader_lock:
81
+ # Double-checked locking: another thread may have raced us here.
82
+ if _vader_pid == current_pid:
83
+ return _vader_analyzer
84
+
85
+ # Initialize (or re-initialize) for this process.
86
+ try:
87
+ import warnings
88
+ with warnings.catch_warnings():
89
+ warnings.filterwarnings(
90
+ "ignore", category=DeprecationWarning, module="vaderSentiment",
91
+ )
92
+ from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer
93
+ _vader_analyzer = SentimentIntensityAnalyzer()
94
+ except ImportError:
95
+ logger.warning("vaderSentiment not installed — emotional tagging disabled")
96
+ _vader_analyzer = None
97
+
98
+ # Write _vader_pid LAST so that the fast-path can safely use it as a
99
+ # "initialization complete" signal.
100
+ _vader_pid = current_pid
101
+
41
102
  return _vader_analyzer
42
103
 
43
104