Codex worker questions and owner interruption — VUH-1688
The native-controller attachment now extracts the thread UUID from a Codex rollout path. Previously it used the whole rollout basename, lost the hired controller, and fell through to the native queue. This also explains the reported unavailable question-answer channel.
Codex 0.160 async questions are item/started and item/completed notifications
containing an agentMessage with delivery: "async" and structured questions.
The item's ID is the function call_id; there is no async server request to
answer. Question IDs match the native TUI's JSON encoding of
["request_user_input_async", call_id, index]. Replies use
<send_user_message_question_reply> user-input envelopes, a fresh
clientUserMessageId, and ordinary turn/steer while active or turn/start
when idle. Confirmation requires the exact returned turn's user message,
client ID, and content. The original tool's {accepted: true} is no answer
receipt. Async replies have no atomic first-answer arbitration against an
owner reply; uncertain replies are latched against duplicate sends.
Sync questions retain their app-server response and winning tool-output check. Blocking questions hold normal sends; async questions preserve native nonblocking behavior. Both reach the exact persisted hiring conversation, and the read-only roster projection shows “Waiting on a question” with its ID without consuming a completion watch. Native approval prompts remain owner decisions.
Matching turn/completed events with status completed, interrupted, or
failed release the active turn. An idle notification without a completion
event triggers a read-only thread/read reconciliation: the exact active turn
must be terminal, with no newer active turn. Pending sends wait for this proof;
stale or foreign completions cannot clear a newer turn. Interrupted/failed
turns retire stale questions, while unresolved async questions survive normal
completion and remain answerable.
The integration uses the real Clankie adapter, client, WebSocket transport, hire ownership, roster projection, lead wake, and answer control. Only the external app-server and Herdr view are fixture boundaries. It does not call a model, sign in, touch a live pane, write Linear, or substitute terminal input. Restored owned control avoids erroneous native-queue fallback; this is not a claim that historical messages in an unmanaged native queue were drained.
Protocol provenance
Installed codex --version reported codex-cli 0.160.0.
The installed codex app-server generate-json-schema output and these primary
tagged sources ground the fixture and reply framing:
- Async tool handler
- Native async question identities
- Native answered-question envelope
- Native app-server session dispatch
- Persisted native user-message identity
Verification
All checks passed on the final source and protocol fixtures:
- Focused Vitest: 122/122, ten files. The three new integration cases cover path-bound hire/lead/roster routing, active async answers and nonblocking follow-ups, interrupted-turn idle reconciliation, immediate release from an authenticated terminal event while an older read is held, late-read fencing, completed-before-subscription question hydration, idle reply turns, native UTF-8 framing, and missing-receipt/duplicate-answer refusal.
@clankie/clankieand@clankie/agent-hoststypechecks: passed.- Changed TypeScript lint, formatting, and
git diff --check: passed. - Documentation link, retired-claim, and public-guide checks: passed.
The focused test command was:
pnpm exec vitest run --config vitest.config.ts \
apps/clankie/test/codex-app-server.test.ts \
apps/clankie/test/codex-seat-adapter.test.ts \
apps/clankie/test/codex-user-input.test.ts \
apps/clankie/test/codex-hired-seat-protocol.integration.test.ts \
apps/clankie/test/fleet-seat-boundary.test.ts \
apps/clankie/test/fleet-seat-events.test.ts \
apps/clankie/test/worker-lead-routing.test.ts \
apps/clankie/test/seat-adapter-hire.test.ts \
apps/clankie/test/codex-seat-driver.test.ts \
apps/clankie/test/remote-codex-app-server.test.ts
Live owner/TUI verification remains a separate manual check. Historical
unmanaged native-queue drainage is unverified. No full pnpm check, eval,
sign-in, or live model call was run.
Manual check
- From the intended lead conversation, hire a Codex worker with a brief asking
it to use
request_user_input_asyncfor a worktree choice. Keep that worker's native Herdr pane visible. - Verify the hiring conversation receives the question, exact
call_id, and JSON-encoded question IDs, and its roster summary shows the pending request. - Reply with
message_seatusing onlyquestionAnswer, the observed request ID, and all supplied question IDs. Confirmstatus: answeredand a matching attributed user message in that same native thread. An active turn should be steered without an interrupt; an already completed question turn should start one reply turn. - Ask another async question and answer in the native pane. Verify a later lead reply to its resolved ID is refused. Do not resend an uncertain reply.
- Start another worker turn, interrupt it with Esc in its pane, and send a
follow-up from its lead. Confirm the idle seat accepts the follow-up once,
with no
queued_until_turn_endfallback. Complete it and send a second follow-up; confirm both return to the same lead. - Repeat with a blocking sync question. The lead reply should resolve the native request without interrupting it, and a normal follow-up should be held until the blocking prompt resolves. Permission prompts must still require the owner.
The fleet tool bridge returned HTTP 403 during this work. Issue evidence must
be relayed through the lead if it remains unavailable. At handoff, catalog
metadata recovered, but linear_get_issue still failed with a service timeout;
no issue comment was posted through an unverified connection.
Lead review correction
The async confirmation snapshot is checked for another native userMessage
reply envelope with a different client ID answering the same question IDs.
When the owner replied after the controller's guard and both messages landed,
the lead receives unconfirmed with detail answered_concurrently_by_owner,
rather than answered. The answer remains non-retryable. This detects the
observed race; it does not create upstream first-answer arbitration.
Cold hydration skips unanswered questions in older completed turns, while retaining the latest completed question and reconciling owner replies.
Review-fix checks passed: 64/64 focused tests in the four native protocol/adapter/parser/integration files, including 5/5 real WebSocket integration cases; scoped service typecheck, changed-file lint/format, and diff checks passed. The race fixture injects the distinct owner reply at native RPC receipt after the guard and before the lead reply is accepted; both replies are persisted. No live TUI or model was used.
