@assuredloop/orca-bot-kit 0.12.0 → 0.13.0
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/package.json +1 -1
- package/src/bot.js +12 -6
- package/src/cli.js +10 -4
- package/src/conversations.js +8 -1
- package/src/orca.js +6 -1
- package/src/permissions.js +66 -25
- package/src/usage.js +80 -6
package/package.json
CHANGED
package/src/bot.js
CHANGED
|
@@ -277,14 +277,15 @@ export function allowedNow(bots, bot, rules, flag = '--allow') {
|
|
|
277
277
|
* added }: the whole list now, and what this call put in it.
|
|
278
278
|
*
|
|
279
279
|
* Only the list: the rules are written into the harness's settings by the
|
|
280
|
-
* caller, as the rules build is run by the caller of `changeBot`.
|
|
280
|
+
* caller, as the rules build is run by the caller of `changeBot`. With `write`
|
|
281
|
+
* false it only refuses what the edit would refuse, and writes nothing.
|
|
281
282
|
*/
|
|
282
|
-
export function allowRules(bots, bot, rules) {
|
|
283
|
+
export function allowRules(bots, bot, rules, { write = true } = {}) {
|
|
283
284
|
const { home, was } = allowedNow(bots, bot, rules);
|
|
284
285
|
const added = [...new Set(rules)].filter((rule) => !was.includes(rule));
|
|
285
286
|
const allow = [...was, ...added];
|
|
286
287
|
if (added.length > 0) {
|
|
287
|
-
editBot(bots, bot, `allow ${added.join(', ')} for ${bot}`, (doc) => doc.set('allow', allow), (before) => ({ ...before, allow }));
|
|
288
|
+
editBot(bots, bot, `allow ${added.join(', ')} for ${bot}`, (doc) => doc.set('allow', allow), (before) => ({ ...before, allow }), write);
|
|
288
289
|
}
|
|
289
290
|
return { bot, home, allow, added };
|
|
290
291
|
}
|
|
@@ -410,10 +411,15 @@ function editBot(bots, bot, what, edit, expected, write = true) {
|
|
|
410
411
|
const source = readFileSync(file, 'utf8');
|
|
411
412
|
const doc = parseDocument(source);
|
|
412
413
|
edit(doc);
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
414
|
+
const refusal = new Error(`${file} cannot be changed to ${what} without changing something else in it, so nothing was written. Make the change by hand.`);
|
|
415
|
+
let text;
|
|
416
|
+
try {
|
|
417
|
+
// An edit that leaves an alias with no anchor cannot be written at all.
|
|
418
|
+
text = doc.toString(YAML_OUT);
|
|
419
|
+
} catch {
|
|
420
|
+
throw refusal;
|
|
416
421
|
}
|
|
422
|
+
if (!changesExactly(source, text, expected)) throw refusal;
|
|
417
423
|
if (write && text !== source) writeFileSync(file, text);
|
|
418
424
|
}
|
|
419
425
|
|
package/src/cli.js
CHANGED
|
@@ -19,7 +19,7 @@ import { initBots } from './init.js';
|
|
|
19
19
|
import { APPROVALS, HARNESSES, ownCli, shellWord, workDirOf } from './launch.js';
|
|
20
20
|
import { checkMail, lookUp, noMailboxYet, sendMessage } from './message.js';
|
|
21
21
|
import { orcaCli, orcaTrouble, RELOAD_LINE } from './orca.js';
|
|
22
|
-
import { allowCommand, beyondDefaults, refuseBroad, refuseNoCodexForm, runsOnClaude, runsOnCodex, takeBack, writePermissions } from './permissions.js';
|
|
22
|
+
import { allowCommand, allowIn, beyondDefaults, refuseBroad, refuseNoCodexForm, runsOnClaude, runsOnCodex, takeBack, writePermissions } from './permissions.js';
|
|
23
23
|
import { pauseSessions, unpauseSessions } from './pause.js';
|
|
24
24
|
import { recordSession, SHELL_ENV, TAB_ENV } from './record.js';
|
|
25
25
|
import { restartSessions } from './restart.js';
|
|
@@ -618,10 +618,14 @@ const commands = {
|
|
|
618
618
|
}
|
|
619
619
|
// Refused before anything is written, so a bad rule, or a bad list already
|
|
620
620
|
// there, leaves the charter as it was too.
|
|
621
|
+
let allowInto;
|
|
621
622
|
if (values.allow !== undefined) {
|
|
622
623
|
const { home } = allowedNow(bots, values.bot, values.allow);
|
|
623
624
|
refuseBroad(home, values.allow);
|
|
624
|
-
|
|
625
|
+
const bot = readBot(home, values.bot);
|
|
626
|
+
refuseNoCodexForm(bot, values.allow);
|
|
627
|
+
const { allow } = allowRules(bots, values.bot, values.allow, { write: false });
|
|
628
|
+
allowInto = allowIn(bots, home, { ...bot, allow });
|
|
625
629
|
}
|
|
626
630
|
let takeBackFrom;
|
|
627
631
|
if (values.disallow !== undefined) {
|
|
@@ -662,9 +666,11 @@ const commands = {
|
|
|
662
666
|
}
|
|
663
667
|
}
|
|
664
668
|
if (values.allow !== undefined) {
|
|
669
|
+
// Written at once, as the charter's rules are built at once (#344): the
|
|
670
|
+
// harness's files first, bot.yaml last, so a write that fails leaves
|
|
671
|
+
// `allow` as it was, and the same command can run again (#383).
|
|
672
|
+
const permissions = allowInto();
|
|
665
673
|
const allowed = allowRules(bots, values.bot, values.allow);
|
|
666
|
-
// Written at once, as the charter's rules are built at once (#344).
|
|
667
|
-
const permissions = writePermissions(bots, allowed.home, readBot(allowed.home));
|
|
668
674
|
Object.assign(answer, { allow: allowed.allow, permissions });
|
|
669
675
|
const codex = permissions.find((entry) => entry.unwritten !== undefined);
|
|
670
676
|
lines.push(
|
package/src/conversations.js
CHANGED
|
@@ -50,9 +50,16 @@ export function conversationsIn(harness, home, since) {
|
|
|
50
50
|
*/
|
|
51
51
|
export function hasConversation(harness, home, id) {
|
|
52
52
|
if (harness === 'claude') return existsSync(claudeTranscript(home, id));
|
|
53
|
-
return
|
|
53
|
+
return codexRollout(id) !== undefined;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Where Codex keeps the conversation `id`, whatever folder it ran in, found by
|
|
58
|
+
* the file name Codex makes from the id; `undefined` when it has none.
|
|
59
|
+
*/
|
|
60
|
+
export const codexRollout = (id) =>
|
|
61
|
+
rollouts(codexDir()).find((file) => path.basename(file).endsWith(`-${id}.jsonl`));
|
|
62
|
+
|
|
56
63
|
/**
|
|
57
64
|
* Where Claude Code keeps the conversation `id` of this bot home, whether or
|
|
58
65
|
* not it has written it yet.
|
package/src/orca.js
CHANGED
|
@@ -180,7 +180,12 @@ export const deleteProject = (setupId) => orca(['project', 'setup-delete', '--se
|
|
|
180
180
|
/** Said after a run that made or renamed a project, whatever `tellWindow` answered, and after a removal `reloadWindow` could not follow. */
|
|
181
181
|
export const RELOAD_LINE = "If Orca's sidebar does not show it, reload the window with Cmd+Shift+R.";
|
|
182
182
|
|
|
183
|
-
/**
|
|
183
|
+
/**
|
|
184
|
+
* How long Orca's client waits for its runtime, and how long the kit waits for
|
|
185
|
+
* the client. Measured beside the full unit suite on Orca 1.4.214, the whole
|
|
186
|
+
* call took at most 721 ms and the call inside the client at most 447 ms
|
|
187
|
+
* (tech notes, #384).
|
|
188
|
+
*/
|
|
184
189
|
const CLIENT_WAIT_MS = 2000;
|
|
185
190
|
const CLIENT_KILL_MS = 3000;
|
|
186
191
|
|
package/src/permissions.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// `Bash(<words>:*)` becomes a `prefix_rule` of those words in
|
|
20
20
|
// `.codex/rules/obk.rules`, a file the kit owns whole and rewrites from `allow`.
|
|
21
21
|
|
|
22
|
-
import { accessSync, constants, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
22
|
+
import { accessSync, constants, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
23
23
|
import path from 'node:path';
|
|
24
24
|
|
|
25
25
|
import { leadsOutside } from './bot.js';
|
|
@@ -267,10 +267,10 @@ export const allowCommand = (bots, bot, rules) => [
|
|
|
267
267
|
* the kit may not write is an entry with its `trouble` rather than a throw.
|
|
268
268
|
*/
|
|
269
269
|
export function writePermissions(bots, home, bot, { keepGoing = false } = {}) {
|
|
270
|
-
const writers = [[runsOnClaude, FILE,
|
|
271
|
-
return writers.filter(([runs]) => runs(bot)).map(([, file,
|
|
270
|
+
const writers = [[runsOnClaude, FILE, planClaude], [runsOnCodex, CODEX_FILE, planCodex]];
|
|
271
|
+
return writers.filter(([runs]) => runs(bot)).map(([, file, plan]) => {
|
|
272
272
|
try {
|
|
273
|
-
return
|
|
273
|
+
return plan(bots, home, bot).write();
|
|
274
274
|
} catch (error) {
|
|
275
275
|
if (!keepGoing) throw error;
|
|
276
276
|
return { bot: bot.name, file: path.join(home, file), written: [], waiting: [], trouble: error.message };
|
|
@@ -279,30 +279,55 @@ export function writePermissions(bots, home, bot, { keepGoing = false } = {}) {
|
|
|
279
279
|
}
|
|
280
280
|
|
|
281
281
|
/**
|
|
282
|
-
*
|
|
283
|
-
*
|
|
282
|
+
* Check every file `bot change --allow` writes before any is written (#383):
|
|
283
|
+
* bot.yaml, and the files of the harnesses the bot runs on, read and found
|
|
284
|
+
* writable. `bot` is the bot as it will be, with the new rules in `allow`.
|
|
285
|
+
* What comes back writes the harness files, one entry per file, Claude's
|
|
286
|
+
* first; the caller writes bot.yaml after, so a write that fails leaves
|
|
287
|
+
* `allow` as it was, for the same command to run again.
|
|
288
|
+
*/
|
|
289
|
+
export function allowIn(bots, home, bot) {
|
|
290
|
+
refuseUnwritable(path.join(home, 'bot.yaml'));
|
|
291
|
+
const plans = [[runsOnClaude, planClaude], [runsOnCodex, planCodex]]
|
|
292
|
+
.filter(([runs]) => runs(bot)).map(([, plan]) => plan(bots, home, bot));
|
|
293
|
+
for (const plan of plans) if (plan.writes) refuseUnwritable(plan.answer.file);
|
|
294
|
+
return () => plans.map((plan) => plan.write());
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* What writing the bot's allowed rules into its Claude settings would do,
|
|
299
|
+
* read and refused before anything is written: `{ answer, writes, write }`.
|
|
300
|
+
* `write()` gives `{ bot, file, written, waiting }`, with `written` the rules
|
|
301
|
+
* it added.
|
|
284
302
|
*
|
|
285
303
|
* Only rules `allow` holds are written. Everything else in the file stays as it
|
|
286
304
|
* is, the user's own entries and their order included; a rule missing from the
|
|
287
305
|
* file goes after them, and a run with nothing to add writes nothing.
|
|
288
306
|
*/
|
|
289
|
-
function
|
|
307
|
+
function planClaude(bots, home, bot) {
|
|
290
308
|
const file = path.join(home, FILE);
|
|
291
309
|
const allowed = allowOf(home, bot);
|
|
292
310
|
const waiting = waitingFor(bots, home, bot, 'claude');
|
|
293
311
|
const answer = { bot: bot.name, file, written: [], waiting };
|
|
294
|
-
|
|
312
|
+
const none = { answer, writes: false, write: () => answer };
|
|
313
|
+
if (allowed.length === 0) return none;
|
|
295
314
|
|
|
296
315
|
refuseOutside(home, file);
|
|
297
316
|
const settings = readSettings(file, 'the permission rules the user allowed');
|
|
298
317
|
const present = presentIn(settings, file);
|
|
299
318
|
const missing = [...new Set(allowed)].filter((rule) => !present.includes(rule));
|
|
300
|
-
if (missing.length === 0) return
|
|
319
|
+
if (missing.length === 0) return none;
|
|
301
320
|
|
|
302
321
|
const wanted = { ...settings, permissions: { ...settings.permissions, allow: [...present, ...missing] } };
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
322
|
+
return {
|
|
323
|
+
answer,
|
|
324
|
+
writes: true,
|
|
325
|
+
write: () => {
|
|
326
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
327
|
+
writeFileSync(file, `${JSON.stringify(wanted, null, 2)}\n`);
|
|
328
|
+
return { ...answer, written: missing };
|
|
329
|
+
},
|
|
330
|
+
};
|
|
306
331
|
}
|
|
307
332
|
|
|
308
333
|
/**
|
|
@@ -358,7 +383,7 @@ export function takeBack(bots, home, bot, rules) {
|
|
|
358
383
|
entries.push({ bot: bot.name, file, written: [], removed, waiting: waitingFor(bots, home, after, 'claude') });
|
|
359
384
|
}
|
|
360
385
|
if (runsOnCodex(after)) {
|
|
361
|
-
const entry =
|
|
386
|
+
const entry = planCodex(bots, home, after).write();
|
|
362
387
|
// A line goes only when no rule still allowed gives it.
|
|
363
388
|
const kept = [...codexOf(home, after).lines.keys()];
|
|
364
389
|
const removed = taken.filter((rule) => {
|
|
@@ -371,12 +396,20 @@ export function takeBack(bots, home, bot, rules) {
|
|
|
371
396
|
};
|
|
372
397
|
}
|
|
373
398
|
|
|
374
|
-
/**
|
|
399
|
+
/**
|
|
400
|
+
* A refusal, before anything is written, of a file the kit could not write: one
|
|
401
|
+
* not there yet is made in the nearest folder that is, which the kit must be
|
|
402
|
+
* able to enter and write in, and which must be a folder.
|
|
403
|
+
*/
|
|
375
404
|
function refuseUnwritable(file) {
|
|
405
|
+
let where = file;
|
|
406
|
+
while (!existsSync(where) && path.dirname(where) !== where) where = path.dirname(where);
|
|
376
407
|
try {
|
|
377
|
-
|
|
408
|
+
if (where !== file && !statSync(where).isDirectory()) throw Object.assign(new Error('not a folder'), { code: 'ENOTDIR' });
|
|
409
|
+
accessSync(where, where === file ? constants.W_OK : constants.W_OK | constants.X_OK);
|
|
378
410
|
} catch (error) {
|
|
379
|
-
|
|
411
|
+
const at = where === file ? '' : ` at ${where}`;
|
|
412
|
+
throw new Error(`${file} cannot be written (${error.code ?? error.message}${at}), so nothing was changed. Let the kit write it, then run the command again.`);
|
|
380
413
|
}
|
|
381
414
|
}
|
|
382
415
|
|
|
@@ -417,16 +450,18 @@ function codexOf(home, bot) {
|
|
|
417
450
|
const ruleLines = (text) => text.split('\n').map((line) => line.trim()).filter((line) => line !== '' && !line.startsWith('#'));
|
|
418
451
|
|
|
419
452
|
/**
|
|
420
|
-
*
|
|
453
|
+
* What writing the Codex form of what the bot is allowed into
|
|
454
|
+
* `.codex/rules/obk.rules` would do, as `planClaude` gives it. `write()` gives
|
|
421
455
|
* `{ bot, file, written, waiting, unwritten }`, with `written` every rule in
|
|
422
|
-
* the file when
|
|
423
|
-
*
|
|
456
|
+
* the file when it wrote it. The kit owns the file whole and writes it only
|
|
457
|
+
* when it would change; with nothing to write and no file, it makes none.
|
|
424
458
|
*/
|
|
425
|
-
function
|
|
459
|
+
function planCodex(bots, home, bot) {
|
|
426
460
|
const file = path.join(home, CODEX_FILE);
|
|
427
461
|
const { lines, unwritten } = codexOf(home, bot);
|
|
428
462
|
const answer = { bot: bot.name, file, written: [], waiting: waitingFor(bots, home, bot, 'codex'), unwritten };
|
|
429
|
-
|
|
463
|
+
const none = { answer, writes: false, write: () => answer };
|
|
464
|
+
if (lines.size === 0 && !existsSync(file)) return none;
|
|
430
465
|
|
|
431
466
|
refuseOutside(home, file);
|
|
432
467
|
const text = [
|
|
@@ -434,10 +469,16 @@ function writeCodex(bots, home, bot) {
|
|
|
434
469
|
'# obk rewrites this whole file: put rules of your own in another file in this folder.',
|
|
435
470
|
...lines.keys(),
|
|
436
471
|
].join('\n');
|
|
437
|
-
if (existsSync(file) && readFileSync(file, 'utf8') === `${text}\n`) return
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
472
|
+
if (existsSync(file) && readFileSync(file, 'utf8') === `${text}\n`) return none;
|
|
473
|
+
return {
|
|
474
|
+
answer,
|
|
475
|
+
writes: true,
|
|
476
|
+
write: () => {
|
|
477
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
478
|
+
writeFileSync(file, `${text}\n`);
|
|
479
|
+
return { ...answer, written: [...lines.values()].flat() };
|
|
480
|
+
},
|
|
481
|
+
};
|
|
441
482
|
}
|
|
442
483
|
|
|
443
484
|
/**
|
package/src/usage.js
CHANGED
|
@@ -25,6 +25,9 @@
|
|
|
25
25
|
// `last_token_usage` counts a call written down twice twice. So a call is
|
|
26
26
|
// the difference in the running total from the event before: none is a
|
|
27
27
|
// repeat, and a fall is a new window, whose own `last_token_usage` counts.
|
|
28
|
+
// A rollout's first record counts its own figure too: a child's running
|
|
29
|
+
// total can start with its parent's inside it, and a fork can begin with
|
|
30
|
+
// its origin's records copied in, which are the origin's calls (#376).
|
|
28
31
|
// 2. The two harnesses do not mean the same thing by `input_tokens`. Claude
|
|
29
32
|
// Code leaves the cache reads out of it; Codex counts them inside it. So
|
|
30
33
|
// the Codex figure has its cached tokens taken back out, and `input` means
|
|
@@ -40,7 +43,7 @@ import { readFileSync, realpathSync } from 'node:fs';
|
|
|
40
43
|
|
|
41
44
|
import { botDir, botNames, readBot } from './bot.js';
|
|
42
45
|
import { readBook, sessionIdsIn } from './book.js';
|
|
43
|
-
import { claudeSubagentTranscripts, transcriptsIn } from './conversations.js';
|
|
46
|
+
import { claudeSubagentTranscripts, codexRollout, transcriptsIn } from './conversations.js';
|
|
44
47
|
import { HARNESSES, harnessOf } from './launch.js';
|
|
45
48
|
|
|
46
49
|
/** The kinds a call's tokens are reported in, the same on either harness. */
|
|
@@ -193,7 +196,10 @@ function counted(one, window) {
|
|
|
193
196
|
return { entries, subagent: index > 0 };
|
|
194
197
|
});
|
|
195
198
|
if (one.harness === 'claude') fromClaude(sources, window, tally);
|
|
196
|
-
else
|
|
199
|
+
else {
|
|
200
|
+
const copied = originOf(sources[0].entries, tally);
|
|
201
|
+
if (copied !== null) fromCodex(sources[0].entries, window, tally, copied);
|
|
202
|
+
}
|
|
197
203
|
if (tally.calls === 0 && KINDS.every((kind) => tally.tokens[kind] === 0)) return { gaps: tally.gaps };
|
|
198
204
|
|
|
199
205
|
return { gaps: tally.gaps, row: {
|
|
@@ -351,13 +357,22 @@ const CODEX_FIELDS = [
|
|
|
351
357
|
* counted either, and is said to be; the running total follows on from it. A
|
|
352
358
|
* call whose figure is needed and has something missing is not counted, and is
|
|
353
359
|
* said to be; nothing missing is taken as zero.
|
|
360
|
+
*
|
|
361
|
+
* A fork begins with the records of the conversation it was forked from, copied
|
|
362
|
+
* in and stamped at the fork's start, or all at one time in a file Codex rebuilt.
|
|
363
|
+
* Those are the origin's calls, counted with the origin: the leading records
|
|
364
|
+
* that are in `copied`, both figures, are followed and not counted (#376).
|
|
365
|
+
* A leading record with a figure missing may be a copy or the fork's own call;
|
|
366
|
+
* it is not counted, and is said to be.
|
|
354
367
|
*/
|
|
355
|
-
function fromCodex(entries, window, tally) {
|
|
368
|
+
function fromCodex(entries, window, tally, copied) {
|
|
356
369
|
let model;
|
|
357
370
|
let effort;
|
|
358
371
|
let running;
|
|
359
372
|
// Whether a running total with something missing came since the last complete one.
|
|
360
373
|
let broken = false;
|
|
374
|
+
// Whether the records so far are all the origin's, copied into a fork.
|
|
375
|
+
let copying = copied !== undefined;
|
|
361
376
|
|
|
362
377
|
for (const entry of entries) {
|
|
363
378
|
const when = Date.parse(entry.timestamp ?? '');
|
|
@@ -381,10 +396,26 @@ function fromCodex(entries, window, tally) {
|
|
|
381
396
|
if (info === undefined || info === null) continue;
|
|
382
397
|
|
|
383
398
|
const total = info.total_token_usage;
|
|
399
|
+
const whole = complete(total, CODEX_READ) && complete(info.last_token_usage, CODEX_READ);
|
|
400
|
+
if (copying && whole && copied.has(key(info))) {
|
|
401
|
+
running = total;
|
|
402
|
+
broken = false;
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
// A record with a figure missing cannot be told from a copy while copies
|
|
406
|
+
// may still be coming: it is not counted and is said to be, and the next
|
|
407
|
+
// record is matched as before.
|
|
408
|
+
const unknown = copying && !whole;
|
|
409
|
+
if (!unknown) copying = false;
|
|
410
|
+
|
|
384
411
|
let used;
|
|
385
412
|
// No running total at all is a running total with every figure missing:
|
|
386
413
|
// the next one's rise would take this call in again (#302).
|
|
387
|
-
if (
|
|
414
|
+
if (unknown && complete(total, CODEX_READ)) {
|
|
415
|
+
used = null;
|
|
416
|
+
running = total;
|
|
417
|
+
broken = false;
|
|
418
|
+
} else if (!complete(total, CODEX_READ)) {
|
|
388
419
|
used = null;
|
|
389
420
|
broken = true;
|
|
390
421
|
} else {
|
|
@@ -412,13 +443,56 @@ function fromCodex(entries, window, tally) {
|
|
|
412
443
|
/** The fields Codex itself writes; it has no cache writes to report. */
|
|
413
444
|
const CODEX_READ = CODEX_FIELDS.filter((field) => field !== 'cache_write_input_tokens').concat('total_tokens');
|
|
414
445
|
|
|
446
|
+
/**
|
|
447
|
+
* A usage record as one value, both its figures: a call of the child's own can
|
|
448
|
+
* bring the running total back to one the origin once had, but not with the same
|
|
449
|
+
* figure of its own.
|
|
450
|
+
*/
|
|
451
|
+
const key = (info) => [info.total_token_usage, info.last_token_usage]
|
|
452
|
+
.map((figure) => CODEX_READ.map((field) => figure[field]).join('/')).join('|');
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* The usage records of the conversation a Codex rollout was forked from, which
|
|
456
|
+
* it may begin with copies of (tech notes, section 3). `undefined` when it names
|
|
457
|
+
* no origin. When the origin cannot be read whole, a line of it broken or a
|
|
458
|
+
* figure missing from a record, where the copies end cannot be told, so none of
|
|
459
|
+
* the fork is counted: `null`, and said to be.
|
|
460
|
+
*/
|
|
461
|
+
function originOf(entries, tally) {
|
|
462
|
+
const id = entries.find((entry) => entry.type === 'session_meta')?.payload?.forked_from_id;
|
|
463
|
+
if (typeof id !== 'string' || id === '') return undefined;
|
|
464
|
+
|
|
465
|
+
const file = codexRollout(id);
|
|
466
|
+
const origin = file === undefined ? { unreadable: 1 } : transcript(file);
|
|
467
|
+
// No `info` is a note about rate limits, not a record of usage.
|
|
468
|
+
const records = origin.unreadable > 0 ? [] : origin.entries
|
|
469
|
+
.filter((entry) => entry.type === 'event_msg' && entry.payload?.type === 'token_count')
|
|
470
|
+
.map((entry) => entry.payload.info)
|
|
471
|
+
.filter((info) => info !== undefined && info !== null);
|
|
472
|
+
const whole = origin.unreadable === 0 && origin.broken === 0 && records.every((info) =>
|
|
473
|
+
complete(info.total_token_usage, CODEX_READ) && complete(info.last_token_usage, CODEX_READ));
|
|
474
|
+
if (!whole) {
|
|
475
|
+
tally.gaps.unreadable_transcripts += 1;
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
return new Set(records.map(key));
|
|
479
|
+
}
|
|
480
|
+
|
|
415
481
|
/**
|
|
416
482
|
* What one Codex event says its call used, measured against the running total
|
|
417
483
|
* before it: `undefined` where the event is the one before written down again,
|
|
418
|
-
* and `null` where what it used cannot be known.
|
|
484
|
+
* and `null` where what it used cannot be known. The first has nothing before
|
|
485
|
+
* it, and its running total can hold what a parent spent before this rollout
|
|
486
|
+
* began, so its own figure is what it used. An own figure of nothing in every
|
|
487
|
+
* field is a note of where the running total stands, not a call: wherever else
|
|
488
|
+
* Codex writes one, the running total does not move (tech notes, section 3).
|
|
419
489
|
*/
|
|
420
490
|
function spent(running, total, info) {
|
|
421
|
-
if (running === undefined)
|
|
491
|
+
if (running === undefined) {
|
|
492
|
+
const own = info.last_token_usage;
|
|
493
|
+
if (complete(own, CODEX_READ) && CODEX_FIELDS.every((field) => number(own[field]) === 0)) return undefined;
|
|
494
|
+
return perCall(info);
|
|
495
|
+
}
|
|
422
496
|
|
|
423
497
|
const moved = total.total_tokens - running.total_tokens;
|
|
424
498
|
if (moved === 0) return undefined;
|