@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.
- package/README.md +33 -4
- package/dist/src/access_grant.d.ts +21 -0
- package/dist/src/access_grant.js +33 -4
- package/dist/src/access_grant.js.map +1 -1
- package/dist/src/claude_invoker.d.ts +28 -0
- package/dist/src/claude_invoker.js +68 -7
- package/dist/src/claude_invoker.js.map +1 -1
- package/dist/src/cli.d.ts +44 -0
- package/dist/src/cli.js +358 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/cli_entry.d.ts +2 -0
- package/dist/src/cli_entry.js +23 -0
- package/dist/src/cli_entry.js.map +1 -0
- package/dist/src/composition.d.ts +2 -0
- package/dist/src/composition.js.map +1 -1
- package/dist/src/fs_atomic.d.ts +1 -0
- package/dist/src/fs_atomic.js +34 -0
- package/dist/src/fs_atomic.js.map +1 -0
- package/dist/src/genome_schema.d.ts +5 -1
- package/dist/src/genome_schema.js +12 -1
- package/dist/src/genome_schema.js.map +1 -1
- package/dist/src/genome_writer.d.ts +2 -2
- package/dist/src/genome_writer.js +17 -4
- package/dist/src/genome_writer.js.map +1 -1
- package/dist/src/gig_tracker.d.ts +29 -1
- package/dist/src/gig_tracker.js +27 -0
- package/dist/src/gig_tracker.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/ledger.d.ts +3 -0
- package/dist/src/ledger.js.map +1 -1
- package/dist/src/loader.d.ts +2 -0
- package/dist/src/loader.js +10 -0
- package/dist/src/loader.js.map +1 -1
- package/dist/src/mcp.js +82 -20
- package/dist/src/mcp.js.map +1 -1
- package/dist/src/outputs.d.ts +74 -0
- package/dist/src/outputs.js +106 -71
- package/dist/src/outputs.js.map +1 -1
- package/dist/src/registry.d.ts +13 -0
- package/dist/src/registry.js +112 -0
- package/dist/src/registry.js.map +1 -1
- package/dist/src/reuse.d.ts +276 -0
- package/dist/src/reuse.js +215 -0
- package/dist/src/reuse.js.map +1 -0
- package/dist/src/runtime.d.ts +157 -4
- package/dist/src/runtime.js +519 -30
- package/dist/src/runtime.js.map +1 -1
- package/dist/src/server.d.ts +20 -0
- package/dist/src/server.js +733 -28
- package/dist/src/server.js.map +1 -1
- package/dist/src/server_relay.d.ts +2 -0
- package/dist/src/server_relay.js +31 -8
- package/dist/src/server_relay.js.map +1 -1
- package/dist/src/skill_runner.mjs +8 -2
- package/dist/src/skill_subprocess.d.ts +47 -5
- package/dist/src/skill_subprocess.js +242 -11
- package/dist/src/skill_subprocess.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +11 -7
- package/dist/src/version.js.map +1 -1
- 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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"}
|
package/dist/src/runtime.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
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
|