Repository navigation
Operator handoff (agentctl handoff now) expires without the model ever invoking the handoff skill #182
Description
What happened
While the agent was working on #179, the operator ran agentctl handoff now. The request (req-5427e55cdf31) hit its 240-second model deadline without the model invoking the handoff skill. The run was interrupted and finished as operator_handoff_expired, so no handoff note was written and the in-progress work was not preserved cooperatively.
The point of handoff now is a graceful stop. In this run it behaved like a hard kill that took 4 minutes.
Log excerpt (issue #179, 2026-10-08)
17:36:37 limits_checked turns=31; elapsed_seconds=367; main_context_tokens=106404; cache_hit_rate=0.0
17:36:51 operator_handoff_model_deadline_expired request=req-5427e55cdf31; deadline_seconds=240
17:36:51 token_budget_reconciled mode=measured; interrupted.estimated_tokens=0; main_hit_rate=0.1045
17:36:51 model_execution_finished status=operator_handoff_expired; Operator handoff req-5427e55cdf31 reached its
240-second model deadline without the model invoking the handoff skill.
17:36:51 transcripts_copied session_id=325efb0d-cc61-430c-8e88-ddb3234f934d; files=1; bytes=1047566
Elapsed was 367s at expiry, so the request was accepted about 127s into the attempt. Only about 31 turns ran overall, which means the model completed few turns while the request was pending.
Expected
After agentctl handoff now, the model should get the operator handoff instruction at the next tool boundary, or through the single fallback follow-up query after the 60s delivery window. It should then invoke the handoff skill and commit a note with reason: operator_request well before the 240s deadline. The status should be handoff_requested, not operator_handoff_expired.
What to investigate
The excerpt doesn't show which step failed. The run log and the copied transcript (session 325efb0d-…) should answer these:
- Was
operator_handoff_context_deliveredlogged? If so, the model got the instruction and ignored it or didn't finish in time. - Was
operator_handoff_fallback(oroperator_handoff_fallback_blocked) logged at about 60s after acceptance? If neither shows up, the fallback never fired (see_operator_due_action/_operator_stream_waitinsimple_coding_agent/model_execution.py). - Was
operator_handoff_begunlogged? If so, the skill started but didn't finish before the deadline. - Was the model stuck in one long tool call or subagent the whole time? A long-running tool can stop any tool boundary from being reached, and the fallback query has to interrupt it.
- Is 240s realistic for the handoff itself, given the main cache hit rate was about 10% and each turn re-sent about 106k uncached tokens? Cheaper token handoff: supply git state, finish in one Write + one Bash #179 is about making the token handoff cheaper, and the same cost applies here.
Relevant code
simple_coding_agent/model_execution.py:_operator_due_action,_operator_stream_wait,_run_operator_fallback,_receive_terminal(expire branch),_operator_handoff_instruction/_operator_handoff_followup_prompt_OPERATOR_HANDOFF_DELIVERY_WINDOW_SECONDS = 60,_OPERATOR_HANDOFF_MODEL_DEADLINE_SECONDS = 240simple_coding_agent/control.py(deadline computation),simple_coding_agent/agentctl.py
Acceptance criteria
- Root cause identified from the Cheaper token handoff: supply git state, finish in one Write + one Bash #179 run log/transcript and written up on this issue
- A regression test that reproduces the failure mode (e.g. the model is mid-tool or the instruction goes undelivered when the request is accepted) and confirms the handoff skill gets invoked before the deadline
- When an operator handoff expires, the log shows which step was reached (delivered / fallback / begun), so this kind of failure can be diagnosed from the log alone
Activity
lbacik commentedon Oct 11, 2026
Agent Brief
Category: bug
Summary: The operator handoff instruction can be delivered to a subagent instead of the main thread. That turns off the fallback, so the request expires without a handoff, and the expiry evidence doesn't show which step was reached.
Current behavior:
The run log and transcript from #179 aren't available for triage. The analysis below comes from reading the code in
ModelExecutor(the operator handoff path) and is the hypothesis to confirm.Delivery isn't restricted to the main thread. The PostToolUse hook injects the operator handoff instruction (
_operator_handoff_instruction) asadditionalContextat the first tool boundary on any thread. It then sets the "context delivered" flag. If that first boundary is a subagent's tool result:handoffskill invocations count);_operator_handoff_followup_prompt, due after the 60s delivery window) is skipped;operator_handoff_expired.The token soft-threshold instruction on the same hook already avoids this: it is delivered only on a main-thread boundary, or once no subagent is still running. The operator path has no such check, and no existing operator handoff test exercises a subagent (
agent_idset) boundary.The expiry evidence can be wrong. The
OPERATOR_HANDOFF_EXPIREDexplanation always says "without the model invoking the handoff skill", even when the skill was invoked (begun) but didn't finish before the deadline. Theoperator_handoff_model_deadline_expiredlog event records only the request id and deadline. It doesn't say whether the instruction was delivered (or on which thread), whether the fallback fired or was blocked, or whether the skill began. So the Cheaper token handoff: supply git state, finish in one Write + one Bash #179 failure can't be diagnosed from the log alone.Desired behavior:
The operator handoff instruction counts as delivered only when it reaches the main thread. A subagent tool boundary doesn't deliver it and doesn't mark it delivered.
If no main-thread boundary happens within the delivery window, the single fallback (interrupt + follow-up query) fires as designed. This includes the case where only subagent boundaries happened.
The existing rules stay as they are:
operator_handoff_fallback_blocked);On expiry, both the
OPERATOR_HANDOFF_EXPIREDexplanation and theoperator_handoff_model_deadline_expiredlog event state which steps were reached:The explanation must not say the skill was never invoked when it was.
Key interfaces:
ModelExecutor's PostToolUse hook (_record_post_tool_use): the operator delivery branch needs the same main-thread check (_is_main_thread) that the token soft-threshold branch uses._operator_due_action/_operator_stream_wait: the fallback must stay due while delivery hasn't reached the main thread.ModelExecutionStatus.OPERATOR_HANDOFF_EXPIREDevidence and theoperator_handoff_model_deadline_expiredevent: add the delivered/fallback/begun step trail.delivered/begun): reportdeliveredonly for a real main-thread or fallback delivery.Acceptance criteria:
agent_idset). Without the fix, the test shows the instruction going to the subagent and the fallback being skipped. With the fix, the subagent boundary injects no operator instruction, and the request isn't marked delivered.handoffskill in the main thread, the result ishandoff_requested, notoperator_handoff_expired.begun.operator_handoff_model_deadline_expireddetail includes the delivered/fallback/begun steps for at least the "never delivered" and "delivered, not begun" cases.agentctl handoff now) expires without the model ever invoking the handoff skill #182 states the root cause as confirmed by the regression test.Out of scope:
_OPERATOR_HANDOFF_DELIVERY_WINDOW_SECONDS(60s) or_OPERATOR_HANDOFF_MODEL_DEADLINE_SECONDS(240s).lbacik commentedon Oct 11, 2026
Agent Attempt Result: complete
Branch: agent/issue-182
PR: #203
Evidence
UV_PROJECT_ENVIRONMENT=/home/agent/venv-py311 uv run --python 3.11 --extra dev ruff check && UV_PROJECT_ENVIRONMENT=/home/agent/venv-py311 uv run --python 3.11 --extra dev pytest && UV_PROJECT_ENVIRONMENT=/home/agent/venv-py313 uv run --python 3.13 --extra dev pytest→ exit code 0Details
Claude SDK completed the implementation workflow.