@gajae-code/coding-agent 0.5.1 → 0.5.3

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 (165) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +1 -1
  3. package/dist/types/async/job-manager.d.ts +6 -0
  4. package/dist/types/cli/setup-cli.d.ts +8 -1
  5. package/dist/types/commands/setup.d.ts +7 -0
  6. package/dist/types/config/file-lock.d.ts +24 -2
  7. package/dist/types/config/model-registry.d.ts +4 -0
  8. package/dist/types/config/models-config-schema.d.ts +5 -0
  9. package/dist/types/config/settings-schema.d.ts +62 -0
  10. package/dist/types/dap/client.d.ts +2 -1
  11. package/dist/types/edit/read-file.d.ts +6 -0
  12. package/dist/types/eval/js/context-manager.d.ts +3 -0
  13. package/dist/types/eval/js/executor.d.ts +1 -0
  14. package/dist/types/exec/bash-executor.d.ts +2 -0
  15. package/dist/types/gjc-runtime/state-writer.d.ts +64 -2
  16. package/dist/types/gjc-runtime/tmux-sessions.d.ts +7 -1
  17. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +10 -0
  18. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +29 -0
  19. package/dist/types/lsp/types.d.ts +2 -0
  20. package/dist/types/modes/bridge/bridge-mode.d.ts +1 -0
  21. package/dist/types/modes/components/model-selector.d.ts +2 -0
  22. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  23. package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
  24. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -0
  25. package/dist/types/modes/components/tool-execution.d.ts +1 -0
  26. package/dist/types/modes/interactive-mode.d.ts +1 -1
  27. package/dist/types/modes/rpc/rpc-mode.d.ts +56 -1
  28. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
  29. package/dist/types/modes/theme/defaults/index.d.ts +302 -0
  30. package/dist/types/modes/theme/theme.d.ts +1 -0
  31. package/dist/types/modes/types.d.ts +1 -1
  32. package/dist/types/runtime/process-lifecycle.d.ts +108 -0
  33. package/dist/types/runtime-mcp/transports/stdio.d.ts +1 -0
  34. package/dist/types/runtime-mcp/types.d.ts +2 -0
  35. package/dist/types/session/agent-session.d.ts +17 -1
  36. package/dist/types/session/artifacts.d.ts +4 -1
  37. package/dist/types/session/history-storage.d.ts +2 -2
  38. package/dist/types/session/session-manager.d.ts +10 -1
  39. package/dist/types/session/streaming-output.d.ts +5 -0
  40. package/dist/types/setup/credential-import.d.ts +79 -0
  41. package/dist/types/slash-commands/helpers/fast-status-report.d.ts +76 -0
  42. package/dist/types/task/executor.d.ts +1 -0
  43. package/dist/types/task/render.d.ts +1 -1
  44. package/dist/types/tools/bash.d.ts +1 -0
  45. package/dist/types/tools/browser/tab-supervisor.d.ts +9 -0
  46. package/dist/types/tools/sqlite-reader.d.ts +2 -1
  47. package/dist/types/tools/subagent-render.d.ts +7 -1
  48. package/dist/types/tools/subagent.d.ts +21 -0
  49. package/dist/types/tools/ultragoal-ask-guard.d.ts +5 -0
  50. package/dist/types/web/search/index.d.ts +4 -4
  51. package/dist/types/web/search/provider.d.ts +16 -20
  52. package/dist/types/web/search/providers/base.d.ts +2 -1
  53. package/dist/types/web/search/providers/openai-compatible.d.ts +9 -0
  54. package/dist/types/web/search/types.d.ts +14 -2
  55. package/package.json +7 -7
  56. package/scripts/build-binary.ts +7 -0
  57. package/src/async/job-manager.ts +153 -39
  58. package/src/cli/args.ts +2 -0
  59. package/src/cli/fast-help.ts +2 -0
  60. package/src/cli/setup-cli.ts +138 -3
  61. package/src/commands/setup.ts +5 -1
  62. package/src/commands/ultragoal.ts +3 -1
  63. package/src/config/file-lock-gc.ts +14 -2
  64. package/src/config/file-lock.ts +63 -13
  65. package/src/config/model-profile-activation.ts +15 -3
  66. package/src/config/model-profiles.ts +15 -15
  67. package/src/config/model-registry.ts +21 -1
  68. package/src/config/models-config-schema.ts +1 -0
  69. package/src/config/settings-schema.ts +62 -0
  70. package/src/dap/client.ts +105 -64
  71. package/src/dap/session.ts +44 -7
  72. package/src/defaults/gjc/skills/ultragoal/SKILL.md +30 -8
  73. package/src/edit/read-file.ts +19 -1
  74. package/src/eval/js/context-manager.ts +228 -65
  75. package/src/eval/js/executor.ts +2 -0
  76. package/src/eval/js/index.ts +1 -0
  77. package/src/eval/js/worker-core.ts +10 -6
  78. package/src/eval/py/executor.ts +68 -19
  79. package/src/eval/py/kernel.ts +46 -22
  80. package/src/eval/py/runner.py +68 -14
  81. package/src/exec/bash-executor.ts +49 -13
  82. package/src/gjc-runtime/deep-interview-recorder.ts +40 -0
  83. package/src/gjc-runtime/launch-tmux.ts +3 -4
  84. package/src/gjc-runtime/ralplan-runtime.ts +174 -12
  85. package/src/gjc-runtime/state-runtime.ts +2 -1
  86. package/src/gjc-runtime/state-writer.ts +254 -7
  87. package/src/gjc-runtime/tmux-gc.ts +88 -38
  88. package/src/gjc-runtime/tmux-sessions.ts +44 -6
  89. package/src/gjc-runtime/ultragoal-guard.ts +155 -0
  90. package/src/gjc-runtime/ultragoal-runtime.ts +1227 -31
  91. package/src/gjc-runtime/workflow-manifest.generated.json +44 -0
  92. package/src/gjc-runtime/workflow-manifest.ts +12 -0
  93. package/src/harness-control-plane/owner.ts +3 -2
  94. package/src/harness-control-plane/rpc-adapter.ts +1 -1
  95. package/src/hooks/skill-state.ts +121 -2
  96. package/src/internal-urls/artifact-protocol.ts +10 -1
  97. package/src/internal-urls/docs-index.generated.ts +14 -10
  98. package/src/lsp/client.ts +64 -26
  99. package/src/lsp/defaults.json +1 -0
  100. package/src/lsp/index.ts +2 -1
  101. package/src/lsp/lspmux.ts +33 -9
  102. package/src/lsp/types.ts +2 -0
  103. package/src/main.ts +14 -4
  104. package/src/modes/acp/acp-agent.ts +4 -2
  105. package/src/modes/bridge/bridge-mode.ts +23 -1
  106. package/src/modes/components/assistant-message.ts +10 -2
  107. package/src/modes/components/bash-execution.ts +5 -1
  108. package/src/modes/components/eval-execution.ts +5 -1
  109. package/src/modes/components/history-search.ts +5 -2
  110. package/src/modes/components/model-selector.ts +60 -2
  111. package/src/modes/components/oauth-selector.ts +5 -0
  112. package/src/modes/components/provider-onboarding-selector.ts +6 -1
  113. package/src/modes/components/runtime-mcp-add-wizard.ts +58 -7
  114. package/src/modes/components/skill-message.ts +24 -16
  115. package/src/modes/components/tool-execution.ts +6 -0
  116. package/src/modes/controllers/extension-ui-controller.ts +33 -6
  117. package/src/modes/controllers/input-controller.ts +5 -0
  118. package/src/modes/controllers/selector-controller.ts +86 -2
  119. package/src/modes/interactive-mode.ts +11 -1
  120. package/src/modes/rpc/rpc-mode.ts +132 -18
  121. package/src/modes/shared/agent-wire/command-dispatch.ts +5 -2
  122. package/src/modes/shared/agent-wire/host-tool-bridge.ts +3 -0
  123. package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
  124. package/src/modes/theme/defaults/claude-code.json +100 -0
  125. package/src/modes/theme/defaults/codex.json +100 -0
  126. package/src/modes/theme/defaults/index.ts +6 -0
  127. package/src/modes/theme/defaults/opencode.json +102 -0
  128. package/src/modes/theme/theme.ts +2 -2
  129. package/src/modes/types.ts +1 -1
  130. package/src/modes/utils/ui-helpers.ts +5 -2
  131. package/src/prompts/agents/executor.md +5 -2
  132. package/src/runtime/process-lifecycle.ts +400 -0
  133. package/src/runtime-mcp/manager.ts +164 -50
  134. package/src/runtime-mcp/transports/http.ts +12 -11
  135. package/src/runtime-mcp/transports/stdio.ts +64 -38
  136. package/src/runtime-mcp/types.ts +3 -0
  137. package/src/sdk.ts +39 -1
  138. package/src/session/agent-session.ts +190 -33
  139. package/src/session/artifacts.ts +17 -2
  140. package/src/session/blob-store.ts +36 -2
  141. package/src/session/history-storage.ts +32 -11
  142. package/src/session/session-manager.ts +99 -31
  143. package/src/session/streaming-output.ts +54 -3
  144. package/src/setup/credential-import.ts +429 -0
  145. package/src/skill-state/deep-interview-mutation-guard.ts +2 -1
  146. package/src/slash-commands/builtin-registry.ts +30 -3
  147. package/src/slash-commands/helpers/fast-status-report.ts +111 -0
  148. package/src/task/executor.ts +7 -1
  149. package/src/task/render.ts +18 -7
  150. package/src/tools/archive-reader.ts +10 -1
  151. package/src/tools/ask.ts +4 -2
  152. package/src/tools/bash.ts +11 -4
  153. package/src/tools/browser/tab-supervisor.ts +22 -0
  154. package/src/tools/browser.ts +38 -4
  155. package/src/tools/cron.ts +1 -1
  156. package/src/tools/read.ts +11 -12
  157. package/src/tools/sqlite-reader.ts +19 -5
  158. package/src/tools/subagent-render.ts +119 -29
  159. package/src/tools/subagent.ts +147 -7
  160. package/src/tools/ultragoal-ask-guard.ts +39 -0
  161. package/src/web/search/index.ts +25 -25
  162. package/src/web/search/provider.ts +178 -87
  163. package/src/web/search/providers/base.ts +2 -1
  164. package/src/web/search/providers/openai-compatible.ts +151 -0
  165. package/src/web/search/types.ts +47 -22
