@gajae-code/coding-agent 0.15.5 → 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 +85 -1
- package/dist/types/async/job-manager.d.ts +83 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
- package/dist/types/cli/model-presets-cli.d.ts +9 -0
- package/dist/types/cli/root-flags.d.ts +14 -0
- package/dist/types/cli/update-cli.d.ts +15 -0
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/commands/auth-gateway.d.ts +3 -0
- package/dist/types/commands/gc.d.ts +13 -0
- package/dist/types/commands/launch.d.ts +10 -0
- package/dist/types/commands/model-presets.d.ts +21 -0
- package/dist/types/config/autorouting-tier-map.d.ts +120 -0
- package/dist/types/config/file-lock.d.ts +54 -2
- package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
- package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +3 -2
- package/dist/types/config/model-preset-registry.d.ts +368 -0
- package/dist/types/config/model-profile-activation.d.ts +39 -0
- package/dist/types/config/model-profiles.d.ts +5 -3
- package/dist/types/config/model-registry.d.ts +17 -5
- package/dist/types/config/settings-schema.d.ts +19 -1
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
- package/dist/types/coordinator-mcp/projection-scan.d.ts +54 -0
- package/dist/types/crash/record-loader.d.ts +15 -0
- package/dist/types/extensibility/extensions/types.d.ts +2 -5
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -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-lock.d.ts +35 -6
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +72 -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/index.d.ts +1 -0
- package/dist/types/main.d.ts +3 -1
- 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/jobs-overlay-model.d.ts +4 -2
- 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/jobs-observer.d.ts +36 -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 +80 -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/kind-aware-reconciliation.d.ts +3 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -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/client/discovery.d.ts +1 -0
- 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/model-pin.d.ts +7 -2
- package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +67 -1
- 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/prompt-deadline-manager.d.ts +11 -1
- package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +11 -0
- package/dist/types/sdk/session.d.ts +16 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -1
- package/dist/types/session/account-inventory.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +24 -7
- package/dist/types/session/credential-store-errors.d.ts +1 -0
- package/dist/types/session/fold-coordinator.d.ts +201 -0
- package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
- package/dist/types/session/sdk-run-capability.d.ts +2 -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/telemetry/control.d.ts +11 -0
- package/dist/types/telemetry/events.d.ts +20 -0
- package/dist/types/telemetry/index.d.ts +3 -0
- package/dist/types/telemetry/transport.d.ts +22 -0
- package/dist/types/tools/bash-interactive.d.ts +23 -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 +10 -5
- package/dist/types/web/search/providers/base.d.ts +9 -4
- package/dist/types/web/search/providers/xai.d.ts +2 -1
- package/dist/types/web/search/types.d.ts +14 -0
- package/package.json +10 -7
- package/scripts/generate-hotkeys-docs.ts +4 -0
- package/scripts/generate-sdk-operation-inventory.ts +9 -3
- package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
- package/src/async/job-manager.ts +671 -39
- package/src/cli/accounts-cli.ts +9 -5
- package/src/cli/args.ts +17 -0
- package/src/cli/auth-broker-cli.ts +40 -12
- package/src/cli/auth-gateway-cli.ts +273 -97
- package/src/cli/config-cli.ts +2 -2
- package/src/cli/fast-help.ts +4 -0
- package/src/cli/model-presets-cli.ts +100 -0
- package/src/cli/root-flags.ts +5 -0
- package/src/cli/update-cli.ts +117 -16
- package/src/cli.ts +6 -2
- package/src/commands/auth-gateway.ts +12 -8
- package/src/commands/gc.ts +19 -0
- package/src/commands/launch.ts +86 -18
- package/src/commands/model-presets.ts +24 -0
- package/src/commands/sdk.ts +113 -21
- package/src/config/autorouting-tier-map.ts +44 -0
- package/src/config/file-lock-gc.ts +9 -4
- package/src/config/file-lock.ts +890 -99
- package/src/config/internal/model-preset-registry-test-state.ts +28 -0
- package/src/config/internal/model-preset-registry-test-support.ts +11 -0
- package/src/config/keybindings.ts +6 -2
- package/src/config/model-preset-registry.ts +2795 -0
- package/src/config/model-profile-activation.ts +115 -28
- package/src/config/model-profile-contract.ts +4 -1
- package/src/config/model-profiles.ts +56 -14
- package/src/config/model-registry.ts +1044 -122
- package/src/config/model-resolver.ts +5 -0
- package/src/config/settings-schema.ts +19 -1
- package/src/config/settings.ts +11 -1
- package/src/coordinator-mcp/model-preset.ts +32 -17
- package/src/coordinator-mcp/projection-scan.ts +592 -0
- package/src/coordinator-mcp/question-state.ts +9 -11
- package/src/coordinator-mcp/server.ts +111 -42
- package/src/coordinator-mcp/session-reaper.ts +9 -3
- package/src/crash/index-store.ts +2 -0
- package/src/crash/record-loader.ts +10 -0
- package/src/extensibility/extensions/loader.ts +0 -1
- package/src/extensibility/extensions/types.ts +2 -3
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
- package/src/gjc-runtime/empty-delete-gc.ts +301 -0
- package/src/gjc-runtime/gc-render.ts +29 -0
- package/src/gjc-runtime/gc-runtime.ts +174 -11
- 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 +1321 -147
- package/src/gjc-runtime/session-state-sidecar.ts +1004 -25
- 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/index.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/internal-urls/issue-pr-protocol.ts +4 -2
- package/src/lsp/config.ts +4 -3
- package/src/lsp/path-trust.ts +12 -6
- package/src/main.ts +66 -4
- 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 +616 -161
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
- package/src/modes/components/jobs-overlay-model.ts +99 -16
- package/src/modes/components/model-selector.ts +158 -18
- 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 +29 -10
- package/src/modes/components/tool-status-header.ts +151 -15
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +51 -6
- package/src/modes/controllers/selector-controller.ts +191 -96
- package/src/modes/interactive-mode.ts +14 -1
- package/src/modes/jobs-observer.ts +149 -16
- package/src/modes/print-mode.ts +64 -0
- package/src/modes/runtime-init.ts +1 -1
- package/src/modes/ui-language.ts +49 -2
- package/src/modes/utils/injected-user-submission.ts +15 -1
- package/src/prompts/system/master-mode.md +28 -0
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/fold-receipt.md +3 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +17 -2
- package/src/sdk/broker/broker.ts +1671 -34
- 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 +217 -46
- 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 +46 -5
- package/src/sdk/bus/index.ts +51 -4
- package/src/sdk/bus/kind-aware-reconciliation.ts +36 -6
- package/src/sdk/bus/prompt-reconciliation.ts +28 -4
- package/src/sdk/bus/slack-conversation.ts +5 -1
- 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 +276 -6
- package/src/sdk/client/client.ts +14 -8
- package/src/sdk/client/discovery.ts +3 -1
- package/src/sdk/host/control/dispatch.ts +101 -37
- 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/model-pin.ts +185 -25
- package/src/sdk/host/sdk-run-capability.ts +1 -0
- package/src/sdk/host/session-runtime.test.ts +581 -43
- package/src/sdk/host/session-runtime.ts +655 -101
- 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/prompt-deadline-manager.ts +16 -1
- package/src/sdk/protocol/adapter-validation.ts +27 -0
- package/src/sdk/protocol/operation-inventory.generated.json +53 -2
- package/src/sdk/protocol/operation-registry.ts +17 -4
- package/src/sdk/router/session-router.ts +294 -154
- package/src/sdk/session.ts +440 -167
- package/src/sdk/startup-capability.ts +1 -0
- package/src/session/account-inventory.ts +27 -11
- package/src/session/agent-session.ts +1292 -180
- package/src/session/blob-store.ts +15 -5
- package/src/session/credential-store-errors.ts +2 -0
- package/src/session/fold-coordinator.ts +433 -0
- package/src/session/internal/managed-session-storage.ts +153 -78
- package/src/session/sdk-run-capability-internal.ts +15 -0
- package/src/session/sdk-run-capability.ts +2 -0
- package/src/session/session-manager.ts +342 -28
- package/src/session/yield-queue.ts +24 -10
- package/src/setup/frictionless-onboarding.ts +2 -0
- package/src/setup/provider-presets.json +1 -1
- package/src/slash-commands/helpers/credential-switch.ts +6 -1
- package/src/task/executor.ts +19 -3
- package/src/telemetry/control.ts +26 -0
- package/src/telemetry/events.ts +1268 -0
- package/src/telemetry/index.ts +3 -0
- package/src/telemetry/transport.ts +87 -0
- package/src/tools/bash-interactive.ts +182 -85
- package/src/tools/bash.ts +1028 -109
- package/src/tools/gh.ts +104 -48
- package/src/tools/github-cache.ts +2 -2
- package/src/tools/index.ts +11 -5
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/web/search/provider.ts +120 -11
- package/src/web/search/providers/anthropic.ts +89 -17
- package/src/web/search/providers/base.ts +9 -4
- package/src/web/search/providers/gemini.ts +87 -15
- package/src/web/search/providers/openai-compatible.ts +23 -8
- package/src/web/search/providers/xai.ts +98 -12
- package/src/web/search/types.ts +16 -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,11 +49,13 @@ 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";
|
|
55
56
|
import type { SdkPromptTerminalOutcome } from "../../sdk/prompt-status";
|
|
56
57
|
import { PromptActivity, type PromptWatchdogClock, systemPromptWatchdogClock } from "../../sdk/prompt-watchdog";
|
|
58
|
+
import { validateRequiredPromptText } from "../../sdk/protocol/adapter-validation";
|
|
57
59
|
import { type SessionAttachment, SessionRouter, type SessionRouterFrame } from "../../sdk/router";
|
|
58
60
|
import { SessionListTraversalError, sessionListPageFromResponse, traverseSessionList } from "../../sdk/session-list";
|
|
59
61
|
import { resolveAcpAbortScope } from "./abort-scope";
|
|
@@ -105,6 +107,8 @@ interface PromptWaiter {
|
|
|
105
107
|
acknowledged: boolean;
|
|
106
108
|
/** True once turn.prompt / skill.invoke has been sent; cancel must not fake-settle after this. */
|
|
107
109
|
dispatched: boolean;
|
|
110
|
+
/** True only while the dispatched control request can still reveal its correlation. */
|
|
111
|
+
acknowledgementPending: boolean;
|
|
108
112
|
|
|
109
113
|
/** Highest inbound frame sequence already observed when the prompt was acknowledged. */
|
|
110
114
|
boundary: number;
|
|
@@ -113,8 +117,12 @@ interface PromptWaiter {
|
|
|
113
117
|
emittedAssistantText: string;
|
|
114
118
|
settled: boolean;
|
|
115
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 }>;
|
|
116
124
|
/** Frames for an already-settled correlation held until acknowledgement resolves ownership. */
|
|
117
|
-
deferredFrames: JsonObject
|
|
125
|
+
deferredFrames: Array<{ frame: JsonObject; publicationGeneration: number }>;
|
|
118
126
|
/** Activity frames held until acknowledgement establishes exact prompt ownership. */
|
|
119
127
|
deferredActivityFrames: JsonObject[];
|
|
120
128
|
/** Clock reading of the last frame proven to belong to this prompt; watchdog silence baseline. */
|
|
@@ -143,6 +151,7 @@ interface PromptWaiter {
|
|
|
143
151
|
}
|
|
144
152
|
|
|
145
153
|
type PromptCorrelation = { commandId?: string; turnId?: string };
|
|
154
|
+
type PromptPhaseOwner = PromptWaiter | "background" | undefined;
|
|
146
155
|
|
|
147
156
|
type BrokerConnection = { adapter: AcpSdkAdapter; client: SdkClient };
|
|
148
157
|
type PendingAttachment = { epoch: number; task: Promise<void> };
|
|
@@ -156,10 +165,15 @@ type SessionRecord = {
|
|
|
156
165
|
reconnectUnsubscribe: () => void;
|
|
157
166
|
/** Per-session frame work queue; callbacks never race prompt ownership. */
|
|
158
167
|
frameTail: Promise<void>;
|
|
168
|
+
/** Advances when an owned terminal retires all earlier frame publications. */
|
|
169
|
+
publicationGeneration: number;
|
|
159
170
|
/** Monotonic at WebSocket ingress, before queued work begins. */
|
|
160
171
|
inboundSequence: number;
|
|
161
172
|
/** Updated at ingress so a prompt acknowledgement can distinguish a steer from a fresh turn. */
|
|
162
173
|
busy: boolean;
|
|
174
|
+
backgroundBusy: boolean;
|
|
175
|
+
backgroundAnonymousCount: number;
|
|
176
|
+
backgroundCorrelations: PromptCorrelation[];
|
|
163
177
|
/** Start/update args retained because tool_execution_end does not carry them. */
|
|
164
178
|
toolArgs: Map<string, unknown>;
|
|
165
179
|
/** Message projection state for correlationless session-scoped assistant events. */
|
|
@@ -182,7 +196,7 @@ function promptWaiterRetired(record: SessionRecord, waiter: PromptWaiter): boole
|
|
|
182
196
|
|
|
183
197
|
type BrokerSession = {
|
|
184
198
|
sessionId: string;
|
|
185
|
-
locator?: {
|
|
199
|
+
locator?: { cwd?: string; worktreeRoot?: string | null; stateRoot?: string };
|
|
186
200
|
live?: boolean;
|
|
187
201
|
endpointGeneration?: number;
|
|
188
202
|
endpointMtimeMs?: number;
|
|
@@ -272,13 +286,14 @@ export function paginateAcpSessions(
|
|
|
272
286
|
offset: number,
|
|
273
287
|
sessionMetadata: ReadonlyMap<string, { title?: string; updatedAt?: string }> = new Map(),
|
|
274
288
|
): ListSessionsResponse {
|
|
289
|
+
const canonicalCwd = cwd === undefined ? undefined : resolveEquivalentPath(cwd);
|
|
275
290
|
const filtered = listed
|
|
276
291
|
.map(value => object(value) as BrokerSession | undefined)
|
|
277
292
|
.filter(
|
|
278
|
-
(value): value is BrokerSession & { locator: {
|
|
279
|
-
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",
|
|
280
295
|
)
|
|
281
|
-
.filter(value =>
|
|
296
|
+
.filter(value => canonicalCwd === undefined || value.locator.cwd === canonicalCwd);
|
|
282
297
|
const sessions = filtered.slice(offset, offset + SESSION_PAGE_SIZE).map(value => {
|
|
283
298
|
const metadata = sessionMetadata.get(value.sessionId);
|
|
284
299
|
const updatedAt =
|
|
@@ -291,7 +306,7 @@ export function paginateAcpSessions(
|
|
|
291
306
|
: undefined;
|
|
292
307
|
return {
|
|
293
308
|
sessionId: value.sessionId,
|
|
294
|
-
cwd: value.locator.
|
|
309
|
+
cwd: value.locator.cwd,
|
|
295
310
|
title:
|
|
296
311
|
typeof metadata?.title === "string" && metadata.title
|
|
297
312
|
? metadata.title
|
|
@@ -490,7 +505,12 @@ function terminalOutcome(event: JsonObject): SdkPromptTerminalOutcome | undefine
|
|
|
490
505
|
typeof outcome.message === "string" &&
|
|
491
506
|
(outcome.provenance === "agent_failed" || outcome.provenance === "deadline")
|
|
492
507
|
)
|
|
493
|
-
return
|
|
508
|
+
return {
|
|
509
|
+
kind: "failed",
|
|
510
|
+
code: outcome.code,
|
|
511
|
+
message: outcome.message,
|
|
512
|
+
provenance: outcome.provenance,
|
|
513
|
+
};
|
|
494
514
|
return undefined;
|
|
495
515
|
}
|
|
496
516
|
|
|
@@ -994,7 +1014,6 @@ export function acpPromptPayload(blocks: PromptRequest["prompt"]): {
|
|
|
994
1014
|
text.push(block.text);
|
|
995
1015
|
break;
|
|
996
1016
|
case "image":
|
|
997
|
-
if (block.uri) text.push(`[Image URI: ${block.uri}]`);
|
|
998
1017
|
images.push({ data: block.data, mimeType: block.mimeType });
|
|
999
1018
|
break;
|
|
1000
1019
|
case "resource_link":
|
|
@@ -1027,7 +1046,6 @@ export function acpPromptPayload(blocks: PromptRequest["prompt"]): {
|
|
|
1027
1046
|
"unsupported_content",
|
|
1028
1047
|
`Unsupported embedded resource MIME type: ${mimeType}`,
|
|
1029
1048
|
);
|
|
1030
|
-
text.push(`[Resource: ${resource.uri}]\nMIME: ${mimeType}`);
|
|
1031
1049
|
images.push({ data: resource.blob, mimeType });
|
|
1032
1050
|
break;
|
|
1033
1051
|
}
|
|
@@ -1038,7 +1056,7 @@ export function acpPromptPayload(blocks: PromptRequest["prompt"]): {
|
|
|
1038
1056
|
}
|
|
1039
1057
|
}
|
|
1040
1058
|
if (text.length === 0 && images.length === 0)
|
|
1041
|
-
throw new AcpSdkAdapterError("invalid_input", "
|
|
1059
|
+
throw new AcpSdkAdapterError("invalid_input", "Prompt must not be empty.");
|
|
1042
1060
|
return { text: text.join("\n"), images };
|
|
1043
1061
|
}
|
|
1044
1062
|
|
|
@@ -1183,6 +1201,18 @@ export class AcpAgent implements Agent {
|
|
|
1183
1201
|
readonly #pendingRouterFrames = new Map<string, Record<string, unknown>[]>();
|
|
1184
1202
|
#routerStartPromise: Promise<void> | undefined;
|
|
1185
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>>();
|
|
1186
1216
|
readonly #attaching = new Map<string, PendingAttachment>();
|
|
1187
1217
|
readonly #resolvingExisting = new Map<string, PendingAttachment>();
|
|
1188
1218
|
readonly #knownSessionCwds = new Map<string, string>();
|
|
@@ -1410,38 +1440,34 @@ export class AcpAgent implements Agent {
|
|
|
1410
1440
|
|
|
1411
1441
|
async listSessions(params: ListSessionsRequest): Promise<ListSessionsResponse> {
|
|
1412
1442
|
if (params.cwd) this.#assertAbsoluteCwd(params.cwd);
|
|
1443
|
+
const canonicalCwd = params.cwd ? await canonicalSessionCwd(params.cwd) : undefined;
|
|
1413
1444
|
const adapter = await this.#brokerAdapter();
|
|
1414
1445
|
const listing = await collectAcpSessionList(input => adapter.global("session.list", input));
|
|
1415
1446
|
const listed = Array.isArray(listing.sessions) ? listing.sessions : [];
|
|
1416
1447
|
|
|
1417
|
-
if (
|
|
1448
|
+
if (canonicalCwd) {
|
|
1418
1449
|
const discovered = new Set<string>();
|
|
1419
1450
|
for (const session of listed) {
|
|
1420
1451
|
const candidate = object(session) as BrokerSession | undefined;
|
|
1421
1452
|
if (
|
|
1422
1453
|
typeof candidate?.sessionId !== "string" ||
|
|
1423
|
-
typeof candidate.locator?.
|
|
1424
|
-
|
|
1454
|
+
typeof candidate.locator?.cwd !== "string" ||
|
|
1455
|
+
candidate.locator.cwd !== canonicalCwd
|
|
1425
1456
|
)
|
|
1426
1457
|
continue;
|
|
1427
1458
|
if (discovered.has(candidate.sessionId))
|
|
1428
1459
|
throw new AcpSdkAdapterError("conflict", `Broker returned duplicate session id: ${candidate.sessionId}`);
|
|
1429
1460
|
discovered.add(candidate.sessionId);
|
|
1430
1461
|
const knownCwd = this.#knownSessionCwds.get(candidate.sessionId);
|
|
1431
|
-
if (knownCwd &&
|
|
1462
|
+
if (knownCwd && (await canonicalSessionCwd(knownCwd)) !== canonicalCwd)
|
|
1432
1463
|
throw new AcpSdkAdapterError(
|
|
1433
1464
|
"conflict",
|
|
1434
1465
|
`ACP session ${candidate.sessionId} has conflicting cwd authority.`,
|
|
1435
1466
|
);
|
|
1436
|
-
this.#knownSessionCwds.set(candidate.sessionId,
|
|
1467
|
+
this.#knownSessionCwds.set(candidate.sessionId, canonicalCwd);
|
|
1437
1468
|
}
|
|
1438
1469
|
}
|
|
1439
|
-
return paginateAcpSessions(
|
|
1440
|
-
listed,
|
|
1441
|
-
params.cwd ?? undefined,
|
|
1442
|
-
this.#cursor(params.cursor),
|
|
1443
|
-
this.#knownSessionMetadata,
|
|
1444
|
-
);
|
|
1470
|
+
return paginateAcpSessions(listed, canonicalCwd, this.#cursor(params.cursor), this.#knownSessionMetadata);
|
|
1445
1471
|
}
|
|
1446
1472
|
|
|
1447
1473
|
closeSession(params: CloseSessionRequest): Promise<CloseSessionResponse> {
|
|
@@ -1502,6 +1528,8 @@ export class AcpAgent implements Agent {
|
|
|
1502
1528
|
this.#ownedSessionIds.delete(params.sessionId);
|
|
1503
1529
|
this.#knownSessionMcpServers.delete(params.sessionId);
|
|
1504
1530
|
this.#knownSessionMetadata.delete(params.sessionId);
|
|
1531
|
+
this.#retiredPromptCorrelations.delete(params.sessionId);
|
|
1532
|
+
this.#retiredPromptAcknowledgements.delete(params.sessionId);
|
|
1505
1533
|
return {};
|
|
1506
1534
|
}
|
|
1507
1535
|
throw error;
|
|
@@ -1518,6 +1546,8 @@ export class AcpAgent implements Agent {
|
|
|
1518
1546
|
this.#knownSessionMcpServers.delete(params.sessionId);
|
|
1519
1547
|
this.#knownSessionMetadata.delete(params.sessionId);
|
|
1520
1548
|
this.#pendingDeleteLocators.delete(params.sessionId);
|
|
1549
|
+
this.#retiredPromptCorrelations.delete(params.sessionId);
|
|
1550
|
+
this.#retiredPromptAcknowledgements.delete(params.sessionId);
|
|
1521
1551
|
return {};
|
|
1522
1552
|
} finally {
|
|
1523
1553
|
this.#finishTeardown(params.sessionId);
|
|
@@ -1576,10 +1606,32 @@ export class AcpAgent implements Agent {
|
|
|
1576
1606
|
if (!record) throw new AcpSdkAdapterError("not_found", `Unknown session, not found: ${params.sessionId}`);
|
|
1577
1607
|
if (record.activePrompt) throw new AcpSdkAdapterError("conflict", "ACP session already has an active prompt.");
|
|
1578
1608
|
if (record.authFailure) throw new AcpSdkAdapterError("authentication_failed", record.authFailure);
|
|
1579
|
-
|
|
1580
|
-
|
|
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
|
+
|
|
1581
1624
|
const payload = acpPromptPayload(params.prompt);
|
|
1582
1625
|
const skillInvocation = acpSkillInvocation(params.prompt);
|
|
1626
|
+
if (!skillInvocation) {
|
|
1627
|
+
const promptError = validateRequiredPromptText("turn.prompt", {
|
|
1628
|
+
text: payload.text,
|
|
1629
|
+
images: payload.images,
|
|
1630
|
+
});
|
|
1631
|
+
if (promptError) throw new AcpSdkAdapterError(promptError.code, promptError.message);
|
|
1632
|
+
}
|
|
1633
|
+
// A new turn starts uncancelled; a stale flag must never settle it as `cancelled`.
|
|
1634
|
+
record.cancelRequested = false;
|
|
1583
1635
|
if (isAcpUnavailableSlashCommand(payload.text)) {
|
|
1584
1636
|
await this.#publishSessionUpdate(
|
|
1585
1637
|
params.sessionId,
|
|
@@ -1619,14 +1671,18 @@ export class AcpAgent implements Agent {
|
|
|
1619
1671
|
MAX_PROMPT_FRAME_BYTES / 1024,
|
|
1620
1672
|
)} KiB transport limit. Attach a smaller or more compressed image.`,
|
|
1621
1673
|
);
|
|
1674
|
+
record.publicationGeneration++;
|
|
1622
1675
|
const { promise: response, resolve, reject } = Promise.withResolvers<PromptResponse>();
|
|
1623
1676
|
const waiter: PromptWaiter = {
|
|
1624
1677
|
acknowledged: false,
|
|
1625
1678
|
dispatched: false,
|
|
1679
|
+
acknowledgementPending: false,
|
|
1626
1680
|
boundary: record.inboundSequence,
|
|
1627
1681
|
correlation: {},
|
|
1628
1682
|
emittedAssistantText: "",
|
|
1629
1683
|
settled: false,
|
|
1684
|
+
terminalReserved: false,
|
|
1685
|
+
failureDiagnostics: [],
|
|
1630
1686
|
deferredFrames: [],
|
|
1631
1687
|
deferredActivityFrames: [],
|
|
1632
1688
|
lastFrameAt: this.#promptWatchdogClock.now(),
|
|
@@ -1644,13 +1700,20 @@ export class AcpAgent implements Agent {
|
|
|
1644
1700
|
await record.adapter.ensureProviders();
|
|
1645
1701
|
} catch (error) {
|
|
1646
1702
|
if (waiter.settled || record.cancelRequested || record.activePrompt !== waiter) {
|
|
1703
|
+
this.#retiredPromptAcknowledgements.delete(params.sessionId);
|
|
1647
1704
|
if (!waiter.settled) await this.#settleCancelledPrompt(params.sessionId, record, waiter);
|
|
1648
1705
|
return await response;
|
|
1649
1706
|
}
|
|
1650
|
-
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
|
+
}
|
|
1651
1713
|
throw error;
|
|
1652
1714
|
}
|
|
1653
1715
|
if (waiter.settled || record.cancelRequested || record.activePrompt !== waiter) {
|
|
1716
|
+
this.#retiredPromptAcknowledgements.delete(params.sessionId);
|
|
1654
1717
|
if (!waiter.settled) await this.#settleCancelledPrompt(params.sessionId, record, waiter);
|
|
1655
1718
|
return await response;
|
|
1656
1719
|
}
|
|
@@ -1681,6 +1744,7 @@ export class AcpAgent implements Agent {
|
|
|
1681
1744
|
return await response;
|
|
1682
1745
|
}
|
|
1683
1746
|
|
|
1747
|
+
waiter.acknowledgementPending = true;
|
|
1684
1748
|
const acknowledgementTask = (async (): Promise<PromptResponse> => {
|
|
1685
1749
|
if (waiter.settled || record.activePrompt !== waiter) return await response;
|
|
1686
1750
|
const acknowledgement = skillInvocation
|
|
@@ -1696,20 +1760,33 @@ export class AcpAgent implements Agent {
|
|
|
1696
1760
|
"invalid_prompt_acknowledgement",
|
|
1697
1761
|
"SDK prompt acknowledgement must accept the prompt and include commandId and turnId.",
|
|
1698
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
|
+
}
|
|
1699
1784
|
// Retain the acknowledgement ingress boundary with its complete correlation.
|
|
1700
1785
|
waiter.boundary = record.inboundSequence;
|
|
1701
1786
|
waiter.correlation = acknowledgementCorrelation;
|
|
1702
1787
|
waiter.acknowledged = true;
|
|
1703
1788
|
if (promptWaiterRetired(record, waiter)) {
|
|
1704
|
-
|
|
1705
|
-
!record.settledPromptCorrelations.some(settled =>
|
|
1706
|
-
correlationsExactlyMatch(settled, acknowledgementCorrelation),
|
|
1707
|
-
)
|
|
1708
|
-
) {
|
|
1709
|
-
record.settledPromptCorrelations.push(acknowledgementCorrelation);
|
|
1710
|
-
while (record.settledPromptCorrelations.length > SETTLED_PROMPT_CORRELATION_RETENTION)
|
|
1711
|
-
record.settledPromptCorrelations.shift();
|
|
1712
|
-
}
|
|
1789
|
+
this.#rememberSettledPromptCorrelation(params.sessionId, record, acknowledgementCorrelation);
|
|
1713
1790
|
return await response;
|
|
1714
1791
|
}
|
|
1715
1792
|
// Frames held while ownership was unknown belong to this prompt only when the
|
|
@@ -1718,18 +1795,47 @@ export class AcpAgent implements Agent {
|
|
|
1718
1795
|
let observedDeferredActivity = false;
|
|
1719
1796
|
for (const deferredFrame of deferredActivityFrames) {
|
|
1720
1797
|
const deferredEvent = receivedSdkEvent(deferredFrame)?.event;
|
|
1721
|
-
|
|
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) {
|
|
1722
1822
|
this.#observePromptActivity(waiter, deferredFrame);
|
|
1723
1823
|
observedDeferredActivity = true;
|
|
1724
1824
|
}
|
|
1725
1825
|
}
|
|
1726
1826
|
if (observedDeferredActivity) this.#armPromptWatchdog(params.sessionId, record, waiter);
|
|
1727
1827
|
const deferred = waiter.deferredFrames.splice(0);
|
|
1728
|
-
for (const deferredFrame of deferred) {
|
|
1828
|
+
for (const { frame: deferredFrame, publicationGeneration: deferredGeneration } of deferred) {
|
|
1729
1829
|
const deferredEvent = receivedSdkEvent(deferredFrame)?.event;
|
|
1730
1830
|
if (!deferredEvent) continue;
|
|
1731
1831
|
const deferredCorrelation = sdkFrameCorrelation(deferredFrame, deferredEvent) ?? {};
|
|
1732
|
-
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");
|
|
1733
1839
|
if (!hasCompleteCorrelation(deferredCorrelation)) {
|
|
1734
1840
|
if (deferredIsTerminal)
|
|
1735
1841
|
logDroppedPromptTerminal(
|
|
@@ -1742,6 +1848,18 @@ export class AcpAgent implements Agent {
|
|
|
1742
1848
|
continue;
|
|
1743
1849
|
}
|
|
1744
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
|
+
}
|
|
1745
1863
|
if (
|
|
1746
1864
|
!matchesPrompt &&
|
|
1747
1865
|
record.settledPromptCorrelations.some(settled => correlationsExactlyMatch(settled, deferredCorrelation))
|
|
@@ -1758,17 +1876,26 @@ export class AcpAgent implements Agent {
|
|
|
1758
1876
|
);
|
|
1759
1877
|
continue;
|
|
1760
1878
|
}
|
|
1879
|
+
if (deferredIsTerminal) {
|
|
1880
|
+
waiter.terminalReserved = true;
|
|
1881
|
+
clearPromptWatchdog(waiter);
|
|
1882
|
+
}
|
|
1761
1883
|
const task = record.frameTail.then(
|
|
1762
|
-
async () =>
|
|
1763
|
-
|
|
1764
|
-
record.frameTail = task.catch(
|
|
1765
|
-
async error =>
|
|
1766
|
-
await this.#failSession(params.sessionId, record.adapter, this.#frameProcessingFailure(error)),
|
|
1884
|
+
async () =>
|
|
1885
|
+
await this.#handleSdkFrame(params.sessionId, record.adapter, deferredFrame, deferredGeneration),
|
|
1767
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
|
+
});
|
|
1768
1892
|
}
|
|
1769
|
-
this.#settlePrompt(record, waiter);
|
|
1893
|
+
this.#settlePrompt(params.sessionId, record, waiter);
|
|
1770
1894
|
return await response;
|
|
1771
|
-
})()
|
|
1895
|
+
})().finally(() => {
|
|
1896
|
+
waiter.acknowledgementPending = false;
|
|
1897
|
+
this.#retiredPromptAcknowledgements.delete(params.sessionId);
|
|
1898
|
+
});
|
|
1772
1899
|
// Settlement can win before the SDK answers `turn.prompt`; acknowledgement processing
|
|
1773
1900
|
// must remain alive to tombstone its eventual correlation without holding the ACP caller.
|
|
1774
1901
|
void acknowledgementTask.catch(() => undefined);
|
|
@@ -1781,15 +1908,15 @@ export class AcpAgent implements Agent {
|
|
|
1781
1908
|
clearPromptWatchdog(waiter);
|
|
1782
1909
|
waiter.terminal = undefined;
|
|
1783
1910
|
waiter.settled = true;
|
|
1784
|
-
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
|
+
}
|
|
1785
1916
|
// Keep a late terminal for this (cancelled) turn closed, exactly like the
|
|
1786
1917
|
// other settlement paths, so an aborted run's trailing frame can never
|
|
1787
1918
|
// publish over a later prompt that reuses the identity.
|
|
1788
|
-
|
|
1789
|
-
record.settledPromptCorrelations.push(waiter.correlation);
|
|
1790
|
-
while (record.settledPromptCorrelations.length > SETTLED_PROMPT_CORRELATION_RETENTION)
|
|
1791
|
-
record.settledPromptCorrelations.shift();
|
|
1792
|
-
}
|
|
1919
|
+
this.#rememberSettledPromptCorrelation(params.sessionId, record, waiter.correlation);
|
|
1793
1920
|
// A prompt cancelled before the SDK acknowledged it still ends this turn by
|
|
1794
1921
|
// client request, and ACP is explicit: "Agents MUST catch these errors and
|
|
1795
1922
|
// return the semantically meaningful `cancelled` stop reason, so that Clients
|
|
@@ -1800,7 +1927,6 @@ export class AcpAgent implements Agent {
|
|
|
1800
1927
|
// The client's turn is settled by the return; the advisory idle publication
|
|
1801
1928
|
// must not gate it and must still be attempted so the running phase is
|
|
1802
1929
|
// released (gjcRunning:false) instead of spinning behind a settled cancel.
|
|
1803
|
-
void this.#publishPromptPhaseIdle(params.sessionId, record.adapter);
|
|
1804
1930
|
return { stopReason: "cancelled" };
|
|
1805
1931
|
}
|
|
1806
1932
|
throw error;
|
|
@@ -1845,7 +1971,9 @@ export class AcpAgent implements Agent {
|
|
|
1845
1971
|
"abort_unacknowledged",
|
|
1846
1972
|
"SDK did not acknowledge cancellation of the active prompt.",
|
|
1847
1973
|
);
|
|
1848
|
-
if (waiter)
|
|
1974
|
+
if (waiter) {
|
|
1975
|
+
waiter.cancelAcknowledged = true;
|
|
1976
|
+
}
|
|
1849
1977
|
if (waiter && record.activePrompt !== waiter) {
|
|
1850
1978
|
waiter.cancelAttemptResolve?.(true);
|
|
1851
1979
|
return;
|
|
@@ -1858,10 +1986,16 @@ export class AcpAgent implements Agent {
|
|
|
1858
1986
|
!waiter.settled
|
|
1859
1987
|
) {
|
|
1860
1988
|
record.activePrompt = undefined;
|
|
1989
|
+
record.busy = record.backgroundBusy;
|
|
1990
|
+
record.cancelRequested = false;
|
|
1991
|
+
clearPromptWatchdog(waiter);
|
|
1861
1992
|
waiter.settled = true;
|
|
1993
|
+
this.#fenceRetiredPromptAcknowledgement(params.sessionId, waiter);
|
|
1862
1994
|
waiter.deferredFrames.length = 0;
|
|
1995
|
+
waiter.deferredActivityFrames.length = 0;
|
|
1863
1996
|
waiter.terminal = undefined;
|
|
1864
1997
|
waiter.resolve({ stopReason: "cancelled" });
|
|
1998
|
+
this.#flushFailureDiagnostics(params.sessionId, waiter, record.adapter);
|
|
1865
1999
|
// Release the running phase for consistency with the resolved cancel; the
|
|
1866
2000
|
// publication is advisory and must never gate the settlement above.
|
|
1867
2001
|
void this.#publishPromptPhaseIdle(params.sessionId, record.adapter);
|
|
@@ -1926,20 +2060,19 @@ export class AcpAgent implements Agent {
|
|
|
1926
2060
|
// The authoritative terminal wins whenever it arrives in time; this only runs
|
|
1927
2061
|
// when nothing settled the prompt the client already asked to cancel.
|
|
1928
2062
|
if (this.#sessions.get(id) !== record || record.activePrompt !== waiter || waiter.settled) return;
|
|
2063
|
+
if (waiter.terminalReserved) return;
|
|
1929
2064
|
record.activePrompt = undefined;
|
|
2065
|
+
if (!record.backgroundBusy) record.busy = false;
|
|
1930
2066
|
clearPromptWatchdog(waiter);
|
|
1931
2067
|
record.cancelRequested = false;
|
|
1932
2068
|
waiter.settled = true;
|
|
2069
|
+
this.#fenceRetiredPromptAcknowledgement(id, waiter);
|
|
1933
2070
|
waiter.deferredFrames.length = 0;
|
|
1934
2071
|
waiter.deferredActivityFrames.length = 0;
|
|
1935
2072
|
waiter.terminal = undefined;
|
|
1936
2073
|
// A late terminal for this turn must stay closed rather than publish over a
|
|
1937
2074
|
// prompt the client has already been told is cancelled.
|
|
1938
|
-
|
|
1939
|
-
record.settledPromptCorrelations.push(waiter.correlation);
|
|
1940
|
-
while (record.settledPromptCorrelations.length > SETTLED_PROMPT_CORRELATION_RETENTION)
|
|
1941
|
-
record.settledPromptCorrelations.shift();
|
|
1942
|
-
}
|
|
2075
|
+
this.#rememberSettledPromptCorrelation(id, record, waiter.correlation);
|
|
1943
2076
|
// The client's turn ends with the resolution, not with the advisory idle update.
|
|
1944
2077
|
// Publishing the phase transition first made a backpressured ACP transport hold
|
|
1945
2078
|
// the settlement hostage: `sessionUpdate` awaits the stream write, a client that
|
|
@@ -1947,6 +2080,7 @@ export class AcpAgent implements Agent {
|
|
|
1947
2080
|
// ran — the acknowledged cancel left the prompt pending forever, the client
|
|
1948
2081
|
// force-cancelled, and the next prompt collided with the stale foreground turn.
|
|
1949
2082
|
waiter.resolve({ stopReason: "cancelled" });
|
|
2083
|
+
this.#flushFailureDiagnostics(id, waiter, record.adapter);
|
|
1950
2084
|
void this.#publishPromptPhaseIdle(id, record.adapter);
|
|
1951
2085
|
}
|
|
1952
2086
|
|
|
@@ -2137,14 +2271,15 @@ export class AcpAgent implements Agent {
|
|
|
2137
2271
|
}
|
|
2138
2272
|
|
|
2139
2273
|
async #scopedBrokerSession(id: string, cwd: string): Promise<BrokerSession | undefined> {
|
|
2274
|
+
const canonicalCwd = await canonicalSessionCwd(cwd);
|
|
2140
2275
|
const adapter = await this.#brokerAdapter();
|
|
2141
|
-
const result = await collectAcpSessionList(input => adapter.global("session.list", input), { cwd });
|
|
2276
|
+
const result = await collectAcpSessionList(input => adapter.global("session.list", input), { cwd: canonicalCwd });
|
|
2142
2277
|
|
|
2143
2278
|
const matches: BrokerSession[] = [];
|
|
2144
2279
|
for (const item of Array.isArray(result?.sessions) ? result.sessions : []) {
|
|
2145
2280
|
const session = object(item) as BrokerSession | undefined;
|
|
2146
2281
|
if (session?.sessionId !== id) continue;
|
|
2147
|
-
if (typeof session.locator?.
|
|
2282
|
+
if (typeof session.locator?.cwd !== "string" || session.locator.cwd !== canonicalCwd)
|
|
2148
2283
|
throw new AcpSdkAdapterError("conflict", `Broker returned conflicting session scope for ${id}.`);
|
|
2149
2284
|
matches.push(session);
|
|
2150
2285
|
}
|
|
@@ -2239,10 +2374,14 @@ export class AcpAgent implements Agent {
|
|
|
2239
2374
|
unsubscribe: () => {},
|
|
2240
2375
|
reconnectUnsubscribe: () => {},
|
|
2241
2376
|
frameTail: Promise.resolve(),
|
|
2242
|
-
|
|
2377
|
+
publicationGeneration: 0,
|
|
2378
|
+
settledPromptCorrelations: this.#retiredPromptCorrelations.get(id) ?? [],
|
|
2243
2379
|
inboundSequence: 0,
|
|
2244
2380
|
connectionId: adapter.connectionId,
|
|
2245
2381
|
busy: false,
|
|
2382
|
+
backgroundBusy: false,
|
|
2383
|
+
backgroundAnonymousCount: 0,
|
|
2384
|
+
backgroundCorrelations: [],
|
|
2246
2385
|
toolArgs: new Map(),
|
|
2247
2386
|
};
|
|
2248
2387
|
record.unsubscribe = adapter.onFrame(frame => this.#enqueueSdkFrame(id, adapter!, frame));
|
|
@@ -2292,6 +2431,40 @@ export class AcpAgent implements Agent {
|
|
|
2292
2431
|
}
|
|
2293
2432
|
}
|
|
2294
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
|
+
|
|
2295
2468
|
#recoverSessionAfterTransportFailure(id: string, adapter: AcpSdkAdapter, error: Error): void {
|
|
2296
2469
|
const record = this.#sessions.get(id);
|
|
2297
2470
|
if (!record || record.adapter !== adapter) return;
|
|
@@ -2325,6 +2498,8 @@ export class AcpAgent implements Agent {
|
|
|
2325
2498
|
const mcpServers = this.#knownSessionMcpServers.get(id) ?? [];
|
|
2326
2499
|
try {
|
|
2327
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));
|
|
2328
2503
|
} catch (attachError) {
|
|
2329
2504
|
const detail = attachError instanceof Error ? attachError.message : String(attachError);
|
|
2330
2505
|
logger.warn(`ACP session ${id} auto-reattach after transport loss failed: ${detail}`);
|
|
@@ -2346,6 +2521,8 @@ export class AcpAgent implements Agent {
|
|
|
2346
2521
|
this.#ownedSessionIds.delete(id);
|
|
2347
2522
|
this.#knownSessionMcpServers.delete(id);
|
|
2348
2523
|
this.#knownSessionMetadata.delete(id);
|
|
2524
|
+
this.#retiredPromptCorrelations.delete(id);
|
|
2525
|
+
this.#retiredPromptAcknowledgements.delete(id);
|
|
2349
2526
|
}
|
|
2350
2527
|
|
|
2351
2528
|
async #closeOwnedSession(id: string): Promise<CloseSessionResponse> {
|
|
@@ -2360,6 +2537,8 @@ export class AcpAgent implements Agent {
|
|
|
2360
2537
|
this.#ownedSessionIds.delete(id);
|
|
2361
2538
|
this.#knownSessionMcpServers.delete(id);
|
|
2362
2539
|
this.#knownSessionMetadata.delete(id);
|
|
2540
|
+
this.#retiredPromptCorrelations.delete(id);
|
|
2541
|
+
this.#retiredPromptAcknowledgements.delete(id);
|
|
2363
2542
|
return {};
|
|
2364
2543
|
} finally {
|
|
2365
2544
|
this.#finishTeardown(id);
|
|
@@ -2378,10 +2557,14 @@ export class AcpAgent implements Agent {
|
|
|
2378
2557
|
try {
|
|
2379
2558
|
this.#advanceSessionEpoch(id);
|
|
2380
2559
|
if (record) {
|
|
2560
|
+
const waiter = record.activePrompt;
|
|
2561
|
+
if (waiter) {
|
|
2562
|
+
this.#rememberSettledPromptCorrelation(id, record, waiter.correlation);
|
|
2563
|
+
this.#fenceRetiredPromptAcknowledgement(id, waiter);
|
|
2564
|
+
}
|
|
2381
2565
|
this.#sessions.delete(id);
|
|
2382
2566
|
record.unsubscribe();
|
|
2383
2567
|
record.reconnectUnsubscribe();
|
|
2384
|
-
const waiter = record.activePrompt;
|
|
2385
2568
|
record.activePrompt = undefined;
|
|
2386
2569
|
// `session/close` is the client asking to end its own work, so the pending turn
|
|
2387
2570
|
// settles as `cancelled` rather than surfacing a spurious error. ACP: "Agents
|
|
@@ -2441,7 +2624,11 @@ export class AcpAgent implements Agent {
|
|
|
2441
2624
|
record.reconnectUnsubscribe();
|
|
2442
2625
|
const waiter = record.activePrompt;
|
|
2443
2626
|
record.activePrompt = undefined;
|
|
2444
|
-
if (waiter)
|
|
2627
|
+
if (waiter) {
|
|
2628
|
+
clearPromptWatchdog(waiter);
|
|
2629
|
+
this.#rememberSettledPromptCorrelation(id, record, waiter.correlation);
|
|
2630
|
+
this.#fenceRetiredPromptAcknowledgement(id, waiter);
|
|
2631
|
+
}
|
|
2445
2632
|
waiter?.reject(error);
|
|
2446
2633
|
try {
|
|
2447
2634
|
await adapter.close();
|
|
@@ -2510,13 +2697,53 @@ export class AcpAgent implements Agent {
|
|
|
2510
2697
|
|
|
2511
2698
|
#observeSessionActivity(record: SessionRecord, frame: JsonObject): void {
|
|
2512
2699
|
if (frame.type === "activity") {
|
|
2513
|
-
if (frame.state === "busy")
|
|
2514
|
-
|
|
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
|
+
}
|
|
2515
2717
|
return;
|
|
2516
2718
|
}
|
|
2517
2719
|
const event = receivedSdkEvent(frame)?.event;
|
|
2518
|
-
if (event?.type === "agent_start")
|
|
2519
|
-
|
|
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
|
+
}
|
|
2520
2747
|
}
|
|
2521
2748
|
|
|
2522
2749
|
#frameProcessingFailure(error: unknown): AcpSdkAdapterError {
|
|
@@ -2553,7 +2780,7 @@ export class AcpAgent implements Agent {
|
|
|
2553
2780
|
*/
|
|
2554
2781
|
#refreshPromptWatchdog(id: string, record: SessionRecord, frame: JsonObject): void {
|
|
2555
2782
|
const waiter = record.activePrompt;
|
|
2556
|
-
if (!waiter || waiter.settled) return;
|
|
2783
|
+
if (!waiter || waiter.settled || waiter.terminalReserved) return;
|
|
2557
2784
|
const event = receivedSdkEvent(frame)?.event;
|
|
2558
2785
|
const correlation = watchdogCorrelationFrom(frame, event);
|
|
2559
2786
|
if (!waiter.acknowledged) {
|
|
@@ -2589,7 +2816,7 @@ export class AcpAgent implements Agent {
|
|
|
2589
2816
|
* turn can no longer be observed, it does not cancel or tear down the session.
|
|
2590
2817
|
*/
|
|
2591
2818
|
async #expirePromptWatchdog(id: string, record: SessionRecord, waiter: PromptWaiter): Promise<void> {
|
|
2592
|
-
if (record.activePrompt !== waiter || waiter.settled) return;
|
|
2819
|
+
if (record.activePrompt !== waiter || waiter.settled || waiter.terminalReserved) return;
|
|
2593
2820
|
const silenceMs = Math.max(0, this.#promptWatchdogClock.now() - waiter.lastFrameAt);
|
|
2594
2821
|
const cause = this.#promptTransportGone(id, record) ?? "the SDK session host stopped producing frames";
|
|
2595
2822
|
logger.error("acp_prompt_watchdog_expired", {
|
|
@@ -2619,20 +2846,48 @@ export class AcpAgent implements Agent {
|
|
|
2619
2846
|
#enqueueSdkFrame(id: string, adapter: AcpSdkAdapter, frame: JsonObject): void {
|
|
2620
2847
|
const record = this.#sessions.get(id);
|
|
2621
2848
|
if (!record || record.adapter !== adapter) return;
|
|
2622
|
-
if (
|
|
2849
|
+
if (frame.type !== "hello" && frame.type !== "server_hello" && typeof frame.connectionId === "string")
|
|
2850
|
+
record.connectionId = frame.connectionId;
|
|
2623
2851
|
// Ingress ordering is recorded before queued work begins.
|
|
2624
2852
|
this.#observeSessionActivity(record, frame);
|
|
2625
2853
|
// Correlation is checked at ingress before a prompt-owned frame may refresh the
|
|
2626
2854
|
// watchdog, so queued processing cannot turn unrelated host traffic into turn liveness.
|
|
2627
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
|
+
}
|
|
2628
2875
|
++record.inboundSequence;
|
|
2629
|
-
const
|
|
2630
|
-
record.frameTail
|
|
2631
|
-
|
|
2632
|
-
|
|
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
|
+
});
|
|
2633
2883
|
}
|
|
2634
2884
|
|
|
2635
|
-
async #handleSdkFrame(
|
|
2885
|
+
async #handleSdkFrame(
|
|
2886
|
+
id: string,
|
|
2887
|
+
adapter: AcpSdkAdapter,
|
|
2888
|
+
frame: JsonObject,
|
|
2889
|
+
ingressPublicationGeneration?: number,
|
|
2890
|
+
): Promise<void> {
|
|
2636
2891
|
const record = this.#sessions.get(id);
|
|
2637
2892
|
if (!record || record.adapter !== adapter) return;
|
|
2638
2893
|
if ((frame.type === "hello" || frame.type === "server_hello") && typeof frame.connectionId === "string") {
|
|
@@ -2642,19 +2897,18 @@ export class AcpAgent implements Agent {
|
|
|
2642
2897
|
const waiter = record.activePrompt;
|
|
2643
2898
|
if (waiter && !waiter.settled && !waiter.terminal) {
|
|
2644
2899
|
record.activePrompt = undefined;
|
|
2900
|
+
record.busy = record.backgroundBusy;
|
|
2645
2901
|
clearPromptWatchdog(waiter);
|
|
2646
2902
|
waiter.settled = true;
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
while (record.settledPromptCorrelations.length > SETTLED_PROMPT_CORRELATION_RETENTION)
|
|
2650
|
-
record.settledPromptCorrelations.shift();
|
|
2651
|
-
}
|
|
2903
|
+
this.#rememberSettledPromptCorrelation(id, record, waiter.correlation);
|
|
2904
|
+
this.#fenceRetiredPromptAcknowledgement(id, waiter);
|
|
2652
2905
|
waiter.reject(
|
|
2653
2906
|
new AcpSdkAdapterError(
|
|
2654
2907
|
"connection_closed",
|
|
2655
2908
|
"The prompt owner connection was lost before completion.",
|
|
2656
2909
|
),
|
|
2657
2910
|
);
|
|
2911
|
+
this.#flushFailureDiagnostics(id, waiter, record.adapter);
|
|
2658
2912
|
// The turn is settled by the rejection; release the running phase
|
|
2659
2913
|
// best-effort so a cancelled prompt is not left reporting working.
|
|
2660
2914
|
void this.#publishPromptPhaseIdle(id, record.adapter);
|
|
@@ -2665,7 +2919,9 @@ export class AcpAgent implements Agent {
|
|
|
2665
2919
|
const received = receivedSdkEvent(frame);
|
|
2666
2920
|
if (!received) return;
|
|
2667
2921
|
const { event, wirePayload } = received;
|
|
2668
|
-
|
|
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");
|
|
2669
2925
|
if (event.type === "notice" && event.source === "autorouting" && typeof event.message === "string") {
|
|
2670
2926
|
record.routingInactiveNotice = event.message;
|
|
2671
2927
|
return;
|
|
@@ -2673,21 +2929,58 @@ export class AcpAgent implements Agent {
|
|
|
2673
2929
|
const derivedCorrelation = sdkFrameCorrelation(frame, event);
|
|
2674
2930
|
const correlation = derivedCorrelation ?? {};
|
|
2675
2931
|
const activePrompt = record.activePrompt;
|
|
2676
|
-
|
|
2932
|
+
let terminalPromptOwner: PromptWaiter | undefined;
|
|
2677
2933
|
const settledCorrelation = record.settledPromptCorrelations.some(settled =>
|
|
2678
2934
|
correlationsMatch(settled, correlation),
|
|
2679
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);
|
|
2680
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
|
+
}
|
|
2681
2974
|
// Terminal ownership requires a complete identity. Unowned, partial, and
|
|
2682
2975
|
// duplicate terminals are never allowed to publish or query anything.
|
|
2683
|
-
if (
|
|
2976
|
+
if (activePrompt.settled) return;
|
|
2684
2977
|
if (!hasCompleteCorrelation(correlation)) {
|
|
2685
2978
|
logDroppedPromptTerminal(id, event, "incomplete_correlation", correlation, activePrompt.correlation);
|
|
2686
2979
|
return;
|
|
2687
2980
|
}
|
|
2688
2981
|
if (!activePrompt.acknowledged) {
|
|
2689
2982
|
// Hold the entire frame until the prompt acknowledgement proves ownership.
|
|
2690
|
-
activePrompt.deferredFrames.push(frame);
|
|
2983
|
+
activePrompt.deferredFrames.push({ frame, publicationGeneration });
|
|
2691
2984
|
return;
|
|
2692
2985
|
}
|
|
2693
2986
|
const matchesPrompt = correlationsExactlyMatch(activePrompt.correlation, correlation);
|
|
@@ -2697,26 +2990,30 @@ export class AcpAgent implements Agent {
|
|
|
2697
2990
|
return;
|
|
2698
2991
|
}
|
|
2699
2992
|
if (activePrompt.terminal) return;
|
|
2993
|
+
record.busy = record.backgroundBusy;
|
|
2994
|
+
terminalPromptOwner = activePrompt;
|
|
2995
|
+
this.#advanceTerminalGeneration(record);
|
|
2996
|
+
publicationGeneration = record.publicationGeneration;
|
|
2700
2997
|
if (!outcome) {
|
|
2701
|
-
const detail =
|
|
2702
|
-
typeof (event as { error?: { message?: unknown } }).error?.message === "string"
|
|
2703
|
-
? (event as { error: { message: string } }).error.message
|
|
2704
|
-
: "the prompt terminal omitted a valid normalized outcome";
|
|
2705
2998
|
await this.#rejectPrompt(
|
|
2706
2999
|
record,
|
|
2707
3000
|
id,
|
|
2708
3001
|
activePrompt,
|
|
2709
|
-
new AcpSdkAdapterError("connection_closed",
|
|
3002
|
+
new AcpSdkAdapterError("connection_closed", "ACP prompt terminal was invalid or incomplete."),
|
|
2710
3003
|
);
|
|
2711
3004
|
return;
|
|
2712
3005
|
}
|
|
2713
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);
|
|
2714
3011
|
}
|
|
2715
3012
|
if (!isTerminal && derivedCorrelation === undefined) return;
|
|
2716
3013
|
if (!isTerminal && hasCorrelation(correlation)) {
|
|
2717
3014
|
if (!activePrompt || activePrompt.settled) return;
|
|
2718
3015
|
if (!activePrompt.acknowledged) {
|
|
2719
|
-
activePrompt.deferredFrames.push(frame);
|
|
3016
|
+
activePrompt.deferredFrames.push({ frame, publicationGeneration });
|
|
2720
3017
|
return;
|
|
2721
3018
|
}
|
|
2722
3019
|
if (!correlationsExactlyMatch(activePrompt.correlation, correlation)) return;
|
|
@@ -2725,18 +3022,19 @@ export class AcpAgent implements Agent {
|
|
|
2725
3022
|
// Frames for an already-settled correlation stay closed until an active prompt
|
|
2726
3023
|
// acknowledges the exact same identity.
|
|
2727
3024
|
if (activePrompt && !activePrompt.settled && !activePrompt.acknowledged) {
|
|
2728
|
-
activePrompt.deferredFrames.push(frame);
|
|
3025
|
+
activePrompt.deferredFrames.push({ frame, publicationGeneration });
|
|
2729
3026
|
return;
|
|
2730
3027
|
}
|
|
2731
3028
|
if (!activePrompt || activePrompt.settled || !correlationsMatch(activePrompt.correlation, correlation)) return;
|
|
2732
3029
|
}
|
|
2733
3030
|
const promptOwner =
|
|
2734
|
-
|
|
3031
|
+
terminalPromptOwner ??
|
|
3032
|
+
(activePrompt &&
|
|
2735
3033
|
!activePrompt.settled &&
|
|
2736
3034
|
activePrompt.acknowledged &&
|
|
2737
3035
|
correlationsExactlyMatch(activePrompt.correlation, correlation)
|
|
2738
3036
|
? activePrompt
|
|
2739
|
-
: undefined;
|
|
3037
|
+
: undefined);
|
|
2740
3038
|
const toolCallId = typeof event.toolCallId === "string" ? event.toolCallId : undefined;
|
|
2741
3039
|
if (
|
|
2742
3040
|
toolCallId &&
|
|
@@ -2759,7 +3057,9 @@ export class AcpAgent implements Agent {
|
|
|
2759
3057
|
},
|
|
2760
3058
|
};
|
|
2761
3059
|
const notifications = wirePayload
|
|
2762
|
-
?
|
|
3060
|
+
? event.type === "agent_failed" && !object(event.error)
|
|
3061
|
+
? []
|
|
3062
|
+
: mapAgentWireEventPayloadToAcpSessionUpdates(wirePayload as never, id, mapperOptions)
|
|
2763
3063
|
: event.type === "agent_failed" && object(event.error)
|
|
2764
3064
|
? mapAgentSessionEventToAcpSessionUpdates(event as unknown as AgentSessionEvent, id, mapperOptions)
|
|
2765
3065
|
: [];
|
|
@@ -2770,7 +3070,21 @@ export class AcpAgent implements Agent {
|
|
|
2770
3070
|
notification.update.content.type === "text"
|
|
2771
3071
|
)
|
|
2772
3072
|
promptOwner.emittedAssistantText += notification.update.content.text;
|
|
2773
|
-
|
|
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
|
+
);
|
|
2774
3088
|
}
|
|
2775
3089
|
if (toolCallId && event.type === "tool_execution_end") record.toolArgs.delete(toolCallId);
|
|
2776
3090
|
if (event.type === "agent_start") {
|
|
@@ -2785,15 +3099,64 @@ export class AcpAgent implements Agent {
|
|
|
2785
3099
|
},
|
|
2786
3100
|
},
|
|
2787
3101
|
adapter,
|
|
3102
|
+
promptOwner ? publicationGeneration : undefined,
|
|
2788
3103
|
);
|
|
2789
3104
|
}
|
|
2790
3105
|
if (event.type === "message_end" && object(event.message)?.role === "assistant") {
|
|
2791
3106
|
if (promptOwner) promptOwner.messageProgress = undefined;
|
|
2792
3107
|
else record.sessionMessageProgress = undefined;
|
|
2793
3108
|
}
|
|
2794
|
-
if (event
|
|
2795
|
-
|
|
2796
|
-
|
|
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);
|
|
2797
3160
|
const resolution = resolveAcpFinalText(promptOwner.emittedAssistantText, finalText);
|
|
2798
3161
|
if (resolution.kind === "emit") {
|
|
2799
3162
|
promptOwner.emittedAssistantText += resolution.text;
|
|
@@ -2808,6 +3171,7 @@ export class AcpAgent implements Agent {
|
|
|
2808
3171
|
},
|
|
2809
3172
|
},
|
|
2810
3173
|
adapter,
|
|
3174
|
+
publicationGeneration,
|
|
2811
3175
|
);
|
|
2812
3176
|
} else if (resolution.kind === "divergent") {
|
|
2813
3177
|
logger.warn("acp_final_text_diverged", {
|
|
@@ -2818,19 +3182,27 @@ export class AcpAgent implements Agent {
|
|
|
2818
3182
|
finalLength: resolution.final.text.length,
|
|
2819
3183
|
});
|
|
2820
3184
|
}
|
|
2821
|
-
}
|
|
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;
|
|
3196
|
+
}
|
|
3197
|
+
if (failedTerminal) {
|
|
3198
|
+
void decorationStart.then(async () => await this.#publishPromptPhaseIdle(id, adapter));
|
|
3199
|
+
return;
|
|
2822
3200
|
}
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
// watchdog rescued it.
|
|
2829
|
-
if (activePrompt) this.#settlePrompt(record, activePrompt);
|
|
2830
|
-
// agent_failed is correlated diagnostic state while the run is still live.
|
|
2831
|
-
// Only the authoritative agent_end boundary may publish end-of-turn usage,
|
|
2832
|
-
// title, and idle integration updates.
|
|
2833
|
-
if (event.type === "agent_end") await this.#emitEndOfTurnUpdates(id, adapter);
|
|
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
|
+
});
|
|
2834
3206
|
}
|
|
2835
3207
|
|
|
2836
3208
|
async #rejectPrompt(
|
|
@@ -2838,26 +3210,26 @@ export class AcpAgent implements Agent {
|
|
|
2838
3210
|
id: string,
|
|
2839
3211
|
waiter: PromptWaiter,
|
|
2840
3212
|
error: AcpSdkAdapterError,
|
|
3213
|
+
publishIdle = true,
|
|
2841
3214
|
): Promise<void> {
|
|
2842
3215
|
if (record.activePrompt !== waiter || waiter.settled) return;
|
|
2843
3216
|
record.activePrompt = undefined;
|
|
3217
|
+
if (!waiter.acknowledged) record.busy = record.backgroundBusy;
|
|
2844
3218
|
clearPromptWatchdog(waiter);
|
|
2845
3219
|
waiter.settled = true;
|
|
3220
|
+
this.#fenceRetiredPromptAcknowledgement(id, waiter);
|
|
2846
3221
|
waiter.deferredFrames.length = 0;
|
|
2847
3222
|
waiter.deferredActivityFrames.length = 0;
|
|
2848
3223
|
waiter.terminal = undefined;
|
|
2849
|
-
|
|
2850
|
-
record.settledPromptCorrelations.push(waiter.correlation);
|
|
2851
|
-
while (record.settledPromptCorrelations.length > SETTLED_PROMPT_CORRELATION_RETENTION)
|
|
2852
|
-
record.settledPromptCorrelations.shift();
|
|
2853
|
-
}
|
|
3224
|
+
this.#rememberSettledPromptCorrelation(id, record, waiter.correlation);
|
|
2854
3225
|
// The turn is over even though it ended badly, so the client's running phase has
|
|
2855
3226
|
// to be released. Rejection is the settlement; the advisory idle publication must
|
|
2856
3227
|
// never gate it (a backpressured transport would otherwise hold the rejection
|
|
2857
3228
|
// forever). Skipping the idle publish entirely is what left a client composer
|
|
2858
3229
|
// spinning on a turn that already produced its terminal.
|
|
2859
3230
|
waiter.reject(error);
|
|
2860
|
-
|
|
3231
|
+
this.#flushFailureDiagnostics(id, waiter, record.adapter);
|
|
3232
|
+
if (publishIdle) void this.#publishPromptPhaseIdle(id, record.adapter);
|
|
2861
3233
|
}
|
|
2862
3234
|
|
|
2863
3235
|
/**
|
|
@@ -2872,20 +3244,62 @@ export class AcpAgent implements Agent {
|
|
|
2872
3244
|
async #publishPromptPhaseIdle(id: string, adapter: AcpSdkAdapter): Promise<void> {
|
|
2873
3245
|
const record = this.#sessions.get(id);
|
|
2874
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> {
|
|
2875
3272
|
try {
|
|
2876
|
-
await
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
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
|
+
}
|
|
2883
3297
|
} catch {
|
|
2884
3298
|
// The client transport is gone; there is no phase left to restore.
|
|
2885
3299
|
}
|
|
2886
3300
|
}
|
|
2887
3301
|
|
|
2888
|
-
#settlePrompt(record: SessionRecord, waiter: PromptWaiter): void {
|
|
3302
|
+
#settlePrompt(id: string, record: SessionRecord, waiter: PromptWaiter): void {
|
|
2889
3303
|
if (record.activePrompt !== waiter || waiter.settled || !waiter.acknowledged || !waiter.terminal) return;
|
|
2890
3304
|
// A terminal captured before acknowledgement is only this prompt's terminal when the
|
|
2891
3305
|
// eventual acknowledgement correlates with it; otherwise it belonged to an earlier prompt.
|
|
@@ -2896,12 +3310,8 @@ export class AcpAgent implements Agent {
|
|
|
2896
3310
|
record.activePrompt = undefined;
|
|
2897
3311
|
clearPromptWatchdog(waiter);
|
|
2898
3312
|
waiter.settled = true;
|
|
2899
|
-
if (hasCorrelation(waiter.correlation)) {
|
|
2900
|
-
record.settledPromptCorrelations.push(waiter.correlation);
|
|
2901
|
-
while (record.settledPromptCorrelations.length > SETTLED_PROMPT_CORRELATION_RETENTION)
|
|
2902
|
-
record.settledPromptCorrelations.shift();
|
|
2903
|
-
}
|
|
2904
3313
|
const { outcome } = waiter.terminal;
|
|
3314
|
+
this.#rememberSettledPromptCorrelation(id, record, waiter.correlation);
|
|
2905
3315
|
if (outcome.kind === "stopped") {
|
|
2906
3316
|
waiter.resolve({ stopReason: outcome.reason });
|
|
2907
3317
|
return;
|
|
@@ -2909,7 +3319,7 @@ export class AcpAgent implements Agent {
|
|
|
2909
3319
|
waiter.reject(new AcpSdkAdapterError(outcome.code, outcome.message));
|
|
2910
3320
|
}
|
|
2911
3321
|
|
|
2912
|
-
async #emitEndOfTurnUpdates(id: string, adapter: AcpSdkAdapter): Promise<void> {
|
|
3322
|
+
async #emitEndOfTurnUpdates(id: string, adapter: AcpSdkAdapter, publicationGeneration: number): Promise<void> {
|
|
2913
3323
|
let usage: JsonObject | undefined;
|
|
2914
3324
|
let title: string | undefined;
|
|
2915
3325
|
try {
|
|
@@ -2930,42 +3340,70 @@ export class AcpAgent implements Agent {
|
|
|
2930
3340
|
}
|
|
2931
3341
|
// The prompt settled before these queries were asked, so a host that answers late
|
|
2932
3342
|
// must not report the session idle after the next turn has already started.
|
|
2933
|
-
|
|
2934
|
-
|
|
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 });
|
|
2935
3376
|
await this.#publishSessionUpdate(
|
|
2936
3377
|
id,
|
|
2937
3378
|
{
|
|
2938
3379
|
sessionId: id,
|
|
2939
3380
|
update: {
|
|
2940
|
-
sessionUpdate: "
|
|
2941
|
-
|
|
2942
|
-
|
|
3381
|
+
sessionUpdate: "session_info_update",
|
|
3382
|
+
...(title ? { title } : {}),
|
|
3383
|
+
updatedAt,
|
|
3384
|
+
_meta: { gjcPhase: "idle", running: false, gjcRunning: false },
|
|
2943
3385
|
},
|
|
2944
3386
|
},
|
|
2945
3387
|
adapter,
|
|
3388
|
+
publicationGeneration,
|
|
2946
3389
|
);
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
updatedAt,
|
|
2958
|
-
_meta: { gjcPhase: "idle", running: false, gjcRunning: false },
|
|
2959
|
-
},
|
|
2960
|
-
},
|
|
2961
|
-
adapter,
|
|
2962
|
-
);
|
|
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;
|
|
2963
3400
|
}
|
|
2964
3401
|
|
|
2965
3402
|
async #publishSessionUpdate(
|
|
2966
3403
|
id: string,
|
|
2967
3404
|
notification: SessionNotification,
|
|
2968
3405
|
expectedAdapter?: AcpSdkAdapter,
|
|
3406
|
+
publicationGeneration?: number,
|
|
2969
3407
|
): Promise<void> {
|
|
2970
3408
|
// A session that is gone has no update channel, so this is a drop and not a failure:
|
|
2971
3409
|
// whoever owned the frame is the one that ended the session. Callers whose bookkeeping
|
|
@@ -2974,7 +3412,27 @@ export class AcpAgent implements Agent {
|
|
|
2974
3412
|
if (!record || (expectedAdapter && record.adapter !== expectedAdapter)) return;
|
|
2975
3413
|
try {
|
|
2976
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
|
+
}
|
|
2977
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;
|
|
2978
3436
|
const failure = this.#frameProcessingFailure(error);
|
|
2979
3437
|
await this.#failSession(id, record.adapter, failure);
|
|
2980
3438
|
throw failure;
|
|
@@ -3510,17 +3968,8 @@ export class AcpAgent implements Agent {
|
|
|
3510
3968
|
record.adapter,
|
|
3511
3969
|
);
|
|
3512
3970
|
}
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
{
|
|
3516
|
-
sessionId: id,
|
|
3517
|
-
update: {
|
|
3518
|
-
sessionUpdate: "session_info_update",
|
|
3519
|
-
_meta: { gjcPhase: "idle", running: false, gjcRunning: false },
|
|
3520
|
-
},
|
|
3521
|
-
},
|
|
3522
|
-
record.adapter,
|
|
3523
|
-
);
|
|
3971
|
+
const current = this.#sessions.get(id);
|
|
3972
|
+
if (current) await this.#publishPromptPhase(id, current.adapter, this.#promptPhaseOwner(current));
|
|
3524
3973
|
})().catch(() => undefined);
|
|
3525
3974
|
});
|
|
3526
3975
|
}
|
|
@@ -3679,6 +4128,12 @@ export class AcpAgent implements Agent {
|
|
|
3679
4128
|
this.#ownedSessionIds.clear();
|
|
3680
4129
|
this.#knownSessionMcpServers.clear();
|
|
3681
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();
|
|
3682
4137
|
this.#pendingDeleteLocators.clear();
|
|
3683
4138
|
this.#pendingCloseIdempotencyKeys.clear();
|
|
3684
4139
|
if (this.#lifecycleOperations.size === 0) this.#lifecycleOperations.clear();
|