@manyoars/oars 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/README.md +44 -29
  2. package/dist/cli.d.ts +4 -1
  3. package/dist/cli.js +102 -32
  4. package/dist/commands/login.d.ts +1 -1
  5. package/dist/commands/login.js +1 -1
  6. package/dist/gate/hook-main.js +13 -1
  7. package/dist/gate/hook-process.d.ts +11 -1
  8. package/dist/gate/hook-process.js +50 -2
  9. package/dist/gate/hook-record.d.ts +3 -0
  10. package/dist/gate/hook-record.js +8 -0
  11. package/dist/gate/hook.d.ts +2 -0
  12. package/dist/gate/hook.js +3 -0
  13. package/dist/gate/replay.js +2 -5
  14. package/dist/gate/report.js +1 -1
  15. package/dist/gate/transcripts.js +1 -1
  16. package/dist/hook-bin.js +27 -2
  17. package/dist/issuer.d.ts +7 -9
  18. package/dist/issuer.js +10 -32
  19. package/dist/main.js +3 -0
  20. package/dist/sessions/actions.js +78 -0
  21. package/dist/sessions/allow.d.ts +10 -1
  22. package/dist/sessions/allow.js +29 -9
  23. package/dist/sessions/baseline.d.ts +1 -1
  24. package/dist/sessions/baseline.js +6 -4
  25. package/dist/sessions/call-shape.d.ts +19 -0
  26. package/dist/sessions/call-shape.js +85 -0
  27. package/dist/sessions/cell-increments.d.ts +5 -1
  28. package/dist/sessions/cell-increments.js +84 -50
  29. package/dist/sessions/cells.d.ts +33 -3
  30. package/dist/sessions/cells.js +89 -9
  31. package/dist/sessions/codex.js +93 -27
  32. package/dist/sessions/command.d.ts +11 -1
  33. package/dist/sessions/command.js +64 -18
  34. package/dist/sessions/coverage.d.ts +19 -0
  35. package/dist/sessions/coverage.js +56 -0
  36. package/dist/sessions/cursor.js +2 -2
  37. package/dist/sessions/derive.js +72 -6
  38. package/dist/sessions/exclusion.d.ts +6 -0
  39. package/dist/sessions/exclusion.js +8 -0
  40. package/dist/sessions/extract.d.ts +4 -2
  41. package/dist/sessions/extract.js +58 -26
  42. package/dist/sessions/hook-calls.js +1 -1
  43. package/dist/sessions/hook-index.d.ts +14 -1
  44. package/dist/sessions/hook-index.js +10 -3
  45. package/dist/sessions/hooks.d.ts +5 -1
  46. package/dist/sessions/hooks.js +133 -22
  47. package/dist/sessions/identity.d.ts +8 -3
  48. package/dist/sessions/identity.js +45 -15
  49. package/dist/sessions/install.d.ts +18 -2
  50. package/dist/sessions/install.js +53 -8
  51. package/dist/sessions/mcp.d.ts +3 -2
  52. package/dist/sessions/mcp.js +8 -4
  53. package/dist/sessions/motif.d.ts +2 -1
  54. package/dist/sessions/motif.js +2 -2
  55. package/dist/sessions/opencode.d.ts +33 -0
  56. package/dist/sessions/opencode.js +319 -0
  57. package/dist/sessions/otel.d.ts +60 -0
  58. package/dist/sessions/otel.js +494 -0
  59. package/dist/sessions/package-books.d.ts +48 -0
  60. package/dist/sessions/package-books.js +120 -0
  61. package/dist/sessions/package-resolve.d.ts +2 -0
  62. package/dist/sessions/package-resolve.js +3 -1
  63. package/dist/sessions/pi.d.ts +36 -0
  64. package/dist/sessions/pi.js +353 -0
  65. package/dist/sessions/pool-books.d.ts +21 -0
  66. package/dist/sessions/pool-books.js +75 -0
  67. package/dist/sessions/pool-choices.d.ts +5 -0
  68. package/dist/sessions/pool-choices.js +11 -0
  69. package/dist/sessions/pool-command.d.ts +76 -5
  70. package/dist/sessions/pool-command.js +263 -136
  71. package/dist/sessions/pool-nightly-deps.d.ts +3 -0
  72. package/dist/sessions/pool-nightly-deps.js +11 -0
  73. package/dist/sessions/pool-nightly.d.ts +10 -0
  74. package/dist/sessions/pool-nightly.js +85 -0
  75. package/dist/sessions/pool-outbox.d.ts +15 -2
  76. package/dist/sessions/pool-outbox.js +79 -26
  77. package/dist/sessions/pool-print.d.ts +1 -1
  78. package/dist/sessions/pool-raw.d.ts +27 -0
  79. package/dist/sessions/pool-raw.js +68 -0
  80. package/dist/sessions/pool-replica.d.ts +35 -0
  81. package/dist/sessions/pool-replica.js +91 -0
  82. package/dist/sessions/pool-send-lock.d.ts +20 -0
  83. package/dist/sessions/pool-send-lock.js +87 -0
  84. package/dist/sessions/pool-sent.d.ts +4 -0
  85. package/dist/sessions/pool-sent.js +26 -0
  86. package/dist/sessions/pool-totals.d.ts +10 -0
  87. package/dist/sessions/pool-totals.js +21 -0
  88. package/dist/sessions/pool-wire.d.ts +240 -30
  89. package/dist/sessions/pool-wire.js +695 -215
  90. package/dist/sessions/public-vocabulary.d.ts +2 -3
  91. package/dist/sessions/public-vocabulary.js +2 -9
  92. package/dist/sessions/query.d.ts +16 -4
  93. package/dist/sessions/query.js +32 -10
  94. package/dist/sessions/read-cost.js +14 -3
  95. package/dist/sessions/read-recovery-edge.d.ts +2 -0
  96. package/dist/sessions/read-recovery-edge.js +26 -0
  97. package/dist/sessions/read.d.ts +8 -1
  98. package/dist/sessions/read.js +6 -3
  99. package/dist/sessions/recall-bundle.d.ts +12 -0
  100. package/dist/sessions/recall-bundle.js +33 -0
  101. package/dist/sessions/recall-frames.d.ts +18 -0
  102. package/dist/sessions/recall-frames.js +44 -0
  103. package/dist/sessions/recall-next.d.ts +145 -0
  104. package/dist/sessions/recall-next.js +241 -0
  105. package/dist/sessions/recall-object.d.ts +2 -0
  106. package/dist/sessions/recall-object.js +1 -0
  107. package/dist/sessions/recall-push.d.ts +2 -2
  108. package/dist/sessions/recall-push.js +3 -51
  109. package/dist/sessions/recall-reads.d.ts +7 -0
  110. package/dist/sessions/recall-reads.js +7 -0
  111. package/dist/sessions/recovery-calls.d.ts +21 -0
  112. package/dist/sessions/recovery-calls.js +25 -0
  113. package/dist/sessions/redact.d.ts +11 -0
  114. package/dist/sessions/redact.js +59 -0
  115. package/dist/sessions/schema.d.ts +59 -9
  116. package/dist/sessions/schema.js +22 -3
  117. package/dist/sessions/session-shape.d.ts +6 -3
  118. package/dist/sessions/session-shape.js +19 -6
  119. package/dist/sessions/sources.js +75 -1
  120. package/dist/sessions/store.d.ts +13 -1
  121. package/dist/sessions/store.js +36 -16
  122. package/dist/sessions/survival.d.ts +2 -0
  123. package/dist/sessions/survival.js +2 -2
  124. package/dist/sessions/transcript-tail.d.ts +14 -0
  125. package/dist/sessions/transcript-tail.js +64 -0
  126. package/dist/sessions/vscode-copilot.js +24 -7
  127. package/dist/telemetry/command.d.ts +8 -0
  128. package/dist/telemetry/command.js +85 -0
  129. package/dist/telemetry/consent.d.ts +17 -0
  130. package/dist/telemetry/consent.js +63 -0
  131. package/dist/telemetry/crash.d.ts +8 -0
  132. package/dist/telemetry/crash.js +52 -0
  133. package/dist/telemetry/emit.d.ts +31 -0
  134. package/dist/telemetry/emit.js +42 -0
  135. package/dist/telemetry/fields.d.ts +16 -0
  136. package/dist/telemetry/fields.js +26 -0
  137. package/dist/telemetry/health.d.ts +21 -0
  138. package/dist/telemetry/health.js +108 -0
  139. package/dist/telemetry/hook-emit.d.ts +4 -0
  140. package/dist/telemetry/hook-emit.js +18 -0
  141. package/dist/telemetry/measures.d.ts +32 -0
  142. package/dist/telemetry/measures.js +128 -0
  143. package/dist/telemetry/observe.d.ts +13 -0
  144. package/dist/telemetry/observe.js +51 -0
  145. package/dist/telemetry/read-events.d.ts +9 -0
  146. package/dist/telemetry/read-events.js +17 -0
  147. package/dist/telemetry/record.d.ts +17 -0
  148. package/dist/telemetry/record.js +26 -0
  149. package/dist/telemetry/report.d.ts +77 -0
  150. package/dist/telemetry/report.js +188 -0
  151. package/dist/telemetry/schema.d.ts +40 -0
  152. package/dist/telemetry/schema.js +187 -0
  153. package/dist/telemetry/send-main.d.ts +1 -0
  154. package/dist/telemetry/send-main.js +11 -0
  155. package/dist/telemetry/send.d.ts +36 -0
  156. package/dist/telemetry/send.js +172 -0
  157. package/dist/telemetry/spool.d.ts +17 -0
  158. package/dist/telemetry/spool.js +70 -0
  159. package/dist/telemetry/wire.d.ts +25 -0
  160. package/dist/telemetry/wire.js +76 -0
  161. package/package.json +2 -2