@@ -40,6 +40,7 @@ import {
40
40
  isBlobRef,
41
41
  isImageDataUrl,
42
42
  MemoryBlobStore,
43
+ ResidentBlobMissingError,
43
44
  resolveImageData,
44
45
  resolveImageDataUrl,
45
46
  resolveResidentImageDataSync,
@@ -888,8 +889,27 @@ async function resolvePersistedBlobRefs(value: unknown, blobStore: BlobStore, ke
888
889
  );
889
890
  }
890
891
 
892
+ /**
893
+ * Run async tasks with bounded concurrency so an image-heavy resume never materializes
894
+ * every blob's base64 simultaneously (F8: avoids the transient OOM spike of an unbounded
895
+ * Promise.all over all historical images).
896
+ */
897
+ const BLOB_RESOLVE_CONCURRENCY = 8;
898
+ async function runWithConcurrency(tasks: Array<() => Promise<void>>, limit: number): Promise<void> {
899
+ let next = 0;
900
+ const worker = async (): Promise<void> => {
901
+ while (next < tasks.length) {
902
+ const index = next;
903
+ next += 1;
904
+ await tasks[index]!();
905
+ }
906
+ };
907
+ const workerCount = Math.max(1, Math.min(limit, tasks.length));
908
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
909
+ }
910
+
891
911
  async function resolveBlobRefsInEntries(entries: FileEntry[], blobStore: BlobStore): Promise<void> {
892
- const promises: Promise<void>[] = [];
912
+ const tasks: Array<() => Promise<void>> = [];
893
913
 
894
914
  for (const entry of entries) {
895
915
  if (entry.type === "session") continue;
@@ -901,22 +921,19 @@ async function resolveBlobRefsInEntries(entries: FileEntry[], blobStore: BlobSto
901
921
  contentArray = entry.content;
902
922
  }
903
923
 
904
- if (contentArray) {
905
- for (const block of contentArray) {
906
- if (isImageBlock(block) && isBlobRef(block.data)) {
907
- promises.push(
908
- resolveImageData(blobStore, block.data).then(resolved => {
909
- block.data = resolved;
910
- }),
911
- );
924
+ tasks.push(async () => {
925
+ if (contentArray) {
926
+ for (const block of contentArray) {
927
+ if (isImageBlock(block) && isBlobRef(block.data)) {
928
+ block.data = await resolveImageData(blobStore, block.data);
929
+ }
912
930
  }
913
931
  }
914
- }
915
-
916
- promises.push(resolvePersistedBlobRefs(entry, blobStore));
932
+ await resolvePersistedBlobRefs(entry, blobStore);
933
+ });
917
934
  }
918
935
 
919
- await Promise.all(promises);
936
+ await runWithConcurrency(tasks, BLOB_RESOLVE_CONCURRENCY);
920
937
  }
921
938
 
922
939
  /**
@@ -1201,6 +1218,12 @@ interface ResidentBlobStores {
1201
1218
  sessionFile?: string;
1202
1219
  }
1203
1220
 
1221
+ type ResidentBlobMissingPolicy = "throw" | "placeholder";
1222
+
1223
+ function residentBlobMissingPlaceholder(error: ResidentBlobMissingError): string {
1224
+ return `[Session resident ${error.kind} blob missing: sha256:${error.hash}; original content unavailable]`;
1225
+ }
1226
+
1204
1227
  function externalizeResidentValueSync(obj: unknown, stores: ResidentBlobStores, key?: string): unknown {
1205
1228
  if (obj === null || obj === undefined) return obj;
1206
1229
  if (typeof obj === "string") {
@@ -1256,6 +1279,7 @@ function materializeResidentValueSync(
1256
1279
  stores: ResidentBlobStores,
1257
1280
  key?: string,
1258
1281
  cache = new Map<string, string>(),
1282
+ missingPolicy: ResidentBlobMissingPolicy = "throw",
1259
1283
  ): unknown {
1260
1284
  if (obj === null || obj === undefined) return obj;
1261
1285
  if (typeof obj === "string") return obj;
@@ -1263,19 +1287,28 @@ function materializeResidentValueSync(
1263
1287
  const cacheKey = `${obj.kind}:${obj.ref}`;
1264
1288
  const cached = cache.get(cacheKey);
1265
1289
  if (cached !== undefined) return cached;
1266
- const resolved =
1267
- obj.kind === "imageUrl"
1268
- ? resolveResidentImageDataUrlSync(stores.imageStore, obj.ref, stores)
1269
- : obj.kind === "imageData"
1270
- ? resolveResidentImageDataSync(stores.imageStore, obj.ref, stores)
1271
- : resolveTextBlobSync(stores.textStore, obj.ref, stores);
1290
+ let resolved: string;
1291
+ try {
1292
+ resolved =
1293
+ obj.kind === "imageUrl"
1294
+ ? resolveResidentImageDataUrlSync(stores.imageStore, obj.ref, stores)
1295
+ : obj.kind === "imageData"
1296
+ ? resolveResidentImageDataSync(stores.imageStore, obj.ref, stores)
1297
+ : resolveTextBlobSync(stores.textStore, obj.ref, stores);
1298
+ } catch (err) {
1299
+ if (missingPolicy === "placeholder" && err instanceof ResidentBlobMissingError) {
1300
+ resolved = residentBlobMissingPlaceholder(err);
1301
+ } else {
1302
+ throw err;
1303
+ }
1304
+ }
1272
1305
  cache.set(cacheKey, resolved);
1273
1306
  return resolved;
1274
1307
  }
1275
1308
  if (Array.isArray(obj)) {
1276
1309
  let changed = false;
1277
1310
  const result = obj.map(item => {
1278
- const newItem = materializeResidentValueSync(item, stores, key, cache);
1311
+ const newItem = materializeResidentValueSync(item, stores, key, cache, missingPolicy);
1279
1312
  if (newItem !== item) changed = true;
1280
1313
  return newItem;
1281
1314
  });
@@ -1284,7 +1317,7 @@ function materializeResidentValueSync(
1284
1317
  if (typeof obj === "object") {
1285
1318
  let changed = false;
1286
1319
  const entries = Object.entries(obj).map(([childKey, value]) => {
1287
- const newValue = materializeResidentValueSync(value, stores, childKey, cache);
1320
+ const newValue = materializeResidentValueSync(value, stores, childKey, cache, missingPolicy);
1288
1321
  if (newValue !== value) changed = true;
1289
1322
  return [childKey, newValue] as const;
1290
1323
  });
@@ -1297,8 +1330,9 @@ function materializeResidentEntrySync<T extends FileEntry | SessionEntry>(
1297
1330
  entry: T,
1298
1331
  stores: ResidentBlobStores,
1299
1332
  cache: Map<string, string>,
1333
+ missingPolicy: ResidentBlobMissingPolicy = "throw",
1300
1334
  ): T {
1301
- return materializeResidentValueSync(entry, stores, undefined, cache) as T;
1335
+ return materializeResidentValueSync(entry, stores, undefined, cache, missingPolicy) as T;
1302
1336
  }
1303
1337
 
1304
1338
  function materializeResidentEntriesSync<T extends FileEntry | SessionEntry>(
@@ -1308,6 +1342,37 @@ function materializeResidentEntriesSync<T extends FileEntry | SessionEntry>(
1308
1342
  const cache = new Map<string, string>();
1309
1343
  return entries.map(entry => materializeResidentEntrySync(entry, stores, cache));
1310
1344
  }
1345
+
1346
+ function materializeResidentEntryForPersistenceSync<T extends FileEntry | SessionEntry>(
1347
+ entry: T,
1348
+ stores: ResidentBlobStores,
1349
+ cache: Map<string, string>,
1350
+ ): T {
1351
+ return materializeResidentEntrySync(entry, stores, cache, "placeholder");
1352
+ }
1353
+
1354
+ function materializeResidentEntriesForPersistenceSync<T extends FileEntry | SessionEntry>(
1355
+ entries: T[],
1356
+ stores: ResidentBlobStores,
1357
+ ): T[] {
1358
+ const cache = new Map<string, string>();
1359
+ return entries.map(entry => materializeResidentEntryForPersistenceSync(entry, stores, cache));
1360
+ }
1361
+
1362
+ export function residentBlobSentinelForTests(kind: ResidentBlobKind, ref: string): ResidentBlobSentinel {
1363
+ return residentBlobSentinel(kind, ref);
1364
+ }
1365
+
1366
+ export function materializeResidentEntriesForPersistenceForTests<T>(
1367
+ entries: T[],
1368
+ textStore: BlobStore,
1369
+ imageStore: BlobStore = textStore,
1370
+ ): T[] {
1371
+ return materializeResidentEntriesForPersistenceSync(entries as Array<T & FileEntry>, {
1372
+ textStore,
1373
+ imageStore,
1374
+ }) as T[];
1375
+ }
1311
1376
  function cloneJsonSemantic<T>(value: T): T {
1312
1377
  if (value === null || value === undefined || typeof value !== "object") return value;
1313
1378
  if (Array.isArray(value)) return value.map(item => cloneJsonSemantic(item)) as T;
@@ -2109,7 +2174,7 @@ export class SessionManager {
2109
2174
  #labelRevision = 0;
2110
2175
  #replayMetadataRevision = 0;
2111
2176
  #materializedEntriesRevision = -1;
2112
- #materializedEntriesCache: WeakRef<SessionEntry[]> | undefined;
2177
+ #materializedEntriesCache: SessionEntry[] | undefined;
2113
2178
  #sessionContextCache: WeakRef<SessionContext> | undefined;
2114
2179
  #sessionContextEntryRevision = -1;
2115
2180
  #sessionContextLeafRevision = -1;
@@ -2817,7 +2882,7 @@ export class SessionManager {
2817
2882
  await this.#queuePersistTask(async () => {
2818
2883
  await this.#closePersistWriterInternal();
2819
2884
  const entries = await Promise.all(
2820
- materializeResidentEntriesSync(this.#fileEntries, this.#residentBlobStores()).map(entry =>
2885
+ materializeResidentEntriesForPersistenceSync(this.#fileEntries, this.#residentBlobStores()).map(entry =>
2821
2886
  prepareEntryForPersistence(entry, this.#blobStore),
2822
2887
  ),
2823
2888
  );
@@ -2831,8 +2896,8 @@ export class SessionManager {
2831
2896
  #rewriteFileSync(): void {
2832
2897
  if (!this.persist || !this.#sessionFile) return;
2833
2898
  this.#closePersistWriterInternalSync();
2834
- const entries = materializeResidentEntriesSync(this.#fileEntries, this.#residentBlobStores()).map(entry =>
2835
- prepareEntryForPersistenceSync(entry, this.#blobStore),
2899
+ const entries = materializeResidentEntriesForPersistenceSync(this.#fileEntries, this.#residentBlobStores()).map(
2900
+ entry => prepareEntryForPersistenceSync(entry, this.#blobStore),
2836
2901
  );
2837
2902
  this.#writeEntriesAtomicallySync(entries);
2838
2903
  this.#needsFullRewriteOnNextPersist = false;
@@ -3139,7 +3204,11 @@ export class SessionManager {
3139
3204
  this.#rewriteFile().catch(() => {});
3140
3205
  return;
3141
3206
  }
3142
- const materializedEntry = materializeResidentEntrySync(entry, this.#residentBlobStores(), new Map());
3207
+ const materializedEntry = materializeResidentEntryForPersistenceSync(
3208
+ entry,
3209
+ this.#residentBlobStores(),
3210
+ new Map(),
3211
+ );
3143
3212
  const persistedEntry = prepareEntryForPersistenceSync(materializedEntry, this.#blobStore);
3144
3213
  writer.writeSync(persistedEntry);
3145
3214
  } catch (err) {
@@ -3608,15 +3677,14 @@ export class SessionManager {
3608
3677
  * change the leaf pointer. Entries cannot be modified or deleted.
3609
3678
  */
3610
3679
  #getMaterializedEntriesInternal(): SessionEntry[] {
3611
- if (this.#materializedEntriesRevision === this.#entryRevision) {
3612
- const cached = this.#materializedEntriesCache?.deref();
3613
- if (cached) return cached;
3680
+ if (this.#materializedEntriesRevision === this.#entryRevision && this.#materializedEntriesCache) {
3681
+ return this.#materializedEntriesCache;
3614
3682
  }
3615
3683
  const resolvedTextBlobCache = new Map<string, string>();
3616
3684
  const materializedEntries = this.#fileEntries
3617
3685
  .filter((e): e is SessionEntry => e.type !== "session")
3618
3686
  .map(entry => materializeResidentEntrySync(entry, this.#residentBlobStores(), resolvedTextBlobCache));
3619
- this.#materializedEntriesCache = new WeakRef(materializedEntries);
3687
+ this.#materializedEntriesCache = materializedEntries;
3620
3688
  this.#materializedEntriesRevision = this.#entryRevision;
3621
3689
  return materializedEntries;
3622
3690
  }
@@ -15,6 +15,7 @@ function sanitizeOutputChunk(rawChunk: string): string {
15
15
  export const DEFAULT_MAX_LINES = 3000;
16
16
  export const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB
17
17
  export const DEFAULT_MAX_COLUMN = 1024; // Max chars per grep match line
18
+ export const DEFAULT_ARTIFACT_MAX_BYTES = 10 * 1024 * 1024; // 10MB
18
19
 
19
20
  const NL = "\n";
20
21
 
@@ -41,6 +42,8 @@ export interface OutputSummary {
41
42
  columnTruncatedLines?: number;
42
43
  /** Artifact ID for internal URL access (artifact://<id>) when truncated */
43
44
  artifactId?: string;
45
+ /** Bytes omitted from artifact storage after the artifact hard cap was reached. */
46
+ artifactTruncatedBytes?: number;
44
47
  }
45
48
 
46
49
  export interface OutputSinkOptions {
@@ -61,6 +64,8 @@ export interface OutputSinkOptions {
61
64
  * writes still respect the budget. Default 0 = no per-line cap.
62
65
  */
63
66
  maxColumns?: number;
67
+ /** Hard cap for artifact writes/pending replay. Default DEFAULT_ARTIFACT_MAX_BYTES. */
68
+ artifactMaxBytes?: number;
64
69
  onChunk?: (chunk: string) => void;
65
70
  /** Minimum ms between onChunk calls. 0 = every chunk (default). */
66
71
  chunkThrottleMs?: number;
@@ -668,6 +673,9 @@ export class OutputSink {
668
673
  #sawData = false;
669
674
  #truncated = false;
670
675
  #lastChunkTime = 0;
676
+ #artifactBytes = 0;
677
+ #artifactTruncatedBytes = 0;
678
+ #artifactTruncationNoticeWritten = false;
671
679
 
672
680
  // Per-line column cap streaming state (persists across `push` calls so a
673
681
  // long line split across chunks still trips the same trigger).
@@ -697,6 +705,7 @@ export class OutputSink {
697
705
  readonly #onRawChunk?: (chunk: string) => void;
698
706
  readonly #chunkThrottleMs: number;
699
707
  readonly #maxColumns: number;
708
+ readonly #artifactMaxBytes: number;
700
709
 
701
710
  constructor(options?: OutputSinkOptions) {
702
711
  const {
@@ -708,6 +717,7 @@ export class OutputSink {
708
717
  onChunk,
709
718
  chunkThrottleMs = 0,
710
719
  onRawChunk,
720
+ artifactMaxBytes = DEFAULT_ARTIFACT_MAX_BYTES,
711
721
  } = options ?? {};
712
722
  this.#artifactPath = artifactPath;
713
723
  this.#artifactId = artifactId;
@@ -717,6 +727,7 @@ export class OutputSink {
717
727
  this.#onChunk = onChunk;
718
728
  this.#onRawChunk = onRawChunk;
719
729
  this.#chunkThrottleMs = chunkThrottleMs;
730
+ this.#artifactMaxBytes = Math.max(0, artifactMaxBytes);
720
731
  }
721
732
 
722
733
  #headText(): string {
@@ -907,6 +918,40 @@ export class OutputSink {
907
918
  }
908
919
  }
909
920
 
921
+ #artifactTruncationNotice(droppedBytes: number): string {
922
+ return `\n[artifact truncated after ${this.#artifactBytes} bytes; omitted at least ${droppedBytes} bytes]\n`;
923
+ }
924
+
925
+ #capArtifactChunk(chunk: string, bytes: number): { chunk: string; bytes: number } | null {
926
+ if (bytes === 0) return null;
927
+ if (this.#artifactMaxBytes <= 0 || this.#artifactBytes >= this.#artifactMaxBytes) {
928
+ this.#artifactTruncatedBytes += bytes;
929
+ return null;
930
+ }
931
+ const room = this.#artifactMaxBytes - this.#artifactBytes;
932
+ if (bytes <= room) {
933
+ return { chunk, bytes };
934
+ }
935
+ const kept = truncateHeadBytes(chunk, room);
936
+ this.#artifactTruncatedBytes += bytes - kept.bytes;
937
+ return kept.bytes > 0 ? { chunk: kept.text, bytes: kept.bytes } : null;
938
+ }
939
+
940
+ #writeArtifactTruncationNotice(): void {
941
+ if (this.#artifactTruncatedBytes <= 0 || this.#artifactTruncationNoticeWritten) return;
942
+ const notice = this.#artifactTruncationNotice(this.#artifactTruncatedBytes);
943
+ try {
944
+ if (this.#fileReady && this.#file) {
945
+ this.#file.sink.write(notice);
946
+ } else {
947
+ this.#queuePendingFileWrite(notice, Buffer.byteLength(notice, "utf-8"));
948
+ }
949
+ this.#artifactTruncationNoticeWritten = true;
950
+ } catch {
951
+ /* ignore */
952
+ }
953
+ }
954
+
910
955
  #queuePendingFileWrite(chunk: string, bytes = Buffer.byteLength(chunk, "utf-8")): void {
911
956
  if (!this.#pendingFileWrites) this.#pendingFileWrites = [chunk];
912
957
  else this.#pendingFileWrites.push(chunk);
@@ -915,14 +960,17 @@ export class OutputSink {
915
960
  }
916
961
 
917
962
  #enqueueFileWrite(chunk: string, bytes: number): void {
963
+ const capped = this.#capArtifactChunk(chunk, bytes);
964
+ if (!capped) return;
965
+ this.#artifactBytes += capped.bytes;
918
966
  if (!this.#fileReady || !this.#file) {
919
- this.#queuePendingFileWrite(chunk, bytes);
967
+ this.#queuePendingFileWrite(capped.chunk, capped.bytes);
920
968
  if (this.#willOverflow(bytes) || this.#pendingFileWriteBytes > this.#spillThreshold) this.#createFileSink();
921
969
  return;
922
970
  }
923
971
 
924
972
  try {
925
- this.#file.sink.write(chunk);
973
+ this.#file.sink.write(capped.chunk);
926
974
  } catch {
927
975
  try {
928
976
  void this.#file.sink.end();
@@ -931,7 +979,7 @@ export class OutputSink {
931
979
  }
932
980
  this.#file = undefined;
933
981
  this.#fileReady = false;
934
- this.#queuePendingFileWrite(chunk, bytes);
982
+ this.#queuePendingFileWrite(capped.chunk, capped.bytes);
935
983
  this.#createFileSink();
936
984
  }
937
985
  }
@@ -1019,6 +1067,8 @@ export class OutputSink {
1019
1067
  const totalLines = this.#sawData ? this.#totalLines + 1 : 0;
1020
1068
 
1021
1069
  let artifactId: string | undefined;
1070
+ if (this.#artifactTruncatedBytes > 0) this.#createFileSink();
1071
+ this.#writeArtifactTruncationNotice();
1022
1072
  if (this.#file) {
1023
1073
  artifactId = this.#file.artifactId;
1024
1074
  await this.#file.sink.end();
@@ -1095,6 +1145,7 @@ export class OutputSink {
1095
1145
  elidedLines,
1096
1146
  columnDroppedBytes: this.#columnDroppedBytes > 0 ? this.#columnDroppedBytes : undefined,
1097
1147
  columnTruncatedLines: this.#columnTruncatedLines > 0 ? this.#columnTruncatedLines : undefined,
1148
+ artifactTruncatedBytes: this.#artifactTruncatedBytes > 0 ? this.#artifactTruncatedBytes : undefined,
1098
1149
  artifactId,
1099
1150
  };
1100
1151
  }