@manyoars/oars 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) hide show
  1. package/README.md +132 -28
  2. package/dist/api.js +1 -1
  3. package/dist/bin.js +6 -29
  4. package/dist/cli.d.ts +6 -3
  5. package/dist/cli.js +367 -88
  6. package/dist/commands/keys.d.ts +31 -0
  7. package/dist/commands/keys.js +65 -0
  8. package/dist/commands/login.js +2 -2
  9. package/dist/errors.d.ts +13 -13
  10. package/dist/errors.js +28 -11
  11. package/dist/gate/checks.d.ts +68 -0
  12. package/dist/gate/checks.js +584 -0
  13. package/dist/gate/command.d.ts +14 -0
  14. package/dist/gate/command.js +61 -0
  15. package/dist/gate/hook-fill-main.d.ts +1 -0
  16. package/dist/gate/hook-fill-main.js +16 -0
  17. package/dist/gate/hook-fill.d.ts +38 -0
  18. package/dist/gate/hook-fill.js +94 -0
  19. package/dist/gate/hook-main.d.ts +1 -0
  20. package/dist/gate/hook-main.js +35 -0
  21. package/dist/gate/hook-process.d.ts +14 -0
  22. package/dist/gate/hook-process.js +64 -0
  23. package/dist/gate/hook.d.ts +46 -0
  24. package/dist/gate/hook.js +256 -0
  25. package/dist/gate/install.d.ts +25 -0
  26. package/dist/gate/install.js +126 -0
  27. package/dist/gate/local-bin.d.ts +8 -0
  28. package/dist/gate/local-bin.js +36 -0
  29. package/dist/gate/notes.d.ts +19 -0
  30. package/dist/gate/notes.js +27 -0
  31. package/dist/gate/novelty.d.ts +35 -0
  32. package/dist/gate/novelty.js +99 -0
  33. package/dist/gate/policy.d.ts +25 -0
  34. package/dist/gate/policy.js +73 -0
  35. package/dist/gate/record.d.ts +120 -0
  36. package/dist/gate/record.js +125 -0
  37. package/dist/gate/registry-state.d.ts +10 -0
  38. package/dist/gate/registry-state.js +30 -0
  39. package/dist/gate/registry.d.ts +56 -0
  40. package/dist/gate/registry.js +94 -0
  41. package/dist/gate/replay.d.ts +20 -0
  42. package/dist/gate/replay.js +257 -0
  43. package/dist/gate/report.d.ts +50 -0
  44. package/dist/gate/report.js +89 -0
  45. package/dist/gate/scope.d.ts +7 -0
  46. package/dist/gate/scope.js +27 -0
  47. package/dist/gate/transcripts.d.ts +68 -0
  48. package/dist/gate/transcripts.js +232 -0
  49. package/dist/hook-bin.d.ts +1 -0
  50. package/dist/hook-bin.js +25 -0
  51. package/dist/issuer.d.ts +7 -1
  52. package/dist/issuer.js +18 -1
  53. package/dist/main.d.ts +1 -0
  54. package/dist/main.js +64 -0
  55. package/dist/oidc.js +1 -1
  56. package/dist/sessions/actions.d.ts +35 -0
  57. package/dist/sessions/actions.js +351 -0
  58. package/dist/sessions/allow.d.ts +14 -0
  59. package/dist/sessions/allow.js +51 -0
  60. package/dist/sessions/archive.d.ts +11 -0
  61. package/dist/sessions/archive.js +38 -0
  62. package/dist/sessions/audit.d.ts +42 -0
  63. package/dist/sessions/audit.js +104 -0
  64. package/dist/sessions/baseline.d.ts +62 -0
  65. package/dist/sessions/baseline.js +188 -0
  66. package/dist/sessions/capture-runtime.d.ts +10 -0
  67. package/dist/sessions/capture-runtime.js +12 -0
  68. package/dist/sessions/cell-increments.d.ts +43 -0
  69. package/dist/sessions/cell-increments.js +209 -0
  70. package/dist/sessions/cells.d.ts +79 -0
  71. package/dist/sessions/cells.js +181 -0
  72. package/dist/sessions/codex.d.ts +53 -0
  73. package/dist/sessions/codex.js +679 -0
  74. package/dist/sessions/command.d.ts +117 -0
  75. package/dist/sessions/command.js +373 -0
  76. package/dist/sessions/copilot.d.ts +40 -0
  77. package/dist/sessions/copilot.js +432 -0
  78. package/dist/sessions/cursor.d.ts +24 -0
  79. package/dist/sessions/cursor.js +324 -0
  80. package/dist/sessions/derive.d.ts +57 -0
  81. package/dist/sessions/derive.js +320 -0
  82. package/dist/sessions/exclusion.d.ts +81 -0
  83. package/dist/sessions/exclusion.js +353 -0
  84. package/dist/sessions/extract.d.ts +179 -0
  85. package/dist/sessions/extract.js +1774 -0
  86. package/dist/sessions/fold.d.ts +17 -0
  87. package/dist/sessions/fold.js +71 -0
  88. package/dist/sessions/frame.d.ts +24 -0
  89. package/dist/sessions/frame.js +95 -0
  90. package/dist/sessions/gemini.d.ts +37 -0
  91. package/dist/sessions/gemini.js +613 -0
  92. package/dist/sessions/gold-arms.d.ts +67 -0
  93. package/dist/sessions/gold-arms.js +172 -0
  94. package/dist/sessions/gold.d.ts +197 -0
  95. package/dist/sessions/gold.js +463 -0
  96. package/dist/sessions/graph.d.ts +12 -0
  97. package/dist/sessions/graph.js +34 -0
  98. package/dist/sessions/grok.d.ts +36 -0
  99. package/dist/sessions/grok.js +421 -0
  100. package/dist/sessions/hook-calls.d.ts +3 -0
  101. package/dist/sessions/hook-calls.js +47 -0
  102. package/dist/sessions/hook-index.d.ts +53 -0
  103. package/dist/sessions/hook-index.js +157 -0
  104. package/dist/sessions/hooks.d.ts +13 -0
  105. package/dist/sessions/hooks.js +170 -0
  106. package/dist/sessions/identity.d.ts +22 -0
  107. package/dist/sessions/identity.js +101 -0
  108. package/dist/sessions/install.d.ts +54 -0
  109. package/dist/sessions/install.js +201 -0
  110. package/dist/sessions/lineage.d.ts +19 -0
  111. package/dist/sessions/lineage.js +118 -0
  112. package/dist/sessions/mcp.d.ts +6 -0
  113. package/dist/sessions/mcp.js +153 -0
  114. package/dist/sessions/merkle.d.ts +7 -0
  115. package/dist/sessions/merkle.js +28 -0
  116. package/dist/sessions/motif.d.ts +43 -0
  117. package/dist/sessions/motif.js +59 -0
  118. package/dist/sessions/package-resolve.d.ts +30 -0
  119. package/dist/sessions/package-resolve.js +183 -0
  120. package/dist/sessions/pending.d.ts +34 -0
  121. package/dist/sessions/pending.js +142 -0
  122. package/dist/sessions/pool-command.d.ts +44 -0
  123. package/dist/sessions/pool-command.js +542 -0
  124. package/dist/sessions/pool-delta.d.ts +54 -0
  125. package/dist/sessions/pool-delta.js +190 -0
  126. package/dist/sessions/pool-outbox.d.ts +63 -0
  127. package/dist/sessions/pool-outbox.js +260 -0
  128. package/dist/sessions/pool-print.d.ts +26 -0
  129. package/dist/sessions/pool-print.js +68 -0
  130. package/dist/sessions/pool-wire.d.ts +225 -0
  131. package/dist/sessions/pool-wire.js +620 -0
  132. package/dist/sessions/pool.d.ts +32 -0
  133. package/dist/sessions/pool.js +164 -0
  134. package/dist/sessions/projection.d.ts +75 -0
  135. package/dist/sessions/projection.js +165 -0
  136. package/dist/sessions/public-vocabulary.d.ts +73 -0
  137. package/dist/sessions/public-vocabulary.js +302 -0
  138. package/dist/sessions/query.d.ts +111 -0
  139. package/dist/sessions/query.js +246 -0
  140. package/dist/sessions/read-adoption.d.ts +2 -0
  141. package/dist/sessions/read-adoption.js +27 -0
  142. package/dist/sessions/read-call-shape.d.ts +2 -0
  143. package/dist/sessions/read-call-shape.js +25 -0
  144. package/dist/sessions/read-change.d.ts +2 -0
  145. package/dist/sessions/read-change.js +30 -0
  146. package/dist/sessions/read-cost.d.ts +4 -0
  147. package/dist/sessions/read-cost.js +37 -0
  148. package/dist/sessions/read-day-move.d.ts +3 -0
  149. package/dist/sessions/read-day-move.js +65 -0
  150. package/dist/sessions/read-fit.d.ts +7 -0
  151. package/dist/sessions/read-fit.js +41 -0
  152. package/dist/sessions/read-interaction.d.ts +5 -0
  153. package/dist/sessions/read-interaction.js +62 -0
  154. package/dist/sessions/read-lead-lag.d.ts +15 -0
  155. package/dist/sessions/read-lead-lag.js +81 -0
  156. package/dist/sessions/read-live.d.ts +6 -0
  157. package/dist/sessions/read-live.js +38 -0
  158. package/dist/sessions/read-method.d.ts +2 -0
  159. package/dist/sessions/read-method.js +29 -0
  160. package/dist/sessions/read-next-step.d.ts +2 -0
  161. package/dist/sessions/read-next-step.js +35 -0
  162. package/dist/sessions/read-order.d.ts +54 -0
  163. package/dist/sessions/read-order.js +62 -0
  164. package/dist/sessions/read-readable.d.ts +8 -0
  165. package/dist/sessions/read-readable.js +24 -0
  166. package/dist/sessions/read-recovery.d.ts +20 -0
  167. package/dist/sessions/read-recovery.js +97 -0
  168. package/dist/sessions/read-rows.d.ts +13 -0
  169. package/dist/sessions/read-rows.js +47 -0
  170. package/dist/sessions/read-substitution.d.ts +5 -0
  171. package/dist/sessions/read-substitution.js +51 -0
  172. package/dist/sessions/read-support.d.ts +48 -0
  173. package/dist/sessions/read-support.js +73 -0
  174. package/dist/sessions/read-tool-day.d.ts +2 -0
  175. package/dist/sessions/read-tool-day.js +26 -0
  176. package/dist/sessions/read-version-boundary.d.ts +31 -0
  177. package/dist/sessions/read-version-boundary.js +95 -0
  178. package/dist/sessions/read.d.ts +79 -0
  179. package/dist/sessions/read.js +84 -0
  180. package/dist/sessions/recall-consequence.d.ts +45 -0
  181. package/dist/sessions/recall-consequence.js +179 -0
  182. package/dist/sessions/recall-object.d.ts +137 -0
  183. package/dist/sessions/recall-object.js +77 -0
  184. package/dist/sessions/recall-pool.d.ts +19 -0
  185. package/dist/sessions/recall-pool.js +96 -0
  186. package/dist/sessions/recall-push.d.ts +26 -0
  187. package/dist/sessions/recall-push.js +95 -0
  188. package/dist/sessions/recall-reads.d.ts +117 -0
  189. package/dist/sessions/recall-reads.js +410 -0
  190. package/dist/sessions/recall-render.d.ts +19 -0
  191. package/dist/sessions/recall-render.js +168 -0
  192. package/dist/sessions/redact.d.ts +6 -0
  193. package/dist/sessions/redact.js +54 -0
  194. package/dist/sessions/repo-root.d.ts +10 -0
  195. package/dist/sessions/repo-root.js +101 -0
  196. package/dist/sessions/schema.d.ts +644 -0
  197. package/dist/sessions/schema.js +148 -0
  198. package/dist/sessions/session-shape.d.ts +73 -0
  199. package/dist/sessions/session-shape.js +342 -0
  200. package/dist/sessions/sources.d.ts +50 -0
  201. package/dist/sessions/sources.js +159 -0
  202. package/dist/sessions/steps.d.ts +105 -0
  203. package/dist/sessions/steps.js +250 -0
  204. package/dist/sessions/store.d.ts +297 -0
  205. package/dist/sessions/store.js +1478 -0
  206. package/dist/sessions/strata.d.ts +26 -0
  207. package/dist/sessions/strata.js +77 -0
  208. package/dist/sessions/survival.d.ts +25 -0
  209. package/dist/sessions/survival.js +230 -0
  210. package/dist/sessions/verify.d.ts +53 -0
  211. package/dist/sessions/verify.js +286 -0
  212. package/dist/sessions/vocab-load.d.ts +26 -0
  213. package/dist/sessions/vocab-load.js +61 -0
  214. package/dist/sessions/vscode-copilot.d.ts +38 -0
  215. package/dist/sessions/vscode-copilot.js +477 -0
  216. package/package.json +4 -5
  217. package/vocab/crates.json +1 -0
  218. package/vocab/go.json +1 -0
  219. package/vocab/harness.json +1 -0
  220. package/vocab/homebrew.json +1 -0
  221. package/vocab/manifest.json +1 -0
  222. package/vocab/maven.json +1 -0
  223. package/vocab/models.json +1 -0
  224. package/vocab/npm.json +1 -0
  225. package/vocab/programs.json +1 -0
  226. package/vocab/pypi.json +1 -0
  227. package/vocab/rubygems.json +1 -0
  228. package/dist/commands/deploy-input.d.ts +0 -38
  229. package/dist/commands/deploy-input.js +0 -150
  230. package/dist/commands/deploy.d.ts +0 -22
  231. package/dist/commands/deploy.js +0 -124
  232. package/dist/commands/status.d.ts +0 -7
  233. package/dist/commands/status.js +0 -18
  234. package/dist/commands/templates.d.ts +0 -11
  235. package/dist/commands/templates.js +0 -32
  236. package/dist/commands/test-charge.d.ts +0 -11
  237. package/dist/commands/test-charge.js +0 -34
  238. package/dist/journal.d.ts +0 -26
  239. package/dist/journal.js +0 -58
  240. package/parity/deploy-plans/paid-api.json +0 -34
  241. package/parity/deploy-plans/per-task-agent.json +0 -34
