farming-code 2.2.18 → 2.2.20
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 +145 -42
- 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 +369 -59
- package/backend/terminal-screen-worker-pool.js +15 -6
- package/backend/update-service.js +46 -23
- package/backend/usage-monitor.js +11 -0
- package/dist/assets/App-BupehIbc.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-BAt0Q_Op.css +1 -0
- package/dist/assets/{index-fp8VhOes.js → index-DuMjesL2.js} +2 -2
- package/dist/assets/main-y2aK_56c.css +1 -0
- package/dist/index.html +1 -1
- package/frontend/skins/crt/app.js +257 -35
- package/frontend/skins/crt/index.html +21 -38
- package/frontend/skins/crt/styles/billing.css +6 -1
- package/package.json +1 -1
- package/dist/assets/App-DTAQilnX.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
|
@@ -1414,6 +1414,16 @@ class AgentManager extends EventEmitter {
|
|
|
1414
1414
|
projectOrder: finiteOrder(record.projectOrder),
|
|
1415
1415
|
pinnedOrder: finiteOrder(record.pinnedOrder),
|
|
1416
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),
|
|
1417
1427
|
persistentSessionId: record.id || '',
|
|
1418
1428
|
runtimeAgentId: record.runtimeAgentId || '',
|
|
1419
1429
|
reviveTerminalState: record.serializedState || null,
|
|
@@ -1461,7 +1471,10 @@ class AgentManager extends EventEmitter {
|
|
|
1461
1471
|
replacement.attentionUpdatedAt = finiteNumberOrNull(record.attentionUpdatedAt);
|
|
1462
1472
|
replacement.readAttentionAt = finiteNumberOrNull(record.readAttentionAt);
|
|
1463
1473
|
replacement.attentionReason = record.attentionReason || '';
|
|
1474
|
+
replacement.attentionOutputEpoch = record.attentionOutputEpoch || '';
|
|
1464
1475
|
replacement.attentionOutputSeq = finiteNumberOrNull(record.attentionOutputSeq);
|
|
1476
|
+
replacement.readOutputEpoch = record.readOutputEpoch || '';
|
|
1477
|
+
replacement.readOutputSeq = finiteNumberOrNull(record.readOutputSeq);
|
|
1465
1478
|
replacement.unread = agentAttentionUnread(replacement);
|
|
1466
1479
|
this.ensurePersistentAgentSession(replacement);
|
|
1467
1480
|
if (sessionKey) liveProviderSessions.add(sessionKey);
|
|
@@ -1737,6 +1750,7 @@ class AgentManager extends EventEmitter {
|
|
|
1737
1750
|
if (!agent || !this.configManager || typeof this.configManager.ensureAgentSessionRecord !== 'function') {
|
|
1738
1751
|
return '';
|
|
1739
1752
|
}
|
|
1753
|
+
const previousPersistentSessionId = agent.persistentSessionId || '';
|
|
1740
1754
|
const sessionOptions = agent.providerSessionKey
|
|
1741
1755
|
? this.acpSessionOptionsByKey.get(agent.providerSessionKey)
|
|
1742
1756
|
: null;
|
|
@@ -1747,9 +1761,39 @@ class AgentManager extends EventEmitter {
|
|
|
1747
1761
|
} : {}),
|
|
1748
1762
|
...patch,
|
|
1749
1763
|
});
|
|
1750
|
-
if (persistentSessionId
|
|
1764
|
+
if (persistentSessionId) {
|
|
1751
1765
|
agent.persistentSessionId = persistentSessionId;
|
|
1752
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
|
+
}
|
|
1753
1797
|
return persistentSessionId;
|
|
1754
1798
|
}
|
|
1755
1799
|
|
|
@@ -2828,19 +2872,19 @@ class AgentManager extends EventEmitter {
|
|
|
2828
2872
|
shellLastCommandStartedAt: null,
|
|
2829
2873
|
shellLastCommandFinishedAt: null,
|
|
2830
2874
|
shellLastCommandDurationMs: null,
|
|
2831
|
-
pinned:
|
|
2875
|
+
pinned: options.pinned === true,
|
|
2832
2876
|
projectOrder: finiteOrder(options.projectOrder),
|
|
2833
2877
|
pinnedOrder: finiteOrder(options.pinnedOrder),
|
|
2834
|
-
attentionSeq:
|
|
2835
|
-
readAttentionSeq:
|
|
2836
|
-
attentionUpdatedAt:
|
|
2837
|
-
readAttentionAt:
|
|
2838
|
-
attentionReason: '',
|
|
2839
|
-
attentionOutputEpoch: '',
|
|
2840
|
-
attentionOutputSeq:
|
|
2841
|
-
readOutputEpoch: '',
|
|
2842
|
-
readOutputSeq:
|
|
2843
|
-
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),
|
|
2844
2888
|
archived: false,
|
|
2845
2889
|
archivedAt: null,
|
|
2846
2890
|
canForkNewWorktree: this.canCreateForkWorktree(projectWorkspace || workspace),
|
|
@@ -3102,6 +3146,9 @@ class AgentManager extends EventEmitter {
|
|
|
3102
3146
|
agentRecord.persistentSessionId = this.ensurePersistentAgentSession(agentRecord, {
|
|
3103
3147
|
visibleOnMainPage: true,
|
|
3104
3148
|
archived: false,
|
|
3149
|
+
...(options.customTitleExplicit === true
|
|
3150
|
+
? { customTitle: agentRecord.customTitle }
|
|
3151
|
+
: {}),
|
|
3105
3152
|
});
|
|
3106
3153
|
|
|
3107
3154
|
const agent = this.agents.get(agentId);
|
|
@@ -3717,7 +3764,7 @@ class AgentManager extends EventEmitter {
|
|
|
3717
3764
|
|
|
3718
3765
|
const customTitle = String(title || '').trim().slice(0, 80);
|
|
3719
3766
|
agent.customTitle = customTitle;
|
|
3720
|
-
this.ensurePersistentAgentSession(agent);
|
|
3767
|
+
this.ensurePersistentAgentSession(agent, { customTitle });
|
|
3721
3768
|
this.updateEngineProviderSessionMetadata(agent);
|
|
3722
3769
|
this.emit('update');
|
|
3723
3770
|
return { agentId, customTitle };
|
|
@@ -4379,24 +4426,33 @@ class AgentManager extends EventEmitter {
|
|
|
4379
4426
|
}
|
|
4380
4427
|
|
|
4381
4428
|
async rollbackPermanentWorktree(created) {
|
|
4382
|
-
if (!created?.workspace || !created?.sourceWorkspace)
|
|
4429
|
+
if (!created?.workspace || !created?.sourceWorkspace) {
|
|
4430
|
+
return { rolledBack: true };
|
|
4431
|
+
}
|
|
4432
|
+
const errors = [];
|
|
4383
4433
|
try {
|
|
4384
4434
|
await execFileAsync('git', ['-C', created.sourceWorkspace, 'worktree', 'remove', '--force', created.workspace], {
|
|
4385
4435
|
timeout: 60000,
|
|
4386
4436
|
maxBuffer: 1024 * 1024 * 4,
|
|
4387
4437
|
});
|
|
4388
|
-
} catch {
|
|
4389
|
-
|
|
4438
|
+
} catch (error) {
|
|
4439
|
+
const message = error && error.stderr ? String(error.stderr).trim() : '';
|
|
4440
|
+
errors.push(message || 'Failed to remove the created worktree');
|
|
4390
4441
|
}
|
|
4391
|
-
if (
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
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
|
+
}
|
|
4399
4452
|
}
|
|
4453
|
+
return errors.length > 0
|
|
4454
|
+
? { rolledBack: false, error: errors.join('; ') }
|
|
4455
|
+
: { rolledBack: true };
|
|
4400
4456
|
}
|
|
4401
4457
|
|
|
4402
4458
|
async inspectForkWorktreeProject(workspace) {
|
|
@@ -4498,13 +4554,6 @@ class AgentManager extends EventEmitter {
|
|
|
4498
4554
|
}
|
|
4499
4555
|
|
|
4500
4556
|
const projectAgents = this.agentsForProjectWorkspace(inspected.workspace);
|
|
4501
|
-
const removedMainPageSessionKeys = this.removeMainPageProviderSessionsForAgents(projectAgents);
|
|
4502
|
-
const archivedAgentIds = [];
|
|
4503
|
-
for (const agent of projectAgents) {
|
|
4504
|
-
const result = await this.archiveAgent(agent.id);
|
|
4505
|
-
if (result && !result.error) archivedAgentIds.push(agent.id);
|
|
4506
|
-
}
|
|
4507
|
-
|
|
4508
4557
|
const args = ['-C', inspected.workspace, 'worktree', 'remove'];
|
|
4509
4558
|
if (options.force === true) args.push('--force');
|
|
4510
4559
|
args.push(inspected.workspace);
|
|
@@ -4518,18 +4567,31 @@ class AgentManager extends EventEmitter {
|
|
|
4518
4567
|
const message = error && error.stderr ? String(error.stderr).trim() : '';
|
|
4519
4568
|
return {
|
|
4520
4569
|
...inspected,
|
|
4521
|
-
archivedAgentIds,
|
|
4522
|
-
removedMainPageSessionKeys,
|
|
4523
4570
|
error: message || 'Failed to delete git worktree',
|
|
4524
4571
|
};
|
|
4525
4572
|
}
|
|
4526
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
|
+
}
|
|
4527
4586
|
return {
|
|
4528
4587
|
workspace: inspected.workspace,
|
|
4529
4588
|
deleted: true,
|
|
4530
4589
|
forced: options.force === true,
|
|
4531
4590
|
archivedAgentIds,
|
|
4532
|
-
removedMainPageSessionKeys,
|
|
4591
|
+
removedMainPageSessionKeys: Array.from(new Set(removedMainPageSessionKeys)),
|
|
4592
|
+
...(cleanupErrors.length > 0 ? {
|
|
4593
|
+
cleanupError: cleanupErrors.join('; '),
|
|
4594
|
+
} : {}),
|
|
4533
4595
|
};
|
|
4534
4596
|
}
|
|
4535
4597
|
|
|
@@ -4686,7 +4748,7 @@ class AgentManager extends EventEmitter {
|
|
|
4686
4748
|
}
|
|
4687
4749
|
}
|
|
4688
4750
|
|
|
4689
|
-
async archiveAgent(agentId) {
|
|
4751
|
+
async archiveAgent(agentId, options = {}) {
|
|
4690
4752
|
const agent = this.agents.get(agentId);
|
|
4691
4753
|
if (!agent) {
|
|
4692
4754
|
return { error: 'Agent not found' };
|
|
@@ -4695,13 +4757,26 @@ class AgentManager extends EventEmitter {
|
|
|
4695
4757
|
return { error: 'Main Agent cannot be archived' };
|
|
4696
4758
|
}
|
|
4697
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;
|
|
4698
4777
|
const removedMainPageSessionKeys = this.removeMainPageProviderSessionsForAgents([agent]);
|
|
4699
4778
|
this.ensurePersistentAgentSession(agent, { visibleOnMainPage: false });
|
|
4700
|
-
|
|
4701
|
-
reason: 'manual-archive',
|
|
4702
|
-
recordHistory: !isEphemeralShellAgent(agent),
|
|
4703
|
-
});
|
|
4704
|
-
this.scheduleCodexSessionArchive(agent);
|
|
4779
|
+
if (options.scheduleProviderArchive !== false) this.scheduleCodexSessionArchive(agent);
|
|
4705
4780
|
return { agentId, archived: true, removed: true, removedMainPageSessionKeys };
|
|
4706
4781
|
}
|
|
4707
4782
|
|
|
@@ -4736,15 +4811,42 @@ class AgentManager extends EventEmitter {
|
|
|
4736
4811
|
|
|
4737
4812
|
async killAgent(agentId, options = {}) {
|
|
4738
4813
|
const agent = this.agents.get(agentId);
|
|
4739
|
-
if (!agent) return;
|
|
4814
|
+
if (!agent) return { agentId, killed: true, missing: true };
|
|
4740
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
|
+
}
|
|
4741
4820
|
try {
|
|
4742
|
-
const engine = this.engineBridge.getEngine(agent.engineName);
|
|
4743
4821
|
if (engine) {
|
|
4744
4822
|
await engine.killSession(agentId);
|
|
4745
4823
|
}
|
|
4746
4824
|
} catch (error) {
|
|
4747
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
|
+
}
|
|
4748
4850
|
}
|
|
4749
4851
|
|
|
4750
4852
|
if (options.recordHistory !== false && !isEphemeralShellAgent(agent)) {
|
|
@@ -4771,6 +4873,7 @@ class AgentManager extends EventEmitter {
|
|
|
4771
4873
|
if (options.emitUpdate !== false) {
|
|
4772
4874
|
this.emit('update');
|
|
4773
4875
|
}
|
|
4876
|
+
return { agentId, killed: true };
|
|
4774
4877
|
}
|
|
4775
4878
|
|
|
4776
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
|
+
};
|
|
@@ -222,13 +222,14 @@ class ConfigManager {
|
|
|
222
222
|
if (!expanded) continue;
|
|
223
223
|
const resolved = path.resolve(expanded);
|
|
224
224
|
if (resolved === path.parse(resolved).root || this.isInternalWorkspace(resolved)) continue;
|
|
225
|
-
let canonical;
|
|
225
|
+
let canonical = resolved;
|
|
226
226
|
try {
|
|
227
227
|
canonical = fs.realpathSync(resolved);
|
|
228
|
-
if (!fs.statSync(canonical).isDirectory()) continue;
|
|
229
228
|
} catch {
|
|
230
|
-
|
|
229
|
+
// Project membership is durable. A temporarily unavailable path remains
|
|
230
|
+
// until the user explicitly removes it.
|
|
231
231
|
}
|
|
232
|
+
if (this.isInternalWorkspace(canonical)) continue;
|
|
232
233
|
if (seen.has(canonical)) continue;
|
|
233
234
|
seen.add(canonical);
|
|
234
235
|
result.push(canonical);
|
|
@@ -752,6 +753,97 @@ class ConfigManager {
|
|
|
752
753
|
};
|
|
753
754
|
}
|
|
754
755
|
|
|
756
|
+
mountProjectWorkspace(workspace) {
|
|
757
|
+
const expanded = this.expandWorkspacePath(workspace);
|
|
758
|
+
const resolved = expanded ? path.resolve(expanded) : '';
|
|
759
|
+
let canonicalWorkspace = '';
|
|
760
|
+
if (resolved) {
|
|
761
|
+
try {
|
|
762
|
+
canonicalWorkspace = fs.realpathSync(resolved);
|
|
763
|
+
if (!fs.statSync(canonicalWorkspace).isDirectory()) canonicalWorkspace = '';
|
|
764
|
+
} catch {
|
|
765
|
+
canonicalWorkspace = '';
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
if (
|
|
769
|
+
!canonicalWorkspace
|
|
770
|
+
|| canonicalWorkspace === path.parse(canonicalWorkspace).root
|
|
771
|
+
|| this.isInternalWorkspace(canonicalWorkspace)
|
|
772
|
+
) {
|
|
773
|
+
throw new Error('Project workspace is invalid or unavailable');
|
|
774
|
+
}
|
|
775
|
+
const current = this.settings.projectWorkspaces || [];
|
|
776
|
+
if (!current.includes(canonicalWorkspace)) {
|
|
777
|
+
this.commitProjectMembership({
|
|
778
|
+
projectWorkspaces: [canonicalWorkspace, ...current],
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
return {
|
|
782
|
+
workspace: canonicalWorkspace,
|
|
783
|
+
projectWorkspaces: [...(this.settings.projectWorkspaces || [])],
|
|
784
|
+
pinnedProjectWorkspaces: [...(this.settings.pinnedProjectWorkspaces || [])],
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
removeProjectWorkspace(workspace) {
|
|
789
|
+
const expanded = this.expandWorkspacePath(workspace);
|
|
790
|
+
if (!expanded) throw new Error('Project workspace is required');
|
|
791
|
+
const resolved = path.resolve(expanded);
|
|
792
|
+
const candidates = new Set([expanded, resolved]);
|
|
793
|
+
try {
|
|
794
|
+
candidates.add(fs.realpathSync(resolved));
|
|
795
|
+
} catch {
|
|
796
|
+
// A deleted worktree can still be removed by its last canonical path.
|
|
797
|
+
}
|
|
798
|
+
const current = this.settings.projectWorkspaces || [];
|
|
799
|
+
const nextProjects = current.filter(entry => !candidates.has(entry));
|
|
800
|
+
const currentPinned = this.settings.pinnedProjectWorkspaces || [];
|
|
801
|
+
const nextPinned = currentPinned.filter(entry => !candidates.has(entry));
|
|
802
|
+
if (nextProjects.length !== current.length || nextPinned.length !== currentPinned.length) {
|
|
803
|
+
this.commitProjectMembership({
|
|
804
|
+
projectWorkspaces: nextProjects,
|
|
805
|
+
pinnedProjectWorkspaces: nextPinned,
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
return {
|
|
809
|
+
workspace: current.find(entry => candidates.has(entry)) || resolved,
|
|
810
|
+
projectWorkspaces: [...(this.settings.projectWorkspaces || [])],
|
|
811
|
+
pinnedProjectWorkspaces: [...(this.settings.pinnedProjectWorkspaces || [])],
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
setProjectWorkspacePinned(workspace, pinned) {
|
|
816
|
+
const [canonicalWorkspace] = this.normalizeProjectWorkspaces([workspace]);
|
|
817
|
+
if (!canonicalWorkspace || !(this.settings.projectWorkspaces || []).includes(canonicalWorkspace)) {
|
|
818
|
+
throw new Error('Project does not exist');
|
|
819
|
+
}
|
|
820
|
+
const current = this.settings.pinnedProjectWorkspaces || [];
|
|
821
|
+
const nextPinned = pinned
|
|
822
|
+
? [...current.filter(entry => entry !== canonicalWorkspace), canonicalWorkspace]
|
|
823
|
+
: current.filter(entry => entry !== canonicalWorkspace);
|
|
824
|
+
if (
|
|
825
|
+
nextPinned.length !== current.length
|
|
826
|
+
|| nextPinned.some((entry, index) => entry !== current[index])
|
|
827
|
+
) {
|
|
828
|
+
this.commitProjectMembership({ pinnedProjectWorkspaces: nextPinned });
|
|
829
|
+
}
|
|
830
|
+
return {
|
|
831
|
+
workspace: canonicalWorkspace,
|
|
832
|
+
projectWorkspaces: [...(this.settings.projectWorkspaces || [])],
|
|
833
|
+
pinnedProjectWorkspaces: [...(this.settings.pinnedProjectWorkspaces || [])],
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
commitProjectMembership(settingsPatch) {
|
|
838
|
+
const previousSettings = this.settings;
|
|
839
|
+
try {
|
|
840
|
+
this.updateSettings(settingsPatch);
|
|
841
|
+
} catch (error) {
|
|
842
|
+
this.settings = previousSettings;
|
|
843
|
+
throw error;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
755
847
|
getMainPageSessionKeys() {
|
|
756
848
|
return this.sessionStore ? this.sessionStore.getMainPageSessionKeys() : [];
|
|
757
849
|
}
|
|
@@ -776,6 +868,10 @@ class ConfigManager {
|
|
|
776
868
|
return this.sessionStore ? this.sessionStore.ensureRecordForAgent(agent, patch) : '';
|
|
777
869
|
}
|
|
778
870
|
|
|
871
|
+
getAgentSessionRecordForProviderSessionKey(sessionKey) {
|
|
872
|
+
return this.sessionStore ? this.sessionStore.getRecordForProviderSessionKey(sessionKey) : null;
|
|
873
|
+
}
|
|
874
|
+
|
|
779
875
|
setProviderSessionDisplayState(sessionKey, patch = {}) {
|
|
780
876
|
return this.sessionStore ? this.sessionStore.setProviderSessionDisplayState(sessionKey, patch) : '';
|
|
781
877
|
}
|
|
@@ -794,7 +890,17 @@ class ConfigManager {
|
|
|
794
890
|
|
|
795
891
|
writeSettingsFile() {
|
|
796
892
|
fs.mkdirSync(this.farmingDir, { recursive: true });
|
|
797
|
-
|
|
893
|
+
const tempFile = `${this.settingsFile}.${process.pid}.${Date.now()}.tmp`;
|
|
894
|
+
try {
|
|
895
|
+
fs.writeFileSync(tempFile, JSON.stringify(this.settings, null, 2));
|
|
896
|
+
fs.renameSync(tempFile, this.settingsFile);
|
|
897
|
+
} finally {
|
|
898
|
+
try {
|
|
899
|
+
fs.unlinkSync(tempFile);
|
|
900
|
+
} catch {
|
|
901
|
+
// A successful rename already removed the temporary path.
|
|
902
|
+
}
|
|
903
|
+
}
|
|
798
904
|
}
|
|
799
905
|
|
|
800
906
|
appendTaskHistory(entry) {
|
|
@@ -6,6 +6,24 @@ const storageLayout = require('./storage-layout');
|
|
|
6
6
|
|
|
7
7
|
const SESSION_ID_PREFIX = 'fsess';
|
|
8
8
|
const MAX_MAIN_PAGE_SESSION_KEYS = 50;
|
|
9
|
+
const PRODUCT_STATE_FIELDS = [
|
|
10
|
+
'projectWorkspace',
|
|
11
|
+
'task',
|
|
12
|
+
'workflowTemplate',
|
|
13
|
+
'pinned',
|
|
14
|
+
'projectOrder',
|
|
15
|
+
'pinnedOrder',
|
|
16
|
+
'attentionSeq',
|
|
17
|
+
'readAttentionSeq',
|
|
18
|
+
'attentionUpdatedAt',
|
|
19
|
+
'readAttentionAt',
|
|
20
|
+
'attentionReason',
|
|
21
|
+
'attentionOutputEpoch',
|
|
22
|
+
'attentionOutputSeq',
|
|
23
|
+
'readOutputEpoch',
|
|
24
|
+
'readOutputSeq',
|
|
25
|
+
'customTitle',
|
|
26
|
+
];
|
|
9
27
|
|
|
10
28
|
function now() {
|
|
11
29
|
return Date.now();
|
|
@@ -184,10 +202,15 @@ class FarmingSessionStore {
|
|
|
184
202
|
attentionUpdatedAt: typeof agent.attentionUpdatedAt === 'number' ? agent.attentionUpdatedAt : null,
|
|
185
203
|
readAttentionAt: typeof agent.readAttentionAt === 'number' ? agent.readAttentionAt : null,
|
|
186
204
|
attentionReason: typeof agent.attentionReason === 'string' ? agent.attentionReason : '',
|
|
205
|
+
attentionOutputEpoch: typeof agent.attentionOutputEpoch === 'string' ? agent.attentionOutputEpoch : '',
|
|
187
206
|
attentionOutputSeq: typeof agent.attentionOutputSeq === 'number' ? agent.attentionOutputSeq : null,
|
|
207
|
+
readOutputEpoch: typeof agent.readOutputEpoch === 'string' ? agent.readOutputEpoch : '',
|
|
208
|
+
readOutputSeq: typeof agent.readOutputSeq === 'number' ? agent.readOutputSeq : null,
|
|
188
209
|
archived: agent.archived === true,
|
|
189
210
|
archivedAt: typeof agent.archivedAt === 'number' ? agent.archivedAt : null,
|
|
190
|
-
|
|
211
|
+
...(typeof agent.customTitle === 'string' && agent.customTitle
|
|
212
|
+
? { customTitle: agent.customTitle }
|
|
213
|
+
: {}),
|
|
191
214
|
title: typeof agent.customTitle === 'string' && agent.customTitle
|
|
192
215
|
? agent.customTitle
|
|
193
216
|
: (typeof agent.providerSessionTitle === 'string' && agent.providerSessionTitle
|
|
@@ -222,6 +245,9 @@ class FarmingSessionStore {
|
|
|
222
245
|
providerSessionTemporary: false,
|
|
223
246
|
updatedAt: now(),
|
|
224
247
|
};
|
|
248
|
+
if (typeof record.customTitle === 'string' && record.customTitle) {
|
|
249
|
+
record.title = record.customTitle;
|
|
250
|
+
}
|
|
225
251
|
index.providerSessionRecords[sessionKey] = id;
|
|
226
252
|
this.writeRecord(record);
|
|
227
253
|
this.writeIndex();
|
|
@@ -231,10 +257,38 @@ class FarmingSessionStore {
|
|
|
231
257
|
ensureRecordForAgent(agent, patch = {}) {
|
|
232
258
|
const providerSessionKey = this.providerSessionKeyForAgent(agent);
|
|
233
259
|
if (providerSessionKey) {
|
|
260
|
+
const previousId = safeSessionFileName(agent?.persistentSessionId)
|
|
261
|
+
? agent.persistentSessionId
|
|
262
|
+
: '';
|
|
263
|
+
const previous = previousId ? this.readRecord(previousId) : null;
|
|
264
|
+
const canonical = this.getRecordForProviderSessionKey(providerSessionKey);
|
|
265
|
+
const agentPatch = this.recordPatchFromAgent(agent);
|
|
266
|
+
if (canonical && previousId && canonical.id !== previousId) {
|
|
267
|
+
PRODUCT_STATE_FIELDS.forEach(field => {
|
|
268
|
+
if (Object.prototype.hasOwnProperty.call(canonical, field)) {
|
|
269
|
+
agentPatch[field] = canonical[field];
|
|
270
|
+
} else if (previous && Object.prototype.hasOwnProperty.call(previous, field)) {
|
|
271
|
+
agentPatch[field] = previous[field];
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
234
275
|
const id = this.ensureRecordForProviderSessionKey(providerSessionKey, {
|
|
235
|
-
...
|
|
276
|
+
...agentPatch,
|
|
236
277
|
...patch,
|
|
237
278
|
}, agent?.persistentSessionId || '');
|
|
279
|
+
if (previousId && previousId !== id) {
|
|
280
|
+
if (previous && (previous.providerSessionTemporary === true || !previous.providerSessionKey)) {
|
|
281
|
+
this.writeRecord({
|
|
282
|
+
...previous,
|
|
283
|
+
runtimeAgentId: '',
|
|
284
|
+
visibleOnMainPage: false,
|
|
285
|
+
archived: true,
|
|
286
|
+
archivedAt: now(),
|
|
287
|
+
mergedInto: id,
|
|
288
|
+
updatedAt: now(),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
238
292
|
return id;
|
|
239
293
|
}
|
|
240
294
|
|
|
@@ -345,6 +399,12 @@ class FarmingSessionStore {
|
|
|
345
399
|
return this.ensureIndex().mainPageSessionKeys.slice();
|
|
346
400
|
}
|
|
347
401
|
|
|
402
|
+
getRecordForProviderSessionKey(sessionKey) {
|
|
403
|
+
const index = this.ensureIndex();
|
|
404
|
+
const id = index.providerSessionRecords[sessionKey];
|
|
405
|
+
return safeSessionFileName(id) ? this.readRecord(id) : null;
|
|
406
|
+
}
|
|
407
|
+
|
|
348
408
|
listAgentRecords() {
|
|
349
409
|
this.ensureIndex();
|
|
350
410
|
let names = [];
|