@sweny-ai/core 0.1.178 → 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)
@@ -30,17 +49,31 @@
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
51
  * route the edge taken after the visit (null = the run ended), and the rung that chose it
33
- * usage spend of one agent attempt (cumulative; live while it runs, final when it returns)
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.
@@ -56,11 +89,15 @@ import type { Logger, NodeResult, NodeUsage, RouteRung, Skill, Workflow } from "
56
89
  import type { SafeOutputIntent, WriteStageState } from "./safe-outputs.js";
57
90
  import { type Spend } from "./budget.js";
58
91
  import type { DeciderCounters } from "./decider.js";
59
- /** v2: records are authenticated with a per-run HMAC key (v1 used a public checksum and is refused). */
60
- export declare const JOURNAL_SCHEMA_VERSION = 2;
61
- export declare const JOURNAL_DIR: string;
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;
62
99
  export declare const JOURNAL_FILE = "journal.ndjson";
63
- /** 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. */
64
101
  export declare const JOURNAL_KEEP = 20;
65
102
  export declare const REDACTED = "[redacted]";
66
103
  /** sha256 (hex) of `value` as canonical JSON: keys sorted, undefined dropped. Same as run history's workflow hash. */
@@ -88,16 +125,69 @@ export declare function instructionHash(sources: Record<string, {
88
125
  export declare function toolsHash(skills: Map<string, Skill>, swenyVersion?: string, harnessId?: string, secrets?: string[]): string;
89
126
  /** sweny's per-user state dir: `$SWENY_STATE_DIR`, else `$XDG_STATE_HOME/sweny`, else `~/.local/state/sweny`. */
90
127
  export declare function swenyStateDir(env?: Record<string, string | undefined>): string;
91
- /** Where run keys live. Never inside the workspace. */
92
- export declare function runKeyDir(env?: Record<string, string | undefined>): string;
93
- /** The key file for one run in one workspace (the workspace path is hashed, not stored). */
94
- 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;
95
146
  /** Thrown when a journal's key is missing or unreadable: its records cannot be checked. */
96
147
  export declare class JournalKeyError extends Error {
97
148
  constructor(file: string, why: string);
98
149
  }
99
150
  /** Read a run key. Throws {@link JournalKeyError}. */
100
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;
101
191
  /** True for keys like `GITHUB_TOKEN`, `apiKey`, `client-secret`, `Authorization`. */
102
192
  export declare function isSecretKey(key: string): boolean;
103
193
  /** Values of secret-named env vars / config entries, longest first (8+ chars, so short words never match). */
@@ -185,15 +275,20 @@ export declare class JournalMismatchError extends Error {
185
275
  readonly what: string[];
186
276
  constructor(what: string[]);
187
277
  }
188
- /** Thrown when another live process holds the run's journal. */
189
- export declare class JournalLockedError extends Error {
190
- 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);
191
281
  }
192
282
  export interface JournalRead {
193
283
  file: string;
284
+ /** Authenticated, chained records only: everything a resume replays or seeds spend from. */
194
285
  records: JournalRecord[];
195
- /** 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). */
196
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;
197
292
  /**
198
293
  * Set when an invalid record sits before valid ones (not a torn tail), or when
199
294
  * a whole record fails authentication (tampering, never a torn write). Not repairable.
@@ -202,18 +297,31 @@ export interface JournalRead {
202
297
  /** The line whose record failed authentication, when that is why the journal is refused. */
203
298
  forgedAtLine?: number;
204
299
  }
205
- /**
206
- * Read a journal. With `repair`, a torn tail is truncated on disk back to the
207
- * last valid record. Records are authenticated with the run's key (`key`, else
208
- * loaded from the state dir for the run the path names). Throws
209
- * {@link JournalVersionError} for another format and {@link JournalKeyError}
210
- * when the key is missing.
211
- */
212
- 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. */
213
302
  repair?: boolean;
303
+ /** The run key; default: the `key` file next to the journal. */
214
304
  key?: Buffer;
215
- keyDir?: string;
216
- }): 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;
217
325
  export type VisitAction = "replay" | "write-stage" | "rerun";
218
326
  export interface PlannedVisit {
219
327
  node: string;
@@ -250,6 +358,12 @@ export interface ResumePlan {
250
358
  receipts: Map<string, OutputRecord>;
251
359
  /** Spend every earlier attempt journaled: the run budget starts here on resume. */
252
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[];
253
367
  /** The decider's mode at run start and its counters after the last journaled route (#357). */
254
368
  decider?: JournalDeciderState;
255
369
  }
@@ -321,8 +435,9 @@ export type JournalReplay = {
321
435
  kind: "checkpoint";
322
436
  } & JournalCheckpoint);
323
437
  /**
324
- * The executor's view of a journal (`ExecuteOptions.journal`). Every hook is
325
- * 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.
326
441
  */
