Skip to content

fix(tooling): spawn-peer OK only when the peer's agent actually took a turn - #139

Open
heavygee wants to merge 1 commit into
mainfrom
fix/spawn-peer-verify-agent-active
Open

fix(tooling): spawn-peer OK only when the peer's agent actually took a turn#139
heavygee wants to merge 1 commit into
mainfrom
fix/spawn-peer-verify-agent-active

Conversation

@heavygee

@heavygee heavygee commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Estate-side half of upstream tiann#1752 (upstream fix: tiann#1753).

Problem

hapi-spawn-peer's final gate was messages>=1 — but the wrapper's own remit ping is that message. A peer whose agent died at startup (a rejected --model spawns a child that never survives the agent handshake) archived with our ping as its only turn, and the wrapper still printed:

hapi-spawn-peer: OK <id>  name="…"  messages>=1

That is the empty shell the wrapper exists to catch, one turn later. Two peers were handed remits this way (b853fbd9…, 52e454f0…) with an OK on both.

Why active alone is not the fix

The first cut of this gated on session.active, and cold review killed it: the hub marks a session active when the CLI socket connects, and sessionFactory connects that socket before launching the agent. So by the time POST /spawn has returned us a sessionId, active is already true — for healthy and doomed spawns alike.

What separates them is what happens next. A child that dies at agent handshake either sends session-end or stops heartbeating, and the hub drops it inactive within ~30s (sessionCache.expireInactive).

Fix

Poll for an agent turn — a message beyond our ping, or thinking — and leave early on it. Otherwise wait that window out and require the session to be still active at the end.

  • exit 4 — remit never landed (unchanged, empty shell)
  • exit 5 — remit landed but the agent never took a turn and went inactive (new), with a diagnosis pointing at the machine's catalog (agent --list-models) rather than a CLI --help example
  • exit 0 — now carries proof=agent-turn or proof=still-active-after-Ns
  • exit 2 — a non-numeric HAPI_SPAWN_PEER_VERIFY_TIMEOUT_S is rejected up front, instead of crashing under set -e after the remit has already gone out

Default wait is 60s; a healthy peer leaves early on its first turn.

Tests

scripts/tooling/hapi-spawn-peer.test.sh drives the real wrapper against a stub hub modelled on the real one — active from t=0 in every scenario — across live / dead / quiet / silent / bad-timeout:

$ ./scripts/tooling/hapi-spawn-peer.test.sh
OK agent takes a turn reports OK (exit 0)
OK remit on a dead agent fails closed (exit 5)
OK quiet but still-active peer reports OK (exit 0)
OK undelivered remit fails closed (exit 4)
OK non-numeric verify timeout is a usage error (exit 2)
hapi-spawn-peer.test.sh: all patterns OK

Red-green verified: with origin/main's wrapper restored, the dead-agent case exits 0 instead of 5.

🤖 Generated with Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2c923ecdf6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/tooling/hapi-spawn-peer.sh Outdated
sleep 1
COUNT=$(message_count)
ACTIVE=$(session_active)
if [[ "${COUNT:-0}" -ge 1 && ( "$ACTIVE" == "true" || "${COUNT:-0}" -ge 2 ) ]]; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Gate success on the agent-ready signal

For the default Cursor ACP path, active does not prove that the underlying agent initialized: ApiSessionClient emits session-alive immediately when its hub socket connects (cli/src/api/apiSession.ts:328-341), while Cursor emits the distinct session-ready event only after establishing the ACP session (cli/src/cursor/cursorAcpRemoteLauncher.ts:367-388). A slow invalid-model or authentication failure can therefore accept the ping during this pre-ready window and satisfy this predicate on its first poll; if the failure finishes sooner, runCursor also persists a Cursor Agent failed event as a second message (cli/src/cursor/runCursor.ts:196-203), allowing the COUNT >= 2 branch to report success after archival. Gate on actual readiness or classify the message contents rather than accepting either transient activity or any second stored row.

Useful? React with 👍 / 👎.

if [[ "${COUNT:-0}" -ge 1 && ( "$ACTIVE" == "true" || "${COUNT:-0}" -ge 2 ) ]]; then
break
fi
if (( SECONDS >= DEADLINE )); then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enforce the deadline during HTTP polling

