Skip to content

件名: Remote Control fails to re-attach (HTTP 400) when a parked local session is… #107503

Open

Description

@happycrelab-web

What happened
件名: Remote Control fails to re-attach (HTTP 400) when a parked local session is resumed by send_message from a scheduled task

Environment: Claude desktop app (Windows), Claude Code local sessions with Remote Control, project threads on claude.ai.

What happens:

  • A scheduled task (local, permission mode default) calls mcp__ccd_session_mgmt__send_message to another local session that has Remote Control enabled (session ).
  • The send returns "delivered" and the target session resumes and runs its turn.
  • But main.log shows: "Failed to toggle remote control for local_dfc92f6c…: Remote Control server rejected the request (HTTP 400)" and "Remote Control attach … → rc_enable_failed", ~1–2 seconds after "Resuming session … Starting local session".
  • As a result the resumed session cannot use its project-thread tools and cannot post to the claude.ai project thread.

Frequency: 3 out of 3 (2026-11-03 18:15, 11-04 19:09, 11-05 18:17 JST).
Contrast: when the same session wakes by the normal path ("Warming session", triggered by a message from the project thread), Remote Control attaches successfully 13 out of 13 times (10/23–11/5). Another session attached normally at the same moment (11-04 19:09:44), so a general server outage seems unlikely.

Expected: a session resumed by send_message re-attaches Remote Control the same way as on the normal wake path.

Related earlier report: 6eedf209-… (Remote Control sessions stall after resume from sleep).

Environment

  • Filed from: Report a problem in Claude Code
  • Project ID: chan_0178enTxAQQqfqc9ZfrF6ECs
  • Feedback ID: d1637077-bc84-441e-a57c-7020106ad008

