Skip to content

Session wedges permanently when a queued-lane message lands at turn end (idle finalization suppressed, queue never drains) #4755

Description

@NSTA1

Describe the bug

A session can end a turn and enter a permanently wedged state: neither idle nor
running. It accepts no further input, the app shows it as stopped, and queuing a
message into it silently does nothing. The only recovery is to kill the process.

The process does not crash. It stays alive, Responding=True, at zero CPU, with
no ERROR or WARN line anywhere in its log, and its last session events are a
clean assistant.turn_end. It is parked, not looping and not faulted.

The signature is a single log line, emitted 91 ms after the final turn end:

2026-09-07T08:44:37.505Z  assistant.turn_end                                (events.jsonl)
2026-09-07T08:44:37.596Z [DEBUG] [rust:copilot_runtime::session::registry]
  Session idle finalization suppressed
  {"queue_processing":false,"queued_lane":true,"immediate_lane":false,"host_pending_send":false}

A message arrived on the queued lane while the turn was running. At turn end
the registry correctly declined to finalize the session as idle because the lane
was non-empty, but the queue processor was never started (queue_processing: false) and nothing ever starts it. So the session cannot go idle (the queue is
non-empty) and cannot drain the queue (nothing is processing it). A lost wakeup
between turn-end idle finalization and the queued-lane drain.

The immediate lane does not rescue it. After the wedge I sent the session a
send_session_message with delivery_mode: "immediate". It was recorded as a
pending_messages_modified telemetry event and then never delivered, never
written to events.jsonl, and never processed. Once wedged, both lanes are dead.

Which sessions hit it. I grepped all 62 process logs on this machine for the
suppression line. It appears in exactly three, and all three are the same
workload shape: a long-running parent session that spawns several child sessions
with coordinate_with_creator: true and then runs multi-minute turns, so it
takes inbound cross-session messages at unpredictable moments. The examined
session made 12 create_session calls and received 11 inbound cross-session
messages before wedging.

The lane is the discriminator. In that same session, three earlier inbound
cross-session messages arrived as delivery="steering" (the immediate lane, at
08:32:00Z, 08:33:46Z and 08:34:30Z) and were all consumed normally mid-turn. The
message that wedged the session went to the queued lane. Since
send_session_message defaults to delivery_mode: "enqueue", any child session
replying to its parent with the default lands on the lane that can wedge.

That makes this reachable by ordinary multi-session orchestration, which is a
documented feature, not an exotic configuration.

Affected version

1.0.83-5 (also observed on the immediately preceding build; three occurrences
across 2026-09-06 and 2026-09-07). Running inside the GitHub Copilot desktop app,
which drives this CLI as one process per session.

Steps to reproduce the behavior

It is a race, so it reproduces probabilistically rather than deterministically.
Three occurrences in two days on a machine running this pattern continuously.

  1. Start a parent session and have it spawn several child sessions with
    create_session, passing coordinate_with_creator: true.
  2. Keep the parent busy in long turns (multiple minutes each).
  3. Have the children report back with send_session_message using the default
    delivery_mode (enqueue), at times they choose, so at least one message
    lands close to the instant a parent turn ends.
  4. Occasionally the parent's final turn end logs Session idle finalization suppressed with queued_lane: true and queue_processing: false, and the
    session is wedged from that moment on.

To confirm a wedge rather than a long turn: the process is alive and responding,
CPU is flat at zero, the log has no error, and no further events are appended to
events.jsonl.

Expected behavior

Suppressing idle finalization because the queued lane is non-empty should
guarantee the queue processor is subsequently started. The two decisions want to
be one atomic transition, so a session cannot end up in a state where it is
ineligible for idle and has nothing scheduled to drain the queue.

Failing that, either of these would make it self-healing rather than terminal:

  • a watchdog that re-checks a session which is non-idle with queue_processing: false and no in-flight turn, and starts the drain;
  • an immediate-lane delivery forcing a drain attempt, so a wedged session can
    be recovered by messaging it instead of by killing the process.

It would also help if pending_messages_modified were not reported as success to
the sender when the target session can never consume the message. From the
caller's side the send appears to succeed.

Additional context

Recovery, for anyone else who hits this: find the pid from the
inuse.<pid>.lock file in ~/.copilot/session-state/<sessionId>/ and stop that
process. The app restarts the session, replays a "Continue from where you left
off" prompt, and resumes normally. events.jsonl history and the git worktree
are untouched; only the undelivered pending messages are lost, and they were
never going to be delivered. I confirmed this: the wedged session restarted and
took a new turn 3 minutes later.

Diagnosis is straightforward if you have the logs, since the suppression line
names the exact state:

Get-ChildItem ~/.copilot/logs/*.log |
  ForEach-Object { Select-String -Path $_ -Pattern 'Session idle finalization suppressed' }

Environment

  • Operating system: Windows 11 Enterprise
  • CPU architecture: AMD64 (x86_64)
  • Host: GitHub Copilot desktop app (one CLI process per session), sessions are
    git worktree-backed
  • Shell: PowerShell

Observed impact. The wedged session was coordinating a multi-session
workstream, so the failure is not merely a lost message: the parent stops
supervising children that are still running, and there is no signal to the user
beyond the session appearing stopped. I have had to work around it by changing
our agent definitions to send all child-to-parent reports with delivery_mode: "immediate" and to stop setting notify_on_idle, purely to keep traffic off the
queued lane. That reduces the exposure but obviously does not close the race.

Activity

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

    area:agentsSub-agents, fleet, autopilot, plan mode, background agents, and custom agentsarea:sessionsSession management, resume, history, session picker, and session state

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions