@manyoars/oars 0.3.0 → 0.4.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 (161) hide show
  1. package/README.md +44 -29
  2. package/dist/cli.d.ts +4 -1
  3. package/dist/cli.js +102 -32
  4. package/dist/commands/login.d.ts +1 -1
  5. package/dist/commands/login.js +1 -1
  6. package/dist/gate/hook-main.js +13 -1
  7. package/dist/gate/hook-process.d.ts +11 -1
  8. package/dist/gate/hook-process.js +50 -2
  9. package/dist/gate/hook-record.d.ts +3 -0
  10. package/dist/gate/hook-record.js +8 -0
  11. package/dist/gate/hook.d.ts +2 -0
  12. package/dist/gate/hook.js +3 -0
  13. package/dist/gate/replay.js +2 -5
  14. package/dist/gate/report.js +1 -1
  15. package/dist/gate/transcripts.js +1 -1
  16. package/dist/hook-bin.js +27 -2
  17. package/dist/issuer.d.ts +7 -9
  18. package/dist/issuer.js +10 -32
  19. package/dist/main.js +3 -0
  20. package/dist/sessions/actions.js +78 -0
  21. package/dist/sessions/allow.d.ts +10 -1
  22. package/dist/sessions/allow.js +29 -9
  23. package/dist/sessions/baseline.d.ts +1 -1
  24. package/dist/sessions/baseline.js +6 -4
  25. package/dist/sessions/call-shape.d.ts +19 -0
  26. package/dist/sessions/call-shape.js +85 -0
  27. package/dist/sessions/cell-increments.d.ts +5 -1
  28. package/dist/sessions/cell-increments.js +84 -50
  29. package/dist/sessions/cells.d.ts +33 -3
  30. package/dist/sessions/cells.js +89 -9
  31. package/dist/sessions/codex.js +93 -27
  32. package/dist/sessions/command.d.ts +11 -1
  33. package/dist/sessions/command.js +64 -18
  34. package/dist/sessions/coverage.d.ts +19 -0
  35. package/dist/sessions/coverage.js +56 -0
  36. package/dist/sessions/cursor.js +2 -2
  37. package/dist/sessions/derive.js +72 -6
  38. package/dist/sessions/exclusion.d.ts +6 -0
  39. package/dist/sessions/exclusion.js +8 -0
  40. package/dist/sessions/extract.d.ts +4 -2
  41. package/dist/sessions/extract.js +58 -26
  42. package/dist/sessions/hook-calls.js +1 -1
  43. package/dist/sessions/hook-index.d.ts +14 -1
  44. package/dist/sessions/hook-index.js +10 -3
  45. package/dist/sessions/hooks.d.ts +5 -1
  46. package/dist/sessions/hooks.js +133 -22
  47. package/dist/sessions/identity.d.ts +8 -3
  48. package/dist/sessions/identity.js +45 -15
  49. package/dist/sessions/install.d.ts +18 -2
  50. package/dist/sessions/install.js +53 -8
  51. package/dist/sessions/mcp.d.ts +3 -2
  52. package/dist/sessions/mcp.js +8 -4
  53. package/dist/sessions/motif.d.ts +2 -1
  54. package/dist/sessions/motif.js +2 -2
  55. package/dist/sessions/opencode.d.ts +33 -0
  56. package/dist/sessions/opencode.js +319 -0
  57. package/dist/sessions/otel.d.ts +60 -0
  58. package/dist/sessions/otel.js +494 -0
  59. package/dist/sessions/package-books.d.ts +48 -0
  60. package/dist/sessions/package-books.js +120 -0
  61. package/dist/sessions/package-resolve.d.ts +2 -0
  62. package/dist/sessions/package-resolve.js +3 -1
  63. package/dist/sessions/pi.d.ts +36 -0
  64. package/dist/sessions/pi.js +353 -0
  65. package/dist/sessions/pool-books.d.ts +21 -0
  66. package/dist/sessions/pool-books.js +75 -0
  67. package/dist/sessions/pool-choices.d.ts +5 -0
  68. package/dist/sessions/pool-choices.js +11 -0
  69. package/dist/sessions/pool-command.d.ts +76 -5
  70. package/dist/sessions/pool-command.js +263 -136
  71. package/dist/sessions/pool-nightly-deps.d.ts +3 -0
  72. package/dist/sessions/pool-nightly-deps.js +11 -0
  73. package/dist/sessions/pool-nightly.d.ts +10 -0
  74. package/dist/sessions/pool-nightly.js +85 -0
  75. package/dist/sessions/pool-outbox.d.ts +15 -2
  76. package/dist/sessions/pool-outbox.js +79 -26
  77. package/dist/sessions/pool-print.d.ts +1 -1
  78. package/dist/sessions/pool-raw.d.ts +27 -0
  79. package/dist/sessions/pool-raw.js +68 -0
  80. package/dist/sessions/pool-replica.d.ts +35 -0
  81. package/dist/sessions/pool-replica.js +91 -0
  82. package/dist/sessions/pool-send-lock.d.ts +20 -0
  83. package/dist/sessions/pool-send-lock.js +87 -0
  84. package/dist/sessions/pool-sent.d.ts +4 -0
  85. package/dist/sessions/pool-sent.js +26 -0
  86. package/dist/sessions/pool-totals.d.ts +10 -0
  87. package/dist/sessions/pool-totals.js +21 -0
  88. package/dist/sessions/pool-wire.d.ts +240 -30
  89. package/dist/sessions/pool-wire.js +695 -215
  90. package/dist/sessions/public-vocabulary.d.ts +2 -3
  91. package/dist/sessions/public-vocabulary.js +2 -9
  92. package/dist/sessions/query.d.ts +16 -4
  93. package/dist/sessions/query.js +32 -10
  94. package/dist/sessions/read-cost.js +14 -3
  95. package/dist/sessions/read-recovery-edge.d.ts +2 -0
  96. package/dist/sessions/read-recovery-edge.js +26 -0
  97. package/dist/sessions/read.d.ts +8 -1
  98. package/dist/sessions/read.js +6 -3
  99. package/dist/sessions/recall-bundle.d.ts +12 -0
  100. package/dist/sessions/recall-bundle.js +33 -0
  101. package/dist/sessions/recall-frames.d.ts +18 -0
  102. package/dist/sessions/recall-frames.js +44 -0
  103. package/dist/sessions/recall-next.d.ts +145 -0
  104. package/dist/sessions/recall-next.js +241 -0
  105. package/dist/sessions/recall-object.d.ts +2 -0
  106. package/dist/sessions/recall-object.js +1 -0
  107. package/dist/sessions/recall-push.d.ts +2 -2
  108. package/dist/sessions/recall-push.js +3 -51
  109. package/dist/sessions/recall-reads.d.ts +7 -0
  110. package/dist/sessions/recall-reads.js +7 -0
  111. package/dist/sessions/recovery-calls.d.ts +21 -0
  112. package/dist/sessions/recovery-calls.js +25 -0
  113. package/dist/sessions/redact.d.ts +11 -0
  114. package/dist/sessions/redact.js +59 -0
  115. package/dist/sessions/schema.d.ts +59 -9
  116. package/dist/sessions/schema.js +22 -3
  117. package/dist/sessions/session-shape.d.ts +6 -3
  118. package/dist/sessions/session-shape.js +19 -6
  119. package/dist/sessions/sources.js +75 -1
  120. package/dist/sessions/store.d.ts +13 -1
  121. package/dist/sessions/store.js +36 -16
  122. package/dist/sessions/survival.d.ts +2 -0
  123. package/dist/sessions/survival.js +2 -2
  124. package/dist/sessions/transcript-tail.d.ts +14 -0
  125. package/dist/sessions/transcript-tail.js +64 -0
  126. package/dist/sessions/vscode-copilot.js +24 -7
  127. package/dist/telemetry/command.d.ts +8 -0
  128. package/dist/telemetry/command.js +85 -0
  129. package/dist/telemetry/consent.d.ts +17 -0
  130. package/dist/telemetry/consent.js +63 -0
  131. package/dist/telemetry/crash.d.ts +8 -0
  132. package/dist/telemetry/crash.js +52 -0
  133. package/dist/telemetry/emit.d.ts +31 -0
  134. package/dist/telemetry/emit.js +42 -0
  135. package/dist/telemetry/fields.d.ts +16 -0
  136. package/dist/telemetry/fields.js +26 -0
  137. package/dist/telemetry/health.d.ts +21 -0
  138. package/dist/telemetry/health.js +108 -0
  139. package/dist/telemetry/hook-emit.d.ts +4 -0
  140. package/dist/telemetry/hook-emit.js +18 -0
  141. package/dist/telemetry/measures.d.ts +32 -0
  142. package/dist/telemetry/measures.js +128 -0
  143. package/dist/telemetry/observe.d.ts +13 -0
  144. package/dist/telemetry/observe.js +51 -0
  145. package/dist/telemetry/read-events.d.ts +9 -0
  146. package/dist/telemetry/read-events.js +17 -0
  147. package/dist/telemetry/record.d.ts +17 -0
  148. package/dist/telemetry/record.js +26 -0
  149. package/dist/telemetry/report.d.ts +77 -0
  150. package/dist/telemetry/report.js +188 -0
  151. package/dist/telemetry/schema.d.ts +40 -0
  152. package/dist/telemetry/schema.js +187 -0
  153. package/dist/telemetry/send-main.d.ts +1 -0
  154. package/dist/telemetry/send-main.js +11 -0
  155. package/dist/telemetry/send.d.ts +36 -0
  156. package/dist/telemetry/send.js +172 -0
  157. package/dist/telemetry/spool.d.ts +17 -0
  158. package/dist/telemetry/spool.js +70 -0
  159. package/dist/telemetry/wire.d.ts +25 -0
  160. package/dist/telemetry/wire.js +76 -0
  161. package/package.json +2 -2
