farming-code 2.2.14 → 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 (136) hide show
  1. package/README.md +3 -3
  2. package/README.zh_cn.md +3 -3
  3. package/THIRD_PARTY_NOTICES.md +17 -1
  4. package/backend/acp/packaged-codex-acp.js +13 -0
  5. package/backend/acp-runtime.js +413 -24
  6. package/backend/agent-manager.js +572 -557
  7. package/backend/agent-provider-session.js +15 -1
  8. package/backend/agent-runtime-binding.js +9 -55
  9. package/backend/chat-runtime.js +28 -0
  10. package/backend/codex-rollout-follower.js +83 -0
  11. package/backend/codex-session-archive.js +12 -1
  12. package/backend/codex-session-history.js +94 -0
  13. package/backend/codex-transcript.js +2 -1
  14. package/backend/config-manager.js +25 -17
  15. package/backend/control-api.js +3 -1
  16. package/backend/farming-app-cli.js +21 -0
  17. package/backend/farming-session-store.js +2 -1
  18. package/backend/native-pty-host-client.js +59 -2
  19. package/backend/npm-update-helper.js +65 -7
  20. package/backend/provider-adapters.js +123 -38
  21. package/backend/provider-session-service.js +138 -34
  22. package/backend/server.js +94 -105
  23. package/backend/storage-layout.js +5 -16
  24. package/backend/update-service.js +94 -5
  25. package/backend/usage-history-client.js +305 -0
  26. package/backend/usage-history-scanner.generated.js +2250 -0
  27. package/backend/usage-history-scanner.ts +2718 -0
  28. package/backend/usage-history-smoke.js +64 -0
  29. package/backend/usage-history-worker.js +34 -0
  30. package/backend/usage-monitor.js +188 -588
  31. package/backend/vendor/usage-parsers/CC_STATISTICS_LICENSE +21 -0
  32. package/backend/vendor/usage-parsers/CODEXBAR_LICENSE +21 -0
  33. package/backend/vendor/usage-parsers/VENDOR.json +45 -0
  34. package/dist/acp/LICENSE.codex-acp +190 -0
  35. package/dist/acp/codex-acp-1.1.4.js +30718 -0
  36. package/dist/assets/App-6yWdwzj8.js +210 -0
  37. package/dist/assets/{FileEditorMarkdownPreview-UlVd0O4D.js → FileEditorMarkdownPreview-R0jwwLME.js} +4 -4
  38. package/dist/assets/FileEditorPane-DNy3LAqS.js +2 -0
  39. package/dist/assets/{IconGlyphs-AU22gPDY.js → IconGlyphs-BsUfqpKN.js} +1 -1
  40. package/dist/assets/ProjectFilesSection-BhDEUVHA.js +12 -0
  41. package/dist/assets/{ReviewPage-CeDKDyZh.js → ReviewPage-ChtByBe3.js} +1 -1
  42. package/dist/assets/{abnfDiagram-VRR7QNED-BgawzDQT.js → abnfDiagram-VRR7QNED-nnglrXFi.js} +1 -1
  43. package/dist/assets/{architectureDiagram-ZJ3FMSHR-DUShwPg0.js → architectureDiagram-ZJ3FMSHR-BEXMaFfY.js} +1 -1
  44. package/dist/assets/{blockDiagram-677ZJIJ3-CeGZviXR.js → blockDiagram-677ZJIJ3-1mCezSBk.js} +1 -1
  45. package/dist/assets/{c4Diagram-LMCZKHZV-CLhYht6S.js → c4Diagram-LMCZKHZV-DhTiI6Uv.js} +1 -1
  46. package/dist/assets/channel-BVWNatUB.js +1 -0
  47. package/dist/assets/{chunk-32BRIVSS-LFYpT-Cu.js → chunk-32BRIVSS-CCdsKd73.js} +1 -1
  48. package/dist/assets/{chunk-52WLFC77-BH6jDw9J.js → chunk-52WLFC77-Gncjk4LF.js} +1 -1
  49. package/dist/assets/{chunk-C7G6YPKG-B_mIUlb7.js → chunk-C7G6YPKG-CiDVUP82.js} +1 -1
  50. package/dist/assets/{chunk-EX3LRPZG-C63hVuAJ.js → chunk-EX3LRPZG-z6gXP2_N.js} +1 -1
  51. package/dist/assets/{chunk-FWX5IMBZ-CboX-WF_.js → chunk-FWX5IMBZ-DMZBccQa.js} +2 -2
  52. package/dist/assets/{chunk-HOUHSVGY-BOgSd6Un.js → chunk-HOUHSVGY-D8tIeoPk.js} +1 -1
  53. package/dist/assets/{chunk-ICXQ74PX-C0K-pgZt.js → chunk-ICXQ74PX-b6oQ2Q17.js} +1 -1
  54. package/dist/assets/{chunk-MOJQB5TN-CVMfwN_7.js → chunk-MOJQB5TN-DAVH_p2a.js} +1 -1
  55. package/dist/assets/{chunk-OGEWGWER-DLT3pxyw.js → chunk-OGEWGWER-DxoiMCI-.js} +1 -1
  56. package/dist/assets/{chunk-PUDLZKDR-Cd079yIQ.js → chunk-PUDLZKDR-NsOWh7HM.js} +1 -1
  57. package/dist/assets/{chunk-Q4XR5HBZ-D0jHa6SO.js → chunk-Q4XR5HBZ-Di4H7L21.js} +1 -1
  58. package/dist/assets/{chunk-V7JOEXUC-CVDtR75H.js → chunk-V7JOEXUC-DttpZg9Y.js} +1 -1
  59. package/dist/assets/{chunk-VAUOI2AC-B-W2_9Ot.js → chunk-VAUOI2AC-UHNr_yhX.js} +1 -1
  60. package/dist/assets/{chunk-VR4S4FIN-FD3kWh32.js → chunk-VR4S4FIN-D-Fmtd4Z.js} +1 -1
  61. package/dist/assets/{chunk-WYO6CB5R-C2bGaS5r.js → chunk-WYO6CB5R-CUxMtfK2.js} +4 -4
  62. package/dist/assets/{chunk-ZGVPDNZ5-ymgts0zo.js → chunk-ZGVPDNZ5-CoV2QEl0.js} +1 -1
  63. package/dist/assets/classDiagram-OUVF2IWQ-Db0ib5iE.js +1 -0
  64. package/dist/assets/classDiagram-v2-EOCWNBFH-Bkf69XF9.js +1 -0
  65. package/dist/assets/code-dark-5esj0bRu.css +1 -0
  66. package/dist/assets/{cynefinDiagram-TSTJHNR4-CwpobXIl.js → cynefinDiagram-TSTJHNR4-0xPI92Lg.js} +1 -1
  67. package/dist/assets/{dagre-VKFMJZFB-Dkhlfjna.js → dagre-VKFMJZFB-DfuMG4gU.js} +1 -1
  68. package/dist/assets/{diagram-FQU43EPY-hA8dicp6.js → diagram-FQU43EPY-CT00bLZ6.js} +1 -1
  69. package/dist/assets/{diagram-G47NLZAW-BTY6JTzO.js → diagram-G47NLZAW-Xbh7zXUx.js} +1 -1
  70. package/dist/assets/{diagram-NH7WQ7WH-xkaHHX9M.js → diagram-NH7WQ7WH-SRDVPfn7.js} +1 -1
  71. package/dist/assets/{diagram-OA4YK3LP-gk_fg9UU.js → diagram-OA4YK3LP-CQRli4Np.js} +1 -1
  72. package/dist/assets/{diagram-WEI45ONY-BvC8xd6T.js → diagram-WEI45ONY-DMf17WQi.js} +1 -1
  73. package/dist/assets/{ebnfDiagram-CCIWWBDH-CR7RaNat.js → ebnfDiagram-CCIWWBDH-DhpFkgJ5.js} +1 -1
  74. package/dist/assets/{erDiagram-Q63AITRT-_M-JFPbj.js → erDiagram-Q63AITRT-1IwxWmJo.js} +1 -1
  75. package/dist/assets/file-icons-DpbrD5ZP.js +1 -0
  76. package/dist/assets/flowDiagram-23GEKE2U-DYFo2xmB.js +1 -0
  77. package/dist/assets/{ganttDiagram-NO4QXBWP-Bl8P1yf_.js → ganttDiagram-NO4QXBWP-BNCdfyr1.js} +1 -1
  78. package/dist/assets/{gitGraphDiagram-IHSO6WYX-DoHa5ysz.js → gitGraphDiagram-IHSO6WYX-pbyWZuiX.js} +1 -1
  79. package/dist/assets/{index-BA-iH2Kg.js → index-C8ZFKye1.js} +2 -2
  80. package/dist/assets/{infoDiagram-FWYZ7A6U-DSFcSuUs.js → infoDiagram-FWYZ7A6U-BcTOgDbe.js} +1 -1
  81. package/dist/assets/{ishikawaDiagram-FXEZZL3T-DdkqullI.js → ishikawaDiagram-FXEZZL3T-DCdvGKrn.js} +1 -1
  82. package/dist/assets/{journeyDiagram-5HDEW3XC-B4hRnQJr.js → journeyDiagram-5HDEW3XC-BZ-1q_IT.js} +1 -1
  83. package/dist/assets/{kanban-definition-HUTT4EX6-tm2FZB2E.js → kanban-definition-HUTT4EX6-BIgejCjj.js} +1 -1
  84. package/dist/assets/{line-CmsjK4kn.js → line-CfptFMmX.js} +1 -1
  85. package/dist/assets/main-DSO3i_Gs.css +1 -0
  86. package/dist/assets/{mermaid.core-C7UT_EEL.js → mermaid.core-gSpX30pV.js} +3 -3
  87. package/dist/assets/{mindmap-definition-LN4V7U3C-DplfjGms.js → mindmap-definition-LN4V7U3C-BKBaI4LW.js} +1 -1
  88. package/dist/assets/{pegDiagram-2B236MQR-lu3SdkQs.js → pegDiagram-2B236MQR-sBvDY3VX.js} +1 -1
  89. package/dist/assets/{pieDiagram-ENE6RG2P-3fHbmDf9.js → pieDiagram-ENE6RG2P-DWlWcu08.js} +1 -1
  90. package/dist/assets/{quadrantDiagram-ABIIQ3AL-a5qoWz-X.js → quadrantDiagram-ABIIQ3AL-DZutUd3J.js} +1 -1
  91. package/dist/assets/{railroadDiagram-RFXS5EU6-D2TOuXW9.js → railroadDiagram-RFXS5EU6-DQb3lXGM.js} +1 -1
  92. package/dist/assets/{requirementDiagram-TGXJPOKE-LQBWpUvd.js → requirementDiagram-TGXJPOKE-DqFGXu2e.js} +1 -1
  93. package/dist/assets/{sankeyDiagram-HTMAVEWB-B1ohYoyK.js → sankeyDiagram-HTMAVEWB-d7ECahnO.js} +1 -1
  94. package/dist/assets/{sequenceDiagram-DBY2YBRQ-C3vM-hMq.js → sequenceDiagram-DBY2YBRQ-DdsbKqct.js} +1 -1
  95. package/dist/assets/{stateDiagram-2N3HPSRC-BvFhOryI.js → stateDiagram-2N3HPSRC-P6iSrgwR.js} +1 -1
  96. package/dist/assets/stateDiagram-v2-6OUMAXLB-DIYSf_qh.js +1 -0
  97. package/dist/assets/{swimlanes-5IMT3BWC-DKF_b-2s.js → swimlanes-5IMT3BWC-BGnZJx1M.js} +1 -1
  98. package/dist/assets/swimlanesDiagram-G3AALYLV-BIkrlKrZ.js +8 -0
  99. package/dist/assets/{timeline-definition-FHXFAJF6-vAs11ux_.js → timeline-definition-FHXFAJF6-rA-r3aUR.js} +1 -1
  100. package/dist/assets/{vennDiagram-L72KCM5P-BybTX_9B.js → vennDiagram-L72KCM5P-DiVKUjA9.js} +1 -1
  101. package/dist/assets/{wardleyDiagram-EHGQE667-CicUmMBR.js → wardleyDiagram-EHGQE667-Cfxh7UOn.js} +1 -1
  102. package/dist/assets/workspace-editor-model-D0Im34S5.js +1 -0
  103. package/dist/assets/{workspace-editor-monaco-BICJESG0.js → workspace-editor-monaco-BIpqcWf1.js} +1 -1
  104. package/dist/assets/workspace-editor-monaco-DLlws5Ig.js +1 -0
  105. package/dist/assets/{workspace-view-state-DvYG_9PH.js → workspace-view-state-BpPINu84.js} +1 -1
  106. package/dist/assets/{xychartDiagram-FW5EYKEG-BhWDyCX-.js → xychartDiagram-FW5EYKEG--hmryQeL.js} +1 -1
  107. package/dist/crt-mermaid-renderer.js +313 -313
  108. package/dist/index.html +1 -1
  109. package/frontend/session-bridge.js +24 -2
  110. package/frontend/skins/crt/app.js +203 -85
  111. package/frontend/skins/crt/index.html +2 -11
  112. package/frontend/skins/crt/styles/effects.css +2 -34
  113. package/package.json +30 -12
  114. package/shared/backend-connection-status.d.ts +25 -0
  115. package/shared/backend-connection-status.js +40 -0
  116. package/shared/browser-protocol.js +3 -3
  117. package/backend/app-server-api.js +0 -917
  118. package/backend/codex-app-server-home.js +0 -39
  119. package/backend/codex-app-server-runtime.js +0 -901
  120. package/dist/assets/App-Bumi772v.js +0 -206
  121. package/dist/assets/FileEditorPane-Dxx4rKOg.js +0 -2
  122. package/dist/assets/ProjectFilesSection-DE7Nb9Jk.js +0 -12
  123. package/dist/assets/channel-Uw90AO0b.js +0 -1
  124. package/dist/assets/classDiagram-OUVF2IWQ-DKW_dtku.js +0 -1
  125. package/dist/assets/classDiagram-v2-EOCWNBFH-C8Fi7-yv.js +0 -1
  126. package/dist/assets/code-dark-DPiuyeSp.css +0 -1
  127. package/dist/assets/code-mobile-3C2Rr-CN.css +0 -1
  128. package/dist/assets/file-icons-Bw2qd5iT.js +0 -1
  129. package/dist/assets/flowDiagram-23GEKE2U-DAiS5Y7G.js +0 -1
  130. package/dist/assets/main-Bqtb5kuz.css +0 -1
  131. package/dist/assets/stateDiagram-v2-6OUMAXLB-WMrz4PNo.js +0 -1
  132. package/dist/assets/swimlanesDiagram-G3AALYLV-BGswC9oC.js +0 -8
  133. package/dist/assets/workspace-editor-model-BQol4qbA.js +0 -1
  134. package/dist/assets/workspace-editor-monaco-DRJ7IaXk.js +0 -1
  135. package/scripts/sync-ghostty-vendor.js +0 -66
  136. /package/dist/assets/{core-D0LFJkDt.js → core-B9MwRjDE.js} +0 -0
@@ -1,12 +1,16 @@
1
+ const fs = require('fs');
1
2
  const path = require('path');
2
3
  const { findAgentSession } = require('./agent-session-history');
3
- const { listCodexSessions } = require('./codex-session-history');
4
- const { isLinkedWorktreeOf } = require('./git-worktree-info');
4
+ const {
5
+ codexSessionDateKeys,
6
+ listCodexSessionIdentities,
7
+ } = require('./codex-session-history');
5
8
  const { mainPageAgentSessionKey } = require('./main-page-session');
6
9
  const { isTemporaryProviderSessionId } = require('./provider-session-id');
7
10
 
8
11
  const CODEX_RESOLVE_COOLDOWN_MS = 1000;
9
- const CODEX_MATCH_GRACE_MS = 30 * 1000;
12
+ const CODEX_MATCH_WINDOW_MS = 30 * 1000;
13
+ const CODEX_STARTUP_RETRY_DELAYS_MS = [500, 1000, 2000, 4000, 8000, 12000];
10
14
  const TITLE_RESOLVE_COOLDOWN_MS = 30 * 1000;
11
15
 
12
16
  function normalizePath(value) {
@@ -16,6 +20,16 @@ function normalizePath(value) {
16
20
  return trimmed.replace(/[\\/]+$/, '');
17
21
  }
18
22
 
23
+ function canonicalPath(value) {
24
+ const normalized = normalizePath(value);
25
+ if (!normalized) return '';
26
+ try {
27
+ return normalizePath(fs.realpathSync.native(normalized));
28
+ } catch {
29
+ return normalized;
30
+ }
31
+ }
32
+
19
33
  function timestampMs(value) {
20
34
  const parsed = Date.parse(value || '');
21
35
  return Number.isFinite(parsed) ? parsed : 0;
@@ -26,10 +40,14 @@ class ProviderSessionService {
26
40
  this.agents = options.agents || new Map();
27
41
  this.getProviderHomes = options.getProviderHomes || (() => undefined);
28
42
  this.commit = options.commit || (() => {});
29
- this.listCodexSessions = options.listCodexSessions || listCodexSessions;
43
+ this.listCodexSessionIdentities = options.listCodexSessionIdentities || listCodexSessionIdentities;
30
44
  this.findAgentSession = options.findAgentSession || findAgentSession;
31
- this.isLinkedWorktreeOf = options.isLinkedWorktreeOf || isLinkedWorktreeOf;
45
+ this.codexStartupRetryDelaysMs = Array.isArray(options.codexStartupRetryDelaysMs)
46
+ ? options.codexStartupRetryDelaysMs
47
+ : CODEX_STARTUP_RETRY_DELAYS_MS;
32
48
  this.resolutions = new Map();
49
+ this.codexIdentityScans = new Map();
50
+ this.codexStartupRetries = new Map();
33
51
  }
34
52
 
35
53
  activate(agentId) {
@@ -38,6 +56,7 @@ class ProviderSessionService {
38
56
 
39
57
  if (agent.providerSessionProvider === 'codex' && agent.providerSessionTemporary === true) {
40
58
  this.observe(agentId, { force: true });
59
+ this.scheduleTemporaryCodexStartupRetry(agentId);
41
60
  return;
42
61
  }
43
62
 
@@ -54,10 +73,71 @@ class ProviderSessionService {
54
73
  stop(agentId) {
55
74
  this.resolutions.delete(`codex:${agentId}`);
56
75
  this.resolutions.delete(`title:${agentId}`);
76
+ const startupRetry = this.codexStartupRetries.get(agentId);
77
+ if (startupRetry?.timer) clearTimeout(startupRetry.timer);
78
+ this.codexStartupRetries.delete(agentId);
57
79
  }
58
80
 
59
81
  dispose() {
82
+ for (const startupRetry of this.codexStartupRetries.values()) {
83
+ if (startupRetry?.timer) clearTimeout(startupRetry.timer);
84
+ }
60
85
  this.resolutions.clear();
86
+ this.codexIdentityScans.clear();
87
+ this.codexStartupRetries.clear();
88
+ }
89
+
90
+ scheduleTemporaryCodexStartupRetry(agentId) {
91
+ if (this.codexStartupRetries.has(agentId)) return;
92
+ const agent = this.agents.get(agentId);
93
+ const retry = {
94
+ attempt: 0,
95
+ timer: null,
96
+ deadlineAt: (Number(agent?.startedAt) || Date.now()) + CODEX_MATCH_WINDOW_MS,
97
+ };
98
+ this.codexStartupRetries.set(agentId, retry);
99
+
100
+ const scheduleNext = () => {
101
+ if (this.codexStartupRetries.get(agentId) !== retry) return;
102
+ const agent = this.agents.get(agentId);
103
+ if (
104
+ !agent
105
+ || agent.providerSessionProvider !== 'codex'
106
+ || agent.providerSessionTemporary !== true
107
+ ) {
108
+ this.stop(agentId);
109
+ return;
110
+ }
111
+ const delayMs = Number(this.codexStartupRetryDelaysMs[retry.attempt]);
112
+ if (!Number.isFinite(delayMs) || delayMs < 0) {
113
+ this.codexStartupRetries.delete(agentId);
114
+ return;
115
+ }
116
+ if (Date.now() + delayMs > retry.deadlineAt) {
117
+ this.codexStartupRetries.delete(agentId);
118
+ return;
119
+ }
120
+ retry.attempt += 1;
121
+ retry.timer = setTimeout(async () => {
122
+ retry.timer = null;
123
+ if (
124
+ this.codexStartupRetries.get(agentId) !== retry
125
+ || Date.now() > retry.deadlineAt
126
+ ) {
127
+ if (this.codexStartupRetries.get(agentId) === retry) {
128
+ this.codexStartupRetries.delete(agentId);
129
+ }
130
+ return;
131
+ }
132
+ const resolved = await this.resolveTemporaryCodex(agentId, { force: true });
133
+ if (this.codexStartupRetries.get(agentId) !== retry) return;
134
+ if (resolved) return;
135
+ scheduleNext();
136
+ }, delayMs);
137
+ retry.timer.unref?.();
138
+ };
139
+
140
+ scheduleNext();
61
141
  }
62
142
 
63
143
  bindConfirmed(agentId, provider, sessionId) {
@@ -97,6 +177,14 @@ class ProviderSessionService {
97
177
  this.resolutions.delete(`codex:${agentId}`);
98
178
  return Promise.resolve(false);
99
179
  }
180
+ const startedAt = Number(agent.startedAt) || 0;
181
+ if (
182
+ options.force !== true
183
+ && startedAt
184
+ && Date.now() > startedAt + CODEX_MATCH_WINDOW_MS
185
+ ) {
186
+ return Promise.resolve(false);
187
+ }
100
188
 
101
189
  return this.runResolution(
102
190
  'codex',
@@ -167,15 +255,30 @@ class ProviderSessionService {
167
255
  }
168
256
 
169
257
  async findTemporaryCodexSession(agent) {
170
- const sessions = await this.listCodexSessions({
171
- codexHome: agent.providerHomePath || undefined,
172
- limit: 100,
173
- scanLimit: 1000,
174
- });
258
+ const startedAt = Number(agent.startedAt) || 0;
259
+ const codexHome = agent.providerHomePath || '';
260
+ const scanKey = [
261
+ codexHome,
262
+ ...codexSessionDateKeys(startedAt, CODEX_MATCH_WINDOW_MS),
263
+ ].join('\0');
264
+ let scan = this.codexIdentityScans.get(scanKey);
265
+ if (!scan) {
266
+ scan = Promise.resolve(this.listCodexSessionIdentities({
267
+ codexHome: codexHome || undefined,
268
+ startedAt,
269
+ windowMs: CODEX_MATCH_WINDOW_MS,
270
+ })).finally(() => {
271
+ if (this.codexIdentityScans.get(scanKey) === scan) {
272
+ this.codexIdentityScans.delete(scanKey);
273
+ }
274
+ });
275
+ this.codexIdentityScans.set(scanKey, scan);
276
+ }
277
+ const sessions = await scan;
175
278
  const workspace = normalizePath(
176
- agent?.gitWorktree?.workspace || agent?.projectWorkspace || agent?.cwd || ''
279
+ agent?.projectWorkspace || agent?.cwd || agent?.gitWorktree?.workspace || ''
177
280
  );
178
- const startedAt = Number(agent.startedAt) || 0;
281
+ if (!workspace) return null;
179
282
  const homeId = String(agent.providerHomeId || 'default').trim() || 'default';
180
283
  const claimedSessionIds = new Set([...this.agents.values()]
181
284
  .filter(candidate => candidate?.id !== agent.id
@@ -189,35 +292,36 @@ class ProviderSessionService {
189
292
  if (!session?.id || claimedSessionIds.has(session.id)) return false;
190
293
  const sessionWorkspace = normalizePath(session.workspace || session.cwd);
191
294
  if (workspace && !sessionWorkspace) return false;
192
- const sessionTime = timestampMs(session.createdAt || session.updatedAt);
193
- if (!sessionTime || !startedAt) return true;
194
- return sessionTime >= startedAt - CODEX_MATCH_GRACE_MS;
195
- })
196
- .sort((a, b) => {
197
- const aTime = timestampMs(a.createdAt || a.updatedAt);
198
- const bTime = timestampMs(b.createdAt || b.updatedAt);
199
- const aDistance = startedAt && aTime ? Math.abs(aTime - startedAt) : Number.MAX_SAFE_INTEGER;
200
- const bDistance = startedAt && bTime ? Math.abs(bTime - startedAt) : Number.MAX_SAFE_INTEGER;
201
- if (aDistance !== bDistance) return aDistance - bDistance;
202
- return bTime - aTime;
295
+ const sessionTime = timestampMs(session.createdAt);
296
+ if (!sessionTime || !startedAt) return false;
297
+ return Math.abs(sessionTime - startedAt) <= CODEX_MATCH_WINDOW_MS;
203
298
  });
204
299
 
205
- const exact = candidates.find(session => (
206
- !workspace || workspace === normalizePath(session.workspace || session.cwd)
300
+ const exact = candidates.filter(session => (
301
+ workspace === normalizePath(session.workspace || session.cwd)
207
302
  ));
208
- if (exact) return exact;
209
- if (!workspace) return candidates[0] || null;
210
-
211
- for (const session of candidates.slice(0, 12)) {
212
- const sessionWorkspace = normalizePath(session.workspace || session.cwd);
213
- if (!sessionWorkspace) continue;
214
- if (await this.isLinkedWorktreeOf(workspace, sessionWorkspace)) return session;
215
- }
303
+ if (exact.length === 1) return exact[0];
304
+ if (exact.length > 1) return null;
305
+ const canonicalWorkspace = canonicalPath(workspace);
306
+ const canonical = candidates.filter(session => (
307
+ canonicalWorkspace === canonicalPath(session.workspace || session.cwd)
308
+ ));
309
+ if (canonical.length === 1) return canonical[0];
216
310
  return null;
217
311
  }
218
312
 
219
313
  confirm(agentId, { provider, sessionId, source, title, workspace }) {
220
- const previousSessionId = this.agents.get(agentId)?.providerSessionId || '';
314
+ const current = this.agents.get(agentId);
315
+ const homeId = String(current?.providerHomeId || 'default').trim() || 'default';
316
+ const claimedByAnotherAgent = [...this.agents.values()].some(candidate => (
317
+ candidate?.id !== agentId
318
+ && candidate?.providerSessionProvider === provider
319
+ && candidate?.providerSessionId === sessionId
320
+ && candidate?.providerSessionTemporary !== true
321
+ && (String(candidate.providerHomeId || 'default').trim() || 'default') === homeId
322
+ ));
323
+ if (claimedByAnotherAgent) return false;
324
+ const previousSessionId = current?.providerSessionId || '';
221
325
  const agent = this.bindConfirmed(agentId, provider, sessionId);
222
326
  if (!agent) return false;
223
327
  const providerSessionTitle = String(title || '').trim().slice(0, 160);
package/backend/server.js CHANGED
@@ -50,7 +50,6 @@ const { discoverSlashCommands } = require('./slash-command-discovery');
50
50
  const { FarmingUpdateService } = require('./update-service');
51
51
  const { isRestartBlockingAgent } = require('./agent-activity');
52
52
  const { inputPartsFromMessage } = require('./input-parts');
53
- const { AppServerApiBridge, createAppServerApiRouter } = require('./app-server-api');
54
53
  const { cleanupTerminalRuntime } = require('./terminal-runtime-cleanup');
55
54
  const { QrShareTicketStore, SHARE_TICKET_TTL_MS } = require('./qr-share-tickets');
56
55
  const { ReviewStateStore } = require('./review-state-store');
@@ -123,8 +122,11 @@ const updateService = new FarmingUpdateService({
123
122
  packagedRuntime: Boolean(process.pkg || process.env.FARMING_PACKAGED_RUNTIME === '1'),
124
123
  getUpdateUrl: () => configManager.getSettings().updateUrl || '',
125
124
  });
126
- const appServerApiBridge = new AppServerApiBridge();
127
- const usageMonitor = new UsageMonitor({ agentManager, getProviderHomes: configuredProviderHomes });
125
+ const usageMonitor = new UsageMonitor({
126
+ agentManager,
127
+ configDir: configManager.farmingDir,
128
+ getProviderHomes: configuredProviderHomes,
129
+ });
128
130
  const codexContextWindowReader = new CodexContextWindowReader();
129
131
  const usageSummaryCache = new AsyncCache(() => usageMonitor.getUsageSummary(), {
130
132
  ttlMs: 15_000,
@@ -531,10 +533,6 @@ app.use(routePath(BASE_PATH, '/api/control'), createControlRouter(agentManager,
531
533
  && process.env.FARMING_E2E_FAKE_EXECUTABLES === '1',
532
534
  }));
533
535
 
534
- app.use(routePath(BASE_PATH, '/api/app-server'), createAppServerApiRouter({
535
- bridge: appServerApiBridge,
536
- }));
537
-
538
536
  app.get([
539
537
  BASE_PATH || '/',
540
538
  `${BASE_PATH || ''}/`,
@@ -602,9 +600,21 @@ const IMAGE_ATTACHMENT_EXTENSIONS = {
602
600
  'image/gif': 'gif',
603
601
  'image/webp': 'webp',
604
602
  };
603
+ const AUDIO_ATTACHMENT_EXTENSIONS = {
604
+ 'audio/aac': 'aac',
605
+ 'audio/flac': 'flac',
606
+ 'audio/mp4': 'm4a',
607
+ 'audio/mpeg': 'mp3',
608
+ 'audio/ogg': 'ogg',
609
+ 'audio/wav': 'wav',
610
+ 'audio/wave': 'wav',
611
+ 'audio/webm': 'webm',
612
+ 'audio/x-wav': 'wav',
613
+ };
605
614
  const IMAGE_ATTACHMENT_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
606
615
  const IMAGE_ATTACHMENT_GC_INTERVAL_MS = 60 * 60 * 1000;
607
616
  const IMAGE_ATTACHMENT_FILENAME_RE = /^pasted-image-\d+-[a-f0-9]{8}\.(?:png|jpg|gif|webp)$/;
617
+ const AUDIO_ATTACHMENT_FILENAME_RE = /^pasted-audio-\d+-[a-f0-9]{8}\.(?:aac|flac|m4a|mp3|ogg|wav|webm)$/;
608
618
  let lastImageAttachmentGcAt = 0;
609
619
 
610
620
  function imageAttachmentExtension(contentType) {
@@ -612,6 +622,11 @@ function imageAttachmentExtension(contentType) {
612
622
  return IMAGE_ATTACHMENT_EXTENSIONS[normalized] || '';
613
623
  }
614
624
 
625
+ function audioAttachmentExtension(contentType) {
626
+ const normalized = String(contentType || '').split(';')[0].trim().toLowerCase();
627
+ return AUDIO_ATTACHMENT_EXTENSIONS[normalized] || '';
628
+ }
629
+
615
630
  function imageAttachmentsDir() {
616
631
  return path.join(configManager.farmingDir, 'attachments');
617
632
  }
@@ -634,7 +649,7 @@ async function cleanupExpiredImageAttachments(options = {}) {
634
649
 
635
650
  const cutoff = now - IMAGE_ATTACHMENT_RETENTION_MS;
636
651
  await Promise.all(entries.map(async (entry) => {
637
- if (!entry.isFile() || !IMAGE_ATTACHMENT_FILENAME_RE.test(entry.name)) return;
652
+ if (!entry.isFile() || (!IMAGE_ATTACHMENT_FILENAME_RE.test(entry.name) && !AUDIO_ATTACHMENT_FILENAME_RE.test(entry.name))) return;
638
653
 
639
654
  const filePath = path.join(attachmentsDir, entry.name);
640
655
  try {
@@ -684,6 +699,30 @@ app.post(
684
699
  }
685
700
  );
686
701
 
702
+ app.post(
703
+ routePath(BASE_PATH, '/api/attachments/audio'),
704
+ express.raw({ type: 'audio/*', limit: '25mb' }),
705
+ (req, res) => {
706
+ const contentType = String(req.headers['content-type'] || '').split(';')[0].trim().toLowerCase();
707
+ const extension = audioAttachmentExtension(contentType);
708
+ if (!extension) {
709
+ res.status(415).json({ error: 'unsupported audio type' });
710
+ return;
711
+ }
712
+ if (!Buffer.isBuffer(req.body) || req.body.length === 0) {
713
+ res.status(400).json({ error: 'empty audio attachment' });
714
+ return;
715
+ }
716
+ const attachmentsDir = imageAttachmentsDir();
717
+ fs.mkdirSync(attachmentsDir, { recursive: true });
718
+ const filename = `pasted-audio-${Date.now()}-${crypto.randomBytes(4).toString('hex')}.${extension}`;
719
+ const filePath = path.join(attachmentsDir, filename);
720
+ fs.writeFileSync(filePath, req.body);
721
+ void cleanupExpiredImageAttachments();
722
+ res.status(201).json({ path: filePath, name: filename, type: contentType, size: req.body.length });
723
+ }
724
+ );
725
+
687
726
  app.get(routePath(BASE_PATH, '/api/codex/models'), async (req, res) => {
688
727
  try {
689
728
  const catalog = await codexModelOptionsCache.get('catalog');
@@ -968,8 +1007,8 @@ app.get(routePath(BASE_PATH, '/api/agents/:agentId/session-view'), async (req, r
968
1007
  });
969
1008
 
970
1009
  app.get(routePath(BASE_PATH, '/api/agents/:agentId/codex-transcript'), async (req, res) => {
971
- // Legacy JSONL transcript reader. New App Server Agents use the dedicated
972
- // structured endpoint below; Terminal Agents stay in their terminal UI.
1010
+ // Legacy JSONL transcript reader. ACP Chat uses the dedicated ACP endpoint;
1011
+ // Terminal Agents stay in their terminal UI.
973
1012
  const providerSession = agentManager.getAgentProviderSession(req.params.agentId);
974
1013
  if (!providerSession) {
975
1014
  res.status(404).json({ error: 'Agent not found' });
@@ -999,9 +1038,7 @@ app.get(routePath(BASE_PATH, '/api/agents/:agentId/codex-transcript'), async (re
999
1038
  : DEFAULT_CODEX_TRANSCRIPT_MAX_TURNS;
1000
1039
  const transcript = await readCodexTranscript(providerSession.sessionId, {
1001
1040
  maxTurns,
1002
- codexHome: runtimeKind(providerSession) === 'app-server'
1003
- ? (providerSession.codexAppServerHomePath || providerSession.providerHomePath || '')
1004
- : (providerSession.providerHomePath || ''),
1041
+ codexHome: providerSession.providerHomePath || '',
1005
1042
  });
1006
1043
  res.json({ transcript });
1007
1044
  } catch (error) {
@@ -1010,20 +1047,6 @@ app.get(routePath(BASE_PATH, '/api/agents/:agentId/codex-transcript'), async (re
1010
1047
  }
1011
1048
  });
1012
1049
 
1013
- app.get(routePath(BASE_PATH, '/api/agents/:agentId/codex-app-server-transcript'), async (req, res) => {
1014
- try {
1015
- const requestedMaxTurns = Number.parseInt(String(req.query.maxTurns || ''), 10);
1016
- const maxTurns = Number.isFinite(requestedMaxTurns)
1017
- ? Math.min(MAX_CODEX_TRANSCRIPT_TURNS, Math.max(20, requestedMaxTurns))
1018
- : DEFAULT_CODEX_TRANSCRIPT_MAX_TURNS;
1019
- const transcript = agentManager.getCodexAppServerTranscript(req.params.agentId, { maxTurns });
1020
- res.json({ transcript });
1021
- } catch (error) {
1022
- const message = error && error.message ? error.message : 'Failed to read Codex App Server transcript';
1023
- res.status(/not using the Codex App Server runtime/i.test(message) ? 409 : 500).json({ error: message });
1024
- }
1025
- });
1026
-
1027
1050
  app.get(routePath(BASE_PATH, '/api/agents/:agentId/json-cli-transcript'), async (req, res) => {
1028
1051
  try {
1029
1052
  const transcript = agentManager.getJsonCliTranscript(req.params.agentId, {
@@ -1191,6 +1214,15 @@ app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-session/authenticate'),
1191
1214
  }
1192
1215
  });
1193
1216
 
1217
+ app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-session/logout'), async (req, res) => {
1218
+ try {
1219
+ res.json(await agentManager.logoutAcpAgent(req.params.agentId));
1220
+ } catch (error) {
1221
+ const message = error && error.message ? error.message : 'Failed to log out ACP Agent';
1222
+ res.status(message === 'Agent not found' ? 404 : 409).json({ error: message });
1223
+ }
1224
+ });
1225
+
1194
1226
  app.post(routePath(BASE_PATH, '/api/agents/:agentId/acp-session/fork'), express.json(), async (req, res) => {
1195
1227
  try {
1196
1228
  res.json(await agentManager.forkAcpSession(req.params.agentId, req.body || {}));
@@ -1243,41 +1275,6 @@ app.patch(routePath(BASE_PATH, '/api/agents/:agentId/acp-session'), express.json
1243
1275
  }
1244
1276
  });
1245
1277
 
1246
- app.get(routePath(BASE_PATH, '/api/agents/:agentId/codex-goal'), async (req, res) => {
1247
- try {
1248
- const goal = await agentManager.getCodexAppServerGoal(req.params.agentId);
1249
- res.json({ goal });
1250
- } catch (error) {
1251
- const message = error && error.message ? error.message : 'Failed to read Codex goal';
1252
- res.status(message === 'Agent not found' ? 404 : 400).json({ error: message });
1253
- }
1254
- });
1255
-
1256
- app.patch(routePath(BASE_PATH, '/api/agents/:agentId/codex-goal'), express.json(), async (req, res) => {
1257
- try {
1258
- const body = req.body || {};
1259
- const goal = await agentManager.setCodexAppServerGoal(req.params.agentId, {
1260
- objective: typeof body.objective === 'string' ? body.objective : undefined,
1261
- status: typeof body.status === 'string' ? body.status : undefined,
1262
- ...(Object.prototype.hasOwnProperty.call(body, 'tokenBudget') ? { tokenBudget: body.tokenBudget } : {}),
1263
- });
1264
- res.json({ goal });
1265
- } catch (error) {
1266
- const message = error && error.message ? error.message : 'Failed to update Codex goal';
1267
- res.status(message === 'Agent not found' ? 404 : 400).json({ error: message });
1268
- }
1269
- });
1270
-
1271
- app.delete(routePath(BASE_PATH, '/api/agents/:agentId/codex-goal'), async (req, res) => {
1272
- try {
1273
- await agentManager.clearCodexAppServerGoal(req.params.agentId);
1274
- res.json({ goal: null });
1275
- } catch (error) {
1276
- const message = error && error.message ? error.message : 'Failed to clear Codex goal';
1277
- res.status(message === 'Agent not found' ? 404 : 400).json({ error: message });
1278
- }
1279
- });
1280
-
1281
1278
  app.post(routePath(BASE_PATH, '/api/agents/:agentId/codex-terminal-profile'), express.json(), async (req, res) => {
1282
1279
  const requestAbort = new globalThis.AbortController();
1283
1280
  const abortRequest = () => requestAbort.abort(new Error('Terminal profile request was canceled'));
@@ -1710,7 +1707,7 @@ async function resumeAgentSessionById(provider, rawSessionId, options = {}) {
1710
1707
  requiredCliVersion: normalizedProvider === 'codex' && session ? session.cliVersion : '',
1711
1708
  projectWorkspace: session ? (session.workspace || session.cwd || '') : '',
1712
1709
  source: shouldFork ? resumeSource.replace('-history:', '-history-fork:') : resumeSource,
1713
- agentRuntimeMode: options.agentRuntimeMode === 'acp' ? 'acp' : 'terminal',
1710
+ agentRuntimeMode: ['chat', 'acp'].includes(options.agentRuntimeMode) ? 'chat' : 'terminal',
1714
1711
  acpHistoryMode: options.acpHistoryMode === 'resume' ? 'resume' : 'load',
1715
1712
  providerHomeId: resolvedProviderHomeId,
1716
1713
  providerHomePath: session ? (session.providerHomePath || '') : '',
@@ -1754,7 +1751,7 @@ async function startResumedAgentSession(req, res, provider, rawSessionId) {
1754
1751
  allowUnarchiveArchived,
1755
1752
  providerHomeId: req.body && typeof req.body.providerHomeId === 'string' ? req.body.providerHomeId : '',
1756
1753
  customTitle: req.body && typeof req.body.customTitle === 'string' ? req.body.customTitle : '',
1757
- agentRuntimeMode: req.body && req.body.agentRuntimeMode === 'acp' ? 'acp' : 'terminal',
1754
+ agentRuntimeMode: req.body && ['chat', 'acp'].includes(req.body.agentRuntimeMode) ? 'chat' : 'terminal',
1758
1755
  acpHistoryMode: req.body && req.body.acpHistoryMode === 'resume' ? 'resume' : 'load',
1759
1756
  });
1760
1757
 
@@ -2018,66 +2015,66 @@ async function sendInputMessage(ws, data) {
2018
2015
 
2019
2016
  async function sendComposerInputMessage(ws, data) {
2020
2017
  const targetAgentId = resolveInputTargetAgentId(ws, data);
2018
+ const requestId = typeof data.requestId === 'string' ? data.requestId : '';
2019
+ const responseAgentId = targetAgentId || (typeof data.agentId === 'string' ? data.agentId : '');
2020
+ const respond = (accepted, message = '') => {
2021
+ if (!requestId || !responseAgentId) return;
2022
+ ws.send(JSON.stringify({
2023
+ type: 'composer-input-result',
2024
+ requestId,
2025
+ agentId: responseAgentId,
2026
+ accepted,
2027
+ ...(message ? { message } : {}),
2028
+ }));
2029
+ };
2021
2030
  const message = typeof data.message === 'string' ? data.message : '';
2022
2031
  const content = [];
2023
2032
  if (message.trim()) content.push({ type: 'text', text: message });
2024
2033
  const attachmentsRoot = path.resolve(imageAttachmentsDir());
2025
2034
  const attachments = Array.isArray(data.attachments) ? data.attachments.slice(0, 8) : [];
2026
2035
  for (const attachment of attachments) {
2027
- if (attachment?.kind !== 'image' || typeof attachment.path !== 'string') continue;
2036
+ if (!['image', 'audio'].includes(attachment?.kind) || typeof attachment.path !== 'string') continue;
2028
2037
  const filePath = path.resolve(attachment.path);
2029
2038
  if (!filePath.startsWith(`${attachmentsRoot}${path.sep}`)) continue;
2030
- const mimeType = typeof attachment.type === 'string' && /^image\/(?:png|jpe?g|gif|webp)$/i.test(attachment.type)
2039
+ const mimeType = typeof attachment.type === 'string' && attachment.kind === 'image' && /^image\/(?:png|jpe?g|gif|webp)$/i.test(attachment.type)
2031
2040
  ? attachment.type.toLowerCase()
2032
- : '';
2041
+ : (typeof attachment.type === 'string' && attachment.kind === 'audio' && Object.hasOwn(AUDIO_ATTACHMENT_EXTENSIONS, attachment.type.toLowerCase())
2042
+ ? attachment.type.toLowerCase()
2043
+ : '');
2033
2044
  if (!mimeType) continue;
2034
2045
  try {
2035
2046
  const data = await fs.promises.readFile(filePath);
2036
2047
  if (data.length === 0 || data.length > 12 * 1024 * 1024) continue;
2037
2048
  content.push({
2038
- type: 'image',
2049
+ type: attachment.kind,
2039
2050
  data: data.toString('base64'),
2040
2051
  mimeType,
2052
+ path: filePath,
2041
2053
  uri: pathToFileURL(filePath).href,
2042
2054
  });
2043
2055
  } catch {
2044
2056
  // The text fallback already explains failed or unavailable uploads.
2045
2057
  }
2046
2058
  }
2047
- if (!targetAgentId || content.length === 0) return;
2059
+ if (!targetAgentId || content.length === 0) {
2060
+ respond(false, 'Composer message is empty');
2061
+ return;
2062
+ }
2048
2063
  const targetAgent = agentManager.getState().agents.find(agent => agent.id === targetAgentId);
2049
2064
  const structuredRuntime = targetAgent && runtimeKind(targetAgent) !== 'terminal';
2050
2065
  if (!structuredRuntime) {
2051
- ws.send(JSON.stringify({
2052
- type: 'error',
2053
- message: 'Terminal Composer input requires the active terminal owner',
2054
- }));
2066
+ const error = 'Terminal Composer input requires the active terminal owner';
2067
+ if (requestId) respond(false, error);
2068
+ else ws.send(JSON.stringify({ type: 'error', message: error }));
2055
2069
  return;
2056
2070
  }
2057
2071
  try {
2058
2072
  await agentManager.sendComposerMessage(targetAgentId, content);
2073
+ respond(true);
2059
2074
  } catch (error) {
2060
- ws.send(JSON.stringify({
2061
- type: 'error',
2062
- message: error && error.message ? error.message : 'Failed to send Composer message',
2063
- }));
2064
- }
2065
- }
2066
-
2067
- async function respondToAppServerRequest(ws, data) {
2068
- const targetAgentId = resolveInputTargetAgentId(ws, data);
2069
- const requestId = typeof data.requestId === 'string' ? data.requestId : '';
2070
- if (!targetAgentId || !requestId) return;
2071
- try {
2072
- agentManager.respondToCodexAppServerRequest(targetAgentId, requestId, data.result, {
2073
- reject: data.reject === true,
2074
- reason: typeof data.reason === 'string' ? data.reason : '',
2075
- });
2076
- } catch (error) {
2077
- ws.send(JSON.stringify({
2078
- type: 'error',
2079
- message: error && error.message ? error.message : 'Failed to respond to Codex App Server request',
2080
- }));
2075
+ const message = error && error.message ? error.message : 'Failed to send Composer message';
2076
+ if (requestId) respond(false, message);
2077
+ else ws.send(JSON.stringify({ type: 'error', message }));
2081
2078
  }
2082
2079
  }
2083
2080
 
@@ -2107,12 +2104,11 @@ function handleMessage(ws, data) {
2107
2104
  workflowTemplate: typeof data.workflowTemplate === 'string' ? data.workflowTemplate : '',
2108
2105
  customTitle: typeof data.customTitle === 'string' ? data.customTitle : '',
2109
2106
  codexApprovalMode: typeof data.codexApprovalMode === 'string' ? data.codexApprovalMode : undefined,
2110
- codexRuntimeMode: data.codexRuntimeMode === 'app-server' || data.codexRuntimeMode === 'cli'
2111
- ? data.codexRuntimeMode
2112
- : undefined,
2113
- agentRuntimeMode: ['json', 'acp'].includes(data.agentRuntimeMode) ? data.agentRuntimeMode : 'terminal',
2107
+ agentRuntimeMode: ['json', 'acp', 'chat'].includes(data.agentRuntimeMode) ? data.agentRuntimeMode : 'terminal',
2114
2108
  acpHistoryMode: data.acpHistoryMode === 'resume' ? 'resume' : 'load',
2115
2109
  providerHomeId: typeof data.providerHomeId === 'string' ? data.providerHomeId : '',
2110
+ ...(Array.isArray(data.additionalDirectories) ? { additionalDirectories: data.additionalDirectories } : {}),
2111
+ ...(Array.isArray(data.mcpServers) ? { mcpServers: data.mcpServers } : {}),
2116
2112
  ...(data.dangerouslySkipPermissions === true ? { dangerouslySkipPermissions: true } : {}),
2117
2113
  });
2118
2114
  break;
@@ -2129,12 +2125,6 @@ function handleMessage(ws, data) {
2129
2125
  }
2130
2126
  break;
2131
2127
 
2132
- case 'app-server-request-response':
2133
- {
2134
- void respondToAppServerRequest(ws, data);
2135
- }
2136
- break;
2137
-
2138
2128
  case 'acp-permission-response':
2139
2129
  try {
2140
2130
  agentManager.respondToAcpPermission(
@@ -2717,7 +2707,6 @@ module.exports = {
2717
2707
  server,
2718
2708
  wss,
2719
2709
  agentManager,
2720
- appServerApiBridge,
2721
2710
  workspaceFileService,
2722
2711
  handleMessage,
2723
2712
  resolveCliBinDir,
@@ -1,6 +1,5 @@
1
1
  const os = require('os');
2
2
  const path = require('path');
3
- const crypto = require('crypto');
4
3
 
5
4
  function farmingConfigDir(env = process.env) {
6
5
  return env.FARMING_CONFIG_DIR || path.join(env.HOME || os.homedir(), '.farming');
@@ -42,6 +41,10 @@ function reviewSessionsFile(configDir) {
42
41
  return path.join(historyDir(configDir), 'review-sessions.json');
43
42
  }
44
43
 
44
+ function usageHistoryCacheFile(configDir) {
45
+ return path.join(historyDir(configDir), 'usage-history-v2.sqlite3');
46
+ }
47
+
45
48
  function sessionIndexFile(configDir) {
46
49
  return path.join(sessionsDir(configDir), 'index.json');
47
50
  }
@@ -110,23 +113,8 @@ function farmingNetTrustFile(configDir) {
110
113
  return path.join(configDir, 'farming-net-trust.json');
111
114
  }
112
115
 
113
- function codexAppServerRuntimeDir(configDir) {
114
- return path.join(configDir, 'c');
115
- }
116
-
117
- function codexAppServerAgentHome(configDir, agentId) {
118
- const safeAgentId = String(agentId || '').trim();
119
- if (!/^agent-[A-Za-z0-9_-]+$/.test(safeAgentId)) {
120
- throw new Error('Invalid Codex App Server agent id');
121
- }
122
- const homeId = crypto.createHash('sha256').update(safeAgentId).digest('hex').slice(0, 16);
123
- return path.join(codexAppServerRuntimeDir(configDir), homeId);
124
- }
125
-
126
116
  module.exports = {
127
117
  acpCheckpointsDir,
128
- codexAppServerAgentHome,
129
- codexAppServerRuntimeDir,
130
118
  farmingNetInstancesFile,
131
119
  farmingNetServerLogFile,
132
120
  farmingNetServerStateFile,
@@ -153,4 +141,5 @@ module.exports = {
153
141
  themeSettingsFile,
154
142
  updateLogFile,
155
143
  updateStateFile,
144
+ usageHistoryCacheFile,
156
145
  };