@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
@@ -2,6 +2,7 @@ import type { AgentToolUpdateCallback } from "@gajae-code/agent-core";
2
2
 
3
3
  import { sanitizeText } from "@gajae-code/utils";
4
4
  import { formatBytes } from "../tools/render-utils";
5
+ import { isValidArtifactId } from "../utils/artifact-id";
5
6
  import { sanitizeWithOptionalSixelPassthrough } from "../utils/sixel";
6
7
 
7
8
  function sanitizeOutputChunk(rawChunk: string): string {
@@ -49,6 +50,47 @@ export type TerminalArtifactPublisher = (
49
50
  info: { totalBytes: number; omittedBytes: number },
50
51
  ) => Promise<TerminalArtifactPublishResult>;
51
52
 
53
+ const TERMINAL_ARTIFACT_PUBLISH_WAIT_MS = 500;
54
+ const TERMINAL_ARTIFACT_PENDING_LIMIT = 64;
55
+ const pendingTerminalArtifactPublishesByOwner = new WeakMap<object, Set<Promise<TerminalArtifactPublishResult>>>();
56
+
57
+ function pendingTerminalArtifactPublishesForOwner(owner: object): Set<Promise<TerminalArtifactPublishResult>> {
58
+ let pending = pendingTerminalArtifactPublishesByOwner.get(owner);
59
+ if (!pending) {
60
+ pending = new Set();
61
+ pendingTerminalArtifactPublishesByOwner.set(owner, pending);
62
+ }
63
+ return pending;
64
+ }
65
+
66
+ function terminalPublisherOwner(publisher: TerminalArtifactPublisher): object {
67
+ return (publisher as TerminalArtifactPublisher & { owner?: object }).owner ?? publisher;
68
+ }
69
+
70
+ async function publishTerminalArtifactBounded(
71
+ publisher: TerminalArtifactPublisher,
72
+ content: string,
73
+ info: { totalBytes: number; omittedBytes: number },
74
+ ): Promise<TerminalArtifactPublishResult> {
75
+ const pendingTerminalArtifactPublishes = pendingTerminalArtifactPublishesForOwner(terminalPublisherOwner(publisher));
76
+ if (pendingTerminalArtifactPublishes.size >= TERMINAL_ARTIFACT_PENDING_LIMIT) {
77
+ return { status: "failed", diagnostic: "pending publisher limit reached for this session" };
78
+ }
79
+ const promise = Promise.resolve().then(() => publisher(content, info));
80
+ pendingTerminalArtifactPublishes.add(promise);
81
+ void promise.then(
82
+ () => pendingTerminalArtifactPublishes.delete(promise),
83
+ () => pendingTerminalArtifactPublishes.delete(promise),
84
+ );
85
+ return Promise.race([
86
+ promise,
87
+ Bun.sleep(TERMINAL_ARTIFACT_PUBLISH_WAIT_MS).then(() => ({
88
+ status: "failed" as const,
89
+ diagnostic: `did not settle within ${TERMINAL_ARTIFACT_PUBLISH_WAIT_MS}ms`,
90
+ })),
91
+ ]);
92
+ }
93
+
52
94
  // =============================================================================
53
95
  // Interfaces
54
96
  // =============================================================================
@@ -64,18 +106,60 @@ export interface OutputSummary {
64
106
  elidedBytes?: number;
65
107
  /** Lines elided from the middle when head-retain mode is active. */
66
108
  elidedLines?: number;
109
+ /** Exact source ranges retained by head/tail middle-elision windows. */
110
+ headRange?: { start: number; end: number };
111
+ tailRange?: { start: number; end: number };
112
+ /** The retained tail ends with only a byte fragment of its source line. */
113
+ lastLinePartial?: boolean;
114
+ /** The retained head ends with only a byte fragment of its source line. */
115
+ firstLinePartial?: boolean;
67
116
  /** Bytes dropped by the per-line column cap (sum across all lines). */
68
117
  columnDroppedBytes?: number;
69
118
  /** Number of distinct lines that hit the per-line column cap. */
70
119
  columnTruncatedLines?: number;
120
+ /** Configured per-line byte/column cap that caused visible omission. */
121
+ columnMax?: number;
71
122
  /** Artifact ID for internal URL access (artifact://<id>) when output was persisted. */
72
123
  artifactId?: string;
124
+ /** Artifact existence was proven by successful publication or file finalization. */
125
+ artifactVerified?: boolean;
73
126
  /** Bytes omitted from artifact storage after the artifact hard cap was reached. */
74
127
  artifactTruncatedBytes?: number;
128
+ /** Bytes dropped before the Bash executor received the native output stream. */
129
+ sourceTruncatedBytes?: number;
130
+ /** Exact source capture completeness could not be proven. */
131
+ sourceCaptureIncomplete?: boolean;
75
132
  /** Bounded diagnostic when artifact writer or terminal publisher creation, write, finalization, or publication failed. */
76
133
  artifactFailureDiagnostic?: string;
77
134
  }
78
135
 
136
+ const kCurrentExecutionArtifactProof = Symbol("OutputSink.CurrentExecutionArtifactProof");
137
+ interface CurrentExecutionArtifactProof {
138
+ artifactId: string;
139
+ payload: string;
140
+ consumed: boolean;
141
+ }
142
+
143
+ /** Verify that this exact OutputSink summary came from a successful current-execution publication. */
144
+ export function hasCurrentExecutionArtifactProof(
145
+ summary: OutputSummary,
146
+ artifactId: string,
147
+ payload?: string,
148
+ ): boolean {
149
+ const proof = (summary as OutputSummary & { [kCurrentExecutionArtifactProof]?: CurrentExecutionArtifactProof })[
150
+ kCurrentExecutionArtifactProof
151
+ ];
152
+ if (
153
+ !proof ||
154
+ proof.artifactId !== artifactId ||
155
+ proof.consumed ||
156
+ (payload !== undefined && proof.payload !== payload)
157
+ )
158
+ return false;
159
+ proof.consumed = true;
160
+ return true;
161
+ }
162
+
79
163
  export interface OutputSinkOptions {
80
164
  /**
81
165
  * Deprecated managed artifact pathname. Bare paths are deliberately ignored:
@@ -141,7 +225,10 @@ export interface TruncationResult {
141
225
  elidedBytes?: number;
142
226
  /** Lines elided from the middle (truncateMiddle only). */
143
227
  elidedLines?: number;
228
+ headRange?: { start: number; end: number };
229
+ tailRange?: { start: number; end: number };
144
230
  lastLinePartial?: boolean;
231
+ firstLinePartial?: boolean;
145
232
  firstLineExceedsLimit?: boolean;
146
233
  lastLineExceedsLimit?: boolean;
147
234
  }
@@ -793,7 +880,7 @@ export function truncateMiddle(content: string, options: TruncationOptions = {})
793
880
  const head = windows.head;
794
881
  const tail = windows.tail;
795
882
  if (!head || !tail) return noTruncResult(content, windows.totalLines, windows.totalBytes);
796
- if (head.lines + tail.lines >= windows.totalLines)
883
+ if (head.lines + tail.lines >= windows.totalLines && tail.kind !== "partial-line")
797
884
  return noTruncResult(content, windows.totalLines, windows.totalBytes);
798
885
  const legacyElidedLines = Math.max(0, windows.totalLines - head.lines - tail.lines);
799
886
  const legacyElidedBytes =
@@ -802,7 +889,7 @@ export function truncateMiddle(content: string, options: TruncationOptions = {})
802
889
  : windows.elidedBytes;
803
890
  const marker = formatMiddleElisionMarker(legacyElidedLines, legacyElidedBytes);
804
891
  const markerBytes = Buffer.byteLength(marker, "utf-8");
805
- return {
892
+ const result: TruncationResult = {
806
893
  content: `${head.content}\n${marker}\n${tail.content}`,
807
894
  truncated: true,
808
895
  truncatedBy: "middle",
@@ -816,6 +903,11 @@ export function truncateMiddle(content: string, options: TruncationOptions = {})
816
903
  firstLineExceedsLimit: false,
817
904
  ...(tail.lastLinePartial ? { lastLineExceedsLimit: true } : {}),
818
905
  };
906
+ Object.defineProperties(result, {
907
+ headRange: { value: { start: head.origin.startLine, end: head.origin.endLine } },
908
+ tailRange: { value: { start: tail.origin.startLine, end: tail.origin.endLine } },
909
+ });
910
+ return result;
819
911
  }
820
912
  }
