@gajae-code/coding-agent 0.12.0 → 0.12.2

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 (247) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/dist/types/async/job-manager.d.ts +17 -3
  3. package/dist/types/config/settings-schema.d.ts +56 -2
  4. package/dist/types/exa/mcp-client.d.ts +10 -1
  5. package/dist/types/exec/bash-executor.d.ts +45 -12
  6. package/dist/types/extensibility/extensions/types.d.ts +16 -2
  7. package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
  8. package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
  9. package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
  10. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
  11. package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
  12. package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
  13. package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
  14. package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
  15. package/dist/types/gjc-runtime/ralplan-review-conflicts.d.ts +114 -0
  16. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +23 -3
  17. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  18. package/dist/types/gjc-runtime/team-runtime.d.ts +25 -0
  19. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
  20. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +10 -0
  21. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +37 -80
  22. package/dist/types/modes/acp/acp-agent.d.ts +2 -19
  23. package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
  24. package/dist/types/modes/components/irc-sidebar.d.ts +16 -0
  25. package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
  26. package/dist/types/modes/components/session-observer-overlay.d.ts +18 -0
  27. package/dist/types/modes/components/settings-selector.d.ts +9 -0
  28. package/dist/types/modes/components/status-line/gh.d.ts +5 -0
  29. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
  30. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  31. package/dist/types/modes/controllers/event-controller.d.ts +1 -1
  32. package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
  33. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  34. package/dist/types/modes/interactive-mode.d.ts +9 -0
  35. package/dist/types/modes/theme/theme.d.ts +3 -1
  36. package/dist/types/modes/types.d.ts +32 -0
  37. package/dist/types/reminders/star-reminder.d.ts +3 -13
  38. package/dist/types/runtime-mcp/manager.d.ts +1 -0
  39. package/dist/types/sdk/acp/adapter.d.ts +3 -1
  40. package/dist/types/sdk/broker/transport.d.ts +2 -0
  41. package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
  42. package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -1
  43. package/dist/types/sdk/bus/config.d.ts +6 -2
  44. package/dist/types/sdk/bus/index.d.ts +29 -0
  45. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +8 -1
  46. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  47. package/dist/types/sdk/bus/reconciliation-store.d.ts +7 -3
  48. package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
  49. package/dist/types/sdk/bus/rich-render.d.ts +2 -0
  50. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
  51. package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
  52. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -4
  53. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
  54. package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
  55. package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
  56. package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
  57. package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
  58. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  59. package/dist/types/sdk/prompt-status.d.ts +23 -3
  60. package/dist/types/sdk/session.d.ts +6 -0
  61. package/dist/types/session/agent-session.d.ts +40 -2
  62. package/dist/types/session/artifacts.d.ts +8 -0
  63. package/dist/types/session/blob-store.d.ts +57 -4
  64. package/dist/types/session/internal/managed-session-storage.d.ts +30 -4
  65. package/dist/types/session/session-manager.d.ts +23 -6
  66. package/dist/types/session/session-storage.d.ts +4 -0
  67. package/dist/types/session/streaming-output.d.ts +36 -7
  68. package/dist/types/setup/credential-import.d.ts +12 -0
  69. package/dist/types/skill-state/active-state.d.ts +12 -0
  70. package/dist/types/skill-state/workflow-hud.d.ts +6 -0
  71. package/dist/types/task/executor.d.ts +3 -2
  72. package/dist/types/task/index.d.ts +7 -1
  73. package/dist/types/task/output-manager.d.ts +1 -0
  74. package/dist/types/task/provider-retry-status.d.ts +8 -0
  75. package/dist/types/task/receipt.d.ts +9 -1
  76. package/dist/types/task/types.d.ts +23 -0
  77. package/dist/types/tools/ask-answer-registry.d.ts +2 -2
  78. package/dist/types/tools/bash-interactive.d.ts +4 -1
  79. package/dist/types/tools/bash.d.ts +3 -1
  80. package/dist/types/tools/image-gen.d.ts +4 -0
  81. package/dist/types/tools/index.d.ts +6 -0
  82. package/dist/types/tools/output-meta.d.ts +23 -2
  83. package/dist/types/tools/path-utils.d.ts +1 -0
  84. package/dist/types/tools/read.d.ts +0 -6
  85. package/dist/types/tools/review.d.ts +5 -7
  86. package/dist/types/tools/subagent.d.ts +2 -0
  87. package/dist/types/utils/gh.d.ts +13 -0
  88. package/dist/types/utils/image-resize.d.ts +1 -0
  89. package/dist/types/web/search/providers/kimi.d.ts +2 -0
  90. package/dist/types/web/search/providers/searxng.d.ts +7 -0
  91. package/dist/types/web/search/providers/xai.d.ts +2 -0
  92. package/package.json +10 -8
  93. package/scripts/acp-conformance-agent.ts +218 -0
  94. package/scripts/capture-gjc-bundle-settings.ts +306 -0
  95. package/scripts/dogfood-ralplan-review-conflicts.ts +272 -0
  96. package/scripts/generate-sdk-operation-inventory.ts +5 -0
  97. package/scripts/resident-memory-bench.ts +1341 -0
  98. package/scripts/run-acp-conformance.ts +290 -0
  99. package/src/async/job-manager.ts +99 -39
  100. package/src/cli/fast-help.ts +1 -1
  101. package/src/cli/plugin-cli.ts +176 -18
  102. package/src/commands/team.ts +19 -1
  103. package/src/config/file-lock.ts +11 -2
  104. package/src/config/settings-schema.ts +53 -2
  105. package/src/coordinator-mcp/question-gate-codec.ts +1 -1
  106. package/src/coordinator-mcp/server.ts +5 -12
  107. package/src/debug/index.ts +42 -17
  108. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -0
  109. package/src/defaults/gjc/skills/ralplan/SKILL.md +25 -20
  110. package/src/defaults/gjc/skills/team/SKILL.md +2 -0
  111. package/src/defaults/gjc/skills/ultragoal/SKILL.md +39 -26
  112. package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
  113. package/src/defaults/gjc-defaults.ts +4 -4
  114. package/src/exa/mcp-client.ts +12 -3
  115. package/src/exec/bash-executor.ts +144 -27
  116. package/src/extensibility/extensions/runner.ts +15 -0
  117. package/src/extensibility/extensions/types.ts +18 -1
  118. package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
  119. package/src/extensibility/gjc-plugins/index.ts +19 -2
  120. package/src/extensibility/gjc-plugins/installer.ts +189 -50
  121. package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
  122. package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
  123. package/src/extensibility/gjc-plugins/observability.ts +3 -3
  124. package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
  125. package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
  126. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
  127. package/src/extensibility/gjc-plugins/schema.ts +84 -12
  128. package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
  129. package/src/extensibility/gjc-plugins/types.ts +141 -0
  130. package/src/extensibility/gjc-plugins/validation.ts +9 -0
  131. package/src/gjc-runtime/launch-tmux.ts +25 -6
  132. package/src/gjc-runtime/launch-worktree.ts +103 -1
  133. package/src/gjc-runtime/ledger-event-renderer.ts +1 -0
  134. package/src/gjc-runtime/managed-owner-admission.ts +1 -1
  135. package/src/gjc-runtime/ralplan-review-conflicts.ts +472 -0
  136. package/src/gjc-runtime/ralplan-runtime.ts +491 -27
  137. package/src/gjc-runtime/team-launch.ts +8 -0
  138. package/src/gjc-runtime/team-runtime.ts +140 -32
  139. package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
  140. package/src/gjc-runtime/tmux-owner-isolation.ts +10 -0
  141. package/src/gjc-runtime/tmux-sessions.ts +58 -13
  142. package/src/gjc-runtime/ultragoal-evidence.ts +6 -74
  143. package/src/gjc-runtime/ultragoal-runtime.ts +635 -612
  144. package/src/gjc-runtime/workflow-manifest.generated.json +37 -99
  145. package/src/gjc-runtime/workflow-manifest.ts +21 -70
  146. package/src/internal-urls/docs-index.generated.ts +16 -16
  147. package/src/modes/DESIGN.md +104 -0
  148. package/src/modes/acp/acp-agent.ts +298 -186
  149. package/src/modes/components/gjc-bundle-settings.ts +508 -0
  150. package/src/modes/components/irc-sidebar.ts +30 -1
  151. package/src/modes/components/notifications-settings-editor.ts +12 -1
  152. package/src/modes/components/oauth-selector.ts +68 -30
  153. package/src/modes/components/session-observer-overlay.ts +305 -86
  154. package/src/modes/components/settings-selector.ts +56 -5
  155. package/src/modes/components/status-line/gh.ts +16 -0
  156. package/src/modes/components/tool-status-header.ts +3 -15
  157. package/src/modes/components/transcript-viewer-overlay.ts +99 -24
  158. package/src/modes/controllers/command-controller.ts +42 -34
  159. package/src/modes/controllers/event-controller.ts +27 -16
  160. package/src/modes/controllers/extension-ui-controller.ts +193 -34
  161. package/src/modes/controllers/selector-controller.ts +160 -22
  162. package/src/modes/interactive-mode.ts +189 -62
  163. package/src/modes/jobs-observer.ts +1 -1
  164. package/src/modes/runtime-init.ts +3 -0
  165. package/src/modes/theme/theme.ts +11 -9
  166. package/src/modes/types.ts +88 -0
  167. package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
  168. package/src/prompts/system/eager-todo.md +8 -4
  169. package/src/prompts/system/system-prompt.md +11 -4
  170. package/src/prompts/tools/bash.md +5 -5
  171. package/src/prompts/tools/search-tool-bm25.md +5 -0
  172. package/src/prompts/tools/task.md +1 -1
  173. package/src/reminders/star-reminder.ts +4 -42
  174. package/src/runtime-mcp/manager.ts +19 -4
  175. package/src/sdk/acp/adapter.ts +22 -6
  176. package/src/sdk/broker/lifecycle.ts +18 -13
  177. package/src/sdk/broker/transport.ts +35 -3
  178. package/src/sdk/bus/attachment-registry.ts +5 -1
  179. package/src/sdk/bus/chat-daemon-cli.ts +4 -39
  180. package/src/sdk/bus/chat-daemon-control.ts +7 -3
  181. package/src/sdk/bus/config.ts +18 -2
  182. package/src/sdk/bus/index.ts +661 -126
  183. package/src/sdk/bus/kind-aware-reconciliation.ts +148 -73
  184. package/src/sdk/bus/lifecycle-commands.ts +4 -1
  185. package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
  186. package/src/sdk/bus/reconciliation-store.ts +95 -16
  187. package/src/sdk/bus/rich-draft.ts +1 -1
  188. package/src/sdk/bus/rich-render.ts +2 -2
  189. package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
  190. package/src/sdk/bus/telegram-cli.ts +33 -8
  191. package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
  192. package/src/sdk/bus/telegram-daemon-contract.ts +12 -4
  193. package/src/sdk/bus/telegram-daemon.ts +916 -23
  194. package/src/sdk/bus/telegram-reference.ts +40 -3
  195. package/src/sdk/bus/threaded-render.ts +48 -3
  196. package/src/sdk/bus/topic-registry.ts +20 -0
  197. package/src/sdk/host/query/revision-store.ts +36 -2
  198. package/src/sdk/host/reverse-leases.ts +44 -7
  199. package/src/sdk/prompt-status.ts +16 -3
  200. package/src/sdk/protocol/operation-inventory.generated.json +33 -0
  201. package/src/sdk/session.ts +89 -9
  202. package/src/session/agent-session.ts +844 -173
  203. package/src/session/artifacts.ts +22 -0
  204. package/src/session/auth-broker-config.ts +10 -3
  205. package/src/session/blob-store.ts +915 -24
  206. package/src/session/internal/managed-session-scope.ts +200 -111
  207. package/src/session/internal/managed-session-storage.ts +492 -62
  208. package/src/session/session-manager.ts +1614 -435
  209. package/src/session/session-storage.ts +38 -45
  210. package/src/session/streaming-output.ts +238 -68
  211. package/src/setup/credential-import.ts +75 -13
  212. package/src/skill-state/active-state.ts +32 -0
  213. package/src/skill-state/workflow-hud.ts +17 -0
  214. package/src/skill-state/workflow-mutation-guard.ts +496 -2
  215. package/src/task/executor.ts +122 -39
  216. package/src/task/index.ts +280 -45
  217. package/src/task/output-manager.ts +24 -14
  218. package/src/task/provider-retry-status.ts +16 -0
  219. package/src/task/receipt.ts +34 -8
  220. package/src/task/render.ts +6 -2
  221. package/src/task/types.ts +108 -0
  222. package/src/tools/ask-answer-registry.ts +12 -6
  223. package/src/tools/ask.ts +86 -55
  224. package/src/tools/ast-edit.ts +1 -0
  225. package/src/tools/ast-grep.ts +1 -0
  226. package/src/tools/bash-interactive.ts +8 -3
  227. package/src/tools/bash.ts +503 -60
  228. package/src/tools/find.ts +1 -0
  229. package/src/tools/image-gen.ts +29 -5
  230. package/src/tools/index.ts +6 -0
  231. package/src/tools/output-meta.ts +108 -14
  232. package/src/tools/path-utils.ts +3 -2
  233. package/src/tools/read.ts +18 -0
  234. package/src/tools/review.ts +5 -7
  235. package/src/tools/search.ts +1 -0
  236. package/src/tools/subagent-render.ts +6 -2
  237. package/src/tools/subagent.ts +6 -0
  238. package/src/tools/telegram-send.ts +6 -3
  239. package/src/utils/gh.ts +39 -0
  240. package/src/utils/image-resize.ts +3 -0
  241. package/src/web/search/providers/anthropic.ts +4 -4
  242. package/src/web/search/providers/kimi.ts +15 -5
  243. package/src/web/search/providers/searxng.ts +47 -4
  244. package/src/web/search/providers/xai.ts +7 -2
  245. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
  246. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
  247. package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