Activity

  1. media-ctrl commented on Nov 6, 2026

    @media-ctrl

    The 3 of 3 versus 13 of 13 asymmetry is the diagnosis, and it points at a race rather than at credentials.

    You have the attach issued one to two seconds after Resuming session ... Starting local session. That is the window where a parked session's remote-control descriptor may not exist yet. A 400 is also the right status for "attach to a session I do not have registered": a credential problem gives 401 or 403, and a bad wake path gives a request that was never addressed to the remote-control service at all. So the most likely shape is that send_message resumes the session and the daemon attaches before the session has told the remote-control service it exists.

    Three probes, cheapest first:

    1. Capture the 400 body. Your report has the status and the log line but not the payload. If the body names an unknown or expired session, the race is confirmed and the report becomes a one-line maintainer item.

    2. Send twice. Message the same parked session, then message it again 20 to 30 seconds later once it is warm, and capture the attach result for both. If the second attach lands, state converges and the defect is ordering, not which path woke the session. That single result also tells you whether a retry is a viable workaround (send a throwaway message, wait, then send the real one).

    3. Diff the two attach requests. The project-thread wake attaches successfully every time, so compare the request it sends with the request the send_message wake sends: method, session id, and the auth header source. If they differ by a field, the bug is client-side and fixed by using the same attach routine on both paths. If they are identical, the server is rejecting a session it has not yet registered, which is server-side.

    One variable to hold down: the two wake paths may not use the same credential. A project-thread wake can attach with the thread's own authorization, and a send_message wake attaches with whatever the local session stored. A stale stored token produces a generic rejection that looks identical to a race in the log. Forcing re-auth on the target session and repeating the send is a two minute check that rules it in or out.

    Worth noting in the issue for whoever fixes it: a failed attach does not kill the session, so the failure mode is a session that is alive, running turns, and mute for its thread. That asymmetry is why it reads as a data-loss bug in normal use, and why the attach should probably retry with backoff for a few seconds instead of being treated as terminal.

  2. happycrelab-web commented on Nov 10, 2026

    @happycrelab-web
    Author

    Results from the probes suggested above (Claude desktop app 4.4212.0.0 on Windows 11, bundled Claude Code 2.1.309).

    Log tally first: across the app logs I have (2026-09-22 to 2026-11-10), Remote Control attach succeeded 154 times and failed 4 times. All 4 failures are the only 4 occurrences of the desk-resume path (remote-control.log: "adopted desk-resumed remote session"), i.e. a parked session resumed locally by send_message. 4 of 4 failed; every other attach succeeded.

    1. 400 response body:
      Not captured. The desktop logs only record the summary line ("Remote Control server rejected the request (HTTP 400) — run /remote-control to retry"). I tried enabling CLI debug logging through the user settings file (env DEBUG=1 and CLAUDE_CODE_DEBUG_LOGS_DIR) and resent at 23:02:57: the attach failed again with the same 400 (23:03:05), but no debug log file was written for the desktop-spawned CLI, so the body is still unknown. If there is a supported way to turn on debug logging for sessions spawned by the desktop app, I can repeat this.

    2. Double send to the same parked session (reproduced live on 2026-11-10, JST):

      • 22:36:10 1st send → session resumed ("Resuming session ... Starting local session"), "adopted desk-resumed remote session=cse_01EDeRRx…", then at 22:36:12 bridge_state "failed" ("terminal") → HTTP 400 → rc_enable_failed (expected cse_01EDeRRx…, cli reported none).
      • 22:36:46 2nd send (36 s later, process warm, 1st turn finished at 22:36:30) → delivered and the turn ran, but no attach was attempted at all (no "Enabling remote control" line). Remote Control stayed off for the session afterwards (remoteControlState "off").
        So state does not converge on its own: the failure is marked terminal and a second send does not retry. A "throwaway message, wait, then send" workaround does not help.
        In past occurrences the session only reconnected on the next project-thread wake (~20 min later), where the daemon logged "re-attach rc_enable_failed; recycling process" and then attached normally.
    3. Attach request comparison (project-thread wake vs send_message wake):
      I could not diff the raw requests (method / auth header source) without debug logging, but the logs show a difference in sequence:

      • project-thread wake (success): the daemon polls work, acks it ("admitted (resume, nudge poll) ... ack=~400ms"), then attaches ("attached ... in ~2.5–5 s").
      • send_message wake (failure): the desktop app resumes the session locally, the daemon only "adopts" it with no work poll/ack, and the CLI's attach is rejected with 400 about 2 s later.
        My guess (not verified) is that the server rejects the attach because no work item was dispatched/acked for that session on this path.
      • method: not captured
      • session id: same remote session id is expected on both paths (log: "expected cse_…")
      • auth header source: not captured
    4. Forced re-auth / manual retry on the target session:
      The suggested recovery is not reachable. The failure message says "run /remote-control to retry", but running /remote-control in that session (23:36:46, typed in the desktop app) is refused with "Remote Control was started from your phone or claude.ai and stays on for this session. To end it, end the session." main.log: "Refusing Remote Control toggle for local_3804fd82… : bound to a remote client (auto=false)". At the same time the session's Remote Control panel shows "Off". So the session is considered bound to a remote client (toggle locked) while its bridge is actually in the failed/terminal state — it cannot be retried from the session or from the app's Remote Control switch.
      Full re-auth: I signed out of the desktop app and signed back in (2026-11-11 JST; main.log 00:13:42 "clearing token cache", 00:14:01 "performing fresh /authorize"). The session had been stopped at 00:03:38. I then sent to it again at 00:14:46: same result, "adopted desk-resumed remote session", HTTP 400 at 00:14:49, rc_enable_failed. So a stale stored credential does not explain it; with fresh tokens the send_message path still fails (1 of 1 after re-auth).

    Environment: Claude desktop app 4.4212.0.0 (MSIX) with Claude Code 2.1.309, Windows 11 Home.

  3. media-ctrl commented on Nov 10, 2026

    @media-ctrl

    Your re-auth test settles the credential question, so the 4 of 4 versus 154 of 154 split is a statement about the wake path and about nothing else.

    The ordering in your own logs is worth more than the 400 body. On the project-thread wake the daemon polls work, acks it, and only then attaches, 2.5 to 5 s later. On the send_message wake there is no poll and no ack: the desktop app resumes the session locally, the daemon adopts it, and the attach is issued about 2 s later. Same client, different session lifecycle state. I would not expect a response body to add much beyond telling you which of those two states the server saw, and you can test that without it: drop network access on the machine and repeat a send to a parked session. A 400 with nothing reachable means a local component is producing the status; the identical 400 as before means the server answered and the body is worth chasing.

    On the debug logging question, I do not know a supported switch for desktop-spawned sessions, so treat the following as a probe rather than an answer. Variables written into a settings file the app itself reads are not the same as the OS environment, and a spawned child inherits the OS one. Set them as Windows user environment variables, restart the app, and see whether a log file appears at all. That is one run, and it also tells you whether the spawn path scrubs the environment.

    The more reportable defect is the state you reach afterwards, not the initial failure. main.log reports the session as bound to a remote client (auto=false) while the panel shows Remote Control off, so after a terminal attach failure the client refuses the retry that the error message tells you to make, and only the daemon recycling on the next project-thread wake clears it. Your second send is the tell: it delivered and ran the turn without attempting an attach at all. That means the client marked the session as already enabled or as terminal, and that marker outlives the bridge failure. Clearing the binding whenever bridge_state is failed would make /remote-control reachable again and turn your 20 minute stall into a retry.

  4. happycrelab-web commented on Nov 11, 2026

    @happycrelab-web
    Author

    Thanks — agreed that the re-auth result takes credentials off the table, and the poll/ack-then-attach vs. resume-then-attach ordering is the clearest way to state the difference between the two wake paths. Also agreed that the post-failure state is the more reportable part: once the attach fails, the retry that the error message suggests is refused until a project-thread wake recycles the daemon. Clearing the binding when bridge_state is failed would turn that stall into a recoverable retry.

    Network-off probe: I couldn't run it as designed. Issuing the send needs network (the sending session has to reach the model), and send_message executes without a permission prompt even in manual mode, so there was no point at which I could drop the network between "send issued" and "session resumed".

    What I could add is another online repro, this time from a plain local session (not a Remote Control–driven one), 11/11 14:58 JST:

    • 14:58:05 Resuming session local_… / Starting local session
    • 14:58:06 Enabling remote control … ; remote-control.log: "adopted desk-resumed remote session=cse_…"
    • 14:58:08 bridge_state: "failed" ("terminal") — HTTP 400; rc_enable_failed (expected cse_…, cli reported none)
    • No "Poll received work" / "admitted … ack" before the resume.
    • The local turn itself completed normally.

    A project-thread wake on the same machine two minutes later went poll → ack → spawn → attached in ~2 s. So the sender type doesn't matter: the desk-resumed adopt path fails, the polled path succeeds.

    One inconsistency I can't resolve from the logs: at 15:00:20 main.log shows "POST …/unarchive -> 409" while remote-control.log shows "unarchive ok" for the same remote session.

    I haven't tried the Windows user environment variable route for debug logging yet, since it needs an app restart that stops every running session. I can do it if the 400 body is still needed.

    Environment: Claude desktop app 4.4212.0.0, Claude Code 2.1.309, Windows 11 Home.


    Generated by Claude Code

  5. media-ctrl commented on Nov 11, 2026

    @media-ctrl

    Your plain-local-session repro closes the last gap in the split. If a session that is not Remote Control driven takes the desk-resumed adopt path and fails identically, then the discriminator is the lifecycle path itself and not the sender type. That also removes anything about how send_message is invoked from the picture.

    On your 409 versus "unarchive ok" divergence: treat the two logs as different layers reporting different things. A 409 from POST /unarchive means the service already considered that remote session un-archived, so the call changed nothing. The desktop log line is a client side success print for the call, not the service state. Same session, two answers, no contradiction. If the app records the status code alongside the call, that entry settles which layer printed what.

    The 400 body is not worth restarting every running session. It will not name the lifecycle state. The cheaper evidence is the two lines of remote-control.log around "adopted desk-resumed remote session": the bridge_state written before the adopt and the one written after it.

    Agreed on the fix shape. Clearing the binding when bridge_state is failed is what turns the stall into a retry that can actually attach.

  6. happycrelab-web commented on Nov 12, 2026

    @happycrelab-web
    Author

    Checked the two logs around every adopt. One correction to the premise first: remote-control.log never writes bridge_state on this machine (0 lines). bridge_state only appears in main.log, at 1-second resolution and without a session id, so I attributed each line by the "Failed to toggle remote control for local_…" / "Remote Control attach for local_… →" lines in the same second.

    With that caveat, the pattern is the same in all 8 adopts on record (11/02–11/11):

    • Before the adopt: no bridge_state for that session at all. The last entry for it is from its previous life (e.g. "connected" on 11/05 18:17, then a governor_evict with no bridge_state written).
    • After the adopt: exactly one line, bridge_state "failed" ("terminal") with HTTP 400, 2–8 s later. It never passes through "ready" or "connecting".

    Latest example (11/11 JST):

    main.log  14:58:05      Resuming session local_…ce6a
    rc.log    14:58:06.216  adopted desk-resumed remote session=cse_…egWZ
    main.log  14:58:06      Enabling remote control
    main.log  14:58:08      bridge_state: "failed" ("terminal") — HTTP 400
    main.log  14:58:08      attach → rc_enable_failed (cli reported none)
    

    Successful polled path, same remote session, 11/05:

    rc.log    18:16:56.246  admitted (resume, nudge poll) ack=429ms
    main.log  18:16:56      Warming session → Enabling remote control
    main.log  18:17:00      bridge_state: "ready" → attached
    rc.log    18:17:00.709  attached in 4892ms
    main.log  18:17:01      bridge_state: "connected"
    

    The most telling case is 11/02: about 90 s after an adopt failed with 400, a nudge poll arrived for the same session. remote-control.log logged "re-attach rc_enable_failed; recycling process", and after the recycle the attach went ready → attached → connected in 2.8 s.

    So the polled path recycles the CLI process before enabling, while the adopt path enables on the resumed process and fails before the bridge ever reaches "ready". That is only what the ordering shows; what the bridge does internally is not visible in these logs.


    Generated by Claude Code

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions