@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
@@ -1,8 +1,9 @@
1
- import { describe, expect, test } from "bun:test";
1
+ import { describe, expect, spyOn, test } from "bun:test";
2
2
  import { createHash } from "node:crypto";
3
- import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises";
4
4
  import * as os from "node:os";
5
5
  import * as path from "node:path";
6
+ import { logger } from "@gajae-code/utils";
6
7
  import { AsyncJobManager } from "../../async";
7
8
  import type { Settings } from "../../config/settings";
8
9
  import type { ExtensionAPI, ExtensionContext, ExtensionTranscriptEntry } from "../../extensibility/extensions";
@@ -254,7 +255,10 @@ test("native prompt reconciliation fails closed for an explicitly empty assistan
254
255
  receiptState: "missing",
255
256
  });
256
257
  reconciliation.noteTransition(correlation, { type: "agent_end", finalText: "late text" });
257
- expect(reconciliation.lookup({ clientRef: "native-empty-ref" })).toMatchObject({ status: "failed" });
258
+ expect(reconciliation.lookup({ clientRef: "native-empty-ref" })).toMatchObject({
259
+ status: "failed",
260
+ receiptState: "present",
261
+ });
258
262
  });
259
263
 
260
264
  test("broker reconciliation fails closed for an empty prompt and persists the first terminal result", async () => {
@@ -283,7 +287,9 @@ test("broker reconciliation fails closed for an empty prompt and persists the fi
283
287
  expect(reloaded.lookupResult("prompt", { clientRef: "broker-empty-ref" })).toMatchObject({
284
288
  status: "failed",
285
289
  error: { code: "prompt_failed" },
290
+ content: { text: "late text", byteLength: 9, truncated: false },
286
291
  });
292
+ expect(reloaded.lookup("prompt", { clientRef: "broker-empty-ref" })).toMatchObject({ receiptState: "present" });
287
293
  const noOpCorrelation = { commandId: "broker-noop-command", turnId: "broker-noop-turn" };
288
294
  await reconciliation.noteAccepted("prompt", noOpCorrelation, "broker-noop-ref");
289
295
  await reconciliation.finalizeOutcome(
@@ -446,6 +452,42 @@ test("SDK-only finalizeOutcome keeps legacy recordError positional compatibility
446
452
  });
447
453
  });
448
454
 
455
+ test("SDK-only terminal outcomes survive result projection and durable reload", async () => {
456
+ let records: unknown[] = [];
457
+ const store = {
458
+ path: null,
459
+ load: async () => records,
460
+ transact: async (mutator: (current: never[]) => never[]) => {
461
+ records = mutator(records as never);
462
+ },
463
+ } as never;
464
+ const stopped = { kind: "stopped", reason: "cancelled", provenance: "client_cancel" } as const;
465
+ const reconciliation = createInvocationReconciliation({ store });
466
+ const correlation = { commandId: "outcome-command", turnId: "outcome-turn" };
467
+ await reconciliation.noteAccepted("prompt", correlation, "outcome-ref");
468
+ await reconciliation.claimPendingOutcome("prompt", correlation, stopped);
469
+ await reconciliation.finalizeOutcome("prompt", correlation);
470
+ const projected = reconciliation.lookupResult("prompt", { clientRef: "outcome-ref" });
471
+ expect(projected).toMatchObject({ status: "terminal_ok", outcome: stopped, receiptState: "unknown" });
472
+ expect(reconciliation.lookup("prompt", { clientRef: "outcome-ref" })).toMatchObject({ outcome: stopped });
473
+
474
+ const reopened = createInvocationReconciliation({ store });
475
+ await reopened.hydrate();
476
+ expect(reopened.lookupResult("prompt", { clientRef: "outcome-ref" })).toMatchObject({
477
+ status: "terminal_ok",
478
+ outcome: stopped,
479
+ });
480
+
481
+ const direct = createInvocationReconciliation();
482
+ const directCorrelation = { commandId: "direct-outcome-command", turnId: "direct-outcome-turn" };
483
+ await direct.noteAccepted("prompt", directCorrelation, "direct-outcome-ref");
484
+ await direct.noteTransition("prompt", directCorrelation, { type: "agent_end", outcome: stopped });
485
+ expect(direct.lookupResult("prompt", { clientRef: "direct-outcome-ref" })).toMatchObject({
486
+ status: "terminal_ok",
487
+ outcome: stopped,
488
+ });
489
+ });
490
+
449
491
  test("a late agent failure never overwrites the reason an already terminal record carries", async () => {
450
492
  const reconciliation = createInvocationReconciliation();
451
493
  const correlation = { commandId: "first-reason-command", turnId: "first-reason-turn" };
@@ -493,7 +535,7 @@ test("a failed acceptance persist rolls back the provisional record and reservat
493
535
  "accept persist failed",
494
536
  );
495
537
  reconciliation.release("prompt", "accept-failed-ref");
496
- expect(reconciliation.lookup("prompt", first)).toEqual({ status: "unknown" });
538
+ expect(reconciliation.lookup("prompt", first)).toEqual({ status: "unknown", receiptState: "unknown" });
497
539
 
498
540
  const retry = { commandId: "accept-retry-command", turnId: "accept-retry-turn" };
499
541
  reconciliation.admit("prompt", "accept-failed-ref");
@@ -548,6 +590,54 @@ test("a failed uncertainty persist restores the durable deadline terminal until
548
590
  ]);
549
591
  });
