@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/agent-env.d.ts +5 -3
- package/dist/agent-env.js +12 -7
- package/dist/cli/config-file.d.ts +22 -1
- package/dist/cli/config-file.js +42 -0
- package/dist/cli/final-output.js +4 -1
- package/dist/cli/main.d.ts +2 -1
- package/dist/cli/main.js +7 -8
- package/dist/cli/resume.d.ts +10 -1
- package/dist/cli/resume.js +77 -7
- package/dist/cli/run-history.d.ts +3 -7
- package/dist/cli/run-history.js +1 -10
- package/dist/cli/run-output.d.ts +16 -10
- package/dist/cli/run-output.js +19 -13
- package/dist/cli/ticket.js +3 -2
- package/dist/decider.d.ts +176 -64
- package/dist/decider.js +475 -155
- package/dist/executor.d.ts +50 -7
- package/dist/executor.js +533 -93
- package/dist/harness/acp.js +2 -0
- package/dist/harness/claude-code.d.ts +18 -0
- package/dist/harness/claude-code.js +51 -6
- package/dist/harness/codex.js +2 -0
- package/dist/harness/pi.js +2 -0
- package/dist/harness/sandbox-wrapper.js +8 -8
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/journal.d.ts +258 -74
- package/dist/journal.js +749 -208
- package/dist/schema.d.ts +189 -241
- package/dist/schema.js +56 -36
- package/dist/skills/github.js +39 -0
- package/dist/types.d.ts +55 -3
- package/dist/types.js +22 -0
- package/dist/when.d.ts +8 -1
- package/dist/when.js +8 -1
- package/dist/workflow-builder.js +3 -1
- package/dist/workflows/browser.js +2 -2
- package/dist/workflows/implement.yml +37 -5
- package/dist/workflows/triage.yml +40 -6
- package/package.json +1 -1
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* the
|
|
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;
|
|
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
|
-
*
|
|
40
|
-
* the
|
|
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
|
|
43
|
-
*
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
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
|
|
91
|
-
export declare function
|
|
92
|
-
/** The
|
|
93
|
-
export declare function
|
|
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
|
|
184
|
-
export declare class
|
|
185
|
-
constructor(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
211
|
-
|
|
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`).
|
|
300
|
-
*
|
|
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
|
-
|
|
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 (
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
487
|
+
/** Journals kept per workspace (default {@link JOURNAL_KEEP}). */
|
|
338
488
|
keep?: number;
|
|
339
|
-
/** Where run
|
|
340
|
-
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
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
|
|
543
|
+
/** Per-run HMAC key; lives only here and in the run's private state dir. */
|
|
373
544
|
private key;
|
|
374
|
-
private
|
|
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
|
|
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
|
|
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;
|