Skip to content

fix(subagents): fall back when subagent cwd was deleted mid-session#3300

Open
xouyang1 wants to merge 1 commit into
letta-ai:mainfrom
xouyang1:fix/subagent-stale-cwd
Open

fix(subagents): fall back when subagent cwd was deleted mid-session#3300
xouyang1 wants to merge 1 commit into
letta-ai:mainfrom
xouyang1:fix/subagent-stale-cwd

Conversation

@xouyang1

@xouyang1 xouyang1 commented Jul 10, 2026

Copy link
Copy Markdown

Summary

Subagents are spawned with cwd taken from USER_CWD, captured at session start — or at EnterWorktree, which migrates the session anchor into the new worktree — and never re-validated. (The anchor is mirrored in process.env.USER_CWD, the process cwd, and runtime context; the env copy takes precedence at spawn and is the only one never validated — getCurrentWorkingDirectory already 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 emits error without close (a bare null exit 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 reports no_changes, which renders as "Dreamed; no durable memory changes were needed."

Field incident

A session entered a worktree for a multi-file editing pass (EnterWorktree migrated 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 of EnterWorktree, so the session anchor was left pointing at a deleted directory. Every reflection after that died at spawn: 20 consecutive Subagent exited with code null failures, 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.ts is at its file-size baseline): resolveSubagentWorkingDirectory moved out and now validates every candidate cwd with isUsableDirectory, falling back with a debugWarn — the same read-time validate-and-fall-back pattern as fix(listener): fall back to boot dir when persisted cwd is deleted #3099 and as getCurrentWorkingDirectory itself.
  • src/agent/subagents/manager.ts: capture the spawn error and report Subagent process failed to start: <error> (cwd: <dir>) instead of a bare null exit; add a debugWarn to 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_worktree holds 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 an ExitWorktree/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.
  • The reflection agent's prompt contract addresses everything via $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.
  • A failure-aware backoff for the step-count trigger may be worth considering separately; with the cause fixed and failures now loud, per-turn retry is reasonable for transient errors.

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 deleted USER_CWD (with and without memoryScope, mirroring the field incident) and a deleted inherited memory root
  • bun 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 fail
  • bun 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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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