@@ -0,0 +1,117 @@
1
+ import { type CanonicalAction } from './actions.js';
2
+ import { type RecallQuery, type RecallResult } from './query.js';
3
+ import { type Family } from './read.js';
4
+ import { type CurrentPool } from './recall-pool.js';
5
+ import { type Recall, type RecallForm, type RecallSlice } from './recall-object.js';
6
+ import { type ErrorObservation, type Frame, type ToolObservation } from './schema.js';
7
+ import type { Store } from './store.js';
8
+ export type ReadFamily = Family | 'entity';
9
+ export interface RecallRead {
10
+ readId: string;
11
+ family: ReadFamily;
12
+ /** The native tool name, as recorded. A renderer holds it to the public vocabulary, because a name can carry text. */
13
+ tool: string;
14
+ /** The call's input key names; empty for a read not about a call shape. */
15
+ keys: string[];
16
+ /** Sessions. */
17
+ n: number;
18
+ /** Errors, for recovery and fit; calls, for entity. */
19
+ count: number;
20
+ /** All calls of the shape, the denominator of `count`; null when the read has none. */
21
+ calls: number | null;
22
+ /** One operator's store is one cluster (spec Section 6). */
23
+ clusters: number;
24
+ firstDay: string | null;
25
+ lastDay: string | null;
26
+ harnesses: string[];
27
+ harnessVersions: string[];
28
+ models: string[];
29
+ /** Native tool names that came next, most common first. */
30
+ followed: Array<[string, number]>;
31
+ }
32
+ /** A signature's stand-in: the hash a stored recovery read holds as `slice.sig`. */
33
+ export declare const readIdOf: (sig: string) => string;
34
+ /** The ID of a call's shape: its tool and input key names, nothing the caller typed as a value. */
35
+ export declare const toolShapeId: (tool: string, keys: readonly string[]) => string;
36
+ export declare const recoveryReads: (store: Store, keep?: (error: ErrorObservation) => boolean) => RecallRead[];
37
+ /** The main-line calls of each session, one row per call, in call order. */
38
+ export declare const callsBySession: (store: Store) => Map<string, ToolObservation[]>;
39
+ export interface FitFloors {
40
+ minFailedSessions: number;
41
+ minFailedShare: number;
42
+ minCalls: number;
43
+ }
44
+ /**
45
+ * The floors a call shape needs before it is pushed. On the operator's store (6,264 sessions, 361,914 calls) they keep 14 shapes, among them the wrong-input
46
+ * calls that always fail (`Monitor` with `query`, `Read` with `path`), and leave Bash, whose failures do not depend on its shape. Open parameters.
47
+ */
48
+ export declare const FIT_FLOORS: FitFloors;
49
+ /** `keep` narrows which calls are tallied, not what can come next: the call after a kept one is what followed it, wherever the slice ends. */
50
+ export declare const fitReads: (store: Store, floors?: FitFloors, keep?: (call: ToolObservation) => boolean) => RecallRead[];
51
+ /** The action of a tool under the first of its harnesses whose table lists it, else under the first harness (the default one when a read names none). */
52
+ export declare const actionOfShape: (tool: string, harnesses: readonly string[]) => CanonicalAction;
53
+ /**
54
+ * What a push must beat: a shape is pushed only when its failure share is at least this many times the share of all calls of its action. The floors above keep
55
+ * a shape that fails often; this keeps one that fails more often than its action does, and guards a store whose base rate is high. On the replay of 2026-11-02 it kept
56
+ * 79 of 79 pushes, at 4.5x and 8.6x their action's share: a real signal, not a defect. Open parameter, like the floors.
57
+ */
58
+ export declare const PUSH_MIN_LIFT = 2;
59
+ /**
60
+ * The reads whose shape fails at `lift` times the share of its whole action or more, where the action's share counts every call of every shape of it, those under the floors too.
61
+ * A tool no table lists is the action `other` (or `mcp.call`), so it is held to that action's share like any other and can enter the index and fire. Compared in integers, so a tie is exact.
62
+ */
63
+ export declare const aboveBaseRate: (store: Store, reads: readonly RecallRead[], lift?: number) => RecallRead[];
64
+ /** The snapshot a store answers from: its latest deposit. A deposit time compares with a recall's `at`, which is what "older than the recall" needs. */
65
+ export declare const poolVersionOf: (store: Store) => string;
66
+ export interface ClosedQuery {
67
+ form: RecallForm;
68
+ prefix?: readonly string[];
69
+ entity?: string;
70
+ family?: string;
71
+ slice?: RecallSlice;
72
+ text?: string;
73
+ }
74
+ export type PullInput = ClosedQuery | RecallQuery;
75
+ /** The closed form a command line asks for; undefined when it names none, and only `recall()`'s inputs are left. */
76
+ export declare const closedQueryOf: (options: {
77
+ pattern: readonly string[];
78
+ entity?: string | undefined;
79
+ family?: string | undefined;
80
+ text?: string | undefined;
81
+ cls?: string | undefined;
82
+ sinceDays?: number | undefined;
83
+ }) => ClosedQuery | undefined;
84
+ /** The reads a query's form asks for, best first, over the local store: an empty list is a miss, which a recall keeps. */
85
+ export declare const answerQuery: (store: Store, input: ClosedQuery, now: number, current?: CurrentPool) => RecallRead[];
86
+ /** The families a question can answer. `lead-lag` is experimental, so it stays with `oars sessions read`. */
87
+ export declare const RECALL_FAMILIES: readonly Family[];
88
+ /**
89
+ * Why this pull cannot be asked; undefined when it can. Only `recovery` and `fit` count their answers from the store's own rows, so any other closed form or family cannot apply a
90
+ * slice, and answering them anyway would state counts the slice did not narrow. The text names a form or a family that has passed `isFamily`, so it holds nothing an asker typed.
91
+ */
92
+ export declare const pullRefusal: (input: PullInput) => string | undefined;
93
+ /** The forms a pull can name, in the order a refusal lists them. `error` with `path` is one form: the signature lookup. */
94
+ export declare const FORM_NAMES: readonly ["prefix", "entity", "family", "text", "error or path"];
95
+ /**
96
+ * Why a pull naming several forms cannot be asked; undefined when it names at most one. Answering the first form and dropping the rest would state counts for a question the asker did
97
+ * not put, and the rendered answer names no signature to show it. The text lists fixed form names, so it holds nothing an asker typed.
98
+ */
99
+ export declare const formsRefusal: (named: readonly (typeof FORM_NAMES)[number][]) => string | undefined;
100
+ export interface PullContext {
101
+ /** The session asking; null when the pull came from outside one, which is every pull until the MCP path can name it. */
102
+ sid: string | null;
103
+ now: number;
104
+ /** The asker's frame, when the caller knows it; otherwise `sid` may name a stored session, whose frame stands in. */
105
+ frame: Frame | null;
106
+ redactDomains?: readonly string[];
107
+ /** The pool an answer's stored read comes from; without one, an answer keeps its stand-in hash. */
108
+ current?: CurrentPool | undefined;
109
+ }
110
+ export interface PullResult {
111
+ recall: Recall;
112
+ reads: RecallRead[];
113
+ /** What `recall()` found, when the pull came through its `error`, `path`, or `cls` inputs. */
114
+ legacy?: RecallResult;
115
+ }
116
+ /** A pull always answers (spec 2.7): the holdback runs on pushes alone. */
117
+ export declare const pull: (store: Store, input: PullInput, context: PullContext) => PullResult;
@@ -0,0 +1,410 @@
1
+ // The reads a recall answers from, counted over the store: a signature is a read in family `recovery` (spec Section 7), a tool call's input shape a read in family `fit`, the
2
+ // pre-call half of recovery, and a public name a read in family `entity`. `recall-pool.ts` puts each under the ID of the stored read it rests on; a hash here is what stands in
3
+ // where the pool holds none, and the only ID an `entity` read has, because no family stores a read per public name.
4
+ import { createHash } from 'node:crypto';
5
+ import { ACTION_TABLES, actionOf, mcpOf } from './actions.js';
6
+ import { utcDay } from './frame.js';
7
+ import { LITERAL, PUBLIC_PACKAGES, publicModel, publicVersion } from './public-vocabulary.js';
8
+ import { recall, recallRows } from './query.js';
9
+ import { contentHash, FAMILIES } from './read.js';
10
+ import { namedByPrefix, readsOfFamily, resolveReads } from './recall-pool.js';
11
+ import { makeRecall } from './recall-object.js';
12
+ import { closedName, evidenceOf, renderAnswer } from './recall-render.js';
13
+ import { makeRedactor } from './redact.js';
14
+ import { harnessOf, PROMPT_CLASSES } from './schema.js';
15
+ const FOLLOWED = 3;
16
+ const DAY_MS = 86_400_000;
17
+ const digest = (parts) => createHash('sha256').update(JSON.stringify(parts)).digest('hex').slice(0, 16);
18
+ /** A signature's stand-in: the hash a stored recovery read holds as `slice.sig`. */
19
+ export const readIdOf = (sig) => contentHash(sig);
20
+ /** The ID of a call's shape: its tool and input key names, nothing the caller typed as a value. */
21
+ export const toolShapeId = (tool, keys) => digest(['fit', tool, [...keys].sort()]);
22
+ const top = (following) => [...following].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, FOLLOWED);
23
+ const modelsByPrompt = (store) => new Map(store.prompts.map((prompt) => [`${prompt.session}#${String(prompt.index)}`, prompt.model]));
24
+ const bump = (counts, key) => {
25
+ counts.set(key, (counts.get(key) ?? 0) + 1);
26
+ };
27
+ const dayRange = (days) => {
28
+ const known = [...days].filter((day) => day !== null).sort();
29
+ return { firstDay: known[0] ?? null, lastDay: known[known.length - 1] ?? null };
30
+ };
31
+ const harnessesOf = (store) => new Map([...store.sessions, ...store.subagents].map((session) => [session.session, harnessOf(session)]));
32
+ /** Which harnesses a set of sessions ran under; a session with no record reads as the default, the way `harnessOf` reads one with no field. */
33
+ const harnessNames = (sessions, harnesses) => [...new Set([...sessions].map((session) => harnesses.get(session) ?? harnessOf({})))].sort();
34
+ export const recoveryReads = (store, keep = () => true) => {
35
+ const models = modelsByPrompt(store);
36
+ const harnesses = harnessesOf(store);
37
+ const bySig = new Map();
38
+ for (const error of store.errors) {
39
+ if (keep(error))
40
+ bySig.set(error.sig, [...(bySig.get(error.sig) ?? []), error]);
41
+ }
42
+ const reads = [...bySig].map(([sig, list]) => {
43
+ const following = new Map();
44
+ for (const tool of list.flatMap((error) => error.next))
45
+ bump(following, tool);
46
+ const sessions = new Set(list.map((error) => error.session));
47
+ return {
48
+ readId: readIdOf(sig),
49
+ family: 'recovery',
50
+ tool: list[0]?.tool ?? '',
51
+ keys: [],
52
+ n: sessions.size,
53
+ count: list.length,
54
+ calls: null,
55
+ clusters: 1,
56
+ ...dayRange(list.map((error) => utcDay(error.ts))),
57
+ harnesses: harnessNames(sessions, harnesses),
58
+ harnessVersions: [...new Set(list.flatMap((error) => (error.harnessVersion === null ? [] : [error.harnessVersion])))].sort(),
59
+ models: [...new Set(list.flatMap((error) => models.get(`${error.session}#${String(error.prompt)}`) ?? []))].sort(),
60
+ followed: top(following),
61
+ };
62
+ });
63
+ return reads.sort((a, b) => b.n - a.n || b.count - a.count || a.readId.localeCompare(b.readId));
64
+ };
65
+ /**
66
+ * One row per call on the main line: a sidechain's repeats are not a new call, and a resumed transcript's replay is a second row for one tool-use id, so the
67
+ * row with a status wins. `loadStore` folds these already (`foldToolRows`), so over a loaded store only the sidechain filter does anything; the rest guards a store built by hand.
68
+ */
69
+ const oneRowPerCall = (tools) => {
70
+ const kept = new Map();
71
+ const anonymous = [];
72
+ for (const tool of tools) {
73
+ if (tool.side)
74
+ continue;
75
+ if (tool.toolUseId === null) {
76
+ anonymous.push(tool);
77
+ continue;
78
+ }
79
+ const key = `${tool.session} ${tool.toolUseId}`;
80
+ const held = kept.get(key);
81
+ if (held === undefined || (held.status === 'none' && tool.status !== 'none'))
82
+ kept.set(key, tool);
83
+ }
84
+ return [...kept.values(), ...anonymous];
85
+ };
86
+ /** The main-line calls of each session, one row per call, in call order. */
87
+ export const callsBySession = (store) => {
88
+ const bySession = new Map();
89
+ for (const tool of oneRowPerCall(store.tools)) {
90
+ const list = bySession.get(tool.session);
91
+ if (list === undefined)
92
+ bySession.set(tool.session, [tool]);
93
+ else
94
+ list.push(tool);
95
+ }
96
+ for (const list of bySession.values())
97
+ list.sort((a, b) => a.n - b.n);
98
+ return bySession;
99
+ };
100
+ /**
101
+ * The floors a call shape needs before it is pushed. On the operator's store (6,264 sessions, 361,914 calls) they keep 14 shapes, among them the wrong-input
102
+ * calls that always fail (`Monitor` with `query`, `Read` with `path`), and leave Bash, whose failures do not depend on its shape. Open parameters.
103
+ */
104
+ export const FIT_FLOORS = { minFailedSessions: 3, minFailedShare: 0.1, minCalls: 20 };
105
+ /** `keep` narrows which calls are tallied, not what can come next: the call after a kept one is what followed it, wherever the slice ends. */
106
+ export const fitReads = (store, floors = FIT_FLOORS, keep = () => true) => {
107
+ const models = modelsByPrompt(store);
108
+ const harnesses = harnessesOf(store);
109
+ const shapes = new Map();
110
+ for (const calls of callsBySession(store).values()) {
111
+ for (const [position, call] of calls.entries()) {
112
+ if (!keep(call))
113
+ continue;
114
+ const id = toolShapeId(call.tool, call.keys);
115
+ const shape = shapes.get(id) ?? { tool: call.tool, keys: [...call.keys].sort(), calls: 0, failed: 0, sessions: new Set(), following: new Map(), days: [], versions: new Set(), models: new Set() };
116
+ shapes.set(id, shape);
117
+ shape.calls += 1;
118
+ if (call.status !== 'error')
119
+ continue;
120
+ shape.failed += 1;
121
+ shape.sessions.add(call.session);
122
+ shape.days.push(utcDay(call.ts));
123
+ if (call.harnessVersion !== null)
124
+ shape.versions.add(call.harnessVersion);
125
+ const model = models.get(`${call.session}#${String(call.prompt)}`);
126
+ if (model !== undefined)
127
+ shape.models.add(model);
128
+ const next = calls[position + 1];
129
+ if (next !== undefined)
130
+ bump(shape.following, next.tool);
131
+ }
132
+ }
133
+ const reads = [];
134
+ for (const [readId, shape] of shapes) {
135
+ if (shape.sessions.size < floors.minFailedSessions || shape.calls < floors.minCalls || shape.failed / shape.calls < floors.minFailedShare)
136
+ continue;
137
+ reads.push({
138
+ readId,
139
+ family: 'fit',
140
+ tool: shape.tool,
141
+ keys: shape.keys,
142
+ n: shape.sessions.size,
143
+ count: shape.failed,
144
+ calls: shape.calls,
145
+ clusters: 1,
146
+ ...dayRange(shape.days),
147
+ harnesses: harnessNames(shape.sessions, harnesses),
148
+ harnessVersions: [...shape.versions].sort(),
149
+ models: [...shape.models].sort(),
150
+ followed: top(shape.following),
151
+ });
152
+ }
153
+ return reads.sort((a, b) => b.count - a.count || a.readId.localeCompare(b.readId));
154
+ };
155
+ /** The action of a tool under the first of its harnesses whose table lists it, else under the first harness (the default one when a read names none). */
156
+ export const actionOfShape = (tool, harnesses) => {
157
+ const tables = Object.entries(ACTION_TABLES).flatMap(([name, table]) => (harnesses.includes(name) ? [table] : []));
158
+ return actionOf(tables.find((table) => Object.hasOwn(table.rows, tool)) ?? tables[0] ?? ACTION_TABLES[harnessOf({})], tool).action;
159
+ };
160
+ /**
161
+ * What a push must beat: a shape is pushed only when its failure share is at least this many times the share of all calls of its action. The floors above keep
162
+ * a shape that fails often; this keeps one that fails more often than its action does, and guards a store whose base rate is high. On the replay of 2026-11-02 it kept
163
+ * 79 of 79 pushes, at 4.5x and 8.6x their action's share: a real signal, not a defect. Open parameter, like the floors.
164
+ */
165
+ export const PUSH_MIN_LIFT = 2;
166
+ /**
167
+ * The reads whose shape fails at `lift` times the share of its whole action or more, where the action's share counts every call of every shape of it, those under the floors too.
168
+ * A tool no table lists is the action `other` (or `mcp.call`), so it is held to that action's share like any other and can enter the index and fire. Compared in integers, so a tie is exact.
169
+ */
170
+ export const aboveBaseRate = (store, reads, lift = PUSH_MIN_LIFT) => {
171
+ const harnesses = harnessesOf(store);
172
+ const byAction = new Map();
173
+ for (const calls of callsBySession(store).values()) {
174
+ for (const call of calls) {
175
+ const action = actionOfShape(call.tool, [harnesses.get(call.session) ?? harnessOf({})]);
176
+ const tally = byAction.get(action) ?? { calls: 0, failed: 0 };
177
+ byAction.set(action, tally);
178
+ tally.calls += 1;
179
+ if (call.status === 'error')
180
+ tally.failed += 1;
181
+ }
182
+ }
183
+ return reads.filter((read) => {
184
+ const base = byAction.get(actionOfShape(read.tool, read.harnesses));
185
+ // A fit read always counts its calls (`calls` is null only for entity reads), and its own calls are in the tally, so `base` is there; the fallbacks keep the types honest.
186
+ return base !== undefined && read.calls !== null && read.count * base.calls >= lift * base.failed * read.calls;
187
+ });
188
+ };
189
+ /** The snapshot a store answers from: its latest deposit. A deposit time compares with a recall's `at`, which is what "older than the recall" needs. */
190
+ export const poolVersionOf = (store) => store.sessions.reduce((latest, session) => (session.at > latest ? session.at : latest), '');
191
+ const namedInTable = (name, tool) => {
192
+ if (tool === name)
193
+ return true;
194
+ for (const table of Object.values(ACTION_TABLES)) {
195
+ if (actionOf(table, tool).action === name || mcpOf(table, tool)?.server === name)
196
+ return true;
197
+ }
198
+ return false;
199
+ };
200
+ const entityReads = (store, name) => {
201
+ const models = modelsByPrompt(store);
202
+ const harnesses = harnessesOf(store);
203
+ // Calls and a model's requests are two counts, so each is its own read: their sum is neither. Only the model's read lists a model, which is how the answer names its noun.
204
+ const usage = (idParts, listed, countOf) => {
205
+ const counted = store.sessions.filter((session) => countOf(session) > 0);
206
+ if (counted.length === 0)
207
+ return [];
208
+ const sessions = new Set(counted.map((session) => session.session));
209
+ const count = counted.reduce((sum, session) => sum + countOf(session), 0);
210
+ return [
211
+ {
212
+ readId: digest([...idParts, sessions.size, count]),
213
+ family: 'entity',
214
+ tool: name,
215
+ keys: [],
216
+ n: sessions.size,
217
+ count,
218
+ calls: null,
219
+ clusters: 1,
220
+ ...dayRange(counted.flatMap((session) => [utcDay(session.tsFirst), utcDay(session.tsLast)])),
221
+ harnesses: harnessNames(sessions, harnesses),
222
+ harnessVersions: [],
223
+ models: listed,
224
+ followed: [],
225
+ },
226
+ ];
227
+ };
228
+ const calls = usage(['entity', name], [], (session) => Object.entries(session.tools).reduce((sum, [tool, times]) => (namedInTable(name, tool) ? sum + times : sum), 0));
229
+ const requests = usage(['entity', 'model', name], [name], (session) => session.models[name] ?? 0);
230
+ // A pull's name is class A, so only a name the public vocabulary lists is looked for: a private package gets no answer that its asker could read as a count of it.
231
+ const pulling = new Map();
232
+ if (PUBLIC_PACKAGES.has(name)) {
233
+ for (const [session, list] of callsBySession(store))
234
+ pulling.set(session, list.filter((call) => call.pulls?.some((one) => one.name === name)).length);
235
+ }
236
+ const pulled = usage(['entity', 'package', name], [], (session) => pulling.get(session.session) ?? 0);
237
+ const errors = recoveryReads(store, (error) => namedInTable(name, error.tool) || models.get(`${error.session}#${String(error.prompt)}`) === name);
238
+ return [...calls, ...requests, ...pulled, ...errors];
239
+ };
240
+ const promptKey = (row) => `${row.session}#${String(row.prompt)}`;
241
+ /** The calls and errors a slice holds. A class comes through `clsOf`, because an error row states its own and a call only through its prompt. */
242
+ const sliceKeeps = (store, slice, now, clsOf) => {
243
+ const models = modelsByPrompt(store);
244
+ const cutoff = slice.sinceDays === undefined ? undefined : new Date(now - slice.sinceDays * DAY_MS).toISOString();
245
+ return (row) => (slice.harnessVersion === undefined || row.harnessVersion === slice.harnessVersion) &&
246
+ (slice.model === undefined || models.get(promptKey(row)) === slice.model) &&
247
+ (slice.tool === undefined || row.tool === slice.tool) &&
248
+ (slice.cls === undefined || clsOf(row) === slice.cls) &&
249
+ (cutoff === undefined || (row.ts !== null && row.ts >= cutoff));
250
+ };
251
+ /** `current` answers every family the store's own rows do not: its reads are already cut, so `pullRefusal` turns away a slice they cannot apply. With no pool, there is nothing to answer from. */
252
+ const questionReads = (store, family, slice, now, current) => {
253
+ if (family === undefined)
254
+ return [];
255
+ if (family === 'recovery')
256
+ return recoveryReads(store, sliceKeeps(store, slice, now, (error) => error.cls));
257
+ if (family !== 'fit')
258
+ return current === undefined ? [] : readsOfFamily(family, current);
259
+ const classes = new Map(store.prompts.map((prompt) => [`${prompt.session}#${String(prompt.index)}`, prompt.cls]));
260
+ return fitReads(store, FIT_FLOORS, sliceKeeps(store, slice, now, (call) => classes.get(promptKey(call))));
261
+ };
262
+ const patternReads = (store, prefix, current) => {
263
+ const named = current === undefined ? { standIns: [], reads: [] } : namedByPrefix(prefix, current);
264
+ const ids = new Set([...prefix, ...named.standIns]);
265
+ return [...[...recoveryReads(store), ...fitReads(store)].filter((read) => ids.has(read.readId)), ...named.reads];
266
+ };
267
+ const textReads = (store, text) => {
268
+ const needle = text.trim().toLowerCase();
269
+ if (needle === '')
270
+ return [];
271
+ return recoveryReads(store, (error) => error.sig.toLowerCase().includes(needle) || error.example.toLowerCase().includes(needle));
272
+ };
273
+ const isClosed = (input) => 'form' in input;
274
+ /** The closed form a command line asks for; undefined when it names none, and only `recall()`'s inputs are left. */
275
+ export const closedQueryOf = (options) => {
276
+ const slice = { ...(options.cls === undefined ? {} : { cls: options.cls }), ...(options.sinceDays === undefined ? {} : { sinceDays: options.sinceDays }) };
277
+ if (options.pattern.length > 0)
278
+ return { form: 'pattern', prefix: options.pattern, slice };
279
+ if (options.entity !== undefined)
280
+ return { form: 'entity', entity: options.entity, slice };
281
+ if (options.text !== undefined)
282
+ return { form: 'text', text: options.text, slice };
283
+ if (options.family === undefined)
284
+ return undefined;
285
+ return { form: 'question', family: options.family, slice };
286
+ };
287
+ const ANSWER_LIMIT = 5;
288
+ /** The reads a query's form asks for, best first, over the local store: an empty list is a miss, which a recall keeps. */
289
+ export const answerQuery = (store, input, now, current) => {
290
+ const reads = () => {
291
+ switch (input.form) {
292
+ case 'pattern':
293
+ return patternReads(store, input.prefix ?? [], current);
294
+ case 'entity':
295
+ return input.entity === undefined ? [] : entityReads(store, input.entity);
296
+ case 'question':
297
+ return questionReads(store, input.family, input.slice ?? {}, now, current);
298
+ case 'text':
299
+ return input.text === undefined ? [] : textReads(store, input.text);
300
+ }
301
+ };
302
+ const answers = reads().slice(0, ANSWER_LIMIT);
303
+ return current === undefined ? answers : resolveReads(answers, current.reads);
304
+ };
305
+ const ID_SHAPE = /^(read:)?[0-9a-f]{16}$/;
306
+ const isIdShape = (value) => ID_SHAPE.test(value);
307
+ const isPublicName = (value) => closedName(value) === value;
308
+ const isFamily = (value) => FAMILIES.some((family) => family === value);
309
+ /** The families a question can answer. `lead-lag` is experimental, so it stays with `oars sessions read`. */
310
+ export const RECALL_FAMILIES = FAMILIES.filter((family) => family !== 'lead-lag');
311
+ /**
312
+ * Why this pull cannot be asked; undefined when it can. Only `recovery` and `fit` count their answers from the store's own rows, so any other closed form or family cannot apply a
313
+ * slice, and answering them anyway would state counts the slice did not narrow. The text names a form or a family that has passed `isFamily`, so it holds nothing an asker typed.
314
+ */
315
+ export const pullRefusal = (input) => {
316
+ if (!isClosed(input) || Object.keys(input.slice ?? {}).length === 0)
317
+ return undefined;
318
+ if (input.form !== 'question')
319
+ return `The ${input.form} form counts all time, which a class or a number of days cannot narrow. Ask without them.`;
320
+ const { family } = input;
321
+ if (family === undefined || family === 'recovery' || family === 'fit' || !isFamily(family))
322
+ return undefined;
323
+ return `Family ${family} answers from stored reads, which a class or a number of days cannot narrow. Ask without them, or slice its rows with oars sessions read --family ${family} --slice <axis>=<value>.`;
324
+ };
325
+ /** The forms a pull can name, in the order a refusal lists them. `error` with `path` is one form: the signature lookup. */
326
+ export const FORM_NAMES = ['prefix', 'entity', 'family', 'text', 'error or path'];
327
+ /**
328
+ * Why a pull naming several forms cannot be asked; undefined when it names at most one. Answering the first form and dropping the rest would state counts for a question the asker did
329
+ * not put, and the rendered answer names no signature to show it. The text lists fixed form names, so it holds nothing an asker typed.
330
+ */
331
+ export const formsRefusal = (named) => named.length > 1 ? `Name one form per pull: ${FORM_NAMES.join(', ')}. This one named ${named.join(' and ')}.` : undefined;
332
+ const isPromptClass = (value) => PROMPT_CLASSES.some((cls) => cls === value);
333
+ const isPublicModel = (value) => publicModel(value) === value;
334
+ const isPublicVersion = (value) => publicVersion(value) === value;
335
+ /**
336
+ * What a recall stores of its query. Every field but `text` is class P, so each holds a value that passes its closed test or `<literal>`, and what it refused joins `text`,
337
+ * class A, where what an asker typed belongs. The answer has already been read from the query as asked.
338
+ */
339
+ const recordedQuery = ({ form, prefix, entity, family, slice, text }) => {
340
+ const typed = text === undefined ? [] : [text];
341
+ const close = (value, passes) => {
342
+ if (passes(value))
343
+ return value;
344
+ typed.push(value);
345
+ return LITERAL;
346
+ };
347
+ const closed = {
348
+ form,
349
+ ...(prefix === undefined ? {} : { prefix: prefix.map((id) => close(id, isIdShape)) }),
350
+ ...(entity === undefined ? {} : { entity: close(entity, isPublicName) }),
351
+ ...(family === undefined ? {} : { family: close(family, isFamily) }),
352
+ ...(slice === undefined
353
+ ? {}
354
+ : {
355
+ slice: {
356
+ ...(slice.harnessVersion === undefined ? {} : { harnessVersion: close(slice.harnessVersion, isPublicVersion) }),
357
+ ...(slice.model === undefined ? {} : { model: close(slice.model, isPublicModel) }),
358
+ ...(slice.tool === undefined ? {} : { tool: close(slice.tool, isPublicName) }),
359
+ ...(slice.cls === undefined ? {} : { cls: close(slice.cls, isPromptClass) }),
360
+ ...(slice.sinceDays === undefined ? {} : { sinceDays: slice.sinceDays }),
361
+ },
362
+ }),
363
+ };
364
+ return typed.length === 0 ? closed : { ...closed, text: typed.join(' ') };
365
+ };
366
+ /** The closed form a `recall()` input is recorded under. An error and a path are text the asker typed, so both stay in class A, and the narrowing that applied is kept as the slice. */
367
+ const closedOf = (query) => {
368
+ const slice = { ...(query.cls === undefined ? {} : { cls: query.cls }), ...(query.sinceDays === undefined ? {} : { sinceDays: query.sinceDays }) };
369
+ const text = [query.error, query.path].filter((typed) => typed !== undefined).join(' ');
370
+ if (text === '')
371
+ return recordedQuery({ form: 'question', family: 'recovery', slice });
372
+ return recordedQuery({ form: 'text', text, ...(Object.keys(slice).length === 0 ? {} : { slice }) });
373
+ };
374
+ /** The stored frame of the session asking, as of today: its harness and model are what the session ran under, and its day is when it started. */
375
+ const frameOfSession = (store, sid, day) => {
376
+ const frame = sid === null ? undefined : store.sessions.find((session) => session.session === sid)?.frame;
377
+ return frame === undefined ? null : { ...frame, day };
378
+ };
379
+ /** A pull always answers (spec 2.7): the holdback runs on pushes alone. */
380
+ export const pull = (store, input, context) => {
381
+ const at = new Date(context.now).toISOString();
382
+ const legacy = isClosed(input) ? undefined : recall(store, input, context.now, undefined, context.redactDomains);
383
+ const { current } = context;
384
+ const day = utcDay(at);
385
+ const frame = context.frame ?? frameOfSession(store, context.sid, day);
386
+ const asker = frame ?? { harness: null, harnessVersion: null, model: null, day };
387
+ const legacyReads = (query) => {
388
+ const found = recoveryReads(store, recallRows(store, query, context.now, context.redactDomains).keep).slice(0, ANSWER_LIMIT);
389
+ return current === undefined ? found : resolveReads(found, current.reads);
390
+ };
391
+ const reads = isClosed(input) ? answerQuery(store, input, context.now, current) : legacyReads(input);
392
+ const { rendered, names } = renderAnswer(reads, asker);
393
+ const recorded = isClosed(input) ? recordedQuery(input) : closedOf(input);
394
+ // What an asker typed is class A: redacted here, as a deposit redacts what it reads.
395
+ const redact = makeRedactor(context.redactDomains);
396
+ const made = makeRecall({
397
+ sid: context.sid,
398
+ at,
399
+ frame,
400
+ mode: 'pull',
401
+ query: recorded.text === undefined ? recorded : { ...recorded, text: redact(recorded.text) },
402
+ poolVersion: poolVersionOf(store),
403
+ arm: 'shown',
404
+ reads: reads.map((read) => read.readId),
405
+ rendered,
406
+ names,
407
+ evidence: reads.map((read) => evidenceOf(read, asker)),
408
+ });
409
+ return { recall: made, reads, ...(legacy === undefined ? {} : { legacy }) };
410
+ };
@@ -0,0 +1,19 @@
1
+ import type { RecallDelta, RecallEvidence } from './recall-object.js';
2
+ import type { RecallRead } from './recall-reads.js';
3
+ import { type Frame, type Harness } from './schema.js';
4
+ /** A public name as it stands: a package, a model, a canonical action, a tool a harness's table lists, or a published MCP server. Anything else is what the asker or a record wrote. */
5
+ export declare const closedName: (name: string) => string;
6
+ /** What a delta reads of the asker. A pull from outside any known session knows only the day: naming a harness for it would read as `same`, a guess. */
7
+ export type RecallAsker = Pick<Frame, 'harnessVersion' | 'model' | 'day'> & {
8
+ harness: Harness | null;
9
+ };
10
+ export declare const deltaOf: (asker: RecallAsker | null, read: RecallRead) => RecallDelta;
11
+ /** A class P row an agent also reads over MCP, so its names pass the same closed tests a rendered answer does, and a value they refuse is dropped. */
12
+ export declare const evidenceOf: (read: RecallRead, asker: RecallAsker | null) => RecallEvidence;
13
+ export declare const count: (value: number, noun: string) => string;
14
+ export declare const renderAnswer: (reads: readonly RecallRead[], asker: RecallAsker | null) => {
15
+ rendered: string;
16
+ names: string[];
17
+ };
18
+ /** The text for a miss: what was asked for is left out, because the asker wrote it. */
19
+ export declare const MISS_TEXT = "oars recall: no answer in the local store.";