@letta-ai/letta-code 0.24.3 → 0.24.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/letta.js CHANGED
@@ -3269,7 +3269,7 @@ var package_default;
3269
3269
  var init_package = __esm(() => {
3270
3270
  package_default = {
3271
3271
  name: "@letta-ai/letta-code",
3272
- version: "0.24.3",
3272
+ version: "0.24.5",
3273
3273
  description: "Letta Code is a CLI tool for interacting with stateful Letta agents from the terminal.",
3274
3274
  type: "module",
3275
3275
  bin: {
@@ -4721,6 +4721,7 @@ var init_timing = __esm(() => {
4721
4721
  // src/agent/client.ts
4722
4722
  var exports_client = {};
4723
4723
  __export(exports_client, {
4724
+ isDesktopLocalProxyUrl: () => isDesktopLocalProxyUrl,
4724
4725
  getServerUrl: () => getServerUrl,
4725
4726
  getMemfsServerUrl: () => getMemfsServerUrl,
4726
4727
  getClient: () => getClient,
@@ -4774,12 +4775,33 @@ function getServerUrl() {
4774
4775
  const settings = settingsManager.getSettings();
4775
4776
  return process.env.LETTA_BASE_URL || settings.env?.LETTA_BASE_URL || LETTA_CLOUD_API_URL;
4776
4777
  }
4778
+ function isLocalhostUrl(value) {
4779
+ if (!value)
4780
+ return false;
4781
+ try {
4782
+ const parsed = new URL(value);
4783
+ return ["localhost", "127.0.0.1", "::1", "[::1]"].includes(parsed.hostname);
4784
+ } catch {
4785
+ return false;
4786
+ }
4787
+ }
4788
+ function isDesktopLocalProxyUrl(value) {
4789
+ return process.env.LETTA_DESKTOP_DEBUG_PANEL === "1" && isLocalhostUrl(value);
4790
+ }
4777
4791
  function getMemfsServerUrl() {
4778
4792
  let settings = null;
4779
4793
  try {
4780
4794
  settings = settingsManager.getSettings();
4781
4795
  } catch {}
4782
- return process.env.LETTA_MEMFS_BASE_URL || settings?.env?.LETTA_MEMFS_BASE_URL || LETTA_CLOUD_API_URL;
4796
+ const configuredMemfsUrl = process.env.LETTA_MEMFS_BASE_URL || settings?.env?.LETTA_MEMFS_BASE_URL;
4797
+ if (configuredMemfsUrl) {
4798
+ return configuredMemfsUrl;
4799
+ }
4800
+ const apiUrl = process.env.LETTA_BASE_URL || settings?.env?.LETTA_BASE_URL;
4801
+ if (apiUrl && isDesktopLocalProxyUrl(apiUrl)) {
4802
+ return apiUrl;
4803
+ }
4804
+ return LETTA_CLOUD_API_URL;
4783
4805
  }
4784
4806
  async function getClient() {
4785
4807
  if (_testClientOverride) {
@@ -7128,6 +7150,13 @@ If worktree creation fails (locked index), retry up to 3 times with backoff (sle
7128
7150
  ### 2. Read existing memory
7129
7151
  Read the memory files in your worktree, to understand what already exists in the memory filesystem.
7130
7152
 
7153
+ Before adding or expanding \`system/\` memory, measure its current token footprint:
7154
+ \`\`\`bash
7155
+ letta memory tokens --format json --quiet --memory-dir "$WORKTREE_DIR/$BRANCH_NAME"
7156
+ \`\`\`
7157
+
7158
+ This command is memory-mode safe. Treat it as measurement only: use the reported \`total_tokens\` and per-file breakdown to decide whether new findings belong in \`system/\` or external memory. Do not use custom token-counting scripts, \`npx\`, \`awk\`, or \`find -exec wc\` for this.
7159
+
7131
7160
  ### 3. Read and analyze history
7132
7161
 
7133
7162
  Your prompt will specify a pre-split JSONL chunk file and its source format. Use these patterns to read it:
@@ -7765,6 +7794,11 @@ You can create, delete, or modify files — including their contents, names, and
7765
7794
  - Why did the agent make the mistakes it did? What was missing from context?
7766
7795
  - Why did the user have to make corrections?
7767
7796
  - Does anything in memory contradict the observed conversation history, or need updating?
7797
+ - Check the current size of \`system/\` before adding or expanding in-context memory:
7798
+ \`\`\`bash
7799
+ letta memory tokens --format json --quiet
7800
+ \`\`\`
7801
+ This command is memory-mode safe. Treat it as measurement only: decide whether a size is concerning based on the actual context and the value of the proposed change. Prefer adding durable detail to external memory unless the information is important enough to stay in-context.
7768
7802
 
7769
7803
  ### Step 4: Update memory files (if needed)
7770
7804
 
@@ -9669,6 +9703,17 @@ var init_models2 = __esm(() => {
9669
9703
  parallel_tool_calls: true
9670
9704
  }
9671
9705
  },
9706
+ {
9707
+ id: "deepseek-v4-pro",
9708
+ handle: "openrouter/deepseek/deepseek-v4-pro",
9709
+ label: "DeepSeek V4 Pro",
9710
+ description: "DeepSeek's V4 Pro model",
9711
+ updateArgs: {
9712
+ context_window: 1048576,
9713
+ max_output_tokens: 384000,
9714
+ parallel_tool_calls: true
9715
+ }
9716
+ },
9672
9717
  {
9673
9718
  id: "glm-5.1",
9674
9719
  handle: "zai/glm-5.1",
@@ -38513,6 +38558,8 @@ __export(exports_memoryGit, {
38513
38558
  ensureLocalMemfsGitConfig: () => ensureLocalMemfsGitConfig,
38514
38559
  commitAndSyncMemoryWrite: () => commitAndSyncMemoryWrite,
38515
38560
  cloneMemoryRepo: () => cloneMemoryRepo,
38561
+ buildNonInteractiveGitEnv: () => buildNonInteractiveGitEnv,
38562
+ buildGitAuthArgs: () => buildGitAuthArgs,
38516
38563
  assertMemoryRepoReadyForWrite: () => assertMemoryRepoReadyForWrite,
38517
38564
  addGitMemoryTag: () => addGitMemoryTag,
38518
38565
  PRE_COMMIT_HOOK_SCRIPT: () => PRE_COMMIT_HOOK_SCRIPT,
@@ -38622,11 +38669,27 @@ async function getAuthToken() {
38622
38669
  const client = await getClient();
38623
38670
  return client._options?.apiKey ?? "";
38624
38671
  }
38625
- async function runGit(cwd2, args, token) {
38626
- const authArgs = token ? [
38672
+ function buildGitAuthArgs(token) {
38673
+ return [
38674
+ "-c",
38675
+ "credential.helper=",
38676
+ "-c",
38677
+ "core.askPass=",
38627
38678
  "-c",
38628
38679
  `http.extraHeader=Authorization: Basic ${Buffer.from(`letta:${token}`).toString("base64")}`
38629
- ] : [];
38680
+ ];
38681
+ }
38682
+ function buildNonInteractiveGitEnv(env3 = process.env) {
38683
+ return {
38684
+ ...env3,
38685
+ GIT_TERMINAL_PROMPT: "0",
38686
+ GCM_INTERACTIVE: "never",
38687
+ GIT_ASKPASS: "",
38688
+ SSH_ASKPASS: ""
38689
+ };
38690
+ }
38691
+ async function runGit(cwd2, args, token) {
38692
+ const authArgs = token ? buildGitAuthArgs(token) : [];
38630
38693
  const allArgs = [...authArgs, ...args];
38631
38694
  let loggableArgs = args;
38632
38695
  if (args[0] === "config" && typeof args[1] === "string" && args[1].includes("credential") && args[1].includes(".helper")) {
@@ -38637,6 +38700,7 @@ async function runGit(cwd2, args, token) {
38637
38700
  debugLog("memfs-git", `git ${loggableArgs.join(" ")} (in ${cwd2})`);
38638
38701
  const result = await execFile("git", allArgs, {
38639
38702
  cwd: cwd2,
38703
+ env: buildNonInteractiveGitEnv(),
38640
38704
  maxBuffer: 10 * 1024 * 1024,
38641
38705
  timeout: 60000
38642
38706
  });
@@ -47319,13 +47383,25 @@ function pushUnique(list, seen, entry) {
47319
47383
  seen.add(key);
47320
47384
  list.push(entry);
47321
47385
  }
47386
+ function normalizePowerShellCommand(command) {
47387
+ const trimmed = command.trim();
47388
+ if (trimmed.startsWith("&") || trimmed.startsWith('"') || trimmed.startsWith("'")) {
47389
+ return trimmed.startsWith("&") ? trimmed : `& ${trimmed}`;
47390
+ }
47391
+ return trimmed;
47392
+ }
47393
+ function buildPowerShellCommand(command) {
47394
+ const powerShellCommand = normalizePowerShellCommand(command);
47395
+ const aliasPrelude = POWERSHELL_ENV_ALIASES.map((name) => `$${name} = $env:${name}`).join("; ");
47396
+ return `${aliasPrelude}; ${powerShellCommand}`;
47397
+ }
47322
47398
  function windowsLaunchers(command) {
47323
47399
  const trimmed = command.trim();
47324
47400
  if (!trimmed)
47325
47401
  return [];
47326
47402
  const launchers = [];
47327
47403
  const seen = new Set;
47328
- const powerShellCommand = trimmed.startsWith("&") || trimmed.startsWith('"') || trimmed.startsWith("'") ? trimmed.startsWith("&") ? trimmed : `& ${trimmed}` : trimmed;
47404
+ const powerShellCommand = buildPowerShellCommand(trimmed);
47329
47405
  pushUnique(launchers, seen, [
47330
47406
  "powershell.exe",
47331
47407
  "-NoProfile",
@@ -47407,7 +47483,19 @@ function buildShellLaunchers(command, options) {
47407
47483
  const login = options?.login ?? false;
47408
47484
  return process.platform === "win32" ? windowsLaunchers(command) : unixLaunchers(command, login);
47409
47485
  }
47410
- var SEP2 = "\x00";
47486
+ var SEP2 = "\x00", POWERSHELL_ENV_ALIASES;
47487
+ var init_shellLaunchers = __esm(() => {
47488
+ POWERSHELL_ENV_ALIASES = [
47489
+ "MEMORY_DIR",
47490
+ "LETTA_MEMORY_DIR",
47491
+ "AGENT_ID",
47492
+ "LETTA_AGENT_ID",
47493
+ "LETTA_PARENT_AGENT_ID",
47494
+ "CONVERSATION_ID",
47495
+ "LETTA_CONVERSATION_ID",
47496
+ "USER_CWD"
47497
+ ];
47498
+ });
47411
47499
 
47412
47500
  // src/hooks/types.ts
47413
47501
  function supportsPromptHooks(event) {
@@ -47863,6 +47951,7 @@ async function executeHooksParallel(hooks, input, workingDirectory = process.cwd
47863
47951
  }
47864
47952
  var DEFAULT_TIMEOUT_MS = 60000;
47865
47953
  var init_executor = __esm(() => {
47954
+ init_shellLaunchers();
47866
47955
  init_prompt_executor();
47867
47956
  init_types2();
47868
47957
  });
@@ -48895,6 +48984,7 @@ __export(exports_memoryFilesystem, {
48895
48984
  renderMemoryFilesystemTree: () => renderMemoryFilesystemTree,
48896
48985
  labelFromRelativePath: () => labelFromRelativePath,
48897
48986
  isMemfsEnabledOnServer: () => isMemfsEnabledOnServer,
48987
+ isLettaMemfsServer: () => isLettaMemfsServer,
48898
48988
  isLettaCloud: () => isLettaCloud,
48899
48989
  getMemorySystemDir: () => getMemorySystemDir,
48900
48990
  getMemoryFilesystemRoot: () => getMemoryFilesystemRoot,
@@ -49082,8 +49172,8 @@ function renderMemoryFilesystemTree(systemLabels, detachedLabels, options = {})
49082
49172
  }
49083
49173
  async function applyMemfsFlags(agentId, memfsFlag, noMemfsFlag, options) {
49084
49174
  const { settingsManager: settingsManager2 } = await Promise.resolve().then(() => (init_settings_manager(), exports_settings_manager));
49085
- if (memfsFlag && !await isLettaCloud()) {
49086
- throw new Error("--memfs is only available on Letta Cloud (api.letta.com).");
49175
+ if (memfsFlag && !await isLettaMemfsServer()) {
49176
+ throw new Error(await getMemfsSyncUnavailableMessage());
49087
49177
  }
49088
49178
  const hasExplicitToggle = Boolean(memfsFlag || noMemfsFlag);
49089
49179
  const localMemfsEnabled = settingsManager2.isMemfsEnabled(agentId);
@@ -49154,6 +49244,24 @@ async function isLettaCloud() {
49154
49244
  const serverUrl = getServerUrl2();
49155
49245
  return serverUrl.includes("api.letta.com") || process.env.LETTA_MEMFS_LOCAL === "1" || process.env.LETTA_API_KEY === "local-desktop";
49156
49246
  }
49247
+ function getServerHostLabel(serverUrl) {
49248
+ const trimmed = serverUrl.trim();
49249
+ try {
49250
+ return new URL(trimmed).host || trimmed;
49251
+ } catch {
49252
+ return trimmed.replace(/^https?:\/\//, "").replace(/\/+$/, "") || trimmed;
49253
+ }
49254
+ }
49255
+ async function isLettaMemfsServer() {
49256
+ const { getMemfsServerUrl: getMemfsServerUrl2, isDesktopLocalProxyUrl: isDesktopLocalProxyUrl2 } = await Promise.resolve().then(() => (init_client2(), exports_client));
49257
+ const memfsServerUrl = getMemfsServerUrl2();
49258
+ return memfsServerUrl.includes("api.letta.com") || isDesktopLocalProxyUrl2(memfsServerUrl) || process.env.LETTA_MEMFS_LOCAL === "1" || process.env.LETTA_API_KEY === "local-desktop";
49259
+ }
49260
+ async function getMemfsSyncUnavailableMessage() {
49261
+ const { getMemfsServerUrl: getMemfsServerUrl2 } = await Promise.resolve().then(() => (init_client2(), exports_client));
49262
+ const memfsServerUrl = getMemfsServerUrl2();
49263
+ return `MemFS sync failed (expected api.letta.com, got ${getServerHostLabel(memfsServerUrl)})`;
49264
+ }
49157
49265
  async function enableMemfsIfCloud(agentId) {
49158
49266
  if (!await isLettaCloud())
49159
49267
  return;
@@ -54922,13 +55030,19 @@ function validateWorktreePath(command, cwd2) {
54922
55030
  }
54923
55031
  return null;
54924
55032
  }
55033
+ function rebuildCachedLauncher(command) {
55034
+ if (!cachedWorkingLauncher)
55035
+ return null;
55036
+ const cachedExecutable = cachedWorkingLauncher[0]?.toLowerCase();
55037
+ if (!cachedExecutable)
55038
+ return null;
55039
+ const launchers = buildShellLaunchers(command);
55040
+ return launchers.find((launcher) => launcher[0]?.toLowerCase() === cachedExecutable) ?? null;
55041
+ }
54925
55042
  function getBackgroundLauncher(command) {
54926
- if (cachedWorkingLauncher) {
54927
- const [executable, ...launcherArgs] = cachedWorkingLauncher;
54928
- if (executable) {
54929
- return [executable, ...launcherArgs.slice(0, -1), command];
54930
- }
54931
- }
55043
+ const cachedLauncher = rebuildCachedLauncher(command);
55044
+ if (cachedLauncher)
55045
+ return cachedLauncher;
54932
55046
  const launchers = buildShellLaunchers(command);
54933
55047
  return launchers[0] || [];
54934
55048
  }
@@ -54944,9 +55058,8 @@ async function spawnCommand(command, options) {
54944
55058
  });
54945
55059
  }
54946
55060
  if (cachedWorkingLauncher) {
54947
- const [executable, ...launcherArgs] = cachedWorkingLauncher;
54948
- if (executable) {
54949
- const newLauncher = [executable, ...launcherArgs.slice(0, -1), command];
55061
+ const newLauncher = rebuildCachedLauncher(command);
55062
+ if (newLauncher) {
54950
55063
  try {
54951
55064
  const result = await spawnWithLauncher(newLauncher, {
54952
55065
  cwd: options.cwd,
@@ -55195,6 +55308,7 @@ var init_Bash2 = __esm(() => {
55195
55308
  init_worktree_ownership();
55196
55309
  init_process_manager();
55197
55310
  init_shellEnv();
55311
+ init_shellLaunchers();
55198
55312
  init_shellRunner();
55199
55313
  init_truncation();
55200
55314
  });
@@ -71382,6 +71496,7 @@ var DEFAULT_TIMEOUT = 120000;
71382
71496
  var init_Shell2 = __esm(() => {
71383
71497
  init_runtime_context();
71384
71498
  init_shellEnv();
71499
+ init_shellLaunchers();
71385
71500
  init_shellRunner();
71386
71501
  });
71387
71502
 
@@ -71527,6 +71642,7 @@ var init_ShellCommand2 = __esm(() => {
71527
71642
  init_context();
71528
71643
  init_readOnlyShell();
71529
71644
  init_Shell2();
71645
+ init_shellLaunchers();
71530
71646
  init_shellRunner();
71531
71647
  init_truncation();
71532
71648
  });
@@ -87563,6 +87679,60 @@ class StreamProcessor {
87563
87679
  }
87564
87680
 
87565
87681
  // src/cli/helpers/stream.ts
87682
+ function summarizeStreamForDebug(stream2) {
87683
+ if (!stream2 || typeof stream2 !== "object") {
87684
+ return `type=${typeof stream2}`;
87685
+ }
87686
+ const record = stream2;
87687
+ const ctor = stream2.constructor?.name;
87688
+ const controller = record.controller && typeof record.controller === "object" ? record.controller : null;
87689
+ const keys = Object.keys(record).slice(0, 8);
87690
+ return [
87691
+ `ctor=${ctor ?? "unknown"}`,
87692
+ `asyncIterator=${typeof record[Symbol.asyncIterator]}`,
87693
+ `controller=${typeof record.controller}`,
87694
+ `controllerAbort=${typeof controller?.abort}`,
87695
+ `controllerSignal=${typeof controller?.signal}`,
87696
+ keys.length > 0 ? `keys=${keys.join(",")}` : "keys=(none)"
87697
+ ].join(" ");
87698
+ }
87699
+ function summarizeChunkForDebug(chunk) {
87700
+ if (!chunk) {
87701
+ return "none";
87702
+ }
87703
+ const record = chunk;
87704
+ const parts = [`message_type=${chunk.message_type ?? "unknown"}`];
87705
+ for (const key of ["run_id", "seq_id", "id", "otid", "tool_call_id"]) {
87706
+ const value = record[key];
87707
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
87708
+ parts.push(`${key}=${value}`);
87709
+ }
87710
+ }
87711
+ if (chunk.message_type === "stop_reason") {
87712
+ parts.push(`stop_reason=${String(record.stop_reason ?? "unknown")}`);
87713
+ }
87714
+ const toolCalls = record.tool_calls;
87715
+ if (Array.isArray(toolCalls)) {
87716
+ parts.push(`tool_calls=${toolCalls.length}`);
87717
+ }
87718
+ return parts.join(" ");
87719
+ }
87720
+ function abortStreamController(stream2, reason) {
87721
+ const controller = stream2.controller;
87722
+ if (!controller || typeof controller !== "object") {
87723
+ debugWarn("drainStream", "stream.controller is unavailable during %s - cannot abort HTTP request (%s)", reason, summarizeStreamForDebug(stream2));
87724
+ return;
87725
+ }
87726
+ const controllerRecord = controller;
87727
+ if (controllerRecord.signal?.aborted) {
87728
+ return;
87729
+ }
87730
+ if (typeof controllerRecord.abort !== "function") {
87731
+ debugWarn("drainStream", "stream.controller.abort is unavailable during %s - cannot abort HTTP request (%s)", reason, summarizeStreamForDebug(stream2));
87732
+ return;
87733
+ }
87734
+ controllerRecord.abort();
87735
+ }
87566
87736
  function hasPaginatedItems(response) {
87567
87737
  return !Array.isArray(response) && typeof response.getPaginatedItems === "function";
87568
87738
  }
@@ -87640,28 +87810,26 @@ async function drainStream(stream2, buffers, refresh, abortSignal, onFirstMessag
87640
87810
  let stopReason = null;
87641
87811
  let hasCalledFirstMessage = false;
87642
87812
  let fallbackError = null;
87813
+ let lastChunkDebugSummary = "none";
87643
87814
  let abortedViaListener = false;
87644
87815
  const startAbortGen = buffers.abortGeneration || 0;
87645
87816
  const abortHandler = () => {
87646
87817
  abortedViaListener = true;
87647
- if (!stream2.controller) {
87648
- debugWarn("drainStream", "stream.controller is undefined - cannot abort HTTP request");
87649
- return;
87650
- }
87651
- if (!stream2.controller.signal.aborted) {
87652
- stream2.controller.abort();
87653
- }
87818
+ abortStreamController(stream2, "abort_signal");
87654
87819
  };
87655
87820
  if (abortSignal && !abortSignal.aborted) {
87656
87821
  abortSignal.addEventListener("abort", abortHandler, { once: true });
87657
87822
  } else if (abortSignal?.aborted) {
87658
87823
  abortedViaListener = true;
87659
- if (stream2.controller && !stream2.controller.signal.aborted) {
87660
- stream2.controller.abort();
87661
- }
87824
+ abortStreamController(stream2, "pre_aborted_signal");
87662
87825
  }
87663
87826
  try {
87827
+ const asyncIterator = stream2[Symbol.asyncIterator];
87828
+ if (typeof asyncIterator !== "function") {
87829
+ throw new TypeError(`Stream is not async iterable (${summarizeStreamForDebug(stream2)})`);
87830
+ }
87664
87831
  for await (const chunk of stream2) {
87832
+ lastChunkDebugSummary = summarizeChunkForDebug(chunk);
87665
87833
  recordTuiJsonPayload(`stream_chunk:${chunk.message_type ?? "unknown"}`, chunk);
87666
87834
  if ((buffers.abortGeneration || 0) !== startAbortGen) {
87667
87835
  stopReason = "cancelled";
@@ -87730,7 +87898,10 @@ async function drainStream(stream2, buffers, refresh, abortSignal, onFirstMessag
87730
87898
  const errorMessage = e instanceof Error ? e.message : String(e);
87731
87899
  const sdkDiagnostic = consumeLastSDKDiagnostic();
87732
87900
  const errorMessageWithDiagnostic = sdkDiagnostic ? `${errorMessage} [${sdkDiagnostic}]` : errorMessage;
87733
- debugWarn("drainStream", "Stream error caught:", errorMessage);
87901
+ debugWarn("drainStream", "Stream error caught: %s last_chunk=%s stream=%s", errorMessageWithDiagnostic, lastChunkDebugSummary, summarizeStreamForDebug(stream2));
87902
+ if (e instanceof Error && e.stack) {
87903
+ debugWarn("drainStream", "Stream error stack: %s", e.stack);
87904
+ }
87734
87905
  if (!streamProcessor.lastRunId && e instanceof APIError2 && e.error) {
87735
87906
  const errorObj = e.error;
87736
87907
  if ("run_id" in errorObj && typeof errorObj.run_id === "string") {
@@ -95763,7 +95934,7 @@ function isSyncCommand(value) {
95763
95934
  return false;
95764
95935
  }
95765
95936
  const candidate = value;
95766
- return candidate.type === "sync" && isRuntimeScope(candidate.runtime);
95937
+ return candidate.type === "sync" && isRuntimeScope(candidate.runtime) && (candidate.recover_approvals === undefined || typeof candidate.recover_approvals === "boolean");
95767
95938
  }
95768
95939
  function isTerminalSpawnCommand(value) {
95769
95940
  if (!value || typeof value !== "object")
@@ -96444,12 +96615,14 @@ function runDetachedListenerTask(commandName, task2) {
96444
96615
  async function replaySyncStateForRuntime(listenerRuntime, socket, scope, opts) {
96445
96616
  const syncScopedRuntime = getOrCreateScopedRuntime(listenerRuntime, scope.agent_id, scope.conversation_id);
96446
96617
  const recoverFn = opts?.recoverApprovalStateForSync ?? recoverApprovalStateForSync;
96447
- try {
96448
- await recoverFn(syncScopedRuntime, scope);
96449
- } catch (error) {
96450
- trackListenerError("listener_sync_recovery_failed", error, "listener_sync_recovery");
96451
- if (isDebugEnabled()) {
96452
- console.warn("[Listen] Sync approval recovery failed:", error);
96618
+ if (opts?.recoverApprovals ?? true) {
96619
+ try {
96620
+ await recoverFn(syncScopedRuntime, scope);
96621
+ } catch (error) {
96622
+ trackListenerError("listener_sync_recovery_failed", error, "listener_sync_recovery");
96623
+ if (isDebugEnabled()) {
96624
+ console.warn("[Listen] Sync approval recovery failed:", error);
96625
+ }
96453
96626
  }
96454
96627
  }
96455
96628
  emitStateSync(socket, listenerRuntime, scope);
@@ -98796,7 +98969,9 @@ async function connectWithRetry(runtime, opts, attempt = 0, startTime = Date.now
98796
98969
  console.log(`[Listen V2] Dropping sync: runtime mismatch or closed`);
98797
98970
  return;
98798
98971
  }
98799
- await replaySyncStateForRuntime(runtime, socket, parsed.runtime);
98972
+ await replaySyncStateForRuntime(runtime, socket, parsed.runtime, {
98973
+ recoverApprovals: parsed.recover_approvals !== false
98974
+ });
98800
98975
  return;
98801
98976
  }
98802
98977
  if (parsed.type === "input") {
@@ -147564,7 +147739,9 @@ function runWithLauncher(launcher, inputJson, timeout, signal, workingDirectory,
147564
147739
  });
147565
147740
  }
147566
147741
  var MAX_STDOUT_BYTES = 4096;
147567
- var init_statusLineRuntime = () => {};
147742
+ var init_statusLineRuntime = __esm(() => {
147743
+ init_shellLaunchers();
147744
+ });
147568
147745
 
147569
147746
  // src/cli/helpers/subagentAggregation.ts
147570
147747
  function hasInProgressTaskToolCalls(order, byId, _emittedIds) {
@@ -159073,6 +159250,7 @@ __export(exports_memoryFilesystem2, {
159073
159250
  renderMemoryFilesystemTree: () => renderMemoryFilesystemTree2,
159074
159251
  labelFromRelativePath: () => labelFromRelativePath2,
159075
159252
  isMemfsEnabledOnServer: () => isMemfsEnabledOnServer2,
159253
+ isLettaMemfsServer: () => isLettaMemfsServer2,
159076
159254
  isLettaCloud: () => isLettaCloud2,
159077
159255
  getMemorySystemDir: () => getMemorySystemDir2,
159078
159256
  getMemoryFilesystemRoot: () => getMemoryFilesystemRoot2,
@@ -159260,8 +159438,8 @@ function renderMemoryFilesystemTree2(systemLabels, detachedLabels, options = {})
159260
159438
  }
159261
159439
  async function applyMemfsFlags2(agentId, memfsFlag, noMemfsFlag, options) {
159262
159440
  const { settingsManager: settingsManager3 } = await Promise.resolve().then(() => (init_settings_manager(), exports_settings_manager));
159263
- if (memfsFlag && !await isLettaCloud2()) {
159264
- throw new Error("--memfs is only available on Letta Cloud (api.letta.com).");
159441
+ if (memfsFlag && !await isLettaMemfsServer2()) {
159442
+ throw new Error(await getMemfsSyncUnavailableMessage2());
159265
159443
  }
159266
159444
  const hasExplicitToggle = Boolean(memfsFlag || noMemfsFlag);
159267
159445
  const localMemfsEnabled = settingsManager3.isMemfsEnabled(agentId);
@@ -159332,6 +159510,24 @@ async function isLettaCloud2() {
159332
159510
  const serverUrl = getServerUrl2();
159333
159511
  return serverUrl.includes("api.letta.com") || process.env.LETTA_MEMFS_LOCAL === "1" || process.env.LETTA_API_KEY === "local-desktop";
159334
159512
  }
159513
+ function getServerHostLabel2(serverUrl) {
159514
+ const trimmed = serverUrl.trim();
159515
+ try {
159516
+ return new URL(trimmed).host || trimmed;
159517
+ } catch {
159518
+ return trimmed.replace(/^https?:\/\//, "").replace(/\/+$/, "") || trimmed;
159519
+ }
159520
+ }
159521
+ async function isLettaMemfsServer2() {
159522
+ const { getMemfsServerUrl: getMemfsServerUrl2, isDesktopLocalProxyUrl: isDesktopLocalProxyUrl2 } = await Promise.resolve().then(() => (init_client2(), exports_client));
159523
+ const memfsServerUrl = getMemfsServerUrl2();
159524
+ return memfsServerUrl.includes("api.letta.com") || isDesktopLocalProxyUrl2(memfsServerUrl) || process.env.LETTA_MEMFS_LOCAL === "1" || process.env.LETTA_API_KEY === "local-desktop";
159525
+ }
159526
+ async function getMemfsSyncUnavailableMessage2() {
159527
+ const { getMemfsServerUrl: getMemfsServerUrl2 } = await Promise.resolve().then(() => (init_client2(), exports_client));
159528
+ const memfsServerUrl = getMemfsServerUrl2();
159529
+ return `MemFS sync failed (expected api.letta.com, got ${getServerHostLabel2(memfsServerUrl)})`;
159530
+ }
159335
159531
  async function enableMemfsIfCloud2(agentId) {
159336
159532
  if (!await isLettaCloud2())
159337
159533
  return;
@@ -168653,4 +168849,4 @@ Error during initialization: ${message}`);
168653
168849
  }
168654
168850
  main();
168655
168851
 
168656
- //# debugId=22CE2EBE31776A7764756E2164756E21
168852
+ //# debugId=CB5FADB2FE73B65E64756E2164756E21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letta-ai/letta-code",
3
- "version": "0.24.3",
3
+ "version": "0.24.5",
4
4
  "description": "Letta Code is a CLI tool for interacting with stateful Letta agents from the terminal.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,11 +24,12 @@ Below are additional common issues with context and how they can be resolved:
24
24
  #### System prompt bloat
25
25
  Memories compiled into the system prompt (contained in `system/`) should take up about 10% of the total context size (usually ~15-20K tokens). This is a soft target, not a hard requirement.
26
26
 
27
- Use the following script to evaluate the token usage of the system prompt:
27
+ Use the built-in CLI to evaluate token usage of the system prompt:
28
28
  ```bash
29
- npx tsx <SKILL_DIR>/scripts/estimate_system_tokens.ts --memory-dir "$MEMORY_DIR"
29
+ letta memory tokens --format json --quiet
30
30
  ```
31
- Where `<SKILL_DIR>` is the Skill Directory shown when the skill was loaded (visible in the injection header).
31
+
32
+ The command reports `total_tokens` and per-file estimates for `system/`. It is only a measurement tool; decide whether to intervene based on the actual context and the guidance below.
32
33
 
33
34
  **Why detail is load-bearing (read this before cutting anything)**: In-context detail does more than carry information. It does at least four things, and byte-counting sweeps only see the first:
34
35
  1. **Information** — the literal facts stated
@@ -1,128 +0,0 @@
1
- #!/usr/bin/env npx tsx
2
- /**
3
- * Estimate token usage of system prompt memory files.
4
- *
5
- * Self-contained — no imports from the letta-code source tree.
6
- *
7
- * Usage:
8
- * npx tsx estimate_system_tokens.ts --memory-dir "$MEMORY_DIR"
9
- * npx tsx estimate_system_tokens.ts --memory-dir ~/.letta/agents/<id>/memory
10
- */
11
-
12
- import { existsSync, readdirSync, readFileSync } from "node:fs";
13
- import { join } from "node:path";
14
-
15
- // Codex heuristic: ~4 bytes per token (codex-rs/core/src/truncate.rs APPROX_BYTES_PER_TOKEN = 4)
16
- const BYTES_PER_TOKEN = 4;
17
-
18
- function estimateTokens(text: string): number {
19
- return Math.ceil(Buffer.byteLength(text, "utf8") / BYTES_PER_TOKEN);
20
- }
21
-
22
- type FileEstimate = {
23
- path: string;
24
- tokens: number;
25
- };
26
-
27
- type ParsedArgs = {
28
- memoryDir?: string;
29
- top: number;
30
- };
31
-
32
- function parseArgs(argv: string[]): ParsedArgs {
33
- const parsed: ParsedArgs = { top: 20 };
34
-
35
- for (let i = 0; i < argv.length; i++) {
36
- const arg = argv[i];
37
- if (arg === "--memory-dir") {
38
- parsed.memoryDir = argv[i + 1];
39
- i++;
40
- continue;
41
- }
42
- if (arg === "--top") {
43
- const raw = argv[i + 1];
44
- const value = Number.parseInt(raw ?? "", 10);
45
- if (!Number.isNaN(value) && value >= 0) {
46
- parsed.top = value;
47
- }
48
- i++;
49
- }
50
- }
51
-
52
- return parsed;
53
- }
54
-
55
- function normalizePath(value: string): string {
56
- return value.replaceAll("\\", "/");
57
- }
58
-
59
- function walkMarkdownFiles(dir: string): string[] {
60
- if (!existsSync(dir)) {
61
- return [];
62
- }
63
-
64
- const out: string[] = [];
65
- const entries = readdirSync(dir, { withFileTypes: true });
66
-
67
- for (const entry of entries) {
68
- if (entry.name.startsWith(".")) {
69
- continue;
70
- }
71
- const full = join(dir, entry.name);
72
- if (entry.isDirectory()) {
73
- out.push(...walkMarkdownFiles(full));
74
- continue;
75
- }
76
- if (entry.isFile() && entry.name.endsWith(".md")) {
77
- out.push(full);
78
- }
79
- }
80
-
81
- return out;
82
- }
83
-
84
- function formatNumber(value: number): string {
85
- return value.toLocaleString("en-US");
86
- }
87
-
88
- function main(): number {
89
- const args = parseArgs(process.argv.slice(2));
90
- const memoryDir = args.memoryDir || process.env.MEMORY_DIR;
91
-
92
- if (!memoryDir) {
93
- console.error("Missing memory dir. Pass --memory-dir or set MEMORY_DIR.");
94
- return 1;
95
- }
96
-
97
- const systemDir = join(memoryDir, "system");
98
- if (!existsSync(systemDir)) {
99
- console.error(`Missing system directory: ${systemDir}`);
100
- return 1;
101
- }
102
-
103
- const files = walkMarkdownFiles(systemDir).sort();
104
- const rows: FileEstimate[] = [];
105
-
106
- for (const filePath of files) {
107
- const text = readFileSync(filePath, "utf8");
108
- const rel = normalizePath(filePath.slice(memoryDir.length + 1));
109
- rows.push({ path: rel, tokens: estimateTokens(text) });
110
- }
111
-
112
- const estimatedTotalTokens = rows.reduce((sum, row) => sum + row.tokens, 0);
113
-
114
- console.log("Estimated total tokens");
115
- console.log(` ${formatNumber(estimatedTotalTokens)}`);
116
-
117
- console.log("\nPer-file token estimates");
118
- console.log(` ${"tokens".padStart(8)} path`);
119
-
120
- const sortedRows = [...rows].sort((a, b) => b.tokens - a.tokens);
121
- for (const row of sortedRows.slice(0, Math.max(0, args.top))) {
122
- console.log(` ${formatNumber(row.tokens).padStart(8)} ${row.path}`);
123
- }
124
-
125
- return 0;
126
- }
127
-
128
- process.exit(main());