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.
Files changed (107) hide show
  1. package/README.md +42 -28
  2. package/README.zh_cn.md +42 -28
  3. package/THIRD_PARTY_NOTICES.md +11 -2
  4. package/backend/acp-checkpoint-store.js +198 -0
  5. package/backend/acp-runtime.js +306 -83
  6. package/backend/acp-session-state.js +202 -6
  7. package/backend/acp-transcript.js +112 -0
  8. package/backend/agent-activity.js +6 -157
  9. package/backend/agent-manager.js +1592 -741
  10. package/backend/agent-provider-session.js +17 -242
  11. package/backend/agent-runtime-binding.js +219 -0
  12. package/backend/agent-session-history.js +66 -1
  13. package/backend/auth.js +79 -6
  14. package/backend/codex-models.js +81 -84
  15. package/backend/codex-session-archive.js +54 -0
  16. package/backend/codex-terminal-profile.js +500 -0
  17. package/backend/codex-transcript-sanitizer.js +12 -0
  18. package/backend/codex-transcript.js +230 -8
  19. package/backend/config-manager.js +35 -0
  20. package/backend/control-api.js +192 -17
  21. package/backend/executable-discovery.js +2 -0
  22. package/backend/farming-net-pass.js +285 -0
  23. package/backend/farming-net-registry.js +112 -0
  24. package/backend/farming-net-server.js +298 -0
  25. package/backend/farming-session-store.js +5 -13
  26. package/backend/git-worktree-info.js +181 -0
  27. package/backend/local-session-engine.js +411 -186
  28. package/backend/main-page-session.js +5 -2
  29. package/backend/native-pty-controller-generation.js +106 -0
  30. package/backend/native-pty-host-client.js +275 -7
  31. package/backend/native-pty-host-identity.js +86 -0
  32. package/backend/native-pty-host.js +813 -114
  33. package/backend/native-session-engine.js +100 -28
  34. package/backend/packaged-node-pty.js +22 -2
  35. package/backend/provider-adapters.js +253 -0
  36. package/backend/provider-session-service.js +241 -0
  37. package/backend/runtime-observation.js +81 -0
  38. package/backend/server.js +354 -84
  39. package/backend/session-engine-bridge.js +21 -2
  40. package/backend/session-engine-router.js +1 -1
  41. package/backend/session-engine.js +1 -1
  42. package/backend/session-stream-protocol.js +185 -0
  43. package/backend/storage-layout.js +55 -0
  44. package/backend/terminal-attach-checkpoint.js +74 -0
  45. package/backend/terminal-exit-quiescence.js +39 -0
  46. package/backend/terminal-reducer-flow-control.js +97 -0
  47. package/backend/terminal-screen-state.js +11 -2
  48. package/backend/terminal-screen-worker-pool.js +59 -6
  49. package/backend/terminal-screen-worker-thread.js +97 -57
  50. package/backend/terminal-screen-worker.js +133 -51
  51. package/backend/terminal-state-serialization.js +127 -0
  52. package/backend/terminal-status.js +23 -4
  53. package/backend/usage-monitor.js +176 -17
  54. package/backend/workspace-directory.js +232 -0
  55. package/backend/workspace-file-router.js +182 -76
  56. package/backend/workspace-file-service.js +319 -4
  57. package/backend/workspace-root-registry.js +164 -0
  58. package/dist/assets/App-DWsa7DXG.js +206 -0
  59. package/dist/assets/{FileEditorMarkdownPreview-elKWc8Im.js → FileEditorMarkdownPreview-UlVd0O4D.js} +92 -92
  60. package/dist/assets/FileEditorPane-Dxx4rKOg.js +2 -0
  61. package/dist/assets/IconGlyphs-AU22gPDY.js +1 -0
  62. package/dist/assets/ProjectFilesSection-DE7Nb9Jk.js +12 -0
  63. package/dist/assets/ReviewPage-CeDKDyZh.js +3 -0
  64. package/dist/assets/code-dark-DPiuyeSp.css +1 -0
  65. package/dist/assets/file-icons-Bw2qd5iT.js +1 -0
  66. package/dist/assets/{index-BrbljRqn.js → index-CUFPJoa5.js} +3 -3
  67. package/dist/assets/main-Bqtb5kuz.css +1 -0
  68. package/dist/assets/{monaco.contribution-CyLosfZI.js → monaco.contribution-BXz-lfFB.js} +2 -2
  69. package/dist/assets/{tsMode-DGZTlTj8.js → tsMode-B0oUTcXQ.js} +1 -1
  70. package/dist/assets/workspace-editor-model-BQol4qbA.js +1 -0
  71. package/dist/assets/workspace-editor-monaco-BICJESG0.js +4 -0
  72. package/dist/assets/workspace-editor-monaco-DRJ7IaXk.js +1 -0
  73. package/dist/assets/workspace-view-state-DvYG_9PH.js +7 -0
  74. package/dist/assets/workspace-working-copy-D8-s_Sgh.js +1 -0
  75. package/dist/farming-2/site.webmanifest +2 -0
  76. package/dist/index.html +1 -1
  77. package/frontend/farming-net/app.css +625 -0
  78. package/frontend/farming-net/app.js +268 -0
  79. package/frontend/farming-net/index.html +86 -0
  80. package/frontend/reading-anchor.js +198 -0
  81. package/frontend/session-bridge.js +12 -3
  82. package/frontend/session-modal-bridge.js +5 -12
  83. package/frontend/skins/crt/app.js +1978 -793
  84. package/frontend/skins/crt/index.html +313 -23
  85. package/frontend/skins/crt/styles/billing.css +294 -223
  86. package/frontend/skins/crt/styles/monochrome-green.css +7 -2
  87. package/frontend/terminal-replay.js +372 -0
  88. package/package.json +10 -3
  89. package/shared/browser-protocol.d.ts +5 -0
  90. package/shared/browser-protocol.js +130 -0
  91. package/dist/assets/App-8dYAM6ql.js +0 -124
  92. package/dist/assets/FileEditorPane-RWiFD2cq.js +0 -5
  93. package/dist/assets/IconGlyphs-DfL0EBnj.js +0 -1
  94. package/dist/assets/ProjectFilesSection-Q4PDsWmM.js +0 -12
  95. package/dist/assets/ReviewPage-BaXu1ZdX.js +0 -3
  96. package/dist/assets/code-dark-CDkOQAtK.css +0 -1
  97. package/dist/assets/file-icons-EFUGSSwf.js +0 -1
  98. package/dist/assets/main-D073SnW4.css +0 -1
  99. package/dist/assets/qoder-C9LmmOSf.svg +0 -1
  100. package/dist/assets/qoder-Cf9gl0Y5.svg +0 -1
  101. package/dist/assets/qoder-gHCinseV.svg +0 -1
  102. package/dist/assets/workspace-view-state-CTyDzk2D.js +0 -1
  103. package/dist/assets/zsh-CLpveKlF.svg +0 -1
  104. package/dist/assets/zsh-FxSpMPbz.svg +0 -1
  105. /package/dist/assets/{api-D1lyBYIQ.js → api-D8nyOEbz.js} +0 -0
  106. /package/dist/assets/{core-ZlAPicox.js → core-D0LFJkDt.js} +0 -0
  107. /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
  };
@@ -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 = 30;
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 = now;
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) events.push(event);
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 > 0,
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: options.now,
1381
+ now,
1237
1382
  force: options.fresh === true,
1238
1383
  });
1239
- return buildUsageDayDetail(history.providerEvents, { date });
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
- codex: codexUsage.tokenEvents,
1272
- claude: claudeUsage.tokenEvents,
1273
- opencode: openCodeUsage.tokenEvents,
1274
- }, { now, windowMs: historyWindowMs });
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: { available: true, status: 'Local session export', source: 'opencode session export' },
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: openCodeUsage.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,