superlocalmemory 3.8.14 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ATTRIBUTION.md +4 -4
- package/CHANGELOG.md +113 -137
- 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 +32 -5
- 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/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 -424
- 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/{M034_scene_fact_members.py → M039_scene_fact_members.py} +15 -5
- package/src/superlocalmemory/storage/migrations/__init__.py +4 -4
- package/src/superlocalmemory/storage/schema.py +10 -2
- 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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
2
|
# Licensed under AGPL-3.0-or-later
|
|
3
3
|
|
|
4
|
-
"""
|
|
4
|
+
"""M039 — normalized scene/fact membership for bounded assignment.
|
|
5
5
|
|
|
6
6
|
``memory_scenes.fact_ids_json`` remains the public compatibility format. This
|
|
7
7
|
additive projection provides the indexed reverse lookup needed to map nearest
|
|
@@ -13,7 +13,7 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
import sqlite3
|
|
15
15
|
|
|
16
|
-
NAME = "
|
|
16
|
+
NAME = "M039_scene_fact_members"
|
|
17
17
|
DB_TARGET = "memory"
|
|
18
18
|
|
|
19
19
|
DDL = """
|
|
@@ -32,6 +32,10 @@ CREATE TABLE IF NOT EXISTS memory_scenes (
|
|
|
32
32
|
FOREIGN KEY (profile_id) REFERENCES profiles(profile_id) ON DELETE CASCADE
|
|
33
33
|
);
|
|
34
34
|
CREATE INDEX IF NOT EXISTS idx_scenes_profile ON memory_scenes(profile_id);
|
|
35
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_scenes_profile_scene
|
|
36
|
+
ON memory_scenes (profile_id, scene_id);
|
|
37
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_facts_profile_fact
|
|
38
|
+
ON atomic_facts (profile_id, fact_id);
|
|
35
39
|
|
|
36
40
|
CREATE TABLE IF NOT EXISTS scene_fact_members (
|
|
37
41
|
profile_id TEXT NOT NULL,
|
|
@@ -39,8 +43,10 @@ CREATE TABLE IF NOT EXISTS scene_fact_members (
|
|
|
39
43
|
fact_id TEXT NOT NULL,
|
|
40
44
|
position INTEGER NOT NULL DEFAULT 0,
|
|
41
45
|
PRIMARY KEY (scene_id, fact_id),
|
|
42
|
-
FOREIGN KEY (
|
|
43
|
-
|
|
46
|
+
FOREIGN KEY (profile_id, scene_id)
|
|
47
|
+
REFERENCES memory_scenes(profile_id, scene_id) ON DELETE CASCADE,
|
|
48
|
+
FOREIGN KEY (profile_id, fact_id)
|
|
49
|
+
REFERENCES atomic_facts(profile_id, fact_id) ON DELETE CASCADE,
|
|
44
50
|
FOREIGN KEY (profile_id) REFERENCES profiles(profile_id) ON DELETE CASCADE
|
|
45
51
|
);
|
|
46
52
|
|
|
@@ -102,7 +108,7 @@ def verify(conn: sqlite3.Connection) -> bool:
|
|
|
102
108
|
(str(row[0]), str(row[1]))
|
|
103
109
|
for row in conn.execute(
|
|
104
110
|
"SELECT name, type FROM sqlite_master "
|
|
105
|
-
"WHERE name IN (?, ?, ?, ?, ?)"
|
|
111
|
+
"WHERE name IN (?, ?, ?, ?, ?, ?, ?)"
|
|
106
112
|
,
|
|
107
113
|
(
|
|
108
114
|
"scene_fact_members",
|
|
@@ -110,6 +116,8 @@ def verify(conn: sqlite3.Connection) -> bool:
|
|
|
110
116
|
"idx_scene_fact_members_order",
|
|
111
117
|
"trg_scene_fact_members_insert",
|
|
112
118
|
"trg_scene_fact_members_update",
|
|
119
|
+
"uq_scenes_profile_scene",
|
|
120
|
+
"uq_facts_profile_fact",
|
|
113
121
|
),
|
|
114
122
|
).fetchall()
|
|
115
123
|
}
|
|
@@ -119,6 +127,8 @@ def verify(conn: sqlite3.Connection) -> bool:
|
|
|
119
127
|
("idx_scene_fact_members_order", "index"),
|
|
120
128
|
("trg_scene_fact_members_insert", "trigger"),
|
|
121
129
|
("trg_scene_fact_members_update", "trigger"),
|
|
130
|
+
("uq_scenes_profile_scene", "index"),
|
|
131
|
+
("uq_facts_profile_fact", "index"),
|
|
122
132
|
}
|
|
123
133
|
|
|
124
134
|
|
|
@@ -28,8 +28,8 @@ from . import (
|
|
|
28
28
|
M020_model_state_integrity,
|
|
29
29
|
M029_behavioral_history_indexes,
|
|
30
30
|
M030_entity_explorer_indexes,
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
M038_learning_feedback_channel,
|
|
32
|
+
M039_scene_fact_members,
|
|
33
33
|
)
|
|
34
34
|
|
|
35
35
|
# ---------------------------------------------------------------------------
|
|
@@ -83,8 +83,8 @@ __all__ = (
|
|
|
83
83
|
"M020_model_state_integrity",
|
|
84
84
|
"M029_behavioral_history_indexes",
|
|
85
85
|
"M030_entity_explorer_indexes",
|
|
86
|
-
"
|
|
87
|
-
"
|
|
86
|
+
"M038_learning_feedback_channel",
|
|
87
|
+
"M039_scene_fact_members",
|
|
88
88
|
# Legacy re-exports (backward compat):
|
|
89
89
|
"CURRENT_SCHEMA_VERSION",
|
|
90
90
|
"get_schema_version",
|
|
@@ -180,6 +180,8 @@ CREATE TABLE IF NOT EXISTS atomic_facts (
|
|
|
180
180
|
embedding TEXT,
|
|
181
181
|
fisher_mean TEXT,
|
|
182
182
|
fisher_variance TEXT,
|
|
183
|
+
-- Tracks how many accesses had Fisher applied; delta-based update in maintenance
|
|
184
|
+
fisher_last_applied_access INTEGER NOT NULL DEFAULT 0,
|
|
183
185
|
|
|
184
186
|
-- Lifecycle
|
|
185
187
|
lifecycle TEXT NOT NULL DEFAULT 'active'
|
|
@@ -453,6 +455,10 @@ CREATE TABLE IF NOT EXISTS memory_scenes (
|
|
|
453
455
|
|
|
454
456
|
CREATE INDEX IF NOT EXISTS idx_scenes_profile
|
|
455
457
|
ON memory_scenes (profile_id);
|
|
458
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_scenes_profile_scene
|
|
459
|
+
ON memory_scenes (profile_id, scene_id);
|
|
460
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_facts_profile_fact
|
|
461
|
+
ON atomic_facts (profile_id, fact_id);
|
|
456
462
|
"""
|
|
457
463
|
|
|
458
464
|
|
|
@@ -467,8 +473,10 @@ CREATE TABLE IF NOT EXISTS scene_fact_members (
|
|
|
467
473
|
fact_id TEXT NOT NULL,
|
|
468
474
|
position INTEGER NOT NULL DEFAULT 0,
|
|
469
475
|
PRIMARY KEY (scene_id, fact_id),
|
|
470
|
-
FOREIGN KEY (
|
|
471
|
-
|
|
476
|
+
FOREIGN KEY (profile_id, scene_id)
|
|
477
|
+
REFERENCES memory_scenes(profile_id, scene_id) ON DELETE CASCADE,
|
|
478
|
+
FOREIGN KEY (profile_id, fact_id)
|
|
479
|
+
REFERENCES atomic_facts(profile_id, fact_id) ON DELETE CASCADE,
|
|
472
480
|
FOREIGN KEY (profile_id) REFERENCES profiles(profile_id) ON DELETE CASCADE
|
|
473
481
|
);
|
|
474
482
|
|
|
@@ -67,6 +67,16 @@ class CommandRejectedError(WriteCoordinatorError):
|
|
|
67
67
|
self.error_code = error_code
|
|
68
68
|
|
|
69
69
|
|
|
70
|
+
class WriterStalledError(WriteCoordinatorError):
|
|
71
|
+
"""Raised by submit()/execute() when the worker thread has stalled past STALL_THRESHOLD.
|
|
72
|
+
|
|
73
|
+
This is the circuit-breaker fast-fail: instead of enqueueing behind a dead worker
|
|
74
|
+
(causing every new caller to wait for their per-item timeout), new callers get
|
|
75
|
+
this immediate, actionable error. Admin remediation: check /operations/failed,
|
|
76
|
+
then restart the daemon if the stall persists.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
|
|
70
80
|
class Lane(StrEnum):
|
|
71
81
|
"""Scheduling lanes, ordered to protect foreground memory operations."""
|
|
72
82
|
|
|
@@ -236,12 +246,18 @@ class WriteCoordinator:
|
|
|
236
246
|
this coordinator.
|
|
237
247
|
"""
|
|
238
248
|
|
|
249
|
+
#: Default stall threshold in seconds. Well above the per-item 1s deadline.
|
|
250
|
+
#: The watchdog is inert for any item that completes within this window,
|
|
251
|
+
#: preserving healthy-path performance byte-for-byte.
|
|
252
|
+
_DEFAULT_STALL_THRESHOLD_S: float = 30.0
|
|
253
|
+
|
|
239
254
|
def __init__(
|
|
240
255
|
self,
|
|
241
256
|
db_path: str | Path,
|
|
242
257
|
*,
|
|
243
258
|
owner_id: str | None = None,
|
|
244
259
|
max_queue_depth: int = _MAX_QUEUE_DEPTH,
|
|
260
|
+
stall_threshold: float | None = None,
|
|
245
261
|
) -> None:
|
|
246
262
|
if max_queue_depth < 1:
|
|
247
263
|
raise ValueError("max_queue_depth must be at least one")
|
|
@@ -274,6 +290,15 @@ class WriteCoordinator:
|
|
|
274
290
|
self._worker_ident: int | None = None
|
|
275
291
|
self._capability_token = object()
|
|
276
292
|
self._handlers: dict[CommandKind, CommandHandler] = {}
|
|
293
|
+
# ---- Stall watchdog state (additive — inert on the healthy path) ----
|
|
294
|
+
# Protected by self._condition for thread-safety.
|
|
295
|
+
self._stall_threshold: float = (
|
|
296
|
+
stall_threshold if stall_threshold is not None else self._DEFAULT_STALL_THRESHOLD_S
|
|
297
|
+
)
|
|
298
|
+
self._inflight_started_at: float | None = None
|
|
299
|
+
self._inflight_op_id: str | None = None
|
|
300
|
+
self._writer_stalled: bool = False
|
|
301
|
+
self._stall_logged_once: bool = False
|
|
277
302
|
|
|
278
303
|
@property
|
|
279
304
|
def db_path(self) -> Path:
|
|
@@ -285,6 +310,46 @@ class WriteCoordinator:
|
|
|
285
310
|
"""Opaque daemon instance identifier recorded in the ownership lease."""
|
|
286
311
|
return self._owner_id
|
|
287
312
|
|
|
313
|
+
@property
|
|
314
|
+
def stall_threshold(self) -> float:
|
|
315
|
+
"""Seconds before an in-flight item is considered stalled."""
|
|
316
|
+
return self._stall_threshold
|
|
317
|
+
|
|
318
|
+
@property
|
|
319
|
+
def writer_stalled(self) -> bool:
|
|
320
|
+
"""True when the worker thread has exceeded stall_threshold on one item.
|
|
321
|
+
|
|
322
|
+
This is a dynamic check — computed from the current inflight age without
|
|
323
|
+
requiring a submitter to trigger _check_stall(). Safe to poll from any thread.
|
|
324
|
+
"""
|
|
325
|
+
with self._condition:
|
|
326
|
+
if self._writer_stalled:
|
|
327
|
+
return True
|
|
328
|
+
started = self._inflight_started_at
|
|
329
|
+
if started is None:
|
|
330
|
+
return False
|
|
331
|
+
return (time.monotonic() - started) > self._stall_threshold
|
|
332
|
+
|
|
333
|
+
def inflight_info(self) -> dict:
|
|
334
|
+
"""Return stall health snapshot for /health and get_status.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
{"stalled": bool, "op_id": str | None, "age_s": float | None}
|
|
338
|
+
|
|
339
|
+
Pure read — never raises.
|
|
340
|
+
"""
|
|
341
|
+
with self._condition:
|
|
342
|
+
started = self._inflight_started_at
|
|
343
|
+
stalled = self._writer_stalled
|
|
344
|
+
op_id = self._inflight_op_id
|
|
345
|
+
if started is None:
|
|
346
|
+
return {"stalled": False, "op_id": None, "age_s": None}
|
|
347
|
+
now = time.monotonic()
|
|
348
|
+
age = now - started
|
|
349
|
+
# Dynamic stall check (same logic as writer_stalled property)
|
|
350
|
+
is_stalled = stalled or (age > self._stall_threshold)
|
|
351
|
+
return {"stalled": is_stalled, "op_id": op_id, "age_s": round(age, 2)}
|
|
352
|
+
|
|
288
353
|
def claim_ownership(self) -> bool:
|
|
289
354
|
"""Claim the cross-platform owner lease without waiting on another daemon."""
|
|
290
355
|
if _portalocker is None:
|
|
@@ -407,6 +472,34 @@ class WriteCoordinator:
|
|
|
407
472
|
if context is not None:
|
|
408
473
|
context.__exit__(None, None, None)
|
|
409
474
|
|
|
475
|
+
def _check_stall(self) -> None:
|
|
476
|
+
"""Circuit-breaker check: raise WriterStalledError if worker is stalled.
|
|
477
|
+
|
|
478
|
+
Called from submit() and execute() BEFORE enqueueing. Inert on the
|
|
479
|
+
healthy path (started_at is None or within stall_threshold).
|
|
480
|
+
Thread-safe: reads only condition-protected state.
|
|
481
|
+
"""
|
|
482
|
+
with self._condition:
|
|
483
|
+
started = self._inflight_started_at
|
|
484
|
+
if started is None:
|
|
485
|
+
return
|
|
486
|
+
age = time.monotonic() - started
|
|
487
|
+
if age > self._stall_threshold:
|
|
488
|
+
if not self._stall_logged_once:
|
|
489
|
+
import logging as _log
|
|
490
|
+
_log.getLogger(__name__).critical(
|
|
491
|
+
"WriteCoordinator: worker thread stalled for %.1fs on op=%s; "
|
|
492
|
+
"new submitters will receive WriterStalledError",
|
|
493
|
+
age,
|
|
494
|
+
self._inflight_op_id,
|
|
495
|
+
)
|
|
496
|
+
self._stall_logged_once = True
|
|
497
|
+
self._writer_stalled = True
|
|
498
|
+
if self._writer_stalled:
|
|
499
|
+
raise WriterStalledError(
|
|
500
|
+
"write subsystem stalled; admin remediation required"
|
|
501
|
+
)
|
|
502
|
+
|
|
410
503
|
def execute(
|
|
411
504
|
self,
|
|
412
505
|
sql: str,
|
|
@@ -424,6 +517,7 @@ class WriteCoordinator:
|
|
|
424
517
|
raise ValueError("sql must be a non-empty statement")
|
|
425
518
|
if timeout <= 0:
|
|
426
519
|
raise ValueError("timeout must be greater than zero")
|
|
520
|
+
self._check_stall() # circuit-breaker (inert if no stall)
|
|
427
521
|
lane = self._coerce_lane(priority)
|
|
428
522
|
self.start()
|
|
429
523
|
item = _Execution(
|
|
@@ -467,6 +561,7 @@ class WriteCoordinator:
|
|
|
467
561
|
raise TypeError("command must be a WriteCommand")
|
|
468
562
|
if timeout <= 0:
|
|
469
563
|
raise ValueError("timeout must be greater than zero")
|
|
564
|
+
self._check_stall() # circuit-breaker (inert if no stall)
|
|
470
565
|
lane = self._coerce_lane(priority)
|
|
471
566
|
self.start()
|
|
472
567
|
item = _Execution(
|
|
@@ -569,8 +664,25 @@ class WriteCoordinator:
|
|
|
569
664
|
return Lane.FOREGROUND
|
|
570
665
|
|
|
571
666
|
def _execute_item(self, conn: sqlite3.Connection, item: _Execution) -> None:
|
|
667
|
+
# ------------------------------------------------------------------
|
|
668
|
+
# Stall watchdog: record inflight state so _check_stall() can detect
|
|
669
|
+
# a frozen worker. Cleared in the finally block. This guard is
|
|
670
|
+
# strictly ADDITIVE — the item execution path is unchanged.
|
|
671
|
+
# ------------------------------------------------------------------
|
|
672
|
+
op_id: str | None = None
|
|
673
|
+
if item.command is not None:
|
|
674
|
+
op_id = item.command.command_id
|
|
675
|
+
with self._condition:
|
|
676
|
+
self._inflight_started_at = time.monotonic()
|
|
677
|
+
self._inflight_op_id = op_id
|
|
678
|
+
|
|
572
679
|
if item.cancelled or time.monotonic() >= item.deadline:
|
|
573
680
|
item.error = WriteDeadlineExceededError("canonical write expired before execution")
|
|
681
|
+
with self._condition:
|
|
682
|
+
self._inflight_started_at = None
|
|
683
|
+
self._inflight_op_id = None
|
|
684
|
+
self._writer_stalled = False
|
|
685
|
+
self._stall_logged_once = False
|
|
574
686
|
item.completion.set()
|
|
575
687
|
return
|
|
576
688
|
remaining = max(0.0, item.deadline - time.monotonic())
|
|
@@ -578,6 +690,11 @@ class WriteCoordinator:
|
|
|
578
690
|
item.error = WriteDeadlineExceededError(
|
|
579
691
|
"canonical write expired waiting for its process lock"
|
|
580
692
|
)
|
|
693
|
+
with self._condition:
|
|
694
|
+
self._inflight_started_at = None
|
|
695
|
+
self._inflight_op_id = None
|
|
696
|
+
self._writer_stalled = False
|
|
697
|
+
self._stall_logged_once = False
|
|
581
698
|
item.completion.set()
|
|
582
699
|
return
|
|
583
700
|
try:
|
|
@@ -637,6 +754,13 @@ class WriteCoordinator:
|
|
|
637
754
|
item.error.__cause__ = exc
|
|
638
755
|
finally:
|
|
639
756
|
self._process_write_lock.release()
|
|
757
|
+
# Clear inflight state and reset stall breaker so next caller can proceed.
|
|
758
|
+
with self._condition:
|
|
759
|
+
self._inflight_started_at = None
|
|
760
|
+
self._inflight_op_id = None
|
|
761
|
+
if self._writer_stalled:
|
|
762
|
+
self._writer_stalled = False
|
|
763
|
+
self._stall_logged_once = False
|
|
640
764
|
item.completion.set()
|
|
641
765
|
|
|
642
766
|
def _execute_command(self, conn: sqlite3.Connection, command: WriteCommand) -> WriteResult:
|
|
@@ -800,4 +924,5 @@ __all__ = [
|
|
|
800
924
|
"WriteCoordinatorError",
|
|
801
925
|
"WriteDeadlineExceededError",
|
|
802
926
|
"WriteResult",
|
|
927
|
+
"WriterStalledError",
|
|
803
928
|
]
|
|
@@ -73,10 +73,34 @@ class TrustScorer:
|
|
|
73
73
|
return self._compute_trust(alpha, beta)
|
|
74
74
|
|
|
75
75
|
def get_fact_trust(self, fact_id: str, profile_id: str) -> float:
|
|
76
|
-
"""Get trust for a fact.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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="
|
|
53
|
-
<i class="bi bi-lock-fill" aria-hidden="true"></i> LOCAL
|
|
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
|
-
<
|
|
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 = '
|
|
198
|
+
if (mode === 'a') bannerDetail.textContent = 'Local inference — legal 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.
|
|
1070
|
-
'Reset
|
|
1071
|
-
|
|
1072
|
-
|
|
1069
|
+
const ok = await window.confirmDestructive({
|
|
1070
|
+
title: 'Reset learning data',
|
|
1071
|
+
target: 'All learned patterns and ranking signals',
|
|
1072
|
+
consequence: 'Memories will be preserved, but learned patterns and ranking signals will be deleted.',
|
|
1073
|
+
confirmLabel: 'Reset',
|
|
1074
|
+
});
|
|
1073
1075
|
if (!ok) return;
|
|
1074
1076
|
status.textContent = 'Resetting…';
|
|
1075
1077
|
try {
|
|
@@ -10,11 +10,9 @@ async function loadCompliance() {
|
|
|
10
10
|
var filterValue = filterEl ? filterEl.value : '';
|
|
11
11
|
|
|
12
12
|
try {
|
|
13
|
-
var
|
|
14
|
-
if (
|
|
15
|
-
var
|
|
16
|
-
if (!response.ok) throw new Error('HTTP ' + response.status);
|
|
17
|
-
var data = await response.json();
|
|
13
|
+
var statusResp = await fetch('/api/compliance/status');
|
|
14
|
+
if (!statusResp.ok) throw new Error('HTTP ' + statusResp.status);
|
|
15
|
+
var data = await statusResp.json();
|
|
18
16
|
_complianceData = data;
|
|
19
17
|
|
|
20
18
|
if (!data.available) {
|
|
@@ -24,20 +22,37 @@ async function loadCompliance() {
|
|
|
24
22
|
|
|
25
23
|
renderComplianceStats(data);
|
|
26
24
|
renderCompliancePolicies(data);
|
|
27
|
-
|
|
25
|
+
|
|
26
|
+
// Task C: fetch real audit trail from dedicated endpoint with filters
|
|
27
|
+
var auditUrl = '/api/compliance/audit?limit=100';
|
|
28
|
+
if (filterValue) auditUrl += '&event_type=' + encodeURIComponent(filterValue);
|
|
29
|
+
var auditResp = await fetch(auditUrl);
|
|
30
|
+
var auditData = auditResp.ok ? await auditResp.json() : null;
|
|
31
|
+
renderComplianceAudit(auditData || data);
|
|
28
32
|
|
|
29
33
|
var badge = document.getElementById('compliance-profile-badge');
|
|
30
34
|
if (badge) badge.textContent = data.active_profile || 'default';
|
|
35
|
+
|
|
36
|
+
// Task C: show chain-integrity indicator
|
|
37
|
+
var chainEl = document.getElementById('cp-chain-integrity');
|
|
38
|
+
if (chainEl && auditData) {
|
|
39
|
+
var ok = auditData.chain_verified !== false;
|
|
40
|
+
chainEl.innerHTML = ok
|
|
41
|
+
? '<span class="badge bg-success"><i class="bi bi-shield-check"></i> Chain verified</span>'
|
|
42
|
+
: '<span class="badge bg-danger"><i class="bi bi-shield-x"></i> Chain integrity issue</span>';
|
|
43
|
+
}
|
|
31
44
|
} catch (error) {
|
|
32
45
|
console.error('Error loading compliance:', error);
|
|
33
46
|
}
|
|
34
47
|
}
|
|
35
48
|
|
|
49
|
+
// Task D: Fix KPI field reads — /api/compliance/status returns top-level fields,
|
|
50
|
+
// NOT a nested .stats object. audit_events_count, retention_policies (array),
|
|
51
|
+
// abac_policies_count are all top-level.
|
|
36
52
|
function renderComplianceStats(data) {
|
|
37
|
-
|
|
38
|
-
animateCounter('cp-
|
|
39
|
-
animateCounter('cp-
|
|
40
|
-
animateCounter('cp-abac-count', stats.abac_count || 0);
|
|
53
|
+
animateCounter('cp-audit-count', data.audit_events_count || 0);
|
|
54
|
+
animateCounter('cp-retention-count', (data.retention_policies || []).length);
|
|
55
|
+
animateCounter('cp-abac-count', data.abac_policies_count || 0);
|
|
41
56
|
}
|
|
42
57
|
|
|
43
58
|
function renderCompliancePolicies(data) {
|
|
@@ -58,7 +73,7 @@ function renderCompliancePolicies(data) {
|
|
|
58
73
|
table.className = 'table table-sm table-hover mb-0';
|
|
59
74
|
var thead = document.createElement('thead');
|
|
60
75
|
var headRow = document.createElement('tr');
|
|
61
|
-
['Policy Name', 'Retention (days)', 'Category', 'Action', 'Created'].forEach(function(h) {
|
|
76
|
+
['Policy Name', 'Retention (days)', 'Category', 'Action', 'Created', ''].forEach(function(h) {
|
|
62
77
|
var th = document.createElement('th');
|
|
63
78
|
th.textContent = h;
|
|
64
79
|
headRow.appendChild(th);
|
|
@@ -106,16 +121,55 @@ function renderCompliancePolicies(data) {
|
|
|
106
121
|
dateCell.textContent = formatDate(pol.created_at || '');
|
|
107
122
|
row.appendChild(dateCell);
|
|
108
123
|
|
|
124
|
+
// Task E: Delete button per row — calls DELETE /api/compliance/retention-policy?name=
|
|
125
|
+
var actCell = document.createElement('td');
|
|
126
|
+
var delBtn = document.createElement('button');
|
|
127
|
+
delBtn.className = 'btn btn-sm btn-outline-danger';
|
|
128
|
+
delBtn.textContent = 'Delete';
|
|
129
|
+
delBtn.addEventListener('click', (function(policyName) {
|
|
130
|
+
return async function() {
|
|
131
|
+
var confirmed = await confirmDestructive({
|
|
132
|
+
title: 'Delete retention policy',
|
|
133
|
+
target: policyName,
|
|
134
|
+
consequence: 'This policy will stop applying to all memories.',
|
|
135
|
+
confirmLabel: 'Delete',
|
|
136
|
+
});
|
|
137
|
+
if (!confirmed) return;
|
|
138
|
+
delBtn.disabled = true;
|
|
139
|
+
try {
|
|
140
|
+
var r = await fetch(
|
|
141
|
+
'/api/compliance/retention-policy?name=' + encodeURIComponent(policyName),
|
|
142
|
+
{ method: 'DELETE' }
|
|
143
|
+
);
|
|
144
|
+
var d = await r.json().catch(function() { return {}; });
|
|
145
|
+
if (d.success !== false) {
|
|
146
|
+
showToast('Policy deleted.');
|
|
147
|
+
loadCompliance();
|
|
148
|
+
} else {
|
|
149
|
+
showToast((d.error || 'Delete failed.'));
|
|
150
|
+
delBtn.disabled = false;
|
|
151
|
+
}
|
|
152
|
+
} catch (e) {
|
|
153
|
+
showToast('Network error deleting policy.');
|
|
154
|
+
delBtn.disabled = false;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}(pol.name || '')));
|
|
158
|
+
actCell.appendChild(delBtn);
|
|
159
|
+
row.appendChild(actCell);
|
|
160
|
+
|
|
109
161
|
tbody.appendChild(row);
|
|
110
162
|
}
|
|
111
163
|
table.appendChild(tbody);
|
|
112
164
|
container.appendChild(table);
|
|
113
165
|
}
|
|
114
166
|
|
|
167
|
+
// Task C: renderComplianceAudit accepts data from either the status endpoint
|
|
168
|
+
// (recent_audit_events) or the dedicated audit endpoint (events). Tries both.
|
|
115
169
|
function renderComplianceAudit(data) {
|
|
116
170
|
var container = document.getElementById('compliance-audit-content');
|
|
117
171
|
if (!container) return;
|
|
118
|
-
var events = data.audit_events || [];
|
|
172
|
+
var events = data.events || data.recent_audit_events || data.audit_events || [];
|
|
119
173
|
container.textContent = '';
|
|
120
174
|
|
|
121
175
|
if (events.length === 0) {
|
|
@@ -354,11 +354,21 @@ async function loadDashboard() {
|
|
|
354
354
|
if (dashVer) dashVer.textContent = ver;
|
|
355
355
|
if (settVer) settVer.textContent = ver;
|
|
356
356
|
|
|
357
|
-
// OD dashboard subtitle
|
|
357
|
+
// OD dashboard subtitle — locality comes from the mode record via API
|
|
358
|
+
// (data.data_locality_label). Never hardcode a fixed locality claim;
|
|
359
|
+
// Mode C is provider-assisted and must not claim data never leaves.
|
|
358
360
|
var subtitle = document.getElementById('od-dash-subtitle');
|
|
359
361
|
if (subtitle && data.mode_name) {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
+
var locality = (data.data_locality_label || '').trim();
|
|
363
|
+
var parts = [
|
|
364
|
+
'Mode ' + data.mode.toUpperCase(),
|
|
365
|
+
data.mode_name,
|
|
366
|
+
];
|
|
367
|
+
if (locality) {
|
|
368
|
+
parts.push(locality);
|
|
369
|
+
}
|
|
370
|
+
parts.push('v' + (ver || '?'));
|
|
371
|
+
subtitle.textContent = parts.join(' · ');
|
|
362
372
|
}
|
|
363
373
|
|
|
364
374
|
// Update mode badge in sidebar (ng-premount hidden element)
|
|
@@ -312,8 +312,14 @@ function showPrivacyDetails() {
|
|
|
312
312
|
/**
|
|
313
313
|
* Reset all learning data.
|
|
314
314
|
*/
|
|
315
|
-
function resetLearningData() {
|
|
316
|
-
|
|
315
|
+
async function resetLearningData() {
|
|
316
|
+
var confirmed = await confirmDestructive({
|
|
317
|
+
title: 'Reset learning data',
|
|
318
|
+
target: 'All learned patterns and ranking signals',
|
|
319
|
+
consequence: 'Your memories will be preserved.',
|
|
320
|
+
confirmLabel: 'Reset',
|
|
321
|
+
});
|
|
322
|
+
if (!confirmed) return;
|
|
317
323
|
|
|
318
324
|
fetch('/api/learning/reset', {method: 'POST'})
|
|
319
325
|
.then(function(r) { return r.json(); })
|
|
@@ -299,7 +299,13 @@ async function compactDryRun() {
|
|
|
299
299
|
}
|
|
300
300
|
|
|
301
301
|
async function compactExecute() {
|
|
302
|
-
|
|
302
|
+
var confirmed = await confirmDestructive({
|
|
303
|
+
title: 'Apply compaction',
|
|
304
|
+
target: 'All eligible memories',
|
|
305
|
+
consequence: 'Transitions memories to lower lifecycle states.',
|
|
306
|
+
confirmLabel: 'Apply',
|
|
307
|
+
});
|
|
308
|
+
if (!confirmed) return;
|
|
303
309
|
try {
|
|
304
310
|
var response = await fetch('/api/lifecycle/compact', {
|
|
305
311
|
method: 'POST',
|