327
442
  export interface ExecutionJournal {
328
443
  begin(info: JournalBeginInfo): void;
@@ -337,23 +452,30 @@ export interface ExecutionJournal {
337
452
  route(from: string, to: string | null, rung?: RouteRung, decider?: DeciderCounters): void;
338
453
  /**
339
454
  * Spend of one agent attempt, cumulative for the attempt: `final` when the
340
- * 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).
341
456
  */
342
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;
343
460
  /** Spend earlier attempts of this run already journaled (resume); the run budget starts from it. */
344
461
  priorSpend?(): Spend | undefined;
462
+ /** Receipt `degraded` entries this journal adds to the run (e.g. `journal_unsandboxed`). */
463
+ degraded?(): string[];
345
464
  /** The decider state earlier attempts of this run journaled (resume); undefined otherwise. */
346
465
  deciderResume?(): JournalDeciderState | undefined;
347
466
  }
348
- /** Minimum gap between two live usage records of one attempt. Final reports are always written. */
349
- export declare const USAGE_JOURNAL_INTERVAL_MS = 2000;
350
- /** 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. */
351
468
  export interface JournalFaults {
469
+ /** Before anything of the record reaches the disk. */
352
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;
353
475
  }
354
476
  export interface RunJournalOptions {
355
477
  runId: string;
356
- /** 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(). */
357
479
  cwd?: string;
358
480
  /** Workflow file the run came from, so `resume` can load it again. */
359
481
  workflowFile?: string;
@@ -362,57 +484,80 @@ export interface RunJournalOptions {
362
484
  env?: Record<string, string | undefined>;
363
485
  logger?: Logger;
364
486
  faults?: JournalFaults;
365
- /** Journals kept on disk (default {@link JOURNAL_KEEP}). */
487
+ /** Journals kept per workspace (default {@link JOURNAL_KEEP}). */
366
488
  keep?: number;
367
- /** Where run keys live (default {@link runKeyDir}). Never inside the workspace. */
368
- keyDir?: string;
489
+ /** Where run journals live (default {@link runStateRoot}). Never inside the workspace. */
490
+ stateRoot?: string;
369
491
  }
370
492
  export interface ResumeJournalOptions extends RunJournalOptions {
371
493
  read: JournalRead;
372
494
  plan: ResumePlan;
495
+ /** The run's lock, taken before the journal was read ({@link lockRun}). */
496
+ lock: RunLock;
373
497
  /** Resume even though the workflow, instructions, input or tools changed. */
374
498
  force?: boolean;
375
499
  /** Re-send writes whose outcome cannot be confirmed on the provider. */
376
500
  allowRepeatWrites?: boolean;
377
501
  }
378
- export declare function journalDir(cwd: string, runId: string): string;
379
- /** Run ids with a journal on disk, oldest first. */
380
- export declare function listJournalRuns(cwd?: string): string[];
381
- /** Exact run id, or a unique prefix. */
382
- export declare function findJournalRun(ref: string, cwd?: string): string | undefined;
383
- /** Delete the oldest journals (and their keys) beyond `keep`, never `except`. Never throws. */
384
- 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;
385
522
  export declare class RunJournal implements ExecutionJournal {
386
523
  readonly runId: string;
387
524
  readonly dir: string;
388
525
  readonly file: string;
389
526
  private readonly cwd;
527
+ private readonly root;
390
528
  private readonly opts;
391
529
  private readonly logger;
392
530
  private readonly resume?;
393
531
  private fd;
394
532
  private seq;
395
- 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;
396
539
  private began;
397
540
  private closed;
398
541
  private secrets;
399
542
  private consumed;
400
- /** 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. */
401
544
  private key;
402
- private readonly keyFile;
545
+ private lock;
403
546
  /** Last live usage record per attempt: time and spend, for throttling. */
404
547
  private lastUsage;
548
+ /** Visits already recorded as run without an enforced sandbox. */
549
+ private uncontainedVisits;
405
550
  private constructor();
406
551
  /** A journal for a new run. Nothing touches disk until the executor calls `begin`. */
407
552
  static create(opts: RunJournalOptions): RunJournal;
408
- /** 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. */
409
554
  static openForResume(opts: ResumeJournalOptions): RunJournal;
410
- /** 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. */
411
556
  get active(): boolean;
412
- private takeLock;
413
- /** The run dir is private (0700); no link on the path from cwd is followed (safe-file.ts). */
414
- private makeDir;
415
557
  private releaseLock;
558
+ /** Mark the journal failed and throw: the run must stop here. */
559
+ private fail;
560
+ private simulatedDeath;
416
561
  private append;
417
562
  private closeFd;
418
563
  private clean;
@@ -423,9 +568,19 @@ export declare class RunJournal implements ExecutionJournal {
423
568
  checkpoint(node: string, iteration: number, cp: JournalCheckpoint): void;
424
569
  nodeEnd(node: string, iteration: number, result: NodeResult, writeState: WriteStageState): void;
425
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
+ */
426
579
  usage(node: string, iteration: number, attempt: number, usage: NodeUsage, final: boolean): void;
580
+ uncontained(node: string, iteration: number): void;
581
+ degraded(): string[];
427
582
  priorSpend(): Spend | undefined;
428
- /** 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. */
429
584
  end(status: "success" | "failed" | "crashed"): void;
430
585
  wrapWrites(node: string, iteration: number, skills: Map<string, Skill>): Map<string, Skill>;
431
586
  private wrapTool;