@manyoars/oars 0.2.0 → 0.4.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 (320) hide show
  1. package/README.md +147 -28
  2. package/dist/api.js +1 -1
  3. package/dist/bin.js +6 -29
  4. package/dist/cli.d.ts +10 -4
  5. package/dist/cli.js +440 -91
  6. package/dist/commands/keys.d.ts +31 -0
  7. package/dist/commands/keys.js +65 -0
  8. package/dist/commands/login.d.ts +1 -1
  9. package/dist/commands/login.js +3 -3
  10. package/dist/errors.d.ts +13 -13
  11. package/dist/errors.js +28 -11
  12. package/dist/gate/checks.d.ts +68 -0
  13. package/dist/gate/checks.js +584 -0
  14. package/dist/gate/command.d.ts +14 -0
  15. package/dist/gate/command.js +61 -0
  16. package/dist/gate/hook-fill-main.d.ts +1 -0
  17. package/dist/gate/hook-fill-main.js +16 -0
  18. package/dist/gate/hook-fill.d.ts +38 -0
  19. package/dist/gate/hook-fill.js +94 -0
  20. package/dist/gate/hook-main.d.ts +1 -0
  21. package/dist/gate/hook-main.js +47 -0
  22. package/dist/gate/hook-process.d.ts +24 -0
  23. package/dist/gate/hook-process.js +112 -0
  24. package/dist/gate/hook-record.d.ts +3 -0
  25. package/dist/gate/hook-record.js +8 -0
  26. package/dist/gate/hook.d.ts +48 -0
  27. package/dist/gate/hook.js +259 -0
  28. package/dist/gate/install.d.ts +25 -0
  29. package/dist/gate/install.js +126 -0
  30. package/dist/gate/local-bin.d.ts +8 -0
  31. package/dist/gate/local-bin.js +36 -0
  32. package/dist/gate/notes.d.ts +19 -0
  33. package/dist/gate/notes.js +27 -0
  34. package/dist/gate/novelty.d.ts +35 -0
  35. package/dist/gate/novelty.js +99 -0
  36. package/dist/gate/policy.d.ts +25 -0
  37. package/dist/gate/policy.js +73 -0
  38. package/dist/gate/record.d.ts +120 -0
  39. package/dist/gate/record.js +125 -0
  40. package/dist/gate/registry-state.d.ts +10 -0
  41. package/dist/gate/registry-state.js +30 -0
  42. package/dist/gate/registry.d.ts +56 -0
  43. package/dist/gate/registry.js +94 -0
  44. package/dist/gate/replay.d.ts +20 -0
  45. package/dist/gate/replay.js +254 -0
  46. package/dist/gate/report.d.ts +50 -0
  47. package/dist/gate/report.js +89 -0
  48. package/dist/gate/scope.d.ts +7 -0
  49. package/dist/gate/scope.js +27 -0
  50. package/dist/gate/transcripts.d.ts +68 -0
  51. package/dist/gate/transcripts.js +232 -0
  52. package/dist/hook-bin.d.ts +1 -0
  53. package/dist/hook-bin.js +50 -0
  54. package/dist/issuer.d.ts +10 -6
  55. package/dist/issuer.js +19 -24
  56. package/dist/main.d.ts +1 -0
  57. package/dist/main.js +67 -0
  58. package/dist/oidc.js +1 -1
  59. package/dist/sessions/actions.d.ts +35 -0
  60. package/dist/sessions/actions.js +429 -0
  61. package/dist/sessions/allow.d.ts +23 -0
  62. package/dist/sessions/allow.js +71 -0
  63. package/dist/sessions/archive.d.ts +11 -0
  64. package/dist/sessions/archive.js +38 -0
  65. package/dist/sessions/audit.d.ts +42 -0
  66. package/dist/sessions/audit.js +104 -0
  67. package/dist/sessions/baseline.d.ts +62 -0
  68. package/dist/sessions/baseline.js +190 -0
  69. package/dist/sessions/call-shape.d.ts +19 -0
  70. package/dist/sessions/call-shape.js +85 -0
  71. package/dist/sessions/capture-runtime.d.ts +10 -0
  72. package/dist/sessions/capture-runtime.js +12 -0
  73. package/dist/sessions/cell-increments.d.ts +47 -0
  74. package/dist/sessions/cell-increments.js +243 -0
  75. package/dist/sessions/cells.d.ts +109 -0
  76. package/dist/sessions/cells.js +261 -0
  77. package/dist/sessions/codex.d.ts +53 -0
  78. package/dist/sessions/codex.js +745 -0
  79. package/dist/sessions/command.d.ts +127 -0
  80. package/dist/sessions/command.js +419 -0
  81. package/dist/sessions/copilot.d.ts +40 -0
  82. package/dist/sessions/copilot.js +432 -0
  83. package/dist/sessions/coverage.d.ts +19 -0
  84. package/dist/sessions/coverage.js +56 -0
  85. package/dist/sessions/cursor.d.ts +24 -0
  86. package/dist/sessions/cursor.js +324 -0
  87. package/dist/sessions/derive.d.ts +57 -0
  88. package/dist/sessions/derive.js +386 -0
  89. package/dist/sessions/exclusion.d.ts +87 -0
  90. package/dist/sessions/exclusion.js +361 -0
  91. package/dist/sessions/extract.d.ts +181 -0
  92. package/dist/sessions/extract.js +1806 -0
  93. package/dist/sessions/fold.d.ts +17 -0
  94. package/dist/sessions/fold.js +71 -0
  95. package/dist/sessions/frame.d.ts +24 -0
  96. package/dist/sessions/frame.js +95 -0
  97. package/dist/sessions/gemini.d.ts +37 -0
  98. package/dist/sessions/gemini.js +613 -0
  99. package/dist/sessions/gold-arms.d.ts +67 -0
  100. package/dist/sessions/gold-arms.js +172 -0
  101. package/dist/sessions/gold.d.ts +197 -0
  102. package/dist/sessions/gold.js +463 -0
  103. package/dist/sessions/graph.d.ts +12 -0
  104. package/dist/sessions/graph.js +34 -0
  105. package/dist/sessions/grok.d.ts +36 -0
  106. package/dist/sessions/grok.js +421 -0
  107. package/dist/sessions/hook-calls.d.ts +3 -0
  108. package/dist/sessions/hook-calls.js +47 -0
  109. package/dist/sessions/hook-index.d.ts +66 -0
  110. package/dist/sessions/hook-index.js +164 -0
  111. package/dist/sessions/hooks.d.ts +17 -0
  112. package/dist/sessions/hooks.js +281 -0
  113. package/dist/sessions/identity.d.ts +27 -0
  114. package/dist/sessions/identity.js +131 -0
  115. package/dist/sessions/install.d.ts +70 -0
  116. package/dist/sessions/install.js +246 -0
  117. package/dist/sessions/lineage.d.ts +19 -0
  118. package/dist/sessions/lineage.js +118 -0
  119. package/dist/sessions/mcp.d.ts +7 -0
  120. package/dist/sessions/mcp.js +157 -0
  121. package/dist/sessions/merkle.d.ts +7 -0
  122. package/dist/sessions/merkle.js +28 -0
  123. package/dist/sessions/motif.d.ts +44 -0
  124. package/dist/sessions/motif.js +59 -0
  125. package/dist/sessions/opencode.d.ts +33 -0
  126. package/dist/sessions/opencode.js +319 -0
  127. package/dist/sessions/otel.d.ts +60 -0
  128. package/dist/sessions/otel.js +494 -0
  129. package/dist/sessions/package-books.d.ts +48 -0
  130. package/dist/sessions/package-books.js +120 -0
  131. package/dist/sessions/package-resolve.d.ts +32 -0
  132. package/dist/sessions/package-resolve.js +185 -0
  133. package/dist/sessions/pending.d.ts +34 -0
  134. package/dist/sessions/pending.js +142 -0
  135. package/dist/sessions/pi.d.ts +36 -0
  136. package/dist/sessions/pi.js +353 -0
  137. package/dist/sessions/pool-books.d.ts +21 -0
  138. package/dist/sessions/pool-books.js +75 -0
  139. package/dist/sessions/pool-choices.d.ts +5 -0
  140. package/dist/sessions/pool-choices.js +11 -0
  141. package/dist/sessions/pool-command.d.ts +115 -0
  142. package/dist/sessions/pool-command.js +669 -0
  143. package/dist/sessions/pool-delta.d.ts +54 -0
  144. package/dist/sessions/pool-delta.js +190 -0
  145. package/dist/sessions/pool-nightly-deps.d.ts +3 -0
  146. package/dist/sessions/pool-nightly-deps.js +11 -0
  147. package/dist/sessions/pool-nightly.d.ts +10 -0
  148. package/dist/sessions/pool-nightly.js +85 -0
  149. package/dist/sessions/pool-outbox.d.ts +76 -0
  150. package/dist/sessions/pool-outbox.js +313 -0
  151. package/dist/sessions/pool-print.d.ts +26 -0
  152. package/dist/sessions/pool-print.js +68 -0
  153. package/dist/sessions/pool-raw.d.ts +27 -0
  154. package/dist/sessions/pool-raw.js +68 -0
  155. package/dist/sessions/pool-replica.d.ts +35 -0
  156. package/dist/sessions/pool-replica.js +91 -0
  157. package/dist/sessions/pool-send-lock.d.ts +20 -0
  158. package/dist/sessions/pool-send-lock.js +87 -0
  159. package/dist/sessions/pool-sent.d.ts +4 -0
  160. package/dist/sessions/pool-sent.js +26 -0
  161. package/dist/sessions/pool-totals.d.ts +10 -0
  162. package/dist/sessions/pool-totals.js +21 -0
  163. package/dist/sessions/pool-wire.d.ts +435 -0
  164. package/dist/sessions/pool-wire.js +1100 -0
  165. package/dist/sessions/pool.d.ts +32 -0
  166. package/dist/sessions/pool.js +164 -0
  167. package/dist/sessions/projection.d.ts +75 -0
  168. package/dist/sessions/projection.js +165 -0
  169. package/dist/sessions/public-vocabulary.d.ts +72 -0
  170. package/dist/sessions/public-vocabulary.js +295 -0
  171. package/dist/sessions/query.d.ts +123 -0
  172. package/dist/sessions/query.js +268 -0
  173. package/dist/sessions/read-adoption.d.ts +2 -0
  174. package/dist/sessions/read-adoption.js +27 -0
  175. package/dist/sessions/read-call-shape.d.ts +2 -0
  176. package/dist/sessions/read-call-shape.js +25 -0
  177. package/dist/sessions/read-change.d.ts +2 -0
  178. package/dist/sessions/read-change.js +30 -0
  179. package/dist/sessions/read-cost.d.ts +4 -0
  180. package/dist/sessions/read-cost.js +48 -0
  181. package/dist/sessions/read-day-move.d.ts +3 -0
  182. package/dist/sessions/read-day-move.js +65 -0
  183. package/dist/sessions/read-fit.d.ts +7 -0
  184. package/dist/sessions/read-fit.js +41 -0
  185. package/dist/sessions/read-interaction.d.ts +5 -0
  186. package/dist/sessions/read-interaction.js +62 -0
  187. package/dist/sessions/read-lead-lag.d.ts +15 -0
  188. package/dist/sessions/read-lead-lag.js +81 -0
  189. package/dist/sessions/read-live.d.ts +6 -0
  190. package/dist/sessions/read-live.js +38 -0
  191. package/dist/sessions/read-method.d.ts +2 -0
  192. package/dist/sessions/read-method.js +29 -0
  193. package/dist/sessions/read-next-step.d.ts +2 -0
  194. package/dist/sessions/read-next-step.js +35 -0
  195. package/dist/sessions/read-order.d.ts +54 -0
  196. package/dist/sessions/read-order.js +62 -0
  197. package/dist/sessions/read-readable.d.ts +8 -0
  198. package/dist/sessions/read-readable.js +24 -0
  199. package/dist/sessions/read-recovery-edge.d.ts +2 -0
  200. package/dist/sessions/read-recovery-edge.js +26 -0
  201. package/dist/sessions/read-recovery.d.ts +20 -0
  202. package/dist/sessions/read-recovery.js +97 -0
  203. package/dist/sessions/read-rows.d.ts +13 -0
  204. package/dist/sessions/read-rows.js +47 -0
  205. package/dist/sessions/read-substitution.d.ts +5 -0
  206. package/dist/sessions/read-substitution.js +51 -0
  207. package/dist/sessions/read-support.d.ts +48 -0
  208. package/dist/sessions/read-support.js +73 -0
  209. package/dist/sessions/read-tool-day.d.ts +2 -0
  210. package/dist/sessions/read-tool-day.js +26 -0
  211. package/dist/sessions/read-version-boundary.d.ts +31 -0
  212. package/dist/sessions/read-version-boundary.js +95 -0
  213. package/dist/sessions/read.d.ts +86 -0
  214. package/dist/sessions/read.js +87 -0
  215. package/dist/sessions/recall-bundle.d.ts +12 -0
  216. package/dist/sessions/recall-bundle.js +33 -0
  217. package/dist/sessions/recall-consequence.d.ts +45 -0
  218. package/dist/sessions/recall-consequence.js +179 -0
  219. package/dist/sessions/recall-frames.d.ts +18 -0
  220. package/dist/sessions/recall-frames.js +44 -0
  221. package/dist/sessions/recall-next.d.ts +145 -0
  222. package/dist/sessions/recall-next.js +241 -0
  223. package/dist/sessions/recall-object.d.ts +139 -0
  224. package/dist/sessions/recall-object.js +78 -0
  225. package/dist/sessions/recall-pool.d.ts +19 -0
  226. package/dist/sessions/recall-pool.js +96 -0
  227. package/dist/sessions/recall-push.d.ts +26 -0
  228. package/dist/sessions/recall-push.js +47 -0
  229. package/dist/sessions/recall-reads.d.ts +124 -0
  230. package/dist/sessions/recall-reads.js +417 -0
  231. package/dist/sessions/recall-render.d.ts +19 -0
  232. package/dist/sessions/recall-render.js +168 -0
  233. package/dist/sessions/recovery-calls.d.ts +21 -0
  234. package/dist/sessions/recovery-calls.js +25 -0
  235. package/dist/sessions/redact.d.ts +17 -0
  236. package/dist/sessions/redact.js +113 -0
  237. package/dist/sessions/repo-root.d.ts +10 -0
  238. package/dist/sessions/repo-root.js +101 -0
  239. package/dist/sessions/schema.d.ts +694 -0
  240. package/dist/sessions/schema.js +167 -0
  241. package/dist/sessions/session-shape.d.ts +76 -0
  242. package/dist/sessions/session-shape.js +355 -0
  243. package/dist/sessions/sources.d.ts +50 -0
  244. package/dist/sessions/sources.js +233 -0
  245. package/dist/sessions/steps.d.ts +105 -0
  246. package/dist/sessions/steps.js +250 -0
  247. package/dist/sessions/store.d.ts +309 -0
  248. package/dist/sessions/store.js +1498 -0
  249. package/dist/sessions/strata.d.ts +26 -0
  250. package/dist/sessions/strata.js +77 -0
  251. package/dist/sessions/survival.d.ts +27 -0
  252. package/dist/sessions/survival.js +230 -0
  253. package/dist/sessions/transcript-tail.d.ts +14 -0
  254. package/dist/sessions/transcript-tail.js +64 -0
  255. package/dist/sessions/verify.d.ts +53 -0
  256. package/dist/sessions/verify.js +286 -0
  257. package/dist/sessions/vocab-load.d.ts +26 -0
  258. package/dist/sessions/vocab-load.js +61 -0
  259. package/dist/sessions/vscode-copilot.d.ts +38 -0
  260. package/dist/sessions/vscode-copilot.js +494 -0
  261. package/dist/telemetry/command.d.ts +8 -0
  262. package/dist/telemetry/command.js +85 -0
  263. package/dist/telemetry/consent.d.ts +17 -0
  264. package/dist/telemetry/consent.js +63 -0
  265. package/dist/telemetry/crash.d.ts +8 -0
  266. package/dist/telemetry/crash.js +52 -0
  267. package/dist/telemetry/emit.d.ts +31 -0
  268. package/dist/telemetry/emit.js +42 -0
  269. package/dist/telemetry/fields.d.ts +16 -0
  270. package/dist/telemetry/fields.js +26 -0
  271. package/dist/telemetry/health.d.ts +21 -0
  272. package/dist/telemetry/health.js +108 -0
  273. package/dist/telemetry/hook-emit.d.ts +4 -0
  274. package/dist/telemetry/hook-emit.js +18 -0
  275. package/dist/telemetry/measures.d.ts +32 -0
  276. package/dist/telemetry/measures.js +128 -0
  277. package/dist/telemetry/observe.d.ts +13 -0
  278. package/dist/telemetry/observe.js +51 -0
  279. package/dist/telemetry/read-events.d.ts +9 -0
  280. package/dist/telemetry/read-events.js +17 -0
  281. package/dist/telemetry/record.d.ts +17 -0
  282. package/dist/telemetry/record.js +26 -0
  283. package/dist/telemetry/report.d.ts +77 -0
  284. package/dist/telemetry/report.js +188 -0
  285. package/dist/telemetry/schema.d.ts +40 -0
  286. package/dist/telemetry/schema.js +187 -0
  287. package/dist/telemetry/send-main.d.ts +1 -0
  288. package/dist/telemetry/send-main.js +11 -0
  289. package/dist/telemetry/send.d.ts +36 -0
  290. package/dist/telemetry/send.js +172 -0
  291. package/dist/telemetry/spool.d.ts +17 -0
  292. package/dist/telemetry/spool.js +70 -0
  293. package/dist/telemetry/wire.d.ts +25 -0
  294. package/dist/telemetry/wire.js +76 -0
  295. package/package.json +4 -5
  296. package/vocab/crates.json +1 -0
  297. package/vocab/go.json +1 -0
  298. package/vocab/harness.json +1 -0
  299. package/vocab/homebrew.json +1 -0
  300. package/vocab/manifest.json +1 -0
  301. package/vocab/maven.json +1 -0
  302. package/vocab/models.json +1 -0
  303. package/vocab/npm.json +1 -0
  304. package/vocab/programs.json +1 -0
  305. package/vocab/pypi.json +1 -0
  306. package/vocab/rubygems.json +1 -0
  307. package/dist/commands/deploy-input.d.ts +0 -38
  308. package/dist/commands/deploy-input.js +0 -150
  309. package/dist/commands/deploy.d.ts +0 -22
  310. package/dist/commands/deploy.js +0 -124
  311. package/dist/commands/status.d.ts +0 -7
  312. package/dist/commands/status.js +0 -18
  313. package/dist/commands/templates.d.ts +0 -11
  314. package/dist/commands/templates.js +0 -32
  315. package/dist/commands/test-charge.d.ts +0 -11
  316. package/dist/commands/test-charge.js +0 -34
  317. package/dist/journal.d.ts +0 -26
  318. package/dist/journal.js +0 -58
  319. package/parity/deploy-plans/paid-api.json +0 -34
  320. package/parity/deploy-plans/per-task-agent.json +0 -34
