@gajae-code/coding-agent 0.15.6 → 0.16.0
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 +59 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/root-flags.d.ts +14 -0
- package/dist/types/cli/update-cli.d.ts +13 -0
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/commands/launch.d.ts +10 -0
- package/dist/types/config/autorouting-tier-map.d.ts +24 -0
- package/dist/types/config/file-lock.d.ts +5 -0
- package/dist/types/config/model-registry.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +10 -1
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
- package/dist/types/crash/record-loader.d.ts +15 -0
- package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -1
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
- package/dist/types/master-mode/context.d.ts +21 -0
- package/dist/types/master-mode/first-request.d.ts +34 -0
- package/dist/types/master-mode/snapshot.d.ts +32 -0
- package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
- package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
- package/dist/types/modes/components/status-line/segments.d.ts +10 -0
- package/dist/types/modes/print-mode.d.ts +17 -0
- package/dist/types/modes/ui-language.d.ts +7 -1
- package/dist/types/sdk/broker/broker.d.ts +77 -1
- package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
- package/dist/types/sdk/broker/index.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +26 -0
- package/dist/types/sdk/broker/master-capability.d.ts +10 -0
- package/dist/types/sdk/broker/session-index.d.ts +40 -11
- package/dist/types/sdk/broker/session-scope.d.ts +72 -0
- package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
- package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
- package/dist/types/sdk/broker/state-version.d.ts +4 -1
- package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
- package/dist/types/sdk/bus/index.d.ts +6 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/cli/index.d.ts +1 -0
- package/dist/types/sdk/cli/master-cli.d.ts +63 -0
- package/dist/types/sdk/cli/rows.d.ts +4 -3
- package/dist/types/sdk/cli/session-cli.d.ts +18 -2
- package/dist/types/sdk/client/client.d.ts +3 -2
- package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
- package/dist/types/sdk/host/control/index.d.ts +1 -1
- package/dist/types/sdk/host/control/operations.d.ts +2 -0
- package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
- package/dist/types/sdk/host/host.d.ts +10 -0
- package/dist/types/sdk/host/session-runtime.d.ts +52 -0
- package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
- package/dist/types/sdk/lifecycle/service.d.ts +15 -3
- package/dist/types/sdk/lifecycle-session.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +2 -0
- package/dist/types/sdk/session.d.ts +16 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -1
- package/dist/types/session/agent-session.d.ts +5 -0
- package/dist/types/session/credential-store-errors.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +65 -0
- package/dist/types/session/yield-queue.d.ts +5 -4
- package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +4 -0
- package/dist/types/tools/gh.d.ts +8 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +6 -0
- package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
- package/src/cli/accounts-cli.ts +9 -5
- package/src/cli/args.ts +17 -0
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/root-flags.ts +5 -0
- package/src/cli/update-cli.ts +77 -11
- package/src/commands/launch.ts +86 -18
- package/src/commands/sdk.ts +113 -21
- package/src/config/autorouting-tier-map.ts +12 -0
- package/src/config/file-lock.ts +158 -83
- package/src/config/model-preset-registry.ts +1 -5
- package/src/config/model-profiles.ts +5 -5
- package/src/config/model-registry.ts +23 -0
- package/src/config/model-resolver.ts +5 -0
- package/src/config/settings-schema.ts +8 -1
- package/src/config/settings.ts +5 -0
- package/src/coordinator-mcp/projection-scan.ts +21 -0
- package/src/coordinator-mcp/question-state.ts +9 -11
- package/src/coordinator-mcp/server.ts +94 -20
- package/src/crash/index-store.ts +2 -0
- package/src/crash/record-loader.ts +10 -0
- package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
- package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
- package/src/gjc-runtime/session-state-lock.ts +79 -6
- package/src/gjc-runtime/session-state-sidecar.ts +929 -23
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
- package/src/gjc-runtime/tmux-sessions.ts +252 -13
- package/src/hooks/skill-state.ts +6 -0
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/internal-urls/issue-pr-protocol.ts +4 -2
- package/src/main.ts +57 -2
- package/src/master-mode/context.ts +50 -0
- package/src/master-mode/first-request.ts +65 -0
- package/src/master-mode/snapshot.ts +205 -0
- package/src/modes/acp/acp-agent.ts +605 -156
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
- package/src/modes/components/oauth-selector.ts +6 -1
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/model-name.ts +53 -0
- package/src/modes/components/status-line/priority-row.ts +107 -0
- package/src/modes/components/status-line/segments.ts +23 -9
- package/src/modes/components/tool-status-header.ts +151 -15
- package/src/modes/controllers/input-controller.ts +28 -0
- package/src/modes/controllers/selector-controller.ts +191 -96
- package/src/modes/print-mode.ts +64 -0
- package/src/modes/ui-language.ts +49 -2
- package/src/prompts/system/master-mode.md +28 -0
- package/src/sdk/broker/broker.ts +1636 -26
- package/src/sdk/broker/endpoint-authority.ts +86 -0
- package/src/sdk/broker/ensure.ts +5 -0
- package/src/sdk/broker/index.ts +3 -0
- package/src/sdk/broker/lifecycle.ts +186 -41
- package/src/sdk/broker/master-capability.ts +232 -0
- package/src/sdk/broker/session-index.ts +121 -17
- package/src/sdk/broker/session-scope.ts +272 -0
- package/src/sdk/broker/spawn-authority.ts +1031 -0
- package/src/sdk/broker/spawn-substrate.ts +548 -0
- package/src/sdk/broker/state-version.ts +26 -14
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/broker/worktree-occupancy.ts +90 -0
- package/src/sdk/bus/chat-daemon-control.ts +5 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +35 -4
- package/src/sdk/bus/index.ts +51 -4
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/cli/index.ts +1 -0
- package/src/sdk/cli/master-cli.ts +259 -0
- package/src/sdk/cli/rows.ts +11 -8
- package/src/sdk/cli/session-cli.ts +264 -5
- package/src/sdk/client/client.ts +14 -8
- package/src/sdk/host/control/dispatch.ts +99 -38
- package/src/sdk/host/control/index.ts +2 -0
- package/src/sdk/host/control/operations.ts +2 -0
- package/src/sdk/host/control/runtime-gate.ts +203 -11
- package/src/sdk/host/host.ts +27 -2
- package/src/sdk/host/session-runtime.test.ts +210 -6
- package/src/sdk/host/session-runtime.ts +291 -49
- package/src/sdk/lifecycle/broker-client.ts +28 -0
- package/src/sdk/lifecycle/service.ts +279 -20
- package/src/sdk/lifecycle-session.ts +10 -2
- package/src/sdk/protocol/operation-inventory.generated.json +51 -0
- package/src/sdk/protocol/operation-registry.ts +13 -3
- package/src/sdk/router/session-router.ts +67 -59
- package/src/sdk/session.ts +357 -155
- package/src/sdk/startup-capability.ts +1 -0
- package/src/session/agent-session.ts +640 -103
- package/src/session/blob-store.ts +15 -5
- package/src/session/credential-store-errors.ts +2 -0
- package/src/session/internal/managed-session-storage.ts +153 -78
- package/src/session/session-manager.ts +337 -24
- package/src/session/yield-queue.ts +24 -10
- package/src/setup/frictionless-onboarding.ts +2 -0
- package/src/telemetry/events.ts +1138 -12
- package/src/tools/bash.ts +256 -42
- package/src/tools/gh.ts +104 -48
- package/src/tools/github-cache.ts +2 -2
- package/src/tools/index.ts +4 -0
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
type SetSessionModeRequest,
|
|
37
37
|
type SetSessionModeResponse,
|
|
38
38
|
} from "@agentclientprotocol/sdk";
|
|
39
|
-
import { getAgentDir, logger } from "@gajae-code/utils";
|
|
39
|
+
import { getAgentDir, logger, resolveEquivalentPath } from "@gajae-code/utils";
|
|
40
40
|
import packageJson from "../../../package.json" with { type: "json" };
|
|
41
41
|
import {
|
|
42
42
|
ACP_SESSION_RECONNECT,
|
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
import { resolveAcpFinalText } from "../../sdk/acp/final-text";
|
|
50
50
|
import { ACP_MCP_LIFECYCLE_TIMEOUT_MS, type SessionLifecycleMcpServer } from "../../sdk/acp/mcp";
|
|
51
51
|
import { ensureBroker } from "../../sdk/broker/ensure";
|
|
52
|
+
import { canonicalSessionCwd } from "../../sdk/broker/session-index";
|
|
52
53
|
import { readSdkBrokerDiscovery, SdkClient, SdkClientError } from "../../sdk/client";
|
|
53
54
|
import type { AbortScope } from "../../sdk/host/control/operations";
|
|
54
55
|
import { SYNTHETIC_PROVIDER_ID } from "../../sdk/model-profile-namespace";
|
|
@@ -106,6 +107,8 @@ interface PromptWaiter {
|
|
|
106
107
|
acknowledged: boolean;
|
|
107
108
|
/** True once turn.prompt / skill.invoke has been sent; cancel must not fake-settle after this. */
|
|
108
109
|
dispatched: boolean;
|
|
110
|
+
/** True only while the dispatched control request can still reveal its correlation. */
|
|
111
|
+
acknowledgementPending: boolean;
|
|
109
112
|
|
|
110
113
|
/** Highest inbound frame sequence already observed when the prompt was acknowledged. */
|
|
111
114
|
boundary: number;
|
|
@@ -114,8 +117,12 @@ interface PromptWaiter {
|
|
|
114
117
|
emittedAssistantText: string;
|
|
115
118
|
settled: boolean;
|
|
116
119
|
terminal?: { outcome: SdkPromptTerminalOutcome; correlation: PromptCorrelation };
|
|
120
|
+
/** Exact terminal observed at ingress; no later timeout/cancel/frame failure may supersede it. */
|
|
121
|
+
terminalReserved: boolean;
|
|
122
|
+
/** Additive diagnostics held until settlement so they cannot block the terminal response writer. */
|
|
123
|
+
failureDiagnostics: Array<{ notification: SessionNotification; publicationGeneration: number }>;
|
|
117
124
|
/** Frames for an already-settled correlation held until acknowledgement resolves ownership. */
|
|
118
|
-
deferredFrames: JsonObject
|
|
125
|
+
deferredFrames: Array<{ frame: JsonObject; publicationGeneration: number }>;
|
|
119
126
|
/** Activity frames held until acknowledgement establishes exact prompt ownership. */
|
|
120
127
|
deferredActivityFrames: JsonObject[];
|
|
121
128
|
/** Clock reading of the last frame proven to belong to this prompt; watchdog silence baseline. */
|
|
@@ -144,6 +151,7 @@ interface PromptWaiter {
|
|
|
144
151
|
}
|
|
145
152
|
|
|
146
153
|
type PromptCorrelation = { commandId?: string; turnId?: string };
|
|
154
|
+
type PromptPhaseOwner = PromptWaiter | "background" | undefined;
|
|
147
155
|
|
|
148
156
|
type BrokerConnection = { adapter: AcpSdkAdapter; client: SdkClient };
|
|
149
157
|
type PendingAttachment = { epoch: number; task: Promise<void> };
|
|
@@ -157,10 +165,15 @@ type SessionRecord = {
|
|
|
157
165
|
reconnectUnsubscribe: () => void;
|
|
158
166
|
/** Per-session frame work queue; callbacks never race prompt ownership. */
|
|
159
167
|
frameTail: Promise<void>;
|
|
168
|
+
/** Advances when an owned terminal retires all earlier frame publications. */
|
|
169
|
+
publicationGeneration: number;
|
|
160
170
|
/** Monotonic at WebSocket ingress, before queued work begins. */
|
|
161
171
|
inboundSequence: number;
|
|
162
172
|
/** Updated at ingress so a prompt acknowledgement can distinguish a steer from a fresh turn. */
|
|
163
173
|
busy: boolean;
|
|
174
|
+
backgroundBusy: boolean;
|
|
175
|
+
backgroundAnonymousCount: number;
|
|
176
|
+
backgroundCorrelations: PromptCorrelation[];
|
|
164
177
|
/** Start/update args retained because tool_execution_end does not carry them. */
|
|
165
178
|
toolArgs: Map<string, unknown>;
|
|
166
179
|
/** Message projection state for correlationless session-scoped assistant events. */
|
|
@@ -183,7 +196,7 @@ function promptWaiterRetired(record: SessionRecord, waiter: PromptWaiter): boole
|
|
|
183
196
|
|
|
184
197
|
type BrokerSession = {
|
|
185
198
|
sessionId: string;
|
|
186
|
-
locator?: {
|
|
199
|
+
locator?: { cwd?: string; worktreeRoot?: string | null; stateRoot?: string };
|
|
187
200
|
live?: boolean;
|
|
188
201
|
endpointGeneration?: number;
|
|
189
202
|
endpointMtimeMs?: number;
|
|
@@ -273,13 +286,14 @@ export function paginateAcpSessions(
|
|
|
273
286
|
offset: number,
|
|
274
287
|
sessionMetadata: ReadonlyMap<string, { title?: string; updatedAt?: string }> = new Map(),
|
|
275
288
|
): ListSessionsResponse {
|
|
289
|
+
const canonicalCwd = cwd === undefined ? undefined : resolveEquivalentPath(cwd);
|
|
276
290
|
const filtered = listed
|
|
277
291
|
.map(value => object(value) as BrokerSession | undefined)
|
|
278
292
|
.filter(
|
|
279
|
-
(value): value is BrokerSession & { locator: {
|
|
280
|
-
typeof value?.sessionId === "string" && typeof value.locator?.
|
|
293
|
+
(value): value is BrokerSession & { locator: { cwd: string } } =>
|
|
294
|
+
typeof value?.sessionId === "string" && typeof value.locator?.cwd === "string",
|
|
281
295
|
)
|
|
282
|
-
.filter(value =>
|
|
296
|
+
.filter(value => canonicalCwd === undefined || value.locator.cwd === canonicalCwd);
|
|
283
297
|
const sessions = filtered.slice(offset, offset + SESSION_PAGE_SIZE).map(value => {
|
|
284
298
|
const metadata = sessionMetadata.get(value.sessionId);
|
|
285
299
|
const updatedAt =
|
|
@@ -292,7 +306,7 @@ export function paginateAcpSessions(
|
|
|
292
306
|
: undefined;
|
|
293
307
|
return {
|
|
294
308
|
sessionId: value.sessionId,
|
|
295
|
-
cwd: value.locator.
|
|
309
|
+
cwd: value.locator.cwd,
|
|
296
310
|
title:
|
|
297
311
|
typeof metadata?.title === "string" && metadata.title
|
|
298
312
|
? metadata.title
|
|
@@ -491,7 +505,12 @@ function terminalOutcome(event: JsonObject): SdkPromptTerminalOutcome | undefine
|
|
|
491
505
|
typeof outcome.message === "string" &&
|
|
492
506
|
(outcome.provenance === "agent_failed" || outcome.provenance === "deadline")
|
|
493
507
|
)
|
|
494
|
-
return
|
|
508
|
+
return {
|
|
509
|
+
kind: "failed",
|
|
510
|
+
code: outcome.code,
|
|
511
|
+
message: outcome.message,
|
|
512
|
+
provenance: outcome.provenance,
|
|
513
|
+
};
|
|
495
514
|
return undefined;
|
|
496
515
|
}
|
|
497
516
|
|
|
@@ -1182,6 +1201,18 @@ export class AcpAgent implements Agent {
|
|
|
1182
1201
|
readonly #pendingRouterFrames = new Map<string, Record<string, unknown>[]>();
|
|
1183
1202
|
#routerStartPromise: Promise<void> | undefined;
|
|
1184
1203
|
readonly #sessions = new Map<string, SessionRecord>();
|
|
1204
|
+
/** Retain settled prompt identities across automatic transport reattachment. */
|
|
1205
|
+
readonly #retiredPromptCorrelations = new Map<string, PromptCorrelation[]>();
|
|
1206
|
+
/** Prevent a successor admission while a retired prompt can still reveal its identity. */
|
|
1207
|
+
readonly #retiredPromptAcknowledgements = new Set<string>();
|
|
1208
|
+
/** Ready terminal metadata writes retain ownership across same-id record replacement. */
|
|
1209
|
+
readonly #terminalMetadataTails = new Map<string, Promise<void>>();
|
|
1210
|
+
/** Unstreamed terminal text retains transcript ownership across same-id replacement. */
|
|
1211
|
+
readonly #finalTextTails = new Map<string, Promise<void>>();
|
|
1212
|
+
/** Generic failure diagnostics publish independently from authoritative terminal ingress. */
|
|
1213
|
+
readonly #failureDiagnosticTails = new Map<string, Promise<void>>();
|
|
1214
|
+
/** Prompt phase writes retain ordering across same-id record replacement. */
|
|
1215
|
+
readonly #promptPhaseTails = new Map<string, Promise<void>>();
|
|
1185
1216
|
readonly #attaching = new Map<string, PendingAttachment>();
|
|
1186
1217
|
readonly #resolvingExisting = new Map<string, PendingAttachment>();
|
|
1187
1218
|
readonly #knownSessionCwds = new Map<string, string>();
|
|
@@ -1409,38 +1440,34 @@ export class AcpAgent implements Agent {
|
|
|
1409
1440
|
|
|
1410
1441
|
async listSessions(params: ListSessionsRequest): Promise<ListSessionsResponse> {
|
|
1411
1442
|
if (params.cwd) this.#assertAbsoluteCwd(params.cwd);
|
|
1443
|
+
const canonicalCwd = params.cwd ? await canonicalSessionCwd(params.cwd) : undefined;
|
|
1412
1444
|
const adapter = await this.#brokerAdapter();
|
|
1413
1445
|
const listing = await collectAcpSessionList(input => adapter.global("session.list", input));
|
|
1414
1446
|
const listed = Array.isArray(listing.sessions) ? listing.sessions : [];
|
|
1415
1447
|
|
|
1416
|
-
if (
|
|
1448
|
+
if (canonicalCwd) {
|
|
1417
1449
|
const discovered = new Set<string>();
|
|
1418
1450
|
for (const session of listed) {
|
|
1419
1451
|
const candidate = object(session) as BrokerSession | undefined;
|
|
1420
1452
|
if (
|
|
1421
1453
|
typeof candidate?.sessionId !== "string" ||
|
|
1422
|
-
typeof candidate.locator?.
|
|
1423
|
-
|
|
1454
|
+
typeof candidate.locator?.cwd !== "string" ||
|
|
1455
|
+
candidate.locator.cwd !== canonicalCwd
|
|
1424
1456
|
)
|
|
1425
1457
|
continue;
|
|
1426
1458
|
if (discovered.has(candidate.sessionId))
|
|
1427
1459
|
throw new AcpSdkAdapterError("conflict", `Broker returned duplicate session id: ${candidate.sessionId}`);
|
|
1428
1460
|
discovered.add(candidate.sessionId);
|
|
1429
1461
|
const knownCwd = this.#knownSessionCwds.get(candidate.sessionId);
|
|
1430
|
-
if (knownCwd &&
|
|
1462
|
+
if (knownCwd && (await canonicalSessionCwd(knownCwd)) !== canonicalCwd)
|
|
1431
1463
|
throw new AcpSdkAdapterError(
|
|
1432
1464
|
"conflict",
|
|
1433
1465
|
`ACP session ${candidate.sessionId} has conflicting cwd authority.`,
|
|
1434
1466
|
);
|
|
1435
|
-
this.#knownSessionCwds.set(candidate.sessionId,
|
|
1467
|
+
this.#knownSessionCwds.set(candidate.sessionId, canonicalCwd);
|
|
1436
1468
|
}
|
|
1437
1469
|
}
|
|
1438
|
-
return paginateAcpSessions(
|
|
1439
|
-
listed,
|
|
1440
|
-
params.cwd ?? undefined,
|
|
1441
|
-
this.#cursor(params.cursor),
|
|
1442
|
-
this.#knownSessionMetadata,
|
|
1443
|
-
);
|
|
1470
|
+
return paginateAcpSessions(listed, canonicalCwd, this.#cursor(params.cursor), this.#knownSessionMetadata);
|
|
1444
1471
|
}
|
|
1445
1472
|
|
|
1446
1473
|
closeSession(params: CloseSessionRequest): Promise<CloseSessionResponse> {
|
|
@@ -1501,6 +1528,8 @@ export class AcpAgent implements Agent {
|
|
|
1501
1528
|
this.#ownedSessionIds.delete(params.sessionId);
|
|
1502
1529
|
this.#knownSessionMcpServers.delete(params.sessionId);
|
|
1503
1530
|
this.#knownSessionMetadata.delete(params.sessionId);
|
|
1531
|
+
this.#retiredPromptCorrelations.delete(params.sessionId);
|
|
1532
|
+
this.#retiredPromptAcknowledgements.delete(params.sessionId);
|
|
1504
1533
|
return {};
|
|
1505
1534
|
}
|
|
1506
1535
|
throw error;
|
|
@@ -1517,6 +1546,8 @@ export class AcpAgent implements Agent {
|
|
|
1517
1546
|
this.#knownSessionMcpServers.delete(params.sessionId);
|
|
1518
1547
|
this.#knownSessionMetadata.delete(params.sessionId);
|
|
1519
1548
|
this.#pendingDeleteLocators.delete(params.sessionId);
|
|
1549
|
+
this.#retiredPromptCorrelations.delete(params.sessionId);
|
|
1550
|
+
this.#retiredPromptAcknowledgements.delete(params.sessionId);
|
|
1520
1551
|
return {};
|
|
1521
1552
|
} finally {
|
|
1522
1553
|
this.#finishTeardown(params.sessionId);
|
|
@@ -1575,6 +1606,21 @@ export class AcpAgent implements Agent {
|
|
|
1575
1606
|
if (!record) throw new AcpSdkAdapterError("not_found", `Unknown session, not found: ${params.sessionId}`);
|
|
1576
1607
|
if (record.activePrompt) throw new AcpSdkAdapterError("conflict", "ACP session already has an active prompt.");
|
|
1577
1608
|
if (record.authFailure) throw new AcpSdkAdapterError("authentication_failed", record.authFailure);
|
|
1609
|
+
if (this.#retiredPromptAcknowledgements.has(params.sessionId))
|
|
1610
|
+
throw new AcpSdkAdapterError(
|
|
1611
|
+
"conflict",
|
|
1612
|
+
"ACP session is still reconciling a previous prompt acknowledgement.",
|
|
1613
|
+
);
|
|
1614
|
+
if (this.#finalTextTails.has(params.sessionId))
|
|
1615
|
+
throw new AcpSdkAdapterError("conflict", "ACP session is still publishing the previous prompt's final text.");
|
|
1616
|
+
if (this.#terminalMetadataTails.has(params.sessionId))
|
|
1617
|
+
throw new AcpSdkAdapterError("conflict", "ACP session is still publishing the previous prompt's metadata.");
|
|
1618
|
+
if (this.#failureDiagnosticTails.has(params.sessionId))
|
|
1619
|
+
throw new AcpSdkAdapterError(
|
|
1620
|
+
"conflict",
|
|
1621
|
+
"ACP session is still publishing the previous prompt's failure diagnostic.",
|
|
1622
|
+
);
|
|
1623
|
+
|
|
1578
1624
|
const payload = acpPromptPayload(params.prompt);
|
|
1579
1625
|
const skillInvocation = acpSkillInvocation(params.prompt);
|
|
1580
1626
|
if (!skillInvocation) {
|
|
@@ -1625,14 +1671,18 @@ export class AcpAgent implements Agent {
|
|
|
1625
1671
|
MAX_PROMPT_FRAME_BYTES / 1024,
|
|
1626
1672
|
)} KiB transport limit. Attach a smaller or more compressed image.`,
|
|
1627
1673
|
);
|
|
1674
|
+
record.publicationGeneration++;
|
|
1628
1675
|
const { promise: response, resolve, reject } = Promise.withResolvers<PromptResponse>();
|
|
1629
1676
|
const waiter: PromptWaiter = {
|
|
1630
1677
|
acknowledged: false,
|
|
1631
1678
|
dispatched: false,
|
|
1679
|
+
acknowledgementPending: false,
|
|
1632
1680
|
boundary: record.inboundSequence,
|
|
1633
1681
|
correlation: {},
|
|
1634
1682
|
emittedAssistantText: "",
|
|
1635
1683
|
settled: false,
|
|
1684
|
+
terminalReserved: false,
|
|
1685
|
+
failureDiagnostics: [],
|
|
1636
1686
|
deferredFrames: [],
|
|
1637
1687
|
deferredActivityFrames: [],
|
|
1638
1688
|
lastFrameAt: this.#promptWatchdogClock.now(),
|
|
@@ -1650,13 +1700,20 @@ export class AcpAgent implements Agent {
|
|
|
1650
1700
|
await record.adapter.ensureProviders();
|
|
1651
1701
|
} catch (error) {
|
|
1652
1702
|
if (waiter.settled || record.cancelRequested || record.activePrompt !== waiter) {
|
|
1703
|
+
this.#retiredPromptAcknowledgements.delete(params.sessionId);
|
|
1653
1704
|
if (!waiter.settled) await this.#settleCancelledPrompt(params.sessionId, record, waiter);
|
|
1654
1705
|
return await response;
|
|
1655
1706
|
}
|
|
1656
|
-
if (record.activePrompt === waiter)
|
|
1707
|
+
if (record.activePrompt === waiter) {
|
|
1708
|
+
record.activePrompt = undefined;
|
|
1709
|
+
record.busy = record.backgroundBusy;
|
|
1710
|
+
this.#retiredPromptAcknowledgements.delete(params.sessionId);
|
|
1711
|
+
void this.#publishPromptPhaseIdle(params.sessionId, record.adapter);
|
|
1712
|
+
}
|
|
1657
1713
|
throw error;
|
|
1658
1714
|
}
|
|
1659
1715
|
if (waiter.settled || record.cancelRequested || record.activePrompt !== waiter) {
|
|
1716
|
+
this.#retiredPromptAcknowledgements.delete(params.sessionId);
|
|
1660
1717
|
if (!waiter.settled) await this.#settleCancelledPrompt(params.sessionId, record, waiter);
|
|
1661
1718
|
return await response;
|
|
1662
1719
|
}
|
|
@@ -1687,6 +1744,7 @@ export class AcpAgent implements Agent {
|
|
|
1687
1744
|
return await response;
|
|
1688
1745
|
}
|
|
1689
1746
|
|
|
1747
|
+
waiter.acknowledgementPending = true;
|
|
1690
1748
|
const acknowledgementTask = (async (): Promise<PromptResponse> => {
|
|
1691
1749
|
if (waiter.settled || record.activePrompt !== waiter) return await response;
|
|
1692
1750
|
const acknowledgement = skillInvocation
|
|
@@ -1702,20 +1760,33 @@ export class AcpAgent implements Agent {
|
|
|
1702
1760
|
"invalid_prompt_acknowledgement",
|
|
1703
1761
|
"SDK prompt acknowledgement must accept the prompt and include commandId and turnId.",
|
|
1704
1762
|
);
|
|
1763
|
+
if (this.#hasRetiredPromptCorrelation(params.sessionId, record, acknowledgementCorrelation)) {
|
|
1764
|
+
try {
|
|
1765
|
+
const abortAcknowledgement = await record.adapter.cancel("turn");
|
|
1766
|
+
if (!isAbortAcknowledged(abortAcknowledgement, "turn"))
|
|
1767
|
+
throw new Error("SDK did not confirm retirement of the reused prompt correlation.");
|
|
1768
|
+
} catch (error) {
|
|
1769
|
+
await this.#failSession(
|
|
1770
|
+
params.sessionId,
|
|
1771
|
+
record.adapter,
|
|
1772
|
+
new AcpSdkAdapterError(
|
|
1773
|
+
"connection_closed",
|
|
1774
|
+
`ACP could not retire a prompt with a reused correlation: ${error instanceof Error ? error.message : String(error)}`,
|
|
1775
|
+
),
|
|
1776
|
+
);
|
|
1777
|
+
throw error;
|
|
1778
|
+
}
|
|
1779
|
+
throw new AcpSdkAdapterError(
|
|
1780
|
+
"invalid_prompt_acknowledgement",
|
|
1781
|
+
"SDK prompt acknowledgement reused a settled commandId/turnId pair.",
|
|
1782
|
+
);
|
|
1783
|
+
}
|
|
1705
1784
|
// Retain the acknowledgement ingress boundary with its complete correlation.
|
|
1706
1785
|
waiter.boundary = record.inboundSequence;
|
|
1707
1786
|
waiter.correlation = acknowledgementCorrelation;
|
|
1708
1787
|
waiter.acknowledged = true;
|
|
1709
1788
|
if (promptWaiterRetired(record, waiter)) {
|
|
1710
|
-
|
|
1711
|
-
!record.settledPromptCorrelations.some(settled =>
|
|
1712
|
-
correlationsExactlyMatch(settled, acknowledgementCorrelation),
|
|
1713
|
-
)
|
|
1714
|
-
) {
|
|
1715
|
-
record.settledPromptCorrelations.push(acknowledgementCorrelation);
|
|
1716
|
-
while (record.settledPromptCorrelations.length > SETTLED_PROMPT_CORRELATION_RETENTION)
|
|
1717
|
-
record.settledPromptCorrelations.shift();
|
|
1718
|
-
}
|
|
1789
|
+
this.#rememberSettledPromptCorrelation(params.sessionId, record, acknowledgementCorrelation);
|
|
1719
1790
|
return await response;
|
|
1720
1791
|
}
|
|
1721
1792
|
// Frames held while ownership was unknown belong to this prompt only when the
|
|
@@ -1724,18 +1795,47 @@ export class AcpAgent implements Agent {
|
|
|
1724
1795
|
let observedDeferredActivity = false;
|
|
1725
1796
|
for (const deferredFrame of deferredActivityFrames) {
|
|
1726
1797
|
const deferredEvent = receivedSdkEvent(deferredFrame)?.event;
|
|
1727
|
-
|
|
1798
|
+
const matchesWaiter = correlationsExactlyMatch(
|
|
1799
|
+
waiter.correlation,
|
|
1800
|
+
watchdogCorrelationFrom(deferredFrame, deferredEvent),
|
|
1801
|
+
);
|
|
1802
|
+
if (deferredEvent?.type === "agent_start" && !matchesWaiter) {
|
|
1803
|
+
const deferredCorrelation = sdkFrameCorrelation(deferredFrame, deferredEvent);
|
|
1804
|
+
const tombstoned =
|
|
1805
|
+
deferredCorrelation &&
|
|
1806
|
+
record.settledPromptCorrelations.some(settled =>
|
|
1807
|
+
correlationsExactlyMatch(settled, deferredCorrelation),
|
|
1808
|
+
);
|
|
1809
|
+
if (!tombstoned) {
|
|
1810
|
+
record.backgroundBusy = true;
|
|
1811
|
+
if (deferredCorrelation && hasCompleteCorrelation(deferredCorrelation)) {
|
|
1812
|
+
if (
|
|
1813
|
+
!record.backgroundCorrelations.some(owner =>
|
|
1814
|
+
correlationsExactlyMatch(owner, deferredCorrelation),
|
|
1815
|
+
)
|
|
1816
|
+
)
|
|
1817
|
+
record.backgroundCorrelations.push(deferredCorrelation);
|
|
1818
|
+
} else record.backgroundAnonymousCount++;
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
if (matchesWaiter) {
|
|
1728
1822
|
this.#observePromptActivity(waiter, deferredFrame);
|
|
1729
1823
|
observedDeferredActivity = true;
|
|
1730
1824
|
}
|
|
1731
1825
|
}
|
|
1732
1826
|
if (observedDeferredActivity) this.#armPromptWatchdog(params.sessionId, record, waiter);
|
|
1733
1827
|
const deferred = waiter.deferredFrames.splice(0);
|
|
1734
|
-
for (const deferredFrame of deferred) {
|
|
1828
|
+
for (const { frame: deferredFrame, publicationGeneration: deferredGeneration } of deferred) {
|
|
1735
1829
|
const deferredEvent = receivedSdkEvent(deferredFrame)?.event;
|
|
1736
1830
|
if (!deferredEvent) continue;
|
|
1737
1831
|
const deferredCorrelation = sdkFrameCorrelation(deferredFrame, deferredEvent) ?? {};
|
|
1738
|
-
const
|
|
1832
|
+
const deferredOutcome =
|
|
1833
|
+
deferredEvent.type === "agent_end" || deferredEvent.type === "agent_failed"
|
|
1834
|
+
? terminalOutcome(deferredEvent)
|
|
1835
|
+
: undefined;
|
|
1836
|
+
const deferredIsTerminal =
|
|
1837
|
+
(deferredEvent.type === "agent_end" && deferredOutcome !== undefined) ||
|
|
1838
|
+
(deferredEvent.type === "agent_failed" && deferredOutcome?.kind === "failed");
|
|
1739
1839
|
if (!hasCompleteCorrelation(deferredCorrelation)) {
|
|
1740
1840
|
if (deferredIsTerminal)
|
|
1741
1841
|
logDroppedPromptTerminal(
|
|
@@ -1748,6 +1848,18 @@ export class AcpAgent implements Agent {
|
|
|
1748
1848
|
continue;
|
|
1749
1849
|
}
|
|
1750
1850
|
const matchesPrompt = correlationsExactlyMatch(waiter.correlation, deferredCorrelation);
|
|
1851
|
+
if (
|
|
1852
|
+
!matchesPrompt &&
|
|
1853
|
+
deferredIsTerminal &&
|
|
1854
|
+
record.backgroundCorrelations.some(owner => correlationsExactlyMatch(owner, deferredCorrelation))
|
|
1855
|
+
) {
|
|
1856
|
+
record.backgroundCorrelations = record.backgroundCorrelations.filter(
|
|
1857
|
+
owner => !correlationsExactlyMatch(owner, deferredCorrelation),
|
|
1858
|
+
);
|
|
1859
|
+
record.backgroundBusy = record.backgroundAnonymousCount > 0 || record.backgroundCorrelations.length > 0;
|
|
1860
|
+
record.busy = true;
|
|
1861
|
+
continue;
|
|
1862
|
+
}
|
|
1751
1863
|
if (
|
|
1752
1864
|
!matchesPrompt &&
|
|
1753
1865
|
record.settledPromptCorrelations.some(settled => correlationsExactlyMatch(settled, deferredCorrelation))
|
|
@@ -1764,17 +1876,26 @@ export class AcpAgent implements Agent {
|
|
|
1764
1876
|
);
|
|
1765
1877
|
continue;
|
|
1766
1878
|
}
|
|
1879
|
+
if (deferredIsTerminal) {
|
|
1880
|
+
waiter.terminalReserved = true;
|
|
1881
|
+
clearPromptWatchdog(waiter);
|
|
1882
|
+
}
|
|
1767
1883
|
const task = record.frameTail.then(
|
|
1768
|
-
async () =>
|
|
1769
|
-
|
|
1770
|
-
record.frameTail = task.catch(
|
|
1771
|
-
async error =>
|
|
1772
|
-
await this.#failSession(params.sessionId, record.adapter, this.#frameProcessingFailure(error)),
|
|
1884
|
+
async () =>
|
|
1885
|
+
await this.#handleSdkFrame(params.sessionId, record.adapter, deferredFrame, deferredGeneration),
|
|
1773
1886
|
);
|
|
1887
|
+
record.frameTail = task.catch(async error => {
|
|
1888
|
+
if (deferredGeneration !== record.publicationGeneration) return;
|
|
1889
|
+
if (record.activePrompt?.terminalReserved) return;
|
|
1890
|
+
await this.#failSession(params.sessionId, record.adapter, this.#frameProcessingFailure(error));
|
|
1891
|
+
});
|
|
1774
1892
|
}
|
|
1775
|
-
this.#settlePrompt(record, waiter);
|
|
1893
|
+
this.#settlePrompt(params.sessionId, record, waiter);
|
|
1776
1894
|
return await response;
|
|
1777
|
-
})()
|
|
1895
|
+
})().finally(() => {
|
|
1896
|
+
waiter.acknowledgementPending = false;
|
|
1897
|
+
this.#retiredPromptAcknowledgements.delete(params.sessionId);
|
|
1898
|
+
});
|
|
1778
1899
|
// Settlement can win before the SDK answers `turn.prompt`; acknowledgement processing
|
|
1779
1900
|
// must remain alive to tombstone its eventual correlation without holding the ACP caller.
|
|
1780
1901
|
void acknowledgementTask.catch(() => undefined);
|
|
@@ -1787,15 +1908,15 @@ export class AcpAgent implements Agent {
|
|
|
1787
1908
|
clearPromptWatchdog(waiter);
|
|
1788
1909
|
waiter.terminal = undefined;
|
|
1789
1910
|
waiter.settled = true;
|
|
1790
|
-
if (record.activePrompt === waiter)
|
|
1911
|
+
if (record.activePrompt === waiter) {
|
|
1912
|
+
record.activePrompt = undefined;
|
|
1913
|
+
record.busy = record.backgroundBusy;
|
|
1914
|
+
void this.#publishPromptPhaseIdle(params.sessionId, record.adapter);
|
|
1915
|
+
}
|
|
1791
1916
|
// Keep a late terminal for this (cancelled) turn closed, exactly like the
|
|
1792
1917
|
// other settlement paths, so an aborted run's trailing frame can never
|
|
1793
1918
|
// publish over a later prompt that reuses the identity.
|
|
1794
|
-
|
|
1795
|
-
record.settledPromptCorrelations.push(waiter.correlation);
|
|
1796
|
-
while (record.settledPromptCorrelations.length > SETTLED_PROMPT_CORRELATION_RETENTION)
|
|
1797
|
-
record.settledPromptCorrelations.shift();
|
|
1798
|
-
}
|
|
1919
|
+
this.#rememberSettledPromptCorrelation(params.sessionId, record, waiter.correlation);
|
|
1799
1920
|
// A prompt cancelled before the SDK acknowledged it still ends this turn by
|
|
1800
1921
|
// client request, and ACP is explicit: "Agents MUST catch these errors and
|
|
1801
1922
|
// return the semantically meaningful `cancelled` stop reason, so that Clients
|
|
@@ -1806,7 +1927,6 @@ export class AcpAgent implements Agent {
|
|
|
1806
1927
|
// The client's turn is settled by the return; the advisory idle publication
|
|
1807
1928
|
// must not gate it and must still be attempted so the running phase is
|
|
1808
1929
|
// released (gjcRunning:false) instead of spinning behind a settled cancel.
|
|
1809
|
-
void this.#publishPromptPhaseIdle(params.sessionId, record.adapter);
|
|
1810
1930
|
return { stopReason: "cancelled" };
|
|
1811
1931
|
}
|
|
1812
1932
|
throw error;
|
|
@@ -1851,7 +1971,9 @@ export class AcpAgent implements Agent {
|
|
|
1851
1971
|
"abort_unacknowledged",
|
|
1852
1972
|
"SDK did not acknowledge cancellation of the active prompt.",
|
|
1853
1973
|
);
|
|
1854
|
-
if (waiter)
|
|
1974
|
+
if (waiter) {
|
|
1975
|
+
waiter.cancelAcknowledged = true;
|
|
1976
|
+
}
|
|
1855
1977
|
if (waiter && record.activePrompt !== waiter) {
|
|
1856
1978
|
waiter.cancelAttemptResolve?.(true);
|
|
1857
1979
|
return;
|
|
@@ -1864,10 +1986,16 @@ export class AcpAgent implements Agent {
|
|
|
1864
1986
|
!waiter.settled
|
|
1865
1987
|
) {
|
|
1866
1988
|
record.activePrompt = undefined;
|
|
1989
|
+
record.busy = record.backgroundBusy;
|
|
1990
|
+
record.cancelRequested = false;
|
|
1991
|
+
clearPromptWatchdog(waiter);
|
|
1867
1992
|
waiter.settled = true;
|
|
1993
|
+
this.#fenceRetiredPromptAcknowledgement(params.sessionId, waiter);
|
|
1868
1994
|
waiter.deferredFrames.length = 0;
|
|
1995
|
+
waiter.deferredActivityFrames.length = 0;
|
|
1869
1996
|
waiter.terminal = undefined;
|
|
1870
1997
|
waiter.resolve({ stopReason: "cancelled" });
|
|
1998
|
+
this.#flushFailureDiagnostics(params.sessionId, waiter, record.adapter);
|
|
1871
1999
|
// Release the running phase for consistency with the resolved cancel; the
|
|
1872
2000
|
// publication is advisory and must never gate the settlement above.
|
|
1873
2001
|
void this.#publishPromptPhaseIdle(params.sessionId, record.adapter);
|
|
@@ -1932,20 +2060,19 @@ export class AcpAgent implements Agent {
|
|
|
1932
2060
|
// The authoritative terminal wins whenever it arrives in time; this only runs
|
|
1933
2061
|
// when nothing settled the prompt the client already asked to cancel.
|
|
1934
2062
|
if (this.#sessions.get(id) !== record || record.activePrompt !== waiter || waiter.settled) return;
|
|
2063
|
+
if (waiter.terminalReserved) return;
|
|
1935
2064
|
record.activePrompt = undefined;
|
|
2065
|
+
if (!record.backgroundBusy) record.busy = false;
|
|
1936
2066
|
clearPromptWatchdog(waiter);
|
|
1937
2067
|
record.cancelRequested = false;
|
|
1938
2068
|
waiter.settled = true;
|
|
2069
|
+
this.#fenceRetiredPromptAcknowledgement(id, waiter);
|
|
1939
2070
|
waiter.deferredFrames.length = 0;
|
|
1940
2071
|
waiter.deferredActivityFrames.length = 0;
|
|
1941
2072
|
waiter.terminal = undefined;
|
|
1942
2073
|
// A late terminal for this turn must stay closed rather than publish over a
|
|
1943
2074
|
// prompt the client has already been told is cancelled.
|
|
1944
|
-
|
|
1945
|
-
record.settledPromptCorrelations.push(waiter.correlation);
|
|
1946
|
-
while (record.settledPromptCorrelations.length > SETTLED_PROMPT_CORRELATION_RETENTION)
|
|
1947
|
-
record.settledPromptCorrelations.shift();
|
|
1948
|
-
}
|
|
2075
|
+
this.#rememberSettledPromptCorrelation(id, record, waiter.correlation);
|
|
1949
2076
|
// The client's turn ends with the resolution, not with the advisory idle update.
|
|
1950
2077
|
// Publishing the phase transition first made a backpressured ACP transport hold
|
|
1951
2078
|
// the settlement hostage: `sessionUpdate` awaits the stream write, a client that
|
|
@@ -1953,6 +2080,7 @@ export class AcpAgent implements Agent {
|
|
|
1953
2080
|
// ran — the acknowledged cancel left the prompt pending forever, the client
|
|
1954
2081
|
// force-cancelled, and the next prompt collided with the stale foreground turn.
|
|
1955
2082
|
waiter.resolve({ stopReason: "cancelled" });
|
|
2083
|
+
this.#flushFailureDiagnostics(id, waiter, record.adapter);
|
|
1956
2084
|
void this.#publishPromptPhaseIdle(id, record.adapter);
|
|
1957
2085
|
}
|
|
1958
2086
|
|
|
@@ -2143,14 +2271,15 @@ export class AcpAgent implements Agent {
|
|
|
2143
2271
|
}
|
|
2144
2272
|
|
|
2145
2273
|
async #scopedBrokerSession(id: string, cwd: string): Promise<BrokerSession | undefined> {
|
|
2274
|
+
const canonicalCwd = await canonicalSessionCwd(cwd);
|
|
2146
2275
|
const adapter = await this.#brokerAdapter();
|
|
2147
|
-
const result = await collectAcpSessionList(input => adapter.global("session.list", input), { cwd });
|
|
2276
|
+
const result = await collectAcpSessionList(input => adapter.global("session.list", input), { cwd: canonicalCwd });
|
|
2148
2277
|
|
|
2149
2278
|
const matches: BrokerSession[] = [];
|
|
2150
2279
|
for (const item of Array.isArray(result?.sessions) ? result.sessions : []) {
|
|
2151
2280
|
const session = object(item) as BrokerSession | undefined;
|
|
2152
2281
|
if (session?.sessionId !== id) continue;
|
|
2153
|
-
if (typeof session.locator?.
|
|
2282
|
+
if (typeof session.locator?.cwd !== "string" || session.locator.cwd !== canonicalCwd)
|
|
2154
2283
|
throw new AcpSdkAdapterError("conflict", `Broker returned conflicting session scope for ${id}.`);
|
|
2155
2284
|
matches.push(session);
|
|
2156
2285
|
}
|
|
@@ -2245,10 +2374,14 @@ export class AcpAgent implements Agent {
|
|
|
2245
2374
|
unsubscribe: () => {},
|
|
2246
2375
|
reconnectUnsubscribe: () => {},
|
|
2247
2376
|
frameTail: Promise.resolve(),
|
|
2248
|
-
|
|
2377
|
+
publicationGeneration: 0,
|
|
2378
|
+
settledPromptCorrelations: this.#retiredPromptCorrelations.get(id) ?? [],
|
|
2249
2379
|
inboundSequence: 0,
|
|
2250
2380
|
connectionId: adapter.connectionId,
|
|
2251
2381
|
busy: false,
|
|
2382
|
+
backgroundBusy: false,
|
|
2383
|
+
backgroundAnonymousCount: 0,
|
|
2384
|
+
backgroundCorrelations: [],
|
|
2252
2385
|
toolArgs: new Map(),
|
|
2253
2386
|
};
|
|
2254
2387
|
record.unsubscribe = adapter.onFrame(frame => this.#enqueueSdkFrame(id, adapter!, frame));
|
|
@@ -2298,6 +2431,40 @@ export class AcpAgent implements Agent {
|
|
|
2298
2431
|
}
|
|
2299
2432
|
}
|
|
2300
2433
|
|
|
2434
|
+
#rememberSettledPromptCorrelation(id: string, record: SessionRecord, correlation: PromptCorrelation): void {
|
|
2435
|
+
if (!hasCompleteCorrelation(correlation)) return;
|
|
2436
|
+
const shared = this.#retiredPromptCorrelations.get(id);
|
|
2437
|
+
const retained = shared ?? record.settledPromptCorrelations;
|
|
2438
|
+
if (shared && shared !== record.settledPromptCorrelations) {
|
|
2439
|
+
for (const candidate of record.settledPromptCorrelations) {
|
|
2440
|
+
if (
|
|
2441
|
+
hasCompleteCorrelation(candidate) &&
|
|
2442
|
+
!retained.some(settled => correlationsExactlyMatch(settled, candidate))
|
|
2443
|
+
)
|
|
2444
|
+
retained.push(candidate);
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
if (!retained.some(settled => correlationsExactlyMatch(settled, correlation))) retained.push(correlation);
|
|
2448
|
+
while (retained.length > SETTLED_PROMPT_CORRELATION_RETENTION) retained.shift();
|
|
2449
|
+
record.settledPromptCorrelations = retained;
|
|
2450
|
+
this.#retiredPromptCorrelations.set(id, retained);
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
#hasRetiredPromptCorrelation(id: string, record: SessionRecord, correlation: PromptCorrelation): boolean {
|
|
2454
|
+
return (this.#retiredPromptCorrelations.get(id) ?? record.settledPromptCorrelations).some(settled =>
|
|
2455
|
+
correlationsExactlyMatch(settled, correlation),
|
|
2456
|
+
);
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
#fenceRetiredPromptAcknowledgement(id: string, waiter: PromptWaiter): void {
|
|
2460
|
+
if (waiter.dispatched && waiter.acknowledgementPending && !waiter.acknowledged)
|
|
2461
|
+
this.#retiredPromptAcknowledgements.add(id);
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
#advanceTerminalGeneration(record: SessionRecord): void {
|
|
2465
|
+
record.publicationGeneration++;
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2301
2468
|
#recoverSessionAfterTransportFailure(id: string, adapter: AcpSdkAdapter, error: Error): void {
|
|
2302
2469
|
const record = this.#sessions.get(id);
|
|
2303
2470
|
if (!record || record.adapter !== adapter) return;
|
|
@@ -2331,6 +2498,8 @@ export class AcpAgent implements Agent {
|
|
|
2331
2498
|
const mcpServers = this.#knownSessionMcpServers.get(id) ?? [];
|
|
2332
2499
|
try {
|
|
2333
2500
|
await this.#attachExisting(id, cwd, mcpServers);
|
|
2501
|
+
const current = this.#sessions.get(id);
|
|
2502
|
+
if (current) void this.#publishPromptPhase(id, current.adapter, this.#promptPhaseOwner(current));
|
|
2334
2503
|
} catch (attachError) {
|
|
2335
2504
|
const detail = attachError instanceof Error ? attachError.message : String(attachError);
|
|
2336
2505
|
logger.warn(`ACP session ${id} auto-reattach after transport loss failed: ${detail}`);
|
|
@@ -2352,6 +2521,8 @@ export class AcpAgent implements Agent {
|
|
|
2352
2521
|
this.#ownedSessionIds.delete(id);
|
|
2353
2522
|
this.#knownSessionMcpServers.delete(id);
|
|
2354
2523
|
this.#knownSessionMetadata.delete(id);
|
|
2524
|
+
this.#retiredPromptCorrelations.delete(id);
|
|
2525
|
+
this.#retiredPromptAcknowledgements.delete(id);
|
|
2355
2526
|
}
|
|
2356
2527
|
|
|
2357
2528
|
async #closeOwnedSession(id: string): Promise<CloseSessionResponse> {
|
|
@@ -2366,6 +2537,8 @@ export class AcpAgent implements Agent {
|
|
|
2366
2537
|
this.#ownedSessionIds.delete(id);
|
|
2367
2538
|
this.#knownSessionMcpServers.delete(id);
|
|
2368
2539
|
this.#knownSessionMetadata.delete(id);
|
|
2540
|
+
this.#retiredPromptCorrelations.delete(id);
|
|
2541
|
+
this.#retiredPromptAcknowledgements.delete(id);
|
|
2369
2542
|
return {};
|
|
2370
2543
|
} finally {
|
|
2371
2544
|
this.#finishTeardown(id);
|
|
@@ -2384,10 +2557,14 @@ export class AcpAgent implements Agent {
|
|
|
2384
2557
|
try {
|
|
2385
2558
|
this.#advanceSessionEpoch(id);
|
|
2386
2559
|
if (record) {
|
|
2560
|
+
const waiter = record.activePrompt;
|
|
2561
|
+
if (waiter) {
|
|
2562
|
+
this.#rememberSettledPromptCorrelation(id, record, waiter.correlation);
|
|
2563
|
+
this.#fenceRetiredPromptAcknowledgement(id, waiter);
|
|
2564
|
+
}
|
|
2387
2565
|
this.#sessions.delete(id);
|
|
2388
2566
|
record.unsubscribe();
|
|
2389
2567
|
record.reconnectUnsubscribe();
|
|
2390
|
-
const waiter = record.activePrompt;
|
|
2391
2568
|
record.activePrompt = undefined;
|
|
2392
2569
|
// `session/close` is the client asking to end its own work, so the pending turn
|
|
2393
2570
|
// settles as `cancelled` rather than surfacing a spurious error. ACP: "Agents
|
|
@@ -2447,7 +2624,11 @@ export class AcpAgent implements Agent {
|
|
|
2447
2624
|
record.reconnectUnsubscribe();
|
|
2448
2625
|
const waiter = record.activePrompt;
|
|
2449
2626
|
record.activePrompt = undefined;
|
|
2450
|
-
if (waiter)
|
|
2627
|
+
if (waiter) {
|
|
2628
|
+
clearPromptWatchdog(waiter);
|
|
2629
|
+
this.#rememberSettledPromptCorrelation(id, record, waiter.correlation);
|
|
2630
|
+
this.#fenceRetiredPromptAcknowledgement(id, waiter);
|
|
2631
|
+
}
|
|
2451
2632
|
waiter?.reject(error);
|
|
2452
2633
|
try {
|
|
2453
2634
|
await adapter.close();
|
|
@@ -2516,13 +2697,53 @@ export class AcpAgent implements Agent {
|
|
|
2516
2697
|
|
|
2517
2698
|
#observeSessionActivity(record: SessionRecord, frame: JsonObject): void {
|
|
2518
2699
|
if (frame.type === "activity") {
|
|
2519
|
-
if (frame.state === "busy")
|
|
2520
|
-
|
|
2700
|
+
if (frame.state === "busy") {
|
|
2701
|
+
record.busy = true;
|
|
2702
|
+
if (!record.activePrompt) {
|
|
2703
|
+
record.backgroundBusy = true;
|
|
2704
|
+
record.backgroundAnonymousCount = Math.max(record.backgroundAnonymousCount, 1);
|
|
2705
|
+
}
|
|
2706
|
+
} else if (frame.state === "idle") {
|
|
2707
|
+
if (record.backgroundAnonymousCount > 0 || record.backgroundCorrelations.length > 0) {
|
|
2708
|
+
record.busy = true;
|
|
2709
|
+
record.backgroundBusy = true;
|
|
2710
|
+
return;
|
|
2711
|
+
}
|
|
2712
|
+
record.busy = false;
|
|
2713
|
+
record.backgroundBusy = false;
|
|
2714
|
+
record.backgroundAnonymousCount = 0;
|
|
2715
|
+
record.backgroundCorrelations.length = 0;
|
|
2716
|
+
}
|
|
2521
2717
|
return;
|
|
2522
2718
|
}
|
|
2523
2719
|
const event = receivedSdkEvent(frame)?.event;
|
|
2524
|
-
if (event?.type === "agent_start")
|
|
2525
|
-
|
|
2720
|
+
if (event?.type === "agent_start") {
|
|
2721
|
+
const correlation = sdkFrameCorrelation(frame, event);
|
|
2722
|
+
if (
|
|
2723
|
+
correlation &&
|
|
2724
|
+
record.settledPromptCorrelations.some(settled => correlationsExactlyMatch(settled, correlation))
|
|
2725
|
+
)
|
|
2726
|
+
return;
|
|
2727
|
+
if (
|
|
2728
|
+
record.activePrompt?.terminalReserved &&
|
|
2729
|
+
correlation &&
|
|
2730
|
+
correlationsExactlyMatch(record.activePrompt.correlation, correlation)
|
|
2731
|
+
)
|
|
2732
|
+
return;
|
|
2733
|
+
record.busy = true;
|
|
2734
|
+
if (
|
|
2735
|
+
!record.activePrompt ||
|
|
2736
|
+
record.activePrompt.terminalReserved ||
|
|
2737
|
+
(record.activePrompt.acknowledged &&
|
|
2738
|
+
(!correlation || !correlationsExactlyMatch(record.activePrompt.correlation, correlation)))
|
|
2739
|
+
) {
|
|
2740
|
+
record.backgroundBusy = true;
|
|
2741
|
+
if (correlation && hasCompleteCorrelation(correlation)) {
|
|
2742
|
+
if (!record.backgroundCorrelations.some(owner => correlationsExactlyMatch(owner, correlation)))
|
|
2743
|
+
record.backgroundCorrelations.push(correlation);
|
|
2744
|
+
} else record.backgroundAnonymousCount++;
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2526
2747
|
}
|
|
2527
2748
|
|
|
2528
2749
|
#frameProcessingFailure(error: unknown): AcpSdkAdapterError {
|
|
@@ -2559,7 +2780,7 @@ export class AcpAgent implements Agent {
|
|
|
2559
2780
|
*/
|
|
2560
2781
|
#refreshPromptWatchdog(id: string, record: SessionRecord, frame: JsonObject): void {
|
|
2561
2782
|
const waiter = record.activePrompt;
|
|
2562
|
-
if (!waiter || waiter.settled) return;
|
|
2783
|
+
if (!waiter || waiter.settled || waiter.terminalReserved) return;
|
|
2563
2784
|
const event = receivedSdkEvent(frame)?.event;
|
|
2564
2785
|
const correlation = watchdogCorrelationFrom(frame, event);
|
|
2565
2786
|
if (!waiter.acknowledged) {
|
|
@@ -2595,7 +2816,7 @@ export class AcpAgent implements Agent {
|
|
|
2595
2816
|
* turn can no longer be observed, it does not cancel or tear down the session.
|
|
2596
2817
|
*/
|
|
2597
2818
|
async #expirePromptWatchdog(id: string, record: SessionRecord, waiter: PromptWaiter): Promise<void> {
|
|
2598
|
-
if (record.activePrompt !== waiter || waiter.settled) return;
|
|
2819
|
+
if (record.activePrompt !== waiter || waiter.settled || waiter.terminalReserved) return;
|
|
2599
2820
|
const silenceMs = Math.max(0, this.#promptWatchdogClock.now() - waiter.lastFrameAt);
|
|
2600
2821
|
const cause = this.#promptTransportGone(id, record) ?? "the SDK session host stopped producing frames";
|
|
2601
2822
|
logger.error("acp_prompt_watchdog_expired", {
|
|
@@ -2625,20 +2846,48 @@ export class AcpAgent implements Agent {
|
|
|
2625
2846
|
#enqueueSdkFrame(id: string, adapter: AcpSdkAdapter, frame: JsonObject): void {
|
|
2626
2847
|
const record = this.#sessions.get(id);
|
|
2627
2848
|
if (!record || record.adapter !== adapter) return;
|
|
2628
|
-
if (
|
|
2849
|
+
if (frame.type !== "hello" && frame.type !== "server_hello" && typeof frame.connectionId === "string")
|
|
2850
|
+
record.connectionId = frame.connectionId;
|
|
2629
2851
|
// Ingress ordering is recorded before queued work begins.
|
|
2630
2852
|
this.#observeSessionActivity(record, frame);
|
|
2631
2853
|
// Correlation is checked at ingress before a prompt-owned frame may refresh the
|
|
2632
2854
|
// watchdog, so queued processing cannot turn unrelated host traffic into turn liveness.
|
|
2633
2855
|
this.#refreshPromptWatchdog(id, record, frame);
|
|
2856
|
+
const received = receivedSdkEvent(frame);
|
|
2857
|
+
const ingressEvent = received?.event;
|
|
2858
|
+
const ingressOutcome =
|
|
2859
|
+
ingressEvent?.type === "agent_end" || ingressEvent?.type === "agent_failed"
|
|
2860
|
+
? terminalOutcome(ingressEvent)
|
|
2861
|
+
: undefined;
|
|
2862
|
+
const ingressIsTerminal =
|
|
2863
|
+
(ingressEvent?.type === "agent_end" && ingressOutcome !== undefined) ||
|
|
2864
|
+
(ingressEvent?.type === "agent_failed" && ingressOutcome?.kind === "failed");
|
|
2865
|
+
const ingressCorrelation = ingressEvent ? sdkFrameCorrelation(frame, ingressEvent) : undefined;
|
|
2866
|
+
if (
|
|
2867
|
+
ingressIsTerminal &&
|
|
2868
|
+
record.activePrompt?.acknowledged &&
|
|
2869
|
+
ingressCorrelation &&
|
|
2870
|
+
correlationsExactlyMatch(record.activePrompt.correlation, ingressCorrelation)
|
|
2871
|
+
) {
|
|
2872
|
+
record.activePrompt.terminalReserved = true;
|
|
2873
|
+
clearPromptWatchdog(record.activePrompt);
|
|
2874
|
+
}
|
|
2634
2875
|
++record.inboundSequence;
|
|
2635
|
-
const
|
|
2636
|
-
record.frameTail
|
|
2637
|
-
|
|
2638
|
-
|
|
2876
|
+
const frameGeneration = record.publicationGeneration;
|
|
2877
|
+
const task = record.frameTail.then(async () => await this.#handleSdkFrame(id, adapter, frame, frameGeneration));
|
|
2878
|
+
record.frameTail = task.catch(async error => {
|
|
2879
|
+
if (frameGeneration !== record.publicationGeneration) return;
|
|
2880
|
+
if (record.activePrompt?.terminalReserved) return;
|
|
2881
|
+
await this.#failSession(id, adapter, this.#frameProcessingFailure(error));
|
|
2882
|
+
});
|
|
2639
2883
|
}
|
|
2640
2884
|
|
|
2641
|
-
async #handleSdkFrame(
|
|
2885
|
+
async #handleSdkFrame(
|
|
2886
|
+
id: string,
|
|
2887
|
+
adapter: AcpSdkAdapter,
|
|
2888
|
+
frame: JsonObject,
|
|
2889
|
+
ingressPublicationGeneration?: number,
|
|
2890
|
+
): Promise<void> {
|
|
2642
2891
|
const record = this.#sessions.get(id);
|
|
2643
2892
|
if (!record || record.adapter !== adapter) return;
|
|
2644
2893
|
if ((frame.type === "hello" || frame.type === "server_hello") && typeof frame.connectionId === "string") {
|
|
@@ -2648,19 +2897,18 @@ export class AcpAgent implements Agent {
|
|
|
2648
2897
|
const waiter = record.activePrompt;
|
|
2649
2898
|
if (waiter && !waiter.settled && !waiter.terminal) {
|
|
2650
2899
|
record.activePrompt = undefined;
|
|
2900
|
+
record.busy = record.backgroundBusy;
|
|
2651
2901
|
clearPromptWatchdog(waiter);
|
|
2652
2902
|
waiter.settled = true;
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
while (record.settledPromptCorrelations.length > SETTLED_PROMPT_CORRELATION_RETENTION)
|
|
2656
|
-
record.settledPromptCorrelations.shift();
|
|
2657
|
-
}
|
|
2903
|
+
this.#rememberSettledPromptCorrelation(id, record, waiter.correlation);
|
|
2904
|
+
this.#fenceRetiredPromptAcknowledgement(id, waiter);
|
|
2658
2905
|
waiter.reject(
|
|
2659
2906
|
new AcpSdkAdapterError(
|
|
2660
2907
|
"connection_closed",
|
|
2661
2908
|
"The prompt owner connection was lost before completion.",
|
|
2662
2909
|
),
|
|
2663
2910
|
);
|
|
2911
|
+
this.#flushFailureDiagnostics(id, waiter, record.adapter);
|
|
2664
2912
|
// The turn is settled by the rejection; release the running phase
|
|
2665
2913
|
// best-effort so a cancelled prompt is not left reporting working.
|
|
2666
2914
|
void this.#publishPromptPhaseIdle(id, record.adapter);
|
|
@@ -2671,7 +2919,9 @@ export class AcpAgent implements Agent {
|
|
|
2671
2919
|
const received = receivedSdkEvent(frame);
|
|
2672
2920
|
if (!received) return;
|
|
2673
2921
|
const { event, wirePayload } = received;
|
|
2674
|
-
|
|
2922
|
+
let publicationGeneration = ingressPublicationGeneration ?? record.publicationGeneration;
|
|
2923
|
+
const outcome = event.type === "agent_end" || event.type === "agent_failed" ? terminalOutcome(event) : undefined;
|
|
2924
|
+
const isTerminal = event.type === "agent_end" || (event.type === "agent_failed" && outcome?.kind === "failed");
|
|
2675
2925
|
if (event.type === "notice" && event.source === "autorouting" && typeof event.message === "string") {
|
|
2676
2926
|
record.routingInactiveNotice = event.message;
|
|
2677
2927
|
return;
|
|
@@ -2679,21 +2929,58 @@ export class AcpAgent implements Agent {
|
|
|
2679
2929
|
const derivedCorrelation = sdkFrameCorrelation(frame, event);
|
|
2680
2930
|
const correlation = derivedCorrelation ?? {};
|
|
2681
2931
|
const activePrompt = record.activePrompt;
|
|
2682
|
-
|
|
2932
|
+
let terminalPromptOwner: PromptWaiter | undefined;
|
|
2683
2933
|
const settledCorrelation = record.settledPromptCorrelations.some(settled =>
|
|
2684
2934
|
correlationsMatch(settled, correlation),
|
|
2685
2935
|
);
|
|
2936
|
+
const ownedBackgroundCorrelation = hasCompleteCorrelation(correlation)
|
|
2937
|
+
? record.backgroundCorrelations.find(owner => correlationsExactlyMatch(owner, correlation))
|
|
2938
|
+
: undefined;
|
|
2939
|
+
const ownsBackgroundTerminal =
|
|
2940
|
+
ownedBackgroundCorrelation !== undefined ||
|
|
2941
|
+
(!hasCorrelation(correlation) && record.backgroundAnonymousCount > 0);
|
|
2686
2942
|
if (isTerminal) {
|
|
2943
|
+
if (!activePrompt) {
|
|
2944
|
+
if (!record.backgroundBusy || !ownsBackgroundTerminal || settledCorrelation) return;
|
|
2945
|
+
if (ownedBackgroundCorrelation)
|
|
2946
|
+
record.backgroundCorrelations = record.backgroundCorrelations.filter(
|
|
2947
|
+
owner => !correlationsExactlyMatch(owner, ownedBackgroundCorrelation),
|
|
2948
|
+
);
|
|
2949
|
+
else record.backgroundAnonymousCount--;
|
|
2950
|
+
record.backgroundBusy = record.backgroundAnonymousCount > 0 || record.backgroundCorrelations.length > 0;
|
|
2951
|
+
record.busy = record.backgroundBusy;
|
|
2952
|
+
await this.#publishPromptPhase(id, record.adapter, undefined);
|
|
2953
|
+
return;
|
|
2954
|
+
}
|
|
2955
|
+
const matchesActivePrompt =
|
|
2956
|
+
hasCompleteCorrelation(correlation) && correlationsExactlyMatch(activePrompt.correlation, correlation);
|
|
2957
|
+
if (
|
|
2958
|
+
record.backgroundBusy &&
|
|
2959
|
+
ownsBackgroundTerminal &&
|
|
2960
|
+
!matchesActivePrompt &&
|
|
2961
|
+
!settledCorrelation &&
|
|
2962
|
+
(!hasCorrelation(correlation) || hasCompleteCorrelation(correlation))
|
|
2963
|
+
) {
|
|
2964
|
+
if (ownedBackgroundCorrelation)
|
|
2965
|
+
record.backgroundCorrelations = record.backgroundCorrelations.filter(
|
|
2966
|
+
owner => !correlationsExactlyMatch(owner, ownedBackgroundCorrelation),
|
|
2967
|
+
);
|
|
2968
|
+
else record.backgroundAnonymousCount--;
|
|
2969
|
+
record.backgroundBusy = record.backgroundAnonymousCount > 0 || record.backgroundCorrelations.length > 0;
|
|
2970
|
+
record.busy = true;
|
|
2971
|
+
await this.#publishPromptPhase(id, record.adapter, activePrompt);
|
|
2972
|
+
return;
|
|
2973
|
+
}
|
|
2687
2974
|
// Terminal ownership requires a complete identity. Unowned, partial, and
|
|
2688
2975
|
// duplicate terminals are never allowed to publish or query anything.
|
|
2689
|
-
if (
|
|
2976
|
+
if (activePrompt.settled) return;
|
|
2690
2977
|
if (!hasCompleteCorrelation(correlation)) {
|
|
2691
2978
|
logDroppedPromptTerminal(id, event, "incomplete_correlation", correlation, activePrompt.correlation);
|
|
2692
2979
|
return;
|
|
2693
2980
|
}
|
|
2694
2981
|
if (!activePrompt.acknowledged) {
|
|
2695
2982
|
// Hold the entire frame until the prompt acknowledgement proves ownership.
|
|
2696
|
-
activePrompt.deferredFrames.push(frame);
|
|
2983
|
+
activePrompt.deferredFrames.push({ frame, publicationGeneration });
|
|
2697
2984
|
return;
|
|
2698
2985
|
}
|
|
2699
2986
|
const matchesPrompt = correlationsExactlyMatch(activePrompt.correlation, correlation);
|
|
@@ -2703,26 +2990,30 @@ export class AcpAgent implements Agent {
|
|
|
2703
2990
|
return;
|
|
2704
2991
|
}
|
|
2705
2992
|
if (activePrompt.terminal) return;
|
|
2993
|
+
record.busy = record.backgroundBusy;
|
|
2994
|
+
terminalPromptOwner = activePrompt;
|
|
2995
|
+
this.#advanceTerminalGeneration(record);
|
|
2996
|
+
publicationGeneration = record.publicationGeneration;
|
|
2706
2997
|
if (!outcome) {
|
|
2707
|
-
const detail =
|
|
2708
|
-
typeof (event as { error?: { message?: unknown } }).error?.message === "string"
|
|
2709
|
-
? (event as { error: { message: string } }).error.message
|
|
2710
|
-
: "the prompt terminal omitted a valid normalized outcome";
|
|
2711
2998
|
await this.#rejectPrompt(
|
|
2712
2999
|
record,
|
|
2713
3000
|
id,
|
|
2714
3001
|
activePrompt,
|
|
2715
|
-
new AcpSdkAdapterError("connection_closed",
|
|
3002
|
+
new AcpSdkAdapterError("connection_closed", "ACP prompt terminal was invalid or incomplete."),
|
|
2716
3003
|
);
|
|
2717
3004
|
return;
|
|
2718
3005
|
}
|
|
2719
3006
|
activePrompt.terminal = { outcome, correlation };
|
|
3007
|
+
// Failure diagnostics are useful but advisory. Settle before any mapped
|
|
3008
|
+
// session update can await a backpressured client transport; otherwise an
|
|
3009
|
+
// already-decided failure can still lose to the inactivity watchdog.
|
|
3010
|
+
this.#settlePrompt(id, record, activePrompt);
|
|
2720
3011
|
}
|
|
2721
3012
|
if (!isTerminal && derivedCorrelation === undefined) return;
|
|
2722
3013
|
if (!isTerminal && hasCorrelation(correlation)) {
|
|
2723
3014
|
if (!activePrompt || activePrompt.settled) return;
|
|
2724
3015
|
if (!activePrompt.acknowledged) {
|
|
2725
|
-
activePrompt.deferredFrames.push(frame);
|
|
3016
|
+
activePrompt.deferredFrames.push({ frame, publicationGeneration });
|
|
2726
3017
|
return;
|
|
2727
3018
|
}
|
|
2728
3019
|
if (!correlationsExactlyMatch(activePrompt.correlation, correlation)) return;
|
|
@@ -2731,18 +3022,19 @@ export class AcpAgent implements Agent {
|
|
|
2731
3022
|
// Frames for an already-settled correlation stay closed until an active prompt
|
|
2732
3023
|
// acknowledges the exact same identity.
|
|
2733
3024
|
if (activePrompt && !activePrompt.settled && !activePrompt.acknowledged) {
|
|
2734
|
-
activePrompt.deferredFrames.push(frame);
|
|
3025
|
+
activePrompt.deferredFrames.push({ frame, publicationGeneration });
|
|
2735
3026
|
return;
|
|
2736
3027
|
}
|
|
2737
3028
|
if (!activePrompt || activePrompt.settled || !correlationsMatch(activePrompt.correlation, correlation)) return;
|
|
2738
3029
|
}
|
|
2739
3030
|
const promptOwner =
|
|
2740
|
-
|
|
3031
|
+
terminalPromptOwner ??
|
|
3032
|
+
(activePrompt &&
|
|
2741
3033
|
!activePrompt.settled &&
|
|
2742
3034
|
activePrompt.acknowledged &&
|
|
2743
3035
|
correlationsExactlyMatch(activePrompt.correlation, correlation)
|
|
2744
3036
|
? activePrompt
|
|
2745
|
-
: undefined;
|
|
3037
|
+
: undefined);
|
|
2746
3038
|
const toolCallId = typeof event.toolCallId === "string" ? event.toolCallId : undefined;
|
|
2747
3039
|
if (
|
|
2748
3040
|
toolCallId &&
|
|
@@ -2765,7 +3057,9 @@ export class AcpAgent implements Agent {
|
|
|
2765
3057
|
},
|
|
2766
3058
|
};
|
|
2767
3059
|
const notifications = wirePayload
|
|
2768
|
-
?
|
|
3060
|
+
? event.type === "agent_failed" && !object(event.error)
|
|
3061
|
+
? []
|
|
3062
|
+
: mapAgentWireEventPayloadToAcpSessionUpdates(wirePayload as never, id, mapperOptions)
|
|
2769
3063
|
: event.type === "agent_failed" && object(event.error)
|
|
2770
3064
|
? mapAgentSessionEventToAcpSessionUpdates(event as unknown as AgentSessionEvent, id, mapperOptions)
|
|
2771
3065
|
: [];
|
|
@@ -2776,7 +3070,21 @@ export class AcpAgent implements Agent {
|
|
|
2776
3070
|
notification.update.content.type === "text"
|
|
2777
3071
|
)
|
|
2778
3072
|
promptOwner.emittedAssistantText += notification.update.content.text;
|
|
2779
|
-
|
|
3073
|
+
// The prompt rejection carries the sanitized failure diagnostic. Publishing
|
|
3074
|
+
// a second session update after settlement would be stale as soon as the client
|
|
3075
|
+
// starts a replacement turn, and an in-flight transport write cannot be revoked.
|
|
3076
|
+
if (!(event.type === "agent_failed" && isTerminal))
|
|
3077
|
+
if (event.type === "agent_failed") {
|
|
3078
|
+
const diagnosticOwner = promptOwner ?? record.activePrompt;
|
|
3079
|
+
if (diagnosticOwner) diagnosticOwner.failureDiagnostics.push({ notification, publicationGeneration });
|
|
3080
|
+
else this.#scheduleFailureDiagnostic(id, notification, adapter, publicationGeneration);
|
|
3081
|
+
} else
|
|
3082
|
+
await this.#publishSessionUpdate(
|
|
3083
|
+
id,
|
|
3084
|
+
notification,
|
|
3085
|
+
adapter,
|
|
3086
|
+
promptOwner ? publicationGeneration : undefined,
|
|
3087
|
+
);
|
|
2780
3088
|
}
|
|
2781
3089
|
if (toolCallId && event.type === "tool_execution_end") record.toolArgs.delete(toolCallId);
|
|
2782
3090
|
if (event.type === "agent_start") {
|
|
@@ -2791,15 +3099,64 @@ export class AcpAgent implements Agent {
|
|
|
2791
3099
|
},
|
|
2792
3100
|
},
|
|
2793
3101
|
adapter,
|
|
3102
|
+
promptOwner ? publicationGeneration : undefined,
|
|
2794
3103
|
);
|
|
2795
3104
|
}
|
|
2796
3105
|
if (event.type === "message_end" && object(event.message)?.role === "assistant") {
|
|
2797
3106
|
if (promptOwner) promptOwner.messageProgress = undefined;
|
|
2798
3107
|
else record.sessionMessageProgress = undefined;
|
|
2799
3108
|
}
|
|
2800
|
-
if (event
|
|
2801
|
-
|
|
2802
|
-
|
|
3109
|
+
if (isTerminal) this.#scheduleTerminalUpdates(id, adapter, publicationGeneration, event, promptOwner);
|
|
3110
|
+
}
|
|
3111
|
+
|
|
3112
|
+
#scheduleFailureDiagnostic(
|
|
3113
|
+
id: string,
|
|
3114
|
+
notification: SessionNotification,
|
|
3115
|
+
adapter: AcpSdkAdapter,
|
|
3116
|
+
publicationGeneration: number,
|
|
3117
|
+
): void {
|
|
3118
|
+
const prior = this.#failureDiagnosticTails.get(id) ?? Promise.resolve();
|
|
3119
|
+
const task = prior.then(async () => {
|
|
3120
|
+
await Bun.sleep(0);
|
|
3121
|
+
const owner = this.#sessions.get(id);
|
|
3122
|
+
if (!owner || owner.adapter !== adapter) return;
|
|
3123
|
+
await this.#connection.sessionUpdate(notification);
|
|
3124
|
+
const finalTextTail = this.#finalTextTails.get(id);
|
|
3125
|
+
if (finalTextTail) await finalTextTail;
|
|
3126
|
+
const current = this.#sessions.get(id);
|
|
3127
|
+
if (current) await this.#publishPromptPhase(id, current.adapter, this.#promptPhaseOwner(current));
|
|
3128
|
+
});
|
|
3129
|
+
let tail: Promise<void>;
|
|
3130
|
+
tail = task
|
|
3131
|
+
.catch(error => {
|
|
3132
|
+
logger.warn("acp_failure_diagnostic_update_failed", { sessionId: id, error: String(error) });
|
|
3133
|
+
})
|
|
3134
|
+
.finally(() => {
|
|
3135
|
+
if (this.#failureDiagnosticTails.get(id) === tail) this.#failureDiagnosticTails.delete(id);
|
|
3136
|
+
});
|
|
3137
|
+
this.#failureDiagnosticTails.set(id, tail);
|
|
3138
|
+
}
|
|
3139
|
+
|
|
3140
|
+
#flushFailureDiagnostics(id: string, waiter: PromptWaiter, adapter: AcpSdkAdapter): void {
|
|
3141
|
+
for (const { notification, publicationGeneration } of waiter.failureDiagnostics)
|
|
3142
|
+
this.#scheduleFailureDiagnostic(id, notification, adapter, publicationGeneration);
|
|
3143
|
+
waiter.failureDiagnostics.length = 0;
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
#scheduleTerminalUpdates(
|
|
3147
|
+
id: string,
|
|
3148
|
+
adapter: AcpSdkAdapter,
|
|
3149
|
+
publicationGeneration: number,
|
|
3150
|
+
event: JsonObject,
|
|
3151
|
+
promptOwner: PromptWaiter | undefined,
|
|
3152
|
+
): void {
|
|
3153
|
+
const failedTerminal = event.type === "agent_failed";
|
|
3154
|
+
if (promptOwner) this.#flushFailureDiagnostics(id, promptOwner, adapter);
|
|
3155
|
+
let decorationStart = Promise.resolve();
|
|
3156
|
+
const finalText = typeof event.finalText === "string" ? event.finalText : "";
|
|
3157
|
+
if (promptOwner && finalText) {
|
|
3158
|
+
const finalTextTask = (async () => {
|
|
3159
|
+
await Bun.sleep(0);
|
|
2803
3160
|
const resolution = resolveAcpFinalText(promptOwner.emittedAssistantText, finalText);
|
|
2804
3161
|
if (resolution.kind === "emit") {
|
|
2805
3162
|
promptOwner.emittedAssistantText += resolution.text;
|
|
@@ -2814,6 +3171,7 @@ export class AcpAgent implements Agent {
|
|
|
2814
3171
|
},
|
|
2815
3172
|
},
|
|
2816
3173
|
adapter,
|
|
3174
|
+
publicationGeneration,
|
|
2817
3175
|
);
|
|
2818
3176
|
} else if (resolution.kind === "divergent") {
|
|
2819
3177
|
logger.warn("acp_final_text_diverged", {
|
|
@@ -2824,19 +3182,27 @@ export class AcpAgent implements Agent {
|
|
|
2824
3182
|
finalLength: resolution.final.text.length,
|
|
2825
3183
|
});
|
|
2826
3184
|
}
|
|
2827
|
-
}
|
|
3185
|
+
})();
|
|
3186
|
+
let finalTextTail: Promise<void>;
|
|
3187
|
+
finalTextTail = finalTextTask
|
|
3188
|
+
.catch(error => {
|
|
3189
|
+
logger.warn("acp_terminal_update_failed", { sessionId: id, error: String(error) });
|
|
3190
|
+
})
|
|
3191
|
+
.finally(() => {
|
|
3192
|
+
if (this.#finalTextTails.get(id) === finalTextTail) this.#finalTextTails.delete(id);
|
|
3193
|
+
});
|
|
3194
|
+
this.#finalTextTails.set(id, finalTextTail);
|
|
3195
|
+
decorationStart = finalTextTail;
|
|
2828
3196
|
}
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
// title, and idle integration updates.
|
|
2839
|
-
if (event.type === "agent_end") await this.#emitEndOfTurnUpdates(id, adapter);
|
|
3197
|
+
if (failedTerminal) {
|
|
3198
|
+
void decorationStart.then(async () => await this.#publishPromptPhaseIdle(id, adapter));
|
|
3199
|
+
return;
|
|
3200
|
+
}
|
|
3201
|
+
void decorationStart
|
|
3202
|
+
.then(async () => await this.#emitEndOfTurnUpdates(id, adapter, publicationGeneration))
|
|
3203
|
+
.catch(error => {
|
|
3204
|
+
logger.warn("acp_terminal_update_failed", { sessionId: id, error: String(error) });
|
|
3205
|
+
});
|
|
2840
3206
|
}
|
|
2841
3207
|
|
|
2842
3208
|
async #rejectPrompt(
|
|
@@ -2844,26 +3210,26 @@ export class AcpAgent implements Agent {
|
|
|
2844
3210
|
id: string,
|
|
2845
3211
|
waiter: PromptWaiter,
|
|
2846
3212
|
error: AcpSdkAdapterError,
|
|
3213
|
+
publishIdle = true,
|
|
2847
3214
|
): Promise<void> {
|
|
2848
3215
|
if (record.activePrompt !== waiter || waiter.settled) return;
|
|
2849
3216
|
record.activePrompt = undefined;
|
|
3217
|
+
if (!waiter.acknowledged) record.busy = record.backgroundBusy;
|
|
2850
3218
|
clearPromptWatchdog(waiter);
|
|
2851
3219
|
waiter.settled = true;
|
|
3220
|
+
this.#fenceRetiredPromptAcknowledgement(id, waiter);
|
|
2852
3221
|
waiter.deferredFrames.length = 0;
|
|
2853
3222
|
waiter.deferredActivityFrames.length = 0;
|
|
2854
3223
|
waiter.terminal = undefined;
|
|
2855
|
-
|
|
2856
|
-
record.settledPromptCorrelations.push(waiter.correlation);
|
|
2857
|
-
while (record.settledPromptCorrelations.length > SETTLED_PROMPT_CORRELATION_RETENTION)
|
|
2858
|
-
record.settledPromptCorrelations.shift();
|
|
2859
|
-
}
|
|
3224
|
+
this.#rememberSettledPromptCorrelation(id, record, waiter.correlation);
|
|
2860
3225
|
// The turn is over even though it ended badly, so the client's running phase has
|
|
2861
3226
|
// to be released. Rejection is the settlement; the advisory idle publication must
|
|
2862
3227
|
// never gate it (a backpressured transport would otherwise hold the rejection
|
|
2863
3228
|
// forever). Skipping the idle publish entirely is what left a client composer
|
|
2864
3229
|
// spinning on a turn that already produced its terminal.
|
|
2865
3230
|
waiter.reject(error);
|
|
2866
|
-
|
|
3231
|
+
this.#flushFailureDiagnostics(id, waiter, record.adapter);
|
|
3232
|
+
if (publishIdle) void this.#publishPromptPhaseIdle(id, record.adapter);
|
|
2867
3233
|
}
|
|
2868
3234
|
|
|
2869
3235
|
/**
|
|
@@ -2878,20 +3244,62 @@ export class AcpAgent implements Agent {
|
|
|
2878
3244
|
async #publishPromptPhaseIdle(id: string, adapter: AcpSdkAdapter): Promise<void> {
|
|
2879
3245
|
const record = this.#sessions.get(id);
|
|
2880
3246
|
if (!record || record.adapter !== adapter) return;
|
|
3247
|
+
await this.#publishPromptPhase(id, adapter, this.#promptPhaseOwner(record));
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
#promptPhaseOwner(record: SessionRecord): PromptPhaseOwner {
|
|
3251
|
+
return record.activePrompt ?? (record.busy ? "background" : undefined);
|
|
3252
|
+
}
|
|
3253
|
+
|
|
3254
|
+
async #publishPromptPhase(id: string, adapter: AcpSdkAdapter, observedPrompt: PromptPhaseOwner): Promise<void> {
|
|
3255
|
+
const pending = this.#promptPhaseTails.get(id);
|
|
3256
|
+
if (pending) {
|
|
3257
|
+
await pending;
|
|
3258
|
+
const current = this.#sessions.get(id);
|
|
3259
|
+
if (current) await this.#publishPromptPhase(id, current.adapter, this.#promptPhaseOwner(current));
|
|
3260
|
+
return;
|
|
3261
|
+
}
|
|
3262
|
+
const task = this.#runPromptPhase(id, adapter, observedPrompt);
|
|
3263
|
+
let tail: Promise<void>;
|
|
3264
|
+
tail = task.finally(() => {
|
|
3265
|
+
if (this.#promptPhaseTails.get(id) === tail) this.#promptPhaseTails.delete(id);
|
|
3266
|
+
});
|
|
3267
|
+
this.#promptPhaseTails.set(id, tail);
|
|
3268
|
+
await tail;
|
|
3269
|
+
}
|
|
3270
|
+
|
|
3271
|
+
async #runPromptPhase(id: string, adapter: AcpSdkAdapter, observedPrompt: PromptPhaseOwner): Promise<void> {
|
|
2881
3272
|
try {
|
|
2882
|
-
await
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
3273
|
+
await Bun.sleep(0);
|
|
3274
|
+
for (;;) {
|
|
3275
|
+
await this.#connection.sessionUpdate({
|
|
3276
|
+
sessionId: id,
|
|
3277
|
+
update: {
|
|
3278
|
+
sessionUpdate: "session_info_update",
|
|
3279
|
+
...(observedPrompt ? { updatedAt: new Date().toISOString() } : {}),
|
|
3280
|
+
_meta: observedPrompt
|
|
3281
|
+
? { gjcPhase: "working", running: true, gjcRunning: true }
|
|
3282
|
+
: { gjcPhase: "idle", running: false, gjcRunning: false },
|
|
3283
|
+
},
|
|
3284
|
+
});
|
|
3285
|
+
// A prompt may start or finish while any phase write is backpressured.
|
|
3286
|
+
// Continue until the last published phase describes the current waiter.
|
|
3287
|
+
const current = this.#sessions.get(id);
|
|
3288
|
+
if (!current) return;
|
|
3289
|
+
if (current.adapter !== adapter) {
|
|
3290
|
+
void this.#publishPromptPhase(id, current.adapter, this.#promptPhaseOwner(current));
|
|
3291
|
+
return;
|
|
3292
|
+
}
|
|
3293
|
+
const currentOwner = this.#promptPhaseOwner(current);
|
|
3294
|
+
if (currentOwner === observedPrompt) return;
|
|
3295
|
+
observedPrompt = currentOwner;
|
|
3296
|
+
}
|
|
2889
3297
|
} catch {
|
|
2890
3298
|
// The client transport is gone; there is no phase left to restore.
|
|
2891
3299
|
}
|
|
2892
3300
|
}
|
|
2893
3301
|
|
|
2894
|
-
#settlePrompt(record: SessionRecord, waiter: PromptWaiter): void {
|
|
3302
|
+
#settlePrompt(id: string, record: SessionRecord, waiter: PromptWaiter): void {
|
|
2895
3303
|
if (record.activePrompt !== waiter || waiter.settled || !waiter.acknowledged || !waiter.terminal) return;
|
|
2896
3304
|
// A terminal captured before acknowledgement is only this prompt's terminal when the
|
|
2897
3305
|
// eventual acknowledgement correlates with it; otherwise it belonged to an earlier prompt.
|
|
@@ -2902,12 +3310,8 @@ export class AcpAgent implements Agent {
|
|
|
2902
3310
|
record.activePrompt = undefined;
|
|
2903
3311
|
clearPromptWatchdog(waiter);
|
|
2904
3312
|
waiter.settled = true;
|
|
2905
|
-
if (hasCorrelation(waiter.correlation)) {
|
|
2906
|
-
record.settledPromptCorrelations.push(waiter.correlation);
|
|
2907
|
-
while (record.settledPromptCorrelations.length > SETTLED_PROMPT_CORRELATION_RETENTION)
|
|
2908
|
-
record.settledPromptCorrelations.shift();
|
|
2909
|
-
}
|
|
2910
3313
|
const { outcome } = waiter.terminal;
|
|
3314
|
+
this.#rememberSettledPromptCorrelation(id, record, waiter.correlation);
|
|
2911
3315
|
if (outcome.kind === "stopped") {
|
|
2912
3316
|
waiter.resolve({ stopReason: outcome.reason });
|
|
2913
3317
|
return;
|
|
@@ -2915,7 +3319,7 @@ export class AcpAgent implements Agent {
|
|
|
2915
3319
|
waiter.reject(new AcpSdkAdapterError(outcome.code, outcome.message));
|
|
2916
3320
|
}
|
|
2917
3321
|
|
|
2918
|
-
async #emitEndOfTurnUpdates(id: string, adapter: AcpSdkAdapter): Promise<void> {
|
|
3322
|
+
async #emitEndOfTurnUpdates(id: string, adapter: AcpSdkAdapter, publicationGeneration: number): Promise<void> {
|
|
2919
3323
|
let usage: JsonObject | undefined;
|
|
2920
3324
|
let title: string | undefined;
|
|
2921
3325
|
try {
|
|
@@ -2936,42 +3340,70 @@ export class AcpAgent implements Agent {
|
|
|
2936
3340
|
}
|
|
2937
3341
|
// The prompt settled before these queries were asked, so a host that answers late
|
|
2938
3342
|
// must not report the session idle after the next turn has already started.
|
|
2939
|
-
|
|
2940
|
-
|
|
3343
|
+
const ownsCurrentGeneration = (): boolean => {
|
|
3344
|
+
const current = this.#sessions.get(id);
|
|
3345
|
+
return Boolean(
|
|
3346
|
+
current &&
|
|
3347
|
+
current.adapter === adapter &&
|
|
3348
|
+
current.publicationGeneration === publicationGeneration &&
|
|
3349
|
+
!current.activePrompt &&
|
|
3350
|
+
!current.busy,
|
|
3351
|
+
);
|
|
3352
|
+
};
|
|
3353
|
+
if (!ownsCurrentGeneration()) return;
|
|
3354
|
+
const record = this.#sessions.get(id);
|
|
3355
|
+
if (!record || record.adapter !== adapter) return;
|
|
3356
|
+
const observedPhaseOwner = this.#promptPhaseOwner(record);
|
|
3357
|
+
const publishTask = (async () => {
|
|
3358
|
+
if (typeof usage?.tokens === "number" && typeof usage.contextWindow === "number") {
|
|
3359
|
+
await this.#publishSessionUpdate(
|
|
3360
|
+
id,
|
|
3361
|
+
{
|
|
3362
|
+
sessionId: id,
|
|
3363
|
+
update: {
|
|
3364
|
+
sessionUpdate: "usage_update",
|
|
3365
|
+
size: usage.contextWindow,
|
|
3366
|
+
used: usage.tokens,
|
|
3367
|
+
},
|
|
3368
|
+
},
|
|
3369
|
+
adapter,
|
|
3370
|
+
publicationGeneration,
|
|
3371
|
+
);
|
|
3372
|
+
}
|
|
3373
|
+
if (!ownsCurrentGeneration()) return;
|
|
3374
|
+
const updatedAt = new Date().toISOString();
|
|
3375
|
+
this.#knownSessionMetadata.set(id, { ...(title ? { title } : {}), updatedAt });
|
|
2941
3376
|
await this.#publishSessionUpdate(
|
|
2942
3377
|
id,
|
|
2943
3378
|
{
|
|
2944
3379
|
sessionId: id,
|
|
2945
3380
|
update: {
|
|
2946
|
-
sessionUpdate: "
|
|
2947
|
-
|
|
2948
|
-
|
|
3381
|
+
sessionUpdate: "session_info_update",
|
|
3382
|
+
...(title ? { title } : {}),
|
|
3383
|
+
updatedAt,
|
|
3384
|
+
_meta: { gjcPhase: "idle", running: false, gjcRunning: false },
|
|
2949
3385
|
},
|
|
2950
3386
|
},
|
|
2951
3387
|
adapter,
|
|
3388
|
+
publicationGeneration,
|
|
2952
3389
|
);
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
updatedAt,
|
|
2964
|
-
_meta: { gjcPhase: "idle", running: false, gjcRunning: false },
|
|
2965
|
-
},
|
|
2966
|
-
},
|
|
2967
|
-
adapter,
|
|
2968
|
-
);
|
|
3390
|
+
const current = this.#sessions.get(id);
|
|
3391
|
+
if (current && this.#promptPhaseOwner(current) !== observedPhaseOwner)
|
|
3392
|
+
await this.#publishPromptPhase(id, current.adapter, this.#promptPhaseOwner(current));
|
|
3393
|
+
})();
|
|
3394
|
+
let metadataTail: Promise<void>;
|
|
3395
|
+
metadataTail = publishTask.finally(() => {
|
|
3396
|
+
if (this.#terminalMetadataTails.get(id) === metadataTail) this.#terminalMetadataTails.delete(id);
|
|
3397
|
+
});
|
|
3398
|
+
this.#terminalMetadataTails.set(id, metadataTail);
|
|
3399
|
+
await metadataTail;
|
|
2969
3400
|
}
|
|
2970
3401
|
|
|
2971
3402
|
async #publishSessionUpdate(
|
|
2972
3403
|
id: string,
|
|
2973
3404
|
notification: SessionNotification,
|
|
2974
3405
|
expectedAdapter?: AcpSdkAdapter,
|
|
3406
|
+
publicationGeneration?: number,
|
|
2975
3407
|
): Promise<void> {
|
|
2976
3408
|
// A session that is gone has no update channel, so this is a drop and not a failure:
|
|
2977
3409
|
// whoever owned the frame is the one that ended the session. Callers whose bookkeeping
|
|
@@ -2980,7 +3412,27 @@ export class AcpAgent implements Agent {
|
|
|
2980
3412
|
if (!record || (expectedAdapter && record.adapter !== expectedAdapter)) return;
|
|
2981
3413
|
try {
|
|
2982
3414
|
await this.#connection.sessionUpdate(notification);
|
|
3415
|
+
const current = this.#sessions.get(id);
|
|
3416
|
+
if (
|
|
3417
|
+
current &&
|
|
3418
|
+
((expectedAdapter && current.adapter !== expectedAdapter) ||
|
|
3419
|
+
(publicationGeneration !== undefined && publicationGeneration !== current.publicationGeneration))
|
|
3420
|
+
) {
|
|
3421
|
+
void this.#publishPromptPhase(id, current.adapter, this.#promptPhaseOwner(current));
|
|
3422
|
+
}
|
|
2983
3423
|
} catch (error) {
|
|
3424
|
+
// A validated terminal can retire this publication while it is blocked in
|
|
3425
|
+
// the client transport. Its failure belongs to the drained prompt and must
|
|
3426
|
+
// not tear down a successor that has since taken session ownership.
|
|
3427
|
+
const current = this.#sessions.get(id);
|
|
3428
|
+
if (
|
|
3429
|
+
publicationGeneration !== undefined &&
|
|
3430
|
+
current &&
|
|
3431
|
+
current.adapter === record.adapter &&
|
|
3432
|
+
current.activePrompt?.terminalReserved
|
|
3433
|
+
)
|
|
3434
|
+
return;
|
|
3435
|
+
if (publicationGeneration !== undefined && publicationGeneration !== record.publicationGeneration) return;
|
|
2984
3436
|
const failure = this.#frameProcessingFailure(error);
|
|
2985
3437
|
await this.#failSession(id, record.adapter, failure);
|
|
2986
3438
|
throw failure;
|
|
@@ -3516,17 +3968,8 @@ export class AcpAgent implements Agent {
|
|
|
3516
3968
|
record.adapter,
|
|
3517
3969
|
);
|
|
3518
3970
|
}
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
{
|
|
3522
|
-
sessionId: id,
|
|
3523
|
-
update: {
|
|
3524
|
-
sessionUpdate: "session_info_update",
|
|
3525
|
-
_meta: { gjcPhase: "idle", running: false, gjcRunning: false },
|
|
3526
|
-
},
|
|
3527
|
-
},
|
|
3528
|
-
record.adapter,
|
|
3529
|
-
);
|
|
3971
|
+
const current = this.#sessions.get(id);
|
|
3972
|
+
if (current) await this.#publishPromptPhase(id, current.adapter, this.#promptPhaseOwner(current));
|
|
3530
3973
|
})().catch(() => undefined);
|
|
3531
3974
|
});
|
|
3532
3975
|
}
|
|
@@ -3685,6 +4128,12 @@ export class AcpAgent implements Agent {
|
|
|
3685
4128
|
this.#ownedSessionIds.clear();
|
|
3686
4129
|
this.#knownSessionMcpServers.clear();
|
|
3687
4130
|
this.#knownSessionMetadata.clear();
|
|
4131
|
+
this.#retiredPromptCorrelations.clear();
|
|
4132
|
+
this.#retiredPromptAcknowledgements.clear();
|
|
4133
|
+
this.#terminalMetadataTails.clear();
|
|
4134
|
+
this.#finalTextTails.clear();
|
|
4135
|
+
this.#failureDiagnosticTails.clear();
|
|
4136
|
+
this.#promptPhaseTails.clear();
|
|
3688
4137
|
this.#pendingDeleteLocators.clear();
|
|
3689
4138
|
this.#pendingCloseIdempotencyKeys.clear();
|
|
3690
4139
|
if (this.#lifecycleOperations.size === 0) this.#lifecycleOperations.clear();
|