farming-code 2.2.16 → 2.2.18
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 +477 -54
- 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-DTAQilnX.js +210 -0
- package/dist/assets/code-dark-5esj0bRu.css +1 -0
- package/dist/assets/{index-iKBvPjWS.js → index-fp8VhOes.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,12 @@ 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;
|
|
82
|
+
const CODEX_TERMINAL_START_READY_TIMEOUT_MS = 30_000;
|
|
83
|
+
const CODEX_TERMINAL_START_READY_POLL_MS = 50;
|
|
84
|
+
const CODEX_TERMINAL_START_OUTPUT_LIMIT = 64 * 1024;
|
|
79
85
|
const SHELL_PROMPT_ENV_KEYS = [
|
|
80
86
|
'PS1',
|
|
81
87
|
'PS2',
|
|
@@ -88,6 +94,40 @@ const SHELL_PROMPT_ENV_KEYS = [
|
|
|
88
94
|
];
|
|
89
95
|
const execFileAsync = promisify(execFile);
|
|
90
96
|
|
|
97
|
+
function withBoundedWait(promise, timeoutMs, label) {
|
|
98
|
+
let timer;
|
|
99
|
+
const timeout = new Promise((_, reject) => {
|
|
100
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
101
|
+
timer.unref?.();
|
|
102
|
+
});
|
|
103
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function canonicalProviderHomePath(value) {
|
|
107
|
+
const resolved = path.resolve(String(value || '').trim() || '.');
|
|
108
|
+
try {
|
|
109
|
+
return fs.realpathSync.native(resolved);
|
|
110
|
+
} catch {
|
|
111
|
+
return resolved;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function deletePrecreatedProviderSession(options = {}) {
|
|
116
|
+
const provider = String(options.provider || '').trim().toLowerCase();
|
|
117
|
+
const sessionId = String(options.sessionId || '').trim();
|
|
118
|
+
if (!isSafeProviderSessionId(sessionId)) throw new Error('Invalid pre-created provider session id');
|
|
119
|
+
let args = [];
|
|
120
|
+
if (provider === 'codex') args = ['delete', '--force', sessionId];
|
|
121
|
+
else if (provider === 'opencode') args = ['session', 'delete', sessionId];
|
|
122
|
+
else throw new Error(`${provider || 'Provider'} does not support pre-created session rollback`);
|
|
123
|
+
await execFileAsync(options.executable, args, {
|
|
124
|
+
cwd: options.cwd,
|
|
125
|
+
env: options.env,
|
|
126
|
+
timeout: 15_000,
|
|
127
|
+
maxBuffer: 1024 * 1024,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
91
131
|
function trimSessionOutput(output) {
|
|
92
132
|
const text = typeof output === 'string' ? output : '';
|
|
93
133
|
return text.length > SESSION_OUTPUT_LIMIT ? text.slice(-SESSION_OUTPUT_LIMIT) : text;
|
|
@@ -176,12 +216,6 @@ function applyTerminalStateCursor(agent, runtimeEpoch, outputSeq, stateRevision,
|
|
|
176
216
|
return true;
|
|
177
217
|
}
|
|
178
218
|
|
|
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
219
|
function codexCommandContinuesSession(command) {
|
|
186
220
|
const parts = parseCommand(command);
|
|
187
221
|
return path.basename(parts[0] || '') === 'codex'
|
|
@@ -378,13 +412,23 @@ function hasAgentOutputAfterAttentionBaseline(agent) {
|
|
|
378
412
|
return false;
|
|
379
413
|
}
|
|
380
414
|
|
|
381
|
-
function
|
|
382
|
-
if (!
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
415
|
+
function shouldRestoreAgentFromMetadata(record, mainPageSessionKeys) {
|
|
416
|
+
if (!record || record.archived === true) return false;
|
|
417
|
+
if (record.wantsMain === true) return true;
|
|
418
|
+
if (
|
|
419
|
+
record.providerSessionTemporary === true
|
|
420
|
+
|| isTemporaryProviderSessionId(record.providerSessionId)
|
|
421
|
+
) {
|
|
422
|
+
return record.visibleOnMainPage === true;
|
|
423
|
+
}
|
|
424
|
+
const provider = String(record.providerSessionProvider || record.provider || '').trim();
|
|
425
|
+
const sessionKey = record.providerSessionKey || mainPageAgentSessionKey(
|
|
426
|
+
provider,
|
|
427
|
+
record.providerSessionId,
|
|
428
|
+
record.providerHomeId || 'default'
|
|
429
|
+
);
|
|
430
|
+
if (sessionKey) return mainPageSessionKeys.has(sessionKey);
|
|
431
|
+
return record.visibleOnMainPage === true;
|
|
388
432
|
}
|
|
389
433
|
|
|
390
434
|
function recoveredEngineSessionId(entry, metadata = {}) {
|
|
@@ -549,6 +593,7 @@ class AgentManager extends EventEmitter {
|
|
|
549
593
|
);
|
|
550
594
|
this.agents = new RuntimeAgentMap();
|
|
551
595
|
this.mainAgentId = null;
|
|
596
|
+
this.mainAgentStartReservation = null;
|
|
552
597
|
this.lastActivity = new Map();
|
|
553
598
|
this.lastActivityUpdate = new Map();
|
|
554
599
|
this.outputEvents = new Map(); // Map<agentId, Array<{timestamp, bytes}>> for rate tracking
|
|
@@ -558,6 +603,8 @@ class AgentManager extends EventEmitter {
|
|
|
558
603
|
this.resizeDrains = new Map();
|
|
559
604
|
this.inputQueues = new Map();
|
|
560
605
|
this.codexTerminalProfileQueues = new Map();
|
|
606
|
+
this.codexTerminalStartQueues = new Map();
|
|
607
|
+
this.codexTerminalStartOutput = new Map();
|
|
561
608
|
this.agentWorktreeResolveGeneration = new Map();
|
|
562
609
|
this.permissionRestartInFlight = new Map();
|
|
563
610
|
this.runtimeRestartInFlight = new Map();
|
|
@@ -580,6 +627,12 @@ class AgentManager extends EventEmitter {
|
|
|
580
627
|
}
|
|
581
628
|
: {}),
|
|
582
629
|
});
|
|
630
|
+
this.createProviderSessionIdentity = typeof options.createProviderSessionIdentity === 'function'
|
|
631
|
+
? options.createProviderSessionIdentity
|
|
632
|
+
: createOptions => this.acpRuntime.createSessionIdentity(createOptions);
|
|
633
|
+
this.deleteProviderSessionIdentity = typeof options.deleteProviderSessionIdentity === 'function'
|
|
634
|
+
? options.deleteProviderSessionIdentity
|
|
635
|
+
: deletePrecreatedProviderSession;
|
|
583
636
|
this.archiveCodexSession = options.archiveCodexSession || archiveCodexSession;
|
|
584
637
|
this.unarchiveCodexSession = options.unarchiveCodexSession || unarchiveCodexSession;
|
|
585
638
|
this.heartbeatInterval = null;
|
|
@@ -718,6 +771,16 @@ class AgentManager extends EventEmitter {
|
|
|
718
771
|
|
|
719
772
|
this.reviveAgentRuntime(agent);
|
|
720
773
|
agent.output = trimSessionOutput(agent.output + data);
|
|
774
|
+
if (this.codexTerminalStartOutput.has(sessionId)) {
|
|
775
|
+
const startupOutput = `${this.codexTerminalStartOutput.get(sessionId) || ''}${data}`;
|
|
776
|
+
this.codexTerminalStartOutput.set(
|
|
777
|
+
sessionId,
|
|
778
|
+
startupOutput.slice(-CODEX_TERMINAL_START_OUTPUT_LIMIT),
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
if (agent.providerSessionProvider === 'codex' && agent.providerSessionTemporary === true) {
|
|
782
|
+
void this.providerSessionService.resolveTemporaryCodex(sessionId);
|
|
783
|
+
}
|
|
721
784
|
const outputAt = Date.now();
|
|
722
785
|
agent.lastEngineOutputAt = outputAt;
|
|
723
786
|
this.lastActivity.set(sessionId, outputAt);
|
|
@@ -1086,6 +1149,7 @@ class AgentManager extends EventEmitter {
|
|
|
1086
1149
|
const persistedRecords = this.configManager && typeof this.configManager.listAgentSessionRecords === 'function'
|
|
1087
1150
|
? this.configManager.listAgentSessionRecords()
|
|
1088
1151
|
: [];
|
|
1152
|
+
const mainPageSessionKeys = new Set(this.getMainPageSessionKeys());
|
|
1089
1153
|
const persistedByRuntimeAgentId = new Map(persistedRecords
|
|
1090
1154
|
.filter(record => record && record.runtimeAgentId)
|
|
1091
1155
|
.map(record => [record.runtimeAgentId, record]));
|
|
@@ -1096,6 +1160,11 @@ class AgentManager extends EventEmitter {
|
|
|
1096
1160
|
const state = entry.state || {};
|
|
1097
1161
|
const agentId = recoveredEngineSessionId(entry, engineMetadata);
|
|
1098
1162
|
const persisted = persistedByRuntimeAgentId.get(agentId);
|
|
1163
|
+
const desiredMetadata = persisted || engineMetadata;
|
|
1164
|
+
if (!shouldRestoreAgentFromMetadata(desiredMetadata, mainPageSessionKeys)) {
|
|
1165
|
+
await this.killRecoveredEngineSession(entry, engineMetadata, agentId);
|
|
1166
|
+
continue;
|
|
1167
|
+
}
|
|
1099
1168
|
// The persisted runtime mode is authoritative. A PTY can outlive the
|
|
1100
1169
|
// server long enough to appear in native-host recovery after the Agent
|
|
1101
1170
|
// has already switched to ACP. Recovering that stale PTY first would
|
|
@@ -1114,6 +1183,7 @@ class AgentManager extends EventEmitter {
|
|
|
1114
1183
|
// even briefly makes Chat/Terminal switching disappear until a later
|
|
1115
1184
|
// provider resolver update happens to repair it.
|
|
1116
1185
|
source: persisted.source || engineMetadata.source,
|
|
1186
|
+
persistentSessionId: persisted.id || persisted.persistentSessionId || engineMetadata.persistentSessionId,
|
|
1117
1187
|
projectWorkspace: persisted.projectWorkspace || engineMetadata.projectWorkspace,
|
|
1118
1188
|
provider: persistedProvider || engineMetadata.provider,
|
|
1119
1189
|
providerSessionProvider: persistedProvider || engineMetadata.providerSessionProvider,
|
|
@@ -1131,21 +1201,23 @@ class AgentManager extends EventEmitter {
|
|
|
1131
1201
|
terminalInputReceived: Object.prototype.hasOwnProperty.call(persisted, 'terminalInputReceived')
|
|
1132
1202
|
? persisted.terminalInputReceived === true
|
|
1133
1203
|
: engineMetadata.terminalInputReceived,
|
|
1204
|
+
customTitle: Object.prototype.hasOwnProperty.call(persisted, 'customTitle')
|
|
1205
|
+
? persisted.customTitle
|
|
1206
|
+
: engineMetadata.customTitle,
|
|
1134
1207
|
...legacyRuntimeMetadata(persisted),
|
|
1135
1208
|
pinned: persisted.pinned === true,
|
|
1136
1209
|
projectOrder: finiteOrder(persisted.projectOrder) ?? finiteOrder(engineMetadata.projectOrder),
|
|
1137
1210
|
pinnedOrder: finiteOrder(persisted.pinnedOrder) ?? finiteOrder(engineMetadata.pinnedOrder),
|
|
1138
1211
|
} : engineMetadata;
|
|
1139
1212
|
if (!agentId || this.agents.has(agentId)) continue;
|
|
1140
|
-
if (!shouldRecoverEngineSession(metadata)) {
|
|
1141
|
-
await this.killRecoveredEngineSession(entry, metadata, agentId);
|
|
1142
|
-
continue;
|
|
1143
|
-
}
|
|
1144
1213
|
|
|
1145
1214
|
const agentRecord = this.recoveredAgentRecord(agentId, entry.engineName || metadata.engineName || 'native', metadata, state);
|
|
1146
1215
|
ensureAgentOrders(agentRecord, Array.from(this.agents.values()));
|
|
1147
1216
|
agentRecord.lastObservedTurnActive = this.isAgentAttentionTurnActive(agentRecord);
|
|
1148
|
-
this.ensurePersistentAgentSession(agentRecord
|
|
1217
|
+
this.ensurePersistentAgentSession(agentRecord, {
|
|
1218
|
+
visibleOnMainPage: true,
|
|
1219
|
+
archived: false,
|
|
1220
|
+
});
|
|
1149
1221
|
this.agents.set(agentId, agentRecord);
|
|
1150
1222
|
void this.refreshAgentWorktree(agentId);
|
|
1151
1223
|
this.lastActivity.set(agentId, state.lastActivityAt || metadata.lastActivityAt || Date.now());
|
|
@@ -1172,6 +1244,7 @@ class AgentManager extends EventEmitter {
|
|
|
1172
1244
|
|
|
1173
1245
|
async restoreTerminalSessionsAfterRuntimeRotation(records, rotations) {
|
|
1174
1246
|
const mainPageOrder = new Map(this.getMainPageSessionKeys().map((key, index) => [key, index]));
|
|
1247
|
+
const mainPageSessionKeys = new Set(mainPageOrder.keys());
|
|
1175
1248
|
const liveProviderSessions = new Set(
|
|
1176
1249
|
[...this.agents.values()]
|
|
1177
1250
|
.filter(agent => agent?.providerSessionProvider && agent?.providerSessionId)
|
|
@@ -1265,8 +1338,9 @@ class AgentManager extends EventEmitter {
|
|
|
1265
1338
|
return (Number(right.updatedAt) || 0) - (Number(left.updatedAt) || 0);
|
|
1266
1339
|
})
|
|
1267
1340
|
: fallbackCandidates;
|
|
1341
|
+
const desiredCandidates = candidates.filter(record => shouldRestoreAgentFromMetadata(record, mainPageSessionKeys));
|
|
1268
1342
|
|
|
1269
|
-
if (
|
|
1343
|
+
if (desiredCandidates.length > 0) {
|
|
1270
1344
|
const rotationSummary = (Array.isArray(rotations) ? rotations : []).map(rotation => {
|
|
1271
1345
|
const { serializedTerminalState, ...rest } = rotation || {};
|
|
1272
1346
|
return {
|
|
@@ -1277,13 +1351,13 @@ class AgentManager extends EventEmitter {
|
|
|
1277
1351
|
};
|
|
1278
1352
|
});
|
|
1279
1353
|
console.warn(
|
|
1280
|
-
`Restoring ${
|
|
1354
|
+
`Restoring ${desiredCandidates.length} Terminal session(s) after native PTY runtime rotation`,
|
|
1281
1355
|
rotationSummary
|
|
1282
1356
|
);
|
|
1283
1357
|
}
|
|
1284
1358
|
|
|
1285
1359
|
let changed = false;
|
|
1286
|
-
for (const record of
|
|
1360
|
+
for (const record of desiredCandidates) {
|
|
1287
1361
|
if (record.wantsMain === true && this.mainAgentId) continue;
|
|
1288
1362
|
const provider = String(record.providerSessionProvider || record.provider || '').trim();
|
|
1289
1363
|
const sessionId = record.providerSessionId;
|
|
@@ -1293,6 +1367,19 @@ class AgentManager extends EventEmitter {
|
|
|
1293
1367
|
record.providerHomeId || 'default'
|
|
1294
1368
|
);
|
|
1295
1369
|
if (sessionKey && liveProviderSessions.has(sessionKey)) continue;
|
|
1370
|
+
if (
|
|
1371
|
+
provider === 'codex'
|
|
1372
|
+
&& record.terminalInputReceived === true
|
|
1373
|
+
&& (
|
|
1374
|
+
record.providerSessionTemporary === true
|
|
1375
|
+
|| isTemporaryProviderSessionId(sessionId)
|
|
1376
|
+
)
|
|
1377
|
+
) {
|
|
1378
|
+
console.warn(
|
|
1379
|
+
`Refusing to replace Codex Terminal ${record.runtimeAgentId || sessionId} after native PTY runtime rotation without an exact resume id`
|
|
1380
|
+
);
|
|
1381
|
+
continue;
|
|
1382
|
+
}
|
|
1296
1383
|
const canResumeProvider = Boolean(getProviderAdapter(provider))
|
|
1297
1384
|
&& isSafeProviderSessionId(sessionId);
|
|
1298
1385
|
const command = canResumeProvider
|
|
@@ -1386,8 +1473,12 @@ class AgentManager extends EventEmitter {
|
|
|
1386
1473
|
async recoverAcpSessions() {
|
|
1387
1474
|
if (!this.acpRuntime || !this.configManager || typeof this.configManager.listAgentSessionRecords !== 'function') return;
|
|
1388
1475
|
const mainPageOrder = new Map(this.getMainPageSessionKeys().map((key, index) => [key, index]));
|
|
1476
|
+
const mainPageSessionKeys = new Set(mainPageOrder.keys());
|
|
1389
1477
|
const records = this.configManager.listAgentSessionRecords()
|
|
1390
|
-
.filter(record =>
|
|
1478
|
+
.filter(record => (
|
|
1479
|
+
shouldRestoreAgentFromMetadata(record, mainPageSessionKeys)
|
|
1480
|
+
&& runtimeKind(record) === 'acp'
|
|
1481
|
+
))
|
|
1391
1482
|
.sort((left, right) => {
|
|
1392
1483
|
const leftOrder = mainPageOrder.get(left.providerSessionKey);
|
|
1393
1484
|
const rightOrder = mainPageOrder.get(right.providerSessionKey);
|
|
@@ -1716,6 +1807,7 @@ class AgentManager extends EventEmitter {
|
|
|
1716
1807
|
readOutputSeq: finiteNumberOrNull(agent.readOutputSeq),
|
|
1717
1808
|
projectOrder: finiteOrder(agent.projectOrder),
|
|
1718
1809
|
pinnedOrder: finiteOrder(agent.pinnedOrder),
|
|
1810
|
+
customTitle: agent.customTitle || '',
|
|
1719
1811
|
})).catch((error) => {
|
|
1720
1812
|
console.warn('Failed to update provider session metadata:', error && (error.message || error));
|
|
1721
1813
|
});
|
|
@@ -2193,6 +2285,8 @@ class AgentManager extends EventEmitter {
|
|
|
2193
2285
|
this.resizeDrains.clear();
|
|
2194
2286
|
this.inputQueues.clear();
|
|
2195
2287
|
this.codexTerminalProfileQueues.clear();
|
|
2288
|
+
this.codexTerminalStartQueues.clear();
|
|
2289
|
+
this.codexTerminalStartOutput.clear();
|
|
2196
2290
|
this.acpSessionOptionsByKey.clear();
|
|
2197
2291
|
if (this.jsonCliRuntime) {
|
|
2198
2292
|
for (const agentId of this.agents.keys()) this.jsonCliRuntime.unregisterAgent(agentId);
|
|
@@ -2280,11 +2374,134 @@ class AgentManager extends EventEmitter {
|
|
|
2280
2374
|
});
|
|
2281
2375
|
}
|
|
2282
2376
|
|
|
2377
|
+
async stopUncertainTerminalSession(engine, agentId) {
|
|
2378
|
+
if (typeof engine?.killSession !== 'function' || typeof engine?.getSessionState !== 'function') {
|
|
2379
|
+
throw new Error('Session engine cannot prove an uncertain Terminal start has stopped');
|
|
2380
|
+
}
|
|
2381
|
+
await withBoundedWait(
|
|
2382
|
+
Promise.resolve(engine.killSession(agentId)),
|
|
2383
|
+
UNCERTAIN_TERMINAL_STOP_TIMEOUT_MS,
|
|
2384
|
+
'Terminal kill request',
|
|
2385
|
+
);
|
|
2386
|
+
const deadline = Date.now() + UNCERTAIN_TERMINAL_STOP_TIMEOUT_MS;
|
|
2387
|
+
while (Date.now() <= deadline) {
|
|
2388
|
+
const state = await withBoundedWait(
|
|
2389
|
+
Promise.resolve(engine.getSessionState(agentId)),
|
|
2390
|
+
TERMINAL_STOP_STATE_READ_TIMEOUT_MS,
|
|
2391
|
+
'Terminal stop-state read',
|
|
2392
|
+
);
|
|
2393
|
+
if (!state || ['dead', 'exited', 'stopped'].includes(String(state.status || ''))) return;
|
|
2394
|
+
await new Promise(resolve => setTimeout(resolve, TERMINAL_STOP_POLL_MS));
|
|
2395
|
+
}
|
|
2396
|
+
throw new Error(`Terminal ${agentId} did not reach an exited state after kill`);
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
async waitForCodexTerminalStart(agentId) {
|
|
2400
|
+
const deadline = Date.now() + CODEX_TERMINAL_START_READY_TIMEOUT_MS;
|
|
2401
|
+
while (Date.now() <= deadline) {
|
|
2402
|
+
const agent = this.agents.get(agentId);
|
|
2403
|
+
if (!agent) throw new Error(`Codex Terminal ${agentId} disappeared during startup`);
|
|
2404
|
+
if (['dead', 'stopped'].includes(agent.status) || agent.engineStatus === 'exited') {
|
|
2405
|
+
const detail = trimSessionOutput(agent.previewText || agent.output || '').trim();
|
|
2406
|
+
throw new Error(detail || `Codex Terminal ${agentId} exited during startup`);
|
|
2407
|
+
}
|
|
2408
|
+
if ((this.codexTerminalStartOutput.get(agentId) || '').includes('\u001b')) {
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2411
|
+
await new Promise(resolve => setTimeout(resolve, CODEX_TERMINAL_START_READY_POLL_MS));
|
|
2412
|
+
}
|
|
2413
|
+
throw new Error(`Codex Terminal ${agentId} did not become ready within ${CODEX_TERMINAL_START_READY_TIMEOUT_MS}ms`);
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
async enqueueCodexTerminalStart(providerHomeKey, operation) {
|
|
2417
|
+
const key = canonicalProviderHomePath(providerHomeKey || '.');
|
|
2418
|
+
const previous = this.codexTerminalStartQueues.get(key) || Promise.resolve();
|
|
2419
|
+
const next = previous.catch(() => {}).then(operation);
|
|
2420
|
+
this.codexTerminalStartQueues.set(key, next);
|
|
2421
|
+
try {
|
|
2422
|
+
return await next;
|
|
2423
|
+
} finally {
|
|
2424
|
+
if (this.codexTerminalStartQueues.get(key) === next) {
|
|
2425
|
+
this.codexTerminalStartQueues.delete(key);
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2283
2430
|
async startAgent(command, customWorkspace, callback, options = {}) {
|
|
2284
2431
|
if (options.wantsMain !== false && options.skipRecoveryWait !== true) {
|
|
2285
2432
|
await this.whenRecovered();
|
|
2286
2433
|
}
|
|
2287
2434
|
|
|
2435
|
+
const wantsMain = options.wantsMain === true || (options.wantsMain !== false && !this.mainAgentId);
|
|
2436
|
+
if (!wantsMain) {
|
|
2437
|
+
return this.startAgentUnreserved(command, customWorkspace, callback, {
|
|
2438
|
+
...options,
|
|
2439
|
+
wantsMain: false,
|
|
2440
|
+
skipRecoveryWait: true,
|
|
2441
|
+
});
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
if (this.mainAgentStartReservation) {
|
|
2445
|
+
const outcome = await this.mainAgentStartReservation.promise;
|
|
2446
|
+
if (callback) callback(outcome.agentId, outcome.error);
|
|
2447
|
+
return outcome.agentId;
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
const existingMainStart = this.findActiveMainAgentStart();
|
|
2451
|
+
if (existingMainStart) {
|
|
2452
|
+
if (this.mainAgentId !== existingMainStart.id) {
|
|
2453
|
+
this.mainAgentId = existingMainStart.id;
|
|
2454
|
+
this.emit('update');
|
|
2455
|
+
}
|
|
2456
|
+
console.log('Main Agent already starting or running:', existingMainStart.id);
|
|
2457
|
+
if (callback) callback(existingMainStart.id);
|
|
2458
|
+
return existingMainStart.id;
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
let resolveReservation;
|
|
2462
|
+
const reservation = {
|
|
2463
|
+
promise: new Promise(resolve => {
|
|
2464
|
+
resolveReservation = resolve;
|
|
2465
|
+
}),
|
|
2466
|
+
};
|
|
2467
|
+
this.mainAgentStartReservation = reservation;
|
|
2468
|
+
let outcome = null;
|
|
2469
|
+
let callbackCalled = false;
|
|
2470
|
+
const reservedCallback = (agentId, error) => {
|
|
2471
|
+
callbackCalled = true;
|
|
2472
|
+
outcome = { agentId: agentId || null, error: error || null };
|
|
2473
|
+
if (callback) callback(agentId, error);
|
|
2474
|
+
};
|
|
2475
|
+
try {
|
|
2476
|
+
const agentId = await this.startAgentUnreserved(command, customWorkspace, reservedCallback, {
|
|
2477
|
+
...options,
|
|
2478
|
+
wantsMain: true,
|
|
2479
|
+
skipRecoveryWait: true,
|
|
2480
|
+
});
|
|
2481
|
+
if (!callbackCalled) {
|
|
2482
|
+
outcome = { agentId: agentId || null, error: null };
|
|
2483
|
+
if (callback) callback(agentId);
|
|
2484
|
+
}
|
|
2485
|
+
return agentId;
|
|
2486
|
+
} catch (error) {
|
|
2487
|
+
if (!callbackCalled) {
|
|
2488
|
+
outcome = { agentId: null, error: error && (error.message || String(error)) };
|
|
2489
|
+
if (callback) callback(null, outcome.error);
|
|
2490
|
+
}
|
|
2491
|
+
throw error;
|
|
2492
|
+
} finally {
|
|
2493
|
+
resolveReservation(outcome || { agentId: null, error: 'Main Agent failed to start' });
|
|
2494
|
+
if (this.mainAgentStartReservation === reservation) {
|
|
2495
|
+
this.mainAgentStartReservation = null;
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
async startAgentUnreserved(command, customWorkspace, callback, options = {}) {
|
|
2501
|
+
if (options.wantsMain !== false && options.skipRecoveryWait !== true) {
|
|
2502
|
+
await this.whenRecovered();
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2288
2505
|
const wantsMain = options.wantsMain === true || (options.wantsMain !== false && !this.mainAgentId);
|
|
2289
2506
|
if (wantsMain) {
|
|
2290
2507
|
const existingMainStart = this.findActiveMainAgentStart();
|
|
@@ -2357,13 +2574,28 @@ class AgentManager extends EventEmitter {
|
|
|
2357
2574
|
});
|
|
2358
2575
|
|
|
2359
2576
|
const hasResumeSource = Boolean(resumedSessionFromSource(resolvedSource));
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2577
|
+
const commandProviderSessionPlan = hasResumeSource
|
|
2578
|
+
? buildAgentProviderSessionPlan({
|
|
2579
|
+
command,
|
|
2580
|
+
program,
|
|
2581
|
+
args: launch.args,
|
|
2582
|
+
source: 'ui',
|
|
2583
|
+
})
|
|
2584
|
+
: null;
|
|
2585
|
+
const commandContinuesProviderSession = commandProviderSessionPlan
|
|
2586
|
+
&& commandProviderSessionPlan.temporary !== true
|
|
2587
|
+
&& commandProviderSessionPlan.provider === providerSessionPlan.provider
|
|
2588
|
+
&& commandProviderSessionPlan.id === providerSessionPlan.id;
|
|
2589
|
+
if (
|
|
2590
|
+
providerSessionPlan.source === 'resume-source'
|
|
2591
|
+
&& hasResumeSource
|
|
2592
|
+
&& !commandContinuesProviderSession
|
|
2593
|
+
) {
|
|
2594
|
+
providerSessionPlan = commandProviderSessionPlan;
|
|
2595
|
+
}
|
|
2596
|
+
if (providerSessionPlan.error) {
|
|
2597
|
+
if (callback) callback(null, providerSessionPlan.error);
|
|
2598
|
+
return null;
|
|
2367
2599
|
}
|
|
2368
2600
|
|
|
2369
2601
|
let args = providerSessionPlan.args;
|
|
@@ -2446,13 +2678,6 @@ class AgentManager extends EventEmitter {
|
|
|
2446
2678
|
}
|
|
2447
2679
|
}
|
|
2448
2680
|
|
|
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
2681
|
if (!fs.existsSync(workspace)) {
|
|
2457
2682
|
console.log('Workspace does not exist:', workspace);
|
|
2458
2683
|
if (callback) callback(null, `Workspace does not exist: ${workspace}`);
|
|
@@ -2519,8 +2744,7 @@ class AgentManager extends EventEmitter {
|
|
|
2519
2744
|
: 'terminal';
|
|
2520
2745
|
// A fresh structured runtime does not need a provider CLI resume id yet:
|
|
2521
2746
|
// 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.
|
|
2747
|
+
// after connecting. Fresh Terminal sessions are handled separately below.
|
|
2524
2748
|
const structuredRuntimeProvider = ['json', 'acp', 'chat'].includes(requestedAgentRuntimeMode)
|
|
2525
2749
|
? homeProvider
|
|
2526
2750
|
: providerSessionPlan.provider;
|
|
@@ -2630,6 +2854,135 @@ class AgentManager extends EventEmitter {
|
|
|
2630
2854
|
attentionSuppressUntil: 0
|
|
2631
2855
|
};
|
|
2632
2856
|
|
|
2857
|
+
let precreatedProviderSession = null;
|
|
2858
|
+
if (
|
|
2859
|
+
providerSessionPlan.precreate === true
|
|
2860
|
+
&& !useAcp
|
|
2861
|
+
&& !useJsonCli
|
|
2862
|
+
) {
|
|
2863
|
+
const adapter = getProviderAdapter(providerSessionPlan.provider);
|
|
2864
|
+
if (typeof adapter?.terminalResumeArgs !== 'function') {
|
|
2865
|
+
const message = `${providerSessionPlan.provider} cannot resume a pre-created Terminal session`;
|
|
2866
|
+
if (callback) callback(null, message);
|
|
2867
|
+
return null;
|
|
2868
|
+
}
|
|
2869
|
+
try {
|
|
2870
|
+
const requestedIdentityWorkspace = this.expandWorkspacePath(providerSessionPlan.identityWorkspace || '');
|
|
2871
|
+
const identityWorkspace = requestedIdentityWorkspace
|
|
2872
|
+
? (path.isAbsolute(requestedIdentityWorkspace)
|
|
2873
|
+
? path.resolve(requestedIdentityWorkspace)
|
|
2874
|
+
: path.resolve(workspace, requestedIdentityWorkspace))
|
|
2875
|
+
: workspace;
|
|
2876
|
+
let identityWorkspaceExists = false;
|
|
2877
|
+
try {
|
|
2878
|
+
identityWorkspaceExists = fs.statSync(identityWorkspace).isDirectory();
|
|
2879
|
+
} catch {
|
|
2880
|
+
identityWorkspaceExists = false;
|
|
2881
|
+
}
|
|
2882
|
+
if (!identityWorkspaceExists) {
|
|
2883
|
+
throw new Error(`Workspace does not exist: ${identityWorkspace}`);
|
|
2884
|
+
}
|
|
2885
|
+
const identityEnv = this.buildAgentEnv(agentId, agentRecord);
|
|
2886
|
+
const requestedAdditionalDirectories = Array.isArray(options.additionalDirectories)
|
|
2887
|
+
? options.additionalDirectories
|
|
2888
|
+
: [];
|
|
2889
|
+
const requestedMcpServers = Array.isArray(options.mcpServers) ? options.mcpServers : [];
|
|
2890
|
+
const created = await this.createProviderSessionIdentity({
|
|
2891
|
+
provider: providerSessionPlan.provider,
|
|
2892
|
+
executable: spawnProgram,
|
|
2893
|
+
env: identityEnv,
|
|
2894
|
+
cwd: identityWorkspace,
|
|
2895
|
+
providerHomeId: agentRecord.providerHomeId || 'default',
|
|
2896
|
+
approvalMode: agentRecord.launchPermissionMode || 'approve',
|
|
2897
|
+
model: codexModel,
|
|
2898
|
+
reasoningEffort: codexReasoningEffort,
|
|
2899
|
+
serviceTier: codexServiceTier,
|
|
2900
|
+
additionalDirectories: requestedAdditionalDirectories,
|
|
2901
|
+
mcpServers: requestedMcpServers,
|
|
2902
|
+
});
|
|
2903
|
+
const providerSessionId = String(created?.sessionId || '').trim();
|
|
2904
|
+
if (!isSafeProviderSessionId(providerSessionId)) {
|
|
2905
|
+
throw new Error(`${providerSessionPlan.provider} ACP session/new returned an invalid session id`);
|
|
2906
|
+
}
|
|
2907
|
+
const normalizedSessionOptions = created?.sessionRequestOptions || {
|
|
2908
|
+
additionalDirectories: requestedAdditionalDirectories.map(directory => path.resolve(identityWorkspace, directory)),
|
|
2909
|
+
mcpServers: JSON.parse(JSON.stringify(requestedMcpServers)),
|
|
2910
|
+
};
|
|
2911
|
+
args = adapter.terminalResumeArgs(args, providerSessionId, providerSessionPlan);
|
|
2912
|
+
providerSessionPlan = {
|
|
2913
|
+
...providerSessionPlan,
|
|
2914
|
+
id: providerSessionId,
|
|
2915
|
+
precreate: false,
|
|
2916
|
+
temporary: false,
|
|
2917
|
+
source: 'acp-precreated',
|
|
2918
|
+
args,
|
|
2919
|
+
};
|
|
2920
|
+
agentRecord.providerSessionId = providerSessionId;
|
|
2921
|
+
agentRecord.providerSessionKey = mainPageAgentSessionKey(
|
|
2922
|
+
providerSessionPlan.provider,
|
|
2923
|
+
providerSessionId,
|
|
2924
|
+
agentRecord.providerHomeId || 'default'
|
|
2925
|
+
);
|
|
2926
|
+
agentRecord.providerSessionTemporary = false;
|
|
2927
|
+
agentRecord.providerSessionSource = providerSessionPlan.source;
|
|
2928
|
+
agentRecord.providerSessionResolvedAt = Date.now();
|
|
2929
|
+
this.acpSessionOptionsByKey.set(agentRecord.providerSessionKey, {
|
|
2930
|
+
additionalDirectories: Array.isArray(normalizedSessionOptions.additionalDirectories)
|
|
2931
|
+
? [...normalizedSessionOptions.additionalDirectories]
|
|
2932
|
+
: [],
|
|
2933
|
+
mcpServers: Array.isArray(normalizedSessionOptions.mcpServers)
|
|
2934
|
+
? JSON.parse(JSON.stringify(normalizedSessionOptions.mcpServers))
|
|
2935
|
+
: [],
|
|
2936
|
+
});
|
|
2937
|
+
precreatedProviderSession = {
|
|
2938
|
+
provider: providerSessionPlan.provider,
|
|
2939
|
+
executable: spawnProgram,
|
|
2940
|
+
env: identityEnv,
|
|
2941
|
+
cwd: identityWorkspace,
|
|
2942
|
+
sessionId: providerSessionId,
|
|
2943
|
+
sessionKey: agentRecord.providerSessionKey,
|
|
2944
|
+
};
|
|
2945
|
+
} catch (error) {
|
|
2946
|
+
let identityRollbackError = null;
|
|
2947
|
+
let identityRetainedReason = '';
|
|
2948
|
+
const orphanedIdentity = error?.providerSessionIdentity;
|
|
2949
|
+
if (
|
|
2950
|
+
orphanedIdentity
|
|
2951
|
+
&& orphanedIdentity.producerStopped === true
|
|
2952
|
+
&& isSafeProviderSessionId(orphanedIdentity.sessionId)
|
|
2953
|
+
) {
|
|
2954
|
+
try {
|
|
2955
|
+
await this.deleteProviderSessionIdentity(orphanedIdentity);
|
|
2956
|
+
} catch (cleanupError) {
|
|
2957
|
+
identityRollbackError = cleanupError;
|
|
2958
|
+
}
|
|
2959
|
+
} else if (orphanedIdentity && !isSafeProviderSessionId(orphanedIdentity.sessionId)) {
|
|
2960
|
+
identityRetainedReason = 'provider returned an unsafe session id; it was retained without invoking CLI rollback';
|
|
2961
|
+
} else if (orphanedIdentity) {
|
|
2962
|
+
identityRetainedReason = 'provider session retained because ACP producer shutdown could not be proven';
|
|
2963
|
+
}
|
|
2964
|
+
const baseMessage = error && error.message
|
|
2965
|
+
? error.message
|
|
2966
|
+
: `Failed to create ${providerSessionPlan.provider} session identity`;
|
|
2967
|
+
let message = baseMessage;
|
|
2968
|
+
if (identityRollbackError) {
|
|
2969
|
+
message = `${baseMessage}; provider session rollback failed: ${identityRollbackError.message || identityRollbackError}`;
|
|
2970
|
+
} else if (identityRetainedReason) {
|
|
2971
|
+
message = `${baseMessage}; ${identityRetainedReason}`;
|
|
2972
|
+
}
|
|
2973
|
+
console.error('Failed to create provider session identity:', error);
|
|
2974
|
+
if (callback) callback(null, message);
|
|
2975
|
+
return null;
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
const logArgs = args.map((arg, index) => (
|
|
2980
|
+
index > 0 && args[index - 1] === '--append-system-prompt'
|
|
2981
|
+
? '<farming-main-agent-bootstrap>'
|
|
2982
|
+
: arg
|
|
2983
|
+
));
|
|
2984
|
+
console.log('Starting agent:', program, logArgs, 'workspace:', workspace, spawnProgram !== program ? `resolved: ${spawnProgram}` : '');
|
|
2985
|
+
|
|
2633
2986
|
ensureAgentOrders(agentRecord, Array.from(this.agents.values()));
|
|
2634
2987
|
this.agents.set(agentId, agentRecord);
|
|
2635
2988
|
void this.refreshAgentWorktree(agentId);
|
|
@@ -2716,17 +3069,40 @@ class AgentManager extends EventEmitter {
|
|
|
2716
3069
|
acpRuntime.error = '';
|
|
2717
3070
|
}
|
|
2718
3071
|
|
|
2719
|
-
agentRecord.persistentSessionId = this.ensurePersistentAgentSession(agentRecord);
|
|
2720
3072
|
if (!useJsonCli && !useAcp) {
|
|
2721
|
-
const
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
3073
|
+
const serializeCodexStartup = agentRecord.providerSessionProvider === 'codex'
|
|
3074
|
+
&& resolution.engineName === 'native';
|
|
3075
|
+
const startTerminal = async () => {
|
|
3076
|
+
const engineLaunch = {
|
|
3077
|
+
command: spawnProgram,
|
|
3078
|
+
args,
|
|
3079
|
+
cwd: workspace,
|
|
3080
|
+
category: resolution.spec ? resolution.spec.category : 'shell',
|
|
3081
|
+
reviveState: options.reviveTerminalState || null,
|
|
3082
|
+
};
|
|
3083
|
+
if (serializeCodexStartup) this.codexTerminalStartOutput.set(agentId, '');
|
|
3084
|
+
try {
|
|
3085
|
+
await this.createAgentEngineSession(agentRecord, resolution.engine, engineLaunch);
|
|
3086
|
+
if (serializeCodexStartup) {
|
|
3087
|
+
await this.waitForCodexTerminalStart(agentId);
|
|
3088
|
+
}
|
|
3089
|
+
} finally {
|
|
3090
|
+
if (serializeCodexStartup) this.codexTerminalStartOutput.delete(agentId);
|
|
3091
|
+
}
|
|
2727
3092
|
};
|
|
2728
|
-
|
|
3093
|
+
if (serializeCodexStartup) {
|
|
3094
|
+
await this.enqueueCodexTerminalStart(
|
|
3095
|
+
providerHomePath || resolvedProviderHomeId || 'default',
|
|
3096
|
+
startTerminal
|
|
3097
|
+
);
|
|
3098
|
+
} else {
|
|
3099
|
+
await startTerminal();
|
|
3100
|
+
}
|
|
2729
3101
|
}
|
|
3102
|
+
agentRecord.persistentSessionId = this.ensurePersistentAgentSession(agentRecord, {
|
|
3103
|
+
visibleOnMainPage: true,
|
|
3104
|
+
archived: false,
|
|
3105
|
+
});
|
|
2730
3106
|
|
|
2731
3107
|
const agent = this.agents.get(agentId);
|
|
2732
3108
|
if (agent && agent.status === 'pending') {
|
|
@@ -2745,6 +3121,33 @@ class AgentManager extends EventEmitter {
|
|
|
2745
3121
|
return agentId;
|
|
2746
3122
|
} catch (error) {
|
|
2747
3123
|
console.error('Failed to start agent:', error);
|
|
3124
|
+
let runtimeCleanupError = null;
|
|
3125
|
+
if (!useJsonCli && !useAcp) {
|
|
3126
|
+
try {
|
|
3127
|
+
await this.stopUncertainTerminalSession(resolution.engine, agentId);
|
|
3128
|
+
} catch (engineCleanupError) {
|
|
3129
|
+
runtimeCleanupError = engineCleanupError;
|
|
3130
|
+
console.error(
|
|
3131
|
+
'Failed to stop partially started Agent runtime:',
|
|
3132
|
+
engineCleanupError && (engineCleanupError.message || engineCleanupError)
|
|
3133
|
+
);
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
let rollbackError = null;
|
|
3137
|
+
if (precreatedProviderSession && !runtimeCleanupError) {
|
|
3138
|
+
try {
|
|
3139
|
+
await this.deleteProviderSessionIdentity(precreatedProviderSession);
|
|
3140
|
+
} catch (cleanupError) {
|
|
3141
|
+
rollbackError = cleanupError;
|
|
3142
|
+
console.error(
|
|
3143
|
+
'Failed to roll back pre-created provider session:',
|
|
3144
|
+
cleanupError && (cleanupError.message || cleanupError)
|
|
3145
|
+
);
|
|
3146
|
+
}
|
|
3147
|
+
this.acpSessionOptionsByKey.delete(precreatedProviderSession.sessionKey);
|
|
3148
|
+
} else if (precreatedProviderSession) {
|
|
3149
|
+
this.acpSessionOptionsByKey.delete(precreatedProviderSession.sessionKey);
|
|
3150
|
+
}
|
|
2748
3151
|
if (options.restoreRuntimeAgentIdOnFailure && agentRecord.persistentSessionId) {
|
|
2749
3152
|
const failedAgentId = agentRecord.id;
|
|
2750
3153
|
try {
|
|
@@ -2773,7 +3176,14 @@ class AgentManager extends EventEmitter {
|
|
|
2773
3176
|
}
|
|
2774
3177
|
|
|
2775
3178
|
this.emit('update');
|
|
2776
|
-
|
|
3179
|
+
const startupError = error && (error.message || String(error));
|
|
3180
|
+
const cleanupSuffix = rollbackError
|
|
3181
|
+
? `; provider session rollback failed: ${rollbackError.message || rollbackError}`
|
|
3182
|
+
: '';
|
|
3183
|
+
const runtimeCleanupSuffix = runtimeCleanupError
|
|
3184
|
+
? `; Terminal rollback failed and the provider session was retained: ${runtimeCleanupError.message || runtimeCleanupError}`
|
|
3185
|
+
: '';
|
|
3186
|
+
if (callback) callback(null, `${startupError}${runtimeCleanupSuffix}${cleanupSuffix}`);
|
|
2777
3187
|
return null;
|
|
2778
3188
|
}
|
|
2779
3189
|
}
|
|
@@ -3129,15 +3539,16 @@ class AgentManager extends EventEmitter {
|
|
|
3129
3539
|
const engine = this.engineBridge.getEngine(agent.engineName);
|
|
3130
3540
|
if (!engine) return;
|
|
3131
3541
|
|
|
3542
|
+
const submittedUserInput = markUserInput && hasSubmittedTerminalInput(input);
|
|
3543
|
+
if (submittedUserInput && agent.terminalInputReceived !== true) {
|
|
3544
|
+
agent.terminalInputReceived = true;
|
|
3545
|
+
this.ensurePersistentAgentSession(agent);
|
|
3546
|
+
this.updateEngineProviderSessionMetadata(agent);
|
|
3547
|
+
this.emit('agent-update', { agentId, patch: { terminalInputReceived: true } });
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3132
3550
|
try {
|
|
3133
|
-
const submittedUserInput = markUserInput && hasSubmittedTerminalInput(input);
|
|
3134
3551
|
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
3552
|
if (submittedUserInput) {
|
|
3142
3553
|
this.providerSessionService.observe(agentId, { force: true });
|
|
3143
3554
|
}
|
|
@@ -3306,6 +3717,8 @@ class AgentManager extends EventEmitter {
|
|
|
3306
3717
|
|
|
3307
3718
|
const customTitle = String(title || '').trim().slice(0, 80);
|
|
3308
3719
|
agent.customTitle = customTitle;
|
|
3720
|
+
this.ensurePersistentAgentSession(agent);
|
|
3721
|
+
this.updateEngineProviderSessionMetadata(agent);
|
|
3309
3722
|
this.emit('update');
|
|
3310
3723
|
return { agentId, customTitle };
|
|
3311
3724
|
}
|
|
@@ -3749,6 +4162,9 @@ class AgentManager extends EventEmitter {
|
|
|
3749
4162
|
const startsFreshCodexSession = provider === 'codex'
|
|
3750
4163
|
&& !hasResumableSession
|
|
3751
4164
|
&& (agent.providerSessionTemporary === true || !String(agent.providerSessionId || '').trim());
|
|
4165
|
+
if (startsFreshCodexSession && agent.terminalInputReceived === true) {
|
|
4166
|
+
return { error: 'Permission changes require a resumable provider session. Try again after the session id is available.' };
|
|
4167
|
+
}
|
|
3752
4168
|
if (!hasResumableSession && !startsFreshCodexSession) {
|
|
3753
4169
|
return { error: 'Permission changes require a resumable provider session. Try again after the session id is available.' };
|
|
3754
4170
|
}
|
|
@@ -4125,6 +4541,12 @@ class AgentManager extends EventEmitter {
|
|
|
4125
4541
|
if (!['same-worktree', 'new-worktree'].includes(mode)) {
|
|
4126
4542
|
return { error: 'Unsupported fork mode' };
|
|
4127
4543
|
}
|
|
4544
|
+
if (agent.providerSessionProvider === 'codex' && agent.providerSessionTemporary === true) {
|
|
4545
|
+
await this.providerSessionService.resolveTemporaryCodex(agentId, { force: true });
|
|
4546
|
+
if (agent.providerSessionTemporary === true) {
|
|
4547
|
+
return { error: 'Fork requires a resumable Codex session. Try again after the session id is available.' };
|
|
4548
|
+
}
|
|
4549
|
+
}
|
|
4128
4550
|
|
|
4129
4551
|
const sourceWorkspace = effectiveAgentWorkspaceRoot(agent);
|
|
4130
4552
|
const resumedSession = agent.providerSessionProvider
|
|
@@ -4274,6 +4696,7 @@ class AgentManager extends EventEmitter {
|
|
|
4274
4696
|
}
|
|
4275
4697
|
|
|
4276
4698
|
const removedMainPageSessionKeys = this.removeMainPageProviderSessionsForAgents([agent]);
|
|
4699
|
+
this.ensurePersistentAgentSession(agent, { visibleOnMainPage: false });
|
|
4277
4700
|
await this.killAgent(agentId, {
|
|
4278
4701
|
reason: 'manual-archive',
|
|
4279
4702
|
recordHistory: !isEphemeralShellAgent(agent),
|