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,137 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
"""M039 — normalized scene/fact membership for bounded assignment.
|
|
5
|
+
|
|
6
|
+
``memory_scenes.fact_ids_json`` remains the public compatibility format. This
|
|
7
|
+
additive projection provides the indexed reverse lookup needed to map nearest
|
|
8
|
+
fact-vector hits back to candidate scenes without scanning every scene for
|
|
9
|
+
every ingested fact. Triggers keep all existing scene write paths synchronized.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import sqlite3
|
|
15
|
+
|
|
16
|
+
NAME = "M039_scene_fact_members"
|
|
17
|
+
DB_TARGET = "memory"
|
|
18
|
+
|
|
19
|
+
DDL = """
|
|
20
|
+
BEGIN IMMEDIATE;
|
|
21
|
+
|
|
22
|
+
-- Keep the deferred migration independently safe for partial/legacy installs.
|
|
23
|
+
-- On normal daemon startup MemoryEngine has already created this table.
|
|
24
|
+
CREATE TABLE IF NOT EXISTS memory_scenes (
|
|
25
|
+
scene_id TEXT PRIMARY KEY,
|
|
26
|
+
profile_id TEXT NOT NULL DEFAULT 'default',
|
|
27
|
+
theme TEXT NOT NULL DEFAULT '',
|
|
28
|
+
fact_ids_json TEXT NOT NULL DEFAULT '[]',
|
|
29
|
+
entity_ids_json TEXT NOT NULL DEFAULT '[]',
|
|
30
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
31
|
+
last_updated TEXT NOT NULL DEFAULT (datetime('now')),
|
|
32
|
+
FOREIGN KEY (profile_id) REFERENCES profiles(profile_id) ON DELETE CASCADE
|
|
33
|
+
);
|
|
34
|
+
CREATE INDEX IF NOT EXISTS idx_scenes_profile ON memory_scenes(profile_id);
|
|
35
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_scenes_profile_scene
|
|
36
|
+
ON memory_scenes (profile_id, scene_id);
|
|
37
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_facts_profile_fact
|
|
38
|
+
ON atomic_facts (profile_id, fact_id);
|
|
39
|
+
|
|
40
|
+
CREATE TABLE IF NOT EXISTS scene_fact_members (
|
|
41
|
+
profile_id TEXT NOT NULL,
|
|
42
|
+
scene_id TEXT NOT NULL,
|
|
43
|
+
fact_id TEXT NOT NULL,
|
|
44
|
+
position INTEGER NOT NULL DEFAULT 0,
|
|
45
|
+
PRIMARY KEY (scene_id, fact_id),
|
|
46
|
+
FOREIGN KEY (profile_id, scene_id)
|
|
47
|
+
REFERENCES memory_scenes(profile_id, scene_id) ON DELETE CASCADE,
|
|
48
|
+
FOREIGN KEY (profile_id, fact_id)
|
|
49
|
+
REFERENCES atomic_facts(profile_id, fact_id) ON DELETE CASCADE,
|
|
50
|
+
FOREIGN KEY (profile_id) REFERENCES profiles(profile_id) ON DELETE CASCADE
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_scene_fact_members_lookup
|
|
54
|
+
ON scene_fact_members (profile_id, fact_id, scene_id);
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_scene_fact_members_order
|
|
56
|
+
ON scene_fact_members (scene_id, position);
|
|
57
|
+
|
|
58
|
+
CREATE TRIGGER IF NOT EXISTS trg_scene_fact_members_insert
|
|
59
|
+
AFTER INSERT ON memory_scenes
|
|
60
|
+
BEGIN
|
|
61
|
+
DELETE FROM scene_fact_members WHERE scene_id = NEW.scene_id;
|
|
62
|
+
INSERT OR IGNORE INTO scene_fact_members
|
|
63
|
+
(profile_id, scene_id, fact_id, position)
|
|
64
|
+
SELECT NEW.profile_id, NEW.scene_id, af.fact_id, CAST(member.key AS INTEGER)
|
|
65
|
+
FROM json_each(
|
|
66
|
+
CASE WHEN json_valid(NEW.fact_ids_json)
|
|
67
|
+
THEN NEW.fact_ids_json ELSE '[]' END
|
|
68
|
+
) AS member
|
|
69
|
+
JOIN atomic_facts AS af
|
|
70
|
+
ON af.fact_id = member.value
|
|
71
|
+
AND af.profile_id = NEW.profile_id;
|
|
72
|
+
END;
|
|
73
|
+
|
|
74
|
+
CREATE TRIGGER IF NOT EXISTS trg_scene_fact_members_update
|
|
75
|
+
AFTER UPDATE OF profile_id, fact_ids_json ON memory_scenes
|
|
76
|
+
BEGIN
|
|
77
|
+
DELETE FROM scene_fact_members WHERE scene_id = NEW.scene_id;
|
|
78
|
+
INSERT OR IGNORE INTO scene_fact_members
|
|
79
|
+
(profile_id, scene_id, fact_id, position)
|
|
80
|
+
SELECT NEW.profile_id, NEW.scene_id, af.fact_id, CAST(member.key AS INTEGER)
|
|
81
|
+
FROM json_each(
|
|
82
|
+
CASE WHEN json_valid(NEW.fact_ids_json)
|
|
83
|
+
THEN NEW.fact_ids_json ELSE '[]' END
|
|
84
|
+
) AS member
|
|
85
|
+
JOIN atomic_facts AS af
|
|
86
|
+
ON af.fact_id = member.value
|
|
87
|
+
AND af.profile_id = NEW.profile_id;
|
|
88
|
+
END;
|
|
89
|
+
|
|
90
|
+
INSERT OR IGNORE INTO scene_fact_members
|
|
91
|
+
(profile_id, scene_id, fact_id, position)
|
|
92
|
+
SELECT ms.profile_id, ms.scene_id, af.fact_id, CAST(member.key AS INTEGER)
|
|
93
|
+
FROM memory_scenes AS ms
|
|
94
|
+
JOIN json_each(
|
|
95
|
+
CASE WHEN json_valid(ms.fact_ids_json) THEN ms.fact_ids_json ELSE '[]' END
|
|
96
|
+
) AS member
|
|
97
|
+
JOIN atomic_facts AS af
|
|
98
|
+
ON af.fact_id = member.value
|
|
99
|
+
AND af.profile_id = ms.profile_id;
|
|
100
|
+
|
|
101
|
+
COMMIT;
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def verify(conn: sqlite3.Connection) -> bool:
|
|
106
|
+
"""Verify the table, covering indexes, and synchronization triggers."""
|
|
107
|
+
objects = {
|
|
108
|
+
(str(row[0]), str(row[1]))
|
|
109
|
+
for row in conn.execute(
|
|
110
|
+
"SELECT name, type FROM sqlite_master "
|
|
111
|
+
"WHERE name IN (?, ?, ?, ?, ?, ?, ?)"
|
|
112
|
+
,
|
|
113
|
+
(
|
|
114
|
+
"scene_fact_members",
|
|
115
|
+
"idx_scene_fact_members_lookup",
|
|
116
|
+
"idx_scene_fact_members_order",
|
|
117
|
+
"trg_scene_fact_members_insert",
|
|
118
|
+
"trg_scene_fact_members_update",
|
|
119
|
+
"uq_scenes_profile_scene",
|
|
120
|
+
"uq_facts_profile_fact",
|
|
121
|
+
),
|
|
122
|
+
).fetchall()
|
|
123
|
+
}
|
|
124
|
+
return objects == {
|
|
125
|
+
("scene_fact_members", "table"),
|
|
126
|
+
("idx_scene_fact_members_lookup", "index"),
|
|
127
|
+
("idx_scene_fact_members_order", "index"),
|
|
128
|
+
("trg_scene_fact_members_insert", "trigger"),
|
|
129
|
+
("trg_scene_fact_members_update", "trigger"),
|
|
130
|
+
("uq_scenes_profile_scene", "index"),
|
|
131
|
+
("uq_facts_profile_fact", "index"),
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def repair(conn: sqlite3.Connection) -> None:
|
|
136
|
+
"""Restore an accidentally dropped projection and re-backfill it."""
|
|
137
|
+
conn.executescript(DDL)
|
|
@@ -28,7 +28,8 @@ from . import (
|
|
|
28
28
|
M020_model_state_integrity,
|
|
29
29
|
M029_behavioral_history_indexes,
|
|
30
30
|
M030_entity_explorer_indexes,
|
|
31
|
-
|
|
31
|
+
M038_learning_feedback_channel,
|
|
32
|
+
M039_scene_fact_members,
|
|
32
33
|
)
|
|
33
34
|
|
|
34
35
|
# ---------------------------------------------------------------------------
|
|
@@ -82,7 +83,8 @@ __all__ = (
|
|
|
82
83
|
"M020_model_state_integrity",
|
|
83
84
|
"M029_behavioral_history_indexes",
|
|
84
85
|
"M030_entity_explorer_indexes",
|
|
85
|
-
"
|
|
86
|
+
"M038_learning_feedback_channel",
|
|
87
|
+
"M039_scene_fact_members",
|
|
86
88
|
# Legacy re-exports (backward compat):
|
|
87
89
|
"CURRENT_SCHEMA_VERSION",
|
|
88
90
|
"get_schema_version",
|
|
@@ -43,6 +43,7 @@ _TABLES: Final[tuple[str, ...]] = (
|
|
|
43
43
|
"entity_aliases",
|
|
44
44
|
"entity_profiles",
|
|
45
45
|
"memory_scenes",
|
|
46
|
+
"scene_fact_members",
|
|
46
47
|
"temporal_events",
|
|
47
48
|
"graph_edges",
|
|
48
49
|
"consolidation_log",
|
|
@@ -179,6 +180,8 @@ CREATE TABLE IF NOT EXISTS atomic_facts (
|
|
|
179
180
|
embedding TEXT,
|
|
180
181
|
fisher_mean TEXT,
|
|
181
182
|
fisher_variance TEXT,
|
|
183
|
+
-- Tracks how many accesses had Fisher applied; delta-based update in maintenance
|
|
184
|
+
fisher_last_applied_access INTEGER NOT NULL DEFAULT 0,
|
|
182
185
|
|
|
183
186
|
-- Lifecycle
|
|
184
187
|
lifecycle TEXT NOT NULL DEFAULT 'active'
|
|
@@ -452,6 +455,67 @@ CREATE TABLE IF NOT EXISTS memory_scenes (
|
|
|
452
455
|
|
|
453
456
|
CREATE INDEX IF NOT EXISTS idx_scenes_profile
|
|
454
457
|
ON memory_scenes (profile_id);
|
|
458
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_scenes_profile_scene
|
|
459
|
+
ON memory_scenes (profile_id, scene_id);
|
|
460
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_facts_profile_fact
|
|
461
|
+
ON atomic_facts (profile_id, fact_id);
|
|
462
|
+
"""
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
# ---------------------------------------------------------------------------
|
|
466
|
+
# Normalized scene/fact membership projection (bounded scene assignment)
|
|
467
|
+
# ---------------------------------------------------------------------------
|
|
468
|
+
|
|
469
|
+
_SQL_SCENE_FACT_MEMBERS: Final[str] = """
|
|
470
|
+
CREATE TABLE IF NOT EXISTS scene_fact_members (
|
|
471
|
+
profile_id TEXT NOT NULL,
|
|
472
|
+
scene_id TEXT NOT NULL,
|
|
473
|
+
fact_id TEXT NOT NULL,
|
|
474
|
+
position INTEGER NOT NULL DEFAULT 0,
|
|
475
|
+
PRIMARY KEY (scene_id, fact_id),
|
|
476
|
+
FOREIGN KEY (profile_id, scene_id)
|
|
477
|
+
REFERENCES memory_scenes(profile_id, scene_id) ON DELETE CASCADE,
|
|
478
|
+
FOREIGN KEY (profile_id, fact_id)
|
|
479
|
+
REFERENCES atomic_facts(profile_id, fact_id) ON DELETE CASCADE,
|
|
480
|
+
FOREIGN KEY (profile_id) REFERENCES profiles(profile_id) ON DELETE CASCADE
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
CREATE INDEX IF NOT EXISTS idx_scene_fact_members_lookup
|
|
484
|
+
ON scene_fact_members (profile_id, fact_id, scene_id);
|
|
485
|
+
CREATE INDEX IF NOT EXISTS idx_scene_fact_members_order
|
|
486
|
+
ON scene_fact_members (scene_id, position);
|
|
487
|
+
|
|
488
|
+
CREATE TRIGGER IF NOT EXISTS trg_scene_fact_members_insert
|
|
489
|
+
AFTER INSERT ON memory_scenes
|
|
490
|
+
BEGIN
|
|
491
|
+
DELETE FROM scene_fact_members WHERE scene_id = NEW.scene_id;
|
|
492
|
+
INSERT OR IGNORE INTO scene_fact_members
|
|
493
|
+
(profile_id, scene_id, fact_id, position)
|
|
494
|
+
SELECT NEW.profile_id, NEW.scene_id, af.fact_id, CAST(member.key AS INTEGER)
|
|
495
|
+
FROM json_each(
|
|
496
|
+
CASE WHEN json_valid(NEW.fact_ids_json)
|
|
497
|
+
THEN NEW.fact_ids_json ELSE '[]' END
|
|
498
|
+
) AS member
|
|
499
|
+
JOIN atomic_facts AS af
|
|
500
|
+
ON af.fact_id = member.value
|
|
501
|
+
AND af.profile_id = NEW.profile_id;
|
|
502
|
+
END;
|
|
503
|
+
|
|
504
|
+
CREATE TRIGGER IF NOT EXISTS trg_scene_fact_members_update
|
|
505
|
+
AFTER UPDATE OF profile_id, fact_ids_json ON memory_scenes
|
|
506
|
+
BEGIN
|
|
507
|
+
DELETE FROM scene_fact_members WHERE scene_id = NEW.scene_id;
|
|
508
|
+
INSERT OR IGNORE INTO scene_fact_members
|
|
509
|
+
(profile_id, scene_id, fact_id, position)
|
|
510
|
+
SELECT NEW.profile_id, NEW.scene_id, af.fact_id, CAST(member.key AS INTEGER)
|
|
511
|
+
FROM json_each(
|
|
512
|
+
CASE WHEN json_valid(NEW.fact_ids_json)
|
|
513
|
+
THEN NEW.fact_ids_json ELSE '[]' END
|
|
514
|
+
) AS member
|
|
515
|
+
JOIN atomic_facts AS af
|
|
516
|
+
ON af.fact_id = member.value
|
|
517
|
+
AND af.profile_id = NEW.profile_id;
|
|
518
|
+
END;
|
|
455
519
|
"""
|
|
456
520
|
|
|
457
521
|
|
|
@@ -803,6 +867,7 @@ _DDL_ORDERED: Final[tuple[str, ...]] = (
|
|
|
803
867
|
_SQL_ENTITY_ALIASES,
|
|
804
868
|
_SQL_ENTITY_PROFILES,
|
|
805
869
|
_SQL_MEMORY_SCENES,
|
|
870
|
+
_SQL_SCENE_FACT_MEMBERS,
|
|
806
871
|
_SQL_TEMPORAL_EVENTS,
|
|
807
872
|
_SQL_GRAPH_EDGES,
|
|
808
873
|
_SQL_CONSOLIDATION_LOG,
|
|
@@ -887,6 +952,8 @@ def drop_all_tables(conn: sqlite3.Connection) -> None:
|
|
|
887
952
|
"atomic_facts_fts_insert",
|
|
888
953
|
"atomic_facts_fts_delete",
|
|
889
954
|
"atomic_facts_fts_update",
|
|
955
|
+
"trg_scene_fact_members_insert",
|
|
956
|
+
"trg_scene_fact_members_update",
|
|
890
957
|
):
|
|
891
958
|
conn.execute(f"DROP TRIGGER IF EXISTS {trigger}")
|
|
892
959
|
|
|
@@ -67,6 +67,16 @@ class CommandRejectedError(WriteCoordinatorError):
|
|
|
67
67
|
self.error_code = error_code
|
|
68
68
|
|
|
69
69
|
|
|
70
|
+
class WriterStalledError(WriteCoordinatorError):
|
|
71
|
+
"""Raised by submit()/execute() when the worker thread has stalled past STALL_THRESHOLD.
|
|
72
|
+
|
|
73
|
+
This is the circuit-breaker fast-fail: instead of enqueueing behind a dead worker
|
|
74
|
+
(causing every new caller to wait for their per-item timeout), new callers get
|
|
75
|
+
this immediate, actionable error. Admin remediation: check /operations/failed,
|
|
76
|
+
then restart the daemon if the stall persists.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
|
|
70
80
|
class Lane(StrEnum):
|
|
71
81
|
"""Scheduling lanes, ordered to protect foreground memory operations."""
|
|
72
82
|
|
|
@@ -236,12 +246,18 @@ class WriteCoordinator:
|
|
|
236
246
|
this coordinator.
|
|
237
247
|
"""
|
|
238
248
|
|
|
249
|
+
#: Default stall threshold in seconds. Well above the per-item 1s deadline.
|
|
250
|
+
#: The watchdog is inert for any item that completes within this window,
|
|
251
|
+
#: preserving healthy-path performance byte-for-byte.
|
|
252
|
+
_DEFAULT_STALL_THRESHOLD_S: float = 30.0
|
|
253
|
+
|
|
239
254
|
def __init__(
|
|
240
255
|
self,
|
|
241
256
|
db_path: str | Path,
|
|
242
257
|
*,
|
|
243
258
|
owner_id: str | None = None,
|
|
244
259
|
max_queue_depth: int = _MAX_QUEUE_DEPTH,
|
|
260
|
+
stall_threshold: float | None = None,
|
|
245
261
|
) -> None:
|
|
246
262
|
if max_queue_depth < 1:
|
|
247
263
|
raise ValueError("max_queue_depth must be at least one")
|
|
@@ -274,6 +290,15 @@ class WriteCoordinator:
|
|
|
274
290
|
self._worker_ident: int | None = None
|
|
275
291
|
self._capability_token = object()
|
|
276
292
|
self._handlers: dict[CommandKind, CommandHandler] = {}
|
|
293
|
+
# ---- Stall watchdog state (additive — inert on the healthy path) ----
|
|
294
|
+
# Protected by self._condition for thread-safety.
|
|
295
|
+
self._stall_threshold: float = (
|
|
296
|
+
stall_threshold if stall_threshold is not None else self._DEFAULT_STALL_THRESHOLD_S
|
|
297
|
+
)
|
|
298
|
+
self._inflight_started_at: float | None = None
|
|
299
|
+
self._inflight_op_id: str | None = None
|
|
300
|
+
self._writer_stalled: bool = False
|
|
301
|
+
self._stall_logged_once: bool = False
|
|
277
302
|
|
|
278
303
|
@property
|
|
279
304
|
def db_path(self) -> Path:
|
|
@@ -285,6 +310,46 @@ class WriteCoordinator:
|
|
|
285
310
|
"""Opaque daemon instance identifier recorded in the ownership lease."""
|
|
286
311
|
return self._owner_id
|
|
287
312
|
|
|
313
|
+
@property
|
|
314
|
+
def stall_threshold(self) -> float:
|
|
315
|
+
"""Seconds before an in-flight item is considered stalled."""
|
|
316
|
+
return self._stall_threshold
|
|
317
|
+
|
|
318
|
+
@property
|
|
319
|
+
def writer_stalled(self) -> bool:
|
|
320
|
+
"""True when the worker thread has exceeded stall_threshold on one item.
|
|
321
|
+
|
|
322
|
+
This is a dynamic check — computed from the current inflight age without
|
|
323
|
+
requiring a submitter to trigger _check_stall(). Safe to poll from any thread.
|
|
324
|
+
"""
|
|
325
|
+
with self._condition:
|
|
326
|
+
if self._writer_stalled:
|
|
327
|
+
return True
|
|
328
|
+
started = self._inflight_started_at
|
|
329
|
+
if started is None:
|
|
330
|
+
return False
|
|
331
|
+
return (time.monotonic() - started) > self._stall_threshold
|
|
332
|
+
|
|
333
|
+
def inflight_info(self) -> dict:
|
|
334
|
+
"""Return stall health snapshot for /health and get_status.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
{"stalled": bool, "op_id": str | None, "age_s": float | None}
|
|
338
|
+
|
|
339
|
+
Pure read — never raises.
|
|
340
|
+
"""
|
|
341
|
+
with self._condition:
|
|
342
|
+
started = self._inflight_started_at
|
|
343
|
+
stalled = self._writer_stalled
|
|
344
|
+
op_id = self._inflight_op_id
|
|
345
|
+
if started is None:
|
|
346
|
+
return {"stalled": False, "op_id": None, "age_s": None}
|
|
347
|
+
now = time.monotonic()
|
|
348
|
+
age = now - started
|
|
349
|
+
# Dynamic stall check (same logic as writer_stalled property)
|
|
350
|
+
is_stalled = stalled or (age > self._stall_threshold)
|
|
351
|
+
return {"stalled": is_stalled, "op_id": op_id, "age_s": round(age, 2)}
|
|
352
|
+
|
|
288
353
|
def claim_ownership(self) -> bool:
|
|
289
354
|
"""Claim the cross-platform owner lease without waiting on another daemon."""
|
|
290
355
|
if _portalocker is None:
|
|
@@ -407,6 +472,34 @@ class WriteCoordinator:
|
|
|
407
472
|
if context is not None:
|
|
408
473
|
context.__exit__(None, None, None)
|
|
409
474
|
|
|
475
|
+
def _check_stall(self) -> None:
|
|
476
|
+
"""Circuit-breaker check: raise WriterStalledError if worker is stalled.
|
|
477
|
+
|
|
478
|
+
Called from submit() and execute() BEFORE enqueueing. Inert on the
|
|
479
|
+
healthy path (started_at is None or within stall_threshold).
|
|
480
|
+
Thread-safe: reads only condition-protected state.
|
|
481
|
+
"""
|
|
482
|
+
with self._condition:
|
|
483
|
+
started = self._inflight_started_at
|
|
484
|
+
if started is None:
|
|
485
|
+
return
|
|
486
|
+
age = time.monotonic() - started
|
|
487
|
+
if age > self._stall_threshold:
|
|
488
|
+
if not self._stall_logged_once:
|
|
489
|
+
import logging as _log
|
|
490
|
+
_log.getLogger(__name__).critical(
|
|
491
|
+
"WriteCoordinator: worker thread stalled for %.1fs on op=%s; "
|
|
492
|
+
"new submitters will receive WriterStalledError",
|
|
493
|
+
age,
|
|
494
|
+
self._inflight_op_id,
|
|
495
|
+
)
|
|
496
|
+
self._stall_logged_once = True
|
|
497
|
+
self._writer_stalled = True
|
|
498
|
+
if self._writer_stalled:
|
|
499
|
+
raise WriterStalledError(
|
|
500
|
+
"write subsystem stalled; admin remediation required"
|
|
501
|
+
)
|
|
502
|
+
|
|
410
503
|
def execute(
|
|
411
504
|
self,
|
|
412
505
|
sql: str,
|
|
@@ -424,6 +517,7 @@ class WriteCoordinator:
|
|
|
424
517
|
raise ValueError("sql must be a non-empty statement")
|
|
425
518
|
if timeout <= 0:
|
|
426
519
|
raise ValueError("timeout must be greater than zero")
|
|
520
|
+
self._check_stall() # circuit-breaker (inert if no stall)
|
|
427
521
|
lane = self._coerce_lane(priority)
|
|
428
522
|
self.start()
|
|
429
523
|
item = _Execution(
|
|
@@ -467,6 +561,7 @@ class WriteCoordinator:
|
|
|
467
561
|
raise TypeError("command must be a WriteCommand")
|
|
468
562
|
if timeout <= 0:
|
|
469
563
|
raise ValueError("timeout must be greater than zero")
|
|
564
|
+
self._check_stall() # circuit-breaker (inert if no stall)
|
|
470
565
|
lane = self._coerce_lane(priority)
|
|
471
566
|
self.start()
|
|
472
567
|
item = _Execution(
|
|
@@ -569,8 +664,25 @@ class WriteCoordinator:
|
|
|
569
664
|
return Lane.FOREGROUND
|
|
570
665
|
|
|
571
666
|
def _execute_item(self, conn: sqlite3.Connection, item: _Execution) -> None:
|
|
667
|
+
# ------------------------------------------------------------------
|
|
668
|
+
# Stall watchdog: record inflight state so _check_stall() can detect
|
|
669
|
+
# a frozen worker. Cleared in the finally block. This guard is
|
|
670
|
+
# strictly ADDITIVE — the item execution path is unchanged.
|
|
671
|
+
# ------------------------------------------------------------------
|
|
672
|
+
op_id: str | None = None
|
|
673
|
+
if item.command is not None:
|
|
674
|
+
op_id = item.command.command_id
|
|
675
|
+
with self._condition:
|
|
676
|
+
self._inflight_started_at = time.monotonic()
|
|
677
|
+
self._inflight_op_id = op_id
|
|
678
|
+
|
|
572
679
|
if item.cancelled or time.monotonic() >= item.deadline:
|
|
573
680
|
item.error = WriteDeadlineExceededError("canonical write expired before execution")
|
|
681
|
+
with self._condition:
|
|
682
|
+
self._inflight_started_at = None
|
|
683
|
+
self._inflight_op_id = None
|
|
684
|
+
self._writer_stalled = False
|
|
685
|
+
self._stall_logged_once = False
|
|
574
686
|
item.completion.set()
|
|
575
687
|
return
|
|
576
688
|
remaining = max(0.0, item.deadline - time.monotonic())
|
|
@@ -578,6 +690,11 @@ class WriteCoordinator:
|
|
|
578
690
|
item.error = WriteDeadlineExceededError(
|
|
579
691
|
"canonical write expired waiting for its process lock"
|
|
580
692
|
)
|
|
693
|
+
with self._condition:
|
|
694
|
+
self._inflight_started_at = None
|
|
695
|
+
self._inflight_op_id = None
|
|
696
|
+
self._writer_stalled = False
|
|
697
|
+
self._stall_logged_once = False
|
|
581
698
|
item.completion.set()
|
|
582
699
|
return
|
|
583
700
|
try:
|
|
@@ -637,6 +754,13 @@ class WriteCoordinator:
|
|
|
637
754
|
item.error.__cause__ = exc
|
|
638
755
|
finally:
|
|
639
756
|
self._process_write_lock.release()
|
|
757
|
+
# Clear inflight state and reset stall breaker so next caller can proceed.
|
|
758
|
+
with self._condition:
|
|
759
|
+
self._inflight_started_at = None
|
|
760
|
+
self._inflight_op_id = None
|
|
761
|
+
if self._writer_stalled:
|
|
762
|
+
self._writer_stalled = False
|
|
763
|
+
self._stall_logged_once = False
|
|
640
764
|
item.completion.set()
|
|
641
765
|
|
|
642
766
|
def _execute_command(self, conn: sqlite3.Connection, command: WriteCommand) -> WriteResult:
|
|
@@ -800,4 +924,5 @@ __all__ = [
|
|
|
800
924
|
"WriteCoordinatorError",
|
|
801
925
|
"WriteDeadlineExceededError",
|
|
802
926
|
"WriteResult",
|
|
927
|
+
"WriterStalledError",
|
|
803
928
|
]
|
|
@@ -73,10 +73,34 @@ class TrustScorer:
|
|
|
73
73
|
return self._compute_trust(alpha, beta)
|
|
74
74
|
|
|
75
75
|
def get_fact_trust(self, fact_id: str, profile_id: str) -> float:
|
|
76
|
-
"""Get trust for a fact.
|
|
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 {
|