@foxden-app/foxclaw 0.3.15 → 0.3.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/dist/codex_app/local_usage.d.ts +13 -6
- package/dist/codex_app/local_usage.js +96 -58
- package/dist/controller/controller.d.ts +12 -1
- package/dist/controller/controller.js +173 -24
- package/dist/i18n.d.ts +14 -8
- package/dist/i18n.js +14 -8
- package/docs/user-manual.md +8 -7
- package/docs/zh/user-manual.md +8 -7
- package/package.json +1 -1
- package/skills/foxclaw/SKILL.md +2 -0
- package/skills/npm-publish/SKILL.md +3 -0
|
@@ -11,14 +11,21 @@ export interface CodexLocalUsageStats {
|
|
|
11
11
|
turns: number;
|
|
12
12
|
usageEvents: number;
|
|
13
13
|
totals: CodexLocalUsageTotals;
|
|
14
|
-
|
|
14
|
+
responseThroughput: CodexLocalResponseThroughputStats;
|
|
15
15
|
latestSessionMtimeMs: number | null;
|
|
16
16
|
}
|
|
17
|
-
export interface
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
export interface CodexLocalUsageSnapshot {
|
|
18
|
+
computedAtMs: number;
|
|
19
|
+
stats: CodexLocalUsageStats;
|
|
20
|
+
}
|
|
21
|
+
export interface CodexLocalResponseThroughputStats {
|
|
22
|
+
completedTurns: number;
|
|
23
|
+
visibleOutputTokens: number;
|
|
20
24
|
seconds: number;
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
recentCompletedTurns: number;
|
|
26
|
+
recentVisibleOutputTokens: number;
|
|
27
|
+
recentSeconds: number;
|
|
23
28
|
}
|
|
24
29
|
export declare function readCodexLocalUsageStats(codexHome?: string): Promise<CodexLocalUsageStats>;
|
|
30
|
+
export declare function readCodexLocalUsageSnapshot(snapshotPath: string): Promise<CodexLocalUsageSnapshot | null>;
|
|
31
|
+
export declare function writeCodexLocalUsageSnapshot(snapshotPath: string, snapshot: CodexLocalUsageSnapshot): Promise<void>;
|
|
@@ -3,6 +3,7 @@ import fs from 'node:fs/promises';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import readline from 'node:readline';
|
|
6
|
+
const RECENT_COMPLETED_TURNS = 10;
|
|
6
7
|
export async function readCodexLocalUsageStats(codexHome = resolveCodexHome()) {
|
|
7
8
|
const sessionFiles = await listJsonlFiles([
|
|
8
9
|
path.join(codexHome, 'sessions'),
|
|
@@ -13,7 +14,7 @@ export async function readCodexLocalUsageStats(codexHome = resolveCodexHome()) {
|
|
|
13
14
|
let sessionsWithUsage = 0;
|
|
14
15
|
let usageEvents = 0;
|
|
15
16
|
let latestSessionMtimeMs = null;
|
|
16
|
-
const
|
|
17
|
+
const completedTurnSamples = [];
|
|
17
18
|
for (const filePath of sessionFiles) {
|
|
18
19
|
const stat = await fs.stat(filePath).catch(() => null);
|
|
19
20
|
if (stat) {
|
|
@@ -21,7 +22,7 @@ export async function readCodexLocalUsageStats(codexHome = resolveCodexHome()) {
|
|
|
21
22
|
}
|
|
22
23
|
const fileUsage = await readSessionUsage(filePath, turnIds);
|
|
23
24
|
usageEvents += fileUsage.usageEvents;
|
|
24
|
-
|
|
25
|
+
completedTurnSamples.push(...fileUsage.completedTurnSamples);
|
|
25
26
|
if (fileUsage.totalUsage) {
|
|
26
27
|
sessionsWithUsage += 1;
|
|
27
28
|
addUsage(totals, fileUsage.totalUsage);
|
|
@@ -33,10 +34,28 @@ export async function readCodexLocalUsageStats(codexHome = resolveCodexHome()) {
|
|
|
33
34
|
turns: turnIds.size,
|
|
34
35
|
usageEvents,
|
|
35
36
|
totals,
|
|
36
|
-
|
|
37
|
+
responseThroughput: summarizeResponseThroughput(completedTurnSamples),
|
|
37
38
|
latestSessionMtimeMs,
|
|
38
39
|
};
|
|
39
40
|
}
|
|
41
|
+
export async function readCodexLocalUsageSnapshot(snapshotPath) {
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(await fs.readFile(snapshotPath, 'utf8'));
|
|
44
|
+
if (!isFiniteNumber(parsed.computedAtMs) || !isCodexLocalUsageStats(parsed.stats)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return { computedAtMs: parsed.computedAtMs, stats: parsed.stats };
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export async function writeCodexLocalUsageSnapshot(snapshotPath, snapshot) {
|
|
54
|
+
await fs.mkdir(path.dirname(snapshotPath), { recursive: true });
|
|
55
|
+
const temporaryPath = `${snapshotPath}.${process.pid}.tmp`;
|
|
56
|
+
await fs.writeFile(temporaryPath, `${JSON.stringify(snapshot, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
57
|
+
await fs.rename(temporaryPath, snapshotPath);
|
|
58
|
+
}
|
|
40
59
|
function resolveCodexHome() {
|
|
41
60
|
return process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
42
61
|
}
|
|
@@ -69,8 +88,8 @@ async function readSessionUsage(filePath, turnIds) {
|
|
|
69
88
|
const reader = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
70
89
|
let totalUsage = null;
|
|
71
90
|
let usageEvents = 0;
|
|
72
|
-
let
|
|
73
|
-
const
|
|
91
|
+
let activeTurn = null;
|
|
92
|
+
const completedTurnSamples = [];
|
|
74
93
|
try {
|
|
75
94
|
for await (const line of reader) {
|
|
76
95
|
if (!line.trim())
|
|
@@ -87,28 +106,49 @@ async function readSessionUsage(filePath, turnIds) {
|
|
|
87
106
|
if (turnId) {
|
|
88
107
|
turnIds.add(turnId);
|
|
89
108
|
}
|
|
90
|
-
if (timestampMs !== null
|
|
91
|
-
|
|
109
|
+
if (timestampMs !== null
|
|
110
|
+
&& turnId
|
|
111
|
+
&& event?.type === 'event_msg'
|
|
112
|
+
&& event?.payload?.type === 'task_started') {
|
|
113
|
+
activeTurn = { turnId, startedAtMs: timestampMs, visibleOutputTokens: 0 };
|
|
92
114
|
}
|
|
93
115
|
const info = event?.payload?.info;
|
|
94
116
|
const lastTokenUsage = info?.last_token_usage ?? info?.lastTokenUsage;
|
|
95
117
|
if (lastTokenUsage) {
|
|
96
118
|
usageEvents += 1;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
generationStartMs = timestampMs;
|
|
119
|
+
if (activeTurn) {
|
|
120
|
+
activeTurn.visibleOutputTokens += visibleOutputTokens(lastTokenUsage);
|
|
100
121
|
}
|
|
101
122
|
}
|
|
102
123
|
const totalTokenUsage = info?.total_token_usage ?? info?.totalTokenUsage;
|
|
103
124
|
if (totalTokenUsage) {
|
|
104
125
|
totalUsage = totalTokenUsage;
|
|
105
126
|
}
|
|
127
|
+
const completedTurn = activeTurn;
|
|
128
|
+
if (completedTurn !== null
|
|
129
|
+
&& timestampMs !== null
|
|
130
|
+
&& turnId
|
|
131
|
+
&& completedTurn.turnId === turnId
|
|
132
|
+
&& event?.type === 'event_msg'
|
|
133
|
+
&& (event?.payload?.type === 'task_complete' || event?.payload?.type === 'turn_aborted')) {
|
|
134
|
+
if (event.payload.type === 'task_complete' && completedTurn.visibleOutputTokens > 0) {
|
|
135
|
+
const seconds = (timestampMs - completedTurn.startedAtMs) / 1000;
|
|
136
|
+
if (Number.isFinite(seconds) && seconds > 0) {
|
|
137
|
+
completedTurnSamples.push({
|
|
138
|
+
completedAtMs: timestampMs,
|
|
139
|
+
visibleOutputTokens: completedTurn.visibleOutputTokens,
|
|
140
|
+
seconds,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
activeTurn = null;
|
|
145
|
+
}
|
|
106
146
|
}
|
|
107
147
|
}
|
|
108
148
|
finally {
|
|
109
149
|
reader.close();
|
|
110
150
|
}
|
|
111
|
-
return { usageEvents, totalUsage,
|
|
151
|
+
return { usageEvents, totalUsage, completedTurnSamples };
|
|
112
152
|
}
|
|
113
153
|
function emptyTotals() {
|
|
114
154
|
return {
|
|
@@ -119,14 +159,37 @@ function emptyTotals() {
|
|
|
119
159
|
totalTokens: 0,
|
|
120
160
|
};
|
|
121
161
|
}
|
|
122
|
-
function
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
162
|
+
function isCodexLocalUsageStats(value) {
|
|
163
|
+
if (!value || typeof value !== 'object') {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
const stats = value;
|
|
167
|
+
const totals = stats.totals;
|
|
168
|
+
const throughput = stats.responseThroughput;
|
|
169
|
+
return isFiniteNumber(stats.sessionFiles)
|
|
170
|
+
&& isFiniteNumber(stats.sessionsWithUsage)
|
|
171
|
+
&& isFiniteNumber(stats.turns)
|
|
172
|
+
&& isFiniteNumber(stats.usageEvents)
|
|
173
|
+
&& Boolean(totals)
|
|
174
|
+
&& isFiniteNumber(totals?.inputTokens)
|
|
175
|
+
&& isFiniteNumber(totals?.cachedInputTokens)
|
|
176
|
+
&& isFiniteNumber(totals?.outputTokens)
|
|
177
|
+
&& isFiniteNumber(totals?.reasoningOutputTokens)
|
|
178
|
+
&& isFiniteNumber(totals?.totalTokens)
|
|
179
|
+
&& Boolean(throughput)
|
|
180
|
+
&& isFiniteNumber(throughput?.completedTurns)
|
|
181
|
+
&& isFiniteNumber(throughput?.visibleOutputTokens)
|
|
182
|
+
&& isFiniteNumber(throughput?.seconds)
|
|
183
|
+
&& isFiniteNumber(throughput?.recentCompletedTurns)
|
|
184
|
+
&& isFiniteNumber(throughput?.recentVisibleOutputTokens)
|
|
185
|
+
&& isFiniteNumber(throughput?.recentSeconds)
|
|
186
|
+
&& isNullableFiniteNumber(stats.latestSessionMtimeMs);
|
|
187
|
+
}
|
|
188
|
+
function isFiniteNumber(value) {
|
|
189
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
190
|
+
}
|
|
191
|
+
function isNullableFiniteNumber(value) {
|
|
192
|
+
return value === null || isFiniteNumber(value);
|
|
130
193
|
}
|
|
131
194
|
function addUsage(totals, usage) {
|
|
132
195
|
const inputTokens = numberField(usage, 'input_tokens', 'inputTokens');
|
|
@@ -138,34 +201,21 @@ function addUsage(totals, usage) {
|
|
|
138
201
|
totals.reasoningOutputTokens += numberField(usage, 'reasoning_output_tokens', 'reasoningOutputTokens');
|
|
139
202
|
totals.totalTokens += totalTokens || inputTokens + outputTokens;
|
|
140
203
|
}
|
|
141
|
-
function
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
target.seconds += source.seconds;
|
|
145
|
-
if (source.latestTokensPerSecond !== null
|
|
146
|
-
&& source.latestSampleAtMs !== null
|
|
147
|
-
&& (target.latestSampleAtMs === null || source.latestSampleAtMs > target.latestSampleAtMs)) {
|
|
148
|
-
target.latestTokensPerSecond = source.latestTokensPerSecond;
|
|
149
|
-
target.latestSampleAtMs = source.latestSampleAtMs;
|
|
150
|
-
}
|
|
204
|
+
function visibleOutputTokens(usage) {
|
|
205
|
+
return Math.max(0, numberField(usage, 'output_tokens', 'outputTokens')
|
|
206
|
+
- numberField(usage, 'reasoning_output_tokens', 'reasoningOutputTokens'));
|
|
151
207
|
}
|
|
152
|
-
function
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
stats.samples += 1;
|
|
165
|
-
stats.outputTokens += outputTokens;
|
|
166
|
-
stats.seconds += seconds;
|
|
167
|
-
stats.latestTokensPerSecond = outputTokens / seconds;
|
|
168
|
-
stats.latestSampleAtMs = timestampMs;
|
|
208
|
+
function summarizeResponseThroughput(samples) {
|
|
209
|
+
const ordered = samples.slice().sort((left, right) => left.completedAtMs - right.completedAtMs);
|
|
210
|
+
const recent = ordered.slice(-RECENT_COMPLETED_TURNS);
|
|
211
|
+
return {
|
|
212
|
+
completedTurns: ordered.length,
|
|
213
|
+
visibleOutputTokens: ordered.reduce((total, sample) => total + sample.visibleOutputTokens, 0),
|
|
214
|
+
seconds: ordered.reduce((total, sample) => total + sample.seconds, 0),
|
|
215
|
+
recentCompletedTurns: recent.length,
|
|
216
|
+
recentVisibleOutputTokens: recent.reduce((total, sample) => total + sample.visibleOutputTokens, 0),
|
|
217
|
+
recentSeconds: recent.reduce((total, sample) => total + sample.seconds, 0),
|
|
218
|
+
};
|
|
169
219
|
}
|
|
170
220
|
function numberField(source, snakeKey, camelKey) {
|
|
171
221
|
const snakeValue = source[snakeKey];
|
|
@@ -180,15 +230,3 @@ function parseTimestampMs(value) {
|
|
|
180
230
|
const timestamp = Date.parse(value);
|
|
181
231
|
return Number.isFinite(timestamp) ? timestamp : null;
|
|
182
232
|
}
|
|
183
|
-
function isGenerationBoundary(event) {
|
|
184
|
-
const payload = event?.payload;
|
|
185
|
-
if (event?.type === 'response_item' && payload?.type === 'function_call_output') {
|
|
186
|
-
return true;
|
|
187
|
-
}
|
|
188
|
-
if (event?.type !== 'event_msg') {
|
|
189
|
-
return false;
|
|
190
|
-
}
|
|
191
|
-
return payload?.type === 'task_started'
|
|
192
|
-
|| payload?.type === 'exec_command_end'
|
|
193
|
-
|| payload?.type === 'user_message';
|
|
194
|
-
}
|
|
@@ -32,6 +32,10 @@ export declare class BridgeSessionCore {
|
|
|
32
32
|
private authRotationInProgress;
|
|
33
33
|
private authRotationFailedTargets;
|
|
34
34
|
private localUsageCache;
|
|
35
|
+
private localUsageCacheLoaded;
|
|
36
|
+
private localUsageRefresh;
|
|
37
|
+
private authQuotaSnapshots;
|
|
38
|
+
private authQuotaSnapshotsLoaded;
|
|
35
39
|
private lastRemoteControlStatus;
|
|
36
40
|
private pendingThreadRenames;
|
|
37
41
|
private pendingThreadNewCwds;
|
|
@@ -263,9 +267,16 @@ export declare class BridgeSessionCore {
|
|
|
263
267
|
private buildNativeCollaborationMode;
|
|
264
268
|
private buildCodexUsageStatusLines;
|
|
265
269
|
private buildCodexLocalUsageStatusLines;
|
|
266
|
-
private
|
|
270
|
+
private formatCodexLocalResponseThroughputStatusLines;
|
|
267
271
|
private resolveFastStatusLabel;
|
|
268
272
|
private readCachedCodexLocalUsageStats;
|
|
273
|
+
private refreshCodexLocalUsageIfNeeded;
|
|
274
|
+
private refreshCodexLocalUsageStats;
|
|
275
|
+
private codexLocalUsageSnapshotPath;
|
|
276
|
+
private refreshCurrentCodexAuthQuota;
|
|
277
|
+
private readCodexAuthQuotaSnapshots;
|
|
278
|
+
private writeCodexAuthQuotaSnapshots;
|
|
279
|
+
private codexAuthQuotaSnapshotPath;
|
|
269
280
|
private sendThreadContextSummary;
|
|
270
281
|
private handleModelCommand;
|
|
271
282
|
private handleEffortCommand;
|
|
@@ -11,7 +11,7 @@ import { TELEGRAM_MESSAGE_LIMIT, chunkTelegramMessage, chunkTelegramStreamMessag
|
|
|
11
11
|
import { isDefaultTelegramScope, resolveTelegramAddressing } from '../telegram/addressing.js';
|
|
12
12
|
import { BRIDGE_SCOPE_WEIXIN_PREFIX, parseTelegramTargetFromBridgeScope, parseWeixinBridgeScope } from '../core/bridge_scope.js';
|
|
13
13
|
import { resolveTelegramRenderRoute } from '../telegram/rendering.js';
|
|
14
|
-
import { readCodexLocalUsageStats } from '../codex_app/local_usage.js';
|
|
14
|
+
import { readCodexLocalUsageSnapshot, readCodexLocalUsageStats, writeCodexLocalUsageSnapshot, } from '../codex_app/local_usage.js';
|
|
15
15
|
import { normalizeTurnActivityEvent, } from './activity.js';
|
|
16
16
|
import { normalizeAccessPreset, resolveAccessMode } from './access.js';
|
|
17
17
|
import { diffObservedTurn, findLatestTurn, findLiveTurn } from './observer.js';
|
|
@@ -23,7 +23,9 @@ class UserFacingError extends Error {
|
|
|
23
23
|
const OBSERVED_THREAD_POLL_MS = 1500;
|
|
24
24
|
const OBSERVED_CLI_USER_LABEL = 'codex-cli-user';
|
|
25
25
|
const DEFAULT_COLLABORATION_MODE = 'default';
|
|
26
|
-
const
|
|
26
|
+
const CODEX_LOCAL_USAGE_REFRESH_MS = 30 * 60_000;
|
|
27
|
+
const CODEX_LOCAL_USAGE_SNAPSHOT_FILENAME = 'codex-local-usage.json';
|
|
28
|
+
const CODEX_AUTH_QUOTA_SNAPSHOT_FILENAME = 'codex-auth-quota.json';
|
|
27
29
|
const USER_INPUT_SUBMITTED_NOTICE_MS = 90_000;
|
|
28
30
|
const SELF_UPDATE_STATUS_POLL_MS = 1000;
|
|
29
31
|
const PLAN_IMPLEMENTATION_CODING_MESSAGE = 'Implement the plan.';
|
|
@@ -110,6 +112,10 @@ export class BridgeSessionCore {
|
|
|
110
112
|
authRotationInProgress = false;
|
|
111
113
|
authRotationFailedTargets = new Set();
|
|
112
114
|
localUsageCache = null;
|
|
115
|
+
localUsageCacheLoaded = false;
|
|
116
|
+
localUsageRefresh = null;
|
|
117
|
+
authQuotaSnapshots = {};
|
|
118
|
+
authQuotaSnapshotsLoaded = false;
|
|
113
119
|
lastRemoteControlStatus = null;
|
|
114
120
|
pendingThreadRenames = new Map();
|
|
115
121
|
pendingThreadNewCwds = new Map();
|
|
@@ -186,6 +192,9 @@ export class BridgeSessionCore {
|
|
|
186
192
|
await this.app.start();
|
|
187
193
|
await this.restorePendingUserInputs();
|
|
188
194
|
await this.cleanupStaleTurnPreviews();
|
|
195
|
+
void this.refreshCodexLocalUsageIfNeeded().catch((error) => {
|
|
196
|
+
this.logger.warn('codex.local_usage_background_refresh_failed', { error: formatUserError(error) });
|
|
197
|
+
});
|
|
189
198
|
this.updateStatus();
|
|
190
199
|
}
|
|
191
200
|
/** Begin Telegram Bot API long-polling after handlers and Codex are ready. */
|
|
@@ -348,7 +357,11 @@ export class BridgeSessionCore {
|
|
|
348
357
|
const binding = this.store.getBinding(scopeId);
|
|
349
358
|
const settings = this.store.getChatSettings(scopeId);
|
|
350
359
|
const access = this.resolveEffectiveAccess(scopeId, settings);
|
|
351
|
-
const fastStatus = await
|
|
360
|
+
const [fastStatus, codexUsageLines, codexLocalUsageLines] = await Promise.all([
|
|
361
|
+
this.resolveFastStatusLabel(locale, settings),
|
|
362
|
+
this.buildCodexUsageStatusLines(locale),
|
|
363
|
+
this.buildCodexLocalUsageStatusLines(locale),
|
|
364
|
+
]);
|
|
352
365
|
const appServer = this.app.getServerStatus();
|
|
353
366
|
const appServerLabel = appServer.pid && appServer.port
|
|
354
367
|
? `${appServer.running ? t(locale, 'status_app_server_running') : t(locale, 'status_app_server_stale')} pid=${appServer.pid} port=${appServer.port}`
|
|
@@ -376,8 +389,8 @@ export class BridgeSessionCore {
|
|
|
376
389
|
t(locale, 'status_pending_user_inputs', { value: this.store.countPendingUserInputs() }),
|
|
377
390
|
t(locale, 'status_active_turns', { value: this.activeTurns.size }),
|
|
378
391
|
];
|
|
379
|
-
lines.push(...
|
|
380
|
-
lines.push(...
|
|
392
|
+
lines.push(...codexUsageLines);
|
|
393
|
+
lines.push(...codexLocalUsageLines);
|
|
381
394
|
await this.sendMessage(scopeId, lines.join('\n'));
|
|
382
395
|
return;
|
|
383
396
|
}
|
|
@@ -3743,6 +3756,7 @@ export class BridgeSessionCore {
|
|
|
3743
3756
|
return;
|
|
3744
3757
|
}
|
|
3745
3758
|
const state = await this.listCodexAuthState();
|
|
3759
|
+
await this.refreshCurrentCodexAuthQuota(state);
|
|
3746
3760
|
const record = {
|
|
3747
3761
|
localId: crypto.randomBytes(8).toString('hex'),
|
|
3748
3762
|
chatId: scopeId,
|
|
@@ -4384,7 +4398,12 @@ export class BridgeSessionCore {
|
|
|
4384
4398
|
return candidate ? { candidate, fromLabel: state.currentLabel, toLabel: await authPathDisplayLabel(candidate.path) } : null;
|
|
4385
4399
|
}
|
|
4386
4400
|
async listCodexAuthState() {
|
|
4387
|
-
|
|
4401
|
+
const state = await listCodexAuthState(this.store.listDisabledCodexAuthCandidateNames());
|
|
4402
|
+
const snapshots = await this.readCodexAuthQuotaSnapshots();
|
|
4403
|
+
state.candidates.forEach((candidate) => {
|
|
4404
|
+
candidate.quota = snapshots[candidate.name] ?? null;
|
|
4405
|
+
});
|
|
4406
|
+
return state;
|
|
4388
4407
|
}
|
|
4389
4408
|
async readCodexAuthSwitchLabels(candidate) {
|
|
4390
4409
|
const state = await this.listCodexAuthState();
|
|
@@ -4486,7 +4505,7 @@ export class BridgeSessionCore {
|
|
|
4486
4505
|
}
|
|
4487
4506
|
lines.push(t(locale, 'status_codex_usage_window', {
|
|
4488
4507
|
window: formatRateLimitWindowLabel(locale, window, kind),
|
|
4489
|
-
percent:
|
|
4508
|
+
percent: formatRemainingUsagePercent(window.usedPercent),
|
|
4490
4509
|
reset: window.resetsAt
|
|
4491
4510
|
? t(locale, 'status_codex_usage_reset', { value: formatLocalTimestamp(window.resetsAt) })
|
|
4492
4511
|
: '',
|
|
@@ -4505,7 +4524,14 @@ export class BridgeSessionCore {
|
|
|
4505
4524
|
}
|
|
4506
4525
|
async buildCodexLocalUsageStatusLines(locale) {
|
|
4507
4526
|
try {
|
|
4508
|
-
const
|
|
4527
|
+
const snapshot = await this.readCachedCodexLocalUsageStats();
|
|
4528
|
+
void this.refreshCodexLocalUsageIfNeeded(snapshot).catch((error) => {
|
|
4529
|
+
this.logger.warn('codex.local_usage_background_refresh_failed', { error: formatUserError(error) });
|
|
4530
|
+
});
|
|
4531
|
+
if (!snapshot) {
|
|
4532
|
+
return [t(locale, 'status_codex_local_usage_refreshing')];
|
|
4533
|
+
}
|
|
4534
|
+
const stats = snapshot.stats;
|
|
4509
4535
|
if (stats.sessionFiles === 0 || stats.sessionsWithUsage === 0) {
|
|
4510
4536
|
return [];
|
|
4511
4537
|
}
|
|
@@ -4518,11 +4544,15 @@ export class BridgeSessionCore {
|
|
|
4518
4544
|
t(locale, 'status_codex_local_tokens', {
|
|
4519
4545
|
total: formatTokenCount(stats.totals.totalTokens),
|
|
4520
4546
|
input: formatTokenCount(stats.totals.inputTokens),
|
|
4547
|
+
visible: formatTokenCount(Math.max(0, stats.totals.outputTokens - stats.totals.reasoningOutputTokens)),
|
|
4521
4548
|
output: formatTokenCount(stats.totals.outputTokens),
|
|
4522
4549
|
cached: formatTokenCount(stats.totals.cachedInputTokens),
|
|
4523
4550
|
reasoning: formatTokenCount(stats.totals.reasoningOutputTokens),
|
|
4524
4551
|
}),
|
|
4525
|
-
...this.
|
|
4552
|
+
...this.formatCodexLocalResponseThroughputStatusLines(locale, stats),
|
|
4553
|
+
t(locale, 'status_codex_local_snapshot_at', {
|
|
4554
|
+
value: formatLocalTimestamp(snapshot.computedAtMs / 1000),
|
|
4555
|
+
}),
|
|
4526
4556
|
];
|
|
4527
4557
|
}
|
|
4528
4558
|
catch (error) {
|
|
@@ -4530,16 +4560,21 @@ export class BridgeSessionCore {
|
|
|
4530
4560
|
return [t(locale, 'status_codex_local_usage_unavailable', { error: formatShortStatusError(error) })];
|
|
4531
4561
|
}
|
|
4532
4562
|
}
|
|
4533
|
-
|
|
4534
|
-
const
|
|
4535
|
-
if (
|
|
4563
|
+
formatCodexLocalResponseThroughputStatusLines(locale, stats) {
|
|
4564
|
+
const throughput = stats.responseThroughput;
|
|
4565
|
+
if (throughput.completedTurns === 0
|
|
4566
|
+
|| throughput.visibleOutputTokens <= 0
|
|
4567
|
+
|| throughput.seconds <= 0
|
|
4568
|
+
|| throughput.recentCompletedTurns === 0
|
|
4569
|
+
|| throughput.recentVisibleOutputTokens <= 0
|
|
4570
|
+
|| throughput.recentSeconds <= 0) {
|
|
4536
4571
|
return [];
|
|
4537
4572
|
}
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4573
|
+
return [t(locale, 'status_codex_local_throughput', {
|
|
4574
|
+
overall: formatCompactNumber(throughput.visibleOutputTokens / throughput.seconds),
|
|
4575
|
+
recent: formatCompactNumber(throughput.recentVisibleOutputTokens / throughput.recentSeconds),
|
|
4576
|
+
recentTurns: formatTokenCount(throughput.recentCompletedTurns),
|
|
4577
|
+
turns: formatTokenCount(throughput.completedTurns),
|
|
4543
4578
|
})];
|
|
4544
4579
|
}
|
|
4545
4580
|
async resolveFastStatusLabel(locale, settings) {
|
|
@@ -4554,13 +4589,87 @@ export class BridgeSessionCore {
|
|
|
4554
4589
|
}
|
|
4555
4590
|
}
|
|
4556
4591
|
async readCachedCodexLocalUsageStats() {
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
return this.localUsageCache.stats;
|
|
4592
|
+
if (this.localUsageCacheLoaded) {
|
|
4593
|
+
return this.localUsageCache;
|
|
4560
4594
|
}
|
|
4595
|
+
this.localUsageCache = await readCodexLocalUsageSnapshot(this.codexLocalUsageSnapshotPath());
|
|
4596
|
+
this.localUsageCacheLoaded = true;
|
|
4597
|
+
return this.localUsageCache;
|
|
4598
|
+
}
|
|
4599
|
+
async refreshCodexLocalUsageIfNeeded(snapshot) {
|
|
4600
|
+
const current = snapshot === undefined ? await this.readCachedCodexLocalUsageStats() : snapshot;
|
|
4601
|
+
if (current && Date.now() - current.computedAtMs < CODEX_LOCAL_USAGE_REFRESH_MS) {
|
|
4602
|
+
return;
|
|
4603
|
+
}
|
|
4604
|
+
if (this.localUsageRefresh) {
|
|
4605
|
+
return;
|
|
4606
|
+
}
|
|
4607
|
+
this.localUsageRefresh = this.refreshCodexLocalUsageStats().finally(() => {
|
|
4608
|
+
this.localUsageRefresh = null;
|
|
4609
|
+
});
|
|
4610
|
+
await this.localUsageRefresh;
|
|
4611
|
+
}
|
|
4612
|
+
async refreshCodexLocalUsageStats() {
|
|
4561
4613
|
const stats = await readCodexLocalUsageStats();
|
|
4562
|
-
|
|
4563
|
-
|
|
4614
|
+
const snapshot = { computedAtMs: Date.now(), stats };
|
|
4615
|
+
this.localUsageCache = snapshot;
|
|
4616
|
+
this.localUsageCacheLoaded = true;
|
|
4617
|
+
await writeCodexLocalUsageSnapshot(this.codexLocalUsageSnapshotPath(), snapshot);
|
|
4618
|
+
}
|
|
4619
|
+
codexLocalUsageSnapshotPath() {
|
|
4620
|
+
return path.join(path.dirname(this.config.statusPath), CODEX_LOCAL_USAGE_SNAPSHOT_FILENAME);
|
|
4621
|
+
}
|
|
4622
|
+
async refreshCurrentCodexAuthQuota(state) {
|
|
4623
|
+
const candidate = state.candidates.find(entry => entry.isCurrent);
|
|
4624
|
+
if (!candidate) {
|
|
4625
|
+
return;
|
|
4626
|
+
}
|
|
4627
|
+
try {
|
|
4628
|
+
const snapshot = selectCodexRateLimitSnapshot(await this.app.readAccountRateLimits());
|
|
4629
|
+
if (!snapshot) {
|
|
4630
|
+
return;
|
|
4631
|
+
}
|
|
4632
|
+
const quota = authQuotaSnapshotFromRateLimit(snapshot);
|
|
4633
|
+
candidate.quota = quota;
|
|
4634
|
+
this.authQuotaSnapshots[candidate.name] = quota;
|
|
4635
|
+
await this.writeCodexAuthQuotaSnapshots();
|
|
4636
|
+
}
|
|
4637
|
+
catch (error) {
|
|
4638
|
+
this.logger.warn('codex.auth_quota_refresh_failed', { error: formatUserError(error) });
|
|
4639
|
+
}
|
|
4640
|
+
}
|
|
4641
|
+
async readCodexAuthQuotaSnapshots() {
|
|
4642
|
+
if (this.authQuotaSnapshotsLoaded) {
|
|
4643
|
+
return this.authQuotaSnapshots;
|
|
4644
|
+
}
|
|
4645
|
+
this.authQuotaSnapshotsLoaded = true;
|
|
4646
|
+
try {
|
|
4647
|
+
const parsed = JSON.parse(await fs.readFile(this.codexAuthQuotaSnapshotPath(), 'utf8'));
|
|
4648
|
+
if (parsed && typeof parsed === 'object') {
|
|
4649
|
+
for (const [name, value] of Object.entries(parsed)) {
|
|
4650
|
+
if (isCodexAuthQuotaSnapshot(value)) {
|
|
4651
|
+
this.authQuotaSnapshots[name] = value;
|
|
4652
|
+
}
|
|
4653
|
+
}
|
|
4654
|
+
}
|
|
4655
|
+
}
|
|
4656
|
+
catch {
|
|
4657
|
+
// A missing or invalid historical cache should not block the auth panel.
|
|
4658
|
+
}
|
|
4659
|
+
return this.authQuotaSnapshots;
|
|
4660
|
+
}
|
|
4661
|
+
async writeCodexAuthQuotaSnapshots() {
|
|
4662
|
+
const snapshotPath = this.codexAuthQuotaSnapshotPath();
|
|
4663
|
+
await fs.mkdir(path.dirname(snapshotPath), { recursive: true });
|
|
4664
|
+
const temporaryPath = `${snapshotPath}.${process.pid}.tmp`;
|
|
4665
|
+
await fs.writeFile(temporaryPath, `${JSON.stringify(this.authQuotaSnapshots, null, 2)}\n`, {
|
|
4666
|
+
encoding: 'utf8',
|
|
4667
|
+
mode: 0o600,
|
|
4668
|
+
});
|
|
4669
|
+
await fs.rename(temporaryPath, snapshotPath);
|
|
4670
|
+
}
|
|
4671
|
+
codexAuthQuotaSnapshotPath() {
|
|
4672
|
+
return path.join(path.dirname(this.config.statusPath), CODEX_AUTH_QUOTA_SNAPSHOT_FILENAME);
|
|
4564
4673
|
}
|
|
4565
4674
|
async sendThreadContextSummary(scopeId, locale, threadId) {
|
|
4566
4675
|
try {
|
|
@@ -7039,6 +7148,7 @@ async function listCodexAuthState(disabledNames = new Set()) {
|
|
|
7039
7148
|
isCurrent: currentTargetPath === candidatePath,
|
|
7040
7149
|
disabled: disabledNames.has(entry.name),
|
|
7041
7150
|
mtimeMs: stat.mtimeMs,
|
|
7151
|
+
quota: null,
|
|
7042
7152
|
});
|
|
7043
7153
|
}
|
|
7044
7154
|
candidates.sort((left, right) => left.name.localeCompare(right.name, undefined, { numeric: true }));
|
|
@@ -7138,12 +7248,13 @@ function renderAuthListMessage(locale, state, includeWeixinCopyPaste = false) {
|
|
|
7138
7248
|
return lines.join('\n');
|
|
7139
7249
|
}
|
|
7140
7250
|
lines.push(t(locale, 'auth_candidate_count', { value: state.candidates.length }));
|
|
7251
|
+
lines.push(t(locale, 'auth_quota_legend'));
|
|
7141
7252
|
state.candidates.forEach((candidate, index) => {
|
|
7142
7253
|
const marker = candidate.isCurrent ? ' *' : '';
|
|
7143
7254
|
const status = candidate.disabled
|
|
7144
7255
|
? t(locale, 'auth_candidate_status_disabled')
|
|
7145
7256
|
: t(locale, 'auth_candidate_status_enabled');
|
|
7146
|
-
lines.push(`${index + 1}. ${candidate.name}${marker} [${status}]`);
|
|
7257
|
+
lines.push(`${index + 1}. ${formatAuthQuotaPrefix(candidate.quota)}|${candidate.name}${marker} [${status}]`);
|
|
7147
7258
|
});
|
|
7148
7259
|
if (includeWeixinCopyPaste) {
|
|
7149
7260
|
lines.push('', t(locale, 'weixin_copy_paste_divider'), t(locale, 'weixin_copy_auth_title'), ...state.candidates.map((_candidate, index) => `/auth use ${index + 1}`), ...state.candidates.map((candidate, index) => candidate.disabled
|
|
@@ -7155,7 +7266,7 @@ function renderAuthListMessage(locale, state, includeWeixinCopyPaste = false) {
|
|
|
7155
7266
|
function authChoiceKeyboard(locale, record) {
|
|
7156
7267
|
const rows = record.candidates.map((candidate, index) => [
|
|
7157
7268
|
{
|
|
7158
|
-
text: clipButtonText(`${candidate.isCurrent ? '✅ ' : '🔐 '}${candidate.name}${candidate.disabled ? ' · off' : ''}`),
|
|
7269
|
+
text: clipButtonText(`${candidate.isCurrent ? '✅ ' : '🔐 '}${formatAuthQuotaPrefix(candidate.quota)}|${candidate.name}${candidate.disabled ? ' · off' : ''}`),
|
|
7159
7270
|
callback_data: `auth:${record.localId}:${index}`,
|
|
7160
7271
|
},
|
|
7161
7272
|
{
|
|
@@ -7692,6 +7803,44 @@ function formatUsagePercent(value) {
|
|
|
7692
7803
|
}
|
|
7693
7804
|
return formatCompactNumber(value);
|
|
7694
7805
|
}
|
|
7806
|
+
function formatRemainingUsagePercent(usedPercent) {
|
|
7807
|
+
const remainingPercent = remainingUsagePercent(usedPercent);
|
|
7808
|
+
return remainingPercent === null ? '?' : formatUsagePercent(remainingPercent);
|
|
7809
|
+
}
|
|
7810
|
+
function authQuotaSnapshotFromRateLimit(snapshot) {
|
|
7811
|
+
return {
|
|
7812
|
+
capturedAtMs: Date.now(),
|
|
7813
|
+
primaryRemainingPercent: snapshot.primary ? remainingUsagePercent(snapshot.primary.usedPercent) : null,
|
|
7814
|
+
secondaryRemainingPercent: snapshot.secondary ? remainingUsagePercent(snapshot.secondary.usedPercent) : null,
|
|
7815
|
+
};
|
|
7816
|
+
}
|
|
7817
|
+
function remainingUsagePercent(usedPercent) {
|
|
7818
|
+
if (!Number.isFinite(usedPercent)) {
|
|
7819
|
+
return null;
|
|
7820
|
+
}
|
|
7821
|
+
return Math.max(0, Math.min(100, 100 - usedPercent));
|
|
7822
|
+
}
|
|
7823
|
+
function formatAuthQuotaPrefix(snapshot) {
|
|
7824
|
+
if (!snapshot) {
|
|
7825
|
+
return '--|--';
|
|
7826
|
+
}
|
|
7827
|
+
const primary = snapshot.primaryRemainingPercent === null ? '--' : formatUsagePercent(snapshot.primaryRemainingPercent);
|
|
7828
|
+
const secondary = snapshot.secondaryRemainingPercent === null ? '--' : formatUsagePercent(snapshot.secondaryRemainingPercent);
|
|
7829
|
+
return `${primary}|${secondary}`;
|
|
7830
|
+
}
|
|
7831
|
+
function isCodexAuthQuotaSnapshot(value) {
|
|
7832
|
+
if (!value || typeof value !== 'object') {
|
|
7833
|
+
return false;
|
|
7834
|
+
}
|
|
7835
|
+
const snapshot = value;
|
|
7836
|
+
return typeof snapshot.capturedAtMs === 'number'
|
|
7837
|
+
&& Number.isFinite(snapshot.capturedAtMs)
|
|
7838
|
+
&& isNullableFiniteNumber(snapshot.primaryRemainingPercent)
|
|
7839
|
+
&& isNullableFiniteNumber(snapshot.secondaryRemainingPercent);
|
|
7840
|
+
}
|
|
7841
|
+
function isNullableFiniteNumber(value) {
|
|
7842
|
+
return value === null || (typeof value === 'number' && Number.isFinite(value));
|
|
7843
|
+
}
|
|
7695
7844
|
function formatCompactNumber(value) {
|
|
7696
7845
|
return Number.isInteger(value) ? String(value) : value.toFixed(1).replace(/\.0$/, '');
|
|
7697
7846
|
}
|
package/dist/i18n.d.ts
CHANGED
|
@@ -90,7 +90,7 @@ declare const MESSAGES: {
|
|
|
90
90
|
readonly status_app_server: "Codex app-server: {value}";
|
|
91
91
|
readonly status_app_server_running: "running";
|
|
92
92
|
readonly status_app_server_stale: "stale";
|
|
93
|
-
readonly status_user_agent: "
|
|
93
|
+
readonly status_user_agent: "Codex/FoxClaw agent: {value}";
|
|
94
94
|
readonly status_current_thread: "Current thread: {value}";
|
|
95
95
|
readonly status_configured_model: "Configured model: {value}";
|
|
96
96
|
readonly status_configured_effort: "Configured effort: {value}";
|
|
@@ -107,12 +107,14 @@ declare const MESSAGES: {
|
|
|
107
107
|
readonly status_codex_account: "Codex account: {value}";
|
|
108
108
|
readonly status_codex_plan: "Codex plan: {value}";
|
|
109
109
|
readonly status_codex_usage_title: "Codex usage ({value}):";
|
|
110
|
-
readonly status_codex_usage_window: "{window}: {percent}%
|
|
110
|
+
readonly status_codex_usage_window: "{window}: {percent}% remaining{reset}";
|
|
111
111
|
readonly status_codex_usage_reset: ", resets {value}";
|
|
112
112
|
readonly status_codex_usage_unavailable: "Codex usage: unavailable ({error})";
|
|
113
113
|
readonly status_codex_local_history: "Codex local history: {sessions} sessions, {turns} turns, {events} usage records";
|
|
114
|
-
readonly status_codex_local_tokens: "Codex local tokens: total {total}; input {input}, output {
|
|
115
|
-
readonly
|
|
114
|
+
readonly status_codex_local_tokens: "Codex local tokens: total {total}; input {input}, visible output {visible}, reasoning output {reasoning}, total output {output}, cached input {cached}";
|
|
115
|
+
readonly status_codex_local_throughput: "Codex visible reply throughput (end-to-end, excluding reasoning): overall {overall} token/s, last {recentTurns} completed turns {recent} token/s ({turns} completed turns sampled)";
|
|
116
|
+
readonly status_codex_local_snapshot_at: "Codex local stats snapshot: {value}";
|
|
117
|
+
readonly status_codex_local_usage_refreshing: "Codex local history: building snapshot in background";
|
|
116
118
|
readonly status_codex_local_usage_unavailable: "Codex local history: unavailable ({error})";
|
|
117
119
|
readonly status_codex_credits: "Codex credits: {value}";
|
|
118
120
|
readonly status_codex_limit_reached: "Codex limit: {value}";
|
|
@@ -131,6 +133,7 @@ declare const MESSAGES: {
|
|
|
131
133
|
readonly auth_current: "Current auth: {value}";
|
|
132
134
|
readonly auth_dir: "Auth dir: {value}";
|
|
133
135
|
readonly auth_candidate_count: "Candidates: {value}";
|
|
136
|
+
readonly auth_quota_legend: "Quota remaining: 5h|7d|auth";
|
|
134
137
|
readonly auth_candidate_status_enabled: "enabled";
|
|
135
138
|
readonly auth_candidate_status_disabled: "disabled";
|
|
136
139
|
readonly auth_candidate_enabled: "Enabled auth candidate for auto-rotation: {value}";
|
|
@@ -651,7 +654,7 @@ declare const MESSAGES: {
|
|
|
651
654
|
readonly status_app_server: "Codex app-server:{value}";
|
|
652
655
|
readonly status_app_server_running: "运行中";
|
|
653
656
|
readonly status_app_server_stale: "失效";
|
|
654
|
-
readonly status_user_agent: "
|
|
657
|
+
readonly status_user_agent: "Codex/FoxClaw 标识:{value}";
|
|
655
658
|
readonly status_current_thread: "当前线程:{value}";
|
|
656
659
|
readonly status_configured_model: "已配置模型:{value}";
|
|
657
660
|
readonly status_configured_effort: "已配置推理强度:{value}";
|
|
@@ -668,12 +671,14 @@ declare const MESSAGES: {
|
|
|
668
671
|
readonly status_codex_account: "Codex 账号:{value}";
|
|
669
672
|
readonly status_codex_plan: "Codex 套餐:{value}";
|
|
670
673
|
readonly status_codex_usage_title: "Codex 用量({value}):";
|
|
671
|
-
readonly status_codex_usage_window: "{window}
|
|
674
|
+
readonly status_codex_usage_window: "{window}:剩余 {percent}%{reset}";
|
|
672
675
|
readonly status_codex_usage_reset: ",重置时间 {value}";
|
|
673
676
|
readonly status_codex_usage_unavailable: "Codex 用量:无法获取({error})";
|
|
674
677
|
readonly status_codex_local_history: "Codex 本地历史:{sessions} 个会话,{turns} 轮,{events} 条用量记录";
|
|
675
|
-
readonly status_codex_local_tokens: "Codex 本地 Token:总计 {total};输入 {input}
|
|
676
|
-
readonly
|
|
678
|
+
readonly status_codex_local_tokens: "Codex 本地 Token:总计 {total};输入 {input},可见输出 {visible},推理输出 {reasoning},总输出 {output},缓存输入 {cached}";
|
|
679
|
+
readonly status_codex_local_throughput: "Codex 可见答复吞吐(端到端,排除推理 token):整体 {overall} token/s,最近 {recentTurns} 个完成轮次 {recent} token/s({turns} 个完成轮次样本)";
|
|
680
|
+
readonly status_codex_local_snapshot_at: "Codex 本地统计快照:{value}";
|
|
681
|
+
readonly status_codex_local_usage_refreshing: "Codex 本地历史:正在后台生成统计快照";
|
|
677
682
|
readonly status_codex_local_usage_unavailable: "Codex 本地历史:无法获取({error})";
|
|
678
683
|
readonly status_codex_credits: "Codex 额度:{value}";
|
|
679
684
|
readonly status_codex_limit_reached: "Codex 限制:{value}";
|
|
@@ -692,6 +697,7 @@ declare const MESSAGES: {
|
|
|
692
697
|
readonly auth_current: "当前 auth:{value}";
|
|
693
698
|
readonly auth_dir: "Auth 目录:{value}";
|
|
694
699
|
readonly auth_candidate_count: "候选数量:{value}";
|
|
700
|
+
readonly auth_quota_legend: "额度剩余:5h|7d|auth";
|
|
695
701
|
readonly auth_candidate_status_enabled: "启用";
|
|
696
702
|
readonly auth_candidate_status_disabled: "禁用";
|
|
697
703
|
readonly auth_candidate_enabled: "已启用 auth 候选自动轮换:{value}";
|
package/dist/i18n.js
CHANGED
|
@@ -88,7 +88,7 @@ const MESSAGES = {
|
|
|
88
88
|
status_app_server: 'Codex app-server: {value}',
|
|
89
89
|
status_app_server_running: 'running',
|
|
90
90
|
status_app_server_stale: 'stale',
|
|
91
|
-
status_user_agent: '
|
|
91
|
+
status_user_agent: 'Codex/FoxClaw agent: {value}',
|
|
92
92
|
status_current_thread: 'Current thread: {value}',
|
|
93
93
|
status_configured_model: 'Configured model: {value}',
|
|
94
94
|
status_configured_effort: 'Configured effort: {value}',
|
|
@@ -105,12 +105,14 @@ const MESSAGES = {
|
|
|
105
105
|
status_codex_account: 'Codex account: {value}',
|
|
106
106
|
status_codex_plan: 'Codex plan: {value}',
|
|
107
107
|
status_codex_usage_title: 'Codex usage ({value}):',
|
|
108
|
-
status_codex_usage_window: '{window}: {percent}%
|
|
108
|
+
status_codex_usage_window: '{window}: {percent}% remaining{reset}',
|
|
109
109
|
status_codex_usage_reset: ', resets {value}',
|
|
110
110
|
status_codex_usage_unavailable: 'Codex usage: unavailable ({error})',
|
|
111
111
|
status_codex_local_history: 'Codex local history: {sessions} sessions, {turns} turns, {events} usage records',
|
|
112
|
-
status_codex_local_tokens: 'Codex local tokens: total {total}; input {input}, output {
|
|
113
|
-
|
|
112
|
+
status_codex_local_tokens: 'Codex local tokens: total {total}; input {input}, visible output {visible}, reasoning output {reasoning}, total output {output}, cached input {cached}',
|
|
113
|
+
status_codex_local_throughput: 'Codex visible reply throughput (end-to-end, excluding reasoning): overall {overall} token/s, last {recentTurns} completed turns {recent} token/s ({turns} completed turns sampled)',
|
|
114
|
+
status_codex_local_snapshot_at: 'Codex local stats snapshot: {value}',
|
|
115
|
+
status_codex_local_usage_refreshing: 'Codex local history: building snapshot in background',
|
|
114
116
|
status_codex_local_usage_unavailable: 'Codex local history: unavailable ({error})',
|
|
115
117
|
status_codex_credits: 'Codex credits: {value}',
|
|
116
118
|
status_codex_limit_reached: 'Codex limit: {value}',
|
|
@@ -129,6 +131,7 @@ const MESSAGES = {
|
|
|
129
131
|
auth_current: 'Current auth: {value}',
|
|
130
132
|
auth_dir: 'Auth dir: {value}',
|
|
131
133
|
auth_candidate_count: 'Candidates: {value}',
|
|
134
|
+
auth_quota_legend: 'Quota remaining: 5h|7d|auth',
|
|
132
135
|
auth_candidate_status_enabled: 'enabled',
|
|
133
136
|
auth_candidate_status_disabled: 'disabled',
|
|
134
137
|
auth_candidate_enabled: 'Enabled auth candidate for auto-rotation: {value}',
|
|
@@ -649,7 +652,7 @@ const MESSAGES = {
|
|
|
649
652
|
status_app_server: 'Codex app-server:{value}',
|
|
650
653
|
status_app_server_running: '运行中',
|
|
651
654
|
status_app_server_stale: '失效',
|
|
652
|
-
status_user_agent: '
|
|
655
|
+
status_user_agent: 'Codex/FoxClaw 标识:{value}',
|
|
653
656
|
status_current_thread: '当前线程:{value}',
|
|
654
657
|
status_configured_model: '已配置模型:{value}',
|
|
655
658
|
status_configured_effort: '已配置推理强度:{value}',
|
|
@@ -666,12 +669,14 @@ const MESSAGES = {
|
|
|
666
669
|
status_codex_account: 'Codex 账号:{value}',
|
|
667
670
|
status_codex_plan: 'Codex 套餐:{value}',
|
|
668
671
|
status_codex_usage_title: 'Codex 用量({value}):',
|
|
669
|
-
status_codex_usage_window: '{window}
|
|
672
|
+
status_codex_usage_window: '{window}:剩余 {percent}%{reset}',
|
|
670
673
|
status_codex_usage_reset: ',重置时间 {value}',
|
|
671
674
|
status_codex_usage_unavailable: 'Codex 用量:无法获取({error})',
|
|
672
675
|
status_codex_local_history: 'Codex 本地历史:{sessions} 个会话,{turns} 轮,{events} 条用量记录',
|
|
673
|
-
status_codex_local_tokens: 'Codex 本地 Token:总计 {total};输入 {input}
|
|
674
|
-
|
|
676
|
+
status_codex_local_tokens: 'Codex 本地 Token:总计 {total};输入 {input},可见输出 {visible},推理输出 {reasoning},总输出 {output},缓存输入 {cached}',
|
|
677
|
+
status_codex_local_throughput: 'Codex 可见答复吞吐(端到端,排除推理 token):整体 {overall} token/s,最近 {recentTurns} 个完成轮次 {recent} token/s({turns} 个完成轮次样本)',
|
|
678
|
+
status_codex_local_snapshot_at: 'Codex 本地统计快照:{value}',
|
|
679
|
+
status_codex_local_usage_refreshing: 'Codex 本地历史:正在后台生成统计快照',
|
|
675
680
|
status_codex_local_usage_unavailable: 'Codex 本地历史:无法获取({error})',
|
|
676
681
|
status_codex_credits: 'Codex 额度:{value}',
|
|
677
682
|
status_codex_limit_reached: 'Codex 限制:{value}',
|
|
@@ -690,6 +695,7 @@ const MESSAGES = {
|
|
|
690
695
|
auth_current: '当前 auth:{value}',
|
|
691
696
|
auth_dir: 'Auth 目录:{value}',
|
|
692
697
|
auth_candidate_count: '候选数量:{value}',
|
|
698
|
+
auth_quota_legend: '额度剩余:5h|7d|auth',
|
|
693
699
|
auth_candidate_status_enabled: '启用',
|
|
694
700
|
auth_candidate_status_disabled: '禁用',
|
|
695
701
|
auth_candidate_enabled: '已启用 auth 候选自动轮换:{value}',
|
package/docs/user-manual.md
CHANGED
|
@@ -230,7 +230,7 @@ Later commands are sorted by recent usage. Plain text, photos, and files continu
|
|
|
230
230
|
|
|
231
231
|
### 3.2 `/status`, `/account`, `/quota`, `/update`
|
|
232
232
|
|
|
233
|
-
- `/status`: FoxClaw, app-server, current thread binding, model, access, and Codex usage summary.
|
|
233
|
+
- `/status`: FoxClaw, app-server, current thread binding, model, access, and Codex usage summary. Local session, token, and visible-reply-throughput metrics use a background-generated historical snapshot instead of scanning large logs during the request; throughput is computed end-to-end for completed turns, excluding reasoning tokens while including waiting and tool execution time.
|
|
234
234
|
- `/account`: current Codex account.
|
|
235
235
|
- `/quota`: Codex usage and quota window.
|
|
236
236
|
- `/update`: upgrade FoxClaw, run checks, and restart the service; it refuses while a turn, approval, or question is active, then reports the result after restart.
|
|
@@ -387,7 +387,7 @@ If the login is cancelled or fails, FoxClaw tries to restore the previous auth t
|
|
|
387
387
|
|
|
388
388
|
### 6.3 The `/auth` Panel
|
|
389
389
|
|
|
390
|
-
`/auth` lists candidate accounts, the current account, and the auth directory. It also provides buttons for switching, disabling, login, and reload.
|
|
390
|
+
`/auth` lists candidate accounts, the current account, and the auth directory. It also provides buttons for switching, disabling, login, and reload. The `5h|7d` numbers before each filename are the last recorded remaining percentages for the two quota windows; the current auth is refreshed when the panel opens, while other candidates are not switched merely to query quota.
|
|
391
391
|
|
|
392
392
|
Approximation:
|
|
393
393
|
|
|
@@ -396,16 +396,17 @@ Codex auth
|
|
|
396
396
|
Current: auth.json_personal
|
|
397
397
|
Auth dir: /home/alice/.codex
|
|
398
398
|
Candidates: 2
|
|
399
|
-
|
|
400
|
-
|
|
399
|
+
Quota remaining: 5h|7d|auth
|
|
400
|
+
1. 20|25|auth.json_personal * [enabled]
|
|
401
|
+
2. --|--|auth.json_team [enabled]
|
|
401
402
|
|
|
402
|
-
[✅ auth.json_personal] [✅]
|
|
403
|
-
[🔐 auth.json_team] [✅]
|
|
403
|
+
[✅ 20|25|auth.json_personal] [✅]
|
|
404
|
+
[🔐 --|--|auth.json_team] [✅]
|
|
404
405
|
[🛡️ Access] [🔑 Login]
|
|
405
406
|
[🔄 Reload auth]
|
|
406
407
|
```
|
|
407
408
|
|
|
408
|
-
The right-side `✅` / `⏸️` button shows the current state. Tapping it toggles enabled/disabled, and the refreshed list shows the new state.
|
|
409
|
+
The right-side `✅` / `⏸️` button shows the current state. Tapping it toggles enabled/disabled, and the refreshed list shows the new state. `--|--` means no quota snapshot has been observed for that candidate yet.
|
|
409
410
|
|
|
410
411
|
Equivalent commands:
|
|
411
412
|
|
package/docs/zh/user-manual.md
CHANGED
|
@@ -230,7 +230,7 @@ TG_ALLOWED_TOPIC_ID=42
|
|
|
230
230
|
|
|
231
231
|
### 3.2 `/status`、`/account`、`/quota`、`/update`
|
|
232
232
|
|
|
233
|
-
- `/status`:查看 FoxClaw、app-server、当前绑定线程、模型、权限和 Codex
|
|
233
|
+
- `/status`:查看 FoxClaw、app-server、当前绑定线程、模型、权限和 Codex 用量摘要。本地 session/Token/可见答复吞吐使用后台生成的历史快照,避免状态查询现场扫描大量日志;答复吞吐按完成轮次端到端耗时计算,排除推理 token,但包含等待与工具执行时间。
|
|
234
234
|
- `/account`:查看当前 Codex 登录账号。
|
|
235
235
|
- `/quota`:查看 Codex 用量和额度窗口。
|
|
236
236
|
- `/update`:升级 FoxClaw、自检并重启服务;当前有运行中回复、审批或待确认问题时会拒绝执行,重启后会回报结果。
|
|
@@ -387,7 +387,7 @@ cp -L ~/.codex/auth.json ~/.codex/auth.json_personal
|
|
|
387
387
|
|
|
388
388
|
### 6.3 `/auth` 面板
|
|
389
389
|
|
|
390
|
-
`/auth` 会列出候选账号、当前账号和 auth
|
|
390
|
+
`/auth` 会列出候选账号、当前账号和 auth 目录,并提供按钮切换、禁用、登录和重载。每个候选名前的 `5h|7d` 数字表示上次记录到的两个额度窗口剩余百分比;当前 auth 会在打开面板时刷新,其他候选不会为了查询额度被自动切换。
|
|
391
391
|
|
|
392
392
|
示意:
|
|
393
393
|
|
|
@@ -396,16 +396,17 @@ Codex auth
|
|
|
396
396
|
Current: auth.json_personal
|
|
397
397
|
Auth dir: /home/alice/.codex
|
|
398
398
|
Candidates: 2
|
|
399
|
-
|
|
400
|
-
|
|
399
|
+
额度剩余:5h|7d|auth
|
|
400
|
+
1. 20|25|auth.json_personal * [enabled]
|
|
401
|
+
2. --|--|auth.json_team [enabled]
|
|
401
402
|
|
|
402
|
-
[✅ auth.json_personal] [✅]
|
|
403
|
-
[🔐 auth.json_team] [✅]
|
|
403
|
+
[✅ 20|25|auth.json_personal] [✅]
|
|
404
|
+
[🔐 --|--|auth.json_team] [✅]
|
|
404
405
|
[🛡️ Access] [🔑 设备登录]
|
|
405
406
|
[🔄 Reload auth]
|
|
406
407
|
```
|
|
407
408
|
|
|
408
|
-
右侧 `✅` / `⏸️`
|
|
409
|
+
右侧 `✅` / `⏸️` 表示当前状态。点一下会切换启用/禁用,列表刷新后图标会随状态变化。`--|--` 表示该候选还没有额度历史快照。
|
|
409
410
|
|
|
410
411
|
命令等价用法:
|
|
411
412
|
|
package/package.json
CHANGED
package/skills/foxclaw/SKILL.md
CHANGED
|
@@ -190,10 +190,12 @@ Use this checklist when the user asks for standard closing actions, release wrap
|
|
|
190
190
|
- If `doctor` fails only because `DEFAULT_CWD` is missing, report that separately; do not treat it as evidence that the service update failed.
|
|
191
191
|
7. Publish to npm when requested:
|
|
192
192
|
- Prefer GitHub Actions trusted publishing via `.github/workflows/publish.yml`: bump and commit the package version, push `main`, then push a matching `v<version>` tag. The tag version must match `package.json`.
|
|
193
|
+
- Treat `workflow_dispatch` only as a retry path from an existing matching release tag; do not manually run publishing from `main`.
|
|
193
194
|
- Configure npmjs.com trusted publishing for `foxden-app/foxclaw` and workflow `publish.yml`; do not store npm tokens if OIDC trusted publishing is available.
|
|
194
195
|
- Temporary fallback: store an npm automation/bypass-2FA token as the GitHub Actions secret `NPM_TOKEN`. Never print the token or commit it.
|
|
195
196
|
- Check `npm whoami` and `npm view @foxden-app/foxclaw version`.
|
|
196
197
|
- If the target version is already published, bump with `npm version patch --no-git-tag-version` before validation and commit.
|
|
198
|
+
- If a workflow fails before `npm publish`, inspect that failed GitHub Actions step before attributing it to npm trusted publishing.
|
|
197
199
|
- Manual fallback only: run `BROWSER=true npm publish` in a TTY.
|
|
198
200
|
- If npm returns `ENEEDAUTH`, report that npm publish is blocked by registry login and leave the package unreported as published.
|
|
199
201
|
- Never print npm tokens or `.npmrc` auth values.
|
|
@@ -73,6 +73,8 @@ Use this path when the repo has `.github/workflows/publish.yml` and npmjs.com ha
|
|
|
73
73
|
npm view <package-name> version
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
+
If the workflow provides `workflow_dispatch` as a recovery path, dispatch it only from an existing release tag whose version matches `package.json`. Do not dispatch publishing from a branch containing an unpublished version unless the workflow explicitly resolves and validates a release tag.
|
|
77
|
+
|
|
76
78
|
Do not store or print npm tokens when trusted publishing is available. If trusted publishing is not configured on npmjs.com, the workflow can use a GitHub Actions secret named `NPM_TOKEN` as a temporary fallback. Store only automation/bypass-2FA tokens there, never paste tokens in chat or commit them.
|
|
77
79
|
|
|
78
80
|
### Manual Publish
|
|
@@ -124,4 +126,5 @@ Press ENTER to open in the browser...
|
|
|
124
126
|
- If `xdg-open` fails because the environment has no browser, rerun with `BROWSER=true npm publish`.
|
|
125
127
|
- If the auth link expires or the publish process exits, rerun `BROWSER=true npm publish` to generate a new link.
|
|
126
128
|
- If the user provides a classic authenticator OTP instead of using the web link, publish can be retried with `npm publish --otp <code>`, but prefer web auth when the user asks for a clickable confirmation link.
|
|
129
|
+
- If a GitHub Actions release fails before the `npm publish` step, such as during checkout or action download authentication, do not diagnose it as a trusted publishing rejection. Inspect the failed step and GitHub Actions status first.
|
|
127
130
|
- Never print npm tokens or `.npmrc` auth values.
|