farming-code 2.2.8 → 2.2.12
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 +42 -28
- package/README.zh_cn.md +42 -28
- package/THIRD_PARTY_NOTICES.md +11 -2
- package/backend/acp-checkpoint-store.js +198 -0
- package/backend/acp-runtime.js +306 -83
- package/backend/acp-session-state.js +202 -6
- package/backend/acp-transcript.js +112 -0
- package/backend/agent-activity.js +6 -157
- package/backend/agent-manager.js +1592 -741
- package/backend/agent-provider-session.js +17 -242
- package/backend/agent-runtime-binding.js +219 -0
- package/backend/agent-session-history.js +66 -1
- package/backend/auth.js +79 -6
- package/backend/codex-models.js +81 -84
- package/backend/codex-session-archive.js +54 -0
- package/backend/codex-terminal-profile.js +500 -0
- package/backend/codex-transcript-sanitizer.js +12 -0
- package/backend/codex-transcript.js +230 -8
- package/backend/config-manager.js +35 -0
- package/backend/control-api.js +192 -17
- package/backend/executable-discovery.js +2 -0
- package/backend/farming-net-pass.js +285 -0
- package/backend/farming-net-registry.js +112 -0
- package/backend/farming-net-server.js +298 -0
- package/backend/farming-session-store.js +5 -13
- package/backend/git-worktree-info.js +181 -0
- package/backend/local-session-engine.js +411 -186
- package/backend/main-page-session.js +5 -2
- package/backend/native-pty-controller-generation.js +106 -0
- package/backend/native-pty-host-client.js +275 -7
- package/backend/native-pty-host-identity.js +86 -0
- package/backend/native-pty-host.js +813 -114
- package/backend/native-session-engine.js +100 -28
- package/backend/packaged-node-pty.js +22 -2
- package/backend/provider-adapters.js +253 -0
- package/backend/provider-session-service.js +241 -0
- package/backend/runtime-observation.js +81 -0
- package/backend/server.js +354 -84
- package/backend/session-engine-bridge.js +21 -2
- package/backend/session-engine-router.js +1 -1
- package/backend/session-engine.js +1 -1
- package/backend/session-stream-protocol.js +185 -0
- package/backend/storage-layout.js +55 -0
- package/backend/terminal-attach-checkpoint.js +74 -0
- package/backend/terminal-exit-quiescence.js +39 -0
- package/backend/terminal-reducer-flow-control.js +97 -0
- package/backend/terminal-screen-state.js +11 -2
- package/backend/terminal-screen-worker-pool.js +59 -6
- package/backend/terminal-screen-worker-thread.js +97 -57
- package/backend/terminal-screen-worker.js +133 -51
- package/backend/terminal-state-serialization.js +127 -0
- package/backend/terminal-status.js +23 -4
- package/backend/usage-monitor.js +176 -17
- package/backend/workspace-directory.js +232 -0
- package/backend/workspace-file-router.js +182 -76
- package/backend/workspace-file-service.js +319 -4
- package/backend/workspace-root-registry.js +164 -0
- package/dist/assets/App-DWsa7DXG.js +206 -0
- package/dist/assets/{FileEditorMarkdownPreview-elKWc8Im.js → FileEditorMarkdownPreview-UlVd0O4D.js} +92 -92
- package/dist/assets/FileEditorPane-Dxx4rKOg.js +2 -0
- package/dist/assets/IconGlyphs-AU22gPDY.js +1 -0
- package/dist/assets/ProjectFilesSection-DE7Nb9Jk.js +12 -0
- package/dist/assets/ReviewPage-CeDKDyZh.js +3 -0
- package/dist/assets/code-dark-DPiuyeSp.css +1 -0
- package/dist/assets/file-icons-Bw2qd5iT.js +1 -0
- package/dist/assets/{index-BrbljRqn.js → index-CUFPJoa5.js} +3 -3
- package/dist/assets/main-Bqtb5kuz.css +1 -0
- package/dist/assets/{monaco.contribution-CyLosfZI.js → monaco.contribution-BXz-lfFB.js} +2 -2
- package/dist/assets/{tsMode-DGZTlTj8.js → tsMode-B0oUTcXQ.js} +1 -1
- package/dist/assets/workspace-editor-model-BQol4qbA.js +1 -0
- package/dist/assets/workspace-editor-monaco-BICJESG0.js +4 -0
- package/dist/assets/workspace-editor-monaco-DRJ7IaXk.js +1 -0
- package/dist/assets/workspace-view-state-DvYG_9PH.js +7 -0
- package/dist/assets/workspace-working-copy-D8-s_Sgh.js +1 -0
- package/dist/farming-2/site.webmanifest +2 -0
- package/dist/index.html +1 -1
- package/frontend/farming-net/app.css +625 -0
- package/frontend/farming-net/app.js +268 -0
- package/frontend/farming-net/index.html +86 -0
- package/frontend/reading-anchor.js +198 -0
- package/frontend/session-bridge.js +12 -3
- package/frontend/session-modal-bridge.js +5 -12
- package/frontend/skins/crt/app.js +1978 -793
- package/frontend/skins/crt/index.html +313 -23
- package/frontend/skins/crt/styles/billing.css +294 -223
- package/frontend/skins/crt/styles/monochrome-green.css +7 -2
- package/frontend/terminal-replay.js +372 -0
- package/package.json +10 -3
- package/shared/browser-protocol.d.ts +5 -0
- package/shared/browser-protocol.js +130 -0
- package/dist/assets/App-8dYAM6ql.js +0 -124
- package/dist/assets/FileEditorPane-RWiFD2cq.js +0 -5
- package/dist/assets/IconGlyphs-DfL0EBnj.js +0 -1
- package/dist/assets/ProjectFilesSection-Q4PDsWmM.js +0 -12
- package/dist/assets/ReviewPage-BaXu1ZdX.js +0 -3
- package/dist/assets/code-dark-CDkOQAtK.css +0 -1
- package/dist/assets/file-icons-EFUGSSwf.js +0 -1
- package/dist/assets/main-D073SnW4.css +0 -1
- package/dist/assets/qoder-C9LmmOSf.svg +0 -1
- package/dist/assets/qoder-Cf9gl0Y5.svg +0 -1
- package/dist/assets/qoder-gHCinseV.svg +0 -1
- package/dist/assets/workspace-view-state-CTyDzk2D.js +0 -1
- package/dist/assets/zsh-CLpveKlF.svg +0 -1
- package/dist/assets/zsh-FxSpMPbz.svg +0 -1
- /package/dist/assets/{api-D1lyBYIQ.js → api-D8nyOEbz.js} +0 -0
- /package/dist/assets/{core-ZlAPicox.js → core-D0LFJkDt.js} +0 -0
- /package/dist/assets/{useWorkspaceMenuKeyboard-CneKAZUJ.js → useWorkspaceMenuKeyboard-Brws6Ar9.js} +0 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const TERMINAL_STATE_VERSION = 1;
|
|
2
|
+
const MAX_SERIALIZED_TERMINAL_STATE_BYTES = 32 * 1024 * 1024;
|
|
3
|
+
const MAX_REPLAY_EVENT_BYTES = 8 * 1024 * 1024;
|
|
4
|
+
|
|
5
|
+
function finitePositiveInteger(value, fallback) {
|
|
6
|
+
const parsed = Math.floor(Number(value));
|
|
7
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function normalizeReplayEvent(replayEvent) {
|
|
11
|
+
const events = Array.isArray(replayEvent?.events) ? replayEvent.events : [];
|
|
12
|
+
if (events.length !== 1) {
|
|
13
|
+
throw new Error('Serialized terminal state requires exactly one replay event');
|
|
14
|
+
}
|
|
15
|
+
const event = events[0] || {};
|
|
16
|
+
const data = typeof event.data === 'string' ? event.data : '';
|
|
17
|
+
if (Buffer.byteLength(data, 'utf8') > MAX_REPLAY_EVENT_BYTES) {
|
|
18
|
+
throw new Error('Serialized terminal replay event exceeds the size limit');
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
events: [{
|
|
22
|
+
data,
|
|
23
|
+
cols: finitePositiveInteger(event.cols, 80),
|
|
24
|
+
rows: finitePositiveInteger(event.rows, 30),
|
|
25
|
+
}],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeTerminalStateEntry(entry) {
|
|
30
|
+
if (!entry || typeof entry !== 'object') {
|
|
31
|
+
throw new Error('Serialized terminal state entry must be an object');
|
|
32
|
+
}
|
|
33
|
+
const id = typeof entry.id === 'string' ? entry.id.trim() : '';
|
|
34
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,199}$/.test(id)) {
|
|
35
|
+
throw new Error('Serialized terminal state has an invalid session id');
|
|
36
|
+
}
|
|
37
|
+
const metadata = entry.metadata && typeof entry.metadata === 'object' && !Array.isArray(entry.metadata)
|
|
38
|
+
? { ...entry.metadata }
|
|
39
|
+
: {};
|
|
40
|
+
const processDetails = entry.processDetails && typeof entry.processDetails === 'object'
|
|
41
|
+
? entry.processDetails
|
|
42
|
+
: {};
|
|
43
|
+
const processLaunchConfig = entry.processLaunchConfig && typeof entry.processLaunchConfig === 'object'
|
|
44
|
+
? entry.processLaunchConfig
|
|
45
|
+
: {};
|
|
46
|
+
return {
|
|
47
|
+
id,
|
|
48
|
+
metadata: {
|
|
49
|
+
...metadata,
|
|
50
|
+
agentId: id,
|
|
51
|
+
},
|
|
52
|
+
processDetails: {
|
|
53
|
+
cwd: typeof processDetails.cwd === 'string' ? processDetails.cwd : '',
|
|
54
|
+
title: typeof processDetails.title === 'string' ? processDetails.title : '',
|
|
55
|
+
},
|
|
56
|
+
processLaunchConfig: {
|
|
57
|
+
command: typeof processLaunchConfig.command === 'string' ? processLaunchConfig.command : '',
|
|
58
|
+
args: Array.isArray(processLaunchConfig.args)
|
|
59
|
+
? processLaunchConfig.args.filter(arg => typeof arg === 'string')
|
|
60
|
+
: [],
|
|
61
|
+
category: typeof processLaunchConfig.category === 'string' ? processLaunchConfig.category : '',
|
|
62
|
+
},
|
|
63
|
+
replayEvent: normalizeReplayEvent(entry.replayEvent),
|
|
64
|
+
timestamp: Number.isFinite(Number(entry.timestamp))
|
|
65
|
+
? Math.max(0, Math.floor(Number(entry.timestamp)))
|
|
66
|
+
: Date.now(),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function serializeTerminalState(entries) {
|
|
71
|
+
const state = (Array.isArray(entries) ? entries : []).map(normalizeTerminalStateEntry);
|
|
72
|
+
const ids = new Set();
|
|
73
|
+
for (const entry of state) {
|
|
74
|
+
if (ids.has(entry.id)) {
|
|
75
|
+
throw new Error(`Serialized terminal state contains duplicate session id ${entry.id}`);
|
|
76
|
+
}
|
|
77
|
+
ids.add(entry.id);
|
|
78
|
+
}
|
|
79
|
+
const serialized = JSON.stringify({
|
|
80
|
+
version: TERMINAL_STATE_VERSION,
|
|
81
|
+
state,
|
|
82
|
+
});
|
|
83
|
+
if (Buffer.byteLength(serialized, 'utf8') > MAX_SERIALIZED_TERMINAL_STATE_BYTES) {
|
|
84
|
+
throw new Error('Serialized terminal state exceeds the size limit');
|
|
85
|
+
}
|
|
86
|
+
return serialized;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function deserializeTerminalState(serialized) {
|
|
90
|
+
if (typeof serialized !== 'string' || !serialized) return [];
|
|
91
|
+
if (Buffer.byteLength(serialized, 'utf8') > MAX_SERIALIZED_TERMINAL_STATE_BYTES) {
|
|
92
|
+
throw new Error('Serialized terminal state exceeds the size limit');
|
|
93
|
+
}
|
|
94
|
+
let parsed;
|
|
95
|
+
try {
|
|
96
|
+
parsed = JSON.parse(serialized);
|
|
97
|
+
} catch {
|
|
98
|
+
throw new Error('Serialized terminal state is not valid JSON');
|
|
99
|
+
}
|
|
100
|
+
if (!parsed || typeof parsed !== 'object' || parsed.version !== TERMINAL_STATE_VERSION || !Array.isArray(parsed.state)) {
|
|
101
|
+
throw new Error('Serialized terminal state has an unsupported format or version');
|
|
102
|
+
}
|
|
103
|
+
const state = parsed.state.map(normalizeTerminalStateEntry);
|
|
104
|
+
const ids = new Set();
|
|
105
|
+
for (const entry of state) {
|
|
106
|
+
if (ids.has(entry.id)) {
|
|
107
|
+
throw new Error(`Serialized terminal state contains duplicate session id ${entry.id}`);
|
|
108
|
+
}
|
|
109
|
+
ids.add(entry.id);
|
|
110
|
+
}
|
|
111
|
+
return state;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function terminalReplayText(entry) {
|
|
115
|
+
const normalized = normalizeTerminalStateEntry(entry);
|
|
116
|
+
return normalized.replayEvent.events[0].data;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = {
|
|
120
|
+
MAX_REPLAY_EVENT_BYTES,
|
|
121
|
+
MAX_SERIALIZED_TERMINAL_STATE_BYTES,
|
|
122
|
+
TERMINAL_STATE_VERSION,
|
|
123
|
+
deserializeTerminalState,
|
|
124
|
+
normalizeTerminalStateEntry,
|
|
125
|
+
serializeTerminalState,
|
|
126
|
+
terminalReplayText,
|
|
127
|
+
};
|
|
@@ -150,13 +150,12 @@ function latestTerminalKindFromText(title, previewText) {
|
|
|
150
150
|
|
|
151
151
|
function inferKindFromText(title, previewText, command) {
|
|
152
152
|
const commandName = executableName(command).toLowerCase();
|
|
153
|
-
if (commandName === 'claude') return 'claude';
|
|
154
|
-
if (commandName === 'codex') return 'codex';
|
|
155
153
|
if (DIRECT_PROCESS_AGENTS.has(commandName)) return 'process';
|
|
156
|
-
if (commandName && !SHELL_COMMANDS.has(commandName)) return 'process';
|
|
157
|
-
|
|
154
|
+
if (commandName && !SHELL_COMMANDS.has(commandName) && commandName !== 'codex' && commandName !== 'claude') return 'process';
|
|
158
155
|
const terminalKind = latestTerminalKindFromText(title, previewText);
|
|
159
156
|
if (terminalKind) return terminalKind;
|
|
157
|
+
if (commandName === 'claude') return 'claude';
|
|
158
|
+
if (commandName === 'codex') return 'codex';
|
|
160
159
|
if (SHELL_COMMANDS.has(commandName)) return 'shell';
|
|
161
160
|
return commandName ? 'process' : 'unknown';
|
|
162
161
|
}
|
|
@@ -350,7 +349,27 @@ function deriveTerminalStatus(options = {}) {
|
|
|
350
349
|
return status;
|
|
351
350
|
}
|
|
352
351
|
|
|
352
|
+
function terminalInputReady(options = {}) {
|
|
353
|
+
const status = deriveTerminalStatus(options);
|
|
354
|
+
if (status.activity !== 'idle') return false;
|
|
355
|
+
|
|
356
|
+
const previewText = typeof options.previewText === 'string' ? options.previewText : '';
|
|
357
|
+
if (status.kind === 'codex') {
|
|
358
|
+
const text = stripTerminalControlSequences(previewText).replace(/\r/g, '').toLowerCase();
|
|
359
|
+
return lastCodexIdleFooterIndex(text) >= 0 || codexBlockedIndex(text) >= 0;
|
|
360
|
+
}
|
|
361
|
+
if (status.kind === 'claude') {
|
|
362
|
+
const text = stripTerminalControlSequences(previewText).replace(/\r/g, '');
|
|
363
|
+
return /(?:^|\n)\s*❯(?:\s|$)/u.test(text);
|
|
364
|
+
}
|
|
365
|
+
if (status.kind === 'shell') {
|
|
366
|
+
return options.terminalBusy === false || terminalTextLooksIdleShellPrompt(previewText);
|
|
367
|
+
}
|
|
368
|
+
return Boolean(stripTerminalControlSequences(previewText).trim());
|
|
369
|
+
}
|
|
370
|
+
|
|
353
371
|
module.exports = {
|
|
354
372
|
deriveTerminalStatus,
|
|
373
|
+
terminalInputReady,
|
|
355
374
|
terminalTextLooksIdleShellPrompt,
|
|
356
375
|
};
|
package/backend/usage-monitor.js
CHANGED
|
@@ -9,10 +9,11 @@ const { attachQuotaForecasts } = require('./usage-forecast');
|
|
|
9
9
|
const execFileAsync = promisify(execFile);
|
|
10
10
|
|
|
11
11
|
const USAGE_WINDOW_MS = 5 * 60 * 1000;
|
|
12
|
-
const USAGE_TIMELINE_WINDOW_MS = 60 * 60 * 1000;
|
|
13
|
-
const USAGE_TIMELINE_BUCKET_COUNT =
|
|
12
|
+
const USAGE_TIMELINE_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
13
|
+
const USAGE_TIMELINE_BUCKET_COUNT = 24;
|
|
14
14
|
const USAGE_DAILY_DAYS = 52 * 7;
|
|
15
15
|
const USAGE_DAILY_CACHE_MS = 5 * 60 * 1000;
|
|
16
|
+
const USAGE_LIVE_DAY_CACHE_MS = 5 * 1000;
|
|
16
17
|
const JSONL_FILE_LIMIT = 60;
|
|
17
18
|
const JSONL_SCAN_LIMIT = 2000;
|
|
18
19
|
const DAILY_JSONL_FILE_LIMIT = 5000;
|
|
@@ -151,6 +152,32 @@ function addTokenBreakdown(target, source) {
|
|
|
151
152
|
return target;
|
|
152
153
|
}
|
|
153
154
|
|
|
155
|
+
function usageAgentIdFromFilePath(filePath) {
|
|
156
|
+
const basename = path.basename(String(filePath || ''), path.extname(String(filePath || '')));
|
|
157
|
+
const uuid = basename.match(/([0-9a-f]{8}-[0-9a-f-]{27})$/i);
|
|
158
|
+
return uuid?.[1] || basename || 'unattributed';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function usageAgentLabel(provider, agentId) {
|
|
162
|
+
const providerName = {
|
|
163
|
+
codex: 'Codex',
|
|
164
|
+
claude: 'Claude',
|
|
165
|
+
opencode: 'OpenCode',
|
|
166
|
+
qoder: 'Qoder',
|
|
167
|
+
}[provider] || provider;
|
|
168
|
+
if (!agentId || agentId === 'unattributed') return providerName;
|
|
169
|
+
const shortId = agentId.length > 12 ? `…${agentId.slice(-6)}` : agentId;
|
|
170
|
+
return `${providerName} · ${shortId}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function attributeUsageEvent(event, provider, agentId) {
|
|
174
|
+
return {
|
|
175
|
+
...event,
|
|
176
|
+
agentId,
|
|
177
|
+
agentLabel: usageAgentLabel(provider, agentId),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
154
181
|
function subtractTokenBreakdown(current, previous) {
|
|
155
182
|
const result = emptyTokenBreakdown();
|
|
156
183
|
for (const field of Object.keys(result)) {
|
|
@@ -267,7 +294,9 @@ function buildUsageTimeline(providerEvents, options = {}) {
|
|
|
267
294
|
const windowMs = Math.max(60_000, options.windowMs ?? USAGE_TIMELINE_WINDOW_MS);
|
|
268
295
|
const bucketCount = Math.max(1, Math.floor(options.bucketCount ?? USAGE_TIMELINE_BUCKET_COUNT));
|
|
269
296
|
const bucketMs = windowMs / bucketCount;
|
|
270
|
-
const endAt =
|
|
297
|
+
const endAt = options.alignToBucket
|
|
298
|
+
? Math.ceil(now / bucketMs) * bucketMs
|
|
299
|
+
: now;
|
|
271
300
|
const startAt = endAt - windowMs;
|
|
272
301
|
const providerNames = Object.keys(providerEvents || {});
|
|
273
302
|
const points = Array.from({ length: bucketCount }, (_, index) => ({
|
|
@@ -651,11 +680,14 @@ function buildUsageDayDetail(providerEvents, options = {}) {
|
|
|
651
680
|
hour,
|
|
652
681
|
label: String(hour).padStart(2, '0'),
|
|
653
682
|
...emptyTokenBreakdown(),
|
|
683
|
+
agents: {},
|
|
654
684
|
}));
|
|
655
685
|
const providers = Object.fromEntries(
|
|
656
686
|
providerNames.map(provider => [provider, emptyTokenBreakdown()]),
|
|
657
687
|
);
|
|
658
688
|
const total = emptyTokenBreakdown();
|
|
689
|
+
const agents = new Map();
|
|
690
|
+
const agentLabels = options.agentLabels instanceof Map ? options.agentLabels : new Map();
|
|
659
691
|
|
|
660
692
|
for (const provider of providerNames) {
|
|
661
693
|
for (const event of providerEvents[provider] || []) {
|
|
@@ -663,8 +695,24 @@ function buildUsageDayDetail(providerEvents, options = {}) {
|
|
|
663
695
|
if (localDateKey(timestamp) !== date) continue;
|
|
664
696
|
const hour = new Date(timestamp).getHours();
|
|
665
697
|
if (!Number.isInteger(hour) || hour < 0 || hour > 23) continue;
|
|
698
|
+
const agentId = String(event?.agentId || 'unattributed');
|
|
699
|
+
const agentKey = `${provider}:${agentId}`;
|
|
700
|
+
let agent = agents.get(agentKey);
|
|
701
|
+
if (!agent) {
|
|
702
|
+
agent = {
|
|
703
|
+
key: agentKey,
|
|
704
|
+
provider,
|
|
705
|
+
sessionId: agentId === 'unattributed' ? '' : agentId,
|
|
706
|
+
label: agentLabels.get(agentKey) || event?.agentLabel || usageAgentLabel(provider, agentId),
|
|
707
|
+
...emptyTokenBreakdown(),
|
|
708
|
+
};
|
|
709
|
+
agents.set(agentKey, agent);
|
|
710
|
+
}
|
|
711
|
+
if (!hours[hour].agents[agentKey]) hours[hour].agents[agentKey] = emptyTokenBreakdown();
|
|
666
712
|
addTokenBreakdown(hours[hour], event);
|
|
713
|
+
addTokenBreakdown(hours[hour].agents[agentKey], event);
|
|
667
714
|
addTokenBreakdown(providers[provider], event);
|
|
715
|
+
addTokenBreakdown(agent, event);
|
|
668
716
|
addTokenBreakdown(total, event);
|
|
669
717
|
}
|
|
670
718
|
}
|
|
@@ -676,6 +724,9 @@ function buildUsageDayDetail(providerEvents, options = {}) {
|
|
|
676
724
|
total,
|
|
677
725
|
hours,
|
|
678
726
|
providers,
|
|
727
|
+
agents: Array.from(agents.values()).sort((left, right) => (
|
|
728
|
+
right.totalTokens - left.totalTokens || left.label.localeCompare(right.label)
|
|
729
|
+
)),
|
|
679
730
|
};
|
|
680
731
|
}
|
|
681
732
|
|
|
@@ -708,9 +759,11 @@ async function readDailyFileEvents(filePath, provider, minimumMtimeMs = 0) {
|
|
|
708
759
|
}
|
|
709
760
|
}
|
|
710
761
|
|
|
762
|
+
const agentId = usageAgentIdFromFilePath(filePath);
|
|
763
|
+
const attributedEvents = events.map(event => attributeUsageEvent(event, provider, agentId));
|
|
711
764
|
const truncated = stat.size > JSONL_TAIL_BYTES;
|
|
712
|
-
dailyFileEventCache.set(cacheKey, { signature, events, truncated });
|
|
713
|
-
return { events, mtimeMs: stat.mtimeMs, truncated };
|
|
765
|
+
dailyFileEventCache.set(cacheKey, { signature, events: attributedEvents, truncated });
|
|
766
|
+
return { events: attributedEvents, mtimeMs: stat.mtimeMs, truncated };
|
|
714
767
|
}
|
|
715
768
|
|
|
716
769
|
async function collectCodexDailyEventsWithRipgrep(roots, options = {}) {
|
|
@@ -764,7 +817,9 @@ async function collectCodexDailyEventsWithRipgrep(roots, options = {}) {
|
|
|
764
817
|
states.set(filePath, state);
|
|
765
818
|
}
|
|
766
819
|
const event = codexTokenEventFromRecord(record, state);
|
|
767
|
-
if (event && event.timestamp >= cutoffMs && event.timestamp <= now + 60_000)
|
|
820
|
+
if (event && event.timestamp >= cutoffMs && event.timestamp <= now + 60_000) {
|
|
821
|
+
events.push(attributeUsageEvent(event, 'codex', usageAgentIdFromFilePath(filePath)));
|
|
822
|
+
}
|
|
768
823
|
});
|
|
769
824
|
child.once('close', code => {
|
|
770
825
|
if (unavailable || (code !== 0 && code !== 1)) {
|
|
@@ -847,6 +902,7 @@ async function collectOpenCodeDailyEvents(homePaths, options = {}) {
|
|
|
847
902
|
const sessions = [];
|
|
848
903
|
const seenSessionIds = new Set();
|
|
849
904
|
let successfulHomes = 0;
|
|
905
|
+
let successfulExports = 0;
|
|
850
906
|
let partial = false;
|
|
851
907
|
let reason = '';
|
|
852
908
|
|
|
@@ -880,6 +936,7 @@ async function collectOpenCodeDailyEvents(homePaths, options = {}) {
|
|
|
880
936
|
const cacheKey = `${session.openCodeHome}:${session.id}`;
|
|
881
937
|
const cached = openCodeSessionEventCache.get(cacheKey);
|
|
882
938
|
if (cached?.updatedAt === session.updatedAt) {
|
|
939
|
+
successfulExports += 1;
|
|
883
940
|
events.push(...cached.events);
|
|
884
941
|
continue;
|
|
885
942
|
}
|
|
@@ -889,7 +946,9 @@ async function collectOpenCodeDailyEvents(homePaths, options = {}) {
|
|
|
889
946
|
{ openCodeHome: session.openCodeHome, timeoutMs: OPENCODE_COMMAND_TIMEOUT_MS },
|
|
890
947
|
);
|
|
891
948
|
const exported = JSON.parse(String(result?.stdout || '{}'));
|
|
892
|
-
const sessionEvents = openCodeTokenEventsFromExport(exported, { cutoffMs, now })
|
|
949
|
+
const sessionEvents = openCodeTokenEventsFromExport(exported, { cutoffMs, now })
|
|
950
|
+
.map(event => attributeUsageEvent(event, 'opencode', session.id));
|
|
951
|
+
successfulExports += 1;
|
|
893
952
|
openCodeSessionEventCache.set(cacheKey, { updatedAt: session.updatedAt, events: sessionEvents });
|
|
894
953
|
events.push(...sessionEvents);
|
|
895
954
|
} catch (error) {
|
|
@@ -906,9 +965,11 @@ async function collectOpenCodeDailyEvents(homePaths, options = {}) {
|
|
|
906
965
|
return {
|
|
907
966
|
events,
|
|
908
967
|
partial,
|
|
909
|
-
available: successfulHomes
|
|
968
|
+
available: successfulHomes === homePaths.length
|
|
969
|
+
&& (sessions.length === 0 || successfulExports === sessions.length),
|
|
910
970
|
reason,
|
|
911
971
|
sessionCount: sessions.length,
|
|
972
|
+
exportCount: successfulExports,
|
|
912
973
|
};
|
|
913
974
|
}
|
|
914
975
|
|
|
@@ -980,6 +1041,8 @@ async function collectUsageHistory(options = {}) {
|
|
|
980
1041
|
available: openCode.available,
|
|
981
1042
|
homeCount: openCodeHomes.length,
|
|
982
1043
|
sessionCount: openCode.sessionCount,
|
|
1044
|
+
exportCount: openCode.exportCount,
|
|
1045
|
+
partial: openCode.partial,
|
|
983
1046
|
source: 'opencode session export',
|
|
984
1047
|
...(openCode.reason ? { reason: openCode.reason } : {}),
|
|
985
1048
|
},
|
|
@@ -1195,10 +1258,40 @@ class UsageMonitor {
|
|
|
1195
1258
|
this.dailyDays = options.dailyDays ?? USAGE_DAILY_DAYS;
|
|
1196
1259
|
this.dailyCacheMs = options.dailyCacheMs ?? USAGE_DAILY_CACHE_MS;
|
|
1197
1260
|
this.dailyCache = { value: null, fetchedAt: 0, pending: null };
|
|
1261
|
+
this.liveDayCacheMs = options.liveDayCacheMs ?? USAGE_LIVE_DAY_CACHE_MS;
|
|
1262
|
+
this.liveDayCache = { date: '', value: null, fetchedAt: 0, pending: null };
|
|
1198
1263
|
}
|
|
1199
1264
|
|
|
1200
1265
|
invalidateDailyCache() {
|
|
1201
1266
|
this.dailyCache.fetchedAt = 0;
|
|
1267
|
+
this.liveDayCache.fetchedAt = 0;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
usageAgentLabels() {
|
|
1271
|
+
const labels = new Map();
|
|
1272
|
+
const agents = this.agentManager?.getState?.().agents;
|
|
1273
|
+
if (!Array.isArray(agents)) return labels;
|
|
1274
|
+
for (const agent of agents) {
|
|
1275
|
+
const provider = String(agent?.providerSessionProvider || '').trim();
|
|
1276
|
+
const sessionId = String(agent?.providerSessionId || '').trim();
|
|
1277
|
+
if (!provider || !sessionId) continue;
|
|
1278
|
+
const label = String(
|
|
1279
|
+
agent.customTitle
|
|
1280
|
+
|| agent.task
|
|
1281
|
+
|| agent.sessionTitle
|
|
1282
|
+
|| agent.providerSessionTitle
|
|
1283
|
+
|| '',
|
|
1284
|
+
).trim();
|
|
1285
|
+
if (label) labels.set(`${provider}:${sessionId}`, label);
|
|
1286
|
+
}
|
|
1287
|
+
return labels;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
buildUsageDay(providerEvents, date) {
|
|
1291
|
+
return buildUsageDayDetail(providerEvents, {
|
|
1292
|
+
date,
|
|
1293
|
+
agentLabels: this.usageAgentLabels(),
|
|
1294
|
+
});
|
|
1202
1295
|
}
|
|
1203
1296
|
|
|
1204
1297
|
getDailyUsage(options = {}) {
|
|
@@ -1232,11 +1325,63 @@ class UsageMonitor {
|
|
|
1232
1325
|
}
|
|
1233
1326
|
|
|
1234
1327
|
async getUsageDay(date, options = {}) {
|
|
1328
|
+
const now = options.now ?? Date.now();
|
|
1329
|
+
if (options.live === true && String(date || '').trim() === localDateKey(now)) {
|
|
1330
|
+
const liveDate = String(date).trim();
|
|
1331
|
+
const dailyFallback = this.dailyCache.value?.providerEvents
|
|
1332
|
+
? this.buildUsageDay(this.dailyCache.value.providerEvents, liveDate)
|
|
1333
|
+
: null;
|
|
1334
|
+
const cachedFallback = this.liveDayCache.date === liveDate
|
|
1335
|
+
? this.liveDayCache.value
|
|
1336
|
+
: null;
|
|
1337
|
+
const fallback = cachedFallback || dailyFallback;
|
|
1338
|
+
const recoverWithFallback = error => {
|
|
1339
|
+
if (!fallback) throw error;
|
|
1340
|
+
if (this.liveDayCache.date === liveDate) {
|
|
1341
|
+
this.liveDayCache.value = fallback;
|
|
1342
|
+
this.liveDayCache.fetchedAt = now;
|
|
1343
|
+
}
|
|
1344
|
+
return fallback;
|
|
1345
|
+
};
|
|
1346
|
+
if (
|
|
1347
|
+
options.fresh !== true
|
|
1348
|
+
&& this.liveDayCache.date === liveDate
|
|
1349
|
+
&& this.liveDayCache.value
|
|
1350
|
+
&& now - this.liveDayCache.fetchedAt < this.liveDayCacheMs
|
|
1351
|
+
) {
|
|
1352
|
+
return this.liveDayCache.value;
|
|
1353
|
+
}
|
|
1354
|
+
if (this.liveDayCache.pending && this.liveDayCache.date === liveDate) {
|
|
1355
|
+
return this.liveDayCache.pending.catch(recoverWithFallback);
|
|
1356
|
+
}
|
|
1357
|
+
this.liveDayCache.date = liveDate;
|
|
1358
|
+
const pending = collectUsageHistory({
|
|
1359
|
+
codexHome: this.codexHome,
|
|
1360
|
+
claudeHome: this.claudeHome,
|
|
1361
|
+
openCodeHome: this.openCodeHome,
|
|
1362
|
+
qoderHome: this.qoderHome,
|
|
1363
|
+
providerHomes: this.getProviderHomes ? this.getProviderHomes() : undefined,
|
|
1364
|
+
openCodeCommandRunner: this.openCodeCommandRunner,
|
|
1365
|
+
now,
|
|
1366
|
+
days: 1,
|
|
1367
|
+
}).then(history => {
|
|
1368
|
+
const detail = this.buildUsageDay(history.providerEvents, liveDate);
|
|
1369
|
+
if (this.liveDayCache.date === liveDate) {
|
|
1370
|
+
this.liveDayCache.value = detail;
|
|
1371
|
+
this.liveDayCache.fetchedAt = now;
|
|
1372
|
+
}
|
|
1373
|
+
return detail;
|
|
1374
|
+
}).finally(() => {
|
|
1375
|
+
if (this.liveDayCache.pending === pending) this.liveDayCache.pending = null;
|
|
1376
|
+
});
|
|
1377
|
+
this.liveDayCache.pending = pending;
|
|
1378
|
+
return pending.catch(recoverWithFallback);
|
|
1379
|
+
}
|
|
1235
1380
|
const history = await this.getDailyUsage({
|
|
1236
|
-
now
|
|
1381
|
+
now,
|
|
1237
1382
|
force: options.fresh === true,
|
|
1238
1383
|
});
|
|
1239
|
-
return
|
|
1384
|
+
return this.buildUsageDay(history.providerEvents, date);
|
|
1240
1385
|
}
|
|
1241
1386
|
|
|
1242
1387
|
async getUsageSummary(options = {}) {
|
|
@@ -1267,11 +1412,12 @@ class UsageMonitor {
|
|
|
1267
1412
|
historyWindowMs,
|
|
1268
1413
|
source: 'opencode session export',
|
|
1269
1414
|
});
|
|
1270
|
-
const timeline = buildUsageTimeline({
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
}
|
|
1415
|
+
const timeline = buildUsageTimeline(history.providerEvents, {
|
|
1416
|
+
now,
|
|
1417
|
+
windowMs: historyWindowMs,
|
|
1418
|
+
alignToBucket: true,
|
|
1419
|
+
});
|
|
1420
|
+
const openCodeCoverage = history.coverage.find(entry => entry.provider === 'opencode');
|
|
1275
1421
|
const qoderCoverage = history.coverage.find(entry => entry.provider === 'qoder');
|
|
1276
1422
|
|
|
1277
1423
|
return {
|
|
@@ -1297,13 +1443,25 @@ class UsageMonitor {
|
|
|
1297
1443
|
{
|
|
1298
1444
|
provider: 'opencode',
|
|
1299
1445
|
providerName: 'OpenCode',
|
|
1300
|
-
auth: {
|
|
1446
|
+
auth: {
|
|
1447
|
+
available: openCodeCoverage?.available === true,
|
|
1448
|
+
status: openCodeCoverage?.available === true
|
|
1449
|
+
? 'Local session export'
|
|
1450
|
+
: openCodeCoverage?.reason || 'OpenCode unavailable',
|
|
1451
|
+
source: 'opencode session export',
|
|
1452
|
+
},
|
|
1301
1453
|
quota: {
|
|
1302
1454
|
available: false,
|
|
1303
1455
|
source: 'opencode session export',
|
|
1304
1456
|
reason: 'OpenCode session exports do not expose quota remaining.',
|
|
1305
1457
|
},
|
|
1306
|
-
tokenUsage:
|
|
1458
|
+
tokenUsage: openCodeCoverage?.available === true
|
|
1459
|
+
? openCodeUsage.tokenUsage
|
|
1460
|
+
: {
|
|
1461
|
+
...openCodeUsage.tokenUsage,
|
|
1462
|
+
available: false,
|
|
1463
|
+
reason: openCodeCoverage?.reason || 'OpenCode token usage is unavailable.',
|
|
1464
|
+
},
|
|
1307
1465
|
},
|
|
1308
1466
|
{
|
|
1309
1467
|
provider: 'qoder',
|
|
@@ -1340,6 +1498,7 @@ module.exports = {
|
|
|
1340
1498
|
USAGE_TIMELINE_BUCKET_COUNT,
|
|
1341
1499
|
USAGE_DAILY_DAYS,
|
|
1342
1500
|
USAGE_DAILY_CACHE_MS,
|
|
1501
|
+
USAGE_LIVE_DAY_CACHE_MS,
|
|
1343
1502
|
UsageMonitor,
|
|
1344
1503
|
buildUsageTimeline,
|
|
1345
1504
|
buildDailyUsage,
|