@manyoars/oars 0.4.1 → 0.5.2
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.
- package/README.md +79 -29
- package/dist/bin.js +3 -1
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +207 -21
- package/dist/club.d.ts +19 -0
- package/dist/club.js +132 -0
- package/dist/gate/hook-fill-main.js +1 -1
- package/dist/gate/hook-fill.d.ts +1 -1
- package/dist/gate/hook-fill.js +1 -1
- package/dist/gate/hook-main.js +7 -5
- package/dist/gate/hook.d.ts +12 -1
- package/dist/gate/hook.js +38 -7
- package/dist/hook-bin.js +23 -4
- package/dist/main.js +5 -0
- package/dist/node-floor.d.ts +10 -0
- package/dist/node-floor.js +23 -0
- package/dist/oars-hook.d.ts +2 -0
- package/dist/oars-hook.js +7 -0
- package/dist/sessions/allow.d.ts +1 -0
- package/dist/sessions/allow.js +1 -1
- package/dist/sessions/call-shape.d.ts +111 -0
- package/dist/sessions/call-shape.js +464 -0
- package/dist/sessions/candidates.d.ts +65 -0
- package/dist/sessions/candidates.js +236 -0
- package/dist/sessions/card-command.d.ts +24 -0
- package/dist/sessions/card-command.js +71 -0
- package/dist/sessions/card-font.d.ts +6 -0
- package/dist/sessions/card-font.js +36 -0
- package/dist/sessions/card-line.d.ts +30 -0
- package/dist/sessions/card-line.js +105 -0
- package/dist/sessions/card-render.d.ts +15 -0
- package/dist/sessions/card-render.js +146 -0
- package/dist/sessions/card.d.ts +59 -0
- package/dist/sessions/card.js +107 -0
- package/dist/sessions/cell-increments.d.ts +14 -3
- package/dist/sessions/cell-increments.js +79 -12
- package/dist/sessions/cells.d.ts +42 -4
- package/dist/sessions/cells.js +72 -18
- package/dist/sessions/codex-hook-bin.d.ts +1 -0
- package/dist/sessions/codex-hook-bin.js +24 -0
- package/dist/sessions/codex-hook-install.d.ts +21 -0
- package/dist/sessions/codex-hook-install.js +47 -0
- package/dist/sessions/codex-hook-view.d.ts +20 -0
- package/dist/sessions/codex-hook-view.js +20 -0
- package/dist/sessions/codex-hook.d.ts +77 -0
- package/dist/sessions/codex-hook.js +255 -0
- package/dist/sessions/codex.d.ts +9 -0
- package/dist/sessions/codex.js +104 -18
- package/dist/sessions/command.d.ts +17 -2
- package/dist/sessions/command.js +145 -33
- package/dist/sessions/copilot.d.ts +3 -0
- package/dist/sessions/copilot.js +12 -4
- package/dist/sessions/derive.js +2 -1
- package/dist/sessions/empty-state.d.ts +17 -0
- package/dist/sessions/empty-state.js +38 -0
- package/dist/sessions/exclusion.js +48 -3
- package/dist/sessions/extract.d.ts +16 -3
- package/dist/sessions/extract.js +281 -24
- package/dist/sessions/gemini.d.ts +2 -0
- package/dist/sessions/gemini.js +29 -2
- package/dist/sessions/harness-files.d.ts +27 -0
- package/dist/sessions/harness-files.js +109 -0
- package/dist/sessions/head-only.d.ts +7 -0
- package/dist/sessions/head-only.js +21 -0
- package/dist/sessions/hook-calls.js +4 -3
- package/dist/sessions/hooks.d.ts +1 -1
- package/dist/sessions/hooks.js +6 -2
- package/dist/sessions/identity.d.ts +6 -0
- package/dist/sessions/injected.d.ts +13 -0
- package/dist/sessions/injected.js +260 -0
- package/dist/sessions/install.d.ts +31 -20
- package/dist/sessions/install.js +80 -82
- package/dist/sessions/load-status.d.ts +41 -0
- package/dist/sessions/load-status.js +156 -0
- package/dist/sessions/machine-child-deps.d.ts +5 -0
- package/dist/sessions/machine-child-deps.js +83 -0
- package/dist/sessions/machine-child.d.ts +43 -0
- package/dist/sessions/machine-child.js +120 -0
- package/dist/sessions/machine-credential.d.ts +10 -0
- package/dist/sessions/machine-credential.js +20 -0
- package/dist/sessions/machine-enroll.d.ts +47 -0
- package/dist/sessions/machine-enroll.js +230 -0
- package/dist/sessions/machine-hold.d.ts +49 -0
- package/dist/sessions/machine-hold.js +88 -0
- package/dist/sessions/machine-ids.d.ts +6 -0
- package/dist/sessions/machine-ids.js +9 -0
- package/dist/sessions/machine-notice.d.ts +21 -0
- package/dist/sessions/machine-notice.js +89 -0
- package/dist/sessions/machine-pull.d.ts +40 -0
- package/dist/sessions/machine-pull.js +220 -0
- package/dist/sessions/machine-repo.d.ts +7 -0
- package/dist/sessions/machine-repo.js +30 -0
- package/dist/sessions/machine-scan-cache.d.ts +12 -0
- package/dist/sessions/machine-scan-cache.js +47 -0
- package/dist/sessions/machine-send.d.ts +12 -0
- package/dist/sessions/machine-send.js +156 -0
- package/dist/sessions/machine-stop.d.ts +22 -0
- package/dist/sessions/machine-stop.js +53 -0
- package/dist/sessions/machine.d.ts +41 -0
- package/dist/sessions/machine.js +89 -0
- package/dist/sessions/opencode.d.ts +2 -0
- package/dist/sessions/opencode.js +6 -0
- package/dist/sessions/otel.d.ts +21 -0
- package/dist/sessions/otel.js +109 -38
- package/dist/sessions/owner-only.d.ts +7 -0
- package/dist/sessions/owner-only.js +34 -0
- package/dist/sessions/pi.d.ts +13 -1
- package/dist/sessions/pi.js +27 -13
- package/dist/sessions/pool-books.d.ts +1 -1
- package/dist/sessions/pool-books.js +1 -1
- package/dist/sessions/pool-choices.d.ts +4 -2
- package/dist/sessions/pool-choices.js +4 -2
- package/dist/sessions/pool-command.d.ts +46 -6
- package/dist/sessions/pool-command.js +613 -136
- package/dist/sessions/pool-delta.d.ts +7 -0
- package/dist/sessions/pool-delta.js +28 -5
- package/dist/sessions/pool-due.d.ts +43 -0
- package/dist/sessions/pool-due.js +113 -0
- package/dist/sessions/pool-nightly.d.ts +9 -1
- package/dist/sessions/pool-nightly.js +65 -21
- package/dist/sessions/pool-outbox.d.ts +51 -5
- package/dist/sessions/pool-outbox.js +217 -24
- package/dist/sessions/pool-print.d.ts +1 -1
- package/dist/sessions/pool-raw.js +4 -3
- package/dist/sessions/pool-replica.d.ts +4 -4
- package/dist/sessions/pool-replica.js +26 -7
- package/dist/sessions/pool-send-lock.js +1 -1
- package/dist/sessions/pool-sent.d.ts +2 -0
- package/dist/sessions/pool-sent.js +21 -4
- package/dist/sessions/pool-wire.d.ts +176 -15
- package/dist/sessions/pool-wire.js +297 -33
- package/dist/sessions/pool.js +4 -2
- package/dist/sessions/public-vocabulary.d.ts +5 -0
- package/dist/sessions/public-vocabulary.js +16 -0
- package/dist/sessions/read-call-shape.js +12 -6
- package/dist/sessions/read-fit.d.ts +5 -0
- package/dist/sessions/read-fit.js +23 -2
- package/dist/sessions/read-interaction.js +1 -1
- package/dist/sessions/read-lead-lag.js +1 -1
- package/dist/sessions/read-readable.js +1 -1
- package/dist/sessions/read.d.ts +1 -1
- package/dist/sessions/recall-consequence.d.ts +2 -2
- package/dist/sessions/recall-consequence.js +14 -9
- package/dist/sessions/recall-object.d.ts +9 -2
- package/dist/sessions/receipt.d.ts +8 -0
- package/dist/sessions/receipt.js +78 -0
- package/dist/sessions/recovery-calls.d.ts +3 -2
- package/dist/sessions/recovery-calls.js +9 -3
- package/dist/sessions/redact.d.ts +11 -2
- package/dist/sessions/redact.js +201 -23
- package/dist/sessions/repo-root.d.ts +7 -0
- package/dist/sessions/repo-root.js +17 -13
- package/dist/sessions/reversion.d.ts +8 -0
- package/dist/sessions/reversion.js +114 -0
- package/dist/sessions/schema.d.ts +78 -4
- package/dist/sessions/schema.js +31 -1
- package/dist/sessions/session-shape.d.ts +2 -1
- package/dist/sessions/session-shape.js +15 -0
- package/dist/sessions/sources.d.ts +9 -0
- package/dist/sessions/sources.js +16 -5
- package/dist/sessions/steps.js +3 -2
- package/dist/sessions/store.d.ts +41 -2
- package/dist/sessions/store.js +68 -16
- package/dist/sessions/try-demo.d.ts +8 -0
- package/dist/sessions/try-demo.js +90 -0
- package/dist/sessions/try.d.ts +13 -0
- package/dist/sessions/try.js +50 -0
- package/dist/sessions/vscode-copilot.d.ts +4 -0
- package/dist/sessions/vscode-copilot.js +25 -18
- package/dist/telemetry/command.js +12 -6
- package/dist/telemetry/consent.d.ts +9 -3
- package/dist/telemetry/consent.js +25 -9
- package/dist/telemetry/fields.d.ts +11 -0
- package/dist/telemetry/fields.js +56 -0
- package/dist/telemetry/measures.d.ts +13 -0
- package/dist/telemetry/measures.js +30 -0
- package/dist/telemetry/observe.d.ts +15 -2
- package/dist/telemetry/observe.js +34 -7
- package/dist/telemetry/region.d.ts +6 -0
- package/dist/telemetry/region.js +28 -0
- package/dist/telemetry/report.js +1 -1
- package/dist/telemetry/schema.d.ts +3 -0
- package/dist/telemetry/schema.js +11 -1
- package/dist/telemetry/send.d.ts +5 -2
- package/dist/telemetry/send.js +15 -7
- package/dist/telemetry/words.d.ts +6 -0
- package/dist/telemetry/words.js +6 -0
- package/package.json +24 -5
package/dist/sessions/schema.js
CHANGED
|
@@ -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 = '
|
|
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;
|
package/dist/sessions/sources.js
CHANGED
|
@@ -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,
|
|
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) =>
|
|
170
|
-
storeFile: (root) =>
|
|
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:
|
|
222
|
-
|
|
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,
|
package/dist/sessions/steps.js
CHANGED
|
@@ -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,
|
|
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
|
|
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) => {
|
package/dist/sessions/store.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
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
|
package/dist/sessions/store.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { derivedSalt } from './machine-ids.js';
|
|
1
2
|
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
2
3
|
import { createReadStream } from 'node:fs';
|
|
3
4
|
import { performance } from 'node:perf_hooks';
|
|
@@ -15,6 +16,7 @@ import { callPositions, captureSession } from './extract.js';
|
|
|
15
16
|
import { foldToolRows, withoutReplays } from './fold.js';
|
|
16
17
|
import { asRepoSize } from './public-vocabulary.js';
|
|
17
18
|
import { makeRepoIdentity, makeRepoRoot } from './repo-root.js';
|
|
19
|
+
import { mkdirOwnerOnly } from './owner-only.js';
|
|
18
20
|
import { makeRedactor } from './redact.js';
|
|
19
21
|
import { CAPTURE_VERSION, EXTRACTOR_VERSION, OLDEST_READABLE_CAPTURE, } from './schema.js';
|
|
20
22
|
const isMissing = (error) => error instanceof Error && 'code' in error && error.code === 'ENOENT';
|
|
@@ -25,6 +27,7 @@ export const paths = (storeDir) => ({
|
|
|
25
27
|
skeletons: join(storeDir, 'skeletons'),
|
|
26
28
|
calls: join(storeDir, 'mcp-calls.jsonl'),
|
|
27
29
|
hookCalls: join(storeDir, 'hook-calls.jsonl'),
|
|
30
|
+
hookRows: join(storeDir, 'hook-rows.jsonl'),
|
|
28
31
|
pipelineRuns: join(storeDir, 'pipeline-runs.jsonl'),
|
|
29
32
|
salt: join(storeDir, 'salt'),
|
|
30
33
|
raw: join(storeDir, 'raw'),
|
|
@@ -80,14 +83,16 @@ export const readState = async (storeDir) => {
|
|
|
80
83
|
for (const [entry, mode] of Object.entries(pool.folders))
|
|
81
84
|
if (mode === 'skeleton' || mode === 'off')
|
|
82
85
|
folders[entry] = mode;
|
|
83
|
-
|
|
86
|
+
const yesVersion = typeof pool.rawConsentVersion === 'string' && pool.rawConsentVersion !== '' ? pool.rawConsentVersion : null;
|
|
87
|
+
const granted = pool.rawConsent === 'granted' && yesVersion !== null;
|
|
88
|
+
return { files, redactDomains: domains, allow, pool: { rawConsent: granted ? 'granted' : pool.rawConsent === 'declined' ? 'declined' : null, rawConsentVersion: granted ? yesVersion : null, withdrawalPending: pool.withdrawalPending === true && !granted, folders } };
|
|
84
89
|
}
|
|
85
90
|
}
|
|
86
91
|
catch (error) {
|
|
87
92
|
if (!isMissing(error) && !(error instanceof SyntaxError))
|
|
88
93
|
throw error;
|
|
89
94
|
}
|
|
90
|
-
return { files: {}, redactDomains: [], allow: [], pool: { rawConsent: null, folders: {} } };
|
|
95
|
+
return { files: {}, redactDomains: [], allow: [], pool: { rawConsent: null, rawConsentVersion: null, withdrawalPending: false, folders: {} } };
|
|
91
96
|
};
|
|
92
97
|
/** 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
98
|
export const updatePoolChoices = (storeDir, change) => withLock(storeDir, async (held) => {
|
|
@@ -111,7 +116,7 @@ export const writeAtomic = async (file, text) => {
|
|
|
111
116
|
await rename(temp, file);
|
|
112
117
|
};
|
|
113
118
|
export { slugOf };
|
|
114
|
-
const projectFolders = async (projectsRoot) => {
|
|
119
|
+
export const projectFolders = async (projectsRoot) => {
|
|
115
120
|
try {
|
|
116
121
|
return (await readdir(projectsRoot, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
117
122
|
}
|
|
@@ -164,6 +169,7 @@ const contextFor = async (runtime, state, now, held) => {
|
|
|
164
169
|
domains: [...state.redactDomains].sort(),
|
|
165
170
|
redact: makeRedactor(state.redactDomains),
|
|
166
171
|
salt,
|
|
172
|
+
repoSalt: runtime.repoSalt ?? derivedSalt(salt.salt, 'repoKey/1'),
|
|
167
173
|
repoRoot: makeRepoRoot(),
|
|
168
174
|
repoIdentity: makeRepoIdentity(),
|
|
169
175
|
captureRuntime: captureRuntime(),
|
|
@@ -175,7 +181,7 @@ const contextFor = async (runtime, state, now, held) => {
|
|
|
175
181
|
now,
|
|
176
182
|
};
|
|
177
183
|
};
|
|
178
|
-
const mainTranscript = (folder, name) => ({ file: join(folder, name), session: name.slice(0, -'.jsonl'.length), relative: name });
|
|
184
|
+
export const mainTranscript = (folder, name) => ({ file: join(folder, name), session: name.slice(0, -'.jsonl'.length), relative: name });
|
|
179
185
|
export const listDir = async (dir) => {
|
|
180
186
|
try {
|
|
181
187
|
return (await readdir(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -221,7 +227,7 @@ export const subagentFiles = async (folder, sessions) => {
|
|
|
221
227
|
return found;
|
|
222
228
|
};
|
|
223
229
|
// The subagent transcripts of the given sessions, each with its `.meta.json` read.
|
|
224
|
-
const subagentTranscripts = async (folder, sessions) => {
|
|
230
|
+
export const subagentTranscripts = async (folder, sessions) => {
|
|
225
231
|
const found = [];
|
|
226
232
|
for (const { dir, path, parent, name } of await subagentFiles(folder, sessions)) {
|
|
227
233
|
const agent = name.slice(0, -'.jsonl'.length);
|
|
@@ -340,6 +346,7 @@ const depositFile = async (context, project, transcript) => {
|
|
|
340
346
|
bytes: info.size,
|
|
341
347
|
redact: context.redact,
|
|
342
348
|
salt: context.salt.salt,
|
|
349
|
+
repoSalt: context.repoSalt,
|
|
343
350
|
sid: context.salt.sid,
|
|
344
351
|
repoRoot: context.repoRoot,
|
|
345
352
|
repoIdentity: context.repoIdentity,
|
|
@@ -439,6 +446,24 @@ const commitCapture = async (context, key, extract, cursor) => {
|
|
|
439
446
|
await writeAtomic(join(files.skeletons, `${extract.session.session}.json`), JSON.stringify(extract.skeleton));
|
|
440
447
|
context.state.files[key] = { ...cursor, v: CAPTURE_VERSION, s: context.salt.sid, d: context.domains, repoSize: extract.session.frame?.stack.size ?? null };
|
|
441
448
|
};
|
|
449
|
+
/**
|
|
450
|
+
* Records that a session is superseded: its capture of no turns, marked with the session that holds them now, appended as the next rev (the log is never edited), and its
|
|
451
|
+
* skeleton removed, since a skeleton is derived and a reader of skeletons would count the session. The cursor records it, so a repeat deposit adds nothing. The nested agents of the
|
|
452
|
+
* session are read again, since their parent is now the merged session.
|
|
453
|
+
*/
|
|
454
|
+
const commitSupersede = async (context, key, extract, by, cursor) => {
|
|
455
|
+
const files = paths(context.runtime.storeDir);
|
|
456
|
+
extract.session.supersededBy = by;
|
|
457
|
+
await requireHold(context.held);
|
|
458
|
+
await appendFile(files.observations, `${JSON.stringify(extract.session)}\n`, { mode: 0o600 });
|
|
459
|
+
await requireHold(context.held);
|
|
460
|
+
await rm(join(files.skeletons, `${extract.session.session}.json`), { force: true });
|
|
461
|
+
context.state.files[key] = { ...cursor, v: CAPTURE_VERSION, s: context.salt.sid, d: context.domains, repoSize: null };
|
|
462
|
+
// A nested agent's id is `<trace>-<span>`, so it stays when its parent moves to the merged session, and its spans may not change: stale its cursor so the next read names the new parent.
|
|
463
|
+
for (const [other, entry] of Object.entries(context.state.files))
|
|
464
|
+
if (other.startsWith(`${key}-`))
|
|
465
|
+
entry.v = '';
|
|
466
|
+
};
|
|
442
467
|
/** Another deposit holds the store lock and the wait ran out: the one failure the Stop hook defers instead of counting as a failed run. */
|
|
443
468
|
export class StoreLockedError extends CliError {
|
|
444
469
|
constructor() {
|
|
@@ -496,13 +521,19 @@ const breakStale = async (lock, staleMs) => {
|
|
|
496
521
|
await rm(guard, { force: true });
|
|
497
522
|
}
|
|
498
523
|
};
|
|
524
|
+
/**
|
|
525
|
+
* What a command a person typed waits for the store lock (`deposit --deny`, the pool choices). The background sweep starts after any command and its deposit
|
|
526
|
+
* holds the lock for the whole run, which a full re-capture stretches past 10 minutes, so a short wait made `--deny` fail right when the hold window
|
|
527
|
+
* wants it to work; two minutes is the wait the send lock gives. The hook path keeps `HOOK_LOCK`: a hook must not block a session.
|
|
528
|
+
*/
|
|
529
|
+
const FOREGROUND_LOCK = { waitMs: 120_000, staleMs: 600_000 };
|
|
499
530
|
/**
|
|
500
531
|
* Runs `work` as the only writer of `state.json` and the observations file. The SessionEnd hooks of parallel sessions and the nightly
|
|
501
532
|
* run each read, extract, and rewrite `state.json`; without this one's `files` and `allow` entries are lost to another's write.
|
|
502
533
|
*/
|
|
503
|
-
export const withLock = async (storeDir, work, options =
|
|
534
|
+
export const withLock = async (storeDir, work, options = FOREGROUND_LOCK) => {
|
|
504
535
|
const lock = join(storeDir, 'store.lock');
|
|
505
|
-
await
|
|
536
|
+
await mkdirOwnerOnly(storeDir);
|
|
506
537
|
const started = Date.now();
|
|
507
538
|
// The token names this claim, so a holder whose lock was broken cannot delete the next holder's lock when it ends.
|
|
508
539
|
const token = `${String(process.pid)} ${randomUUID()}`;
|
|
@@ -654,11 +685,11 @@ const depositLocked = async (runtime, options, holds, trace) => {
|
|
|
654
685
|
return result;
|
|
655
686
|
};
|
|
656
687
|
/** The table in force: the runtime's own (a test's), else every source. */
|
|
657
|
-
const sourcesOf = (runtime) => runtime.sources ?? SOURCES;
|
|
688
|
+
export const sourcesOf = (runtime) => runtime.sources ?? SOURCES;
|
|
658
689
|
/** A source's name is the first `state.json` key segment, and no project folder uses it, so the Claude Code walk skips it. */
|
|
659
690
|
const isSource = (table, name) => table.some((source) => source.name === name);
|
|
660
691
|
/** The roots the runtime names, by source name; `cursorRoot` stays the alias of the Cursor entry. */
|
|
661
|
-
const sourceRoots = (runtime) => new Map(Object.entries({ ...(runtime.cursorRoot === undefined ? {} : { cursor: runtime.cursorRoot }), ...runtime.sourceRoots }));
|
|
692
|
+
export const sourceRoots = (runtime) => new Map(Object.entries({ ...(runtime.cursorRoot === undefined ? {} : { cursor: runtime.cursorRoot }), ...runtime.sourceRoots }));
|
|
662
693
|
/** Reports an unreadable source store or file, once per reason: the full path for the terminal, the base name for the hook-call record. */
|
|
663
694
|
const noteReason = (counts, file, reason) => {
|
|
664
695
|
const line = `${file}: ${reason}`;
|
|
@@ -696,16 +727,23 @@ const readSource = async (context, source, root, result, trace) => {
|
|
|
696
727
|
for (const entry of store.unreadable)
|
|
697
728
|
noteReason(counts, entry.file, entry.reason);
|
|
698
729
|
try {
|
|
699
|
-
|
|
730
|
+
// The superseded sessions first: committing one stales the cursors of its nested agents, which are then read in this same deposit.
|
|
731
|
+
const ordered = [...store.sessions.filter((session) => session.supersededBy !== undefined), ...store.sessions.filter((session) => session.supersededBy === undefined)];
|
|
732
|
+
for (const session of ordered) {
|
|
700
733
|
const key = `${source.name}/${session.id}`;
|
|
701
734
|
const previous = context.state.files[key];
|
|
735
|
+
// A session this store never held has nothing to supersede.
|
|
736
|
+
if (session.supersededBy !== undefined && previous === undefined)
|
|
737
|
+
continue;
|
|
702
738
|
if (previous !== undefined &&
|
|
703
739
|
previous.size === session.size &&
|
|
704
740
|
previous.mtimeMs === session.mtimeMs &&
|
|
705
741
|
previous.v === CAPTURE_VERSION &&
|
|
706
742
|
previous.s === context.salt.sid &&
|
|
707
743
|
sameSet(previous.d, context.domains)) {
|
|
708
|
-
|
|
744
|
+
// A superseded session is recorded once and is no file of its own, so it is not one more file left unchanged.
|
|
745
|
+
if (session.supersededBy === undefined)
|
|
746
|
+
result.unchanged += 1;
|
|
709
747
|
continue;
|
|
710
748
|
}
|
|
711
749
|
let text;
|
|
@@ -729,6 +767,7 @@ const readSource = async (context, source, root, result, trace) => {
|
|
|
729
767
|
bytes: Buffer.byteLength(text),
|
|
730
768
|
redact: context.redact,
|
|
731
769
|
salt: context.salt.salt,
|
|
770
|
+
repoSalt: context.repoSalt,
|
|
732
771
|
sid: context.salt.sid,
|
|
733
772
|
repoRoot: context.repoRoot,
|
|
734
773
|
repoIdentity: context.repoIdentity,
|
|
@@ -739,6 +778,10 @@ const readSource = async (context, source, root, result, trace) => {
|
|
|
739
778
|
});
|
|
740
779
|
// The input names the frame and the action table; the session record carries the harness for `harnessOf`, `books --by harness`, and `measure`.
|
|
741
780
|
extract.session.harness = source.harness;
|
|
781
|
+
if (session.supersededBy !== undefined) {
|
|
782
|
+
await commitSupersede(context, key, extract, `${source.name}-${session.supersededBy}`, { size: session.size, mtimeMs: session.mtimeMs, rev });
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
742
785
|
await commitCapture(context, key, extract, { size: session.size, mtimeMs: session.mtimeMs, rev });
|
|
743
786
|
tally(trace, extract, session.subagent !== undefined);
|
|
744
787
|
if (session.subagent === undefined) {
|
|
@@ -918,7 +961,7 @@ const rawRelatives = async (dir, path) => {
|
|
|
918
961
|
}
|
|
919
962
|
return found.sort();
|
|
920
963
|
};
|
|
921
|
-
const HOOK_LOCK = { waitMs:
|
|
964
|
+
export const HOOK_LOCK = { waitMs: 500, staleMs: 600_000 };
|
|
922
965
|
/**
|
|
923
966
|
* Deposits one transcript, as the SessionEnd hook does: only from a folder that earlier `--allow` entries name, and idempotent by size and
|
|
924
967
|
* mtime. It does not rebuild `derived.json` or the hook index (that is a whole-store pass; the nightly run does it), so it stays fast.
|
|
@@ -927,7 +970,7 @@ const HOOK_LOCK = { waitMs: 3_000, staleMs: 600_000 };
|
|
|
927
970
|
*/
|
|
928
971
|
export const depositSession = async (runtime, transcript, now, trace) => {
|
|
929
972
|
try {
|
|
930
|
-
// The hook path waits
|
|
973
|
+
// The hook path waits under a second for the lock, well under the harness's 30 s ceiling on the hook (install.ts), so a SessionEnd behind the sender's sweep cannot hold session exit; `deferSession` covers a lock still held.
|
|
931
974
|
return await withLock(runtime.storeDir, (held) => depositSessionLocked(runtime, transcript, now, held, trace), HOOK_LOCK);
|
|
932
975
|
}
|
|
933
976
|
catch (error) {
|
|
@@ -1091,12 +1134,20 @@ const internTool = (tool, intern) => ({
|
|
|
1091
1134
|
target: tool.target === null ? null : intern(tool.target),
|
|
1092
1135
|
...(tool.segments === undefined ? {} : { segments: tool.segments.map((segment) => ({ ...segment, run: intern(segment.run) })) }),
|
|
1093
1136
|
});
|
|
1137
|
+
const withoutMessageUsage = (session) => {
|
|
1138
|
+
if (session.messageUsage === undefined)
|
|
1139
|
+
return session;
|
|
1140
|
+
const rest = { ...session };
|
|
1141
|
+
Reflect.deleteProperty(rest, 'messageUsage');
|
|
1142
|
+
return rest;
|
|
1143
|
+
};
|
|
1094
1144
|
/**
|
|
1095
1145
|
* 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
|
|
1096
1146
|
* (EXTRACTOR_VERSION rules); a session whose last deposit was version 2 keeps the prompt and error lines that deposit wrote.
|
|
1097
1147
|
* `afterFirstPass` runs between the two passes over the log: a seam for the test that compacts the log there.
|
|
1148
|
+
* `options.messageUsage` keeps the per-message usage entries on each session record; without it they are dropped as the record is read.
|
|
1098
1149
|
*/
|
|
1099
|
-
export const loadStore = async (storeDir, afterFirstPass) => {
|
|
1150
|
+
export const loadStore = async (storeDir, afterFirstPass, options = {}) => {
|
|
1100
1151
|
// Two passes, so a superseded record is never held: the first keeps one number per session, the second keeps only the records at that rev.
|
|
1101
1152
|
// A re-capture leaves the old copies in the log until a compaction, and holding them as well doubled the heap of every read.
|
|
1102
1153
|
// Both passes read one open file: a lock-free reader can meet a compaction between them, and a second open would see the compacted log, where
|
|
@@ -1119,7 +1170,7 @@ export const loadStore = async (storeDir, afterFirstPass) => {
|
|
|
1119
1170
|
if (observation === undefined || observation.rev !== latest.get(observation.session))
|
|
1120
1171
|
continue;
|
|
1121
1172
|
if (observation.kind === 'session')
|
|
1122
|
-
of(observation.session).session = observation;
|
|
1173
|
+
of(observation.session).session = options.messageUsage === true ? observation : withoutMessageUsage(observation);
|
|
1123
1174
|
else if (observation.kind === 'user-prompt')
|
|
1124
1175
|
of(observation.session).prompts.push(observation);
|
|
1125
1176
|
else if (observation.kind === 'tool')
|
|
@@ -1138,7 +1189,8 @@ export const loadStore = async (storeDir, afterFirstPass) => {
|
|
|
1138
1189
|
const redact = makeRedactor((await readState(storeDir)).redactDomains);
|
|
1139
1190
|
const agents = [];
|
|
1140
1191
|
for (const { session: written, prompts, tools: writtenTools, errorLines: writtenErrorLines } of captured.values()) {
|
|
1141
|
-
|
|
1192
|
+
// A superseded session holds no turns: the session that folded it in holds them.
|
|
1193
|
+
if (written === undefined || written.supersededBy !== undefined)
|
|
1142
1194
|
continue;
|
|
1143
1195
|
// One tool call is one record: a replay an older capture wrote beside the call is folded here.
|
|
1144
1196
|
const folded = foldToolRows(writtenTools, writtenErrorLines);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The invented working directory the demo sessions claim; no such folder is read. Resolved, because a folder's name is its path's slug on this platform. */
|
|
2
|
+
export declare const DEMO_CWD: string;
|
|
3
|
+
/** The folder Claude Code would name for `DEMO_CWD` under `~/.claude/projects`. */
|
|
4
|
+
export declare const DEMO_FOLDER: string;
|
|
5
|
+
/** An error the three demo sessions all hit, as a stranger would paste it. */
|
|
6
|
+
export declare const DEMO_ERROR = "unexpected EOF while looking for matching quote";
|
|
7
|
+
/** Writes the demo sessions under `<projectsRoot>/<DEMO_FOLDER>/`, the layout Claude Code uses. */
|
|
8
|
+
export declare const writeDemoSessions: (projectsRoot: string) => Promise<void>;
|