@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,313 @@
1
+ // The outbox (`oars pool deposit` and `oars pool outbox`): every batch that may leave this machine is first written here, as canonical JSON lines, and `deposit` stops. Nothing in this file
2
+ // looks inside a batch except to name its fields, at every depth, for `--explain`; what a batch holds is the projection's business.
3
+ import { createHash } from 'node:crypto';
4
+ import { appendFile, mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';
5
+ import { join } from 'node:path';
6
+ import { SKELETON_FORMAT, VOCAB_VERSION } from './pool-wire.js';
7
+ import { contentHash } from './read.js';
8
+ /** What `JSON.stringify` writes for a value, or undefined for one JSON cannot hold (the lib types call this a string always). */
9
+ const encoded = (value) => JSON.stringify(value);
10
+ /**
11
+ * JSON with keys sorted at every depth, so two writers of one batch write the same bytes. The sorted entries are written out directly: rebuilding an object would put an integer-like key such as `10` before `9` whatever the sort said.
12
+ * A value JSON cannot hold is left out of an object and written as null in an array, as `JSON.stringify` does.
13
+ */
14
+ const canonical = (value) => {
15
+ if (Array.isArray(value))
16
+ return `[${value.map(canonical).join(',')}]`;
17
+ if (typeof value === 'object' && value !== null) {
18
+ const entries = Object.entries(value)
19
+ .filter(([, entry]) => encoded(entry) !== undefined)
20
+ .sort(([a], [b]) => (a < b ? -1 : 1));
21
+ return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonical(entry)}`).join(',')}}`;
22
+ }
23
+ return encoded(value) ?? 'null';
24
+ };
25
+ /** The batch as one canonical JSON line: sorted keys, no optional whitespace, UTF-8 when written, and a newline at the end. These are the bytes that are hashed, printed, and sent. */
26
+ export const canonicalBytes = (batch) => `${canonical(batch)}\n`;
27
+ /** The most bytes one request of a deposit carries. The pool refuses a request past 1 MiB, so a batch past this goes in parts, each of them under that cap. */
28
+ export const PART_BYTES = 512 * 1024;
29
+ /**
30
+ * The batch cut into parts of at most `limit` UTF-8 bytes, never inside a character, so the parts join back to exactly the bytes that were hashed. A batch within the limit is one part.
31
+ */
32
+ export const partsOf = (bytes, limit = PART_BYTES) => {
33
+ const parts = [];
34
+ let part = '';
35
+ let size = 0;
36
+ for (const character of bytes) {
37
+ const width = Buffer.byteLength(character, 'utf8');
38
+ if (size + width > limit) {
39
+ parts.push(part);
40
+ part = '';
41
+ size = 0;
42
+ }
43
+ part += character;
44
+ size += width;
45
+ }
46
+ parts.push(part);
47
+ return parts;
48
+ };
49
+ export const batchHash = (bytes) => createHash('sha256').update(Buffer.from(bytes, 'utf8')).digest('hex');
50
+ /**
51
+ * `oars pool outbox --explain`: one row for each field name a batch holds, at any depth, saying what admits its values. A field with no row fails the command, so a field added to the batch adds its row here.
52
+ * A name that sits at more than one depth (`status`, `harness`, `frame`) has one row that reads true at each.
53
+ */
54
+ export const FIELD_ROWS = {
55
+ action: 'The canonical action of a step, one of a closed list this client holds; never the harness tool text.',
56
+ agentType: 'The type of the subagent whose run holds a step: a built-in type of the harness, else the word custom; null on a step of the main run.',
57
+ allocated: 'Whether the step\'s token figure was divided among the calls of one model response (true) rather than measured for the call alone; absent when measured. A flag only, never a count.',
58
+ argClass: 'The kinds of value a call\'s arguments held, a sorted set of a closed list: path, url, pattern, command, number, flag, text, and none; never a value.',
59
+ cacheRead: 'A bucket of the tokens read from the cache in a prompt, a number, or null when the source never recorded it.',
60
+ cacheWrite: 'A bucket of the tokens written to the cache in a prompt, a number, or null when the source never recorded it.',
61
+ change: 'How far a delta moved, a signed bucket of its metric (a lower bound) in a number from -7 to 7 for calls per session and from -11 to 11 for thinking tokens; never the exact count.',
62
+ cls: 'The class of a prompt, one of a closed list of eight, computed from the kinds of tools it used; never its words.',
63
+ day: 'The UTC calendar day of the session as YYYY-MM-DD, or null; never a time of day.',
64
+ deltaKind: 'How a delta changed what the client ran: added, removed, or changed, one of a closed list the field allows; never a value.',
65
+ deltas: 'The changes the client found between its own before and after windows, each sent once with the session that filled the after window: a kind, a field, a name, a registry, a metric, and a bucketed change; never a session id, a root, a date, or an exact value.',
66
+ durationMs: 'The time of one tool call in milliseconds, exact, a number or null.',
67
+ effort: 'The effort level the model ran at, only when it is a public one; else the literal placeholder.',
68
+ errorCode: 'Why a tool call failed, one class of a closed list: an exit class (0, 1, 2, 126, 127, signal), an HTTP status class, a harness class (user-rejected, interrupted, permission-denied, input-validation, tool-use-error, timeout), or other; null on a call that did not fail or whose harness records no class. Never result text.',
69
+ editFollowed: 'Whether one of the calls that followed a failed call wrote a file, true or false: what followed, never that the failure was fixed.',
70
+ errors: 'A bucket of how many tool calls failed in a prompt, a number.',
71
+ explore: 'A bucket of how many steps of a prompt only looked (read a file, searched files, searched the web, or fetched a page), a number or null; never the exact count.',
72
+ field: 'What a delta is about, one of plugin, package, permissionMode, and effort: the field alone, never its value.',
73
+ flags: 'The flags of a shell command, each a flag name, never its value: the name as it is after redaction; `private` when redaction changes it, an exclusion term matches it, it is not a name, or it is over 128 characters. Capture still drops a subcommand or flag off the pinned vocabulary of its program, so a real session sends no off-list subcommand (null) and no off-list flag; a subcommand or flag the vocabulary lists goes as its name. A multi-letter single-dash flag no public list holds (`-race`) is dropped.',
74
+ fieldsVersion: 'The version of the field list this client builds a batch from: a constant of this client, never a value from a session.',
75
+ format: 'The skeleton format number: a constant of this client, never a value from a session.',
76
+ filled: 'Whether the harness recorded a setup or a tool funnel for the session, true or false; false means the lists beside it are empty because nothing was recorded, not because no tool was in reach.',
77
+ frame: 'Where a session ran: the harness and its version, the model, the effort, the day, the platform, the shell, and the stack, each checked against its closed list, and the reach of the session (see `reach`). On a step it is a number, the position of the frame its call ran in, in the transcript list of frames.',
78
+ edges: 'The edges arriving at a step, each a kind and the position of the step it names, in the same transcript or, for a spawned edge, in the transcript that spawned this one; absent when none arrive.',
79
+ frames: 'The distinct frames a transcript\'s steps ran in, each as a frame above with the reach of the session; a step names one by position.',
80
+ frameworks: 'The frameworks of the project, each from a closed public list; never a path or a name of yours.',
81
+ harness: 'Which harness wrote the session, one of a closed list.',
82
+ harnessVersion: 'The harness version: the name as it is after redaction; `private` when redaction changes it, an exclusion term matches it, it is not a name, or it is over 128 characters. A transcript or prompt frame carries the value capture closed it to (the public name, else `<literal>`), sent as it is; the version of a call, which capture keeps as its line names it, goes as the string after the gate.',
83
+ host: 'The host of a web fetch or search, the name as it is after redaction; `private` when redaction changes it, an exclusion term matches it, it is not a name, or it is over 128 characters; null when the step names no host. Capture keeps the host as the address names it, without credentials, path, or query. The pool replaces a host with a keyed hash on arrival and serves it only once enough accounts report it.',
84
+ hour: 'The UTC hour of day, 0 to 23, beside the day; never a minute, a second, or an offset.',
85
+ id: "A hash the client computes per account from the account and the session, so a redeposit replaces and two accounts' sessions cannot be joined. Never the session id.",
86
+ identity: 'What a step called: a shell program with its subcommand and flags, an MCP server and tool, a skill, or a web host, each a name after redaction or `private`; null for an action with none.',
87
+ input: 'A bucket of the input tokens in a prompt, a number, or null when the source never recorded it.',
88
+ kind: 'How an edge came about, one of a closed list: next, spawned, retried, or after-error.',
89
+ keys: 'The input key names of a call, sorted: a public tool\'s from its published list and any other tool\'s only when each passes the name gate; none for a tool that went as private. Names only, never a value.',
90
+ keysDropped: 'A bucket of the key names of a call that were not sent, off a public tool\'s list or kept private by the gate, a number; never the names.',
91
+ intervention: 'Whether a human stepped in after a prompt\'s work and how, one of a closed list: none, after-error (the prompt\'s last call failed), redo (the next prompt edited the same file), revert (the next prompt undid the edit), user-stop (the human rejected a call or interrupted the turn), or unknown; absent when the prompt predates the signal. An outcome row, not a grade; never the next prompt\'s words.',
92
+ language: 'A programming language of the project, from a closed public list, or other for any language off the list, beside its share bucket.',
93
+ languageShare: 'How much of the files a session touched are in one language, one of four buckets: none, under 10 percent, 10 up to 50, or 50 and over; never a count of files.',
94
+ languageShares: 'The share bucket of each language of the project whose share is known; a language with no share is in the languages list alone.',
95
+ languages: 'The programming languages of the project, each from a closed public list.',
96
+ leaves: 'How many lines of a transcript its root covers, a count.',
97
+ merkleRoot: 'The Merkle root of a transcript: a SHA-256 over the hashes of its lines, one root for each transcript, the main one first; never a line.',
98
+ metric: 'What a delta measures, calls per session or thinking tokens, one of a closed list the field fixes.',
99
+ model: 'The model name: the name as it is after redaction; `private` when redaction changes it, an exclusion term matches it, it is not a name, or it is over 128 characters. A transcript or prompt frame carries the value capture closed it to (the public name, else `<literal>`), sent as it is; the model of a call, which capture keeps as its line names it, goes as the string after the gate.',
100
+ ms: 'A bucket of the milliseconds a step took (a lower bound), a number or null; never the exact time.',
101
+ name: 'The name of a package a shell command pulled or a delta removed, as capture kept it: the name as it is after redaction; `private` when redaction changes it, an exclusion term matches it, it is not a name, or it is over 128 characters. A plugin in a delta is always private, and null when a delta names its field alone.',
102
+ native: 'The name the harness gave the tool: the name as it is after redaction; `private` when redaction changes it, an exclusion term matches it, it is not a name, or it is over 128 characters.',
103
+ next: 'The actions of up to the next 5 calls of the same prompt after a failed call, each one of the closed list of canonical actions: what followed, never a fix; no command, path, or text.',
104
+ output: 'A bucket of the output tokens in a prompt, a number, or null when the source never recorded it.',
105
+ parent: 'The position of the call that spawned this step, in the tools of the transcript that spawnedIn names, a number, or null.',
106
+ platform: 'The operating system family, one of a closed list.',
107
+ program: 'The program a shell command ran, its basename only: the name as it is after redaction; `private` when redaction changes it, an exclusion term matches it, it is not a name, or it is over 128 characters.',
108
+ prompt: 'Which prompt of the batch a tool call follows, a position in this batch, or null.',
109
+ prompts: 'The prompts of a transcript: each a class, bucketed counts and tokens, and a frame; never their words.',
110
+ pulls: 'The packages a shell command pulled from a registry: the registry, the public package name, and its version.',
111
+ recoveryCalls: 'For a failed call, what followed it: the actions of up to its next 5 calls and whether one wrote a file. Absent on a call that did not fail; never a signature or text.',
112
+ registry: 'Which registry a pull or a removed package came from, one of a closed list; null for a delta whose name is private or absent.',
113
+ remainingTokens: 'A bucket of the tokens the run spent after a step (a lower bound), a number, or null when steps follow and none reported tokens: the bucket of the exact sum, with the subagent runs a main-run step spawned later in the session.',
114
+ server: 'The MCP server a call went to: the name as it is after redaction; `private` when redaction changes it, an exclusion term matches it, it is not a name, or it is over 128 characters.',
115
+ shell: 'The shell a command ran in, one of a closed list, or null.',
116
+ side: 'Whether a tool call ran as a side call, true or false.',
117
+ spawnedIn: 'The position, among the transcripts of the batch, of the transcript that spawned this one, a number; null for the main run and for a subagent whose spawner the batch does not hold.',
118
+ size: 'A size class of the repository, one of a closed list, or null; never a count of files.',
119
+ mcp: 'The MCP servers in reach of a session (offered, loaded, or called), each the name as it is after redaction; `private` when redaction changes it, an exclusion term matches it, it is not a name, or it is over 128 characters, sorted; never a tool, an argument, or a path.',
120
+ orgSize: 'How many people the operator\'s organization has, one of a closed list of bands or unknown; set by the operator on this machine, never looked up.',
121
+ plugins: 'The plugins in reach of a session, each the word private, since no plugin list is pinned; their number, never their names.',
122
+ reach: 'The MCP servers, skills, and plugins in reach of a session, and whether the harness recorded them; every name is the name as it is after redaction; `private` when redaction changes it, an exclusion term matches it, it is not a name, or it is over 128 characters (a plugin is always private).',
123
+ repoKey: "A hash of the repository salted with this store's salt: one store's sessions in one repository share it; one account on two machines gets two keys until the account salt lands. 16 hex characters; never a path, a remote, or a name.",
124
+ repoVisibility: 'Whether the repository is public or private, or unknown; set by the operator on this machine, never looked up.',
125
+ skill: 'The skill a step ran (a skill-run name capture keeps as it is after redaction), or the skill whose run the step\'s call sat in, the name as it is after redaction; `private` when redaction changes it, an exclusion term matches it, it is not a name, or it is over 128 characters. Absent on a step outside a skill\'s run.',
126
+ skills: 'The skills in reach of a session (called in it), each the name as it is after redaction; `private` when redaction changes it, an exclusion term matches it, it is not a name, or it is over 128 characters, sorted.',
127
+ stack: 'The languages, frameworks, and size class of the project.',
128
+ status: 'Whether a tool call or a step succeeded: ok, error, or none.',
129
+ steps: 'The steps of a tool call, each an action in closed vocabulary with its bucketed cost and spawner.',
130
+ team: 'Whether the repository\'s local git log shows one commit author or several, or unknown; a count taken on this machine and dropped, never an author.',
131
+ sub: 'The subcommand of a shell program, or null: the name as it is after redaction; `private` when redaction changes it, an exclusion term matches it, it is not a name, or it is over 128 characters. Capture still drops a subcommand or flag off the pinned vocabulary of its program, so a real session sends no off-list subcommand (null) and no off-list flag; a subcommand or flag the vocabulary lists goes as its name.',
132
+ thinking: 'A bucket of the thinking tokens in a prompt, a number, or null when the source never recorded it.',
133
+ target: 'The position of the step an edge names, a number counted over the steps of one transcript in call order.',
134
+ targetClass: 'The class of the path a call touched, one of a closed list: source, test, config, doc, lockfile, generated, data, none, other; never the path or its name.',
135
+ timingPrint: 'A hash of the call durations of the main transcript in the order they ran, or null under 50 timed calls; the calls of a subagent are not in it.',
136
+ tokens: 'A bucket of tokens (a lower bound of the count), a number or null; a step holds one, never the exact count.',
137
+ tool: 'The tool of an MCP call: the name as it is after redaction, beside a server that is not private and in a harness whose spelling delimits the server; else the literal placeholder.',
138
+ tools: 'The tool calls of a transcript, each a status and duration and its steps, in the order they ran; never their targets or arguments.',
139
+ transcripts: 'The main transcript, then each subagent: its frame, prompts, and tool calls, every value checked against its field\'s list or the name shape before the batch is built.',
140
+ usage: 'The tokens of a prompt as buckets (lower bounds): input, cache write, cache read, output, and thinking.',
141
+ usageResponses: 'A bucket of the responses of a prompt that reported usage, what a partial usage sum covers; left out only for a record captured before the coverage existed (capture version 20).',
142
+ version: 'The version of a pulled package, only when it is a registry version; else the literal placeholder or null.',
143
+ vocabVersion: 'The version of the closed vocabulary this client built the batch under: a constant of this client, never a value from a session.',
144
+ wallSec: 'A bucket of the seconds a prompt took, or null.',
145
+ };
146
+ /**
147
+ * Every field name a batch holds, at any depth, sorted: the rows above. A batch is filed under this list, the skeleton format, and `VOCAB_VERSION`, so one built under an older list, form, or vocabulary is never printed or sent.
148
+ * A test holds this list equal to the fields of a real batch, so a field the sample holds without its row fails there.
149
+ */
150
+ export const BATCH_FIELDS = Object.keys(FIELD_ROWS).sort();
151
+ const fieldsStamp = (fields, format) => createHash('sha256').update(`${String(format)}:${fields.join(',')}`).digest('hex').slice(0, 8);
152
+ /** The folder name of the batches this client builds now. A folder of any other name holds batches built under an older vocabulary, field list, or skeleton format. */
153
+ export const currentStamp = (fields = BATCH_FIELDS, format = SKELETON_FORMAT) => `v${VOCAB_VERSION}-${fieldsStamp(fields, format)}`;
154
+ /** One server's outbox: batches are per server, since a printed batch carries that server's credential. */
155
+ export const outboxFolder = (storeDir, server) => join(storeDir, 'pool-outbox', contentHash(server));
156
+ const BATCH_FILE = /^([0-9a-f]{16})\.([0-9a-f]{64})\.jsonl$/;
157
+ /** Writes one session's batch under the current stamp. A session has one batch: an older batch of it is removed, and an identical one is written again, so an edited file is restored. */
158
+ export const writeBatch = async (outbox, key, batch, stamp = currentStamp()) => {
159
+ const bytes = canonicalBytes(batch);
160
+ const hash = batchHash(bytes);
161
+ const folder = join(outbox, stamp);
162
+ await mkdir(folder, { recursive: true });
163
+ for (const name of await readdir(folder)) {
164
+ const found = BATCH_FILE.exec(name);
165
+ if (found?.[1] === key && found[2] !== hash)
166
+ await rm(join(folder, name), { force: true });
167
+ }
168
+ await writeFile(join(folder, `${key}.${hash}.jsonl`), bytes, 'utf8');
169
+ return { hash, folder, bytes: Buffer.byteLength(bytes) };
170
+ };
171
+ export const isMissing = (error) => error instanceof Error && 'code' in error && error.code === 'ENOENT';
172
+ /** A path that is missing, or has a file where a folder should be, holds nothing. */
173
+ const isAbsent = (error) => isMissing(error) || (error instanceof Error && 'code' in error && error.code === 'ENOTDIR');
174
+ /** The names in a folder; a folder that does not exist yet holds none. */
175
+ const namesIn = async (folder) => {
176
+ try {
177
+ return (await readdir(folder)).sort();
178
+ }
179
+ catch (error) {
180
+ if (isAbsent(error))
181
+ return [];
182
+ throw error;
183
+ }
184
+ };
185
+ /** Removes every batch under this stamp whose session is not in `kept`, and returns how many went. A session that fails a gate, leaves the allowlist, or is gone from the store never enters the outbox, so an earlier pass's batch of it cannot stay. */
186
+ export const removeBatchesExcept = async (outbox, kept, stamp = currentStamp(), only) => {
187
+ const folder = join(outbox, stamp);
188
+ let removed = 0;
189
+ for (const name of await namesIn(folder)) {
190
+ const found = BATCH_FILE.exec(name);
191
+ if (found !== null && !kept.has(found[1] ?? '') && (only === undefined || found[1] === only)) {
192
+ await rm(join(folder, name), { force: true });
193
+ removed += 1;
194
+ }
195
+ }
196
+ return removed;
197
+ };
198
+ /** Removes the batch of one session, if it has one under this stamp, and returns how many files went. */
199
+ export const removeBatchOf = async (outbox, key, stamp = currentStamp()) => removeBatchesExcept(outbox, new Set(), stamp, key);
200
+ /** The bytes of a batch file, when they still hash to the name the file was written under; undefined when the file is gone or was edited. */
201
+ export const unchangedBytes = async (file, hash) => {
202
+ let bytes;
203
+ try {
204
+ bytes = await readFile(file, 'utf8');
205
+ }
206
+ catch (error) {
207
+ if (isMissing(error))
208
+ return undefined;
209
+ throw error;
210
+ }
211
+ return batchHash(bytes) === hash ? bytes : undefined;
212
+ };
213
+ /** The batches built under this client's vocabulary, field list, and skeleton format, and how many of their files no longer hash to their names. An edited file is not printed and cannot be sent: run `deposit` again to rebuild it. */
214
+ export const heldBatches = async (outbox, stamp = currentStamp()) => {
215
+ const folder = join(outbox, stamp);
216
+ const batches = [];
217
+ let edited = 0;
218
+ for (const name of await namesIn(folder)) {
219
+ const found = BATCH_FILE.exec(name);
220
+ if (found === null)
221
+ continue;
222
+ const [, key = '', hash = ''] = found;
223
+ const file = join(folder, name);
224
+ const bytes = await unchangedBytes(file, hash);
225
+ if (bytes === undefined)
226
+ edited += 1;
227
+ else
228
+ batches.push({ key, hash, file, bytes });
229
+ }
230
+ return { batches, edited };
231
+ };
232
+ const shownFile = (outbox) => join(outbox, 'shown.log');
233
+ /** Records that this hash was printed, one line for each print: the lock below sends only a hash that appears here. */
234
+ export const markShown = async (outbox, hash, at) => {
235
+ await mkdir(outbox, { recursive: true });
236
+ await appendFile(shownFile(outbox), `${hash} ${new Date(at).toISOString()}\n`, 'utf8');
237
+ };
238
+ const wasShown = async (outbox, hash) => {
239
+ try {
240
+ return (await readFile(shownFile(outbox), 'utf8')).split('\n').some((line) => line.split(' ')[0] === hash);
241
+ }
242
+ catch (error) {
243
+ if (isMissing(error))
244
+ return false;
245
+ throw error;
246
+ }
247
+ };
248
+ /**
249
+ * The lock on a send: a batch goes only if it is in the outbox, its hash was printed, and its file still hashes to that hash. The first check that fails names why. The nightly task sends with `requireShown: false`,
250
+ * because covenant acceptance is its consent and it prints nothing; the other two checks still hold.
251
+ */
252
+ export const lockedBatch = async (outbox, hash, options = {}) => {
253
+ const folder = join(outbox, options.stamp ?? currentStamp());
254
+ const name = (await namesIn(folder)).find((entry) => BATCH_FILE.exec(entry)?.[2] === hash);
255
+ if (name === undefined)
256
+ return { ok: false, code: 'batch_unknown' };
257
+ if (options.requireShown !== false && !(await wasShown(outbox, hash)))
258
+ return { ok: false, code: 'batch_not_shown' };
259
+ const file = join(folder, name);
260
+ const bytes = await unchangedBytes(file, hash);
261
+ return bytes === undefined ? { ok: false, code: 'batch_changed' } : { ok: true, file, bytes };
262
+ };
263
+ /** Folders of an outbox other than the current stamp's: batches built under another vocabulary, field list, or skeleton format. */
264
+ const staleFolders = async (outbox, stamp) => {
265
+ const names = [];
266
+ for (const name of await namesIn(outbox)) {
267
+ if (name !== stamp && STAMP_FOLDER.test(name))
268
+ names.push(join(outbox, name));
269
+ }
270
+ return names;
271
+ };
272
+ const STAMP_FOLDER = /^v[^./]+-[0-9a-f]{8}$/;
273
+ const batchesIn = async (folder) => (await namesIn(folder)).filter((name) => BATCH_FILE.test(name)).length;
274
+ /** Batches in one server's outbox that were built under another vocabulary, field list, or skeleton format: never printed, never sent. */
275
+ export const staleBatches = async (outbox, stamp = currentStamp()) => {
276
+ let count = 0;
277
+ for (const folder of await staleFolders(outbox, stamp))
278
+ count += await batchesIn(folder);
279
+ return count;
280
+ };
281
+ /** Every server's stale batches, for `verify`. */
282
+ export const staleBatchesAnywhere = async (storeDir, stamp = currentStamp()) => {
283
+ const root = join(storeDir, 'pool-outbox');
284
+ let count = 0;
285
+ for (const name of await namesIn(root))
286
+ count += await staleBatches(join(root, name), stamp);
287
+ return count;
288
+ };
289
+ /** Removes one server's stale batches, and returns how many there were. */
290
+ export const discardStale = async (outbox, stamp = currentStamp()) => {
291
+ const count = await staleBatches(outbox, stamp);
292
+ for (const folder of await staleFolders(outbox, stamp))
293
+ await rm(folder, { recursive: true, force: true });
294
+ return count;
295
+ };
296
+ const collect = (value, into) => {
297
+ if (Array.isArray(value)) {
298
+ for (const entry of value)
299
+ collect(entry, into);
300
+ }
301
+ else if (typeof value === 'object' && value !== null) {
302
+ for (const [key, entry] of Object.entries(value)) {
303
+ into.add(key);
304
+ collect(entry, into);
305
+ }
306
+ }
307
+ };
308
+ /** Every field name in a batch, at any depth, each once and sorted. */
309
+ export const fieldsOf = (batchBytes) => {
310
+ const names = new Set();
311
+ collect(JSON.parse(batchBytes), names);
312
+ return [...names].sort();
313
+ };
@@ -0,0 +1,26 @@
1
+ /** One request as it will leave this machine. */
2
+ export interface Outgoing {
3
+ method: 'GET' | 'POST' | 'PUT';
4
+ url: URL;
5
+ /** Every header this code sets. The HTTP layer adds `host`, `connection`, and `content-length` itself, and `printed` lists those too. */
6
+ headers: Array<[string, string]>;
7
+ body?: string;
8
+ }
9
+ /**
10
+ * The headers Node's `fetch` would otherwise add on its own, set here so the print lists all of them. `identity` also means a reply is never compressed, so the bytes read are the bytes signed.
11
+ * `sec-fetch-mode` and `accept-language` carry no information; they are pinned to what `fetch` sends anyway. A conditional request also gets `cache-control: no-cache` and `pragma: no-cache` from `fetch`; `ask` sets both beside `if-none-match`.
12
+ * `pool-socket.test.ts` compares the print with the headers a loopback server reads off the socket, so a header a later Node adds fails there.
13
+ */
14
+ export declare const TRANSPORT_HEADERS: ReadonlyArray<[string, string]>;
15
+ /** The credential's size, and nothing else about it. */
16
+ export declare const maskOf: (secret: string) => string;
17
+ /** The secrets of one run. A value added once is masked in every print after, wherever it appears. */
18
+ export declare class Masker {
19
+ private readonly secrets;
20
+ add(secret: string): void;
21
+ mask(text: string): string;
22
+ }
23
+ /** The request as the wire carries it: the request line, `host` and `connection` first as the HTTP layer writes them, then the headers set here, then the length of a body. */
24
+ export declare const printed: (request: Outgoing, masker: Masker) => string;
25
+ /** The line the operator reads when the pool says to wait: a count of seconds, or an HTTP date, in plain words. Undefined when it is neither. */
26
+ export declare const waitWords: (retryAfter: string, now: number) => string | undefined;
@@ -0,0 +1,68 @@
1
+ // What `oars pool fetch` and `oars pool login` print before they send: the request line and every header, then the body, with a credential
2
+ // masked as `<credential: n bytes>`. The text printed is built from the very headers and body handed to `fetch`, so the two cannot drift apart.
3
+ /**
4
+ * The headers Node's `fetch` would otherwise add on its own, set here so the print lists all of them. `identity` also means a reply is never compressed, so the bytes read are the bytes signed.
5
+ * `sec-fetch-mode` and `accept-language` carry no information; they are pinned to what `fetch` sends anyway. A conditional request also gets `cache-control: no-cache` and `pragma: no-cache` from `fetch`; `ask` sets both beside `if-none-match`.
6
+ * `pool-socket.test.ts` compares the print with the headers a loopback server reads off the socket, so a header a later Node adds fails there.
7
+ */
8
+ export const TRANSPORT_HEADERS = [
9
+ ['accept-encoding', 'identity'],
10
+ ['accept-language', '*'],
11
+ ['sec-fetch-mode', 'cors'],
12
+ ];
13
+ /** The credential's size, and nothing else about it. */
14
+ export const maskOf = (secret) => `<credential: ${String(Buffer.byteLength(secret))} bytes>`;
15
+ /** Every way a secret can be written into a header or a body: bare, form-encoded, percent-encoded, and as the inside of a JSON string. */
16
+ const spellings = (secret) => {
17
+ const form = new URLSearchParams({ k: secret }).toString().slice('k='.length);
18
+ const inJson = JSON.stringify(secret).slice(1, -1);
19
+ return [...new Set([secret, form, encodeURIComponent(secret), inJson])];
20
+ };
21
+ /** The secrets of one run. A value added once is masked in every print after, wherever it appears. */
22
+ export class Masker {
23
+ secrets = new Set();
24
+ add(secret) {
25
+ if (secret !== '')
26
+ this.secrets.add(secret);
27
+ }
28
+ mask(text) {
29
+ // Longest first, so a secret that holds another is replaced whole.
30
+ const found = [...this.secrets].flatMap((secret) => spellings(secret).map((spelling) => ({ spelling, mask: maskOf(secret) }))).sort((a, b) => b.spelling.length - a.spelling.length);
31
+ return found.reduce((masked, { spelling, mask }) => masked.split(spelling).join(mask), text);
32
+ }
33
+ }
34
+ /** The request as the wire carries it: the request line, `host` and `connection` first as the HTTP layer writes them, then the headers set here, then the length of a body. */
35
+ export const printed = (request, masker) => {
36
+ const { method, url, headers, body } = request;
37
+ const lines = [`${method} ${url.pathname}${url.search} HTTP/1.1`, `host: ${url.host}`, 'connection: keep-alive', ...headers.map(([name, value]) => `${name}: ${value}`)];
38
+ if (body !== undefined)
39
+ lines.push(`content-length: ${String(Buffer.byteLength(body))}`);
40
+ return masker.mask(`${lines.join('\n')}\n${body === undefined ? '' : `\n${body}\n`}`);
41
+ };
42
+ /** The line the operator reads when the pool says to wait: a count of seconds, or an HTTP date, in plain words. Undefined when it is neither. */
43
+ export const waitWords = (retryAfter, now) => {
44
+ const seconds = /^\d+$/.test(retryAfter.trim()) ? Number(retryAfter.trim()) : Math.ceil((Date.parse(retryAfter) - now) / 1000);
45
+ if (!Number.isFinite(seconds))
46
+ return undefined;
47
+ if (seconds <= 0)
48
+ return 'now';
49
+ const units = [
50
+ ['day', 86_400],
51
+ ['hour', 3_600],
52
+ ['minute', 60],
53
+ ];
54
+ // Only two units are shown, so the last one shown is rounded up: an operator who retries at the time named must not get another 429.
55
+ const lead = units.findIndex(([, size]) => seconds >= size);
56
+ const step = lead < 0 ? 1 : (units[Math.min(lead + 1, units.length - 1)]?.[1] ?? 1);
57
+ const parts = [];
58
+ let left = Math.ceil(seconds / step) * step;
59
+ for (const [name, size] of units) {
60
+ const count = Math.floor(left / size);
61
+ left -= count * size;
62
+ if (count > 0)
63
+ parts.push(`${String(count)} ${name}${count === 1 ? '' : 's'}`);
64
+ }
65
+ if (parts.length === 0)
66
+ parts.push(`${String(left)} second${left === 1 ? '' : 's'}`);
67
+ return `in ${parts.slice(0, 2).join(' and ')}`;
68
+ };
@@ -0,0 +1,27 @@
1
+ /** Parts of a raw copy, each at most this many bytes: under the pool's 1 MB per request. */
2
+ export declare const RAW_PART_BYTES: number;
3
+ /** The folder of the raw copies this client builds now; one built under another redaction or capture version is never sent. */
4
+ export declare const rawStamp: () => string;
5
+ export declare const rawHash: (bytes: string) => string;
6
+ /** Writes one redacted transcript for a session, replacing an earlier copy of the same place. */
7
+ export declare const writeRaw: (outbox: string, key: string, place: number, bytes: string) => Promise<{
8
+ hash: string;
9
+ bytes: number;
10
+ }>;
11
+ export interface HeldRaw {
12
+ key: string;
13
+ place: number;
14
+ hash: string;
15
+ file: string;
16
+ body: string;
17
+ bytes: number;
18
+ }
19
+ /** The raw copies under the current stamp that still hash to their names; `edited` counts those that no longer do, which are neither printed nor sent. */
20
+ export declare const heldRaw: (outbox: string) => Promise<{
21
+ copies: HeldRaw[];
22
+ edited: number;
23
+ }>;
24
+ /** Removes every raw copy whose session key is not in `kept` (all of them when none is), and returns how many went. A folder switched off, a consent withdrawn, or a session no longer passing the gates leaves nothing queued. */
25
+ export declare const removeRawExcept: (outbox: string, kept: ReadonlySet<string>, only?: string) => Promise<number>;
26
+ /** The parts a raw copy goes in, in order. */
27
+ export declare const rawParts: (body: string) => string[];
@@ -0,0 +1,68 @@
1
+ // The raw part of the outbox (t884): one redacted copy of each transcript of a session that passed the gates, in the same outbox as the batches and printed by the same command. A copy is named for its
2
+ // session key, its place among the session's transcripts (0 is the main one), and the SHA-256 of its redacted bytes, which is the hash the pool's `/v1/raw/:sha256` routes take.
3
+ import { createHash } from 'node:crypto';
4
+ import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';
5
+ import { join } from 'node:path';
6
+ import { partsOf } from './pool-outbox.js';
7
+ import { REDACTION_VERSION } from './redact.js';
8
+ import { CAPTURE_VERSION } from './schema.js';
9
+ const RAW_FILE = /^([0-9a-f]{16})\.(\d+)\.([0-9a-f]{64})\.redacted$/;
10
+ /** Parts of a raw copy, each at most this many bytes: under the pool's 1 MB per request. */
11
+ export const RAW_PART_BYTES = 512 * 1024;
12
+ /** The folder of the raw copies this client builds now; one built under another redaction or capture version is never sent. */
13
+ export const rawStamp = () => `raw-r${REDACTION_VERSION}-c${CAPTURE_VERSION}`;
14
+ export const rawHash = (bytes) => createHash('sha256').update(Buffer.from(bytes, 'utf8')).digest('hex');
15
+ const namesIn = async (folder) => {
16
+ try {
17
+ return (await readdir(folder)).sort();
18
+ }
19
+ catch {
20
+ return [];
21
+ }
22
+ };
23
+ /** Writes one redacted transcript for a session, replacing an earlier copy of the same place. */
24
+ export const writeRaw = async (outbox, key, place, bytes) => {
25
+ const folder = join(outbox, rawStamp());
26
+ const hash = rawHash(bytes);
27
+ await mkdir(folder, { recursive: true });
28
+ for (const name of await namesIn(folder)) {
29
+ const found = RAW_FILE.exec(name);
30
+ if (found?.[1] === key && Number(found[2]) === place && found[3] !== hash)
31
+ await rm(join(folder, name), { force: true });
32
+ }
33
+ await writeFile(join(folder, `${key}.${String(place)}.${hash}.redacted`), bytes, 'utf8');
34
+ return { hash, bytes: Buffer.byteLength(bytes) };
35
+ };
36
+ /** The raw copies under the current stamp that still hash to their names; `edited` counts those that no longer do, which are neither printed nor sent. */
37
+ export const heldRaw = async (outbox) => {
38
+ const folder = join(outbox, rawStamp());
39
+ const copies = [];
40
+ let edited = 0;
41
+ for (const name of await namesIn(folder)) {
42
+ const found = RAW_FILE.exec(name);
43
+ if (found === null)
44
+ continue;
45
+ const file = join(folder, name);
46
+ const body = await readFile(file, 'utf8');
47
+ if (rawHash(body) === found[3])
48
+ copies.push({ key: found[1] ?? '', place: Number(found[2]), hash: found[3], file, body, bytes: Buffer.byteLength(body) });
49
+ else
50
+ edited += 1;
51
+ }
52
+ return { copies, edited };
53
+ };
54
+ /** Removes every raw copy whose session key is not in `kept` (all of them when none is), and returns how many went. A folder switched off, a consent withdrawn, or a session no longer passing the gates leaves nothing queued. */
55
+ export const removeRawExcept = async (outbox, kept, only) => {
56
+ const folder = join(outbox, rawStamp());
57
+ let removed = 0;
58
+ for (const name of await namesIn(folder)) {
59
+ const found = RAW_FILE.exec(name);
60
+ if (found !== null && !kept.has(found[1] ?? '') && (only === undefined || found[1] === only)) {
61
+ await rm(join(folder, name), { force: true });
62
+ removed += 1;
63
+ }
64
+ }
65
+ return removed;
66
+ };
67
+ /** The parts a raw copy goes in, in order. */
68
+ export const rawParts = (body) => partsOf(body, RAW_PART_BYTES);
@@ -0,0 +1,35 @@
1
+ import { type Bundle, type PinnedKey } from './pool-wire.js';
2
+ import type { Harness } from './schema.js';
3
+ export declare const textIn: (value: object, key: string) => string | undefined;
4
+ export declare const numberIn: (value: object, key: string) => number | undefined;
5
+ export declare const isObject: (value: unknown) => value is object;
6
+ /** One server's sign-in, as `login` saved it. */
7
+ export interface SignIn {
8
+ server: string;
9
+ account: string;
10
+ token: string;
11
+ expiresAt: number;
12
+ /** Pinned at the first login to this server; every bundle is checked against it. */
13
+ signingKey: PinnedKey;
14
+ }
15
+ export declare const signInOf: (value: unknown) => SignIn | undefined;
16
+ export declare const signInsFile: (storeDir: string) => string;
17
+ /** Every saved sign-in. A file that cannot be read stops the command: treating it as empty would forget a pin and trust whatever key the server names next. */
18
+ export declare const loadSignIns: (storeDir: string, step: string) => Promise<SignIn[]>;
19
+ /** One folder per harness under the server's, so a fetch for one harness never overwrites another's bundle of the same version. */
20
+ export declare const replicaFile: (storeDir: string, server: string, harness: Harness, harnessVersion: string) => string;
21
+ /** What an earlier fetch kept: the bundle, checked again against the pinned key, and the ETag the pool sent with it. Nothing readable and verified is nothing kept, and the next fetch is then an ordinary one. */
22
+ export declare const keptReplica: (file: string, key: PinnedKey, harness: Harness, harnessVersion: string) => Promise<{
23
+ etag: string | undefined;
24
+ bundle: Bundle;
25
+ } | undefined>;
26
+ /** A session's harness and the version it runs, as a hook input, a transcript, or the store states them. */
27
+ export interface Setup {
28
+ harness: Harness;
29
+ harnessVersion: string;
30
+ }
31
+ /**
32
+ * The kept bundle for a session's own harness and version, from the first signed-in server that kept one which still verifies; undefined otherwise. It never throws: a damaged
33
+ * sign-in file, a missing or corrupt replica, or one signed for another harness all mean the caller answers from the local store alone, and a hook must not fail on any of them.
34
+ */
35
+ export declare const sessionBundle: (storeDir: string, setup: Setup | undefined) => Promise<Bundle | undefined>;
@@ -0,0 +1,91 @@
1
+ // The kept pool bundle: where `oars pool fetch` keeps one harness version's signed bundle, the sign-ins whose pinned keys verify it, and the one reader every caller uses (t926).
2
+ // `poolFetch` reads it for its ETag, and recall, the MCP tool, and the hooks read it for the session's own harness and version. A reader for recall is fail-open: no bundle,
3
+ // one for another harness, or one that does not verify means local reads only, and nothing is printed.
4
+ import { readFile } from 'node:fs/promises';
5
+ import { join } from 'node:path';
6
+ import { CliError, ExitCode } from '../errors.js';
7
+ import { isMissing } from './pool-outbox.js';
8
+ import { isBundleVersion, verifyBundle } from './pool-wire.js';
9
+ import { contentHash } from './read.js';
10
+ export const textIn = (value, key) => {
11
+ const found = Reflect.get(value, key);
12
+ return typeof found === 'string' && found !== '' ? found : undefined;
13
+ };
14
+ export const numberIn = (value, key) => {
15
+ const found = Reflect.get(value, key);
16
+ return typeof found === 'number' && Number.isFinite(found) ? found : undefined;
17
+ };
18
+ export const isObject = (value) => typeof value === 'object' && value !== null;
19
+ const keyOf = (value) => {
20
+ if (!isObject(value))
21
+ return undefined;
22
+ const [kty, crv, x] = [textIn(value, 'kty'), textIn(value, 'crv'), textIn(value, 'x')];
23
+ return kty === 'OKP' && crv === 'Ed25519' && x !== undefined ? { kty, crv, x } : undefined;
24
+ };
25
+ export const signInOf = (value) => {
26
+ if (!isObject(value))
27
+ return undefined;
28
+ const [server, account, token, expiresAt] = [textIn(value, 'server'), textIn(value, 'account'), textIn(value, 'token'), numberIn(value, 'expiresAt')];
29
+ const signingKey = keyOf(Reflect.get(value, 'signingKey'));
30
+ return server === undefined || account === undefined || token === undefined || expiresAt === undefined || signingKey === undefined ? undefined : { server, account, token, expiresAt, signingKey };
31
+ };
32
+ export const signInsFile = (storeDir) => join(storeDir, 'pool-accounts.json');
33
+ /** Every saved sign-in. A file that cannot be read stops the command: treating it as empty would forget a pin and trust whatever key the server names next. */
34
+ export const loadSignIns = async (storeDir, step) => {
35
+ const file = signInsFile(storeDir);
36
+ let text;
37
+ try {
38
+ text = await readFile(file, 'utf8');
39
+ }
40
+ catch (error) {
41
+ if (isMissing(error))
42
+ return [];
43
+ throw error;
44
+ }
45
+ const damaged = new CliError(`${file} cannot be read. Remove it, then run oars pool login again.`, ExitCode.INVALID, step);
46
+ let parsed;
47
+ try {
48
+ parsed = JSON.parse(text);
49
+ }
50
+ catch {
51
+ throw damaged;
52
+ }
53
+ if (!Array.isArray(parsed))
54
+ throw damaged;
55
+ const entries = parsed;
56
+ return entries.map((entry) => {
57
+ const signIn = signInOf(entry);
58
+ if (signIn === undefined)
59
+ throw damaged;
60
+ return signIn;
61
+ });
62
+ };
63
+ /** One folder per harness under the server's, so a fetch for one harness never overwrites another's bundle of the same version. */
64
+ export const replicaFile = (storeDir, server, harness, harnessVersion) => join(storeDir, 'pool-replica', contentHash(server), harness, `${encodeURIComponent(harnessVersion)}.json`);
65
+ /** What an earlier fetch kept: the bundle, checked again against the pinned key, and the ETag the pool sent with it. Nothing readable and verified is nothing kept, and the next fetch is then an ordinary one. */
66
+ export const keptReplica = async (file, key, harness, harnessVersion) => {
67
+ let parsed;
68
+ try {
69
+ parsed = JSON.parse(await readFile(file, 'utf8'));
70
+ }
71
+ catch {
72
+ return undefined;
73
+ }
74
+ const verified = await verifyBundle(parsed, key, harness, harnessVersion);
75
+ return verified.ok ? { etag: isObject(parsed) ? textIn(parsed, 'etag') : undefined, bundle: verified.bundle } : undefined;
76
+ };
77
+ /**
78
+ * The kept bundle for a session's own harness and version, from the first signed-in server that kept one which still verifies; undefined otherwise. It never throws: a damaged
79
+ * sign-in file, a missing or corrupt replica, or one signed for another harness all mean the caller answers from the local store alone, and a hook must not fail on any of them.
80
+ */
81
+ export const sessionBundle = async (storeDir, setup) => {
82
+ if (setup === undefined || !isBundleVersion(setup.harnessVersion))
83
+ return undefined;
84
+ const signIns = await loadSignIns(storeDir, 'recall').catch(() => []);
85
+ for (const { server, signingKey } of signIns) {
86
+ const kept = await keptReplica(replicaFile(storeDir, server, setup.harness, setup.harnessVersion), signingKey, setup.harness, setup.harnessVersion);
87
+ if (kept !== undefined)
88
+ return kept.bundle;
89
+ }
90
+ return undefined;
91
+ };