From e37e997b98fd6d2e9c96e841fb6878b8bbf81bfd Mon Sep 17 00:00:00 2001 From: Shubham Naik Date: Thu, 12 Mar 2026 11:34:04 -0700 Subject: [PATCH] feat: persist working directory cache across restarts [LET-7949] (#1364) Co-authored-by: Letta Code --- src/websocket/listen-client.ts | 53 +++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/src/websocket/listen-client.ts b/src/websocket/listen-client.ts index e1c7c42..47c97e7 100644 --- a/src/websocket/listen-client.ts +++ b/src/websocket/listen-client.ts @@ -3,7 +3,9 @@ * Connects to Letta Cloud and receives messages to execute locally */ -import { readdir, realpath, stat } from "node:fs/promises"; +import { existsSync } from "node:fs"; +import { mkdir, readdir, realpath, stat, writeFile } from "node:fs/promises"; +import { homedir } from "node:os"; import path from "node:path"; import { APIError } from "@letta-ai/letta-client/core/error"; import type { Stream } from "@letta-ai/letta-client/core/streaming"; @@ -630,7 +632,7 @@ function createRuntime(): ListenerRuntime { pendingInterruptedToolCallIds: null, reminderState: createSharedReminderState(), bootWorkingDirectory, - workingDirectoryByConversation: new Map(), + workingDirectoryByConversation: loadPersistedCwdMap(), queuedMessagesByItemId: new Map(), queuePumpActive: false, queuePumpScheduled: false, @@ -739,6 +741,48 @@ function getConversationWorkingDirectory( ); } +// --------------------------------------------------------------------------- +// CWD persistence (opt-in via PERSIST_CWD=1, used by letta-code-desktop) +// --------------------------------------------------------------------------- + +const shouldPersistCwd = process.env.PERSIST_CWD === "1"; + +function getCwdCachePath(): string { + return path.join(homedir(), ".letta", "cwd-cache.json"); +} + +function loadPersistedCwdMap(): Map { + if (!shouldPersistCwd) return new Map(); + try { + const cachePath = getCwdCachePath(); + if (!existsSync(cachePath)) return new Map(); + const raw = require("fs").readFileSync(cachePath, "utf-8") as string; + const parsed = JSON.parse(raw) as Record; + // Validate entries: only keep directories that still exist + const map = new Map(); + for (const [key, value] of Object.entries(parsed)) { + if (typeof value === "string" && existsSync(value)) { + map.set(key, value); + } + } + return map; + } catch { + return new Map(); + } +} + +function persistCwdMap(map: Map): void { + if (!shouldPersistCwd) return; + const cachePath = getCwdCachePath(); + const obj: Record = Object.fromEntries(map); + // Fire-and-forget write, don't block the event loop + void mkdir(path.dirname(cachePath), { recursive: true }) + .then(() => writeFile(cachePath, JSON.stringify(obj, null, 2))) + .catch(() => { + // Silently ignore write failures + }); +} + function setConversationWorkingDirectory( runtime: ListenerRuntime, agentId: string | null, @@ -748,10 +792,11 @@ function setConversationWorkingDirectory( const scopeKey = getWorkingDirectoryScopeKey(agentId, conversationId); if (workingDirectory === runtime.bootWorkingDirectory) { runtime.workingDirectoryByConversation.delete(scopeKey); - return; + } else { + runtime.workingDirectoryByConversation.set(scopeKey, workingDirectory); } - runtime.workingDirectoryByConversation.set(scopeKey, workingDirectory); + persistCwdMap(runtime.workingDirectoryByConversation); } function clearRuntimeTimers(runtime: ListenerRuntime): void {