@sweny-ai/core 0.1.177 → 0.1.179

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/dist/journal.d.ts CHANGED
@@ -2,23 +2,42 @@
2
2
  * Run journal (#363): an append-only log of one workflow run, enough to resume
3
3
  * a killed or failed run without re-running the nodes that already finished.
4
4
  *
5
- * File: `.sweny/runs/<run-id>/journal.ndjson` (mode 0600), one JSON record per
6
- * line. Every record carries the format version (`v`), a sequence number (`seq`)
7
- * and an authentication code (`h`, HMAC-SHA256 of the record without `h`, under
8
- * a per-run key). Each append is written and fsync'd before the executor moves
9
- * on, so a record on disk is a fact.
5
+ * Location: the journal never lives in the workspace. Each run gets a private
6
+ * directory in the user's sweny state dir (`$SWENY_STATE_DIR`, else
7
+ * `$XDG_STATE_HOME/sweny`, else `~/.local/state/sweny`):
10
8
  *
11
- * Integrity: the key is 32 random bytes made when the run starts and kept
12
- * OUTSIDE the workspace, in the user's sweny state dir
13
- * (`$SWENY_STATE_DIR`, else `$XDG_STATE_HOME/sweny`, else
14
- * `~/.local/state/sweny`), under `run-keys/` (dir 0700, file 0600). Anything
15
- * that can write the workspace but not that dir (an agent confined to the
16
- * workspace, a checked-out branch, an artifact) cannot mint a record resume
17
- * accepts. Both sandboxes deny reading the key dir; an unsandboxed agent
18
- * running as the same OS user can still read it, which is why resume also
19
- * checks the record sequence (see {@link buildResumePlan}) and every replayed
20
- * route against the workflow's real edges. A resume on another machine needs
21
- * the same state dir.
9
+ * runs/<workspace-hash>/<run-id>/ (dirs 0700, files 0600, opened O_NOFOLLOW)
10
+ * key 32 random bytes, the run's HMAC key
11
+ * journal.ndjson one JSON record per line, fsync'd per append
12
+ * head.json the record being appended, written (atomically) first
13
+ * meta.json run id, workspace hash, creation time; authenticated, drives retention
14
+ * lock the process that owns the run (pid + start time)
15
+ *
16
+ * Both agent sandboxes deny reading AND writing the whole `runs/` tree, so an
17
+ * agent confined to the workspace cannot read the key, edit or cut the
18
+ * journal, delete the lock or fake a run. The workspace keeps only user-facing
19
+ * artifacts (`.sweny/runs/<run-id>/output.md` and the run history record).
20
+ * Journals an older sweny wrote in the workspace are refused for resume.
21
+ *
22
+ * Integrity: every record carries the format version (`v`), a sequence number
23
+ * (`seq`) and `h`, an HMAC-SHA256 under the run key over the run id, the seq,
24
+ * the previous record's `h` and the record body. The chain means records
25
+ * cannot be reordered, duplicated, or spliced in from another run. The head
26
+ * file holds the full record being appended, written and renamed into place
27
+ * BEFORE the journal append: a crash between the two leaves a journal exactly
28
+ * one record short, and resume restores that record from the head. A journal
29
+ * shorter than that, or one that disagrees with the head, is refused, so the
30
+ * end of the journal cannot be cut away even by something that can write the
31
+ * state dir but not read the key.
32
+ *
33
+ * All of this holds only for an agent under an enforced sandbox. An
34
+ * unsandboxed agent running as the same OS user can read the key and rewrite
35
+ * the state dir. Each such visit is journaled (`agent:unsandboxed`), and a
36
+ * resume of that run warns and marks the receipt `journal_unsandboxed`.
37
+ * Resume also checks the record sequence (see {@link buildResumePlan}) and
38
+ * every replayed route against the workflow's real edges. A state dir whose
39
+ * real path is inside the workspace is refused. A resume on another machine
40
+ * needs the same state dir.
22
41
  *
23
42
  * Records, in run order:
24
43
  * run:start workflow / instruction / input / tool hashes, harness id, input (redacted)
@@ -29,18 +48,32 @@
29
48
  * output:intent a write is about to be applied (idempotency key + tool)
30
49
  * output:applied the write returned (key + the ids it produced)