@@ -0,0 +1,85 @@
1
+ /** Where a path target sits in the project, as a closed list. `none` is a call with no path target (a shell call, a search, a fetch). */
2
+ export const TARGET_CLASSES = ['source', 'test', 'config', 'doc', 'lockfile', 'generated', 'data', 'none', 'other'];
3
+ /** What an argument holds, as a closed list: a call's arguments are the sorted set of these. `none` is a call with no arguments. */
4
+ export const ARG_CLASSES = ['path', 'url', 'pattern', 'command', 'number', 'flag', 'text', 'none'];
5
+ const words = (list) => new Set(list.split(/\s+/).filter((word) => word !== ''));
6
+ const LOCKFILES = words('package-lock.json npm-shrinkwrap.json yarn.lock pnpm-lock.yaml bun.lockb bun.lock cargo.lock poetry.lock pipfile.lock uv.lock gemfile.lock composer.lock go.sum gradle.lockfile packages.lock.json podfile.lock pubspec.lock mix.lock flake.lock');
7
+ const GENERATED_DIRS = words('dist build out node_modules .next .nuxt .astro .svelte-kit coverage target __generated__ generated .turbo .cache __pycache__ vendor');
8
+ const TEST_DIRS = words('test tests __tests__ spec specs e2e __mocks__ fixtures testdata cypress');
9
+ const DOC_DIRS = words('docs doc documentation');
10
+ const SOURCE_EXTENSIONS = words('ts tsx mts cts js jsx mjs cjs py pyi java kt kts go rs rb php cs c h cc cpp hpp swift scala sh bash zsh ps1 psm1 sql astro vue svelte css scss sass less html htm lua dart ex exs erl clj hs ml r jl pl zig groovy gradle');
11
+ const DOC_EXTENSIONS = words('md mdx rst txt adoc org tex');
12
+ const CONFIG_EXTENSIONS = words('json jsonc json5 yaml yml toml ini cfg conf properties env plist editorconfig xml');
13
+ const DATA_EXTENSIONS = words('csv tsv ndjson jsonl parquet avro arrow db sqlite sqlite3 log xlsx xls dat bin pkl');
14
+ const CONFIG_BASENAMES = words('dockerfile makefile justfile procfile gemfile rakefile pom.xml build.gradle settings.gradle .gitignore .gitattributes .npmrc .nvmrc .prettierrc .eslintrc .dockerignore .editorconfig');
15
+ const DOC_BASENAMES = words('readme license licence changelog contributing notice authors codeowners');
16
+ const TEST_NAME = /(?:^|[._-])(?:test|tests|spec)(?:[._-]|$)|(?:_test|_spec|Test|Tests|Spec)\.[a-z]+$/;
17
+ const GENERATED_NAME = /\.(?:min\.(?:js|css)|d\.ts|map|generated\.[a-z]+|pb\.go|g\.dart)$/;
18
+ /**
19
+ * The class of a path target, from the base that hashed it and the path's own suffix. A call with no path target (`base` null) is `none`; the order below is the order a path is asked,
20
+ * so a lockfile is never `config` and a `tests/data.json` is `test`. The mapping is pinned in `call-shape.test.ts`. `path` is the key the target is hashed on (relative to the repository or the cwd, never the absolute path), so the folders above a repository do not decide the class; it is read here and goes no further.
21
+ */
22
+ export const targetClassOf = (path, base) => {
23
+ if (base === null || path === undefined || path === '')
24
+ return 'none';
25
+ const parts = path.replace(/\\/g, '/').split('/').filter((part) => part !== '' && part !== '.');
26
+ const name = parts.at(-1) ?? '';
27
+ const lower = name.toLowerCase();
28
+ const dirs = parts.slice(0, -1).map((part) => part.toLowerCase());
29
+ const dot = lower.lastIndexOf('.');
30
+ const extension = dot > 0 ? lower.slice(dot + 1) : lower.startsWith('.') ? lower.slice(1) : '';
31
+ if (LOCKFILES.has(lower))
32
+ return 'lockfile';
33
+ if (dirs.some((dir) => GENERATED_DIRS.has(dir)) || GENERATED_NAME.test(lower))
34
+ return 'generated';
35
+ if (dirs.some((dir) => TEST_DIRS.has(dir)) || TEST_NAME.test(name))
36
+ return 'test';
37
+ const stem = dot > 0 ? lower.slice(0, dot) : lower;
38
+ if (DOC_EXTENSIONS.has(extension) || DOC_BASENAMES.has(stem) || dirs.some((dir) => DOC_DIRS.has(dir)))
39
+ return 'doc';
40
+ if (CONFIG_BASENAMES.has(lower) || /^\.[a-z0-9_.-]*(?:rc|ignore)(?:\.[a-z]+)?$/.test(lower) || /\.config\.[a-z]+$/.test(lower) || /^tsconfig[a-z0-9.-]*\.json$/.test(lower) || /^\.env(?:\.|$)/.test(lower))
41
+ return 'config';
42
+ if (SOURCE_EXTENSIONS.has(extension))
43
+ return 'source';
44
+ if (DATA_EXTENSIONS.has(extension))
45
+ return 'data';
46
+ if (CONFIG_EXTENSIONS.has(extension))
47
+ return 'config';
48
+ return 'other';
49
+ };
50
+ const PATH_ARGS = words('file_path notebook_path path cwd workdir directory dir filepath filename file');
51
+ const URL_ARGS = words('url uri href');
52
+ const PATTERN_ARGS = words('pattern regex regexp glob include exclude');
53
+ const COMMAND_ARGS = words('command cmd script');
54
+ const URL_VALUE = /^[a-z][a-z0-9+.-]*:\/\/\S+$/i;
55
+ const argClassOfEntry = (key, value) => {
56
+ if (value === null || value === undefined)
57
+ return null;
58
+ if (typeof value === 'number')
59
+ return 'number';
60
+ if (typeof value === 'boolean')
61
+ return 'flag';
62
+ const lower = key.toLowerCase();
63
+ if (PATH_ARGS.has(lower))
64
+ return 'path';
65
+ if (URL_ARGS.has(lower) || (typeof value === 'string' && URL_VALUE.test(value)))
66
+ return 'url';
67
+ if (PATTERN_ARGS.has(lower))
68
+ return 'pattern';
69
+ if (COMMAND_ARGS.has(lower))
70
+ return 'command';
71
+ return 'text';
72
+ };
73
+ /**
74
+ * The classes of a call's arguments, as a sorted set: which of path, URL, pattern, command, number, flag, and text its input holds, or just `none` when it holds no value. A key name
75
+ * gives the class of a string (`file_path` is a path, `pattern` a pattern, `command` a command), a number or a boolean gives its own, and any other value is text. The values are read here and go no further.
76
+ */
77
+ export const argClassOf = (input) => {
78
+ const held = new Set();
79
+ for (const [key, value] of Object.entries(input)) {
80
+ const kind = argClassOfEntry(key, value);
81
+ if (kind !== null)
82
+ held.add(kind);
83
+ }
84
+ return held.size === 0 ? ['none'] : ARG_CLASSES.filter((kind) => held.has(kind)).sort();
85
+ };
@@ -15,6 +15,10 @@ export interface CellRow {
15
15
  ok: number;
16
16
  failed: number;
17
17
  tokens: number;
18
+ /** How many of the batch's sessions recorded input, the coverage of `tokens`; the served column sums it. */
19
+ tokenSessions: number;
20
+ /** How many of the batch's prompts (or steps) `tokens` covers: the divisor of a per-prompt mean. */
21
+ tokenPrompts: number;
18
22
  durationMs: number;
19
23
  }
