diff --git a/references/campaign-state.md b/references/campaign-state.md index 19f1879..bc49829 100644 --- a/references/campaign-state.md +++ b/references/campaign-state.md @@ -286,6 +286,10 @@ reading its verdicts and findings, check that `admission.provenance.faculty` is `ruling`, `resolution` and `finding` with `root` = ``, plus `"semantic": { "text": "", "top_k": 10 }`. +Once the operator installs the hook, `tools/recall.py` also puts the nearest in-force +rulings, questions and follow-ups into every prompt; the recipe remains the +deliberate check before raising a question. + ## Substrate gaps The known gaps in the mind's substrate are `follow_up`s in the mind, not a diff --git a/tools/INDEX.md b/tools/INDEX.md index f629edb..60a6dac 100644 --- a/tools/INDEX.md +++ b/tools/INDEX.md @@ -13,4 +13,6 @@ gets a Soul pass before merge. | `tools/mutation-table.sh` | One verify job that runs a list of hand mutants (file, before, after, test) against a revision and returns a kill table: CAUGHT with the killing test, SURVIVED, UNVIABLE, TIMEOUT, NOAPPLY, ERROR. Refuses a red or zero-test baseline and a red BUILD; rows print live. Isolation is Docker's: the baselines run once in a container that holds the revision, that container is committed to a throwaway image, and every mutant runs in a fresh `--rm` container from it, so nothing a run writes (tracked, untracked or ignored, such as `target/`) reaches the next mutant; the image is removed when the job ends, killed or not. The shared registry and sccache volumes and the network are not isolated. A file-level load failure (vitest `FAIL [ ]`, jest `FAIL ` plus `Test suite failed to run`) reads UNVIABLE. Replaces per-agent runmut/mutrun/strykrun scripts. | Soul, Hands (scoped run) | `references/briefs.md` (Hands verification, Soul reruns) | | `tools/testparse.sh` | Sourced library: failing test names and pass/fail/skip counts from cargo, dotnet, vitest, jest and pytest logs, and test files that failed to load (`LOADFAIL`). Shared by the summary and the kill table. | the two scripts above | not named by a brief; it lives and dies with them | | `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 runner's host driver and container half under a mock docker. 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 [--since ] [--only [,...]] [--spec-json ] [--max-lines ]`. Carries a cut_spec's `file_changes` anchors 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) and windows of at most 100 lines numbered in worktree lines, 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, retried once on Unavailable; `--spec-json` takes a saved view). `--since ` 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/context-pack.py` | `py -3 tools/context-pack.py [--since ] [--only [,...]] [--spec-json ] [--max-lines ]`. Carries a cut_spec's `file_changes` anchors 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) and windows of at most 100 lines numbered in worktree lines, 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 ` 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` lists id, title and a short operator quote per item, within `RECALL_BUDGET` characters (4000), 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), so set the hook `timeout` above it. One attempt, no retry. Standard library only; writes nothing. | the operator's `UserPromptSubmit` hook in `~/.claude/settings.json` | `references/campaign-state.md` (Precedent) | diff --git a/tools/fixtures/context-pack/fake-eureka-state.py b/tools/fixtures/context-pack/fake-eureka-state.py index cb1bda2..42d6ec0 100644 --- a/tools/fixtures/context-pack/fake-eureka-state.py +++ b/tools/fixtures/context-pack/fake-eureka-state.py @@ -3,8 +3,11 @@ FAKE_SPEC: the view result to return. FAKE_MODE=unavailable answers isError Unavailable (naming HUGINN_ENDPOINT, as the real daemon's text might). FAKE_COUNT: a file that gains one line per view call. FAKE_MODE=needenv answers the spec only when -EUREKA_INSTANCE is "t" and HUGINN_ENDPOINT equals FAKE_NEED, else isError Misconfigured.""" -import json, os, sys +EUREKA_INSTANCE is "t" and HUGINN_ENDPOINT equals FAKE_NEED, else isError Misconfigured. +tools/call query (recall.py): FAKE_COUNT gains a "query" line, FAKE_ARGS gains the call's arguments as one +JSON line, FAKE_SLEEP seconds pass before the answer (then FAKE_MARK, if set, is written), and the answer is {"items": {"Documents": FAKE_ITEMS}} +(a file holding the list); FAKE_MODE unavailable and garbage answer as they do for view.""" +import json, os, sys, time inited = False for ln in sys.stdin: @@ -19,6 +22,24 @@ for ln in sys.stdin: res = {"protocolVersion": "2024-11-05", "capabilities": {}, "serverInfo": {"name": "fake", "version": "0"}} else: p = msg.get("params", {}) + if p.get("name") == "query" and inited and msg["method"] == "tools/call" and msg["id"] == 2: + with open(os.environ["FAKE_COUNT"], "a") as f: + f.write("query\n") + 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_MARK"): # written only by a child that outlived its caller's deadline + open(os.environ["FAKE_MARK"], "w").write("alive") + if os.environ.get("FAKE_MODE") == "unavailable": + res = {"isError": True, "content": [{"type": "text", "text": "Unavailable"}]} + elif os.environ.get("FAKE_MODE") == "garbage": + res = {"content": [{"type": "text", "text": "not json"}]} + else: + docs = json.load(open(os.environ["FAKE_ITEMS"], encoding="utf-8")) + res = {"content": [{"type": "text", "text": json.dumps({"items": {"Documents": docs}})}]} + sys.stdout.write(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": res}) + "\n") + sys.stdout.flush() + continue if (not inited or msg["method"] != "tools/call" or p.get("name") != "view" or msg["id"] != 2 or p.get("arguments") != {"id": {"kind": "CutSpec", "id": os.environ["FAKE_WANT"]}}): sys.exit(1) diff --git a/tools/recall.py b/tools/recall.py new file mode 100644 index 0000000..c7265eb --- /dev/null +++ b/tools/recall.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +"""Push what the mind already holds into an agent's context. + +Usage: recall.py prompt (a UserPromptSubmit command hook; the hook JSON is on stdin) + +Queries the rulings, questions and follow-ups in force, across every root, ranked +by Huginn's embeddings against the prompt, and prints one hook JSON whose +additionalContext lists the nearest. Huginn's ranking is the only judge of +relevance. It never blocks a prompt: every failure, the deadline included, exits 0 +with empty stdout. One attempt, no retry. Standard library only. +Env: RECALL_TOP_K (5), RECALL_BUDGET characters (4000), RECALL_DEADLINE_MS (4000; +set the hook's own timeout above it). +""" +import json +import os +import sys +import time + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import mind # noqa: E402 + +SCHEMAS = ["epiphany.pipeline.ruling.v2", "epiphany.pipeline.question.v2", + "epiphany.pipeline.follow_up.v2"] +QUERY_BYTES = 1000 +MIN_WORDS = 3 +ITEM_CHARS = 240 +QUOTE_CHARS = 300 +HEADER = ("The in-force rulings, questions and follow-ups in the Eureka mind nearest this " + "prompt, ranked by Huginn and possibly irrelevant; read one in full with the " + "eureka-state view tool by id:") + + +def env_int(name, default): + try: + v = int(os.environ[name]) + except (KeyError, ValueError): + return default + return v if v > 0 else default + + +def one_line(text, limit): + return " ".join(text.split())[:limit] + + +def line_of(item): + ident = item["id"]["id"] + value = item["document"]["value"] + title = one_line(value["item"], ITEM_CHARS) if item["document"]["kind"] == "follow_up" else value["title"] + out = "- %s: %s" % (ident, one_line(title, 1000)) + quote = value.get("operator_quote") + if isinstance(quote, str) and quote and len(quote) <= QUOTE_CHARS: + out += ' | operator: "%s"' % one_line(quote, QUOTE_CHARS) + return out + + +def render(items, budget): + """The header and as many whole item lines as fit the budget, in rank order.""" + kept = [HEADER] + for ln in items: + if len("\n".join(kept + [ln])) > budget: + break + kept.append(ln) + return "\n".join(kept) if len(kept) > 1 else "" + + +def prompt_mode(t0): + prompt = json.loads(sys.stdin.buffer.read().decode("utf-8"))["prompt"] + if not isinstance(prompt, str) or len(prompt.split()) < MIN_WORDS: + return "" + text = prompt.encode("utf-8")[:QUERY_BYTES].decode("utf-8", "ignore") + k = env_int("RECALL_TOP_K", 5) + selection = {"schemas": SCHEMAS, "projection": "document", + "fields": [{"index": "in_force", "op": "any_of", "values": ["true"]}]} + left = env_int("RECALL_DEADLINE_MS", 4000) / 1000.0 - (time.monotonic() - t0) + answer = mind.call("query", {"selection": selection, "semantic": {"text": text, "top_k": k}}, left) + lines = [line_of(i) for i in answer["items"]["Documents"]] + return render(lines, env_int("RECALL_BUDGET", 4000)) + + +def main(): + t0 = time.monotonic() + try: + if sys.argv[1:] != ["prompt"]: + return + context = prompt_mode(t0) + if context: + out = {"hookSpecificOutput": {"hookEventName": "UserPromptSubmit", + "additionalContext": context}} + sys.stdout.write(json.dumps(out)) + sys.stdout.flush() + except Exception: + pass + + +if __name__ == "__main__": + main() diff --git a/tools/test-tools.sh b/tools/test-tools.sh index d93b9a6..7741af9 100644 --- a/tools/test-tools.sh +++ b/tools/test-tools.sh @@ -513,4 +513,147 @@ mcp env -u EUREKA_INSTANCE HUGINN_ENDPOINT=rudp://caller FAKE_MODE=needenv FAKE_ cchk "the caller environment overrides the binding" 3 "$r"; cmp -s "$T/cp.out" "$T/cp.want" && echo "ok context-pack caller env wins over binding env" || { echo "FAIL context-pack caller env lost"; fail=1; } # standard library plus git only if grep -qE '^import (requests|yaml|tree_sitter)|^from (requests|yaml|tree_sitter)|ctags|tree-sitter|"rg"' "$CP"; then echo "FAIL context-pack: a dependency beyond git"; fail=1; else echo "ok context-pack: standard library and git only"; fi +# recall.py: the UserPromptSubmit hook that pushes the nearest in-force rulings, questions and follow-ups +# into a prompt. Driven against the fake binding; R1-R7 are the rules the cut_spec names. +RC=$here/recall.py +rcl() { mcp env FAKE_ARGS="$(wp "$T/rc.args")" FAKE_ITEMS="$(wp "$T/rc.items")" "$@"; } +# rrun [VAR=val ...]: one hook run; sets rc.out (stdout), rr (exit status), rms (elapsed ms) +rrun() { + local in=$1; shift; : >"$T/cp.count"; : >"$T/rc.args" + local a; a=$(date +%s%N) + rcl env "$@" $PY "$RC" prompt <"$in" >"$T/rc.out" 2>"$T/rc.err"; rr=$? + rms=$(( ($(date +%s%N) - a) / 1000000 )) +} +# rrun2: two runs, the faster one's elapsed time (a deadline bug shows in both; a busy host slows one) +rrun2() { local a; rrun "$@"; a=$rms; rrun "$@"; [ $a -lt $rms ] && rms=$a; } +rcalls() { grep -c '^query$' "$T/cp.count"; } +rchk() { if [ "$2" = 0 ] && [ ! -s "$T/rc.out" ] && [ ! -s "$T/rc.err" ]; then echo "ok recall $1"; else echo "FAIL recall $1: exit $2, stdout $(wc -c <"$T/rc.out") bytes, stderr $(wc -c <"$T/rc.err") bytes"; fail=1; fi; } +rctx() { $PY -c 'import json,sys;sys.stdout.write(json.load(open(sys.argv[1]))["hookSpecificOutput"]["additionalContext"])' "$(wp "$T/rc.out")"; } +$PY - "$(wp "$T")" <<'PYE' +import json, sys +t = sys.argv[1] +def doc(kind, label, value): + return {"id": {"id": "t:%s:%s" % (kind, label), "kind": kind.title().replace("_", "")}, + "document": {"kind": kind, "value": value}, "status": "InForce"} +items = [doc("ruling", "alpha", {"title": "Retrieval is pushed", "operator_quote": "pop into context"}), + doc("question", "beta", {"title": "Which fork", "operator_quote": None}), + doc("follow_up", "gamma", {"item": "f" * 500, "source": {}}), + doc("ruling", "delta", {"title": "Edge in", "operator_quote": "q" * 300}), + doc("ruling", "eps", {"title": "Edge out", "operator_quote": "q" * 301})] +json.dump(items, open(t + "/rc.items", "w")) +json.dump([items[0], doc("ruling", "big", {"title": "B" * 190, "operator_quote": None}), items[1]], open(t + "/rc.items2", "w")) +json.dump([], open(t + "/rc.items0", "w")) +def hook(name, prompt): + json.dump({"hook_event_name": "UserPromptSubmit", "session_id": "s", "prompt": prompt}, open(t + "/" + name, "w")) +hook("rc.in", "who is on the first batch") +hook("rc.in2", "two words") +hook("rc.in0", "") +hook("rc.inmb", "a " + "日 " * 300) +hook("rc.in3", "one two three") +open(t + "/rc.inbad", "w").write("not json") +json.dump({"hook_event_name": "UserPromptSubmit"}, open(t + "/rc.innoprompt", "w")) +PYE +RIN=$T/rc.in +# the happy path: one hook JSON; a header, then one line per item in rank order +rrun "$RIN"; [ $rr = 0 ] && [ ! -s "$T/rc.err" ] && echo "ok recall exits 0 with a hook JSON" || { echo "FAIL recall happy path: exit $rr"; fail=1; } +$PY - "$(wp "$T/rc.out")" <<'PYE' && echo "ok recall output is one UserPromptSubmit hook JSON" || { echo "FAIL recall hook JSON shape"; fail=1; } +import json, sys +o = json.load(open(sys.argv[1])) +assert list(o) == ["hookSpecificOutput"] +assert o["hookSpecificOutput"]["hookEventName"] == "UserPromptSubmit" +assert list(o["hookSpecificOutput"]) == ["hookEventName", "additionalContext"] +PYE +rctx >"$T/rc.full" +$PY - "$(wp "$T/rc.full")" <<'PYE' && echo "ok recall lists id and title, a short operator quote, a follow_up cut to 240, no quote over 300" || { echo "FAIL recall line format"; fail=1; } +import sys +L = open(sys.argv[1], encoding="utf-8").read().split("\n") +assert "possibly irrelevant" in L[0] and "view" in L[0] and "ranked" in L[0] +assert L[1] == '- t:ruling:alpha: Retrieval is pushed | operator: "pop into context"', L[1] +assert L[2] == "- t:question:beta: Which fork", L[2] +assert L[3] == "- t:follow_up:gamma: " + "f" * 240, L[3] +assert L[4] == '- t:ruling:delta: Edge in | operator: "%s"' % ("q" * 300), L[4] +assert L[5] == "- t:ruling:eps: Edge out", L[5] +assert len(L) == 6 +PYE +# R4: the selection is exactly the three kinds, in force, across every root, as documents; the text is the prompt +rrun "$RIN" RECALL_TOP_K=7 +$PY - "$(wp "$T/rc.args")" "$(wp "$T/rc.in")" <<'PYE' && echo "ok recall R4 selection is the three kinds, in force, document projection, top_k from RECALL_TOP_K" || { echo "FAIL recall R4 selection"; fail=1; } +import json, sys +a = [json.loads(l) for l in open(sys.argv[1])] +assert len(a) == 1 +a = a[0] +assert a["selection"] == {"schemas": ["epiphany.pipeline.ruling.v2", "epiphany.pipeline.question.v2", "epiphany.pipeline.follow_up.v2"], + "projection": "document", "fields": [{"index": "in_force", "op": "any_of", "values": ["true"]}]}, a["selection"] +assert a["semantic"] == {"text": json.load(open(sys.argv[2]))["prompt"], "top_k": 7}, a["semantic"] +PYE +rrun "$RIN"; [ "$($PY -c 'import json,sys;print(json.loads(open(sys.argv[1]).readline())["semantic"]["top_k"])' "$(wp "$T/rc.args")")" = 5 ] && echo "ok recall top_k defaults to 5" || { echo "FAIL recall default top_k"; fail=1; } +# R3: the query text is the prompt cut to at most 1000 bytes at a character boundary +rrun "$T/rc.inmb" +$PY - "$(wp "$T/rc.args")" "$(wp "$T/rc.inmb")" <<'PYE' && echo "ok recall R3 a multibyte prompt is cut to a valid UTF-8 prefix of at most 1000 bytes" || { echo "FAIL recall R3 byte bound"; fail=1; } +import json, sys +sent = json.loads(open(sys.argv[1], encoding="utf-8").readline())["semantic"]["text"] +full = json.load(open(sys.argv[2], encoding="utf-8"))["prompt"] +n = len(sent.encode("utf-8")) +assert len(full.encode("utf-8")) > 1200 and full.startswith(sent) and n == 998, n +PYE +$PY -c 'import json,sys;json.dump({"prompt": "ab " * 400}, open(sys.argv[1], "w"))' "$(wp "$T/rc.inasc")" +rrun "$T/rc.inasc"; [ "$($PY -c 'import json,sys;print(len(json.loads(open(sys.argv[1]).readline())["semantic"]["text"].encode()))' "$(wp "$T/rc.args")")" = 1000 ] && echo "ok recall R3 an ASCII prompt is cut to exactly 1000 bytes" || { echo "FAIL recall R3 ASCII cut is not 1000 bytes"; fail=1; } +# R6: fewer than 3 words is not queried; R7: one attempt +rrun "$T/rc.in2"; rchk "R6 a two-word prompt prints nothing" $rr; [ "$(rcalls)" = 0 ] && echo "ok recall R6 a two-word prompt makes zero calls" || { echo "FAIL recall R6 queried a two-word prompt"; fail=1; } +rrun "$T/rc.in0"; [ "$(rcalls)" = 0 ] && echo "ok recall R6 an empty prompt makes zero calls" || { echo "FAIL recall R6 queried an empty prompt"; fail=1; } +rrun "$T/rc.in3"; [ "$(rcalls)" = 1 ] && echo "ok recall R6 a three-word prompt is queried" || { echo "FAIL recall R6 three words not queried"; fail=1; } +rrun "$RIN" FAKE_MODE=unavailable; rchk "R1 Unavailable prints nothing" $rr; [ "$(rcalls)" = 1 ] && echo "ok recall R7 Unavailable is one attempt, no retry" || { echo "FAIL recall R7 attempts: $(rcalls)"; fail=1; } +# R1: every failure is exit 0 and empty stdout +rrun "$RIN" FAKE_MODE=garbage; rchk "R1 an unparsable answer" $rr +rrun "$RIN" HOME="$(wp "$T")/nohome" USERPROFILE="$(wp "$T")/nohome"; rchk "R1 no binding" $rr +rrun "$RIN" EUREKA_STATE_EXE="$T/CANARY-exe"; rchk "R1 an unspawnable binding" $rr +rrun "$T/rc.inbad"; rchk "R1 malformed stdin" $rr; [ "$(rcalls)" = 0 ] && echo "ok recall malformed stdin makes no call" || { echo "FAIL recall called on malformed stdin"; fail=1; } +rrun "$T/rc.innoprompt"; rchk "R1 stdin with no prompt" $rr +: >"$T/cp.count"; rcl $PY "$RC" <"$RIN" >"$T/rc.out" 2>"$T/rc.err"; rchk "R1 no subcommand" $? +# R5: the budget drops items whole from the tail and never cuts a line +$PY - "$(wp "$T/rc.full")" "$(wp "$T")" <<'PYE' +import sys +full = open(sys.argv[1], encoding="utf-8").read() +L = full.split("\n") +one = len(L[0]) + 1 + len(L[1]) +open(sys.argv[2] + "/rc.b", "w").write(" ".join(str(n) for n in (len(full), len(full) - 1, one, one - 1, len(L[0]) + 4))) +PYE +set -- $(cat "$T/rc.b") +rrun "$RIN" RECALL_BUDGET=$1; rctx >"$T/rc.ctx"; cmp -s "$T/rc.ctx" "$T/rc.full" && echo "ok recall R5 a budget equal to the output keeps every item" || { echo "FAIL recall R5 exact budget"; fail=1; } +rrun "$RIN" RECALL_BUDGET=$2; rctx >"$T/rc.ctx" +$PY - "$(wp "$T/rc.ctx")" "$(wp "$T/rc.full")" "$2" <<'PYE' && echo "ok recall R5 one character short drops the last item whole" || { echo "FAIL recall R5 budget minus one"; fail=1; } +import sys +got = open(sys.argv[1], encoding="utf-8").read() +want = "\n".join(open(sys.argv[2], encoding="utf-8").read().split("\n")[:-1]) +assert got == want and len(got) <= int(sys.argv[3]) +PYE +rrun "$RIN" RECALL_BUDGET=$3; rctx >"$T/rc.ctx"; [ "$(wc -l <"$T/rc.ctx" | tr -d ' ')" = 1 ] && echo "ok recall R5 the header and one item fit their exact budget" || { echo "FAIL recall R5 one-item budget"; fail=1; } +rrun "$RIN" RECALL_BUDGET=$4; rchk "R5 a budget below the header and one item prints nothing" $rr +rrun "$RIN" RECALL_BUDGET=$5; rchk "R5 a budget with no room for an item prints nothing" $rr +# an item that does not fit ends the list, even when a later one would fit +rcl env FAKE_ITEMS="$(wp "$T/rc.items2")" $PY "$RC" prompt <"$RIN" >"$T/rc.out" 2>"$T/rc.err"; rctx >"$T/rc.full2" +$PY - "$(wp "$T/rc.full2")" "$(wp "$T")" <<'PYE' +import sys +L = open(sys.argv[1], encoding="utf-8").read().split("\n") +assert len(L) == 4 and len(L[2]) > len(L[3]), L +open(sys.argv[2] + "/rc.b2", "w").write(str(len(L[0]) + len(L[1]) + len(L[3]) + 2)) +PYE +rcl env FAKE_ITEMS="$(wp "$T/rc.items2")" RECALL_BUDGET="$(cat "$T/rc.b2")" $PY "$RC" prompt <"$RIN" >"$T/rc.out" 2>"$T/rc.err"; rctx >"$T/rc.ctx" +[ "$(wc -l <"$T/rc.ctx" | tr -d ' ')" = 1 ] && echo "ok recall R5 items drop from the tail: a big item ends the list" || { echo "FAIL recall R5 skipped over a big item"; fail=1; } +rcl env FAKE_ITEMS="$(wp "$T/rc.items0")" $PY "$RC" prompt <"$RIN" >"$T/rc.out" 2>"$T/rc.err"; rchk "R5 zero items prints nothing" $? +# R2: the deadline is a function of RECALL_DEADLINE_MS, and the child is killed +rm -f "$T/rc.mark" +rrun2 "$RIN" FAKE_SLEEP=3 FAKE_MARK="$(wp "$T/rc.mark")" RECALL_DEADLINE_MS=1000; rchk "R2 deadline 1000 prints nothing" $rr +[ $rms -ge 900 ] && [ $rms -le 1600 ] && echo "ok recall R2 deadline 1000 ms returns in ${rms} ms" || { echo "FAIL recall R2 deadline 1000 took ${rms} ms"; fail=1; } +rrun2 "$RIN" FAKE_SLEEP=3 RECALL_DEADLINE_MS=2000; rchk "R2 deadline 2000 prints nothing" $rr +[ $rms -ge 1900 ] && [ $rms -le 2600 ] && echo "ok recall R2 deadline 2000 ms returns in ${rms} ms" || { echo "FAIL recall R2 deadline 2000 took ${rms} ms"; fail=1; } +rrun2 "$RIN" FAKE_SLEEP=6; rchk "R2 default deadline prints nothing" $rr +[ $rms -ge 3900 ] && [ $rms -le 4900 ] && echo "ok recall R2 the default deadline is 4000 ms (${rms} ms)" || { echo "FAIL recall R2 default deadline took ${rms} ms"; fail=1; } +rrun "$RIN" FAKE_SLEEP=0.5 RECALL_DEADLINE_MS=1000; [ -s "$T/rc.out" ] && echo "ok recall R2 an answer inside the deadline is printed" || { echo "FAIL recall R2 answer inside the deadline lost"; fail=1; } +$PY -c 'import time;time.sleep(3)' +[ ! -e "$T/rc.mark" ] && echo "ok recall R2 the child is killed at the deadline" || { echo "FAIL recall R2 the child outlived the deadline"; fail=1; } +# the mind client and the recall hook: standard library only +for f in mind.py recall.py; do + if grep -qE '^import (requests|yaml)|^from (requests|yaml)' "$here/$f"; then echo "FAIL $f: a dependency beyond the standard library"; fail=1; else echo "ok $f: standard library only"; fi +done exit $fail