fix(subagents): fall back when subagent cwd was deleted mid-session#3300
Open
xouyang1 wants to merge 1 commit into
Open
fix(subagents): fall back when subagent cwd was deleted mid-session#3300xouyang1 wants to merge 1 commit into
xouyang1 wants to merge 1 commit into
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Subagents are spawned with
cwdtaken fromUSER_CWD, captured at session start — or atEnterWorktree, which migrates the session anchor into the new worktree — and never re-validated. (The anchor is mirrored inprocess.env.USER_CWD, the process cwd, and runtime context; the env copy takes precedence at spawn and is the only one never validated —getCurrentWorkingDirectoryalready validates the runtime-context copy.) If that directory is deleted mid-session, every subsequent subagent spawn fails with ENOENT before the child executes an instruction, and the failure is invisible: a spawn failure emitserrorwithoutclose(a barenullexit code), the non-zero-exit branch returned without logging, and the reflection finalizer — inspecting the freshly created memory worktree that the dead child never got to write to — finds it clean with zero commits and reportsno_changes, which renders as "Dreamed; no durable memory changes were needed."Field incident
A session entered a worktree for a multi-file editing pass (
EnterWorktreemigrated the anchor). Eighteen minutes later the user asked the agent to stop using worktrees and work in the main checkout. The agent complied at the only layer it could reach — folded the changes onto main with raw git and removed the worktree — but there is no inverse ofEnterWorktree, so the session anchor was left pointing at a deleted directory. Every reflection after that died at spawn: 20 consecutiveSubagent exited with code nullfailures, each rendered as a benign no-op. Because failed reflections never advance the reflected-through pointer, the step-count trigger re-fired at every turn end, and memory consolidation silently stalled — the unreflected transcript backlog grew to 71 steps while the agent's direct memory writes continued to mask the gap. Recovery ultimately required the user to manually re-point the session's working directory in the desktop app; the agent had no operation available to repair its own anchor.Changes
src/agent/subagents/working-directory.ts(new module;manager.tsis at its file-size baseline):resolveSubagentWorkingDirectorymoved out and now validates every candidate cwd withisUsableDirectory, falling back with adebugWarn— the same read-time validate-and-fall-back pattern as fix(listener): fall back to boot dir when persisted cwd is deleted #3099 and asgetCurrentWorkingDirectoryitself.src/agent/subagents/manager.ts: capture the spawnerrorand reportSubagent process failed to start: <error> (cwd: <dir>)instead of a bare null exit; add adebugWarnto the previously silent non-zero-exit branch (the observability gap fix(memory): surface reflection failure details #3024 — closed as stale — also addressed).Scope notes for reviewers
This is deliberately the backstop half of the fix: it makes a dead anchor survivable and attributable at every consumer, including non-cooperative deletions (#3253 is the same class on the Bash-tool surface). The intent-restoring half is design-level, so recording the observations rather than prescribing:
enter_worktreeholds the previous cwd only transiently (for lock bookkeeping), not as anchor provenance — and git's own worktree→parent linkage is destroyed by the removal itself. Recording{current, parentCheckout}would make anExitWorktree/re-anchor operation well-defined; today, honoring "stop using this worktree" is only expressible as filesystem surgery below the layer that owns the anchor, which is exactly what stranded it.$MEMORY_DIR; its cwd is consumed only for project-settings resolution. Whether memory-profile children should pin to the harness-owned memory scope directory (giving up per-project settings) or keep the project cwd with this PR's fallback is a policy question — as is whether project-profile subagents would prefer fail-fast-with-context over running from a fallback directory.Test plan
bun test src/agent/subagent-model-resolution.test.ts— existing cwd-resolution tests converted to real temp dirs; three new tests cover a deletedUSER_CWD(with and withoutmemoryScope, mirroring the field incident) and a deleted inherited memory rootbun test src/tools/subagent-parent-id-propagation.test.ts src/headless-bidirectional-reflection.test.ts src/agent/memory-worktree.test.ts src/cli/memory-subagent-recompile.test.ts— 87 pass / 0 failbun run typecheck,bun run lint(changed files),check:cycles,check:boundaries,check:file-size(baseline ratcheted 1577 → 1556),check:module-ownership🤖 Generated with Claude Code