farming-code 2.2.17 → 2.2.19
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/backend/agent-manager.js +226 -49
- package/backend/bundle-update-helper.js +109 -0
- package/backend/config-manager.js +110 -4
- package/backend/farming-session-store.js +62 -2
- package/backend/server.js +364 -58
- package/backend/update-service.js +46 -23
- package/dist/assets/App-CbXYBccJ.js +210 -0
- package/dist/assets/{FileEditorPane-DNy3LAqS.js → FileEditorPane-CJs6kfZO.js} +1 -1
- package/dist/assets/{IconGlyphs-BsUfqpKN.js → IconGlyphs-CZzDr5o2.js} +1 -1
- package/dist/assets/{ProjectFilesSection-BhDEUVHA.js → ProjectFilesSection-DWCV07lA.js} +1 -1
- package/dist/assets/{ReviewPage-ChtByBe3.js → ReviewPage-DBKNOWsJ.js} +1 -1
- package/dist/assets/code-dark-qIBiLBWV.css +1 -0
- package/dist/assets/{index-C8ZFKye1.js → index-DQdtPfCk.js} +2 -2
- package/dist/assets/main-yzZEfgft.css +1 -0
- package/dist/index.html +1 -1
- package/frontend/skins/crt/app.js +91 -9
- package/frontend/skins/crt/index.html +16 -28
- package/package.json +1 -1
- package/dist/assets/App-6yWdwzj8.js +0 -210
- package/dist/assets/code-dark-5esj0bRu.css +0 -1
- package/dist/assets/main-DSO3i_Gs.css +0 -1
package/backend/agent-manager.js
CHANGED
|
@@ -79,6 +79,9 @@ const AGENT_DISCOVERY_CACHE_MAX_AGE_MS = 3_000;
|
|
|
79
79
|
const UNCERTAIN_TERMINAL_STOP_TIMEOUT_MS = 5_000;
|
|
80
80
|
const TERMINAL_STOP_STATE_READ_TIMEOUT_MS = 1_000;
|
|
81
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;
|
|
82
85
|
const SHELL_PROMPT_ENV_KEYS = [
|
|
83
86
|
'PS1',
|
|
84
87
|
'PS2',
|
|
@@ -100,6 +103,15 @@ function withBoundedWait(promise, timeoutMs, label) {
|
|
|
100
103
|
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
101
104
|
}
|
|
102
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
|
+
|
|
103
115
|
async function deletePrecreatedProviderSession(options = {}) {
|
|
104
116
|
const provider = String(options.provider || '').trim().toLowerCase();
|
|
105
117
|
const sessionId = String(options.sessionId || '').trim();
|
|
@@ -591,6 +603,8 @@ class AgentManager extends EventEmitter {
|
|
|
591
603
|
this.resizeDrains = new Map();
|
|
592
604
|
this.inputQueues = new Map();
|
|
593
605
|
this.codexTerminalProfileQueues = new Map();
|
|
606
|
+
this.codexTerminalStartQueues = new Map();
|
|
607
|
+
this.codexTerminalStartOutput = new Map();
|
|
594
608
|
this.agentWorktreeResolveGeneration = new Map();
|
|
595
609
|
this.permissionRestartInFlight = new Map();
|
|
596
610
|
this.runtimeRestartInFlight = new Map();
|
|
@@ -757,6 +771,13 @@ class AgentManager extends EventEmitter {
|
|
|
757
771
|
|
|
758
772
|
this.reviveAgentRuntime(agent);
|
|
759
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
|
+
}
|
|
760
781
|
if (agent.providerSessionProvider === 'codex' && agent.providerSessionTemporary === true) {
|
|
761
782
|
void this.providerSessionService.resolveTemporaryCodex(sessionId);
|
|
762
783
|
}
|
|
@@ -1393,6 +1414,16 @@ class AgentManager extends EventEmitter {
|
|
|
1393
1414
|
projectOrder: finiteOrder(record.projectOrder),
|
|
1394
1415
|
pinnedOrder: finiteOrder(record.pinnedOrder),
|
|
1395
1416
|
customTitle: record.customTitle || '',
|
|
1417
|
+
pinned: record.pinned === true,
|
|
1418
|
+
attentionSeq: finiteNonNegativeInteger(record.attentionSeq),
|
|
1419
|
+
readAttentionSeq: finiteNonNegativeInteger(record.readAttentionSeq),
|
|
1420
|
+
attentionUpdatedAt: finiteNumberOrNull(record.attentionUpdatedAt),
|
|
1421
|
+
readAttentionAt: finiteNumberOrNull(record.readAttentionAt),
|
|
1422
|
+
attentionReason: record.attentionReason || '',
|
|
1423
|
+
attentionOutputEpoch: record.attentionOutputEpoch || '',
|
|
1424
|
+
attentionOutputSeq: finiteNumberOrNull(record.attentionOutputSeq),
|
|
1425
|
+
readOutputEpoch: record.readOutputEpoch || '',
|
|
1426
|
+
readOutputSeq: finiteNumberOrNull(record.readOutputSeq),
|
|
1396
1427
|
persistentSessionId: record.id || '',
|
|
1397
1428
|
runtimeAgentId: record.runtimeAgentId || '',
|
|
1398
1429
|
reviveTerminalState: record.serializedState || null,
|
|
@@ -1440,7 +1471,10 @@ class AgentManager extends EventEmitter {
|
|
|
1440
1471
|
replacement.attentionUpdatedAt = finiteNumberOrNull(record.attentionUpdatedAt);
|
|
1441
1472
|
replacement.readAttentionAt = finiteNumberOrNull(record.readAttentionAt);
|
|
1442
1473
|
replacement.attentionReason = record.attentionReason || '';
|
|
1474
|
+
replacement.attentionOutputEpoch = record.attentionOutputEpoch || '';
|
|
1443
1475
|
replacement.attentionOutputSeq = finiteNumberOrNull(record.attentionOutputSeq);
|
|
1476
|
+
replacement.readOutputEpoch = record.readOutputEpoch || '';
|
|
1477
|
+
replacement.readOutputSeq = finiteNumberOrNull(record.readOutputSeq);
|
|
1444
1478
|
replacement.unread = agentAttentionUnread(replacement);
|
|
1445
1479
|
this.ensurePersistentAgentSession(replacement);
|
|
1446
1480
|
if (sessionKey) liveProviderSessions.add(sessionKey);
|
|
@@ -1716,6 +1750,7 @@ class AgentManager extends EventEmitter {
|
|
|
1716
1750
|
if (!agent || !this.configManager || typeof this.configManager.ensureAgentSessionRecord !== 'function') {
|
|
1717
1751
|
return '';
|
|
1718
1752
|
}
|
|
1753
|
+
const previousPersistentSessionId = agent.persistentSessionId || '';
|
|
1719
1754
|
const sessionOptions = agent.providerSessionKey
|
|
1720
1755
|
? this.acpSessionOptionsByKey.get(agent.providerSessionKey)
|
|
1721
1756
|
: null;
|
|
@@ -1726,9 +1761,39 @@ class AgentManager extends EventEmitter {
|
|
|
1726
1761
|
} : {}),
|
|
1727
1762
|
...patch,
|
|
1728
1763
|
});
|
|
1729
|
-
if (persistentSessionId
|
|
1764
|
+
if (persistentSessionId) {
|
|
1730
1765
|
agent.persistentSessionId = persistentSessionId;
|
|
1731
1766
|
}
|
|
1767
|
+
if (
|
|
1768
|
+
persistentSessionId
|
|
1769
|
+
&& previousPersistentSessionId
|
|
1770
|
+
&& persistentSessionId !== previousPersistentSessionId
|
|
1771
|
+
&& agent.providerSessionKey
|
|
1772
|
+
&& typeof this.configManager.getAgentSessionRecordForProviderSessionKey === 'function'
|
|
1773
|
+
) {
|
|
1774
|
+
const record = this.configManager.getAgentSessionRecordForProviderSessionKey(agent.providerSessionKey);
|
|
1775
|
+
if (record) {
|
|
1776
|
+
agent.projectWorkspace = record.projectWorkspace || agent.projectWorkspace || '';
|
|
1777
|
+
agent.task = typeof record.task === 'string' ? record.task : (agent.task || '');
|
|
1778
|
+
agent.workflowTemplate = typeof record.workflowTemplate === 'string'
|
|
1779
|
+
? record.workflowTemplate
|
|
1780
|
+
: (agent.workflowTemplate || '');
|
|
1781
|
+
agent.customTitle = typeof record.customTitle === 'string' ? record.customTitle : '';
|
|
1782
|
+
agent.pinned = record.pinned === true;
|
|
1783
|
+
agent.projectOrder = finiteOrder(record.projectOrder);
|
|
1784
|
+
agent.pinnedOrder = finiteOrder(record.pinnedOrder);
|
|
1785
|
+
agent.attentionSeq = finiteNonNegativeInteger(record.attentionSeq);
|
|
1786
|
+
agent.readAttentionSeq = finiteNonNegativeInteger(record.readAttentionSeq);
|
|
1787
|
+
agent.attentionUpdatedAt = finiteNumberOrNull(record.attentionUpdatedAt);
|
|
1788
|
+
agent.readAttentionAt = finiteNumberOrNull(record.readAttentionAt);
|
|
1789
|
+
agent.attentionReason = record.attentionReason || '';
|
|
1790
|
+
agent.attentionOutputEpoch = record.attentionOutputEpoch || '';
|
|
1791
|
+
agent.attentionOutputSeq = finiteNumberOrNull(record.attentionOutputSeq);
|
|
1792
|
+
agent.readOutputEpoch = record.readOutputEpoch || '';
|
|
1793
|
+
agent.readOutputSeq = finiteNumberOrNull(record.readOutputSeq);
|
|
1794
|
+
agent.unread = agentAttentionUnread(agent);
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1732
1797
|
return persistentSessionId;
|
|
1733
1798
|
}
|
|
1734
1799
|
|
|
@@ -2264,6 +2329,8 @@ class AgentManager extends EventEmitter {
|
|
|
2264
2329
|
this.resizeDrains.clear();
|
|
2265
2330
|
this.inputQueues.clear();
|
|
2266
2331
|
this.codexTerminalProfileQueues.clear();
|
|
2332
|
+
this.codexTerminalStartQueues.clear();
|
|
2333
|
+
this.codexTerminalStartOutput.clear();
|
|
2267
2334
|
this.acpSessionOptionsByKey.clear();
|
|
2268
2335
|
if (this.jsonCliRuntime) {
|
|
2269
2336
|
for (const agentId of this.agents.keys()) this.jsonCliRuntime.unregisterAgent(agentId);
|
|
@@ -2373,6 +2440,37 @@ class AgentManager extends EventEmitter {
|
|
|
2373
2440
|
throw new Error(`Terminal ${agentId} did not reach an exited state after kill`);
|
|
2374
2441
|
}
|
|
2375
2442
|
|
|
2443
|
+
async waitForCodexTerminalStart(agentId) {
|
|
2444
|
+
const deadline = Date.now() + CODEX_TERMINAL_START_READY_TIMEOUT_MS;
|
|
2445
|
+
while (Date.now() <= deadline) {
|
|
2446
|
+
const agent = this.agents.get(agentId);
|
|
2447
|
+
if (!agent) throw new Error(`Codex Terminal ${agentId} disappeared during startup`);
|
|
2448
|
+
if (['dead', 'stopped'].includes(agent.status) || agent.engineStatus === 'exited') {
|
|
2449
|
+
const detail = trimSessionOutput(agent.previewText || agent.output || '').trim();
|
|
2450
|
+
throw new Error(detail || `Codex Terminal ${agentId} exited during startup`);
|
|
2451
|
+
}
|
|
2452
|
+
if ((this.codexTerminalStartOutput.get(agentId) || '').includes('\u001b')) {
|
|
2453
|
+
return;
|
|
2454
|
+
}
|
|
2455
|
+
await new Promise(resolve => setTimeout(resolve, CODEX_TERMINAL_START_READY_POLL_MS));
|
|
2456
|
+
}
|
|
2457
|
+
throw new Error(`Codex Terminal ${agentId} did not become ready within ${CODEX_TERMINAL_START_READY_TIMEOUT_MS}ms`);
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
async enqueueCodexTerminalStart(providerHomeKey, operation) {
|
|
2461
|
+
const key = canonicalProviderHomePath(providerHomeKey || '.');
|
|
2462
|
+
const previous = this.codexTerminalStartQueues.get(key) || Promise.resolve();
|
|
2463
|
+
const next = previous.catch(() => {}).then(operation);
|
|
2464
|
+
this.codexTerminalStartQueues.set(key, next);
|
|
2465
|
+
try {
|
|
2466
|
+
return await next;
|
|
2467
|
+
} finally {
|
|
2468
|
+
if (this.codexTerminalStartQueues.get(key) === next) {
|
|
2469
|
+
this.codexTerminalStartQueues.delete(key);
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2376
2474
|
async startAgent(command, customWorkspace, callback, options = {}) {
|
|
2377
2475
|
if (options.wantsMain !== false && options.skipRecoveryWait !== true) {
|
|
2378
2476
|
await this.whenRecovered();
|
|
@@ -2774,19 +2872,19 @@ class AgentManager extends EventEmitter {
|
|
|
2774
2872
|
shellLastCommandStartedAt: null,
|
|
2775
2873
|
shellLastCommandFinishedAt: null,
|
|
2776
2874
|
shellLastCommandDurationMs: null,
|
|
2777
|
-
pinned:
|
|
2875
|
+
pinned: options.pinned === true,
|
|
2778
2876
|
projectOrder: finiteOrder(options.projectOrder),
|
|
2779
2877
|
pinnedOrder: finiteOrder(options.pinnedOrder),
|
|
2780
|
-
attentionSeq:
|
|
2781
|
-
readAttentionSeq:
|
|
2782
|
-
attentionUpdatedAt:
|
|
2783
|
-
readAttentionAt:
|
|
2784
|
-
attentionReason: '',
|
|
2785
|
-
attentionOutputEpoch: '',
|
|
2786
|
-
attentionOutputSeq:
|
|
2787
|
-
readOutputEpoch: '',
|
|
2788
|
-
readOutputSeq:
|
|
2789
|
-
unread:
|
|
2878
|
+
attentionSeq: finiteNonNegativeInteger(options.attentionSeq),
|
|
2879
|
+
readAttentionSeq: finiteNonNegativeInteger(options.readAttentionSeq),
|
|
2880
|
+
attentionUpdatedAt: finiteNumberOrNull(options.attentionUpdatedAt),
|
|
2881
|
+
readAttentionAt: finiteNumberOrNull(options.readAttentionAt),
|
|
2882
|
+
attentionReason: typeof options.attentionReason === 'string' ? options.attentionReason : '',
|
|
2883
|
+
attentionOutputEpoch: typeof options.attentionOutputEpoch === 'string' ? options.attentionOutputEpoch : '',
|
|
2884
|
+
attentionOutputSeq: finiteNumberOrNull(options.attentionOutputSeq),
|
|
2885
|
+
readOutputEpoch: typeof options.readOutputEpoch === 'string' ? options.readOutputEpoch : '',
|
|
2886
|
+
readOutputSeq: finiteNumberOrNull(options.readOutputSeq),
|
|
2887
|
+
unread: finiteNonNegativeInteger(options.attentionSeq) > finiteNonNegativeInteger(options.readAttentionSeq),
|
|
2790
2888
|
archived: false,
|
|
2791
2889
|
archivedAt: null,
|
|
2792
2890
|
canForkNewWorktree: this.canCreateForkWorktree(projectWorkspace || workspace),
|
|
@@ -3016,18 +3114,41 @@ class AgentManager extends EventEmitter {
|
|
|
3016
3114
|
}
|
|
3017
3115
|
|
|
3018
3116
|
if (!useJsonCli && !useAcp) {
|
|
3019
|
-
const
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3117
|
+
const serializeCodexStartup = agentRecord.providerSessionProvider === 'codex'
|
|
3118
|
+
&& resolution.engineName === 'native';
|
|
3119
|
+
const startTerminal = async () => {
|
|
3120
|
+
const engineLaunch = {
|
|
3121
|
+
command: spawnProgram,
|
|
3122
|
+
args,
|
|
3123
|
+
cwd: workspace,
|
|
3124
|
+
category: resolution.spec ? resolution.spec.category : 'shell',
|
|
3125
|
+
reviveState: options.reviveTerminalState || null,
|
|
3126
|
+
};
|
|
3127
|
+
if (serializeCodexStartup) this.codexTerminalStartOutput.set(agentId, '');
|
|
3128
|
+
try {
|
|
3129
|
+
await this.createAgentEngineSession(agentRecord, resolution.engine, engineLaunch);
|
|
3130
|
+
if (serializeCodexStartup) {
|
|
3131
|
+
await this.waitForCodexTerminalStart(agentId);
|
|
3132
|
+
}
|
|
3133
|
+
} finally {
|
|
3134
|
+
if (serializeCodexStartup) this.codexTerminalStartOutput.delete(agentId);
|
|
3135
|
+
}
|
|
3025
3136
|
};
|
|
3026
|
-
|
|
3137
|
+
if (serializeCodexStartup) {
|
|
3138
|
+
await this.enqueueCodexTerminalStart(
|
|
3139
|
+
providerHomePath || resolvedProviderHomeId || 'default',
|
|
3140
|
+
startTerminal
|
|
3141
|
+
);
|
|
3142
|
+
} else {
|
|
3143
|
+
await startTerminal();
|
|
3144
|
+
}
|
|
3027
3145
|
}
|
|
3028
3146
|
agentRecord.persistentSessionId = this.ensurePersistentAgentSession(agentRecord, {
|
|
3029
3147
|
visibleOnMainPage: true,
|
|
3030
3148
|
archived: false,
|
|
3149
|
+
...(options.customTitleExplicit === true
|
|
3150
|
+
? { customTitle: agentRecord.customTitle }
|
|
3151
|
+
: {}),
|
|
3031
3152
|
});
|
|
3032
3153
|
|
|
3033
3154
|
const agent = this.agents.get(agentId);
|
|
@@ -3643,7 +3764,7 @@ class AgentManager extends EventEmitter {
|
|
|
3643
3764
|
|
|
3644
3765
|
const customTitle = String(title || '').trim().slice(0, 80);
|
|
3645
3766
|
agent.customTitle = customTitle;
|
|
3646
|
-
this.ensurePersistentAgentSession(agent);
|
|
3767
|
+
this.ensurePersistentAgentSession(agent, { customTitle });
|
|
3647
3768
|
this.updateEngineProviderSessionMetadata(agent);
|
|
3648
3769
|
this.emit('update');
|
|
3649
3770
|
return { agentId, customTitle };
|
|
@@ -4305,24 +4426,33 @@ class AgentManager extends EventEmitter {
|
|
|
4305
4426
|
}
|
|
4306
4427
|
|
|
4307
4428
|
async rollbackPermanentWorktree(created) {
|
|
4308
|
-
if (!created?.workspace || !created?.sourceWorkspace)
|
|
4429
|
+
if (!created?.workspace || !created?.sourceWorkspace) {
|
|
4430
|
+
return { rolledBack: true };
|
|
4431
|
+
}
|
|
4432
|
+
const errors = [];
|
|
4309
4433
|
try {
|
|
4310
4434
|
await execFileAsync('git', ['-C', created.sourceWorkspace, 'worktree', 'remove', '--force', created.workspace], {
|
|
4311
4435
|
timeout: 60000,
|
|
4312
4436
|
maxBuffer: 1024 * 1024 * 4,
|
|
4313
4437
|
});
|
|
4314
|
-
} catch {
|
|
4315
|
-
|
|
4438
|
+
} catch (error) {
|
|
4439
|
+
const message = error && error.stderr ? String(error.stderr).trim() : '';
|
|
4440
|
+
errors.push(message || 'Failed to remove the created worktree');
|
|
4316
4441
|
}
|
|
4317
|
-
if (
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4442
|
+
if (created.branch) {
|
|
4443
|
+
try {
|
|
4444
|
+
await execFileAsync('git', ['-C', created.sourceWorkspace, 'branch', '-D', created.branch], {
|
|
4445
|
+
timeout: 30000,
|
|
4446
|
+
maxBuffer: 1024 * 1024 * 4,
|
|
4447
|
+
});
|
|
4448
|
+
} catch (error) {
|
|
4449
|
+
const message = error && error.stderr ? String(error.stderr).trim() : '';
|
|
4450
|
+
errors.push(message || 'Failed to delete the created worktree branch');
|
|
4451
|
+
}
|
|
4325
4452
|
}
|
|
4453
|
+
return errors.length > 0
|
|
4454
|
+
? { rolledBack: false, error: errors.join('; ') }
|
|
4455
|
+
: { rolledBack: true };
|
|
4326
4456
|
}
|
|
4327
4457
|
|
|
4328
4458
|
async inspectForkWorktreeProject(workspace) {
|
|
@@ -4424,13 +4554,6 @@ class AgentManager extends EventEmitter {
|
|
|
4424
4554
|
}
|
|
4425
4555
|
|
|
4426
4556
|
const projectAgents = this.agentsForProjectWorkspace(inspected.workspace);
|
|
4427
|
-
const removedMainPageSessionKeys = this.removeMainPageProviderSessionsForAgents(projectAgents);
|
|
4428
|
-
const archivedAgentIds = [];
|
|
4429
|
-
for (const agent of projectAgents) {
|
|
4430
|
-
const result = await this.archiveAgent(agent.id);
|
|
4431
|
-
if (result && !result.error) archivedAgentIds.push(agent.id);
|
|
4432
|
-
}
|
|
4433
|
-
|
|
4434
4557
|
const args = ['-C', inspected.workspace, 'worktree', 'remove'];
|
|
4435
4558
|
if (options.force === true) args.push('--force');
|
|
4436
4559
|
args.push(inspected.workspace);
|
|
@@ -4444,18 +4567,31 @@ class AgentManager extends EventEmitter {
|
|
|
4444
4567
|
const message = error && error.stderr ? String(error.stderr).trim() : '';
|
|
4445
4568
|
return {
|
|
4446
4569
|
...inspected,
|
|
4447
|
-
archivedAgentIds,
|
|
4448
|
-
removedMainPageSessionKeys,
|
|
4449
4570
|
error: message || 'Failed to delete git worktree',
|
|
4450
4571
|
};
|
|
4451
4572
|
}
|
|
4452
4573
|
|
|
4574
|
+
const archivedAgentIds = [];
|
|
4575
|
+
const removedMainPageSessionKeys = [];
|
|
4576
|
+
const cleanupErrors = [];
|
|
4577
|
+
for (const agent of projectAgents) {
|
|
4578
|
+
const result = await this.archiveAgent(agent.id, { requireEngineExit: true });
|
|
4579
|
+
if (result && !result.error) {
|
|
4580
|
+
archivedAgentIds.push(agent.id);
|
|
4581
|
+
removedMainPageSessionKeys.push(...(result.removedMainPageSessionKeys || []));
|
|
4582
|
+
} else {
|
|
4583
|
+
cleanupErrors.push(`${agent.id}: ${result?.error || 'Failed to archive Agent'}`);
|
|
4584
|
+
}
|
|
4585
|
+
}
|
|
4453
4586
|
return {
|
|
4454
4587
|
workspace: inspected.workspace,
|
|
4455
4588
|
deleted: true,
|
|
4456
4589
|
forced: options.force === true,
|
|
4457
4590
|
archivedAgentIds,
|
|
4458
|
-
removedMainPageSessionKeys,
|
|
4591
|
+
removedMainPageSessionKeys: Array.from(new Set(removedMainPageSessionKeys)),
|
|
4592
|
+
...(cleanupErrors.length > 0 ? {
|
|
4593
|
+
cleanupError: cleanupErrors.join('; '),
|
|
4594
|
+
} : {}),
|
|
4459
4595
|
};
|
|
4460
4596
|
}
|
|
4461
4597
|
|
|
@@ -4612,7 +4748,7 @@ class AgentManager extends EventEmitter {
|
|
|
4612
4748
|
}
|
|
4613
4749
|
}
|
|
4614
4750
|
|
|
4615
|
-
async archiveAgent(agentId) {
|
|
4751
|
+
async archiveAgent(agentId, options = {}) {
|
|
4616
4752
|
const agent = this.agents.get(agentId);
|
|
4617
4753
|
if (!agent) {
|
|
4618
4754
|
return { error: 'Agent not found' };
|
|
@@ -4621,13 +4757,26 @@ class AgentManager extends EventEmitter {
|
|
|
4621
4757
|
return { error: 'Main Agent cannot be archived' };
|
|
4622
4758
|
}
|
|
4623
4759
|
|
|
4760
|
+
if (options.requireEngineExit !== true) {
|
|
4761
|
+
const removedMainPageSessionKeys = this.removeMainPageProviderSessionsForAgents([agent]);
|
|
4762
|
+
this.ensurePersistentAgentSession(agent, { visibleOnMainPage: false });
|
|
4763
|
+
await this.killAgent(agentId, {
|
|
4764
|
+
reason: options.reason || 'manual-archive',
|
|
4765
|
+
recordHistory: options.recordHistory !== false && !isEphemeralShellAgent(agent),
|
|
4766
|
+
});
|
|
4767
|
+
if (options.scheduleProviderArchive !== false) this.scheduleCodexSessionArchive(agent);
|
|
4768
|
+
return { agentId, archived: true, removed: true, removedMainPageSessionKeys };
|
|
4769
|
+
}
|
|
4770
|
+
|
|
4771
|
+
const killResult = await this.killAgent(agentId, {
|
|
4772
|
+
reason: options.reason || 'manual-archive',
|
|
4773
|
+
recordHistory: options.recordHistory !== false && !isEphemeralShellAgent(agent),
|
|
4774
|
+
requireEngineExit: true,
|
|
4775
|
+
});
|
|
4776
|
+
if (killResult?.error) return killResult;
|
|
4624
4777
|
const removedMainPageSessionKeys = this.removeMainPageProviderSessionsForAgents([agent]);
|
|
4625
4778
|
this.ensurePersistentAgentSession(agent, { visibleOnMainPage: false });
|
|
4626
|
-
|
|
4627
|
-
reason: 'manual-archive',
|
|
4628
|
-
recordHistory: !isEphemeralShellAgent(agent),
|
|
4629
|
-
});
|
|
4630
|
-
this.scheduleCodexSessionArchive(agent);
|
|
4779
|
+
if (options.scheduleProviderArchive !== false) this.scheduleCodexSessionArchive(agent);
|
|
4631
4780
|
return { agentId, archived: true, removed: true, removedMainPageSessionKeys };
|
|
4632
4781
|
}
|
|
4633
4782
|
|
|
@@ -4662,15 +4811,42 @@ class AgentManager extends EventEmitter {
|
|
|
4662
4811
|
|
|
4663
4812
|
async killAgent(agentId, options = {}) {
|
|
4664
4813
|
const agent = this.agents.get(agentId);
|
|
4665
|
-
if (!agent) return;
|
|
4814
|
+
if (!agent) return { agentId, killed: true, missing: true };
|
|
4666
4815
|
|
|
4816
|
+
const engine = this.engineBridge.getEngine(agent.engineName);
|
|
4817
|
+
if (options.requireEngineExit === true && !engine) {
|
|
4818
|
+
return { agentId, error: 'Agent runtime is unavailable; process exit cannot be verified' };
|
|
4819
|
+
}
|
|
4667
4820
|
try {
|
|
4668
|
-
const engine = this.engineBridge.getEngine(agent.engineName);
|
|
4669
4821
|
if (engine) {
|
|
4670
4822
|
await engine.killSession(agentId);
|
|
4671
4823
|
}
|
|
4672
4824
|
} catch (error) {
|
|
4673
4825
|
console.error('Failed to kill agent:', error);
|
|
4826
|
+
if (options.requireEngineExit === true) {
|
|
4827
|
+
return { agentId, error: error.message || 'Failed to stop Agent runtime' };
|
|
4828
|
+
}
|
|
4829
|
+
}
|
|
4830
|
+
|
|
4831
|
+
if (options.requireEngineExit === true && engine) {
|
|
4832
|
+
const deadline = Date.now() + 3000;
|
|
4833
|
+
let lastState = null;
|
|
4834
|
+
while (Date.now() < deadline) {
|
|
4835
|
+
try {
|
|
4836
|
+
lastState = await engine.getSessionState(agentId);
|
|
4837
|
+
} catch (error) {
|
|
4838
|
+
if (isSessionNotAvailableError(error)) {
|
|
4839
|
+
lastState = null;
|
|
4840
|
+
break;
|
|
4841
|
+
}
|
|
4842
|
+
return { agentId, error: error.message || 'Failed to verify Agent process exit' };
|
|
4843
|
+
}
|
|
4844
|
+
if (!isLiveEngineSessionState(lastState)) break;
|
|
4845
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
4846
|
+
}
|
|
4847
|
+
if (isLiveEngineSessionState(lastState)) {
|
|
4848
|
+
return { agentId, error: 'Agent process did not exit within 3 seconds' };
|
|
4849
|
+
}
|
|
4674
4850
|
}
|
|
4675
4851
|
|
|
4676
4852
|
if (options.recordHistory !== false && !isEphemeralShellAgent(agent)) {
|
|
@@ -4697,6 +4873,7 @@ class AgentManager extends EventEmitter {
|
|
|
4697
4873
|
if (options.emitUpdate !== false) {
|
|
4698
4874
|
this.emit('update');
|
|
4699
4875
|
}
|
|
4876
|
+
return { agentId, killed: true };
|
|
4700
4877
|
}
|
|
4701
4878
|
|
|
4702
4879
|
async getAgentSessionText(agentId) {
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
|
+
|
|
5
|
+
function writeJsonAtomic(filePath, value) {
|
|
6
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
7
|
+
const temporaryPath = `${filePath}.${process.pid}.tmp`;
|
|
8
|
+
fs.writeFileSync(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
9
|
+
fs.renameSync(temporaryPath, filePath);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function appendLog(logPath, message) {
|
|
13
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
14
|
+
fs.appendFileSync(logPath, `[${new Date().toISOString()}] ${message}\n`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function validatePayload(payload) {
|
|
18
|
+
if (!payload || typeof payload !== 'object') throw new Error('Missing bundle update payload');
|
|
19
|
+
for (const key of ['stateFile', 'logPath', 'releaseDir', 'installer']) {
|
|
20
|
+
if (!path.isAbsolute(String(payload[key] || ''))) throw new Error(`Invalid bundle update ${key}`);
|
|
21
|
+
}
|
|
22
|
+
if (!/^[0-9A-Za-z.+-]+$/.test(String(payload.version || ''))) {
|
|
23
|
+
throw new Error('Invalid bundle update version');
|
|
24
|
+
}
|
|
25
|
+
return payload;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function stateFor(payload, phase, extra = {}) {
|
|
29
|
+
return {
|
|
30
|
+
method: payload.method,
|
|
31
|
+
targetMethod: payload.targetMethod,
|
|
32
|
+
phase,
|
|
33
|
+
version: payload.version,
|
|
34
|
+
previousVersion: payload.previousVersion,
|
|
35
|
+
startedAt: payload.startedAt,
|
|
36
|
+
logPath: payload.logPath,
|
|
37
|
+
...extra,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function commandEnvironment() {
|
|
42
|
+
const env = { ...process.env };
|
|
43
|
+
delete env.FARMING_BUNDLE_UPDATE_PAYLOAD;
|
|
44
|
+
delete env.FARMING_RUN_SERVER;
|
|
45
|
+
delete env.FARMING_RUN_NATIVE_PTY_HOST;
|
|
46
|
+
return env;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function runInstaller(payload) {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const logFd = fs.openSync(payload.logPath, 'a');
|
|
52
|
+
const child = spawn('bash', [payload.installer, 'install'], {
|
|
53
|
+
cwd: payload.releaseDir,
|
|
54
|
+
env: commandEnvironment(),
|
|
55
|
+
stdio: ['ignore', logFd, logFd],
|
|
56
|
+
});
|
|
57
|
+
child.once('error', error => {
|
|
58
|
+
fs.closeSync(logFd);
|
|
59
|
+
reject(error);
|
|
60
|
+
});
|
|
61
|
+
child.once('exit', (code, signal) => {
|
|
62
|
+
fs.closeSync(logFd);
|
|
63
|
+
if (code === 0) {
|
|
64
|
+
resolve();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
reject(new Error(`bundle installer exited with ${signal || code}`));
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function runBundleUpdate(rawPayload) {
|
|
73
|
+
const payload = validatePayload(rawPayload);
|
|
74
|
+
writeJsonAtomic(payload.stateFile, stateFor(payload, 'installing'));
|
|
75
|
+
await new Promise(resolve => setTimeout(resolve, 1_000));
|
|
76
|
+
try {
|
|
77
|
+
await runInstaller(payload);
|
|
78
|
+
writeJsonAtomic(payload.stateFile, stateFor(payload, 'succeeded', {
|
|
79
|
+
completedAt: new Date().toISOString(),
|
|
80
|
+
}));
|
|
81
|
+
appendLog(payload.logPath, `Farming updated to ${payload.version}`);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
const message = error && error.message ? error.message : String(error);
|
|
84
|
+
writeJsonAtomic(payload.stateFile, stateFor(payload, 'failed', {
|
|
85
|
+
error: message,
|
|
86
|
+
completedAt: new Date().toISOString(),
|
|
87
|
+
}));
|
|
88
|
+
appendLog(payload.logPath, `Update failed: ${message}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (require.main === module) {
|
|
93
|
+
let payload;
|
|
94
|
+
try {
|
|
95
|
+
payload = JSON.parse(process.env.FARMING_BUNDLE_UPDATE_PAYLOAD || '');
|
|
96
|
+
} catch {
|
|
97
|
+
console.error('Invalid FARMING_BUNDLE_UPDATE_PAYLOAD');
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
runBundleUpdate(payload).catch(error => {
|
|
101
|
+
console.error(error.message || error);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
runBundleUpdate,
|
|
108
|
+
validatePayload,
|
|
109
|
+
};
|