Skip to content

Operator handoff (agentctl handoff now) expires without the model ever invoking the handoff skill #182

Closed

Description

@lbacik

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:

  1. Was operator_handoff_context_delivered logged? If so, the model got the instruction and ignored it or didn't finish in time.
  2. Was operator_handoff_fallback (or operator_handoff_fallback_blocked) logged at about 60s after acceptance? If neither shows up, the fallback never fired (see _operator_due_action / _operator_stream_wait in simple_coding_agent/model_execution.py).
  3. Was operator_handoff_begun logged? If so, the skill started but didn't finish before the deadline.
  4. 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.
  5. 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 = 240
  • simple_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

  1. added
    bugSomething isn't working
    ready-for-agentFully specified implementation issue ready for an unattended agent
    and removed on Oct 8, 2026
  2. lbacik commented on Oct 11, 2026

    @lbacik
    OwnerAuthor

    This was generated by AI during triage.

    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.

    1. Delivery isn't restricted to the main thread. The PostToolUse hook injects the operator handoff instruction (_operator_handoff_instruction) as additionalContext at the first tool boundary on any thread. It then sets the "context delivered" flag. If that first boundary is a subagent's tool result:

      • the subagent gets the instruction, and a subagent can't perform the handoff (only main-thread handoff skill invocations count);
      • because the request now counts as delivered, the single fallback follow-up query (_operator_handoff_followup_prompt, due after the 60s delivery window) is skipped;
      • the main thread is never told. The run continues until the 240s model deadline and ends as 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_id set) boundary.

    2. The expiry evidence can be wrong. The OPERATOR_HANDOFF_EXPIRED explanation always says "without the model invoking the handoff skill", even when the skill was invoked (begun) but didn't finish before the deadline. The operator_handoff_model_deadline_expired log 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:

      • newer pending requests are adopted until delivery, and the request is pinned once delivered or begun;
      • the fallback never bypasses the hard token limit (operator_handoff_fallback_blocked);
      • the 240s deadline still ends the run even after the skill has begun.
    • On expiry, both the OPERATOR_HANDOFF_EXPIRED explanation and the operator_handoff_model_deadline_expired log event state which steps were reached:

      • delivered: no, at a main-thread tool boundary, or via the fallback;
      • fallback: not due, fired, or blocked;
      • begun: yes or no.

      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_EXPIRED evidence and the operator_handoff_model_deadline_expired event: add the delivered/fallback/begun step trail.
    • Operator reporter events (delivered / begun): report delivered only for a real main-thread or fallback delivery.

    Acceptance criteria:

    • A regression test reproduces the suspected Cheaper token handoff: supply git state, finish in one Write + one Bash #179 failure. An operator request is latched, and the first PostToolUse boundary after it comes from a subagent (agent_id set). 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.
    • A test shows that when only subagent boundaries happen during the delivery window, the fallback follow-up query is issued once. If the model then invokes the handoff skill in the main thread, the result is handoff_requested, not operator_handoff_expired.
    • A test shows that a main-thread boundary after a subagent boundary still delivers the instruction exactly once.
    • A test shows that on expiry after the skill began, the explanation doesn't claim the skill was never invoked, and the log event records begun.
    • A test shows that on expiry the operator_handoff_model_deadline_expired detail includes the delivered/fallback/begun steps for at least the "never delivered" and "delivered, not begun" cases.
    • The existing operator handoff tests still pass unchanged, unless one of them encodes delivery on a subagent boundary.
    • A short comment on Operator handoff (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:

  3. self-assigned this
    on Oct 11, 2026
  4. lbacik commented on Oct 11, 2026

    @lbacik
    OwnerAuthor

    Agent Attempt Result: complete

    Branch: agent/issue-182
    PR: #203

    Evidence

    • Check command: 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 0
    • Review cycles: 0/2
    • Review findings: all clear

    Details

    Claude SDK completed the implementation workflow.

  5. removed
    ready-for-agentFully specified implementation issue ready for an unattended agent
    on Oct 11, 2026
  6. removed their assignment
    on Oct 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions