Skip to content

fix(coding-agent): trust supervisor-approved session renames in worker mode - #1814

Closed
snimu wants to merge 17 commits into
mainfrom
snimu/supervised-rename-authority
Closed

fix(coding-agent): trust supervisor-approved session renames in worker mode#1814
snimu wants to merge 17 commits into
mainfrom
snimu/supervised-rename-authority

Conversation

@snimu

@snimu snimu commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

What was wrong

Renaming a session could fail even though the rename was valid. In worker mode the supervisor already reserves and validates the new name, but the worker then validated it a second time against its own copy of the peer roster — which can be stale (sync failures are suppressed). So a name the supervisor had correctly approved could be rejected by the worker holding outdated data.

The fix

  • Worker-mode command renames (rename, active rename_saved_session, set_session_name) now go through a mutation-only path: the supervisor's reservation is trusted, the worker just applies the name and appends the RLM ledger rename.
  • Standalone daemons (no supervisor) keep the full local reservation + validation — nothing changes there.
  • One regression test: with the worker-side validator stubbed to throw, a supervisor-forwarded rename succeeds and the validator is never called.

18 production lines, deletion-first (the second validation layer is gone, not worked around).

How it's verified

tsgo clean, biome clean, focused rename suites pass (daemon + supervisor), full-suite failures reproduce identically on main (pre-existing, unrelated). Implemented and independently reviewed by two different models; the reviewer traced all worker command paths and the authenticated-socket gate.

Follow-up candidates noted in review (not in this PR, same bug class, pre-existing): the non-active rename_saved_session branch and worker-mode create-with-name paths still double-validate.

Note: intentionally no Linear ticket for this cleanup stack, so that check stays red.


Note

Medium Risk
RPC calls and agent waits can hang indefinitely if the child stays alive without responding; supervised renames and single-send messaging change failure and delivery semantics in multi-process setups.

Overview
This PR fixes three daemon/RPC reliability issues and adds regression tests.

Supervised session renames: In worker mode, rename, rename_saved_session (active session), and set_session_name now call setStateSessionNameForCommand, which applies the name via applyStateSessionName without a second local availability check. Standalone daemons still use setStateSessionName with reservation and validation.

Remote agent messages: sendRemoteAgentSessionMessage only retries connect/hello; once connected it sends once. If the connection drops after the supervisor accepted the message, the error surfaces instead of resending (avoids duplicate delivery).

RPC client: RpcClient removes default timeouts on send, refine, waitForIdle, and collectEvents. Transport failures (spawn error, stdout close, stop) reject pending requests and event waiters through failPendingOperations. prompt validates the RPC response; promptAndWait uses cancellable event collection so a failed prompt does not leave waiters hanging.

Reviewed by Cursor Bugbot for commit 1cb0f22. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Trust supervisor-approved session renames in worker mode and remove RPC client timeouts

  • In worker mode, AgentDaemon now applies supervisor-approved session names directly via applyStateSessionName without re-validating; non-worker mode keeps the existing reservation flow
  • Removes client-side timeouts from RpcClient for long-running RPC commands and agent turns; requests now resolve on response or reject on transport failure/stop
  • Adds transport error propagation: RpcClient latches transportError, fails all pending requests and event waiters on spawn/pipe/close failures, and includes stderr context
  • Fixes sendRemoteAgentSessionMessage to send remote agent messages exactly once; if the connection drops after the supervisor receives the message but before the response, the call fails without retrying
  • Risk: removing timeouts in RpcClient.send means hung child processes (stdin open, no response) will block indefinitely instead of timing out; verify callers handle the new failure-on-transport-error behavior and that prompt() now throws on command-level failures via getData(...)
📊 Macroscope summarized 1cb0f22. 5 files reviewed, 2 issues evaluated, 1 issue filtered, 1 comment posted

🗂️ Filtered Issues

packages/coding-agent/src/modes/daemon/daemon-mode.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 4005: rename_saved_session validates command.sessionPath in the supervisor but, for an active session, forwards the rename to the worker selected by activeSessionId. A client can supply the path of a different session, so the supervisor checks the wrong family/name scope; this new worker-mode path then skips local validation and renames the active session to a duplicate sibling name. Require the supplied path to match the routed active session or validate using that session's actual file. [ Failed validation ]

Linear: ENG-5648


Supersedes #1702 (recreated as a plain PR against main; GitHub's stack lock prevented retargeting the stacked PR).

}, 1000);

this.process?.on("exit", () => {
const timeout = setTimeout(() => child.kill("SIGKILL"), 1000);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High rpc/rpc-client.ts:158

stop() can remain pending indefinitely when a descendant keeps the child’s stdout or stderr pipe open: after the 1-second timer sends SIGKILL, it still waits for close, which is delayed by those inherited pipes. Resolve the shutdown promise from the timeout callback after sending SIGKILL, preserving the previous bounded shutdown behavior.

- 			const timeout = setTimeout(() => child.kill("SIGKILL"), 1000);
+			const timeout = setTimeout(() => {
+				child.kill("SIGKILL");
+				resolve();
+			}, 1000);
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/modes/rpc/rpc-client.ts around line 158:

`stop()` can remain pending indefinitely when a descendant keeps the child’s stdout or stderr pipe open: after the 1-second timer sends `SIGKILL`, it still waits for `close`, which is delayed by those inherited pipes. Resolve the shutdown promise from the timeout callback after sending `SIGKILL`, preserving the previous bounded shutdown behavior.

Evidence trail:
packages/coding-agent/src/modes/rpc/rpc-client.ts:111-115, 150-164 @ REVIEWED_COMMIT
git diff MERGE_BASE REVIEWED_COMMIT -- packages/coding-agent/src/modes/rpc/rpc-client.ts
https://nodejs.org/api/child_process.html#event-close

@snimu

snimu commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

Superseded: the chained stack was restructured into independent PRs (byte-identical combined tree). A fresh standalone PR for this change follows on the same branch name.

@snimu snimu closed this Aug 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant