farming-code 2.2.16 → 2.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/README.zh_cn.md +2 -2
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/backend/acp-runtime.js +221 -10
- package/backend/agent-manager.js +477 -54
- package/backend/agent-provider-session.js +15 -1
- package/backend/codex-rollout-follower.js +83 -0
- package/backend/codex-session-archive.js +12 -1
- package/backend/codex-session-history.js +94 -0
- package/backend/config-manager.js +3 -3
- package/backend/farming-app-cli.js +8 -0
- package/backend/native-pty-host-client.js +59 -2
- package/backend/provider-adapters.js +117 -36
- package/backend/provider-session-service.js +138 -34
- package/backend/server.js +5 -1
- package/backend/storage-layout.js +5 -0
- package/backend/usage-history-client.js +305 -0
- package/backend/usage-history-scanner.generated.js +2250 -0
- package/backend/usage-history-scanner.ts +2718 -0
- package/backend/usage-history-smoke.js +64 -0
- package/backend/usage-history-worker.js +34 -0
- package/backend/usage-monitor.js +188 -588
- package/backend/vendor/usage-parsers/CC_STATISTICS_LICENSE +21 -0
- package/backend/vendor/usage-parsers/CODEXBAR_LICENSE +21 -0
- package/backend/vendor/usage-parsers/VENDOR.json +45 -0
- package/dist/assets/App-DTAQilnX.js +210 -0
- package/dist/assets/code-dark-5esj0bRu.css +1 -0
- package/dist/assets/{index-iKBvPjWS.js → index-fp8VhOes.js} +2 -2
- package/dist/assets/main-DSO3i_Gs.css +1 -0
- package/dist/index.html +1 -1
- package/frontend/skins/crt/app.js +9 -3
- package/package.json +12 -8
- package/shared/backend-connection-status.d.ts +25 -0
- package/shared/backend-connection-status.js +40 -0
- package/dist/assets/App-y6p2yRar.js +0 -206
- package/dist/assets/code-dark-DKBGxoPX.css +0 -1
- package/dist/assets/main-CdkLOXYl.css +0 -1
|
@@ -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 {
|
|
4
|
-
|
|
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
|
|
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.
|
|
43
|
+
this.listCodexSessionIdentities = options.listCodexSessionIdentities || listCodexSessionIdentities;
|
|
30
44
|
this.findAgentSession = options.findAgentSession || findAgentSession;
|
|
31
|
-
this.
|
|
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
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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?.
|
|
279
|
+
agent?.projectWorkspace || agent?.cwd || agent?.gitWorktree?.workspace || ''
|
|
177
280
|
);
|
|
178
|
-
|
|
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
|
|
193
|
-
if (!sessionTime || !startedAt) return
|
|
194
|
-
return sessionTime
|
|
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.
|
|
206
|
-
|
|
300
|
+
const exact = candidates.filter(session => (
|
|
301
|
+
workspace === normalizePath(session.workspace || session.cwd)
|
|
207
302
|
));
|
|
208
|
-
if (exact) return exact;
|
|
209
|
-
if (
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
|
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
|
@@ -122,7 +122,11 @@ const updateService = new FarmingUpdateService({
|
|
|
122
122
|
packagedRuntime: Boolean(process.pkg || process.env.FARMING_PACKAGED_RUNTIME === '1'),
|
|
123
123
|
getUpdateUrl: () => configManager.getSettings().updateUrl || '',
|
|
124
124
|
});
|
|
125
|
-
const usageMonitor = new UsageMonitor({
|
|
125
|
+
const usageMonitor = new UsageMonitor({
|
|
126
|
+
agentManager,
|
|
127
|
+
configDir: configManager.farmingDir,
|
|
128
|
+
getProviderHomes: configuredProviderHomes,
|
|
129
|
+
});
|
|
126
130
|
const codexContextWindowReader = new CodexContextWindowReader();
|
|
127
131
|
const usageSummaryCache = new AsyncCache(() => usageMonitor.getUsageSummary(), {
|
|
128
132
|
ttlMs: 15_000,
|
|
@@ -41,6 +41,10 @@ function reviewSessionsFile(configDir) {
|
|
|
41
41
|
return path.join(historyDir(configDir), 'review-sessions.json');
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
function usageHistoryCacheFile(configDir) {
|
|
45
|
+
return path.join(historyDir(configDir), 'usage-history-v2.sqlite3');
|
|
46
|
+
}
|
|
47
|
+
|
|
44
48
|
function sessionIndexFile(configDir) {
|
|
45
49
|
return path.join(sessionsDir(configDir), 'index.json');
|
|
46
50
|
}
|
|
@@ -137,4 +141,5 @@ module.exports = {
|
|
|
137
141
|
themeSettingsFile,
|
|
138
142
|
updateLogFile,
|
|
139
143
|
updateStateFile,
|
|
144
|
+
usageHistoryCacheFile,
|
|
140
145
|
};
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const { Worker } = require('worker_threads');
|
|
4
|
+
const { usageHistoryCacheFile } = require('./storage-layout');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
7
|
+
const DEFAULT_RETENTION_DAYS = 52 * 7;
|
|
8
|
+
const DEFAULT_RECENT_RAW_MS = 24 * 60 * 60 * 1000;
|
|
9
|
+
const DEFAULT_SCAN_BUDGET_MS = 5_000;
|
|
10
|
+
const BACKGROUND_SCAN_DELAY_MS = 100;
|
|
11
|
+
const RESULT_REUSE_MS = 2_000;
|
|
12
|
+
const SOURCE_WORKER_FILE = 'usage-history-worker.js';
|
|
13
|
+
const PACKAGED_WORKER_FILE = 'usage-history-worker.pkg.js';
|
|
14
|
+
let sharedWorkerSession = null;
|
|
15
|
+
let nextWorkerRequestId = 1;
|
|
16
|
+
|
|
17
|
+
function resolveWorkerFile() {
|
|
18
|
+
if (!process.pkg && process.env.FARMING_PACKAGED_RUNTIME !== '1') {
|
|
19
|
+
return SOURCE_WORKER_FILE;
|
|
20
|
+
}
|
|
21
|
+
const packaged = path.join(__dirname, PACKAGED_WORKER_FILE);
|
|
22
|
+
return fs.existsSync(packaged) ? PACKAGED_WORKER_FILE : SOURCE_WORKER_FILE;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function runUsageWorker(request, options = {}) {
|
|
26
|
+
if (options.WorkerClass) {
|
|
27
|
+
return runOneShotUsageWorker(request, options);
|
|
28
|
+
}
|
|
29
|
+
const workerFile = path.join(__dirname, resolveWorkerFile());
|
|
30
|
+
if (!sharedWorkerSession || sharedWorkerSession.workerFile !== workerFile) {
|
|
31
|
+
sharedWorkerSession?.terminate();
|
|
32
|
+
sharedWorkerSession = createSharedWorkerSession(workerFile, options.WorkerClass || Worker);
|
|
33
|
+
}
|
|
34
|
+
return sharedWorkerSession.run(request, options.timeoutMs || DEFAULT_TIMEOUT_MS);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function runOneShotUsageWorker(request, options = {}) {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const worker = new (options.WorkerClass || Worker)(
|
|
40
|
+
path.join(__dirname, resolveWorkerFile()),
|
|
41
|
+
{ workerData: { request } },
|
|
42
|
+
);
|
|
43
|
+
let settled = false;
|
|
44
|
+
const finish = (error, value) => {
|
|
45
|
+
if (settled) return;
|
|
46
|
+
settled = true;
|
|
47
|
+
clearTimeout(timeout);
|
|
48
|
+
worker.terminate().catch(() => {});
|
|
49
|
+
if (error) reject(error);
|
|
50
|
+
else resolve(value);
|
|
51
|
+
};
|
|
52
|
+
const timeout = setTimeout(() => {
|
|
53
|
+
const error = new Error(
|
|
54
|
+
`TypeScript usage scanner exceeded ${options.timeoutMs || DEFAULT_TIMEOUT_MS}ms`,
|
|
55
|
+
);
|
|
56
|
+
error.code = 'ETIMEDOUT';
|
|
57
|
+
finish(error);
|
|
58
|
+
}, options.timeoutMs || DEFAULT_TIMEOUT_MS);
|
|
59
|
+
timeout.unref?.();
|
|
60
|
+
worker.once('error', finish);
|
|
61
|
+
worker.once('exit', (code) => {
|
|
62
|
+
if (!settled) {
|
|
63
|
+
const error = new Error(
|
|
64
|
+
code === 0
|
|
65
|
+
? 'TypeScript usage scanner exited without returning a result'
|
|
66
|
+
: `TypeScript usage scanner exited with code ${code}`,
|
|
67
|
+
);
|
|
68
|
+
error.code = 'EUSAGEWORKER';
|
|
69
|
+
finish(error);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
worker.once('message', (message) => {
|
|
73
|
+
if (message?.error) {
|
|
74
|
+
const error = new Error(message.error.message || 'Usage scanner failed');
|
|
75
|
+
error.code = message.error.code || 'EUSAGE';
|
|
76
|
+
error.stack = message.error.stack || error.stack;
|
|
77
|
+
finish(error);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
finish(null, message?.result);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function createSharedWorkerSession(workerFile, WorkerClass) {
|
|
86
|
+
const worker = new WorkerClass(workerFile);
|
|
87
|
+
const pending = new Map();
|
|
88
|
+
const failAll = (error) => {
|
|
89
|
+
for (const request of pending.values()) {
|
|
90
|
+
clearTimeout(request.timeout);
|
|
91
|
+
request.reject(error);
|
|
92
|
+
}
|
|
93
|
+
pending.clear();
|
|
94
|
+
if (sharedWorkerSession?.worker === worker) sharedWorkerSession = null;
|
|
95
|
+
};
|
|
96
|
+
worker.on('message', (message) => {
|
|
97
|
+
const request = pending.get(message?.requestId);
|
|
98
|
+
if (!request) return;
|
|
99
|
+
pending.delete(message.requestId);
|
|
100
|
+
clearTimeout(request.timeout);
|
|
101
|
+
if (message?.error) {
|
|
102
|
+
const error = new Error(message.error.message || 'Usage scanner failed');
|
|
103
|
+
error.code = message.error.code || 'EUSAGE';
|
|
104
|
+
error.stack = message.error.stack || error.stack;
|
|
105
|
+
request.reject(error);
|
|
106
|
+
} else {
|
|
107
|
+
request.resolve(message?.result);
|
|
108
|
+
}
|
|
109
|
+
if (pending.size === 0) worker.unref?.();
|
|
110
|
+
});
|
|
111
|
+
worker.once('error', failAll);
|
|
112
|
+
worker.once('exit', (code) => {
|
|
113
|
+
if (pending.size > 0) {
|
|
114
|
+
const error = new Error(
|
|
115
|
+
code === 0
|
|
116
|
+
? 'TypeScript usage scanner exited before returning all results'
|
|
117
|
+
: `TypeScript usage scanner exited with code ${code}`,
|
|
118
|
+
);
|
|
119
|
+
error.code = 'EUSAGEWORKER';
|
|
120
|
+
failAll(error);
|
|
121
|
+
} else if (sharedWorkerSession?.worker === worker) {
|
|
122
|
+
sharedWorkerSession = null;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
worker.unref?.();
|
|
126
|
+
return {
|
|
127
|
+
worker,
|
|
128
|
+
workerFile,
|
|
129
|
+
run(request, timeoutMs) {
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
131
|
+
const requestId = nextWorkerRequestId++;
|
|
132
|
+
const timeout = setTimeout(() => {
|
|
133
|
+
if (!pending.delete(requestId)) return;
|
|
134
|
+
const error = new Error(`TypeScript usage scanner exceeded ${timeoutMs}ms`);
|
|
135
|
+
error.code = 'ETIMEDOUT';
|
|
136
|
+
reject(error);
|
|
137
|
+
worker.terminate().catch(() => {});
|
|
138
|
+
if (sharedWorkerSession?.worker === worker) sharedWorkerSession = null;
|
|
139
|
+
}, timeoutMs);
|
|
140
|
+
timeout.unref?.();
|
|
141
|
+
pending.set(requestId, { resolve, reject, timeout });
|
|
142
|
+
worker.ref?.();
|
|
143
|
+
worker.postMessage({ requestId, request });
|
|
144
|
+
});
|
|
145
|
+
},
|
|
146
|
+
terminate() {
|
|
147
|
+
const error = new Error('Usage scanner worker was replaced');
|
|
148
|
+
error.code = 'EUSAGEWORKER';
|
|
149
|
+
failAll(error);
|
|
150
|
+
worker.terminate().catch(() => {});
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
class UsageHistoryClient {
|
|
156
|
+
constructor(options = {}) {
|
|
157
|
+
this.configDir = options.configDir;
|
|
158
|
+
this.timeoutMs = options.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
159
|
+
this.runner = options.runner || runUsageWorker;
|
|
160
|
+
this.pending = null;
|
|
161
|
+
this.pendingKey = '';
|
|
162
|
+
this.cached = null;
|
|
163
|
+
this.cachedAt = 0;
|
|
164
|
+
this.cacheKey = '';
|
|
165
|
+
this.backgroundTimer = null;
|
|
166
|
+
this.backgroundGeneration = 0;
|
|
167
|
+
this.backgroundDelayMs = options.backgroundDelayMs || BACKGROUND_SCAN_DELAY_MS;
|
|
168
|
+
this.backgroundErrorDelayMs = options.backgroundErrorDelayMs || 5_000;
|
|
169
|
+
this.backgroundErrorRetries = 0;
|
|
170
|
+
this.backgroundStalls = 0;
|
|
171
|
+
this.backgroundProgressSignature = '';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
invoke(request) {
|
|
175
|
+
return this.runner(request, { timeoutMs: this.timeoutMs });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
storeResult(result, cacheKey) {
|
|
179
|
+
if (cacheKey !== this.cacheKey) return;
|
|
180
|
+
this.cached = result;
|
|
181
|
+
this.cachedAt = Number.isFinite(result?.sampledAt)
|
|
182
|
+
? result.sampledAt
|
|
183
|
+
: Date.now();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
scheduleBackgroundScan(request, cacheKey, result) {
|
|
187
|
+
if (result?.cache?.scan_complete !== false || cacheKey !== this.cacheKey) {
|
|
188
|
+
this.backgroundErrorRetries = 0;
|
|
189
|
+
this.backgroundStalls = 0;
|
|
190
|
+
this.backgroundProgressSignature = '';
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (this.backgroundTimer) return;
|
|
194
|
+
const hasErrors = Number(result?.cache?.errors) > 0;
|
|
195
|
+
this.backgroundErrorRetries = hasErrors ? this.backgroundErrorRetries + 1 : 0;
|
|
196
|
+
const progressSignature = [
|
|
197
|
+
result?.cache?.pending_directories,
|
|
198
|
+
result?.cache?.pending_files,
|
|
199
|
+
result?.cache?.discovered_files,
|
|
200
|
+
result?.cache?.committed_bytes,
|
|
201
|
+
].join(':');
|
|
202
|
+
if (Number(result?.cache?.enumerated_entries) > 0) {
|
|
203
|
+
this.backgroundProgressSignature = progressSignature;
|
|
204
|
+
this.backgroundStalls = 0;
|
|
205
|
+
} else if (progressSignature === this.backgroundProgressSignature) {
|
|
206
|
+
this.backgroundStalls += 1;
|
|
207
|
+
} else {
|
|
208
|
+
this.backgroundProgressSignature = progressSignature;
|
|
209
|
+
this.backgroundStalls = 0;
|
|
210
|
+
}
|
|
211
|
+
if (this.backgroundErrorRetries >= 3 || this.backgroundStalls >= 3) return;
|
|
212
|
+
const generation = this.backgroundGeneration;
|
|
213
|
+
const retryDelayMs = hasErrors
|
|
214
|
+
? this.backgroundErrorDelayMs
|
|
215
|
+
: this.backgroundDelayMs;
|
|
216
|
+
this.backgroundTimer = setTimeout(() => {
|
|
217
|
+
this.backgroundTimer = null;
|
|
218
|
+
if (generation !== this.backgroundGeneration || cacheKey !== this.cacheKey) return;
|
|
219
|
+
const backgroundRequest = { ...request, nowMs: Date.now() };
|
|
220
|
+
const pending = this.invoke(backgroundRequest).then((nextResult) => {
|
|
221
|
+
this.storeResult(nextResult, cacheKey);
|
|
222
|
+
this.scheduleBackgroundScan(backgroundRequest, cacheKey, nextResult);
|
|
223
|
+
return nextResult;
|
|
224
|
+
}).catch(() => {
|
|
225
|
+
// Keep the last usable snapshot. A later normal request retries.
|
|
226
|
+
}).finally(() => {
|
|
227
|
+
if (this.pending === pending) {
|
|
228
|
+
this.pending = null;
|
|
229
|
+
this.pendingKey = '';
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
this.pending = pending;
|
|
233
|
+
this.pendingKey = cacheKey;
|
|
234
|
+
}, retryDelayMs);
|
|
235
|
+
this.backgroundTimer.unref?.();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
collect(options = {}) {
|
|
239
|
+
const now = options.now ?? Date.now();
|
|
240
|
+
const retentionDays = options.retentionDays ?? DEFAULT_RETENTION_DAYS;
|
|
241
|
+
const roots = {
|
|
242
|
+
codex: Array.from(new Set(options.codexRoots || [])).sort(),
|
|
243
|
+
claude: Array.from(new Set(options.claudeRoots || [])).sort(),
|
|
244
|
+
};
|
|
245
|
+
const recentRawMs = options.recentRawMs ?? DEFAULT_RECENT_RAW_MS;
|
|
246
|
+
const cacheKey = JSON.stringify({ roots, retentionDays, recentRawMs });
|
|
247
|
+
if (this.cacheKey && this.cacheKey !== cacheKey) {
|
|
248
|
+
this.backgroundGeneration += 1;
|
|
249
|
+
if (this.backgroundTimer) clearTimeout(this.backgroundTimer);
|
|
250
|
+
this.backgroundTimer = null;
|
|
251
|
+
this.backgroundErrorRetries = 0;
|
|
252
|
+
this.backgroundStalls = 0;
|
|
253
|
+
this.backgroundProgressSignature = '';
|
|
254
|
+
}
|
|
255
|
+
if (
|
|
256
|
+
options.fresh !== true
|
|
257
|
+
&& this.cached
|
|
258
|
+
&& this.cacheKey === cacheKey
|
|
259
|
+
&& (
|
|
260
|
+
now - this.cachedAt <= RESULT_REUSE_MS
|
|
261
|
+
|| this.backgroundTimer
|
|
262
|
+
|| (this.pending && this.pendingKey === cacheKey)
|
|
263
|
+
)
|
|
264
|
+
) {
|
|
265
|
+
return Promise.resolve(this.cached);
|
|
266
|
+
}
|
|
267
|
+
if (this.pending && this.pendingKey === cacheKey) return this.pending;
|
|
268
|
+
if (this.pending) return this.pending.then(() => this.collect(options));
|
|
269
|
+
this.cacheKey = cacheKey;
|
|
270
|
+
this.backgroundErrorRetries = 0;
|
|
271
|
+
this.backgroundStalls = 0;
|
|
272
|
+
this.backgroundProgressSignature = '';
|
|
273
|
+
const request = {
|
|
274
|
+
cacheFile: usageHistoryCacheFile(this.configDir),
|
|
275
|
+
legacyCacheFile: options.legacyCacheFile,
|
|
276
|
+
nowMs: now,
|
|
277
|
+
retentionDays,
|
|
278
|
+
recentRawMs,
|
|
279
|
+
scanBudgetMs: options.scanBudgetMs ?? DEFAULT_SCAN_BUDGET_MS,
|
|
280
|
+
roots,
|
|
281
|
+
};
|
|
282
|
+
const pending = this.invoke(request).then((result) => {
|
|
283
|
+
this.storeResult(result, cacheKey);
|
|
284
|
+
this.scheduleBackgroundScan(request, cacheKey, result);
|
|
285
|
+
return result;
|
|
286
|
+
}).finally(() => {
|
|
287
|
+
if (this.pending === pending) {
|
|
288
|
+
this.pending = null;
|
|
289
|
+
this.pendingKey = '';
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
this.pending = pending;
|
|
293
|
+
this.pendingKey = cacheKey;
|
|
294
|
+
return pending;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
module.exports = {
|
|
299
|
+
UsageHistoryClient,
|
|
300
|
+
DEFAULT_RECENT_RAW_MS,
|
|
301
|
+
DEFAULT_RETENTION_DAYS,
|
|
302
|
+
DEFAULT_SCAN_BUDGET_MS,
|
|
303
|
+
resolveWorkerFile,
|
|
304
|
+
runUsageWorker,
|
|
305
|
+
};
|