550
592
 
593
+ test("SDK-only markUncertain persists an active-schema record without quarantining siblings", async () => {
594
+ const root = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-uncertain-reload-"));
595
+ try {
596
+ const sessionFile = path.join(root, "session.jsonl");
597
+ await Bun.write(sessionFile, "");
598
+ const store = createReconciliationStore({ sessionFile, sessionId: "uncertain-reload" });
599
+ const reconciliation = createInvocationReconciliation({ store });
600
+ const correlation = { commandId: "uncertain-reload-command", turnId: "uncertain-reload-turn" };
601
+ const sibling = { commandId: "uncertain-sibling-command", turnId: "uncertain-sibling-turn" };
602
+ await reconciliation.noteAccepted("prompt", correlation, "uncertain-reload-ref");
603
+ await reconciliation.finalizeOutcome("prompt", correlation, {
604
+ kind: "failed",
605
+ code: "prompt_deadline_exceeded",
606
+ message: "deadline",
607
+ });
608
+ await reconciliation.noteAccepted("skill", sibling, "uncertain-sibling-ref");
609
+ await reconciliation.finalizeOutcome("skill", sibling, {
610
+ kind: "stopped",
611
+ code: "cancelled",
612
+ message: "cancelled",
613
+ });
614
+
615
+ await reconciliation.markUncertain("prompt", correlation, undefined, 5_000);
616
+ const active = store
617
+ .snapshot()
618
+ .find(record => record.kind === "prompt" && record.commandId === correlation.commandId) as
619
+ | Record<string, unknown>
620
+ | undefined;
621
+ expect(active).toMatchObject({ status: "accepted", deadlineRecoveryPending: true, deadlineMaxAt: 5_000 });
622
+ expect(active?.terminalAt).toBeUndefined();
623
+ expect(active?.receiptState).toBeUndefined();
624
+ expect(active?.outcome).toBeUndefined();
625
+ expect(active?.pendingOutcome).toBeUndefined();
626
+ expect(active?.pendingReceiptState).toBeUndefined();
627
+
628
+ const reopenedStore = createReconciliationStore({ sessionFile, sessionId: "uncertain-reload" });
629
+ const reopened = createInvocationReconciliation({ store: reopenedStore });
630
+ await reopened.hydrate();
631
+ expect(reopened.lookup("prompt", { clientRef: "uncertain-reload-ref" })).toMatchObject({ status: "accepted" });
632
+ expect(reopened.lookup("skill", { clientRef: "uncertain-sibling-ref" })).toMatchObject({
633
+ status: "terminal_ok",
634
+ });
635
+ expect((await readdir(root)).some(name => name.includes("corrupt"))).toBe(false);
636
+ } finally {
637
+ await rm(root, { recursive: true, force: true });
638
+ }
639
+ });
640
+
551
641
  test("durable reload keeps agent_end terminal across a paused successor transition", async () => {
552
642
  let records: unknown[] = [];
553
643
  let pauseWrites = 0;
@@ -911,8 +1001,13 @@ test("finalization without an explicit outcome follows the noteTransition eviden
911
1001
  await legacyArg5.finalizeOutcome("prompt", legacyCorrelation, undefined, undefined, {
912
1002
  content: { version: 1, type: "text", text: "legacy", byteLength: 6, truncated: false },
913
1003
  });
914
- expect(legacyArg5.lookup("prompt", { clientRef: "final-legacy-ref" })).toMatchObject({
1004
+ await legacyArg5.noteTransition("prompt", legacyCorrelation, {
1005
+ type: "agent_end",
1006
+ content: { version: 1, type: "text", text: "later", byteLength: 5, truncated: false },
1007
+ });
1008
+ expect(legacyArg5.lookupResult("prompt", { clientRef: "final-legacy-ref" })).toMatchObject({
915
1009
  status: "terminal_ok",
1010
+ content: { text: "legacy", byteLength: 6, truncated: false },
916
1011
  });
917
1012
 
918
1013
  const explicitFailure = createInvocationReconciliation({ store: makeLocalStore() });
@@ -924,9 +1019,58 @@ test("finalization without an explicit outcome follows the noteTransition eviden
924
1019
  message: "provider rejected",
925
1020
  });
926
1021
  expect(explicitFailure.lookup("prompt", { clientRef: "final-failure-ref" })).toMatchObject({
1022
+ status: "failed",
1023
+ receiptState: "unknown",
1024
+ outcome: { kind: "failed", code: "prompt_failed", provenance: "agent_failed" },
1025
+ error: { code: "prompt_failed" },
1026
+ });
1027
+ });
1028
+
1029
+ test("provider failures outrank later stopped outcomes in both SDK-only terminal paths", async () => {
1030
+ const providerError = Object.assign(new Error("provider rejected"), { code: "provider_rejected" });
1031
+ const transition = createInvocationReconciliation();
1032
+ const transitionCorrelation = { commandId: "precedence-transition-command", turnId: "precedence-transition-turn" };
1033
+ await transition.noteAccepted("prompt", transitionCorrelation, "precedence-transition-ref");
1034
+ await transition.noteTransition("prompt", transitionCorrelation, { type: "agent_failed", error: providerError });
1035
+ await transition.noteTransition("prompt", transitionCorrelation, {
1036
+ type: "agent_end",
1037
+ outcome: { kind: "stopped", reason: "cancelled", provenance: "client_cancel" },
1038
+ });
1039
+ expect(transition.lookupResult("prompt", { clientRef: "precedence-transition-ref" })).toMatchObject({
1040
+ status: "failed",
1041
+ error: { code: "provider_rejected" },
1042
+ });
1043
+
1044
+ const finalized = createInvocationReconciliation();
1045
+ const finalizedCorrelation = { commandId: "precedence-finalize-command", turnId: "precedence-finalize-turn" };
1046
+ await finalized.noteAccepted("prompt", finalizedCorrelation, "precedence-finalize-ref");
1047
+ await finalized.noteTransition("prompt", finalizedCorrelation, { type: "agent_failed", error: providerError });
1048
+ await finalized.claimPendingOutcome("prompt", finalizedCorrelation, {
1049
+ kind: "failed",
1050
+ code: "prompt_deadline_exceeded",
1051
+ message: "deadline",
1052
+ });
1053
+ await finalized.finalizeOutcome("prompt", finalizedCorrelation);
1054
+ expect(finalized.lookupResult("prompt", { clientRef: "precedence-finalize-ref" })).toMatchObject({
927
1055
  status: "failed",
928
1056
  error: { code: "provider_rejected" },
929
1057
  });
1058
+
1059
+ const generic = createInvocationReconciliation();
1060
+ const genericCorrelation = { commandId: "precedence-generic-command", turnId: "precedence-generic-turn" };
1061
+ await generic.noteAccepted("prompt", genericCorrelation, "precedence-generic-ref");
1062
+ await generic.noteTransition("prompt", genericCorrelation, {
1063
+ type: "agent_failed",
1064
+ error: Object.assign(new Error("generic failure"), { code: "agent_failed" }),
1065
+ });
1066
+ await generic.noteTransition("prompt", genericCorrelation, {
1067
+ type: "agent_end",
1068
+ outcome: { kind: "stopped", reason: "cancelled", provenance: "client_cancel" },
1069
+ });
1070
+ expect(generic.lookupResult("prompt", { clientRef: "precedence-generic-ref" })).toMatchObject({
1071
+ status: "failed",
1072
+ error: { code: "agent_failed", message: "Prompt submission failed." },
1073
+ });
930
1074
  });
931
1075
 
932
1076
  test("a reason attached after a prompt settled is never replaced by a later failure", async () => {
@@ -945,6 +1089,8 @@ test("a reason attached after a prompt settled is never replaced by a later fail
945
1089
  clientRef: "late-reason-ref",
946
1090
  acceptedAt: expect.any(Number),
947
1091
  terminalAt: expect.any(Number),
1092
+ receiptState: "present",
1093
+ content: { version: 1, type: "text", text: "completed", byteLength: 9, truncated: false },
948
1094
  });
949
1095
  // A failure delivered after the record settled enriches it with the sanitized reason and
950
1096
  // leaves the terminal claim itself (status, terminalAt, identity) untouched.
@@ -1052,6 +1198,7 @@ test("capacity eviction still releases the clientRef and reports honest unknown"
1052
1198
  }
1053
1199
  expect(reconciliation.lookup("prompt", { clientRef: "capacity-ref-1" })).toEqual({
1054
1200
  status: "unknown",
1201
+ receiptState: "unknown",
1055
1202
  });
1056
1203
  expect(() => reconciliation.admit("prompt", "capacity-ref-1")).not.toThrow();
1057
1204
  const retained = {
@@ -1073,6 +1220,7 @@ describe("SessionSdkSessionRuntime", () => {
1073
1220
  const transport = memoryTransport();
1074
1221
  const runtime = new SessionSdkSessionRuntime({
1075
1222
  transport,
1223
+ eventRevision: () => 7,
1076
1224
  control: async (_connectionId, frame) => ({ id: frame.id, ok: true, result: { operation: frame.operation } }),
1077
1225
  query: async (_connectionId, frame) => ({ id: frame.id, ok: true, result: { query: frame.query } }),
1078
1226
  });
@@ -1092,7 +1240,7 @@ describe("SessionSdkSessionRuntime", () => {
1092
1240
  });
1093
1241
  transport.feed("client", { type: "query_request", id: "query", query: "Q18", input: {} });
1094
1242
  await Bun.sleep(0);
1095
- expect(transport.broadcasts.some(frame => frame.kind === "session_ready")).toBe(true);
1243
+ expect(transport.broadcasts).toContainEqual(expect.objectContaining({ kind: "session_ready", revision: 7 }));
1096
1244
  expect(transport.sent).toEqual(
1097
1245
  expect.arrayContaining([
1098
1246
  expect.objectContaining({ type: "event_replay_result", id: "replay", ok: true }),
@@ -1102,6 +1250,110 @@ describe("SessionSdkSessionRuntime", () => {
1102
1250
  );
1103
1251
  await runtime.stop();
1104
1252
  });
1253
+ test("SDK-only host publishes one replayable bash_folded frame per fold and drops the subscription on shutdown", async () => {
1254
+ const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-bash-folded-"));
1255
+ const handlers = new Map<string, (event: unknown, ctx: ExtensionContext) => Promise<void> | void>();
1256
+ const api = {
1257
+ on(event: string, handler: (event: unknown, ctx: ExtensionContext) => Promise<void> | void) {
1258
+ handlers.set(event, handler);
1259
+ },
1260
+ sendUserMessage: async () => {},
1261
+ } as unknown as ExtensionAPI;
1262
+ const transport = memoryTransport();
1263
+ createSdkSessionRuntimeExtension(api, { agentDir: cwd, createTransport: async () => transport });
1264
+ const listeners = new Set<(event: { jobId: string; generation: string; reason: string }) => void>();
1265
+ const ctx = {
1266
+ ...extensionContext(transport.sessionId, cwd),
1267
+ onJobFold: (listener: (event: { jobId: string; generation: string; reason: string }) => void) => {
1268
+ listeners.add(listener);
1269
+ return () => listeners.delete(listener);
1270
+ },
1271
+ } as unknown as ExtensionContext;
1272
+ try {
1273
+ await handlers.get("session_start")?.({}, ctx);
1274
+ expect(listeners.size).toBe(1);
1275
+ for (const listener of listeners) listener({ jobId: "bg_7", generation: "bg_7:1", reason: "steer" });
1276
+ transport.feed("client", { type: "event_replay", id: "fold-replay", sinceSeq: 0 });
1277
+ await Bun.sleep(0);
1278
+ const replay = transport.sent.find(
1279
+ frame => frame.type === "event_replay_result" && frame.id === "fold-replay",
1280
+ ) as { events?: Array<{ kind?: unknown; payload?: Record<string, unknown> }> } | undefined;
1281
+ const folded = replay?.events?.filter(event => event.kind === "bash_folded") ?? [];
1282
+ expect(folded).toEqual([
1283
+ expect.objectContaining({
1284
+ kind: "bash_folded",
1285
+ payload: {
1286
+ type: "bash_folded",
1287
+ sessionId: transport.sessionId,
1288
+ jobId: "bg_7",
1289
+ generation: "bg_7:1",
1290
+ reason: "steer",
1291
+ },
1292
+ }),
1293
+ ]);
1294
+ } finally {
1295
+ await handlers.get("session_shutdown")?.({}, ctx);
1296
+ }
1297
+ expect(listeners.size).toBe(0);
1298
+ });
1299
+
1300
+ test("SDK-only host logs a bash_folded publication failure and keeps the fold replayable", async () => {
1301
+ const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-bash-folded-fail-"));
1302
+ const handlers = new Map<string, (event: unknown, ctx: ExtensionContext) => Promise<void> | void>();
1303
+ const api = {
1304
+ on(event: string, handler: (event: unknown, ctx: ExtensionContext) => Promise<void> | void) {
1305
+ handlers.set(event, handler);
1306
+ },
1307
+ sendUserMessage: async () => {},
1308
+ } as unknown as ExtensionAPI;
1309
+ const transport = memoryTransport();
1310
+ const broadcastFrame = transport.broadcastFrame?.bind(transport);
1311
+ let failNextBroadcast = false;
1312
+ transport.broadcastFrame = frame => {
1313
+ if (failNextBroadcast && frame.kind === "bash_folded") {
1314
+ failNextBroadcast = false;
1315
+ throw new Error("broadcast socket closed");
1316
+ }
1317
+ broadcastFrame?.(frame);
1318
+ };
1319
+ createSdkSessionRuntimeExtension(api, { agentDir: cwd, createTransport: async () => transport });
1320
+ const listeners = new Set<(event: { jobId: string; generation: string; reason: string }) => void>();
1321
+ const ctx = {
1322
+ ...extensionContext(transport.sessionId, cwd),
1323
+ onJobFold: (listener: (event: { jobId: string; generation: string; reason: string }) => void) => {
1324
+ listeners.add(listener);
1325
+ return () => listeners.delete(listener);
1326
+ },
1327
+ } as unknown as ExtensionContext;
1328
+ const warnings: Array<{ message: string; data?: Record<string, unknown> }> = [];
1329
+ const warn = spyOn(logger, "warn").mockImplementation((message, data) => {
1330
+ warnings.push({ message: String(message), data: data as Record<string, unknown> | undefined });
1331
+ });
1332
+ try {
1333
+ await handlers.get("session_start")?.({}, ctx);
1334
+ failNextBroadcast = true;
1335
+ // The listener call must not throw back into the manager: publication failure is evidence, not a fold error.
1336
+ for (const listener of listeners) listener({ jobId: "bg_9", generation: "bg_9:1", reason: "chord" });
1337
+ expect(warnings.filter(entry => entry.message === "sdk: bash_folded publication failed")).toEqual([
1338
+ expect.objectContaining({
1339
+ data: expect.objectContaining({ jobId: "bg_9", reason: "chord", error: "broadcast socket closed" }),
1340
+ }),
1341
+ ]);
1342
+ // The frame was appended to the ring before the broadcast failed, so it still replays.
1343
+ transport.feed("client", { type: "event_replay", id: "fold-replay-fail", sinceSeq: 0 });
1344
+ await Bun.sleep(0);
1345
+ const replay = transport.sent.find(
1346
+ frame => frame.type === "event_replay_result" && frame.id === "fold-replay-fail",
1347
+ ) as { events?: Array<{ kind?: unknown; payload?: Record<string, unknown> }> } | undefined;
1348
+ expect(replay?.events?.filter(event => event.kind === "bash_folded")).toEqual([
1349
+ expect.objectContaining({ payload: expect.objectContaining({ jobId: "bg_9", reason: "chord" }) }),
1350
+ ]);
1351
+ } finally {
1352
+ warn.mockRestore();
1353
+ await handlers.get("session_shutdown")?.({}, ctx);
1354
+ }
1355
+ });
1356
+
1105
1357
  test("SDK-only host admits, replays, and conflicts terminal abort requests durably", async () => {
1106
1358
  const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-terminal-abort-"));
1107
1359
  const operatorCapability = "runtime-terminal-abort-capability";
@@ -2582,26 +2834,32 @@ describe("SessionSdkSessionRuntime", () => {
2582
2834
 
2583
2835
  // B's accepted receipt is redacted to {} once its submission rejects,
2584
2836
  // so select its record by the stable clientRef instead.
2585
- // Let the async rejection settle and retire B's pending entry.
2586
- await Bun.sleep(50);
2587
2837
  // The rejected submission must TERMINALIZE as failed (#4668 review
2588
2838
  // P1): agent_failed alone is diagnostic-only, so the settlement also
2589
2839
  // writes agent_end. Before that fix B stayed accepted forever.
2590
2840
  {
2591
2841
  const ids = { clientRef: "fail-b-ref" };
2592
- transport.feed("conn-b", {
2593
- type: "query_request",
2594
- id: "fail-b-status",
2595
- query: "turn.prompt_status",
2596
- input: { kind: "prompt", ...ids },
2597
- } as SdkFrame);
2598
- // waitResponse polls control_response only; queries get query_response.
2599
2842
  const statusDeadline = Date.now() + 15_000;
2600
- while (!transport.sent.some(frame => frame.id === "fail-b-status")) {
2843
+ let status: Record<string, unknown> | undefined;
2844
+ for (let attempt = 0; status === undefined; attempt += 1) {
2845
+ const id = `fail-b-status-${attempt}`;
2846
+ transport.feed("conn-b", {
2847
+ type: "query_request",
2848
+ id,
2849
+ query: "turn.prompt_status",
2850
+ input: { kind: "prompt", ...ids },
2851
+ } as SdkFrame);
2852
+ while (!transport.sent.some(frame => frame.id === id)) {
2853
+ if (Date.now() > statusDeadline) throw new Error("Timed out waiting for fail-b-status");
2854
+ await Bun.sleep(20);
2855
+ }
2856
+ const response = transport.sent.find(frame => frame.id === id);
2857
+ const result = response?.result as Record<string, unknown> | undefined;
2858
+ if (result?.status === "failed") status = response;
2859
+ else await Bun.sleep(20);
2601
2860
  if (Date.now() > statusDeadline) throw new Error("Timed out waiting for fail-b-status");
2602
- await Bun.sleep(20);
2603
2861
  }
2604
- expect(transport.sent.find(frame => frame.id === "fail-b-status")).toMatchObject({
2862
+ expect(status).toMatchObject({
2605
2863
  ok: true,
2606
2864
  result: expect.objectContaining({ status: "failed" }),
2607
2865
  });
@@ -3510,6 +3768,8 @@ async function invocationHarness(
3510
3768
  onLifecycleDrainTimeout?: () => void;
3511
3769
  onFailureDiagnosticKeyCount?: (count: number) => void;
3512
3770
  agentFailedWriteFailures?: number;
3771
+ branch?: unknown[];
3772
+ onInvocationCompletionReconciled?: (kind: string, correlation: { commandId: string; turnId: string }) => void;
3513
3773
  },
3514
3774
  ): Promise<InvocationHarness> {
3515
3775
  const waiters = new Map<string, (frame: ResponseFrame) => void>();
@@ -3552,6 +3812,9 @@ async function invocationHarness(
3552
3812
  ...(hooks.onFailureDiagnosticKeyCount
3553
3813
  ? { onFailureDiagnosticKeyCountForTests: hooks.onFailureDiagnosticKeyCount }
3554
3814
  : {}),
3815
+ ...(hooks.onInvocationCompletionReconciled
3816
+ ? { onInvocationCompletionReconciledForTests: hooks.onInvocationCompletionReconciled }
3817
+ : {}),
3555
3818
  ...(hooks.settings ? { settings: hooks.settings } : {}),
3556
3819
  createTransport: async ({ sessionId: id, stateRoot, token }) => ({
3557
3820
  sessionId: id,
@@ -3590,7 +3853,7 @@ async function invocationHarness(
3590
3853
  getSessionId: () => sessionId,
3591
3854
  getSessionFile: () => path.join(cwd, ".gjc", "state", `${sessionId}.jsonl`),
3592
3855
  getSessionName: () => undefined,
3593
- getBranch: () => [],
3856
+ getBranch: () => hooks.branch ?? [],
3594
3857
  },
3595
3858
  };
3596
3859
  await handlers.get("session_start")?.({}, ctx);
@@ -4465,7 +4728,9 @@ describe("post-acceptance invocation terminalization", () => {
4465
4728
  expect(secondIds.turnId).not.toBe(firstIds.turnId);
4466
4729
  await harness.emit("agent_start");
4467
4730
  await successorStarted.promise;
4468
- await harness.emit("agent_end");
4731
+ await harness.emit("agent_end", {
4732
+ messages: [{ role: "assistant", content: "predecessor delayed" }],
4733
+ });
4469
4734
  expect(await harness.query("turn.prompt_status", secondIds)).toMatchObject({
4470
4735
  result: { status: expect.stringMatching(/accepted|in_flight/) },
4471
4736
  });
@@ -4473,10 +4738,23 @@ describe("post-acceptance invocation terminalization", () => {
4473
4738
  status: "failed",
4474
4739
  error: { code: "aborted" },
4475
4740
  });
4741
+ const firstResult = await harness.query("turn.result", { kind: "prompt", ...firstIds });
4742
+ expect(firstResult).toMatchObject({
4743
+ result: { status: "failed", content: { text: "predecessor delayed" } },
4744
+ });
4476
4745
  await harness.emit("agent_end", { messages: [{ role: "assistant", content: "completed" }] });
4477
4746
  expect(await settledStatus(harness, "turn.prompt_status", secondIds)).toMatchObject({
4478
4747
  status: "terminal_ok",
4479
4748
  });
4749
+ expect(await harness.query("turn.result", { kind: "prompt", ...secondIds })).toMatchObject({
4750
+ result: {
4751
+ status: "terminal_ok",
4752
+ content: { text: "completed", byteLength: 9, truncated: false },
4753
+ },
4754
+ });
4755
+ expect(await harness.query("turn.result", { kind: "prompt", ...firstIds })).toMatchObject({
4756
+ result: { content: { text: "predecessor delayed" } },
4757
+ });
4480
4758
  expect(prompts).toBe(2);
4481
4759
  await harness.stop();
4482
4760
  } finally {
@@ -4494,7 +4772,7 @@ describe("post-acceptance invocation terminalization", () => {
4494
4772
  const harness = await invocationHarness("successor-progress", cwd, {
4495
4773
  settings: {
4496
4774
  get: (key: string) =>
4497
- key === "sdk.promptDeadlineMs" ? 25 : key === "sdk.promptMaxRuntimeMs" ? 60_000 : undefined,
4775
+ key === "sdk.promptDeadlineMs" ? 2_000 : key === "sdk.promptMaxRuntimeMs" ? 60_000 : undefined,
4498
4776
  } as unknown as Settings,
4499
4777
  sendUserMessage: async (_content, options) => {
4500
4778
  prompts += 1;
@@ -4519,7 +4797,7 @@ describe("post-acceptance invocation terminalization", () => {
4519
4797
  await successorStarted.promise;
4520
4798
  for (let index = 0; index < 4; index += 1) {
4521
4799
  await harness.emit("tool_execution_start");
4522
- await Bun.sleep(15);
4800
+ await Bun.sleep(800);
4523
4801
  }
4524
4802
  expect(await harness.query("turn.prompt_status", successorIds)).toMatchObject({
4525
4803
  result: { status: expect.stringMatching(/accepted|in_flight/) },
@@ -4673,6 +4951,190 @@ describe("post-acceptance invocation terminalization", () => {
4673
4951
  await rm(cwd, { recursive: true, force: true });
4674
4952
  }
4675
4953
  });
4954
+ test("captures exact bounded content from actual prompt agent_end messages", async () => {
4955
+ const cases = [
4956
+ { name: "short", agent: "SDK_OK", expected: "SDK_OK", bytes: 6, truncated: false },
4957
+ { name: "max", agent: "x".repeat(16_384), expected: "x".repeat(16_384), bytes: 16_384, truncated: false },
4958
+ { name: "blank", agent: " ", expected: undefined, bytes: 0, truncated: false },
4959
+ {
4960
+ name: "overflow",
4961
+ agent: `${"😀".repeat(4_096)}tail`,
4962
+ expected: "😀".repeat(4_096),
4963
+ bytes: 16_384,
4964
+ truncated: true,
4965
+ },
4966
+ ] as const;
4967
+ for (const testCase of cases) {
4968
+ const cwd = await mkdtemp(path.join(os.tmpdir(), `gjc-terminal-content-${testCase.name}-`));
4969
+ try {
4970
+ const harness = await invocationHarness(`terminal-content-${testCase.name}`, cwd, {
4971
+ sendUserMessage: async (_content, options) => {
4972
+ await options?.onPreflightAcceptCommit?.();
4973
+ await Promise.withResolvers<void>().promise;
4974
+ },
4975
+ });
4976
+ const accepted = await harness.control("turn.prompt", { text: "hello" });
4977
+ await harness.emit("agent_start");
4978
+ await harness.emit("agent_end", { messages: [{ role: "assistant", content: testCase.agent }] });
4979
+ const result = await settledStatus(harness, "turn.result", {
4980
+ kind: "prompt",
4981
+ commandId: accepted.result?.commandId,
4982
+ turnId: accepted.result?.turnId,
4983
+ });
4984
+ if (testCase.expected === undefined) {
4985
+ expect(result).toMatchObject({ status: "failed" });
4986
+ expect((result as Record<string, unknown>).content).toBeUndefined();
4987
+ } else {
4988
+ expect(result).toMatchObject({
4989
+ status: "terminal_ok",
4990
+ content: {
4991
+ text: testCase.expected,
4992
+ byteLength: testCase.bytes,
4993
+ truncated: testCase.truncated,
4994
+ },
4995
+ });
4996
+ }
4997
+ await harness.stop();
4998
+ } finally {
4999
+ await rm(cwd, { recursive: true, force: true });
5000
+ }
5001
+ }
5002
+ });
5003
+ test("does not reuse a previous branch assistant for a new textless prompt", async () => {
5004
+ const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-terminal-order-previous-assistant-"));
5005
+ try {
5006
+ const harness = await invocationHarness("terminal-order-previous-assistant", cwd, {
5007
+ branch: [{ type: "message", message: { role: "assistant", content: "PREVIOUS" } }],
5008
+ sendUserMessage: async (_content, options) => {
5009
+ await options?.onPreflightAcceptCommit?.();
5010
+ await Promise.withResolvers<void>().promise;
5011
+ },
5012
+ });
5013
+ const accepted = await harness.control("turn.prompt", { text: "textless" });
5014
+ const selector = {
5015
+ kind: "prompt" as const,
5016
+ commandId: accepted.result?.commandId,
5017
+ turnId: accepted.result?.turnId,
5018
+ };
5019
+ await harness.emit("agent_start");
5020
+ await harness.emit("agent_end", { messages: [] });
5021
+ const result = await settledStatus(harness, "turn.result", selector);
5022
+ expect((result as Record<string, unknown>).content).toBeUndefined();
5023
+ await harness.stop();
5024
+ } finally {
5025
+ await rm(cwd, { recursive: true, force: true });
5026
+ }
5027
+ });
5028
+ test("reconciles contract-valid skill completion and agent_end ordering", async () => {
5029
+ const cases = [
5030
+ {
5031
+ name: "completion-real-agent-real",
5032
+ order: "completion-first",
5033
+ completion: "completion",
5034
+ agent: "agent",
5035
+ expected: "completion",
5036
+ },
5037
+ {
5038
+ name: "completion-blank-agent-real",
5039
+ order: "completion-first",
5040
+ completion: " ",
5041
+ agent: "agent",
5042
+ expected: "agent",
5043
+ },
5044
+ {
5045
+ name: "completion-none-agent-real",
5046
+ order: "completion-first",
5047
+ completion: null,
5048
+ agent: "agent",
5049
+ expected: "agent",
5050
+ },
5051
+ {
5052
+ name: "agent-real-completion-real",
5053
+ order: "agent-first",
5054
+ completion: "completion",
5055
+ agent: "agent",
5056
+ expected: "agent",
5057
+ },
5058
+ {
5059
+ name: "agent-real-completion-blank",
5060
+ order: "agent-first",
5061
+ completion: " ",
5062
+ agent: "agent",
5063
+ expected: "agent",
5064
+ },
5065
+ {
5066
+ name: "agent-blank-completion-real",
5067
+ order: "agent-first",
5068
+ completion: "completion",
5069
+ agent: " ",
5070
+ expected: "completion",
5071
+ },
5072
+ {
5073
+ name: "agent-blank-completion-none",
5074
+ order: "agent-first",
5075
+ completion: null,
5076
+ agent: " ",
5077
+ expected: undefined,
5078
+ },
5079
+ {
5080
+ name: "agent-none-completion-real",
5081
+ order: "agent-first",
5082
+ completion: "completion",
5083
+ agent: null,
5084
+ expected: "completion",
5085
+ },
5086
+ {
5087
+ name: "agent-none-completion-blank",
5088
+ order: "agent-first",
5089
+ completion: " ",
5090
+ agent: null,
5091
+ expected: undefined,
5092
+ },
5093
+ ] as const;
5094
+ for (const testCase of cases) {
5095
+ const cwd = await mkdtemp(path.join(os.tmpdir(), `gjc-skill-terminal-order-${testCase.name}-`));
5096
+ const completion = Promise.withResolvers<unknown>();
5097
+ const completionReconciled = Promise.withResolvers<void>();
5098
+ try {
5099
+ const harness = await invocationHarness(`skill-terminal-order-${testCase.name}`, cwd, {
5100
+ onInvocationCompletionReconciled: kind => {
5101
+ if (kind === "skill") completionReconciled.resolve();
5102
+ },
5103
+ invokeSkill: async (_name, _args, options) => {
5104
+ await options?.onPreflightAcceptCommit?.();
5105
+ return await completion.promise;
5106
+ },
5107
+ });
5108
+ const accepted = await harness.control("skill.invoke", { name: "ralplan" });
5109
+ const selector = {
5110
+ kind: "skill" as const,
5111
+ commandId: accepted.result?.commandId,
5112
+ turnId: accepted.result?.turnId,
5113
+ };
5114
+ await harness.emit("agent_start");
5115
+ const emitAgentEnd = () =>
5116
+ harness.emit("agent_end", {
5117
+ messages: testCase.agent === null ? [] : [{ role: "assistant", content: testCase.agent }],
5118
+ });
5119
+ if (testCase.order === "completion-first") {
5120
+ completion.resolve(testCase.completion);
5121
+ await completionReconciled.promise;
5122
+ await emitAgentEnd();
5123
+ } else {
5124
+ await emitAgentEnd();
5125
+ completion.resolve(testCase.completion);
5126
+ await completionReconciled.promise;
5127
+ }
5128
+ const result = await harness.query("turn.result", selector);
5129
+ const content = (result.result as { content?: { text?: string } } | undefined)?.content;
5130
+ expect(content?.text).toBe(testCase.expected);
5131
+ await harness.stop();
5132
+ } finally {
5133
+ completion.resolve(undefined);
5134
+ await rm(cwd, { recursive: true, force: true });
5135
+ }
5136
+ }
5137
+ }, 30_000);
4676
5138
  test("a queued follow-up prompt is not terminalized before the turn runs", async () => {
4677
5139
  const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-terminalize-followup-"));
4678
5140
  try {
@@ -4791,7 +5253,7 @@ describe("post-acceptance invocation terminalization", () => {
4791
5253
  const rejected = await harness.control("turn.prompt", { text: "hello", clientRef: "preflight-ref" });
4792
5254
  expect(rejected.ok).toBe(false);
4793
5255
  const status = await harness.query("turn.prompt_status", { clientRef: "preflight-ref" });
4794
- expect(status.result).toEqual({ status: "unknown" });
5256
+ expect(status.result as Record<string, unknown>).toEqual({ status: "unknown", receiptState: "unknown" });
4795
5257
  await harness.stop();
4796
5258
  } finally {
4797
5259
  await Bun.sleep(50);
@@ -5098,9 +5560,13 @@ describe("accepted-control zero-execution bound (#4668)", () => {
5098
5560
  // renewal wiring at the SDK boundary.
5099
5561
  const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-renew-attached-"));
5100
5562
  try {
5563
+ const renewalSettings = {
5564
+ get: (key: string) =>
5565
+ key === "sdk.promptDeadlineMs" ? 2_000 : key === "sdk.promptMaxRuntimeMs" ? 60_000 : undefined,
5566
+ } as unknown as Settings;
5101
5567
  let promoted: ((promotion: { startsOwnRun: boolean }) => void) | undefined;
5102
5568
  const harness = await invocationHarness("renew-attached", cwd, {
5103
- settings: zeroProgressSettings,
5569
+ settings: renewalSettings,
5104
5570
  sendUserMessage: async (content, options) => {
5105
5571
  await options?.onPreflightAcceptCommit?.();
5106
5572
  if (content === "consumed") {
@@ -5119,12 +5585,12 @@ describe("accepted-control zero-execution bound (#4668)", () => {
5119
5585
  expect(consumed.ok).toBe(true);
5120
5586
  promoted?.({ startsOwnRun: false });
5121
5587
  const ids = { commandId: consumed.result?.commandId, turnId: consumed.result?.turnId };
5122
- // Past the 25ms lease, repeated tool activity keeps the attached
5123
- // correlation alive: without renewal it would already be
5124
- // prompt_deadline_exceeded by the first sleep boundary.
5588
+ // Past the initial 2s lease, repeated tool activity keeps the
5589
+ // attached correlation alive. Each interval stays well within one
5590
+ // renewed lease while their cumulative duration exceeds the original.
5125
5591
  for (let i = 0; i < 3; i += 1) {
5126
5592
  await harness.emit("tool_execution_start");
5127
- await Bun.sleep(15);
5593
+ await Bun.sleep(800);
5128
5594
  }
5129
5595
  const midRun = await harness.query("turn.prompt_status", ids);
5130
5596
  expect(midRun.result?.status).not.toBe("failed");
@@ -5328,7 +5794,7 @@ describe("accepted-control zero-execution bound (#4668)", () => {
5328
5794
  const harness = await invocationHarness("dispatch-race-lease", cwd, {
5329
5795
  settings: {
5330
5796
  get: (key: string) =>
5331
- key === "sdk.promptDeadlineMs" ? 25 : key === "sdk.promptMaxRuntimeMs" ? 60_000 : undefined,
5797
+ key === "sdk.promptDeadlineMs" ? 2_000 : key === "sdk.promptMaxRuntimeMs" ? 60_000 : undefined,
5332
5798
  } as unknown as Settings,
5333
5799
  sendUserMessage: async (content, options) => {
5334
5800
  await options?.onPreflightAcceptCommit?.();
@@ -5353,9 +5819,9 @@ describe("accepted-control zero-execution bound (#4668)", () => {
5353
5819
  const raced = await harness.control("turn.prompt", { text: "raced" });
5354
5820
  expect(raced.ok).toBe(true);
5355
5821
  const idsRaced = { commandId: raced.result?.commandId, turnId: raced.result?.turnId };
5356
- // Far past the 25ms lease while queued: the acceptance-anchored lease
5822
+ // Far past the 2s lease while queued: the acceptance-anchored lease
5357
5823
  // was dropped at the divert disposition, so no false deadline fire.
5358
- await Bun.sleep(120);
5824
+ await Bun.sleep(2_200);
5359
5825
  const queued = await harness.query("turn.prompt_status", idsRaced);
5360
5826
  expect(queued.result?.status).not.toBe("failed");
5361
5827
  // Real consumption re-leases and attaches to the in-flight run.