@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,179 @@
1
+ // What a recall led to (recall spec Sections 2.7 and 4). A pure function of `recalls.jsonl` and the capture records, computed at derive and never captured live, so
2
+ // `recalls.jsonl` stays append-only and a change here needs no re-deposit. Held recalls get a consequence too: they are the baseline a shown recall reads against. Only a
3
+ // shown recall touches the graph, because a held one reached nobody: it adds no `recall.*` step and exposes no cell.
4
+ import { motifsOf, stepsOfCall } from './motif.js';
5
+ import { bucketOf, floorOf } from './pool-wire.js';
6
+ import { harnessOf } from './schema.js';
7
+ /** A consequence window ends at the prompt's end or after this many calls (spec Section 2.7). */
8
+ export const WINDOW_CALLS = 20;
9
+ const callKey = (tool) => `${tool.session}#${String(tool.n)}`;
10
+ const promptKey = (session, index) => `${session}#${String(index)}`;
11
+ /**
12
+ * Each session's main line, in order. A hook inside a subagent reports the parent's session id, so a subagent's push is placed on the parent's line by its time; that is
13
+ * a weak spot of the window, not of the arm.
14
+ */
15
+ const mainLines = (store) => {
16
+ const lines = new Map();
17
+ for (const tool of store.tools) {
18
+ if (tool.side)
19
+ continue;
20
+ const line = lines.get(tool.session);
21
+ if (line === undefined)
22
+ lines.set(tool.session, [tool]);
23
+ else
24
+ line.push(tool);
25
+ }
26
+ for (const line of lines.values())
27
+ line.sort((a, b) => a.n - b.n);
28
+ return lines;
29
+ };
30
+ /**
31
+ * The first call stamped after the recall. A call's line is written before its pre-tool hook runs, and a hook's text reaches the model after that call was already decided, so
32
+ * the call a push fired on is behind it and the window opens on the next decision.
33
+ */
34
+ const positionOf = (line, at) => {
35
+ const next = line.findIndex((tool) => tool.ts !== null && tool.ts > at);
36
+ return next < 0 ? line.length : next;
37
+ };
38
+ const outcomeOf = (prompt) => ({
39
+ errors: prompt?.errors ?? null,
40
+ verify: prompt?.verify ?? null,
41
+ stopped: prompt?.stopped ?? null,
42
+ commits: prompt?.commits ?? null,
43
+ redo: prompt?.redo ?? null,
44
+ });
45
+ const consequenceOf = (recall, session, line, position, harness, prompts) => {
46
+ const prompt = (line[position - 1] ?? line[position])?.prompt ?? null;
47
+ const calls = [];
48
+ let end = 'session';
49
+ for (const tool of line.slice(position)) {
50
+ if (calls.length === WINDOW_CALLS) {
51
+ end = 'calls';
52
+ break;
53
+ }
54
+ if (tool.prompt !== prompt) {
55
+ end = 'prompt';
56
+ break;
57
+ }
58
+ calls.push(tool);
59
+ }
60
+ const steps = calls.flatMap((tool) => stepsOfCall(tool, harness));
61
+ const names = new Set(recall.answer.names);
62
+ const measured = calls.flatMap((tool) => tool.steps ?? []).flatMap((step) => step.cost.tokens ?? []);
63
+ return {
64
+ window: { from: position, to: position + calls.length, calls: calls.length, end },
65
+ // Shape level only: it is the ID that joins across accounts, and named windows would multiply a row that exists per recall.
66
+ motifs: [...new Set(motifsOf(steps).flatMap(({ motif }) => (motif.level === 'shape' ? [motif.motifId] : [])))],
67
+ followed: steps.some((step) => names.has(step.action)),
68
+ cost: { tokens: measured.length === 0 ? null : measured.reduce((sum, tokens) => sum + tokens, 0), durationMs: steps.reduce((sum, step) => sum + step.durationMs, 0) },
69
+ outcome: outcomeOf(prompts.get(promptKey(session, prompt))),
70
+ };
71
+ };
72
+ /** Fills `n` and `consequence`. A recall with no session, or from a session this store does not hold, has nothing to read and comes back as it was. */
73
+ export const deriveConsequences = (store, recalls) => {
74
+ const lines = mainLines(store);
75
+ const prompts = new Map(store.prompts.map((prompt) => [promptKey(prompt.session, prompt.index), prompt]));
76
+ const sessions = new Map(store.sessions.map((session) => [session.session, session]));
77
+ return recalls.map((recall) => {
78
+ const session = recall.sid === null ? undefined : sessions.get(recall.sid);
79
+ if (session === undefined)
80
+ return recall;
81
+ const line = lines.get(session.session) ?? [];
82
+ const position = positionOf(line, recall.at);
83
+ return { ...recall, n: position, consequence: consequenceOf(recall, session.session, line, position, harnessOf(session), prompts) };
84
+ });
85
+ };
86
+ export const NO_RECALLS = {
87
+ place: (_tool, steps) => steps.map((step) => ({ step, exposed: false })),
88
+ exposedPrompt: () => false,
89
+ };
90
+ const pushTo = (map, key, step) => {
91
+ const found = map.get(key);
92
+ if (found === undefined)
93
+ map.set(key, [step]);
94
+ else
95
+ found.push(step);
96
+ };
97
+ /** The step enters a cell after the store is floored, so its own count is floored here: no cell holds a number finer than its bucket's floor (rule 5). */
98
+ const recallStep = (recall) => ({
99
+ step: { action: recall.mode === 'pull' ? 'recall.pull' : 'recall.push', identity: null, status: 'ok', tokens: floorOf('tokens', bucketOf('tokens', recall.answer.tokens)), durationMs: 0 },
100
+ exposed: true,
101
+ });
102
+ /**
103
+ * Takes recalls that have been through `deriveConsequences`. An MCP pull has no session id, so the only step it has is the one in its transcript; a pull that names its session
104
+ * (`--session`) has no MCP call, so its step is this one.
105
+ */
106
+ export const recallGraphOf = (store, recalls) => {
107
+ const lines = mainLines(store);
108
+ const exposedCalls = new Set();
109
+ const exposedPrompts = new Set();
110
+ const before = new Map();
111
+ const after = new Map();
112
+ for (const recall of recalls) {
113
+ const line = recall.sid === null ? undefined : lines.get(recall.sid);
114
+ if (recall.arm === 'held' || recall.consequence === undefined || line === undefined)
115
+ continue;
116
+ const { from, to } = recall.consequence.window;
117
+ // A shown miss showed no evidence, so its window is not an exposure; the step stays, since the pull happened.
118
+ if (recall.answer.kind !== 'miss') {
119
+ for (const tool of line.slice(from, to)) {
120
+ exposedCalls.add(callKey(tool));
121
+ exposedPrompts.add(promptKey(tool.session, tool.prompt));
122
+ }
123
+ }
124
+ const previous = line[from - 1];
125
+ const first = line[from];
126
+ if (previous !== undefined)
127
+ pushTo(after, callKey(previous), recallStep(recall));
128
+ else if (first !== undefined)
129
+ pushTo(before, callKey(first), recallStep(recall));
130
+ }
131
+ return {
132
+ place: (tool, steps) => {
133
+ const key = callKey(tool);
134
+ const exposed = exposedCalls.has(key);
135
+ return [...(before.get(key) ?? []), ...steps.map((step) => ({ step, exposed })), ...(after.get(key) ?? [])];
136
+ },
137
+ exposedPrompt: (session, index) => exposedPrompts.has(promptKey(session, index)),
138
+ };
139
+ };
140
+ const sharesACall = (one, other) => Math.max(one.from, other.from) < Math.min(one.to, other.to);
141
+ /** Takes recalls that have been through `deriveConsequences`; `day` is a UTC day, so one day of hooks reads as one set of rows. A shown window from any day can overlap a held one. */
142
+ export const recallCounts = (recalls, day) => {
143
+ const rows = new Map();
144
+ const shown = new Map();
145
+ for (const recall of recalls) {
146
+ if (recall.arm === 'shown' && recall.consequence !== undefined)
147
+ shown.set(recall.sid, [...(shown.get(recall.sid) ?? []), recall.consequence.window]);
148
+ }
149
+ for (const recall of recalls) {
150
+ if (day !== undefined && !recall.at.startsWith(day))
151
+ continue;
152
+ const key = [recall.mode, recall.arm, recall.answer.kind].join(' ');
153
+ const held = rows.get(key) ?? { row: { mode: recall.mode, arm: recall.arm, kind: recall.answer.kind, recalls: 0, sessions: 0, tokens: 0, windows: 0, followed: 0, windowCalls: 0, windowTokens: null, overlapped: 0 }, sessions: new Set() };
154
+ rows.set(key, held);
155
+ const { row, sessions } = held;
156
+ row.recalls += 1;
157
+ row.tokens += recall.answer.tokens;
158
+ if (recall.sid !== null)
159
+ sessions.add(recall.sid);
160
+ row.sessions = sessions.size;
161
+ const { consequence } = recall;
162
+ if (consequence === undefined)
163
+ continue;
164
+ row.windows += 1;
165
+ if (consequence.followed)
166
+ row.followed += 1;
167
+ row.windowCalls += consequence.window.calls;
168
+ if (consequence.cost.tokens !== null)
169
+ row.windowTokens = (row.windowTokens ?? 0) + consequence.cost.tokens;
170
+ if (recall.arm === 'held' && (shown.get(recall.sid) ?? []).some((window) => sharesACall(consequence.window, window)))
171
+ row.overlapped += 1;
172
+ }
173
+ return [...rows.entries()].sort(([a], [b]) => (a < b ? -1 : 1)).map(([, { row }]) => row);
174
+ };
175
+ export const formatRecallCounts = (rows) => rows.length === 0
176
+ ? 'No recalls in that span.'
177
+ : rows
178
+ .map((row) => `${row.mode} ${row.arm} ${row.kind} | ${String(row.recalls)} recalls, ${String(row.sessions)} sessions, ${String(row.tokens)} tokens | ${String(row.windows)} windows, ${String(row.followed)} followed, ${String(row.windowCalls)} calls, ${row.windowTokens === null ? 'window tokens unmeasured' : `${String(row.windowTokens)} window tokens`}, ${String(row.overlapped)} overlapped`)
179
+ .join('\n');
@@ -0,0 +1,18 @@
1
+ import { type Pool } from './cells.js';
2
+ import { type Position, type RecoveryEdges } from './recall-next.js';
3
+ import type { Store } from './store.js';
4
+ /** The positions one frame keeps: a table prints at most eight, so twelve leaves room for the bundle's to rank ahead without carrying every position a store has. */
5
+ export declare const PER_FRAME_POSITIONS = 12;
6
+ export interface NextByFrame {
7
+ recovery: {
8
+ [frame: string]: RecoveryEdges;
9
+ };
10
+ positions: {
11
+ [frame: string]: Position[];
12
+ };
13
+ }
14
+ /**
15
+ * The recovery edges and split positions for each harness (key `<harness>/*`, for a session whose version is not stated) and each harness version bucket (`<harness>/<bucket>`).
16
+ * Every recovery key is kept, a below-floor one included, so a hook can tell "a cell, too thin to serve" from "no cell".
17
+ */
18
+ export declare const nextByFrame: (pool: Pool, store: Store) => NextByFrame;
@@ -0,0 +1,44 @@
1
+ // The local half of a next-node read, in the asker's frame (t926 Item 9). The pool's next-step and recovery reads sum every harness and version a store holds, so a table built from
2
+ // them would tell a Codex session what Claude Code sessions did. This derives the two families again over the cells of each harness and version bucket, once per deposit, and the
3
+ // hook index keeps the result by frame key; a hook then reads the one entry for the session it serves. It is its own module because the families it imports are the pool's,
4
+ // which a lookup hook must not load (the index step imports this one lazily, as it does `pool.js`).
5
+ import { bucketFields } from './cells.js';
6
+ import { makeRead, orderResolver } from './read.js';
7
+ import { deriveNextStep } from './read-next-step.js';
8
+ import { deriveRecoveryEdges } from './read-recovery-edge.js';
9
+ import { positionsOf, recoveryEdgesOf } from './recall-next.js';
10
+ /** The positions one frame keeps: a table prints at most eight, so twelve leaves room for the bundle's to rank ahead without carrying every position a store has. */
11
+ export const PER_FRAME_POSITIONS = 12;
12
+ const readsOf = (pool, store) => {
13
+ const asOf = { pool: pool.version, day: pool.lastDay };
14
+ const orderOf = orderResolver([]);
15
+ return [...deriveNextStep({ pool, store }), ...deriveRecoveryEdges({ pool, store })].map((body) => makeRead({ ...body, asOf }, orderOf));
16
+ };
17
+ /**
18
+ * The recovery edges and split positions for each harness (key `<harness>/*`, for a session whose version is not stated) and each harness version bucket (`<harness>/<bucket>`).
19
+ * Every recovery key is kept, a below-floor one included, so a hook can tell "a cell, too thin to serve" from "no cell".
20
+ */
21
+ export const nextByFrame = (pool, store) => {
22
+ const frames = new Map();
23
+ for (const cell of pool.cells) {
24
+ if (cell.frameBucket.startsWith('env:'))
25
+ continue;
26
+ const { harness, harnessBucket } = bucketFields(cell.frameBucket);
27
+ for (const key of [`${harness}/*`, `${harness}/${harnessBucket}`]) {
28
+ const buckets = frames.get(key) ?? new Set();
29
+ frames.set(key, buckets);
30
+ buckets.add(cell.frameBucket);
31
+ }
32
+ }
33
+ const out = { recovery: {}, positions: {} };
34
+ for (const [key, buckets] of frames) {
35
+ const reads = readsOf({ ...pool, cells: pool.cells.filter((cell) => buckets.has(cell.frameBucket)) }, store);
36
+ const recovery = recoveryEdgesOf(reads);
37
+ const positions = positionsOf(reads).slice(0, PER_FRAME_POSITIONS);
38
+ if (Object.keys(recovery).length > 0)
39
+ out.recovery[key] = recovery;
40
+ if (positions.length > 0)
41
+ out.positions[key] = positions;
42
+ }
43
+ return out;
44
+ };
@@ -0,0 +1,145 @@
1
+ import type { Read } from './read.js';
2
+ import { type Recall } from './recall-object.js';
3
+ import { type RecallRead } from './recall-reads.js';
4
+ import type { Frame, Harness } from './schema.js';
5
+ /**
6
+ * The count a recovery edge needs before it is served from the local pool. The pool's own served floor is `K_CELL` (5 accounts, `packages/pool/src/derive.ts`); a bundle's read has
7
+ * cleared it already, so only a local edge is held to this, counted in followed steps because one operator's store is one account.
8
+ */
9
+ export declare const RECOVERY_FLOOR = 5;
10
+ /** The steps a position needs behind it before it can be called split: t782's qualifying cell is a cell topped up to 20 sessions. */
11
+ export declare const POSITION_MIN_N = 20;
12
+ /** The band of t782 plan 1.7: the leading option's share, 20 to 80 percent inclusive. A settled position (above) and a diffuse one (below) are not listed. */
13
+ export declare const POSITION_BAND: {
14
+ readonly low: 0.2;
15
+ readonly high: 0.8;
16
+ };
17
+ /**
18
+ * The bytes a table may add to a turn's context. Eight lines of about 70 bytes: a session start or a prompt carries it every time it is sent, and a table the agent has to
19
+ * read past is one it stops reading. Bundle positions come first, then local ones, each by observations; the cap cuts from the end, so a local position goes before a bundle one.
20
+ */
21
+ export declare const TABLE_CAP_BYTES = 600;
22
+ export interface RecoveryEdge {
23
+ next: string;
24
+ followed: number;
25
+ /** The failures of this action and code over every week, the one denominator of every edge's share. */
26
+ of: number;
27
+ clusters: number;
28
+ /** The heaviest week's read, the one the row cites. */
29
+ readId: string;
30
+ day: string | null;
31
+ }
32
+ export interface RecoveryEdges {
33
+ [actionAndCode: string]: RecoveryEdge[];
34
+ }
35
+ export interface Position {
36
+ prefix: string;
37
+ n: number;
38
+ /** The three most common next steps with their counts. */
39
+ next: Array<[string, number]>;
40
+ clusters: number;
41
+ readId: string;
42
+ day: string | null;
43
+ }
44
+ /** The reads one source (the kept bundle, or the local pool) gives the two hooks. */
45
+ export interface NextReads {
46
+ poolVersion: string;
47
+ recovery: RecoveryEdges;
48
+ positions: Position[];
49
+ }
50
+ /**
51
+ * The recovery edges of a set of reads, by `action/errorCode`, each next step summed over its weeks. A next step that is not a canonical action (or `none`) is left out, so the
52
+ * names a hook shows are the closed ones whatever a store holds.
53
+ */
54
+ export declare const recoveryEdgesOf: (reads: readonly Read[], keep?: (followed: number) => boolean) => RecoveryEdges;
55
+ /** The positions of a set of reads where the next step is split by t782's rule: enough observations, and the leading step's share inside the band. */
56
+ export declare const positionsOf: (reads: readonly Read[]) => Position[];
57
+ export type RecoveryDecision = {
58
+ kind: 'serve';
59
+ source: 'bundle' | 'local';
60
+ edges: RecoveryEdge[];
61
+ poolVersion: string;
62
+ }
63
+ /** The top next step is the failed action again: nothing to add to what the agent does anyway. */
64
+ | {
65
+ kind: 'retry';
66
+ }
67
+ /** A local edge under the floor. */
68
+ | {
69
+ kind: 'floor';
70
+ }
71
+ /** A cell covers this action and error class and its top next step is that nothing followed: the failure is covered, so no older line answers it either. */
72
+ | {
73
+ kind: 'ended';
74
+ }
75
+ /** No cell for this action and error class. */
76
+ | {
77
+ kind: 'none';
78
+ };
79
+ /**
80
+ * What a failure is answered with: the bundle's edges for this action and error class if it holds any (they cleared the pool's floor), else the local pool's. Silent when the top
81
+ * next step is a same-step retry (direction 13c), when a local edge is under `RECOVERY_FLOOR`, or when the top is that nothing followed (`ended`).
82
+ */
83
+ export declare const recoveryDecision: (bundle: NextReads | undefined, local: {
84
+ recovery: RecoveryEdges;
85
+ poolVersion: string;
86
+ } | undefined, action: string, code: string) => RecoveryDecision;
87
+ /** The recovery read as text: names (closed) and numbers. */
88
+ export declare const renderRecovery: (action: string, code: string, edges: readonly RecoveryEdge[]) => {
89
+ rendered: string;
90
+ names: string[];
91
+ };
92
+ /** The positions table, with every position that fits under the cap and nothing when none does. */
93
+ export declare const renderPositions: (positions: readonly Position[]) => {
94
+ rendered: string;
95
+ listed: Position[];
96
+ names: string[];
97
+ };
98
+ /**
99
+ * The key a hook index files the local pool's reads under for a session: its harness and the bucket of the version it runs (`recall-frames.ts`). A session whose version is not
100
+ * stated is read across every version of its harness, which is the filter that can be applied honestly.
101
+ */
102
+ export declare const frameKeyOf: (asker: Asker) => string;
103
+ /** The bundle's and the local pool's positions as one list, a prefix once: the bundle's positions by observations, then the local ones it lacks by observations. */
104
+ export declare const mergePositions: (bundle: readonly Position[], local: readonly Position[]) => Position[];
105
+ export interface Asker {
106
+ harness: Harness;
107
+ harnessVersion: string | null;
108
+ }
109
+ export interface NextInput {
110
+ storeDir: string;
111
+ sid: string;
112
+ /** The tail of the transcript the hook was given, which states the version and model; empty when it gave none. */
113
+ tail: string;
114
+ platform: string;
115
+ at: string;
116
+ }
117
+ /** The asker's frame, from the tail of its transcript. */
118
+ export declare const frameFor: (input: Pick<NextInput, "tail" | "platform" | "at">) => Frame;
119
+ /** The reads of the kept bundle for the asker's harness and version; undefined with no version stated, no bundle, or a bundle that does not verify (never an error). */
120
+ export declare const bundleNext: (storeDir: string, asker: Asker) => Promise<NextReads | undefined>;
121
+ /**
122
+ * A served or held read: the row is written before anything is injected, and the arm is the session's for this query against this pool. The row is the recall's exposure record
123
+ * (ids and counts, and the rendered text a held read would have shown); an arm that kept it back is returned so the hook injects nothing.
124
+ */
125
+ export declare const recordRead: (input: NextInput, frame: Frame, query: Recall["query"], armPrefix: readonly string[], poolVersion: string, rendered: string, names: readonly string[], reads: readonly RecallRead[]) => Promise<Recall>;
126
+ /** The recovery read for one failure, as a recall row. */
127
+ export declare const recoveryRecall: (input: NextInput, setup: Asker, action: string, code: string, decision: Extract<RecoveryDecision, {
128
+ kind: "serve";
129
+ }>) => Promise<Recall>;
130
+ /** The old signature line's recall: the local signature index, held to the same retry rule and floor. */
131
+ export declare const signatureRecall: (input: NextInput, setup: Asker, tool: string, sig: string, found: {
132
+ sessions: number;
133
+ followed: Array<[string, number]>;
134
+ }, poolVersion: string) => Promise<Recall>;
135
+ /**
136
+ * The signature line is a same-step retry when the tool that failed is the one that most often followed, strictly: a tie with another tool is not a retry-only cell. It keeps its
137
+ * own floor, the index's two sessions: it states how many sessions saw the error, so a thin count is printed as thin, which a cell's share is not.
138
+ */
139
+ export declare const isSignatureRetry: (tool: string, found: {
140
+ followed: Array<[string, number]>;
141
+ }) => boolean;
142
+ /** The positions table for a session, as a recall row; undefined when no position qualifies or the table is the one `previous` already sent. */
143
+ export declare const positionsRecall: (input: NextInput, setup: Asker, positions: readonly Position[], poolVersion: string, previous?: string) => Promise<Recall | undefined>;
144
+ /** The text the session was last sent a table with, from `recalls.jsonl`; undefined when it was never sent one. */
145
+ export declare const lastTable: (storeDir: string, sid: string) => Promise<string | undefined>;
@@ -0,0 +1,241 @@
1
+ // The next-node read (t926): what the agent is shown at the moments before it decides. Two reads, both from cells and never from text: the recovery edge (what followed this
2
+ // failure, for this action and error class) and the split positions (the places in a repo's earlier work where the next step is not settled). A read is built from the kept
3
+ // bundle for the session's setup first and the local pool's reads second, and every shown or held one is a `Recall` row, so the holdback arms are real.
4
+ import { CANONICAL_ACTIONS } from './actions.js';
5
+ import { harnessBucket } from './cells.js';
6
+ import { appendRecall, armOf, makeRecall, parseRecalls, recallsPath } from './recall-object.js';
7
+ import { frameOfTail, tailOf } from './recall-push.js';
8
+ import { sessionBundle } from './pool-replica.js';
9
+ import { readIdOf } from './recall-reads.js';
10
+ import { closedName, count, evidenceOf } from './recall-render.js';
11
+ import { LITERAL } from './public-vocabulary.js';
12
+ /**
13
+ * The count a recovery edge needs before it is served from the local pool. The pool's own served floor is `K_CELL` (5 accounts, `packages/pool/src/derive.ts`); a bundle's read has
14
+ * cleared it already, so only a local edge is held to this, counted in followed steps because one operator's store is one account.
15
+ */
16
+ export const RECOVERY_FLOOR = 5;
17
+ /** The steps a position needs behind it before it can be called split: t782's qualifying cell is a cell topped up to 20 sessions. */
18
+ export const POSITION_MIN_N = 20;
19
+ /** The band of t782 plan 1.7: the leading option's share, 20 to 80 percent inclusive. A settled position (above) and a diffuse one (below) are not listed. */
20
+ export const POSITION_BAND = { low: 0.2, high: 0.8 };
21
+ /**
22
+ * The bytes a table may add to a turn's context. Eight lines of about 70 bytes: a session start or a prompt carries it every time it is sent, and a table the agent has to
23
+ * read past is one it stops reading. Bundle positions come first, then local ones, each by observations; the cap cuts from the end, so a local position goes before a bundle one.
24
+ */
25
+ export const TABLE_CAP_BYTES = 600;
26
+ const TOP_NEXT = 3;
27
+ const MAX_FOLLOWED = 3;
28
+ const isCount = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0;
29
+ const isAction = (name) => CANONICAL_ACTIONS.some((action) => action === name);
30
+ const byWeight = (a, b) => b.followed - a.followed || a.next.localeCompare(b.next);
31
+ /**
32
+ * The recovery edges of a set of reads, by `action/errorCode`, each next step summed over its weeks. A next step that is not a canonical action (or `none`) is left out, so the
33
+ * names a hook shows are the closed ones whatever a store holds.
34
+ */
35
+ export const recoveryEdgesOf = (reads, keep = () => true) => {
36
+ const grouped = new Map();
37
+ // The failures of one action and error class, once: every week's reads of its next steps share a week's failures, so a next step seen in fewer weeks must not get a smaller denominator.
38
+ // A week's failures are the sum of what followed them, so the total over every next step is that sum over all its weeks.
39
+ const failures = new Map();
40
+ for (const read of reads) {
41
+ if (read.family !== 'recovery')
42
+ continue;
43
+ const { action, errorCode, next } = read.slice;
44
+ const { followed, of } = read.value;
45
+ if (action === undefined || errorCode === undefined || next === undefined || !isCount(followed) || !isCount(of) || followed === 0)
46
+ continue;
47
+ const key = `${action}/${errorCode}`;
48
+ failures.set(key, (failures.get(key) ?? 0) + followed);
49
+ if (!(next === 'none' || isAction(next)))
50
+ continue;
51
+ const edges = grouped.get(key) ?? new Map();
52
+ grouped.set(key, edges);
53
+ const have = edges.get(next);
54
+ if (have === undefined) {
55
+ edges.set(next, { edge: { next, followed, of, clusters: read.clusters, readId: read.readId, day: read.asOf.day }, heaviest: followed });
56
+ continue;
57
+ }
58
+ have.edge.followed += followed;
59
+ have.edge.clusters = Math.max(have.edge.clusters, read.clusters);
60
+ if (followed > have.heaviest) {
61
+ have.heaviest = followed;
62
+ have.edge.readId = read.readId;
63
+ }
64
+ if (read.asOf.day !== null && (have.edge.day === null || read.asOf.day > have.edge.day))
65
+ have.edge.day = read.asOf.day;
66
+ }
67
+ const edges = {};
68
+ for (const [key, byNext] of grouped) {
69
+ const sorted = [...byNext.values()].map((weighted) => ({ ...weighted.edge, of: failures.get(key) ?? weighted.edge.of })).sort(byWeight);
70
+ if (sorted[0] !== undefined && keep(sorted[0].followed))
71
+ edges[key] = sorted.slice(0, MAX_FOLLOWED);
72
+ }
73
+ return edges;
74
+ };
75
+ /** True when a position's prefix names only canonical actions (no identity, no class): the table prints names and numbers and nothing a store wrote. */
76
+ const closedPrefix = (prefix) => prefix.split('>').every(isAction);
77
+ /** The positions of a set of reads where the next step is split by t782's rule: enough observations, and the leading step's share inside the band. */
78
+ export const positionsOf = (reads) => {
79
+ const positions = [];
80
+ for (const read of reads) {
81
+ if (read.family !== 'next-step')
82
+ continue;
83
+ const { prefix } = read.slice;
84
+ const { next, topShare } = read.value;
85
+ if (prefix === undefined || !closedPrefix(prefix) || !isCount(topShare) || topShare < POSITION_BAND.low || topShare > POSITION_BAND.high || read.n < POSITION_MIN_N)
86
+ continue;
87
+ if (typeof next !== 'object' || next === null)
88
+ continue;
89
+ const ranked = Object.entries(next)
90
+ .filter(([action, times]) => isAction(action) && isCount(times))
91
+ .sort(([a, one], [b, two]) => two - one || a.localeCompare(b))
92
+ .slice(0, TOP_NEXT);
93
+ if (ranked.length < 2)
94
+ continue;
95
+ positions.push({ prefix, n: read.n, next: ranked, clusters: read.clusters, readId: read.readId, day: read.asOf.day });
96
+ }
97
+ return positions.sort((a, b) => b.n - a.n || a.prefix.localeCompare(b.prefix));
98
+ };
99
+ /**
100
+ * What a failure is answered with: the bundle's edges for this action and error class if it holds any (they cleared the pool's floor), else the local pool's. Silent when the top
101
+ * next step is a same-step retry (direction 13c), when a local edge is under `RECOVERY_FLOOR`, or when the top is that nothing followed (`ended`).
102
+ */
103
+ export const recoveryDecision = (bundle, local, action, code) => {
104
+ const key = `${action}/${code}`;
105
+ const fromBundle = bundle?.recovery[key];
106
+ const source = fromBundle === undefined || bundle === undefined ? undefined : { edges: fromBundle, source: 'bundle', poolVersion: bundle.poolVersion };
107
+ const fromLocal = local?.recovery[key];
108
+ const chosen = source ?? (fromLocal === undefined || local === undefined ? undefined : { edges: fromLocal, source: 'local', poolVersion: local.poolVersion });
109
+ const top = chosen?.edges[0];
110
+ if (chosen === undefined || top === undefined)
111
+ return { kind: 'none' };
112
+ // A tie with another next step is not a retry-only cell, so it is served; the strict lead is what direction 13c silences.
113
+ const second = chosen.edges[1];
114
+ if (top.next === action && (second === undefined || top.followed > second.followed))
115
+ return { kind: 'retry' };
116
+ if (top.next === 'none')
117
+ return { kind: 'ended' };
118
+ if (chosen.source === 'local' && top.followed < RECOVERY_FLOOR)
119
+ return { kind: 'floor' };
120
+ return { kind: 'serve', ...chosen };
121
+ };
122
+ const percent = (part, whole) => `${String(Math.round((part / whole) * 100))}%`;
123
+ /** The recovery read as text: names (closed) and numbers. */
124
+ export const renderRecovery = (action, code, edges) => {
125
+ const shown = edges.filter((edge) => closedName(edge.next) !== LITERAL);
126
+ const total = shown[0]?.of ?? 0;
127
+ const lines = shown.map((edge) => `${edge.next} x${String(edge.followed)}${total > 0 ? ` (${percent(edge.followed, total)})` : ''}`);
128
+ const rendered = [`oars recall, counts from earlier sessions: after a failed ${action} (${code})${total > 0 ? ` in ${count(total, 'failure')}` : ''}, the next step was`, ` ${lines.join(', ')}`, ` read ${shown[0]?.readId ?? ''} (recovery)`].join('\n');
129
+ return { rendered, names: shown.map((edge) => edge.next) };
130
+ };
131
+ /** The positions table, with every position that fits under the cap and nothing when none does. */
132
+ export const renderPositions = (positions) => {
133
+ const header = 'oars recall, next steps that earlier sessions split on (counts, not advice):';
134
+ const listed = [];
135
+ let used = Buffer.byteLength(header);
136
+ const lines = [];
137
+ for (const position of positions) {
138
+ const line = `- after ${position.prefix}: ${position.next.map(([action, times]) => `${action} ${percent(times, position.n)}`).join(', ')} (n ${String(position.n)})`;
139
+ const size = Buffer.byteLength(line) + 1;
140
+ if (used + size > TABLE_CAP_BYTES)
141
+ break;
142
+ used += size;
143
+ listed.push(position);
144
+ lines.push(line);
145
+ }
146
+ return { rendered: listed.length === 0 ? '' : [header, ...lines].join('\n'), listed, names: [...new Set(listed.flatMap((position) => position.next.map(([action]) => action)))] };
147
+ };
148
+ /**
149
+ * The key a hook index files the local pool's reads under for a session: its harness and the bucket of the version it runs (`recall-frames.ts`). A session whose version is not
150
+ * stated is read across every version of its harness, which is the filter that can be applied honestly.
151
+ */
152
+ export const frameKeyOf = (asker) => `${asker.harness}/${asker.harnessVersion === null ? '*' : harnessBucket(asker.harnessVersion)}`;
153
+ /** The bundle's and the local pool's positions as one list, a prefix once: the bundle's positions by observations, then the local ones it lacks by observations. */
154
+ export const mergePositions = (bundle, local) => {
155
+ const byObservations = (a, b) => b.n - a.n || a.prefix.localeCompare(b.prefix);
156
+ const seen = new Set(bundle.map((position) => position.prefix));
157
+ return [...[...bundle].sort(byObservations), ...local.filter((position) => !seen.has(position.prefix)).sort(byObservations)];
158
+ };
159
+ /** A read as the evidence record wants it: counts and closed names only. */
160
+ const evidenceRead = (read, setup) => ({
161
+ readId: read.readId,
162
+ family: read.family,
163
+ tool: read.tool,
164
+ keys: [],
165
+ n: read.n,
166
+ count: read.count,
167
+ calls: read.calls,
168
+ clusters: read.clusters,
169
+ firstDay: null,
170
+ lastDay: read.day,
171
+ harnesses: [setup.harness],
172
+ harnessVersions: setup.harnessVersion === null ? [] : [setup.harnessVersion],
173
+ models: [],
174
+ followed: read.followed,
175
+ });
176
+ /** The asker's frame, from the tail of its transcript. */
177
+ export const frameFor = (input) => frameOfTail(input.tail, input.platform, input.at);
178
+ /** The reads of the kept bundle for the asker's harness and version; undefined with no version stated, no bundle, or a bundle that does not verify (never an error). */
179
+ export const bundleNext = async (storeDir, asker) => {
180
+ const bundle = asker.harnessVersion === null ? undefined : await sessionBundle(storeDir, { harness: asker.harness, harnessVersion: asker.harnessVersion });
181
+ return bundle === undefined ? undefined : { poolVersion: bundle.asOf.pool, recovery: recoveryEdgesOf(bundle.reads), positions: positionsOf(bundle.reads) };
182
+ };
183
+ /**
184
+ * A served or held read: the row is written before anything is injected, and the arm is the session's for this query against this pool. The row is the recall's exposure record
185
+ * (ids and counts, and the rendered text a held read would have shown); an arm that kept it back is returned so the hook injects nothing.
186
+ */
187
+ export const recordRead = async (input, frame, query, armPrefix, poolVersion, rendered, names, reads) => {
188
+ const recall = makeRecall({
189
+ sid: input.sid,
190
+ at: input.at,
191
+ frame,
192
+ mode: 'push',
193
+ query,
194
+ poolVersion,
195
+ arm: armOf(input.sid, armPrefix, poolVersion),
196
+ reads: reads.map((read) => read.readId),
197
+ rendered,
198
+ names,
199
+ evidence: reads.map((read) => evidenceOf(read, frame)),
200
+ });
201
+ await appendRecall(input.storeDir, recall);
202
+ return recall;
203
+ };
204
+ /** The recovery read for one failure, as a recall row. */
205
+ export const recoveryRecall = (input, setup, action, code, decision) => {
206
+ const frame = frameFor(input);
207
+ const { rendered, names } = renderRecovery(action, code, decision.edges);
208
+ const top = decision.edges[0];
209
+ const read = evidenceRead({ readId: top?.readId ?? '', family: 'recovery', tool: action, n: top?.followed ?? 0, count: top?.followed ?? 0, calls: top?.of ?? null, clusters: top?.clusters ?? 1, day: top?.day ?? null, followed: decision.edges.map((edge) => [edge.next, edge.followed]) }, setup);
210
+ return recordRead(input, frame, { form: 'question', family: 'recovery', prefix: [`${action}/${code}`] }, [`${action}/${code}`], decision.poolVersion, rendered, names, [read]);
211
+ };
212
+ /** The old signature line's recall: the local signature index, held to the same retry rule and floor. */
213
+ export const signatureRecall = (input, setup, tool, sig, found, poolVersion) => {
214
+ const frame = frameFor(input);
215
+ const followed = found.followed.map(([next]) => next).join(', ');
216
+ const rendered = `oars recall: seen in ${String(found.sessions)} sessions; what followed: ${followed === '' ? 'nothing recorded' : followed}`;
217
+ const read = evidenceRead({ readId: readIdOf(sig), family: 'recovery', tool, n: found.sessions, count: found.sessions, calls: null, clusters: 1, day: null, followed: found.followed }, setup);
218
+ return recordRead(input, frame, { form: 'question', family: 'recovery', prefix: [readIdOf(sig)] }, [readIdOf(sig)], poolVersion, rendered, found.followed.map(([next]) => next), [read]);
219
+ };
220
+ /**
221
+ * The signature line is a same-step retry when the tool that failed is the one that most often followed, strictly: a tie with another tool is not a retry-only cell. It keeps its
222
+ * own floor, the index's two sessions: it states how many sessions saw the error, so a thin count is printed as thin, which a cell's share is not.
223
+ */
224
+ export const isSignatureRetry = (tool, found) => {
225
+ const [first, second] = found.followed;
226
+ return first?.[0] === tool && (second === undefined || first[1] > second[1]);
227
+ };
228
+ /** The positions table for a session, as a recall row; undefined when no position qualifies or the table is the one `previous` already sent. */
229
+ export const positionsRecall = (input, setup, positions, poolVersion, previous) => {
230
+ const { rendered, listed, names } = renderPositions(positions);
231
+ if (listed.length === 0 || rendered === previous)
232
+ return Promise.resolve(undefined);
233
+ const frame = frameFor(input);
234
+ const reads = listed.map((position) => evidenceRead({ readId: position.readId, family: 'next-step', tool: position.next[0]?.[0] ?? '', n: position.n, count: position.next[0]?.[1] ?? 0, calls: position.n, clusters: position.clusters, day: position.day, followed: position.next }, setup));
235
+ return recordRead(input, frame, { form: 'question', family: 'next-step', prefix: ['positions'] }, ['positions'], poolVersion, rendered, names, reads);
236
+ };
237
+ /** The text the session was last sent a table with, from `recalls.jsonl`; undefined when it was never sent one. */
238
+ export const lastTable = async (storeDir, sid) => {
239
+ const mine = parseRecalls(await tailOf(recallsPath(storeDir))).filter((recall) => recall.mode === 'push' && recall.sid === sid && recall.query.family === 'next-step');
240
+ return mine.at(-1)?.answer.rendered;
241
+ };