@gajae-code/coding-agent 0.17.4 → 0.17.5

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 (84) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/dist/types/config/autorouting-tier-map.d.ts +1031 -3
  3. package/dist/types/config/file-lock.d.ts +4 -2
  4. package/dist/types/config/model-registry.d.ts +3 -1
  5. package/dist/types/config/model-resolver.d.ts +1 -1
  6. package/dist/types/exec/bash-executor.d.ts +2 -0
  7. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +11 -2
  8. package/dist/types/gjc-runtime/state-writer.d.ts +14 -1
  9. package/dist/types/harness-control-plane/control-endpoint.d.ts +4 -1
  10. package/dist/types/harness-control-plane/finalize.d.ts +14 -3
  11. package/dist/types/main.d.ts +1 -1
  12. package/dist/types/runtime-mcp/loader.d.ts +2 -0
  13. package/dist/types/runtime-mcp/manager.d.ts +13 -6
  14. package/dist/types/sdk/broker/broker.d.ts +4 -2
  15. package/dist/types/sdk/broker/ensure.d.ts +3 -0
  16. package/dist/types/sdk/broker/identity.d.ts +2 -0
  17. package/dist/types/sdk/broker/managed-task-dag.d.ts +1335 -0
  18. package/dist/types/sdk/broker/managed-task-verification.d.ts +37 -0
  19. package/dist/types/sdk/broker/spawn-authority.d.ts +8 -0
  20. package/dist/types/sdk/broker/startup-failure.d.ts +4 -0
  21. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +5 -1
  22. package/dist/types/sdk/prompt-deadline-flush.d.ts +2 -0
  23. package/dist/types/sdk/session.d.ts +3 -3
  24. package/dist/types/session/agent-session.d.ts +7 -2
  25. package/dist/types/task/types.d.ts +2 -0
  26. package/dist/types/tools/bash-interactive.d.ts +1 -0
  27. package/dist/types/tools/fetch.d.ts +10 -1
  28. package/dist/types/tools/subagent.d.ts +8 -1
  29. package/package.json +7 -7
  30. package/src/cli/public-command-registry.ts +8 -2
  31. package/src/cli/setup-cli.ts +1 -0
  32. package/src/cli/stats-cli.ts +11 -1
  33. package/src/config/autorouting-tier-map.ts +529 -3
  34. package/src/config/file-lock.ts +59 -14
  35. package/src/config/model-discovery-manager.ts +4 -4
  36. package/src/config/model-profile-activation.ts +83 -23
  37. package/src/config/model-registry.ts +72 -12
  38. package/src/config/model-resolver.ts +6 -1
  39. package/src/coordinator-mcp/question-state.ts +4 -1
  40. package/src/coordinator-mcp/server.ts +14 -6
  41. package/src/exec/bash-executor.ts +3 -0
  42. package/src/extensibility/gjc-plugins/runtime-adapters.ts +46 -8
  43. package/src/gjc-runtime/state-writer.ts +317 -6
  44. package/src/harness-control-plane/control-endpoint.ts +24 -6
  45. package/src/harness-control-plane/finalize.ts +312 -25
  46. package/src/harness-control-plane/owner.ts +323 -89
  47. package/src/internal-urls/docs-index.generated.ts +6 -4
  48. package/src/main.ts +69 -19
  49. package/src/modes/acp/acp-agent.ts +581 -37
  50. package/src/modes/components/gjc-bundle-settings.ts +21 -1
  51. package/src/modes/components/tool-status-header.ts +72 -6
  52. package/src/modes/controllers/runtime-mcp-command-controller.ts +1 -1
  53. package/src/runtime-mcp/loader.ts +6 -3
  54. package/src/runtime-mcp/manager.ts +177 -75
  55. package/src/sdk/broker/broker.ts +1057 -150
  56. package/src/sdk/broker/ensure.ts +21 -3
  57. package/src/sdk/broker/identity.ts +13 -0
  58. package/src/sdk/broker/managed-task-dag.ts +1927 -0
  59. package/src/sdk/broker/managed-task-verification.ts +642 -0
  60. package/src/sdk/broker/spawn-authority.ts +140 -7
  61. package/src/sdk/broker/startup-failure.ts +80 -1
  62. package/src/sdk/broker/transport.ts +1 -0
  63. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  64. package/src/sdk/bus/index.ts +22 -6
  65. package/src/sdk/bus/telegram-daemon-contract.ts +5 -1
  66. package/src/sdk/cli/session-cli.ts +18 -0
  67. package/src/sdk/host/session-runtime.test.ts +46 -0
  68. package/src/sdk/host/session-runtime.ts +13 -6
  69. package/src/sdk/lifecycle/service.ts +2 -0
  70. package/src/sdk/prompt-deadline-flush.ts +26 -4
  71. package/src/sdk/session.ts +587 -115
  72. package/src/session/agent-session.ts +40 -12
  73. package/src/session/internal/managed-session-scope.ts +10 -0
  74. package/src/session/internal/managed-session-storage.ts +30 -1
  75. package/src/setup/provider-presets.json +2 -1
  76. package/src/slash-commands/builtin-registry.ts +1 -1
  77. package/src/task/executor.ts +46 -17
  78. package/src/task/types.ts +2 -0
  79. package/src/tools/bash-interactive.ts +2 -0
  80. package/src/tools/bash.ts +20 -0
  81. package/src/tools/fetch.ts +21 -5
  82. package/src/tools/subagent-render.ts +24 -1
  83. package/src/tools/subagent.ts +27 -1
  84. package/src/utils/mupdf.ts +11 -4
@@ -92,6 +92,8 @@ const SESSION_PAGE_SIZE = 50;
92
92
  const MAX_ACP_REPLAY_PAGES = 10_000;
93
93
  /** Bounded retention of settled prompt correlations so late duplicates stay closed. */
94
94
  const SETTLED_PROMPT_CORRELATION_RETENTION = 16;
95
+ /** Bounded valid terminal candidates retained while an uncertain prompt has only clientRef identity. */
96
+ const PROVISIONAL_ABORT_TERMINAL_CANDIDATE_RETENTION = 16;
95
97
  /**
96
98
  * A cancelled prompt must still settle. The SDK acknowledges `turn.abort` before the
97
99
  * aborted run publishes its normalized terminal, and agent-owned async work that
@@ -225,6 +227,15 @@ interface PromptWaiter {
225
227
  }
226
228
 
227
229
  type PromptCorrelation = { commandId?: string; turnId?: string };
230
+ type UncertainAbortOwner = {
231
+ kind: PromptWaiter["invocationKind"];
232
+ clientRef: string;
233
+ correlation?: { commandId: string; turnId: string };
234
+ };
235
+ type UncertainPromptRecoveryResult =
236
+ | { kind: "terminal" }
237
+ | { kind: "uncertain"; error: AcpSdkAdapterError }
238
+ | { kind: "ownership_lost" };
228
239
  type PromptPhaseOwner = PromptWaiter | "background" | undefined;
229
240
 
230
241
  type BrokerConnection = { adapter: AcpSdkAdapter; client: SdkClient };
@@ -263,6 +274,9 @@ type SessionRecord = {
263
274
  activePrompt?: PromptWaiter;
264
275
  /** One bounded read-only recovery owner for this attachment and waiter. */
265
276
  statusRecovery?: PromptWaiter;
277
+ statusRecoveryTask?: Promise<UncertainPromptRecoveryResult>;
278
+ /** An uncertain abort's exact late terminal is required before a successor may start. */
279
+ uncertainAbortOwner?: UncertainAbortOwner;
266
280
  /** Set by `session/cancel` so an in-flight prompt settles as `cancelled`, never as an error. */
