ftown-bridge 0.20.4 → 0.20.6
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/centrifugo-client.d.ts +11 -0
- package/dist/centrifugo-client.js +76 -0
- package/dist/centrifugo-client.js.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/jsonl-fold-cache.d.ts +59 -0
- package/dist/jsonl-fold-cache.js +213 -0
- package/dist/jsonl-fold-cache.js.map +1 -0
- package/dist/security/bridge/direct.d.ts +1 -1
- package/dist/security/bridge/direct.js +1 -2
- package/dist/security/bridge/direct.js.map +1 -1
- package/dist/security/bridge/security.d.ts +11 -0
- package/dist/security/bridge/security.js +50 -5
- package/dist/security/bridge/security.js.map +1 -1
- package/dist/security/bridge/snapshot.d.ts +2 -1
- package/dist/security/bridge/snapshot.js +26 -3
- package/dist/security/bridge/snapshot.js.map +1 -1
- package/dist/security/security-nudge.d.ts +31 -0
- package/dist/security/security-nudge.js +98 -0
- package/dist/security/security-nudge.js.map +1 -0
- package/dist/usage-collector.d.ts +3 -0
- package/dist/usage-collector.js +150 -178
- package/dist/usage-collector.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export const SECURITY_NUDGE_TYPE = 'ftown_security_changed';
|
|
2
|
+
/** `bridges:security#${userId}`; userId is the Centrifugo user (account email). */
|
|
3
|
+
export function securityNudgeChannel(userId) {
|
|
4
|
+
if (!userId)
|
|
5
|
+
throw new Error('securityNudgeChannel: userId is required');
|
|
6
|
+
return `bridges:security#${userId}`;
|
|
7
|
+
}
|
|
8
|
+
export function securityNudge(scope) {
|
|
9
|
+
return { type: SECURITY_NUDGE_TYPE, version: 1, scope };
|
|
10
|
+
}
|
|
11
|
+
export function parseSecurityNudge(data) {
|
|
12
|
+
if (typeof data !== 'object' || data === null || Array.isArray(data))
|
|
13
|
+
return null;
|
|
14
|
+
const proto = Object.getPrototypeOf(data);
|
|
15
|
+
if (proto !== Object.prototype && proto !== null)
|
|
16
|
+
return null;
|
|
17
|
+
const record = data;
|
|
18
|
+
const keys = Object.keys(record);
|
|
19
|
+
if (keys.length !== 3 || !keys.includes('type') || !keys.includes('version') || !keys.includes('scope'))
|
|
20
|
+
return null;
|
|
21
|
+
const type = record.type;
|
|
22
|
+
const version = record.version;
|
|
23
|
+
const scope = record.scope;
|
|
24
|
+
if (type !== SECURITY_NUDGE_TYPE || version !== 1)
|
|
25
|
+
return null;
|
|
26
|
+
return scope === 'approvals' || scope === 'account' ? scope : null;
|
|
27
|
+
}
|
|
28
|
+
/** Minimum spacing between nudge-triggered refreshes of one scope at one receiver. */
|
|
29
|
+
export const SECURITY_NUDGE_MIN_SPACING_MS = 1000;
|
|
30
|
+
/** Bridge-only safety re-read of account trust when no nudge arrives. */
|
|
31
|
+
export const ACCOUNT_SAFETY_REFRESH_MS = 3600000;
|
|
32
|
+
const defaultTimers = {
|
|
33
|
+
now: () => Date.now(),
|
|
34
|
+
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
35
|
+
clearTimeout: (handle) => clearTimeout(handle),
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Calls `run` for request()s, never two runs at once, and never two run starts closer than minSpacingMs.
|
|
39
|
+
* Requests during a run or inside the spacing window collapse into ONE trailing run.
|
|
40
|
+
* `run` rejections are swallowed. After dispose(), nothing runs. Timers use the injected `timers`.
|
|
41
|
+
*/
|
|
42
|
+
export function createCoalescedTrigger(run, minSpacingMs, timers = defaultTimers) {
|
|
43
|
+
let disposed = false;
|
|
44
|
+
let running = false;
|
|
45
|
+
let pending = false;
|
|
46
|
+
let lastStart = null;
|
|
47
|
+
let timer = null;
|
|
48
|
+
const schedule = () => {
|
|
49
|
+
if (disposed || running || !pending || timer !== null)
|
|
50
|
+
return;
|
|
51
|
+
const wait = lastStart === null ? 0 : lastStart + minSpacingMs - timers.now();
|
|
52
|
+
if (wait <= 0) {
|
|
53
|
+
start();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
timer = timers.setTimeout(() => { timer = null; schedule(); }, wait);
|
|
57
|
+
};
|
|
58
|
+
const finish = () => {
|
|
59
|
+
running = false;
|
|
60
|
+
schedule();
|
|
61
|
+
};
|
|
62
|
+
const start = () => {
|
|
63
|
+
pending = false;
|
|
64
|
+
running = true;
|
|
65
|
+
lastStart = timers.now();
|
|
66
|
+
let result;
|
|
67
|
+
try {
|
|
68
|
+
result = run();
|
|
69
|
+
}
|
|
70
|
+
catch (_error) {
|
|
71
|
+
finish();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (result && typeof result.then === 'function') {
|
|
75
|
+
result.then(finish, finish);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
finish();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
return {
|
|
82
|
+
request() {
|
|
83
|
+
if (disposed)
|
|
84
|
+
return;
|
|
85
|
+
pending = true;
|
|
86
|
+
schedule();
|
|
87
|
+
},
|
|
88
|
+
dispose() {
|
|
89
|
+
disposed = true;
|
|
90
|
+
pending = false;
|
|
91
|
+
if (timer !== null) {
|
|
92
|
+
timers.clearTimeout(timer);
|
|
93
|
+
timer = null;
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=security-nudge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security-nudge.js","sourceRoot":"","sources":["../../src/security/security-nudge.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type JsonlFoldCache } from './jsonl-fold-cache.js';
|
|
1
2
|
import type { Session, SessionUsage } from './types.js';
|
|
2
3
|
/**
|
|
3
4
|
* Per-session token usage extraction from harness-native session files.
|
|
@@ -38,6 +39,8 @@ export interface UsageCollectorOptions {
|
|
|
38
39
|
museSessionsDir?: string;
|
|
39
40
|
/** Cap on wall-clock read time per collection. Default 15s. */
|
|
40
41
|
timeoutMs?: number;
|
|
42
|
+
/** Incremental read cache. Default: defaultJsonlFoldCache (shared process-wide). Tests inject a fresh one. */
|
|
43
|
+
foldCache?: JsonlFoldCache;
|
|
41
44
|
}
|
|
42
45
|
/**
|
|
43
46
|
* Claude Code project-directory slug: every non-alphanumeric character of the
|
package/dist/usage-collector.js
CHANGED
|
@@ -3,7 +3,39 @@ import { readFile, readdir, realpath, stat } from 'node:fs/promises';
|
|
|
3
3
|
import { createInterface } from 'node:readline';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { join, resolve, sep } from 'node:path';
|
|
6
|
+
import { defaultJsonlFoldCache } from './jsonl-fold-cache.js';
|
|
6
7
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
8
|
+
function newModelSums() {
|
|
9
|
+
return { byModel: new Map(), counted: 0 };
|
|
10
|
+
}
|
|
11
|
+
function modelAcc(state, model) {
|
|
12
|
+
let acc = state.byModel.get(model);
|
|
13
|
+
if (!acc) {
|
|
14
|
+
acc = { model, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
|
|
15
|
+
state.byModel.set(model, acc);
|
|
16
|
+
}
|
|
17
|
+
return acc;
|
|
18
|
+
}
|
|
19
|
+
/** Builds the SessionUsage for per-model sums; copies, so the result never aliases fold state. */
|
|
20
|
+
function usageFromModels(byModel, harness) {
|
|
21
|
+
const perModel = [...byModel.values()].map((m) => ({ ...m }));
|
|
22
|
+
const sum = (pick) => perModel.reduce((acc, m) => acc + pick(m), 0);
|
|
23
|
+
const inputTokens = sum((m) => m.inputTokens);
|
|
24
|
+
const outputTokens = sum((m) => m.outputTokens);
|
|
25
|
+
const cacheReadTokens = sum((m) => m.cacheReadTokens);
|
|
26
|
+
const cacheWriteTokens = sum((m) => m.cacheWriteTokens);
|
|
27
|
+
return {
|
|
28
|
+
inputTokens,
|
|
29
|
+
outputTokens,
|
|
30
|
+
cacheReadTokens,
|
|
31
|
+
cacheWriteTokens,
|
|
32
|
+
totalTokens: inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens,
|
|
33
|
+
models: perModel.map((m) => m.model),
|
|
34
|
+
perModel,
|
|
35
|
+
harness,
|
|
36
|
+
collectedAt: new Date().toISOString(),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
7
39
|
/**
|
|
8
40
|
* Claude Code project-directory slug: every non-alphanumeric character of the
|
|
9
41
|
* working directory becomes '-'. Verified against real transcript dirs on this
|
|
@@ -93,48 +125,29 @@ async function collectPiUsage(workingDir, sessionCreatedAt, options, nativeSessi
|
|
|
93
125
|
const filePath = await containedPiSessionFile(baseDir, candidatePath);
|
|
94
126
|
if (!filePath)
|
|
95
127
|
return null;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
128
|
+
const usage = await (options.foldCache ?? defaultJsonlFoldCache).fold(filePath, piUsageFold, options.timeoutMs ?? DEFAULT_TIMEOUT_MS, (state) => (state.counted === 0 ? null : usageFromModels(state.byModel, 'pi')));
|
|
129
|
+
return usage ?? null;
|
|
130
|
+
}
|
|
131
|
+
const piUsageFold = {
|
|
132
|
+
kind: 'pi-usage-v1',
|
|
133
|
+
init: newModelSums,
|
|
134
|
+
step(state, raw) {
|
|
99
135
|
const entry = raw;
|
|
100
136
|
const message = entry?.type === 'message' ? entry.message : undefined;
|
|
101
137
|
const usage = message?.role === 'assistant' ? message.usage : undefined;
|
|
102
138
|
if (!message || !usage || typeof message.model !== 'string' || !message.model)
|
|
103
|
-
|
|
139
|
+
return;
|
|
104
140
|
const model = typeof message.provider === 'string' && message.provider
|
|
105
141
|
? `${message.provider}/${message.model}`
|
|
106
142
|
: message.model;
|
|
107
|
-
|
|
108
|
-
if (!acc) {
|
|
109
|
-
acc = { model, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
|
|
110
|
-
byModel.set(model, acc);
|
|
111
|
-
}
|
|
143
|
+
const acc = modelAcc(state, model);
|
|
112
144
|
acc.inputTokens += safeTokenCount(usage.input);
|
|
113
145
|
acc.outputTokens += safeTokenCount(usage.output);
|
|
114
146
|
acc.cacheReadTokens += safeTokenCount(usage.cacheRead);
|
|
115
147
|
acc.cacheWriteTokens += safeTokenCount(usage.cacheWrite);
|
|
116
|
-
counted += 1;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return null;
|
|
120
|
-
const perModel = [...byModel.values()];
|
|
121
|
-
const sum = (pick) => perModel.reduce((total, model) => total + pick(model), 0);
|
|
122
|
-
const inputTokens = sum((model) => model.inputTokens);
|
|
123
|
-
const outputTokens = sum((model) => model.outputTokens);
|
|
124
|
-
const cacheReadTokens = sum((model) => model.cacheReadTokens);
|
|
125
|
-
const cacheWriteTokens = sum((model) => model.cacheWriteTokens);
|
|
126
|
-
return {
|
|
127
|
-
inputTokens,
|
|
128
|
-
outputTokens,
|
|
129
|
-
cacheReadTokens,
|
|
130
|
-
cacheWriteTokens,
|
|
131
|
-
totalTokens: inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens,
|
|
132
|
-
models: perModel.map((model) => model.model),
|
|
133
|
-
perModel,
|
|
134
|
-
harness: 'pi',
|
|
135
|
-
collectedAt: new Date().toISOString(),
|
|
136
|
-
};
|
|
137
|
-
}
|
|
148
|
+
state.counted += 1;
|
|
149
|
+
},
|
|
150
|
+
};
|
|
138
151
|
function safeTokenCount(value) {
|
|
139
152
|
return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : 0;
|
|
140
153
|
}
|
|
@@ -183,64 +196,39 @@ async function collectClaudeUsage(claudeSessionId, workingDir, options) {
|
|
|
183
196
|
return null;
|
|
184
197
|
const baseDir = options.claudeProjectsDir ?? join(homedir(), '.claude', 'projects');
|
|
185
198
|
const filePath = join(baseDir, claudeProjectSlug(workingDir), `${claudeSessionId}.jsonl`);
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const seenMessageIds = new Set();
|
|
194
|
-
for await (const raw of jsonlLines(filePath, deadline)) {
|
|
199
|
+
const usage = await (options.foldCache ?? defaultJsonlFoldCache).fold(filePath, claudeUsageFold, options.timeoutMs ?? DEFAULT_TIMEOUT_MS, (state) => (state.counted === 0 ? null : usageFromModels(state.byModel, 'claude')));
|
|
200
|
+
return usage ?? null;
|
|
201
|
+
}
|
|
202
|
+
const claudeUsageFold = {
|
|
203
|
+
kind: 'claude-usage-v1',
|
|
204
|
+
init: () => ({ ...newModelSums(), seenMessageIds: new Set() }),
|
|
205
|
+
step(state, raw) {
|
|
195
206
|
const entry = raw;
|
|
196
207
|
if (entry?.type !== 'assistant')
|
|
197
|
-
|
|
208
|
+
return;
|
|
198
209
|
const message = entry.message;
|
|
199
210
|
const usage = message?.usage;
|
|
200
211
|
if (!message || !usage)
|
|
201
|
-
|
|
212
|
+
return;
|
|
202
213
|
const model = message.model ?? '';
|
|
203
214
|
// '<synthetic>' rows are harness-injected placeholders with zero usage.
|
|
204
215
|
if (!model || model === '<synthetic>')
|
|
205
|
-
|
|
216
|
+
return;
|
|
206
217
|
const id = message.id;
|
|
207
218
|
if (id) {
|
|
208
|
-
if (seenMessageIds.has(id))
|
|
209
|
-
|
|
210
|
-
seenMessageIds.add(id);
|
|
219
|
+
if (state.seenMessageIds.has(id))
|
|
220
|
+
return;
|
|
221
|
+
state.seenMessageIds.add(id);
|
|
211
222
|
}
|
|
212
223
|
// Models can vary mid-session — attribute tokens per message's model.
|
|
213
|
-
|
|
214
|
-
if (!acc) {
|
|
215
|
-
acc = { model, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
|
|
216
|
-
byModel.set(model, acc);
|
|
217
|
-
}
|
|
224
|
+
const acc = modelAcc(state, model);
|
|
218
225
|
acc.inputTokens += usage.input_tokens ?? 0;
|
|
219
226
|
acc.outputTokens += usage.output_tokens ?? 0;
|
|
220
227
|
acc.cacheReadTokens += usage.cache_read_input_tokens ?? 0;
|
|
221
228
|
acc.cacheWriteTokens += usage.cache_creation_input_tokens ?? 0;
|
|
222
|
-
counted += 1;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return null;
|
|
226
|
-
const perModel = [...byModel.values()];
|
|
227
|
-
const sum = (pick) => perModel.reduce((acc, m) => acc + pick(m), 0);
|
|
228
|
-
const inputTokens = sum((m) => m.inputTokens);
|
|
229
|
-
const outputTokens = sum((m) => m.outputTokens);
|
|
230
|
-
const cacheReadTokens = sum((m) => m.cacheReadTokens);
|
|
231
|
-
const cacheWriteTokens = sum((m) => m.cacheWriteTokens);
|
|
232
|
-
return {
|
|
233
|
-
inputTokens,
|
|
234
|
-
outputTokens,
|
|
235
|
-
cacheReadTokens,
|
|
236
|
-
cacheWriteTokens,
|
|
237
|
-
totalTokens: inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens,
|
|
238
|
-
models: perModel.map((m) => m.model),
|
|
239
|
-
perModel,
|
|
240
|
-
harness: 'claude',
|
|
241
|
-
collectedAt: new Date().toISOString(),
|
|
242
|
-
};
|
|
243
|
-
}
|
|
229
|
+
state.counted += 1;
|
|
230
|
+
},
|
|
231
|
+
};
|
|
244
232
|
/** Resolve ~/.codex/sessions/<yyyy>/<mm>/<dd>/rollout-*-<id>.jsonl (date-partitioned). */
|
|
245
233
|
async function findCodexRollout(baseDir, codexSessionId) {
|
|
246
234
|
const suffix = `-${codexSessionId}.jsonl`;
|
|
@@ -282,41 +270,16 @@ async function collectCodexUsage(codexSessionId, options) {
|
|
|
282
270
|
const filePath = await findCodexRollout(baseDir, codexSessionId);
|
|
283
271
|
if (!filePath)
|
|
284
272
|
return null;
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
let lastTotals = null;
|
|
288
|
-
for await (const raw of jsonlLines(filePath, deadline)) {
|
|
289
|
-
const entry = raw;
|
|
290
|
-
const payload = entry?.payload;
|
|
291
|
-
if (!payload)
|
|
292
|
-
continue;
|
|
293
|
-
if (entry.type === 'turn_context' && payload.model) {
|
|
294
|
-
if (!models.includes(payload.model))
|
|
295
|
-
models.push(payload.model);
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
// token_count events carry cumulative totals — the LAST one wins. Some
|
|
299
|
-
// token_count events have info: null (rate-limit-only updates); skip them.
|
|
300
|
-
if (payload.type === 'token_count') {
|
|
301
|
-
const totals = payload.info?.total_token_usage;
|
|
302
|
-
if (totals) {
|
|
303
|
-
lastTotals = {
|
|
304
|
-
input: totals.input_tokens ?? 0,
|
|
305
|
-
cached: totals.cached_input_tokens ?? 0,
|
|
306
|
-
output: totals.output_tokens ?? 0,
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
if (!lastTotals)
|
|
273
|
+
const totals = await (options.foldCache ?? defaultJsonlFoldCache).fold(filePath, codexUsageFold, options.timeoutMs ?? DEFAULT_TIMEOUT_MS, (state) => (state.lastTotals ? { models: [...state.models], ...state.lastTotals } : null));
|
|
274
|
+
if (!totals)
|
|
312
275
|
return null;
|
|
313
276
|
// Codex counts cached tokens INSIDE input_tokens (input 11276 / cached 9088 /
|
|
314
277
|
// total 11305 = 11276 + 29 in real rollouts) — split them so totalTokens
|
|
315
278
|
// (sum of the four) matches codex's own total_tokens. output_tokens already
|
|
316
279
|
// includes reasoning tokens. Codex reports no cache writes.
|
|
317
|
-
const cacheReadTokens = Math.min(
|
|
318
|
-
const inputTokens =
|
|
319
|
-
const outputTokens =
|
|
280
|
+
const cacheReadTokens = Math.min(totals.cached, totals.input);
|
|
281
|
+
const inputTokens = totals.input - cacheReadTokens;
|
|
282
|
+
const outputTokens = totals.output;
|
|
320
283
|
const cacheWriteTokens = 0;
|
|
321
284
|
// Codex rollouts carry only cumulative totals — there is no per-model
|
|
322
285
|
// attribution, so perModel is deliberately absent (models lists what ran).
|
|
@@ -326,11 +289,38 @@ async function collectCodexUsage(codexSessionId, options) {
|
|
|
326
289
|
cacheReadTokens,
|
|
327
290
|
cacheWriteTokens,
|
|
328
291
|
totalTokens: inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens,
|
|
329
|
-
models,
|
|
292
|
+
models: totals.models,
|
|
330
293
|
harness: 'codex',
|
|
331
294
|
collectedAt: new Date().toISOString(),
|
|
332
295
|
};
|
|
333
296
|
}
|
|
297
|
+
const codexUsageFold = {
|
|
298
|
+
kind: 'codex-usage-v1',
|
|
299
|
+
init: () => ({ models: [], lastTotals: null }),
|
|
300
|
+
step(state, raw) {
|
|
301
|
+
const entry = raw;
|
|
302
|
+
const payload = entry?.payload;
|
|
303
|
+
if (!payload)
|
|
304
|
+
return;
|
|
305
|
+
if (entry.type === 'turn_context' && payload.model) {
|
|
306
|
+
if (!state.models.includes(payload.model))
|
|
307
|
+
state.models.push(payload.model);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
// token_count events carry cumulative totals — the LAST one wins. Some
|
|
311
|
+
// token_count events have info: null (rate-limit-only updates); skip them.
|
|
312
|
+
if (payload.type === 'token_count') {
|
|
313
|
+
const totals = payload.info?.total_token_usage;
|
|
314
|
+
if (totals) {
|
|
315
|
+
state.lastTotals = {
|
|
316
|
+
input: totals.input_tokens ?? 0,
|
|
317
|
+
cached: totals.cached_input_tokens ?? 0,
|
|
318
|
+
output: totals.output_tokens ?? 0,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
};
|
|
334
324
|
/**
|
|
335
325
|
* Resolve the kimi-code session dir that this ftown session spawned.
|
|
336
326
|
*
|
|
@@ -393,54 +383,57 @@ async function collectKimiCodeUsage(workingDir, sessionCreatedAt, options) {
|
|
|
393
383
|
catch {
|
|
394
384
|
return null;
|
|
395
385
|
}
|
|
386
|
+
const cache = options.foldCache ?? defaultJsonlFoldCache;
|
|
396
387
|
let counted = 0;
|
|
397
388
|
// Keyed by model id; Map preserves first-appearance order across agents.
|
|
398
389
|
const byModel = new Map();
|
|
399
390
|
for (const agent of agentNames) {
|
|
400
391
|
const wirePath = join(agentsDir, agent, 'wire.jsonl');
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
continue;
|
|
411
|
-
let acc = byModel.get(model);
|
|
392
|
+
const sums = await cache.fold(wirePath, kimiUsageFold, options.timeoutMs ?? DEFAULT_TIMEOUT_MS, (state) => ({
|
|
393
|
+
byModel: new Map([...state.byModel].map(([k, m]) => [k, { ...m }])),
|
|
394
|
+
counted: state.counted,
|
|
395
|
+
}));
|
|
396
|
+
if (!sums)
|
|
397
|
+
continue;
|
|
398
|
+
counted += sums.counted;
|
|
399
|
+
for (const m of sums.byModel.values()) {
|
|
400
|
+
const acc = byModel.get(m.model);
|
|
412
401
|
if (!acc) {
|
|
413
|
-
|
|
414
|
-
|
|
402
|
+
byModel.set(m.model, m);
|
|
403
|
+
continue;
|
|
415
404
|
}
|
|
416
|
-
|
|
417
|
-
acc.
|
|
418
|
-
acc.
|
|
419
|
-
acc.
|
|
420
|
-
acc.cacheWriteTokens += usage.inputCacheCreation ?? 0;
|
|
421
|
-
counted += 1;
|
|
405
|
+
acc.inputTokens += m.inputTokens;
|
|
406
|
+
acc.outputTokens += m.outputTokens;
|
|
407
|
+
acc.cacheReadTokens += m.cacheReadTokens;
|
|
408
|
+
acc.cacheWriteTokens += m.cacheWriteTokens;
|
|
422
409
|
}
|
|
423
410
|
}
|
|
424
411
|
if (counted === 0)
|
|
425
412
|
return null;
|
|
426
|
-
|
|
427
|
-
const sum = (pick) => perModel.reduce((acc, m) => acc + pick(m), 0);
|
|
428
|
-
const inputTokens = sum((m) => m.inputTokens);
|
|
429
|
-
const outputTokens = sum((m) => m.outputTokens);
|
|
430
|
-
const cacheReadTokens = sum((m) => m.cacheReadTokens);
|
|
431
|
-
const cacheWriteTokens = sum((m) => m.cacheWriteTokens);
|
|
432
|
-
return {
|
|
433
|
-
inputTokens,
|
|
434
|
-
outputTokens,
|
|
435
|
-
cacheReadTokens,
|
|
436
|
-
cacheWriteTokens,
|
|
437
|
-
totalTokens: inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens,
|
|
438
|
-
models: perModel.map((m) => m.model),
|
|
439
|
-
perModel,
|
|
440
|
-
harness: 'kimi-code',
|
|
441
|
-
collectedAt: new Date().toISOString(),
|
|
442
|
-
};
|
|
413
|
+
return usageFromModels(byModel, 'kimi-code');
|
|
443
414
|
}
|
|
415
|
+
const kimiUsageFold = {
|
|
416
|
+
kind: 'kimi-usage-v1',
|
|
417
|
+
init: newModelSums,
|
|
418
|
+
step(state, raw) {
|
|
419
|
+
const entry = raw;
|
|
420
|
+
if (entry?.type !== 'usage.record')
|
|
421
|
+
return;
|
|
422
|
+
const usage = entry.usage;
|
|
423
|
+
if (!usage)
|
|
424
|
+
return;
|
|
425
|
+
const model = entry.model ?? '';
|
|
426
|
+
if (!model)
|
|
427
|
+
return;
|
|
428
|
+
const acc = modelAcc(state, model);
|
|
429
|
+
// Per-turn incremental records — SUM across all of them.
|
|
430
|
+
acc.inputTokens += usage.inputOther ?? 0;
|
|
431
|
+
acc.outputTokens += usage.output ?? 0;
|
|
432
|
+
acc.cacheReadTokens += usage.inputCacheRead ?? 0;
|
|
433
|
+
acc.cacheWriteTokens += usage.inputCacheCreation ?? 0;
|
|
434
|
+
state.counted += 1;
|
|
435
|
+
},
|
|
436
|
+
};
|
|
444
437
|
/**
|
|
445
438
|
* Resolve the muse session.jsonl that this ftown session spawned.
|
|
446
439
|
*
|
|
@@ -578,8 +571,7 @@ async function collectMuseUsageById(museSessionId, options) {
|
|
|
578
571
|
const filePath = await findMuseSessionFileById(baseDir, museSessionId);
|
|
579
572
|
if (!filePath)
|
|
580
573
|
return null;
|
|
581
|
-
|
|
582
|
-
return sumMuseSessionFile(filePath, deadline);
|
|
574
|
+
return sumMuseSessionFile(filePath, options);
|
|
583
575
|
}
|
|
584
576
|
async function collectMuseUsage(workingDir, sessionCreatedAt, options) {
|
|
585
577
|
const baseDir = options.museSessionsDir
|
|
@@ -588,28 +580,27 @@ async function collectMuseUsage(workingDir, sessionCreatedAt, options) {
|
|
|
588
580
|
const filePath = await resolveMuseSessionFile(baseDir, workingDir, sessionCreatedAt, deadline);
|
|
589
581
|
if (!filePath)
|
|
590
582
|
return null;
|
|
591
|
-
return sumMuseSessionFile(filePath,
|
|
583
|
+
return sumMuseSessionFile(filePath, options);
|
|
592
584
|
}
|
|
593
|
-
async function sumMuseSessionFile(filePath,
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
585
|
+
async function sumMuseSessionFile(filePath, options) {
|
|
586
|
+
const usage = await (options.foldCache ?? defaultJsonlFoldCache).fold(filePath, museUsageFold, options.timeoutMs ?? DEFAULT_TIMEOUT_MS, (state) => (state.counted === 0 ? null : usageFromModels(state.byModel, 'muse')));
|
|
587
|
+
return usage ?? null;
|
|
588
|
+
}
|
|
589
|
+
const museUsageFold = {
|
|
590
|
+
kind: 'muse-usage-v1',
|
|
591
|
+
init: newModelSums,
|
|
592
|
+
step(state, raw) {
|
|
598
593
|
const entry = raw;
|
|
599
594
|
if (entry?.payload_type !== 'runtime.session')
|
|
600
|
-
|
|
595
|
+
return;
|
|
601
596
|
const event = entry.payload?.kind === 'run' ? entry.payload.event : undefined;
|
|
602
597
|
if (event?.kind !== 'model_completed')
|
|
603
|
-
|
|
598
|
+
return;
|
|
604
599
|
const usage = event.usage;
|
|
605
600
|
const model = event.model ?? '';
|
|
606
601
|
if (!usage || !model)
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
if (!acc) {
|
|
610
|
-
acc = { model, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
|
|
611
|
-
byModel.set(model, acc);
|
|
612
|
-
}
|
|
602
|
+
return;
|
|
603
|
+
const acc = modelAcc(state, model);
|
|
613
604
|
// Per-call records — SUM across all of them. Verified against real logs:
|
|
614
605
|
// input_tokens INCLUDES cached tokens (cached(n) ≈ input(n-1)), so split
|
|
615
606
|
// them codex-style; cached_tokens duplicates cache_read_tokens (ignored);
|
|
@@ -620,26 +611,7 @@ async function sumMuseSessionFile(filePath, deadline) {
|
|
|
620
611
|
acc.outputTokens += safeTokenCount(usage.output_tokens);
|
|
621
612
|
acc.cacheReadTokens += cacheRead;
|
|
622
613
|
acc.cacheWriteTokens += safeTokenCount(usage.cache_write_tokens);
|
|
623
|
-
counted += 1;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
return null;
|
|
627
|
-
const perModel = [...byModel.values()];
|
|
628
|
-
const sum = (pick) => perModel.reduce((acc, m) => acc + pick(m), 0);
|
|
629
|
-
const inputTokens = sum((m) => m.inputTokens);
|
|
630
|
-
const outputTokens = sum((m) => m.outputTokens);
|
|
631
|
-
const cacheReadTokens = sum((m) => m.cacheReadTokens);
|
|
632
|
-
const cacheWriteTokens = sum((m) => m.cacheWriteTokens);
|
|
633
|
-
return {
|
|
634
|
-
inputTokens,
|
|
635
|
-
outputTokens,
|
|
636
|
-
cacheReadTokens,
|
|
637
|
-
cacheWriteTokens,
|
|
638
|
-
totalTokens: inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens,
|
|
639
|
-
models: perModel.map((m) => m.model),
|
|
640
|
-
perModel,
|
|
641
|
-
harness: 'muse',
|
|
642
|
-
collectedAt: new Date().toISOString(),
|
|
643
|
-
};
|
|
644
|
-
}
|
|
614
|
+
state.counted += 1;
|
|
615
|
+
},
|
|
616
|
+
};
|
|
645
617
|
//# sourceMappingURL=usage-collector.js.map
|