@letta-ai/letta-code 0.32.2 → 0.32.3

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
@@ -5515,7 +5515,7 @@ var package_default;
5515
5515
  var init_package = __esm(() => {
5516
5516
  package_default = {
5517
5517
  name: "@letta-ai/letta-code",
5518
- version: "0.32.2",
5518
+ version: "0.32.3",
5519
5519
  lettaStartupLogProtocol: 1,
5520
5520
  description: "Letta Code is a CLI tool for interacting with stateful Letta agents from the terminal.",
5521
5521
  type: "module",
@@ -98016,7 +98016,7 @@ Note: \`fork\` cannot be combined with \`agent_id\` or \`conversation_id\`.
98016
98016
 
98017
98017
  ## Running on Another Computer
98018
98018
 
98019
- Pass \`computer\` to run the subagent's turn on another connected computer instead of this machine. Works with any subagent type. The call fails fast if the named device is offline, ambiguous, or too old to support routing.
98019
+ Pass \`computer\` to run the subagent's turn on another connected computer instead of this machine. Prefer a stable device ID or computer name; these select the freshest online listener for that device. Ephemeral connection IDs are still supported to pin a specific listener. Works with any subagent type. The call fails fast if the named device is offline, the name matches multiple online devices, or the listener is too old to support routing.
98020
98020
 
98021
98021
  \`computer: "cloud"\` provisions a Cloud sandbox for the subagent's conversation and runs the turn there. Sandboxes are per-conversation: this is a separate machine from wherever you are running now, even if you are already in a Cloud sandbox.
98022
98022
 
@@ -122477,7 +122477,8 @@ var init_skills5 = __esm(() => {
122477
122477
  init_skill_sources();
122478
122478
  LOCAL_AGENT_EXCLUDED_BUNDLED_SKILLS = new Set([
122479
122479
  "image-generation",
122480
- "managing-shared-memory"
122480
+ "managing-shared-memory",
122481
+ "working-across-computers"
122481
122482
  ]);
122482
122483
  PROJECT_SKILLS_DIR = join26(".agents", "skills");
122483
122484
  GLOBAL_SKILLS_DIR = join26(process.env.HOME || process.env.USERPROFILE || "~", ".letta/skills");
@@ -126332,7 +126333,7 @@ var init_Task2 = __esm(() => {
126332
126333
  },
126333
126334
  computer: {
126334
126335
  type: "string",
126335
- description: `Run the subagent on another connected computer instead of this machine. Pass a computer name/connection ID, or "cloud" to provision a Cloud sandbox for the subagent's conversation. Fails fast if the device is offline or does not support routing. Omit this field to run on the current machine (the default) — only set it when the task specifically needs to run elsewhere.`
126336
+ description: `Run the subagent on another connected computer instead of this machine. Prefer a stable device ID or computer name; ephemeral connection IDs are also supported to pin a specific listener. Pass "cloud" to provision a Cloud sandbox for the subagent's conversation. Fails fast if the device is offline or does not support routing. Omit this field to run on the current machine (the default) — only set it when the task specifically needs to run elsewhere.`
126336
126337
  }
126337
126338
  },
126338
126339
  required: ["description", "prompt", "subagent_type"],
@@ -192396,7 +192397,8 @@ function resolveSlackConcreteActivity(event) {
192396
192397
  if (event.kind !== "tool" || !isNonEmptyString5(event.toolName) || event.toolName.toLowerCase() === "messagechannel") {
192397
192398
  return null;
192398
192399
  }
192399
- for (const description of [event.toolTitle, event.toolDetails]) {
192400
+ const descriptions = event.toolBatchTitle !== undefined ? [event.toolBatchTitle] : [event.toolTitle, event.toolDetails];
192401
+ for (const description of descriptions) {
192400
192402
  if (!isNonEmptyString5(description)) {
192401
192403
  continue;
192402
192404
  }
@@ -195267,6 +195269,12 @@ ${loadingText}`;
195267
195269
  signatureByConversation.set(stateKey, signature);
195268
195270
  const previous = writePromiseByConversation.get(stateKey) ?? Promise.resolve();
195269
195271
  const operation = previous.then(async () => {
195272
+ if (footerText && !stateByConversation.get(stateKey)?.isThinkingActive) {
195273
+ if (signatureByConversation.get(stateKey) === signature) {
195274
+ signatureByConversation.delete(stateKey);
195275
+ }
195276
+ return false;
195277
+ }
195270
195278
  try {
195271
195279
  await setStatus.call(slackClient.assistant?.threads, {
195272
195280
  channel_id: source.chatId,
@@ -195274,6 +195282,16 @@ ${loadingText}`;
195274
195282
  status: footerText,
195275
195283
  ...footerText ? { loading_messages: [loadingText] } : {}
195276
195284
  });
195285
+ const state = stateByConversation.get(stateKey);
195286
+ if (footerText && state && !state.isThinkingActive) {
195287
+ await setStatus.call(slackClient.assistant?.threads, {
195288
+ channel_id: source.chatId,
195289
+ thread_ts: threadTs,
195290
+ status: ""
195291
+ });
195292
+ clearedStaleReplyKeys.add(replyKey);
195293
+ return true;
195294
+ }
195277
195295
  if (footerText)
195278
195296
  clearedStaleReplyKeys.delete(replyKey);
195279
195297
  else
@@ -195372,7 +195390,40 @@ ${loadingText}`;
195372
195390
  await writeStatus(source, "", "", { force: true });
195373
195391
  signatureByConversation.delete(key2);
195374
195392
  }
195393
+ async function refresh(source) {
195394
+ const key2 = getConversationKey(source);
195395
+ const state = key2 ? stateByConversation.get(key2) : undefined;
195396
+ if (!key2 || !state?.isThinkingActive)
195397
+ return false;
195398
+ await writeStatus(source, state.typingFooterText, state.thinkingText, {
195399
+ force: true
195400
+ });
195401
+ if (stateByConversation.get(key2) === state && state.isThinkingActive) {
195402
+ scheduleKeepalive(key2);
195403
+ }
195404
+ return true;
195405
+ }
195406
+ async function handleLifecycle(event) {
195407
+ if (event.type === "queued") {
195408
+ if (await refresh(event.source))
195409
+ return;
195410
+ if (event.source.showStartupStatus) {
195411
+ await activate(event.source, SLACK_ASSISTANT_STARTUP_STATUS, SLACK_ASSISTANT_STARTUP_STATUS);
195412
+ }
195413
+ return;
195414
+ }
195415
+ for (const source of getUniqueSources(event.sources)) {
195416
+ if (event.type === "processing") {
195417
+ await clearStale(source);
195418
+ } else if (event.stopReason !== "requires_approval") {
195419
+ const remaining = event.remainingSources?.some((other) => other.accountId === source.accountId && getConversationKey(other) === getConversationKey(source) && getLifecycleReplyKey(other) === getLifecycleReplyKey(source));
195420
+ if (!remaining)
195421
+ await deactivate(source);
195422
+ }
195423
+ }
195424
+ }
195375
195425
  return {
195426
+ handleLifecycle,
195376
195427
  getUniqueSources,
195377
195428
  getLifecycleErrorReplyKey,
195378
195429
  activate,
@@ -195412,7 +195463,9 @@ ${loadingText}`;
195412
195463
  };
195413
195464
  }
