maka-agent 0.2.0-dev.30.20260912 → 0.2.0-dev.31.20260913
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/THIRD_PARTY_NOTICES.txt +392 -0
- package/dist/acp/maka-acp-agent.js +8 -2
- package/dist/acp/prompt-content.js +225 -0
- package/dist/acp/session-event-mapper.js +103 -0
- package/dist/acp/session-registry.js +654 -17
- package/dist/acp/stdio-server.js +10 -1
- package/dist/cli-core.js +1 -1
- package/dist/pi-transcript-tools.js +3 -2
- package/dist/pi-transcript.js +12 -2
- package/dist/runtime-host-session-channel.js +63 -9
- package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
- package/node_modules/@maka/core/dist/artifacts.js +6 -0
- package/node_modules/@maka/core/dist/attachments.js +7 -2
- package/node_modules/@maka/core/dist/durable-tool-result-projection.js +3 -1
- package/node_modules/@maka/core/dist/events.js +21 -0
- package/node_modules/@maka/core/dist/goal.js +44 -2
- package/node_modules/@maka/core/dist/permission.js +0 -1
- package/node_modules/@maka/core/dist/runtime-boundary.js +73 -29
- package/node_modules/@maka/core/dist/runtime-event.js +6 -3
- package/node_modules/@maka/core/dist/runtime-handoff.js +10 -2
- package/node_modules/@maka/core/dist/runtime-logical-execution.js +39 -14
- package/node_modules/@maka/core/dist/tool-result-archive-evidence.js +3 -1
- package/node_modules/@maka/core/dist/tool-result-record-schema.js +2 -1
- package/node_modules/@maka/core/dist/unified-diff.js +33 -26
- package/node_modules/@maka/runtime/dist/ai-sdk-compaction.js +49 -178
- package/node_modules/@maka/runtime/dist/ai-sdk-message-projection.js +7 -2
- package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +15 -53
- package/node_modules/@maka/runtime/dist/bash-model-output.js +7 -1
- package/node_modules/@maka/runtime/dist/bash-tail-buffer.js +13 -52
- package/node_modules/@maka/runtime/dist/builtin-tools.js +59 -111
- package/node_modules/@maka/runtime/dist/child-fd-input.js +3 -2
- package/node_modules/@maka/runtime/dist/context-budget-policy.js +1 -11
- package/node_modules/@maka/runtime/dist/context-budget.js +12 -1
- package/node_modules/@maka/runtime/dist/conversation-copy.js +104 -9
- package/node_modules/@maka/runtime/dist/filesystem-executor.js +10 -6
- package/node_modules/@maka/runtime/dist/filesystem-worker/client.js +77 -19
- package/node_modules/@maka/runtime/dist/filesystem-worker/launch-spec.js +66 -55
- package/node_modules/@maka/runtime/dist/filesystem-worker/operations.js +34 -97
- package/node_modules/@maka/runtime/dist/filesystem-worker/protocol.js +25 -10
- package/node_modules/@maka/runtime/dist/filesystem-worker/worker-entry.js +2 -0
- package/node_modules/@maka/runtime/dist/glob-search.js +74 -0
- package/node_modules/@maka/runtime/dist/goal-continuation.js +78 -8
- package/node_modules/@maka/runtime/dist/goal-state.js +7 -0
- package/node_modules/@maka/runtime/dist/grep-search.js +155 -0
- package/node_modules/@maka/runtime/dist/ledger-tool-result-archive-reader.js +84 -94
- package/node_modules/@maka/runtime/dist/model-history.js +25 -3
- package/node_modules/@maka/runtime/dist/pipe-tail-collector.js +6 -9
- package/node_modules/@maka/runtime/dist/read-page.js +231 -0
- package/node_modules/@maka/runtime/dist/request-projection.js +2 -15
- package/node_modules/@maka/runtime/dist/ripgrep-executable.js +62 -0
- package/node_modules/@maka/runtime/dist/ripgrep-guidance.js +81 -0
- package/node_modules/@maka/runtime/dist/runtime-event-read-model.js +187 -20
- package/node_modules/@maka/runtime/dist/runtime-read-model.js +2 -1
- package/node_modules/@maka/runtime/dist/sandbox/linux-profile-path.js +7 -1
- package/node_modules/@maka/runtime/dist/sandbox/linux-sandbox.js +20 -5
- package/node_modules/@maka/runtime/dist/session-manager.js +6 -6
- package/node_modules/@maka/runtime/dist/session-recap.js +9 -5
- package/node_modules/@maka/runtime/dist/shell-exec.js +7 -25
- package/node_modules/@maka/runtime/dist/shell-run-manager.js +1 -1
- package/node_modules/@maka/runtime/dist/shell-run-tool-result.js +1 -1
- package/node_modules/@maka/runtime/dist/shell-tools.js +6 -12
- package/node_modules/@maka/runtime/dist/tool-availability.js +0 -1
- package/node_modules/@maka/runtime/dist/tool-output.js +1 -1
- package/node_modules/@maka/runtime/dist/tool-result-archive-capability.js +24 -36
- package/node_modules/@maka/runtime/dist/tool-result-archive-resource.js +32 -527
- package/node_modules/@maka/runtime/dist/tool-result-archive-transition.js +17 -119
- package/node_modules/@maka/runtime/dist/tool-result-archive.js +7 -25
- package/node_modules/@maka/runtime/dist/tool-runtime.js +7 -5
- package/node_modules/@maka/runtime/dist/workers/filesystem-worker.js +6933 -3681
- package/node_modules/@maka/runtime/dist/workspace-executor.js +20 -24
- package/node_modules/@maka/runtime/package.json +2 -0
- package/node_modules/@maka/runtime-host/dist/client/host-handoff-copy.js +185 -191
- package/node_modules/@maka/runtime-host/dist/client/reconnecting-connection.js +3 -0
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +5 -1
- package/node_modules/@maka/runtime-host/dist/protocol/message.js +2 -2
- package/node_modules/@maka/runtime-host/dist/protocol/operations.js +1 -0
- package/node_modules/@maka/runtime-host/dist/protocol/session-continuity.js +2 -2
- package/node_modules/@maka/runtime-host/dist/protocol/turn.js +10 -2
- package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +38 -0
- package/node_modules/@maka/runtime-host/dist/server/execution-artifacts.js +2 -8
- package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +2 -0
- package/node_modules/@maka/runtime-host/dist/server/goal-coordinator.js +81 -7
- package/node_modules/@maka/runtime-host/dist/server/hosted-execution-tool-profile.js +13 -13
- package/node_modules/@maka/runtime-host/dist/server/interaction-coordinator.js +65 -0
- package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +1 -0
- package/node_modules/@maka/runtime-host/dist/server/session-continuity-coordinator.js +5 -2
- package/node_modules/@maka/runtime-host/dist/server/session-transcript-reader.js +88 -72
- package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-action-gate.js +21 -4
- package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +145 -1
- package/node_modules/@maka/storage/dist/agent-run-store.js +5 -2
- package/node_modules/@maka/storage/dist/execution-stores.js +2 -1
- package/node_modules/@maka/storage/dist/runtime-transcript-query.js +27 -14
- package/node_modules/@maka/storage/dist/sqlite-runtime-store.js +69 -3
- package/node_modules/balanced-match/LICENSE.md +23 -0
- package/node_modules/balanced-match/README.md +57 -0
- package/node_modules/balanced-match/dist/commonjs/index.js +59 -0
- package/node_modules/balanced-match/dist/commonjs/package.json +3 -0
- package/node_modules/balanced-match/dist/esm/index.js +54 -0
- package/node_modules/balanced-match/dist/esm/package.json +3 -0
- package/node_modules/balanced-match/package.json +68 -0
- package/node_modules/brace-expansion/LICENSE +23 -0
- package/node_modules/brace-expansion/README.md +105 -0
- package/node_modules/brace-expansion/dist/commonjs/index.js +289 -0
- package/node_modules/brace-expansion/dist/commonjs/package.json +3 -0
- package/node_modules/brace-expansion/dist/esm/index.js +285 -0
- package/node_modules/brace-expansion/dist/esm/package.json +3 -0
- package/node_modules/brace-expansion/package.json +64 -0
- package/node_modules/glob/LICENSE.md +63 -0
- package/node_modules/glob/README.md +1203 -0
- package/node_modules/glob/dist/commonjs/glob.js +248 -0
- package/node_modules/glob/dist/commonjs/has-magic.js +27 -0
- package/node_modules/glob/dist/commonjs/ignore.js +119 -0
- package/node_modules/glob/dist/commonjs/index.js +68 -0
- package/node_modules/glob/dist/commonjs/index.min.js +4 -0
- package/node_modules/glob/dist/commonjs/package.json +3 -0
- package/node_modules/glob/dist/commonjs/pattern.js +223 -0
- package/node_modules/glob/dist/commonjs/processor.js +301 -0
- package/node_modules/glob/dist/commonjs/walker.js +387 -0
- package/node_modules/glob/dist/esm/glob.js +244 -0
- package/node_modules/glob/dist/esm/has-magic.js +23 -0
- package/node_modules/glob/dist/esm/ignore.js +115 -0
- package/node_modules/glob/dist/esm/index.js +55 -0
- package/node_modules/glob/dist/esm/index.min.js +4 -0
- package/node_modules/glob/dist/esm/package.json +3 -0
- package/node_modules/glob/dist/esm/pattern.js +219 -0
- package/node_modules/glob/dist/esm/processor.js +294 -0
- package/node_modules/glob/dist/esm/walker.js +381 -0
- package/node_modules/glob/package.json +98 -0
- package/node_modules/minimatch/LICENSE.md +55 -0
- package/node_modules/minimatch/README.md +528 -0
- package/node_modules/minimatch/dist/commonjs/assert-valid-pattern.js +14 -0
- package/node_modules/minimatch/dist/commonjs/ast.js +845 -0
- package/node_modules/minimatch/dist/commonjs/brace-expressions.js +150 -0
- package/node_modules/minimatch/dist/commonjs/escape.js +30 -0
- package/node_modules/minimatch/dist/commonjs/index.js +1127 -0
- package/node_modules/minimatch/dist/commonjs/package.json +3 -0
- package/node_modules/minimatch/dist/commonjs/unescape.js +38 -0
- package/node_modules/minimatch/dist/esm/assert-valid-pattern.js +10 -0
- package/node_modules/minimatch/dist/esm/ast.js +841 -0
- package/node_modules/minimatch/dist/esm/brace-expressions.js +146 -0
- package/node_modules/minimatch/dist/esm/escape.js +26 -0
- package/node_modules/minimatch/dist/esm/index.js +1114 -0
- package/node_modules/minimatch/dist/esm/package.json +3 -0
- package/node_modules/minimatch/dist/esm/unescape.js +34 -0
- package/node_modules/minimatch/package.json +73 -0
- package/node_modules/minipass/LICENSE.md +55 -0
- package/node_modules/minipass/README.md +825 -0
- package/node_modules/minipass/dist/commonjs/index.js +1038 -0
- package/node_modules/minipass/dist/commonjs/package.json +3 -0
- package/node_modules/minipass/dist/esm/index.js +1020 -0
- package/node_modules/minipass/dist/esm/package.json +3 -0
- package/node_modules/minipass/package.json +77 -0
- package/node_modules/path-scurry/LICENSE.md +55 -0
- package/node_modules/path-scurry/README.md +636 -0
- package/node_modules/path-scurry/dist/commonjs/index.js +2018 -0
- package/node_modules/path-scurry/dist/commonjs/package.json +3 -0
- package/node_modules/path-scurry/dist/esm/index.js +1983 -0
- package/node_modules/path-scurry/dist/esm/package.json +3 -0
- package/node_modules/path-scurry/node_modules/lru-cache/LICENSE.md +55 -0
- package/node_modules/path-scurry/node_modules/lru-cache/README.md +469 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/diagnostics-channel.js +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.js +1726 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/index.min.js +2 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/browser/perf.js +10 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel-cjs.d.cts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/diagnostics-channel.js +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.js +1726 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/index.min.js +2 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/diagnostics-channel.js +9 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.js +1726 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/index.min.js +2 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/node/perf.js +10 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/package.json +3 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/commonjs/perf.js +10 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/diagnostics-channel.js +4 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.js +1722 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/index.min.js +2 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/browser/perf.js +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel-esm.d.mts.map +1 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/diagnostics-channel.js +19 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.js +1722 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/index.min.js +2 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/diagnostics-channel.js +6 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.js +1722 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/index.min.js +2 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/node/perf.js +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/package.json +3 -0
- package/node_modules/path-scurry/node_modules/lru-cache/dist/esm/perf.js +7 -0
- package/node_modules/path-scurry/node_modules/lru-cache/package.json +154 -0
- package/node_modules/path-scurry/package.json +72 -0
- package/package.json +1 -1
- package/node_modules/@maka/runtime/dist/active-tool-result-prune.js +0 -248
- package/node_modules/@maka/runtime/dist/active-tool-result-working-set.js +0 -218
- package/node_modules/@maka/runtime/dist/archive-read-tool.js +0 -115
|
@@ -20,17 +20,31 @@ import { randomUUID } from 'node:crypto';
|
|
|
20
20
|
import { realpath } from 'node:fs/promises';
|
|
21
21
|
import { isAbsolute, normalize } from 'node:path';
|
|
22
22
|
import { RequestError, } from '@agentclientprotocol/sdk';
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
23
|
+
import { isRuntimeHostTerminalTurn } from '@maka/runtime-host/adapter';
|
|
24
|
+
import { readRuntimeHostConnectionCatalog, readRuntimeHostSessionCatalogPage, RuntimeHostCatalogReadError, RuntimeHostOperationError, RuntimeHostRequestInterruptedError, RuntimeHostSubscriptionError, RuntimeHostSessionCatalogRevisionChangedError, } from '@maka/runtime-host/client';
|
|
25
|
+
import { SESSION_CATALOG_CURSOR_MAX_BYTES, SESSION_CATALOG_CWD_MAX_BYTES, HOST_OPERATION_SPECS, } from '@maka/runtime-host/protocol';
|
|
26
|
+
import { RuntimeHostSessionChannel } from '../runtime-host-session-channel.js';
|
|
27
|
+
import { RuntimeHostSessionUpdateError, getRuntimeHostSession, requireRuntimeHostSessionProjection, updateRuntimeHostSession, } from '../runtime-host-session-update.js';
|
|
26
28
|
import { AcpSessionConfigInputError, createAcpSessionConfigPatch, projectAcpSessionConfigOptions, validateAcpSessionConfigOptionRequest, } from './session-configuration.js';
|
|
29
|
+
import { AcpSessionEventMapper } from './session-event-mapper.js';
|
|
30
|
+
import { mapAcpPromptContent, publishAcpPromptAttachments } from './prompt-content.js';
|
|
27
31
|
const ACP_SESSION_CURSOR_MAX_BYTES = 8 * 1024;
|
|
32
|
+
const ADMISSION_QUERY_MAX_ATTEMPTS = 5;
|
|
33
|
+
const ADMISSION_QUERY_TIMEOUT_MS = 1_000;
|
|
34
|
+
const ADMISSION_QUERY_RETRY_MS = 25;
|
|
28
35
|
/** Owns all Runtime Host resources associated with one ACP connection. */
|
|
29
36
|
export class AcpSessionRegistry {
|
|
30
37
|
#connect;
|
|
31
38
|
#newSessionId;
|
|
39
|
+
#newTurnId;
|
|
32
40
|
#inFlightOperations = new Set();
|
|
33
41
|
#ownedSessionIds = new Set();
|
|
42
|
+
#attachments = new Map();
|
|
43
|
+
#attachmentOpenControllers = new Map();
|
|
44
|
+
#attachmentConfigurations = new Map();
|
|
45
|
+
#pendingConfigSets = new Map();
|
|
46
|
+
#activePrompts = new Map();
|
|
47
|
+
#sessionCloseTasks = new Map();
|
|
34
48
|
#connection;
|
|
35
49
|
#connectTask;
|
|
36
50
|
#connectAbortController;
|
|
@@ -40,6 +54,7 @@ export class AcpSessionRegistry {
|
|
|
40
54
|
constructor(options) {
|
|
41
55
|
this.#connect = options.connect;
|
|
42
56
|
this.#newSessionId = options.newSessionId ?? randomUUID;
|
|
57
|
+
this.#newTurnId = options.newTurnId ?? randomUUID;
|
|
43
58
|
}
|
|
44
59
|
async create(params) {
|
|
45
60
|
this.#assertOpen('session.create');
|
|
@@ -61,7 +76,54 @@ export class AcpSessionRegistry {
|
|
|
61
76
|
catch (error) {
|
|
62
77
|
throw requestErrorFromConfigInput(error);
|
|
63
78
|
}
|
|
64
|
-
|
|
79
|
+
const configuration = this.#attachmentConfigurations.get(params.sessionId);
|
|
80
|
+
const operation = this.#track(configuration
|
|
81
|
+
? this.#queueConfiguration(configuration, () => this.#setConfigOption(params, configuration))
|
|
82
|
+
: this.#setConfigOption(params));
|
|
83
|
+
let pending = this.#pendingConfigSets.get(params.sessionId);
|
|
84
|
+
if (!pending) {
|
|
85
|
+
pending = new Set();
|
|
86
|
+
this.#pendingConfigSets.set(params.sessionId, pending);
|
|
87
|
+
}
|
|
88
|
+
pending.add(operation);
|
|
89
|
+
try {
|
|
90
|
+
return await operation;
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
pending.delete(operation);
|
|
94
|
+
if (pending.size === 0)
|
|
95
|
+
this.#pendingConfigSets.delete(params.sessionId);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async prompt(params, context) {
|
|
99
|
+
this.#assertOpen('turn.start');
|
|
100
|
+
this.#assertOwned(params.sessionId);
|
|
101
|
+
return this.#track(this.#prompt(params, context));
|
|
102
|
+
}
|
|
103
|
+
async cancel(params) {
|
|
104
|
+
if (this.#closing)
|
|
105
|
+
return;
|
|
106
|
+
await this.#cancelSession(params.sessionId);
|
|
107
|
+
}
|
|
108
|
+
async close(params) {
|
|
109
|
+
this.#assertOpen('session.close');
|
|
110
|
+
const existing = this.#sessionCloseTasks.get(params.sessionId);
|
|
111
|
+
if (existing)
|
|
112
|
+
return existing;
|
|
113
|
+
this.#assertOwned(params.sessionId);
|
|
114
|
+
this.#ownedSessionIds.delete(params.sessionId);
|
|
115
|
+
const configuration = this.#attachmentConfigurations.get(params.sessionId);
|
|
116
|
+
const delivery = configuration?.delivery;
|
|
117
|
+
this.#attachmentConfigurations.delete(params.sessionId);
|
|
118
|
+
const task = this.#track(this.#closeSession(params.sessionId, delivery));
|
|
119
|
+
this.#sessionCloseTasks.set(params.sessionId, task);
|
|
120
|
+
const forget = () => {
|
|
121
|
+
if (this.#sessionCloseTasks.get(params.sessionId) === task) {
|
|
122
|
+
this.#sessionCloseTasks.delete(params.sessionId);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
void task.then(forget, forget);
|
|
126
|
+
return task;
|
|
65
127
|
}
|
|
66
128
|
dispose() {
|
|
67
129
|
this.#closing = true;
|
|
@@ -69,6 +131,499 @@ export class AcpSessionRegistry {
|
|
|
69
131
|
this.#disposeTask ??= this.#dispose();
|
|
70
132
|
return this.#disposeTask;
|
|
71
133
|
}
|
|
134
|
+
async #prompt(params, context) {
|
|
135
|
+
const turnId = this.#newTurnId();
|
|
136
|
+
const active = {
|
|
137
|
+
sessionId: params.sessionId,
|
|
138
|
+
turnId,
|
|
139
|
+
mapper: new AcpSessionEventMapper({
|
|
140
|
+
sessionId: params.sessionId,
|
|
141
|
+
notify: async (notification) => {
|
|
142
|
+
if (!this.#closing && this.#ownedSessionIds.has(params.sessionId)) {
|
|
143
|
+
await context.notify(notification);
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
}),
|
|
147
|
+
waiters: new Set(),
|
|
148
|
+
dispatchStarted: false,
|
|
149
|
+
startRequestSettled: false,
|
|
150
|
+
admissionSettled: false,
|
|
151
|
+
cancelled: false,
|
|
152
|
+
finished: false,
|
|
153
|
+
};
|
|
154
|
+
this.#addActivePrompt(active);
|
|
155
|
+
const onAbort = () => {
|
|
156
|
+
void this.#cancelPrompt(active).catch(() => undefined);
|
|
157
|
+
};
|
|
158
|
+
context.signal.addEventListener('abort', onAbort, { once: true });
|
|
159
|
+
if (context.signal.aborted)
|
|
160
|
+
onAbort();
|
|
161
|
+
try {
|
|
162
|
+
const content = await mapAcpPromptContent(params.prompt);
|
|
163
|
+
let startInput;
|
|
164
|
+
try {
|
|
165
|
+
startInput = HOST_OPERATION_SPECS['turn.start'].decodeInput({
|
|
166
|
+
sessionId: params.sessionId,
|
|
167
|
+
turnId,
|
|
168
|
+
content,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
throw RequestError.invalidParams({ field: 'prompt', reason: 'runtime_host_admission_rejected' }, 'Prompt cannot be admitted by Runtime Host');
|
|
173
|
+
}
|
|
174
|
+
if (active.cancelled)
|
|
175
|
+
return { stopReason: await this.#cancelledStopReason(active) };
|
|
176
|
+
const connection = await this.#getConnection('subscription.open');
|
|
177
|
+
let attachment;
|
|
178
|
+
try {
|
|
179
|
+
attachment = await this.#ensureAttachment(params.sessionId, connection, context.notify);
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
if (active.cancelled)
|
|
183
|
+
return { stopReason: await this.#cancelledStopReason(active) };
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
active.attachment = attachment;
|
|
187
|
+
this.#wake(active);
|
|
188
|
+
if (active.cancelled)
|
|
189
|
+
return { stopReason: await this.#cancelledStopReason(active) };
|
|
190
|
+
try {
|
|
191
|
+
startInput = {
|
|
192
|
+
...startInput,
|
|
193
|
+
content: await publishAcpPromptAttachments(content, {
|
|
194
|
+
sessionId: params.sessionId,
|
|
195
|
+
connection,
|
|
196
|
+
assertActive: () => {
|
|
197
|
+
if (active.cancelled)
|
|
198
|
+
throw new Error('ACP prompt cancelled before Turn admission');
|
|
199
|
+
this.#assertOpen('turn.start');
|
|
200
|
+
this.#assertOwned(params.sessionId);
|
|
201
|
+
},
|
|
202
|
+
}),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
if (error instanceof RequestError)
|
|
207
|
+
throw error;
|
|
208
|
+
throw requestErrorFromRuntimeHost(error, 'artifact.ingest');
|
|
209
|
+
}
|
|
210
|
+
if (active.cancelled)
|
|
211
|
+
return { stopReason: await this.#cancelledStopReason(active) };
|
|
212
|
+
const observation = this.#consumePromptEvents(active, attachment.eventsForTurn(turnId));
|
|
213
|
+
// Mark the observer as handled immediately: turn.start may still be in flight
|
|
214
|
+
// when the live subscription reports a failure.
|
|
215
|
+
void observation.catch(() => undefined);
|
|
216
|
+
active.dispatchStarted = true;
|
|
217
|
+
this.#wake(active);
|
|
218
|
+
try {
|
|
219
|
+
const result = await connection.request('turn.start', startInput);
|
|
220
|
+
active.startRequestSettled = true;
|
|
221
|
+
active.admissionSettled = true;
|
|
222
|
+
if (result.kind === 'started')
|
|
223
|
+
active.startedTurn = result.turn;
|
|
224
|
+
this.#wake(active);
|
|
225
|
+
if (result.kind === 'blocked') {
|
|
226
|
+
const error = new Error('Runtime Host blocked the requested Turn');
|
|
227
|
+
attachment.failTurn(turnId, error);
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
// A lost dispatched response does not establish whether Host admitted
|
|
233
|
+
// this Turn. Retain this attempt until subscription or query facts do.
|
|
234
|
+
active.startRequestSettled = true;
|
|
235
|
+
active.admissionSettled ||= !(error instanceof RuntimeHostRequestInterruptedError && error.dispatch === 'dispatched');
|
|
236
|
+
if (!active.admissionSettled) {
|
|
237
|
+
this.#queryPromptAdmission(active, connection);
|
|
238
|
+
}
|
|
239
|
+
this.#wake(active);
|
|
240
|
+
attachment.failTurn(turnId, error);
|
|
241
|
+
if (!active.cancelled)
|
|
242
|
+
throw requestErrorFromRuntimeHost(error, 'turn.start');
|
|
243
|
+
}
|
|
244
|
+
if (active.cancelled) {
|
|
245
|
+
await active.stopTask?.catch(() => undefined);
|
|
246
|
+
return { stopReason: await this.#cancelledStopReason(active) };
|
|
247
|
+
}
|
|
248
|
+
const stopReason = await observation;
|
|
249
|
+
return { stopReason };
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
// A failed projection must not leave the corresponding Host Turn running.
|
|
253
|
+
active.stopTask ??= this.#stopPromptWhenObservable(active);
|
|
254
|
+
await active.stopTask.catch(() => undefined);
|
|
255
|
+
if (active.cancelled)
|
|
256
|
+
return { stopReason: await this.#cancelledStopReason(active) };
|
|
257
|
+
if (active.admissionFailure)
|
|
258
|
+
throw active.admissionFailure;
|
|
259
|
+
if (error instanceof RequestError)
|
|
260
|
+
throw error;
|
|
261
|
+
throw requestErrorFromRuntimeHost(error, 'subscription.open');
|
|
262
|
+
}
|
|
263
|
+
finally {
|
|
264
|
+
context.signal.removeEventListener('abort', onAbort);
|
|
265
|
+
// A terminal subscription event can precede the Stop response. Retain this
|
|
266
|
+
// prompt so close/dispose cannot release its connection while Stop is in flight.
|
|
267
|
+
await active.stopTask?.catch(() => undefined);
|
|
268
|
+
active.finished = true;
|
|
269
|
+
this.#wake(active);
|
|
270
|
+
this.#removeActivePrompt(active);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
async #consumePromptEvents(active, events) {
|
|
274
|
+
try {
|
|
275
|
+
for await (const event of events) {
|
|
276
|
+
if (!active.cancelled)
|
|
277
|
+
await active.mapper.accept(event);
|
|
278
|
+
}
|
|
279
|
+
return active.cancelled ? this.#cancelledStopReason(active) : 'end_turn';
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
if (active.cancelled)
|
|
283
|
+
return this.#cancelledStopReason(active);
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async #cancelledStopReason(active) {
|
|
288
|
+
await active.mapper.flush();
|
|
289
|
+
return 'cancelled';
|
|
290
|
+
}
|
|
291
|
+
#cancelSession(sessionId) {
|
|
292
|
+
const active = [...(this.#activePrompts.get(sessionId) ?? [])];
|
|
293
|
+
const cancellations = active.map((prompt) => this.#cancelPrompt(prompt));
|
|
294
|
+
this.#attachmentOpenControllers.get(sessionId)?.abort();
|
|
295
|
+
const attachment = this.#attachments.get(sessionId);
|
|
296
|
+
if (attachment) {
|
|
297
|
+
cancellations.push(attachment.then(async (opened) => {
|
|
298
|
+
const root = opened.snapshot.rootTurn;
|
|
299
|
+
// Local prompts already latch cancellation across pending turn.start.
|
|
300
|
+
// An idle attachment may also observe a Turn started by another client.
|
|
301
|
+
if (root &&
|
|
302
|
+
!isRuntimeHostTerminalTurn(root) &&
|
|
303
|
+
!active.some((prompt) => prompt.turnId === root.turnId)) {
|
|
304
|
+
await this.#connection?.request('turn.stop', {
|
|
305
|
+
sessionId: root.sessionId,
|
|
306
|
+
turnId: root.turnId,
|
|
307
|
+
runId: root.runId,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}, () => undefined));
|
|
311
|
+
}
|
|
312
|
+
return Promise.allSettled(cancellations);
|
|
313
|
+
}
|
|
314
|
+
async #cancelPrompt(active) {
|
|
315
|
+
active.cancelled = true;
|
|
316
|
+
this.#wake(active);
|
|
317
|
+
if ([...(this.#activePrompts.get(active.sessionId) ?? [])].every((prompt) => prompt.cancelled && !prompt.dispatchStarted)) {
|
|
318
|
+
this.#attachmentOpenControllers.get(active.sessionId)?.abort();
|
|
319
|
+
}
|
|
320
|
+
active.stopTask ??= this.#stopPromptWhenObservable(active);
|
|
321
|
+
await Promise.all([
|
|
322
|
+
active.mapper.flush(),
|
|
323
|
+
active.stopTask.catch((error) => {
|
|
324
|
+
// End only this prompt's observation. Failed delivery does not establish
|
|
325
|
+
// a terminal Host Turn, and teardown still receives the original error.
|
|
326
|
+
active.attachment?.failTurn(active.turnId, error);
|
|
327
|
+
throw error;
|
|
328
|
+
}),
|
|
329
|
+
]);
|
|
330
|
+
}
|
|
331
|
+
async #stopPromptWhenObservable(active) {
|
|
332
|
+
if (!active.dispatchStarted)
|
|
333
|
+
return;
|
|
334
|
+
while (!active.finished) {
|
|
335
|
+
const observed = active.attachment?.snapshot.rootTurn;
|
|
336
|
+
// Subscription teardown can precede the start response. Keep the admitted
|
|
337
|
+
// identity until exact Stop completes, even when observation has ended.
|
|
338
|
+
const root = observed?.turnId === active.turnId ? observed : active.startedTurn;
|
|
339
|
+
if (root) {
|
|
340
|
+
if (isRuntimeHostTerminalTurn(root))
|
|
341
|
+
return;
|
|
342
|
+
const connection = this.#connection;
|
|
343
|
+
if (!connection)
|
|
344
|
+
return;
|
|
345
|
+
try {
|
|
346
|
+
await connection.request('turn.stop', {
|
|
347
|
+
sessionId: root.sessionId,
|
|
348
|
+
turnId: root.turnId,
|
|
349
|
+
runId: root.runId,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
console.error('[acp] Host Stop delivery failed:', error);
|
|
354
|
+
throw error;
|
|
355
|
+
}
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (active.admissionSettled && active.startRequestSettled)
|
|
359
|
+
return;
|
|
360
|
+
if (active.admissionFailure) {
|
|
361
|
+
console.error('[acp] Host Turn admission remains unknown:', active.admissionFailure);
|
|
362
|
+
throw active.admissionFailure;
|
|
363
|
+
}
|
|
364
|
+
await this.#waitForPromptChange(active);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
#queryPromptAdmission(active, connection) {
|
|
368
|
+
// Recovery and the lost start response can both request this read. Keep one
|
|
369
|
+
// bounded retry task; neither a healthy subscription nor a failed query
|
|
370
|
+
// establishes whether a dispatched start was admitted.
|
|
371
|
+
active.admissionQuery ??= this.#readPromptAdmission(active, connection);
|
|
372
|
+
}
|
|
373
|
+
async #readPromptAdmission(active, connection) {
|
|
374
|
+
let lastError;
|
|
375
|
+
for (let attempt = 0; attempt < ADMISSION_QUERY_MAX_ATTEMPTS; attempt += 1) {
|
|
376
|
+
if (active.finished || active.admissionSettled || (this.#closing && attempt > 0))
|
|
377
|
+
return;
|
|
378
|
+
const observed = active.attachment?.snapshot.rootTurn;
|
|
379
|
+
if (observed?.turnId === active.turnId) {
|
|
380
|
+
active.startedTurn = observed;
|
|
381
|
+
active.admissionSettled = true;
|
|
382
|
+
this.#wake(active);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
const turn = await connection.request('turn.query', { sessionId: active.sessionId, turnId: active.turnId }, ADMISSION_QUERY_TIMEOUT_MS);
|
|
387
|
+
if (!active.finished && !active.admissionSettled) {
|
|
388
|
+
active.startedTurn = turn;
|
|
389
|
+
active.admissionSettled = true;
|
|
390
|
+
this.#wake(active);
|
|
391
|
+
}
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
if (error instanceof RuntimeHostOperationError && error.code === 'not_found') {
|
|
396
|
+
active.admissionSettled = true;
|
|
397
|
+
this.#wake(active);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
lastError = error;
|
|
401
|
+
}
|
|
402
|
+
if (active.finished || active.admissionSettled || this.#closing)
|
|
403
|
+
return;
|
|
404
|
+
if (attempt + 1 < ADMISSION_QUERY_MAX_ATTEMPTS) {
|
|
405
|
+
await this.#waitForPromptChange(active, ADMISSION_QUERY_RETRY_MS * 2 ** attempt);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (active.attachment?.snapshot.rootTurn?.turnId === active.turnId) {
|
|
409
|
+
this.#wake(active);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
active.admissionFailure = RequestError.internalError({
|
|
413
|
+
source: 'runtime_host',
|
|
414
|
+
operation: 'turn.query',
|
|
415
|
+
code: 'outcome_unknown',
|
|
416
|
+
reason: 'admission_query_failed',
|
|
417
|
+
attempts: ADMISSION_QUERY_MAX_ATTEMPTS,
|
|
418
|
+
cause: runtimeHostErrorData(lastError, 'turn.query'),
|
|
419
|
+
}, 'Runtime Host Turn admission could not be established; Stop could not be confirmed');
|
|
420
|
+
this.#wake(active);
|
|
421
|
+
}
|
|
422
|
+
async #ensureAttachment(sessionId, connection, notify) {
|
|
423
|
+
const existing = this.#attachments.get(sessionId);
|
|
424
|
+
if (existing)
|
|
425
|
+
return existing;
|
|
426
|
+
const openingController = new AbortController();
|
|
427
|
+
this.#attachmentOpenControllers.set(sessionId, openingController);
|
|
428
|
+
const configuration = {
|
|
429
|
+
notify,
|
|
430
|
+
// Setters can outlive an absent or failed attachment. Their responses
|
|
431
|
+
// must precede refreshes delivered by the new attachment's queue.
|
|
432
|
+
tail: Promise.allSettled([...(this.#pendingConfigSets.get(sessionId) ?? [])]),
|
|
433
|
+
};
|
|
434
|
+
this.#attachmentConfigurations.set(sessionId, configuration);
|
|
435
|
+
let task;
|
|
436
|
+
let attachment;
|
|
437
|
+
let earlyFailure;
|
|
438
|
+
const failAttachment = (error) => {
|
|
439
|
+
if (!attachment) {
|
|
440
|
+
earlyFailure = error;
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
this.#retireFailedAttachment(sessionId, task, attachment, error);
|
|
444
|
+
};
|
|
445
|
+
task = RuntimeHostSessionChannel.open({
|
|
446
|
+
connection,
|
|
447
|
+
signal: openingController.signal,
|
|
448
|
+
openInitialSessionSubscription: connection.openSessionSubscriptionOnce.bind(connection),
|
|
449
|
+
sessionId,
|
|
450
|
+
now: Date.now,
|
|
451
|
+
onTurnStarted: () => undefined,
|
|
452
|
+
onRuntimeResourceChanged: () => undefined,
|
|
453
|
+
onSnapshotChanged: (snapshot) => {
|
|
454
|
+
this.#wakeSession(sessionId);
|
|
455
|
+
if (configuration.metadataRevision === undefined) {
|
|
456
|
+
configuration.metadataRevision = snapshot.session.metadataRevision;
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (configuration.metadataRevision === snapshot.session.metadataRevision)
|
|
460
|
+
return;
|
|
461
|
+
configuration.metadataRevision = snapshot.session.metadataRevision;
|
|
462
|
+
void this.#queueConfiguration(configuration, async () => {
|
|
463
|
+
if (!this.#configurationIsLive(sessionId, configuration))
|
|
464
|
+
return;
|
|
465
|
+
const session = await getRuntimeHostSession(connection, sessionId);
|
|
466
|
+
if (!session)
|
|
467
|
+
throw unknownSessionError();
|
|
468
|
+
const configOptions = await this.#projectConfigOptions(connection, session);
|
|
469
|
+
await this.#notifyConfiguration(sessionId, configuration, configOptions);
|
|
470
|
+
}).catch((error) => {
|
|
471
|
+
// Closing or replacing the attachment intentionally invalidates any
|
|
472
|
+
// in-flight presentation refresh; its interrupted read is no longer actionable.
|
|
473
|
+
if (this.#configurationIsLive(sessionId, configuration)) {
|
|
474
|
+
console.error('[acp] Session configuration refresh failed:', error);
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
},
|
|
478
|
+
onTranscriptReplaced: (turnId, messages) => {
|
|
479
|
+
for (const active of this.#activePrompts.get(sessionId) ?? []) {
|
|
480
|
+
if (active.turnId === turnId && !active.cancelled) {
|
|
481
|
+
void active.mapper.replaceTranscript(turnId, messages).catch((error) => {
|
|
482
|
+
active.attachment?.failTurn(turnId, error);
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
onInteractionPending: (pending) => {
|
|
488
|
+
if (![...(this.#activePrompts.get(sessionId) ?? [])].some((active) => active.turnId === pending.turnId && active.dispatchStarted)) {
|
|
489
|
+
// An idle attachment may observe another client's Turn. Retain its
|
|
490
|
+
// identity so a later ACP cancel/close can still stop that root.
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
// Full interaction mapping belongs to the next ACP capability increment.
|
|
494
|
+
// Retire observation so the prompt's existing failure path stops its exact Turn.
|
|
495
|
+
failAttachment(RequestError.internalError({ source: 'adapter', code: 'unsupported_interaction', kind: pending.request.kind }, 'This ACP adapter does not support interactions yet; the prompt failed'));
|
|
496
|
+
},
|
|
497
|
+
onInteractionResolved: () => undefined,
|
|
498
|
+
onTranscriptSettlement: () => undefined,
|
|
499
|
+
onGoalChanged: () => undefined,
|
|
500
|
+
onFailed: failAttachment,
|
|
501
|
+
onRecovered: () => {
|
|
502
|
+
for (const active of this.#activePrompts.get(sessionId) ?? []) {
|
|
503
|
+
if (active.attachment !== attachment ||
|
|
504
|
+
!active.startRequestSettled ||
|
|
505
|
+
active.admissionSettled) {
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
// Recovery may hydrate a snapshot taken before start admission.
|
|
509
|
+
// An absent root needs a fresh query; a matching root can be stopped
|
|
510
|
+
// directly by the existing cancellation task.
|
|
511
|
+
if (attachment?.snapshot.rootTurn?.turnId !== active.turnId) {
|
|
512
|
+
this.#queryPromptAdmission(active, connection);
|
|
513
|
+
}
|
|
514
|
+
this.#wake(active);
|
|
515
|
+
}
|
|
516
|
+
},
|
|
517
|
+
})
|
|
518
|
+
.then(({ channel }) => {
|
|
519
|
+
channel.activate();
|
|
520
|
+
attachment = channel;
|
|
521
|
+
if (earlyFailure) {
|
|
522
|
+
this.#retireFailedAttachment(sessionId, task, channel, earlyFailure);
|
|
523
|
+
throw earlyFailure;
|
|
524
|
+
}
|
|
525
|
+
if (this.#closing || !this.#ownedSessionIds.has(sessionId)) {
|
|
526
|
+
return channel.close().then(() => {
|
|
527
|
+
throw this.#closing ? registryClosedError('subscription.open') : unknownSessionError();
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
return channel;
|
|
531
|
+
})
|
|
532
|
+
.catch((error) => {
|
|
533
|
+
if (this.#attachments.get(sessionId) === task) {
|
|
534
|
+
this.#attachments.delete(sessionId);
|
|
535
|
+
this.#attachmentConfigurations.delete(sessionId);
|
|
536
|
+
}
|
|
537
|
+
if (error instanceof RequestError)
|
|
538
|
+
throw error;
|
|
539
|
+
throw requestErrorFromRuntimeHost(error, 'subscription.open');
|
|
540
|
+
})
|
|
541
|
+
.finally(() => {
|
|
542
|
+
if (this.#attachmentOpenControllers.get(sessionId) === openingController) {
|
|
543
|
+
this.#attachmentOpenControllers.delete(sessionId);
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
this.#attachments.set(sessionId, task);
|
|
547
|
+
return task;
|
|
548
|
+
}
|
|
549
|
+
#retireFailedAttachment(sessionId, task, attachment, error) {
|
|
550
|
+
if (this.#attachments.get(sessionId) === task) {
|
|
551
|
+
this.#attachments.delete(sessionId);
|
|
552
|
+
this.#attachmentConfigurations.delete(sessionId);
|
|
553
|
+
}
|
|
554
|
+
for (const active of this.#activePrompts.get(sessionId) ?? []) {
|
|
555
|
+
if (active.attachment !== attachment)
|
|
556
|
+
continue;
|
|
557
|
+
// Losing observation cannot settle a dispatched start. Its pending
|
|
558
|
+
// response or bounded admission query still owns the exact Stop identity.
|
|
559
|
+
attachment.failTurn(active.turnId, error);
|
|
560
|
+
this.#wake(active);
|
|
561
|
+
}
|
|
562
|
+
void attachment.close().catch(() => undefined);
|
|
563
|
+
}
|
|
564
|
+
async #closeSession(sessionId, delivery) {
|
|
565
|
+
const cancellation = await this.#cancelSession(sessionId);
|
|
566
|
+
const attachmentTask = this.#attachments.get(sessionId);
|
|
567
|
+
this.#attachments.delete(sessionId);
|
|
568
|
+
let closeError;
|
|
569
|
+
if (attachmentTask) {
|
|
570
|
+
try {
|
|
571
|
+
// A rejected open has no retained resource; close still releases ownership.
|
|
572
|
+
const attachment = await attachmentTask.catch(() => undefined);
|
|
573
|
+
await attachment?.close();
|
|
574
|
+
}
|
|
575
|
+
catch (error) {
|
|
576
|
+
closeError = error;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
await delivery;
|
|
580
|
+
const failedCancellation = cancellation.find((result) => result.status === 'rejected');
|
|
581
|
+
if (failedCancellation)
|
|
582
|
+
throw failedCancellation.reason;
|
|
583
|
+
if (closeError)
|
|
584
|
+
throw closeError;
|
|
585
|
+
return {};
|
|
586
|
+
}
|
|
587
|
+
#addActivePrompt(active) {
|
|
588
|
+
const prompts = this.#activePrompts.get(active.sessionId);
|
|
589
|
+
if (prompts)
|
|
590
|
+
prompts.add(active);
|
|
591
|
+
else
|
|
592
|
+
this.#activePrompts.set(active.sessionId, new Set([active]));
|
|
593
|
+
}
|
|
594
|
+
#removeActivePrompt(active) {
|
|
595
|
+
const prompts = this.#activePrompts.get(active.sessionId);
|
|
596
|
+
prompts?.delete(active);
|
|
597
|
+
if (prompts?.size === 0)
|
|
598
|
+
this.#activePrompts.delete(active.sessionId);
|
|
599
|
+
}
|
|
600
|
+
#wakeSession(sessionId) {
|
|
601
|
+
for (const active of this.#activePrompts.get(sessionId) ?? [])
|
|
602
|
+
this.#wake(active);
|
|
603
|
+
}
|
|
604
|
+
#wake(active) {
|
|
605
|
+
for (const resolve of active.waiters)
|
|
606
|
+
resolve();
|
|
607
|
+
active.waiters.clear();
|
|
608
|
+
}
|
|
609
|
+
#waitForPromptChange(active, timeoutMs) {
|
|
610
|
+
return new Promise((resolve) => {
|
|
611
|
+
let timer;
|
|
612
|
+
const wake = () => {
|
|
613
|
+
if (timer !== undefined)
|
|
614
|
+
clearTimeout(timer);
|
|
615
|
+
active.waiters.delete(wake);
|
|
616
|
+
resolve();
|
|
617
|
+
};
|
|
618
|
+
active.waiters.add(wake);
|
|
619
|
+
if (timeoutMs !== undefined)
|
|
620
|
+
timer = setTimeout(wake, timeoutMs);
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
#assertOwned(sessionId) {
|
|
624
|
+
if (!this.#ownedSessionIds.has(sessionId))
|
|
625
|
+
throw unknownSessionError();
|
|
626
|
+
}
|
|
72
627
|
async #create(params) {
|
|
73
628
|
const connection = await this.#getConnection('session.create');
|
|
74
629
|
const sessionId = this.#newSessionId();
|
|
@@ -83,18 +638,23 @@ export class AcpSessionRegistry {
|
|
|
83
638
|
catch (error) {
|
|
84
639
|
throw requestErrorFromRuntimeHost(error, 'session.create', { sessionId });
|
|
85
640
|
}
|
|
86
|
-
|
|
641
|
+
// Session creation has committed. Optional presentation failures must not
|
|
642
|
+
// turn that success into an unreachable durable Session.
|
|
643
|
+
let configOptions;
|
|
87
644
|
try {
|
|
88
|
-
created = requireRuntimeHostSessionProjection(result, 'session.create');
|
|
645
|
+
const created = requireRuntimeHostSessionProjection(result, 'session.create');
|
|
646
|
+
configOptions = await this.#projectConfigOptions(connection, created);
|
|
89
647
|
}
|
|
90
|
-
catch
|
|
91
|
-
|
|
648
|
+
catch {
|
|
649
|
+
// The client can still prompt, configure, list, or close the returned ID.
|
|
92
650
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
651
|
+
// Do not admit mutations while projection is pending, or resurrect ownership
|
|
652
|
+
// if connection shutdown raced the successful Host creation.
|
|
653
|
+
if (!this.#closing)
|
|
654
|
+
this.#ownedSessionIds.add(sessionId);
|
|
655
|
+
return { sessionId, ...(configOptions ? { configOptions } : {}) };
|
|
96
656
|
}
|
|
97
|
-
async #setConfigOption(params) {
|
|
657
|
+
async #setConfigOption(params, configuration) {
|
|
98
658
|
const connection = await this.#getConnection('session.configuration.update');
|
|
99
659
|
let committed;
|
|
100
660
|
try {
|
|
@@ -104,13 +664,46 @@ export class AcpSessionRegistry {
|
|
|
104
664
|
patch: createAcpSessionConfigPatch(params),
|
|
105
665
|
}), {
|
|
106
666
|
operation: 'session.configuration.update',
|
|
107
|
-
assertRequestAllowed: () =>
|
|
667
|
+
assertRequestAllowed: () => {
|
|
668
|
+
this.#assertOpen('session.configuration.update');
|
|
669
|
+
this.#assertOwned(params.sessionId);
|
|
670
|
+
},
|
|
108
671
|
});
|
|
109
672
|
}
|
|
110
673
|
catch (error) {
|
|
111
674
|
throw requestErrorFromSessionUpdate(error, 'session.configuration.update');
|
|
112
675
|
}
|
|
113
|
-
|
|
676
|
+
const configOptions = await this.#projectConfigOptions(connection, committed);
|
|
677
|
+
if (configuration)
|
|
678
|
+
await this.#notifyConfiguration(params.sessionId, configuration, configOptions);
|
|
679
|
+
return { configOptions };
|
|
680
|
+
}
|
|
681
|
+
#configurationIsLive(sessionId, configuration) {
|
|
682
|
+
return (!this.#closing &&
|
|
683
|
+
this.#ownedSessionIds.has(sessionId) &&
|
|
684
|
+
this.#attachmentConfigurations.get(sessionId) === configuration);
|
|
685
|
+
}
|
|
686
|
+
#queueConfiguration(configuration, operation) {
|
|
687
|
+
// Serialize asynchronous catalog projection and delivery, not Host frames:
|
|
688
|
+
// session-channel/projector remain the only subscription ordering authority.
|
|
689
|
+
// A local set emits its committed options before its response; subscription
|
|
690
|
+
// refreshes observed during that set follow its notification in this queue.
|
|
691
|
+
const result = configuration.tail.then(operation, operation);
|
|
692
|
+
configuration.tail = result.catch(() => undefined);
|
|
693
|
+
return result;
|
|
694
|
+
}
|
|
695
|
+
async #notifyConfiguration(sessionId, configuration, configOptions) {
|
|
696
|
+
if (!this.#configurationIsLive(sessionId, configuration))
|
|
697
|
+
return;
|
|
698
|
+
const options = JSON.stringify(configOptions);
|
|
699
|
+
if (configuration.options === options)
|
|
700
|
+
return;
|
|
701
|
+
configuration.delivery = configuration.notify({
|
|
702
|
+
sessionId,
|
|
703
|
+
update: { sessionUpdate: 'config_option_update', configOptions },
|
|
704
|
+
});
|
|
705
|
+
await configuration.delivery;
|
|
706
|
+
configuration.options = options;
|
|
114
707
|
}
|
|
115
708
|
async #projectConfigOptions(connection, session) {
|
|
116
709
|
let catalog;
|
|
@@ -163,9 +756,42 @@ export class AcpSessionRegistry {
|
|
|
163
756
|
};
|
|
164
757
|
}
|
|
165
758
|
async #dispose() {
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
759
|
+
const sessionIds = new Set([...this.#activePrompts.keys(), ...this.#attachments.keys()]);
|
|
760
|
+
const activePrompts = [...sessionIds].flatMap((sessionId) => [
|
|
761
|
+
...(this.#activePrompts.get(sessionId) ?? []),
|
|
762
|
+
]);
|
|
763
|
+
const cancellations = [...sessionIds].map((sessionId) => this.#cancelSession(sessionId));
|
|
764
|
+
const attachments = [...this.#attachments.values()];
|
|
765
|
+
this.#attachments.clear();
|
|
766
|
+
const configurations = [...this.#attachmentConfigurations.values()];
|
|
767
|
+
this.#attachmentConfigurations.clear();
|
|
768
|
+
await Promise.allSettled(attachments.map(async (attachment) => (await attachment).close()));
|
|
769
|
+
await Promise.allSettled(activePrompts.map(async (active) => {
|
|
770
|
+
while (active.dispatchStarted && !active.startRequestSettled && !active.finished) {
|
|
771
|
+
await this.#waitForPromptChange(active);
|
|
772
|
+
}
|
|
773
|
+
}));
|
|
774
|
+
const unknownAdmissions = activePrompts.filter((active) => {
|
|
775
|
+
const observed = active.attachment?.snapshot.rootTurn;
|
|
776
|
+
const hasStopIdentity = observed?.turnId === active.turnId || active.startedTurn !== undefined;
|
|
777
|
+
return active.dispatchStarted && !active.admissionSettled && !hasStopIdentity;
|
|
778
|
+
});
|
|
779
|
+
if (unknownAdmissions.length > 0) {
|
|
780
|
+
// At shutdown the attachment is already closed and each start request has
|
|
781
|
+
// settled, leaving recovery/query as the only remaining fact source.
|
|
782
|
+
// Close the owned connection so those reads cannot deadlock EOF cleanup.
|
|
783
|
+
await Promise.allSettled([this.#closeOwnedConnection()]);
|
|
784
|
+
for (const active of unknownAdmissions) {
|
|
785
|
+
active.admissionSettled = true;
|
|
786
|
+
this.#wake(active);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
await Promise.allSettled(cancellations);
|
|
790
|
+
await Promise.allSettled([this.#closeOwnedConnection()]);
|
|
791
|
+
await Promise.allSettled([
|
|
792
|
+
...this.#inFlightOperations,
|
|
793
|
+
...configurations.map(({ tail }) => tail),
|
|
794
|
+
]);
|
|
169
795
|
this.#ownedSessionIds.clear();
|
|
170
796
|
}
|
|
171
797
|
#closeOwnedConnection() {
|
|
@@ -237,6 +863,9 @@ export class AcpSessionRegistry {
|
|
|
237
863
|
throw registryClosedError(operation);
|
|
238
864
|
}
|
|
239
865
|
}
|
|
866
|
+
function unknownSessionError() {
|
|
867
|
+
return RequestError.invalidParams({ reason: 'unknown_session' }, 'Session is not owned by this ACP connection');
|
|
868
|
+
}
|
|
240
869
|
function registryClosedError(operation) {
|
|
241
870
|
return RequestError.internalError({ source: 'runtime_host', operation, code: 'registry_closed' }, 'ACP session registry is closed');
|
|
242
871
|
}
|
|
@@ -302,6 +931,14 @@ function runtimeHostErrorData(error, operation) {
|
|
|
302
931
|
dispatch: error.dispatch,
|
|
303
932
|
};
|
|
304
933
|
}
|
|
934
|
+
if (error instanceof RuntimeHostSubscriptionError) {
|
|
935
|
+
return {
|
|
936
|
+
source: 'runtime_host',
|
|
937
|
+
operation,
|
|
938
|
+
code: 'subscription_failure',
|
|
939
|
+
reason: error.reason,
|
|
940
|
+
};
|
|
941
|
+
}
|
|
305
942
|
if (error instanceof RuntimeHostCatalogReadError) {
|
|
306
943
|
return {
|
|
307
944
|
source: 'runtime_host',
|