@gajae-code/coding-agent 0.16.0 → 0.16.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 (164) hide show
  1. package/CHANGELOG.md +52 -1
  2. package/dist/types/async/job-manager.d.ts +32 -1
  3. package/dist/types/browser-backend.d.ts +12 -0
  4. package/dist/types/cli/update-cli.d.ts +1 -0
  5. package/dist/types/commands/sdk.d.ts +2 -0
  6. package/dist/types/config/billing-path.d.ts +25 -0
  7. package/dist/types/config/config-schema-version.d.ts +13 -1
  8. package/dist/types/config/model-registry.d.ts +16 -0
  9. package/dist/types/config/settings-schema.d.ts +17 -7
  10. package/dist/types/extensibility/extensions/types.d.ts +8 -3
  11. package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
  12. package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
  13. package/dist/types/main.d.ts +1 -0
  14. package/dist/types/modes/acp/acp-agent.d.ts +21 -21
  15. package/dist/types/modes/components/model-selector.d.ts +4 -0
  16. package/dist/types/modes/components/session-selector.d.ts +1 -1
  17. package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
  18. package/dist/types/modes/interactive-mode.d.ts +16 -2
  19. package/dist/types/modes/theme/theme.d.ts +2 -1
  20. package/dist/types/modes/types.d.ts +2 -0
  21. package/dist/types/sdk/broker/broker.d.ts +2 -1
  22. package/dist/types/sdk/broker/ensure.d.ts +3 -0
  23. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
  24. package/dist/types/sdk/broker/lifecycle.d.ts +14 -0
  25. package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
  26. package/dist/types/sdk/broker/state-version.d.ts +17 -0
  27. package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
  28. package/dist/types/sdk/bus/index.d.ts +3 -1
  29. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
  30. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
  31. package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
  32. package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
  33. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  34. package/dist/types/sdk/cli/rows.d.ts +15 -2
  35. package/dist/types/sdk/cli/session-cli.d.ts +2 -1
  36. package/dist/types/sdk/host/events.d.ts +2 -0
  37. package/dist/types/sdk/host/host.d.ts +2 -0
  38. package/dist/types/sdk/host/query/cursor.d.ts +4 -2
  39. package/dist/types/sdk/host/query/handlers.d.ts +1 -0
  40. package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
  41. package/dist/types/sdk/host/session-runtime.d.ts +18 -17
  42. package/dist/types/sdk/lifecycle-session.d.ts +1 -1
  43. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
  44. package/dist/types/sdk/receipt-state.d.ts +2 -0
  45. package/dist/types/sdk/router/session-router.d.ts +4 -0
  46. package/dist/types/sdk/turn-result.d.ts +6 -0
  47. package/dist/types/session/agent-session.d.ts +47 -7
  48. package/dist/types/session/fold-coordinator.d.ts +29 -2
  49. package/dist/types/session/session-manager.d.ts +14 -2
  50. package/dist/types/tools/ask.d.ts +1 -0
  51. package/dist/types/tools/bash.d.ts +12 -0
  52. package/dist/types/tools/browser/launch.d.ts +10 -0
  53. package/dist/types/tools/browser/registry.d.ts +8 -0
  54. package/dist/types/tools/index.d.ts +4 -10
  55. package/dist/types/tools/job.d.ts +3 -1
  56. package/dist/types/utils/external-editor.d.ts +12 -0
  57. package/examples/extensions/README.md +1 -1
  58. package/examples/extensions/pirate.ts +7 -4
  59. package/package.json +7 -7
  60. package/scripts/generate-sdk-operation-inventory.ts +7 -1
  61. package/src/async/job-manager.ts +107 -9
  62. package/src/browser-backend.ts +29 -0
  63. package/src/cli/plugin-cli.ts +29 -0
  64. package/src/cli/update-cli.ts +63 -12
  65. package/src/commands/sdk.ts +22 -7
  66. package/src/config/billing-path.ts +42 -0
  67. package/src/config/config-schema-version.ts +18 -1
  68. package/src/config/model-preset-registry.ts +177 -18
  69. package/src/config/model-registry.ts +268 -42
  70. package/src/config/settings-schema.ts +19 -7
  71. package/src/config/settings.ts +46 -15
  72. package/src/coordinator-mcp/projection-scan.ts +6 -6
  73. package/src/coordinator-mcp/server.ts +21 -1
  74. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  75. package/src/extensibility/extensions/runner.ts +27 -0
  76. package/src/extensibility/extensions/types.ts +8 -3
  77. package/src/extensibility/gjc-plugins/installer.ts +51 -11
  78. package/src/extensibility/runtime-skill-discovery.ts +110 -3
  79. package/src/extensibility/skills.ts +22 -12
  80. package/src/gjc-runtime/launch-worktree.ts +285 -4
  81. package/src/gjc-runtime/session-state-lock.ts +156 -32
  82. package/src/gjc-runtime/session-state-sidecar.ts +10 -17
  83. package/src/gjc-runtime/tmux-owner-isolation.ts +16 -1
  84. package/src/internal-urls/docs-index.generated.ts +11 -11
  85. package/src/main.ts +4 -1
  86. package/src/modes/acp/acp-agent.ts +34 -19
  87. package/src/modes/components/model-selector.ts +76 -8
  88. package/src/modes/components/session-selector.ts +23 -15
  89. package/src/modes/components/sessions-dashboard.ts +8 -5
  90. package/src/modes/controllers/command-controller.ts +1 -1
  91. package/src/modes/controllers/event-controller.ts +30 -4
  92. package/src/modes/controllers/extension-ui-controller.ts +13 -13
  93. package/src/modes/controllers/input-controller.ts +2 -2
  94. package/src/modes/controllers/selector-controller.ts +10 -3
  95. package/src/modes/interactive-mode.ts +36 -12
  96. package/src/modes/prompt-action-autocomplete.ts +30 -4
  97. package/src/modes/runtime-init.ts +12 -9
  98. package/src/modes/theme/theme.ts +5 -0
  99. package/src/modes/types.ts +2 -0
  100. package/src/modes/utils/ui-helpers.ts +22 -3
  101. package/src/prompts/agent-fragments/browser-aside.md +22 -0
  102. package/src/prompts/tools/ask.md +1 -1
  103. package/src/prompts/tools/bash.md +1 -0
  104. package/src/prompts/tools/job.md +1 -1
  105. package/src/sdk/acp/adapter.ts +6 -1
  106. package/src/sdk/broker/broker.ts +10 -1
  107. package/src/sdk/broker/ensure.ts +76 -3
  108. package/src/sdk/broker/lifecycle-ledger.ts +6 -0
  109. package/src/sdk/broker/lifecycle.ts +254 -34
  110. package/src/sdk/broker/spawn-substrate.ts +45 -2
  111. package/src/sdk/broker/startup-budget.ts +144 -2
  112. package/src/sdk/broker/state-version.ts +17 -0
  113. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  114. package/src/sdk/bus/chat-daemon-runtime.ts +118 -24
  115. package/src/sdk/bus/discord-daemon.ts +50 -10
  116. package/src/sdk/bus/index.ts +33 -3
  117. package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
  118. package/src/sdk/bus/lifecycle-commands.ts +4 -0
  119. package/src/sdk/bus/notification-service.ts +22 -5
  120. package/src/sdk/bus/prompt-reconciliation.ts +7 -2
  121. package/src/sdk/bus/reconciliation-store.ts +55 -31
  122. package/src/sdk/bus/slack-conversation.ts +1 -1
  123. package/src/sdk/bus/slack-daemon.ts +72 -38
  124. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  125. package/src/sdk/cli/rows.ts +75 -7
  126. package/src/sdk/cli/session-cli.ts +68 -35
  127. package/src/sdk/host/control/dispatch.ts +2 -2
  128. package/src/sdk/host/events.ts +26 -2
  129. package/src/sdk/host/host.ts +6 -1
  130. package/src/sdk/host/query/cursor.ts +52 -7
  131. package/src/sdk/host/query/handlers.ts +33 -12
  132. package/src/sdk/host/query/revision-store.ts +12 -0
  133. package/src/sdk/host/session-runtime.test.ts +497 -31
  134. package/src/sdk/host/session-runtime.ts +387 -54
  135. package/src/sdk/host/surface-policy.ts +3 -3
  136. package/src/sdk/lifecycle-session.ts +7 -2
  137. package/src/sdk/prompt-deadline-manager.ts +2 -1
  138. package/src/sdk/protocol/operation-inventory.generated.json +54 -3
  139. package/src/sdk/protocol/operation-registry.ts +1 -1
  140. package/src/sdk/receipt-state.ts +10 -0
  141. package/src/sdk/router/session-router.ts +210 -94
  142. package/src/sdk/session.ts +59 -41
  143. package/src/sdk/turn-result.ts +22 -0
  144. package/src/session/agent-session.ts +1033 -260
  145. package/src/session/fold-coordinator.ts +40 -3
  146. package/src/session/session-manager.ts +517 -77
  147. package/src/slash-commands/builtin-registry.ts +41 -0
  148. package/src/task/executor.ts +9 -4
  149. package/src/tools/ask-contract.test.ts +64 -0
  150. package/src/tools/ask-contract.ts +36 -1
  151. package/src/tools/ask.ts +10 -8
  152. package/src/tools/bash-interactive.ts +11 -2
  153. package/src/tools/bash.ts +158 -33
  154. package/src/tools/browser/launch.ts +37 -0
  155. package/src/tools/browser/registry.ts +99 -9
  156. package/src/tools/cron.ts +6 -4
  157. package/src/tools/descriptors.test.ts +15 -0
  158. package/src/tools/descriptors.ts +5 -1
  159. package/src/tools/index.ts +4 -6
  160. package/src/tools/job.ts +4 -1
  161. package/src/tools/monitor.ts +12 -9
  162. package/src/tools/resolve.ts +30 -10
  163. package/src/tools/tool-catalog.generated.ts +2 -2
  164. package/src/utils/external-editor.ts +86 -1
