@tangle-network/agent-app 0.44.54 → 0.44.55

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.
@@ -9,13 +9,13 @@ import { T as TurnEventStore } from '../turn-buffer-COMQUwO2.js';
9
9
  import { M as ModelFailoverAttempt } from '../failover-D-3UXXTb.js';
10
10
  export { D as DEFAULT_STALE_TURN_LOCK_GRACE_MS, a as DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS, R as ReconcileStaleTurnLockOptions, b as ReconcileStaleTurnLockResult, S as StaleTurnLockSandboxProbeResult, c as StaleTurnLockSessionProbeResult, r as reconcileStaleTurnLock } from '../stale-turn-lock-DucQzvXu.js';
11
11
  import { J as JsonRecord } from '../stream-normalizer-CnPnMaTp.js';
12
+ import { SandboxInstance } from '@tangle-network/sandbox';
12
13
  import { SandboxExecChannel, PromptInputPart } from '../sandbox/index.js';
13
14
  import '@tangle-network/agent-interface';
14
15
  import '../contract-CQNvv5th.js';
15
16
  import '../types-CCeYywdS.js';
16
17
  import '../plans/index.js';
17
18
  import '@tangle-network/sandbox/core';
18
- import '@tangle-network/sandbox';
19
19
  import '../auth-DJs6lfAs.js';
20
20
  import '../types-DbU-oO5h.js';
21
21
  import '../harness/index.js';
@@ -1086,9 +1086,10 @@ type DetachedTurnParts = Array<Record<string, unknown>>;
1086
1086
  interface DetachedTurnFinal {
1087
1087
  text?: string;
1088
1088
  usage?: ChatTurnUsage;
1089
- /** The structured parts to persist when this receipt IS the result (the
1090
- * cached / finished-server-side path). Omitted when the record only carries
1091
- * a usage receipt. */
1089
+ /** The structured parts to persist when this receipt is more complete than
1090
+ * the live stream (cached, finished server-side, or a fast stream that
1091
+ * delivered scalar text before its message-part events). Omitted when the
1092
+ * record only carries a usage receipt. */
1092
1093
  parts?: DetachedTurnParts;
1093
1094
  }
1094
1095
  /** Define options for managing and projecting a detached turn event stream in a session */
