bareloop 0.28.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/types/replay.d.ts CHANGED
@@ -1,3 +1,41 @@
1
+ /**
2
+ * `[startTs, endTs]` (epoch ms; `endTs` possibly `Infinity`) bounding the
3
+ * gate-audit rows that actually belong to ONE run's own spine. A sidecar
4
+ * file can be SHARED across several runs whose spines happen to reuse the
5
+ * same filename (measured directly on a real archived patient —
6
+ * pulselog-person-live-2, run mu2p83go: its sidecar carries rows from 7
7
+ * distinct `run_id`s spanning 06:56Z..13:25Z on one day, but this run's own
8
+ * `job-start`..`job-end` window is 13:19:40Z..13:25:48Z; every row before
9
+ * that window belongs to an earlier, unrelated run sharing the sidecar
10
+ * path). No `step`/`run_id` field on a gate-audit row is a reliable enough
11
+ * key on its own (see this file's header) — bounding by `ts` against this
12
+ * run's own `job-start`/`job-end` is the only honest scope.
13
+ *
14
+ * `startTs` is this run's own `job-start.ts` (`-Infinity` when absent —
15
+ * never a guessed lower bound). `endTs` is `job-end.ts` when a `job-end`
16
+ * was actually reached, else `Infinity` (a died/still-running run — never
17
+ * clamped to the spine's own last record's `ts`, which would wrongly
18
+ * exclude a real tool call the gate logged just AFTER the spine's own last
19
+ * record landed: spine and gate-audit are written by different seams and
20
+ * are not guaranteed to interleave exactly).
21
+ *
22
+ * ONE shared owner for this rule (F195): `replayRun` (this file) uses it to
23
+ * scope every audit row it hands to `runBehaviour`, so `bareloop replay`
24
+ * and every caller of `replayRun`/`replayOne` (including the panel,
25
+ * `src/panel/server.js`) read the same, honest figure — never two places
26
+ * independently re-deriving (and risking drifting from) the same rule.
27
+ * @param {{ts?: string}|null} [jobStart]
28
+ * @param {{ts?: string}|null} [jobEnd]
29
+ * @returns {{startTs: number, endTs: number}}
30
+ */
31
+ export function auditWindow(jobStart?: {
32
+ ts?: string;
33
+ } | null, jobEnd?: {
34
+ ts?: string;
35
+ } | null): {
36
+ startTs: number;
37
+ endTs: number;
38
+ };
1
39
  /**
2
40
  * Reconstruct one run's whole story from its spine + gate-audit. Pure: no
3
41
  * IO, no spine writes, no verdict. Malformed/non-object entries in either
@@ -18,16 +56,26 @@
18
56
  *
19
57
  * @param {any[]} spineEvents parsed records from `u-<id>.jsonl`
20
58
  * @param {any[]} [auditEvents] parsed records from the sibling `-gate-audit.jsonl`
21
- * @param {{runId?: string|null}} [opts] `runId`: caller-supplied (the spine
22
- * carries no run-id field of its own — see the file header)
59
+ * @param {{runId?: string|null, auditAvailable?: boolean}} [opts] `runId`:
60
+ * caller-supplied (the spine carries no run-id field of its own — see the
61
+ * file header). `auditAvailable` (default `true`, back-compat: every
62
+ * direct caller in this codebase that hands a literal `[]` here means a
63
+ * REAL, known-empty audit, not "unknown") — set `false` only when the
64
+ * caller could not even determine whether a gate-audit sidecar exists (no
65
+ * file found on disk, or deliberately not read). When `false`, every
66
+ * tool-call figure this function derives (`behaviour`, each occurrence's
67
+ * `toolCalls`) reads `null` (unknown), never `0` — doctrine: unknown is
68
+ * reported as unknown, never rendered as zero.
23
69
  */