@@ -106,6 +106,8 @@ export interface SessionStorageWriterOpenOptions {
106
106
  export interface ManagedSessionSecurityContext {
107
107
  readonly kind: "managed";
108
108
  readonly agentDir: string;
109
+ /** Logical profile root for process-local caches; distinct from managed pathname authority. */
110
+ readonly profileAgentDir: string;
109
111
  readonly sessionsRoot: string;
110
112
  readonly sessionDir: string;
111
113
  readonly rootAuthority: ManagedDirectoryRoot;
@@ -117,12 +119,18 @@ const managedSecurityContexts = new WeakSet<ManagedSessionSecurityContext>();
117
119
  /** @internal Create the only accepted managed writer authority object. */
118
120
  export function createManagedSessionSecurityContext(input: {
119
121
  agentDir: string;
122
+ /** Optional for compatibility with existing authority-only callers. */
123
+ profileAgentDir?: string;
120
124
  sessionsRoot: string;
121
125
  sessionDir: string;
122
126
  rootAuthority: ManagedDirectoryRoot;
123
127
  retainedAuthority?: native.RecoveryFsRoot;
124
128
  }): ManagedSessionSecurityContext {
125
- const context = Object.freeze({ kind: "managed" as const, ...input });
129
+ const context = Object.freeze({
130
+ kind: "managed" as const,
131
+ ...input,
132
+ profileAgentDir: input.profileAgentDir ?? input.agentDir,
133
+ });
126
134
  managedSecurityContexts.add(context);
127
135
  return context;
128
136
  }
@@ -446,7 +454,7 @@ function secureOwnerOnlyFileDescriptor(
446
454
  ): void {
447
455
  if (securityContext && !isValidManagedSecurityContext(securityContext))
448
456
  throw new Error("Invalid managed session security context");
449
- if (process.platform !== "linux" || !securityContext) {
457
+ if (process.platform !== "linux" && process.platform !== "win32") {
450
458
  if (operation === "apply") {
451
459
  const applied = validateNativeSecurityResult(
452
460
  native.applyOwnerOnlyPathSecurity(pathname, "file"),
@@ -463,7 +471,7 @@ function secureOwnerOnlyFileDescriptor(
463
471
  if (!verified.ok) throw new Error(`Owner-only security rejected ${pathname}: ${verified.code}`);
464
472
  return;
465
473
  }
466
- if (!pathIsWithin(securityContext.sessionDir, pathname))
474
+ if (securityContext && !pathIsWithin(securityContext.sessionDir, pathname))
467
475
  throw new Error(`Managed writer escaped its session directory: ${pathname}`);
468
476
  const result = validateNativeSecurityResult(
469
477
  operation === "apply"
@@ -900,6 +908,28 @@ export class FileSessionStorage implements SessionStorage {
900
908
  }
901
909
 
902
910
  const parentIdentity = this.#directoryIdentity(path.dirname(transcriptPath));
911
+ const durablyRecordArtifactPhase = (): void => {
912
+ if (process.platform === "win32") return;
913
+ let descriptor: number | undefined;
914
+ try {
915
+ descriptor = fs.openSync(
916
+ path.dirname(transcriptPath),
917
+ fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW,
918
+ );
919
+ const durableParent = fs.fstatSync(descriptor, { bigint: true });
920
+ if (
921
+ !durableParent.isDirectory() ||
922
+ durableParent.dev !== parentIdentity.dev ||
923
+ durableParent.ino !== parentIdentity.ino
924
+ )
925
+ throw new Error("parent_changed");
926
+ fs.fsyncSync(descriptor);
927
+ } catch (error) {
928
+ throw new SessionDeleteVerificationError("artifacts", "durability_failed", { cause: toError(error) });
929
+ } finally {
930
+ if (descriptor !== undefined) fs.closeSync(descriptor);
931
+ }
932
+ };
903
933
  if (detachedArtifactsPath) {
904
934
  if (
905
935
  !expectedArtifactsIdentity ||
@@ -930,6 +960,7 @@ export class FileSessionStorage implements SessionStorage {
930
960
  "artifacts",
931
961
  "Native artifact removal returned an unauthorized root",
932
962
  );
963
+ durablyRecordArtifactPhase();
933
964
  return {
934
965
  kind: "cleanup_pending",
935
966
  phase: "artifacts",
@@ -970,6 +1001,7 @@ export class FileSessionStorage implements SessionStorage {
970
1001
  "artifacts",
971
1002
  "Authorized artifact removal root remains after restart",
972
1003
  );
1004
+ durablyRecordArtifactPhase();
973
1005
  return { kind: "artifacts_removed", phase: "artifacts", transcriptIdentity };
974
1006
  }
975
1007
 
@@ -1014,27 +1046,7 @@ export class FileSessionStorage implements SessionStorage {
1014
1046
  `Exact artifact detach rejected: ${detach.ok ? "missing_path" : detach.code}`,
1015
1047
  );
1016
1048
  }
1017
- if (!detach.ok && process.platform !== "win32") {
1018
- let descriptor: number | undefined;
1019
- try {
1020
- descriptor = fs.openSync(
1021
- path.dirname(transcriptPath),
1022
- fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW,
1023
- );
1024
- const durableParent = fs.fstatSync(descriptor, { bigint: true });
1025
- if (
1026
- !durableParent.isDirectory() ||
1027
- durableParent.dev !== parentIdentity.dev ||
1028
- durableParent.ino !== parentIdentity.ino
1029
- )
1030
- throw new Error("parent_changed");
1031
- fs.fsyncSync(descriptor);
1032
- } catch (error) {
1033
- throw new SessionDeleteVerificationError("artifacts", "durability_failed", { cause: toError(error) });
1034
- } finally {
1035
- if (descriptor !== undefined) fs.closeSync(descriptor);
1036
- }
1037
- }
1049
+ if (!detach.ok) durablyRecordArtifactPhase();
1038
1050
  if (!detach.ok) {
1039
1051
  return {
1040
1052
  kind: "cleanup_pending",
@@ -1058,6 +1070,7 @@ export class FileSessionStorage implements SessionStorage {
1058
1070
  "artifacts",
1059
1071
  "Native artifact removal returned an unauthorized root",
1060
1072
  );
1073
+ durablyRecordArtifactPhase();
1061
1074
  return {
1062
1075
  kind: "cleanup_pending",
1063
1076
  phase: "artifacts",
@@ -1082,27 +1095,7 @@ export class FileSessionStorage implements SessionStorage {
1082
1095
  }
1083
1096
  }
1084
1097
  if (!artifactsRemoved) {
1085
- if (process.platform !== "win32") {
1086
- let descriptor: number | undefined;
1087
- try {
1088
- descriptor = fs.openSync(
1089
- path.dirname(transcriptPath),
1090
- fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW,
1091
- );
1092
- const durableParent = fs.fstatSync(descriptor, { bigint: true });
1093
- if (
1094
- !durableParent.isDirectory() ||
1095
- durableParent.dev !== parentIdentity.dev ||
1096
- durableParent.ino !== parentIdentity.ino
1097
- )
1098
- throw new Error("parent_changed");
1099
- fs.fsyncSync(descriptor);
1100
- } catch (error) {
1101
- throw new SessionDeleteVerificationError("artifacts", "durability_failed", { cause: toError(error) });
1102
- } finally {
1103
- if (descriptor !== undefined) fs.closeSync(descriptor);
1104
- }
1105
- }
1098
+ durablyRecordArtifactPhase();
1106
1099
  return { kind: "artifacts_removed", phase: "artifacts", transcriptIdentity };
1107
1100
  }
1108
1101
  if (hasDetachedTranscript) {
@@ -28,6 +28,27 @@ const NL = "\n";
28
28
 
29
29
  const ELLIPSIS = "…";
30
30
 
31
+ const ARTIFACT_WRITER_DIAGNOSTIC_MAX_BYTES = 256;
32
+
33
+ /**
34
+ * Terminal publisher used when managed artifact storage exposes an ID but no
35
+ * writable pathname. The sink supplies raw content retained up to its artifact
36
+ * hard cap; `omittedBytes` is the source-byte count already omitted by that cap.
37
+ * A successful publisher may report additional omitted bytes (for example, a
38
+ * downstream storage cap); those counts are added to the sink summary. Failed
39
+ * and unavailable results are surfaced through the bounded artifact diagnostic
40
+ * without exposing an unresolvable artifact URL.
41
+ */
42
+ export type TerminalArtifactPublishResult =
43
+ | { status: "published"; artifactId: string; omittedBytes?: number }
44
+ | { status: "unavailable" }
45
+ | { status: "failed"; diagnostic: string };
46
+
47
+ export type TerminalArtifactPublisher = (
48
+ content: string,
49
+ info: { totalBytes: number; omittedBytes: number },
50
+ ) => Promise<TerminalArtifactPublishResult>;
51
+
31
52
  // =============================================================================
32
53
  // Interfaces
33
54
  // =============================================================================
@@ -47,10 +68,12 @@ export interface OutputSummary {
47
68
  columnDroppedBytes?: number;
48
69
  /** Number of distinct lines that hit the per-line column cap. */
49
70
  columnTruncatedLines?: number;
50
- /** Artifact ID for internal URL access (artifact://<id>) when truncated */
71
+ /** Artifact ID for internal URL access (artifact://<id>) when output was persisted. */
51
72
  artifactId?: string;
52
73
  /** Bytes omitted from artifact storage after the artifact hard cap was reached. */
53
74
  artifactTruncatedBytes?: number;
75
+ /** Bounded diagnostic when artifact writer or terminal publisher creation, write, finalization, or publication failed. */
76
+ artifactFailureDiagnostic?: string;
54
77
  }
55
78
 
56
79
  export interface OutputSinkOptions {
@@ -60,6 +83,12 @@ export interface OutputSinkOptions {
60
83
  */
61
84
  artifactPath?: string;
62
85
  artifactId?: string;
86
+ /**
87
+ * Optional terminal publisher for managed artifact stores that do not expose a
88
+ * writable path. It is invoked only when visible spill/truncation requires an
89
+ * artifact, and receives raw content bounded by `artifactMaxBytes`.
90
+ */
91
+ artifactPublisher?: TerminalArtifactPublisher;
63
92
  /** Tail buffer budget (bytes). Default DEFAULT_MAX_BYTES. */
64
93
  spillThreshold?: number;
65
94
  /**
@@ -913,6 +942,9 @@ export class OutputSink {
913
942
  #artifactBytes = 0;
914
943
  #artifactTruncatedBytes = 0;
915
944
  #artifactTruncationNoticeWritten = false;
945
+ #artifactFailureDiagnostic?: string;
946
+ #publishedArtifactId?: string;
947
+ #artifactFinalizationFailed = false;
916
948
 
917
949
  // Per-line column cap streaming state (persists across `push` calls so a
918
950
  // long line split across chunks still trips the same trigger).
@@ -927,9 +959,13 @@ export class OutputSink {
927
959
  };
928
960
 
929
961
  // Raw prefix chunks not yet confirmed written to the file sink. This queue is
930
- // the only artifact replay source; retained head/tail windows are lossy views.
962
+ // the pathname-backed artifact replay source; retained head/tail windows are lossy views.
931
963
  #pendingFileWrites?: string[];
932
964
  #pendingFileWriteBytes = 0;
965
+ // Raw chunks retained for a terminal publisher. This queue is capped by
966
+ // `#artifactMaxBytes` and is never used by pathname-backed sinks.
967
+ #pendingArtifactContent?: string[];
968
+ #artifactPublishAttempted = false;
933
969
  #finalized = false;
934
970
 
935
971
  #fileReady = false;
@@ -943,6 +979,7 @@ export class OutputSink {
943
979
  readonly #chunkThrottleMs: number;
944
980
  readonly #maxColumns: number;
945
981
  readonly #artifactMaxBytes: number;
982
+ readonly #artifactPublisher?: TerminalArtifactPublisher;
946
983
  readonly #coalesceSanitize: boolean;
947
984
  #coalesceBuf = "";
948
985
 
@@ -957,6 +994,7 @@ export class OutputSink {
957
994
  chunkThrottleMs = 0,
958
995
  onRawChunk,
959
996
  artifactMaxBytes = DEFAULT_ARTIFACT_MAX_BYTES,
997
+ artifactPublisher,
960
998
  coalesceSanitize = process.env.PI_OUTPUT_SANITIZE_COALESCE === "1",
961
999
  } = options ?? {};
962
1000
  // Managed callers omit artifactPath at the allocation boundary; explicit callers
@@ -970,6 +1008,7 @@ export class OutputSink {
970
1008
  this.#onRawChunk = onRawChunk;
971
1009
  this.#chunkThrottleMs = chunkThrottleMs;
972
1010
  this.#artifactMaxBytes = Math.max(0, artifactMaxBytes);
1011
+ this.#artifactPublisher = artifactPublisher;
973
1012
  this.#coalesceSanitize = coalesceSanitize;
974
1013
  }
975
1014
 
@@ -1060,11 +1099,14 @@ export class OutputSink {
1060
1099
  // open, keep an independent raw replay prefix because retained head/tail
1061
1100
  // windows are trimmed and cannot reconstruct byte-correct artifacts.
1062
1101
  if (this.#artifactPath && this.#maxColumns === 0) this.#enqueueFileWrite(rawChunk, rawBytes);
1102
+ else if (this.#artifactPublisher && this.#maxColumns === 0)
1103
+ this.#enqueueTerminalArtifactChunk(rawChunk, rawBytes);
1063
1104
 
1064
1105
  if (rawBytes === 0) return;
1065
1106
 
1066
1107
  const visibleChunk = this.#maxColumns > 0 ? this.#applyColumnCap(sanitizedChunk) : sanitizedChunk;
1067
1108
  if (this.#artifactPath && this.#maxColumns > 0) this.#enqueueFileWrite(rawChunk, rawBytes);
1109
+ else if (this.#artifactPublisher && this.#maxColumns > 0) this.#enqueueTerminalArtifactChunk(rawChunk, rawBytes);
1068
1110
  if (this.#columnDroppedBytes > 0) this.#createFileSink();
1069
1111
  const visibleBytes = Buffer.byteLength(visibleChunk, "utf-8");
1070
1112
  if (visibleChunk.length > 0) {
@@ -1186,6 +1228,45 @@ export class OutputSink {
1186
1228
  return `\n[artifact truncated after ${this.#artifactBytes} bytes; omitted at least ${droppedBytes} bytes]\n`;
1187
1229
  }
1188
1230
 
1231
+ #recordArtifactFailure(phase: "create" | "write" | "end", error: unknown): void {
1232
+ if (this.#artifactFailureDiagnostic) return;
1233
+ const message = (error instanceof Error ? error.message : String(error)).replace(/\s+/gu, " ").trim();
1234
+ const normalized = message || "unknown storage error";
1235
+ this.#artifactFailureDiagnostic = truncateHeadBytes(
1236
+ `${phase}: ${normalized}`,
1237
+ ARTIFACT_WRITER_DIAGNOSTIC_MAX_BYTES,
1238
+ ).text;
1239
+ }
1240
+
1241
+ #recordTerminalArtifactDiagnostic(kind: "unavailable" | "failed", error?: unknown): void {
1242
+ if (this.#artifactFailureDiagnostic) return;
1243
+ const message =
1244
+ error === undefined
1245
+ ? "terminal artifact publisher unavailable"
1246
+ : (error instanceof Error ? error.message : String(error)).replace(/\s+/gu, " ").trim() ||
1247
+ "unknown publication error";
1248
+ this.#artifactFailureDiagnostic = truncateHeadBytes(
1249
+ `${kind}: ${message}`,
1250
+ ARTIFACT_WRITER_DIAGNOSTIC_MAX_BYTES,
1251
+ ).text;
1252
+ }
1253
+
1254
+ // A failed write may have committed only a prefix, so the replay queue is no longer trustworthy.
1255
+ #failArtifact(phase: "create" | "write" | "end", error: unknown, sink?: Bun.FileSink): void {
1256
+ this.#recordArtifactFailure(phase, error);
1257
+ this.#artifactFinalizationFailed = true;
1258
+ this.#file = undefined;
1259
+ this.#fileReady = false;
1260
+ this.#pendingFileWrites = undefined;
1261
+ this.#pendingFileWriteBytes = 0;
1262
+ if (!sink) return;
1263
+ try {
1264
+ void Promise.resolve(sink.end()).catch(endError => this.#recordArtifactFailure("end", endError));
1265
+ } catch (endError) {
1266
+ this.#recordArtifactFailure("end", endError);
1267
+ }
1268
+ }
1269
+
1189
1270
  #capArtifactChunk(chunk: string, bytes: number): { chunk: string; bytes: number } | null {
1190
1271
  if (bytes === 0) return null;
1191
1272
  if (this.#artifactMaxBytes <= 0 || this.#artifactBytes >= this.#artifactMaxBytes) {
@@ -1201,18 +1282,23 @@ export class OutputSink {
1201
1282
  return kept.bytes > 0 ? { chunk: kept.text, bytes: kept.bytes } : null;
1202
1283
  }
1203
1284
 
1204
- #writeArtifactTruncationNotice(): void {
1205
- if (this.#artifactTruncatedBytes <= 0 || this.#artifactTruncationNoticeWritten) return;
1285
+ async #writeArtifactTruncationNotice(): Promise<void> {
1286
+ if (
1287
+ this.#artifactFinalizationFailed ||
1288
+ this.#artifactTruncatedBytes <= 0 ||
1289
+ this.#artifactTruncationNoticeWritten
1290
+ )
1291
+ return;
1206
1292
  const notice = this.#artifactTruncationNotice(this.#artifactTruncatedBytes);
1207
1293
  try {
1208
1294
  if (this.#fileReady && this.#file) {
1209
- this.#file.sink.write(notice);
1295
+ await this.#file.sink.write(notice);
1210
1296
  } else {
1211
1297
  this.#queuePendingFileWrite(notice, Buffer.byteLength(notice, "utf-8"));
1212
1298
  }
1213
1299
  this.#artifactTruncationNoticeWritten = true;
1214
- } catch {
1215
- /* ignore */
1300
+ } catch (error) {
1301
+ this.#failArtifact("write", error, this.#file?.sink);
1216
1302
  }
1217
1303
  }
1218
1304
 
@@ -1224,6 +1310,7 @@ export class OutputSink {
1224
1310
  }
1225
1311
 
1226
1312
  #enqueueFileWrite(chunk: string, bytes: number): void {
1313
+ if (this.#artifactFinalizationFailed || this.#finalized) return;
1227
1314
  const capped = this.#capArtifactChunk(chunk, bytes);
1228
1315
  if (!capped) return;
1229
1316
  this.#artifactBytes += capped.bytes;
@@ -1235,51 +1322,113 @@ export class OutputSink {
1235
1322
 
1236
1323
  try {
1237
1324
  this.#file.sink.write(capped.chunk);
1238
- } catch {
1239
- try {
1240
- void this.#file.sink.end();
1241
- } catch {
1242
- /* ignore */
1243
- }
1244
- this.#file = undefined;
1245
- this.#fileReady = false;
1246
- this.#queuePendingFileWrite(capped.chunk, capped.bytes);
1247
- this.#createFileSink();
1325
+ } catch (error) {
1326
+ this.#failArtifact("write", error, this.#file.sink);
1327
+ }
1328
+ }
1329
+
1330
+ #enqueueTerminalArtifactChunk(chunk: string, bytes: number): void {
1331
+ if (this.#artifactFinalizationFailed || this.#finalized) return;
1332
+ const capped = this.#capArtifactChunk(chunk, bytes);
1333
+ if (!capped) return;
1334
+ this.#artifactBytes += capped.bytes;
1335
+ if (!this.#pendingArtifactContent) this.#pendingArtifactContent = [capped.chunk];
1336
+ else this.#pendingArtifactContent.push(capped.chunk);
1337
+ const pending = this.#pendingArtifactContent;
1338
+ if (!pending) return;
1339
+ if (pending.length > MAX_PENDING) {
1340
+ pending[0] = pending.join("");
1341
+ pending.length = 1;
1248
1342
  }
1249
1343
  }
1250
1344
 
1251
1345
  #createFileSink(): boolean {
1252
- if (this.#finalized) return false;
1346
+ if (this.#finalized || this.#artifactFinalizationFailed) return false;
1253
1347
 
1254
1348
  if (!this.#artifactPath) return false;
1255
1349
  if (this.#fileReady) return this.#file != null;
1350
+
1351
+ let sink: Bun.FileSink;
1256
1352
  try {
1257
- const sink = Bun.file(this.#artifactPath).writer();
1258
- this.#file = { path: this.#artifactPath, artifactId: this.#artifactId, sink };
1353
+ sink = Bun.file(this.#artifactPath).writer();
1354
+ } catch (error) {
1355
+ this.#recordArtifactFailure("create", error);
1356
+ return false;
1357
+ }
1358
+ this.#file = { path: this.#artifactPath, artifactId: this.#artifactId, sink };
1259
1359
 
1360
+ try {
1260
1361
  const pending = this.#pendingFileWrites;
1261
1362
  if (pending) {
1262
1363
  for (const chunk of pending) sink.write(chunk);
1263
1364
  }
1365
+ } catch (error) {
1366
+ this.#failArtifact("write", error, sink);
1367
+ return false;
1368
+ }
1264
1369
 
1265
- this.#fileReady = true;
1266
- this.#pendingFileWrites = undefined;
1267
- this.#pendingFileWriteBytes = 0;
1370
+ this.#fileReady = true;
1371
+ this.#pendingFileWrites = undefined;
1372
+ this.#pendingFileWriteBytes = 0;
1373
+ return true;
1374
+ }
1375
+
1376
+ #shouldPublishTerminalArtifact(): boolean {
1377
+ return (
1378
+ this.#artifactPublisher !== undefined &&
1379
+ this.#artifactPath === undefined &&
1380
+ (this.#truncated || this.#columnDroppedBytes > 0 || this.#artifactTruncatedBytes > 0)
1381
+ );
1382
+ }
1268
1383
 
1269
- return true;
1270
- } catch {
1271
- try {
1272
- void this.#file?.sink?.end();
1273
- } catch {
1274
- /* ignore */
1384
+ async #publishTerminalArtifact(): Promise<string | undefined> {
1385
+ if (
1386
+ !this.#shouldPublishTerminalArtifact() ||
1387
+ this.#artifactPublishAttempted ||
1388
+ this.#artifactFinalizationFailed
1389
+ ) {
1390
+ return this.#publishedArtifactId;
1391
+ }
1392
+ this.#artifactPublishAttempted = true;
1393
+ const content = this.#pendingArtifactContent?.join("") ?? "";
1394
+ const omittedBytes = this.#artifactTruncatedBytes;
1395
+ try {
1396
+ const published = await this.#artifactPublisher!(content, {
1397
+ totalBytes: this.#totalBytes,
1398
+ omittedBytes,
1399
+ });
1400
+ if (published.status === "unavailable") {
1401
+ this.#recordTerminalArtifactDiagnostic("unavailable");
1402
+ this.#artifactFinalizationFailed = true;
1403
+ return undefined;
1275
1404
  }
1276
- this.#file = undefined;
1277
- // Keep #pendingFileWriteBytes in sync with the preserved queue so
1278
- // later retry/threshold decisions don't undercount retained bytes.
1279
- this.#pendingFileWriteBytes = this.#pendingFileWrites
1280
- ? this.#pendingFileWrites.reduce((sum, chunk) => sum + Buffer.byteLength(chunk), 0)
1281
- : 0;
1282
- return false;
1405
+ if (published.status === "failed") {
1406
+ this.#recordTerminalArtifactDiagnostic("failed", published.diagnostic);
1407
+ this.#artifactFinalizationFailed = true;
1408
+ return undefined;
1409
+ }
1410
+ if (published.artifactId.length === 0) {
1411
+ this.#recordTerminalArtifactDiagnostic("failed", "storage returned no artifact id");
1412
+ this.#artifactFinalizationFailed = true;
1413
+ return undefined;
1414
+ }
1415
+ if (published.omittedBytes !== undefined) {
1416
+ if (!Number.isSafeInteger(published.omittedBytes) || published.omittedBytes < 0) {
1417
+ this.#recordTerminalArtifactDiagnostic("failed", "publisher returned invalid omitted-byte accounting");
1418
+ this.#artifactFinalizationFailed = true;
1419
+ return undefined;
1420
+ }
1421
+ this.#artifactTruncatedBytes += published.omittedBytes;
1422
+ }
1423
+ this.#publishedArtifactId = published.artifactId;
1424
+ this.#finalized = true;
1425
+ return this.#publishedArtifactId;
1426
+ } catch (error) {
1427
+ this.#recordTerminalArtifactDiagnostic("failed", error);
1428
+ this.#artifactFinalizationFailed = true;
1429
+ return undefined;
1430
+ } finally {
1431
+ this.#pendingArtifactContent = undefined;
1283
1432
  }
1284
1433
  }
1285
1434
 
@@ -1302,25 +1451,34 @@ export class OutputSink {
1302
1451
  * minimizer rewrites the captured output after the raw bytes have already
1303
1452
  * been streamed.
1304
1453
  *
1305
- * After this call the buffer is authoritative: streaming counters realign
1306
- * to the replacement, the retained head window is cleared, and head
1307
- * retention is disabled so subsequent `push()` calls append directly to the
1308
- * tail buffer instead of repopulating the (now meaningless) head window
1309
- * — which would otherwise reorder content and trip the middle-elision
1310
- * branch in `dump()` against stale totals.
1454
+ * After this call the replacement is authoritative: counters reflect its full
1455
+ * size, the configured head+tail byte windows are retained with UTF-8-safe
1456
+ * boundaries, and future head accumulation is disabled so later `push()` calls
1457
+ * append directly to the tail without reordering the replacement.
1311
1458
  */
1312
1459
  replace(text: string): void {
1313
1460
  this.#coalesceBuf = "";
1314
- this.#setTail(text);
1461
+ const replacementBytes = Buffer.byteLength(text, "utf-8");
1315
1462
  this.#head = "";
1316
1463
  this.#headBytes = 0;
1464
+ this.#setTail("");
1465
+
1466
+ if (this.#headLimit > 0) {
1467
+ const head = truncateHeadBytes(text, this.#headLimit);
1468
+ this.#appendHead(head.text, head.bytes);
1469
+ const tailText = text.substring(head.text.length);
1470
+ this.#setTail(tailText, replacementBytes - head.bytes);
1471
+ } else {
1472
+ this.#setTail(text, replacementBytes);
1473
+ }
1474
+ this.#trimTailTo(this.#spillThreshold);
1317
1475
  this.#headRetentionDisabled = true;
1318
- this.#totalBytes = this.#bufferBytes;
1319
- this.#processedBytes = this.#bufferBytes;
1476
+ this.#totalBytes = replacementBytes;
1477
+ this.#processedBytes = replacementBytes;
1320
1478
  this.#totalLines = countNewlines(text);
1321
1479
  this.#processedLines = this.#totalLines;
1322
1480
  this.#sawData = text.length > 0;
1323
- this.#truncated = false;
1481
+ this.#truncated = replacementBytes > this.#headBytes + this.#bufferBytes;
1324
1482
  this.#currentLineBytes = 0;
1325
1483
  this.#columnEllipsisAdded = false;
1326
1484
  this.#columnDroppedBytes = 0;
@@ -1333,18 +1491,35 @@ export class OutputSink {
1333
1491
  const totalLines = this.#sawData ? this.#totalLines + 1 : 0;
1334
1492
 
1335
1493
  let artifactId: string | undefined;
1336
- if (this.#artifactTruncatedBytes > 0) this.#createFileSink();
1337
- this.#writeArtifactTruncationNotice();
1338
- if (this.#file) {
1339
- artifactId = this.#file.artifactId;
1340
- await this.#file.sink.end();
1341
- this.#finalized = true;
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
+ }
1342
1516
  }
1343
1517
  if (this.#finalized) {
1344
1518
  // Terminal: the artifact is closed; replay state is no longer needed.
1345
1519
  this.#pendingFileWrites = undefined;
1346
1520
  this.#pendingFileWriteBytes = 0;
1347
1521
  this.#fileReady = false;
1522
+ this.#pendingArtifactContent = undefined;
1348
1523
  }
1349
1524
  // Non-finalized dumps (no artifact sink ever opened) keep the raw replay
1350
1525
  // queue so a later post-dump push that spills produces a CUMULATIVE
@@ -1370,24 +1545,18 @@ export class OutputSink {
1370
1545
  let elidedLines: number | undefined;
1371
1546
 
1372
1547
  if (headBytes > 0 && effectiveTotalBytes > headBytes + tailBytes) {
1373
- // Middle was elided. Emit head + marker + tail.
1548
+ // Middle was elided. Emit both explicit windows, including when they are
1549
+ // fragments of one source line (the marker still carries byte evidence).
1374
1550
  elidedBytes = Math.max(0, effectiveTotalBytes - headBytes - tailBytes);
1375
1551
  elidedLines = Math.max(0, processedTotalLines - headLines - tailLines);
1376
- if (elidedLines === 0) {
1377
- body = headText;
1378
- outputBytes = headBytes;
1379
- outputLines = headLines;
1380
- this.#truncated = true;
1381
- } else {
1382
- const marker = formatMiddleElisionMarker(elidedLines, elidedBytes);
1383
- const markerBytes = Buffer.byteLength(marker, "utf-8");
1384
- const headSep = headText.endsWith("\n") ? "" : "\n";
1385
- const tailSep = tailBuf.startsWith("\n") ? "" : "\n";
1386
- body = `${headText}${headSep}${marker}${tailSep}${tailBuf}`;
1387
- outputBytes = headBytes + markerBytes + tailBytes + headSep.length + tailSep.length;
1388
- outputLines = headLines + 1 + tailLines;
1389
- this.#truncated = true;
1390
- }
1552
+ const marker = formatMiddleElisionMarker(elidedLines, elidedBytes);
1553
+ const markerBytes = Buffer.byteLength(marker, "utf-8");
1554
+ const headSep = headText.endsWith("\n") ? "" : "\n";
1555
+ const tailSep = tailBuf.startsWith("\n") ? "" : "\n";
1556
+ body = `${headText}${headSep}${marker}${tailSep}${tailBuf}`;
1557
+ outputBytes = headBytes + markerBytes + tailBytes + headSep.length + tailSep.length;
1558
+ outputLines = headLines + 1 + tailLines;
1559
+ this.#truncated = true;
1391
1560
  } else if (headBytes > 0) {
1392
1561
  // Head + tail combine into the full buffered output (no overlap or elision).
1393
1562
  body = `${headText}${tailBuf}`;
@@ -1412,6 +1581,7 @@ export class OutputSink {
1412
1581
  columnDroppedBytes: this.#columnDroppedBytes > 0 ? this.#columnDroppedBytes : undefined,
1413
1582
  columnTruncatedLines: this.#columnTruncatedLines > 0 ? this.#columnTruncatedLines : undefined,
1414
1583
  artifactTruncatedBytes: this.#artifactTruncatedBytes > 0 ? this.#artifactTruncatedBytes : undefined,
1584
+ artifactFailureDiagnostic: this.#artifactFailureDiagnostic,
1415
1585
  artifactId,
1416
1586
  };
1417
1587
  }