When the hub accepts connections but stalls these GETs, the deadline is checked only after both requests complete. Each request permits a ten-second transfer (curl --help all defines --max-time as “Maximum time allowed for transfer”), so one iteration can consume roughly 21 seconds before reaching this check; HAPI_SPAWN_PEER_VERIFY_TIMEOUT_S=3 can therefore block for about 21 seconds, and the documented 45-second default can reach about 63 seconds. Bound each request by the remaining verification budget or check the deadline between calls so automation can rely on the advertised timeout.

Useful? React with 👍 / 👎.

Comment thread scripts/tooling/hapi-spawn-peer.sh Outdated
err "VERIFY FAILED: session $PEER_ID took the remit but never became active"
err " and produced no agent output within ${VERIFY_TIMEOUT_S}s."
err " Most likely the agent died at startup - check --agent/--model against"
err " the machine's catalog (agent --list-models), not a CLI --help example."

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use the guarded agent-specific catalog lookup

This recovery instruction is unsafe on the common same-machine Cursor setup: the repository explicitly documents that starting agent --list-models while a Cursor ACP process is running terminates that process with SIGTERM (cli/src/agent/backends/acp/agentCliGuard.ts:17-26), and the built-in catalog implementation deliberately refuses this probe while the ACP lock is held (cli/src/modules/common/cursorModels.ts:135-143). A parent Cursor session that sees its spawned peer fail can therefore follow this advice and crash itself or another peer; for --agent values other than Cursor, the command also queries the wrong agent's models. Direct users to the hub's guarded, agent-specific machine catalog instead of the raw Cursor command.

Useful? React with 👍 / 👎.

…a turn

The final check was messages>=1, but the wrapper's own remit ping is that
message. A peer whose agent died at startup — a rejected --model spawns a
child that never survives the agent handshake — archived with our ping as
its only turn and the wrapper still printed OK. That is exactly the empty
shell this wrapper exists to catch, one turn later.

`active` alone would not have fixed it: the hub marks a session active
when the CLI socket connects, and sessionFactory connects that socket
before launching the agent, so every spawn reads active the moment we
have its id. What separates a live peer from a corpse is what happens
next — a dead child sends session-end or stops heartbeating, and the hub
drops it inactive within ~30s.

So: poll for an agent turn (a message beyond our ping, or thinking) and
leave early on it; otherwise wait that window out and require the session
to be still active at the end. Undelivered remit still exits 4;
delivered-onto-a-corpse now exits 5 with a diagnosis pointing at the
machine's model catalog rather than a CLI --help example. The OK line
carries which proof it got. A non-numeric
HAPI_SPAWN_PEER_VERIFY_TIMEOUT_S is now a usage error up front instead of
a set -e crash after the remit has already gone out.

Adds hapi-spawn-peer.test.sh, which drives the wrapper against a stub hub
modelled on the real one (active from t=0 in every scenario) across live,
dead, quiet, silent and bad-timeout. Red-green verified: on the pre-fix
wrapper the dead-agent case exits 0.

Context: upstream tiann#1752, tiann#1753

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
@heavygee
heavygee force-pushed the fix/spawn-peer-verify-agent-active branch from 2c923ec to a4382a2 Compare September 2, 2026 11:51
@heavygee heavygee changed the title fix(tooling): spawn-peer OK only when the peer's agent actually came up fix(tooling): spawn-peer OK only when the peer's agent actually took a turn Sep 2, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a4382a26ac

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +213 to +215
VERIFY_TIMEOUT_S="${HAPI_SPAWN_PEER_VERIFY_TIMEOUT_S:-60}"
[[ "$VERIFY_TIMEOUT_S" =~ ^[0-9]+$ ]] \
|| die "HAPI_SPAWN_PEER_VERIFY_TIMEOUT_S must be a whole number of seconds, got: $VERIFY_TIMEOUT_S"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate the timeout before spawning the peer

When HAPI_SPAWN_PEER_VERIFY_TIMEOUT_S is nonnumeric, this check runs only after the script has spawned and renamed the peer and delivered its handoff. The script then exits 2 despite those side effects, so automation that retries after the apparent usage failure can create duplicate peers and duplicate work; parse and validate the timeout before the first API mutation.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant