@letta-ai/letta-code 0.30.8 → 0.30.10

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.
Files changed (34) hide show
  1. package/dist/gateway-core.js +8 -2
  2. package/dist/gateway-core.js.map +3 -3
  3. package/dist/mcp-client.js +2 -2
  4. package/dist/mcp-client.js.map +1 -1
  5. package/dist/types/agent/message.d.ts.map +1 -1
  6. package/dist/types/backend/local/local-store.d.ts.map +1 -1
  7. package/dist/types/channels/gateway-core.d.ts.map +1 -1
  8. package/dist/types/cli/helpers/accumulator.d.ts +1 -0
  9. package/dist/types/cli/helpers/accumulator.d.ts.map +1 -1
  10. package/dist/types/cli/helpers/stream-terminal-eof-guard.d.ts +14 -0
  11. package/dist/types/cli/helpers/stream-terminal-eof-guard.d.ts.map +1 -0
  12. package/dist/types/cli/helpers/stream.d.ts +1 -0
  13. package/dist/types/cli/helpers/stream.d.ts.map +1 -1
  14. package/dist/types/mods/mod-engine.d.ts +3 -7
  15. package/dist/types/mods/mod-engine.d.ts.map +1 -1
  16. package/dist/types/telemetry/error-reporting.d.ts +7 -0
  17. package/dist/types/telemetry/error-reporting.d.ts.map +1 -1
  18. package/dist/types/telemetry/index.d.ts +10 -0
  19. package/dist/types/telemetry/index.d.ts.map +1 -1
  20. package/dist/types/tools/impl/shell-runner.d.ts.map +1 -1
  21. package/dist/types/tools/manager.d.ts.map +1 -1
  22. package/dist/types/tools/toolset.d.ts +1 -0
  23. package/dist/types/tools/toolset.d.ts.map +1 -1
  24. package/dist/types/types/protocol_v2.d.ts +8 -9
  25. package/dist/types/types/protocol_v2.d.ts.map +1 -1
  26. package/dist/types/utils/secrets.d.ts.map +1 -1
  27. package/dist/types/websocket/listener/types.d.ts +1 -0
  28. package/dist/types/websocket/listener/types.d.ts.map +1 -1
  29. package/letta.js +367 -97
  30. package/package.json +1 -1
  31. package/scripts/dev.cjs +1 -2
  32. package/scripts/source-file-size-baseline.json +10 -9
  33. package/scripts/test-home-preload.ts +66 -0
  34. package/skills/creating-mods/references/ui.md +11 -1
package/letta.js CHANGED
@@ -3306,6 +3306,16 @@ function trackBoundaryError(options) {
3306
3306
  recentChunks: options.recentChunks
3307
3307
  });
3308
3308
  }
3309
+ function trackEndTurnNoAssistant(params) {
3310
+ telemetry.trackError("end_turn_no_assistant", `end_turn fell back to ${params.fallbackKind}`, "headless_result_extraction", {
3311
+ modelId: params.modelHandle,
3312
+ runId: params.runId,
3313
+ isSubagent: params.isSubagent,
3314
+ subagentType: params.subagentType,
3315
+ modelHandle: params.modelHandle,
3316
+ fallbackKind: params.fallbackKind
3317
+ });
3318
+ }
3309
3319
  var init_error_reporting = __esm(() => {
3310
3320
  init_telemetry();
3311
3321
  });
@@ -4084,6 +4094,10 @@ async function writeJsonFile(path2, data, options) {
4084
4094
  var init_fs = () => {};
4085
4095
 
4086
4096
  // src/utils/secrets.ts
4097
+ function scopeServiceName(name) {
4098
+ const testPrefix = process.env.LETTA_TEST_SECRETS_SERVICE_PREFIX?.trim();
4099
+ return testPrefix ? `${testPrefix}:${name}` : name;
4100
+ }
4087
4101
  function getErrorMessage(error) {
4088
4102
  return error instanceof Error ? error.message : String(error);
4089
4103
  }
@@ -4245,7 +4259,7 @@ async function isKeychainAvailable() {
4245
4259
  return false;
4246
4260
  }
4247
4261
  }
4248
- var secrets, secretsAvailable = false, SERVICE_NAME = "letta-code", API_KEY_NAME = "letta-api-key", REFRESH_TOKEN_NAME = "letta-refresh-token", warnedSecretReadFailures, secretGetOverrideForTests = null;
4262
+ var secrets, secretsAvailable = false, SERVICE_NAME, API_KEY_NAME = "letta-api-key", REFRESH_TOKEN_NAME = "letta-refresh-token", warnedSecretReadFailures, secretGetOverrideForTests = null;
4249
4263
  var init_secrets = __esm(() => {
4250
4264
  init_debug();
4251
4265
  try {
@@ -4254,6 +4268,7 @@ var init_secrets = __esm(() => {
4254
4268
  } catch {
4255
4269
  secretsAvailable = false;
4256
4270
  }
4271
+ SERVICE_NAME = scopeServiceName("letta-code");
4257
4272
  warnedSecretReadFailures = new Set;
4258
4273
  });
4259
4274
 
@@ -5461,7 +5476,7 @@ var package_default;
5461
5476
  var init_package = __esm(() => {
5462
5477
  package_default = {
5463
5478
  name: "@letta-ai/letta-code",
5464
- version: "0.30.8",
5479
+ version: "0.30.10",
5465
5480
  description: "Letta Code is a CLI tool for interacting with stateful Letta agents from the terminal.",
5466
5481
  type: "module",
5467
5482
  packageManager: "bun@1.3.0",
@@ -6441,7 +6456,13 @@ class TelemetryManager {
6441
6456
  model_id: options?.modelId,
6442
6457
  run_id: options?.runId,
6443
6458
  recent_chunks: options?.recentChunks,
6444
- debug_log_tail: debugLogFile.getTail()
6459
+ debug_log_tail: debugLogFile.getTail(),
6460
+ is_subagent: options?.isSubagent,
6461
+ subagent_type: options?.subagentType,
6462
+ model_handle: options?.modelHandle,
6463
+ fallback_kind: options?.fallbackKind,
6464
+ platform: process.platform,
6465
+ version: getVersion()
6445
6466
  };
6446
6467
  this.track("error", data);
6447
6468
  }
@@ -154249,14 +154270,29 @@ function spawnWithLauncher(launcher, options3) {
154249
154270
  stdio: ["ignore", "pipe", "pipe"],
154250
154271
  detached: process.platform !== "win32"
154251
154272
  });
154252
- const killProcessGroup = (signal) => {
154273
+ const killProcessTree = (signal) => {
154253
154274
  if (childProcess.pid) {
154275
+ if (process.platform === "win32") {
154276
+ const taskkill = spawn2("taskkill.exe", ["/pid", String(childProcess.pid), "/t", "/f"], {
154277
+ stdio: "ignore",
154278
+ windowsHide: true
154279
+ });
154280
+ taskkill.once("error", () => {
154281
+ try {
154282
+ childProcess.kill("SIGKILL");
154283
+ } catch {}
154284
+ });
154285
+ taskkill.once("close", (code2) => {
154286
+ if (code2 === 0)
154287
+ return;
154288
+ try {
154289
+ childProcess.kill("SIGKILL");
154290
+ } catch {}
154291
+ });
154292
+ return;
154293
+ }
154254
154294
  try {
154255
- if (process.platform !== "win32") {
154256
- process.kill(-childProcess.pid, signal);
154257
- } else {
154258
- childProcess.kill(signal);
154259
- }
154295
+ process.kill(-childProcess.pid, signal);
154260
154296
  } catch {
154261
154297
  try {
154262
154298
  childProcess.kill(signal);
@@ -154268,20 +154304,28 @@ function spawnWithLauncher(launcher, options3) {
154268
154304
  const stderrChunks = [];
154269
154305
  let timedOut = false;
154270
154306
  let killTimer = null;
154271
- const timeoutId = options3.timeoutMs ? setTimeout(() => {
154272
- timedOut = true;
154273
- killProcessGroup("SIGTERM");
154274
- }, options3.timeoutMs) : null;
154275
- const abortHandler = () => {
154276
- killProcessGroup("SIGTERM");
154307
+ let completed = false;
154308
+ const terminateProcess = () => {
154309
+ if (process.platform === "win32") {
154310
+ killProcessTree("SIGKILL");
154311
+ return;
154312
+ }
154313
+ killProcessTree("SIGTERM");
154277
154314
  if (!killTimer) {
154278
154315
  killTimer = setTimeout(() => {
154279
- if (childProcess.exitCode === null && !childProcess.killed) {
154280
- killProcessGroup("SIGKILL");
154316
+ if (!completed) {
154317
+ killProcessTree("SIGKILL");
154281
154318
  }
154282
- }, ABORT_KILL_TIMEOUT_MS);
154319
+ }, FORCE_KILL_GRACE_MS);
154283
154320
  }
154284
154321
  };
154322
+ const timeoutId = options3.timeoutMs ? setTimeout(() => {
154323
+ timedOut = true;
154324
+ terminateProcess();
154325
+ }, options3.timeoutMs) : null;
154326
+ const abortHandler = () => {
154327
+ terminateProcess();
154328
+ };
154285
154329
  if (options3.signal) {
154286
154330
  options3.signal.addEventListener("abort", abortHandler, { once: true });
154287
154331
  }
@@ -154294,6 +154338,7 @@ function spawnWithLauncher(launcher, options3) {
154294
154338
  options3.onOutput?.(chunk.toString("utf8"), "stderr");
154295
154339
  });
154296
154340
  childProcess.on("error", (err) => {
154341
+ completed = true;
154297
154342
  if (timeoutId)
154298
154343
  clearTimeout(timeoutId);
154299
154344
  if (killTimer) {
@@ -154306,6 +154351,7 @@ function spawnWithLauncher(launcher, options3) {
154306
154351
  reject(buildSpawnError(err, executable, options3.cwd));
154307
154352
  });
154308
154353
  childProcess.on("close", (code2) => {
154354
+ completed = true;
154309
154355
  if (timeoutId)
154310
154356
  clearTimeout(timeoutId);
154311
154357
  if (killTimer) {
@@ -154340,7 +154386,7 @@ function spawnWithLauncher(launcher, options3) {
154340
154386
  });
154341
154387
  });
154342
154388
  }
154343
- var ShellExecutionError, ABORT_KILL_TIMEOUT_MS = 2000;
154389
+ var ShellExecutionError, FORCE_KILL_GRACE_MS = 2000;
154344
154390
  var init_shell_runner = __esm(() => {
154345
154391
  init_usable_directory();
154346
154392
  init_worktree_ownership();
@@ -157771,9 +157817,6 @@ async function sendMessageStreamWithBackend(backend, conversationId, messages, o
157771
157817
  }
157772
157818
  }
157773
157819
  const extraHeaders = {};
157774
- if (process.env.LETTA_RESPONSES_WS === "1") {
157775
- extraHeaders["X-Experimental-OpenAI-Responses-Websocket"] = "true";
157776
- }
157777
157820
  if (previousResponseId) {
157778
157821
  extraHeaders[RESPONSE_STATE_HEADER] = encodeResponseStateHeader({
157779
157822
  v: 1,
@@ -328456,7 +328499,7 @@ function createNoopModPanelHandle() {
328456
328499
  update() {}
328457
328500
  };
328458
328501
  }
328459
- function createLettaModApi(registry2, owner, capabilities, getClient2, onChange, onDiagnostic, builtinCommandIds, reservedToolNames, signal) {
328502
+ function createLettaModApi(registry2, owner, capabilities, getClient2, onChange, onDiagnostic, onNotification, builtinCommandIds, reservedToolNames, signal) {
328460
328503
  const isLive = () => isOwnerLive(registry2, owner);
328461
328504
  const guardLive = (capability) => {
328462
328505
  if (isLive())
@@ -328766,11 +328809,16 @@ function createLettaModApi(registry2, owner, capabilities, getClient2, onChange,
328766
328809
  },
328767
328810
  unregister: unregisterPermission
328768
328811
  },
328769
- diagnostics: {
328770
- report: reportDiagnostic
328771
- },
328812
+ diagnostics: { report: reportDiagnostic },
328772
328813
  ui: {
328773
328814
  closePanel,
328815
+ notify(message) {
328816
+ if (!capabilities.ui.panels || !message.trim())
328817
+ return;
328818
+ if (!guardLive({ id: "notify", kind: "panel" }))
328819
+ return;
328820
+ onNotification?.(message.trim());
328821
+ },
328774
328822
  openPanel(panel) {
328775
328823
  if (!capabilities.ui.panels) {
328776
328824
  return createNoopModPanelHandle();
@@ -328893,7 +328941,7 @@ async function loadLocalMods(options3) {
328893
328941
  if (typeof factory !== "function") {
328894
328942
  throw new Error("Mod must export a default function or activate() function");
328895
328943
  }
328896
- const dispose = await factory(createLettaModApi(registry2, owner, capabilities, getConfiguredClient, onChange, options3.onDiagnostic, builtinCommandIds, reservedToolNames, abortController.signal));
328944
+ const dispose = await factory(createLettaModApi(registry2, owner, capabilities, getConfiguredClient, onChange, options3.onDiagnostic, options3.onNotification, builtinCommandIds, reservedToolNames, abortController.signal));
328897
328945
  if (typeof dispose === "function") {
328898
328946
  registry2.disposers.push({
328899
328947
  abortController,
@@ -352398,13 +352446,7 @@ async function resolveBaseToolNamesForModel(modelIdentifier, options3) {
352398
352446
  baseToolNames = TOOL_NAMES;
352399
352447
  }
352400
352448
  if (options3?.include && options3.include.length > 0) {
352401
- const seen = new Set(baseToolNames);
352402
- for (const name of options3.include) {
352403
- if (!seen.has(name)) {
352404
- baseToolNames.push(name);
352405
- seen.add(name);
352406
- }
352407
- }
352449
+ baseToolNames = [...new Set([...baseToolNames, ...options3.include])];
352408
352450
  }
352409
352451
  if (options3?.exclude && options3.exclude.length > 0) {
352410
352452
  const excludeSet = new Set(options3.exclude);
@@ -353572,6 +353614,18 @@ function resolveIncludedToolNames(toolNames) {
353572
353614
  return internalName;
353573
353615
  });
353574
353616
  }
353617
+ function resolveAllowlistedToolNames(allowlist) {
353618
+ if (!allowlist)
353619
+ return [];
353620
+ const toolNames = [];
353621
+ for (const allowedName of allowlist) {
353622
+ const internalName = getInternalToolName(allowedName);
353623
+ if (Object.hasOwn(TOOL_DEFINITIONS, internalName)) {
353624
+ toolNames.push(internalName);
353625
+ }
353626
+ }
353627
+ return toolNames;
353628
+ }
353575
353629
  function appendUniqueToolNames(baseToolNames, includedToolNames) {
353576
353630
  const result = [...baseToolNames];
353577
353631
  const seen = new Set(result);
@@ -353663,7 +353717,7 @@ async function prepareToolExecutionContextForResolvedTarget(params) {
353663
353717
  } = params;
353664
353718
  const effectiveModel = modelIdentifier && modelIdentifier.length > 0 ? resolveModel(modelIdentifier) ?? modelIdentifier : null;
353665
353719
  const effectiveToolsetPreference = clientToolset?.base ?? toolsetPreference;
353666
- const includedToolNames = resolveIncludedToolNames(clientToolset?.include);
353720
+ const includedToolNames = appendUniqueToolNames(resolveIncludedToolNames(clientToolset?.include), resolveAllowlistedToolNames(clientToolAllowlist));
353667
353721
  if (effectiveToolsetPreference === "auto") {
353668
353722
  const derivedToolset = effectiveModel ? deriveToolsetFromModel(effectiveModel, providerType) : "default";
353669
353723
  const scopedModContext2 = buildModInvocationContext({
@@ -353742,6 +353796,7 @@ async function prepareToolExecutionContextForScope(params) {
353742
353796
  externalToolScopeIds,
353743
353797
  workingDirectory,
353744
353798
  permissionModeState,
353799
+ skillsDirectory,
353745
353800
  skillSources,
353746
353801
  cachedAgent,
353747
353802
  modContext,
@@ -353796,6 +353851,7 @@ async function prepareToolExecutionContextForScope(params) {
353796
353851
  agentName: agent2.name ?? null,
353797
353852
  conversationId: scopedConversationId,
353798
353853
  workingDirectory,
353854
+ ...skillsDirectory !== undefined ? { skillsDirectory } : {},
353799
353855
  ...skillSources !== undefined ? { skillSources } : {}
353800
353856
  }
353801
353857
  });
@@ -356516,6 +356572,9 @@ function updateLocalConversationRecord(current, body, updatedAt) {
356516
356572
  if (typeof bodyRecord.summary === "string" || bodyRecord.summary === null) {
356517
356573
  next.summary = bodyRecord.summary;
356518
356574
  }
356575
+ if (isStringArray2(bodyRecord.tags)) {
356576
+ next.tags = bodyRecord.tags;
356577
+ }
356519
356578
  return next;
356520
356579
  }
356521
356580
  function normalizeAgentRecord(value, defaultAgentModel) {
@@ -412081,35 +412140,35 @@ function buildSlackConversationSummary(msg) {
412081
412140
  if (msg.chatType === "direct") {
412082
412141
  if (msg.threadId?.trim()) {
412083
412142
  const preview2 = truncateChannelSummaryPreview(msg.text);
412084
- return preview2 ? `[Slack] DM thread with ${msg.senderName?.trim() || msg.senderId}: ${preview2}` : `[Slack] DM thread with ${msg.senderName?.trim() || msg.senderId}`;
412143
+ return preview2 ? `DM thread with ${msg.senderName?.trim() || msg.senderId}: ${preview2}` : `DM thread with ${msg.senderName?.trim() || msg.senderId}`;
412085
412144
  }
412086
- return `[Slack] DM with ${msg.senderName?.trim() || msg.senderId}`;
412145
+ return `DM with ${msg.senderName?.trim() || msg.senderId}`;
412087
412146
  }
412088
412147
  const preview = truncateChannelSummaryPreview(msg.text);
412089
412148
  const channelLabel = msg.chatLabel && msg.chatLabel !== msg.chatId ? ` in ${msg.chatLabel}` : "";
412090
412149
  if (preview)
412091
- return `[Slack] Thread${channelLabel}: ${preview}`;
412092
- return `[Slack] Thread${channelLabel || ` ${msg.chatId}`}`;
412150
+ return `Thread${channelLabel}: ${preview}`;
412151
+ return `Thread${channelLabel || ` ${msg.chatId}`}`;
412093
412152
  }
412094
412153
  function buildDiscordConversationSummary(msg) {
412095
412154
  if (msg.chatType === "direct") {
412096
- return `[Discord] DM with ${msg.senderName?.trim() || msg.senderId}`;
412155
+ return `DM with ${msg.senderName?.trim() || msg.senderId}`;
412097
412156
  }
412098
412157
  const preview = truncateChannelSummaryPreview(msg.text);
412099
412158
  const channelLabel = msg.chatLabel && msg.chatLabel !== msg.chatId ? ` in ${msg.chatLabel}` : "";
412100
412159
  if (preview)
412101
- return `[Discord] Thread${channelLabel}: ${preview}`;
412102
- return `[Discord] Thread${channelLabel || ` ${msg.chatId}`}`;
412160
+ return `Thread${channelLabel}: ${preview}`;
412161
+ return `Thread${channelLabel || ` ${msg.chatId}`}`;
412103
412162
  }
412104
412163
  function buildTelegramConversationSummary(msg) {
412105
412164
  if (msg.chatType === "direct") {
412106
- return `[Telegram] DM with ${msg.senderName?.trim() || msg.senderId}`;
412165
+ return `DM with ${msg.senderName?.trim() || msg.senderId}`;
412107
412166
  }
412108
412167
  const preview = truncateChannelSummaryPreview(msg.text);
412109
412168
  const channelLabel = msg.chatLabel && msg.chatLabel !== msg.chatId ? ` in ${msg.chatLabel}` : "";
412110
412169
  if (preview)
412111
- return `[Telegram] Topic${channelLabel}: ${preview}`;
412112
- return `[Telegram] Topic${channelLabel || ` ${msg.chatId}`}`;
412170
+ return `Topic${channelLabel}: ${preview}`;
412171
+ return `Topic${channelLabel || ` ${msg.chatId}`}`;
412113
412172
  }
412114
412173
  function buildWhatsAppConversationSummary(msg) {
412115
412174
  if (msg.chatType === "direct") {
@@ -447354,7 +447413,7 @@ function isRuntimeStartCommand(value) {
447354
447413
  if (!value || typeof value !== "object")
447355
447414
  return false;
447356
447415
  const c = value;
447357
- return c.type === "runtime_start" && typeof c.request_id === "string" && (c.agent_id === undefined || typeof c.agent_id === "string") && (c.create_agent === undefined || isRuntimeStartCreateAgentOptions(c.create_agent)) && (c.conversation_id === undefined || typeof c.conversation_id === "string") && (c.create_conversation === undefined || isRuntimeStartCreateConversationOptions(c.create_conversation)) && (c.cwd === undefined || c.cwd === null || typeof c.cwd === "string") && (c.mode === undefined || isDevicePermissionMode(c.mode)) && (c.skill_sources === undefined || isSkillSourceArray(c.skill_sources)) && (c.preserve_skill_sources === undefined || typeof c.preserve_skill_sources === "boolean") && (c.client_info === undefined || isRuntimeStartClientInfo(c.client_info)) && (c.recover_approvals === undefined || typeof c.recover_approvals === "boolean") && (c.force_device_status === undefined || typeof c.force_device_status === "boolean") && (c.wait_for_replay === undefined || typeof c.wait_for_replay === "boolean") && (c.external_tools === undefined || Array.isArray(c.external_tools) && c.external_tools.every(isRuntimeStartExternalToolsGroup));
447416
+ return c.type === "runtime_start" && typeof c.request_id === "string" && (c.agent_id === undefined || typeof c.agent_id === "string") && (c.create_agent === undefined || isRuntimeStartCreateAgentOptions(c.create_agent)) && (c.conversation_id === undefined || typeof c.conversation_id === "string") && (c.create_conversation === undefined || isRuntimeStartCreateConversationOptions(c.create_conversation)) && (c.conversation_source_tags === undefined || isStringArray7(c.conversation_source_tags)) && (c.cwd === undefined || c.cwd === null || typeof c.cwd === "string") && (c.mode === undefined || isDevicePermissionMode(c.mode)) && (c.skill_sources === undefined || isSkillSourceArray(c.skill_sources)) && (c.preserve_skill_sources === undefined || typeof c.preserve_skill_sources === "boolean") && (c.client_info === undefined || isRuntimeStartClientInfo(c.client_info)) && (c.recover_approvals === undefined || typeof c.recover_approvals === "boolean") && (c.force_device_status === undefined || typeof c.force_device_status === "boolean") && (c.wait_for_replay === undefined || typeof c.wait_for_replay === "boolean") && (c.external_tools === undefined || Array.isArray(c.external_tools) && c.external_tools.every(isRuntimeStartExternalToolsGroup));
447358
447417
  }
447359
447418
  function isTerminalSpawnCommand(value) {
447360
447419
  if (!value || typeof value !== "object")
@@ -450433,6 +450492,34 @@ async function resolveRuntimeStartConversation(parsed, agent2, created) {
450433
450492
  created.conversation = true;
450434
450493
  return conversation;
450435
450494
  }
450495
+ function removeMatchingSourcePrefix(summary, sourceTags) {
450496
+ if (typeof summary !== "string")
450497
+ return summary;
450498
+ const match4 = summary.match(/^\s*\[([^\]]+)\]\s*/);
450499
+ if (!match4)
450500
+ return summary;
450501
+ const prefix = match4[1]?.trim().toLowerCase();
450502
+ const matchesSourceTag = sourceTags.some((tag) => LEGACY_SUMMARY_PREFIX_BY_SOURCE_TAG[tag] === prefix);
450503
+ return matchesSourceTag ? summary.slice(match4[0].length) : summary;
450504
+ }
450505
+ async function applyRuntimeStartConversationSourceTags(parsed, conversation) {
450506
+ const sourceTags = parsed.conversation_source_tags;
450507
+ if (conversation.id === "default" || !sourceTags?.length) {
450508
+ return conversation;
450509
+ }
450510
+ const currentTags = Reflect.get(conversation, "tags");
450511
+ const existingTags = Array.isArray(currentTags) ? currentTags.filter((tag) => typeof tag === "string") : [];
450512
+ const missingTags = sourceTags.filter((tag) => !existingTags.includes(tag));
450513
+ const summary = removeMatchingSourcePrefix(conversation.summary, sourceTags);
450514
+ const summaryChanged = summary !== conversation.summary;
450515
+ if (missingTags.length === 0 && !summaryChanged) {
450516
+ return conversation;
450517
+ }
450518
+ return getBackend().updateConversation(conversation.id, {
450519
+ ...missingTags.length > 0 ? { tags: [...new Set([...existingTags, ...missingTags])] } : {},
450520
+ ...summaryChanged ? { summary } : {}
450521
+ });
450522
+ }
450436
450523
  async function applyRuntimeStartState(parsed, context3, scope, scopedRuntime) {
450437
450524
  if (parsed.skill_sources === undefined && parsed.preserve_skill_sources !== true) {
450438
450525
  scopedRuntime.skillSources = undefined;
@@ -450473,6 +450560,7 @@ async function handleRuntimeStartCommand(parsed, context3) {
450473
450560
  validateRuntimeStartShape(parsed);
450474
450561
  agent2 = await resolveRuntimeStartAgent(parsed, created);
450475
450562
  conversation = await resolveRuntimeStartConversation(parsed, agent2, created);
450563
+ conversation = await applyRuntimeStartConversationSourceTags(parsed, conversation);
450476
450564
  runtimeScope = buildRuntimeScope(agent2, conversation);
450477
450565
  const { connectionId } = context3;
450478
450566
  const assertConnectionOpen = () => {
@@ -450527,6 +450615,7 @@ function handleRuntimeStartProtocolCommand(parsed, context3) {
450527
450615
  });
450528
450616
  return true;
450529
450617
  }
450618
+ var LEGACY_SUMMARY_PREFIX_BY_SOURCE_TAG;
450530
450619
  var init_runtime_start = __esm(async () => {
450531
450620
  init_create5();
450532
450621
  init_create_agent_request();
@@ -450541,6 +450630,12 @@ var init_runtime_start = __esm(async () => {
450541
450630
  init_external_tools(),
450542
450631
  init_protocol_inbound()
450543
450632
  ]);
450633
+ LEGACY_SUMMARY_PREFIX_BY_SOURCE_TAG = {
450634
+ "channel:discord": "discord",
450635
+ "channel:slack": "slack",
450636
+ "channel:telegram": "telegram",
450637
+ "origin:schedule": "schedule"
450638
+ };
450544
450639
  });
450545
450640
 
450546
450641
  // src/websocket/listener/commands/settings.ts
@@ -451373,10 +451468,10 @@ function getRecoverableStatusNoticeVisibility(kind) {
451373
451468
  return "transcript";
451374
451469
  }
451375
451470
  }
451376
- function getRecoverableRetryNoticeVisibility(kind, attempt) {
451471
+ function getRecoverableRetryNoticeVisibility(kind) {
451377
451472
  switch (kind) {
451378
451473
  case "transient_provider_retry":
451379
- return attempt === 1 ? "debug_only" : "transcript";
451474
+ return "transcript";
451380
451475
  default:
451381
451476
  return "transcript";
451382
451477
  }
@@ -451526,7 +451621,7 @@ function emitRecoverableStatusNotice(socket, runtime, params) {
451526
451621
  });
451527
451622
  }
451528
451623
  function emitRecoverableRetryNotice(socket, runtime, params) {
451529
- const visibility = getRecoverableRetryNoticeVisibility(params.kind, params.attempt);
451624
+ const visibility = getRecoverableRetryNoticeVisibility(params.kind);
451530
451625
  if (visibility === "debug_only") {
451531
451626
  debugLog("recovery", `Debug-only retry notice (${params.kind}, attempt ${params.attempt}/${params.maxAttempts}): ${params.message}`);
451532
451627
  mirrorRecoverableNoticeToDesktopDebugPanel(params.message);
@@ -453218,6 +453313,50 @@ var init_stream_resume = __esm(() => {
453218
453313
  init_client2();
453219
453314
  });
453220
453315
 
453316
+ // src/cli/helpers/stream-terminal-eof-guard.ts
453317
+ function getTerminalEofGraceMs() {
453318
+ const raw2 = process.env.LETTA_STREAM_TERMINAL_EOF_GRACE_MS;
453319
+ if (raw2) {
453320
+ const parsed = Number(raw2);
453321
+ if (Number.isFinite(parsed) && parsed > 0) {
453322
+ return parsed;
453323
+ }
453324
+ }
453325
+ return DEFAULT_TERMINAL_EOF_GRACE_MS;
453326
+ }
453327
+ function createTerminalEofGuard(context3) {
453328
+ let timer = null;
453329
+ let fired = false;
453330
+ return {
453331
+ arm: () => {
453332
+ if (timer) {
453333
+ clearTimeout(timer);
453334
+ }
453335
+ const graceMs = getTerminalEofGraceMs();
453336
+ timer = setTimeout(() => {
453337
+ fired = true;
453338
+ debugWarn("drainStream", "Terminal-EOF guard fired: stop_reason=%s received but stream did not end within %dms - aborting HTTP read", context3.getStopReason(), graceMs);
453339
+ telemetry.trackError("stream_terminal_eof_guard_fired", `Stream received stop_reason=${context3.getStopReason()} but HTTP body did not end within ${graceMs}ms`, "stream_drain", {
453340
+ runId: context3.getRunId() ?? undefined
453341
+ });
453342
+ context3.abortHttpRead();
453343
+ }, graceMs);
453344
+ },
453345
+ clear: () => {
453346
+ if (timer) {
453347
+ clearTimeout(timer);
453348
+ timer = null;
453349
+ }
453350
+ },
453351
+ fired: () => fired
453352
+ };
453353
+ }
453354
+ var DEFAULT_TERMINAL_EOF_GRACE_MS = 2000;
453355
+ var init_stream_terminal_eof_guard = __esm(() => {
453356
+ init_telemetry();
453357
+ init_debug();
453358
+ });
453359
+
453221
453360
  // src/cli/helpers/stream.ts
453222
453361
  function summarizeStreamForDebug(stream12) {
453223
453362
  if (!stream12 || typeof stream12 !== "object") {
@@ -453283,6 +453422,11 @@ async function drainStream(stream12, buffers, refresh, abortSignal, onFirstMessa
453283
453422
  let fallbackError = null;
453284
453423
  let lastChunkDebugSummary = "none";
453285
453424
  let abortedViaListener = false;
453425
+ const terminalEofGuard = createTerminalEofGuard({
453426
+ getStopReason: () => streamProcessor.stopReason,
453427
+ getRunId: () => streamProcessor.lastRunId,
453428
+ abortHttpRead: () => abortStreamController(stream12, "terminal_eof_guard")
453429
+ });
453286
453430
  const startAbortGen = buffers.abortGeneration || 0;
453287
453431
  const abortHandler = () => {
453288
453432
  abortedViaListener = true;
@@ -453323,6 +453467,9 @@ async function drainStream(stream12, buffers, refresh, abortSignal, onFirstMessa
453323
453467
  logTiming(`TTFT: ${formatDuration(ttft)} (from POST to first content)`);
453324
453468
  }
453325
453469
  const { shouldOutput, errorInfo, updatedApproval } = streamProcessor.processChunk(chunk);
453470
+ if (streamProcessor.stopReason !== null) {
453471
+ terminalEofGuard.arm();
453472
+ }
453326
453473
  try {
453327
453474
  chunkLog.append(chunk);
453328
453475
  } catch {}
@@ -453397,6 +453544,7 @@ async function drainStream(stream12, buffers, refresh, abortSignal, onFirstMessa
453397
453544
  }
453398
453545
  queueMicrotask(refresh);
453399
453546
  } finally {
453547
+ terminalEofGuard.clear();
453400
453548
  try {
453401
453549
  chunkLog.flush();
453402
453550
  } catch {}
@@ -453409,6 +453557,11 @@ async function drainStream(stream12, buffers, refresh, abortSignal, onFirstMessa
453409
453557
  if (!stopReason && streamProcessor.stopReason) {
453410
453558
  stopReason = streamProcessor.stopReason;
453411
453559
  }
453560
+ if (terminalEofGuard.fired()) {
453561
+ upsertStatusLine(buffers, `terminal-eof-${startTime}`, [
453562
+ "Stream did not close after completing, continued without waiting"
453563
+ ]);
453564
+ }
453412
453565
  if (abortedViaListener && !stopReason) {
453413
453566
  stopReason = "cancelled";
453414
453567
  markIncompleteToolsAsCancelled(buffers, true, "user_interrupt");
@@ -453462,7 +453615,8 @@ async function drainStream(stream12, buffers, refresh, abortSignal, onFirstMessa
453462
453615
  lastRunId: streamProcessor.lastRunId,
453463
453616
  lastSeqId: streamProcessor.lastSeqId,
453464
453617
  apiDurationMs,
453465
- fallbackError
453618
+ fallbackError,
453619
+ terminalEofGuardFired: terminalEofGuard.fired()
453466
453620
  };
453467
453621
  }
453468
453622
  async function drainStreamWithResume(stream12, buffers, refresh, abortSignal, onFirstMessage, onChunkProcessed, contextTracker, seenSeqIdThreshold, resumePolicy) {
@@ -453655,6 +453809,7 @@ var init_stream = __esm(async () => {
453655
453809
  init_tui_perf();
453656
453810
  init_chunk_log();
453657
453811
  init_stream_resume();
453812
+ init_stream_terminal_eof_guard();
453658
453813
  await __promiseAll([
453659
453814
  init_message(),
453660
453815
  init_accumulator()
@@ -453835,6 +453990,62 @@ var init_approval_suggestions = __esm(async () => {
453835
453990
  ]);
453836
453991
  });
453837
453992
 
453993
+ // src/websocket/listener/cloud-retry-message.ts
453994
+ function isRecord12(value) {
453995
+ return typeof value === "object" && value !== null && !Array.isArray(value);
453996
+ }
453997
+ function optionalString4(value) {
453998
+ return typeof value === "string" && value.length > 0 ? value : null;
453999
+ }
454000
+ function parseCloudRetryMessage(value) {
454001
+ if (!isRecord12(value) || value.message_type !== "retry_message") {
454002
+ return null;
454003
+ }
454004
+ const attempt = value.attempt;
454005
+ const maxAttempts = value.max_attempts;
454006
+ const delayMs = value.delay_ms;
454007
+ const retryKind = value.retry_kind;
454008
+ if (typeof value.message !== "string" || value.message.length === 0 || retryKind !== "provider_retry" && retryKind !== "transport_fallback" || typeof attempt !== "number" || !Number.isInteger(attempt) || attempt < 1 || typeof maxAttempts !== "number" || !Number.isInteger(maxAttempts) || maxAttempts < attempt || typeof delayMs !== "number" || !Number.isFinite(delayMs) || delayMs < 0 || typeof value.provider !== "string" || value.provider.length === 0) {
454009
+ return null;
454010
+ }
454011
+ return {
454012
+ message: value.message,
454013
+ retryKind,
454014
+ attempt,
454015
+ maxAttempts,
454016
+ delayMs,
454017
+ provider: value.provider,
454018
+ fromTransport: optionalString4(value.from_transport),
454019
+ toTransport: optionalString4(value.to_transport),
454020
+ errorCode: optionalString4(value.error_code),
454021
+ runId: optionalString4(value.run_id),
454022
+ stepId: optionalString4(value.step_id)
454023
+ };
454024
+ }
454025
+ function normalizeCloudRetryWireMessage(value) {
454026
+ const retry3 = parseCloudRetryMessage(value);
454027
+ if (!retry3) {
454028
+ return null;
454029
+ }
454030
+ return {
454031
+ ...createLifecycleMessageBase("retry", retry3.runId),
454032
+ message: retry3.message,
454033
+ reason: "llm_api_error",
454034
+ attempt: retry3.attempt,
454035
+ max_attempts: retry3.maxAttempts,
454036
+ delay_ms: retry3.delayMs,
454037
+ retry_kind: retry3.retryKind,
454038
+ provider: retry3.provider,
454039
+ from_transport: retry3.fromTransport,
454040
+ to_transport: retry3.toTransport,
454041
+ error_code: retry3.errorCode,
454042
+ step_id: retry3.stepId
454043
+ };
454044
+ }
454045
+ var init_cloud_retry_message = __esm(async () => {
454046
+ await init_protocol_outbound();
454047
+ });
454048
+
453838
454049
  // src/websocket/listener/turn-input-state.ts
453839
454050
  function ensureTurnInputMessageOtids(messages) {
453840
454051
  let didChange = false;
@@ -454111,7 +454322,7 @@ async function drainRecoveryStreamWithEmission(recoveryStream, socket, runtime,
454111
454322
  });
454112
454323
  }
454113
454324
  if (shouldOutput) {
454114
- const normalizedChunk = normalizeToolReturnWireMessage(chunk);
454325
+ const normalizedChunk = normalizeCloudRetryWireMessage(chunk) ?? normalizeToolReturnWireMessage(chunk);
454115
454326
  if (normalizedChunk) {
454116
454327
  emitCanonicalMessageDelta(socket, runtime, {
454117
454328
  ...normalizedChunk,
@@ -454558,6 +454769,7 @@ var init_recovery = __esm(async () => {
454558
454769
  init_stream(),
454559
454770
  init_toolset(),
454560
454771
  init_approval_suggestions(),
454772
+ init_cloud_retry_message(),
454561
454773
  init_continuation_input(),
454562
454774
  init_interrupts(),
454563
454775
  init_mod_adapter2(),
@@ -454610,16 +454822,17 @@ var init_approval_recovery = __esm(() => {
454610
454822
  });
454611
454823
 
454612
454824
  // src/websocket/listener/provider-fallback.ts
454613
- function createProviderFallbackState(agent2) {
454825
+ function createProviderFallbackState(agent2, overrideModel) {
454614
454826
  const llmConfig = agent2?.llm_config;
454615
454827
  const model = llmConfig?.model;
454616
454828
  if (!model) {
454617
- return { sourceModelId: null, attempted: false };
454829
+ return { sourceModelId: null, attempted: false, overrideModel };
454618
454830
  }
454619
454831
  const modelInfo = getModelInfoForLlmConfig(model, llmConfig) ?? getModelInfo(model);
454620
454832
  return {
454621
454833
  sourceModelId: modelInfo?.id ?? model,
454622
- attempted: false
454834
+ attempted: false,
454835
+ overrideModel
454623
454836
  };
454624
454837
  }
454625
454838
  function maybeApplyProviderFallback(state, attempt) {
@@ -458040,17 +458253,18 @@ async function emitListenerTurnStart(options3) {
458040
458253
  conversationId: options3.conversationId,
458041
458254
  input: options3.input
458042
458255
  };
458043
- await createListenerModEvents(modAdapters).emit("turn_start", event2, context3);
458256
+ const emission = await createListenerModEvents(modAdapters).emit("turn_start", event2, context3);
458044
458257
  const cancel = getTurnStartCancel(event2);
458045
458258
  if (cancel) {
458046
458259
  return { cancelled: true, reason: cancel.reason };
458047
458260
  }
458048
458261
  return {
458049
458262
  cancelled: false,
458263
+ handlerCount: emission.handlerCount,
458050
458264
  input: isTurnInputArray(event2.input) ? event2.input : options3.input
458051
458265
  };
458052
458266
  } catch {
458053
- return { cancelled: false, input: options3.input };
458267
+ return { cancelled: false, handlerCount: 0, input: options3.input };
458054
458268
  }
458055
458269
  }
458056
458270
  async function emitListenerTurnEnd(options3) {
@@ -459137,13 +459351,20 @@ async function prepareListenerTurn(params) {
459137
459351
  workingDirectory,
459138
459352
  permissionMode: permissionModeState.mode,
459139
459353
  cachedAgent
459140
- }) : { cancelled: false, input: messagesToSend };
459354
+ }) : { cancelled: false, handlerCount: 0, input: messagesToSend };
459141
459355
  if (isInterrupted()) {
459142
459356
  return { kind: "interrupted" };
459143
459357
  }
459144
459358
  if (turnStartEmission.cancelled) {
459145
459359
  return { kind: "cancelled", reason: turnStartEmission.reason };
459146
459360
  }
459361
+ let overrideModel;
459362
+ if (turnStartEmission.handlerCount > 0) {
459363
+ try {
459364
+ const conversation = await getBackend().retrieveConversation(conversationId);
459365
+ overrideModel = conversation.model ?? undefined;
459366
+ } catch {}
459367
+ }
459147
459368
  const currentInput = ensureTurnInputMessageOtids(turnStartEmission.input);
459148
459369
  const turnInput = createTurnInputState(currentInput, getInboundImageFailureModes({
459149
459370
  imageFailureMode: msg.imageFailureMode,
@@ -459153,7 +459374,8 @@ async function prepareListenerTurn(params) {
459153
459374
  inboundUserTranscriptLines = buildInboundUserTranscriptLines(currentInput);
459154
459375
  }
459155
459376
  const modAdapters = await ensureListenerModAdaptersForAgent(runtime.listener, agentId);
459156
- const environmentDeviceId = connectionId ? runtime.listener.connections.get(connectionId)?.options.deviceId : undefined;
459377
+ const listenerOptions = connectionId ? runtime.listener.connections.get(connectionId)?.options : runtime.listener.connections.values().next().value?.options;
459378
+ const environmentDeviceId = listenerOptions?.deviceId;
459157
459379
  const preparedToolContext = await prepareToolExecutionContextForScope({
459158
459380
  connectionId,
459159
459381
  environmentDeviceId,
@@ -459165,6 +459387,7 @@ async function prepareListenerTurn(params) {
459165
459387
  externalToolScopeIds: msg.externalToolScopeIds,
459166
459388
  workingDirectory,
459167
459389
  permissionModeState,
459390
+ skillsDirectory: listenerOptions?.skillsDirectory,
459168
459391
  skillSources: runtime.skillSources,
459169
459392
  cachedAgent,
459170
459393
  modContext: createListenerAgentModContext(agentId),
@@ -459185,7 +459408,8 @@ async function prepareListenerTurn(params) {
459185
459408
  pendingNormalizationInterruptedToolCallIds: [
459186
459409
  ...queuedInterruptedToolCallIds
459187
459410
  ],
459188
- preparedToolContext
459411
+ preparedToolContext,
459412
+ ...overrideModel ? { overrideModel } : {}
459189
459413
  };
459190
459414
  }
459191
459415
  var init_turn_setup = __esm(async () => {
@@ -459338,7 +459562,7 @@ async function handleIncomingMessageInner(msg, socket, runtime, onStatusChange,
459338
459562
  }
459339
459563
  let turnInput = setup.turnInput;
459340
459564
  const inboundUserTranscriptLines = setup.inboundUserTranscriptLines;
459341
- const providerFallback = createProviderFallbackState(setup.getCachedAgent());
459565
+ const providerFallback = createProviderFallbackState(setup.getCachedAgent(), setup.overrideModel);
459342
459566
  let pendingNormalizationInterruptedToolCallIds = setup.pendingNormalizationInterruptedToolCallIds;
459343
459567
  const preparedToolContext = setup.preparedToolContext;
459344
459568
  const buildSendOptions = () => ({
@@ -459430,7 +459654,7 @@ async function handleIncomingMessageInner(msg, socket, runtime, onStatusChange,
459430
459654
  }
459431
459655
  }
459432
459656
  if (shouldOutput) {
459433
- const normalizedChunk = normalizeToolReturnWireMessage(chunk);
459657
+ const normalizedChunk = normalizeCloudRetryWireMessage(chunk) ?? normalizeToolReturnWireMessage(chunk);
459434
459658
  if (normalizedChunk) {
459435
459659
  emitCanonicalMessageDelta(socket, runtime, {
459436
459660
  ...normalizedChunk,
@@ -459446,8 +459670,14 @@ async function handleIncomingMessageInner(msg, socket, runtime, onStatusChange,
459446
459670
  const stopReason = result.stopReason;
459447
459671
  const approvals = result.approvals || [];
459448
459672
  const fallbackError = result.fallbackError ?? null;
459449
- if (finishIfInterrupted(runId || runtime.activeRunId)) {
459450
- break;
459673
+ if (result.terminalEofGuardFired) {
459674
+ emitStatusDelta(socket, runtime, {
459675
+ message: "Stream did not close after completing, continued without waiting",
459676
+ level: "warning",
459677
+ runId: runId || runtime.activeRunId,
459678
+ agentId,
459679
+ conversationId
459680
+ });
459451
459681
  }
459452
459682
  if (finishIfInterrupted(runId || runtime.activeRunId)) {
459453
459683
  break;
@@ -459900,6 +460130,7 @@ var init_turn = __esm(async () => {
459900
460130
  init_message(),
459901
460131
  init_accumulator(),
459902
460132
  init_stream(),
460133
+ init_cloud_retry_message(),
459903
460134
  init_interrupts(),
459904
460135
  init_protocol_outbound(),
459905
460136
  init_recoverable_notices(),
@@ -461503,7 +461734,18 @@ function createMissedPongWatchdog(maxUnansweredPings) {
461503
461734
  }
461504
461735
  };
461505
461736
  }
461506
- function startConnectionHeartbeat(runtime, transport, onStale, sendPing) {
461737
+ function getCurrentStreamTransport(runtime, controlTransport) {
461738
+ for (const connection of runtime.connections.values()) {
461739
+ if (connection.writer !== controlTransport)
461740
+ continue;
461741
+ const streamTransport = connection.streamWriter;
461742
+ if (streamTransport && streamTransport !== controlTransport) {
461743
+ return streamTransport;
461744
+ }
461745
+ }
461746
+ return null;
461747
+ }
461748
+ function startConnectionHeartbeat(runtime, transport, onStale, sendPing, options3 = {}) {
461507
461749
  runtime.lastPongAt = Date.now();
461508
461750
  const maxUnansweredPings = Math.max(1, Math.ceil(LISTENER_PONG_TIMEOUT_MS / LISTENER_HEARTBEAT_INTERVAL_MS));
461509
461751
  const watchdog = createMissedPongWatchdog(maxUnansweredPings);
@@ -461513,10 +461755,14 @@ function startConnectionHeartbeat(runtime, transport, onStale, sendPing) {
461513
461755
  return;
461514
461756
  }
461515
461757
  const sentAt = Date.now();
461516
- if (sendPing()) {
461758
+ if (sendPing(transport)) {
461517
461759
  watchdog.recordPing(sentAt);
461518
461760
  }
461519
- }, LISTENER_HEARTBEAT_INTERVAL_MS);
461761
+ const streamTransport = getCurrentStreamTransport(runtime, transport);
461762
+ if (streamTransport) {
461763
+ sendPing(streamTransport);
461764
+ }
461765
+ }, options3.intervalMs ?? LISTENER_HEARTBEAT_INTERVAL_MS);
461520
461766
  }
461521
461767
  var init_heartbeat = __esm(() => {
461522
461768
  init_constants3();
@@ -463963,7 +464209,7 @@ function dispatchInboundMessageWhenReady(params) {
463963
464209
  emitListenerStatus(listener, options3.onStatusChange, options3.connectionId);
463964
464210
  rememberAcceptedInputDisposition(runtime, clientMessageId, "started");
463965
464211
  acknowledgeInput({ accepted: true, disposition: "started" });
463966
- await processIncomingMessage(incoming, socket, runtime, options3.onStatusChange, options3.connectionId);
464212
+ await processIncomingMessage(incoming, getOrCreateProcessTransport(listener), runtime, options3.onStatusChange, options3.connectionId);
463967
464213
  emitListenerStatus(listener, options3.onStatusChange, options3.connectionId);
463968
464214
  if (runtime.queueRuntime.length > 0 || runtime.queuePumpScheduled || runtime.queuePumpActive) {
463969
464215
  scheduleQueuePump(runtime, socket, options3, processQueuedTurn);
@@ -463980,12 +464226,13 @@ function dispatchInboundMessageWhenReady(params) {
463980
464226
  }
463981
464227
  var MAX_ACCEPTED_INPUT_DISPOSITIONS = 4096;
463982
464228
  var init_inbound_dispatch = __esm(async () => {
464229
+ init_connection();
463983
464230
  init_runtime();
463984
464231
  await init_queue();
463985
464232
  });
463986
464233
 
463987
464234
  // src/websocket/listener/protocol-logging.ts
463988
- function isRecord12(value) {
464235
+ function isRecord13(value) {
463989
464236
  return typeof value === "object" && value !== null && !Array.isArray(value);
463990
464237
  }
463991
464238
  function formatLogValue(value) {
@@ -464009,14 +464256,14 @@ function pushField(fields, key2, value, label = key2) {
464009
464256
  fields.push(`${label}=${formatted}`);
464010
464257
  }
464011
464258
  function summarizeInputPayload(payload) {
464012
- if (!isRecord12(payload))
464259
+ if (!isRecord13(payload))
464013
464260
  return [];
464014
464261
  const fields = [];
464015
464262
  pushField(fields, "kind", payload.kind);
464016
464263
  if (payload.kind === "create_message") {
464017
464264
  pushField(fields, "messages", payload.messages);
464018
464265
  pushField(fields, "client_tool_allowlist", payload.client_tool_allowlist);
464019
- if (isRecord12(payload.client_toolset)) {
464266
+ if (isRecord13(payload.client_toolset)) {
464020
464267
  pushField(fields, "client_toolset.base", payload.client_toolset.base);
464021
464268
  pushField(fields, "client_toolset.include", payload.client_toolset.include);
464022
464269
  }
@@ -464033,9 +464280,9 @@ function summarizeRuntimeStartCommand(command) {
464033
464280
  const fields = [];
464034
464281
  pushField(fields, "agent_id", command.agent_id, "agent");
464035
464282
  pushField(fields, "conversation_id", command.conversation_id, "conversation");
464036
- if (isRecord12(command.create_agent))
464283
+ if (isRecord13(command.create_agent))
464037
464284
  fields.push("create_agent=true");
464038
- if (isRecord12(command.create_conversation)) {
464285
+ if (isRecord13(command.create_conversation)) {
464039
464286
  fields.push("create_conversation=true");
464040
464287
  }
464041
464288
  pushField(fields, "cwd", command.cwd);
@@ -464044,17 +464291,17 @@ function summarizeRuntimeStartCommand(command) {
464044
464291
  return fields;
464045
464292
  }
464046
464293
  function summarizeV2Command(parsed) {
464047
- if (!isRecord12(parsed) || typeof parsed.type !== "string")
464294
+ if (!isRecord13(parsed) || typeof parsed.type !== "string")
464048
464295
  return "unknown";
464049
464296
  const fields = [];
464050
- const runtime = isRecord12(parsed.runtime) ? parsed.runtime : null;
464297
+ const runtime = isRecord13(parsed.runtime) ? parsed.runtime : null;
464051
464298
  if (runtime) {
464052
464299
  fields.push(`runtime=${runtime.agent_id ?? "<unknown>"}/${runtime.conversation_id ?? "<unknown>"}`);
464053
464300
  }
464054
464301
  pushField(fields, "request_id", parsed.request_id);
464055
464302
  if (parsed.type === "input") {
464056
464303
  fields.push(...summarizeInputPayload(parsed.payload));
464057
- } else if (parsed.type === "change_device_state" && isRecord12(parsed.payload)) {
464304
+ } else if (parsed.type === "change_device_state" && isRecord13(parsed.payload)) {
464058
464305
  pushField(fields, "mode", parsed.payload.mode);
464059
464306
  pushField(fields, "cwd", parsed.payload.cwd);
464060
464307
  pushField(fields, "agent_id", parsed.payload.agent_id);
@@ -464064,7 +464311,7 @@ function summarizeV2Command(parsed) {
464064
464311
  } else if (parsed.type === "runtime_external_tools_update" && Array.isArray(parsed.updates)) {
464065
464312
  fields.push(`updates=${parsed.updates.length}`);
464066
464313
  fields.push(`runtimes=${parsed.updates.reduce((count, update2) => {
464067
- return count + (isRecord12(update2) && Array.isArray(update2.runtimes) ? update2.runtimes.length : 0);
464314
+ return count + (isRecord13(update2) && Array.isArray(update2.runtimes) ? update2.runtimes.length : 0);
464068
464315
  }, 0)}`);
464069
464316
  } else {
464070
464317
  for (const key2 of [
@@ -465096,8 +465343,8 @@ async function startConnectedListenerRuntime(runtime, transport, opts, processQu
465096
465343
  startConnectionHeartbeat(runtime, transport, () => {
465097
465344
  trackListenerError4("listener_pong_timeout", new Error(`No relay pong within ${LISTENER_PONG_TIMEOUT_MS}ms; terminating half-open socket to force reconnect`), "listener_heartbeat");
465098
465345
  runtime.socket?.terminate();
465099
- }, () => {
465100
- return safeTransportSend(transport, { type: "ping" }, "listener_ping_send_failed", "listener_heartbeat");
465346
+ }, (heartbeatTransport) => {
465347
+ return safeTransportSend(heartbeatTransport, { type: "ping" }, "listener_ping_send_failed", "listener_heartbeat");
465101
465348
  });
465102
465349
  }
465103
465350
  if (options3.startProcessServices === false)
@@ -466127,7 +466374,7 @@ function decodeJwtClaims(token2, sharedSecret) {
466127
466374
  } catch {
466128
466375
  return { error: unauthorized("invalid websocket jwt") };
466129
466376
  }
466130
- if (!isRecord13(header) || header.alg !== "HS256") {
466377
+ if (!isRecord14(header) || header.alg !== "HS256") {
466131
466378
  return { error: unauthorized("invalid websocket jwt") };
466132
466379
  }
466133
466380
  const expectedSignature = createHmac("sha256", sharedSecret).update(`${encodedHeader}.${encodedClaims}`).digest();
@@ -466165,7 +466412,7 @@ function audienceMatches(actual, expectedAudience) {
466165
466412
  return false;
466166
466413
  }
466167
466414
  function isJwtClaims(value) {
466168
- if (!isRecord13(value) || !Number.isSafeInteger(value.exp)) {
466415
+ if (!isRecord14(value) || !Number.isSafeInteger(value.exp)) {
466169
466416
  return false;
466170
466417
  }
466171
466418
  if (value.nbf !== undefined && !Number.isSafeInteger(value.nbf)) {
@@ -466185,7 +466432,7 @@ function isJwtClaims(value) {
466185
466432
  }
466186
466433
  return true;
466187
466434
  }
466188
- function isRecord13(value) {
466435
+ function isRecord14(value) {
466189
466436
  return typeof value === "object" && value !== null && !Array.isArray(value);
466190
466437
  }
466191
466438
  function base64UrlDecode(value) {
@@ -468375,13 +468622,14 @@ function shouldAcquireStandaloneListenerLock() {
468375
468622
  return shouldAcquireManualListenerLock(getSpawnerListenerInstanceId(), process.env.LETTA_DESKTOP_MODE === "1");
468376
468623
  }
468377
468624
  function printListenUsage() {
468378
- console.log(`Usage: letta server [--env-name <name>] [--channels <list>] [--debug]
468625
+ console.log(`Usage: letta server [--env-name <name>] [--channels <list>] [--skills <path>] [--debug]
468379
468626
  `);
468380
468627
  console.log(`Register this letta-code instance to receive messages from Letta Cloud.
468381
468628
  `);
468382
468629
  console.log("Options:");
468383
468630
  console.log(" --env-name <name> Friendly name for this environment (uses hostname if not provided)");
468384
468631
  console.log(" --channels <list> Comma-separated channel names to enable (e.g. telegram)");
468632
+ console.log(" --skills <path> Use this directory for environment-provided skills");
468385
468633
  console.log(" --install-channel-runtimes Install missing runtime deps for the selected channels before startup");
468386
468634
  console.log(" --debug Plain-text mode: log all WebSocket events instead of interactive UI");
468387
468635
  console.log(` -h, --help Show this help message
@@ -468415,6 +468663,7 @@ async function runListenSubcommand(argv) {
468415
468663
  return 1;
468416
468664
  }
468417
468665
  const debugMode = !!values2.debug;
468666
+ const skillsDirectory = values2.skills ?? process.env.LETTA_SKILLS_DIRECTORY;
468418
468667
  if (values2.help) {
468419
468668
  printListenUsage();
468420
468669
  return 0;
@@ -468730,6 +468979,7 @@ async function runListenSubcommand(argv) {
468730
468979
  supportsSplitStatusChannels: nextSupportsSplitStatusChannels,
468731
468980
  deviceId,
468732
468981
  connectionName,
468982
+ skillsDirectory,
468733
468983
  onWsEvent: shouldLogWsEvents ? wsEventLogger : undefined,
468734
468984
  onStatusChange: (status) => {
468735
468985
  sessionLog.log(`status: ${status}`);
@@ -468795,6 +469045,7 @@ async function runListenSubcommand(argv) {
468795
469045
  supportsSplitStatusChannels: nextSupportsSplitStatusChannels,
468796
469046
  deviceId,
468797
469047
  connectionName,
469048
+ skillsDirectory,
468798
469049
  onWsEvent: shouldLogWsEvents ? wsEventLogger : undefined,
468799
469050
  onStatusChange: (status) => {
468800
469051
  sessionLog.log(`status: ${status}`);
@@ -468887,6 +469138,7 @@ var init_listen = __esm(async () => {
468887
469138
  LISTEN_OPTIONS = {
468888
469139
  "env-name": { type: "string" },
468889
469140
  channels: { type: "string" },
469141
+ skills: { type: "string" },
468890
469142
  "install-channel-runtimes": { type: "boolean" },
468891
469143
  help: { type: "boolean", short: "h" },
468892
469144
  debug: { type: "boolean" }
@@ -470434,7 +470686,7 @@ import {
470434
470686
  } from "node:fs";
470435
470687
  import { tmpdir as tmpdir9 } from "node:os";
470436
470688
  import path41 from "node:path";
470437
- function isRecord14(value) {
470689
+ function isRecord15(value) {
470438
470690
  return typeof value === "object" && value !== null && !Array.isArray(value);
470439
470691
  }
470440
470692
  function isPathInsideOrEqual2(childPath, parentPath) {
@@ -470450,7 +470702,7 @@ function readPackageJson(packageJsonPath) {
470450
470702
  } catch (error54) {
470451
470703
  throw new Error(`Could not read package.json: ${error54 instanceof Error ? error54.message : String(error54)}`);
470452
470704
  }
470453
- if (!isRecord14(parsed)) {
470705
+ if (!isRecord15(parsed)) {
470454
470706
  throw new Error("package.json must be an object");
470455
470707
  }
470456
470708
  return parsed;
@@ -470460,7 +470712,7 @@ function formatRepository(repository) {
470460
470712
  const trimmed2 = repository.trim();
470461
470713
  return trimmed2 || undefined;
470462
470714
  }
470463
- if (!isRecord14(repository))
470715
+ if (!isRecord15(repository))
470464
470716
  return;
470465
470717
  const url2 = repository.url;
470466
470718
  if (typeof url2 !== "string")
@@ -470990,7 +471242,7 @@ function hasRuntimeDependencies(packageJson) {
470990
471242
  if (!packageJson)
470991
471243
  return false;
470992
471244
  const dependencies4 = packageJson.dependencies;
470993
- return isRecord14(dependencies4) && Object.keys(dependencies4).length > 0;
471245
+ return isRecord15(dependencies4) && Object.keys(dependencies4).length > 0;
470994
471246
  }
470995
471247
  function readPackageJsonIfExists(packageDirectory) {
470996
471248
  const packageJsonPath = path41.join(packageDirectory, "package.json");
@@ -477806,6 +478058,9 @@ function uniqueSources(sources) {
477806
478058
  byKey.set(sourceKey(source2), source2);
477807
478059
  return [...byKey.values()];
477808
478060
  }
478061
+ function channelTagsForSources(sources) {
478062
+ return [...new Set(sources.map((source2) => `channel:${source2.channel}`))];
478063
+ }
477809
478064
  function stopReasonFromDelta(message) {
477810
478065
  const delta2 = message.delta;
477811
478066
  return delta2.message_type === "stop_reason" && "stop_reason" in delta2 && typeof delta2.stop_reason === "string" ? delta2.stop_reason : null;
@@ -478047,9 +478302,11 @@ class ChannelGateway {
478047
478302
  }
478048
478303
  async performRuntimeRegistration(state, delivery) {
478049
478304
  const tool2 = await this.hooks.buildExternalTool(delivery.runtime, delivery.sources);
478305
+ const conversationTags = channelTagsForSources(delivery.sources);
478050
478306
  const signature = JSON.stringify({
478051
478307
  mode: delivery.defaultPermissionMode ?? null,
478052
- tool: tool2
478308
+ tool: tool2,
478309
+ conversationTags
478053
478310
  });
478054
478311
  if (state.registrationSignature === signature && state.registration) {
478055
478312
  return state.registration;
@@ -478057,6 +478314,7 @@ class ChannelGateway {
478057
478314
  const registration = this.client.runtimeStart({
478058
478315
  agent_id: delivery.runtime.agent_id,
478059
478316
  conversation_id: delivery.runtime.conversation_id,
478317
+ ...conversationTags.length > 0 ? { conversation_source_tags: conversationTags } : {},
478060
478318
  ...delivery.defaultPermissionMode ? { mode: delivery.defaultPermissionMode } : {},
478061
478319
  recover_approvals: true,
478062
478320
  force_device_status: false,
@@ -492188,7 +492446,7 @@ async function connectMcpServer(config3, options3 = {}) {
492188
492446
  return {
492189
492447
  content: Array.isArray(result.content) ? result.content : [],
492190
492448
  ...result.isError === true ? { isError: true } : {},
492191
- ...isRecord15(result.structuredContent) ? { structuredContent: result.structuredContent } : {}
492449
+ ...isRecord16(result.structuredContent) ? { structuredContent: result.structuredContent } : {}
492192
492450
  };
492193
492451
  },
492194
492452
  close: async () => {
@@ -492261,11 +492519,11 @@ function mergeHeaders4(init, headers) {
492261
492519
  };
492262
492520
  }
492263
492521
  function normalizeInputSchema(value) {
492264
- if (isRecord15(value) && value.type === "object")
492522
+ if (isRecord16(value) && value.type === "object")
492265
492523
  return value;
492266
492524
  return { type: "object", properties: {} };
492267
492525
  }
492268
- function isRecord15(value) {
492526
+ function isRecord16(value) {
492269
492527
  return typeof value === "object" && value !== null && !Array.isArray(value);
492270
492528
  }
492271
492529
  var DEFAULT_CLIENT_INFO;
@@ -492277,7 +492535,7 @@ var init_mcp_client = __esm(() => {
492277
492535
  init_streamableHttp();
492278
492536
  DEFAULT_CLIENT_INFO = {
492279
492537
  name: "letta-code",
492280
- version: "0.30.8"
492538
+ version: "0.30.10"
492281
492539
  };
492282
492540
  });
492283
492541
 
@@ -492673,7 +492931,7 @@ function toExternalToolResult(result) {
492673
492931
  };
492674
492932
  }
492675
492933
  function normalizeContent2(item) {
492676
- if (isRecord16(item)) {
492934
+ if (isRecord17(item)) {
492677
492935
  if (item.type === "text" && typeof item.text === "string") {
492678
492936
  return { type: "text", text: item.text };
492679
492937
  }
@@ -492687,7 +492945,7 @@ function normalizeContent2(item) {
492687
492945
  }
492688
492946
  return { type: "text", text: JSON.stringify(item) };
492689
492947
  }
492690
- function isRecord16(value) {
492948
+ function isRecord17(value) {
492691
492949
  return typeof value === "object" && value !== null && !Array.isArray(value);
492692
492950
  }
492693
492951
  var CLIENT_MCP_RUNTIME_KEY;
@@ -495437,6 +495695,15 @@ ${loadedContents.join(`
495437
495695
  const lastReasoning = reversed.find((line) => line.kind === "reasoning" && ("text" in line) && typeof line.text === "string" && line.text.trim().length > 0);
495438
495696
  const lastToolResult = reversed.find((line) => line.kind === "tool_call" && ("resultText" in line) && typeof line.resultText === "string" && (line.resultText ?? "").trim().length > 0);
495439
495697
  const resultText = lastAssistant?.text || lastReasoning?.text || lastToolResult?.resultText || "No assistant response found";
495698
+ if (!lastAssistant && (lastReasoning || lastToolResult)) {
495699
+ trackEndTurnNoAssistant({
495700
+ fallbackKind: lastReasoning ? "reasoning" : "tool_call",
495701
+ modelHandle: agent2.llm_config?.model ?? model,
495702
+ runId: lastKnownRunId ?? undefined,
495703
+ isSubagent,
495704
+ subagentType: systemPromptPreset ?? agent2.tags?.find((t2) => t2.startsWith("type:"))?.slice(5)
495705
+ });
495706
+ }
495440
495707
  const stats = sessionStats.getSnapshot();
495441
495708
  const usage = {
495442
495709
  prompt_tokens: stats.usage.promptTokens,
@@ -500356,8 +500623,9 @@ function useLocalModAdapter(context3, options3 = {}) {
500356
500623
  ...agentModsDirectory ? { agentModsDirectory } : {},
500357
500624
  disabled,
500358
500625
  getBackend,
500359
- getClient
500360
- }), [agentModsDirectory, disabled]);
500626
+ getClient,
500627
+ onNotification: options3.onNotification
500628
+ }), [agentModsDirectory, disabled, options3.onNotification]);
500361
500629
  const snapshot = import_react43.useSyncExternalStore(adapter.subscribe, adapter.getSnapshot, adapter.getSnapshot);
500362
500630
  import_react43.useEffect(() => {
500363
500631
  adapter.reload();
@@ -552980,6 +553248,7 @@ function App2({
552980
553248
  }, [isExecutingTool]);
552981
553249
  const refreshDerivedRef = import_react120.useRef(null);
552982
553250
  const appendTaskNotificationEvents = import_react120.useCallback((summaries) => appendTaskNotificationEventsToBuffer(summaries, buffersRef.current, () => uid("event"), () => refreshDerivedRef.current?.()), []);
553251
+ const appendModNotification = import_react120.useCallback((message) => appendTaskNotificationEvents([message]), [appendTaskNotificationEvents]);
552983
553252
  const consumeQueuedMessages = import_react120.useCallback(() => {
552984
553253
  const len = tuiQueueRef.current?.length ?? 0;
552985
553254
  if (len === 0)
@@ -553399,7 +553668,8 @@ function App2({
553399
553668
  const agentModsDirectory = modContext.memfs.enabled && modContext.memfs.memoryDir ? join92(modContext.memfs.memoryDir, "mods") : null;
553400
553669
  const modAdapter = useLocalModAdapter(modContext, {
553401
553670
  agentModsDirectory,
553402
- disabled: modsDisabled
553671
+ disabled: modsDisabled,
553672
+ onNotification: appendModNotification
553403
553673
  });
553404
553674
  import_react120.useEffect(() => {
553405
553675
  modAdapterRef.current = modAdapter;
@@ -559380,4 +559650,4 @@ function registerBunOAuthFlows() {
559380
559650
  registerBunOAuthFlows();
559381
559651
  await init_src5().then(() => exports_src2);
559382
559652
 
559383
- //# debugId=11C66507CCCFFD3064756E2164756E21
559653
+ //# debugId=BC797C70CE06CED564756E2164756E21