20
24
  export interface StratumRow {
@@ -35,7 +39,7 @@ export interface Increments {
35
39
  * Rules 1 and 2: a window names a shell step by its program and subcommand alone, and each flag and each pull of it adds a one-step cell of its own.
36
40
  */
37
41
  export declare const cellsOf: (store: Store, graph?: RecallGraph) => CellSet;
38
- /** A local store with each shell step read as a deposit reads it (`projectedSteps`), so its named motifs are the ones the pool holds. */
42
+ /** A local store with each shell step read as a deposit reads it (`projectedSteps`), so its named motifs are the ones the pool holds. It uses the open gate, which keeps the name shape and length but applies no redactor or exclusion term: a local read sends nothing. */
39
43
  export declare const projectedStore: (store: Store, read: ShellReader) => Store;
40
44
  /** One account's deposits as a store. A session's id is its skeleton's, which the client hashes per account, so it joins nothing across accounts. */
41
45
  export declare const storeOf: (skeletons: readonly SessionSkeleton[]) => Store;
@@ -1,10 +1,11 @@
1
1
  // The cell computation a deposit and a local read share (pool vocabulary spec Section 2, "the deposit form", and Section 4, rules 1 to 6). The pool's ingest turns a batch's skeleton into
2
2
  // increments with `incrementsOf`, and a local read derives its cells with `cellsOf` over the store it holds, projected the way a deposit projects it (`projectedStore`). Both end in the
3
3
  // one `cellsOf`, so a session gives the same cells at the pool and on the machine. This file reads no file system: the pool's Worker bundles it.
4
+ import { ARG_CLASSES } from './call-shape.js';
4
5
  import { bucketFields, deriveCells, ENVIRONMENT_BUCKET, isEnvironmentCell } from './cells.js';
5
- import { bucketOf, floorOf, projectedSteps } from './pool-wire.js';
6
+ import { bucketOf, floorOf, OTHER, projectedSteps } from './pool-wire.js';
6
7
  import { NO_RECALLS } from './recall-consequence.js';
7
- import { harnessOf } from './schema.js';
8
+ import { USAGE_FIELDS, harnessOf } from './schema.js';
8
9
  const floorTokens = (value) => floorOf('tokens', bucketOf('tokens', value));
9
10
  const floorUsage = ({ input, cacheWrite, cacheRead, output, thinking }) => ({
10
11
  input: floorTokens(input),
@@ -55,14 +56,25 @@ export const cellsOf = (store, graph = NO_RECALLS) => {
55
56
  const cells = [...whole.cells, ...marginal.cells.filter(({ motifId }) => named.has(motifId))].sort((a, b) => order(a.motifId, b.motifId) || order(a.frameBucket, b.frameBucket) || order(a.grain, b.grain) || order(a.day, b.day) || Number(a.exposed) - Number(b.exposed));
56
57
  return { cells, motifs: [...motifs.values()].sort((a, b) => order(a.motifId, b.motifId)) };
57
58
  };
58
- /** A local store with each shell step read as a deposit reads it (`projectedSteps`), so its named motifs are the ones the pool holds. */
59
+ /** A local store with each shell step read as a deposit reads it (`projectedSteps`), so its named motifs are the ones the pool holds. It uses the open gate, which keeps the name shape and length but applies no redactor or exclusion term: a local read sends nothing. */
59
60
  export const projectedStore = (store, read) => {
60
61
  const harnesses = new Map([...store.sessions, ...store.subagents].map((session) => [session.session, harnessOf(session)]));
61
62
  return { ...store, tools: store.tools.map((tool) => (tool.steps === undefined ? tool : { ...tool, steps: projectedSteps(tool, read, harnesses.get(tool.session) ?? 'claude-code') })) };
62
63
  };
63
64
  const NO_USAGE = { input: 0, cacheWrite: 0, cacheRead: 0, output: 0, thinking: 0 };
65
+ const isKnown = (transcript) => transcript.harness !== OTHER;
66
+ /** A frame whose own harness is `other` takes its transcript's: the two are the same harness, and a frame cannot name one the transcript does not. A repo size this reader does not know is a size the frame does not name. */
67
+ const frameOf = (frame, harness) => ({
68
+ ...frame,
69
+ harness: frame.harness === OTHER ? harness : frame.harness,
70
+ stack: { ...frame.stack, size: frame.stack.size === OTHER ? null : frame.stack.size },
71
+ });
72
+ /** A class this reader does not know counts in `mixed`, the class a prompt that fits no other takes. */
73
+ const classOf = (cls) => (cls === OTHER ? 'mixed' : cls);
74
+ /** A status this reader does not know is a segment the call did not report. */
75
+ const statusOf = (status) => (status === OTHER ? 'none' : status);
64
76
  /** A skeleton's transcript as the record `deriveCells` reads. Everything a skeleton leaves out stays empty, so no derivation here can come to depend on it unnoticed. */
65
- const sessionRecord = (id, parent, { harness, frame }) => ({
77
+ const sessionRecord = (id, parent, spawnedIn, { harness, frame }) => ({
66
78
  kind: 'session',
67
79
  cv: '',
68
80
  sid: '',
@@ -71,7 +83,7 @@ const sessionRecord = (id, parent, { harness, frame }) => ({
71
83
  parent,
72
84
  agentType: null,
73
85
  spawnedBy: null,
74
- spawnedIn: null,
86
+ spawnedIn,
75
87
  rev: 1,
76
88
  at: '',
77
89
  bytes: 0,
@@ -87,7 +99,8 @@ const sessionRecord = (id, parent, { harness, frame }) => ({
87
99
  tools: {},
88
100
  files: [],
89
101
  fileHashes: [],
90
- repo: null,
102
+ // The store's salted key, so a read that groups by repo works on pooled data from one store (one machine). It joins no cell.
103
+ repo: frame.repoKey ?? null,
91
104
  urls: [],
92
105
  searches: [],
93
106
  bashCmds: 0,
@@ -99,9 +112,9 @@ const sessionRecord = (id, parent, { harness, frame }) => ({
99
112
  costUsd: null,
100
113
  usage: NO_USAGE,
101
114
  harness,
102
- frame,
115
+ frame: frameOf(frame, harness),
103
116
  });
104
- const promptRecords = (id, { prompts }) => prompts.map(({ cls, errors, wallSec, usage, explore, frame }, index) => ({
117
+ const promptRecords = (id, { harness, prompts }) => prompts.map(({ cls, errors, wallSec, usage, usageResponses, explore, frame }, index) => ({
105
118
  kind: 'prompt',
106
119
  v: '',
107
120
  session: id,
@@ -110,7 +123,7 @@ const promptRecords = (id, { prompts }) => prompts.map(({ cls, errors, wallSec,
110
123
  ts: null,
111
124
  wallSec: wallSec === null ? null : floorOf('seconds', wallSec),
112
125
  head: '',
113
- cls,
126
+ cls: classOf(cls),
114
127
  tools: [],
115
128
  shape: '',
116
129
  errors: floorOf('count', errors),
@@ -120,54 +133,71 @@ const promptRecords = (id, { prompts }) => prompts.map(({ cls, errors, wallSec,
120
133
  model: frame.model ?? '',
121
134
  efforts: frame.effort === null ? [] : [frame.effort],
122
135
  skills: [],
136
+ // A category the wire sent as null was never recorded: it reads as a placeholder zero here and is left out of `usageObserved`.
123
137
  usage: {
124
- input: floorOf('tokens', usage.input),
125
- cacheWrite: floorOf('tokens', usage.cacheWrite),
126
- cacheRead: floorOf('tokens', usage.cacheRead),
127
- output: floorOf('tokens', usage.output),
128
- thinking: floorOf('tokens', usage.thinking),
138
+ input: floorOf('tokens', usage.input ?? 0),
139
+ cacheWrite: floorOf('tokens', usage.cacheWrite ?? 0),
140
+ cacheRead: floorOf('tokens', usage.cacheRead ?? 0),
141
+ output: floorOf('tokens', usage.output ?? 0),
142
+ thinking: floorOf('tokens', usage.thinking ?? 0),
129
143
  },
144
+ usageObserved: USAGE_FIELDS.filter((field) => usage[field] !== null),
145
+ ...(usageResponses === undefined ? {} : { usageResponses: floorOf('count', usageResponses) }),
130
146
  recovery: [],
131
147
  // The lowest value of the bucket, as every other bucketed value counts; a prompt that sent none counts none.
132
148
  explore: explore === null ? 0 : floorOf('calls', explore),
133
149
  act: 0,
134
150
  bigrams: {},
135
151
  contextFingerprint: null,
136
- frame,
152
+ frame: frameOf(frame, harness),
137
153
  }));
138
- const toolRecords = (id, main, { tools }) => tools.map(({ side, prompt, status, durationMs, steps }, n) => ({
139
- kind: 'tool',
140
- cv: '',
141
- sid: '',
142
- session: id,
143
- rev: 1,
144
- parent: main,
145
- agentType: null,
146
- n,
147
- ts: null,
148
- tool: steps[0]?.native ?? '',
149
- status,
150
- target: null,
151
- targetBase: null,
152
- keys: [],
153
- durationMs,
154
- toolUseId: '',
155
- harnessVersion: null,
156
- side,
157
- prompt,
158
- steps: steps.map(({ action, native, identity, status: reported, parent, tokens, ms }, seg) => ({
159
- action,
160
- native,
161
- identity,
162
- status: reported,
163
- parent,
164
- seg,
165
- edges: [],
166
- cost: { tokens: tokens === null ? null : floorOf('tokens', tokens), durationMs: ms === null ? null : floorOf('ms', ms) },
154
+ /** A step's place as the store names it: its call's position and its segment. A transcript's steps are numbered across its calls in order, which is what an edge's target counts. */
155
+ const stepPlaces = ({ tools }) => tools.flatMap(({ steps }, n) => steps.map((_, seg) => ({ n, seg })));
156
+ const toolRecords = (id, main, transcript, spawner) => {
157
+ const own = stepPlaces(transcript);
158
+ const spawning = spawner === null ? [] : stepPlaces(spawner);
159
+ return transcript.tools.map(({ side, prompt, status, durationMs, errorCode, keys, targetClass, argClass, steps }, n) => ({
160
+ kind: 'tool',
161
+ cv: '',
162
+ sid: '',
163
+ session: id,
164
+ rev: 1,
165
+ parent: main,
166
+ agentType: null,
167
167
  n,
168
- frameId: '',
169
- })),
170
- }));
168
+ ts: null,
169
+ tool: steps[0]?.native ?? '',
170
+ status: statusOf(status),
171
+ target: null,
172
+ targetBase: null,
173
+ // The names the wire carried, which are the only ones a skeleton-derived store ever holds; `keysDropped` is a count of names not sent and has no stored form.
174
+ keys: keys ?? [],
175
+ ...(targetClass === undefined ? {} : { targetClass }),
176
+ ...(argClass === undefined ? {} : { argClass: ARG_CLASSES.filter((kind) => argClass.includes(kind)).sort() }),
177
+ durationMs,
178
+ toolUseId: '',
179
+ harnessVersion: null,
180
+ side,
181
+ prompt,
182
+ ...(errorCode === null ? {} : { errorCode }),
183
+ steps: steps.map(({ action, native, identity, status: reported, parent, tokens, ms, edges }, seg) => ({
184
+ action,
185
+ native,
186
+ identity,
187
+ status: statusOf(reported),
188
+ parent,
189
+ seg,
190
+ // A target is read back as the call position and segment it names, in this transcript or, for a `spawned` edge, in the spawning one. A target the transcript does not hold names nothing and is left out, and so does a kind this reader does not know (`other`).
191
+ edges: (edges ?? []).flatMap(({ kind, target }) => {
192
+ const place = (kind === 'spawned' ? spawning : own)[target];
193
+ return place === undefined || kind === OTHER ? [] : [{ kind, n: place.n, seg: place.seg }];
194
+ }),
195
+ cost: { tokens: tokens === null ? null : floorOf('tokens', tokens), durationMs: ms === null ? null : floorOf('ms', ms) },
196
+ n,
197
+ frameId: '',
198
+ })),
199
+ }));
200
+ };
171
201
  /** One account's deposits as a store. A session's id is its skeleton's, which the client hashes per account, so it joins nothing across accounts. */
172
202
  export const storeOf = (skeletons) => {
173
203
  const store = { sessions: [], prompts: [], errors: [], subagents: [], tools: [], newerCaptures: 0 };
@@ -175,10 +205,13 @@ export const storeOf = (skeletons) => {
175
205
  const nameOf = (index) => `${id}#${String(index)}`;
176
206
  const main = nameOf(0);
177
207
  transcripts.forEach((transcript, index) => {
208
+ if (!isKnown(transcript))
209
+ return;
178
210
  const session = nameOf(index);
179
- (index === 0 ? store.sessions : store.subagents).push(sessionRecord(session, index === 0 ? null : main, transcript));
211
+ const spawner = transcript.spawnedIn === null ? null : (transcripts[transcript.spawnedIn] ?? null);
212
+ (index === 0 ? store.sessions : store.subagents).push(sessionRecord(session, index === 0 ? null : main, transcript.spawnedIn === null ? null : nameOf(transcript.spawnedIn), transcript));
180
213
  store.prompts.push(...promptRecords(session, transcript));
181
- store.tools.push(...toolRecords(session, main, transcript));
214
+ store.tools.push(...toolRecords(session, main, transcript, spawner));
182
215
  });
183
216
  }
184
217
  return store;
@@ -190,7 +223,8 @@ export const incrementsOf = (skeleton) => {
190
223
  for (const cell of derived.cells) {
191
224
  const exposed = cell.exposed ? 1 : 0;
192
225
  const key = `${cell.motifId}/${cell.frameBucket}/${cell.grain}/${cell.day}/${String(exposed)}`;
193
- cells.set(key, { key, harnessBucket: isEnvironmentCell(cell) ? ENVIRONMENT_BUCKET : bucketFields(cell.frameBucket).harnessBucket, motifId: cell.motifId, frameBucket: cell.frameBucket, grain: cell.grain, day: cell.day, exposed, sessions: cell.sessions, ok: cell.ok, failed: cell.failed, tokens: cell.tokens, durationMs: cell.durationMs });
226
+ // The served column is a running sum, so a cell no session of this batch recorded tokens for adds nothing to it; the sum itself holds only what was observed, and `tokenSessions` and `tokenPrompts` say how much of the cell it covers.
227
+ cells.set(key, { key, harnessBucket: isEnvironmentCell(cell) ? ENVIRONMENT_BUCKET : bucketFields(cell.frameBucket).harnessBucket, motifId: cell.motifId, frameBucket: cell.frameBucket, grain: cell.grain, day: cell.day, exposed, sessions: cell.sessions, ok: cell.ok, failed: cell.failed, tokens: cell.tokens ?? 0, tokenSessions: cell.tokenSessions ?? 0, tokenPrompts: cell.tokenPrompts ?? 0, durationMs: cell.durationMs });
194
228
  }
195
229
  // A session counts once per value, so each value is one session of this batch.
196
230
  const strata = new Map();
@@ -6,7 +6,7 @@ import type { Store } from './store.js';
6
6
  export type Grain = 'day' | 'week';
7
7
  export interface Cell {
8
8
  motifId: string;
9
- /** `harness/versionBucket/model`, public names (`frameBucketOf`). Effort is not in it (rule 3). */
9
+ /** `harness/versionBucket/model`, public names (`frameBucketOf`), or `env:<platform>:<shell>` for a (name, `platform`, `shell`) cell, which names no harness, version, or model (`ENVIRONMENT_BUCKET`). Effort is not in it (rule 3). */
10
10
  frameBucket: string;
11
11
  /** What `day` names: a UTC day, or a UTC week. A read takes the grain it answers from and never the other. */
12
12
  grain: Grain;
@@ -20,8 +20,15 @@ export interface Cell {
20
20
  ok: number;
21
21
  /** Windows whose last step failed; a `class` cell: prompts with an error. A step with no result is in neither. */
22
22
  failed: number;
23
- /** Summed over each window's steps; a `class` cell: the prompts' whole usage. */
24
- tokens: number;
23
+ /**
24
+ * Summed over each window's steps; a `class` cell: the whole usage of the prompts that recorded input, counting only the categories each recorded (a placeholder zero beside
25
+ * `usageObserved` is not a measurement). Null when no session recorded any: absent, never a measured 0.
26
+ */
27
+ tokens: number | null;
28
+ /** How many of `sessions` recorded the cost `tokens` sums (a class cell: input; a step cell: a step cost), which is the coverage the `tokens` figure has (a session that recorded input but not every category still counts as covered, so the figure can be partial by category); less than `sessions` makes it a partial sum. Absent on a cell read back from a stored sum written before the pool held coverage. */
29
+ tokenSessions?: number;
30
+ /** How many of the cell's prompts the `tokens` figure covers (a step cell counts steps): the divisor of a per-prompt mean, since `tokens` holds only prompts that recorded input. Absent like `tokenSessions`. */
31
+ tokenPrompts?: number;
25
32
  durationMs: number;
26
33
  /** Whether these counts sit inside a shown recall's consequence window, wholly or in part. It is part of the cell's key, so a motif's exposed and unexposed counts stay apart. */
27
34
  exposed: boolean;
@@ -34,6 +41,27 @@ export declare const EFFORT_CLASS = "effort:";
34
41
  * carries `week` and `exposure` but no frame axis: rule 3 bars joining `effort` to `model` or `harnessVersion`, and the pair would be four dimensions over `q_dims`.
35
42
  */
36
43
  export declare const effortClass: (effort: string, thinking: number) => string;
44
+ /**
45
+ * The two day-cell families of a failed call by its `errorCode` (t868): `error:<code>/<name>` keeps the model and no version beside the code, and `error-version:<code>/<name>` keeps the harness version bucket and no model
46
+ * (`versionBucketOf`). Each joins three dimensions: name, code, and either model or version. A class motif with no prompt class in it, so a read of prompt classes skips both by prefix (`isPromptClass`).
47
+ */
48
+ export declare const ERROR_CLASS = "error:";
49
+ export declare const ERROR_VERSION_CLASS = "error-version:";
50
+ /**
51
+ * The week cell of what followed a failed call (t868): `recovery:<code>/<failed action>><next action>`, where the next action is that of the call right after it in the same prompt, or `none` when nothing
52
+ * followed. It keeps the harness version bucket and no model, so it joins three dimensions: the failed-to-next edge, code, and version. A class motif with no prompt class in it, like the others.
53
+ */
54
+ export declare const RECOVERY_CLASS = "recovery:";
55
+ export declare const NO_NEXT = "none";
56
+ export declare const recoveryClass: (code: string, failed: string, next: string) => string;
57
+ /** The three parts of a recovery class name, or null for a name that is not one. An action name holds no `>` or `/`, and a code holds no `/`. */
58
+ export declare const parseRecoveryClass: (cls: string) => {
59
+ code: string;
60
+ failed: string;
61
+ next: string;
62
+ } | null;
63
+ /** Whether a class motif names a prompt class, as opposed to one of the side cells (effort, or a failed call's code or what followed it). */
64
+ export declare const isPromptClass: (cls: string | null) => boolean;
37
65
  /** The frame bucket of a cell that joins no axis. */
38
66
  export declare const NO_AXES = "-";
39
67
  /**
@@ -43,6 +71,8 @@ export declare const NO_AXES = "-";
43
71
  export declare const harnessBucket: (version: string | null) => string;
44
72
  /** Only what most comparisons cross: platform, shell, and stack are on the frame and in the records, so a later extractor can add them without new capture. */
45
73
  export declare const frameBucketOf: (frame: Frame) => string;
74
+ /** The frame bucket of a cell that keeps the version and drops the model: `harness/versionBucket/-`. */
75
+ export declare const versionBucketOf: (frame: Frame) => string;
46
76
  export declare const bucketFields: (bucket: string) => {
47
77
  harness: string;
48
78
  harnessBucket: string;
@@ -1,11 +1,12 @@
1
1
  // The cell (recall spec Section 2.5): counts for one motif in one frame bucket in one UTC week (or, for a live cell, one UTC day), order 0. Cells are computed from the capture records, which
2
2
  // stay the primary record, so a cell file can be dropped and rebuilt and any later derivation can run over old data. Nothing here reads raw.
3
3
  import { closedFrame, utcDay } from './frame.js';
4
- import { classMotif, motifsOf, stepsOfCall } from './motif.js';
4
+ import { classMotif, motifName, motifsOf, stepsOfCall } from './motif.js';
5
5
  import { bucketOf } from './pool-wire.js';
6
- import { CALL_SHAPE_METHOD, cellRef, contentHash, METHODS } from './read.js';
6
+ import { CALL_SHAPE_METHOD, cellRef, contentHash, METHODS, RECOVERY_EDGE_METHOD } from './read.js';
7
+ import { recoveryCallsOf } from './recovery-calls.js';
7
8
  import { NO_RECALLS } from './recall-consequence.js';
8
- import { EXTRACTOR_VERSION, harnessOf } from './schema.js';
9
+ import { ERROR_CODES, EXTRACTOR_VERSION, harnessOf, observesUsage } from './schema.js';
9
10
  export const UNKNOWN = 'unknown';
10
11
  /** The class name an effort cell carries: a class motif with no prompt class in it, so a read of prompt classes skips it by this prefix. */
11
12
  export const EFFORT_CLASS = 'effort:';
@@ -14,6 +15,26 @@ export const EFFORT_CLASS = 'effort:';
14
15
  * carries `week` and `exposure` but no frame axis: rule 3 bars joining `effort` to `model` or `harnessVersion`, and the pair would be four dimensions over `q_dims`.
15
16
  */
16
17
  export const effortClass = (effort, thinking) => `${EFFORT_CLASS}${effort}/thinking:${String(bucketOf('tokens', thinking))}`;
18
+ /**
19
+ * The two day-cell families of a failed call by its `errorCode` (t868): `error:<code>/<name>` keeps the model and no version beside the code, and `error-version:<code>/<name>` keeps the harness version bucket and no model
20
+ * (`versionBucketOf`). Each joins three dimensions: name, code, and either model or version. A class motif with no prompt class in it, so a read of prompt classes skips both by prefix (`isPromptClass`).
21
+ */
22
+ export const ERROR_CLASS = 'error:';
23
+ export const ERROR_VERSION_CLASS = 'error-version:';
24
+ /**
25
+ * The week cell of what followed a failed call (t868): `recovery:<code>/<failed action>><next action>`, where the next action is that of the call right after it in the same prompt, or `none` when nothing
26
+ * followed. It keeps the harness version bucket and no model, so it joins three dimensions: the failed-to-next edge, code, and version. A class motif with no prompt class in it, like the others.
27
+ */
28
+ export const RECOVERY_CLASS = 'recovery:';
29
+ export const NO_NEXT = 'none';
30
+ export const recoveryClass = (code, failed, next) => `${RECOVERY_CLASS}${code}/${failed}>${next}`;
31
+ /** The three parts of a recovery class name, or null for a name that is not one. An action name holds no `>` or `/`, and a code holds no `/`. */
32
+ export const parseRecoveryClass = (cls) => {
33
+ const found = /^recovery:([^/]+)\/([^>/]+)>([^>/]+)$/.exec(cls);
34
+ return found === null ? null : { code: found[1] ?? '', failed: found[2] ?? '', next: found[3] ?? '' };
35
+ };
36
+ /** Whether a class motif names a prompt class, as opposed to one of the side cells (effort, or a failed call's code or what followed it). */
37
+ export const isPromptClass = (cls) => cls !== null && ![EFFORT_CLASS, ERROR_CLASS, ERROR_VERSION_CLASS, RECOVERY_CLASS].some((prefix) => cls.startsWith(prefix));
17
38
  /** The frame bucket of a cell that joins no axis. */
18
39
  export const NO_AXES = '-';
19
40
  const ONE_OPERATOR = 1;
@@ -29,6 +50,8 @@ export const harnessBucket = (version) => {
29
50
  };
30
51
  /** Only what most comparisons cross: platform, shell, and stack are on the frame and in the records, so a later extractor can add them without new capture. */
31
52
  export const frameBucketOf = (frame) => [frame.harness, harnessBucket(frame.harnessVersion), frame.model ?? UNKNOWN].join('/');
53
+ /** The frame bucket of a cell that keeps the version and drops the model: `harness/versionBucket/-`. */
54
+ export const versionBucketOf = (frame) => [frame.harness, harnessBucket(frame.harnessVersion), NO_AXES].join('/');
32
55
  export const bucketFields = (bucket) => {
33
56
  const [harness = UNKNOWN, version = UNKNOWN, model = UNKNOWN] = bucket.split('/');
34
57
  return { harness, harnessBucket: version, model };
@@ -69,7 +92,9 @@ export const cellIdOf = (cell) => {
69
92
  cellIds.set(cell, id);
70
93
  return id;
71
94
  };
72
- const newTally = (motifId, frameBucket, grain, day, exposed) => ({ motifId, frameBucket, grain, day, sessions: new Set(), ok: 0, failed: 0, tokens: 0, durationMs: 0, exposed });
95
+ /** The usage categories a prompt's tokens add up: thinking is part of the output already billed, so it is not summed. */
96
+ const TOKEN_FIELDS = ['input', 'cacheWrite', 'cacheRead', 'output'];
97
+ const newTally = (motifId, frameBucket, grain, day, exposed) => ({ motifId, frameBucket, grain, day, sessions: new Set(), ok: 0, failed: 0, tokens: 0, recorded: new Set(), recordedUnits: 0, durationMs: 0, exposed });
73
98
  const compare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
74
99
  /** A frame for a record captured before frames existed, from the fields it does hold, held to the same public vocabulary. */
75
100
  const oldFrame = (parts) => closedFrame({
@@ -143,11 +168,60 @@ export const deriveCells = (store, graph = NO_RECALLS) => {
143
168
  if (status === 'error')
144
169
  tally.failed += 1;
145
170
  for (const step of window) {
146
- tally.tokens += step.tokens;
171
+ // A step with no recorded cost has no tokens to count, so it joins neither the sum nor the coverage (its duration is still counted).
172
+ if (step.tokens !== null) {
173
+ tally.tokens += step.tokens;
174
+ tally.recorded.add(first.parent);
175
+ tally.recordedUnits += 1;
176
+ }
147
177
  tally.durationMs += step.durationMs;
148
178
  }
149
179
  }
150
180
  }
181
+ // What followed each failed call that has a code (t868): a week cell per (code, failed action, next action).
182
+ // A call is read by its first step's action, as a skeleton's is; a record captured before steps has the action its native name maps to.
183
+ const calls = line.map((call) => ({ side: call.side, prompt: call.prompt, status: call.status, steps: stepsOfCall(call, harness) }));
184
+ for (const [at, tool] of line.entries()) {
185
+ const followed = recoveryCallsOf(calls, at);
186
+ if (followed === null || tool.errorCode === undefined)
187
+ continue;
188
+ const code = ERROR_CODES.has(tool.errorCode) ? tool.errorCode : 'other';
189
+ const motif = classMotif(recoveryClass(code, calls[at]?.steps[0]?.action ?? 'other', followed.next[0] ?? NO_NEXT));
190
+ motifs.set(motif.motifId, motif);
191
+ const tally = tallyAt(motif, versionBucketOf(frame), 'week', weekStartOf(frame.day ?? UNKNOWN), false);
192
+ tally.sessions.add(first.parent);
193
+ tally.failed += 1;
194
+ }
195
+ // The failed calls by their `errorCode` (t868). Rule 6: a day cell exists for a known day only.
196
+ const failedDay = frame.day ?? UNKNOWN;
197
+ for (const tool of failedDay === UNKNOWN ? [] : line) {
198
+ if (tool.status !== 'error' || tool.errorCode === undefined)
199
+ continue;
200
+ const code = ERROR_CODES.has(tool.errorCode) ? tool.errorCode : 'other';
201
+ for (const counted of stepsOfCall(tool, harness)) {
202
+ if (counted.status !== 'error')
203
+ continue;
204
+ // The call's name at both levels: its action alone, and a named call (a flag or a pull would be a second dimension, so those steps are named by program and subcommand alone upstream).
205
+ for (const { motif: named } of motifsOf([counted])) {
206
+ if (named.level === 'named' && counted.identity === null)
207
+ continue;
208
+ const name = motifName(named);
209
+ for (const [prefix, bucket] of [[ERROR_CLASS, `${frame.harness}/${NO_AXES}/${frame.model ?? UNKNOWN}`], [ERROR_VERSION_CLASS, versionBucketOf(frame)]]) {
210
+ const motif = classMotif(`${prefix}${code}/${name}`);
211
+ motifs.set(motif.motifId, motif);
212
+ const tally = tallyAt(motif, bucket, 'day', failedDay, false);
213
+ tally.sessions.add(first.parent);
214
+ tally.failed += 1;
215
+ if (counted.tokens !== null) {
216
+ tally.tokens += counted.tokens;
217
+ tally.recorded.add(first.parent);
218
+ tally.recordedUnits += 1;
219
+ }
220
+ tally.durationMs += counted.durationMs;
221
+ }
222
+ }
223
+ }
224
+ }
151
225
  }
152
226
  for (const prompt of store.prompts) {
153
227
  const session = sessions.get(prompt.session);
@@ -160,14 +234,20 @@ export const deriveCells = (store, graph = NO_RECALLS) => {
160
234
  tally.failed += 1;
161
235
  else
162
236
  tally.ok += 1;
163
- const { input, cacheWrite, cacheRead, output } = prompt.usage;
164
- tally.tokens += input + cacheWrite + cacheRead + output;
237
+ // A prompt that recorded no input has no whole usage to count, and a category it never recorded is a placeholder zero, so neither enters the sum.
238
+ if (observesUsage(prompt, 'input')) {
239
+ tally.recorded.add(prompt.session);
240
+ tally.recordedUnits += 1;
241
+ for (const field of TOKEN_FIELDS)
242
+ if (observesUsage(prompt, field))
243
+ tally.tokens += prompt.usage[field];
244
+ }
165
245
  tally.durationMs += (prompt.wallSec ?? 0) * 1000;
166
246
  }
167
247
  }
168
248
  const cells = [...tallies.values()]
169
249
  .sort((a, b) => compare(a.motifId, b.motifId) || compare(a.frameBucket, b.frameBucket) || compare(a.grain, b.grain) || compare(a.day, b.day) || Number(a.exposed) - Number(b.exposed))
170
- .map((tally) => ({ motifId: tally.motifId, frameBucket: tally.frameBucket, grain: tally.grain, day: tally.day, sessions: tally.sessions.size, clusters: ONE_OPERATOR, ok: tally.ok, failed: tally.failed, tokens: tally.tokens, durationMs: tally.durationMs, exposed: tally.exposed }));
250
+ .map((tally) => ({ motifId: tally.motifId, frameBucket: tally.frameBucket, grain: tally.grain, day: tally.day, sessions: tally.sessions.size, clusters: ONE_OPERATOR, ok: tally.ok, failed: tally.failed, tokens: tally.recorded.size === 0 ? null : tally.tokens, tokenSessions: tally.recorded.size, tokenPrompts: tally.recordedUnits, durationMs: tally.durationMs, exposed: tally.exposed }));
171
251
  return { cells, motifs: [...motifs.values()].sort((a, b) => compare(a.motifId, b.motifId)) };
172
252
  };
173
253
  export const serializeCells = (cells) => cells.map((cell) => `${JSON.stringify(cell)}\n`).join('');
@@ -176,6 +256,6 @@ const errorRowsRead = (errors) => errors.map(({ session, agent, toolUseId, promp
176
256
  export const poolOf = (set, errors) => ({
177
257
  cells: set.cells,
178
258
  motifs: new Map(set.motifs.map((motif) => [motif.motifId, motif])),
179
- version: contentHash([EXTRACTOR_VERSION, METHODS, CALL_SHAPE_METHOD, set.cells.map(cellIdOf), errorRowsRead(errors)]),
259
+ version: contentHash([EXTRACTOR_VERSION, METHODS, CALL_SHAPE_METHOD, RECOVERY_EDGE_METHOD, set.cells.map(cellIdOf), errorRowsRead(errors)]),
180
260
  lastDay: set.cells.reduce((last, cell) => (cell.day !== UNKNOWN && (last === null || cell.day > last) ? cell.day : last), null),
181
261
  });