@@ -1,12 +1,13 @@
1
1
  import type { CanonicalAction } from './actions.js';
2
+ import type { ArgClass, TargetClass } from './call-shape.js';
2
3
  import type { RepoSize } from './public-vocabulary.js';
3
4
  import type { SessionFunnel, SessionSetup, SessionTrust } from './session-shape.js';
4
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. */
5
- export declare const CAPTURE_VERSION = "17";
6
+ export declare const CAPTURE_VERSION = "25";
6
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. */
7
8
  export declare const OLDEST_READABLE_CAPTURE = 3;
8
9
  /** Stamped `v` on every derived record. A bump re-derives from the log; no transcript is read. */
9
- export declare const EXTRACTOR_VERSION = "12";
10
+ export declare const EXTRACTOR_VERSION = "15";
10
11
  export declare const PROMPT_HEAD_CHARS = 300;
11
12
  export declare const ERROR_EXAMPLE_CHARS = 160;
12
13
  /** The reference scripts cut a normalized signature at 110 characters. */
@@ -15,6 +16,12 @@ export declare const SIGNATURE_CHARS = 110;
15
16
  export declare const ERROR_LINE_CHARS = 1000;
16
17
  /** Per-session cap on `files` and `fileHashes` (L3): the hand run on the copy store had p99 61 and max 209 files per session; 100 is the round number above p99. */
17
18
  export declare const FILES_PER_SESSION_MAX = 100;
19
+ /**
20
+ * What the next human prompt did to a prompt's work (t882, `docs/sessions-store.md` "Prompt intervention"): a closed list, not a grade. `user-stop` (EXTRACTOR 14) is a prompt the human stopped: a rejected call, or an interrupt. `unknown` is a prompt the records cannot say anything about (the last of its
21
+ * transcript, or one whose next prompt ran a shell call captured before `segments`); a prompt derived before EXTRACTOR 13 has no value at all, which is a different thing, and one derived at 13 read a stop as `unknown`.
22
+ */
23
+ export declare const PROMPT_INTERVENTIONS: readonly ["none", "after-error", "redo", "revert", "user-stop", "unknown"];
24
+ export type PromptIntervention = (typeof PROMPT_INTERVENTIONS)[number];
18
25
  export declare const PROMPT_CLASSES: readonly ["no-tools", "orchestration", "research", "edit-verify", "edit-only", "explore-only", "shell-only", "mixed"];
19
26
  export type PromptClass = (typeof PROMPT_CLASSES)[number];
20
27
  /** The statuses agents recover from differently (pool vocabulary spec Section 7); any other status is `other`. */
@@ -32,7 +39,14 @@ export interface Usage {
32
39
  output: number;
33
40
  thinking: number;
34
41
  }
35
- export type Harness = 'claude-code' | 'cursor' | 'codex' | 'copilot' | 'gemini' | 'grok' | 'vscode-copilot';
42
+ /** The five usage categories, in the order a record lists the ones it observed. */
43
+ export declare const USAGE_FIELDS: readonly ["input", "cacheWrite", "cacheRead", "output", "thinking"];
44
+ export type UsageField = (typeof USAGE_FIELDS)[number];
45
+ /** Whether a stored session or prompt recorded `field`. A record with no `usageObserved` was captured before version 20, when every category read as observed. */
46
+ export declare const observesUsage: (record: {
47
+ usageObserved?: readonly UsageField[];
48
+ }, field: UsageField) => boolean;
49
+ export type Harness = 'claude-code' | 'cursor' | 'codex' | 'copilot' | 'gemini' | 'grok' | 'opencode' | 'pi' | 'vscode-copilot' | 'otel';
36
50
  /** The harness a session record came from; a record that does not name one is Claude Code's. */