@@ -0,0 +1,1100 @@
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 as a closed list of fields, each value redacted first: 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, publicVersion, PUBLIC_PACKAGES, projectedAgentType, PUBLIC_FLAGS, REPO_SIZES, TOOL_INPUT_KEYS, VOCAB_VERSION } from './public-vocabulary.js';
6
+ import { ARG_CLASSES, TARGET_CLASSES } from './call-shape.js';
7
+ import { recoveryCallsOf, RECOVERY_CALLS } from './recovery-calls.js';
8
+ import { projectLanguage, projectLanguageShare } from './strata.js';
9
+ import { ERROR_CODES, LANGUAGE_SHARES, PROMPT_INTERVENTIONS } from './schema.js';
10
+ // Format 3 (step 1 of the wire): a tool carries its `errorCode` and, for a failed call, what followed it (`recoveryCalls`). Formats 1 and 2 are refused by name; nothing is deployed, so nothing is migrated.
11
+ export const SKELETON_FORMAT = 3;
12
+ /**
13
+ * The version of the field list: every field name a batch can hold (`BATCH_FIELDS` in `pool-outbox.ts`). It moves with any field added, so a server told the version knows which names a batch may carry,
14
+ * and a test pins the list to it (`pool-wire-additive.test.ts`). A field is deprecated and left on the list, never removed (conclusion section 17, rule 3).
15
+ */
16
+ export const FIELD_LIST_VERSION = 8;
17
+ export const BUNDLE_FORMAT = 1;
18
+ /** One constant: the pool serves only the vocabulary version the client holds. */
19
+ export { VOCAB_VERSION };
20
+ /**
21
+ * 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
22
+ * 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.
23
+ */
24
+ const EDGES = {
25
+ tokens: Array.from({ length: 11 }, (_, power) => 1000 * 2 ** power),
26
+ ms: Array.from({ length: 17 }, (_, power) => 100 * 2 ** power),
27
+ seconds: Array.from({ length: 17 }, (_, power) => 2 ** power),
28
+ count: [1, 2, 3, 5, 9, 17, 33],
29
+ // 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.
30
+ calls: [1, 2, 3, 5, 9, 17, 33, 65, 129, 257, 513, 1025, 2049, 4097],
31
+ };
32
+ /** The bucket a value falls in. */
33
+ export const bucketOf = (kind, value) => EDGES[kind].filter((edge) => value >= edge).length;
34
+ /** 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. */
35
+ export const projectExplore = (stored) => (stored !== undefined && Number.isSafeInteger(stored) && stored >= 0 ? bucketOf('calls', stored) : null);
36
+ /** The lowest value of a bucket, which is what a derivation counts a bucketed value as: it never reads above what the sender held. */
37
+ export const floorOf = (kind, bucket) => EDGES[kind][bucket - 1] ?? 0;
38
+ /** The closed list of how a step came about (`schema.ts` `StepEdge`). */
39
+ export const EDGE_KINDS = ['next', 'spawned', 'retried', 'after-error'];
40
+ /** How many people the operator's organization has (an operator's setting, read locally, never looked up). */
41
+ export const ORG_SIZES = ['solo', '2-10', '11-100', '101-1000', 'over-1000', 'unknown'];
42
+ /** Whether the repository's local git log shows one commit author or several (distinct authors, counted and then dropped). */
43
+ export const TEAMS = ['solo', 'team', 'unknown'];
44
+ /** Whether the repository is public or private (an operator's setting, read locally, never looked up). */
45
+ export const REPO_VISIBILITIES = ['public', 'private', 'unknown'];
46
+ export const NO_COVERAGE = { orgSize: 'unknown', team: 'unknown', repoVisibility: 'unknown' };
47
+ /** A repo key is the store-salted hash a session record holds of its repository's identity: sixteen lowercase hex characters. */
48
+ const REPO_KEY = /^[0-9a-f]{16}$/;
49
+ /** The UTC hour of day, 0 to 23, of a timestamp, whatever offset it was written in; null when there is none or it does not parse. */
50
+ export const hourOf = (ts) => {
51
+ if (ts === null || ts === undefined || ts === '')
52
+ return null;
53
+ const millis = Date.parse(ts);
54
+ return Number.isNaN(millis) ? null : new Date(millis).getUTCHours();
55
+ };
56
+ /** A usage category's `tokens` bucket, or null when the record says the source never recorded it. A record that predates `usageObserved` reads every category as observed. */
57
+ const observedBucket = (value, observed, field) => (observed === undefined || observed.includes(field) ? bucketOf('tokens', value) : null);
58
+ /** W's minimum for a timing print (deposit-weighting spec, layer 5): fewer timed calls than this say too little to tell sessions apart. */
59
+ export const MIN_TIMED_CALLS = 50;
60
+ /** A session has ended once no transcript of it has gained a line for this long (`session_idle_hours`, spec Section 7). */
61
+ export const SESSION_IDLE_HOURS = 6;
62
+ const HOUR_MS = 60 * 60 * 1000;
63
+ /**
64
+ * 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
65
+ * 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
66
+ * 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.
67
+ */
68
+ export const hasEnded = (records, now, written) => now - Math.max(written, ...records.transcripts.map(({ session }) => session.rev)) >= SESSION_IDLE_HOURS * HOUR_MS;
69
+ const groupBy = (items, keyOf) => {
70
+ const groups = new Map();
71
+ for (const item of items) {
72
+ const key = keyOf(item);
73
+ const group = groups.get(key);
74
+ if (group === undefined)
75
+ groups.set(key, [item]);
76
+ else
77
+ group.push(item);
78
+ }
79
+ return groups;
80
+ };
81
+ /** Every main session of a store, with its records. */
82
+ export const sessionsOf = (store, roots) => {
83
+ // Grouped once: a fleet's store holds thousands of sessions over hundreds of thousands of rows, and a scan per session would be quadratic.
84
+ const prompts = groupBy(store.prompts, (prompt) => prompt.session);
85
+ const tools = groupBy(store.tools, (tool) => tool.session);
86
+ const subagents = groupBy(store.subagents, (subagent) => subagent.parent);
87
+ const recordsOf = (session) => ({ session, prompts: prompts.get(session.session) ?? [], tools: tools.get(session.session) ?? [], root: roots.get(session.session) ?? null });
88
+ // 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`.
89
+ return store.sessions.map((main) => ({ main, transcripts: [main, ...[...(subagents.get(main.session) ?? [])].sort((a, b) => (a.session < b.session ? -1 : 1))].map(recordsOf) }));
90
+ };
91
+ /** The most characters a name may hold on the wire. A longer value is not a name: it counts as redacted. */
92
+ export const NAME_MAX = 128;
93
+ // A name is a word: letters, digits, and a few joiners, with an optional leading `@` for a scope. A mis-parsed command line, a path, an email, or a quoted string fails it. A `/` is not a joiner here: a name holding one is a path (`src/acme/plan.md`).
94
+ const WIRE_NAME = /^@?[A-Za-z0-9_][A-Za-z0-9_.+-]*(?::[A-Za-z0-9_.+-]+)*$/;
95
+ // A pull's package `name` is the one place a `/` belongs: a scope (`@scope/pkg`) or a Go module path (`golang.org/x/tools`).
96
+ const WIRE_PULL_NAME = /^@?[A-Za-z0-9_][A-Za-z0-9_.+-]*(?:[/:][A-Za-z0-9_.+-]+)*$/;
97
+ /** Whether a value has the shape of a name (and so can be told from a command line, a path, or an email) and is within `NAME_MAX`. */
98
+ export const isWireName = (name) => name.length <= NAME_MAX && WIRE_NAME.test(name);
99
+ /** The shape of a pull's package name: a name, or a scope or module path with `/` in it. */
100
+ export const isWirePullName = (name) => name.length <= NAME_MAX && WIRE_PULL_NAME.test(name);
101
+ /** No redaction and no terms: the name shape and the length limit still apply. A local read, which sends nothing, uses it. */
102
+ export const OPEN_GATE = { redact: (text) => text, terms: [] };
103
+ /** The redactor leaves the name and each of its underscore-delimited parts as they are: `_` is a word character to it, so a host inside `mcp__git.acme.internal__run` is invisible to a pass over the whole name. */
104
+ const unchanged = (gate, name) => gate.redact(name) === name && name.split(/_+/).every((part) => gate.redact(part) === part);
105
+ /**
106
+ * A name as the wire holds it: the value itself, or `private` when it counts as redacted. A value counts as redacted when the redactor changes it, it holds an exclusion term, it fails the name shape, or it is over
107
+ * `NAME_MAX` characters, because a secret, an employer's name, or a mis-parsed command is not a name and nothing downstream can take it back. `keys` (t870) sends its names through this same function.
108
+ */
109
+ export const wireName = (gate, name, hasShape = isWireName) => {
110
+ const lower = name.toLowerCase();
111
+ return hasShape(name) && unchanged(gate, name) && !gate.terms.some((term) => term !== '' && lower.includes(term.toLowerCase())) ? name : PRIVATE;
112
+ };
113
+ const FLAG_DASHES = /^-{1,2}/;
114
+ const SHORT_FLAG = /^-[A-Za-z]$/;
115
+ // The long form has its own shape, with no `:` (the joiner `WIRE_NAME` allows for `scope:name`), because `--password:hunter2` is a value joined to its flag and no part of it is a name.
116
+ const LONG_FLAG = /^--[A-Za-z0-9][A-Za-z0-9_.+-]*$/;
117
+ /**
118
+ * The shape of a flag name: a long flag (two dashes, then a name with no `:`) or a short one (one dash, one letter), or a single-dash token longer than one letter only when the public flag list holds it (`-NoProfile`, `-Command`). A value joined to its flag
119
+ * fails it: `--token=abc`, `--password:hunter2`, and a short flag with its value attached (`-psecret123`, `-uuser:pass`), which no list holds.
120
+ */
121
+ const isWireFlag = (flag) => SHORT_FLAG.test(flag) || PUBLIC_FLAGS.has(flag) || (flag.length <= NAME_MAX && LONG_FLAG.test(flag));
122
+ /** A multi-letter single-dash flag no public list holds (`go test -race`, `-psecret123`): the client before t900 dropped it, so this one does too, and an old and a new client key one motif for one command. */
123
+ const isDroppedFlag = (flag) => flag.length > 2 && flag.startsWith('-') && !flag.startsWith('--') && !PUBLIC_FLAGS.has(flag);
124
+ /** A flag name as the wire holds it: the name after the one gate, or `private` when it counts as redacted. Only the name is ever read, never a value. */
125
+ const wireFlag = (gate, flag) => (isWireFlag(flag) && wireName(gate, flag.replace(FLAG_DASHES, '')) !== PRIVATE ? flag : PRIVATE);
126
+ /**
127
+ * A call's key names as the wire holds them (t870): names only, never a value. A public tool (one with a pinned input list) sends the names on its list and counts the rest; any other tool sends a name only when it passes the
128
+ * name gate, and a tool that went as `private` sends none, so a private tool's keys name no private work. `dropped` counts the distinct names not sent.
129
+ */
130
+ const wireKeys = (stored, native, gate) => {
131
+ const distinct = [...new Set(stored)];
132
+ const listed = native === undefined ? undefined : TOOL_INPUT_KEYS.get(native);
133
+ const kept = native === undefined || native === PRIVATE ? [] : distinct.filter((name) => (listed === undefined ? wireName(gate, name) !== PRIVATE : listed.has(name)));
134
+ return { keys: kept.sort(), dropped: distinct.length - kept.length };
135
+ };
136
+ /** A stored closed-list member, else dropped: a hand-edited record never puts a value off the list on the wire. */
137
+ const memberOf = (list, value) => list.find((entry) => entry === value);
138
+ /** The basename of a program as capture read it: a path never goes on the wire. */
139
+ const basenameOf = (program) => program.replace(/^.*[\\/]/, '');
140
+ /**
141
+ * A projected command as the server's parser reads it: the program as its basename after the gate (the stored identity reads `<literal>` for a program off the curated list, so the name comes from the call's segments),
142
+ * and the subcommand and flag names the reader gives, each through the same gate and never a flag value. Which of them the reader holds for a program is capture's; the wire adds no list of its own.
143
+ */
144
+ const wireCommand = ({ program, subcommand, flags }, raw, gate) => {
145
+ const named = program !== PRIVATE ? program : raw;
146
+ if (named === undefined)
147
+ return { program: PRIVATE, sub: null, flags: [] };
148
+ const sent = wireName(gate, basenameOf(named));
149
+ // A private program holds no subcommand or flag: the parser refuses one that does.
150
+ if (sent === PRIVATE)
151
+ return { program: PRIVATE, sub: null, flags: [] };
152
+ return { program: sent, sub: subcommand === null ? null : wireName(gate, subcommand), flags: flags.filter((flag) => !isDroppedFlag(flag)).map((flag) => wireFlag(gate, flag)) };
153
+ };
154
+ /** A pull in the wire's spelling: its package name after the gate. A version beside a name that is not on the public list could identify it, so it goes as `<literal>` (null stays null), the rule capture applies. */
155
+ const wirePull = (pull, gate) => {
156
+ const name = wireName(gate, pull.name, isWirePullName);
157
+ return { ...pull, name, version: !PUBLIC_PACKAGES.has(name) && pull.version !== null ? LITERAL : pull.version };
158
+ };
159
+ /** A non-shell step's identity in the wire's spelling; a shell step's comes from `wireCommand`. `raw` is the harness's own tool name, class A, which an MCP call's masked identity is read back from. */
160
+ const wireIdentity = (identity, harness, raw, gate) => {
161
+ if (identity === null || 'program' in identity)
162
+ return identity;
163
+ if ('server' in identity) {
164
+ const table = ACTION_TABLES[harness];
165
+ const parsed = mcpOf(table, raw);
166
+ const server = wireName(gate, identity.server === LITERAL ? (parsed?.server ?? LITERAL) : identity.server);
167
+ // A tool beside a masked server has no server to belong to, and a harness that does not delimit the server cannot split the tool from it: both go as the literal.
168
+ const name = identity.tool === LITERAL ? parsed?.tool : identity.tool;
169
+ const tool = server === PRIVATE || !table.toolDelimited || name === undefined ? PRIVATE : wireName(gate, name);
170
+ return { server, tool: tool === PRIVATE ? LITERAL : tool };
171
+ }
172
+ if ('skill' in identity)
173
+ return { skill: wireName(gate, identity.skill) };
174
+ return { host: identity.host === null ? null : wireName(gate, identity.host) };
175
+ };
176
+ /**
177
+ * A stored tool's steps as the wire holds them: each shell step's identity is replaced by the projected command, and every long-tail name (a native name, MCP server, skill, host, program, or pull name) is
178
+ * sent as it is after the gate, or `private` when it counts as redacted. A `private` program holds no pull. The one place a deposit and a local read take it from.
179
+ */
180
+ export const projectedSteps = (tool, read, harness, gate = OPEN_GATE) => {
181
+ const commands = read(tool);
182
+ const steps = tool.steps ?? [];
183
+ // `segments` pair with the steps when their counts agree (`projectLiteralStep`): segment `at` is step `at`, and its `run` starts with the program's basename.
184
+ const segments = tool.segments?.length === steps.length ? tool.segments : undefined;
185
+ return steps.map((step, at) => {
186
+ const native = admittedNative(harness, step.native) ? step.native : wireName(gate, step.native === LITERAL ? tool.tool : step.native);
187
+ if (step.action !== 'shell.exec')
188
+ return { ...step, native, identity: wireIdentity(step.identity, harness, tool.tool, gate) };
189
+ const command = wireCommand(commands[at] ?? { program: PRIVATE, subcommand: null, flags: [] }, segments?.[at]?.run.split(' ')[0], gate);
190
+ const pulls = step.identity !== null && 'program' in step.identity ? step.identity.pulls : undefined;
191
+ return { ...step, native, identity: pulls === undefined || command.program === PRIVATE ? command : { ...command, pulls: pulls.map((pull) => wirePull(pull, gate)) } };
192
+ });
193
+ };
194
+ /** A failed call's stored `errorCode` as the wire holds it: a member of the closed list, else `other`; null for a call that did not fail and for one captured without a code (the wire does not guess one). */
195
+ const wireErrorCode = ({ status, errorCode }) => (status !== 'error' || errorCode === undefined ? null : ERROR_CODES.has(errorCode) ? errorCode : 'other');
196
+ /** A transcript's calls in the order they ran, which is the order a skeleton holds them in. */
197
+ const byCall = (tools) => [...tools].sort((a, b) => a.n - b.n);
198
+ const placesOf = (tools) => {
199
+ const calls = new Map();
200
+ const steps = new Map();
201
+ for (const [position, { n, steps: held }] of byCall(tools).entries()) {
202
+ calls.set(n, position);
203
+ for (const step of held ?? [])
204
+ steps.set(`${String(n)}/${String(step.seg)}`, steps.size);
205
+ }
206
+ return { calls, steps };
207
+ };
208
+ /** 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. */
209
+ const carrier = (steps) => Math.max(0, steps.findIndex(({ action }) => action === 'agent.spawn'));
210
+ /**
211
+ * A model or harness version string as the wire holds it: the string after the gate (`wireName`), null staying null. The pool assigns its class. Capture's closed value (`<literal>`) goes as it is, the form a client before
212
+ * t900 sent, so an old and a new client key one closed frame to one pool cell; the parser accepts it.
213
+ */
214
+ const wireString = (gate, value) => (value === null || value === LITERAL ? value : wireName(gate, value));
215
+ /**
216
+ * A stored frame with the UTC hour of `ts` beside its day, and 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
217
+ * 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.
218
+ */
219
+ const wireFrame = (frame, strata, gate, ts) => {
220
+ const shares = new Map();
221
+ for (const { language, share } of strata?.languages ?? []) {
222
+ const held = projectLanguageShare(share);
223
+ const name = projectLanguage(language);
224
+ if (held !== null && !shares.has(name))
225
+ shares.set(name, held);
226
+ }
227
+ const hour = hourOf(ts);
228
+ return { ...frame, harnessVersion: wireString(gate, frame.harnessVersion), model: wireString(gate, frame.model), stack: { ...frame.stack, languageShares: [...shares].map(([language, languageShare]) => ({ language, languageShare })) }, ...(hour === null ? {} : { hour }) };
229
+ };
230
+ /** Each of `names` after the gate (a name that counts as redacted reads `private`), sorted. */
231
+ const listedAll = (gate, names) => [...new Set(names)].map((name) => wireName(gate, name)).sort();
232
+ /**
233
+ * The reach a session's record shows (t867): the servers of the tools its `funnel` offered or loaded and of the MCP tools it called, the skills it called, and the plugins its `setup` names. A record
234
+ * with neither `setup` nor `funnel` is unfilled (a harness that records none, or a capture before version 12) and lists nothing, so the absence is not read as no tools.
235
+ */
236
+ const reachOf = (session, tools, harness, gate) => {
237
+ if (session.setup === undefined && session.funnel === undefined)
238
+ return { filled: false, mcp: [], skills: [], plugins: [] };
239
+ const table = ACTION_TABLES[harness];
240
+ const servers = new Set();
241
+ const skills = new Set();
242
+ for (const name of [...(session.funnel?.offered ?? []), ...(session.funnel?.loaded ?? [])]) {
243
+ const server = mcpOf(table, name)?.server;
244
+ if (server !== undefined)
245
+ servers.add(server);
246
+ }
247
+ for (const { steps, skill } of tools) {
248
+ if (skill !== undefined)
249
+ skills.add(skill);
250
+ for (const { identity } of steps ?? []) {
251
+ if (identity !== null && 'server' in identity)
252
+ servers.add(identity.server);
253
+ if (identity !== null && 'skill' in identity)
254
+ skills.add(identity.skill);
255
+ }
256
+ }
257
+ return { filled: true, mcp: listedAll(gate, servers), skills: listedAll(gate, skills), plugins: (session.setup?.plugins ?? []).map(() => PRIVATE) };
258
+ };
259
+ const transcriptOf = ({ session, prompts, tools, root }, spawner, spawned, agentType, read, gate, coverage) => {
260
+ if (session.frame === undefined || root === null)
261
+ return null;
262
+ const harness = session.harness ?? 'claude-code';
263
+ const framedPrompts = [];
264
+ const promptAt = new Map();
265
+ for (const { index, cls, errors, wallSec, usage, usageObserved, usageResponses, frame, strata, ts, intervention } of [...prompts].sort((a, b) => a.index - b.index)) {
266
+ if (frame === undefined)
267
+ return null;
268
+ promptAt.set(index, framedPrompts.length);
269
+ framedPrompts.push({
270
+ cls,
271
+ errors: bucketOf('count', errors),
272
+ wallSec: wallSec === null ? null : bucketOf('seconds', wallSec),
273
+ usage: {
274
+ input: observedBucket(usage.input, usageObserved, 'input'),
275
+ cacheWrite: observedBucket(usage.cacheWrite, usageObserved, 'cacheWrite'),
276
+ cacheRead: observedBucket(usage.cacheRead, usageObserved, 'cacheRead'),
277
+ output: observedBucket(usage.output, usageObserved, 'output'),
278
+ thinking: observedBucket(usage.thinking, usageObserved, 'thinking'),
279
+ },
280
+ explore: projectExplore(strata?.explore),
281
+ ...(usageResponses === undefined ? {} : { usageResponses: bucketOf('count', usageResponses) }),
282
+ ...(intervention === undefined ? {} : { intervention }),
283
+ frame: wireFrame(frame, strata, gate, ts),
284
+ });
285
+ }
286
+ // The frames the steps ran in: the transcript's own first, then one for each distinct (prompt frame, call model, call harness version) a call shows, so a model that changes in the middle of a prompt
287
+ // reaches the steps that ran on it. A call that recorded neither takes its prompt's frame, or the transcript's before the first prompt.
288
+ const reach = reachOf(session, tools, harness, gate);
289
+ // The repo key and the bands describe the session, not a prompt, so they sit where the reach does. A session in no repository sends no key, and so does one whose record holds anything but a key's shape: the emitter is the privacy control.
290
+ const where = { reach, ...(session.repo !== null && REPO_KEY.test(session.repo) ? { repoKey: session.repo } : {}), ...coverage };
291
+ const transcriptFrame = { ...wireFrame(session.frame, session.strata, gate, session.tsFirst), ...where };
292
+ const frames = [transcriptFrame];
293
+ const frameAtKey = new Map([[JSON.stringify(transcriptFrame), 0]]);
294
+ const frameOfCall = ({ model, harnessVersion }, promptPosition) => {
295
+ const base = promptPosition === null ? undefined : framedPrompts[promptPosition]?.frame;
296
+ const held = {
297
+ ...(base ?? transcriptFrame),
298
+ ...(model === undefined ? {} : { model: wireString(gate, model) }),
299
+ ...(harnessVersion === null ? {} : { harnessVersion: wireString(gate, harnessVersion) }),
300
+ ...where,
301
+ };
302
+ const key = JSON.stringify(held);
303
+ const found = frameAtKey.get(key);
304
+ if (found !== undefined)
305
+ return found;
306
+ frames.push(held);
307
+ frameAtKey.set(key, frames.length - 1);
308
+ return frames.length - 1;
309
+ };
310
+ const steppedTools = [];
311
+ const own = placesOf(tools);
312
+ // 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.
313
+ const exact = [];
314
+ let unrecorded = 0;
315
+ const ordered = byCall(tools);
316
+ for (const [at, tool] of ordered.entries()) {
317
+ if (tool.steps === undefined)
318
+ return null;
319
+ const { side, prompt, status, durationMs } = tool;
320
+ const errorCode = wireErrorCode(tool);
321
+ const recoveryCalls = recoveryCallsOf(ordered, at);
322
+ // 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
323
+ // the calls that shared a prompt still share one, and no position reads as a prompt.
324
+ if (prompt !== null && !promptAt.has(prompt))
325
+ promptAt.set(prompt, framedPrompts.length + unrecorded++);
326
+ const promptPosition = prompt === null ? null : (promptAt.get(prompt) ?? null);
327
+ const projected = projectedSteps(tool, read, harness, gate);
328
+ exact.push(projected.map(({ cost }) => cost.tokens));
329
+ const frame = frameOfCall(tool, promptPosition);
330
+ const steps = projected.map(({ action, native, identity, status: reported, cost, parent, edges }) => {
331
+ // An edge names a step by its position in the transcript it sits in; a `spawned` edge sits in the spawner, and one with no spawner in the batch goes unsent, since it names nothing here.
332
+ const arriving = edges.flatMap(({ kind, n: from, seg: fromSeg }) => {
333
+ const target = (kind === 'spawned' ? spawner?.places : own)?.steps.get(`${String(from)}/${String(fromSeg)}`);
334
+ return target === undefined ? [] : [{ kind, target }];
335
+ });
336
+ return {
337
+ action,
338
+ native,
339
+ identity,
340
+ status: reported,
341
+ tokens: cost.tokens === null ? null : bucketOf('tokens', cost.tokens),
342
+ ms: cost.durationMs === null ? null : bucketOf('ms', cost.durationMs),
343
+ ...(cost.allocated === true ? { allocated: true } : {}),
344
+ remainingTokens: 0,
345
+ agentType,
346
+ parent: parent === null || spawner === null ? null : (spawner.places.calls.get(parent) ?? null),
347
+ frame,
348
+ ...(arriving.length === 0 ? {} : { edges: arriving }),
349
+ ...(tool.skill === undefined ? {} : { skill: wireName(gate, tool.skill) }),
350
+ };
351
+ });
352
+ const { keys, dropped } = wireKeys(tool.keys, steps[0]?.native, gate);
353
+ const targetClass = memberOf(TARGET_CLASSES, tool.targetClass);
354
+ const stored = tool.argClass;
355
+ const argClass = stored === undefined ? undefined : ARG_CLASSES.filter((kind) => stored.includes(kind));
356
+ steppedTools.push({
357
+ side,
358
+ prompt: promptPosition,
359
+ status,
360
+ durationMs,
361
+ errorCode,
362
+ ...(recoveryCalls === null ? {} : { recoveryCalls }),
363
+ ...(keys.length === 0 ? {} : { keys }),
364
+ ...(dropped === 0 ? {} : { keysDropped: bucketOf('count', dropped) }),
365
+ ...(targetClass === undefined ? {} : { targetClass }),
366
+ ...(argClass === undefined ? {} : { argClass: argClass.sort() }),
367
+ steps,
368
+ });
369
+ }
370
+ // 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).
371
+ // `later` sums the steps that reported tokens; `laterKnown` is how many did, so a run whose later steps all reported nothing stays null instead of reading as zero.
372
+ let later = 0;
373
+ let laterSteps = 0;
374
+ let laterKnown = 0;
375
+ for (let call = steppedTools.length - 1; call >= 0; call -= 1) {
376
+ const steps = steppedTools[call]?.steps ?? [];
377
+ const held = carrier(steps);
378
+ for (let at = steps.length - 1; at >= 0; at -= 1) {
379
+ const step = steps[at];
380
+ if (step === undefined)
381
+ continue;
382
+ step.remainingTokens = laterSteps > 0 && laterKnown === 0 ? null : bucketOf('tokens', later);
383
+ const own = exact[call]?.[at] ?? null;
384
+ const spawnedTokens = at === held ? (spawned.get(call) ?? 0) : 0;
385
+ later += (own ?? 0) + spawnedTokens;
386
+ laterSteps += 1;
387
+ if (own !== null || spawnedTokens > 0)
388
+ laterKnown += 1;
389
+ }
390
+ }
391
+ return { merkleRoot: root.merkleRoot, leaves: root.leaves, harness, frame: transcriptFrame, frames, spawnedIn: spawner?.index ?? null, prompts: framedPrompts, tools: steppedTools };
392
+ };
393
+ /**
394
+ * 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
395
+ * 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.
396
+ */
397
+ export const skeletonOf = (records, id, read, print, deltas = [], gate = OPEN_GATE, coverage = NO_COVERAGE) => {
398
+ const mainTools = byCall(records.transcripts[0]?.tools ?? []);
399
+ const places = records.transcripts.map(({ tools }) => placesOf(tools));
400
+ const spawners = places[0]?.calls ?? new Map();
401
+ // A subagent's step names the call that spawned it by that call's `n` in the transcript its session record names as `spawnedIn`, which the skeleton holds as a position in the batch: that transcript's, and the call's within it.
402
+ // A record that names none the batch holds, or itself, or lies on a cycle of spawners, sends no link; one whose spawner sits on a cycle it is not on keeps its link.
403
+ const spawnerOf = records.transcripts.map(({ session }, index) => {
404
+ const at = index === 0 || session.spawnedIn === null ? -1 : records.transcripts.findIndex((held) => held.session.session === session.spawnedIn);
405
+ return at === -1 ? null : at;
406
+ });
407
+ const cyclic = (index) => {
408
+ let at = spawnerOf[index] ?? null;
409
+ for (let hop = 0; hop < spawnerOf.length && at !== null && at !== index; hop += 1)
410
+ at = spawnerOf[at] ?? null;
411
+ return at === index;
412
+ };
413
+ // 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.
414
+ const spawned = new Map();
415
+ for (const [index, entry] of records.transcripts.entries()) {
416
+ if (spawnerOf[index] !== 0)
417
+ continue;
418
+ for (const tool of entry.tools) {
419
+ for (const { cost, parent } of tool.steps ?? []) {
420
+ const position = parent === null ? undefined : spawners.get(parent);
421
+ if (position !== undefined)
422
+ spawned.set(position, (spawned.get(position) ?? 0) + (cost.tokens ?? 0));
423
+ }
424
+ }
425
+ }
426
+ const transcripts = [];
427
+ for (const [index, entry] of records.transcripts.entries()) {
428
+ const at = spawnerOf[index] ?? null;
429
+ const spawnerPlaces = at === null || cyclic(index) ? undefined : places[at];
430
+ const transcript = transcriptOf(entry, at === null || spawnerPlaces === undefined ? null : { index: at, places: spawnerPlaces }, index === 0 ? spawned : new Map(), index === 0 ? null : projectedAgentType(entry.session.agentType ?? CUSTOM_AGENT), read, gate, coverage);
431
+ if (transcript === null)
432
+ return null;
433
+ transcripts.push(transcript);
434
+ }
435
+ const durations = mainTools.flatMap(({ durationMs }) => (durationMs === null ? [] : [durationMs]));
436
+ return { format: SKELETON_FORMAT, vocabVersion: VOCAB_VERSION, fieldsVersion: FIELD_LIST_VERSION, id, transcripts, timingPrint: durations.length < MIN_TIMED_CALLS ? null : print(durations), deltas: [...deltas] };
437
+ };
438
+ /**
439
+ * The version bucket of a session that recorded none: Cursor, Gemini, and Grok Build keep no harness version in their local files, and neither does a VS Code Copilot Chat session whose requests carry no `agent.extensionVersion`, so their cells are
440
+ * `<harness>/unknown/<model>`. It is the one non-numeric word a bundle can be asked for, so those harnesses are served the cells they have.
441
+ */
442
+ export const UNKNOWN_VERSION = 'unknown';
443
+ /** A harness version a bundle can be asked for: public, so the request carries nothing private, or `unknown` for a harness with none on record. The admitted list replaces this shape rule when it ships. */
444
+ export const isBundleVersion = (version) => version === UNKNOWN_VERSION || (version !== LITERAL && publicVersion(version) === version);
445
+ /** Whether `name` is a harness the schema names. */
446
+ export const isHarness = (name) => HARNESSES.some((harness) => harness === name);
447
+ class Refused extends Error {
448
+ field;
449
+ reason;
450
+ constructor(field, reason) {
451
+ super(`${field}: ${reason}`);
452
+ this.field = field;
453
+ this.reason = reason;
454
+ }
455
+ }
456
+ const isFields = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
457
+ const under = (at, key) => (at === '' ? key : `${at}.${key}`);
458
+ /** The value a reader gives an enumerated field whose value it does not know (conclusion section 17, rule 3). */
459
+ export const OTHER = 'other';
460
+ /** The one way into the bag: an entry over `MAX_EXTENSION_BYTES` (as JSON, in UTF-8) is refused as too large at its path, whatever kind it is and whatever the deposit's own cap is. */
461
+ const bagged = (held, entry) => {
462
+ if (new TextEncoder().encode(JSON.stringify(entry.value)).length > MAX_EXTENSION_BYTES)
463
+ throw new Refused(entry.at, 'too large');
464
+ held.extensions.push(entry);
465
+ };
466
+ /**
467
+ * A value no vocabulary of this reader holds: counted, kept as sent in the bag, and read as `other`. The bag is the stored form of what the reader does not serve; the client's projection is the gate on what it
468
+ * sends (lead 194), so the pool keeps the value and serves nothing from it.
469
+ */
470
+ const unknownValue = (held, at, sent) => {
471
+ held.others += 1;
472
+ bagged(held, { at, kind: 'value', value: sent });
473
+ return OTHER;
474
+ };
475
+ /** The fields a client may send a literal beside (`<field>Literal`) when the field itself says `other`: the enumerated fields, whose values a closed list holds. */
476
+ export const ENUMERATED_FIELDS = new Set(['harness', 'harnessVersion', 'model', 'effort', 'platform', 'shell', 'size', 'languages', 'frameworks', 'language', 'languageShare', 'cls', 'status', 'action', 'native', 'agentType', 'program', 'sub', 'flags', 'registry', 'name', 'version', 'server', 'tool', 'skill', 'host', 'deltaKind', 'field', 'metric', 'intervention']);
477
+ /**
478
+ * The fields that may be absent at format 3, by the level of the batch that holds them. An absent one reads as null, or as empty for a list, exactly as a sent null does, so a batch with all of them left out is
479
+ * served from what remains. A field the cells need to be sound (a harness, a status, a bucket of tokens) is never here.
480
+ */
481
+ export const OPTIONAL = {
482
+ batch: ['timingPrint', 'deltas'],
483
+ frame: ['harnessVersion', 'model', 'effort', 'day', 'shell', 'reach', 'repoKey', 'orgSize', 'team', 'repoVisibility', 'hour'],
484
+ stack: ['size', 'languageShares'],
485
+ prompt: ['wallSec', 'explore', 'usageResponses', 'intervention'],
486
+ tool: ['prompt', 'durationMs', 'errorCode', 'recoveryCalls', 'keys', 'keysDropped', 'targetClass', 'argClass'],
487
+ step: ['tokens', 'ms', 'allocated', 'parent', 'skill', 'edges', 'remainingTokens'],
488
+ transcript: ['spawnedIn'],
489
+ };
490
+ /** The most bytes (as JSON) one field this reader does not know, or one literal, may hold: an enumerated value or a literal never needs more, and a newer client's larger structure belongs in a new format version. The deposit's own cap is no part of this. */
491
+ export const MAX_EXTENSION_BYTES = 8 * 1024;
492
+ const LITERAL_SUFFIX = 'Literal';
493
+ const FIELD_NAME = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
494
+ /**
495
+ * The object at `at`, holding every field of `known` and, of the rest, any of `optional`. A name that is neither is not refused: it goes to the bag as it came (rule 3), so a newer client loses nothing
496
+ * against this reader. The emitter is what keeps a class A field out of a batch, not this check. A shape error still refuses: a name that is no field name, a missing field, a literal that is not text.
497
+ */
498
+ const fieldsOf = (held, value, at, known, optional = []) => {
499
+ if (!isFields(value))
500
+ throw new Refused(at === '' ? 'skeleton' : at, 'not an object');
501
+ for (const key of known)
502
+ if (!Object.hasOwn(value, key))
503
+ throw new Refused(under(at, key), 'missing');
504
+ for (const key of Object.keys(value)) {
505
+ if (known.includes(key) || optional.includes(key))
506
+ continue;
507
+ if (!FIELD_NAME.test(key))
508
+ throw new Refused(under(at, key), 'not a field name');
509
+ const entry = value[key];
510
+ const stem = key.endsWith(LITERAL_SUFFIX) ? key.slice(0, -LITERAL_SUFFIX.length) : undefined;
511
+ // A literal sits beside an `other` and nowhere else; beside a value this reader knows it is a field it does not know.
512
+ const literal = stem !== undefined && ENUMERATED_FIELDS.has(stem) && (known.includes(stem) || optional.includes(stem)) && value[stem] === OTHER;
513
+ if (literal)
514
+ textOf(entry, under(at, key));
515
+ bagged(held, { at: under(at, key), kind: literal ? 'literal' : 'field', value: entry });
516
+ }
517
+ return value;
518
+ };
519
+ const listOf = (value, at, item) => {
520
+ if (!Array.isArray(value))
521
+ throw new Refused(at, 'not a list');
522
+ const entries = value;
523
+ return entries.map((entry, index) => item(entry, `${at}[${String(index)}]`));
524
+ };
525
+ const textOf = (value, at) => {
526
+ if (typeof value !== 'string')
527
+ throw new Refused(at, 'not text');
528
+ return value;
529
+ };
530
+ /** 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. */
531
+ const countOf = (value, at) => {
532
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0)
533
+ throw new Refused(at, 'not a count');
534
+ return value;
535
+ };
536
+ /** The allocation marker is `true` or absent: a depositor cannot say `false`, since an unmarked step is the measured one. */
537
+ const allocatedOf = (value, at) => {
538
+ if (value !== true)
539
+ throw new Refused(at, 'not true');
540
+ return true;
541
+ };
542
+ /** An absent field reads as null: an optional field is absent or null alike. */
543
+ const orNull = (value, at, item) => (value === null || value === undefined ? null : item(value, at));
544
+ /** An absent list reads as empty. */
545
+ const listOrEmpty = (value, at, item) => (value === undefined ? [] : listOf(value, at, item));
546
+ const booleanOf = (value, at) => {
547
+ if (typeof value !== 'boolean')
548
+ throw new Refused(at, 'not a boolean');
549
+ return value;
550
+ };
551
+ /** A value from a closed list, or `other` for a text the list does not hold; the list narrows the type, so no caller needs a cast. A value that is no text is a shape error. */
552
+ const oneOf = (held, list, value, at) => {
553
+ const name = textOf(value, at);
554
+ return list.find((entry) => entry === name) ?? (name === OTHER ? OTHER : unknownValue(held, at, name));
555
+ };
556
+ const inSet = (held, allowed, value, at) => {
557
+ const name = textOf(value, at);
558
+ return name === PRIVATE || name === OTHER || allowed.has(name) ? name : unknownValue(held, at, name);
559
+ };
560
+ /** A long-tail name field: `private`, `other`, or a name (the pool assigns its class, `packages/pool/src/field-classes.ts`). A value that is no name is refused: a client that sends one has not run its gate. */
561
+ const nameIn = (value, at, hasShape = isWireName) => {
562
+ const name = textOf(value, at);
563
+ if (name === PRIVATE || name === OTHER || hasShape(name))
564
+ return name;
565
+ throw new Refused(at, 'not a name');
566
+ };
567
+ /** A model or harness version: null, or a long-tail name (`private`, `other`, the older client's literal, or a name). A value that is no name is set aside, as a native name is. */
568
+ const nameOrNull = (held, value, at) => {
569
+ if (value === null || value === undefined)
570
+ return null;
571
+ const name = textOf(value, at);
572
+ return name === LITERAL ? name : nameValue(held, name, at);
573
+ };
574
+ /** A long-tail name in a field whose value the pool classes: `private`, `other`, or a name; a value that is no name is set aside. */
575
+ const nameValue = (held, name, at) => (name === PRIVATE || name === OTHER || isWireName(name) ? name : unknownValue(held, at, name));
576
+ const exactlyIn = (held, allowed) => (value, at) => {
577
+ const name = textOf(value, at);
578
+ return allowed.has(name) || name === OTHER ? name : unknownValue(held, at, name);
579
+ };
580
+ /** `publicEffort` 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. */
581
+ const heldTo = (held, isHeld, value, at) => {
582
+ if (value === null || value === undefined)
583
+ return null;
584
+ const name = textOf(value, at);
585
+ return name === OTHER || isHeld(name) === name ? name : unknownValue(held, at, name);
586
+ };
587
+ // `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.
588
+ const allHarnesses = (list) => list;
589
+ export const HARNESSES = allHarnesses(['claude-code', 'cursor', 'codex', 'copilot', 'gemini', 'grok', 'opencode', 'pi', 'vscode-copilot', 'otel']);
590
+ const PROMPT_CLASSES = ['no-tools', 'orchestration', 'research', 'edit-verify', 'edit-only', 'explore-only', 'shell-only', 'mixed'];
591
+ const STATUSES = ['ok', 'error', 'none'];
592
+ const PLATFORMS = new Set(['aix', 'android', 'cygwin', 'darwin', 'freebsd', 'haiku', 'linux', 'netbsd', 'openbsd', 'sunos', 'win32']);
593
+ const SHELLS = new Set(Object.values(ACTION_TABLES).flatMap((table) => Object.values(table.shells)));
594
+ /** The registries a package comes from; a pull may also come from a skill, plugin, or MCP registry. */
595
+ export const PACKAGE_REGISTRIES = ['npm', 'pypi', 'crates', 'go', 'rubygems', 'homebrew'];
596
+ const NO_NAME = new Set();
597
+ const REGISTRIES = [...PACKAGE_REGISTRIES, 'skill', 'plugin', 'mcp'];
598
+ const DAY = /^\d{4}-\d{2}-\d{2}$/;
599
+ const ID = /^[0-9a-f]{16}$/;
600
+ /** 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. */
601
+ const isCalendarDay = (text) => {
602
+ if (!DAY.test(text))
603
+ return false;
604
+ const parsed = Date.parse(text);
605
+ return !Number.isNaN(parsed) && new Date(parsed).toISOString().slice(0, 10) === text;
606
+ };
607
+ /** 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. */
608
+ const SHARE_LANGUAGES = new Set([...LANGUAGES, 'other']);
609
+ const parseLanguageShare = (held, value, at) => {
610
+ const raw = fieldsOf(held, value, at, ['language', 'languageShare']);
611
+ return { language: exactlyIn(held, SHARE_LANGUAGES)(raw.language, under(at, 'language')), languageShare: oneOf(held, LANGUAGE_SHARES, raw.languageShare, under(at, 'languageShare')) };
612
+ };
613
+ /** A reach, each list held to its own row; an unfilled reach lists nothing. A plugin has no pinned list, so only the word `private` stands for one. */
614
+ const parseReach = (held, value, at) => {
615
+ const raw = fieldsOf(held, value, at, ['filled', 'mcp', 'skills', 'plugins']);
616
+ const filled = booleanOf(raw.filled, under(at, 'filled'));
617
+ const reach = {
618
+ filled,
619
+ mcp: listOf(raw.mcp, under(at, 'mcp'), (entry, entryAt) => nameIn(entry, entryAt)),
620
+ skills: listOf(raw.skills, under(at, 'skills'), (entry, entryAt) => nameIn(entry, entryAt)),
621
+ plugins: listOf(raw.plugins, under(at, 'plugins'), (entry, entryAt) => inSet(held, NO_NAME, entry, entryAt)),
622
+ };
623
+ if (!filled && reach.mcp.length + reach.skills.length + reach.plugins.length > 0)
624
+ throw new Refused(at, 'an unfilled reach lists nothing');
625
+ return reach;
626
+ };
627
+ /** Sixteen lowercase hex characters and nothing else, so no path or remote text can ride in a repo key. */
628
+ const parseRepoKey = (value, at) => {
629
+ const text = textOf(value, at);
630
+ if (!REPO_KEY.test(text))
631
+ throw new Refused(at, 'outside the vocabulary');
632
+ return text;
633
+ };
634
+ const parseHour = (value, at) => {
635
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 0 || value > 23)
636
+ throw new Refused(at, 'not an hour');
637
+ return value;
638
+ };
639
+ const parseFrame = (held, value, at) => {
640
+ const raw = fieldsOf(held, value, at, ['harness', 'platform', 'stack'], OPTIONAL.frame);
641
+ const stackAt = under(at, 'stack');
642
+ const stack = fieldsOf(held, raw.stack, stackAt, ['languages', 'frameworks'], OPTIONAL.stack);
643
+ const day = orNull(raw.day, under(at, 'day'), (entry, entryAt) => {
644
+ const text = textOf(entry, entryAt);
645
+ if (!isCalendarDay(text))
646
+ throw new Refused(entryAt, 'not a UTC day');
647
+ return text;
648
+ });
649
+ return {
650
+ harness: oneOf(held, HARNESSES, raw.harness, under(at, 'harness')),
651
+ harnessVersion: nameOrNull(held, raw.harnessVersion, under(at, 'harnessVersion')),
652
+ model: nameOrNull(held, raw.model, under(at, 'model')),
653
+ effort: heldTo(held, publicEffort, raw.effort, under(at, 'effort')),
654
+ day,
655
+ platform: exactlyIn(held, PLATFORMS)(raw.platform, under(at, 'platform')),
656
+ shell: orNull(raw.shell, under(at, 'shell'), exactlyIn(held, SHELLS)),
657
+ stack: {
658
+ languages: listOf(stack.languages, under(stackAt, 'languages'), exactlyIn(held, LANGUAGES)),
659
+ frameworks: listOf(stack.frameworks, under(stackAt, 'frameworks'), exactlyIn(held, FRAMEWORKS)),
660
+ size: orNull(stack.size, under(stackAt, 'size'), (entry, entryAt) => oneOf(held, REPO_SIZES, entry, entryAt)),
661
+ languageShares: listOrEmpty(stack.languageShares, under(stackAt, 'languageShares'), (entry, entryAt) => parseLanguageShare(held, entry, entryAt)),
662
+ },
663
+ ...(raw.reach === undefined ? {} : { reach: parseReach(held, raw.reach, under(at, 'reach')) }),
664
+ ...(raw.repoKey === undefined ? {} : { repoKey: parseRepoKey(raw.repoKey, under(at, 'repoKey')) }),
665
+ ...(raw.orgSize === undefined ? {} : { orgSize: oneOf(held, ORG_SIZES, raw.orgSize, under(at, 'orgSize')) }),
666
+ ...(raw.team === undefined ? {} : { team: oneOf(held, TEAMS, raw.team, under(at, 'team')) }),
667
+ ...(raw.repoVisibility === undefined ? {} : { repoVisibility: oneOf(held, REPO_VISIBILITIES, raw.repoVisibility, under(at, 'repoVisibility')) }),
668
+ ...(raw.hour === undefined ? {} : { hour: parseHour(raw.hour, under(at, 'hour')) }),
669
+ };
670
+ };
671
+ const bucketIn = (kind, value, at) => {
672
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 0 || value > EDGES[kind].length)
673
+ throw new Refused(at, 'not a bucket');
674
+ return value;
675
+ };
676
+ const parseUsage = (held, value, at) => {
677
+ const raw = fieldsOf(held, value, at, ['input', 'cacheWrite', 'cacheRead', 'output', 'thinking']);
678
+ const bucket = (field) => orNull(raw[field], under(at, field), (entry, entryAt) => bucketIn('tokens', entry, entryAt));
679
+ return { input: bucket('input'), cacheWrite: bucket('cacheWrite'), cacheRead: bucket('cacheRead'), output: bucket('output'), thinking: bucket('thinking') };
680
+ };
681
+ const parsePull = (held, value, at) => {
682
+ const raw = fieldsOf(held, value, at, ['registry', 'name', 'version']);
683
+ const name = nameIn(raw.name, under(at, 'name'), isWirePullName);
684
+ const version = orNull(raw.version, under(at, 'version'), (entry, entryAt) => {
685
+ const text = textOf(entry, entryAt);
686
+ // A version beside a masked name could identify it: capture masks the two together.
687
+ if (!PUBLIC_PACKAGES.has(name) && text !== LITERAL)
688
+ throw new Refused(entryAt, 'a name off the public list holds no version');
689
+ return text === LITERAL || text === OTHER || isRegistryVersion(text) ? text : unknownValue(held, entryAt, text);
690
+ });
691
+ return { registry: oneOf(held, REGISTRIES, raw.registry, under(at, 'registry')), name, version };
692
+ };
693
+ const parseFlag = (held, value, at) => {
694
+ const flag = textOf(value, at);
695
+ return flag === PRIVATE || flag === OTHER || isWireFlag(flag) ? flag : unknownValue(held, at, flag);
696
+ };
697
+ /** What a reader sets aside whole: a part of a step it cannot read against a vocabulary. Its place is noted and its content kept as sent, as for any value it does not know (see `unknownValue`). */
698
+ const setAside = (held, at, sent) => {
699
+ bagged(held, { at, kind: 'value', value: sent });
700
+ };
701
+ const parseIdentity = (held, harness, action, value, at) => {
702
+ if (value === null)
703
+ return null;
704
+ // An action this reader does not know holds an identity it cannot read: the step goes without one and the place of what it held is noted.
705
+ if (action === OTHER) {
706
+ setAside(held, at, value);
707
+ return null;
708
+ }
709
+ if (action === 'shell.exec') {
710
+ const raw = fieldsOf(held, value, at, ['program', 'sub', 'flags'], ['pulls']);
711
+ // A program is a name, `private`, or `other`; only `other` holds no subcommand, flag, or pull here.
712
+ const program = textOf(raw.program, under(at, 'program'));
713
+ nameIn(program, under(at, 'program'));
714
+ // The subcommands, flags, and pulls of a program this reader does not know cannot be read against its lists: it takes none, and notes what it left.
715
+ if (program === OTHER) {
716
+ for (const key of ['sub', 'flags', 'pulls'])
717
+ if (raw[key] !== undefined && !(raw[key] === null || (Array.isArray(raw[key]) && raw[key].length === 0)))
718
+ setAside(held, under(at, key), raw[key]);
719
+ return { program: OTHER, sub: null, flags: [] };
720
+ }
721
+ if (program === PRIVATE) {
722
+ if (!(Array.isArray(raw.flags) && raw.flags.length === 0))
723
+ throw new Refused(under(at, 'flags'), 'a private program holds no flags');
724
+ if (raw.pulls !== undefined)
725
+ throw new Refused(under(at, 'pulls'), 'a private program holds no pulls');
726
+ }
727
+ // A private program holds no subcommand, so one it sends is set aside, as its flags are refused.
728
+ const sub = orNull(raw.sub, under(at, 'sub'), (entry, entryAt) => (program === PRIVATE ? unknownValue(held, entryAt, textOf(entry, entryAt)) : nameValue(held, textOf(entry, entryAt), entryAt)));
729
+ const flags = listOf(raw.flags, under(at, 'flags'), (entry, entryAt) => parseFlag(held, entry, entryAt));
730
+ return raw.pulls === undefined ? { program, sub, flags } : { program, sub, flags, pulls: listOf(raw.pulls, under(at, 'pulls'), (entry, entryAt) => parsePull(held, entry, entryAt)) };
731
+ }
732
+ if (action === 'mcp.call') {
733
+ const raw = fieldsOf(held, value, at, ['server', 'tool']);
734
+ const server = nameIn(raw.server, under(at, 'server'));
735
+ const tool = textOf(raw.tool, under(at, 'tool'));
736
+ // The same rule capture applies (`callIdentity`), so a name it would have masked is refused here rather than stored.
737
+ if (harness === OTHER) {
738
+ // The name crosses as the literal, and what the client sent is kept in the bag.
739
+ if (tool !== OTHER && tool !== LITERAL)
740
+ setAside(held, under(at, 'tool'), tool);
741
+ return { server, tool: tool === OTHER ? tool : LITERAL };
742
+ }
743
+ const admitted = tool === LITERAL || tool === OTHER || (server !== PRIVATE && ACTION_TABLES[harness].toolDelimited && (isPublicMcpTool(server, tool) || isWireName(tool)));
744
+ return { server, tool: admitted ? tool : unknownValue(held, under(at, 'tool'), tool) };
745
+ }
746
+ if (action === 'skill.run')
747
+ return { skill: nameIn(fieldsOf(held, value, at, ['skill']).skill, under(at, 'skill')) };
748
+ if (action === 'web.fetch' || action === 'web.search') {
749
+ const { host } = fieldsOf(held, value, at, ['host']);
750
+ return { host: orNull(host, under(at, 'host'), (entry, entryAt) => nameIn(entry, entryAt)) };
751
+ }
752
+ throw new Refused(at, 'this action has no identity');
753
+ };
754
+ /** Whether the harness's table names this native, so it can cross as itself; anything else (including the literal) crosses as `private`. */
755
+ const admittedNative = (harness, native) => {
756
+ if (native === PRIVATE)
757
+ return true;
758
+ if (harness === OTHER)
759
+ return false;
760
+ const table = ACTION_TABLES[harness];
761
+ if (Object.hasOwn(table.rows, native))
762
+ return true;
763
+ const mcp = mcpOf(table, native);
764
+ return mcp !== undefined && table.toolDelimited && isPublicMcpTool(mcp.server, mcp.tool);
765
+ };
766
+ const parseNative = (held, harness, value, at) => {
767
+ const native = textOf(value, at);
768
+ // The tools of a harness this reader does not know are off any table it holds, so they cross as `private`, the way capture masks a name its table lacks.
769
+ if (harness === OTHER)
770
+ return native === OTHER || isWireName(native) ? native : unknownValue(held, at, native);
771
+ return native === OTHER || admittedNative(harness, native) || isWireName(native) ? native : unknownValue(held, at, native);
772
+ };
773
+ /** 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. */
774
+ const parseAgentType = (held, value, at) => {
775
+ const name = textOf(value, at);
776
+ return name === OTHER || projectedAgentType(name) === name ? name : unknownValue(held, at, name);
777
+ };
778
+ const parseEdge = (held, value, at) => {
779
+ const raw = fieldsOf(held, value, at, ['kind', 'target']);
780
+ return { kind: oneOf(held, EDGE_KINDS, raw.kind, under(at, 'kind')), target: countOf(raw.target, under(at, 'target')) };
781
+ };
782
+ const parseStep = (held, harness, value, at) => {
783
+ const raw = fieldsOf(held, value, at, ['action', 'native', 'identity', 'status', 'agentType', 'frame'], OPTIONAL.step);
784
+ const action = oneOf(held, CANONICAL_ACTIONS, raw.action, under(at, 'action'));
785
+ return {
786
+ action,
787
+ native: parseNative(held, harness, raw.native, under(at, 'native')),
788
+ identity: parseIdentity(held, harness, action, raw.identity, under(at, 'identity')),
789
+ status: oneOf(held, STATUSES, raw.status, under(at, 'status')),
790
+ tokens: orNull(raw.tokens, under(at, 'tokens'), (entry, entryAt) => bucketIn('tokens', entry, entryAt)),
791
+ ms: orNull(raw.ms, under(at, 'ms'), (entry, entryAt) => bucketIn('ms', entry, entryAt)),
792
+ ...(raw.allocated === undefined ? {} : { allocated: allocatedOf(raw.allocated, under(at, 'allocated')) }),
793
+ remainingTokens: orNull(raw.remainingTokens, under(at, 'remainingTokens'), (entry, entryAt) => bucketIn('tokens', entry, entryAt)),
794
+ agentType: orNull(raw.agentType, under(at, 'agentType'), (entry, entryAt) => parseAgentType(held, entry, entryAt)),
795
+ parent: orNull(raw.parent, under(at, 'parent'), countOf),
796
+ frame: countOf(raw.frame, under(at, 'frame')),
797
+ ...(raw.edges === undefined ? {} : { edges: listOf(raw.edges, under(at, 'edges'), (entry, entryAt) => parseEdge(held, entry, entryAt)) }),
798
+ ...(raw.skill === undefined ? {} : { skill: nameIn(raw.skill, under(at, 'skill')) }),
799
+ };
800
+ };
801
+ /** A call's `errorCode`: absent or null is none; otherwise a member of the closed list (a value off it reads as `other` and goes to the bag), and only on a call that did not succeed or sit out. */
802
+ const parseErrorCode = (held, status, value, at) => {
803
+ if (value === undefined || value === null)
804
+ return null;
805
+ const code = exactlyIn(held, ERROR_CODES)(value, at);
806
+ if (status === 'ok' || status === 'none')
807
+ throw new Refused(at, 'only a failed call has one');
808
+ return code;
809
+ };
810
+ /** A call's `recoveryCalls`: absent is none; otherwise at most 5 actions (a value off the list reads as `other`) and a flag, and only on a call that did not succeed or sit out; the transcript parse then refuses one on a call whose status is not `error`. */
811
+ const parseRecovery = (held, status, value, at) => {
812
+ if (value === undefined)
813
+ return undefined;
814
+ if (status === 'ok' || status === 'none')
815
+ throw new Refused(at, 'only a failed call has one');
816
+ const raw = fieldsOf(held, value, at, ['next', 'editFollowed']);
817
+ const next = listOf(raw.next, under(at, 'next'), (entry, entryAt) => oneOf(held, CANONICAL_ACTIONS, entry, entryAt));
818
+ if (next.length > RECOVERY_CALLS)
819
+ throw new Refused(under(at, 'next'), `more than ${String(RECOVERY_CALLS)} calls`);
820
+ return { next, editFollowed: booleanOf(raw.editFollowed, under(at, 'editFollowed')) };
821
+ };
822
+ /**
823
+ * A call's `keys`: absent is none. A tool with a pinned input list sends names on it, and a name off it reads as `other` and goes to the bag; any other tool's names must have the shape of a name. Key names
824
+ * only, so a value that is no name shape is not a key.
825
+ */
826
+ const parseKeys = (held, native, value, at) => {
827
+ if (value === undefined)
828
+ return undefined;
829
+ const listed = native === undefined ? undefined : TOOL_INPUT_KEYS.get(native);
830
+ return listOf(value, at, (entry, entryAt) => {
831
+ const name = textOf(entry, entryAt);
832
+ return (listed === undefined ? isWireName(name) : listed.has(name)) ? name : unknownValue(held, entryAt, name);
833
+ });
834
+ };
835
+ const parseTool = (held, harness, value, at) => {
836
+ const raw = fieldsOf(held, value, at, ['side', 'status', 'steps'], OPTIONAL.tool);
837
+ const status = oneOf(held, STATUSES, raw.status, under(at, 'status'));
838
+ const errorCode = parseErrorCode(held, status, raw.errorCode, under(at, 'errorCode'));
839
+ const recoveryCalls = parseRecovery(held, status, raw.recoveryCalls, under(at, 'recoveryCalls'));
840
+ const steps = listOf(raw.steps, under(at, 'steps'), (entry, entryAt) => parseStep(held, harness, entry, entryAt));
841
+ const keys = parseKeys(held, steps[0]?.native, raw.keys, under(at, 'keys'));
842
+ const keysDropped = raw.keysDropped === undefined ? undefined : bucketIn('count', raw.keysDropped, under(at, 'keysDropped'));
843
+ const targetClass = raw.targetClass === undefined ? undefined : oneOf(held, TARGET_CLASSES, raw.targetClass, under(at, 'targetClass'));
844
+ const argClass = raw.argClass === undefined ? undefined : listOf(raw.argClass, under(at, 'argClass'), (entry, entryAt) => oneOf(held, ARG_CLASSES, entry, entryAt));
845
+ return {
846
+ side: booleanOf(raw.side, under(at, 'side')),
847
+ prompt: orNull(raw.prompt, under(at, 'prompt'), countOf),
848
+ status,
849
+ durationMs: orNull(raw.durationMs, under(at, 'durationMs'), countOf),
850
+ errorCode,
851
+ ...(recoveryCalls === undefined ? {} : { recoveryCalls }),
852
+ ...(keys === undefined ? {} : { keys }),
853
+ ...(keysDropped === undefined ? {} : { keysDropped }),
854
+ ...(targetClass === undefined ? {} : { targetClass }),
855
+ ...(argClass === undefined ? {} : { argClass }),
856
+ steps,
857
+ };
858
+ };
859
+ const parsePrompt = (held, value, at) => {
860
+ const raw = fieldsOf(held, value, at, ['cls', 'errors', 'usage', 'frame'], OPTIONAL.prompt);
861
+ return {
862
+ cls: oneOf(held, PROMPT_CLASSES, raw.cls, under(at, 'cls')),
863
+ errors: bucketIn('count', raw.errors, under(at, 'errors')),
864
+ wallSec: orNull(raw.wallSec, under(at, 'wallSec'), (entry, entryAt) => bucketIn('seconds', entry, entryAt)),
865
+ usage: parseUsage(held, raw.usage, under(at, 'usage')),
866
+ explore: orNull(raw.explore, under(at, 'explore'), (entry, entryAt) => bucketIn('calls', entry, entryAt)),
867
+ ...(raw.usageResponses === undefined ? {} : { usageResponses: bucketIn('count', raw.usageResponses, under(at, 'usageResponses')) }),
868
+ // A sent null reads as an absent field, the way every optional field does.
869
+ ...(raw.intervention === undefined || raw.intervention === null ? {} : { intervention: oneOf(held, PROMPT_INTERVENTIONS, raw.intervention, under(at, 'intervention')) }),
870
+ frame: parseFrame(held, raw.frame, under(at, 'frame')),
871
+ };
872
+ };
873
+ const ROOT = /^[0-9a-f]{64}$/;
874
+ const PRINT = /^[0-9a-f]{16}$/;
875
+ const parseRoot = (value, at) => {
876
+ const root = textOf(value, at);
877
+ if (!ROOT.test(root))
878
+ throw new Refused(at, 'not a SHA-256');
879
+ return root;
880
+ };
881
+ const parseTranscript = (held, value, at) => {
882
+ const raw = fieldsOf(held, value, at, ['merkleRoot', 'leaves', 'harness', 'frame', 'frames', 'prompts', 'tools'], OPTIONAL.transcript);
883
+ const harness = oneOf(held, HARNESSES, raw.harness, under(at, 'harness'));
884
+ const merkleRoot = parseRoot(raw.merkleRoot, under(at, 'merkleRoot'));
885
+ const leaves = countOf(raw.leaves, under(at, 'leaves'));
886
+ // Read in the order the fields are met, so the bag holds what was set aside in that order.
887
+ const frame = parseFrame(held, raw.frame, under(at, 'frame'));
888
+ const frames = listOf(raw.frames, under(at, 'frames'), (entry, entryAt) => parseFrame(held, entry, entryAt));
889
+ // Absent reads as null: a client that names no spawner sends none.
890
+ const spawnedIn = orNull(raw.spawnedIn, under(at, 'spawnedIn'), countOf);
891
+ const prompts = listOf(raw.prompts, under(at, 'prompts'), (entry, entryAt) => parsePrompt(held, entry, entryAt));
892
+ const tools = listOf(raw.tools, under(at, 'tools'), (entry, entryAt) => parseTool(held, harness, entry, entryAt));
893
+ // What followed a call is read off the calls themselves, so a list that disagrees with them is a wrong deposit, not a new reading.
894
+ tools.forEach((tool, index) => {
895
+ if (tool.recoveryCalls !== undefined && JSON.stringify(tool.recoveryCalls) !== JSON.stringify(recoveryCallsOf(tools, index)))
896
+ throw new Refused(`${at}.tools[${String(index)}].recoveryCalls`, 'not what followed the call');
897
+ });
898
+ return { merkleRoot, leaves, harness, frame, frames, spawnedIn, prompts, tools };
899
+ };
900
+ const DELTA_FIELDS = ['plugin', 'package', 'permissionMode', 'effort'];
901
+ /** 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. */
902
+ const KINDS_OF = { plugin: ['added', 'removed'], package: ['removed'], permissionMode: ['changed'], effort: ['changed'] };
903
+ /** The one metric each field's change is measured in, and the bucket edges that metric is held to. */
904
+ const METRIC_OF = { plugin: 'callsPerSession', package: 'callsPerSession', permissionMode: 'callsPerSession', effort: 'thinking' };
905
+ const METRIC_EDGES = { callsPerSession: 'count', thinking: 'tokens' };
906
+ const ALL_KINDS = ['added', 'removed', 'changed'];
907
+ const ALL_METRICS = ['callsPerSession', 'thinking'];
908
+ const nullOnly = (value, at) => {
909
+ if (value !== null)
910
+ throw new Refused(at, 'outside the vocabulary');
911
+ return null;
912
+ };
913
+ /**
914
+ * 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
915
+ * and an effort are named by their field and carry no name. The change is a signed bucket of the field's metric.
916
+ */
917
+ const parseDelta = (held, value, at) => {
918
+ const raw = fieldsOf(held, value, at, ['deltaKind', 'field', 'name', 'registry', 'metric', 'change']);
919
+ const field = oneOf(held, DELTA_FIELDS, raw.field, under(at, 'field'));
920
+ // A field this reader does not know has no row to hold its kind, metric, name, or registry to: the first two read against every kind and metric, and the last two are kept as sent.
921
+ const kinds = field === OTHER ? ALL_KINDS : KINDS_OF[field];
922
+ const deltaKind = oneOf(held, kinds, raw.deltaKind, under(at, 'deltaKind'));
923
+ const metric = oneOf(held, field === OTHER ? ALL_METRICS : [METRIC_OF[field]], raw.metric, under(at, 'metric'));
924
+ const named = field === 'plugin' || field === 'package';
925
+ if (field === OTHER) {
926
+ for (const key of ['name', 'registry'])
927
+ if (raw[key] !== null)
928
+ setAside(held, under(at, key), raw[key]);
929
+ }
930
+ const name = named ? inSet(held, field === 'package' ? PUBLIC_PACKAGES : NO_NAME, raw.name, under(at, 'name')) : field === OTHER ? null : nullOnly(raw.name, under(at, 'name'));
931
+ const registry = field === 'package' && name !== PRIVATE ? oneOf(held, PACKAGE_REGISTRIES, raw.registry, under(at, 'registry')) : field === OTHER ? null : nullOnly(raw.registry, under(at, 'registry'));
932
+ const change = raw.change;
933
+ const limit = metric === OTHER ? Math.max(...ALL_METRICS.map((kind) => EDGES[METRIC_EDGES[kind]].length)) : EDGES[METRIC_EDGES[metric]].length;
934
+ if (typeof change !== 'number' || !Number.isInteger(change) || Math.abs(change) > limit)
935
+ throw new Refused(under(at, 'change'), 'not a signed bucket');
936
+ return { deltaKind, field, name, registry, metric, change };
937
+ };
938
+ const VOCAB_TEXT = /^[0-9A-Za-z._-]{1,32}$/;
939
+ /** The skeleton in `value`, rebuilt from the fields read, with the bag of what was set aside; else the first field that is a shape error, and why. */
940
+ export const parseSkeleton = (value) => {
941
+ const held = { extensions: [], others: 0 };
942
+ try {
943
+ // The earlier forms are named, since their fields would otherwise read as a list of strangers; any other format is a form this reader does not know.
944
+ if (isFields(value) && typeof value.format === 'number' && value.format < SKELETON_FORMAT)
945
+ throw new Refused('format', `the format ${String(value.format)} form is no longer read; send format ${String(SKELETON_FORMAT)}`);
946
+ const raw = fieldsOf(held, value, '', ['format', 'vocabVersion', 'fieldsVersion', 'id', 'transcripts'], OPTIONAL.batch);
947
+ if (raw.format !== SKELETON_FORMAT)
948
+ throw new Refused('format', 'not a format this version reads');
949
+ const vocabVersion = textOf(raw.vocabVersion, 'vocabVersion');
950
+ if (!VOCAB_TEXT.test(vocabVersion))
951
+ throw new Refused('vocabVersion', 'not a version');
952
+ const fieldsVersion = countOf(raw.fieldsVersion, 'fieldsVersion');
953
+ const id = textOf(raw.id, 'id');
954
+ if (!ID.test(id))
955
+ throw new Refused('id', 'not a hash');
956
+ const deltas = listOrEmpty(raw.deltas, 'deltas', (entry, entryAt) => parseDelta(held, entry, entryAt));
957
+ const transcripts = listOf(raw.transcripts, 'transcripts', (entry, entryAt) => parseTranscript(held, entry, entryAt));
958
+ if (transcripts.length === 0)
959
+ throw new Refused('transcripts', 'empty');
960
+ // A counting batch is what lifts the fetch cap, so a deposit has to hold something to derive from.
961
+ if (transcripts.every(({ prompts, tools }) => prompts.length === 0 && tools.length === 0))
962
+ throw new Refused('transcripts', 'holds no prompt and no tool');
963
+ // 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.
964
+ const timed = (transcripts[0]?.tools ?? []).filter(({ durationMs }) => durationMs !== null).length;
965
+ const timingPrint = orNull(raw.timingPrint, 'timingPrint', (entry, entryAt) => {
966
+ const text = textOf(entry, entryAt);
967
+ if (!PRINT.test(text))
968
+ throw new Refused(entryAt, 'not a hash');
969
+ return text;
970
+ });
971
+ if ((timingPrint === null) !== (timed < MIN_TIMED_CALLS))
972
+ throw new Refused('timingPrint', timingPrint === null ? 'missing: the main run holds enough timed calls' : 'not allowed: the main run holds too few timed calls');
973
+ // 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.
974
+ transcripts.forEach(({ tools }, index) => {
975
+ tools.forEach(({ steps }, call) => {
976
+ steps.forEach(({ agentType }, position) => {
977
+ if ((agentType === null) !== (index === 0))
978
+ 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');
979
+ });
980
+ });
981
+ });
982
+ // Positions are the only ordinals on the wire, so each must name something the batch holds.
983
+ // A spawner is another transcript of the batch whose own spawners lead to a run with none, so the batch is a forest and the main run is its first root.
984
+ transcripts.forEach(({ spawnedIn }, index) => {
985
+ if (spawnedIn === null)
986
+ return;
987
+ const spawnedAt = `transcripts[${String(index)}].spawnedIn`;
988
+ if (index === 0)
989
+ throw new Refused(spawnedAt, 'the main run has no spawner');
990
+ if (spawnedIn >= transcripts.length || spawnedIn === index)
991
+ throw new Refused(spawnedAt, 'no such transcript');
992
+ let above = spawnedIn;
993
+ for (let hop = 0; hop < transcripts.length && above !== null; hop += 1)
994
+ above = transcripts[above]?.spawnedIn ?? null;
995
+ if (above !== null)
996
+ throw new Refused(spawnedAt, 'a spawner cycle');
997
+ });
998
+ const stepsOf = transcripts.map(({ tools }) => tools.reduce((all, { steps }) => all + steps.length, 0));
999
+ transcripts.forEach(({ prompts, tools, frames, spawnedIn }, index) => {
1000
+ if (frames.length === 0)
1001
+ throw new Refused(`transcripts[${String(index)}].frames`, 'empty');
1002
+ // The call and prompt of every step in the transcript's order, which an edge's target is a position in.
1003
+ const placed = tools.flatMap(({ prompt, steps }) => steps.map(() => ({ prompt })));
1004
+ let before = 0;
1005
+ tools.forEach(({ prompt, steps }, call) => {
1006
+ const callAt = `transcripts[${String(index)}].tools[${String(call)}]`;
1007
+ // Past the prompts it names a prompt with no record, numbered in order of appearance: no more of those than calls.
1008
+ if (prompt !== null && prompt >= prompts.length + tools.length)
1009
+ throw new Refused(`${callAt}.prompt`, 'no such prompt position');
1010
+ steps.forEach(({ parent, frame, edges }, position) => {
1011
+ const stepAt = `${callAt}.steps[${String(position)}]`;
1012
+ if (parent !== null && (spawnedIn === null || parent >= (transcripts[spawnedIn]?.tools.length ?? 0)))
1013
+ throw new Refused(`${stepAt}.parent`, 'no such call in the spawning transcript');
1014
+ if (frame >= frames.length)
1015
+ throw new Refused(`${stepAt}.frame`, 'no such frame position');
1016
+ const here = before + position;
1017
+ (edges ?? []).forEach(({ kind, target }, edge) => {
1018
+ const edgeAt = `${stepAt}.edges[${String(edge)}]`;
1019
+ if (kind === OTHER)
1020
+ return;
1021
+ if (kind === 'spawned') {
1022
+ if (spawnedIn === null)
1023
+ throw new Refused(`${edgeAt}.kind`, 'a spawned edge needs a spawning transcript');
1024
+ if (target >= (stepsOf[spawnedIn] ?? 0))
1025
+ throw new Refused(`${edgeAt}.target`, 'no such step in the spawning transcript');
1026
+ return;
1027
+ }
1028
+ // The other kinds name an earlier step of the same transcript; a next stays inside one prompt.
1029
+ if (target >= here)
1030
+ throw new Refused(`${edgeAt}.target`, 'not an earlier step of this transcript');
1031
+ if (kind === 'next' && placed[target]?.prompt !== prompt)
1032
+ throw new Refused(`${edgeAt}.target`, 'a next edge crosses a prompt');
1033
+ });
1034
+ });
1035
+ before += steps.length;
1036
+ });
1037
+ });
1038
+ return { ok: true, skeleton: { format: SKELETON_FORMAT, vocabVersion, fieldsVersion, id, transcripts, timingPrint, deltas }, extensions: held.extensions, others: held.others };
1039
+ }
1040
+ catch (error) {
1041
+ if (error instanceof Refused)
1042
+ return { ok: false, field: error.field, reason: error.reason };
1043
+ throw error;
1044
+ }
1045
+ };
1046
+ const isSignedBundle = (value) => isFields(value) && typeof value.payload === 'string' && typeof value.signature === 'string';
1047
+ const isFloor = (value) => typeof value === 'number' && Number.isSafeInteger(value) && value >= 1;
1048
+ /** The floors a bundle states, or none: a client prints what is here, so a value that is not a whole number of at least 1 is refused and not shown, while a family this client does not know is carried (in `--json`) and not printed, since `floorSentence` prints only the families this client knows (additive, section 17 rule 3). */
1049
+ const isFloors = (value) => isFields(value) && isFloor(value.public) && isFloor(value.disclosure) && (value.families === undefined || (isFields(value.families) && Object.values(value.families).every(isFloor)));
1050
+ const isSourced = (value) => isFields(value) && ((value.state === 'value' && typeof value.value === 'number') || (value.state === 'no source yet' && typeof value.reason === 'string'));
1051
+ const isCount = (value) => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
1052
+ /** The set of four a bundle may carry: every field the `books` read prints is present and of its type, with its format, vocabulary version, and the two served lists checked as well, so a signed bundle with a malformed block reads as not a bundle, not as a crash in the printer. */
1053
+ const isBooks = (value) => isFields(value) &&
1054
+ isFloor(value.floor) &&
1055
+ value.format === BUNDLE_FORMAT &&
1056
+ typeof value.vocabVersion === 'string' &&
1057
+ isFields(value.asOf) &&
1058
+ (value.asOf.day === null || typeof value.asOf.day === 'string') &&
1059
+ isFields(value.served) &&
1060
+ isCount(value.served.total) &&
1061
+ Array.isArray(value.served.byWeek) &&
1062
+ Array.isArray(value.served.byTuple) &&
1063
+ isCount(value.backlog) &&
1064
+ isSourced(value.panel) &&
1065
+ (value.panelDay === null || typeof value.panelDay === 'string') &&
1066
+ isFields(value.demand) &&
1067
+ isSourced(value.demand.comparisonReadOwners) &&
1068
+ isSourced(value.demand.followOnReads) &&
1069
+ isFields(value.goal) &&
1070
+ isSourced(value.goal.readYield) &&
1071
+ isSourced(value.goal.costPerServedCell) &&
1072
+ isSourced(value.goal.yieldPerServedCell);
1073
+ // A bundle states the floor it applied (`floor`), and a client cannot tell which floor a bundle without one applied, so a format 1 bundle with no `floor` is refused as not a bundle this version reads.
1074
+ const isBundle = (value) => isFields(value) && value.format === BUNDLE_FORMAT && typeof value.harness === 'string' && isHarness(value.harness) && typeof value.harnessVersion === 'string' && typeof value.vocabVersion === 'string' && typeof value.floor === 'number' && isFields(value.asOf) && Array.isArray(value.cells) && Array.isArray(value.motifs) && Array.isArray(value.reads) && Array.isArray(value.strata) && (value.floors === undefined || isFloors(value.floors)) && (value.books === undefined || isBooks(value.books));
1075
+ const signatureHolds = async ({ payload, signature }, key) => {
1076
+ try {
1077
+ const verifier = await crypto.subtle.importKey('jwk', { kty: key.kty, crv: key.crv, x: key.x }, 'Ed25519', false, ['verify']);
1078
+ return await crypto.subtle.verify('Ed25519', verifier, Uint8Array.from(atob(signature), (char) => char.charCodeAt(0)), new TextEncoder().encode(payload));
1079
+ }
1080
+ catch {
1081
+ return false;
1082
+ }
1083
+ };
1084
+ /** 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. */
1085
+ export const verifyBundle = async (signed, key, harness, harnessVersion) => {
1086
+ if (!isSignedBundle(signed))
1087
+ return { ok: false, reason: 'not a signed bundle' };
1088
+ if (!(await signatureHolds(signed, key)))
1089
+ return { ok: false, reason: 'the signature does not match the key pinned at login' };
1090
+ const bundle = JSON.parse(signed.payload);
1091
+ if (!isBundle(bundle))
1092
+ return { ok: false, reason: 'not a signed bundle this version reads' };
1093
+ if (bundle.vocabVersion !== VOCAB_VERSION)
1094
+ return { ok: false, reason: `this bundle is for vocabulary version ${bundle.vocabVersion}, not ${VOCAB_VERSION}` };
1095
+ if (bundle.harness !== harness)
1096
+ return { ok: false, reason: `this bundle is for harness ${bundle.harness}, not ${harness}` };
1097
+ if (bundle.harnessVersion !== harnessVersion)
1098
+ return { ok: false, reason: `this bundle is for harness version ${bundle.harnessVersion}, not ${harnessVersion}` };
1099
+ return { ok: true, bundle };
1100
+ };