24
- export function replayRun(spineEvents: any[], auditEvents?: any[], { runId }?: {
70
+ export function replayRun(spineEvents: any[], auditEvents?: any[], { runId, auditAvailable }?: {
25
71
  runId?: string | null;
72
+ auditAvailable?: boolean;
26
73
  }): {
27
74
  runId: string | null;
28
75
  job: any;
29
76
  verdictType: any;
30
77
  model: any;
78
+ provider: any;
31
79
  code: {
32
80
  version: any;
33
81
  sha: any;
@@ -71,14 +119,51 @@ export function replayRun(spineEvents: any[], auditEvents?: any[], { runId }?: {
71
119
  diffUsd: number;
72
120
  } | null;
73
121
  timelineKind: "steps" | "iterations";
122
+ scoutPlan: {
123
+ rounds: number;
124
+ toolCalls: number | null;
125
+ wallMs: number | null;
126
+ spentUsd: number | null;
127
+ unpricedRounds: number;
128
+ tripped: {
129
+ category: any;
130
+ detail: string | null;
131
+ } | null;
132
+ transportRetries: {
133
+ count: number;
134
+ label: string;
135
+ error: string | null;
136
+ } | null;
137
+ scoutRounds: number;
138
+ planRounds: number;
139
+ } | null;
74
140
  steps: {
75
141
  checks: {
76
142
  passed: number;
77
143
  failed: number;
78
144
  };
79
145
  treeChanged: boolean;
146
+ attempts: {
147
+ n: number;
148
+ iteration: number | null;
149
+ outcome: "green" | "red";
150
+ }[];
151
+ attemptWindows: {
152
+ n: number;
153
+ iteration: number | null;
154
+ outcome: "green" | "red";
155
+ startSeq: number;
156
+ endSeq: number;
157
+ startTs: number | null;
158
+ endTs: number | null;
159
+ exitEvalDetail: string | null;
160
+ }[];
161
+ stepStartSeq: any;
162
+ stepEndSeq: any;
163
+ stepStartTs: number | null;
164
+ stepEndTs: number | null;
80
165
  rounds: number;
81
- toolCalls: number;
166
+ toolCalls: number | null;
82
167
  wallMs: number | null;
83
168
  spentUsd: number | null;
84
169
  unpricedRounds: number;
@@ -96,8 +181,12 @@ export function replayRun(spineEvents: any[], auditEvents?: any[], { runId }?: {
96
181
  outcome: any;
97
182
  }[];
98
183
  iterations: {
184
+ windowStartSeq: any;
185
+ windowEndSeq: any;
186
+ windowStartTs: number | null;
187
+ windowEndTs: number | null;
99
188
  rounds: number;
100
- toolCalls: number;
189
+ toolCalls: number | null;
101
190
  wallMs: number | null;
102
191
  spentUsd: number | null;
103
192
  unpricedRounds: number;
@@ -118,6 +207,15 @@ export function replayRun(spineEvents: any[], auditEvents?: any[], { runId }?: {
118
207
  stages: any;
119
208
  } | null;
120
209
  }[];
210
+ fixLoop: {
211
+ attempts: any[];
212
+ rounds: any;
213
+ toolCalls: any;
214
+ wallMs: any;
215
+ spentUsd: any;
216
+ unpricedRounds: any;
217
+ } | null;
218
+ parts: any[];
121
219
  replans: any;
122
220
  close: {
123
221
  source: "close-verdict" | "outer-close";
@@ -137,7 +235,7 @@ export function replayRun(spineEvents: any[], auditEvents?: any[], { runId }?: {
137
235
  uniquePaths: number;
138
236
  repeats: number;
139
237
  repeatPct: number | null;
140
- };
238
+ } | null;
141
239
  memoryCache: any;
142
240
  skipped: number;
143
241
  };
@@ -0,0 +1,112 @@
1
+ /**
2
+ * `~/.config/bareloop` (or the injected override) — the one home directory
3
+ * for both the keys file (PRD §7d, not this module's concern) and the run
4
+ * list.
5
+ * @param {string} [override] test seam; a caller in production never passes it
6
+ * @returns {string}
7
+ */
8
+ export function runlistHome(override?: string): string;
9
+ /**
10
+ * `<home>/runs.jsonl`.
11
+ * @param {string} [home]
12
+ * @returns {string}
13
+ */
14
+ export function runlistPath(home?: string): string;
15
+ /**
16
+ * @typedef {{ at: string, runid: string, job: string, spine: string, patient: string|null, via: 'run-u'|'bundle'|'backfill' }} RunRow
17
+ */
18
+ /**
19
+ * Append one row to the run list, mkdir -p'ing the home directory first.
20
+ * IDEMPOTENT BY RUNID: a row already carrying `row.runid` is never
21
+ * duplicated (a resume, or a re-run of `backfill`, must not double-list one
22
+ * run) — the file is read once first to check.
23
+ *
24
+ * Perms: the home directory is created `0700` and the file kept `0600`
25
+ * (chmod'd after every append, in case it pre-existed with wider perms) —
26
+ * the same "outside any repo, keys-file-adjacent" posture PRD §7d states for
27
+ * `.env` in this same directory, even though this file carries no secret
28
+ * itself.
29
+ *
30
+ * THROWS on any IO failure (permission denied, disk full, …) — the caller
31
+ * (a live run) is required to catch this and continue the run rather than
32
+ * let a list-append failure abort paid work (hamr's rule: a panel list must
33
+ * never block real work). This function itself does not swallow, so a
34
+ * direct caller (e.g. `backfillRuns`, a $0 path) sees the real error.
35
+ * @param {RunRow} row
36
+ * @param {{ home?: string }} [opts]
37
+ * @returns {{ appended: boolean, reason?: 'duplicate-runid' }}
38
+ */
39
+ export function appendRun(row: RunRow, opts?: {
40
+ home?: string;
41
+ }): {
42
+ appended: boolean;
43
+ reason?: "duplicate-runid";
44
+ };
45
+ /**
46
+ * Tolerant reader — reuses {@link parseJsonl} (never a second hand-rolled
47
+ * parser). An absent file reads as an empty list, not an error: a fresh
48
+ * install has never run `appendRun` yet.
49
+ * @param {{ home?: string }} [opts]
50
+ * @returns {{ rows: RunRow[], skipped: number }}
51
+ */
52
+ export function readRunList(opts?: {
53
+ home?: string;
54
+ }): {
55
+ rows: RunRow[];
56
+ skipped: number;
57
+ };
58
+ /**
59
+ * `bareloop runs backfill <dir>` — recursively scan `dir` (bounded to
60
+ * {@link MAX_BACKFILL_DEPTH} levels, never into `node_modules`/`.git`,
61
+ * never following a symlinked directory) for archived spine files — both
62
+ * the free-standing layout (e.g. `<dir>/<x>/u-<id>.jsonl`, at any depth) and
63
+ * the bundle layout (`<dir>/.../runs/<runid>/spine.jsonl`) — and add one row
64
+ * per spine whose RESOLVED ABSOLUTE PATH is not already in the list.
65
+ *
66
+ * F197: dedup is by spine path, never by `runidForSpine`'s own derived
67
+ * runid. A spine carrying no `runid` field falls back to a filename-derived
68
+ * id (e.g. `run` for a `run.jsonl` with no runid inside) — a SECOND such
69
+ * file elsewhere (same basename, different directory: not a rare shape,
70
+ * `bareloop-patients/quickstart-proof/run.jsonl` is a real example) is a
71
+ * DIFFERENT run, not a duplicate, and dedup-by-runid would have silently
72
+ * dropped it. When the derived runid collides with one already in the list
73
+ * (pre-existing or added earlier in this same pass), the row's OWN runid is
74
+ * disambiguated (`<base>~2`, `<base>~3`, …) so every row keeps a runid the
75
+ * panel's `/api/runs/:runid` lookup can resolve unambiguously — see
76
+ * {@link RUNID_RE} in `src/panel/server.js`, updated to allow `~`.
77
+ *
78
+ * IDEMPOTENT: running twice adds nothing the second time (the resolved-path
79
+ * set already contains every spine this pass would otherwise re-add). Never
80
+ * silent about what it found: every candidate `.jsonl` is bucketed into
81
+ * exactly one of added / already listed / skipped (not a spine, or
82
+ * unreadable).
83
+ * @param {string} dir
84
+ * @param {{ home?: string }} [opts]
85
+ * @returns {{ added: number, alreadyListed: number, skipped: number, addedRows: RunRow[] }}
86
+ */
87
+ export function backfillRuns(dir: string, opts?: {
88
+ home?: string;
89
+ }): {
90
+ added: number;
91
+ alreadyListed: number;
92
+ skipped: number;
93
+ addedRows: RunRow[];
94
+ };
95
+ /**
96
+ * One printable line per row for `bareloop runs` (no subcommand): `job
97
+ * (runid) · date · spine path`, with `file missing` appended when the row's
98
+ * `spine` no longer exists on disk (an archive moved/pruned since the row
99
+ * was written — never silently listed as if it were still there).
100
+ * @param {RunRow} row
101
+ * @returns {string}
102
+ */
103
+ export function formatRunRow(row: RunRow): string;
104
+ export const MAX_BACKFILL_DEPTH: 6;
105
+ export type RunRow = {
106
+ at: string;
107
+ runid: string;
108
+ job: string;
109
+ spine: string;
110
+ patient: string | null;
111
+ via: "run-u" | "bundle" | "backfill";
112
+ };
@@ -52,6 +52,13 @@ export type Deps = {
52
52
  provider?: any;
53
53
  providerFor?: ((tier: string) => any) | undefined;
54
54
  judgeProvider?: any;
55
+ /**
56
+ * F196 — a test seam for `appendRun`'s
57
+ * (src/runlist.js) `home` override, the same shape `deps.provider` already
58
+ * is. Production never sets it; `appendRun` falls through to its own
59
+ * `os.homedir()`-based default.
60
+ */
61
+ runlistHome?: string | undefined;
55
62
  };
56
63
  export type RunOpts = {
57
64
  /**