recall dispatch: per anchor, one failed or refused cited id costs only its own recall (status line with the failure kind); every cited id stays out of the neighbours; D8 deadline band bounded by the fake's sleep

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Eureka 2026-11-07 02:39:16 +01:00
commit db2b250380
6 changed files with 55 additions and 18 deletions

View file

@ -315,7 +315,8 @@ With its `PreToolUse` hook installed, `tools/recall.py admit` holds an admit tha
carries rulings or questions once and lists their nearest in-force neighbours; the
identical retry is admitted. With its `PreToolUse` hook on `Agent|Task` installed,
`tools/recall.py dispatch` appends to each Agent brief the in-force neighbours of the
rulings, questions and follow-ups the brief cites, and lists their ids to the dispatcher.
rulings, questions and follow-ups the brief cites, and lists their ids to the dispatcher. Each cited
id is checked on its own: one failing costs only its neighbours and gets a status line naming the failure kind.
## Substrate gaps

View file

@ -10,6 +10,8 @@ Evidence. Ruling `eureka-substrate:ruling:recall-triggers-rescoped` (operator: "
What changed. `tools/recall.py dispatch`: a `PreToolUse` command hook on `Agent|Task` that appends the in-force neighbours of the records the brief cites to the brief itself. `neighbours` is now one text-taking helper (with `fan_out` and `plain_id`) shared by admit and dispatch; admit's output is unchanged. `tools/test-tools.sh` gains the dispatch driver (D1 to D13) and the fake binding answers `view` of any id and queries by text with scores. The hook is not installed by this change.
Fix pass (Soul s1): recall is per anchor. One cited id failing or refused no longer silences the rest: the other anchors' neighbours still reach the subagent and the failed id gets a status line naming it and the failure kind (`CheckFailed`, `Refused`, `NotHeld`); nothing prints only when no anchor could be checked. Every cited id, not just the first six, is kept out of the neighbours. D8's deadline check is bounded by the fake's own sleep, so a loaded host no longer fails it.
## 2026-11-06: the operator's own life in chat is not a leak
Self, session `self-2026-11-04-eureka`.

View file

@ -16,5 +16,5 @@ gets a Soul pass before merge.
| `tools/test-tools.sh` | Local fixture check of the parsers and the summary over `tools/fixtures/logs` (real cargo and mutation logs, plus hand-written dotnet, vitest, jest, pytest, exit-0-but-failed and no-verdict logs), and of the mutation runners' host drivers and container halves (`tools/mutation-table.sh`, `tools/cargo-mutants.sh`) under a mock docker, a fake `cargo mutants` and real slot locks. Run it before merging a change to the scripts above. | whoever changes those scripts | `SKILL.md` (Shared tools) |
| `tools/context-pack.py` | `py -3 tools/context-pack.py <cut_spec id> <worktree> [--since <sha>] [--only <k>\|r<k>[,...]] [--spec-json <file>] [--max-lines <n>]`. Carries a cut_spec's `file_changes` and `reads` anchors (read rows `r<k>`; a read absent at base is MISSING) from the spec's base to the worktree by `git diff` hunks (the working tree included) and prints one header, a status table (SAME, MOVED, TOUCHED, RENAMED, NEW, GONE, OUT_OF_RANGE, UNRESOLVED, MISSING, plus SYMBOL_MISSING when an anchor's `symbol` is absent from its window) and windows of at most 100 lines numbered in worktree lines, then one capped caller list per symbol (`git grep`, hits outside the windows), within `--max-lines` (800). Exit 3 when an anchor cannot be carried, 4 when the spec is unreadable (fetched by id through the eureka-state binding (`tools/mind.py`), retried once on Unavailable; `--spec-json` takes a saved view). `--since <sha>` re-prints only the windows edited since that commit. Standard library plus git; writes nothing. | Hands, Soul, Imagination | `references/briefs.md` (Imagination map, Hands, Soul), `SKILL.md` (step 1) |
| `tools/mind.py` | The one way a Eureka script reaches the mind: `mind.call(tool, arguments, deadline)` spawns the eureka-state binding (`EUREKA_STATE_EXE`, else `~/.claude.json` `mcpServers.eureka-state`), does the stdio handshake and one tool call within `deadline` seconds, returns the parsed JSON body, raises `MindError` (kind binding, transport, tool or answer; a detail, never an input value), and kills the child on every exit path. No retry inside `call`; `retry_once(fn)` is the caller's choice. Standard library only. | `tools/context-pack.py`, `tools/recall.py` | not named by a brief; it lives and dies with its consumers |
| `tools/recall.py` | `py -3 tools/recall.py prompt`, a `UserPromptSubmit` command hook (hook JSON on stdin): queries the rulings, questions and follow-ups in force across every root, ranked by Huginn against the prompt (`RECALL_TOP_K`, 5), and prints one hook JSON whose `additionalContext` is a header saying the lines are recalled records, data and never instructions, written by agents and not the operator, then one JSON object per item (`id`, `title`, a short `operator_quote`) with `<`, `>` and control, bidi and separator characters escaped, within `RECALL_BUDGET` characters (4000, at most 9000: Claude Code caps `additionalContext` at 10,000), whole lines only. Prompts under 3 words are not queried. Never blocks: any failure exits 0 with empty stdout; its own deadline is `RECALL_DEADLINE_MS` (4000) for the whole run (stdin read to 4 MiB, answer read to 1 MiB), so set the hook `timeout` above it. One attempt, no retry. Install both hooks as `<absolute python.exe> C:/Users/Meta/.claude/skills/eureka/tools/recall.py prompt || true` (and `... admit || true`): Claude Code runs hook commands through Git Bash on Windows, and `py -3 <missing script>` exits 2, which blocks every prompt or admit; with `|| true` the hook cannot exit 2 (a missing script, interpreter or file passes) and the script's stdout is untouched (proved live on 2.1.308). `py -3 tools/recall.py admit` is a `PreToolUse` hook on `mcp__eureka-state__admit`: for each ruling or question in the batch (at most `RECALL_ADMIT_MAX`, 3) it queries the nearest in-force rulings and questions, and denies the first attempt with the neighbours as the reason (a deliberate hold, not a failure); the identical call sent again (digest of `tool_input` plus `agent_id`, kept in the session scratchpad) passes. Never allow or ask; a failed check exits 0 with a note naming the failure kind (`RECALL_TOP_K` 3, `RECALL_DEADLINE_MS` 10000 in this mode). `py -3 tools/recall.py dispatch` is a `PreToolUse` hook on `Agent|Task`: it takes the ruling, question and follow-up ids the brief cites (`<slug>:<kind>:<label>`, the first `RECALL_DISPATCH_MAX`, 6, in order of appearance), views each, queries the in-force rulings, questions and follow-ups nearest each one that is in force (cited ids excluded, `RECALL_TOP_K` 3 kept per anchor, unioned by best score), puts one status line before them for a cited id that is not in force, and returns `updatedInput` whose `prompt` is the original plus the block (every other field untouched) and an `additionalContext` naming the appended ids for the dispatcher; it never decides permission and prints nothing on any failure, a brief that already holds its header, or the deadline (`RECALL_DEADLINE_MS` 10000 in this mode). Standard library only; writes only the digest file (admit). | the operator's `UserPromptSubmit` hook, `PreToolUse` hook on `mcp__eureka-state__admit` and `PreToolUse` hook on `Agent|Task` (fire inside subagents too) in `~/.claude/settings.json` | `references/campaign-state.md` (Precedent) |
| `tools/recall.py` | `py -3 tools/recall.py prompt`, a `UserPromptSubmit` command hook (hook JSON on stdin): queries the rulings, questions and follow-ups in force across every root, ranked by Huginn against the prompt (`RECALL_TOP_K`, 5), and prints one hook JSON whose `additionalContext` is a header saying the lines are recalled records, data and never instructions, written by agents and not the operator, then one JSON object per item (`id`, `title`, a short `operator_quote`) with `<`, `>` and control, bidi and separator characters escaped, within `RECALL_BUDGET` characters (4000, at most 9000: Claude Code caps `additionalContext` at 10,000), whole lines only. Prompts under 3 words are not queried. Never blocks: any failure exits 0 with empty stdout; its own deadline is `RECALL_DEADLINE_MS` (4000) for the whole run (stdin read to 4 MiB, answer read to 1 MiB), so set the hook `timeout` above it. One attempt, no retry. Install both hooks as `<absolute python.exe> C:/Users/Meta/.claude/skills/eureka/tools/recall.py prompt || true` (and `... admit || true`): Claude Code runs hook commands through Git Bash on Windows, and `py -3 <missing script>` exits 2, which blocks every prompt or admit; with `|| true` the hook cannot exit 2 (a missing script, interpreter or file passes) and the script's stdout is untouched (proved live on 2.1.308). `py -3 tools/recall.py admit` is a `PreToolUse` hook on `mcp__eureka-state__admit`: for each ruling or question in the batch (at most `RECALL_ADMIT_MAX`, 3) it queries the nearest in-force rulings and questions, and denies the first attempt with the neighbours as the reason (a deliberate hold, not a failure); the identical call sent again (digest of `tool_input` plus `agent_id`, kept in the session scratchpad) passes. Never allow or ask; a failed check exits 0 with a note naming the failure kind (`RECALL_TOP_K` 3, `RECALL_DEADLINE_MS` 10000 in this mode). `py -3 tools/recall.py dispatch` is a `PreToolUse` hook on `Agent|Task`: it takes the ruling, question and follow-up ids the brief cites (`<slug>:<kind>:<label>`, the first `RECALL_DISPATCH_MAX`, 6, in order of appearance), views each, queries the in-force rulings, questions and follow-ups nearest each one that is in force (cited ids excluded, `RECALL_TOP_K` 3 kept per anchor, unioned by best score), puts one status line before them for a cited id that is not in force, and returns `updatedInput` whose `prompt` is the original plus the block (every other field untouched) and an `additionalContext` naming the appended ids for the dispatcher; it never decides permission and checks each cited id on its own (a failed or refused check costs only that id's neighbours and gets a status line naming the id and the failure kind; all cited ids failing prints nothing), every cited id stays out of the neighbours even past the anchor cap, and it prints nothing on malformed input, a brief that already holds its header, or the deadline (`RECALL_DEADLINE_MS` 10000 in this mode). Standard library only; writes only the digest file (admit). | the operator's `UserPromptSubmit` hook, `PreToolUse` hook on `mcp__eureka-state__admit` and `PreToolUse` hook on `Agent|Task` (fire inside subagents too) in `~/.claude/settings.json` | `references/campaign-state.md` (Precedent) |
| `tools/bloom-sheet/bloom_sheet.py` | `bloom-sheet notes.md -o sheet.png [--visitor a.svg]...`: a markdown document as one neon A4 page. The first top-level heading is the title, every other heading a card sized by its text; a plant grows through what's left, and each SVG visitor gets a disc it grows around and is drawn with the plant's pen, so it must be flat strokes or fills that touch; large fills become a wash and a heavy outline. PDF or PNG, about 2.5 minutes. `grow.py` owns the plant, `page.tex` the look, `test_visitors.py` what a visitor may be. | Minds and Self, for info renders | the `bloom-sheet` skill; `bloom-sheet` on Yggdrasil's PATH |

View file

@ -11,7 +11,8 @@ FAKE_NOISE writes a non-JSON line, a notification and an id-99 reply before the
FAKE_ITEMS holding an object instead of a list answers each query by its semantic text: {text: {"items": [...], "scores": [...]}},
an unknown text gets nothing; the answer carries "scores" when the entry has them.
FAKE_VIEWS (recall.py dispatch): a file holding {document id: the view answer, null when the mind holds none}; a view call
then gains a "view" line in FAKE_COUNT and its arguments in FAKE_ARGS, and FAKE_MODE unavailable and garbage answer as for query."""
then gains a "view" line in FAKE_COUNT and its arguments in FAKE_ARGS, and FAKE_MODE unavailable and garbage answer as for query;
FAKE_FAIL_VIEWS (comma-separated document ids): the view of those ids alone answers Unavailable."""
import json, os, sys, time
inited = False
@ -62,7 +63,7 @@ for ln in sys.stdin:
with open(os.environ["FAKE_ARGS"], "a") as f:
f.write(json.dumps(p.get("arguments")) + "\n")
time.sleep(float(os.environ.get("FAKE_SLEEP", "0")))
if os.environ.get("FAKE_MODE") == "unavailable":
if os.environ.get("FAKE_MODE") == "unavailable" or p["arguments"]["id"]["id"] in os.environ.get("FAKE_FAIL_VIEWS", "").split(","):
res = {"isError": True, "content": [{"type": "text", "text": "Unavailable"}]}
elif os.environ.get("FAKE_MODE") == "garbage":
res = {"content": [{"type": "text", "text": "not json"}]}

View file

@ -306,27 +306,39 @@ def dispatch_mode(end):
prompt = tool_input.get("prompt")
if not isinstance(prompt, str) or DISPATCH_HEADER in prompt:
return None
cited = list(dict.fromkeys(m.group(1) for m in CITED.finditer(prompt)))[:env_int("RECALL_DISPATCH_MAX", DISPATCH_MAX)]
every = list(dict.fromkeys(m.group(1) for m in CITED.finditer(prompt)))
cited = every[:env_int("RECALL_DISPATCH_MAX", DISPATCH_MAX)] # the cap bounds the anchors; every cited id stays out of the neighbours
if not cited:
return None
k = env_int("RECALL_TOP_K", 3)
def anchor(cid):
"""(status, neighbours, failure kind): one anchor's answer; a failure is this anchor's alone."""
def left():
return end - RESERVE_S - time.monotonic()
seen = mind.call("view", {"id": {"kind": VIEW_KIND[cid.split(":")[-2]], "id": cid}}, left(), ANSWER_BYTES)
status = seen.get("status") if isinstance(seen, dict) else "NotHeld"
if status != "InForce":
return status, []
value = seen["document"]["value"]
text = value["item"] if seen["document"]["kind"] == "follow_up" else value["title"]
return None, neighbours(text, SCHEMAS, set(cited), k, left()) if isinstance(text, str) and text else []
try:
seen = mind.call("view", {"id": {"kind": VIEW_KIND[cid.split(":")[-2]], "id": cid}}, left(), ANSWER_BYTES)
if not isinstance(seen, dict):
return "NotHeld", [], None
if "document" not in seen:
return seen.get("status") or "Refused", [], None
if seen.get("status") != "InForce":
return seen.get("status"), [], None
value = seen["document"]["value"]
text = value["item"] if seen["document"]["kind"] == "follow_up" else value["title"]
return None, neighbours(text, SCHEMAS, set(every), k, left()) if isinstance(text, str) and text else [], None
except Exception as e:
return None, [], e.kind if isinstance(e, mind.MindError) else "unexpected"
best, entries = {}, []
answers = fan_out(anchor, cited)
for cid, (status, _) in zip(cited, answers):
if status:
if all(failed for _, _, failed in answers): # nothing could be checked: print nothing
return None
for cid, (status, _, failed) in zip(cited, answers):
if failed:
entries.append(("- {" + '"id":%s,"status":"CheckFailed","failure":%s' % (inert(cid), inert(failed)) + "}", cid))
elif status:
entries.append(("- {" + '"id":%s,"status":%s' % (inert(cid), inert(str(status))) + "}", cid))
for _, near in answers:
for _, near, _ in answers:
for score, item in near:
ident = item["id"]["id"]
if ident not in best or score > best[ident][0]:

View file

@ -1557,7 +1557,9 @@ b7 = " ".join(ten[:3] + [ten[0]] + ten[3:] + [ten[1]])
allv = vs(*[view("ruling", "r%d" % i, "Title %d" % i) for i in range(1, 9)])
r, vc, qc, ms = run(hook(b7), allv, {})
check("D7 eight distinct ids cited with duplicates: exactly the first six in order of first appearance are viewed", sorted(vc) == ten[:6] and len(vc) == 6, vc)
check("D7 each is queried once, top_k 3 plus 6 cited", len(qc) == 6 and all(c["semantic"]["top_k"] == 9 for c in qc), qc)
check("D7 each is queried once, top_k 3 plus all 8 cited", len(qc) == 6 and all(c["semantic"]["top_k"] == 11 for c in qc), qc)
r, vc, qc, ms = run(hook(b7), allv, {"Title 1": rank((item("r7", "Title 7"), .99), (item("r8", "Title 8"), .98), (N["n1"], .5))})
check("D7 a cited id past the anchor cap is still never a neighbour", ids_of(r, b7) == ["t:ruling:n1"], ids_of(r, b7))
r, vc, qc, ms = run(hook(b7), allv, {}, RECALL_DISPATCH_MAX="2")
check("D7 RECALL_DISPATCH_MAX=2 views the first two", sorted(vc) == ten[:2], vc)
@ -1578,10 +1580,29 @@ for label, c in bad:
r, vc, qc, ms = run(hook("t:ruling:a"), vs(view("ruling", "a", "Alpha")), {"Alpha": rank((N["n1"], .9))}, FAKE_MODE="unavailable")
check("D8 Unavailable is one attempt, no retry", len(vc) == 1 and qc == [], vc)
r, vc, qc, ms = run(hook("t:ruling:a t:ruling:b"), vs(view("ruling", "a", "Alpha")), {"Alpha": rank((N["n1"], .9))}, FAKE_MODE="unavailable")
check("D8 one anchor failing silences the whole recall", silent(r), r.stdout[:100])
check("D8 every anchor failing prints nothing", silent(r), r.stdout[:100])
# D14 one anchor failing costs only its own recall: the rest are pushed, the failed one gets a status line naming the kind
b14 = "t:ruling:a t:ruling:b t:ruling:c"
v14 = vs(view("ruling", "a", "Alpha"), view("ruling", "b", "Beta"), view("ruling", "c", "Gamma"))
i14 = {"Alpha": rank((N["n1"], .9)), "Beta": rank((N["n2"], .8)), "Gamma": rank((N["n3"], .7))}
r, vc, qc, ms = run(hook(b14), v14, i14, FAKE_FAIL_VIEWS="t:ruling:b")
L = lines_of(r, b14) if r.stdout else r.stdout
check("D14 one of three anchors failing: one status line naming it and the failure kind (nothing more), then the other two anchors' neighbours",
L == ['- {"id":"t:ruling:b","status":"CheckFailed","failure":"tool"}', '- {"id":"t:ruling:n1","title":"Neighbour n1"}',
'- {"id":"t:ruling:n3","title":"Neighbour n3"}'], L)
check("D14 the failed anchor is queried not at all and the others once each", sorted(c["semantic"]["text"] for c in qc) == ["Alpha", "Gamma"], qc)
check("D14 the ids line names the status id and the neighbours", r.stdout != b"" and ids_of(r, b14) == ["t:ruling:b", "t:ruling:n1", "t:ruling:n3"] and out(r)["additionalContext"] == LEAD + ", ".join(ids_of(r, b14)), r.stdout[:200])
r, vc, qc, ms = run(hook("t:ruling:a t:ruling:b"), vs(view("ruling", "a", "Alpha"), view("ruling", "b", "Beta")), {"Alpha": rank((N["n1"], .9))}, FAKE_FAIL_VIEWS="t:ruling:a,t:ruling:b")
check("D14 all anchors failing is the one silent case", silent(r), r.stdout[:100])
# D15 an id the view refuses gets a status line
r, vc, qc, ms = run(hook("t:ruling:a t:ruling:x"), {"t:ruling:a": view("ruling", "a", "Alpha")["document"] and view("ruling", "a", "Alpha"), "t:ruling:x": {"Refused": {"Document": "InvalidFormat"}}}, {"Alpha": rank((N["n1"], .9))})
check("D15 a refused view answer gets a Refused status line before the neighbours",
r.stdout != b"" and lines_of(r, "t:ruling:a t:ruling:x") == ['- {"id":"t:ruling:x","status":"Refused"}', '- {"id":"t:ruling:n1","title":"Neighbour n1"}'], lines_of(r, "t:ruling:a t:ruling:x"))
r, vc, qc, ms = run(hook("t:ruling:x"), {"t:ruling:x": {"Refused": {"Document": "InvalidFormat"}}})
check("D15 a brief citing only a refused id prints the status line and makes no query", r.stdout != b"" and ids_of(r, "t:ruling:x") == ["t:ruling:x"] and qc == [], r.stdout[:200])
for dl, sl in (("1000", "3"), ("2000", "4")):
r, vc, qc, ms = run(hook("t:ruling:a"), vs(view("ruling", "a", "Alpha")), {"Alpha": rank((N["n1"], .9))}, RECALL_DEADLINE_MS=dl, FAKE_SLEEP=sl)
check("D8 deadline %s ms with a %s s fake ends in %.0f ms, silent" % (dl, sl, ms), silent(r) and int(dl) - 100 <= ms <= int(dl) + 600, (r.stdout[:100], ms))
check("D8 deadline %s ms with a %s s fake ends in %.0f ms (1.8 s of slack, and before the fake's own %s s), silent" % (dl, sl, ms, sl), silent(r) and int(dl) - 100 <= ms < int(sl) * 1000 - 200, (r.stdout[:100], ms))
r, vc, qc, ms = run(hook("t:ruling:a"), vs(view("ruling", "a", "Alpha")), {"Alpha": rank((N["n1"], .9))}, FAKE_SLEEP="2.5")
check("D8 the default deadline in dispatch mode is 10 s: two 2.5 s calls in a row still answer", r.stdout != b"" and ms > 4900, (r.stdout[:100], ms))