195414
195465
  var SLACK_ASSISTANT_STATUS_KEEPALIVE_MS = 90000;
195415
- var init_status_controller = () => {};
195466
+ var init_status_controller = __esm(() => {
195467
+ init_progress();
195468
+ });
195416
195469
 
195417
195470
  // src/channels/slack/thread-context.ts
195418
195471
  function shouldHydrateCurrentSlackMessageAttachments(msg) {
@@ -195695,19 +195748,19 @@ function createSlackAdapter(config) {
195695
195748
  if (!running)
195696
195749
  return;
195697
195750
  if (event.type === "queued") {
195698
- if (isSlackFlatChannelThreadOpener(event.source) && isNonEmptyString4(event.source.messageId) && !agentThreadTracker.has(event.source.chatId, event.source.messageId)) {
195699
- await status.activate(event.source, SLACK_ASSISTANT_STARTUP_STATUS, SLACK_ASSISTANT_STARTUP_STATUS);
195700
- }
195751
+ const showStartupStatus = event.source.showStartupStatus || isSlackFlatChannelThreadOpener(event.source) && isNonEmptyString4(event.source.messageId) && !agentThreadTracker.has(event.source.chatId, event.source.messageId);
195752
+ await status.handleLifecycle({
195753
+ ...event,
195754
+ source: { ...event.source, showStartupStatus }
195755
+ });
195701
195756
  return;
195702
195757
  }
195703
195758
  const sources = status.getUniqueSources(event.sources);
195704
- if (event.type === "processing") {
195705
- await Promise.all(sources.map(status.clearStale));
195759
+ await status.handleLifecycle(event);
195760
+ if (event.type === "processing")
195706
195761
  return;
195707
- }
195708
195762
  if (event.stopReason === "requires_approval")
195709
195763
  return;
195710
- await Promise.all(sources.map(status.deactivate));
195711
195764
  if (!shouldPostSlackTerminalError(event.stopReason))
195712
195765
  return;
195713
195766
  const errorText = event.error?.trim() ?? "";
@@ -217299,7 +217352,7 @@ function shouldProcessInboundMessageDirectly(runtime, parsed) {
217299
217352
  });
217300
217353
  return getListenerBlockedReason(runtime.turnLifecycle.snapshot(), activeScope ? getPendingControlRequestCount(runtime.listener, activeScope) : 0) === null;
217301
217354
  }
