@gajae-code/coding-agent 0.12.4 → 0.12.6

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 (139) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +4 -1
  3. package/dist/types/async/job-manager.d.ts +9 -1
  4. package/dist/types/eval/backend.d.ts +2 -2
  5. package/dist/types/exec/bash-executor.d.ts +10 -8
  6. package/dist/types/extensibility/extensions/runner.d.ts +9 -7
  7. package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
  8. package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
  9. package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
  10. package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
  11. package/dist/types/gjc-runtime/team-store.d.ts +0 -4
  12. package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
  13. package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
  14. package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
  15. package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
  16. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
  17. package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +83 -0
  18. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +57 -4
  19. package/dist/types/internal-urls/registry-helpers.d.ts +18 -0
  20. package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
  21. package/dist/types/modes/components/plugin-settings.d.ts +2 -0
  22. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  23. package/dist/types/sdk/broker/broker.d.ts +18 -0
  24. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
  25. package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
  26. package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -2
  27. package/dist/types/sdk/bus/index.d.ts +4 -0
  28. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -3
  29. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
  30. package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
  31. package/dist/types/sdk/host/host.d.ts +23 -2
  32. package/dist/types/sdk/host/reverse-leases.d.ts +7 -4
  33. package/dist/types/session/agent-session.d.ts +5 -4
  34. package/dist/types/session/artifacts.d.ts +8 -5
  35. package/dist/types/session/attempt-record-store.d.ts +19 -0
  36. package/dist/types/session/client-bridge.d.ts +14 -0
  37. package/dist/types/session/internal/managed-session-storage.d.ts +7 -23
  38. package/dist/types/session/messages.d.ts +4 -1
  39. package/dist/types/session/session-manager.d.ts +7 -5
  40. package/dist/types/session/session-storage.d.ts +8 -0
  41. package/dist/types/session/streaming-output.d.ts +32 -0
  42. package/dist/types/task/executor.d.ts +1 -1
  43. package/dist/types/task/index.d.ts +2 -1
  44. package/dist/types/task/receipt.d.ts +3 -0
  45. package/dist/types/task/types.d.ts +194 -0
  46. package/dist/types/task/worktree.d.ts +13 -6
  47. package/dist/types/tools/bash.d.ts +1 -1
  48. package/dist/types/tools/fetch.d.ts +3 -0
  49. package/dist/types/tools/output-meta.d.ts +37 -5
  50. package/dist/types/tools/subagent.d.ts +2 -0
  51. package/dist/types/tools/tool-result.d.ts +1 -0
  52. package/dist/types/utils/artifact-id.d.ts +2 -0
  53. package/dist/types/utils/git.d.ts +1 -0
  54. package/package.json +8 -8
  55. package/scripts/capture-sticky-viewport-showcase.ts +131 -5
  56. package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
  57. package/scripts/verify-sticky-viewport-showcase.ts +620 -93
  58. package/src/async/job-manager.ts +13 -5
  59. package/src/config/model-profiles.ts +8 -0
  60. package/src/config/settings-schema.ts +17 -3
  61. package/src/cursor.ts +20 -13
  62. package/src/defaults/gjc/skills/ultragoal/SKILL.md +5 -2
  63. package/src/eval/backend.ts +2 -2
  64. package/src/exec/bash-executor.ts +425 -151
  65. package/src/extensibility/extensions/runner.ts +80 -6
  66. package/src/extensibility/extensions/wrapper.ts +22 -15
  67. package/src/extensibility/runtime-skill-discovery.ts +4 -6
  68. package/src/gjc-runtime/launch-tmux.ts +309 -293
  69. package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
  70. package/src/gjc-runtime/psmux-detect.ts +52 -6
  71. package/src/gjc-runtime/team-launch.ts +50 -2
  72. package/src/gjc-runtime/team-runtime.ts +716 -251
  73. package/src/gjc-runtime/team-store.ts +25 -7
  74. package/src/gjc-runtime/team-workers.ts +161 -1
  75. package/src/gjc-runtime/tmux-common.ts +16 -4
  76. package/src/gjc-runtime/tmux-gc.ts +37 -5
  77. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
  78. package/src/gjc-runtime/tmux-provider-context.ts +451 -0
  79. package/src/gjc-runtime/tmux-sessions.ts +409 -156
  80. package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
  81. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
  82. package/src/gjc-runtime/ultragoal-review-source.ts +378 -0
  83. package/src/gjc-runtime/ultragoal-runtime.ts +640 -159
  84. package/src/gjc-runtime/workflow-manifest.generated.json +40 -1
  85. package/src/gjc-runtime/workflow-manifest.ts +12 -0
  86. package/src/internal-urls/agent-protocol.ts +98 -38
  87. package/src/internal-urls/artifact-protocol.ts +28 -17
  88. package/src/internal-urls/docs-index.generated.ts +6 -6
  89. package/src/internal-urls/local-protocol.ts +39 -18
  90. package/src/internal-urls/registry-helpers.ts +62 -0
  91. package/src/modes/acp/acp-agent.ts +51 -13
  92. package/src/modes/acp/acp-client-bridge.ts +154 -11
  93. package/src/modes/components/gjc-bundle-settings.ts +2 -0
  94. package/src/modes/components/plugin-settings.ts +5 -0
  95. package/src/modes/components/settings-selector.ts +4 -0
  96. package/src/modes/controllers/selector-controller.ts +1 -0
  97. package/src/prompts/tools/bash.md +2 -2
  98. package/src/prompts/tools/monitor.md +4 -4
  99. package/src/sdk/acp/adapter.ts +200 -9
  100. package/src/sdk/broker/broker.ts +93 -10
  101. package/src/sdk/broker/lifecycle-ledger.ts +227 -11
  102. package/src/sdk/broker/lifecycle.ts +692 -98
  103. package/src/sdk/bus/chat-daemon-control.ts +33 -5
  104. package/src/sdk/bus/index.ts +347 -14
  105. package/src/sdk/bus/telegram-daemon-contract.ts +7 -3
  106. package/src/sdk/bus/telegram-daemon.ts +283 -44
  107. package/src/sdk/bus/topic-registry.ts +133 -10
  108. package/src/sdk/host/host.ts +187 -9
  109. package/src/sdk/host/reverse-leases.ts +249 -42
  110. package/src/sdk/session.ts +8 -6
  111. package/src/session/agent-session.ts +930 -354
  112. package/src/session/artifacts.ts +211 -74
  113. package/src/session/attempt-record-store.ts +106 -0
  114. package/src/session/client-bridge.ts +15 -0
  115. package/src/session/internal/managed-session-scope.ts +332 -198
  116. package/src/session/internal/managed-session-storage.ts +162 -450
  117. package/src/session/messages.ts +32 -7
  118. package/src/session/session-manager.ts +31 -28
  119. package/src/session/session-storage.ts +190 -53
  120. package/src/session/streaming-output.ts +198 -40
  121. package/src/task/executor.ts +5 -2
  122. package/src/task/index.ts +277 -64
  123. package/src/task/provider-retry-status.ts +4 -1
  124. package/src/task/receipt.ts +26 -2
  125. package/src/task/render.ts +42 -1
  126. package/src/task/types.ts +52 -0
  127. package/src/task/worktree.ts +122 -43
  128. package/src/tools/bash.ts +818 -239
  129. package/src/tools/eval.ts +99 -4
  130. package/src/tools/fetch.ts +77 -15
  131. package/src/tools/find.ts +7 -3
  132. package/src/tools/monitor.ts +7 -1
  133. package/src/tools/output-meta.ts +706 -116
  134. package/src/tools/search.ts +9 -4
  135. package/src/tools/subagent-render.ts +5 -2
  136. package/src/tools/subagent.ts +8 -0
  137. package/src/tools/tool-result.ts +1 -0
  138. package/src/utils/artifact-id.ts +6 -0
  139. package/src/utils/git.ts +2 -0
@@ -15,19 +15,80 @@ import type { ImageContent, TextContent } from "@gajae-code/ai";
15
15
  import { getDefault, type Settings } from "../config/settings";
16
16
  import { formatGroupedDiagnosticMessages } from "../lsp/utils";
17
17
  import type { Theme } from "../modes/theme/theme";
18
- import { sessionArtifactCapability } from "../session/session-manager";
18
+ import { type SessionArtifactCapability, sessionArtifactCapability } from "../session/session-manager";
19
19
  import {
20
+ DEFAULT_ARTIFACT_MAX_BYTES,
20
21
  formatMiddleElisionMarker,
22
+ hasCurrentExecutionArtifactProof,
21
23
  type OutputSummary,
22
24
  type ReadWindow,
23
25
  type TruncationResult,
26
+ truncateHeadBytes,
24
27
  truncateMiddle,
25
28
  truncateTail,
26
29
  } from "../session/streaming-output";
30
+ import { isValidArtifactId } from "../utils/artifact-id";
31
+
32
+ export { isValidArtifactId } from "../utils/artifact-id";
27
33
 
28
34
  import { formatBytes, wrapBrackets } from "./render-utils";
29
35
  import { renderError } from "./tool-errors";
30
36
 
