fix(issue-18): bind the heartbeat timer to the agent process; scope started to the attempt; ignore re-sent finished dispatches
Review round 1 (claude NOT PASS) corrections: - B2: the timer now watches a process - `--watch-pid`, or by default the nearest agent-CLI ancestor of the publishing command - and stops within one period once it is gone, so a crashed agent stops heartbeating and is closed after K x N; a remote runner watches itself. Documented as process liveness (an alive-but-stuck agent keeps heartbeating). - R1: `_wait_started` accepts an events.log `started` only when it carries the current dispatch token (loop jobs share one events.log). - R2: the remote runner ignores a re-sent dispatch of an attempt that already reached its terminal. - R3: registry.md EOF blank line (git diff --check clean). - R4: the ack error states the effective budget when the wall clock cut it. - P3: stop_timer verifies the pidfile pid is this job's timer before signalling; a retained state message keeps its own seq watermark so it can never make the monotonic guard drop its own event. - MULTI_AGENT_RULES.md timeout section updated for the contract. - tests: agent-process death stops the timer, recycled-pid safety, attempt-scoped started, re-sent dispatch; acceptance 3 now kills the agent process instead of the timer. Help captures refreshed (--watch-pid). The wrapper can be sourced with MAM_DELEGATE_JOB_NO_MAIN=1 for helper-level tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
This commit is contained in:
1 parent
c17c87439d
commit
0550320590
11 files changed
+270
-29
No files matched your search
@@ -604,6 +604,11 @@ _wait_started() {
|
||||
wall=$(( ${START_TIME:-$(date +%s)} + ${TIMEOUT:-3600} ))
|
||||
[ "$deadline" -gt "$wall" ] && deadline=$wall
|
||||
local ev_log="$REGISTRY_DIR/$job_id.events.log"
|
||||
# A loop job keeps one events.log across iterations: only a `started` that
|
||||
# carries THIS attempt's dispatch token counts (review R1).
|
||||
local started_re='"event": *"started"'
|
||||
[ -n "${_disp_tok:-}" ] && started_re='"event": *"started".*"dispatch_token": *"'"$_disp_tok"'"'
|
||||
local budget=$(( deadline - $(date +%s) ))
|
||||
while :; do
|
||||
st="$(_disk_status "$job_id")"
|
||||
if [ -n "$st" ] && [ "$st" != "pending" ]; then
|
||||
@@ -612,7 +617,7 @@ _wait_started() {
|
||||
fi
|
||||
# publish_event records `started` here before its wire publish and before
|
||||
# the status sync, so a slow broker cannot delay the acknowledgement.
|
||||
if [ -f "$ev_log" ] && grep -Eq '"event": *"started"' "$ev_log" 2>/dev/null; then
|
||||
if [ -f "$ev_log" ] && grep -Eq "$started_re" "$ev_log" 2>/dev/null; then
|
||||
echo "inject: job $job_id recorded 'started' in events.log; treating the prompt as delivered and continuing." >&2
|
||||
return 0
|
||||
fi
|
||||
@@ -620,7 +625,10 @@ _wait_started() {
|
||||
echo "inject: subscriber received started for job $job_id; treating the prompt as delivered and continuing." >&2
|
||||
return 0
|
||||
fi
|
||||
[ "$(date +%s)" -ge "$deadline" ] && return 1
|
||||
if [ "$(date +%s)" -ge "$deadline" ]; then
|
||||
ACK_WAITED_S="$budget"
|
||||
return 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
@@ -951,7 +959,7 @@ inject_agent() {
|
||||
local ack_s="${MAM_INJECT_ACK_GRACE_S:-$ACK_TIMEOUT}"
|
||||
if ! _wait_started "$job_id" "$ack_s" "${4:-}"; then
|
||||
trap - EXIT
|
||||
echo "ERROR: 프롬프트 주입 실패 — 세션 '$sess': no 'started' within ${ack_s}s (ack timeout); not re-injecting" >&2
|
||||
echo "ERROR: 프롬프트 주입 실패 — 세션 '$sess': no 'started' within ${ACK_WAITED_S:-$ack_s}s (ack timeout ${ack_s}s, bounded by the wall clock); not re-injecting" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -976,7 +984,7 @@ remote_dispatch() {
|
||||
fi
|
||||
local ack_s="${MAM_INJECT_ACK_GRACE_S:-$ACK_TIMEOUT}"
|
||||
if ! _wait_started "$job_id" "$ack_s" "$sub_log"; then
|
||||
echo "ERROR: remote session '$sess' published no 'started' within ${ack_s}s (ack timeout); not re-dispatching" >&2
|
||||
echo "ERROR: remote session '$sess' published no 'started' within ${ACK_WAITED_S:-$ack_s}s (ack timeout ${ack_s}s, bounded by the wall clock); not re-dispatching" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "작업이 원격 세션 '$sess'에 전달되었습니다."
|
||||
@@ -1069,4 +1077,7 @@ main() {
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
# Tests source this file to exercise single helpers (MAM_DELEGATE_JOB_NO_MAIN=1).
|
||||
if [ "${MAM_DELEGATE_JOB_NO_MAIN:-0}" != "1" ]; then
|
||||
main "$@"
|
||||
fi
|
||||
@@ -205,4 +205,3 @@ $PY scripts/registry.py logs --list # every job, live status
|
||||
| `transport` | `herdr` (pane injection) or `remote` (signed retained dispatch over the broker) | `herdr` |
|
||||
|
||||
`<registry>/<job>.heartbeat.pid` / `.heartbeat.log` belong to the running heartbeat timer; a terminal publish removes the pidfile.
|
||||
|
||||
Reference in new issue
Block a user