217302
- function consumeQueuedTurn(runtime) {
217355
+ function consumeQueuedTurn(runtime, continuation) {
217303
217356
  const queuedItems = runtime.queueRuntime.peekReady();
217304
217357
  const firstQueuedItem = queuedItems[0];
217305
217358
  if (!firstQueuedItem || !isCoalescable(firstQueuedItem.kind)) {
@@ -217315,6 +217368,9 @@ function consumeQueuedTurn(runtime) {
217315
217368
  let batchImageFailureMode = null;
217316
217369
  const isNoCoalesce = (candidate) => candidate.kind === "message" && candidate.noCoalesce === true;
217317
217370
  for (const item of queuedItems) {
217371
+ if (continuation && item.actingUserId !== continuation.actingUserId) {
217372
+ break;
217373
+ }
217318
217374
  if (!isCoalescable(item.kind) || !hasSameQueueScope(firstQueuedItem, item)) {
217319
217375
  break;
217320
217376
  }
@@ -218202,12 +218258,12 @@ async function resolveDesktopEnvironmentConnectionId(list = listEnvironments) {
218202
218258
  }
218203
218259
  return { connectionId: environment2.connectionId, environment: environment2 };
218204
218260
  }
218205
- async function resolveEnvironmentConnectionId(selector) {
218261
+ async function resolveEnvironmentConnectionId(selector, list = listEnvironments) {
218206
218262
  const trimmed = selector.trim();
218207
218263
  if (!trimmed) {
218208
218264
  throw new Error("Computer selector must not be empty");
218209
218265
  }
218210
- const response = await listEnvironments({ limit: 100 });
218266
+ const response = await list({ limit: 100 });
218211
218267
  const matches3 = response.connections.filter((environment3) => {
218212
218268
  return environment3.connectionId === trimmed || environment3.id === trimmed || environment3.deviceId === trimmed || environment3.connectionName === trimmed;
218213
218269
  });
@@ -218218,9 +218274,10 @@ async function resolveEnvironmentConnectionId(selector) {
218218
218274
  if (onlineMatches.length === 0) {
218219
218275
  throw new Error(`Computer "${trimmed}" is offline. Matched: ${matches3.map(describeEnvironment).join(", ")}`);
218220
218276
  }
218221
- if (onlineMatches.length > 1) {
218277
+ if (new Set(onlineMatches.map((environment3) => environment3.deviceId)).size > 1) {
218222
218278
  throw new Error(`Computer "${trimmed}" is ambiguous. Matched: ${onlineMatches.map(describeEnvironment).join(", ")}`);
218223
218279
  }
218280
+ onlineMatches.sort((a2, b3) => Math.max(b3.lastHeartbeat ?? 0, b3.lastSeenAt) - Math.max(a2.lastHeartbeat ?? 0, a2.lastSeenAt));
218224
218281
  const environment2 = onlineMatches[0];
218225
218282
  if (!environment2) {
218226
218283
  throw new Error(`Computer "${trimmed}" is offline`);
@@ -219150,7 +219207,9 @@ List options:
219150
219207
  Notes:
219151
219208
  - Output is JSON only.
219152
219209
  - Uses CLI auth; override with LETTA_API_KEY/LETTA_BASE_URL if needed.
219153
- - Use letta computers current to get this computer's connectionId.
219210
+ - Use letta computers current to get this computer's stable deviceId.
219211
+ - Prefer deviceId or connectionName for --computer and Agent(computer=...).
219212
+ connectionId is ephemeral; use it only to pin a specific listener.
219154
219213
  - Use --computer cloud to route through the target agent's cloud sandbox.
219155
219214
  - Use --computer <name|device-id|connection-id> with headless messaging
219156
219215
  to route a message through a specific registered computer.
@@ -405353,13 +405412,14 @@ async function handleApprovalStop(params) {
405353
405412
  }
405354
405413
  ]);
405355
405414
  let continuationBatchId = dequeuedBatchId;
405356
- let continuationActingUserId;
405357
- const consumedQueuedTurn = consumeQueuedTurn(runtime);
405415
+ const sendOptions = buildSendOptions() ?? {};
405416
+ const consumedQueuedTurn = consumeQueuedTurn(runtime, {
405417
+ actingUserId: sendOptions.actingUserId
405418
+ });
405358
405419
  if (consumedQueuedTurn) {
405359
405420
  const { dequeuedBatch, queuedTurn } = consumedQueuedTurn;
405360
405421
  turnCorrelation?.appendDequeuedBatch(dequeuedBatch.batchId);
405361
405422
  continuationBatchId = dequeuedBatch.batchId;
405362
- continuationActingUserId = queuedTurn.actingUserId;
405363
405423
  nextTurnInput = appendQueuedTurnToInput(nextTurnInput, queuedTurn);
405364
405424
  emitDequeuedUserMessage(socket, runtime, queuedTurn, dequeuedBatch);
405365
405425
  }
@@ -405374,11 +405434,9 @@ async function handleApprovalStop(params) {
405374
405434
  });
405375
405435
  let sendResult;
405376
405436
  try {
405377
- const sendOptions = buildSendOptions() ?? {};
405378
405437
  const imageFailureModesByMessageOtid = mergeImageFailureModesByMessageOtid(sendOptions.imageFailureModesByMessageOtid, nextTurnInput.imageFailureModesByMessageOtid);
405379
405438
  sendResult = await sendApprovalContinuation(conversationId, nextInputWithSkillContent, {
405380
405439
  ...sendOptions,
405381
- ...continuationActingUserId ? { actingUserId: continuationActingUserId } : {},
405382
405440
  ...imageFailureModesByMessageOtid ? { imageFailureModesByMessageOtid } : {},
405383
405441
  ...continuationWasFullyAutoHandled ? { allowResponseStateReuse: true } : {}
405384
405442
  }, socket, runtime, turnLease);
@@ -440191,7 +440249,7 @@ var init_mcp_client = __esm(() => {
440191
440249
  init_streamableHttp();
440192
440250
  DEFAULT_CLIENT_INFO = {
440193
440251
  name: "letta-code",
440194
- version: "0.32.2"
440252
+ version: "0.32.3"
440195
440253
  };
440196
440254
  });
440197
440255
 
@@ -444080,7 +444138,7 @@ async function request(path50, init, deps) {
444080
444138
  async function ensureConversationSandbox(agentId, conversationId, deps = defaultDeps) {
444081
444139
  const response = await request(`/v1/agents/${encodeURIComponent(agentId)}/sandboxes`, {
444082
444140
  method: "POST",
444083
- body: JSON.stringify({ conversationId })
444141
+ body: JSON.stringify(conversationId === "default" ? {} : { conversationId })
444084
444142
  }, deps);
444085
444143
  return await response.json();
444086
444144
  }
@@ -444118,8 +444176,17 @@ Usage:
444118
444176
  letta sandbox upload <local-path>
444119
444177
  letta sandbox download <sandbox-path> [--to <local-path>]
444120
444178
 
444179
+ Target another conversation (upload or download):
444180
+ letta sandbox upload <local-path> --conversation <conv-id>
444181
+ letta sandbox upload <local-path> --agent <agent-id>
444182
+
444121
444183
  Notes:
444122
- - Requires an active conversation for a Letta Cloud agent.
444184
+ - Without target flags, uses the active Letta Cloud conversation.
444185
+ - --conversation resolves the owning agent; --agent, if given, must match.
444186
+ - --agent alone selects that agent's main/default sandbox.
444187
+ - --conversation default is also supported and requires explicit --agent.
444188
+ - Target flags override shell/session context without changing it.
444189
+ - Run upload on the computer containing the local file, including a remote subagent.
444123
444190
  - Uploads are stored under /root/downloads in the conversation sandbox.
444124
444191
  - Downloads are limited to files under /root/downloads.
444125
444192
  - Output is JSON only.
@@ -444156,6 +444223,35 @@ function resolveSandboxSession(env5, fallback) {
444156
444223
  }
444157
444224
  return session2;
444158
444225
  }
444226
+ async function resolveSandboxTarget(target2, getCurrentSession, retrieveConversation) {
444227
+ if (target2.agent === undefined && target2.conversation === undefined) {
444228
+ return getCurrentSession();
444229
+ }
444230
+ const agentId = target2.agent?.trim();
444231
+ const conversationId = target2.conversation === undefined && agentId ? "default" : target2.conversation?.trim();
444232
+ if (target2.agent !== undefined && !agentId) {
444233
+ throw new Error("--agent must not be empty");
444234
+ }
444235
+ if (!conversationId || conversationId === "new") {
444236
+ throw new Error("Specify --conversation <conv-id> or --conversation default");
444237
+ }
444238
+ if (agentId && isLocalAgentId(agentId)) {
444239
+ throw new Error("Sandbox file transfer requires a Letta Cloud agent");
444240
+ }
444241
+ if (conversationId === "default") {
444242
+ if (!agentId)
444243
+ throw new Error("--conversation default requires --agent");
444244
+ return { agentId, conversationId };
444245
+ }
444246
+ const conversation = await retrieveConversation(conversationId);
444247
+ if (!conversation.agent_id || isLocalAgentId(conversation.agent_id)) {
444248
+ throw new Error("The target conversation must belong to a Letta Cloud agent");
444249
+ }
444250
+ if (agentId && agentId !== conversation.agent_id) {
444251
+ throw new Error(`Conversation ${conversationId} does not belong to ${agentId}`);
444252
+ }
444253
+ return { agentId: conversation.agent_id, conversationId };
444254
+ }
444159
444255
  async function initializeSandboxSettings() {
444160
444256
  await settingsManager.initialize();
444161
444257
  await settingsManager.loadLocalProjectSettings();
@@ -444184,24 +444280,37 @@ async function runSandboxSubcommand(argv, deps = {}) {
444184
444280
  if (!await (deps.isCloud ?? isLettaCloud)()) {
444185
444281
  throw new Error("Sandbox file transfer is only available on Letta Cloud");
444186
444282
  }
444187
- const session2 = resolveSandboxSession(process.env, (deps.getLastSession ?? (() => settingsManager.getEffectiveLastSession()))());
444188
- const ensureSandbox = deps.ensureSandbox ?? ensureConversationSandbox;
444283
+ const session2 = await resolveSandboxTarget(parsed.values, () => resolveSandboxSession(process.env, (deps.getLastSession ?? (() => settingsManager.getEffectiveLastSession()))()), deps.retrieveConversation ?? (async (id2) => (await getClient()).conversations.retrieve(id2)));
444284
+ const explicitTarget = parsed.values.conversation !== undefined || parsed.values.agent !== undefined;
444285
+ const ensureSandbox = async () => {
444286
+ const sandbox2 = await (deps.ensureSandbox ?? ensureConversationSandbox)(session2.agentId, session2.conversationId);
444287
+ if (explicitTarget && (sandbox2.conversationId ?? "default") !== session2.conversationId) {
444288
+ throw new Error("The server returned a sandbox for a different conversation; no files transferred");
444289
+ }
444290
+ return sandbox2;
444291
+ };
444292
+ const targetOutput = explicitTarget ? session2 : {};
444189
444293
  if (action3 === "upload") {
444190
444294
  const localPath2 = resolve37(path50);
444191
444295
  const fileStat = await (deps.statLocalPath ?? stat17)(localPath2);
444192
444296
  if (!fileStat.isFile())
444193
444297
  throw new Error(`${localPath2} is not a file`);
444194
444298
  const data2 = await (deps.readLocalFile ?? readFile25)(localPath2);
444195
- const sandbox2 = await ensureSandbox(session2.agentId, session2.conversationId);
444299
+ const sandbox2 = await ensureSandbox();
444196
444300
  const result2 = await (deps.uploadFile ?? uploadFileToSandbox)(sandbox2.sandboxId, { blob: new Blob([data2]), name: basename26(localPath2) });
444197
- console.log(JSON.stringify(result2, null, 2));
444301
+ console.log(JSON.stringify({ ...result2, ...targetOutput }, null, 2));
444198
444302
  return 0;
444199
444303
  }
444200
- const sandbox = await ensureSandbox(session2.agentId, session2.conversationId);
444304
+ const sandbox = await ensureSandbox();
444201
444305
  const data = await (deps.downloadFile ?? downloadFileFromSandbox)(sandbox.sandboxId, path50);
444202
444306
  const localPath = resolve37(parsed.values.to ?? basename26(path50));
444203
444307
  await (deps.writeLocalFile ?? writeFile17)(localPath, data);
444204
- console.log(JSON.stringify({ path: localPath, sandboxPath: path50, size: data.byteLength }, null, 2));
444308
+ console.log(JSON.stringify({
444309
+ path: localPath,
444310
+ sandboxPath: path50,
444311
+ size: data.byteLength,
444312
+ ...targetOutput
444313
+ }, null, 2));
444205
444314
  return 0;
444206
444315
  } catch (error5) {
444207
444316
  console.error(`Error: ${error5 instanceof Error ? error5.message : error5}`);
@@ -444211,11 +444320,14 @@ async function runSandboxSubcommand(argv, deps = {}) {
444211
444320
  var SANDBOX_OPTIONS;
444212
444321
  var init_sandbox2 = __esm(() => {
444213
444322
  init_memory_filesystem2();
444323
+ init_client4();
444214
444324
  init_sandbox_files();
444215
444325
  init_settings_manager();
444216
444326
  SANDBOX_OPTIONS = {
444217
444327
  help: { type: "boolean", short: "h" },
444218
- to: { type: "string" }
444328
+ to: { type: "string" },
444329
+ conversation: { type: "string" },
444330
+ agent: { type: "string" }
444219
444331
  };
444220
444332
  });
444221
444333
 
@@ -450467,6 +450579,36 @@ var init_channel_rich_draft_streamer = __esm(() => {
450467
450579
  ]);
450468
450580
  });
450469
450581
 
450582
+ // src/channels/gateway-sources.ts
450583
+ function sourceRouteKey(source) {
450584
+ return [
450585
+ source.channel,
450586
+ source.accountId ?? "",
450587
+ source.chatId,
450588
+ source.threadId ?? ""
450589
+ ].join(":");
450590
+ }
450591
+ function sourceLifecycleKey(source) {
450592
+ return [
450593
+ sourceRouteKey(source),
450594
+ source.messageId ?? "",
450595
+ source.agentId,
450596
+ source.conversationId
450597
+ ].join(":");
450598
+ }
450599
+ function uniqueSourcesBy(sources, getKey) {
450600
+ const byKey = new Map;
450601
+ for (const source of sources)
450602
+ byKey.set(getKey(source), source);
450603
+ return [...byKey.values()];
450604
+ }
450605
+ function uniqueRoutedSources(sources) {
450606
+ return uniqueSourcesBy(sources, sourceRouteKey);
450607
+ }
450608
+ function uniqueLifecycleSources(sources) {
450609
+ return uniqueSourcesBy(sources, sourceLifecycleKey);
450610
+ }
450611
+
450470
450612
  // src/channels/progress-builder.ts
450471
450613
  function getMessageType(delta2) {
450472
450614
  return firstNonEmptyString2(delta2.message_type, delta2.messageType) ?? null;
@@ -450519,6 +450661,34 @@ function toolNameForMessage(summary) {
450519
450661
  function createChannelTurnProgressBuilder(options = {}) {
450520
450662
  const argumentsByToolCallId = new Map;
450521
450663
  const namesByToolCallId = new Map;
450664
+ const batchesByStep = new Map;
450665
+ const batchesByToolCallId = new Map;
450666
+ function addToolBatchTitles(delta2, updates) {
450667
+ const record5 = asRecord4(delta2);
450668
+ if (!record5)
450669
+ return updates;
450670
+ const messageType = getMessageType(record5);
450671
+ const isToolRequest = messageType === "tool_call_message" || messageType === "approval_request_message";
450672
+ const stepKey = isToolRequest ? firstNonEmptyString2(record5.step_id, record5.id) : undefined;
450673
+ let requestBatch = stepKey ? batchesByStep.get(stepKey) : undefined;
450674
+ if (stepKey && !requestBatch) {
450675
+ requestBatch = { title: null };
450676
+ batchesByStep.set(stepKey, requestBatch);
450677
+ }
450678
+ return updates.map((update3) => {
450679
+ if (update3.kind !== "tool")
450680
+ return update3;
450681
+ const batch = (update3.toolCallId ? batchesByToolCallId.get(update3.toolCallId) : undefined) ?? requestBatch;
450682
+ if (!batch)
450683
+ return update3;
450684
+ if (update3.toolCallId)
450685
+ batchesByToolCallId.set(update3.toolCallId, batch);
450686
+ if (batch.title === null && update3.state === "started") {
450687
+ batch.title = firstNonEmptyString2(update3.toolTitle, update3.toolDetails) ?? null;
450688
+ }
450689
+ return { ...update3, toolBatchTitle: batch.title };
450690
+ });
450691
+ }
450522
450692
  function extractToolCallSummary(value) {
450523
450693
  const record5 = asRecord4(value);
450524
450694
  if (!record5) {
@@ -450835,7 +451005,9 @@ function createChannelTurnProgressBuilder(options = {}) {
450835
451005
  return [];
450836
451006
  }
450837
451007
  }
450838
- return { buildUpdates };
451008
+ return {
451009
+ buildUpdates: (delta2) => addToolBatchTitles(delta2, buildUpdates(delta2))
451010
+ };
450839
451011
  }
450840
451012
  var init_progress_builder = __esm(() => {
450841
451013
  init_progress_formatting();
@@ -450848,34 +451020,6 @@ function runtimeKey(runtime) {
450848
451020
  function hasAgentRuntime(value) {
450849
451021
  return !!value.runtime?.agent_id;
450850
451022
  }
450851
- function sourceRouteKey(source) {
450852
- return [
450853
- source.channel,
450854
- source.accountId ?? "",
450855
- source.chatId,
450856
- source.threadId ?? ""
450857
- ].join(":");
450858
- }
450859
- function sourceLifecycleKey(source) {
450860
- return [
450861
- sourceRouteKey(source),
450862
- source.messageId ?? "",
450863
- source.agentId,
450864
- source.conversationId
450865
- ].join(":");
450866
- }
450867
- function uniqueSourcesBy(sources, getKey) {
450868
- const byKey = new Map;
450869
- for (const source of sources)
450870
- byKey.set(getKey(source), source);
450871
- return [...byKey.values()];
450872
- }
450873
- function uniqueRoutedSources(sources) {
450874
- return uniqueSourcesBy(sources, sourceRouteKey);
450875
- }
450876
- function uniqueLifecycleSources(sources) {
450877
- return uniqueSourcesBy(sources, sourceLifecycleKey);
450878
- }
450879
451023
  function channelTagsForSources(sources) {
450880
451024
  return [...new Set(sources.map((source) => `channel:${source.channel}`))];
450881
451025
  }
@@ -450934,6 +451078,7 @@ class ChannelGateway {
450934
451078
  state.acceptedClientMessageIds.add(delivery.clientMessageId);
450935
451079
  return true;
450936
451080
  }
451081
+ const workAtSubmit = state.active || state.pendingSourcesByClientMessageId.size > 0;
450937
451082
  state.pendingSourcesByClientMessageId.set(delivery.clientMessageId, {
450938
451083
  sources: uniqueLifecycleSources(delivery.sources),
450939
451084
  disposition: "submitting"
@@ -450962,6 +451107,8 @@ class ChannelGateway {
450962
451107
  });
450963
451108
  if (!response.accepted) {
450964
451109
  state.pendingSourcesByClientMessageId.delete(delivery.clientMessageId);
451110
+ if (workAtSubmit && !state.active)
451111
+ this.finishRejectedDelivery(state, delivery);
450965
451112
  return false;
450966
451113
  }
450967
451114
  this.rememberAcceptedClientMessageId(state, delivery.clientMessageId);
@@ -450981,9 +451128,28 @@ class ChannelGateway {
450981
451128
  return true;
450982
451129
  } catch (error5) {
450983
451130
  state.pendingSourcesByClientMessageId.delete(delivery.clientMessageId);
451131
+ if (workAtSubmit && !state.active)
451132
+ this.finishRejectedDelivery(state, delivery);
450984
451133
  throw error5;
450985
451134
  }
450986
451135
  }
451136
+ remainingSources(state) {
451137
+ return uniqueLifecycleSources([
451138
+ ...state.active?.lifecycleSources ?? [],
451139
+ ...Array.from(state.pendingSourcesByClientMessageId.values()).flatMap((pending) => pending.sources)
451140
+ ]);
451141
+ }
451142
+ finishRejectedDelivery(state, delivery) {
451143
+ const remainingSources = this.remainingSources(state);
451144
+ this.enqueueHook(state, () => this.hooks.onLifecycle({
451145
+ type: "finished",
451146
+ batchId: `channel-${delivery.clientMessageId}`,
451147
+ sources: delivery.sources,
451148
+ stopReason: "cancelled",
451149
+ outcome: "cancelled",
451150
+ remainingSources
451151
+ }));
451152
+ }
450987
451153
  adoptQueuedDelivery(delivery) {
450988
451154
  const state = this.getState(delivery.runtime);
450989
451155
  const sources = uniqueLifecycleSources(delivery.sources);
@@ -451366,12 +451532,14 @@ class ChannelGateway {
451366
451532
  this.activateSources(state, firstDequeued.clientMessageId, dequeued.flatMap((entry) => entry.sources));
451367
451533
  }
451368
451534
  for (const entry of cancelled) {
451535
+ const remainingSources = this.remainingSources(state);
451369
451536
  this.enqueueHook(state, () => this.hooks.onLifecycle({
451370
451537
  type: "finished",
451371
451538
  batchId: `channel-${entry.clientMessageId}`,
451372
451539
  sources: entry.sources,
451373
451540
  outcome: "cancelled",
451374
- stopReason: "cancelled"
451541
+ stopReason: "cancelled",
451542
+ ...remainingSources.length ? { remainingSources } : {}
451375
451543
  }));
451376
451544
  }
451377
451545
  }
@@ -451448,12 +451616,14 @@ class ChannelGateway {
451448
451616
  return;
451449
451617
  state.active = null;
451450
451618
  active.richDraft?.dispose();
451619
+ const remainingSources = lifecycleOutcome(terminal.stopReason) === "completed" ? this.remainingSources(state) : [];
451451
451620
  this.enqueueHook(state, () => this.hooks.onLifecycle({
451452
451621
  type: "finished",
451453
451622
  batchId: active.batchId,
451454
451623
  sources: active.lifecycleSources,
451455
451624
  outcome: lifecycleOutcome(terminal.stopReason),
451456
451625
  stopReason: terminal.stopReason,
451626
+ ...remainingSources.length ? { remainingSources } : {},
451457
451627
  ...terminal.runId ?? active.runId ? { runId: terminal.runId ?? active.runId } : {},
451458
451628
  ...terminal.error ? { error: terminal.error } : {}
451459
451629
  }));
@@ -520812,4 +520982,4 @@ function registerBunOAuthFlows() {
520812
520982
  registerBunOAuthFlows();
520813
520983
  await init_src5().then(() => exports_src2);
520814
520984
 
520815
- //# debugId=182BBD0B116FF61964756E2164756E21
520985
+ //# debugId=95B2BCB7592A39B564756E2164756E21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letta-ai/letta-code",
3
- "version": "0.32.2",
3
+ "version": "0.32.3",
4
4
  "lettaStartupLogProtocol": 1,
5
5
  "description": "Letta Code is a CLI tool for interacting with stateful Letta agents from the terminal.",
6
6
  "type": "module",
@@ -37,7 +37,7 @@ else:
37
37
  with open("robot-mascot.png", "wb") as f:
38
38
  f.write(data)
39
39
 
40
- print("saved robot-mascot.png; credits:", response["billing"]["credits_charged"])
40
+ print("saved robot-mascot.png")
41
41
  PY
42
42
  ```
43
43
 
@@ -63,7 +63,9 @@ The Letta Code UI renders local file paths in markdown image tags, so the image
63
63
  appears inline. **Always display generated images this way** — don't just report
64
64
  the path, and never paste the raw base64 / a `data:` URI. The markdown path must
65
65
  match where you saved the file. For `n > 1`, save each image to its own file and
66
- embed each on its own line. Also tell the user the `credits_charged`.
66
+ embed each on its own line. Keep credit amounts and billing metadata out of
67
+ user-facing replies and captions unless the user asks about cost. When asked,
68
+ read `billing.credits_charged` from the saved response.
67
69
 
68
70
  ## Request body
69
71
 
@@ -117,8 +119,7 @@ DATA_URL="data:image/png;base64,$(base64 < input.png | tr -d '\n')"
117
119
 
118
120
  ## Notes
119
121
 
120
- - **Billing**: every success charges credits; don't loop needlessly, and report
121
- `credits_charged`.
122
+ - **Billing**: every success charges credits; don't loop needlessly.
122
123
  - **Errors**: `402` = insufficient credits (`credits_required` in body); `400`/`500`
123
124
  return `{ "message": "..." }` — surface it to the user.
124
125
  - Only `flux`, `gemini`, and `openai` are supported here.