@eir-labs/coltrane 0.4.1 → 0.5.1

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.
Files changed (63) hide show
  1. package/README.md +33 -4
  2. package/dist/src/access_grant.d.ts +21 -0
  3. package/dist/src/access_grant.js +33 -4
  4. package/dist/src/access_grant.js.map +1 -1
  5. package/dist/src/claude_invoker.d.ts +28 -0
  6. package/dist/src/claude_invoker.js +68 -7
  7. package/dist/src/claude_invoker.js.map +1 -1
  8. package/dist/src/cli.d.ts +44 -0
  9. package/dist/src/cli.js +358 -0
  10. package/dist/src/cli.js.map +1 -0
  11. package/dist/src/cli_entry.d.ts +2 -0
  12. package/dist/src/cli_entry.js +23 -0
  13. package/dist/src/cli_entry.js.map +1 -0
  14. package/dist/src/composition.d.ts +2 -0
  15. package/dist/src/composition.js.map +1 -1
  16. package/dist/src/fs_atomic.d.ts +1 -0
  17. package/dist/src/fs_atomic.js +34 -0
  18. package/dist/src/fs_atomic.js.map +1 -0
  19. package/dist/src/genome_schema.d.ts +5 -1
  20. package/dist/src/genome_schema.js +12 -1
  21. package/dist/src/genome_schema.js.map +1 -1
  22. package/dist/src/genome_writer.d.ts +2 -2
  23. package/dist/src/genome_writer.js +17 -4
  24. package/dist/src/genome_writer.js.map +1 -1
  25. package/dist/src/gig_tracker.d.ts +29 -1
  26. package/dist/src/gig_tracker.js +27 -0
  27. package/dist/src/gig_tracker.js.map +1 -1
  28. package/dist/src/index.d.ts +1 -0
  29. package/dist/src/index.js +1 -0
  30. package/dist/src/index.js.map +1 -1
  31. package/dist/src/ledger.d.ts +3 -0
  32. package/dist/src/ledger.js.map +1 -1
  33. package/dist/src/loader.d.ts +2 -0
  34. package/dist/src/loader.js +10 -0
  35. package/dist/src/loader.js.map +1 -1
  36. package/dist/src/mcp.js +82 -20
  37. package/dist/src/mcp.js.map +1 -1
  38. package/dist/src/outputs.d.ts +74 -0
  39. package/dist/src/outputs.js +106 -71
  40. package/dist/src/outputs.js.map +1 -1
  41. package/dist/src/registry.d.ts +13 -0
  42. package/dist/src/registry.js +112 -0
  43. package/dist/src/registry.js.map +1 -1
  44. package/dist/src/reuse.d.ts +276 -0
  45. package/dist/src/reuse.js +215 -0
  46. package/dist/src/reuse.js.map +1 -0
  47. package/dist/src/runtime.d.ts +157 -4
  48. package/dist/src/runtime.js +519 -30
  49. package/dist/src/runtime.js.map +1 -1
  50. package/dist/src/server.d.ts +20 -0
  51. package/dist/src/server.js +733 -28
  52. package/dist/src/server.js.map +1 -1
  53. package/dist/src/server_relay.d.ts +2 -0
  54. package/dist/src/server_relay.js +31 -8
  55. package/dist/src/server_relay.js.map +1 -1
  56. package/dist/src/skill_runner.mjs +8 -2
  57. package/dist/src/skill_subprocess.d.ts +47 -5
  58. package/dist/src/skill_subprocess.js +242 -11
  59. package/dist/src/skill_subprocess.js.map +1 -1
  60. package/dist/src/version.d.ts +1 -1
  61. package/dist/src/version.js +11 -7
  62. package/dist/src/version.js.map +1 -1
  63. package/package.json +8 -7
