Skip to content

fix: repair stale conversation working directories#3318

Merged
cpacker merged 15 commits into
mainfrom
fix/let-9597-prune-stale-cwd-map
Jul 14, 2026
Merged

fix: repair stale conversation working directories#3318
cpacker merged 15 commits into
mainfrom
fix/let-9597-prune-stale-cwd-map

Conversation

@just-cameron

@just-cameron just-cameron commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Summary

  • prunes cwd mappings only when paths are confirmed missing or non-directories, preserving mappings on transient filesystem failures such as EACCES, EIO, and EMFILE
  • applies per-entry remote-settings patches to the latest on-disk state and publishes with atomic temp-file replacement, so concurrent listeners preserve one another's cwd and permission updates
  • serializes writers with fully initialized hard-link lock tokens and an immutable recovery-claim chain; crashed lock owners and crashed recovery owners can be taken over without deleting a newly acquired lock
  • uses a fresh captured path/token handle for every acquisition, keeps failed generations dirty, and bounds shutdown drain/cleanup retries to one deadline
  • journals conditional cwd deletions before lock acquisition, tags the source process's queued delete with the journal ID, and suppresses that delete once another writer has incorporated the journal
  • records explicit cwd choices as unconditional per-key assignments even when they match the process-local cache, so a later same-path reassignment wins over an older repair
  • validates the live boot cwd at every operational fallback and advances cwd_revision when it repairs a renamed or deleted boot path to a usable process-wide fallback
  • makes the listener the sole SIGINT/SIGTERM/SIGHUP shutdown owner so settings and telemetry flush in order
  • emits an optional monotonic cwd_revision after cwd changes and rejected stale cwd requests
  • recovers expected ENOENT and ENOTDIR cwd races by re-emitting authoritative device status without exposing raw filesystem errors in the transcript

Companion Desktop live-state fix: https://github.com/letta-ai/letta-cloud/pull/12985

Fixes https://linear.app/letta/issue/LET-9647/got-an-enoent-users-should-never-see-these-unless-its-a-true
Also repairs https://linear.app/letta/issue/LET-9597/repair-conversation-cwd-after-deleting-active-worktree

Root cause

Deleted worktree paths survived in listener and Desktop cwd maps. Export paths could replay those stale values, handleCwdChange surfaced realpath failures verbatim, and unordered settings writes could restore a mapping after repair. Multiple listener processes shared one settings file but previously published process-local full snapshots, allowing one listener to overwrite another's newer map.

Test plan

  • current settings, lock, control-input, protocol-ergonomics, and full listener-protocol suites: 181/181 passed
  • dead primary locks and abandoned recovery claims are recovered in regression tests
  • every acquisition uses a fresh token; unknown PID-liveness errors fail safe
  • 12 rounds of 16 simultaneous processes preserved all cwd and permission entries across clean, dead-lock, and orphan-claim starts
  • 12 contended repair processes durably replayed every deletion after all deleted paths were recreated
  • a fully written journal temp is replayed after a simulated crash before atomic rename
  • a real second process can apply a repair journal and explicitly reassign the same path without the source process's queued delete clobbering it later
  • failed writes remain dirty; unchanged saves retry; sync cwd repair fences older async generations
  • empty legacy migration persists durably and cannot resurrect a stale entry
  • bun run check (12/12 checks passed)
  • final fresh-context review of frozen heads 051fe90 / 7b301add40: no medium-or-higher findings

Risk / rollback

  • The lock protocol is scoped to processes on one host sharing a local filesystem. Listener replacement is quiescent: the old listener must stop before a version with a different lock protocol starts.
  • Ordinary saves remain asynchronous. Lock/filesystem failures stay queued, while graceful shutdown uses a bounded retry deadline and reports an unsuccessful drain rather than acknowledging dirty state as settled.
  • Conditional cwd deletions are journaled before the bounded wait; the main settings snapshot records applied journal IDs before journal cleanup, making crash replay idempotent and fencing the source writer's corresponding queued delete.
  • Only confirmed missing and non-directory paths are pruned. Unexpected stat failures preserve the mapping so the underlying error remains observable.
  • cwd_revision is additive and optional for wire compatibility; repeated-same-cwd repair requires both updated listener and renderer versions.

Letta Code (agent-c2adbf5c-8419-4211-8cd8-3740db164974)

Prune deleted or non-directory conversation cwd entries before listener exports cwd maps so clients do not reuse stale worktree paths.

👾 Generated with [Letta Code](https://letta.com)

Co-Authored-By: Letta Code <noreply@letta.com>

@cpacker cpacker left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Requesting changes because the exported map is clean only in memory; startup pruning is not durable.

P1: stale rows filtered during load are never persisted. loadRemoteSettings() drops missing paths at lines 60-68 before ListenerRuntime.workingDirectoryByConversation is built. Later, pruneStaleConversationWorkingDirectories() sees no stale entries, returns false, and never calls persistCwdMap(). The dead row remains in remote-settings.json; if that filesystem path is recreated and the listener restarts, the stale conversation mapping resurrects.

The same validation uses existsSync, so a regular file is accepted even though a cwd must be a directory.

Please make startup validation durable (and directory-aware), then add a disk/restart regression: write a stale persisted row, load the runtime, verify the persisted file is repaired, recreate the old path, reset/reload, and verify the mapping does not return. The current tests construct stale entries directly in the already-loaded runtime map, so they do not exercise this producer path.

@cpacker cpacker left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Follow-up 4260d4e4 resolves the requested changes. Startup validation now requires a real directory and synchronously writes the repaired remote-settings.json before returning. The new filesystem-backed regression begins from persisted stale state, verifies writeback, recreates the deleted path, resets the cache, and proves the old scope does not resurrect.

Verified locally: focused listener suites 165/165 and bun run check 12/12.

@cpacker cpacker changed the title fix: prune stale cwd map entries before export fix: repair stale conversation working directories Jul 14, 2026
@cpacker
cpacker requested a review from sarahwooders as a code owner July 14, 2026 07:04
@cpacker
cpacker merged commit 7d3afd5 into main Jul 14, 2026
23 checks passed
@cpacker
cpacker deleted the fix/let-9597-prune-stale-cwd-map branch July 14, 2026 16:34
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.

2 participants