267
281
  cancelRequested?: boolean;
268
282
  /** True once the session's first logical prompt has settled; gates first-turn readiness retries. */
@@ -607,6 +621,14 @@ function sdkFrameCorrelation(frame: JsonObject, event?: JsonObject): PromptCorre
607
621
  return strictCorrelationFrom(frame, event);
608
622
  }
609
623
 
624
+ function sdkFrameBelongsToSession(frame: JsonObject, event: JsonObject | undefined, sessionId: string): boolean {
625
+ return (
626
+ frame.routerSessionId === sessionId &&
627
+ (frame.sessionId === undefined || frame.sessionId === sessionId) &&
628
+ (event?.sessionId === undefined || event.sessionId === sessionId)
629
+ );
630
+ }
631
+
610
632
  /**
611
633
  * Conflicting envelope/event identities own no prompt and therefore cannot
612
634
  * refresh a watchdog or publish into either turn.
@@ -618,7 +640,7 @@ function watchdogCorrelationFrom(frame: JsonObject, event?: JsonObject): PromptC
618
640
  function logDroppedPromptTerminal(
619
641
  sessionId: string,
620
642
  event: JsonObject,
621
- reason: "incomplete_correlation" | "correlation_mismatch",
643
+ reason: "incomplete_correlation" | "correlation_mismatch" | "session_mismatch",
622
644
  actual: PromptCorrelation,
623
645
  expected?: PromptCorrelation,
624
646
  ): void {
@@ -1048,18 +1070,20 @@ const ROUTER_PASSTHROUGH_FRAME_TYPES = new Set([
1048
1070
  "reverse_request_cancelled",
1049
1071
  ]);
1050
1072
 
1051
- function acpFrameFromRouted(frame: SessionRouterFrame): JsonObject {
1073
+ function acpFrameFromRouted(frame: SessionRouterFrame, attachmentSessionId: string): JsonObject {
1052
1074
  if (
1053
1075
  frame.body.type === "activity" ||
1054
1076
  frame.body.type === "agent_start" ||
1055
1077
  frame.body.type === "agent_end" ||
1056
1078
  frame.body.type === "agent_failed"
1057
1079
  )
1058
- return frame.body;
1059
- if (typeof frame.body.type === "string" && ROUTER_PASSTHROUGH_FRAME_TYPES.has(frame.body.type)) return frame.body;
1080
+ return { ...frame.body, routerSessionId: attachmentSessionId };
1081
+ if (typeof frame.body.type === "string" && ROUTER_PASSTHROUGH_FRAME_TYPES.has(frame.body.type))
1082
+ return { ...frame.body, routerSessionId: attachmentSessionId };
1060
1083
  const connectionId = typeof frame.body.connectionId === "string" ? frame.body.connectionId : undefined;
1061
1084
  return {
1062
1085
  type: "event",
1086
+ routerSessionId: attachmentSessionId,
1063
1087
  ...(frame.name === undefined ? {} : { kind: frame.name }),
1064
1088
  ...(frame.sessionId === undefined ? {} : { sessionId: frame.sessionId }),
1065
1089
  ...(frame.commandId === undefined ? {} : { commandId: frame.commandId }),
@@ -1617,6 +1641,8 @@ export class AcpAgent implements Agent {
1617
1641
  readonly #pendingRouterFrames = new Map<string, Record<string, unknown>[]>();
1618
1642
  #routerStartPromise: Promise<void> | undefined;
1619
1643
  readonly #sessions = new Map<string, SessionRecord>();
1644
+ /** Exact abort owners survive SessionRecord replacement until terminal proof or retirement. */
1645
+ readonly #uncertainAbortOwners = new Map<string, UncertainAbortOwner>();
1620
1646
  /** Retain settled prompt identities across automatic transport reattachment. */
1621
1647
  readonly #retiredPromptCorrelations = new Map<string, PromptCorrelation[]>();
1622
1648
  /** Prevent a successor admission while a retired prompt can still reveal its identity. */
@@ -1689,7 +1715,7 @@ export class AcpAgent implements Agent {
1689
1715
  await adapter.attachmentReady(attachment);
1690
1716
  },
1691
1717
  onFrame: (attachment, frame) => {
1692
- const acpFrame = acpFrameFromRouted(frame);
1718
+ const acpFrame = acpFrameFromRouted(frame, attachment.sessionId);
1693
1719
  const adapter =
1694
1720
  this.#sessions.get(attachment.sessionId)?.adapter ??
1695
1721
  this.#pendingRouterAdapters.get(attachment.sessionId);
@@ -1934,6 +1960,7 @@ export class AcpAgent implements Agent {
1934
1960
  { sessionId: params.sessionId },
1935
1961
  this.#lifecycleIdempotencyKey(params.sessionId, "session.delete"),
1936
1962
  );
1963
+ this.#uncertainAbortOwners.delete(params.sessionId);
1937
1964
  return {};
1938
1965
  }
1939
1966
  this.#beginTeardown(params.sessionId);
@@ -1965,6 +1992,7 @@ export class AcpAgent implements Agent {
1965
1992
  this.#knownSessionMetadata.delete(params.sessionId);
1966
1993
  this.#retiredPromptCorrelations.delete(params.sessionId);
1967
1994
  this.#retiredPromptAcknowledgements.delete(params.sessionId);
1995
+ this.#uncertainAbortOwners.delete(params.sessionId);
1968
1996
  return {};
1969
1997
  }
1970
1998
  throw error;
@@ -1976,6 +2004,7 @@ export class AcpAgent implements Agent {
1976
2004
  { sessionId: params.sessionId, sessionPath: saved, cwd, target: { path: cwd } },
1977
2005
  this.#lifecycleIdempotencyKey(params.sessionId, "session.delete"),
1978
2006
  );
2007
+ this.#uncertainAbortOwners.delete(params.sessionId);
1979
2008
  this.#knownSessionCwds.delete(params.sessionId);
1980
2009
  this.#ownedSessionIds.delete(params.sessionId);
1981
2010
  this.#knownSessionMcpServers.delete(params.sessionId);