31
50
  * node:end final result of the visit and the write-stage counters
32
- * route the edge taken after the visit (null = the run ended)
33
- * usage spend of one agent attempt (cumulative; live while it runs, final when it returns)
51
+ * route the edge taken after the visit (null = the run ended), and the rung that chose it
52
+ * usage spend of one agent attempt (cumulative; one per model turn that raised it, written
53
+ * synchronously, and the final figure when the attempt returns)
54
+ * agent:unsandboxed the visit's agent ran without an enforced sandbox
34
55
  * run:end success | failed | crashed
35
56
  *
57
+ * Failure: an append that cannot be written (head or journal) is fatal. The
58
+ * executor stops the run before the next model call or side effect and fails
59
+ * the node with the reason, so a resume never under-counts spend or repeats a
60
+ * write the journal did not see.
61
+ *
36
62
  * Spend: a resume seeds the run budget with every attempt's journaled usage,
37
- * so a crash never resets the whole-run ceiling.
63
+ * from authenticated records only, so a crash never resets the whole-run ceiling.
64
+ *
65
+ * Recovery: an unterminated tail that is not complete JSON (power loss
66
+ * mid-append) is truncated. An unterminated tail that IS complete JSON is
67
+ * never assumed torn: it must authenticate as the next record, or the journal
68
+ * is refused. Corruption in the middle of the file is refused.
38
69
  *
39
- * Recovery: a torn or corrupt tail (power loss mid-append) is truncated back to
40
- * the last valid record. Corruption in the middle of the file is refused.
70
+ * Lock: taken with O_CREAT|O_EXCL before anything is read or repaired, and
71
+ * held for the whole run or resume. A lock whose process is gone (pid not
72
+ * alive, or alive with a different start time) is taken over under a
73
+ * second exclusive lock, re-checked first, so two resumes cannot both win.
41
74
  *
42
- * Privacy: the journal is local and holds what the run needs to continue (node
43
- * data, eval verdicts, safe-output intents). It never holds environment values:
75
+ * Privacy: the journal holds what the run needs to continue (node data, eval
76
+ * verdicts, safe-output intents). It never holds environment values:
44
77
  * secret-looking keys, known token shapes, and the values of secret env vars
45
78
  * and skill credentials are replaced with `[redacted]` before anything is
46
79
  * written. Tool call inputs and outputs are not journaled.
@@ -52,14 +85,19 @@
52
85
  * (the process died between the API call and the receipt) is looked up on the
53
86
  * provider by a marker embedded in the body before anything is re-sent.
54
87
  */
55
- import type { Logger, NodeResult, NodeUsage, Skill, Workflow } from "./types.js";
88
+ import type { Logger, NodeResult, NodeUsage, RouteRung, Skill, Workflow } from "./types.js";
56
89
  import type { SafeOutputIntent, WriteStageState } from "./safe-outputs.js";
57
90
  import { type Spend } from "./budget.js";
58
- /** v2: records are authenticated with a per-run HMAC key (v1 used a public checksum and is refused). */
59
- export declare const JOURNAL_SCHEMA_VERSION = 2;
60
- export declare const JOURNAL_DIR: string;
91
+ import type { DeciderCounters } from "./decider.js";
92
+ /**
93
+ * v3: each record's HMAC chains the run id and the previous record's HMAC.
94
+ * (v2 authenticated records one by one; v1 used a public checksum. Both are refused.)
95
+ */
96
+ export declare const JOURNAL_SCHEMA_VERSION = 3;
97
+ /** Where journals lived before v3: in the workspace. Never read for resume, only recognized to refuse. */
98
+ export declare const LEGACY_JOURNAL_DIR: string;
61
99
  export declare const JOURNAL_FILE = "journal.ndjson";
62
- /** Journals kept on disk; the oldest beyond this are deleted when a new run starts. */
100
+ /** Journals kept per workspace; the oldest beyond this are deleted when a new run starts. */
63
101
  export declare const JOURNAL_KEEP = 20;
64
102
  export declare const REDACTED = "[redacted]";
65
103
  /** sha256 (hex) of `value` as canonical JSON: keys sorted, undefined dropped. Same as run history's workflow hash. */
