@manyoars/oars 0.2.0 → 0.3.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.
Files changed (241) hide show
  1. package/README.md +132 -28
  2. package/dist/api.js +1 -1
  3. package/dist/bin.js +6 -29
  4. package/dist/cli.d.ts +6 -3
  5. package/dist/cli.js +367 -88
  6. package/dist/commands/keys.d.ts +31 -0
  7. package/dist/commands/keys.js +65 -0
  8. package/dist/commands/login.js +2 -2
  9. package/dist/errors.d.ts +13 -13
  10. package/dist/errors.js +28 -11
  11. package/dist/gate/checks.d.ts +68 -0
  12. package/dist/gate/checks.js +584 -0
  13. package/dist/gate/command.d.ts +14 -0
  14. package/dist/gate/command.js +61 -0
  15. package/dist/gate/hook-fill-main.d.ts +1 -0
  16. package/dist/gate/hook-fill-main.js +16 -0
  17. package/dist/gate/hook-fill.d.ts +38 -0
  18. package/dist/gate/hook-fill.js +94 -0
  19. package/dist/gate/hook-main.d.ts +1 -0
  20. package/dist/gate/hook-main.js +35 -0
  21. package/dist/gate/hook-process.d.ts +14 -0
  22. package/dist/gate/hook-process.js +64 -0
  23. package/dist/gate/hook.d.ts +46 -0
  24. package/dist/gate/hook.js +256 -0
  25. package/dist/gate/install.d.ts +25 -0
  26. package/dist/gate/install.js +126 -0
  27. package/dist/gate/local-bin.d.ts +8 -0
  28. package/dist/gate/local-bin.js +36 -0
  29. package/dist/gate/notes.d.ts +19 -0
  30. package/dist/gate/notes.js +27 -0
  31. package/dist/gate/novelty.d.ts +35 -0
  32. package/dist/gate/novelty.js +99 -0
  33. package/dist/gate/policy.d.ts +25 -0
  34. package/dist/gate/policy.js +73 -0
  35. package/dist/gate/record.d.ts +120 -0
  36. package/dist/gate/record.js +125 -0
  37. package/dist/gate/registry-state.d.ts +10 -0
  38. package/dist/gate/registry-state.js +30 -0
  39. package/dist/gate/registry.d.ts +56 -0
  40. package/dist/gate/registry.js +94 -0
  41. package/dist/gate/replay.d.ts +20 -0
  42. package/dist/gate/replay.js +257 -0
  43. package/dist/gate/report.d.ts +50 -0
  44. package/dist/gate/report.js +89 -0
  45. package/dist/gate/scope.d.ts +7 -0
  46. package/dist/gate/scope.js +27 -0
  47. package/dist/gate/transcripts.d.ts +68 -0
  48. package/dist/gate/transcripts.js +232 -0
  49. package/dist/hook-bin.d.ts +1 -0
  50. package/dist/hook-bin.js +25 -0
  51. package/dist/issuer.d.ts +7 -1
  52. package/dist/issuer.js +18 -1
  53. package/dist/main.d.ts +1 -0
  54. package/dist/main.js +64 -0
  55. package/dist/oidc.js +1 -1
  56. package/dist/sessions/actions.d.ts +35 -0
  57. package/dist/sessions/actions.js +351 -0
  58. package/dist/sessions/allow.d.ts +14 -0
  59. package/dist/sessions/allow.js +51 -0
  60. package/dist/sessions/archive.d.ts +11 -0
  61. package/dist/sessions/archive.js +38 -0
  62. package/dist/sessions/audit.d.ts +42 -0
  63. package/dist/sessions/audit.js +104 -0
  64. package/dist/sessions/baseline.d.ts +62 -0
  65. package/dist/sessions/baseline.js +188 -0
  66. package/dist/sessions/capture-runtime.d.ts +10 -0
  67. package/dist/sessions/capture-runtime.js +12 -0
  68. package/dist/sessions/cell-increments.d.ts +43 -0
  69. package/dist/sessions/cell-increments.js +209 -0
  70. package/dist/sessions/cells.d.ts +79 -0
  71. package/dist/sessions/cells.js +181 -0
  72. package/dist/sessions/codex.d.ts +53 -0
  73. package/dist/sessions/codex.js +679 -0
  74. package/dist/sessions/command.d.ts +117 -0
  75. package/dist/sessions/command.js +373 -0
  76. package/dist/sessions/copilot.d.ts +40 -0
  77. package/dist/sessions/copilot.js +432 -0
  78. package/dist/sessions/cursor.d.ts +24 -0
  79. package/dist/sessions/cursor.js +324 -0
  80. package/dist/sessions/derive.d.ts +57 -0
  81. package/dist/sessions/derive.js +320 -0
  82. package/dist/sessions/exclusion.d.ts +81 -0
  83. package/dist/sessions/exclusion.js +353 -0
  84. package/dist/sessions/extract.d.ts +179 -0
  85. package/dist/sessions/extract.js +1774 -0
  86. package/dist/sessions/fold.d.ts +17 -0
  87. package/dist/sessions/fold.js +71 -0
  88. package/dist/sessions/frame.d.ts +24 -0
  89. package/dist/sessions/frame.js +95 -0
  90. package/dist/sessions/gemini.d.ts +37 -0
  91. package/dist/sessions/gemini.js +613 -0
  92. package/dist/sessions/gold-arms.d.ts +67 -0
  93. package/dist/sessions/gold-arms.js +172 -0
  94. package/dist/sessions/gold.d.ts +197 -0
  95. package/dist/sessions/gold.js +463 -0
  96. package/dist/sessions/graph.d.ts +12 -0
  97. package/dist/sessions/graph.js +34 -0
  98. package/dist/sessions/grok.d.ts +36 -0
  99. package/dist/sessions/grok.js +421 -0
  100. package/dist/sessions/hook-calls.d.ts +3 -0
  101. package/dist/sessions/hook-calls.js +47 -0
  102. package/dist/sessions/hook-index.d.ts +53 -0
  103. package/dist/sessions/hook-index.js +157 -0
  104. package/dist/sessions/hooks.d.ts +13 -0
  105. package/dist/sessions/hooks.js +170 -0
  106. package/dist/sessions/identity.d.ts +22 -0
  107. package/dist/sessions/identity.js +101 -0
  108. package/dist/sessions/install.d.ts +54 -0
  109. package/dist/sessions/install.js +201 -0
  110. package/dist/sessions/lineage.d.ts +19 -0
  111. package/dist/sessions/lineage.js +118 -0
  112. package/dist/sessions/mcp.d.ts +6 -0
  113. package/dist/sessions/mcp.js +153 -0
  114. package/dist/sessions/merkle.d.ts +7 -0
  115. package/dist/sessions/merkle.js +28 -0
  116. package/dist/sessions/motif.d.ts +43 -0
  117. package/dist/sessions/motif.js +59 -0
  118. package/dist/sessions/package-resolve.d.ts +30 -0
  119. package/dist/sessions/package-resolve.js +183 -0
  120. package/dist/sessions/pending.d.ts +34 -0
  121. package/dist/sessions/pending.js +142 -0
  122. package/dist/sessions/pool-command.d.ts +44 -0
  123. package/dist/sessions/pool-command.js +542 -0
  124. package/dist/sessions/pool-delta.d.ts +54 -0
  125. package/dist/sessions/pool-delta.js +190 -0
  126. package/dist/sessions/pool-outbox.d.ts +63 -0
  127. package/dist/sessions/pool-outbox.js +260 -0
  128. package/dist/sessions/pool-print.d.ts +26 -0
  129. package/dist/sessions/pool-print.js +68 -0
  130. package/dist/sessions/pool-wire.d.ts +225 -0
  131. package/dist/sessions/pool-wire.js +620 -0
  132. package/dist/sessions/pool.d.ts +32 -0
  133. package/dist/sessions/pool.js +164 -0
  134. package/dist/sessions/projection.d.ts +75 -0
  135. package/dist/sessions/projection.js +165 -0
  136. package/dist/sessions/public-vocabulary.d.ts +73 -0
  137. package/dist/sessions/public-vocabulary.js +302 -0
  138. package/dist/sessions/query.d.ts +111 -0
  139. package/dist/sessions/query.js +246 -0
  140. package/dist/sessions/read-adoption.d.ts +2 -0
  141. package/dist/sessions/read-adoption.js +27 -0
  142. package/dist/sessions/read-call-shape.d.ts +2 -0
  143. package/dist/sessions/read-call-shape.js +25 -0
  144. package/dist/sessions/read-change.d.ts +2 -0
  145. package/dist/sessions/read-change.js +30 -0
  146. package/dist/sessions/read-cost.d.ts +4 -0
  147. package/dist/sessions/read-cost.js +37 -0
  148. package/dist/sessions/read-day-move.d.ts +3 -0
  149. package/dist/sessions/read-day-move.js +65 -0
  150. package/dist/sessions/read-fit.d.ts +7 -0
  151. package/dist/sessions/read-fit.js +41 -0
  152. package/dist/sessions/read-interaction.d.ts +5 -0
  153. package/dist/sessions/read-interaction.js +62 -0
  154. package/dist/sessions/read-lead-lag.d.ts +15 -0
  155. package/dist/sessions/read-lead-lag.js +81 -0
  156. package/dist/sessions/read-live.d.ts +6 -0
  157. package/dist/sessions/read-live.js +38 -0
  158. package/dist/sessions/read-method.d.ts +2 -0
  159. package/dist/sessions/read-method.js +29 -0
  160. package/dist/sessions/read-next-step.d.ts +2 -0
  161. package/dist/sessions/read-next-step.js +35 -0
  162. package/dist/sessions/read-order.d.ts +54 -0
  163. package/dist/sessions/read-order.js +62 -0
  164. package/dist/sessions/read-readable.d.ts +8 -0
  165. package/dist/sessions/read-readable.js +24 -0
  166. package/dist/sessions/read-recovery.d.ts +20 -0
  167. package/dist/sessions/read-recovery.js +97 -0
  168. package/dist/sessions/read-rows.d.ts +13 -0
  169. package/dist/sessions/read-rows.js +47 -0
  170. package/dist/sessions/read-substitution.d.ts +5 -0
  171. package/dist/sessions/read-substitution.js +51 -0
  172. package/dist/sessions/read-support.d.ts +48 -0
  173. package/dist/sessions/read-support.js +73 -0
  174. package/dist/sessions/read-tool-day.d.ts +2 -0
  175. package/dist/sessions/read-tool-day.js +26 -0
  176. package/dist/sessions/read-version-boundary.d.ts +31 -0
  177. package/dist/sessions/read-version-boundary.js +95 -0
  178. package/dist/sessions/read.d.ts +79 -0
  179. package/dist/sessions/read.js +84 -0
  180. package/dist/sessions/recall-consequence.d.ts +45 -0
  181. package/dist/sessions/recall-consequence.js +179 -0
  182. package/dist/sessions/recall-object.d.ts +137 -0
  183. package/dist/sessions/recall-object.js +77 -0
  184. package/dist/sessions/recall-pool.d.ts +19 -0
  185. package/dist/sessions/recall-pool.js +96 -0
  186. package/dist/sessions/recall-push.d.ts +26 -0
  187. package/dist/sessions/recall-push.js +95 -0
  188. package/dist/sessions/recall-reads.d.ts +117 -0
  189. package/dist/sessions/recall-reads.js +410 -0
  190. package/dist/sessions/recall-render.d.ts +19 -0
  191. package/dist/sessions/recall-render.js +168 -0
  192. package/dist/sessions/redact.d.ts +6 -0
  193. package/dist/sessions/redact.js +54 -0
  194. package/dist/sessions/repo-root.d.ts +10 -0
  195. package/dist/sessions/repo-root.js +101 -0
  196. package/dist/sessions/schema.d.ts +644 -0
  197. package/dist/sessions/schema.js +148 -0
  198. package/dist/sessions/session-shape.d.ts +73 -0
  199. package/dist/sessions/session-shape.js +342 -0
  200. package/dist/sessions/sources.d.ts +50 -0
  201. package/dist/sessions/sources.js +159 -0
  202. package/dist/sessions/steps.d.ts +105 -0
  203. package/dist/sessions/steps.js +250 -0
  204. package/dist/sessions/store.d.ts +297 -0
  205. package/dist/sessions/store.js +1478 -0
  206. package/dist/sessions/strata.d.ts +26 -0
  207. package/dist/sessions/strata.js +77 -0
  208. package/dist/sessions/survival.d.ts +25 -0
  209. package/dist/sessions/survival.js +230 -0
  210. package/dist/sessions/verify.d.ts +53 -0
  211. package/dist/sessions/verify.js +286 -0
  212. package/dist/sessions/vocab-load.d.ts +26 -0
  213. package/dist/sessions/vocab-load.js +61 -0
  214. package/dist/sessions/vscode-copilot.d.ts +38 -0
  215. package/dist/sessions/vscode-copilot.js +477 -0
  216. package/package.json +4 -5
  217. package/vocab/crates.json +1 -0
  218. package/vocab/go.json +1 -0
  219. package/vocab/harness.json +1 -0
  220. package/vocab/homebrew.json +1 -0
  221. package/vocab/manifest.json +1 -0
  222. package/vocab/maven.json +1 -0
  223. package/vocab/models.json +1 -0
  224. package/vocab/npm.json +1 -0
  225. package/vocab/programs.json +1 -0
  226. package/vocab/pypi.json +1 -0
  227. package/vocab/rubygems.json +1 -0
  228. package/dist/commands/deploy-input.d.ts +0 -38
  229. package/dist/commands/deploy-input.js +0 -150
  230. package/dist/commands/deploy.d.ts +0 -22
  231. package/dist/commands/deploy.js +0 -124
  232. package/dist/commands/status.d.ts +0 -7
  233. package/dist/commands/status.js +0 -18
  234. package/dist/commands/templates.d.ts +0 -11
  235. package/dist/commands/templates.js +0 -32
  236. package/dist/commands/test-charge.d.ts +0 -11
  237. package/dist/commands/test-charge.js +0 -34
  238. package/dist/journal.d.ts +0 -26
  239. package/dist/journal.js +0 -58
  240. package/parity/deploy-plans/paid-api.json +0 -34
  241. package/parity/deploy-plans/per-task-agent.json +0 -34