@@ -0,0 +1,215 @@
1
+ // Reuse a sealed output instead of re-deriving it.
2
+ //
3
+ // ONE idea, two ranges. A gig CHECKPOINT lets a failed run restart from the phase it died
4
+ // on instead of from zero (reuse across attempts at the same gig); a chair-level REUSE CACHE
5
+ // lets a chair be served from a prior gig's sealed output instead of invoking the model
6
+ // (reuse across gigs). Both answer the same question — "is there an existing sealed output
7
+ // that stands in for the one I am about to pay to derive?" — and both live or die on the
8
+ // same discipline: an exact, defensible key, and a refusal to serve anything that would not
9
+ // pass the seal boundary today.
10
+ //
11
+ // WHY THIS IS NOT MERELY AN OPTIMISATION. Substituting a sealed output means the artifact an
12
+ // operator acts on was produced under conditions that are no longer visible in the run. If
13
+ // the genome moved between the two, the provenance chain silently splices two different
14
+ // systems together: sealed outputs from run A consumed by chairs from genome B, with nothing
15
+ // in `input_shas`, `genome_hash` or `run_fingerprint` recording that it happened. So every
16
+ // substitution here is gated on a stated IDENTITY, and every substitution that happens is
17
+ // REPORTED. A silent saving is indistinguishable from a bug.
18
+ //
19
+ // THE GOVERNING ASYMMETRY (borrowed, deliberately, from the downstream precedent in
20
+ // grant-writing-coltrane's `dashboard/src/lib/requirements-cache.ts`): a MISS IS FREE — it
21
+ // costs the cold run that would have happened anyway. A WRONG HIT IS NOT. Every ambiguous
22
+ // case below therefore resolves to "do the work".
23
+ //
24
+ // PURITY. Key derivation and entry validation are pure functions of their arguments — no
25
+ // clock, no filesystem, no registry handle. The file-backed stores at the bottom are the only
26
+ // I/O. That split is what makes the correctness half exhaustively testable.
27
+ import * as fs from "node:fs";
28
+ import * as path from "node:path";
29
+ import { writeFileAtomic } from "./fs_atomic.js";
30
+ import { sha256Hex, canonJson } from "./canonical_form.js";
31
+ /**
32
+ * Bump when the key LAYOUT changes (a new component, a different framing). Old keys then
33
+ * simply stop being addressable, which is the correct outcome: a key computed under a
34
+ * different layout is not a weaker match, it is a different question.
35
+ */
36
+ export const REUSE_KEY_VERSION = "cr1";
37
+ /** Bump when the on-disk checkpoint shape changes. A checkpoint of another version is refused. */
38
+ export const CHECKPOINT_SCHEMA_VERSION = 1;
39
+ /** Bump when the on-disk reuse-entry shape changes. An entry of another version is a miss. */
40
+ export const REUSE_SCHEMA_VERSION = 1;
41
+ export class ReuseStoreError extends Error {
42
+ }
43
+ /**
44
+ * Fingerprint one type definition: its core, its required list, and its whole schema.
45
+ *
46
+ * Deliberately OVER-SENSITIVE — any edit to the type, including a nested one, invalidates
47
+ * every entry sealed under the old shape. Type edits are rare; a miss costs one cold
48
+ * invocation; serving an object the current seal validator would reject costs a run that
49
+ * dies at the terminal phase. The trade is not close.
50
+ */
51
+ export function typeShapeFingerprint(def) {
52
+ return sha256Hex(canonJson({
53
+ extends: def.extends,
54
+ required: [...(def.required_fields ?? [])].sort(),
55
+ schema: def.schema ?? null,
56
+ }));
57
+ }
58
+ /**
59
+ * Fold every producer definition a run depends on into one hash.
60
+ *
61
+ * Whole agent definitions, plus each resolved skill's verified `code_hash` — not the slug,
62
+ * which is precisely what an edit-under-a-stable-slug leaves unchanged.
63
+ */
64
+ export function producersSha(input) {
65
+ return sha256Hex(canonJson({
66
+ agents: input.agents,
67
+ skills: [...(input.skills ?? [])]
68
+ .map((s) => ({ slug: s.slug, code_hash: s.code_hash }))
69
+ .sort((a, b) => (a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0)),
70
+ }));
71
+ }
72
+ export function runIdentityMismatch(a, b) {
73
+ const out = [];
74
+ for (const k of Object.keys(a)) {
75
+ if (a[k] !== b[k])
76
+ out.push(`${k}: checkpoint="${a[k]}" current="${b[k]}"`);
77
+ }
78
+ return out;
79
+ }
80
+ /**
81
+ * The cache key. Length-prefixed framing so no component can forge a delimiter and
82
+ * masquerade as a different key layout.
83
+ *
84
+ * NOT in the key: the TYPE FINGERPRINTS of the sealed outputs. Those ride on the ENTRY and
85
+ * are re-checked on read. Putting them in the key (as the downstream precedent does) would
86
+ * make a stale entry unaddressable — safe, but a SILENT miss, indistinguishable from
87
+ * never-cached. This engine has spent a lot of effort making refusals say why they happened,
88
+ * and the entry-side check gives a stale entry a name (`type-fingerprint-mismatch`) instead
89
+ * of a shrug. It is equally safe here because, unlike that precedent, every injection also
90
+ * crosses the real seal boundary before anything becomes durable.
91
+ */
92
+ export function reuseCacheKey(input) {
93
+ const frame = (...parts) => parts.map((p) => `${p.length}:${p}`).join("|");
94
+ return sha256Hex(frame(REUSE_KEY_VERSION, input.canonical_form_version, input.standard_slug, input.phase, canonJson(input.chair), canonJson(input.agent ?? null), canonJson(input.skill_provenance ?? null), canonJson([...input.skills].map((s) => [s.slug, s.version, s.code_hash]).sort()), canonJson([...input.input_shas].sort()), input.gig_input_sha, input.model_version, input.depth, canonJson([...input.output_types].sort())));
95
+ }
96
+ /**
97
+ * Is this entry still safe to serve — on the evidence available WITHOUT touching the store?
98
+ *
99
+ * This is the cheap half of the guard. The authoritative half is that every injected output
100
+ * is re-validated through `OutputStore.validateWrite` and then re-sealed through
101
+ * `OutputStore.write`, so it crosses exactly the boundary a fresh output crosses: the #263
102
+ * core-agreement check, the registry schema, and the #227/#228 core substance floor. Nothing
103
+ * is injected that a fresh seal would have refused.
104
+ *
105
+ * Both halves exist because they fail in different directions. The fingerprint catches a
106
+ * genome edit BEFORE any data is touched and can name it. The seal catches everything else —
107
+ * a hand-edited entry, an entry written by an older engine whose substance floor was looser,
108
+ * a registry that resolves the type to a different core than it did. Neither subsumes the
109
+ * other, and the expensive one is the one that must not be skipped.
110
+ */
111
+ export function checkReuseEntry(entry, fingerprintOf) {
112
+ if (entry.schema_version !== REUSE_SCHEMA_VERSION) {
113
+ return { ok: false, reason: "schema-version", detail: `entry is v${entry.schema_version}, engine reads v${REUSE_SCHEMA_VERSION}` };
114
+ }
115
+ if (!Array.isArray(entry.outputs) || entry.outputs.length === 0) {
116
+ // A chair that sealed nothing is legal (#243, every promised type optional) but it is not
117
+ // cacheable: "sealed nothing" and "was never cached" are the same bytes, so serving it
118
+ // would be asserting a fact the entry cannot carry.
119
+ return { ok: false, reason: "empty-entry", detail: "entry records no sealed outputs" };
120
+ }
121
+ for (const o of entry.outputs) {
122
+ const now = fingerprintOf(o.domain_type);
123
+ if (now === "") {
124
+ // A cache that cannot check its entries must not serve them.
125
+ return { ok: false, reason: "type-unfingerprintable", detail: `the registry cannot describe "${o.domain_type}"` };
126
+ }
127
+ if (now !== o.type_fingerprint) {
128
+ return { ok: false, reason: "type-fingerprint-mismatch", detail: `"${o.domain_type}" has changed shape since this entry was sealed` };
129
+ }
130
+ }
131
+ return { ok: true };
132
+ }
133
+ // ───────────────────────────────────────────────────────────────────────────────
134
+ // File-backed stores
135
+ // ───────────────────────────────────────────────────────────────────────────────
136
+ // Write-then-rename. A torn checkpoint would be read as damage and refuse a resume that was,
137
+ // in fact, resumable — the failure mode a partial `appendFileSync` would introduce.
138
+ //
139
+ // Shared with the genome writer (src/fs_atomic.ts), which needed the same guarantee and did not
140
+ // have it. Two implementations of one concern, one documented and one absent, is how the two
141
+ // identity gates in this file came to disagree.
142
+ const writeAtomic = writeFileAtomic;
143
+ function readJsonFile(file) {
144
+ if (!fs.existsSync(file))
145
+ return undefined;
146
+ const text = fs.readFileSync(file, "utf8");
147
+ try {
148
+ return JSON.parse(text);
149
+ }
150
+ catch (e) {
151
+ throw new ReuseStoreError(`"${file}" is unreadable: ${e instanceof Error ? e.message : String(e)}`);
152
+ }
153
+ }
154
+ /** Checkpoints under `<dir>/checkpoints/<gig_id>.json`, alongside `outputs/` and `refs/`. */
155
+ export function createCheckpointStore(persistDir) {
156
+ const dir = path.join(persistDir, "checkpoints");
157
+ return {
158
+ read(gig_id) {
159
+ // A gig id reaches this from a caller's argument; it must not be able to name a path.
160
+ if (!/^[A-Za-z0-9._-]+$/.test(gig_id))
161
+ return undefined;
162
+ return readJsonFile(path.join(dir, `${gig_id}.json`));
163
+ },
164
+ write(cp) {
165
+ writeAtomic(path.join(dir, `${cp.gig_id}.json`), JSON.stringify(cp));
166
+ },
167
+ remove(gig_id) {
168
+ // Same path guard as read(): a gig id arrives from a caller's argument and must not be
169
+ // able to name a path. Doubly so here, where the operation DELETES.
170
+ if (!/^[A-Za-z0-9._-]+$/.test(gig_id))
171
+ return;
172
+ try {
173
+ fs.rmSync(path.join(dir, `${gig_id}.json`), { force: true });
174
+ }
175
+ catch { /* best-effort */ }
176
+ },
177
+ };
178
+ }
179
+ /** Reuse entries under `<dir>/reuse/<cache_key>.json`. Keys are sha256 hex — flat is fine. */
180
+ export function createReuseStore(persistDir) {
181
+ const dir = path.join(persistDir, "reuse");
182
+ return {
183
+ get(cache_key) {
184
+ if (!/^[0-9a-f]{64}$/.test(cache_key))
185
+ return undefined;
186
+ return readJsonFile(path.join(dir, `${cache_key}.json`));
187
+ },
188
+ put(entry) {
189
+ writeAtomic(path.join(dir, `${entry.cache_key}.json`), JSON.stringify(entry));
190
+ },
191
+ };
192
+ }
193
+ /** An in-memory pair for tests and for a caller that wants reuse within one process. */
194
+ export function createMemoryCheckpointStore() {
195
+ const m = new Map();
196
+ return {
197
+ read: (id) => {
198
+ const raw = m.get(id);
199
+ return raw === undefined ? undefined : JSON.parse(raw);
200
+ },
201
+ write: (cp) => void m.set(cp.gig_id, JSON.stringify(cp)),
202
+ remove: (id) => void m.delete(id),
203
+ };
204
+ }
205
+ export function createMemoryReuseStore() {
206
+ const m = new Map();
207
+ return {
208
+ get: (k) => {
209
+ const raw = m.get(k);
210
+ return raw === undefined ? undefined : JSON.parse(raw);
211
+ },
212
+ put: (e) => void m.set(e.cache_key, JSON.stringify(e)),
213
+ };
214
+ }
215
+ //# sourceMappingURL=reuse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reuse.js","sourceRoot":"","sources":["../../src/reuse.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import type { Standard, Agent, Chair } from "./composition.js";
2
+ import { type CheckpointStore, type ReuseStore } from "./reuse.js";
2
3
  import type { OutputStore, OutputRecord } from "./outputs.js";
