farming-code 2.2.20 → 2.2.22
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/README.md +5 -5
- package/README.zh_cn.md +5 -5
- package/THIRD_PARTY_NOTICES.md +5 -8
- package/backend/acp/client-services.js +37 -5
- package/backend/acp/patch-decisions.js +19 -3
- package/backend/acp-runtime.js +1201 -198
- package/backend/acp-transcript.js +66 -0
- package/backend/agent-lifecycle-journal.js +133 -0
- package/backend/agent-manager.js +2715 -354
- package/backend/agent-order.js +8 -4
- package/backend/agent-runtime-binding.js +1 -0
- package/backend/bundle-update-helper.js +5 -1
- package/backend/config-manager.js +16 -0
- package/backend/control-api.js +146 -20
- package/backend/farming-app-cli.js +335 -10
- package/backend/farming-session-store.js +283 -36
- package/backend/json-cli-runtime.js +120 -6
- package/backend/npm-update-helper.js +97 -18
- package/backend/provider-adapters.js +18 -6
- package/backend/review-session-service.js +31 -17
- package/backend/server.js +332 -102
- package/backend/storage-layout.js +10 -0
- package/backend/theme-manager.js +23 -5
- package/backend/update-service.js +239 -27
- package/backend/workspace-file-router.js +9 -3
- package/backend/workspace-file-service.js +292 -62
- package/dist/acp/codex-acp-1.1.4.js +32 -1
- package/dist/assets/App-CmZdNavW.js +210 -0
- package/dist/assets/{FileEditorMarkdownPreview-R0jwwLME.js → FileEditorMarkdownPreview-KbnE8VC8.js} +1 -1
- package/dist/assets/FileEditorPane-5_HUejjd.js +2 -0
- package/dist/assets/IconGlyphs-C2WFcoJS.js +1 -0
- package/dist/assets/ProjectFilesSection-C1RAJWOE.js +12 -0
- package/dist/assets/{ReviewPage-DBKNOWsJ.js → ReviewPage-L1vh1Ggs.js} +3 -3
- package/dist/assets/code-dark-BKtvf0Xc.css +1 -0
- package/dist/assets/file-icons-CHWt5nSy.js +1 -0
- package/dist/assets/{index-DuMjesL2.js → index-RvUiMpGP.js} +2 -2
- package/dist/assets/main-XMXgyJJt.css +1 -0
- package/dist/assets/workspace-view-state-BqB4HM7r.js +7 -0
- package/dist/index.html +1 -1
- package/frontend/skins/crt/app.js +199 -81
- package/package.json +9 -3
- package/dist/assets/App-BupehIbc.js +0 -210
- package/dist/assets/FileEditorPane-CJs6kfZO.js +0 -2
- package/dist/assets/IconGlyphs-CZzDr5o2.js +0 -1
- package/dist/assets/ProjectFilesSection-DWCV07lA.js +0 -12
- package/dist/assets/code-dark-BAt0Q_Op.css +0 -1
- package/dist/assets/file-icons-DpbrD5ZP.js +0 -1
- package/dist/assets/main-y2aK_56c.css +0 -1
- package/dist/assets/workspace-view-state-BpPINu84.js +0 -7
package/backend/acp-runtime.js
CHANGED
|
@@ -13,7 +13,7 @@ const { readCodexHistoryImageData } = require('./codex-transcript');
|
|
|
13
13
|
const { AcpClientFileSystem, AcpClientTerminalManager } = require('./acp/client-services');
|
|
14
14
|
const { PACKAGED_CODEX_ACP_ARG } = require('./acp/packaged-codex-acp');
|
|
15
15
|
const { permissionSecurityWarnings } = require('./acp/permission-security');
|
|
16
|
-
const { rejectPatch } = require('./acp/patch-decisions');
|
|
16
|
+
const { patchBlock, rejectPatch } = require('./acp/patch-decisions');
|
|
17
17
|
const { getProviderAdapter, listProviderAdapters } = require('./provider-adapters');
|
|
18
18
|
const { isSafeProviderSessionId } = require('./provider-session-id');
|
|
19
19
|
|
|
@@ -35,7 +35,7 @@ const CODEX_SET_SESSION_MODEL_METHOD = 'session/set_model';
|
|
|
35
35
|
const CODEX_STEER_METHOD = '_codex/session/steer';
|
|
36
36
|
const CODEX_ACP_PACKAGE = '@agentclientprotocol/codex-acp';
|
|
37
37
|
const CODEX_ACP_VERSION = '1.1.4';
|
|
38
|
-
const CODEX_ACP_SHA256 = '
|
|
38
|
+
const CODEX_ACP_SHA256 = 'e36876cc2250737c719644e6c69ab054a3f8b58071fad45c44407013082942ff';
|
|
39
39
|
const CODEX_ACP_VENDOR_ENTRY = path.join(
|
|
40
40
|
__dirname,
|
|
41
41
|
'..',
|
|
@@ -234,7 +234,8 @@ function validateElicitationContent(request, content) {
|
|
|
234
234
|
function interactiveRuntimeState(binding, fallback = '') {
|
|
235
235
|
if (binding.pendingPermissions.size > 0) return 'waiting-for-permission';
|
|
236
236
|
if (binding.pendingElicitations.size > 0) return 'waiting-for-input';
|
|
237
|
-
if (binding.
|
|
237
|
+
if (binding.activeTurn?.phase === 'running') return 'working';
|
|
238
|
+
if (binding.activeTurn?.phase === 'cancelling') return 'interrupting';
|
|
238
239
|
if (['connecting', 'idle', 'error'].includes(String(fallback || ''))) return fallback;
|
|
239
240
|
return binding.sessionId ? 'idle' : 'connecting';
|
|
240
241
|
}
|
|
@@ -336,6 +337,98 @@ function signalProcessTree(binding, signal) {
|
|
|
336
337
|
binding.child.kill(signal);
|
|
337
338
|
}
|
|
338
339
|
|
|
340
|
+
async function stopBindingProcessAndWait(binding) {
|
|
341
|
+
if (await waitForProcessTreeExit(binding, IDENTITY_ADAPTER_GRACEFUL_EXIT_MS)) return;
|
|
342
|
+
signalProcessTree(binding, 'SIGTERM');
|
|
343
|
+
if (await waitForProcessTreeExit(binding, IDENTITY_ADAPTER_TERMINATE_MS)) return;
|
|
344
|
+
signalProcessTree(binding, 'SIGKILL');
|
|
345
|
+
if (await waitForProcessTreeExit(binding, IDENTITY_ADAPTER_KILL_MS)) return;
|
|
346
|
+
throw new Error(`ACP identity adapter process tree ${binding.child?.pid || ''} did not exit`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function describeAcpProcessGroup(pid) {
|
|
350
|
+
const processId = Number(pid);
|
|
351
|
+
if (!Number.isSafeInteger(processId) || processId <= 0 || process.platform === 'win32') {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
let stdout;
|
|
355
|
+
try {
|
|
356
|
+
({ stdout } = await execFileAsync(
|
|
357
|
+
'/bin/ps',
|
|
358
|
+
['-p', String(processId), '-o', 'pid=', '-o', 'pgid=', '-o', 'lstart='],
|
|
359
|
+
{ encoding: 'utf8', timeout: 1_000, maxBuffer: 16_384 },
|
|
360
|
+
));
|
|
361
|
+
} catch (error) {
|
|
362
|
+
if (error?.code === 1 || error?.code === 'ESRCH') return null;
|
|
363
|
+
throw error;
|
|
364
|
+
}
|
|
365
|
+
const match = String(stdout || '').trim().match(/^(\d+)\s+(\d+)\s+(.+)$/);
|
|
366
|
+
if (!match || Number(match[1]) !== processId) {
|
|
367
|
+
throw new Error(`Could not read ACP process identity for pid ${processId}`);
|
|
368
|
+
}
|
|
369
|
+
if (Number(match[2]) !== processId) {
|
|
370
|
+
throw new Error(`ACP process ${processId} did not become its own process-group leader`);
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
pid: processId,
|
|
374
|
+
processGroupId: Number(match[2]),
|
|
375
|
+
startedAt: match[3].trim(),
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function stopPersistedAcpProcessGroup(identity) {
|
|
380
|
+
const expected = identity && typeof identity === 'object' ? identity : null;
|
|
381
|
+
if (
|
|
382
|
+
!expected
|
|
383
|
+
|| !Number.isSafeInteger(Number(expected.pid))
|
|
384
|
+
|| !Number.isSafeInteger(Number(expected.processGroupId))
|
|
385
|
+
|| !String(expected.startedAt || '').trim()
|
|
386
|
+
) {
|
|
387
|
+
return { stopped: false, missingProof: true };
|
|
388
|
+
}
|
|
389
|
+
const processGroupId = Number(expected.processGroupId);
|
|
390
|
+
const groupHasExited = () => {
|
|
391
|
+
try {
|
|
392
|
+
process.kill(-processGroupId, 0);
|
|
393
|
+
return false;
|
|
394
|
+
} catch (error) {
|
|
395
|
+
return error?.code === 'ESRCH';
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
const current = await describeAcpProcessGroup(Number(expected.pid));
|
|
399
|
+
if (!current && groupHasExited()) return { stopped: true, alreadyExited: true };
|
|
400
|
+
if (
|
|
401
|
+
current
|
|
402
|
+
&& (
|
|
403
|
+
current.processGroupId !== processGroupId
|
|
404
|
+
|| current.startedAt !== String(expected.startedAt)
|
|
405
|
+
)
|
|
406
|
+
) {
|
|
407
|
+
return { stopped: false, identityMismatch: true };
|
|
408
|
+
}
|
|
409
|
+
const waitForExit = async timeoutMs => {
|
|
410
|
+
const deadline = Date.now() + timeoutMs;
|
|
411
|
+
while (Date.now() <= deadline) {
|
|
412
|
+
if (groupHasExited()) return true;
|
|
413
|
+
await new Promise(resolve => setTimeout(resolve, 25));
|
|
414
|
+
}
|
|
415
|
+
return groupHasExited();
|
|
416
|
+
};
|
|
417
|
+
const signal = value => {
|
|
418
|
+
try {
|
|
419
|
+
process.kill(-processGroupId, value);
|
|
420
|
+
} catch (error) {
|
|
421
|
+
if (error?.code !== 'ESRCH') throw error;
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
signal('SIGTERM');
|
|
426
|
+
if (await waitForExit(IDENTITY_ADAPTER_TERMINATE_MS)) return { stopped: true };
|
|
427
|
+
signal('SIGKILL');
|
|
428
|
+
if (await waitForExit(IDENTITY_ADAPTER_KILL_MS)) return { stopped: true };
|
|
429
|
+
return { stopped: false, timedOut: true };
|
|
430
|
+
}
|
|
431
|
+
|
|
339
432
|
async function deleteProviderSessionIdentity(options = {}) {
|
|
340
433
|
const provider = String(options.provider || '').trim().toLowerCase();
|
|
341
434
|
const sessionId = String(options.sessionId || '').trim();
|
|
@@ -386,22 +479,66 @@ class AcpRuntime extends EventEmitter {
|
|
|
386
479
|
this.historyReplayQuietMs = options.historyReplayQuietMs ?? DEFAULT_HISTORY_REPLAY_QUIET_MS;
|
|
387
480
|
this.historyReplayMaxWaitMs = options.historyReplayMaxWaitMs ?? DEFAULT_HISTORY_REPLAY_MAX_WAIT_MS;
|
|
388
481
|
this.deleteProviderSessionIdentity = options.deleteProviderSessionIdentity || deleteProviderSessionIdentity;
|
|
482
|
+
this.describeProcessGroup = options.describeAcpProcessGroup || describeAcpProcessGroup;
|
|
389
483
|
this.checkpointStore = options.checkpointStore
|
|
390
484
|
|| (options.configDir ? new AcpCheckpointStore(options.configDir, options.checkpointOptions) : null);
|
|
391
485
|
this.bindings = new Map();
|
|
392
|
-
this.
|
|
393
|
-
this.
|
|
486
|
+
this.disposing = false;
|
|
487
|
+
this.disposePromise = null;
|
|
488
|
+
this.disposed = false;
|
|
394
489
|
this.clientFileSystem = options.clientFileSystem || new AcpClientFileSystem();
|
|
395
490
|
this.clientTerminals = options.clientTerminals || new AcpClientTerminalManager({ spawn: options.terminalSpawn });
|
|
396
491
|
}
|
|
397
492
|
|
|
398
493
|
async prepareAgent(options = {}) {
|
|
494
|
+
if (this.disposing || this.disposed) {
|
|
495
|
+
throw new Error('ACP runtime is shutting down');
|
|
496
|
+
}
|
|
399
497
|
const agentId = String(options.agentId || '');
|
|
400
498
|
if (!agentId) throw new Error('ACP Agent id is required');
|
|
401
499
|
if (this.bindings.has(agentId)) throw new Error('ACP Agent is already registered');
|
|
402
500
|
const provider = String(options.provider || '').trim().toLowerCase();
|
|
403
501
|
const launch = this.resolveLaunch(provider, options);
|
|
404
502
|
const cwd = path.resolve(options.cwd || process.cwd());
|
|
503
|
+
const requestedSessionId = String(options.sessionId || '').trim();
|
|
504
|
+
const forkSourceSessionId = String(options.forkSourceSessionId || '').trim();
|
|
505
|
+
const forkSourceCheckpoint = options.forkSourceCheckpoint || null;
|
|
506
|
+
const forkSourceSessionCheckpoint = forkSourceCheckpoint?.version === 2
|
|
507
|
+
? forkSourceCheckpoint.sessionState
|
|
508
|
+
: forkSourceCheckpoint;
|
|
509
|
+
const revisionBase = Number.isFinite(Number(options.revisionBase))
|
|
510
|
+
? Math.max(0, Math.floor(Number(options.revisionBase)))
|
|
511
|
+
: 0;
|
|
512
|
+
let forkSourceCheckpointState = null;
|
|
513
|
+
if (forkSourceSessionId) {
|
|
514
|
+
if (requestedSessionId) {
|
|
515
|
+
throw new Error('ACP fork startup cannot also load a target session');
|
|
516
|
+
}
|
|
517
|
+
if (!isSafeProviderSessionId(forkSourceSessionId)) {
|
|
518
|
+
throw new Error('ACP fork startup requires a safe exact source session id');
|
|
519
|
+
}
|
|
520
|
+
if (!forkSourceCheckpoint) {
|
|
521
|
+
throw new Error('ACP fork startup requires an exact source checkpoint');
|
|
522
|
+
}
|
|
523
|
+
const checkpointCwd = String(forkSourceSessionCheckpoint?.cwd || '').trim();
|
|
524
|
+
if (
|
|
525
|
+
String(forkSourceSessionCheckpoint?.provider || '') !== provider
|
|
526
|
+
|| String(forkSourceSessionCheckpoint?.sessionId || '') !== forkSourceSessionId
|
|
527
|
+
|| !checkpointCwd
|
|
528
|
+
|| path.resolve(checkpointCwd) !== cwd
|
|
529
|
+
) {
|
|
530
|
+
throw new Error('ACP fork source checkpoint does not match the requested source');
|
|
531
|
+
}
|
|
532
|
+
forkSourceCheckpointState = AcpSessionState.fromCheckpoint(forkSourceSessionCheckpoint, {
|
|
533
|
+
provider,
|
|
534
|
+
sessionId: forkSourceSessionId,
|
|
535
|
+
cwd,
|
|
536
|
+
maxUpdates: this.maxUpdates,
|
|
537
|
+
});
|
|
538
|
+
if (!forkSourceCheckpointState) {
|
|
539
|
+
throw new Error('ACP fork source checkpoint is invalid');
|
|
540
|
+
}
|
|
541
|
+
}
|
|
405
542
|
const sessionRequestOptions = acpSessionRequestOptions(options, cwd);
|
|
406
543
|
const binding = {
|
|
407
544
|
agentId,
|
|
@@ -413,7 +550,7 @@ class AcpRuntime extends EventEmitter {
|
|
|
413
550
|
launch,
|
|
414
551
|
restartOptions: { ...options, agentId, provider },
|
|
415
552
|
approvalMode: options.approvalMode || 'approve',
|
|
416
|
-
ownsProcessGroup:
|
|
553
|
+
ownsProcessGroup: process.platform !== 'win32',
|
|
417
554
|
child: null,
|
|
418
555
|
connection: null,
|
|
419
556
|
initializeResponse: null,
|
|
@@ -430,23 +567,43 @@ class AcpRuntime extends EventEmitter {
|
|
|
430
567
|
elicitationResolvers: new Map(),
|
|
431
568
|
activeElicitations: new Map(),
|
|
432
569
|
subagentStates: new Map(),
|
|
570
|
+
subagentControls: new Map(),
|
|
571
|
+
nextSubagentGeneration: 1,
|
|
433
572
|
interactionOrigins: new Map(),
|
|
434
|
-
|
|
573
|
+
// One binding owns at most one Turn. All Turn controls and its terminal
|
|
574
|
+
// commit are fenced by this object before they may mutate shared state.
|
|
575
|
+
activeTurn: null,
|
|
576
|
+
nextTurnId: 1,
|
|
435
577
|
supportsSteer: false,
|
|
436
|
-
steerTail: null,
|
|
437
578
|
historyReplayActive: false,
|
|
438
579
|
sessionState: null,
|
|
439
580
|
authTerminal: null,
|
|
440
581
|
patchDecisions: new Map(),
|
|
582
|
+
patchDecisionInFlight: new Map(),
|
|
441
583
|
checkpointProof: null,
|
|
584
|
+
sessionMutation: null,
|
|
585
|
+
configMutationTail: null,
|
|
442
586
|
stderr: '',
|
|
587
|
+
exited: false,
|
|
443
588
|
updatedAt: new Date().toISOString(),
|
|
444
589
|
};
|
|
445
590
|
this.bindings.set(agentId, binding);
|
|
446
591
|
this.emitRuntime(binding);
|
|
447
592
|
|
|
448
593
|
try {
|
|
449
|
-
const
|
|
594
|
+
const gatedLaunch = binding.ownsProcessGroup
|
|
595
|
+
? {
|
|
596
|
+
command: '/bin/sh',
|
|
597
|
+
args: [
|
|
598
|
+
'-c',
|
|
599
|
+
'IFS= read -r farming_acp_start || exit 0; exec "$@"',
|
|
600
|
+
'farming-acp-start-gate',
|
|
601
|
+
launch.command,
|
|
602
|
+
...launch.args,
|
|
603
|
+
],
|
|
604
|
+
}
|
|
605
|
+
: launch;
|
|
606
|
+
const child = this.spawn(gatedLaunch.command, gatedLaunch.args, {
|
|
450
607
|
cwd: binding.cwd,
|
|
451
608
|
env: binding.env,
|
|
452
609
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -463,13 +620,30 @@ class AcpRuntime extends EventEmitter {
|
|
|
463
620
|
this.handleExit(binding, code === 0 ? null : new Error(detail));
|
|
464
621
|
}
|
|
465
622
|
});
|
|
623
|
+
if (binding.ownsProcessGroup && typeof options.onProcessStarted === 'function') {
|
|
624
|
+
const processIdentity = await this.describeProcessGroup(child.pid);
|
|
625
|
+
if (!processIdentity) {
|
|
626
|
+
throw new Error(`ACP process ${child.pid || ''} exited before its identity was persisted`);
|
|
627
|
+
}
|
|
628
|
+
await options.onProcessStarted(processIdentity);
|
|
629
|
+
}
|
|
630
|
+
if (binding.ownsProcessGroup) {
|
|
631
|
+
await new Promise((resolve, reject) => {
|
|
632
|
+
child.stdin.write('start\n', error => {
|
|
633
|
+
if (error) reject(error);
|
|
634
|
+
else resolve();
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
}
|
|
466
638
|
|
|
467
639
|
const connection = this.createConnection
|
|
468
640
|
? await this.createConnection(this.clientHandlers(binding), child, binding)
|
|
469
641
|
: await this.officialConnection(this.clientHandlers(binding), child);
|
|
470
642
|
binding.connection = connection;
|
|
643
|
+
this.requireOpenBinding(binding);
|
|
471
644
|
connection.closed.catch(error => this.handleExit(binding, error));
|
|
472
645
|
const sdk = await loadAcpSdk();
|
|
646
|
+
this.requireOpenBinding(binding);
|
|
473
647
|
binding.initializeResponse = await withTimeout(connection.initialize({
|
|
474
648
|
protocolVersion: sdk.PROTOCOL_VERSION,
|
|
475
649
|
clientCapabilities: {
|
|
@@ -483,20 +657,99 @@ class AcpRuntime extends EventEmitter {
|
|
|
483
657
|
},
|
|
484
658
|
clientInfo: { name: 'farming', title: 'Farming', version: packageJson.version || '0.0.0' },
|
|
485
659
|
}), this.initializeTimeoutMs, 'ACP initialize');
|
|
660
|
+
this.requireOpenBinding(binding);
|
|
486
661
|
if (binding.initializeResponse.protocolVersion !== sdk.PROTOCOL_VERSION) {
|
|
487
662
|
throw new Error(`ACP protocol version mismatch: Agent selected ${binding.initializeResponse.protocolVersion}, Farming supports ${sdk.PROTOCOL_VERSION}`);
|
|
488
663
|
}
|
|
489
664
|
binding.supportsSteer = binding.provider === 'codex'
|
|
490
665
|
&& supportsCodexSteer(binding.initializeResponse.agentCapabilities);
|
|
491
666
|
|
|
492
|
-
const requestedSessionId = String(options.sessionId || '').trim();
|
|
493
|
-
const revisionBase = Number.isFinite(Number(options.revisionBase))
|
|
494
|
-
? Math.max(0, Math.floor(Number(options.revisionBase)))
|
|
495
|
-
: 0;
|
|
496
667
|
const sessionRequest = { sessionId: requestedSessionId, ...binding.sessionRequestOptions };
|
|
497
668
|
let sessionResponse;
|
|
498
669
|
let historyMode = 'new';
|
|
499
|
-
if (
|
|
670
|
+
if (forkSourceSessionId) {
|
|
671
|
+
const capabilities = binding.initializeResponse.agentCapabilities || {};
|
|
672
|
+
if (!capabilities.loadSession || !capabilities.sessionCapabilities?.fork) {
|
|
673
|
+
throw new Error(`${provider} ACP Agent cannot create an owned fork`);
|
|
674
|
+
}
|
|
675
|
+
if (!capabilities.sessionCapabilities?.close) {
|
|
676
|
+
throw new Error(`${provider} ACP Agent cannot release the loaded fork source`);
|
|
677
|
+
}
|
|
678
|
+
binding.sessionId = forkSourceSessionId;
|
|
679
|
+
binding.sessionState = new AcpSessionState({
|
|
680
|
+
provider,
|
|
681
|
+
sessionId: forkSourceSessionId,
|
|
682
|
+
cwd: binding.cwd,
|
|
683
|
+
maxUpdates: this.maxUpdates,
|
|
684
|
+
revisionBase,
|
|
685
|
+
});
|
|
686
|
+
binding.historyReplayActive = true;
|
|
687
|
+
let loadedSource;
|
|
688
|
+
try {
|
|
689
|
+
loadedSource = await withTimeout(
|
|
690
|
+
connection.loadSession({ sessionId: forkSourceSessionId, ...binding.sessionRequestOptions }),
|
|
691
|
+
this.sessionSetupTimeoutMs,
|
|
692
|
+
'ACP fork source session/load',
|
|
693
|
+
);
|
|
694
|
+
this.requireOpenBinding(binding);
|
|
695
|
+
if (provider === 'qoder') await this.waitForHistoryReplay(binding);
|
|
696
|
+
this.requireOpenBinding(binding);
|
|
697
|
+
} finally {
|
|
698
|
+
binding.historyReplayActive = false;
|
|
699
|
+
}
|
|
700
|
+
binding.sessionState.finishHistoryReplay();
|
|
701
|
+
const forked = await withTimeout(
|
|
702
|
+
connection.unstable_forkSession({
|
|
703
|
+
sessionId: forkSourceSessionId,
|
|
704
|
+
...binding.sessionRequestOptions,
|
|
705
|
+
}),
|
|
706
|
+
this.sessionSetupTimeoutMs,
|
|
707
|
+
'ACP owned session/fork',
|
|
708
|
+
);
|
|
709
|
+
const forkedSessionId = String(forked?.sessionId || '').trim();
|
|
710
|
+
if (
|
|
711
|
+
!isSafeProviderSessionId(forkedSessionId)
|
|
712
|
+
|| forkedSessionId === forkSourceSessionId
|
|
713
|
+
) {
|
|
714
|
+
throw new Error('ACP owned session/fork returned an invalid new session id');
|
|
715
|
+
}
|
|
716
|
+
if (typeof options.onForkSessionCreated === 'function') {
|
|
717
|
+
await options.onForkSessionCreated(forkedSessionId);
|
|
718
|
+
}
|
|
719
|
+
this.requireOpenBinding(binding);
|
|
720
|
+
await withTimeout(
|
|
721
|
+
connection.closeSession({ sessionId: forkSourceSessionId }),
|
|
722
|
+
this.requestTimeoutMs,
|
|
723
|
+
'ACP fork source session/close',
|
|
724
|
+
);
|
|
725
|
+
this.requireOpenBinding(binding);
|
|
726
|
+
const forkUpdates = binding.subagentStates.get(forkedSessionId);
|
|
727
|
+
const restoredForkSource = this.restoreBindingCheckpoint(binding, forkSourceCheckpoint, {
|
|
728
|
+
sessionId: forkedSessionId,
|
|
729
|
+
});
|
|
730
|
+
if (!restoredForkSource) {
|
|
731
|
+
throw new Error('ACP fork source checkpoint could not be restored');
|
|
732
|
+
}
|
|
733
|
+
binding.sessionId = forkedSessionId;
|
|
734
|
+
binding.sessionState = restoredForkSource.sessionState;
|
|
735
|
+
binding.subagentStates = restoredForkSource.subagentStates;
|
|
736
|
+
this.resetSubagentControls(binding);
|
|
737
|
+
binding.patchDecisions = restoredForkSource.patchDecisions;
|
|
738
|
+
delete binding.restartOptions.forkSourceSessionId;
|
|
739
|
+
delete binding.restartOptions.forkSourceCheckpoint;
|
|
740
|
+
delete binding.restartOptions.onForkSessionCreated;
|
|
741
|
+
if (forkUpdates?.updates?.some(item => item?.update?.sessionUpdate === 'available_commands_update')) {
|
|
742
|
+
binding.sessionState.availableCommands = clone(forkUpdates.availableCommands);
|
|
743
|
+
}
|
|
744
|
+
sessionResponse = {
|
|
745
|
+
...loadedSource,
|
|
746
|
+
...forked,
|
|
747
|
+
sessionId: forkedSessionId,
|
|
748
|
+
modes: forked?.modes || loadedSource?.modes,
|
|
749
|
+
configOptions: forked?.configOptions || loadedSource?.configOptions,
|
|
750
|
+
};
|
|
751
|
+
historyMode = 'fork';
|
|
752
|
+
} else if (requestedSessionId) {
|
|
500
753
|
const capabilities = binding.initializeResponse.agentCapabilities || {};
|
|
501
754
|
let opened = false;
|
|
502
755
|
let restoredCheckpointState = null;
|
|
@@ -507,6 +760,7 @@ class AcpRuntime extends EventEmitter {
|
|
|
507
760
|
this.checkpointIdentity(binding, requestedSessionId),
|
|
508
761
|
{ allowDirty: true },
|
|
509
762
|
);
|
|
763
|
+
this.requireOpenBinding(binding);
|
|
510
764
|
restoredCheckpoint = this.restoreBindingCheckpoint(binding, saved?.state, {
|
|
511
765
|
sessionId: requestedSessionId,
|
|
512
766
|
});
|
|
@@ -516,10 +770,12 @@ class AcpRuntime extends EventEmitter {
|
|
|
516
770
|
const providerStateMatches = restoredCheckpoint?.complete === true && saved?.exact === true
|
|
517
771
|
? await this.checkpointMatchesProviderSession(connection, capabilities, sessionRequest, saved)
|
|
518
772
|
: false;
|
|
773
|
+
this.requireOpenBinding(binding);
|
|
519
774
|
if (providerStateMatches) {
|
|
520
775
|
binding.sessionId = requestedSessionId;
|
|
521
776
|
binding.sessionState = restoredCheckpointState;
|
|
522
777
|
binding.subagentStates = restoredCheckpoint.subagentStates;
|
|
778
|
+
this.resetSubagentControls(binding);
|
|
523
779
|
binding.patchDecisions = restoredCheckpoint.patchDecisions;
|
|
524
780
|
binding.checkpointProof = restoredCheckpoint.providerProof;
|
|
525
781
|
try {
|
|
@@ -528,9 +784,11 @@ class AcpRuntime extends EventEmitter {
|
|
|
528
784
|
this.sessionSetupTimeoutMs,
|
|
529
785
|
'ACP session/resume'
|
|
530
786
|
);
|
|
787
|
+
this.requireOpenBinding(binding);
|
|
531
788
|
historyMode = 'checkpoint';
|
|
532
789
|
opened = true;
|
|
533
790
|
} catch (error) {
|
|
791
|
+
this.requireOpenBinding(binding);
|
|
534
792
|
if (!capabilities.loadSession) throw error;
|
|
535
793
|
console.warn(`ACP checkpoint resume failed for ${provider}; replaying history:`, error && (error.message || error));
|
|
536
794
|
}
|
|
@@ -554,7 +812,9 @@ class AcpRuntime extends EventEmitter {
|
|
|
554
812
|
this.sessionSetupTimeoutMs,
|
|
555
813
|
'ACP session/load'
|
|
556
814
|
);
|
|
815
|
+
this.requireOpenBinding(binding);
|
|
557
816
|
if (provider === 'qoder') await this.waitForHistoryReplay(binding);
|
|
817
|
+
this.requireOpenBinding(binding);
|
|
558
818
|
} finally {
|
|
559
819
|
binding.historyReplayActive = false;
|
|
560
820
|
}
|
|
@@ -568,8 +828,11 @@ class AcpRuntime extends EventEmitter {
|
|
|
568
828
|
} catch {
|
|
569
829
|
// Local history images can still be restored from their adapter paths.
|
|
570
830
|
}
|
|
831
|
+
this.requireOpenBinding(binding);
|
|
571
832
|
await binding.sessionState.hydrateCodexHistoryAttachments({ imageDataByPath });
|
|
833
|
+
this.requireOpenBinding(binding);
|
|
572
834
|
}
|
|
835
|
+
this.restorePatchDecisions(binding, restoredCheckpoint?.patchDecisions);
|
|
573
836
|
historyMode = 'load';
|
|
574
837
|
opened = true;
|
|
575
838
|
} else if (!opened && capabilities.sessionCapabilities?.resume) {
|
|
@@ -586,6 +849,7 @@ class AcpRuntime extends EventEmitter {
|
|
|
586
849
|
this.sessionSetupTimeoutMs,
|
|
587
850
|
'ACP session/resume'
|
|
588
851
|
);
|
|
852
|
+
this.requireOpenBinding(binding);
|
|
589
853
|
historyMode = 'resume';
|
|
590
854
|
opened = true;
|
|
591
855
|
} else {
|
|
@@ -597,6 +861,7 @@ class AcpRuntime extends EventEmitter {
|
|
|
597
861
|
this.sessionSetupTimeoutMs,
|
|
598
862
|
'ACP session/new'
|
|
599
863
|
);
|
|
864
|
+
this.requireOpenBinding(binding);
|
|
600
865
|
const returnedSessionId = String(sessionResponse.sessionId || '').trim();
|
|
601
866
|
if (!isSafeProviderSessionId(returnedSessionId)) {
|
|
602
867
|
binding.untrustedSessionId = returnedSessionId;
|
|
@@ -605,6 +870,7 @@ class AcpRuntime extends EventEmitter {
|
|
|
605
870
|
binding.sessionId = returnedSessionId;
|
|
606
871
|
binding.sessionState = new AcpSessionState({ provider, sessionId: binding.sessionId, cwd: binding.cwd, maxUpdates: this.maxUpdates });
|
|
607
872
|
}
|
|
873
|
+
this.requireOpenBinding(binding);
|
|
608
874
|
binding.modes = sessionResponse?.modes || null;
|
|
609
875
|
binding.configOptions = sessionResponse?.configOptions || [];
|
|
610
876
|
binding.sessionState.currentModeId = String(binding.modes?.currentModeId || '');
|
|
@@ -619,6 +885,7 @@ class AcpRuntime extends EventEmitter {
|
|
|
619
885
|
await this.applySessionConfigOption(binding, fastOption.id, fastEnabled, { emit: false });
|
|
620
886
|
}
|
|
621
887
|
}
|
|
888
|
+
this.requireOpenBinding(binding);
|
|
622
889
|
binding.state = 'idle';
|
|
623
890
|
binding.updatedAt = new Date().toISOString();
|
|
624
891
|
this.scheduleCheckpoint(binding, { exact: true });
|
|
@@ -670,7 +937,45 @@ class AcpRuntime extends EventEmitter {
|
|
|
670
937
|
attachProviderSessionIdentity(runtimeError, identity);
|
|
671
938
|
}
|
|
672
939
|
} else {
|
|
673
|
-
|
|
940
|
+
let runtimeCleanupVerified = false;
|
|
941
|
+
try {
|
|
942
|
+
if (this.isCurrentBinding(binding)) {
|
|
943
|
+
runtimeCleanupVerified = await this.unregisterAgentAndWait(agentId, binding);
|
|
944
|
+
} else {
|
|
945
|
+
try {
|
|
946
|
+
binding.connection?.close();
|
|
947
|
+
} catch {
|
|
948
|
+
// Process-tree cleanup below remains authoritative.
|
|
949
|
+
}
|
|
950
|
+
await stopBindingProcessAndWait(binding);
|
|
951
|
+
runtimeCleanupVerified = true;
|
|
952
|
+
}
|
|
953
|
+
} catch (cleanupError) {
|
|
954
|
+
Object.defineProperty(runtimeError, 'adapterCleanupError', {
|
|
955
|
+
value: cleanupError,
|
|
956
|
+
enumerable: false,
|
|
957
|
+
configurable: true,
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
Object.defineProperties(runtimeError, {
|
|
961
|
+
runtimeCleanupAttempted: {
|
|
962
|
+
value: true,
|
|
963
|
+
enumerable: false,
|
|
964
|
+
configurable: true,
|
|
965
|
+
},
|
|
966
|
+
runtimeCleanupVerified: {
|
|
967
|
+
value: runtimeCleanupVerified === true,
|
|
968
|
+
enumerable: false,
|
|
969
|
+
configurable: true,
|
|
970
|
+
},
|
|
971
|
+
});
|
|
972
|
+
if (!runtimeCleanupVerified && !this.isCurrentBinding(binding)) {
|
|
973
|
+
try {
|
|
974
|
+
binding.connection?.close();
|
|
975
|
+
} catch {
|
|
976
|
+
// The detached binding remains only as an error cleanup handle.
|
|
977
|
+
}
|
|
978
|
+
}
|
|
674
979
|
}
|
|
675
980
|
throw runtimeError;
|
|
676
981
|
}
|
|
@@ -738,6 +1043,199 @@ class AcpRuntime extends EventEmitter {
|
|
|
738
1043
|
return result;
|
|
739
1044
|
}
|
|
740
1045
|
|
|
1046
|
+
isCurrentBinding(binding) {
|
|
1047
|
+
return Boolean(binding) && this.bindings.get(binding.agentId) === binding;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
requireCurrentBinding(binding) {
|
|
1051
|
+
if (!this.isCurrentBinding(binding)) {
|
|
1052
|
+
throw new Error('ACP Agent binding is no longer active');
|
|
1053
|
+
}
|
|
1054
|
+
return binding;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
isOpenBinding(binding) {
|
|
1058
|
+
return this.isCurrentBinding(binding) && binding.exited !== true;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
requireOpenBinding(binding) {
|
|
1062
|
+
if (this.disposing || this.disposed) throw new Error('ACP runtime is shutting down');
|
|
1063
|
+
this.requireCurrentBinding(binding);
|
|
1064
|
+
if (binding.exited) throw new Error('ACP Agent connection is closed');
|
|
1065
|
+
return binding;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
isCurrentTurn(binding, turn) {
|
|
1069
|
+
return this.isOpenBinding(binding) && binding.activeTurn === turn;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
requireCurrentTurn(binding, turn, phases = null) {
|
|
1073
|
+
this.requireOpenBinding(binding);
|
|
1074
|
+
if (
|
|
1075
|
+
binding.activeTurn !== turn
|
|
1076
|
+
|| (Array.isArray(phases) && !phases.includes(turn?.phase))
|
|
1077
|
+
) {
|
|
1078
|
+
throw new Error('No active Codex turn to steer');
|
|
1079
|
+
}
|
|
1080
|
+
return turn;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
beginTurn(binding) {
|
|
1084
|
+
this.requireOpenBinding(binding);
|
|
1085
|
+
if (binding.activeTurn) throw new Error(`ACP Agent is not ready (${binding.state})`);
|
|
1086
|
+
let resolveCompletion;
|
|
1087
|
+
const completion = new Promise(resolve => {
|
|
1088
|
+
resolveCompletion = resolve;
|
|
1089
|
+
});
|
|
1090
|
+
const turn = {
|
|
1091
|
+
id: binding.nextTurnId++,
|
|
1092
|
+
phase: 'admitting',
|
|
1093
|
+
previousState: binding.state,
|
|
1094
|
+
providerSettled: false,
|
|
1095
|
+
controlTail: Promise.resolve(),
|
|
1096
|
+
cancelPromise: null,
|
|
1097
|
+
completion,
|
|
1098
|
+
resolveCompletion,
|
|
1099
|
+
};
|
|
1100
|
+
binding.activeTurn = turn;
|
|
1101
|
+
return turn;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
finishTurn(binding, turn, result) {
|
|
1105
|
+
if (binding.activeTurn !== turn) return false;
|
|
1106
|
+
binding.activeTurn = null;
|
|
1107
|
+
turn.phase = 'completed';
|
|
1108
|
+
turn.resolveCompletion(result);
|
|
1109
|
+
return true;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
cancelAdmittingTurn(binding, turn) {
|
|
1113
|
+
if (binding.activeTurn !== turn || turn.phase !== 'admitting') return false;
|
|
1114
|
+
binding.activeTurn = null;
|
|
1115
|
+
turn.phase = 'cancelled';
|
|
1116
|
+
binding.state = turn.previousState || 'idle';
|
|
1117
|
+
turn.resolveCompletion({ status: 'cancelled-before-submission' });
|
|
1118
|
+
this.emitRuntime(binding);
|
|
1119
|
+
return true;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
async enqueueTurnControl(turn, operation) {
|
|
1123
|
+
const previous = turn.controlTail || Promise.resolve();
|
|
1124
|
+
const pending = previous.catch(() => {}).then(operation);
|
|
1125
|
+
turn.controlTail = pending;
|
|
1126
|
+
try {
|
|
1127
|
+
return await pending;
|
|
1128
|
+
} finally {
|
|
1129
|
+
if (turn.controlTail === pending) turn.controlTail = Promise.resolve();
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
ensureSubagentControl(binding, sessionId) {
|
|
1134
|
+
const id = String(sessionId || '');
|
|
1135
|
+
if (!id) return null;
|
|
1136
|
+
let control = binding.subagentControls.get(id);
|
|
1137
|
+
if (!control) {
|
|
1138
|
+
control = {
|
|
1139
|
+
sessionId: id,
|
|
1140
|
+
generation: binding.nextSubagentGeneration++,
|
|
1141
|
+
phase: 'active',
|
|
1142
|
+
error: '',
|
|
1143
|
+
cancelPromise: null,
|
|
1144
|
+
};
|
|
1145
|
+
binding.subagentControls.set(id, control);
|
|
1146
|
+
}
|
|
1147
|
+
return control;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
resetSubagentControls(binding) {
|
|
1151
|
+
binding.subagentControls.clear();
|
|
1152
|
+
for (const sessionId of binding.subagentStates.keys()) {
|
|
1153
|
+
const control = this.ensureSubagentControl(binding, sessionId);
|
|
1154
|
+
const parentTool = binding.sessionState?.entries.find(entry => (
|
|
1155
|
+
entry?.type === 'tool'
|
|
1156
|
+
&& String(entry?._meta?.subagent_session_info?.session_id || '') === sessionId
|
|
1157
|
+
));
|
|
1158
|
+
if (parentTool) {
|
|
1159
|
+
this.updateSubagentControlFromParent(binding, parentTool);
|
|
1160
|
+
} else {
|
|
1161
|
+
// Restored evidence without a parent Tool cannot prove that a child
|
|
1162
|
+
// from the previous adapter process is still active.
|
|
1163
|
+
control.phase = 'completed';
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
activeSubagentSessionIds(binding) {
|
|
1169
|
+
return [...binding.subagentStates.keys()].filter(sessionId => {
|
|
1170
|
+
const control = this.ensureSubagentControl(binding, sessionId);
|
|
1171
|
+
if (control?.cancelPromise || ['active', 'cancelling'].includes(control?.phase)) return true;
|
|
1172
|
+
const hasInteraction = [...binding.pendingPermissions.values(), ...binding.pendingElicitations.values()]
|
|
1173
|
+
.some(request => String(request?.sessionId || '') === sessionId);
|
|
1174
|
+
if (hasInteraction) return true;
|
|
1175
|
+
const parentTool = binding.sessionState?.entries.find(entry => (
|
|
1176
|
+
entry?.type === 'tool'
|
|
1177
|
+
&& String(entry?._meta?.subagent_session_info?.session_id || '') === sessionId
|
|
1178
|
+
));
|
|
1179
|
+
return ['pending', 'in_progress', 'in-progress', 'running']
|
|
1180
|
+
.includes(String(parentTool?.status || '').toLowerCase());
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
updateSubagentControlFromParent(binding, update) {
|
|
1185
|
+
const sessionId = String(update?._meta?.subagent_session_info?.session_id || '');
|
|
1186
|
+
if (!sessionId || !binding.subagentStates.has(sessionId)) return;
|
|
1187
|
+
const control = this.ensureSubagentControl(binding, sessionId);
|
|
1188
|
+
const status = String(update?.status || '').toLowerCase();
|
|
1189
|
+
if (['cancelled', 'canceled'].includes(status)) {
|
|
1190
|
+
control.phase = 'cancelled';
|
|
1191
|
+
control.error = '';
|
|
1192
|
+
} else if (['completed', 'complete'].includes(status)) {
|
|
1193
|
+
control.phase = 'completed';
|
|
1194
|
+
control.error = '';
|
|
1195
|
+
} else if (['failed', 'error'].includes(status)) {
|
|
1196
|
+
control.phase = 'error';
|
|
1197
|
+
control.error = String(update?.title || 'Subagent failed');
|
|
1198
|
+
} else if (
|
|
1199
|
+
['pending', 'in_progress', 'in-progress', 'running'].includes(status)
|
|
1200
|
+
&& !['cancelling', 'cancelled', 'completed', 'error'].includes(control.phase)
|
|
1201
|
+
) {
|
|
1202
|
+
control.phase = 'active';
|
|
1203
|
+
control.error = '';
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
beginSessionMutation(binding, action) {
|
|
1208
|
+
this.requireOpenBinding(binding);
|
|
1209
|
+
if (
|
|
1210
|
+
binding.sessionMutation
|
|
1211
|
+
|| binding.configMutationTail
|
|
1212
|
+
|| binding.patchDecisionInFlight.size > 0
|
|
1213
|
+
|| binding.activeTurn
|
|
1214
|
+
|| !['idle', 'error'].includes(binding.state)
|
|
1215
|
+
) {
|
|
1216
|
+
throw new Error(`ACP Agent is not ready for ${action} (${binding.state})`);
|
|
1217
|
+
}
|
|
1218
|
+
const mutation = { action };
|
|
1219
|
+
binding.sessionMutation = mutation;
|
|
1220
|
+
return mutation;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
endSessionMutation(binding, mutation) {
|
|
1224
|
+
if (binding.sessionMutation === mutation) binding.sessionMutation = null;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
requireConfigMutationReady(binding) {
|
|
1228
|
+
this.requireOpenBinding(binding);
|
|
1229
|
+
if (
|
|
1230
|
+
binding.sessionMutation
|
|
1231
|
+
|| binding.patchDecisionInFlight.size > 0
|
|
1232
|
+
|| binding.activeTurn
|
|
1233
|
+
|| !['idle', 'error'].includes(binding.state)
|
|
1234
|
+
) {
|
|
1235
|
+
throw new Error(`ACP Agent is not ready for configuration changes (${binding.state})`);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
741
1239
|
checkpointIdentity(binding, sessionId = binding?.sessionId) {
|
|
742
1240
|
if (!binding || !sessionId) return null;
|
|
743
1241
|
return {
|
|
@@ -749,6 +1247,7 @@ class AcpRuntime extends EventEmitter {
|
|
|
749
1247
|
}
|
|
750
1248
|
|
|
751
1249
|
scheduleCheckpoint(binding, _options = {}) {
|
|
1250
|
+
if (!this.isOpenBinding(binding)) return;
|
|
752
1251
|
const identity = this.checkpointIdentity(binding);
|
|
753
1252
|
if (!this.checkpointStore || !identity || !binding.sessionState) return;
|
|
754
1253
|
// ACP currently has no conditional resume or provider-owned revision
|
|
@@ -759,15 +1258,19 @@ class AcpRuntime extends EventEmitter {
|
|
|
759
1258
|
}
|
|
760
1259
|
|
|
761
1260
|
async markCheckpointDirty(binding) {
|
|
1261
|
+
this.requireOpenBinding(binding);
|
|
762
1262
|
const identity = this.checkpointIdentity(binding);
|
|
763
1263
|
if (!this.checkpointStore || !identity) return;
|
|
764
1264
|
await this.checkpointStore.markDirty(identity);
|
|
1265
|
+
this.requireOpenBinding(binding);
|
|
765
1266
|
}
|
|
766
1267
|
|
|
767
1268
|
async writeCheckpoint(binding, _options = {}) {
|
|
1269
|
+
this.requireOpenBinding(binding);
|
|
768
1270
|
const identity = this.checkpointIdentity(binding);
|
|
769
1271
|
if (!this.checkpointStore || !identity || !binding.sessionState) return;
|
|
770
1272
|
await this.checkpointStore.write(identity, this.bindingCheckpoint(binding), { exact: false });
|
|
1273
|
+
this.requireOpenBinding(binding);
|
|
771
1274
|
}
|
|
772
1275
|
|
|
773
1276
|
bindingCheckpoint(binding) {
|
|
@@ -830,6 +1333,29 @@ class AcpRuntime extends EventEmitter {
|
|
|
830
1333
|
};
|
|
831
1334
|
}
|
|
832
1335
|
|
|
1336
|
+
restorePatchDecisions(binding, decisions) {
|
|
1337
|
+
if (!(decisions instanceof Map) || !binding.sessionState) return;
|
|
1338
|
+
for (const [key, decision] of decisions) {
|
|
1339
|
+
const separator = key.indexOf('\n');
|
|
1340
|
+
if (separator <= 0) continue;
|
|
1341
|
+
const toolCallId = key.slice(0, separator);
|
|
1342
|
+
const requestedPath = key.slice(separator + 1);
|
|
1343
|
+
const entry = binding.sessionState.toolEntries.get(toolCallId);
|
|
1344
|
+
if (!entry || binding.sessionState.isInternalEntry(entry)) continue;
|
|
1345
|
+
try {
|
|
1346
|
+
patchBlock(entry, binding.cwd, requestedPath);
|
|
1347
|
+
} catch {
|
|
1348
|
+
continue;
|
|
1349
|
+
}
|
|
1350
|
+
binding.patchDecisions.set(key, decision);
|
|
1351
|
+
entry._meta = { ...(entry._meta || {}) };
|
|
1352
|
+
entry._meta.farming_patch_decisions = {
|
|
1353
|
+
...(entry._meta.farming_patch_decisions || {}),
|
|
1354
|
+
[requestedPath]: decision,
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
833
1359
|
async checkpointMatchesProviderSession(connection, capabilities, request, saved) {
|
|
834
1360
|
const proof = saved?.state?.providerProof;
|
|
835
1361
|
if (
|
|
@@ -874,6 +1400,7 @@ class AcpRuntime extends EventEmitter {
|
|
|
874
1400
|
let lastChangedAt = startedAt;
|
|
875
1401
|
while (Date.now() - startedAt < this.historyReplayMaxWaitMs) {
|
|
876
1402
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
1403
|
+
if (!this.isOpenBinding(binding)) return;
|
|
877
1404
|
const now = Date.now();
|
|
878
1405
|
const updateCount = binding.sessionState?.updates.length || 0;
|
|
879
1406
|
if (updateCount !== lastUpdateCount) {
|
|
@@ -888,8 +1415,17 @@ class AcpRuntime extends EventEmitter {
|
|
|
888
1415
|
}
|
|
889
1416
|
|
|
890
1417
|
clientHandlers(binding) {
|
|
1418
|
+
const openClientRequest = handler => async request => {
|
|
1419
|
+
this.requireOpenBinding(binding);
|
|
1420
|
+
if (binding.state === 'closed') throw new Error('ACP session is closed');
|
|
1421
|
+
const response = await handler(request);
|
|
1422
|
+
this.requireOpenBinding(binding);
|
|
1423
|
+
if (binding.state === 'closed') throw new Error('ACP session is closed');
|
|
1424
|
+
return response;
|
|
1425
|
+
};
|
|
891
1426
|
return {
|
|
892
1427
|
sessionUpdate: notification => {
|
|
1428
|
+
if (!this.isOpenBinding(binding) || binding.state === 'closed') return;
|
|
893
1429
|
const notificationSessionId = String(notification?.sessionId || '');
|
|
894
1430
|
const isPrimarySession = !binding.sessionId || !notificationSessionId || notificationSessionId === binding.sessionId;
|
|
895
1431
|
let targetState = isPrimarySession ? binding.sessionState : binding.subagentStates.get(notificationSessionId);
|
|
@@ -902,8 +1438,17 @@ class AcpRuntime extends EventEmitter {
|
|
|
902
1438
|
});
|
|
903
1439
|
binding.subagentStates.set(notificationSessionId, targetState);
|
|
904
1440
|
}
|
|
1441
|
+
if (!isPrimarySession && targetState) {
|
|
1442
|
+
this.ensureSubagentControl(binding, notificationSessionId);
|
|
1443
|
+
const parentTool = binding.sessionState?.entries.find(entry => (
|
|
1444
|
+
entry?.type === 'tool'
|
|
1445
|
+
&& String(entry?._meta?.subagent_session_info?.session_id || '') === notificationSessionId
|
|
1446
|
+
));
|
|
1447
|
+
if (parentTool) this.updateSubagentControlFromParent(binding, parentTool);
|
|
1448
|
+
}
|
|
905
1449
|
if (targetState?.apply(notification)) {
|
|
906
1450
|
const update = notification?.update;
|
|
1451
|
+
if (isPrimarySession) this.updateSubagentControlFromParent(binding, update);
|
|
907
1452
|
if (isPrimarySession && update?.sessionUpdate === 'current_mode_update' && binding.modes) {
|
|
908
1453
|
binding.modes = { ...binding.modes, currentModeId: String(update.currentModeId || '') };
|
|
909
1454
|
}
|
|
@@ -918,7 +1463,7 @@ class AcpRuntime extends EventEmitter {
|
|
|
918
1463
|
if (parentTool) binding.sessionState.touchEntry(parentTool);
|
|
919
1464
|
}
|
|
920
1465
|
binding.updatedAt = new Date().toISOString();
|
|
921
|
-
if (isPrimarySession && !binding.historyReplayActive && !binding.
|
|
1466
|
+
if (isPrimarySession && !binding.historyReplayActive && !binding.activeTurn) {
|
|
922
1467
|
this.scheduleCheckpoint(binding, { exact: true });
|
|
923
1468
|
}
|
|
924
1469
|
// A loaded history can contain hundreds of ordered updates. Applying
|
|
@@ -929,22 +1474,35 @@ class AcpRuntime extends EventEmitter {
|
|
|
929
1474
|
}
|
|
930
1475
|
},
|
|
931
1476
|
requestPermission: request => this.requestPermission(binding, request),
|
|
932
|
-
readTextFile: request => this.clientFileSystem.readTextFile(binding, request),
|
|
933
|
-
writeTextFile: request => this.clientFileSystem.writeTextFile(binding, request),
|
|
934
|
-
createTerminal: request => this.clientTerminals.create(binding, request),
|
|
935
|
-
terminalOutput: request => this.clientTerminals.output(binding, request),
|
|
936
|
-
waitForTerminalExit: request => this.clientTerminals.waitForExit(binding, request),
|
|
937
|
-
killTerminal: request => this.clientTerminals.kill(binding, request),
|
|
938
|
-
releaseTerminal: request => this.clientTerminals.release(binding, request),
|
|
1477
|
+
readTextFile: openClientRequest(request => this.clientFileSystem.readTextFile(binding, request)),
|
|
1478
|
+
writeTextFile: openClientRequest(request => this.clientFileSystem.writeTextFile(binding, request)),
|
|
1479
|
+
createTerminal: openClientRequest(request => this.clientTerminals.create(binding, request)),
|
|
1480
|
+
terminalOutput: openClientRequest(request => this.clientTerminals.output(binding, request)),
|
|
1481
|
+
waitForTerminalExit: openClientRequest(request => this.clientTerminals.waitForExit(binding, request)),
|
|
1482
|
+
killTerminal: openClientRequest(request => this.clientTerminals.kill(binding, request)),
|
|
1483
|
+
releaseTerminal: openClientRequest(request => this.clientTerminals.release(binding, request)),
|
|
939
1484
|
unstable_createElicitation: request => this.requestElicitation(binding, request),
|
|
940
1485
|
unstable_completeElicitation: notification => this.completeElicitation(binding, notification),
|
|
941
1486
|
};
|
|
942
1487
|
}
|
|
943
1488
|
|
|
944
1489
|
requestPermission(binding, request) {
|
|
1490
|
+
if (!this.isOpenBinding(binding) || binding.state === 'closed') {
|
|
1491
|
+
return { outcome: { outcome: 'cancelled' } };
|
|
1492
|
+
}
|
|
1493
|
+
const requestSessionId = String(request?.sessionId || '');
|
|
1494
|
+
if (
|
|
1495
|
+
!requestSessionId
|
|
1496
|
+
|| (
|
|
1497
|
+
requestSessionId !== binding.sessionId
|
|
1498
|
+
&& !binding.subagentStates.has(requestSessionId)
|
|
1499
|
+
)
|
|
1500
|
+
) {
|
|
1501
|
+
return { outcome: { outcome: 'cancelled' } };
|
|
1502
|
+
}
|
|
945
1503
|
const automatic = autoPermissionResponse(request, binding.approvalMode);
|
|
946
1504
|
if (automatic) return automatic;
|
|
947
|
-
const requestId = `acp-permission-${
|
|
1505
|
+
const requestId = `acp-permission-${crypto.randomUUID()}`;
|
|
948
1506
|
binding.interactionOrigins.set(requestId, binding.state);
|
|
949
1507
|
binding.state = 'waiting-for-permission';
|
|
950
1508
|
const pending = { ...JSON.parse(JSON.stringify(request)), requestId };
|
|
@@ -979,6 +1537,7 @@ class AcpRuntime extends EventEmitter {
|
|
|
979
1537
|
}
|
|
980
1538
|
|
|
981
1539
|
requestElicitation(binding, request) {
|
|
1540
|
+
if (!this.isOpenBinding(binding) || binding.state === 'closed') return { action: 'cancel' };
|
|
982
1541
|
const hasSessionScope = typeof request?.sessionId === 'string' && request.sessionId.length > 0;
|
|
983
1542
|
const requestSessionId = hasSessionScope ? String(request.sessionId) : '';
|
|
984
1543
|
const isPrimarySession = hasSessionScope && requestSessionId === binding.sessionId;
|
|
@@ -989,7 +1548,7 @@ class AcpRuntime extends EventEmitter {
|
|
|
989
1548
|
if (!['form', 'url'].includes(String(request?.mode || ''))) {
|
|
990
1549
|
return { action: 'cancel' };
|
|
991
1550
|
}
|
|
992
|
-
const requestId = `acp-elicitation-${
|
|
1551
|
+
const requestId = `acp-elicitation-${crypto.randomUUID()}`;
|
|
993
1552
|
const cloned = JSON.parse(JSON.stringify(request));
|
|
994
1553
|
const protocolRequestId = Object.prototype.hasOwnProperty.call(cloned, 'requestId')
|
|
995
1554
|
? cloned.requestId
|
|
@@ -1033,61 +1592,141 @@ class AcpRuntime extends EventEmitter {
|
|
|
1033
1592
|
}
|
|
1034
1593
|
|
|
1035
1594
|
completeElicitation(binding, notification) {
|
|
1595
|
+
if (!this.isOpenBinding(binding) || binding.state === 'closed') return;
|
|
1036
1596
|
const elicitationId = String(notification?.elicitationId || '');
|
|
1037
1597
|
if (elicitationId) binding.activeElicitations.delete(elicitationId);
|
|
1038
1598
|
this.emitRuntime(binding);
|
|
1039
1599
|
}
|
|
1040
1600
|
|
|
1041
|
-
async
|
|
1601
|
+
async submitMessage(agentId, prompt, options = {}) {
|
|
1042
1602
|
const binding = this.requireBinding(agentId);
|
|
1043
|
-
|
|
1603
|
+
while (true) {
|
|
1604
|
+
this.requireOpenBinding(binding);
|
|
1605
|
+
const turn = binding.activeTurn;
|
|
1606
|
+
if (turn?.phase === 'blocked') {
|
|
1607
|
+
throw new Error(`ACP Agent is not ready (${binding.state})`);
|
|
1608
|
+
}
|
|
1609
|
+
if (
|
|
1610
|
+
turn?.phase === 'running'
|
|
1611
|
+
&& turn.providerSettled !== true
|
|
1612
|
+
&& binding.provider === 'codex'
|
|
1613
|
+
&& binding.supportsSteer === true
|
|
1614
|
+
) {
|
|
1615
|
+
try {
|
|
1616
|
+
const result = await this.steer(agentId, prompt);
|
|
1617
|
+
options.onSubmitted?.();
|
|
1618
|
+
return { steered: true, ...result };
|
|
1619
|
+
} catch (error) {
|
|
1620
|
+
if (!isCodexSteerUnavailableError(error)) throw error;
|
|
1621
|
+
this.requireOpenBinding(binding);
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
if (binding.activeTurn === turn && turn) {
|
|
1625
|
+
await turn.completion;
|
|
1626
|
+
continue;
|
|
1627
|
+
}
|
|
1628
|
+
if (binding.activeTurn) continue;
|
|
1629
|
+
return this.prompt(agentId, prompt, options);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
async prompt(agentId, prompt, options = {}) {
|
|
1634
|
+
const binding = this.requireBinding(agentId);
|
|
1635
|
+
if (
|
|
1636
|
+
binding.exited
|
|
1637
|
+
|| binding.sessionMutation
|
|
1638
|
+
|| binding.activeTurn
|
|
1639
|
+
|| !['idle', 'error'].includes(binding.state)
|
|
1640
|
+
) {
|
|
1641
|
+
throw new Error(`ACP Agent is not ready (${binding.state})`);
|
|
1642
|
+
}
|
|
1044
1643
|
const rawContent = Array.isArray(prompt) ? prompt : [{ type: 'text', text: String(prompt || '') }];
|
|
1045
1644
|
const content = promptContentForCapabilities(
|
|
1046
1645
|
rawContent,
|
|
1047
1646
|
binding.initializeResponse?.agentCapabilities || {},
|
|
1048
1647
|
);
|
|
1049
|
-
|
|
1648
|
+
const turn = this.beginTurn(binding);
|
|
1649
|
+
try {
|
|
1650
|
+
const configMutation = binding.configMutationTail;
|
|
1651
|
+
const patchDecisions = [...binding.patchDecisionInFlight.values()].map(item => item.promise);
|
|
1652
|
+
await Promise.allSettled([
|
|
1653
|
+
...(configMutation ? [configMutation] : []),
|
|
1654
|
+
...patchDecisions,
|
|
1655
|
+
]);
|
|
1656
|
+
if (!this.isCurrentTurn(binding, turn) || turn.phase !== 'admitting') {
|
|
1657
|
+
throw new Error('ACP prompt was cancelled before submission');
|
|
1658
|
+
}
|
|
1659
|
+
await this.markCheckpointDirty(binding);
|
|
1660
|
+
if (!this.isCurrentTurn(binding, turn) || turn.phase !== 'admitting') {
|
|
1661
|
+
throw new Error('ACP prompt was cancelled before submission');
|
|
1662
|
+
}
|
|
1663
|
+
} catch (error) {
|
|
1664
|
+
if (this.isCurrentTurn(binding, turn)) {
|
|
1665
|
+
binding.state = turn.previousState;
|
|
1666
|
+
this.finishTurn(binding, turn, { status: 'admission-failed' });
|
|
1667
|
+
}
|
|
1668
|
+
throw error;
|
|
1669
|
+
}
|
|
1050
1670
|
binding.sessionState.beginPrompt(content);
|
|
1051
|
-
|
|
1671
|
+
turn.phase = 'running';
|
|
1052
1672
|
binding.state = 'working';
|
|
1053
1673
|
binding.error = '';
|
|
1054
1674
|
binding.stopReason = '';
|
|
1055
1675
|
this.emitRuntime(binding);
|
|
1056
1676
|
this.emitSession(binding);
|
|
1677
|
+
let response;
|
|
1057
1678
|
try {
|
|
1058
|
-
const
|
|
1679
|
+
const responsePromise = binding.connection.prompt({ sessionId: binding.sessionId, prompt: content });
|
|
1680
|
+
try {
|
|
1681
|
+
options.onSubmitted?.();
|
|
1682
|
+
} catch {
|
|
1683
|
+
// Submission is already owned by the provider; admission callbacks cannot roll it back.
|
|
1684
|
+
}
|
|
1685
|
+
response = await responsePromise;
|
|
1686
|
+
turn.providerSettled = true;
|
|
1687
|
+
} catch (error) {
|
|
1688
|
+
turn.providerSettled = true;
|
|
1689
|
+
const runtimeError = new Error(acpErrorMessage(error), { cause: error });
|
|
1690
|
+
if (!this.isCurrentTurn(binding, turn)) throw runtimeError;
|
|
1691
|
+
await this.enqueueTurnControl(turn, async () => {
|
|
1692
|
+
if (!this.isCurrentTurn(binding, turn)) return;
|
|
1693
|
+
binding.stopReason = 'error';
|
|
1694
|
+
// JSON-RPC implementations commonly move the actionable provider text
|
|
1695
|
+
// into error.data.details. Classify the normalized message so the
|
|
1696
|
+
// ordered transcript and runtime snapshot cannot disagree.
|
|
1697
|
+
binding.sessionState.recordError(runtimeError.message, acpErrorKind(runtimeError));
|
|
1698
|
+
binding.sessionState.completePrompt('error');
|
|
1699
|
+
binding.state = 'error';
|
|
1700
|
+
binding.error = runtimeError.message;
|
|
1701
|
+
binding.updatedAt = new Date().toISOString();
|
|
1702
|
+
this.finishTurn(binding, turn, { status: 'error', error: runtimeError });
|
|
1703
|
+
this.scheduleCheckpoint(binding, { exact: true });
|
|
1704
|
+
this.emitSession(binding);
|
|
1705
|
+
this.emitRuntime(binding);
|
|
1706
|
+
});
|
|
1707
|
+
throw runtimeError;
|
|
1708
|
+
}
|
|
1709
|
+
return this.enqueueTurnControl(turn, async () => {
|
|
1710
|
+
this.requireCurrentTurn(binding, turn);
|
|
1059
1711
|
binding.stopReason = String(response?.stopReason || '');
|
|
1060
1712
|
binding.sessionState.completePrompt(binding.stopReason);
|
|
1061
|
-
binding.promptActive = false;
|
|
1062
1713
|
binding.state = 'idle';
|
|
1714
|
+
binding.error = '';
|
|
1063
1715
|
binding.updatedAt = new Date().toISOString();
|
|
1716
|
+
this.finishTurn(binding, turn, { status: 'completed', stopReason: binding.stopReason });
|
|
1064
1717
|
this.scheduleCheckpoint(binding, { exact: true });
|
|
1065
1718
|
this.emitSession(binding);
|
|
1066
1719
|
this.emitRuntime(binding);
|
|
1067
1720
|
return { sessionId: binding.sessionId, stopReason: binding.stopReason };
|
|
1068
|
-
}
|
|
1069
|
-
const runtimeError = new Error(acpErrorMessage(error), { cause: error });
|
|
1070
|
-
binding.stopReason = 'error';
|
|
1071
|
-
// JSON-RPC implementations commonly move the actionable provider text
|
|
1072
|
-
// into error.data.details. Classify the normalized message so the
|
|
1073
|
-
// ordered transcript and runtime snapshot cannot disagree.
|
|
1074
|
-
binding.sessionState.recordError(runtimeError.message, acpErrorKind(runtimeError));
|
|
1075
|
-
binding.sessionState.completePrompt('error');
|
|
1076
|
-
binding.promptActive = false;
|
|
1077
|
-
binding.state = 'error';
|
|
1078
|
-
binding.error = runtimeError.message;
|
|
1079
|
-
this.scheduleCheckpoint(binding, { exact: true });
|
|
1080
|
-
this.emitSession(binding);
|
|
1081
|
-
this.emitRuntime(binding);
|
|
1082
|
-
throw runtimeError;
|
|
1083
|
-
}
|
|
1721
|
+
});
|
|
1084
1722
|
}
|
|
1085
1723
|
|
|
1086
1724
|
canSteer(agentId) {
|
|
1087
1725
|
const binding = this.bindings.get(agentId);
|
|
1088
1726
|
return binding?.provider === 'codex'
|
|
1089
1727
|
&& binding.supportsSteer === true
|
|
1090
|
-
&& binding.
|
|
1728
|
+
&& binding.activeTurn?.phase === 'running'
|
|
1729
|
+
&& binding.activeTurn?.providerSettled !== true
|
|
1091
1730
|
&& Boolean(binding.sessionId)
|
|
1092
1731
|
&& Boolean(binding.connection);
|
|
1093
1732
|
}
|
|
@@ -1102,10 +1741,15 @@ class AcpRuntime extends EventEmitter {
|
|
|
1102
1741
|
rawContent,
|
|
1103
1742
|
binding.initializeResponse?.agentCapabilities || {},
|
|
1104
1743
|
);
|
|
1744
|
+
const turn = binding.activeTurn;
|
|
1745
|
+
this.requireCurrentTurn(binding, turn, ['running']);
|
|
1105
1746
|
const clientMessageId = `farming-steer-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
1106
1747
|
const operation = async () => {
|
|
1107
|
-
|
|
1748
|
+
this.requireCurrentTurn(binding, turn, ['running']);
|
|
1749
|
+
if (turn.providerSettled) throw new Error('No active Codex turn to steer');
|
|
1108
1750
|
await this.markCheckpointDirty(binding);
|
|
1751
|
+
this.requireCurrentTurn(binding, turn, ['running']);
|
|
1752
|
+
if (turn.providerSettled) throw new Error('No active Codex turn to steer');
|
|
1109
1753
|
const response = await withTimeout(
|
|
1110
1754
|
binding.connection.request(CODEX_STEER_METHOD, {
|
|
1111
1755
|
sessionId: binding.sessionId,
|
|
@@ -1115,62 +1759,118 @@ class AcpRuntime extends EventEmitter {
|
|
|
1115
1759
|
this.requestTimeoutMs,
|
|
1116
1760
|
'Codex ACP steer',
|
|
1117
1761
|
);
|
|
1762
|
+
this.requireCurrentTurn(binding, turn, ['running']);
|
|
1118
1763
|
return {
|
|
1119
1764
|
sessionId: binding.sessionId,
|
|
1120
1765
|
turnId: String(response?.turnId || ''),
|
|
1121
1766
|
clientMessageId,
|
|
1122
1767
|
};
|
|
1123
1768
|
};
|
|
1124
|
-
|
|
1125
|
-
const pending = previous.catch(() => {}).then(operation);
|
|
1126
|
-
binding.steerTail = pending;
|
|
1127
|
-
try {
|
|
1128
|
-
return await pending;
|
|
1129
|
-
} finally {
|
|
1130
|
-
if (binding.steerTail === pending) binding.steerTail = null;
|
|
1131
|
-
}
|
|
1769
|
+
return this.enqueueTurnControl(turn, operation);
|
|
1132
1770
|
}
|
|
1133
1771
|
|
|
1134
1772
|
async cancel(agentId) {
|
|
1135
1773
|
const binding = this.requireBinding(agentId);
|
|
1774
|
+
this.requireOpenBinding(binding);
|
|
1775
|
+
if (binding.state === 'closed') throw new Error('ACP session is closed');
|
|
1136
1776
|
if (!binding.sessionId) return false;
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
}
|
|
1141
|
-
for (const resolve of binding.elicitationResolvers.values()) resolve({ action: 'cancel' });
|
|
1142
|
-
binding.permissionResolvers.clear();
|
|
1143
|
-
binding.pendingPermissions.clear();
|
|
1144
|
-
binding.elicitationResolvers.clear();
|
|
1145
|
-
binding.pendingElicitations.clear();
|
|
1146
|
-
binding.activeElicitations.clear();
|
|
1147
|
-
binding.interactionOrigins.clear();
|
|
1148
|
-
this.emitRuntime(binding);
|
|
1149
|
-
try {
|
|
1150
|
-
await withTimeout(
|
|
1151
|
-
binding.connection.cancel({ sessionId: binding.sessionId }),
|
|
1152
|
-
this.cancelTimeoutMs,
|
|
1153
|
-
'ACP session/cancel'
|
|
1154
|
-
);
|
|
1777
|
+
const turn = binding.activeTurn;
|
|
1778
|
+
if (turn?.phase === 'admitting') {
|
|
1779
|
+
this.cancelAdmittingTurn(binding, turn);
|
|
1155
1780
|
return true;
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
binding.
|
|
1159
|
-
binding.
|
|
1160
|
-
binding.
|
|
1161
|
-
|
|
1162
|
-
|
|
1781
|
+
}
|
|
1782
|
+
if (!turn && (
|
|
1783
|
+
binding.sessionMutation
|
|
1784
|
+
|| binding.configMutationTail
|
|
1785
|
+
|| binding.patchDecisionInFlight.size > 0
|
|
1786
|
+
)) {
|
|
1787
|
+
throw new Error(`ACP Agent is not ready for cancellation (${binding.state})`);
|
|
1788
|
+
}
|
|
1789
|
+
if (turn?.cancelPromise) return turn.cancelPromise;
|
|
1790
|
+
|
|
1791
|
+
const stateBeforeCancellation = binding.state;
|
|
1792
|
+
let cancelAcknowledged = false;
|
|
1793
|
+
const cancelOperation = async () => {
|
|
1794
|
+
const activeSubagentSessionIds = this.activeSubagentSessionIds(binding);
|
|
1795
|
+
if (turn) {
|
|
1796
|
+
this.requireCurrentTurn(binding, turn, ['running']);
|
|
1797
|
+
if (turn.providerSettled) throw new Error('No active turn to cancel');
|
|
1798
|
+
turn.phase = 'cancelling';
|
|
1799
|
+
}
|
|
1800
|
+
binding.state = 'interrupting';
|
|
1801
|
+
for (const resolve of binding.permissionResolvers.values()) {
|
|
1802
|
+
resolve({ outcome: { outcome: 'cancelled' } });
|
|
1803
|
+
}
|
|
1804
|
+
for (const resolve of binding.elicitationResolvers.values()) resolve({ action: 'cancel' });
|
|
1805
|
+
binding.permissionResolvers.clear();
|
|
1806
|
+
binding.pendingPermissions.clear();
|
|
1807
|
+
binding.elicitationResolvers.clear();
|
|
1808
|
+
binding.pendingElicitations.clear();
|
|
1809
|
+
binding.activeElicitations.clear();
|
|
1810
|
+
binding.interactionOrigins.clear();
|
|
1163
1811
|
this.emitRuntime(binding);
|
|
1164
|
-
|
|
1812
|
+
await Promise.all([
|
|
1813
|
+
withTimeout(
|
|
1814
|
+
binding.connection.cancel({ sessionId: binding.sessionId }),
|
|
1815
|
+
this.cancelTimeoutMs,
|
|
1816
|
+
'ACP session/cancel'
|
|
1817
|
+
),
|
|
1818
|
+
...activeSubagentSessionIds.map(sessionId => this.cancelSubagentControl(binding, sessionId)),
|
|
1819
|
+
]);
|
|
1820
|
+
this.requireOpenBinding(binding);
|
|
1821
|
+
cancelAcknowledged = true;
|
|
1822
|
+
return true;
|
|
1823
|
+
};
|
|
1824
|
+
const cancellation = (async () => {
|
|
1825
|
+
try {
|
|
1826
|
+
if (turn) await this.enqueueTurnControl(turn, cancelOperation);
|
|
1827
|
+
else await cancelOperation();
|
|
1828
|
+
if (!turn) {
|
|
1829
|
+
if (binding.state === 'interrupting') {
|
|
1830
|
+
binding.state = ['idle', 'error'].includes(stateBeforeCancellation)
|
|
1831
|
+
? stateBeforeCancellation
|
|
1832
|
+
: 'idle';
|
|
1833
|
+
}
|
|
1834
|
+
binding.updatedAt = new Date().toISOString();
|
|
1835
|
+
this.emitRuntime(binding);
|
|
1836
|
+
return true;
|
|
1837
|
+
}
|
|
1838
|
+
await withTimeout(turn.completion, this.cancelTimeoutMs, 'ACP turn cancellation');
|
|
1839
|
+
return true;
|
|
1840
|
+
} catch (error) {
|
|
1841
|
+
const runtimeError = new Error(acpErrorMessage(error), { cause: error });
|
|
1842
|
+
if (!this.isOpenBinding(binding)) throw runtimeError;
|
|
1843
|
+
const markBlocked = () => {
|
|
1844
|
+
if (turn && binding.activeTurn !== turn) return;
|
|
1845
|
+
if (turn) turn.phase = 'blocked';
|
|
1846
|
+
binding.state = 'error';
|
|
1847
|
+
binding.error = runtimeError.message;
|
|
1848
|
+
binding.stopReason = turn && cancelAcknowledged ? 'cancel_timeout' : 'cancel_error';
|
|
1849
|
+
binding.updatedAt = new Date().toISOString();
|
|
1850
|
+
this.emitSession(binding);
|
|
1851
|
+
this.emitRuntime(binding);
|
|
1852
|
+
};
|
|
1853
|
+
if (turn) await this.enqueueTurnControl(turn, markBlocked);
|
|
1854
|
+
else markBlocked();
|
|
1855
|
+
throw runtimeError;
|
|
1856
|
+
}
|
|
1857
|
+
})();
|
|
1858
|
+
if (turn) turn.cancelPromise = cancellation;
|
|
1859
|
+
try {
|
|
1860
|
+
return await cancellation;
|
|
1861
|
+
} finally {
|
|
1862
|
+
if (turn?.cancelPromise === cancellation) turn.cancelPromise = null;
|
|
1165
1863
|
}
|
|
1166
1864
|
}
|
|
1167
1865
|
|
|
1168
|
-
|
|
1169
|
-
const
|
|
1170
|
-
|
|
1171
|
-
if (
|
|
1172
|
-
|
|
1866
|
+
cancelSubagentControl(binding, targetSessionId) {
|
|
1867
|
+
const control = this.ensureSubagentControl(binding, targetSessionId);
|
|
1868
|
+
if (control.cancelPromise) return control.cancelPromise;
|
|
1869
|
+
if (['cancelled', 'completed'].includes(control.phase)) {
|
|
1870
|
+
return Promise.resolve({ cancelled: true, sessionId: targetSessionId });
|
|
1173
1871
|
}
|
|
1872
|
+
control.phase = 'cancelling';
|
|
1873
|
+
control.error = '';
|
|
1174
1874
|
for (const [requestId, pending] of binding.pendingPermissions.entries()) {
|
|
1175
1875
|
if (String(pending?.sessionId || '') !== targetSessionId) continue;
|
|
1176
1876
|
binding.permissionResolvers.get(requestId)?.({ outcome: { outcome: 'cancelled' } });
|
|
@@ -1190,28 +1890,67 @@ class AcpRuntime extends EventEmitter {
|
|
|
1190
1890
|
}
|
|
1191
1891
|
binding.state = interactiveRuntimeState(binding, binding.state);
|
|
1192
1892
|
this.emitRuntime(binding);
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1893
|
+
const cancellation = (async () => {
|
|
1894
|
+
try {
|
|
1895
|
+
await withTimeout(
|
|
1896
|
+
binding.connection.cancel({ sessionId: targetSessionId }),
|
|
1897
|
+
this.cancelTimeoutMs,
|
|
1898
|
+
'ACP subagent session/cancel'
|
|
1899
|
+
);
|
|
1900
|
+
this.requireOpenBinding(binding);
|
|
1901
|
+
if (binding.subagentControls.get(targetSessionId) !== control) {
|
|
1902
|
+
throw new Error('ACP subagent control is no longer active');
|
|
1903
|
+
}
|
|
1904
|
+
control.phase = 'cancelled';
|
|
1905
|
+
control.error = '';
|
|
1906
|
+
binding.updatedAt = new Date().toISOString();
|
|
1907
|
+
this.emitSession(binding);
|
|
1908
|
+
this.emitRuntime(binding);
|
|
1909
|
+
return { cancelled: true, sessionId: targetSessionId };
|
|
1910
|
+
} catch (error) {
|
|
1911
|
+
if (this.isOpenBinding(binding) && binding.subagentControls.get(targetSessionId) === control) {
|
|
1912
|
+
control.phase = 'error';
|
|
1913
|
+
control.error = acpErrorMessage(error);
|
|
1914
|
+
binding.updatedAt = new Date().toISOString();
|
|
1915
|
+
this.emitSession(binding);
|
|
1916
|
+
this.emitRuntime(binding);
|
|
1917
|
+
}
|
|
1918
|
+
throw error;
|
|
1919
|
+
}
|
|
1920
|
+
})();
|
|
1921
|
+
control.cancelPromise = cancellation;
|
|
1922
|
+
void cancellation.finally(() => {
|
|
1923
|
+
if (binding.subagentControls.get(targetSessionId) === control && control.cancelPromise === cancellation) {
|
|
1924
|
+
control.cancelPromise = null;
|
|
1925
|
+
}
|
|
1926
|
+
}).catch(() => {});
|
|
1927
|
+
return cancellation;
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
async cancelSubagent(agentId, sessionId) {
|
|
1931
|
+
const binding = this.requireBinding(agentId);
|
|
1932
|
+
this.requireOpenBinding(binding);
|
|
1933
|
+
const targetSessionId = String(sessionId || '');
|
|
1934
|
+
if (!targetSessionId || targetSessionId === binding.sessionId || !binding.subagentStates.has(targetSessionId)) {
|
|
1935
|
+
throw new Error('ACP subagent session not found');
|
|
1936
|
+
}
|
|
1937
|
+
return this.cancelSubagentControl(binding, targetSessionId);
|
|
1202
1938
|
}
|
|
1203
1939
|
|
|
1204
1940
|
async listSessions(agentId, options = {}) {
|
|
1205
1941
|
const binding = this.requireBinding(agentId);
|
|
1942
|
+
this.requireOpenBinding(binding);
|
|
1206
1943
|
const capabilities = binding.initializeResponse?.agentCapabilities?.sessionCapabilities;
|
|
1207
1944
|
if (!capabilities?.list) throw new Error(`${binding.provider} ACP Agent does not support session/list`);
|
|
1208
|
-
|
|
1945
|
+
const response = await withTimeout(binding.connection.listSessions({
|
|
1209
1946
|
...(options.cwd ? { cwd: path.resolve(options.cwd) } : {}),
|
|
1210
1947
|
...(Array.isArray(options.additionalDirectories)
|
|
1211
1948
|
? { additionalDirectories: acpSessionRequestOptions(options, options.cwd || binding.cwd).additionalDirectories }
|
|
1212
1949
|
: {}),
|
|
1213
1950
|
...(options.cursor ? { cursor: String(options.cursor) } : {}),
|
|
1214
1951
|
}), this.requestTimeoutMs, 'ACP session/list');
|
|
1952
|
+
this.requireOpenBinding(binding);
|
|
1953
|
+
return response;
|
|
1215
1954
|
}
|
|
1216
1955
|
|
|
1217
1956
|
getSessionRequestOptions(agentId) {
|
|
@@ -1221,37 +1960,55 @@ class AcpRuntime extends EventEmitter {
|
|
|
1221
1960
|
|
|
1222
1961
|
async authenticate(agentId, methodId) {
|
|
1223
1962
|
const binding = this.requireBinding(agentId);
|
|
1963
|
+
this.requireOpenBinding(binding);
|
|
1964
|
+
if (binding.state === 'closed') throw new Error('ACP session is closed');
|
|
1965
|
+
const mutation = this.beginSessionMutation(binding, 'authentication');
|
|
1224
1966
|
const method = binding.initializeResponse?.authMethods?.find(item => item.id === methodId);
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1967
|
+
let terminalReservationTransferred = false;
|
|
1968
|
+
try {
|
|
1969
|
+
if (!method) throw new Error('Unknown ACP authentication method');
|
|
1970
|
+
if (method.type === 'terminal' || method?._meta?.['terminal-auth']) {
|
|
1971
|
+
const result = await this.startTerminalAuthentication(binding, method, mutation);
|
|
1972
|
+
terminalReservationTransferred = true;
|
|
1973
|
+
return result;
|
|
1974
|
+
}
|
|
1975
|
+
await withTimeout(
|
|
1976
|
+
binding.connection.authenticate({ methodId }),
|
|
1977
|
+
this.requestTimeoutMs,
|
|
1978
|
+
'ACP authenticate'
|
|
1979
|
+
);
|
|
1980
|
+
this.requireOpenBinding(binding);
|
|
1981
|
+
binding.error = '';
|
|
1982
|
+
binding.stopReason = '';
|
|
1983
|
+
binding.state = interactiveRuntimeState(binding, 'idle');
|
|
1984
|
+
binding.updatedAt = new Date().toISOString();
|
|
1985
|
+
this.emitRuntime(binding);
|
|
1986
|
+
this.emitSession(binding);
|
|
1987
|
+
return { authenticated: true, methodId };
|
|
1988
|
+
} finally {
|
|
1989
|
+
if (!terminalReservationTransferred) this.endSessionMutation(binding, mutation);
|
|
1228
1990
|
}
|
|
1229
|
-
await withTimeout(
|
|
1230
|
-
binding.connection.authenticate({ methodId }),
|
|
1231
|
-
this.requestTimeoutMs,
|
|
1232
|
-
'ACP authenticate'
|
|
1233
|
-
);
|
|
1234
|
-
binding.error = '';
|
|
1235
|
-
binding.stopReason = '';
|
|
1236
|
-
binding.state = interactiveRuntimeState(binding, 'idle');
|
|
1237
|
-
binding.updatedAt = new Date().toISOString();
|
|
1238
|
-
this.emitRuntime(binding);
|
|
1239
|
-
this.emitSession(binding);
|
|
1240
|
-
return { authenticated: true, methodId };
|
|
1241
1991
|
}
|
|
1242
1992
|
|
|
1243
1993
|
async logout(agentId) {
|
|
1244
1994
|
const binding = this.requireBinding(agentId);
|
|
1245
|
-
|
|
1246
|
-
|
|
1995
|
+
this.requireOpenBinding(binding);
|
|
1996
|
+
const mutation = this.beginSessionMutation(binding, 'logout');
|
|
1997
|
+
try {
|
|
1998
|
+
if (binding.initializeResponse?.agentCapabilities?.auth?.logout == null) {
|
|
1999
|
+
throw new Error(`${binding.provider} ACP Agent does not support logout`);
|
|
2000
|
+
}
|
|
2001
|
+
await withTimeout(binding.connection.logout({}), this.requestTimeoutMs, 'ACP logout');
|
|
2002
|
+
this.requireOpenBinding(binding);
|
|
2003
|
+
binding.error = '';
|
|
2004
|
+
binding.stopReason = '';
|
|
2005
|
+
binding.updatedAt = new Date().toISOString();
|
|
2006
|
+
this.emitRuntime(binding);
|
|
2007
|
+
this.emitSession(binding);
|
|
2008
|
+
return { loggedOut: true };
|
|
2009
|
+
} finally {
|
|
2010
|
+
this.endSessionMutation(binding, mutation);
|
|
1247
2011
|
}
|
|
1248
|
-
await withTimeout(binding.connection.logout({}), this.requestTimeoutMs, 'ACP logout');
|
|
1249
|
-
binding.error = '';
|
|
1250
|
-
binding.stopReason = '';
|
|
1251
|
-
binding.updatedAt = new Date().toISOString();
|
|
1252
|
-
this.emitRuntime(binding);
|
|
1253
|
-
this.emitSession(binding);
|
|
1254
|
-
return { loggedOut: true };
|
|
1255
2012
|
}
|
|
1256
2013
|
|
|
1257
2014
|
terminalAuthenticationLaunch(binding, method) {
|
|
@@ -1271,7 +2028,9 @@ class AcpRuntime extends EventEmitter {
|
|
|
1271
2028
|
};
|
|
1272
2029
|
}
|
|
1273
2030
|
|
|
1274
|
-
async startTerminalAuthentication(binding, method) {
|
|
2031
|
+
async startTerminalAuthentication(binding, method, mutation) {
|
|
2032
|
+
this.requireOpenBinding(binding);
|
|
2033
|
+
if (binding.sessionMutation !== mutation) throw new Error('ACP authentication reservation is no longer active');
|
|
1275
2034
|
if (binding.authTerminal?.state === 'running') throw new Error('ACP terminal authentication is already running');
|
|
1276
2035
|
const launch = this.terminalAuthenticationLaunch(binding, method);
|
|
1277
2036
|
const created = await this.clientTerminals.create(binding, {
|
|
@@ -1282,6 +2041,19 @@ class AcpRuntime extends EventEmitter {
|
|
|
1282
2041
|
env: Object.entries(launch.env).map(([name, value]) => ({ name, value: String(value) })),
|
|
1283
2042
|
outputByteLimit: 2 * 1024 * 1024,
|
|
1284
2043
|
});
|
|
2044
|
+
try {
|
|
2045
|
+
this.requireOpenBinding(binding);
|
|
2046
|
+
} catch (error) {
|
|
2047
|
+
try {
|
|
2048
|
+
this.clientTerminals.release(binding, {
|
|
2049
|
+
sessionId: binding.sessionId,
|
|
2050
|
+
terminalId: created.terminalId,
|
|
2051
|
+
});
|
|
2052
|
+
} catch {
|
|
2053
|
+
// Binding teardown may already have removed the terminal.
|
|
2054
|
+
}
|
|
2055
|
+
throw error;
|
|
2056
|
+
}
|
|
1285
2057
|
binding.authTerminal = {
|
|
1286
2058
|
terminalId: created.terminalId,
|
|
1287
2059
|
methodId: String(method.id || ''),
|
|
@@ -1296,10 +2068,11 @@ class AcpRuntime extends EventEmitter {
|
|
|
1296
2068
|
sessionId: binding.sessionId,
|
|
1297
2069
|
terminalId: created.terminalId,
|
|
1298
2070
|
})).then(async exit => {
|
|
1299
|
-
if (this.
|
|
2071
|
+
if (!this.isOpenBinding(binding) || binding.sessionMutation !== mutation) return;
|
|
1300
2072
|
if (exit.exitCode !== 0) {
|
|
1301
2073
|
binding.authTerminal.state = 'failed';
|
|
1302
2074
|
binding.authTerminal.error = `Sign-in command exited ${exit.exitCode ?? exit.signal ?? ''}`.trim();
|
|
2075
|
+
this.endSessionMutation(binding, mutation);
|
|
1303
2076
|
binding.updatedAt = new Date().toISOString();
|
|
1304
2077
|
this.emitRuntime(binding);
|
|
1305
2078
|
this.emitSession(binding);
|
|
@@ -1308,18 +2081,20 @@ class AcpRuntime extends EventEmitter {
|
|
|
1308
2081
|
binding.authTerminal.state = 'completed';
|
|
1309
2082
|
binding.updatedAt = new Date().toISOString();
|
|
1310
2083
|
this.emitSession(binding);
|
|
1311
|
-
await this.restartAgentConnection(binding.agentId).catch(error => {
|
|
2084
|
+
await this.restartAgentConnection(binding.agentId, mutation).catch(error => {
|
|
1312
2085
|
const current = this.bindings.get(binding.agentId);
|
|
1313
|
-
if (
|
|
2086
|
+
if (current !== binding || current.exited) return;
|
|
2087
|
+
this.endSessionMutation(current, mutation);
|
|
1314
2088
|
current.state = 'error';
|
|
1315
2089
|
current.error = acpErrorMessage(error);
|
|
1316
2090
|
current.updatedAt = new Date().toISOString();
|
|
1317
2091
|
this.emitRuntime(current);
|
|
1318
2092
|
});
|
|
1319
2093
|
}).catch(error => {
|
|
1320
|
-
if (this.
|
|
2094
|
+
if (!this.isOpenBinding(binding) || binding.sessionMutation !== mutation) return;
|
|
1321
2095
|
binding.authTerminal.state = 'failed';
|
|
1322
2096
|
binding.authTerminal.error = acpErrorMessage(error);
|
|
2097
|
+
this.endSessionMutation(binding, mutation);
|
|
1323
2098
|
binding.updatedAt = new Date().toISOString();
|
|
1324
2099
|
this.emitRuntime(binding);
|
|
1325
2100
|
this.emitSession(binding);
|
|
@@ -1327,8 +2102,15 @@ class AcpRuntime extends EventEmitter {
|
|
|
1327
2102
|
return { authenticated: false, methodId: method.id, terminalId: created.terminalId };
|
|
1328
2103
|
}
|
|
1329
2104
|
|
|
1330
|
-
async restartAgentConnection(agentId) {
|
|
2105
|
+
async restartAgentConnection(agentId, mutation = null) {
|
|
1331
2106
|
const binding = this.requireBinding(agentId);
|
|
2107
|
+
this.requireOpenBinding(binding);
|
|
2108
|
+
if (binding.activeTurn || (binding.sessionMutation && binding.sessionMutation !== mutation)) {
|
|
2109
|
+
throw new Error(`ACP Agent is not ready for restart (${binding.state})`);
|
|
2110
|
+
}
|
|
2111
|
+
if (mutation && binding.sessionMutation !== mutation) {
|
|
2112
|
+
throw new Error('ACP restart reservation is no longer active');
|
|
2113
|
+
}
|
|
1332
2114
|
const revisionBase = Number(binding.sessionState?.revision || 0);
|
|
1333
2115
|
const options = {
|
|
1334
2116
|
...binding.restartOptions,
|
|
@@ -1341,60 +2123,109 @@ class AcpRuntime extends EventEmitter {
|
|
|
1341
2123
|
...(revisionBase > 0 ? { revisionBase } : {}),
|
|
1342
2124
|
...(binding.sessionId ? { historyMode: 'checkpoint' } : {}),
|
|
1343
2125
|
};
|
|
1344
|
-
if (binding.sessionState && !binding.
|
|
2126
|
+
if (binding.sessionState && !binding.activeTurn) {
|
|
1345
2127
|
await this.writeCheckpoint(binding, { exact: true });
|
|
1346
2128
|
}
|
|
1347
|
-
this.
|
|
2129
|
+
this.requireOpenBinding(binding);
|
|
2130
|
+
await this.unregisterAgentAndWait(agentId, binding);
|
|
1348
2131
|
return this.prepareAgent(options);
|
|
1349
2132
|
}
|
|
1350
2133
|
|
|
1351
2134
|
async forkSession(agentId, options = {}) {
|
|
2135
|
+
return this.runWithForkReservation(agentId, options, async binding => {
|
|
2136
|
+
const sessionOptions = acpSessionRequestOptions({
|
|
2137
|
+
additionalDirectories: options.additionalDirectories ?? binding.sessionRequestOptions.additionalDirectories,
|
|
2138
|
+
mcpServers: options.mcpServers ?? binding.sessionRequestOptions.mcpServers,
|
|
2139
|
+
}, options.cwd || binding.cwd);
|
|
2140
|
+
const response = await withTimeout(binding.connection.unstable_forkSession({
|
|
2141
|
+
sessionId: options.sessionId || binding.sessionId,
|
|
2142
|
+
...sessionOptions,
|
|
2143
|
+
}), this.sessionSetupTimeoutMs, 'ACP session/fork');
|
|
2144
|
+
this.requireOpenBinding(binding);
|
|
2145
|
+
const sessionId = String(response?.sessionId || '').trim();
|
|
2146
|
+
if (!isSafeProviderSessionId(sessionId) || sessionId === binding.sessionId) {
|
|
2147
|
+
throw new Error('ACP session/fork returned an invalid new session id');
|
|
2148
|
+
}
|
|
2149
|
+
return { ...response, sessionId };
|
|
2150
|
+
});
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
async runWithForkReservation(agentId, options = {}, operation) {
|
|
1352
2154
|
const binding = this.requireBinding(agentId);
|
|
1353
|
-
const
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
2155
|
+
const mutation = this.beginSessionMutation(binding, 'fork');
|
|
2156
|
+
try {
|
|
2157
|
+
const capabilities = binding.initializeResponse?.agentCapabilities?.sessionCapabilities;
|
|
2158
|
+
if (!capabilities?.fork) throw new Error(`${binding.provider} ACP Agent does not support session/fork`);
|
|
2159
|
+
if (options.requireLoad === true && !binding.initializeResponse?.agentCapabilities?.loadSession) {
|
|
2160
|
+
throw new Error(`${binding.provider} ACP Agent cannot load a forked conversation`);
|
|
2161
|
+
}
|
|
2162
|
+
if (
|
|
2163
|
+
Number.isFinite(options.expectedRevision)
|
|
2164
|
+
&& Number(binding.sessionState?.revision || 0) !== Math.floor(options.expectedRevision)
|
|
2165
|
+
) {
|
|
2166
|
+
throw new Error('ACP conversation changed before it could be forked. Try again from the latest answer.');
|
|
2167
|
+
}
|
|
2168
|
+
if (typeof operation !== 'function') throw new Error('ACP fork operation is required');
|
|
2169
|
+
return await operation(binding);
|
|
2170
|
+
} finally {
|
|
2171
|
+
this.endSessionMutation(binding, mutation);
|
|
2172
|
+
}
|
|
1363
2173
|
}
|
|
1364
2174
|
|
|
1365
2175
|
async deleteSession(agentId, sessionId) {
|
|
1366
2176
|
const binding = this.requireBinding(agentId);
|
|
1367
|
-
const
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
2177
|
+
const mutation = this.beginSessionMutation(binding, 'session deletion');
|
|
2178
|
+
try {
|
|
2179
|
+
const capabilities = binding.initializeResponse?.agentCapabilities?.sessionCapabilities;
|
|
2180
|
+
if (!capabilities?.delete) throw new Error(`${binding.provider} ACP Agent does not support session/delete`);
|
|
2181
|
+
await withTimeout(
|
|
2182
|
+
binding.connection.deleteSession({ sessionId: String(sessionId || '') }),
|
|
2183
|
+
this.requestTimeoutMs,
|
|
2184
|
+
'ACP session/delete'
|
|
2185
|
+
);
|
|
2186
|
+
this.requireOpenBinding(binding);
|
|
2187
|
+
return { deleted: true, sessionId: String(sessionId || '') };
|
|
2188
|
+
} finally {
|
|
2189
|
+
this.endSessionMutation(binding, mutation);
|
|
2190
|
+
}
|
|
1375
2191
|
}
|
|
1376
2192
|
|
|
1377
2193
|
async closeSession(agentId) {
|
|
1378
2194
|
const binding = this.requireBinding(agentId);
|
|
1379
|
-
const
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
2195
|
+
const mutation = this.beginSessionMutation(binding, 'session close');
|
|
2196
|
+
try {
|
|
2197
|
+
const capabilities = binding.initializeResponse?.agentCapabilities?.sessionCapabilities;
|
|
2198
|
+
if (!capabilities?.close) throw new Error(`${binding.provider} ACP Agent does not support session/close`);
|
|
2199
|
+
await withTimeout(
|
|
2200
|
+
binding.connection.closeSession({ sessionId: binding.sessionId }),
|
|
2201
|
+
this.requestTimeoutMs,
|
|
2202
|
+
'ACP session/close'
|
|
2203
|
+
);
|
|
2204
|
+
this.requireOpenBinding(binding);
|
|
2205
|
+
binding.state = 'closed';
|
|
2206
|
+
this.emitRuntime(binding);
|
|
2207
|
+
return { closed: true, sessionId: binding.sessionId };
|
|
2208
|
+
} finally {
|
|
2209
|
+
this.endSessionMutation(binding, mutation);
|
|
2210
|
+
}
|
|
1389
2211
|
}
|
|
1390
2212
|
|
|
1391
2213
|
async setSessionMode(agentId, modeId) {
|
|
1392
2214
|
const binding = this.requireBinding(agentId);
|
|
2215
|
+
this.requireConfigMutationReady(binding);
|
|
2216
|
+
return this.enqueueSessionConfigMutation(binding, () => (
|
|
2217
|
+
this.setSessionModeNow(binding, modeId)
|
|
2218
|
+
));
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
async setSessionModeNow(binding, modeId) {
|
|
2222
|
+
this.requireOpenBinding(binding);
|
|
1393
2223
|
await withTimeout(
|
|
1394
2224
|
binding.connection.setSessionMode({ sessionId: binding.sessionId, modeId: String(modeId || '') }),
|
|
1395
2225
|
this.requestTimeoutMs,
|
|
1396
2226
|
'ACP session/set_mode'
|
|
1397
2227
|
);
|
|
2228
|
+
this.requireOpenBinding(binding);
|
|
1398
2229
|
binding.sessionState.currentModeId = String(modeId || '');
|
|
1399
2230
|
if (binding.modes) binding.modes = { ...binding.modes, currentModeId: binding.sessionState.currentModeId };
|
|
1400
2231
|
this.scheduleCheckpoint(binding, { exact: true });
|
|
@@ -1404,12 +2235,14 @@ class AcpRuntime extends EventEmitter {
|
|
|
1404
2235
|
|
|
1405
2236
|
async setSessionConfigOption(agentId, configId, value) {
|
|
1406
2237
|
const binding = this.requireBinding(agentId);
|
|
2238
|
+
this.requireConfigMutationReady(binding);
|
|
1407
2239
|
return this.enqueueSessionConfigMutation(binding, () => (
|
|
1408
2240
|
this.setSessionConfigOptionNow(binding, configId, value)
|
|
1409
2241
|
));
|
|
1410
2242
|
}
|
|
1411
2243
|
|
|
1412
2244
|
async setSessionConfigOptionNow(binding, configId, value) {
|
|
2245
|
+
this.requireOpenBinding(binding);
|
|
1413
2246
|
const option = binding.configOptions?.find(candidate => candidate.id === String(configId || ''));
|
|
1414
2247
|
if (
|
|
1415
2248
|
binding.provider === 'codex'
|
|
@@ -1437,17 +2270,52 @@ class AcpRuntime extends EventEmitter {
|
|
|
1437
2270
|
|
|
1438
2271
|
async setSessionConfigOptions(agentId, changes) {
|
|
1439
2272
|
const binding = this.requireBinding(agentId);
|
|
2273
|
+
this.requireConfigMutationReady(binding);
|
|
1440
2274
|
return this.enqueueSessionConfigMutation(binding, () => (
|
|
1441
2275
|
this.setSessionConfigOptionsNow(binding, changes)
|
|
1442
2276
|
));
|
|
1443
2277
|
}
|
|
1444
2278
|
|
|
1445
2279
|
async setSessionConfigOptionsNow(binding, changes) {
|
|
2280
|
+
this.requireOpenBinding(binding);
|
|
1446
2281
|
const normalized = Array.isArray(changes)
|
|
1447
2282
|
? changes.filter(change => change && typeof change.configId === 'string' && Object.prototype.hasOwnProperty.call(change, 'value'))
|
|
1448
2283
|
: [];
|
|
1449
2284
|
if (normalized.length === 0) throw new Error('ACP config options are required');
|
|
1450
2285
|
|
|
2286
|
+
const originalValues = new Map();
|
|
2287
|
+
for (const change of normalized) {
|
|
2288
|
+
if (originalValues.has(change.configId)) continue;
|
|
2289
|
+
const option = binding.configOptions?.find(candidate => candidate.id === change.configId);
|
|
2290
|
+
if (option && Object.prototype.hasOwnProperty.call(option, 'currentValue')) {
|
|
2291
|
+
originalValues.set(change.configId, option.currentValue);
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
try {
|
|
2295
|
+
return await this.applySessionConfigOptionsNow(binding, normalized);
|
|
2296
|
+
} catch (error) {
|
|
2297
|
+
const rollbackErrors = [];
|
|
2298
|
+
for (const [configId, value] of [...originalValues.entries()].reverse()) {
|
|
2299
|
+
const current = binding.configOptions?.find(candidate => candidate.id === configId);
|
|
2300
|
+
if (current?.currentValue === value) continue;
|
|
2301
|
+
try {
|
|
2302
|
+
await this.setSessionConfigOptionNow(binding, configId, value);
|
|
2303
|
+
} catch (rollbackError) {
|
|
2304
|
+
rollbackErrors.push(rollbackError);
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
if (rollbackErrors.length > 0 && error && typeof error === 'object') {
|
|
2308
|
+
Object.defineProperty(error, 'configRollbackError', {
|
|
2309
|
+
value: new AggregateError(rollbackErrors, 'ACP config rollback failed'),
|
|
2310
|
+
enumerable: false,
|
|
2311
|
+
configurable: true,
|
|
2312
|
+
});
|
|
2313
|
+
}
|
|
2314
|
+
throw error;
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
async applySessionConfigOptionsNow(binding, normalized) {
|
|
1451
2319
|
const configById = new Map((binding.configOptions || []).map(option => [option.id, option]));
|
|
1452
2320
|
const modelChange = normalized.find(change => {
|
|
1453
2321
|
const option = configById.get(change.configId);
|
|
@@ -1497,6 +2365,7 @@ class AcpRuntime extends EventEmitter {
|
|
|
1497
2365
|
}
|
|
1498
2366
|
|
|
1499
2367
|
async refreshCodexSessionModel(binding, model, effort) {
|
|
2368
|
+
this.requireOpenBinding(binding);
|
|
1500
2369
|
await withTimeout(
|
|
1501
2370
|
binding.connection.request(CODEX_SET_SESSION_MODEL_METHOD, {
|
|
1502
2371
|
sessionId: binding.sessionId,
|
|
@@ -1505,9 +2374,11 @@ class AcpRuntime extends EventEmitter {
|
|
|
1505
2374
|
this.requestTimeoutMs,
|
|
1506
2375
|
'Codex ACP session/set_model capability refresh'
|
|
1507
2376
|
);
|
|
2377
|
+
this.requireOpenBinding(binding);
|
|
1508
2378
|
}
|
|
1509
2379
|
|
|
1510
2380
|
async applySessionConfigOption(binding, configId, value, options = {}) {
|
|
2381
|
+
this.requireOpenBinding(binding);
|
|
1511
2382
|
const normalizedConfigId = String(configId || '');
|
|
1512
2383
|
const currentOption = binding.configOptions?.find(candidate => candidate.id === normalizedConfigId);
|
|
1513
2384
|
if (options.force !== true && currentOption?.currentValue === value) {
|
|
@@ -1524,6 +2395,7 @@ class AcpRuntime extends EventEmitter {
|
|
|
1524
2395
|
this.requestTimeoutMs,
|
|
1525
2396
|
'ACP session/set_config_option'
|
|
1526
2397
|
);
|
|
2398
|
+
this.requireOpenBinding(binding);
|
|
1527
2399
|
binding.configOptions = response?.configOptions || binding.configOptions;
|
|
1528
2400
|
const confirmed = binding.configOptions?.find(candidate => candidate.id === normalizedConfigId);
|
|
1529
2401
|
if (confirmed?.currentValue === request.value) break;
|
|
@@ -1545,6 +2417,10 @@ class AcpRuntime extends EventEmitter {
|
|
|
1545
2417
|
errorKind: binding.error ? acpErrorKind(binding.error) : '',
|
|
1546
2418
|
stopReason: binding.stopReason,
|
|
1547
2419
|
supportsSteer: binding.supportsSteer === true,
|
|
2420
|
+
supportsFork: Boolean(
|
|
2421
|
+
binding.initializeResponse?.agentCapabilities?.sessionCapabilities?.fork != null
|
|
2422
|
+
&& binding.initializeResponse?.agentCapabilities?.loadSession
|
|
2423
|
+
),
|
|
1548
2424
|
protocolVersion: binding.initializeResponse?.protocolVersion || null,
|
|
1549
2425
|
agentInfo: binding.initializeResponse?.agentInfo || null,
|
|
1550
2426
|
capabilities: binding.initializeResponse?.agentCapabilities || {},
|
|
@@ -1625,33 +2501,82 @@ class AcpRuntime extends EventEmitter {
|
|
|
1625
2501
|
|
|
1626
2502
|
async decidePatch(agentId, toolCallId, requestedPath, decision) {
|
|
1627
2503
|
const binding = this.requireBinding(agentId);
|
|
1628
|
-
|
|
2504
|
+
this.requireOpenBinding(binding);
|
|
2505
|
+
if (
|
|
2506
|
+
binding.sessionMutation
|
|
2507
|
+
|| binding.configMutationTail
|
|
2508
|
+
|| binding.activeTurn
|
|
2509
|
+
|| !['idle', 'error'].includes(binding.state)
|
|
2510
|
+
) {
|
|
2511
|
+
throw new Error('Wait for the Agent to finish before deciding a patch');
|
|
2512
|
+
}
|
|
1629
2513
|
const entry = binding.sessionState?.toolEntries.get(String(toolCallId || ''));
|
|
1630
2514
|
if (entry && binding.sessionState.isInternalEntry(entry)) throw new Error('ACP tool call not found');
|
|
1631
2515
|
if (!entry) throw new Error('ACP tool call not found');
|
|
1632
2516
|
const normalizedDecision = String(decision || '').trim().toLowerCase();
|
|
1633
2517
|
if (!['keep', 'revert'].includes(normalizedDecision)) throw new Error('ACP patch decision is invalid');
|
|
1634
2518
|
const key = `${String(toolCallId || '')}\n${String(requestedPath || '')}`;
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
if (
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
2519
|
+
const expectedAction = normalizedDecision === 'keep' ? 'kept' : 'reverted';
|
|
2520
|
+
const existing = binding.patchDecisionInFlight.get(key);
|
|
2521
|
+
if (existing) {
|
|
2522
|
+
if (existing.decision !== normalizedDecision) {
|
|
2523
|
+
throw new Error('ACP patch file already has a different decision in progress');
|
|
2524
|
+
}
|
|
2525
|
+
return existing.promise;
|
|
2526
|
+
}
|
|
2527
|
+
const committed = binding.patchDecisions.get(key);
|
|
2528
|
+
if (committed) {
|
|
2529
|
+
if (committed !== expectedAction) throw new Error('ACP patch file already has a different decision');
|
|
2530
|
+
const operation = (async () => {
|
|
2531
|
+
await this.writeCheckpoint(binding, { exact: true });
|
|
2532
|
+
return {
|
|
2533
|
+
action: committed,
|
|
2534
|
+
path: String(requestedPath || ''),
|
|
2535
|
+
toolCallId: String(toolCallId || ''),
|
|
2536
|
+
};
|
|
2537
|
+
})();
|
|
2538
|
+
const pending = { decision: normalizedDecision, promise: operation };
|
|
2539
|
+
binding.patchDecisionInFlight.set(key, pending);
|
|
2540
|
+
try {
|
|
2541
|
+
return await operation;
|
|
2542
|
+
} finally {
|
|
2543
|
+
if (binding.patchDecisionInFlight.get(key) === pending) {
|
|
2544
|
+
binding.patchDecisionInFlight.delete(key);
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
const operation = (async () => {
|
|
2549
|
+
let result = { action: 'kept', path: String(requestedPath || '') };
|
|
2550
|
+
if (normalizedDecision === 'revert') {
|
|
2551
|
+
result = await rejectPatch({ entry, root: binding.cwd, requestedPath });
|
|
2552
|
+
this.requireOpenBinding(binding);
|
|
2553
|
+
}
|
|
2554
|
+
binding.patchDecisions.set(key, result.action);
|
|
2555
|
+
entry._meta = { ...(entry._meta || {}) };
|
|
2556
|
+
entry._meta.farming_patch_decisions = {
|
|
2557
|
+
...(entry._meta.farming_patch_decisions || {}),
|
|
2558
|
+
[String(requestedPath || '')]: result.action,
|
|
2559
|
+
};
|
|
2560
|
+
binding.sessionState?.touchEntry(entry);
|
|
2561
|
+
binding.updatedAt = new Date().toISOString();
|
|
2562
|
+
await this.writeCheckpoint(binding, { exact: true });
|
|
2563
|
+
this.emitSession(binding);
|
|
2564
|
+
return { ...result, toolCallId: String(toolCallId || '') };
|
|
2565
|
+
})();
|
|
2566
|
+
const pending = { decision: normalizedDecision, promise: operation };
|
|
2567
|
+
binding.patchDecisionInFlight.set(key, pending);
|
|
2568
|
+
try {
|
|
2569
|
+
return await operation;
|
|
2570
|
+
} finally {
|
|
2571
|
+
if (binding.patchDecisionInFlight.get(key) === pending) {
|
|
2572
|
+
binding.patchDecisionInFlight.delete(key);
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
1651
2575
|
}
|
|
1652
2576
|
|
|
1653
2577
|
killTerminal(agentId, terminalId) {
|
|
1654
2578
|
const binding = this.requireBinding(agentId);
|
|
2579
|
+
this.requireOpenBinding(binding);
|
|
1655
2580
|
this.clientTerminals.kill(binding, {
|
|
1656
2581
|
sessionId: binding.sessionId,
|
|
1657
2582
|
terminalId: String(terminalId || ''),
|
|
@@ -1661,6 +2586,7 @@ class AcpRuntime extends EventEmitter {
|
|
|
1661
2586
|
|
|
1662
2587
|
inputTerminal(agentId, terminalId, input) {
|
|
1663
2588
|
const binding = this.requireBinding(agentId);
|
|
2589
|
+
this.requireOpenBinding(binding);
|
|
1664
2590
|
return this.clientTerminals.input(binding, {
|
|
1665
2591
|
sessionId: binding.sessionId,
|
|
1666
2592
|
terminalId,
|
|
@@ -1670,6 +2596,7 @@ class AcpRuntime extends EventEmitter {
|
|
|
1670
2596
|
|
|
1671
2597
|
resizeTerminal(agentId, terminalId, cols, rows) {
|
|
1672
2598
|
const binding = this.requireBinding(agentId);
|
|
2599
|
+
this.requireOpenBinding(binding);
|
|
1673
2600
|
return this.clientTerminals.resize(binding, {
|
|
1674
2601
|
sessionId: binding.sessionId,
|
|
1675
2602
|
terminalId,
|
|
@@ -1692,17 +2619,26 @@ class AcpRuntime extends EventEmitter {
|
|
|
1692
2619
|
.some(request => String(request?.sessionId || '') === id);
|
|
1693
2620
|
const pendingElicitation = [...binding.pendingElicitations.values()]
|
|
1694
2621
|
.some(request => String(request?.sessionId || '') === id);
|
|
2622
|
+
const control = this.ensureSubagentControl(binding, id);
|
|
1695
2623
|
const active = ['pending', 'in_progress', 'in-progress', 'running'].includes(status);
|
|
1696
2624
|
const failed = ['failed', 'error'].includes(status);
|
|
1697
2625
|
const stateName = pendingPermission
|
|
1698
2626
|
? 'waiting-for-permission'
|
|
1699
2627
|
: pendingElicitation
|
|
1700
2628
|
? 'waiting-for-input'
|
|
1701
|
-
:
|
|
1702
|
-
? '
|
|
1703
|
-
:
|
|
2629
|
+
: control.phase === 'cancelling'
|
|
2630
|
+
? 'interrupting'
|
|
2631
|
+
: control.phase === 'error'
|
|
1704
2632
|
? 'error'
|
|
1705
|
-
: '
|
|
2633
|
+
: ['cancelled', 'completed'].includes(control.phase)
|
|
2634
|
+
? 'idle'
|
|
2635
|
+
: active
|
|
2636
|
+
? 'working'
|
|
2637
|
+
: failed
|
|
2638
|
+
? 'error'
|
|
2639
|
+
: 'idle';
|
|
2640
|
+
const controlFailed = control.phase === 'error';
|
|
2641
|
+
const cancelled = control.phase === 'cancelled';
|
|
1706
2642
|
return {
|
|
1707
2643
|
version: 2,
|
|
1708
2644
|
protocol: 'acp',
|
|
@@ -1713,13 +2649,17 @@ class AcpRuntime extends EventEmitter {
|
|
|
1713
2649
|
updatedAt: binding.updatedAt,
|
|
1714
2650
|
truncated: state.truncated === true,
|
|
1715
2651
|
state: stateName,
|
|
1716
|
-
error: failed ? String(parentTool?.title || 'Subagent failed') : '',
|
|
1717
|
-
errorKind: failed ? 'agent' : '',
|
|
1718
|
-
stopReason: failed ? 'error' : active ? '' : 'end_turn',
|
|
2652
|
+
error: controlFailed ? control.error : failed ? String(parentTool?.title || 'Subagent failed') : '',
|
|
2653
|
+
errorKind: controlFailed || failed ? 'agent' : '',
|
|
2654
|
+
stopReason: controlFailed || failed ? 'error' : cancelled ? 'cancelled' : active ? '' : 'end_turn',
|
|
1719
2655
|
...state.transcriptSlice(options),
|
|
1720
2656
|
};
|
|
1721
2657
|
}
|
|
1722
2658
|
|
|
2659
|
+
hasBinding(agentId) {
|
|
2660
|
+
return this.bindings.has(agentId);
|
|
2661
|
+
}
|
|
2662
|
+
|
|
1723
2663
|
requireBinding(agentId) {
|
|
1724
2664
|
const binding = this.bindings.get(agentId);
|
|
1725
2665
|
if (!binding) throw new Error('ACP Agent is not registered');
|
|
@@ -1727,6 +2667,7 @@ class AcpRuntime extends EventEmitter {
|
|
|
1727
2667
|
}
|
|
1728
2668
|
|
|
1729
2669
|
emitRuntime(binding) {
|
|
2670
|
+
if (!this.isCurrentBinding(binding)) return false;
|
|
1730
2671
|
this.emit('agent-runtime', {
|
|
1731
2672
|
agentId: binding.agentId,
|
|
1732
2673
|
provider: binding.provider,
|
|
@@ -1736,6 +2677,10 @@ class AcpRuntime extends EventEmitter {
|
|
|
1736
2677
|
errorKind: binding.error ? acpErrorKind(binding.error) : '',
|
|
1737
2678
|
stopReason: binding.stopReason,
|
|
1738
2679
|
supportsSteer: binding.supportsSteer === true,
|
|
2680
|
+
supportsFork: Boolean(
|
|
2681
|
+
binding.initializeResponse?.agentCapabilities?.sessionCapabilities?.fork != null
|
|
2682
|
+
&& binding.initializeResponse?.agentCapabilities?.loadSession
|
|
2683
|
+
),
|
|
1739
2684
|
pendingPermission: binding.pendingPermissions.values().next().value || null,
|
|
1740
2685
|
pendingPermissions: [...binding.pendingPermissions.values()],
|
|
1741
2686
|
pendingElicitation: binding.pendingElicitations.values().next().value || null,
|
|
@@ -1743,23 +2688,40 @@ class AcpRuntime extends EventEmitter {
|
|
|
1743
2688
|
activeElicitations: [...binding.activeElicitations.values()],
|
|
1744
2689
|
updatedAt: binding.updatedAt,
|
|
1745
2690
|
});
|
|
2691
|
+
return true;
|
|
1746
2692
|
}
|
|
1747
2693
|
|
|
1748
2694
|
emitSession(binding) {
|
|
2695
|
+
if (!this.isCurrentBinding(binding)) return false;
|
|
1749
2696
|
this.emit('session', {
|
|
1750
2697
|
agentId: binding.agentId,
|
|
1751
2698
|
updatedAt: binding.updatedAt,
|
|
1752
2699
|
revision: binding.sessionState?.revision || 0,
|
|
1753
2700
|
});
|
|
2701
|
+
return true;
|
|
1754
2702
|
}
|
|
1755
2703
|
|
|
1756
2704
|
handleExit(binding, error) {
|
|
1757
|
-
if (this.
|
|
2705
|
+
if (!this.isCurrentBinding(binding) || binding.exited) return;
|
|
2706
|
+
binding.exited = true;
|
|
2707
|
+
const turn = binding.activeTurn;
|
|
2708
|
+
const promptWasActive = Boolean(turn && turn.phase !== 'admitting');
|
|
2709
|
+
binding.activeTurn = null;
|
|
1758
2710
|
if (error) {
|
|
1759
2711
|
binding.state = 'error';
|
|
1760
2712
|
binding.error = acpErrorMessage(error);
|
|
2713
|
+
binding.stopReason = 'error';
|
|
1761
2714
|
} else if (binding.state !== 'error') {
|
|
1762
2715
|
binding.state = 'stopped';
|
|
2716
|
+
binding.stopReason = promptWasActive ? 'stopped' : binding.stopReason;
|
|
2717
|
+
}
|
|
2718
|
+
if (promptWasActive && binding.sessionState) {
|
|
2719
|
+
if (error) binding.sessionState.recordError(binding.error, acpErrorKind(error));
|
|
2720
|
+
binding.sessionState.completePrompt(binding.stopReason);
|
|
2721
|
+
}
|
|
2722
|
+
if (turn) {
|
|
2723
|
+
turn.phase = 'completed';
|
|
2724
|
+
turn.resolveCompletion({ status: 'connection-closed', error: error || null });
|
|
1763
2725
|
}
|
|
1764
2726
|
for (const resolve of binding.permissionResolvers.values()) {
|
|
1765
2727
|
resolve({ outcome: { outcome: 'cancelled' } });
|
|
@@ -1772,14 +2734,23 @@ class AcpRuntime extends EventEmitter {
|
|
|
1772
2734
|
binding.activeElicitations.clear();
|
|
1773
2735
|
binding.interactionOrigins.clear();
|
|
1774
2736
|
binding.subagentStates.clear();
|
|
2737
|
+
binding.subagentControls.clear();
|
|
1775
2738
|
this.clientTerminals.cleanupAgent(binding.agentId);
|
|
2739
|
+
if (promptWasActive) this.emitSession(binding);
|
|
1776
2740
|
this.emitRuntime(binding);
|
|
1777
2741
|
}
|
|
1778
2742
|
|
|
1779
|
-
detachAgentBinding(binding) {
|
|
2743
|
+
detachAgentBinding(binding, options = {}) {
|
|
1780
2744
|
if (!binding || this.bindings.get(binding.agentId) !== binding) return false;
|
|
1781
|
-
if (binding.sessionState && !binding.
|
|
1782
|
-
this.bindings.delete(binding.agentId);
|
|
2745
|
+
if (binding.sessionState && !binding.activeTurn) this.scheduleCheckpoint(binding, { exact: true });
|
|
2746
|
+
if (options.retainForCleanup !== true) this.bindings.delete(binding.agentId);
|
|
2747
|
+
binding.exited = true;
|
|
2748
|
+
const turn = binding.activeTurn;
|
|
2749
|
+
binding.activeTurn = null;
|
|
2750
|
+
if (turn) {
|
|
2751
|
+
turn.phase = 'completed';
|
|
2752
|
+
turn.resolveCompletion({ status: 'binding-detached' });
|
|
2753
|
+
}
|
|
1783
2754
|
for (const resolve of binding.permissionResolvers.values()) resolve({ outcome: { outcome: 'cancelled' } });
|
|
1784
2755
|
for (const resolve of binding.elicitationResolvers.values()) resolve({ action: 'cancel' });
|
|
1785
2756
|
binding.permissionResolvers.clear();
|
|
@@ -1789,6 +2760,7 @@ class AcpRuntime extends EventEmitter {
|
|
|
1789
2760
|
binding.activeElicitations.clear();
|
|
1790
2761
|
binding.interactionOrigins.clear();
|
|
1791
2762
|
binding.subagentStates.clear();
|
|
2763
|
+
binding.subagentControls.clear();
|
|
1792
2764
|
this.clientTerminals.cleanupAgent(binding.agentId);
|
|
1793
2765
|
try {
|
|
1794
2766
|
binding.connection?.close();
|
|
@@ -1805,30 +2777,59 @@ class AcpRuntime extends EventEmitter {
|
|
|
1805
2777
|
return true;
|
|
1806
2778
|
}
|
|
1807
2779
|
|
|
1808
|
-
unregisterAgent(agentId) {
|
|
2780
|
+
unregisterAgent(agentId, expectedBinding = null) {
|
|
1809
2781
|
const binding = this.bindings.get(agentId);
|
|
2782
|
+
if (expectedBinding && binding !== expectedBinding) return;
|
|
1810
2783
|
if (!binding || !this.detachAgentBinding(binding)) return;
|
|
1811
2784
|
signalProcessTree(binding, 'SIGTERM');
|
|
1812
2785
|
}
|
|
1813
2786
|
|
|
1814
|
-
async unregisterAgentAndWait(agentId) {
|
|
2787
|
+
async unregisterAgentAndWait(agentId, expectedBinding = null) {
|
|
1815
2788
|
const binding = this.bindings.get(agentId);
|
|
1816
|
-
if (
|
|
1817
|
-
if (
|
|
1818
|
-
|
|
1819
|
-
if (
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
2789
|
+
if (expectedBinding && binding !== expectedBinding) return false;
|
|
2790
|
+
if (!binding || !this.detachAgentBinding(binding, { retainForCleanup: true })) return false;
|
|
2791
|
+
await stopBindingProcessAndWait(binding);
|
|
2792
|
+
if (this.bindings.get(agentId) === binding) this.bindings.delete(agentId);
|
|
2793
|
+
return true;
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
dispose() {
|
|
2797
|
+
if (this.disposed) return Promise.resolve();
|
|
2798
|
+
if (this.disposePromise) return this.disposePromise;
|
|
2799
|
+
|
|
2800
|
+
this.disposing = true;
|
|
2801
|
+
const disposePromise = this.performDispose();
|
|
2802
|
+
this.disposePromise = disposePromise;
|
|
2803
|
+
void disposePromise.finally(() => {
|
|
2804
|
+
if (this.disposePromise === disposePromise) this.disposePromise = null;
|
|
2805
|
+
if (!this.disposed) this.disposing = false;
|
|
2806
|
+
}).catch(() => {});
|
|
2807
|
+
return disposePromise;
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
resumeAfterDisposeAbort() {
|
|
2811
|
+
this.disposed = false;
|
|
2812
|
+
this.disposing = false;
|
|
1823
2813
|
}
|
|
1824
2814
|
|
|
1825
|
-
async
|
|
2815
|
+
async performDispose() {
|
|
1826
2816
|
for (const binding of this.bindings.values()) {
|
|
1827
|
-
if (binding.sessionState && !binding.
|
|
2817
|
+
if (binding.sessionState && !binding.activeTurn) this.scheduleCheckpoint(binding, { exact: true });
|
|
1828
2818
|
}
|
|
1829
2819
|
if (this.checkpointStore) await this.checkpointStore.flush();
|
|
1830
|
-
|
|
2820
|
+
const failures = [];
|
|
2821
|
+
for (const agentId of [...this.bindings.keys()]) {
|
|
2822
|
+
try {
|
|
2823
|
+
await this.unregisterAgentAndWait(agentId);
|
|
2824
|
+
} catch (error) {
|
|
2825
|
+
failures.push(error);
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
if (failures.length > 0) {
|
|
2829
|
+
throw new AggregateError(failures, 'One or more ACP Agent process trees did not exit');
|
|
2830
|
+
}
|
|
1831
2831
|
if (this.checkpointStore) await this.checkpointStore.dispose();
|
|
2832
|
+
this.disposed = true;
|
|
1832
2833
|
}
|
|
1833
2834
|
}
|
|
1834
2835
|
|
|
@@ -1839,8 +2840,10 @@ module.exports = {
|
|
|
1839
2840
|
acpErrorKind,
|
|
1840
2841
|
autoPermissionResponse,
|
|
1841
2842
|
codexAcpEnvironment,
|
|
2843
|
+
describeAcpProcessGroup,
|
|
1842
2844
|
promptContentForCapabilities,
|
|
1843
2845
|
resolveAcpLaunch,
|
|
2846
|
+
stopPersistedAcpProcessGroup,
|
|
1844
2847
|
supportsCodexSteer,
|
|
1845
2848
|
isCodexSteerUnavailableError,
|
|
1846
2849
|
CODEX_STEER_METHOD,
|