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.
- package/ATTRIBUTION.md +4 -4
- package/CHANGELOG.md +113 -121
- package/README.md +65 -63
- package/docs/pi-dev-integration.md +1 -1
- package/package.json +6 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +1 -1
- package/plugin/skills/slm-recall/SKILL.md +1 -1
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +1 -1
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-governance/SKILL.md +248 -0
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-loop/SKILL.md +99 -0
- package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
- package/plugin-src/skills/slm-profile/SKILL.md +148 -0
- package/plugin-src/skills/slm-recall/SKILL.md +1 -1
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-scope/SKILL.md +176 -0
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +11 -4
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +125 -11
- package/src/superlocalmemory/cli/daemon.py +5 -1
- package/src/superlocalmemory/cli/main.py +35 -2
- package/src/superlocalmemory/cli/ops_cmd.py +281 -0
- package/src/superlocalmemory/cli/setup_wizard.py +1 -1
- package/src/superlocalmemory/compliance/audit.py +65 -0
- package/src/superlocalmemory/compliance/eu_ai_act.py +27 -57
- package/src/superlocalmemory/compliance/gdpr.py +416 -20
- package/src/superlocalmemory/compliance/retention.py +74 -22
- package/src/superlocalmemory/compliance/scheduler.py +78 -9
- package/src/superlocalmemory/core/actor_context.py +166 -0
- package/src/superlocalmemory/core/admission.py +549 -0
- package/src/superlocalmemory/core/backend_orchestrator.py +23 -10
- package/src/superlocalmemory/core/config.py +202 -24
- package/src/superlocalmemory/core/consolidation_engine.py +13 -13
- package/src/superlocalmemory/core/context_cache.py +28 -0
- package/src/superlocalmemory/core/embeddings.py +64 -2
- package/src/superlocalmemory/core/engine.py +7 -2
- package/src/superlocalmemory/core/engine_ingestion.py +65 -3
- package/src/superlocalmemory/core/engine_wiring.py +36 -9
- package/src/superlocalmemory/core/ingest_policy.py +38 -0
- package/src/superlocalmemory/core/maintenance.py +255 -0
- package/src/superlocalmemory/core/modes.py +40 -13
- package/src/superlocalmemory/core/mutations.py +437 -44
- package/src/superlocalmemory/core/operation_policy.py +92 -0
- package/src/superlocalmemory/core/operation_policy_registry.py +542 -0
- package/src/superlocalmemory/core/operation_request.py +127 -0
- package/src/superlocalmemory/core/ops_remediation.py +542 -0
- package/src/superlocalmemory/core/recall_pipeline.py +7 -0
- package/src/superlocalmemory/core/remember_runtime.py +202 -4
- package/src/superlocalmemory/core/remote_mode.py +20 -5
- package/src/superlocalmemory/core/store_pipeline.py +150 -0
- package/src/superlocalmemory/core/topic_signature.py +19 -4
- package/src/superlocalmemory/core/transactions/__init__.py +78 -0
- package/src/superlocalmemory/core/transactions/concrete_owners.py +597 -0
- package/src/superlocalmemory/core/transactions/erasure.py +825 -0
- package/src/superlocalmemory/core/transactions/manifest.py +255 -0
- package/src/superlocalmemory/core/transactions/manifest_key.py +155 -0
- package/src/superlocalmemory/core/transactions/obligations.py +272 -0
- package/src/superlocalmemory/core/transactions/owners.py +114 -0
- package/src/superlocalmemory/core/transactions/reconciler.py +285 -0
- package/src/superlocalmemory/core/transactions/service.py +330 -0
- package/src/superlocalmemory/core/worker_pool.py +33 -5
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +70 -28
- package/src/superlocalmemory/encoding/emotional.py +75 -14
- package/src/superlocalmemory/encoding/scene_builder.py +115 -13
- package/src/superlocalmemory/encoding/temporal_parser.py +4 -0
- package/src/superlocalmemory/evolution/blind_verifier.py +11 -4
- package/src/superlocalmemory/evolution/evolution_store.py +244 -4
- package/src/superlocalmemory/evolution/llm_dispatch.py +40 -0
- package/src/superlocalmemory/evolution/model_selection.py +18 -3
- package/src/superlocalmemory/evolution/mutation_generator.py +3 -0
- package/src/superlocalmemory/evolution/skill_activator.py +270 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +281 -59
- package/src/superlocalmemory/evolution/types.py +30 -8
- package/src/superlocalmemory/graph/cozo_backend.py +17 -9
- package/src/superlocalmemory/hooks/auto_invoker.py +2 -1
- package/src/superlocalmemory/hooks/auto_recall.py +64 -30
- package/src/superlocalmemory/hooks/codex_assets.py +14 -1
- package/src/superlocalmemory/infra/backup.py +434 -7
- package/src/superlocalmemory/infra/process_reaper.py +18 -0
- package/src/superlocalmemory/infra/self_heal.py +401 -0
- package/src/superlocalmemory/learning/feedback.py +52 -9
- package/src/superlocalmemory/loops/engine.py +10 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
- package/src/superlocalmemory/mcp/http_transport.py +30 -331
- package/src/superlocalmemory/mcp/profiles.py +5 -0
- package/src/superlocalmemory/mcp/resources.py +8 -0
- package/src/superlocalmemory/mcp/server.py +51 -4
- package/src/superlocalmemory/mcp/shared.py +19 -0
- package/src/superlocalmemory/mcp/tools_active.py +25 -4
- package/src/superlocalmemory/mcp/tools_code_graph.py +26 -18
- package/src/superlocalmemory/mcp/tools_context.py +50 -8
- package/src/superlocalmemory/mcp/tools_core.py +69 -21
- package/src/superlocalmemory/mcp/tools_evolution.py +9 -2
- package/src/superlocalmemory/mcp/tools_learning.py +21 -10
- package/src/superlocalmemory/mcp/tools_loops.py +29 -18
- package/src/superlocalmemory/mcp/tools_mesh.py +8 -0
- package/src/superlocalmemory/mcp/tools_ops.py +115 -0
- package/src/superlocalmemory/mcp/tools_optimize.py +4 -0
- package/src/superlocalmemory/mcp/tools_v28.py +10 -3
- package/src/superlocalmemory/mcp/tools_v3.py +34 -14
- package/src/superlocalmemory/mcp/tools_v33.py +18 -33
- package/src/superlocalmemory/mesh/broker.py +124 -46
- package/src/superlocalmemory/mesh/broker_security.py +470 -0
- package/src/superlocalmemory/mesh/discovery.py +365 -0
- package/src/superlocalmemory/mesh/lock_protocol.py +313 -0
- package/src/superlocalmemory/mesh/node_identity.py +97 -0
- package/src/superlocalmemory/mesh/outbox_remote.py +429 -0
- package/src/superlocalmemory/mesh/remote_sync.py +511 -28
- package/src/superlocalmemory/mesh/state_sync.py +286 -0
- package/src/superlocalmemory/optimize/config/store.py +45 -0
- package/src/superlocalmemory/parameterization/cross_project.py +12 -0
- package/src/superlocalmemory/parameterization/prompt_injector.py +13 -11
- package/src/superlocalmemory/parameterization/prompt_lifecycle.py +8 -2
- package/src/superlocalmemory/parameterization/workflow_miner.py +17 -0
- package/src/superlocalmemory/retrieval/ann_index.py +5 -0
- package/src/superlocalmemory/retrieval/bm25_channel.py +49 -2
- package/src/superlocalmemory/retrieval/engine.py +19 -4
- package/src/superlocalmemory/retrieval/fusion.py +4 -1
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -3
- package/src/superlocalmemory/retrieval/remote_reranker.py +47 -22
- package/src/superlocalmemory/retrieval/reranker.py +32 -1
- package/src/superlocalmemory/retrieval/temporal_channel.py +16 -3
- package/src/superlocalmemory/retrieval/temporal_utils.py +107 -0
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +155 -42
- package/src/superlocalmemory/retrieval/vector_store.py +214 -8
- package/src/superlocalmemory/server/api.py +5 -5
- package/src/superlocalmemory/server/egress_policy.py +258 -0
- package/src/superlocalmemory/server/rbac_enforce.py +32 -0
- package/src/superlocalmemory/server/route_mutations.py +20 -0
- package/src/superlocalmemory/server/routes/compliance.py +153 -7
- package/src/superlocalmemory/server/routes/data_io.py +43 -2
- package/src/superlocalmemory/server/routes/events.py +15 -0
- package/src/superlocalmemory/server/routes/memories.py +56 -3
- package/src/superlocalmemory/server/routes/mesh.py +82 -1
- package/src/superlocalmemory/server/routes/mesh_lock.py +54 -0
- package/src/superlocalmemory/server/routes/mesh_state.py +63 -0
- package/src/superlocalmemory/server/routes/v3_api.py +50 -27
- package/src/superlocalmemory/server/routes/ws.py +86 -0
- package/src/superlocalmemory/server/ui.py +6 -6
- package/src/superlocalmemory/server/unified_daemon.py +942 -119
- package/src/superlocalmemory/storage/_migration_internals.py +568 -0
- package/src/superlocalmemory/storage/_schema_version.py +110 -0
- package/src/superlocalmemory/storage/database.py +329 -24
- package/src/superlocalmemory/storage/embedding_migrator.py +246 -51
- package/src/superlocalmemory/storage/erasure_fence.py +45 -0
- package/src/superlocalmemory/storage/generation_fence.py +63 -0
- package/src/superlocalmemory/storage/migration_runner.py +140 -417
- package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +40 -0
- package/src/superlocalmemory/storage/migrations/M033_projection_transactions.py +148 -0
- package/src/superlocalmemory/storage/migrations/M034_obligation_integrity.py +58 -0
- package/src/superlocalmemory/storage/migrations/M035_erasure_receipts.py +113 -0
- package/src/superlocalmemory/storage/migrations/M036_vector_row_map.py +107 -0
- package/src/superlocalmemory/storage/migrations/M037_manifest_hmac_version.py +162 -0
- package/src/superlocalmemory/storage/migrations/{M033_learning_feedback_channel.py → M038_learning_feedback_channel.py} +3 -3
- package/src/superlocalmemory/storage/migrations/M039_scene_fact_members.py +137 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +4 -2
- package/src/superlocalmemory/storage/schema.py +67 -0
- package/src/superlocalmemory/storage/write_coordinator.py +125 -0
- package/src/superlocalmemory/trust/scorer.py +28 -4
- package/src/superlocalmemory/ui/index.html +14 -3
- package/src/superlocalmemory/ui/js/auto-settings.js +12 -1
- package/src/superlocalmemory/ui/js/brain.js +6 -4
- package/src/superlocalmemory/ui/js/compliance.js +66 -12
- package/src/superlocalmemory/ui/js/dashboard.js +13 -3
- package/src/superlocalmemory/ui/js/feedback.js +8 -2
- package/src/superlocalmemory/ui/js/lifecycle.js +7 -1
- package/src/superlocalmemory/ui/js/modal.js +272 -5
- package/src/superlocalmemory/ui/js/od-backup.js +9 -2
- package/src/superlocalmemory/ui/js/od-compliance-ext.js +301 -0
- package/src/superlocalmemory/ui/js/od-operations.js +154 -23
- package/src/superlocalmemory/ui/js/od-ops-health.js +417 -0
- package/src/superlocalmemory/ui/js/od-optimize.js +35 -21
- package/src/superlocalmemory/ui/js/od-team.js +9 -2
- package/src/superlocalmemory/ui/js/optimize.js +13 -16
- package/src/superlocalmemory/ui/js/profiles.js +7 -3
- package/src/superlocalmemory/ui/js/settings.js +7 -1
- package/src/superlocalmemory/vector/lancedb_backend.py +19 -9
- package/src/superlocalmemory/attribution/mathematical_dna.py +0 -235
- package/src/superlocalmemory/cli/post_install.py +0 -114
- package/src/superlocalmemory/core/clock_monitor.py +0 -45
- package/src/superlocalmemory/core/db_pool.py +0 -80
- package/src/superlocalmemory/core/error_catalog.py +0 -113
- package/src/superlocalmemory/core/loop_watchdog.py +0 -56
- package/src/superlocalmemory/core/priority_queue.py +0 -61
- package/src/superlocalmemory/core/pruning_engine.py +0 -216
- package/src/superlocalmemory/core/queue_dispatcher.py +0 -73
- package/src/superlocalmemory/core/slmignore.py +0 -125
- package/src/superlocalmemory/infra/heartbeat_monitor.py +0 -140
- package/src/superlocalmemory/infra/webhook_dispatcher.py +0 -247
- package/src/superlocalmemory/learning/quantization_scheduler.py +0 -320
- 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 "",
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
748
|
-
|
|
749
|
-
)
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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
|
|
19
|
+
import os
|
|
20
|
+
import threading
|
|
20
21
|
from dataclasses import dataclass
|
|
21
22
|
|
|
22
23
|
logger = logging.getLogger(__name__)
|
|
23
24
|
|
|
24
|
-
|
|
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
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
_vader_analyzer
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|