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.
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 +396 -47
  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-6yWdwzj8.js +210 -0
  27. package/dist/assets/code-dark-5esj0bRu.css +1 -0
  28. package/dist/assets/{index-iKBvPjWS.js → index-C8ZFKye1.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,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 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 || '');
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 (candidates.length > 0) {
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 ${candidates.length} Terminal session(s) after native PTY runtime rotation`,
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 candidates) {
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 => record && record.archived !== true && runtimeKind(record) === 'acp')
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
- if (providerSessionPlan.source === 'resume-source' && hasResumeSource && !hasResumeArg(launch.args)) {
2361
- providerSessionPlan = buildAgentProviderSessionPlan({
2362
- command,
2363
- program,
2364
- args: launch.args,
2365
- source: 'ui',
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. OpenCode is the important case here because its
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
- if (callback) callback(null, error.message);
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),