37
+ const kCurrentExecutionArtifactProof = Symbol("OutputMeta.CurrentExecutionArtifactProof");
38
+ const pendingArtifactLookupsByOwner = new WeakMap<SessionArtifactCapability, Set<Promise<boolean>>>();
39
+
40
+ function bindCurrentExecutionArtifactProof(truncation: TruncationMeta, summary: OutputSummary): void {
41
+ const artifactId = truncation.artifactId;
42
+ if (!artifactId || !hasCurrentExecutionArtifactProof(summary, artifactId, summary.output)) return;
43
+ Object.defineProperty(truncation, kCurrentExecutionArtifactProof, {
44
+ value: { artifactId, payload: summary.output, consumed: false },
45
+ enumerable: false,
46
+ configurable: false,
47
+ writable: false,
48
+ });
49
+ }
50
+
51
+ function hasBoundCurrentExecutionArtifactProof(
52
+ truncation: TruncationMeta,
53
+ artifactId: string,
54
+ payload: string,
55
+ ): boolean {
56
+ const proof = (
57
+ truncation as TruncationMeta & {
58
+ [kCurrentExecutionArtifactProof]?: { artifactId: string; payload: string; consumed: boolean };
59
+ }
60
+ )[kCurrentExecutionArtifactProof];
61
+ if (!proof || proof.artifactId !== artifactId || proof.payload !== payload || proof.consumed) return false;
62
+ proof.consumed = true;
63
+ return true;
64
+ }
65
+
66
+ function pendingArtifactLookupsForOwner(capability: SessionArtifactCapability): Set<Promise<boolean>> {
67
+ let pending = pendingArtifactLookupsByOwner.get(capability);
68
+ if (!pending) {
69
+ pending = new Set();
70
+ pendingArtifactLookupsByOwner.set(capability, pending);
71
+ }
72
+ return pending;
73
+ }
74
+
75
+ async function resolveArtifactForCurrentSessionBounded(
76
+ capability: SessionArtifactCapability | undefined,
77
+ artifactId: string,
78
+ ): Promise<boolean> {
79
+ if (!capability) return false;
80
+ const pending = pendingArtifactLookupsForOwner(capability);
81
+ if (pending.size >= INLINE_ARTIFACT_PENDING_LIMIT) return false;
82
+ const lookup = Promise.resolve()
83
+ .then(async () => (await capability.getArtifactPath(artifactId)) !== null)
84
+ .catch(() => false);
85
+ pending.add(lookup);
86
+ void lookup.then(
87
+ () => pending.delete(lookup),
88
+ () => pending.delete(lookup),
89
+ );
90
+ return Promise.race([lookup, Bun.sleep(INLINE_ARTIFACT_SAVE_WAIT_MS).then(() => false)]);
91
+ }
31
92
  /**
32
93
  * Truncation metadata for the output notice.
33
94
  */
@@ -55,8 +116,16 @@ export interface TruncationMeta {
55
116
  elidedLines?: number;
56
117
  /** Artifact ID when output was persisted; completeness is tracked separately below. */
57
118
  artifactId?: string;
119
+ /** Artifact existence was proven by a successful save or publisher receipt. */
120
+ artifactVerified?: boolean;
58
121
  /** Bytes omitted from an artifact after its hard storage cap was reached. */
59
122
  artifactTruncatedBytes?: number;
123
+ /** Bytes dropped before Bash received the native output stream. */
124
+ sourceTruncatedBytes?: number;
125
+ /** Exact source capture completeness could not be proven. */
126
+ sourceCaptureIncomplete?: boolean;
127
+ /** Bytes omitted from the visible output by per-line column capping. */
128
+ columnDroppedBytes?: number;
60
129
  /** Bounded diagnostic when artifact writer creation, write, or finalization failed. */
61
130
  artifactFailureDiagnostic?: string;
62
131
  /** Next offset for pagination (head truncation only) */
@@ -97,6 +166,8 @@ export interface OutputMeta {
97
166
  source?: SourceMeta;
98
167
  diagnostics?: DiagnosticMeta;
99
168
  limits?: LimitsMeta;
169
+ /** Diagnostics were omitted to preserve the configured inline byte cap. */
170
+ diagnosticsOmitted?: boolean;
100
171
  }
101
172
 
102
173
  // =============================================================================
@@ -108,6 +179,9 @@ export interface TruncationOptions {
108
179
  startLine?: number;
109
180
  totalFileLines?: number;
110
181
  artifactId?: string;
182
+ artifactVerified?: boolean;
183
+ artifactFailureDiagnostic?: string;
184
+ sourceCaptureIncomplete?: boolean;
111
185
  maxBytes?: number;
112
186
  noticeOwner?: "body";
113
187
  }
@@ -125,6 +199,8 @@ export interface TruncationTextOptions {
125
199
  totalBytes?: number;
126
200
  maxBytes?: number;
127
201
  artifactId?: string;
202
+ artifactVerified?: boolean;
203
+ sourceCaptureIncomplete?: boolean;
128
204
  noticeOwner?: "body";
129
205
  }
130
206
 
@@ -147,7 +223,17 @@ export class OutputMetaBuilder {
147
223
  truncation(result: TruncationResult, options: TruncationOptions): this {
148
224
  if (!result.truncated) return this;
149
225
 
150
- const { direction, startLine = 1, totalFileLines, artifactId, noticeOwner, maxBytes } = options;
226
+ const {
227
+ direction,
228
+ startLine = 1,
229
+ totalFileLines,
230
+ artifactId,
231
+ artifactVerified,
232
+ artifactFailureDiagnostic,
233
+ sourceCaptureIncomplete,
234
+ noticeOwner,
235
+ maxBytes,
236
+ } = options;
151
237
  const outputLines = result.outputLines ?? result.totalLines;
152
238
  const outputBytes = result.outputBytes ?? result.totalBytes;
153
239
  const isMiddle = direction === "middle" || result.truncatedBy === "middle";
@@ -163,12 +249,6 @@ export class OutputMetaBuilder {
163
249
  if (isMiddle) {
164
250
  const elidedLines = result.elidedLines ?? Math.max(0, effectiveTotalLines - outputLines);
165
251
  const elidedBytes = result.elidedBytes ?? Math.max(0, result.totalBytes - outputBytes);
166
- // Reconstruct head/tail line ranges. The kept output spans the first
167
- // `headLines` lines and the last `tailLines` lines of the source; lines
168
- // in the middle (count == elidedLines) are dropped.
169
- const keptLines = Math.max(0, outputLines - 1); // -1 for marker line
170
- const headLines = Math.ceil(keptLines / 2);
171
- const tailLines = keptLines - headLines;
172
252
  this.#meta.truncation = {
173
253
  direction: "middle",
174
254
  truncatedBy: "middle",
@@ -178,12 +258,14 @@ export class OutputMetaBuilder {
178
258
  outputLines,
179
259
  outputBytes,
180
260
  ...(maxBytes !== undefined ? { maxBytes } : {}),
181
- headRange: headLines > 0 ? { start: 1, end: headLines } : undefined,
182
- tailRange:
183
- tailLines > 0 ? { start: effectiveTotalLines - tailLines + 1, end: effectiveTotalLines } : undefined,
261
+ headRange: result.firstLinePartial ? undefined : result.headRange,
262
+ tailRange: result.lastLinePartial ? undefined : result.tailRange,
184
263
  elidedLines,
185
264
  elidedBytes,
186
265
  artifactId,
266
+ artifactVerified,
267
+ artifactFailureDiagnostic,
268
+ sourceCaptureIncomplete: sourceCaptureIncomplete || undefined,
187
269
  };
188
270
  return this;
189
271
  }
@@ -208,8 +290,11 @@ export class OutputMetaBuilder {
208
290
  outputLines,
209
291
  outputBytes,
210
292
  ...(maxBytes !== undefined ? { maxBytes } : {}),
211
- shownRange: { start: shownStart, end: shownEnd },
293
+ shownRange: direction === "tail" && result.lastLinePartial ? undefined : { start: shownStart, end: shownEnd },
212
294
  artifactId,
295
+ artifactVerified,
296
+ artifactFailureDiagnostic,
297
+ sourceCaptureIncomplete: sourceCaptureIncomplete || undefined,
213
298
  nextOffset: direction === "head" ? shownEnd + 1 : undefined,
214
299
  };
215
300
 
@@ -221,6 +306,7 @@ export class OutputMetaBuilder {
221
306
  windows: ReadWindow,
222
307
  options: {
223
308
  artifactId?: string;
309
+ artifactVerified?: boolean;
224
310
  noticeOwner?: "body";
225
311
  maxBytes?: number;
226
312
  rangeBase?: "file" | "window";
@@ -228,7 +314,7 @@ export class OutputMetaBuilder {
228
314
  ): this {
229
315
  if (windows.kind === "full") return this;
230
316
 
231
- const { artifactId, noticeOwner, rangeBase } = options;
317
+ const { artifactId, artifactVerified, noticeOwner, rangeBase } = options;
232
318
  const maxBytes = options.maxBytes ?? (windows as ReadWindow & { maxBytes?: number }).maxBytes;
233
319
  const outputLinesOverride = (windows as ReadWindow & { outputLinesOverride?: number }).outputLinesOverride;
234
320
  const outputBytesOverride = (windows as ReadWindow & { outputBytesOverride?: number }).outputBytesOverride;
@@ -264,6 +350,7 @@ export class OutputMetaBuilder {
264
350
  sourceBytes: partialTail.sourceLineBytes,
265
351
  },
266
352
  artifactId,
353
+ artifactVerified,
267
354
  };
268
355
  return this;
269
356
  }
@@ -282,6 +369,7 @@ export class OutputMetaBuilder {
282
369
  ...(maxBytes !== undefined ? { maxBytes } : {}),
283
370
  shownRange: { start: head.origin.startLine, end: head.origin.endLine },
284
371
  artifactId,
372
+ artifactVerified,
285
373
  nextOffset: head.origin.endLine + 1,
286
374
  };
287
375
  return this;
@@ -301,6 +389,7 @@ export class OutputMetaBuilder {
301
389
  ...(maxBytes !== undefined ? { maxBytes } : {}),
302
390
  shownRange: { start: tail.origin.startLine, end: tail.origin.endLine },
303
391
  artifactId,
392
+ artifactVerified,
304
393
  };
305
394
  return this;
306
395
  }
@@ -319,10 +408,12 @@ export class OutputMetaBuilder {
319
408
  outputBytes: head.bytes + tail.bytes + Buffer.byteLength(marker, "utf-8") + 2,
320
409
  ...(maxBytes !== undefined ? { maxBytes } : {}),
321
410
  headRange: { start: head.origin.startLine, end: head.origin.endLine },
322
- tailRange: { start: tail.origin.startLine, end: tail.origin.endLine },
411
+ tailRange:
412
+ tail.kind === "partial-line" ? undefined : { start: tail.origin.startLine, end: tail.origin.endLine },
323
413
  elidedLines: windows.elidedLines,
324
414
  elidedBytes: windows.elidedBytes,
325
415
  artifactId,
416
+ artifactVerified,
326
417
  };
327
418
  return this;
328
419
  }
@@ -330,14 +421,45 @@ export class OutputMetaBuilder {
330
421
  /** Add truncation info from OutputSummary. No-op if not truncated or artifact evidence is absent. */
331
422
  truncationFromSummary(summary: OutputSummary, options: TruncationSummaryOptions): this {
332
423
  const artifactFailureDiagnostic = summary.artifactFailureDiagnostic;
424
+ const artifactLossValid = isValidLossCounter(summary.artifactTruncatedBytes);
425
+ const sourceLossValid = isValidLossCounter(summary.sourceTruncatedBytes);
426
+ const columnLossValid =
427
+ isValidLossCounter(summary.columnDroppedBytes) &&
428
+ isValidLossCounter(summary.columnTruncatedLines) &&
429
+ isValidLossCounter(summary.columnMax);
430
+ const sourceCaptureFlagValid =
431
+ summary.sourceCaptureIncomplete === undefined || typeof summary.sourceCaptureIncomplete === "boolean";
333
432
  const artifactTruncatedBytes =
334
- summary.artifactTruncatedBytes != null && summary.artifactTruncatedBytes > 0
433
+ artifactLossValid && summary.artifactTruncatedBytes != null && summary.artifactTruncatedBytes > 0
335
434
  ? summary.artifactTruncatedBytes
336
435
  : undefined;
436
+ const sourceTruncatedBytes =
437
+ sourceLossValid && summary.sourceTruncatedBytes != null && summary.sourceTruncatedBytes > 0
438
+ ? summary.sourceTruncatedBytes
439
+ : undefined;
440
+ const columnDroppedBytes =
441
+ columnLossValid && summary.columnDroppedBytes != null && summary.columnDroppedBytes > 0
442
+ ? summary.columnDroppedBytes
443
+ : undefined;
444
+ const sourceCaptureIncomplete =
445
+ summary.sourceCaptureIncomplete === true ||
446
+ !sourceCaptureFlagValid ||
447
+ !artifactLossValid ||
448
+ !sourceLossValid ||
449
+ !columnLossValid
450
+ ? true
451
+ : undefined;
452
+ if (columnLossValid && (summary.columnTruncatedLines ?? 0) > 0 && (summary.columnMax ?? 0) > 0) {
453
+ this.columnTruncated(summary.columnMax ?? 0);
454
+ }
455
+ const sourceCoordinatesIncomplete =
456
+ sourceTruncatedBytes !== undefined || sourceCaptureIncomplete === true || columnDroppedBytes !== undefined;
337
457
  const hasArtifactEvidence =
338
458
  summary.artifactId !== undefined ||
339
459
  artifactFailureDiagnostic !== undefined ||
340
- artifactTruncatedBytes !== undefined;
460
+ artifactTruncatedBytes !== undefined ||
461
+ sourceTruncatedBytes !== undefined ||
462
+ sourceCaptureIncomplete !== undefined;
341
463
  if (!summary.truncated && !hasArtifactEvidence) return this;
342
464
 
343
465
  const { direction, startLine = 1, totalFileLines, noticeOwner } = options;
@@ -346,18 +468,26 @@ export class OutputMetaBuilder {
346
468
  summary.artifactId !== undefined && summary.output.includes(`artifact://${summary.artifactId}`);
347
469
  const bodyOwnsArtifact =
348
470
  bodyHasArtifact &&
349
- (artifactTruncatedBytes === undefined ||
350
- (summary.artifactId !== undefined &&
351
- summary.output.includes(formatArtifactReference(summary.artifactId, artifactTruncatedBytes))));
471
+ (artifactTruncatedBytes === undefined &&
472
+ sourceTruncatedBytes === undefined &&
473
+ sourceCaptureIncomplete === undefined &&
474
+ summary.columnDroppedBytes === undefined
475
+ ? true
476
+ : summary.artifactId !== undefined &&
477
+ summary.output.includes(
478
+ formatArtifactReference(
479
+ summary.artifactId,
480
+ artifactTruncatedBytes,
481
+ sourceTruncatedBytes,
482
+ sourceCaptureIncomplete,
483
+ ),
484
+ ));
352
485
  const owner =
353
486
  noticeOwner !== undefined ? { noticeOwner } : bodyOwnsArtifact ? { noticeOwner: "body" as const } : {};
354
487
 
355
488
  // Middle elision: the sink retained head + tail with an elision marker.
356
489
  if (summary.elidedBytes != null && summary.elidedBytes > 0) {
357
490
  const elidedLines = summary.elidedLines ?? Math.max(0, totalLines - summary.outputLines);
358
- const keptLines = Math.max(0, summary.outputLines - 1); // -1 for marker line
359
- const headLines = Math.ceil(keptLines / 2);
360
- const tailLines = keptLines - headLines;
361
491
  this.#meta.truncation = {
362
492
  direction: "middle",
363
493
  truncatedBy: "middle",
@@ -366,14 +496,19 @@ export class OutputMetaBuilder {
366
496
  totalBytes: summary.totalBytes,
367
497
  outputLines: summary.outputLines,
368
498
  outputBytes: summary.outputBytes,
369
- headRange: headLines > 0 ? { start: 1, end: headLines } : undefined,
370
- tailRange: tailLines > 0 ? { start: totalLines - tailLines + 1, end: totalLines } : undefined,
499
+ headRange: sourceCoordinatesIncomplete || summary.firstLinePartial ? undefined : summary.headRange,
500
+ tailRange: sourceCoordinatesIncomplete || summary.lastLinePartial ? undefined : summary.tailRange,
371
501
  elidedBytes: summary.elidedBytes,
372
502
  elidedLines,
373
503
  artifactId: summary.artifactId,
504
+ artifactVerified: summary.artifactVerified,
374
505
  artifactTruncatedBytes,
506
+ sourceTruncatedBytes,
507
+ sourceCaptureIncomplete,
508
+ columnDroppedBytes,
375
509
  artifactFailureDiagnostic,
376
510
  };
511
+ bindCurrentExecutionArtifactProof(this.#meta.truncation, summary);
377
512
  return this;
378
513
  }
379
514
 
@@ -403,12 +538,20 @@ export class OutputMetaBuilder {
403
538
  totalBytes: summary.totalBytes,
404
539
  outputLines: summary.outputLines,
405
540
  outputBytes: summary.outputBytes,
406
- shownRange: { start: shownStart, end: shownEnd },
541
+ shownRange:
542
+ sourceCoordinatesIncomplete || (direction === "tail" && summary.lastLinePartial)
543
+ ? undefined
544
+ : { start: shownStart, end: shownEnd },
407
545
  artifactId: summary.artifactId,
546
+ artifactVerified: summary.artifactVerified,
408
547
  artifactTruncatedBytes,
548
+ sourceTruncatedBytes,
549
+ sourceCaptureIncomplete,
550
+ columnDroppedBytes,
409
551
  artifactFailureDiagnostic,
410
- nextOffset: direction === "head" ? shownEnd + 1 : undefined,
552
+ nextOffset: direction === "head" && !sourceCoordinatesIncomplete ? shownEnd + 1 : undefined,
411
553
  };
554
+ bindCurrentExecutionArtifactProof(this.#meta.truncation, summary);
412
555
 
413
556
  return this;
414
557
  }
@@ -453,8 +596,13 @@ export class OutputMetaBuilder {
453
596
  outputBytes,
454
597
  maxBytes: options.maxBytes,
455
598
  artifactId: options.artifactId,
456
- shownRange: { start: shownStart, end: shownEnd },
457
- nextOffset: options.direction === "head" ? shownEnd + 1 : undefined,
599
+ artifactVerified: options.artifactVerified,
600
+ shownRange:
601
+ options.sourceCaptureIncomplete || options.direction === "middle"
602
+ ? undefined
603
+ : { start: shownStart, end: shownEnd },
604
+ sourceCaptureIncomplete: options.sourceCaptureIncomplete || undefined,
605
+ nextOffset: options.direction === "head" && !options.sourceCaptureIncomplete ? shownEnd + 1 : undefined,
458
606
  };
459
607
 
460
608
  return this;
@@ -549,33 +697,196 @@ export function formatFullOutputReference(artifactId: string): string {
549
697
  return `Read artifact://${artifactId} for full output`;
550
698
  }
551
699
 
552
- /**
553
- * Format an artifact reference without claiming completeness when storage was hard-capped.
554
- */
555
- export function formatArtifactReference(artifactId: string, artifactTruncatedBytes?: number): string {
700
+ function boundArtifactFailureDiagnostic(error: unknown): string {
701
+ const message = (error instanceof Error ? error.message : String(error)).replace(/\s+/gu, " ").trim();
702
+ return truncateHeadBytes(message || "unknown storage error", 512).text;
703
+ }
704
+
705
+ const INLINE_ARTIFACT_SAVE_WAIT_MS = 500;
706
+ const INLINE_ARTIFACT_PENDING_LIMIT = 64;
707
+ const pendingInlineArtifactSavesByOwner = new WeakMap<SessionArtifactCapability, Set<Promise<unknown>>>();
708
+
709
+ function pendingInlineArtifactSavesForOwner(capability: SessionArtifactCapability): Set<Promise<unknown>> {
710
+ let pending = pendingInlineArtifactSavesByOwner.get(capability);
711
+ if (!pending) {
712
+ pending = new Set<Promise<unknown>>();
713
+ pendingInlineArtifactSavesByOwner.set(capability, pending);
714
+ }
715
+ return pending;
716
+ }
717
+
718
+ async function saveInlineArtifactBounded(
719
+ capability: SessionArtifactCapability,
720
+ save: () => Promise<string | null | undefined>,
721
+ resolvePath: (artifactId: string) => Promise<string | null>,
722
+ ): Promise<
723
+ { status: "ok"; artifactId: string; artifactTruncatedBytes?: number } | { status: "failed"; diagnostic: string }
724
+ > {
725
+ const pendingInlineArtifactSaves = pendingInlineArtifactSavesForOwner(capability);
726
+ if (pendingInlineArtifactSaves.size >= INLINE_ARTIFACT_PENDING_LIMIT) {
727
+ return { status: "failed", diagnostic: "pending artifact save limit reached for this session" };
728
+ }
729
+ const promise = Promise.resolve()
730
+ .then(save)
731
+ .then(async artifactId => {
732
+ if (!isValidArtifactId(artifactId)) {
733
+ return { status: "failed", diagnostic: "storage returned an invalid artifact id" } as const;
734
+ }
735
+ const artifactPath = await resolvePath(artifactId);
736
+ if (!artifactPath) {
737
+ return { status: "failed", diagnostic: "saved artifact is not protocol-resolvable" } as const;
738
+ }
739
+ return { status: "ok", artifactId } as const;
740
+ })
741
+ .catch(error => ({ status: "failed", diagnostic: boundArtifactFailureDiagnostic(error) }) as const);
742
+ pendingInlineArtifactSaves.add(promise);
743
+ void promise.then(
744
+ () => pendingInlineArtifactSaves.delete(promise),
745
+ () => pendingInlineArtifactSaves.delete(promise),
746
+ );
747
+ return Promise.race([
748
+ promise,
749
+ Bun.sleep(INLINE_ARTIFACT_SAVE_WAIT_MS).then(
750
+ () => ({ status: "failed", diagnostic: `did not settle within ${INLINE_ARTIFACT_SAVE_WAIT_MS}ms` }) as const,
751
+ ),
752
+ ]);
753
+ }
754
+
755
+ function genericArtifactOmittedBytes(content: string): number | undefined {
756
+ const contentBytes = Buffer.byteLength(content, "utf-8");
757
+ if (contentBytes <= DEFAULT_ARTIFACT_MAX_BYTES) return undefined;
758
+ const retained = truncateHeadBytes(content, DEFAULT_ARTIFACT_MAX_BYTES);
759
+ return contentBytes - retained.bytes;
760
+ }
761
+
762
+ const ARTIFACT_FAILURE_PREVIEW_DEFAULT_BYTES = 8 * 1024;
763
+
764
+ export interface ArtifactFailurePreview {
765
+ text: string;
766
+ omittedBytes: number;
767
+ }
768
+
769
+ /** Build a bounded, UTF-8-safe preview when artifact storage cannot retain the full payload. */
770
+ export function createArtifactFailurePreview(
771
+ fullText: string,
772
+ maxInlineBytes: number | undefined,
773
+ diagnostic: string,
774
+ ): ArtifactFailurePreview {
775
+ const totalBytes = Buffer.byteLength(fullText, "utf-8");
776
+ const previewLimit =
777
+ maxInlineBytes !== undefined && maxInlineBytes > 0 ? maxInlineBytes : ARTIFACT_FAILURE_PREVIEW_DEFAULT_BYTES;
778
+ const boundedDiagnostic = boundArtifactFailureDiagnostic(diagnostic);
779
+ let bodyBudget = Math.min(Math.max(0, totalBytes - 1), previewLimit);
780
+ for (let attempt = 0; attempt < 8; attempt++) {
781
+ const body = truncateTail(fullText, { maxBytes: bodyBudget }).content;
782
+ const bodyBytes = Buffer.byteLength(body, "utf-8");
783
+ const omittedBytes = Math.max(1, totalBytes - bodyBytes);
784
+ const marker = `\n\n[${omittedBytes} bytes omitted; no artifact available: ${boundedDiagnostic}]\n\n`;
785
+ const markerBytes = Buffer.byteLength(marker, "utf-8");
786
+ if (bodyBytes + markerBytes <= previewLimit) {
787
+ return { text: `${body}${marker}`, omittedBytes };
788
+ }
789
+ bodyBudget = Math.max(0, Math.min(bodyBudget - 1, previewLimit - markerBytes));
790
+ }
791
+ const marker = `\n\n[${totalBytes} bytes omitted; no artifact available: ${boundedDiagnostic}]\n\n`;
792
+ return { text: truncateHeadBytes(marker, previewLimit).text, omittedBytes: totalBytes };
793
+ }
794
+
795
+ /** Format an artifact reference without claiming completeness when capture or storage omitted bytes. */
796
+ export function formatArtifactReference(
797
+ artifactId: string,
798
+ artifactTruncatedBytes?: number,
799
+ sourceTruncatedBytes?: number,
800
+ sourceCaptureIncomplete?: boolean,
801
+ ): string {
802
+ const omissions: string[] = [];
803
+ if (!isValidLossCounter(sourceTruncatedBytes) || !isValidLossCounter(artifactTruncatedBytes)) {
804
+ omissions.push("loss accounting was invalid");
805
+ }
806
+ if (sourceTruncatedBytes != null && sourceTruncatedBytes > 0) {
807
+ omissions.push(`at least ${formatBytes(sourceTruncatedBytes)} dropped before Bash capture`);
808
+ }
809
+ if (sourceCaptureIncomplete) {
810
+ omissions.push("source capture completeness could not be proven");
811
+ }
556
812
  if (artifactTruncatedBytes != null && artifactTruncatedBytes > 0) {
557
- return `Read artifact://${artifactId} for retained output (at least ${formatBytes(artifactTruncatedBytes)} omitted by the artifact storage cap)`;
813
+ omissions.push(`at least ${formatBytes(artifactTruncatedBytes)} omitted by the artifact storage cap`);
558
814
  }
559
- return formatFullOutputReference(artifactId);
815
+ return omissions.length > 0
816
+ ? `Read artifact://${artifactId} for retained output (${omissions.join("; ")})`
817
+ : formatFullOutputReference(artifactId);
560
818
  }
561
819
 
562
- function formatTruncationArtifactNotice(truncation: TruncationMeta): string {
563
- const reference = truncation.artifactId
564
- ? formatArtifactReference(truncation.artifactId, truncation.artifactTruncatedBytes)
565
- : undefined;
566
- if (truncation.artifactFailureDiagnostic) {
567
- const failure = `Artifact storage failed: ${truncation.artifactFailureDiagnostic}`;
568
- if (reference) return `${reference}; ${failure}`;
569
- return failure;
820
+ export interface ArtifactEvidence {
821
+ artifactId?: string;
822
+ artifactVerified?: boolean;
823
+ artifactTruncatedBytes?: number;
824
+ sourceTruncatedBytes?: number;
825
+ sourceCaptureIncomplete?: boolean;
826
+ artifactFailureDiagnostic?: string;
827
+ columnDroppedBytes?: number;
828
+ }
829
+
830
+ export function formatArtifactEvidenceNotice(evidence: ArtifactEvidence): string | undefined {
831
+ const parts: string[] = [];
832
+ if (
833
+ isValidArtifactId(evidence.artifactId) &&
834
+ evidence.artifactVerified === true &&
835
+ !evidence.artifactFailureDiagnostic
836
+ ) {
837
+ parts.push(
838
+ formatArtifactReference(
839
+ evidence.artifactId,
840
+ evidence.artifactTruncatedBytes,
841
+ evidence.sourceTruncatedBytes,
842
+ evidence.sourceCaptureIncomplete || (evidence.columnDroppedBytes ?? 0) > 0,
843
+ ),
844
+ );
845
+ if ((evidence.columnDroppedBytes ?? 0) > 0) {
846
+ parts.push(`Visible line caps omitted ${formatBytes(evidence.columnDroppedBytes ?? 0)}`);
847
+ }
848
+ } else {
849
+ if (evidence.artifactId && evidence.artifactVerified !== true) {
850
+ parts.push("Artifact availability could not be proven");
851
+ }
852
+ if ((evidence.sourceTruncatedBytes ?? 0) > 0) {
853
+ parts.push(
854
+ `Bash capture omitted at least ${formatBytes(evidence.sourceTruncatedBytes ?? 0)} before artifact storage`,
855
+ );
856
+ }
857
+ if (evidence.sourceCaptureIncomplete) {
858
+ parts.push("Source capture completeness could not be proven before retained output was finalized");
859
+ }
860
+ if ((evidence.artifactTruncatedBytes ?? 0) > 0) {
861
+ parts.push(`Artifact storage omitted at least ${formatBytes(evidence.artifactTruncatedBytes ?? 0)}`);
862
+ }
863
+ if ((evidence.columnDroppedBytes ?? 0) > 0) {
864
+ parts.push(`Visible line caps omitted ${formatBytes(evidence.columnDroppedBytes ?? 0)}`);
865
+ }
866
+ if (parts.length > 0) {
867
+ parts.push("no artifact reference is available");
868
+ }
869
+ }
870
+ if (evidence.artifactFailureDiagnostic) {
871
+ parts.push(`Artifact storage failed: ${evidence.artifactFailureDiagnostic}`);
872
+ }
873
+ if (!evidence.artifactId && parts.length > 0 && !parts.includes("no artifact reference is available")) {
874
+ parts.push("no artifact reference is available");
570
875
  }
571
- if (reference) return reference;
572
- return `Artifact storage omitted at least ${formatBytes(truncation.artifactTruncatedBytes ?? 0)}; no artifact reference is available`;
876
+ return parts.length > 0 ? parts.join("; ") : undefined;
877
+ }
878
+
879
+ function formatTruncationArtifactNotice(truncation: TruncationMeta): string {
880
+ return formatArtifactEvidenceNotice(truncation) ?? "";
573
881
  }
574
882
 
575
883
  function hasArtifactNotice(truncation: TruncationMeta): boolean {
576
884
  return (
577
885
  truncation.artifactId != null ||
578
886
  (truncation.artifactTruncatedBytes ?? 0) > 0 ||
887
+ (truncation.sourceTruncatedBytes ?? 0) > 0 ||
888
+ truncation.sourceCaptureIncomplete ||
889
+ (truncation.columnDroppedBytes ?? 0) > 0 ||
579
890
  truncation.artifactFailureDiagnostic != null
580
891
  );
581
892
  }
@@ -588,6 +899,16 @@ function formatTruncationRangeTotal(truncation: TruncationMeta): string {
588
899
 
589
900
  export function formatTruncationMetaNotice(truncation: TruncationMeta): string {
590
901
  const rangeTotal = formatTruncationRangeTotal(truncation);
902
+ if ((truncation.sourceTruncatedBytes ?? 0) > 0 || truncation.sourceCaptureIncomplete) {
903
+ let notice = `Showing ${truncation.outputLines} retained line${truncation.outputLines === 1 ? "" : "s"} from an incomplete Bash capture`;
904
+ if (truncation.truncatedBy === "bytes" && truncation.maxBytes != null) {
905
+ notice += ` (${formatBytes(truncation.maxBytes)} limit)`;
906
+ }
907
+ if (hasArtifactNotice(truncation)) {
908
+ notice += `. ${formatTruncationArtifactNotice(truncation)}`;
909
+ }
910
+ return notice;
911
+ }
591
912
  if (truncation.partialLine) {
592
913
  let notice = `Showing last ${formatBytes(truncation.partialLine.bytes)} of line ${truncation.partialLine.line} of ${rangeTotal}`;
593
914
  if (truncation.partialLine.sourceBytes > truncation.partialLine.bytes) {
@@ -614,7 +935,7 @@ export function formatTruncationMetaNotice(truncation: TruncationMeta): string {
614
935
  } else if (headPart && tailPart) {
615
936
  notice = `Showing ${headPart} and ${tailPart} of ${rangeTotal}; ${elidedLines.toLocaleString()} middle line${elidedLines === 1 ? "" : "s"} (${formatBytes(elidedBytes)}) elided`;
616
937
  } else {
617
- notice = `Showing ${truncation.outputLines} of ${rangeTotal}${truncation.rangeBase === "window" ? "" : " lines"}; ${formatBytes(elidedBytes)} middle bytes elided`;
938
+ notice = `Showing retained head and tail fragments of ${rangeTotal}; ${formatBytes(elidedBytes)} middle bytes elided`;
618
939
  }
619
940
  if (hasArtifactNotice(truncation)) {
620
941
  notice += `. ${formatTruncationArtifactNotice(truncation)}`;
@@ -688,8 +1009,12 @@ export function formatOutputNotice(meta: OutputMeta | undefined): string {
688
1009
  let diagnosticsNotice = "";
689
1010
  if (meta.diagnostics && meta.diagnostics.messages.length > 0) {
690
1011
  const d = meta.diagnostics;
691
- diagnosticsNotice = `\n\nLSP Diagnostics (${d.summary}):\n${formatGroupedDiagnosticMessages(d.messages)}`;
1012
+ const grouped = formatGroupedDiagnosticMessages(d.messages);
1013
+ const bounded = truncateHeadBytes(grouped, 1024);
1014
+ const suffix = bounded.bytes < Buffer.byteLength(grouped, "utf-8") ? "\n[diagnostics truncated]" : "";
1015
+ diagnosticsNotice = `\n\nLSP Diagnostics (${d.summary}):\n${bounded.text}${suffix}`;
692
1016
  }
1017
+ if (meta.diagnosticsOmitted) parts.push("Diagnostics omitted by inline result cap");
693
1018
 
694
1019
  const notice = parts.length ? `\n\n[${parts.join(". ")}]` : "";
695
1020
  return notice + diagnosticsNotice;
@@ -791,13 +1116,15 @@ function getSpillConfig(s: Settings | undefined) {
791
1116
  | "tools.maxInlineResultBytes"
792
1117
  | "tools.readArtifactSpillThreshold";
793
1118
  const get = <P extends Path>(path: P) => s?.get(path) ?? getDefault(path);
1119
+ const configuredInlineKiB = get("tools.maxInlineResultBytes");
1120
+ const maxInlineBytes = configuredInlineKiB > 0 ? Math.max(1024, Math.floor(configuredInlineKiB * 1024)) : 0;
794
1121
  return {
795
1122
  threshold: get("tools.artifactSpillThreshold") * 1024,
796
1123
  readThreshold: get("tools.readArtifactSpillThreshold") * 1024,
797
1124
  tailBytes: get("tools.artifactTailBytes") * 1024,
798
1125
  tailLines: get("tools.artifactTailLines"),
799
1126
  headBytes: get("tools.artifactHeadBytes") * 1024,
800
- maxInlineBytes: get("tools.maxInlineResultBytes") * 1024,
1127
+ maxInlineBytes,
801
1128
  };
802
1129
  }
803
1130
 
@@ -818,7 +1145,12 @@ export const BASH_DEFAULT_OUTPUT_TAIL_BYTES = 1024;
818
1145
  * budget still wins.
819
1146
  */
820
1147
  export function resolveBashOutputSinkTailBytes(s: Settings): number {
821
- return s.has("tools.artifactTailBytes") ? s.get("tools.artifactTailBytes") * 1024 : BASH_DEFAULT_OUTPUT_TAIL_BYTES;
1148
+ const configuredTailBytes = s.get("tools.artifactTailBytes");
1149
+ const hasExplicitTailBytes =
1150
+ typeof s.has === "function" ? s.has("tools.artifactTailBytes") : configuredTailBytes !== undefined;
1151
+ return hasExplicitTailBytes && configuredTailBytes !== undefined
1152
+ ? configuredTailBytes * 1024
1153
+ : BASH_DEFAULT_OUTPUT_TAIL_BYTES;
822
1154
  }
823
1155
  /**
824
1156
  * Bash keeps only the tail unless the user explicitly opts into the shared
@@ -826,7 +1158,111 @@ export function resolveBashOutputSinkTailBytes(s: Settings): number {
826
1158
  * tools without silently turning Bash back into middle-elision mode.
827
1159
  */
828
1160
  export function resolveBashOutputSinkHeadBytes(s: Settings): number {
829
- return s.has("tools.artifactHeadBytes") ? resolveOutputSinkHeadBytes(s) : 0;
1161
+ const configuredHeadBytes = s.get("tools.artifactHeadBytes");
1162
+ const hasExplicitHeadBytes =
1163
+ typeof s.has === "function" ? s.has("tools.artifactHeadBytes") : configuredHeadBytes !== undefined;
1164
+ return hasExplicitHeadBytes && configuredHeadBytes !== undefined ? configuredHeadBytes * 1024 : 0;
1165
+ }
1166
+
1167
+ function isValidLossCounter(value: number | undefined): boolean {
1168
+ return value === undefined || (Number.isSafeInteger(value) && value >= 0);
1169
+ }
1170
+
1171
+ function stripBodyArtifactReferences(result: AgentToolResult, artifactId: string): AgentToolResult["content"] {
1172
+ return result.content.map(block => {
1173
+ if (block.type !== "text") return block;
1174
+ const lines = block.text.split("\n");
1175
+ const kept = lines.filter(line => {
1176
+ const trimmed = line.trim();
1177
+ return !(
1178
+ (trimmed.startsWith("[raw output:") && trimmed.includes(`artifact://${artifactId}`)) ||
1179
+ trimmed.startsWith(`Read artifact://${artifactId} `)
1180
+ );
1181
+ });
1182
+ return kept.length === lines.length ? block : { ...block, text: kept.join("\n") };
1183
+ });
1184
+ }
1185
+
1186
+ async function sanitizeArtifactIdEvidence(
1187
+ result: AgentToolResult,
1188
+ context: AgentToolContext | undefined,
1189
+ ): Promise<AgentToolResult> {
1190
+ const meta: OutputMeta | undefined = result.details?.meta;
1191
+ const truncation = meta?.truncation;
1192
+ if (!truncation) return result;
1193
+ const artifactId = truncation.artifactId;
1194
+ const artifactIdValid = artifactId === undefined || isValidArtifactId(artifactId);
1195
+ const countersValid =
1196
+ isValidLossCounter(truncation.artifactTruncatedBytes) &&
1197
+ isValidLossCounter(truncation.sourceTruncatedBytes) &&
1198
+ isValidLossCounter(truncation.columnDroppedBytes);
1199
+ const sourceCaptureFlagValid =
1200
+ truncation.sourceCaptureIncomplete === undefined || typeof truncation.sourceCaptureIncomplete === "boolean";
1201
+ const sourceCoordinatesIncomplete =
1202
+ !countersValid ||
1203
+ !sourceCaptureFlagValid ||
1204
+ truncation.sourceCaptureIncomplete === true ||
1205
+ (truncation.sourceTruncatedBytes ?? 0) > 0;
1206
+ const artifactCapability = artifactCapabilityForContext(context);
1207
+ const artifactResolvable =
1208
+ artifactId === undefined ? true : await resolveArtifactForCurrentSessionBounded(artifactCapability, artifactId);
1209
+ const resultPayload = result.content.map(block => (block.type === "text" ? block.text : "")).join("");
1210
+ const artifactProven =
1211
+ artifactId === undefined ||
1212
+ (truncation.artifactVerified === true &&
1213
+ hasBoundCurrentExecutionArtifactProof(truncation, artifactId, resultPayload) &&
1214
+ artifactResolvable);
1215
+ const artifactFailed = truncation.artifactFailureDiagnostic !== undefined;
1216
+ const invalidateArtifact = artifactId !== undefined && (!artifactIdValid || !artifactProven || artifactFailed);
1217
+ const staleFullClaim =
1218
+ artifactId !== undefined &&
1219
+ (sourceCoordinatesIncomplete ||
1220
+ (truncation.artifactTruncatedBytes ?? 0) > 0 ||
1221
+ (truncation.columnDroppedBytes ?? 0) > 0);
1222
+ if (
1223
+ artifactIdValid &&
1224
+ countersValid &&
1225
+ sourceCaptureFlagValid &&
1226
+ !sourceCoordinatesIncomplete &&
1227
+ !invalidateArtifact &&
1228
+ !staleFullClaim
1229
+ )
1230
+ return result;
1231
+ const diagnostic = !artifactIdValid
1232
+ ? "existing metadata contained an invalid artifact id"
1233
+ : artifactFailed
1234
+ ? truncation.artifactFailureDiagnostic
1235
+ : !artifactProven
1236
+ ? artifactResolvable
1237
+ ? "existing metadata did not prove artifact availability"
1238
+ : "existing artifact is not protocol-resolvable in the current session"
1239
+ : undefined;
1240
+ return {
1241
+ ...result,
1242
+ content:
1243
+ artifactId && (invalidateArtifact || staleFullClaim)
1244
+ ? stripBodyArtifactReferences(result, artifactId)
1245
+ : result.content,
1246
+ details: {
1247
+ ...(result.details ?? {}),
1248
+ meta: {
1249
+ ...meta,
1250
+ truncation: {
1251
+ ...truncation,
1252
+ noticeOwner: invalidateArtifact || staleFullClaim ? undefined : truncation.noticeOwner,
1253
+ artifactId: invalidateArtifact ? undefined : artifactId,
1254
+ artifactVerified: invalidateArtifact ? false : truncation.artifactVerified,
1255
+ sourceCaptureIncomplete:
1256
+ !countersValid || !sourceCaptureFlagValid || truncation.sourceCaptureIncomplete || undefined,
1257
+ shownRange: sourceCoordinatesIncomplete ? undefined : truncation.shownRange,
1258
+ headRange: sourceCoordinatesIncomplete ? undefined : truncation.headRange,
1259
+ tailRange: sourceCoordinatesIncomplete ? undefined : truncation.tailRange,
1260
+ nextOffset: sourceCoordinatesIncomplete ? undefined : truncation.nextOffset,
1261
+ artifactFailureDiagnostic: diagnostic,
1262
+ },
1263
+ },
1264
+ },
1265
+ };
830
1266
  }
831
1267
 
832
1268
  /**
@@ -858,35 +1294,95 @@ async function spillLargeResultToArtifact(
858
1294
  return result;
859
1295
  }
860
1296
 
861
- const artifactCapability = artifactCapabilityForContext(context);
862
- if (!artifactCapability) return result;
863
- const { threshold, readThreshold, tailBytes, tailLines, headBytes } = getSpillConfig(context?.settings);
1297
+ const { threshold, readThreshold, tailBytes, tailLines, headBytes, maxInlineBytes } = getSpillConfig(
1298
+ context?.settings,
1299
+ );
864
1300
  // `read` manages its own per-range truncation, but the combined multi-range
865
1301
  // output has no cap — enforce a read-specific (higher) combined threshold
866
1302
  // instead of exempting read entirely. 0 disables read spill (backstop only).
867
1303
  const effectiveThreshold = toolName === "read" ? readThreshold : threshold;
868
- if (effectiveThreshold <= 0) return result;
869
-
870
- // Skip if tool already saved an artifact
871
1304
  const existingMeta: OutputMeta | undefined = result.details?.meta;
872
- if (existingMeta?.truncation?.artifactId) return result;
873
1305
 
874
- // Measure total text content
1306
+ // Measure total text content before any threshold early return so the absolute
1307
+ // inline cap is enforced even when token-based spilling is disabled.
875
1308
  const textParts: string[] = [];
876
1309
  for (const block of result.content) {
877
- if (block.type === "text" && block.text) {
878
- textParts.push(block.text);
879
- }
1310
+ if (block.type === "text" && block.text) textParts.push(block.text);
880
1311
  }
881
1312
  if (textParts.length === 0) return result;
882
1313
 
883
1314
  const fullText = textParts.length === 1 ? textParts[0] : textParts.join("\n");
1315
+ const existingTruncation = existingMeta?.truncation;
1316
+ const existingArtifactProven =
1317
+ existingTruncation?.artifactId !== undefined &&
1318
+ isValidArtifactId(existingTruncation.artifactId) &&
1319
+ existingTruncation.artifactFailureDiagnostic === undefined &&
1320
+ existingTruncation.artifactVerified === true;
1321
+ if (existingArtifactProven) return result;
884
1322
  const totalBytes = Buffer.byteLength(fullText, "utf-8");
885
- if (totalBytes <= effectiveThreshold) return result;
1323
+ const exceedsInlineCap = maxInlineBytes > 0 && totalBytes > maxInlineBytes;
1324
+ if (!exceedsInlineCap && (effectiveThreshold <= 0 || totalBytes <= effectiveThreshold)) return result;
886
1325
 
887
- // Save full output as artifact
888
- const artifactId = await artifactCapability.saveArtifact(fullText, toolName);
889
- if (!artifactId) return result;
1326
+ const artifactCapability = artifactCapabilityForContext(context);
1327
+ const saveFailure = (diagnostic: string): AgentToolResult => {
1328
+ const failurePreview = createArtifactFailurePreview(
1329
+ fullText,
1330
+ maxInlineBytes > 0 ? maxInlineBytes : undefined,
1331
+ diagnostic,
1332
+ );
1333
+ const outputLines = failurePreview.text.length > 0 ? failurePreview.text.split("\n").length : 0;
1334
+ const outputBytes = Buffer.byteLength(failurePreview.text, "utf-8");
1335
+ const failureTruncation: TruncationMeta = {
1336
+ ...(existingTruncation ?? {}),
1337
+ direction: "tail",
1338
+ truncatedBy: "bytes",
1339
+ noticeOwner: undefined,
1340
+ totalLines: fullText.length > 0 ? fullText.split("\n").length : 0,
1341
+ totalBytes,
1342
+ outputLines,
1343
+ outputBytes,
1344
+ maxBytes: maxInlineBytes > 0 ? maxInlineBytes : undefined,
1345
+ shownRange: undefined,
1346
+ headRange: undefined,
1347
+ tailRange: undefined,
1348
+ nextOffset: undefined,
1349
+ artifactId: undefined,
1350
+ artifactVerified: false,
1351
+ sourceCaptureIncomplete: true,
1352
+ artifactFailureDiagnostic: diagnostic,
1353
+ };
1354
+ const content: (TextContent | ImageContent)[] = result.content.filter(block => block.type !== "text");
1355
+ content.push({ type: "text", text: failurePreview.text });
1356
+ return {
1357
+ ...result,
1358
+ content,
1359
+ details: {
1360
+ ...(result.details ?? {}),
1361
+ meta: { ...(existingMeta ?? {}), truncation: failureTruncation },
1362
+ },
1363
+ };
1364
+ };
1365
+ if (!artifactCapability) return saveFailure("artifact storage is unavailable");
1366
+
1367
+ // Save full output through the bounded, validating path. Failed threshold
1368
+ // spills preserve their diagnostic even when the absolute backstop is disabled.
1369
+ const saveOutcome = await saveInlineArtifactBounded(
1370
+ artifactCapability,
1371
+ () => artifactCapability.saveArtifact(fullText, toolName),
1372
+ artifactId => artifactCapability.getArtifactPath(artifactId),
1373
+ );
1374
+ if (saveOutcome.status !== "ok") return saveFailure(saveOutcome.diagnostic);
1375
+
1376
+ const artifactId = saveOutcome.artifactId;
1377
+ const newArtifactOmissions = genericArtifactOmittedBytes(fullText) ?? 0;
1378
+ const priorArtifactOmissions = isValidLossCounter(existingTruncation?.artifactTruncatedBytes)
1379
+ ? (existingTruncation?.artifactTruncatedBytes ?? 0)
1380
+ : 0;
1381
+ const artifactTruncatedBytes = Math.max(newArtifactOmissions, priorArtifactOmissions) || undefined;
1382
+ const sourceTruncatedBytes = existingTruncation?.sourceTruncatedBytes;
1383
+ const columnDroppedBytes = existingTruncation?.columnDroppedBytes;
1384
+ const sourceCaptureIncomplete =
1385
+ existingTruncation?.sourceCaptureIncomplete || (columnDroppedBytes ?? 0) > 0 || undefined;
890
1386
 
891
1387
  // Truncate: middle elision when a head budget is configured, otherwise tail-only.
892
1388
  const useMiddle = headBytes > 0;
@@ -902,25 +1398,20 @@ async function spillLargeResultToArtifact(
902
1398
  maxLines: tailLines,
903
1399
  });
904
1400
 
905
- // Replace text blocks with single truncated block, keep images
1401
+ // Replace text blocks with single truncated block, keep images.
906
1402
  const newContent: (TextContent | ImageContent)[] = [];
907
1403
  for (const block of result.content) {
908
- if (block.type !== "text") {
909
- newContent.push(block);
910
- }
1404
+ if (block.type !== "text") newContent.push(block);
911
1405
  }
912
1406
  newContent.push({ type: "text", text: truncated.content });
913
1407
 
914
- // Build truncation meta
1408
+ // Build truncation meta.
915
1409
  const outputLines = truncated.outputLines ?? truncated.totalLines;
916
1410
  const outputBytes = truncated.outputBytes ?? truncated.totalBytes;
917
1411
  let truncationMeta: TruncationMeta;
918
1412
  if (truncated.truncatedBy === "middle") {
919
1413
  const elidedLines = truncated.elidedLines ?? Math.max(0, truncated.totalLines - outputLines);
920
1414
  const elidedBytes = truncated.elidedBytes ?? Math.max(0, truncated.totalBytes - outputBytes);
921
- const keptLines = Math.max(0, outputLines - 1); // -1 for marker line
922
- const headLines = Math.ceil(keptLines / 2);
923
- const tailLineCount = keptLines - headLines;
924
1415
  truncationMeta = {
925
1416
  direction: "middle",
926
1417
  truncatedBy: "middle",
@@ -929,14 +1420,16 @@ async function spillLargeResultToArtifact(
929
1420
  outputLines,
930
1421
  outputBytes,
931
1422
  maxBytes: headBytes + tailBytes,
932
- headRange: headLines > 0 ? { start: 1, end: headLines } : undefined,
933
- tailRange:
934
- tailLineCount > 0
935
- ? { start: truncated.totalLines - tailLineCount + 1, end: truncated.totalLines }
936
- : undefined,
1423
+ headRange: sourceCaptureIncomplete ? undefined : truncated.headRange,
1424
+ tailRange: sourceCaptureIncomplete ? undefined : truncated.tailRange,
937
1425
  elidedLines,
938
1426
  elidedBytes,
939
1427
  artifactId,
1428
+ artifactVerified: true,
1429
+ artifactTruncatedBytes,
1430
+ sourceTruncatedBytes,
1431
+ sourceCaptureIncomplete,
1432
+ columnDroppedBytes,
940
1433
  };
941
1434
  } else {
942
1435
  const shownStart = truncated.totalLines - outputLines + 1;
@@ -948,8 +1441,13 @@ async function spillLargeResultToArtifact(
948
1441
  outputLines,
949
1442
  outputBytes,
950
1443
  maxBytes: tailBytes,
951
- shownRange: { start: shownStart, end: truncated.totalLines },
1444
+ shownRange: sourceCaptureIncomplete ? undefined : { start: shownStart, end: truncated.totalLines },
952
1445
  artifactId,
1446
+ artifactVerified: true,
1447
+ artifactTruncatedBytes,
1448
+ sourceTruncatedBytes,
1449
+ sourceCaptureIncomplete,
1450
+ columnDroppedBytes,
953
1451
  };
954
1452
  }
955
1453
 
@@ -996,26 +1494,68 @@ async function enforceInlineResultBackstop(
996
1494
  textParts.push(block.text);
997
1495
  }
998
1496
  }
999
- if (textParts.length === 0) return result;
1000
1497
 
1001
1498
  const renderedText = textParts.length === 1 ? textParts[0] : textParts.join("\n");
1002
1499
  const fullText = stripBodyOwnedTruncationFooter(renderedText, result.details);
1003
- const totalBytes = Buffer.byteLength(fullText, "utf-8");
1004
- if (totalBytes <= maxInlineBytes) return result;
1005
-
1006
- // Reuse an existing artifact (avoid double-artifacting); otherwise save the
1007
- // full output so the truncated view keeps a reference to the complete text.
1500
+ const totalBytes = Buffer.byteLength(renderedText, "utf-8");
1008
1501
  const existingMeta: OutputMeta | undefined = result.details?.meta;
1009
- let artifactId = existingMeta?.truncation?.artifactId;
1502
+ const existingNoticeBytes = Buffer.byteLength(formatOutputNotice(existingMeta), "utf-8");
1503
+ if (totalBytes + existingNoticeBytes <= maxInlineBytes) return result;
1504
+
1505
+ // Reuse only storage-complete artifacts. Source-loss evidence remains bound
1506
+ // to the rendered body even when a new complete artifact is saved.
1507
+ const existingTruncation = existingMeta?.truncation;
1508
+ const previousArtifactId = existingTruncation?.artifactId;
1509
+ const previousArtifactIdValid = previousArtifactId === undefined || isValidArtifactId(previousArtifactId);
1510
+ const previousArtifactProven =
1511
+ previousArtifactId !== undefined && previousArtifactIdValid && existingTruncation?.artifactVerified === true;
1512
+ const existingArtifactStorageComplete =
1513
+ previousArtifactProven && existingTruncation?.artifactFailureDiagnostic === undefined;
1514
+ let artifactId = existingArtifactStorageComplete ? previousArtifactId : undefined;
1515
+ let artifactFailureDiagnostic = previousArtifactIdValid
1516
+ ? existingTruncation?.artifactFailureDiagnostic
1517
+ : "existing metadata contained an invalid artifact id";
1518
+ let artifactTruncatedBytes = existingTruncation?.artifactTruncatedBytes;
1519
+ let artifactVerified = existingArtifactStorageComplete;
1520
+ let savedReplacementArtifact = false;
1010
1521
  const artifactCapability = artifactCapabilityForContext(context);
1011
- if (!artifactId && artifactCapability) {
1012
- artifactId = (await artifactCapability.saveArtifact(fullText, toolName)) ?? undefined;
1522
+ if (!artifactId && artifactCapability && fullText.length > 0) {
1523
+ const saveOutcome = await saveInlineArtifactBounded(
1524
+ artifactCapability,
1525
+ () => artifactCapability.saveArtifact(fullText, toolName),
1526
+ artifactId => artifactCapability.getArtifactPath(artifactId),
1527
+ );
1528
+ if (saveOutcome.status === "ok") {
1529
+ artifactId = saveOutcome.artifactId;
1530
+ const newArtifactOmissions = genericArtifactOmittedBytes(fullText) ?? 0;
1531
+ const priorArtifactOmissions = artifactTruncatedBytes ?? 0;
1532
+ artifactTruncatedBytes = Math.max(newArtifactOmissions, priorArtifactOmissions) || undefined;
1533
+ artifactFailureDiagnostic = undefined;
1534
+ artifactVerified = true;
1535
+ savedReplacementArtifact = true;
1536
+ } else {
1537
+ artifactFailureDiagnostic = saveOutcome.diagnostic;
1538
+ }
1539
+ } else if (!artifactId && !artifactCapability) {
1540
+ artifactFailureDiagnostic ??= "artifact storage is unavailable";
1013
1541
  }
1014
-
1015
- // Budget head+tail below the cap, reserving room for the elision marker so the
1016
- // composed `<head>\n<marker>\n<tail>` view never exceeds the configured cap.
1542
+ const sourceTruncatedBytes = existingTruncation?.sourceTruncatedBytes;
1543
+ const columnDroppedBytes = existingTruncation?.columnDroppedBytes;
1544
+ const sourceCaptureIncomplete =
1545
+ existingTruncation?.sourceCaptureIncomplete ||
1546
+ (savedReplacementArtifact && (columnDroppedBytes ?? 0) > 0) ||
1547
+ undefined;
1548
+ const sourceCoordinatesIncomplete = (sourceTruncatedBytes ?? 0) > 0 || sourceCaptureIncomplete === true;
1549
+
1550
+ // Reserve the actual pre-existing notice plus room for the bounded truncation
1551
+ // evidence appended below. This prevents large diagnostics from bypassing the
1552
+ // final inline cap while retaining both head/tail content when space permits.
1017
1553
  const MARKER_RESERVE = 256;
1018
- const budget = maxInlineBytes - MARKER_RESERVE;
1554
+ const NOTICE_RESERVE = Math.min(
1555
+ Math.max(0, maxInlineBytes - MARKER_RESERVE),
1556
+ Math.max(4096, existingNoticeBytes + 1024),
1557
+ );
1558
+ const budget = Math.max(0, maxInlineBytes - MARKER_RESERVE - NOTICE_RESERVE);
1019
1559
  const useMiddle = headBytes > 0 && budget > 0;
1020
1560
  let truncated = useMiddle
1021
1561
  ? truncateMiddle(fullText, {
@@ -1024,11 +1564,10 @@ async function enforceInlineResultBackstop(
1024
1564
  maxHeadBytes: Math.min(headBytes, Math.floor(budget / 2)),
1025
1565
  maxHeadLines: tailLines,
1026
1566
  })
1027
- : truncateTail(fullText, { maxBytes: maxInlineBytes, maxLines: tailLines });
1567
+ : truncateTail(fullText, { maxBytes: budget, maxLines: tailLines });
1028
1568
 
1029
- // Defensive clamp: guarantee the contract even for pathological marker sizes.
1030
- if (Buffer.byteLength(truncated.content, "utf-8") > maxInlineBytes) {
1031
- truncated = truncateTail(fullText, { maxBytes: maxInlineBytes, maxLines: tailLines });
1569
+ if (Buffer.byteLength(truncated.content, "utf-8") > budget) {
1570
+ truncated = truncateTail(fullText, { maxBytes: budget, maxLines: tailLines });
1032
1571
  }
1033
1572
 
1034
1573
  const newContent: (TextContent | ImageContent)[] = [];
@@ -1054,6 +1593,12 @@ async function enforceInlineResultBackstop(
1054
1593
  elidedLines: truncated.elidedLines ?? Math.max(0, truncated.totalLines - outputLines),
1055
1594
  elidedBytes: truncated.elidedBytes ?? Math.max(0, truncated.totalBytes - outputBytes),
1056
1595
  artifactId,
1596
+ artifactVerified,
1597
+ artifactTruncatedBytes,
1598
+ sourceTruncatedBytes,
1599
+ sourceCaptureIncomplete,
1600
+ columnDroppedBytes,
1601
+ artifactFailureDiagnostic,
1057
1602
  }
1058
1603
  : {
1059
1604
  direction: "tail",
@@ -1063,13 +1608,71 @@ async function enforceInlineResultBackstop(
1063
1608
  outputLines,
1064
1609
  outputBytes,
1065
1610
  maxBytes: maxInlineBytes,
1066
- shownRange: { start: truncated.totalLines - outputLines + 1, end: truncated.totalLines },
1611
+ shownRange: sourceCoordinatesIncomplete
1612
+ ? undefined
1613
+ : { start: truncated.totalLines - outputLines + 1, end: truncated.totalLines },
1067
1614
  artifactId,
1615
+ artifactVerified,
1616
+ artifactTruncatedBytes,
1617
+ sourceTruncatedBytes,
1618
+ sourceCaptureIncomplete,
1619
+ columnDroppedBytes,
1620
+ artifactFailureDiagnostic,
1068
1621
  };
1069
1622
 
1070
- const newMeta: OutputMeta = { ...(existingMeta ?? {}), truncation: truncationMeta };
1623
+ let newMeta: OutputMeta = { ...(existingMeta ?? {}), truncation: truncationMeta };
1624
+ let finalText = truncated.content;
1625
+ let renderedBytes = Buffer.byteLength(finalText, "utf-8") + Buffer.byteLength(formatOutputNotice(newMeta), "utf-8");
1626
+ if (renderedBytes > maxInlineBytes && newMeta.diagnostics) {
1627
+ newMeta = { ...newMeta, diagnostics: undefined, diagnosticsOmitted: true };
1628
+ renderedBytes = Buffer.byteLength(finalText, "utf-8") + Buffer.byteLength(formatOutputNotice(newMeta), "utf-8");
1629
+ }
1630
+ if (renderedBytes > maxInlineBytes) {
1631
+ const compactTruncation = newMeta.truncation
1632
+ ? {
1633
+ ...newMeta.truncation,
1634
+ artifactFailureDiagnostic: newMeta.truncation.artifactFailureDiagnostic
1635
+ ? truncateHeadBytes(newMeta.truncation.artifactFailureDiagnostic, 64).text
1636
+ : undefined,
1637
+ shownRange: undefined,
1638
+ headRange: undefined,
1639
+ tailRange: undefined,
1640
+ nextOffset: undefined,
1641
+ }
1642
+ : undefined;
1643
+ newMeta = { ...newMeta, truncation: compactTruncation, limits: undefined };
1644
+ const noticeBytes = Buffer.byteLength(formatOutputNotice(newMeta), "utf-8");
1645
+ const bodyBudget = Math.max(0, maxInlineBytes - noticeBytes);
1646
+ const finalTruncated = truncateTail(finalText, { maxBytes: bodyBudget, maxLines: tailLines });
1647
+ finalText = finalTruncated.content;
1648
+ if (newMeta.truncation) {
1649
+ newMeta = {
1650
+ ...newMeta,
1651
+ truncation: {
1652
+ ...newMeta.truncation,
1653
+ direction: "tail",
1654
+ truncatedBy: "bytes",
1655
+ outputBytes: Buffer.byteLength(finalText, "utf-8"),
1656
+ outputLines: finalText.length > 0 ? finalText.split("\n").length : 0,
1657
+ },
1658
+ };
1659
+ }
1660
+ }
1661
+ const boundedContent = newContent.map(block => (block.type === "text" ? { ...block, text: finalText } : block));
1071
1662
  const newDetails = { ...(result.details ?? {}), meta: newMeta };
1072
- return { ...result, content: newContent, details: newDetails };
1663
+ return { ...result, content: boundedContent, details: newDetails };
1664
+ }
1665
+
1666
+ export async function finalizeToolResultForDelivery(
1667
+ result: AgentToolResult,
1668
+ toolName: string,
1669
+ context: AgentToolContext | undefined,
1670
+ ): Promise<AgentToolResult> {
1671
+ let finalized = await sanitizeArtifactIdEvidence(result, context);
1672
+ finalized = await spillLargeResultToArtifact(finalized, toolName, context);
1673
+ finalized = await enforceInlineResultBackstop(finalized, toolName, context);
1674
+ const meta: OutputMeta | undefined = finalized.details?.meta;
1675
+ return meta ? { ...finalized, content: appendOutputNotice(finalized.content, meta) } : finalized;
1073
1676
  }
1074
1677
 
1075
1678
  // =============================================================================
@@ -1087,24 +1690,11 @@ async function wrappedExecute(
1087
1690
  const originalExecute = this[kUnwrappedExecute];
1088
1691
 
1089
1692
  try {
1090
- let result = await originalExecute.call(this, toolCallId, params, signal, onUpdate, context);
1091
-
1092
- // Spill large results to artifact, truncate to tail
1093
- result = await spillLargeResultToArtifact(result, this.name, context);
1094
-
1095
- // Absolute inline-size backstop: catches oversized text the threshold spill
1096
- // skipped (read exemption, tools with pre-existing partial artifact meta).
1097
- result = await enforceInlineResultBackstop(result, this.name, context);
1098
-
1099
- // Append notices from meta
1100
- const meta: OutputMeta | undefined = result.details?.meta;
1101
- if (meta) {
1102
- return {
1103
- ...result,
1104
- content: appendOutputNotice(result.content, meta),
1105
- };
1106
- }
1107
- return result;
1693
+ return await finalizeToolResultForDelivery(
1694
+ await originalExecute.call(this, toolCallId, params, signal, onUpdate, context),
1695
+ this.name,
1696
+ context,
1697
+ );
1108
1698
  } catch (e) {
1109
1699
  // Re-throw with formatted message so agent-loop sets isError flag
1110
1700
  throw new Error(renderError(e));