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
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
3
|
# Part of SuperLocalMemory V3
|
|
4
4
|
|
|
5
|
-
"""SuperLocalMemory V3.3 -- Hopfield Associative Memory (6th Retrieval
|
|
5
|
+
"""SuperLocalMemory V3.3 -- Hopfield Associative Memory (6th of 6 Retrieval Channels).
|
|
6
6
|
|
|
7
7
|
Modern Continuous Hopfield Network retrieval channel based on
|
|
8
8
|
Ramsauer et al. (2020): "Hopfield Networks is All You Need".
|
|
9
9
|
|
|
10
|
+
Channel lineup (6 total): semantic, BM25, entity_graph, temporal,
|
|
11
|
+
spreading_activation, hopfield (this module).
|
|
12
|
+
|
|
10
13
|
The Hopfield channel excels at pattern completion for vague/noisy queries.
|
|
11
14
|
It operates on the same embedding space as the semantic channel but uses
|
|
12
15
|
an energy-based attention mechanism instead of cosine similarity.
|
|
@@ -14,7 +17,7 @@ an energy-based attention mechanism instead of cosine similarity.
|
|
|
14
17
|
Key features:
|
|
15
18
|
- Full memory matrix path for stores < 10K facts
|
|
16
19
|
- ANN pre-filter path for stores 10K-100K (VectorStore KNN -> Hopfield refinement)
|
|
17
|
-
- Skip path for stores > 100K (other 5 channels are sufficient)
|
|
20
|
+
- Skip path for stores > 100K (other 5 non-Hopfield channels are sufficient)
|
|
18
21
|
- TTL-based matrix cache to avoid rebuilding every query
|
|
19
22
|
- Returns [] on any error (HR-06)
|
|
20
23
|
|
|
@@ -38,7 +41,7 @@ logger = logging.getLogger(__name__)
|
|
|
38
41
|
|
|
39
42
|
|
|
40
43
|
class HopfieldChannel:
|
|
41
|
-
"""6th retrieval
|
|
44
|
+
"""6th of 6 retrieval channels: Modern Hopfield associative memory.
|
|
42
45
|
|
|
43
46
|
Implements the RetrievalChannel protocol::
|
|
44
47
|
|
|
@@ -48,6 +51,9 @@ class HopfieldChannel:
|
|
|
48
51
|
computes Hopfield attention scores (softmax of scaled dot products),
|
|
49
52
|
then ranks facts by similarity to the completed pattern.
|
|
50
53
|
|
|
54
|
+
Six-channel retrieval model: semantic, BM25, entity_graph, temporal,
|
|
55
|
+
spreading_activation, hopfield (this channel).
|
|
56
|
+
|
|
51
57
|
Routing logic (per LLD Section 2.2):
|
|
52
58
|
- n > skip_threshold (100K): return [] immediately
|
|
53
59
|
- n > prefilter_threshold (10K): ANN pre-filter + Hopfield on subset
|
|
@@ -48,6 +48,7 @@ Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
|
48
48
|
from __future__ import annotations
|
|
49
49
|
|
|
50
50
|
import json
|
|
51
|
+
import ipaddress
|
|
51
52
|
import logging
|
|
52
53
|
import math
|
|
53
54
|
import os
|
|
@@ -92,9 +93,6 @@ _MAX_ATTEMPTS = 2
|
|
|
92
93
|
# logs; the operator must never have to guess whether reranking is running.
|
|
93
94
|
_FAILURE_RELOG_INTERVAL_S = 60.0
|
|
94
95
|
|
|
95
|
-
_ERROR_BODY_SNIPPET_CHARS = 200
|
|
96
|
-
|
|
97
|
-
|
|
98
96
|
class RemoteRerankerError(RuntimeError):
|
|
99
97
|
"""A remote rerank request failed (transport, status, or schema)."""
|
|
100
98
|
|
|
@@ -161,6 +159,12 @@ def _validate_endpoint_url(endpoint: str) -> str | None:
|
|
|
161
159
|
"retrieval.cross_encoder_endpoint has no host; expected something "
|
|
162
160
|
"like \"http://127.0.0.1:8041/v1/rerank\"."
|
|
163
161
|
)
|
|
162
|
+
if parsed.query or parsed.fragment:
|
|
163
|
+
return (
|
|
164
|
+
"retrieval.cross_encoder_endpoint must not include a query string "
|
|
165
|
+
"or fragment. Put bearer credentials in "
|
|
166
|
+
"SLM_CROSS_ENCODER_API_KEY and configure a clean endpoint URL."
|
|
167
|
+
)
|
|
164
168
|
if parsed.username or parsed.password:
|
|
165
169
|
# httpx logs "HTTP Request: POST <url>" at INFO using str(url), which
|
|
166
170
|
# renders an embedded password in full. This module never logs the raw
|
|
@@ -173,9 +177,26 @@ def _validate_endpoint_url(endpoint: str) -> str | None:
|
|
|
173
177
|
"retrieval.cross_encoder_api_key; it is sent as a Bearer header "
|
|
174
178
|
"and never logged."
|
|
175
179
|
)
|
|
180
|
+
if parsed.scheme == "http" and not _is_loopback_host(parsed.hostname):
|
|
181
|
+
return (
|
|
182
|
+
"retrieval.cross_encoder_endpoint must use HTTPS for non-loopback "
|
|
183
|
+
"hosts because recall queries and candidate memory text cross this "
|
|
184
|
+
"connection. Plain HTTP is allowed only for localhost/loopback."
|
|
185
|
+
)
|
|
176
186
|
return None
|
|
177
187
|
|
|
178
188
|
|
|
189
|
+
def _is_loopback_host(hostname: str) -> bool:
|
|
190
|
+
"""Return True only for literal loopback names/addresses (no DNS trust)."""
|
|
191
|
+
host = (hostname or "").rstrip(".").lower()
|
|
192
|
+
if host == "localhost" or host.endswith(".localhost"):
|
|
193
|
+
return True
|
|
194
|
+
try:
|
|
195
|
+
return ipaddress.ip_address(host).is_loopback
|
|
196
|
+
except ValueError:
|
|
197
|
+
return False
|
|
198
|
+
|
|
199
|
+
|
|
179
200
|
def normalize_rerank_endpoint(endpoint: str) -> str:
|
|
180
201
|
"""Append ``/rerank`` when the URL stops at the API root.
|
|
181
202
|
|
|
@@ -208,7 +229,15 @@ def redact_endpoint(endpoint: str) -> str:
|
|
|
208
229
|
netloc = f"{netloc}:{parsed.port}"
|
|
209
230
|
if parsed.username or parsed.password:
|
|
210
231
|
netloc = f"***@{netloc}"
|
|
211
|
-
return urlunparse(parsed._replace(netloc=netloc))
|
|
232
|
+
return urlunparse(parsed._replace(netloc=netloc, query="", fragment=""))
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _redact_remote_text(text: str) -> str:
|
|
236
|
+
"""Remove recognized secrets and PII before a remote trust boundary."""
|
|
237
|
+
from superlocalmemory.core.pii import redact_pii_text
|
|
238
|
+
from superlocalmemory.core.security_primitives import redact_secrets
|
|
239
|
+
|
|
240
|
+
return redact_pii_text(redact_secrets(str(text), aggression="high"))
|
|
212
241
|
|
|
213
242
|
|
|
214
243
|
# ---------------------------------------------------------------------------
|
|
@@ -240,15 +269,14 @@ def parse_rerank_response(payload: Any, expected: int) -> list[float]:
|
|
|
240
269
|
index = _coerce_index(item, position, expected)
|
|
241
270
|
if scores[index] is not None:
|
|
242
271
|
raise RemoteRerankerError(
|
|
243
|
-
|
|
272
|
+
"rerank endpoint returned a duplicate document index"
|
|
244
273
|
)
|
|
245
274
|
scores[index] = _coerce_score(item, index)
|
|
246
275
|
|
|
247
276
|
missing = [i for i, s in enumerate(scores) if s is None]
|
|
248
277
|
if missing:
|
|
249
278
|
raise RemoteRerankerError(
|
|
250
|
-
|
|
251
|
-
f"{missing[:5]}{'…' if len(missing) > 5 else ''}"
|
|
279
|
+
"rerank endpoint omitted one or more document scores"
|
|
252
280
|
)
|
|
253
281
|
return [float(s) for s in scores] # type: ignore[arg-type]
|
|
254
282
|
|
|
@@ -264,8 +292,8 @@ def _extract_results_array(payload: Any) -> list[Any]:
|
|
|
264
292
|
results = payload.get("results")
|
|
265
293
|
if results is None:
|
|
266
294
|
raise RemoteRerankerError(
|
|
267
|
-
|
|
268
|
-
|
|
295
|
+
"rerank response has no 'results' array. Is "
|
|
296
|
+
"cross_encoder_endpoint pointing at a "
|
|
269
297
|
f"rerank route and not, say, /v1/embeddings?"
|
|
270
298
|
)
|
|
271
299
|
if not isinstance(results, list):
|
|
@@ -280,12 +308,12 @@ def _coerce_index(item: dict, position: int, expected: int) -> int:
|
|
|
280
308
|
raw = item.get("index", position)
|
|
281
309
|
if isinstance(raw, bool) or not isinstance(raw, int):
|
|
282
310
|
raise RemoteRerankerError(
|
|
283
|
-
f"rerank result #{position} has non-integer index
|
|
311
|
+
f"rerank result #{position} has a non-integer index"
|
|
284
312
|
)
|
|
285
313
|
if not 0 <= raw < expected:
|
|
286
314
|
raise RemoteRerankerError(
|
|
287
|
-
f"rerank result #{position} has out-of-range index
|
|
288
|
-
f"
|
|
315
|
+
f"rerank result #{position} has an out-of-range index "
|
|
316
|
+
f"for a {expected}-document request"
|
|
289
317
|
)
|
|
290
318
|
return raw
|
|
291
319
|
|
|
@@ -297,18 +325,18 @@ def _coerce_score(item: dict, index: int) -> float:
|
|
|
297
325
|
if isinstance(raw, bool) or not isinstance(raw, (int, float)):
|
|
298
326
|
raise RemoteRerankerError(
|
|
299
327
|
f"rerank result for index {index} has non-numeric "
|
|
300
|
-
f"{key}
|
|
328
|
+
f"{key}"
|
|
301
329
|
)
|
|
302
330
|
value = float(raw)
|
|
303
331
|
if not math.isfinite(value):
|
|
304
332
|
raise RemoteRerankerError(
|
|
305
333
|
f"rerank result for index {index} has non-finite "
|
|
306
|
-
f"{key}
|
|
334
|
+
f"{key}"
|
|
307
335
|
)
|
|
308
336
|
return value
|
|
309
337
|
raise RemoteRerankerError(
|
|
310
338
|
f"rerank result for index {index} has neither 'relevance_score' nor "
|
|
311
|
-
|
|
339
|
+
"'score'"
|
|
312
340
|
)
|
|
313
341
|
|
|
314
342
|
|
|
@@ -494,8 +522,8 @@ class RemoteReranker:
|
|
|
494
522
|
headers["Authorization"] = f"Bearer {self._api_key}"
|
|
495
523
|
body = {
|
|
496
524
|
"model": self._model_name,
|
|
497
|
-
"query": query,
|
|
498
|
-
"documents": documents,
|
|
525
|
+
"query": _redact_remote_text(query),
|
|
526
|
+
"documents": [_redact_remote_text(document) for document in documents],
|
|
499
527
|
}
|
|
500
528
|
|
|
501
529
|
last_error: RemoteRerankerError | None = None
|
|
@@ -534,12 +562,9 @@ class RemoteReranker:
|
|
|
534
562
|
f"followed — configure the final URL directly."
|
|
535
563
|
)
|
|
536
564
|
if resp.status_code >= 400:
|
|
537
|
-
snippet = raw[:_ERROR_BODY_SNIPPET_CHARS].decode(
|
|
538
|
-
"utf-8", "replace",
|
|
539
|
-
)
|
|
540
565
|
message = (
|
|
541
|
-
f"HTTP {resp.status_code} from {self.safe_endpoint}
|
|
542
|
-
|
|
566
|
+
f"HTTP {resp.status_code} from {self.safe_endpoint}; "
|
|
567
|
+
"response body suppressed"
|
|
543
568
|
)
|
|
544
569
|
if resp.status_code >= 500:
|
|
545
570
|
raise _RetryableRemoteError(message)
|
|
@@ -433,7 +433,38 @@ class CrossEncoderReranker:
|
|
|
433
433
|
|
|
434
434
|
@staticmethod
|
|
435
435
|
def _readline_with_timeout(stream: Any, timeout_seconds: float) -> str:
|
|
436
|
-
"""Read a line from stream with timeout. Returns '' on timeout.
|
|
436
|
+
"""Read a line from stream with timeout. Returns '' on timeout.
|
|
437
|
+
|
|
438
|
+
Prefer a deadline-driven selector poll of the stream's file descriptor
|
|
439
|
+
(POSIX pipes). That path never spawns a helper thread, so a hung
|
|
440
|
+
worker cannot leak reader threads or pin the pipe FD across timeouts.
|
|
441
|
+
A thread fallback remains only for streams without a usable fileno
|
|
442
|
+
(unit-test mocks) and for Windows, where selectors cannot wait on
|
|
443
|
+
pipes.
|
|
444
|
+
"""
|
|
445
|
+
import selectors
|
|
446
|
+
|
|
447
|
+
timeout_seconds = max(0.0, float(timeout_seconds))
|
|
448
|
+
fd: int | None
|
|
449
|
+
try:
|
|
450
|
+
raw_fd = stream.fileno()
|
|
451
|
+
fd = raw_fd if isinstance(raw_fd, int) else None
|
|
452
|
+
except (AttributeError, OSError, ValueError, TypeError):
|
|
453
|
+
fd = None
|
|
454
|
+
|
|
455
|
+
# Windows select()/selectors only accept sockets, not subprocess pipes.
|
|
456
|
+
if fd is not None and sys.platform != "win32":
|
|
457
|
+
try:
|
|
458
|
+
with selectors.DefaultSelector() as sel:
|
|
459
|
+
sel.register(fd, selectors.EVENT_READ)
|
|
460
|
+
events = sel.select(timeout=timeout_seconds)
|
|
461
|
+
if not events:
|
|
462
|
+
return ""
|
|
463
|
+
line = stream.readline()
|
|
464
|
+
return line if line else ""
|
|
465
|
+
except (OSError, ValueError):
|
|
466
|
+
return ""
|
|
467
|
+
|
|
437
468
|
result_container: list[str] = []
|
|
438
469
|
error_container: list[Exception] = []
|
|
439
470
|
|
|
@@ -14,7 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
|
|
15
15
|
import logging
|
|
16
16
|
import math
|
|
17
|
-
from datetime import datetime
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
18
|
from typing import TYPE_CHECKING
|
|
19
19
|
|
|
20
20
|
from dateutil.parser import ParserError, parse as dateutil_parse
|
|
@@ -30,6 +30,18 @@ logger = logging.getLogger(__name__)
|
|
|
30
30
|
_MAX_PROXIMITY_DAYS: float = 365.0
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
def _as_utc(dt: datetime | None) -> datetime | None:
|
|
34
|
+
"""Coerce a datetime to timezone-aware UTC (naive values are assumed UTC).
|
|
35
|
+
|
|
36
|
+
Keeps proximity and interval comparisons from mixing naive and aware values.
|
|
37
|
+
"""
|
|
38
|
+
if dt is None:
|
|
39
|
+
return None
|
|
40
|
+
if dt.tzinfo is None:
|
|
41
|
+
return dt.replace(tzinfo=timezone.utc)
|
|
42
|
+
return dt.astimezone(timezone.utc)
|
|
43
|
+
|
|
44
|
+
|
|
33
45
|
def _parse_iso(s: str | None) -> datetime | None:
|
|
34
46
|
if not s:
|
|
35
47
|
return None
|
|
@@ -39,11 +51,11 @@ def _parse_iso(s: str | None) -> datetime | None:
|
|
|
39
51
|
# across thousands of events — that was ~2.6s of the recall. dateutil is
|
|
40
52
|
# now only the fallback for non-ISO strings.
|
|
41
53
|
try:
|
|
42
|
-
return datetime.fromisoformat(s.replace("Z", "+00:00"))
|
|
54
|
+
return _as_utc(datetime.fromisoformat(s.replace("Z", "+00:00")))
|
|
43
55
|
except (ValueError, TypeError):
|
|
44
56
|
pass
|
|
45
57
|
try:
|
|
46
|
-
return dateutil_parse(s)
|
|
58
|
+
return _as_utc(dateutil_parse(s))
|
|
47
59
|
except (ParserError, ValueError, OverflowError, TypeError):
|
|
48
60
|
return None
|
|
49
61
|
|
|
@@ -95,6 +107,7 @@ class TemporalChannel:
|
|
|
95
107
|
query_dt = _parse_iso(dates.get("referenced_date"))
|
|
96
108
|
if query_dt is None:
|
|
97
109
|
query_dt = self._try_parse(query)
|
|
110
|
+
query_dt = _as_utc(query_dt)
|
|
98
111
|
|
|
99
112
|
# Strategy 1: Entity-temporal metadata search
|
|
100
113
|
# "When did Alice...?" → find all temporal events for Alice
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V4 | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
|
|
5
|
+
"""Temporal utility helpers for bi-temporal retrieval (Phase 4b).
|
|
6
|
+
|
|
7
|
+
STORED TIMESTAMP FORMAT (empirically verified, Phase 4b):
|
|
8
|
+
The fact_temporal_validity table stores system_expired_at and valid_until
|
|
9
|
+
via datetime.now(timezone.utc).isoformat() which produces:
|
|
10
|
+
"YYYY-MM-DDTHH:MM:SS.microseconds+00:00" (Python isoformat, +00:00 suffix)
|
|
11
|
+
|
|
12
|
+
normalize_as_of() outputs canonical datetime.isoformat() (preserving any
|
|
13
|
+
sub-second precision, same +00:00 suffix as the stored values) so that SQL
|
|
14
|
+
string comparisons ("system_expired_at <= as_of") are lexicographically
|
|
15
|
+
correct AND the inclusive boundary round-trips (audit P0/CRIT-1):
|
|
16
|
+
|
|
17
|
+
"2026-03-01T00:00:00.123456+00:00" <= "2026-06-01T00:00:00+00:00"
|
|
18
|
+
→ first 10 chars "2026-03-01" < "2026-06-01" → TRUE ✓
|
|
19
|
+
|
|
20
|
+
At sub-second boundary (same second, stored has micros):
|
|
21
|
+
"2024-01-01T12:00:00.123456+00:00" <= "2024-01-01T12:00:00+00:00"
|
|
22
|
+
→ at pos 19: '.' (46) > '+' (43) → FALSE
|
|
23
|
+
Interpretation: stored supersession is 0.123456s AFTER as_of → fact
|
|
24
|
+
was still valid at that exact second. Semantically correct.
|
|
25
|
+
|
|
26
|
+
Contrast with SQLite strftime('%Y-%m-%dT%H:%M:%SZ', 'now') → "Z" suffix.
|
|
27
|
+
If as_of used "Z" suffix and stored uses "+00:00":
|
|
28
|
+
"2024-01-01T12:00:00.123456+00:00" <= "2024-01-01T12:00:00Z"
|
|
29
|
+
→ at pos 19: '.' (46) < 'Z' (90) → TRUE
|
|
30
|
+
This would WRONGLY demote a fact whose supersession happened 0.123456s
|
|
31
|
+
AFTER as_of. Using "+00:00" avoids this error.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
from datetime import datetime, timezone
|
|
37
|
+
from typing import Optional
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def normalize_as_of(s: object) -> Optional[str]:
|
|
41
|
+
"""Parse and normalize an as_of timestamp string to UTC ISO 8601.
|
|
42
|
+
|
|
43
|
+
Accepts:
|
|
44
|
+
- "2024-01-01T12:00:00Z" → "2024-01-01T12:00:00+00:00"
|
|
45
|
+
- "2024-01-01T12:00:00+05:30" → "2024-01-01T06:30:00+00:00"
|
|
46
|
+
- "2024-01-01" → "2024-01-01T00:00:00+00:00"
|
|
47
|
+
- "2024-01-01T12:00:00" → "2024-01-01T12:00:00+00:00" (naive → UTC)
|
|
48
|
+
- "2024-01-01T12:00:00+00:00" → "2024-01-01T12:00:00+00:00"
|
|
49
|
+
|
|
50
|
+
Returns None (not empty string) on invalid input so callers can
|
|
51
|
+
distinguish "no as_of" from "bad as_of" and reject or ignore accordingly.
|
|
52
|
+
|
|
53
|
+
Output format: canonical UTC ISO-8601 via datetime.isoformat():
|
|
54
|
+
- "YYYY-MM-DDTHH:MM:SS+00:00", OR
|
|
55
|
+
"YYYY-MM-DDTHH:MM:SS.ffffff+00:00" when the input carries sub-second
|
|
56
|
+
precision.
|
|
57
|
+
- Audit P0 (CRIT-1): sub-second precision is PRESERVED (not stripped) so
|
|
58
|
+
a caller can round-trip a real stored system_expired_at / valid_until
|
|
59
|
+
value — always written with microseconds by datetime.now(UTC).isoformat()
|
|
60
|
+
— and hit the inclusive boundary (`system_expired_at <= as_of`) EXACTLY.
|
|
61
|
+
Stripping microseconds made that boundary unreachable and silently
|
|
62
|
+
wrong on both temporal axes.
|
|
63
|
+
- +00:00 suffix matches the stored format so SQL string comparisons are
|
|
64
|
+
lexicographically correct (see module docstring for analysis).
|
|
65
|
+
|
|
66
|
+
Requirements:
|
|
67
|
+
Python >=3.11 (fromisoformat handles "Z" suffix natively). The
|
|
68
|
+
fallback .replace("Z", "+00:00") is kept for defensive compatibility.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
s: Raw as_of string from user/HTTP/MCP/CLI input. May be None or
|
|
72
|
+
non-string; both return None (treated as "no as_of" by callers).
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Normalized UTC string "YYYY-MM-DDTHH:MM:SS+00:00", or None on error.
|
|
76
|
+
"""
|
|
77
|
+
if not s or not isinstance(s, str):
|
|
78
|
+
return None
|
|
79
|
+
s = s.strip()
|
|
80
|
+
if not s:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
# Try ISO 8601 parse. Python 3.11 handles "Z" natively; the replace()
|
|
84
|
+
# makes this safe on any 3.x that might be used in tests.
|
|
85
|
+
for candidate in (s, s.replace("Z", "+00:00")):
|
|
86
|
+
try:
|
|
87
|
+
dt = datetime.fromisoformat(candidate)
|
|
88
|
+
# Naive datetime → assume UTC (documented assumption).
|
|
89
|
+
if dt.tzinfo is None:
|
|
90
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
91
|
+
else:
|
|
92
|
+
dt = dt.astimezone(timezone.utc)
|
|
93
|
+
# Audit P0: isoformat() preserves microseconds when present so the
|
|
94
|
+
# value round-trips against microsecond-bearing stored timestamps.
|
|
95
|
+
return dt.isoformat()
|
|
96
|
+
except ValueError:
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
# Date-only "YYYY-MM-DD" — treat as midnight UTC.
|
|
100
|
+
try:
|
|
101
|
+
dt = datetime.strptime(s[:10], "%Y-%m-%d").replace(tzinfo=timezone.utc)
|
|
102
|
+
return dt.isoformat()
|
|
103
|
+
except ValueError:
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
__all__ = ("normalize_as_of",)
|