Recall hook: the mind recalls into context on every prompt #25

Merged
eureka-admin merged 4 commits from eureka-substrate/recall-prompt into main 2026-10-22 17:49:47 +00:00
2 changed files with 140 additions and 87 deletions
Showing only changes of commit 5e71d8e4ff - Show all commits

tools: one mind client (mind.py); context-pack's private MCP client deleted

mind.call spawns the eureka-state binding, does the stdio handshake and one
tool call within a deadline, and kills the child on every exit path.
context-pack's mcp_view_once is deleted; mcp_view delegates to mind with
retry_once. Messages, exit codes, retry and env rules are unchanged (all 228
context-pack checks pass).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Eureka 2026-10-20 15:55:46 +02:00

View file

@ -15,17 +15,16 @@ anchor, so a budget smaller than the table prints the table and no window.
import argparse
import json
import os
import queue
import re
import subprocess
import sys
import threading
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import mind # noqa: E402
WINDOW_CAP = 100
CONTEXT = 3
DEFAULT_MAX_LINES = 800
ERROR_CODES = ("Unavailable", "Rejected", "Misconfigured", "TooLarge",
"Unencodable", "InvalidInput", "Internal")
HUNK = re.compile(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@")
HEX = re.compile(r"^[0-9a-fA-F]{7,40}$")
@ -112,93 +111,16 @@ def check_spec_shape(spec):
return spec
def mcp_view_once(spec_id):
exe = os.environ.get("EUREKA_STATE_EXE")
env = dict(os.environ)
args = []
try:
with open(os.path.join(os.path.expanduser("~"), ".claude.json"), encoding="utf-8") as f:
binding = json.load(f)["mcpServers"]["eureka-state"]
exe = exe or binding.get("command")
args = list(binding.get("args", []))
for k, v in (binding.get("env") or {}).items():
env.setdefault(k, v)
except (OSError, ValueError, KeyError, TypeError):
if not exe:
raise Fail(4, "binding: eureka-state is not configured")
if not exe:
raise Fail(4, "binding: eureka-state has no command")
try:
proc = subprocess.Popen([exe, *args], stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, env=env)
except OSError:
raise Fail(4, "transport: Unavailable")
lines = queue.Queue()
def pump():
for ln in proc.stdout:
lines.put(ln)
lines.put(None)
threading.Thread(target=pump, daemon=True).start()
def send(obj):
proc.stdin.write((json.dumps(obj) + "\n").encode("utf-8"))
proc.stdin.flush()
def reply(want):
while True:
try:
ln = lines.get(timeout=30)
except queue.Empty:
raise Fail(4, "transport: Unavailable")
if ln is None:
raise Fail(4, "transport: Unavailable")
try:
msg = json.loads(ln)
except ValueError:
continue
if isinstance(msg, dict) and msg.get("id") == want:
return msg
try:
send({"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {"protocolVersion": "2024-11-05", "capabilities": {},
"clientInfo": {"name": "context-pack", "version": "1"}}})
reply(1)
send({"jsonrpc": "2.0", "method": "notifications/initialized"})
send({"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"name": "view", "arguments": {"id": {"kind": "CutSpec", "id": spec_id}}}})
res = reply(2).get("result") or {}
except OSError:
raise Fail(4, "transport: Unavailable")
finally:
try:
proc.stdin.close()
proc.terminate()
except OSError:
pass
body = "".join(c.get("text", "") for c in res.get("content", []) if isinstance(c, dict))
if res.get("isError"):
code = next((c for c in ERROR_CODES if c in body), "error")
raise Fail(4, "view: " + code)
try:
view = json.loads(body)
except ValueError:
raise Fail(4, "view: unparsable answer")
if view is None:
raise Fail(4, "view: no such cut_spec")
return spec_from_json(view)
def mcp_view(spec_id):
if ":cut_spec:" not in spec_id:
raise Fail(2, "spec id: not a cut_spec id")
try:
return mcp_view_once(spec_id)
except Fail as first:
if first.msg.split(": ")[-1] != "Unavailable":
raise
return mcp_view_once(spec_id)
view = mind.retry_once(lambda: mind.call("view", {"id": {"kind": "CutSpec", "id": spec_id}}, 60))
except mind.MindError as e:
raise Fail(4, ("view" if e.kind in ("tool", "answer") else e.kind) + ": " + e.detail)
if view is None:
raise Fail(4, "view: no such cut_spec")
return spec_from_json(view)
def load_spec(spec_id, spec_json):

131
tools/mind.py Normal file
View file

@ -0,0 +1,131 @@
#!/usr/bin/env python3
"""The one way a Eureka script reaches the mind: the eureka-state stdio MCP binding.
call(tool, arguments, deadline) spawns the binding, does initialize,
notifications/initialized and tools/call, and returns the parsed JSON body of the
answer. It raises MindError for every failure and kills the child on every exit
path. It never retries; retry_once(fn) is the caller's choice. Standard library only.
The binding: EUREKA_STATE_EXE overrides the command; ~/.claude.json
mcpServers.eureka-state gives command, args and env; the caller's environment wins
over the binding's env. A MindError names a kind and a detail and never echoes an
input value (an endpoint, a path, a line of config).
"""
import json
import os
import queue
import subprocess
import threading
import time
ERROR_CODES = ("Unavailable", "Rejected", "Misconfigured", "TooLarge",
"Unencodable", "InvalidInput", "Internal")
class MindError(Exception):
"""kind: binding (no binding or no command), transport (spawn, pipe, deadline),
tool (the daemon refused; detail is an ERROR_CODES entry or "error"), answer
(the body was not JSON). detail is the code, always "Unavailable" for transport."""
def __init__(self, kind, detail):
super().__init__(kind + ": " + detail)
self.kind, self.detail = kind, detail
@property
def unavailable(self):
return self.detail == "Unavailable"
def binding():
exe = os.environ.get("EUREKA_STATE_EXE")
args = []
env = dict(os.environ)
try:
with open(os.path.join(os.path.expanduser("~"), ".claude.json"), encoding="utf-8") as f:
b = json.load(f)["mcpServers"]["eureka-state"]
exe = exe or b.get("command")
args = list(b.get("args", []))
for k, v in (b.get("env") or {}).items():
env.setdefault(k, v)
except (OSError, ValueError, KeyError, TypeError):
if not exe:
raise MindError("binding", "eureka-state is not configured")
if not exe:
raise MindError("binding", "eureka-state has no command")
return [exe, *args], env
def call(tool, arguments, deadline):
"""One tool call within `deadline` seconds in all. Returns the parsed JSON body."""
end = time.monotonic() + deadline
argv, env = binding()
try:
proc = subprocess.Popen(argv, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, env=env)
except OSError:
raise MindError("transport", "Unavailable")
lines = queue.Queue()
def pump():
for ln in proc.stdout:
lines.put(ln)
lines.put(None)
threading.Thread(target=pump, daemon=True).start()
def send(obj):
proc.stdin.write((json.dumps(obj) + "\n").encode("utf-8"))
proc.stdin.flush()
def reply(want):
while True:
try:
ln = lines.get(timeout=max(end - time.monotonic(), 0))
except queue.Empty:
raise MindError("transport", "Unavailable")
if ln is None:
raise MindError("transport", "Unavailable")
try:
msg = json.loads(ln)
except ValueError:
continue
if isinstance(msg, dict) and msg.get("id") == want:
return msg
try:
send({"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {"protocolVersion": "2024-11-05", "capabilities": {},
"clientInfo": {"name": "eureka-tools", "version": "1"}}})
reply(1)
send({"jsonrpc": "2.0", "method": "notifications/initialized"})
send({"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"name": tool, "arguments": arguments}})
res = reply(2).get("result") or {}
except OSError:
raise MindError("transport", "Unavailable")
finally:
try:
proc.stdin.close()
except OSError:
pass
try:
proc.kill()
proc.wait(timeout=2)
except (OSError, subprocess.TimeoutExpired):
pass
body = "".join(c.get("text", "") for c in res.get("content", []) if isinstance(c, dict))
if res.get("isError"):
raise MindError("tool", next((c for c in ERROR_CODES if c in body), "error"))
try:
return json.loads(body)
except ValueError:
raise MindError("answer", "unparsable answer")
def retry_once(fn):
"""Run fn; on an Unavailable MindError run it once more. Any other error stands."""
try:
return fn()
except MindError as first:
if not first.unavailable:
raise
return fn()