37
51
  export declare const harnessOf: (session: {
38
52
  harness?: Harness;
@@ -171,6 +185,14 @@ export interface SessionObservation {
171
185
  costUsd: number | null;
172
186
  /** Summed assistant usage of the file's own line (a main transcript's sidechain lines are not counted; a subagent transcript is its own line). */
173
187
  usage: Usage;
188
+ /**
189
+ * Which `usage` categories at least one counted response reported (class P). A category not listed was never recorded by the source, so its `usage` number is a
190
+ * placeholder zero and not a measurement; a listed one is a sum over `usageResponses` responses, which a partial sum covers only in part. Absent on a record captured
191
+ * before version 20: every category reads as observed there.
192
+ */
193
+ usageObserved?: UsageField[];
194
+ /** 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
+ usageResponses?: number;
174
196
  /** 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. */
175
197
  setup?: SessionSetup;
176
198
  /** The tool names at each funnel stage the transcript shows (class A); same absence rules. */
@@ -188,7 +210,7 @@ export type PromptStop = 'interrupt' | 'rejected';
188
210
  /**
189
211
  * One per user prompt: the `prompt_graphs.py` and `cost_per_class.py` records joined. Derived. Classes: `session`,
190
212
  * `contextFingerprint`, `frame`, `head`, `ts`, `skills`, `verify`, `stopped`, `commits`, and `redo` (A); `kind`, `v`, `rev`, `index`, `wallSec`, `cls`, `tools`,
191
- * `shape`, `errors`, `thinking`, `sideTools`, `turns`, `model`, `efforts`, `usage`, `recovery`, `explore`, `act`, and `bigrams` (P).
213
+ * `shape`, `errors`, `thinking`, `sideTools`, `turns`, `model`, `efforts`, `usage`, `recovery`, `explore`, `act`, `bigrams`, and `intervention` (P).
192
214
  */
193
215
  export interface PromptObservation {
194
216
  kind: 'prompt';
@@ -216,6 +238,9 @@ export interface PromptObservation {
216
238
  efforts: string[];
217
239
  skills: string[];
218
240
  usage: Usage;
241
+ /** Carried from the capture record: the categories and response count the prompt's `usage` rests on (see `UserPromptObservation`). */
242
+ usageObserved?: UsageField[];
243
+ usageResponses?: number;
219
244
  /** The two tools after the first error of each error run (the recovery pair); empty when no error. */
220
245
  recovery: string[];
221
246
  explore: number;
@@ -235,6 +260,8 @@ export interface PromptObservation {
235
260
  commits?: number;
236
261
  /** A later prompt edited a path this one edited, within `REDO_WINDOW` prompts, with no commit `commits` can see between them. */
237
262
  redo?: boolean;
263
+ /** Whether a human stepped in after this prompt's work, and how (EXTRACTOR 13): what the next prompt did, never a grade. Absent on a prompt derived earlier. Class P: a member of a closed list. */
264
+ intervention?: PromptIntervention;
238
265
  }
239
266
  /**
240
267
  * One per tool error: the `error_signatures.py` record. Derived. Classes: `session`, `agent`, `ts`, `sig`, `example` (A); `kind`, `v`,
@@ -268,7 +295,7 @@ export interface ErrorObservation {
268
295
  agent?: string;
269
296
  }
270
297
  export type ToolStatus = 'ok' | 'error' | 'none';
271
- /** A package a shell step pulled, in public names (`<literal>` outside the allowlist). */
298
+ /** A package a shell step pulled. The package name is kept as it is after redaction (`<literal>` when redaction changes it, it is not a name, or it is over 128 characters); the registry is a public name. */
272
299
  export interface PullIdentity {
273
300
  registry: string;
274
301
  name: string;
@@ -280,6 +307,11 @@ export interface ShellIdentity {
280
307
  program: string;
281
308
  sub: string | null;
282
309
  flags: string[];
310
+ /**
311
+ * Only on `git checkout` (CAPTURE 23), the class of what it acts on and never the target's text: `path` where the line proves a path (a `--` marker, or a flag that only takes
312
+ * paths), `word` where a target word the segment drops (a slash, quotes) stands with no such proof, so it is a branch or a path and the line cannot say which. Absent with no such word.
313
+ */
314
+ target?: 'path' | 'word';
283
315
  /** Absent when the command pulled nothing. */
284
316
  pulls?: PullIdentity[];
285
317
  }
@@ -295,6 +327,8 @@ export interface StepCost {
295
327
  /** The output tokens of the call's response, shared evenly among that response's calls; null when it reported none. */
296
328
  tokens: number | null;
297
329
  durationMs: number | null;
330
+ /** True when `tokens` was divided among the response's calls, not measured for this call; absent when the response made one call. */
331
+ allocated?: true;
298
332
  }
299
333
  /**
300
334
  * How a step came about (recall spec Section 2.3), naming the step it came from by position. `next`: order within a prompt's line of work.
@@ -315,7 +349,7 @@ export interface StepObservation {
315
349
  action: CanonicalAction;
316
350
  /** The harness's own name for the tool when the action table lists it or every name it holds is public, else `<literal>` (the tool record keeps it, class A). */
317
351
  native: string;
318
- /** What the step acted on, in public names only; null for an action that has none (a file's path never appears here). */
352
+ /** What the step acted on; a skill-run name, host, and pull name are kept as they are after redaction (`<literal>` when redaction changes them, they are not names, or they are over 128 characters), and every other name is public or `<literal>`. Null for an action that has none (a file's path never appears here). */
319
353
  identity: StepIdentity;
320
354
  /** The call's status when it reports this segment; `none` for a segment it does not, such as one a pipe hides. */
321
355
  status: ToolStatus;
@@ -361,17 +395,25 @@ export interface ToolObservation {
361
395
  targetBase: TargetBase | null;
362
396
  /** The input's key names, sorted. Class A (EXTRACTOR 11). */
363
397
  keys: string[];
398
+ /** 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
+ targetClass?: TargetClass;
400
+ /** 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
+ argClass?: ArgClass[];
364
402
  /** 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. */
365
403
  errorCode?: string;
366
404
  /** From the call's line to its first result's line; null when either timestamp is missing or no result is in the file. */
367
405
  durationMs: number | null;
368
406
  toolUseId: string | null;
369
407
  harnessVersion: string | null;
408
+ /** The model the call's own line names (`requestedModel`, else the message's model), so a step is attributed to the model that made it and not only to its prompt's majority (t867). Absent when the line names none and on a record captured before it. Class P, like the prompt's. */
409
+ model?: string;
410
+ /** The skill whose run the call's line sits in (`attributionSkill`): the harness stops naming it on the first line after the run, so that is where a run ends. Absent outside a run and on an earlier record. Class A, since a skill's name can be private. */
411
+ skill?: string;
370
412
  /** A sidechain line inside a main transcript. */
371
413
  side: boolean;
372
414
  /** The prompt index the call ran under; null before the first prompt. */
373
415
  prompt: number | null;
374
- /** What the call was in the canonical vocabulary; absent on a record captured before this field existed. Class P. */
416
+ /** What the call was in the canonical vocabulary; absent on a record captured before this field existed. Class A since CAPTURE 24: a step identity's skill-run name, host, and pull name are kept as they are after redaction and can be private; class P before. */
375
417
  steps?: StepObservation[];
376
418
  /** Each command of a shell call, in order (CAPTURE 6); on every shell call and no other. Absent on a record captured earlier. */
377
419
  segments?: ShellSegment[];
@@ -457,6 +499,14 @@ export interface UserPromptObservation {
457
499
  efforts: string[];
458
500
  skills: string[];
459
501
  usage: Usage;
502
+ /**
503
+ * Which `usage` categories at least one counted response reported (class P). A category not listed was never recorded by the source, so its `usage` number is a
504
+ * placeholder zero and not a measurement; a listed one is a sum over `usageResponses` responses, which a partial sum covers only in part. Absent on a record captured
505
+ * before version 20: every category reads as observed there.
506
+ */
507
+ usageObserved?: UsageField[];
508
+ /** How many responses reported any usage (class P); compare with `assistantTurns` or `turns` for the share a sum covers. Same absence rule as `usageObserved`. */
509
+ usageResponses?: number;
460
510
  /** Class A: the session's prompt fingerprint at the time of the prompt, when one had been seen. */
461
511
  promptFingerprint: string | null;
462
512
  /** Where and when this prompt ran; absent on a record captured before this field existed. Class A (EXTRACTOR 11): its fields pool only as cells. */
@@ -590,7 +640,7 @@ export interface RecallCall {
590
640
  /** 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`. */
591
641
  export interface HookCall {
592
642
  at: string;
593
- event: 'session-start' | 'pre-tool' | 'post-tool' | 'stop' | 'nightly';
643
+ event: 'session-start' | 'prompt' | 'pre-tool' | 'post-tool' | 'stop' | 'nightly';
594
644
  /** The Claude Code session the hook ran in; null for the nightly run. */
595
645
  session: string | null;
596
646
  /** Whether the hook had something to say (an injected block, a recall hit, or a session deposited). */
@@ -606,7 +656,7 @@ export interface HookCall {
606
656
  export declare const STEPS: readonly ["deposit", "verify", "derive", "label", "index", "recall", "read"];
607
657
  export type Step = (typeof STEPS)[number];
608
658
  /** The trigger names of spec 4.1 (the ones nothing local starts are in the schema only: `weekly` (A21) and the hosted `sync`, `cron`, `release`, `run-end`, and `run-start`). The SessionEnd hook is `stop`, never `session-end`. */
609
- export declare const TRIGGERS: readonly ["session-start", "pre-tool", "post-tool", "stop", "nightly", "manual", "weekly", "sync", "cron", "release", "run-end", "run-start"];
659
+ export declare const TRIGGERS: readonly ["session-start", "prompt", "pre-tool", "post-tool", "stop", "nightly", "manual", "weekly", "sync", "cron", "release", "run-end", "run-start"];
610
660
  export type Trigger = (typeof TRIGGERS)[number];
611
661
  /** `deferred`: a Stop deposit that found the store locked and recorded its session in the pending list. It is neither a success nor a failed run. */
612
662
  export declare const STEP_STATUSES: readonly ["ok", "error", "skipped", "deferred"];
@@ -30,6 +30,16 @@
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 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
+ //
35
+ // 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
36
+ // path) from `git checkout feature/x` (a branch or a path). It also writes a Codex `turn_aborted` with reason `interrupted` as the interrupt marker user line, so that prompt record exists and the session's
37
+ // `prompts`, `tsLast` (the marker is a line), and skeleton move; a Codex session with no such turn is unchanged. A `git checkout` step captured before reads as it did (no `target`). EXTRACTOR 15 reads a Codex or Gemini decline in its fixed text as `user-stop`.
38
+ //
39
+ // Version 22 (CAPTURE 22, t870) adds the optional `targetClass` and `argClass` to a tool call (`call-shape.ts`): the class of its path target (`source`, `test`, `config`, `doc`, `lockfile`, `generated`, `data`, `none`, `other`) and the sorted set
40
+ // of the classes of its arguments (`path`, `url`, `pattern`, `command`, `number`, `flag`, `text`, `none`), each computed from the path and the input at capture and stored as a closed-list member, never the path or a value. It changes no value
41
+ // captured before, so EXTRACTOR stays 12; a version-21 store's next deposit captures every transcript again, and a call of an older record has neither field.
42
+ //
33
43
  // EXTRACTOR 12 gives a derived prompt the strata of its user-prompt record, so its own `explore` count reaches the pool's wire as a bucket. No transcript is read: a store at
34
44
  // 11 re-derives from its log alone.
35
45
  //
@@ -78,11 +88,11 @@
78
88
  // 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
79
89
  // organization domains the operator names. The skeleton holds hashes only.
80
90
  /** 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. */
81
- export const CAPTURE_VERSION = '17';
91
+ export const CAPTURE_VERSION = '25';
82
92
  /** 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. */
83
93
  export const OLDEST_READABLE_CAPTURE = 3;
84
94
  /** Stamped `v` on every derived record. A bump re-derives from the log; no transcript is read. */
85
- export const EXTRACTOR_VERSION = '12';
95
+ export const EXTRACTOR_VERSION = '15';
86
96
  export const PROMPT_HEAD_CHARS = 300;
87
97
  export const ERROR_EXAMPLE_CHARS = 160;
88
98
  /** The reference scripts cut a normalized signature at 110 characters. */
@@ -91,6 +101,11 @@ export const SIGNATURE_CHARS = 110;
91
101
  export const ERROR_LINE_CHARS = 1000;
92
102
  /** Per-session cap on `files` and `fileHashes` (L3): the hand run on the copy store had p99 61 and max 209 files per session; 100 is the round number above p99. */
93
103
  export const FILES_PER_SESSION_MAX = 100;
104
+ /**
105
+ * What the next human prompt did to a prompt's work (t882, `docs/sessions-store.md` "Prompt intervention"): a closed list, not a grade. `user-stop` (EXTRACTOR 14) is a prompt the human stopped: a rejected call, or an interrupt. `unknown` is a prompt the records cannot say anything about (the last of its
106
+ * transcript, or one whose next prompt ran a shell call captured before `segments`); a prompt derived before EXTRACTOR 13 has no value at all, which is a different thing, and one derived at 13 read a stop as `unknown`.
107
+ */
108
+ export const PROMPT_INTERVENTIONS = ['none', 'after-error', 'redo', 'revert', 'user-stop', 'unknown'];
94
109
  export const PROMPT_CLASSES = ['no-tools', 'orchestration', 'research', 'edit-verify', 'edit-only', 'explore-only', 'shell-only', 'mixed'];
95
110
  /** The statuses agents recover from differently (pool vocabulary spec Section 7); any other status is `other`. */
96
111
  export const HTTP_STATUSES = ['400', '401', '403', '404', '408', '409', '413', '429', '500', '502', '503', '504', '529'];
@@ -100,6 +115,10 @@ export const EXIT_CLASSES = ['0', '1', '2', '126', '127', 'signal'];
100
115
  export const HARNESS_ERROR_CLASSES = ['user-rejected', 'interrupted', 'permission-denied', 'input-validation', 'tool-use-error', 'timeout'];
101
116
  /** The `errorCode` of a failed call (class P): a member of this set, never text from the result. `other` covers an exit status or HTTP status outside the lists, and a failure that names no class. */
102
117
  export const ERROR_CODES = new Set([...EXIT_CLASSES, ...HTTP_STATUSES, ...HARNESS_ERROR_CLASSES, 'other']);
118
+ /** The five usage categories, in the order a record lists the ones it observed. */
119
+ export const USAGE_FIELDS = ['input', 'cacheWrite', 'cacheRead', 'output', 'thinking'];
120
+ /** Whether a stored session or prompt recorded `field`. A record with no `usageObserved` was captured before version 20, when every category read as observed. */
121
+ export const observesUsage = (record, field) => record.usageObserved === undefined || record.usageObserved.includes(field);
103
122
  /** The harness a session record came from; a record that does not name one is Claude Code's. */
104
123
  export const harnessOf = (session) => session.harness ?? 'claude-code';
105
124
  /** A language's share of the files a transcript touched (pool vocabulary spec Section 7): none, incidental, secondary, primary. The lower edge is inclusive, so 10 percent is secondary and 50 is primary. */
@@ -109,7 +128,7 @@ export const REPO_FILE_BUCKETS = ['under-100', '100-1k', '1k-10k', '10k-100k', '
109
128
  /** The pipeline steps the log names (runtime-commons pipeline spec 3.1); `label` is the local labeler step (A5) that no command runs yet; `index` is the hook-index write the nightly and manual runs log. */
110
129
  export const STEPS = ['deposit', 'verify', 'derive', 'label', 'index', 'recall', 'read'];
111
130
  /** The trigger names of spec 4.1 (the ones nothing local starts are in the schema only: `weekly` (A21) and the hosted `sync`, `cron`, `release`, `run-end`, and `run-start`). The SessionEnd hook is `stop`, never `session-end`. */
112
- export const TRIGGERS = ['session-start', 'pre-tool', 'post-tool', 'stop', 'nightly', 'manual', 'weekly', 'sync', 'cron', 'release', 'run-end', 'run-start'];
131
+ export const TRIGGERS = ['session-start', 'prompt', 'pre-tool', 'post-tool', 'stop', 'nightly', 'manual', 'weekly', 'sync', 'cron', 'release', 'run-end', 'run-start'];
113
132
  /** `deferred`: a Stop deposit that found the store locked and recorded its session in the pending list. It is neither a success nor a failed run. */
114
133
  export const STEP_STATUSES = ['ok', 'error', 'skipped', 'deferred'];
115
134
  const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -4,12 +4,15 @@ type Obj = {
4
4
  };
5
5
  /** The harness's tool-rejection results, both worded for the operator declining: a classifier or hook denial is policy, and the model carries on. */
6
6
  export declare const REJECTED_RE: RegExp;
7
- /** The harness writes this as a user line when the operator cut a turn short; a rejection is followed by it too, so it is not a second interrupt. */
7
+ /** The harness writes this as a user line when the operator cut a turn short; a rejection is followed by it too, so it is not a second interrupt. The Codex reader writes it for a `turn_aborted` with reason `interrupted` (CAPTURE 23). */
8
8
  export declare const INTERRUPT_MARKER = "[Request interrupted by user";
9
+ /**
10
+ * A decline in the fixed text Codex or Gemini CLI writes for a call the operator refused: Codex's `exec command rejected by user` and `patch rejected by user` (events.rs:456-457), and Gemini's `[Operation Cancelled]`
11
+ * followed by a user cancel or denial. The text is the whole match, never a guess from other wording; `errorCodeOf` classes the same lines `user-rejected`, and the `user-stop` bucket reads them (EXTRACTOR 15).
12
+ */
13
+ export declare const DECLINED_RE: RegExp;
9
14
  /** An adapter's neutral line naming tools: `offered` and `loaded` are arrays of tool names. */
10
15
  export declare const SHAPE_TOOLS_LINE = "oars-tools";
11
- /** An adapter's neutral line for a turn the operator cut short, for a harness whose record is not a Claude marker line. */
12
- export declare const SHAPE_INTERRUPT_LINE = "oars-interrupt";
13
16
  export type TrustEventKind = 'interrupt' | 'rejection' | 'denial';
14
17
  /** One stop, with the tool in flight (null when none is known) and the prompt it fell in (null before the first). */
15
18
  export interface TrustEvent {
@@ -4,14 +4,19 @@
4
4
  // here is projected or uploaded.
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
- // two neutral lines an adapter may write for a harness whose records are not Claude-shaped (`oars-tools`, `oars-interrupt`).
7
+ // one neutral line an adapter may write for a harness whose records are not Claude-shaped (`oars-tools`).
8
8
  const isObj = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
9
9
  const str = (value) => (typeof value === 'string' ? value : undefined);
10
10
  const names = (value) => (Array.isArray(value) ? value.filter((item) => typeof item === 'string' && item !== '') : []);
11
11
  /** The harness's tool-rejection results, both worded for the operator declining: a classifier or hook denial is policy, and the model carries on. */
12
12
  export const REJECTED_RE = /^The user doesn't want to (?:proceed with this tool use|take this action)/;
13
- /** The harness writes this as a user line when the operator cut a turn short; a rejection is followed by it too, so it is not a second interrupt. */
13
+ /** The harness writes this as a user line when the operator cut a turn short; a rejection is followed by it too, so it is not a second interrupt. The Codex reader writes it for a `turn_aborted` with reason `interrupted` (CAPTURE 23). */
14
14
  export const INTERRUPT_MARKER = '[Request interrupted by user';
15
+ /**
16
+ * A decline in the fixed text Codex or Gemini CLI writes for a call the operator refused: Codex's `exec command rejected by user` and `patch rejected by user` (events.rs:456-457), and Gemini's `[Operation Cancelled]`
17
+ * followed by a user cancel or denial. The text is the whole match, never a guess from other wording; `errorCodeOf` classes the same lines `user-rejected`, and the `user-stop` bucket reads them (EXTRACTOR 15).
18
+ */
19
+ export const DECLINED_RE = /^(?:(?:exec command|patch) rejected by user\s*$|\[Operation Cancelled\] (?:Reason: )?(?:User (?:cancelled|denied)|Operation cancelled by user))/;
15
20
  /** A policy refusal: the permission rules or the auto-mode classifier (`Permission ... has been denied`), or a hook (`PreToolUse:Bash hook error`). Not the human's. */
16
21
  const DENIED_RE = /^(?:Permission (?:to use .{1,200}?|for this action) has been denied|[A-Za-z]+:[^\s]+ hook error|Hook [A-Za-z]+:[^\s]+ denied)/;
17
22
  /** A plugin's MCP server names its tools `mcp__plugin_<plugin>_<server>__<tool>`; that is the only evidence of an installed plugin read here.
@@ -21,8 +26,6 @@ const PLUGIN_TOOL_RE = /^mcp__plugin_([A-Za-z0-9-]+)_/;
21
26
  const STOP_HOOK_RE = /^Stop(?::|$)/;
22
27
  /** An adapter's neutral line naming tools: `offered` and `loaded` are arrays of tool names. */
23
28
  export const SHAPE_TOOLS_LINE = 'oars-tools';
24
- /** An adapter's neutral line for a turn the operator cut short, for a harness whose record is not a Claude marker line. */
25
- export const SHAPE_INTERRUPT_LINE = 'oars-interrupt';
26
29
  /** The permission modes each harness documents; any other value is stored as `other` (spec row `permissionMode`). Keyed by harness so a new one must add its entry. */
27
30
  const PERMISSION_MODES = {
28
31
  'claude-code': new Set(['default', 'acceptEdits', 'plan', 'auto', 'dontAsk', 'bypassPermissions']),
@@ -35,6 +38,12 @@ const PERMISSION_MODES = {
35
38
  cursor: new Set(),
36
39
  // Grok Build's reader reads no mode (its `current_mode_update` carries one, but this reader does not read it), so no value is documented to keep.
37
40
  grok: new Set(),
41
+ // OpenCode's reader reads no permission mode (a session records the agent, not a mode), so no value is documented to keep.
42
+ opencode: new Set(),
43
+ // Pi has no permission modes: its tools run as the user, and a session records none.
44
+ pi: new Set(),
45
+ // A trace carries no permission mode.
46
+ otel: new Set(),
38
47
  };
39
48
  /**
40
49
  * Which trust counts a harness's transcript records completely. A zero is stored only where the harness would have shown the event; where it would not,
@@ -52,6 +61,12 @@ const COMPLETE = {
52
61
  cursor: { interrupts: false, rejections: false, denials: false, timing: false },
53
62
  // Grok Build's reader writes the prompts and replies with their line times (seconds), and reads no cancel, tool refusal, or hook denial.
54
63
  grok: { interrupts: false, rejections: false, denials: false, timing: true },
64
+ // OpenCode's reader writes each message with its own time (so the runs are real) and reads no interrupt, refusal, or denial: a refused or aborted tool is an error state with a message.
65
+ opencode: { interrupts: false, rejections: false, denials: false, timing: true },
66
+ // Pi's reader writes the prompts and the responses with their entry times, and reads no interrupt, refusal, or denial: an aborted call is a tool error and no more.
67
+ pi: { interrupts: false, rejections: false, denials: false, timing: true },
68
+ // A trace's spans carry their own start and end times; it records no interrupt, refusal, or denial count.
69
+ otel: { interrupts: false, rejections: false, denials: false, timing: true },
55
70
  };
56
71
  const firstText = (content) => {
57
72
  if (typeof content === 'string')
@@ -217,8 +232,6 @@ export const createShape = (harness, stored, options) => {
217
232
  offer(names(line.offered));
218
233
  load(names(line.loaded));
219
234
  }
220
- else if (type === SHAPE_INTERRUPT_LINE)
221
- interrupt(inFlight(), at.prompt);
222
235
  else if (type === 'attachment') {
223
236
  if (isObj(line.attachment))
224
237
  attachment(line.attachment);
@@ -7,6 +7,9 @@ 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';
11
+ import { openOtelStore, otelRoot } from './otel.js';
12
+ import { opencodeRoot, openOpencodeStore } from './opencode.js';
10
13
  import { openVscodeCopilotStore, vscodeAppData } from './vscode-copilot.js';
11
14
  /** A session handed back to its own store was listed by it, so a miss is a bug in the caller. */
12
15
  const need = (value) => {
@@ -132,6 +135,54 @@ const grok = {
132
135
  };
133
136
  },
134
137
  };
138
+ const opencode = {
139
+ name: 'opencode',
140
+ harness: 'opencode',
141
+ label: 'OpenCode',
142
+ root: (_platform, env, home) => opencodeRoot(env, home),
143
+ storeFile: (root) => join(root, 'opencode.db'),
144
+ open: async (root) => {
145
+ const store = await openOpencodeStore(root);
146
+ if (store === undefined)
147
+ return undefined;
148
+ const byId = new Map(store.sessions.map((session) => [session.id, session]));
149
+ return {
150
+ sessions: store.sessions.map((session) => ({
151
+ id: session.id,
152
+ folder: session.folder,
153
+ size: session.size,
154
+ mtimeMs: session.mtimeMs,
155
+ ...(session.parent === null ? {} : { subagent: { parent: `opencode-${session.parent}`, agentType: session.agent, spawnedBy: null } }),
156
+ })),
157
+ transcript: (session) => store.transcript(need(byId.get(session.id))),
158
+ rejected: store.rejected,
159
+ unreadable: [],
160
+ where: () => join(root, 'opencode.db'),
161
+ close: store.close,
162
+ };
163
+ },
164
+ };
165
+ const pi = {
166
+ name: 'pi',
167
+ harness: 'pi',
168
+ label: 'Pi',
169
+ root: (_platform, env, home) => piRoot(env, home),
170
+ storeFile: (root) => join(root, 'sessions'),
171
+ open: async (root) => {
172
+ const store = await openPiStore(root);
173
+ if (store === undefined)
174
+ return undefined;
175
+ const byId = new Map(store.sessions.map((session) => [session.id, session]));
176
+ return {
177
+ sessions: store.sessions.map((session) => ({ id: session.id, folder: session.folder, size: session.size, mtimeMs: session.mtimeMs })),
178
+ transcript: async (session) => await store.transcript(need(byId.get(session.id))),
179
+ rejected: store.rejected,
180
+ unreadable: store.unreadable,
181
+ where: (session) => byId.get(session.id)?.file ?? root,
182
+ close: () => undefined,
183
+ };
184
+ },
185
+ };
135
186
  const vscodeCopilot = {
136
187
  name: 'vscode-copilot',
137
188
  harness: 'vscode-copilot',
@@ -153,7 +204,30 @@ const vscodeCopilot = {
153
204
  };
154
205
  },
155
206
  };
207
+ // A folder of OTLP JSON trace files (`$OARS_OTEL_TRACES`, else the config folder's `otel-traces`): a configured folder rather than an `import` command, so the cursor, the
208
+ // allowlist, the nightly run, and the fail-open reads are the ones every other source has.
209
+ const otel = {
210
+ name: 'otel',
211
+ harness: 'otel',
212
+ label: 'OpenTelemetry GenAI',
213
+ root: (platform, env, home) => otelRoot(env, home, platform),
214
+ storeFile: (root) => root,
215
+ open: async (root) => {
216
+ const store = await openOtelStore(root);
217
+ if (store === undefined)
218
+ return undefined;
219
+ const byId = new Map(store.sessions.map((session) => [session.id, session]));
220
+ 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))),
223
+ rejected: store.rejected,
224
+ unreadable: store.unreadable,
225
+ where: (session) => byId.get(session.id)?.file ?? root,
226
+ close: () => undefined,
227
+ };
228
+ },
229
+ };
156
230
  /** Every non-Claude source, in the order the deposit reads them. */
157
- export const SOURCES = [cursor, codex, copilot, gemini, grok, vscodeCopilot];
231
+ export const SOURCES = [cursor, codex, copilot, gemini, grok, opencode, pi, vscodeCopilot, otel];
158
232
  /** How a source is named in a deposit line; a name the table does not know (a test's) is its own label. */
159
233
  export const sourceLabel = (name, table = SOURCES) => table.find((source) => source.name === name)?.label ?? name;
@@ -13,7 +13,7 @@ export interface SessionsRuntime {
13
13
  projectsRoot: string;
14
14
  /** Cursor's `User` folder (`cursorRoot` in cursor.ts), kept as the alias of `sourceRoots.cursor`; absent means no Cursor source. */
15
15
  cursorRoot?: string;
16
- /** Where each non-Claude source's store lives, by `Source.name` (sources.ts); a source with no root is not read. `deposit` reads one only when `--allow` names it. */
16
+ /** Where each non-Claude source's store lives, by `Source.name` (sources.ts); a source with no root is not read; one with a root is read unless the allowlist names other entries only or denies it. */
17
17
  sourceRoots?: {
18
18
  [source: string]: string;
19
19
  };
@@ -52,7 +52,17 @@ export interface State {
52
52
  redactDomains: string[];
53
53
  /** Every `--allow` entry any deposit used (folders, prefixes, globs): the SessionEnd hook deposits a session only from a folder these name. */
54
54
  allow: string[];
55
+ /** What the person chose about sending to a pool: the raw-copy consent and the folders switched down. Never read by capture. */
56
+ pool: PoolChoices;
55
57
  }
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). */
59
+ export interface PoolChoices {
60
+ rawConsent: 'granted' | 'declined' | null;
61
+ folders: {
62
+ [entry: string]: FolderSend;
63
+ };
64
+ }
65
+ export type FolderSend = 'skeleton' | 'off';
56
66
  export declare const paths: (storeDir: string) => {
57
67
  observations: string;
58
68
  state: string;
@@ -77,6 +87,8 @@ export declare const sidOf: (salt: string) => string;
77
87
  */
78
88
  export declare const localSalt: (storeDir: string) => Promise<AccountSalt>;
79
89
  export declare const readState: (storeDir: string) => Promise<State>;
90
+ /** Changes the pool choices under the store lock, so a deposit running at the same time keeps them (it writes the state it read). */
91
+ export declare const updatePoolChoices: (storeDir: string, change: (choices: PoolChoices) => void) => Promise<PoolChoices>;
80
92
  export declare const readRedactDomains: (storeDir: string) => Promise<string[]>;
81
93
  /** The `--allow` entries and `--redact-domain` set earlier deposits used; the nightly run repeats them. */
82
94
  export declare const readSaved: (storeDir: string) => Promise<{
@@ -4,7 +4,7 @@ import { performance } from 'node:perf_hooks';
4
4
  import { appendFile, mkdir, open, readdir, readFile, rename, rm, stat, utimes, writeFile } from 'node:fs/promises';
5
5
  import { basename, dirname, join, resolve } from 'node:path';
6
6
  import { CliError, ExitCode } from '../errors.js';
7
- import { compileAllow, requireEntries, slugOf } from './allow.js';
7
+ import { compileAllow, mergeAllow, runAllow, slugOf } from './allow.js';
8
8
  import { SOURCES } from './sources.js';
9
9
  import { buildIndex, indexPath } from './hook-index.js';
10
10
  import { archiveRaw } from './archive.js';
@@ -74,15 +74,30 @@ export const readState = async (storeDir) => {
74
74
  }
75
75
  const domains = Array.isArray(parsed.redactDomains) ? parsed.redactDomains.filter((entry) => typeof entry === 'string') : [];
76
76
  const allow = Array.isArray(parsed.allow) ? parsed.allow.filter((entry) => typeof entry === 'string') : [];
77
- return { files, redactDomains: domains, allow };
77
+ const pool = isRecord(parsed.pool) ? parsed.pool : {};
78
+ const folders = {};
79
+ if (isRecord(pool.folders))
80
+ for (const [entry, mode] of Object.entries(pool.folders))
81
+ if (mode === 'skeleton' || mode === 'off')
82
+ folders[entry] = mode;
83
+ return { files, redactDomains: domains, allow, pool: { rawConsent: pool.rawConsent === 'granted' || pool.rawConsent === 'declined' ? pool.rawConsent : null, folders } };
78
84
  }
79
85
  }
80
86
  catch (error) {
81
87
  if (!isMissing(error) && !(error instanceof SyntaxError))
82
88
  throw error;
83
89
  }
84
- return { files: {}, redactDomains: [], allow: [] };
90
+ return { files: {}, redactDomains: [], allow: [], pool: { rawConsent: null, folders: {} } };
85
91
  };
92
+ /** Changes the pool choices under the store lock, so a deposit running at the same time keeps them (it writes the state it read). */
93
+ export const updatePoolChoices = (storeDir, change) => withLock(storeDir, async (held) => {
94
+ const state = await readState(storeDir);
95
+ change(state.pool);
96
+ if (!(await held()))
97
+ throw new StoreLockLostError();
98
+ await writeAtomic(paths(storeDir).state, JSON.stringify(state));
99
+ return state.pool;
100
+ });
86
101
  export const readRedactDomains = async (storeDir) => (await readState(storeDir)).redactDomains;
87
102
  /** The `--allow` entries and `--redact-domain` set earlier deposits used; the nightly run repeats them. */
88
103
  export const readSaved = async (storeDir) => {
@@ -385,14 +400,17 @@ export const refresh = async (runtime, redactDomains, at, trace, held = () => Pr
385
400
  }
386
401
  // The pool is derived for the index's fit reads only: one that cannot be read or written costs the index those, never the index itself (`currentPool` says so on stderr). Loaded here, not at the top: the pool imports every read family, and a lookup hook never reaches this step. It runs before the clock starts, so the index's `ms` is the index and not the derive (the first pool run takes 12 to 17 s on a store copy).
387
402
  // A module that will not load costs the index the same fit reads, with one line on stderr, as `currentPool` does when the pool cannot be built.
388
- const reads = await import('./pool.js').then(async ({ currentPool }) => (await currentPool(runtime.storeDir, store))?.reads ?? [], (error) => {
403
+ const current = await import('./pool.js').then(async ({ currentPool }) => currentPool(runtime.storeDir, store), (error) => {
389
404
  process.stderr.write(`oars: the read pool module could not be loaded, and the index keeps no fit reads: ${error instanceof Error ? error.message : String(error)}
390
405
  `);
391
- return [];
406
+ return undefined;
392
407
  });
408
+ const reads = current?.reads ?? [];
393
409
  const indexStarted = performance.now();
394
410
  try {
395
- const index = buildIndex(store, redactDomains, at, reads);
411
+ // The next-node reads for each harness and version, derived over the same pool; a deposit that has no pool leaves the index without them.
412
+ const next = current === undefined ? undefined : (await import('./recall-frames.js')).nextByFrame(current.pool, store);
413
+ const index = buildIndex(store, redactDomains, at, reads, next);
396
414
  await requireHold(held);
397
415
  await writeAtomic(indexPath(runtime.storeDir), JSON.stringify(index));
398
416
  if (trace !== undefined) {
@@ -529,32 +547,32 @@ export const withLock = async (storeDir, work, options = { waitMs: 10_000, stale
529
547
  }
530
548
  };
531
549
  export const deposit = (runtime, options, trace) => withLock(runtime.storeDir, (holds) => depositLocked(runtime, options, holds, trace));
532
- /** Writes `state.json` only while this run holds the lock; a run that lost it throws before the write, so the holder that took over keeps its entries. */
550
+ /** Throws `StoreLockLostError` when this run's token no longer stands. */
533
551
  const requireHold = async (held) => {
534
552
  if (!(await held()))
535
553
  throw new StoreLockLostError();
536
554
  };
555
+ /** Writes `state.json` only while this run holds the lock; a run that lost it throws before the write, so the holder that took over keeps its entries. */
537
556
  const writeStateHeld = async (runtime, state, held) => {
538
- if (!(await held()))
539
- throw new StoreLockLostError();
557
+ await requireHold(held);
540
558
  await writeAtomic(paths(runtime.storeDir).state, JSON.stringify(state));
541
559
  };
542
560
  const depositLocked = async (runtime, options, holds, trace) => {
543
- requireEntries(options.allow);
544
- const allow = compileAllow(options.allow);
561
+ const state = await readState(runtime.storeDir);
562
+ const entries = runAllow(state.allow, options.allow);
563
+ const allow = compileAllow(entries);
545
564
  const folders = await projectFolders(runtime.projectsRoot);
546
565
  const allowed = folders.filter((name) => allow.allows(name));
547
566
  const roots = sourceRoots(runtime);
548
567
  const table = sourcesOf(runtime);
549
568
  const missing = allow.unmatched([...folders, ...table.filter((source) => roots.has(source.name)).map((source) => source.name)]);
550
- const state = await readState(runtime.storeDir);
551
569
  refuseNewer(state);
552
570
  // A log written by a newer oars is refused even when state.json is older or lost; the Stop-hook path reads only state.json.
553
571
  const held = (await scanLog(paths(runtime.storeDir).observations)).newerCaptures;
554
572
  if (held > 0)
555
573
  throw new CliError(`The log holds captures by a newer oars (sessions: ${String(held)}); upgrade oars.`, ExitCode.UNEXPECTED, 'unexpected');
556
574
  state.redactDomains = [...new Set([...state.redactDomains, ...options.redactDomains])];
557
- state.allow = [...new Set([...state.allow, ...options.allow])];
575
+ state.allow = mergeAllow(state.allow, options.allow);
558
576
  const context = await contextFor(runtime, state, options.now, holds);
559
577
  const result = {
560
578
  sessions: 0,
@@ -871,7 +889,8 @@ const rawOnlyTranscripts = async (context, project, onDisk) => {
871
889
  // Listed from raw/ itself, so a lost state.json (no entries to name the files) still finds them; a file with no entry counts as behind.
872
890
  const behind = (await rawRelatives(root, [])).filter((relative) => {
873
891
  const entry = context.state.files[`${prefix}${relative}`];
874
- return !onDisk.has(`${prefix}${relative}`) && (entry === undefined || entry.v !== CAPTURE_VERSION || entry.s !== context.salt.sid);
892
+ // A domain named after the capture changes what the redactor masks, so a file recorded under another domain set is behind too.
893
+ return !onDisk.has(`${prefix}${relative}`) && (entry === undefined || entry.v !== CAPTURE_VERSION || entry.s !== context.salt.sid || !sameSet(entry.d, context.domains));
875
894
  });
876
895
  const found = [];
877
896
  for (const relative of behind) {
@@ -1214,6 +1233,8 @@ const writeLive = async (file, temp, latest) => {
1214
1233
  */
1215
1234
  export const compactLog = async (storeDir, renameWaitMs = LOG_RENAME_WAIT_MS, held = () => Promise.resolve(true)) => {
1216
1235
  const file = paths(storeDir).observations;
1236
+ // A run that lost the lock must not sweep a temp file the later holder is writing.
1237
+ await requireHold(held);
1217
1238
  // Under the lock no other compaction is running, so any temp file here is one a crash left.
1218
1239
  for (const entry of await listDir(storeDir)) {
1219
1240
  if (entry.name.startsWith(`${basename(file)}.`) && entry.name.endsWith('.tmp'))
@@ -1230,8 +1251,7 @@ export const compactLog = async (storeDir, renameWaitMs = LOG_RENAME_WAIT_MS, he
1230
1251
  if ((await stat(file)).size !== size)
1231
1252
  throw new Error('the log changed while it was being rewritten');
1232
1253
  // The size check cannot see a run that took a broken lock and has not appended yet; its token can.
1233
- if (!(await held()))
1234
- throw new StoreLockLostError();
1254
+ await requireHold(held);
1235
1255
  await renameWhenFree(temp, file, renameWaitMs);
1236
1256
  }
1237
1257
  finally {
@@ -15,6 +15,8 @@ export type SurvivalCounts = {
15
15
  /** Runs one read-only git command in a repository and answers its stdout; rejects on any failure. */
16
16
  export type Git = (root: string, args: readonly string[], stdin?: string) => Promise<string>;
17
17
  export declare const survivalFile: (storeDir: string) => string;
18
+ export declare const runGit: Git;
19
+ export declare const cwdOfProject: (runtime: Pick<SessionsRuntime, "projectsRoot" | "storeDir">, project: string, sessions: ReadonlySet<string>) => Promise<string | undefined>;
18
20
  /** The latest state of each sha. The file only adds to what `books` shows, so a missing or unreadable one reads as nothing observed yet. */
19
21
  export declare const readSurvival: (storeDir: string) => Promise<Map<string, SurvivalState>>;
20
22
  /**