@manyoars/oars 0.4.1 → 0.5.1

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 (183) hide show
  1. package/README.md +79 -29
  2. package/dist/bin.js +3 -1
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +206 -20
  5. package/dist/club.d.ts +19 -0
  6. package/dist/club.js +132 -0
  7. package/dist/gate/hook-fill-main.js +1 -1
  8. package/dist/gate/hook-fill.d.ts +1 -1
  9. package/dist/gate/hook-fill.js +1 -1
  10. package/dist/gate/hook-main.js +7 -5
  11. package/dist/gate/hook.d.ts +12 -1
  12. package/dist/gate/hook.js +38 -7
  13. package/dist/hook-bin.js +23 -4
  14. package/dist/main.js +5 -0
  15. package/dist/node-floor.d.ts +10 -0
  16. package/dist/node-floor.js +23 -0
  17. package/dist/oars-hook.d.ts +2 -0
  18. package/dist/oars-hook.js +7 -0
  19. package/dist/sessions/allow.d.ts +1 -0
  20. package/dist/sessions/allow.js +1 -1
  21. package/dist/sessions/call-shape.d.ts +111 -0
  22. package/dist/sessions/call-shape.js +464 -0
  23. package/dist/sessions/candidates.d.ts +65 -0
  24. package/dist/sessions/candidates.js +236 -0
  25. package/dist/sessions/card-command.d.ts +24 -0
  26. package/dist/sessions/card-command.js +71 -0
  27. package/dist/sessions/card-font.d.ts +6 -0
  28. package/dist/sessions/card-font.js +36 -0
  29. package/dist/sessions/card-line.d.ts +30 -0
  30. package/dist/sessions/card-line.js +105 -0
  31. package/dist/sessions/card-render.d.ts +15 -0
  32. package/dist/sessions/card-render.js +146 -0
  33. package/dist/sessions/card.d.ts +59 -0
  34. package/dist/sessions/card.js +107 -0
  35. package/dist/sessions/cell-increments.d.ts +14 -3
  36. package/dist/sessions/cell-increments.js +79 -12
  37. package/dist/sessions/cells.d.ts +42 -4
  38. package/dist/sessions/cells.js +72 -18
  39. package/dist/sessions/codex-hook-bin.d.ts +1 -0
  40. package/dist/sessions/codex-hook-bin.js +24 -0
  41. package/dist/sessions/codex-hook-install.d.ts +21 -0
  42. package/dist/sessions/codex-hook-install.js +47 -0
  43. package/dist/sessions/codex-hook-view.d.ts +20 -0
  44. package/dist/sessions/codex-hook-view.js +20 -0
  45. package/dist/sessions/codex-hook.d.ts +77 -0
  46. package/dist/sessions/codex-hook.js +255 -0
  47. package/dist/sessions/codex.d.ts +9 -0
  48. package/dist/sessions/codex.js +104 -18
  49. package/dist/sessions/command.d.ts +17 -2
  50. package/dist/sessions/command.js +145 -33
  51. package/dist/sessions/copilot.d.ts +3 -0
  52. package/dist/sessions/copilot.js +12 -4
  53. package/dist/sessions/derive.js +2 -1
  54. package/dist/sessions/empty-state.d.ts +17 -0
  55. package/dist/sessions/empty-state.js +38 -0
  56. package/dist/sessions/exclusion.js +48 -3
  57. package/dist/sessions/extract.d.ts +16 -3
  58. package/dist/sessions/extract.js +281 -24
  59. package/dist/sessions/gemini.d.ts +2 -0
  60. package/dist/sessions/gemini.js +29 -2
  61. package/dist/sessions/harness-files.d.ts +27 -0
  62. package/dist/sessions/harness-files.js +109 -0
  63. package/dist/sessions/head-only.d.ts +7 -0
  64. package/dist/sessions/head-only.js +21 -0
  65. package/dist/sessions/hook-calls.js +4 -3
  66. package/dist/sessions/hooks.d.ts +1 -1
  67. package/dist/sessions/hooks.js +6 -2
  68. package/dist/sessions/identity.d.ts +6 -0
  69. package/dist/sessions/injected.d.ts +13 -0
  70. package/dist/sessions/injected.js +212 -0
  71. package/dist/sessions/install.d.ts +31 -20
  72. package/dist/sessions/install.js +80 -82
  73. package/dist/sessions/load-status.d.ts +41 -0
  74. package/dist/sessions/load-status.js +156 -0
  75. package/dist/sessions/machine-child-deps.d.ts +5 -0
  76. package/dist/sessions/machine-child-deps.js +83 -0
  77. package/dist/sessions/machine-child.d.ts +43 -0
  78. package/dist/sessions/machine-child.js +120 -0
  79. package/dist/sessions/machine-credential.d.ts +10 -0
  80. package/dist/sessions/machine-credential.js +20 -0
  81. package/dist/sessions/machine-enroll.d.ts +47 -0
  82. package/dist/sessions/machine-enroll.js +230 -0
  83. package/dist/sessions/machine-hold.d.ts +49 -0
  84. package/dist/sessions/machine-hold.js +88 -0
  85. package/dist/sessions/machine-ids.d.ts +6 -0
  86. package/dist/sessions/machine-ids.js +9 -0
  87. package/dist/sessions/machine-notice.d.ts +21 -0
  88. package/dist/sessions/machine-notice.js +89 -0
  89. package/dist/sessions/machine-pull.d.ts +40 -0
  90. package/dist/sessions/machine-pull.js +220 -0
  91. package/dist/sessions/machine-repo.d.ts +7 -0
  92. package/dist/sessions/machine-repo.js +30 -0
  93. package/dist/sessions/machine-scan-cache.d.ts +12 -0
  94. package/dist/sessions/machine-scan-cache.js +47 -0
  95. package/dist/sessions/machine-send.d.ts +12 -0
  96. package/dist/sessions/machine-send.js +156 -0
  97. package/dist/sessions/machine-stop.d.ts +22 -0
  98. package/dist/sessions/machine-stop.js +53 -0
  99. package/dist/sessions/machine.d.ts +41 -0
  100. package/dist/sessions/machine.js +89 -0
  101. package/dist/sessions/opencode.d.ts +2 -0
  102. package/dist/sessions/opencode.js +6 -0
  103. package/dist/sessions/otel.d.ts +21 -0
  104. package/dist/sessions/otel.js +109 -38
  105. package/dist/sessions/owner-only.d.ts +7 -0
  106. package/dist/sessions/owner-only.js +34 -0
  107. package/dist/sessions/pi.d.ts +13 -1
  108. package/dist/sessions/pi.js +27 -13
  109. package/dist/sessions/pool-books.d.ts +1 -1
  110. package/dist/sessions/pool-books.js +1 -1
  111. package/dist/sessions/pool-choices.d.ts +4 -2
  112. package/dist/sessions/pool-choices.js +4 -2
  113. package/dist/sessions/pool-command.d.ts +46 -6
  114. package/dist/sessions/pool-command.js +577 -133
  115. package/dist/sessions/pool-delta.d.ts +7 -0
  116. package/dist/sessions/pool-delta.js +28 -5
  117. package/dist/sessions/pool-due.d.ts +43 -0
  118. package/dist/sessions/pool-due.js +113 -0
  119. package/dist/sessions/pool-nightly.d.ts +9 -1
  120. package/dist/sessions/pool-nightly.js +65 -21
  121. package/dist/sessions/pool-outbox.d.ts +51 -5
  122. package/dist/sessions/pool-outbox.js +217 -24
  123. package/dist/sessions/pool-print.d.ts +1 -1
  124. package/dist/sessions/pool-raw.js +4 -3
  125. package/dist/sessions/pool-replica.d.ts +4 -4
  126. package/dist/sessions/pool-replica.js +26 -7
  127. package/dist/sessions/pool-send-lock.js +1 -1
  128. package/dist/sessions/pool-sent.d.ts +2 -0
  129. package/dist/sessions/pool-sent.js +21 -4
  130. package/dist/sessions/pool-wire.d.ts +172 -12
  131. package/dist/sessions/pool-wire.js +284 -30
  132. package/dist/sessions/pool.js +4 -2
  133. package/dist/sessions/public-vocabulary.d.ts +5 -0
  134. package/dist/sessions/public-vocabulary.js +16 -0
  135. package/dist/sessions/read-call-shape.js +12 -6
  136. package/dist/sessions/read-fit.d.ts +5 -0
  137. package/dist/sessions/read-fit.js +23 -2
  138. package/dist/sessions/read.d.ts +1 -1
  139. package/dist/sessions/recall-consequence.d.ts +2 -2
  140. package/dist/sessions/recall-consequence.js +14 -9
  141. package/dist/sessions/recall-object.d.ts +9 -2
  142. package/dist/sessions/receipt.d.ts +8 -0
  143. package/dist/sessions/receipt.js +78 -0
  144. package/dist/sessions/recovery-calls.d.ts +3 -2
  145. package/dist/sessions/recovery-calls.js +9 -3
  146. package/dist/sessions/redact.d.ts +2 -2
  147. package/dist/sessions/redact.js +116 -14
  148. package/dist/sessions/reversion.d.ts +8 -0
  149. package/dist/sessions/reversion.js +114 -0
  150. package/dist/sessions/schema.d.ts +78 -4
  151. package/dist/sessions/schema.js +31 -1
  152. package/dist/sessions/session-shape.d.ts +2 -1
  153. package/dist/sessions/session-shape.js +15 -0
  154. package/dist/sessions/sources.d.ts +9 -0
  155. package/dist/sessions/sources.js +16 -5
  156. package/dist/sessions/steps.js +3 -2
  157. package/dist/sessions/store.d.ts +41 -2
  158. package/dist/sessions/store.js +68 -16
  159. package/dist/sessions/try-demo.d.ts +8 -0
  160. package/dist/sessions/try-demo.js +90 -0
  161. package/dist/sessions/try.d.ts +13 -0
  162. package/dist/sessions/try.js +50 -0
  163. package/dist/sessions/vscode-copilot.d.ts +4 -0
  164. package/dist/sessions/vscode-copilot.js +25 -18
  165. package/dist/telemetry/command.js +12 -6
  166. package/dist/telemetry/consent.d.ts +9 -3
  167. package/dist/telemetry/consent.js +25 -9
  168. package/dist/telemetry/fields.d.ts +11 -0
  169. package/dist/telemetry/fields.js +56 -0
  170. package/dist/telemetry/measures.d.ts +13 -0
  171. package/dist/telemetry/measures.js +30 -0
  172. package/dist/telemetry/observe.d.ts +15 -2
  173. package/dist/telemetry/observe.js +34 -7
  174. package/dist/telemetry/region.d.ts +6 -0
  175. package/dist/telemetry/region.js +28 -0
  176. package/dist/telemetry/report.js +1 -1
  177. package/dist/telemetry/schema.d.ts +3 -0
  178. package/dist/telemetry/schema.js +11 -1
  179. package/dist/telemetry/send.d.ts +5 -2
  180. package/dist/telemetry/send.js +15 -7
  181. package/dist/telemetry/words.d.ts +6 -0
  182. package/dist/telemetry/words.js +6 -0
  183. package/package.json +23 -4
