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
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
|
|
4
|
+
"""What happened to each retrieval channel on one recall.
|
|
5
|
+
|
|
6
|
+
WHY THIS EXISTS
|
|
7
|
+
---------------
|
|
8
|
+
A channel that crashes on every query and a channel that correctly found
|
|
9
|
+
nothing produced the same observable result: absence. Both simply had no entry
|
|
10
|
+
in the fused candidate map, and the only trace was a log line on a machine
|
|
11
|
+
nobody is reading.
|
|
12
|
+
|
|
13
|
+
That matters because the channels are not interchangeable. Lexical search
|
|
14
|
+
finding nothing for a conceptual question is the system working. Lexical search
|
|
15
|
+
raising on every question is an outage that looks, from the outside, like a
|
|
16
|
+
store with nothing in it — the user sees weaker answers and has no way to tell
|
|
17
|
+
which of the two they are getting.
|
|
18
|
+
|
|
19
|
+
There is a third case the absence hid, and it is the worst of them: a channel
|
|
20
|
+
that never ran. When the query embedding is unavailable, three of the five
|
|
21
|
+
channels are never even dispatched. Nothing in the answer said so.
|
|
22
|
+
|
|
23
|
+
WHY MORE THAN "ok / empty / error"
|
|
24
|
+
----------------------------------
|
|
25
|
+
Because the remedies differ, and a status whose remedy is ambiguous is a status
|
|
26
|
+
nobody acts on. ``error`` is a bug to fix. ``timeout`` is a capacity or data-size
|
|
27
|
+
problem. ``no_embedding`` means the embedding provider is down and several
|
|
28
|
+
channels are silently offline together. ``disabled`` and ``not_configured`` are
|
|
29
|
+
someone's deliberate choice and must not read as faults — which is the point:
|
|
30
|
+
without naming them, an operator reading a list of missing channels cannot tell
|
|
31
|
+
their own configuration apart from a failure.
|
|
32
|
+
|
|
33
|
+
STRINGS, NOT AN ENUM
|
|
34
|
+
--------------------
|
|
35
|
+
This crosses the MCP and HTTP surfaces, where it is JSON either way. A str-valued
|
|
36
|
+
constant serialises without a custom encoder and compares equal to what a client
|
|
37
|
+
sends back.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
from typing import Literal
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"ALL_STATUSES",
|
|
46
|
+
"CHANNEL_NAMES",
|
|
47
|
+
"ChannelStatus",
|
|
48
|
+
"DISABLED",
|
|
49
|
+
"EMPTY",
|
|
50
|
+
"ERROR",
|
|
51
|
+
"NOT_CONFIGURED",
|
|
52
|
+
"NO_CANDIDATES",
|
|
53
|
+
"NO_EMBEDDING",
|
|
54
|
+
"OK",
|
|
55
|
+
"TIMEOUT",
|
|
56
|
+
"is_fault",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
ChannelStatus = Literal[
|
|
60
|
+
"ok",
|
|
61
|
+
"empty",
|
|
62
|
+
"error",
|
|
63
|
+
"timeout",
|
|
64
|
+
"disabled",
|
|
65
|
+
"not_configured",
|
|
66
|
+
"no_embedding",
|
|
67
|
+
"no_candidates",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
#: Ran and contributed candidates.
|
|
71
|
+
OK: ChannelStatus = "ok"
|
|
72
|
+
#: Ran, contributed nothing. A legitimate answer, not a fault.
|
|
73
|
+
EMPTY: ChannelStatus = "empty"
|
|
74
|
+
#: Raised. This answer is missing whatever this channel alone could see.
|
|
75
|
+
ERROR: ChannelStatus = "error"
|
|
76
|
+
#: Abandoned at the hang guard. The answer is incomplete, not merely late.
|
|
77
|
+
TIMEOUT: ChannelStatus = "timeout"
|
|
78
|
+
#: Excluded by configuration or by a per-recall ablation flag.
|
|
79
|
+
DISABLED: ChannelStatus = "disabled"
|
|
80
|
+
#: No such channel on this engine — not built, or its dependency is absent.
|
|
81
|
+
NOT_CONFIGURED: ChannelStatus = "not_configured"
|
|
82
|
+
#: Needed the query embedding, which was unavailable. Several channels fail
|
|
83
|
+
#: together this way, and none of them individually did anything wrong.
|
|
84
|
+
NO_EMBEDDING: ChannelStatus = "no_embedding"
|
|
85
|
+
#: Had nothing to work on. Distinct from ``empty``, which claims a search
|
|
86
|
+
#: happened: a channel that re-scores other channels' candidates never searched
|
|
87
|
+
#: at all when there were none, and if the reason there were none is that the
|
|
88
|
+
#: others failed, calling this "found nothing" hides the actual fault.
|
|
89
|
+
NO_CANDIDATES: ChannelStatus = "no_candidates"
|
|
90
|
+
|
|
91
|
+
ALL_STATUSES: frozenset[str] = frozenset({
|
|
92
|
+
OK, EMPTY, ERROR, TIMEOUT, DISABLED, NOT_CONFIGURED, NO_EMBEDDING,
|
|
93
|
+
NO_CANDIDATES,
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
#: Every channel a recall can report on, so a caller can tell "this channel had
|
|
97
|
+
#: no status recorded" (a gap in the reporting) from "this channel reported that
|
|
98
|
+
#: it did nothing" (an answer). A missing key is a bug; ``empty`` is not.
|
|
99
|
+
CHANNEL_NAMES: tuple[str, ...] = (
|
|
100
|
+
"semantic",
|
|
101
|
+
"bm25",
|
|
102
|
+
"temporal",
|
|
103
|
+
"hopfield",
|
|
104
|
+
"spreading_activation",
|
|
105
|
+
"entity_graph",
|
|
106
|
+
"profile",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
#: Statuses that mean the answer is worse than it should have been. ``empty``,
|
|
110
|
+
#: ``disabled`` and ``not_configured`` are deliberately absent: the first is a
|
|
111
|
+
#: valid finding and the other two are somebody's decision.
|
|
112
|
+
_FAULTS: frozenset[str] = frozenset({ERROR, TIMEOUT, NO_EMBEDDING})
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def is_fault(status: str | None) -> bool:
|
|
116
|
+
"""Whether this status means the answer was degraded."""
|
|
117
|
+
return (status or "") in _FAULTS
|
|
@@ -26,7 +26,12 @@ import threading
|
|
|
26
26
|
import time
|
|
27
27
|
from typing import TYPE_CHECKING, Any, Protocol
|
|
28
28
|
|
|
29
|
-
from superlocalmemory.core.config import
|
|
29
|
+
from superlocalmemory.core.config import (
|
|
30
|
+
CANONICAL_RECALL_LIMIT,
|
|
31
|
+
ChannelWeights,
|
|
32
|
+
RetrievalConfig,
|
|
33
|
+
)
|
|
34
|
+
from superlocalmemory.retrieval import channel_status as chstat
|
|
30
35
|
from superlocalmemory.retrieval.fusion import FusionResult, weighted_rrf
|
|
31
36
|
from superlocalmemory.retrieval.strategy import QueryStrategy, QueryStrategyClassifier
|
|
32
37
|
from superlocalmemory.retrieval.temporal_validity_filter import (
|
|
@@ -195,7 +200,7 @@ class RetrievalEngine:
|
|
|
195
200
|
|
|
196
201
|
def recall(
|
|
197
202
|
self, query: str, profile_id: str,
|
|
198
|
-
mode: Mode = Mode.A, limit: int =
|
|
203
|
+
mode: Mode = Mode.A, limit: int = CANONICAL_RECALL_LIMIT,
|
|
199
204
|
*,
|
|
200
205
|
extra_disabled_channels: set[str] | None = None,
|
|
201
206
|
include_global: bool = False,
|
|
@@ -246,19 +251,41 @@ class RetrievalEngine:
|
|
|
246
251
|
strat = self._strategy.classify(query, self._base_weights)
|
|
247
252
|
_em("classify")
|
|
248
253
|
|
|
249
|
-
#
|
|
250
|
-
|
|
254
|
+
# What each channel did, so the caller can tell an answer apart from
|
|
255
|
+
# an outage. Owned by this call for the same reason the dropped set is:
|
|
256
|
+
# a dict on the engine would have two concurrent recalls overwriting
|
|
257
|
+
# each other's report.
|
|
258
|
+
channel_status: dict[str, str] = {}
|
|
259
|
+
|
|
260
|
+
# Profile shortcut (runs before channel search).
|
|
261
|
+
#
|
|
262
|
+
# The ablation flag is honoured here too. It was not, so an operator who
|
|
263
|
+
# switched this channel off still had it searched, still had its weight
|
|
264
|
+
# doubled on a hit, and read a status of "ok" or "empty" — which reports
|
|
265
|
+
# their own configuration as a live channel's finding.
|
|
266
|
+
_profile_disabled = "profile" in set(self._config.disabled_channels) | set(
|
|
267
|
+
extra_disabled_channels or (),
|
|
268
|
+
)
|
|
269
|
+
if _profile_disabled:
|
|
270
|
+
profile_hits = []
|
|
271
|
+
channel_status["profile"] = chstat.DISABLED
|
|
272
|
+
elif self._profile_channel is not None:
|
|
251
273
|
try:
|
|
252
274
|
profile_hits = self._profile_channel.search(
|
|
253
275
|
query, profile_id, top_k=10,
|
|
254
276
|
)
|
|
277
|
+
channel_status["profile"] = (
|
|
278
|
+
chstat.OK if profile_hits else chstat.EMPTY
|
|
279
|
+
)
|
|
255
280
|
if profile_hits:
|
|
256
281
|
strat.weights["profile"] = 2.0
|
|
257
282
|
except Exception as exc:
|
|
258
283
|
logger.warning("Profile channel: %s", exc)
|
|
259
284
|
profile_hits = []
|
|
285
|
+
channel_status["profile"] = chstat.ERROR
|
|
260
286
|
else:
|
|
261
287
|
profile_hits = []
|
|
288
|
+
channel_status["profile"] = chstat.NOT_CONFIGURED
|
|
262
289
|
|
|
263
290
|
# Dynamic top-k for aggregation queries
|
|
264
291
|
effective_limit = 100 if strat.query_type == "aggregation" else limit
|
|
@@ -276,6 +303,7 @@ class RetrievalEngine:
|
|
|
276
303
|
as_of=as_of, known_as_of=known_as_of, valid_at=valid_at,
|
|
277
304
|
include_unknown=include_unknown,
|
|
278
305
|
dropped_channels=dropped_channels,
|
|
306
|
+
channel_status=channel_status,
|
|
279
307
|
)
|
|
280
308
|
_em("run_channels")
|
|
281
309
|
# One request may need admission before fusion and again after optional
|
|
@@ -393,9 +421,29 @@ class RetrievalEngine:
|
|
|
393
421
|
# this stage re-scores every fused candidate and then re-sorts them, so
|
|
394
422
|
# whether it ran decided the top answer outright rather than adding to
|
|
395
423
|
# it. Bound by data if it ever needs bounding, never by elapsed time.
|
|
396
|
-
if
|
|
397
|
-
|
|
398
|
-
|
|
424
|
+
if self._entity is None:
|
|
425
|
+
channel_status["entity_graph"] = chstat.NOT_CONFIGURED
|
|
426
|
+
elif "entity_graph" in set(self._config.disabled_channels) | set(
|
|
427
|
+
extra_disabled_channels or (),
|
|
428
|
+
):
|
|
429
|
+
# Both the configured set AND the per-call one, the same way the
|
|
430
|
+
# profile channel resolves it. Reading only the configured set meant
|
|
431
|
+
# a caller that switched this channel off for one recall still had
|
|
432
|
+
# it run, and still saw it reported as though it were the caller's
|
|
433
|
+
# own setting that was being honoured.
|
|
434
|
+
channel_status["entity_graph"] = chstat.DISABLED
|
|
435
|
+
elif not fused:
|
|
436
|
+
# It did not run, and saying "found nothing" would be a different
|
|
437
|
+
# claim. This channel re-scores other channels' candidates rather
|
|
438
|
+
# than producing its own, so with nothing fused there is nothing for
|
|
439
|
+
# it to do — and if the reason nothing fused is that the other five
|
|
440
|
+
# failed, reporting this one as having searched and come back empty
|
|
441
|
+
# hides that.
|
|
442
|
+
channel_status["entity_graph"] = chstat.NO_CANDIDATES
|
|
443
|
+
else:
|
|
444
|
+
# One chain, evaluated once. Repeating the three conditions to guard
|
|
445
|
+
# the work separately is how a status starts describing a decision
|
|
446
|
+
# the code no longer makes.
|
|
399
447
|
try:
|
|
400
448
|
candidate_ids = [fr.fact_id for fr in fused[:100]]
|
|
401
449
|
eg_scores = self._entity.score_candidates(
|
|
@@ -405,6 +453,9 @@ class RetrievalEngine:
|
|
|
405
453
|
include_global=include_global,
|
|
406
454
|
include_shared=include_shared,
|
|
407
455
|
)
|
|
456
|
+
channel_status["entity_graph"] = (
|
|
457
|
+
chstat.OK if eg_scores else chstat.EMPTY
|
|
458
|
+
)
|
|
408
459
|
if eg_scores:
|
|
409
460
|
boosted = []
|
|
410
461
|
for fr in fused:
|
|
@@ -423,6 +474,7 @@ class RetrievalEngine:
|
|
|
423
474
|
fused = sorted(boosted, key=lambda r: (-r.fused_score, r.fact_id))
|
|
424
475
|
except Exception as exc:
|
|
425
476
|
logger.warning("Entity graph signal enhancement: %s", exc)
|
|
477
|
+
channel_status["entity_graph"] = chstat.ERROR
|
|
426
478
|
|
|
427
479
|
# Brain Core S402: bridge and scene expansion append candidates after
|
|
428
480
|
# the channel boundary. Reapply the same hard correction-admission rule
|
|
@@ -565,6 +617,7 @@ class RetrievalEngine:
|
|
|
565
617
|
# community. Precomputed summary lookup only — no per-query LLM.
|
|
566
618
|
community_context=self._community_context(results, profile_id),
|
|
567
619
|
incomplete_channels=tuple(sorted(dropped_channels)),
|
|
620
|
+
channel_status=dict(channel_status),
|
|
568
621
|
)
|
|
569
622
|
|
|
570
623
|
# -- Community context (Wave Q2b) --------------------------------------
|
|
@@ -977,6 +1030,7 @@ class RetrievalEngine:
|
|
|
977
1030
|
valid_at: str | None = None,
|
|
978
1031
|
include_unknown: bool = False,
|
|
979
1032
|
dropped_channels: set[str] | None = None,
|
|
1033
|
+
channel_status: dict[str, str] | None = None,
|
|
980
1034
|
) -> dict[str, list[tuple[str, float]]]:
|
|
981
1035
|
"""Run active retrieval channels.
|
|
982
1036
|
|
|
@@ -994,6 +1048,12 @@ class RetrievalEngine:
|
|
|
994
1048
|
than merely late. It is a caller-owned set passed down per recall and
|
|
995
1049
|
deliberately not an attribute of self — two concurrent recalls sharing
|
|
996
1050
|
one would report each other's losses (the v3.4.64 race).
|
|
1051
|
+
|
|
1052
|
+
``channel_status``, likewise caller-owned, receives one entry per
|
|
1053
|
+
channel saying what became of it. Every channel gets exactly one:
|
|
1054
|
+
those that cannot run are recorded before dispatch with the reason, and
|
|
1055
|
+
every dispatched channel is recorded by the collection loop below —
|
|
1056
|
+
which iterates the futures, so it cannot skip one.
|
|
997
1057
|
"""
|
|
998
1058
|
import os as _os_e
|
|
999
1059
|
import time as _time_e
|
|
@@ -1023,23 +1083,50 @@ class RetrievalEngine:
|
|
|
1023
1083
|
except Exception as exc:
|
|
1024
1084
|
logger.warning("Query embedding failed: %s", exc)
|
|
1025
1085
|
|
|
1086
|
+
# Why a channel will not run, recorded BEFORE dispatch. An embedding
|
|
1087
|
+
# failure silently takes three of the five channels down together, and
|
|
1088
|
+
# the answer never said so: it looked exactly like a store with nothing
|
|
1089
|
+
# relevant in it. Configuration and ablation are recorded too, so an
|
|
1090
|
+
# operator reading a list of absent channels can tell their own choices
|
|
1091
|
+
# apart from a fault.
|
|
1092
|
+
if channel_status is not None:
|
|
1093
|
+
for _name, _obj, _needs_emb in (
|
|
1094
|
+
("semantic", self._semantic, True),
|
|
1095
|
+
("bm25", self._bm25, False),
|
|
1096
|
+
("temporal", self._temporal, False),
|
|
1097
|
+
("hopfield", self._hopfield, True),
|
|
1098
|
+
("spreading_activation", self._spreading_activation, True),
|
|
1099
|
+
):
|
|
1100
|
+
if _obj is None:
|
|
1101
|
+
channel_status[_name] = chstat.NOT_CONFIGURED
|
|
1102
|
+
elif _name in disabled:
|
|
1103
|
+
channel_status[_name] = chstat.DISABLED
|
|
1104
|
+
elif _needs_emb and q_emb is None:
|
|
1105
|
+
channel_status[_name] = chstat.NO_EMBEDDING
|
|
1106
|
+
|
|
1026
1107
|
# v3.4.53: collect channel callables and run in parallel.
|
|
1027
1108
|
# Each channel is a standalone search — no shared mutable state,
|
|
1028
1109
|
# no ordering dependencies. SQLite WAL mode permits concurrent reads.
|
|
1029
1110
|
futures: dict[str, concurrent.futures.Future] = {}
|
|
1030
1111
|
|
|
1031
1112
|
def _safe_channel(name: str, fn, *args):
|
|
1032
|
-
"""Run a single channel, returning (name, result_or_None).
|
|
1113
|
+
"""Run a single channel, returning (name, result_or_None, status).
|
|
1114
|
+
|
|
1115
|
+
Returning the status alongside the result is what separates "found
|
|
1116
|
+
nothing" from "raised": both used to come back as ``None``.
|
|
1117
|
+
"""
|
|
1033
1118
|
_cs = _time_e.monotonic() if _et else 0.0
|
|
1034
1119
|
try:
|
|
1035
1120
|
res = fn(*args)
|
|
1036
1121
|
if _et:
|
|
1037
1122
|
logger.warning("[RECALL-TIMING] channel.%-16s %.0f ms",
|
|
1038
1123
|
name, (_time_e.monotonic() - _cs) * 1000.0)
|
|
1039
|
-
|
|
1124
|
+
if res:
|
|
1125
|
+
return (name, res, chstat.OK)
|
|
1126
|
+
return (name, None, chstat.EMPTY)
|
|
1040
1127
|
except Exception as exc:
|
|
1041
1128
|
logger.warning("%s channel: %s", name, exc)
|
|
1042
|
-
return (name, None)
|
|
1129
|
+
return (name, None, chstat.ERROR)
|
|
1043
1130
|
|
|
1044
1131
|
executor = self._channel_executor
|
|
1045
1132
|
if self._semantic is not None and q_emb is not None and "semantic" not in disabled:
|
|
@@ -1111,14 +1198,22 @@ class RetrievalEngine:
|
|
|
1111
1198
|
)
|
|
1112
1199
|
if dropped_channels is not None:
|
|
1113
1200
|
dropped_channels.add(name)
|
|
1201
|
+
# Same branch as the dropped set on purpose: two writes in one
|
|
1202
|
+
# place cannot disagree about which channels timed out.
|
|
1203
|
+
if channel_status is not None:
|
|
1204
|
+
channel_status[name] = chstat.TIMEOUT
|
|
1114
1205
|
fut.cancel() # no-op if already running; prevents queued jobs from starting
|
|
1115
1206
|
continue
|
|
1116
1207
|
try:
|
|
1117
|
-
ch_name, result = fut.result()
|
|
1208
|
+
ch_name, result, status = fut.result()
|
|
1209
|
+
if channel_status is not None:
|
|
1210
|
+
channel_status[ch_name] = status
|
|
1118
1211
|
if result:
|
|
1119
1212
|
out[ch_name] = result
|
|
1120
1213
|
except Exception as exc:
|
|
1121
1214
|
logger.warning("Channel %s failed: %s", name, exc)
|
|
1215
|
+
if channel_status is not None:
|
|
1216
|
+
channel_status[name] = chstat.ERROR
|
|
1122
1217
|
|
|
1123
1218
|
# Apply registered post-retrieval filters (forgetting filter, etc.).
|
|
1124
1219
|
# Pass as_of in context dict when set so the bi-temporal validity filter
|