@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.
- package/CHANGELOG.md +73 -0
- package/dist/types/config/autorouting-tier-map.d.ts +1031 -3
- package/dist/types/config/file-lock.d.ts +4 -2
- package/dist/types/config/model-registry.d.ts +3 -1
- package/dist/types/config/model-resolver.d.ts +1 -1
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +11 -2
- package/dist/types/gjc-runtime/state-writer.d.ts +14 -1
- package/dist/types/harness-control-plane/control-endpoint.d.ts +4 -1
- package/dist/types/harness-control-plane/finalize.d.ts +14 -3
- package/dist/types/main.d.ts +1 -1
- package/dist/types/runtime-mcp/loader.d.ts +2 -0
- package/dist/types/runtime-mcp/manager.d.ts +13 -6
- package/dist/types/sdk/broker/broker.d.ts +4 -2
- package/dist/types/sdk/broker/ensure.d.ts +3 -0
- package/dist/types/sdk/broker/identity.d.ts +2 -0
- package/dist/types/sdk/broker/managed-task-dag.d.ts +1335 -0
- package/dist/types/sdk/broker/managed-task-verification.d.ts +37 -0
- package/dist/types/sdk/broker/spawn-authority.d.ts +8 -0
- package/dist/types/sdk/broker/startup-failure.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +5 -1
- package/dist/types/sdk/prompt-deadline-flush.d.ts +2 -0
- package/dist/types/sdk/session.d.ts +3 -3
- package/dist/types/session/agent-session.d.ts +7 -2
- package/dist/types/task/types.d.ts +2 -0
- package/dist/types/tools/bash-interactive.d.ts +1 -0
- package/dist/types/tools/fetch.d.ts +10 -1
- package/dist/types/tools/subagent.d.ts +8 -1
- package/package.json +7 -7
- package/src/cli/public-command-registry.ts +8 -2
- package/src/cli/setup-cli.ts +1 -0
- package/src/cli/stats-cli.ts +11 -1
- package/src/config/autorouting-tier-map.ts +529 -3
- package/src/config/file-lock.ts +59 -14
- package/src/config/model-discovery-manager.ts +4 -4
- package/src/config/model-profile-activation.ts +83 -23
- package/src/config/model-registry.ts +72 -12
- package/src/config/model-resolver.ts +6 -1
- package/src/coordinator-mcp/question-state.ts +4 -1
- package/src/coordinator-mcp/server.ts +14 -6
- package/src/exec/bash-executor.ts +3 -0
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +46 -8
- package/src/gjc-runtime/state-writer.ts +317 -6
- package/src/harness-control-plane/control-endpoint.ts +24 -6
- package/src/harness-control-plane/finalize.ts +312 -25
- package/src/harness-control-plane/owner.ts +323 -89
- package/src/internal-urls/docs-index.generated.ts +6 -4
- package/src/main.ts +69 -19
- package/src/modes/acp/acp-agent.ts +581 -37
- package/src/modes/components/gjc-bundle-settings.ts +21 -1
- package/src/modes/components/tool-status-header.ts +72 -6
- package/src/modes/controllers/runtime-mcp-command-controller.ts +1 -1
- package/src/runtime-mcp/loader.ts +6 -3
- package/src/runtime-mcp/manager.ts +177 -75
- package/src/sdk/broker/broker.ts +1057 -150
- package/src/sdk/broker/ensure.ts +21 -3
- package/src/sdk/broker/identity.ts +13 -0
- package/src/sdk/broker/managed-task-dag.ts +1927 -0
- package/src/sdk/broker/managed-task-verification.ts +642 -0
- package/src/sdk/broker/spawn-authority.ts +140 -7
- package/src/sdk/broker/startup-failure.ts +80 -1
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/index.ts +22 -6
- package/src/sdk/bus/telegram-daemon-contract.ts +5 -1
- package/src/sdk/cli/session-cli.ts +18 -0
- package/src/sdk/host/session-runtime.test.ts +46 -0
- package/src/sdk/host/session-runtime.ts +13 -6
- package/src/sdk/lifecycle/service.ts +2 -0
- package/src/sdk/prompt-deadline-flush.ts +26 -4
- package/src/sdk/session.ts +587 -115
- package/src/session/agent-session.ts +40 -12
- package/src/session/internal/managed-session-scope.ts +10 -0
- package/src/session/internal/managed-session-storage.ts +30 -1
- package/src/setup/provider-presets.json +2 -1
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/task/executor.ts +46 -17
- package/src/task/types.ts +2 -0
- package/src/tools/bash-interactive.ts +2 -0
- package/src/tools/bash.ts +20 -0
- package/src/tools/fetch.ts +21 -5
- package/src/tools/subagent-render.ts +24 -1
- package/src/tools/subagent.ts +27 -1
- 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))
|
|
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
|
|
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
|
-
|
|
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}
|
|
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:
|
|
3071
|
-
backgroundBusy:
|
|
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.#
|
|
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;
|
|
3175
|
-
//
|
|
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
|
-
|
|
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<
|
|
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
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
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)
|
|
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 (
|
|
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);
|