superlocalmemory 4.0.9 → 4.1.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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +245 -0
- package/README.md +7 -7
- package/package.json +4 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +4 -4
- 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 +2 -2
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +5 -5
- package/plugin/skills/slm-recall/SKILL.md +102 -15
- package/plugin/skills/slm-remember/SKILL.md +35 -3
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +29 -3
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +16 -8
- 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 +1 -1
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-loop/SKILL.md +2 -2
- package/plugin-src/skills/slm-mesh/SKILL.md +1 -1
- package/plugin-src/skills/slm-profile/SKILL.md +5 -5
- package/plugin-src/skills/slm-recall/SKILL.md +102 -15
- package/plugin-src/skills/slm-remember/SKILL.md +35 -3
- package/plugin-src/skills/slm-scope/SKILL.md +1 -1
- package/plugin-src/skills/slm-session/SKILL.md +29 -3
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +308 -20
- package/src/superlocalmemory/cli/daemon.py +30 -0
- package/src/superlocalmemory/cli/db_migrate.py +71 -1
- package/src/superlocalmemory/cli/gdpr_cmd.py +15 -2
- package/src/superlocalmemory/cli/main.py +26 -4
- package/src/superlocalmemory/code_graph/bridge/maintenance.py +8 -0
- package/src/superlocalmemory/code_graph/database.py +44 -0
- package/src/superlocalmemory/compliance/gdpr.py +449 -39
- package/src/superlocalmemory/core/admission.py +231 -11
- package/src/superlocalmemory/core/backend_orchestrator.py +190 -84
- package/src/superlocalmemory/core/config.py +90 -11
- package/src/superlocalmemory/core/consolidation_engine.py +34 -0
- package/src/superlocalmemory/core/engine.py +140 -11
- package/src/superlocalmemory/core/fact_consolidator.py +316 -125
- package/src/superlocalmemory/core/graph_analyzer.py +76 -112
- package/src/superlocalmemory/core/graph_metrics.py +597 -0
- package/src/superlocalmemory/core/graph_pruner.py +121 -0
- package/src/superlocalmemory/core/maintenance.py +44 -6
- package/src/superlocalmemory/core/maintenance_scheduler.py +205 -0
- package/src/superlocalmemory/core/memory_health.py +266 -0
- package/src/superlocalmemory/core/mode_capability.py +111 -0
- package/src/superlocalmemory/core/ollama_validator.py +315 -0
- package/src/superlocalmemory/core/operation_policy_registry.py +1 -1
- package/src/superlocalmemory/core/operation_request.py +1 -1
- package/src/superlocalmemory/core/ops_remediation.py +2 -2
- package/src/superlocalmemory/core/projection_drain.py +380 -0
- package/src/superlocalmemory/core/recall_pipeline.py +390 -3
- package/src/superlocalmemory/core/recall_worker.py +6 -3
- package/src/superlocalmemory/core/scale_autopromote.py +196 -0
- package/src/superlocalmemory/core/scale_engine.py +16 -2
- package/src/superlocalmemory/core/score_contract.py +21 -1
- package/src/superlocalmemory/core/session_identity.py +85 -0
- package/src/superlocalmemory/core/status_contract.py +108 -0
- package/src/superlocalmemory/core/store_pipeline.py +78 -3
- package/src/superlocalmemory/core/worker_pool.py +4 -4
- package/src/superlocalmemory/core/working_memory.py +288 -0
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +51 -7
- package/src/superlocalmemory/encoding/context_generator.py +1 -1
- package/src/superlocalmemory/encoding/entity_resolver.py +38 -0
- package/src/superlocalmemory/encoding/fact_extractor.py +18 -14
- package/src/superlocalmemory/encoding/prospective_markers.py +262 -0
- package/src/superlocalmemory/encoding/type_router.py +12 -12
- package/src/superlocalmemory/evolution/mutation_generator.py +30 -4
- package/src/superlocalmemory/graph/cozo_adjacency.py +122 -0
- package/src/superlocalmemory/graph/cozo_backend.py +103 -138
- package/src/superlocalmemory/hooks/portable_kit.py +10 -2
- package/src/superlocalmemory/learning/bandit.py +43 -0
- package/src/superlocalmemory/learning/consolidation_worker.py +54 -0
- package/src/superlocalmemory/learning/database.py +60 -3
- package/src/superlocalmemory/learning/entity_compiler.py +21 -58
- package/src/superlocalmemory/learning/feedback.py +3 -1
- package/src/superlocalmemory/learning/outcomes.py +47 -16
- package/src/superlocalmemory/learning/pattern_miner.py +28 -3
- package/src/superlocalmemory/learning/pattern_miner_constants.py +43 -0
- package/src/superlocalmemory/learning/pcos.py +291 -0
- package/src/superlocalmemory/learning/reward_from_outcomes.py +365 -0
- package/src/superlocalmemory/learning/reward_proxy.py +100 -10
- package/src/superlocalmemory/learning/signal_kinds.py +79 -0
- package/src/superlocalmemory/mcp/profiles.py +14 -2
- package/src/superlocalmemory/mcp/server.py +1 -1
- package/src/superlocalmemory/mcp/session_binding.py +92 -0
- package/src/superlocalmemory/mcp/tools_active.py +2 -1
- package/src/superlocalmemory/mcp/tools_core.py +71 -42
- package/src/superlocalmemory/mcp/tools_ops.py +2 -2
- package/src/superlocalmemory/mcp/tools_v28.py +20 -1
- package/src/superlocalmemory/parameterization/pattern_extractor.py +14 -1
- package/src/superlocalmemory/parameterization/soft_prompt_generator.py +98 -0
- package/src/superlocalmemory/retrieval/bm25_channel.py +68 -11
- package/src/superlocalmemory/retrieval/channel_status.py +117 -0
- package/src/superlocalmemory/retrieval/engine.py +106 -11
- package/src/superlocalmemory/retrieval/entity_channel.py +217 -257
- package/src/superlocalmemory/retrieval/graph_adjacency.py +219 -0
- package/src/superlocalmemory/retrieval/scope_policy.py +42 -1
- package/src/superlocalmemory/retrieval/semantic_channel.py +47 -5
- package/src/superlocalmemory/retrieval/spreading.py +288 -0
- package/src/superlocalmemory/retrieval/temporal_channel.py +13 -1
- package/src/superlocalmemory/retrieval/vector_store.py +63 -0
- package/src/superlocalmemory/server/api.py +26 -2
- package/src/superlocalmemory/server/asset_versions.py +171 -0
- package/src/superlocalmemory/server/bandit_loops.py +17 -1
- package/src/superlocalmemory/server/rbac_enforce.py +26 -6
- package/src/superlocalmemory/server/recall_serializer.py +9 -0
- package/src/superlocalmemory/server/routes/abstraction.py +201 -0
- package/src/superlocalmemory/server/routes/behavioral.py +75 -10
- package/src/superlocalmemory/server/routes/compliance.py +98 -18
- package/src/superlocalmemory/server/routes/config_api.py +186 -4
- package/src/superlocalmemory/server/routes/data_io.py +29 -1
- package/src/superlocalmemory/server/routes/entity.py +13 -1
- package/src/superlocalmemory/server/routes/evolution.py +178 -0
- package/src/superlocalmemory/server/routes/ingest.py +8 -0
- package/src/superlocalmemory/server/routes/learning_telemetry.py +2 -1
- package/src/superlocalmemory/server/routes/memories.py +49 -7
- package/src/superlocalmemory/server/routes/mesh.py +1 -1
- package/src/superlocalmemory/server/routes/timeline.py +4 -0
- package/src/superlocalmemory/server/routes/v3_api.py +193 -17
- package/src/superlocalmemory/server/ui.py +24 -1
- package/src/superlocalmemory/server/unified_daemon.py +292 -9
- package/src/superlocalmemory/storage/_migration_internals.py +35 -0
- package/src/superlocalmemory/storage/_schema_version.py +24 -3
- package/src/superlocalmemory/storage/database.py +598 -82
- package/src/superlocalmemory/storage/embedding_codec.py +71 -0
- package/src/superlocalmemory/storage/lineage_retention.py +236 -0
- package/src/superlocalmemory/storage/logical_edges.py +43 -2
- package/src/superlocalmemory/storage/migration_runner.py +130 -0
- package/src/superlocalmemory/storage/migrations/M043_quarantine_display_summaries.py +488 -0
- package/src/superlocalmemory/storage/migrations/M044_play_carries_its_own_evidence.py +127 -0
- package/src/superlocalmemory/storage/migrations/M045_fact_outcome_score.py +158 -0
- package/src/superlocalmemory/storage/migrations/M046_prospective_memory_has_its_own_name.py +620 -0
- package/src/superlocalmemory/storage/migrations/M047_fisher_vectors_are_stored_like_every_other_vector.py +306 -0
- package/src/superlocalmemory/storage/migrations/M048_upcoming_holds_only_what_is_upcoming.py +207 -0
- package/src/superlocalmemory/storage/migrations/M049_a_schema_version_marker_is_one_row.py +201 -0
- package/src/superlocalmemory/storage/migrations.py +18 -2
- package/src/superlocalmemory/storage/models.py +40 -1
- package/src/superlocalmemory/storage/projection_outbox.py +346 -0
- package/src/superlocalmemory/storage/retention_policy.py +860 -0
- package/src/superlocalmemory/storage/schema.py +110 -1
- package/src/superlocalmemory/storage/write_coordinator.py +19 -2
- package/src/superlocalmemory/summaries/base.py +1 -1
- package/src/superlocalmemory/summaries/non_answer.py +223 -0
- package/src/superlocalmemory/trust/scorer.py +43 -1
- package/src/superlocalmemory/ui/index.html +10 -19
- package/src/superlocalmemory/ui/js/event-delegation.js +12 -1
- package/src/superlocalmemory/ui/js/od-health.js +28 -6
- package/src/superlocalmemory/ui/js/od-memories.js +209 -1
- package/src/superlocalmemory/ui/js/od-ops-health.js +1 -1
- package/src/superlocalmemory/ui/js/od-settings.js +87 -1
- package/src/superlocalmemory/ui/js/recall-lab.js +78 -3
|
@@ -21,9 +21,12 @@ Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
|
21
21
|
|
|
22
22
|
from __future__ import annotations
|
|
23
23
|
|
|
24
|
+
import logging
|
|
24
25
|
import sqlite3
|
|
25
26
|
from typing import Final
|
|
26
27
|
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
27
30
|
# ---------------------------------------------------------------------------
|
|
28
31
|
# Constants
|
|
29
32
|
# ---------------------------------------------------------------------------
|
|
@@ -57,7 +60,9 @@ _TABLES: Final[tuple[str, ...]] = (
|
|
|
57
60
|
"config",
|
|
58
61
|
"entity_communities",
|
|
59
62
|
"community_summaries",
|
|
63
|
+
"consolidated_summaries",
|
|
60
64
|
"persona_summary",
|
|
65
|
+
"projection_outbox",
|
|
61
66
|
)
|
|
62
67
|
|
|
63
68
|
_FTS_TABLES: Final[tuple[str, ...]] = (
|
|
@@ -153,7 +158,7 @@ CREATE TABLE IF NOT EXISTS atomic_facts (
|
|
|
153
158
|
content TEXT NOT NULL,
|
|
154
159
|
fact_type TEXT NOT NULL DEFAULT 'semantic'
|
|
155
160
|
CHECK (fact_type IN (
|
|
156
|
-
'episodic', 'semantic', 'opinion', '
|
|
161
|
+
'episodic', 'semantic', 'opinion', 'prospective'
|
|
157
162
|
)),
|
|
158
163
|
|
|
159
164
|
-- Entities (JSON arrays)
|
|
@@ -190,6 +195,11 @@ CREATE TABLE IF NOT EXISTS atomic_facts (
|
|
|
190
195
|
CHECK (lifecycle IN (
|
|
191
196
|
'active', 'warm', 'cold', 'archived'
|
|
192
197
|
)),
|
|
198
|
+
-- Withheld from retrieval without being destroyed. Set by repair, never
|
|
199
|
+
-- by a normal write; enforced in exactly one place,
|
|
200
|
+
-- DatabaseManager.get_facts_by_ids, which every channel's candidates are
|
|
201
|
+
-- re-authorised through and which the engine hydrates from.
|
|
202
|
+
quarantined INTEGER NOT NULL DEFAULT 0,
|
|
193
203
|
langevin_position TEXT,
|
|
194
204
|
|
|
195
205
|
-- Emotional
|
|
@@ -842,6 +852,51 @@ CREATE INDEX IF NOT EXISTS idx_comm_summ_profile
|
|
|
842
852
|
ON community_summaries(profile_id);
|
|
843
853
|
"""
|
|
844
854
|
|
|
855
|
+
# Display-only consolidated summaries.
|
|
856
|
+
#
|
|
857
|
+
# A summary of a cluster of facts is a VIEW of memory, not a memory. Between
|
|
858
|
+
# v3.6.4 and 4.0.9 the fact consolidator wrote its summaries straight into
|
|
859
|
+
# atomic_facts with a raw INSERT, which put model-authored prose into the
|
|
860
|
+
# retrieval corpus alongside the user's own words — where it out-ranked them,
|
|
861
|
+
# because those rows carried every entity in their cluster and so had more
|
|
862
|
+
# entity links than any real fact.
|
|
863
|
+
#
|
|
864
|
+
# This table restores the boundary. community_summaries is the precedent to
|
|
865
|
+
# read it by: written by one owner, read only after retrieval has finished, and
|
|
866
|
+
# named by no channel. tests/test_retrieval/test_summaries_stay_out_of_recall.py
|
|
867
|
+
# fails if a retrieval module so much as mentions it.
|
|
868
|
+
#
|
|
869
|
+
# source_earliest / source_latest are the honest dates for a derived row. A
|
|
870
|
+
# summary has no observation_date of its own — it was never observed — but the
|
|
871
|
+
# span of what it summarises is real, and it is what lets the dashboard say
|
|
872
|
+
# which stretch of work a summary covers.
|
|
873
|
+
CONSOLIDATED_SUMMARIES_DDL: Final[str] = """
|
|
874
|
+
CREATE TABLE IF NOT EXISTS consolidated_summaries (
|
|
875
|
+
summary_id TEXT PRIMARY KEY,
|
|
876
|
+
profile_id TEXT NOT NULL,
|
|
877
|
+
entity_id TEXT NOT NULL DEFAULT '',
|
|
878
|
+
entity_name TEXT NOT NULL DEFAULT '',
|
|
879
|
+
content TEXT NOT NULL,
|
|
880
|
+
source_fact_ids TEXT NOT NULL DEFAULT '[]',
|
|
881
|
+
source_count INTEGER NOT NULL DEFAULT 0,
|
|
882
|
+
char_count INTEGER NOT NULL DEFAULT 0,
|
|
883
|
+
generated_by TEXT NOT NULL DEFAULT 'extractive'
|
|
884
|
+
CHECK (generated_by IN (
|
|
885
|
+
'extractive', 'ollama', 'cloud', 'migrated'
|
|
886
|
+
)),
|
|
887
|
+
scope TEXT NOT NULL DEFAULT 'personal',
|
|
888
|
+
shared_with TEXT,
|
|
889
|
+
source_earliest TEXT,
|
|
890
|
+
source_latest TEXT,
|
|
891
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
892
|
+
UNIQUE (profile_id, entity_id, content)
|
|
893
|
+
);
|
|
894
|
+
CREATE INDEX IF NOT EXISTS idx_consolidated_summaries_profile
|
|
895
|
+
ON consolidated_summaries(profile_id, created_at DESC);
|
|
896
|
+
CREATE INDEX IF NOT EXISTS idx_consolidated_summaries_entity
|
|
897
|
+
ON consolidated_summaries(profile_id, entity_id);
|
|
898
|
+
"""
|
|
899
|
+
|
|
845
900
|
# Wave Q3: progressive-abstraction top tier — one persona roll-up per profile
|
|
846
901
|
# consuming the top community summaries (additive; safe on existing DBs).
|
|
847
902
|
# Recall-gated (never auto-injected into hot recall) and size-bounded to avoid
|
|
@@ -861,6 +916,10 @@ CREATE TABLE IF NOT EXISTS persona_summary (
|
|
|
861
916
|
# Ordered DDL list (tables before FTS, respects FK order)
|
|
862
917
|
# ---------------------------------------------------------------------------
|
|
863
918
|
|
|
919
|
+
#: Imported rather than restated so the table has exactly one definition; the
|
|
920
|
+
#: module that owns the queue owns its shape.
|
|
921
|
+
from superlocalmemory.storage.projection_outbox import DDL as _PROJECTION_OUTBOX_DDL
|
|
922
|
+
|
|
864
923
|
_DDL_ORDERED: Final[tuple[str, ...]] = (
|
|
865
924
|
_SQL_SCHEMA_VERSION,
|
|
866
925
|
_SQL_PROFILES,
|
|
@@ -893,8 +952,15 @@ _DDL_ORDERED: Final[tuple[str, ...]] = (
|
|
|
893
952
|
_SQL_ENTITY_COMMUNITIES,
|
|
894
953
|
# Wave Q2: community summaries (additive; safe on existing DBs)
|
|
895
954
|
_SQL_COMMUNITY_SUMMARIES,
|
|
955
|
+
CONSOLIDATED_SUMMARIES_DDL,
|
|
896
956
|
# Wave Q3: persona roll-up tier (additive; safe on existing DBs)
|
|
897
957
|
_SQL_PERSONA_SUMMARY,
|
|
958
|
+
# The queue that makes a graph/vector projection write survive a crash.
|
|
959
|
+
# Created here, at every engine init, rather than only by its migration:
|
|
960
|
+
# if this table is missing, facts stop being projected and the only symptom
|
|
961
|
+
# is a memory that cannot be recalled. That invariant must not be
|
|
962
|
+
# contingent on a migration pass having succeeded.
|
|
963
|
+
_PROJECTION_OUTBOX_DDL,
|
|
898
964
|
)
|
|
899
965
|
|
|
900
966
|
|
|
@@ -902,6 +968,38 @@ _DDL_ORDERED: Final[tuple[str, ...]] = (
|
|
|
902
968
|
# Public API
|
|
903
969
|
# ---------------------------------------------------------------------------
|
|
904
970
|
|
|
971
|
+
#: (table, column, column definition). Applied with ALTER TABLE ADD COLUMN,
|
|
972
|
+
#: which SQLite offers no IF NOT EXISTS form of, so presence is checked first.
|
|
973
|
+
_ADDITIVE_COLUMNS: Final[tuple[tuple[str, str, str], ...]] = (
|
|
974
|
+
("atomic_facts", "quarantined", "INTEGER NOT NULL DEFAULT 0"),
|
|
975
|
+
)
|
|
976
|
+
|
|
977
|
+
|
|
978
|
+
def _add_missing_columns(conn: sqlite3.Connection) -> None:
|
|
979
|
+
"""Add columns that upgraded databases predate. Idempotent.
|
|
980
|
+
|
|
981
|
+
A missing table is not an error: this runs inside create_all_tables, so the
|
|
982
|
+
table is created moments earlier in the same call, and a database old enough
|
|
983
|
+
to lack it entirely has nothing to alter.
|
|
984
|
+
"""
|
|
985
|
+
for table, column, definition in _ADDITIVE_COLUMNS:
|
|
986
|
+
try:
|
|
987
|
+
present = any(
|
|
988
|
+
row[1] == column
|
|
989
|
+
for row in conn.execute(f"PRAGMA table_info({table})")
|
|
990
|
+
)
|
|
991
|
+
if not present:
|
|
992
|
+
conn.execute(
|
|
993
|
+
f"ALTER TABLE {table} ADD COLUMN {column} {definition}"
|
|
994
|
+
)
|
|
995
|
+
except sqlite3.Error as exc:
|
|
996
|
+
# Never fatal. A store that cannot take the column keeps working;
|
|
997
|
+
# get_facts_by_ids checks for the column before filtering on it.
|
|
998
|
+
logger.warning(
|
|
999
|
+
"additive column %s.%s not applied: %s", table, column, exc,
|
|
1000
|
+
)
|
|
1001
|
+
|
|
1002
|
+
|
|
905
1003
|
def create_all_tables(conn: sqlite3.Connection) -> None:
|
|
906
1004
|
"""Create every table, index, trigger, and FTS virtual table.
|
|
907
1005
|
|
|
@@ -921,6 +1019,17 @@ def create_all_tables(conn: sqlite3.Connection) -> None:
|
|
|
921
1019
|
for ddl in V32_DDL:
|
|
922
1020
|
conn.executescript(ddl)
|
|
923
1021
|
|
|
1022
|
+
# Additive columns on tables that predate them.
|
|
1023
|
+
#
|
|
1024
|
+
# CREATE TABLE IF NOT EXISTS cannot add a column to a table that already
|
|
1025
|
+
# exists, so an upgraded database gets the column here rather than only from
|
|
1026
|
+
# a migration. Doing it at every engine init makes the invariant "if
|
|
1027
|
+
# atomic_facts exists then quarantined exists" hold even when the migration
|
|
1028
|
+
# pass failed or was never reached — which matters because withholding a
|
|
1029
|
+
# poisoned row from retrieval must not be contingent on a migration having
|
|
1030
|
+
# succeeded.
|
|
1031
|
+
_add_missing_columns(conn)
|
|
1032
|
+
|
|
924
1033
|
# Seed schema version on first run.
|
|
925
1034
|
existing = conn.execute(
|
|
926
1035
|
"SELECT COUNT(*) AS n FROM schema_version"
|
|
@@ -631,10 +631,27 @@ class WriteCoordinator:
|
|
|
631
631
|
conn.close()
|
|
632
632
|
|
|
633
633
|
def _open_connection(self) -> sqlite3.Connection:
|
|
634
|
-
|
|
634
|
+
"""The coordinator's own connection, on the same waiting policy as the rest.
|
|
635
|
+
|
|
636
|
+
It waited one second while every other writer waited ten. A caller
|
|
637
|
+
legitimately holding the write lock for longer than a second -- a large
|
|
638
|
+
batch, a store during maintenance -- made the coordinator's next write
|
|
639
|
+
raise "database is locked" and lose the item, while an ordinary write
|
|
640
|
+
issued at the same moment simply waited and succeeded.
|
|
641
|
+
|
|
642
|
+
Reproduced: a 3,010 ms hold blocked the coordinator for 1,004 ms and then
|
|
643
|
+
dropped its write. There was no reason for the two to disagree, and the
|
|
644
|
+
shorter one belonged to the path whose failure loses data rather than
|
|
645
|
+
retries.
|
|
646
|
+
"""
|
|
647
|
+
from superlocalmemory.storage.database import _BUSY_TIMEOUT_MS
|
|
648
|
+
|
|
649
|
+
conn = sqlite3.connect(
|
|
650
|
+
str(self._db_path), timeout=_BUSY_TIMEOUT_MS / 1000.0,
|
|
651
|
+
)
|
|
635
652
|
conn.row_factory = sqlite3.Row
|
|
636
653
|
conn.execute("PRAGMA foreign_keys=ON")
|
|
637
|
-
conn.execute("PRAGMA busy_timeout=
|
|
654
|
+
conn.execute(f"PRAGMA busy_timeout={_BUSY_TIMEOUT_MS}")
|
|
638
655
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
639
656
|
return conn
|
|
640
657
|
|
|
@@ -62,7 +62,7 @@ class SummaryResult:
|
|
|
62
62
|
|
|
63
63
|
# ── coverage constants ──────────────────────────────────────────────────────
|
|
64
64
|
#
|
|
65
|
-
# Use these strings; the
|
|
65
|
+
# Use these strings; the tests check for their presence
|
|
66
66
|
# and the values must be human-interpretable without this file.
|
|
67
67
|
|
|
68
68
|
COVERAGE_FULL = "full"
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
|
|
4
|
+
"""Refuse to store a model's non-answer as if it were a memory.
|
|
5
|
+
|
|
6
|
+
A summarizer is handed a cluster of facts and asked to merge them. When the
|
|
7
|
+
cluster has nothing in common the model does not fail — it answers the question
|
|
8
|
+
it was asked, in prose, and that prose is indistinguishable from a summary to
|
|
9
|
+
any caller that only checks ``if summary:``. On the author's own store the
|
|
10
|
+
result was rows reading
|
|
11
|
+
|
|
12
|
+
"Unfortunately, there is no information available about 'Gateway', 'State',
|
|
13
|
+
'Bounded', or 'Claude' in the provided text."
|
|
14
|
+
|
|
15
|
+
sitting at ranks 1, 2 and 3 for "what am I working on".
|
|
16
|
+
|
|
17
|
+
WHAT THIS IS AND IS NOT
|
|
18
|
+
-----------------------
|
|
19
|
+
This is a **forward guard**: it stops the next such row from being written. It
|
|
20
|
+
is deliberately NOT the repair for rows already stored, because it cannot be.
|
|
21
|
+
Measured across the 307 retrieval-eligible consolidated rows on the author's
|
|
22
|
+
store, this predicate rejects **34** and lets **273** through — because those
|
|
23
|
+
273 are fluent, plausible, entirely generic prose ("The Pro and
|
|
24
|
+
SuperLocalMemory (SLM) projects have made significant progress...") that no
|
|
25
|
+
honest content predicate can separate from a real summary. Repairing by content
|
|
26
|
+
would have cleared a ninth of the problem and declared victory. Existing rows
|
|
27
|
+
are handled by provenance instead.
|
|
28
|
+
|
|
29
|
+
Measured the other way, on 3,894 genuine facts, it rejects 70 — and all 70 are
|
|
30
|
+
real defects, not false positives: 68 memories carry raw tool-call markup
|
|
31
|
+
scraped in from a transcript, and 2 are a model's refusal that was stored as
|
|
32
|
+
though it were a memory ("I cannot verify when ... ended a session. Can I help
|
|
33
|
+
you with something else?"). Zero legitimate memories are rejected.
|
|
34
|
+
|
|
35
|
+
So the bar here is: catch text that is *addressed to the prompt* rather than
|
|
36
|
+
*about the facts*, and nothing else. Everything is anchored to the start of the
|
|
37
|
+
text or to a whole leading sentence, because a genuine memory may legitimately
|
|
38
|
+
contain "there is no" in the middle of a sentence.
|
|
39
|
+
|
|
40
|
+
Companion to ``clean_llm_summary`` in :mod:`superlocalmemory.summaries.base`,
|
|
41
|
+
which strips scaffolding *around* an answer. This one rejects text that is
|
|
42
|
+
scaffolding *all the way through*. Run the stripper first: "Here is a concise
|
|
43
|
+
summary paragraph: <real content>" is salvageable and must not be discarded.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
import re
|
|
49
|
+
|
|
50
|
+
__all__ = ["is_non_answer", "NON_ANSWER_PATTERNS", "MIN_USEFUL_CHARS"]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
#: A *merged summary* this short did not merge anything. The summarizers already
|
|
54
|
+
#: refuse model output under 50 characters; this is the same floor applied to
|
|
55
|
+
#: text that arrived by another route (extractive mode, or a pre-computed summary
|
|
56
|
+
#: handed in by a caller).
|
|
57
|
+
#:
|
|
58
|
+
#: It is NOT a floor for memories in general, and ``is_non_answer`` therefore
|
|
59
|
+
#: does not apply it unless a caller asks. Measured on the author's store, 730
|
|
60
|
+
#: of 3,894 genuine facts are under 50 characters and every sampled one is a
|
|
61
|
+
#: real memory — "2026-05-02 is the date when the session ended",
|
|
62
|
+
#: "This is the case for keeping AMS." Baking this floor into the default would
|
|
63
|
+
#: have made the guard reject a fifth of a user's memory as junk. The floor is
|
|
64
|
+
#: the *caller's* policy about its own output, not a fact about text.
|
|
65
|
+
MIN_USEFUL_CHARS = 50
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
#: Each entry is (regex, why-it-is-not-a-memory). The reason travels with the
|
|
69
|
+
#: pattern so a future reader can tell whether a new false positive means the
|
|
70
|
+
#: pattern is wrong or the input genuinely is a non-answer.
|
|
71
|
+
_PATTERN_SOURCES: tuple[tuple[str, str], ...] = (
|
|
72
|
+
(
|
|
73
|
+
r"^\W*(?:unfortunately|regrettably|sadly)\b[^.!?]*\bno\b",
|
|
74
|
+
"opens by apologising for having nothing to say",
|
|
75
|
+
),
|
|
76
|
+
(
|
|
77
|
+
r"^\W*there\s+(?:is|are)\s+no\s+"
|
|
78
|
+
r"(?:information|mention|reference|facts?|details?|data|content)\b",
|
|
79
|
+
"states the absence of input rather than summarising input",
|
|
80
|
+
),
|
|
81
|
+
(
|
|
82
|
+
r"\bno\s+(?:information|facts?|details?|data)\s+"
|
|
83
|
+
r"(?:is|are|was|were)?\s*(?:available|provided|given|present)\b",
|
|
84
|
+
"reports an empty input set",
|
|
85
|
+
),
|
|
86
|
+
(
|
|
87
|
+
r"^\W*(?:i\s+(?:cannot|can't|can\s+not|am\s+unable\s+to)|"
|
|
88
|
+
r"it\s+is\s+not\s+possible\s+to)\b",
|
|
89
|
+
"declines the task",
|
|
90
|
+
),
|
|
91
|
+
(
|
|
92
|
+
r"^\W*(?:i\s+don'?t|i\s+do\s+not)\s+(?:have|see|find)\b",
|
|
93
|
+
"declines the task in the first person",
|
|
94
|
+
),
|
|
95
|
+
(
|
|
96
|
+
r"\bthe\s+(?:provided|given|above|following)\s+"
|
|
97
|
+
r"(?:text|facts?|context|input|information)\b",
|
|
98
|
+
"refers to the prompt, so it is talking to the asker, not about the memory",
|
|
99
|
+
),
|
|
100
|
+
(
|
|
101
|
+
r"^\W*(?:as\s+an?\s+(?:ai|language\s+model)|i'?m\s+an?\s+ai)\b",
|
|
102
|
+
"identifies itself as a model",
|
|
103
|
+
),
|
|
104
|
+
(
|
|
105
|
+
r"^\W*(?:please\s+)?(?:provide|share|give)\s+(?:me\s+)?"
|
|
106
|
+
r"(?:more|the|some|additional)\b",
|
|
107
|
+
"asks the user for input instead of answering",
|
|
108
|
+
),
|
|
109
|
+
# The five below were added after running the first four against all 1,035
|
|
110
|
+
# summaries stored on the author's machine. They catch 14 rows the original
|
|
111
|
+
# set let through -- every one a measured string from that store, not a
|
|
112
|
+
# guess about what a model might say.
|
|
113
|
+
(
|
|
114
|
+
r"^\W*i\s+(?:did\s*n[o']?t|didn'?t)\s+receive\b",
|
|
115
|
+
"says it was given nothing to summarise",
|
|
116
|
+
),
|
|
117
|
+
(
|
|
118
|
+
r"\bthere\s+(?:is|are|was|were)\s+n(?:o|ot)\s+\d+\s+facts?\b",
|
|
119
|
+
"argues with the number of facts it was asked to merge",
|
|
120
|
+
),
|
|
121
|
+
(
|
|
122
|
+
r"\bthe\s+text\s+(?:snippet\s+)?"
|
|
123
|
+
r"(?:appears|seems|does\s+not|doesn'?t|is\s+not)\b",
|
|
124
|
+
"describes the prompt instead of summarising it",
|
|
125
|
+
),
|
|
126
|
+
(
|
|
127
|
+
r"\bin\s+the\s+text\s+(?:provided|given|above|supplied)\b"
|
|
128
|
+
r"|\bthe\s+text\s+(?:provided|supplied)\b",
|
|
129
|
+
"refers to the prompt (word order the earlier rule missed)",
|
|
130
|
+
),
|
|
131
|
+
(
|
|
132
|
+
r"^\W*i\s+must\s+point\s+out\b",
|
|
133
|
+
"opens with meta-commentary about the request",
|
|
134
|
+
),
|
|
135
|
+
(
|
|
136
|
+
# "This appears to be a detailed log of progress in writing..." — the
|
|
137
|
+
# model describing the shape of what it was shown rather than saying
|
|
138
|
+
# what it says. Six of the first twelve rows the dashboard rendered
|
|
139
|
+
# opened this way. Deliberately narrower than it could be: "This is a
|
|
140
|
+
# summary of an audit session" is clumsy but it does summarise, so it
|
|
141
|
+
# is left alone.
|
|
142
|
+
r"^\W*this\s+(?:appears|seems)\s+to\s+be\b"
|
|
143
|
+
r"|^\W*this\s+text\s+(?:is|appears|seems)\b",
|
|
144
|
+
"describes the shape of the input instead of its content",
|
|
145
|
+
),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
#: Compiled once. ``re.IGNORECASE`` throughout — the casing of a refusal is not
|
|
149
|
+
#: information. ``re.DOTALL`` is deliberately NOT set: ``[^.!?]*`` and the
|
|
150
|
+
#: leading anchors are meant to stay within the opening sentence.
|
|
151
|
+
NON_ANSWER_PATTERNS: tuple[tuple[re.Pattern[str], str], ...] = tuple(
|
|
152
|
+
(re.compile(src, re.IGNORECASE), why) for src, why in _PATTERN_SOURCES
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
#: Tool-call and markup fragments. A memory whose content carries these was
|
|
156
|
+
#: assembled from a transcript that still had its plumbing attached; the user
|
|
157
|
+
#: sees raw XML in their own memory list. Matched anywhere, not anchored,
|
|
158
|
+
#: because the fragment can appear at any offset in a spliced transcript.
|
|
159
|
+
_MARKUP = re.compile(
|
|
160
|
+
r"</(?:content|antml:parameter|parameter|invoke|function_calls|thinking)>"
|
|
161
|
+
r"|<(?:antml:)?(?:parameter|invoke|function_calls)\b"
|
|
162
|
+
r"|<\|(?:im_start|im_end|endoftext)\|>",
|
|
163
|
+
re.IGNORECASE,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def is_non_answer(
|
|
168
|
+
text: str | None,
|
|
169
|
+
*,
|
|
170
|
+
min_chars: int = 0,
|
|
171
|
+
model_written: bool = True,
|
|
172
|
+
) -> tuple[bool, str]:
|
|
173
|
+
"""Whether ``text`` is a model talking about the prompt, not a memory.
|
|
174
|
+
|
|
175
|
+
Returns ``(rejected, reason)``. ``reason`` is empty when the text is
|
|
176
|
+
acceptable, and otherwise names which rule fired — callers log it, so a
|
|
177
|
+
silent rejection never happens and a false positive is diagnosable from
|
|
178
|
+
normal logs rather than needing a repro.
|
|
179
|
+
|
|
180
|
+
``min_chars`` defaults to **no floor**. A caller that knows its own output
|
|
181
|
+
should be long — a summarizer merging three or more facts — passes
|
|
182
|
+
:data:`MIN_USEFUL_CHARS`. See that constant for why this is not the default.
|
|
183
|
+
|
|
184
|
+
``model_written=False`` applies only the rules that hold for text no model
|
|
185
|
+
composed: the length floor and the markup check. Every other rule here
|
|
186
|
+
describes a MODEL's behaviour — refusing, apologising, discussing the
|
|
187
|
+
prompt — and an extractive summary is the owner's own sentences reassembled,
|
|
188
|
+
so a memory that happens to say "the facts provided" would be rejected for
|
|
189
|
+
having phrased itself like a chatbot. Nothing is lost by exempting it: an
|
|
190
|
+
extractive summary cannot refuse, because there is nobody there to refuse.
|
|
191
|
+
|
|
192
|
+
Cheap and side-effect free: safe to call on every candidate write.
|
|
193
|
+
|
|
194
|
+
>>> is_non_answer("Unfortunately, there is no information available.")[0]
|
|
195
|
+
True
|
|
196
|
+
>>> is_non_answer("Varun ships SLM 4.0.10 with an auto-repair migration.")
|
|
197
|
+
(False, '')
|
|
198
|
+
>>> is_non_answer("The team has no information silo left to dismantle.")[0]
|
|
199
|
+
False
|
|
200
|
+
>>> is_non_answer("2026-05-02 is the date the session ended.")
|
|
201
|
+
(False, '')
|
|
202
|
+
>>> is_non_answer("Too short.", min_chars=MIN_USEFUL_CHARS)[0]
|
|
203
|
+
True
|
|
204
|
+
"""
|
|
205
|
+
if text is None:
|
|
206
|
+
return True, "empty"
|
|
207
|
+
stripped = text.strip()
|
|
208
|
+
if not stripped:
|
|
209
|
+
return True, "empty"
|
|
210
|
+
if min_chars and len(stripped) < min_chars:
|
|
211
|
+
return True, f"shorter than {min_chars} characters"
|
|
212
|
+
|
|
213
|
+
if _MARKUP.search(stripped):
|
|
214
|
+
return True, "contains tool-call or chat-template markup"
|
|
215
|
+
|
|
216
|
+
if not model_written:
|
|
217
|
+
return False, ""
|
|
218
|
+
|
|
219
|
+
for pattern, why in NON_ANSWER_PATTERNS:
|
|
220
|
+
if pattern.search(stripped):
|
|
221
|
+
return True, why
|
|
222
|
+
|
|
223
|
+
return False, ""
|
|
@@ -34,6 +34,31 @@ _DEFAULT_ALPHA = 1.0
|
|
|
34
34
|
_DEFAULT_BETA = 1.0
|
|
35
35
|
_DEFAULT_TRUST = 0.5
|
|
36
36
|
|
|
37
|
+
#: Identities that are the absence of an identity. A caller that does not say
|
|
38
|
+
#: who it is lands in one of these, and on a real store the ``unknown`` bucket
|
|
39
|
+
#: had accumulated 1,708 pieces of evidence and a trust of 1.000 — the highest
|
|
40
|
+
#: of any agent in the store, higher than the named agents doing the actual
|
|
41
|
+
#: work. That number is the sum of everybody's behaviour, and it was being
|
|
42
|
+
#: handed to whoever arrived next without a name.
|
|
43
|
+
#:
|
|
44
|
+
#: It matters twice over. The trust gate lets a write through at 0.3 and a
|
|
45
|
+
#: delete at 0.5, so a caller with no identity cleared both by the widest
|
|
46
|
+
#: margin available. And a fact with no trust record of its own inherits the
|
|
47
|
+
#: trust of whoever created it, so 39 facts on that store were being ranked at
|
|
48
|
+
#: the maximum promotion for having no author.
|
|
49
|
+
#:
|
|
50
|
+
#: A catch-all is not an identity. It reads as the neutral prior every time and
|
|
51
|
+
#: accumulates nothing, which leaves the gate exactly where it was for an
|
|
52
|
+
#: anonymous caller (0.5 clears both thresholds) while removing the escalation.
|
|
53
|
+
ANONYMOUS_IDENTITIES: frozenset[str] = frozenset({
|
|
54
|
+
"", "unknown", "unspecified", "anonymous", "none", "null",
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def is_anonymous(agent_id: str) -> bool:
|
|
59
|
+
"""Whether this identifier names nobody in particular."""
|
|
60
|
+
return (agent_id or "").strip().lower() in ANONYMOUS_IDENTITIES
|
|
61
|
+
|
|
37
62
|
# Signal strengths (how much each event shifts alpha/beta)
|
|
38
63
|
_SIGNAL_WEIGHTS: dict[str, tuple[float, float]] = {
|
|
39
64
|
# (delta_alpha, delta_beta)
|
|
@@ -68,7 +93,14 @@ class TrustScorer:
|
|
|
68
93
|
# ------------------------------------------------------------------
|
|
69
94
|
|
|
70
95
|
def get_agent_trust(self, agent_id: str, profile_id: str) -> float:
|
|
71
|
-
"""Get trust score for an agent. Returns
|
|
96
|
+
"""Get trust score for an agent. Returns the neutral prior if unknown.
|
|
97
|
+
|
|
98
|
+
An identifier that names nobody in particular always reads as the
|
|
99
|
+
prior, whatever has been written against that bucket. See
|
|
100
|
+
``ANONYMOUS_IDENTITIES``.
|
|
101
|
+
"""
|
|
102
|
+
if is_anonymous(agent_id):
|
|
103
|
+
return _DEFAULT_TRUST
|
|
72
104
|
alpha, beta = self._get_beta_params("agent", agent_id, profile_id)
|
|
73
105
|
return self._compute_trust(alpha, beta)
|
|
74
106
|
|
|
@@ -131,6 +163,16 @@ class TrustScorer:
|
|
|
131
163
|
Returns:
|
|
132
164
|
Updated trust score for the agent.
|
|
133
165
|
"""
|
|
166
|
+
if is_anonymous(agent_id):
|
|
167
|
+
# Reputation belongs to somebody. Accumulating it against the
|
|
168
|
+
# catch-all is how that bucket reached the highest trust in the
|
|
169
|
+
# store while naming no one.
|
|
170
|
+
logger.debug(
|
|
171
|
+
"trust signal ignored: %r names nobody (signal=%s)",
|
|
172
|
+
agent_id, signal_type,
|
|
173
|
+
)
|
|
174
|
+
return _DEFAULT_TRUST
|
|
175
|
+
|
|
134
176
|
weights = _SIGNAL_WEIGHTS.get(signal_type, (0.0, 0.0))
|
|
135
177
|
delta_alpha, delta_beta = weights
|
|
136
178
|
|
|
@@ -113,27 +113,19 @@
|
|
|
113
113
|
</div>
|
|
114
114
|
</div>
|
|
115
115
|
|
|
116
|
-
<!--
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
116
|
+
<!-- The visible navigation is the sidebar, built by od-shell.js.
|
|
117
|
+
These hidden buttons exist only so od-shell's ``activateTab`` can
|
|
118
|
+
dispatch Bootstrap's ``shown.bs.tab`` on a real element for panes
|
|
119
|
+
it does not render itself — today that is dashboard-pane alone,
|
|
120
|
+
whose data reload hangs off that event. Every button here must
|
|
121
|
+
target a pane that exists: nine did not, and clicking one did
|
|
122
|
+
nothing at all. -->
|
|
122
123
|
<ul class="nav nav-tabs d-none" id="mainTabs" role="tablist" aria-hidden="true">
|
|
123
124
|
<li class="nav-item"><button class="nav-link active" id="dashboard-tab" data-bs-toggle="tab" data-bs-target="#dashboard-pane" type="button"></button></li>
|
|
124
125
|
<li class="nav-item"><button class="nav-link" id="graph-tab" data-bs-toggle="tab" data-bs-target="#graph-pane" type="button"></button></li>
|
|
125
126
|
<li class="nav-item"><button class="nav-link" id="memories-tab" data-bs-toggle="tab" data-bs-target="#memories-pane" type="button"></button></li>
|
|
126
|
-
<li class="nav-item"><button class="nav-link" id="recall-lab-tab" data-bs-toggle="tab" data-bs-target="#recall-lab-pane" type="button"></button></li>
|
|
127
|
-
<li class="nav-item"><button class="nav-link" id="clusters-tab" data-bs-toggle="tab" data-bs-target="#clusters-pane" type="button"></button></li>
|
|
128
|
-
<li class="nav-item"><button class="nav-link" id="timeline-tab" data-bs-toggle="tab" data-bs-target="#timeline-pane" type="button"></button></li>
|
|
129
127
|
<li class="nav-item"><button class="nav-link" id="brain-tab" data-bs-toggle="tab" data-bs-target="#brain-pane" type="button" role="tab" aria-controls="brain-pane"></button></li>
|
|
130
|
-
<li class="nav-item"><button class="nav-link" id="events-tab" data-bs-toggle="tab" data-bs-target="#events-pane" type="button"></button></li>
|
|
131
128
|
<li class="nav-item"><button class="nav-link" id="agents-tab" data-bs-toggle="tab" data-bs-target="#agents-pane" type="button"></button></li>
|
|
132
|
-
<li class="nav-item"><button class="nav-link" id="trust-tab" data-bs-toggle="tab" data-bs-target="#trust-pane" type="button"></button></li>
|
|
133
|
-
<li class="nav-item"><button class="nav-link" id="lifecycle-tab" data-bs-toggle="tab" data-bs-target="#lifecycle-pane" type="button"></button></li>
|
|
134
|
-
<li class="nav-item"><button class="nav-link" id="compliance-tab" data-bs-toggle="tab" data-bs-target="#compliance-pane" type="button"></button></li>
|
|
135
|
-
<li class="nav-item"><button class="nav-link" id="math-health-tab" data-bs-toggle="tab" data-bs-target="#math-health-pane" type="button"></button></li>
|
|
136
|
-
<li class="nav-item"><button class="nav-link" id="ide-tab" data-bs-toggle="tab" data-bs-target="#ide-pane" type="button"></button></li>
|
|
137
129
|
<li class="nav-item"><button class="nav-link" id="settings-tab" data-bs-toggle="tab" data-bs-target="#settings-pane" type="button"></button></li>
|
|
138
130
|
</ul>
|
|
139
131
|
|
|
@@ -956,7 +948,7 @@
|
|
|
956
948
|
<h5 class="mb-0"><i class="bi bi-shield-lock text-success"></i> Compliance & audit</h5>
|
|
957
949
|
<span class="badge bg-secondary" id="compliance-profile-badge">default</span>
|
|
958
950
|
</div>
|
|
959
|
-
<p class="text-muted small mb-3">Set retention policies and access controls. In Mode A,
|
|
951
|
+
<p class="text-muted small mb-3">Set retention policies and access controls. In Mode A nothing leaves your device, which is a control your compliance programme can rely on — it is not a compliance certification, and this product does not issue one.</p>
|
|
960
952
|
<div class="row g-3 mb-3">
|
|
961
953
|
<div class="col-md-4"><div class="border rounded p-2 text-center"><div class="fw-bold fs-4" id="cp-audit-count">-</div><small class="text-muted">Audit events</small></div></div>
|
|
962
954
|
<div class="col-md-4"><div class="border rounded p-2 text-center"><div class="fw-bold fs-4" id="cp-retention-count">-</div><small class="text-muted">Retention policies</small></div></div>
|
|
@@ -1084,7 +1076,7 @@
|
|
|
1084
1076
|
<div class="btn-group w-100" role="group">
|
|
1085
1077
|
<input type="radio" class="btn-check" name="settings-mode-radio" id="mode-a-radio" value="a" checked>
|
|
1086
1078
|
<label class="btn btn-outline-success" for="mode-a-radio">
|
|
1087
|
-
<strong>Mode A</strong><br><small>Zero Cloud
|
|
1079
|
+
<strong>Mode A</strong><br><small>Zero Cloud</small>
|
|
1088
1080
|
</label>
|
|
1089
1081
|
<input type="radio" class="btn-check" name="settings-mode-radio" id="mode-b-radio" value="b">
|
|
1090
1082
|
<label class="btn btn-outline-info" for="mode-b-radio">
|
|
@@ -1585,7 +1577,6 @@
|
|
|
1585
1577
|
<!-- Neural Glass shell (v3.4.21 restructured) -->
|
|
1586
1578
|
<script src="static/js/ng-health.js?v=345"></script>
|
|
1587
1579
|
<script src="static/js/ng-ingestion.js?v=345"></script>
|
|
1588
|
-
<script src="static/js/ng-entities.js?v=3410"></script>
|
|
1589
1580
|
<script src="static/js/ng-skills.js?v=3411"></script>
|
|
1590
1581
|
<script src="static/js/ng-mesh.js?v=345"></script>
|
|
1591
1582
|
<!-- OD Shell v1.0 — replaces ng-shell.js; CSP-safe single-page tab switching -->
|
|
@@ -1604,7 +1595,7 @@
|
|
|
1604
1595
|
<script src="static/js/od-ops-health.js?v=400"></script>
|
|
1605
1596
|
<script src="static/js/od-team.js?v=379"></script>
|
|
1606
1597
|
<script src="static/js/od-graph.js?v=6812bf6c"></script>
|
|
1607
|
-
<script src="static/js/od-memories.js?v=
|
|
1598
|
+
<script src="static/js/od-memories.js?v=997f1674"></script>
|
|
1608
1599
|
<script src="static/js/od-entities.js?v=379"></script>
|
|
1609
1600
|
<!-- Multi-Agent Memory pane (v3.8.0): visualises memory written by multiple agents -->
|
|
1610
1601
|
<script src="static/js/od-agents.js?v=c75ff3c2"></script>
|
|
@@ -38,7 +38,18 @@
|
|
|
38
38
|
'load-agents': () => loadAgents(),
|
|
39
39
|
'load-clusters': () => loadClusters(),
|
|
40
40
|
'load-compliance': () => loadCompliance(),
|
|
41
|
-
|
|
41
|
+
// Re-renders through the current renderer. The button used to call a
|
|
42
|
+
// function from a superseded implementation of this pane, which was
|
|
43
|
+
// removed; naming it directly would have made Refresh throw.
|
|
44
|
+
'load-entity-explorer': () => {
|
|
45
|
+
const pane = document.getElementById('entities-pane');
|
|
46
|
+
if (pane && typeof window.odRenderEntities === 'function') {
|
|
47
|
+
pane.innerHTML = '';
|
|
48
|
+
window.odRenderEntities(pane);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (typeof window.loadEntityExplorer === 'function') window.loadEntityExplorer();
|
|
52
|
+
},
|
|
42
53
|
'load-graph': () => loadGraph(),
|
|
43
54
|
'load-health-monitor': () => loadHealthMonitor(),
|
|
44
55
|
'load-ingestion-status': () => loadIngestionStatus(),
|
|
@@ -216,12 +216,33 @@
|
|
|
216
216
|
var strip = document.getElementById('od-h-status');
|
|
217
217
|
if (!strip) return;
|
|
218
218
|
|
|
219
|
-
// Card 1: Daemon — from /health
|
|
220
|
-
|
|
221
|
-
|
|
219
|
+
// Card 1: Daemon — from /health.
|
|
220
|
+
// `status` is the literal string "ok" on every reply the daemon is alive
|
|
221
|
+
// enough to send, so reading it said "Healthy" for a daemon that had not
|
|
222
|
+
// finished starting, or whose search was not answering. `runtime_state` is
|
|
223
|
+
// the field that actually varies.
|
|
224
|
+
var runtime = healthData ? String(healthData.runtime_state || '') : '';
|
|
225
|
+
var RUNTIME_LABELS = {
|
|
226
|
+
serving_full: 'Healthy',
|
|
227
|
+
serving_degraded: 'Degraded',
|
|
228
|
+
warming: 'Starting up',
|
|
229
|
+
not_ready: 'Not ready'
|
|
230
|
+
};
|
|
231
|
+
var daemonCls = !healthData ? 'warn'
|
|
232
|
+
: (runtime === 'serving_full' ? 'ok'
|
|
233
|
+
: (runtime === 'warming' ? 'neutral' : 'warn'));
|
|
234
|
+
var daemonVal = !healthData ? 'Unreachable'
|
|
235
|
+
: (RUNTIME_LABELS[runtime] || (healthData.status === 'ok' ? 'Healthy' : 'Degraded'));
|
|
222
236
|
var daemonDetail = healthData
|
|
223
237
|
? 'port 8765 · v' + esc(String(healthData.version || '?'))
|
|
224
238
|
: 'daemon unreachable';
|
|
239
|
+
if (healthData && healthData.projection && healthData.projection.behind) {
|
|
240
|
+
// The graph copy being behind is not a daemon fault, and it does change
|
|
241
|
+
// what a search returns, so it belongs on the card the operator reads.
|
|
242
|
+
daemonDetail += ' · graph copy ' + esc(String(healthData.projection.depth || 0))
|
|
243
|
+
+ ' behind';
|
|
244
|
+
if (daemonCls === 'ok') daemonCls = 'neutral';
|
|
245
|
+
}
|
|
225
246
|
|
|
226
247
|
// Card 2: Memory DB — from /api/stats
|
|
227
248
|
var dbMb = (statsData && statsData.overview)
|
|
@@ -239,10 +260,11 @@
|
|
|
239
260
|
var mathVal = mathCls === 'ok' ? 'Active' : (mathOverall ? esc(mathOverall) : 'Unknown');
|
|
240
261
|
var mathDetail = esc(String(mathLayerCount)) + ' / 3 layers online';
|
|
241
262
|
|
|
242
|
-
// Card 4: Mesh broker
|
|
263
|
+
// Card 4: Mesh broker. There is no endpoint for this yet, so the card says
|
|
264
|
+
// so rather than implying something was measured and came back unknown.
|
|
243
265
|
var meshCls = 'neutral';
|
|
244
|
-
var meshVal = '
|
|
245
|
-
var meshDetail = '
|
|
266
|
+
var meshVal = 'Not set up';
|
|
267
|
+
var meshDetail = 'coordinating several machines is not configured here';
|
|
246
268
|
|
|
247
269
|
var CARDS = [
|
|
248
270
|
{ label: 'Daemon', cls: daemonCls, value: daemonVal, detail: daemonDetail },
|