@@ -87,16 +125,69 @@ export declare function instructionHash(sources: Record<string, {
87
125
  export declare function toolsHash(skills: Map<string, Skill>, swenyVersion?: string, harnessId?: string, secrets?: string[]): string;
88
126
  /** sweny's per-user state dir: `$SWENY_STATE_DIR`, else `$XDG_STATE_HOME/sweny`, else `~/.local/state/sweny`. */
89
127
  export declare function swenyStateDir(env?: Record<string, string | undefined>): string;
90
- /** Where run keys live. Never inside the workspace. */
91
- export declare function runKeyDir(env?: Record<string, string | undefined>): string;
92
- /** The key file for one run in one workspace (the workspace path is hashed, not stored). */
93
- export declare function runKeyFile(cwd: string, runId: string, keyDir?: string): string;
128
+ /** Where every run journal (and its key, head, meta and lock) lives. Agents can neither read nor write it. */
129
+ export declare function runStateRoot(env?: Record<string, string | undefined>): string;
130
+ /** The workspace's directory name under {@link runStateRoot}: its real path hashed, never stored. */
131
+ export declare function workspaceScope(cwd: string): string;
132
+ /** One run's private directory: `<state>/runs/<workspace-hash>/<run-id>/`. */
133
+ export declare function journalDir(cwd: string, runId: string, root?: string): string;
134
+ /** The run's key file (inside its private directory). */
135
+ export declare function runKeyFile(cwd: string, runId: string, root?: string): string;
136
+ /** Thrown when the journal's state dir resolves into the workspace, where an agent could edit it. */
137
+ export declare class JournalLocationError extends Error {
138
+ constructor(dir: string, cwd: string);
139
+ }
140
+ /**
141
+ * Refuse a state dir that resolves into the workspace. Real paths are compared
142
+ * (the dir must exist), so a symlink anywhere in either chain counts as where
143
+ * it points. Throws {@link JournalLocationError}.
144
+ */
145
+ export declare function assertStateOutsideWorkspace(dir: string, cwd: string): void;
94
146
  /** Thrown when a journal's key is missing or unreadable: its records cannot be checked. */
95
147
  export declare class JournalKeyError extends Error {
96
148
  constructor(file: string, why: string);
97
149
  }
98
150
  /** Read a run key. Throws {@link JournalKeyError}. */
99
151
  export declare function loadRunKey(file: string): Buffer;
152
+ /** Thrown when the journal is shorter than, or disagrees with, the record its head file holds. */
153
+ export declare class JournalRollbackError extends Error {
154
+ constructor(message: string);
155
+ }
156
+ /**
157
+ * The record being appended (seq 0 and no record before the first append).
158
+ * Written atomically BEFORE the record goes to the journal, so the journal is
159
+ * either at the head or exactly one record short of it (a crash in between),
160
+ * never anything else.
161
+ */
162
+ export interface JournalHead {
163
+ seq: number;
164
+ record: JournalRecord | null;
165
+ }
166
+ /** Read and authenticate a run's head file. Throws {@link JournalRollbackError}. */
167
+ export declare function loadJournalHead(dir: string, key: Buffer, runId: string): JournalHead;
168
+ /** Thrown when another live process holds the run. */
169
+ export declare class JournalLockedError extends Error {
170
+ readonly pid: number;
171
+ constructor(pid: number);
172
+ }
173
+ /** Start time (ms since epoch) of a process, where the OS exposes it cheaply (Linux); else undefined. */
174
+ export declare function processStartTime(pid: number): number | undefined;
175
+ /** Test seam for the lock: called after a stale lock was read, before it is taken over. */
176
+ export interface LockHooks {
177
+ afterStaleRead?(): void;
178
+ }
179
+ /** An acquired run lock. */
180
+ export interface RunLock {
181
+ readonly file: string;
182
+ release(): void;
183
+ }
184
+ /**
185
+ * Take a run's lock: O_CREAT|O_EXCL in the run's private dir. A lock whose
186
+ * process is gone is taken over under a second exclusive lock, and only if it
187
+ * still holds the exact bytes judged stale, so two processes racing for a
188
+ * stale lock cannot both win. Throws {@link JournalLockedError}.
189
+ */
190
+ export declare function acquireRunLock(dir: string, hooks?: LockHooks): RunLock;
100
191
  /** True for keys like `GITHUB_TOKEN`, `apiKey`, `client-secret`, `Authorization`. */
101
192
  export declare function isSecretKey(key: string): boolean;
102
193
  /** Values of secret-named env vars / config entries, longest first (8+ chars, so short words never match). */
@@ -139,6 +230,10 @@ export interface RunStartRecord extends JournalRecord {
139
230
  sweny_version?: string;
140
231
  input: unknown;
141
232
  input_redacted: boolean;
233
+ decider?: {
234
+ mode: "on" | "off";
235
+ reason?: string;
236
+ };
142
237
  }
143
238
  export interface WriteStateSnapshot {
144
239
  counts: Array<[string, number]>;
@@ -180,15 +275,20 @@ export declare class JournalMismatchError extends Error {
180
275
  readonly what: string[];
181
276
  constructor(what: string[]);
182
277
  }
183
- /** Thrown when another live process holds the run's journal. */
184
- export declare class JournalLockedError extends Error {
185
- constructor(pid: number);
278
+ /** Thrown when the journal cannot be written. Fatal: the executor stops the run before anything else happens. */
279
+ export declare class JournalWriteError extends Error {
280
+ constructor(message: string);
186
281
  }
187
282
  export interface JournalRead {
188
283
  file: string;
284
+ /** Authenticated, chained records only: everything a resume replays or seeds spend from. */
189
285
  records: JournalRecord[];
190
- /** Bytes dropped from a torn / corrupt tail (0 when the file was clean). */
286
+ /** Bytes dropped from a torn tail (0 when the file was clean). */
191
287
  truncatedBytes: number;
288
+ /** The last record is whole and authenticated but lost its line end (repair restores it). */
289
+ missingNewline?: boolean;
290
+ /** The last record was missing from the journal (a crash after the head write) and was restored from the head. */
291
+ restored?: boolean;
192
292
  /**
193
293
  * Set when an invalid record sits before valid ones (not a torn tail), or when
194
294
  * a whole record fails authentication (tampering, never a torn write). Not repairable.
@@ -197,18 +297,31 @@ export interface JournalRead {
197
297
  /** The line whose record failed authentication, when that is why the journal is refused. */
198
298
  forgedAtLine?: number;
199
299
  }
200
- /**
201
- * Read a journal. With `repair`, a torn tail is truncated on disk back to the
202
- * last valid record. Records are authenticated with the run's key (`key`, else
203
- * loaded from the state dir for the run the path names). Throws
204
- * {@link JournalVersionError} for another format and {@link JournalKeyError}
205
- * when the key is missing.
206
- */
207
- export declare function readJournal(file: string, opts?: {
300
+ export interface ReadJournalOptions {
301
+ /** Truncate a torn tail on disk, restore a lost final line end, and re-append a record restored from the head. */
208
302
  repair?: boolean;
303
+ /** The run key; default: the `key` file next to the journal. */
209
304
  key?: Buffer;
210
- keyDir?: string;
211
- }): JournalRead;
305
+ /** The run id the records are chained to; default: the journal's directory name. */
306
+ runId?: string;
307
+ /**
308
+ * The run's head; default: the authenticated `head.json` next to the journal.
309
+ * `false` skips the length check: only for inspecting raw bytes, never on a
310
+ * path that resumes.
311
+ */
312
+ head?: JournalHead | false;
313
+ }
314
+ /**
315
+ * Read a journal from a run's private directory. Records are authenticated
316
+ * and chained with the run key, then checked against the head: the journal
317
+ * must end at the head's record, or one short of it (then that record is
318
+ * restored from the head). Only an unterminated final segment that is not
319
+ * complete JSON is a torn append; with `repair` it is cut. Throws
320
+ * {@link JournalVersionError} for another format, {@link JournalKeyError}
321
+ * when the key is missing and {@link JournalRollbackError} when the journal
322
+ * was cut back or does not match its head.
323
+ */
324
+ export declare function readJournal(file: string, opts?: ReadJournalOptions): JournalRead;
212
325
  export type VisitAction = "replay" | "write-stage" | "rerun";
213
326
  export interface PlannedVisit {
214
327
  node: string;
@@ -218,6 +331,8 @@ export interface PlannedVisit {
218
331
  status?: NodeResult["status"];
219
332
  /** Route taken after the visit: a node id, null for run end, undefined when not journaled. */
220
333
  next?: string | null;
334
+ /** Who chose that route, when it was a decision (#357). */
335
+ rung?: RouteRung;
221
336
  /** Writes the checkpointed write stage already applied (receipts in the journal). */
222
337
  applied: number;
223
338
  /** Writes with an intent but no receipt: checked on the provider before anything is re-sent. */
@@ -243,6 +358,14 @@ export interface ResumePlan {
243
358
  receipts: Map<string, OutputRecord>;
244
359
  /** Spend every earlier attempt journaled: the run budget starts here on resume. */
245
360
  priorSpend: Spend;
361
+ /**
362
+ * Nodes whose agent ran without an enforced sandbox in an earlier attempt.
363
+ * Such an agent could read the run key, so the journal's spend and write
364
+ * records are only as trustworthy as that agent.
365
+ */
366
+ unsandboxed: string[];
367
+ /** The decider's mode at run start and its counters after the last journaled route (#357). */
368
+ decider?: JournalDeciderState;
246
369
  }
247
370
  /**
248
371
  * Check that the records are a sequence the executor can write: one run:start
@@ -281,6 +404,21 @@ export interface JournalBeginInfo {
281
404
  config: Record<string, string>;
282
405
  writeState: WriteStageState;
283
406
  harnessId?: string;
407
+ /** Whether the run's decider ran (#357), journaled so a resume keeps the same mode. */
408
+ decider?: {
409
+ mode: "on" | "off";
410
+ reason?: string;
411
+ };
412
+ }
413
+ /**
414
+ * The decider as an earlier attempt left it (#357): its mode at run start and
415
+ * the breaker / cap counters after the last journaled route. Cap and breaker
416
+ * are per logical run, so a resume continues them.
417
+ */
418
+ export interface JournalDeciderState {
419
+ mode: "on" | "off";
420
+ reason?: string;
421
+ counters?: DeciderCounters;
284
422
  }
285
423
  export interface JournalCheckpoint {
286
424
  result: NodeResult;
@@ -292,12 +430,14 @@ export type JournalReplay = {
292
430
  kind: "complete";
293
431
  result: NodeResult;
294
432
  next?: string | null;
433
+ rung?: RouteRung;
295
434
  } | ({
296
435
  kind: "checkpoint";
297
436
  } & JournalCheckpoint);
298
437
  /**
299
- * The executor's view of a journal (`ExecuteOptions.journal`). Every hook is
300
- * best effort except at resume, where `begin` refuses a changed run.
438
+ * The executor's view of a journal (`ExecuteOptions.journal`). A hook that
439
+ * cannot write its record throws {@link JournalWriteError}: the executor
440
+ * stops the run there. At resume, `begin` refuses a changed run.
301
441
  */
302
442
  export interface ExecutionJournal {
303
443
  begin(info: JournalBeginInfo): void;
@@ -308,24 +448,34 @@ export interface ExecutionJournal {
308
448
  /** The skill map the write stage applies through: write tools journaled and made idempotent. */
309
449
  wrapWrites(node: string, iteration: number, skills: Map<string, Skill>): Map<string, Skill>;
310
450
  nodeEnd(node: string, iteration: number, result: NodeResult, writeState: WriteStageState): void;
311
- route(from: string, to: string | null): void;
451
+ /** The edge taken after a visit, and for a decision the rung that chose it (#357), so resume replays it. */
452
+ route(from: string, to: string | null, rung?: RouteRung, decider?: DeciderCounters): void;
312
453
  /**
313
454
  * Spend of one agent attempt, cumulative for the attempt: `final` when the
314
- * attempt returned, otherwise a live report (the journal may throttle those).
455
+ * attempt returned, otherwise a live report (written at once when it raises the spend).
315
456
  */
316
457
  usage?(node: string, iteration: number, attempt: number, usage: NodeUsage, final: boolean): void;
458
+ /** The visit's agent ran without an enforced sandbox: recorded so a resume can say so. */
459
+ uncontained?(node: string, iteration: number): void;
317
460
  /** Spend earlier attempts of this run already journaled (resume); the run budget starts from it. */
318
461
  priorSpend?(): Spend | undefined;
462
+ /** Receipt `degraded` entries this journal adds to the run (e.g. `journal_unsandboxed`). */
463
+ degraded?(): string[];
464
+ /** The decider state earlier attempts of this run journaled (resume); undefined otherwise. */
465
+ deciderResume?(): JournalDeciderState | undefined;
319
466
  }
320
- /** Minimum gap between two live usage records of one attempt. Final reports are always written. */
321
- export declare const USAGE_JOURNAL_INTERVAL_MS = 2000;
322
- /** Test seam: called before each append; a throw simulates the process dying there. */
467
+ /** Test seams: a throw simulates the process dying at that point of an append. */
323
468
  export interface JournalFaults {
469
+ /** Before anything of the record reaches the disk. */
324
470
  beforeAppend?(record: JournalRecord): void;
471
+ /** After the head holds the record, before the journal append. */
472
+ afterHeadUpdate?(record: JournalRecord): void;
473
+ /** Replaces the journal write (a throw is an I/O failure, not a process death). */
474
+ writeJournal?(fd: number, line: string): void;
325
475
  }
326
476
  export interface RunJournalOptions {
327
477
  runId: string;
328
- /** Base directory; the journal goes in `<cwd>/.sweny/runs/<run-id>/`. Default: process.cwd(). */
478
+ /** The workspace the run belongs to (its real path, hashed, names the run's state dir). Default: process.cwd(). */
329
479
  cwd?: string;
330
480
  /** Workflow file the run came from, so `resume` can load it again. */
331
481
  workflowFile?: string;
@@ -334,69 +484,103 @@ export interface RunJournalOptions {
334
484
  env?: Record<string, string | undefined>;
335
485
  logger?: Logger;
336
486
  faults?: JournalFaults;
337
- /** Journals kept on disk (default {@link JOURNAL_KEEP}). */
487
+ /** Journals kept per workspace (default {@link JOURNAL_KEEP}). */
338
488
  keep?: number;
339
- /** Where run keys live (default {@link runKeyDir}). Never inside the workspace. */
340
- keyDir?: string;
489
+ /** Where run journals live (default {@link runStateRoot}). Never inside the workspace. */
490
+ stateRoot?: string;
341
491
  }
342
492
  export interface ResumeJournalOptions extends RunJournalOptions {
343
493
  read: JournalRead;
344
494
  plan: ResumePlan;
495
+ /** The run's lock, taken before the journal was read ({@link lockRun}). */
496
+ lock: RunLock;
345
497
  /** Resume even though the workflow, instructions, input or tools changed. */
346
498
  force?: boolean;
347
499
  /** Re-send writes whose outcome cannot be confirmed on the provider. */
348
500
  allowRepeatWrites?: boolean;
349
501
  }
350
- export declare function journalDir(cwd: string, runId: string): string;
351
- /** Run ids with a journal on disk, oldest first. */
352
- export declare function listJournalRuns(cwd?: string): string[];
353
- /** Exact run id, or a unique prefix. */
354
- export declare function findJournalRun(ref: string, cwd?: string): string | undefined;
355
- /** Delete the oldest journals (and their keys) beyond `keep`, never `except`. Never throws. */
356
- export declare function pruneJournals(cwd: string, keep?: number, except?: string, keyDir?: string): number;
502
+ /** A run's journal file. */
503
+ export declare function journalFile(cwd: string, runId: string, root?: string): string;
504
+ /**
505
+ * Runs of this workspace with a journal, oldest first. Only the state dir is
506
+ * read, and only runs whose metadata authenticates count: nothing in the
507
+ * workspace can add, hide or reorder a run.
508
+ */
509
+ export declare function listJournalRuns(cwd?: string, root?: string): string[];
510
+ /** Exact run id, or a unique prefix, among this workspace's journaled runs. */
511
+ export declare function findJournalRun(ref: string, cwd?: string, root?: string): string | undefined;
512
+ /** A journal an older sweny left in the workspace for this run id (or unique prefix), if any. Never read. */
513
+ export declare function findLegacyJournal(ref: string, cwd?: string): string | undefined;
514
+ /**
515
+ * Delete this workspace's oldest journals beyond `keep`, never `except`, by
516
+ * their authenticated creation time. Runs whose metadata does not
517
+ * authenticate are never deleted. Never throws.
518
+ */
519
+ export declare function pruneJournals(cwd: string, keep?: number, except?: string, root?: string): number;
520
+ /** Take a run's lock before reading its journal (resume). Throws {@link JournalLockedError}. */
521
+ export declare function lockRun(cwd: string, runId: string, root?: string, hooks?: LockHooks): RunLock;
357
522
  export declare class RunJournal implements ExecutionJournal {
358
523
  readonly runId: string;
359
524
  readonly dir: string;
360
525
  readonly file: string;
361
526
  private readonly cwd;
527
+ private readonly root;
362
528
  private readonly opts;
363
529
  private readonly logger;
364
530
  private readonly resume?;
365
531
  private fd;
366
532
  private seq;
367
- private dead;
533
+ /** MAC of the last record written: the next record chains to it. */
534
+ private prev;
535
+ /** Set once an append failed: every later append throws it again. */
536
+ private failure;
537
+ /** Set when a fault seam simulated the process dying: nothing more is written. */
538
+ private killed;
368
539
  private began;
369
540
  private closed;
370
541
  private secrets;
371
542
  private consumed;
372
- /** Per-run HMAC key; lives only here and in the key file outside the workspace. */
543
+ /** Per-run HMAC key; lives only here and in the run's private state dir. */
373
544
  private key;
374
- private readonly keyFile;
545
+ private lock;
375
546
  /** Last live usage record per attempt: time and spend, for throttling. */
376
547
  private lastUsage;
548
+ /** Visits already recorded as run without an enforced sandbox. */
549
+ private uncontainedVisits;
377
550
  private constructor();
378
551
  /** A journal for a new run. Nothing touches disk until the executor calls `begin`. */
379
552
  static create(opts: RunJournalOptions): RunJournal;
380
- /** A journal that continues `opts.read` (already repaired) under the resume plan. */
553
+ /** A journal that continues `opts.read` (already repaired, under `opts.lock`) under the resume plan. */
381
554
  static openForResume(opts: ResumeJournalOptions): RunJournal;
382
- /** False once an append failed: the rest of the run is not journaled. */
555
+ /** False once an append failed (or the process was simulated dead): the run cannot be resumed past that point. */
383
556
  get active(): boolean;
384
- private takeLock;
385
- /** The run dir is private (0700); no link on the path from cwd is followed (safe-file.ts). */
386
- private makeDir;
387
557
  private releaseLock;
558
+ /** Mark the journal failed and throw: the run must stop here. */
559
+ private fail;
560
+ private simulatedDeath;
388
561
  private append;
389
562
  private closeFd;
390
563
  private clean;
391
564
  begin(info: JournalBeginInfo): void;
565
+ deciderResume(): JournalDeciderState | undefined;
392
566
  replay(node: string, iteration: number): JournalReplay | undefined;
393
567
  nodeStart(node: string, iteration: number): void;
394
568
  checkpoint(node: string, iteration: number, cp: JournalCheckpoint): void;
395
569
  nodeEnd(node: string, iteration: number, result: NodeResult, writeState: WriteStageState): void;
396
- route(from: string, to: string | null): void;
570
+ route(from: string, to: string | null, rung?: RouteRung, decider?: DeciderCounters): void;
571
+ /**
572
+ * Journal one usage report, synchronously, whenever it raises the attempt's
573
+ * spend (reports are cumulative; the largest figure seen per unit counts).
574
+ * Nothing is held back: harnesses report once per model turn (Claude Code
575
+ * per assistant message, ACP per usage update), so every turn's spend is on
576
+ * disk before the next turn runs, and a process killed at any point has lost
577
+ * at most the turn it was in.
578
+ */
397
579
  usage(node: string, iteration: number, attempt: number, usage: NodeUsage, final: boolean): void;
580
+ uncontained(node: string, iteration: number): void;
581
+ degraded(): string[];
398
582
  priorSpend(): Spend | undefined;
399
- /** Close the run: `run:end`, then release the lock. Safe to call twice (one run:end). */
583
+ /** Close the run: `run:end` (when the journal still works), then release the lock. Never throws. */
400
584
  end(status: "success" | "failed" | "crashed"): void;
401
585
  wrapWrites(node: string, iteration: number, skills: Map<string, Skill>): Map<string, Skill>;
402
586
  private wrapTool;