@@ -0,0 +1,114 @@
1
+ // A later reversion of the work a prompt did, read from the calls alone. Three ties: a clean `git revert <word>` whose word is a prefix of a commit sha the capture kept (or the other way round); an edit whose old and new text
2
+ // are the swap of an earlier edit's on the same path, or a Write that sets the text an earlier Write of that path set after a different Write changed it (both from the digests of the exact text); and a clean `git restore` or
3
+ // `git checkout --` that names a path an earlier edit changed. The result is descriptive: it says the work was undone later, never that it was wrong or that undoing it was the reason. The original outcome stays as it was and this sits beside it.
4
+ import { EDITS, SHELL } from './extract.js';
5
+ const NEAREST_FIRST = ['same-prompt', 'later-prompt', 'later-session'];
6
+ /** The shortest abbreviation git prints, and a full sha. Anything else a revert names (`HEAD`, a branch, a range, a short guess) is not a sha, so it ties to nothing. */
7
+ const SHA = /^[0-9a-f]{7,40}$/;
8
+ /**
9
+ * The sha-shaped words of a segment that is `git revert <words>`. A bare `git revert` is `--abort`, `--continue`, or `--quit` with the flag dropped, and names nothing. A `--no-commit` revert reads the same as a committed one,
10
+ * because the flag is not kept.
11
+ */
12
+ const revertWords = (run) => (run.startsWith('git revert ') ? run.slice('git revert '.length).split(' ').map((word) => word.toLowerCase()).filter((word) => SHA.test(word)) : []);
13
+ const tied = (commit, word) => commit.startsWith(word) || word.startsWith(commit);
14
+ /** Adds to the list a key holds, making it once, so a hot path costs one push per call and not a copy of the list. */
15
+ const append = (lists, key, items) => {
16
+ const held = lists.get(key);
17
+ if (held === undefined)
18
+ lists.set(key, [...items]);
19
+ else
20
+ held.push(...items);
21
+ };
22
+ /** Where `later` ran against `earlier`, or null when it is not later. Across sessions the calls are ordered by their time, so a call with none cannot be placed, and both sessions must sit in one known repository. */
23
+ const whereOf = (earlier, later, repos) => {
24
+ if (earlier.session === later.session) {
25
+ if (later.n < earlier.n || (later.n === earlier.n && later.k <= earlier.k))
26
+ return null;
27
+ return later.prompt === earlier.prompt ? 'same-prompt' : 'later-prompt';
28
+ }
29
+ const repo = repos.get(earlier.session) ?? null;
30
+ const sameRepo = repo !== null && repo === (repos.get(later.session) ?? null);
31
+ return sameRepo && earlier.ts !== null && later.ts !== null && later.ts > earlier.ts ? 'later-session' : null;
32
+ };
33
+ /**
34
+ * Whether a commit kept the edit before the restore ran: a `git restore` returns the file to the last commit, so an edit committed since is what it returns to, not what it undoes. A commit in the restore's own call counts, since
35
+ * the order inside a call is not kept. Across sessions a commit of the edit's session after it counts, and so does a commit of the restore's session before the restore unless both stamps are known and the commit is stamped before the
36
+ * edit; with a stamp missing the position alone decides.
37
+ */
38
+ const keptBy = (committed, edit, restore) => {
39
+ const before = committed.get(edit.session) ?? [];
40
+ if (edit.session === restore.session)
41
+ return before.some((commit) => commit.n > edit.n && commit.n <= restore.n);
42
+ return before.some((commit) => commit.n > edit.n) || (committed.get(restore.session) ?? []).some((commit) => commit.n <= restore.n && !(commit.ts !== null && edit.ts !== null && commit.ts < edit.ts));
43
+ };
44
+ /**
45
+ * Reads every main-line call once: the clean shell calls' commits, revert words (read segments only, since a segment whose result the call's status does not report (`read` false) could have failed behind another command), and
46
+ * restored paths; and every edit that did not fail, with its digests. A subagent's call is not on the main line, so it is not read. Returns the nearest reversion of a prompt's work, or null for none found.
47
+ */
48
+ export const reversionsOf = (store) => {
49
+ const repos = new Map(store.sessions.map((session) => [session.session, session.repo]));
50
+ const commits = [];
51
+ const reverts = [];
52
+ const committed = new Map();
53
+ const edited = new Map();
54
+ const written = new Map();
55
+ const restored = [];
56
+ for (const tool of store.tools) {
57
+ if (tool.side || tool.prompt === null)
58
+ continue;
59
+ const at = { session: tool.session, n: tool.n, k: 0, ts: tool.ts, prompt: tool.prompt };
60
+ if (EDITS.has(tool.tool) && tool.status !== 'error' && tool.target !== null) {
61
+ append(edited, tool.target, [at]);
62
+ append(written, tool.target, (tool.digests ?? []).map((held, k) => ({ ...at, k, old: held.old ?? null, now: held.new })));
63
+ }
64
+ if (tool.status !== 'ok' || !SHELL.has(tool.tool))
65
+ continue;
66
+ for (const sha of tool.commits ?? [])
67
+ if (sha !== null && SHA.test(sha))
68
+ commits.push({ ...at, sha });
69
+ if ((tool.commits ?? []).length > 0)
70
+ append(committed, tool.session, [{ n: tool.n, ts: tool.ts }]);
71
+ for (const target of tool.restores ?? [])
72
+ restored.push({ ...at, target });
73
+ for (const segment of tool.segments ?? [])
74
+ if (segment.read)
75
+ for (const word of revertWords(segment.run))
76
+ reverts.push({ ...at, sha: word });
77
+ }
78
+ const found = new Map();
79
+ const note = (earlier, later) => {
80
+ const where = whereOf(earlier, later, repos);
81
+ if (where === null)
82
+ return;
83
+ const key = `${earlier.session}#${String(earlier.prompt)}`;
84
+ const held = found.get(key);
85
+ if (held === undefined || NEAREST_FIRST.indexOf(where) < NEAREST_FIRST.indexOf(held))
86
+ found.set(key, where);
87
+ };
88
+ for (const commit of commits)
89
+ for (const revert of reverts)
90
+ if (tied(commit.sha, revert.sha))
91
+ note(commit, revert);
92
+ for (const pairs of written.values()) {
93
+ for (const later of pairs) {
94
+ for (const earlier of pairs) {
95
+ // An edit that changed nothing (old equals new) is its own swap, so it undoes nothing.
96
+ if (earlier.old !== null && earlier.old !== earlier.now && later.old === earlier.now && later.now === earlier.old)
97
+ note(earlier, later);
98
+ if (earlier.old !== null || later.old !== null || earlier.now !== later.now)
99
+ continue;
100
+ // A Write that sets the text an earlier Write set undoes the different Writes between them, not the earlier one.
101
+ for (const between of pairs) {
102
+ if (between.old === null && between.now !== later.now && whereOf(earlier, between, repos) !== null && whereOf(between, later, repos) !== null)
103
+ note(between, later);
104
+ }
105
+ }
106
+ }
107
+ }
108
+ for (const restore of restored) {
109
+ for (const edit of edited.get(restore.target) ?? [])
110
+ if (!keptBy(committed, edit, restore))
111
+ note(edit, restore);
112
+ }
113
+ return (session, index) => found.get(`${session}#${String(index)}`) ?? null;
114
+ };
@@ -1,9 +1,9 @@
1
1
  import type { CanonicalAction } from './actions.js';