@@ -0,0 +1,620 @@
1
+ // What crosses the wire between `oars pool` and the pool server (recall spec Sections 2.5, 4, and 5). A skeleton is one session in closed vocabulary: the records a cell is
2
+ // computed from, with every class A field left out, so any later derivation can run over old data while nothing private leaves. The client builds a skeleton and checks it before
3
+ // sending, and the server parses it again, field by field, because a live URL takes anything from anyone. This file imports nothing from the file system: the Worker bundles it.
4
+ import { ACTION_TABLES, CANONICAL_ACTIONS, mcpOf } from './actions.js';
5
+ import { CUSTOM_AGENT, FRAMEWORKS, isPublicMcpTool, isRegistryVersion, LANGUAGES, LITERAL, PRIVATE, publicEffort, publicModel, publicVersion, PUBLIC_FLAGS, PUBLIC_HOSTS, PUBLIC_MCP_SERVERS, PUBLIC_PACKAGES, PUBLIC_PROGRAMS, PUBLIC_SKILLS, projectedAgentType, PUBLIC_SUBCOMMANDS, REPO_SIZES, VOCAB_VERSION } from './public-vocabulary.js';
6
+ import { projectLanguage, projectLanguageShare } from './strata.js';
7
+ import { LANGUAGE_SHARES } from './schema.js';
8
+ export const SKELETON_FORMAT = 2;
9
+ export const BUNDLE_FORMAT = 1;
10
+ /** One constant: the pool serves only the vocabulary version the client holds. */
11
+ export { VOCAB_VERSION };
12
+ /**
13
+ * Section 7's edges. A bucket is the count of edges a value reaches, so bucket 0 is under the first edge and the last is the first edge's doubling past the cap. `tokens`: under 1k, then
14
+ * doubling to 1.024M and over. `ms`: under 100 ms, then doubling to 6,553.6 s and over. `seconds`: under 1 s, then doubling to 65,536 s and over. `count`: 0, 1, 2, 3-4, 5-8, 9-16, 17-32, over 32.
15
+ */
16
+ const EDGES = {
17
+ tokens: Array.from({ length: 11 }, (_, power) => 1000 * 2 ** power),
18
+ ms: Array.from({ length: 17 }, (_, power) => 100 * 2 ** power),
19
+ seconds: Array.from({ length: 17 }, (_, power) => 2 ** power),
20
+ count: [1, 2, 3, 5, 9, 17, 33],
21
+ // A call count of a session: the `count` edges, then each edge doubling its width up to over 4,096 (spec Section 7), because a long session passes 32 routinely.
22
+ calls: [1, 2, 3, 5, 9, 17, 33, 65, 129, 257, 513, 1025, 2049, 4097],
23
+ };
24
+ /** The bucket a value falls in. */
25
+ export const bucketOf = (kind, value) => EDGES[kind].filter((edge) => value >= edge).length;
26
+ /** A stored `explore` count as it pools: its `calls` bucket, or null for a value no count holds (a record captured before the strata has none). The exact count never leaves. */
27
+ export const projectExplore = (stored) => (stored !== undefined && Number.isSafeInteger(stored) && stored >= 0 ? bucketOf('calls', stored) : null);
28
+ /** The lowest value of a bucket, which is what a derivation counts a bucketed value as: it never reads above what the sender held. */
29
+ export const floorOf = (kind, bucket) => EDGES[kind][bucket - 1] ?? 0;
30
+ /** W's minimum for a timing print (deposit-weighting spec, layer 5): fewer timed calls than this say too little to tell sessions apart. */
31
+ export const MIN_TIMED_CALLS = 50;
32
+ /** A session has ended once no transcript of it has gained a line for this long (`session_idle_hours`, spec Section 7). */
33
+ export const SESSION_IDLE_HOURS = 6;
34
+ const HOUR_MS = 60 * 60 * 1000;
35
+ /**
36
+ * Whether a session's batch may be built at `now`: no transcript of it, main or subagent, has gained a line for `SESSION_IDLE_HOURS`. A transcript gains a line when capture sees it change, which
37
+ * stamps a new `rev` in epoch milliseconds, but a transcript can grow after the last capture, so `written` (the newest write to any raw file of the session, 0 when none is known) counts too: the
38
+ * session was last active at the later of the two. A subagent that died without an end marker holds its session back at most that long.
39
+ */
40
+ export const hasEnded = (records, now, written) => now - Math.max(written, ...records.transcripts.map(({ session }) => session.rev)) >= SESSION_IDLE_HOURS * HOUR_MS;
41
+ const groupBy = (items, keyOf) => {
42
+ const groups = new Map();
43
+ for (const item of items) {
44
+ const key = keyOf(item);
45
+ const group = groups.get(key);
46
+ if (group === undefined)
47
+ groups.set(key, [item]);
48
+ else
49
+ group.push(item);
50
+ }
51
+ return groups;
52
+ };
53
+ /** Every main session of a store, with its records. */
54
+ export const sessionsOf = (store, roots) => {
55
+ // Grouped once: a fleet's store holds thousands of sessions over hundreds of thousands of rows, and a scan per session would be quadratic.
56
+ const prompts = groupBy(store.prompts, (prompt) => prompt.session);
57
+ const tools = groupBy(store.tools, (tool) => tool.session);
58
+ const subagents = groupBy(store.subagents, (subagent) => subagent.parent);
59
+ const recordsOf = (session) => ({ session, prompts: prompts.get(session.session) ?? [], tools: tools.get(session.session) ?? [], root: roots.get(session.session) ?? null });
60
+ // A subagent whose main transcript the store does not hold is in no group: it cannot be folded, and counting it alone would raise `n`.
61
+ return store.sessions.map((main) => ({ main, transcripts: [main, ...[...(subagents.get(main.session) ?? [])].sort((a, b) => (a.session < b.session ? -1 : 1))].map(recordsOf) }));
62
+ };
63
+ const SHORT_FLAG = /^-[A-Za-z]$/;
64
+ /** The flags the wire has a row for: the curated list and a single letter. */
65
+ const isWireFlag = (flag) => PUBLIC_FLAGS.has(flag) || SHORT_FLAG.test(flag);
66
+ const PRIVATE_COMMAND = { program: PRIVATE, sub: null, flags: [] };
67
+ /**
68
+ * A projected command as the server's parser reads it. The client's snapshot lists more programs than the curated list the server holds, and a name the server has no row for
69
+ * does not go on the wire (Section 1.2): such a program is `private`, and a subcommand or flag outside the curated lists is dropped, as the projection drops an unlisted one.
70
+ */
71
+ const wireCommand = ({ program, subcommand, flags }) => {
72
+ if (!PUBLIC_PROGRAMS.has(program))
73
+ return PRIVATE_COMMAND;
74
+ return { program, sub: subcommand !== null && PUBLIC_SUBCOMMANDS.get(program)?.has(subcommand) === true ? subcommand : null, flags: flags.filter(isWireFlag) };
75
+ };
76
+ /** The wire's one spelling of "not admitted": a name the server's list does not hold goes as `private`, whatever the local store spells it. */
77
+ const listed = (allowed, name) => (allowed.has(name) ? name : PRIVATE);
78
+ /** A pull in the wire's spelling: its package name is a list member or `private`. A version beside a masked name could identify it, so it goes as `<literal>` (null stays null), the rule capture applies. */
79
+ const wirePull = (pull) => {
80
+ const name = listed(PUBLIC_PACKAGES, pull.name);
81
+ return { ...pull, name, version: name === PRIVATE && pull.version !== null ? LITERAL : pull.version };
82
+ };
83
+ /** A non-shell step's identity in the wire's spelling; a shell step's comes from `wireCommand`. */
84
+ const wireIdentity = (identity, harness) => {
85
+ if (identity === null || 'program' in identity)
86
+ return identity;
87
+ if ('server' in identity) {
88
+ const server = listed(PUBLIC_MCP_SERVERS, identity.server);
89
+ // A tool beside a masked server has no list to be checked against, so it goes as the literal. Beside a listed one it goes as itself only where the server's list still holds it
90
+ // and the harness's spelling delimits the server (the parser's rule); a tool an older list held, or one stored before that rule, goes as the literal rather than refusing the session.
91
+ const named = server !== PRIVATE && ACTION_TABLES[harness].toolDelimited && isPublicMcpTool(server, identity.tool);
92
+ return { server, tool: named ? identity.tool : LITERAL };
93
+ }
94
+ if ('skill' in identity)
95
+ return { skill: listed(PUBLIC_SKILLS, identity.skill) };
96
+ return { host: identity.host === null ? null : listed(PUBLIC_HOSTS, identity.host) };
97
+ };
98
+ /**
99
+ * A stored tool's steps as the wire holds them: each shell step's identity is replaced by the projected command, and every name the server has no row for (a native name, MCP server,
100
+ * skill, host, or pull name) reads `private`. A `private` program holds no pull, so a stored step whose public program has since left the list carries none. The one place a deposit and a local read take it from.
101
+ */
102
+ export const projectedSteps = (tool, read, harness) => {
103
+ const commands = read(tool);
104
+ return (tool.steps ?? []).map((step, at) => {
105
+ const native = admittedNative(harness, step.native) ? step.native : PRIVATE;
106
+ if (step.action !== 'shell.exec')
107
+ return { ...step, native, identity: wireIdentity(step.identity, harness) };
108
+ const command = wireCommand(commands[at] ?? { program: PRIVATE, subcommand: null, flags: [] });
109
+ const pulls = step.identity !== null && 'program' in step.identity ? step.identity.pulls : undefined;
110
+ return { ...step, native, identity: pulls === undefined || command.program === PRIVATE ? command : { ...command, pulls: pulls.map(wirePull) } };
111
+ });
112
+ };
113
+ /** A transcript's calls in the order they ran, which is the order a skeleton holds them in. */
114
+ const byCall = (tools) => [...tools].sort((a, b) => a.n - b.n);
115
+ /** The step of a call that carries the runs it spawned: its first spawn step, else its first step. A call with no step carries none. */
116
+ const carrier = (steps) => Math.max(0, steps.findIndex(({ action }) => action === 'agent.spawn'));
117
+ /**
118
+ * A stored frame with its stack's language shares: each stored share is read through its projection, so a share outside the list is dropped and its language stays in `languages`. A stored
119
+ * language outside the pinned list is `other`, and `other` holds the first share a record gives it. A record captured before the strata holds none.
120
+ */
121
+ const wireFrame = (frame, strata) => {
122
+ const shares = new Map();
123
+ for (const { language, share } of strata?.languages ?? []) {
124
+ const held = projectLanguageShare(share);
125
+ const name = projectLanguage(language);
126
+ if (held !== null && !shares.has(name))
127
+ shares.set(name, held);
128
+ }
129
+ return { ...frame, stack: { ...frame.stack, languageShares: [...shares].map(([language, languageShare]) => ({ language, languageShare })) } };
130
+ };
131
+ const transcriptOf = ({ session, prompts, tools, root }, spawners, spawned, agentType, read) => {
132
+ if (session.frame === undefined || root === null)
133
+ return null;
134
+ const harness = session.harness ?? 'claude-code';
135
+ const framedPrompts = [];
136
+ const promptAt = new Map();
137
+ for (const { index, cls, errors, wallSec, usage, frame, strata } of [...prompts].sort((a, b) => a.index - b.index)) {
138
+ if (frame === undefined)
139
+ return null;
140
+ promptAt.set(index, framedPrompts.length);
141
+ framedPrompts.push({
142
+ cls,
143
+ errors: bucketOf('count', errors),
144
+ wallSec: wallSec === null ? null : bucketOf('seconds', wallSec),
145
+ usage: { input: bucketOf('tokens', usage.input), cacheWrite: bucketOf('tokens', usage.cacheWrite), cacheRead: bucketOf('tokens', usage.cacheRead), output: bucketOf('tokens', usage.output), thinking: bucketOf('tokens', usage.thinking) },
146
+ explore: projectExplore(strata?.explore),
147
+ frame: wireFrame(frame, strata),
148
+ });
149
+ }
150
+ const steppedTools = [];
151
+ // The exact tokens of every step, by call and step, which the `remainingTokens` buckets below are computed from: a bucket of a sum is not a sum of buckets.
152
+ const exact = [];
153
+ let unrecorded = 0;
154
+ for (const tool of byCall(tools)) {
155
+ if (tool.steps === undefined)
156
+ return null;
157
+ const { side, prompt, status, durationMs } = tool;
158
+ // A subagent's call names the prompt that spawned it, which its own transcript holds no record of: such a number takes the next position past the prompts, one per distinct number, so
159
+ // the calls that shared a prompt still share one, and no position reads as a prompt.
160
+ if (prompt !== null && !promptAt.has(prompt))
161
+ promptAt.set(prompt, framedPrompts.length + unrecorded++);
162
+ const promptPosition = prompt === null ? null : (promptAt.get(prompt) ?? null);
163
+ const projected = projectedSteps(tool, read, harness);
164
+ exact.push(projected.map(({ cost }) => cost.tokens ?? 0));
165
+ const steps = projected.map(({ action, native, identity, status: reported, cost, parent }) => ({
166
+ action,
167
+ native,
168
+ identity,
169
+ status: reported,
170
+ tokens: cost.tokens === null ? null : bucketOf('tokens', cost.tokens),
171
+ ms: cost.durationMs === null ? null : bucketOf('ms', cost.durationMs),
172
+ remainingTokens: 0,
173
+ agentType,
174
+ parent: parent === null || spawners === null ? null : (spawners.get(parent) ?? null),
175
+ }));
176
+ steppedTools.push({ side, prompt: promptPosition, status, durationMs, steps });
177
+ }
178
+ // A step's remaining tokens are those of the steps after it in its own run; a main-run step also counts the subagent runs its later steps spawned (a run belongs to the spawning call's carrier step).
179
+ let later = 0;
180
+ for (let call = steppedTools.length - 1; call >= 0; call -= 1) {
181
+ const steps = steppedTools[call]?.steps ?? [];
182
+ const held = carrier(steps);
183
+ for (let at = steps.length - 1; at >= 0; at -= 1) {
184
+ const step = steps[at];
185
+ if (step === undefined)
186
+ continue;
187
+ step.remainingTokens = bucketOf('tokens', later);
188
+ later += (exact[call]?.[at] ?? 0) + (at === held ? (spawned.get(call) ?? 0) : 0);
189
+ }
190
+ }
191
+ return { merkleRoot: root.merkleRoot, leaves: root.leaves, harness, frame: wireFrame(session.frame, session.strata), prompts: framedPrompts, tools: steppedTools };
192
+ };
193
+ /**
194
+ * The skeleton of a session; null when any record of it was captured before steps and frames, or a transcript of it has no root, which holds no closed reading to send. The batch holds the main
195
+ * transcript's run first, then each subagent's as its own run; its `id` is the main session's, and its one `timingPrint` is over the main transcript's calls alone.
196
+ */
197
+ export const skeletonOf = (records, id, read, print, deltas = []) => {
198
+ const mainTools = byCall(records.transcripts[0]?.tools ?? []);
199
+ // A subagent's step names the call that spawned it by that call's `n` in the main run, which the skeleton holds as the call's position.
200
+ const spawners = new Map(mainTools.map(({ n }, position) => [n, position]));
201
+ const spawnedBy = (entry) => entry.session.spawnedIn === records.main.session;
202
+ // The exact tokens of the runs each main-run call spawned. A run a subagent spawned names a call of that other transcript, so it joins no main-run step.
203
+ const spawned = new Map();
204
+ for (const entry of records.transcripts.slice(1)) {
205
+ if (!spawnedBy(entry))
206
+ continue;
207
+ for (const tool of entry.tools) {
208
+ for (const { cost, parent } of tool.steps ?? []) {
209
+ const position = parent === null ? undefined : spawners.get(parent);
210
+ if (position !== undefined)
211
+ spawned.set(position, (spawned.get(position) ?? 0) + (cost.tokens ?? 0));
212
+ }
213
+ }
214
+ }
215
+ const transcripts = [];
216
+ for (const [index, entry] of records.transcripts.entries()) {
217
+ // Only a call of the main run has a position here: a step of a subagent a subagent spawned names a call of that other transcript, so it sends no link rather than a false one.
218
+ const transcript = transcriptOf(entry, index > 0 && spawnedBy(entry) ? spawners : null, index === 0 ? spawned : new Map(), index === 0 ? null : projectedAgentType(entry.session.agentType ?? CUSTOM_AGENT), read);
219
+ if (transcript === null)
220
+ return null;
221
+ transcripts.push(transcript);
222
+ }
223
+ const durations = mainTools.flatMap(({ durationMs }) => (durationMs === null ? [] : [durationMs]));
224
+ return { format: SKELETON_FORMAT, id, transcripts, timingPrint: durations.length < MIN_TIMED_CALLS ? null : print(durations), deltas: [...deltas] };
225
+ };
226
+ /** A harness version a bundle can be asked for: public, so the request carries nothing private. The admitted list replaces this shape rule when it ships. */
227
+ export const isBundleVersion = (version) => version !== LITERAL && publicVersion(version) === version;
228
+ /** Whether `name` is a harness the schema names. */
229
+ export const isHarness = (name) => HARNESSES.some((harness) => harness === name);
230
+ class Refused extends Error {
231
+ field;
232
+ reason;
233
+ constructor(field, reason) {
234
+ super(`${field}: ${reason}`);
235
+ this.field = field;
236
+ this.reason = reason;
237
+ }
238
+ }
239
+ const isFields = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
240
+ const under = (at, key) => (at === '' ? key : `${at}.${key}`);
241
+ /** The object at `at`, holding every field of `known` and, of the rest, only `optional`. Nothing unnamed passes, which is what keeps a class A field out. */
242
+ const fieldsOf = (value, at, known, optional = []) => {
243
+ if (!isFields(value))
244
+ throw new Refused(at === '' ? 'skeleton' : at, 'not an object');
245
+ for (const key of Object.keys(value))
246
+ if (!known.includes(key) && !optional.includes(key))
247
+ throw new Refused(under(at, key), 'not a field of a skeleton');
248
+ for (const key of known)
249
+ if (!Object.hasOwn(value, key))
250
+ throw new Refused(under(at, key), 'missing');
251
+ return value;
252
+ };
253
+ const listOf = (value, at, item) => {
254
+ if (!Array.isArray(value))
255
+ throw new Refused(at, 'not a list');
256
+ const entries = value;
257
+ return entries.map((entry, index) => item(entry, `${at}[${String(index)}]`));
258
+ };
259
+ const textOf = (value, at) => {
260
+ if (typeof value !== 'string')
261
+ throw new Refused(at, 'not text');
262
+ return value;
263
+ };
264
+ /** Whole and safe: a depositor's value is added into cells every account is served, and one huge number would turn a sum into Infinity, which JSON writes as null. */
265
+ const countOf = (value, at) => {
266
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0)
267
+ throw new Refused(at, 'not a count');
268
+ return value;
269
+ };
270
+ const orNull = (value, at, item) => (value === null ? null : item(value, at));
271
+ const booleanOf = (value, at) => {
272
+ if (typeof value !== 'boolean')
273
+ throw new Refused(at, 'not a boolean');
274
+ return value;
275
+ };
276
+ /** A value from a closed list; the list narrows the type, so no caller needs a cast. */
277
+ const oneOf = (list, value, at) => {
278
+ const found = list.find((entry) => entry === value);
279
+ if (found === undefined)
280
+ throw new Refused(at, 'outside the vocabulary');
281
+ return found;
282
+ };
283
+ const inSet = (allowed, value, at) => {
284
+ const name = textOf(value, at);
285
+ if (name !== PRIVATE && !allowed.has(name))
286
+ throw new Refused(at, 'outside the vocabulary');
287
+ return name;
288
+ };
289
+ const exactlyIn = (allowed) => (value, at) => {
290
+ if (typeof value !== 'string' || !allowed.has(value))
291
+ throw new Refused(at, 'outside the vocabulary');
292
+ return value;
293
+ };
294
+ /** `publicModel` and its like hand back what is public and the literal for what is not, so only a value they return unchanged is in the vocabulary. */
295
+ const heldTo = (isHeld, value, at) => {
296
+ if (value === null)
297
+ return null;
298
+ const name = textOf(value, at);
299
+ if (isHeld(name) !== name)
300
+ throw new Refused(at, 'outside the vocabulary');
301
+ return name;
302
+ };
303
+ // `satisfies` checks every entry is a `Harness` but not that every `Harness` is listed; this refuses a list that leaves one out at compile time.
304
+ const allHarnesses = (list) => list;
305
+ export const HARNESSES = allHarnesses(['claude-code', 'cursor', 'codex', 'copilot', 'gemini', 'grok', 'vscode-copilot']);
306
+ const PROMPT_CLASSES = ['no-tools', 'orchestration', 'research', 'edit-verify', 'edit-only', 'explore-only', 'shell-only', 'mixed'];
307
+ const STATUSES = ['ok', 'error', 'none'];
308
+ const PLATFORMS = new Set(['aix', 'android', 'cygwin', 'darwin', 'freebsd', 'haiku', 'linux', 'netbsd', 'openbsd', 'sunos', 'win32']);
309
+ const SHELLS = new Set(Object.values(ACTION_TABLES).flatMap((table) => Object.values(table.shells)));
310
+ /** The registries a package comes from; a pull may also come from a skill, plugin, or MCP registry. */
311
+ export const PACKAGE_REGISTRIES = ['npm', 'pypi', 'crates', 'go', 'rubygems', 'homebrew'];
312
+ const REGISTRIES = [...PACKAGE_REGISTRIES, 'skill', 'plugin', 'mcp'];
313
+ const DAY = /^\d{4}-\d{2}-\d{2}$/;
314
+ const ID = /^[0-9a-f]{16}$/;
315
+ /** A real date, not just its shape. `Date.parse` rolls 2026-02-31 over to March instead of failing, so the day must also survive the trip back to text. */
316
+ const isCalendarDay = (text) => {
317
+ if (!DAY.test(text))
318
+ return false;
319
+ const parsed = Date.parse(text);
320
+ return !Number.isNaN(parsed) && new Date(parsed).toISOString().slice(0, 10) === text;
321
+ };
322
+ /** The languages a share can name: the pinned list and `other`, which `projectLanguage` gives any language off the list. The frame's own `languages` stay closed to the list. */
323
+ const SHARE_LANGUAGES = new Set([...LANGUAGES, 'other']);
324
+ const parseLanguageShare = (value, at) => {
325
+ const raw = fieldsOf(value, at, ['language', 'languageShare']);
326
+ return { language: exactlyIn(SHARE_LANGUAGES)(raw.language, under(at, 'language')), languageShare: oneOf(LANGUAGE_SHARES, raw.languageShare, under(at, 'languageShare')) };
327
+ };
328
+ const parseFrame = (value, at) => {
329
+ const raw = fieldsOf(value, at, ['harness', 'harnessVersion', 'model', 'effort', 'day', 'platform', 'shell', 'stack']);
330
+ const stackAt = under(at, 'stack');
331
+ const stack = fieldsOf(raw.stack, stackAt, ['languages', 'frameworks', 'size', 'languageShares']);
332
+ const day = orNull(raw.day, under(at, 'day'), (entry, entryAt) => {
333
+ const text = textOf(entry, entryAt);
334
+ if (!isCalendarDay(text))
335
+ throw new Refused(entryAt, 'not a UTC day');
336
+ return text;
337
+ });
338
+ return {
339
+ harness: oneOf(HARNESSES, raw.harness, under(at, 'harness')),
340
+ harnessVersion: heldTo(publicVersion, raw.harnessVersion, under(at, 'harnessVersion')),
341
+ model: heldTo(publicModel, raw.model, under(at, 'model')),
342
+ effort: heldTo(publicEffort, raw.effort, under(at, 'effort')),
343
+ day,
344
+ platform: exactlyIn(PLATFORMS)(raw.platform, under(at, 'platform')),
345
+ shell: orNull(raw.shell, under(at, 'shell'), exactlyIn(SHELLS)),
346
+ stack: {
347
+ languages: listOf(stack.languages, under(stackAt, 'languages'), exactlyIn(LANGUAGES)),
348
+ frameworks: listOf(stack.frameworks, under(stackAt, 'frameworks'), exactlyIn(FRAMEWORKS)),
349
+ size: orNull(stack.size, under(stackAt, 'size'), (entry, entryAt) => oneOf(REPO_SIZES, entry, entryAt)),
350
+ languageShares: listOf(stack.languageShares, under(stackAt, 'languageShares'), parseLanguageShare),
351
+ },
352
+ };
353
+ };
354
+ const bucketIn = (kind, value, at) => {
355
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 0 || value > EDGES[kind].length)
356
+ throw new Refused(at, 'not a bucket');
357
+ return value;
358
+ };
359
+ const parseUsage = (value, at) => {
360
+ const raw = fieldsOf(value, at, ['input', 'cacheWrite', 'cacheRead', 'output', 'thinking']);
361
+ return {
362
+ input: bucketIn('tokens', raw.input, under(at, 'input')),
363
+ cacheWrite: bucketIn('tokens', raw.cacheWrite, under(at, 'cacheWrite')),
364
+ cacheRead: bucketIn('tokens', raw.cacheRead, under(at, 'cacheRead')),
365
+ output: bucketIn('tokens', raw.output, under(at, 'output')),
366
+ thinking: bucketIn('tokens', raw.thinking, under(at, 'thinking')),
367
+ };
368
+ };
369
+ const parsePull = (value, at) => {
370
+ const raw = fieldsOf(value, at, ['registry', 'name', 'version']);
371
+ const name = inSet(PUBLIC_PACKAGES, raw.name, under(at, 'name'));
372
+ const version = orNull(raw.version, under(at, 'version'), (entry, entryAt) => {
373
+ const text = textOf(entry, entryAt);
374
+ if (text !== LITERAL && !isRegistryVersion(text))
375
+ throw new Refused(entryAt, 'outside the vocabulary');
376
+ // A version beside a masked name could identify it: capture masks the two together.
377
+ if (name === PRIVATE && text !== LITERAL)
378
+ throw new Refused(entryAt, 'a private name holds no version');
379
+ return text;
380
+ });
381
+ return { registry: oneOf(REGISTRIES, raw.registry, under(at, 'registry')), name, version };
382
+ };
383
+ const parseFlag = (value, at) => {
384
+ const flag = textOf(value, at);
385
+ if (!isWireFlag(flag))
386
+ throw new Refused(at, 'outside the vocabulary');
387
+ return flag;
388
+ };
389
+ const parseIdentity = (harness, action, value, at) => {
390
+ if (value === null)
391
+ return null;
392
+ if (action === 'shell.exec') {
393
+ const raw = fieldsOf(value, at, ['program', 'sub', 'flags'], ['pulls']);
394
+ // A program is a list member or `private`, and a `private` program holds no subcommand, flag, or pull.
395
+ const program = textOf(raw.program, under(at, 'program'));
396
+ if (program !== PRIVATE && !PUBLIC_PROGRAMS.has(program))
397
+ throw new Refused(under(at, 'program'), 'outside the vocabulary');
398
+ if (program === PRIVATE) {
399
+ if (!(Array.isArray(raw.flags) && raw.flags.length === 0))
400
+ throw new Refused(under(at, 'flags'), 'a private program holds no flags');
401
+ if (raw.pulls !== undefined)
402
+ throw new Refused(under(at, 'pulls'), 'a private program holds no pulls');
403
+ }
404
+ const subs = PUBLIC_SUBCOMMANDS.get(program) ?? new Set();
405
+ const sub = orNull(raw.sub, under(at, 'sub'), exactlyIn(subs));
406
+ const flags = listOf(raw.flags, under(at, 'flags'), parseFlag);
407
+ return raw.pulls === undefined ? { program, sub, flags } : { program, sub, flags, pulls: listOf(raw.pulls, under(at, 'pulls'), parsePull) };
408
+ }
409
+ if (action === 'mcp.call') {
410
+ const raw = fieldsOf(value, at, ['server', 'tool']);
411
+ const server = inSet(PUBLIC_MCP_SERVERS, raw.server, under(at, 'server'));
412
+ const tool = textOf(raw.tool, under(at, 'tool'));
413
+ // The same rule capture applies (`callIdentity`), so a name it would have masked is refused here rather than stored.
414
+ if (tool !== LITERAL && !(ACTION_TABLES[harness].toolDelimited && isPublicMcpTool(server, tool)))
415
+ throw new Refused(under(at, 'tool'), 'outside the vocabulary');
416
+ return { server, tool };
417
+ }
418
+ if (action === 'skill.run')
419
+ return { skill: inSet(PUBLIC_SKILLS, fieldsOf(value, at, ['skill']).skill, under(at, 'skill')) };
420
+ if (action === 'web.fetch' || action === 'web.search') {
421
+ const { host } = fieldsOf(value, at, ['host']);
422
+ return { host: orNull(host, under(at, 'host'), (entry, entryAt) => inSet(PUBLIC_HOSTS, entry, entryAt)) };
423
+ }
424
+ throw new Refused(at, 'this action has no identity');
425
+ };
426
+ /** Whether the harness's table names this native, so it can cross as itself; anything else (including the literal) crosses as `private`. */
427
+ const admittedNative = (harness, native) => {
428
+ if (native === PRIVATE)
429
+ return true;
430
+ const table = ACTION_TABLES[harness];
431
+ if (Object.hasOwn(table.rows, native))
432
+ return true;
433
+ const mcp = mcpOf(table, native);
434
+ return mcp !== undefined && table.toolDelimited && isPublicMcpTool(mcp.server, mcp.tool);
435
+ };
436
+ const parseNative = (harness, value, at) => {
437
+ const native = textOf(value, at);
438
+ if (!admittedNative(harness, native))
439
+ throw new Refused(at, 'outside the vocabulary');
440
+ return native;
441
+ };
442
+ /** A subagent type as the projection leaves it: a built-in of a harness, or `custom`; a custom agent's own name is private and never admitted. */
443
+ const parseAgentType = (value, at) => {
444
+ const name = textOf(value, at);
445
+ if (projectedAgentType(name) !== name)
446
+ throw new Refused(at, 'outside the vocabulary');
447
+ return name;
448
+ };
449
+ const parseStep = (harness, value, at) => {
450
+ const raw = fieldsOf(value, at, ['action', 'native', 'identity', 'status', 'tokens', 'ms', 'remainingTokens', 'agentType', 'parent']);
451
+ const action = oneOf(CANONICAL_ACTIONS, raw.action, under(at, 'action'));
452
+ return {
453
+ action,
454
+ native: parseNative(harness, raw.native, under(at, 'native')),
455
+ identity: parseIdentity(harness, action, raw.identity, under(at, 'identity')),
456
+ status: oneOf(STATUSES, raw.status, under(at, 'status')),
457
+ tokens: orNull(raw.tokens, under(at, 'tokens'), (entry, entryAt) => bucketIn('tokens', entry, entryAt)),
458
+ ms: orNull(raw.ms, under(at, 'ms'), (entry, entryAt) => bucketIn('ms', entry, entryAt)),
459
+ remainingTokens: bucketIn('tokens', raw.remainingTokens, under(at, 'remainingTokens')),
460
+ agentType: orNull(raw.agentType, under(at, 'agentType'), parseAgentType),
461
+ parent: orNull(raw.parent, under(at, 'parent'), countOf),
462
+ };
463
+ };
464
+ const parseTool = (harness, value, at) => {
465
+ const raw = fieldsOf(value, at, ['side', 'prompt', 'status', 'durationMs', 'steps']);
466
+ return {
467
+ side: booleanOf(raw.side, under(at, 'side')),
468
+ prompt: orNull(raw.prompt, under(at, 'prompt'), countOf),
469
+ status: oneOf(STATUSES, raw.status, under(at, 'status')),
470
+ durationMs: orNull(raw.durationMs, under(at, 'durationMs'), countOf),
471
+ steps: listOf(raw.steps, under(at, 'steps'), (entry, entryAt) => parseStep(harness, entry, entryAt)),
472
+ };
473
+ };
474
+ const parsePrompt = (value, at) => {
475
+ const raw = fieldsOf(value, at, ['cls', 'errors', 'wallSec', 'usage', 'explore', 'frame']);
476
+ return {
477
+ cls: oneOf(PROMPT_CLASSES, raw.cls, under(at, 'cls')),
478
+ errors: bucketIn('count', raw.errors, under(at, 'errors')),
479
+ wallSec: orNull(raw.wallSec, under(at, 'wallSec'), (entry, entryAt) => bucketIn('seconds', entry, entryAt)),
480
+ usage: parseUsage(raw.usage, under(at, 'usage')),
481
+ explore: orNull(raw.explore, under(at, 'explore'), (entry, entryAt) => bucketIn('calls', entry, entryAt)),
482
+ frame: parseFrame(raw.frame, under(at, 'frame')),
483
+ };
484
+ };
485
+ const ROOT = /^[0-9a-f]{64}$/;
486
+ const PRINT = /^[0-9a-f]{16}$/;
487
+ const parseRoot = (value, at) => {
488
+ const root = textOf(value, at);
489
+ if (!ROOT.test(root))
490
+ throw new Refused(at, 'not a SHA-256');
491
+ return root;
492
+ };
493
+ const parseTranscript = (value, at) => {
494
+ const raw = fieldsOf(value, at, ['merkleRoot', 'leaves', 'harness', 'frame', 'prompts', 'tools']);
495
+ const harness = oneOf(HARNESSES, raw.harness, under(at, 'harness'));
496
+ return {
497
+ merkleRoot: parseRoot(raw.merkleRoot, under(at, 'merkleRoot')),
498
+ leaves: countOf(raw.leaves, under(at, 'leaves')),
499
+ harness,
500
+ frame: parseFrame(raw.frame, under(at, 'frame')),
501
+ prompts: listOf(raw.prompts, under(at, 'prompts'), parsePrompt),
502
+ tools: listOf(raw.tools, under(at, 'tools'), (entry, entryAt) => parseTool(harness, entry, entryAt)),
503
+ };
504
+ };
505
+ const DELTA_FIELDS = ['plugin', 'package', 'permissionMode', 'effort'];
506
+ /** The kinds each field can take: a plugin comes or goes, a package only goes (an addition is adoption, which `pool` reads from the calls), and a mode or an effort changes. */
507
+ const KINDS_OF = { plugin: ['added', 'removed'], package: ['removed'], permissionMode: ['changed'], effort: ['changed'] };
508
+ /** The one metric each field's change is measured in, and the bucket edges that metric is held to. */
509
+ const METRIC_OF = { plugin: 'callsPerSession', package: 'callsPerSession', permissionMode: 'callsPerSession', effort: 'thinking' };
510
+ const METRIC_EDGES = { callsPerSession: 'count', thinking: 'tokens' };
511
+ const NO_NAME = new Set();
512
+ const nullOnly = (value, at) => {
513
+ if (value !== null)
514
+ throw new Refused(at, 'outside the vocabulary');
515
+ return null;
516
+ };
517
+ /**
518
+ * The entry of a delta, held to its field's own row. A plugin has no registry to pass, so only the word `private` stands for one; a package is a public name with its registry, or `private` with none; a mode
519
+ * and an effort are named by their field and carry no name. The change is a signed bucket of the field's metric.
520
+ */
521
+ const parseDelta = (value, at) => {
522
+ const raw = fieldsOf(value, at, ['deltaKind', 'field', 'name', 'registry', 'metric', 'change']);
523
+ const field = oneOf(DELTA_FIELDS, raw.field, under(at, 'field'));
524
+ const deltaKind = oneOf(KINDS_OF[field], raw.deltaKind, under(at, 'deltaKind'));
525
+ const metric = oneOf([METRIC_OF[field]], raw.metric, under(at, 'metric'));
526
+ const name = field === 'plugin' || field === 'package' ? inSet(field === 'package' ? PUBLIC_PACKAGES : NO_NAME, raw.name, under(at, 'name')) : nullOnly(raw.name, under(at, 'name'));
527
+ const registry = field === 'package' && name !== PRIVATE ? oneOf(PACKAGE_REGISTRIES, raw.registry, under(at, 'registry')) : nullOnly(raw.registry, under(at, 'registry'));
528
+ const change = raw.change;
529
+ if (typeof change !== 'number' || !Number.isInteger(change) || Math.abs(change) > EDGES[METRIC_EDGES[metric]].length)
530
+ throw new Refused(under(at, 'change'), 'not a signed bucket');
531
+ return { deltaKind, field, name, registry, metric, change };
532
+ };
533
+ /** The skeleton in `value`, rebuilt from the fields read; else the first field that is not closed vocabulary, and why. */
534
+ export const parseSkeleton = (value) => {
535
+ try {
536
+ // The first form sent exact usage; it is named here, since its fields would otherwise read as a list of strangers.
537
+ if (isFields(value) && value.format === 1)
538
+ throw new Refused('format', 'the exact-count form (format 1) is no longer read; send format 2');
539
+ const raw = fieldsOf(value, '', ['format', 'id', 'transcripts', 'timingPrint', 'deltas']);
540
+ if (raw.format !== SKELETON_FORMAT)
541
+ throw new Refused('format', 'not a format this version reads');
542
+ const id = textOf(raw.id, 'id');
543
+ if (!ID.test(id))
544
+ throw new Refused('id', 'not a hash');
545
+ const deltas = listOf(raw.deltas, 'deltas', parseDelta);
546
+ const transcripts = listOf(raw.transcripts, 'transcripts', parseTranscript);
547
+ if (transcripts.length === 0)
548
+ throw new Refused('transcripts', 'empty');
549
+ // A counting batch is what lifts the fetch cap, so a deposit has to hold something to derive from.
550
+ if (transcripts.every(({ prompts, tools }) => prompts.length === 0 && tools.length === 0))
551
+ throw new Refused('transcripts', 'holds no prompt and no tool');
552
+ // The main run's calls make the one print, so it exists exactly when the main run holds enough timed calls; a subagent's calls never decide it.
553
+ const timed = (transcripts[0]?.tools ?? []).filter(({ durationMs }) => durationMs !== null).length;
554
+ const timingPrint = orNull(raw.timingPrint, 'timingPrint', (entry, entryAt) => {
555
+ const text = textOf(entry, entryAt);
556
+ if (!PRINT.test(text))
557
+ throw new Refused(entryAt, 'not a hash');
558
+ return text;
559
+ });
560
+ if ((timingPrint === null) !== (timed < MIN_TIMED_CALLS))
561
+ throw new Refused('timingPrint', timingPrint === null ? 'missing: the main run holds enough timed calls' : 'not allowed: the main run holds too few timed calls');
562
+ // A step carries the type of the subagent whose run holds it, so the main run's steps carry none and every subagent step carries one.
563
+ transcripts.forEach(({ tools }, index) => {
564
+ tools.forEach(({ steps }, call) => {
565
+ steps.forEach(({ agentType }, position) => {
566
+ if ((agentType === null) !== (index === 0))
567
+ throw new Refused(`transcripts[${String(index)}].tools[${String(call)}].steps[${String(position)}].agentType`, index === 0 ? 'a main-run step has no subagent type' : 'a subagent step needs its type');
568
+ });
569
+ });
570
+ });
571
+ // Positions are the only ordinals on the wire, so each must name something the batch holds.
572
+ const mainCalls = transcripts[0]?.tools.length ?? 0;
573
+ transcripts.forEach(({ prompts, tools }, index) => {
574
+ tools.forEach(({ prompt, steps }, call) => {
575
+ const callAt = `transcripts[${String(index)}].tools[${String(call)}]`;
576
+ // Past the prompts it names a prompt with no record, numbered in order of appearance: no more of those than calls.
577
+ if (prompt !== null && prompt >= prompts.length + tools.length)
578
+ throw new Refused(`${callAt}.prompt`, 'no such prompt position');
579
+ steps.forEach(({ parent }, position) => {
580
+ if (parent !== null && parent >= mainCalls)
581
+ throw new Refused(`${callAt}.steps[${String(position)}].parent`, 'no such call in the main run');
582
+ });
583
+ });
584
+ });
585
+ return { ok: true, skeleton: { format: SKELETON_FORMAT, id, transcripts, timingPrint, deltas } };
586
+ }
587
+ catch (error) {
588
+ if (error instanceof Refused)
589
+ return { ok: false, field: error.field, reason: error.reason };
590
+ throw error;
591
+ }
592
+ };
593
+ const isSignedBundle = (value) => isFields(value) && typeof value.payload === 'string' && typeof value.signature === 'string';
594
+ const isBundle = (value) => isFields(value) && value.format === BUNDLE_FORMAT && typeof value.harness === 'string' && isHarness(value.harness) && typeof value.harnessVersion === 'string' && typeof value.vocabVersion === 'string' && isFields(value.asOf) && Array.isArray(value.cells) && Array.isArray(value.motifs) && Array.isArray(value.reads) && Array.isArray(value.strata);
595
+ const signatureHolds = async ({ payload, signature }, key) => {
596
+ try {
597
+ const verifier = await crypto.subtle.importKey('jwk', { kty: key.kty, crv: key.crv, x: key.x }, 'Ed25519', false, ['verify']);
598
+ return await crypto.subtle.verify('Ed25519', verifier, Uint8Array.from(atob(signature), (char) => char.charCodeAt(0)), new TextEncoder().encode(payload));
599
+ }
600
+ catch {
601
+ return false;
602
+ }
603
+ };
604
+ /** The bundle, only when the pinned key signed these bytes for the harness and version that were asked for: another harness's or version's bundle, under a good signature, is still the wrong answer. */
605
+ export const verifyBundle = async (signed, key, harness, harnessVersion) => {
606
+ if (!isSignedBundle(signed))
607
+ return { ok: false, reason: 'not a signed bundle' };
608
+ if (!(await signatureHolds(signed, key)))
609
+ return { ok: false, reason: 'the signature does not match the key pinned at login' };
610
+ const bundle = JSON.parse(signed.payload);
611
+ if (!isBundle(bundle))
612
+ return { ok: false, reason: 'not a signed bundle this version reads' };
613
+ if (bundle.vocabVersion !== VOCAB_VERSION)
614
+ return { ok: false, reason: `this bundle is for vocabulary version ${bundle.vocabVersion}, not ${VOCAB_VERSION}` };
615
+ if (bundle.harness !== harness)
616
+ return { ok: false, reason: `this bundle is for harness ${bundle.harness}, not ${harness}` };
617
+ if (bundle.harnessVersion !== harnessVersion)
618
+ return { ok: false, reason: `this bundle is for harness version ${bundle.harnessVersion}, not ${harnessVersion}` };
619
+ return { ok: true, bundle };
620
+ };
@@ -0,0 +1,32 @@
1
+ import { type Pool } from './cells.js';
2
+ import { type Read } from './read.js';
3
+ import { type Store } from './store.js';
4
+ export declare const poolFiles: (storeDir: string) => {
5
+ cells: string;
6
+ motifs: string;
7
+ consequences: string;
8
+ reads: (version: string) => string;
9
+ };
10
+ /** Every family's order 1 reads of one pool, in family order and then by slice, so a rerun writes the same bytes. */
11
+ export declare const deriveFirstOrder: (pool: Pool, store: Store) => Read[];
12
+ /** Order 1, then each stage of `relations` over everything before it. */
13
+ export declare const deriveReads: (pool: Pool, store: Store) => Read[];
14
+ export declare const serializeReads: (reads: readonly Read[]) => string;
15
+ /** The current pool's reads: already written when this pool has been read before, else derived and written once. */
16
+ export declare const refreshPool: (storeDir: string, store: Store) => Promise<{
17
+ pool: Pool;
18
+ reads: Read[];
19
+ }>;
20
+ /** The pool a deposit's index step reads. It answers without one (spec 2.7): a pool that cannot be read or written costs only the stored read IDs, whose answers keep their stand-ins, and one line on stderr says so. */
21
+ export declare const currentPool: (storeDir: string, store: Store) => Promise<{
22
+ pool: Pool;
23
+ reads: Read[];
24
+ } | undefined>;
25
+ /**
26
+ * The pool a pull answers from: the newest one already stored, with nothing derived or written. A pull used to refresh the pool, and a deposit that moved it left the next pull to
27
+ * derive every read again (12 to 17 s on a real store) and write a file nothing prunes. Only a store with no stored pool derives one, once, and a stored one is read after that.
28
+ */
29
+ export declare const servedPool: (storeDir: string, store: Store) => Promise<{
30
+ pool: Pool;
31
+ reads: Read[];
32
+ } | undefined>;