@@ -1142,8 +1143,10 @@ interface DetachedTurnOptions {
1142
1143
  /** Authoritative final receipt, consulted whenever a re-invoke finds a prior
1143
1144
  * buffer: (a) an already-`complete` turn returns it as the cached result,
1144
1145
  * (b) a `running` turn (crash mid-run) uses it to detect a run that finished
1145
- * server-side, and (c) a clean run whose stream carried no usage/text falls
1146
- * back to it. Typically `() => box.findCompletedTurn(turnId)`. */
1146
+ * server-side, and (c) a clean run whose stream carried no usage or only
1147
+ * scalar text falls back to it. For Sandbox runs, use
1148
+ * `readCompletedSandboxTurn` so the exact completed session message
1149
+ * restores tool/file parts as well as text. */
1147
1150
  completedResult?: () => Promise<DetachedTurnFinal | null | undefined>;
1148
1151
  /** Clear the prior partial buffer for `turnId` before a genuine re-stream.
1149
1152
  * A crash mid-run leaves buffered rows at seqs 1..N with status `running`;
@@ -1236,6 +1239,31 @@ interface DetachedTurnResult {
1236
1239
  */
1237
1240
  declare function runDetachedTurn(opts: DetachedTurnOptions): Promise<DetachedTurnResult>;
1238
1241
 
1242
+ /**
1243
+ * Recover a detached Sandbox turn from its durable completed records.
1244
+ *
1245
+ * A fast detached run can finish before the live event subscriber receives
1246
+ * every message-part event. The Sandbox still retains two exact records: a
1247
+ * turn-id keyed result cache and the completed assistant message on the
1248
+ * session. This adapter joins them without guessing across turns, then returns
1249
+ * the same `DetachedTurnFinal` shape `runDetachedTurn` already consumes.
1250
+ */
1251
+
1252
+ /** The official Sandbox methods needed for completed-turn recovery. */
1253
+ type CompletedSandboxTurnSource = Pick<SandboxInstance, 'findCompletedTurn' | 'session'>;
1254
+ /** Options for resolving one exact detached turn. */
1255
+ interface ReadCompletedSandboxTurnOptions {
1256
+ turnId: string;
1257
+ sessionId: string;
1258
+ log?: (message: string, meta?: Record<string, unknown>) => void;
1259
+ }
1260
+ /**
1261
+ * Read the exact completed turn. The session-level result is consulted only
1262
+ * when the matching completed assistant message is still the latest message;
1263
+ * otherwise that result could belong to a newer turn on the same session.
1264
+ */
1265
+ declare function readCompletedSandboxTurn(box: CompletedSandboxTurnSource, options: ReadCompletedSandboxTurnOptions): Promise<DetachedTurnFinal | null>;
1266
+
1239
1267
  /** Resolve chat route events and materialize their durable state records */
1240
1268
  interface ChatRouteDurableProjection {
1241
1269
  observe(event: unknown): void | Promise<void>;
@@ -1755,4 +1783,4 @@ interface PromoteAgentFilePartOptions {
1755
1783
  /** Promote a part of an agent file with optional byte limits and MIME type detection */
1756
1784
  declare function promoteAgentFilePart(options: PromoteAgentFilePartOptions): Promise<PromoteFilePartResult>;
1757
1785
 
1758
- export { type AssistantDraftSnapshot, type AssistantDraftStore, type AssistantDraftWriter, type AssistantDraftWriterOptions, type AssistantRowValues, type AttachmentPathArgs, type AttachmentPathCheck, type AttachmentReadResult, type AttachmentUploadAuthorization, type AttachmentWriteResult, type BuildDispatchPartsInput, ChatAttachmentKind, type ChatRouteDurableProjection, type ChatRouteDurableProjectionLogger, type ChatRouteEvent, type ChatTurnAuthorization, type ChatTurnAuthorizeArgs, type ChatTurnCompleteInput, ChatTurnFilePartInput, type ChatTurnGateResult, type ChatTurnHeartbeat, type ChatTurnInputPatch, type ChatTurnLifecycle, type ChatTurnLifecycleComplete, type ChatTurnLifecycleError, type ChatTurnLifecycleStart, type ChatTurnLock, type ChatTurnLockResult, type ChatTurnMessageStore, type ChatTurnModelAttribution, type ChatTurnModelFailover, ChatTurnPartInput, type ChatTurnProduceArgs, ChatTurnRequestPayload, type ChatTurnRouteProducer, type ChatTurnRoutes, type ChatTurnUsage, type CreateAttachmentUploadRouteOptions, type CreateChatTurnRoutesOptions, type CreateUploadRouteOptions, type DetachedTurnFinal, type DetachedTurnOptions, type DetachedTurnParts, type DetachedTurnResult, type DispatchPartsOutcome, type DraftPersistenceTuning, type DraftStoredMessage, type EmptyTurnRetryInfo, type FilePartPromotionOutcome, MAX_EMPTY_TURN_RETRIES, type ModelFailoverStreamHandle, type ModelFailoverStreamOptions, type ModelFallbackInfo, type OpenModelStream, PROMOTE_MAX_FILE_BYTES, type PromoteAgentFilePartOptions, type PromoteFilePartResult, PromptInputPart, type RawAgentFilePart, type ReadAttachmentFn, type ReadSandboxMentionFn, type ResolveChatAttachmentsOptions, type ResolveChatAttachmentsResult, type SandboxChatProducerOptions, type SandboxUploadSink, UPLOAD_INLINE_MAX_BYTES, UPLOAD_MAX_FILE_BYTES, type UploadAuthorization, type UploadedChatFile, type WriteAttachmentFn, assistantRowIdForTurn, buildDispatchParts, bytesToBase64, classifyTerminalFailure, createAssistantDraftWriter, createAttachmentUploadRoute, createChatTurnRoutes, createSandboxChatProducer, createUploadRoute, defaultValidateAttachmentPath, isCommittingSandboxEvent, isDraftContentEvent, normalizeChatPromptForSandbox, promoteAgentFilePart, resolveChatAttachments, resolveEmptyTurnRetries, rowIdOf, runDetachedTurn, sanitizeUploadFilename, sniffMimeFromName, storeSupportsDraftPersistence, streamWithModelFailover, summarizeFailoverReason, withDurableChatProjection };
1786
+ export { type AssistantDraftSnapshot, type AssistantDraftStore, type AssistantDraftWriter, type AssistantDraftWriterOptions, type AssistantRowValues, type AttachmentPathArgs, type AttachmentPathCheck, type AttachmentReadResult, type AttachmentUploadAuthorization, type AttachmentWriteResult, type BuildDispatchPartsInput, ChatAttachmentKind, type ChatRouteDurableProjection, type ChatRouteDurableProjectionLogger, type ChatRouteEvent, type ChatTurnAuthorization, type ChatTurnAuthorizeArgs, type ChatTurnCompleteInput, ChatTurnFilePartInput, type ChatTurnGateResult, type ChatTurnHeartbeat, type ChatTurnInputPatch, type ChatTurnLifecycle, type ChatTurnLifecycleComplete, type ChatTurnLifecycleError, type ChatTurnLifecycleStart, type ChatTurnLock, type ChatTurnLockResult, type ChatTurnMessageStore, type ChatTurnModelAttribution, type ChatTurnModelFailover, ChatTurnPartInput, type ChatTurnProduceArgs, ChatTurnRequestPayload, type ChatTurnRouteProducer, type ChatTurnRoutes, type ChatTurnUsage, type CompletedSandboxTurnSource, type CreateAttachmentUploadRouteOptions, type CreateChatTurnRoutesOptions, type CreateUploadRouteOptions, type DetachedTurnFinal, type DetachedTurnOptions, type DetachedTurnParts, type DetachedTurnResult, type DispatchPartsOutcome, type DraftPersistenceTuning, type DraftStoredMessage, type EmptyTurnRetryInfo, type FilePartPromotionOutcome, MAX_EMPTY_TURN_RETRIES, type ModelFailoverStreamHandle, type ModelFailoverStreamOptions, type ModelFallbackInfo, type OpenModelStream, PROMOTE_MAX_FILE_BYTES, type PromoteAgentFilePartOptions, type PromoteFilePartResult, PromptInputPart, type RawAgentFilePart, type ReadAttachmentFn, type ReadCompletedSandboxTurnOptions, type ReadSandboxMentionFn, type ResolveChatAttachmentsOptions, type ResolveChatAttachmentsResult, type SandboxChatProducerOptions, type SandboxUploadSink, UPLOAD_INLINE_MAX_BYTES, UPLOAD_MAX_FILE_BYTES, type UploadAuthorization, type UploadedChatFile, type WriteAttachmentFn, assistantRowIdForTurn, buildDispatchParts, bytesToBase64, classifyTerminalFailure, createAssistantDraftWriter, createAttachmentUploadRoute, createChatTurnRoutes, createSandboxChatProducer, createUploadRoute, defaultValidateAttachmentPath, isCommittingSandboxEvent, isDraftContentEvent, normalizeChatPromptForSandbox, promoteAgentFilePart, readCompletedSandboxTurn, resolveChatAttachments, resolveEmptyTurnRetries, rowIdOf, runDetachedTurn, sanitizeUploadFilename, sniffMimeFromName, storeSupportsDraftPersistence, streamWithModelFailover, summarizeFailoverReason, withDurableChatProjection };
@@ -66,6 +66,7 @@ import {
66
66
  import {
67
67
  asRecord,
68
68
  asString,
69
+ collapseRedundantTextParts,
69
70
  draftAssistantParts,
70
71
  finalizeAssistantParts,
71
72
  finalizePendingInteractionParts,
@@ -1027,6 +1028,28 @@ function streamWithModelFailover(options) {
1027
1028
  };
1028
1029
  }
1029
1030
 
1031
+ // src/chat-routes/sandbox-turn-usage.ts
1032
+ function addStepFinishUsage(part, usage) {
1033
+ const tokens = asRecord(part.tokens);
1034
+ if (tokens) {
1035
+ const cache = asRecord(tokens.cache);
1036
+ const add = (current, value) => {
1037
+ const n = Number(value);
1038
+ if (!Number.isFinite(n)) return current;
1039
+ return (current ?? 0) + n;
1040
+ };
1041
+ usage.inputTokens = add(usage.inputTokens, tokens.input);
1042
+ usage.outputTokens = add(usage.outputTokens, tokens.output);
1043
+ usage.reasoningTokens = add(usage.reasoningTokens, tokens.reasoning);
1044
+ if (cache) {
1045
+ usage.cacheReadTokens = add(usage.cacheReadTokens, cache.read);
1046
+ usage.cacheWriteTokens = add(usage.cacheWriteTokens, cache.write);
1047
+ }
1048
+ }
1049
+ const cost = Number(part.cost);
1050
+ if (Number.isFinite(cost)) usage.costUsd = (usage.costUsd ?? 0) + cost;
1051
+ }
1052
+
1030
1053
  // src/chat-routes/sandbox-producer.ts
1031
1054
  function textDelta(tracker, key, part, rawDelta) {
1032
1055
  const explicit = typeof rawDelta === "string" ? rawDelta : void 0;
@@ -1058,26 +1081,6 @@ function parseEffectiveBackend(data) {
1058
1081
  ...source ? { servedSource: source } : {}
1059
1082
  };
1060
1083
  }
1061
- function usageFromStepFinish(part, usage) {
1062
- const tokens = asRecord(part.tokens);
1063
- if (tokens) {
1064
- const cache = asRecord(tokens.cache);
1065
- const add = (current, value) => {
1066
- const n = Number(value);
1067
- if (!Number.isFinite(n)) return current;
1068
- return (current ?? 0) + n;
1069
- };
1070
- usage.inputTokens = add(usage.inputTokens, tokens.input);
1071
- usage.outputTokens = add(usage.outputTokens, tokens.output);
1072
- usage.reasoningTokens = add(usage.reasoningTokens, tokens.reasoning);
1073
- if (cache) {
1074
- usage.cacheReadTokens = add(usage.cacheReadTokens, cache.read);
1075
- usage.cacheWriteTokens = add(usage.cacheWriteTokens, cache.write);
1076
- }
1077
- }
1078
- const cost = Number(part.cost);
1079
- if (Number.isFinite(cost)) usage.costUsd = (usage.costUsd ?? 0) + cost;
1080
- }
1081
1084
  function toFiniteNumber(value) {
1082
1085
  if (typeof value === "number") return Number.isFinite(value) ? value : null;
1083
1086
  if (typeof value === "string" && value.trim() !== "") {
@@ -1316,7 +1319,7 @@ function createSandboxChatProducer(options) {
1316
1319
  continue;
1317
1320
  }
1318
1321
  if (partType === "step-finish") {
1319
- usageFromStepFinish(part, usage);
1322
+ addStepFinishUsage(part, usage);
1320
1323
  recordPersistedPart(part, void 0, `step-finish:#${stepCounter++}`);
1321
1324
  const promptTokens = usage.inputTokens ?? 0;
1322
1325
  const completionTokens = usage.outputTokens ?? 0;
@@ -1558,11 +1561,11 @@ function errorMessageOf(ev) {
1558
1561
  function hasUsage(usage) {
1559
1562
  return typeof usage.inputTokens === "number" && usage.inputTokens > 0;
1560
1563
  }
1561
- function cachedResultFrom(final) {
1564
+ function cachedResultFrom(final, persisted) {
1562
1565
  return {
1563
1566
  state: "completed",
1564
- text: final?.text ?? "",
1565
- parts: final?.parts ?? [],
1567
+ text: final?.text ?? persisted?.content ?? "",
1568
+ parts: final?.parts ?? persisted?.parts ?? [],
1566
1569
  usage: final?.usage ?? {},
1567
1570
  cached: true
1568
1571
  };
@@ -1571,6 +1574,12 @@ async function runDetachedTurn(opts) {
1571
1574
  const { store, turnId, scopeId } = opts;
1572
1575
  let producer;
1573
1576
  const servingModel = () => producer?.model ?? opts.model;
1577
+ const persistedRow = async () => {
1578
+ if (!opts.persist) return void 0;
1579
+ return (await opts.persist.store.listMessages(opts.persist.threadId)).find(
1580
+ (message) => message.id === (opts.persist?.messageId ?? assistantRowIdForTurn(turnId))
1581
+ );
1582
+ };
1574
1583
  let draft;
1575
1584
  if (opts.persist) {
1576
1585
  const { store: persistStore, threadId, messageId, transformText, ...tuning } = opts.persist;
@@ -1594,10 +1603,8 @@ async function runDetachedTurn(opts) {
1594
1603
  ...opts.log ? { log: opts.log } : {}
1595
1604
  });
1596
1605
  }
1597
- const settleRow = async (base) => {
1598
- const persisted = !producer && opts.persist ? (await opts.persist.store.listMessages(opts.persist.threadId)).find(
1599
- (message) => message.id === (opts.persist?.messageId ?? assistantRowIdForTurn(turnId))
1600
- ) : void 0;
1606
+ const settleRow = async (base, cachedPersisted) => {
1607
+ const persisted = cachedPersisted ?? (!producer ? await persistedRow() : void 0);
1601
1608
  const info = producer?.modelFailover?.();
1602
1609
  const attribution = producer?.modelAttribution?.();
1603
1610
  const model = producer?.model ?? persisted?.model ?? opts.model;
@@ -1660,14 +1667,21 @@ async function runDetachedTurn(opts) {
1660
1667
  } catch (err) {
1661
1668
  opts.log?.("[chat-routes] runDetachedTurn getStatus failed; treating as no prior", { turnId, err: String(err) });
1662
1669
  }
1663
- if (prior === "complete") return await settleRow(cachedResultFrom(await completed()));
1670
+ if (prior === "complete") {
1671
+ const persisted = await persistedRow();
1672
+ return await settleRow(
1673
+ cachedResultFrom(await completed(), persisted),
1674
+ persisted
1675
+ );
1676
+ }
1664
1677
  if (prior === "running") {
1665
1678
  const final = await completed();
1666
1679
  if (final) {
1667
1680
  await store.setStatus(turnId, "complete", scopeId).catch((err) => {
1668
1681
  opts.log?.("[chat-routes] runDetachedTurn failed to settle a completed running turn", { turnId, err: String(err) });
1669
1682
  });
1670
- return await settleRow(cachedResultFrom(final));
1683
+ const persisted = await persistedRow();
1684
+ return await settleRow(cachedResultFrom(final, persisted), persisted);
1671
1685
  }
1672
1686
  if (opts.resetBuffer) {
1673
1687
  await opts.resetBuffer(turnId).catch((err) => {
@@ -1714,17 +1728,171 @@ async function runDetachedTurn(opts) {
1714
1728
  const text = producer.finalText?.() ?? "";
1715
1729
  const parts = producer.assistantParts?.() ?? [];
1716
1730
  let usage = producer.usage?.() ?? {};
1717
- if (!runError && !hasUsage(usage)) {
1731
+ const onlyTextParts = parts.every(
1732
+ (part) => String(part.type ?? "") === "text"
1733
+ );
1734
+ if (!runError && (!hasUsage(usage) || !text || onlyTextParts)) {
1718
1735
  const final = await completed();
1719
1736
  if (final?.usage) usage = { ...usage, ...final.usage };
1720
- if (!text && final?.text) {
1721
- return await settleRow({ state: "completed", text: final.text, parts: final.parts ?? parts, usage, cached: false });
1722
- }
1737
+ return await settleRow({
1738
+ state: "completed",
1739
+ text: final?.text ?? text,
1740
+ parts: final?.parts?.length ? final.parts : parts,
1741
+ usage,
1742
+ cached: false
1743
+ });
1723
1744
  }
1724
1745
  if (runError) return await settleRow({ state: "failed", text, parts, usage, error: runError, cached: false });
1725
1746
  return await settleRow({ state: "completed", text, parts, usage, cached: false });
1726
1747
  }
1727
1748
 
1749
+ // src/chat-routes/completed-sandbox-turn.ts
1750
+ function finiteNumber(value) {
1751
+ if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
1752
+ if (typeof value !== "string" || !value.trim()) return void 0;
1753
+ const parsed = Number(value);
1754
+ return Number.isFinite(parsed) ? parsed : void 0;
1755
+ }
1756
+ function firstNumber(record, keys) {
1757
+ for (const key of keys) {
1758
+ const value = finiteNumber(record?.[key]);
1759
+ if (value !== void 0) return value;
1760
+ }
1761
+ return void 0;
1762
+ }
1763
+ function usageFromResult(result) {
1764
+ const raw = asRecord(result?.usage) ?? asRecord(result?.tokenUsage);
1765
+ const inputTokens = firstNumber(raw, ["inputTokens", "promptTokens", "input"]);
1766
+ const outputTokens = firstNumber(raw, ["outputTokens", "completionTokens", "output"]);
1767
+ const reasoningTokens = firstNumber(raw, ["reasoningTokens", "reasoning"]);
1768
+ const cacheReadTokens = firstNumber(raw, [
1769
+ "cacheReadTokens",
1770
+ "cacheReadInputTokens",
1771
+ "cacheRead"
1772
+ ]);
1773
+ const cacheWriteTokens = firstNumber(raw, [
1774
+ "cacheWriteTokens",
1775
+ "cacheCreationInputTokens",
1776
+ "cacheWrite"
1777
+ ]);
1778
+ const costUsd = firstNumber(result, ["costUsd", "totalCostUsd", "cost"]) ?? firstNumber(raw, ["costUsd", "cost"]);
1779
+ return {
1780
+ ...inputTokens !== void 0 ? { inputTokens } : {},
1781
+ ...outputTokens !== void 0 ? { outputTokens } : {},
1782
+ ...reasoningTokens !== void 0 ? { reasoningTokens } : {},
1783
+ ...cacheReadTokens !== void 0 ? { cacheReadTokens } : {},
1784
+ ...cacheWriteTokens !== void 0 ? { cacheWriteTokens } : {},
1785
+ ...costUsd !== void 0 ? { costUsd } : {}
1786
+ };
1787
+ }
1788
+ function textFromResult(result) {
1789
+ return asString(result?.response) ?? asString(result?.finalText) ?? asString(result?.text) ?? asString(result?.output);
1790
+ }
1791
+ function completedMessagesForTurn(messages, turnId) {
1792
+ return messages.filter(
1793
+ (message) => message.role === "assistant" && message.metadata?.turnId === turnId && message.metadata?.interrupted !== true && (message.metadata?.completed === true || message.metadata?.status === "completed")
1794
+ );
1795
+ }
1796
+ function normalizedMessageParts(message) {
1797
+ const normalized = [];
1798
+ const usage = {};
1799
+ for (const raw of message.parts) {
1800
+ const record = asRecord(raw);
1801
+ if (!record) continue;
1802
+ const part = normalizePersistedPart(record);
1803
+ if (!part) continue;
1804
+ normalized.push(part);
1805
+ if (part.type === "step-finish") addStepFinishUsage(part, usage);
1806
+ }
1807
+ const collapsed = collapseRedundantTextParts(normalized);
1808
+ const derivedText = collapsed.filter((part) => part.type === "text").map((part) => String(part.text ?? "")).join("");
1809
+ return { parts: normalized, usage, derivedText };
1810
+ }
1811
+ function projectMessageParts(parts, finalText) {
1812
+ const order = [];
1813
+ const map = /* @__PURE__ */ new Map();
1814
+ for (let index = 0; index < parts.length; index += 1) {
1815
+ const part = parts[index];
1816
+ const type = String(part.type ?? "");
1817
+ const key = type === "step-start" || type === "step-finish" ? `${type}:#${index}` : getPartKey(part);
1818
+ if (!map.has(key)) order.push(key);
1819
+ map.set(key, mergePersistedPart(map.get(key), part));
1820
+ }
1821
+ return finalizeAssistantParts(order, map, finalText);
1822
+ }
1823
+ function hasUsage2(usage) {
1824
+ return Object.values(usage).some(
1825
+ (value) => typeof value === "number" && Number.isFinite(value)
1826
+ );
1827
+ }
1828
+ async function readCompletedSandboxTurn(box, options) {
1829
+ const { turnId, sessionId, log } = options;
1830
+ const session = box.session(sessionId);
1831
+ const [cacheOutcome, messagesOutcome] = await Promise.allSettled([
1832
+ box.findCompletedTurn(turnId, { sessionId }),
1833
+ session.messages({ limit: 1e3 })
1834
+ ]);
1835
+ if (cacheOutcome.status === "rejected") {
1836
+ log?.("[chat-routes] completed Sandbox turn cache lookup failed", {
1837
+ turnId,
1838
+ sessionId,
1839
+ error: String(cacheOutcome.reason)
1840
+ });
1841
+ }
1842
+ if (messagesOutcome.status === "rejected") {
1843
+ log?.("[chat-routes] completed Sandbox session message lookup failed", {
1844
+ turnId,
1845
+ sessionId,
1846
+ error: String(messagesOutcome.reason)
1847
+ });
1848
+ }
1849
+ const rawCached = cacheOutcome.status === "fulfilled" ? cacheOutcome.value : null;
1850
+ const cached = rawCached && rawCached.turnId === turnId && rawCached.sessionId === sessionId ? rawCached : null;
1851
+ if (rawCached && !cached) {
1852
+ log?.("[chat-routes] ignored mismatched completed Sandbox turn cache record", {
1853
+ turnId,
1854
+ sessionId
1855
+ });
1856
+ }
1857
+ const messages = messagesOutcome.status === "fulfilled" ? messagesOutcome.value : [];
1858
+ const matchingMessages = completedMessagesForTurn(messages, turnId);
1859
+ const matchingCount = matchingMessages.length;
1860
+ const message = matchingCount === 1 ? matchingMessages[0] : null;
1861
+ if (matchingCount > 1) {
1862
+ log?.("[chat-routes] ignored ambiguous completed Sandbox session messages", {
1863
+ turnId,
1864
+ sessionId,
1865
+ matchingCount
1866
+ });
1867
+ }
1868
+ if (!cached && !message) return null;
1869
+ let result = cached ? asRecord(cached.result) : void 0;
1870
+ const latestConversationMessage = messages.filter((candidate) => candidate.role === "user" || candidate.role === "assistant").at(-1);
1871
+ if (!result && message && latestConversationMessage === message) {
1872
+ try {
1873
+ result = asRecord(await session.result());
1874
+ } catch (error) {
1875
+ log?.("[chat-routes] completed Sandbox session result lookup failed", {
1876
+ turnId,
1877
+ sessionId,
1878
+ error: String(error)
1879
+ });
1880
+ }
1881
+ }
1882
+ const recovered = message ? normalizedMessageParts(message) : null;
1883
+ const text = textFromResult(result) ?? recovered?.derivedText;
1884
+ const resultUsage = usageFromResult(result);
1885
+ const usage = { ...recovered?.usage ?? {}, ...resultUsage };
1886
+ const resultParts = Array.isArray(result?.parts) ? result.parts.map((part) => asRecord(part)).filter((part) => Boolean(part)).map((part) => normalizePersistedPart(part)).filter((part) => Boolean(part)) : null;
1887
+ const sourceParts = recovered?.parts ?? resultParts;
1888
+ const parts = sourceParts ? projectMessageParts(sourceParts, text ?? recovered?.derivedText ?? "") : void 0;
1889
+ return {
1890
+ ...text !== void 0 ? { text } : {},
1891
+ ...hasUsage2(usage) ? { usage } : {},
1892
+ ...parts ? { parts } : {}
1893
+ };
1894
+ }
1895
+
1728
1896
  // src/chat-routes/durable-projection.ts
1729
1897
  function errorMessage(error) {
1730
1898
  return error instanceof Error ? error.message : String(error);
@@ -2537,6 +2705,7 @@ export {
2537
2705
  parseFileMentions,
2538
2706
  promoteAgentFilePart,
2539
2707
  promptPartsByteSize,
2708
+ readCompletedSandboxTurn,
2540
2709
  reconcileStaleTurnLock,
2541
2710
  resolveChatAttachments,
2542
2711
  resolveEmptyTurnRetries,