@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
@@ -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 { executeSkill } from "./skill_subprocess.js";
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
- const gig_id = deps.gig_id ?? randomUUID();
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
- remaining.set(ch.role, ch);
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
- return { chair, phaseName, skill_dir: dir, primitive, domain_type, output_specs, inputs, skills: [], missing_skills: [] };
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
- // Seal one record per type THIS CHAIR promises (#174): the output_contract is the SELECTOR,
663
- // not just a check — a chair bound to a multi-output agent seals only the subset it declares,
664
- // intersected with the agent's real outputs (so a stray contract entry can't conjure a type
665
- // the agent doesn't produce; the post-invocation check below still reports that mismatch).
666
- // Empty contract (legacy hand-rolled chair) fall back to the agent's full output set.
667
- const wanted = chair.output_contract.length
668
- ? agent.output_types.filter((t) => chair.output_contract.includes(t))
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
- const r = executeSkill(p.skill_dir, skillInput);
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. Only now does anything become durable.
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
  }