821
913
  }
@@ -932,6 +1024,8 @@ export class OutputSink {
932
1024
  #head = "";
933
1025
  #headBytes = 0;
934
1026
  #headRetentionDisabled = false;
1027
+ #headEndsMidLine = false;
1028
+ #tailStartsMidLine = false;
935
1029
  #totalLines = 0; // newline count
936
1030
  #totalBytes = 0;
937
1031
  #processedBytes = 0;
@@ -967,6 +1061,8 @@ export class OutputSink {
967
1061
  #pendingArtifactContent?: string[];
968
1062
  #artifactPublishAttempted = false;
969
1063
  #finalized = false;
1064
+ #finalizing = false;
1065
+ #artifactFinalization?: Promise<string | undefined>;
970
1066
 
971
1067
  #fileReady = false;
972
1068
 
@@ -1020,9 +1116,10 @@ export class OutputSink {
1020
1116
  return this.#buffer;
1021
1117
  }
1022
1118
 
1023
- #setTail(text: string, bytes = Buffer.byteLength(text, "utf-8")): void {
1119
+ #setTail(text: string, bytes = Buffer.byteLength(text, "utf-8"), startsMidLine = false): void {
1024
1120
  this.#buffer = text;
1025
1121
  this.#bufferBytes = bytes;
1122
+ this.#tailStartsMidLine = startsMidLine;
1026
1123
  }
1027
1124
 
1028
1125
  #appendTail(text: string, bytes: number): void {
@@ -1037,9 +1134,12 @@ export class OutputSink {
1037
1134
 
1038
1135
  #trimTailTo(maxBytes: number): void {
1039
1136
  if (this.#bufferBytes <= maxBytes) return;
1040
- const { text, bytes } = truncateTailBytes(this.#buffer, maxBytes);
1137
+ const previous = this.#buffer;
1138
+ const { text, bytes } = truncateTailBytes(previous, maxBytes);
1139
+ const removed = previous.slice(0, previous.length - text.length);
1041
1140
  this.#buffer = text;
1042
1141
  this.#bufferBytes = bytes;
1142
+ this.#tailStartsMidLine = removed.length > 0 && !removed.endsWith("\n");
1043
1143
  }
1044
1144
 
1045
1145
  /**
@@ -1050,6 +1150,7 @@ export class OutputSink {
1050
1150
  // F21: with coalescing enabled, accumulate raw chunks and process them in batches; the default
1051
1151
  // (disabled) path calls #ingest directly and is byte-identical to the historical per-chunk path.
1052
1152
  push(chunk: string): void {
1153
+ if (this.#finalized || this.#finalizing) return;
1053
1154
  if (!this.#coalesceSanitize) {
1054
1155
  this.#ingest(chunk);
1055
1156
  return;
@@ -1132,6 +1233,10 @@ export class OutputSink {
1132
1233
  }
1133
1234
  }
1134
1235
 
1236
+ if (this.#headBytes > 0 && this.#bufferBytes === 0 && tailBytes > 0) {
1237
+ this.#headEndsMidLine = !this.#head.endsWith("\n") && !tailChunk.startsWith("\n");
1238
+ this.#tailStartsMidLine = this.#headEndsMidLine;
1239
+ }
1135
1240
  this.#pushTail(tailChunk, tailBytes);
1136
1241
  }
1137
1242
 
@@ -1216,7 +1321,8 @@ export class OutputSink {
1216
1321
  // Avoid creating a giant intermediate string when chunk alone dominates.
1217
1322
  if (dataBytes >= threshold) {
1218
1323
  const { text, bytes } = truncateTailBytes(chunk, threshold);
1219
- this.#setTail(text, bytes);
1324
+ const removed = chunk.slice(0, chunk.length - text.length);
1325
+ this.#setTail(text, bytes, removed.length > 0 && !removed.endsWith("\n"));
1220
1326
  } else {
1221
1327
  // Intermediate size is bounded (<= threshold + dataBytes), safe to concat.
1222
1328
  this.#appendTail(chunk, dataBytes);
@@ -1390,13 +1496,19 @@ export class OutputSink {
1390
1496
  return this.#publishedArtifactId;
1391
1497
  }
1392
1498
  this.#artifactPublishAttempted = true;
1499
+ this.#finalizing = true;
1393
1500
  const content = this.#pendingArtifactContent?.join("") ?? "";
1394
1501
  const omittedBytes = this.#artifactTruncatedBytes;
1395
1502
  try {
1396
- const published = await this.#artifactPublisher!(content, {
1503
+ const published = await publishTerminalArtifactBounded(this.#artifactPublisher!, content, {
1397
1504
  totalBytes: this.#totalBytes,
1398
1505
  omittedBytes,
1399
1506
  });
1507
+ if (typeof published !== "object" || published === null || !("status" in published)) {
1508
+ this.#recordTerminalArtifactDiagnostic("failed", "publisher returned an invalid result");
1509
+ this.#artifactFinalizationFailed = true;
1510
+ return undefined;
1511
+ }
1400
1512
  if (published.status === "unavailable") {
1401
1513
  this.#recordTerminalArtifactDiagnostic("unavailable");
1402
1514
  this.#artifactFinalizationFailed = true;
@@ -1407,8 +1519,13 @@ export class OutputSink {
1407
1519
  this.#artifactFinalizationFailed = true;
1408
1520
  return undefined;
1409
1521
  }
1410
- if (published.artifactId.length === 0) {
1411
- this.#recordTerminalArtifactDiagnostic("failed", "storage returned no artifact id");
1522
+ if (published.status !== "published") {
1523
+ this.#recordTerminalArtifactDiagnostic("failed", "publisher returned an invalid result");
1524
+ this.#artifactFinalizationFailed = true;
1525
+ return undefined;
1526
+ }
1527
+ if (!isValidArtifactId(published.artifactId)) {
1528
+ this.#recordTerminalArtifactDiagnostic("failed", "storage returned an invalid artifact id");
1412
1529
  this.#artifactFinalizationFailed = true;
1413
1530
  return undefined;
1414
1531
  }
@@ -1432,6 +1549,48 @@ export class OutputSink {
1432
1549
  }
1433
1550
  }
1434
1551
 
1552
+ #shouldFinalizeArtifact(): boolean {
1553
+ return (
1554
+ this.#finalized ||
1555
+ this.#artifactFinalizationFailed ||
1556
+ this.#shouldPublishTerminalArtifact() ||
1557
+ this.#file !== undefined ||
1558
+ (this.#artifactPath !== undefined && this.#artifactTruncatedBytes > 0)
1559
+ );
1560
+ }
1561
+
1562
+ async #finalizeArtifact(): Promise<string | undefined> {
1563
+ let artifactId: string | undefined;
1564
+ if (this.#finalized) artifactId = this.#publishedArtifactId ?? this.#artifactId;
1565
+ else if (!this.#artifactFinalizationFailed) {
1566
+ if (this.#artifactPublisher && !this.#artifactPath) artifactId = await this.#publishTerminalArtifact();
1567
+ if (!this.#artifactFinalizationFailed && this.#artifactPath) {
1568
+ if (this.#artifactTruncatedBytes > 0) this.#createFileSink();
1569
+ if (this.#file) {
1570
+ this.#finalizing = true;
1571
+ await this.#writeArtifactTruncationNotice();
1572
+ try {
1573
+ await this.#file.sink.end();
1574
+ artifactId = this.#file.artifactId;
1575
+ this.#finalized = true;
1576
+ } catch (error) {
1577
+ this.#recordArtifactFailure("end", error);
1578
+ this.#artifactFinalizationFailed = true;
1579
+ this.#file = undefined;
1580
+ this.#fileReady = false;
1581
+ }
1582
+ }
1583
+ }
1584
+ }
1585
+ if (this.#finalized) {
1586
+ this.#pendingFileWrites = undefined;
1587
+ this.#pendingFileWriteBytes = 0;
1588
+ this.#fileReady = false;
1589
+ this.#pendingArtifactContent = undefined;
1590
+ }
1591
+ return artifactId;
1592
+ }
1593
+
1435
1594
  createInput(): WritableStream<Uint8Array | string> {
1436
1595
  const dec = new TextDecoder("utf-8", { ignoreBOM: true });
1437
1596
  const finalize = () => {
@@ -1457,17 +1616,20 @@ export class OutputSink {
1457
1616
  * append directly to the tail without reordering the replacement.
1458
1617
  */
1459
1618
  replace(text: string): void {
1619
+ if (this.#finalized || this.#finalizing) return;
1460
1620
  this.#coalesceBuf = "";
1461
1621
  const replacementBytes = Buffer.byteLength(text, "utf-8");
1462
1622
  this.#head = "";
1463
1623
  this.#headBytes = 0;
1464
1624
  this.#setTail("");
1625
+ this.#headEndsMidLine = false;
1465
1626
 
1466
1627
  if (this.#headLimit > 0) {
1467
1628
  const head = truncateHeadBytes(text, this.#headLimit);
1468
1629
  this.#appendHead(head.text, head.bytes);
1469
1630
  const tailText = text.substring(head.text.length);
1470
- this.#setTail(tailText, replacementBytes - head.bytes);
1631
+ this.#headEndsMidLine = tailText.length > 0 && !head.text.endsWith("\n") && !tailText.startsWith("\n");
1632
+ this.#setTail(tailText, replacementBytes - head.bytes, this.#headEndsMidLine);
1471
1633
  } else {
1472
1634
  this.#setTail(text, replacementBytes);
1473
1635
  }
@@ -1491,35 +1653,9 @@ export class OutputSink {
1491
1653
  const totalLines = this.#sawData ? this.#totalLines + 1 : 0;
1492
1654
 
1493
1655
  let artifactId: string | undefined;
1494
- if (this.#finalized) {
1495
- artifactId = this.#publishedArtifactId ?? this.#artifactId;
1496
- } else if (!this.#artifactFinalizationFailed) {
1497
- if (this.#artifactPublisher && !this.#artifactPath) {
1498
- artifactId = await this.#publishTerminalArtifact();
1499
- }
1500
- if (!this.#artifactFinalizationFailed && this.#artifactPath) {
1501
- if (this.#artifactTruncatedBytes > 0) this.#createFileSink();
1502
- await this.#writeArtifactTruncationNotice();
1503
- if (this.#file) {
1504
- try {
1505
- await this.#file.sink.end();
1506
- artifactId = this.#file.artifactId;
1507
- this.#finalized = true;
1508
- } catch (error) {
1509
- this.#recordArtifactFailure("end", error);
1510
- this.#artifactFinalizationFailed = true;
1511
- this.#file = undefined;
1512
- this.#fileReady = false;
1513
- }
1514
- }
1515
- }
1516
- }
1517
- if (this.#finalized) {
1518
- // Terminal: the artifact is closed; replay state is no longer needed.
1519
- this.#pendingFileWrites = undefined;
1520
- this.#pendingFileWriteBytes = 0;
1521
- this.#fileReady = false;
1522
- this.#pendingArtifactContent = undefined;
1656
+ if (this.#shouldFinalizeArtifact()) {
1657
+ this.#artifactFinalization ??= this.#finalizeArtifact();
1658
+ artifactId = await this.#artifactFinalization;
1523
1659
  }
1524
1660
  // Non-finalized dumps (no artifact sink ever opened) keep the raw replay
1525
1661
  // queue so a later post-dump push that spills produces a CUMULATIVE
@@ -1543,6 +1679,8 @@ export class OutputSink {
1543
1679
  let outputLines: number;
1544
1680
  let elidedBytes: number | undefined;
1545
1681
  let elidedLines: number | undefined;
1682
+ let headRange: { start: number; end: number } | undefined;
1683
+ let tailRange: { start: number; end: number } | undefined;
1546
1684
 
1547
1685
  if (headBytes > 0 && effectiveTotalBytes > headBytes + tailBytes) {
1548
1686
  // Middle was elided. Emit both explicit windows, including when they are
@@ -1555,7 +1693,12 @@ export class OutputSink {
1555
1693
  const tailSep = tailBuf.startsWith("\n") ? "" : "\n";
1556
1694
  body = `${headText}${headSep}${marker}${tailSep}${tailBuf}`;
1557
1695
  outputBytes = headBytes + markerBytes + tailBytes + headSep.length + tailSep.length;
1558
- outputLines = headLines + 1 + tailLines;
1696
+ outputLines = body.length > 0 ? countNewlines(body) + 1 : 0;
1697
+ headRange = headLines > 0 && !this.#headEndsMidLine ? { start: 1, end: headLines } : undefined;
1698
+ tailRange =
1699
+ tailLines > 0 && !this.#tailStartsMidLine
1700
+ ? { start: processedTotalLines - tailLines + 1, end: processedTotalLines }
1701
+ : undefined;
1559
1702
  this.#truncated = true;
1560
1703
  } else if (headBytes > 0) {
1561
1704
  // Head + tail combine into the full buffered output (no overlap or elision).
@@ -1569,7 +1712,7 @@ export class OutputSink {
1569
1712
  }
1570
1713
 
1571
1714
  if (this.#columnDroppedBytes > 0) this.#truncated = true;
1572
- return {
1715
+ const summary: OutputSummary = {
1573
1716
  output: `${noticeLine}${body}`,
1574
1717
  truncated: this.#truncated,
1575
1718
  totalLines,
@@ -1578,12 +1721,27 @@ export class OutputSink {
1578
1721
  outputBytes,
1579
1722
  elidedBytes,
1580
1723
  elidedLines,
1724
+ headRange,
1725
+ tailRange,
1726
+ firstLinePartial: this.#headEndsMidLine || undefined,
1727
+ lastLinePartial: this.#tailStartsMidLine || undefined,
1581
1728
  columnDroppedBytes: this.#columnDroppedBytes > 0 ? this.#columnDroppedBytes : undefined,
1582
1729
  columnTruncatedLines: this.#columnTruncatedLines > 0 ? this.#columnTruncatedLines : undefined,
1730
+ columnMax: this.#columnDroppedBytes > 0 ? this.#maxColumns : undefined,
1583
1731
  artifactTruncatedBytes: this.#artifactTruncatedBytes > 0 ? this.#artifactTruncatedBytes : undefined,
1584
1732
  artifactFailureDiagnostic: this.#artifactFailureDiagnostic,
1585
1733
  artifactId,
1734
+ artifactVerified: artifactId !== undefined && this.#publishedArtifactId === artifactId,
1586
1735
  };
1736
+ if (summary.artifactVerified === true && artifactId !== undefined) {
1737
+ Object.defineProperty(summary, kCurrentExecutionArtifactProof, {
1738
+ value: { artifactId, payload: summary.output, consumed: false } satisfies CurrentExecutionArtifactProof,
1739
+ enumerable: true,
1740
+ configurable: false,
1741
+ writable: false,
1742
+ });
1743
+ }
1744
+ return summary;
1587
1745
  }
1588
1746
  }
1589
1747
 
@@ -280,7 +280,7 @@ export class ManagedTaskPersistence {
280
280
  throw new Error("Managed task persistence authority is unavailable");
281
281
  }
282
282
 
283
- async openSession(): Promise<SessionManager> {
283
+ async openSession(cwd: string): Promise<SessionManager> {
284
284
  const store = this.#artifacts.getManagedStore();
285
285
  if (!store) throw new Error("Managed task persistence authority is unavailable");
286
286
  this.#artifacts.assertManagedBinding();
@@ -289,6 +289,8 @@ export class ManagedTaskPersistence {
289
289
  sessionFile,
290
290
  SessionManager.nestedManagedDestination(store, this.#artifacts.dir),
291
291
  store,
292
+ undefined,
293
+ cwd,
292
294
  );
293
295
  this.#artifacts.assertManagedBinding();
294
296
  return session;
@@ -1533,6 +1535,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1533
1535
  requestedModel: modelSubstitutionWarning?.requested ?? resolvedModelString,
1534
1536
  effectiveModel: resolvedModelString,
1535
1537
  modelFellBack: authFallbackUsed === true,
1538
+ fastMode: progress.fastMode,
1536
1539
  });
1537
1540
  }
1538
1541
  if (model?.contextWindow && model.contextWindow > 0) {
@@ -1547,7 +1550,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1547
1550
  effectiveThinkingLevelForWarning = effectiveThinkingLevel;
1548
1551
 
1549
1552
  const sessionManager = options.managedPersistence
1550
- ? await awaitAbortable(options.managedPersistence.openSession())
1553
+ ? await awaitAbortable(options.managedPersistence.openSession(worktree ?? cwd))
1551
1554
  : sessionFile
1552
1555
  ? await awaitAbortable(
1553
1556
  SessionManager.open(sessionFile, SessionManager.explicitDestination(path.dirname(sessionFile))),