@eir-labs/coltrane 0.4.0 → 0.5.0
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/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/skill_runner.mjs +8 -2
- package/dist/src/skill_subprocess.d.ts +55 -5
- package/dist/src/skill_subprocess.js +215 -11
- package/dist/src/skill_subprocess.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +6 -4
package/dist/src/runtime.js
CHANGED
|
@@ -5,13 +5,15 @@
|
|
|
5
5
|
// that carries model_version + (empty, v0) eval_scores — honestly un-tempered.
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
import { PRIMITIVE_OUTPUT_TYPE, CORE_TYPES } from "./core_types.js";
|
|
8
|
-
import {
|
|
8
|
+
import { executeSkillAsync } from "./skill_subprocess.js";
|
|
9
9
|
import { loadSkillPackage } from "./skills.js";
|
|
10
|
+
import { resolveModel } from "./claude_invoker.js";
|
|
10
11
|
// core type → the process primitive that produces it (reverse of PRIMITIVE_OUTPUT_TYPE).
|
|
11
12
|
// A skill-backed chair seals its output as this primitive/core when its output_contract is
|
|
12
13
|
// a core type.
|
|
13
14
|
const CORE_TO_PRIMITIVE = Object.fromEntries(Object.entries(PRIMITIVE_OUTPUT_TYPE).map(([prim, core]) => [String(core), prim]));
|
|
14
15
|
import { sha256Hex, canonJson, runFingerprint, outputContentHash, CANONICAL_FORM_VERSION } from "./canonical_form.js";
|
|
16
|
+
import { producersSha, reuseCacheKey, checkReuseEntry, runIdentityMismatch, CHECKPOINT_SCHEMA_VERSION, REUSE_SCHEMA_VERSION, } from "./reuse.js";
|
|
15
17
|
import { LEDGER_SCHEMA_VERSION } from "./ledger.js";
|
|
16
18
|
/**
|
|
17
19
|
* #236 — settled spend used to be discarded on every failed gig: `usage` was written only on
|
|
@@ -34,6 +36,26 @@ export function partialBudgetState(e) {
|
|
|
34
36
|
}
|
|
35
37
|
export class RuntimeError extends Error {
|
|
36
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* A resume was requested and cannot be honoured. Thrown BEFORE any chair is prepared, so a
|
|
41
|
+
* refused resume costs nothing.
|
|
42
|
+
*
|
|
43
|
+
* Distinct from RuntimeError because it is not a crash and not a composition defect — it is
|
|
44
|
+
* the engine declining to splice two runs together. `drift` names exactly which identity
|
|
45
|
+
* fields disagree, so "it refused" is never the whole answer an operator gets.
|
|
46
|
+
*/
|
|
47
|
+
export class ResumeRefused extends Error {
|
|
48
|
+
gig_id;
|
|
49
|
+
drift;
|
|
50
|
+
constructor(gig_id, why, drift = []) {
|
|
51
|
+
super(`ResumeRefused: cannot resume gig "${gig_id}" — ${why}` +
|
|
52
|
+
(drift.length > 0 ? ` [${drift.join("; ")}]` : "") +
|
|
53
|
+
`. Re-dispatch without resume_from to run this cold.`);
|
|
54
|
+
this.name = "ResumeRefused";
|
|
55
|
+
this.gig_id = gig_id;
|
|
56
|
+
this.drift = drift;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
37
59
|
/**
|
|
38
60
|
* Raised when a gig is cancelled through `RunDeps.signal` (#249). Distinct from RuntimeError
|
|
39
61
|
* so a caller can tell "an operator stopped this" from "this crashed" — #251's point that a
|
|
@@ -179,9 +201,25 @@ function outputSatisfiesType(output, declared) {
|
|
|
179
201
|
return false;
|
|
180
202
|
}
|
|
181
203
|
export async function runGig(standard, gigInput, deps) {
|
|
182
|
-
|
|
204
|
+
// A resumed run CONTINUES the gig it resumes: same id, so the restored outputs stay in-gig
|
|
205
|
+
// and `OutputStore.trace` (which scopes its walk to one gig_id) still reaches them. Two ids
|
|
206
|
+
// for one gig would make the provenance chain end at the resume boundary.
|
|
207
|
+
if (deps.resume_from !== undefined && deps.gig_id !== undefined && deps.gig_id !== deps.resume_from) {
|
|
208
|
+
throw new ResumeRefused(deps.resume_from, `the caller supplied a different gig_id ("${deps.gig_id}") — a resumed run continues the gig it resumes, it does not fork one`);
|
|
209
|
+
}
|
|
210
|
+
const gig_id = deps.resume_from ?? deps.gig_id ?? randomUUID();
|
|
183
211
|
const started_at = new Date().toISOString();
|
|
184
212
|
const produced = [];
|
|
213
|
+
// Hoisted: `genome_hash` used to be computed at the very end, purely for the ledger row.
|
|
214
|
+
// Both halves of reuse need it at t=0 — it is the field that decides whether two runs are
|
|
215
|
+
// the same pipeline, and a gate that fires after the money is spent is not a gate.
|
|
216
|
+
const genome_hash = genomeHash(standard);
|
|
217
|
+
// Hash the gig input LAZILY. Three callers want it now (#196's provenance backfill, the
|
|
218
|
+
// resume identity, and the reuse key) but a hostile or circular payload must not be
|
|
219
|
+
// canonicalized on a run that never needs it — which is every run that uses none of the
|
|
220
|
+
// three. Memoized, so it is computed at most once.
|
|
221
|
+
let gigInputShaCache;
|
|
222
|
+
const gigInputSha = () => (gigInputShaCache ??= sha256Hex(canonJson(gigInput)));
|
|
185
223
|
// #195 — settled model spend, accumulated from each agent invocation's `result` event (the
|
|
186
224
|
// stream-json result carries usage + total_cost_usd + a per-model breakdown). These were
|
|
187
225
|
// forwarded to onEvent but dropped; we fold them here and persist on the ledger entry. JS is
|
|
@@ -402,6 +440,168 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
402
440
|
// #243 — chairs that sealed fewer types than their output_contract promised, collected for
|
|
403
441
|
// the manifest. Recording, not enforcement: see the seal loop.
|
|
404
442
|
const unfulfilledOutputs = [];
|
|
443
|
+
// ── reuse a sealed output instead of re-deriving it ────────────────────────────────────
|
|
444
|
+
//
|
|
445
|
+
// Everything a run skipped, and why. Populated by BOTH halves, because from the manifest's
|
|
446
|
+
// point of view they are the same event: a chair that did not run, and the sealed output
|
|
447
|
+
// that stood in for it.
|
|
448
|
+
const skipped = [];
|
|
449
|
+
const reuseReport = { hits: [], rejected: [], writes: 0, write_errors: [] };
|
|
450
|
+
let resumedFrom;
|
|
451
|
+
let checkpointError;
|
|
452
|
+
/**
|
|
453
|
+
* What "the same run" means, computed once. See RunIdentity in src/reuse.ts for why each
|
|
454
|
+
* field is here and why `run_fingerprint` is not.
|
|
455
|
+
*/
|
|
456
|
+
/**
|
|
457
|
+
* Every resolved skill's verified code_hash, slug-keyed. The code IS the producer for a
|
|
458
|
+
* skill chair, and `meta.version` can stay put across a rewrite — `loadSkillPackage`
|
|
459
|
+
* computes the hash from the bytes, which is what makes this honest.
|
|
460
|
+
*/
|
|
461
|
+
const resolvedSkillHashes = () => {
|
|
462
|
+
const out = [];
|
|
463
|
+
for (const [slug, dir] of deps.skill_dirs ?? []) {
|
|
464
|
+
try {
|
|
465
|
+
const pkg = loadSkillPackage(dir);
|
|
466
|
+
out.push({ slug, code_hash: pkg.codeHash ?? "" });
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
// Unreadable here means unusable at dispatch too; record the absence rather than
|
|
470
|
+
// silently folding nothing, so a skill that vanished moves the identity.
|
|
471
|
+
out.push({ slug, code_hash: "<unreadable>" });
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return out;
|
|
475
|
+
};
|
|
476
|
+
const identity = () => ({
|
|
477
|
+
standard_slug: standard.slug,
|
|
478
|
+
genome_hash,
|
|
479
|
+
// #278 review — genome_hash does NOT see an agent's identity/method/constraints/tools,
|
|
480
|
+
// nor a skill's code. Those are the producer, and editing one under a stable slug is the
|
|
481
|
+
// ordinary response to a bad run. Without this the resume gate accepted exactly that.
|
|
482
|
+
producers_sha: producersSha({ agents: standard.agents, skills: resolvedSkillHashes() }),
|
|
483
|
+
gig_input_sha: gigInputSha(),
|
|
484
|
+
model_version: deps.model_version ?? "unknown",
|
|
485
|
+
depth: deps.depth ?? "",
|
|
486
|
+
canonical_form_version: CANONICAL_FORM_VERSION,
|
|
487
|
+
});
|
|
488
|
+
// Roles restored from a checkpoint: they are already sealed, so they never enter a phase's
|
|
489
|
+
// `remaining` map and are never prepared, budgeted or invoked.
|
|
490
|
+
const restoredRoles = new Map();
|
|
491
|
+
// The checkpoint we will WRITE, accumulated as chairs complete. Seeded from the checkpoint we
|
|
492
|
+
// READ, so a second failure does not throw away the first attempt's progress — otherwise a
|
|
493
|
+
// gig that failed twice would be resumable only back to the second attempt's starting point.
|
|
494
|
+
const checkpointRoles = new Map();
|
|
495
|
+
let checkpointStartedAt = started_at;
|
|
496
|
+
if (deps.resume_from !== undefined) {
|
|
497
|
+
if (!deps.checkpoints) {
|
|
498
|
+
throw new ResumeRefused(gig_id, "no checkpoint store is wired, so there is nothing to resume from");
|
|
499
|
+
}
|
|
500
|
+
let cp;
|
|
501
|
+
try {
|
|
502
|
+
cp = deps.checkpoints.read(gig_id);
|
|
503
|
+
}
|
|
504
|
+
catch (e) {
|
|
505
|
+
throw new ResumeRefused(gig_id, `its checkpoint could not be read — ${e instanceof Error ? e.message : String(e)}`);
|
|
506
|
+
}
|
|
507
|
+
if (!cp)
|
|
508
|
+
throw new ResumeRefused(gig_id, "no checkpoint exists for it (nothing was ever recorded as complete)");
|
|
509
|
+
if (cp.schema_version !== CHECKPOINT_SCHEMA_VERSION) {
|
|
510
|
+
throw new ResumeRefused(gig_id, `its checkpoint is schema v${cp.schema_version} and this engine reads v${CHECKPOINT_SCHEMA_VERSION}`);
|
|
511
|
+
}
|
|
512
|
+
// THE GATE. A resume into a moved genome would have chairs from genome B consuming sealed
|
|
513
|
+
// outputs from genome A, and nothing in input_shas / genome_hash / run_fingerprint would
|
|
514
|
+
// record that it happened — the manifest would describe a system that never existed.
|
|
515
|
+
const drift = runIdentityMismatch(cp.identity, identity());
|
|
516
|
+
if (drift.length > 0) {
|
|
517
|
+
throw new ResumeRefused(gig_id, "it was checkpointed under a different run identity", drift);
|
|
518
|
+
}
|
|
519
|
+
const rolesInStandard = new Set(standard.phases.flatMap((p) => p.chairs.map((c) => c.role)));
|
|
520
|
+
for (const r of cp.roles) {
|
|
521
|
+
// Unreachable past the genome gate (roles live in `standard.phases`, which genomeHash
|
|
522
|
+
// folds) — but "provably impossible" is not a reason to inject silently if it happens.
|
|
523
|
+
if (!rolesInStandard.has(r.role)) {
|
|
524
|
+
throw new ResumeRefused(gig_id, `its checkpoint names role "${r.role}", which this standard does not define`);
|
|
525
|
+
}
|
|
526
|
+
const records = [];
|
|
527
|
+
for (let i = 0; i < r.output_ids.length; i++) {
|
|
528
|
+
const id = r.output_ids[i];
|
|
529
|
+
const rec = deps.outputs.get(id);
|
|
530
|
+
if (!rec) {
|
|
531
|
+
throw new ResumeRefused(gig_id, `its checkpoint names output "${id}" for role "${r.role}", which the output store no longer holds`);
|
|
532
|
+
}
|
|
533
|
+
if (rec.content_sha !== r.content_shas[i]) {
|
|
534
|
+
throw new ResumeRefused(gig_id, `output "${id}" (role "${r.role}") has a different content_sha than the checkpoint recorded — the store moved under it`);
|
|
535
|
+
}
|
|
536
|
+
// genomeHash folds the standard and its agents; it does NOT fold the domain-type
|
|
537
|
+
// registry. So a type that changed shape between attempts is invisible to the gate
|
|
538
|
+
// above, and its already-sealed records would be injected into a run whose validator
|
|
539
|
+
// no longer agrees with them. Same fingerprint tool the chair-level cache uses.
|
|
540
|
+
const fp = deps.outputs.typeFingerprint(rec.domain_type);
|
|
541
|
+
if (fp === "") {
|
|
542
|
+
throw new ResumeRefused(gig_id, `the registry can no longer describe type "${rec.domain_type}" (role "${r.role}"), so its sealed output cannot be checked`);
|
|
543
|
+
}
|
|
544
|
+
if (fp !== r.type_fingerprints[i]) {
|
|
545
|
+
throw new ResumeRefused(gig_id, `type "${rec.domain_type}" (role "${r.role}") has changed shape since that output was sealed`);
|
|
546
|
+
}
|
|
547
|
+
records.push(rec);
|
|
548
|
+
}
|
|
549
|
+
restoredRoles.set(r.role, { phase: r.phase, records });
|
|
550
|
+
checkpointRoles.set(r.role, r);
|
|
551
|
+
}
|
|
552
|
+
checkpointStartedAt = cp.started_at;
|
|
553
|
+
resumedFrom = {
|
|
554
|
+
from_gig_id: gig_id,
|
|
555
|
+
checkpoint_at: cp.updated_at,
|
|
556
|
+
roles: cp.roles.map((r) => ({ phase: r.phase, role: r.role, output_types: [...r.domain_types] })),
|
|
557
|
+
outputs_restored: [...restoredRoles.values()].reduce((n, v) => n + v.records.length, 0),
|
|
558
|
+
...(cp.prior_usage !== undefined ? { prior_usage: cp.prior_usage } : {}),
|
|
559
|
+
};
|
|
560
|
+
emit({
|
|
561
|
+
type: "gig_resumed", from_gig_id: gig_id,
|
|
562
|
+
roles: [...restoredRoles.keys()], outputs: resumedFrom.outputs_restored,
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
/** Record a completed chair against the checkpoint we will write. */
|
|
566
|
+
function noteCheckpointRole(role, phaseName, records) {
|
|
567
|
+
if (!deps.checkpoints || records.length === 0)
|
|
568
|
+
return;
|
|
569
|
+
checkpointRoles.set(role, {
|
|
570
|
+
role, phase: phaseName,
|
|
571
|
+
output_ids: records.map((r) => r.id),
|
|
572
|
+
content_shas: records.map((r) => r.content_sha),
|
|
573
|
+
domain_types: records.map((r) => r.domain_type),
|
|
574
|
+
type_fingerprints: records.map((r) => deps.outputs.typeFingerprint(r.domain_type)),
|
|
575
|
+
sealed_at: new Date().toISOString(),
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Flush the checkpoint. Called at every dispatch-batch boundary — including BEFORE the throw
|
|
580
|
+
* that a failed batch raises, so a batch whose siblings succeeded still banks them.
|
|
581
|
+
*
|
|
582
|
+
* Swallow-and-report: a checkpoint write that fails must not kill a run that is otherwise
|
|
583
|
+
* fine (the money is already spent), but it must not be invisible either — a caller who
|
|
584
|
+
* believes the run is resumable and is wrong finds out at the worst possible moment.
|
|
585
|
+
*/
|
|
586
|
+
function saveCheckpoint() {
|
|
587
|
+
if (!deps.checkpoints || checkpointRoles.size === 0)
|
|
588
|
+
return;
|
|
589
|
+
try {
|
|
590
|
+
const prior = finalizeUsage();
|
|
591
|
+
deps.checkpoints.write({
|
|
592
|
+
schema_version: CHECKPOINT_SCHEMA_VERSION,
|
|
593
|
+
gig_id,
|
|
594
|
+
identity: identity(),
|
|
595
|
+
started_at: checkpointStartedAt,
|
|
596
|
+
updated_at: new Date().toISOString(),
|
|
597
|
+
roles: [...checkpointRoles.values()],
|
|
598
|
+
...(prior ? { prior_usage: JSON.parse(JSON.stringify(prior)) } : {}),
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
catch (e) {
|
|
602
|
+
checkpointError ??= e instanceof Error ? e.message : String(e);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
405
605
|
try {
|
|
406
606
|
for (const phase of standard.phases) {
|
|
407
607
|
checkpoint(); // between phases — the cheapest place to stop, and the biggest saving
|
|
@@ -412,8 +612,31 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
412
612
|
// into a single RuntimeError naming every failing chair role. Cross-phase
|
|
413
613
|
// depends_on works because `producedByRole` carries across phases.
|
|
414
614
|
const remaining = new Map();
|
|
415
|
-
for (const ch of phase.chairs)
|
|
416
|
-
|
|
615
|
+
for (const ch of phase.chairs) {
|
|
616
|
+
// A role restored from this gig's checkpoint never enters the frontier at all: not
|
|
617
|
+
// prepared, not budgeted, not invoked. Seeding `producedByRole` here (rather than before
|
|
618
|
+
// the phase loop) keeps `produced` in phase order, which the legacy no-depends_on input
|
|
619
|
+
// gathering and ChairSelectionView both read.
|
|
620
|
+
const restored = restoredRoles.get(ch.role);
|
|
621
|
+
if (!restored) {
|
|
622
|
+
remaining.set(ch.role, ch);
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
producedByRole.set(ch.role, restored.records);
|
|
626
|
+
produced.push(...restored.records);
|
|
627
|
+
const row = {
|
|
628
|
+
phase: phase.name, role: ch.role, reason: "resume", source_gig_id: gig_id,
|
|
629
|
+
output_types: restored.records.map((r) => r.domain_type),
|
|
630
|
+
content_shas: restored.records.map((r) => r.content_sha),
|
|
631
|
+
};
|
|
632
|
+
skipped.push(row);
|
|
633
|
+
emit({
|
|
634
|
+
type: "chair_skipped", phase: phase.name, role: ch.role, reason: "resume",
|
|
635
|
+
source_gig_id: gig_id, output_types: row.output_types,
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
if (remaining.size === 0)
|
|
639
|
+
continue; // wholly restored phase — nothing to dispatch
|
|
417
640
|
while (remaining.size > 0) {
|
|
418
641
|
checkpoint(); // between dispatch batches — stops the NEXT topological level from firing
|
|
419
642
|
// Topological level: every chair whose depends_on ⊂ already-produced roles.
|
|
@@ -473,6 +696,7 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
473
696
|
else {
|
|
474
697
|
producedByRole.set(ch.role, r.value);
|
|
475
698
|
produced.push(...r.value);
|
|
699
|
+
noteCheckpointRole(ch.role, phase.name, r.value);
|
|
476
700
|
}
|
|
477
701
|
}
|
|
478
702
|
// #233 — BATCH BOUNDARY is the only point at which real settled dollars can be
|
|
@@ -480,6 +704,10 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
480
704
|
// of them was invoked, so no chair could have seen its siblings' cost. Reporting only.
|
|
481
705
|
if (budget)
|
|
482
706
|
budget.settled_usd = usage.total_cost_usd;
|
|
707
|
+
// Bank progress BEFORE the failure throw below. A batch whose siblings succeeded has
|
|
708
|
+
// durable outputs either way; the checkpoint is what makes them reachable next time,
|
|
709
|
+
// and writing it only on the happy path would forfeit exactly the runs that need it.
|
|
710
|
+
saveCheckpoint();
|
|
483
711
|
if (failures.length > 0) {
|
|
484
712
|
// A cancellation that reached the chair's child (level 3) surfaces here as a rejected
|
|
485
713
|
// chair. Report it as the cancellation it is — not as "chair(s) failed", which is the
|
|
@@ -501,6 +729,93 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
501
729
|
return { domain_type: dt, core_type: core, primitive };
|
|
502
730
|
});
|
|
503
731
|
}
|
|
732
|
+
/**
|
|
733
|
+
* Is there a prior sealed output that stands in for what this chair is about to derive?
|
|
734
|
+
*
|
|
735
|
+
* Runs at PREP time, before the budget gate, because a chair that will not be invoked must
|
|
736
|
+
* not be charged for the context it will not consume. Returns the key on every path — a
|
|
737
|
+
* miss still needs it, since the key is what the post-seal cache write is addressed by.
|
|
738
|
+
*
|
|
739
|
+
* A plain miss (no entry) is silent: nothing was found, there is nothing to say. An entry
|
|
740
|
+
* that was FOUND and refused is always reported, on the event stream and in the manifest —
|
|
741
|
+
* "the cache stopped hitting" must never be something an operator has to guess at.
|
|
742
|
+
*/
|
|
743
|
+
function lookupReuse(a) {
|
|
744
|
+
const store = deps.reuse;
|
|
745
|
+
if (!store)
|
|
746
|
+
return undefined;
|
|
747
|
+
const key = reuseCacheKey({
|
|
748
|
+
standard_slug: standard.slug,
|
|
749
|
+
phase: a.phaseName,
|
|
750
|
+
chair: a.chair,
|
|
751
|
+
agent: a.agent ?? null,
|
|
752
|
+
...(a.skill_provenance !== undefined ? { skill_provenance: a.skill_provenance } : {}),
|
|
753
|
+
skills: a.skills.map((s) => ({
|
|
754
|
+
slug: s.slug,
|
|
755
|
+
version: Number(s.version ?? 0),
|
|
756
|
+
code_hash: s.code_hash ?? "",
|
|
757
|
+
})),
|
|
758
|
+
input_shas: a.inputs.map((i) => i.content_sha),
|
|
759
|
+
gig_input_sha: gigInputSha(),
|
|
760
|
+
model_version: deps.model_version ?? "unknown",
|
|
761
|
+
depth: deps.depth ?? "",
|
|
762
|
+
output_types: a.output_specs.map((s) => s.domain_type),
|
|
763
|
+
canonical_form_version: CANONICAL_FORM_VERSION,
|
|
764
|
+
});
|
|
765
|
+
const reject = (reason, detail) => {
|
|
766
|
+
reuseReport.rejected.push({ phase: a.phaseName, role: a.chair.role, cache_key: key, reason, ...(detail !== undefined ? { detail } : {}) });
|
|
767
|
+
emit({ type: "reuse_rejected", phase: a.phaseName, role: a.chair.role, cache_key: key, reason, ...(detail !== undefined ? { detail } : {}) });
|
|
768
|
+
return { key };
|
|
769
|
+
};
|
|
770
|
+
let entry;
|
|
771
|
+
try {
|
|
772
|
+
entry = store.get(key);
|
|
773
|
+
}
|
|
774
|
+
catch (e) {
|
|
775
|
+
return reject("unreadable", e instanceof Error ? e.message : String(e));
|
|
776
|
+
}
|
|
777
|
+
if (!entry)
|
|
778
|
+
return { key }; // a plain miss — free, and the status quo
|
|
779
|
+
const check = checkReuseEntry(entry, (t) => deps.outputs.typeFingerprint(t));
|
|
780
|
+
if (!check.ok)
|
|
781
|
+
return reject(check.reason ?? "rejected", check.detail);
|
|
782
|
+
// THE AUTHORITATIVE GUARD. Reuse must never become a way to skip a check: #243 made
|
|
783
|
+
// `output_contract` a floor and #263 made `core_type` agree with the registry, and a
|
|
784
|
+
// recalled output owes those invariants exactly as much as a derived one does.
|
|
785
|
+
//
|
|
786
|
+
// Every record is put through `validateWrite` — the SAME gate `write` runs, from the same
|
|
787
|
+
// implementation — and re-hashed under THIS run's resolved core/primitive/domain. Two
|
|
788
|
+
// properties fall out. First, an entry whose bytes no longer satisfy their type is
|
|
789
|
+
// refused rather than injected, whatever made it stale (a genome edit the fingerprint
|
|
790
|
+
// caught, an older engine's looser floor, a hand edit). Second, deciding here — before a
|
|
791
|
+
// single write — is what makes a multi-output chair all-or-nothing: an entry whose second
|
|
792
|
+
// record fails cannot leave its first one durable.
|
|
793
|
+
for (const o of entry.outputs) {
|
|
794
|
+
const spec = a.output_specs.find((s) => s.domain_type === o.domain_type);
|
|
795
|
+
if (!spec)
|
|
796
|
+
return reject("seal-rejected", `the entry carries "${o.domain_type}", which this chair does not seal`);
|
|
797
|
+
const gate = deps.outputs.validateWrite({ core_type: spec.core_type, domain_type: o.domain_type, data: o.data });
|
|
798
|
+
if (!gate.valid)
|
|
799
|
+
return reject("seal-rejected", gate.reason);
|
|
800
|
+
// Re-hashing proves the substitution is content-identical to what the original seal
|
|
801
|
+
// produced. It is also what lets a reused run carry the same `run_fingerprint` as the
|
|
802
|
+
// cold run it stands in for — the claim reuse is implicitly making.
|
|
803
|
+
const sha = outputContentHash({
|
|
804
|
+
core_type: spec.core_type,
|
|
805
|
+
domain_type: o.domain_type,
|
|
806
|
+
domain_type_version: 1,
|
|
807
|
+
domain: a.domain,
|
|
808
|
+
primitive: spec.primitive,
|
|
809
|
+
phase: a.phaseName,
|
|
810
|
+
agent_slug: a.producer_slug,
|
|
811
|
+
data: o.data,
|
|
812
|
+
});
|
|
813
|
+
if (sha !== o.content_sha) {
|
|
814
|
+
return reject("content-sha-mismatch", `re-sealing "${o.domain_type}" here yields a different content_sha than the entry recorded`);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return { key, hit: { cache_key: key, source_gig_id: entry.source_gig_id, outputs: entry.outputs } };
|
|
818
|
+
}
|
|
504
819
|
function prepareChair(chair, phaseName) {
|
|
505
820
|
// A skill-backed chair runs the skill's deterministic code half — no agent, no model.
|
|
506
821
|
if (chair.skill_slug && (chair.agent_slug ?? "") === "") {
|
|
@@ -534,7 +849,29 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
534
849
|
}
|
|
535
850
|
// A skill-backed chair seals exactly one output (its deterministic code returns one blob).
|
|
536
851
|
const output_specs = [{ domain_type, core_type: core, primitive }];
|
|
537
|
-
|
|
852
|
+
// The producer of a skill chair is its CODE, so the key must name the verified code_hash
|
|
853
|
+
// — a skill whose implementation changed under a stable slug is a different producer.
|
|
854
|
+
// Loaded here only when reuse is on, and a load failure degrades to "no key" (a miss)
|
|
855
|
+
// rather than throwing: executeChair raises the real error a moment later, and a
|
|
856
|
+
// prep-time throw would change which layer reports it.
|
|
857
|
+
let skillIdentity;
|
|
858
|
+
if (deps.reuse) {
|
|
859
|
+
try {
|
|
860
|
+
const pkg = loadSkillPackage(dir);
|
|
861
|
+
skillIdentity = { slug: pkg.meta.slug, version: pkg.meta.version, code_hash: pkg.codeHash ?? "", tier: pkg.meta.permission?.tier ?? 0 };
|
|
862
|
+
}
|
|
863
|
+
catch { /* no identity → no key → no reuse for this chair */ }
|
|
864
|
+
}
|
|
865
|
+
const skillReuse = skillIdentity === undefined
|
|
866
|
+
? undefined
|
|
867
|
+
: lookupReuse({ chair, phaseName, inputs, output_specs, skill_provenance: skillIdentity, skills: [], producer_slug: chair.skill_slug, domain: standard.domain });
|
|
868
|
+
return {
|
|
869
|
+
chair, phaseName, skill_dir: dir, primitive, domain_type, output_specs, inputs,
|
|
870
|
+
skills: [], missing_skills: [],
|
|
871
|
+
producer_slug: chair.skill_slug, domain: standard.domain,
|
|
872
|
+
...(skillReuse ? { reuse_key: skillReuse.key } : {}),
|
|
873
|
+
...(skillReuse?.hit ? { reuse_hit: skillReuse.hit } : {}),
|
|
874
|
+
};
|
|
538
875
|
}
|
|
539
876
|
const agent = standard.agents.find((a) => a.slug === chair.agent_slug);
|
|
540
877
|
if (!agent)
|
|
@@ -636,6 +973,20 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
636
973
|
// used to be indistinguishable from a skilled one in the artifact, the ledger AND the diff.
|
|
637
974
|
emit({ type: "skills_unresolved", phase: phaseName, role: chair.role, agent: agent.slug, missing: [...missing] });
|
|
638
975
|
}
|
|
976
|
+
// Seal one record per type THIS CHAIR promises (#174): the output_contract is the SELECTOR,
|
|
977
|
+
// not just a check — a chair bound to a multi-output agent seals only the subset it declares,
|
|
978
|
+
// intersected with the agent's real outputs (so a stray contract entry can't conjure a type
|
|
979
|
+
// the agent doesn't produce; the post-invocation check below still reports that mismatch).
|
|
980
|
+
// Empty contract (legacy hand-rolled chair) → fall back to the agent's full output set.
|
|
981
|
+
const wanted = chair.output_contract.length
|
|
982
|
+
? agent.output_types.filter((t) => chair.output_contract.includes(t))
|
|
983
|
+
: agent.output_types;
|
|
984
|
+
const output_specs = outputSpecsFor(wanted, primitive);
|
|
985
|
+
const domain = agent.domain ?? standard.domain;
|
|
986
|
+
// REUSE LOOKUP — deliberately ABOVE the budget gate. A chair served from cache consumes no
|
|
987
|
+
// context, so charging it (or worse, refusing it for lack of allowance) would be the budget
|
|
988
|
+
// enforcing a cost that is not going to be incurred.
|
|
989
|
+
const lookup = lookupReuse({ chair, phaseName, inputs, output_specs, agent, skills, producer_slug: agent.slug, domain });
|
|
639
990
|
// BUDGET GATE — pre-invocation, and a RESERVATION only (#232). Synchronous so
|
|
640
991
|
// BudgetExhausted (and a TypeError thrown from JSON.stringify on a circular gig_input)
|
|
641
992
|
// propagate unwrapped to the caller rather than being aggregated as a chair failure.
|
|
@@ -647,7 +998,7 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
647
998
|
// charged and `invokeAndWriteChair` then ran for nobody. The operator saw spend for work
|
|
648
999
|
// that never started, and that inflated figure is what BudgetExhausted.state reported.
|
|
649
1000
|
let reservedCost;
|
|
650
|
-
if (budget) {
|
|
1001
|
+
if (budget && !lookup?.hit) {
|
|
651
1002
|
const cost = computeAppendCost({ agent, phase: phaseName, inputs, gig_input: gigInput }, budget.base_cost, budget.k);
|
|
652
1003
|
const available = budget.balance - reserved;
|
|
653
1004
|
if (available < cost) {
|
|
@@ -659,16 +1010,13 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
659
1010
|
reserved += cost;
|
|
660
1011
|
reservedCost = cost;
|
|
661
1012
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
: agent.output_types;
|
|
670
|
-
const output_specs = outputSpecsFor(wanted, primitive);
|
|
671
|
-
return { chair, phaseName, agent, primitive, domain_type, output_specs, inputs, skills, missing_skills: missing, ...(reservedCost !== undefined ? { cost: reservedCost } : {}) };
|
|
1013
|
+
return {
|
|
1014
|
+
chair, phaseName, agent, primitive, domain_type, output_specs, inputs, skills,
|
|
1015
|
+
missing_skills: missing, producer_slug: agent.slug, domain,
|
|
1016
|
+
...(reservedCost !== undefined ? { cost: reservedCost } : {}),
|
|
1017
|
+
...(lookup ? { reuse_key: lookup.key } : {}),
|
|
1018
|
+
...(lookup?.hit ? { reuse_hit: lookup.hit } : {}),
|
|
1019
|
+
};
|
|
672
1020
|
}
|
|
673
1021
|
// #232 — convert a chair's reservation into settled spend, or release it. `spent` moves ONLY
|
|
674
1022
|
// for a chair whose invocation actually returned, which is what the budget contract always
|
|
@@ -702,13 +1050,65 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
702
1050
|
}
|
|
703
1051
|
}
|
|
704
1052
|
async function executeChair(p) {
|
|
705
|
-
const { chair, phaseName, inputs, skills, output_specs } = p;
|
|
1053
|
+
const { chair, phaseName, inputs, skills, output_specs, producer_slug, domain } = p;
|
|
706
1054
|
const t0 = Date.now();
|
|
1055
|
+
// ── REUSE HIT ────────────────────────────────────────────────────────────────────────
|
|
1056
|
+
// Everything that could refuse this was decided at prep, before a byte was written. What
|
|
1057
|
+
// is left is a normal seal: the record is written through the SAME `deps.outputs.write`
|
|
1058
|
+
// gate a derived one crosses, into THIS gig, with THIS gig's `input_refs`/`input_shas`
|
|
1059
|
+
// and provenance edges. The only thing skipped is the invocation.
|
|
1060
|
+
//
|
|
1061
|
+
// No `chair_start` is emitted — the chair did not start. `chair_skipped` is a different
|
|
1062
|
+
// event precisely so a monitor cannot render a recall as a very fast derivation.
|
|
1063
|
+
if (p.reuse_hit) {
|
|
1064
|
+
const hit = p.reuse_hit;
|
|
1065
|
+
const written = [];
|
|
1066
|
+
for (const o of hit.outputs) {
|
|
1067
|
+
const spec = output_specs.find((s) => s.domain_type === o.domain_type);
|
|
1068
|
+
const rec = deps.outputs.write({
|
|
1069
|
+
core_type: spec.core_type,
|
|
1070
|
+
domain_type: o.domain_type,
|
|
1071
|
+
domain,
|
|
1072
|
+
gig_id,
|
|
1073
|
+
agent_slug: producer_slug,
|
|
1074
|
+
from_role: chair.role,
|
|
1075
|
+
phase: phaseName,
|
|
1076
|
+
primitive: spec.primitive,
|
|
1077
|
+
data: o.data,
|
|
1078
|
+
input_refs: inputs.map((i) => i.id),
|
|
1079
|
+
input_shas: inputs.map((i) => i.content_sha),
|
|
1080
|
+
...(o.skill_provenance ? { skill_provenance: o.skill_provenance } : {}),
|
|
1081
|
+
reused_from: { output_id: o.source_output_id, gig_id: hit.source_gig_id, cache_key: hit.cache_key },
|
|
1082
|
+
});
|
|
1083
|
+
for (const i of inputs)
|
|
1084
|
+
deps.outputs.addRef(rec.id, i.id, "derived_from", spec.primitive);
|
|
1085
|
+
written.push(rec);
|
|
1086
|
+
}
|
|
1087
|
+
const types = written.map((w) => w.domain_type);
|
|
1088
|
+
// #278 review — a recalled chair owes the SAME manifest row a derived one does. The
|
|
1089
|
+
// early return skipped the `unfulfilled_outputs` push below, so a declared-optional
|
|
1090
|
+
// shortfall present in the cold run vanished on the reuse hit. The engine's own comment
|
|
1091
|
+
// three lines from that push says a declared-optional absence "is still a fact about
|
|
1092
|
+
// this run", and hiding it here made a reused run's manifest quietly better than the
|
|
1093
|
+
// run it stands in for — while carrying an identical run_fingerprint.
|
|
1094
|
+
const reusedMissing = output_specs.map((sp) => sp.domain_type).filter((t) => !types.includes(t));
|
|
1095
|
+
if (reusedMissing.length > 0) {
|
|
1096
|
+
unfulfilledOutputs.push({ role: chair.role, phase: phaseName, missing: reusedMissing });
|
|
1097
|
+
}
|
|
1098
|
+
skipped.push({
|
|
1099
|
+
phase: phaseName, role: chair.role, reason: "reuse", source_gig_id: hit.source_gig_id,
|
|
1100
|
+
output_types: types, content_shas: written.map((w) => w.content_sha), cache_key: hit.cache_key,
|
|
1101
|
+
});
|
|
1102
|
+
reuseReport.hits.push({ phase: phaseName, role: chair.role, cache_key: hit.cache_key, source_gig_id: hit.source_gig_id, output_types: types });
|
|
1103
|
+
emit({
|
|
1104
|
+
type: "chair_skipped", phase: phaseName, role: chair.role, reason: "reuse",
|
|
1105
|
+
source_gig_id: hit.source_gig_id, output_types: types, cache_key: hit.cache_key,
|
|
1106
|
+
});
|
|
1107
|
+
return written;
|
|
1108
|
+
}
|
|
707
1109
|
const producerHint = chair.skill_slug || p.agent?.slug || chair.agent_slug || chair.role;
|
|
708
1110
|
emit({ type: "chair_start", phase: phaseName, role: chair.role, producer: producerHint });
|
|
709
1111
|
let data;
|
|
710
|
-
let producer_slug;
|
|
711
|
-
let domain;
|
|
712
1112
|
// Skill-backed chairs record which skill (version + verified code_hash + tier) sealed the
|
|
713
1113
|
// output, so the ledger entry traces back to the exact SkillChainEvent. Undefined for agents.
|
|
714
1114
|
let skill_provenance;
|
|
@@ -718,12 +1118,16 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
718
1118
|
// when it's a root chair). This is the proper fix for "an LLM should not babysit a
|
|
719
1119
|
// deterministic command": the command IS the chair.
|
|
720
1120
|
const skillInput = inputs.length > 0 ? Object.assign({}, ...inputs.map((i) => i.data)) : gigInput;
|
|
721
|
-
|
|
1121
|
+
// #253 — the ASYNC path, threaded with the run's abort signal. `executeSkill` uses
|
|
1122
|
+
// spawnSync, which blocks the event loop for the skill's whole timeout (120s by
|
|
1123
|
+
// default), so the cooperative abort chain could not run and the abort event could not
|
|
1124
|
+
// even be DELIVERED. `gig_abort` during a skill chair was a promise the engine could
|
|
1125
|
+
// not keep — #249's shape again, but a missing opportunity to kill rather than a
|
|
1126
|
+
// missing kill.
|
|
1127
|
+
const r = await executeSkillAsync(p.skill_dir, skillInput, 120_000, { signal: deps.signal });
|
|
722
1128
|
if (!r.ok)
|
|
723
1129
|
throw new RuntimeError(`skill chair "${chair.role}" ("${chair.skill_slug}") failed: ${r.error}`);
|
|
724
1130
|
data = (r.output && typeof r.output === "object" ? r.output : {});
|
|
725
|
-
producer_slug = chair.skill_slug;
|
|
726
|
-
domain = standard.domain;
|
|
727
1131
|
const pkg = loadSkillPackage(p.skill_dir);
|
|
728
1132
|
skill_provenance = {
|
|
729
1133
|
slug: pkg.meta.slug,
|
|
@@ -767,8 +1171,6 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
767
1171
|
}
|
|
768
1172
|
}
|
|
769
1173
|
}
|
|
770
|
-
producer_slug = agent.slug;
|
|
771
|
-
domain = agent.domain ?? standard.domain;
|
|
772
1174
|
}
|
|
773
1175
|
// Seal one record per type this chair seals. The invoker blob may be keyed by domain_type
|
|
774
1176
|
// (a SENSE+JUDGE agent returns { hit: {...}, verdict: {...} } → a Signal AND a Judgment from
|
|
@@ -814,10 +1216,6 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
814
1216
|
set.add(inp.content_sha);
|
|
815
1217
|
shasByType.set(inp.domain_type, set);
|
|
816
1218
|
}
|
|
817
|
-
// Hash the gig input lazily — only when a placeholder actually resolves to it (most outputs have
|
|
818
|
-
// no *_sha fields, and a hostile/circular gig input shouldn't be canonicalized unless needed).
|
|
819
|
-
let gigInputShaCache;
|
|
820
|
-
const gigInputSha = () => (gigInputShaCache ??= sha256Hex(canonJson(gigInput)));
|
|
821
1219
|
const resolveSha = (field) => {
|
|
822
1220
|
const bare = field.replace(/_sha$/i, "");
|
|
823
1221
|
const only = (type) => {
|
|
@@ -926,7 +1324,31 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
926
1324
|
`of promised [${promised.join(", ")}]. ` +
|
|
927
1325
|
`If a type is legitimately conditional, declare it in the chair's optional_outputs.`);
|
|
928
1326
|
}
|
|
929
|
-
// Contract satisfied
|
|
1327
|
+
// Contract satisfied — and now the SEAL gates, still before anything is durable.
|
|
1328
|
+
//
|
|
1329
|
+
// #243 moved the contract checks ahead of the writes, which stopped a chair that
|
|
1330
|
+
// under-delivered from leaving orphans behind. It did not close the whole hole:
|
|
1331
|
+
// `write()` also validates (core agreement #263, the registry schema, the #227/#228
|
|
1332
|
+
// substance floor), so a chair whose SECOND output failed one of those had already
|
|
1333
|
+
// flushed its first to `outputs/<gig_id>.jsonl`. Same outcome by a different door —
|
|
1334
|
+
// sealed records belonging to a gig that failed, and two audit surfaces disagreeing by
|
|
1335
|
+
// construction.
|
|
1336
|
+
//
|
|
1337
|
+
// `validateWrite` is the gate `write` runs, asked as a question instead. The reuse path
|
|
1338
|
+
// already uses it to make a multi-output entry all-or-nothing; the derived path gets it
|
|
1339
|
+
// for exactly the same reason.
|
|
1340
|
+
for (const { spec, slice } of resolved) {
|
|
1341
|
+
const check = deps.outputs.validateWrite({
|
|
1342
|
+
core_type: spec.core_type,
|
|
1343
|
+
domain_type: spec.domain_type,
|
|
1344
|
+
data: slice,
|
|
1345
|
+
});
|
|
1346
|
+
if (!check.valid) {
|
|
1347
|
+
throw new RuntimeError(`chair "${chair.role}" cannot seal "${spec.domain_type}": ${check.reason}. ` +
|
|
1348
|
+
`Nothing was written — a chair's outputs are all-or-nothing.`);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
// Only now does anything become durable.
|
|
930
1352
|
const written = [];
|
|
931
1353
|
for (const { spec, slice } of resolved) {
|
|
932
1354
|
const rec = deps.outputs.write({
|
|
@@ -941,12 +1363,60 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
941
1363
|
data: slice,
|
|
942
1364
|
input_refs: inputs.map((i) => i.id),
|
|
943
1365
|
input_shas: inputs.map((i) => i.content_sha), // #196 — real predecessor hashes, engine-stamped
|
|
1366
|
+
// WHICH model produced this, resolved through the invoker's own function so the stamp
|
|
1367
|
+
// and the spawn cannot disagree. Absent for a skill-backed chair — no model ran, and
|
|
1368
|
+
// absent must mean unknown rather than "the default".
|
|
1369
|
+
...(p.agent
|
|
1370
|
+
? {
|
|
1371
|
+
model: resolveModel(p.agent.model_tier, deps.model_version),
|
|
1372
|
+
...(p.agent.model_tier ? { model_tier: p.agent.model_tier } : {}),
|
|
1373
|
+
}
|
|
1374
|
+
: {}),
|
|
944
1375
|
skill_provenance,
|
|
945
1376
|
});
|
|
946
1377
|
for (const i of inputs)
|
|
947
1378
|
deps.outputs.addRef(rec.id, i.id, "derived_from", spec.primitive);
|
|
948
1379
|
written.push(rec);
|
|
949
1380
|
}
|
|
1381
|
+
// Populate the cache. Only from a DERIVED chair (a recall has nothing new to record) and
|
|
1382
|
+
// only when this run opted in — the store is cross-gig by construction, so writing to it
|
|
1383
|
+
// is the decision that this run's outputs may stand in for another's.
|
|
1384
|
+
//
|
|
1385
|
+
// A write failure is recorded, not raised: the run is complete and correct either way, and
|
|
1386
|
+
// killing a finished $6 gig because a cache file would not persist is the wrong trade. It
|
|
1387
|
+
// is still not silent — a cache nobody can write is one an operator should know about.
|
|
1388
|
+
if (deps.reuse && p.reuse_key && !p.reuse_hit && written.length > 0) {
|
|
1389
|
+
const entryOutputs = [];
|
|
1390
|
+
let cacheable = true;
|
|
1391
|
+
for (const w of written) {
|
|
1392
|
+
const fp = deps.outputs.typeFingerprint(w.domain_type);
|
|
1393
|
+
// An entry whose type cannot be described could never be validated on read, so it
|
|
1394
|
+
// would be refused there. Not writing it is the same decision, made earlier.
|
|
1395
|
+
if (fp === "") {
|
|
1396
|
+
cacheable = false;
|
|
1397
|
+
break;
|
|
1398
|
+
}
|
|
1399
|
+
entryOutputs.push({
|
|
1400
|
+
core_type: w.core_type, domain_type: w.domain_type, domain: w.domain,
|
|
1401
|
+
primitive: w.primitive, agent_slug: w.agent_slug, phase: phaseName,
|
|
1402
|
+
data: w.data, content_sha: w.content_sha, type_fingerprint: fp, source_output_id: w.id,
|
|
1403
|
+
...(w.skill_provenance ? { skill_provenance: w.skill_provenance } : {}),
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
if (cacheable) {
|
|
1407
|
+
try {
|
|
1408
|
+
deps.reuse.put({
|
|
1409
|
+
schema_version: REUSE_SCHEMA_VERSION, cache_key: p.reuse_key,
|
|
1410
|
+
source_gig_id: gig_id, source_role: chair.role,
|
|
1411
|
+
created_at: new Date().toISOString(), outputs: entryOutputs,
|
|
1412
|
+
});
|
|
1413
|
+
reuseReport.writes++;
|
|
1414
|
+
}
|
|
1415
|
+
catch (e) {
|
|
1416
|
+
reuseReport.write_errors.push({ phase: phaseName, role: chair.role, reason: e instanceof Error ? e.message : String(e) });
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
950
1420
|
// A DECLARED-optional absence is still a fact about this run. Legitimising a shortfall is
|
|
951
1421
|
// not the same as hiding it, so it keeps its row in the manifest.
|
|
952
1422
|
if (missing.length > 0)
|
|
@@ -960,7 +1430,6 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
960
1430
|
});
|
|
961
1431
|
return written;
|
|
962
1432
|
}
|
|
963
|
-
const genome_hash = genomeHash(standard);
|
|
964
1433
|
// Content-address each output (not its random UUID) so the fingerprint is
|
|
965
1434
|
// reproducible: an honest replay of the same outputs recomputes the same
|
|
966
1435
|
// hashes, while changed content shifts them. See outputContentHash.
|
|
@@ -1018,6 +1487,14 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
1018
1487
|
budget.agent_state = "settled";
|
|
1019
1488
|
budget.settled_usd = usage.total_cost_usd; // #233 — final reconciliation of REAL dollars
|
|
1020
1489
|
}
|
|
1490
|
+
// The gig finished, so there is nothing left to resume — drop its checkpoint. Without this
|
|
1491
|
+
// every gig a deployment ever runs leaves a file behind forever. Only the SUCCESS path clears
|
|
1492
|
+
// it: a failed or aborted run's checkpoint is exactly what a later resume reads, and this line
|
|
1493
|
+
// is not reached on either.
|
|
1494
|
+
try {
|
|
1495
|
+
deps.checkpoints?.remove(gig_id);
|
|
1496
|
+
}
|
|
1497
|
+
catch { /* reclaiming disk must not fail a run that succeeded */ }
|
|
1021
1498
|
const result = { gig_id, standard_slug: standard.slug, genome_hash, run_fingerprint, outputs: produced, eval_scores, status: "complete" };
|
|
1022
1499
|
if (settledUsage)
|
|
1023
1500
|
result.usage = settledUsage;
|
|
@@ -1027,6 +1504,18 @@ export async function runGig(standard, gigInput, deps) {
|
|
|
1027
1504
|
result.unresolved_evals = unresolved_evals;
|
|
1028
1505
|
if (unfulfilledOutputs.length > 0)
|
|
1029
1506
|
result.unfulfilled_outputs = unfulfilledOutputs;
|
|
1507
|
+
// Say what was skipped and why. The ABSENCE of these fields is itself a claim — that every
|
|
1508
|
+
// chair in this manifest ran — so they are present only when there is something to report,
|
|
1509
|
+
// and `reuse` is present whenever the cache was wired even if nothing hit (a zero-hit run is
|
|
1510
|
+
// a fact about the cache, not an absence of one).
|
|
1511
|
+
if (skipped.length > 0)
|
|
1512
|
+
result.skipped = skipped;
|
|
1513
|
+
if (resumedFrom)
|
|
1514
|
+
result.resumed_from = resumedFrom;
|
|
1515
|
+
if (deps.reuse)
|
|
1516
|
+
result.reuse = reuseReport;
|
|
1517
|
+
if (checkpointError !== undefined)
|
|
1518
|
+
result.checkpoint_error = checkpointError;
|
|
1030
1519
|
emit({ type: "gig_complete", outputs: produced.length });
|
|
1031
1520
|
return result;
|
|
1032
1521
|
}
|