feat: persist working directory cache across restarts [LET-7949] (#1364)
Co-authored-by: Letta Code <noreply@letta.com>
This commit is contained in:
@@ -3,7 +3,9 @@
|
|||||||
* Connects to Letta Cloud and receives messages to execute locally
|
* 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 path from "node:path";
|
||||||
import { APIError } from "@letta-ai/letta-client/core/error";
|
import { APIError } from "@letta-ai/letta-client/core/error";
|
||||||
import type { Stream } from "@letta-ai/letta-client/core/streaming";
|
import type { Stream } from "@letta-ai/letta-client/core/streaming";
|
||||||
@@ -630,7 +632,7 @@ function createRuntime(): ListenerRuntime {
|
|||||||
pendingInterruptedToolCallIds: null,
|
pendingInterruptedToolCallIds: null,
|
||||||
reminderState: createSharedReminderState(),
|
reminderState: createSharedReminderState(),
|
||||||
bootWorkingDirectory,
|
bootWorkingDirectory,
|
||||||
workingDirectoryByConversation: new Map<string, string>(),
|
workingDirectoryByConversation: loadPersistedCwdMap(),
|
||||||
queuedMessagesByItemId: new Map<string, IncomingMessage>(),
|
queuedMessagesByItemId: new Map<string, IncomingMessage>(),
|
||||||
queuePumpActive: false,
|
queuePumpActive: false,
|
||||||
queuePumpScheduled: 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<string, string> {
|
||||||
|
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<string, string>;
|
||||||
|
// Validate entries: only keep directories that still exist
|
||||||
|
const map = new Map<string, string>();
|
||||||
|
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<string, string>): void {
|
||||||
|
if (!shouldPersistCwd) return;
|
||||||
|
const cachePath = getCwdCachePath();
|
||||||
|
const obj: Record<string, string> = 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(
|
function setConversationWorkingDirectory(
|
||||||
runtime: ListenerRuntime,
|
runtime: ListenerRuntime,
|
||||||
agentId: string | null,
|
agentId: string | null,
|
||||||
@@ -748,10 +792,11 @@ function setConversationWorkingDirectory(
|
|||||||
const scopeKey = getWorkingDirectoryScopeKey(agentId, conversationId);
|
const scopeKey = getWorkingDirectoryScopeKey(agentId, conversationId);
|
||||||
if (workingDirectory === runtime.bootWorkingDirectory) {
|
if (workingDirectory === runtime.bootWorkingDirectory) {
|
||||||
runtime.workingDirectoryByConversation.delete(scopeKey);
|
runtime.workingDirectoryByConversation.delete(scopeKey);
|
||||||
return;
|
} else {
|
||||||
|
runtime.workingDirectoryByConversation.set(scopeKey, workingDirectory);
|
||||||
}
|
}
|
||||||
|
|
||||||
runtime.workingDirectoryByConversation.set(scopeKey, workingDirectory);
|
persistCwdMap(runtime.workingDirectoryByConversation);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearRuntimeTimers(runtime: ListenerRuntime): void {
|
function clearRuntimeTimers(runtime: ListenerRuntime): void {
|
||||||
|
|||||||
Reference in New Issue
Block a user