amfs-http-server 0.3.51__tar.gz → 0.3.52__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: amfs-http-server
3
- Version: 0.3.51
3
+ Version: 0.3.52
4
4
  Summary: AMFS HTTP/REST API server with SSE support
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Python: >=3.11
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "amfs-http-server"
3
- version = "0.3.51"
3
+ version = "0.3.52"
4
4
  description = "AMFS HTTP/REST API server with SSE support"
5
5
  requires-python = ">=3.11"
6
6
  license = "Apache-2.0"
@@ -30,7 +30,7 @@ import time
30
30
  from concurrent.futures import ThreadPoolExecutor
31
31
  from contextlib import asynccontextmanager
32
32
  from datetime import UTC, datetime, timedelta, timezone
33
- from collections.abc import Mapping, Sequence
33
+ from collections.abc import Callable, Mapping, Sequence
34
34
  from typing import Any
35
35
 
36
36
  import uvicorn
@@ -50,7 +50,7 @@ from amfs_core.aggregates import (
50
50
  )
51
51
  from amfs_core.ranking import composite_score
52
52
  from amfs_core.ranking import entry_text as _entry_text
53
- from amfs_core.scope import SqlScope
53
+ from amfs_core.scope import SqlScope, covers
54
54
  from amfs_core.ranking import keyword_coverage as _keyword_coverage
55
55
  from amfs_core.reuse_value import REUSE_VALUE_HEADER, reuse_value_block
56
56
  from amfs_core.capture import scan_captured_arguments, scan_captured_text