2
- import type { ArgClass, TargetClass } from './call-shape.js';
2
+ import type { ArgClass, ArgFeature, ArgName, ArgValue, ChangeKind, DiffShape, TargetClass } from './call-shape.js';
3
3
  import type { RepoSize } from './public-vocabulary.js';
4
4
  import type { SessionFunnel, SessionSetup, SessionTrust } from './session-shape.js';
5
5
  /** Stamped `cv` on every captured record. Bump it when the captured field list changes or a stored value of an existing captured record or skeleton changes: a bump re-reads every transcript. */
6
- export declare const CAPTURE_VERSION = "25";
6
+ export declare const CAPTURE_VERSION = "35";
7
7
  /** Capture versions the reader derives from. A version-3 record is upgraded on read; a newer one is read for the fields this code knows. */
8
8
  export declare const OLDEST_READABLE_CAPTURE = 3;
9
9
  /** Stamped `v` on every derived record. A bump re-derives from the log; no transcript is read. */
@@ -39,6 +39,36 @@ export interface Usage {
39
39
  output: number;
40
40
  thinking: number;
41
41
  }
42
+ /**
43
+ * One model message's token counts (class L: numbers and ids only, never content). Claude Code writes a message over several content-block lines that each repeat the same `usage`, so
44
+ * a message is counted once, keyed by its `message.id` (else the line's `requestId`); a Codex response is keyed by the response id the reader gives it. The counts belong to the
45
+ * message and are never split across its tool calls: `calls` lists the tool-use ids the message made, and a later read can allocate. A category the source did not report is absent,
46
+ * never a zero. Summing these over a session record gives the deduplicated total for the file, `side` entries included.
47
+ */
48
+ export interface MessageUsage {
49
+ /** The message id, else the request id; null when the line holds neither (each such line is its own message). */
50
+ id: string | null;
51
+ /** The tool-use ids of the calls this message made, in order; empty when it made none. */
52
+ calls: string[];
53
+ /** True for a message on a sidechain line of a main transcript, which the session's `usage` total leaves out. */
54
+ side?: true;
55
+ input?: number;
56
+ cacheWrite?: number;
57
+ cacheRead?: number;
58
+ output?: number;
59
+ /** From `output_tokens_details.thinking_tokens` (Codex: the reasoning output tokens); absent when the source did not report it. */
60
+ thinking?: number;
61
+ /** Codex only: the plan share used as the response's event reported it. Absent when the event held no rate-limit block, never a zero. */
62
+ plan?: {
63
+ primary?: PlanWindow;
64
+ secondary?: PlanWindow;
65
+ };
66
+ }
67
+ /** One plan window a Codex event reports (`rate_limits.primary` or `.secondary`): the percent used and the window's length in minutes when given. */
68
+ export interface PlanWindow {
69
+ usedPercent: number;
70
+ windowMinutes?: number;
71
+ }
42
72
  /** The five usage categories, in the order a record lists the ones it observed. */
43
73
  export declare const USAGE_FIELDS: readonly ["input", "cacheWrite", "cacheRead", "output", "thinking"];
