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.
Files changed (37) hide show
  1. package/README.md +2 -2
  2. package/README.zh_cn.md +2 -2
  3. package/THIRD_PARTY_NOTICES.md +16 -0
  4. package/backend/acp-runtime.js +221 -10
  5. package/backend/agent-manager.js +477 -54
  6. package/backend/agent-provider-session.js +15 -1
  7. package/backend/codex-rollout-follower.js +83 -0
  8. package/backend/codex-session-archive.js +12 -1
  9. package/backend/codex-session-history.js +94 -0
  10. package/backend/config-manager.js +3 -3
  11. package/backend/farming-app-cli.js +8 -0
  12. package/backend/native-pty-host-client.js +59 -2
  13. package/backend/provider-adapters.js +117 -36
  14. package/backend/provider-session-service.js +138 -34
  15. package/backend/server.js +5 -1
  16. package/backend/storage-layout.js +5 -0
  17. package/backend/usage-history-client.js +305 -0
  18. package/backend/usage-history-scanner.generated.js +2250 -0
  19. package/backend/usage-history-scanner.ts +2718 -0
  20. package/backend/usage-history-smoke.js +64 -0
  21. package/backend/usage-history-worker.js +34 -0
  22. package/backend/usage-monitor.js +188 -588
  23. package/backend/vendor/usage-parsers/CC_STATISTICS_LICENSE +21 -0
  24. package/backend/vendor/usage-parsers/CODEXBAR_LICENSE +21 -0
  25. package/backend/vendor/usage-parsers/VENDOR.json +45 -0
  26. package/dist/assets/App-DTAQilnX.js +210 -0
  27. package/dist/assets/code-dark-5esj0bRu.css +1 -0
  28. package/dist/assets/{index-iKBvPjWS.js → index-fp8VhOes.js} +2 -2
  29. package/dist/assets/main-DSO3i_Gs.css +1 -0
  30. package/dist/index.html +1 -1
  31. package/frontend/skins/crt/app.js +9 -3
  32. package/package.json +12 -8
  33. package/shared/backend-connection-status.d.ts +25 -0
  34. package/shared/backend-connection-status.js +40 -0
  35. package/dist/assets/App-y6p2yRar.js +0 -206
  36. package/dist/assets/code-dark-DKBGxoPX.css +0 -1
  37. package/dist/assets/main-CdkLOXYl.css +0 -1
@@ -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 shouldRecoverEngineSession(metadata) {
382
- if (!metadata) return false;
383
- // Main Agent shells are a product session, not an ephemeral scratch shell.
384
- // Keep them attached to the persistent native PTY host across server restarts.
385
- if (metadata.wantsMain === true) return true;
386
- if (metadata.category === 'shell') return false;
387
- return !isShellProgram(metadata.forkCommand || metadata.command || '');
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 (candidates.length > 0) {
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 ${candidates.length} Terminal session(s) after native PTY runtime rotation`,
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 candidates) {
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 => record && record.archived !== true && runtimeKind(record) === 'acp')
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
- if (providerSessionPlan.source === 'resume-source' && hasResumeSource && !hasResumeArg(launch.args)) {
2361
- providerSessionPlan = buildAgentProviderSessionPlan({
2362
- command,
2363
- program,
2364
- args: launch.args,
2365
- source: 'ui',
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. OpenCode is the important case here because its
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 engineLaunch = {
2722
- command: spawnProgram,
2723
- args,
2724
- cwd: workspace,
2725
- category: resolution.spec ? resolution.spec.category : 'shell',
2726
- reviveState: options.reviveTerminalState || null,
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
- await this.createAgentEngineSession(agentRecord, resolution.engine, engineLaunch);
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
- if (callback) callback(null, error.message);
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),