@@ -56,15 +56,22 @@ import {
56
56
  import { projectQ10Models } from "../models.js";
57
57
  import { PromptDeadlineManager, type PromptTerminalTransitionEvidence } from "../prompt-deadline-manager";
58
58
  import { formatPromptFailureForLocalLog, sanitizePromptFailure } from "../prompt-failure";
59
+ import type { SdkPromptTerminalOutcome } from "../prompt-status";
59
60
  import { validateRequiredPromptText } from "../protocol/adapter-validation";
60
61
  import { OPERATIONS } from "../protocol/operation-registry";
62
+ import { type ReceiptState, reduceReceiptState } from "../receipt-state";
61
63
  import {
62
64
  createKindAwareReconciliation,
63
65
  createReconciliationStore,
64
66
  type KindAwareReconciliation,
65
67
  resolveReconciliationSessionFile,
66
68
  } from "../reconciliation-extensions";
67
- import { sanitizeTurnResultContent, type TurnResultContent } from "../turn-result";
69
+ import {
70
+ reduceTurnResultContent,
71
+ reportableTurnResultContent,
72
+ sanitizeTurnResultContent,
73
+ type TurnResultContent,
74
+ } from "../turn-result";
68
75
  import { type ControlSurface, controlRequestFromFrame, dispatchControl, terminalAbortIdentity } from "./control";
69
76
  import {
70
77
  BROKER_RUNTIME_ABORT_CAPABILITY_FIELD,
@@ -224,6 +231,8 @@ export interface SdkOnlyInvocationRecord extends InvocationCorrelation {
224
231
  startedAt?: number;
225
232
  terminalAt?: number;
226
233
  error?: { code: string; message: string };
234
+ content?: TurnResultContent;
235
+ receiptState?: Exclude<ReceiptState, "absent">;
227
236
  outcome?: unknown;
228
237
  pendingOutcome?: unknown;
229
238
  skillName?: string;
@@ -499,6 +508,8 @@ export interface CreateSdkSessionRuntimeOptions {
499
508
  onLifecycleDrainTimeoutForTests?: () => void;
500
509
  /** Test-only observation of bounded failure-diagnostic deduplication state. */
501
510
  onFailureDiagnosticKeyCountForTests?: (count: number) => void;
511
+ /** Test-only observation after a resolved invocation completion is reconciled. */
512
+ onInvocationCompletionReconciledForTests?: (kind: InvocationKind, correlation: InvocationCorrelation) => void;
502
513
  }
503
514
 
504
515
  function unavailable(operation: string): () => never {
@@ -516,7 +527,67 @@ export interface InvocationCorrelation {
516
527
 
517
528
  export type InvocationKind = "prompt" | "skill" | "steer";
518
529
  type InvocationStatus = "accepted" | "in_flight" | "terminal_ok" | "failed" | "uncertain";
530
+ type InvocationOutcome = SdkPromptTerminalOutcome;
531
+ type InvocationOutcomeInput = {
532
+ kind: string;
533
+ reason?: unknown;
534
+ code?: unknown;
535
+ message?: unknown;
536
+ provenance?: unknown;
537
+ };
519
538
  const EMPTY_PROMPT_FAILURE = { code: "prompt_failed", message: "Prompt submission failed." } as const;
539
+ const PRESERVED_PROVIDER_FAILURE_CODES = new Set([
540
+ "provider_down",
541
+ "provider_unavailable",
542
+ "provider_rejected",
543
+ "upstream_stream_interrupted",
544
+ "argument_validation",
545
+ "execution",
546
+ "upstream_error",
547
+ "transport_reset",
548
+ ]);
549
+ const isPreservedProviderFailure = (code: string | undefined): boolean =>
550
+ code !== undefined && (PRESERVED_PROVIDER_FAILURE_CODES.has(code) || code.startsWith("provider_http_"));
551
+
552
+ /**
553
+ * Durable reconciliation accepts only the bounded outcome vocabulary from
554
+ * reconciliation-store.ts. Provider diagnostics keep their specific safe code
555
+ * in `error`, while the durable outcome is normalized to the public failure
556
+ * classifier so a failed terminal can never be quarantined on reload.
557
+ */
558
+ function canonicalFailedOutcome(
559
+ failure?: { code?: unknown; message?: unknown },
560
+ provenance: "agent_failed" | "deadline" = "agent_failed",
561
+ ): InvocationOutcome {
562
+ const deadline = provenance === "deadline" || failure?.code === "prompt_deadline_exceeded";
563
+ return {
564
+ kind: "failed",
565
+ code: deadline ? "prompt_deadline_exceeded" : "prompt_failed",
566
+ message: deadline ? "Prompt deadline exceeded." : EMPTY_PROMPT_FAILURE.message,
567
+ provenance: deadline ? "deadline" : "agent_failed",
568
+ };
569
+ }
570
+
571
+ function canonicalTerminalOutcome(
572
+ outcome: unknown,
573
+ failure?: { code?: unknown; message?: unknown },
574
+ ): InvocationOutcome | undefined {
575
+ if (outcome && typeof outcome === "object") {
576
+ const candidate = outcome as { kind?: unknown; reason?: unknown; provenance?: unknown; code?: unknown };
577
+ if (candidate.kind === "stopped") {
578
+ if (candidate.reason === "cancelled" && candidate.provenance === "client_cancel")
579
+ return { kind: "stopped", reason: "cancelled", provenance: "client_cancel" };
580
+ return undefined;
581
+ }
582
+ if (candidate.kind === "failed")
583
+ return canonicalFailedOutcome(
584
+ { code: candidate.code, message: (outcome as { message?: unknown }).message },
585
+ candidate.provenance === "deadline" ? "deadline" : "agent_failed",
586
+ );
587
+ }
588
+ if (failure !== undefined) return canonicalFailedOutcome(failure);
589
+ return undefined;
590
+ }
520
591
  interface InvocationRecord extends InvocationCorrelation {
521
592
  kind: InvocationKind;
522
593
  revision: number;
@@ -527,6 +598,9 @@ interface InvocationRecord extends InvocationCorrelation {
527
598
  startedAt?: number;
528
599
  terminalAt?: number;
529
600
  error?: { code: string; message: string };
601
+ content?: TurnResultContent;
602
+ receiptState?: Exclude<ReceiptState, "absent">;
603
+ outcome?: unknown;
530
604
  }
531
605
  export interface InvocationReconciliation {
532
606
  /** Shared v2 reconciliation owner; present for durable terminal admission. */
@@ -542,7 +616,7 @@ export interface InvocationReconciliation {
542
616
  type: "agent_start" | "agent_end";
543
617
  content?: TurnResultContent;
544
618
  hasActivity?: boolean;
545
- outcome?: { kind: "stopped"; reason: "cancelled"; provenance: "client_cancel" };
619
+ outcome?: InvocationOutcome;
546
620
  }
547
621
  | { type: "agent_failed"; error: unknown; content?: TurnResultContent; hasActivity?: boolean },
548
622
  ): Promise<void>;
@@ -557,8 +631,8 @@ export interface InvocationReconciliation {
557
631
  claimPendingOutcome(
558
632
  kind: InvocationKind,
559
633
  correlation: InvocationCorrelation,
560
- outcome: { kind: string; code: string; message: string; provenance?: string },
561
- ): Promise<unknown>;
634
+ outcome: InvocationOutcomeInput,
635
+ ): Promise<InvocationOutcome>;
562
636
  // Positional compatibility (exact-head review P1): the 4th argument is either
563
637
  // the generation-fence isCurrent callback or the legacy recordError object;
564
638
  // both shapes are normalized in the implementation so a legacy object can
@@ -566,7 +640,7 @@ export interface InvocationReconciliation {
566
640
  finalizeOutcome(
567
641
  kind: InvocationKind,
568
642
  correlation: InvocationCorrelation,
569
- outcome?: { kind: string; code: string; message: string; provenance?: string },
643
+ outcome?: InvocationOutcomeInput,
570
644
  arg4?: (() => boolean) | { code: string; message: string },
571
645
  // Terminal evidence accepted by finalizeOutcome (exact-head review P1/P2):
572
646
  // when an explicit outcome is absent, a content-bearing, activity-bearing,
@@ -809,25 +883,71 @@ export function createInvocationReconciliation(
809
883
  // so a real lifecycle event is never swallowed.
810
884
  }
811
885
  const current = records.get(recordKey);
886
+ const incomingOutcome = frame.type === "agent_end" ? canonicalTerminalOutcome(frame.outcome) : undefined;
887
+ const providerFailure =
888
+ current?.error !== undefined && current.error.code !== "prompt_deadline_exceeded";
889
+ if (
890
+ current !== undefined &&
891
+ frame.type === "agent_end" &&
892
+ incomingOutcome?.kind === "failed" &&
893
+ incomingOutcome.provenance !== "deadline"
894
+ ) {
895
+ const content = reduceTurnResultContent(current.content, frame.content);
896
+ const failed = {
897
+ ...current,
898
+ revision: ++mutationRevision,
899
+ status: "failed" as const,
900
+ outcome: incomingOutcome,
901
+ error:
902
+ current.error?.code !== "prompt_deadline_exceeded" && current.error !== undefined
903
+ ? current.error
904
+ : { code: incomingOutcome.code, message: incomingOutcome.message },
905
+ content,
906
+ receiptState: reduceReceiptState(
907
+ current.receiptState,
908
+ reportableTurnResultContent(content) ? "present" : undefined,
909
+ ),
910
+ };
911
+ delete (failed as unknown as { pendingOutcome?: unknown }).pendingOutcome;
912
+ delete (failed as unknown as { pendingReceiptState?: unknown }).pendingReceiptState;
913
+ records.set(recordKey, failed);
914
+ try {
915
+ await persist();
916
+ } catch (error) {
917
+ if (records.get(recordKey) === failed) records.set(recordKey, current);
918
+ throw error;
919
+ }
920
+ return;
921
+ }
812
922
  if (
813
923
  frame.type === "agent_end" &&
814
924
  current === pending.finalizedRecord &&
925
+ incomingOutcome?.kind !== "failed" &&
926
+ !providerFailure &&
815
927
  (kind !== "prompt" ||
816
- frame.outcome?.kind === "stopped" ||
928
+ incomingOutcome?.kind === "stopped" ||
817
929
  frame.hasActivity === true ||
818
930
  frame.content?.text.trim())
819
931
  ) {
820
932
  // A real lifecycle end that arrived during deadline finalization is
821
933
  // stronger evidence than the synthetic deadline outcome. Upgrade the
822
- // durable terminal instead of treating the event as a duplicate.
934
+ // durable terminal instead of treating it as a duplicate.
935
+ const content = reduceTurnResultContent(current.content, frame.content);
823
936
  const upgraded = {
824
937
  ...current,
825
938
  revision: ++mutationRevision,
826
939
  status: "terminal_ok" as const,
940
+ content,
941
+ ...(incomingOutcome?.kind === "stopped" ? { outcome: incomingOutcome } : {}),
942
+ receiptState: reduceReceiptState(
943
+ current.receiptState,
944
+ reportableTurnResultContent(content) ? "present" : undefined,
945
+ ),
827
946
  };
828
947
  // Drop the superseded synthetic deadline error so the surfaced payload is
829
948
  // not the contradictory {status: terminal_ok, error: prompt_deadline_exceeded}.
830
949
  delete (upgraded as Partial<InvocationRecord>).error;
950
+ if (incomingOutcome?.kind !== "stopped") delete (upgraded as Partial<InvocationRecord>).outcome;
831
951
  records.set(recordKey, upgraded);
832
952
  try {
833
953
  await persist();
@@ -851,18 +971,115 @@ export function createInvocationReconciliation(
851
971
  }
852
972
  }
853
973
  if (record.terminalAt !== undefined) {
974
+ if (frame.type === "agent_end") {
975
+ const incomingOutcome = canonicalTerminalOutcome(frame.outcome);
976
+ if (incomingOutcome?.kind === "failed" && incomingOutcome.provenance !== "deadline") {
977
+ const content = reduceTurnResultContent(record.content, frame.content);
978
+ const failed = {
979
+ ...record,
980
+ revision: ++mutationRevision,
981
+ status: "failed" as const,
982
+ outcome: incomingOutcome,
983
+ error:
984
+ record.error?.code !== "prompt_deadline_exceeded" && record.error !== undefined
985
+ ? record.error
986
+ : { code: incomingOutcome.code, message: incomingOutcome.message },
987
+ content,
988
+ receiptState: reduceReceiptState(
989
+ record.receiptState,
990
+ reportableTurnResultContent(content) ? "present" : undefined,
991
+ ),
992
+ };
993
+ delete (failed as unknown as { pendingOutcome?: unknown }).pendingOutcome;
994
+ delete (failed as unknown as { pendingReceiptState?: unknown }).pendingReceiptState;
995
+ records.set(recordKey, failed);
996
+ try {
997
+ await persist();
998
+ } catch (error) {
999
+ if (records.get(recordKey) === failed) records.set(recordKey, record);
1000
+ throw error;
1001
+ }
1002
+ return;
1003
+ }
1004
+ }
1005
+ if (frame.type === "agent_failed" && record.error?.code === "prompt_deadline_exceeded") {
1006
+ const failure = sanitizePromptFailure(frame.error);
1007
+ const next = {
1008
+ ...record,
1009
+ revision: ++mutationRevision,
1010
+ status: "failed" as const,
1011
+ error: failure,
1012
+ outcome: canonicalFailedOutcome(failure),
1013
+ };
1014
+ delete (next as unknown as { pendingOutcome?: unknown }).pendingOutcome;
1015
+ delete (next as unknown as { pendingReceiptState?: unknown }).pendingReceiptState;
1016
+ records.set(recordKey, next);
1017
+ try {
1018
+ await persist();
1019
+ } catch (error) {
1020
+ if (records.get(recordKey) === next) records.set(recordKey, record);
1021
+ throw error;
1022
+ }
1023
+ return;
1024
+ }
1025
+ if (frame.type === "agent_end" && record.error?.code !== "prompt_deadline_exceeded") {
1026
+ const content = reduceTurnResultContent(record.content, frame.content);
1027
+ const receiptState = reduceReceiptState(
1028
+ record.receiptState,
1029
+ reportableTurnResultContent(content) ? "present" : undefined,
1030
+ );
1031
+ let outcome = record.outcome;
1032
+ const incomingOutcome = canonicalTerminalOutcome(frame.outcome);
1033
+ if (outcome === undefined) {
1034
+ if (record.status === "failed" && record.error !== undefined)
1035
+ outcome = canonicalFailedOutcome(record.error);
1036
+ else if (record.status === "terminal_ok" && incomingOutcome?.kind === "stopped")
1037
+ outcome = incomingOutcome;
1038
+ }
1039
+ if (content !== record.content || receiptState !== record.receiptState || outcome !== record.outcome) {
1040
+ const enriched = {
1041
+ ...record,
1042
+ revision: ++mutationRevision,
1043
+ content,
1044
+ receiptState,
1045
+ ...(outcome === undefined ? {} : { outcome }),
1046
+ };
1047
+ records.set(recordKey, enriched);
1048
+ try {
1049
+ await persist();
1050
+ } catch (error) {
1051
+ if (records.get(recordKey) === enriched) records.set(recordKey, record);
1052
+ throw error;
1053
+ }
1054
+ }
1055
+ return;
1056
+ }
854
1057
  if (
855
1058
  frame.type === "agent_end" &&
856
1059
  record.error?.code === "prompt_deadline_exceeded" &&
1060
+ canonicalTerminalOutcome(frame.outcome)?.kind !== "failed" &&
857
1061
  (kind !== "prompt" ||
858
- frame.outcome?.kind === "stopped" ||
1062
+ canonicalTerminalOutcome(frame.outcome)?.kind === "stopped" ||
859
1063
  frame.hasActivity === true ||
860
1064
  frame.content?.text.trim())
861
1065
  ) {
862
- const upgraded = { ...record, revision: ++mutationRevision, status: "terminal_ok" as const };
1066
+ const content = reduceTurnResultContent(record.content, frame.content);
1067
+ const incomingOutcome = canonicalTerminalOutcome(frame.outcome);
1068
+ const upgraded = {
1069
+ ...record,
1070
+ revision: ++mutationRevision,
1071
+ status: "terminal_ok" as const,
1072
+ content,
1073
+ ...(incomingOutcome?.kind === "stopped" ? { outcome: incomingOutcome } : {}),
1074
+ receiptState: reduceReceiptState(
1075
+ record.receiptState,
1076
+ reportableTurnResultContent(content) ? "present" : undefined,
1077
+ ),
1078
+ };
863
1079
  // The synthetic deadline error is superseded evidence; terminal_ok must not
864
1080
  // surface a deadline failure for a prompt that actually completed.
865
1081
  delete (upgraded as Partial<InvocationRecord>).error;
1082
+ if (incomingOutcome?.kind !== "stopped") delete (upgraded as Partial<InvocationRecord>).outcome;
866
1083
  records.set(recordKey, upgraded);
867
1084
  try {
868
1085
  await persist();
@@ -919,19 +1136,43 @@ export function createInvocationReconciliation(
919
1136
  if (next.error === undefined || (next.error.code === "agent_failed" && failure.code !== "agent_failed"))
920
1137
  next.error = failure;
921
1138
  } else {
1139
+ const pendingOutcome = (next as unknown as { pendingOutcome?: unknown }).pendingOutcome;
1140
+ delete (next as unknown as { pendingOutcome?: unknown }).pendingOutcome;
1141
+ delete (next as unknown as { pendingReceiptState?: unknown }).pendingReceiptState;
1142
+ next.content = reduceTurnResultContent(next.content, frame.content);
1143
+ const incomingOutcome = canonicalTerminalOutcome(frame.outcome) ?? canonicalTerminalOutcome(pendingOutcome);
922
1144
  if (
1145
+ incomingOutcome?.kind === "stopped" &&
1146
+ (next.error === undefined || next.error.code === "prompt_deadline_exceeded")
1147
+ ) {
1148
+ next.outcome = incomingOutcome;
1149
+ if (next.error?.code === "prompt_deadline_exceeded") delete next.error;
1150
+ next.status = "terminal_ok";
1151
+ } else if (incomingOutcome?.kind === "failed") {
1152
+ next.outcome = incomingOutcome;
1153
+ if (next.error === undefined)
1154
+ next.error = { code: incomingOutcome.code, message: incomingOutcome.message };
1155
+ next.status = "failed";
1156
+ } else if (next.error !== undefined) {
1157
+ next.outcome = canonicalFailedOutcome(next.error);
1158
+ next.status = "failed";
1159
+ } else if (
923
1160
  kind === "prompt" &&
924
- next.error === undefined &&
925
1161
  frame.type === "agent_end" &&
926
- frame.outcome?.kind !== "stopped" &&
927
1162
  !frame.content?.text.trim() &&
928
1163
  !frame.hasActivity
929
1164
  ) {
930
1165
  next.status = "failed";
931
1166
  next.error = EMPTY_PROMPT_FAILURE;
932
- } else next.status = next.error === undefined ? "terminal_ok" : "failed";
1167
+ next.outcome = canonicalFailedOutcome(EMPTY_PROMPT_FAILURE);
1168
+ } else next.status = "terminal_ok";
1169
+ next.receiptState = reduceReceiptState(
1170
+ next.receiptState,
1171
+ reportableTurnResultContent(next.content) ? "present" : "missing",
1172
+ );
933
1173
  next.terminalAt = Date.now();
934
1174
  }
1175
+
935
1176
  records.set(recordKey, next);
936
1177
  try {
937
1178
  await persist();
@@ -942,20 +1183,24 @@ export function createInvocationReconciliation(
942
1183
  },
943
1184
  lookup(kind, selector) {
944
1185
  const record = find(kind, selector);
945
- if (!record) return { status: "unknown" };
1186
+ if (!record) return { status: "unknown", receiptState: "unknown" };
946
1187
  const identity = {
947
1188
  commandId: record.commandId,
948
1189
  turnId: record.turnId,
949
1190
  ...(record.clientRef === undefined ? {} : { clientRef: record.clientRef }),
950
1191
  acceptedAt: record.acceptedAt,
951
1192
  };
952
- if (record.status === "accepted") return { status: "accepted", ...identity };
953
- if (record.status === "in_flight") return { status: "in_flight", ...identity, startedAt: record.startedAt };
1193
+ if (record.status === "accepted") return { status: "accepted", receiptState: "absent", ...identity };
1194
+ if (record.status === "in_flight")
1195
+ return { status: "in_flight", receiptState: "absent", ...identity, startedAt: record.startedAt };
954
1196
  return {
955
1197
  status: record.status,
956
1198
  ...identity,
957
1199
  ...(record.startedAt === undefined ? {} : { startedAt: record.startedAt }),
958
1200
  terminalAt: record.terminalAt,
1201
+ receiptState: record.receiptState ?? "unknown",
1202
+ ...(record.outcome === undefined ? {} : { outcome: record.outcome }),
1203
+ ...(reportableTurnResultContent(record.content) ? { content: record.content } : {}),
959
1204
  ...(record.error === undefined ? {} : { error: record.error }),
960
1205
  };
961
1206
  },
@@ -982,11 +1227,13 @@ export function createInvocationReconciliation(
982
1227
  hydrate,
983
1228
  async claimPendingOutcome(kind, correlation, outcome) {
984
1229
  const record = records.get(key(kind, correlation));
985
- if (!record || record.terminalAt !== undefined || record.kind !== kind) return outcome;
1230
+ const normalized = canonicalTerminalOutcome(outcome);
1231
+ if (normalized === undefined) throw new Error("Invalid terminal outcome.");
1232
+ if (!record || record.terminalAt !== undefined || record.kind !== kind) return normalized;
986
1233
  const pending = (record as unknown as { pendingOutcome?: unknown }).pendingOutcome;
987
- if (pending !== undefined) return pending;
1234
+ if (pending !== undefined) return canonicalTerminalOutcome(pending) ?? normalized;
988
1235
  const next = { ...record, revision: ++mutationRevision } as InvocationRecord & { pendingOutcome?: unknown };
989
- next.pendingOutcome = outcome;
1236
+ next.pendingOutcome = normalized;
990
1237
  records.set(key(kind, correlation), next);
991
1238
  try {
992
1239
  await persist();
@@ -994,7 +1241,7 @@ export function createInvocationReconciliation(
994
1241
  if (records.get(key(kind, correlation)) === next) records.set(key(kind, correlation), record);
995
1242
  throw error;
996
1243
  }
997
- return outcome;
1244
+ return normalized;
998
1245
  },
999
1246
  async finalizeOutcome(
1000
1247
  kind,
@@ -1026,36 +1273,64 @@ export function createInvocationReconciliation(
1026
1273
  const recordKey = key(kind, correlation);
1027
1274
  const record = records.get(recordKey);
1028
1275
  if (!record || record.terminalAt !== undefined || record.kind !== kind) return;
1029
- const finalOutcome = (outcome ??
1030
- (record as unknown as { pendingOutcome?: { kind: string; code: string; message: string } })
1031
- .pendingOutcome) as { kind: string; code: string; message: string } | undefined;
1276
+ const requestedOutcome = canonicalTerminalOutcome(
1277
+ outcome ?? (record as unknown as { pendingOutcome?: unknown }).pendingOutcome,
1278
+ );
1279
+ const priorFailure = record.error;
1280
+ // A provider diagnostic recorded before a deadline finalization is stronger
1281
+ // than the synthetic deadline claim. Preserve it as the terminal failure
1282
+ // intent instead of allowing a later agent_end to look successful.
1283
+ const finalOutcome =
1284
+ priorFailure !== undefined && priorFailure.code !== "prompt_deadline_exceeded"
1285
+ ? canonicalFailedOutcome(priorFailure)
1286
+ : requestedOutcome?.kind === "stopped"
1287
+ ? requestedOutcome
1288
+ : priorFailure !== undefined &&
1289
+ (requestedOutcome === undefined ||
1290
+ (requestedOutcome.kind === "failed" &&
1291
+ (requestedOutcome.code === "prompt_deadline_exceeded" ||
1292
+ priorFailure.code !== "prompt_deadline_exceeded")))
1293
+ ? canonicalFailedOutcome(priorFailure)
1294
+ : requestedOutcome;
1032
1295
  const previousRecord = { ...record };
1033
1296
  const finalizedRecord: InvocationRecord = { ...record, revision: ++mutationRevision, terminalAt: Date.now() };
1034
- if (finalOutcome?.kind === "failed") {
1297
+ finalizedRecord.content = reduceTurnResultContent(finalizedRecord.content, evidence?.content);
1298
+ let resolvedOutcome = finalOutcome;
1299
+ if (resolvedOutcome?.kind === "stopped") {
1300
+ // Stopped is a successful terminal boundary: it must never coexist with
1301
+ // an earlier diagnostic failure.
1302
+ finalizedRecord.status = "terminal_ok";
1303
+ delete finalizedRecord.error;
1304
+ } else if (resolvedOutcome?.kind === "failed") {
1035
1305
  finalizedRecord.status = "failed";
1036
- // Legacy positional recordError overrides the recorded cause; the
1037
- // outcome's own code/message is the default (exact-head review P1).
1038
1306
  finalizedRecord.error =
1039
1307
  recordError !== undefined
1040
1308
  ? { code: recordError.code, message: recordError.message }
1041
- : { code: finalOutcome.code, message: finalOutcome.message };
1042
- } else if (kind === "prompt" && finalOutcome === undefined) {
1309
+ : priorFailure !== undefined &&
1310
+ (resolvedOutcome.code === "prompt_deadline_exceeded" ||
1311
+ priorFailure.code !== "prompt_deadline_exceeded")
1312
+ ? priorFailure
1313
+ : { code: resolvedOutcome.code, message: resolvedOutcome.message };
1314
+ } else if (kind === "prompt") {
1043
1315
  // Evidence-based empty predicate (exact-head review P1/P2): the same
1044
1316
  // semantics as the noteTransition agent_end classifier. A
1045
- // content-bearing, activity-bearing, or explicitly stopped
1046
- // finalization without an explicit outcome is a successful terminal;
1317
+ // content-bearing or activity-bearing finalization is successful;
1047
1318
  // only a genuinely empty, no-activity completion fails closed.
1048
1319
  const terminalText = evidence?.content?.text.trim() ?? "";
1049
1320
  if (terminalText === "" && !evidence?.hasActivity && evidence?.outcomeKind !== "stopped") {
1321
+ resolvedOutcome = canonicalFailedOutcome(EMPTY_PROMPT_FAILURE);
1050
1322
  finalizedRecord.status = "failed";
1051
1323
  finalizedRecord.error = EMPTY_PROMPT_FAILURE;
1052
- } else {
1053
- finalizedRecord.status = "terminal_ok";
1054
- }
1055
- } else {
1056
- finalizedRecord.status = "terminal_ok";
1057
- }
1058
- (finalizedRecord as unknown as Record<string, unknown>).pendingOutcome = undefined;
1324
+ } else finalizedRecord.status = "terminal_ok";
1325
+ } else finalizedRecord.status = "terminal_ok";
1326
+ finalizedRecord.receiptState = reduceReceiptState(
1327
+ finalizedRecord.receiptState,
1328
+ reportableTurnResultContent(finalizedRecord.content) ? "present" : "unknown",
1329
+ );
1330
+ if (resolvedOutcome === undefined) delete finalizedRecord.outcome;
1331
+ else finalizedRecord.outcome = resolvedOutcome;
1332
+ delete (finalizedRecord as unknown as Record<string, unknown>).pendingOutcome;
1333
+ delete (finalizedRecord as unknown as Record<string, unknown>).pendingReceiptState;
1059
1334
  if (isCurrent !== undefined && !isCurrent()) return;
1060
1335
  const commit = Promise.withResolvers<void>();
1061
1336
  // This promise is an internal coordination signal for lifecycle transitions
@@ -1105,8 +1380,13 @@ export function createInvocationReconciliation(
1105
1380
  };
1106
1381
  if (isCurrent !== undefined && !isCurrent()) return;
1107
1382
  delete next.terminalAt;
1108
- delete next.error;
1109
- delete (next as unknown as { outcome?: unknown }).outcome;
1383
+ if (next.error?.code === "prompt_deadline_exceeded" || !isPreservedProviderFailure(next.error?.code))
1384
+ delete next.error;
1385
+ delete next.receiptState;
1386
+ delete (next as unknown as { outcome?: unknown; pendingOutcome?: unknown; pendingReceiptState?: unknown })
1387
+ .outcome;
1388
+ delete (next as unknown as { pendingOutcome?: unknown }).pendingOutcome;
1389
+ delete (next as unknown as { pendingReceiptState?: unknown }).pendingReceiptState;
1110
1390
  (next as unknown as { deadlineRecoveryPending?: boolean }).deadlineRecoveryPending = true;
1111
1391
  if (deadlineMaxAt !== undefined) (next as unknown as { deadlineMaxAt?: number }).deadlineMaxAt = deadlineMaxAt;
1112
1392
  records.set(recordKey, next);
@@ -1145,6 +1425,7 @@ export interface SdkSurfaceFactoryOptions {
1145
1425
  }) => unknown;
1146
1426
  steerStatusLookup?: (selector: { commandId?: string; turnId?: string; clientRef?: string }) => unknown;
1147
1427
  hostTools?: boolean | (() => boolean);
1428
+ getRuntimeHost?: () => SessionSdkHost | undefined;
1148
1429
  }
1149
1430
 
1150
1431
  /** Shared policy, capability, and query-surface factory for every SDK transport. */
@@ -1174,6 +1455,7 @@ function createQuerySurface(
1174
1455
  }) => unknown;
1175
1456
  steerStatusLookup?: (selector: { commandId?: string; turnId?: string; clientRef?: string }) => unknown;
1176
1457
  hostTools?: boolean | (() => boolean);
1458
+ getRuntimeHost?: () => SessionSdkHost | undefined;
1177
1459
  } = {},
1178
1460
  ): SessionSurface {
1179
1461
  const policy =
@@ -1577,6 +1859,28 @@ function createQuerySurface(
1577
1859
  turnId?: string;
1578
1860
  clientRef?: string;
1579
1861
  }) => (options.turnResultLookup ?? (value => reconciliation.lookupResult(value.kind, value)))(selector),
1862
+ getCheckpointSnapshot: () => {
1863
+ const host = options.getRuntimeHost?.();
1864
+ if (!host)
1865
+ throw Object.assign(new Error("Atomic session checkpoint capture is unavailable."), {
1866
+ code: "unavailable",
1867
+ });
1868
+ const entries =
1869
+ typeof (ctx as Partial<ExtensionContext>).getTranscript === "function" ? ctx.getTranscript() : undefined;
1870
+ if (!Array.isArray(entries))
1871
+ throw Object.assign(new Error("Atomic session checkpoint capture is unavailable."), {
1872
+ code: "unavailable",
1873
+ });
1874
+ return {
1875
+ entries,
1876
+ watermark: {
1877
+ revision: entries.length,
1878
+ generation: host.events.generation,
1879
+ seq: host.events.sequence,
1880
+ idle: ctx.isIdle(),
1881
+ },
1882
+ };
1883
+ },
1580
1884
  getSteerStatus: (selector: { commandId?: string; turnId?: string; clientRef?: string }) =>
1581
1885
  (options.steerStatusLookup ?? (value => reconciliation.lookup("steer", value)))(selector),
1582
1886
  getModelProfiles: async () => {
@@ -1626,6 +1930,7 @@ export function createSdkSurfaceFactory(
1626
1930
  turnResultLookup: options.turnResultLookup,
1627
1931
  steerStatusLookup: options.steerStatusLookup,
1628
1932
  hostTools: options.hostTools,
1933
+ getRuntimeHost: options.getRuntimeHost,
1629
1934
  });
1630
1935
  return {
1631
1936
  policy,
@@ -1933,6 +2238,7 @@ function createControlSurface(
1933
2238
  ) => void,
1934
2239
  canResolveGate: () => boolean = () => true,
1935
2240
  trackGateResolution: <T>(resolution: Promise<T>) => Promise<T> = async resolution => await resolution,
2241
+ onInvocationCompletionReconciledForTests?: (kind: InvocationKind, correlation: InvocationCorrelation) => void,
1936
2242
  ): ControlSurface {
1937
2243
  const normalizePromptImages = (value: unknown): ImageContent[] => {
1938
2244
  if (!Array.isArray(value)) return [];
@@ -2180,18 +2486,9 @@ function createControlSurface(
2180
2486
  try {
2181
2487
  await reconciliation.noteTransition(kind, correlation, {
2182
2488
  type: "agent_end",
2183
- ...(typeof result === "string"
2184
- ? {
2185
- content: {
2186
- version: 1,
2187
- type: "text",
2188
- text: result,
2189
- byteLength: 0,
2190
- truncated: false,
2191
- },
2192
- }
2193
- : {}),
2489
+ ...(typeof result === "string" ? { content: sanitizeTurnResultContent(result) } : {}),
2194
2490
  });
2491
+ onInvocationCompletionReconciledForTests?.(kind, correlation);
2195
2492
  retirePendingOwner?.(kind, correlation, "always");
2196
2493
  } catch (transitionError) {
2197
2494
  if (kind === "prompt" || kind === "skill") {
@@ -3844,7 +4141,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
3844
4141
  lifecycleOwner?: LifecycleOwner,
3845
4142
  terminalContent?: TurnResultContent,
3846
4143
  terminalHasActivity = false,
3847
- terminalOutcome?: { kind: "stopped"; reason: "cancelled"; provenance: "client_cancel" },
4144
+ terminalOutcome?: InvocationOutcome,
3848
4145
  ): Promise<void> => {
3849
4146
  const current = lifecycleOwner?.state ?? active;
3850
4147
  if (!current) return;
@@ -4109,7 +4406,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4109
4406
  ? ({
4110
4407
  content: terminalContent,
4111
4408
  hasActivity: terminalHasActivity,
4112
- outcome: terminalOutcome,
4409
+ ...(terminalOutcome?.kind === "stopped" ? { outcome: terminalOutcome } : {}),
4113
4410
  } satisfies PromptTerminalTransitionEvidence)
4114
4411
  : undefined,
4115
4412
  );
@@ -4313,7 +4610,9 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4313
4610
  const terminalOutcome =
4314
4611
  event.stopReason === "cancelled"
4315
4612
  ? ({ kind: "stopped", reason: "cancelled", provenance: "client_cancel" } as const)
4316
- : undefined;
4613
+ : failure === undefined
4614
+ ? undefined
4615
+ : canonicalFailedOutcome(failure);
4317
4616
  return trackLifecycle(async () => {
4318
4617
  if (failure && !failureAlreadyPublished) {
4319
4618
  for (const key of genericFailureKeys) owner?.failureDiagnosticKeys.delete(key);
@@ -4502,6 +4801,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4502
4801
  });
4503
4802
  await Promise.race([settled, timeout]);
4504
4803
  };
4804
+ let runtime: SessionSdkSessionRuntime;
4505
4805
  const surfaceFactory = createSdkSurfaceFactory({
4506
4806
  ctx,
4507
4807
  id: sessionId,
@@ -4511,6 +4811,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4511
4811
  steerStatusLookup: selector => steerReconciliation.lookupSteer(selector),
4512
4812
  configOverrides: options.configOverrides,
4513
4813
  settings: options.settings,
4814
+ getRuntimeHost: () => runtime?.host,
4514
4815
  });
4515
4816
  const queryHandlers = new QueryHandlers(surfaceFactory.query, sessionId, revisions, cursors);
4516
4817
  const inputGate = { quiescing: false };
@@ -4567,7 +4868,6 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4567
4868
  skillRecoveryControllers.delete(key);
4568
4869
  });
4569
4870
  };
4570
- let runtime: SessionSdkSessionRuntime;
4571
4871
  // Durable-first bounded terminalization for accepted submissions that leave
4572
4872
  // their queue or race a run WITHOUT consumption (exact-head review: clearing
4573
4873
  // the lease before the durable writes strands the row accepted with no
@@ -4856,6 +5156,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4856
5156
  },
4857
5157
  () => acceptingGateResolutions,
4858
5158
  trackGateResolution,
5159
+ options.onInvocationCompletionReconciledForTests,
4859
5160
  );
4860
5161
  const installProviderDefinitions = (capability: string, definitions: unknown): void => {
4861
5162
  if (capability === "permission") {
@@ -4923,6 +5224,10 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
4923
5224
  };
4924
5225
  runtime = new SessionSdkSessionRuntime({
4925
5226
  transport,
5227
+ eventRevision: () =>
5228
+ typeof (ctx as Partial<ExtensionContext>).getTranscript === "function"
5229
+ ? ctx.getTranscript().length
5230
+ : undefined,
4926
5231
  masterCapabilityVerify: frame =>
4927
5232
  verifyMasterCapabilityFrame({
4928
5233
  frame,
@@ -5034,9 +5339,37 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
5034
5339
  },
5035
5340
  });
5036
5341
  if (isAutoroutingInactive(api)) markAutoroutingInactive(runtime.host);
5037
- const disposeGate = ctx.workflowGate?.onGateEmitted?.(gate =>
5342
+ const disposeWorkflowGate = ctx.workflowGate?.onGateEmitted?.(gate =>
5038
5343
  runtime.emitEvent({ kind: "workflow_gate", payload: gate }),
5039
5344
  );
5345
+ // Every fold (steer, chord, auto-background timer, `bash.background`)
5346
+ // publishes one replayable `bash_folded` frame; `runtime.jobs.list`
5347
+ // `foldReason` remains the source of truth clients reconcile against after
5348
+ // a replay, since a replayed frame may describe a job that has since ended.
5349
+ const disposeFold = ctx.onJobFold?.(event => {
5350
+ try {
5351
+ runtime.emitEvent({
5352
+ type: "bash_folded",
5353
+ sessionId,
5354
+ jobId: event.jobId,
5355
+ generation: event.generation,
5356
+ reason: event.reason,
5357
+ });
5358
+ } catch (error) {
5359
+ // The fold already happened; publication failure must not affect it,
5360
+ // but it must leave evidence because a client relying on the event
5361
+ // instead of runtime.jobs.list would otherwise never learn of the fold.
5362
+ logger.warn("sdk: bash_folded publication failed", {
5363
+ jobId: event.jobId,
5364
+ reason: event.reason,
5365
+ error: error instanceof Error ? error.message : String(error),
5366
+ });
5367
+ }
5368
+ });
5369
+ const disposeGate = (): void => {
5370
+ disposeWorkflowGate?.();
5371
+ disposeFold?.();
5372
+ };
5040
5373
  let publishedEndpointUrl: string | undefined;
5041
5374
  let brokerRegistered = false;
5042
5375
  const registerBroker = async (): Promise<void> => {