@@ -962,6 +962,103 @@ def _search_sync(adapter: Any, query: SearchQuery, branch: Any) -> list[MemoryEn
962
962
  return adapter.search(query)
963
963
 
964
964
 
965
+ async def _branch_treatment(
966
+ *,
967
+ branch: str,
968
+ entity_path: str | None,
969
+ topical: str,
970
+ query_vector: list[float] | None,
971
+ embedder: Any,
972
+ pool: int,
973
+ vis: Any,
974
+ include_artifacts: bool,
975
+ is_artifact: Callable[[MemoryEntry], bool],
976
+ ) -> list[tuple[MemoryEntry, float, float]]:
977
+ """The entries *branch* changed under *entity_path* that are about *topical*.
978
+
979
+ Retrieve's step 9b: what a read routed to a repair branch exists to put in
980
+ front of the agent. Returns ``(entry, similarity, keyword)`` triples for
981
+ the branch's added or modified entries that the query's semantic pool on
982
+ the branch holds (no confidence gate), or that a lexical read on the
983
+ branch returns where there are no vectors. Empty on ``main``, with no
984
+ scope, on an adapter without branches, when the branch changed nothing
985
+ under the scope, or when the query is about none of it. Never raises:
986
+ a failure here leaves the ranking as it was.
987
+ """
988
+ if not isinstance(branch, str) or branch.strip() in ("", "main") or not entity_path:
989
+ return []
990
+ mem = _get_memory()
991
+ diff_fn = getattr(mem._adapter, "diff_branch", None)
992
+ if not callable(diff_fn):
993
+ return []
994
+ namespace = getattr(mem._adapter, "_namespace", None) or "default"
995
+ try:
996
+ diff = await _offload(_db_executor, diff_fn, branch, namespace=namespace)
997
+ except Exception: # noqa: BLE001 - best-effort
998
+ logger.debug("branch diff for retrieve failed", exc_info=True)
999
+ return []
1000
+ changed: set[tuple[str, str]] = {
1001
+ (d.entity_path, d.key)
1002
+ for d in diff or []
1003
+ if getattr(d, "diff_type", "") in ("added", "modified")
1004
+ and covers(entity_path, d.entity_path)
1005
+ and not _is_synthetic_key(d.key)
1006
+ and not _is_excluded_entity(d.entity_path)
1007
+ }
1008
+ if not changed:
1009
+ return []
1010
+
1011
+ # Which of them the query is about, read on the branch with no gate: the
1012
+ # semantic pool where vectors exist, the lexical channel where they do not.
1013
+ found: dict[tuple[str, str], tuple[MemoryEntry, float, float]] = {}
1014
+ if embedder is not None and _async_adapter is not None:
1015
+ try:
1016
+ pairs = await _async_adapter.semantic_search(
1017
+ SemanticQuery(
1018
+ text=topical, entity_path=entity_path, min_confidence=0.0,
1019
+ limit=pool, embedding=query_vector,
1020
+ ),
1021
+ embedder,
1022
+ branch=branch,
1023
+ )
1024
+ except Exception: # noqa: BLE001
1025
+ logger.debug("branch semantic read for retrieve failed", exc_info=True)
1026
+ pairs = []
1027
+ floor = _retrieve_min_semantic()
1028
+ for entry, sim in pairs:
1029
+ ref = (entry.entity_path, entry.key)
1030
+ if ref in changed and float(sim) > 0.0 and float(sim) >= floor:
1031
+ found[ref] = (entry, float(sim), 0.0)
1032
+ lex = SearchQuery(
1033
+ query=topical, entity_path=entity_path, min_confidence=0.0, limit=pool,
1034
+ sort_by="confidence", depth=3, include_artifacts=include_artifacts,
1035
+ )
1036
+ try:
1037
+ if _async_adapter is not None:
1038
+ rows = await _async_adapter.search(lex, branch=branch)
1039
+ else:
1040
+ rows = await _offload(_db_executor, _search_sync, mem._adapter, lex, branch)
1041
+ except Exception: # noqa: BLE001
1042
+ logger.debug("branch lexical read for retrieve failed", exc_info=True)
1043
+ rows = []
1044
+ for entry in rows:
1045
+ ref = (entry.entity_path, entry.key)
1046
+ if ref not in changed:
1047
+ continue
1048
+ slot = found.get(ref)
1049
+ found[ref] = (entry, slot[1], 1.0) if slot else (entry, 0.0, 1.0)
1050
+
1051
+ out = [
1052
+ t for t in found.values()
1053
+ if getattr(t[0], "discredited_at", None) is None
1054
+ and (include_artifacts or not is_artifact(t[0]))
1055
+ ]
1056
+ if out and vis is not None and vis.should_filter():
1057
+ allowed = {e.entry_key for e in vis.filter_entries([t[0] for t in out])}
1058
+ out = [t for t in out if t[0].entry_key in allowed]
1059
+ return out
1060
+
1061
+
965
1062
  async def _discredited_below_gate(
966
1063
  entity_path: str,
967
1064
  *,
@@ -3325,6 +3422,94 @@ async def retrieve_entries(
3325
3422
  kept.append((entry, score, bd))
3326
3423
  scored = kept
3327
3424
 
3425
+ # 9b. The branch under test. A read routed to a repair branch is the
3426
+ # canary arm of a fix: the branch exists to put that fix in front of
3427
+ # the agent, and the entries it changed are the treatment. Ranked
3428
+ # like any other entry they do not arrive. The repair loop writes a
3429
+ # correction at 60% of the confidence of the rule it corrects — under
3430
+ # the caller's gate as often as not — onto a scope where outcome
3431
+ # learning has already written dozens of validated near-duplicates
3432
+ # of the stale rule, and confidence is a ranking term. Measured on
3433
+ # clbench (2026-09-30): 18 canaries, 14-197 branch reads each, zero
3434
+ # reads of the fix's own entry. Both arms read the same rules, the
3435
+ # canary compared two untreated populations, and 0 vs 0 recurrence
3436
+ # said nothing about the fix.
3437
+ #
3438
+ # So the branch's changed entries that are *about this query* lead
3439
+ # the list, in their own order, marked ``_pinned`` so the client can
3440
+ # see why. "About this query" is the semantic pool on the branch with
3441
+ # no confidence gate (the fix's confidence is the repair loop's
3442
+ # business, not a reason to hide the treatment from its own arm), or
3443
+ # a lexical hit where there are no vectors; an entry the query is not
3444
+ # about stays where it is — a fix to the CI rule is not pinned onto a
3445
+ # support ticket. Discredited entries are not pinned: a fix the record
3446
+ # has already failed is the avoid list's, not the head's; nor is a
3447
+ # procedure whose preconditions contradict the run's environment —
3448
+ # it is named in ``_meta.not_applicable`` like any other. Main reads
3449
+ # are untouched.
3450
+ pinned = await _branch_treatment(
3451
+ branch=branch,
3452
+ entity_path=req.entity_path,
3453
+ topical=topical,
3454
+ query_vector=query_vectors.get(topical),
3455
+ embedder=embedder,
3456
+ pool=pool,
3457
+ vis=vis,
3458
+ include_artifacts=req.include_artifacts,
3459
+ is_artifact=_is_artifact,
3460
+ )
3461
+ if pinned:
3462
+ if environment:
3463
+ from amfs_core.models import preconditions_status as _preconditions_status
3464
+
3465
+ # Named once: a pinned procedure above the caller's gate was a
3466
+ # candidate too, and step 9 has already recorded it.
3467
+ named = {(row["entity_path"], row["key"]) for row in not_applicable}
3468
+ applicable: list[tuple[MemoryEntry, float, float]] = []
3469
+ for entry, sim, keyword in pinned:
3470
+ if _is_procedure(entry):
3471
+ status, detail = _preconditions_status(entry.value, environment)
3472
+ if status == "not_applicable":
3473
+ if (entry.entity_path, entry.key) not in named:
3474
+ not_applicable.append({
3475
+ "entity_path": entry.entity_path, "key": entry.key, "why": detail,
3476
+ })
3477
+ continue
3478
+ applicable.append((entry, sim, keyword))
3479
+ pinned = applicable
3480
+ if pinned:
3481
+ pinned_scored: list[tuple[MemoryEntry, float, dict[str, Any]]] = []
3482
+ for entry, sim, keyword in pinned:
3483
+ written = getattr(entry.provenance, "written_at", None)
3484
+ if written is not None:
3485
+ if written.tzinfo is None:
3486
+ written = written.replace(tzinfo=UTC)
3487
+ recency = 0.5 ** (max(0.0, (now - written).total_seconds() / 86400.0) / half_life)
3488
+ else:
3489
+ recency = 0.0
3490
+ artifact = _is_artifact(entry)
3491
+ procedure = _is_procedure(entry)
3492
+ evidence = _evidence_signal(entry)
3493
+ bd = {
3494
+ "semantic": sim,
3495
+ "relevance": req.semantic_weight * sim + keyword_weight * keyword,
3496
+ "recency": recency,
3497
+ "confidence": float(entry.confidence),
3498
+ "keyword": keyword,
3499
+ "evidence": evidence,
3500
+ "evidence_status": entry.evidence_status,
3501
+ "is_artifact": artifact,
3502
+ "is_procedure": procedure,
3503
+ "pinned": branch,
3504
+ }
3505
+ conf = float(entry.confidence)
3506
+ pinned_scored.append((
3507
+ entry, _composite(sim, recency, conf, keyword, artifact, evidence, procedure), bd,
3508
+ ))
3509
+ pinned_scored.sort(key=lambda t: t[1], reverse=True)
3510
+ pinned_keys = {e.entry_key for e, _, _ in pinned_scored}
3511
+ scored = pinned_scored + [t for t in scored if t[0].entry_key not in pinned_keys]
3512
+
3328
3513
  # 10. Reuse accounting. Semantic retrieval is the primary way memories
3329
3514
  # (e.g. browser-extension clips) get surfaced and used, but it
3330
3515
  # historically incremented no counter — so value metrics (rework
@@ -3620,6 +3805,10 @@ async def retrieve_entries(
3620
3805
  # agent reads the top-level field.
3621
3806
  data["evidence_status"] = breakdown.get("evidence_status")
3622
3807
  data["_rescued"] = True
3808
+ if breakdown.get("pinned"):
3809
+ # Changed on the branch this read was routed to (step 9b): the
3810
+ # treatment of the canary arm, ahead of the ranking on purpose.
3811
+ data["_pinned"] = True
3623
3812
  if req.compact:
3624
3813
  data["_breakdown"] = {"evidence_status": breakdown.get("evidence_status")}
3625
3814
  else: