@gajae-code/coding-agent 0.12.6 → 0.12.7

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 (70) hide show
  1. package/CHANGELOG.md +3 -1
  2. package/dist/types/async/job-manager.d.ts +36 -1
  3. package/dist/types/eval/backend.d.ts +2 -2
  4. package/dist/types/exec/bash-executor.d.ts +8 -10
  5. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -36
  6. package/dist/types/internal-urls/registry-helpers.d.ts +0 -18
  7. package/dist/types/sdk/bus/chat-daemon-control.d.ts +1 -3
  8. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +2 -4
  9. package/dist/types/sdk/host/host.d.ts +2 -23
  10. package/dist/types/sdk/host/reverse-leases.d.ts +4 -7
  11. package/dist/types/session/artifacts.d.ts +5 -8
  12. package/dist/types/session/client-bridge.d.ts +0 -14
  13. package/dist/types/session/internal/managed-session-storage.d.ts +2 -5
  14. package/dist/types/session/messages.d.ts +1 -4
  15. package/dist/types/session/session-manager.d.ts +4 -5
  16. package/dist/types/session/streaming-output.d.ts +0 -32
  17. package/dist/types/task/receipt.d.ts +1 -2
  18. package/dist/types/task/types.d.ts +43 -178
  19. package/dist/types/tools/bash.d.ts +1 -1
  20. package/dist/types/tools/fetch.d.ts +0 -3
  21. package/dist/types/tools/output-meta.d.ts +5 -37
  22. package/dist/types/tools/subagent.d.ts +15 -0
  23. package/dist/types/tools/todo-write.d.ts +3 -0
  24. package/dist/types/tools/tool-result.d.ts +0 -1
  25. package/package.json +8 -8
  26. package/src/async/job-manager.ts +430 -46
  27. package/src/cursor.ts +13 -20
  28. package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -3
  29. package/src/eval/backend.ts +2 -2
  30. package/src/exec/bash-executor.ts +151 -425
  31. package/src/gjc-runtime/ultragoal-runtime.ts +3 -280
  32. package/src/gjc-runtime/workflow-manifest.generated.json +1 -40
  33. package/src/gjc-runtime/workflow-manifest.ts +0 -12
  34. package/src/internal-urls/agent-protocol.ts +38 -98
  35. package/src/internal-urls/artifact-protocol.ts +17 -28
  36. package/src/internal-urls/docs-index.generated.ts +1 -1
  37. package/src/internal-urls/registry-helpers.ts +0 -62
  38. package/src/modes/acp/acp-agent.ts +0 -24
  39. package/src/modes/acp/acp-client-bridge.ts +11 -154
  40. package/src/prompts/tools/bash.md +2 -2
  41. package/src/sdk/acp/adapter.ts +9 -200
  42. package/src/sdk/bus/chat-daemon-control.ts +3 -5
  43. package/src/sdk/bus/index.ts +8 -324
  44. package/src/sdk/bus/telegram-daemon-contract.ts +2 -4
  45. package/src/sdk/host/host.ts +9 -187
  46. package/src/sdk/host/reverse-leases.ts +42 -249
  47. package/src/session/agent-session.ts +15 -178
  48. package/src/session/artifacts.ts +74 -211
  49. package/src/session/client-bridge.ts +0 -15
  50. package/src/session/internal/managed-session-storage.ts +5 -104
  51. package/src/session/messages.ts +7 -32
  52. package/src/session/session-manager.ts +21 -12
  53. package/src/session/streaming-output.ts +40 -198
  54. package/src/task/index.ts +230 -64
  55. package/src/task/receipt.ts +2 -4
  56. package/src/task/render.ts +0 -30
  57. package/src/task/types.ts +10 -34
  58. package/src/tools/bash.ts +239 -818
  59. package/src/tools/eval.ts +4 -99
  60. package/src/tools/fetch.ts +15 -77
  61. package/src/tools/find.ts +3 -7
  62. package/src/tools/output-meta.ts +114 -696
  63. package/src/tools/search.ts +4 -9
  64. package/src/tools/subagent.ts +97 -75
  65. package/src/tools/todo-write.ts +47 -0
  66. package/src/tools/tool-result.ts +0 -1
  67. package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +0 -83
  68. package/dist/types/utils/artifact-id.d.ts +0 -2
  69. package/src/gjc-runtime/ultragoal-review-source.ts +0 -378
  70. package/src/utils/artifact-id.ts +0 -6
@@ -15,80 +15,19 @@ 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 { type SessionArtifactCapability, sessionArtifactCapability } from "../session/session-manager";
18
+ import { sessionArtifactCapability } from "../session/session-manager";
19
19
  import {
20
- DEFAULT_ARTIFACT_MAX_BYTES,
21
20
  formatMiddleElisionMarker,
22
- hasCurrentExecutionArtifactProof,
23
21
  type OutputSummary,
24
22
  type ReadWindow,
25
23
  type TruncationResult,
26
- truncateHeadBytes,
27
24
  truncateMiddle,
28
25
  truncateTail,
29
26
  } from "../session/streaming-output";
30
- import { isValidArtifactId } from "../utils/artifact-id";
31
-
32
- export { isValidArtifactId } from "../utils/artifact-id";
33
27
 
34
28
  import { formatBytes, wrapBrackets } from "./render-utils";
35
29
  import { renderError } from "./tool-errors";
36
30
 
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
- }
92
31
  /**
93
32
  * Truncation metadata for the output notice.
94
33
  */
@@ -116,16 +55,8 @@ export interface TruncationMeta {
116
55
  elidedLines?: number;
117
56
  /** Artifact ID when output was persisted; completeness is tracked separately below. */
118
57
  artifactId?: string;
119
- /** Artifact existence was proven by a successful save or publisher receipt. */
120
- artifactVerified?: boolean;
121
58
  /** Bytes omitted from an artifact after its hard storage cap was reached. */
122
59
  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;
129
60
  /** Bounded diagnostic when artifact writer creation, write, or finalization failed. */
130
61
  artifactFailureDiagnostic?: string;
131
62
  /** Next offset for pagination (head truncation only) */
@@ -166,8 +97,6 @@ export interface OutputMeta {
166
97
  source?: SourceMeta;
167
98
  diagnostics?: DiagnosticMeta;
168
99
  limits?: LimitsMeta;
169
- /** Diagnostics were omitted to preserve the configured inline byte cap. */
170
- diagnosticsOmitted?: boolean;
171
100
  }
172
101
 
173
102
  // =============================================================================
@@ -179,9 +108,6 @@ export interface TruncationOptions {
179
108
  startLine?: number;
180
109
  totalFileLines?: number;
181
110
  artifactId?: string;
182
- artifactVerified?: boolean;
183
- artifactFailureDiagnostic?: string;
184
- sourceCaptureIncomplete?: boolean;
185
111
  maxBytes?: number;
186
112
  noticeOwner?: "body";
187
113
  }
@@ -199,8 +125,6 @@ export interface TruncationTextOptions {
199
125
  totalBytes?: number;
200
126
  maxBytes?: number;
201
127
  artifactId?: string;
202
- artifactVerified?: boolean;
203
- sourceCaptureIncomplete?: boolean;
204
128
  noticeOwner?: "body";
205
129
  }
206
130
 
@@ -223,17 +147,7 @@ export class OutputMetaBuilder {
223
147
  truncation(result: TruncationResult, options: TruncationOptions): this {
224
148
  if (!result.truncated) return this;
225
149
 
226
- const {
227
- direction,
228
- startLine = 1,
229
- totalFileLines,
230
- artifactId,
231
- artifactVerified,
232
- artifactFailureDiagnostic,
233
- sourceCaptureIncomplete,
234
- noticeOwner,
235
- maxBytes,
236
- } = options;
150
+ const { direction, startLine = 1, totalFileLines, artifactId, noticeOwner, maxBytes } = options;
237
151
  const outputLines = result.outputLines ?? result.totalLines;
238
152
  const outputBytes = result.outputBytes ?? result.totalBytes;
239
153
  const isMiddle = direction === "middle" || result.truncatedBy === "middle";
@@ -249,6 +163,12 @@ export class OutputMetaBuilder {
249
163
  if (isMiddle) {
250
164
  const elidedLines = result.elidedLines ?? Math.max(0, effectiveTotalLines - outputLines);
251
165
  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;
252
172
  this.#meta.truncation = {
253
173
  direction: "middle",
254
174
  truncatedBy: "middle",
@@ -258,14 +178,12 @@ export class OutputMetaBuilder {
258
178
  outputLines,
259
179
  outputBytes,
260
180
  ...(maxBytes !== undefined ? { maxBytes } : {}),
261
- headRange: result.firstLinePartial ? undefined : result.headRange,
262
- tailRange: result.lastLinePartial ? undefined : result.tailRange,
181
+ headRange: headLines > 0 ? { start: 1, end: headLines } : undefined,
182
+ tailRange:
183
+ tailLines > 0 ? { start: effectiveTotalLines - tailLines + 1, end: effectiveTotalLines } : undefined,
263
184
  elidedLines,
264
185
  elidedBytes,
265
186
  artifactId,
266
- artifactVerified,
267
- artifactFailureDiagnostic,
268
- sourceCaptureIncomplete: sourceCaptureIncomplete || undefined,
269
187
  };
270
188
  return this;
271
189
  }
@@ -290,11 +208,8 @@ export class OutputMetaBuilder {
290
208
  outputLines,
291
209
  outputBytes,
292
210
  ...(maxBytes !== undefined ? { maxBytes } : {}),
293
- shownRange: direction === "tail" && result.lastLinePartial ? undefined : { start: shownStart, end: shownEnd },
211
+ shownRange: { start: shownStart, end: shownEnd },
294
212
  artifactId,
295
- artifactVerified,
296
- artifactFailureDiagnostic,
297
- sourceCaptureIncomplete: sourceCaptureIncomplete || undefined,
298
213
  nextOffset: direction === "head" ? shownEnd + 1 : undefined,
299
214
  };
300
215
 
@@ -306,7 +221,6 @@ export class OutputMetaBuilder {
306
221
  windows: ReadWindow,
307
222
  options: {
308
223
  artifactId?: string;
309
- artifactVerified?: boolean;
310
224
  noticeOwner?: "body";
311
225
  maxBytes?: number;
312
226
  rangeBase?: "file" | "window";
@@ -314,7 +228,7 @@ export class OutputMetaBuilder {
314
228
  ): this {
315
229
  if (windows.kind === "full") return this;
316
230
 
317
- const { artifactId, artifactVerified, noticeOwner, rangeBase } = options;
231
+ const { artifactId, noticeOwner, rangeBase } = options;
318
232
  const maxBytes = options.maxBytes ?? (windows as ReadWindow & { maxBytes?: number }).maxBytes;
319
233
  const outputLinesOverride = (windows as ReadWindow & { outputLinesOverride?: number }).outputLinesOverride;
320
234
  const outputBytesOverride = (windows as ReadWindow & { outputBytesOverride?: number }).outputBytesOverride;
@@ -350,7 +264,6 @@ export class OutputMetaBuilder {
350
264
  sourceBytes: partialTail.sourceLineBytes,
351
265
  },
352
266
  artifactId,
353
- artifactVerified,
354
267
  };
355
268
  return this;
356
269
  }
@@ -369,7 +282,6 @@ export class OutputMetaBuilder {
369
282
  ...(maxBytes !== undefined ? { maxBytes } : {}),
370
283
  shownRange: { start: head.origin.startLine, end: head.origin.endLine },
371
284
  artifactId,
372
- artifactVerified,
373
285
  nextOffset: head.origin.endLine + 1,
374
286
  };
375
287
  return this;
@@ -389,7 +301,6 @@ export class OutputMetaBuilder {
389
301
  ...(maxBytes !== undefined ? { maxBytes } : {}),
390
302
  shownRange: { start: tail.origin.startLine, end: tail.origin.endLine },
391
303
  artifactId,
392
- artifactVerified,
393
304
  };
394
305
  return this;
395
306
  }
@@ -408,12 +319,10 @@ export class OutputMetaBuilder {
408
319
  outputBytes: head.bytes + tail.bytes + Buffer.byteLength(marker, "utf-8") + 2,
409
320
  ...(maxBytes !== undefined ? { maxBytes } : {}),
410
321
  headRange: { start: head.origin.startLine, end: head.origin.endLine },
411
- tailRange:
412
- tail.kind === "partial-line" ? undefined : { start: tail.origin.startLine, end: tail.origin.endLine },
322
+ tailRange: { start: tail.origin.startLine, end: tail.origin.endLine },
413
323
  elidedLines: windows.elidedLines,
414
324
  elidedBytes: windows.elidedBytes,
415
325
  artifactId,
416
- artifactVerified,
417
326
  };
418
327
  return this;
419
328
  }
@@ -421,45 +330,14 @@ export class OutputMetaBuilder {
421
330
  /** Add truncation info from OutputSummary. No-op if not truncated or artifact evidence is absent. */
422
331
  truncationFromSummary(summary: OutputSummary, options: TruncationSummaryOptions): this {
423
332
  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";
432
333
  const artifactTruncatedBytes =
433
- artifactLossValid && summary.artifactTruncatedBytes != null && summary.artifactTruncatedBytes > 0
334
+ summary.artifactTruncatedBytes != null && summary.artifactTruncatedBytes > 0
434
335
  ? summary.artifactTruncatedBytes
435
336
  : 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;
457
337
  const hasArtifactEvidence =
458
338
  summary.artifactId !== undefined ||
459
339
  artifactFailureDiagnostic !== undefined ||
460
- artifactTruncatedBytes !== undefined ||
461
- sourceTruncatedBytes !== undefined ||
462
- sourceCaptureIncomplete !== undefined;
340
+ artifactTruncatedBytes !== undefined;
463
341
  if (!summary.truncated && !hasArtifactEvidence) return this;
464
342
 
465
343
  const { direction, startLine = 1, totalFileLines, noticeOwner } = options;
@@ -468,26 +346,18 @@ export class OutputMetaBuilder {
468
346
  summary.artifactId !== undefined && summary.output.includes(`artifact://${summary.artifactId}`);
469
347
  const bodyOwnsArtifact =
470
348
  bodyHasArtifact &&
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
- ));
349
+ (artifactTruncatedBytes === undefined ||
350
+ (summary.artifactId !== undefined &&
351
+ summary.output.includes(formatArtifactReference(summary.artifactId, artifactTruncatedBytes))));
485
352
  const owner =
486
353
  noticeOwner !== undefined ? { noticeOwner } : bodyOwnsArtifact ? { noticeOwner: "body" as const } : {};
487
354
 
488
355
  // Middle elision: the sink retained head + tail with an elision marker.
489
356
  if (summary.elidedBytes != null && summary.elidedBytes > 0) {
490
357
  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;
491
361
  this.#meta.truncation = {
492
362
  direction: "middle",
493
363
  truncatedBy: "middle",
@@ -496,19 +366,14 @@ export class OutputMetaBuilder {
496
366
  totalBytes: summary.totalBytes,
497
367
  outputLines: summary.outputLines,
498
368
  outputBytes: summary.outputBytes,
499
- headRange: sourceCoordinatesIncomplete || summary.firstLinePartial ? undefined : summary.headRange,
500
- tailRange: sourceCoordinatesIncomplete || summary.lastLinePartial ? undefined : summary.tailRange,
369
+ headRange: headLines > 0 ? { start: 1, end: headLines } : undefined,
370
+ tailRange: tailLines > 0 ? { start: totalLines - tailLines + 1, end: totalLines } : undefined,
501
371
  elidedBytes: summary.elidedBytes,
502
372
  elidedLines,
503
373
  artifactId: summary.artifactId,
504
- artifactVerified: summary.artifactVerified,
505
374
  artifactTruncatedBytes,
506
- sourceTruncatedBytes,
507
- sourceCaptureIncomplete,
508
- columnDroppedBytes,
509
375
  artifactFailureDiagnostic,
510
376
  };
511
- bindCurrentExecutionArtifactProof(this.#meta.truncation, summary);
512
377
  return this;
513
378
  }
514
379
 
@@ -538,20 +403,12 @@ export class OutputMetaBuilder {
538
403
  totalBytes: summary.totalBytes,
539
404
  outputLines: summary.outputLines,
540
405
  outputBytes: summary.outputBytes,
541
- shownRange:
542
- sourceCoordinatesIncomplete || (direction === "tail" && summary.lastLinePartial)
543
- ? undefined
544
- : { start: shownStart, end: shownEnd },
406
+ shownRange: { start: shownStart, end: shownEnd },
545
407
  artifactId: summary.artifactId,
546
- artifactVerified: summary.artifactVerified,
547
408
  artifactTruncatedBytes,
548
- sourceTruncatedBytes,
549
- sourceCaptureIncomplete,
550
- columnDroppedBytes,
551
409
  artifactFailureDiagnostic,
552
- nextOffset: direction === "head" && !sourceCoordinatesIncomplete ? shownEnd + 1 : undefined,
410
+ nextOffset: direction === "head" ? shownEnd + 1 : undefined,
553
411
  };
554
- bindCurrentExecutionArtifactProof(this.#meta.truncation, summary);
555
412
 
556
413
  return this;
557
414
  }
@@ -596,13 +453,8 @@ export class OutputMetaBuilder {
596
453
  outputBytes,
597
454
  maxBytes: options.maxBytes,
598
455
  artifactId: options.artifactId,
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,
456
+ shownRange: { start: shownStart, end: shownEnd },
457
+ nextOffset: options.direction === "head" ? shownEnd + 1 : undefined,
606
458
  };
607
459
 
608
460
  return this;
@@ -697,196 +549,33 @@ export function formatFullOutputReference(artifactId: string): string {
697
549
  return `Read artifact://${artifactId} for full output`;
698
550
  }
699
551
 
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
- }
552
+ /**
553
+ * Format an artifact reference without claiming completeness when storage was hard-capped.
554
+ */
555
+ export function formatArtifactReference(artifactId: string, artifactTruncatedBytes?: number): string {
812
556
  if (artifactTruncatedBytes != null && artifactTruncatedBytes > 0) {
813
- omissions.push(`at least ${formatBytes(artifactTruncatedBytes)} omitted by the artifact storage cap`);
557
+ return `Read artifact://${artifactId} for retained output (at least ${formatBytes(artifactTruncatedBytes)} omitted by the artifact storage cap)`;
814
558
  }
815
- return omissions.length > 0
816
- ? `Read artifact://${artifactId} for retained output (${omissions.join("; ")})`
817
- : formatFullOutputReference(artifactId);
818
- }
819
-
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");
875
- }
876
- return parts.length > 0 ? parts.join("; ") : undefined;
559
+ return formatFullOutputReference(artifactId);
877
560
  }
878
561
 
879
562
  function formatTruncationArtifactNotice(truncation: TruncationMeta): string {
880
- return formatArtifactEvidenceNotice(truncation) ?? "";
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;
570
+ }
571
+ if (reference) return reference;
572
+ return `Artifact storage omitted at least ${formatBytes(truncation.artifactTruncatedBytes ?? 0)}; no artifact reference is available`;
881
573
  }
882
574
 
883
575
  function hasArtifactNotice(truncation: TruncationMeta): boolean {
884
576
  return (
885
577
  truncation.artifactId != null ||
886
578
  (truncation.artifactTruncatedBytes ?? 0) > 0 ||
887
- (truncation.sourceTruncatedBytes ?? 0) > 0 ||
888
- truncation.sourceCaptureIncomplete ||
889
- (truncation.columnDroppedBytes ?? 0) > 0 ||
890
579
  truncation.artifactFailureDiagnostic != null
891
580
  );
892
581
  }
@@ -899,16 +588,6 @@ function formatTruncationRangeTotal(truncation: TruncationMeta): string {
899
588
 
900
589
  export function formatTruncationMetaNotice(truncation: TruncationMeta): string {
901
590
  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
- }
912
591
  if (truncation.partialLine) {
913
592
  let notice = `Showing last ${formatBytes(truncation.partialLine.bytes)} of line ${truncation.partialLine.line} of ${rangeTotal}`;
914
593
  if (truncation.partialLine.sourceBytes > truncation.partialLine.bytes) {
@@ -935,7 +614,7 @@ export function formatTruncationMetaNotice(truncation: TruncationMeta): string {
935
614
  } else if (headPart && tailPart) {
936
615
  notice = `Showing ${headPart} and ${tailPart} of ${rangeTotal}; ${elidedLines.toLocaleString()} middle line${elidedLines === 1 ? "" : "s"} (${formatBytes(elidedBytes)}) elided`;
937
616
  } else {
938
- notice = `Showing retained head and tail fragments of ${rangeTotal}; ${formatBytes(elidedBytes)} middle bytes elided`;
617
+ notice = `Showing ${truncation.outputLines} of ${rangeTotal}${truncation.rangeBase === "window" ? "" : " lines"}; ${formatBytes(elidedBytes)} middle bytes elided`;
939
618
  }
940
619
  if (hasArtifactNotice(truncation)) {
941
620
  notice += `. ${formatTruncationArtifactNotice(truncation)}`;
@@ -1009,12 +688,8 @@ export function formatOutputNotice(meta: OutputMeta | undefined): string {
1009
688
  let diagnosticsNotice = "";
1010
689
  if (meta.diagnostics && meta.diagnostics.messages.length > 0) {
1011
690
  const d = meta.diagnostics;
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}`;
691
+ diagnosticsNotice = `\n\nLSP Diagnostics (${d.summary}):\n${formatGroupedDiagnosticMessages(d.messages)}`;
1016
692
  }
1017
- if (meta.diagnosticsOmitted) parts.push("Diagnostics omitted by inline result cap");
1018
693
 
1019
694
  const notice = parts.length ? `\n\n[${parts.join(". ")}]` : "";
1020
695
  return notice + diagnosticsNotice;
@@ -1116,15 +791,13 @@ function getSpillConfig(s: Settings | undefined) {
1116
791
  | "tools.maxInlineResultBytes"
1117
792
  | "tools.readArtifactSpillThreshold";
1118
793
  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;
1121
794
  return {
1122
795
  threshold: get("tools.artifactSpillThreshold") * 1024,
1123
796
  readThreshold: get("tools.readArtifactSpillThreshold") * 1024,
1124
797
  tailBytes: get("tools.artifactTailBytes") * 1024,
1125
798
  tailLines: get("tools.artifactTailLines"),
1126
799
  headBytes: get("tools.artifactHeadBytes") * 1024,
1127
- maxInlineBytes,
800
+ maxInlineBytes: get("tools.maxInlineResultBytes") * 1024,
1128
801
  };
1129
802
  }
1130
803
 
@@ -1164,107 +837,6 @@ export function resolveBashOutputSinkHeadBytes(s: Settings): number {
1164
837
  return hasExplicitHeadBytes && configuredHeadBytes !== undefined ? configuredHeadBytes * 1024 : 0;
1165
838
  }
1166
839
 
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
- };
1266
- }
1267
-
1268
840
  /**
1269
841
  * Resolve the per-line column cap from session settings. Shared by streaming
1270
842
  * executors (bash/python/ssh/eval via OutputSink) and the `read` tool's
@@ -1294,95 +866,35 @@ async function spillLargeResultToArtifact(
1294
866
  return result;
1295
867
  }
1296
868
 
1297
- const { threshold, readThreshold, tailBytes, tailLines, headBytes, maxInlineBytes } = getSpillConfig(
1298
- context?.settings,
1299
- );
869
+ const artifactCapability = artifactCapabilityForContext(context);
870
+ if (!artifactCapability) return result;
871
+ const { threshold, readThreshold, tailBytes, tailLines, headBytes } = getSpillConfig(context?.settings);
1300
872
  // `read` manages its own per-range truncation, but the combined multi-range
1301
873
  // output has no cap — enforce a read-specific (higher) combined threshold
1302
874
  // instead of exempting read entirely. 0 disables read spill (backstop only).
1303
875
  const effectiveThreshold = toolName === "read" ? readThreshold : threshold;
876
+ if (effectiveThreshold <= 0) return result;
877
+
878
+ // Skip if tool already saved an artifact
1304
879
  const existingMeta: OutputMeta | undefined = result.details?.meta;
880
+ if (existingMeta?.truncation?.artifactId) return result;
1305
881
 
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.
882
+ // Measure total text content
1308
883
  const textParts: string[] = [];
1309
884
  for (const block of result.content) {
1310
- if (block.type === "text" && block.text) textParts.push(block.text);
885
+ if (block.type === "text" && block.text) {
886
+ textParts.push(block.text);
887
+ }
1311
888
  }
1312
889
  if (textParts.length === 0) return result;
1313
890
 
1314
891
  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;
1322
892
  const totalBytes = Buffer.byteLength(fullText, "utf-8");
1323
- const exceedsInlineCap = maxInlineBytes > 0 && totalBytes > maxInlineBytes;
1324
- if (!exceedsInlineCap && (effectiveThreshold <= 0 || totalBytes <= effectiveThreshold)) return result;
893
+ if (totalBytes <= effectiveThreshold) return result;
1325
894
 
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;
895
+ // Save full output as artifact
896
+ const artifactId = await artifactCapability.saveArtifact(fullText, toolName);
897
+ if (!artifactId) return result;
1386
898
 
1387
899
  // Truncate: middle elision when a head budget is configured, otherwise tail-only.
1388
900
  const useMiddle = headBytes > 0;
@@ -1398,20 +910,25 @@ async function spillLargeResultToArtifact(
1398
910
  maxLines: tailLines,
1399
911
  });
1400
912
 
1401
- // Replace text blocks with single truncated block, keep images.
913
+ // Replace text blocks with single truncated block, keep images
1402
914
  const newContent: (TextContent | ImageContent)[] = [];
1403
915
  for (const block of result.content) {
1404
- if (block.type !== "text") newContent.push(block);
916
+ if (block.type !== "text") {
917
+ newContent.push(block);
918
+ }
1405
919
  }
1406
920
  newContent.push({ type: "text", text: truncated.content });
1407
921
 
1408
- // Build truncation meta.
922
+ // Build truncation meta
1409
923
  const outputLines = truncated.outputLines ?? truncated.totalLines;
1410
924
  const outputBytes = truncated.outputBytes ?? truncated.totalBytes;
1411
925
  let truncationMeta: TruncationMeta;
1412
926
  if (truncated.truncatedBy === "middle") {
1413
927
  const elidedLines = truncated.elidedLines ?? Math.max(0, truncated.totalLines - outputLines);
1414
928
  const elidedBytes = truncated.elidedBytes ?? Math.max(0, truncated.totalBytes - outputBytes);
929
+ const keptLines = Math.max(0, outputLines - 1); // -1 for marker line
930
+ const headLines = Math.ceil(keptLines / 2);
931
+ const tailLineCount = keptLines - headLines;
1415
932
  truncationMeta = {
1416
933
  direction: "middle",
1417
934
  truncatedBy: "middle",
@@ -1420,16 +937,14 @@ async function spillLargeResultToArtifact(
1420
937
  outputLines,
1421
938
  outputBytes,
1422
939
  maxBytes: headBytes + tailBytes,
1423
- headRange: sourceCaptureIncomplete ? undefined : truncated.headRange,
1424
- tailRange: sourceCaptureIncomplete ? undefined : truncated.tailRange,
940
+ headRange: headLines > 0 ? { start: 1, end: headLines } : undefined,
941
+ tailRange:
942
+ tailLineCount > 0
943
+ ? { start: truncated.totalLines - tailLineCount + 1, end: truncated.totalLines }
944
+ : undefined,
1425
945
  elidedLines,
1426
946
  elidedBytes,
1427
947
  artifactId,
1428
- artifactVerified: true,
1429
- artifactTruncatedBytes,
1430
- sourceTruncatedBytes,
1431
- sourceCaptureIncomplete,
1432
- columnDroppedBytes,
1433
948
  };
1434
949
  } else {
1435
950
  const shownStart = truncated.totalLines - outputLines + 1;
@@ -1441,13 +956,8 @@ async function spillLargeResultToArtifact(
1441
956
  outputLines,
1442
957
  outputBytes,
1443
958
  maxBytes: tailBytes,
1444
- shownRange: sourceCaptureIncomplete ? undefined : { start: shownStart, end: truncated.totalLines },
959
+ shownRange: { start: shownStart, end: truncated.totalLines },
1445
960
  artifactId,
1446
- artifactVerified: true,
1447
- artifactTruncatedBytes,
1448
- sourceTruncatedBytes,
1449
- sourceCaptureIncomplete,
1450
- columnDroppedBytes,
1451
961
  };
1452
962
  }
1453
963
 
@@ -1494,68 +1004,26 @@ async function enforceInlineResultBackstop(
1494
1004
  textParts.push(block.text);
1495
1005
  }
1496
1006
  }
1007
+ if (textParts.length === 0) return result;
1497
1008
 
1498
1009
  const renderedText = textParts.length === 1 ? textParts[0] : textParts.join("\n");
1499
1010
  const fullText = stripBodyOwnedTruncationFooter(renderedText, result.details);
1500
- const totalBytes = Buffer.byteLength(renderedText, "utf-8");
1011
+ const totalBytes = Buffer.byteLength(fullText, "utf-8");
1012
+ if (totalBytes <= maxInlineBytes) return result;
1013
+
1014
+ // Reuse an existing artifact (avoid double-artifacting); otherwise save the
1015
+ // full output so the truncated view keeps a reference to the complete text.
1501
1016
  const existingMeta: OutputMeta | undefined = result.details?.meta;
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;
1017
+ let artifactId = existingMeta?.truncation?.artifactId;
1521
1018
  const artifactCapability = artifactCapabilityForContext(context);
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";
1019
+ if (!artifactId && artifactCapability) {
1020
+ artifactId = (await artifactCapability.saveArtifact(fullText, toolName)) ?? undefined;
1541
1021
  }
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.
1022
+
1023
+ // Budget head+tail below the cap, reserving room for the elision marker so the
1024
+ // composed `<head>\n<marker>\n<tail>` view never exceeds the configured cap.
1553
1025
  const MARKER_RESERVE = 256;
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);
1026
+ const budget = maxInlineBytes - MARKER_RESERVE;
1559
1027
  const useMiddle = headBytes > 0 && budget > 0;
1560
1028
  let truncated = useMiddle
1561
1029
  ? truncateMiddle(fullText, {
@@ -1564,10 +1032,11 @@ async function enforceInlineResultBackstop(
1564
1032
  maxHeadBytes: Math.min(headBytes, Math.floor(budget / 2)),
1565
1033
  maxHeadLines: tailLines,
1566
1034
  })
1567
- : truncateTail(fullText, { maxBytes: budget, maxLines: tailLines });
1035
+ : truncateTail(fullText, { maxBytes: maxInlineBytes, maxLines: tailLines });
1568
1036
 
1569
- if (Buffer.byteLength(truncated.content, "utf-8") > budget) {
1570
- truncated = truncateTail(fullText, { maxBytes: budget, maxLines: tailLines });
1037
+ // Defensive clamp: guarantee the contract even for pathological marker sizes.
1038
+ if (Buffer.byteLength(truncated.content, "utf-8") > maxInlineBytes) {
1039
+ truncated = truncateTail(fullText, { maxBytes: maxInlineBytes, maxLines: tailLines });
1571
1040
  }
1572
1041
 
1573
1042
  const newContent: (TextContent | ImageContent)[] = [];
@@ -1593,12 +1062,6 @@ async function enforceInlineResultBackstop(
1593
1062
  elidedLines: truncated.elidedLines ?? Math.max(0, truncated.totalLines - outputLines),
1594
1063
  elidedBytes: truncated.elidedBytes ?? Math.max(0, truncated.totalBytes - outputBytes),
1595
1064
  artifactId,
1596
- artifactVerified,
1597
- artifactTruncatedBytes,
1598
- sourceTruncatedBytes,
1599
- sourceCaptureIncomplete,
1600
- columnDroppedBytes,
1601
- artifactFailureDiagnostic,
1602
1065
  }
1603
1066
  : {
1604
1067
  direction: "tail",
@@ -1608,71 +1071,13 @@ async function enforceInlineResultBackstop(
1608
1071
  outputLines,
1609
1072
  outputBytes,
1610
1073
  maxBytes: maxInlineBytes,
1611
- shownRange: sourceCoordinatesIncomplete
1612
- ? undefined
1613
- : { start: truncated.totalLines - outputLines + 1, end: truncated.totalLines },
1074
+ shownRange: { start: truncated.totalLines - outputLines + 1, end: truncated.totalLines },
1614
1075
  artifactId,
1615
- artifactVerified,
1616
- artifactTruncatedBytes,
1617
- sourceTruncatedBytes,
1618
- sourceCaptureIncomplete,
1619
- columnDroppedBytes,
1620
- artifactFailureDiagnostic,
1621
1076
  };
1622
1077
 
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));
1078
+ const newMeta: OutputMeta = { ...(existingMeta ?? {}), truncation: truncationMeta };
1662
1079
  const newDetails = { ...(result.details ?? {}), meta: newMeta };
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;
1080
+ return { ...result, content: newContent, details: newDetails };
1676
1081
  }
1677
1082
 
1678
1083
  // =============================================================================
@@ -1690,11 +1095,24 @@ async function wrappedExecute(
1690
1095
  const originalExecute = this[kUnwrappedExecute];
1691
1096
 
1692
1097
  try {
1693
- return await finalizeToolResultForDelivery(
1694
- await originalExecute.call(this, toolCallId, params, signal, onUpdate, context),
1695
- this.name,
1696
- context,
1697
- );
1098
+ let result = await originalExecute.call(this, toolCallId, params, signal, onUpdate, context);
1099
+
1100
+ // Spill large results to artifact, truncate to tail
1101
+ result = await spillLargeResultToArtifact(result, this.name, context);
1102
+
1103
+ // Absolute inline-size backstop: catches oversized text the threshold spill
1104
+ // skipped (read exemption, tools with pre-existing partial artifact meta).
1105
+ result = await enforceInlineResultBackstop(result, this.name, context);
1106
+
1107
+ // Append notices from meta
1108
+ const meta: OutputMeta | undefined = result.details?.meta;
1109
+ if (meta) {
1110
+ return {
1111
+ ...result,
1112
+ content: appendOutputNotice(result.content, meta),
1113
+ };
1114
+ }
1115
+ return result;
1698
1116
  } catch (e) {
1699
1117
  // Re-throw with formatted message so agent-loop sets isError flag
1700
1118
  throw new Error(renderError(e));