@@ -2204,6 +2233,11 @@ export class AcpAgent implements Agent {
2204
2233
  if (record.pendingFirstPromptRetry && record.pendingFirstPromptRetry !== retryReservation)
2205
2234
  throw new AcpSdkAdapterError("conflict", "ACP session is retrying its first prompt.");
2206
2235
  if (record.activePrompt) throw new AcpSdkAdapterError("conflict", "ACP session already has an active prompt.");
2236
+ if (record.uncertainAbortOwner)
2237
+ throw new AcpSdkAdapterError(
2238
+ "conflict",
2239
+ "ACP session is still awaiting the uncertainly aborted turn's terminal.",
2240
+ );
2207
2241
  if (record.authFailure) throw new AcpSdkAdapterError("authentication_failed", record.authFailure);
2208
2242
  if (this.#retiredPromptAcknowledgements.has(params.sessionId))
2209
2243
  throw new AcpSdkAdapterError(
@@ -2419,7 +2453,67 @@ export class AcpAgent implements Agent {
2419
2453
  record.adapter !== promptAdapter ||
2420
2454
  record.activePrompt !== waiter
2421
2455
  ) {
2456
+ const current = this.#sessions.get(params.sessionId);
2457
+ const provisionalOwner = current?.uncertainAbortOwner;
2458
+ if (
2459
+ provisionalOwner?.kind === waiter.invocationKind &&
2460
+ provisionalOwner.clientRef === waiter.clientRef &&
2461
+ provisionalOwner.correlation === undefined &&
2462
+ this.#hasRetiredPromptCorrelation(params.sessionId, record, acknowledgementCorrelation)
2463
+ ) {
2464
+ // A stale ACK cannot bind a provisional abort owner. Keep the fence
2465
+ // until this session is retired or new exact evidence is available.
2466
+ waiter.deferredFrames.length = 0;
2467
+ return await response;
2468
+ }
2469
+ this.#bindUncertainAbortOwner(params.sessionId, waiter, acknowledgementCorrelation);
2422
2470
  this.#rememberSettledPromptCorrelation(params.sessionId, record, acknowledgementCorrelation);
2471
+ const retainedOwner = current?.uncertainAbortOwner;
2472
+ if (
2473
+ current &&
2474
+ retainedOwner?.kind === waiter.invocationKind &&
2475
+ retainedOwner.clientRef === waiter.clientRef &&
2476
+ retainedOwner.correlation &&
2477
+ correlationsExactlyMatch(retainedOwner.correlation, acknowledgementCorrelation)
2478
+ ) {
2479
+ await current.frameTail;
2480
+ const deferredFrames = waiter.deferredFrames.splice(0);
2481
+ let reconciledDeferredTerminal = false;
2482
+ for (const deferred of deferredFrames) {
2483
+ const deferredEvent = receivedSdkEvent(deferred.frame)?.event;
2484
+ if (!deferredEvent) continue;
2485
+ const deferredOutcome = terminalOutcome(deferredEvent);
2486
+ const validDeferredTerminal =
2487
+ (deferredEvent.type === "agent_end" && deferredOutcome !== undefined) ||
2488
+ (deferredEvent.type === "agent_failed" && deferredOutcome?.kind === "failed");
2489
+ const deferredCorrelation = sdkFrameCorrelation(deferred.frame, deferredEvent);
2490
+ if (
2491
+ !validDeferredTerminal ||
2492
+ !sdkFrameBelongsToSession(deferred.frame, deferredEvent, params.sessionId) ||
2493
+ !deferredCorrelation ||
2494
+ !hasCompleteCorrelation(deferredCorrelation) ||
2495
+ !correlationsExactlyMatch(deferredCorrelation, acknowledgementCorrelation)
2496
+ )
2497
+ continue;
2498
+ waiter.terminalReserved = true;
2499
+ await this.#handleSdkFrame(
2500
+ params.sessionId,
2501
+ current.adapter,
2502
+ deferred.frame,
2503
+ current.publicationGeneration,
2504
+ );
2505
+ reconciledDeferredTerminal = true;
2506
+ break;
2507
+ }
2508
+ if (!reconciledDeferredTerminal)
2509
+ void this.#recoverUncertainAbortAfterLateAcknowledgement(
2510
+ params.sessionId,
2511
+ current.adapter,
2512
+ retainedOwner,
2513
+ );
2514
+ } else {
2515
+ waiter.deferredFrames.length = 0;
2516
+ }
2423
2517
  return await response;
2424
2518
  }
2425
2519
  if (this.#hasRetiredPromptCorrelation(params.sessionId, record, acknowledgementCorrelation)) {
@@ -2447,6 +2541,7 @@ export class AcpAgent implements Agent {
2447
2541
  waiter.boundary = record.inboundSequence;
2448
2542
  waiter.correlation = acknowledgementCorrelation;
2449
2543
  waiter.acknowledged = true;
2544
+ this.#bindUncertainAbortOwner(params.sessionId, waiter, acknowledgementCorrelation);
2450
2545
  if (promptWaiterRetired(record, waiter)) {
2451
2546
  this.#rememberSettledPromptCorrelation(params.sessionId, record, acknowledgementCorrelation);
2452
2547
  return await response;
@@ -2680,6 +2775,49 @@ export class AcpAgent implements Agent {
2680
2775
  }
2681
2776
  waiter?.cancelAttemptResolve?.(true);
2682
2777
  } catch (error) {
2778
+ let cancellationError = error;
2779
+ const abortDetails = error instanceof SdkClientError ? object(error.details) : undefined;
2780
+ if (
2781
+ error instanceof SdkClientError &&
2782
+ error.code === "uncertain_after_send" &&
2783
+ abortDetails?.operation === "turn.abort" &&
2784
+ waiter &&
2785
+ record.activePrompt === waiter &&
2786
+ waiter.dispatched &&
2787
+ !waiter.settled
2788
+ ) {
2789
+ if (waiter.terminalReserved) {
2790
+ waiter.cancelAttemptResolve?.(true);
2791
+ return;
2792
+ }
2793
+ this.#retainUncertainAbortOwner(params.sessionId, record, waiter);
2794
+ let recovery = record.statusRecovery === waiter ? record.statusRecoveryTask : undefined;
2795
+ if (!recovery && this.#startUncertainPromptRecovery(params.sessionId, record, waiter))
2796
+ recovery = record.statusRecoveryTask;
2797
+ if (recovery) {
2798
+ const recoveryResult = await recovery;
2799
+ if (
2800
+ recoveryResult.kind === "terminal" ||
2801
+ (recoveryResult.kind === "ownership_lost" && waiter.terminalReserved)
2802
+ ) {
2803
+ record.cancelRequested = false;
2804
+ waiter.cancelAttemptResolve?.(true);
2805
+ return;
2806
+ }
2807
+ cancellationError =
2808
+ recoveryResult.kind === "uncertain"
2809
+ ? recoveryResult.error
2810
+ : new AcpSdkAdapterError(
2811
+ "terminal_uncertain",
2812
+ "ACP abort recovery lost ownership before exact terminal evidence was resolved. No mutation was replayed.",
2813
+ );
2814
+ } else {
2815
+ cancellationError = new AcpSdkAdapterError(
2816
+ "terminal_uncertain",
2817
+ "ACP abort could not retain recovery ownership. No mutation was replayed.",
2818
+ );
2819
+ }
2820
+ }
2683
2821
  // With no active prompt the cancel intent normally clears — except when a first-turn
2684
2822
  // retry is reserved across its backoff gap. That reservation owner has not yet observed
2685
2823
  // the cancel; clearing it here would let the retry resubmit a turn the client cancelled
@@ -2704,7 +2842,7 @@ export class AcpAgent implements Agent {
2704
2842
  waiter.cancelAttempt = undefined;
2705
2843
  waiter.cancelAttemptResolve = undefined;
2706
2844
  }
2707
- throw error;
2845
+ throw cancellationError;
2708
2846
  } finally {
2709
2847
  if (waiter) {
2710
2848
  waiter.pendingCancelAttempts = Math.max(0, (waiter.pendingCancelAttempts ?? 1) - 1);
@@ -2735,6 +2873,22 @@ export class AcpAgent implements Agent {
2735
2873
  if (!waiter.cancelAcknowledged) return;
2736
2874
  if (this.#sessions.get(id) !== record || record.activePrompt !== waiter || waiter.settled) return;
2737
2875
  if (waiter.terminalReserved) return;
2876
+ // An overlapping acknowledged cancel does not supersede the earlier
2877
+ // uncertain abort's bounded terminal lookup. Let that exact read either
2878
+ // prove a terminal or retire the waiter as still uncertain before grace
2879
+ // settlement can discard its foreground recovery owner.
2880
+ if (record.statusRecovery === waiter && this.#ownsUncertainAbort(record, waiter)) return;
2881
+ const uncertainAbortOwner = this.#uncertainAbortOwners.get(id);
2882
+ if (uncertainAbortOwner && this.#ownsUncertainAbort(record, waiter)) {
2883
+ record.uncertainAbortOwner = uncertainAbortOwner;
2884
+ const ownerCorrelation = uncertainAbortOwner.correlation;
2885
+ if (
2886
+ ownerCorrelation &&
2887
+ !record.backgroundCorrelations.some(owner => correlationsExactlyMatch(owner, ownerCorrelation))
2888
+ )
2889
+ record.backgroundCorrelations.push(ownerCorrelation);
2890
+ record.backgroundBusy = true;
2891
+ }
2738
2892
  record.activePrompt = undefined;
2739
2893
  if (!record.backgroundBusy) record.busy = false;
2740
2894
  clearPromptWatchdog(waiter);
@@ -3001,11 +3155,7 @@ export class AcpAgent implements Agent {
3001
3155
  this.#pendingRouterFrames.set(id, bufferedFrames);
3002
3156
  try {
3003
3157
  await this.#ensureRouterReady();
3004
- const attachment = lifecycleResult
3005
- ? await this.#router.adoptLifecycleResult(lifecycleResult, { sessionId: id, cwd })
3006
- : this.#router.attachment(id);
3007
- if (!attachment)
3008
- throw new AcpSdkAdapterError("unavailable", `ACP session ${id} has no current Router attachment.`);
3158
+ if (lifecycleResult) await this.#router.adoptLifecycleResult(lifecycleResult, { sessionId: id, cwd });
3009
3159
  let currentAttachment = this.#router.attachment(id);
3010
3160
  for (let attempt = 0; !currentAttachment && attempt < 40; attempt++) {
3011
3161
  await Bun.sleep(50);
@@ -3013,7 +3163,7 @@ export class AcpAgent implements Agent {
3013
3163
  currentAttachment = this.#router.attachment(id);
3014
3164
  }
3015
3165
  if (!currentAttachment)
3016
- throw new AcpSdkAdapterError("unavailable", `ACP session ${id} lost exact Router authority.`);
3166
+ throw new AcpSdkAdapterError("unavailable", `ACP session ${id} has no current Router attachment.`);
3017
3167
  adapter = new AcpSdkAdapter({
3018
3168
  router: this.#router,
3019
3169
  attachment: currentAttachment,
@@ -3054,6 +3204,7 @@ export class AcpAgent implements Agent {
3054
3204
  const exactAttachment = this.#router.attachment(id) ?? currentAttachment;
3055
3205
  if (!exactAttachment.isCurrent())
3056
3206
  throw new AcpSdkAdapterError("unavailable", `ACP session ${id} lost exact Router authority.`);
3207
+ const retainedAbortOwner = this.#uncertainAbortOwners.get(id);
3057
3208
  const record: SessionRecord = {
3058
3209
  cwd,
3059
3210
  adapter,
@@ -3067,11 +3218,12 @@ export class AcpAgent implements Agent {
3067
3218
  settledPromptCorrelations: this.#retiredPromptCorrelations.get(id) ?? [],
3068
3219
  inboundSequence: 0,
3069
3220
  connectionId: adapter.connectionId,
3070
- busy: false,
3071
- backgroundBusy: false,
3221
+ busy: retainedAbortOwner !== undefined,
3222
+ backgroundBusy: retainedAbortOwner !== undefined,
3072
3223
  backgroundAnonymousCount: 0,
3073
- backgroundCorrelations: [],
3224
+ backgroundCorrelations: retainedAbortOwner?.correlation ? [retainedAbortOwner.correlation] : [],
3074
3225
  toolArgs: new Map(),
3226
+ uncertainAbortOwner: retainedAbortOwner,
3075
3227
  };
3076
3228
  // Preserve first-turn state across reattachment (issue #5574). The startup-readiness
3077
3229
  // retry only guards a session's very first logical prompt; a fresh record built on
@@ -3108,9 +3260,11 @@ export class AcpAgent implements Agent {
3108
3260
  try {
3109
3261
  await this.#teardownSession(id, "attachment failed", false);
3110
3262
  } finally {
3111
- this.#knownSessionCwds.delete(id);
3263
+ if (!this.#uncertainAbortOwners.has(id)) {
3264
+ this.#knownSessionCwds.delete(id);
3265
+ this.#knownSessionMcpServers.delete(id);
3266
+ }
3112
3267
  this.#ownedSessionIds.delete(id);
3113
- this.#knownSessionMcpServers.delete(id);
3114
3268
  }
3115
3269
  } else if (adapter) {
3116
3270
  try {
@@ -3162,6 +3316,181 @@ export class AcpAgent implements Agent {
3162
3316
  if (this.#retiredPromptAcknowledgements.get(id) === waiter) this.#retiredPromptAcknowledgements.delete(id);
3163
3317
  }
3164
3318
 
3319
+ #retainUncertainAbortOwner(id: string, record: SessionRecord, waiter: PromptWaiter): void {
3320
+ let owner: UncertainAbortOwner = {
3321
+ kind: waiter.invocationKind,
3322
+ clientRef: waiter.clientRef,
3323
+ };
3324
+ if (hasCompleteCorrelation(waiter.correlation))
3325
+ owner = {
3326
+ ...owner,
3327
+ correlation: { commandId: waiter.correlation.commandId, turnId: waiter.correlation.turnId },
3328
+ };
3329
+ const retained = this.#uncertainAbortOwners.get(id);
3330
+ if (
3331
+ retained &&
3332
+ (retained.kind !== owner.kind ||
3333
+ retained.clientRef !== owner.clientRef ||
3334
+ (retained.correlation !== undefined &&
3335
+ owner.correlation !== undefined &&
3336
+ !correlationsExactlyMatch(retained.correlation, owner.correlation)))
3337
+ ) {
3338
+ record.uncertainAbortOwner = retained;
3339
+ return;
3340
+ }
3341
+ if (retained?.correlation && !owner.correlation) owner = retained;
3342
+ this.#uncertainAbortOwners.set(id, owner);
3343
+ record.uncertainAbortOwner = owner;
3344
+ }
3345
+
3346
+ #bindUncertainAbortOwner(id: string, waiter: PromptWaiter, correlation: PromptCorrelation): void {
3347
+ if (!hasCompleteCorrelation(correlation)) return;
3348
+ const retained = this.#uncertainAbortOwners.get(id);
3349
+ if (!retained || retained.kind !== waiter.invocationKind || retained.clientRef !== waiter.clientRef) return;
3350
+ if (retained.correlation && !correlationsExactlyMatch(retained.correlation, correlation)) return;
3351
+ const boundCorrelation = { commandId: correlation.commandId, turnId: correlation.turnId };
3352
+ const owner: UncertainAbortOwner = {
3353
+ kind: retained.kind,
3354
+ clientRef: retained.clientRef,
3355
+ correlation: boundCorrelation,
3356
+ };
3357
+ this.#uncertainAbortOwners.set(id, owner);
3358
+ const current = this.#sessions.get(id);
3359
+ if (!current) return;
3360
+ current.uncertainAbortOwner = owner;
3361
+ if (current.activePrompt !== waiter || waiter.settled) {
3362
+ if (!current.backgroundCorrelations.some(background => correlationsExactlyMatch(background, correlation)))
3363
+ current.backgroundCorrelations.push(boundCorrelation);
3364
+ current.backgroundBusy = true;
3365
+ current.busy = true;
3366
+ }
3367
+ }
3368
+
3369
+ async #recoverUncertainAbortAfterLateAcknowledgement(
3370
+ id: string,
3371
+ adapter: AcpSdkAdapter,
3372
+ owner: UncertainAbortOwner,
3373
+ ): Promise<void> {
3374
+ const correlation = owner.correlation;
3375
+ if (!correlation) return;
3376
+ let timer: NodeJS.Timeout | undefined;
3377
+ let timedOut = false;
3378
+ try {
3379
+ const deadline = Promise.withResolvers<never>();
3380
+ timer = setTimeout(() => {
3381
+ timedOut = true;
3382
+ deadline.reject(new Error("status query timeout"));
3383
+ }, 5_000);
3384
+ const response = object(
3385
+ await Promise.race([
3386
+ adapter.query("turn.result", {
3387
+ kind: owner.kind,
3388
+ commandId: correlation.commandId,
3389
+ turnId: correlation.turnId,
3390
+ }),
3391
+ deadline.promise,
3392
+ ]),
3393
+ );
3394
+ const status = object(response?.result) ?? response;
3395
+ const reportedCorrelation = strictCorrelationFrom(response, status);
3396
+ const outcome = status ? terminalOutcome(status) : undefined;
3397
+ const failure = object(status?.error);
3398
+ const conflictingIdentity = [response, status].some(
3399
+ value =>
3400
+ (value?.kind !== undefined && value.kind !== owner.kind) ||
3401
+ (value?.sessionId !== undefined && value.sessionId !== id) ||
3402
+ (value?.clientRef !== undefined && value.clientRef !== owner.clientRef),
3403
+ );
3404
+ const failedReceipt =
3405
+ status?.status === "failed" &&
3406
+ ((outcome?.kind === "failed" &&
3407
+ (status.receiptState === "present" ||
3408
+ status.receiptState === "missing" ||
3409
+ status.receiptState === "unknown")) ||
3410
+ (outcome === undefined &&
3411
+ (status.receiptState === "present" ||
3412
+ status.receiptState === "missing" ||
3413
+ status.receiptState === "unknown") &&
3414
+ typeof failure?.code === "string" &&
3415
+ /^[a-zA-Z0-9_.-]{1,64}$/.test(failure.code) &&
3416
+ typeof failure.message === "string" &&
3417
+ failure.message.trim().length > 0 &&
3418
+ failure.message.length <= 512));
3419
+ const terminal =
3420
+ (status?.status === "terminal_ok" &&
3421
+ outcome?.kind === "stopped" &&
3422
+ (status.receiptState === "present" || status.receiptState === "missing")) ||
3423
+ failedReceipt;
3424
+ // The original prompt has already settled as terminal_uncertain on this
3425
+ // detached follow-up path. Missing receipt remains a failure classification;
3426
+ // this proof retires only the session ownership fence, never the old result
3427
+ // as a successful stop reason.
3428
+ if (
3429
+ !terminal ||
3430
+ conflictingIdentity ||
3431
+ status?.kind !== owner.kind ||
3432
+ !reportedCorrelation ||
3433
+ !hasCompleteCorrelation(reportedCorrelation) ||
3434
+ !correlationsExactlyMatch(correlation, reportedCorrelation)
3435
+ )
3436
+ return;
3437
+ const record = this.#sessions.get(id);
3438
+ const retained = this.#uncertainAbortOwners.get(id);
3439
+ if (
3440
+ !record ||
3441
+ record.adapter !== adapter ||
3442
+ retained?.kind !== owner.kind ||
3443
+ retained.clientRef !== owner.clientRef ||
3444
+ !retained.correlation ||
3445
+ !correlationsExactlyMatch(retained.correlation, correlation)
3446
+ )
3447
+ return;
3448
+ this.#clearUncertainAbortOwner(id, record, owner.kind, correlation);
3449
+ record.backgroundCorrelations = record.backgroundCorrelations.filter(
3450
+ background => !correlationsExactlyMatch(background, correlation),
3451
+ );
3452
+ record.backgroundBusy = record.backgroundAnonymousCount > 0 || record.backgroundCorrelations.length > 0;
3453
+ record.busy = record.backgroundBusy;
3454
+ void this.#publishPromptPhaseIdle(id, adapter);
3455
+ } catch {
3456
+ logger.warn("acp_uncertain_abort_late_ack_lookup_unresolved", {
3457
+ sessionId: id,
3458
+ reason: timedOut ? "timeout" : "unavailable",
3459
+ });
3460
+ } finally {
3461
+ if (timer !== undefined) clearTimeout(timer);
3462
+ }
3463
+ }
3464
+
3465
+ #clearUncertainAbortOwner(
3466
+ id: string,
3467
+ record: SessionRecord,
3468
+ kind: PromptWaiter["invocationKind"],
3469
+ correlation: PromptCorrelation,
3470
+ ): void {
3471
+ const owner = this.#uncertainAbortOwners.get(id);
3472
+ if (owner?.kind === kind && owner.correlation && correlationsExactlyMatch(owner.correlation, correlation))
3473
+ this.#uncertainAbortOwners.delete(id);
3474
+ const recordOwner = record.uncertainAbortOwner;
3475
+ if (
3476
+ recordOwner?.kind === kind &&
3477
+ recordOwner.correlation &&
3478
+ correlationsExactlyMatch(recordOwner.correlation, correlation)
3479
+ )
3480
+ record.uncertainAbortOwner = undefined;
3481
+ }
3482
+
3483
+ #ownsUncertainAbort(record: SessionRecord, waiter: PromptWaiter): boolean {
3484
+ const owner = record.uncertainAbortOwner;
3485
+ return (
3486
+ owner?.kind === waiter.invocationKind &&
3487
+ owner.clientRef === waiter.clientRef &&
3488
+ (!owner.correlation ||
3489
+ !hasCompleteCorrelation(waiter.correlation) ||
3490
+ correlationsExactlyMatch(owner.correlation, waiter.correlation))
3491
+ );
3492
+ }
3493
+
3165
3494
  #advanceTerminalGeneration(record: SessionRecord): void {
3166
3495
  record.publicationGeneration++;
3167
3496
  }
@@ -3170,9 +3499,17 @@ export class AcpAgent implements Agent {
3170
3499
  const record = this.#sessions.get(id);
3171
3500
  if (!record || record.adapter !== adapter) return;
3172
3501
  if (error instanceof SdkClientError && error.code === "uncertain_after_send") {
3502
+ const details = object(error.details);
3503
+ const abortWaiter = record.activePrompt;
3504
+ if (details?.operation === "turn.abort" && abortWaiter?.dispatched && !abortWaiter.settled) {
3505
+ if (abortWaiter.terminalReserved) return;
3506
+ this.#retainUncertainAbortOwner(id, record, abortWaiter);
3507
+ if (record.statusRecovery !== abortWaiter) this.#startUncertainPromptRecovery(id, record, abortWaiter);
3508
+ return;
3509
+ }
3173
3510
  // Provider-registration requests share this transport error. Recover only when the
3174
- // prompt's own dispatched acknowledgement is still pending; an acknowledged turn
3175
- // must continue until its terminal frame arrives.
3511
+ // prompt's own dispatched acknowledgement is still pending; ordinary acknowledged
3512
+ // turns continue until their terminal frame arrives.
3176
3513
  const waiter = record.activePrompt;
3177
3514
  if (waiter?.dispatched && waiter.acknowledgementPending && !waiter.acknowledged)
3178
3515
  this.#startUncertainPromptRecovery(id, record, waiter);
@@ -3193,6 +3530,7 @@ export class AcpAgent implements Agent {
3193
3530
  return;
3194
3531
  }
3195
3532
  const waiter = record.activePrompt;
3533
+ if (waiter?.terminalReserved && this.#ownsUncertainAbort(record, waiter)) return;
3196
3534
  if (waiter?.dispatched) {
3197
3535
  // A prior uncertainty notification already owns recovery for this waiter. Keep
3198
3536
  // the in-flight lookup alive; only declined, non-recovering waiters fall through
@@ -3215,7 +3553,14 @@ export class AcpAgent implements Agent {
3215
3553
  return false;
3216
3554
  record.statusRecovery = waiter;
3217
3555
  clearPromptWatchdog(waiter);
3218
- void this.#recoverUncertainPrompt(id, record, record.adapter, waiter);
3556
+ const recovery = this.#recoverUncertainPrompt(id, record, record.adapter, waiter);
3557
+ record.statusRecoveryTask = recovery;
3558
+ const clearRecovery = (): void => {
3559
+ if (record.statusRecovery !== waiter) return;
3560
+ record.statusRecovery = undefined;
3561
+ record.statusRecoveryTask = undefined;
3562
+ };
3563
+ void recovery.then(clearRecovery, clearRecovery);
3219
3564
  return true;
3220
3565
  }
3221
3566
 
@@ -3224,7 +3569,7 @@ export class AcpAgent implements Agent {
3224
3569
  record: SessionRecord,
3225
3570
  adapter: AcpSdkAdapter,
3226
3571
  waiter: PromptWaiter,
3227
- ): Promise<void> {
3572
+ ): Promise<UncertainPromptRecoveryResult> {
3228
3573
  const ownsRecovery = (): boolean =>
3229
3574
  this.#sessions.get(id) === record &&
3230
3575
  record.adapter === adapter &&
@@ -3262,7 +3607,7 @@ export class AcpAgent implements Agent {
3262
3607
  }
3263
3608
  };
3264
3609
  await Promise.race([refreshAttachment(), deadline.promise]);
3265
- if (!ownsRecovery()) return;
3610
+ if (!ownsRecovery()) return { kind: "ownership_lost" };
3266
3611
  // Bound observation only: the mutation and the read are never replayed or aborted.
3267
3612
  const response = object(
3268
3613
  await Promise.race([
@@ -3273,7 +3618,7 @@ export class AcpAgent implements Agent {
3273
3618
  deadline.promise,
3274
3619
  ]),
3275
3620
  );
3276
- if (!ownsRecovery()) return;
3621
+ if (!ownsRecovery()) return { kind: "ownership_lost" };
3277
3622
  const status = object(response?.result) ?? response;
3278
3623
  const correlation = strictCorrelationFrom(response, status);
3279
3624
  const outcome = status ? terminalOutcome(status) : undefined;
@@ -3314,10 +3659,35 @@ export class AcpAgent implements Agent {
3314
3659
  ) {
3315
3660
  waiter.correlation = correlation;
3316
3661
  waiter.acknowledged = true;
3662
+ this.#bindUncertainAbortOwner(id, waiter, correlation);
3663
+ this.#clearUncertainAbortOwner(id, record, waiter.invocationKind, correlation);
3317
3664
  record.busy = record.backgroundBusy;
3318
3665
  this.#advanceTerminalGeneration(record);
3319
3666
  void this.#rejectPrompt(record, id, waiter, new AcpSdkAdapterError(failure.code, failure.message));
3320
- return;
3667
+ return { kind: "terminal" };
3668
+ } else if (
3669
+ this.#ownsUncertainAbort(record, waiter) &&
3670
+ status.status === "terminal_ok" &&
3671
+ outcome?.kind === "stopped" &&
3672
+ outcome.reason === "end_turn" &&
3673
+ status.receiptState === "missing"
3674
+ ) {
3675
+ waiter.correlation = correlation;
3676
+ waiter.acknowledged = true;
3677
+ this.#bindUncertainAbortOwner(id, waiter, correlation);
3678
+ this.#clearUncertainAbortOwner(id, record, waiter.invocationKind, correlation);
3679
+ record.busy = record.backgroundBusy;
3680
+ this.#advanceTerminalGeneration(record);
3681
+ void this.#rejectPrompt(
3682
+ record,
3683
+ id,
3684
+ waiter,
3685
+ new AcpSdkAdapterError(
3686
+ "prompt_failed",
3687
+ "Runtime completed without reportable final response text or artifact_path.",
3688
+ ),
3689
+ );
3690
+ return { kind: "terminal" };
3321
3691
  } else if (
3322
3692
  (status.status === "terminal_ok" &&
3323
3693
  outcome?.kind === "stopped" &&
@@ -3331,6 +3701,8 @@ export class AcpAgent implements Agent {
3331
3701
  ) {
3332
3702
  waiter.correlation = correlation;
3333
3703
  waiter.acknowledged = true;
3704
+ this.#bindUncertainAbortOwner(id, waiter, correlation);
3705
+ this.#clearUncertainAbortOwner(id, record, waiter.invocationKind, correlation);
3334
3706
  record.busy = record.backgroundBusy;
3335
3707
  this.#advanceTerminalGeneration(record);
3336
3708
  const settledOutcome =
@@ -3357,8 +3729,43 @@ export class AcpAgent implements Agent {
3357
3729
  waiter,
3358
3730
  publicationBarrier,
3359
3731
  );
3360
- return;
3732
+ return { kind: "terminal" };
3361
3733
  } else {
3734
+ if (!acknowledgedAtLookup) {
3735
+ waiter.correlation = correlation;
3736
+ waiter.acknowledged = true;
3737
+ this.#bindUncertainAbortOwner(id, waiter, correlation);
3738
+ const deferred = waiter.deferredFrames.find(({ frame }) => {
3739
+ const candidate = receivedSdkEvent(frame)?.event;
3740
+ if (!candidate) return false;
3741
+ const candidateOutcome = terminalOutcome(candidate);
3742
+ const validTerminal =
3743
+ (candidate.type === "agent_end" && candidateOutcome !== undefined) ||
3744
+ (candidate.type === "agent_failed" && candidateOutcome?.kind === "failed");
3745
+ const candidateCorrelation = sdkFrameCorrelation(frame, candidate);
3746
+ return (
3747
+ validTerminal &&
3748
+ sdkFrameBelongsToSession(frame, candidate, id) &&
3749
+ candidateCorrelation !== undefined &&
3750
+ correlationsExactlyMatch(candidateCorrelation, correlation)
3751
+ );
3752
+ });
3753
+ if (deferred) {
3754
+ waiter.deferredFrames.length = 0;
3755
+ waiter.terminalReserved = true;
3756
+ clearPromptWatchdog(waiter);
3757
+ await record.frameTail;
3758
+ if (
3759
+ this.#sessions.get(id) !== record ||
3760
+ record.adapter !== adapter ||
3761
+ record.activePrompt !== waiter ||
3762
+ waiter.settled
3763
+ )
3764
+ return { kind: "ownership_lost" };
3765
+ await this.#handleSdkFrame(id, adapter, deferred.frame, record.publicationGeneration);
3766
+ return { kind: "terminal" };
3767
+ }
3768
+ }
3362
3769
  detail = "status has no usable retained terminal outcome and receipt";
3363
3770
  }
3364
3771
  } catch {
@@ -3366,15 +3773,30 @@ export class AcpAgent implements Agent {
3366
3773
  } finally {
3367
3774
  if (timer !== undefined) clearTimeout(timer);
3368
3775
  }
3369
- if (!ownsRecovery()) return;
3370
- await this.#failSession(
3371
- id,
3372
- adapter,
3373
- new AcpSdkAdapterError(
3374
- "terminal_uncertain",
3375
- `ACP prompt outcome remains uncertain: ${detail}. No mutation was replayed. Inspect turn.result for kind=${waiter.invocationKind}, clientRef=${waiter.clientRef}, commandId=${waiter.correlation.commandId ?? "unknown"}, turnId=${waiter.correlation.turnId ?? "unknown"} before submitting more work.`,
3376
- ),
3776
+ if (!ownsRecovery()) return { kind: "ownership_lost" };
3777
+ const uncertain = new AcpSdkAdapterError(
3778
+ "terminal_uncertain",
3779
+ `ACP prompt outcome remains uncertain: ${detail}. No mutation was replayed. Inspect turn.result for kind=${waiter.invocationKind}, clientRef=${waiter.clientRef}, commandId=${waiter.correlation.commandId ?? "unknown"}, turnId=${waiter.correlation.turnId ?? "unknown"} before submitting more work.`,
3377
3780
  );
3781
+ const uncertainAbortOwner = this.#uncertainAbortOwners.get(id);
3782
+ if (uncertainAbortOwner?.kind === waiter.invocationKind && uncertainAbortOwner.clientRef === waiter.clientRef) {
3783
+ record.uncertainAbortOwner = uncertainAbortOwner;
3784
+ const ownerCorrelation = uncertainAbortOwner.correlation;
3785
+ if (
3786
+ ownerCorrelation &&
3787
+ !record.backgroundCorrelations.some(owner => correlationsExactlyMatch(owner, ownerCorrelation))
3788
+ )
3789
+ record.backgroundCorrelations.push(ownerCorrelation);
3790
+ record.backgroundBusy = true;
3791
+ record.busy = true;
3792
+ record.cancelRequested = false;
3793
+ record.statusRecovery = undefined;
3794
+ record.statusRecoveryTask = undefined;
3795
+ await this.#rejectPrompt(record, id, waiter, uncertain);
3796
+ return { kind: "uncertain", error: uncertain };
3797
+ }
3798
+ await this.#failSession(id, adapter, uncertain);
3799
+ return { kind: "uncertain", error: uncertain };
3378
3800
  }
3379
3801
 
3380
3802
  async #recoverSessionAfterTransportFailureAsync(
@@ -3520,7 +3942,10 @@ export class AcpAgent implements Agent {
3520
3942
  .join("; ");
3521
3943
  throw aggregateAcpFailure("terminal_uncertain", `ACP session cleanup is uncertain: ${detail}`, failures);
3522
3944
  }
3523
- if (closeRemote) this.#pendingCloseIdempotencyKeys.delete(id);
3945
+ if (closeRemote) {
3946
+ this.#pendingCloseIdempotencyKeys.delete(id);
3947
+ this.#uncertainAbortOwners.delete(id);
3948
+ }
3524
3949
  } finally {
3525
3950
  this.#finishTeardown(id);
3526
3951
  }
@@ -3693,6 +4118,7 @@ export class AcpAgent implements Agent {
3693
4118
  const waiter = record.activePrompt;
3694
4119
  if (!waiter || waiter.settled || waiter.terminalReserved) return;
3695
4120
  const event = receivedSdkEvent(frame)?.event;
4121
+ if (event && !sdkFrameBelongsToSession(frame, event, id)) return;
3696
4122
  const correlation = watchdogCorrelationFrom(frame, event);
3697
4123
  if (!waiter.acknowledged) {
3698
4124
  if (hasCorrelation(correlation)) waiter.deferredActivityFrames.push(frame);
@@ -3787,6 +4213,20 @@ export class AcpAgent implements Agent {
3787
4213
  #enqueueSdkFrame(id: string, adapter: AcpSdkAdapter, frame: JsonObject): void {
3788
4214
  const record = this.#sessions.get(id);
3789
4215
  if (!record || record.adapter !== adapter) return;
4216
+ const received = receivedSdkEvent(frame);
4217
+ const ingressEvent = received?.event;
4218
+ if (!sdkFrameBelongsToSession(frame, ingressEvent, id)) {
4219
+ if (ingressEvent?.type === "agent_end" || ingressEvent?.type === "agent_failed")
4220
+ logDroppedPromptTerminal(
4221
+ id,
4222
+ ingressEvent,
4223
+ "session_mismatch",
4224
+ sdkFrameCorrelation(frame, ingressEvent) ?? {},
4225
+ record.activePrompt?.correlation,
4226
+ );
4227
+ else logger.warn(`ACP session ${id} dropped an event from a foreign session identity.`);
4228
+ return;
4229
+ }
3790
4230
  if (frame.type !== "hello" && frame.type !== "server_hello" && typeof frame.connectionId === "string")
3791
4231
  record.connectionId = frame.connectionId;
3792
4232
  // Ingress ordering is recorded before queued work begins.
@@ -3794,8 +4234,6 @@ export class AcpAgent implements Agent {
3794
4234
  // Correlation is checked at ingress before a prompt-owned frame may refresh the
3795
4235
  // watchdog, so queued processing cannot turn unrelated host traffic into turn liveness.
3796
4236
  this.#refreshPromptWatchdog(id, record, frame);
3797
- const received = receivedSdkEvent(frame);
3798
- const ingressEvent = received?.event;
3799
4237
  const ingressOutcome =
3800
4238
  ingressEvent?.type === "agent_end" || ingressEvent?.type === "agent_failed"
3801
4239
  ? terminalOutcome(ingressEvent)
@@ -3846,6 +4284,7 @@ export class AcpAgent implements Agent {
3846
4284
  return;
3847
4285
  }
3848
4286
  if (waiter.terminalReserved) return;
4287
+ if (record.statusRecovery === waiter && this.#ownsUncertainAbort(record, waiter)) return;
3849
4288
  if (record.statusRecovery === waiter || (waiter.dispatched && !waiter.acknowledged)) {
3850
4289
  await this.#failSession(
3851
4290
  id,
@@ -3900,14 +4339,75 @@ export class AcpAgent implements Agent {
3900
4339
  const ownsBackgroundTerminal =
3901
4340
  ownedBackgroundCorrelation !== undefined ||
3902
4341
  (!hasCorrelation(correlation) && record.backgroundAnonymousCount > 0);
4342
+ const provisionalAbortOwner = this.#uncertainAbortOwners.get(id);
4343
+ const retiredPromptAcknowledgement = this.#retiredPromptAcknowledgements.get(id);
4344
+ const validTerminalProof =
4345
+ (event.type === "agent_end" && outcome !== undefined) ||
4346
+ (event.type === "agent_failed" && outcome?.kind === "failed");
4347
+ if (isTerminal && !sdkFrameBelongsToSession(frame, event, id)) {
4348
+ logDroppedPromptTerminal(id, event, "session_mismatch", correlation, activePrompt?.correlation);
4349
+ return;
4350
+ }
4351
+ if (
4352
+ isTerminal &&
4353
+ !activePrompt &&
4354
+ provisionalAbortOwner &&
4355
+ !provisionalAbortOwner.correlation &&
4356
+ retiredPromptAcknowledgement?.settled &&
4357
+ retiredPromptAcknowledgement.acknowledgementPending &&
4358
+ !retiredPromptAcknowledgement.acknowledged &&
4359
+ provisionalAbortOwner.kind === retiredPromptAcknowledgement.invocationKind &&
4360
+ provisionalAbortOwner.clientRef === retiredPromptAcknowledgement.clientRef &&
4361
+ validTerminalProof &&
4362
+ hasCompleteCorrelation(correlation) &&
4363
+ sdkFrameBelongsToSession(frame, event, id) &&
4364
+ !this.#hasRetiredPromptCorrelation(id, record, correlation)
4365
+ ) {
4366
+ // Query-by-clientRef has not yet yielded the command/turn identity. Retain
4367
+ // a bounded set of distinct candidates without publishing them until the
4368
+ // delayed prompt acknowledgement proves which exact correlation belongs.
4369
+ const duplicateCandidate = retiredPromptAcknowledgement.deferredFrames.some(({ frame: candidateFrame }) => {
4370
+ const candidateEvent = receivedSdkEvent(candidateFrame)?.event;
4371
+ const candidateCorrelation = candidateEvent
4372
+ ? sdkFrameCorrelation(candidateFrame, candidateEvent)
4373
+ : undefined;
4374
+ return candidateCorrelation !== undefined && correlationsExactlyMatch(candidateCorrelation, correlation);
4375
+ });
4376
+ if (
4377
+ !duplicateCandidate &&
4378
+ retiredPromptAcknowledgement.deferredFrames.length < PROVISIONAL_ABORT_TERMINAL_CANDIDATE_RETENTION
4379
+ )
4380
+ retiredPromptAcknowledgement.deferredFrames.push({ frame, publicationGeneration });
4381
+ return;
4382
+ }
3903
4383
  if (isTerminal) {
3904
4384
  if (!activePrompt) {
3905
- if (!record.backgroundBusy || !ownsBackgroundTerminal || settledCorrelation) return;
4385
+ if (
4386
+ !record.backgroundBusy ||
4387
+ !ownsBackgroundTerminal ||
4388
+ (settledCorrelation && !ownedBackgroundCorrelation)
4389
+ )
4390
+ return;
4391
+ const uncertainAbortOwner = this.#uncertainAbortOwners.get(id);
4392
+ if (
4393
+ uncertainAbortOwner?.correlation &&
4394
+ ownedBackgroundCorrelation &&
4395
+ correlationsExactlyMatch(uncertainAbortOwner.correlation, ownedBackgroundCorrelation) &&
4396
+ !validTerminalProof
4397
+ )
4398
+ return;
3906
4399
  if (ownedBackgroundCorrelation)
3907
4400
  record.backgroundCorrelations = record.backgroundCorrelations.filter(
3908
4401
  owner => !correlationsExactlyMatch(owner, ownedBackgroundCorrelation),
3909
4402
  );
3910
4403
  else record.backgroundAnonymousCount--;
4404
+ if (
4405
+ uncertainAbortOwner?.correlation &&
4406
+ ownedBackgroundCorrelation &&
4407
+ validTerminalProof &&
4408
+ correlationsExactlyMatch(uncertainAbortOwner.correlation, ownedBackgroundCorrelation)
4409
+ )
4410
+ this.#clearUncertainAbortOwner(id, record, uncertainAbortOwner.kind, ownedBackgroundCorrelation);
3911
4411
  record.backgroundBusy = record.backgroundAnonymousCount > 0 || record.backgroundCorrelations.length > 0;
3912
4412
  record.busy = record.backgroundBusy;
3913
4413
  await this.#publishPromptPhase(id, record.adapter, undefined);
@@ -3956,6 +4456,18 @@ export class AcpAgent implements Agent {
3956
4456
  this.#advanceTerminalGeneration(record);
3957
4457
  publicationGeneration = record.publicationGeneration;
3958
4458
  if (!outcome) {
4459
+ const uncertainAbortOwner = this.#uncertainAbortOwners.get(id);
4460
+ if (
4461
+ uncertainAbortOwner?.kind === activePrompt.invocationKind &&
4462
+ uncertainAbortOwner.correlation &&
4463
+ correlationsExactlyMatch(uncertainAbortOwner.correlation, correlation)
4464
+ ) {
4465
+ record.uncertainAbortOwner = uncertainAbortOwner;
4466
+ if (!record.backgroundCorrelations.some(owner => correlationsExactlyMatch(owner, correlation)))
4467
+ record.backgroundCorrelations.push(uncertainAbortOwner.correlation);
4468
+ record.backgroundBusy = true;
4469
+ record.busy = true;
4470
+ }
3959
4471
  await this.#rejectPrompt(
3960
4472
  record,
3961
4473
  id,
@@ -3964,6 +4476,13 @@ export class AcpAgent implements Agent {
3964
4476
  );
3965
4477
  return;
3966
4478
  }
4479
+ const uncertainAbortOwner = this.#uncertainAbortOwners.get(id);
4480
+ if (
4481
+ uncertainAbortOwner?.kind === activePrompt.invocationKind &&
4482
+ uncertainAbortOwner.correlation &&
4483
+ correlationsExactlyMatch(uncertainAbortOwner.correlation, correlation)
4484
+ )
4485
+ this.#clearUncertainAbortOwner(id, record, uncertainAbortOwner.kind, correlation);
3967
4486
  // When a client cancel has been requested, a trailing stopped terminal may
3968
4487
  // still carry the normal `end_turn` reason (the model finished its response as
3969
4488
  // the cancel arrived mid-stream, or the cancel was processed before the prompt
@@ -4238,7 +4757,32 @@ export class AcpAgent implements Agent {
4238
4757
  clearPromptWatchdog(waiter);
4239
4758
  waiter.settled = true;
4240
4759
  this.#fenceRetiredPromptAcknowledgement(id, waiter);
4760
+ const abortOwner = this.#uncertainAbortOwners.get(id);
4761
+ const preserveAbortTerminal =
4762
+ abortOwner?.kind === waiter.invocationKind &&
4763
+ abortOwner.clientRef === waiter.clientRef &&
4764
+ abortOwner.correlation === undefined &&
4765
+ waiter.acknowledgementPending &&
4766
+ !waiter.acknowledged;
4767
+ const candidateTerminal = preserveAbortTerminal
4768
+ ? waiter.deferredFrames.find(({ frame }) => {
4769
+ const candidate = receivedSdkEvent(frame)?.event;
4770
+ if (!candidate) return false;
4771
+ const candidateOutcome = terminalOutcome(candidate);
4772
+ const validTerminal =
4773
+ (candidate.type === "agent_end" && candidateOutcome !== undefined) ||
4774
+ (candidate.type === "agent_failed" && candidateOutcome?.kind === "failed");
4775
+ const candidateCorrelation = sdkFrameCorrelation(frame, candidate);
4776
+ return (
4777
+ validTerminal &&
4778
+ sdkFrameBelongsToSession(frame, candidate, id) &&
4779
+ hasCompleteCorrelation(candidateCorrelation ?? {}) &&
4780
+ !this.#hasRetiredPromptCorrelation(id, record, candidateCorrelation ?? {})
4781
+ );
4782
+ })
4783
+ : undefined;
4241
4784
  waiter.deferredFrames.length = 0;
4785
+ if (candidateTerminal) waiter.deferredFrames.push(candidateTerminal);
4242
4786
  waiter.deferredActivityFrames.length = 0;
4243
4787
  waiter.terminal = undefined;
4244
4788
  this.#rememberSettledPromptCorrelation(id, record, waiter.correlation);