fix(issue-23-24): the dialog handler sees the ready banner on its own; only this dispatch's started is delivery evidence

Review round 1 (planner-reviewer-claude-01, reviewer-agy-01,
planner-reviewer-codex-01): one blocking defect and two P2s, all confirmed.

B-1. handle_startup_dialogs took its ready tokens only from the environment,
which wait_for_tui_ready fills when it evaluates the adapter facts. Called on
its own - as resume_session.sh calls it, with no readiness wait before it -
it had no tokens, its early-return branch could never fire, and every resume
of every agent idled through the full 20 s timeout (base: 0.03 s). rc was 0
throughout, so nothing failed; the resume tests just took ~30 s each.
_mam_load_dialog_facts now also takes MAM_STRONG_READY_TOKENS / MAM_READY_TOKENS
from the facts, but only when the caller has not set them, so a caller-set
value (wait_for_tui_ready's own, or a test steering readiness) is never
clobbered - the concern that made it take a single fact in the first place.
S5d pins one-iteration return on a ready first frame for five adapters with
no environment; S5e pins that caller tokens survive; R4 gets a wall-clock cap
so a timeout-idle can no longer pass as success. The old
_MAM_READY_TOKENS_CLAUDE fallback was left defined but unread - removed.

R1 / N-2. delegate_wait_started treated any status past `pending` as the
agent's acknowledgement. codex reproduced three false positives: `error` and
`cancelled` records with no started line, and a `running` record from another
dispatch. The status branch is gone; the evidence is a started line carrying
this dispatch's token, which the local publisher writes before it syncs the
status, so nothing that really started is missed (S7c re-pinned on the line
with running/completed/error statuses; S7f/S7g pin the three negatives).

N-4, taken with it: the events.log line is parsed as JSON rather than pattern-
matched. publish_event nests dispatch_token under `data`, and the old regex
only matched because of key order (S7h, real line shape and a reordered one).

R2 / N-1. Two tests hardcoded <repo>/.venv/bin/python and failed with
FileNotFoundError in every review worktree. sys.executable.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
GodopuandClaude Fable 5.1 committed 2026-09-22 10:25:52 +00:00
1 parent 0dfbb2cc8a
commit 10a678c010
3 files changed
+150 -42

No files matched your search

+51 -31
View File
@@ -71,7 +71,6 @@ done
_MAM_MODAL_TOKENS='Do you trust the files|Yes, proceed|No, exit|Allow this|browser to authenticate|Resuming the full session|Resume from summary|Try the new fullscreen renderer\?'
_MAM_HINT_TOKENS='Use arrow keys|Esc to cancel|Press Enter to continue'
_MAM_DIALOG_TOKENS="${_MAM_MODAL_TOKENS}|${_MAM_HINT_TOKENS}"
_MAM_READY_TOKENS_CLAUDE='Anthropic|Assistant|Chat|Welcome|Claude Code|Opus|Sonnet|Haiku'
# Workspace-relative defaults with environment overrides (Phase Z)
HOME_DIR="${HOME_DIR:-$HOME}"
@@ -2142,35 +2141,42 @@ delegate_publish_event() {
# line lands in the job's events.log, which publish_event writes before its
# wire publish so a slow broker cannot delay the verdict.
#
# Returns 0 on that evidence, 1 if neither appeared within T. Never re-injects:
# the single-injection contract is fail-closed.
# Returns 0 on that evidence, 1 if it did not appear within T. Never
# re-injects: the single-injection contract is fail-closed.
#
# The evidence is a `started` line in the job's events.log carrying THIS
# dispatch's token (nested under `data`, as publish_event writes it; the line
# is parsed as JSON, not pattern-matched, so key order is irrelevant). The
# registry's status is deliberately NOT evidence: a record can leave `pending`
# because another writer closed it (`error`, `cancelled`) or because a
# different dispatch is `running`, none of which says this agent saw this
# prompt (review R1). The local publisher writes the log line before it syncs
# the status, so nothing that really started is missed.
delegate_wait_started() {
local job_id="$1" ack_s="${2:-180}" tok="${3:-}"
[ -n "$job_id" ] || return 1
local reg_dir="${MAM_JOBS_DIR:-${WORKSPACE_ROOT:-$PWD}/.mam/jobs}"
local rec="$reg_dir/$job_id.json"
local ev_log="$reg_dir/$job_id.events.log"
local started_re='"event": *"started"'
if [ -n "$tok" ]; then
started_re='"event": *"started".*"dispatch_token": *"'"$tok"'"'
fi
local deadline st
local deadline
deadline=$(( $(date +%s) + ack_s ))
while :; do
st=""
if [ -f "$rec" ]; then
st=$(REC="$rec" python3 -c "
import json, os
try:
print(json.load(open(os.environ['REC'])).get('status', '') or '')
except Exception:
print('')
" 2>/dev/null || true)
fi
if [ -n "$st" ] && [ "$st" != "pending" ]; then
return 0
fi
if [ -f "$ev_log" ] && grep -Eq "$started_re" "$ev_log" 2>/dev/null; then
if [ -f "$ev_log" ] && EV_LOG="$ev_log" EV_TOK="$tok" python3 -c '
import json, os, sys
tok = os.environ.get("EV_TOK", "")
with open(os.environ["EV_LOG"], encoding="utf-8", errors="replace") as f:
for line in f:
try:
e = json.loads(line)
except Exception:
continue
if not isinstance(e, dict) or e.get("event") != "started":
continue
data = e.get("data") if isinstance(e.get("data"), dict) else {}
seen = data.get("dispatch_token") or e.get("dispatch_token") or ""
if not tok or seen == tok:
sys.exit(0)
sys.exit(1)
' 2>/dev/null; then
return 0
fi
if [ "$(date +%s)" -ge "$deadline" ]; then
@@ -2752,21 +2758,35 @@ send_keys_safe() {
}
# _mam_load_dialog_facts <agent>
# Populate MAM_STARTUP_DIALOGS for <agent>, once per agent per shell. Only that
# one fact is taken: `facts` is an eval contract (F8), and evaluating all of it
# here would clobber ready/placeholder tokens a caller deliberately set.
# An agent the adapters do not know keeps an empty list.
# Populate MAM_STARTUP_DIALOGS for <agent>, once per agent per shell, and the
# ready tokens (MAM_STRONG_READY_TOKENS / MAM_READY_TOKENS) only when the caller
# has not set them. `facts` is an eval contract (F8); evaluating all of it here
# would clobber tokens a caller deliberately set, so only these are taken, and
# the tokens only fill a gap. Without them the handler could never recognise a
# ready screen when called on its own - resume_session.sh does exactly that,
# with no wait_for_tui_ready before it - and idled through its whole timeout on
# every resume (review B-1). An agent the adapters do not know keeps an empty
# list and no tokens.
_mam_load_dialog_facts() {
local agent="$1" _f=""
local agent="$1" _f="" _d="" _s="" _r=""
if [ "${_MAM_DIALOGS_AGENT:-}" = "$agent" ]; then
return 0
fi
_f="$("$(_delegate_py_bin)" -m lib_py.agents facts "$agent" 2>/dev/null | sed -n 's/^MAM_STARTUP_DIALOGS=//p')" || _f=""
if [ -n "$_f" ]; then
eval "MAM_STARTUP_DIALOGS=$_f"
_f="$("$(_delegate_py_bin)" -m lib_py.agents facts "$agent" 2>/dev/null)" || _f=""
_d="$(printf '%s\n' "$_f" | sed -n 's/^MAM_STARTUP_DIALOGS=//p')"
_s="$(printf '%s\n' "$_f" | sed -n 's/^MAM_STRONG_READY_TOKENS=//p')"
_r="$(printf '%s\n' "$_f" | sed -n 's/^MAM_READY_TOKENS=//p')"
if [ -n "$_d" ]; then
eval "MAM_STARTUP_DIALOGS=$_d"
else
MAM_STARTUP_DIALOGS='[]'
fi
if [ -z "${MAM_STRONG_READY_TOKENS:-}" ] && [ -n "$_s" ]; then
eval "MAM_STRONG_READY_TOKENS=$_s"
fi
if [ -z "${MAM_READY_TOKENS:-}" ] && [ -n "$_r" ]; then
eval "MAM_READY_TOKENS=$_r"
fi
_MAM_DIALOGS_AGENT="$agent"
return 0
}