farming-code 2.2.16 → 2.2.17
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 +2 -2
- package/README.zh_cn.md +2 -2
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/backend/acp-runtime.js +221 -10
- package/backend/agent-manager.js +396 -47
- package/backend/agent-provider-session.js +15 -1
- package/backend/codex-rollout-follower.js +83 -0
- package/backend/codex-session-archive.js +12 -1
- package/backend/codex-session-history.js +94 -0
- package/backend/config-manager.js +3 -3
- package/backend/farming-app-cli.js +8 -0
- package/backend/native-pty-host-client.js +59 -2
- package/backend/provider-adapters.js +117 -36
- package/backend/provider-session-service.js +138 -34
- package/backend/server.js +5 -1
- package/backend/storage-layout.js +5 -0
- package/backend/usage-history-client.js +305 -0
- package/backend/usage-history-scanner.generated.js +2250 -0
- package/backend/usage-history-scanner.ts +2718 -0
- package/backend/usage-history-smoke.js +64 -0
- package/backend/usage-history-worker.js +34 -0
- package/backend/usage-monitor.js +188 -588
- package/backend/vendor/usage-parsers/CC_STATISTICS_LICENSE +21 -0
- package/backend/vendor/usage-parsers/CODEXBAR_LICENSE +21 -0
- package/backend/vendor/usage-parsers/VENDOR.json +45 -0
- package/dist/assets/App-6yWdwzj8.js +210 -0
- package/dist/assets/code-dark-5esj0bRu.css +1 -0
- package/dist/assets/{index-iKBvPjWS.js → index-C8ZFKye1.js} +2 -2
- package/dist/assets/main-DSO3i_Gs.css +1 -0
- package/dist/index.html +1 -1
- package/frontend/skins/crt/app.js +9 -3
- package/package.json +12 -8
- package/shared/backend-connection-status.d.ts +25 -0
- package/shared/backend-connection-status.js +40 -0
- package/dist/assets/App-y6p2yRar.js +0 -206
- package/dist/assets/code-dark-DKBGxoPX.css +0 -1
- package/dist/assets/main-CdkLOXYl.css +0 -1
package/backend/agent-manager.js
CHANGED
|
@@ -76,6 +76,9 @@ const MISSING_ENGINE_SESSION_STARTUP_GRACE_MS = 5000;
|
|
|
76
76
|
const MIN_TERMINAL_RESIZE_COLS = 40;
|
|
77
77
|
const MIN_TERMINAL_RESIZE_ROWS = 10;
|
|
78
78
|
const AGENT_DISCOVERY_CACHE_MAX_AGE_MS = 3_000;
|
|
79
|
+
const UNCERTAIN_TERMINAL_STOP_TIMEOUT_MS = 5_000;
|
|
80
|
+
const TERMINAL_STOP_STATE_READ_TIMEOUT_MS = 1_000;
|
|
81
|
+
const TERMINAL_STOP_POLL_MS = 50;
|
|
79
82
|
const SHELL_PROMPT_ENV_KEYS = [
|
|
80
83
|
'PS1',
|
|
81
84
|
'PS2',
|
|
@@ -88,6 +91,31 @@ const SHELL_PROMPT_ENV_KEYS = [
|
|
|
88
91
|
];
|
|
89
92
|
const execFileAsync = promisify(execFile);
|
|
90
93
|
|
|
94
|
+
function withBoundedWait(promise, timeoutMs, label) {
|
|
95
|
+
let timer;
|
|
96
|
+
const timeout = new Promise((_, reject) => {
|
|
97
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
98
|
+
timer.unref?.();
|
|
99
|
+
});
|
|
100
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function deletePrecreatedProviderSession(options = {}) {
|
|
104
|
+
const provider = String(options.provider || '').trim().toLowerCase();
|
|
105
|
+
const sessionId = String(options.sessionId || '').trim();
|
|
106
|
+
if (!isSafeProviderSessionId(sessionId)) throw new Error('Invalid pre-created provider session id');
|
|
107
|
+
let args = [];
|
|
108
|
+
if (provider === 'codex') args = ['delete', '--force', sessionId];
|
|
109
|
+
else if (provider === 'opencode') args = ['session', 'delete', sessionId];
|
|
110
|
+
else throw new Error(`${provider || 'Provider'} does not support pre-created session rollback`);
|
|
111
|
+
await execFileAsync(options.executable, args, {
|
|
112
|
+
cwd: options.cwd,
|
|
113
|
+
env: options.env,
|
|
114
|
+
timeout: 15_000,
|
|
115
|
+
maxBuffer: 1024 * 1024,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
91
119
|
function trimSessionOutput(output) {
|
|
92
120
|
const text = typeof output === 'string' ? output : '';
|
|
93
121
|
return text.length > SESSION_OUTPUT_LIMIT ? text.slice(-SESSION_OUTPUT_LIMIT) : text;
|
|
@@ -176,12 +204,6 @@ function applyTerminalStateCursor(agent, runtimeEpoch, outputSeq, stateRevision,
|
|
|
176
204
|
return true;
|
|
177
205
|
}
|
|
178
206
|
|
|
179
|
-
function hasResumeArg(args) {
|
|
180
|
-
return Array.isArray(args)
|
|
181
|
-
? args.some(arg => arg === '--resume' || (typeof arg === 'string' && arg.startsWith('--resume=')))
|
|
182
|
-
: false;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
207
|
function codexCommandContinuesSession(command) {
|
|
186
208
|
const parts = parseCommand(command);
|
|
187
209
|
return path.basename(parts[0] || '') === 'codex'
|
|
@@ -378,13 +400,23 @@ function hasAgentOutputAfterAttentionBaseline(agent) {
|
|
|
378
400
|
return false;
|
|
379
401
|
}
|
|
380
402
|
|
|
381
|
-
function
|
|
382
|
-
if (!
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
403
|
+
function shouldRestoreAgentFromMetadata(record, mainPageSessionKeys) {
|
|
404
|
+
if (!record || record.archived === true) return false;
|
|
405
|
+
if (record.wantsMain === true) return true;
|
|
406
|
+
if (
|
|
407
|
+
record.providerSessionTemporary === true
|
|
408
|
+
|| isTemporaryProviderSessionId(record.providerSessionId)
|
|
409
|
+
) {
|
|
410
|
+
return record.visibleOnMainPage === true;
|
|
411
|
+
}
|
|
412
|
+
const provider = String(record.providerSessionProvider || record.provider || '').trim();
|
|
413
|
+
const sessionKey = record.providerSessionKey || mainPageAgentSessionKey(
|
|
414
|
+
provider,
|
|
415
|
+
record.providerSessionId,
|
|
416
|
+
record.providerHomeId || 'default'
|
|
417
|
+
);
|
|
418
|
+
if (sessionKey) return mainPageSessionKeys.has(sessionKey);
|
|
419
|
+
return record.visibleOnMainPage === true;
|
|
388
420
|
}
|
|
389
421
|
|
|
390
422
|
function recoveredEngineSessionId(entry, metadata = {}) {
|
|
@@ -549,6 +581,7 @@ class AgentManager extends EventEmitter {
|
|
|
549
581
|
);
|
|
550
582
|
this.agents = new RuntimeAgentMap();
|
|
551
583
|
this.mainAgentId = null;
|
|
584
|
+
this.mainAgentStartReservation = null;
|
|
552
585
|
this.lastActivity = new Map();
|
|
553
586
|
this.lastActivityUpdate = new Map();
|
|
554
587
|
this.outputEvents = new Map(); // Map<agentId, Array<{timestamp, bytes}>> for rate tracking
|
|
@@ -580,6 +613,12 @@ class AgentManager extends EventEmitter {
|
|
|
580
613
|
}
|
|
581
614
|
: {}),
|
|
582
615
|
});
|
|
616
|
+
this.createProviderSessionIdentity = typeof options.createProviderSessionIdentity === 'function'
|
|
617
|
+
? options.createProviderSessionIdentity
|
|
618
|
+
: createOptions => this.acpRuntime.createSessionIdentity(createOptions);
|
|
619
|
+
this.deleteProviderSessionIdentity = typeof options.deleteProviderSessionIdentity === 'function'
|
|
620
|
+
? options.deleteProviderSessionIdentity
|
|
621
|
+
: deletePrecreatedProviderSession;
|
|
583
622
|
this.archiveCodexSession = options.archiveCodexSession || archiveCodexSession;
|
|
584
623
|
this.unarchiveCodexSession = options.unarchiveCodexSession || unarchiveCodexSession;
|
|
585
624
|
this.heartbeatInterval = null;
|
|
@@ -718,6 +757,9 @@ class AgentManager extends EventEmitter {
|
|
|
718
757
|
|
|
719
758
|
this.reviveAgentRuntime(agent);
|
|
720
759
|
agent.output = trimSessionOutput(agent.output + data);
|
|
760
|
+
if (agent.providerSessionProvider === 'codex' && agent.providerSessionTemporary === true) {
|
|
761
|
+
void this.providerSessionService.resolveTemporaryCodex(sessionId);
|
|
762
|
+
}
|
|
721
763
|
const outputAt = Date.now();
|
|
722
764
|
agent.lastEngineOutputAt = outputAt;
|
|
723
765
|
this.lastActivity.set(sessionId, outputAt);
|
|
@@ -1086,6 +1128,7 @@ class AgentManager extends EventEmitter {
|
|
|
1086
1128
|
const persistedRecords = this.configManager && typeof this.configManager.listAgentSessionRecords === 'function'
|
|
1087
1129
|
? this.configManager.listAgentSessionRecords()
|
|
1088
1130
|
: [];
|
|
1131
|
+
const mainPageSessionKeys = new Set(this.getMainPageSessionKeys());
|
|
1089
1132
|
const persistedByRuntimeAgentId = new Map(persistedRecords
|
|
1090
1133
|
.filter(record => record && record.runtimeAgentId)
|
|
1091
1134
|
.map(record => [record.runtimeAgentId, record]));
|
|
@@ -1096,6 +1139,11 @@ class AgentManager extends EventEmitter {
|
|
|
1096
1139
|
const state = entry.state || {};
|
|
1097
1140
|
const agentId = recoveredEngineSessionId(entry, engineMetadata);
|
|
1098
1141
|
const persisted = persistedByRuntimeAgentId.get(agentId);
|
|
1142
|
+
const desiredMetadata = persisted || engineMetadata;
|
|
1143
|
+
if (!shouldRestoreAgentFromMetadata(desiredMetadata, mainPageSessionKeys)) {
|
|
1144
|
+
await this.killRecoveredEngineSession(entry, engineMetadata, agentId);
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1099
1147
|
// The persisted runtime mode is authoritative. A PTY can outlive the
|
|
1100
1148
|
// server long enough to appear in native-host recovery after the Agent
|
|
1101
1149
|
// has already switched to ACP. Recovering that stale PTY first would
|
|
@@ -1114,6 +1162,7 @@ class AgentManager extends EventEmitter {
|
|
|
1114
1162
|
// even briefly makes Chat/Terminal switching disappear until a later
|
|
1115
1163
|
// provider resolver update happens to repair it.
|
|
1116
1164
|
source: persisted.source || engineMetadata.source,
|
|
1165
|
+
persistentSessionId: persisted.id || persisted.persistentSessionId || engineMetadata.persistentSessionId,
|
|
1117
1166
|
projectWorkspace: persisted.projectWorkspace || engineMetadata.projectWorkspace,
|
|
1118
1167
|
provider: persistedProvider || engineMetadata.provider,
|
|
1119
1168
|
providerSessionProvider: persistedProvider || engineMetadata.providerSessionProvider,
|
|
@@ -1131,21 +1180,23 @@ class AgentManager extends EventEmitter {
|
|
|
1131
1180
|
terminalInputReceived: Object.prototype.hasOwnProperty.call(persisted, 'terminalInputReceived')
|
|
1132
1181
|
? persisted.terminalInputReceived === true
|
|
1133
1182
|
: engineMetadata.terminalInputReceived,
|
|
1183
|
+
customTitle: Object.prototype.hasOwnProperty.call(persisted, 'customTitle')
|
|
1184
|
+
? persisted.customTitle
|
|
1185
|
+
: engineMetadata.customTitle,
|
|
1134
1186
|
...legacyRuntimeMetadata(persisted),
|
|
1135
1187
|
pinned: persisted.pinned === true,
|
|
1136
1188
|
projectOrder: finiteOrder(persisted.projectOrder) ?? finiteOrder(engineMetadata.projectOrder),
|
|
1137
1189
|
pinnedOrder: finiteOrder(persisted.pinnedOrder) ?? finiteOrder(engineMetadata.pinnedOrder),
|
|
1138
1190
|
} : engineMetadata;
|
|
1139
1191
|
if (!agentId || this.agents.has(agentId)) continue;
|
|
1140
|
-
if (!shouldRecoverEngineSession(metadata)) {
|
|
1141
|
-
await this.killRecoveredEngineSession(entry, metadata, agentId);
|
|
1142
|
-
continue;
|
|
1143
|
-
}
|
|
1144
1192
|
|
|
1145
1193
|
const agentRecord = this.recoveredAgentRecord(agentId, entry.engineName || metadata.engineName || 'native', metadata, state);
|
|
1146
1194
|
ensureAgentOrders(agentRecord, Array.from(this.agents.values()));
|
|
1147
1195
|
agentRecord.lastObservedTurnActive = this.isAgentAttentionTurnActive(agentRecord);
|
|
1148
|
-
this.ensurePersistentAgentSession(agentRecord
|
|
1196
|
+
this.ensurePersistentAgentSession(agentRecord, {
|
|
1197
|
+
visibleOnMainPage: true,
|
|
1198
|
+
archived: false,
|
|
1199
|
+
});
|
|
1149
1200
|
this.agents.set(agentId, agentRecord);
|
|
1150
1201
|
void this.refreshAgentWorktree(agentId);
|
|
1151
1202
|
this.lastActivity.set(agentId, state.lastActivityAt || metadata.lastActivityAt || Date.now());
|
|
@@ -1172,6 +1223,7 @@ class AgentManager extends EventEmitter {
|
|
|
1172
1223
|
|
|
1173
1224
|
async restoreTerminalSessionsAfterRuntimeRotation(records, rotations) {
|
|
1174
1225
|
const mainPageOrder = new Map(this.getMainPageSessionKeys().map((key, index) => [key, index]));
|
|
1226
|
+
const mainPageSessionKeys = new Set(mainPageOrder.keys());
|
|
1175
1227
|
const liveProviderSessions = new Set(
|
|
1176
1228
|
[...this.agents.values()]
|
|
1177
1229
|
.filter(agent => agent?.providerSessionProvider && agent?.providerSessionId)
|
|
@@ -1265,8 +1317,9 @@ class AgentManager extends EventEmitter {
|
|
|
1265
1317
|
return (Number(right.updatedAt) || 0) - (Number(left.updatedAt) || 0);
|
|
1266
1318
|
})
|
|
1267
1319
|
: fallbackCandidates;
|
|
1320
|
+
const desiredCandidates = candidates.filter(record => shouldRestoreAgentFromMetadata(record, mainPageSessionKeys));
|
|
1268
1321
|
|
|
1269
|
-
if (
|
|
1322
|
+
if (desiredCandidates.length > 0) {
|
|
1270
1323
|
const rotationSummary = (Array.isArray(rotations) ? rotations : []).map(rotation => {
|
|
1271
1324
|
const { serializedTerminalState, ...rest } = rotation || {};
|
|
1272
1325
|
return {
|
|
@@ -1277,13 +1330,13 @@ class AgentManager extends EventEmitter {
|
|
|
1277
1330
|
};
|
|
1278
1331
|
});
|
|
1279
1332
|
console.warn(
|
|
1280
|
-
`Restoring ${
|
|
1333
|
+
`Restoring ${desiredCandidates.length} Terminal session(s) after native PTY runtime rotation`,
|
|
1281
1334
|
rotationSummary
|
|
1282
1335
|
);
|
|
1283
1336
|
}
|
|
1284
1337
|
|
|
1285
1338
|
let changed = false;
|
|
1286
|
-
for (const record of
|
|
1339
|
+
for (const record of desiredCandidates) {
|
|
1287
1340
|
if (record.wantsMain === true && this.mainAgentId) continue;
|
|
1288
1341
|
const provider = String(record.providerSessionProvider || record.provider || '').trim();
|
|
1289
1342
|
const sessionId = record.providerSessionId;
|
|
@@ -1293,6 +1346,19 @@ class AgentManager extends EventEmitter {
|
|
|
1293
1346
|
record.providerHomeId || 'default'
|
|
1294
1347
|
);
|
|
1295
1348
|
if (sessionKey && liveProviderSessions.has(sessionKey)) continue;
|
|
1349
|
+
if (
|
|
1350
|
+
provider === 'codex'
|
|
1351
|
+
&& record.terminalInputReceived === true
|
|
1352
|
+
&& (
|
|
1353
|
+
record.providerSessionTemporary === true
|
|
1354
|
+
|| isTemporaryProviderSessionId(sessionId)
|
|
1355
|
+
)
|
|
1356
|
+
) {
|
|
1357
|
+
console.warn(
|
|
1358
|
+
`Refusing to replace Codex Terminal ${record.runtimeAgentId || sessionId} after native PTY runtime rotation without an exact resume id`
|
|
1359
|
+
);
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1296
1362
|
const canResumeProvider = Boolean(getProviderAdapter(provider))
|
|
1297
1363
|
&& isSafeProviderSessionId(sessionId);
|
|
1298
1364
|
const command = canResumeProvider
|
|
@@ -1386,8 +1452,12 @@ class AgentManager extends EventEmitter {
|
|
|
1386
1452
|
async recoverAcpSessions() {
|
|
1387
1453
|
if (!this.acpRuntime || !this.configManager || typeof this.configManager.listAgentSessionRecords !== 'function') return;
|
|
1388
1454
|
const mainPageOrder = new Map(this.getMainPageSessionKeys().map((key, index) => [key, index]));
|
|
1455
|
+
const mainPageSessionKeys = new Set(mainPageOrder.keys());
|
|
1389
1456
|
const records = this.configManager.listAgentSessionRecords()
|
|
1390
|
-
.filter(record =>
|
|
1457
|
+
.filter(record => (
|
|
1458
|
+
shouldRestoreAgentFromMetadata(record, mainPageSessionKeys)
|
|
1459
|
+
&& runtimeKind(record) === 'acp'
|
|
1460
|
+
))
|
|
1391
1461
|
.sort((left, right) => {
|
|
1392
1462
|
const leftOrder = mainPageOrder.get(left.providerSessionKey);
|
|
1393
1463
|
const rightOrder = mainPageOrder.get(right.providerSessionKey);
|
|
@@ -1716,6 +1786,7 @@ class AgentManager extends EventEmitter {
|
|
|
1716
1786
|
readOutputSeq: finiteNumberOrNull(agent.readOutputSeq),
|
|
1717
1787
|
projectOrder: finiteOrder(agent.projectOrder),
|
|
1718
1788
|
pinnedOrder: finiteOrder(agent.pinnedOrder),
|
|
1789
|
+
customTitle: agent.customTitle || '',
|
|
1719
1790
|
})).catch((error) => {
|
|
1720
1791
|
console.warn('Failed to update provider session metadata:', error && (error.message || error));
|
|
1721
1792
|
});
|
|
@@ -2280,11 +2351,103 @@ class AgentManager extends EventEmitter {
|
|
|
2280
2351
|
});
|
|
2281
2352
|
}
|
|
2282
2353
|
|
|
2354
|
+
async stopUncertainTerminalSession(engine, agentId) {
|
|
2355
|
+
if (typeof engine?.killSession !== 'function' || typeof engine?.getSessionState !== 'function') {
|
|
2356
|
+
throw new Error('Session engine cannot prove an uncertain Terminal start has stopped');
|
|
2357
|
+
}
|
|
2358
|
+
await withBoundedWait(
|
|
2359
|
+
Promise.resolve(engine.killSession(agentId)),
|
|
2360
|
+
UNCERTAIN_TERMINAL_STOP_TIMEOUT_MS,
|
|
2361
|
+
'Terminal kill request',
|
|
2362
|
+
);
|
|
2363
|
+
const deadline = Date.now() + UNCERTAIN_TERMINAL_STOP_TIMEOUT_MS;
|
|
2364
|
+
while (Date.now() <= deadline) {
|
|
2365
|
+
const state = await withBoundedWait(
|
|
2366
|
+
Promise.resolve(engine.getSessionState(agentId)),
|
|
2367
|
+
TERMINAL_STOP_STATE_READ_TIMEOUT_MS,
|
|
2368
|
+
'Terminal stop-state read',
|
|
2369
|
+
);
|
|
2370
|
+
if (!state || ['dead', 'exited', 'stopped'].includes(String(state.status || ''))) return;
|
|
2371
|
+
await new Promise(resolve => setTimeout(resolve, TERMINAL_STOP_POLL_MS));
|
|
2372
|
+
}
|
|
2373
|
+
throw new Error(`Terminal ${agentId} did not reach an exited state after kill`);
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2283
2376
|
async startAgent(command, customWorkspace, callback, options = {}) {
|
|
2284
2377
|
if (options.wantsMain !== false && options.skipRecoveryWait !== true) {
|
|
2285
2378
|
await this.whenRecovered();
|
|
2286
2379
|
}
|
|
2287
2380
|
|
|
2381
|
+
const wantsMain = options.wantsMain === true || (options.wantsMain !== false && !this.mainAgentId);
|
|
2382
|
+
if (!wantsMain) {
|
|
2383
|
+
return this.startAgentUnreserved(command, customWorkspace, callback, {
|
|
2384
|
+
...options,
|
|
2385
|
+
wantsMain: false,
|
|
2386
|
+
skipRecoveryWait: true,
|
|
2387
|
+
});
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
if (this.mainAgentStartReservation) {
|
|
2391
|
+
const outcome = await this.mainAgentStartReservation.promise;
|
|
2392
|
+
if (callback) callback(outcome.agentId, outcome.error);
|
|
2393
|
+
return outcome.agentId;
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
const existingMainStart = this.findActiveMainAgentStart();
|
|
2397
|
+
if (existingMainStart) {
|
|
2398
|
+
if (this.mainAgentId !== existingMainStart.id) {
|
|
2399
|
+
this.mainAgentId = existingMainStart.id;
|
|
2400
|
+
this.emit('update');
|
|
2401
|
+
}
|
|
2402
|
+
console.log('Main Agent already starting or running:', existingMainStart.id);
|
|
2403
|
+
if (callback) callback(existingMainStart.id);
|
|
2404
|
+
return existingMainStart.id;
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
let resolveReservation;
|
|
2408
|
+
const reservation = {
|
|
2409
|
+
promise: new Promise(resolve => {
|
|
2410
|
+
resolveReservation = resolve;
|
|
2411
|
+
}),
|
|
2412
|
+
};
|
|
2413
|
+
this.mainAgentStartReservation = reservation;
|
|
2414
|
+
let outcome = null;
|
|
2415
|
+
let callbackCalled = false;
|
|
2416
|
+
const reservedCallback = (agentId, error) => {
|
|
2417
|
+
callbackCalled = true;
|
|
2418
|
+
outcome = { agentId: agentId || null, error: error || null };
|
|
2419
|
+
if (callback) callback(agentId, error);
|
|
2420
|
+
};
|
|
2421
|
+
try {
|
|
2422
|
+
const agentId = await this.startAgentUnreserved(command, customWorkspace, reservedCallback, {
|
|
2423
|
+
...options,
|
|
2424
|
+
wantsMain: true,
|
|
2425
|
+
skipRecoveryWait: true,
|
|
2426
|
+
});
|
|
2427
|
+
if (!callbackCalled) {
|
|
2428
|
+
outcome = { agentId: agentId || null, error: null };
|
|
2429
|
+
if (callback) callback(agentId);
|
|
2430
|
+
}
|
|
2431
|
+
return agentId;
|
|
2432
|
+
} catch (error) {
|
|
2433
|
+
if (!callbackCalled) {
|
|
2434
|
+
outcome = { agentId: null, error: error && (error.message || String(error)) };
|
|
2435
|
+
if (callback) callback(null, outcome.error);
|
|
2436
|
+
}
|
|
2437
|
+
throw error;
|
|
2438
|
+
} finally {
|
|
2439
|
+
resolveReservation(outcome || { agentId: null, error: 'Main Agent failed to start' });
|
|
2440
|
+
if (this.mainAgentStartReservation === reservation) {
|
|
2441
|
+
this.mainAgentStartReservation = null;
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
async startAgentUnreserved(command, customWorkspace, callback, options = {}) {
|
|
2447
|
+
if (options.wantsMain !== false && options.skipRecoveryWait !== true) {
|
|
2448
|
+
await this.whenRecovered();
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2288
2451
|
const wantsMain = options.wantsMain === true || (options.wantsMain !== false && !this.mainAgentId);
|
|
2289
2452
|
if (wantsMain) {
|
|
2290
2453
|
const existingMainStart = this.findActiveMainAgentStart();
|
|
@@ -2357,13 +2520,28 @@ class AgentManager extends EventEmitter {
|
|
|
2357
2520
|
});
|
|
2358
2521
|
|
|
2359
2522
|
const hasResumeSource = Boolean(resumedSessionFromSource(resolvedSource));
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2523
|
+
const commandProviderSessionPlan = hasResumeSource
|
|
2524
|
+
? buildAgentProviderSessionPlan({
|
|
2525
|
+
command,
|
|
2526
|
+
program,
|
|
2527
|
+
args: launch.args,
|
|
2528
|
+
source: 'ui',
|
|
2529
|
+
})
|
|
2530
|
+
: null;
|
|
2531
|
+
const commandContinuesProviderSession = commandProviderSessionPlan
|
|
2532
|
+
&& commandProviderSessionPlan.temporary !== true
|
|
2533
|
+
&& commandProviderSessionPlan.provider === providerSessionPlan.provider
|
|
2534
|
+
&& commandProviderSessionPlan.id === providerSessionPlan.id;
|
|
2535
|
+
if (
|
|
2536
|
+
providerSessionPlan.source === 'resume-source'
|
|
2537
|
+
&& hasResumeSource
|
|
2538
|
+
&& !commandContinuesProviderSession
|
|
2539
|
+
) {
|
|
2540
|
+
providerSessionPlan = commandProviderSessionPlan;
|
|
2541
|
+
}
|
|
2542
|
+
if (providerSessionPlan.error) {
|
|
2543
|
+
if (callback) callback(null, providerSessionPlan.error);
|
|
2544
|
+
return null;
|
|
2367
2545
|
}
|
|
2368
2546
|
|
|
2369
2547
|
let args = providerSessionPlan.args;
|
|
@@ -2446,13 +2624,6 @@ class AgentManager extends EventEmitter {
|
|
|
2446
2624
|
}
|
|
2447
2625
|
}
|
|
2448
2626
|
|
|
2449
|
-
const logArgs = args.map((arg, index) => (
|
|
2450
|
-
index > 0 && args[index - 1] === '--append-system-prompt'
|
|
2451
|
-
? '<farming-main-agent-bootstrap>'
|
|
2452
|
-
: arg
|
|
2453
|
-
));
|
|
2454
|
-
console.log('Starting agent:', program, logArgs, 'workspace:', workspace, spawnProgram !== program ? `resolved: ${spawnProgram}` : '');
|
|
2455
|
-
|
|
2456
2627
|
if (!fs.existsSync(workspace)) {
|
|
2457
2628
|
console.log('Workspace does not exist:', workspace);
|
|
2458
2629
|
if (callback) callback(null, `Workspace does not exist: ${workspace}`);
|
|
@@ -2519,8 +2690,7 @@ class AgentManager extends EventEmitter {
|
|
|
2519
2690
|
: 'terminal';
|
|
2520
2691
|
// A fresh structured runtime does not need a provider CLI resume id yet:
|
|
2521
2692
|
// ACP/JSON creates the provider session and writes the resulting id back
|
|
2522
|
-
// after connecting.
|
|
2523
|
-
// terminal CLI does not accept a pre-generated id for a fresh session.
|
|
2693
|
+
// after connecting. Fresh Terminal sessions are handled separately below.
|
|
2524
2694
|
const structuredRuntimeProvider = ['json', 'acp', 'chat'].includes(requestedAgentRuntimeMode)
|
|
2525
2695
|
? homeProvider
|
|
2526
2696
|
: providerSessionPlan.provider;
|
|
@@ -2630,6 +2800,135 @@ class AgentManager extends EventEmitter {
|
|
|
2630
2800
|
attentionSuppressUntil: 0
|
|
2631
2801
|
};
|
|
2632
2802
|
|
|
2803
|
+
let precreatedProviderSession = null;
|
|
2804
|
+
if (
|
|
2805
|
+
providerSessionPlan.precreate === true
|
|
2806
|
+
&& !useAcp
|
|
2807
|
+
&& !useJsonCli
|
|
2808
|
+
) {
|
|
2809
|
+
const adapter = getProviderAdapter(providerSessionPlan.provider);
|
|
2810
|
+
if (typeof adapter?.terminalResumeArgs !== 'function') {
|
|
2811
|
+
const message = `${providerSessionPlan.provider} cannot resume a pre-created Terminal session`;
|
|
2812
|
+
if (callback) callback(null, message);
|
|
2813
|
+
return null;
|
|
2814
|
+
}
|
|
2815
|
+
try {
|
|
2816
|
+
const requestedIdentityWorkspace = this.expandWorkspacePath(providerSessionPlan.identityWorkspace || '');
|
|
2817
|
+
const identityWorkspace = requestedIdentityWorkspace
|
|
2818
|
+
? (path.isAbsolute(requestedIdentityWorkspace)
|
|
2819
|
+
? path.resolve(requestedIdentityWorkspace)
|
|
2820
|
+
: path.resolve(workspace, requestedIdentityWorkspace))
|
|
2821
|
+
: workspace;
|
|
2822
|
+
let identityWorkspaceExists = false;
|
|
2823
|
+
try {
|
|
2824
|
+
identityWorkspaceExists = fs.statSync(identityWorkspace).isDirectory();
|
|
2825
|
+
} catch {
|
|
2826
|
+
identityWorkspaceExists = false;
|
|
2827
|
+
}
|
|
2828
|
+
if (!identityWorkspaceExists) {
|
|
2829
|
+
throw new Error(`Workspace does not exist: ${identityWorkspace}`);
|
|
2830
|
+
}
|
|
2831
|
+
const identityEnv = this.buildAgentEnv(agentId, agentRecord);
|
|
2832
|
+
const requestedAdditionalDirectories = Array.isArray(options.additionalDirectories)
|
|
2833
|
+
? options.additionalDirectories
|
|
2834
|
+
: [];
|
|
2835
|
+
const requestedMcpServers = Array.isArray(options.mcpServers) ? options.mcpServers : [];
|
|
2836
|
+
const created = await this.createProviderSessionIdentity({
|
|
2837
|
+
provider: providerSessionPlan.provider,
|
|
2838
|
+
executable: spawnProgram,
|
|
2839
|
+
env: identityEnv,
|
|
2840
|
+
cwd: identityWorkspace,
|
|
2841
|
+
providerHomeId: agentRecord.providerHomeId || 'default',
|
|
2842
|
+
approvalMode: agentRecord.launchPermissionMode || 'approve',
|
|
2843
|
+
model: codexModel,
|
|
2844
|
+
reasoningEffort: codexReasoningEffort,
|
|
2845
|
+
serviceTier: codexServiceTier,
|
|
2846
|
+
additionalDirectories: requestedAdditionalDirectories,
|
|
2847
|
+
mcpServers: requestedMcpServers,
|
|
2848
|
+
});
|
|
2849
|
+
const providerSessionId = String(created?.sessionId || '').trim();
|
|
2850
|
+
if (!isSafeProviderSessionId(providerSessionId)) {
|
|
2851
|
+
throw new Error(`${providerSessionPlan.provider} ACP session/new returned an invalid session id`);
|
|
2852
|
+
}
|
|
2853
|
+
const normalizedSessionOptions = created?.sessionRequestOptions || {
|
|
2854
|
+
additionalDirectories: requestedAdditionalDirectories.map(directory => path.resolve(identityWorkspace, directory)),
|
|
2855
|
+
mcpServers: JSON.parse(JSON.stringify(requestedMcpServers)),
|
|
2856
|
+
};
|
|
2857
|
+
args = adapter.terminalResumeArgs(args, providerSessionId, providerSessionPlan);
|
|
2858
|
+
providerSessionPlan = {
|
|
2859
|
+
...providerSessionPlan,
|
|
2860
|
+
id: providerSessionId,
|
|
2861
|
+
precreate: false,
|
|
2862
|
+
temporary: false,
|
|
2863
|
+
source: 'acp-precreated',
|
|
2864
|
+
args,
|
|
2865
|
+
};
|
|
2866
|
+
agentRecord.providerSessionId = providerSessionId;
|
|
2867
|
+
agentRecord.providerSessionKey = mainPageAgentSessionKey(
|
|
2868
|
+
providerSessionPlan.provider,
|
|
2869
|
+
providerSessionId,
|
|
2870
|
+
agentRecord.providerHomeId || 'default'
|
|
2871
|
+
);
|
|
2872
|
+
agentRecord.providerSessionTemporary = false;
|
|
2873
|
+
agentRecord.providerSessionSource = providerSessionPlan.source;
|
|
2874
|
+
agentRecord.providerSessionResolvedAt = Date.now();
|
|
2875
|
+
this.acpSessionOptionsByKey.set(agentRecord.providerSessionKey, {
|
|
2876
|
+
additionalDirectories: Array.isArray(normalizedSessionOptions.additionalDirectories)
|
|
2877
|
+
? [...normalizedSessionOptions.additionalDirectories]
|
|
2878
|
+
: [],
|
|
2879
|
+
mcpServers: Array.isArray(normalizedSessionOptions.mcpServers)
|
|
2880
|
+
? JSON.parse(JSON.stringify(normalizedSessionOptions.mcpServers))
|
|
2881
|
+
: [],
|
|
2882
|
+
});
|
|
2883
|
+
precreatedProviderSession = {
|
|
2884
|
+
provider: providerSessionPlan.provider,
|
|
2885
|
+
executable: spawnProgram,
|
|
2886
|
+
env: identityEnv,
|
|
2887
|
+
cwd: identityWorkspace,
|
|
2888
|
+
sessionId: providerSessionId,
|
|
2889
|
+
sessionKey: agentRecord.providerSessionKey,
|
|
2890
|
+
};
|
|
2891
|
+
} catch (error) {
|
|
2892
|
+
let identityRollbackError = null;
|
|
2893
|
+
let identityRetainedReason = '';
|
|
2894
|
+
const orphanedIdentity = error?.providerSessionIdentity;
|
|
2895
|
+
if (
|
|
2896
|
+
orphanedIdentity
|
|
2897
|
+
&& orphanedIdentity.producerStopped === true
|
|
2898
|
+
&& isSafeProviderSessionId(orphanedIdentity.sessionId)
|
|
2899
|
+
) {
|
|
2900
|
+
try {
|
|
2901
|
+
await this.deleteProviderSessionIdentity(orphanedIdentity);
|
|
2902
|
+
} catch (cleanupError) {
|
|
2903
|
+
identityRollbackError = cleanupError;
|
|
2904
|
+
}
|
|
2905
|
+
} else if (orphanedIdentity && !isSafeProviderSessionId(orphanedIdentity.sessionId)) {
|
|
2906
|
+
identityRetainedReason = 'provider returned an unsafe session id; it was retained without invoking CLI rollback';
|
|
2907
|
+
} else if (orphanedIdentity) {
|
|
2908
|
+
identityRetainedReason = 'provider session retained because ACP producer shutdown could not be proven';
|
|
2909
|
+
}
|
|
2910
|
+
const baseMessage = error && error.message
|
|
2911
|
+
? error.message
|
|
2912
|
+
: `Failed to create ${providerSessionPlan.provider} session identity`;
|
|
2913
|
+
let message = baseMessage;
|
|
2914
|
+
if (identityRollbackError) {
|
|
2915
|
+
message = `${baseMessage}; provider session rollback failed: ${identityRollbackError.message || identityRollbackError}`;
|
|
2916
|
+
} else if (identityRetainedReason) {
|
|
2917
|
+
message = `${baseMessage}; ${identityRetainedReason}`;
|
|
2918
|
+
}
|
|
2919
|
+
console.error('Failed to create provider session identity:', error);
|
|
2920
|
+
if (callback) callback(null, message);
|
|
2921
|
+
return null;
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
const logArgs = args.map((arg, index) => (
|
|
2926
|
+
index > 0 && args[index - 1] === '--append-system-prompt'
|
|
2927
|
+
? '<farming-main-agent-bootstrap>'
|
|
2928
|
+
: arg
|
|
2929
|
+
));
|
|
2930
|
+
console.log('Starting agent:', program, logArgs, 'workspace:', workspace, spawnProgram !== program ? `resolved: ${spawnProgram}` : '');
|
|
2931
|
+
|
|
2633
2932
|
ensureAgentOrders(agentRecord, Array.from(this.agents.values()));
|
|
2634
2933
|
this.agents.set(agentId, agentRecord);
|
|
2635
2934
|
void this.refreshAgentWorktree(agentId);
|
|
@@ -2716,7 +3015,6 @@ class AgentManager extends EventEmitter {
|
|
|
2716
3015
|
acpRuntime.error = '';
|
|
2717
3016
|
}
|
|
2718
3017
|
|
|
2719
|
-
agentRecord.persistentSessionId = this.ensurePersistentAgentSession(agentRecord);
|
|
2720
3018
|
if (!useJsonCli && !useAcp) {
|
|
2721
3019
|
const engineLaunch = {
|
|
2722
3020
|
command: spawnProgram,
|
|
@@ -2727,6 +3025,10 @@ class AgentManager extends EventEmitter {
|
|
|
2727
3025
|
};
|
|
2728
3026
|
await this.createAgentEngineSession(agentRecord, resolution.engine, engineLaunch);
|
|
2729
3027
|
}
|
|
3028
|
+
agentRecord.persistentSessionId = this.ensurePersistentAgentSession(agentRecord, {
|
|
3029
|
+
visibleOnMainPage: true,
|
|
3030
|
+
archived: false,
|
|
3031
|
+
});
|
|
2730
3032
|
|
|
2731
3033
|
const agent = this.agents.get(agentId);
|
|
2732
3034
|
if (agent && agent.status === 'pending') {
|
|
@@ -2745,6 +3047,33 @@ class AgentManager extends EventEmitter {
|
|
|
2745
3047
|
return agentId;
|
|
2746
3048
|
} catch (error) {
|
|
2747
3049
|
console.error('Failed to start agent:', error);
|
|
3050
|
+
let runtimeCleanupError = null;
|
|
3051
|
+
if (!useJsonCli && !useAcp) {
|
|
3052
|
+
try {
|
|
3053
|
+
await this.stopUncertainTerminalSession(resolution.engine, agentId);
|
|
3054
|
+
} catch (engineCleanupError) {
|
|
3055
|
+
runtimeCleanupError = engineCleanupError;
|
|
3056
|
+
console.error(
|
|
3057
|
+
'Failed to stop partially started Agent runtime:',
|
|
3058
|
+
engineCleanupError && (engineCleanupError.message || engineCleanupError)
|
|
3059
|
+
);
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
let rollbackError = null;
|
|
3063
|
+
if (precreatedProviderSession && !runtimeCleanupError) {
|
|
3064
|
+
try {
|
|
3065
|
+
await this.deleteProviderSessionIdentity(precreatedProviderSession);
|
|
3066
|
+
} catch (cleanupError) {
|
|
3067
|
+
rollbackError = cleanupError;
|
|
3068
|
+
console.error(
|
|
3069
|
+
'Failed to roll back pre-created provider session:',
|
|
3070
|
+
cleanupError && (cleanupError.message || cleanupError)
|
|
3071
|
+
);
|
|
3072
|
+
}
|
|
3073
|
+
this.acpSessionOptionsByKey.delete(precreatedProviderSession.sessionKey);
|
|
3074
|
+
} else if (precreatedProviderSession) {
|
|
3075
|
+
this.acpSessionOptionsByKey.delete(precreatedProviderSession.sessionKey);
|
|
3076
|
+
}
|
|
2748
3077
|
if (options.restoreRuntimeAgentIdOnFailure && agentRecord.persistentSessionId) {
|
|
2749
3078
|
const failedAgentId = agentRecord.id;
|
|
2750
3079
|
try {
|
|
@@ -2773,7 +3102,14 @@ class AgentManager extends EventEmitter {
|
|
|
2773
3102
|
}
|
|
2774
3103
|
|
|
2775
3104
|
this.emit('update');
|
|
2776
|
-
|
|
3105
|
+
const startupError = error && (error.message || String(error));
|
|
3106
|
+
const cleanupSuffix = rollbackError
|
|
3107
|
+
? `; provider session rollback failed: ${rollbackError.message || rollbackError}`
|
|
3108
|
+
: '';
|
|
3109
|
+
const runtimeCleanupSuffix = runtimeCleanupError
|
|
3110
|
+
? `; Terminal rollback failed and the provider session was retained: ${runtimeCleanupError.message || runtimeCleanupError}`
|
|
3111
|
+
: '';
|
|
3112
|
+
if (callback) callback(null, `${startupError}${runtimeCleanupSuffix}${cleanupSuffix}`);
|
|
2777
3113
|
return null;
|
|
2778
3114
|
}
|
|
2779
3115
|
}
|
|
@@ -3129,15 +3465,16 @@ class AgentManager extends EventEmitter {
|
|
|
3129
3465
|
const engine = this.engineBridge.getEngine(agent.engineName);
|
|
3130
3466
|
if (!engine) return;
|
|
3131
3467
|
|
|
3468
|
+
const submittedUserInput = markUserInput && hasSubmittedTerminalInput(input);
|
|
3469
|
+
if (submittedUserInput && agent.terminalInputReceived !== true) {
|
|
3470
|
+
agent.terminalInputReceived = true;
|
|
3471
|
+
this.ensurePersistentAgentSession(agent);
|
|
3472
|
+
this.updateEngineProviderSessionMetadata(agent);
|
|
3473
|
+
this.emit('agent-update', { agentId, patch: { terminalInputReceived: true } });
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3132
3476
|
try {
|
|
3133
|
-
const submittedUserInput = markUserInput && hasSubmittedTerminalInput(input);
|
|
3134
3477
|
const result = await engine.sendInput(agentId, input, { expectedRuntimeEpoch });
|
|
3135
|
-
if (submittedUserInput && agent.terminalInputReceived !== true) {
|
|
3136
|
-
agent.terminalInputReceived = true;
|
|
3137
|
-
this.ensurePersistentAgentSession(agent);
|
|
3138
|
-
this.updateEngineProviderSessionMetadata(agent);
|
|
3139
|
-
this.emit('agent-update', { agentId, patch: { terminalInputReceived: true } });
|
|
3140
|
-
}
|
|
3141
3478
|
if (submittedUserInput) {
|
|
3142
3479
|
this.providerSessionService.observe(agentId, { force: true });
|
|
3143
3480
|
}
|
|
@@ -3306,6 +3643,8 @@ class AgentManager extends EventEmitter {
|
|
|
3306
3643
|
|
|
3307
3644
|
const customTitle = String(title || '').trim().slice(0, 80);
|
|
3308
3645
|
agent.customTitle = customTitle;
|
|
3646
|
+
this.ensurePersistentAgentSession(agent);
|
|
3647
|
+
this.updateEngineProviderSessionMetadata(agent);
|
|
3309
3648
|
this.emit('update');
|
|
3310
3649
|
return { agentId, customTitle };
|
|
3311
3650
|
}
|
|
@@ -3749,6 +4088,9 @@ class AgentManager extends EventEmitter {
|
|
|
3749
4088
|
const startsFreshCodexSession = provider === 'codex'
|
|
3750
4089
|
&& !hasResumableSession
|
|
3751
4090
|
&& (agent.providerSessionTemporary === true || !String(agent.providerSessionId || '').trim());
|
|
4091
|
+
if (startsFreshCodexSession && agent.terminalInputReceived === true) {
|
|
4092
|
+
return { error: 'Permission changes require a resumable provider session. Try again after the session id is available.' };
|
|
4093
|
+
}
|
|
3752
4094
|
if (!hasResumableSession && !startsFreshCodexSession) {
|
|
3753
4095
|
return { error: 'Permission changes require a resumable provider session. Try again after the session id is available.' };
|
|
3754
4096
|
}
|
|
@@ -4125,6 +4467,12 @@ class AgentManager extends EventEmitter {
|
|
|
4125
4467
|
if (!['same-worktree', 'new-worktree'].includes(mode)) {
|
|
4126
4468
|
return { error: 'Unsupported fork mode' };
|
|
4127
4469
|
}
|
|
4470
|
+
if (agent.providerSessionProvider === 'codex' && agent.providerSessionTemporary === true) {
|
|
4471
|
+
await this.providerSessionService.resolveTemporaryCodex(agentId, { force: true });
|
|
4472
|
+
if (agent.providerSessionTemporary === true) {
|
|
4473
|
+
return { error: 'Fork requires a resumable Codex session. Try again after the session id is available.' };
|
|
4474
|
+
}
|
|
4475
|
+
}
|
|
4128
4476
|
|
|
4129
4477
|
const sourceWorkspace = effectiveAgentWorkspaceRoot(agent);
|
|
4130
4478
|
const resumedSession = agent.providerSessionProvider
|
|
@@ -4274,6 +4622,7 @@ class AgentManager extends EventEmitter {
|
|
|
4274
4622
|
}
|
|
4275
4623
|
|
|
4276
4624
|
const removedMainPageSessionKeys = this.removeMainPageProviderSessionsForAgents([agent]);
|
|
4625
|
+
this.ensurePersistentAgentSession(agent, { visibleOnMainPage: false });
|
|
4277
4626
|
await this.killAgent(agentId, {
|
|
4278
4627
|
reason: 'manual-archive',
|
|
4279
4628
|
recordHistory: !isEphemeralShellAgent(agent),
|