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/agent-manager.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const EventEmitter = require('events');
|
|
2
2
|
const { execFile } = require('child_process');
|
|
3
|
+
const crypto = require('crypto');
|
|
3
4
|
const fs = require('fs');
|
|
4
5
|
const os = require('os');
|
|
5
6
|
const path = require('path');
|
|
@@ -32,13 +33,17 @@ const {
|
|
|
32
33
|
applyProviderHomeEnvironment,
|
|
33
34
|
getProviderAdapter,
|
|
34
35
|
isFreshAcpSessionSource,
|
|
36
|
+
providerAcpForkMode,
|
|
35
37
|
providerCapabilities,
|
|
36
38
|
providerForProgram,
|
|
37
39
|
providerSupportsRuntime,
|
|
38
40
|
} = require('./provider-adapters');
|
|
39
41
|
const { deriveTerminalStatus } = require('./terminal-status');
|
|
40
42
|
const { JsonCliRuntime } = require('./json-cli-runtime');
|
|
41
|
-
const {
|
|
43
|
+
const {
|
|
44
|
+
AcpRuntime,
|
|
45
|
+
stopPersistedAcpProcessGroup,
|
|
46
|
+
} = require('./acp-runtime');
|
|
42
47
|
const { chatRuntimeForProvider, isChatMode } = require('./chat-runtime');
|
|
43
48
|
const { acpToolChanges, acpToolDetail, acpToolReviewChanges, acpTranscriptToolEntry } = require('./acp-transcript');
|
|
44
49
|
const {
|
|
@@ -62,6 +67,15 @@ const { inspectGitWorktree } = require('./git-worktree-info');
|
|
|
62
67
|
const { deserializeTerminalState } = require('./terminal-state-serialization');
|
|
63
68
|
const { compareNativePtyRuntimeEpochs } = require('./native-pty-controller-generation');
|
|
64
69
|
const { canonicalWorkspacePath } = require('./workspace-root-registry');
|
|
70
|
+
const {
|
|
71
|
+
TERMINAL_OPERATION_STATES,
|
|
72
|
+
activeLifecycleOperation,
|
|
73
|
+
beginLifecycleOperation,
|
|
74
|
+
latestLifecycleOperation,
|
|
75
|
+
lifecycleJournal,
|
|
76
|
+
setLifecycleOperationResult,
|
|
77
|
+
transitionLifecycleOperation,
|
|
78
|
+
} = require('./agent-lifecycle-journal');
|
|
65
79
|
|
|
66
80
|
const SESSION_OUTPUT_LIMIT = 10000;
|
|
67
81
|
const AGENT_USAGE_RATE_WINDOW_MS = 5 * 60 * 1000;
|
|
@@ -80,6 +94,7 @@ const UNCERTAIN_TERMINAL_STOP_TIMEOUT_MS = 5_000;
|
|
|
80
94
|
const TERMINAL_STOP_STATE_READ_TIMEOUT_MS = 1_000;
|
|
81
95
|
const TERMINAL_STOP_POLL_MS = 50;
|
|
82
96
|
const CODEX_TERMINAL_START_READY_TIMEOUT_MS = 30_000;
|
|
97
|
+
const WORKTREE_DELETE_START_DRAIN_TIMEOUT_MS = 30_000;
|
|
83
98
|
const CODEX_TERMINAL_START_READY_POLL_MS = 50;
|
|
84
99
|
const CODEX_TERMINAL_START_OUTPUT_LIMIT = 64 * 1024;
|
|
85
100
|
const SHELL_PROMPT_ENV_KEYS = [
|
|
@@ -92,8 +107,92 @@ const SHELL_PROMPT_ENV_KEYS = [
|
|
|
92
107
|
'RPS1',
|
|
93
108
|
'PROMPT_COMMAND',
|
|
94
109
|
];
|
|
110
|
+
const CREATE_ROLLBACK_FIELDS = [
|
|
111
|
+
'runtimeAgentId',
|
|
112
|
+
'command',
|
|
113
|
+
'forkCommand',
|
|
114
|
+
'cwd',
|
|
115
|
+
'projectWorkspace',
|
|
116
|
+
'mainWorkspace',
|
|
117
|
+
'source',
|
|
118
|
+
'providerHomePath',
|
|
119
|
+
'providerSessionTemporary',
|
|
120
|
+
'providerSessionSource',
|
|
121
|
+
'providerSessionResolvedAt',
|
|
122
|
+
'providerSessionTitle',
|
|
123
|
+
'providerSessionWorkspace',
|
|
124
|
+
'terminalInputReceived',
|
|
125
|
+
'structuredRuntimeProcess',
|
|
126
|
+
'legacyAcpProcessExitAcknowledgedAt',
|
|
127
|
+
'acpAdditionalDirectories',
|
|
128
|
+
'acpMcpServers',
|
|
129
|
+
'agentRuntimeMode',
|
|
130
|
+
'acpState',
|
|
131
|
+
'acpError',
|
|
132
|
+
'acpStopReason',
|
|
133
|
+
'acpPendingPermission',
|
|
134
|
+
'acpPendingPermissions',
|
|
135
|
+
'acpPendingElicitation',
|
|
136
|
+
'acpPendingElicitations',
|
|
137
|
+
'acpActiveElicitations',
|
|
138
|
+
'acpSessionUpdatedAt',
|
|
139
|
+
'acpSessionRevision',
|
|
140
|
+
'jsonCliState',
|
|
141
|
+
'jsonCliError',
|
|
142
|
+
'jsonCliTranscriptUpdatedAt',
|
|
143
|
+
'engine',
|
|
144
|
+
'category',
|
|
145
|
+
'task',
|
|
146
|
+
'workflowTemplate',
|
|
147
|
+
'wantsMain',
|
|
148
|
+
'pinned',
|
|
149
|
+
'projectOrder',
|
|
150
|
+
'pinnedOrder',
|
|
151
|
+
'attentionSeq',
|
|
152
|
+
'readAttentionSeq',
|
|
153
|
+
'attentionUpdatedAt',
|
|
154
|
+
'readAttentionAt',
|
|
155
|
+
'attentionReason',
|
|
156
|
+
'attentionOutputEpoch',
|
|
157
|
+
'attentionOutputSeq',
|
|
158
|
+
'readOutputEpoch',
|
|
159
|
+
'readOutputSeq',
|
|
160
|
+
'archived',
|
|
161
|
+
'archivedAt',
|
|
162
|
+
'visibleOnMainPage',
|
|
163
|
+
'customTitle',
|
|
164
|
+
'title',
|
|
165
|
+
'startedAt',
|
|
166
|
+
];
|
|
95
167
|
const execFileAsync = promisify(execFile);
|
|
96
168
|
|
|
169
|
+
function createRollbackState(record) {
|
|
170
|
+
if (!record || typeof record !== 'object') return null;
|
|
171
|
+
const state = {};
|
|
172
|
+
for (const field of CREATE_ROLLBACK_FIELDS) {
|
|
173
|
+
const value = Object.prototype.hasOwnProperty.call(record, field)
|
|
174
|
+
? record[field]
|
|
175
|
+
: null;
|
|
176
|
+
state[field] = value === undefined ? null : JSON.parse(JSON.stringify(value));
|
|
177
|
+
}
|
|
178
|
+
state.runtimeAgentId = String(record.runtimeAgentId || '');
|
|
179
|
+
state.visibleOnMainPage = record.visibleOnMainPage === true;
|
|
180
|
+
state.archived = record.archived === true;
|
|
181
|
+
state.customTitle = typeof record.customTitle === 'string' ? record.customTitle : '';
|
|
182
|
+
return state;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function createFailurePatch(operation, fallbackRuntimeAgentId = '') {
|
|
186
|
+
const previousState = operation?.request?.previousState;
|
|
187
|
+
if (previousState && typeof previousState === 'object') {
|
|
188
|
+
return JSON.parse(JSON.stringify(previousState));
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
visibleOnMainPage: Boolean(fallbackRuntimeAgentId),
|
|
192
|
+
runtimeAgentId: String(fallbackRuntimeAgentId || ''),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
97
196
|
function withBoundedWait(promise, timeoutMs, label) {
|
|
98
197
|
let timer;
|
|
99
198
|
const timeout = new Promise((_, reject) => {
|
|
@@ -395,6 +494,12 @@ function agentAttentionUnread(agent) {
|
|
|
395
494
|
return finiteNonNegativeInteger(agent && agent.attentionSeq) > finiteNonNegativeInteger(agent && agent.readAttentionSeq);
|
|
396
495
|
}
|
|
397
496
|
|
|
497
|
+
function setAgentRecordId(agent, agentRecordId) {
|
|
498
|
+
if (!agent || typeof agentRecordId !== 'string' || !agentRecordId) return;
|
|
499
|
+
agent.agentRecordId = agentRecordId;
|
|
500
|
+
agent.persistentSessionId = agentRecordId;
|
|
501
|
+
}
|
|
502
|
+
|
|
398
503
|
function hasAgentOutputAfterAttentionBaseline(agent) {
|
|
399
504
|
if (!agent || agent.attentionRequiresNewOutput !== true) return true;
|
|
400
505
|
const baselineSeq = finiteNumberOrNull(agent.attentionBaselineOutputSeq);
|
|
@@ -414,6 +519,15 @@ function hasAgentOutputAfterAttentionBaseline(agent) {
|
|
|
414
519
|
|
|
415
520
|
function shouldRestoreAgentFromMetadata(record, mainPageSessionKeys) {
|
|
416
521
|
if (!record || record.archived === true) return false;
|
|
522
|
+
const latestOperation = latestLifecycleOperation(record);
|
|
523
|
+
if (latestOperation?.type === 'delete' && latestOperation.state === 'succeeded') return false;
|
|
524
|
+
if (
|
|
525
|
+
latestOperation?.type === 'create'
|
|
526
|
+
&& latestOperation.state === 'succeeded'
|
|
527
|
+
&& record.visibleOnMainPage === true
|
|
528
|
+
) {
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
417
531
|
if (record.wantsMain === true) return true;
|
|
418
532
|
if (
|
|
419
533
|
record.providerSessionTemporary === true
|
|
@@ -431,6 +545,43 @@ function shouldRestoreAgentFromMetadata(record, mainPageSessionKeys) {
|
|
|
431
545
|
return record.visibleOnMainPage === true;
|
|
432
546
|
}
|
|
433
547
|
|
|
548
|
+
function lifecycleOperationBlocksRuntimeStart(record) {
|
|
549
|
+
const operation = activeLifecycleOperation(record);
|
|
550
|
+
return operation && ['create', 'delete', 'archive', 'runtime-switch'].includes(operation.type)
|
|
551
|
+
? operation
|
|
552
|
+
: null;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function requiresLegacyAcpExitAcknowledgement(record) {
|
|
556
|
+
if (
|
|
557
|
+
runtimeKind(record) !== 'acp'
|
|
558
|
+
|| record?.structuredRuntimeProcess
|
|
559
|
+
|| record?.legacyAcpProcessExitAcknowledgedAt
|
|
560
|
+
) {
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
if (record?.requiresProcessExitAcknowledgement === true) return true;
|
|
564
|
+
const operation = activeLifecycleOperation(record);
|
|
565
|
+
return Boolean(
|
|
566
|
+
operation
|
|
567
|
+
&& ['delete', 'archive'].includes(operation.type)
|
|
568
|
+
&& operation.request?.structuredProcessProofRequired === true,
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function publicActiveLifecycleOperation(agent) {
|
|
573
|
+
const operation = activeLifecycleOperation(agent);
|
|
574
|
+
if (!operation) return null;
|
|
575
|
+
return {
|
|
576
|
+
id: operation.id,
|
|
577
|
+
type: operation.type,
|
|
578
|
+
state: operation.state,
|
|
579
|
+
error: operation.error || '',
|
|
580
|
+
startedAt: operation.startedAt || null,
|
|
581
|
+
updatedAt: operation.updatedAt || null,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
434
585
|
function recoveredEngineSessionId(entry, metadata = {}) {
|
|
435
586
|
return entry && (entry.sessionId || entry.agentId || metadata.agentId) || '';
|
|
436
587
|
}
|
|
@@ -440,6 +591,33 @@ function agentDisplayName(command) {
|
|
|
440
591
|
return getProviderAdapter(providerForProgram(program))?.displayName || program;
|
|
441
592
|
}
|
|
442
593
|
|
|
594
|
+
function stableJsonValue(value) {
|
|
595
|
+
if (Array.isArray(value)) return value.map(stableJsonValue);
|
|
596
|
+
if (!value || typeof value !== 'object') return value;
|
|
597
|
+
return Object.keys(value).sort().reduce((result, key) => {
|
|
598
|
+
const child = value[key];
|
|
599
|
+
if (!['function', 'symbol', 'undefined'].includes(typeof child)) {
|
|
600
|
+
result[key] = stableJsonValue(child);
|
|
601
|
+
}
|
|
602
|
+
return result;
|
|
603
|
+
}, {});
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function createOperationSignature(command, customWorkspace, options = {}) {
|
|
607
|
+
const semanticOptions = { ...options };
|
|
608
|
+
[
|
|
609
|
+
'createRequestId',
|
|
610
|
+
'lifecycleToken',
|
|
611
|
+
'skipRecoveryWait',
|
|
612
|
+
'startAdmissionToken',
|
|
613
|
+
].forEach(field => delete semanticOptions[field]);
|
|
614
|
+
return crypto.createHash('sha256').update(JSON.stringify(stableJsonValue({
|
|
615
|
+
command: String(command || '').trim(),
|
|
616
|
+
workspace: String(customWorkspace || ''),
|
|
617
|
+
options: semanticOptions,
|
|
618
|
+
}))).digest('hex');
|
|
619
|
+
}
|
|
620
|
+
|
|
443
621
|
function titleComparisonKey(title) {
|
|
444
622
|
return String(title || '')
|
|
445
623
|
.trim()
|
|
@@ -606,9 +784,13 @@ class AgentManager extends EventEmitter {
|
|
|
606
784
|
this.codexTerminalStartQueues = new Map();
|
|
607
785
|
this.codexTerminalStartOutput = new Map();
|
|
608
786
|
this.agentWorktreeResolveGeneration = new Map();
|
|
609
|
-
this.
|
|
610
|
-
this.
|
|
611
|
-
this.
|
|
787
|
+
this.agentLifecycleOperations = new Map();
|
|
788
|
+
this.agentStartAdmissions = new Map();
|
|
789
|
+
this.createRequestAdmissions = new Map();
|
|
790
|
+
this.projectWorkspaceDeleteAdmissions = new Map();
|
|
791
|
+
this.activeInputOperations = new Set();
|
|
792
|
+
this.verifiedStoppedAgentIds = new Set();
|
|
793
|
+
this.codexSessionMutationQueues = new Map();
|
|
612
794
|
// Standard ACP session inputs may contain MCP credentials. Keep the live
|
|
613
795
|
// copy outside browser-facing Agent records; crash recovery persists it
|
|
614
796
|
// only through the private Farming session store.
|
|
@@ -635,7 +817,16 @@ class AgentManager extends EventEmitter {
|
|
|
635
817
|
: deletePrecreatedProviderSession;
|
|
636
818
|
this.archiveCodexSession = options.archiveCodexSession || archiveCodexSession;
|
|
637
819
|
this.unarchiveCodexSession = options.unarchiveCodexSession || unarchiveCodexSession;
|
|
820
|
+
this.stopPersistedAcpProcessGroup = options.stopPersistedAcpProcessGroup
|
|
821
|
+
|| stopPersistedAcpProcessGroup;
|
|
822
|
+
// Upgrade compatibility is the product default: records created before
|
|
823
|
+
// ACP process identities existed must still resume their provider Session.
|
|
824
|
+
// Strict callers may opt out explicitly for cleanup-safety diagnostics.
|
|
825
|
+
this.allowUnprovenLegacyAcpRecovery = options.allowUnprovenLegacyAcpRecovery !== false;
|
|
638
826
|
this.heartbeatInterval = null;
|
|
827
|
+
this.disposing = false;
|
|
828
|
+
this.disposeFrozen = false;
|
|
829
|
+
this.disposePromise = null;
|
|
639
830
|
this.disposed = false;
|
|
640
831
|
this.systemMonitor = new SystemMonitor();
|
|
641
832
|
this.startTime = Date.now();
|
|
@@ -657,6 +848,8 @@ class AgentManager extends EventEmitter {
|
|
|
657
848
|
},
|
|
658
849
|
});
|
|
659
850
|
this.recoveryPromise = Promise.resolve();
|
|
851
|
+
this.recoveryComplete = !(this.configManager && this.configManager.farmingDir);
|
|
852
|
+
this.recoveryError = null;
|
|
660
853
|
this.taskHistory = (this.configManager && this.configManager.getTaskHistory)
|
|
661
854
|
? [...this.configManager.getTaskHistory()]
|
|
662
855
|
: [];
|
|
@@ -666,9 +859,14 @@ class AgentManager extends EventEmitter {
|
|
|
666
859
|
this.bindJsonCliRuntimeEvents();
|
|
667
860
|
this.bindAcpRuntimeEvents();
|
|
668
861
|
if (this.configManager && this.configManager.farmingDir) {
|
|
669
|
-
this.recoveryPromise = this.recoverEngineSessions()
|
|
670
|
-
|
|
671
|
-
|
|
862
|
+
this.recoveryPromise = this.recoverEngineSessions()
|
|
863
|
+
.then(() => {
|
|
864
|
+
this.recoveryComplete = true;
|
|
865
|
+
})
|
|
866
|
+
.catch((error) => {
|
|
867
|
+
this.recoveryError = error;
|
|
868
|
+
console.warn('Failed to recover engine sessions:', error && (error.message || error));
|
|
869
|
+
});
|
|
672
870
|
}
|
|
673
871
|
}
|
|
674
872
|
|
|
@@ -699,7 +897,7 @@ class AgentManager extends EventEmitter {
|
|
|
699
897
|
|
|
700
898
|
bindAcpRuntimeEvents() {
|
|
701
899
|
if (!this.acpRuntime || typeof this.acpRuntime.on !== 'function') return;
|
|
702
|
-
this.acpRuntime.on('agent-runtime', ({ agentId, state, error, sessionId, stopReason, supportsSteer, pendingPermission, pendingPermissions, pendingElicitation, pendingElicitations, activeElicitations, updatedAt }) => {
|
|
900
|
+
this.acpRuntime.on('agent-runtime', ({ agentId, state, error, sessionId, stopReason, supportsSteer, supportsFork, pendingPermission, pendingPermissions, pendingElicitation, pendingElicitations, activeElicitations, updatedAt }) => {
|
|
703
901
|
const agent = this.agents.get(agentId);
|
|
704
902
|
const runtime = runtimeBindingOf(agent, 'acp');
|
|
705
903
|
if (!runtime) return;
|
|
@@ -707,6 +905,7 @@ class AgentManager extends EventEmitter {
|
|
|
707
905
|
runtime.error = error || '';
|
|
708
906
|
runtime.stopReason = stopReason || '';
|
|
709
907
|
runtime.supportsSteer = supportsSteer === true;
|
|
908
|
+
runtime.supportsFork = supportsFork === true;
|
|
710
909
|
runtime.pendingPermission = pendingPermission || null;
|
|
711
910
|
runtime.pendingPermissions = Array.isArray(pendingPermissions) ? pendingPermissions : [];
|
|
712
911
|
runtime.pendingElicitation = pendingElicitation || null;
|
|
@@ -1153,6 +1352,9 @@ class AgentManager extends EventEmitter {
|
|
|
1153
1352
|
const persistedByRuntimeAgentId = new Map(persistedRecords
|
|
1154
1353
|
.filter(record => record && record.runtimeAgentId)
|
|
1155
1354
|
.map(record => [record.runtimeAgentId, record]));
|
|
1355
|
+
const recoveredRuntimeAgentIds = new Set((recovered || [])
|
|
1356
|
+
.map(entry => recoveredEngineSessionId(entry, entry?.metadata || {}))
|
|
1357
|
+
.filter(Boolean));
|
|
1156
1358
|
let changed = false;
|
|
1157
1359
|
|
|
1158
1360
|
for (const entry of recovered || []) {
|
|
@@ -1161,7 +1363,13 @@ class AgentManager extends EventEmitter {
|
|
|
1161
1363
|
const agentId = recoveredEngineSessionId(entry, engineMetadata);
|
|
1162
1364
|
const persisted = persistedByRuntimeAgentId.get(agentId);
|
|
1163
1365
|
const desiredMetadata = persisted || engineMetadata;
|
|
1164
|
-
|
|
1366
|
+
const persistedLifecycleOperation = activeLifecycleOperation(desiredMetadata);
|
|
1367
|
+
const hasRecoverableLifecycleOperation = persistedLifecycleOperation
|
|
1368
|
+
&& ['create', 'delete', 'archive'].includes(persistedLifecycleOperation.type);
|
|
1369
|
+
if (
|
|
1370
|
+
!hasRecoverableLifecycleOperation
|
|
1371
|
+
&& !shouldRestoreAgentFromMetadata(desiredMetadata, mainPageSessionKeys)
|
|
1372
|
+
) {
|
|
1165
1373
|
await this.killRecoveredEngineSession(entry, engineMetadata, agentId);
|
|
1166
1374
|
continue;
|
|
1167
1375
|
}
|
|
@@ -1204,6 +1412,7 @@ class AgentManager extends EventEmitter {
|
|
|
1204
1412
|
customTitle: Object.prototype.hasOwnProperty.call(persisted, 'customTitle')
|
|
1205
1413
|
? persisted.customTitle
|
|
1206
1414
|
: engineMetadata.customTitle,
|
|
1415
|
+
lifecycleJournal: lifecycleJournal(persisted),
|
|
1207
1416
|
...legacyRuntimeMetadata(persisted),
|
|
1208
1417
|
pinned: persisted.pinned === true,
|
|
1209
1418
|
projectOrder: finiteOrder(persisted.projectOrder) ?? finiteOrder(engineMetadata.projectOrder),
|
|
@@ -1214,20 +1423,72 @@ class AgentManager extends EventEmitter {
|
|
|
1214
1423
|
const agentRecord = this.recoveredAgentRecord(agentId, entry.engineName || metadata.engineName || 'native', metadata, state);
|
|
1215
1424
|
ensureAgentOrders(agentRecord, Array.from(this.agents.values()));
|
|
1216
1425
|
agentRecord.lastObservedTurnActive = this.isAgentAttentionTurnActive(agentRecord);
|
|
1217
|
-
this.ensurePersistentAgentSession(agentRecord, {
|
|
1218
|
-
visibleOnMainPage: true,
|
|
1219
|
-
archived: false,
|
|
1220
|
-
});
|
|
1221
1426
|
this.agents.set(agentId, agentRecord);
|
|
1427
|
+
const recoveredLifecycleOperation = activeLifecycleOperation(agentRecord);
|
|
1428
|
+
if (
|
|
1429
|
+
recoveredLifecycleOperation?.type === 'create'
|
|
1430
|
+
&& recoveredLifecycleOperation.state === 'pending'
|
|
1431
|
+
) {
|
|
1432
|
+
try {
|
|
1433
|
+
this.transitionPersistentAgentOperation(
|
|
1434
|
+
agentRecord,
|
|
1435
|
+
recoveredLifecycleOperation.id,
|
|
1436
|
+
'succeeded',
|
|
1437
|
+
'',
|
|
1438
|
+
{ visibleOnMainPage: true, archived: false },
|
|
1439
|
+
);
|
|
1440
|
+
} catch (error) {
|
|
1441
|
+
this.markRecoveredAgentLifecycleBlocked(agentRecord, recoveredLifecycleOperation, error);
|
|
1442
|
+
}
|
|
1443
|
+
} else if (
|
|
1444
|
+
recoveredLifecycleOperation?.type === 'delete'
|
|
1445
|
+
|| recoveredLifecycleOperation?.type === 'archive'
|
|
1446
|
+
) {
|
|
1447
|
+
const result = recoveredLifecycleOperation.type === 'delete'
|
|
1448
|
+
? await this.killAgent(agentId, {
|
|
1449
|
+
reason: 'delete-recovery',
|
|
1450
|
+
skipRecoveryWait: true,
|
|
1451
|
+
})
|
|
1452
|
+
: await this.archiveAgent(agentId, {
|
|
1453
|
+
reason: 'archive-recovery',
|
|
1454
|
+
skipRecoveryWait: true,
|
|
1455
|
+
});
|
|
1456
|
+
if (result?.error) {
|
|
1457
|
+
console.warn(
|
|
1458
|
+
`Failed to resume Agent ${recoveredLifecycleOperation.type} ${agentId}: ${result.error}`,
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
changed = true;
|
|
1462
|
+
continue;
|
|
1463
|
+
} else if (recoveredLifecycleOperation) {
|
|
1464
|
+
this.markRecoveredAgentLifecycleBlocked(agentRecord, recoveredLifecycleOperation);
|
|
1465
|
+
} else {
|
|
1466
|
+
this.ensurePersistentAgentSession(agentRecord, {
|
|
1467
|
+
visibleOnMainPage: true,
|
|
1468
|
+
archived: false,
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
const recoveredUpdate = this.reconcilePersistedAgentUpdate(agentRecord);
|
|
1472
|
+
if (recoveredUpdate?.error) {
|
|
1473
|
+
console.warn(`Failed to reconcile Agent update ${agentId}: ${recoveredUpdate.error}`);
|
|
1474
|
+
}
|
|
1222
1475
|
void this.refreshAgentWorktree(agentId);
|
|
1223
1476
|
this.lastActivity.set(agentId, state.lastActivityAt || metadata.lastActivityAt || Date.now());
|
|
1224
1477
|
if (agentRecord.wantsMain && !this.mainAgentId) {
|
|
1225
1478
|
this.mainAgentId = agentId;
|
|
1226
1479
|
}
|
|
1480
|
+
this.rememberMainPageProviderSession(agentRecord);
|
|
1227
1481
|
this.providerSessionService.activate(agentId);
|
|
1228
1482
|
changed = true;
|
|
1229
1483
|
}
|
|
1230
1484
|
|
|
1485
|
+
if (this.reconcileMissingTerminalLifecycleOperations(
|
|
1486
|
+
persistedRecords,
|
|
1487
|
+
recoveredRuntimeAgentIds,
|
|
1488
|
+
)) {
|
|
1489
|
+
changed = true;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1231
1492
|
if (changed) {
|
|
1232
1493
|
this.emit('update');
|
|
1233
1494
|
}
|
|
@@ -1240,6 +1501,83 @@ class AgentManager extends EventEmitter {
|
|
|
1240
1501
|
}
|
|
1241
1502
|
|
|
1242
1503
|
await this.recoverAcpSessions();
|
|
1504
|
+
if (this.reconcileDetachedPersistedAgentUpdates()) {
|
|
1505
|
+
this.emit('update');
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
markRecoveredAgentLifecycleBlocked(agent, operation, cause = null) {
|
|
1510
|
+
const reason = cause
|
|
1511
|
+
? `Agent ${operation.type} operation ${operation.id} recovery could not be committed: ${cause.message || cause}`
|
|
1512
|
+
: `Agent ${operation.type} operation ${operation.id} must be resolved before restart`;
|
|
1513
|
+
agent.status = 'error';
|
|
1514
|
+
agent.engineStatus = 'lifecycle-blocked';
|
|
1515
|
+
const runtime = runtimeBindingOf(agent);
|
|
1516
|
+
if (runtime && Object.prototype.hasOwnProperty.call(runtime, 'state')) {
|
|
1517
|
+
runtime.state = 'error';
|
|
1518
|
+
runtime.error = reason;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
reconcileMissingTerminalLifecycleOperations(records, recoveredRuntimeAgentIds) {
|
|
1523
|
+
let changed = false;
|
|
1524
|
+
for (const record of Array.isArray(records) ? records : []) {
|
|
1525
|
+
const agentId = String(record?.runtimeAgentId || '').trim();
|
|
1526
|
+
const operation = activeLifecycleOperation(record);
|
|
1527
|
+
if (
|
|
1528
|
+
!agentId
|
|
1529
|
+
|| recoveredRuntimeAgentIds.has(agentId)
|
|
1530
|
+
|| runtimeKind(record) !== 'terminal'
|
|
1531
|
+
|| !operation
|
|
1532
|
+
|| !['create', 'delete', 'archive'].includes(operation.type)
|
|
1533
|
+
) {
|
|
1534
|
+
continue;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
const recoveredAgent = this.recoveredAgentRecord(
|
|
1538
|
+
agentId,
|
|
1539
|
+
record.engine || 'native',
|
|
1540
|
+
{ ...record, persistentSessionId: record.id || record.persistentSessionId || '' },
|
|
1541
|
+
{ status: 'exited' },
|
|
1542
|
+
);
|
|
1543
|
+
setAgentRecordId(recoveredAgent, record.id || record.persistentSessionId || '');
|
|
1544
|
+
try {
|
|
1545
|
+
if (operation.type === 'create') {
|
|
1546
|
+
this.transitionPersistentAgentOperation(
|
|
1547
|
+
recoveredAgent,
|
|
1548
|
+
operation.id,
|
|
1549
|
+
'failed',
|
|
1550
|
+
'Create runtime was not present in the authoritative native-host recovery set',
|
|
1551
|
+
createFailurePatch(
|
|
1552
|
+
operation,
|
|
1553
|
+
operation.request?.previousRuntimeAgentId,
|
|
1554
|
+
),
|
|
1555
|
+
);
|
|
1556
|
+
} else {
|
|
1557
|
+
this.transitionPersistentAgentOperation(
|
|
1558
|
+
recoveredAgent,
|
|
1559
|
+
operation.id,
|
|
1560
|
+
'succeeded',
|
|
1561
|
+
'',
|
|
1562
|
+
{
|
|
1563
|
+
visibleOnMainPage: false,
|
|
1564
|
+
archived: true,
|
|
1565
|
+
archivedAt: Date.now(),
|
|
1566
|
+
runtimeAgentId: '',
|
|
1567
|
+
},
|
|
1568
|
+
);
|
|
1569
|
+
this.removeMainPageProviderSessionsForAgents([recoveredAgent]);
|
|
1570
|
+
if (operation.type === 'archive') this.scheduleCodexSessionArchive(recoveredAgent);
|
|
1571
|
+
}
|
|
1572
|
+
changed = true;
|
|
1573
|
+
} catch (error) {
|
|
1574
|
+
console.warn(
|
|
1575
|
+
`Failed to reconcile missing Terminal Agent ${agentId} ${operation.type}:`,
|
|
1576
|
+
error && (error.message || error),
|
|
1577
|
+
);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
return changed;
|
|
1243
1581
|
}
|
|
1244
1582
|
|
|
1245
1583
|
async restoreTerminalSessionsAfterRuntimeRotation(records, rotations) {
|
|
@@ -1485,11 +1823,17 @@ class AgentManager extends EventEmitter {
|
|
|
1485
1823
|
|
|
1486
1824
|
async recoverAcpSessions() {
|
|
1487
1825
|
if (!this.acpRuntime || !this.configManager || typeof this.configManager.listAgentSessionRecords !== 'function') return;
|
|
1826
|
+
await this.reconcilePersistedAcpLifecycleOperations(
|
|
1827
|
+
this.configManager.listAgentSessionRecords(),
|
|
1828
|
+
);
|
|
1488
1829
|
const mainPageOrder = new Map(this.getMainPageSessionKeys().map((key, index) => [key, index]));
|
|
1489
1830
|
const mainPageSessionKeys = new Set(mainPageOrder.keys());
|
|
1490
1831
|
const records = this.configManager.listAgentSessionRecords()
|
|
1491
1832
|
.filter(record => (
|
|
1492
|
-
|
|
1833
|
+
(
|
|
1834
|
+
shouldRestoreAgentFromMetadata(record, mainPageSessionKeys)
|
|
1835
|
+
|| lifecycleOperationBlocksRuntimeStart(record)
|
|
1836
|
+
)
|
|
1493
1837
|
&& runtimeKind(record) === 'acp'
|
|
1494
1838
|
))
|
|
1495
1839
|
.sort((left, right) => {
|
|
@@ -1506,14 +1850,29 @@ class AgentManager extends EventEmitter {
|
|
|
1506
1850
|
const agentId = String(record.runtimeAgentId || '').trim();
|
|
1507
1851
|
const provider = String(record.providerSessionProvider || record.provider || '').trim();
|
|
1508
1852
|
const sessionId = String(record.providerSessionId || record.codexAppServerThreadId || '').trim();
|
|
1509
|
-
|
|
1853
|
+
const blockedOperation = lifecycleOperationBlocksRuntimeStart(record);
|
|
1854
|
+
if (
|
|
1855
|
+
!agentId
|
|
1856
|
+
|| !providerSupportsRuntime(provider, 'acp')
|
|
1857
|
+
|| (!sessionId && !blockedOperation)
|
|
1858
|
+
) {
|
|
1859
|
+
continue;
|
|
1860
|
+
}
|
|
1510
1861
|
const agent = this.agents.get(agentId);
|
|
1511
1862
|
if (!agent) {
|
|
1512
1863
|
const recoveredAgent = this.recoveredAgentRecord(agentId, record.engine || 'native', record, { status: 'running' });
|
|
1513
1864
|
ensureAgentOrders(recoveredAgent, Array.from(this.agents.values()));
|
|
1514
|
-
recoveredAgent
|
|
1865
|
+
setAgentRecordId(recoveredAgent, record.id || '');
|
|
1515
1866
|
recoveredAgent.engineStarted = false;
|
|
1516
|
-
|
|
1867
|
+
if (blockedOperation) {
|
|
1868
|
+
recoveredAgent.status = 'error';
|
|
1869
|
+
recoveredAgent.engineStatus = 'lifecycle-blocked';
|
|
1870
|
+
const runtime = runtimeBindingOf(recoveredAgent, 'acp');
|
|
1871
|
+
runtime.state = 'error';
|
|
1872
|
+
runtime.error = `Agent ${blockedOperation.type} operation ${blockedOperation.id} must be resolved before restart`;
|
|
1873
|
+
} else {
|
|
1874
|
+
runtimeBindingOf(recoveredAgent, 'acp').state = 'connecting';
|
|
1875
|
+
}
|
|
1517
1876
|
this.agents.set(agentId, recoveredAgent);
|
|
1518
1877
|
void this.refreshAgentWorktree(agentId);
|
|
1519
1878
|
this.lastActivity.set(agentId, Date.now());
|
|
@@ -1521,13 +1880,55 @@ class AgentManager extends EventEmitter {
|
|
|
1521
1880
|
}
|
|
1522
1881
|
this.emit('update');
|
|
1523
1882
|
|
|
1883
|
+
for (const record of records) {
|
|
1884
|
+
const agentId = String(record.runtimeAgentId || '').trim();
|
|
1885
|
+
const agent = this.agents.get(agentId);
|
|
1886
|
+
if (!agent) continue;
|
|
1887
|
+
const recoveredUpdate = this.reconcilePersistedAgentUpdate(agent);
|
|
1888
|
+
if (recoveredUpdate?.error) {
|
|
1889
|
+
console.warn(`Failed to reconcile Agent update ${agentId}: ${recoveredUpdate.error}`);
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1524
1893
|
for (const record of records) {
|
|
1525
1894
|
const agentId = String(record.runtimeAgentId || '').trim();
|
|
1526
1895
|
const provider = String(record.providerSessionProvider || record.provider || '').trim();
|
|
1527
1896
|
const sessionId = String(record.providerSessionId || record.codexAppServerThreadId || '').trim();
|
|
1528
1897
|
const agent = this.agents.get(agentId);
|
|
1529
1898
|
if (!agent || !sessionId || !providerSupportsRuntime(provider, 'acp')) continue;
|
|
1899
|
+
if (lifecycleOperationBlocksRuntimeStart(record)) continue;
|
|
1530
1900
|
try {
|
|
1901
|
+
if (
|
|
1902
|
+
!record.structuredRuntimeProcess
|
|
1903
|
+
&& !this.allowUnprovenLegacyAcpRecovery
|
|
1904
|
+
&& !record.legacyAcpProcessExitAcknowledgedAt
|
|
1905
|
+
) {
|
|
1906
|
+
const cleanupError = new Error(
|
|
1907
|
+
'Legacy ACP process exit cannot be proven after restart; automatic recovery is blocked',
|
|
1908
|
+
);
|
|
1909
|
+
cleanupError.code = 'ACP_PROCESS_CLEANUP_UNCERTAIN';
|
|
1910
|
+
throw cleanupError;
|
|
1911
|
+
}
|
|
1912
|
+
if (record.structuredRuntimeProcess) {
|
|
1913
|
+
let cleanup;
|
|
1914
|
+
try {
|
|
1915
|
+
cleanup = await this.stopPersistedAcpProcessGroup(record.structuredRuntimeProcess);
|
|
1916
|
+
} catch (cause) {
|
|
1917
|
+
const cleanupError = new Error(
|
|
1918
|
+
`Persisted ACP process exit proof failed: ${cause.message || cause}`,
|
|
1919
|
+
{ cause },
|
|
1920
|
+
);
|
|
1921
|
+
cleanupError.code = 'ACP_PROCESS_CLEANUP_UNCERTAIN';
|
|
1922
|
+
throw cleanupError;
|
|
1923
|
+
}
|
|
1924
|
+
if (cleanup.stopped !== true) {
|
|
1925
|
+
const cleanupError = new Error('Persisted ACP process identity could not be safely stopped');
|
|
1926
|
+
cleanupError.code = 'ACP_PROCESS_CLEANUP_UNCERTAIN';
|
|
1927
|
+
throw cleanupError;
|
|
1928
|
+
}
|
|
1929
|
+
agent.structuredRuntimeProcess = null;
|
|
1930
|
+
this.ensurePersistentAgentSession(agent);
|
|
1931
|
+
}
|
|
1531
1932
|
const executableName = getProviderAdapter(provider).executable;
|
|
1532
1933
|
const executable = resolveAgentExecutable(executableName) || executableName;
|
|
1533
1934
|
const approvalMode = agent.launchPermissionMode || (
|
|
@@ -1552,6 +1953,13 @@ class AgentManager extends EventEmitter {
|
|
|
1552
1953
|
serviceTier: 'config',
|
|
1553
1954
|
additionalDirectories: Array.isArray(record.acpAdditionalDirectories) ? record.acpAdditionalDirectories : [],
|
|
1554
1955
|
mcpServers: Array.isArray(record.acpMcpServers) ? record.acpMcpServers : [],
|
|
1956
|
+
onProcessStarted: async processIdentity => {
|
|
1957
|
+
agent.structuredRuntimeProcess = {
|
|
1958
|
+
kind: 'acp-process-group',
|
|
1959
|
+
...processIdentity,
|
|
1960
|
+
};
|
|
1961
|
+
this.ensurePersistentAgentSession(agent);
|
|
1962
|
+
},
|
|
1555
1963
|
});
|
|
1556
1964
|
agent.providerSessionId = prepared.sessionId;
|
|
1557
1965
|
agent.providerSessionKey = mainPageAgentSessionKey(
|
|
@@ -1575,13 +1983,19 @@ class AgentManager extends EventEmitter {
|
|
|
1575
1983
|
agent.status = 'running';
|
|
1576
1984
|
agent.engineStatus = 'running';
|
|
1577
1985
|
agent.engineStarted = false;
|
|
1986
|
+
agent.requiresProcessExitAcknowledgement = false;
|
|
1578
1987
|
this.ensurePersistentAgentSession(agent);
|
|
1988
|
+
this.rememberMainPageProviderSession(agent);
|
|
1579
1989
|
} catch (error) {
|
|
1580
1990
|
const runtime = runtimeBindingOf(agent, 'acp');
|
|
1581
1991
|
runtime.state = 'error';
|
|
1582
1992
|
runtime.error = `ACP recovery failed: ${error && (error.message || error)}`;
|
|
1583
|
-
|
|
1584
|
-
agent.
|
|
1993
|
+
const cleanupUncertain = error?.code === 'ACP_PROCESS_CLEANUP_UNCERTAIN';
|
|
1994
|
+
agent.status = cleanupUncertain ? 'error' : 'stopped';
|
|
1995
|
+
agent.engineStatus = cleanupUncertain ? 'cleanup-uncertain' : 'stopped';
|
|
1996
|
+
agent.requiresProcessExitAcknowledgement = cleanupUncertain
|
|
1997
|
+
&& !record.structuredRuntimeProcess
|
|
1998
|
+
&& !record.legacyAcpProcessExitAcknowledgedAt;
|
|
1585
1999
|
agent.engineStarted = false;
|
|
1586
2000
|
agent.exitedAt = Date.now();
|
|
1587
2001
|
this.ensurePersistentAgentSession(agent);
|
|
@@ -1590,6 +2004,158 @@ class AgentManager extends EventEmitter {
|
|
|
1590
2004
|
this.emit('update');
|
|
1591
2005
|
}
|
|
1592
2006
|
|
|
2007
|
+
async reconcilePersistedAcpLifecycleOperations(records) {
|
|
2008
|
+
for (const record of Array.isArray(records) ? records : []) {
|
|
2009
|
+
const operation = activeLifecycleOperation(record);
|
|
2010
|
+
if (
|
|
2011
|
+
runtimeKind(record) !== 'acp'
|
|
2012
|
+
|| !operation
|
|
2013
|
+
|| !['create', 'delete', 'archive'].includes(operation.type)
|
|
2014
|
+
) {
|
|
2015
|
+
continue;
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
const processProofRequired = operation.request?.structuredProcessProofRequired === true
|
|
2019
|
+
|| Boolean(record.structuredRuntimeProcess);
|
|
2020
|
+
if (processProofRequired && !record.legacyAcpProcessExitAcknowledgedAt) {
|
|
2021
|
+
try {
|
|
2022
|
+
const cleanup = await this.stopPersistedAcpProcessGroup(record.structuredRuntimeProcess);
|
|
2023
|
+
if (
|
|
2024
|
+
cleanup.stopped !== true
|
|
2025
|
+
&& !(
|
|
2026
|
+
cleanup.missingProof === true
|
|
2027
|
+
&& operation.request?.structuredProcessStartGated === true
|
|
2028
|
+
)
|
|
2029
|
+
) {
|
|
2030
|
+
continue;
|
|
2031
|
+
}
|
|
2032
|
+
} catch (error) {
|
|
2033
|
+
console.warn(
|
|
2034
|
+
`Could not prove persisted ACP process exit for ${record.runtimeAgentId || operation.id}:`,
|
|
2035
|
+
error && (error.message || error),
|
|
2036
|
+
);
|
|
2037
|
+
continue;
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
const agentId = String(
|
|
2042
|
+
operation.type === 'create'
|
|
2043
|
+
? operation.request?.agentId || record.runtimeAgentId || ''
|
|
2044
|
+
: record.runtimeAgentId || '',
|
|
2045
|
+
).trim();
|
|
2046
|
+
if (!agentId) continue;
|
|
2047
|
+
const staged = this.recoveredAgentRecord(
|
|
2048
|
+
agentId,
|
|
2049
|
+
record.engine || 'native',
|
|
2050
|
+
{ ...record, persistentSessionId: record.id || '' },
|
|
2051
|
+
{ status: 'exited' },
|
|
2052
|
+
);
|
|
2053
|
+
setAgentRecordId(staged, record.id || '');
|
|
2054
|
+
staged.structuredRuntimeProcess = null;
|
|
2055
|
+
try {
|
|
2056
|
+
if (operation.type === 'create') {
|
|
2057
|
+
this.transitionPersistentAgentOperation(
|
|
2058
|
+
staged,
|
|
2059
|
+
operation.id,
|
|
2060
|
+
'failed',
|
|
2061
|
+
'Create ACP process was stopped during restart recovery',
|
|
2062
|
+
createFailurePatch(operation, operation.request?.previousRuntimeAgentId),
|
|
2063
|
+
);
|
|
2064
|
+
} else {
|
|
2065
|
+
this.transitionPersistentAgentOperation(
|
|
2066
|
+
staged,
|
|
2067
|
+
operation.id,
|
|
2068
|
+
'succeeded',
|
|
2069
|
+
'',
|
|
2070
|
+
{
|
|
2071
|
+
visibleOnMainPage: false,
|
|
2072
|
+
archived: true,
|
|
2073
|
+
archivedAt: Date.now(),
|
|
2074
|
+
runtimeAgentId: '',
|
|
2075
|
+
structuredRuntimeProcess: null,
|
|
2076
|
+
},
|
|
2077
|
+
);
|
|
2078
|
+
this.removeMainPageProviderSessionsForAgents([staged]);
|
|
2079
|
+
if (operation.type === 'archive') this.scheduleCodexSessionArchive(staged);
|
|
2080
|
+
}
|
|
2081
|
+
} catch (error) {
|
|
2082
|
+
console.warn(
|
|
2083
|
+
`Failed to reconcile ACP Agent ${agentId} ${operation.type}:`,
|
|
2084
|
+
error && (error.message || error),
|
|
2085
|
+
);
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
reconcileDetachedPersistedAgentUpdates() {
|
|
2091
|
+
if (!this.configManager || typeof this.configManager.listAgentSessionRecords !== 'function') {
|
|
2092
|
+
return false;
|
|
2093
|
+
}
|
|
2094
|
+
let changed = false;
|
|
2095
|
+
for (const record of this.configManager.listAgentSessionRecords()) {
|
|
2096
|
+
const operation = activeLifecycleOperation(record);
|
|
2097
|
+
const agentId = String(record?.runtimeAgentId || '').trim();
|
|
2098
|
+
if (
|
|
2099
|
+
operation?.type !== 'update'
|
|
2100
|
+
|| !agentId
|
|
2101
|
+
|| this.agents.has(agentId)
|
|
2102
|
+
) {
|
|
2103
|
+
continue;
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
const staged = this.recoveredAgentRecord(
|
|
2107
|
+
agentId,
|
|
2108
|
+
record.engine || 'native',
|
|
2109
|
+
record,
|
|
2110
|
+
{ status: 'stopped' },
|
|
2111
|
+
);
|
|
2112
|
+
setAgentRecordId(staged, record.id || '');
|
|
2113
|
+
const request = operation.request || {};
|
|
2114
|
+
if (Object.prototype.hasOwnProperty.call(request, 'customTitle')) {
|
|
2115
|
+
staged.customTitle = String(request.customTitle || '').trim().slice(0, 80);
|
|
2116
|
+
}
|
|
2117
|
+
if (Object.prototype.hasOwnProperty.call(request, 'task')) {
|
|
2118
|
+
staged.task = String(request.task || '').trim().slice(0, 240);
|
|
2119
|
+
}
|
|
2120
|
+
if (typeof request.pinned === 'boolean') {
|
|
2121
|
+
staged.pinned = request.pinned;
|
|
2122
|
+
if (!staged.pinned) staged.pinnedOrder = null;
|
|
2123
|
+
}
|
|
2124
|
+
if (request.archived === false) {
|
|
2125
|
+
staged.archived = false;
|
|
2126
|
+
staged.archivedAt = null;
|
|
2127
|
+
}
|
|
2128
|
+
if (typeof request.readAttentionSeq === 'number') {
|
|
2129
|
+
staged.readAttentionSeq = Math.min(
|
|
2130
|
+
finiteNonNegativeInteger(staged.attentionSeq),
|
|
2131
|
+
Math.max(
|
|
2132
|
+
finiteNonNegativeInteger(staged.readAttentionSeq),
|
|
2133
|
+
finiteNonNegativeInteger(request.readAttentionSeq),
|
|
2134
|
+
),
|
|
2135
|
+
);
|
|
2136
|
+
staged.unread = agentAttentionUnread(staged);
|
|
2137
|
+
}
|
|
2138
|
+
if (
|
|
2139
|
+
typeof request.readOutputEpoch === 'string'
|
|
2140
|
+
&& typeof request.readOutputSeq === 'number'
|
|
2141
|
+
) {
|
|
2142
|
+
staged.readOutputEpoch = request.readOutputEpoch;
|
|
2143
|
+
staged.readOutputSeq = finiteNonNegativeInteger(request.readOutputSeq);
|
|
2144
|
+
}
|
|
2145
|
+
transitionLifecycleOperation(staged, operation.id, 'succeeded');
|
|
2146
|
+
try {
|
|
2147
|
+
this.ensurePersistentAgentSession(staged);
|
|
2148
|
+
changed = true;
|
|
2149
|
+
} catch (error) {
|
|
2150
|
+
console.warn(
|
|
2151
|
+
`Failed to reconcile detached Agent update ${operation.id}:`,
|
|
2152
|
+
error && (error.message || error),
|
|
2153
|
+
);
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
return changed;
|
|
2157
|
+
}
|
|
2158
|
+
|
|
1593
2159
|
async killRecoveredEngineSession(entry, metadata, agentId) {
|
|
1594
2160
|
if (!this.engineBridge || typeof this.engineBridge.killSession !== 'function') return;
|
|
1595
2161
|
const engineName = entry.engineName || metadata.engineName || 'native';
|
|
@@ -1602,6 +2168,12 @@ class AgentManager extends EventEmitter {
|
|
|
1602
2168
|
|
|
1603
2169
|
async whenRecovered() {
|
|
1604
2170
|
await this.recoveryPromise;
|
|
2171
|
+
if (this.recoveryError) {
|
|
2172
|
+
throw new Error(
|
|
2173
|
+
`Agent lifecycle recovery failed: ${this.recoveryError.message || this.recoveryError}`,
|
|
2174
|
+
{ cause: this.recoveryError },
|
|
2175
|
+
);
|
|
2176
|
+
}
|
|
1605
2177
|
}
|
|
1606
2178
|
|
|
1607
2179
|
recoveredAgentRecord(agentId, engineName, metadata, state) {
|
|
@@ -1609,6 +2181,7 @@ class AgentManager extends EventEmitter {
|
|
|
1609
2181
|
const providerSessionProvider = metadata.providerSessionProvider || metadata.provider || '';
|
|
1610
2182
|
const providerSessionId = metadata.providerSessionId || metadata.codexAppServerThreadId || '';
|
|
1611
2183
|
const runtimeBinding = runtimeBindingFor(runtimeKind(metadata), metadata);
|
|
2184
|
+
const agentRecordId = metadata.agentRecordId || metadata.persistentSessionId || metadata.id || '';
|
|
1612
2185
|
return {
|
|
1613
2186
|
id: agentId,
|
|
1614
2187
|
command: metadata.forkCommand || metadata.command || '',
|
|
@@ -1646,6 +2219,16 @@ class AgentManager extends EventEmitter {
|
|
|
1646
2219
|
providerSessionTitle: metadata.providerSessionTitle || '',
|
|
1647
2220
|
providerSessionWorkspace: metadata.providerSessionWorkspace || '',
|
|
1648
2221
|
terminalInputReceived: metadata.terminalInputReceived === true,
|
|
2222
|
+
structuredRuntimeProcess: metadata.structuredRuntimeProcess
|
|
2223
|
+
&& typeof metadata.structuredRuntimeProcess === 'object'
|
|
2224
|
+
? JSON.parse(JSON.stringify(metadata.structuredRuntimeProcess))
|
|
2225
|
+
: null,
|
|
2226
|
+
legacyAcpProcessExitAcknowledgedAt:
|
|
2227
|
+
typeof metadata.legacyAcpProcessExitAcknowledgedAt === 'number'
|
|
2228
|
+
? metadata.legacyAcpProcessExitAcknowledgedAt
|
|
2229
|
+
: null,
|
|
2230
|
+
requiresProcessExitAcknowledgement:
|
|
2231
|
+
requiresLegacyAcpExitAcknowledgement(metadata),
|
|
1649
2232
|
// Legacy App Server records normalize to ACP at the runtime-binding
|
|
1650
2233
|
// boundary. Codex thread ids are valid ACP session ids, so the existing
|
|
1651
2234
|
// conversation remains recoverable without restarting App Server.
|
|
@@ -1655,7 +2238,9 @@ class AgentManager extends EventEmitter {
|
|
|
1655
2238
|
restartedFromAgentIds: Array.isArray(metadata.restartedFromAgentIds)
|
|
1656
2239
|
? metadata.restartedFromAgentIds.filter(id => typeof id === 'string' && id)
|
|
1657
2240
|
: [],
|
|
1658
|
-
|
|
2241
|
+
agentRecordId,
|
|
2242
|
+
persistentSessionId: agentRecordId,
|
|
2243
|
+
lifecycleJournal: lifecycleJournal(metadata),
|
|
1659
2244
|
customTitle: metadata.customTitle || '',
|
|
1660
2245
|
terminalBusy: typeof state.terminalBusy === 'boolean' ? state.terminalBusy : null,
|
|
1661
2246
|
shellCwd: state.shellCwd || metadata.cwd || '',
|
|
@@ -1750,24 +2335,25 @@ class AgentManager extends EventEmitter {
|
|
|
1750
2335
|
if (!agent || !this.configManager || typeof this.configManager.ensureAgentSessionRecord !== 'function') {
|
|
1751
2336
|
return '';
|
|
1752
2337
|
}
|
|
1753
|
-
|
|
2338
|
+
this.assertPersistentAgentRuntimeOwner(agent);
|
|
2339
|
+
const previousAgentRecordId = agent.agentRecordId || agent.persistentSessionId || '';
|
|
1754
2340
|
const sessionOptions = agent.providerSessionKey
|
|
1755
2341
|
? this.acpSessionOptionsByKey.get(agent.providerSessionKey)
|
|
1756
2342
|
: null;
|
|
1757
|
-
const
|
|
2343
|
+
const agentRecordId = this.configManager.ensureAgentSessionRecord(agent, {
|
|
1758
2344
|
...(sessionOptions ? {
|
|
1759
2345
|
acpAdditionalDirectories: [...sessionOptions.additionalDirectories],
|
|
1760
2346
|
acpMcpServers: JSON.parse(JSON.stringify(sessionOptions.mcpServers)),
|
|
1761
2347
|
} : {}),
|
|
1762
2348
|
...patch,
|
|
1763
2349
|
});
|
|
1764
|
-
if (
|
|
1765
|
-
agent
|
|
2350
|
+
if (agentRecordId) {
|
|
2351
|
+
setAgentRecordId(agent, agentRecordId);
|
|
1766
2352
|
}
|
|
1767
2353
|
if (
|
|
1768
|
-
|
|
1769
|
-
&&
|
|
1770
|
-
&&
|
|
2354
|
+
agentRecordId
|
|
2355
|
+
&& previousAgentRecordId
|
|
2356
|
+
&& agentRecordId !== previousAgentRecordId
|
|
1771
2357
|
&& agent.providerSessionKey
|
|
1772
2358
|
&& typeof this.configManager.getAgentSessionRecordForProviderSessionKey === 'function'
|
|
1773
2359
|
) {
|
|
@@ -1794,7 +2380,214 @@ class AgentManager extends EventEmitter {
|
|
|
1794
2380
|
agent.unread = agentAttentionUnread(agent);
|
|
1795
2381
|
}
|
|
1796
2382
|
}
|
|
1797
|
-
return
|
|
2383
|
+
return agentRecordId;
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
assertPersistentAgentRuntimeOwner(agent) {
|
|
2387
|
+
if (
|
|
2388
|
+
!agent?.providerSessionKey
|
|
2389
|
+
|| typeof this.configManager?.getAgentSessionRecordForProviderSessionKey !== 'function'
|
|
2390
|
+
) {
|
|
2391
|
+
return;
|
|
2392
|
+
}
|
|
2393
|
+
const canonical = this.configManager.getAgentSessionRecordForProviderSessionKey(
|
|
2394
|
+
agent.providerSessionKey,
|
|
2395
|
+
);
|
|
2396
|
+
const currentOwner = String(canonical?.runtimeAgentId || '').trim();
|
|
2397
|
+
const requestedOwner = String(agent.id || '').trim();
|
|
2398
|
+
if (!currentOwner || currentOwner === requestedOwner) return;
|
|
2399
|
+
|
|
2400
|
+
const ownerAgent = this.agents.get(currentOwner);
|
|
2401
|
+
if (!ownerAgent && !this.recoveryComplete) {
|
|
2402
|
+
throw new Error(
|
|
2403
|
+
`Agent session ${agent.providerSessionKey} ownership cannot be changed before recovery completes`,
|
|
2404
|
+
);
|
|
2405
|
+
}
|
|
2406
|
+
const ownerIsStopped = !ownerAgent
|
|
2407
|
+
|| this.verifiedStoppedAgentIds.has(currentOwner)
|
|
2408
|
+
|| ['dead', 'stopped'].includes(String(ownerAgent.status || ''))
|
|
2409
|
+
|| ownerAgent.engineStatus === 'exited';
|
|
2410
|
+
if (ownerIsStopped) return;
|
|
2411
|
+
|
|
2412
|
+
throw new Error(
|
|
2413
|
+
`Agent session ${agent.providerSessionKey} is owned by Runtime ${currentOwner}, not ${requestedOwner}`,
|
|
2414
|
+
);
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
beginPersistentAgentOperation(agent, type, requestKey, request = {}) {
|
|
2418
|
+
const previousJournal = agent.lifecycleJournal
|
|
2419
|
+
? JSON.parse(JSON.stringify(agent.lifecycleJournal))
|
|
2420
|
+
: null;
|
|
2421
|
+
const result = beginLifecycleOperation(agent, type, requestKey, request);
|
|
2422
|
+
if (result.conflict) {
|
|
2423
|
+
return {
|
|
2424
|
+
error: `Agent operation ${result.conflict.id} (${result.conflict.type}) has not reached a terminal state`,
|
|
2425
|
+
conflict: result.conflict,
|
|
2426
|
+
};
|
|
2427
|
+
}
|
|
2428
|
+
if (result.joined && result.operation.state === 'blocked') {
|
|
2429
|
+
transitionLifecycleOperation(agent, result.operation.id, 'pending');
|
|
2430
|
+
}
|
|
2431
|
+
try {
|
|
2432
|
+
const persistentSessionId = this.ensurePersistentAgentSession(agent);
|
|
2433
|
+
if (
|
|
2434
|
+
typeof this.configManager?.ensureAgentSessionRecord === 'function'
|
|
2435
|
+
&& !persistentSessionId
|
|
2436
|
+
) {
|
|
2437
|
+
throw new Error('Agent session store did not return a persistent id');
|
|
2438
|
+
}
|
|
2439
|
+
} catch (error) {
|
|
2440
|
+
if (previousJournal) agent.lifecycleJournal = previousJournal;
|
|
2441
|
+
else delete agent.lifecycleJournal;
|
|
2442
|
+
return { error: `Failed to persist Agent ${type} intent: ${error.message || error}` };
|
|
2443
|
+
}
|
|
2444
|
+
return {
|
|
2445
|
+
operation: activeLifecycleOperation(agent) || result.operation,
|
|
2446
|
+
joined: result.joined,
|
|
2447
|
+
};
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
transitionPersistentAgentOperation(agent, operationId, state, error = '', patch = {}) {
|
|
2451
|
+
const previousJournal = agent.lifecycleJournal
|
|
2452
|
+
? JSON.parse(JSON.stringify(agent.lifecycleJournal))
|
|
2453
|
+
: null;
|
|
2454
|
+
const operation = transitionLifecycleOperation(agent, operationId, state, error);
|
|
2455
|
+
if (!operation) throw new Error(`Agent operation ${operationId} was not found`);
|
|
2456
|
+
try {
|
|
2457
|
+
const persistentSessionId = this.ensurePersistentAgentSession(agent, patch);
|
|
2458
|
+
if (
|
|
2459
|
+
typeof this.configManager?.ensureAgentSessionRecord === 'function'
|
|
2460
|
+
&& !persistentSessionId
|
|
2461
|
+
) {
|
|
2462
|
+
throw new Error('Agent session store did not return a persistent id');
|
|
2463
|
+
}
|
|
2464
|
+
} catch (persistError) {
|
|
2465
|
+
if (previousJournal) agent.lifecycleJournal = previousJournal;
|
|
2466
|
+
else delete agent.lifecycleJournal;
|
|
2467
|
+
throw persistError;
|
|
2468
|
+
}
|
|
2469
|
+
return operation;
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
beginPersistentAgentUpdate(agent, requestKey, request) {
|
|
2473
|
+
const latest = latestLifecycleOperation(agent);
|
|
2474
|
+
if (
|
|
2475
|
+
latest?.type === 'update'
|
|
2476
|
+
&& latest.state === 'succeeded'
|
|
2477
|
+
&& latest.requestKey === requestKey
|
|
2478
|
+
) {
|
|
2479
|
+
return { operation: latest, deduplicated: true };
|
|
2480
|
+
}
|
|
2481
|
+
return this.beginPersistentAgentOperation(agent, 'update', requestKey, request);
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
reconcilePersistedAgentUpdate(agent) {
|
|
2485
|
+
const operation = activeLifecycleOperation(agent);
|
|
2486
|
+
if (operation?.type !== 'update') return null;
|
|
2487
|
+
const request = operation.request || {};
|
|
2488
|
+
if (Object.prototype.hasOwnProperty.call(request, 'customTitle')) {
|
|
2489
|
+
return this.renameAgent(agent.id, request.customTitle);
|
|
2490
|
+
}
|
|
2491
|
+
if (Object.prototype.hasOwnProperty.call(request, 'task')) {
|
|
2492
|
+
return this.setAgentTask(agent.id, request.task);
|
|
2493
|
+
}
|
|
2494
|
+
return this.updateAgentFlags(agent.id, request);
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
async replayPersistentCreateRequest(createRequestId, requestSignature = '') {
|
|
2498
|
+
if (!this.configManager || typeof this.configManager.listAgentSessionRecords !== 'function') {
|
|
2499
|
+
return null;
|
|
2500
|
+
}
|
|
2501
|
+
const requestKey = `create-request:${String(createRequestId || '').trim().slice(0, 160)}`;
|
|
2502
|
+
if (requestKey === 'create-request:') return null;
|
|
2503
|
+
const matches = this.configManager.listAgentSessionRecords()
|
|
2504
|
+
.flatMap(record => {
|
|
2505
|
+
const entries = lifecycleJournal(record).entries;
|
|
2506
|
+
return entries.map((operation, index) => ({ record, entries, operation, index }));
|
|
2507
|
+
})
|
|
2508
|
+
.filter(item => item.operation.type === 'create' && item.operation.requestKey === requestKey)
|
|
2509
|
+
.sort((left, right) => (
|
|
2510
|
+
(Number(right.operation.updatedAt) || 0) - (Number(left.operation.updatedAt) || 0)
|
|
2511
|
+
));
|
|
2512
|
+
const match = matches[0];
|
|
2513
|
+
if (!match) return null;
|
|
2514
|
+
if (
|
|
2515
|
+
match.operation.request?.signature
|
|
2516
|
+
&& match.operation.request.signature !== requestSignature
|
|
2517
|
+
) {
|
|
2518
|
+
return {
|
|
2519
|
+
error: `Create request ${createRequestId} was already used for different Agent parameters`,
|
|
2520
|
+
};
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
const agentId = String(
|
|
2524
|
+
match.operation.request?.agentId
|
|
2525
|
+
|| match.record.runtimeAgentId
|
|
2526
|
+
|| '',
|
|
2527
|
+
).trim();
|
|
2528
|
+
const inFlight = agentId ? this.agentLifecycleOperations.get(agentId) : null;
|
|
2529
|
+
if (inFlight && !TERMINAL_OPERATION_STATES.has(match.operation.state)) {
|
|
2530
|
+
await inFlight.promise.catch(() => {});
|
|
2531
|
+
return this.replayPersistentCreateRequest(createRequestId, requestSignature);
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
const removedLater = match.entries.slice(match.index + 1).some(operation => (
|
|
2535
|
+
['delete', 'archive'].includes(operation.type)
|
|
2536
|
+
&& operation.state === 'succeeded'
|
|
2537
|
+
));
|
|
2538
|
+
if (removedLater) {
|
|
2539
|
+
return {
|
|
2540
|
+
error: `Create request ${createRequestId} already completed, but its Agent was later removed`,
|
|
2541
|
+
};
|
|
2542
|
+
}
|
|
2543
|
+
if (match.operation.state === 'succeeded') {
|
|
2544
|
+
if (agentId && this.agents.has(agentId)) {
|
|
2545
|
+
return {
|
|
2546
|
+
agentId,
|
|
2547
|
+
deduplicated: true,
|
|
2548
|
+
createResult: match.operation.result,
|
|
2549
|
+
};
|
|
2550
|
+
}
|
|
2551
|
+
return {
|
|
2552
|
+
error: `Create request ${createRequestId} already succeeded, but its Runtime is not available`,
|
|
2553
|
+
};
|
|
2554
|
+
}
|
|
2555
|
+
if (TERMINAL_OPERATION_STATES.has(match.operation.state)) {
|
|
2556
|
+
return {
|
|
2557
|
+
error: match.operation.error
|
|
2558
|
+
|| `Create request ${createRequestId} already finished with state ${match.operation.state}`,
|
|
2559
|
+
};
|
|
2560
|
+
}
|
|
2561
|
+
return {
|
|
2562
|
+
agentId,
|
|
2563
|
+
error: match.operation.error
|
|
2564
|
+
|| `Create request ${createRequestId} is awaiting lifecycle recovery`,
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
recordCreateRequestResult(agentId, createRequestId, result) {
|
|
2569
|
+
const agent = this.agents.get(agentId);
|
|
2570
|
+
if (!agent) return { error: 'Agent not found' };
|
|
2571
|
+
const requestKey = `create-request:${String(createRequestId || '').trim().slice(0, 160)}`;
|
|
2572
|
+
const operation = lifecycleJournal(agent).entries.find(candidate => (
|
|
2573
|
+
candidate.type === 'create'
|
|
2574
|
+
&& candidate.requestKey === requestKey
|
|
2575
|
+
&& candidate.state === 'succeeded'
|
|
2576
|
+
));
|
|
2577
|
+
if (!operation) return { error: 'Create operation was not found' };
|
|
2578
|
+
const staged = {
|
|
2579
|
+
...agent,
|
|
2580
|
+
lifecycleJournal: lifecycleJournal(agent),
|
|
2581
|
+
};
|
|
2582
|
+
setLifecycleOperationResult(staged, operation.id, result);
|
|
2583
|
+
try {
|
|
2584
|
+
this.ensurePersistentAgentSession(staged);
|
|
2585
|
+
} catch (error) {
|
|
2586
|
+
return { error: `Failed to persist Create result: ${error.message || error}` };
|
|
2587
|
+
}
|
|
2588
|
+
agent.lifecycleJournal = staged.lifecycleJournal;
|
|
2589
|
+
setAgentRecordId(agent, staged.agentRecordId || staged.persistentSessionId || '');
|
|
2590
|
+
return { agentId, operationId: operation.id, result };
|
|
1798
2591
|
}
|
|
1799
2592
|
|
|
1800
2593
|
rememberMainPageProviderSession(agent) {
|
|
@@ -1803,6 +2596,7 @@ class AgentManager extends EventEmitter {
|
|
|
1803
2596
|
if (!this.configManager) {
|
|
1804
2597
|
return;
|
|
1805
2598
|
}
|
|
2599
|
+
this.assertPersistentAgentRuntimeOwner(agent);
|
|
1806
2600
|
|
|
1807
2601
|
const sessionKey = mainPageAgentSessionKey(agent.providerSessionProvider, agent.providerSessionId, agent.providerHomeId || '');
|
|
1808
2602
|
if (!sessionKey) return;
|
|
@@ -1813,7 +2607,7 @@ class AgentManager extends EventEmitter {
|
|
|
1813
2607
|
}
|
|
1814
2608
|
if (typeof this.configManager.rememberAgentSessionRecord === 'function') {
|
|
1815
2609
|
const persistentSessionId = this.configManager.rememberAgentSessionRecord(agent);
|
|
1816
|
-
|
|
2610
|
+
setAgentRecordId(agent, persistentSessionId);
|
|
1817
2611
|
return;
|
|
1818
2612
|
}
|
|
1819
2613
|
this.setMainPageSessionKeys([
|
|
@@ -2313,17 +3107,99 @@ class AgentManager extends EventEmitter {
|
|
|
2313
3107
|
}, interval);
|
|
2314
3108
|
}
|
|
2315
3109
|
|
|
2316
|
-
|
|
2317
|
-
if (this.disposed) return;
|
|
2318
|
-
this.
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
3110
|
+
dispose(options = {}) {
|
|
3111
|
+
if (this.disposed) return Promise.resolve();
|
|
3112
|
+
if (this.disposePromise) return this.disposePromise;
|
|
3113
|
+
|
|
3114
|
+
this.disposing = true;
|
|
3115
|
+
const disposePromise = this.performDispose(options);
|
|
3116
|
+
this.disposePromise = disposePromise;
|
|
3117
|
+
void disposePromise.finally(() => {
|
|
3118
|
+
if (this.disposePromise === disposePromise) this.disposePromise = null;
|
|
3119
|
+
if (!this.disposed && !this.disposeFrozen) this.disposing = false;
|
|
3120
|
+
}).catch(() => {});
|
|
3121
|
+
return disposePromise;
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
async performDispose(options = {}) {
|
|
3125
|
+
await this.recoveryPromise;
|
|
3126
|
+
await this.drainAcceptedAgentOperations();
|
|
3127
|
+
|
|
3128
|
+
const jsonBindingIds = new Set(this.jsonCliRuntime?.bindings?.keys?.() || []);
|
|
3129
|
+
const acpBindingIds = new Set(this.acpRuntime?.bindings?.keys?.() || []);
|
|
3130
|
+
const runtimeCleanupFailures = [];
|
|
3131
|
+
let jsonCleanupFailed = false;
|
|
3132
|
+
let acpCleanupFailed = false;
|
|
3133
|
+
if (this.jsonCliRuntime && typeof this.jsonCliRuntime.dispose === 'function') {
|
|
3134
|
+
try {
|
|
3135
|
+
await this.jsonCliRuntime.dispose();
|
|
3136
|
+
} catch (error) {
|
|
3137
|
+
jsonCleanupFailed = true;
|
|
3138
|
+
runtimeCleanupFailures.push(error);
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
if (this.acpRuntime && typeof this.acpRuntime.dispose === 'function') {
|
|
3142
|
+
try {
|
|
3143
|
+
await this.acpRuntime.dispose();
|
|
3144
|
+
} catch (error) {
|
|
3145
|
+
acpCleanupFailed = true;
|
|
3146
|
+
runtimeCleanupFailures.push(error);
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
let agentStateChanged = false;
|
|
3151
|
+
const forgetStoppedStructuredAgents = (agentIds, runtime, kind) => {
|
|
3152
|
+
for (const agentId of agentIds) {
|
|
3153
|
+
if (runtime?.bindings?.has?.(agentId)) continue;
|
|
3154
|
+
const agent = this.agents.get(agentId);
|
|
3155
|
+
if (!agent || runtimeKind(agent) !== kind) continue;
|
|
3156
|
+
this.forgetStoppedAgentRecord(agentId, { emitUpdate: false });
|
|
3157
|
+
agentStateChanged = true;
|
|
3158
|
+
}
|
|
3159
|
+
};
|
|
3160
|
+
forgetStoppedStructuredAgents(jsonBindingIds, this.jsonCliRuntime, 'json');
|
|
3161
|
+
forgetStoppedStructuredAgents(acpBindingIds, this.acpRuntime, 'acp');
|
|
3162
|
+
const markUncertainStructuredAgents = (agentIds, runtime, kind, failed) => {
|
|
3163
|
+
if (!failed) return;
|
|
3164
|
+
for (const agentId of agentIds) {
|
|
3165
|
+
if (!runtime?.bindings?.has?.(agentId)) continue;
|
|
3166
|
+
if (runtimeKind(this.agents.get(agentId)) !== kind) continue;
|
|
3167
|
+
this.markStructuredAgentCleanupUncertain(
|
|
3168
|
+
agentId,
|
|
3169
|
+
kind,
|
|
3170
|
+
`${kind.toUpperCase()} runtime cleanup could not prove process exit`,
|
|
3171
|
+
{ emitUpdate: false },
|
|
3172
|
+
);
|
|
3173
|
+
agentStateChanged = true;
|
|
3174
|
+
}
|
|
3175
|
+
};
|
|
3176
|
+
markUncertainStructuredAgents(jsonBindingIds, this.jsonCliRuntime, 'json', jsonCleanupFailed);
|
|
3177
|
+
markUncertainStructuredAgents(acpBindingIds, this.acpRuntime, 'acp', acpCleanupFailed);
|
|
3178
|
+
if (agentStateChanged) this.emit('update');
|
|
3179
|
+
|
|
3180
|
+
if (runtimeCleanupFailures.length > 0) {
|
|
3181
|
+
this.jsonCliRuntime?.resumeAfterDisposeAbort?.();
|
|
3182
|
+
this.acpRuntime?.resumeAfterDisposeAbort?.();
|
|
3183
|
+
throw new AggregateError(runtimeCleanupFailures, 'Agent runtime cleanup could not be verified');
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
this.disposeFrozen = true;
|
|
3187
|
+
if (this.engineBridge && typeof this.engineBridge.dispose === 'function') {
|
|
3188
|
+
await this.engineBridge.dispose({
|
|
3189
|
+
preserveHost: options.preserveTerminalHost === true,
|
|
3190
|
+
});
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3193
|
+
if (this.heartbeatInterval) {
|
|
3194
|
+
clearInterval(this.heartbeatInterval);
|
|
3195
|
+
this.heartbeatInterval = null;
|
|
2322
3196
|
}
|
|
2323
3197
|
this.providerSessionService.dispose();
|
|
2324
3198
|
this.agentWorktreeResolveGeneration.clear();
|
|
2325
|
-
this.
|
|
2326
|
-
this.
|
|
3199
|
+
this.agentLifecycleOperations.clear();
|
|
3200
|
+
this.agentStartAdmissions.clear();
|
|
3201
|
+
this.activeInputOperations.clear();
|
|
3202
|
+
this.verifiedStoppedAgentIds.clear();
|
|
2327
3203
|
this.permissionRestartSuppressedAgentIds.clear();
|
|
2328
3204
|
this.pendingResizeByAgent.clear();
|
|
2329
3205
|
this.resizeDrains.clear();
|
|
@@ -2332,14 +3208,22 @@ class AgentManager extends EventEmitter {
|
|
|
2332
3208
|
this.codexTerminalStartQueues.clear();
|
|
2333
3209
|
this.codexTerminalStartOutput.clear();
|
|
2334
3210
|
this.acpSessionOptionsByKey.clear();
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
3211
|
+
this.disposed = true;
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
async drainAcceptedAgentOperations() {
|
|
3215
|
+
while (true) {
|
|
3216
|
+
const pending = new Set();
|
|
3217
|
+
for (const entry of new Set(this.agentLifecycleOperations.values())) {
|
|
3218
|
+
if (entry?.promise) pending.add(entry.promise);
|
|
3219
|
+
}
|
|
3220
|
+
for (const entry of this.agentStartAdmissions.values()) {
|
|
3221
|
+
if (entry?.promise) pending.add(entry.promise);
|
|
3222
|
+
}
|
|
3223
|
+
for (const operation of this.activeInputOperations) pending.add(operation);
|
|
3224
|
+
for (const operation of this.resizeDrains.values()) pending.add(operation);
|
|
3225
|
+
if (pending.size === 0) return;
|
|
3226
|
+
await Promise.allSettled([...pending]);
|
|
2343
3227
|
}
|
|
2344
3228
|
}
|
|
2345
3229
|
|
|
@@ -2471,9 +3355,134 @@ class AgentManager extends EventEmitter {
|
|
|
2471
3355
|
}
|
|
2472
3356
|
}
|
|
2473
3357
|
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
3358
|
+
startAgent(command, customWorkspace, callback, options = {}) {
|
|
3359
|
+
const lifecycleEntry = options.lifecycleToken
|
|
3360
|
+
? [...new Set(this.agentLifecycleOperations.values())]
|
|
3361
|
+
.find(entry => entry.token === options.lifecycleToken)
|
|
3362
|
+
: null;
|
|
3363
|
+
if (this.disposing && !lifecycleEntry) {
|
|
3364
|
+
const error = 'Farming is shutting down; new Agents are not accepted';
|
|
3365
|
+
if (callback) callback(null, error);
|
|
3366
|
+
return Promise.resolve(null);
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
const createRequestId = typeof options.createRequestId === 'string'
|
|
3370
|
+
? options.createRequestId.trim().slice(0, 160)
|
|
3371
|
+
: '';
|
|
3372
|
+
const createRequestSignature = createOperationSignature(command, customWorkspace, options);
|
|
3373
|
+
const existingCreateRequest = createRequestId
|
|
3374
|
+
? this.createRequestAdmissions.get(createRequestId)
|
|
3375
|
+
: null;
|
|
3376
|
+
if (existingCreateRequest) {
|
|
3377
|
+
if (existingCreateRequest.signature !== createRequestSignature) {
|
|
3378
|
+
const error = `Create request ${createRequestId} is already in progress with different Agent parameters`;
|
|
3379
|
+
if (callback) callback(null, error);
|
|
3380
|
+
return Promise.resolve(null);
|
|
3381
|
+
}
|
|
3382
|
+
return existingCreateRequest.promise.then(outcome => {
|
|
3383
|
+
if (callback) {
|
|
3384
|
+
callback(
|
|
3385
|
+
outcome.agentId || null,
|
|
3386
|
+
outcome.error || null,
|
|
3387
|
+
{
|
|
3388
|
+
...(outcome.metadata || {}),
|
|
3389
|
+
deduplicated: true,
|
|
3390
|
+
},
|
|
3391
|
+
);
|
|
3392
|
+
}
|
|
3393
|
+
return outcome.agentId || null;
|
|
3394
|
+
});
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3397
|
+
let resolveAdmission;
|
|
3398
|
+
const token = Symbol('agent-start-admission');
|
|
3399
|
+
const promise = new Promise(resolve => {
|
|
3400
|
+
resolveAdmission = resolve;
|
|
3401
|
+
});
|
|
3402
|
+
const requestedProjectWorkspace = options.wantsMain === true
|
|
3403
|
+
? ''
|
|
3404
|
+
: (typeof options.projectWorkspace === 'string' && options.projectWorkspace.trim()
|
|
3405
|
+
? options.projectWorkspace
|
|
3406
|
+
: customWorkspace);
|
|
3407
|
+
const admission = {
|
|
3408
|
+
token,
|
|
3409
|
+
promise,
|
|
3410
|
+
workspaceKey: requestedProjectWorkspace
|
|
3411
|
+
? canonicalWorkspacePath(this.expandWorkspacePath(requestedProjectWorkspace))
|
|
3412
|
+
: '',
|
|
3413
|
+
};
|
|
3414
|
+
this.agentStartAdmissions.set(token, admission);
|
|
3415
|
+
let callbackOutcome = null;
|
|
3416
|
+
const reportStart = (agentId, error, metadata = {}) => {
|
|
3417
|
+
callbackOutcome = {
|
|
3418
|
+
agentId: agentId || null,
|
|
3419
|
+
error: error || null,
|
|
3420
|
+
metadata,
|
|
3421
|
+
};
|
|
3422
|
+
if (callback) callback(agentId, error, metadata);
|
|
3423
|
+
};
|
|
3424
|
+
const startPromise = this.startAgentAdmitted(command, customWorkspace, reportStart, {
|
|
3425
|
+
...options,
|
|
3426
|
+
startAdmissionToken: token,
|
|
3427
|
+
});
|
|
3428
|
+
const admittedPromise = Promise.resolve(startPromise).finally(() => {
|
|
3429
|
+
if (this.agentStartAdmissions.get(token) === admission) {
|
|
3430
|
+
this.agentStartAdmissions.delete(token);
|
|
3431
|
+
}
|
|
3432
|
+
resolveAdmission();
|
|
3433
|
+
});
|
|
3434
|
+
if (!createRequestId) return admittedPromise;
|
|
3435
|
+
|
|
3436
|
+
const outcomePromise = admittedPromise.then(agentId => (
|
|
3437
|
+
callbackOutcome || {
|
|
3438
|
+
agentId: agentId || null,
|
|
3439
|
+
error: agentId ? null : 'Failed to start Agent',
|
|
3440
|
+
}
|
|
3441
|
+
));
|
|
3442
|
+
const requestAdmission = {
|
|
3443
|
+
signature: createRequestSignature,
|
|
3444
|
+
promise: outcomePromise,
|
|
3445
|
+
};
|
|
3446
|
+
this.createRequestAdmissions.set(createRequestId, requestAdmission);
|
|
3447
|
+
void outcomePromise.finally(() => {
|
|
3448
|
+
if (this.createRequestAdmissions.get(createRequestId) === requestAdmission) {
|
|
3449
|
+
this.createRequestAdmissions.delete(createRequestId);
|
|
3450
|
+
}
|
|
3451
|
+
}).catch(() => {});
|
|
3452
|
+
return outcomePromise.then(outcome => outcome.agentId || null);
|
|
3453
|
+
}
|
|
3454
|
+
|
|
3455
|
+
async startAgentAdmitted(command, customWorkspace, callback, options = {}) {
|
|
3456
|
+
const createRequestId = typeof options.createRequestId === 'string'
|
|
3457
|
+
? options.createRequestId.trim().slice(0, 160)
|
|
3458
|
+
: '';
|
|
3459
|
+
if (options.skipRecoveryWait !== true) {
|
|
3460
|
+
try {
|
|
3461
|
+
await this.whenRecovered();
|
|
3462
|
+
} catch (error) {
|
|
3463
|
+
if (callback) callback(null, error.message || String(error));
|
|
3464
|
+
return null;
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
const createRequestSignature = createOperationSignature(command, customWorkspace, options);
|
|
3468
|
+
if (createRequestId) {
|
|
3469
|
+
const replay = await this.replayPersistentCreateRequest(
|
|
3470
|
+
createRequestId,
|
|
3471
|
+
createRequestSignature,
|
|
3472
|
+
);
|
|
3473
|
+
if (replay) {
|
|
3474
|
+
if (callback) {
|
|
3475
|
+
callback(
|
|
3476
|
+
replay.agentId || null,
|
|
3477
|
+
replay.error || null,
|
|
3478
|
+
{
|
|
3479
|
+
deduplicated: replay.deduplicated === true,
|
|
3480
|
+
createResult: replay.createResult || null,
|
|
3481
|
+
},
|
|
3482
|
+
);
|
|
3483
|
+
}
|
|
3484
|
+
return replay.agentId || null;
|
|
3485
|
+
}
|
|
2477
3486
|
}
|
|
2478
3487
|
|
|
2479
3488
|
const wantsMain = options.wantsMain === true || (options.wantsMain !== false && !this.mainAgentId);
|
|
@@ -2721,6 +3730,18 @@ class AgentManager extends EventEmitter {
|
|
|
2721
3730
|
}
|
|
2722
3731
|
}
|
|
2723
3732
|
}
|
|
3733
|
+
|
|
3734
|
+
const projectWorkspaceKey = canonicalWorkspacePath(projectWorkspace);
|
|
3735
|
+
const startAdmission = this.agentStartAdmissions.get(options.startAdmissionToken);
|
|
3736
|
+
if (startAdmission) startAdmission.workspaceKey = projectWorkspaceKey;
|
|
3737
|
+
const deletingProjectWorkspace = projectWorkspaceKey
|
|
3738
|
+
? [...this.projectWorkspaceDeleteAdmissions.keys()]
|
|
3739
|
+
.find(candidate => isSameOrDescendantPath(candidate, projectWorkspaceKey))
|
|
3740
|
+
: '';
|
|
3741
|
+
if (deletingProjectWorkspace) {
|
|
3742
|
+
if (callback) callback(null, `Project worktree is being deleted: ${projectWorkspace}`);
|
|
3743
|
+
return null;
|
|
3744
|
+
}
|
|
2724
3745
|
|
|
2725
3746
|
if (!fs.existsSync(workspace)) {
|
|
2726
3747
|
console.log('Workspace does not exist:', workspace);
|
|
@@ -2786,6 +3807,18 @@ class AgentManager extends EventEmitter {
|
|
|
2786
3807
|
const requestedAgentRuntimeMode = ['json', 'acp', 'chat'].includes(options.agentRuntimeMode)
|
|
2787
3808
|
? options.agentRuntimeMode
|
|
2788
3809
|
: 'terminal';
|
|
3810
|
+
if (
|
|
3811
|
+
requestedAgentRuntimeMode === 'json'
|
|
3812
|
+
&& options.allowLegacyJsonRuntime !== true
|
|
3813
|
+
) {
|
|
3814
|
+
if (callback) {
|
|
3815
|
+
callback(
|
|
3816
|
+
null,
|
|
3817
|
+
'JSON CLI Chat is a legacy compatibility reader and cannot create a new Agent. Use Chat (ACP) or Terminal.',
|
|
3818
|
+
);
|
|
3819
|
+
}
|
|
3820
|
+
return null;
|
|
3821
|
+
}
|
|
2789
3822
|
// A fresh structured runtime does not need a provider CLI resume id yet:
|
|
2790
3823
|
// ACP/JSON creates the provider session and writes the resulting id back
|
|
2791
3824
|
// after connecting. Fresh Terminal sessions are handled separately below.
|
|
@@ -2836,14 +3869,23 @@ class AgentManager extends EventEmitter {
|
|
|
2836
3869
|
providerSessionProvider: useAcp || useJsonCli ? structuredRuntimeProvider : (providerSessionPlan.provider || ''),
|
|
2837
3870
|
providerHomeId: resolvedProviderHomeId,
|
|
2838
3871
|
providerHomePath,
|
|
2839
|
-
providerSessionId: providerSessionPlan.id || '',
|
|
2840
|
-
providerSessionKey:
|
|
2841
|
-
|
|
3872
|
+
providerSessionId: acpGeneratedFreshSession ? '' : (providerSessionPlan.id || ''),
|
|
3873
|
+
providerSessionKey: acpGeneratedFreshSession
|
|
3874
|
+
? ''
|
|
3875
|
+
: mainPageAgentSessionKey(
|
|
3876
|
+
providerSessionPlan.provider,
|
|
3877
|
+
providerSessionPlan.id,
|
|
3878
|
+
providerHome ? providerHome.id : providerHomeId,
|
|
3879
|
+
),
|
|
3880
|
+
providerSessionTemporary: acpGeneratedFreshSession || providerSessionPlan.temporary === true,
|
|
2842
3881
|
providerSessionSource: providerSessionPlan.source || '',
|
|
2843
|
-
providerSessionResolvedAt: providerSessionPlan.temporary === true
|
|
3882
|
+
providerSessionResolvedAt: acpGeneratedFreshSession || providerSessionPlan.temporary === true
|
|
3883
|
+
? null
|
|
3884
|
+
: Date.now(),
|
|
2844
3885
|
providerSessionTitle: typeof options.providerSessionTitle === 'string' ? options.providerSessionTitle.trim().slice(0, 160) : '',
|
|
2845
3886
|
providerSessionWorkspace: '',
|
|
2846
3887
|
terminalInputReceived: false,
|
|
3888
|
+
structuredRuntimeProcess: null,
|
|
2847
3889
|
runtimeBinding: useAcp
|
|
2848
3890
|
? runtimeBindingFor('acp', { state: 'connecting' })
|
|
2849
3891
|
: (useJsonCli
|
|
@@ -2852,7 +3894,9 @@ class AgentManager extends EventEmitter {
|
|
|
2852
3894
|
events: Array.isArray(options.jsonCliEvents) ? options.jsonCliEvents : [],
|
|
2853
3895
|
})
|
|
2854
3896
|
: runtimeBindingFor('terminal')),
|
|
2855
|
-
forkedFromProviderSessionId:
|
|
3897
|
+
forkedFromProviderSessionId: typeof options.forkedFromProviderSessionId === 'string'
|
|
3898
|
+
? options.forkedFromProviderSessionId
|
|
3899
|
+
: (providerSessionPlan.forkedFromProviderSessionId || ''),
|
|
2856
3900
|
restartedFromAgentId: typeof options.restartedFromAgentId === 'string' ? options.restartedFromAgentId : '',
|
|
2857
3901
|
restartedFromAgentIds: Array.isArray(options.restartedFromAgentIds)
|
|
2858
3902
|
? Array.from(new Set(options.restartedFromAgentIds.filter(id => typeof id === 'string' && id)))
|
|
@@ -2860,7 +3904,12 @@ class AgentManager extends EventEmitter {
|
|
|
2860
3904
|
runtimeSwitchVerifiedSessionId: typeof options.runtimeSwitchVerifiedSessionId === 'string'
|
|
2861
3905
|
? options.runtimeSwitchVerifiedSessionId
|
|
2862
3906
|
: '',
|
|
2863
|
-
|
|
3907
|
+
agentRecordId: typeof options.agentRecordId === 'string'
|
|
3908
|
+
? options.agentRecordId
|
|
3909
|
+
: (typeof options.persistentSessionId === 'string' ? options.persistentSessionId : ''),
|
|
3910
|
+
persistentSessionId: typeof options.agentRecordId === 'string'
|
|
3911
|
+
? options.agentRecordId
|
|
3912
|
+
: (typeof options.persistentSessionId === 'string' ? options.persistentSessionId : ''),
|
|
2864
3913
|
customTitle: typeof options.customTitle === 'string' ? options.customTitle.trim().slice(0, 80) : '',
|
|
2865
3914
|
terminalBusy: null,
|
|
2866
3915
|
shellCwd: '',
|
|
@@ -2898,6 +3947,90 @@ class AgentManager extends EventEmitter {
|
|
|
2898
3947
|
attentionSuppressUntil: 0
|
|
2899
3948
|
};
|
|
2900
3949
|
|
|
3950
|
+
let previousPersistentRuntimeAgentId = '';
|
|
3951
|
+
let previousPersistentRecord = null;
|
|
3952
|
+
if (
|
|
3953
|
+
agentRecord.providerSessionKey
|
|
3954
|
+
&& typeof this.configManager?.getAgentSessionRecordForProviderSessionKey === 'function'
|
|
3955
|
+
) {
|
|
3956
|
+
const existingRecord = this.configManager.getAgentSessionRecordForProviderSessionKey(
|
|
3957
|
+
agentRecord.providerSessionKey,
|
|
3958
|
+
);
|
|
3959
|
+
if (existingRecord) {
|
|
3960
|
+
previousPersistentRecord = existingRecord;
|
|
3961
|
+
agentRecord.lifecycleJournal = lifecycleJournal(existingRecord);
|
|
3962
|
+
previousPersistentRuntimeAgentId = String(existingRecord.runtimeAgentId || '').trim();
|
|
3963
|
+
}
|
|
3964
|
+
}
|
|
3965
|
+
|
|
3966
|
+
let finishStartLifecycle = () => {};
|
|
3967
|
+
if (
|
|
3968
|
+
options.lifecycleToken
|
|
3969
|
+
&& !this.adoptAgentLifecycleOperation(agentId, options.lifecycleToken)
|
|
3970
|
+
) {
|
|
3971
|
+
if (callback) callback(null, 'Agent lifecycle operation is no longer active');
|
|
3972
|
+
return null;
|
|
3973
|
+
}
|
|
3974
|
+
if (!options.lifecycleToken) {
|
|
3975
|
+
finishStartLifecycle = this.beginAgentStartLifecycleOperation(agentId, options.startAdmissionToken);
|
|
3976
|
+
if (!finishStartLifecycle) {
|
|
3977
|
+
if (callback) callback(null, 'Agent lifecycle operation already in progress');
|
|
3978
|
+
return null;
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
|
|
3982
|
+
const createAdmission = this.beginPersistentAgentOperation(
|
|
3983
|
+
agentRecord,
|
|
3984
|
+
'create',
|
|
3985
|
+
options.createRequestId
|
|
3986
|
+
? `create-request:${String(options.createRequestId).trim().slice(0, 160)}`
|
|
3987
|
+
: `create:${agentId}`,
|
|
3988
|
+
{
|
|
3989
|
+
agentId,
|
|
3990
|
+
previousRuntimeAgentId: (
|
|
3991
|
+
typeof options.restoreRuntimeAgentIdOnFailure === 'string'
|
|
3992
|
+
&& options.restoreRuntimeAgentIdOnFailure
|
|
3993
|
+
)
|
|
3994
|
+
? options.restoreRuntimeAgentIdOnFailure
|
|
3995
|
+
: previousPersistentRuntimeAgentId,
|
|
3996
|
+
previousState: createRollbackState(previousPersistentRecord),
|
|
3997
|
+
signature: createOperationSignature(command, customWorkspace, options),
|
|
3998
|
+
command: agentRecord.command,
|
|
3999
|
+
cwd: agentRecord.cwd,
|
|
4000
|
+
runtimeKind: runtimeKind(agentRecord),
|
|
4001
|
+
structuredProcessProofRequired: useAcp,
|
|
4002
|
+
structuredProcessStartGated: useAcp && process.platform !== 'win32',
|
|
4003
|
+
},
|
|
4004
|
+
);
|
|
4005
|
+
if (createAdmission.error) {
|
|
4006
|
+
finishStartLifecycle();
|
|
4007
|
+
if (callback) callback(null, createAdmission.error);
|
|
4008
|
+
return null;
|
|
4009
|
+
}
|
|
4010
|
+
const createOperationId = createAdmission.operation.id;
|
|
4011
|
+
const rollbackCreatePatch = () => createFailurePatch(
|
|
4012
|
+
createAdmission.operation,
|
|
4013
|
+
options.restoreRuntimeAgentIdOnFailure,
|
|
4014
|
+
);
|
|
4015
|
+
const restoreCreateSessionOptions = () => {
|
|
4016
|
+
const previousState = createAdmission.operation.request?.previousState;
|
|
4017
|
+
const sessionKey = previousPersistentRecord?.providerSessionKey
|
|
4018
|
+
|| agentRecord.providerSessionKey;
|
|
4019
|
+
if (!sessionKey) return;
|
|
4020
|
+
if (
|
|
4021
|
+
previousState
|
|
4022
|
+
&& Array.isArray(previousState.acpAdditionalDirectories)
|
|
4023
|
+
&& Array.isArray(previousState.acpMcpServers)
|
|
4024
|
+
) {
|
|
4025
|
+
this.acpSessionOptionsByKey.set(sessionKey, {
|
|
4026
|
+
additionalDirectories: [...previousState.acpAdditionalDirectories],
|
|
4027
|
+
mcpServers: JSON.parse(JSON.stringify(previousState.acpMcpServers)),
|
|
4028
|
+
});
|
|
4029
|
+
} else {
|
|
4030
|
+
this.acpSessionOptionsByKey.delete(sessionKey);
|
|
4031
|
+
}
|
|
4032
|
+
};
|
|
4033
|
+
|
|
2901
4034
|
let precreatedProviderSession = null;
|
|
2902
4035
|
if (
|
|
2903
4036
|
providerSessionPlan.precreate === true
|
|
@@ -2907,6 +4040,19 @@ class AgentManager extends EventEmitter {
|
|
|
2907
4040
|
const adapter = getProviderAdapter(providerSessionPlan.provider);
|
|
2908
4041
|
if (typeof adapter?.terminalResumeArgs !== 'function') {
|
|
2909
4042
|
const message = `${providerSessionPlan.provider} cannot resume a pre-created Terminal session`;
|
|
4043
|
+
try {
|
|
4044
|
+
this.transitionPersistentAgentOperation(
|
|
4045
|
+
agentRecord,
|
|
4046
|
+
createOperationId,
|
|
4047
|
+
'failed',
|
|
4048
|
+
message,
|
|
4049
|
+
rollbackCreatePatch(),
|
|
4050
|
+
);
|
|
4051
|
+
restoreCreateSessionOptions();
|
|
4052
|
+
} catch (persistError) {
|
|
4053
|
+
console.error('Failed to persist Create failure:', persistError);
|
|
4054
|
+
}
|
|
4055
|
+
finishStartLifecycle();
|
|
2910
4056
|
if (callback) callback(null, message);
|
|
2911
4057
|
return null;
|
|
2912
4058
|
}
|
|
@@ -3015,6 +4161,23 @@ class AgentManager extends EventEmitter {
|
|
|
3015
4161
|
message = `${baseMessage}; ${identityRetainedReason}`;
|
|
3016
4162
|
}
|
|
3017
4163
|
console.error('Failed to create provider session identity:', error);
|
|
4164
|
+
try {
|
|
4165
|
+
this.transitionPersistentAgentOperation(
|
|
4166
|
+
agentRecord,
|
|
4167
|
+
createOperationId,
|
|
4168
|
+
identityRetainedReason || identityRollbackError ? 'blocked' : 'failed',
|
|
4169
|
+
message,
|
|
4170
|
+
identityRetainedReason || identityRollbackError
|
|
4171
|
+
? { visibleOnMainPage: true, archived: false }
|
|
4172
|
+
: rollbackCreatePatch(),
|
|
4173
|
+
);
|
|
4174
|
+
if (!(identityRetainedReason || identityRollbackError)) {
|
|
4175
|
+
restoreCreateSessionOptions();
|
|
4176
|
+
}
|
|
4177
|
+
} catch (persistError) {
|
|
4178
|
+
message = `${message}; failed to persist Create failure: ${persistError.message || persistError}`;
|
|
4179
|
+
}
|
|
4180
|
+
finishStartLifecycle();
|
|
3018
4181
|
if (callback) callback(null, message);
|
|
3019
4182
|
return null;
|
|
3020
4183
|
}
|
|
@@ -3027,15 +4190,15 @@ class AgentManager extends EventEmitter {
|
|
|
3027
4190
|
));
|
|
3028
4191
|
console.log('Starting agent:', program, logArgs, 'workspace:', workspace, spawnProgram !== program ? `resolved: ${spawnProgram}` : '');
|
|
3029
4192
|
|
|
3030
|
-
|
|
3031
|
-
this.agents.set(agentId, agentRecord);
|
|
3032
|
-
void this.refreshAgentWorktree(agentId);
|
|
3033
|
-
|
|
3034
|
-
this.lastActivity.set(agentId, Date.now());
|
|
3035
|
-
|
|
3036
|
-
this.emit('update');
|
|
4193
|
+
let structuredRuntimeRegistered = false;
|
|
3037
4194
|
|
|
3038
4195
|
try {
|
|
4196
|
+
ensureAgentOrders(agentRecord, Array.from(this.agents.values()));
|
|
4197
|
+
this.agents.set(agentId, agentRecord);
|
|
4198
|
+
void this.refreshAgentWorktree(agentId);
|
|
4199
|
+
this.lastActivity.set(agentId, Date.now());
|
|
4200
|
+
this.emit('update');
|
|
4201
|
+
|
|
3039
4202
|
if (useJsonCli) {
|
|
3040
4203
|
const jsonRuntime = runtimeBindingOf(agentRecord, 'json');
|
|
3041
4204
|
this.jsonCliRuntime.registerAgent({
|
|
@@ -3049,6 +4212,7 @@ class AgentManager extends EventEmitter {
|
|
|
3049
4212
|
autoApprove: options.dangerouslySkipPermissions === true,
|
|
3050
4213
|
initialEvents: jsonRuntime.events,
|
|
3051
4214
|
});
|
|
4215
|
+
structuredRuntimeRegistered = true;
|
|
3052
4216
|
}
|
|
3053
4217
|
|
|
3054
4218
|
if (useAcp) {
|
|
@@ -3088,7 +4252,50 @@ class AgentManager extends EventEmitter {
|
|
|
3088
4252
|
serviceTier: codexServiceTier,
|
|
3089
4253
|
additionalDirectories,
|
|
3090
4254
|
mcpServers,
|
|
4255
|
+
forkSourceSessionId: options.acpForkSourceSessionId || '',
|
|
4256
|
+
forkSourceCheckpoint: options.acpForkSourceCheckpoint || null,
|
|
4257
|
+
onForkSessionCreated: async sessionId => {
|
|
4258
|
+
const exactSessionId = String(sessionId || '').trim();
|
|
4259
|
+
if (!isSafeProviderSessionId(exactSessionId)) {
|
|
4260
|
+
throw new Error('ACP fork startup returned an unsafe provider session id');
|
|
4261
|
+
}
|
|
4262
|
+
agentRecord.providerSessionId = exactSessionId;
|
|
4263
|
+
agentRecord.providerSessionKey = mainPageAgentSessionKey(
|
|
4264
|
+
structuredRuntimeProvider,
|
|
4265
|
+
exactSessionId,
|
|
4266
|
+
agentRecord.providerHomeId || 'default'
|
|
4267
|
+
);
|
|
4268
|
+
agentRecord.providerSessionTemporary = false;
|
|
4269
|
+
agentRecord.providerSessionSource = 'acp-fork';
|
|
4270
|
+
agentRecord.providerSessionResolvedAt = Date.now();
|
|
4271
|
+
let normalizedSessionOptions = { additionalDirectories, mcpServers };
|
|
4272
|
+
try {
|
|
4273
|
+
normalizedSessionOptions = this.acpRuntime.getSessionRequestOptions(agentId);
|
|
4274
|
+
} catch {
|
|
4275
|
+
// The live binding already validated the scope. Retain the caller
|
|
4276
|
+
// copy only for custom runtimes that do not expose it.
|
|
4277
|
+
}
|
|
4278
|
+
this.acpSessionOptionsByKey.set(agentRecord.providerSessionKey, {
|
|
4279
|
+
additionalDirectories: [...normalizedSessionOptions.additionalDirectories],
|
|
4280
|
+
mcpServers: JSON.parse(JSON.stringify(normalizedSessionOptions.mcpServers)),
|
|
4281
|
+
});
|
|
4282
|
+
if (typeof options.onAcpForkSessionCreated === 'function') {
|
|
4283
|
+
await options.onAcpForkSessionCreated(exactSessionId);
|
|
4284
|
+
}
|
|
4285
|
+
this.ensurePersistentAgentSession(agentRecord);
|
|
4286
|
+
},
|
|
4287
|
+
onProcessStarted: async processIdentity => {
|
|
4288
|
+
agentRecord.structuredRuntimeProcess = {
|
|
4289
|
+
kind: 'acp-process-group',
|
|
4290
|
+
...processIdentity,
|
|
4291
|
+
};
|
|
4292
|
+
this.ensurePersistentAgentSession(agentRecord);
|
|
4293
|
+
},
|
|
3091
4294
|
});
|
|
4295
|
+
if (typeof options.onAcpSessionPrepared === 'function') {
|
|
4296
|
+
await options.onAcpSessionPrepared(prepared);
|
|
4297
|
+
}
|
|
4298
|
+
structuredRuntimeRegistered = true;
|
|
3092
4299
|
agentRecord.providerSessionId = prepared.sessionId;
|
|
3093
4300
|
agentRecord.providerSessionKey = mainPageAgentSessionKey(
|
|
3094
4301
|
structuredRuntimeProvider,
|
|
@@ -3143,13 +4350,19 @@ class AgentManager extends EventEmitter {
|
|
|
3143
4350
|
await startTerminal();
|
|
3144
4351
|
}
|
|
3145
4352
|
}
|
|
3146
|
-
|
|
4353
|
+
this.transitionPersistentAgentOperation(agentRecord, createOperationId, 'succeeded', '', {
|
|
3147
4354
|
visibleOnMainPage: true,
|
|
3148
4355
|
archived: false,
|
|
3149
4356
|
...(options.customTitleExplicit === true
|
|
3150
4357
|
? { customTitle: agentRecord.customTitle }
|
|
3151
4358
|
: {}),
|
|
3152
4359
|
});
|
|
4360
|
+
if (
|
|
4361
|
+
previousPersistentRuntimeAgentId
|
|
4362
|
+
&& previousPersistentRuntimeAgentId !== agentId
|
|
4363
|
+
) {
|
|
4364
|
+
this.forgetStoppedAgentRecord(previousPersistentRuntimeAgentId, { emitUpdate: false });
|
|
4365
|
+
}
|
|
3153
4366
|
|
|
3154
4367
|
const agent = this.agents.get(agentId);
|
|
3155
4368
|
if (agent && agent.status === 'pending') {
|
|
@@ -3163,13 +4376,37 @@ class AgentManager extends EventEmitter {
|
|
|
3163
4376
|
}
|
|
3164
4377
|
|
|
3165
4378
|
this.providerSessionService.activate(agentId);
|
|
4379
|
+
finishStartLifecycle();
|
|
3166
4380
|
if (callback) callback(agentId);
|
|
3167
4381
|
this.emit('update');
|
|
3168
4382
|
return agentId;
|
|
3169
4383
|
} catch (error) {
|
|
3170
4384
|
console.error('Failed to start agent:', error);
|
|
3171
4385
|
let runtimeCleanupError = null;
|
|
3172
|
-
if (
|
|
4386
|
+
if (useJsonCli && structuredRuntimeRegistered) {
|
|
4387
|
+
try {
|
|
4388
|
+
const stopped = await this.jsonCliRuntime.unregisterAgentAndWait(agentId);
|
|
4389
|
+
if (stopped !== true) {
|
|
4390
|
+
throw new Error('JSON runtime binding disappeared before exit was verified', { cause: error });
|
|
4391
|
+
}
|
|
4392
|
+
} catch (cleanupError) {
|
|
4393
|
+
runtimeCleanupError = cleanupError;
|
|
4394
|
+
}
|
|
4395
|
+
} else if (
|
|
4396
|
+
useAcp
|
|
4397
|
+
&& error?.runtimeCleanupVerified !== true
|
|
4398
|
+
&& (structuredRuntimeRegistered || error?.runtimeCleanupAttempted === true)
|
|
4399
|
+
) {
|
|
4400
|
+
try {
|
|
4401
|
+
const stopped = await this.acpRuntime.unregisterAgentAndWait(agentId);
|
|
4402
|
+
if (stopped !== true) {
|
|
4403
|
+
throw error?.adapterCleanupError
|
|
4404
|
+
|| new Error('ACP runtime binding disappeared before exit was verified', { cause: error });
|
|
4405
|
+
}
|
|
4406
|
+
} catch (cleanupError) {
|
|
4407
|
+
runtimeCleanupError = cleanupError;
|
|
4408
|
+
}
|
|
4409
|
+
} else if (!useJsonCli && !useAcp) {
|
|
3173
4410
|
try {
|
|
3174
4411
|
await this.stopUncertainTerminalSession(resolution.engine, agentId);
|
|
3175
4412
|
} catch (engineCleanupError) {
|
|
@@ -3180,6 +4417,12 @@ class AgentManager extends EventEmitter {
|
|
|
3180
4417
|
);
|
|
3181
4418
|
}
|
|
3182
4419
|
}
|
|
4420
|
+
if (runtimeCleanupError) {
|
|
4421
|
+
console.error(
|
|
4422
|
+
'Failed to stop partially started Agent runtime:',
|
|
4423
|
+
runtimeCleanupError.message || runtimeCleanupError
|
|
4424
|
+
);
|
|
4425
|
+
}
|
|
3183
4426
|
let rollbackError = null;
|
|
3184
4427
|
if (precreatedProviderSession && !runtimeCleanupError) {
|
|
3185
4428
|
try {
|
|
@@ -3195,19 +4438,55 @@ class AgentManager extends EventEmitter {
|
|
|
3195
4438
|
} else if (precreatedProviderSession) {
|
|
3196
4439
|
this.acpSessionOptionsByKey.delete(precreatedProviderSession.sessionKey);
|
|
3197
4440
|
}
|
|
3198
|
-
|
|
3199
|
-
|
|
4441
|
+
const startupError = error && (error.message || String(error));
|
|
4442
|
+
const cleanupSuffix = rollbackError
|
|
4443
|
+
? `; provider session rollback failed: ${rollbackError.message || rollbackError}`
|
|
4444
|
+
: '';
|
|
4445
|
+
const runtimeCleanupSuffix = runtimeCleanupError
|
|
4446
|
+
? `; runtime cleanup could not be verified and Agent ${agentId} was retained for retry: ${runtimeCleanupError.message || runtimeCleanupError}`
|
|
4447
|
+
: '';
|
|
4448
|
+
if (runtimeCleanupError) {
|
|
4449
|
+
agentRecord.status = 'error';
|
|
4450
|
+
agentRecord.engineStatus = 'cleanup-uncertain';
|
|
4451
|
+
const runtime = runtimeBindingOf(agentRecord);
|
|
4452
|
+
if (runtime && Object.prototype.hasOwnProperty.call(runtime, 'state')) {
|
|
4453
|
+
runtime.state = 'error';
|
|
4454
|
+
runtime.error = runtimeCleanupError.message || String(runtimeCleanupError);
|
|
4455
|
+
}
|
|
3200
4456
|
try {
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
4457
|
+
this.transitionPersistentAgentOperation(
|
|
4458
|
+
agentRecord,
|
|
4459
|
+
createOperationId,
|
|
4460
|
+
'blocked',
|
|
4461
|
+
runtimeCleanupError.message || String(runtimeCleanupError),
|
|
4462
|
+
);
|
|
4463
|
+
} catch (persistError) {
|
|
4464
|
+
runtimeCleanupError = new Error(
|
|
4465
|
+
`${runtimeCleanupError.message || runtimeCleanupError}; failed to persist blocked Create: ${persistError.message || persistError}`,
|
|
3207
4466
|
);
|
|
3208
|
-
} finally {
|
|
3209
|
-
agentRecord.id = failedAgentId;
|
|
3210
4467
|
}
|
|
4468
|
+
finishStartLifecycle();
|
|
4469
|
+
this.emit('update');
|
|
4470
|
+
if (callback) callback(agentId, `${startupError}${runtimeCleanupSuffix}${cleanupSuffix}`);
|
|
4471
|
+
return null;
|
|
4472
|
+
}
|
|
4473
|
+
try {
|
|
4474
|
+
this.transitionPersistentAgentOperation(
|
|
4475
|
+
agentRecord,
|
|
4476
|
+
createOperationId,
|
|
4477
|
+
'failed',
|
|
4478
|
+
startupError,
|
|
4479
|
+
rollbackCreatePatch(),
|
|
4480
|
+
);
|
|
4481
|
+
restoreCreateSessionOptions();
|
|
4482
|
+
} catch (persistError) {
|
|
4483
|
+
agentRecord.status = 'error';
|
|
4484
|
+
agentRecord.engineStatus = 'metadata-commit-uncertain';
|
|
4485
|
+
const message = `${startupError}; failed to persist Create failure: ${persistError.message || persistError}`;
|
|
4486
|
+
finishStartLifecycle();
|
|
4487
|
+
this.emit('update');
|
|
4488
|
+
if (callback) callback(agentId, message);
|
|
4489
|
+
return null;
|
|
3211
4490
|
}
|
|
3212
4491
|
this.agents.delete(agentId);
|
|
3213
4492
|
this.lastActivity.delete(agentId);
|
|
@@ -3222,26 +4501,38 @@ class AgentManager extends EventEmitter {
|
|
|
3222
4501
|
this.mainAgentId = null;
|
|
3223
4502
|
}
|
|
3224
4503
|
|
|
4504
|
+
finishStartLifecycle();
|
|
3225
4505
|
this.emit('update');
|
|
3226
|
-
const startupError = error && (error.message || String(error));
|
|
3227
|
-
const cleanupSuffix = rollbackError
|
|
3228
|
-
? `; provider session rollback failed: ${rollbackError.message || rollbackError}`
|
|
3229
|
-
: '';
|
|
3230
|
-
const runtimeCleanupSuffix = runtimeCleanupError
|
|
3231
|
-
? `; Terminal rollback failed and the provider session was retained: ${runtimeCleanupError.message || runtimeCleanupError}`
|
|
3232
|
-
: '';
|
|
3233
4506
|
if (callback) callback(null, `${startupError}${runtimeCleanupSuffix}${cleanupSuffix}`);
|
|
3234
4507
|
return null;
|
|
3235
4508
|
}
|
|
3236
4509
|
}
|
|
3237
4510
|
|
|
3238
|
-
|
|
4511
|
+
trackInputOperation(operation) {
|
|
4512
|
+
this.activeInputOperations.add(operation);
|
|
4513
|
+
void operation.finally(() => {
|
|
4514
|
+
this.activeInputOperations.delete(operation);
|
|
4515
|
+
}).catch(() => {});
|
|
4516
|
+
return operation;
|
|
4517
|
+
}
|
|
4518
|
+
|
|
4519
|
+
assertAgentOperationAdmission() {
|
|
4520
|
+
if (this.disposing) {
|
|
4521
|
+
throw new Error('Farming is shutting down; Agent operations are not accepted');
|
|
4522
|
+
}
|
|
4523
|
+
}
|
|
4524
|
+
|
|
4525
|
+
async enqueueInputOperation(agentId, operation, options = {}) {
|
|
4526
|
+
if (this.disposing && options.admitted !== true) {
|
|
4527
|
+
throw new Error('Farming is shutting down; Agent input is not accepted');
|
|
4528
|
+
}
|
|
3239
4529
|
const previous = this.inputQueues.get(agentId) || Promise.resolve();
|
|
3240
4530
|
const next = previous
|
|
3241
4531
|
.catch(() => {})
|
|
3242
4532
|
.then(operation);
|
|
3243
4533
|
|
|
3244
4534
|
this.inputQueues.set(agentId, next);
|
|
4535
|
+
this.trackInputOperation(next);
|
|
3245
4536
|
try {
|
|
3246
4537
|
return await next;
|
|
3247
4538
|
} finally {
|
|
@@ -3252,6 +4543,9 @@ class AgentManager extends EventEmitter {
|
|
|
3252
4543
|
}
|
|
3253
4544
|
|
|
3254
4545
|
async enqueueInputOperationUntilReleased(agentId, operation) {
|
|
4546
|
+
if (this.disposing) {
|
|
4547
|
+
throw new Error('Farming is shutting down; Agent input is not accepted');
|
|
4548
|
+
}
|
|
3255
4549
|
const previous = this.inputQueues.get(agentId) || Promise.resolve();
|
|
3256
4550
|
let released = false;
|
|
3257
4551
|
let resolveReleased;
|
|
@@ -3266,6 +4560,7 @@ class AgentManager extends EventEmitter {
|
|
|
3266
4560
|
|
|
3267
4561
|
const ready = previous.catch(() => {});
|
|
3268
4562
|
const completion = ready.then(() => operation(release));
|
|
4563
|
+
this.trackInputOperation(completion);
|
|
3269
4564
|
completion.catch(() => release());
|
|
3270
4565
|
const boundary = ready.then(() => releasedPromise);
|
|
3271
4566
|
this.inputQueues.set(agentId, boundary);
|
|
@@ -3282,12 +4577,12 @@ class AgentManager extends EventEmitter {
|
|
|
3282
4577
|
}
|
|
3283
4578
|
|
|
3284
4579
|
async sendComposerMessage(agentId, message) {
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
4580
|
+
const agent = this.agents.get(agentId);
|
|
4581
|
+
if (agent && isAcpAgent(agent)) {
|
|
4582
|
+
return this.enqueueInputOperationUntilReleased(
|
|
4583
|
+
agentId,
|
|
4584
|
+
releaseInput => this.sendComposerMessageNow(agentId, message, { releaseInput }),
|
|
4585
|
+
);
|
|
3291
4586
|
}
|
|
3292
4587
|
return this.enqueueInputOperation(agentId, () => this.sendComposerMessageNow(agentId, message));
|
|
3293
4588
|
}
|
|
@@ -3383,7 +4678,7 @@ class AgentManager extends EventEmitter {
|
|
|
3383
4678
|
return applied;
|
|
3384
4679
|
}
|
|
3385
4680
|
|
|
3386
|
-
async sendComposerMessageNow(agentId, message) {
|
|
4681
|
+
async sendComposerMessageNow(agentId, message, options = {}) {
|
|
3387
4682
|
const agent = this.agents.get(agentId);
|
|
3388
4683
|
if (!agent) throw new Error('Agent not found');
|
|
3389
4684
|
const prompt = normalizedComposerPrompt(message);
|
|
@@ -3404,10 +4699,15 @@ class AgentManager extends EventEmitter {
|
|
|
3404
4699
|
}
|
|
3405
4700
|
|
|
3406
4701
|
if (isAcpAgent(agent)) {
|
|
3407
|
-
|
|
3408
|
-
const
|
|
3409
|
-
|
|
3410
|
-
|
|
4702
|
+
this.requireLiveAcpAgent(agentId);
|
|
4703
|
+
const result = await this.acpRuntime.submitMessage(agentId, prompt, {
|
|
4704
|
+
onSubmitted: options.releaseInput,
|
|
4705
|
+
});
|
|
4706
|
+
if (result.steered !== true) {
|
|
4707
|
+
const runtime = runtimeBindingOf(agent, 'acp');
|
|
4708
|
+
runtime.state = 'idle';
|
|
4709
|
+
runtime.stopReason = result.stopReason || '';
|
|
4710
|
+
}
|
|
3411
4711
|
this.ensurePersistentAgentSession(agent);
|
|
3412
4712
|
return { kind: 'acp', ...result };
|
|
3413
4713
|
}
|
|
@@ -3416,16 +4716,6 @@ class AgentManager extends EventEmitter {
|
|
|
3416
4716
|
return { kind: 'terminal' };
|
|
3417
4717
|
}
|
|
3418
4718
|
|
|
3419
|
-
async sendAcpSteerNow(agentId, message) {
|
|
3420
|
-
const agent = this.agents.get(agentId);
|
|
3421
|
-
if (!agent) throw new Error('Agent not found');
|
|
3422
|
-
if (!isAcpAgent(agent)) throw new Error('Agent is not using ACP Chat');
|
|
3423
|
-
const prompt = normalizedComposerPrompt(message);
|
|
3424
|
-
const result = await this.acpRuntime.steer(agentId, prompt);
|
|
3425
|
-
this.ensurePersistentAgentSession(agent);
|
|
3426
|
-
return { kind: 'acp', steered: true, ...result };
|
|
3427
|
-
}
|
|
3428
|
-
|
|
3429
4719
|
getJsonCliTranscript(agentId, options = {}) {
|
|
3430
4720
|
const agent = this.agents.get(agentId);
|
|
3431
4721
|
if (!agent) throw new Error('Agent not found');
|
|
@@ -3434,16 +4724,12 @@ class AgentManager extends EventEmitter {
|
|
|
3434
4724
|
}
|
|
3435
4725
|
|
|
3436
4726
|
getAcpSession(agentId, options = {}) {
|
|
3437
|
-
|
|
3438
|
-
if (!agent) throw new Error('Agent not found');
|
|
3439
|
-
if (!isAcpAgent(agent)) throw new Error('Agent is not using the ACP runtime');
|
|
4727
|
+
this.requireLiveAcpAgent(agentId);
|
|
3440
4728
|
return this.acpRuntime.getSession(agentId, options);
|
|
3441
4729
|
}
|
|
3442
4730
|
|
|
3443
4731
|
getAcpTranscript(agentId, options = {}) {
|
|
3444
|
-
|
|
3445
|
-
if (!agent) throw new Error('Agent not found');
|
|
3446
|
-
if (!isAcpAgent(agent)) throw new Error('Agent is not using the ACP runtime');
|
|
4732
|
+
this.requireLiveAcpAgent(agentId);
|
|
3447
4733
|
const transcript = this.acpRuntime.getTranscriptSession(agentId, options);
|
|
3448
4734
|
return {
|
|
3449
4735
|
...transcript,
|
|
@@ -3452,9 +4738,7 @@ class AgentManager extends EventEmitter {
|
|
|
3452
4738
|
}
|
|
3453
4739
|
|
|
3454
4740
|
getAcpToolDetail(agentId, toolCallId) {
|
|
3455
|
-
|
|
3456
|
-
if (!agent) throw new Error('Agent not found');
|
|
3457
|
-
if (!isAcpAgent(agent)) throw new Error('Agent is not using the ACP runtime');
|
|
4741
|
+
this.requireLiveAcpAgent(agentId);
|
|
3458
4742
|
const entry = this.acpRuntime.getToolEntry(agentId, toolCallId);
|
|
3459
4743
|
if (!entry) throw new Error('ACP tool call not found');
|
|
3460
4744
|
const subagentSessionId = String(entry?._meta?.subagent_session_info?.session_id || '');
|
|
@@ -3473,34 +4757,37 @@ class AgentManager extends EventEmitter {
|
|
|
3473
4757
|
}
|
|
3474
4758
|
|
|
3475
4759
|
killAcpTerminal(agentId, terminalId) {
|
|
4760
|
+
this.assertAgentOperationAdmission();
|
|
3476
4761
|
this.getAcpSession(agentId);
|
|
3477
4762
|
return this.acpRuntime.killTerminal(agentId, terminalId);
|
|
3478
4763
|
}
|
|
3479
4764
|
|
|
3480
4765
|
inputAcpTerminal(agentId, terminalId, input) {
|
|
4766
|
+
this.assertAgentOperationAdmission();
|
|
3481
4767
|
this.getAcpSession(agentId);
|
|
3482
4768
|
return this.acpRuntime.inputTerminal(agentId, terminalId, input);
|
|
3483
4769
|
}
|
|
3484
4770
|
|
|
3485
4771
|
resizeAcpTerminal(agentId, terminalId, cols, rows) {
|
|
4772
|
+
this.assertAgentOperationAdmission();
|
|
3486
4773
|
this.getAcpSession(agentId);
|
|
3487
4774
|
return this.acpRuntime.resizeTerminal(agentId, terminalId, cols, rows);
|
|
3488
4775
|
}
|
|
3489
4776
|
|
|
3490
4777
|
cancelAcpSubagent(agentId, sessionId) {
|
|
4778
|
+
this.assertAgentOperationAdmission();
|
|
3491
4779
|
this.getAcpSession(agentId);
|
|
3492
4780
|
return this.acpRuntime.cancelSubagent(agentId, sessionId);
|
|
3493
4781
|
}
|
|
3494
4782
|
|
|
3495
4783
|
decideAcpPatch(agentId, toolCallId, requestedPath, decision) {
|
|
4784
|
+
this.assertAgentOperationAdmission();
|
|
3496
4785
|
this.getAcpSession(agentId);
|
|
3497
4786
|
return this.acpRuntime.decidePatch(agentId, toolCallId, requestedPath, decision);
|
|
3498
4787
|
}
|
|
3499
4788
|
|
|
3500
4789
|
getAcpReviewChanges(agentId, toolCallIds) {
|
|
3501
|
-
|
|
3502
|
-
if (!agent) throw new Error('Agent not found');
|
|
3503
|
-
if (!isAcpAgent(agent)) throw new Error('Agent is not using the ACP runtime');
|
|
4790
|
+
this.requireLiveAcpAgent(agentId);
|
|
3504
4791
|
if (!Array.isArray(toolCallIds) || toolCallIds.length === 0 || toolCallIds.length > 256) {
|
|
3505
4792
|
throw new Error('ACP review tool calls are invalid');
|
|
3506
4793
|
}
|
|
@@ -3515,62 +4802,84 @@ class AgentManager extends EventEmitter {
|
|
|
3515
4802
|
}
|
|
3516
4803
|
|
|
3517
4804
|
listAcpSessions(agentId, options = {}) {
|
|
3518
|
-
|
|
3519
|
-
if (!agent) throw new Error('Agent not found');
|
|
3520
|
-
if (!isAcpAgent(agent)) throw new Error('Agent is not using the ACP runtime');
|
|
4805
|
+
this.requireLiveAcpAgent(agentId);
|
|
3521
4806
|
return this.acpRuntime.listSessions(agentId, options);
|
|
3522
4807
|
}
|
|
3523
4808
|
|
|
3524
4809
|
respondToAcpPermission(agentId, requestId, optionId, cancelled = false) {
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
if (!isAcpAgent(agent)) throw new Error('Agent is not using the ACP runtime');
|
|
4810
|
+
this.assertAgentOperationAdmission();
|
|
4811
|
+
this.requireLiveAcpAgent(agentId);
|
|
3528
4812
|
return this.acpRuntime.respondPermission(agentId, requestId, optionId, cancelled);
|
|
3529
4813
|
}
|
|
3530
4814
|
|
|
3531
4815
|
respondToAcpElicitation(agentId, requestId, action, content) {
|
|
4816
|
+
this.assertAgentOperationAdmission();
|
|
4817
|
+
this.requireLiveAcpAgent(agentId);
|
|
4818
|
+
return this.acpRuntime.respondElicitation(agentId, requestId, action, content);
|
|
4819
|
+
}
|
|
4820
|
+
|
|
4821
|
+
requireLiveAcpAgent(agentId) {
|
|
3532
4822
|
const agent = this.agents.get(agentId);
|
|
3533
4823
|
if (!agent) throw new Error('Agent not found');
|
|
3534
4824
|
if (!isAcpAgent(agent)) throw new Error('Agent is not using the ACP runtime');
|
|
3535
|
-
|
|
4825
|
+
if (typeof this.acpRuntime.hasBinding === 'function' && !this.acpRuntime.hasBinding(agentId)) {
|
|
4826
|
+
const runtime = runtimeBindingOf(agent, 'acp');
|
|
4827
|
+
const message = runtime?.error || (
|
|
4828
|
+
runtime?.state === 'connecting'
|
|
4829
|
+
? 'ACP Agent is still connecting'
|
|
4830
|
+
: 'ACP Agent runtime is unavailable'
|
|
4831
|
+
);
|
|
4832
|
+
const error = new Error(message);
|
|
4833
|
+
error.code = 'ACP_RUNTIME_UNAVAILABLE';
|
|
4834
|
+
throw error;
|
|
4835
|
+
}
|
|
4836
|
+
return agent;
|
|
3536
4837
|
}
|
|
3537
4838
|
|
|
3538
4839
|
authenticateAcpAgent(agentId, methodId) {
|
|
4840
|
+
this.assertAgentOperationAdmission();
|
|
3539
4841
|
this.getAcpSession(agentId);
|
|
3540
4842
|
return this.acpRuntime.authenticate(agentId, methodId);
|
|
3541
4843
|
}
|
|
3542
4844
|
|
|
3543
4845
|
logoutAcpAgent(agentId) {
|
|
4846
|
+
this.assertAgentOperationAdmission();
|
|
3544
4847
|
this.getAcpSession(agentId);
|
|
3545
4848
|
return this.acpRuntime.logout(agentId);
|
|
3546
4849
|
}
|
|
3547
4850
|
|
|
3548
4851
|
forkAcpSession(agentId, options = {}) {
|
|
4852
|
+
this.assertAgentOperationAdmission();
|
|
3549
4853
|
this.getAcpSession(agentId);
|
|
3550
4854
|
return this.acpRuntime.forkSession(agentId, options);
|
|
3551
4855
|
}
|
|
3552
4856
|
|
|
3553
4857
|
deleteAcpSession(agentId, sessionId) {
|
|
4858
|
+
this.assertAgentOperationAdmission();
|
|
3554
4859
|
this.getAcpSession(agentId);
|
|
3555
4860
|
return this.acpRuntime.deleteSession(agentId, sessionId);
|
|
3556
4861
|
}
|
|
3557
4862
|
|
|
3558
4863
|
closeAcpSession(agentId) {
|
|
4864
|
+
this.assertAgentOperationAdmission();
|
|
3559
4865
|
this.getAcpSession(agentId);
|
|
3560
4866
|
return this.acpRuntime.closeSession(agentId);
|
|
3561
4867
|
}
|
|
3562
4868
|
|
|
3563
4869
|
setAcpSessionMode(agentId, modeId) {
|
|
4870
|
+
this.assertAgentOperationAdmission();
|
|
3564
4871
|
this.getAcpSession(agentId);
|
|
3565
4872
|
return this.acpRuntime.setSessionMode(agentId, modeId);
|
|
3566
4873
|
}
|
|
3567
4874
|
|
|
3568
4875
|
setAcpSessionConfigOption(agentId, configId, value) {
|
|
4876
|
+
this.assertAgentOperationAdmission();
|
|
3569
4877
|
this.getAcpSession(agentId);
|
|
3570
4878
|
return this.acpRuntime.setSessionConfigOption(agentId, configId, value);
|
|
3571
4879
|
}
|
|
3572
4880
|
|
|
3573
4881
|
setAcpSessionConfigOptions(agentId, changes) {
|
|
4882
|
+
this.assertAgentOperationAdmission();
|
|
3574
4883
|
this.getAcpSession(agentId);
|
|
3575
4884
|
return this.acpRuntime.setSessionConfigOptions(agentId, changes);
|
|
3576
4885
|
}
|
|
@@ -3623,6 +4932,7 @@ class AgentManager extends EventEmitter {
|
|
|
3623
4932
|
}
|
|
3624
4933
|
|
|
3625
4934
|
async interruptAgent(agentId, options = {}) {
|
|
4935
|
+
this.assertAgentOperationAdmission();
|
|
3626
4936
|
const agent = this.agents.get(agentId);
|
|
3627
4937
|
if (!agent) return;
|
|
3628
4938
|
try {
|
|
@@ -3671,6 +4981,7 @@ class AgentManager extends EventEmitter {
|
|
|
3671
4981
|
}
|
|
3672
4982
|
|
|
3673
4983
|
requestAgentSessionResize(agentId, cols, rows) {
|
|
4984
|
+
if (this.disposing) return false;
|
|
3674
4985
|
this.pendingResizeByAgent.set(agentId, { cols, rows });
|
|
3675
4986
|
if (this.resizeDrains.has(agentId)) return true;
|
|
3676
4987
|
|
|
@@ -3757,49 +5068,160 @@ class AgentManager extends EventEmitter {
|
|
|
3757
5068
|
}
|
|
3758
5069
|
|
|
3759
5070
|
renameAgent(agentId, title) {
|
|
5071
|
+
if (this.disposing) {
|
|
5072
|
+
return { error: 'Farming is shutting down; Agent updates are not accepted' };
|
|
5073
|
+
}
|
|
5074
|
+
const lifecycleOperation = this.agentLifecycleOperations.get(agentId);
|
|
5075
|
+
if (lifecycleOperation) {
|
|
5076
|
+
return { error: `Agent lifecycle change already in progress: ${lifecycleOperation.label}` };
|
|
5077
|
+
}
|
|
3760
5078
|
const agent = this.agents.get(agentId);
|
|
3761
5079
|
if (!agent) {
|
|
3762
5080
|
return { error: 'Agent not found' };
|
|
3763
5081
|
}
|
|
3764
5082
|
|
|
3765
5083
|
const customTitle = String(title || '').trim().slice(0, 80);
|
|
5084
|
+
const admission = this.beginPersistentAgentUpdate(
|
|
5085
|
+
agent,
|
|
5086
|
+
`rename:${customTitle}`,
|
|
5087
|
+
{ customTitle },
|
|
5088
|
+
);
|
|
5089
|
+
if (admission.error) return { error: `Failed to rename Agent: ${admission.error}` };
|
|
5090
|
+
if (admission.deduplicated) {
|
|
5091
|
+
return { agentId, customTitle, operationId: admission.operation.id, deduplicated: true };
|
|
5092
|
+
}
|
|
5093
|
+
const staged = {
|
|
5094
|
+
...agent,
|
|
5095
|
+
customTitle,
|
|
5096
|
+
lifecycleJournal: lifecycleJournal(agent),
|
|
5097
|
+
};
|
|
5098
|
+
transitionLifecycleOperation(staged, admission.operation.id, 'succeeded');
|
|
5099
|
+
try {
|
|
5100
|
+
this.ensurePersistentAgentSession(staged, { customTitle });
|
|
5101
|
+
} catch (error) {
|
|
5102
|
+
return {
|
|
5103
|
+
error: `Failed to rename Agent: ${error.message || error}`,
|
|
5104
|
+
operationId: admission.operation.id,
|
|
5105
|
+
retryable: true,
|
|
5106
|
+
};
|
|
5107
|
+
}
|
|
3766
5108
|
agent.customTitle = customTitle;
|
|
3767
|
-
|
|
5109
|
+
agent.lifecycleJournal = staged.lifecycleJournal;
|
|
5110
|
+
setAgentRecordId(agent, staged.agentRecordId || staged.persistentSessionId || '');
|
|
3768
5111
|
this.updateEngineProviderSessionMetadata(agent);
|
|
3769
5112
|
this.emit('update');
|
|
3770
|
-
return { agentId, customTitle };
|
|
5113
|
+
return { agentId, customTitle, operationId: admission.operation.id };
|
|
3771
5114
|
}
|
|
3772
5115
|
|
|
3773
5116
|
setAgentTask(agentId, task) {
|
|
5117
|
+
if (this.disposing) {
|
|
5118
|
+
return { error: 'Farming is shutting down; Agent updates are not accepted' };
|
|
5119
|
+
}
|
|
5120
|
+
const lifecycleOperation = this.agentLifecycleOperations.get(agentId);
|
|
5121
|
+
if (lifecycleOperation) {
|
|
5122
|
+
return { error: `Agent lifecycle change already in progress: ${lifecycleOperation.label}` };
|
|
5123
|
+
}
|
|
3774
5124
|
const agent = this.agents.get(agentId);
|
|
3775
5125
|
if (!agent) {
|
|
3776
5126
|
return { error: 'Agent not found' };
|
|
3777
5127
|
}
|
|
3778
5128
|
|
|
3779
5129
|
const nextTask = String(task || '').trim().slice(0, 240);
|
|
5130
|
+
const admission = this.beginPersistentAgentUpdate(
|
|
5131
|
+
agent,
|
|
5132
|
+
`task:${nextTask}`,
|
|
5133
|
+
{ task: nextTask },
|
|
5134
|
+
);
|
|
5135
|
+
if (admission.error) return { error: `Failed to update Agent task: ${admission.error}` };
|
|
5136
|
+
if (admission.deduplicated) {
|
|
5137
|
+
return { agentId, task: nextTask, operationId: admission.operation.id, deduplicated: true };
|
|
5138
|
+
}
|
|
5139
|
+
const staged = {
|
|
5140
|
+
...agent,
|
|
5141
|
+
task: nextTask,
|
|
5142
|
+
lifecycleJournal: lifecycleJournal(agent),
|
|
5143
|
+
};
|
|
5144
|
+
transitionLifecycleOperation(staged, admission.operation.id, 'succeeded');
|
|
5145
|
+
try {
|
|
5146
|
+
this.ensurePersistentAgentSession(staged, { task: nextTask });
|
|
5147
|
+
} catch (error) {
|
|
5148
|
+
return {
|
|
5149
|
+
error: `Failed to update Agent task: ${error.message || error}`,
|
|
5150
|
+
operationId: admission.operation.id,
|
|
5151
|
+
retryable: true,
|
|
5152
|
+
};
|
|
5153
|
+
}
|
|
3780
5154
|
agent.task = nextTask;
|
|
5155
|
+
agent.lifecycleJournal = staged.lifecycleJournal;
|
|
5156
|
+
setAgentRecordId(agent, staged.agentRecordId || staged.persistentSessionId || '');
|
|
3781
5157
|
this.emit('update');
|
|
3782
|
-
return { agentId, task: nextTask };
|
|
5158
|
+
return { agentId, task: nextTask, operationId: admission.operation.id };
|
|
3783
5159
|
}
|
|
3784
5160
|
|
|
3785
5161
|
updateAgentFlags(agentId, flags) {
|
|
5162
|
+
if (this.disposing) {
|
|
5163
|
+
return { error: 'Farming is shutting down; Agent updates are not accepted' };
|
|
5164
|
+
}
|
|
5165
|
+
const lifecycleOperation = this.agentLifecycleOperations.get(agentId);
|
|
5166
|
+
if (lifecycleOperation) {
|
|
5167
|
+
return { error: `Agent lifecycle change already in progress: ${lifecycleOperation.label}` };
|
|
5168
|
+
}
|
|
3786
5169
|
const agent = this.agents.get(agentId);
|
|
3787
5170
|
if (!agent) {
|
|
3788
5171
|
return { error: 'Agent not found' };
|
|
3789
5172
|
}
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
5173
|
+
if (flags.archived === true) {
|
|
5174
|
+
if (agent.id === this.mainAgentId) {
|
|
5175
|
+
return { error: 'Main Agent cannot be archived' };
|
|
5176
|
+
}
|
|
5177
|
+
return { error: 'Use archiveAgent to archive live agents' };
|
|
5178
|
+
}
|
|
5179
|
+
|
|
5180
|
+
const persistedFlags = {};
|
|
5181
|
+
[
|
|
5182
|
+
'pinned',
|
|
5183
|
+
'unread',
|
|
5184
|
+
'archived',
|
|
5185
|
+
'readAttentionSeq',
|
|
5186
|
+
'readOutputEpoch',
|
|
5187
|
+
'readOutputSeq',
|
|
5188
|
+
].forEach(field => {
|
|
5189
|
+
if (Object.prototype.hasOwnProperty.call(flags, field)) persistedFlags[field] = flags[field];
|
|
5190
|
+
});
|
|
5191
|
+
const requestKey = `flags:${JSON.stringify(persistedFlags)}`;
|
|
5192
|
+
const activeUpdate = activeLifecycleOperation(agent);
|
|
5193
|
+
const needsLifecycleJournal = typeof persistedFlags.pinned === 'boolean'
|
|
5194
|
+
|| persistedFlags.archived === false
|
|
5195
|
+
|| (
|
|
5196
|
+
activeUpdate?.type === 'update'
|
|
5197
|
+
&& activeUpdate.requestKey === requestKey
|
|
5198
|
+
);
|
|
5199
|
+
const admission = needsLifecycleJournal
|
|
5200
|
+
? this.beginPersistentAgentOperation(
|
|
5201
|
+
agent,
|
|
5202
|
+
'update',
|
|
5203
|
+
requestKey,
|
|
5204
|
+
persistedFlags,
|
|
5205
|
+
)
|
|
5206
|
+
: null;
|
|
5207
|
+
if (admission?.error) return { error: `Failed to update Agent: ${admission.error}` };
|
|
5208
|
+
|
|
5209
|
+
const staged = {
|
|
5210
|
+
...agent,
|
|
5211
|
+
lifecycleJournal: lifecycleJournal(agent),
|
|
5212
|
+
};
|
|
5213
|
+
const updates = {};
|
|
5214
|
+
let structuralUpdateChanged = false;
|
|
5215
|
+
let readUpdateChanged = false;
|
|
5216
|
+
if (typeof flags.pinned === 'boolean') {
|
|
5217
|
+
const wasPinned = staged.pinned === true;
|
|
5218
|
+
staged.pinned = flags.pinned;
|
|
5219
|
+
structuralUpdateChanged = structuralUpdateChanged || wasPinned !== staged.pinned;
|
|
5220
|
+
updates.pinned = staged.pinned;
|
|
5221
|
+
if (!wasPinned && staged.pinned) {
|
|
5222
|
+
staged.pinnedOrder = nextPinnedOrder(Array.from(this.agents.values()));
|
|
3801
5223
|
}
|
|
3802
|
-
updates.pinnedOrder = finiteOrder(
|
|
5224
|
+
updates.pinnedOrder = finiteOrder(staged.pinnedOrder);
|
|
3803
5225
|
}
|
|
3804
5226
|
|
|
3805
5227
|
if (
|
|
@@ -3807,53 +5229,97 @@ class AgentManager extends EventEmitter {
|
|
|
3807
5229
|
&& typeof flags.readOutputSeq === 'number'
|
|
3808
5230
|
&& Number.isFinite(flags.readOutputSeq)
|
|
3809
5231
|
) {
|
|
3810
|
-
const
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
5232
|
+
const currentRuntimeEpoch = typeof staged.runtimeEpoch === 'string' ? staged.runtimeEpoch : '';
|
|
5233
|
+
const currentOutputSeq = finiteNumberOrNull(staged.lastOutputSeq);
|
|
5234
|
+
if (
|
|
5235
|
+
currentRuntimeEpoch
|
|
5236
|
+
&& flags.readOutputEpoch === currentRuntimeEpoch
|
|
5237
|
+
&& currentOutputSeq !== null
|
|
5238
|
+
) {
|
|
5239
|
+
const nextOutputSeq = Math.min(currentOutputSeq, Math.max(0, Math.floor(flags.readOutputSeq)));
|
|
5240
|
+
const previousOutputSeq = staged.readOutputEpoch === currentRuntimeEpoch
|
|
5241
|
+
? finiteNumberOrNull(staged.readOutputSeq)
|
|
5242
|
+
: null;
|
|
5243
|
+
const readOutputSeq = previousOutputSeq === null
|
|
5244
|
+
? nextOutputSeq
|
|
5245
|
+
: Math.max(previousOutputSeq, nextOutputSeq);
|
|
5246
|
+
readUpdateChanged = readUpdateChanged
|
|
5247
|
+
|| staged.readOutputEpoch !== currentRuntimeEpoch
|
|
5248
|
+
|| previousOutputSeq !== readOutputSeq;
|
|
5249
|
+
staged.readOutputEpoch = currentRuntimeEpoch;
|
|
5250
|
+
staged.readOutputSeq = readOutputSeq;
|
|
5251
|
+
}
|
|
5252
|
+
updates.readOutputEpoch = typeof staged.readOutputEpoch === 'string' ? staged.readOutputEpoch : '';
|
|
5253
|
+
updates.readOutputSeq = finiteNumberOrNull(staged.readOutputSeq);
|
|
3815
5254
|
}
|
|
3816
5255
|
|
|
3817
5256
|
if (typeof flags.unread === 'boolean') {
|
|
3818
|
-
const
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
5257
|
+
const previousReadSeq = finiteNonNegativeInteger(staged.readAttentionSeq);
|
|
5258
|
+
const previousUnread = staged.unread === true;
|
|
5259
|
+
if (flags.unread) {
|
|
5260
|
+
if (finiteNonNegativeInteger(staged.attentionSeq) === 0) {
|
|
5261
|
+
const now = Date.now();
|
|
5262
|
+
staged.attentionSeq = 1;
|
|
5263
|
+
staged.attentionUpdatedAt = now;
|
|
5264
|
+
staged.attentionReason = 'manual-unread';
|
|
5265
|
+
staged.attentionOutputEpoch = typeof staged.runtimeEpoch === 'string' ? staged.runtimeEpoch : '';
|
|
5266
|
+
staged.attentionOutputSeq = Number.isFinite(staged.lastOutputSeq) ? staged.lastOutputSeq : null;
|
|
5267
|
+
staged.attentionAutoReadNext = false;
|
|
5268
|
+
}
|
|
5269
|
+
staged.readAttentionSeq = Math.max(0, finiteNonNegativeInteger(staged.attentionSeq) - 1);
|
|
5270
|
+
} else {
|
|
5271
|
+
staged.readAttentionSeq = finiteNonNegativeInteger(staged.attentionSeq);
|
|
5272
|
+
}
|
|
5273
|
+
staged.readAttentionAt = Date.now();
|
|
5274
|
+
staged.unread = agentAttentionUnread(staged);
|
|
5275
|
+
readUpdateChanged = readUpdateChanged
|
|
5276
|
+
|| previousReadSeq !== staged.readAttentionSeq
|
|
5277
|
+
|| previousUnread !== staged.unread;
|
|
5278
|
+
updates.unread = staged.unread;
|
|
5279
|
+
updates.attentionSeq = finiteNonNegativeInteger(staged.attentionSeq);
|
|
5280
|
+
updates.readAttentionSeq = finiteNonNegativeInteger(staged.readAttentionSeq);
|
|
3826
5281
|
}
|
|
3827
5282
|
|
|
3828
5283
|
if (typeof flags.readAttentionSeq === 'number' && Number.isFinite(flags.readAttentionSeq)) {
|
|
3829
|
-
const
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
5284
|
+
const previousReadSeq = finiteNonNegativeInteger(staged.readAttentionSeq);
|
|
5285
|
+
const previousUnread = staged.unread === true;
|
|
5286
|
+
const attentionSeq = finiteNonNegativeInteger(staged.attentionSeq);
|
|
5287
|
+
staged.readAttentionSeq = Math.min(
|
|
5288
|
+
attentionSeq,
|
|
5289
|
+
Math.max(previousReadSeq, finiteNonNegativeInteger(flags.readAttentionSeq)),
|
|
5290
|
+
);
|
|
5291
|
+
staged.readAttentionAt = Date.now();
|
|
5292
|
+
staged.unread = agentAttentionUnread(staged);
|
|
5293
|
+
readUpdateChanged = readUpdateChanged
|
|
5294
|
+
|| previousReadSeq !== staged.readAttentionSeq
|
|
5295
|
+
|| previousUnread !== staged.unread;
|
|
5296
|
+
updates.unread = staged.unread;
|
|
5297
|
+
updates.attentionSeq = attentionSeq;
|
|
5298
|
+
updates.readAttentionSeq = staged.readAttentionSeq;
|
|
3842
5299
|
}
|
|
3843
5300
|
|
|
3844
5301
|
if (flags.archived === false) {
|
|
3845
|
-
structuralUpdateChanged = structuralUpdateChanged ||
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
updates.archived =
|
|
3849
|
-
updates.archivedAt =
|
|
5302
|
+
structuralUpdateChanged = structuralUpdateChanged || staged.archived === true || staged.archivedAt !== null;
|
|
5303
|
+
staged.archived = false;
|
|
5304
|
+
staged.archivedAt = null;
|
|
5305
|
+
updates.archived = staged.archived;
|
|
5306
|
+
updates.archivedAt = staged.archivedAt;
|
|
3850
5307
|
}
|
|
3851
5308
|
|
|
3852
|
-
if (
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
this.ensurePersistentAgentSession(
|
|
5309
|
+
if (admission) {
|
|
5310
|
+
transitionLifecycleOperation(staged, admission.operation.id, 'succeeded');
|
|
5311
|
+
}
|
|
5312
|
+
try {
|
|
5313
|
+
this.ensurePersistentAgentSession(staged);
|
|
5314
|
+
} catch (error) {
|
|
5315
|
+
return {
|
|
5316
|
+
error: `Failed to update Agent: ${error.message || error}`,
|
|
5317
|
+
...(admission ? { operationId: admission.operation.id } : {}),
|
|
5318
|
+
retryable: true,
|
|
5319
|
+
};
|
|
5320
|
+
}
|
|
5321
|
+
Object.assign(agent, staged);
|
|
5322
|
+
if (structuralUpdateChanged || readUpdateChanged) {
|
|
3857
5323
|
this.updateEngineProviderSessionMetadata(agent);
|
|
3858
5324
|
}
|
|
3859
5325
|
if (structuralUpdateChanged) {
|
|
@@ -3873,6 +5339,7 @@ class AgentManager extends EventEmitter {
|
|
|
3873
5339
|
...updates,
|
|
3874
5340
|
changed: structuralUpdateChanged || readUpdateChanged,
|
|
3875
5341
|
requiresState: structuralUpdateChanged,
|
|
5342
|
+
...(admission ? { operationId: admission.operation.id } : {}),
|
|
3876
5343
|
};
|
|
3877
5344
|
}
|
|
3878
5345
|
|
|
@@ -3884,18 +5351,7 @@ class AgentManager extends EventEmitter {
|
|
|
3884
5351
|
String(afterAgentId || ''),
|
|
3885
5352
|
);
|
|
3886
5353
|
if (result.error) return result;
|
|
3887
|
-
|
|
3888
|
-
const updates = [];
|
|
3889
|
-
for (const [updatedAgentId, projectOrder] of result.updates) {
|
|
3890
|
-
const agent = this.agents.get(updatedAgentId);
|
|
3891
|
-
if (!agent) continue;
|
|
3892
|
-
agent.projectOrder = projectOrder;
|
|
3893
|
-
this.ensurePersistentAgentSession(agent);
|
|
3894
|
-
this.updateEngineProviderSessionMetadata(agent);
|
|
3895
|
-
updates.push({ agentId: updatedAgentId, projectOrder });
|
|
3896
|
-
}
|
|
3897
|
-
this.emit('update');
|
|
3898
|
-
return { agentId, projectOrder: finiteOrder(this.agents.get(agentId)?.projectOrder), updates };
|
|
5354
|
+
return this.commitAgentOrderUpdates(agentId, result.updates, 'projectOrder');
|
|
3899
5355
|
}
|
|
3900
5356
|
|
|
3901
5357
|
reorderPinnedAgent(agentId, { beforeAgentId = '', afterAgentId = '' } = {}) {
|
|
@@ -3906,21 +5362,76 @@ class AgentManager extends EventEmitter {
|
|
|
3906
5362
|
String(afterAgentId || ''),
|
|
3907
5363
|
);
|
|
3908
5364
|
if (result.error) return result;
|
|
5365
|
+
return this.commitAgentOrderUpdates(agentId, result.updates, 'pinnedOrder');
|
|
5366
|
+
}
|
|
3909
5367
|
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
5368
|
+
commitAgentOrderUpdates(agentId, orderUpdates, field) {
|
|
5369
|
+
const staged = [...orderUpdates].map(([updatedAgentId, order]) => ({
|
|
5370
|
+
agent: this.agents.get(updatedAgentId),
|
|
5371
|
+
order,
|
|
5372
|
+
})).filter(item => item.agent);
|
|
5373
|
+
const conflicting = staged.find(item => this.agentLifecycleOperations.has(item.agent.id));
|
|
5374
|
+
if (conflicting) {
|
|
5375
|
+
const lifecycleOperation = this.agentLifecycleOperations.get(conflicting.agent.id);
|
|
5376
|
+
return { error: `Agent lifecycle change already in progress: ${lifecycleOperation.label}` };
|
|
5377
|
+
}
|
|
5378
|
+
|
|
5379
|
+
const attempted = [];
|
|
5380
|
+
try {
|
|
5381
|
+
for (const item of staged) {
|
|
5382
|
+
item.stagedAgent = { ...item.agent, [field]: item.order };
|
|
5383
|
+
attempted.push(item);
|
|
5384
|
+
this.ensurePersistentAgentSession(item.stagedAgent);
|
|
5385
|
+
}
|
|
5386
|
+
} catch (error) {
|
|
5387
|
+
let rollbackError = null;
|
|
5388
|
+
for (const item of attempted.reverse()) {
|
|
5389
|
+
try {
|
|
5390
|
+
const agentRecordId = item.stagedAgent.agentRecordId
|
|
5391
|
+
|| item.stagedAgent.persistentSessionId
|
|
5392
|
+
|| item.agent.agentRecordId
|
|
5393
|
+
|| item.agent.persistentSessionId;
|
|
5394
|
+
this.ensurePersistentAgentSession({
|
|
5395
|
+
...item.agent,
|
|
5396
|
+
agentRecordId,
|
|
5397
|
+
persistentSessionId: agentRecordId,
|
|
5398
|
+
});
|
|
5399
|
+
} catch (restoreError) {
|
|
5400
|
+
rollbackError = restoreError;
|
|
5401
|
+
}
|
|
5402
|
+
}
|
|
5403
|
+
return {
|
|
5404
|
+
error: `Failed to reorder Agents: ${error.message || error}${
|
|
5405
|
+
rollbackError ? `; storage rollback failed: ${rollbackError.message || rollbackError}` : ''
|
|
5406
|
+
}`,
|
|
5407
|
+
};
|
|
3918
5408
|
}
|
|
5409
|
+
|
|
5410
|
+
const updates = staged.map(item => {
|
|
5411
|
+
item.agent[field] = item.order;
|
|
5412
|
+
setAgentRecordId(
|
|
5413
|
+
item.agent,
|
|
5414
|
+
item.stagedAgent.agentRecordId || item.stagedAgent.persistentSessionId || '',
|
|
5415
|
+
);
|
|
5416
|
+
this.updateEngineProviderSessionMetadata(item.agent);
|
|
5417
|
+
return { agentId: item.agent.id, [field]: item.order };
|
|
5418
|
+
});
|
|
3919
5419
|
this.emit('update');
|
|
3920
|
-
return {
|
|
5420
|
+
return {
|
|
5421
|
+
agentId,
|
|
5422
|
+
[field]: finiteOrder(this.agents.get(agentId)?.[field]),
|
|
5423
|
+
updates,
|
|
5424
|
+
};
|
|
3921
5425
|
}
|
|
3922
5426
|
|
|
3923
5427
|
reorderAgent(agentId, neighbors = {}) {
|
|
5428
|
+
if (this.disposing) {
|
|
5429
|
+
return { error: 'Farming is shutting down; Agent updates are not accepted' };
|
|
5430
|
+
}
|
|
5431
|
+
const lifecycleOperation = this.agentLifecycleOperations.get(agentId);
|
|
5432
|
+
if (lifecycleOperation) {
|
|
5433
|
+
return { error: `Agent lifecycle change already in progress: ${lifecycleOperation.label}` };
|
|
5434
|
+
}
|
|
3924
5435
|
const agent = this.agents.get(agentId);
|
|
3925
5436
|
if (!agent) return { error: 'Agent not found' };
|
|
3926
5437
|
return agent.pinned === true
|
|
@@ -3934,50 +5445,117 @@ class AgentManager extends EventEmitter {
|
|
|
3934
5445
|
return this.restartAgentWithPermissionMode(agentId, mode);
|
|
3935
5446
|
}
|
|
3936
5447
|
|
|
3937
|
-
|
|
3938
|
-
const inFlight = this.
|
|
5448
|
+
runAgentLifecycleOperation(agentId, key, kind, label, operation, sameKindConflictError = '') {
|
|
5449
|
+
const inFlight = this.agentLifecycleOperations.get(agentId);
|
|
3939
5450
|
if (inFlight) {
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
}
|
|
3944
|
-
|
|
3945
|
-
const restart = this.performAgentPermissionRestart(agentId, mode);
|
|
3946
|
-
const entry = { mode, promise: restart };
|
|
3947
|
-
this.permissionRestartInFlight.set(agentId, entry);
|
|
3948
|
-
try {
|
|
3949
|
-
return await restart;
|
|
3950
|
-
} finally {
|
|
3951
|
-
if (this.permissionRestartInFlight.get(agentId) === entry) {
|
|
3952
|
-
this.permissionRestartInFlight.delete(agentId);
|
|
5451
|
+
if (inFlight.key === key) return inFlight.promise;
|
|
5452
|
+
if (sameKindConflictError && inFlight.kind === kind) {
|
|
5453
|
+
return Promise.resolve({ error: sameKindConflictError });
|
|
3953
5454
|
}
|
|
3954
|
-
|
|
5455
|
+
return inFlight.promise
|
|
5456
|
+
.catch(() => {})
|
|
5457
|
+
.then(() => this.runAgentLifecycleOperation(
|
|
5458
|
+
agentId,
|
|
5459
|
+
key,
|
|
5460
|
+
kind,
|
|
5461
|
+
label,
|
|
5462
|
+
operation,
|
|
5463
|
+
sameKindConflictError,
|
|
5464
|
+
));
|
|
5465
|
+
}
|
|
5466
|
+
if (this.disposing) {
|
|
5467
|
+
return Promise.resolve({ error: 'Farming is shutting down; Agent lifecycle changes are not accepted' });
|
|
5468
|
+
}
|
|
5469
|
+
|
|
5470
|
+
const token = Symbol(key);
|
|
5471
|
+
const promise = Promise.resolve().then(() => operation(token));
|
|
5472
|
+
const entry = { key, kind, label, token, promise, agentIds: new Set([agentId]) };
|
|
5473
|
+
this.agentLifecycleOperations.set(agentId, entry);
|
|
5474
|
+
void promise.finally(() => {
|
|
5475
|
+
for (const ownedAgentId of entry.agentIds) {
|
|
5476
|
+
if (this.agentLifecycleOperations.get(ownedAgentId) === entry) {
|
|
5477
|
+
this.agentLifecycleOperations.delete(ownedAgentId);
|
|
5478
|
+
}
|
|
5479
|
+
}
|
|
5480
|
+
}).catch(() => {});
|
|
5481
|
+
return promise;
|
|
3955
5482
|
}
|
|
3956
5483
|
|
|
3957
|
-
async
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
5484
|
+
async whenAgentLifecycleIdle(agentId) {
|
|
5485
|
+
await this.whenRecovered();
|
|
5486
|
+
while (true) {
|
|
5487
|
+
const inFlight = this.agentLifecycleOperations.get(agentId);
|
|
5488
|
+
if (!inFlight) return;
|
|
5489
|
+
await inFlight.promise.catch(() => {});
|
|
3963
5490
|
}
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
5491
|
+
}
|
|
5492
|
+
|
|
5493
|
+
beginAgentStartLifecycleOperation(agentId, startAdmissionToken) {
|
|
5494
|
+
if (this.agentLifecycleOperations.has(agentId)) return null;
|
|
5495
|
+
if (this.disposing && !this.agentStartAdmissions.has(startAdmissionToken)) return null;
|
|
5496
|
+
let resolveCompletion;
|
|
5497
|
+
const promise = new Promise(resolve => {
|
|
5498
|
+
resolveCompletion = resolve;
|
|
5499
|
+
});
|
|
5500
|
+
const entry = {
|
|
5501
|
+
key: 'start',
|
|
5502
|
+
kind: 'start',
|
|
5503
|
+
label: 'start',
|
|
5504
|
+
token: Symbol('start'),
|
|
5505
|
+
promise,
|
|
5506
|
+
agentIds: new Set([agentId]),
|
|
5507
|
+
};
|
|
5508
|
+
this.agentLifecycleOperations.set(agentId, entry);
|
|
5509
|
+
let finished = false;
|
|
5510
|
+
return () => {
|
|
5511
|
+
if (finished) return;
|
|
5512
|
+
finished = true;
|
|
5513
|
+
if (this.agentLifecycleOperations.get(agentId) === entry) {
|
|
5514
|
+
this.agentLifecycleOperations.delete(agentId);
|
|
3972
5515
|
}
|
|
3973
|
-
|
|
5516
|
+
resolveCompletion({ agentId, started: true });
|
|
5517
|
+
};
|
|
5518
|
+
}
|
|
5519
|
+
|
|
5520
|
+
adoptAgentLifecycleOperation(agentId, lifecycleToken) {
|
|
5521
|
+
if (!lifecycleToken) return false;
|
|
5522
|
+
const entry = [...new Set(this.agentLifecycleOperations.values())]
|
|
5523
|
+
.find(candidate => candidate.token === lifecycleToken);
|
|
5524
|
+
if (!entry) return false;
|
|
5525
|
+
const existing = this.agentLifecycleOperations.get(agentId);
|
|
5526
|
+
if (existing && existing !== entry) return false;
|
|
5527
|
+
entry.agentIds.add(agentId);
|
|
5528
|
+
this.agentLifecycleOperations.set(agentId, entry);
|
|
5529
|
+
return true;
|
|
5530
|
+
}
|
|
5531
|
+
|
|
5532
|
+
restartAgentWithPermissionMode(agentId, mode) {
|
|
5533
|
+
return this.runAgentLifecycleOperation(
|
|
5534
|
+
agentId,
|
|
5535
|
+
`permission-restart:${mode}`,
|
|
5536
|
+
'permission-restart',
|
|
5537
|
+
'permission restart',
|
|
5538
|
+
token => this.performAgentPermissionRestart(agentId, mode, token),
|
|
5539
|
+
'Permission change already in progress',
|
|
5540
|
+
);
|
|
3974
5541
|
}
|
|
3975
5542
|
|
|
3976
|
-
|
|
5543
|
+
restartAgentRuntimeMode(agentId, mode) {
|
|
5544
|
+
return this.runAgentLifecycleOperation(
|
|
5545
|
+
agentId,
|
|
5546
|
+
`runtime-switch:${mode}`,
|
|
5547
|
+
'runtime-switch',
|
|
5548
|
+
'runtime switch',
|
|
5549
|
+
token => this.performAgentRuntimeModeRestart(agentId, mode, token),
|
|
5550
|
+
'Agent runtime switch already in progress',
|
|
5551
|
+
);
|
|
5552
|
+
}
|
|
5553
|
+
|
|
5554
|
+
async performAgentRuntimeModeRestart(agentId, mode, lifecycleToken) {
|
|
3977
5555
|
const agent = this.agents.get(agentId);
|
|
3978
5556
|
if (!agent) return { error: 'Agent not found' };
|
|
3979
5557
|
const provider = agent.providerSessionProvider || '';
|
|
3980
|
-
const requestedMode = ['terminal', '
|
|
5558
|
+
const requestedMode = ['terminal', 'acp', 'chat'].includes(mode) ? mode : '';
|
|
3981
5559
|
const nextMode = requestedMode === 'acp' && provider === 'codex' ? 'chat' : requestedMode;
|
|
3982
5560
|
if (!nextMode) return { error: 'Unsupported Agent runtime mode' };
|
|
3983
5561
|
const currentKind = runtimeKind(agent);
|
|
@@ -4084,6 +5662,7 @@ class AgentManager extends EventEmitter {
|
|
|
4084
5662
|
codexApprovalMode: agent.launchPermissionMode || undefined,
|
|
4085
5663
|
jsonCliEvents: preserved.jsonCliEvents,
|
|
4086
5664
|
runtimeSwitchVerifiedSessionId: startsFreshChatSession ? '' : sessionId,
|
|
5665
|
+
lifecycleToken,
|
|
4087
5666
|
...acpSessionOptions,
|
|
4088
5667
|
...(provider === 'codex' && !startsFreshChatSession
|
|
4089
5668
|
? preserveCodexSessionProfileOptions()
|
|
@@ -4094,6 +5673,7 @@ class AgentManager extends EventEmitter {
|
|
|
4094
5673
|
...restartOptions,
|
|
4095
5674
|
agentRuntimeMode: originalMode,
|
|
4096
5675
|
acpStartFresh: false,
|
|
5676
|
+
...(originalMode === 'json' ? { allowLegacyJsonRuntime: true } : {}),
|
|
4097
5677
|
};
|
|
4098
5678
|
const startReplacement = options => new Promise(resolve => {
|
|
4099
5679
|
let settled = false;
|
|
@@ -4120,7 +5700,14 @@ class AgentManager extends EventEmitter {
|
|
|
4120
5700
|
Object.assign(replacement, preserved);
|
|
4121
5701
|
this.ensurePersistentAgentSession(replacement);
|
|
4122
5702
|
};
|
|
4123
|
-
await this.killAgent(agentId, {
|
|
5703
|
+
const killResult = await this.killAgent(agentId, {
|
|
5704
|
+
reason: 'runtime-switch',
|
|
5705
|
+
recordHistory: false,
|
|
5706
|
+
emitUpdate: false,
|
|
5707
|
+
lifecycleToken,
|
|
5708
|
+
persistDeleteOperation: false,
|
|
5709
|
+
});
|
|
5710
|
+
if (killResult?.error) return killResult;
|
|
4124
5711
|
const switched = await startReplacement(restartOptions);
|
|
4125
5712
|
if (switched.restartedAgentId && !switched.error) {
|
|
4126
5713
|
restorePreservedState(switched.restartedAgentId);
|
|
@@ -4133,11 +5720,21 @@ class AgentManager extends EventEmitter {
|
|
|
4133
5720
|
};
|
|
4134
5721
|
}
|
|
4135
5722
|
if (switched.restartedAgentId && this.agents.has(switched.restartedAgentId)) {
|
|
4136
|
-
await this.killAgent(switched.restartedAgentId, {
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
5723
|
+
const cleanup = await this.killAgent(switched.restartedAgentId, {
|
|
5724
|
+
reason: 'runtime-switch-start-failed',
|
|
5725
|
+
recordHistory: false,
|
|
5726
|
+
emitUpdate: false,
|
|
5727
|
+
lifecycleToken,
|
|
4140
5728
|
});
|
|
5729
|
+
if (cleanup?.error) {
|
|
5730
|
+
this.emit('update');
|
|
5731
|
+
return {
|
|
5732
|
+
agentId,
|
|
5733
|
+
restartedAgentId: switched.restartedAgentId,
|
|
5734
|
+
cleanupUncertain: true,
|
|
5735
|
+
error: `${switched.error || 'Failed to switch Agent runtime'} Replacement cleanup could not be verified: ${cleanup.error}`,
|
|
5736
|
+
};
|
|
5737
|
+
}
|
|
4141
5738
|
}
|
|
4142
5739
|
|
|
4143
5740
|
const restored = await startReplacement(originalOptions);
|
|
@@ -4153,16 +5750,24 @@ class AgentManager extends EventEmitter {
|
|
|
4153
5750
|
warning: `${switched.error || 'Failed to switch Agent runtime'} Original runtime restored.`,
|
|
4154
5751
|
};
|
|
4155
5752
|
}
|
|
5753
|
+
let restoreCleanupError = '';
|
|
4156
5754
|
if (restored.restartedAgentId && this.agents.has(restored.restartedAgentId)) {
|
|
4157
|
-
await this.killAgent(restored.restartedAgentId, {
|
|
5755
|
+
const cleanup = await this.killAgent(restored.restartedAgentId, {
|
|
4158
5756
|
reason: 'runtime-switch-restore-failed',
|
|
4159
5757
|
recordHistory: false,
|
|
4160
5758
|
emitUpdate: false,
|
|
5759
|
+
lifecycleToken,
|
|
4161
5760
|
});
|
|
5761
|
+
restoreCleanupError = cleanup?.error || '';
|
|
4162
5762
|
}
|
|
4163
5763
|
this.emit('update');
|
|
4164
5764
|
return {
|
|
4165
|
-
|
|
5765
|
+
...(restoreCleanupError
|
|
5766
|
+
? { restartedAgentId: restored.restartedAgentId, cleanupUncertain: true }
|
|
5767
|
+
: {}),
|
|
5768
|
+
error: `${switched.error || 'Failed to switch Agent runtime'} Restore also failed: ${
|
|
5769
|
+
restored.error || 'unknown error'
|
|
5770
|
+
}${restoreCleanupError ? ` Cleanup could not be verified: ${restoreCleanupError}` : ''}`,
|
|
4166
5771
|
};
|
|
4167
5772
|
}
|
|
4168
5773
|
|
|
@@ -4181,7 +5786,7 @@ class AgentManager extends EventEmitter {
|
|
|
4181
5786
|
});
|
|
4182
5787
|
}
|
|
4183
5788
|
|
|
4184
|
-
async performAgentPermissionRestart(agentId, mode) {
|
|
5789
|
+
async performAgentPermissionRestart(agentId, mode, lifecycleToken) {
|
|
4185
5790
|
const agent = this.agents.get(agentId);
|
|
4186
5791
|
if (!agent) {
|
|
4187
5792
|
return { error: 'Agent not found' };
|
|
@@ -4255,6 +5860,7 @@ class AgentManager extends EventEmitter {
|
|
|
4255
5860
|
])),
|
|
4256
5861
|
projectOrder: finiteOrder(agent.projectOrder),
|
|
4257
5862
|
pinnedOrder: finiteOrder(agent.pinnedOrder),
|
|
5863
|
+
lifecycleToken,
|
|
4258
5864
|
...acpSessionOptions,
|
|
4259
5865
|
...(provider === 'codex' ? { codexApprovalMode: nextMode } : { claudePermissionMode: nextMode }),
|
|
4260
5866
|
...(provider === 'codex' && hasResumableSession
|
|
@@ -4271,16 +5877,14 @@ class AgentManager extends EventEmitter {
|
|
|
4271
5877
|
readAttentionSeq: finiteNonNegativeInteger(agent.readAttentionSeq),
|
|
4272
5878
|
};
|
|
4273
5879
|
|
|
4274
|
-
this.
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
this.permissionRestartSuppressedAgentIds.delete(agentId);
|
|
4283
|
-
}
|
|
5880
|
+
const killResult = await this.killAgent(agentId, {
|
|
5881
|
+
reason: 'permission-restart',
|
|
5882
|
+
recordHistory: false,
|
|
5883
|
+
emitUpdate: false,
|
|
5884
|
+
lifecycleToken,
|
|
5885
|
+
persistDeleteOperation: false,
|
|
5886
|
+
});
|
|
5887
|
+
if (killResult?.error) return killResult;
|
|
4284
5888
|
|
|
4285
5889
|
return new Promise((resolve) => {
|
|
4286
5890
|
const startResult = this.startAgent(command, effectiveAgentWorkspaceRoot(agent) || null, (restartedAgentId, error) => {
|
|
@@ -4543,7 +6147,43 @@ class AgentManager extends EventEmitter {
|
|
|
4543
6147
|
return removedKeys;
|
|
4544
6148
|
}
|
|
4545
6149
|
|
|
4546
|
-
|
|
6150
|
+
deleteForkWorktreeProject(workspace, options = {}) {
|
|
6151
|
+
const workspaceKey = canonicalWorkspacePath(this.expandWorkspacePath(workspace));
|
|
6152
|
+
if (!workspaceKey) return this.deleteForkWorktreeProjectAdmitted(workspace, options);
|
|
6153
|
+
const inFlight = this.projectWorkspaceDeleteAdmissions.get(workspaceKey);
|
|
6154
|
+
if (inFlight) return inFlight;
|
|
6155
|
+
|
|
6156
|
+
const operation = this.deleteForkWorktreeProjectAdmitted(workspace, options);
|
|
6157
|
+
this.projectWorkspaceDeleteAdmissions.set(workspaceKey, operation);
|
|
6158
|
+
void operation.finally(() => {
|
|
6159
|
+
if (this.projectWorkspaceDeleteAdmissions.get(workspaceKey) === operation) {
|
|
6160
|
+
this.projectWorkspaceDeleteAdmissions.delete(workspaceKey);
|
|
6161
|
+
}
|
|
6162
|
+
}).catch(() => {});
|
|
6163
|
+
return operation;
|
|
6164
|
+
}
|
|
6165
|
+
|
|
6166
|
+
async deleteForkWorktreeProjectAdmitted(workspace, options = {}) {
|
|
6167
|
+
await this.whenRecovered();
|
|
6168
|
+
const workspaceKey = canonicalWorkspacePath(this.expandWorkspacePath(workspace));
|
|
6169
|
+
const relatedStarts = workspaceKey
|
|
6170
|
+
? [...this.agentStartAdmissions.values()].filter(admission => (
|
|
6171
|
+
!admission.workspaceKey
|
|
6172
|
+
|| isSameOrDescendantPath(workspaceKey, admission.workspaceKey)
|
|
6173
|
+
))
|
|
6174
|
+
: [];
|
|
6175
|
+
try {
|
|
6176
|
+
await withBoundedWait(
|
|
6177
|
+
Promise.allSettled(relatedStarts.map(admission => admission.promise)),
|
|
6178
|
+
WORKTREE_DELETE_START_DRAIN_TIMEOUT_MS,
|
|
6179
|
+
`Worktree ${workspaceKey} Agent start drain`,
|
|
6180
|
+
);
|
|
6181
|
+
} catch (error) {
|
|
6182
|
+
return {
|
|
6183
|
+
workspace: workspaceKey,
|
|
6184
|
+
error: error.message || String(error),
|
|
6185
|
+
};
|
|
6186
|
+
}
|
|
4547
6187
|
const inspected = await this.inspectForkWorktreeProject(workspace);
|
|
4548
6188
|
if (inspected.error) return inspected;
|
|
4549
6189
|
if (inspected.requiresForce && options.force !== true) {
|
|
@@ -4553,11 +6193,27 @@ class AgentManager extends EventEmitter {
|
|
|
4553
6193
|
};
|
|
4554
6194
|
}
|
|
4555
6195
|
|
|
6196
|
+
const archivedAgentIds = [];
|
|
6197
|
+
const removedMainPageSessionKeys = [];
|
|
4556
6198
|
const projectAgents = this.agentsForProjectWorkspace(inspected.workspace);
|
|
6199
|
+
for (const agent of projectAgents) {
|
|
6200
|
+
const result = await this.archiveAgent(agent.id, { requireEngineExit: true });
|
|
6201
|
+
if (result && !result.error) {
|
|
6202
|
+
archivedAgentIds.push(agent.id);
|
|
6203
|
+
removedMainPageSessionKeys.push(...(result.removedMainPageSessionKeys || []));
|
|
6204
|
+
} else {
|
|
6205
|
+
return {
|
|
6206
|
+
...inspected,
|
|
6207
|
+
error: `Agent ${agent.id} could not be stopped before deleting the worktree: ${result?.error || 'Failed to archive Agent'}`,
|
|
6208
|
+
archivedAgentIds,
|
|
6209
|
+
removedMainPageSessionKeys: Array.from(new Set(removedMainPageSessionKeys)),
|
|
6210
|
+
};
|
|
6211
|
+
}
|
|
6212
|
+
}
|
|
6213
|
+
|
|
4557
6214
|
const args = ['-C', inspected.workspace, 'worktree', 'remove'];
|
|
4558
6215
|
if (options.force === true) args.push('--force');
|
|
4559
6216
|
args.push(inspected.workspace);
|
|
4560
|
-
|
|
4561
6217
|
try {
|
|
4562
6218
|
await execFileAsync('git', args, {
|
|
4563
6219
|
timeout: 60000,
|
|
@@ -4568,34 +6224,25 @@ class AgentManager extends EventEmitter {
|
|
|
4568
6224
|
return {
|
|
4569
6225
|
...inspected,
|
|
4570
6226
|
error: message || 'Failed to delete git worktree',
|
|
6227
|
+
archivedAgentIds,
|
|
6228
|
+
removedMainPageSessionKeys: Array.from(new Set(removedMainPageSessionKeys)),
|
|
4571
6229
|
};
|
|
4572
6230
|
}
|
|
4573
6231
|
|
|
4574
|
-
const archivedAgentIds = [];
|
|
4575
|
-
const removedMainPageSessionKeys = [];
|
|
4576
|
-
const cleanupErrors = [];
|
|
4577
|
-
for (const agent of projectAgents) {
|
|
4578
|
-
const result = await this.archiveAgent(agent.id, { requireEngineExit: true });
|
|
4579
|
-
if (result && !result.error) {
|
|
4580
|
-
archivedAgentIds.push(agent.id);
|
|
4581
|
-
removedMainPageSessionKeys.push(...(result.removedMainPageSessionKeys || []));
|
|
4582
|
-
} else {
|
|
4583
|
-
cleanupErrors.push(`${agent.id}: ${result?.error || 'Failed to archive Agent'}`);
|
|
4584
|
-
}
|
|
4585
|
-
}
|
|
4586
6232
|
return {
|
|
4587
6233
|
workspace: inspected.workspace,
|
|
4588
6234
|
deleted: true,
|
|
4589
6235
|
forced: options.force === true,
|
|
4590
6236
|
archivedAgentIds,
|
|
4591
6237
|
removedMainPageSessionKeys: Array.from(new Set(removedMainPageSessionKeys)),
|
|
4592
|
-
...(cleanupErrors.length > 0 ? {
|
|
4593
|
-
cleanupError: cleanupErrors.join('; '),
|
|
4594
|
-
} : {}),
|
|
4595
6238
|
};
|
|
4596
6239
|
}
|
|
4597
6240
|
|
|
4598
|
-
async forkAgent(agentId, mode = 'same-worktree') {
|
|
6241
|
+
async forkAgent(agentId, mode = 'same-worktree', options = {}) {
|
|
6242
|
+
await this.whenRecovered();
|
|
6243
|
+
if (this.disposing) {
|
|
6244
|
+
return { error: 'Farming is shutting down; Agent lifecycle changes are not accepted' };
|
|
6245
|
+
}
|
|
4599
6246
|
const agent = this.agents.get(agentId);
|
|
4600
6247
|
if (!agent) {
|
|
4601
6248
|
return { error: 'Agent not found' };
|
|
@@ -4603,6 +6250,22 @@ class AgentManager extends EventEmitter {
|
|
|
4603
6250
|
if (!['same-worktree', 'new-worktree'].includes(mode)) {
|
|
4604
6251
|
return { error: 'Unsupported fork mode' };
|
|
4605
6252
|
}
|
|
6253
|
+
if (options.targetRuntime === 'chat') {
|
|
6254
|
+
if (mode !== 'same-worktree') {
|
|
6255
|
+
return { error: 'Conversation Fork supports only the same worktree' };
|
|
6256
|
+
}
|
|
6257
|
+
const expectedRevision = Number(options.expectedRevision);
|
|
6258
|
+
if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 0) {
|
|
6259
|
+
return { error: 'Conversation Fork requires an exact transcript revision' };
|
|
6260
|
+
}
|
|
6261
|
+
return this.runAgentLifecycleOperation(
|
|
6262
|
+
agentId,
|
|
6263
|
+
`conversation-fork:${expectedRevision}`,
|
|
6264
|
+
'conversation-fork',
|
|
6265
|
+
'conversation fork',
|
|
6266
|
+
lifecycleToken => this.performAcpConversationFork(agentId, expectedRevision, lifecycleToken),
|
|
6267
|
+
);
|
|
6268
|
+
}
|
|
4606
6269
|
if (agent.providerSessionProvider === 'codex' && agent.providerSessionTemporary === true) {
|
|
4607
6270
|
await this.providerSessionService.resolveTemporaryCodex(agentId, { force: true });
|
|
4608
6271
|
if (agent.providerSessionTemporary === true) {
|
|
@@ -4668,53 +6331,308 @@ class AgentManager extends EventEmitter {
|
|
|
4668
6331
|
});
|
|
4669
6332
|
}
|
|
4670
6333
|
|
|
4671
|
-
async
|
|
4672
|
-
const
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
6334
|
+
async performAcpConversationFork(agentId, expectedRevision, lifecycleToken) {
|
|
6335
|
+
const agent = this.agents.get(agentId);
|
|
6336
|
+
if (!agent) return { error: 'Agent not found' };
|
|
6337
|
+
if (runtimeKind(agent) !== 'acp') {
|
|
6338
|
+
return { error: 'Conversation Fork requires an ACP Chat Agent' };
|
|
6339
|
+
}
|
|
6340
|
+
const provider = String(agent.providerSessionProvider || '').trim();
|
|
6341
|
+
const sourceSessionId = String(agent.providerSessionId || '').trim();
|
|
6342
|
+
if (!provider || !isSafeProviderSessionId(sourceSessionId) || agent.providerSessionTemporary === true) {
|
|
6343
|
+
return { error: 'Conversation Fork requires a stable ACP provider session' };
|
|
6344
|
+
}
|
|
6345
|
+
|
|
6346
|
+
const workspace = effectiveAgentWorkspaceRoot(agent);
|
|
6347
|
+
let acpSessionOptions = {};
|
|
6348
|
+
try {
|
|
6349
|
+
acpSessionOptions = this.acpRuntime.getSessionRequestOptions(agentId);
|
|
6350
|
+
} catch (error) {
|
|
6351
|
+
return { error: error.message || 'Failed to read ACP session options' };
|
|
6352
|
+
}
|
|
6353
|
+
|
|
6354
|
+
if (providerAcpForkMode(provider) === 'target-process') {
|
|
6355
|
+
return this.performTargetProcessAcpConversationFork({
|
|
6356
|
+
agent,
|
|
6357
|
+
provider,
|
|
6358
|
+
sourceSessionId,
|
|
6359
|
+
workspace,
|
|
6360
|
+
expectedRevision,
|
|
6361
|
+
lifecycleToken,
|
|
6362
|
+
acpSessionOptions,
|
|
6363
|
+
});
|
|
6364
|
+
}
|
|
6365
|
+
|
|
6366
|
+
let forkedSessionId = '';
|
|
6367
|
+
try {
|
|
6368
|
+
const forked = await this.acpRuntime.forkSession(agentId, {
|
|
6369
|
+
...acpSessionOptions,
|
|
6370
|
+
cwd: workspace,
|
|
6371
|
+
expectedRevision,
|
|
6372
|
+
requireLoad: true,
|
|
6373
|
+
});
|
|
6374
|
+
forkedSessionId = String(forked?.sessionId || '').trim();
|
|
6375
|
+
} catch (error) {
|
|
6376
|
+
return { error: error.message || 'Failed to fork ACP conversation' };
|
|
6377
|
+
}
|
|
6378
|
+
if (!isSafeProviderSessionId(forkedSessionId) || forkedSessionId === sourceSessionId) {
|
|
6379
|
+
return { error: 'ACP Conversation Fork did not return a distinct resumable session' };
|
|
6380
|
+
}
|
|
6381
|
+
|
|
6382
|
+
const command = buildAgentSessionResumeCommand(provider, forkedSessionId, {
|
|
6383
|
+
cwd: workspace,
|
|
6384
|
+
providerHomePath: agent.providerHomePath || '',
|
|
6385
|
+
});
|
|
6386
|
+
if (!command) {
|
|
6387
|
+
return { error: 'Failed to build provider resume command for the forked ACP session' };
|
|
6388
|
+
}
|
|
6389
|
+
|
|
6390
|
+
return new Promise(resolve => {
|
|
6391
|
+
let settled = false;
|
|
6392
|
+
const finish = async (forkedAgentId, error) => {
|
|
6393
|
+
if (settled) return;
|
|
6394
|
+
settled = true;
|
|
6395
|
+
if (error || !forkedAgentId) {
|
|
6396
|
+
let rollbackError = '';
|
|
6397
|
+
if (!forkedAgentId) {
|
|
6398
|
+
try {
|
|
6399
|
+
await this.acpRuntime.deleteSession(agentId, forkedSessionId);
|
|
6400
|
+
} catch (cleanupError) {
|
|
6401
|
+
rollbackError = cleanupError.message || String(cleanupError);
|
|
6402
|
+
}
|
|
6403
|
+
}
|
|
6404
|
+
resolve({
|
|
6405
|
+
error: [
|
|
6406
|
+
error || 'Failed to start forked ACP Chat Agent',
|
|
6407
|
+
rollbackError ? `forked session cleanup failed: ${rollbackError}` : '',
|
|
6408
|
+
].filter(Boolean).join('; '),
|
|
6409
|
+
...(forkedAgentId ? { retainedAgentId: forkedAgentId } : {}),
|
|
6410
|
+
});
|
|
6411
|
+
return;
|
|
6412
|
+
}
|
|
6413
|
+
resolve({
|
|
6414
|
+
agentId: forkedAgentId,
|
|
6415
|
+
workspace,
|
|
6416
|
+
mode: 'same-worktree',
|
|
6417
|
+
targetRuntime: 'chat',
|
|
6418
|
+
providerSessionId: forkedSessionId,
|
|
6419
|
+
});
|
|
6420
|
+
};
|
|
4678
6421
|
|
|
4679
|
-
const unarchivePromise = (async () => {
|
|
4680
|
-
let session;
|
|
4681
6422
|
try {
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
6423
|
+
const started = this.startAgent(command, workspace, finish, {
|
|
6424
|
+
wantsMain: false,
|
|
6425
|
+
parentAgentId: agent.id,
|
|
6426
|
+
task: agent.task ? `Fork: ${agent.task}` : `Fork of ${agent.command}`,
|
|
6427
|
+
workflowTemplate: agent.workflowTemplate || '',
|
|
6428
|
+
source: 'ui-fork-acp-chat',
|
|
6429
|
+
providerHomeId: agent.providerHomeId || 'default',
|
|
6430
|
+
providerHomePath: agent.providerHomePath || '',
|
|
6431
|
+
providerSessionTitle: agent.providerSessionTitle || '',
|
|
6432
|
+
requiredCliVersion: provider === 'codex' ? (agent.requiredCliVersion || '') : '',
|
|
6433
|
+
projectWorkspace: workspace,
|
|
6434
|
+
agentRuntimeMode: 'chat',
|
|
6435
|
+
acpHistoryMode: 'load',
|
|
6436
|
+
runtimeSwitchVerifiedSessionId: forkedSessionId,
|
|
6437
|
+
forkedFromProviderSessionId: sourceSessionId,
|
|
6438
|
+
lifecycleToken,
|
|
6439
|
+
...acpSessionOptions,
|
|
6440
|
+
...(provider === 'codex'
|
|
6441
|
+
? {
|
|
6442
|
+
codexApprovalMode: agent.launchPermissionMode || undefined,
|
|
6443
|
+
...preserveCodexSessionProfileOptions(),
|
|
6444
|
+
}
|
|
6445
|
+
: {}),
|
|
6446
|
+
...(provider === 'claude'
|
|
6447
|
+
? { claudePermissionMode: agent.launchPermissionMode || undefined }
|
|
6448
|
+
: {}),
|
|
4690
6449
|
});
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
6450
|
+
Promise.resolve(started).then(startedAgentId => {
|
|
6451
|
+
if (!startedAgentId) void finish(null, 'Failed to start forked ACP Chat Agent');
|
|
6452
|
+
}).catch(startError => {
|
|
6453
|
+
void finish(null, startError?.message || 'Failed to start forked ACP Chat Agent');
|
|
6454
|
+
});
|
|
6455
|
+
} catch (startError) {
|
|
6456
|
+
void finish(null, startError?.message || 'Failed to start forked ACP Chat Agent');
|
|
4695
6457
|
}
|
|
4696
|
-
|
|
6458
|
+
});
|
|
6459
|
+
}
|
|
4697
6460
|
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
6461
|
+
async performTargetProcessAcpConversationFork(options) {
|
|
6462
|
+
const {
|
|
6463
|
+
agent,
|
|
6464
|
+
provider,
|
|
6465
|
+
sourceSessionId,
|
|
6466
|
+
workspace,
|
|
6467
|
+
expectedRevision,
|
|
6468
|
+
lifecycleToken,
|
|
6469
|
+
acpSessionOptions,
|
|
6470
|
+
} = options;
|
|
6471
|
+
const command = getProviderAdapter(provider)?.executable || provider;
|
|
6472
|
+
let preparedSessionId = '';
|
|
6473
|
+
let result;
|
|
4711
6474
|
try {
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
6475
|
+
result = await this.acpRuntime.runWithForkReservation(
|
|
6476
|
+
agent.id,
|
|
6477
|
+
{ expectedRevision, requireLoad: true },
|
|
6478
|
+
sourceBinding => {
|
|
6479
|
+
const forkSourceCheckpoint = this.acpRuntime.bindingCheckpoint(sourceBinding).exportCheckpoint();
|
|
6480
|
+
if (!forkSourceCheckpoint?.sessionState) {
|
|
6481
|
+
throw new Error('ACP fork source transcript is unavailable');
|
|
6482
|
+
}
|
|
6483
|
+
return new Promise(resolve => {
|
|
6484
|
+
let settled = false;
|
|
6485
|
+
const finish = (forkedAgentId, error) => {
|
|
6486
|
+
if (settled) return;
|
|
6487
|
+
settled = true;
|
|
6488
|
+
if (error || !forkedAgentId) {
|
|
6489
|
+
resolve({
|
|
6490
|
+
error: error || 'Failed to start forked ACP Chat Agent',
|
|
6491
|
+
...(forkedAgentId ? { retainedAgentId: forkedAgentId } : {}),
|
|
6492
|
+
});
|
|
6493
|
+
return;
|
|
6494
|
+
}
|
|
6495
|
+
const forkedAgent = this.agents.get(forkedAgentId);
|
|
6496
|
+
const forkedSessionId = String(forkedAgent?.providerSessionId || preparedSessionId).trim();
|
|
6497
|
+
resolve({
|
|
6498
|
+
agentId: forkedAgentId,
|
|
6499
|
+
workspace,
|
|
6500
|
+
mode: 'same-worktree',
|
|
6501
|
+
targetRuntime: 'chat',
|
|
6502
|
+
providerSessionId: forkedSessionId,
|
|
6503
|
+
});
|
|
6504
|
+
};
|
|
6505
|
+
|
|
6506
|
+
try {
|
|
6507
|
+
const started = this.startAgent(command, workspace, finish, {
|
|
6508
|
+
wantsMain: false,
|
|
6509
|
+
parentAgentId: agent.id,
|
|
6510
|
+
task: agent.task ? `Fork: ${agent.task}` : `Fork of ${agent.command}`,
|
|
6511
|
+
workflowTemplate: agent.workflowTemplate || '',
|
|
6512
|
+
source: 'ui-fork-acp-chat',
|
|
6513
|
+
providerHomeId: agent.providerHomeId || 'default',
|
|
6514
|
+
providerHomePath: agent.providerHomePath || '',
|
|
6515
|
+
providerSessionTitle: agent.providerSessionTitle || '',
|
|
6516
|
+
projectWorkspace: workspace,
|
|
6517
|
+
agentRuntimeMode: 'chat',
|
|
6518
|
+
acpForkSourceSessionId: sourceSessionId,
|
|
6519
|
+
acpForkSourceCheckpoint: forkSourceCheckpoint,
|
|
6520
|
+
forkedFromProviderSessionId: sourceSessionId,
|
|
6521
|
+
lifecycleToken,
|
|
6522
|
+
...acpSessionOptions,
|
|
6523
|
+
onAcpForkSessionCreated: sessionId => {
|
|
6524
|
+
preparedSessionId = String(sessionId || '').trim();
|
|
6525
|
+
},
|
|
6526
|
+
onAcpSessionPrepared: prepared => {
|
|
6527
|
+
preparedSessionId = String(prepared?.sessionId || '').trim();
|
|
6528
|
+
},
|
|
6529
|
+
...(provider === 'claude'
|
|
6530
|
+
? { claudePermissionMode: agent.launchPermissionMode || undefined }
|
|
6531
|
+
: {}),
|
|
6532
|
+
});
|
|
6533
|
+
Promise.resolve(started).then(startedAgentId => {
|
|
6534
|
+
if (!startedAgentId) void finish(null, 'Failed to start forked ACP Chat Agent');
|
|
6535
|
+
}).catch(startError => {
|
|
6536
|
+
void finish(null, startError?.message || 'Failed to start forked ACP Chat Agent');
|
|
6537
|
+
});
|
|
6538
|
+
} catch (startError) {
|
|
6539
|
+
void finish(null, startError?.message || 'Failed to start forked ACP Chat Agent');
|
|
6540
|
+
}
|
|
6541
|
+
});
|
|
6542
|
+
},
|
|
6543
|
+
);
|
|
6544
|
+
} catch (error) {
|
|
6545
|
+
return { error: error.message || 'Failed to fork ACP conversation' };
|
|
6546
|
+
}
|
|
6547
|
+
if (
|
|
6548
|
+
result?.error
|
|
6549
|
+
&& !result.retainedAgentId
|
|
6550
|
+
&& isSafeProviderSessionId(preparedSessionId)
|
|
6551
|
+
) {
|
|
6552
|
+
try {
|
|
6553
|
+
await this.acpRuntime.deleteSession(agent.id, preparedSessionId);
|
|
6554
|
+
} catch (cleanupError) {
|
|
6555
|
+
result.retainedProviderSessionId = preparedSessionId;
|
|
6556
|
+
result.error = `${result.error}; forked session ${preparedSessionId} cleanup failed: ${cleanupError.message || cleanupError}`;
|
|
4716
6557
|
}
|
|
4717
6558
|
}
|
|
6559
|
+
return result;
|
|
6560
|
+
}
|
|
6561
|
+
|
|
6562
|
+
enqueueCodexSessionMutation(sessionId, options, type, operation, joinSameType = false) {
|
|
6563
|
+
const providerHomeId = String(options?.providerHomeId || 'default').trim() || 'default';
|
|
6564
|
+
const key = JSON.stringify([providerHomeId, sessionId]);
|
|
6565
|
+
const current = this.codexSessionMutationQueues.get(key);
|
|
6566
|
+
if (joinSameType && current?.type === type) return current.promise;
|
|
6567
|
+
|
|
6568
|
+
const previous = current?.promise || Promise.resolve();
|
|
6569
|
+
const next = previous.catch(() => {}).then(operation);
|
|
6570
|
+
const entry = { type, promise: next };
|
|
6571
|
+
this.codexSessionMutationQueues.set(key, entry);
|
|
6572
|
+
void next.finally(() => {
|
|
6573
|
+
if (this.codexSessionMutationQueues.get(key) === entry) {
|
|
6574
|
+
this.codexSessionMutationQueues.delete(key);
|
|
6575
|
+
}
|
|
6576
|
+
}).catch(() => {});
|
|
6577
|
+
return next;
|
|
6578
|
+
}
|
|
6579
|
+
|
|
6580
|
+
ensureCodexSessionAvailable(sessionId, options = {}) {
|
|
6581
|
+
const providerHomeId = String(options.providerHomeId || 'default').trim() || 'default';
|
|
6582
|
+
const providerHomePath = String(options.providerHomePath || '').trim();
|
|
6583
|
+
return this.enqueueCodexSessionMutation(
|
|
6584
|
+
sessionId,
|
|
6585
|
+
{ providerHomeId, providerHomePath },
|
|
6586
|
+
'unarchive',
|
|
6587
|
+
async () => {
|
|
6588
|
+
let session;
|
|
6589
|
+
try {
|
|
6590
|
+
session = await findAgentSession('codex', sessionId, {
|
|
6591
|
+
limit: 1000,
|
|
6592
|
+
providerLimit: 1000,
|
|
6593
|
+
scanLimit: 5000,
|
|
6594
|
+
providerHomeId,
|
|
6595
|
+
providerHomes: options.providerHomes || (providerHomePath
|
|
6596
|
+
? { codex: [{ id: providerHomeId, path: providerHomePath }] }
|
|
6597
|
+
: undefined),
|
|
6598
|
+
});
|
|
6599
|
+
} catch (error) {
|
|
6600
|
+
return {
|
|
6601
|
+
error: `Failed to inspect Codex session before unarchiving: ${error && (error.message || error)}`,
|
|
6602
|
+
};
|
|
6603
|
+
}
|
|
6604
|
+
if (!session || session.archived !== true) return null;
|
|
6605
|
+
|
|
6606
|
+
const result = await this.unarchiveCodexSession(sessionId, {
|
|
6607
|
+
...session,
|
|
6608
|
+
cwd: options.cwd || session.cwd || session.workspace,
|
|
6609
|
+
providerHomePath: session.providerHomePath || providerHomePath,
|
|
6610
|
+
});
|
|
6611
|
+
return result?.error ? { error: result.error } : null;
|
|
6612
|
+
},
|
|
6613
|
+
true,
|
|
6614
|
+
);
|
|
6615
|
+
}
|
|
6616
|
+
|
|
6617
|
+
async ensureCodexSessionAvailableForFork(agent, resumedSession, sourceWorkspace) {
|
|
6618
|
+
const providerHomeId = agent.providerHomeId || resumedSession.providerHomeId || 'default';
|
|
6619
|
+
const providerHomePath = agent.providerHomePath || '';
|
|
6620
|
+
const sessionId = resumedSession.sessionId;
|
|
6621
|
+
const result = await this.ensureCodexSessionAvailable(sessionId, {
|
|
6622
|
+
providerHomeId,
|
|
6623
|
+
providerHomePath,
|
|
6624
|
+
providerHomes: providerHomePath
|
|
6625
|
+
? { codex: [{ id: providerHomeId, path: providerHomePath }] }
|
|
6626
|
+
: undefined,
|
|
6627
|
+
// Fork is an action on the live Farming Agent. Its current workspace is
|
|
6628
|
+
// authoritative even when the older provider history cwd no longer exists.
|
|
6629
|
+
cwd: sourceWorkspace,
|
|
6630
|
+
});
|
|
6631
|
+
if (!result?.error) return null;
|
|
6632
|
+
const detail = result.error.startsWith('Failed to inspect Codex session')
|
|
6633
|
+
? result.error.replace('before unarchiving', 'before forking')
|
|
6634
|
+
: `Codex session ${sessionId} is archived and could not be unarchived before forking: ${result.error}`;
|
|
6635
|
+
return { error: detail };
|
|
4718
6636
|
}
|
|
4719
6637
|
|
|
4720
6638
|
recordTaskHistory(agent, options = {}) {
|
|
@@ -4748,36 +6666,126 @@ class AgentManager extends EventEmitter {
|
|
|
4748
6666
|
}
|
|
4749
6667
|
}
|
|
4750
6668
|
|
|
4751
|
-
|
|
6669
|
+
archiveAgent(agentId, options = {}) {
|
|
6670
|
+
if (options.skipRecoveryWait !== true && !this.recoveryComplete) {
|
|
6671
|
+
return this.whenRecovered().then(() => this.archiveAgent(agentId, {
|
|
6672
|
+
...options,
|
|
6673
|
+
skipRecoveryWait: true,
|
|
6674
|
+
}));
|
|
6675
|
+
}
|
|
6676
|
+
const inFlight = this.agentLifecycleOperations.get(agentId);
|
|
6677
|
+
if (inFlight) {
|
|
6678
|
+
return this.runAgentLifecycleOperation(
|
|
6679
|
+
agentId,
|
|
6680
|
+
'archive',
|
|
6681
|
+
'archive',
|
|
6682
|
+
'archive',
|
|
6683
|
+
lifecycleToken => this.performArchiveAgent(agentId, options, lifecycleToken),
|
|
6684
|
+
);
|
|
6685
|
+
}
|
|
4752
6686
|
const agent = this.agents.get(agentId);
|
|
4753
6687
|
if (!agent) {
|
|
4754
|
-
return { error: 'Agent not found' };
|
|
6688
|
+
return Promise.resolve({ error: 'Agent not found' });
|
|
4755
6689
|
}
|
|
4756
6690
|
if (agent.id === this.mainAgentId) {
|
|
4757
|
-
return { error: 'Main Agent cannot be archived' };
|
|
6691
|
+
return Promise.resolve({ error: 'Main Agent cannot be archived' });
|
|
4758
6692
|
}
|
|
4759
6693
|
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
return { agentId, archived: true, removed: true, removedMainPageSessionKeys };
|
|
4769
|
-
}
|
|
6694
|
+
return this.runAgentLifecycleOperation(
|
|
6695
|
+
agentId,
|
|
6696
|
+
'archive',
|
|
6697
|
+
'archive',
|
|
6698
|
+
'archive',
|
|
6699
|
+
lifecycleToken => this.performArchiveAgent(agentId, options, lifecycleToken),
|
|
6700
|
+
);
|
|
6701
|
+
}
|
|
4770
6702
|
|
|
6703
|
+
async performArchiveAgent(agentId, options, lifecycleToken) {
|
|
6704
|
+
const agent = this.agents.get(agentId);
|
|
6705
|
+
if (!agent) return { error: 'Agent not found' };
|
|
6706
|
+
const admission = this.beginPersistentAgentOperation(agent, 'archive', 'archive', {
|
|
6707
|
+
reason: options.reason || 'manual-archive',
|
|
6708
|
+
structuredProcessProofRequired: runtimeKind(agent) === 'acp',
|
|
6709
|
+
});
|
|
6710
|
+
if (admission.error) return { agentId, error: admission.error };
|
|
6711
|
+
const operationId = admission.operation.id;
|
|
4771
6712
|
const killResult = await this.killAgent(agentId, {
|
|
4772
6713
|
reason: options.reason || 'manual-archive',
|
|
4773
|
-
recordHistory:
|
|
6714
|
+
recordHistory: false,
|
|
4774
6715
|
requireEngineExit: true,
|
|
6716
|
+
retainAgentRecord: true,
|
|
6717
|
+
emitUpdate: false,
|
|
6718
|
+
lifecycleToken,
|
|
6719
|
+
persistDeleteOperation: false,
|
|
6720
|
+
skipRecoveryWait: options.skipRecoveryWait === true,
|
|
4775
6721
|
});
|
|
4776
|
-
if (killResult?.error)
|
|
4777
|
-
|
|
4778
|
-
|
|
6722
|
+
if (killResult?.error) {
|
|
6723
|
+
try {
|
|
6724
|
+
this.transitionPersistentAgentOperation(agent, operationId, 'blocked', killResult.error);
|
|
6725
|
+
} catch (error) {
|
|
6726
|
+
return {
|
|
6727
|
+
...killResult,
|
|
6728
|
+
error: `${killResult.error}; failed to persist blocked Archive: ${error.message || error}`,
|
|
6729
|
+
};
|
|
6730
|
+
}
|
|
6731
|
+
return { ...killResult, operationId };
|
|
6732
|
+
}
|
|
6733
|
+
let removedMainPageSessionKeys = [];
|
|
6734
|
+
try {
|
|
6735
|
+
this.transitionPersistentAgentOperation(agent, operationId, 'succeeded', '', {
|
|
6736
|
+
visibleOnMainPage: false,
|
|
6737
|
+
archived: true,
|
|
6738
|
+
archivedAt: Date.now(),
|
|
6739
|
+
runtimeAgentId: '',
|
|
6740
|
+
});
|
|
6741
|
+
} catch (error) {
|
|
6742
|
+
this.emit('update');
|
|
6743
|
+
return {
|
|
6744
|
+
agentId,
|
|
6745
|
+
error: `Agent stopped, but archive metadata could not be saved: ${error.message || error}`,
|
|
6746
|
+
stopped: true,
|
|
6747
|
+
archived: false,
|
|
6748
|
+
retryable: true,
|
|
6749
|
+
operationId,
|
|
6750
|
+
removedMainPageSessionKeys,
|
|
6751
|
+
};
|
|
6752
|
+
}
|
|
6753
|
+
let metadataWarning = '';
|
|
6754
|
+
try {
|
|
6755
|
+
removedMainPageSessionKeys = this.removeMainPageProviderSessionsForAgents([agent]);
|
|
6756
|
+
} catch (error) {
|
|
6757
|
+
metadataWarning = `Agent archived, but main-page membership cleanup failed: ${error.message || error}`;
|
|
6758
|
+
console.error(metadataWarning);
|
|
6759
|
+
}
|
|
6760
|
+
|
|
6761
|
+
let historyWarning = '';
|
|
6762
|
+
if (options.recordHistory !== false && !isEphemeralShellAgent(agent)) {
|
|
6763
|
+
const previousTaskHistory = this.taskHistory;
|
|
6764
|
+
try {
|
|
6765
|
+
this.recordTaskHistory(agent, {
|
|
6766
|
+
reason: options.reason || 'manual-archive',
|
|
6767
|
+
archivedAt: Date.now(),
|
|
6768
|
+
});
|
|
6769
|
+
} catch (error) {
|
|
6770
|
+
this.taskHistory = previousTaskHistory;
|
|
6771
|
+
historyWarning = `Agent stopped, but history could not be saved: ${error.message || error}`;
|
|
6772
|
+
console.error(historyWarning);
|
|
6773
|
+
}
|
|
6774
|
+
}
|
|
6775
|
+
this.forgetStoppedAgentRecord(agentId);
|
|
4779
6776
|
if (options.scheduleProviderArchive !== false) this.scheduleCodexSessionArchive(agent);
|
|
4780
|
-
return {
|
|
6777
|
+
return {
|
|
6778
|
+
agentId,
|
|
6779
|
+
archived: true,
|
|
6780
|
+
removed: true,
|
|
6781
|
+
operationId,
|
|
6782
|
+
removedMainPageSessionKeys,
|
|
6783
|
+
...(
|
|
6784
|
+
metadataWarning || historyWarning
|
|
6785
|
+
? { warning: [metadataWarning, historyWarning].filter(Boolean).join('; ') }
|
|
6786
|
+
: {}
|
|
6787
|
+
),
|
|
6788
|
+
};
|
|
4781
6789
|
}
|
|
4782
6790
|
|
|
4783
6791
|
scheduleCodexSessionArchive(agent) {
|
|
@@ -4797,8 +6805,16 @@ class AgentManager extends EventEmitter {
|
|
|
4797
6805
|
workspace: agent.projectWorkspace || '',
|
|
4798
6806
|
providerHomePath: agent.providerHomePath || '',
|
|
4799
6807
|
};
|
|
4800
|
-
void
|
|
4801
|
-
|
|
6808
|
+
void this.enqueueCodexSessionMutation(
|
|
6809
|
+
sessionId,
|
|
6810
|
+
{
|
|
6811
|
+
providerHomeId: agent.providerHomeId || 'default',
|
|
6812
|
+
providerHomePath: agent.providerHomePath || '',
|
|
6813
|
+
},
|
|
6814
|
+
'archive',
|
|
6815
|
+
() => this.archiveCodexSession(sessionId, session),
|
|
6816
|
+
true,
|
|
6817
|
+
)
|
|
4802
6818
|
.then(result => {
|
|
4803
6819
|
if (result?.error) {
|
|
4804
6820
|
console.error(`Failed to archive Codex session ${sessionId}: ${result.error}`);
|
|
@@ -4809,54 +6825,399 @@ class AgentManager extends EventEmitter {
|
|
|
4809
6825
|
});
|
|
4810
6826
|
}
|
|
4811
6827
|
|
|
4812
|
-
|
|
6828
|
+
admitPersistentDelete(agent, options = {}) {
|
|
6829
|
+
const activeOperation = activeLifecycleOperation(agent);
|
|
6830
|
+
if (
|
|
6831
|
+
activeOperation?.type === 'create'
|
|
6832
|
+
&& ['pending', 'blocked'].includes(activeOperation.state)
|
|
6833
|
+
) {
|
|
6834
|
+
const previousJournal = lifecycleJournal(agent);
|
|
6835
|
+
try {
|
|
6836
|
+
transitionLifecycleOperation(
|
|
6837
|
+
agent,
|
|
6838
|
+
activeOperation.id,
|
|
6839
|
+
'cancelled',
|
|
6840
|
+
'Create was superseded by Delete',
|
|
6841
|
+
);
|
|
6842
|
+
const admittedDelete = beginLifecycleOperation(
|
|
6843
|
+
agent,
|
|
6844
|
+
'delete',
|
|
6845
|
+
'delete',
|
|
6846
|
+
{
|
|
6847
|
+
reason: options.reason || 'manual-kill',
|
|
6848
|
+
structuredProcessProofRequired: runtimeKind(agent) === 'acp',
|
|
6849
|
+
},
|
|
6850
|
+
);
|
|
6851
|
+
const persistentSessionId = this.ensurePersistentAgentSession(agent);
|
|
6852
|
+
if (
|
|
6853
|
+
typeof this.configManager?.ensureAgentSessionRecord === 'function'
|
|
6854
|
+
&& !persistentSessionId
|
|
6855
|
+
) {
|
|
6856
|
+
throw new Error('Agent session store did not return a persistent id');
|
|
6857
|
+
}
|
|
6858
|
+
return {
|
|
6859
|
+
operationId: admittedDelete.operation.id,
|
|
6860
|
+
completesBlockedCreate: false,
|
|
6861
|
+
};
|
|
6862
|
+
} catch (error) {
|
|
6863
|
+
agent.lifecycleJournal = previousJournal;
|
|
6864
|
+
return { error: `Failed to persist Create cleanup retry: ${error.message || error}` };
|
|
6865
|
+
}
|
|
6866
|
+
}
|
|
6867
|
+
const admitted = this.beginPersistentAgentOperation(agent, 'delete', 'delete', {
|
|
6868
|
+
reason: options.reason || 'manual-kill',
|
|
6869
|
+
structuredProcessProofRequired: runtimeKind(agent) === 'acp',
|
|
6870
|
+
});
|
|
6871
|
+
if (admitted.error) return { error: admitted.error };
|
|
6872
|
+
return {
|
|
6873
|
+
operationId: admitted.operation.id,
|
|
6874
|
+
completesBlockedCreate: false,
|
|
6875
|
+
};
|
|
6876
|
+
}
|
|
6877
|
+
|
|
6878
|
+
async requestKillAgent(agentId, options = {}) {
|
|
6879
|
+
if (options.skipRecoveryWait !== true) await this.whenRecovered();
|
|
6880
|
+
const existing = this.agentLifecycleOperations.get(agentId);
|
|
6881
|
+
if (existing && existing.key !== 'kill') {
|
|
6882
|
+
await existing.promise.catch(() => {});
|
|
6883
|
+
return this.requestKillAgent(agentId, options);
|
|
6884
|
+
}
|
|
6885
|
+
const completion = this.killAgent(agentId, options);
|
|
6886
|
+
const operation = activeLifecycleOperation(this.agents.get(agentId));
|
|
6887
|
+
if (!operation || !['create', 'delete'].includes(operation.type)) {
|
|
6888
|
+
const result = await completion;
|
|
6889
|
+
return { result, completion: Promise.resolve(result) };
|
|
6890
|
+
}
|
|
6891
|
+
return {
|
|
6892
|
+
result: {
|
|
6893
|
+
agentId,
|
|
6894
|
+
accepted: true,
|
|
6895
|
+
operationId: operation.id,
|
|
6896
|
+
operationType: operation.type,
|
|
6897
|
+
operationState: operation.state,
|
|
6898
|
+
},
|
|
6899
|
+
completion,
|
|
6900
|
+
};
|
|
6901
|
+
}
|
|
6902
|
+
|
|
6903
|
+
killAgent(agentId, options = {}) {
|
|
6904
|
+
if (options.skipRecoveryWait !== true && !this.recoveryComplete) {
|
|
6905
|
+
return this.whenRecovered().then(() => this.killAgent(agentId, {
|
|
6906
|
+
...options,
|
|
6907
|
+
skipRecoveryWait: true,
|
|
6908
|
+
}));
|
|
6909
|
+
}
|
|
6910
|
+
const inFlight = this.agentLifecycleOperations.get(agentId);
|
|
6911
|
+
if (inFlight) {
|
|
6912
|
+
if (options.lifecycleToken && inFlight.token === options.lifecycleToken) {
|
|
6913
|
+
return this.performKillAgent(agentId, options);
|
|
6914
|
+
}
|
|
6915
|
+
return this.runAgentLifecycleOperation(
|
|
6916
|
+
agentId,
|
|
6917
|
+
'kill',
|
|
6918
|
+
'kill',
|
|
6919
|
+
'kill',
|
|
6920
|
+
lifecycleToken => {
|
|
6921
|
+
const agent = this.agents.get(agentId);
|
|
6922
|
+
if (!agent) return { agentId, killed: true, missing: true };
|
|
6923
|
+
let queuedOptions = options;
|
|
6924
|
+
if (options.persistDeleteOperation !== false && !options.persistentOperationId) {
|
|
6925
|
+
const admitted = this.admitPersistentDelete(agent, options);
|
|
6926
|
+
if (admitted.error) return { agentId, error: admitted.error };
|
|
6927
|
+
queuedOptions = {
|
|
6928
|
+
...options,
|
|
6929
|
+
persistentOperationId: admitted.operationId,
|
|
6930
|
+
completesBlockedCreate: admitted.completesBlockedCreate,
|
|
6931
|
+
};
|
|
6932
|
+
}
|
|
6933
|
+
return this.performKillAgent(agentId, { ...queuedOptions, lifecycleToken });
|
|
6934
|
+
},
|
|
6935
|
+
);
|
|
6936
|
+
}
|
|
6937
|
+
const agent = this.agents.get(agentId);
|
|
6938
|
+
if (!agent) return Promise.resolve({ agentId, killed: true, missing: true });
|
|
6939
|
+
let admittedOptions = options;
|
|
6940
|
+
if (options.persistDeleteOperation !== false && !options.persistentOperationId) {
|
|
6941
|
+
const admitted = this.admitPersistentDelete(agent, options);
|
|
6942
|
+
if (admitted.error) return Promise.resolve({ agentId, error: admitted.error });
|
|
6943
|
+
admittedOptions = {
|
|
6944
|
+
...options,
|
|
6945
|
+
persistentOperationId: admitted.operationId,
|
|
6946
|
+
completesBlockedCreate: admitted.completesBlockedCreate,
|
|
6947
|
+
};
|
|
6948
|
+
}
|
|
6949
|
+
|
|
6950
|
+
return this.runAgentLifecycleOperation(
|
|
6951
|
+
agentId,
|
|
6952
|
+
'kill',
|
|
6953
|
+
'kill',
|
|
6954
|
+
'kill',
|
|
6955
|
+
lifecycleToken => this.performKillAgent(agentId, { ...admittedOptions, lifecycleToken }),
|
|
6956
|
+
);
|
|
6957
|
+
}
|
|
6958
|
+
|
|
6959
|
+
async performKillAgent(agentId, options = {}) {
|
|
4813
6960
|
const agent = this.agents.get(agentId);
|
|
4814
6961
|
if (!agent) return { agentId, killed: true, missing: true };
|
|
4815
6962
|
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
6963
|
+
let persistentOperationId = options.persistentOperationId || '';
|
|
6964
|
+
const completesBlockedCreate = options.completesBlockedCreate === true;
|
|
6965
|
+
const cleanupFailure = (message, kind = '') => {
|
|
6966
|
+
const error = String(message || 'Failed to stop Agent runtime');
|
|
6967
|
+
if (kind === 'acp' || kind === 'json') {
|
|
6968
|
+
return this.markStructuredAgentCleanupUncertain(
|
|
6969
|
+
agentId,
|
|
6970
|
+
kind,
|
|
6971
|
+
error,
|
|
6972
|
+
{ operationId: persistentOperationId },
|
|
6973
|
+
);
|
|
4823
6974
|
}
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
if (
|
|
4827
|
-
|
|
6975
|
+
agent.status = 'error';
|
|
6976
|
+
agent.engineStatus = 'cleanup-uncertain';
|
|
6977
|
+
if (persistentOperationId) {
|
|
6978
|
+
try {
|
|
6979
|
+
this.transitionPersistentAgentOperation(agent, persistentOperationId, 'blocked', error);
|
|
6980
|
+
} catch (persistError) {
|
|
6981
|
+
return {
|
|
6982
|
+
agentId,
|
|
6983
|
+
error: `${error}; failed to persist blocked Agent operation: ${persistError.message || persistError}`,
|
|
6984
|
+
cleanupUncertain: true,
|
|
6985
|
+
retryable: true,
|
|
6986
|
+
};
|
|
6987
|
+
}
|
|
4828
6988
|
}
|
|
4829
|
-
|
|
6989
|
+
this.emit('update');
|
|
6990
|
+
return { agentId, error, cleanupUncertain: true, retryable: true };
|
|
6991
|
+
};
|
|
4830
6992
|
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
6993
|
+
const requireEngineExit = options.requireEngineExit !== false;
|
|
6994
|
+
const currentRuntimeKind = runtimeKind(agent);
|
|
6995
|
+
if (!this.verifiedStoppedAgentIds.has(agentId)) {
|
|
6996
|
+
this.permissionRestartSuppressedAgentIds.add(agentId);
|
|
6997
|
+
try {
|
|
6998
|
+
if (currentRuntimeKind === 'acp') {
|
|
6999
|
+
if (typeof this.acpRuntime?.unregisterAgentAndWait !== 'function') {
|
|
7000
|
+
return cleanupFailure('ACP runtime exit cannot be verified', 'acp');
|
|
7001
|
+
}
|
|
7002
|
+
const stopped = await this.acpRuntime.unregisterAgentAndWait(agentId);
|
|
7003
|
+
if (stopped !== true) {
|
|
7004
|
+
if (!agent.structuredRuntimeProcess) {
|
|
7005
|
+
if (
|
|
7006
|
+
options.acknowledgeUnprovenAcpExit !== true
|
|
7007
|
+
|| agent.requiresProcessExitAcknowledgement !== true
|
|
7008
|
+
) {
|
|
7009
|
+
return cleanupFailure('ACP runtime binding is missing; process exit cannot be verified', 'acp');
|
|
7010
|
+
}
|
|
7011
|
+
const previousAcknowledgedAt = agent.legacyAcpProcessExitAcknowledgedAt;
|
|
7012
|
+
agent.legacyAcpProcessExitAcknowledgedAt = Date.now();
|
|
7013
|
+
agent.requiresProcessExitAcknowledgement = false;
|
|
7014
|
+
try {
|
|
7015
|
+
this.ensurePersistentAgentSession(agent);
|
|
7016
|
+
} catch (error) {
|
|
7017
|
+
agent.legacyAcpProcessExitAcknowledgedAt = previousAcknowledgedAt;
|
|
7018
|
+
agent.requiresProcessExitAcknowledgement = true;
|
|
7019
|
+
throw error;
|
|
7020
|
+
}
|
|
7021
|
+
} else {
|
|
7022
|
+
const cleanup = await this.stopPersistedAcpProcessGroup(agent.structuredRuntimeProcess);
|
|
7023
|
+
if (cleanup.stopped !== true) {
|
|
7024
|
+
return cleanupFailure(
|
|
7025
|
+
'ACP runtime binding is missing and the persisted process identity could not be safely stopped',
|
|
7026
|
+
'acp',
|
|
7027
|
+
);
|
|
7028
|
+
}
|
|
7029
|
+
}
|
|
7030
|
+
}
|
|
7031
|
+
} else if (currentRuntimeKind === 'json') {
|
|
7032
|
+
if (typeof this.jsonCliRuntime?.unregisterAgentAndWait !== 'function') {
|
|
7033
|
+
return cleanupFailure('JSON runtime exit cannot be verified', 'json');
|
|
7034
|
+
}
|
|
7035
|
+
const stopped = await this.jsonCliRuntime.unregisterAgentAndWait(agentId);
|
|
7036
|
+
if (stopped !== true) {
|
|
7037
|
+
return cleanupFailure('JSON runtime binding is missing; process exit cannot be verified', 'json');
|
|
7038
|
+
}
|
|
7039
|
+
} else {
|
|
7040
|
+
const engine = this.engineBridge.getEngine(agent.engineName);
|
|
7041
|
+
if (requireEngineExit && !engine) {
|
|
7042
|
+
return cleanupFailure('Agent runtime is unavailable; process exit cannot be verified');
|
|
7043
|
+
}
|
|
7044
|
+
let killError = null;
|
|
4835
7045
|
try {
|
|
4836
|
-
|
|
7046
|
+
if (engine) {
|
|
7047
|
+
await engine.killSession(agentId);
|
|
7048
|
+
}
|
|
4837
7049
|
} catch (error) {
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
7050
|
+
console.error('Failed to kill agent:', error);
|
|
7051
|
+
killError = error;
|
|
7052
|
+
}
|
|
7053
|
+
|
|
7054
|
+
if (requireEngineExit && engine) {
|
|
7055
|
+
const deadline = Date.now() + 3000;
|
|
7056
|
+
let lastState = null;
|
|
7057
|
+
while (Date.now() < deadline) {
|
|
7058
|
+
try {
|
|
7059
|
+
lastState = await engine.getSessionState(agentId);
|
|
7060
|
+
} catch (error) {
|
|
7061
|
+
if (isSessionNotAvailableError(error)) {
|
|
7062
|
+
lastState = null;
|
|
7063
|
+
break;
|
|
7064
|
+
}
|
|
7065
|
+
return cleanupFailure(error.message || 'Failed to verify Agent process exit');
|
|
7066
|
+
}
|
|
7067
|
+
if (!isLiveEngineSessionState(lastState)) break;
|
|
7068
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
4841
7069
|
}
|
|
4842
|
-
|
|
7070
|
+
if (isLiveEngineSessionState(lastState)) {
|
|
7071
|
+
return cleanupFailure(killError?.message || 'Agent process did not exit within 3 seconds');
|
|
7072
|
+
}
|
|
7073
|
+
} else if (killError) {
|
|
7074
|
+
return cleanupFailure(killError.message || 'Failed to stop Agent runtime');
|
|
4843
7075
|
}
|
|
4844
|
-
if (!isLiveEngineSessionState(lastState)) break;
|
|
4845
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
4846
7076
|
}
|
|
4847
|
-
|
|
4848
|
-
|
|
7077
|
+
} catch (error) {
|
|
7078
|
+
if (currentRuntimeKind === 'acp' || currentRuntimeKind === 'json') {
|
|
7079
|
+
return cleanupFailure(error.message || 'Failed to stop Agent runtime', currentRuntimeKind);
|
|
7080
|
+
}
|
|
7081
|
+
return cleanupFailure(error.message || 'Failed to stop Agent runtime');
|
|
7082
|
+
} finally {
|
|
7083
|
+
this.permissionRestartSuppressedAgentIds.delete(agentId);
|
|
4849
7084
|
}
|
|
4850
7085
|
}
|
|
7086
|
+
if (currentRuntimeKind === 'acp' || currentRuntimeKind === 'json') {
|
|
7087
|
+
agent.structuredRuntimeProcess = null;
|
|
7088
|
+
}
|
|
4851
7089
|
|
|
4852
|
-
if (
|
|
4853
|
-
|
|
7090
|
+
if (completesBlockedCreate) {
|
|
7091
|
+
try {
|
|
7092
|
+
this.transitionPersistentAgentOperation(
|
|
7093
|
+
agent,
|
|
7094
|
+
persistentOperationId,
|
|
7095
|
+
'failed',
|
|
7096
|
+
'Create runtime was stopped by Delete recovery',
|
|
7097
|
+
);
|
|
7098
|
+
} catch (error) {
|
|
7099
|
+
const message = `Agent stopped, but Create cleanup could not be committed: ${error.message || error}`;
|
|
7100
|
+
this.verifiedStoppedAgentIds.add(agentId);
|
|
7101
|
+
agent.status = 'stopped';
|
|
7102
|
+
agent.engineStatus = 'exited';
|
|
7103
|
+
this.emit('update');
|
|
7104
|
+
return {
|
|
7105
|
+
agentId,
|
|
7106
|
+
error: message,
|
|
7107
|
+
stopped: true,
|
|
7108
|
+
retryable: true,
|
|
7109
|
+
operationId: persistentOperationId,
|
|
7110
|
+
};
|
|
7111
|
+
}
|
|
7112
|
+
const admittedDelete = this.beginPersistentAgentOperation(agent, 'delete', 'delete', {
|
|
4854
7113
|
reason: options.reason || 'manual-kill',
|
|
4855
|
-
|
|
7114
|
+
structuredProcessProofRequired: runtimeKind(agent) === 'acp',
|
|
4856
7115
|
});
|
|
7116
|
+
if (admittedDelete.error) {
|
|
7117
|
+
return { agentId, error: admittedDelete.error, stopped: true, retryable: true };
|
|
7118
|
+
}
|
|
7119
|
+
persistentOperationId = admittedDelete.operation.id;
|
|
7120
|
+
}
|
|
7121
|
+
|
|
7122
|
+
if (persistentOperationId) {
|
|
7123
|
+
try {
|
|
7124
|
+
this.transitionPersistentAgentOperation(agent, persistentOperationId, 'succeeded', '', {
|
|
7125
|
+
visibleOnMainPage: false,
|
|
7126
|
+
archived: true,
|
|
7127
|
+
archivedAt: Date.now(),
|
|
7128
|
+
runtimeAgentId: '',
|
|
7129
|
+
});
|
|
7130
|
+
this.removeMainPageProviderSessionsForAgents([agent]);
|
|
7131
|
+
} catch (error) {
|
|
7132
|
+
const message = `Agent stopped, but Delete metadata could not be committed: ${error.message || error}`;
|
|
7133
|
+
console.error(message);
|
|
7134
|
+
this.verifiedStoppedAgentIds.add(agentId);
|
|
7135
|
+
agent.status = 'stopped';
|
|
7136
|
+
agent.engineStatus = 'exited';
|
|
7137
|
+
this.emit('update');
|
|
7138
|
+
return {
|
|
7139
|
+
agentId,
|
|
7140
|
+
error: message,
|
|
7141
|
+
stopped: true,
|
|
7142
|
+
retryable: true,
|
|
7143
|
+
operationId: persistentOperationId,
|
|
7144
|
+
};
|
|
7145
|
+
}
|
|
7146
|
+
}
|
|
7147
|
+
|
|
7148
|
+
let historyWarning = '';
|
|
7149
|
+
if (options.recordHistory !== false && !isEphemeralShellAgent(agent)) {
|
|
7150
|
+
const previousTaskHistory = this.taskHistory;
|
|
7151
|
+
try {
|
|
7152
|
+
this.recordTaskHistory(agent, {
|
|
7153
|
+
reason: options.reason || 'manual-kill',
|
|
7154
|
+
archivedAt: Date.now(),
|
|
7155
|
+
});
|
|
7156
|
+
} catch (error) {
|
|
7157
|
+
this.taskHistory = previousTaskHistory;
|
|
7158
|
+
historyWarning = `Agent stopped, but history could not be saved: ${error.message || error}`;
|
|
7159
|
+
console.error(historyWarning);
|
|
7160
|
+
}
|
|
4857
7161
|
}
|
|
4858
7162
|
|
|
7163
|
+
if (options.retainAgentRecord === true) {
|
|
7164
|
+
this.verifiedStoppedAgentIds.add(agentId);
|
|
7165
|
+
agent.status = 'stopped';
|
|
7166
|
+
agent.engineStatus = 'exited';
|
|
7167
|
+
if (options.emitUpdate !== false) this.emit('update');
|
|
7168
|
+
return {
|
|
7169
|
+
agentId,
|
|
7170
|
+
killed: true,
|
|
7171
|
+
retained: true,
|
|
7172
|
+
...(historyWarning ? { warning: historyWarning } : {}),
|
|
7173
|
+
};
|
|
7174
|
+
}
|
|
7175
|
+
|
|
7176
|
+
this.forgetStoppedAgentRecord(agentId, { emitUpdate: options.emitUpdate !== false });
|
|
7177
|
+
return {
|
|
7178
|
+
agentId,
|
|
7179
|
+
killed: true,
|
|
7180
|
+
...(persistentOperationId ? { operationId: persistentOperationId } : {}),
|
|
7181
|
+
...(historyWarning ? { warning: historyWarning } : {}),
|
|
7182
|
+
};
|
|
7183
|
+
}
|
|
7184
|
+
|
|
7185
|
+
markStructuredAgentCleanupUncertain(agentId, kind, error, options = {}) {
|
|
7186
|
+
const agent = this.agents.get(agentId);
|
|
7187
|
+
const message = String(error || 'Agent runtime exit cannot be verified');
|
|
7188
|
+
if (agent) {
|
|
7189
|
+
agent.status = 'error';
|
|
7190
|
+
agent.engineStatus = 'cleanup-uncertain';
|
|
7191
|
+
const runtime = runtimeBindingOf(agent, kind);
|
|
7192
|
+
if (runtime) {
|
|
7193
|
+
runtime.state = 'error';
|
|
7194
|
+
runtime.error = message;
|
|
7195
|
+
}
|
|
7196
|
+
if (options.operationId) {
|
|
7197
|
+
try {
|
|
7198
|
+
this.transitionPersistentAgentOperation(agent, options.operationId, 'blocked', message);
|
|
7199
|
+
} catch (persistError) {
|
|
7200
|
+
return {
|
|
7201
|
+
agentId,
|
|
7202
|
+
error: `${message}; failed to persist blocked Agent operation: ${persistError.message || persistError}`,
|
|
7203
|
+
cleanupUncertain: true,
|
|
7204
|
+
retryable: true,
|
|
7205
|
+
};
|
|
7206
|
+
}
|
|
7207
|
+
}
|
|
7208
|
+
if (options.emitUpdate !== false) this.emit('update');
|
|
7209
|
+
}
|
|
7210
|
+
return {
|
|
7211
|
+
agentId,
|
|
7212
|
+
error: message,
|
|
7213
|
+
cleanupUncertain: true,
|
|
7214
|
+
retryable: true,
|
|
7215
|
+
};
|
|
7216
|
+
}
|
|
7217
|
+
|
|
7218
|
+
forgetStoppedAgentRecord(agentId, options = {}) {
|
|
4859
7219
|
this.agents.delete(agentId);
|
|
7220
|
+
this.verifiedStoppedAgentIds.delete(agentId);
|
|
4860
7221
|
this.lastActivity.delete(agentId);
|
|
4861
7222
|
this.lastActivityUpdate.delete(agentId);
|
|
4862
7223
|
this.outputEvents.delete(agentId);
|
|
@@ -4870,10 +7231,7 @@ class AgentManager extends EventEmitter {
|
|
|
4870
7231
|
this.mainAgentId = null;
|
|
4871
7232
|
}
|
|
4872
7233
|
|
|
4873
|
-
if (options.emitUpdate !== false)
|
|
4874
|
-
this.emit('update');
|
|
4875
|
-
}
|
|
4876
|
-
return { agentId, killed: true };
|
|
7234
|
+
if (options.emitUpdate !== false) this.emit('update');
|
|
4877
7235
|
}
|
|
4878
7236
|
|
|
4879
7237
|
async getAgentSessionText(agentId) {
|
|
@@ -5219,6 +7577,9 @@ class AgentManager extends EventEmitter {
|
|
|
5219
7577
|
terminalInputReceived: agent.terminalInputReceived === true,
|
|
5220
7578
|
runtimeBinding: publicRuntimeBinding(agent),
|
|
5221
7579
|
runtimeObservation: deriveRuntimeObservation(agent),
|
|
7580
|
+
lifecycleOperation: publicActiveLifecycleOperation(agent),
|
|
7581
|
+
requiresProcessExitAcknowledgement:
|
|
7582
|
+
agent.requiresProcessExitAcknowledgement === true,
|
|
5222
7583
|
forkedFromProviderSessionId: agent.forkedFromProviderSessionId || '',
|
|
5223
7584
|
restartedFromAgentId: agent.restartedFromAgentId || '',
|
|
5224
7585
|
restartedFromAgentIds: Array.isArray(agent.restartedFromAgentIds) ? agent.restartedFromAgentIds : [],
|