maka-agent 0.2.0-dev.53.20260927 → 0.2.0-dev.62.20260928
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/LICENSE +6 -5
- package/dist/workspace-root.js +2 -2
- package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
- package/node_modules/@maka/acp-executor-plugin/dist/index.js +333 -28
- package/node_modules/@maka/acp-executor-plugin/dist/plugin.mjs +283 -27
- package/node_modules/@maka/core/dist/executor-catalog.js +10 -1
- package/node_modules/@maka/core/dist/message-queue-order.js +47 -0
- package/node_modules/@maka/core/dist/model-metadata.js +2 -1
- package/node_modules/@maka/core/dist/redaction.js +42 -3
- package/node_modules/@maka/core/dist/relative-time-policy.js +71 -0
- package/node_modules/@maka/core/dist/relative-time.js +3 -54
- package/node_modules/@maka/core/package.json +1 -0
- package/node_modules/@maka/eval/dist/egress-audit.js +127 -0
- package/node_modules/@maka/eval/dist/harness-executor.js +16 -96
- package/node_modules/@maka/eval/harbor/egress-proxy/entrypoint.sh +4 -4
- package/node_modules/@maka/eval/harbor/egress_filter.py +153 -104
- package/node_modules/@maka/eval/harbor/eval_framework.py +37 -14
- package/node_modules/@maka/eval/harbor/relay_agent.py +4 -6
- package/node_modules/@maka/eval/harbor/run_trial.py +11 -4
- package/node_modules/@maka/runtime/dist/agent-graph-supervisor-wake.js +33 -6
- package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +1 -1
- package/node_modules/@maka/runtime/dist/plugin-executor-backend.js +26 -5
- package/node_modules/@maka/runtime/dist/plugin-executor-service.js +36 -0
- package/node_modules/@maka/runtime/dist/provider-error-classification.js +8 -73
- package/node_modules/@maka/runtime/dist/provider-error-signals.js +31 -0
- package/node_modules/@maka/runtime/dist/provider-retry-policy.js +79 -0
- package/node_modules/@maka/runtime/dist/session-manager.js +6 -4
- package/node_modules/@maka/runtime/dist/session-projection-helpers.js +2 -0
- package/node_modules/@maka/runtime/dist/shell-run-manager.js +8 -8
- package/node_modules/@maka/runtime/dist/terminal-run-commit.js +7 -1
- package/node_modules/@maka/runtime/dist/tool-runtime.js +5 -4
- package/node_modules/@maka/runtime-host/dist/adapter/provider-retry-projector.js +63 -0
- package/node_modules/@maka/runtime-host/dist/adapter/session-projector.js +7 -51
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +9 -3
- package/node_modules/@maka/runtime-host/dist/protocol/message-queue-limits.js +21 -0
- package/node_modules/@maka/runtime-host/dist/protocol/message.js +6 -104
- package/node_modules/@maka/runtime-host/dist/protocol/queue-mutation.js +136 -0
- package/node_modules/@maka/runtime-host/dist/protocol/turn-provider-retry.js +69 -0
- package/node_modules/@maka/runtime-host/dist/protocol/turn.js +2 -43
- package/node_modules/@maka/runtime-host/dist/server/goal-coordinator.js +42 -5
- package/node_modules/@maka/runtime-host/dist/server/message-coordinator.js +105 -233
- package/node_modules/@maka/runtime-host/dist/server/message-queue-state.js +69 -0
- package/node_modules/@maka/runtime-host/dist/server/provider-retry-projection.js +71 -0
- package/node_modules/@maka/runtime-host/dist/server/queued-mutation-executor.js +94 -0
- package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +1 -1
- package/node_modules/@maka/runtime-host/dist/server/session-continuity-coordinator.js +9 -58
- package/node_modules/@maka/storage/dist/agent-run-store-contract.js +0 -3
- package/node_modules/@maka/storage/dist/agent-run-store.js +15 -6
- package/node_modules/@maka/storage/dist/execution-persistence-provider.js +1 -0
- package/node_modules/@maka/storage/dist/execution-stores.js +1 -0
- package/node_modules/@maka/storage/dist/project-catalog.js +41 -42
- package/node_modules/@maka/storage/dist/sqlite-runtime-store.js +102 -31
- package/node_modules/@maka/storage/dist/sqlite-session-metadata-schema.js +52 -1
- package/node_modules/@maka/storage/dist/sqlite-session-metadata-store.js +34 -2
- package/package.json +1 -1
package/LICENSE
CHANGED
|
@@ -469,17 +469,18 @@ THE SOFTWARE.
|
|
|
469
469
|
|
|
470
470
|
electron-builder dependency patches
|
|
471
471
|
|
|
472
|
-
Source: https://www.npmjs.com/package/app-builder-lib/v/26.
|
|
472
|
+
Source: https://www.npmjs.com/package/app-builder-lib/v/26.16.1
|
|
473
473
|
https://www.npmjs.com/package/electron-updater/v/6.8.9
|
|
474
474
|
Repository: https://github.com/electron-userland/electron-builder
|
|
475
|
-
Versions: app-builder-lib 26.
|
|
476
|
-
Dependency patch: patches/app-builder-lib+26.
|
|
475
|
+
Versions: app-builder-lib 26.16.1; electron-updater 6.8.9
|
|
476
|
+
Dependency patch: patches/app-builder-lib+26.16.1.patch
|
|
477
477
|
patches/electron-updater+6.8.9.patch
|
|
478
478
|
License: MIT
|
|
479
479
|
|
|
480
480
|
Maka redistributes source patches that omit modification and access times
|
|
481
|
-
from ZIP archives
|
|
482
|
-
missing. The following MIT
|
|
481
|
+
from 7-Zip ZIP archives, keep macOS ZIP archives on the system zip, and skip
|
|
482
|
+
withdrawn prereleases whose update metadata is missing. The following MIT
|
|
483
|
+
License applies to that material:
|
|
483
484
|
|
|
484
485
|
The MIT License (MIT)
|
|
485
486
|
|
package/dist/workspace-root.js
CHANGED
|
@@ -16,6 +16,6 @@
|
|
|
16
16
|
* specific language governing permissions and limitations
|
|
17
17
|
* under the License.
|
|
18
18
|
*/
|
|
19
|
-
//
|
|
20
|
-
//
|
|
19
|
+
// Keep the CLI compatibility path while storage owns workspace-root resolution.
|
|
20
|
+
// New callers should import the storage subpath directly.
|
|
21
21
|
export { deriveMakaDataRoots, resolveMakaClientDataRoot, resolveMakaDataRoots, resolveMakaWorkspaceRoot, } from '@maka/storage/workspace-root';
|
|
Binary file
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* under the License.
|
|
18
18
|
*/
|
|
19
19
|
import { createHash } from 'node:crypto';
|
|
20
|
-
import { constants } from 'node:fs';
|
|
20
|
+
import { constants, createReadStream } from 'node:fs';
|
|
21
21
|
import { access, mkdtemp, rm, realpath, stat } from 'node:fs/promises';
|
|
22
22
|
import { tmpdir } from 'node:os';
|
|
23
23
|
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
@@ -89,22 +89,56 @@ export class AcpExecutor {
|
|
|
89
89
|
const session = this.#sessions.get(input.conversationKey);
|
|
90
90
|
if (this.#disposed)
|
|
91
91
|
return this.#catalogEntry('unavailable');
|
|
92
|
-
if (session
|
|
93
|
-
return this.#catalogEntry('history_only');
|
|
94
|
-
|
|
92
|
+
if (session && session.cwd !== resolve(input.cwd))
|
|
93
|
+
return this.#catalogEntry('history_only', [], input.configuration?.model);
|
|
94
|
+
if (session?.restoring)
|
|
95
|
+
return this.#catalogEntry('restoring', session.configOptions, input.configuration?.model);
|
|
96
|
+
if (session?.restoreFailed)
|
|
97
|
+
return this.#catalogEntry('restore_failed', session.configOptions, input.configuration?.model);
|
|
98
|
+
if (session?.historyGap)
|
|
99
|
+
return this.#catalogEntry('history_gap', session.configOptions, input.configuration?.model);
|
|
100
|
+
if (session && !session.lost)
|
|
101
|
+
return this.#catalogEntry('ready', session.configOptions, input.configuration?.model);
|
|
102
|
+
if (session?.lost && !session.record)
|
|
103
|
+
return this.#catalogEntry('history_only', session.configOptions, input.configuration?.model);
|
|
104
|
+
const stored = this.#state?.read
|
|
105
|
+
? decodeContinuity(await this.#state.read(input.conversationKey))
|
|
106
|
+
: (await this.#state?.has(input.conversationKey, input.cwd))
|
|
107
|
+
? 'legacy'
|
|
108
|
+
: undefined;
|
|
109
|
+
if (!stored)
|
|
110
|
+
return this.#catalogEntry('ready', [], input.configuration?.model);
|
|
111
|
+
if (stored === 'invalid' || stored === 'legacy' || stored.cwd !== resolve(input.cwd))
|
|
112
|
+
return this.#catalogEntry('history_only', [], input.configuration?.model);
|
|
113
|
+
if (stored.phase === 'reserved')
|
|
114
|
+
return this.#catalogEntry('history_only', [], input.configuration?.model);
|
|
115
|
+
return this.#catalogEntry(stored.phase === 'prompt_pending' || stored.phase === 'history_gap'
|
|
116
|
+
? 'history_gap'
|
|
117
|
+
: 'restorable', [], input.configuration?.model ?? stored.confirmedModel);
|
|
95
118
|
}
|
|
96
119
|
async configureConversation(input, signal) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
session.configuring ||
|
|
102
|
-
!input.configuration?.model)
|
|
120
|
+
if (!input.configuration?.model)
|
|
121
|
+
throw new Error('ACP model change is unavailable');
|
|
122
|
+
const session = await this.#session(input);
|
|
123
|
+
if (session.active || session.configuring)
|
|
103
124
|
throw new Error('ACP model change is unavailable');
|
|
104
125
|
session.configuring = true;
|
|
126
|
+
const wasConnected = !!session.connection;
|
|
105
127
|
try {
|
|
128
|
+
await this.#ensureInitialized(session, signal);
|
|
106
129
|
await this.#applyInitialConfig(session, { model: input.configuration.model }, signal, true);
|
|
107
130
|
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (!wasConnected || session.lost) {
|
|
133
|
+
await this.#lose(session);
|
|
134
|
+
if (session.record?.sessionId && !session.historyGap)
|
|
135
|
+
session.restoreFailed = true;
|
|
136
|
+
const retryable = !session.acpSessionId && !session.continuityReserved && !session.record;
|
|
137
|
+
if (retryable && this.#sessions.get(session.conversationKey) === session)
|
|
138
|
+
this.#sessions.delete(session.conversationKey);
|
|
139
|
+
}
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
108
142
|
finally {
|
|
109
143
|
session.configuring = false;
|
|
110
144
|
}
|
|
@@ -115,12 +149,17 @@ export class AcpExecutor {
|
|
|
115
149
|
? model.options
|
|
116
150
|
.flatMap((entry) => ('options' in entry ? entry.options : [entry]))
|
|
117
151
|
.map((entry) => ({ id: entry.value, name: entry.name }))
|
|
118
|
-
: [];
|
|
152
|
+
: (this.#catalog?.models ?? []);
|
|
119
153
|
return {
|
|
120
154
|
id: this.id,
|
|
121
155
|
displayName: this.displayName,
|
|
122
156
|
readiness,
|
|
123
|
-
...(
|
|
157
|
+
...(model?.type === 'select'
|
|
158
|
+
? (this.#adapter.describeModels?.(models) ?? { models })
|
|
159
|
+
: {
|
|
160
|
+
models,
|
|
161
|
+
...(this.#catalog?.modelGroups ? { modelGroups: this.#catalog.modelGroups } : {}),
|
|
162
|
+
}),
|
|
124
163
|
...(model?.type === 'select'
|
|
125
164
|
? { currentModel: model.currentValue }
|
|
126
165
|
: selected
|
|
@@ -146,12 +185,12 @@ export class AcpExecutor {
|
|
|
146
185
|
request = { ...request, configuration: { model } };
|
|
147
186
|
let session;
|
|
148
187
|
try {
|
|
149
|
-
session = this.#session(request);
|
|
188
|
+
session = await this.#session(request);
|
|
150
189
|
}
|
|
151
190
|
catch (error) {
|
|
152
191
|
return failure(safeErrorMessage(error), errorCode(error));
|
|
153
192
|
}
|
|
154
|
-
if (session.active || session.configuring)
|
|
193
|
+
if (session.active || session.configuring || session.awaitingAck)
|
|
155
194
|
return failure(`${this.displayName} ACP Session is busy`, 'acp_busy', true);
|
|
156
195
|
const active = { context, tools: new Map(), text: '' };
|
|
157
196
|
session.active = active;
|
|
@@ -160,6 +199,7 @@ export class AcpExecutor {
|
|
|
160
199
|
context.signal.throwIfAborted();
|
|
161
200
|
if (request.configuration?.model)
|
|
162
201
|
await this.#applyInitialConfig(session, { model: request.configuration.model }, context.signal);
|
|
202
|
+
await this.#beginPrompt(session, request.turnId);
|
|
163
203
|
const prompt = session.connection.agent.request(methods.agent.session.prompt, {
|
|
164
204
|
sessionId: session.acpSessionId,
|
|
165
205
|
prompt: [{ type: 'text', text: promptText(request) }],
|
|
@@ -167,6 +207,11 @@ export class AcpExecutor {
|
|
|
167
207
|
const response = await this.#awaitPrompt(session, prompt, context.signal);
|
|
168
208
|
if (!response)
|
|
169
209
|
return { status: 'cancelled', reason: 'timeout' };
|
|
210
|
+
// A settled prompt can be checkpointed even when it was cancelled or failed.
|
|
211
|
+
// Timeouts and transport failures never reach this point, and the Runtime
|
|
212
|
+
// must still durably consume the terminal event before acknowledging it.
|
|
213
|
+
if (session.record)
|
|
214
|
+
session.awaitingAck = request.turnId;
|
|
170
215
|
if (context.signal.aborted || response.stopReason === 'cancelled') {
|
|
171
216
|
return { status: 'cancelled', providerStopReason: response.stopReason };
|
|
172
217
|
}
|
|
@@ -179,12 +224,17 @@ export class AcpExecutor {
|
|
|
179
224
|
return { status: 'completed', text: active.text };
|
|
180
225
|
}
|
|
181
226
|
catch (error) {
|
|
227
|
+
if (errorCode(error) === 'acp_history_gap')
|
|
228
|
+
session.historyGap = true;
|
|
182
229
|
await this.#lose(session);
|
|
230
|
+
if (session.record?.sessionId && !session.historyGap)
|
|
231
|
+
session.restoreFailed = true;
|
|
183
232
|
// Retry only when no session/new was attempted. Once continuity is
|
|
184
233
|
// reserved, a lost response may hide an established Agent Session.
|
|
185
234
|
const retryable = !session.acpSessionId &&
|
|
186
235
|
!session.continuityReserved &&
|
|
187
|
-
errorCode(error) !== 'acp_history_only'
|
|
236
|
+
errorCode(error) !== 'acp_history_only' &&
|
|
237
|
+
errorCode(error) !== 'acp_history_gap';
|
|
188
238
|
if (retryable && this.#sessions.get(session.conversationKey) === session)
|
|
189
239
|
this.#sessions.delete(session.conversationKey);
|
|
190
240
|
if (context.signal.aborted) {
|
|
@@ -216,15 +266,22 @@ export class AcpExecutor {
|
|
|
216
266
|
if (failures.length)
|
|
217
267
|
throw new AggregateError(failures, 'ACP process cleanup failed');
|
|
218
268
|
}
|
|
219
|
-
#session(request) {
|
|
269
|
+
async #session(request) {
|
|
220
270
|
const cwd = resolve(request.cwd);
|
|
221
271
|
const existing = this.#sessions.get(request.conversationKey);
|
|
222
272
|
if (existing) {
|
|
223
273
|
if (existing.cwd !== cwd)
|
|
224
274
|
throw new AcpRuntimeError('ACP conversation cannot change workspace', 'acp_cwd_changed');
|
|
225
|
-
if (existing.lost)
|
|
275
|
+
if (!existing.lost)
|
|
276
|
+
return existing;
|
|
277
|
+
if (existing.active || existing.configuring)
|
|
278
|
+
throw new AcpRuntimeError('ACP Session is busy', 'acp_busy');
|
|
279
|
+
if (!existing.record)
|
|
226
280
|
throw new AcpRuntimeError('ACP conversation is history-only because its external process was lost', 'acp_history_only');
|
|
227
|
-
|
|
281
|
+
await existing.loss;
|
|
282
|
+
if (this.#sessions.get(request.conversationKey) !== existing)
|
|
283
|
+
return this.#session(request);
|
|
284
|
+
this.#sessions.delete(request.conversationKey);
|
|
228
285
|
}
|
|
229
286
|
const created = {
|
|
230
287
|
conversationKey: request.conversationKey,
|
|
@@ -246,17 +303,41 @@ export class AcpExecutor {
|
|
|
246
303
|
}
|
|
247
304
|
catch (error) {
|
|
248
305
|
session.initialization = undefined;
|
|
306
|
+
session.restoring = false;
|
|
307
|
+
session.replay = undefined;
|
|
249
308
|
throw error;
|
|
250
309
|
}
|
|
251
310
|
}
|
|
252
311
|
async #initialize(session, signal, probe = false) {
|
|
253
|
-
if (!probe && (await this.#state?.has(session.conversationKey, session.cwd))) {
|
|
254
|
-
throw new AcpRuntimeError('ACP conversation is history-only after the Plugin or Host was restarted', 'acp_history_only');
|
|
255
|
-
}
|
|
256
312
|
const timeout = AbortSignal.timeout(INITIALIZE_TIMEOUT_MS);
|
|
257
313
|
const startupSignal = AbortSignal.any([signal, timeout]);
|
|
258
314
|
startupSignal.throwIfAborted();
|
|
259
315
|
const launch = this.#configured.launch;
|
|
316
|
+
const stored = probe
|
|
317
|
+
? undefined
|
|
318
|
+
: this.#state?.read
|
|
319
|
+
? decodeContinuity(await this.#state.read(session.conversationKey))
|
|
320
|
+
: (await this.#state?.has(session.conversationKey, session.cwd))
|
|
321
|
+
? 'legacy'
|
|
322
|
+
: undefined;
|
|
323
|
+
if (stored === 'legacy')
|
|
324
|
+
throw new AcpRuntimeError('ACP conversation is history-only after the Plugin or Host was restarted', 'acp_history_only');
|
|
325
|
+
if (stored === 'invalid' ||
|
|
326
|
+
(stored && stored.cwd !== session.cwd) ||
|
|
327
|
+
stored?.phase === 'reserved')
|
|
328
|
+
throw new AcpRuntimeError('ACP conversation has no recoverable external Session identity', 'acp_history_only');
|
|
329
|
+
if (stored?.phase === 'history_gap')
|
|
330
|
+
throw new AcpRuntimeError('External Session history cannot be aligned with saved conversation events', 'acp_history_gap');
|
|
331
|
+
if (stored) {
|
|
332
|
+
session.record = stored;
|
|
333
|
+
session.acpSessionId = stored.sessionId;
|
|
334
|
+
session.restoring = true;
|
|
335
|
+
}
|
|
336
|
+
const binding = !probe && this.#state?.write
|
|
337
|
+
? await launchBinding(this.id, launch, startupSignal)
|
|
338
|
+
: undefined;
|
|
339
|
+
if (stored && stored.binding !== binding)
|
|
340
|
+
throw new AcpRuntimeError('ACP Agent installation changed', 'acp_restore_identity_changed');
|
|
260
341
|
const executable = await checkedExecutable(launch.executable);
|
|
261
342
|
await Promise.all((launch.requiredExecutables ?? []).map(checkedExecutable));
|
|
262
343
|
const owner = this.#createConnection({
|
|
@@ -283,11 +364,62 @@ export class AcpExecutor {
|
|
|
283
364
|
]);
|
|
284
365
|
if (initialized.protocolVersion !== 1)
|
|
285
366
|
throw new Error('Unsupported ACP protocol version');
|
|
367
|
+
if (stored) {
|
|
368
|
+
session.replay = { updates: 0, userChunks: 0 };
|
|
369
|
+
const pending = stored.phase === 'prompt_pending';
|
|
370
|
+
const supported = pending
|
|
371
|
+
? initialized.agentCapabilities?.loadSession
|
|
372
|
+
: initialized.agentCapabilities?.sessionCapabilities?.resume;
|
|
373
|
+
if (!supported)
|
|
374
|
+
throw new AcpRuntimeError('ACP Agent cannot restore this Session', 'acp_restore_unsupported');
|
|
375
|
+
const restored = await Promise.race([
|
|
376
|
+
owner.connection.agent.request(pending ? methods.agent.session.load : methods.agent.session.resume, { sessionId: stored.sessionId, cwd: session.cwd, mcpServers: [] }, { cancellationSignal: startupSignal }),
|
|
377
|
+
owner.failed,
|
|
378
|
+
]);
|
|
379
|
+
session.configOptions = restored.configOptions ?? [];
|
|
380
|
+
const restoredModel = currentAcpModel(session.configOptions);
|
|
381
|
+
if (restoredModel)
|
|
382
|
+
await this.#persistConfirmedModel(session, restoredModel);
|
|
383
|
+
session.restoring = false;
|
|
384
|
+
if (pending) {
|
|
385
|
+
// The Agent may have progressed beyond Maka's last durable event. Replay
|
|
386
|
+
// identities are not sufficient to fabricate canonical history: failed
|
|
387
|
+
// turns can change tool IDs between live output and load. Keep every
|
|
388
|
+
// replay notification separate and expose an explicit history gap.
|
|
389
|
+
session.historyGap = true;
|
|
390
|
+
session.record = {
|
|
391
|
+
...session.record,
|
|
392
|
+
phase: 'history_gap',
|
|
393
|
+
pendingTurnId: undefined,
|
|
394
|
+
gapEvidence: {
|
|
395
|
+
replayedUpdates: session.replay?.updates ?? 0,
|
|
396
|
+
replayedUserChunks: session.replay?.userChunks ?? 0,
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
await this.#state?.write?.(session.conversationKey, session.record);
|
|
400
|
+
session.replay = undefined;
|
|
401
|
+
throw new AcpRuntimeError('External Session was restored, but conversation history may have a gap', 'acp_history_gap');
|
|
402
|
+
}
|
|
403
|
+
session.replay = undefined;
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
286
406
|
if (!probe) {
|
|
287
407
|
startupSignal.throwIfAborted();
|
|
288
408
|
// Reserve continuity before asking the Agent to create a Session. A failed
|
|
289
409
|
// or interrupted session/new may still have created one remotely.
|
|
290
|
-
|
|
410
|
+
if (this.#state?.write && binding) {
|
|
411
|
+
session.record = {
|
|
412
|
+
version: 2,
|
|
413
|
+
cwd: session.cwd,
|
|
414
|
+
binding,
|
|
415
|
+
phase: 'reserved',
|
|
416
|
+
committedPrompts: 0,
|
|
417
|
+
};
|
|
418
|
+
await this.#state.write(session.conversationKey, session.record);
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
await this.#state?.mark(session.conversationKey, session.cwd);
|
|
422
|
+
}
|
|
291
423
|
session.continuityReserved = true;
|
|
292
424
|
}
|
|
293
425
|
const created = await Promise.race([
|
|
@@ -296,12 +428,82 @@ export class AcpExecutor {
|
|
|
296
428
|
]);
|
|
297
429
|
session.acpSessionId = created.sessionId;
|
|
298
430
|
session.configOptions = created.configOptions ?? [];
|
|
431
|
+
if (session.record) {
|
|
432
|
+
const established = {
|
|
433
|
+
...session.record,
|
|
434
|
+
phase: 'established',
|
|
435
|
+
sessionId: created.sessionId,
|
|
436
|
+
};
|
|
437
|
+
await this.#state?.write?.(session.conversationKey, established);
|
|
438
|
+
session.record = established;
|
|
439
|
+
const createdModel = currentAcpModel(session.configOptions);
|
|
440
|
+
if (createdModel)
|
|
441
|
+
await this.#persistConfirmedModel(session, createdModel);
|
|
442
|
+
}
|
|
299
443
|
if (!probe) {
|
|
300
444
|
await this.#applyInitialConfig(session, session.configuration?.model
|
|
301
445
|
? { model: session.configuration.model }
|
|
302
446
|
: (launch.initialConfig ?? {}), startupSignal);
|
|
303
447
|
}
|
|
304
448
|
}
|
|
449
|
+
async #beginPrompt(session, turnId) {
|
|
450
|
+
if (!session.record)
|
|
451
|
+
return;
|
|
452
|
+
const pending = {
|
|
453
|
+
...session.record,
|
|
454
|
+
phase: 'prompt_pending',
|
|
455
|
+
pendingTurnId: turnId,
|
|
456
|
+
};
|
|
457
|
+
await this.#state?.write?.(session.conversationKey, pending);
|
|
458
|
+
session.record = pending;
|
|
459
|
+
}
|
|
460
|
+
/** Called only after the Runtime Kernel has durably accepted the terminal event. */
|
|
461
|
+
async acknowledgeExecution(conversationKey, turnId) {
|
|
462
|
+
const session = this.#sessions.get(conversationKey);
|
|
463
|
+
if (!session?.record ||
|
|
464
|
+
session.awaitingAck !== turnId ||
|
|
465
|
+
session.record.phase !== 'prompt_pending' ||
|
|
466
|
+
session.record.pendingTurnId !== turnId)
|
|
467
|
+
return;
|
|
468
|
+
const committed = {
|
|
469
|
+
...session.record,
|
|
470
|
+
phase: 'committed',
|
|
471
|
+
committedPrompts: session.record.committedPrompts + 1,
|
|
472
|
+
pendingTurnId: undefined,
|
|
473
|
+
};
|
|
474
|
+
try {
|
|
475
|
+
await this.#state?.write?.(conversationKey, committed);
|
|
476
|
+
}
|
|
477
|
+
catch (error) {
|
|
478
|
+
session.historyGap = true;
|
|
479
|
+
await this.#lose(session);
|
|
480
|
+
throw error;
|
|
481
|
+
}
|
|
482
|
+
session.record = committed;
|
|
483
|
+
session.awaitingAck = undefined;
|
|
484
|
+
}
|
|
485
|
+
/** The external turn settled, but Maka did not durably consume its terminal event. */
|
|
486
|
+
async abandonExecution(conversationKey, turnId) {
|
|
487
|
+
const session = this.#sessions.get(conversationKey);
|
|
488
|
+
if (!session?.record ||
|
|
489
|
+
session.awaitingAck !== turnId ||
|
|
490
|
+
session.record.phase !== 'prompt_pending' ||
|
|
491
|
+
session.record.pendingTurnId !== turnId)
|
|
492
|
+
return;
|
|
493
|
+
session.historyGap = true;
|
|
494
|
+
session.awaitingAck = undefined;
|
|
495
|
+
session.record = {
|
|
496
|
+
...session.record,
|
|
497
|
+
phase: 'history_gap',
|
|
498
|
+
pendingTurnId: undefined,
|
|
499
|
+
};
|
|
500
|
+
try {
|
|
501
|
+
await this.#state?.write?.(conversationKey, session.record);
|
|
502
|
+
}
|
|
503
|
+
finally {
|
|
504
|
+
await this.#lose(session);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
305
507
|
async #applyInitialConfig(session, values, signal, restoreOnFailure = false) {
|
|
306
508
|
for (const [key, value] of Object.entries(values)) {
|
|
307
509
|
const option = session.configOptions.find((candidate) => candidate.type === 'select' && (candidate.id === key || candidate.category === key));
|
|
@@ -310,8 +512,11 @@ export class AcpExecutor {
|
|
|
310
512
|
const options = option.options.flatMap((entry) => 'options' in entry ? entry.options : [entry]);
|
|
311
513
|
if (!options.some((entry) => entry.value === value))
|
|
312
514
|
throw new AcpRuntimeError(`ACP configuration value is unavailable: ${key}`, 'acp_config_invalid');
|
|
313
|
-
if (option.currentValue === value)
|
|
515
|
+
if (option.currentValue === value) {
|
|
516
|
+
if (key === 'model')
|
|
517
|
+
await this.#persistConfirmedModel(session, value);
|
|
314
518
|
continue;
|
|
519
|
+
}
|
|
315
520
|
signal.throwIfAborted();
|
|
316
521
|
try {
|
|
317
522
|
const updated = await session.connection.agent.request(methods.agent.session.setConfigOption, { sessionId: session.acpSessionId, configId: option.id, value }, { cancellationSignal: signal });
|
|
@@ -319,6 +524,8 @@ export class AcpExecutor {
|
|
|
319
524
|
if (confirmed?.type !== 'select' || confirmed.currentValue !== value)
|
|
320
525
|
throw new AcpRuntimeError('Agent did not confirm the selected configuration', 'acp_config_unconfirmed');
|
|
321
526
|
session.configOptions = updated.configOptions;
|
|
527
|
+
if (key === 'model')
|
|
528
|
+
await this.#persistConfirmedModel(session, value);
|
|
322
529
|
}
|
|
323
530
|
catch (error) {
|
|
324
531
|
// A rejected/unconfirmed mutation can have reached the Agent. Restore the
|
|
@@ -332,6 +539,8 @@ export class AcpExecutor {
|
|
|
332
539
|
confirmed.currentValue === option.currentValue &&
|
|
333
540
|
!session.lost) {
|
|
334
541
|
session.configOptions = rollback.configOptions;
|
|
542
|
+
if (key === 'model')
|
|
543
|
+
await this.#persistConfirmedModel(session, option.currentValue);
|
|
335
544
|
restored = true;
|
|
336
545
|
}
|
|
337
546
|
}
|
|
@@ -345,6 +554,13 @@ export class AcpExecutor {
|
|
|
345
554
|
}
|
|
346
555
|
}
|
|
347
556
|
}
|
|
557
|
+
async #persistConfirmedModel(session, model) {
|
|
558
|
+
if (!session.record || session.record.confirmedModel === model)
|
|
559
|
+
return;
|
|
560
|
+
const record = { ...session.record, confirmedModel: model };
|
|
561
|
+
await this.#state?.write?.(session.conversationKey, record);
|
|
562
|
+
session.record = record;
|
|
563
|
+
}
|
|
348
564
|
#configureClient(session, app) {
|
|
349
565
|
app
|
|
350
566
|
.onNotification(methods.client.session.update, ({ params }) => {
|
|
@@ -352,13 +568,13 @@ export class AcpExecutor {
|
|
|
352
568
|
this.#acceptUpdate(session, params.update);
|
|
353
569
|
})
|
|
354
570
|
.onRequest(methods.client.fs.readTextFile, async ({ params }) => {
|
|
355
|
-
this.#
|
|
571
|
+
this.#assertWorkspaceAccess(session, params.sessionId);
|
|
356
572
|
return {
|
|
357
573
|
content: await readWorkspaceTextFile(session.cwd, params.path, params.line, params.limit),
|
|
358
574
|
};
|
|
359
575
|
})
|
|
360
576
|
.onRequest(methods.client.fs.writeTextFile, async ({ params }) => {
|
|
361
|
-
this.#
|
|
577
|
+
this.#assertWorkspaceAccess(session, params.sessionId);
|
|
362
578
|
await writeWorkspaceTextFile(session.cwd, params.path, params.content);
|
|
363
579
|
return {};
|
|
364
580
|
})
|
|
@@ -366,6 +582,8 @@ export class AcpExecutor {
|
|
|
366
582
|
}
|
|
367
583
|
async #requestPermission(session, request) {
|
|
368
584
|
this.#assertSession(session, request.sessionId);
|
|
585
|
+
if (session.restoring || session.historyGap || session.lost)
|
|
586
|
+
return { outcome: { outcome: 'cancelled' } };
|
|
369
587
|
const active = session.active;
|
|
370
588
|
if (!active)
|
|
371
589
|
return { outcome: { outcome: 'cancelled' } };
|
|
@@ -378,6 +596,16 @@ export class AcpExecutor {
|
|
|
378
596
|
return { outcome };
|
|
379
597
|
}
|
|
380
598
|
#acceptUpdate(session, update) {
|
|
599
|
+
if (session.restoring) {
|
|
600
|
+
if (session.replay) {
|
|
601
|
+
session.replay.updates++;
|
|
602
|
+
if (update.sessionUpdate === 'user_message_chunk')
|
|
603
|
+
session.replay.userChunks++;
|
|
604
|
+
}
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (session.historyGap || session.lost)
|
|
608
|
+
return;
|
|
381
609
|
if (update.sessionUpdate === 'config_option_update') {
|
|
382
610
|
// During our mutation, its response (or rollback response) is authoritative.
|
|
383
611
|
if (!session.configuring && !session.lost)
|
|
@@ -401,6 +629,12 @@ export class AcpExecutor {
|
|
|
401
629
|
if (update.sessionUpdate === 'tool_call_update')
|
|
402
630
|
this.#acceptTool(active, update);
|
|
403
631
|
}
|
|
632
|
+
#assertWorkspaceAccess(session, sessionId) {
|
|
633
|
+
this.#assertSession(session, sessionId);
|
|
634
|
+
if (session.restoring || session.historyGap || session.lost) {
|
|
635
|
+
throw new AcpRuntimeError('ACP filesystem access is unavailable while Session history is untrusted', 'acp_history_gap');
|
|
636
|
+
}
|
|
637
|
+
}
|
|
404
638
|
#acceptTool(active, update) {
|
|
405
639
|
const snapshot = active.tools.get(update.toolCallId) ?? {
|
|
406
640
|
id: update.toolCallId,
|
|
@@ -516,6 +750,10 @@ export class AcpExecutor {
|
|
|
516
750
|
await owner.dispose();
|
|
517
751
|
}
|
|
518
752
|
}
|
|
753
|
+
function currentAcpModel(options) {
|
|
754
|
+
const option = options.find((candidate) => candidate.type === 'select' && (candidate.id === 'model' || candidate.category === 'model'));
|
|
755
|
+
return option?.type === 'select' ? option.currentValue : undefined;
|
|
756
|
+
}
|
|
519
757
|
function toolTextContent(content) {
|
|
520
758
|
return content
|
|
521
759
|
.flatMap((item) => item.type === 'content' && item.content.type === 'text' ? [item.content.text] : [])
|
|
@@ -549,15 +787,82 @@ function pluginStateStore(storage, executorId) {
|
|
|
549
787
|
const key = (conversationKey) => `acp/${executorId}/${createHash('sha256').update(conversationKey).digest('hex')}`;
|
|
550
788
|
return {
|
|
551
789
|
async has(conversationKey) {
|
|
552
|
-
|
|
553
|
-
.value;
|
|
554
|
-
return value?.version === 1;
|
|
790
|
+
return (await storage.get(key(conversationKey))).value !== undefined;
|
|
555
791
|
},
|
|
556
792
|
async mark(conversationKey, cwd) {
|
|
557
793
|
await storage.set(key(conversationKey), { version: 1, cwd });
|
|
558
794
|
},
|
|
795
|
+
async read(conversationKey) {
|
|
796
|
+
return (await storage.get(key(conversationKey))).value;
|
|
797
|
+
},
|
|
798
|
+
async write(conversationKey, state) {
|
|
799
|
+
await storage.set(key(conversationKey), state);
|
|
800
|
+
},
|
|
559
801
|
};
|
|
560
802
|
}
|
|
803
|
+
function decodeContinuity(value) {
|
|
804
|
+
if (value === undefined)
|
|
805
|
+
return undefined;
|
|
806
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
807
|
+
return 'invalid';
|
|
808
|
+
const record = value;
|
|
809
|
+
if (record.version === 1)
|
|
810
|
+
return 'legacy';
|
|
811
|
+
if (record.version !== 2 ||
|
|
812
|
+
typeof record.cwd !== 'string' ||
|
|
813
|
+
!isAbsolute(record.cwd) ||
|
|
814
|
+
typeof record.binding !== 'string' ||
|
|
815
|
+
!/^sha256:[a-f0-9]{64}$/u.test(record.binding) ||
|
|
816
|
+
!Number.isSafeInteger(record.committedPrompts) ||
|
|
817
|
+
record.committedPrompts < 0 ||
|
|
818
|
+
!['reserved', 'established', 'prompt_pending', 'committed', 'history_gap'].includes(String(record.phase)) ||
|
|
819
|
+
(record.sessionId !== undefined &&
|
|
820
|
+
(typeof record.sessionId !== 'string' ||
|
|
821
|
+
!record.sessionId ||
|
|
822
|
+
record.sessionId.length > 4096)) ||
|
|
823
|
+
(record.pendingTurnId !== undefined &&
|
|
824
|
+
(typeof record.pendingTurnId !== 'string' || !record.pendingTurnId)) ||
|
|
825
|
+
(record.confirmedModel !== undefined &&
|
|
826
|
+
(typeof record.confirmedModel !== 'string' || !record.confirmedModel)) ||
|
|
827
|
+
(record.phase === 'reserved' && record.sessionId !== undefined) ||
|
|
828
|
+
(record.phase !== 'reserved' && !record.sessionId) ||
|
|
829
|
+
(record.phase === 'prompt_pending' && !record.pendingTurnId) ||
|
|
830
|
+
(record.phase !== 'prompt_pending' && record.pendingTurnId !== undefined) ||
|
|
831
|
+
(record.gapEvidence !== undefined &&
|
|
832
|
+
(record.phase !== 'history_gap' ||
|
|
833
|
+
!record.gapEvidence ||
|
|
834
|
+
typeof record.gapEvidence !== 'object' ||
|
|
835
|
+
!Number.isSafeInteger(record.gapEvidence.replayedUpdates) ||
|
|
836
|
+
!Number.isSafeInteger(record.gapEvidence.replayedUserChunks) ||
|
|
837
|
+
record.gapEvidence.replayedUpdates < 0 ||
|
|
838
|
+
record.gapEvidence.replayedUserChunks < 0)))
|
|
839
|
+
return 'invalid';
|
|
840
|
+
return record;
|
|
841
|
+
}
|
|
842
|
+
async function launchBinding(adapterId, launch, signal) {
|
|
843
|
+
const paths = [launch.executable, ...(launch.requiredExecutables ?? [])];
|
|
844
|
+
const files = [];
|
|
845
|
+
for (const path of paths) {
|
|
846
|
+
signal.throwIfAborted();
|
|
847
|
+
const resolved = await checkedExecutable(path);
|
|
848
|
+
const hash = createHash('sha256');
|
|
849
|
+
for await (const chunk of createReadStream(resolved)) {
|
|
850
|
+
signal.throwIfAborted();
|
|
851
|
+
hash.update(chunk);
|
|
852
|
+
}
|
|
853
|
+
files.push({ path: resolved, digest: hash.digest('hex') });
|
|
854
|
+
}
|
|
855
|
+
return `sha256:${createHash('sha256')
|
|
856
|
+
.update(JSON.stringify({
|
|
857
|
+
adapterId,
|
|
858
|
+
files,
|
|
859
|
+
args: launch.args ?? [],
|
|
860
|
+
cwd: launch.cwd ?? dirname(files[0].path),
|
|
861
|
+
platform: process.platform,
|
|
862
|
+
arch: process.arch,
|
|
863
|
+
}))
|
|
864
|
+
.digest('hex')}`;
|
|
865
|
+
}
|
|
561
866
|
function validateAdapter(adapter) {
|
|
562
867
|
if (!adapter ||
|
|
563
868
|
typeof adapter !== 'object' ||
|