44
74
  export type UsageField = (typeof USAGE_FIELDS)[number];
@@ -115,7 +145,7 @@ export interface CaptureEnv {
115
145
  /**
116
146
  * One per transcript, captured: the `parse_sessions.py` record, minus anything that is raw content. Classes (spec Section 1.2):
117
147
  * `L` local-only: `project`, `files`. `A` account store: `session`, `parent`, `spawnedBy`, `spawnedIn`, `branches`, `urls`, `searches`, `tldrs`,
118
- * `confidences`, `promptFingerprint`, `repo`, `fileHashes`, `tools`, `setup`, `funnel`, `trust`, `at`, `tsFirst`, `tsLast`. `P` pooled: `kind`, `cv`, `rev`, `sid`,
148
+ * `confidences`, `promptFingerprint`, `repo`, `fileHashes`, `tools`, `setup`, `funnel`, `trust`, `loadedInstructions`, `at`, `tsFirst`, `tsLast`. `P` pooled: `kind`, `cv`, `rev`, `sid`,
119
149
  * the counts, `versions`, `models`, `env`, `costUsd`, `usage` (`at`, `tsFirst`, and `tsLast` at UTC day). `agentType`, `frame`, and `frameId` moved to `A` at EXTRACTOR 11:
120
150
  * a custom agent's name is private, and the joined frame is one value that links rows.
121
151
  */
@@ -126,6 +156,11 @@ export interface SessionObservation {
126
156
  * the Claude Code path's records and CAPTURE_VERSION are unchanged and an older oars reads a Cursor session as a session with no harness version.
127
157
  */
128
158
  harness?: Harness;
159
+ /**
160
+ * Set on a session a later read of its source folded into another (an OpenTelemetry trace deposited before its `gen_ai.conversation.id` arrived): the id of the session that
161
+ * holds its turns now. The record is a later rev of the early session with no turns, so the log stays append-only; a reader skips a session that carries it. Class P.
162
+ */
163
+ supersededBy?: string;
129
164
  /** CAPTURE_VERSION (a version-2 line has `v` instead). Class P. */
130
165
  cv: string;
131
166
  /** The first 8 hex characters of SHA-256 of the salt this record's hashes used; null on a record captured before version 4. Class P. */
@@ -193,12 +228,28 @@ export interface SessionObservation {
193
228
  usageObserved?: UsageField[];
194
229
  /** How many responses reported any usage (class P); compare with `assistantTurns` or `turns` for the share a sum covers. Same absence rule as `usageObserved`. */
195
230
  usageResponses?: number;
231
+ /** One entry per distinct model message that reported usage, in file order (CAPTURE 28); see `MessageUsage`. Absent when the file reports no usage, and on a record captured before version 28. */
232
+ messageUsage?: MessageUsage[];
196
233
  /** What the transcript shows of the setup the session ran with (class A); absent when it shows none, and on a record captured before version 12. */
197
234
  setup?: SessionSetup;
198
235
  /** The tool names at each funnel stage the transcript shows (class A); same absence rules. */
199
236
  funnel?: SessionFunnel;
200
237
  /** How often a human or a policy stopped the session and how long it ran alone (class A); same absence rules. */
201
238
  trust?: SessionTrust;
239
+ /**
240
+ * The instruction files the transcript shows loaded, as counts and levels, never a path (class A; `load-status.ts` joins it with the session-start snapshot). Absent when the
241
+ * transcript holds no such record. Added at CAPTURE 29.
242
+ */
243
+ loadedInstructions?: LoadedInstructions;
244
+ }
245
+ /** What a transcript shows loaded. `levels` is folders above the working folder, one entry per project file. */
246
+ export interface LoadedInstructions {
247
+ user: number;
248
+ levels: number[];
249
+ /** Files loaded from the working folder, by kind; absent on a record captured before version 30. */
250
+ local?: number;
251
+ dotClaude?: number;
252
+ rules?: number;
202
253
  }
203
254
  /**
204
255
  * What the last build or test runner after a prompt's last edit returned: `passed` and `failed` when the call's status reads that runner,
@@ -395,10 +446,28 @@ export interface ToolObservation {
395
446
  targetBase: TargetBase | null;
396
447
  /** The input's key names, sorted. Class A (EXTRACTOR 11). */
397
448
  keys: string[];
449
+ /** How many key names the wire did not send (off a public tool's pinned list, or kept `private` by the gate). Never captured: set only on a record the wire projection made (`projectedStore`) or read back from a skeleton, so a local read and the pool count one `dropped` cell. */
450
+ keysDropped?: number;
398
451
  /** The class of the call's path target (`targetClassOf`): a member of `TARGET_CLASSES`, `none` for a call with no path target. Absent on a record captured before CAPTURE 22. Class P: a closed list, never the path or its base name. */
399
452
  targetClass?: TargetClass;
400
453
  /** The classes of the call's arguments (`argClassOf`), a sorted set of `ARG_CLASSES` members, `['none']` for a call with no argument value. Absent on a record captured before CAPTURE 22. Class P: never a value. */
401
454
  argClass?: ArgClass[];
455
+ /** Each argument's features (`argFeaturesOf`), sorted by key: closed-list facts about its value (size, a path's depth, extension class, and folder kind, a pattern's features), never the value. Absent on a record captured before CAPTURE 27. Class A, as `keys` is: a private tool's key names are private. */
456
+ argFeatures?: ArgFeature[];
457
+ /** Each argument's closed value (`argValuesOf`): a boolean, or a string on its tool's list in `CLOSED_ARG_VALUES`; a value off the list is not stored. Absent on a record captured before CAPTURE 27, and when the call held none. Class A, as `argFeatures`. */
458
+ argValues?: ArgValue[];
459
+ /** The name tokens of the call's values (`argNamesOf`), sorted by kind and name: a file's name, a folder's name below the root, a variable's name, a flag value that is a name, a custom agent's name; never a value. Absent when it holds none and on a record captured before CAPTURE 33. Class A. */
460
+ names?: ArgName[];
461
+ /** The old and new text of an Edit (one pair), a MultiEdit (one pair per edit, in order), or a Write (a new digest only), as a digest of the exact text (`editDigestsOf`); a line ending is a difference. Absent on any other call and on a record captured before CAPTURE 35. Class A, as `target`: the digest is plain, so the same text matches across sessions and accounts. */
462
+ digests?: EditDigest[];
463
+ /** The files a `git restore` or `git checkout --` in a command whose status the call reports names, hashed as `target` is, so a restore reads against an earlier edit of the same path. Absent when the call restores none and on a record captured before CAPTURE 35. Class A. */
464
+ restores?: string[];
465
+ /** One template per command of a shell call (`templatesOf`): its program, subcommand, and flags, as written and masked by the redactor, and every literal argument as a slot of `TEMPLATE_SLOTS`; never an argument. Absent when the call holds no command and on a record captured before CAPTURE 34. Class A. */
466
+ templates?: string[];
467
+ /** What an edit did (`diffShapeOf`), a sorted set of `DIFF_SHAPES` members: `file-written` for a write, else the shapes of the old and new text; never the text. Absent on a call that is no edit, an edit that changes nothing, and a record captured before CAPTURE 34. Class A. */
468
+ diffShape?: DiffShape[];
469
+ /** What changed from the previous call of the same tool in the same prompt (`changeKindOf`), a sorted set of `CHANGE_KINDS` members. Absent on the first such call and on a record captured before CAPTURE 27. Class P. */
470
+ changeKind?: ChangeKind[];
402
471
  /** The class of a failed call (`errorCodeOf`): a member of `ERROR_CODES`, never result text. Absent on a call that did not fail and on a record captured before CAPTURE 14. Class P. */
403
472
  errorCode?: string;
404
473
  /** From the call's line to its first result's line; null when either timestamp is missing or no result is in the file. */
@@ -426,6 +495,11 @@ export interface ToolObservation {
426
495
  /** One entry per commit a clean shell call made, in order: the sha its result names, or null when it names none (CAPTURE 6). Absent when it made none. */
427
496
  commits?: Array<string | null>;
428
497
  }
498
+ /** The digest of one edit's text: the first 32 hex digits of the SHA-256 of the exact UTF-8 text. A Write has `new` only, never the digest of an empty string for `old`. */
499
+ export interface EditDigest {
500
+ old?: string;
501
+ new: string;
502
+ }
429
503
  /** One command of a shell call. */
430
504
  export interface ShellSegment {
431
505
  /** The program's basename and its plain words: no flags, paths, variables, or quoted text; redacted. */
@@ -640,7 +714,7 @@ export interface RecallCall {
640
714
  /** One hook invocation, appended to `hook-calls.jsonl` by the hook itself; `measure` counts these per session. Class A: `session`, `at`, `error`; P: `event`, `hit`, `ms`, `startMs`. */
641
715
  export interface HookCall {
642
716
  at: string;
643
- event: 'session-start' | 'prompt' | 'pre-tool' | 'post-tool' | 'stop' | 'nightly';
717
+ event: 'session-start' | 'prompt' | 'pre-tool' | 'post-tool' | 'stop' | 'nightly' | 'codex-record';
644
718
  /** The Claude Code session the hook ran in; null for the nightly run. */
645
719
  session: string | null;
646
720
  /** Whether the hook had something to say (an injected block, a recall hit, or a session deposited). */
@@ -30,6 +30,36 @@
30
30
  // reads, not moved ones. A Copilot CLI session with rotated `events.N.jsonl` archives is now read as one session, so its `prompts`, `tools`, `lines`, and skeleton move; one
31
31
  // with no archives is unchanged. EXTRACTOR stays 11. A version-16 store's next deposit captures every transcript again from the harness or `raw/`.
32
32
  //
33
+ // Version 32 (CAPTURE 32) is the machine repoKey salt: `repoKey` is derived from the machine's own salt, so every stored repoKey moves and an older store's next deposit captures every transcript again.
34
+ //
35
+ // Version 35 (CAPTURE 35) adds two optional fields to a tool call: `digests` (an Edit, MultiEdit, or Write call's old and new text, each as a digest) and `restores` (the files a `git restore` or `git checkout --` names, hashed as `target` is), and a
36
+ // `git restore` path also gives its file and folder names to `names`. It changes no value captured before; an older store's next deposit captures every transcript again, and a call of an older record has none.
37
+ //
38
+ // Version 33 (CAPTURE 33) adds the optional `names` to a tool call (`argNamesOf`, `call-shape.ts`): the name tokens its values hold, each with its kind, never a value. It changes no value captured before; an older store's next deposit captures every transcript again,
39
+ // and a call of an older record has none.
40
+ //
41
+ // Version 34 (CAPTURE 34) adds the optional `templates` to a shell call (`templatesOf`, `call-shape.ts`): one template per command, its program, subcommand, and flags with every literal argument as a typed slot, never an argument; and the optional `diffShape` to an
42
+ // edit (`diffShapeOf`): the shapes of the change from a closed list, never its text. It changes no value captured
43
+ // before; an older store's next deposit captures every transcript again, and a call of an older record has none.
44
+ //
45
+ // Version 31 (CAPTURE 31) writes a Codex `token_count` event's `input` and `output` only when the event reported them, as the cache categories already were, so a partial event leaves the other
46
+ // unobserved and not a measured zero (`usageObserved`). It changes a stored value of a Codex session; an older store's next deposit captures every transcript again.
47
+ //
48
+ // Version 30 (CAPTURE 30) adds the optional `local`, `dotClaude`, and `rules` counts to `loadedInstructions`: how many `CLAUDE.local.md`, `.claude/CLAUDE.md`, and `.claude/rules/` files the transcript shows
49
+ // loaded from the working folder. It changes no other value; an older store's next deposit captures every transcript again, and a record from before then reads those three as unknown.
50
+ //
51
+ // Version 29 (CAPTURE 29) adds the optional `loadedInstructions` to the session record: counts of the instruction files the transcript shows loaded, by level, never a path. It changes no value captured
52
+ // before; an older store's next deposit captures every transcript again, and a session recorded before then has none until it is.
53
+ //
54
+ // Version 28 (CAPTURE 28) adds the optional `messageUsage` to the session record: one entry per distinct model message (a Claude Code `message.id`, a Codex response), holding that message's token counts,
55
+ // the ids of the tool calls it made, and, for Codex, the plan percent used and window length when the event held a rate-limit block (`MessageUsage`). Numbers and ids only; the counts are never split across the
56
+ // message's calls. For a Codex event that did not report a cache category, that category is no longer observed, so `usageObserved` omits it and the skeleton and pool wire send null where they sent a zero bucket; no other value changes and EXTRACTOR stays as it is. A version-27 store's next deposit captures every transcript again, and a session recorded before then has no `messageUsage`
57
+ // until it is: nothing reads the field back into the older record. A file that reports no usage gets none either way.
58
+ //
59
+ // Version 27 (CAPTURE 27) adds three optional fields to a tool call (`call-shape.ts`), each computed from the input at capture and stored as closed-list members, never a value: `argFeatures` (per key, the value's size and,
60
+ // for a path, its depth, extension class, and folder kind, for a pattern what it uses), `argValues` (a boolean, or a string its tool's closed list holds), and `changeKind` (what changed from the previous call of the
61
+ // same tool in the prompt). It changes no value captured before, so EXTRACTOR stays as it is; a version-26 store's next deposit captures every transcript again, and a call of an older record has none of them.
62
+ //
33
63
  // Version 24 (CAPTURE 24, t903) changes three stored values and no field name: a step identity's skill-run name, host, and pull's package name are kept as they are after the operator's redaction (`redactDomains`, the secret and email patterns, the name shape, and the 128-character cap still apply; the operator's exclusion terms are applied at the deposit's gate, not at capture), where before each was `<literal>` unless a public list held it. A store opened at version 23 or older keeps its `<literal>` values until its next deposit captures every transcript again from the harness or `raw/`; the tool record's `steps` moves from class P to class A because those names can be private.
34
64
  //
35
65
  // Version 23 (CAPTURE 23) keeps a `git checkout` step's target class, `target` `path` or `word` (never the target's text), so the `revert` rule can tell `git checkout -- src/a.ts` and `git checkout --ours src/a.ts` (a
@@ -88,7 +118,7 @@
88
118
  // harness versions, and tool names (an MCP name's part after its prefix is redacted). `redact` masks secrets by pattern and entropy, emails, and the
89
119
  // organization domains the operator names. The skeleton holds hashes only.
90
120
  /** Stamped `cv` on every captured record. Bump it when the captured field list changes or a stored value of an existing captured record or skeleton changes: a bump re-reads every transcript. */
91
- export const CAPTURE_VERSION = '25';
121
+ export const CAPTURE_VERSION = '35';
92
122
  /** Capture versions the reader derives from. A version-3 record is upgraded on read; a newer one is read for the fields this code knows. */
93
123
  export const OLDEST_READABLE_CAPTURE = 3;
94
124
  /** Stamped `v` on every derived record. A bump re-derives from the log; no transcript is read. */
@@ -1,4 +1,4 @@
1
- import type { Harness } from './schema.js';
1
+ import type { Harness, LoadedInstructions } from './schema.js';
2
2
  type Obj = {
3
3
  [key: string]: unknown;
4
4
  };
@@ -52,6 +52,7 @@ export interface SessionTrust {
52
52
  unattendedRun?: number[];
53
53
  }
54
54
  export interface SessionShape {
55
+ loadedInstructions?: LoadedInstructions;
55
56
  setup?: SessionSetup;
56
57
  funnel?: SessionFunnel;
57
58
  trust?: SessionTrust;
@@ -5,6 +5,7 @@
5
5
  //
6
6
  // One collector per transcript, fed line by line by `captureSession`. It reads the Claude Code line shapes (a census of 2.1.312 to 2.1.318) and
7
7
  // one neutral line an adapter may write for a harness whose records are not Claude-shaped (`oars-tools`).
8
+ import { loadedInstructionsOf } from './load-status.js';
8
9
  const isObj = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
9
10
  const str = (value) => (typeof value === 'string' ? value : undefined);
10
11
  const names = (value) => (Array.isArray(value) ? value.filter((item) => typeof item === 'string' && item !== '') : []);
@@ -90,6 +91,8 @@ export const createShape = (harness, stored, options) => {
90
91
  const stopCommands = new Set();
91
92
  let stopSeen = false;
92
93
  const memory = new Map();
94
+ let instructionFiles;
95
+ let cwd;
93
96
  const plugins = new Set();
94
97
  /** Every name offered or removed, in order: the last event on a name decides which side it ends on. */
95
98
  const standing = new Map();
@@ -201,6 +204,15 @@ export const createShape = (harness, stored, options) => {
201
204
  stopSeen = true;
202
205
  }
203
206
  }
207
+ else if (kind === 'instructions' && instructionFiles === undefined && Array.isArray(a.files)) {
208
+ // The first record only: a resumed session logs the set again, and the snapshot it joins with is that session's first.
209
+ instructionFiles = a.files.flatMap((file) => {
210
+ if (!isObj(file))
211
+ return [];
212
+ const path = str(file.path);
213
+ return path === undefined ? [] : [{ path, type: str(file.type) ?? '' }];
214
+ });
215
+ }
204
216
  else if (kind === 'nested_memory' && isObj(a.content) && typeof a.content.content === 'string') {
205
217
  // The same file comes again when the harness reloads it, so one path counts once.
206
218
  const key = str(a.path) ?? str(a.content.path) ?? `#${String(memory.size)}`;
@@ -219,6 +231,7 @@ export const createShape = (harness, stored, options) => {
219
231
  // A sidechain line is a subagent's own, not the main session's setup.
220
232
  if (at.side)
221
233
  return;
234
+ cwd ??= str(line.cwd);
222
235
  const type = str(line.type);
223
236
  if (type === 'permission-mode')
224
237
  mode(line.permissionMode);
@@ -322,6 +335,8 @@ export const createShape = (harness, stored, options) => {
322
335
  setup.instructionBytes = [...memory.values()].reduce((sum, bytes) => sum + bytes, 0);
323
336
  if (Object.keys(setup).length > 0)
324
337
  out.setup = setup;
338
+ if (instructionFiles !== undefined && cwd !== undefined)
339
+ out.loadedInstructions = loadedInstructionsOf(instructionFiles, cwd);
325
340
  const funnel = {};
326
341
  // Two names that redact to one form are one tool, as `tools` keys it.
327
342
  const sideOf = (which) => [...new Set([...standing].filter(([, side]) => side === which).map(([name]) => stored(name)))].sort();
@@ -11,13 +11,22 @@ export interface SourceSession {
11
11
  mtimeMs: number;
12
12
  /** Set when the session is a spawned agent's: it is deposited as a subagent of its parent, so session counts stay main sessions. */
13
13
  subagent?: SubagentOrigin;
14
+ /**
15
+ * Set when a later read folded this session into another (`<id>` of that one): the deposit records the supersede for a session it already holds, with no transcript,
16
+ * and records nothing for one it never held. `size` and `mtimeMs` are 0, a cursor no read session has.
17
+ */
18
+ supersededBy?: string;
14
19
  }
15
20
  export interface SourceStore {
16
21
  sessions: SourceSession[];
17
22
  /** The session as transcript text for `captureSession`; throws when it cannot be read. */
18
23
  transcript: (session: SourceSession) => string | Promise<string>;
24
+ /** True when the session's file holds only the harness's own head records and no turn, so `sessions verify` counts it as empty, not unreadable; absent when the format has no such form. */
25
+ headOnly?: (session: SourceSession) => boolean | Promise<boolean>;
19
26
  /** Sessions skipped because their id is not a safe file name, counted so the skip is never silent. */
20
27
  rejected: number;
28
+ /** Files the reader dropped before they were sessions that held records in a shape it does not know; only `sessions verify` reads it, as seen and unreadable. Absent when the format has no such drop. */
29
+ shapeMoved?: number;
21
30
  /** Items that could not be inspected, warned about once each and never fatal; `reason` names no path. */
22
31
  unreadable: readonly {
23
32
  file: string;
@@ -7,7 +7,7 @@ import { copilotRoot, openCopilotStore } from './copilot.js';
7
7
  import { cursorRoot, openCursorStore } from './cursor.js';
8
8
  import { grokRoot, openGrokStore } from './grok.js';
9
9
  import { geminiRoot, openGeminiStore } from './gemini.js';
10
- import { openPiStore, piRoot } from './pi.js';
10
+ import { openPiStore, piSessionsDir } from './pi.js';
11
11
  import { openOtelStore, otelRoot } from './otel.js';
12
12
  import { opencodeRoot, openOpencodeStore } from './opencode.js';
13
13
  import { openVscodeCopilotStore, vscodeAppData } from './vscode-copilot.js';
@@ -59,6 +59,7 @@ const codex = {
59
59
  ...(session.parent === null ? {} : { subagent: { parent: `codex-${session.parent}`, agentType: session.agentRole, spawnedBy: null } }),
60
60
  })),
61
61
  transcript: async (session) => await store.transcript(need(byId.get(session.id))),
62
+ headOnly: async (session) => await store.headOnly(need(byId.get(session.id))),
62
63
  rejected: store.rejected,
63
64
  unreadable: store.unreadable,
64
65
  where: (session) => byId.get(session.id)?.file ?? root,
@@ -80,6 +81,7 @@ const copilot = {
80
81
  return {
81
82
  sessions: store.sessions.map((session) => ({ id: session.id, folder: session.folder, size: session.size, mtimeMs: session.mtimeMs })),
82
83
  transcript: async (session) => await store.transcript(need(byId.get(session.id))),
84
+ headOnly: async (session) => await store.headOnly(need(byId.get(session.id))),
83
85
  rejected: store.rejected,
84
86
  unreadable: store.unreadable,
85
87
  where: (session) => byId.get(session.id)?.file ?? root,
@@ -108,6 +110,7 @@ const gemini = {
108
110
  })),
109
111
  transcript: async (session) => await store.transcript(need(byId.get(session.id))),
110
112
  rejected: store.rejected,
113
+ shapeMoved: store.shapeMoved,
111
114
  unreadable: store.unreadable,
112
115
  where: (session) => byId.get(session.id)?.file ?? root,
113
116
  close: () => undefined,
@@ -155,6 +158,7 @@ const opencode = {
155
158
  ...(session.parent === null ? {} : { subagent: { parent: `opencode-${session.parent}`, agentType: session.agent, spawnedBy: null } }),
156
159
  })),
157
160
  transcript: (session) => store.transcript(need(byId.get(session.id))),
161
+ headOnly: (session) => store.headOnly(need(byId.get(session.id))),
158
162
  rejected: store.rejected,
159
163
  unreadable: [],
160
164
  where: () => join(root, 'opencode.db'),
@@ -166,8 +170,8 @@ const pi = {
166
170
  name: 'pi',
167
171
  harness: 'pi',
168
172
  label: 'Pi',
169
- root: (_platform, env, home) => piRoot(env, home),
170
- storeFile: (root) => join(root, 'sessions'),
173
+ root: (_platform, env, home) => piSessionsDir(env, home),
174
+ storeFile: (root) => root,
171
175
  open: async (root) => {
172
176
  const store = await openPiStore(root);
173
177
  if (store === undefined)
@@ -176,6 +180,7 @@ const pi = {
176
180
  return {
177
181
  sessions: store.sessions.map((session) => ({ id: session.id, folder: session.folder, size: session.size, mtimeMs: session.mtimeMs })),
178
182
  transcript: async (session) => await store.transcript(need(byId.get(session.id))),
183
+ headOnly: async (session) => await store.headOnly(need(byId.get(session.id))),
179
184
  rejected: store.rejected,
180
185
  unreadable: store.unreadable,
181
186
  where: (session) => byId.get(session.id)?.file ?? root,
@@ -197,6 +202,7 @@ const vscodeCopilot = {
197
202
  return {
198
203
  sessions: store.sessions.map((session) => ({ id: session.id, folder: session.folder, size: session.size, mtimeMs: session.mtimeMs })),
199
204
  transcript: async (session) => await store.transcript(need(byId.get(session.id))),
205
+ headOnly: async (session) => await store.headOnly(need(byId.get(session.id))),
200
206
  rejected: store.rejected,
201
207
  unreadable: store.unreadable,
202
208
  where: (session) => byId.get(session.id)?.files[0] ?? root,
@@ -218,9 +224,14 @@ const otel = {
218
224
  return undefined;
219
225
  const byId = new Map(store.sessions.map((session) => [session.id, session]));
220
226
  return {
221
- sessions: store.sessions.map((session) => ({ id: session.id, folder: session.folder, size: session.size, mtimeMs: session.mtimeMs, ...(session.subagent === undefined ? {} : { subagent: session.subagent }) })),
222
- transcript: (session) => store.transcript(need(byId.get(session.id))),
227
+ sessions: [
228
+ ...store.sessions.map((session) => ({ id: session.id, folder: session.folder, size: session.size, mtimeMs: session.mtimeMs, ...(session.subagent === undefined ? {} : { subagent: session.subagent }) })),
229
+ ...store.superseded.map((entry) => ({ id: entry.id, folder: null, size: 0, mtimeMs: 0, supersededBy: entry.by })),
230
+ ],
231
+ transcript: (session) => (session.supersededBy === undefined ? store.transcript(need(byId.get(session.id))) : ''),
232
+ headOnly: (session) => session.supersededBy === undefined && store.headOnly(need(byId.get(session.id))),
223
233
  rejected: store.rejected,
234
+ shapeMoved: store.shapeMoved,
224
235
  unreadable: store.unreadable,
225
236
  where: (session) => byId.get(session.id)?.file ?? root,
226
237
  close: () => undefined,
@@ -1,16 +1,17 @@
1
1
  // The step log: `pipeline-runs.jsonl`, one row per run of a pipeline step, appended when the step ends (runtime-commons pipeline spec 3.1).
2
2
  // Local only: it sits in the store directory beside `hook-calls.jsonl`, and `measure` prints the spec 3.2 counters from it.
3
3
  import { randomUUID } from 'node:crypto';
4
- import { appendFile, mkdir, readFile } from 'node:fs/promises';
4
+ import { appendFile, readFile } from 'node:fs/promises';
5
5
  import { homedir } from 'node:os';
6
6
  import { performance } from 'node:perf_hooks';
7
7
  import { CliError, ExitCode } from '../errors.js';
8
+ import { mkdirOwnerOnly } from './owner-only.js';
8
9
  import { makeRedactor } from './redact.js';
9
10
  import { CAPTURE_VERSION, EXTRACTOR_VERSION, parseStepRow } from './schema.js';
10
11
  import { digest, newTrace, paths, readRedactDomains } from './store.js';
11
12
  export const newContext = (storeDir, now, trigger) => ({ storeDir, now, run: randomUUID(), trigger });
12
13
  export const appendStep = async (storeDir, row) => {
13
- await mkdir(storeDir, { recursive: true });
14
+ await mkdirOwnerOnly(storeDir);
14
15
  await appendFile(paths(storeDir).pipelineRuns, `${JSON.stringify(row)}\n`, { mode: 0o600 });
15
16
  };
16
17
  export const readSteps = async (storeDir) => {
@@ -3,6 +3,7 @@ import { type FileHandle } from 'node:fs/promises';
3
3
  import { CliError } from '../errors.js';
4
4
  import { slugOf } from './allow.js';
5
5
  import { type Source } from './sources.js';
6
+ import { type SubagentOrigin } from './extract.js';
6
7
  import { type RepoSize } from './public-vocabulary.js';
7
8
  import { type DerivedRecords, type ErrorObservation, type PromptObservation, type RecallCall, type SessionObservation, type SignatureRecord, type StepCounts, type Skeleton, type ToolObservation } from './schema.js';
8
9
  /** Where the store and the operator's Claude Code logs are; `bin.ts` fills it from the environment. */
@@ -21,12 +22,18 @@ export interface SessionsRuntime {
21
22
  sources?: readonly Source[];
22
23
  /** Where the account's salt comes from; absent means this machine's own (`localSalt`). The seam an account identity replaces (M4). */
23
24
  accountSalt?: (storeDir: string) => Promise<AccountSalt>;
25
+ /** The salt repository keys are hashed under; a machine takes it from its enrollment (`OARS_REPO_SALT`). Absent means the one derived from the account salt, as the trusted machine computes it. */
26
+ repoSalt?: string;
24
27
  /** The fresh single-turn reader behind `gold arms`; absent in a run that has none. */
25
28
  evaluator?: GoldEvaluator | undefined;
26
29
  stdin: NodeJS.ReadableStream;
27
30
  stdout: {
28
31
  write: (text: string) => unknown;
29
32
  };
33
+ /** Only its `isTTY` is read: the covenant screen is written to stderr, so a person is at a terminal only when stderr is one too. */
34
+ stderr?: {
35
+ isTTY?: boolean | undefined;
36
+ } | undefined;
30
37
  }
31
38
  interface FileState {
32
39
  size: number;
@@ -55,9 +62,15 @@ export interface State {
55
62
  /** What the person chose about sending to a pool: the raw-copy consent and the folders switched down. Never read by capture. */
56
63
  pool: PoolChoices;
57
64
  }
58
- /** The answer to the raw-copy prompt (`granted`, `declined`, or not asked yet) and the per-folder switches (an `--allow`-style entry mapped to what it may send). */
65
+ /**
66
+ * The answer to the raw-copy prompt (`granted`, `declined`, or not asked yet) and the per-folder switches (an `--allow`-style entry mapped to what it may send).
67
+ * A yes carries the covenant version in force when it was given, because the prompt names the sale and training use that covenant states. A yes with no version predates that wording and reads as not asked.
68
+ */
59
69
  export interface PoolChoices {
60
70
  rawConsent: 'granted' | 'declined' | null;
71
+ rawConsentVersion: string | null;
72
+ /** True from a typed no that the pool was not told, while this machine held a yes, until the pool hears the withdrawal or a new yes is typed: the pool may still hold the yes, so a rerun of the no must not read the local `declined` as nothing to tell. */
73
+ withdrawalPending: boolean;
61
74
  folders: {
62
75
  [entry: string]: FolderSend;
63
76
  };
@@ -70,6 +83,7 @@ export declare const paths: (storeDir: string) => {
70
83
  skeletons: string;
71
84
  calls: string;
72
85
  hookCalls: string;
86
+ hookRows: string;
73
87
  pipelineRuns: string;
74
88
  salt: string;
75
89
  raw: string;
@@ -142,6 +156,7 @@ export interface DepositResult {
142
156
  /** Allowed names that matched no project folder. */
143
157
  missing: string[];
144
158
  }
159
+ export declare const projectFolders: (projectsRoot: string) => Promise<string[]>;
145
160
  /** What a deposit never reads: project folders that hold transcripts but that no `--allow` entry names. */
146
161
  export interface SkippedScope {
147
162
  folders: number;
@@ -156,6 +171,18 @@ export interface SkippedScope {
156
171
  * only, never a transcript, so `verify` and `books` can say what the store does not hold.
157
172
  */
158
173
  export declare const skippedFolders: (runtime: SessionsRuntime) => Promise<SkippedScope>;
174
+ /** A transcript to deposit: a main session file, or a subagent's under `<session>/subagents/` (register A24). */
175
+ export interface Transcript {
176
+ file: string;
177
+ /** `<session>`, or `<parent>~agent-x` / `<parent>~wf_y~agent-x` for a subagent. */
178
+ session: string;
179
+ /** The path under the project folder, the key in `state.json`. */
180
+ relative: string;
181
+ subagent?: SubagentOrigin;
182
+ /** Read from the store's own archive because the harness no longer has the transcript; it is not archived again. */
183
+ fromRaw?: boolean;
184
+ }
185
+ export declare const mainTranscript: (folder: string, name: string) => Transcript;
159
186
  export declare const listDir: (dir: string) => Promise<import("fs").Dirent<string>[]>;
160
187
  interface SubagentFile {
161
188
  dir: string;
@@ -165,6 +192,7 @@ interface SubagentFile {
165
192
  name: string;
166
193
  }
167
194
  export declare const subagentFiles: (folder: string, sessions: readonly string[]) => Promise<SubagentFile[]>;
195
+ export declare const subagentTranscripts: (folder: string, sessions: readonly string[]) => Promise<Transcript[]>;
168
196
  /** The session folders under a project folder: each may hold subagent transcripts, even when its main transcript is gone. */
169
197
  export declare const sessionFolders: (folder: string) => Promise<string[]>;
170
198
  /** What a derive pass read and wrote, for the step log (spec 3.1 `in` and `out`, 4.2 `key`). */
@@ -234,6 +262,10 @@ export declare class StoreLockLostError extends CliError {
234
262
  */
235
263
  export declare const withLock: <T>(storeDir: string, work: (held: LockHold) => Promise<T>, options?: LockOptions) => Promise<T>;
236
264
  export declare const deposit: (runtime: SessionsRuntime, options: DepositOptions, trace?: DepositTrace) => Promise<DepositResult>;
265
+ /** The table in force: the runtime's own (a test's), else every source. */
266
+ export declare const sourcesOf: (runtime: SessionsRuntime) => readonly Source[];
267
+ /** The roots the runtime names, by source name; `cursorRoot` stays the alias of the Cursor entry. */
268
+ export declare const sourceRoots: (runtime: SessionsRuntime) => Map<string, string>;
237
269
  /**
238
270
  * The log's lines, a chunk at a time: the log can pass the longest string Node can hold (536,870,888 characters), so it is never one string.
239
271
  * It splits on the newline alone, not through `readline`, which also ends a line at U+2028 and U+2029: `JSON.stringify` leaves those raw inside a
@@ -250,6 +282,7 @@ export declare const frozenSessions: (runtime: SessionsRuntime, behind: readonly
250
282
  project: string;
251
283
  session: string;
252
284
  }[]) => Promise<Set<string>>;
285
+ export declare const HOOK_LOCK: LockOptions;
253
286
  export type SessionDeposit = 'deposited' | 'unchanged' | 'not-allowed' | 'missing' | 'deferred';
254
287
  /**
255
288
  * Deposits one transcript, as the SessionEnd hook does: only from a folder that earlier `--allow` entries name, and idempotent by size and
@@ -280,12 +313,18 @@ interface LogScan {
280
313
  newerCaptures: number;
281
314
  }
282
315
  export declare const scanLog: (file: string, held?: FileHandle) => Promise<LogScan>;
316
+ /** What a read keeps beyond the default. */
317
+ export type LoadOptions = {
318
+ /** Keep each session's `messageUsage` entries. Nothing in the store reads them yet, and on a large store they are a third more heap, so a read holds them only when asked. */
319
+ messageUsage?: boolean;
320
+ };
283
321
  /**
284
322
  * The store as a reader sees it: for each session only the highest `rev`. A version-3 or later session is derived here from its capture records
285
323
  * (EXTRACTOR_VERSION rules); a session whose last deposit was version 2 keeps the prompt and error lines that deposit wrote.
286
324
  * `afterFirstPass` runs between the two passes over the log: a seam for the test that compacts the log there.
325
+ * `options.messageUsage` keeps the per-message usage entries on each session record; without it they are dropped as the record is read.
287
326
  */
288
- export declare const loadStore: (storeDir: string, afterFirstPass?: () => Promise<void>) => Promise<Store>;
327
+ export declare const loadStore: (storeDir: string, afterFirstPass?: () => Promise<void>, options?: LoadOptions) => Promise<Store>;
289
328
  /**
290
329
  * Drops the superseded revs from the log, so a re-capture does not leave a full copy of every session behind. The caller holds the store lock,
291
330
  * the same one every append takes. Only a line a higher rev of its session replaces goes: a line this version cannot read stays, since a later