3
4
  import { type Ledger, type GigUsage } from "./ledger.js";
4
5
  import type { Depth } from "./pricing.js";
@@ -62,6 +63,29 @@ export type GigProgressEvent = {
62
63
  phase: string;
63
64
  role: string;
64
65
  event: AgentStreamEvent;
66
+ } | {
67
+ type: "gig_resumed";
68
+ from_gig_id: string;
69
+ roles: string[];
70
+ outputs: number;
71
+ } | {
72
+ type: "chair_skipped";
73
+ phase: string;
74
+ role: string;
75
+ /** "resume" — restored from this gig's own checkpoint. "reuse" — a prior gig's output. */
76
+ reason: "resume" | "reuse";
77
+ source_gig_id: string;
78
+ output_types: string[];
79
+ cache_key?: string;
80
+ }
81
+ /** A cache entry was FOUND and refused. A silent refusal is as opaque as a silent hit. */
82
+ | {
83
+ type: "reuse_rejected";
84
+ phase: string;
85
+ role: string;
86
+ cache_key: string;
87
+ reason: string;
88
+ detail?: string;
65
89
  } | {
66
90
  type: "gig_complete";
67
91
  outputs: number;
@@ -118,10 +142,10 @@ export interface RunDeps {
118
142
  * signal is threaded into every AgentInvocationContext, so an invoker that wires it to its
119
143
  * subprocess also stops the in-flight chair. Absent = an uncancellable run (the v0 shape).
120
144
  *
121
- * Known bound: a SKILL-backed chair runs `spawnSync` (src/skill_subprocess.ts), which
122
- * blocks the event loop, so an abort delivered while one is executing is not even RECEIVED
123
- * until it returns. Skill chairs are therefore a hard uncancellable window of up to their
124
- * declared `meta.timeout_ms` (default 120s). Deliberate for now see #253.
145
+ * Skill chairs are cancellable too (#253). They used to run `spawnSync`, which blocks the
146
+ * event loop, so an abort delivered during one was not even RECEIVED until it returned —
147
+ * a hard uncancellable window of up to the skill's `meta.timeout_ms`, 120s by default.
148
+ * `executeSkillAsync` spawns without blocking and SIGKILLs on the signal.
125
149
  */
126
150
  signal?: AbortSignal | undefined;
127
151
  /**
@@ -129,6 +153,49 @@ export interface RunDeps {
129
153
  * the thing that actually spends. Absent = each agent's own `depth_profile` stands.
130
154
  */
131
155
  depth?: Depth | undefined;
156
+ /**
157
+ * Durable per-gig checkpoints. When wired, the runtime records each completed chair's sealed
158
+ * outputs (id + content_sha + type fingerprint) after every dispatch batch, so a run that
159
+ * dies at phase 5 can later be resumed instead of restarting from zero.
160
+ *
161
+ * WRITING is automatic; ACTING on it is not. That asymmetry is deliberate and it is the only
162
+ * one that works: a checkpoint you have to opt into BEFORE the failure is a checkpoint you
163
+ * never have. Recording a fact changes no behaviour; `resume_from` is what changes behaviour,
164
+ * and it is explicit.
165
+ *
166
+ * Absent = no checkpoints written, and `resume_from` is refused.
167
+ */
168
+ checkpoints?: CheckpointStore | undefined;
169
+ /**
170
+ * Resume the named gig. The run CONTINUES that gig — same `gig_id` — rather than minting a
171
+ * new one, because the outputs it restores already carry that id and `OutputStore.trace`
172
+ * scopes the provenance walk to a single gig. A fresh id would truncate every restored
173
+ * ancestor out of the chain, which is the opposite of what a resume is for.
174
+ *
175
+ * FAILURE POSTURE: a resume that cannot be honoured THROWS `ResumeRefused` and spends
176
+ * nothing. It never quietly falls back to a cold run. `resume_from` is a claim about a
177
+ * specific prior run; if the claim is false the caller is wrong about the world and needs to
178
+ * be told. And the "harmless" alternative is not harmless: a silent cold run charges the
179
+ * full price and returns a reply indistinguishable from a resume that worked, so the cost
180
+ * surprise is also an UNOBSERVABLE one. Dropping the flag and re-dispatching is one call
181
+ * away; noticing a silent $6 is not.
182
+ */
183
+ resume_from?: string | undefined;
184
+ /**
185
+ * The chair-level reuse cache. Presence IS the opt-in — the runtime never constructs one —
186
+ * and it enables BOTH reads and writes.
187
+ *
188
+ * Writes are gated by the same flag on purpose. This store is cross-gig by construction, so
189
+ * populating it is itself the decision that run A's sealed outputs may stand in for run B's
190
+ * work. That is a decision, not a side effect of having run something. The cost is that the
191
+ * first opted-in run only populates; the second one hits.
192
+ *
193
+ * FAILURE POSTURE, and it differs from resume's: a found-but-unusable entry is REPORTED and
194
+ * the chair does the work. `reuse` names no specific prior run — "no valid entry" is a
195
+ * normal outcome of a lookup, not a falsified premise — so the honest response is a miss,
196
+ * loudly recorded in `GigResult.reuse.rejected`, not a dead run.
197
+ */
198
+ reuse?: ReuseStore | undefined;
132
199
  }
133
200
  /**
134
201
  * Per-gig cost-budget input. Honors budget-state.json schema (PR #56). Only
@@ -202,6 +269,63 @@ export interface BudgetState {
202
269
  */
203
270
  settled_usd: number;
204
271
  }
272
+ /** One chair that did not run, and what stood in for it. */
273
+ export interface SkippedChair {
274
+ phase: string;
275
+ role: string;
276
+ reason: "resume" | "reuse";
277
+ /** The gig whose sealed output was used. Equal to this gig's id for a resume. */
278
+ source_gig_id: string;
279
+ output_types: string[];
280
+ /** The content_shas served. Identical to what a fresh derivation would have sealed. */
281
+ content_shas: string[];
282
+ /** reuse only — the key that matched, so an operator can reason about WHY it matched. */
283
+ cache_key?: string;
284
+ }
285
+ export interface GigResumeReport {
286
+ from_gig_id: string;
287
+ /** When the checkpoint this resume read was last written. */
288
+ checkpoint_at: string;
289
+ roles: Array<{
290
+ phase: string;
291
+ role: string;
292
+ output_types: string[];
293
+ }>;
294
+ outputs_restored: number;
295
+ /**
296
+ * What the earlier attempt(s) had spent when the checkpoint was written. Deliberately kept
297
+ * OUT of `GigResult.usage`: #235/#236 made `usage` mean "what THIS run actually captured",
298
+ * and widening it to "what the gig cost across attempts" would undo that. Two numbers, both
299
+ * true, reported separately. Absent when the earlier attempt captured no usage.
300
+ */
301
+ prior_usage?: unknown;
302
+ }
303
+ export interface GigReuseReport {
304
+ /** Chairs served from a prior gig's sealed output. */
305
+ hits: Array<{
306
+ phase: string;
307
+ role: string;
308
+ cache_key: string;
309
+ source_gig_id: string;
310
+ output_types: string[];
311
+ }>;
312
+ /** Entries that were FOUND and refused. Never a silent miss — a stale entry gets a name. */
313
+ rejected: Array<{
314
+ phase: string;
315
+ role: string;
316
+ cache_key: string;
317
+ reason: string;
318
+ detail?: string;
319
+ }>;
320
+ /** Entries written this run. */
321
+ writes: number;
322
+ /** Entries that could not be written. Not fatal — the run is unaffected — but not silent. */
323
+ write_errors: Array<{
324
+ phase: string;
325
+ role: string;
326
+ reason: string;
327
+ }>;
328
+ }
205
329
  export interface GigResult {
206
330
  gig_id: string;
207
331
  standard_slug: string;
@@ -235,6 +359,22 @@ export interface GigResult {
235
359
  budget_state?: BudgetState;
236
360
  /** Settled model spend (#195). Present when ≥1 real model invocation ran this gig. */
237
361
  usage?: GigUsage;
362
+ /**
363
+ * Chairs that did not run because a sealed output stood in for them. Present only when
364
+ * something was actually skipped — so its ABSENCE means every chair ran, and its presence
365
+ * is the run stating plainly that part of this manifest was recalled rather than derived.
366
+ */
367
+ skipped?: readonly SkippedChair[];
368
+ /** Present when this run resumed a prior attempt at the same gig. */
369
+ resumed_from?: GigResumeReport;
370
+ /** Present when the reuse cache was wired, whether or not anything hit. */
371
+ reuse?: GigReuseReport;
372
+ /**
373
+ * The checkpoint store was wired and could not be written. The run is unaffected and
374
+ * complete — but it is NOT resumable, and an operator who believes otherwise will find out
375
+ * at the worst possible moment.
376
+ */
377
+ checkpoint_error?: string;
238
378
  }
239
379
  /**
240
380
  * #236 — settled spend used to be discarded on every failed gig: `usage` was written only on
@@ -247,6 +387,19 @@ export declare function partialGigUsage(e: unknown): GigUsage | undefined;
247
387
  export declare function partialBudgetState(e: unknown): BudgetState | undefined;
248
388
  export declare class RuntimeError extends Error {
249
389
  }
390
+ /**
391
+ * A resume was requested and cannot be honoured. Thrown BEFORE any chair is prepared, so a
392
+ * refused resume costs nothing.
393
+ *
394
+ * Distinct from RuntimeError because it is not a crash and not a composition defect — it is
395
+ * the engine declining to splice two runs together. `drift` names exactly which identity
396
+ * fields disagree, so "it refused" is never the whole answer an operator gets.
397
+ */
398
+ export declare class ResumeRefused extends Error {
399
+ readonly gig_id: string;
400
+ readonly drift: readonly string[];
401
+ constructor(gig_id: string, why: string, drift?: readonly string[]);
402
+ }
250
403
  /**
251
404
  * Raised when a gig is cancelled through `RunDeps.signal` (#249). Distinct from RuntimeError
252
405
  * so a caller can tell "an operator stopped this" from "this crashed" — #251's point that a