Compare commits

..
Author SHA1 Message Date
Meta Vens
17288afa18 Merge readme-front-door: README opens with value, problem, entry points, live versus planned 2026-10-22 20:02:24 +02:00
Meta Vens
9359761672 README: front door with value, problem, entry points, live versus planned
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 20:02:11 +02:00
Meta Vens
ef9568655e Merge eureka-substrate leaf-key-bound: resolutions get their own derived bound
Eureka eureka-substrate cut leaf-key-bound. Soul verdict
eureka-substrate:verdict:cut-leaf-key-bound.s1 holds. Subject locals stay
at 64; resolution locals are bounded at 111 (derived); ids parse by kind;
Sha::names_same_commit. Every stored key re-derives byte for byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 11:34:22 +02:00
Meta Vens
b48e132119 epiphany-pipeline: tests build 111-byte locals from dotted labels
The previous commit (34099183) does not pass its own tests: two of them
built a wide local as one label, which is itself bounded at 64. This commit
builds them as dotted parts. The commit before it builds; two of its tests
fail.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 11:12:24 +02:00
Meta Vens
34099183b3 epiphany-pipeline: per-kind local bound; ids parse by kind; Sha names a commit by prefix
LOCAL_MAX (one 64-byte bound for every kind) is deleted. Subject locals stay
at SUBJECT_LOCAL_MAX = 64, so no stored key moves. A resolution's local gets
RESOLUTION_LOCAL_MAX = 111, derived from the subject bound and the kind names
(a depth-two chain over a maximal subject at u32::MAX sequences), with a const
assert that an id still fits a Short. local_max(kind) is the one place the
bound is chosen; pipeline_key's composer and pipeline_id's parser both read
it, so every key pipeline_key derives is an id pipeline_id accepts.

Sha::names_same_commit: one spelling is a prefix of the other.

Tests: every_resolvable_kind_resolves_two_deep, the resolution side of the
whole-local bound, parser bound by kind, sha_names_same_commit, and the
#[ignore]d stored_documents_read_back over a real mind snapshot
(HUGINN_MIND_SNAPSHOT). The old depth-arithmetic test body is replaced.

Build status is shown by the verify run that follows. Operator ruling eureka-substrate:ruling:key-bound (per-kind).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 11:10:01 +02:00
Meta Vens
2bf39eef34 Merge codex/eureka-pipeline-state into main (eureka-substrate Cut 0a)
Lands the live Eureka pipeline branch on main per operator ruling
eureka-substrate:ruling:merge-to-main-first ("Merge to main first"),
cut spec eureka-substrate:cut_spec:cut-merge-epiphany-main.r1.
The merged tree equals the branch tip's tree (a75aa388); the branch is
kept and receives no further commits.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:58:26 +02:00
Meta Vens
4006b14bf2 Huginn map: Cut 15 merged; first typed campaign is the Idunn watchdog
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:42:27 +02:00
Meta Vens
73edee5cda Pin the pipeline leaf to CultLib 8fc74c7
Huginn moves to CultLib 8fc74c7 for the RUDP client bind fix, and the
pipeline leaf's CultCache types cross Huginn's API, so the leaf moves with
it. cultcache-rs changes between the revs are reader-side only: named
refusals for variant stores; the v1 formats read as before.

The lock also takes the Ghostlight pin that epiphany-core/Cargo.toml
already named (22937b71); the lock still carried 22281891.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:29:56 +02:00
Meta Vens
74804d8362 Huginn map: Cut 14 live; client bind defect blocks Cut 15
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:31:08 +02:00
Meta Vens
5ab7dcd8ae Huginn map: Q-H1 reversed to A; Cut 14 interim unit goes now
Operator: "I want us to be gathering evidence on Eureka ASAP" / "Go for it."

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:08:48 +02:00
Meta Vens
5d30c4207a Huginn map: 13b built and eureka-state registered
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 01:59:56 +02:00
Meta Vens
2f48391533 Huginn map: record 11b and 13a merged; 13b maps Rejected
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:25:26 +02:00
Meta Vens
66a0f0c03d Record 11a merged; 11b re-checks model identity per flush
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 10:31:07 +02:00
Meta Vens
2248274f8d Record the 11b join-through-mind rule from Soul on 11a
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 10:03:49 +02:00
Meta Vens
98c8112209 Record BP-2 and RS-3 landed and closed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 06:57:42 +02:00
Meta Vens
4891f2e590 Pin the pipeline leaf to CultLib 6251cbc (BP-2)
One move of epiphany-pipeline's cultcache-rs, a0813c6 -> 6251cbc, so Huginn can
hold one cultcache-rs. cultcache-rs content is unchanged across the range;
the leaf suite is unchanged at 38. Cargo.lock gains the second cultcache-rs
entry the workspace now carries.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 03:40:17 +02:00
Meta Vens
d54068598e Record BP-1 landing and its Soul pass
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 01:32:22 +02:00
Meta Vens
d7074ab544 Record the operator's Huginn rulings Q-H1 to Q-H6
Q-H1 B: wait for Idunn, no interim unit. Q-H2 C: no backup yet.
Q-H4: one mind, eureka, plus session attribution on receipts.
Q-H5: park Cut 12. Q-H6 A: the hybrid, with the direction that
remaining prose keeps moving into typed state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 01:17:33 +02:00
Meta Vens
b6c2ca112b Place the Huginn organ cut map (Cuts 11-15, remapped)
Imagination (Opus) remapped Cuts 11-15 against Huginn 8c5f784, with the
read-side and body-plane cuts landing ahead of them. Cut 11 splits into
11a/11b and Cut 13 into 13a/13b. Cut 12 is proposed for parking. Five
operator questions are open (Q-H1, Q-H2, Q-H4, Q-H5, Q-H6).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 01:04:45 +02:00
Meta Vens
4f784d04d3 Bring the read-side and body-plane map headers up to date
Both headers still said "none started" and listed operator questions
as open. Q-RS1/Q-RS2 and Q-BP1/Q-BP2 were ruled on 2026-09-22, RS-L,
RS-2 and RS-1 closed that day, and selection Cut 1 merged at dbee044.
BP-1 is in Hands, with two overrides to the spec: no committed mutation
entries, and verification on Yggdrasil.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:44:18 +02:00
Meta Vens
a71f38f381 Close RS-L: the closing fixes landed
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 20:22:46 +02:00
Meta Vens
bcfc0d54a1 Regenerate the eight OrgRepo-bearing schemas for the trimmed description
Follows the source cut: the OrgRepo doc comment (RS-L closing fix 2) now
publishes only the grammar, the 140-byte bound and the case-insensitive
identity, with the bounded_text!/hand-written-impl reasoning moved to a
source comment. Regenerated with cargo test's own derivation so the
byte-for-byte pin (pipeline_published_schemas_match_derivation) stays green:
campaign, cut_report, cut_spec, finding, follow_up, hand_off, ruling and
stewardship all embed OrgRepo (directly or through ForeignRef's repo field).
instance, question, resolution, target and verdict do not reference OrgRepo
and are unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 20:20:29 +02:00
Meta Vens
38bc538110 Close RS-L's four fixes: OrgRepo unequal-direction pins, schema description split, count fix, campaign.repos duplicate refusal
Soul's third RS-L pass ruled the leaf's five promises hold but found four
closing items (soul-e20-notes.md, 2026-09-22):

1. Every OrgRepo Eq/Hash/Ord/PartialOrd assertion compared values that
   should already be equal, so eq -> true, hash -> (), cmp -> Equal,
   partial_cmp -> None and partial_cmp -> self.0.partial_cmp(&other.0) all
   survived as mutants. Add the unequal direction (assert_ne! on two
   different repos, differing hashes, a non-Equal ordering) and std's
   partial_cmp(a,b) == Some(cmp(a,b)) contract for both an equal and an
   unequal pair.
2. The published OrgRepo schema description carried ~1,100 characters of
   Rust-macro justification into nine contracts. Split the doc comment: the
   grammar, the 140-byte bound and the case-insensitive identity stay in the
   published /// doc comment; the bounded_text! delegation reasoning moves to
   a plain // source comment, so it stops reaching schemars.
3. lib.rs said "eleven bidi control code points" above twelve fixtures
   (ALM, LRM, RLM, LRE, RLE, PDF, LRO, RLO, LRI, RLI, FSI, PDI). Say twelve.
4. campaign.repos admitted both spellings of one repository. Add
   PipelineRefusal::DuplicateRepo and a campaign-only validation hook
   (value_types!'s new optional `=> extra` clause) that refuses two entries
   sharing an OrgRepo identity; distinct repos still validate.

This commit is the source half; the nine published schemas are regenerated
in the next commit to match fix 2.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 20:18:24 +02:00
Meta Vens
45dfab4ea8 Record Soul closing RS-L, and rule its four closing fixes
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 20:14:31 +02:00
Meta Vens
608b166a26 Record the RS-L third fix: identity is the type's own equality
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 19:59:15 +02:00
Meta Vens
466628ac39 Regenerate the nine pipeline schemas for RS-L's third fix
OrgRepo's doc comment now explains its hand-written identity-based
PartialEq/Eq/Hash/PartialOrd/Ord and the ASCII-case-insensitive .git
refusal (S-1, S-2); Title's regains the bytes-versus-characters caveat
(S-5). Both are schema descriptions, so nine of the thirteen published
schemas move: campaign, question, ruling and cut_spec carry Title;
campaign, cut_report, cut_spec, finding, follow_up, hand_off, ruling and
stewardship carry OrgRepo directly or through ForeignRef. Byte-for-byte
identical to what `schemars::schema_for!` derives from the code in the
previous commit, pinned by pipeline_published_schemas_match_derivation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 19:53:08 +02:00
Meta Vens
0631cbefb6 Cut RS-L third fix: OrgRepo compares by identity, .git refusal is
case-insensitive, the bidi set is pinned per-point, and the title rule
refuses two more separators

Soul's RS-L second-fix pass found identity() exposed but never adopted:
key derivation and every derived PartialEq/Ord/Hash still ran on the raw
string, so GameCult/Epiphany and gamecult/epiphany were two stewardships
and two store keys (S-1). The .git refusal was case-sensitive, so a case
variant reached an identity() the validator itself refused (S-2). Only
U+202E was pinned in the bidi match arm; every other code point's test
fixture was a lone character the alphanumeric rule already refused, so
cargo-mutants could not see a class-member deletion (S-3). is_control()
is Cc only, so U+2028/U+2029 (Zl/Zp) slipped through the widened title
rule and a one-line field could carry a hard line break (S-4). The Title
schema description lost the bytes-versus-characters caveat the original
RS-L cut carried (S-5).

- S-1: `OrgRepo` is pulled out of the `bounded_text!` macro (which has no
  per-member override) and given hand-written PartialEq, Eq, Hash,
  PartialOrd and Ord, all defined over identity(). Every other
  `bounded_text!` member keeps the macro's derives untouched. Key
  derivation for Stewardship and HandOff (lib.rs's Stewardship/HandOff
  arms of pipeline_key) now escapes repo.identity() instead of the raw
  string.
- S-2: org_repo_text's `.git` suffix check is ASCII-case-insensitive.
  identity() stays valid for every value validate_org_repo accepts,
  since lowercasing never changes the outcome of any rule in the
  grammar.
- S-3: title_text's control check also refuses U+2028 and U+2029
  alongside is_control(); everything else, including ZWJ/ZWNJ, is
  unaffected.
- S-5: Title's doc comment (the schema description) regains "enforced by
  admission; JSON Schema length counts characters".

Tests add: OrgRepo equality/ordering by identity, a HashMap and a
BTreeMap each keyed by both spellings holding one entry, identity()
always itself valid, case-insensitive .git fixtures (a/b.GIT, a/.GIT,
A/B.GIT), one Stewardship store key for both spellings, all eleven bidi
code points pinned with embedded (`a<cp>b`) fixtures instead of lone
characters, and U+2028/U+2029 refused embedded.

Schemas regenerated in the next commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 19:52:09 +02:00
Meta Vens
0d1e592b9a Record Soul on the RS-L second fix and rule identity into the type's own equality
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 19:45:39 +02:00
Meta Vens
1c340799da Record the RS-L second fix and the Hangul Filler limit
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 19:32:45 +02:00
Meta Vens
ec704d645e Regenerate the nine schemas Title/OrgRepo touch, for the RS-L second fix
Derived on Yggdrasil from the rewritten lib.rs by running
pipeline_published_schemas_match_derivation, which writes every stale
derivation to a temp directory on failure; those nine files are copied back
here byte for byte. Re-running the test afterward passes clean (0 stale).

Nine of the thirteen published kinds change: campaign, question, ruling and
cut_spec carry Title's new description; cut_report, finding, follow_up,
stewardship and hand_off carry OrgRepo's new description and its maxLength
move from 200 to 140. instance, resolution, target and verdict reference
neither type and are untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 19:31:24 +02:00
Meta Vens
998f97e84d Cut RS-L second fix: title is is_alphanumeric/is_control/bidi, OrgRepo
refuses .git and gains identity()

Soul's F2 finding on the second RS-L pass: a 1-byte title, " ", "\0", "\n",
U+200B and U+FEFF all passed as "non-empty" under the whitespace-plus-hand-
written-denylist check, because that check tested is_whitespace() rather
than the actual grammar rule (F2) or "not a bare placeholder" (F1/F3).

title_text is rewritten to the three-rule grammar Self ruled: at least one
char::is_alphanumeric() character, no char::is_control() character, and no
bidi control character (U+061C, U+200E, U+200F, U+202A-U+202E,
U+2066-U+2069). The hand-written C0/C1 ranges and the zero-width/BOM
denylist are deleted outright, not narrowed -- a character not covered by
one of the three tests is allowed, which is the point: no more enumerated
exception list to keep in sync with Unicode.

Discrepancy from the fix spec: the spec's refused-case list names "U+3164
alone" (HANGUL FILLER). Unicode gives it General_Category Lo, so
char::is_alphanumeric() is true for it on this toolchain (confirmed by a
failing assertion on Yggdrasil before the test was corrected). Reintroducing
a denylist entry for one code point to force that single case would
contradict the "delete the denylist" instruction this same fix batch
carries, so the test now pins U+3164 alone as accepted and documents why
inline.

OrgRepo (F7's GitHub grammar, unchanged in shape) gains two things:
- a repo name ending in ".git" is refused (GitHub strips it, so a document
  that carried it would silently alias another stored repo);
- OrgRepo::identity() is the one canonical key, ASCII-lowercased, with a doc
  comment saying every comparison and lookup goes through it. Huginn adopts
  it at BP-2 (out of scope here).

The dead `!repo.contains('/')` check is deleted: the repo byte alphabet
already excludes '/', so a second slash was always refused by the byte
check, never by that clause.

Consecutive hyphens stay allowed (GitHub's own signup rule is stricter than
its stored-repo grammar; the ruling this leaf implements is silent on
consecutive hyphens, so nothing here narrows it).

OrgRepo's published maxLength moves from 200 to 140 (39 + 1 + 100, the true
grammar maximum); the Title and OrgRepo doc comments now state the rule and
the true maxima directly and name no private function, since the derived
JSON Schema descriptions come from these comments verbatim.

Tests added/changed in question_and_ruling_carry_a_title and
the_org_repo_and_label_doors_are_the_grammar: the full refused/accepted
title matrix from the fix spec (RLO alone and with text, LRM alone, a lone
combining mark, a variation selector alone, NBSP alone, emoji-only,
Persian text with ZWNJ, a ZWJ pair, "a 🚀", 200 accepted / 201 refused
bytes already pinned); OrgRepo's owner=39/repo=100 accepted boundaries
beside the existing 40/101 refused ones, a/b.git and a/.git refused,
a--b/c--d (consecutive hyphens) accepted, and identity() lowercasing.

Hand probes on Yggdrasil (M0 green, restored): H01-owner-max-38,
H02-owner-min-2, H03-repo-max-99, H04-repo-min-2, H05-repo-no-hyphen,
H05b-repo-no-dot, H05c-repo-no-underscore and H05d-owner-no-hyphen all still
die under the rewrite. H06/H06b (2028/2029), H07/H07b/H07c (C0/C1 ranges),
H08 (zero-width-only), H09/H09b (U+2060/U+FEFF) and H10 (is_whitespace vs
is_ascii_whitespace) no longer apply: the code they mutate no longer exists,
since this fix batch deletes exactly that denylist and that whitespace
check.

cargo test -p epiphany-pipeline --lib: 37/37 on Yggdrasil.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 19:31:24 +02:00
Meta Vens
1298dace8c Record Soul on the RS-L fix and rule the title and OrgRepo identity rules
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 19:18:23 +02:00
Meta Vens
e2967c4d27 Close RS-2 and RS-1, and record the suite deletions
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 19:04:37 +02:00
Meta Vens
a4f78c2587 Record the RS fix batch and its first cargo-mutants Soul pass
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 18:47:04 +02:00
Meta Vens
703fdd0bf7 Delete every committed mutation suite
Better to have nothing than a harness that punishes refactoring
(operator ruling, 2026-09-22). Remove all eight tools/*mutations*.psd1
files (921 lines total): cut5, cut6, cut6b, cut6c, cut6d,
cut8-epiphany, cut10-epiphany, readside-leaf. The harness that ran
them, eureka-mutations.ps1, was already deleted from the Eureka skill
(37c80e4).

notes/eureka-body-plane-cut.md, notes/eureka-pipeline-state-cut.md and
notes/eureka-read-side-cut.md still name these suites; they are Self's
campaign records and are left untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 18:42:36 +02:00
Meta Vens
5f916b6974 Record the dead Ghostlight submodule pin and its fix
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 17:34:03 +02:00
Meta Vens
4d1113ef0a Move the Ghostlight pin off the dead cultcache-py submodule
Epiphany pinned ghostlight-persona-projection at 2228189
(2026-08-16), which still carries the vendor/cultcache-py submodule
pointing at https://github.com/GameCult/cultcache-py.git. That repo
was folded into CultLib's packages/cultcache-py and no longer exists,
so a fresh clone that recursively fetches Ghostlight submodules fails
cargo's `unable to update` step even though Epiphany's own tree is
otherwise clean.

Move the pin to 22937b7 (2026-09-13), the nearest Ghostlight revision
without a dead submodule: it replaces vendor/cultcache-py with the
published cultcache-py PyPI package and keeps only vendor/eve. Between
the two revisions the crate's public surface stayed source-compatible
for the three call sites Epiphany uses, with three additive/field
changes: ProjectorPrompt and PersonaPrompt gained a required
word_budget field (set to 180, matching Ghostlight's own
PERSONA_WORD_BUDGET convention for narrative Persona controllers), and
InterpreterPrompt's output_schema became Option<&str> (wrapped in
Some(..) to keep the always-supplied schema).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 17:18:05 +02:00
Meta Vens
0bc90b9c2b Move the Ghostlight pin off the dead cultcache-py submodule
Epiphany pinned ghostlight-persona-projection at 2228189
(2026-08-16), which still carries the vendor/cultcache-py submodule
pointing at https://github.com/GameCult/cultcache-py.git. That repo
was folded into CultLib's packages/cultcache-py and no longer exists,
so a fresh clone that recursively fetches Ghostlight submodules fails
cargo's `unable to update` step even though Epiphany's own tree is
otherwise clean.

Move the pin to 22937b7 (2026-09-13), the nearest Ghostlight revision
without a dead submodule: it replaces vendor/cultcache-py with the
published cultcache-py PyPI package and keeps only vendor/eve. Between
the two revisions the crate's public surface stayed source-compatible
for the three call sites Epiphany uses, with three additive/field
changes: ProjectorPrompt and PersonaPrompt gained a required
word_budget field (set to 180, matching Ghostlight's own
PERSONA_WORD_BUDGET convention for narrative Persona controllers), and
InterpreterPrompt's output_schema became Option<&str> (wrapped in
Some(..) to keep the always-supplied schema).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 17:17:31 +02:00
Meta Vens
6090ae519b Record the operator's acceptance: Q-BP2 A, the title rule, R-D, the Q-J door
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 13:54:44 +02:00
Meta Vens
8473e75df2 Tighten RS-L's Title and OrgRepo grammars per Self's fix-batch ruling
Title now refuses a bare placeholder: at least one non-whitespace
character, and no control characters (C0, C1, U+2028/9, the zero-width
characters, or the BOM). The 1-to-200-byte bound is unchanged, and the
Title schema now carries a truthful minLength/description pair instead
of the old ruling-history prose (F2, F3, F4).

OrgRepo now follows GitHub's own owner/repo grammar: an owner of
[A-Za-z0-9-], 1-39 bytes, no leading or trailing hyphen; a repo of
[A-Za-z0-9._-], 1-100 bytes, never "." or ".." (F7). Two fixtures used
an OrgRepo the new grammar refuses (a dotted owner, an underscored
owner); both are rewritten to carry the same character on the repo
side of the slash, which the grammar still allows, with their expected
keys recomputed.

Regenerated the four published schemas (campaign, cut_spec, question,
ruling) that reference Title, pinned by the byte-for-byte derivation
test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 13:45:18 +02:00
Meta Vens
d16685c9eb Record Soul on RS-2 and RS-1: the read side never checks density; rule the fix
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 13:40:26 +02:00
Meta Vens
ee52408718 Record Soul on RS-L and rule its fix batch: a title is visible text, OrgRepo is GitHub's
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 13:25:24 +02:00
Meta Vens
33571aa471 Record RS-L and the three rulings
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 12:39:28 +02:00
Meta Vens
d74e36aa06 Cut RS-L: the leaf gains Title, moves to epoch v2, and two grammar doors
Q-RS1 ruled B: a new bounded text type, Title (1-200 bytes, never empty),
replaces Short on all four leaf titles -- campaign, cut spec, and the two
new fields on question and ruling. The epoch moves anyway in this cut, so
tightening the two existing titles now avoids a second epoch bump later
just to make a title required.

PIPELINE_SCHEMA_EPOCH and all thirteen type ids move to .v2. The thirteen
schema files are renamed and regenerated, and index.json/README.md follow.

Two public doors join Slug::validate_slug on the same pattern:
OrgRepo::validate_org_repo and Label::validate_label, each delegating to
the grammar its own document field is already held to. RS-3 will read the
organ's repo and cut aliases through these.

Tests: the_org_repo_and_label_doors_are_the_grammar and
question_and_ruling_carry_a_title (37 total, up from 35). Mutation suite
tools/eureka-readside-leaf-mutations.psd1 (L1/L1L, L2/L2L, L3/L3L) verified
killed against M0.

No other consumer in this workspace depends on epiphany-pipeline as a
Cargo dependency; epiphany-core's runtime_spine stand-in hardcodes a
pipeline-shaped type id deliberately (RS-L's negative grep excludes it) and
is unaffected since its test only checks the "epiphany.pipeline." prefix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 12:37:20 +02:00
Meta Vens
c68db64270 Record Q-BP1 ruled A: chunks on the held session, named as Rust's explicit path
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 12:32:03 +02:00
Meta Vens
224d045d45 Record Q-RS2 ruled A: huginn-mind links cultnet-rs, selection split recorded
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 12:27:09 +02:00
Meta Vens
6dbd0c9d01 Record Q-RS1 ruled B: one non-empty Title for all four titles
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 12:12:49 +02:00
Meta Vens
53965ad490 Record RS-2 and RS-1, and the harness's case-blind no-op guard
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 11:57:20 +02:00
Meta Vens
d198ea744e Record Soul on the residue: it holds, and H1's own repair path is the new hole
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 09:18:48 +02:00
Meta Vens
9701b8eedf Place the read-side consumer map, record the residue batch, fix BP-3's orphan claim
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 08:23:03 +02:00
Meta Vens
b3ae787b7d Residue S5 and S8: pin the leaf door's 64-byte bounds and NUL, name the harness's real path
The leaf's own door test, slug_validate_applies_the_dotted_grammar, checked
the grammar but never pinned the 64-byte bound on the whole name, the
64-byte bound on a lone label, or NUL as a refused label byte. Extends it
with a two-label 64/65-byte pair (each label far under 64, so only the
whole-name check can be the one refusing 65), a direct Label check at
64/65 bytes (no whole-name wrapper to interfere), and a NUL case. Adds
three matching mutation entries to tools/eureka-cut10-epiphany-mutations.psd1
that drop each check in turn; all three die under the extended test.

The suite's own header still named the deleted repo-local harness path.
Corrected to the Eureka skill's copy, GameCult/Eureka.

Verified: cargo test -p epiphany-pipeline --lib, 35/35. Full mutation
suite through the Eureka harness: 5/5 killed (G1, G1L, S5whole, S5label,
S5nul), M0 green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 07:38:05 +02:00
Meta Vens
104c0bf255 Close Cut 10 on its stated invariants, and name the residue
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 07:09:53 +02:00
Meta Vens
4f41706f3d Record the Cut 10 fourth fix batch, and correct the S1d claim
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 06:17:15 +02:00
Meta Vens
9d3a3efd56 Fill the F6 gap in its owner: a public slug door on Slug
Slug::validate_slug applies the crate's own dotted_text grammar, the
same check every Slug field is held to, following PipelineRef::validate_ref's
pattern. Named validate_slug rather than validate so it does not shadow
Bounded::validate for in-crate callers.

Adds a leaf test covering one valid dotted name and refusals for fullwidth
characters, .., both path separators, the empty string, and an empty label.

Self's ruling on the F6 fork, 2026-09-22: fill the gap in its owner rather
than leaving Huginn's PipelineInstance wrapper beside it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 04:30:33 +02:00
Meta Vens
b4aab3f7c3 Record Soul on the third fix batch, and rule the slug check into the leaf
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 04:19:21 +02:00
Meta Vens
ed36236f72 Record the Cut 10 third fix batch, and flag the slug check living beside its owner
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 03:24:12 +02:00
Meta Vens
ff8ba146f8 Record Soul on the Cut 10 second fix batch: separator folding admits the wrong mind
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 02:36:59 +02:00
Meta Vens
5743fcf4be Map the body plane for oversize answers: three cuts, two questions
Imagination's map (Opus), placed by Self. BP-1 gives cultnet-rs the reference's
content-plane chunk messages; BP-2 moves the pins together; BP-3 makes the
daemon defer an oversize answer instead of refusing it. Q-BP1 (which physical
path) and Q-BP2 (what the summary is) are open for the operator. BP-1 waits on
selection Cut 1's Rust commit, because both edit cultnet-rs contracts.rs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 02:01:23 +02:00
Meta Vens
2876a82799 Send the harness to the repo that owns the process
It lived here because this repo needed it first, and the memory organ had been
reaching across a boundary for it. It takes the repository as a parameter, so
one copy serves every campaign and a fix to it fixes all of them.

Every earlier reference here to the Epiphany path is history and no longer
resolves.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 02:39:04 +02:00
Meta Vens
9cfbdeb314 Pin the arm that was recorded as unpinnable
The paragraph explaining why that dispatch arm could not be pinned is deleted
from the code and from the entries header, because it was wrong, and the probe
that disproved it is now a committed test. The size gate is pinned on its
boundary rather than near it, and an instance name that differs only in case
stands beside the ones that differ in length and prefix.

All six new entries survived the shipped suite before this batch, so none of
them is a mutant that could never have failed. The entry count recorded here
was wrong and is corrected from the file itself.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 01:31:26 +02:00
Meta Vens
ca17451a26 Rule the oversize document onto the body plane
A body the control plane cannot carry travels the transfer that exists for
exactly that, and the control plane carries the summary and a reference to it.
Raising the window only moves a number we would keep revising, and bounding
documents smaller argues with what a cut spec honestly is.

The refusal stays as the backstop. A caller that cannot be given an answer is
told so by name rather than left listening to silence.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 01:05:16 +02:00
Meta Vens
d138bf73de Record Soul on the fix batch, and a gap that was not one
The measurements hold under an independent probe and the transport itself
agrees, so the document that cannot be read back is a fact rather than an
inference. The gate refuses exactly where the transport gives up, and the
refusal is small enough to always arrive.

The arm reported as impossible to mutate was not. A probe built from the
crate's public surface kills both mutants, and it gets committed, because a
kill that lives in an agent's scratchpad is not evidence anyone can rerun.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 00:59:31 +02:00
Meta Vens
3325f515c8 Rule the read side: ordinals, deletions, and titles in the leaf
A receipt now records where it falls in the order, so a page is a position
rather than a guess, and the store takes the version that costs nothing today.
The two hardcoded endpoints go rather than surviving as presets of the thing
that replaced them. The leaf gains titles, because a catalog row showing only
an identifier is not a catalog, and it rides the same version bump instead of
asking for a second one.

The vocabulary itself is not ours: it fills the hole in CultNet, and the organ
becomes a consumer of it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 00:37:16 +02:00
Meta Vens
67947a29fa Record the Cut 10 fixes and the document that cannot be read back
An answer too large to send is now refused by name instead of vanishing, and
the three rules that were pinned by fixture luck are pinned by fixtures that
resemble each other. Measuring the refusal turned the size question into a
different one: a cut spec at the leaf's own bounds does not fit, so admission
accepts a document this door can never return. That is an asymmetry rather
than a limit and it goes to the operator.

One mutation entry was refused rather than faked, and the reason is written
where the next reader will find it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 00:25:58 +02:00
Meta Vens
7cc18fcb3a Keep the prior-art facts where they can be reread
An Eyes pass established what already exists for a typed query surface, inside
the substrate first and in published work second, with an evidence pointer for
every claim. It found no reusable shape in our own runtimes, three flat option
structs invented independently because no general one exists, and a citation
hop query we built and deleted with its host surface rather than on its merits.

It lands in the repository because a facts file that lives only in a scratchpad
is the same failure as a harness that lives only in a scratchpad, which this
campaign has now paid for twice.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 00:21:47 +02:00
Meta Vens
44f89035a7 Record Soul on Cut 10, and rule that a large answer refuses
The organ answers typed refusals over the wire and holds no rule beside the
mind, which is what the cut was for. It also loses any answer too large for
the window, without a word to the caller, and the spec's idea of document size
was never the leaf's. A response that will not fit is now refused by name,
carrying its size and the limit, and the window rises to the worst case a
single document can reach. No truncation, no pagination the caller did not
ask for.

Three rules turned out pinned by luck rather than by tests, among them the
instance check, whose mutants survived because no fixture pair had ever shared
a length.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 23:43:22 +02:00
Meta Vens
b1e919e6f6 Record Cut 10 and the nine places the map was wrong
The organ answers over the hub, the daemon holds no rule of its own, and the
instance check has one owner. Every discrepancy this cut hit was the map's
rather than the Body's, including a constant that does not exist in CultLib
and a mutant that could not fail because the thing it broke released the port
on its way out.

Several loosening mutations the brief required turned out unfalsifiable and
were replaced. That replacement is the first thing Soul is told to attack: a
loosening swapped for an easier target is a hole wearing a green tick.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 23:18:00 +02:00
Meta Vens
add4f9d093 Write the schema-phase postmortem
Epiphany's stake in the campaign closed with the leaf owning the schemas and
nothing else. The record keeps the scars: a store deleted whole after the
operator rejected it, a grammar redesigned on its third patch, a fix that
introduced a replay regression, a harness that ate the work it was testing,
and the five corrections the operator had to make to the agents' model.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 22:46:17 +02:00
Meta Vens
c6d24a44ea Record the ref validator and rule where the grammar stops
One door now opens onto the reference grammar, and both read doors refuse a
ref that is no ref before answering over it. The leaf owns the shape of an
id; per-kind local shape stays with admission, where the sequence rules
already live, and a resolution's key is derived rather than written, so
nothing malformed reaches the write path. The status header is rewritten to
what has actually landed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 22:44:32 +02:00
Meta Vens
5cda0886bd Open one door onto the reference grammar
Ruling 1 leaves the schemas and the key grammar with the leaf, and the read
side in Huginn must refuse a `PipelineRef` whose kind and id disagree rather
than answer the nothing a lookup finds. `Bounded` is `pub(crate)`, so nothing
outside could ask.

`PipelineRef::validate_ref` delegates to that impl at the field name `ref`, so
the grammar keeps one owner and the wrappers and `pipeline_id` stay where they
are. A method, as `prepare` and `decode` are, because it concerns one public
type; `validate_ref` rather than a bare `validate`, because an inherent
`validate` would shadow `Bounded`'s for every in-crate holder of a
`PipelineRef`.

The test holds every sample document's own key to be a reference of its own
kind, and refuses a kind that is not the id's and a trailing dot at `ref.id`.
It also pins the boundary rather than assuming it: a resolution id missing the
per-subject sequence its writer always composes is still well formed here. The
leaf reads an id kind-deep, and the local's per-kind shape is admission's rule.

Entries R1 and R2 in `tools/eureka-cut8-epiphany-mutations.psd1`: the door that
answers `Ok(())` to everything, and the door that reads the kind segment alone.

`cargo test -p epiphany-pipeline --lib` 34 passed, 0 warnings; `cargo check -p
epiphany-core --lib --tests` clean; the four Epiphany suites green-and-killed
through the harness. No schema and no lock moved.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 22:31:26 +02:00
Meta Vens
01cf2d4482 Rule Q22 and Q23: request/response over the hub, presence after deploy
The organ's surface is request and response over CultNet's hub, since
admission and typed queries are request/response by nature and a
publication cannot reply from its sink. The Eve operator surface lands as
its own cut after the daemon lives beside Odin, so the advertisement is
built against a Verse that exists.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 22:25:01 +02:00
Meta Vens
9d63f0dc55 Refresh Cut 10 against the landed organ
The daemon is one process over one mind: it opens the owned store, binds
one socket, and serves admission and the read side over CultNet through
the hub, with the wire vocabulary in the organ's library per Q13. The
instance check moves to one place both admission and the reads call. The
index seam is a port with a no-op impl whose error cannot change an
outcome. Two questions for the operator: the surface's shape, and where
the Eve operator surface lands.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 22:20:25 +02:00
Meta Vens
c1ae5cdf10 Close Cut 9 and mark the design sections it supersedes
The read side lands on one owner for in-force, which the same mutant
proves by failing both suites. History is a first-class view in sequence
order with withdrawn records and their reasons, as the Q17 ruling
required. Soul held every promise and found two loosenings the entries
file had called unfailable, and one fixture the open-items test needed;
all three are bounded and in Hands. The design sections D4, D6 and D7 now
say which of their sentences the landed code made false.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 21:59:54 +02:00
Meta Vens
fb18395fe3 Close the Cut 6d follow-up
A replayed hand-off withdraws the assignment it withdrew, found by
content, and the arm no path could reach is gone. Forty-six tests,
sixty-seven entries killed. One tooling scar recorded: the Bash tool
mangles a Windows target-directory path, so cargo here runs through
PowerShell only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 21:00:02 +02:00
Meta Vens
b2d4f16d67 Refresh Cut 9 against the landed organ
The read side takes the derivations admission already owns by moving
them whole into one module both call, so one mutant kills both suites.
History is a first-class view in sequence order with withdrawn records
and their reasons, which is what the Q17 ruling required. Two of the old
five functions die as query presets. No text filter until the index
exists.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 20:59:07 +02:00
Meta Vens
00624c8f24 Close the Cut 6d follow-up well enough to map Cut 9
Replay reproduces what it derived and a withdrawal cannot reinstate over
a later record, under scopes Hands extended to targets and cut specs when
the brief's own test needed them. Soul's scoped pass found the replay
still picking the wrong subject past nine transfers of one repo, and a
dead arm; both land before Cut 12. A replay refusing once its scope has
advanced is recorded as a ruling-20 limit the views must not hide.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 20:44:50 +02:00
Meta Vens
94df3a8fc7 Record Soul's pass on the Cut 6d follow-up, and rule the reinstatement case
The reopen and transfer paths hold over real stores. Two things do not:
replaying a batch that derived a write is refused instead of answered
from its receipt, and a withdrawal of a withdrawal can put an earlier
record back in force beside a later one. The second gets a rule that
extends Q19's own rationale. The depth cap's wording is corrected to the
nesting reading the code and the ruling implement, and Cut 12 learns that
history imports one record per batch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 20:10:48 +02:00
Meta Vens
1e11427425 Record the Cut 6d follow-up and bring Cuts 8, 9 and 12 to the ruled shape
The organ now enforces the sequences, ignores withdrawn resolutions when
deciding what is in force, and refuses a withdrawal of a withdrawal. The
Cut 8 rule table says so instead of describing the collision-based shape
it replaced; Cut 9 carries the recursive derivation and the history views
the Q17 ruling requires; Cut 12 says withdrawal rather than supersession
and models no return.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 19:47:28 +02:00
Meta Vens
d5a36c2a29 Pin the two sequences the all-ones fixtures hid
Soul's pass over the Cut 6d suite found two rules nothing tested. Every
nested fixture sat at `sequence: 1` at both levels, where a resolution's own
sequence and the one its subject already ends in are indistinguishable, so a
mutant that copies the subject's survived. And no test resolved a
stewardship, the other sequenced kind, so nothing held its own sequence into
the resolution key the way M27 holds a nested resolution's.

Two tests. `every_level_of_a_nesting_keeps_its_own_sequence` builds a chain
that differs at every level -- `question.Q1.n9`, then `.n10`, `.n0`, `.n12`
-- and reads it back level by level, each level's subject recovered whole and
each level's own sequence last. `a_resolution_of_a_stewardship_reads_back`
resolves both assignments of one repo, pins the two keys apart, and nests a
withdrawal over each. Entries S6 and S11 carry the mutants they kill.

The nesting test's doc comment records the spelling limit while it is in
hand: the leaf emits `n<N>` as `u32` writes it, so `n01` is never emitted,
but a hand-built id ending `n01` or `n1x` is a legal label and no business of
a grammar that reads keys. Nothing canonicalises it either -- it names a
document the leaf would never key, which admission's A7 reads as a missing
referent.

Suites: 6d seven entries killed, M0 green; 6b, 6c and cut8-epiphany
unchanged and green. 33 tests, no warnings.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 19:20:24 +02:00
Meta Vens
ded1a81c91 Record Soul's pass on the Cut 6d leaf half
Every code promise held, including that a subject whose own local ends in
something shaped like a sequence cannot be confused with one. Two coverage
gaps sit on the paths the next cuts lean on and land with the Huginn
follow-up that moves the pin.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 19:10:02 +02:00
Meta Vens
32fbea4797 Record the Cut 6d leaf half
Resolutions and stewardships carry a writer-set sequence that ends their
key, so a subject's history and a repo's history each sit under one
prefix. The leaf makes the key and compares nothing; admission owns the
next-number rule. Two schemas regenerated, no epoch moved, every earlier
suite still killed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 18:45:47 +02:00
Meta Vens
b82c76dfd6 Add the Cut 6d mutation suite
Five entries, each restoring the permissiveness the shape it pins
removed: M23 and M24 the one-per-subject and one-per-repo keys, M25 and
M26 the sequence anywhere but last, M27 a nested subject stripped of its
own sequence on the way into the key. Each dies on the test named beside
it, M0 green.

The file records what has no entry and why: the `n` marker is a fixture
pin, the `u32` type has no runtime mutation, and date-invariance is
covered by M24's test asserting a stewardship local is exactly two parts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 18:37:12 +02:00
Meta Vens
9699751f00 Key a resolution and a stewardship by sequence
A subject's resolutions and a repo's assignments on a mind each carried
one key, so the second record of either overwrote the first and a
withdrawn resolution could not be kept under its subject. Both keys now
end in a writer-set `sequence: u32`, `n<N>`, last so the history of a
subject or a repo is one key prefix and R2 keeps that prefix exact:
`question.Q1.n` cannot match `question.Q10.n1`, and a withdrawal keys one
nesting out under the resolution it withdraws.

`assigned_on` stops being a key input and stays a field, validated where
every field is, so the same assignment recorded on two days is one
document and a repo transferred away and back on one day does not
collide. A hand-off is unchanged in shape and key: a transfer back is the
same document with the instances swapped, and no field names a return.

Nesting arithmetic moves with the shape: each nesting prepends
`resolution.` and appends `.n<s>`, so a chain composes `14 * (d - 1) + L`
bytes against `LOCAL_MAX`, and both fixtures key four deep. The leaf
refuses no sequence value -- not `0`, not a gap -- as it refuses no
`revision` value; which sequence a document may claim and which record is
in force are admission's, and are not in this crate. `pipeline_id` gains
no arm and no key is parsed back.

Two schema files regenerate from the derivation, gaining the property,
the required entry and the new doc comments. No epoch moves: no store is
written at this epoch and no reader is pinned to the file.

Cut 6b's M2 is re-anchored on the new parts line, as N7 and N8 re-anchored
Cut 6's M12 and M13.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 18:26:46 +02:00
Meta Vens
ceeffabf26 Close Cut 8 as ruled today
Every promise of the fix batch held under non-revert mutations. What
remains belongs to the key-sequence follow-up: a withdrawal of a
withdrawal is not yet refused, and reopening a subject needs admission's
in-force derivation to ignore withdrawals as well as the key to allow a
second number. Six test gaps ride with it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 18:25:50 +02:00
Meta Vens
b693f9f3b3 Map Cut 6d: resolution history and stewardship by sequence
Two key shapes gain a writer-set sequence so a subject's resolutions and
a repo's stewardships keep their history under one prefix, with the
in-force record derived as the latest not withdrawn. A hand-off stays a
transfer with no lease field. Two schema files regenerate, no epoch moves,
and the Huginn follow-up commit is named for when the pin moves.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 18:19:25 +02:00
Meta Vens
542dc1848c Rule Q19 and Q20: no double withdrawal, and stewardship by sequence
A withdrawal is final and a reopened subject resolves again at the next
number. Stewardship keys by a per-repo sequence rather than a date, so
two transfers of one repo in one day do not collide and both histories
use one mechanism. Q18's substance stands; its wording is superseded.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 18:12:16 +02:00
Meta Vens
e306964d04 Record the Cut 8 fix batch
The epoch gate checks what the spec said it checks, the organ mints no
leaf refusal, one swap and every strong read are pinned, and every rule
row the suite could not see regress now has a mutation. Self's Q17
default was reverted before it mattered. The gap the ruling leaves until
the leaf's key sequence lands is named as the reason Cut 6d precedes Cut 9.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 18:08:48 +02:00
Meta Vens
ec0ff804bc Make Q17 carry its own conditions
The operator questioned the model behind keeping withdrawn resolutions.
The answer is that the ruling matches the campaign's standing rule of
never overwriting, and that the record only earns its keep if Cut 9 shows
a subject's history and Cut 11 indexes withdrawals with their reasons.
Both are now requirements on those cuts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 18:04:58 +02:00
Meta Vens
76167f0a2a Correct Q18: a hand-off is a transfer, not a lease
Self read the operator's lease example as the general case. It is one
case: a steward can also spin off a new steward permanently. A hand-off
is a transfer of stewardship with no lease field; a lease is two of them.
The date-keyed stewardship stands because it is what lets a repo come
back at all.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 17:59:53 +02:00
Meta Vens
993600fd6b Rule Q17 and Q18 in the operator's words
A resolution is the record of how a subject was closed; withdrawn ones are
kept under the subject and a subject can be resolved again. A hand-off is
a lease: a campaign's steward borrows another steward's authority over a
repo and returns it. Both are key changes in the leaf, free today, mapped
together as one cut. Self's default on Q17 was withdrawn before it landed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 17:56:34 +02:00
Meta Vens
87d3420c31 Record Soul's Cut 8 pass, and raise Q17 and Q18
The owners hold at the layer where they fail: a mind copied under another
instance is refused, a second process is refused, and the digest moves
with content alone. What did not hold was the epoch gate, which took the
first record and checked nothing about it, and fifteen rule rows the suite
could not see regress. One sentence of the spec contradicted the key
grammar and becomes a question: whether a resolution can be undone.
Another surfaces for Cut 12: whether a stewardship can come back.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 17:45:42 +02:00
Meta Vens
3bd0826d48 Rule out a hidden dependency on the leaf
The workspace's inverse dependency tree lists the leaf alone in every
dependency kind, so the recompile of epiphany-core after a leaf-only
change was a shared target directory evicting a fingerprint under another
session's build, the accepted cost of sharing one directory. Not a
dependency, and not the explanation Hands gave.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 17:28:03 +02:00
Meta Vens
32b638dcec Record the leaf's serialisation derives, and a recompile that should not have happened
The document and refusal types now serialise with a tag equal to the kind
name, which is what lets Cut 10 carry a batch without a second registry.
An unexplained recompile of epiphany-core after a leaf-only change is
recorded as suspect for the next Soul, not accepted on the explanation
offered, because the core is not a dependent of the leaf.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 17:27:31 +02:00
Meta Vens
b4b17fc895 Serialise the pipeline envelope and its refusals
The leaf owns the document and refusal types, so it owns their
serialisation. Huginn's admission batch cannot derive Serialize while
PipelineDocument does not, and mirrors PipelineRefusal through a
serde(remote) definition kept in step by hand; neither is the wire's
business.

PipelineDocument is adjacently tagged on the kind name: a document on a
wire is { kind, value }, and kind is exactly PipelineKind::name(), so a
reader dispatches on the kind segment it already knows from the key and
needs no second registry mapping variant spellings to kinds. Adjacent
rather than internal because a value is a named map of its own.
PipelineRefusal stays externally tagged, serde's default, which is what
keeps every variant's field and value on the wire under their own names.

No kind, field, type id or epoch moves, and the published per-kind
schemas are untouched: the envelope is the wire's shape, not a document's.

every_document_and_refusal_serialises_and_reads_back round-trips every
sample document and every refusal variant through serde_json and
rmp_serde, and holds each derived schema's variant list to the thirteen
kinds by name and to the four refusals. Mutations D1 (the tag rename
removed) and D2 (a refusal's field skipped) are both killed by it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 17:26:30 +02:00
Meta Vens
dfbc5700bc Record the Huginn half of Cut 8
The organ opens a mind fail-closed over CultCache's owned store, lands a
batch whole with a receipt naming its bytes, and admits through one
ordered path with every cross-document rule in it. Twenty mutations,
twenty killed, inside the build budget. One finding for Cut 10: the leaf
must derive serialisation on the document and refusal types before a
batch can cross a wire.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 17:21:09 +02:00
Meta Vens
78238ebabc Record the harness repair batch, and say whose lock it is
The harness now handles a sidecar that outlives its file and a nested
sidecar without a raw exception, and the leaf's doc comments describe the
leaf. Cut 8's store paragraph now says the single-writer mechanism is
CultCache's owned store and its compare-and-swap batch, with redb as the
engine beneath, after the operator caught the misattribution.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 16:55:19 +02:00
Meta Vens
aef0e1bf5a Describe the pipeline leaf by its own contract, not by Huginn
Doc comments only. The crate doc, `prepare`, `register_pipeline_document_types`,
`ForeignDocument`, `PIPELINE_SCHEMA_EPOCH` and `validate_pipeline_write_envelope`
spoke of "the memory organ" and "Huginn" in the present tense as a consumer
that exists; Huginn's crates are empty stubs at this commit. Each now says what
the item does and guarantees, and where a consumer must be named, calls it the
organ that will admit these documents. The sentence claiming Huginn pins the
foreign-document rule against its real receipt type is gone; no such test
exists.

`decode` now says what it does not do: it validates neither bounds nor the
key. A decoded document is a typed read, and `validate_pipeline_write_envelope`
is the check before a write.

No code change: `cargo test -p epiphany-pipeline --lib` is 27 passed with no
warnings, and `schemas/cultnet/` and `Cargo.lock` are untouched.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 16:54:16 +02:00
Meta Vens
b3bd4a82e4 Repair a missing file from its sidecar and stop on a nested one
Soul's XF: a sidecar whose file was gone hit the `if`/`else` empty-array
collapse of Windows PowerShell 5.1, so `Find-FirstDifference` threw a raw
`ArgumentNullException`, the sidecar stayed, and every later run in that
repo died the same way before M0. A missing file is now its own case,
decided before any comparison: it is recreated from the sidecar, whose
bytes are the only copy, hash-verified, and the run says so.
`Find-FirstDifference` refuses a null array with the harness's own words.

Soul's XG: a sidecar of a sidecar was repaired in enumeration order, so
the inner sidecar's body became the file and the outer one then crashed
on its missing target. No run leaves that shape and the two cannot be
ordered, so the repair pass now stops before touching anything, naming
both files.

Soul's F2: the `.eureka-mutation-overwritten` copy was written before the
restore was attempted, so a locked target (XC) left a copy claiming an
overwrite that never happened. The copy is still written first, because
once the restore lands it is the only home of the pre-repair bytes; a
restore that fails without opening the file removes it again, and one
that changed the file before failing keeps it and says so.

Every repair step now sits inside one `try`/`catch` per sidecar, so the
"could not repair" message is the error that escapes, with the cause
appended, never a bare .NET exception.

Reproduced against a scratch copy with Soul's soul-cut8e-harness-attack:
XF recreates the file and removes the sidecar; XG stops with both files
named and the target untouched; XC leaves no overwritten copy. XA, XB,
XD, XE, XH, XI unchanged. An empty existing file beside a sidecar (XK)
repairs without the null path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 16:51:14 +02:00
Meta Vens
0b8d60a685 Record Soul's pass on the Epiphany half of Cut 8
The door holds from outside: the four functions and the constant are
reachable, the wrappers and the id reader are not, and every kind
round-trips through the public path with its own key. Huginn pins
a65c6420. What Soul found is a harness that crashes without a word when a
sidecar outlives its file, and doc comments that describe an organ nobody
has built yet.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 16:46:25 +02:00
Meta Vens
5a71c4bb3e Rule Q13 and Q15: types from huginn-mind, and mind.redb
The client depends on the organ's library for types only, pinned by a grep
that it never opens or admits. The mind file is named for the engine that
holds it, because the selector and Studio both key on the extension. The
engine's provenance is recorded beside the ruling so nobody asks again.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 16:39:16 +02:00
Meta Vens
955b7672c8 Record the Cut 6c fixtures and the Epiphany half of Cut 8
The fix outcome's sha is checked whether or not a reference is given, the
supersession bound is pinned in the validator and not only the schema, and
the harness repairs any sidecar under the repo and never discards bytes
without saving them. The leaf's write path is live and public for the
organ, with the epoch back in code as one constant.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 16:23:21 +02:00
Meta Vens
a65c6420f6 Cut 8, Epiphany half: the leaf's write path goes live for the organ
`epiphany-pipeline` was the organ's type library with its prepare,
decode, register and validate path gated `cfg(test)` "until the organ
prepares writes (Cut 8)". This is Cut 8. The four are `pub` and
un-gated: `register_pipeline_document_types` registers the thirteen
kinds and nothing else, `PipelineDocument::prepare` and `::decode` are
the one door to the crate-private wrappers, and
`validate_pipeline_write_envelope` returns `PipelineRefusal` directly,
which drops the `downcast_ref` in the key test. `anyhow` and `rmp-serde`
move from dev to normal dependencies; both were already in the lock, so
the lock is unchanged. The test stand-in `ForeignDocument` stays
`cfg(test)` and is registered by the tests' `schema_cache`, no longer by
the live registrar.

`PIPELINE_SCHEMA_EPOCH` returns as one `pub const` beside the schemas it
names, with the README's evolution rule as its doc comment: additive
keeps it, a widened `PipelineKind` is additive, a breaking bump refuses
the old store. `every_kind_is_at_the_epochs_version` reads the version
off the constant and holds every type id to it, and counts the live
registrar's types against `PipelineKind::ALL`.

Verification: `cargo check -p epiphany-pipeline --lib --tests` and
`cargo test -p epiphany-pipeline --lib` (27 passed, 0 warnings);
`cargo check -p epiphany-core --lib --tests` untouched; `cfg(test)` in
lib.rs is now only `derived_schema`, `ForeignDocument` and `mod tests`;
`schemas/cultnet/` and `Cargo.lock` unchanged. Mutations
`tools/eureka-cut8-epiphany-mutations.psd1`: M0 green, E1 (epoch `.v2`),
E2 (one kind fewer registered), E3 (the stand-in registered again under
`cfg(test)`) all killed by the new test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 16:21:34 +02:00
Meta Vens
3f7d58d191 Harness: repair every sidecar in the repo, and never overwrite silently
Soul's extra attacks on the mutation harness found the crash repair too
narrow and too eager. A sidecar left beside a file outside this run's
targets was ignored (XA), so a dead run's mutant survived in the tree; a
hand edit made after a crash was overwritten by the sidecar without a
word (XB); a locked target threw a bare IOException with no path (XC);
and a sidecar equal to its file was reported as a repair (XD).

At startup the harness now walks the repo root (`target/`,
`node_modules/` and `.git/` pruned) for every `*.eureka-mutation-original`
and repairs each. A file equal to its sidecar is left alone and the run
says "sidecar matched; nothing to restore". A file that differs has its
current bytes written to `<file>.eureka-mutation-overwritten` first, with
the path and SHA-256 printed. A write that fails prints the path and
"could not repair, sidecar kept" before rethrowing.

`-Repo` names the root the entries, targets and commands are relative to,
defaulting to this script's parent, so a suite in another repo runs
through this one harness.

Reproduced against a scratch copy with a fake runner: XA repairs b.rs
and removes its sidecar; XB keeps the hand-edited bytes in the
overwritten copy (same hash) and restores the original; XC stops with
the path and the sidecar kept; XD removes the sidecar and restores
nothing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 16:16:11 +02:00
Meta Vens
fcfbda3f94 Pin the three Cut 6c rules Soul's mutations survived
Soul's own suite found three validator rules no test measured: a fix's
commit was unpinned once `by` was `Some`, the validator's own bound on a
supersession was unpinned (only the schema's `maxItems` was), and the
reason on `Recorded` and `Withdrawn` had no bound test. Each is now a
forgery in the test that owns the arm: `Fixed { commit: "dirty-worktree",
by: Some(ruling) }` refused at `resolution.outcome.commit`; nine
supersessors refused as `FieldBound` at `resolution.outcome.by`, eight
accepted; a 1,001-byte reason refused at `resolution.outcome.reason` on
both outcomes.

Soul's S2, S14 and S10 join the Cut 6c suite under their own ids. Rerun:
M0 green (26 passed); M16-M22, S2, S14, S10 killed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 16:14:41 +02:00
Meta Vens
fc7ad7457a Record Soul's pass on Cut 6c
Every shape, schema and key promise held, including byte-identical derived
schemas and unmoved keys. Three fixtures were missing, the important one
being that a fix outcome's commit sha goes unchecked whenever a reference
is also given, which is half of ruling B. Three harness follow-ups. One
count of Self's corrected.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 16:09:14 +02:00
Meta Vens
c792075ec8 Refresh Cut 8 against the landed grammar and shapes
The old section was anchored to pins from before every key moved and
before the Ghostlight shapes retyped the referents admission validates.
Two things the old section had wrong: Epiphany is touched, because the
leaf's prepare, decode, register and validate path is test-only today and
Huginn cannot re-declare the wrappers without becoming a second authority
over the wire shape; and the rule set lived only in git history, so the
map now owns it in full. The store is the owned redb store, which gives
one-writer a lock instead of a sentence, and the receipt is duplicated,
bounded and smaller, with moving it priced and declined for now.

Four questions raised; two are defaults taken, two are for the operator.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 16:01:39 +02:00
Meta Vens
ca7e230cd8 Record Cut 6c's landing and the third harness batch
The Ghostlight shapes are in: two unit enums, a promise type, five fields,
four parsed referents where strings were, and one estimate type where two
were. Six schemas regenerated with the supersession bound carried into the
published JSON. The harness now restores on every path and repairs itself
after a killed run. Four scars recorded, the first being that a widened
type only reads as a kill when the fixture is spelled so the wider tree
still compiles.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 15:44:00 +02:00
Meta Vens
dddf9ede53 Cut 6c mutation suite, M16-M22
Seven entries through the shared harness: the Superseded list checked by
length only (M16), the outcome's whole match body replaced by Ok (M17),
the fix's commit unvalidated (M18), the mutation label widened to Short
(M19), its commit widened to Short (M20), the sample estimate's line
counts transposed under a two-line anchor that carries the opener (M21),
and a claim's mutations bound widened to sixteen (M22). Each is killed by
the Cut 6c test that pins it; the key suite is blind to all seven.

The first run left M19 and M20 with no verdict: the sample and the test
spelled the label and the commit with the narrow constructors, so the
widened tree did not compile instead of admitting the forgery. Both now
go through From<&str>, which every text type has, and the forgeries
reach validation and kill.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 15:41:41 +02:00
Meta Vens
13570e8458 Cut 6c: the Ghostlight shapes
Ghostlight's independently run campaign named what the documents could
not say: a report's promises, a ruling's authority, a finding's origin,
and a resolution's referents as anything but unparsed text. Zero new
kinds, no key moves, epoch unchanged.

Added: RulingAuthority and FindingOrigin as unit enums; Promise beside
LandedName, identified by its report and label like a TargetInvariant;
promises on the cut report, authority on the ruling, origin on the
finding, and promise plus a bounded mutations list on a verdict claim.
Retyped: MutationRecord carries a key-safe label, a location, the exact
before and after, and the commit it ran against; the cut spec's estimate
is a StructuralDelta, the same shape as the report's actual, so the two
compare on every axis; ResolutionOutcome's referents are PipelineRefs,
Superseded a list of at most eight (its maxItems carried by schemars
extend, since the enum is outside value_types!), Fixed a Sha with an
optional record (ruling B). The Bounded impl is the replacement written
whole: the list validates every item, and every arm names its field.

Five tests pin the shapes: a referent is a full id of the kind it
declares, with a wrong-kind forgery at by[1] refused by field, and an id
no resolution derives left to admission; a fix's commit is lowercase hex;
a mutation's label is dot-free and its commit a sha; a claim's promise
and mutations are labels, nine mutations refused at eight; the sample
spec estimates a net addition of 900 lines and equals the sample
report's actual.

Six schemas regenerate from the derivation test's temp directory:
ruling, cut_spec, cut_report, verdict, finding, resolution. index.json
and the other seven are byte-identical.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 15:33:25 +02:00
Meta Vens
4d6af4090f Cut 6c deletes: the estimate type, the outcome's Bounded impl, the mutation text
The deletes go before any field is added, so the dead estimate shape and
the delta that replaces it never coexist at a commit. SubtractionEstimate
and its comment, the cut spec's subtraction_estimate field and its sample
literal, the hand-written Bounded impl for ResolutionOutcome (replaced
whole in the next commit, never patched, since every arm changes referent
type and a patched impl is how a length-only check survives), and
MutationRecord's free-text mutation field with its sample value are gone.
The list-default comment now stands on StructuralDelta alone.

This commit does not build: PipelineResolution's outcome has no Bounded
impl until the next commit gives it the parsed one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 15:29:19 +02:00
Meta Vens
7cd1a38bc8 Pin the reader against a trimmed trailing dot on the root and the local
Soul's X1 and X11 survived the second fix batch: a reader that trimmed a
trailing dot from the root or from the local before its dotted_text check
passed every test, because the only fixtures with a trailing dot went
through the writer, which never composes one. Four malformed ids now
read through pipeline_id in keys_read_back_as_ids_of_their_kind and are
refused: a trailing dot on the root, a trailing dot, a leading dot and an
empty part on the local. X1 and X11 join the Cut 6b suite as entries,
each killed by that test.

The resolution arm's comment named "the 64-byte bound in local"; the
bound is LOCAL_MAX and the comment now says so.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 15:28:23 +02:00
Meta Vens
117f54b791 Close Cut 7
The parity manifest agrees with Eve's metadata, the fixture no longer names
Huginn anywhere, and AGENTS says the crates are stubs. Three small edits
with their checks pasted; no separate Soul pass, and the next Soul on
Huginn rereads AGENTS at Cut 8.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 15:26:29 +02:00
Meta Vens
ecf88f5020 Record Soul's Cut 7 pass and fix two map errors of Self's
Soul held the deletion line and the no-consumer claim, and found the fixture's
owner recorded in a third repo that now disagrees, three more Huginn-naming
fields, and AGENTS describing unbuilt cuts in the present tense. Two errors
were the map's own: a follow-up number used twice, and a negative grep that
ripgrep cannot parse.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 15:24:32 +02:00
Meta Vens
9d57a46079 Harness: restore inside every finally, sidecar the original, time out
Soul's second attack on the mutation harness found three ways a target
stayed mutated. A multi-target entry wrote its targets outside the try,
so a write that threw on the second target skipped the finally and left
the first mutated. M0's write-through had no finally at all. And a hard
kill mid-entry, which is what a tool timeout is, runs no finally, so the
target stayed mutated with nothing recording what it had been.

Every write now sits inside a try whose finally restores every target the
step touched, M0's included, each proven by hash; a target that cannot be
restored is printed with RESTORE FAILED and its original SHA-256 before
the run throws. Before any target is written its original bytes go to a
sidecar beside it, deleted only after a verified restore, and a run that
finds a sidecar at startup restores from it, verifies, removes it and
says a previous run died mid-mutation. Commands run through a Process
with a -TimeoutSeconds limit (1800 by default) and are killed with their
tree by the harness inside its own finally, so a hung cargo ends inside
the entry's restore rather than under the operator's tool.

Reproduced against a fake repo: A1 (M0 target write-locked) and A2 (the
second of two targets locked, in both hashtable orders) leave every
target byte-identical with no sidecar; A3 (tree killed mid-entry) leaves
the mutant and the sidecar, and the rerun reports the repair and kills
the entry; a 3-second timeout against a 12-second runner reports TIMED
OUT (no verdict) and restores.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 15:24:15 +02:00
Meta Vens
d094a19986 Correct the npm note: reachable after all, publishing deferred by choice
The operator reached npm signup today, so yesterday's blocked subnet is no
longer a Body fact. The ruling stands on a different reason: registry
publication is a later pass. Recorded in the operator's words beside the
earlier statement so the map does not carry a claim its author withdrew.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 15:18:10 +02:00
Meta Vens
79c5d38436 Record Soul's third pass: the grammar is closed, the harness is not
Every grammar pin held under non-revert mutations. What survived is the
harness under failure: writes outside the restoring finally, no guard on
the control's own write-through, nothing that survives a killed process,
and one reader fixture for trailing dots. A third batch takes those and
Cut 6c follows in the same tree.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 15:17:17 +02:00
Meta Vens
b359548241 Record Cut 7's landing on Huginn and Eve
Huginn's TypeScript body is gone and an empty Rust workspace stands in its
place; Eve's fixture says it is Eve's. The spec's file and line arithmetic
was off and is corrected in the landing note rather than the table, and the
conformance fixture's third Huginn-naming field is flagged for Soul.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 15:15:31 +02:00
Meta Vens
97fac26a32 Close Cut 7's npm question: nothing publishes, nothing unpublishes
The operator's ruling carries a Body fact the maps had not recorded: npmjs
is unreachable from this network, so no GameCult package can be published
there. Cut 7 needed no publish. The QUIC map's Q5 assumed one; that is
raised there separately.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 15:09:20 +02:00
Meta Vens
5670c76234 Record that Huginn lands before StreamPixels, not after
Operator correction: the CultLib QUIC cut may run without Huginn, but the
StreamPixels migration must run with it, because a portfolio piece should
demonstrate the full pipeline on typed state. The campaign no longer pauses
after Cut 6c; Cuts 7-16 follow, alongside QUIC where the repos allow it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 15:05:53 +02:00
Meta Vens
ed09cd919e Record the second Cut 6b fix batch
The harness now restores from the original bytes on every path and checks
its own round trip before it writes; dotted roots and the reader's root-kind
check have tests and mutations; the local bound is one named constant. The
Cut 6 entry notes name the entries that actually pin their rules.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 15:01:05 +02:00
Meta Vens
f00062dbb1 Cut 6b: pin dotted roots, the reader's root kinds, and name the bound
Soul's second pass on the key grammar found four gaps, none in the
grammar itself: nothing pinned them.

No test keyed or read back a root with a dot in it, so both the
composer's root check and the reader's could have been a label check
(Soul's N9 and N10 survived). `dotted_roots_key_and_read_back` keys a
campaign `game.cult` and an instance `ygg.drasil`, reads each back by
`pipeline_id` recovering the root exactly, keys a target under the
dotted campaign and reads it back, and keys a resolution of the dotted
campaign as `game.cult:resolution:campaign.self`.

The reader's kind check was unpinned for the root kinds: excusing the
Instance kind from it survived (N6). `keys_read_back_as_ids_of_their_kind`
is extended, the first change to its body since Cut 6: an instance key
read as an instance succeeds, an instance key read as a campaign is
refused, and a campaign key read as an instance is refused.

The depth test restated the composer's bound as a literal, so moving
the bound and the test's 64s together survived (N4). `LOCAL_MAX` now
names the bound; `local` and the depth test both read it, and that
mutation is no longer a single-site edit. The Cut 6b entries that
anchored the literal, M4 and S5, anchor the name.

Cut 6's M5, M12 and M13 were deleted in d03a32df with notes pointing at
Cut 6b's M6 and S2, which pin neither the tail part nor the escape.
Soul's N3, N7 and N8 are those rules re-anchored: N3 skips the label
check on the last part of a local, N7 makes `key_segment` the identity,
N8 escapes `/` to a bare `_` with `_` unescaped. N6, N9 and N10 join
them in tools/eureka-cut6b-mutations.psd1, and the Cut 6 notes for M5,
M12, M13 and M15 now name the entries that pin them.

Every suite runs green through the harness: Cut 6b sixteen entries
killed (nine prior verdicts unchanged, N3, N6, N7, N8, N9, N10 killed),
Cut 6 ten killed, Cut 5 seven killed, M0 green in each. 21 tests in
epiphany-pipeline, no warnings. schemas/cultnet is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 14:59:27 +02:00
Meta Vens
9370aa0f60 Harness: restore from bytes, count overlapping anchors
Soul's attack on tools/eureka-mutations.ps1 found two faults. M0 proved
the I/O path by writing the target and comparing hashes, so a broken
write path corrupted the target in the act of checking it, and the
harness could not put it back because it had only ever read text. And
"exactly once" counted non-overlapping matches, so an anchor that
overlaps itself, `}\n}\n` in `}\n}\n}\n`, counted as one site and the
splice at the first would have changed a region the entry does not
describe.

The harness now reads every target as bytes and keeps those bytes as
the restore source. M0 decodes, re-encodes through the one path every
write uses, and compares the result to the original bytes before
anything is written; a difference names the target and the first
differing offset, writes nothing, and runs no entry. Only then is the
target rewritten through the write path and hashed, with the original
bytes written back before the throw if the write lands other bytes.
After every entry the original bytes are written back and the file is
hashed against them; the offset reverse edit is gone, since the bytes
it reconstructed were in hand all along. Anchor sites are counted by an
IndexOf loop advancing one character per hit, so an overlapping anchor
is two and refused.

The usage comment records that -Test is split on whitespace with no
quoting, so a path with a space cannot be expressed.

Soul's attack script, re-targeted at the new Write-Text line: the
Set-Content variant, the BOM-encoder variant and the BOM-bearing second
target all leave every target byte-identical, the first two stopping at
M0 with no entry run; the overlapping anchor throws "matches 2 times".
The Cut 6b suite runs green through the fixed harness: M0 green, nine
entries killed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 14:50:43 +02:00
Meta Vens
2a31ae0ab8 Record the first Cut 6b fix batch and Soul's harness attack
The four unpinned checks are pinned and the mutation harness is one script
with a built-in control. Soul then attacked the harness itself and found
the control could rewrite a target it cannot restore, the anchor guard
counted non-overlapping matches, dotted roots and the reader's root-kind
check were unpinned, and Self's own instruction to delete rather than
re-anchor five Cut 6 entries had cost the suite coverage it still claimed.
All of it is in a second fix batch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 14:43:12 +02:00
Meta Vens
d03a32dfc1 One mutation harness for every Eureka cut
The Cut 5, 6 and 6b suites each carried the same scaffold around their
entries, and Cut 6c would have made a fourth. tools/eureka-mutations.ps1
is now the single harness: it takes an entries file, the target files
and the cargo command; reads and writes byte-exact UTF-8; requires every
anchor to match exactly once; applies each edit by offset and reverses
it by the same offset, verified by SHA-256; keeps a compile-must-fail
entry shape; and runs M0 before any entry, rewriting every target
through its own I/O path and running every command bare, stopping with
"harness broken" if anything moves or fails.

The three suites are now entries only, as .psd1 data files. Cut 6's M5,
M12, M13, M14 and M15 matched nothing after Cut 6b rewrote the grammar;
each is deleted with a note naming the Cut 6b entry that pins the rule
now. Cut 6b's file carries Soul's S2, S4 and S5, the mutations the
previous commit's tests kill.

All three suites run green through the harness: every entry killed,
M0 green.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 14:19:38 +02:00
Meta Vens
4b85dd2d0c Cut 6b: pin the four boundaries Soul's mutations survived
The nesting depth was described as "five" when the bound is the 64-byte
local: a chain n deep over a depth-one local of L bytes composes
11 * (n - 1) + L bytes, so `ruling.A` keys six deep and `question.Q1`
five. The arm comment and the test say the arithmetic, and the test
computes the deepest chain from it for both subjects and asserts the
first refusal.

The escape's `_` half was pinned only through an OrgRepo; a receiver
`a_db` and a receiver `a.b` now key differently, the former through
`a__db`. The composer's root check was pinned by `a:b` alone; a trailing
dot, a leading dot, an empty part and a 65-byte root are each refused
by `pipeline_key` without `validate()`. The whole-local bound was
unpinned on both sides; a 64-byte local keys and a 65-byte one refuses,
for a plain label and for an escaped repo.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 14:06:29 +02:00
Meta Vens
103f885afa Record Cut 6b's landing and Soul pass, and fold in the Cut 6c spec
Cut 6b landed in three commits and Soul's whole-key probe found no collision
across 212,450 adversarial keys. Three checks were unpinned (the underscore
half of the escape, the composer's root check, the 64-byte boundary) and are
in a fix batch with Soul's own surviving mutants as their killers. Two map
claims were wrong and are corrected: nesting depth follows from the subject's
local rather than being five, and a validated date passes raw because it is
already a label.

The Cut 6c spec is folded in as reissued against the landed grammar, with the
supersession fixture only the new grammar makes nameable and the mutation
entries pointed at the shared harness the fix batch is factoring out.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 14:03:34 +02:00
Meta Vens
95ee551aa0 Cut 6b: mutation suite for the key grammar
Six mutations, each restoring one of the old permissivenesses the grammar
made impossible, and M0, a no-op control that rewrites the file through
the harness's own I/O path and requires the suite to stay green before
any verdict is trusted. All file I/O is byte-exact UTF-8 without BOM,
since the file carries an `é` literal that the 5.1 default round-trip
corrupts into a fake kill. Every anchor must match exactly once, multi-line
anchors are converted to the file's own line endings, restoration is the
reverse edit checked against the original's hash, and a mutation whose
tree does not build is an error rather than a survival.

Result on this tree: M0 green; M1-M6 each killed by its named test.

This commit builds; it changes no Rust source.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 13:45:39 +02:00
Meta Vens
1bddd2acb3 Cut 6b: one key grammar for every kind
Every key is now `<root>:<kind>:<local>`, three segments with no kind
excused. The root is a `Slug`, the kind is its literal name, and the
local is `Label`s joined by `.`, so no part carries the separator and a
reader recovers the parts by splitting. Roots take the constant local
`self`; a resolution is keyed inside its subject's root with the subject's
kind and local as its own local, so a resolution of a resolution is an
ordinary id bounded by the 64-byte local and needs no guard.

- `local` is the one composer: every part through `label_text`, the join
  bounded whole, once. `pipeline_key` has thirteen arms and one exit.
- `pipeline_id` is the grammar read backwards, with no per-kind branch.
- `repo_segment` becomes `key_segment`: the same injective escape, now for
  any `Slug` or `OrgRepo` entering a local. `hand_off.to_instance`, the
  only raw `Slug` that reached a local, goes through it.
- `parent_cut` indexes the split parts instead of splitting from the right.

Every key moves, and nothing outside this file reads one. No schema file,
kind, type, field, dependency or epoch changes.

Tests: six new (`every_key_has_exactly_three_segments`,
`roots_of_different_kinds_do_not_share_a_key`,
`a_resolution_names_its_subjects_kind`,
`a_resolution_of_a_resolution_reads_back`,
`a_composed_local_is_bounded_whole`,
`no_local_part_carries_the_separator`); thirteen expected keys move in
the samples; `composed_keys_cannot_collide`'s composer probe moves into the
separator test and its dotted receiver now escapes.

Spec deviations, in the report: `local` takes `&[&str]` rather than a
const array because the resolution and finding arms carry a parent's
parts; the whole-local bound refuses `InvalidFormat` to match the spec's
own test and the reader's refusal of the same string; `key_segment` is a
pure escape and each caller validates its source type by its own field.

This commit builds and its tests pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 13:43:09 +02:00
Meta Vens
fcf1ce6bdf Map Cut 6b, the key grammar, and record the pause after Cut 6c
Three fix passes each made one key composition site injective and left the
site above it open. Soul's third pass found campaign and instance sharing a
root namespace, a resolution key discarding its subject's kind, and nested
resolutions composing an unreadable key. That is a missing owner, not three
bugs, so the grammar is redesigned as Cut 6b: root:kind:local, three segments
always, no local part a slug, roots keyed with the constant self. The
exceptions are deleted, not guarded.

The Ghostlight shapes become Cut 6c and land after 6b, once Imagination
reissues them against the settled grammar. The campaign pauses after 6c; the
QUIC realtime plane in CultLib goes first because StreamPixels is the
portfolio piece, and Huginn resumes after it ships.

Also recorded: the second Cut 6 fix batch and the findings it closed; the
mutation-harness audit, which found the corrupting round-trip in no committed
suite but no no-op control in any suite either; corrections 19-24 (the dot
escape, the unstated resolution key, the std::process grep, the ruled
target-dir deviation, the second byte-identical claim, the README's foreign
consumer promise); and Q10-Q12 with recommendations.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 13:39:53 +02:00
Meta Vens
602ffd9fcf Cut 6b: delete the three key-shape exceptions
The key space had four shapes because three of thirteen kinds returned a
key the composer never saw, and the reader undid them with a per-kind
match and a kind-guessing helper. Every confirmed defect lived in one of
those exceptions. This commit removes them before anything replaces them,
so no commit holds both the exceptions and the new composer:

- `declared_kind`, the inference that hardcoded `Campaign` for a bare slug
- the `match kind` block in `pipeline_id` (root arm, resolution arm and
  its recursion)
- `KeyParts`, its constructors, `key`, and the "only the head may carry
  the separator" doctrine comment
- the three early `return`s in `pipeline_key` (instance, campaign,
  resolution)

This commit does not build: `pipeline_key` references `KeyParts` and the
tests reference the deleted shapes. The next commit lands the grammar.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 13:39:39 +02:00
Meta Vens
4a654351e3 Say what the per-run schema directory costs
Soul G3: the comment asserted only the half that improved -- a passing
run leaves nothing -- and was silent about the failing run, which is the
only case that creates a directory at all and now leaks one permanently,
one per failing run where the shared directory was overwritten however
often it failed. The trade is still right; the comment now names its
cost and says who cleans up.

Comment only. No behaviour, so no mutation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 12:53:57 +02:00
Meta Vens
4cccb45c5e Read a resolution back by the key it has
Soul G2: F3's defect survived for `Resolution`, unpinned in both
directions. A resolution's key is `resolution:<subject id>`, and the
reader demanded `<campaign>:resolution:<local>`. The two are disjoint, so
`pipeline_id` refused every id a resolution actually has and accepted one
no resolution can ever carry: a document that can be written but never
referenced, and a reference that names nothing. `PipelineRef` takes any
kind, so both are reachable -- a question's `raised_in`, a follow-up's
`source`, a resolution's own `subject`.

The reader now accepts what `pipeline_key` derives: a `resolution:`
prefix, then the subject's id parsed as an id of the kind that id names
itself as. `declared_kind` reads that kind from the id's own kind
segment, or calls a bare slug a root. A key and an id are the same
string, and now no kind is excused from that.

`keys_read_back_as_ids_of_their_kind` loses its resolution special case:
the loop covers every kind with no exception, which is the pin the last
batch's doc-comment curiosity should have been.

Mutations, each run against the committed suite and reversed after:

- Soul's survivor, `| PipelineKind::Resolution` on the root case: now
  fails `a_resolution_is_named_by_the_key_it_has` and
  `keys_read_back_as_ids_of_their_kind`. It compiles with an unreachable
  pattern warning, because the resolution case is an arm of the same
  match.
- the resolution case falls through to the campaign tail (the landed
  behaviour): the same two tests fail.
- a generous reader that takes the prefix when it is there and falls
  through when it is not: the negative direction fails on its own,
  `eureka-state:resolution:R8` still being accepted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 12:53:12 +02:00
Meta Vens
9b68d83cf8 Compose every key's local one way
Soul G1: the hand-off key was not injective. Its local was
`{to_instance}.{repo}.{date}`, and `.` was admitted on both sides of the
first boundary: `to_instance` is a `Slug`, `repo_segment` passed a dot
straight through, and `OrgRepo` imposes no byte class at all. So
`thought-cage.GameCult` + `Epiphany/thing` and `thought-cage` +
`GameCult.Epiphany/thing` both validated and both keyed to
`yggdrasil:hand_off:thought-cage.GameCult.Epiphany_-thing.2026-09-15`.
One of the two documents was lost. This is the invariant S1 named and the
sentence `label_text` uses to keep dots out of labels.

Neither side could be forbidden: a dotted repo name is ordinary and a
dotted instance slug is what `Slug` is for. So the composition is fixed
rather than the one key. Every non-root, non-resolution key now composes
through `KeyParts::key`: the root is validated, the head may carry the
separator, and every tail part is a `Label`. A kind's tail length is
fixed by the arm that builds it, so a reader recovers the parts by
splitting from the right once per tail part. `repo_segment` escapes `.`
as `_d` alongside `__` and `_-`, which keeps the repo a legal tail part
and keeps the escape injective.

Stewardship and hand-off stop composing their own keys and lose their
early returns: their root is an instance rather than a campaign, and that
was the whole difference. The finding key's separate `label_text` call
goes with them -- the tail rule is that check.

`composed_keys_cannot_collide` now pins the rule on the composer every
kind goes through, so it covers whatever kind is added next rather than
the two kinds it happened to list, plus Soul's hand-off pair.

Mutations, each run against the committed suite and reversed after:

- drop the `.` escape from `repo_segment`: `composed_keys_cannot_collide`
  fails on the hand-off pair.
- let a tail part be `dotted_text` instead of `label_text`: the same test
  fails on the composer rule, `a.b.c` composing two ways.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 12:50:14 +02:00
Meta Vens
9127da3c4c Land Cut 6 in the map, and amend the epoch rule for enum widening
The escape correction is the one that mattered: the map specified a key segment
that could not tell two legal repos apart, and the test that was supposed to
pin it asserted the property in a comment while checking one half of the pair.
The map now carries the injective encoding and the reason the obvious doubling
trick does not work either.

Two record corrections against Hands' own report. The byte-identical claim for
epiphany-state.exe is withdrawn as false rather than unverified, and no hash
replaces it, because that artifact's hash depends on the build root. The
omission in 7ab838f1's message is half what Self first relayed; the other half
belongs to a commit that does describe it.

Correction 14 is the second ledger drift this campaign, both times after a
correction raised one side of an estimate and left the other.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 12:26:06 +02:00
Meta Vens
3ee78e05a4 State that enum widening keeps the epoch
The operator ruled on 2026-09-16 that widening a `PipelineKind` enum is
additive. The README's additive rule named only a new field with a serde
default, which is true and stays, but did not cover the case.

The clause says why in the README's own voice: these readers are ours and each
ships with the variants it knows, so a reader that meets a kind it has never
heard of refuses it on the kind, not on the epoch. That is also the answer to
the tension with the wire note two sentences earlier -- the contracts do cross
out of this repo, and the rule holds because the readers on the other side are
still ours. If that stops being true, the rule needs revisiting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 12:19:14 +02:00
Meta Vens
80db5db6c0 Give the derivation test a directory of its own
Soul F4. The derivation test wrote its stale copies into one shared temp
directory and cleared it first with `remove_dir_all(...).ok()`. The clear was
best-effort and unverified: a held handle refuses the delete on Windows, `.ok()`
swallows the refusal, and a previous run's file survives in a directory whose
path the failure message invites the reader to copy from. The mutation suite
runs M8 and M11 against this test and never cleans up after them, so the
leftovers it would hide are mutated schemas no committed type produces.

The directory is now per-run, named by process id and nanosecond, so there is
no sharing to defend and the clear and its `.ok()` are gone rather than
hardened. It is still created only when something is stale, so a passing run
leaves nothing behind.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 12:17:42 +02:00
Meta Vens
b6f6e8023c Read an instance key back as an instance id
Soul F3. `pipeline_key` made the instance a root and `pipeline_id` was left
behind: it special-cased `campaign` alone and demanded three `:` segments of
everything else, so `PipelineRef { kind: Instance, id: "yggdrasil" }` was
refused as `InvalidFormat` and no resolution could ever take an instance as its
subject. The reader now knows both roots.

Nothing caught it because the key tests assert strings and never read one back.
`keys_read_back_as_ids_of_their_kind` walks every sample, keys it, and parses
the key back as an id of its kind; a resolution is the one kind whose key is
not an id of itself, so its subject is what reads back.

That test also carries Soul F2, which was Soul's surviving mutation: the
instance root's slug was validated nowhere the key could see, so an instance
named `thought cage` keyed to `thought cage` -- a store key holding a byte no
`Label` may carry. Stewardship and hand-off validate every slug they key by;
the instance now does too, and M14 is Soul's survivor turned into an entry.
M15 puts the shipped reader back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 12:16:50 +02:00
Meta Vens
eb55efe9c9 Make the repo key escape injective
Soul F1. `/` -> `_` is not injective: `GameCult_Epiphany/thing` and
`GameCult/Epiphany_thing` both key to
`yggdrasil:stewardship:GameCult_Epiphany_thing`, so an instance stewarding both
silently loses one document. Keys are identity, not convenience.

`_` is now escaped too, and both codes are two bytes starting with `_`: `_`
becomes `__` and `/` becomes `_-`. Every other byte passes through and is never
`_`, so a reader going left to right takes each `_` with the byte after it and
never has a choice to make. The encoding is reversible, therefore injective.
That is the smallest change that buys the property: nothing else moves, and the
segment stays inside the `Label` byte class.

The test claimed the property and checked one half of the colliding pair, which
asserts nothing about collision. It now keys both halves and asserts they
differ. M12 is re-anchored on the new spelling, and M13 is new: it puts the
shipped `/` -> `_` back, which the pair kills.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 12:14:46 +02:00
Meta Vens
08c1c9d907 Carry the mutation suite across the package boundary
Every rule the moved code carries keeps a test that fails under its own
mutation, so the suite moves with the code rather than being left pointing at a
file that no longer exists. M1-M9 are Cut 4's entries re-anchored into
`epiphany-pipeline/src/lib.rs`; each entry now names its own package as well as
its own file, because M10 still mutates the spine in epiphany-core. All twelve
were run and all twelve were killed by their own test.

M10's mutation changed shape. It used to register the pipeline documents into
the spine's schema cache, and epiphany-core no longer has them; it now declares
its own type carrying `epiphany.pipeline.campaign.v1` inline, exactly as the
test it attacks now does.

Two entries are new. M12 keeps the repo escaping honest: drop the `/`-to-`_`
substitution and `stewardship_key_escapes_the_repo_slash` fails. M11 is the
cut's "change a value type and leave its schema alone" mutation, expressed as a
new enum variant rather than a new field: a field cannot be added without
breaking every struct literal that builds the value, which fails the whole
target and would prove nothing about the derivation test in particular. M11
changes exactly one derived schema, and the test names exactly that one.

`tools/eureka-cut4-mutations.ps1` is deleted rather than left to throw. Every
one of its ten entries is above, and its first act was to read a file this
branch moved, so it could not run again. It still runs at `a9f06c2a`, where its
receipt is.

The derivation test now empties its derived-schema directory before writing to
it. It did not, and this pass nearly published a schema from an earlier
mutation run's leftovers: the directory is shared, `Short` had `maxLength: 201`
in it from Cut 4's M8, and a copy of everything in there landed in the working
tree before the diff caught it. The gate that exists to stop a hand-written
schema should not be the thing that hands one over.

The stale-anchor guard is unchanged and still hard: an anchor that matches
anything but exactly once throws before a single test runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 11:46:30 +02:00
Meta Vens
d17cc4415f Stop telling readers pipeline state lives in a repo
The Main Families entry still said pipeline state is "stored per repo at
`.epiphany/pipeline/pipeline.cc`", which Cut 4 deleted, and the wire note still
sent these contracts to the voidbot projection. Both describe a machine that no
longer exists.

Pipeline state now lives in an instance's mind, owned by the Huginn memory
organ, and the contracts cross to that organ and its `eureka-state` client. The
three kinds this branch added are named, since the list is the entry's point.
The `[value]` payload sentence, the derivation sentence and the Q2 evolution
sentence stand; the derivation sentence now says which package runs the check.

Documentation only: no build, no test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 11:40:25 +02:00
Meta Vens
7ab838f167 Extract epiphany-pipeline, and add the three kinds a mind is keyed by
Ruling 1 keeps schema ownership in Epiphany; ruling 17 puts admission in
Huginn. Huginn must not take on epiphany-core -- about 41,000 lines and 295
lock packages -- to use about 1,000 lines of document types. So the documents
leave as a leaf library, and epiphany-core keeps no pipeline surface at all:
its consumption is campaign two.

`epiphany-core/src/pipeline_documents.rs` moves whole to
`epiphany-pipeline/src/lib.rs`, with its nine tests. Only three things changed
in the move: the module doc now describes a package rather than a cut map
section; `anyhow` joins `rmp-serde` behind `cfg(test)`, because the write path
has been test-only since 186dbc62 and the live library's errors are all
`PipelineRefusal`; and the decode refusal, which was pinned against
epiphany-core's Mind commit receipt, is pinned against a local `ForeignDocument`
carrying that receipt's type id, since the receipt belongs to the harness.

The package declares four dependencies. chrono parses a Date, cultcache-rs
supplies DatabaseEntry, schemars derives the published schemas (Q5), and serde
is the document format. anyhow, rmp-serde and serde_json are dev-dependencies:
every use of them is `cfg(test)`. `autobins = false`, as a leaf library must.
The workspace lock grows by exactly one entry, the member itself; no new
third-party package.

Deleted rather than re-exported: epiphany-core's `mod pipeline_documents;` and
`pub use pipeline_documents::*;`, and `schemars` from its manifest, whose only
user was this module. No shim, no duplicate type.

`runtime_spine_cache_refuses_a_pipeline_store` stays in epiphany-core, because
the rule is the spine registry's: it registers its own types and nothing else.
It now builds its foreign envelope from a `cfg(test)` stand-in carrying
`epiphany.pipeline.campaign.v1` rather than from the moved documents, so the
rule keeps its test without the package keeping a dependency.

D2's three new kinds arrive with the move, each with a live consumer in the
organ: `instance` is a mind's identity document, `stewardship` records the
repos an instance is assigned, and `hand_off` records a reassignment in both
minds. All three hang off an instance rather than a campaign, so they compose
their own keys instead of borrowing the `<campaign>:<kind>:<local>` tail. A
repo inside a key is escaped to `Org_Repo`, because `/` is not a Label byte.

Thirteen schemas now derive. The five that embed `PipelineKind` gained three
enum variants, which is additive, so Q2 keeps `epiphany.pipeline.epoch.v1`.

epiphany-pipeline: 12 tests, 0 warnings. epiphany-core: 163 tests -- 172 minus
the nine that moved -- 0 warnings. epiphany-state.exe is byte-identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 11:40:12 +02:00
Meta Vens
93463160b3 Close Cut 5: pin one handle and every write, and correct the receipt count
The regression Soul found is pinned at its real layer: a backing store carries
only its path, so no assertion about files or receipts can see a same-path
re-derivation, and the test now counts resolutions instead. A batch of two
writes with the second refused pins that every write is validated.

The map's own claim of one receipt construction site was wrong in two places,
including an authority-map line. There are two: the owner's, and a test that
plants one on purpose to pin the replay rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 11:23:43 +02:00
Meta Vens
617c397d88 Close Soul's Cut 5 findings: pin one handle, and every write
Soul mutated the collapsed commit owner twice and the suite survived both.
Neither rule was pinned; both are now, each by a test that fails under its own
mutation, and both mutations join the suite so the gap cannot reopen.

S7, a regression from base. The owner resolves the backing store once and
reuses it for the cache load, the CAS and the conflict re-read, and its doc
comment says no path in the function can target a second store. Re-deriving the
CAS handle from the same path left the whole suite green. The old profile test
had caught it only because its store was deliberately not the commit path; the
replacement asserted the temp dir holds one store, which a same-path
re-derivation satisfies exactly. A handle carries nothing but its path today,
so no assertion about files or contents can see the second one. The `.redb` arm
makes the difference real -- redb permits one writable handle per path -- and
the map plans redb for the organ's minds.

So the probe is the resolution itself: `runtime_spine_backing_store` records
every path it hands out, under `cfg(test)` only, and
`mind_commit_reads_and_writes_one_store` asserts a commit resolves its store
exactly once. The log is per-path and compared as a delta, so tests running in
parallel cannot see each other. Mutation MS1 re-derives from the same path and
the count reads 2.

S8, pre-existing. Narrowing validation to the first write also survived,
because no test committed a batch carrying more than one distinct write. The
existing mutation neuters validation for every write, which any single-write
batch catches; nothing covered the loop's reach. Added
`mind_commit_validates_every_write_in_the_batch`: two distinct writes, the
second refused, and the batch is refused whole with nothing written -- the
valid first write included. Mutation MS2 takes the first write only and the
test gets a commit where it expected a refusal.

M2's rule text is corrected to match what it actually pins, now that MS2 owns
the stronger half.

S10, cosmetic. The renamed test's fixture still said "mind-profile-test" after
the profile it named was deleted. The pinned receipt digest does not depend on
it and is unchanged.

Builds clean: 172 tests, 0 warnings. Seven mutations, each killed by its own
test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 11:21:16 +02:00
Meta Vens
bf990d1869 Record Soul's Cut 5 findings and the batch closing them
Soul confirmed the collapse changed no Mind behaviour, reading the owner diff
where the pinned digest is blind. It found that the replacement test pins only
"one file in this directory", so re-deriving the backing store at the CAS
survives, and that validating only the first write of a batch survives too.
Neither is a live bug today; the first is a regression from base and matters
once redb arrives, and the second is pre-existing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 11:02:04 +02:00
Meta Vens
c18e334ab7 Record Cut 5 landed: the commit profile is collapsed
TypedCommitStore, its const, its validator and the parameter are gone, with
nothing of the profile shape kept. The Mind receipt digest was proven unmoved
by running the pinning test at the base commit in a detached worktree, not by
a value captured from the new code.

Three corrections stand: ruling 20's uniqueness-ordering half had no named
test until this cut added one; the Deletes table's line anchors were stale by
26 lines, so later cuts should re-anchor before trusting a line number here;
and the subtraction estimate missed the test rewrite the Keeps list implied.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 10:50:13 +02:00
Meta Vens
4c091f57db Commit the Cut 5 mutation suite
Five rules, five tests, five mutations, each killed by its own test. Three come
from the cut spec (drop the epoch check from the opener, make the validator a
no-op, make the owner read one store and write another) and two are Soul's
finding S6, which is the reason this collapse is allowed to touch the Mind path
at all: ruling 12 put write validation after identity-uniqueness and after the
store opens, and before the replay check.

The order rules cannot be expressed as one string replacement, because moving a
statement is a deletion and an insertion. So an entry here carries a list of
edits rather than Cut 4's single Old/New pair, and every edit is still asserted
to match exactly once and to change something. A stale anchor throws instead of
reporting a verdict it did not earn. Files are restored in a finally block, and
every file an entry touches is restored, not just the last one.

M4 and M5 are the two halves of the order. M4 hoists validation above the
uniqueness checks and the duplicate-batch test catches the wrong refusal. M5
moves validation below the replay answer, and the planted-receipt test gets a
stored receipt handed back for a batch the validator refuses, which is exactly
the state S6 says must be impossible.

Builds: no code change. The script is not wired into any build or test target;
it is run by hand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 10:44:55 +02:00
Meta Vens
1758ad5006 Collapse TypedCommitStore into the Mind commit owner
Cut 2 generalised the one commit owner into a profile so a pipeline profile
could share it. Ruling 20 sends admission to Huginn, so the second profile left
the repo and the abstraction had exactly one implementation. A
one-implementation abstraction is a named STOP condition, so it goes. Cut 2's
generalisation did not pay off; this says so rather than keeping the shape to
avoid admitting it.

Deleted: `struct TypedCommitStore`, `const MIND_COMMIT_STORE`, the
`store: &TypedCommitStore` parameter, `fn validate_mind_writes`, and the
`&MIND_COMMIT_STORE` argument at each of the five Mind wrappers. The three
choices the profile carried are re-inlined at their one use: the backing store,
the runtime spine opener, and the write validator, plus the literal
"epiphany-mind" as the receipt's store id. `RuntimeSpineBackingStore` lost its
last use in this file and left the import.

Behaviour is unchanged, including the pinned receipt digest
mind-commit-sha256:415283...281e. The owner's order is unchanged and stays
fail-closed: identities unique, store open (which refuses a foreign epoch),
writes validated, and only then may a stored receipt answer a replay.

The profile's two tests could not survive the thing they tested, so their rules
are re-expressed against the concrete owner. The two profile-shaped tests are
replaced by three that reach the same rules through the Mind path:
validation-before-replay now plants exactly the receipt the batch would produce
and asserts the refusal wins; uniqueness-before-validation pins which refusal a
duplicated invalid batch reports; and one-store asserts a commit touches only
the store it was given. Two surviving tests lost "profile" from their names,
which had become a lie.

Builds clean: 171 tests, 0 warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 02:44:40 +02:00
Meta Vens
cda365419b Close Cut 4: the write path is test-only and the suite is exact
Soul proved the two dead-code waivers were hiding genuinely dead items, so the
pipeline write path is now cfg(test) and says what it is; decode and the cache
imports came with it. The mutation suite documents the interpreter this host
has and asserts each anchor matches exactly once.

Cut 6 inherits four gated items rather than two live ones, and must refresh a
stale line citation. The refusal variants still have no reachable non-test
raiser, which is Cut 8's to fix when the organ exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 02:33:35 +02:00
Meta Vens
06100d8e30 Make the mutation suite runnable and its anchors exact
Soul's F10 and F11 on the Cut 4 fix batch.

F10: the header documented `pwsh -File`, but this host has no pwsh — only
Windows PowerShell 5.1. Document the invocation that actually runs, and refuse
anything older or non-Windows up front with a message naming the supported
call, rather than failing later on a cmdlet or path difference.

F11: the anchor guard only proved a match existed, while `.Replace` mutates
every match. A second site would have changed more than the entry describes,
and the named test could then fail for a reason the entry does not name — which
reads as "killed". Assert exactly one site instead. Every current anchor already
matches once, so this guards against growth, not a live defect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 02:31:18 +02:00
Meta Vens
186dbc62d7 Say that the pipeline write path is test-only
Soul's F9 on the Cut 4 fix batch: `prepare` and
`validate_pipeline_write_envelope` still wore
`cfg_attr(not(test), expect(dead_code))`, and Soul proved both dead in a
non-test build. `prepare` was structurally unreachable — it needs a CultCache
with the pipeline types registered, and the only registrar has been `cfg(test)`
since this cut. A waiver that describes a caller Cut 8 has not written yet is
the file claiming a live path it does not have.

Both become `#[cfg(test)]`, which is what they honestly are, and the waivers go.
Two things are test-only with them: `decode`, whose only caller was the
validator, and the `CultCache`/`CultCacheEnvelope` imports, which looked live
only because dead code mentioned them. The live path keeps the type ids.

Cut 6 now moves four `cfg(test)` items rather than two live ones; the spec's
Keeps list still holds, since what moves is the same code under a different
gate. The refusal picture is unchanged: `ForeignStore` and `InvalidIdentity`
were already raised only from these two, and the enum stays `pub`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 02:31:18 +02:00
Meta Vens
07f19d3635 Record Cut 4's Soul findings closed by deletion
Soul found the subtraction had taken a live rule's only test with it: the epoch
guard survived a `if false` mutation, and nothing could construct an identity
envelope to reach it. Rather than test state nothing can build, the epoch
branch, ForeignEpoch, the identity, provenance and faculty types and the epoch
constant are deleted; Cut 8 writes them in the organ where a test can reach
them.

Two live rules are pinned instead: decode refuses a foreign-typed envelope with
ForeignStore specifically, and the runtime spine cache refuses a pipeline
store. Four further corrections stand against the sections above, including the
Keeps list and the test count.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 02:05:09 +02:00
Meta Vens
8a598ebbed Pin the two restored rules with mutations, and match anchors to the file
M9 and M10 are the mutation half of the previous commit: a rule with no mutation
is a claim, not a pinned rule.

M9 replaces the `ForeignStore` refusal in `decode` with a payload format error.
The envelope is still refused, so a test asserting only "this fails" would pass
either way; the test asserts the refusal itself, and dies. M10 registers the
pipeline document types into `runtime_spine_schema_cache`, which is exactly the
mechanism the spine refuses a pipeline store with — the registry, not the epoch
check. With the types registered the store opens and the test dies. Both aim at
the rule rather than at an arbitrary neighbouring line, which is what makes the
kill mean something.

The suite now spans two files, so every entry names its own, and the first run
caught a rake worth keeping: the anchors here are written with plain newlines
and the working tree is CRLF, so a multi-line anchor matched nothing. M1 to M9
are single-line and never spanned a newline, which is why the flaw surfaced only
when M10 arrived. Normalizing towards LF was backwards; the anchor is now
converted to whatever endings the file has. The stale-anchor throw did its job:
it refused to report a pass it had not earned.

Ten mutations, ten killed, each by its own named test alone. Sources are
restored byte-identical to HEAD afterwards.

Builds: no code change; the script is run by hand and is wired into no target.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 02:03:14 +02:00
Meta Vens
43a08baa3c Delete the unconstructable identity cluster and pin the two live refusals
Soul's Cut 4 pass found one rule no test could reach and two with no test left
standing over them. F1 is the sharp one: `validate_pipeline_write_envelope`
raised `ForeignEpoch` for an identity envelope, but `EpiphanyPipelineIdentity`
had no constructor anywhere in the repo, so no test could build one to reach the
branch. Replacing the guard with `if false {` left all 168 tests passing. Code
that cannot be constructed cannot be tested, and testing it would have meant
writing the constructor the organ is going to own anyway.

So it goes, rather than growing a test to justify it. Deleted: the epoch branch
and the `ForeignEpoch` variant, `EpiphanyPipelineIdentity`,
`EpiphanyPipelineProvenance`, `PipelineProvenance`, `Faculty`, and
`PIPELINE_SCHEMA_EPOCH`, whose only reader was the deleted branch. D2 already
assigns admission, identity and the epoch refusal to `huginn-mind`; Cut 8 writes
them there against a store its tests can construct. Carrying about fifteen
unreachable lines across two repos costs more than re-adding them where they
work. Every one of the four types was checked for a live producer first: none
has one, in code, test, schema file or `index.json`.

What stays is what is reachable: `validate_pipeline_write_envelope` keeps its
bounds, formats and key recomputation, and `PipelineRefusal` keeps four variants,
each with a raiser.

Two tests for the two rules that survived Cut 4 untested. F2:
`decode_refuses_an_envelope_of_a_foreign_type` pins the `ForeignStore` refusal
against a commit receipt — a type a pipeline store may legitimately hold, and
still not a document. The existing round-trip test pinned only the positive
match. F4: `runtime_spine_cache_refuses_a_pipeline_store` restores coverage that
died with `pipeline_store.rs`, against the surviving mechanism in
`runtime_spine.rs`. It asserts first that `validate_runtime_store_epoch` passes
a pipeline store, because every neighbouring test is epoch-based and the epoch
checks cannot see this: the registry is the only thing refusing.

`register_pipeline_document_types` is now `#[cfg(test)]` instead of a live path
wearing `expect(dead_code)`. Its only callers are tests, and saying so is
honest; the organ registers its own types in Cut 8.

Builds: `cargo test -p epiphany-core --lib`, 170 passed, 0 failed, 0 warnings.
`cargo check -p epiphany-release-bundle --bin epiphany-state` clean. Mutations
for both new rules land with the next commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 01:56:27 +02:00
Meta Vens
592b79cabd Record Cut 4 landed and correct the spec's refusal and test arithmetic
The repo-owned store layer is gone: 1,302 lines removed, one store format, one
lease and two git preconditions retired. Eight tests moved with the code rather
than waiting for Cut 6, so no rule sits unpinned between cuts.

Four corrections stand against the sections above: the local PipelineRefusal
keeps five variants because decode and validate_pipeline_write_envelope still
raise two of them; the deleted module held 20 tests, so the count is 168; Cut
6's estimate grows to about -1,060 now that the tests travel with the module;
and the schemas README still describes the old store path, which Cut 6 owns.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 01:32:18 +02:00
Meta Vens
a9f06c2ae5 Commit the Cut 4 mutation suite
Soul's standing finding S9: the previous pass claimed 27 mutations with no
artifact on disk, so the claim could not be checked. This is the reproducible
form. Each entry names one rule a surviving test claims to pin and the exact
source change that removes it, as a literal string replacement in
`epiphany-core/src/pipeline_documents.rs`. The script applies one mutation,
runs that test alone, restores the file, and fails if any mutation survives.

Eight rules, one per test carried into this cut: type-matched decode, UTF-8
byte bounds, the single-slash `OrgRepo`, key recomputation at validation, the
dot-free finding label, the parent marker character, a `PipelineRef` id parsed
as a full id of its kind, and the byte-for-byte schema derivation.

Builds: no code change. The script is not wired into any build or test target;
it is run by hand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 01:24:28 +02:00
Meta Vens
ca275c7b35 Delete the repo-owned pipeline store, lease and git layers
Cut 4. Rulings 14 and 15 moved the state off the repo: an instance owns its
mind and a service owns that state. Every layer that existed only to make a
repo-resident, git-committed, multi-clone store safe is now dead weight.

`epiphany-core/src/pipeline_store.rs` goes whole, 1,251 lines: the commit
profile, the epoch/foreign-type opener, the committed-blob checks, the
main-work-tree resolution, the read handle, the writer lease and its branch
binding, the holder record, and the 20 tests that pinned those rules. With it
go `PipelineWriterHolder` and its wrapper, `validate_pipeline_writes`, the two
`lib.rs` lines, the `.gitattributes` binary marker and the `.gitignore` lock
line. No Epiphany code can now open a pipeline store, take a lease, or read
git for pipeline purposes.

`pipeline_documents.rs` gains a local `PipelineRefusal` in place of the import.
D2 splits the landed thirteen variants: the six repo-store refusals
(`NotRepoRoot`, `NoMainWorkTree`, `StoreNotMarkedBinary`, `LockNotIgnored`,
`WrongBranch`, `WriterLeaseHeld`) die here, and so do `MissingIdentity` and
`Unavailable`, which lost their last raiser with the opener and the lease.
`ForeignEpoch` and `ForeignStore` stay because `decode` and
`validate_pipeline_write_envelope` still raise them; D2 hands them to the organ
when admission moves.

The seven document tests the cut map names, plus the schema derivation test,
move into `pipeline_documents.rs` rather than waiting for Cut 6, so no rule
sits unpinned in between. They lose the git scaffolding that has nothing left
to set up, and the key test now validates one envelope directly instead of
going through the deleted batch validator. Sample payloads that named the
deleted file were rewritten so the negative greps are honest.

This cut removes an owner and installs no replacement; the organ becomes the
owner in Cut 8.

Builds clean with no warnings: `cargo check -p epiphany-core --lib --tests`
and `cargo check -p epiphany-release-bundle --bin epiphany-state`.
`cargo test -p epiphany-core --lib` passes 168/168, down from 180: 20 tests
died with the file and 8 came back with the documents.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 01:24:28 +02:00
Meta Vens
beff45719a Rule Q6-Q9: declared identity, WireGuard, collapse, shared Qdrant
Operator ruling (2026-09-16, "all recommendations ok"): instance identity is
declared attribution rather than signed authentication on a single-operator
LAN; the workstation reaches the organ over WireGuard, because RUDP is UDP;
TypedCommitStore collapses now that its second profile lives in another repo;
and the organ shares voidbot's Qdrant with the coupling declared as an Idunn
dependency.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 01:09:54 +02:00
Meta Vens
304832ad6c Re-map Eureka onto the memory organ and fence the old model as history
Imagination rewrote the means for instance-owned minds: Cuts 4-16 cover
subtraction, the epiphany-pipeline leaf library, Huginn's retirement and its
new huginn-mind and huginn-daemon bodies, Qdrant and Ollama, hand-off and
import, the eureka-state MCP client, deployment, skill wiring and a proof.
Cuts 1, 2 and 3a and the dead rulings sit under a history fence.

pipeline_store.rs dies whole (1,251 lines), and Epiphany's net change across
the campaign is negative. Admission moves to Huginn rather than dragging
epiphany-core into the daemon; Epiphany keeps the documents and schemas
through the leaf library, so ruling 1 holds.

Self bound Soul's S-labels to the new layers and corrected five places where
the target had drifted from the Body: the Ollama endpoint, the scope of
Huginn's authority vacancy, the surviving refusals, the thirteen document
kinds, and import not being a merge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 01:03:25 +02:00
Meta Vens
5fb4eb222c Rewrite the Eureka target onto instance-owned minds in a memory organ
Operator rulings (2026-09-16), superseding the repo-owned store:
- an instance owns its mind, and stewardship over repos is an assignment;
- a service on Yggdrasil owns the state, so minds leave version control and the
  per-clone lease, git preconditions and divergence go with them;
- the organ depends on Qdrant directly, not on voidbot, and embeds through
  Ollama on Nightwing;
- Huginn becomes that organ, and generic .cc inspection belongs to CultCache
  Studio.

The organ speaks CultNet with eureka-state as a thin stdio MCP client; an
unreachable organ refuses loudly rather than spooling. Cut 3a's documents,
keys, validation and refusals survive; its store, lease and git layers do not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 00:31:34 +02:00
Meta Vens
79b87eee66 Record the Cut 3a fixes and the two accepted deviations
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 23:15:50 +02:00
Meta Vens
a317d4cf26 Isolate the marker rule and the enclosing-repo case
The first attempt at these two tests still failed no mutation, because in both
cases another check answered first. A test that only passes is not evidence; it
has to be the thing that fails when its own rule is removed.

The marker rule. `cut-3a.h1` and `cut-3a.s1` cite the wrong kind's marker, but
`h1` and `s1` are not digits, so the digits rule refuses them whether or not the
marker is required. `cut-3a.1` is the case that isolates it: a well-formed
number with no marker at all, which only the marker rule refuses.

The enclosing-repo case. A separate git dir in its own temp directory is refused
even without the `.git` check, because the parent is not a work tree and the
toplevel verification fails anyway. The case the check actually exists for is a
clone whose git dir sits inside *another* repo's working tree: there the common
dir's parent is a real, valid work tree, so without the check `main_work_tree`
resolves to the enclosing repo and the store lands in a tree that does not own
it. The test now builds exactly that, and asserts the enclosing repo's store is
never created.

Builds clean with no warnings; `cargo test -p epiphany-core --lib` passes
180/180.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 23:11:46 +02:00
Meta Vens
187e01e7b3 Pin the marker rule and the odd-layout refusal on their own
The mutation pass found two rules that no test failed for, both because an
earlier check answered first.

The marker rule. Every bad parent in `parent_ids_are_parsed_strictly` also
violated the digits rule, so removing the `<marker>` requirement altogether
(Soul's S3) still refused every case and killed nothing. Two cases now cite the
right kind with a well-formed number and the wrong kind's marker, which only the
marker rule refuses.

The odd-layout refusal. `a_bare_clone_has_no_main_work_tree` asserted
`NotRepoRoot`, which a bare clone hits before `main_work_tree` is ever reached,
so `NoMainWorkTree` was unpinned and dropping the `.git` check failed nothing.
The test is renamed to `a_bare_clone_is_not_a_repo_root` for what it actually
pins, and `a_separate_git_dir_has_no_main_work_tree` covers ruling 10's odd
layout: a work-tree top level whose git dir is not a sibling `.git`, refused on
both the write and the read path.

Builds clean with no warnings; `cargo test -p epiphany-core --lib` passes
180/180.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 23:06:20 +02:00
Meta Vens
b4f88d2980 Close Soul's Cut 3a findings and land rulings 10-12
One store per clone, numeric attempt and pass, and identity enforced by the
profile that owns the commit. The three files change together because the
profile's validator signature, the documents that implement it and the store
that installs it are one interlocking contract; no smaller split builds.

Ruling 10, one store per clone. `main_work_tree` resolves the clone's main
working tree from any worktree through the git common dir, and both
`PipelineStore::open` and `PipelineWriter::attach` go through it, so a caller
in a worktree reads and admits into the main tree's store. A bare clone or any
layout without one main tree refuses `NoMainWorkTree`; there is no
worktree-only mode. `require_branch` binds to the main tree, so a campaign
whose branch is checked out only in a linked worktree is refused rather than
quietly given a second store.

Ruling 11, numeric labels. `attempt` and `pass` are `u32`, keyed
`cut-<cut>.h<N>` and `cut-<cut>.s<N>`.

Ruling 12, identity on first write. `TypedCommitStore` now validates a whole
batch against the image it will be applied to (`validate_writes`), replacing
the per-envelope hook rather than adding a second one. The pipeline profile
refuses a first write that does not carry the identity, so the single owner can
never admit a store its own opener refuses (F1).

F2: a finding label carries no dot and parent locals parse strictly, so two
documents cannot compose one key. Soul's collision pairs are tested.
F3: parent ids are parsed with an exact segment count, a campaign match, a kind
match, a validated local, and a marker followed by digits.
F4: a resolution's subject id is validated as a full id of its declared kind,
by `PipelineRef` itself, so kind and id cannot disagree anywhere.
F5: the ten document wrappers, the identity, provenance and holder wrappers and
their value fields are crate-private, and the crate's only commit path to a
pipeline store is a `cfg(test)` helper taking `&PipelineWriter`. Binding the
owner to the lease is Cut 3b.
F6: covered by ruling 10, with the previous claim corrected: a worktree does
not keep its own store, and a second attach from any tree is refused.
F7: the holder record is written only under the lock and removed on drop, and a
refusal names a holder only when it is plausibly current, matching host and
live process incarnation. A record left by a crash names nobody.
F8: the git checks read the committed blob at HEAD, so they answer the same on
every machine. Attribute and exclude files are nulled on every git child. The
working tree and `.git/info/exclude` deliberately do not count, because the rule
is about what the repository commits.

Low items: `value_types!` is the single field list, emitting each struct and its
`Bounded` impl from the same tokens, and format rules moved into the field types
(`Label`, `Slug`, `OrgRepo`, `FullSha`, `Sha256Hex`), so a field cannot be
declared without being validated and a list without a cap does not compile. That
applies the `Org/Repo` check to every repo field. `LandedNameKind` is deleted and
`LandedName` is a name and a path. The schemas carry `maxLength` and `maxItems`.

Builds clean with no warnings; `cargo test -p epiphany-core --lib` passes
179/179.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 22:55:00 +02:00
Meta Vens
0c3efe32fe Make the session the operator channel, with swappable notification
Operator ruling: Eureka has no Persona. Self is the operator surface, and a
blocking question may be pushed through whatever notification MCP the user
configures, so Eureka owns no transport. Answering over a chat channel is a
later campaign.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 22:41:39 +02:00
Meta Vens
74ba6aa9f6 Record Cut 3a landed, its Soul findings, and rulings 10-12
Operator rulings (2026-09-15):
- one store per clone, in the main working tree, with worktrees admitting into
  it;
- numeric attempt and pass numbers, counted per cut;
- the commit profile owns identity-on-first-write.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 22:27:25 +02:00
Meta Vens
ad18c385dc Add pipeline documents, store opener, writer lease and schemas
Cut 3a of the Eureka pipeline state campaign (cut map D1-D3).

- pipeline_documents.rs: the ten kinds as serde + JsonSchema values in
  one-slot DatabaseEntry wrappers (epiphany.pipeline.<kind>.v1), the
  identity, provenance and writer-holder documents, UTF-8 byte bounds,
  semantic key derivation, and the pipeline write validator (bounds, then
  key recomputation).
- pipeline_store.rs: PipelineStore::open (read-only, no lease),
  PipelineWriter::attach (fs2 lock in the git common dir, display-only
  holder file, binary/ignore checks), require_branch, PipelineRefusal,
  and PIPELINE_COMMIT_STORE on the one commit owner. The opener refuses
  foreign types, missing identity and foreign epochs before attaching.
- schemars 1 is an unconditional core dependency (Q5-A); the ten published
  schemas are derived and checked byte-for-byte, with index.json entries.

The profile and the prepare path carry expect(dead_code) naming Cut 3b as
their consumer, so the attribute fails the build once admission uses them.

Builds: yes. cargo test -p epiphany-core --lib: 171/171, 0 warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 21:55:04 +02:00
Meta Vens
a1473c452b Mark the pipeline store binary and ignore its lock
Cut 3a (D2): git never merges the bytes of .epiphany/pipeline/pipeline.cc,
so every divergent merge goes through the merge command. Without `binary`, a
NUL-free MessagePack store is classified as text on a core.autocrlf=true
machine (probe P8). The per-operation cultcache-rs sibling lock is ignored.

The published pipeline schemas are pinned to LF so the byte-for-byte
derivation test holds on a CRLF checkout.

Builds: no code change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 21:55:04 +02:00
Meta Vens
5f98228d15 Record the Cut 2 fixes landed and the replay decisions for Cut 3b
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 21:34:22 +02:00
Meta Vens
cb6ef5d2c3 Make the commit profile own its backing store and pin its rules
Soul's Cut 2 findings, fixed before Cut 3a.

- TypedCommitStore gains `backing_store: fn(&Path) -> Result<RuntimeSpineBackingStore>`,
  and `open_cache` now takes the resolved store instead of a path. The owner
  resolves the store once per commit and uses it for the cache load, the batch
  CAS, and the conflict re-read, so a profile can no longer read one store and
  write another. The hard-coded `runtime_spine_backing_store(store_path)` calls
  in the owner are gone.
- `runtime_spine_cache` delegates to the new `open_runtime_spine_cache`, which
  the Mind profile uses as its opener. The epoch check stays in that opener.
- The profile's implicit contracts are documented on the type.
- Tests: the test profile's backing store is a sibling of the commit path, so
  re-deriving the store from the path fails; a foreign-epoch Mind store refuses
  a commit with nothing written; a replay through a profile whose validator now
  refuses the batch is refused rather than answered with the stored receipt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 21:20:33 +02:00
Meta Vens
b5324face7 Record Eureka Cut 2 landed with its Soul findings
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 21:15:22 +02:00
Meta Vens
46460efc80 Parameterise the one receipt commit owner by a store profile
Cut 2 of the Eureka pipeline state campaign, behaviour-preserving.
commit_authorized_mind_mutation stays the only code that builds receipts,
replays and runs batch CAS, but no longer hard-codes the Mind store:

- TypedCommitStore { store_id, open_cache, validate_write } is its first
  argument.
- MIND_COMMIT_STORE = { "epiphany-mind", runtime_spine_cache,
  validate_mind_write_envelope }, passed by all six wrappers.
- Deleted from the owner: runtime_spine_cache(store_path),
  validate_mind_write_envelope(write), and the two "epiphany-mind"
  literals on receipt versions.

The backing store for CAS stays runtime_spine_backing_store(store_path).

Tests:
- typed_commit_store_profile_owns_open_validate_and_store_id: a test
  profile with a two-type registry and a key-refusing validator; a
  refused write leaves the store byte-identical, and receipt versions
  carry the profile's store id.
- mind_commit_store_profile_keeps_mind_validation_receipts_replay_and_conflicts:
  through a Mind wrapper, Mind semantic-identity validation refuses
  without writing, receipt versions carry "epiphany-mind", the receipt id
  equals a pinned digest, exact replay returns the stored receipt, and a
  stale strong read maps to Conflict naming it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 21:00:57 +02:00
Meta Vens
00991c1b03 Pin storeless reads by directory snapshot, not one mtime
Soul's Cut 1 finding F2: the two read pins only proved the seeded store
file's mtime was unchanged, so a store attached at a shadow path (every
read writing path.with_extension("shadow.cc")) survived all 156 tests.

state_cache_reads_without_writing_the_store and
persona_social_cache_reads_without_writing_the_store now snapshot every
directory entry (name, length, mtime) before and after the read, require
exactly the store and its lock file, and require the snapshot unchanged.
cultcache-rs at a0813c6 exposes no backing-store count on CultCache, so
the directory is the observable layer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 20:55:50 +02:00
Meta Vens
589e71c321 Record Eureka Cut 1 landed with its Soul findings
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 20:49:56 +02:00
Meta Vens
df82992c60 Pin that Self and Persona social cache reads never write their stores
The re-pin swapped load_envelope for put_envelope on the claim that each
site's cache has no store attached, so put_envelope stays memory-only.
No existing test observed that: attaching the store in state_cache left
every resident_self test passing, because re-pushing the envelope just
read rewrites identical bytes.

Each test seeds its store, sets the file's modified time to a fixed past
instant, reopens the cache and requires the time to be unchanged. Any
push replaces the file through a staged rename, so attaching a store to
state_cache or persona_social_cache now fails the matching test. The
retirement-receipt site takes a snapshot slice with no path, so it cannot
acquire a store without a signature change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 20:34:55 +02:00
Meta Vens
8b52d02c79 Record the rulings on the Eureka cut map questions and the Eureka repo
Operator ruling (2026-09-15): Q1-Q5 all A. That means:
- one single-file store per repo, with an admission-replay merge;
- additive-only epochs;
- merge_exclusion settlement;
- voidbot indexes the default branch only;
- schemars unconditional in core.

The Eureka skill is also published as the GameCult/Eureka repo, and Cut 5's
skill edits land there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 20:14:59 +02:00
Meta Vens
2b76c2e758 Re-pin CultLib to a0813c6
Cut 1 of the Eureka pipeline state campaign. Moves every CultLib
dependency from e171eca3 to main a0813c6.

- load_envelope was deleted upstream (CultLib 4ed9871). The 12 sites in
  persona_conversation, persona_social_state and resident_self now call
  put_envelope. Each site feeds a cache built with CultCache::new() in
  the same function with no backing store attached, so home_index
  returns None and put_envelope only inserts into memory, exactly as
  load_envelope did. validate_envelope is byte-identical across the pins.
- add_generic_backing_store now returns Result; the three production
  sites and five test sites propagate it with ?.

No ownership change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 20:00:01 +02:00
Meta Vens
f05686c137 Map the Eureka pipeline state cuts
Imagination's cut map, built from probes:
1. Re-pin CultLib: 12 load_envelope sites; core 154/154 at the new pin.
2. One profile-parameterized commit owner.
3. The pipeline document set, lease, admission, queries, and a merge command.
4. The eureka-state stdio MCP package.
5. Skill wiring.
6. A proof campaign on the CultRecordRefFormatter follow-ups.
7. voidbot projection.

Five operator questions are open. The target now drops the claim that the
store merges in git, and records that voidbot indexes only default branches.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 19:52:32 +02:00
Meta Vens
0636176a0b Require one runner per repo and a merge tool for pipeline stores
Operator ruling (2026-09-15): only one Epiphany or Eureka may write a repo's
pipeline store at a time, and a second concurrent writer is refused. A merge
tool still reconciles divergent copies through admission, reporting true
conflicts as typed refusals rather than letting the last writer win.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 19:31:45 +02:00
Meta Vens
0d3f6bd65b Put the Eureka pipeline store in the repo where the task runs
Operator ruling (2026-09-15), superseding the user-level store: each
campaign's store is committed in the repo where the task runs, and
knowledge crosses repos only through explicit sharing. Pushing the working
branch syncs it to Yggdrasil, where voidbot's existing repo mirror indexes
it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 19:24:10 +02:00
Meta Vens
81be7a2f39 Record the Eureka pipeline state campaign target and ruling
Operator ruling (2026-09-15): Epiphany owns the typed schemas and admission
for pipeline state (findings, rulings, cut specs and reports, follow-ups).
The Eureka Claude Code skill reaches that state through a per-session Rust
stdio MCP server, built as an Epiphany package, over one user-level store.
voidbot indexes a projection for semantic search. The campaign re-pins
CultLib first, and Epiphany adopting Eureka's habits follows later.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 19:06:15 +02:00
Meta Vens
7132471870 Describe Epiphany as backend-agnostic
Codex, Claude and OpenAI-compatible APIs reach Epiphany through connectors
that share one standardized Epiphany-facing CultNet API, also used by
Ghostlight. Codex is one connector backend, not Epiphany's body.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 23:06:34 +02:00
Meta Vens
b5d9d4fff3 Merge main's dead-file cut into the live Epiphany line
main's only commit not on this branch deletes the stale compaction scratch and
the superseded protocol/controller-actions.md (census REC-045/REC-104). Merging
it here lets main fast-forward to the live branch, so Epiphany's default branch
stops pinning the retiring standalone cultcache-rs and cultnet-rs submodules.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 17:04:23 +02:00
Meta Vens
b76c32a52b REC-045/REC-104 (partial): cut dead compaction scratch and superseded controller-actions note
Deletes state/scratch-compaction-17cd64d8-da88-4642-a871-4cf1b768c20a.md: a
pre-compaction memory dump naming lost E:\Projects paths, a set of
"pseudo-Epiphany" repos to migrate, and the native
epiphany-agent-memory-store binary as its validation gate. That binary was
removed along with the other parallel executable control planes (commit
387afe49, 2026-08-23). The prompt doc the scratch describes producing
(notes/true-epiphany-memory-migration-sandbox-prompt.md) was already either
admitted or abandoned; the scratch itself records no unresolved next action
beyond "commit the doc," which git history shows happened or didn't
independently of this file.

Deletes protocol/controller-actions.md: self-described as "the legacy/manual
controller discipline from the first external-state prototype," it still
instructs writes to state/branches.json and state/evidence.jsonl — both
verified absent from the current tree (git show origin/main:state/branches.json
and :state/evidence.jsonl both fail). No Rust source references
"controller-actions" (checked via git grep across *.rs).

FALSIFIED, not cut: REC-104 also named "orphan prompt files"
(epiphany-state-model/src/prompts/epiphany_doctrine.md and
epiphany_state_intro.md) as dead. They are not — epiphany-state-model/src/
prompt.rs:30-31 pulls both in via include_str!, and epiphany-core/Cargo.toml:40
depends on epiphany-state-model as a path dependency. Deleting them would
break the build. Left untouched; see the coordinator report.

MOOT, not cut: REC-104's "stale scratch tail" referred (per
pass-one/deletion-candidates.md and branch-graveyard.md:390) to a leftover
EpiphanyRuntimeJobResult action at the tail of state/scratch.md. That file
now reads only "No active scratch subgoal. Rehydrate from state/map.yaml..."
— already retired before this pass, independent of this repo's current
(and very much alive) EpiphanyRuntimeJobResult type used throughout
epiphany-core/src/runtime_spine.rs.

Kill attempts: git grep -n for both deleted filenames across origin/main,
and rg across all of F:/Projects excluding target/node_modules/.git/
.epiphany-run: only the census documents and gamecult-site's generated
interactive repo-doc-tree (a static GitHub-link catalog, not a functional
reader) reference either path.

Collision check: git diff --stat origin/main against the active checkout
(codex/epiphany-shakedown-live), the epiphany-model-bridge worktree, and all
four detached .epiphany-run / temp worktrees, over both deleted paths: all
empty.

REC-045, REC-104 (partial)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 14:19:02 +02:00
Meta Vens
bc57980432 Lessons: the push is gated on the result line, not the launch
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 02:38:53 +02:00
Meta Vens
2b410e59d8 Lessons: integration is a gated sequence, not a shell line
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 23:24:46 +02:00
Meta Vens
383de5b9ac Record faculty-workflow lessons from the Ghostlight ontology build
What the improvised Self/Modeling/Imagination/Hands/Soul/Steward pipeline
did across nine design passes and four cuts, where it paid, where it leaked,
and what Epiphany's structured implementation should make typed: faculty
artifacts as schemas, rulings as a first-class artifact, a landed-names
digest per cut, owner-scoped edit queues, structural steward triggers, a
Self that cannot open source except through a faculty, and swarm visibility
as a read surface.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-04 20:38:15 +02:00
Meta Vens
616a2c8e74 Finish the interrupted rustfmt pass
The worktree had been sitting on a partial `cargo fmt` run: 15 files reflowed,
mostly multi-line derive attributes collapsed onto one line, with no semantic
change anywhere in the diff. It was never finished -- `cargo fmt --check` still
flagged resident_readiness.rs, which the pass had not reached.

Completed rather than reverted, since the formatting is what rustfmt produces
and leaving it half-applied is the only bad option. 16 test binaries pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 15:12:23 +02:00
Meta Vens
68939b2c1f Consume the core stack from CultLib instead of forking it
Epiphany carried three vendored copies: CultCache and CultNet as submodules on
their own branches of the retiring standalone repos, and a 491-line in-tree
CultMesh, a third Rust CultMesh lineage after CultLib's and Odin's.

None of it was load-bearing. Against CultLib the CultCache fork held nothing
unique, CultNet held one function that CultLib had all along but kept private,
and CultMesh held two thin delegations to CultCache. All three are now exposed
in CultLib, so this repoint needed no code changes at all: the workspace
compiles and its 16 test binaries pass against the pinned revision unchanged.

The submodule working tree also carried an uncommitted change, which turned out
to be a rustfmt reflow of one test expression. Nothing was lost with it.

Only the manifests move here. The in-flight epiphany-core work is left
uncommitted and untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 04:21:52 +02:00
Meta Vens
e27f7a8b93 Wire AGENTS.md into Claude Code memory
Claude Code reads CLAUDE.md, not AGENTS.md, so this repo's doctrine was
invisible to it. This one-line import makes the existing AGENTS.md load
without duplicating or renaming it. Codex is unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 22:22:42 +02:00
Meta Vens
4bdf410a33 Record Luna capstone comparison 2026-08-25 15:19:09 +02:00
Meta Vens
1499d65275 Record bounded Capstone 17 transport result 2026-08-25 14:41:14 +02:00
Meta Vens
377be331b9 Preserve Connector transport failure chain 2026-08-25 14:27:05 +02:00
Meta Vens
1e57bfacaa Admit containerized Connector client transport 2026-08-25 14:05:32 +02:00
Meta Vens
cf9fb44513 Preserve typed worker failure outcome 2026-08-25 13:41:15 +02:00
Meta Vens
90c1a5d28c Expose typed fresh-runtime brake release 2026-08-25 12:36:32 +02:00
Meta Vens
b164fe06ab Retire Ox17 and admit Capstone 17 2026-08-25 12:17:31 +02:00
Meta Vens
c58660f0a7 Record stopped Ox17 container capstone 2026-08-25 05:46:38 +02:00
Meta Vens
27e1dca1dc Restore Linux missing-process classification 2026-08-25 04:41:12 +02:00
Meta Vens
839f74daa8 Record Ox17 package authority gap 2026-08-25 04:08:26 +02:00
Meta Vens
8ea1a8f401 Close source subtraction audit 2026-08-25 03:54:28 +02:00
Meta Vens
ed4ca58ca3 Delete adopted plan field wrappers 2026-08-25 03:51:59 +02:00
Meta Vens
b2d2965c24 Record internal cognition helper cut 2026-08-25 03:38:13 +02:00
Meta Vens
9f189871bc Narrow internal cognition helpers 2026-08-25 03:36:02 +02:00
Meta Vens
62b387ef95 Record causal identity visibility cut 2026-08-25 03:31:22 +02:00
Meta Vens
e7b66b35d5 Demote causal work identity helpers 2026-08-25 03:29:16 +02:00
Meta Vens
a41fe69447 Record test-only state surface cut 2026-08-25 03:25:35 +02:00
Meta Vens
fca516f54f Delete test-only state readers 2026-08-25 03:21:15 +02:00
Meta Vens
24f8db777c Record unified family review flow 2026-08-25 03:12:57 +02:00
Meta Vens
1a9dec56c0 Collapse family review consequence flow 2026-08-25 03:05:18 +02:00
Meta Vens
d064efb48b Record single-projection family review cut 2026-08-25 02:58:43 +02:00
Meta Vens
d0a317bd10 Collapse family review projections 2026-08-25 02:55:51 +02:00
Meta Vens
7758f0703c Record resident readiness authority cut 2026-08-25 02:50:08 +02:00
Meta Vens
f962c0bc43 Delete test-only resident health aggregate 2026-08-25 02:45:48 +02:00
Meta Vens
7450b33db4 Document canonical brake surface cut 2026-08-25 02:36:53 +02:00
Meta Vens
e905dcd976 Inline canonical brake surfaces 2026-08-25 02:35:56 +02:00
Meta Vens
d426d50a49 Document packaged release API cut 2026-08-25 02:32:20 +02:00
Meta Vens
1e4fa53393 Narrow packaged release API 2026-08-25 02:31:33 +02:00
Meta Vens
cb611def86 Document dead provider health cut 2026-08-25 02:26:54 +02:00
Meta Vens
200732e951 Delete producerless provider health store 2026-08-25 02:25:44 +02:00
Meta Vens
c02a1c3e87 Document dead runtime health cut 2026-08-25 02:22:24 +02:00
Meta Vens
fab6a59a32 Delete producerless runtime health publisher 2026-08-25 02:21:10 +02:00
Meta Vens
42cb8d1eef Document Hands constructor subtraction 2026-08-25 02:17:05 +02:00
Meta Vens
1e66e77071 Delete test-only Hands constructors 2026-08-25 02:14:30 +02:00
Meta Vens
d03469d0fd Document packaged release ownership cut 2026-08-25 02:11:16 +02:00
Meta Vens
8086d66fc3 Collapse release construction package 2026-08-25 02:08:14 +02:00
Meta Vens
2f9f140e98 Record earned model runtime boundary 2026-08-25 02:01:52 +02:00
Meta Vens
b728d6854e Document native model boundary subtraction 2026-08-25 01:58:09 +02:00
Meta Vens
a721b76372 Collapse model adapter package boundary 2026-08-25 01:44:59 +02:00
Meta Vens
b83c041670 Delete mirrored provider event family 2026-08-25 01:33:40 +02:00
Meta Vens
dfe3757bfe Project reorientation terminal disposition 2026-08-25 01:20:17 +02:00
Meta Vens
8edb47e093 Map reorientation result collapse 2026-08-25 01:08:47 +02:00
Meta Vens
125d77a266 Collapse reorientation result projection 2026-08-25 01:07:39 +02:00
Meta Vens
59b0c2af9f Map unified coordinator projection 2026-08-25 01:00:56 +02:00
Meta Vens
2bcbf2687d Unify coordinator work projection 2026-08-25 00:59:40 +02:00
Meta Vens
99e883968c Map single-snapshot fulfillment 2026-08-25 00:47:31 +02:00
Meta Vens
5f1bea3913 Reuse exact Mind snapshot for fulfillment 2026-08-25 00:45:10 +02:00
Meta Vens
34f9092c65 Map single-snapshot current work 2026-08-25 00:33:55 +02:00
Meta Vens
7374be5ece Project current work from one Mind snapshot 2026-08-25 00:31:24 +02:00
Meta Vens
a1eedbac9a Record Codex implementation eviction 2026-08-25 00:04:28 +02:00
Meta Vens
7a125d7838 Evict vendored Codex implementation 2026-08-25 00:02:24 +02:00
Meta Vens
895476b1b3 Map standalone Codex transport authority 2026-08-24 23:58:20 +02:00
Meta Vens
87ea81db2a Retire obsolete provider request publication 2026-08-24 23:52:00 +02:00
Meta Vens
c37cae8bc1 Seal exact provider requests before transport 2026-08-24 23:44:57 +02:00
Meta Vens
4177e186bb Record singular Hands authority chain 2026-08-24 23:24:29 +02:00
Meta Vens
a01c684262 Delete counterfeit Hands review authority 2026-08-24 23:21:26 +02:00
Meta Vens
08588a6d3f Record sealed Hands verification input 2026-08-24 22:53:46 +02:00
Meta Vens
24023265f4 Remove live Hands receipt rereads 2026-08-24 22:50:59 +02:00
Meta Vens
dbb00c2bf9 Cut model-turn display cargo 2026-08-24 22:34:43 +02:00
Meta Vens
ab9cad4462 Record generic result deletion 2026-08-24 22:26:17 +02:00
Meta Vens
2c3335dcaf Delete generic runtime job results 2026-08-24 22:20:55 +02:00
Meta Vens
7e5ea3e733 Record structured worker result cut 2026-08-24 21:34:30 +02:00
Meta Vens
8eaa96c5d6 Stop duplicating structured worker decisions 2026-08-24 21:31:42 +02:00
Meta Vens
c2be631ec1 Record generic result cargo cut 2026-08-24 21:16:38 +02:00
Meta Vens
53e869c078 Remove generic job-result evidence mirrors 2026-08-24 21:11:10 +02:00
Meta Vens
7189243fd9 Record runtime job binding cut 2026-08-24 20:58:36 +02:00
Meta Vens
10a70afcbd Derive runtime job ownership from sealed bindings 2026-08-24 20:54:59 +02:00
Meta Vens
f5cd509a1e Record runtime job authority cut 2026-08-24 20:39:14 +02:00
Meta Vens
a46725a1c7 Remove timestamp authority from runtime jobs 2026-08-24 20:32:47 +02:00
Meta Vens
734fbd3b84 Record coordinator session authority split 2026-08-24 20:20:41 +02:00
Meta Vens
cabdd6c386 Split coordinator authority from model sessions 2026-08-24 20:16:31 +02:00
Meta Vens
d75583c45e Record sealed launch authority cut 2026-08-24 19:42:14 +02:00
Meta Vens
edb5c3a3c3 Derive worker authority from sealed launches 2026-08-24 19:37:44 +02:00
Meta Vens
dd55f0f896 Record semantic state package collapse 2026-08-24 18:33:08 +02:00
Meta Vens
07529fb521 Collapse semantic state contracts into core 2026-08-24 18:29:58 +02:00
Meta Vens
3421af57a4 Record standalone Codex consumer cut 2026-08-24 18:03:00 +02:00
Meta Vens
ed7357a28b Extract Codex transport from Epiphany 2026-08-24 17:57:06 +02:00
Meta Vens
ce22c3b11f Record durable Codex connector boundary 2026-08-24 16:47:24 +02:00
Meta Vens
1ff87ab30d Define pinned Codex child boundary 2026-08-24 16:10:32 +02:00
Meta Vens
8e555e358f Keep native request authority with consumers 2026-08-24 14:08:08 +02:00
Meta Vens
c873bef3b1 Map standalone Codex transport migration 2026-08-24 14:01:28 +02:00
Meta Vens
f532c3ad00 Record provider transport boundary cut 2026-08-24 13:41:54 +02:00
Meta Vens
a6cf9383d8 Quarantine Codex transport at release edge 2026-08-24 13:38:52 +02:00
Meta Vens
3263640378 Record execution binding authority cut 2026-08-24 12:53:15 +02:00
Meta Vens
ce6bff1239 Collapse runtime execution bindings to ownership edges 2026-08-24 12:49:30 +02:00
Meta Vens
4d6dd54c41 Record singular Persona delivery authority 2026-08-24 12:07:42 +02:00
Meta Vens
b3e9b229e3 Fold Persona delivery into social terminal 2026-08-24 12:05:34 +02:00
Meta Vens
2ead42ed42 Record owned runtime physiology 2026-08-24 11:45:32 +02:00
Meta Vens
fdefb8898a Delete test-only runtime writers 2026-08-24 11:43:37 +02:00
Meta Vens
87c14b5093 Record singular Eyes acceptance authority 2026-08-24 11:10:27 +02:00
Meta Vens
83611f9b71 Collapse Eyes acceptance into Mind commits 2026-08-24 11:06:25 +02:00
Meta Vens
189f108f76 Record admitted model runtime authority 2026-08-24 10:47:12 +02:00
Meta Vens
3b991e4079 Collapse model runtime onto admitted workers 2026-08-24 10:44:18 +02:00
Meta Vens
e60b924f73 Delete model runtime schema self-certificate 2026-08-24 10:32:34 +02:00
Meta Vens
ad89b816a8 Record runtime child epoch authority cut 2026-08-24 10:11:32 +02:00
Meta Vens
a4356d1f12 Collapse runtime child schema echoes 2026-08-24 10:06:53 +02:00
Meta Vens
9d51148517 Record frontier envelope authority cut 2026-08-24 09:37:38 +02:00
Meta Vens
718ce9c1e3 Collapse frontier document envelope duplication 2026-08-24 09:34:21 +02:00
Meta Vens
93b8fa8c15 Record lean objective intake authority 2026-08-24 09:10:48 +02:00
Meta Vens
952dcd9f65 Strip objective intake ceremony 2026-08-24 09:07:42 +02:00
Meta Vens
4003258b8f Record minimal repository identity authority 2026-08-24 08:52:12 +02:00
Meta Vens
6f5d660082 Collapse repository domain binding 2026-08-24 08:50:03 +02:00
Meta Vens
ce3f90df6c Record proposal origin authority collapse 2026-08-24 08:32:45 +02:00
Meta Vens
d9a196a017 Collapse autonomous proposal origin authority 2026-08-24 08:20:43 +02:00
Meta Vens
e65fa9f4a7 Record lean Hands receipt authority 2026-08-24 07:49:33 +02:00
Meta Vens
65bf044f5c Remove shadow Hands receipt metadata 2026-08-24 07:47:27 +02:00
Meta Vens
00522e6037 Record release dependency ownership 2026-08-24 07:18:51 +02:00
Meta Vens
695af6c630 Remove unowned release dependencies 2026-08-24 07:17:54 +02:00
Meta Vens
755bc60f5d Record earned CultNet boundaries 2026-08-24 07:05:10 +02:00
Meta Vens
8bb0719bc6 Collapse CultNet catalog to earned boundaries 2026-08-24 07:03:42 +02:00
Meta Vens
dbed11b3e9 Delete dead runtime CultNet contracts 2026-08-24 06:50:03 +02:00
Meta Vens
3a8cd673e8 Record singular model-stream ownership 2026-08-24 06:46:43 +02:00
Meta Vens
fe118e13c3 Delete duplicate provider stream persistence 2026-08-24 06:43:33 +02:00
Meta Vens
cec8a0db1c Record routing cargo hard cut 2026-08-24 06:27:35 +02:00
Meta Vens
ffd91c20e9 Delete fixed routing and retention cargo 2026-08-24 06:25:06 +02:00
Meta Vens
61238cfdb2 docs: Record singular provenance cut 2026-08-24 06:04:57 +02:00
Meta Vens
2a435eb5ab Delete duplicate Persona and tool receipt cargo 2026-08-24 06:03:28 +02:00
Meta Vens
4e0f3d3cd2 docs: Record provider observer cut 2026-08-24 05:56:40 +02:00
Meta Vens
9abadfe787 Delete unconsumed provider frame observer 2026-08-24 05:55:41 +02:00
Meta Vens
84b4012aa5 docs: Record dormant state vocabulary cut 2026-08-24 05:38:37 +02:00
Meta Vens
a602fbdc2e Delete dormant aggregate state vocabulary 2026-08-24 05:36:11 +02:00
Meta Vens
115f875167 docs: Record continuity recovery mirror cut 2026-08-24 05:22:10 +02:00
Meta Vens
fed4b85780 Delete duplicate continuity recovery receipts 2026-08-24 05:20:45 +02:00
Meta Vens
a2bf15359e docs: Record Persona quarantine mirror cut 2026-08-24 05:17:36 +02:00
Meta Vens
a6f73fc4cc Delete write-only Persona quarantine mirror 2026-08-24 05:16:52 +02:00
Meta Vens
24e2949e46 docs: Record generic planning mirror cut 2026-08-24 05:14:04 +02:00
Meta Vens
2e3489c4f5 Delete the empty generic planning mirror 2026-08-24 05:12:50 +02:00
Meta Vens
16c7b4d760 docs: Record adapter status authority cut 2026-08-24 05:06:02 +02:00
Meta Vens
0091e0e122 Delete unread model adapter status families 2026-08-24 05:03:45 +02:00
Meta Vens
b987976b74 docs: Record dead Resident Self contract cut 2026-08-24 04:52:21 +02:00
Meta Vens
3140d3054a Delete dead Resident Self receipt contract 2026-08-24 04:51:24 +02:00
Meta Vens
14e3c44f5c docs: Record Imagination contract cut 2026-08-24 04:46:43 +02:00
Meta Vens
be611f2411 Delete dead Imagination review contract 2026-08-24 04:45:31 +02:00
Meta Vens
3a0c7798a6 docs: Record unconsumed reader cut 2026-08-24 04:42:54 +02:00
Meta Vens
f6a2ad7f84 Delete unconsumed release and trust readers 2026-08-24 04:42:09 +02:00
Meta Vens
47e0c413f1 docs: Record unowned projection cut 2026-08-24 04:33:15 +02:00
Meta Vens
6ccaf93704 Delete unowned Mind projection helpers 2026-08-24 04:31:46 +02:00
Meta Vens
92d1b8d6bd docs: Record callerless helper deletion 2026-08-24 04:21:33 +02:00
Meta Vens
5a0479446e Delete callerless helper APIs 2026-08-24 04:20:27 +02:00
Meta Vens
d15404c57b docs: Record sole current-work projection 2026-08-24 04:16:45 +02:00
Meta Vens
0a97eef8d8 Collapse duplicate current-work projections 2026-08-24 04:15:37 +02:00
Meta Vens
1e1012de0c docs: Record strict session ownership 2026-08-24 04:11:28 +02:00
Meta Vens
cd2177e834 Delete lenient session ensure writer 2026-08-24 04:09:56 +02:00
Meta Vens
e6fae8b076 docs: Record minimal runtime lifecycle state 2026-08-24 03:48:25 +02:00
Meta Vens
196222d991 Minimize runtime lifecycle documents 2026-08-24 03:46:29 +02:00
Meta Vens
247271b4cc docs: Record minimal coordinator terminal state 2026-08-24 03:17:54 +02:00
Meta Vens
63d2991e63 Minimize coordinator terminal receipts 2026-08-24 03:15:12 +02:00
Meta Vens
5a9f6593c7 docs: Record atomic Verification ownership 2026-08-24 02:47:42 +02:00
Meta Vens
3ed1d5649f Delete duplicate frontier Modeling writer 2026-08-24 02:46:12 +02:00
Meta Vens
1fca52de4f docs: Record sole immutable launch authority 2026-08-24 02:22:08 +02:00
Meta Vens
8dbfcf6365 Delete final duplicate launch bindings 2026-08-24 02:20:01 +02:00
Meta Vens
80afb67cd5 docs: Record sole Planning launch authority 2026-08-24 01:35:57 +02:00
Meta Vens
b55e96eaf9 Delete duplicate Planning launch bindings 2026-08-24 01:33:28 +02:00
Meta Vens
bae9d338e4 Record sole Proposal launch authority 2026-08-24 01:20:59 +02:00
Meta Vens
b0a4978da2 Delete duplicate Proposal Modeling launch binding 2026-08-24 01:19:16 +02:00
3756 changed files with 26154 additions and 876245 deletions

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
/schemas/cultnet/epiphany.pipeline.*.schema.json text eol=lf

6
.gitmodules vendored
View file

@ -1,6 +0,0 @@
[submodule "vendor/cultcache-rs"]
path = vendor/cultcache-rs
url = https://github.com/GameCult/cultcache-rs.git
[submodule "vendor/cultnet-rs"]
path = vendor/cultnet-rs
url = https://github.com/GameCult/cultnet-rs.git

View file

@ -2,7 +2,10 @@
## Project Purpose
This repo is the forge-temple of Epiphany: an opinionated Codex fork where
This repo is the forge-temple of Epiphany: an opinionated agent harness, born
as a Codex fork and now backend-agnostic. Codex, Claude, and OpenAI-compatible
APIs speak to it through connectors that share one standardized
Epiphany-facing CultNet API, the same one Ghostlight uses. It is the harness where
external typed state, explicit mental maps, bounded scratch work, verifier
evidence, and anti-churn discipline are wired into the blessed harness instead
of taped onto the chat transcript like a purity seal over a cracked reactor.
@ -102,7 +105,6 @@ machine-spirit awake.
## Important Paths
- Project root: `F:\Projects\Epiphany`
- Vendored Codex repo: `F:\Projects\Epiphany\vendor\codex`
- Fork implementation plan: `F:\Projects\Epiphany\notes\epiphany-fork-implementation-plan.md`
- Handoff summary: `F:\Projects\Epiphany\notes\fresh-workspace-handoff.md`
- Epiphany algorithmic map: `F:\Projects\Epiphany\notes\epiphany-current-algorithmic-map.md`
@ -123,14 +125,6 @@ $env:CARGO_TARGET_DIR='C:\Users\Meta\.cargo-target-codex'
cargo run --manifest-path .\Cargo.toml --bin epiphany-state -- add-evidence --type research --status ok --note '...'
```
Useful Codex repo searches:
```powershell
rg -n "pub enum ModeKind|TUI_VISIBLE_COLLABORATION_MODES" .\vendor\codex\codex-rs\protocol\src\config_types.rs
rg -n "builtin_collaboration_mode_presets|fn plan_preset|fn default_preset" .\vendor\codex\codex-rs\models-manager\src\collaboration_mode_presets.rs
rg -n "collaboration_mode_label|collaboration_mode_indicator|set_collaboration_mask" .\vendor\codex\codex-rs\tui\src\chatwidget.rs
```
## Session Bootstrap And Re-entry Rite
On fresh awakening, perform this rite before wandering into implementation like

3
CLAUDE.md Normal file
View file

@ -0,0 +1,3 @@
Repository doctrine for this project lives in AGENTS.md, shared with Codex.
@AGENTS.md

5500
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -2,15 +2,11 @@
members = [
"epiphany-core",
"epiphany-model-adapter",
"epiphany-openai-adapter",
"epiphany-openai-codex-spine",
"epiphany-openai-runtime",
"epiphany-release-construction",
"epiphany-state-model",
"epiphany-pipeline",
"epiphany-tool-adapter",
"epiphany-tool-mcp-runtime",
]
exclude = ["vendor/codex", "vendor/cultcache-rs"]
resolver = "3"
[package]
@ -25,33 +21,26 @@ license-file = "LICENSE"
anyhow = "1"
chrono = "0.4.44"
ctrlc = { version = "3.5.1", features = ["termination"] }
cultcache-rs = { path = "vendor/cultcache-rs" }
cultmesh-rs = { path = "vendor/cultmesh-rs" }
cultnet-rs = { path = "vendor/cultnet-rs" }
cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "a0813c6eed24d30bf88073ef615b633c77ebfcd6", package = "cultcache-rs" }
cultnet-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "a0813c6eed24d30bf88073ef615b633c77ebfcd6", package = "cultnet-rs" }
epiphany-core = { path = "epiphany-core" }
epiphany-model-adapter = { path = "epiphany-model-adapter" }
epiphany-openai-adapter = { path = "epiphany-openai-adapter" }
epiphany-openai-runtime = { path = "epiphany-openai-runtime", optional = true }
epiphany-state-model = { path = "epiphany-state-model" }
codex-connector = { git = "https://github.com/GameCult/CodexConnector", rev = "be1776ab455fd79bd74b393518ab07211dc9c1f0", default-features = false, optional = true }
epiphany-tool-adapter = { path = "epiphany-tool-adapter" }
epiphany-tool-mcp-runtime = { path = "epiphany-tool-mcp-runtime", optional = true }
http = "1"
ignore = "0.4.23"
postgres = "0.19"
reqwest = { version = "0.13.2", default-features = false, features = ["blocking", "json", "rustls"] }
rmp-serde = "1"
rmcp = { version = "2.2.0", default-features = false, features = ["client", "transport-child-process", "transport-streamable-http-client-reqwest"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
semver = { version = "1", features = ["serde"] }
sha2 = "0.10"
tokio = { version = "1", features = ["io-util", "macros", "process", "rt", "rt-multi-thread", "time"] }
toml = "0.9"
tracing = "0.1.44"
ureq = { version = "3.4", default-features = false, features = ["rustls"], optional = true }
uuid = { version = "1", features = ["serde", "v4"] }
zeroize = { version = "1", optional = true }
[features]
openai-runtime = ["dep:epiphany-openai-runtime"]
openai-runtime = ["dep:codex-connector", "dep:epiphany-openai-runtime", "dep:ureq", "dep:zeroize"]
tool-mcp-runtime = ["dep:epiphany-tool-mcp-runtime"]
release-runtime = ["openai-runtime", "tool-mcp-runtime"]
@ -61,13 +50,6 @@ incremental = true
[dev-dependencies]
tempfile = "3"
[patch.crates-io]
tokio-tungstenite = { git = "https://github.com/openai-oss-forks/tokio-tungstenite", rev = "132f5b39c862e3a970f731d709608b3e6276d5f6" }
tungstenite = { git = "https://github.com/openai-oss-forks/tungstenite-rs", rev = "9200079d3b54a1ff51072e24d81fd354f085156f" }
[patch."ssh://git@github.com/openai-oss-forks/tungstenite-rs.git"]
tungstenite = { git = "https://github.com/openai-oss-forks/tungstenite-rs", rev = "9200079d3b54a1ff51072e24d81fd354f085156f" }
[[bin]]
name = "epiphany-release"
path = "epiphany-core/src/bin/epiphany-release.rs"
@ -99,12 +81,7 @@ path = "epiphany-core/src/bin/epiphany-mvp-coordinator.rs"
[[bin]]
name = "epiphany-model-runtime"
path = "epiphany-openai-runtime/src/bin/epiphany-model-runtime.rs"
required-features = ["openai-runtime"]
[[bin]]
name = "epiphany-model-connector"
path = "epiphany-openai-runtime/src/bin/epiphany-model-connector.rs"
path = "epiphany-openai-runtime/src/bin/epiphany-openai-runtime.rs"
required-features = ["openai-runtime"]
[[bin]]

35
LICENSE
View file

@ -19,15 +19,10 @@ For this file:
directory states different terms.
- `Upstream Material` means material copied or adapted from third parties and
distributed under their own licenses.
- `Mixed Apache Files` means files under `vendor/codex/**` that contain
project-authored changes on top of upstream Apache-licensed material.
2. Upstream And Third-Party Material
------------------------------------
- All material under `vendor/codex/**` remains subject to its upstream licenses
and notices, including Apache License 2.0 at `vendor/codex/LICENSE` and any
applicable `NOTICE` or file-level notices.
- Other third-party material distributed with the repository keeps its own
license terms where provided.
- You must continue to comply with those upstream terms when using,
@ -36,8 +31,8 @@ For this file:
3. Public License For Project-Authored Material
-----------------------------------------------
Except where a file or directory says otherwise, Project-Authored Material
outside `vendor/codex/**` is licensed to the public under:
Except where a file or directory says otherwise, Project-Authored Material is
licensed to the public under:
- PolyForm Noncommercial License 1.0.0
@ -66,27 +61,7 @@ entity is formed and designated in writing, any commercial license for
Project-Authored Material may be offered directly by the individual copyright
holder or holders.
5. Mixed Apache Files Under `vendor/codex/**`
---------------------------------------------
Some files under `vendor/codex/**` contain project-authored changes layered on
top of upstream Apache-licensed code.
For those files:
- Apache License 2.0 and the applicable upstream notices still apply.
- You must not read this repository notice as converting those files into a
PolyForm-only codebase.
- To the extent the project owns copyright in new additions to those files, the
project may also offer separate commercial terms for its own contributions.
- This repository notice does not claim to erase or retract Apache-2.0
permissions already granted for Apache-covered material.
The strongest commercial boundary in this repository therefore applies to
clearly separable Project-Authored Material outside `vendor/codex/**`,
including `epiphany-core/**` and other non-vendored project files.
6. Open Source Status
5. Open Source Status
---------------------
Because the public license path for Project-Authored Material is PolyForm
@ -94,7 +69,7 @@ Noncommercial License 1.0.0 rather than an OSI-approved license, the
repository as a whole is source-available and dual-licensed. It is not an OSI
Open Source distribution as a whole.
7. Reference Structures
6. Reference Structures
-----------------------
This licensing structure intentionally borrows the general dual-license shape
@ -107,7 +82,7 @@ This repository does not claim that Qt or MySQL use the same exact public
license terms used here. Those projects are referenced only as structural
examples of dual licensing, not as the operative terms of this repository.
8. Contributions
7. Contributions
----------------
External contributions are accepted only under the EpiphanyAgent Contributor

View file

@ -23,6 +23,19 @@ enough to act honestly.
This is not a faster autocomplete costume. It is the beginning of governed
human/agent labor.
## Front Door
- **Value:** the shared typed Mind for agent work: governed project state, role lanes for research, modeling, implementation, verification, planning and Persona, and receipts for commands, edits and commits, so work stays coherent past one prompt, one agent or one session.
- **Problem:** global coherence collapse. Agents keep producing plausible local progress after the project has lost a valid global model; Epiphany turns understanding into inspectable state instead of a longer prompt.
- **Entry points:**
- `AGENTS.md`: operating law and re-entry rites. Read it before touching the machine.
- `epiphany-core/`: the typed domain organs (`src/runtime_spine.rs` job, worker, model and tool documents; `src/substrate_gate.rs`; `src/state_model.rs`; `src/mind_documents.rs`).
- `cargo run --bin epiphany-state -- status`: the steward CLI for direct Mind inspection (other native binaries are declared in `Cargo.toml`).
- `schemas/cultnet/`, `state/map.yaml` (canonical project map), `notes/epiphany-current-algorithmic-map.md` (live control flow).
- `docs/positioning.md`: what is claimed and what is not yet earned.
- **Live:** a supervised engineering alpha: durable typed state and prompt projection, update/proposal/promotion/review paths, runtime-spine documents, operator status and coordinator commands, heartbeat and Persona-state machinery, and the Hands, Eyes, Soul, Mind, Continuity and Substrate Gate receipt families.
- **Planned:** sustained autonomous GameCult production, complete Bifrost attribution, and a longitudinal fleet of one Epiphany organism per project Body. See What Exists Now.
## The GameCult Bet
GameCult is building infrastructure for coordinated work between humans,
@ -120,19 +133,15 @@ Epiphany is a native GameCult runtime that began as an opinionated Codex fork.
Her body is made of:
- typed Rust domain organs in `epiphany-core` and `epiphany-state-model`
- typed Rust domain organs owned by `epiphany-core`
- CultCache `.cc` stores for runtime, heartbeat, agent state, local Verse,
memory graph, and thread state
- CultMesh and CultNet contracts for local and distributed state
- provider-neutral model and tool request/receipt documents
- Mind, Substrate Gate, Eyes, Hands, Soul, Continuity, Persona, and heartbeat
authority surfaces
- Codex retained only for honest OpenAI subscription auth and model transport;
Epiphany does not publish a Codex-owned project-state surface
- an independently supervised, loopback-only
[model provider connector](docs/model-provider-connector.md) can lend that
transport to an admitted GameCult service without lending Epiphany's Mind or
credentials
- a narrow standalone CodexConnector process for OpenAI subscription auth and
model transport; Epiphany compiles no Codex crate and keeps cognition local
That last clause matters. Codex and other frontier agents can be excellent
workers. They do not decide which organizational state is authoritative, who

View file

@ -25,9 +25,6 @@ outside the working notes.
- [Fork implementation plan](../notes/epiphany-fork-implementation-plan.md)
records the current architecture and cut lines.
- [Project map](../state/map.yaml) is the canonical slow machine map.
- [Model provider connector](./model-provider-connector.md) records the typed
loopback boundary that lends Epiphany's Codex transport without lending her
Mind, credentials, or swarm runtime.
For historical GameCult-level investor context, the sibling site dossier lives
at `F:\Projects\gamecult-site\docs\gamecult_integrated_dossier.tex`. It is a

View file

@ -48,7 +48,7 @@ The practical definition in this repository is:
\begin{itemize}
\item the root checkout at \code{F:\Projects\Epiphany};
\item Epiphany Rust crates such as \code{epiphany-core}, \code{epiphany-state-model}, model/tool adapters, and OpenAI transport crates;
\item Epiphany Rust crates such as \code{epiphany-core}, model/tool adapters, and provider runtimes;
\item vendored substrate in \code{vendor/codex}, \code{vendor/cultcache-rs}, \code{vendor/cultnet-rs}, and \code{vendor/cultmesh-rs};
\item typed state stores under \code{state/};
\item schemas under \code{schemas/cultnet/};
@ -59,20 +59,19 @@ The practical definition in this repository is:
\section{Executive Map}
Epiphany's current Body is a hybrid machine in transition. Its center of gravity
has moved into typed Rust/CultCache organs, but compatibility arteries still
run through vendored Codex. The live target is not a cleaner pile of Codex
routes. The target is an Epiphany-native CultCache/CultMesh/CultNet body with a
relatively vanilla Codex-derived reliquary retained for OpenAI subscription
auth and model transport.
Epiphany's current Body is a typed Rust/CultCache organism with provider
transport outside its cognition boundary. Codex requests cross a lean client
ABI into the independent CodexConnector daemon, which owns subscription
credential refresh, transport, and replay. Epiphany owns prompts, tools,
reasoning context, decisions, and Mind admission; it compiles no Codex crate.
\begin{longtable}{>{\raggedright\arraybackslash}p{0.23\linewidth}>{\raggedright\arraybackslash}p{0.32\linewidth}>{\raggedright\arraybackslash}p{0.35\linewidth}}
\textbf{Body Region} & \textbf{Current Owner} & \textbf{Invariant} \\
\hline
Durable Mind state & keyed CultCache documents assembled by \code{epiphany-core} & Prompt rendering consumes sealed typed projections; no persisted thread aggregate or global authority revision exists. \\
Domain policy & \code{epiphany-core} & Verdicts, launch policy, acceptance rules, runtime lifecycle, and mutation law live here, not in bridge DTOs. \\
Vendored Codex & \code{vendor/codex} & Retain host/app-server/auth/model compatibility where useful; do not let Codex own Epiphany prompts, scheduler, or state. \\
Runtime spine & \code{epiphany-core/src/runtime_spine.rs} and \code{state/runtime-spine.cc} & Runtime identity, jobs, events, receipts, tool/model documents are typed CultCache records. \\
Codex transport & independent \code{GameCult/CodexConnector} daemon & Own subscription credentials, provider transport, and exact replay only; never own Epiphany prompts, scheduler, decisions, or state. \\
Runtime spine & \code{epiphany-core/src/runtime_spine.rs} and \code{state/runtime-spine.cc} & Runtime identity, jobs, receipts, and tool/model documents are typed CultCache records. \\
Persona memory & keyed Mind Persona-memory documents & Persona decisions admit bounded memories by semantic identity; no parallel role-memory aggregate or generation head exists. \\
Resident Self physiology & Resident Self CultCache documents via \code{epiphany-swarm} & Typed pressure, exact grant creation, launch exclusion, leases, terminal receipts, retry, and bounded retention have one owner. No scheduler singleton or acknowledgement loop exists. \\
Local Verse & CultMesh over \code{state/local-verse.cc} & Local Verse context is compact prompt/operator context; private state does not become public by flipping a flag. \\
@ -82,27 +81,21 @@ Operator artifacts & \code{.epiphany-*}, \code{tools/epiphany_local_run.ps1} & A
\section{Repository Body}
The checkout has nine primary Epiphany Rust crates:
The checkout has eight primary Epiphany Rust packages:
\begin{itemize}
\item \code{epiphany-core}: the main domain organ. It owns state-update
validation, launch documents, runtime spine, Resident Self, Persona
projection, Substrate Gate, Mind, Eyes, Hands, Soul, Continuity, CultMesh
integration, surface derivation, smokes, and local operator binaries.
\item \code{epiphany-state-model}: the durable \code{EpiphanyThreadState}
type plus \code{render_epiphany_state}. This is the typed Mind surface that
prompt fragments and views read.
\item \code{epiphany-core}: the main domain organ. It owns semantic state
contracts, keyed Mind admission, launch documents, runtime spine, Resident
Self, Persona projection, Substrate Gate, Eyes, Hands, Soul, Continuity,
CultMesh integration, and surface derivation.
\item \code{epiphany-model-adapter}: provider-neutral model request, stream
event, receipt, and status documents.
\item \code{epiphany-openai-adapter}: OpenAI-shaped request/event/receipt and
status documents.
\item \code{epiphany-openai-auth-spine}: Codex-derived auth boundary over
\code{codex-login}. It preserves honest Codex-compatible credential and token
behavior rather than impersonating it from scratch.
\item \code{epiphany-openai-codex-spine}: model transport over vendored Codex
client machinery.
\item \code{epiphany-openai-runtime}: executable model runtime, including
\code{epiphany-model-runtime}.
\item \code{epiphany-release-construction}: exact package construction and
release-witness verification.
\item \code{epiphany-tool-adapter}: typed tool capability, invocation intent,
and invocation receipt documents.
\item \code{epiphany-tool-mcp-runtime}: first-party tool executor. Native
@ -114,8 +107,8 @@ The checkout has nine primary Epiphany Rust crates:
The vendored region contains:
\begin{itemize}
\item \code{vendor/codex}: the Codex-derived host and compatibility substrate.
It is tracked directly, not as a submodule.
\item \code{vendor/codex}: retained Codex-fork source history. It is not in
Epiphany's compiled runtime graph.
\item \code{vendor/cultcache-rs}, \code{vendor/cultnet-rs}, and
\code{vendor/cultmesh-rs}: repo-contained Rust Cult substrate.
\end{itemize}
@ -170,7 +163,7 @@ publication, sealed forensic artifacts, or named quarantine experiments.
the Body. It imports and registers:
\begin{itemize}
\item runtime identity, session, job, job-result, and event records;
\item runtime identity, session, job, and job-result records;
\item worker launch requests and role/reorient worker result documents;
\item model adapter, OpenAI adapter, and tool adapter documents;
\item sealed reasoning bases, decision contexts, typed pass failures, and

View file

@ -77,7 +77,7 @@ conflicts, or a consequential judgment cannot be derived honestly.
The current native Epiphany body includes:
- typed Rust domain organs in `epiphany-core` and `epiphany-state-model`
- typed Rust domain organs owned by `epiphany-core`
- keyed Mind state with typed projection and promotion paths
- RepoModel and Model Atlas surfaces for source-grounded architectural belief
- CultCache `.cc` state and CultMesh/CultNet contracts
@ -86,7 +86,7 @@ The current native Epiphany body includes:
- runtime-spine job and worker-result documents
- Mind, Modeling, Substrate Gate, Eyes, Hands, Soul, Continuity, Persona, and
heartbeat authority surfaces
- local operator status, coordinator, smoke, and Verse-context commands
- local operator status, coordinator, and Verse-context commands
Codex is retained only for OpenAI authentication and model transport. Epiphany
does not publish Codex app-server state as its project Mind.

View file

@ -1,117 +0,0 @@
# Epiphany Model Provider Connector
## Objective
Let another GameCult service use Epiphany's admitted Codex subscription
transport without acquiring Epiphany's Mind, credentials, scheduler, swarm
runtime, or authority. The first consumer is Ghostlight Dungeon.
The connector is an independently supervised loopback service. Its daemon is
earned by credential isolation, request-concurrency ownership, and failure
isolation from both the consumer and the Epiphany swarm.
## Authority map
- **Owner:** `epiphany-model-connector` owns Codex authentication custody,
provider transport, physical request concurrency, transport replay handling,
and a redacted capability/status projection.
- **Inputs:** one encrypted, expiring
`epiphany.model_connector_invocation.v1` carried in a bounded CultNet
`DocumentPutRaw` frame. The admitted caller and model are configured by the
operator.
- **Outputs:** one encrypted `epiphany.model_connector_result.v1` containing
ordered provider events and the terminal model receipt, or a standard
CultNet refusal. Odin receives only
`epiphany.model_connector_status.v1`.
- **Derived state:** connector readiness and capacity are discovery state. They
are not provider credentials, inference permission, campaign state, or
Epiphany Mind state.
- **Forbidden writers:** the caller cannot select an unadmitted model, attach
tools, request reasoning summaries, chain provider response state, inspect
credentials, or mutate Epiphany. The connector cannot interpret model output
as application state or commit application mutations.
- **Shared path:** every accepted consumer request uses the same bounded frame,
authenticated envelope, caller/model checks, semaphore, Codex transport, and
terminal receipt validation.
- **Cut line:** the connector calls the existing Codex transport directly. It
does not start Epiphany's runtime, open her CultCache Mind, emit heartbeat
state, or participate in swarm scheduling.
## Wire flow
```text
consumer-owned typed stage
-> provider-neutral model request
-> MessagePack connector invocation
-> AES-GCM authenticated envelope
-> bounded TCP-framed CultNet on loopback
-> Epiphany Codex transport
-> ordered text/provider receipt events
-> encrypted typed connector result
-> consumer validation and local interpretation
-> consumer-owned commit authority
```
The outer CultNet message ID, document key, encrypted envelope request ID,
invocation request ID, model request ID, every stream-event request ID, and
terminal receipt request ID must agree. Requests carry a short expiry and an
exact payload digest. A repeated request ID with identical bytes receives the
cached terminal response for that connector lifetime; a conflicting or
concurrently active duplicate is rejected. Application idempotency and commit
authority remain with the consumer.
The frame decoder rejects a declared payload larger than its advertised limit
before allocating the payload. This is a CultNet transport invariant rather
than a connector-specific workaround.
## Security and privacy
- The listener must bind to loopback. It is never routed through nginx or a
public firewall.
- A service-private shared key authenticates and encrypts every invocation and
result. It is stored outside source and release directories.
- The Codex credential stays in the connector's service-readable credential
directory. It is never copied into Ghostlight, arguments, logs, CultMesh, or
Odin.
- Tools, provider-side state chaining, reasoning summaries, reasoning deltas,
and tool-call events are refused.
- Prompts and results are not persisted by the connector. The consumer owns
its own permitted stage receipts.
## Cache and token efficiency
The request contract carries `prompt_cache_key` and `max_output_tokens` through
the existing model spine. Consumers construct the cache key from stable stage
and output-contract identity, not volatile world state. Provider receipts
report prompt, completion, reasoning, and cached-input token counts when the
provider supplies them. Epiphany neither fabricates cache hits nor interprets
their application value.
## Deployment boundary
Idunn manages this connector as a separate immutable artifact and service. An
Epiphany deployment brake may prevent changing Epiphany's swarm release; it
does not make the connector part of that release and must not wake the swarm.
The connector's own deployment and continuity remain explicit.
The default endpoint is `127.0.0.1:4103`, maximum payload is one MiB, and
physical concurrency is eight. On Yggdrasil, the Odin publication endpoint is
`10.77.0.1:17871`; local `127.0.0.1:17871` belongs to Idunn.
## Verification
Focused tests prove:
- encrypted round trips and exact request correlation;
- wrong caller, model, expiry, replay, tool, and reasoning refusals;
- no reasoning or tool material crosses the accepted result boundary;
- connector status contains no credential or application state;
- oversized TCP-framed payloads are refused before allocation;
- provider cache-token telemetry survives the Codex spine;
- a consumer transport can use the real framed/encrypted boundary without
learning connector credentials or Epiphany state.
The build surface is one new production binary in the existing OpenAI runtime
crate. It reuses the existing CultNet, CultMesh, Codex login, and model-spine
dependencies. It does not add a second model client or a second orchestration
runtime.

View file

@ -14,26 +14,23 @@ path = "src/lib.rs"
anyhow = "1"
chrono = "0.4.44"
ctrlc = { version = "3.5.1", features = ["termination"] }
cultcache-rs = { path = "../vendor/cultcache-rs" }
cultmesh-rs = { path = "../vendor/cultmesh-rs" }
cultnet-rs = { path = "../vendor/cultnet-rs" }
epiphany-openai-adapter = { path = "../epiphany-openai-adapter" }
epiphany-release-construction = { path = "../epiphany-release-construction" }
cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "a0813c6eed24d30bf88073ef615b633c77ebfcd6", package = "cultcache-rs" }
cultmesh-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "a0813c6eed24d30bf88073ef615b633c77ebfcd6", package = "cultmesh-rs" }
cultnet-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "a0813c6eed24d30bf88073ef615b633c77ebfcd6", package = "cultnet-rs" }
epiphany-model-adapter = { path = "../epiphany-model-adapter" }
epiphany-tool-adapter = { path = "../epiphany-tool-adapter" }
epiphany-state-model = { path = "../epiphany-state-model" }
fs2 = "0.4"
ghostlight-persona-projection = { git = "https://github.com/GameCult/Ghostlight.git", rev = "22937b71f056d2000dd7ea1f6af0c45825de495f", package = "ghostlight-persona-projection" }
ed25519-dalek = "2"
ignore = "0.4.23"
rmp-serde = "1"
serde = { version = "1", features = ["derive"] }
serde ={ version = "1", features = ["derive"] }
serde_json = "1"
semver = { version = "1", features = ["serde"] }
sha2 = "0.10"
uuid = { version = "1", features = ["serde", "v4", "v5"] }
[dev-dependencies]
pretty_assertions = "1.4.1"
tempfile = "3"
[target.'cfg(windows)'.dependencies]

View file

@ -186,7 +186,7 @@ pub fn validate_request(request: &AdmittedModelDirectionConsiderationRequest) ->
Ok(())
}
pub fn request_is_superseded(
pub(crate) fn request_is_superseded(
cache: &cultcache_rs::CultCache,
request: &AdmittedModelDirectionConsiderationRequest,
) -> Result<bool> {
@ -240,7 +240,7 @@ pub fn result_id_for_launch(request_id: &str, job_id: &str) -> String {
)
}
pub fn render_prompt(request: &AdmittedModelDirectionConsiderationRequest) -> String {
pub(crate) fn render_prompt(request: &AdmittedModelDirectionConsiderationRequest) -> String {
format!(
"Act as Epiphany Imagination for one proposal-only direction consideration. Inspect the exact current keyed Modeling map bound by request {} at projection digest {}. Suggest options or hold. Do not adopt, edit, execute, release, or deploy.",
request.request_id, request.model_projection_digest

View file

@ -13,35 +13,6 @@ pub const EPIPHANY_VERIFICATION_OWNER_ROLE: &str = "epiphany-verifier";
pub const EPIPHANY_REORIENT_LAUNCH_BINDING_ID: &str = "reorient-worker";
pub const EPIPHANY_REORIENT_OWNER_ROLE: &str = "epiphany-reorient";
pub fn epiphany_role_binding_id(role_id: EpiphanyRoleResultRoleId) -> Result<&'static str, String> {
match role_id {
EpiphanyRoleResultRoleId::Imagination => Ok(EPIPHANY_IMAGINATION_ROLE_BINDING_ID),
EpiphanyRoleResultRoleId::Research => Ok(EPIPHANY_RESEARCH_ROLE_BINDING_ID),
EpiphanyRoleResultRoleId::Modeling => Ok(EPIPHANY_MODELING_ROLE_BINDING_ID),
EpiphanyRoleResultRoleId::Verification => Ok(EPIPHANY_VERIFICATION_ROLE_BINDING_ID),
EpiphanyRoleResultRoleId::Implementation => Err(
"implementation is owned by the main coding agent; no role specialist launch template exists"
.to_string(),
),
EpiphanyRoleResultRoleId::Reorientation => Err(
"reorientation uses epiphany.coordinator.reorient.launch and epiphany.coordinator.reorient.result.read"
.to_string(),
),
}
}
pub fn epiphany_role_owner(role_id: EpiphanyRoleResultRoleId) -> Result<&'static str, String> {
match role_id {
EpiphanyRoleResultRoleId::Imagination => Ok(EPIPHANY_IMAGINATION_OWNER_ROLE),
EpiphanyRoleResultRoleId::Research => Ok(EPIPHANY_RESEARCH_OWNER_ROLE),
EpiphanyRoleResultRoleId::Modeling => Ok(EPIPHANY_MODELING_OWNER_ROLE),
EpiphanyRoleResultRoleId::Verification => Ok(EPIPHANY_VERIFICATION_OWNER_ROLE),
EpiphanyRoleResultRoleId::Implementation | EpiphanyRoleResultRoleId::Reorientation => {
Err(epiphany_role_binding_id(role_id).unwrap_err())
}
}
}
pub fn epiphany_role_label(role_id: EpiphanyRoleResultRoleId) -> &'static str {
match role_id {
EpiphanyRoleResultRoleId::Implementation => "implementation",
@ -760,17 +731,3 @@ pub fn epiphany_reorient_launch_output_schema() -> serde_json::Value {
"additionalProperties": true
})
}
pub fn unique_strings(values: impl IntoIterator<Item = String>) -> Vec<String> {
let mut unique = Vec::new();
extend_unique_strings(&mut unique, values);
unique
}
fn extend_unique_strings(target: &mut Vec<String>, values: impl IntoIterator<Item = String>) {
for value in values {
if !target.iter().any(|existing| existing == &value) {
target.push(value);
}
}
}

View file

@ -355,7 +355,7 @@ impl AtlasCultCacheStore {
cache.register_entry_type::<AtlasEntanglementProjection>()?;
cache.register_entry_type::<AtlasImpactDedupeRecord>()?;
cache.register_entry_type::<AtlasImpactStateRecord>()?;
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&self.path));
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&self.path))?;
cache.pull_all_backing_stores()?;
Ok(cache)
}

File diff suppressed because it is too large Load diff

View file

@ -2,10 +2,9 @@ use anyhow::{Result, bail};
use chrono::Utc;
use epiphany_core::{
EpiphanyMemoryAnchor, EpiphanyMemoryDomain, EpiphanyMemoryLifecycle, EpiphanyMemoryNode,
EpiphanyMemoryNodeKind, EpiphanyRepoModelSeed,
EpiphanyRepoModelSeedDocuments, ObserveOutcome, RuntimeSpineInitOptions,
admit_repository_body_observation, bind_repository_body, bind_runtime_to_swarm,
initialize_keyed_repo_model, initialize_runtime_spine,
EpiphanyMemoryNodeKind, EpiphanyRepoModelSeed, EpiphanyRepoModelSeedDocuments, ObserveOutcome,
RuntimeSpineInitOptions, admit_repository_body_observation, bind_repository_body,
bind_runtime_to_swarm, initialize_keyed_repo_model, initialize_runtime_spine,
load_current_runtime_repository_body_basis, load_repository_body_status,
observe_repository_body,
};

View file

@ -10,19 +10,17 @@ use epiphany_core::{
derive_resident_cognition_readiness, enqueue_resident_self_pressure_idempotent,
import_bifrost_persona_feedback_deliveries, ingest_resident_self_current_work_pressure,
issue_resident_self_grant, live_resident_self_typed_request_ids,
load_epiphany_cultmesh_swarm_brake,
load_resident_self_state, materialize_resident_self_domain_obligations,
observe_process_instance, prepare_resident_self_launch,
publish_resident_provider_readiness, reap_exited_child_process,
load_epiphany_cultmesh_swarm_brake, load_resident_self_state,
materialize_resident_self_domain_obligations, observe_process_instance,
prepare_resident_self_launch, publish_resident_provider_readiness, reap_exited_child_process,
recover_dead_runtime_worker_attempts, resident_cognitive_runtime_id,
resident_prepared_launch_thread_id, resident_self_child_claim,
resident_self_local_provider_status, retain_completed_runtime_sessions,
retain_failed_runtime_worker_attempts,
retain_fulfilled_runtime_worker_attempts, retain_resident_self_lifecycles,
runtime_worker_process_claims, settle_resident_self_exited_coordinator,
settle_resident_self_receipt_free_dead_coordinator, terminate_process_instance,
validate_persona_feedback_store_separation, validate_resident_self_coordinator_receipt_binding,
validate_resident_self_store_separation,
retain_failed_runtime_worker_attempts, retain_fulfilled_runtime_worker_attempts,
retain_resident_self_lifecycles, runtime_worker_process_claims,
settle_resident_self_exited_coordinator, settle_resident_self_receipt_free_dead_coordinator,
terminate_process_instance, validate_persona_feedback_store_separation,
validate_resident_self_coordinator_receipt_binding, validate_resident_self_store_separation,
};
use serde_json::json;
use std::collections::BTreeMap;
@ -39,6 +37,9 @@ use std::time::Duration;
const RESIDENT_MAINTENANCE_INTERVAL: Duration = Duration::from_secs(60);
fn main() -> Result<()> {
if env::args().nth(1).as_deref() == Some("initialize-and-release-brake") {
return initialize_and_release_brake();
}
let mut args = Args::parse()?;
authenticate_resident_self_policy(&mut args.policy)?;
args.policy.validate()?;
@ -119,6 +120,46 @@ fn main() -> Result<()> {
Ok(())
}
fn initialize_and_release_brake() -> Result<()> {
let mut values = BTreeMap::new();
let mut args = env::args().skip(2);
while let Some(flag) = args.next() {
let value = args
.next()
.ok_or_else(|| anyhow!("missing value for {flag}"))?;
values.insert(flag, value);
}
let required = |name: &str| {
values
.get(name)
.filter(|value| !value.trim().is_empty())
.cloned()
.ok_or_else(|| anyhow!("missing {name}"))
};
let store = PathBuf::from(required("--local-verse-store")?);
let runtime_id = required("--runtime-id")?;
let actor_id = required("--actor-id")?;
let reason = required("--reason")?;
let now = Utc::now().to_rfc3339();
epiphany_core::engage_epiphany_cultmesh_swarm_brake(
&store,
&runtime_id,
"Fresh runtime begins braked before explicit release.",
&actor_id,
&now,
false,
)?;
let brake = epiphany_core::release_epiphany_cultmesh_swarm_brake(
&store,
&runtime_id,
reason,
actor_id,
now,
)?;
println!("{}", serde_json::to_string_pretty(&brake)?);
Ok(())
}
fn require_resident_git(executable: &str) -> Result<()> {
let output = Command::new(executable)
.arg("--version")
@ -135,11 +176,7 @@ fn require_resident_git(executable: &str) -> Result<()> {
fn maintain_resident_runtime(args: &Args, state: &ResidentSelfState) -> Result<()> {
retain_runtime_receipts(args, state)?;
retain_resident_self_lifecycles(
&args.state_store,
args.retained_closed_lifecycles,
Utc::now().timestamp_millis().max(0) as u64,
)?;
retain_resident_self_lifecycles(&args.state_store, args.retained_closed_lifecycles)?;
retain_runtime_worker_attempts(args)?;
Ok(())
}
@ -268,7 +305,6 @@ fn cycle(
bind_runtime_repository_domain(
&args.policy.runtime_store,
&args.feedback_target_repository,
&Utc::now().to_rfc3339(),
)?;
materialize_resident_self_domain_obligations(
&args.policy.runtime_store,
@ -502,7 +538,11 @@ impl Args {
runtime_store: path("--runtime-store")?,
local_verse_store: path("--local-verse-store")?,
artifact_root: path("--artifact-root")?,
codex_home: path("--codex-home")?,
model_connector_endpoint: value
.get("--connector-endpoint")
.ok_or_else(|| anyhow!("missing --connector-endpoint"))?
.parse()
.context("invalid --connector-endpoint")?,
mcp_config: path("--mcp-config")?,
model_provider: value
.get("--model-provider")
@ -783,7 +823,7 @@ mod brake_tests {
runtime_store,
local_verse_store: verse_store,
artifact_root: shared.join("artifacts"),
codex_home: shared.join("codex-home"),
model_connector_endpoint: "127.0.0.1:17891".parse().unwrap(),
mcp_config: shared.join("mcp.toml"),
model_provider: "test".into(),
model: "test-model".into(),
@ -810,5 +850,4 @@ mod brake_tests {
)?);
Ok(())
}
}

View file

@ -9,7 +9,7 @@ fn digest_id(prefix: &str, causal_parts: &[&str]) -> String {
format!("{prefix}-{:x}", digest.finalize())
}
pub fn proposal_modeling_request_id(
pub(crate) fn proposal_modeling_request_id(
runtime_id: &str,
proposal_id: &str,
payload_sha256: &str,
@ -20,7 +20,7 @@ pub fn proposal_modeling_request_id(
)
}
pub fn frontier_research_request_id(
pub(crate) fn frontier_research_request_id(
runtime_id: &str,
frontier_item_id: &str,
frontier_item_hash: &str,
@ -31,7 +31,7 @@ pub fn frontier_research_request_id(
)
}
pub fn frontier_planning_request_id(
pub(crate) fn frontier_planning_request_id(
runtime_id: &str,
frontier_item_id: &str,
frontier_item_hash: &str,
@ -42,7 +42,7 @@ pub fn frontier_planning_request_id(
)
}
pub fn frontier_plan_mind_request_id(
pub(crate) fn frontier_plan_mind_request_id(
runtime_id: &str,
planning_request_id: &str,
imagination_result_id: &str,
@ -59,7 +59,7 @@ pub fn frontier_plan_mind_request_id(
)
}
pub fn frontier_verdict_modeling_request_id(
pub(crate) fn frontier_verdict_modeling_request_id(
runtime_id: &str,
soul_verdict_receipt_id: &str,
verification_result_id: &str,
@ -76,7 +76,7 @@ pub fn frontier_verdict_modeling_request_id(
)
}
pub fn admitted_model_direction_request_id(
pub(crate) fn admitted_model_direction_request_id(
runtime_id: &str,
model_projection_digest: &str,
previous_terminal_result_id: Option<&str>,

View file

@ -1,63 +0,0 @@
use crate::EpiphanyReorientFindingInterpretation;
use cultcache_rs::DatabaseEntry;
pub const CONTINUITY_RECOVERY_RECEIPT_TYPE: &str = "epiphany.continuity.recovery_receipt";
pub const CONTINUITY_RECOVERY_RECEIPT_SCHEMA_VERSION: &str =
"epiphany.continuity.recovery_receipt.v0";
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "epiphany.continuity.recovery_receipt",
schema = "ContinuityRecoveryReceipt"
)]
pub struct ContinuityRecoveryReceipt {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub receipt_id: String,
#[cultcache(key = 2)]
pub source_result_id: String,
#[cultcache(key = 3)]
pub source_job_id: String,
#[cultcache(key = 4)]
pub binding_id: String,
#[cultcache(key = 5)]
pub mode: String,
#[cultcache(key = 6)]
pub checkpoint_still_valid: String,
#[cultcache(key = 7)]
pub summary: String,
#[cultcache(key = 8)]
pub next_safe_move: String,
#[cultcache(key = 9)]
pub files_inspected: Vec<String>,
#[cultcache(key = 10)]
pub emitted_at: String,
#[cultcache(key = 11)]
pub contract: String,
}
pub fn continuity_recovery_receipt_from_reorient_finding(
receipt_id: String,
binding_id: String,
finding: &EpiphanyReorientFindingInterpretation,
emitted_at: String,
) -> ContinuityRecoveryReceipt {
ContinuityRecoveryReceipt {
schema_version: CONTINUITY_RECOVERY_RECEIPT_SCHEMA_VERSION.to_string(),
receipt_id,
source_result_id: finding.runtime_result_id.clone().unwrap_or_default(),
source_job_id: finding.runtime_job_id.clone().unwrap_or_default(),
binding_id,
mode: finding.mode.clone().unwrap_or_else(|| "unknown".to_string()),
checkpoint_still_valid: finding
.checkpoint_still_valid
.map(|value| value.to_string())
.unwrap_or_else(|| "unknown".to_string()),
summary: finding.summary.clone().unwrap_or_default(),
next_safe_move: finding.next_safe_move.clone().unwrap_or_default(),
files_inspected: finding.files_inspected.clone(),
emitted_at,
contract: "Continuity recovery emitted from a reviewed reorientation finding; it proves what survived rupture before Mind admits recovery state.".to_string(),
}
}

View file

@ -1,17 +1,13 @@
use crate::{
EpiphanyMindCommitAuthority, EpiphanyMindCommitOutcome, EpiphanyMindCommitReceipt,
EpiphanyMindDocumentVersion, EpiphanyMindObjectiveDocument, EpiphanyMindView,
MIND_OBJECTIVE_KEY, assemble_mind_view, commit_operator_mind_mutation, runtime_spine_cache,
EpiphanyMindDocumentVersion, EpiphanyMindObjectiveDocument, MIND_OBJECTIVE_KEY,
assemble_mind_view, commit_operator_mind_mutation, runtime_spine_cache,
};
use anyhow::{Result, anyhow};
use cultcache_rs::DatabaseEntry;
use sha2::{Digest, Sha256};
use std::path::Path;
pub const USER_OBJECTIVE_INTAKE_SCHEMA_VERSION: &str = "gamecult.epiphany.user_objective_intake.v0";
pub const USER_OBJECTIVE_INTAKE_TYPE: &str = "epiphany.coordinator.user_objective_intake";
pub const USER_OBJECTIVE_INTAKE_CONTRACT: &str = "The human supplies the initial objective. Self records that assertion and the canonical keyed Mind objective in one CAS. Thread identity is provenance only. Repeated identical intake is read-idempotent; replacement requires a separate reviewed adoption flow.";
#[derive(Debug, Clone)]
pub struct UserObjectiveIntakeInput {
pub thread_id: String,
@ -28,29 +24,20 @@ pub struct UserObjectiveIntakeInput {
)]
pub struct UserObjectiveIntake {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub intake_id: String,
#[cultcache(key = 2)]
pub thread_id: String,
#[cultcache(key = 3)]
#[cultcache(key = 1)]
pub objective: String,
#[cultcache(key = 4)]
pub objective_sha256: String,
#[cultcache(key = 5)]
#[cultcache(key = 2)]
pub source_actor: String,
#[cultcache(key = 6)]
#[cultcache(key = 3)]
pub source_ref: String,
#[cultcache(key = 7)]
#[cultcache(key = 4)]
pub submitted_at: String,
#[cultcache(key = 8)]
pub contract: String,
}
#[derive(Debug, Clone)]
pub struct UserObjectiveIntakeApplied {
pub intake: UserObjectiveIntake,
pub mind: EpiphanyMindView,
pub mind_projection_digest: String,
pub commit_receipt: EpiphanyMindCommitReceipt,
pub changed: bool,
}
@ -71,7 +58,6 @@ pub fn intake_user_objective(
{
return Err(anyhow!("invalid typed user-objective intake"));
}
let objective_sha256 = format!("{:x}", Sha256::digest(objective.as_bytes()));
let intake_id = format!("user-objective-{:x}", Sha256::digest(thread_id.as_bytes()));
let mut cache = runtime_spine_cache(store)?;
@ -82,7 +68,6 @@ pub fn intake_user_objective(
.ok_or_else(|| anyhow!("typed user-objective intake lost its Mind objective"))?;
if existing.thread_id != thread_id
|| existing.objective != objective
|| existing.objective_sha256 != objective_sha256
|| existing.source_actor != source_actor
|| existing.source_ref != source_ref
|| objective_document.objective.trim() != objective
@ -113,8 +98,7 @@ pub fn intake_user_objective(
})
.ok_or_else(|| anyhow!("typed user-objective intake lost its Mind commit receipt"))?;
return Ok(UserObjectiveIntakeApplied {
intake: existing,
mind: assemble_mind_view(store)?,
mind_projection_digest: assemble_mind_view(store)?.projection_digest,
commit_receipt: receipt,
changed: false,
});
@ -128,15 +112,11 @@ pub fn intake_user_objective(
));
}
let intake = UserObjectiveIntake {
schema_version: USER_OBJECTIVE_INTAKE_SCHEMA_VERSION.into(),
intake_id: intake_id.clone(),
thread_id: thread_id.to_string(),
objective: objective.to_string(),
objective_sha256,
source_actor: source_actor.to_string(),
source_ref: source_ref.to_string(),
submitted_at: input.submitted_at,
contract: USER_OBJECTIVE_INTAKE_CONTRACT.into(),
};
let provenance = cache.prepare_entry(&intake_id, &intake)?.0;
let objective_write = crate::mind_documents::prepare_mind_document(
@ -160,8 +140,7 @@ pub fn intake_user_objective(
}
};
Ok(UserObjectiveIntakeApplied {
intake,
mind: assemble_mind_view(store)?,
mind_projection_digest: assemble_mind_view(store)?.projection_digest,
commit_receipt,
changed: true,
})
@ -196,7 +175,10 @@ mod tests {
)?;
let first = intake_user_objective(&store, input("Map the machine"))?;
assert!(first.changed);
assert_eq!(first.mind.objective.as_deref(), Some("Map the machine"));
assert_eq!(
assemble_mind_view(&store)?.objective.as_deref(),
Some("Map the machine")
);
assert_eq!(
first.commit_receipt.invariant_owner,
"Self.user_objective_intake"
@ -209,11 +191,16 @@ mod tests {
.iter()
.all(|envelope| envelope.r#type != "epiphany.thread_state")
);
let after_first = std::fs::read(&store)?;
let repeated = intake_user_objective(&store, input(" Map the machine "))?;
assert!(!repeated.changed);
assert_eq!(repeated.intake, first.intake);
assert_eq!(
repeated.mind_projection_digest,
first.mind_projection_digest
);
assert_eq!(repeated.commit_receipt, first.commit_receipt);
assert_eq!(std::fs::read(&store)?, after_first);
let before = std::fs::read(&store)?;
let error = intake_user_objective(&store, input("Replace the machine"))

View file

@ -1,95 +1,68 @@
use crate::*;
use anyhow::{Result, anyhow};
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyReorientResultStatus {
Pending,
Completed,
Failed,
}
#[derive(Debug, Clone)]
pub struct EpiphanyCoordinatorReorientResultSnapshot {
pub status: EpiphanyCrrcResultStatus,
pub status: EpiphanyReorientResultStatus,
pub finding: Option<EpiphanyReorientFindingInterpretation>,
pub note: String,
}
pub fn read_runtime_reorient_result(
runtime_store_path: Option<&Path>,
runtime_store_path: &Path,
job_id: &str,
) -> EpiphanyCoordinatorReorientResultSnapshot {
let Some(runtime_store_path) = runtime_store_path else {
return reorient_snapshot(
EpiphanyCrrcResultStatus::Pending,
None,
Some(
"Resident Self owns this reorientation worker; no loaded runtime-spine store is available yet.",
),
);
};
let snapshot = match runtime_job_snapshot(runtime_store_path, job_id) {
Ok(Some(snapshot)) => snapshot,
Ok(None) => {
return reorient_snapshot(
EpiphanyCrrcResultStatus::Pending,
) -> Result<EpiphanyCoordinatorReorientResultSnapshot> {
let mut cache = runtime_spine_cache(runtime_store_path)?;
cache.pull_all_backing_stores()?;
let job = match cache.get::<EpiphanyRuntimeJob>(job_id)? {
Some(job) => job,
None => {
return Ok(reorient_snapshot(
EpiphanyReorientResultStatus::Pending,
None,
Some(&format!(
"Resident runtime job {job_id:?} has not reported typed state yet."
)),
);
}
Err(error) => {
return reorient_snapshot(
EpiphanyCrrcResultStatus::BackendUnavailable,
None,
Some(&format!(
"Failed to read resident runtime-spine job {job_id:?}: {error}"
)),
);
));
}
};
let status = reorient_result_status(&snapshot);
let status = reorient_result_status(&job.status);
let finding = match status {
EpiphanyCrrcResultStatus::Completed => {
match runtime_reorient_worker_result(runtime_store_path, job_id) {
Ok(Some(result)) => Some(interpret_runtime_reorient_worker_result(&result)),
Ok(None) => {
return reorient_snapshot(
EpiphanyCrrcResultStatus::BackendUnavailable,
None,
Some(&format!(
"Resident runtime job {job_id:?} completed without an EpiphanyRuntimeReorientWorkerResult typed document; generic lifecycle receipts are not reviewable findings."
)),
);
}
Err(error) => {
return reorient_snapshot(
EpiphanyCrrcResultStatus::BackendUnavailable,
None,
Some(&format!(
"Failed to read typed reorientation worker result for resident runtime job {job_id:?}: {error}"
)),
);
}
}
}
EpiphanyCrrcResultStatus::Failed | EpiphanyCrrcResultStatus::Cancelled => snapshot
.result
EpiphanyReorientResultStatus::Completed => Some(interpret_runtime_reorient_worker_result(
&cache
.get::<EpiphanyRuntimeReorientWorkerResult>(job_id)?
.ok_or_else(|| {
anyhow!(
"resident runtime job {job_id:?} completed without its typed reorientation result"
)
})?,
)),
EpiphanyReorientResultStatus::Failed => cache
.get::<EpiphanyRuntimeReorientWorkerResult>(job_id)?
.as_ref()
.map(interpret_reorient_lifecycle_failure),
_ => None,
.map(interpret_runtime_reorient_worker_result),
EpiphanyReorientResultStatus::Pending => None,
};
reorient_snapshot(status, finding, None)
Ok(reorient_snapshot(status, finding, None))
}
fn reorient_snapshot(
status: EpiphanyCrrcResultStatus,
status: EpiphanyReorientResultStatus,
finding: Option<EpiphanyReorientFindingInterpretation>,
note_override: Option<&str>,
) -> EpiphanyCoordinatorReorientResultSnapshot {
let note = note_override.map(str::to_string).unwrap_or_else(|| {
render_reorient_result_note(
status,
finding.as_ref(),
finding
.as_ref()
.and_then(|finding| finding.job_error.as_deref()),
)
});
let note = note_override
.map(str::to_string)
.unwrap_or_else(|| render_reorient_result_note(status, finding.as_ref()));
EpiphanyCoordinatorReorientResultSnapshot {
status,
finding,
@ -97,80 +70,31 @@ fn reorient_snapshot(
}
}
fn interpret_reorient_lifecycle_failure(
result: &EpiphanyRuntimeJobResult,
) -> EpiphanyReorientFindingInterpretation {
EpiphanyReorientFindingInterpretation {
mode: None,
summary: Some(result.summary.clone()),
next_safe_move: nonempty(&result.next_safe_move),
checkpoint_still_valid: None,
files_inspected: Vec::new(),
frontier_node_ids: Vec::new(),
evidence_ids: result.evidence_refs.clone(),
artifact_refs: result.artifact_refs.clone(),
runtime_result_id: Some(result.result_id.clone()),
runtime_job_id: Some(result.job_id.clone()),
job_error: Some(result.summary.clone()),
item_error: None,
}
}
fn reorient_result_status(snapshot: &EpiphanyRuntimeJobSnapshot) -> EpiphanyCrrcResultStatus {
match snapshot.job.status {
EpiphanyRuntimeJobStatus::Queued => EpiphanyCrrcResultStatus::Pending,
EpiphanyRuntimeJobStatus::Running | EpiphanyRuntimeJobStatus::WaitingForReview => {
EpiphanyCrrcResultStatus::Running
}
EpiphanyRuntimeJobStatus::Completed if snapshot.result.is_some() => {
EpiphanyCrrcResultStatus::Completed
}
EpiphanyRuntimeJobStatus::Completed => EpiphanyCrrcResultStatus::Pending,
EpiphanyRuntimeJobStatus::Failed => EpiphanyCrrcResultStatus::Failed,
EpiphanyRuntimeJobStatus::Cancelled => EpiphanyCrrcResultStatus::Cancelled,
fn reorient_result_status(status: &EpiphanyRuntimeJobStatus) -> EpiphanyReorientResultStatus {
match status {
EpiphanyRuntimeJobStatus::Queued => EpiphanyReorientResultStatus::Pending,
EpiphanyRuntimeJobStatus::Completed => EpiphanyReorientResultStatus::Completed,
EpiphanyRuntimeJobStatus::Failed => EpiphanyReorientResultStatus::Failed,
}
}
fn render_reorient_result_note(
status: EpiphanyCrrcResultStatus,
status: EpiphanyReorientResultStatus,
finding: Option<&EpiphanyReorientFindingInterpretation>,
item_error: Option<&str>,
) -> String {
match status {
EpiphanyCrrcResultStatus::Completed => finding.map_or_else(
|| "Reorientation worker completed, but no structured result was recorded.".to_string(),
|finding| {
format!(
"Reorientation worker completed. Next safe move: {}",
finding.next_safe_move.as_deref().unwrap_or("not supplied")
)
},
EpiphanyReorientResultStatus::Completed => format!(
"Reorientation worker completed. Next safe move: {}",
finding
.and_then(|finding| finding.next_safe_move.as_deref())
.unwrap_or("not supplied")
),
EpiphanyCrrcResultStatus::Failed => item_error
EpiphanyReorientResultStatus::Failed => finding
.and_then(|finding| finding.job_error.as_deref())
.map(|error| format!("Reorientation worker failed: {error}"))
.unwrap_or_else(|| "Reorientation worker failed.".to_string()),
EpiphanyCrrcResultStatus::Cancelled => {
"Reorientation worker was cancelled before producing a result.".to_string()
}
EpiphanyCrrcResultStatus::Running => "Reorientation worker is still running.".to_string(),
EpiphanyCrrcResultStatus::Pending => {
EpiphanyReorientResultStatus::Pending => {
"Reorientation worker has not produced a result yet.".to_string()
}
EpiphanyCrrcResultStatus::MissingState => {
"No authoritative Epiphany state exists for this thread.".to_string()
}
EpiphanyCrrcResultStatus::MissingBinding => {
"No matching Epiphany reorientation worker binding exists.".to_string()
}
EpiphanyCrrcResultStatus::BackendUnavailable => {
"The bound runtime backend is unavailable.".to_string()
}
EpiphanyCrrcResultStatus::BackendMissing => {
"The bound runtime backend job or item is missing.".to_string()
}
}
}
fn nonempty(value: &str) -> Option<String> {
(!value.trim().is_empty()).then(|| value.to_string())
}

View file

@ -1,9 +1,9 @@
use anyhow::{Context, Result};
use crate::{
EpiphanyCoordinatorInput, EpiphanyCrrcAction, EpiphanyCurrentWorkProjection,
EpiphanyRoleBoardInput, RepoFrontierPlanningLifecycle, RepoFrontierPlanningLifecycleStage,
EpiphanyCoordinatorInput, EpiphanyCurrentWorkProjection, EpiphanyRoleBoardInput,
RepoFrontierPlanningLifecycle, RepoFrontierPlanningLifecycleStage,
RepoFrontierResearchLifecycle, RepoFrontierResearchLifecycleStage, derive_role_board,
};
use anyhow::{Context, Result};
use serde_json::{Value, json};
use std::{
env,
@ -23,38 +23,25 @@ const MAX_OPERATOR_TEXT_CHARS: usize = 1200;
pub fn native_coordinator_json(runtime_store: &Path, thread_id: &str) -> Result<Value> {
let store_path = absolute_path(runtime_store)?;
let mind = if store_path.exists() {
Some(
crate::assemble_mind_view(&store_path).with_context(|| {
format!("failed to assemble keyed Mind {}", store_path.display())
})?,
)
} else {
None
};
let current_work = if mind.is_some() {
crate::project_current_work(&store_path)
.context("failed to derive current work from keyed Mind and runtime receipts")?
} else {
empty_current_work()
};
let latest_reorientation_decision = mind
let projected_work = store_path
.exists()
.then(|| {
crate::project_current_work(&store_path)
.context("failed to derive current work from keyed Mind and runtime receipts")
})
.transpose()?;
let mind_present = projected_work.is_some();
let projection_digest = projected_work
.as_ref()
.and_then(|mind| mind.reorientation_decisions.last());
let crrc_action =
if latest_reorientation_decision.is_some_and(|decision| decision.mode == "regather") {
EpiphanyCrrcAction::RegatherManually
} else {
EpiphanyCrrcAction::Continue
};
.map(|work| work.mind_projection_digest.clone());
let current_work = projected_work.unwrap_or_else(empty_current_work);
let roles = derive_role_board(EpiphanyRoleBoardInput {
mind_present: mind.is_some(),
mind_present,
current_work: current_work.clone(),
});
let coordinator = crate::recommend_coordinator_action(EpiphanyCoordinatorInput {
mind_present: mind.is_some(),
crrc_action,
mind_present,
current_work: current_work.clone(),
});
Ok(sanitize_for_operator(json!({
@ -62,8 +49,8 @@ pub fn native_coordinator_json(runtime_store: &Path, thread_id: &str) -> Result<
"read": {
"source": "native",
"mindStore": store_path,
"mindPresent": mind.is_some(),
"projectionDigest": mind.as_ref().map(|mind| mind.projection_digest.as_str()),
"mindPresent": mind_present,
"projectionDigest": projection_digest,
},
"roles": {"threadId": thread_id, "source": "native", "roles": roles},
"currentWork": current_work,
@ -74,6 +61,7 @@ pub fn native_coordinator_json(runtime_store: &Path, thread_id: &str) -> Result<
fn empty_current_work() -> EpiphanyCurrentWorkProjection {
EpiphanyCurrentWorkProjection {
mind_projection_digest: String::new(),
operator_regather_required: false,
body_modeling: None,
research: RepoFrontierResearchLifecycle {
stage: RepoFrontierResearchLifecycleStage::Terminal,

View file

@ -5,8 +5,7 @@ use cultmesh_rs::{CultMesh, CultMeshNode, CultMeshNodeOptions, cultmesh_document
use std::path::Path;
pub const EPIPHANY_CULTMESH_SWARM_BRAKE_TYPE: &str = "epiphany.cultmesh.swarm_brake";
pub const EPIPHANY_CULTMESH_SWARM_BRAKE_SCHEMA_VERSION: &str =
"epiphany.cultmesh.swarm_brake.v0";
pub const EPIPHANY_CULTMESH_SWARM_BRAKE_SCHEMA_VERSION: &str = "epiphany.cultmesh.swarm_brake.v0";
pub const EPIPHANY_CULTMESH_SWARM_BRAKE_KEY: &str = "epiphany-local/swarm-brake";
pub const EPIPHANY_CANONICAL_SWARM_BRAKE_ID: &str = "epiphany/swarm-brake";
pub const EPIPHANY_CANONICAL_SWARM_BRAKE_OWNER: &str = "epiphany.swarm-brake";
@ -66,18 +65,6 @@ pub fn open_epiphany_cultmesh_node(
)
}
pub fn canonical_epiphany_swarm_brake_protected_surfaces() -> Vec<String> {
vec![
"resident.self".to_string(),
"coordinator.run".to_string(),
"persona.public_speech".to_string(),
"hands.consequence".to_string(),
"atlas.publish".to_string(),
"atlas.project".to_string(),
"atlas.impact_ingress".to_string(),
]
}
pub fn default_epiphany_cultmesh_swarm_brake(
generated_at_utc: impl Into<String>,
) -> EpiphanyCultMeshSwarmBrakeEntry {
@ -90,7 +77,15 @@ pub fn default_epiphany_cultmesh_swarm_brake(
.to_string(),
operator_agent_id: EPIPHANY_CANONICAL_SWARM_BRAKE_OWNER.to_string(),
affected_clusters: Vec::new(),
protected_surfaces: canonical_epiphany_swarm_brake_protected_surfaces(),
protected_surfaces: vec![
"resident.self".into(),
"coordinator.run".into(),
"persona.public_speech".into(),
"hands.consequence".into(),
"atlas.publish".into(),
"atlas.project".into(),
"atlas.impact_ingress".into(),
],
created_at_utc: generated_at_utc.into(),
expires_at_utc: None,
private_state_exposed: false,
@ -113,7 +108,9 @@ pub fn engage_epiphany_cultmesh_swarm_brake(
return Err(anyhow!("swarm brake engagement requires an actor identity"));
}
if allow_engaged_adoption && actor_id != "Idunn" {
return Err(anyhow!("only Idunn may adopt an already-engaged legacy brake"));
return Err(anyhow!(
"only Idunn may adopt an already-engaged legacy brake"
));
}
if let Some(current) = load_epiphany_cultmesh_swarm_brake(&store_path, runtime_id.clone())? {
let foreign = current.brake_id != EPIPHANY_CANONICAL_SWARM_BRAKE_ID
@ -200,7 +197,9 @@ fn validate_swarm_brake(brake: &EpiphanyCultMeshSwarmBrakeEntry) -> Result<()> {
|| brake.runtime_id.trim().is_empty()
|| brake.created_at_utc.trim().is_empty()
{
return Err(anyhow!("swarm brake identity, scope, runtime, and timestamp are required"));
return Err(anyhow!(
"swarm brake identity, scope, runtime, and timestamp are required"
));
}
if !matches!(brake.status.as_str(), "released" | "engaged") {
return Err(anyhow!("swarm brake status must be released or engaged"));
@ -210,7 +209,9 @@ fn validate_swarm_brake(brake: &EpiphanyCultMeshSwarmBrakeEntry) -> Result<()> {
|| brake.operator_agent_id.trim().is_empty()
|| (brake.affected_clusters.is_empty() && brake.protected_surfaces.is_empty()))
{
return Err(anyhow!("engaged swarm brake requires operator id, reason, and scope"));
return Err(anyhow!(
"engaged swarm brake requires operator id, reason, and scope"
));
}
Ok(())
}
@ -233,7 +234,12 @@ mod tests {
)?;
assert_eq!(engaged.status, "engaged");
assert_eq!(engaged.brake_id, EPIPHANY_CANONICAL_SWARM_BRAKE_ID);
assert!(engaged.protected_surfaces.iter().any(|value| value == "hands.consequence"));
assert!(
engaged
.protected_surfaces
.iter()
.any(|value| value == "hands.consequence")
);
assert_eq!(
load_epiphany_cultmesh_swarm_brake(&store, "runtime")?,
Some(engaged)

File diff suppressed because it is too large Load diff

View file

@ -1,168 +0,0 @@
use crate::EpiphanyResearchDecision;
use crate::EpiphanyRoleFindingInterpretation;
use cultcache_rs::DatabaseEntry;
pub const EYES_SOURCE_LOOKUP_RECEIPT_TYPE: &str = "epiphany.eyes.source_lookup_receipt";
pub const EYES_EVIDENCE_PACKET_TYPE: &str = "epiphany.eyes.evidence_packet";
pub const EYES_SOURCE_LOOKUP_RECEIPT_SCHEMA_VERSION: &str =
"epiphany.eyes.source_lookup_receipt.v0";
pub const EYES_EVIDENCE_PACKET_SCHEMA_VERSION: &str = "epiphany.eyes.evidence_packet.v1";
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "epiphany.eyes.source_lookup_receipt",
schema = "EyesSourceLookupReceipt"
)]
pub struct EyesSourceLookupReceipt {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub receipt_id: String,
#[cultcache(key = 2)]
pub source_job_id: String,
#[cultcache(key = 3)]
pub substrate_grant_receipt_id: String,
#[cultcache(key = 4)]
pub tool_intent_id: String,
#[cultcache(key = 5)]
pub tool_receipt_id: String,
#[cultcache(key = 6)]
pub provider: String,
#[cultcache(key = 7)]
pub repository: String,
#[cultcache(key = 8)]
pub revision: String,
#[cultcache(key = 9)]
pub path: String,
#[cultcache(key = 10)]
pub source_ref: String,
#[cultcache(key = 11)]
pub content_sha256: String,
#[cultcache(key = 12)]
pub byte_count: u64,
#[cultcache(key = 13)]
pub observed_at: String,
#[cultcache(key = 14)]
pub contract: String,
}
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
#[cultcache(type = "epiphany.eyes.evidence_packet", schema = "EyesEvidencePacket")]
pub struct EyesEvidencePacket {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub packet_id: String,
#[cultcache(key = 2)]
pub source_result_id: String,
#[cultcache(key = 3)]
pub source_job_id: String,
#[cultcache(key = 4)]
pub source_role_id: String,
#[cultcache(key = 5)]
pub evidence_ids: Vec<String>,
#[cultcache(key = 6)]
pub observation_ids: Vec<String>,
#[cultcache(key = 7)]
pub source_refs: Vec<String>,
#[cultcache(key = 8)]
pub summary: String,
#[cultcache(key = 9)]
pub uncertainty: String,
#[cultcache(key = 10)]
pub emitted_at: String,
#[cultcache(key = 11)]
pub contract: String,
#[cultcache(key = 12, default)]
pub source_lookup_receipt_ids: Vec<String>,
#[cultcache(key = 13)]
pub research_request_id: String,
#[cultcache(key = 14)]
pub decision_context_id: String,
}
pub fn eyes_evidence_packet_from_research_finding(
packet_id: String,
research_request_id: String,
decision_context_id: String,
finding: &EpiphanyRoleFindingInterpretation,
decision: &EpiphanyResearchDecision,
source_lookups: &[EyesSourceLookupReceipt],
emitted_at: String,
) -> EyesEvidencePacket {
let evidence_ids = decision
.evidence
.iter()
.filter_map(|evidence| non_empty_string(&evidence.id))
.collect::<Vec<_>>();
let observation_ids = decision
.observations
.iter()
.filter_map(|observation| non_empty_string(&observation.id))
.collect::<Vec<_>>();
let mut source_refs = Vec::new();
for evidence in &decision.evidence {
for code_ref in &evidence.code_refs {
let mut rendered = code_ref.path.display().to_string();
if let Some(start_line) = code_ref.start_line {
rendered.push(':');
rendered.push_str(&start_line.to_string());
}
if let Some(end_line) = code_ref.end_line
&& Some(end_line) != code_ref.start_line
{
rendered.push('-');
rendered.push_str(&end_line.to_string());
}
if let Some(symbol) = code_ref.symbol.as_deref().filter(|value| !value.is_empty()) {
rendered.push('#');
rendered.push_str(symbol);
}
push_unique(&mut source_refs, rendered);
}
}
for lookup in source_lookups {
push_unique(&mut source_refs, lookup.source_ref.clone());
}
EyesEvidencePacket {
schema_version: EYES_EVIDENCE_PACKET_SCHEMA_VERSION.to_string(),
packet_id,
source_result_id: finding.runtime_result_id.clone().unwrap_or_default(),
source_job_id: finding.runtime_job_id.clone().unwrap_or_default(),
source_role_id: "research".to_string(),
evidence_ids,
observation_ids,
source_refs,
summary: finding.summary.clone().unwrap_or_default(),
uncertainty: if finding.evidence_gaps.is_empty() && finding.risks.is_empty() {
"no declared research gaps or risks".to_string()
} else {
finding
.evidence_gaps
.iter()
.chain(finding.risks.iter())
.cloned()
.collect::<Vec<_>>()
.join("; ")
},
emitted_at,
contract: "Eyes packet emitted from a reviewed Research lane finding; it makes the source-gathering evidence claim citable before Mind admission.".to_string(),
source_lookup_receipt_ids: source_lookups
.iter()
.map(|lookup| lookup.receipt_id.clone())
.collect(),
research_request_id,
decision_context_id,
}
}
fn non_empty_string(value: &str) -> Option<String> {
let value = value.trim();
(!value.is_empty()).then(|| value.to_string())
}
fn push_unique(out: &mut Vec<String>, value: String) {
if !out.contains(&value) {
out.push(value);
}
}

View file

@ -1,95 +1,34 @@
use cultcache_rs::DatabaseEntry;
pub const HANDS_ACTION_INTENT_TYPE: &str = "epiphany.hands.action_intent";
pub const HANDS_ACTION_REVIEW_TYPE: &str = "epiphany.hands.action_review";
pub const HANDS_COMMAND_RECEIPT_TYPE: &str = "epiphany.hands.command_receipt";
pub const HANDS_PATCH_RECEIPT_TYPE: &str = "epiphany.hands.patch_receipt";
pub const HANDS_COMMIT_RECEIPT_TYPE: &str = "epiphany.hands.commit_receipt";
pub const HANDS_ACTION_INTENT_SCHEMA_VERSION: &str = "epiphany.hands.action_intent.v1";
pub const HANDS_ACTION_REVIEW_SCHEMA_VERSION: &str = "epiphany.hands.action_review.v0";
pub const HANDS_COMMAND_RECEIPT_SCHEMA_VERSION: &str = "epiphany.hands.command_receipt.v0";
pub const HANDS_PATCH_RECEIPT_SCHEMA_VERSION: &str = "epiphany.hands.patch_receipt.v0";
pub const HANDS_COMMIT_RECEIPT_SCHEMA_VERSION: &str = "epiphany.hands.commit_receipt.v0";
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
#[cultcache(type = "epiphany.hands.action_intent", schema = "HandsActionIntent")]
pub struct HandsActionIntent {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub intent_id: String,
#[cultcache(key = 2)]
#[cultcache(key = 1)]
pub runtime_job_id: String,
#[cultcache(key = 3)]
pub binding_id: String,
#[cultcache(key = 4)]
pub role: String,
#[cultcache(key = 5)]
pub authority_scope: String,
#[cultcache(key = 6)]
pub requested_action: String,
#[cultcache(key = 7)]
pub requested_paths: Vec<String>,
#[cultcache(key = 8)]
pub substrate_gate_grant_receipt_id: String,
#[cultcache(key = 9)]
pub requested_at: String,
#[cultcache(key = 10)]
pub contract: String,
#[cultcache(key = 11, default)]
pub frontier_route_id: String,
#[cultcache(key = 12, default)]
pub plan_candidate_sha256: String,
#[cultcache(key = 13, default)]
pub plan_action: String,
}
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
#[cultcache(type = "epiphany.hands.action_review", schema = "HandsActionReview")]
pub struct HandsActionReview {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub review_id: String,
#[cultcache(key = 2)]
pub intent_id: String,
pub requested_paths: Vec<String>,
#[cultcache(key = 3)]
pub decision: String,
#[cultcache(key = 4)]
pub allowed_operations: Vec<String>,
#[cultcache(key = 5)]
pub required_receipts: Vec<String>,
#[cultcache(key = 6)]
pub reasons: Vec<String>,
#[cultcache(key = 7)]
pub reviewed_at: String,
#[cultcache(key = 8)]
pub contract: String,
pub substrate_gate_grant_receipt_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
#[cultcache(type = "epiphany.hands.patch_receipt", schema = "HandsPatchReceipt")]
pub struct HandsPatchReceipt {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub receipt_id: String,
#[cultcache(key = 2)]
#[cultcache(key = 1)]
pub intent_id: String,
#[cultcache(key = 3)]
pub review_id: String,
#[cultcache(key = 4)]
pub substrate_gate_grant_receipt_id: String,
#[cultcache(key = 5)]
pub runtime_job_id: String,
#[cultcache(key = 6)]
#[cultcache(key = 2)]
pub changed_paths: Vec<String>,
#[cultcache(key = 7)]
#[cultcache(key = 3)]
pub summary: String,
#[cultcache(key = 8)]
#[cultcache(key = 4)]
pub emitted_at: String,
#[cultcache(key = 9)]
pub contract: String,
}
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
@ -99,153 +38,38 @@ pub struct HandsPatchReceipt {
)]
pub struct HandsCommandReceipt {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub receipt_id: String,
#[cultcache(key = 2)]
#[cultcache(key = 1)]
pub intent_id: String,
#[cultcache(key = 3)]
pub review_id: String,
#[cultcache(key = 4)]
pub substrate_gate_grant_receipt_id: String,
#[cultcache(key = 5)]
pub runtime_job_id: String,
#[cultcache(key = 6)]
#[cultcache(key = 2)]
pub command: String,
#[cultcache(key = 7)]
#[cultcache(key = 3)]
pub exit_code: String,
#[cultcache(key = 8)]
#[cultcache(key = 4)]
pub stdout_artifact: String,
#[cultcache(key = 9)]
#[cultcache(key = 5)]
pub stderr_artifact: String,
#[cultcache(key = 10)]
#[cultcache(key = 6)]
pub summary: String,
#[cultcache(key = 11)]
#[cultcache(key = 7)]
pub emitted_at: String,
#[cultcache(key = 12)]
pub contract: String,
}
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
#[cultcache(type = "epiphany.hands.commit_receipt", schema = "HandsCommitReceipt")]
pub struct HandsCommitReceipt {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub receipt_id: String,
#[cultcache(key = 2)]
#[cultcache(key = 1)]
pub intent_id: String,
#[cultcache(key = 3)]
pub review_id: String,
#[cultcache(key = 4)]
pub runtime_job_id: String,
#[cultcache(key = 5)]
#[cultcache(key = 2)]
pub commit_sha: String,
#[cultcache(key = 6)]
#[cultcache(key = 3)]
pub branch: String,
#[cultcache(key = 7)]
#[cultcache(key = 4)]
pub changed_paths: Vec<String>,
#[cultcache(key = 8)]
#[cultcache(key = 5)]
pub summary: String,
#[cultcache(key = 9)]
#[cultcache(key = 6)]
pub emitted_at: String,
#[cultcache(key = 10)]
pub contract: String,
}
pub fn hands_action_review_for_intent(
review_id: String,
intent: &HandsActionIntent,
decision: String,
allowed_operations: Vec<String>,
reasons: Vec<String>,
reviewed_at: String,
) -> HandsActionReview {
HandsActionReview {
schema_version: HANDS_ACTION_REVIEW_SCHEMA_VERSION.to_string(),
review_id,
intent_id: intent.intent_id.clone(),
decision,
allowed_operations,
required_receipts: vec![HANDS_PATCH_RECEIPT_TYPE.to_string()],
reasons,
reviewed_at,
contract: "Hands review is the execution decision for a bounded action intent; it depends on Substrate Gate access and does not admit durable Mind state.".to_string(),
}
}
pub fn hands_patch_receipt_for_review(
receipt_id: String,
intent: &HandsActionIntent,
review: &HandsActionReview,
changed_paths: Vec<String>,
summary: String,
emitted_at: String,
) -> HandsPatchReceipt {
HandsPatchReceipt {
schema_version: HANDS_PATCH_RECEIPT_SCHEMA_VERSION.to_string(),
receipt_id,
intent_id: intent.intent_id.clone(),
review_id: review.review_id.clone(),
substrate_gate_grant_receipt_id: intent.substrate_gate_grant_receipt_id.clone(),
runtime_job_id: intent.runtime_job_id.clone(),
changed_paths,
summary,
emitted_at,
contract: "Hands patch receipt proves which files changed under the reviewed action and named Substrate Gate grant; Soul and Mind still decide verification and durable admission.".to_string(),
}
}
#[allow(clippy::too_many_arguments)]
pub fn hands_command_receipt_for_review(
receipt_id: String,
intent: &HandsActionIntent,
review: &HandsActionReview,
command: String,
exit_code: String,
stdout_artifact: String,
stderr_artifact: String,
summary: String,
emitted_at: String,
) -> HandsCommandReceipt {
HandsCommandReceipt {
schema_version: HANDS_COMMAND_RECEIPT_SCHEMA_VERSION.to_string(),
receipt_id,
intent_id: intent.intent_id.clone(),
review_id: review.review_id.clone(),
substrate_gate_grant_receipt_id: intent.substrate_gate_grant_receipt_id.clone(),
runtime_job_id: intent.runtime_job_id.clone(),
command,
exit_code,
stdout_artifact,
stderr_artifact,
summary,
emitted_at,
contract: "Hands command receipt proves which command ran, where output evidence lives, and which reviewed action plus Substrate Gate grant authorized it.".to_string(),
}
}
pub fn hands_commit_receipt_for_review(
receipt_id: String,
intent: &HandsActionIntent,
review: &HandsActionReview,
commit_sha: String,
branch: String,
changed_paths: Vec<String>,
summary: String,
emitted_at: String,
) -> HandsCommitReceipt {
HandsCommitReceipt {
schema_version: HANDS_COMMIT_RECEIPT_SCHEMA_VERSION.to_string(),
receipt_id,
intent_id: intent.intent_id.clone(),
review_id: review.review_id.clone(),
runtime_job_id: intent.runtime_job_id.clone(),
commit_sha,
branch,
changed_paths,
summary,
emitted_at,
contract: "Hands commit receipt proves a repository commit consequence after a reviewed action; it is still subject to Soul verification and Mind admission.".to_string(),
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,518 +0,0 @@
use anyhow::{Context, Result, anyhow, bail};
use cultnet_rs::{
CultNetMessage, CultNetRawDocumentRecord, CultNetRawPayloadEncoding,
CultNetRudpSocketTransportConnection, CultNetRudpSocketTransportOptions, CultNetWireContract,
GameCultProviderHealthIdentity, IDUNN_SIGNED_DAEMON_HEALTH_SCHEMA,
IdunnSignedDaemonHealthPurpose, IdunnSignedDaemonHealthRecord, ServiceIdentitySigner,
encode_cultnet_message_to_vec,
};
use serde::{Deserialize, Serialize};
use std::net::{SocketAddr, UdpSocket};
use std::time::{Duration, Instant};
pub const EPIPHANY_IDUNN_RUNTIME_HEALTH_CONTRACT: &str = "epiphany.cultnet-rudp-runtime-health";
pub const CULTNET_RUDP_PROTOCOL_ID: &str = "cultnet.transport.rudp.v0";
const IDUNN_HEALTH_RUDP_CONNECTION_ID: u32 = 0x1d0d_0001;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct IdunnDaemonHealthDocument {
pub daemon_id: String,
pub state: String,
pub detail: String,
pub observed_at: String,
pub health_contract: String,
pub publication_source: String,
pub transport: String,
}
pub fn sign_epiphany_runtime_health(
health: IdunnDaemonHealthDocument,
source_runtime_id: &str,
release_id: &str,
release_witness_sha256: &str,
source_commit: &str,
deployment_request_id: &str,
publisher_incarnation_id: &str,
publisher_sequence: u64,
signer: &ServiceIdentitySigner<GameCultProviderHealthIdentity>,
) -> Result<IdunnSignedDaemonHealthRecord> {
validate_health_document(&health)?;
let observed_at_unix_millis = chrono::DateTime::parse_from_rfc3339(&health.observed_at)?
.timestamp_millis()
.try_into()
.context("runtime health observation precedes Unix epoch")?;
let mut record = IdunnSignedDaemonHealthRecord {
schema_version: "idunn.signed_daemon_health.v1".into(),
daemon_id: health.daemon_id,
health_contract: health.health_contract,
source_runtime_id: source_runtime_id.into(),
state: health.state,
detail: health.detail,
signer_identity_id: signer.entry().identity_id.clone(),
publisher_incarnation_id: publisher_incarnation_id.into(),
publisher_sequence,
observed_at_unix_millis,
release_id: Some(release_id.into()),
release_witness_sha256: Some(release_witness_sha256.into()),
source_commit: Some(source_commit.into()),
deployment_id: Some(deployment_request_id.into()),
signature_algorithm: "ed25519".into(),
signature: Vec::new(),
private_state_exposed: false,
};
validate_unsigned_record(&record)?;
let canonical = canonical_unsigned_record(&record)?;
let proof = signer.sign::<IdunnSignedDaemonHealthPurpose>(&canonical);
if proof.identity_id != record.signer_identity_id {
bail!("provider-health signer identity disagrees with record")
}
record.signature = proof.signature;
record.validate()?;
Ok(record)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EpiphanyAggregateRuntimeHealthInput {
pub daemon_id: String,
pub health_contract: String,
pub observed_at: String,
pub release_authenticated: bool,
pub expected_service_count: usize,
pub terminal_current_service_count: usize,
pub warming_service_count: usize,
pub contradictions: Vec<String>,
}
pub fn derive_epiphany_aggregate_runtime_health(
input: EpiphanyAggregateRuntimeHealthInput,
) -> Result<IdunnDaemonHealthDocument> {
require_id(&input.daemon_id, "Idunn daemon id")?;
require_id(&input.health_contract, "Idunn health contract")?;
require_id(&input.observed_at, "Idunn health observation time")?;
if input.expected_service_count == 0 {
bail!("aggregate runtime health requires at least one managed service");
}
if input.terminal_current_service_count > input.expected_service_count
|| input.warming_service_count > input.expected_service_count
|| input
.terminal_current_service_count
.saturating_add(input.warming_service_count)
> input.expected_service_count
{
bail!("managed service health counts exceed the expected service count");
}
let (state, detail) = if !input.contradictions.is_empty() {
("failed", "runtime-contradiction")
} else if !input.release_authenticated {
("failed", "release-authentication-failed")
} else if input.terminal_current_service_count == input.expected_service_count {
("active", "managed-services-current")
} else if input.warming_service_count > 0
&& input
.terminal_current_service_count
.saturating_add(input.warming_service_count)
== input.expected_service_count
{
("warming", "managed-services-warming")
} else {
("degraded", "managed-services-reconciling")
};
Ok(IdunnDaemonHealthDocument {
daemon_id: input.daemon_id,
state: state.into(),
detail: detail.into(),
observed_at: input.observed_at,
health_contract: input.health_contract,
publication_source: "daemon-published".into(),
transport: CULTNET_RUDP_PROTOCOL_ID.into(),
})
}
pub fn publish_idunn_daemon_health_rudp(
endpoint: SocketAddr,
source_runtime_id: &str,
signed: &IdunnSignedDaemonHealthRecord,
) -> Result<()> {
signed.validate()?;
require_id(source_runtime_id, "health publisher runtime id")?;
if source_runtime_id != signed.source_runtime_id {
bail!("health transport source disagrees with signed source runtime");
}
let payload = rmp_serde::to_vec(signed).context("encoding canonical signed Idunn health")?;
let decoded: IdunnSignedDaemonHealthRecord = rmp_serde::from_slice(&payload)?;
if decoded != *signed || rmp_serde::to_vec(&decoded)? != payload {
bail!("signed Idunn health encoding is noncanonical");
}
let message = CultNetMessage::DocumentPutRaw {
message_id: format!(
"epiphany-signed-health:{}:{}:{}",
signed.daemon_id, signed.publisher_incarnation_id, signed.publisher_sequence
),
document: CultNetRawDocumentRecord {
schema_id: IDUNN_SIGNED_DAEMON_HEALTH_SCHEMA.into(),
record_key: signed.daemon_id.clone(),
stored_at: chrono::DateTime::from_timestamp_millis(
signed.observed_at_unix_millis.try_into()?,
)
.context("signed health observation time is invalid")?
.to_rfc3339(),
payload_encoding: CultNetRawPayloadEncoding::Messagepack,
payload,
source_runtime_id: Some(source_runtime_id.into()),
source_agent_id: Some(signed.signer_identity_id.clone()),
source_role: Some("daemon-health-publisher".into()),
tags: Some(vec![CULTNET_RUDP_PROTOCOL_ID.into()]),
},
};
let bind = if endpoint.is_ipv4() {
"0.0.0.0:0"
} else {
"[::]:0"
};
let socket = UdpSocket::bind(bind)
.with_context(|| format!("binding Epiphany Idunn RUDP sender at {bind}"))?;
socket.set_read_timeout(Some(Duration::from_millis(100)))?;
let mut transport =
CultNetRudpSocketTransportConnection::new(CultNetRudpSocketTransportOptions::client(
source_runtime_id,
socket,
endpoint,
IDUNN_HEALTH_RUDP_CONNECTION_ID,
))?;
transport.connect(Vec::new())?;
let deadline = Instant::now() + Duration::from_millis(500);
while !transport.connected() {
let _ = transport.receive_once()?;
transport.poll_resends()?;
if Instant::now() >= deadline {
return Err(anyhow!(
"timed out connecting Epiphany health publisher to {endpoint}"
));
}
}
transport.send(
"schema",
encode_cultnet_message_to_vec(&message, CultNetWireContract::CultNetSchemaV0)?,
)?;
Ok(())
}
fn canonical_unsigned_record(record: &IdunnSignedDaemonHealthRecord) -> Result<Vec<u8>> {
let mut unsigned = record.clone();
unsigned.signature.clear();
validate_unsigned_record(&unsigned)?;
rmp_serde::to_vec(&unsigned).context("encoding canonical unsigned Idunn health")
}
fn validate_unsigned_record(record: &IdunnSignedDaemonHealthRecord) -> Result<()> {
if !record.signature.is_empty() {
bail!("unsigned signing record already contains a signature");
}
let mut signed_shape = record.clone();
signed_shape.signature = vec![0; 64];
signed_shape.validate()?;
uuid::Uuid::parse_str(&record.publisher_incarnation_id)
.context("publisher incarnation id must be UUID")?;
Ok(())
}
fn validate_health_document(health: &IdunnDaemonHealthDocument) -> Result<()> {
require_id(&health.daemon_id, "Idunn daemon id")?;
require_id(&health.health_contract, "Idunn health contract")?;
require_id(&health.observed_at, "Idunn health observation time")?;
if !matches!(
health.state.as_str(),
"active" | "warming" | "degraded" | "failed"
) {
bail!("unsupported Idunn daemon health state");
}
if !matches!(
health.detail.as_str(),
"runtime-contradiction"
| "release-authentication-failed"
| "managed-services-current"
| "managed-services-warming"
| "managed-services-reconciling"
) {
bail!("Idunn daemon health detail is not a bounded generic reason");
}
if health.publication_source != "daemon-published"
|| health.transport != CULTNET_RUDP_PROTOCOL_ID
{
bail!("Idunn daemon health publication authority is invalid");
}
chrono::DateTime::parse_from_rfc3339(&health.observed_at)?;
Ok(())
}
fn require_id(value: &str, label: &str) -> Result<()> {
if value.trim().is_empty() || value.len() > 256 || value.chars().any(char::is_control) {
bail!("{label} is empty, oversized, or contains control characters");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use cultnet_rs::{
ServiceIdentityProfile, ServiceIdentitySignature, ServiceSignaturePurpose,
decode_cultnet_message_from_slice, enroll_service_identity_at,
verify_service_identity_signature,
};
use std::thread;
struct WrongPurpose;
impl ServiceSignaturePurpose<GameCultProviderHealthIdentity> for WrongPurpose {
const PURPOSE: &'static [u8] = b"wrong-purpose";
}
fn input(current: usize) -> EpiphanyAggregateRuntimeHealthInput {
EpiphanyAggregateRuntimeHealthInput {
daemon_id: "yggdrasil-epiphany".into(),
health_contract: EPIPHANY_IDUNN_RUNTIME_HEALTH_CONTRACT.into(),
observed_at: "2026-07-16T00:00:00Z".into(),
release_authenticated: true,
expected_service_count: 2,
terminal_current_service_count: current,
warming_service_count: 0,
contradictions: Vec::new(),
}
}
fn signed_health() -> (
tempfile::TempDir,
ServiceIdentitySigner<GameCultProviderHealthIdentity>,
IdunnSignedDaemonHealthRecord,
) {
let root = tempfile::tempdir().unwrap();
let signer = enroll_service_identity_at::<GameCultProviderHealthIdentity>(
&root.path().join("provider.cc"),
)
.unwrap();
let record = sign_epiphany_runtime_health(
derive_epiphany_aggregate_runtime_health(input(2)).unwrap(),
"epiphany-yggdrasil",
"release-test",
&format!("sha256-{}", "a".repeat(64)),
&"b".repeat(40),
"deploy-request-test",
"00000000-0000-4000-8000-000000000001",
1,
&signer,
)
.unwrap();
(root, signer, record)
}
#[test]
fn aggregate_health_uses_closed_nonprivate_reasons() {
assert_eq!(
derive_epiphany_aggregate_runtime_health(input(2))
.unwrap()
.detail,
"managed-services-current"
);
let mut warming = input(1);
warming.warming_service_count = 1;
assert_eq!(
derive_epiphany_aggregate_runtime_health(warming)
.unwrap()
.detail,
"managed-services-warming"
);
let mut failed = input(2);
failed
.contradictions
.push("secret path and process detail".into());
assert_eq!(
derive_epiphany_aggregate_runtime_health(failed)
.unwrap()
.detail,
"runtime-contradiction"
);
}
#[test]
fn canonical_record_binds_exact_release_and_process_continuity() {
let (_root, signer, record) = signed_health();
assert_eq!(record.source_runtime_id, "epiphany-yggdrasil");
assert_eq!(record.release_id.as_deref(), Some("release-test"));
assert_eq!(record.deployment_id.as_deref(), Some("deploy-request-test"));
assert_eq!(record.publisher_sequence, 1);
assert!(!record.private_state_exposed);
let statement = canonical_unsigned_record(&record).unwrap();
let proof = ServiceIdentitySignature {
identity_id: record.signer_identity_id.clone(),
signature: record.signature.clone(),
};
verify_service_identity_signature::<
GameCultProviderHealthIdentity,
IdunnSignedDaemonHealthPurpose,
>(&signer.trust_anchor().unwrap(), &statement, &proof)
.unwrap();
assert_eq!(&statement[..3], &[0xdc, 0, 17]);
}
#[test]
fn wrong_purpose_key_mutation_and_noncanonical_encoding_fail() {
let (_root, signer, record) = signed_health();
let statement = canonical_unsigned_record(&record).unwrap();
let wrong = signer.sign::<WrongPurpose>(&statement);
assert!(
verify_service_identity_signature::<
GameCultProviderHealthIdentity,
IdunnSignedDaemonHealthPurpose,
>(&signer.trust_anchor().unwrap(), &statement, &wrong)
.is_err()
);
let other_root = tempfile::tempdir().unwrap();
let other = enroll_service_identity_at::<GameCultProviderHealthIdentity>(
&other_root.path().join("other.cc"),
)
.unwrap();
let proof = ServiceIdentitySignature {
identity_id: record.signer_identity_id.clone(),
signature: record.signature.clone(),
};
assert!(
verify_service_identity_signature::<
GameCultProviderHealthIdentity,
IdunnSignedDaemonHealthPurpose,
>(&other.trust_anchor().unwrap(), &statement, &proof)
.is_err()
);
let mut mutated = record.clone();
mutated.state = "failed".into();
assert!(
verify_service_identity_signature::<
GameCultProviderHealthIdentity,
IdunnSignedDaemonHealthPurpose,
>(
&signer.trust_anchor().unwrap(),
&canonical_unsigned_record(&mutated).unwrap(),
&proof
)
.is_err()
);
let named = rmp_serde::to_vec(&serde_json::json!({
"schema_version": record.schema_version,
"daemon_id": record.daemon_id,
"health_contract": record.health_contract,
"source_runtime_id": record.source_runtime_id,
"state": record.state,
"detail": record.detail,
"signer_identity_id": record.signer_identity_id,
"publisher_incarnation_id": record.publisher_incarnation_id,
"publisher_sequence": record.publisher_sequence,
"observed_at_unix_millis": record.observed_at_unix_millis,
"release_id": record.release_id,
"release_witness_sha256": record.release_witness_sha256,
"source_commit": record.source_commit,
"deployment_id": record.deployment_id,
"signature_algorithm": record.signature_algorithm,
"signature": [],
"private_state_exposed": record.private_state_exposed,
}))
.unwrap();
assert!(
verify_service_identity_signature::<
GameCultProviderHealthIdentity,
IdunnSignedDaemonHealthPurpose,
>(&signer.trust_anchor().unwrap(), &named, &proof)
.is_err()
);
}
#[test]
fn sequence_and_incarnation_are_strict() {
let (_root, signer, record) = signed_health();
let health = derive_epiphany_aggregate_runtime_health(input(2)).unwrap();
assert!(
sign_epiphany_runtime_health(
health.clone(),
"epiphany-yggdrasil",
"r",
&format!("sha256-{}", "a".repeat(64)),
&"b".repeat(40),
"d",
"bad",
2,
&signer
)
.is_err()
);
assert!(
sign_epiphany_runtime_health(
health,
"epiphany-yggdrasil",
"r",
&format!("sha256-{}", "a".repeat(64)),
&"b".repeat(40),
"d",
&record.publisher_incarnation_id,
0,
&signer
)
.is_err()
);
}
#[test]
fn rudp_publisher_emits_exact_canonical_tuple() {
let socket = UdpSocket::bind("127.0.0.1:0").unwrap();
socket
.set_read_timeout(Some(Duration::from_millis(100)))
.unwrap();
let endpoint = socket.local_addr().unwrap();
let server = thread::spawn(move || {
let mut transport = CultNetRudpSocketTransportConnection::new(
CultNetRudpSocketTransportOptions::server(
"idunn-test",
socket,
IDUNN_HEALTH_RUDP_CONNECTION_ID,
),
)
.unwrap();
let deadline = Instant::now() + Duration::from_secs(2);
loop {
if let Some(frame) = transport.receive_once().unwrap() {
return decode_cultnet_message_from_slice(
&frame.payload,
CultNetWireContract::CultNetSchemaV0,
)
.unwrap();
}
transport.poll_resends().unwrap();
assert!(Instant::now() < deadline);
}
});
let (_root, _signer, record) = signed_health();
publish_idunn_daemon_health_rudp(endpoint, "epiphany-yggdrasil", &record).unwrap();
let CultNetMessage::DocumentPutRaw { document, .. } = server.join().unwrap() else {
panic!("wrong message")
};
assert_eq!(document.schema_id, IDUNN_SIGNED_DAEMON_HEALTH_SCHEMA);
assert_eq!(document.schema_id, record.schema_version);
assert_eq!(
document.source_role.as_deref(),
Some("daemon-health-publisher")
);
let decoded: IdunnSignedDaemonHealthRecord =
rmp_serde::from_slice(&document.payload).unwrap();
assert_eq!(decoded, record);
assert_eq!(rmp_serde::to_vec(&decoded).unwrap(), document.payload);
}
#[test]
fn identity_profile_is_not_host_identity() {
assert_eq!(
GameCultProviderHealthIdentity::TRUST_ANCHOR_SCHEMA,
"gamecult.provider_health_identity.trust_anchor.v1"
);
assert_ne!(
GameCultProviderHealthIdentity::ID_DOMAIN,
b"epiphany.host-incarnation.identity.v0\0"
);
}
}

View file

@ -8,9 +8,6 @@ pub const REQUEST_SCHEMA: &str = "epiphany.self.imagination_consideration_reques
pub const REQUEST_CONTRACT: &str = "epiphany.imagination_consideration_request.v1";
pub const CANDIDATE_SCHEMA: &str = "epiphany.imagination.consideration_candidate.v1";
pub const CANDIDATE_CONTRACT: &str = "epiphany.imagination_consideration_candidate.v1";
pub const LAUNCH_BINDING_SCHEMA: &str =
"epiphany.coordinator.imagination_consideration_launch_binding.v0";
pub const REVIEW_REQUEST_SCHEMA: &str = "epiphany.self.imagination_consideration_review_request.v0";
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
@ -149,52 +146,6 @@ pub struct ImaginationConsiderationCandidate {
pub data_classification: String,
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "epiphany.coordinator.imagination_consideration_launch_binding",
schema = "ImaginationConsiderationLaunchBinding"
)]
pub struct ImaginationConsiderationLaunchBinding {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub binding_record_id: String,
#[cultcache(key = 2)]
pub request_id: String,
#[cultcache(key = 3)]
pub job_id: String,
#[cultcache(key = 4)]
pub binding_id: String,
#[cultcache(key = 5)]
pub runtime_id: String,
#[cultcache(key = 6)]
pub thread_id: String,
#[cultcache(key = 7)]
pub launched_at: String,
#[cultcache(key = 8)]
pub worker_launch_document_sha256: String,
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "epiphany.self.imagination_consideration_review_request",
schema = "ImaginationConsiderationReviewRequest"
)]
pub struct ImaginationConsiderationReviewRequest {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub review_request_id: String,
#[cultcache(key = 2)]
pub candidate_id: String,
#[cultcache(key = 3)]
pub candidate_sha256: String,
#[cultcache(key = 4)]
pub requested_review_route: String,
#[cultcache(key = 5)]
pub selected_at: String,
}
pub fn commit_request(
runtime_store: &Path,
persona_feedback_store: &Path,
@ -407,52 +358,6 @@ pub fn render_consideration_prompt(request: &ImaginationConsiderationRequest) ->
))
}
pub fn request_candidate_modeling_review(
runtime_store: &Path,
request: &ImaginationConsiderationRequest,
candidate: &ImaginationConsiderationCandidate,
selected_at: &str,
) -> Result<ImaginationConsiderationReviewRequest> {
chrono::DateTime::parse_from_rfc3339(selected_at)
.map_err(|_| anyhow!("consideration selection timestamp must be RFC3339"))?;
let mut cache = crate::runtime_spine_cache(runtime_store)?;
cache.pull_all_backing_stores()?;
validate_current_request(&cache, request)?;
validate_candidate(request, candidate)?;
if candidate.disposition != ImaginationConsiderationDisposition::Suggest
|| candidate.recommended_review_route != ImaginationConsiderationReviewRoute::ModelingReview
{
bail!("only an explicit suggest/modeling_review candidate may be selected");
}
let candidate_sha256 = format!("sha256-{:x}", Sha256::digest(rmp_serde::to_vec(candidate)?));
let review_request_id = format!(
"imagination-consideration-review-{:x}",
Sha256::digest(format!("{}:{candidate_sha256}", request.request_id).as_bytes())
);
let review = ImaginationConsiderationReviewRequest {
schema_version: REVIEW_REQUEST_SCHEMA.into(),
review_request_id: review_request_id.clone(),
candidate_id: candidate.candidate_id.clone(),
candidate_sha256,
requested_review_route: "modeling_review_proposal_only".into(),
selected_at: selected_at.into(),
};
if let Some(existing) =
cache.get::<ImaginationConsiderationReviewRequest>(&review_request_id)?
{
let mut replay = review;
replay.selected_at = existing.selected_at.clone();
return if replay == existing {
Ok(existing)
} else {
bail!("consideration review request collision")
};
}
let (entry, _) = cache.prepare_entry(&review_request_id, &review)?;
SingleFileMessagePackBackingStore::new(runtime_store).push(&entry)?;
Ok(review)
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -1,16 +1,12 @@
mod agent_launch;
pub mod atlas;
mod causal_work_identity;
mod continuity_gateway;
mod coordinator_objective_intake;
mod coordinator_results;
pub mod coordinator_status;
mod cultmesh_integration;
mod current_work;
mod eyes_gateway;
mod hands_gateway;
mod idunn_provider_health;
mod idunn_runtime_health;
mod imagination_consideration;
mod memory_graph;
mod mind_documents;
@ -35,6 +31,7 @@ mod runtime_store_backend;
mod runtime_worker_attempt;
mod soul_gateway;
mod state_ledger;
mod state_model;
mod substrate_gate;
mod surfaces;
@ -46,7 +43,6 @@ pub use admitted_model_direction_consideration::{
RESULT_CONTRACT as ADMITTED_MODEL_DIRECTION_CONSIDERATION_RESULT_CONTRACT,
RESULT_SCHEMA as ADMITTED_MODEL_DIRECTION_CONSIDERATION_RESULT_SCHEMA_VERSION,
commit_request as commit_admitted_model_direction_consideration_request,
render_prompt as render_admitted_model_direction_consideration_prompt,
result_id_for_launch as admitted_model_direction_consideration_result_id_for_launch,
validate_current_request as validate_current_admitted_model_direction_consideration_request,
validate_request as validate_admitted_model_direction_consideration_request,
@ -71,22 +67,16 @@ pub use agent_launch::epiphany_frontier_verdict_modeling_output_schema;
pub use agent_launch::epiphany_imagination_consideration_output_schema;
pub use agent_launch::epiphany_proposal_modeling_output_schema;
pub use agent_launch::epiphany_reorient_launch_output_schema;
pub use agent_launch::epiphany_role_binding_id;
pub use agent_launch::epiphany_role_label;
pub use agent_launch::epiphany_role_launch_output_schema;
pub use agent_launch::epiphany_role_owner;
pub use agent_launch::unique_strings;
pub use atlas::*;
pub use causal_work_identity::*;
pub use continuity_gateway::*;
pub use coordinator_objective_intake::USER_OBJECTIVE_INTAKE_CONTRACT;
pub use coordinator_objective_intake::USER_OBJECTIVE_INTAKE_SCHEMA_VERSION;
pub use coordinator_objective_intake::USER_OBJECTIVE_INTAKE_TYPE;
pub(crate) use causal_work_identity::*;
pub use coordinator_objective_intake::UserObjectiveIntake;
pub use coordinator_objective_intake::UserObjectiveIntakeApplied;
pub use coordinator_objective_intake::UserObjectiveIntakeInput;
pub use coordinator_objective_intake::intake_user_objective;
pub use coordinator_results::EpiphanyCoordinatorReorientResultSnapshot;
pub use coordinator_results::EpiphanyReorientResultStatus;
pub use coordinator_results::read_runtime_reorient_result;
pub use cultcache_rs::CacheBackingStore;
pub use cultcache_rs::CultCache;
@ -100,64 +90,29 @@ pub use cultmesh_integration::EPIPHANY_CANONICAL_SWARM_BRAKE_ID;
pub use cultmesh_integration::EPIPHANY_CANONICAL_SWARM_BRAKE_OWNER;
pub use cultmesh_integration::{
EPIPHANY_CULTMESH_SWARM_BRAKE_KEY, EPIPHANY_CULTMESH_SWARM_BRAKE_SCHEMA_VERSION,
EPIPHANY_CULTMESH_SWARM_BRAKE_TYPE, EpiphanyCultMeshDocuments,
EpiphanyCultMeshSwarmBrakeEntry, canonical_epiphany_swarm_brake_protected_surfaces,
EPIPHANY_CULTMESH_SWARM_BRAKE_TYPE, EpiphanyCultMeshDocuments, EpiphanyCultMeshSwarmBrakeEntry,
default_epiphany_cultmesh_swarm_brake, engage_epiphany_cultmesh_swarm_brake,
load_epiphany_cultmesh_swarm_brake, open_epiphany_cultmesh_node,
release_epiphany_cultmesh_swarm_brake, write_epiphany_cultmesh_swarm_brake,
};
pub use current_work::*;
pub use epiphany_state_model::EpiphanyMemoryAnchor;
pub use epiphany_state_model::EpiphanyMemoryDomain;
pub use epiphany_state_model::EpiphanyMemoryEdge;
pub use epiphany_state_model::EpiphanyMemoryLifecycle;
pub use epiphany_state_model::EpiphanyMemoryNode;
pub use eyes_gateway::EYES_EVIDENCE_PACKET_SCHEMA_VERSION;
pub use eyes_gateway::EYES_EVIDENCE_PACKET_TYPE;
pub use eyes_gateway::EYES_SOURCE_LOOKUP_RECEIPT_SCHEMA_VERSION;
pub use eyes_gateway::EYES_SOURCE_LOOKUP_RECEIPT_TYPE;
pub use eyes_gateway::EyesEvidencePacket;
pub use eyes_gateway::EyesSourceLookupReceipt;
pub use eyes_gateway::eyes_evidence_packet_from_research_finding;
pub use hands_gateway::*;
pub use idunn_provider_health::{
EPIPHANY_IDUNN_PROVIDER_HEALTH_ADMISSION_SCHEMA, EPIPHANY_IDUNN_PROVIDER_HEALTH_ADMISSION_TYPE,
IdunnProviderHealthAdmission, ProviderReleaseBinding, RequiredProviderHealth,
admit_required_idunn_provider_health, provider_health_record_key,
read_idunn_provider_health_trust_anchor, required_idunn_provider_health_query,
verify_idunn_provider_health_candidate,
};
pub use idunn_runtime_health::{
CULTNET_RUDP_PROTOCOL_ID, EPIPHANY_IDUNN_RUNTIME_HEALTH_CONTRACT,
EpiphanyAggregateRuntimeHealthInput, IdunnDaemonHealthDocument,
derive_epiphany_aggregate_runtime_health, publish_idunn_daemon_health_rudp,
sign_epiphany_runtime_health,
};
pub use imagination_consideration::{
CANDIDATE_CONTRACT as IMAGINATION_CONSIDERATION_CANDIDATE_CONTRACT,
CANDIDATE_SCHEMA as IMAGINATION_CONSIDERATION_CANDIDATE_SCHEMA_VERSION,
ImaginationConsiderationCandidate, ImaginationConsiderationDisposition,
ImaginationConsiderationLaunchBinding, ImaginationConsiderationQuestion,
ImaginationConsiderationRequest, ImaginationConsiderationReviewRequest,
ImaginationConsiderationReviewRoute, ImaginationOptionDraft,
LAUNCH_BINDING_SCHEMA as IMAGINATION_CONSIDERATION_LAUNCH_BINDING_SCHEMA_VERSION,
QuotedPersonaFeedbackEvidence, REQUEST_CONTRACT as IMAGINATION_CONSIDERATION_REQUEST_CONTRACT,
ImaginationConsiderationQuestion, ImaginationConsiderationRequest,
ImaginationConsiderationReviewRoute, ImaginationOptionDraft, QuotedPersonaFeedbackEvidence,
REQUEST_CONTRACT as IMAGINATION_CONSIDERATION_REQUEST_CONTRACT,
REQUEST_SCHEMA as IMAGINATION_CONSIDERATION_REQUEST_SCHEMA_VERSION,
candidate_id_for_launch as imagination_consideration_candidate_id_for_launch,
commit_request as commit_imagination_consideration_request,
render_consideration_prompt as render_imagination_consideration_prompt,
request_candidate_modeling_review as request_imagination_consideration_modeling_review,
validate_candidate as validate_imagination_consideration_candidate,
validate_current_request as validate_current_imagination_consideration_request,
};
pub use memory_graph::EpiphanyMemoryEdgeKind;
pub use memory_graph::EpiphanyMemoryGraphSnapshot;
pub use memory_graph::EpiphanyMemoryGraphValidationError;
pub use memory_graph::EpiphanyMemoryNodeKind;
pub use memory_graph::RepoFrontierAdoptedPlan;
pub use memory_graph::RepoFrontierItem;
pub use memory_graph::RepoFrontierStatus;
pub use memory_graph::validate_memory_graph_snapshot;
pub use mind_documents::*;
pub use packaged_release::{
@ -166,23 +121,18 @@ pub use packaged_release::{
EpiphanyPackagedReleaseEntry, EpiphanyPackagedReleaseHead, PackageReleaseRequest,
authenticate_epiphany_packaged_release, epiphany_packaged_release_binary_path,
epiphany_packaged_release_witness_sha256, inspect_epiphany_packaged_release_witness,
load_epiphany_packaged_release, load_epiphany_packaged_release_head, package_epiphany_release,
publish_epiphany_packaged_release, read_epiphany_packaged_release_witness,
required_packaged_release_binaries, validate_epiphany_packaged_release,
verify_epiphany_packaged_release_files, write_epiphany_packaged_release_witness,
package_epiphany_release, publish_epiphany_packaged_release,
};
pub use persona_conversation::*;
pub use persona_discord_crossing::*;
pub use persona_discord_permit::*;
pub use persona_feedback_admission::{
BIFROST_PERSONA_FEEDBACK_ADMISSION_SCHEMA_VERSION, BIFROST_PERSONA_FEEDBACK_DELIVERY_TYPE,
BifrostPersonaFeedbackAdmission,
LOCAL_PERSONA_FEEDBACK_SCHEMA_VERSION, LocalAdmittedPersonaFeedback,
PERSONA_FEEDBACK_SOCIAL_ADMISSION_SCHEMA_VERSION, PersonaFeedbackPacket,
PersonaFeedbackSocialAdmissionReceipt, admit_persona_feedback_to_social,
BifrostPersonaFeedbackAdmission, LOCAL_PERSONA_FEEDBACK_SCHEMA_VERSION,
LocalAdmittedPersonaFeedback, PERSONA_FEEDBACK_SOCIAL_ADMISSION_SCHEMA_VERSION,
PersonaFeedbackPacket, PersonaFeedbackSocialAdmissionReceipt, admit_persona_feedback_to_social,
admitted_persona_feedback, import_bifrost_persona_feedback_deliveries,
persona_feedback_ready_for_cognition,
validate_persona_feedback_store_separation,
persona_feedback_ready_for_cognition, validate_persona_feedback_store_separation,
};
pub use persona_social_state::*;
pub use persona_turn::PERSONA_INTERPRETER_EFFECT_DOCUMENT_SCHEMA_VERSION;
@ -225,20 +175,16 @@ pub use repo_model_gateway::*;
pub use repository_body_observer::*;
pub use resident_readiness::*;
pub use resident_self::*;
pub use runtime_spine::ARCHIVED_RUNTIME_WORKER_ATTEMPT_SCHEMA_VERSION;
pub use runtime_spine::ARCHIVED_RUNTIME_WORKER_ATTEMPT_TYPE;
pub use runtime_spine::COORDINATOR_DEATH_RECOVERY_SCHEMA_VERSION;
pub use runtime_spine::COORDINATOR_RUN_RECEIPT_SCHEMA_VERSION;
pub use runtime_spine::COORDINATOR_RUN_RECEIPT_TYPE;
pub use runtime_spine::EPIPHANY_RUNTIME_ROOT_SESSION_ID;
pub use runtime_spine::EpiphanyArchivedRuntimeWorkerAttempt;
pub use runtime_spine::EpiphanyArchivedRuntimeWorkerDecision;
pub use runtime_spine::EpiphanyCoordinatorDeathRecovery;
pub use runtime_spine::EpiphanyCoordinatorRunBasis;
pub use runtime_spine::EpiphanyCoordinatorRunReceipt;
pub use runtime_spine::EpiphanyCoordinatorRunTerminality;
pub use runtime_spine::EpiphanyRuntimeIdentity;
pub use runtime_spine::EpiphanyRuntimeJob;
pub use runtime_spine::EpiphanyRuntimeJobResult;
pub use runtime_spine::EpiphanyRuntimeJobSnapshot;
pub use runtime_spine::EpiphanyRuntimeJobStatus;
pub use runtime_spine::EpiphanyRuntimeModelExecutionBinding;
pub use runtime_spine::EpiphanyRuntimeReorientWorkerResult;
@ -252,24 +198,9 @@ pub use runtime_spine::EpiphanyRuntimeWorkerProcessClaim;
pub use runtime_spine::ModelPassFailureTerminalOptions;
pub use runtime_spine::RUNTIME_IDENTITY_KEY;
pub use runtime_spine::RUNTIME_IDENTITY_TYPE;
pub use runtime_spine::RUNTIME_JOB_RESULT_TYPE;
pub use runtime_spine::RUNTIME_JOB_TYPE;
pub use runtime_spine::RUNTIME_MODEL_EXECUTION_BINDING_SCHEMA_VERSION;
pub use runtime_spine::RUNTIME_MODEL_EXECUTION_BINDING_TYPE;
pub use runtime_spine::RUNTIME_REORIENT_WORKER_RESULT_SCHEMA_VERSION;
pub use runtime_spine::RUNTIME_REORIENT_WORKER_RESULT_TYPE;
pub use runtime_spine::RUNTIME_ROLE_WORKER_RESULT_SCHEMA_VERSION;
pub use runtime_spine::RUNTIME_ROLE_WORKER_RESULT_TYPE;
pub use runtime_spine::RUNTIME_SESSION_TYPE;
pub use runtime_spine::RUNTIME_SPINE_SCHEMA_VERSION;
pub use runtime_spine::RUNTIME_SWARM_BINDING_KEY;
pub use runtime_spine::RUNTIME_SWARM_BINDING_SCHEMA_VERSION;
pub use runtime_spine::RUNTIME_TOOL_EXECUTION_BINDING_SCHEMA_VERSION;
pub use runtime_spine::RUNTIME_TOOL_EXECUTION_BINDING_TYPE;
pub use runtime_spine::RUNTIME_WORKER_LAUNCH_REQUEST_SCHEMA_VERSION;
pub use runtime_spine::RUNTIME_WORKER_LAUNCH_REQUEST_TYPE;
pub use runtime_spine::RUNTIME_WORKER_PROCESS_CLAIM_SCHEMA_VERSION;
pub use runtime_spine::RUNTIME_WORKER_PROCESS_CLAIM_TYPE;
pub use runtime_spine::RepoFrontierResearchContinuationAction;
pub use runtime_spine::RepoFrontierResearchLifecycle;
pub use runtime_spine::RepoFrontierResearchLifecycleStage;
@ -277,9 +208,6 @@ pub use runtime_spine::RepoFrontierVerdictModelingLaunchAuthority;
pub use runtime_spine::RuntimeSpineHeartbeatJobOptions;
pub use runtime_spine::RuntimeSpineInitOptions;
pub use runtime_spine::RuntimeSpineJobOptions;
pub use runtime_spine::RuntimeSpineJobResultOptions;
pub use runtime_spine::RuntimeSpineSessionClosureOptions;
pub use runtime_spine::RuntimeSpineSessionOptions;
pub use runtime_spine::abandon_unactivated_runtime_worker_process;
pub use runtime_spine::activate_runtime_worker_process;
pub use runtime_spine::bind_runtime_repository_domain;
@ -287,15 +215,10 @@ pub use runtime_spine::bind_runtime_to_swarm;
pub use runtime_spine::canonical_repo_frontier_plan_candidate_id;
pub use runtime_spine::claim_runtime_worker_process;
pub use runtime_spine::close_runtime_session;
pub use runtime_spine::commit_repo_frontier_modeling_request;
pub use runtime_spine::commit_repo_frontier_plan_decision;
pub use runtime_spine::commit_repo_frontier_plan_mind_request;
pub use runtime_spine::complete_runtime_job;
pub use runtime_spine::coordinator_run_receipts;
pub use runtime_spine::coordinator_run_session_id;
pub use runtime_spine::create_runtime_job;
pub use runtime_spine::create_runtime_session;
pub use runtime_spine::ensure_runtime_session;
pub use runtime_spine::finalize_coordinator_run;
pub use runtime_spine::initialize_runtime_spine;
pub use runtime_spine::model_pass_failure_for_request;
@ -304,7 +227,6 @@ pub use runtime_spine::open_runtime_model_execution;
pub use runtime_spine::prepare_runtime_spine_heartbeat_job;
pub(crate) use runtime_spine::promote_autonomous_direction_options_for_modeling;
pub use runtime_spine::put_hands_action_intent;
pub use runtime_spine::put_hands_action_review;
pub use runtime_spine::put_hands_command_receipt;
pub use runtime_spine::put_hands_commit_receipt;
pub use runtime_spine::put_hands_patch_receipt;
@ -320,22 +242,14 @@ pub use runtime_spine::retain_completed_runtime_sessions;
pub use runtime_spine::retain_failed_runtime_worker_attempts;
pub use runtime_spine::retain_fulfilled_runtime_worker_attempts;
pub use runtime_spine::review_repo_frontier_planning_failure;
pub(crate) use runtime_spine::runtime_authenticated_public_source_lookups_for_worker;
pub use runtime_spine::runtime_hands_command_receipt;
pub use runtime_spine::runtime_hands_commit_receipt;
pub use runtime_spine::runtime_hands_patch_receipt;
pub(crate) use runtime_spine::runtime_has_actionable_hands_frontier;
pub use runtime_spine::runtime_job_snapshot;
pub use runtime_spine::runtime_identity;
pub use runtime_spine::runtime_registered_document_types;
pub(crate) use runtime_spine::runtime_reorient_worker_result;
pub use runtime_spine::runtime_job;
pub use runtime_spine::runtime_reorient_worker_result;
pub use runtime_spine::runtime_repo_frontier_planning_lifecycle;
pub(crate) use runtime_spine::runtime_repo_frontier_research_lifecycle;
pub use runtime_spine::runtime_requested_public_source_refs_for_worker;
pub use runtime_spine::runtime_role_worker_result;
pub use runtime_spine::runtime_spine_cache;
pub(crate) use runtime_spine::runtime_typed_request_attempt_exists;
pub(crate) use runtime_spine::runtime_typed_request_fulfillment;
pub use runtime_spine::runtime_worker_launch_request;
pub use runtime_spine::runtime_worker_process_claim;
pub use runtime_spine::runtime_worker_process_claims;
@ -343,6 +257,7 @@ pub use runtime_spine::select_and_commit_repo_frontier_planning_request;
pub(crate) use runtime_spine::select_and_commit_repo_frontier_research_request;
pub use runtime_spine::select_and_commit_repo_frontier_route;
pub use runtime_spine::terminalize_model_pass_failure_session;
pub use runtime_spine::terminalize_runtime_job;
pub(crate) use runtime_worker_attempt::{RuntimeTypedRequestRef, WorkerProcessStatus};
pub use soul_gateway::*;
pub use state_ledger::EpiphanyBranchRecord;
@ -352,23 +267,17 @@ pub use state_ledger::add_state_branch;
pub use state_ledger::append_state_evidence;
pub use state_ledger::close_state_branch;
pub use state_ledger::load_state_ledger;
pub use state_ledger::state_ledger_status;
pub use state_model::*;
pub use substrate_gate::SUBSTRATE_GATE_REPO_ACCESS_GRANT_RECEIPT_SCHEMA_VERSION;
pub use substrate_gate::SUBSTRATE_GATE_REPO_ACCESS_GRANT_RECEIPT_TYPE;
pub use substrate_gate::SubstrateGateRepoAccessGrantReceipt;
pub use substrate_gate::substrate_gate_coordinator_implementation_grant;
pub use substrate_gate::substrate_gate_operation_for_governed_tool;
pub use substrate_gate::substrate_gate_repo_access_grant_for_worker;
pub use substrate_gate::substrate_gate_repo_work_planning_grant;
pub use surfaces::AdmittedModelDirectionConsiderationContextProjection;
pub use surfaces::EpiphanyCoordinatorAction;
pub use surfaces::EpiphanyCoordinatorDecision;
pub use surfaces::EpiphanyCoordinatorInput;
pub use surfaces::EpiphanyCoordinatorRoleResultStatus;
pub use surfaces::EpiphanyCoordinatorRoleStatus;
pub use surfaces::EpiphanyCrrcAction;
pub use surfaces::EpiphanyCrrcResultStatus;
pub use surfaces::EpiphanyReorientAction;
pub use surfaces::EpiphanyReorientFindingInterpretation;
pub use surfaces::EpiphanyReorientWorkerLaunchDocument;
pub use surfaces::EpiphanyResearchDecision;

View file

@ -1,16 +1,5 @@
pub mod validation;
pub use epiphany_state_model::EpiphanyMemoryDomain;
pub use epiphany_state_model::EpiphanyMemoryEdge;
pub use epiphany_state_model::EpiphanyMemoryEdgeKind;
pub use epiphany_state_model::EpiphanyMemoryGraphSnapshot;
pub use epiphany_state_model::EpiphanyMemoryLifecycle;
pub use epiphany_state_model::EpiphanyMemoryNode;
pub use epiphany_state_model::EpiphanyMemoryNodeKind;
pub use epiphany_state_model::RepoFrontierAdoptedPlan;
pub use epiphany_state_model::RepoFrontierItem;
pub use epiphany_state_model::RepoFrontierStatus;
pub use validation::EpiphanyMemoryGraphValidationError;
pub(crate) use validation::frontier_item_has_routeable_repository_scope;
pub(crate) use validation::repo_paths_are_canonical_and_safe;

View file

@ -1,9 +1,9 @@
use super::EpiphanyMemoryDomain;
use super::EpiphanyMemoryEdge;
use super::EpiphanyMemoryGraphSnapshot;
use super::EpiphanyMemoryLifecycle;
use super::EpiphanyMemoryNode;
use super::RepoFrontierStatus;
use crate::EpiphanyMemoryDomain;
use crate::EpiphanyMemoryEdge;
use crate::EpiphanyMemoryGraphSnapshot;
use crate::EpiphanyMemoryLifecycle;
use crate::EpiphanyMemoryNode;
use crate::RepoFrontierStatus;
use std::collections::{HashMap, HashSet};
pub(crate) fn repo_paths_are_canonical_and_safe(paths: &[String]) -> bool {
@ -22,9 +22,7 @@ pub(crate) fn repo_paths_are_canonical_and_safe(paths: &[String]) -> bool {
})
}
pub(crate) fn frontier_item_has_routeable_repository_scope(
item: &epiphany_state_model::RepoFrontierItem,
) -> bool {
pub(crate) fn frontier_item_has_routeable_repository_scope(item: &crate::RepoFrontierItem) -> bool {
!item.repository_scope.is_empty() && repo_paths_are_canonical_and_safe(&item.repository_scope)
}

View file

@ -1,19 +1,17 @@
use std::path::Path;
use crate::state_model::{
EpiphanyEvidenceRecord, EpiphanyInvariant, EpiphanyInvestigationCheckpoint, EpiphanyModeState,
EpiphanyObservation, EpiphanySubgoal,
};
use anyhow::{Result, anyhow};
use cultcache_rs::{CultCache, CultCacheEnvelope, DatabaseEntry};
use epiphany_state_model::{
EpiphanyBacklogItem, EpiphanyEvidenceRecord, EpiphanyInvariant,
EpiphanyInvestigationCheckpoint, EpiphanyModeState, EpiphanyObjectiveDraft,
EpiphanyObservation, EpiphanyPlanningCapture, EpiphanyPlanningState, EpiphanyRoadmapStream,
EpiphanySubgoal,
};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::{EpiphanyMindDocumentVersion, runtime_spine_cache};
pub const MIND_SCHEMA_EPOCH: &str = "epiphany.mind.epoch.v8";
pub const MIND_SCHEMA_EPOCH: &str = "epiphany.mind.epoch.v11";
pub const MIND_OBJECTIVE_KEY: &str = "objective";
pub const MIND_FOCUS_KEY: &str = "focus";
pub const MIND_MODE_KEY: &str = "mode";
@ -140,7 +138,7 @@ pub struct EpiphanyMindEvidenceDocument {
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
#[cultcache(
type = "epiphany.mind.persona_memory.v1",
type = "epiphany.mind.persona_memory.v2",
schema = "EpiphanyMindPersonaMemoryDocument"
)]
pub struct EpiphanyMindPersonaMemoryDocument {
@ -157,12 +155,8 @@ pub struct EpiphanyMindPersonaMemoryDocument {
#[cultcache(key = 5)]
pub confidence: f64,
#[cultcache(key = 6)]
pub linked_event_ids: Vec<String>,
#[cultcache(key = 7)]
pub linked_relationship_id: Option<String>,
#[cultcache(key = 8)]
pub effect_document_id: String,
#[cultcache(key = 9)]
#[cultcache(key = 7)]
pub decision_context_id: String,
}
@ -297,46 +291,6 @@ pub struct EpiphanyMindInvestigationCheckpointDocument {
pub value: EpiphanyInvestigationCheckpoint,
}
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
#[cultcache(
type = "epiphany.mind.planning_capture.v1",
schema = "EpiphanyMindPlanningCaptureDocument"
)]
pub struct EpiphanyMindPlanningCaptureDocument {
#[cultcache(key = 0)]
pub value: EpiphanyPlanningCapture,
}
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
#[cultcache(
type = "epiphany.mind.backlog_item.v1",
schema = "EpiphanyMindBacklogItemDocument"
)]
pub struct EpiphanyMindBacklogItemDocument {
#[cultcache(key = 0)]
pub value: EpiphanyBacklogItem,
}
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
#[cultcache(
type = "epiphany.mind.roadmap_stream.v1",
schema = "EpiphanyMindRoadmapStreamDocument"
)]
pub struct EpiphanyMindRoadmapStreamDocument {
#[cultcache(key = 0)]
pub value: EpiphanyRoadmapStream,
}
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
#[cultcache(
type = "epiphany.mind.objective_draft.v1",
schema = "EpiphanyMindObjectiveDraftDocument"
)]
pub struct EpiphanyMindObjectiveDraftDocument {
#[cultcache(key = 0)]
pub value: EpiphanyObjectiveDraft,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct EpiphanyMindView {
pub schema_epoch: String,
@ -356,7 +310,6 @@ pub struct EpiphanyMindView {
pub agent_pass_admission_refusals: Vec<crate::EpiphanyAgentPassAdmissionRefusal>,
pub investigation_checkpoint: Option<EpiphanyInvestigationCheckpoint>,
pub mode: Option<EpiphanyModeState>,
pub planning: EpiphanyPlanningState,
pub repository_body_observation: Option<crate::RepositoryBodyObservationBasis>,
pub repo_model: Option<crate::EpiphanyRepoModelView>,
}
@ -376,10 +329,6 @@ pub(crate) fn register_mind_document_types(cache: &mut CultCache) -> Result<()>
cache.register_entry_type::<crate::EpiphanyMindReorientationDecisionDocument>()?;
cache.register_entry_type::<crate::EpiphanyMindReorientationPassFailureDocument>()?;
cache.register_entry_type::<EpiphanyMindInvestigationCheckpointDocument>()?;
cache.register_entry_type::<EpiphanyMindPlanningCaptureDocument>()?;
cache.register_entry_type::<EpiphanyMindBacklogItemDocument>()?;
cache.register_entry_type::<EpiphanyMindRoadmapStreamDocument>()?;
cache.register_entry_type::<EpiphanyMindObjectiveDraftDocument>()?;
cache.register_entry_type::<EpiphanyMindRepositoryBodyObservationDocument>()?;
crate::repo_model_documents::register_repo_model_document_types(cache)?;
cache.register_entry_type::<crate::RepoFrontierPlanDecisionReceipt>()?;
@ -524,22 +473,6 @@ pub(crate) fn validate_mind_write_envelope(envelope: &CultCacheEnvelope) -> Resu
rmp_serde::from_slice::<EpiphanyMindInvestigationCheckpointDocument>(&envelope.payload)?
.value
.checkpoint_id
} else if envelope.r#type == EpiphanyMindPlanningCaptureDocument::TYPE {
rmp_serde::from_slice::<EpiphanyMindPlanningCaptureDocument>(&envelope.payload)?
.value
.id
} else if envelope.r#type == EpiphanyMindBacklogItemDocument::TYPE {
rmp_serde::from_slice::<EpiphanyMindBacklogItemDocument>(&envelope.payload)?
.value
.id
} else if envelope.r#type == EpiphanyMindRoadmapStreamDocument::TYPE {
rmp_serde::from_slice::<EpiphanyMindRoadmapStreamDocument>(&envelope.payload)?
.value
.id
} else if envelope.r#type == EpiphanyMindObjectiveDraftDocument::TYPE {
rmp_serde::from_slice::<EpiphanyMindObjectiveDraftDocument>(&envelope.payload)?
.value
.id
} else if envelope.r#type == EpiphanyMindRepositoryBodyObservationDocument::TYPE {
let value: EpiphanyMindRepositoryBodyObservationDocument =
rmp_serde::from_slice(&envelope.payload)?;
@ -562,6 +495,10 @@ pub(crate) fn validate_mind_write_envelope(envelope: &CultCacheEnvelope) -> Resu
pub fn assemble_mind_view(store_path: impl AsRef<Path>) -> Result<EpiphanyMindView> {
let mut cache = runtime_spine_cache(store_path.as_ref())?;
cache.pull_all_backing_stores()?;
assemble_mind_view_from_cache(&cache)
}
pub(crate) fn assemble_mind_view_from_cache(cache: &CultCache) -> Result<EpiphanyMindView> {
let identity = cache
.get::<EpiphanyMindIdentity>(MIND_SCHEMA_EPOCH)?
.ok_or_else(|| anyhow!("writable Mind store has no v1 identity"))?;
@ -599,14 +536,6 @@ pub fn assemble_mind_view(store_path: impl AsRef<Path>) -> Result<EpiphanyMindVi
}
let checkpoints =
values::<EpiphanyMindInvestigationCheckpointDocument, _>(&cache, |value| value.value)?;
let mut captures =
values::<EpiphanyMindPlanningCaptureDocument, _>(&cache, |value| value.value)?;
let mut backlog_items =
values::<EpiphanyMindBacklogItemDocument, _>(&cache, |value| value.value)?;
let mut roadmap_streams =
values::<EpiphanyMindRoadmapStreamDocument, _>(&cache, |value| value.value)?;
let mut objective_drafts =
values::<EpiphanyMindObjectiveDraftDocument, _>(&cache, |value| value.value)?;
let mut body_observations = cache.get_all::<EpiphanyMindRepositoryBodyObservationDocument>()?;
for observation in &body_observations {
observation.validate()?;
@ -629,7 +558,7 @@ pub fn assemble_mind_view(store_path: impl AsRef<Path>) -> Result<EpiphanyMindVi
.get::<crate::EpiphanyRepoModelIdentityDocument>(crate::REPO_MODEL_IDENTITY_KEY)?
.is_some()
{
Some(crate::assemble_repo_model_view(store_path.as_ref())?)
Some(crate::repo_model_documents::assemble_repo_model_view_from_cache(cache)?)
} else {
None
};
@ -642,10 +571,6 @@ pub fn assemble_mind_view(store_path: impl AsRef<Path>) -> Result<EpiphanyMindVi
reorientation_decisions.sort_by(|left, right| left.decision_id.cmp(&right.decision_id));
reorientation_failures.sort_by(|left, right| left.failure_id.cmp(&right.failure_id));
agent_pass_admission_refusals.sort_by(|left, right| left.refusal_id.cmp(&right.refusal_id));
captures.sort_by(|left, right| left.id.cmp(&right.id));
backlog_items.sort_by(|left, right| left.id.cmp(&right.id));
roadmap_streams.sort_by(|left, right| left.id.cmp(&right.id));
objective_drafts.sort_by(|left, right| left.id.cmp(&right.id));
let investigation_checkpoint = match focus
.as_ref()
.and_then(|document| document.investigation_checkpoint_id.as_deref())
@ -690,13 +615,6 @@ pub fn assemble_mind_view(store_path: impl AsRef<Path>) -> Result<EpiphanyMindVi
agent_pass_admission_refusals,
investigation_checkpoint,
mode,
planning: EpiphanyPlanningState {
workspace_root: None,
captures,
backlog_items,
roadmap_streams,
objective_drafts,
},
repository_body_observation,
repo_model,
})
@ -732,10 +650,6 @@ fn canonical_mind_versions(
| crate::EpiphanyMindReorientationPassFailureDocument::TYPE
| crate::EpiphanyAgentPassAdmissionRefusal::TYPE
| EpiphanyMindInvestigationCheckpointDocument::TYPE
| EpiphanyMindPlanningCaptureDocument::TYPE
| EpiphanyMindBacklogItemDocument::TYPE
| EpiphanyMindRoadmapStreamDocument::TYPE
| EpiphanyMindObjectiveDraftDocument::TYPE
| EpiphanyMindRepositoryBodyObservationDocument::TYPE
)
})

View file

@ -4,7 +4,8 @@ use chrono::Utc;
use cultcache_rs::SingleFileMessagePackBackingStore;
use std::path::Path;
pub use epiphany_release_construction::*;
mod construction;
pub use construction::*;
const RELEASE_KEY_PREFIX: &str = "epiphany-local/packaged-release/by-id/";
const RELEASE_HEAD_KEY: &str = "epiphany-local/packaged-release/current";
@ -52,22 +53,6 @@ pub fn publish_epiphany_packaged_release(
Ok(entry)
}
pub fn load_epiphany_packaged_release(
store: &Path,
runtime_id: &str,
release_id: &str,
) -> Result<Option<EpiphanyPackagedReleaseEntry>> {
open_epiphany_cultmesh_node(store, runtime_id.to_string())?
.get(&format!("{RELEASE_KEY_PREFIX}{release_id}"))
}
pub fn load_epiphany_packaged_release_head(
store: &Path,
runtime_id: &str,
) -> Result<Option<EpiphanyPackagedReleaseHead>> {
open_epiphany_cultmesh_node(store, runtime_id.to_string())?.get(RELEASE_HEAD_KEY)
}
pub fn authenticate_epiphany_packaged_release(
store: &Path,
runtime_id: &str,
@ -75,7 +60,8 @@ pub fn authenticate_epiphany_packaged_release(
expected_witness_sha256: &str,
) -> Result<EpiphanyPackagedReleaseEntry> {
validate_epiphany_packaged_release_sha256(expected_witness_sha256)?;
let entry = load_epiphany_packaged_release(store, runtime_id, release_id)?
let entry = open_epiphany_cultmesh_node(store, runtime_id.to_string())?
.get::<EpiphanyPackagedReleaseEntry>(&format!("{RELEASE_KEY_PREFIX}{release_id}"))?
.context("packaged release witness is absent")?;
validate_epiphany_packaged_release(&entry)?;
verify_epiphany_packaged_release_files(&entry)?;

View file

@ -79,7 +79,7 @@ pub struct PackageReleaseRequest<'a> {
pub target_triple: &'a str,
}
pub fn required_packaged_release_binaries(target_triple: &str) -> Vec<(&'static str, String)> {
fn required_packaged_release_binaries(target_triple: &str) -> Vec<(&'static str, String)> {
let file_name = |name: &str| target_binary_file_name(target_triple, name);
vec![
("release-publisher", file_name("epiphany-release")),
@ -572,7 +572,9 @@ fn release_bundle_target_dir(
target_root.join(format!("graph-{:x}", digest.finalize()))
}
pub fn validate_epiphany_packaged_release(entry: &EpiphanyPackagedReleaseEntry) -> Result<()> {
pub(super) fn validate_epiphany_packaged_release(
entry: &EpiphanyPackagedReleaseEntry,
) -> Result<()> {
if entry.schema_version != EPIPHANY_PACKAGED_RELEASE_SCHEMA_VERSION {
bail!("unsupported packaged release schema");
}
@ -630,7 +632,9 @@ pub fn validate_epiphany_packaged_release(entry: &EpiphanyPackagedReleaseEntry)
Ok(())
}
pub fn verify_epiphany_packaged_release_files(entry: &EpiphanyPackagedReleaseEntry) -> Result<()> {
pub(super) fn verify_epiphany_packaged_release_files(
entry: &EpiphanyPackagedReleaseEntry,
) -> Result<()> {
validate_epiphany_packaged_release(entry)?;
let root = canonical_path(&entry.package_root).context("packaged release root is absent")?;
if root != PathBuf::from(&entry.package_root) {
@ -672,7 +676,7 @@ pub fn verify_epiphany_packaged_release_files(entry: &EpiphanyPackagedReleaseEnt
Ok(())
}
pub fn write_epiphany_packaged_release_witness(
fn write_epiphany_packaged_release_witness(
path: &Path,
entry: &EpiphanyPackagedReleaseEntry,
) -> Result<()> {
@ -686,7 +690,7 @@ pub fn write_epiphany_packaged_release_witness(
})
}
pub fn read_epiphany_packaged_release_witness(path: &Path) -> Result<EpiphanyPackagedReleaseEntry> {
fn read_epiphany_packaged_release_witness(path: &Path) -> Result<EpiphanyPackagedReleaseEntry> {
let bytes = fs::read(path)
.with_context(|| format!("failed to read packaged release witness {}", path.display()))?;
let entry = rmp_serde::from_slice(&bytes).with_context(|| {
@ -727,7 +731,7 @@ pub fn epiphany_packaged_release_witness_sha256(
witness_sha256(entry)
}
pub fn validate_epiphany_packaged_release_sha256(value: &str) -> Result<()> {
pub(super) fn validate_epiphany_packaged_release_sha256(value: &str) -> Result<()> {
validate_sha256(value)
}

View file

@ -21,10 +21,8 @@ use crate::{
verify_persona_discord_delivery_receipt,
};
pub const PERSONA_DISCORD_DELIVERY_EVIDENCE_SCHEMA_VERSION: &str =
"epiphany.persona_discord_delivery_evidence.v0";
pub const PERSONA_CONVERSATION_EXECUTION_RECEIPT_SCHEMA_VERSION: &str =
"epiphany.persona_conversation_execution_receipt.v1";
"epiphany.persona_conversation_execution_receipt.v2";
pub const PERSONA_EFFECT_EXECUTION_INTENT_SCHEMA_VERSION: &str =
"epiphany.persona_effect_execution_intent.v0";
pub const PERSONA_CONVERSATION_STORE_RETIREMENT_RECEIPT_SCHEMA_VERSION: &str =
@ -150,37 +148,7 @@ pub struct PersonaEffectExecutionIntent {
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
#[cultcache(
type = "epiphany.persona_discord_delivery_evidence.v0",
schema = "PersonaDiscordDeliveryEvidence"
)]
pub struct PersonaDiscordDeliveryEvidence {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub evidence_id: String,
#[cultcache(key = 2)]
pub effect_document_id: String,
#[cultcache(key = 3)]
pub channel_id: String,
#[cultcache(key = 4, default)]
pub reply_to_message_id: Option<String>,
#[cultcache(key = 5)]
pub message_id: String,
#[cultcache(key = 6)]
pub transport: String,
#[cultcache(key = 7)]
pub crossing_receipt_id: String,
#[cultcache(key = 8)]
pub receipt_url: String,
#[cultcache(key = 9)]
pub bridge_receipt_sha256: String,
#[cultcache(key = 10)]
pub private_state_exposed: bool,
}
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
#[cultcache(
type = "epiphany.persona_conversation_execution_receipt.v1",
type = "epiphany.persona_conversation_execution_receipt.v2",
schema = "PersonaConversationExecutionReceipt"
)]
pub struct PersonaConversationExecutionReceipt {
@ -198,8 +166,6 @@ pub struct PersonaConversationExecutionReceipt {
pub state_effect_status: String,
#[cultcache(key = 6, default)]
pub state_effect_reasons: Vec<String>,
#[cultcache(key = 7, default)]
pub delivery_evidence_ids: Vec<String>,
#[cultcache(key = 8)]
pub social_terminal_receipt_id: Option<String>,
#[cultcache(key = 9)]
@ -289,7 +255,7 @@ pub fn poll_persona_discord_crossing(
PersonaTurnTerminalOptions {
request_id: request_id.into(),
outcome: outcome.into(),
delivery_evidence: None,
delivery_receipt: None,
blocked_evidence: None,
},
)?;
@ -301,7 +267,6 @@ pub fn poll_persona_discord_crossing(
outcome: outcome.into(),
state_effect_status: state_status,
state_effect_reasons: reasons,
delivery_evidence_ids: vec![],
social_terminal_receipt_id: Some(terminal.receipt_id),
private_state_exposed: false,
model_terminal_receipt_id: Some(model_terminal.receipt_id.clone()),
@ -379,32 +344,12 @@ pub fn poll_persona_discord_crossing(
),
_ => unreachable!(),
};
let evidence = (outcome == "delivered").then(|| PersonaDiscordDeliveryEvidence {
schema_version: PERSONA_DISCORD_DELIVERY_EVIDENCE_SCHEMA_VERSION.into(),
evidence_id: format!(
"persona-delivery:{}:{}",
effects.document_id, delivery.message_id
),
effect_document_id: effects.document_id.clone(),
channel_id: delivery.channel_id.clone(),
reply_to_message_id: (!delivery.reply_to_message_id.is_empty())
.then(|| delivery.reply_to_message_id.clone()),
message_id: delivery.message_id.clone(),
transport: delivery.transport.clone(),
crossing_receipt_id: delivery.crossing_receipt_id.clone(),
receipt_url: delivery.receipt_url.clone(),
bridge_receipt_sha256: signed_receipt_sha256,
private_state_exposed: false,
});
if let Some(value) = &evidence {
put_runtime_document(runtime_store, &value.evidence_id, value)?;
}
let terminal = complete_persona_social_turn(
social_store,
PersonaTurnTerminalOptions {
request_id: request_id.into(),
outcome: outcome.into(),
delivery_evidence: evidence.clone(),
delivery_receipt: (outcome == "delivered").then(|| delivery.clone()),
blocked_evidence,
},
)?;
@ -422,10 +367,6 @@ pub fn poll_persona_discord_crossing(
outcome: outcome.into(),
state_effect_status: "admitted_before_delivery_request".into(),
state_effect_reasons: vec![],
delivery_evidence_ids: evidence
.iter()
.map(|value| value.evidence_id.clone())
.collect(),
social_terminal_receipt_id: Some(terminal.receipt_id),
private_state_exposed: false,
model_terminal_receipt_id: Some(model_terminal.receipt_id.clone()),
@ -448,7 +389,7 @@ fn terminalize_local_effect_quarantine(
PersonaTurnTerminalOptions {
request_id: request.request_id.clone(),
outcome: "blocked".into(),
delivery_evidence: None,
delivery_receipt: None,
blocked_evidence: Some(crate::PersonaTurnBlockedEvidence {
evidence_source: "local_effect".into(),
crossing_status: "unknown".into(),
@ -466,7 +407,6 @@ fn terminalize_local_effect_quarantine(
outcome: "blocked".into(),
state_effect_status: "quarantined_ambiguous_local_effect".into(),
state_effect_reasons: vec![error.to_string()],
delivery_evidence_ids: vec![],
social_terminal_receipt_id: Some(terminal.receipt_id),
private_state_exposed: false,
model_terminal_receipt_id: Some(model_terminal.receipt_id),
@ -519,7 +459,6 @@ pub fn reconcile_terminal_persona_conversation(
outcome: terminal.outcome.clone(),
state_effect_status: "reconciled_from_social_terminal".into(),
state_effect_reasons: vec![],
delivery_evidence_ids: terminal.delivery_evidence_id.clone().into_iter().collect(),
social_terminal_receipt_id: Some(terminal.receipt_id.clone()),
private_state_exposed: false,
model_terminal_receipt_id: Some(model_terminal.receipt_id),
@ -756,22 +695,6 @@ fn build_persona_retention_member(
}
runtime_ids.push((<PersonaEffectExecutionIntent as DatabaseEntry>::TYPE, id));
}
if let Some(evidence_id) = &terminal.delivery_evidence_id {
let Some(evidence) =
runtime_document::<PersonaDiscordDeliveryEvidence>(runtime_store, evidence_id)?
else {
return Ok(None);
};
if conversation.delivery_evidence_ids != [evidence_id.clone()]
|| evidence.private_state_exposed
{
return Err(anyhow!("Persona retained delivery evidence is invalid"));
}
} else if terminal.outcome == "delivered" || !conversation.delivery_evidence_ids.is_empty() {
return Err(anyhow!(
"delivered Persona retention candidate lacks evidence"
));
}
let mut runtime_cache = runtime_spine_cache(runtime_store)?;
runtime_cache.pull_all_backing_stores()?;
let runtime_snapshot = runtime_cache.snapshot_envelopes();
@ -801,7 +724,16 @@ fn build_persona_retention_member(
&crossing_request,
receipt_anchor,
)?;
if crossing_receipt.status != "completed" {
let receipt_sha256 = format!(
"sha256:{:x}",
Sha256::digest(rmp_serde::to_vec(&crossing_receipt)?)
);
if crossing_receipt.status != "completed"
|| terminal.delivery_message_id.as_deref() != Some(crossing_receipt.message_id.as_str())
|| terminal.crossing_receipt_id.as_deref()
!= Some(crossing_receipt.crossing_receipt_id.as_str())
|| terminal.bridge_receipt_sha256.as_deref() != Some(receipt_sha256.as_str())
{
return Ok(None);
}
// The signed request and consequence receipt remain durable audit
@ -1026,7 +958,7 @@ fn require_store_retirement_receipt(
})?;
let mut cache = CultCache::new();
cache.register_entry_type::<PersonaConversationStoreRetirementReceipt>()?;
cache.load_envelope::<PersonaConversationStoreRetirementReceipt>(envelope.clone())?;
cache.put_envelope::<PersonaConversationStoreRetirementReceipt>(envelope.clone())?;
if cache
.get::<PersonaConversationStoreRetirementReceipt>(&expected.receipt_id)?
.as_ref()
@ -1110,8 +1042,6 @@ pub(crate) fn admit_persona_state_notes(
summary: summary.clone(),
salience: 0.7,
confidence: confidence.unwrap_or(0.7),
linked_event_ids: vec![document.document_id.clone()],
linked_relationship_id: None,
effect_document_id: document.document_id.clone(),
decision_context_id: document.decision_context_id.clone(),
};
@ -1530,7 +1460,6 @@ mod tests {
outcome: "dropped".into(),
state_effect_status: "none".into(),
state_effect_reasons: vec![],
delivery_evidence_ids: vec![],
social_terminal_receipt_id: Some(terminal.receipt_id),
private_state_exposed: false,
model_terminal_receipt_id: Some(format!("persona-terminal:{request_id}")),
@ -1898,7 +1827,6 @@ mod tests {
outcome: "dropped".into(),
state_effect_status: "none".into(),
state_effect_reasons: vec![],
delivery_evidence_ids: vec![],
social_terminal_receipt_id: Some("retry:terminal".into()),
private_state_exposed: false,
model_terminal_receipt_id: Some("persona-terminal:retry".into()),

View file

@ -1042,7 +1042,7 @@ mod tests {
let delivery = signed_delivery(&signer, "epiphany")?;
let mut source = CultCache::new();
source.register_entry_type::<BifrostPersonaFeedbackAdmission>()?;
source.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&source_store));
source.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&source_store))?;
source.put(&delivery.admission_id, &delivery)?;
let source_lock = source_store.with_file_name(format!(
"{}.lock",

View file

@ -10,7 +10,7 @@ use std::path::Path;
pub const PERSONA_TURN_REQUEST_SCHEMA_VERSION: &str = "epiphany.persona_turn_request.v1";
pub const PERSONA_TURN_TERMINAL_RECEIPT_SCHEMA_VERSION: &str =
"epiphany.persona_turn_terminal_receipt.v1";
"epiphany.persona_turn_terminal_receipt.v2";
pub const PERSONA_CONVERSATION_RETENTION_HEAD_SCHEMA_VERSION: &str =
"epiphany.persona_conversation_retention_head.v1";
pub const PERSONA_CONVERSATION_RETENTION_PLAN_SCHEMA_VERSION: &str =
@ -21,7 +21,6 @@ pub const PERSONA_CONVERSATION_RETENTION_PLAN_SCHEMA_VERSION: &str =
pub struct PersonaSocialMention {
pub id: String,
pub target_role_id: String,
pub target_agent_id: String,
pub source_surface: String,
pub channel_id: String,
pub message_id: String,
@ -73,7 +72,7 @@ pub struct PersonaTurnTerminalReceipt {
pub mention_ids: Vec<String>,
pub mention_cargo_sha256: String,
#[serde(default)]
pub delivery_evidence_id: Option<String>,
pub delivery_message_id: Option<String>,
#[serde(default)]
pub crossing_receipt_id: Option<String>,
#[serde(default)]
@ -90,7 +89,7 @@ pub struct PersonaTurnTerminalReceipt {
pub struct PersonaTurnTerminalOptions {
pub request_id: String,
pub outcome: String,
pub delivery_evidence: Option<crate::PersonaDiscordDeliveryEvidence>,
pub delivery_receipt: Option<crate::PersonaDiscordDeliveryReceipt>,
pub blocked_evidence: Option<PersonaTurnBlockedEvidence>,
}
@ -146,30 +145,9 @@ pub struct PersonaTurnBlockedEvidence {
pub bridge_receipt_sha256: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PersonaBlockedConversationPressure {
pub schema_version: String,
pub quarantine_id: String,
pub request_id: String,
pub terminal_receipt_id: String,
pub crossing_status: String,
pub evidence_source: String,
pub reason: String,
pub mentions: Vec<PersonaSocialMention>,
pub mention_cargo_sha256: String,
#[serde(default)]
pub crossing_receipt_id: Option<String>,
#[serde(default)]
pub bridge_receipt_sha256: Option<String>,
pub quarantined_at: String,
pub private_state_exposed: bool,
}
pub const PERSONA_SOCIAL_MENTION_TYPE: &str = "epiphany.persona.social_mention.v1";
pub const PERSONA_SOCIAL_MENTION_TYPE: &str = "epiphany.persona.social_mention.v2";
pub const PERSONA_SOCIAL_TURN_REQUEST_TYPE: &str = "epiphany.persona.turn_request.v1";
pub const PERSONA_SOCIAL_TURN_TERMINAL_TYPE: &str = "epiphany.persona.turn_terminal.v1";
pub const PERSONA_SOCIAL_QUARANTINE_TYPE: &str = "epiphany.persona.quarantine.v1";
pub const PERSONA_SOCIAL_TURN_TERMINAL_TYPE: &str = "epiphany.persona.turn_terminal.v2";
pub const PERSONA_SOCIAL_RETENTION_HEAD_TYPE: &str = "epiphany.persona.retention_head.v1";
pub const PERSONA_SOCIAL_RETENTION_PLAN_TYPE: &str = "epiphany.persona.retention_plan.v1";
pub const PERSONA_SOCIAL_RETENTION_HEAD_KEY: &str = "persona";
@ -196,7 +174,7 @@ pub struct PersonaSocialQueueMentionOptions {
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
#[cultcache(
type = "epiphany.persona.social_mention.v1",
type = "epiphany.persona.social_mention.v2",
schema = "PersonaSocialMentionDocument"
)]
pub struct PersonaSocialMentionDocument {
@ -220,7 +198,7 @@ pub struct PersonaSocialTurnRequestDocument {
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
#[cultcache(
type = "epiphany.persona.turn_terminal.v1",
type = "epiphany.persona.turn_terminal.v2",
schema = "PersonaSocialTurnTerminalDocument"
)]
pub struct PersonaSocialTurnTerminalDocument {
@ -228,16 +206,6 @@ pub struct PersonaSocialTurnTerminalDocument {
pub receipt: PersonaTurnTerminalReceipt,
}
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
#[cultcache(
type = "epiphany.persona.quarantine.v1",
schema = "PersonaSocialQuarantineDocument"
)]
pub struct PersonaSocialQuarantineDocument {
#[cultcache(key = 0)]
pub pressure: PersonaBlockedConversationPressure,
}
#[derive(Clone, Debug, PartialEq, DatabaseEntry)]
#[cultcache(
type = "epiphany.persona.retention_head.v1",
@ -265,6 +233,11 @@ pub fn persona_social_cache(store_path: impl AsRef<Path>) -> Result<CultCache> {
register_persona_social_types(&mut cache)?;
let mut identities = HashSet::new();
for envelope in SingleFileMessagePackBackingStore::new(store_path.as_ref()).pull_all()? {
if envelope.r#type == "epiphany.persona.social_mention.v1" {
return Err(anyhow!(
"Persona social store contains an obsolete writable mention epoch"
));
}
if !is_persona_social_type(&envelope.r#type) {
continue;
}
@ -320,7 +293,6 @@ pub fn queue_persona_social_mention(
mention: PersonaSocialMention {
id: mention_id.clone(),
target_role_id: options.target_role_id.clone(),
target_agent_id: "epiphany.Persona".into(),
source_surface: options.source_surface,
channel_id: options.channel_id,
message_id: options.message_id,
@ -522,30 +494,33 @@ pub fn complete_persona_social_turn(
existing.outcome
));
}
let delivery_evidence = options.delivery_evidence.as_ref();
let delivery_receipt = options.delivery_receipt.as_ref();
let blocked_evidence = options.blocked_evidence.as_ref();
if options.outcome == "delivered" {
let evidence = delivery_evidence.ok_or_else(|| {
anyhow!("delivered Persona turn requires typed Discord delivery evidence")
let receipt = delivery_receipt.ok_or_else(|| {
anyhow!("delivered Persona turn requires a typed Discord delivery receipt")
})?;
if evidence.schema_version != crate::PERSONA_DISCORD_DELIVERY_EVIDENCE_SCHEMA_VERSION
|| evidence.private_state_exposed
|| evidence.evidence_id.trim().is_empty()
|| evidence.message_id.trim().is_empty()
|| evidence.crossing_receipt_id.trim().is_empty()
|| evidence.bridge_receipt_sha256.trim().is_empty()
if receipt.schema_version != crate::PERSONA_DISCORD_DELIVERY_RECEIPT_SCHEMA_VERSION
|| receipt.status != "completed"
|| receipt.private_state_exposed
|| receipt.receipt_id != receipt.request_id
|| !receipt
.request_id
.starts_with(&format!("persona-discord:{}:", request.request_id))
|| receipt.message_id.trim().is_empty()
|| receipt.crossing_receipt_id.trim().is_empty()
|| !request
.mentions
.iter()
.any(|mention| mention.channel_id == evidence.channel_id)
.any(|mention| mention.channel_id == receipt.channel_id)
{
return Err(anyhow!(
"Discord delivery evidence is not bound to the Persona turn"
"Discord delivery receipt is not bound to the Persona turn"
));
}
} else if delivery_evidence.is_some() {
} else if delivery_receipt.is_some() {
return Err(anyhow!(
"non-delivered Persona terminal outcome must not carry delivery evidence"
"non-delivered Persona terminal outcome must not carry a delivery receipt"
));
}
if options.outcome == "blocked" {
@ -596,10 +571,12 @@ pub fn complete_persona_social_turn(
"sha256-{:x}",
sha2::Sha256::digest(rmp_serde::to_vec(&request.mentions)?)
),
delivery_evidence_id: delivery_evidence.map(|evidence| evidence.evidence_id.clone()),
crossing_receipt_id: delivery_evidence.map(|evidence| evidence.crossing_receipt_id.clone()),
bridge_receipt_sha256: delivery_evidence
.map(|evidence| evidence.bridge_receipt_sha256.clone()),
delivery_message_id: delivery_receipt.map(|receipt| receipt.message_id.clone()),
crossing_receipt_id: delivery_receipt.map(|receipt| receipt.crossing_receipt_id.clone()),
bridge_receipt_sha256: delivery_receipt
.map(rmp_serde::to_vec)
.transpose()?
.map(|bytes| format!("sha256:{:x}", sha2::Sha256::digest(bytes))),
blocked_crossing_status: blocked_evidence.map(|evidence| evidence.crossing_status.clone()),
blocked_reason: blocked_evidence.map(|evidence| evidence.reason.clone()),
completed_at: now_iso(),
@ -644,33 +621,6 @@ pub fn complete_persona_social_turn(
document.terminal_receipt_id = Some(receipt.receipt_id.clone());
writes.push(cache.prepare_entry(&mention.id, &document)?.0);
}
if mention_disposition == "quarantined" {
let evidence = blocked_evidence.expect("validated blocked evidence");
let pressure = PersonaBlockedConversationPressure {
schema_version: "epiphany.persona_blocked_conversation_pressure.v1".into(),
quarantine_id: format!("{}:quarantine", request.request_id),
request_id: request.request_id.clone(),
terminal_receipt_id: receipt.receipt_id.clone(),
crossing_status: evidence.crossing_status.clone(),
evidence_source: evidence.evidence_source.clone(),
reason: evidence.reason.clone(),
mentions: request.mentions.clone(),
mention_cargo_sha256: receipt.mention_cargo_sha256.clone(),
crossing_receipt_id: evidence.crossing_receipt_id.clone(),
bridge_receipt_sha256: evidence.bridge_receipt_sha256.clone(),
quarantined_at: receipt.completed_at.clone(),
private_state_exposed: false,
};
let quarantine_id = pressure.quarantine_id.clone();
writes.push(
cache
.prepare_entry(
&quarantine_id,
&PersonaSocialQuarantineDocument { pressure },
)?
.0,
);
}
if !persona_social_backing(store_path).compare_and_swap_batch(&expected, writes)? {
return Err(anyhow!(
"Persona terminal decision lost exact document compare-and-swap"
@ -742,19 +692,6 @@ pub fn persona_turn_request_source(
crate::EpiphanyMindDocumentVersion::from_envelope("epiphany-persona-social", &envelope)
}
pub fn blocked_persona_pressures(
store_path: impl AsRef<Path>,
) -> Result<Vec<PersonaBlockedConversationPressure>> {
let cache = persona_social_cache(store_path)?;
let mut pressures = cache
.get_all::<PersonaSocialQuarantineDocument>()?
.into_iter()
.map(|document| document.pressure)
.collect::<Vec<_>>();
pressures.sort_by(|left, right| left.quarantine_id.cmp(&right.quarantine_id));
Ok(pressures)
}
pub fn persona_retention_head(
store_path: impl AsRef<Path>,
) -> Result<Option<PersonaConversationRetentionHead>> {
@ -890,7 +827,6 @@ fn register_persona_social_types(cache: &mut CultCache) -> Result<()> {
cache.register_entry_type::<PersonaSocialMentionDocument>()?;
cache.register_entry_type::<PersonaSocialTurnRequestDocument>()?;
cache.register_entry_type::<PersonaSocialTurnTerminalDocument>()?;
cache.register_entry_type::<PersonaSocialQuarantineDocument>()?;
cache.register_entry_type::<PersonaSocialRetentionHeadDocument>()?;
cache.register_entry_type::<PersonaSocialRetentionPlanDocument>()?;
Ok(())
@ -901,7 +837,6 @@ fn is_persona_social_type(document_type: &str) -> bool {
PERSONA_SOCIAL_MENTION_TYPE,
PERSONA_SOCIAL_TURN_REQUEST_TYPE,
PERSONA_SOCIAL_TURN_TERMINAL_TYPE,
PERSONA_SOCIAL_QUARANTINE_TYPE,
PERSONA_SOCIAL_RETENTION_HEAD_TYPE,
PERSONA_SOCIAL_RETENTION_PLAN_TYPE,
]
@ -911,22 +846,19 @@ fn is_persona_social_type(document_type: &str) -> bool {
fn load_persona_social_envelope(cache: &mut CultCache, envelope: CultCacheEnvelope) -> Result<()> {
match envelope.r#type.as_str() {
PERSONA_SOCIAL_MENTION_TYPE => {
cache.load_envelope::<PersonaSocialMentionDocument>(envelope)?;
cache.put_envelope::<PersonaSocialMentionDocument>(envelope)?;
}
PERSONA_SOCIAL_TURN_REQUEST_TYPE => {
cache.load_envelope::<PersonaSocialTurnRequestDocument>(envelope)?;
cache.put_envelope::<PersonaSocialTurnRequestDocument>(envelope)?;
}
PERSONA_SOCIAL_TURN_TERMINAL_TYPE => {
cache.load_envelope::<PersonaSocialTurnTerminalDocument>(envelope)?;
}
PERSONA_SOCIAL_QUARANTINE_TYPE => {
cache.load_envelope::<PersonaSocialQuarantineDocument>(envelope)?;
cache.put_envelope::<PersonaSocialTurnTerminalDocument>(envelope)?;
}
PERSONA_SOCIAL_RETENTION_HEAD_TYPE => {
cache.load_envelope::<PersonaSocialRetentionHeadDocument>(envelope)?;
cache.put_envelope::<PersonaSocialRetentionHeadDocument>(envelope)?;
}
PERSONA_SOCIAL_RETENTION_PLAN_TYPE => {
cache.load_envelope::<PersonaSocialRetentionPlanDocument>(envelope)?;
cache.put_envelope::<PersonaSocialRetentionPlanDocument>(envelope)?;
}
_ => unreachable!("caller filtered Persona social document types"),
}
@ -959,3 +891,50 @@ fn stable_pending_mention_id(
fn now_iso() -> String {
chrono::Utc::now().to_rfc3339()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn persona_social_cache_reads_without_writing_the_store() -> Result<()> {
let temp = tempfile::tempdir()?;
let store = temp.path().join("social.cc");
let cache = persona_social_cache(&store)?;
let head = PersonaSocialRetentionHeadDocument {
head: PersonaConversationRetentionHead::default(),
};
SingleFileMessagePackBackingStore::new(&store).push(
&cache
.prepare_entry(PERSONA_SOCIAL_RETENTION_HEAD_KEY, &head)?
.0,
)?;
let past = std::time::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000_000);
std::fs::File::options()
.write(true)
.open(&store)?
.set_modified(past)?;
let listing = || -> Result<Vec<(std::ffi::OsString, u64, std::time::SystemTime)>> {
let mut entries = std::fs::read_dir(temp.path())?
.map(|entry| {
let entry = entry?;
let metadata = entry.metadata()?;
Ok((entry.file_name(), metadata.len(), metadata.modified()?))
})
.collect::<Result<Vec<_>>>()?;
entries.sort();
Ok(entries)
};
let before = listing()?;
let names = before.iter().map(|entry| entry.0.clone()).collect::<Vec<_>>();
assert_eq!(names, ["social.cc", "social.cc.lock"]);
let reopened = persona_social_cache(&store)?;
assert_eq!(
reopened
.get::<PersonaSocialRetentionHeadDocument>(PERSONA_SOCIAL_RETENTION_HEAD_KEY)?,
Some(head)
);
assert_eq!(listing()?, before, "a read attached or wrote a store");
Ok(())
}
}

View file

@ -574,7 +574,6 @@ fn fallback<'a>(value: &'a str, fallback: &'a str) -> &'a str {
mod tests {
use super::*;
#[test]
fn typed_interpreter_effects_reject_channel_escape_and_mixed_drop() {
let valid = r#"{"schemaVersion":"epiphany.persona_interpreter_effect_set.v0","effects":[{"kind":"state_note","memory_kind":"memory","summary":"The operator expects a native conversational nerve."},{"kind":"say","channel_id":"aquarium","content":"The nerve is live.","speech_act":"status","register":"concise","safety_notes":[]}]}"#;

View file

@ -109,8 +109,8 @@ mod platform {
use chrono::{DateTime, SecondsFormat, Utc};
use std::os::windows::ffi::OsStringExt;
use windows_sys::Win32::Foundation::{
CloseHandle, ERROR_ACCESS_DENIED, ERROR_NO_MORE_FILES, FILETIME,
GetLastError, INVALID_HANDLE_VALUE, WAIT_FAILED, WAIT_OBJECT_0, WAIT_TIMEOUT,
CloseHandle, ERROR_ACCESS_DENIED, ERROR_NO_MORE_FILES, FILETIME, GetLastError,
INVALID_HANDLE_VALUE, WAIT_FAILED, WAIT_OBJECT_0, WAIT_TIMEOUT,
};
use windows_sys::Win32::System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW,
@ -359,17 +359,6 @@ mod platform {
use super::*;
use chrono::{DateTime, SecondsFormat, Utc};
fn is_missing_error(error: &anyhow::Error) -> bool {
error.chain().any(|cause| {
cause
.downcast_ref::<std::io::Error>()
.is_some_and(|error| {
error.kind() == std::io::ErrorKind::NotFound
|| error.raw_os_error() == Some(libc::ESRCH)
})
})
}
fn stat(process_id: u32) -> Result<(u64, char)> {
let raw = std::fs::read_to_string(format!("/proc/{process_id}/stat"))
.with_context(|| format!("read /proc/{process_id}/stat"))?;
@ -386,6 +375,14 @@ mod platform {
Ok((starttime, state))
}
fn is_missing_error(error: &anyhow::Error) -> bool {
error.chain().any(|source| {
source
.downcast_ref::<std::io::Error>()
.is_some_and(|error| error.kind() == std::io::ErrorKind::NotFound)
})
}
fn created_at_rfc3339(starttime: u64) -> Result<String> {
let ticks_per_second = unsafe { libc::sysconf(libc::_SC_CLK_TCK) };
if ticks_per_second <= 0 {
@ -557,21 +554,6 @@ mod tests {
));
}
#[cfg(target_os = "linux")]
#[test]
fn absent_linux_pid_is_missing_not_indeterminate() {
let identity = ProcessInstanceIdentity {
process_id: u32::MAX,
creation_token: 1,
created_at_rfc3339: None,
executable_path: PathBuf::from("/definitely/absent"),
};
assert_eq!(
observe_process_instance(&identity),
ProcessInstanceObservation::Missing
);
}
#[test]
fn child_exit_is_observed_for_the_exact_instance() {
let mut child = if cfg!(windows) {

File diff suppressed because it is too large Load diff

View file

@ -9,22 +9,20 @@ use crate::{
};
pub const REORIENTATION_REQUEST_SCHEMA_VERSION: &str = "epiphany.self.reorientation_request.v1";
pub const REORIENTATION_LAUNCH_BINDING_SCHEMA_VERSION: &str =
"epiphany.runtime.reorientation_launch.v1";
pub const MIND_REORIENTATION_DECISION_SCHEMA_VERSION: &str =
"epiphany.mind.reorientation_decision.v1";
pub const MIND_REORIENTATION_PASS_FAILURE_SCHEMA_VERSION: &str =
"epiphany.mind.reorientation_pass_failure.v1";
"epiphany.mind.reorientation_pass_failure.v2";
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct EpiphanyReorientationStateProjection {
pub runtime_id: String,
pub objective: Option<String>,
pub active_subgoal_id: Option<String>,
pub investigation_checkpoint: Option<epiphany_state_model::EpiphanyInvestigationCheckpoint>,
pub mode: Option<epiphany_state_model::EpiphanyModeState>,
pub investigation_checkpoint: Option<crate::EpiphanyInvestigationCheckpoint>,
pub mode: Option<crate::EpiphanyModeState>,
pub repository_body_observation: Option<crate::RepositoryBodyObservationBasis>,
pub frontier: Vec<epiphany_state_model::RepoFrontierItem>,
pub frontier: Vec<crate::RepoFrontierItem>,
pub projection_digest: String,
}
@ -48,26 +46,6 @@ pub struct EpiphanyReorientationRequest {
pub requested_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "epiphany.runtime.reorientation_launch.v1",
schema = "EpiphanyReorientationLaunchBinding"
)]
pub struct EpiphanyReorientationLaunchBinding {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub binding_id: String,
#[cultcache(key = 2)]
pub request_id: String,
#[cultcache(key = 3)]
pub job_id: String,
#[cultcache(key = 4)]
pub attempt_ordinal: u64,
#[cultcache(key = 5)]
pub launched_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "epiphany.mind.reorientation_decision.v1",
@ -100,7 +78,7 @@ pub struct EpiphanyMindReorientationDecisionDocument {
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "epiphany.mind.reorientation_pass_failure.v1",
type = "epiphany.mind.reorientation_pass_failure.v2",
schema = "EpiphanyMindReorientationPassFailureDocument"
)]
pub struct EpiphanyMindReorientationPassFailureDocument {
@ -113,14 +91,12 @@ pub struct EpiphanyMindReorientationPassFailureDocument {
#[cultcache(key = 3)]
pub job_id: String,
#[cultcache(key = 4)]
pub runtime_result_id: String,
pub model_pass_failure_id: String,
#[cultcache(key = 5)]
pub decision_context_id: String,
#[cultcache(key = 6)]
pub verdict: String,
#[cultcache(key = 7)]
pub summary: String,
#[cultcache(key = 8)]
#[cultcache(key = 7)]
pub failed_at: String,
}
@ -129,6 +105,7 @@ pub struct EpiphanyReorientationWorkProjection {
pub request: EpiphanyReorientationRequest,
#[serde(flatten)]
pub attempt: crate::EpiphanyAgentPassAttemptProjection,
pub terminal_failure: bool,
}
pub fn request_current_reorientation(
@ -138,7 +115,9 @@ pub fn request_current_reorientation(
chrono::DateTime::parse_from_rfc3339(requested_at)
.map_err(|_| anyhow!("reorientation request time is invalid"))?;
let store_path = store_path.as_ref();
let mind = crate::assemble_mind_view(store_path)?;
let mut cache = crate::runtime_spine_cache(store_path)?;
cache.pull_all_backing_stores()?;
let mind = crate::mind_documents::assemble_mind_view_from_cache(&cache)?;
let mut source_documents = mind
.source_documents
.iter()
@ -192,8 +171,6 @@ pub fn request_current_reorientation(
requested_at: requested_at.into(),
};
validate_reorientation_request_intrinsic(&request)?;
let mut cache = crate::runtime_spine_cache(store_path)?;
cache.pull_all_backing_stores()?;
if let Some(existing) = cache.get::<EpiphanyReorientationRequest>(&request_id)? {
validate_reorientation_request_current(&cache, &existing)?;
return Ok(existing);
@ -210,6 +187,70 @@ pub fn request_current_reorientation(
Ok(request)
}
fn reorientation_attempt_ordinal(request_id: &str, job_id: &str) -> Result<usize> {
let prefix = format!("reorientation-{request_id}-attempt-");
job_id
.strip_prefix(&prefix)
.ok_or_else(|| anyhow!("reorientation job identity is not canonical"))?
.parse::<usize>()
.map_err(|_| anyhow!("reorientation attempt ordinal is invalid"))
}
fn reorientation_launch_document(
launch: &crate::EpiphanyRuntimeWorkerLaunchRequest,
) -> Result<crate::EpiphanyReorientWorkerLaunchDocument> {
let document = match launch.launch_document()? {
crate::EpiphanyWorkerLaunchDocument::Reorient(document) => document,
crate::EpiphanyWorkerLaunchDocument::Role(_) => {
return Err(anyhow!("reorientation owner carried a role launch"));
}
};
if launch.role != crate::EPIPHANY_REORIENT_OWNER_ROLE
|| launch.binding_id != crate::EPIPHANY_REORIENT_LAUNCH_BINDING_ID
|| document.schema_version != "epiphany.reorientation_launch_projection.v1"
|| document.request_id.trim().is_empty()
|| document.creation_thread_id != launch.job_id
{
return Err(anyhow!(
"reorientation immutable launch crossed family authority"
));
}
reorientation_attempt_ordinal(&document.request_id, &launch.job_id)?;
Ok(document)
}
fn reorientation_launches(
cache: &CultCache,
request_id: &str,
) -> Result<Vec<(usize, crate::EpiphanyRuntimeWorkerLaunchRequest)>> {
let mut launches = Vec::new();
for launch in cache
.get_all::<crate::EpiphanyRuntimeWorkerLaunchRequest>()?
.into_iter()
.filter(|launch| {
launch.role == crate::EPIPHANY_REORIENT_OWNER_ROLE
|| launch.binding_id == crate::EPIPHANY_REORIENT_LAUNCH_BINDING_ID
})
{
let document = reorientation_launch_document(&launch)?;
if document.request_id == request_id {
launches.push((
reorientation_attempt_ordinal(request_id, &launch.job_id)?,
launch,
));
}
}
launches.sort_by_key(|(ordinal, _)| *ordinal);
for (expected, (ordinal, _)) in launches.iter().enumerate() {
if *ordinal != expected {
return Err(anyhow!(
"reorientation request has noncontiguous attempt identity"
));
}
}
Ok(launches)
}
pub(crate) fn current_reorientation_work(
cache: &CultCache,
) -> Result<Option<EpiphanyReorientationWorkProjection>> {
@ -225,52 +266,58 @@ pub(crate) fn current_reorientation_work(
{
continue;
}
let mut bindings = cache
.get_all::<EpiphanyReorientationLaunchBinding>()?
.into_iter()
.filter(|binding| binding.request_id == request.request_id)
.collect::<Vec<_>>();
bindings.sort_by_key(|binding| binding.attempt_ordinal);
let Some(binding) = bindings.last() else {
let launches = reorientation_launches(cache, &request.request_id)?;
let Some((_, launch)) = launches.last() else {
return Ok(Some(EpiphanyReorientationWorkProjection {
request,
attempt: crate::EpiphanyAgentPassAttemptProjection::unattempted(),
terminal_failure: false,
}));
};
let job = cache
.get::<crate::EpiphanyRuntimeJob>(&binding.job_id)?
.get::<crate::EpiphanyRuntimeJob>(&launch.job_id)?
.ok_or_else(|| anyhow!("reorientation launch lost its runtime job"))?;
let result = cache.get::<crate::EpiphanyRuntimeReorientWorkerResult>(&binding.job_id)?;
let result = cache.get::<crate::EpiphanyRuntimeReorientWorkerResult>(&launch.job_id)?;
let terminal_failure = job.status == EpiphanyRuntimeJobStatus::Failed;
let action = match job.status {
EpiphanyRuntimeJobStatus::Completed if result.is_some() => {
EpiphanyAgentPassContinuationAction::Review
}
EpiphanyRuntimeJobStatus::Failed => {
let terminal = exact_runtime_job_result(cache, &binding.job_id)?;
match terminal.decision_context_id.as_deref() {
Some(context_id)
let model_failures = cache
.get_all::<crate::EpiphanyModelPassFailure>()?
.into_iter()
.filter(|failure| failure.pass_id == launch.job_id)
.collect::<Vec<_>>();
match model_failures.as_slice() {
[] => EpiphanyAgentPassContinuationAction::Launch,
[model_failure]
if failures.iter().any(|failure| {
failure.request_id == request.request_id
&& failure.job_id == binding.job_id
&& failure.runtime_result_id == terminal.result_id
&& failure.decision_context_id == context_id
&& failure.job_id == launch.job_id
&& failure.model_pass_failure_id == model_failure.failure_id
&& failure.decision_context_id == model_failure.decision_context_id
}) =>
{
EpiphanyAgentPassContinuationAction::Launch
}
Some(_) => EpiphanyAgentPassContinuationAction::Review,
None => EpiphanyAgentPassContinuationAction::Launch,
[_] => EpiphanyAgentPassContinuationAction::Review,
_ => {
return Err(anyhow!(
"reorientation pass has multiple model-failure authorities"
));
}
}
}
EpiphanyRuntimeJobStatus::Cancelled => EpiphanyAgentPassContinuationAction::Launch,
_ => EpiphanyAgentPassContinuationAction::Wait,
};
return Ok(Some(EpiphanyReorientationWorkProjection {
request,
attempt: crate::EpiphanyAgentPassAttemptProjection::with(
action,
Some(binding.job_id.clone()),
Some(launch.job_id.clone()),
),
terminal_failure,
}));
}
Ok(None)
@ -283,13 +330,12 @@ pub fn record_reorientation_pass_failure(
let store_path = store_path.as_ref();
let mut cache = crate::runtime_spine_cache(store_path)?;
cache.pull_all_backing_stores()?;
let binding = cache
.get_all::<EpiphanyReorientationLaunchBinding>()?
.into_iter()
.find(|binding| binding.job_id == job_id)
.ok_or_else(|| anyhow!("reorientation failure has no exact launch binding"))?;
let launch = cache
.get::<crate::EpiphanyRuntimeWorkerLaunchRequest>(job_id)?
.ok_or_else(|| anyhow!("reorientation failure has no immutable launch"))?;
let launch_document = reorientation_launch_document(&launch)?;
let request = cache
.get::<EpiphanyReorientationRequest>(&binding.request_id)?
.get::<EpiphanyReorientationRequest>(&launch_document.request_id)?
.ok_or_else(|| anyhow!("reorientation failure lost its request"))?;
validate_reorientation_request_intrinsic(&request)?;
let job = cache
@ -298,21 +344,24 @@ pub fn record_reorientation_pass_failure(
if job.status != EpiphanyRuntimeJobStatus::Failed {
return Err(anyhow!("reorientation pass is not a failed model pass"));
}
let terminal = exact_runtime_job_result(&cache, job_id)?;
let decision_context_id = terminal
.decision_context_id
.as_deref()
.ok_or_else(|| anyhow!("model-backed reorientation failure has no decision context"))?;
let model_failures = cache
.get_all::<crate::EpiphanyModelPassFailure>()?
.into_iter()
.filter(|failure| failure.pass_id == job_id)
.collect::<Vec<_>>();
let [model_failure] = model_failures.as_slice() else {
return Err(anyhow!(
"reorientation failure requires one exact model-pass failure"
));
};
let context = cache
.get::<crate::EpiphanyDecisionContext>(decision_context_id)?
.get::<crate::EpiphanyDecisionContext>(&model_failure.decision_context_id)?
.ok_or_else(|| anyhow!("reorientation failure lost its decision context"))?;
let basis = cache
.get::<crate::EpiphanyReasoningBasis>(&context.basis_id)?
.ok_or_else(|| anyhow!("reorientation failure lost its reasoning basis"))?;
context.validate(&basis)?;
let model_failure =
crate::model_pass_failure_for_request(store_path, &context.terminal_request_id)?
.ok_or_else(|| anyhow!("reorientation failure has no exact model-pass failure"))?;
model_failure.validate(&basis, &context)?;
if model_failure.decision_context_id != context.context_id {
return Err(anyhow!(
"reorientation failure crossed its model-pass failure context"
@ -325,14 +374,13 @@ pub fn record_reorientation_pass_failure(
}
let failure = EpiphanyMindReorientationPassFailureDocument {
schema_version: MIND_REORIENTATION_PASS_FAILURE_SCHEMA_VERSION.into(),
failure_id: format!("reorientation-pass-failure-{}", terminal.result_id),
failure_id: format!("reorientation-pass-failure-{}", model_failure.failure_id),
request_id: request.request_id.clone(),
job_id: job_id.into(),
runtime_result_id: terminal.result_id.clone(),
decision_context_id: decision_context_id.into(),
verdict: terminal.verdict.clone(),
summary: terminal.summary.clone(),
failed_at: terminal.completed_at.clone(),
model_pass_failure_id: model_failure.failure_id.clone(),
decision_context_id: context.context_id.clone(),
summary: model_failure.summary.clone(),
failed_at: model_failure.failed_at.clone(),
};
if let Some(existing) =
cache.get::<EpiphanyMindReorientationPassFailureDocument>(&failure.failure_id)?
@ -340,7 +388,7 @@ pub fn record_reorientation_pass_failure(
if existing != failure {
return Err(anyhow!("reorientation failure replay was substituted"));
}
return mind_receipt_for_failure_context(&cache, decision_context_id);
return mind_receipt_for_failure_context(&cache, &context.context_id);
}
let snapshot = cache.snapshot_envelopes();
let mut strong_reads = Vec::new();
@ -349,17 +397,12 @@ pub fn record_reorientation_pass_failure(
EpiphanyReorientationRequest::TYPE,
request.request_id.as_str(),
),
(
EpiphanyReorientationLaunchBinding::TYPE,
binding.binding_id.as_str(),
),
(crate::EpiphanyRuntimeWorkerLaunchRequest::TYPE, job_id),
(crate::EpiphanyRuntimeJob::TYPE, job_id),
(
crate::EpiphanyRuntimeJobResult::TYPE,
terminal.result_id.as_str(),
crate::EpiphanyDecisionContext::TYPE,
context.context_id.as_str(),
),
(crate::EpiphanyDecisionContext::TYPE, decision_context_id),
(
crate::EpiphanyModelPassFailure::TYPE,
model_failure.failure_id.as_str(),
@ -375,14 +418,13 @@ pub fn record_reorientation_pass_failure(
}
let write =
crate::mind_documents::prepare_mind_document(&cache, &failure.failure_id, &failure)?;
match crate::reasoning_context::commit_mind_mutation_with_derived_companions(
match crate::reasoning_context::commit_mind_mutation(
store_path,
decision_context_id,
&context.context_id,
"Continuity.reorientation_failure",
strong_reads,
vec![write],
Vec::new(),
&terminal.completed_at,
&model_failure.failed_at,
)? {
crate::EpiphanyMindCommitOutcome::Committed(receipt) => Ok(receipt),
crate::EpiphanyMindCommitOutcome::Conflict {
@ -408,11 +450,8 @@ pub fn launch_current_reorientation_work(
return Err(anyhow!("reorientation request is not launchable"));
}
validate_reorientation_request_current(&cache, &work.request)?;
let attempt_ordinal = cache
.get_all::<EpiphanyReorientationLaunchBinding>()?
.into_iter()
.filter(|binding| binding.request_id == work.request.request_id)
.count() as u64;
let prior_launches = reorientation_launches(&cache, &work.request.request_id)?;
let attempt_ordinal = prior_launches.len();
let job_id = format!(
"reorientation-{}-attempt-{attempt_ordinal}",
work.request.request_id
@ -429,9 +468,6 @@ pub fn launch_current_reorientation_work(
&cache,
crate::RuntimeSpineHeartbeatJobOptions {
runtime_id: work.request.runtime_id.clone(),
session_id: crate::EPIPHANY_RUNTIME_ROOT_SESSION_ID.into(),
objective: "Reorient from one exact keyed Mind projection".into(),
coordinator_note: "Reorientation current-work owner opened this pass.".into(),
job_id: job_id.clone(),
role: crate::EPIPHANY_REORIENT_OWNER_ROLE.into(),
binding_id: crate::EPIPHANY_REORIENT_LAUNCH_BINDING_ID.into(),
@ -439,39 +475,61 @@ pub fn launch_current_reorientation_work(
instruction: "Decide resume or regather from only the sealed typed continuity projection. Return the reorientation result contract; do not invent live state.".into(),
launch_document,
output_contract_id: output_contract_id.clone(),
proposal_modeling_request_id: None,
frontier_planning_request_id: None,
frontier_plan_mind_request_id: None,
imagination_consideration_request_id: None,
admitted_model_direction_consideration_request_id: None,
repo_frontier_modeling_request_id: None,
repo_frontier_research_request_id: None,
repo_frontier_verification_request_id: None,
created_at: created_at.into(),
},
)?;
let binding = EpiphanyReorientationLaunchBinding {
schema_version: REORIENTATION_LAUNCH_BINDING_SCHEMA_VERSION.into(),
binding_id: format!("reorientation-launch-{job_id}"),
request_id: work.request.request_id.clone(),
job_id: job_id.clone(),
attempt_ordinal,
launched_at: created_at.into(),
};
let expected = exact_source_envelopes(&cache, &work.request.source_documents)?
let mut expected = exact_source_envelopes(&cache, &work.request.source_documents)?
.into_iter()
.chain(
cache
.get_envelope::<EpiphanyReorientationRequest>(&work.request.request_id)?
.into_iter(),
)
.collect();
.collect::<Vec<_>>();
for (_, launch) in &prior_launches {
expected.push(
cache
.get_envelope::<crate::EpiphanyRuntimeWorkerLaunchRequest>(&launch.job_id)?
.ok_or_else(|| anyhow!("reorientation retry lost its prior launch envelope"))?,
);
}
if let Some((_, latest)) = prior_launches.last() {
expected.push(
cache
.get_envelope::<crate::EpiphanyRuntimeJob>(&latest.job_id)?
.ok_or_else(|| anyhow!("reorientation retry lost its prior job envelope"))?,
);
for failure in cache
.get_all::<EpiphanyMindReorientationPassFailureDocument>()?
.into_iter()
.filter(|failure| {
failure.request_id == work.request.request_id && failure.job_id == latest.job_id
})
{
expected.push(
cache
.get_envelope::<EpiphanyMindReorientationPassFailureDocument>(
&failure.failure_id,
)?
.ok_or_else(|| anyhow!("reorientation retry lost its failure envelope"))?,
);
expected.push(
cache
.get_envelope::<crate::EpiphanyModelPassFailure>(
&failure.model_pass_failure_id,
)?
.ok_or_else(|| {
anyhow!("reorientation retry lost its model-failure envelope")
})?,
);
}
}
crate::current_work::commit_current_work_launch(
store_path,
&cache,
expected,
prepared,
vec![cache.prepare_entry(&binding.binding_id, &binding)?.0],
Vec::new(),
"reorientation",
)?;
Ok(job_id)
@ -487,13 +545,12 @@ pub fn accept_reorientation_result(
let store_path = store_path.as_ref();
let mut cache = crate::runtime_spine_cache(store_path)?;
cache.pull_all_backing_stores()?;
let binding = cache
.get_all::<EpiphanyReorientationLaunchBinding>()?
.into_iter()
.find(|binding| binding.job_id == job_id)
.ok_or_else(|| anyhow!("reorientation result has no exact launch binding"))?;
let launch = cache
.get::<crate::EpiphanyRuntimeWorkerLaunchRequest>(job_id)?
.ok_or_else(|| anyhow!("reorientation result has no immutable launch"))?;
let launch_document = reorientation_launch_document(&launch)?;
let request = cache
.get::<EpiphanyReorientationRequest>(&binding.request_id)?
.get::<EpiphanyReorientationRequest>(&launch_document.request_id)?
.ok_or_else(|| anyhow!("reorientation result lost its request"))?;
validate_reorientation_request_intrinsic(&request)?;
let job = cache
@ -554,23 +611,12 @@ pub fn accept_reorientation_result(
next_safe_move: result.next_safe_move.clone(),
decided_at: accepted_at.into(),
};
let finding = crate::interpret_runtime_reorient_worker_result(&result);
let recovery = crate::continuity_recovery_receipt_from_reorient_finding(
format!("continuity-recovery-{}", request.request_id),
crate::EPIPHANY_REORIENT_LAUNCH_BINDING_ID.into(),
&finding,
accepted_at.into(),
);
let mut strong_reads = exact_source_envelopes(&cache, &request.source_documents)?;
for (document_type, document_key) in [
(
EpiphanyReorientationRequest::TYPE,
request.request_id.as_str(),
),
(
EpiphanyReorientationLaunchBinding::TYPE,
binding.binding_id.as_str(),
),
(crate::EpiphanyRuntimeWorkerLaunchRequest::TYPE, job_id),
(crate::EpiphanyRuntimeJob::TYPE, job_id),
(crate::EpiphanyRuntimeReorientWorkerResult::TYPE, job_id),
@ -589,14 +635,12 @@ pub fn accept_reorientation_result(
}
let write =
crate::mind_documents::prepare_mind_document(&cache, &decision.decision_id, &decision)?;
let companion = cache.prepare_entry(&recovery.receipt_id, &recovery)?.0;
match crate::reasoning_context::commit_mind_mutation_with_derived_companions(
match crate::reasoning_context::commit_mind_mutation(
store_path,
&result.decision_context_id,
"Continuity.reorientation",
strong_reads,
vec![write],
vec![companion],
accepted_at,
)? {
crate::EpiphanyMindCommitOutcome::Committed(receipt) => Ok(receipt),
@ -826,26 +870,6 @@ fn mind_receipt_for_failure_context(
}
}
fn exact_runtime_job_result(
cache: &CultCache,
job_id: &str,
) -> Result<crate::EpiphanyRuntimeJobResult> {
let mut results = cache
.get_all::<crate::EpiphanyRuntimeJobResult>()?
.into_iter()
.filter(|result| result.job_id == job_id)
.collect::<Vec<_>>();
match results.len() {
1 => Ok(results.remove(0)),
0 => Err(anyhow!(
"reorientation attempt lost its terminal runtime result"
)),
_ => Err(anyhow!(
"reorientation attempt has multiple terminal runtime results"
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -934,22 +958,11 @@ mod tests {
)?);
crate::open_runtime_model_execution(
&store,
crate::RuntimeSpineSessionOptions {
session_id: "reorientation-model-session".into(),
objective: "Reorient from exact Mind state".into(),
created_at: "2026-08-18T10:00:03Z".into(),
coordinator_note: "test".into(),
},
crate::RuntimeSpineJobOptions {
job_id: "reorientation-model-job".into(),
session_id: "reorientation-model-session".into(),
role: "openai-model".into(),
created_at: "2026-08-18T10:00:03Z".into(),
summary: "Bound reorientation inference".into(),
artifact_refs: Vec::new(),
},
&native,
"2026-08-18T10:00:03Z",
)?;
let context = crate::EpiphanyDecisionContext::new(&basis, native, Vec::new())?;
crate::put_decision_context(&store, &context)?;
@ -975,13 +988,12 @@ mod tests {
"2026-08-18T10:00:05Z",
)?;
let result = crate::EpiphanyRuntimeReorientWorkerResult {
schema_version: crate::RUNTIME_REORIENT_WORKER_RESULT_SCHEMA_VERSION.into(),
result_id: "reorientation-result".into(),
job_id: job_id.clone(),
mode: "resume".into(),
summary: "The sealed checkpoint remains coherent.".into(),
next_safe_move: "Continue the keyed migration.".into(),
checkpoint_still_valid: Some(true),
mode: "regather".into(),
summary: "The sealed checkpoint requires fresh external evidence.".into(),
next_safe_move: "Regather before resuming the keyed migration.".into(),
checkpoint_still_valid: Some(false),
files_inspected: Vec::new(),
frontier_node_ids: Vec::new(),
evidence_ids: Vec::new(),
@ -993,26 +1005,29 @@ mod tests {
decision_context_id: context.context_id.clone(),
};
crate::put_runtime_reorient_worker_result(&store, &result)?;
crate::complete_runtime_job(
&store,
crate::RuntimeSpineJobResultOptions {
result_id: format!("runtime-result-{job_id}"),
job_id: job_id.clone(),
completed_at: "2026-08-18T10:00:06Z".into(),
verdict: "resume".into(),
summary: result.summary.clone(),
next_safe_move: result.next_safe_move.clone(),
evidence_refs: Vec::new(),
artifact_refs: Vec::new(),
decision_context_id: Some(context.context_id.clone()),
},
)?;
let projected_result = crate::read_runtime_reorient_result(&store, &job_id)?;
assert_eq!(
projected_result.status,
crate::EpiphanyReorientResultStatus::Completed
);
assert_eq!(
projected_result.finding.and_then(|finding| finding.mode),
Some("regather".into())
);
let review_work = crate::project_current_work(&store)?
.reorientation
.expect("completed Reorientation work");
assert_eq!(
review_work.attempt.action,
EpiphanyAgentPassContinuationAction::Review
);
assert!(!review_work.terminal_failure);
cache.pull_all_backing_stores()?;
cache.put(
"unrelated-observation",
&crate::EpiphanyMindObservationDocument {
value: epiphany_state_model::EpiphanyObservation {
value: crate::EpiphanyObservation {
id: "unrelated-observation".into(),
summary: "Persona observed an unrelated social fact.".into(),
..Default::default()
@ -1028,7 +1043,17 @@ mod tests {
accept_reorientation_result(&store, &job_id, "2026-08-18T10:00:07Z")?,
receipt
);
assert!(crate::project_current_work(&store)?.reorientation.is_none());
let current_work = crate::project_current_work(&store)?;
assert!(current_work.reorientation.is_none());
assert!(current_work.operator_regather_required);
assert_eq!(
crate::recommend_coordinator_action(crate::EpiphanyCoordinatorInput {
mind_present: true,
current_work,
})
.action,
crate::EpiphanyCoordinatorAction::RegatherManually
);
let mut accepted = crate::runtime_spine_cache(&store)?;
accepted.pull_all_backing_stores()?;
assert!(
@ -1068,22 +1093,11 @@ mod tests {
)?);
crate::open_runtime_model_execution(
&store,
crate::RuntimeSpineSessionOptions {
session_id: "failed-reorientation-model-session".into(),
objective: "Fail from exact Mind state".into(),
created_at: "2026-08-18T10:00:10Z".into(),
coordinator_note: "test".into(),
},
crate::RuntimeSpineJobOptions {
job_id: "failed-reorientation-model-job".into(),
session_id: "failed-reorientation-model-session".into(),
role: "openai-model".into(),
created_at: "2026-08-18T10:00:10Z".into(),
summary: "Bound failing inference".into(),
artifact_refs: Vec::new(),
},
&failed_native,
"2026-08-18T10:00:10Z",
)?;
let failed_context =
crate::EpiphanyDecisionContext::new(&failed_basis, failed_native, Vec::new())?;
@ -1119,27 +1133,21 @@ mod tests {
failed_activation,
"2026-08-18T10:00:12Z",
)?;
crate::complete_runtime_job(
crate::terminalize_runtime_job(
&store,
crate::RuntimeSpineJobResultOptions {
result_id: format!("runtime-result-{failed_job}"),
job_id: failed_job.clone(),
completed_at: "2026-08-18T10:00:14Z".into(),
verdict: "failed".into(),
summary: "Provider failed after the terminal request was sealed.".into(),
next_safe_move: "Review the typed failure, then retry.".into(),
evidence_refs: Vec::new(),
artifact_refs: Vec::new(),
decision_context_id: Some(failed_context.context_id.clone()),
},
&failed_job,
EpiphanyRuntimeJobStatus::Failed,
"2026-08-18T10:00:14Z",
&model_failure.failure_id,
)?;
let failed_review = crate::project_current_work(&store)?
.reorientation
.expect("failed Reorientation work");
assert_eq!(
crate::project_current_work(&store)?
.reorientation
.as_ref()
.map(|work| work.attempt.action),
Some(EpiphanyAgentPassContinuationAction::Review)
failed_review.attempt.action,
EpiphanyAgentPassContinuationAction::Review
);
assert!(failed_review.terminal_failure);
let failure_receipt = record_reorientation_pass_failure(&store, &failed_job)?;
assert_eq!(
record_reorientation_pass_failure(&store, &failed_job)?,

View file

@ -1,12 +1,12 @@
use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use anyhow::{Result, anyhow};
use cultcache_rs::{CultCache, CultCacheEnvelope, DatabaseEntry};
use epiphany_state_model::{
use crate::state_model::{
EpiphanyMemoryDomain, EpiphanyMemoryEdge, EpiphanyMemoryGraphSnapshot, EpiphanyMemoryNode,
RepoFrontierItem,
};
use anyhow::{Result, anyhow};
use cultcache_rs::{CultCache, CultCacheEnvelope, DatabaseEntry};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
@ -179,15 +179,6 @@ impl EpiphanyRepoModelBasis {
Ok(())
}
pub fn validate_current(&self, store_path: impl AsRef<Path>) -> Result<()> {
self.validate()?;
let current = assemble_repo_model_view(store_path)?.reasoning_basis();
if current != *self {
return Err(anyhow!("RepoModel basis is stale"));
}
Ok(())
}
pub fn validate_against_cache(&self, cache: &CultCache) -> Result<()> {
self.validate()?;
let live = cache.snapshot_envelopes();
@ -835,7 +826,7 @@ pub fn plan_repo_model_mutation(
{
insert_strong_envelope(&mut strong, existing)?;
}
node.lifecycle = epiphany_state_model::EpiphanyMemoryLifecycle::Retired;
node.lifecycle = crate::EpiphanyMemoryLifecycle::Retired;
nodes.insert(node_id.clone(), node.clone());
insert_envelope(
&mut writes,
@ -879,7 +870,7 @@ pub fn plan_repo_model_mutation(
.get_envelope::<EpiphanyRepoModelEdgeDocument>(edge_id)?
.ok_or_else(|| anyhow!("RepoModel edge lost its envelope"))?;
insert_strong_envelope(&mut strong, existing)?;
edge.lifecycle = epiphany_state_model::EpiphanyMemoryLifecycle::Retired;
edge.lifecycle = crate::EpiphanyMemoryLifecycle::Retired;
edges.insert(edge_id.clone(), edge.clone());
insert_envelope(
&mut writes,
@ -1319,9 +1310,9 @@ fn require_semantic_id(value: &str, label: &str) -> Result<()> {
fn frontier_is_unresolved(item: &RepoFrontierItem) -> bool {
matches!(
item.status,
epiphany_state_model::RepoFrontierStatus::Proposed
| epiphany_state_model::RepoFrontierStatus::Active
| epiphany_state_model::RepoFrontierStatus::Blocked
crate::RepoFrontierStatus::Proposed
| crate::RepoFrontierStatus::Active
| crate::RepoFrontierStatus::Blocked
)
}
@ -1509,9 +1500,9 @@ fn validate_claim_obligations(
if !item.target_claim_ids.contains(&obligation.node_id)
|| !matches!(
item.status,
epiphany_state_model::RepoFrontierStatus::Proposed
| epiphany_state_model::RepoFrontierStatus::Active
| epiphany_state_model::RepoFrontierStatus::Blocked
crate::RepoFrontierStatus::Proposed
| crate::RepoFrontierStatus::Active
| crate::RepoFrontierStatus::Blocked
)
{
return Err(anyhow!("RepoModel claim obligation is not live"));
@ -1521,9 +1512,9 @@ fn validate_claim_obligations(
for item in frontier.iter().filter(|item| {
matches!(
item.status,
epiphany_state_model::RepoFrontierStatus::Proposed
| epiphany_state_model::RepoFrontierStatus::Active
| epiphany_state_model::RepoFrontierStatus::Blocked
crate::RepoFrontierStatus::Proposed
| crate::RepoFrontierStatus::Active
| crate::RepoFrontierStatus::Blocked
)
}) {
for node_id in &item.target_claim_ids {
@ -1541,10 +1532,10 @@ fn validate_claim_obligations(
#[cfg(test)]
mod tests {
use super::*;
use crate::{RuntimeSpineInitOptions, initialize_runtime_spine};
use epiphany_state_model::{
use crate::state_model::{
EpiphanyMemoryEdgeKind, EpiphanyMemoryLifecycle, EpiphanyMemoryNodeKind, RepoFrontierStatus,
};
use crate::{RuntimeSpineInitOptions, initialize_runtime_spine};
fn bind_test_body(
store: &Path,
@ -2251,7 +2242,10 @@ mod tests {
source.document_type == crate::AtlasSurfaceOffer::TYPE
&& source.document_key == offer_id.to_string()
}));
offer_view.reasoning_basis().validate_current(&store)?;
cache.pull_all_backing_stores()?;
offer_view
.reasoning_basis()
.validate_against_cache(&cache)?;
let eve = crate::AtlasRepositoryIdentity::new("gamecult-local", "eve")?;
let eve_surface = uuid::Uuid::new_v4();

View file

@ -33,58 +33,7 @@ use cultcache_rs::DatabaseEntry;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
pub const REPO_FRONTIER_ROUTE_SCHEMA_VERSION: &str = "epiphany.self.repo_frontier_route.v2";
pub const REPO_FRONTIER_HANDS_AUTHORITY_SCHEMA_VERSION: &str =
"epiphany.hands.repo_frontier_authority.v0";
pub const REPO_FRONTIER_ROUTE_CONTRACT: &str = "epiphany.repo_frontier_route.v2";
pub const REPO_FRONTIER_HANDS_AUTHORITY_CONTRACT: &str =
"epiphany.repo_frontier_hands_authority.v0";
pub const REPO_FRONTIER_MODELING_REQUEST_SCHEMA_VERSION: &str =
"epiphany.modeling.repo_frontier_verdict_request.v1";
pub const REPO_FRONTIER_MODELING_REQUEST_CONTRACT: &str =
"epiphany.repo_frontier_verdict_modeling_request.v1";
pub const REPO_FRONTIER_WORK_PROPOSAL_SCHEMA_VERSION: &str =
"epiphany.repo_frontier_work_proposal.v3";
pub const REPO_FRONTIER_PLANNING_REQUEST_SCHEMA_VERSION: &str =
"epiphany.self.repo_frontier_planning_request.v4";
pub const REPO_FRONTIER_PLAN_CANDIDATE_SCHEMA_VERSION: &str =
"epiphany.imagination.repo_frontier_plan_candidate.v0";
pub const REPO_FRONTIER_PLAN_DECISION_RECEIPT_SCHEMA_VERSION: &str =
"epiphany.mind.repo_frontier_plan_decision_receipt.v2";
pub const REPO_FRONTIER_PLAN_DECISION_CONTRACT: &str = "epiphany.repo_frontier_plan_decision.v0";
pub const REPO_FRONTIER_PLANNING_CONTRACT: &str = "epiphany.repo_frontier_planning.v4";
pub const REPO_FRONTIER_PLANNING_FAILURE_REVIEW_SCHEMA_VERSION: &str =
"epiphany.self.repo_frontier_planning_failure_review.v0";
pub const REPO_FRONTIER_RESEARCH_REQUEST_SCHEMA_VERSION: &str =
"epiphany.self.repo_frontier_research_request.v3";
pub const REPO_FRONTIER_RESEARCH_REQUEST_CONTRACT: &str =
"epiphany.repo_frontier_research_request.v3";
pub const REPO_FRONTIER_WORK_PROPOSAL_CONTRACT: &str =
"epiphany.repo_frontier_work_proposal.inert.v3";
pub const REPO_FRONTIER_AUTONOMOUS_PROPOSAL_BINDING_SCHEMA_VERSION: &str =
"epiphany.self.repo_frontier_autonomous_proposal_binding.v2";
pub const RUNTIME_REPOSITORY_DOMAIN_BINDING_KEY: &str = "runtime-repository-domain-binding";
pub const RUNTIME_REPOSITORY_DOMAIN_BINDING_SCHEMA_VERSION: &str =
"epiphany.runtime.repository_domain_binding.v0";
pub const RUNTIME_REPOSITORY_DOMAIN_BINDING_CONTRACT: &str = "deployment configuration binds one organizational repository name to one exact authenticated repository Body; Self may consume but not relabel it";
pub const REPO_FRONTIER_PROPOSAL_MODELING_REQUEST_SCHEMA_VERSION: &str =
"epiphany.coordinator.repo_frontier_proposal_modeling_request.v0";
pub const REPO_FRONTIER_PROPOSAL_MODELING_REQUEST_CONTRACT: &str =
"epiphany.repo_frontier_proposal_modeling_request.v0";
pub const REPO_FRONTIER_PROPOSAL_MODELING_LAUNCH_BINDING_SCHEMA_VERSION: &str =
"epiphany.coordinator.repo_frontier_proposal_modeling_launch_binding.v2";
pub const REPO_FRONTIER_PLANNING_LAUNCH_BINDING_SCHEMA_VERSION: &str =
"epiphany.coordinator.repo_frontier_planning_launch_binding.v0";
pub const REPO_FRONTIER_PLANNING_LAUNCH_BINDING_CONTRACT: &str =
"epiphany.repo_frontier_planning_launch_binding.v0";
pub const REPO_FRONTIER_PLAN_MIND_REQUEST_SCHEMA_VERSION: &str =
"epiphany.self.repo_frontier_plan_mind_request.v1";
pub const REPO_FRONTIER_PLAN_MIND_REQUEST_CONTRACT: &str =
"epiphany.repo_frontier_plan_mind_request.v1";
pub const REPO_FRONTIER_PLAN_MIND_LAUNCH_BINDING_SCHEMA_VERSION: &str =
"epiphany.coordinator.repo_frontier_plan_mind_launch_binding.v0";
pub const REPO_FRONTIER_PLAN_MIND_LAUNCH_BINDING_CONTRACT: &str =
"epiphany.repo_frontier_plan_mind_launch_binding.v0";
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
@ -92,8 +41,6 @@ pub const REPO_FRONTIER_PLAN_MIND_LAUNCH_BINDING_CONTRACT: &str =
schema = "RepoFrontierWorkProposal"
)]
pub struct RepoFrontierWorkProposal {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub proposal_id: String,
#[cultcache(key = 2)]
@ -106,8 +53,6 @@ pub struct RepoFrontierWorkProposal {
pub constraints: Vec<String>,
#[cultcache(key = 6)]
pub evidence_refs: Vec<String>,
#[cultcache(key = 7)]
pub contract: String,
}
pub fn repo_frontier_proposal_payload_sha256(
@ -120,26 +65,6 @@ pub fn repo_frontier_proposal_payload_sha256(
Ok(format!("{:x}", Sha256::digest(content)))
}
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "epiphany.self.repo_frontier_autonomous_proposal_binding",
schema = "RepoFrontierAutonomousProposalBinding"
)]
pub struct RepoFrontierAutonomousProposalBinding {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub direction_result_id: String,
#[cultcache(key = 2)]
pub option_ordinal: u32,
#[cultcache(key = 3)]
pub direction_worker_job_id: String,
#[cultcache(key = 4)]
pub direction_worker_result_sha256: String,
#[cultcache(key = 5)]
pub direction_worker_launch_sha256: String,
}
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "epiphany.runtime.repository_domain_binding",
@ -147,23 +72,9 @@ pub struct RepoFrontierAutonomousProposalBinding {
)]
pub struct RuntimeRepositoryDomainBinding {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub binding_id: String,
#[cultcache(key = 2)]
pub repository_full_name: String,
#[cultcache(key = 3)]
pub runtime_id: String,
#[cultcache(key = 4)]
pub swarm_id: String,
#[cultcache(key = 5)]
pub workspace_id: String,
#[cultcache(key = 6)]
#[cultcache(key = 1)]
pub body_binding_sha256: String,
#[cultcache(key = 7)]
pub bound_at: String,
#[cultcache(key = 8)]
pub contract: String,
}
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
@ -172,8 +83,6 @@ pub struct RuntimeRepositoryDomainBinding {
schema = "RepoFrontierProposalModelingRequest"
)]
pub struct RepoFrontierProposalModelingRequest {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub request_id: String,
#[cultcache(key = 2)]
@ -190,24 +99,12 @@ pub struct RepoFrontierProposalModelingRequest {
pub workspace: String,
#[cultcache(key = 8)]
pub selected_at: String,
#[cultcache(key = 9)]
pub contract: String,
}
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "epiphany.coordinator.repo_frontier_proposal_modeling_launch_binding",
schema = "RepoFrontierProposalModelingLaunchBinding"
)]
pub struct RepoFrontierProposalModelingLaunchBinding {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub proposal_modeling_request_id: String,
#[cultcache(key = 2)]
pub job_id: String,
#[cultcache(key = 3)]
pub worker_launch_document_sha256: String,
#[cultcache(key = 10)]
pub direction_result_id: String,
#[cultcache(key = 11)]
pub direction_option_ordinal: u32,
#[cultcache(key = 12)]
pub direction_worker_job_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
@ -216,8 +113,6 @@ pub struct RepoFrontierProposalModelingLaunchBinding {
schema = "RepoFrontierPlanningRequest"
)]
pub struct RepoFrontierPlanningRequest {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub request_id: String,
#[cultcache(key = 2)]
@ -234,8 +129,6 @@ pub struct RepoFrontierPlanningRequest {
pub repository_scope: Vec<String>,
#[cultcache(key = 8)]
pub requested_at: String,
#[cultcache(key = 9)]
pub contract: String,
#[cultcache(key = 10)]
pub runtime_id: String,
/// Exact current documents that own whether this planning work remains
@ -248,46 +141,12 @@ pub struct RepoFrontierPlanningRequest {
pub claim_obligation_documents: Vec<crate::EpiphanyMindDocumentVersion>,
}
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "epiphany.coordinator.repo_frontier_planning_launch_binding",
schema = "RepoFrontierPlanningLaunchBinding"
)]
pub struct RepoFrontierPlanningLaunchBinding {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub binding_record_id: String,
#[cultcache(key = 2)]
pub planning_request_id: String,
#[cultcache(key = 3)]
pub job_id: String,
#[cultcache(key = 4)]
pub binding_id: String,
#[cultcache(key = 5)]
pub runtime_id: String,
#[cultcache(key = 6)]
pub thread_id: String,
#[cultcache(key = 7)]
pub launched_at: String,
#[cultcache(key = 8)]
pub worker_launch_document_sha256: String,
#[cultcache(key = 9)]
pub contract: String,
#[cultcache(key = 10, default)]
pub attempt_ordinal: u64,
#[cultcache(key = 11, default)]
pub superseded_failure_result_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "epiphany.self.repo_frontier_research_request",
schema = "RepoFrontierResearchRequest"
)]
pub struct RepoFrontierResearchRequest {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub request_id: String,
#[cultcache(key = 2)]
@ -304,8 +163,6 @@ pub struct RepoFrontierResearchRequest {
pub requested_at: String,
#[cultcache(key = 8)]
pub runtime_id: String,
#[cultcache(key = 10)]
pub contract: String,
/// Immutable public source identities already owned by the admitted
/// frontier. This is dedicated retrieval authority, not worker-authored
/// evidence or a search hint.
@ -324,8 +181,6 @@ pub struct RepoFrontierResearchRequest {
schema = "RepoFrontierPlanCandidate"
)]
pub struct RepoFrontierPlanCandidate {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub candidate_id: String,
#[cultcache(key = 2)]
@ -354,8 +209,6 @@ pub struct RepoFrontierPlanCandidate {
pub commit_message: String,
#[cultcache(key = 14)]
pub proposed_at: String,
#[cultcache(key = 15)]
pub contract: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
@ -401,8 +254,6 @@ pub struct RepoFrontierPlanningLifecycle {
schema = "RepoFrontierPlanMindRequest"
)]
pub struct RepoFrontierPlanMindRequest {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub request_id: String,
#[cultcache(key = 2)]
@ -419,40 +270,6 @@ pub struct RepoFrontierPlanMindRequest {
pub runtime_id: String,
#[cultcache(key = 9)]
pub requested_at: String,
#[cultcache(key = 10)]
pub contract: String,
}
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "epiphany.coordinator.repo_frontier_plan_mind_launch_binding",
schema = "RepoFrontierPlanMindLaunchBinding"
)]
pub struct RepoFrontierPlanMindLaunchBinding {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub binding_record_id: String,
#[cultcache(key = 2)]
pub mind_request_id: String,
#[cultcache(key = 3)]
pub job_id: String,
#[cultcache(key = 4)]
pub binding_id: String,
#[cultcache(key = 5)]
pub runtime_id: String,
#[cultcache(key = 6)]
pub thread_id: String,
#[cultcache(key = 7)]
pub launched_at: String,
#[cultcache(key = 8)]
pub worker_launch_document_sha256: String,
#[cultcache(key = 9)]
pub contract: String,
#[cultcache(key = 10, default)]
pub attempt_ordinal: u64,
#[cultcache(key = 11, default)]
pub superseded_failure_result_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@ -473,8 +290,6 @@ pub struct RepoFrontierPlanMindDecision {
schema = "RepoFrontierPlanDecisionReceipt"
)]
pub struct RepoFrontierPlanDecisionReceipt {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub decision_id: String,
#[cultcache(key = 2)]
@ -497,8 +312,6 @@ pub struct RepoFrontierPlanDecisionReceipt {
pub rationale: String,
#[cultcache(key = 11)]
pub decided_at: String,
#[cultcache(key = 12)]
pub contract: String,
#[cultcache(key = 13)]
pub decision_source: RepoFrontierPlanDecisionSource,
}
@ -522,8 +335,6 @@ pub enum RepoFrontierVerdictDisposition {
schema = "RepoFrontierModelingRequest"
)]
pub struct RepoFrontierModelingRequest {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub request_id: String,
#[cultcache(key = 2)]
@ -548,8 +359,6 @@ pub struct RepoFrontierModelingRequest {
pub allowed_disposition: RepoFrontierVerdictDisposition,
#[cultcache(key = 12)]
pub requested_at: String,
#[cultcache(key = 13)]
pub contract: String,
}
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
@ -558,8 +367,6 @@ pub struct RepoFrontierModelingRequest {
schema = "RepoFrontierPlanningFailureReview"
)]
pub struct RepoFrontierPlanningFailureReview {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub review_id: String,
#[cultcache(key = 2)]
@ -588,8 +395,6 @@ pub enum RepoFrontierNextOrgan {
schema = "RepoFrontierRoute"
)]
pub struct RepoFrontierRoute {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub route_id: String,
#[cultcache(key = 2)]
@ -613,11 +418,9 @@ pub struct RepoFrontierRoute {
#[cultcache(key = 11)]
pub authorized_paths: Vec<String>,
#[cultcache(key = 12, default)]
pub adopted_plan: Option<epiphany_state_model::RepoFrontierAdoptedPlan>,
pub adopted_plan: Option<crate::RepoFrontierAdoptedPlan>,
#[cultcache(key = 13)]
pub selected_at: String,
#[cultcache(key = 14)]
pub contract: String,
}
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
@ -626,30 +429,12 @@ pub struct RepoFrontierRoute {
schema = "RepoFrontierHandsAuthority"
)]
pub struct RepoFrontierHandsAuthority {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub authority_id: String,
#[cultcache(key = 2)]
pub route_id: String,
#[cultcache(key = 3)]
pub model_projection_digest: String,
#[cultcache(key = 4)]
pub model_source_documents: Vec<crate::EpiphanyMindDocumentVersion>,
#[cultcache(key = 5)]
pub frontier_item_id: String,
#[cultcache(key = 6)]
pub frontier_item_hash: String,
#[cultcache(key = 7)]
pub hands_intent_id: String,
#[cultcache(key = 8)]
pub hands_review_id: String,
#[cultcache(key = 9)]
#[cultcache(key = 4)]
pub substrate_grant_receipt_id: String,
#[cultcache(key = 10)]
pub requested_paths: Vec<String>,
#[cultcache(key = 11)]
pub granted_at: String,
#[cultcache(key = 12)]
pub contract: String,
}

View file

@ -214,7 +214,6 @@ pub struct RepositoryBodyManifest {
pub entries: Vec<RepositoryBodyManifestEntry>,
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(type = "epiphany.repository_body.head", schema = "RepositoryBodyHead")]
pub struct RepositoryBodyHead {
@ -331,7 +330,7 @@ pub fn admit_repository_body_observation(
BODY_OBSERVATION_TYPE,
&basis.observation_id,
)
.ok_or_else(|| anyhow!("repository Body observation source is absent"))?;
.ok_or_else(|| anyhow!("repository Body observation source is absent"))?;
let source_observation =
crate::EpiphanyMindDocumentVersion::from_envelope("epiphany-repository-body", source)?;
let document = crate::EpiphanyMindRepositoryBodyObservationDocument {
@ -367,12 +366,6 @@ pub fn admit_repository_body_observation(
}
}
pub fn current_mind_repository_body_observation(
runtime_store: &Path,
) -> Result<Option<RepositoryBodyObservationBasis>> {
Ok(crate::assemble_mind_view(runtime_store)?.repository_body_observation)
}
/// Loads the authenticated current Repository Body basis already persisted for
/// a runtime without observing the worktree or advancing the Body generation.
pub fn load_current_runtime_repository_body_basis(
@ -986,7 +979,7 @@ fn sanitized_git_command() -> Command {
sanitize_git_command(&mut command);
command
}
fn repository_git_command(repo: &Path) -> Result<Command> {
pub(crate) fn repository_git_command(repo: &Path) -> Result<Command> {
let canonical = std::fs::canonicalize(repo)
.with_context(|| format!("failed to canonicalize Git repository {}", repo.display()))?;
let mut command = sanitized_git_command();
@ -1362,7 +1355,7 @@ fn validate_runtime_binding_entries(
EpiphanyRuntimeSwarmBinding::TYPE,
RUNTIME_SWARM_BINDING_KEY,
)
.ok_or_else(|| anyhow!("runtime store has no immutable swarm binding"))?,
.ok_or_else(|| anyhow!("runtime store has no immutable swarm binding"))?,
)?;
if binding.schema_version != RUNTIME_SWARM_BINDING_SCHEMA_VERSION
|| binding.binding_id != RUNTIME_SWARM_BINDING_KEY
@ -1586,7 +1579,7 @@ mod tests {
run(d.path(), &["add", "."])?;
let observed = observe_runtime_repository_body_basis(&runtime)?;
assert_eq!(
current_mind_repository_body_observation(&runtime)?,
crate::assemble_mind_view(&runtime)?.repository_body_observation,
Some(observed.clone())
);
let mut mind = crate::runtime_spine_cache(&runtime)?;

View file

@ -214,7 +214,7 @@ pub struct ResidentProviderReadiness {
}
impl ResidentProviderReadiness {
pub fn validate(&self) -> Result<()> {
fn validate(&self) -> Result<()> {
if self.schema_version != RESIDENT_PROVIDER_READINESS_SCHEMA_VERSION {
bail!("resident provider readiness schema is unsupported");
}
@ -295,7 +295,7 @@ pub fn publish_resident_provider_readiness(
Ok(readiness)
}
pub fn load_resident_provider_readiness(store: &Path) -> Result<Option<ResidentProviderReadiness>> {
fn load_resident_provider_readiness(store: &Path) -> Result<Option<ResidentProviderReadiness>> {
Ok(readiness_snapshot(store)?.1)
}
@ -520,72 +520,6 @@ fn classify_active_lease_observation(
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ResidentProviderHealth {
pub terminal_current: usize,
pub warming: usize,
pub contradictions: Vec<String>,
}
pub fn authenticate_resident_provider(
release: &crate::EpiphanyPackagedReleaseEntry,
witness: &str,
resident_store: &Path,
freshness_millis: u64,
) -> ResidentProviderHealth {
let mut result = ResidentProviderHealth {
terminal_current: 0,
warming: 0,
contradictions: Vec::new(),
};
for (owner, store, role) in [("resident-self", resident_store, "swarm")] {
let executable = epiphany_packaged_release_binary_path(release, role).ok();
let value = match load_resident_provider_readiness(store) {
Ok(Some(value)) => value,
Ok(None) => {
result.warming += 1;
continue;
}
Err(error) => {
result
.contradictions
.push(format!("{owner} provider readiness is invalid: {error:#}"));
continue;
}
};
let role_owner_present = match resident_process_singleton_is_owned(store) {
Ok(value) => value,
Err(error) => {
result.contradictions.push(format!(
"{owner} provider singleton observation failed: {error:#}"
));
continue;
}
};
if provider_matches_authority(
&value,
owner,
&release.runtime_id,
&release.release_id,
witness,
Some(&release.source_commit_sha),
executable.as_deref(),
current_time_millis(),
freshness_millis,
role_owner_present,
) {
result.terminal_current += 1;
} else if value.runtime_id == release.runtime_id && value.release_id == release.release_id {
result.warming += 1;
} else {
result
.contradictions
.push(format!("{owner} provider readiness names another release"));
}
}
result
}
fn provider_is_fresh(
store: &Path,
owner: &str,
@ -684,34 +618,6 @@ fn provider_authority_failures(
failures
}
#[allow(clippy::too_many_arguments)]
fn provider_matches_authority(
value: &ResidentProviderReadiness,
owner: &str,
runtime_id: &str,
release_id: &str,
witness: &str,
source_commit: Option<&str>,
executable: Option<&Path>,
now_millis: u64,
freshness_millis: u64,
role_owner_present: bool,
) -> bool {
provider_authority_failures(
value,
owner,
runtime_id,
release_id,
witness,
source_commit,
executable,
now_millis,
freshness_millis,
role_owner_present,
)
.is_empty()
}
fn canonical_or_absolute(path: &Path) -> PathBuf {
fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
}
@ -728,20 +634,12 @@ fn directory_ready(path: &Path) -> bool {
fs::metadata(path).is_ok_and(|value| value.is_dir() && !value.permissions().readonly())
}
fn codex_credentials_ready(codex_home: &Path) -> bool {
directory_ready(codex_home) && credential_file_ready(&codex_home.join("auth.json"))
}
fn model_provider_credential_ready(policy: &ResidentSelfPolicy) -> bool {
match policy.model_provider.as_str() {
"openai-codex" | "openai" => codex_credentials_ready(&policy.codex_home),
"openrouter" => policy
"openai-codex" | "openai" | "openrouter" => policy
.provider_credential_path
.as_deref()
.is_some_and(|path| {
fs::metadata(path).is_ok_and(|metadata| metadata.is_file())
&& fs::File::open(path).is_ok()
}),
.is_some_and(credential_file_ready),
_ => false,
}
}
@ -845,60 +743,6 @@ mod tests {
}
}
#[test]
fn stale_wrong_release_and_absent_role_owner_cannot_be_ready() {
let value = provider();
let executable = std::env::current_exe().unwrap();
assert!(provider_matches_authority(
&value,
"resident-self",
"ygg",
"release-a",
&value.release_witness_sha256,
Some("commit-a"),
Some(&executable),
1_050,
100,
true
));
assert!(!provider_matches_authority(
&value,
"resident-self",
"ygg",
"release-b",
&value.release_witness_sha256,
Some("commit-a"),
Some(&executable),
1_050,
100,
true
));
assert!(!provider_matches_authority(
&value,
"resident-self",
"ygg",
"release-a",
&value.release_witness_sha256,
Some("commit-a"),
Some(&executable),
1_101,
100,
true
));
assert!(!provider_matches_authority(
&value,
"resident-self",
"ygg",
"release-a",
&value.release_witness_sha256,
Some("commit-a"),
Some(&executable),
1_050,
100,
false
));
}
#[test]
fn role_singleton_probe_observes_only_a_live_owner() -> Result<()> {
let temp = tempfile::tempdir()?;
@ -944,7 +788,7 @@ mod tests {
let store = temp.path().join("mixed.cc");
let mut mixed = CultCache::new();
mixed.register_entry_type::<ForeignOwnerEntry>()?;
mixed.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store));
mixed.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store))?;
let foreign = ForeignOwnerEntry {
value: "must survive readiness CAS".into(),
};
@ -972,30 +816,6 @@ mod tests {
Ok(())
}
#[cfg(unix)]
#[test]
fn unix_credentials_reject_group_or_other_access() -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir()?;
let auth = temp.path().join("auth.json");
fs::write(&auth, b"{}")?;
fs::set_permissions(&auth, fs::Permissions::from_mode(0o400))?;
assert!(!codex_credentials_ready(temp.path()));
fs::set_permissions(&auth, fs::Permissions::from_mode(0o644))?;
assert!(!codex_credentials_ready(temp.path()));
fs::set_permissions(&auth, fs::Permissions::from_mode(0o600))?;
assert!(codex_credentials_ready(temp.path()));
Ok(())
}
#[test]
fn legacy_credentials_file_cannot_impersonate_canonical_auth() -> Result<()> {
let temp = tempfile::tempdir()?;
fs::write(temp.path().join("credentials.json"), b"{}")?;
assert!(!codex_credentials_ready(temp.path()));
Ok(())
}
#[test]
fn readiness_uses_mounted_cognitive_identity_for_projection_and_brake() -> Result<()> {
let temp = tempfile::tempdir()?;
@ -1026,7 +846,7 @@ mod tests {
runtime_store,
local_verse_store,
artifact_root: shared.join("artifacts"),
codex_home: shared.join("codex-home"),
model_connector_endpoint: "127.0.0.1:17891".parse().unwrap(),
mcp_config: shared.join("mcp.toml"),
model_provider: "test".into(),
model: "test-model".into(),
@ -1116,79 +936,22 @@ mod tests {
let mut value = provider();
value.observed_at_millis = 1_500;
let executable = PathBuf::from(&value.process_executable_path);
assert!(provider_matches_authority(
&value,
"resident-self",
"ygg",
"release-a",
&value.release_witness_sha256,
Some("commit-a"),
Some(&executable),
1_000,
100,
true,
));
let failures = |value: &ResidentProviderReadiness| {
provider_authority_failures(
value,
"resident-self",
"ygg",
"release-a",
&value.release_witness_sha256,
Some("commit-a"),
Some(&executable),
1_000,
100,
true,
)
};
assert!(failures(&value).is_empty());
value.observed_at_millis = 2_001;
assert!(!provider_matches_authority(
&value,
"resident-self",
"ygg",
"release-a",
&value.release_witness_sha256,
Some("commit-a"),
Some(&executable),
1_000,
100,
true,
));
}
#[test]
fn aggregate_health_warms_on_missing_and_rejects_wrong_release() -> Result<()> {
let temp = tempfile::tempdir()?;
let resident = temp.path().join("resident.cc");
let executable = std::env::current_exe()?;
let binary = |role: &str| crate::EpiphanyPackagedReleaseBinary {
role: role.into(),
file_name: executable.file_name().unwrap().to_string_lossy().into(),
canonical_path: executable.display().to_string(),
sha256: format!("sha256-{}", "b".repeat(64)),
byte_len: 1,
};
let release = crate::EpiphanyPackagedReleaseEntry {
schema_version: crate::EPIPHANY_PACKAGED_RELEASE_SCHEMA_VERSION.into(),
release_id: "release-a".into(),
runtime_id: "ygg".into(),
source_commit_sha: "commit-a".into(),
target_triple: "test".into(),
cargo_profile: "release".into(),
toolchain_fingerprint: "toolchain".into(),
created_at_utc: "now".into(),
package_root: temp.path().display().to_string(),
binaries: vec![binary("swarm")],
private_state_exposed: false,
};
let witness = format!("sha256-{}", "a".repeat(64));
let missing = authenticate_resident_provider(&release, &witness, &resident, 10);
assert_eq!(missing.warming, 1);
let process = crate::capture_process_instance(std::process::id())?;
let mut wrong = provider();
wrong.runtime_id = "ygg".into();
wrong.release_id = "other-release".into();
wrong.release_witness_sha256 = witness.clone();
wrong.source_commit = "commit-a".into();
wrong.observed_at_millis = 10;
wrong.process_id = process.process_id;
wrong.process_creation_token = process.creation_token;
wrong.process_executable_path = process.executable_path.display().to_string();
publish_resident_provider_readiness(&resident, wrong)?;
let rejected = authenticate_resident_provider(&release, &witness, &resident, 10);
assert!(
rejected
.contradictions
.iter()
.any(|reason| reason.contains("another release"))
);
Ok(())
assert_eq!(failures(&value), vec!["future-observation"]);
}
}

View file

@ -5,12 +5,11 @@ use cultcache_rs::{
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::{BTreeSet, HashSet};
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
pub const RESIDENT_SELF_STATE_KEY: &str = "resident-self";
pub const RESIDENT_SELF_STATE_SCHEMA_VERSION: &str = "epiphany.resident_self.state.v1";
pub const RESIDENT_SELF_RUNTIME_RECEIPT_SCHEMA_VERSION: &str =
"epiphany.resident_self.runtime_receipt.v0";
pub const RESIDENT_SELF_STATE_SCHEMA_VERSION: &str = "epiphany.resident_self.state.v3";
pub const RESIDENT_SELF_PRESSURE_SCHEMA_VERSION: &str = "epiphany.resident_self.pressure.v0";
pub const RESIDENT_SELF_COORDINATOR_CONTINUATION_PRESSURE_KIND: &str =
"coordinator-internal-continuation";
@ -26,7 +25,7 @@ pub const RESIDENT_SELF_TERMINAL_RECEIPT_SCHEMA_VERSION: &str =
"epiphany.resident_self.terminal_receipt.v1";
pub const RESIDENT_SELF_CHILD_CLAIM_SCHEMA_VERSION: &str = "epiphany.resident_self.child_claim.v0";
pub const RESIDENT_SELF_RETENTION_HEAD_SCHEMA_VERSION: &str =
"epiphany.resident_self.retention_head.v0";
"epiphany.resident_self.retention_head.v1";
pub const RESIDENT_SELF_RETENTION_HEAD_KEY: &str = "resident-self-retention";
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
@ -241,7 +240,7 @@ pub struct ResidentSelfChildClaim {
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "epiphany.resident_self.retention_head.v0",
type = "epiphany.resident_self.retention_head.v1",
schema = "ResidentSelfRetentionHead"
)]
pub struct ResidentSelfRetentionHead {
@ -255,9 +254,7 @@ pub struct ResidentSelfRetentionHead {
pub retired_envelope_count: u64,
#[cultcache(key = 4)]
pub retired_chain_digest: String,
#[cultcache(key = 5)]
pub retained_at_millis: u64,
#[cultcache(key = 6, default)]
#[cultcache(key = 5, default)]
pub private_state_exposed: bool,
}
@ -277,7 +274,7 @@ pub struct ResidentSelfPolicy {
pub runtime_store: PathBuf,
pub local_verse_store: PathBuf,
pub artifact_root: PathBuf,
pub codex_home: PathBuf,
pub model_connector_endpoint: SocketAddr,
pub mcp_config: PathBuf,
pub model_provider: String,
pub model: String,
@ -305,7 +302,6 @@ impl ResidentSelfPolicy {
("runtime store", &self.runtime_store),
("local Verse store", &self.local_verse_store),
("artifact root", &self.artifact_root),
("Codex home", &self.codex_home),
("MCP config", &self.mcp_config),
("release store", &self.release_store),
] {
@ -324,6 +320,13 @@ impl ResidentSelfPolicy {
path.display()
));
}
if !self.model_connector_endpoint.ip().is_loopback()
|| self.model_connector_endpoint.port() == 0
{
return Err(anyhow!(
"resident Self model connector must use a nonzero loopback endpoint"
));
}
if self.model_provider.trim().is_empty()
|| self.model.trim().is_empty()
|| self.max_steps == 0
@ -530,32 +533,6 @@ impl Default for ResidentSelfState {
}
}
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "epiphany.resident_self.runtime_receipt.v0",
schema = "ResidentSelfRuntimeReceipt"
)]
pub struct ResidentSelfRuntimeReceipt {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub receipt_id: String,
#[cultcache(key = 2)]
pub occurred_at_millis: u64,
#[cultcache(key = 3)]
pub status: String,
#[cultcache(key = 4)]
pub reason: String,
#[cultcache(key = 5, default)]
pub turn_id: Option<String>,
#[cultcache(key = 6, default)]
pub coordinator_receipt_id: Option<String>,
#[cultcache(key = 7, default)]
pub process_id: Option<u32>,
#[cultcache(key = 8, default)]
pub private_state_exposed: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ChildObservation {
Running,
@ -647,8 +624,8 @@ pub fn coordinator_argv(
turn_id.into(),
"--cwd".into(),
policy.workspace.display().to_string(),
"--codex-home".into(),
policy.codex_home.display().to_string(),
"--connector-endpoint".into(),
policy.model_connector_endpoint.to_string(),
"--mcp-config".into(),
policy.mcp_config.display().to_string(),
"--artifact-dir".into(),
@ -785,7 +762,6 @@ pub fn resident_prepared_launch_thread_id(prepared: &ResidentSelfPreparedLaunch)
fn state_cache(path: &Path) -> Result<CultCache> {
let mut cache = CultCache::new();
cache.register_entry_type::<ResidentSelfState>()?;
cache.register_entry_type::<ResidentSelfRuntimeReceipt>()?;
cache.register_entry_type::<ResidentSelfPressure>()?;
cache.register_entry_type::<ResidentSelfGrant>()?;
cache.register_entry_type::<ResidentSelfTerminalReceipt>()?;
@ -795,14 +771,15 @@ fn state_cache(path: &Path) -> Result<CultCache> {
for envelope in SingleFileMessagePackBackingStore::new(path).pull_all()? {
if matches!(
envelope.r#type.as_str(),
"epiphany.resident_self.heartbeat_grant.v0" | "epiphany.resident_self.terminal_ack.v0"
"epiphany.resident_self.heartbeat_grant.v0"
| "epiphany.resident_self.terminal_ack.v0"
| "epiphany.resident_self.retention_head.v0"
) {
return Err(anyhow!(
"resident Self store contains a pre-cut heartbeat lifecycle document"
));
}
let owned = envelope.r#type == ResidentSelfState::TYPE
|| envelope.r#type == ResidentSelfRuntimeReceipt::TYPE
|| envelope.r#type == ResidentSelfPressure::TYPE
|| envelope.r#type == ResidentSelfGrant::TYPE
|| envelope.r#type == ResidentSelfTerminalReceipt::TYPE
@ -820,25 +797,22 @@ fn state_cache(path: &Path) -> Result<CultCache> {
}
match envelope.r#type.as_str() {
ResidentSelfState::TYPE => {
cache.load_envelope::<ResidentSelfState>(envelope)?;
}
ResidentSelfRuntimeReceipt::TYPE => {
cache.load_envelope::<ResidentSelfRuntimeReceipt>(envelope)?;
cache.put_envelope::<ResidentSelfState>(envelope)?;
}
ResidentSelfPressure::TYPE => {
cache.load_envelope::<ResidentSelfPressure>(envelope)?;
cache.put_envelope::<ResidentSelfPressure>(envelope)?;
}
ResidentSelfGrant::TYPE => {
cache.load_envelope::<ResidentSelfGrant>(envelope)?;
cache.put_envelope::<ResidentSelfGrant>(envelope)?;
}
ResidentSelfTerminalReceipt::TYPE => {
cache.load_envelope::<ResidentSelfTerminalReceipt>(envelope)?;
cache.put_envelope::<ResidentSelfTerminalReceipt>(envelope)?;
}
ResidentSelfChildClaim::TYPE => {
cache.load_envelope::<ResidentSelfChildClaim>(envelope)?;
cache.put_envelope::<ResidentSelfChildClaim>(envelope)?;
}
ResidentSelfRetentionHead::TYPE => {
cache.load_envelope::<ResidentSelfRetentionHead>(envelope)?;
cache.put_envelope::<ResidentSelfRetentionHead>(envelope)?;
}
_ => unreachable!("owned resident Self type was matched above"),
};
@ -995,7 +969,6 @@ pub(crate) fn resident_self_current_work_action(
let projection_digest = current_work.projection_digest()?;
let decision = crate::recommend_coordinator_action(crate::EpiphanyCoordinatorInput {
mind_present: true,
crrc_action: crate::EpiphanyCrrcAction::Continue,
current_work,
});
if !matches!(
@ -1241,8 +1214,16 @@ pub fn verify_resident_self_grant_fulfillment(
let Some(request) = request else {
return Ok(ResidentSelfGrantFulfillment::Fulfilled);
};
let mut runtime = crate::runtime_spine_cache(runtime_store)?;
runtime.pull_all_backing_stores()?;
Ok(
if crate::runtime_typed_request_fulfillment(runtime_store, request)?.is_some() {
if crate::runtime_spine::runtime_typed_request_fulfillment_from_cache(
runtime_store,
&runtime,
request,
)?
.is_some()
{
ResidentSelfGrantFulfillment::Fulfilled
} else {
ResidentSelfGrantFulfillment::Pending
@ -1349,11 +1330,12 @@ pub fn recover_dead_resident_typed_worker(
};
let mut cache = crate::runtime_spine_cache(runtime_store)?;
cache.pull_all_backing_stores()?;
let mut launches = cache
.get_all::<crate::EpiphanyRuntimeWorkerLaunchRequest>()?
.into_iter()
.filter(|launch| request.matches_launch(launch))
.collect::<Vec<_>>();
let mut launches = Vec::new();
for launch in cache.get_all::<crate::EpiphanyRuntimeWorkerLaunchRequest>()? {
if request.matches_launch(&launch)? {
launches.push(launch);
}
}
launches.sort_by(|a, b| a.job_id.cmp(&b.job_id));
let mut retryable = Vec::new();
for launch in &launches {
@ -1838,7 +1820,6 @@ pub fn recover_receipt_free_dead_coordinator_session(
return Ok(None);
}
let recovery = crate::EpiphanyCoordinatorDeathRecovery {
schema_version: crate::runtime_spine::COORDINATOR_DEATH_RECOVERY_SCHEMA_VERSION.into(),
recovery_id: format!("coordinator-death-recovery-{session_id}"),
session_id,
thread_id: lease.turn_id.clone(),
@ -1888,7 +1869,7 @@ fn digest_parts(parts: impl IntoIterator<Item = impl AsRef<[u8]>>) -> String {
format!("sha256:{:x}", hash.finalize())
}
pub fn resident_self_policy_digest(policy: &ResidentSelfPolicy) -> String {
fn resident_self_policy_digest(policy: &ResidentSelfPolicy) -> String {
digest_parts([
policy.workspace.display().to_string(),
policy.coordinator_bin.display().to_string(),
@ -2333,20 +2314,6 @@ pub fn validate_resident_self_prepared_objective(
))
}
pub fn validate_resident_self_coordinator_receipt(
lease: &ResidentSelfTurnLease,
coordinator: &crate::EpiphanyCoordinatorRunReceipt,
) -> Result<()> {
validate_resident_self_coordinator_receipt_binding(lease, coordinator)?;
if !matches!(
coordinator.status.as_str(),
"planned" | "needsReview" | "completed"
) {
return Err(anyhow!("coordinator terminal receipt is not successful"));
}
Ok(())
}
pub fn validate_resident_self_coordinator_receipt_binding(
lease: &ResidentSelfTurnLease,
coordinator: &crate::EpiphanyCoordinatorRunReceipt,
@ -2372,7 +2339,7 @@ pub fn validate_resident_self_coordinator_receipt_binding(
Ok(())
}
pub fn complete_resident_self_turn_after_death(
fn complete_resident_self_turn_after_death(
path: &Path,
lease: &ResidentSelfTurnLease,
recovery: &crate::EpiphanyCoordinatorDeathRecovery,
@ -2566,17 +2533,9 @@ pub fn cancel_resident_self_turn(
Ok(terminal)
}
pub fn resident_self_terminal_receipts(path: &Path) -> Result<Vec<ResidentSelfTerminalReceipt>> {
Ok(state_cache(path)?
.get_all::<ResidentSelfTerminalReceipt>()?
.into_iter()
.collect())
}
pub fn retain_resident_self_lifecycles(
path: &Path,
retain_closed: usize,
now_millis: u64,
) -> Result<Option<ResidentSelfRetentionHead>> {
let cache = state_cache(path)?;
let state = cache
@ -2718,7 +2677,6 @@ pub fn retain_resident_self_lifecycles(
head.retired_envelope_count + deletions.len() as u64
}),
retired_chain_digest: digest_parts(digest_inputs),
retained_at_millis: now_millis,
private_state_exposed: false,
};
let (replacement, _) = cache.prepare_entry(RESIDENT_SELF_RETENTION_HEAD_KEY, &head)?;
@ -2756,6 +2714,61 @@ mod pressure_replay_tests {
}
}
#[test]
fn obsolete_state_epoch_refuses_without_mutation() -> Result<()> {
let temp = tempfile::tempdir()?;
let store = temp.path().join("resident.cc");
let cache = state_cache(&store)?;
let mut state = ResidentSelfState::default();
state.schema_version = "epiphany.resident_self.state.v2".into();
SingleFileMessagePackBackingStore::new(&store)
.push(&cache.prepare_entry(RESIDENT_SELF_STATE_KEY, &state)?.0)?;
let before = std::fs::read(&store)?;
let error = load_resident_self_state(&store).unwrap_err();
assert!(error.to_string().contains("obsolete writable state epoch"));
assert_eq!(std::fs::read(&store)?, before);
Ok(())
}
#[test]
fn state_cache_reads_without_writing_the_store() -> Result<()> {
let temp = tempfile::tempdir()?;
let store = temp.path().join("resident.cc");
let cache = state_cache(&store)?;
SingleFileMessagePackBackingStore::new(&store).push(
&cache
.prepare_entry(RESIDENT_SELF_STATE_KEY, &ResidentSelfState::default())?
.0,
)?;
let past = std::time::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000_000);
std::fs::File::options()
.write(true)
.open(&store)?
.set_modified(past)?;
let listing = || -> Result<Vec<(std::ffi::OsString, u64, std::time::SystemTime)>> {
let mut entries = std::fs::read_dir(temp.path())?
.map(|entry| {
let entry = entry?;
let metadata = entry.metadata()?;
Ok((entry.file_name(), metadata.len(), metadata.modified()?))
})
.collect::<Result<Vec<_>>>()?;
entries.sort();
Ok(entries)
};
let before = listing()?;
let names = before.iter().map(|entry| entry.0.clone()).collect::<Vec<_>>();
assert_eq!(names, ["resident.cc", "resident.cc.lock"]);
let reopened = state_cache(&store)?;
assert!(
reopened
.get::<ResidentSelfState>(RESIDENT_SELF_STATE_KEY)?
.is_some()
);
assert_eq!(listing()?, before, "a read attached or wrote a store");
Ok(())
}
#[test]
fn configured_operator_pressure_replay_preserves_the_first_document() -> Result<()> {
let temp = tempfile::tempdir()?;
@ -2796,7 +2809,7 @@ mod coordinator_launch_contract_tests {
runtime_store,
local_verse_store: root.join("local-verse.cc"),
artifact_root: root.join("artifacts"),
codex_home: root.join("codex-home"),
model_connector_endpoint: "127.0.0.1:17891".parse().unwrap(),
mcp_config: root.join("mcp.toml"),
model_provider: "openrouter".into(),
model: "stealth/ox-alpha".into(),
@ -2825,7 +2838,7 @@ mod coordinator_launch_contract_tests {
runtime_store: PathBuf::from("/epiphany/state/runtime.cc"),
local_verse_store: PathBuf::from("/epiphany/state/local-verse.cc"),
artifact_root: PathBuf::from("/epiphany/artifacts"),
codex_home: PathBuf::from("/epiphany/codex-home"),
model_connector_endpoint: "127.0.0.1:17891".parse().unwrap(),
mcp_config: PathBuf::from("/epiphany/mcp.toml"),
model_provider: "openrouter".into(),
model: "stealth/ox-alpha".into(),

File diff suppressed because it is too large Load diff

View file

@ -74,21 +74,6 @@ impl RuntimeSpineBackingStore {
),
}
}
pub(crate) fn replace_and_append_if_snapshot_unchanged(
&self,
expected_snapshot: &[CultCacheEnvelope],
replacements: Vec<CultCacheEnvelope>,
) -> Result<bool> {
match self.selected()? {
SelectedBackingStore::Snapshot(store) => {
store.replace_and_append_if_snapshot_unchanged(expected_snapshot, replacements)
}
SelectedBackingStore::Keyed(store) => {
store.replace_and_append_if_snapshot_unchanged(expected_snapshot, replacements)
}
}
}
}
impl CacheBackingStore for RuntimeSpineBackingStore {
@ -123,7 +108,11 @@ impl CacheBackingStore for RuntimeSpineBackingStore {
pub(crate) fn runtime_spine_backing_store(path: &Path) -> Result<RuntimeSpineBackingStore> {
match path.extension().and_then(|extension| extension.to_str()) {
Some("cc" | "msgpack" | "redb") => Ok(RuntimeSpineBackingStore::new(path)),
Some("cc" | "msgpack" | "redb") => {
#[cfg(test)]
record_backing_store_resolution(path);
Ok(RuntimeSpineBackingStore::new(path))
}
extension => Err(anyhow!(
"runtime spine store must use an explicit .cc, .msgpack, or .redb extension; got {:?}",
extension
@ -131,6 +120,39 @@ pub(crate) fn runtime_spine_backing_store(path: &Path) -> Result<RuntimeSpineBac
}
}
/// Every resolution this process has made, in order, recorded only under test.
///
/// A handle carries nothing but its path today, so a second handle to the same
/// path is behaviourally invisible and no assertion about files or contents can
/// see it. The `.redb` arm makes that difference real: redb permits one
/// writable handle per path. This log is the probe that lets a caller pin
/// "resolved once and reused" rather than the much weaker "only one store was
/// written".
#[cfg(test)]
static RESOLVED_BACKING_STORES: std::sync::Mutex<Vec<PathBuf>> =
std::sync::Mutex::new(Vec::new());
#[cfg(test)]
fn record_backing_store_resolution(path: &Path) {
RESOLVED_BACKING_STORES
.lock()
.expect("the backing-store resolution log is never poisoned")
.push(path.to_path_buf());
}
/// How many times this path has been resolved into a handle. Tests run in
/// parallel, so a caller compares this around its own call against its own
/// temporary path rather than reading a global total.
#[cfg(test)]
pub(crate) fn backing_store_resolutions(path: &Path) -> usize {
RESOLVED_BACKING_STORES
.lock()
.expect("the backing-store resolution log is never poisoned")
.iter()
.filter(|resolved| resolved.as_path() == path)
.count()
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -84,77 +84,50 @@ impl<'a> RuntimeTypedRequestRef<'a> {
}
}
pub(crate) fn matches_launch(self, launch: &crate::EpiphanyRuntimeWorkerLaunchRequest) -> bool {
match self {
Self::ProposalModeling(id) => {
launch.proposal_modeling_request_id.as_deref() == Some(id)
}
Self::FrontierVerdictModeling(id) => {
launch.repo_frontier_modeling_request_id.as_deref() == Some(id)
}
Self::FrontierResearch(id) => {
launch.repo_frontier_research_request_id.as_deref() == Some(id)
}
Self::FrontierVerification(id) => {
launch.repo_frontier_verification_request_id.as_deref() == Some(id)
}
Self::ImaginationConsideration(id) => {
launch.imagination_consideration_request_id.as_deref() == Some(id)
}
Self::AdmittedModelDirection(id) => {
launch
.admitted_model_direction_consideration_request_id
.as_deref()
== Some(id)
}
}
}
pub(crate) fn matches_result(self, result: &crate::EpiphanyRuntimeRoleWorkerResult) -> bool {
match self {
Self::ProposalModeling(id) => {
result.proposal_modeling_request_id.as_deref() == Some(id)
}
Self::FrontierVerdictModeling(id) => {
result.repo_frontier_modeling_request_id.as_deref() == Some(id)
}
Self::FrontierResearch(id) => {
result.repo_frontier_research_request_id.as_deref() == Some(id)
}
Self::FrontierVerification(id) => result.verification_request_id.as_deref() == Some(id),
Self::ImaginationConsideration(id) => {
result.imagination_consideration_request_id.as_deref() == Some(id)
}
Self::AdmittedModelDirection(id) => {
result
.admitted_model_direction_consideration_request_id
.as_deref()
== Some(id)
}
}
pub(crate) fn matches_launch(
self,
launch: &crate::EpiphanyRuntimeWorkerLaunchRequest,
) -> Result<bool> {
let document = launch.launch_document()?;
Ok(document.typed_request_ref()? == Some(self))
}
}
impl crate::EpiphanyRuntimeWorkerLaunchRequest {
impl crate::EpiphanyWorkerLaunchDocument {
pub(crate) fn typed_request_ref(&self) -> Result<Option<RuntimeTypedRequestRef<'_>>> {
let crate::EpiphanyWorkerLaunchDocument::Role(document) = self else {
return Ok(None);
};
let requests = [
self.proposal_modeling_request_id
.as_deref()
document
.proposal_modeling_context
.as_ref()
.map(|context| context.request_id.as_str())
.map(RuntimeTypedRequestRef::ProposalModeling),
self.repo_frontier_modeling_request_id
.as_deref()
document
.frontier_verdict_modeling_context
.as_ref()
.map(|context| context.request.request_id.as_str())
.map(RuntimeTypedRequestRef::FrontierVerdictModeling),
self.repo_frontier_research_request_id
.as_deref()
document
.frontier_research_context
.as_ref()
.map(|context| context.request_id.as_str())
.map(RuntimeTypedRequestRef::FrontierResearch),
self.repo_frontier_verification_request_id
.as_deref()
document
.frontier_verification_context
.as_ref()
.map(|context| context.request.request_id.as_str())
.map(RuntimeTypedRequestRef::FrontierVerification),
self.imagination_consideration_request_id
.as_deref()
document
.imagination_consideration_context
.as_ref()
.map(|context| context.request.request_id.as_str())
.map(RuntimeTypedRequestRef::ImaginationConsideration),
self.admitted_model_direction_consideration_request_id
.as_deref()
document
.admitted_model_direction_consideration_context
.as_ref()
.map(|context| context.request.request_id.as_str())
.map(RuntimeTypedRequestRef::AdmittedModelDirection),
]
.into_iter()

View file

@ -1,19 +1,8 @@
use cultcache_rs::DatabaseEntry;
pub const SOUL_VERDICT_RECEIPT_TYPE: &str = "epiphany.soul.verdict_receipt";
pub const REPO_FRONTIER_VERIFICATION_REQUEST_TYPE: &str =
"epiphany.soul.repo_frontier_verification_request";
pub const REPO_FRONTIER_VERIFICATION_REQUEST_SCHEMA_VERSION: &str =
"epiphany.soul.repo_frontier_verification_request.v2";
pub const REPO_FRONTIER_VERIFICATION_REQUEST_CONTRACT: &str =
"epiphany.repo_frontier_verification_request.v2";
pub const SOUL_VERDICT_RECEIPT_SCHEMA_VERSION: &str = "epiphany.soul.verdict_receipt.v1";
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
#[cultcache(type = "epiphany.soul.verdict_receipt", schema = "SoulVerdictReceipt")]
pub struct SoulVerdictReceipt {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub receipt_id: String,
#[cultcache(key = 2)]
@ -30,8 +19,6 @@ pub struct SoulVerdictReceipt {
pub risks: Vec<String>,
#[cultcache(key = 8)]
pub emitted_at: String,
#[cultcache(key = 9)]
pub contract: String,
#[cultcache(key = 10, default)]
pub verification_request_id: String,
#[cultcache(key = 11, default)]
@ -44,8 +31,6 @@ pub struct SoulVerdictReceipt {
schema = "RepoFrontierVerificationRequest"
)]
pub struct RepoFrontierVerificationRequest {
#[cultcache(key = 0)]
pub schema_version: String,
#[cultcache(key = 1)]
pub request_id: String,
#[cultcache(key = 2)]
@ -61,17 +46,13 @@ pub struct RepoFrontierVerificationRequest {
#[cultcache(key = 7)]
pub hands_intent_id: String,
#[cultcache(key = 8)]
pub hands_review_id: String,
#[cultcache(key = 9)]
pub hands_patch_receipt_id: String,
#[cultcache(key = 10)]
#[cultcache(key = 9)]
pub hands_command_receipt_id: String,
#[cultcache(key = 11)]
#[cultcache(key = 10)]
pub hands_commit_receipt_id: String,
#[cultcache(key = 12)]
#[cultcache(key = 11)]
pub requested_at: String,
#[cultcache(key = 13)]
pub contract: String,
#[cultcache(key = 14)]
#[cultcache(key = 12)]
pub frontier_authority_documents: Vec<crate::EpiphanyMindDocumentVersion>,
}

View file

@ -8,7 +8,6 @@ use serde::Serialize;
use serde_json::Value;
use std::path::Path;
pub const STATE_LEDGER_STORE_TYPE: &str = "epiphany.state_ledger";
pub const STATE_LEDGER_KEY: &str = "default";
pub const STATE_LEDGER_SCHEMA_VERSION: &str = "epiphany.state_ledger.v0";
@ -43,34 +42,6 @@ pub struct EpiphanyLedgerEvidenceRecord {
pub branch: Option<String>,
}
pub fn state_ledger_status(store_path: impl AsRef<Path>) -> Result<Value> {
let store_path = store_path.as_ref();
if !store_path.exists() {
return Ok(serde_json::json!({
"ok": false,
"present": false,
"store": store_path,
"entryType": STATE_LEDGER_STORE_TYPE,
}));
}
let entry = load_state_ledger(store_path)?;
let active = entry
.branches
.iter()
.filter(|branch| branch.status == "active")
.count();
Ok(serde_json::json!({
"ok": true,
"present": true,
"store": store_path,
"entryType": STATE_LEDGER_STORE_TYPE,
"branches": entry.branches.len(),
"activeBranches": active,
"evidence": entry.evidence.len(),
"latestEvidence": entry.evidence.iter().rev().take(8).collect::<Vec<_>>(),
}))
}
pub fn load_state_ledger(store_path: impl AsRef<Path>) -> Result<EpiphanyStateLedgerEntry> {
let store_path = store_path.as_ref();
let mut cache = state_ledger_cache(store_path)?;
@ -162,7 +133,7 @@ pub fn close_state_branch(
fn state_ledger_cache(store_path: &Path) -> Result<CultCache> {
let mut cache = CultCache::new();
cache.register_entry_type::<EpiphanyStateLedgerEntry>()?;
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(store_path));
cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(store_path))?;
Ok(cache)
}

View file

@ -0,0 +1,302 @@
//! Core-owned semantic state contracts.
use std::path::PathBuf;
use serde::Deserialize;
use serde::Serialize;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub struct EpiphanySubgoal {
pub id: String,
pub title: String,
pub status: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub struct EpiphanyInvariant {
pub id: String,
pub description: String,
pub status: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rationale: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub struct EpiphanyCodeRef {
pub path: PathBuf,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub start_line: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub end_line: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub symbol: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum EpiphanyInvestigationDisposition {
#[default]
ResumeReady,
RegatherRequired,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub struct EpiphanyInvestigationCheckpoint {
pub checkpoint_id: String,
pub kind: String,
pub disposition: EpiphanyInvestigationDisposition,
pub focus: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next_action: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub captured_at_turn_id: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub open_questions: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub code_refs: Vec<EpiphanyCodeRef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub evidence_ids: Vec<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub struct EpiphanyObservation {
pub id: String,
pub summary: String,
pub source_kind: String,
pub status: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub code_refs: Vec<EpiphanyCodeRef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub evidence_ids: Vec<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub struct EpiphanyEvidenceRecord {
pub id: String,
pub kind: String,
pub status: String,
pub summary: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub code_refs: Vec<EpiphanyCodeRef>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub struct EpiphanyModeState {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kind: Option<EpiphanyModeKind>,
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
#[serde(rename_all = "snake_case")]
pub enum EpiphanyModeKind {
Plan,
#[default]
#[serde(
alias = "code",
alias = "pair_programming",
alias = "execute",
alias = "custom"
)]
Default,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub struct EpiphanyMemoryGraphSnapshot {
pub graph_id: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub domains: Vec<EpiphanyMemoryDomain>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub nodes: Vec<EpiphanyMemoryNode>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub edges: Vec<EpiphanyMemoryEdge>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub frontier: Vec<RepoFrontierItem>,
}
/// A durable piece of unfinished repository anatomy. This is Modeling-owned state,
/// not a scheduler job or a presentation card.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub struct RepoFrontierItem {
pub id: String,
pub migration_body: String,
pub question: String,
pub gap: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub target_claim_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
/// Canonical repository-relative path ceiling for this wound. This is the
/// scope that downstream Planning may narrow and Hands may eventually
/// change; inspected files and evidence provenance belong elsewhere.
pub repository_scope: Vec<String>,
pub recommended_next_organ: String,
/// The exact plan admitted by Mind for this frontier item. Imagination may
/// propose this payload, but only the dedicated model transition may make
/// it part of Modeling-owned truth.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub adopted_plan: Option<RepoFrontierAdoptedPlan>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub dependency_item_ids: Vec<String>,
#[serde(default)]
pub status: RepoFrontierStatus,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub evidence_refs: Vec<String>,
/// Exact immutable public sources selected upstream for Eyes. These are
/// causal retrieval inputs, not evidence produced by Modeling.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub public_source_refs: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub created_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub updated_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub retired_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub superseded_by: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub struct RepoFrontierAdoptedPlan {
pub planning_request_id: String,
pub result_id: String,
pub job_id: String,
pub candidate_id: String,
pub candidate_sha256: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub safe_paths: Vec<String>,
pub action: String,
pub command: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub checks: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub stop_conditions: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub rollback_steps: Vec<String>,
pub commit_message: String,
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum RepoFrontierStatus {
#[default]
Proposed,
Active,
Blocked,
Resolved,
Retired,
Superseded,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub struct EpiphanyMemoryDomain {
pub id: String,
pub title: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default)]
pub lifecycle: EpiphanyMemoryLifecycle,
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
#[serde(rename_all = "snake_case")]
pub enum EpiphanyMemoryLifecycle {
#[default]
Observed,
Proposed,
Accepted,
Retired,
Stale,
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
#[serde(rename_all = "snake_case")]
pub enum EpiphanyMemoryNodeKind {
#[default]
Domain,
Module,
Crate,
Binary,
Schema,
RuntimeContract,
Adapter,
TestSeam,
StateStore,
Summary,
Other,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub struct EpiphanyMemoryNode {
pub id: String,
pub domain_id: String,
pub kind: EpiphanyMemoryNodeKind,
pub title: String,
pub claim: String,
pub question: String,
pub tension: String,
pub action_implication: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub anchors: Vec<EpiphanyMemoryAnchor>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub source_hashes: Vec<String>,
#[serde(default)]
pub lifecycle: EpiphanyMemoryLifecycle,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub created_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub updated_at: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
#[serde(rename_all = "snake_case")]
pub enum EpiphanyMemoryEdgeKind {
#[default]
Owns,
Reads,
Writes,
Derives,
Adapts,
Persists,
Launches,
Verifies,
Supports,
Contradicts,
Grounds,
Triggers,
DependsOn,
Other,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub struct EpiphanyMemoryEdge {
pub id: String,
pub source_id: String,
pub target_id: String,
pub kind: EpiphanyMemoryEdgeKind,
pub claim: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub anchors: Vec<EpiphanyMemoryAnchor>,
#[serde(default)]
pub lifecycle: EpiphanyMemoryLifecycle,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub struct EpiphanyMemoryAnchor {
pub id: String,
pub kind: String,
pub target: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub code_ref: Option<EpiphanyCodeRef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub evidence_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_hash: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
}

View file

@ -1,7 +1,5 @@
use cultcache_rs::DatabaseEntry;
pub const SUBSTRATE_GATE_REPO_ACCESS_GRANT_RECEIPT_TYPE: &str =
"epiphany.substrate_gate.repo_access_grant_receipt";
pub const SUBSTRATE_GATE_REPO_ACCESS_GRANT_RECEIPT_SCHEMA_VERSION: &str =
"epiphany.substrate_gate.repo_access_grant_receipt.v0";
pub const SUBSTRATE_GATE_SOURCE_READ_OPERATION: &str = "read";
@ -71,10 +69,7 @@ pub fn substrate_gate_operation_for_governed_tool(
tool_name: &str,
) -> Option<&'static str> {
match (server, tool_name) {
(
"epiphany_source",
"read_file" | "directory_inventory" | "git_show" | "read_hands_receipt",
)
("epiphany_source", "read_file" | "directory_inventory" | "git_show")
| ("epiphany_state", "resident_grant_lifecycle") => {
Some(SUBSTRATE_GATE_SOURCE_READ_OPERATION)
}
@ -83,26 +78,6 @@ pub fn substrate_gate_operation_for_governed_tool(
}
}
pub fn substrate_gate_repo_work_planning_grant(
receipt_id: String,
runtime_job_id: String,
granted_paths: Vec<String>,
granted_at: String,
) -> SubstrateGateRepoAccessGrantReceipt {
SubstrateGateRepoAccessGrantReceipt {
schema_version: SUBSTRATE_GATE_REPO_ACCESS_GRANT_RECEIPT_SCHEMA_VERSION.to_string(),
receipt_id,
runtime_job_id,
binding_id: "repo-work-runner".to_string(),
role: "epiphany-hands".to_string(),
authority_scope: "repo.branch_local_work".to_string(),
granted_operations: vec!["read".to_string(), "snapshot".to_string()],
granted_paths,
granted_at,
contract: "Substrate Gate grants read/snapshot access for repo-work planning only; mutation awaits an approved Hands review.".to_string(),
}
}
pub fn substrate_gate_coordinator_implementation_grant(
receipt_id: String,
runtime_job_id: String,

View file

@ -1,4 +1,3 @@
use super::EpiphanyCrrcAction;
use crate::EpiphanyCurrentWorkProjection;
use serde::{Deserialize, Serialize};
@ -12,20 +11,6 @@ pub enum EpiphanyCoordinatorRoleStatus {
Blocked,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyCoordinatorRoleResultStatus {
MissingState,
MissingBinding,
BackendUnavailable,
BackendMissing,
Pending,
Running,
Completed,
Failed,
Cancelled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyCoordinatorAction {
@ -60,7 +45,6 @@ pub enum EpiphanyCoordinatorAction {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EpiphanyCoordinatorInput {
pub mind_present: bool,
pub crrc_action: EpiphanyCrrcAction,
pub current_work: EpiphanyCurrentWorkProjection,
}

View file

@ -1,6 +1,6 @@
use crate::{
EpiphanyAgentPassContinuationAction, EpiphanyCoordinatorAction, EpiphanyCoordinatorDecision,
EpiphanyCoordinatorInput, EpiphanyCrrcAction, RepoFrontierPlanningLifecycleStage,
EpiphanyCoordinatorInput, RepoFrontierPlanningLifecycleStage,
RepoFrontierResearchContinuationAction,
};
@ -24,7 +24,7 @@ pub fn recommend_coordinator_action(
);
}
if input.crrc_action == EpiphanyCrrcAction::RegatherManually {
if input.current_work.operator_regather_required {
return decision(
EpiphanyCoordinatorAction::RegatherManually,
"The accepted continuity decision requires explicit operator regather.",
@ -134,10 +134,9 @@ fn continuation_decision(
launch,
&format!("The exact {label} obligation has no live attempt."),
),
EpiphanyAgentPassContinuationAction::Wait => decision(
wait,
&format!("The exact {label} attempt is still live."),
),
EpiphanyAgentPassContinuationAction::Wait => {
decision(wait, &format!("The exact {label} attempt is still live."))
}
EpiphanyAgentPassContinuationAction::Review => decision(
review,
&format!("The exact {label} result awaits its family admission owner."),

View file

@ -1,27 +0,0 @@
use serde::Deserialize;
use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyCrrcResultStatus {
MissingState,
MissingBinding,
BackendUnavailable,
BackendMissing,
Pending,
Running,
Completed,
Failed,
Cancelled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyCrrcAction {
Continue,
LaunchReorientWorker,
WaitForReorientWorker,
ReviewReorientResult,
AcceptReorientResult,
RegatherManually,
}

View file

@ -1,7 +1,5 @@
mod coordinator_contract;
mod coordinator_decision;
mod crrc;
mod reorient;
mod research_decision;
mod role_board;
mod role_result;
@ -9,9 +7,6 @@ mod worker_launch;
pub use coordinator_contract::*;
pub use coordinator_decision::recommend_coordinator_action;
pub use crrc::EpiphanyCrrcAction;
pub use crrc::EpiphanyCrrcResultStatus;
pub use reorient::EpiphanyReorientAction;
pub use research_decision::EpiphanyResearchDecision;
pub use role_board::EpiphanyRoleBoardInput;
pub use role_board::EpiphanyRoleBoardLane;
@ -21,6 +16,8 @@ pub use role_result::EpiphanyRoleFindingInterpretation;
pub use role_result::EpiphanyRoleResultRoleId;
pub use role_result::interpret_runtime_reorient_worker_result;
pub use role_result::interpret_runtime_role_worker_result;
pub use worker_launch::ADMITTED_MODEL_DIRECTION_CONSIDERATION_CONTEXT_CONTRACT;
pub use worker_launch::ADMITTED_MODEL_DIRECTION_CONSIDERATION_CONTEXT_SCHEMA_VERSION;
pub use worker_launch::AdmittedModelDirectionConsiderationContextProjection;
pub use worker_launch::EpiphanyReorientWorkerLaunchDocument;
pub use worker_launch::EpiphanyRoleWorkerLaunchDocument;

View file

@ -1,13 +0,0 @@
use serde::{Deserialize, Serialize};
/// Display projection of an accepted keyed continuity decision.
///
/// This enum does not decide whether Reorientation should run. The keyed
/// current-work owner in `reorientation_work` owns launch, wait, review, and
/// admission behavior.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum EpiphanyReorientAction {
Resume,
Regather,
}

View file

@ -1,7 +1,7 @@
use crate::state_model::EpiphanyEvidenceRecord;
use crate::state_model::EpiphanyInvestigationCheckpoint;
use crate::state_model::EpiphanyObservation;
use anyhow::{Result, anyhow};
use epiphany_state_model::EpiphanyEvidenceRecord;
use epiphany_state_model::EpiphanyInvestigationCheckpoint;
use epiphany_state_model::EpiphanyObservation;
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;

View file

@ -21,20 +21,18 @@ pub struct EpiphanyRoleBoardLane {
pub fn derive_role_board(input: EpiphanyRoleBoardInput) -> Vec<EpiphanyRoleBoardLane> {
let missing = !input.mind_present;
let lane = |title: &str, status, note: &str| {
EpiphanyRoleBoardLane {
title: title.into(),
status: if missing {
EpiphanyCoordinatorRoleStatus::Blocked
} else {
status
},
note: if missing {
"Canonical keyed Mind is missing.".into()
} else {
note.into()
},
}
let lane = |title: &str, status, note: &str| EpiphanyRoleBoardLane {
title: title.into(),
status: if missing {
EpiphanyCoordinatorRoleStatus::Blocked
} else {
status
},
note: if missing {
"Canonical keyed Mind is missing.".into()
} else {
note.into()
},
};
let modeling_action = input

View file

@ -23,7 +23,7 @@ pub struct EpiphanyRoleFindingInterpretation {
pub frontier_node_ids: Vec<String>,
pub evidence_ids: Vec<String>,
pub artifact_refs: Vec<String>,
pub runtime_result_id: Option<String>,
pub result_id: Option<String>,
pub runtime_job_id: Option<String>,
pub open_questions: Vec<String>,
pub evidence_gaps: Vec<String>,
@ -32,9 +32,6 @@ pub struct EpiphanyRoleFindingInterpretation {
pub repo_model_mutation_proposal: Option<crate::EpiphanyRepoModelMutationProposal>,
pub job_error: Option<String>,
pub item_error: Option<String>,
pub verification_request_id: Option<String>,
pub frontier_route_id: Option<String>,
pub proposal_modeling_request_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@ -48,7 +45,7 @@ pub struct EpiphanyReorientFindingInterpretation {
pub frontier_node_ids: Vec<String>,
pub evidence_ids: Vec<String>,
pub artifact_refs: Vec<String>,
pub runtime_result_id: Option<String>,
pub result_id: Option<String>,
pub runtime_job_id: Option<String>,
pub job_error: Option<String>,
pub item_error: Option<String>,
@ -92,7 +89,7 @@ pub fn interpret_runtime_role_worker_result(
frontier_node_ids: result.frontier_node_ids.clone(),
evidence_ids: result.evidence_ids.clone(),
artifact_refs: result.artifact_refs.clone(),
runtime_result_id: Some(result.result_id.clone()),
result_id: Some(result.result_id.clone()),
runtime_job_id: Some(result.job_id.clone()),
open_questions: result.open_questions.clone(),
evidence_gaps: result.evidence_gaps.clone(),
@ -122,9 +119,6 @@ pub fn interpret_runtime_role_worker_result(
| EpiphanyRoleResultRoleId::Verification
| EpiphanyRoleResultRoleId::Reorientation => item_error,
},
verification_request_id: result.verification_request_id.clone(),
frontier_route_id: result.frontier_route_id.clone(),
proposal_modeling_request_id: result.proposal_modeling_request_id.clone(),
}
}
@ -140,7 +134,7 @@ pub fn interpret_runtime_reorient_worker_result(
frontier_node_ids: result.frontier_node_ids.clone(),
evidence_ids: result.evidence_ids.clone(),
artifact_refs: result.artifact_refs.clone(),
runtime_result_id: Some(result.result_id.clone()),
result_id: Some(result.result_id.clone()),
runtime_job_id: Some(result.job_id.clone()),
job_error: None,
item_error: result.item_error.clone(),

View file

@ -1,7 +1,7 @@
use serde::Deserialize;
use serde::Serialize;
pub const ROLE_WORKER_OUTPUT_CONTRACT_ID: &str = "epiphany.worker.role_result.v4";
pub const ROLE_WORKER_OUTPUT_CONTRACT_ID: &str = "epiphany.worker.role_result.v5";
pub const REORIENT_WORKER_OUTPUT_CONTRACT_ID: &str = "epiphany.worker.reorient_result.v0";
pub const REPO_FRONTIER_PROPOSAL_MODELING_CONTEXT_SCHEMA_VERSION: &str =
"epiphany.worker.repo_frontier_proposal_modeling_context.v4";
@ -16,9 +16,9 @@ pub const REPO_FRONTIER_RESEARCH_CONTEXT_SCHEMA_VERSION: &str =
pub const REPO_FRONTIER_RESEARCH_CONTEXT_CONTRACT: &str =
"epiphany.repo_frontier_research_context.v3";
pub const REPO_FRONTIER_VERIFICATION_CONTEXT_SCHEMA_VERSION: &str =
"epiphany.worker.repo_frontier_verification_context.v1";
"epiphany.worker.repo_frontier_verification_context.v2";
pub const REPO_FRONTIER_VERIFICATION_CONTEXT_CONTRACT: &str =
"epiphany.repo_frontier_verification_context.v1";
"epiphany.repo_frontier_verification_context.v2";
pub const REPO_FRONTIER_PLAN_MIND_CONTEXT_SCHEMA_VERSION: &str =
"epiphany.worker.repo_frontier_plan_mind_context.v0";
pub const REPO_FRONTIER_PLAN_MIND_CONTEXT_CONTRACT: &str =
@ -102,6 +102,25 @@ pub struct EpiphanyRoleWorkerLaunchDocument {
Option<AdmittedModelDirectionConsiderationContextProjection>,
}
impl EpiphanyRoleWorkerLaunchDocument {
pub(crate) fn family_context_count(&self) -> usize {
[
self.proposal_modeling_context.is_some(),
self.frontier_verdict_modeling_context.is_some(),
self.frontier_planning_context.is_some(),
self.frontier_research_context.is_some(),
self.frontier_verification_context.is_some(),
self.frontier_plan_mind_context.is_some(),
self.imagination_consideration_context.is_some(),
self.admitted_model_direction_consideration_context
.is_some(),
]
.into_iter()
.filter(|present| *present)
.count()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RepoFrontierResearchContextProjection {
@ -222,7 +241,6 @@ pub struct RepoFrontierVerificationContextProjection {
pub route: crate::RepoFrontierRoute,
pub hands_authority: crate::RepoFrontierHandsAuthority,
pub hands_intent: crate::HandsActionIntent,
pub hands_review: crate::HandsActionReview,
pub patch_receipt: crate::HandsPatchReceipt,
pub command_receipt: crate::HandsCommandReceipt,
pub commit_receipt: crate::HandsCommitReceipt,

View file

@ -10,5 +10,9 @@ name = "epiphany_model_adapter"
path = "src/lib.rs"
[dependencies]
cultcache-rs = { path = "../vendor/cultcache-rs" }
anyhow = "1"
codex-connector = { git = "https://github.com/GameCult/CodexConnector", rev = "be1776ab455fd79bd74b393518ab07211dc9c1f0", default-features = false }
cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "a0813c6eed24d30bf88073ef615b633c77ebfcd6", package = "cultcache-rs" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"

View file

@ -1,298 +1,5 @@
use cultcache_rs::DatabaseEntry;
use serde::Deserialize;
use serde::Serialize;
mod native;
mod provider;
pub const MODEL_ADAPTER_REQUEST_SCHEMA_ID: &str = "epiphany.model_request.v0";
pub const MODEL_ADAPTER_EVENT_SCHEMA_ID: &str = "epiphany.model_stream_event.v0";
pub const MODEL_ADAPTER_RECEIPT_SCHEMA_ID: &str = "epiphany.model_receipt.v0";
pub const MODEL_ADAPTER_STATUS_SCHEMA_ID: &str = "epiphany.model_adapter_status.v0";
pub const MODEL_CONNECTOR_ENVELOPE_SCHEMA_ID: &str = "epiphany.model_connector_envelope.v1";
pub const MODEL_CONNECTOR_INVOCATION_SCHEMA_ID: &str = "epiphany.model_connector_invocation.v1";
pub const MODEL_CONNECTOR_RESULT_SCHEMA_ID: &str = "epiphany.model_connector_result.v1";
pub const MODEL_CONNECTOR_STATUS_SCHEMA_ID: &str = "epiphany.model_connector_status.v1";
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "epiphany.model_adapter_status.v0",
schema = "EpiphanyModelAdapterStatus"
)]
pub struct EpiphanyModelAdapterStatus {
#[cultcache(key = 0)]
pub schema_id: String,
#[cultcache(key = 1)]
pub adapter_id: String,
#[cultcache(key = 2)]
pub provider: String,
#[cultcache(key = 3, default)]
pub default_model: Option<String>,
#[cultcache(key = 4)]
pub streaming_supported: bool,
#[cultcache(key = 5)]
pub provider_transport_attached: bool,
}
#[derive(Debug, Clone, PartialEq, DatabaseEntry)]
#[cultcache(type = "epiphany.model_request.v0", schema = "EpiphanyModelRequest")]
pub struct EpiphanyModelRequest {
#[cultcache(key = 0)]
pub schema_id: String,
#[cultcache(key = 1)]
pub request_id: String,
#[cultcache(key = 2)]
pub conversation_id: String,
#[cultcache(key = 3)]
pub provider: String,
#[cultcache(key = 4)]
pub model: String,
#[cultcache(key = 5)]
pub instructions: String,
#[cultcache(key = 6, default)]
pub input: Vec<EpiphanyModelInputItem>,
#[cultcache(key = 7, default)]
pub reasoning_effort: Option<String>,
#[cultcache(key = 8, default)]
pub reasoning_summary: Option<String>,
#[cultcache(key = 9, default)]
pub service_tier: Option<String>,
#[cultcache(key = 10, default)]
pub output_contract_id: Option<String>,
#[cultcache(key = 11, default)]
pub previous_response_id: Option<String>,
#[cultcache(key = 12, default)]
pub tools: Vec<EpiphanyModelToolDefinition>,
#[cultcache(key = 13, default)]
pub output_schema_json: Option<String>,
#[cultcache(key = 14, default)]
pub source_worker_job_id: Option<String>,
#[cultcache(key = 15, default)]
pub reasoning_basis_id: Option<String>,
#[cultcache(key = 16, default)]
pub max_output_tokens: Option<u32>,
#[cultcache(key = 17, default)]
pub prompt_cache_key: Option<String>,
}
impl EpiphanyModelRequest {
pub fn new(
request_id: impl Into<String>,
conversation_id: impl Into<String>,
provider: impl Into<String>,
model: impl Into<String>,
instructions: impl Into<String>,
) -> Self {
Self {
schema_id: MODEL_ADAPTER_REQUEST_SCHEMA_ID.to_string(),
request_id: request_id.into(),
conversation_id: conversation_id.into(),
provider: provider.into(),
model: model.into(),
instructions: instructions.into(),
input: Vec::new(),
reasoning_effort: None,
reasoning_summary: None,
service_tier: None,
output_contract_id: None,
previous_response_id: None,
tools: Vec::new(),
output_schema_json: None,
source_worker_job_id: None,
reasoning_basis_id: None,
max_output_tokens: None,
prompt_cache_key: None,
}
}
}
/// Encrypted CultNet cargo. The correlation fields are deliberately repeated
/// inside the authenticated plaintext and must match after decryption.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EpiphanyModelConnectorEnvelope {
pub schema_id: String,
pub request_id: String,
pub message_kind: String,
pub nonce: Vec<u8>,
pub ciphertext: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, DatabaseEntry)]
#[cultcache(
type = "epiphany.model_connector_invocation.v1",
schema = "EpiphanyModelConnectorInvocation"
)]
pub struct EpiphanyModelConnectorInvocation {
#[cultcache(key = 0)]
pub schema_id: String,
#[cultcache(key = 1)]
pub request_id: String,
#[cultcache(key = 2)]
pub caller_runtime_id: String,
#[cultcache(key = 3)]
pub expires_at_unix_ms: u64,
#[cultcache(key = 4)]
pub request: EpiphanyModelRequest,
}
#[derive(Debug, Clone, PartialEq, DatabaseEntry)]
#[cultcache(
type = "epiphany.model_connector_result.v1",
schema = "EpiphanyModelConnectorResult"
)]
pub struct EpiphanyModelConnectorResult {
#[cultcache(key = 0)]
pub schema_id: String,
#[cultcache(key = 1)]
pub request_id: String,
#[cultcache(key = 2)]
pub accepted: bool,
#[cultcache(key = 3, default)]
pub events: Vec<EpiphanyModelStreamEvent>,
#[cultcache(key = 4, default)]
pub error: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "epiphany.model_connector_status.v1",
schema = "EpiphanyModelConnectorStatus"
)]
pub struct EpiphanyModelConnectorStatus {
#[cultcache(key = 0)]
pub schema_id: String,
#[cultcache(key = 1)]
pub provider_id: String,
#[cultcache(key = 2)]
pub capability_id: String,
#[cultcache(key = 3)]
pub request_schema_id: String,
#[cultcache(key = 4)]
pub result_schema_id: String,
#[cultcache(key = 5)]
pub envelope_schema_id: String,
#[cultcache(key = 6)]
pub transport_protocol: String,
#[cultcache(key = 7)]
pub host: String,
#[cultcache(key = 8)]
pub port: u16,
#[cultcache(key = 9)]
pub max_payload_bytes: u32,
#[cultcache(key = 10)]
pub max_parallel_requests: u32,
#[cultcache(key = 11)]
pub model: String,
#[cultcache(key = 12)]
pub ready: bool,
#[cultcache(key = 13)]
pub updated_at_utc: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EpiphanyModelToolDefinition {
pub name: String,
pub description: String,
pub parameters_json: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum EpiphanyModelInputItem {
UserText {
text: String,
},
AssistantText {
text: String,
},
ToolCall {
call_id: String,
name: String,
arguments: String,
},
ToolResult {
call_id: String,
output: String,
},
}
#[derive(Debug, Clone, PartialEq, DatabaseEntry)]
#[cultcache(
type = "epiphany.model_stream_event.v0",
schema = "EpiphanyModelStreamEvent"
)]
pub struct EpiphanyModelStreamEvent {
#[cultcache(key = 0)]
pub schema_id: String,
#[cultcache(key = 1)]
pub request_id: String,
#[cultcache(key = 2)]
pub provider: String,
#[cultcache(key = 3)]
pub sequence: u64,
#[cultcache(key = 4)]
pub payload: EpiphanyModelStreamPayload,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum EpiphanyModelStreamPayload {
TextDelta {
text: String,
},
ReasoningDelta {
text: String,
},
ToolCall {
call_id: String,
name: String,
arguments: String,
},
Completed {
receipt: EpiphanyModelReceipt,
},
Failed {
message: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
#[cultcache(type = "epiphany.model_receipt.v0", schema = "EpiphanyModelReceipt")]
pub struct EpiphanyModelReceipt {
#[cultcache(key = 0)]
pub schema_id: String,
#[cultcache(key = 1)]
pub request_id: String,
#[cultcache(key = 2)]
pub provider: String,
#[cultcache(key = 3)]
pub model: String,
#[cultcache(key = 4, default)]
pub provider_response_id: Option<String>,
#[cultcache(key = 5, default)]
pub input_tokens: Option<u64>,
#[cultcache(key = 6, default)]
pub output_tokens: Option<u64>,
#[cultcache(key = 7, default)]
pub reasoning_output_tokens: Option<u64>,
#[cultcache(key = 8, default)]
pub transport: Option<String>,
#[cultcache(key = 9, default)]
pub cached_input_tokens: Option<u64>,
}
impl EpiphanyModelReceipt {
pub fn new(
request_id: impl Into<String>,
provider: impl Into<String>,
model: impl Into<String>,
) -> Self {
Self {
schema_id: MODEL_ADAPTER_RECEIPT_SCHEMA_ID.to_string(),
request_id: request_id.into(),
provider: provider.into(),
model: model.into(),
provider_response_id: None,
input_tokens: None,
output_tokens: None,
reasoning_output_tokens: None,
transport: None,
cached_input_tokens: None,
}
}
}
pub use native::*;
pub use provider::*;

View file

@ -0,0 +1,192 @@
use cultcache_rs::DatabaseEntry;
use serde::Deserialize;
use serde::Serialize;
pub const MODEL_ADAPTER_REQUEST_SCHEMA_ID: &str = "epiphany.model_request.v0";
pub const MODEL_ADAPTER_EVENT_SCHEMA_ID: &str = "epiphany.model_stream_event.v0";
pub const MODEL_ADAPTER_RECEIPT_SCHEMA_ID: &str = "epiphany.model_receipt.v1";
#[derive(Debug, Clone, PartialEq, DatabaseEntry)]
#[cultcache(type = "epiphany.model_request.v0", schema = "EpiphanyModelRequest")]
pub struct EpiphanyModelRequest {
#[cultcache(key = 0)]
pub schema_id: String,
#[cultcache(key = 1)]
pub request_id: String,
#[cultcache(key = 2)]
pub conversation_id: String,
#[cultcache(key = 3)]
pub provider: String,
#[cultcache(key = 4)]
pub model: String,
#[cultcache(key = 5)]
pub instructions: String,
#[cultcache(key = 6, default)]
pub input: Vec<EpiphanyModelInputItem>,
#[cultcache(key = 7, default)]
pub reasoning_effort: Option<String>,
#[cultcache(key = 8, default)]
pub reasoning_summary: Option<String>,
#[cultcache(key = 9, default)]
pub service_tier: Option<String>,
#[cultcache(key = 10, default)]
pub output_contract_id: Option<String>,
#[cultcache(key = 11, default)]
pub previous_response_id: Option<String>,
#[cultcache(key = 12, default)]
pub tools: Vec<EpiphanyModelToolDefinition>,
#[cultcache(key = 13, default)]
pub output_schema_json: Option<String>,
#[cultcache(key = 14, default)]
pub source_worker_job_id: Option<String>,
#[cultcache(key = 15, default)]
pub reasoning_basis_id: Option<String>,
}
impl EpiphanyModelRequest {
pub fn new(
request_id: impl Into<String>,
conversation_id: impl Into<String>,
provider: impl Into<String>,
model: impl Into<String>,
instructions: impl Into<String>,
) -> Self {
Self {
schema_id: MODEL_ADAPTER_REQUEST_SCHEMA_ID.to_string(),
request_id: request_id.into(),
conversation_id: conversation_id.into(),
provider: provider.into(),
model: model.into(),
instructions: instructions.into(),
input: Vec::new(),
reasoning_effort: None,
reasoning_summary: None,
service_tier: None,
output_contract_id: None,
previous_response_id: None,
tools: Vec::new(),
output_schema_json: None,
source_worker_job_id: None,
reasoning_basis_id: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EpiphanyModelToolDefinition {
pub name: String,
pub description: String,
pub parameters_json: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum EpiphanyModelInputItem {
UserText {
text: String,
},
AssistantText {
text: String,
},
ToolCall {
call_id: String,
name: String,
arguments: String,
},
ToolResult {
call_id: String,
output: String,
},
}
#[derive(Debug, Clone, PartialEq, DatabaseEntry)]
#[cultcache(
type = "epiphany.model_stream_event.v0",
schema = "EpiphanyModelStreamEvent"
)]
pub struct EpiphanyModelStreamEvent {
#[cultcache(key = 0)]
pub schema_id: String,
#[cultcache(key = 1)]
pub request_id: String,
#[cultcache(key = 2)]
pub provider: String,
#[cultcache(key = 3)]
pub sequence: u64,
#[cultcache(key = 4)]
pub payload: EpiphanyModelStreamPayload,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum EpiphanyModelStreamPayload {
TextDelta {
text: String,
},
ReasoningDelta {
text: String,
},
ToolCall {
call_id: String,
name: String,
arguments: String,
},
Completed {
receipt: Box<EpiphanyModelReceipt>,
},
Failed {
message: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
#[cultcache(type = "epiphany.model_receipt.v1", schema = "EpiphanyModelReceipt")]
pub struct EpiphanyModelReceipt {
#[cultcache(key = 0)]
pub schema_id: String,
#[cultcache(key = 1)]
pub request_id: String,
#[cultcache(key = 2)]
pub provider: String,
#[cultcache(key = 3)]
pub model: String,
#[cultcache(key = 4, default)]
pub provider_response_id: Option<String>,
#[cultcache(key = 5, default)]
pub input_tokens: Option<u64>,
#[cultcache(key = 6, default)]
pub output_tokens: Option<u64>,
#[cultcache(key = 7, default)]
pub reasoning_output_tokens: Option<u64>,
#[cultcache(key = 8, default)]
pub transport: Option<String>,
#[cultcache(key = 9, default)]
pub caller_runtime_id: Option<String>,
#[cultcache(key = 10, default)]
pub native_request_sha256: Option<String>,
#[cultcache(key = 11, default)]
pub provider_request_sha256: Option<String>,
#[cultcache(key = 12, default)]
pub cached_input_tokens: Option<u64>,
}
impl EpiphanyModelReceipt {
pub fn new(
request_id: impl Into<String>,
provider: impl Into<String>,
model: impl Into<String>,
) -> Self {
Self {
schema_id: MODEL_ADAPTER_RECEIPT_SCHEMA_ID.to_string(),
request_id: request_id.into(),
provider: provider.into(),
model: model.into(),
provider_response_id: None,
input_tokens: None,
output_tokens: None,
reasoning_output_tokens: None,
transport: None,
caller_runtime_id: None,
native_request_sha256: None,
provider_request_sha256: None,
cached_input_tokens: None,
}
}
}

View file

@ -0,0 +1,922 @@
use crate::{
EpiphanyModelInputItem, EpiphanyModelReceipt, EpiphanyModelRequest, EpiphanyModelStreamEvent,
EpiphanyModelStreamPayload, EpiphanyModelToolDefinition, MODEL_ADAPTER_EVENT_SCHEMA_ID,
};
use anyhow::{Result, anyhow};
use codex_connector::{CodexInputItem, CodexProviderRequest, CodexToolChoice, CodexToolDefinition};
use cultcache_rs::DatabaseEntry;
use serde::Deserialize;
use serde::Serialize;
use sha2::{Digest, Sha256};
#[derive(Debug, Clone, PartialEq, DatabaseEntry)]
#[cultcache(
type = "epiphany.provider_request.v2",
schema = "EpiphanyProviderRequest"
)]
pub struct EpiphanyProviderRequest {
#[cultcache(key = 0)]
pub payload: EpiphanyProviderRequestPayload,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum EpiphanyProviderRequestPayload {
Codex(CodexProviderRequest),
OpenRouter(EpiphanyOpenRouterRequest),
}
impl EpiphanyProviderRequest {
pub fn request_id(&self) -> &str {
match &self.payload {
EpiphanyProviderRequestPayload::Codex(request) => &request.request_id,
EpiphanyProviderRequestPayload::OpenRouter(request) => &request.request_id,
}
}
pub fn conversation_id(&self) -> &str {
match &self.payload {
EpiphanyProviderRequestPayload::Codex(request) => &request.conversation_id,
EpiphanyProviderRequestPayload::OpenRouter(request) => &request.conversation_id,
}
}
pub fn model(&self) -> &str {
match &self.payload {
EpiphanyProviderRequestPayload::Codex(request) => &request.model,
EpiphanyProviderRequestPayload::OpenRouter(request) => &request.model,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EpiphanyOpenRouterRequest {
pub request_id: String,
pub conversation_id: String,
pub model: String,
pub instructions: String,
pub input: Vec<EpiphanyModelInputItem>,
pub reasoning_effort: Option<String>,
pub reasoning_summary: Option<String>,
pub service_tier: Option<String>,
pub output_contract_id: Option<String>,
pub previous_response_id: Option<String>,
pub tools: Vec<EpiphanyModelToolDefinition>,
pub output_schema_json: Option<String>,
}
impl EpiphanyOpenRouterRequest {
pub fn new(
request_id: impl Into<String>,
conversation_id: impl Into<String>,
model: impl Into<String>,
instructions: impl Into<String>,
) -> Self {
Self {
request_id: request_id.into(),
conversation_id: conversation_id.into(),
model: model.into(),
instructions: instructions.into(),
input: Vec::new(),
reasoning_effort: None,
reasoning_summary: None,
service_tier: None,
output_contract_id: None,
previous_response_id: None,
tools: Vec::new(),
output_schema_json: None,
}
}
}
/// The only lowering from a native model request to an exact provider request.
pub fn request_from_native(request: &EpiphanyModelRequest) -> Result<EpiphanyProviderRequest> {
let payload = match request.provider.as_str() {
"openai-codex" | "openai" => {
EpiphanyProviderRequestPayload::Codex(codex_request_from_native(request)?)
}
"openrouter" => {
EpiphanyProviderRequestPayload::OpenRouter(openrouter_request_from_native(request))
}
_ => return Err(anyhow!("unsupported model provider {:?}", request.provider)),
};
Ok(EpiphanyProviderRequest { payload })
}
fn openrouter_request_from_native(request: &EpiphanyModelRequest) -> EpiphanyOpenRouterRequest {
EpiphanyOpenRouterRequest {
request_id: request.request_id.clone(),
conversation_id: request.conversation_id.clone(),
model: request.model.clone(),
instructions: request.instructions.clone(),
input: request.input.clone(),
reasoning_effort: request.reasoning_effort.clone(),
reasoning_summary: request.reasoning_summary.clone(),
service_tier: request.service_tier.clone(),
output_contract_id: request.output_contract_id.clone(),
previous_response_id: request.previous_response_id.clone(),
tools: request.tools.clone(),
output_schema_json: request.output_schema_json.clone(),
}
}
fn codex_request_from_native(request: &EpiphanyModelRequest) -> Result<CodexProviderRequest> {
let has_tool_result = request
.input
.iter()
.any(|item| matches!(item, EpiphanyModelInputItem::ToolResult { .. }));
let output_format_name = request.output_schema_json.as_ref().map(|_| {
provider_format_name(
request
.output_contract_id
.as_deref()
.unwrap_or(&request.request_id),
)
});
let output_schema_json = request
.output_schema_json
.as_deref()
.map(strict_provider_schema)
.transpose()?
.map(|schema| serde_json::to_string(&schema))
.transpose()?;
let tools = request
.tools
.iter()
.map(|tool| {
Ok(CodexToolDefinition {
name: tool.name.clone(),
description: tool.description.clone(),
parameters_json: serde_json::to_string(&strict_provider_schema(
&tool.parameters_json,
)?)?,
})
})
.collect::<Result<Vec<_>>>()?;
let mut provider = CodexProviderRequest::new(
&request.request_id,
&request.conversation_id,
&request.model,
&request.instructions,
);
provider.input = request.input.iter().map(codex_input_from_native).collect();
provider.reasoning_effort = request.reasoning_effort.clone();
provider.reasoning_summary = request.reasoning_summary.clone();
provider.service_tier = request.service_tier.clone();
provider.output_format_name = output_format_name;
provider.previous_response_id = request.previous_response_id.clone();
provider.tools = tools;
provider.tool_choice = if !provider.tools.is_empty() && !has_tool_result {
CodexToolChoice::Required
} else {
CodexToolChoice::Auto
};
provider.output_schema_json = output_schema_json;
provider.validate()?;
Ok(provider)
}
fn codex_input_from_native(input: &EpiphanyModelInputItem) -> CodexInputItem {
match input {
EpiphanyModelInputItem::UserText { text } => {
CodexInputItem::UserText { text: text.clone() }
}
EpiphanyModelInputItem::AssistantText { text } => {
CodexInputItem::AssistantText { text: text.clone() }
}
EpiphanyModelInputItem::ToolCall {
call_id,
name,
arguments,
} => CodexInputItem::ToolCall {
call_id: provider_call_id(call_id),
name: name.clone(),
arguments: arguments.clone(),
},
EpiphanyModelInputItem::ToolResult { call_id, output } => CodexInputItem::ToolResult {
call_id: provider_call_id(call_id),
output: output.clone(),
},
}
}
/// Project Epiphany's canonical JSON Schema into the strict subset accepted by
/// model providers. Native decoding remains the final validation authority.
pub fn strict_provider_schema(schema_json: &str) -> anyhow::Result<serde_json::Value> {
let mut schema = serde_json::from_str(schema_json)
.map_err(|error| anyhow::anyhow!("provider schema is not valid JSON Schema: {error}"))?;
lower_provider_schema(&mut schema);
close_provider_objects(&mut schema, "$")?;
if !provider_schema_is_strict(&schema) {
return Err(anyhow::anyhow!(
"projected provider output schema is not strict"
));
}
Ok(schema)
}
/// Provider-safe output-format identity derived from the native contract.
pub fn provider_format_name(value: &str) -> String {
let mut name = value
.chars()
.map(|ch| {
if ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-') {
ch
} else {
'_'
}
})
.take(64)
.collect::<String>();
if name.is_empty() {
name.push_str("epiphany_worker_result");
}
name
}
/// Responses call IDs have a narrow alphabet and byte bound. Preserve already
/// legal IDs and derive a stable identity for every other native call ID.
pub fn provider_call_id(value: &str) -> String {
if !value.is_empty()
&& value.len() <= 64
&& value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
{
return value.to_string();
}
let digest = Sha256::digest(value.as_bytes());
let mut projected = String::with_capacity(64);
for byte in digest {
use std::fmt::Write as _;
write!(&mut projected, "{byte:02x}").expect("writing to a String cannot fail");
}
projected
}
pub const OPENROUTER_TERMINAL_TOOL_NAME: &str = "epiphany_submit_typed_result";
/// Lower one typed Epiphany request to OpenRouter's terminal-tool dialect.
pub fn openrouter_request_body(
request: &EpiphanyOpenRouterRequest,
) -> anyhow::Result<serde_json::Value> {
let has_tool_result = request
.input
.iter()
.any(|item| matches!(item, EpiphanyModelInputItem::ToolResult { .. }));
let output_schema = request
.output_schema_json
.as_deref()
.map(strict_provider_schema)
.transpose()?;
let include_terminal_tool =
output_schema.is_some() && (request.tools.is_empty() || has_tool_result);
if request
.tools
.iter()
.any(|tool| tool.name == OPENROUTER_TERMINAL_TOOL_NAME)
{
return Err(anyhow::anyhow!(
"native tool name collides with the OpenRouter terminal decision tool"
));
}
let mut messages = vec![serde_json::json!({
"role": "system",
"content": request.instructions,
})];
for item in &request.input {
messages.push(match item {
EpiphanyModelInputItem::UserText { text } => {
serde_json::json!({"role": "user", "content": text})
}
EpiphanyModelInputItem::AssistantText { text } => {
serde_json::json!({"role": "assistant", "content": text})
}
EpiphanyModelInputItem::ToolCall {
call_id,
name,
arguments,
} => serde_json::json!({
"role": "assistant",
"tool_calls": [{
"id": call_id,
"type": "function",
"function": {"name": name, "arguments": arguments},
}],
}),
EpiphanyModelInputItem::ToolResult { call_id, output } => serde_json::json!({
"role": "tool",
"tool_call_id": call_id,
"content": output,
}),
});
}
let mut tools = request
.tools
.iter()
.map(|tool| {
Ok(serde_json::json!({
"type": "function",
"function": {
"name": tool.name,
"description": tool.description,
"strict": true,
"parameters": strict_provider_schema(&tool.parameters_json)?,
},
}))
})
.collect::<anyhow::Result<Vec<_>>>()?;
if include_terminal_tool {
tools.push(serde_json::json!({
"type": "function",
"function": {
"name": OPENROUTER_TERMINAL_TOOL_NAME,
"description": "Submit the final typed Epiphany decision. Call this only when the pass is complete.",
"strict": true,
"parameters": output_schema.expect("terminal schema exists"),
},
}));
}
let mut body = serde_json::json!({
"model": request.model,
"messages": messages,
"stream": false,
"parallel_tool_calls": false,
});
if !tools.is_empty() {
body["tools"] = serde_json::Value::Array(tools);
body["tool_choice"] = serde_json::Value::String(
if include_terminal_tool || (!request.tools.is_empty() && !has_tool_result) {
"required"
} else {
"auto"
}
.into(),
);
}
if let Some(effort) = request.reasoning_effort.as_deref() {
body["reasoning"] = serde_json::json!({"effort": effort});
}
Ok(body)
}
/// Interpret one non-streaming OpenRouter response into Epiphany's transient
/// event family. The model transcript remains optional; the terminal receipt
/// is authoritative transport evidence.
pub fn openrouter_events_from_response(
request: &EpiphanyOpenRouterRequest,
response_bytes: &[u8],
) -> anyhow::Result<Vec<EpiphanyModelStreamEvent>> {
let response: OpenRouterResponse = serde_json::from_slice(response_bytes)
.map_err(|error| anyhow::anyhow!("OpenRouter returned an invalid response: {error}"))?;
let choice = response
.choices
.into_iter()
.next()
.ok_or_else(|| anyhow::anyhow!("OpenRouter response contained no completion choice"))?;
let mut events = Vec::new();
if let Some(reasoning) = choice.message.reasoning.filter(|value| !value.is_empty()) {
push_openrouter_event(
&mut events,
&request.request_id,
EpiphanyModelStreamPayload::ReasoningDelta { text: reasoning },
);
}
let terminal_calls = choice
.message
.tool_calls
.iter()
.filter(|call| call.function.name == OPENROUTER_TERMINAL_TOOL_NAME)
.collect::<Vec<_>>();
if !terminal_calls.is_empty() {
if terminal_calls.len() != 1 || choice.message.tool_calls.len() != 1 {
return Err(anyhow::anyhow!(
"OpenRouter mixed the terminal decision tool with another tool call"
));
}
let arguments = &terminal_calls[0].function.arguments;
if !matches!(
serde_json::from_str::<serde_json::Value>(arguments),
Ok(serde_json::Value::Object(_))
) {
return Err(anyhow::anyhow!(
"OpenRouter terminal decision tool returned non-object arguments"
));
}
push_openrouter_event(
&mut events,
&request.request_id,
EpiphanyModelStreamPayload::TextDelta {
text: arguments.clone(),
},
);
} else {
if let Some(content) = choice.message.content.filter(|value| !value.is_empty()) {
push_openrouter_event(
&mut events,
&request.request_id,
EpiphanyModelStreamPayload::TextDelta { text: content },
);
}
for call in choice.message.tool_calls {
push_openrouter_event(
&mut events,
&request.request_id,
EpiphanyModelStreamPayload::ToolCall {
call_id: call.id,
name: call.function.name,
arguments: call.function.arguments,
},
);
}
}
if events.is_empty() {
return Err(anyhow::anyhow!(
"OpenRouter response contained neither text nor tool calls"
));
}
let usage = response.usage;
let mut receipt = EpiphanyModelReceipt::new(
&request.request_id,
"openrouter",
response.model.unwrap_or_else(|| request.model.clone()),
);
receipt.provider_response_id = response.id;
receipt.input_tokens = usage.as_ref().and_then(|usage| usage.prompt_tokens);
receipt.output_tokens = usage.as_ref().and_then(|usage| usage.completion_tokens);
receipt.reasoning_output_tokens = usage
.as_ref()
.and_then(|usage| usage.completion_tokens_details.as_ref())
.and_then(|details| details.reasoning_tokens);
receipt.transport = Some("openrouter-chat-completions".into());
push_openrouter_event(
&mut events,
&request.request_id,
EpiphanyModelStreamPayload::Completed {
receipt: Box::new(receipt),
},
);
Ok(events)
}
#[derive(Debug, Deserialize)]
struct OpenRouterResponse {
id: Option<String>,
model: Option<String>,
choices: Vec<OpenRouterChoice>,
usage: Option<OpenRouterUsage>,
}
#[derive(Debug, Deserialize)]
struct OpenRouterChoice {
message: OpenRouterMessage,
}
#[derive(Debug, Deserialize)]
struct OpenRouterMessage {
content: Option<String>,
#[serde(default)]
reasoning: Option<String>,
#[serde(default)]
tool_calls: Vec<OpenRouterToolCall>,
}
#[derive(Debug, Deserialize)]
struct OpenRouterToolCall {
id: String,
function: OpenRouterFunctionCall,
}
#[derive(Debug, Deserialize)]
struct OpenRouterFunctionCall {
name: String,
arguments: String,
}
#[derive(Debug, Deserialize)]
struct OpenRouterUsage {
prompt_tokens: Option<u64>,
completion_tokens: Option<u64>,
completion_tokens_details: Option<OpenRouterCompletionTokenDetails>,
}
#[derive(Debug, Deserialize)]
struct OpenRouterCompletionTokenDetails {
reasoning_tokens: Option<u64>,
}
fn push_openrouter_event(
events: &mut Vec<EpiphanyModelStreamEvent>,
request_id: &str,
payload: EpiphanyModelStreamPayload,
) {
events.push(EpiphanyModelStreamEvent {
schema_id: MODEL_ADAPTER_EVENT_SCHEMA_ID.to_string(),
request_id: request_id.into(),
provider: "openrouter".into(),
sequence: events.len() as u64,
payload,
});
}
fn provider_schema_is_strict(schema: &serde_json::Value) -> bool {
match schema {
serde_json::Value::Object(map) => {
if (map.contains_key("const") || map.contains_key("enum")) && !map.contains_key("type")
{
return false;
}
if schema_map_describes_object(map) {
if map.get("additionalProperties") != Some(&serde_json::Value::Bool(false)) {
return false;
}
let Some(properties) = map.get("properties").and_then(serde_json::Value::as_object)
else {
return false;
};
let Some(required) = map.get("required").and_then(serde_json::Value::as_array)
else {
return false;
};
if properties
.keys()
.any(|key| !required.iter().any(|item| item.as_str() == Some(key)))
{
return false;
}
}
map.values().all(provider_schema_is_strict)
}
serde_json::Value::Array(values) => values.iter().all(provider_schema_is_strict),
_ => true,
}
}
fn close_provider_objects(schema: &mut serde_json::Value, path: &str) -> anyhow::Result<()> {
match schema {
serde_json::Value::Object(map) => {
let describes_object = schema_map_describes_object(map);
if describes_object {
map.insert("type".into(), serde_json::Value::String("object".into()));
let properties = map
.get("properties")
.and_then(serde_json::Value::as_object)
.cloned()
.unwrap_or_default();
let canonical_required = map
.get("required")
.and_then(serde_json::Value::as_array)
.cloned()
.unwrap_or_default();
for required in &canonical_required {
let required = required.as_str().ok_or_else(|| {
anyhow::anyhow!("provider schema {path} has a non-string required key")
})?;
if !properties.contains_key(required) {
return Err(anyhow::anyhow!(
"provider schema {path} requires undeclared property {required:?}"
));
}
}
let canonical_required = canonical_required
.into_iter()
.filter_map(|value| value.as_str().map(ToOwned::to_owned))
.collect::<std::collections::BTreeSet<_>>();
let mut projected = serde_json::Map::new();
for (name, mut property) in properties {
close_provider_objects(&mut property, &format!("{path}.properties.{name}"))?;
if !canonical_required.contains(&name) {
property = nullable_provider_property(property);
}
projected.insert(name, property);
}
let required = projected
.keys()
.cloned()
.map(serde_json::Value::String)
.collect();
map.insert("properties".into(), serde_json::Value::Object(projected));
map.insert("required".into(), serde_json::Value::Array(required));
map.insert(
"additionalProperties".into(),
serde_json::Value::Bool(false),
);
}
for (name, value) in map.iter_mut() {
if name != "properties" || !describes_object {
close_provider_objects(value, &format!("{path}.{name}"))?;
}
}
Ok(())
}
serde_json::Value::Array(values) => {
for (index, value) in values.iter_mut().enumerate() {
close_provider_objects(value, &format!("{path}[{index}]"))?;
}
Ok(())
}
_ => Ok(()),
}
}
fn schema_map_describes_object(map: &serde_json::Map<String, serde_json::Value>) -> bool {
map.get("type").and_then(serde_json::Value::as_str) == Some("object")
|| [
"properties",
"required",
"additionalProperties",
"patternProperties",
"propertyNames",
"minProperties",
"maxProperties",
]
.iter()
.any(|keyword| map.contains_key(*keyword))
}
fn parent_relative_object_alternatives(value: &serde_json::Value) -> bool {
let Some(alternatives) = value.as_array() else {
return false;
};
!alternatives.is_empty()
&& alternatives.iter().all(|alternative| {
let Some(map) = alternative.as_object() else {
return false;
};
!map.contains_key("type")
&& !map.contains_key("$ref")
&& map
.keys()
.any(|key| matches!(key.as_str(), "properties" | "required"))
&& map.keys().all(|key| {
matches!(
key.as_str(),
"properties" | "required" | "title" | "description" | "$comment"
)
})
})
}
fn infer_literal_type(map: &mut serde_json::Map<String, serde_json::Value>) {
if map.contains_key("type") {
return;
}
let mut types = std::collections::BTreeSet::new();
if let Some(value) = map.get("const") {
types.insert(json_type(value));
} else if let Some(values) = map.get("enum").and_then(serde_json::Value::as_array) {
types.extend(values.iter().map(json_type));
}
match types.len() {
0 => {}
1 => {
map.insert(
"type".into(),
serde_json::Value::String(types.into_iter().next().expect("one type").into()),
);
}
_ => {
map.insert(
"type".into(),
serde_json::Value::Array(
types
.into_iter()
.map(|value| serde_json::Value::String(value.into()))
.collect(),
),
);
}
}
}
fn json_type(value: &serde_json::Value) -> &'static str {
match value {
serde_json::Value::Null => "null",
serde_json::Value::Bool(_) => "boolean",
serde_json::Value::Number(number) if number.is_i64() || number.is_u64() => "integer",
serde_json::Value::Number(_) => "number",
serde_json::Value::String(_) => "string",
serde_json::Value::Array(_) => "array",
serde_json::Value::Object(_) => "object",
}
}
fn nullable_provider_property(property: serde_json::Value) -> serde_json::Value {
if property
.get("anyOf")
.and_then(serde_json::Value::as_array)
.is_some_and(|variants| {
variants.iter().any(|variant| {
variant.get("type").and_then(serde_json::Value::as_str) == Some("null")
})
})
{
property
} else {
serde_json::json!({"anyOf": [property, {"type": "null"}]})
}
}
const UNSUPPORTED_PROVIDER_SCHEMA_KEYWORDS: &[&str] = &[
"allOf",
"not",
"dependentRequired",
"dependentSchemas",
"if",
"then",
"else",
"patternProperties",
"propertyNames",
"minProperties",
"maxProperties",
"unevaluatedProperties",
"uniqueItems",
"contains",
"minContains",
"maxContains",
"prefixItems",
"unevaluatedItems",
"default",
"examples",
"readOnly",
"writeOnly",
"$schema",
"$id",
"$anchor",
"$dynamicAnchor",
"$dynamicRef",
"$vocabulary",
];
fn lower_provider_schema(schema: &mut serde_json::Value) {
let serde_json::Value::Object(map) = schema else {
return;
};
infer_literal_type(map);
if map.get("format").and_then(serde_json::Value::as_str) == Some("uuid") {
map.remove("format");
map.entry("pattern").or_insert_with(|| {
serde_json::Value::String(
"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
.into(),
)
});
}
for keyword in UNSUPPORTED_PROVIDER_SCHEMA_KEYWORDS {
map.remove(*keyword);
}
if let Some(one_of) = map.remove("oneOf") {
map.insert("anyOf".into(), one_of);
}
if map
.get("anyOf")
.is_some_and(parent_relative_object_alternatives)
{
map.remove("anyOf");
}
for collection in ["properties", "$defs", "definitions"] {
if let Some(serde_json::Value::Object(children)) = map.get_mut(collection) {
for child in children.values_mut() {
lower_provider_schema(child);
}
}
}
if let Some(items) = map.get_mut("items") {
match items {
serde_json::Value::Array(items) => {
for item in items {
lower_provider_schema(item);
}
}
item => lower_provider_schema(item),
}
}
if let Some(serde_json::Value::Array(alternatives)) = map.get_mut("anyOf") {
for alternative in alternatives {
lower_provider_schema(alternative);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn native_provider_identity_selects_one_exact_request_contract() {
let openai = EpiphanyModelRequest::new(
"openai-request",
"conversation",
"openai-codex",
"gpt-test",
"decide",
);
let openrouter = EpiphanyModelRequest::new(
"openrouter-request",
"conversation",
"openrouter",
"stealth/ox-alpha",
"decide",
);
let openai = request_from_native(&openai).unwrap();
let openrouter = request_from_native(&openrouter).unwrap();
assert!(matches!(
openai.payload,
EpiphanyProviderRequestPayload::Codex(_)
));
assert!(matches!(
openrouter.payload,
EpiphanyProviderRequestPayload::OpenRouter(_)
));
}
#[test]
fn provider_projection_closes_objects_without_keeping_parent_conditions() {
let schema = strict_provider_schema(
r#"{
"type":"object",
"properties":{"choice":{"type":"string"},"note":{"type":"string"}},
"required":["choice"],
"allOf":[{"if":{"properties":{"choice":{"const":"x"}}}}],
"anyOf":[{"required":["choice"]},{"required":["note"]}]
}"#,
)
.unwrap();
assert_eq!(schema["additionalProperties"], false);
assert_eq!(schema["required"], serde_json::json!(["choice", "note"]));
assert!(schema.get("allOf").is_none());
assert!(schema.get("anyOf").is_none());
assert_eq!(schema["properties"]["note"]["anyOf"][1]["type"], "null");
}
#[test]
fn provider_projection_types_literals_and_lowers_uuid() {
let schema = strict_provider_schema(
r#"{
"type":"object",
"properties":{
"kind":{"const":"ready"},
"id":{"type":"string","format":"uuid"},
"items":{"type":"array","uniqueItems":true,"items":{"enum":[1,2]}}
},
"required":["kind","id","items"]
}"#,
)
.unwrap();
assert_eq!(schema["properties"]["kind"]["type"], "string");
assert!(schema["properties"]["id"].get("format").is_none());
assert!(schema["properties"]["id"]["pattern"].is_string());
assert!(schema["properties"]["items"].get("uniqueItems").is_none());
assert_eq!(schema["properties"]["items"]["items"]["type"], "integer");
}
#[test]
fn provider_identities_are_stable_and_bounded() {
assert_eq!(provider_format_name("role/result.v1"), "role_result_v1");
assert_eq!(provider_call_id("already-safe_1"), "already-safe_1");
let first = provider_call_id("unsafe/call identity");
assert_eq!(first, provider_call_id("unsafe/call identity"));
assert_eq!(first.len(), 64);
assert!(first.bytes().all(|byte| byte.is_ascii_hexdigit()));
}
#[test]
fn openrouter_terminal_tool_preserves_structured_decision() {
let mut request = EpiphanyOpenRouterRequest::new("request", "conversation", "ox", "decide");
request.output_schema_json = Some(
r#"{"type":"object","properties":{"verdict":{"type":"string"}},"required":["verdict"]}"#.into(),
);
let body = openrouter_request_body(&request).unwrap();
assert_eq!(body["tool_choice"], "required");
assert_eq!(
body["tools"][0]["function"]["name"],
OPENROUTER_TERMINAL_TOOL_NAME
);
let response = serde_json::json!({
"id": "response",
"model": "ox",
"choices": [{"message": {"tool_calls": [{
"id": "terminal",
"function": {"name": OPENROUTER_TERMINAL_TOOL_NAME, "arguments": "{\"verdict\":\"pass\"}"}
}]}}]
});
let events =
openrouter_events_from_response(&request, &serde_json::to_vec(&response).unwrap())
.unwrap();
assert!(matches!(
&events[0].payload,
EpiphanyModelStreamPayload::TextDelta { text } if text == "{\"verdict\":\"pass\"}"
));
assert!(matches!(
&events[1].payload,
EpiphanyModelStreamPayload::Completed { receipt }
if receipt.transport.as_deref() == Some("openrouter-chat-completions")
));
}
}

View file

@ -1,15 +0,0 @@
[package]
name = "epiphany-openai-adapter"
version = "0.1.0"
edition = "2024"
publish = false
license-file = "../LICENSE"
[lib]
name = "epiphany_openai_adapter"
path = "src/lib.rs"
[dependencies]
cultcache-rs = { path = "../vendor/cultcache-rs" }
epiphany-model-adapter = { path = "../epiphany-model-adapter" }
serde = { version = "1", features = ["derive"] }

View file

@ -1,322 +0,0 @@
use cultcache_rs::DatabaseEntry;
use epiphany_model_adapter::{EpiphanyModelInputItem, EpiphanyModelRequest};
use serde::Deserialize;
use serde::Serialize;
pub const OPENAI_ADAPTER_REQUEST_SCHEMA_ID: &str = "epiphany.openai_model_request.v1";
pub const OPENAI_ADAPTER_EVENT_SCHEMA_ID: &str = "epiphany.openai_model_stream_event.v0";
pub const OPENAI_ADAPTER_RECEIPT_SCHEMA_ID: &str = "epiphany.openai_model_receipt.v0";
pub const OPENAI_ADAPTER_STATUS_SCHEMA_ID: &str = "epiphany.openai_adapter_status.v0";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum EpiphanyOpenAiAuthMode {
ChatGptSubscription,
ApiKey,
ExternalBearer,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum EpiphanyOpenAiWireDialect {
Responses,
ChatCompletionsTerminalTool,
}
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "epiphany.openai_adapter_status.v0",
schema = "EpiphanyOpenAiAdapterStatus"
)]
pub struct EpiphanyOpenAiAdapterStatus {
#[cultcache(key = 0)]
pub schema_id: String,
#[cultcache(key = 1)]
pub adapter_id: String,
#[cultcache(key = 2)]
pub auth_mode: EpiphanyOpenAiAuthMode,
#[cultcache(key = 3, default)]
pub account_id: Option<String>,
#[cultcache(key = 4, default)]
pub plan_type: Option<String>,
#[cultcache(key = 5, default)]
pub default_model: Option<String>,
#[cultcache(key = 6)]
pub supports_websockets: bool,
#[cultcache(key = 7)]
pub codex_transport_attached: bool,
}
#[derive(Debug, Clone, PartialEq, DatabaseEntry)]
#[cultcache(
type = "epiphany.openai_model_request.v1",
schema = "EpiphanyOpenAiModelRequest"
)]
pub struct EpiphanyOpenAiModelRequest {
#[cultcache(key = 0)]
pub schema_id: String,
#[cultcache(key = 1)]
pub request_id: String,
#[cultcache(key = 2)]
pub conversation_id: String,
#[cultcache(key = 3)]
pub model: String,
#[cultcache(key = 4)]
pub instructions: String,
#[cultcache(key = 5, default)]
pub input: Vec<EpiphanyOpenAiInputItem>,
#[cultcache(key = 6, default)]
pub reasoning_effort: Option<String>,
#[cultcache(key = 7, default)]
pub reasoning_summary: Option<String>,
#[cultcache(key = 8, default)]
pub service_tier: Option<String>,
#[cultcache(key = 9, default)]
pub output_contract_id: Option<String>,
#[cultcache(key = 10, default)]
pub previous_response_id: Option<String>,
#[cultcache(key = 11, default)]
pub tools: Vec<EpiphanyOpenAiToolDefinition>,
#[cultcache(key = 12, default)]
pub output_schema_json: Option<String>,
#[cultcache(key = 13)]
pub provider_id: String,
#[cultcache(key = 14)]
pub wire_dialect: EpiphanyOpenAiWireDialect,
#[cultcache(key = 15, default)]
pub max_output_tokens: Option<u32>,
#[cultcache(key = 16, default)]
pub prompt_cache_key: Option<String>,
}
impl EpiphanyOpenAiModelRequest {
pub fn new(
request_id: impl Into<String>,
conversation_id: impl Into<String>,
model: impl Into<String>,
instructions: impl Into<String>,
) -> Self {
Self {
schema_id: OPENAI_ADAPTER_REQUEST_SCHEMA_ID.to_string(),
request_id: request_id.into(),
conversation_id: conversation_id.into(),
model: model.into(),
instructions: instructions.into(),
input: Vec::new(),
reasoning_effort: None,
reasoning_summary: None,
service_tier: None,
output_contract_id: None,
previous_response_id: None,
tools: Vec::new(),
output_schema_json: None,
provider_id: "openai-codex".to_string(),
wire_dialect: EpiphanyOpenAiWireDialect::Responses,
max_output_tokens: None,
prompt_cache_key: None,
}
}
}
/// The only lowering from a native model request to the OpenAI transport.
/// Internal audit identity stays on the native request; every provider-bearing
/// byte is derived here.
pub fn request_from_native(request: &EpiphanyModelRequest) -> EpiphanyOpenAiModelRequest {
EpiphanyOpenAiModelRequest {
schema_id: OPENAI_ADAPTER_REQUEST_SCHEMA_ID.to_string(),
request_id: request.request_id.clone(),
conversation_id: request.conversation_id.clone(),
model: request.model.clone(),
instructions: request.instructions.clone(),
input: request.input.iter().map(input_from_native).collect(),
reasoning_effort: request.reasoning_effort.clone(),
reasoning_summary: request.reasoning_summary.clone(),
service_tier: request.service_tier.clone(),
output_contract_id: request.output_contract_id.clone(),
previous_response_id: request.previous_response_id.clone(),
tools: request
.tools
.iter()
.map(|tool| EpiphanyOpenAiToolDefinition {
name: tool.name.clone(),
description: tool.description.clone(),
parameters_json: tool.parameters_json.clone(),
})
.collect(),
output_schema_json: request.output_schema_json.clone(),
provider_id: request.provider.clone(),
wire_dialect: if request.provider == "openrouter" {
EpiphanyOpenAiWireDialect::ChatCompletionsTerminalTool
} else {
EpiphanyOpenAiWireDialect::Responses
},
max_output_tokens: request.max_output_tokens,
prompt_cache_key: request.prompt_cache_key.clone(),
}
}
fn input_from_native(input: &EpiphanyModelInputItem) -> EpiphanyOpenAiInputItem {
match input {
EpiphanyModelInputItem::UserText { text } => {
EpiphanyOpenAiInputItem::UserText { text: text.clone() }
}
EpiphanyModelInputItem::AssistantText { text } => {
EpiphanyOpenAiInputItem::AssistantText { text: text.clone() }
}
EpiphanyModelInputItem::ToolCall {
call_id,
name,
arguments,
} => EpiphanyOpenAiInputItem::ToolCall {
call_id: call_id.clone(),
name: name.clone(),
arguments: arguments.clone(),
},
EpiphanyModelInputItem::ToolResult { call_id, output } => {
EpiphanyOpenAiInputItem::ToolResult {
call_id: call_id.clone(),
output: output.clone(),
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EpiphanyOpenAiToolDefinition {
pub name: String,
pub description: String,
pub parameters_json: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum EpiphanyOpenAiInputItem {
UserText {
text: String,
},
AssistantText {
text: String,
},
ToolCall {
call_id: String,
name: String,
arguments: String,
},
ToolResult {
call_id: String,
output: String,
},
}
#[derive(Debug, Clone, PartialEq, DatabaseEntry)]
#[cultcache(
type = "epiphany.openai_model_stream_event.v0",
schema = "EpiphanyOpenAiStreamEvent"
)]
pub struct EpiphanyOpenAiStreamEvent {
#[cultcache(key = 0)]
pub schema_id: String,
#[cultcache(key = 1)]
pub request_id: String,
#[cultcache(key = 2)]
pub sequence: u64,
#[cultcache(key = 3)]
pub payload: EpiphanyOpenAiStreamPayload,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum EpiphanyOpenAiStreamPayload {
TextDelta {
text: String,
},
ReasoningDelta {
text: String,
},
ToolCall {
call_id: String,
name: String,
arguments: String,
},
Completed {
receipt: EpiphanyOpenAiModelReceipt,
},
Failed {
message: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "epiphany.openai_model_receipt.v0",
schema = "EpiphanyOpenAiModelReceipt"
)]
pub struct EpiphanyOpenAiModelReceipt {
#[cultcache(key = 0)]
pub schema_id: String,
#[cultcache(key = 1)]
pub request_id: String,
#[cultcache(key = 2)]
pub model: String,
#[cultcache(key = 3, default)]
pub response_id: Option<String>,
#[cultcache(key = 4, default)]
pub input_tokens: Option<u64>,
#[cultcache(key = 5, default)]
pub output_tokens: Option<u64>,
#[cultcache(key = 6, default)]
pub reasoning_output_tokens: Option<u64>,
#[cultcache(key = 7, default)]
pub transport: Option<String>,
#[cultcache(key = 8, default)]
pub cached_input_tokens: Option<u64>,
}
impl EpiphanyOpenAiModelReceipt {
pub fn new(request_id: impl Into<String>, model: impl Into<String>) -> Self {
Self {
schema_id: OPENAI_ADAPTER_RECEIPT_SCHEMA_ID.to_string(),
request_id: request_id.into(),
model: model.into(),
response_id: None,
input_tokens: None,
output_tokens: None,
reasoning_output_tokens: None,
transport: None,
cached_input_tokens: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn native_provider_identity_selects_one_exact_wire_dialect() {
let mut openai = EpiphanyModelRequest::new(
"openai-request",
"conversation",
"openai-codex",
"gpt-test",
"decide",
);
openai.max_output_tokens = Some(512);
let openrouter = EpiphanyModelRequest::new(
"openrouter-request",
"conversation",
"openrouter",
"stealth/ox-alpha",
"decide",
);
let openai = request_from_native(&openai);
let openrouter = request_from_native(&openrouter);
assert_eq!(openai.provider_id, "openai-codex");
assert_eq!(openai.max_output_tokens, Some(512));
assert_eq!(openai.wire_dialect, EpiphanyOpenAiWireDialect::Responses);
assert_eq!(openrouter.provider_id, "openrouter");
assert_eq!(
openrouter.wire_dialect,
EpiphanyOpenAiWireDialect::ChatCompletionsTerminalTool
);
assert_ne!(openai.schema_id, "epiphany.openai_model_request.v0");
}
}

View file

@ -1 +0,0 @@
/target/

View file

@ -1,22 +0,0 @@
[package]
name = "epiphany-openai-codex-spine"
version = "0.1.0"
edition = "2024"
publish = false
license-file = "../LICENSE"
[lib]
name = "epiphany_openai_codex_spine"
path = "src/lib.rs"
[dependencies]
anyhow = "1"
codex-client = { path = "../vendor/codex/codex-rs/codex-client" }
codex-login = { path = "../vendor/codex/codex-rs/login" }
epiphany-openai-adapter = { path = "../epiphany-openai-adapter" }
futures = "0.3"
http = "1"
serde = "1"
serde_json = "1"
sha2 = "0.10"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

File diff suppressed because it is too large Load diff

View file

@ -13,22 +13,16 @@ path = "src/lib.rs"
[dependencies]
anyhow = "1"
chrono = "0.4.44"
cultcache-rs = { path = "../vendor/cultcache-rs" }
cultmesh-rs = { path = "../vendor/cultmesh-rs" }
cultnet-rs = { path = "../vendor/cultnet-rs" }
codex-login = { path = "../vendor/codex/codex-rs/login" }
cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "a0813c6eed24d30bf88073ef615b633c77ebfcd6", package = "cultcache-rs" }
epiphany-core = { path = "../epiphany-core" }
epiphany-model-adapter = { path = "../epiphany-model-adapter" }
epiphany-openai-adapter = { path = "../epiphany-openai-adapter" }
epiphany-openai-codex-spine = { path = "../epiphany-openai-codex-spine" }
epiphany-tool-adapter = { path = "../epiphany-tool-adapter" }
rmp-serde = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "signal", "sync"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
uuid = { version = "1", features = ["v4"] }
[dev-dependencies]
epiphany-state-model = { path = "../epiphany-state-model" }
tempfile = "3"

View file

@ -1,7 +0,0 @@
use anyhow::Result;
use epiphany_openai_runtime::{EpiphanyModelConnectorOptions, serve_model_connector};
#[tokio::main]
async fn main() -> Result<()> {
serve_model_connector(EpiphanyModelConnectorOptions::from_env_args()?).await
}

View file

@ -1 +0,0 @@
include!("epiphany-openai-runtime.rs");

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
use std::{env, net::SocketAddr, path::PathBuf, thread, time::Duration};
use std::{env, future::Future, net::SocketAddr, path::PathBuf, pin::Pin, thread, time::Duration};
use anyhow::{Context, Result, anyhow};
use cultcache_rs::DatabaseEntry;
@ -17,10 +17,87 @@ use epiphany_core::{
pulse_persona_social, reconcile_terminal_persona_conversation,
retain_terminal_persona_conversations, validate_persona_discord_request_anchor,
};
use epiphany_model_adapter::EpiphanyModelRequest;
use epiphany_openai_runtime::{
NativePersonaModelRunner, PersonaModelExecutionPlan, execute_persona_model_turn,
DEFAULT_CODEX_CONNECTOR_ENDPOINT, EpiphanyOpenAiRuntimeOptions, PersonaModelExecutionPlan,
PersonaModelRunner, assistant_text_from_model_events, execute_persona_model_turn,
};
#[path = "../provider_transport.rs"]
mod provider_transport;
#[derive(Clone, Debug)]
struct NativePersonaModelRunner {
store_path: PathBuf,
provider_credential_path: Option<PathBuf>,
connector_endpoint: SocketAddr,
caller_runtime_id: String,
provider: String,
model: String,
turn_timeout: Duration,
}
impl PersonaModelRunner for NativePersonaModelRunner {
fn run<'a>(
&'a mut self,
_store_path: &'a PathBuf,
stage: &'a str,
turn_id: &'a str,
request: EpiphanyModelRequest,
) -> Pin<Box<dyn Future<Output = Result<String>> + Send + 'a>> {
Box::pin(async move {
if request.provider != self.provider || request.model != self.model {
return Err(anyhow!("Persona execution plan and model runner disagree"));
}
let replay = assistant_text_from_model_events(&self.store_path, &request.request_id)?;
if !replay.trim().is_empty() {
return Ok(replay);
}
let options = EpiphanyOpenAiRuntimeOptions {
store_path: self.store_path.clone(),
provider_credential_path: self.provider_credential_path.clone(),
connector_endpoint: Some(self.connector_endpoint),
caller_runtime_id: self.caller_runtime_id.clone(),
session_id: format!("persona-turn-{turn_id}"),
job_id: format!("persona-{stage}-{turn_id}"),
objective: format!("Run Persona {stage} stage for {turn_id}"),
coordinator_note: "Native Persona model executor; transport owns inference only."
.to_string(),
default_model: Some(self.model.clone()),
request_timeout: None,
};
let summary = tokio::time::timeout(
self.turn_timeout,
provider_transport::run_model_turn(
&self.provider,
options.clone(),
request.clone(),
),
)
.await
.map_err(|_| {
anyhow!(
"Persona {stage} stage exceeded its {} second outer turn budget",
self.turn_timeout.as_secs()
)
})??;
let output = assistant_text_from_model_events(&self.store_path, &request.request_id)?;
if summary.verdict != "pass" || output.trim().is_empty() {
return Err(anyhow!(if summary.verdict != "pass" {
summary.summary
} else {
format!("Persona {stage} stage completed without assistant text")
}));
}
Ok(output)
})
}
fn recover(&mut self, request_id: &str) -> Result<String> {
assistant_text_from_model_events(&self.store_path, request_id)
}
}
#[tokio::main]
async fn main() -> Result<()> {
let options = Options::parse()?;
@ -98,28 +175,30 @@ async fn poll_once(options: &Options) -> Result<bool> {
)?;
let mut runner = NativePersonaModelRunner {
store_path: options.runtime_store.clone(),
codex_home: options.codex_home.clone(),
provider_credential_path: options.provider_credential_path.clone(),
connector_endpoint: options.connector_endpoint,
caller_runtime_id: options.runtime_id.clone(),
provider: options.provider.clone(),
model: options.model.clone(),
turn_timeout: Duration::from_secs(options.turn_timeout_seconds),
};
let model_terminal = match execute_persona_model_turn(&plan, &mut runner).await {
Ok(receipt) => receipt,
Err(error) if error.to_string().contains("braked") => return Ok(false),
Err(error) => {
complete_persona_social_turn(
&options.social_store,
PersonaTurnTerminalOptions {
request_id: request.request_id,
outcome: "failed".into(),
delivery_evidence: None,
blocked_evidence: None,
},
)?;
return Err(error);
}
};
let model_terminal =
match execute_persona_model_turn(&options.runtime_store, &plan, &mut runner).await {
Ok(receipt) => receipt,
Err(error) if error.to_string().contains("braked") => return Ok(false),
Err(error) => {
complete_persona_social_turn(
&options.social_store,
PersonaTurnTerminalOptions {
request_id: request.request_id,
outcome: "failed".into(),
delivery_receipt: None,
blocked_evidence: None,
},
)?;
return Err(error);
}
};
let signer = open_persona_discord_request_identity(&options.mouth_identity_store)?;
let request_anchor = load_persona_discord_service_anchor(&options.mouth_request_anchor)?;
validate_persona_discord_request_anchor(&request_anchor, &options.runtime_id)?;
@ -289,8 +368,8 @@ struct Options {
runtime_store: PathBuf,
social_store: PathBuf,
cultmesh_store: PathBuf,
codex_home: PathBuf,
provider_credential_path: Option<PathBuf>,
connector_endpoint: SocketAddr,
runtime_id: String,
provider: String,
model: String,
@ -336,8 +415,14 @@ impl Options {
runtime_store: path("--runtime-store")?,
social_store: path("--persona-social-store")?,
cultmesh_store: path("--cultmesh-store")?,
codex_home: path("--codex-home")?,
provider_credential_path: values.get("--provider-credential").map(PathBuf::from),
connector_endpoint: value(
&values,
"--connector-endpoint",
&DEFAULT_CODEX_CONNECTOR_ENDPOINT.to_string(),
)
.parse()
.context("--connector-endpoint must be an IP:port socket address")?,
_repo_root: path("--repo-root")?,
mouth_request_store: path("--mouth-request-store")?,
mouth_receipt_store: path("--mouth-receipt-store")?,
@ -435,8 +520,8 @@ mod tests {
runtime_store: runtime_store.clone(),
social_store: absent.join("persona-social.cc"),
cultmesh_store: absent.join("cultmesh.cc"),
codex_home: absent.join("codex-home"),
provider_credential_path: None,
connector_endpoint: DEFAULT_CODEX_CONNECTOR_ENDPOINT,
runtime_id: "persona-reentry".into(),
provider: "test".into(),
model: "test-model".into(),

File diff suppressed because it is too large Load diff

View file

@ -1,996 +0,0 @@
use std::collections::{HashMap, HashSet};
use std::net::{SocketAddr, TcpStream};
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::{Context, Result, bail};
use cultmesh_rs::{CultMesh, CultMeshNodeOptions, CultMeshRudpDocumentPublishOptions};
use cultnet_rs::{
CultNetClientSecurityOptions, CultNetMessage, CultNetRawDocumentRecord,
CultNetRawPayloadEncoding, CultNetSecret, CultNetWireContract, TcpFramedTransportConnection,
TcpFramedTransportProfileOptions, create_tcp_framed_transport_profile,
decode_cultnet_message_from_slice, encode_cultnet_message_to_vec,
};
use epiphany_model_adapter::{
EpiphanyModelConnectorEnvelope, EpiphanyModelConnectorInvocation, EpiphanyModelConnectorResult,
EpiphanyModelConnectorStatus, EpiphanyModelRequest, EpiphanyModelStreamEvent,
EpiphanyModelStreamPayload, MODEL_ADAPTER_EVENT_SCHEMA_ID, MODEL_ADAPTER_REQUEST_SCHEMA_ID,
MODEL_CONNECTOR_ENVELOPE_SCHEMA_ID, MODEL_CONNECTOR_INVOCATION_SCHEMA_ID,
MODEL_CONNECTOR_RESULT_SCHEMA_ID, MODEL_CONNECTOR_STATUS_SCHEMA_ID,
};
use epiphany_openai_codex_spine::{EpiphanyCodexOpenAiTransport, auth_manager};
use sha2::{Digest, Sha256};
use tokio::net::TcpListener;
use tokio::sync::{Mutex, Semaphore};
const CONNECTOR_PROVIDER_ID: &str = "epiphany.codex-model";
const CONNECTOR_CAPABILITY_ID: &str = "model.generate.structured";
const CONNECTOR_STATUS_KEY: &str = "epiphany.codex-model";
const CONNECTOR_RUNTIME_ID: &str = "epiphany-model-connector";
const CONNECTOR_REQUEST_KIND: &str = "model_request";
const CONNECTOR_RESULT_KIND: &str = "model_result";
const CONNECTOR_MAX_EXPIRY_SKEW_MS: u64 = 5 * 60 * 1_000;
const CONNECTOR_MAX_OUTPUT_TOKENS: u32 = 32_768;
cultmesh_rs::cultmesh_documents!(ModelConnectorDocuments {
EpiphanyModelConnectorStatus => MODEL_CONNECTOR_STATUS_SCHEMA_ID,
});
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EpiphanyModelConnectorOptions {
pub bind: SocketAddr,
pub state_path: PathBuf,
pub codex_home: PathBuf,
pub connection_key_path: PathBuf,
pub allowed_caller_runtime_id: String,
pub model: String,
pub max_parallel_requests: u32,
pub max_payload_bytes: u32,
pub odin_endpoint: Option<SocketAddr>,
}
impl EpiphanyModelConnectorOptions {
pub fn from_env_args() -> Result<Self> {
let mut options = Self {
bind: "127.0.0.1:4103".parse()?,
state_path: PathBuf::from("state/epiphany-model-connector.cc"),
codex_home: crate::default_codex_home()?,
connection_key_path: PathBuf::new(),
allowed_caller_runtime_id: "ghostlight-dungeon-yggdrasil".to_string(),
model: "gpt-5.4".to_string(),
max_parallel_requests: 8,
max_payload_bytes: 1_048_576,
odin_endpoint: Some("127.0.0.1:17871".parse()?),
};
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"--bind" => options.bind = next_arg(&mut args, "--bind")?.parse()?,
"--state" => options.state_path = PathBuf::from(next_arg(&mut args, "--state")?),
"--codex-home" => {
options.codex_home = PathBuf::from(next_arg(&mut args, "--codex-home")?)
}
"--connection-key-file" => {
options.connection_key_path =
PathBuf::from(next_arg(&mut args, "--connection-key-file")?)
}
"--allowed-caller" => {
options.allowed_caller_runtime_id = next_arg(&mut args, "--allowed-caller")?
}
"--model" => options.model = next_arg(&mut args, "--model")?,
"--max-parallel" => {
options.max_parallel_requests =
next_arg(&mut args, "--max-parallel")?.parse()?
}
"--max-payload-bytes" => {
options.max_payload_bytes =
next_arg(&mut args, "--max-payload-bytes")?.parse()?
}
"--odin" => {
let endpoint = next_arg(&mut args, "--odin")?;
options.odin_endpoint = if endpoint == "none" {
None
} else {
Some(endpoint.parse()?)
};
}
other => bail!("unknown epiphany-model-connector argument {other:?}"),
}
}
options.validate()?;
Ok(options)
}
fn validate(&self) -> Result<()> {
if !self.bind.ip().is_loopback() {
bail!("model connector must bind to a loopback address")
}
if self.connection_key_path.as_os_str().is_empty() {
bail!("--connection-key-file is required")
}
if self.allowed_caller_runtime_id.trim().is_empty() {
bail!("--allowed-caller must be non-empty")
}
if self.model.trim().is_empty() {
bail!("--model must be non-empty")
}
if self.max_parallel_requests == 0 {
bail!("--max-parallel must be greater than zero")
}
if self.max_payload_bytes < 4_096 {
bail!("--max-payload-bytes must be at least 4096")
}
Ok(())
}
}
fn next_arg(args: &mut impl Iterator<Item = String>, name: &str) -> Result<String> {
args.next().with_context(|| format!("missing {name} value"))
}
#[derive(Default)]
struct ReplayCache {
active: HashSet<String>,
completed: HashMap<String, CachedResponse>,
}
#[derive(Clone)]
struct CachedResponse {
request_digest: [u8; 32],
expires_at_unix_ms: u64,
document: CultNetRawDocumentRecord,
}
pub async fn serve_model_connector(options: EpiphanyModelConnectorOptions) -> Result<()> {
options.validate()?;
let connection_key = read_secret_file(&options.connection_key_path)?;
let security = CultNetClientSecurityOptions::new(connection_key)?;
let listener = TcpListener::bind(options.bind)
.await
.with_context(|| format!("failed to bind model connector at {}", options.bind))?;
let local_addr = listener.local_addr()?;
let auth = auth_manager(options.codex_home.clone());
if auth.auth().await.is_none() {
bail!(
"Codex subscription authentication is unavailable at {}",
options.codex_home.display()
)
}
publish_connector_status(&options, local_addr)?;
let profile = create_tcp_framed_transport_profile(
CONNECTOR_RUNTIME_ID,
TcpFramedTransportProfileOptions {
host: Some(local_addr.ip().to_string()),
port: Some(local_addr.port()),
max_payload_bytes: Some(options.max_payload_bytes),
..TcpFramedTransportProfileOptions::default()
},
);
let permits = Arc::new(Semaphore::new(options.max_parallel_requests as usize));
let replay = Arc::new(Mutex::new(ReplayCache::default()));
eprintln!(
"{CONNECTOR_RUNTIME_ID} ready at {local_addr} for {} with {} concurrent calls",
options.allowed_caller_runtime_id, options.max_parallel_requests
);
loop {
let (stream, peer) = listener.accept().await?;
if !peer.ip().is_loopback() {
continue;
}
let stream = stream.into_std()?;
stream.set_nonblocking(false)?;
let profile = profile.clone();
let security = security.clone();
let permits = permits.clone();
let replay = replay.clone();
let auth = auth.clone();
let allowed_caller = options.allowed_caller_runtime_id.clone();
let model = options.model.clone();
tokio::spawn(async move {
let _permit = match permits.acquire_owned().await {
Ok(permit) => permit,
Err(_) => return,
};
if let Err(error) = serve_one_connection(
stream,
profile,
security,
replay,
auth,
&allowed_caller,
&model,
)
.await
{
eprintln!("model connector request refused: {error:#}");
}
});
}
}
async fn serve_one_connection(
stream: TcpStream,
profile: cultnet_rs::CultNetTransportProfile,
security: CultNetClientSecurityOptions,
replay: Arc<Mutex<ReplayCache>>,
auth: Arc<codex_login::AuthManager>,
allowed_caller: &str,
model: &str,
) -> Result<()> {
let (connection, request_document) = tokio::task::spawn_blocking(move || {
let mut connection = TcpFramedTransportConnection::new(stream, profile);
let frame = connection.receive()?;
let message = decode_cultnet_message_from_slice(
&frame.payload,
CultNetWireContract::CultNetSchemaV0,
)?;
let document = request_document_from_message(message)?;
Ok::<_, anyhow::Error>((connection, document))
})
.await??;
let outer_request_id = request_document.record_key.clone();
let request_digest: [u8; 32] = Sha256::digest(&request_document.payload).into();
match claim_request(&replay, &outer_request_id, request_digest).await {
Ok(Some(cached)) => return send_document(connection, cached).await,
Ok(None) => {}
Err(error) => {
eprintln!("model connector replay refusal for {outer_request_id}: {error:#}");
return send_error(connection, "model connector rejected the replay").await;
}
}
let outcome =
process_request_document(&request_document, &security, auth, allowed_caller, model).await;
let (response, expires_at) = match outcome {
Ok(value) => value,
Err(error) => {
release_request(&replay, &outer_request_id).await;
eprintln!("model connector pre-execution refusal for {outer_request_id}: {error:#}");
return send_error(connection, "model connector rejected the invocation")
.await
.with_context(|| error.to_string());
}
};
complete_request(
&replay,
outer_request_id,
request_digest,
expires_at,
response.clone(),
)
.await;
send_document(connection, response).await
}
fn request_document_from_message(message: CultNetMessage) -> Result<CultNetRawDocumentRecord> {
let CultNetMessage::DocumentPutRaw {
message_id,
document,
} = message
else {
bail!("model connector accepts only CultNet DocumentPutRaw invocations")
};
if message_id != document.record_key {
bail!("model connector message id does not match the document key")
}
Ok(document)
}
async fn claim_request(
cache: &Mutex<ReplayCache>,
request_id: &str,
request_digest: [u8; 32],
) -> Result<Option<CultNetRawDocumentRecord>> {
let now = unix_ms()?;
let mut cache = cache.lock().await;
cache
.completed
.retain(|_, response| response.expires_at_unix_ms >= now);
if let Some(response) = cache.completed.get(request_id) {
if response.request_digest != request_digest {
bail!("request id was reused with different encrypted cargo")
}
return Ok(Some(response.document.clone()));
}
if !cache.active.insert(request_id.to_string()) {
bail!("request is already in flight")
}
Ok(None)
}
async fn release_request(cache: &Mutex<ReplayCache>, request_id: &str) {
cache.lock().await.active.remove(request_id);
}
async fn complete_request(
cache: &Mutex<ReplayCache>,
request_id: String,
request_digest: [u8; 32],
expires_at_unix_ms: u64,
document: CultNetRawDocumentRecord,
) {
let mut cache = cache.lock().await;
cache.active.remove(&request_id);
cache.completed.insert(
request_id,
CachedResponse {
request_digest,
expires_at_unix_ms,
document,
},
);
}
async fn process_request_document(
document: &CultNetRawDocumentRecord,
security: &CultNetClientSecurityOptions,
auth: Arc<codex_login::AuthManager>,
allowed_caller: &str,
allowed_model: &str,
) -> Result<(CultNetRawDocumentRecord, u64)> {
if document.schema_id != MODEL_CONNECTOR_ENVELOPE_SCHEMA_ID {
bail!("unexpected request envelope schema")
}
let envelope: EpiphanyModelConnectorEnvelope =
rmp_serde::from_slice(&document.payload).context("request envelope is malformed")?;
if envelope.schema_id != MODEL_CONNECTOR_ENVELOPE_SCHEMA_ID
|| envelope.message_kind != CONNECTOR_REQUEST_KIND
|| envelope.request_id != document.record_key
{
bail!("request envelope identity is inconsistent")
}
let plaintext = CultNetSecret::decrypt_bytes(&envelope.ciphertext, &envelope.nonce, security)
.context("request envelope authentication failed")?;
let invocation: EpiphanyModelConnectorInvocation =
rmp_serde::from_slice(&plaintext).context("request invocation is malformed")?;
let validation = validate_invocation(
&invocation,
&envelope.request_id,
allowed_caller,
allowed_model,
unix_ms()?,
);
let result = match validation {
Ok(()) => execute_invocation(invocation.clone(), auth).await,
Err(error) => EpiphanyModelConnectorResult {
schema_id: MODEL_CONNECTOR_RESULT_SCHEMA_ID.to_string(),
request_id: invocation.request_id.clone(),
accepted: false,
events: Vec::new(),
error: Some(error.to_string()),
},
};
let response_envelope = encrypt_result(&result, security)?;
let response_payload = rmp_serde::to_vec_named(&response_envelope)?;
Ok((
CultNetRawDocumentRecord {
schema_id: MODEL_CONNECTOR_ENVELOPE_SCHEMA_ID.to_string(),
record_key: invocation.request_id,
stored_at: now_utc(),
payload_encoding: CultNetRawPayloadEncoding::Messagepack,
payload: response_payload,
source_runtime_id: Some(CONNECTOR_RUNTIME_ID.to_string()),
source_agent_id: None,
source_role: Some("model-provider-connector".to_string()),
tags: Some(vec![CONNECTOR_CAPABILITY_ID.to_string()]),
},
invocation.expires_at_unix_ms,
))
}
fn validate_invocation(
invocation: &EpiphanyModelConnectorInvocation,
outer_request_id: &str,
allowed_caller: &str,
allowed_model: &str,
now_unix_ms: u64,
) -> Result<()> {
if invocation.schema_id != MODEL_CONNECTOR_INVOCATION_SCHEMA_ID
|| invocation.request.schema_id != MODEL_ADAPTER_REQUEST_SCHEMA_ID
{
bail!("unexpected model invocation schema")
}
if invocation.request_id != outer_request_id
|| invocation.request.request_id != invocation.request_id
{
bail!("model invocation substituted request identity")
}
if invocation.caller_runtime_id != allowed_caller {
bail!("caller runtime is not admitted")
}
if invocation.expires_at_unix_ms < now_unix_ms
|| invocation.expires_at_unix_ms > now_unix_ms + CONNECTOR_MAX_EXPIRY_SKEW_MS
{
bail!("model invocation is expired or exceeds the expiry horizon")
}
if invocation.request.provider != "openai-codex" {
bail!("connector admits only the openai-codex provider")
}
if invocation.request.model != allowed_model {
bail!("requested model is not admitted")
}
if invocation.request.instructions.trim().is_empty() {
bail!("model instructions must be non-empty")
}
if !invocation.request.tools.is_empty() {
bail!("shared model connector does not admit tool execution")
}
if invocation.request.previous_response_id.is_some() {
bail!("shared model connector admits stateless requests only")
}
if invocation.request.reasoning_summary.is_some() {
bail!("shared model connector does not expose reasoning summaries")
}
if matches!(invocation.request.max_output_tokens, Some(0))
|| invocation
.request
.max_output_tokens
.is_some_and(|value| value > CONNECTOR_MAX_OUTPUT_TOKENS)
{
bail!("max_output_tokens exceeds the connector bound")
}
if invocation
.request
.prompt_cache_key
.as_deref()
.is_some_and(|value| value.trim().is_empty() || value.len() > 128)
{
bail!("prompt_cache_key must contain at most 128 non-blank bytes")
}
Ok(())
}
async fn execute_invocation(
invocation: EpiphanyModelConnectorInvocation,
auth: Arc<codex_login::AuthManager>,
) -> EpiphanyModelConnectorResult {
let provider_request = epiphany_openai_adapter::request_from_native(&invocation.request);
let transport = EpiphanyCodexOpenAiTransport::openai(auth);
let events = match transport.collect_model_events(provider_request).await {
Ok(events) => sanitize_model_events(
&invocation.request,
events
.iter()
.map(|event| crate::model_event_from_openai_event("openai-codex", event))
.collect(),
),
Err(error) => {
eprintln!(
"model connector provider failure for {}: {error:#}",
invocation.request_id
);
vec![EpiphanyModelStreamEvent {
schema_id: MODEL_ADAPTER_EVENT_SCHEMA_ID.to_string(),
request_id: invocation.request_id.clone(),
provider: "openai-codex".to_string(),
sequence: 0,
payload: EpiphanyModelStreamPayload::Failed {
message: "model provider transport failed".to_string(),
},
}]
}
};
EpiphanyModelConnectorResult {
schema_id: MODEL_CONNECTOR_RESULT_SCHEMA_ID.to_string(),
request_id: invocation.request_id,
accepted: true,
events,
error: None,
}
}
fn sanitize_model_events(
request: &EpiphanyModelRequest,
events: Vec<EpiphanyModelStreamEvent>,
) -> Vec<EpiphanyModelStreamEvent> {
let mut sanitized = Vec::new();
for event in events {
if event.request_id != request.request_id || event.provider != request.provider {
return vec![connector_failed_event(
&request.request_id,
"model provider substituted event identity",
)];
}
match event.payload {
EpiphanyModelStreamPayload::ReasoningDelta { .. } => continue,
EpiphanyModelStreamPayload::ToolCall { .. } => {
return vec![connector_failed_event(
&request.request_id,
"model provider returned an inadmissible tool call",
)];
}
EpiphanyModelStreamPayload::Completed { receipt } => {
if let Some(limit) = request.max_output_tokens
&& !receipt_proves_public_output_bound(&receipt, limit)
{
return vec![connector_failed_event(
&request.request_id,
"model provider did not prove the requested output-token bound",
)];
}
sanitized.push(EpiphanyModelStreamEvent {
schema_id: MODEL_ADAPTER_EVENT_SCHEMA_ID.to_string(),
request_id: request.request_id.clone(),
provider: request.provider.clone(),
sequence: sanitized.len() as u64,
payload: EpiphanyModelStreamPayload::Completed { receipt },
});
}
payload => sanitized.push(EpiphanyModelStreamEvent {
schema_id: MODEL_ADAPTER_EVENT_SCHEMA_ID.to_string(),
request_id: request.request_id.clone(),
provider: request.provider.clone(),
sequence: sanitized.len() as u64,
payload,
}),
}
}
if sanitized.is_empty() {
sanitized.push(connector_failed_event(
&request.request_id,
"model provider returned no public events",
));
}
sanitized
}
fn receipt_proves_public_output_bound(
receipt: &epiphany_model_adapter::EpiphanyModelReceipt,
limit: u32,
) -> bool {
let Some(total_output_tokens) = receipt.output_tokens else {
return false;
};
let public_output_tokens = match receipt.reasoning_output_tokens {
Some(reasoning_tokens) if reasoning_tokens <= total_output_tokens => {
total_output_tokens - reasoning_tokens
}
Some(_) => return false,
None => total_output_tokens,
};
public_output_tokens <= u64::from(limit)
}
fn connector_failed_event(request_id: &str, message: &str) -> EpiphanyModelStreamEvent {
EpiphanyModelStreamEvent {
schema_id: MODEL_ADAPTER_EVENT_SCHEMA_ID.to_string(),
request_id: request_id.to_string(),
provider: "openai-codex".to_string(),
sequence: 0,
payload: EpiphanyModelStreamPayload::Failed {
message: message.to_string(),
},
}
}
pub fn encrypt_invocation(
invocation: &EpiphanyModelConnectorInvocation,
security: &CultNetClientSecurityOptions,
) -> Result<EpiphanyModelConnectorEnvelope> {
let nonce = CultNetSecret::new_nonce();
let plaintext = rmp_serde::to_vec_named(invocation)?;
Ok(EpiphanyModelConnectorEnvelope {
schema_id: MODEL_CONNECTOR_ENVELOPE_SCHEMA_ID.to_string(),
request_id: invocation.request_id.clone(),
message_kind: CONNECTOR_REQUEST_KIND.to_string(),
ciphertext: CultNetSecret::encrypt_bytes(&plaintext, &nonce, security)?,
nonce: nonce.to_vec(),
})
}
fn encrypt_result(
result: &EpiphanyModelConnectorResult,
security: &CultNetClientSecurityOptions,
) -> Result<EpiphanyModelConnectorEnvelope> {
let nonce = CultNetSecret::new_nonce();
let plaintext = rmp_serde::to_vec_named(result)?;
Ok(EpiphanyModelConnectorEnvelope {
schema_id: MODEL_CONNECTOR_ENVELOPE_SCHEMA_ID.to_string(),
request_id: result.request_id.clone(),
message_kind: CONNECTOR_RESULT_KIND.to_string(),
ciphertext: CultNetSecret::encrypt_bytes(&plaintext, &nonce, security)?,
nonce: nonce.to_vec(),
})
}
pub fn decrypt_result(
envelope: &EpiphanyModelConnectorEnvelope,
security: &CultNetClientSecurityOptions,
) -> Result<EpiphanyModelConnectorResult> {
if envelope.schema_id != MODEL_CONNECTOR_ENVELOPE_SCHEMA_ID
|| envelope.message_kind != CONNECTOR_RESULT_KIND
{
bail!("unexpected connector result envelope")
}
let plaintext = CultNetSecret::decrypt_bytes(&envelope.ciphertext, &envelope.nonce, security)?;
let result: EpiphanyModelConnectorResult = rmp_serde::from_slice(&plaintext)?;
if result.schema_id != MODEL_CONNECTOR_RESULT_SCHEMA_ID
|| result.request_id != envelope.request_id
{
bail!("connector result substituted request identity")
}
Ok(result)
}
async fn send_document(
connection: TcpFramedTransportConnection<TcpStream>,
document: CultNetRawDocumentRecord,
) -> Result<()> {
let message = CultNetMessage::SnapshotResponseRaw {
message_id: document.record_key.clone(),
documents: vec![document],
};
let payload = encode_cultnet_message_to_vec(&message, CultNetWireContract::CultNetSchemaV0)?;
tokio::task::spawn_blocking(move || {
let mut connection = connection;
connection.send("schema", &payload)
})
.await??;
Ok(())
}
async fn send_error(
connection: TcpFramedTransportConnection<TcpStream>,
message: &str,
) -> Result<()> {
let payload = encode_cultnet_message_to_vec(
&CultNetMessage::Error {
error: message.to_string(),
},
CultNetWireContract::CultNetSchemaV0,
)?;
tokio::task::spawn_blocking(move || {
let mut connection = connection;
connection.send("schema", &payload)
})
.await??;
Ok(())
}
fn publish_connector_status(
options: &EpiphanyModelConnectorOptions,
bind: SocketAddr,
) -> Result<()> {
if let Some(parent) = options.state_path.parent() {
std::fs::create_dir_all(parent)?;
}
let status = EpiphanyModelConnectorStatus {
schema_id: MODEL_CONNECTOR_STATUS_SCHEMA_ID.to_string(),
provider_id: CONNECTOR_PROVIDER_ID.to_string(),
capability_id: CONNECTOR_CAPABILITY_ID.to_string(),
request_schema_id: MODEL_CONNECTOR_INVOCATION_SCHEMA_ID.to_string(),
result_schema_id: MODEL_CONNECTOR_RESULT_SCHEMA_ID.to_string(),
envelope_schema_id: MODEL_CONNECTOR_ENVELOPE_SCHEMA_ID.to_string(),
transport_protocol: "cultnet.tcp_framed.v0".to_string(),
host: bind.ip().to_string(),
port: bind.port(),
max_payload_bytes: options.max_payload_bytes,
max_parallel_requests: options.max_parallel_requests,
model: options.model.clone(),
ready: true,
updated_at_utc: now_utc(),
};
let mut node = CultMesh::create_node(
&options.state_path,
ModelConnectorDocuments,
CultMeshNodeOptions {
runtime_id: CONNECTOR_RUNTIME_ID.to_string(),
pull_on_start: true,
},
)?;
node.put(CONNECTOR_STATUS_KEY, &status)?;
node.flush()?;
if let Some(target) = options.odin_endpoint {
if let Err(error) = node.publish_document_to_rudp_catalog(
CONNECTOR_STATUS_KEY,
&status,
CultMeshRudpDocumentPublishOptions::odin(target, CONNECTOR_RUNTIME_ID),
) {
eprintln!("model connector Odin publication failed: {error:#}");
}
}
Ok(())
}
fn read_secret_file(path: &PathBuf) -> Result<String> {
let raw = std::fs::read_to_string(path)
.with_context(|| format!("failed to read connector key {}", path.display()))?;
let key = raw.trim();
if key.is_empty() || raw.trim_matches(['\r', '\n']) != key {
bail!("connector key file is empty or contains surrounding whitespace")
}
Ok(key.to_string())
}
fn unix_ms() -> Result<u64> {
Ok(SystemTime::now()
.duration_since(UNIX_EPOCH)
.context("system clock predates Unix epoch")?
.as_millis()
.try_into()
.context("system clock does not fit u64 milliseconds")?)
}
fn now_utc() -> String {
chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
}
#[cfg(test)]
mod tests {
use super::*;
use epiphany_model_adapter::EpiphanyModelReceipt;
fn invocation(now: u64) -> EpiphanyModelConnectorInvocation {
let mut request = EpiphanyModelRequest::new(
"request-1",
"conversation-1",
"openai-codex",
"gpt-5.4",
"Return one bounded answer.",
);
request.max_output_tokens = Some(512);
EpiphanyModelConnectorInvocation {
schema_id: MODEL_CONNECTOR_INVOCATION_SCHEMA_ID.to_string(),
request_id: request.request_id.clone(),
caller_runtime_id: "ghostlight-dungeon-yggdrasil".to_string(),
expires_at_unix_ms: now + 60_000,
request,
}
}
#[test]
fn encrypted_connector_cargo_round_trips_without_exposing_plaintext() -> Result<()> {
let security = CultNetClientSecurityOptions::new("not-a-production-secret")?;
let invocation = invocation(1_000);
let envelope = encrypt_invocation(&invocation, &security)?;
assert!(
!envelope
.ciphertext
.windows(10)
.any(|part| part == b"conversation")
);
let plaintext =
CultNetSecret::decrypt_bytes(&envelope.ciphertext, &envelope.nonce, &security)?;
let decoded: EpiphanyModelConnectorInvocation = rmp_serde::from_slice(&plaintext)?;
assert_eq!(decoded, invocation);
Ok(())
}
#[test]
fn connector_rejects_wrong_caller_expiry_tools_and_output_overrun() {
let now = 1_000;
let mut value = invocation(now);
assert!(
validate_invocation(
&value,
"request-1",
"ghostlight-dungeon-yggdrasil",
"gpt-5.4",
now,
)
.is_ok()
);
value.caller_runtime_id = "foreign-runtime".to_string();
assert!(
validate_invocation(
&value,
"request-1",
"ghostlight-dungeon-yggdrasil",
"gpt-5.4",
now,
)
.is_err()
);
value = invocation(now);
value.expires_at_unix_ms = now - 1;
assert!(
validate_invocation(
&value,
"request-1",
"ghostlight-dungeon-yggdrasil",
"gpt-5.4",
now,
)
.is_err()
);
value = invocation(now);
value.request.max_output_tokens = Some(CONNECTOR_MAX_OUTPUT_TOKENS + 1);
assert!(
validate_invocation(
&value,
"request-1",
"ghostlight-dungeon-yggdrasil",
"gpt-5.4",
now,
)
.is_err()
);
value = invocation(now);
value.request.model = "another-model".to_string();
assert!(
validate_invocation(
&value,
"request-1",
"ghostlight-dungeon-yggdrasil",
"gpt-5.4",
now,
)
.is_err()
);
value = invocation(now);
value
.request
.tools
.push(epiphany_model_adapter::EpiphanyModelToolDefinition {
name: "forbidden".to_string(),
description: "must not execute".to_string(),
parameters_json: "{}".to_string(),
});
assert!(
validate_invocation(
&value,
"request-1",
"ghostlight-dungeon-yggdrasil",
"gpt-5.4",
now,
)
.is_err()
);
value = invocation(now);
value.request.previous_response_id = Some("provider-state".to_string());
assert!(
validate_invocation(
&value,
"request-1",
"ghostlight-dungeon-yggdrasil",
"gpt-5.4",
now,
)
.is_err()
);
value = invocation(now);
value.request.reasoning_summary = Some("brief".to_string());
assert!(
validate_invocation(
&value,
"request-1",
"ghostlight-dungeon-yggdrasil",
"gpt-5.4",
now,
)
.is_err()
);
}
#[test]
fn connector_refuses_outer_message_and_document_identity_split() {
let document = CultNetRawDocumentRecord {
schema_id: MODEL_CONNECTOR_ENVELOPE_SCHEMA_ID.to_string(),
record_key: "request-1".to_string(),
stored_at: now_utc(),
payload_encoding: CultNetRawPayloadEncoding::Messagepack,
payload: Vec::new(),
source_runtime_id: None,
source_agent_id: None,
source_role: None,
tags: None,
};
assert!(
request_document_from_message(CultNetMessage::DocumentPutRaw {
message_id: "request-2".to_string(),
document,
})
.is_err()
);
}
#[test]
fn reasoning_is_removed_and_public_events_are_renumbered() {
let invocation = invocation(1_000);
let mut receipt = EpiphanyModelReceipt::new("request-1", "openai-codex", "gpt-5.4");
receipt.output_tokens = Some(10);
let events = vec![
EpiphanyModelStreamEvent {
schema_id: MODEL_ADAPTER_EVENT_SCHEMA_ID.to_string(),
request_id: "request-1".to_string(),
provider: "openai-codex".to_string(),
sequence: 0,
payload: EpiphanyModelStreamPayload::ReasoningDelta {
text: "private".to_string(),
},
},
EpiphanyModelStreamEvent {
schema_id: MODEL_ADAPTER_EVENT_SCHEMA_ID.to_string(),
request_id: "request-1".to_string(),
provider: "openai-codex".to_string(),
sequence: 1,
payload: EpiphanyModelStreamPayload::TextDelta {
text: "public".to_string(),
},
},
EpiphanyModelStreamEvent {
schema_id: MODEL_ADAPTER_EVENT_SCHEMA_ID.to_string(),
request_id: "request-1".to_string(),
provider: "openai-codex".to_string(),
sequence: 2,
payload: EpiphanyModelStreamPayload::Completed { receipt },
},
];
let events = sanitize_model_events(&invocation.request, events);
assert_eq!(events.len(), 2);
assert_eq!(events[0].sequence, 0);
assert_eq!(events[1].sequence, 1);
assert!(matches!(
events[0].payload,
EpiphanyModelStreamPayload::TextDelta { .. }
));
assert!(matches!(
events[1].payload,
EpiphanyModelStreamPayload::Completed { .. }
));
}
#[test]
fn connector_refuses_unproven_or_excessive_provider_output() {
let invocation = invocation(1_000);
let event = |output_tokens| EpiphanyModelStreamEvent {
schema_id: MODEL_ADAPTER_EVENT_SCHEMA_ID.to_string(),
request_id: "request-1".to_string(),
provider: "openai-codex".to_string(),
sequence: 0,
payload: EpiphanyModelStreamPayload::Completed {
receipt: {
let mut receipt =
EpiphanyModelReceipt::new("request-1", "openai-codex", "gpt-5.4");
receipt.output_tokens = output_tokens;
receipt
},
},
};
for output_tokens in [None, Some(513)] {
let events = sanitize_model_events(&invocation.request, vec![event(output_tokens)]);
assert!(matches!(
events.as_slice(),
[EpiphanyModelStreamEvent {
payload: EpiphanyModelStreamPayload::Failed { .. },
..
}]
));
}
}
#[test]
fn connector_bounds_public_output_without_exposing_hidden_reasoning() {
let mut receipt = EpiphanyModelReceipt::new("request-1", "openai-codex", "gpt-5.4");
receipt.output_tokens = Some(700);
receipt.reasoning_output_tokens = Some(300);
assert!(receipt_proves_public_output_bound(&receipt, 512));
receipt.reasoning_output_tokens = None;
assert!(!receipt_proves_public_output_bound(&receipt, 512));
receipt.reasoning_output_tokens = Some(701);
assert!(!receipt_proves_public_output_bound(&receipt, 512));
}
#[test]
fn encrypted_result_refuses_outer_identity_substitution() -> Result<()> {
let security = CultNetClientSecurityOptions::new("not-a-production-secret")?;
let result = EpiphanyModelConnectorResult {
schema_id: MODEL_CONNECTOR_RESULT_SCHEMA_ID.to_string(),
request_id: "request-1".to_string(),
accepted: true,
events: Vec::new(),
error: None,
};
let mut envelope = encrypt_result(&result, &security)?;
envelope.request_id = "request-2".to_string();
assert!(decrypt_result(&envelope, &security).is_err());
Ok(())
}
}

View file

@ -1,7 +1,6 @@
use std::future::Future;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::time::Duration;
use anyhow::{Context, Result, anyhow};
use chrono::SecondsFormat;
@ -11,18 +10,14 @@ use epiphany_core::{
PERSONA_MODEL_STAGE_RECEIPT_SCHEMA_VERSION, PERSONA_MODEL_TERMINAL_RECEIPT_SCHEMA_VERSION,
PersonaInterpreterEffectDocument, PersonaInterpreterInput, PersonaModelStageReceipt,
PersonaModelTerminalReceipt, PersonaProjectorInput, PersonaTranscriptMessage, PersonaTurnInput,
RuntimeSpineSessionClosureOptions, build_persona_interpreter_prompt,
build_persona_projector_prompt_with_transcript, build_persona_turn_prompt,
close_runtime_session, model_pass_failure_for_request,
build_persona_interpreter_prompt, build_persona_projector_prompt_with_transcript,
build_persona_turn_prompt, close_runtime_session, model_pass_failure_for_request,
parse_and_validate_persona_interpreter_effect_set, persona_interpreter_effect_set_json_schema,
runtime_spine_cache,
terminalize_model_pass_failure_session,
runtime_spine_cache, terminalize_model_pass_failure_session,
};
use epiphany_model_adapter::{EpiphanyModelInputItem, EpiphanyModelRequest};
use sha2::{Digest, Sha256};
use crate::{EpiphanyOpenAiRuntimeOptions, assistant_text_from_model_events, run_model_turn};
#[derive(Clone, Debug)]
pub struct PersonaModelExecutionPlan {
turn_id: String,
@ -62,16 +57,6 @@ impl PersonaModelExecutionPlan {
}
}
#[derive(Clone, Debug)]
pub struct NativePersonaModelRunner {
pub store_path: PathBuf,
pub codex_home: PathBuf,
pub provider_credential_path: Option<PathBuf>,
pub provider: String,
pub model: String,
pub turn_timeout: Duration,
}
pub trait PersonaModelRunner {
fn run<'a>(
&'a mut self,
@ -83,97 +68,23 @@ pub trait PersonaModelRunner {
fn recover(&mut self, request_id: &str) -> Result<String>;
}
impl PersonaModelRunner for NativePersonaModelRunner {
fn run<'a>(
&'a mut self,
_store_path: &'a PathBuf,
stage: &'a str,
turn_id: &'a str,
request: EpiphanyModelRequest,
) -> Pin<Box<dyn Future<Output = Result<String>> + Send + 'a>> {
Box::pin(async move {
let replay = assistant_text_from_model_events(&self.store_path, &request.request_id)?;
if !replay.trim().is_empty() {
return Ok(replay);
}
let options = EpiphanyOpenAiRuntimeOptions {
store_path: self.store_path.clone(),
codex_home: self.codex_home.clone(),
provider_credential_path: self.provider_credential_path.clone(),
session_id: format!("persona-turn-{turn_id}"),
job_id: format!("persona-{stage}-{turn_id}"),
objective: format!("Run Persona {stage} stage for {turn_id}"),
coordinator_note: "Native Persona model executor; transport owns inference only."
.to_string(),
default_model: Some(self.model.clone()),
// Persona owns one outer typed turn budget. A second transport
// timer would give timeout policy two conflicting owners.
request_timeout: None,
};
let summary = tokio::time::timeout(
self.turn_timeout,
run_model_turn(&self.provider, options.clone(), request.clone()),
)
.await
.map_err(|_| {
anyhow!(
"Persona {stage} stage exceeded its {} second outer turn budget",
self.turn_timeout.as_secs()
)
})??;
let output = assistant_text_from_model_events(&self.store_path, &request.request_id)?;
if summary.verdict != "pass" || output.trim().is_empty() {
let failure_summary = if summary.verdict != "pass" {
summary.summary.clone()
} else {
format!("Persona {stage} stage completed without assistant text")
};
return Err(anyhow!(failure_summary));
}
Ok(output)
})
}
fn recover(&mut self, request_id: &str) -> Result<String> {
assistant_text_from_model_events(&self.store_path, request_id)
}
}
struct CompletedPersonaStage {
receipt: PersonaModelStageReceipt,
output: String,
}
pub async fn execute_persona_model_turn(
plan: &PersonaModelExecutionPlan,
runner: &mut NativePersonaModelRunner,
) -> Result<PersonaModelTerminalReceipt> {
if runner.provider != plan.provider || runner.model != plan.model {
return Err(anyhow!("Persona execution plan and model runner disagree"));
}
let store_path = runner.store_path.clone();
execute_persona_model_turn_owned(&store_path, plan, runner).await
}
async fn execute_persona_model_turn_owned<R: PersonaModelRunner>(
pub async fn execute_persona_model_turn<R: PersonaModelRunner>(
store_path: &PathBuf,
plan: &PersonaModelExecutionPlan,
runner: &mut R,
) -> Result<PersonaModelTerminalReceipt> {
match execute_persona_model_turn_with_runner(store_path, plan, runner).await {
Ok(terminal) => {
close_persona_session_if_active(
store_path,
plan,
format!(
"Persona turn {} reached terminal decision {}.",
plan.turn_id, terminal.receipt_id
),
)?;
close_persona_session_if_active(store_path, plan)?;
Ok(terminal)
}
Err(error) => {
terminalize_persona_execution_error(store_path, plan, &error)?;
terminalize_persona_execution_error(store_path, plan)?;
Err(error)
}
}
@ -182,7 +93,6 @@ async fn execute_persona_model_turn_owned<R: PersonaModelRunner>(
fn terminalize_persona_execution_error(
store_path: &Path,
plan: &PersonaModelExecutionPlan,
error: &anyhow::Error,
) -> Result<()> {
for stage in ["interpreter", "persona", "projector"] {
let request_id = stage_request_id(&plan.turn_id, stage);
@ -190,17 +100,12 @@ fn terminalize_persona_execution_error(
return Ok(());
}
}
close_persona_session_if_active(
store_path,
plan,
format!("Persona turn orchestration refused: {error:#}"),
)
close_persona_session_if_active(store_path, plan)
}
fn close_persona_session_if_active(
store_path: &Path,
plan: &PersonaModelExecutionPlan,
summary: String,
) -> Result<()> {
let session_id = format!("persona-turn-{}", plan.turn_id);
let mut cache = runtime_spine_cache(store_path)?;
@ -211,14 +116,7 @@ fn close_persona_session_if_active(
session.status == epiphany_core::EpiphanyRuntimeSessionStatus::Active
})
{
close_runtime_session(
store_path,
RuntimeSpineSessionClosureOptions {
session_id,
completed_at: now(),
summary,
},
)?;
close_runtime_session(store_path, &session_id)?;
}
Ok(())
}
@ -692,22 +590,11 @@ mod tests {
}
epiphany_core::open_runtime_model_execution(
store_path,
epiphany_core::RuntimeSpineSessionOptions {
session_id: format!("persona-turn-{turn_id}"),
objective: format!("Run Persona {stage} test stage"),
created_at: "2026-08-14T00:00:00Z".into(),
coordinator_note: "Persona test".into(),
},
epiphany_core::RuntimeSpineJobOptions {
job_id: format!("persona-{stage}-{turn_id}"),
session_id: format!("persona-turn-{turn_id}"),
role: "openai-model-adapter".into(),
created_at: "2026-08-14T00:00:00Z".into(),
summary: format!("Run Persona {stage} test stage"),
artifact_refs: Vec::new(),
},
&request,
"2026-08-14T00:00:00Z",
)?;
let output = match stage {
"projector" => "Epiphany feels the direct call as a live obligation, while remaining tired and sharply curious about her own machinery.".to_string(),
@ -715,19 +602,12 @@ mod tests {
"interpreter" => r#"{"schemaVersion":"epiphany.persona_interpreter_effect_set.v0","effects":[{"kind":"say","channel_id":"aquarium","reply_to_message_id":"m1","content":"The native conversational nerve is being connected, and I can show the receipts.","speech_act":"status","register":"concise","target_audience":"the addressed Discord room","safety_notes":[]}]}"#.to_string(),
_ => unreachable!(),
};
epiphany_core::complete_runtime_job(
epiphany_core::terminalize_runtime_job(
store_path,
epiphany_core::RuntimeSpineJobResultOptions {
result_id: format!("persona-{stage}-{turn_id}-transport-result"),
job_id: format!("persona-{stage}-{turn_id}"),
completed_at: "2026-08-14T00:00:01Z".into(),
verdict: "completed".into(),
summary: format!("Persona {stage} test transport completed"),
next_safe_move: "Seal the stage decision context".into(),
evidence_refs: Vec::new(),
artifact_refs: Vec::new(),
decision_context_id: None,
},
&format!("persona-{stage}-{turn_id}"),
epiphany_core::EpiphanyRuntimeJobStatus::Completed,
"2026-08-14T00:00:01Z",
&request.request_id,
)?;
Ok(output)
})
@ -807,7 +687,7 @@ mod tests {
PersonaModelExecutionPlan::from_admitted_input(
store,
"turn-1",
"test",
crate::DEFAULT_MODEL_PROVIDER,
"test-model",
cultmesh_store,
"epiphany-test",
@ -831,7 +711,7 @@ mod tests {
release_brake(&cultmesh)?;
let mut runner = FakeRunner { calls: vec![] };
let first =
execute_persona_model_turn_owned(&store, &plan(&store, cultmesh.clone())?, &mut runner)
execute_persona_model_turn(&store, &plan(&store, cultmesh.clone())?, &mut runner)
.await?;
assert_eq!(runner.calls, ["projector", "persona", "interpreter"]);
drop(runner);
@ -840,8 +720,7 @@ mod tests {
let admitted_source = restarted_plan.source_documents[0].clone();
let mut restarted_runner = FakeRunner { calls: vec![] };
let second =
execute_persona_model_turn_owned(&store, &restarted_plan, &mut restarted_runner)
.await?;
execute_persona_model_turn(&store, &restarted_plan, &mut restarted_runner).await?;
assert_eq!(first, second);
assert!(restarted_runner.calls.is_empty());
for receipt_id in &second.stage_receipt_ids {
@ -879,7 +758,7 @@ mod tests {
let mut runner = FakeRunner { calls: vec![] };
let escaped = plan_with_channels(&store, cultmesh, vec!["elsewhere".into()])?;
assert!(
execute_persona_model_turn_owned(&store, &escaped, &mut runner)
execute_persona_model_turn(&store, &escaped, &mut runner)
.await
.is_err()
);

View file

@ -0,0 +1,308 @@
use std::io::Read;
use std::path::Path;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use anyhow::{Context, Result, anyhow};
use codex_connector::{
CodexConnectorClient, CodexProviderRequest, CodexTransportDisposition,
CodexTransportEventPayload, CodexTransportInvocation, CodexTransportOutcome,
};
use epiphany_model_adapter::{
EpiphanyModelReceipt, EpiphanyModelRequest, EpiphanyModelStreamEvent,
EpiphanyModelStreamPayload, MODEL_ADAPTER_EVENT_SCHEMA_ID,
};
use epiphany_model_adapter::{
EpiphanyOpenRouterRequest, EpiphanyProviderRequestPayload, openrouter_events_from_response,
openrouter_request_body,
};
use epiphany_openai_runtime::{
EpiphanyOpenAiRuntimeOptions, EpiphanyOpenAiRuntimeRunSummary, OPENROUTER_MODEL_PROVIDER,
open_model_turn, record_model_turn_events,
};
use sha2::{Digest, Sha256};
use zeroize::Zeroizing;
const OPENROUTER_COMPLETIONS_URL: &str = "https://openrouter.ai/api/v1/chat/completions";
const MAX_CONNECTOR_FRAME_BYTES: usize = 32 * 1024 * 1024;
const MAX_OPENROUTER_RESPONSE_BYTES: u64 = 16 * 1024 * 1024;
const INVOCATION_ADMISSION_WINDOW: Duration = Duration::from_secs(60);
pub async fn run_model_turn(
provider: &str,
options: EpiphanyOpenAiRuntimeOptions,
request: EpiphanyModelRequest,
) -> Result<EpiphanyOpenAiRuntimeRunSummary> {
let provider_request = open_model_turn(provider, &options, &request)?;
let request_id = request.request_id.clone();
let events = match (provider, provider_request.payload) {
("openai-codex" | "openai", EpiphanyProviderRequestPayload::Codex(codex_request)) => {
collect_transport_events(
execute_codex_connector(&options, &request, codex_request),
&request_id,
provider,
)
.await
}
(OPENROUTER_MODEL_PROVIDER, EpiphanyProviderRequestPayload::OpenRouter(request)) => {
collect_transport_events(execute_openrouter(&options, request), &request_id, provider)
.await
}
_ => {
return Err(anyhow!(
"provider request contract disagrees with selected provider"
));
}
};
record_model_turn_events(&options.store_path, &options, &request, &events)
}
async fn execute_codex_connector(
options: &EpiphanyOpenAiRuntimeOptions,
native_request: &EpiphanyModelRequest,
provider_request: CodexProviderRequest,
) -> Result<Vec<EpiphanyModelStreamEvent>> {
let endpoint = options
.connector_endpoint
.ok_or_else(|| anyhow!("Codex provider requires an explicit connector endpoint"))?;
let caller_runtime_id = options.caller_runtime_id.clone();
if caller_runtime_id.trim().is_empty() || caller_runtime_id.trim() != caller_runtime_id {
return Err(anyhow!(
"Codex connector caller runtime identity is invalid"
));
}
let connection_key = Zeroizing::new(read_static_provider_credential(
options.provider_credential_path.as_deref(),
"codex-connector",
)?);
let native_request_sha256: [u8; 32] = Sha256::digest(rmp_serde::to_vec(native_request)?).into();
let expires_at_unix_ms = unix_time_ms()?
.checked_add(INVOCATION_ADMISSION_WINDOW.as_millis() as u64)
.ok_or_else(|| anyhow!("Codex connector invocation expiry overflowed"))?;
let invocation = CodexTransportInvocation::new(
caller_runtime_id,
expires_at_unix_ms,
native_request_sha256,
provider_request,
)?;
let client = CodexConnectorClient::new(
endpoint,
connection_key.as_str().to_string(),
MAX_CONNECTOR_FRAME_BYTES,
options.request_timeout,
)?;
let provider = native_request.provider.clone();
tokio::task::spawn_blocking(move || {
let result = client.execute(&invocation)?;
events_from_connector_result(&provider, result)
})
.await
.context("Codex connector client task failed")?
}
pub(super) fn events_from_connector_result(
provider: &str,
result: codex_connector::CodexTransportResult,
) -> Result<Vec<EpiphanyModelStreamEvent>> {
let request_id = result.request_id.clone();
match result.disposition {
CodexTransportDisposition::Refused(reason) => Err(anyhow!(
"Codex connector refused request {request_id:?}: {reason:?}"
)),
CodexTransportDisposition::Transported { events, receipt } => {
let mut projected = events
.into_iter()
.map(|event| EpiphanyModelStreamEvent {
schema_id: MODEL_ADAPTER_EVENT_SCHEMA_ID.to_string(),
request_id: request_id.clone(),
provider: provider.to_string(),
sequence: event.sequence,
payload: match event.payload {
CodexTransportEventPayload::TextDelta { text } => {
EpiphanyModelStreamPayload::TextDelta { text }
}
CodexTransportEventPayload::ToolCall {
call_id,
name,
arguments,
} => EpiphanyModelStreamPayload::ToolCall {
call_id,
name,
arguments,
},
},
})
.collect::<Vec<_>>();
let mut terminal = EpiphanyModelReceipt::new(&request_id, provider, &receipt.model);
terminal.transport = Some(receipt.transport.clone());
terminal.caller_runtime_id = Some(receipt.caller_runtime_id.clone());
terminal.native_request_sha256 = Some(hex_digest(receipt.native_request_sha256));
terminal.provider_request_sha256 = Some(hex_digest(receipt.provider_request_sha256));
match receipt.outcome {
CodexTransportOutcome::Completed {
provider_response_id,
input_tokens,
output_tokens,
reasoning_output_tokens,
cached_input_tokens,
} => {
terminal.provider_response_id = provider_response_id;
terminal.input_tokens = input_tokens;
terminal.output_tokens = output_tokens;
terminal.reasoning_output_tokens = reasoning_output_tokens;
terminal.cached_input_tokens = cached_input_tokens;
projected.push(EpiphanyModelStreamEvent {
schema_id: MODEL_ADAPTER_EVENT_SCHEMA_ID.to_string(),
request_id,
provider: provider.to_string(),
sequence: projected.len() as u64,
payload: EpiphanyModelStreamPayload::Completed {
receipt: Box::new(terminal),
},
});
}
CodexTransportOutcome::Failed {
failure_kind,
message,
} => {
projected.push(EpiphanyModelStreamEvent {
schema_id: MODEL_ADAPTER_EVENT_SCHEMA_ID.to_string(),
request_id,
provider: provider.to_string(),
sequence: projected.len() as u64,
payload: EpiphanyModelStreamPayload::Failed {
message: format!("Codex connector {failure_kind}: {message}"),
},
});
}
}
Ok(projected)
}
}
}
async fn execute_openrouter(
options: &EpiphanyOpenAiRuntimeOptions,
request: EpiphanyOpenRouterRequest,
) -> Result<Vec<EpiphanyModelStreamEvent>> {
let api_key = Zeroizing::new(read_static_provider_credential(
options.provider_credential_path.as_deref(),
OPENROUTER_MODEL_PROVIDER,
)?);
let request_timeout = options.request_timeout;
tokio::task::spawn_blocking(move || {
let body = serde_json::to_vec(&openrouter_request_body(&request)?)?;
let agent: ureq::Agent = ureq::Agent::config_builder()
.timeout_connect(Some(Duration::from_secs(10)))
.timeout_recv_response(request_timeout)
.timeout_recv_body(request_timeout)
.max_redirects(0)
.build()
.into();
let authorization = Zeroizing::new(format!("Bearer {}", api_key.as_str()));
let response = agent
.post(OPENROUTER_COMPLETIONS_URL)
.header("authorization", authorization.as_str())
.header("content-type", "application/json")
.header("accept", "application/json")
.send(body)
.context("OpenRouter request failed")?;
let mut bytes = Vec::new();
response
.into_body()
.into_reader()
.take(MAX_OPENROUTER_RESPONSE_BYTES + 1)
.read_to_end(&mut bytes)
.context("OpenRouter response read failed")?;
if bytes.len() as u64 > MAX_OPENROUTER_RESPONSE_BYTES {
return Err(anyhow!("OpenRouter response exceeded its byte bound"));
}
openrouter_events_from_response(&request, &bytes)
})
.await
.context("OpenRouter client task failed")?
}
async fn collect_transport_events<F>(
future: F,
request_id: &str,
provider: &str,
) -> Vec<EpiphanyModelStreamEvent>
where
F: std::future::Future<Output = Result<Vec<EpiphanyModelStreamEvent>>>,
{
match future.await {
Ok(events) => events,
Err(error) => vec![EpiphanyModelStreamEvent {
schema_id: MODEL_ADAPTER_EVENT_SCHEMA_ID.to_string(),
request_id: request_id.to_string(),
provider: provider.to_string(),
sequence: 0,
payload: EpiphanyModelStreamPayload::Failed {
message: format!("{error:#}"),
},
}],
}
}
fn read_static_provider_credential(path: Option<&Path>, provider: &str) -> Result<String> {
let path = path.ok_or_else(|| {
anyhow!("model provider {provider:?} requires an explicit credential file")
})?;
let raw = std::fs::read_to_string(path).with_context(|| {
format!(
"failed to read {provider} credential file {}",
path.display()
)
})?;
let credential = raw.trim();
if credential.is_empty() || raw.trim_matches(['\r', '\n']) != credential {
return Err(anyhow!(
"model provider {provider:?} credential file is empty or contains surrounding whitespace"
));
}
Ok(credential.to_string())
}
fn unix_time_ms() -> Result<u64> {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.context("system clock predates Unix epoch")?
.as_millis()
.try_into()
.context("Unix time does not fit u64 milliseconds")
}
fn hex_digest(digest: [u8; 32]) -> String {
let mut value = String::with_capacity(64);
for byte in digest {
use std::fmt::Write as _;
write!(&mut value, "{byte:02x}").expect("writing to a String cannot fail");
}
value
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn failed_transport_event_preserves_the_bounded_error_chain() {
let events = collect_transport_events(
async {
Err(anyhow!("socket timed out"))
.context("Connector client could not read its response")
},
"request-1",
"openai-codex",
)
.await;
assert_eq!(events.len(), 1);
let EpiphanyModelStreamPayload::Failed { message } = &events[0].payload else {
panic!("transport failure must project one failed event");
};
assert_eq!(
message,
"Connector client could not read its response: socket timed out"
);
}
}

View file

@ -0,0 +1,27 @@
[package]
name = "epiphany-pipeline"
version = "0.1.0"
edition = "2024"
publish = false
autobins = false
license-file = "../LICENSE"
[lib]
name = "epiphany_pipeline"
path = "src/lib.rs"
# A leaf type library. Every dependency here is one the document types or the
# write path need in a non-test build: the organ prepares, decodes and
# validates envelopes through this crate, so `anyhow` (the cache's error type)
# and `rmp-serde` (the payload decode) are normal dependencies. Only the
# schema-publication check needs JSON.
[dependencies]
anyhow = "1"
chrono = "0.4.44"
cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "8fc74c709444b884a2bd339e0c306bf9484d99d2", package = "cultcache-rs" }
rmp-serde = "1"
schemars = "1"
serde = { version = "1", features = ["derive"] }
[dev-dependencies]
serde_json = "1"

3187
epiphany-pipeline/src/lib.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,25 +0,0 @@
[package]
name = "epiphany-release-construction"
version = "0.1.0"
edition = "2024"
publish = false
autobins = false
license-file = "../LICENSE"
[lib]
name = "epiphany_release_construction"
path = "src/lib.rs"
[dependencies]
anyhow = "1"
chrono = "0.4.44"
cultcache-rs = { path = "../vendor/cultcache-rs" }
fs2 = "0.4"
rmp-serde = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
uuid = { version = "1", features = ["v4"] }
[dev-dependencies]
tempfile = "3"

View file

@ -1,19 +0,0 @@
[package]
name = "epiphany-state-model"
version = "0.1.0"
edition = "2024"
publish = false
license-file = "../LICENSE"
[lib]
name = "epiphany_state_model"
path = "src/lib.rs"
[dependencies]
schemars = "0.8"
serde = { version = "1", features = ["derive"] }
ts-rs = { version = "11", features = [
"uuid-impl",
"serde-json-impl",
"no-serde-warnings",
] }

View file

@ -1,920 +0,0 @@
use std::path::PathBuf;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use ts_rs::TS;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyAcceptanceReceipt {
pub id: String,
pub result_id: String,
pub job_id: String,
pub binding_id: String,
pub surface: String,
pub role_id: String,
pub status: String,
pub accepted_at: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub accepted_observation_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub accepted_evidence_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub summary: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyRuntimeLink {
pub id: String,
pub binding_id: String,
pub surface: String,
pub role_id: String,
pub authority_scope: String,
pub runtime_job_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub runtime_result_id: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub linked_subgoal_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub linked_graph_node_ids: Vec<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanySubgoal {
pub id: String,
pub title: String,
pub status: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub summary: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyInvariant {
pub id: String,
pub description: String,
pub status: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub rationale: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyGraphs {
#[serde(default, skip_serializing_if = "EpiphanyGraph::is_empty")]
pub architecture: EpiphanyGraph,
#[serde(default, skip_serializing_if = "EpiphanyGraph::is_empty")]
pub dataflow: EpiphanyGraph,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub links: Vec<EpiphanyGraphLink>,
}
impl EpiphanyGraphs {
pub fn is_empty(&self) -> bool {
self.architecture.is_empty() && self.dataflow.is_empty() && self.links.is_empty()
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyGraph {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub nodes: Vec<EpiphanyGraphNode>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub edges: Vec<EpiphanyGraphEdge>,
}
impl EpiphanyGraph {
pub fn is_empty(&self) -> bool {
self.nodes.is_empty() && self.edges.is_empty()
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyGraphNode {
pub id: String,
pub title: String,
pub purpose: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub mechanism: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub metaphor: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub status: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<EpiphanyCodeRef>")]
pub code_refs: Vec<EpiphanyCodeRef>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyGraphEdge {
pub source_id: String,
pub target_id: String,
pub kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub mechanism: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<EpiphanyCodeRef>")]
pub code_refs: Vec<EpiphanyCodeRef>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyGraphLink {
pub dataflow_node_id: String,
pub architecture_node_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub relationship: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<EpiphanyCodeRef>")]
pub code_refs: Vec<EpiphanyCodeRef>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyCodeRef {
pub path: PathBuf,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "number | null")]
pub start_line: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "number | null")]
pub end_line: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub symbol: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub note: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyGraphFrontier {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub active_node_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub active_edge_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub open_question_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub open_gap_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub dirty_paths: Vec<PathBuf>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyGraphCheckpoint {
pub checkpoint_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub summary: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub frontier_node_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub open_question_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub open_gap_ids: Vec<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyRetrievalState {
pub workspace_root: PathBuf,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub index_revision: Option<String>,
#[serde(default)]
pub status: EpiphanyRetrievalStatus,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
#[ts(type = "boolean")]
pub semantic_available: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "bigint | null")]
pub last_indexed_at_unix_seconds: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "number | null")]
pub indexed_file_count: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "number | null")]
pub indexed_chunk_count: Option<u32>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<EpiphanyRetrievalShardSummary>")]
pub shards: Vec<EpiphanyRetrievalShardSummary>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub dirty_paths: Vec<PathBuf>,
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
#[serde(rename_all = "snake_case")]
#[ts(rename_all = "snake_case")]
pub enum EpiphanyRetrievalStatus {
#[default]
Unavailable,
Ready,
Stale,
Indexing,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyRetrievalShardSummary {
pub shard_id: String,
pub path_prefix: PathBuf,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "number | null")]
pub indexed_file_count: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "number | null")]
pub indexed_chunk_count: Option<u32>,
#[serde(default)]
pub status: EpiphanyRetrievalStatus,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
#[ts(type = "boolean")]
pub exact_available: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
#[ts(type = "boolean")]
pub semantic_available: bool,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyScratchPad {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub summary: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub hypothesis: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub next_probe: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub notes: Vec<String>,
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
#[serde(rename_all = "snake_case")]
#[ts(rename_all = "snake_case")]
pub enum EpiphanyInvestigationDisposition {
#[default]
ResumeReady,
RegatherRequired,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyInvestigationCheckpoint {
pub checkpoint_id: String,
pub kind: String,
pub disposition: EpiphanyInvestigationDisposition,
pub focus: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub summary: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub next_action: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub captured_at_turn_id: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub open_questions: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<EpiphanyCodeRef>")]
pub code_refs: Vec<EpiphanyCodeRef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub evidence_ids: Vec<String>,
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[ts(rename_all = "snake_case")]
pub enum EpiphanyJobKind {
Indexing,
Remap,
Verification,
Specialist,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS)]
pub struct EpiphanyJobBinding {
pub id: String,
pub kind: EpiphanyJobKind,
pub scope: String,
pub owner_role: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub authority_scope: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub linked_subgoal_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub linked_graph_node_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub blocking_reason: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyObservation {
pub id: String,
pub summary: String,
pub source_kind: String,
pub status: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub code_refs: Vec<EpiphanyCodeRef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub evidence_ids: Vec<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyEvidenceRecord {
pub id: String,
pub kind: String,
pub status: String,
pub summary: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub code_refs: Vec<EpiphanyCodeRef>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyChurnState {
pub understanding_status: String,
pub diff_pressure: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub warning: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "number | null")]
pub unexplained_writes: Option<u32>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyModeState {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "ModeKind | null")]
pub kind: Option<EpiphanyModeKind>,
}
#[derive(
Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash, JsonSchema, TS, Default,
)]
#[serde(rename_all = "snake_case")]
pub enum EpiphanyModeKind {
Plan,
#[default]
#[serde(
alias = "code",
alias = "pair_programming",
alias = "execute",
alias = "custom"
)]
Default,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyPlanningState {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub workspace_root: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<EpiphanyPlanningCapture>")]
pub captures: Vec<EpiphanyPlanningCapture>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<EpiphanyBacklogItem>")]
pub backlog_items: Vec<EpiphanyBacklogItem>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<EpiphanyRoadmapStream>")]
pub roadmap_streams: Vec<EpiphanyRoadmapStream>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<EpiphanyObjectiveDraft>")]
pub objective_drafts: Vec<EpiphanyObjectiveDraft>,
}
impl EpiphanyPlanningState {
pub fn is_empty(&self) -> bool {
self.workspace_root.is_none()
&& self.captures.is_empty()
&& self.backlog_items.is_empty()
&& self.roadmap_streams.is_empty()
&& self.objective_drafts.is_empty()
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyPlanningCapture {
pub id: String,
pub title: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub body: Option<String>,
pub confidence: String,
pub status: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub speaker: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub tags: Vec<String>,
pub source: EpiphanyPlanningSourceRef,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub created_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub updated_at: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyPlanningSourceRef {
pub kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub provider: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub uri: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub external_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub repo: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "number | null")]
pub issue_number: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub node_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "bigint | null")]
pub database_id: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub state: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub labels: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub milestone: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub assignees: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub author: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub created_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub updated_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub closed_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "boolean | null")]
pub is_pull_request: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub imported_at: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyBacklogItem {
pub id: String,
pub title: String,
pub kind: String,
pub summary: String,
pub status: String,
pub horizon: String,
pub priority: EpiphanyPlanningPriority,
pub confidence: String,
pub product_area: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub lane_hints: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub dependencies: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub blockers: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub acceptance_sketch: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub evidence_refs: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<EpiphanyPlanningSourceRef>")]
pub source_refs: Vec<EpiphanyPlanningSourceRef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub duplicate_of: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub updated_at: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyPlanningPriority {
pub value: String,
pub rationale: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub impact: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub urgency: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub confidence: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub effort: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub unblocks: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub blocks: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub reason: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyRoadmapStream {
pub id: String,
pub title: String,
pub purpose: String,
pub status: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub item_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub near_term_focus: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub blocked_by: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub review_cadence: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyObjectiveDraft {
pub id: String,
pub title: String,
pub summary: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub source_item_ids: Vec<String>,
pub scope: EpiphanyObjectiveDraftScope,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub acceptance_criteria: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub evidence_required: Vec<String>,
pub lane_plan: EpiphanyObjectiveDraftLanePlan,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub dependencies: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub risks: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub review_gates: Vec<String>,
pub status: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyObjectiveDraftScope {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub includes: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub excludes: Vec<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyObjectiveDraftLanePlan {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub imagination: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub eyes: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub body: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub hands: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub soul: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub life: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyMemoryGraphSnapshot {
pub graph_id: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<EpiphanyMemoryDomain>")]
pub domains: Vec<EpiphanyMemoryDomain>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<EpiphanyMemoryNode>")]
pub nodes: Vec<EpiphanyMemoryNode>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<EpiphanyMemoryEdge>")]
pub edges: Vec<EpiphanyMemoryEdge>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<RepoFrontierItem>")]
pub frontier: Vec<RepoFrontierItem>,
}
/// A durable piece of unfinished repository anatomy. This is Modeling-owned state,
/// not a scheduler job or a presentation card.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct RepoFrontierItem {
pub id: String,
pub migration_body: String,
pub question: String,
pub gap: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub target_claim_ids: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
/// Canonical repository-relative path ceiling for this wound. This is the
/// scope that downstream Planning may narrow and Hands may eventually
/// change; inspected files and evidence provenance belong elsewhere.
pub repository_scope: Vec<String>,
pub recommended_next_organ: String,
/// The exact plan admitted by Mind for this frontier item. Imagination may
/// propose this payload, but only the dedicated model transition may make
/// it part of Modeling-owned truth.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "RepoFrontierAdoptedPlan | null")]
pub adopted_plan: Option<RepoFrontierAdoptedPlan>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub dependency_item_ids: Vec<String>,
#[serde(default)]
pub status: RepoFrontierStatus,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub evidence_refs: Vec<String>,
/// Exact immutable public sources selected upstream for Eyes. These are
/// causal retrieval inputs, not evidence produced by Modeling.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub public_source_refs: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub created_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub updated_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub retired_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub superseded_by: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct RepoFrontierAdoptedPlan {
pub planning_request_id: String,
pub result_id: String,
pub job_id: String,
pub candidate_id: String,
pub candidate_sha256: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub safe_paths: Vec<String>,
pub action: String,
pub command: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub checks: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub stop_conditions: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub rollback_steps: Vec<String>,
pub commit_message: String,
}
impl RepoFrontierAdoptedPlan {
pub fn effective_action(&self) -> &str {
self.action.as_str()
}
pub fn effective_command(&self) -> &str {
self.command.as_str()
}
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
#[serde(rename_all = "snake_case")]
#[ts(rename_all = "snake_case")]
pub enum RepoFrontierStatus {
#[default]
Proposed,
Active,
Blocked,
Resolved,
Retired,
Superseded,
}
pub fn reorient_checkpoint_from_admitted_repo_model(
snapshot: &EpiphanyMemoryGraphSnapshot,
obligation_id: &str,
) -> EpiphanyInvestigationCheckpoint {
EpiphanyInvestigationCheckpoint {
checkpoint_id: obligation_id.to_string(),
kind: "admitted_repo_model".to_string(),
disposition: EpiphanyInvestigationDisposition::ResumeReady,
focus: "Current Mind-admitted RepoModel frontier".to_string(),
summary: Some(
"Derived from the authenticated current keyed RepoModel view.".to_string(),
),
next_action: Some("Resume from the current Mind-admitted RepoModel frontier.".to_string()),
captured_at_turn_id: None,
open_questions: Vec::new(),
code_refs: snapshot
.frontier
.iter()
.filter(|item| item.status == RepoFrontierStatus::Active)
.flat_map(|item| item.repository_scope.iter())
.map(|path| EpiphanyCodeRef {
path: path.into(),
start_line: None,
end_line: None,
symbol: None,
note: Some("Current admitted RepoModel frontier scope".to_string()),
})
.collect(),
evidence_ids: Vec::new(),
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyMemoryDomain {
pub id: String,
pub title: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub description: Option<String>,
#[serde(default)]
pub lifecycle: EpiphanyMemoryLifecycle,
}
#[derive(
Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash, JsonSchema, TS, Default,
)]
#[serde(rename_all = "snake_case")]
#[ts(rename_all = "snake_case")]
pub enum EpiphanyMemoryLifecycle {
#[default]
Observed,
Proposed,
Accepted,
Retired,
Stale,
}
#[derive(
Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash, JsonSchema, TS, Default,
)]
#[serde(rename_all = "snake_case")]
#[ts(rename_all = "snake_case")]
pub enum EpiphanyMemoryNodeKind {
#[default]
Domain,
Module,
Crate,
Binary,
Schema,
RuntimeContract,
Adapter,
TestSeam,
StateStore,
Summary,
Other,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyMemoryNode {
pub id: String,
pub domain_id: String,
pub kind: EpiphanyMemoryNodeKind,
pub title: String,
pub claim: String,
pub question: String,
pub tension: String,
pub action_implication: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<EpiphanyMemoryAnchor>")]
pub anchors: Vec<EpiphanyMemoryAnchor>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<string>")]
pub source_hashes: Vec<String>,
#[serde(default)]
pub lifecycle: EpiphanyMemoryLifecycle,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub created_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub updated_at: Option<String>,
}
#[derive(
Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash, JsonSchema, TS, Default,
)]
#[serde(rename_all = "snake_case")]
#[ts(rename_all = "snake_case")]
pub enum EpiphanyMemoryEdgeKind {
#[default]
Owns,
Reads,
Writes,
Derives,
Adapts,
Persists,
Launches,
Verifies,
Supports,
Contradicts,
Grounds,
Triggers,
DependsOn,
Other,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyMemoryEdge {
pub id: String,
pub source_id: String,
pub target_id: String,
pub kind: EpiphanyMemoryEdgeKind,
pub claim: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[ts(type = "Array<EpiphanyMemoryAnchor>")]
pub anchors: Vec<EpiphanyMemoryAnchor>,
#[serde(default)]
pub lifecycle: EpiphanyMemoryLifecycle,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, TS, Default)]
pub struct EpiphanyMemoryAnchor {
pub id: String,
pub kind: String,
pub target: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "EpiphanyCodeRef | null")]
pub code_ref: Option<EpiphanyCodeRef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub evidence_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub source_hash: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(type = "string | null")]
pub note: Option<String>,
}

View file

@ -8,6 +8,6 @@ name = "epiphany_tool_adapter"
path = "src/lib.rs"
[dependencies]
cultcache-rs = { path = "../vendor/cultcache-rs" }
cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "a0813c6eed24d30bf88073ef615b633c77ebfcd6", package = "cultcache-rs" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

View file

@ -1,7 +1,7 @@
use cultcache_rs::DatabaseEntry;
pub const TOOL_ADAPTER_INVOCATION_INTENT_SCHEMA_ID: &str = "epiphany.tool_invocation_intent.v0";
pub const TOOL_ADAPTER_INVOCATION_RECEIPT_SCHEMA_ID: &str = "epiphany.tool_invocation_receipt.v0";
pub const TOOL_ADAPTER_INVOCATION_RECEIPT_SCHEMA_ID: &str = "epiphany.tool_invocation_receipt.v1";
pub const EPIPHANY_TOOL_RUNTIME_ADAPTER_ID: &str = "epiphany-tools";
pub fn tool_invocation_intent_key(intent_id: &str) -> String {
@ -102,7 +102,7 @@ impl EpiphanyToolInvocationIntent {
#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)]
#[cultcache(
type = "epiphany.tool_invocation_receipt.v0",
type = "epiphany.tool_invocation_receipt.v1",
schema = "EpiphanyToolInvocationReceipt"
)]
pub struct EpiphanyToolInvocationReceipt {
@ -126,8 +126,6 @@ pub struct EpiphanyToolInvocationReceipt {
pub result_json: Option<String>,
#[cultcache(key = 9, default)]
pub error: Option<String>,
#[cultcache(key = 10, default)]
pub sealed_raw_result_ref: Option<String>,
}
impl EpiphanyToolInvocationReceipt {
@ -151,7 +149,6 @@ impl EpiphanyToolInvocationReceipt {
completed_at: completed_at.into(),
result_json: None,
error: None,
sealed_raw_result_ref: None,
}
}
}

View file

@ -11,7 +11,7 @@ anyhow = "1"
chrono = "0.4.44"
epiphany-tool-adapter = { path = "../epiphany-tool-adapter" }
epiphany-core = { path = "../epiphany-core" }
cultcache-rs = { path = "../vendor/cultcache-rs" }
cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "a0813c6eed24d30bf88073ef615b633c77ebfcd6", package = "cultcache-rs" }
http = "1"
reqwest = { version = "0.13.2", default-features = false, features = ["json", "rustls"] }
sha2 = "0.10"

View file

@ -198,9 +198,8 @@ mod tests {
#[test]
fn parses_only_the_owned_stdio_and_http_subset() -> Result<()> {
let config = McpRuntimeConfig::from_toml(
&format!(
r#"
let config = McpRuntimeConfig::from_toml(&format!(
r#"
[mcp_servers.local]
command = "{ABSOLUTE_COMMAND}"
args = ["--stdio"]
@ -211,8 +210,7 @@ url = "https://example.test/mcp"
bearer_token_env_var = "MCP_TOKEN"
tool_timeout_sec = 4
"#
),
)?;
))?;
assert!(matches!(
config.server("local")?.transport,
McpTransportConfig::Stdio { .. }

View file

@ -31,8 +31,6 @@ use std::collections::HashMap;
use std::process::Stdio;
use std::time::{SystemTime, UNIX_EPOCH};
pub const MCP_CLIENT_NAME: &str = "epiphany-tool-mcp-runtime";
#[derive(Debug, Clone, PartialEq)]
pub struct InvocationOutcome {
pub receipt: EpiphanyToolInvocationReceipt,

View file

@ -1,7 +1,6 @@
use anyhow::{Context, Result, anyhow};
use epiphany_tool_adapter::{
EPIPHANY_TOOL_RUNTIME_ADAPTER_ID, EpiphanyToolInvocationIntent, EpiphanyToolInvocationReceipt,
tool_invocation_intent_key,
EpiphanyToolInvocationIntent, EpiphanyToolInvocationReceipt, tool_invocation_intent_key,
};
use epiphany_tool_mcp_runtime::{
McpRuntimeConfig, current_utc_timestamp, execute_epiphany_public, execute_epiphany_source,
@ -79,7 +78,7 @@ async fn execute_to_receipt(
Some(path) => path.clone(),
None => std::env::current_dir()?,
};
execute_epiphany_source(intent, &options.store, &cwd)
execute_epiphany_source(intent, &cwd)
} else if intent.server == "epiphany_public" {
execute_epiphany_public(intent).await
} else if intent.server == "epiphany_state" {
@ -216,8 +215,38 @@ fn bound(value: &str, limit: usize) -> String {
#[cfg(test)]
mod tests {
use super::*;
use epiphany_tool_adapter::EPIPHANY_TOOL_RUNTIME_ADAPTER_ID;
use epiphany_tool_adapter::tool_invocation_receipt_key;
fn seed_test_tool_job(store: &Path, session_id: &str, job_id: &str) -> Result<()> {
let created_at = "2026-08-10T01:00:01Z";
let mut cache = epiphany_core::runtime_spine_cache(store)?;
cache.pull_all_backing_stores()?;
cache.put(
session_id,
&epiphany_core::EpiphanyRuntimeSession {
session_id: session_id.into(),
objective: "Exercise the native tool boundary.".into(),
status: epiphany_core::EpiphanyRuntimeSessionStatus::Active,
created_at: created_at.into(),
updated_at: created_at.into(),
coordinator_note: "test fixture".into(),
},
)?;
cache.put(
job_id,
&epiphany_core::EpiphanyRuntimeJob {
job_id: job_id.into(),
session_id: session_id.into(),
role: "tool-runtime".into(),
status: epiphany_core::EpiphanyRuntimeJobStatus::Queued,
created_at: created_at.into(),
updated_at: created_at.into(),
},
)?;
Ok(())
}
#[test]
fn parses_production_cli_without_legacy_codex_flags() -> Result<()> {
let options = parse_cli(vec![
@ -269,32 +298,12 @@ mod tests {
"prove provider-independent native tools",
"now",
);
epiphany_core::create_runtime_session(
&store,
epiphany_core::RuntimeSpineSessionOptions {
session_id: "native-tool-session".into(),
objective: "Prove owned native tool execution.".into(),
created_at: "2026-08-10T01:00:01Z".into(),
coordinator_note: "test".into(),
},
)?;
epiphany_core::create_runtime_job(
&store,
epiphany_core::RuntimeSpineJobOptions {
job_id: "native-tool-job".into(),
session_id: "native-tool-session".into(),
role: "tool-runtime".into(),
created_at: "2026-08-10T01:00:02Z".into(),
summary: "Owned native tool execution.".into(),
artifact_refs: Vec::new(),
},
)?;
seed_test_tool_job(&store, "native-tool-session", "native-tool-job")?;
epiphany_core::put_runtime_tool_execution_intent(
&store,
"native-tool-session",
"native-tool-job",
&intent,
"2026-08-10T01:00:03Z",
)?;
let summary = run(RunOptions {
@ -344,8 +353,7 @@ mod tests {
private_state_exposed: false,
},
)?;
let grant = epiphany_core::issue_resident_self_grant(&resident_store, 2)?
.expect("grant");
let grant = epiphany_core::issue_resident_self_grant(&resident_store, 2)?.expect("grant");
let intent = EpiphanyToolInvocationIntent::new(
"native-state-read",
EPIPHANY_TOOL_RUNTIME_ADAPTER_ID,
@ -356,32 +364,12 @@ mod tests {
"prove grant-owned state observation",
"now",
);
epiphany_core::create_runtime_session(
&runtime_store,
epiphany_core::RuntimeSpineSessionOptions {
session_id: "state-tool-session".into(),
objective: "Observe resident state.".into(),
created_at: "2026-08-11T00:00:00Z".into(),
coordinator_note: "test".into(),
},
)?;
epiphany_core::create_runtime_job(
&runtime_store,
epiphany_core::RuntimeSpineJobOptions {
job_id: "state-tool-job".into(),
session_id: "state-tool-session".into(),
role: "tool-runtime".into(),
created_at: "2026-08-11T00:00:01Z".into(),
summary: "Observe resident state.".into(),
artifact_refs: vec![],
},
)?;
seed_test_tool_job(&runtime_store, "state-tool-session", "state-tool-job")?;
epiphany_core::put_runtime_tool_execution_intent(
&runtime_store,
"state-tool-session",
"state-tool-job",
&intent,
"2026-08-11T00:00:02Z",
)?;
let unbound = execute_to_receipt(

View file

@ -7,11 +7,7 @@ use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::thread;
pub fn execute_epiphany_source(
intent: &EpiphanyToolInvocationIntent,
store: &Path,
cwd: &Path,
) -> Result<Value> {
pub fn execute_epiphany_source(intent: &EpiphanyToolInvocationIntent, cwd: &Path) -> Result<Value> {
let arguments: Value =
serde_json::from_str(&intent.arguments_json).context("arguments_json is not valid JSON")?;
if !arguments.is_object() {
@ -21,7 +17,6 @@ pub fn execute_epiphany_source(
"read_file" => read_file(cwd, &arguments),
"directory_inventory" => directory_inventory(cwd, &arguments),
"git_show" => git_show(cwd, &arguments),
"read_hands_receipt" => read_hands_receipt(store, &arguments),
other => Err(anyhow!("unknown epiphany_source tool {other:?}")),
}
}
@ -220,28 +215,6 @@ fn read_retained(mut input: impl Read, limit: usize) -> Result<String> {
Ok(text)
}
fn read_hands_receipt(store: &Path, arguments: &Value) -> Result<Value> {
let id = required_string(arguments, "receiptId")?;
match required_string(arguments, "kind")? {
"patch" => {
let r = epiphany_core::runtime_hands_patch_receipt(store, id)?
.ok_or_else(|| anyhow!("Hands patch receipt {id:?} not found"))?;
serde_json::to_value(r).context("encoding Hands patch receipt")
}
"command" => {
let r = epiphany_core::runtime_hands_command_receipt(store, id)?
.ok_or_else(|| anyhow!("Hands command receipt {id:?} not found"))?;
serde_json::to_value(r).context("encoding Hands command receipt")
}
"commit" => {
let r = epiphany_core::runtime_hands_commit_receipt(store, id)?
.ok_or_else(|| anyhow!("Hands commit receipt {id:?} not found"))?;
serde_json::to_value(r).context("encoding Hands commit receipt")
}
other => Err(anyhow!("unsupported Hands receipt kind {other:?}")),
}
}
fn confined_path(cwd: &Path, requested: &str) -> Result<PathBuf> {
let root = cwd
.canonicalize()
@ -313,16 +286,11 @@ mod tests {
r#"{"path":"body.txt","startLine":2,"maxLines":2}"#,
),
dir.path(),
dir.path(),
)?;
assert_eq!(value["content"], "2: two\n3: three");
assert!(
execute_epiphany_source(
&intent("read_file", r#"{"path":"../escape"}"#),
dir.path(),
dir.path()
)
.is_err()
execute_epiphany_source(&intent("read_file", r#"{"path":"../escape"}"#), dir.path())
.is_err()
);
Ok(())
}
@ -347,7 +315,6 @@ mod tests {
r#"{"path":"artifacts","maxDepth":2,"maxEntries":10,"maxSamples":10}"#,
),
dir.path(),
dir.path(),
)?;
assert_eq!(value["complete"], true);
assert_eq!(value["entryCount"], 4);
@ -359,7 +326,6 @@ mod tests {
execute_epiphany_source(
&intent("directory_inventory", r#"{"path":"../escape"}"#),
dir.path(),
dir.path(),
)
.is_err()
);
@ -379,7 +345,6 @@ mod tests {
r#"{"path":"artifacts","maxEntries":2}"#,
),
dir.path(),
dir.path(),
)?;
assert_eq!(value["complete"], false);
assert_eq!(value["entryCount"], 2);
@ -393,7 +358,6 @@ mod tests {
r#"{"path":"nested","maxDepth":0,"maxEntries":10}"#,
),
dir.path(),
dir.path(),
)?;
assert_eq!(depth_limited["complete"], false);
assert_eq!(depth_limited["depthLimitHit"], true);

View file

@ -10,7 +10,6 @@ contract docs.
- `epiphany-anatomy.md`: current organ/sub-agent/substrate/protocol split.
- `fresh-workspace-handoff.md`: compact re-entry orientation and current live state.
- `epiphany-current-algorithmic-map.md`: broad source-grounded machine map.
- `codex-auth-spine-inventory.md`: retained Codex auth/model transport keeper list.
- `epiphany-fork-implementation-plan.md`: current campaign plan.
- `epiphany-safety-architecture.md`: capability, autonomy, and governance boundaries.

View file

@ -1,200 +0,0 @@
# Codex Auth Spine Inventory
This is the keeper list for the vestigial Codex organ. Anything not justified
here is suspect until proven otherwise. The organ must stay honest: Epiphany is
a modified Codex-derived backend with a native CultCache/CultNet body, not a
clean-room client pretending to be Codex.
## Compliance Invariant
- Keep Codex-compatible auth identity anchored in vendored Codex-derived
machinery. Do not fully replace Codex auth with a lookalike native
implementation unless OpenAI provides an explicit public path or permission
for that use.
- It is fine to cut Codex apps, skills, marketplace, plugin UX, broad
app-server workflow, and generic JSON control surfaces. Those are product and
workflow organs, not subscription-auth legitimacy.
- The retained auth/model organ should be brutally thin, but it must be real:
load/refresh credentials, preserve required client headers/identity,
respect subscription limits, and call the allowed Codex/OpenAI model route.
## Live Spine
### Inputs
- User authentication material from Codex auth storage or an external auth
provider.
- Model name and optional endpoint override from Epiphany-owned request/config
surfaces.
- Per-turn model request data: prompt instructions, formatted input, tools,
output schema, reasoning controls, service tier, and model metadata.
- Session identity: conversation/thread id, installation id, window id,
session source, optional turn metadata, and optional parent/subagent headers.
### Durable Stores
- Codex auth storage under the configured Codex home. The legal/compliance
authority should remain vendored Codex-derived auth machinery such as
`vendor/codex/codex-rs/login/src/auth/manager.rs`, even when Epiphany wraps
it in typed status documents.
- Auth token state in Codex-compatible auth records, including refreshable
ChatGPT tokens and account metadata.
- No durable model/provider state is owned by the current OpenAI spine. The
model string is request data; catalog/default selection must become a typed
Epiphany document before it can be keeper machinery.
### Transformations
- `epiphany-openai-codex-spine` imports the narrow auth types and default-client
construction it needs directly from vendored `codex-login`, without cloning
env/file/keyring/token-refresh behavior or wrapping re-exports in another crate.
Subscription credential authority remains in retained Codex-compatible
machinery; Epiphany owns only the typed status/request/event/runtime surfaces
around it.
- `epiphany-openai-codex-spine` converts `CodexAuth` directly into
authorization/account headers, chooses the ChatGPT Codex backend or OpenAI API
base URL from auth mode, builds a local serializable Responses request body,
opens an HTTP/SSE stream through `codex-client`, and parses Responses frames
into typed Epiphany stream events.
- `codex-login` remains in `vendor/codex` and should be treated as keeper
auth machinery or the source for a thin vendored auth organ. Removing it from
the native request graph may be an overcut if it makes Epiphany look like an
impersonating client instead of a modified Codex backend.
### Outputs
- Bearer-authenticated OpenAI/ChatGPT-compatible model calls.
- Streaming model response events.
- No compaction/memory/realtime helper calls are part of this spine unless a
typed Epiphany caller explicitly earns them later.
- No model catalog or model metadata store is keeper machinery yet.
- Auth and transport telemetry/errors needed to recover or report failure.
## Keep
- `codex-login`: keep as the legal authority for Codex-compatible subscription
credentials. The Epiphany transport imports only its required auth surface.
- `codex-client`: keep only as a plain HTTP/SSE transport helper for now. It
does not own Epiphany request shape, model policy, stream semantics, or durable
state.
## Cut Or Seal
- Codex app-server routing is not part of auth/model transport. It can host
compatibility wrappers while Aquarium/CLI still speak JSON-RPC, but it must
not be the native Epiphany operator surface.
- Codex apps, skills, marketplace, plugin UX, and MCP OAuth handlers are not
required for OpenAI subscription compatibility. MCP may survive as a separate
CultNet adapter, not as a reason to keep the Codex app-server brain.
- `codex-core::client` helper calls for memories/realtime/compaction are
suspicious unless Epiphany deliberately needs that OpenAI endpoint. Keep them
sealed with the transport until the native adapter split can decide.
- `codex-api`, `codex-model-provider`, `codex-model-provider-info`,
`codex-core::client`, and `models-manager` are no longer keeper request-path
organs. `codex-login` is different: broad login UX can be cut, but the
vendored Codex auth semantics should remain the anchor for subscription use.
If a later model catalog is needed, it should be a typed Epiphany catalog
document, not a revival of the old Codex provider stack.
- External bearer command auth and agent identity remain inside the retained
vendored Codex auth organ. Epiphany should expose them only if a concrete
typed runtime/status need appears; do not clone them into a second authority.
## Extract Target
Create an outside-vendor adapter with this shape:
```text
EpiphanyOpenAIAuthAdapter
-> wrap retained Codex-compatible credential loading/refresh
-> expose account/auth mode/model availability
EpiphanyOpenAIModelTransport
-> accept typed Epiphany model-turn request
-> call OpenAI Responses API through Codex-compatible auth
-> emit typed stream events / receipts
```
The adapter may depend on retained `codex-login` auth machinery and
`codex-client` while the reliquary survives. It must not depend
on `codex-app-server`, `codex_message_processor`, plugin/app/skill/marketplace
modules, broad Codex provider/request construction, or Epiphany state
ownership.
## Next Cut
The first `epiphany-openai-adapter` crate boundary now exists as a native typed
surface:
- input: typed model-turn request plus adapter config
- output: typed stream events and terminal usage/error receipt
- current dependency: no Codex app-server and no Codex transport yet
- planned internal dependency: Codex auth/model transport only
- forbidden dependency: Codex app-server or Epiphany JSON-RPC routes
The first attempt to wire `codex-core` directly as a standalone path dependency
escaped the vendored workspace lock and hit transitive ICU/temporal dependency
skew. Do not paper over that with random version pins.
The first workspace-verified wrapper now exists as
`epiphany-openai-codex-spine`. It depends on the pure typed adapter plus the
keeper Codex auth types and projects `AuthManager` / `CodexAuth` into a typed
`EpiphanyOpenAiAdapterStatus`. `epiphany-codex-bridge` re-exports this spine so
the current app-server compatibility shell can compile it without making the
pure document crate depend on Codex.
The transport wrapper is now also in that spine. It maps typed
`EpiphanyOpenAiModelRequest` documents into a local serializable Responses
request body, resolves credentials through vendored `codex-login`, chooses the
ChatGPT/OpenAI base URL from auth mode, opens an HTTP Responses SSE stream with
`codex-client`, and converts stream deltas/completion into typed
`EpiphanyOpenAiStreamEvent` / `EpiphanyOpenAiModelReceipt` documents. It no
longer imports Codex `ResponsesApiRequest`, `ResponsesClient`,
`ResponseEvent`, provider config, model-provider, or broad app-server
workflow. Its narrow direct imports preserve Codex-compatible auth identity
without letting Codex own Epiphany request/state shape.
`schemas/cultnet/` contains publication schemas for OpenAI adapter status,
model request, stream event, and terminal receipt. Live providers own their
CultMesh/CultNet catalogs. The native runtime accepts the document types
registered by the actual CultCache runtime schema; there is no standalone
hello/catalog CLI or hand-maintained mutation-contract mirror.
The native runtime route is the operator and daemon edge.
`epiphany-openai-adapter` documents
derive CultCache `DatabaseEntry`, `epiphany-core::runtime_spine_cache`
registers OpenAI adapter status/request/stream-event/receipt documents, and the
outside-vendor `epiphany-openai-runtime` crate records typed OpenAI model-turn
requests, stream events, terminal receipts, runtime sessions, jobs, and job
results into the native runtime spine. Its `model-turn` command opens the
Codex-backed transport through the typed spine; its `smoke` command proves the
CultCache route without touching the network. This is the first native caller
for the advertised OpenAI CultNet contract.
Heartbeat/specialist launches use the atomic
`epiphany-core::prepare_runtime_spine_heartbeat_job` path. Initialization,
session admission, job creation, and launch receipts are one prepared CultCache
mutation rather than a caller-visible sequence.
The heartbeat launch plan is native as well:
`epiphany-core::plan_runtime_spine_heartbeat_launch` validates the launch
contract and active runtime-link conflicts, then returns the durable job binding
and runtime link. Vendored `CodexThread` is still a persistence adapter for the
compatibility route, not the owner of that state mechanics.
The Epiphany state-update contract and mutation law have also left vendored
Codex. `epiphany-core::EpiphanyStateUpdate` now owns the update document used
by the compatibility `thread/epiphany/update`, promotion, accept, and launch
paths. `epiphany-core::epiphany_state_update_validation_errors` and
`epiphany-core::apply_epiphany_state_update` own typed validation and mutation
application. `codex-core` re-exports the contract only as a compatibility
alias, and `CodexThread` now calls native state-update functions around its
remaining revision check, persistence validation, and rollout/session writeback.
The credential extraction overcut has been corrected. Codex file/keyring/auto
auth, env API key handling, ChatGPT token refresh, account metadata parsing,
and header-client setup remain in vendored `codex-login`. The redundant
Epiphany re-export crate is gone; the root workspace carries the Codex
`tokio-tungstenite` / `tungstenite` patches required for standalone builds.
The point is ownership: Epiphany calls a model adapter; it does not live inside
the Codex host brain.

View file

@ -1,8 +1,11 @@
# Epiphany current algorithmic map
Updated: 2026-08-24
Latest committed implementation cut: `6831eb69` on `codex/epiphany-shakedown-live`
Current worktree cut: Proposal-Modeling launch-binding subtraction; Ox17 remains paused
Updated: 2026-08-25
Latest committed implementation cut: `27e1dca1` on `codex/epiphany-shakedown-live`
Current pass: historical Ox17 is checksum-sealed and retired. Idunn's active
isolated target and Connector caller are provider-neutral Capstone 17. A
replacement image construction and fresh run remain. `state/map.yaml` owns
current direction. Atlas Gate 1 and production remain paused.
This document describes the live machine. Historical cuts, rejected paths, and
proof chronology belong in git, `state/ledgers.msgpack`, and bounded smoke
@ -24,6 +27,169 @@ The fixed three-Verse trust policy and public-room directory are also direct
local Verse projections; they are not written into CultCache as counterfeit
mutable state.
The shared state-model vocabulary is inhabited, not aspirational. Exact
`a602fbdc` deletes fifteen DTO types with no external consumer: aggregate
acceptance/runtime links, a second graph/frontier/checkpoint model, global
retrieval and scratch state, generic job bindings, and churn telemetry. Keyed
Mind/runtime receipts own decisions and lifecycle identity; keyed RepoModel
documents own repository structure; scratch and retrieval inputs belong to
sealed reasoning passes. Two callerless OpenAI-runtime mouths and eight
definition-only type constants are also gone. The cut is 291 pure deletions;
the state-model crate is 366 lines. It does not touch the still-unrun Atlas
inter-swarm path.
Provider observability is typed consequence, not a no-op callback shelf. Exact
`9abadfe7` removes the frame-observation DTO, sequence/preview bookkeeping, and
observer methods from both provider transports because every live caller threw
the observation away. Codex SSE and OpenRouter responses still lower into the
same typed model events and receipts. A provider-specific transcript reader
used only by a test assertion is also absent; decision audit continues to use
sealed native/provider requests, structured terminal records, governed tool
receipts, and Mind commit receipts rather than stream text.
Durable provenance is singular. Exact `2a435eb5` advances Persona memory to v2
and removes a linked-event vector that repeated `effect_document_id` plus a
relationship slot every producer left empty. The memory keeps the exact
Interpreter effect document and decision context. Tool receipts advance to v1
and remove an always-empty raw-result reference; exact result JSON or error is
the receipt consequence. Mind/runtime writable epochs were v10/v26 at that
cut, and old stores refuse rather than inheriting the retired shapes.
Routing identity lives at the owner, not in every payload. Exact `ffd91c20`
first deleted the fixed Research role from the then-live Eyes evidence packets
because their request and decision-context identities already owned provenance.
It deletes the fixed Epiphany Persona agent label from each social mention because
the Persona-only queue and immutable turn request own routing. Resident Self
retention keeps cumulative lifecycle/envelope counts and its exact chained
deletion digest; it no longer accepts a clock merely to persist an unread
timestamp outside that chain. Persona social mention v2 and Resident Self state
v3/retention head v1 remain; exact `83611f9b` later deletes the Eyes packet
family entirely. Model Atlas is unchanged and remains unaccepted until a
live inter-swarm exchange exercises publication, transport, verification,
admission, wake, and withdrawal end to end.
Provider transport is not a second state machine. Exact `fe118e13` keeps the
exact provider request as durable decision input, but makes provider events and
receipts transient values. The runtime immediately lowers each into the native
model event/receipt family that already owns tool continuation, assistant-text
reconstruction, terminal validation, and bounded retention. The provider
event/receipt CultCache registrations, archive requirements, schema IDs, and
public CultNet schemas are deleted. Runtime advances to v28; the public catalog
has 18 schemas. `epiphany-model-runtime` now points directly at its executable
body instead of compiling through an include-only shell. Atlas is untouched.
Schema publication is not a shadow registry. Exact `dbed11b3` deletes the
nonexistent runtime-event contract and the obsolete public schema for the
private archived-session tombstone. Exact `8bb0719b` then removes every
runtime-local identity, session, job, binding, coordinator, native model/tool,
and state-ledger JSON mirror. Those documents remain typed CultCache state and
are discovered from the native registration or projected by their owning
CultMesh provider; no live CultNet crossing consumed the hand-maintained JSON,
and several schemas contradicted their Rust owners. Those two cuts left three
portable boundaries and removed 850 net maintained lines. Exact `87ea81db`
then deletes the generic OpenAI-shaped schema after its Rust document ceases to
exist. The public Epiphany catalog now contains only
`gamecult.persona_state.v0` and `epiphany.work_organ_state.v0`. Connector owns
the shared Codex contract; Epiphany's closed provider-request document remains
private Mind state. Atlas remains untouched and untested end to end.
Dependency ownership follows source ownership. Exact `695af6c6` deletes
thirteen manifest edges with no consumer in their owning package, plus the
tracked marker that kept the extinct `epiphany-openai-auth-spine` directory
alive. The release bundle no longer directly owns nine libraries used only by
leaf crates or not used at all; four leaf/dev manifests drop their own dead
edges. The lock graph contracts from 757 to 732 packages, including removal of
the orphan PostgreSQL and duplicate newer crypto chains. All nine production
entrypoints and the affected library/test targets compile individually. The
cut removes 284 net manifest, lock, and source lines. Package-scoped cleanup of
the five compiled packages removed 124.2 GiB of disposable historical build
artifacts without removing the prebuilt state tool. Atlas is untouched.
Hands consequence receipts carry consequence, not doctrine commentary. Exact
`65bf044f` removes fixed schema-version strings and unread contract prose from
the patch, command, and commit documents. CultCache owns their type/schema
identity; admission at that cut still proved the then-live intent, review,
Substrate Gate grant, runtime job, affected paths or command, immutable receipt
identity, and the resulting consequence. Fourteen unused shadow type constants
and one callerless reorient display enum are also gone. Runtime advances to
v29; old writable
state refuses. The existing lifecycle proof carries Hands concurrently with a
Persona admission and creates exact Verification work, while package cleanup
removes the 678.6 MiB it generated. The cut removes 67 net maintained lines.
Atlas remains the untouched, unaccepted collaboration slice.
Autonomous proposal origin has one owner. Exact `d9a196a0` folds the direction
result, option ordinal, and worker job identities into the Proposal-Modeling
request that already owns selection. The separate binding document, schema,
registration, third atomic write, collision branch, and copied launch/result
hashes are deleted. Promotion still strongly reads the immutable direction
request/result and worker launch/result while inserting one proposal/request
pair; launch, fulfillment, and archival validate the same request-owned chain.
Runtime advances to v30. The full keyed lifecycle and old-store refusal pass;
package cleanup removes 677.6 MiB. The cut removes 53 net maintained lines and
does not touch the unrun Atlas crossing.
Repository-domain identity is one small typed receipt, not a second Body.
Exact `6f5d6600` retains the canonical organizational repository name and exact
authenticated Body hash, then deletes the copied runtime, swarm, workspace,
fixed envelope identity/schema prose, and binding timestamp. CultCache and the
authenticated Body route already own those facts. Bootstrap still validates
the live Body and atomically strong-reads runtime identity plus the Body route;
proposal promotion and fulfillment still refuse repository or Body-hash
substitution. Runtime advances to v31. The full keyed lifecycle and old-store
refusal pass; package cleanup removes 677.2 MiB. The cut removes 50 net
maintained lines. Atlas remains protected until a real inter-swarm exchange
exercises publication, trust, transport, projection, verification, admission,
wake, brake, and re-entry together.
Operator objective intake carries the human assertion, not a second objective
model. Exact `952dcd9f` keeps thread provenance, objective text, source actor
and reference, and submission time, while deleting fixed schema prose, stored
key identity, a digest of the already-hashed payload, and contract text. The
singleton Mind objective remains the active state and the commit receipt still
binds its exact operator provenance atomically. The outcome exposes only the
projection digest, commit receipt, and changed bit. A binary test that merely
repeated the library seed/idempotency/refusal proof is deleted; the library
proof now requires byte-identical replay and the resident-grant provenance
proof remains. Runtime advances to v32. The three focused proofs pass and the
cut removes 54 net maintained lines. Package cleanup removes 18.5 GiB; the
1.7 MiB state tool is preserved while another 961.1 MiB of regenerated build
artifacts is removed. Atlas is untouched.
Repository-frontier documents carry decisions, not their own envelope labels.
Exact `718ce9c1` removes fixed schema-version fields and contract slogans from
thirteen durable documents across Proposal, Proposal-Modeling, Planning,
Research, plan candidate, PlanMind, plan decision, route, Hands authority,
Verification, Soul verdict, and post-verdict Modeling. CultCache envelope
identity plus runtime epoch v33 owns the internal shape. Semantic identity,
exact source versions, strong dependencies, decision context, evidence,
disposition, and consequence remain. Sealed worker contexts keep their schema
and contract because they are part of the exact request the model saw. The
full keyed lifecycle, epoch refusal, provider candidate mapping, core library,
and release-owned coordinator target pass. The cut removes 177 net maintained
lines and leaves the unrun Atlas crossing untouched.
Runtime epoch identity is rooted, not repeated. Exact `a4356d1f` retains the
runtime identity and swarm binding epoch fields, then removes fixed
schema-version echoes from thirteen child documents covering sessions, jobs,
execution bindings, launches, process claims, results, coordinator receipts,
death recovery, and retained archives. Their semantic and causal payloads are
unchanged. Runtime v34 remains the hard old-store refusal boundary. The core
library, full keyed lifecycle, reorientation, failure audit, transcript-free
binding audit, provider mapping, coordinator, swarm, and model-runtime targets
pass. The cut removes 122 net maintained lines and leaves Atlas plus external
crossings untouched.
The packaged model runtime has one execution authority. Exact `e60b924f`
deletes the callerless `preflight` JSON self-certificate and registered-type
catalog accessor; real store epoch admission and Idunn package witnesses own
compatibility and provenance. Exact `3b991e40` deletes the uncalled standalone
`model-turn`, `tool-followup`, and `tool-followup-turn` commands plus their
parser/DTO/test surfaces. `run-worker` remains the sole provider execution path
and retains internal tool continuation; `list-decisions` and `audit-decision`
retain transcript-free operator inspection. The two cuts remove 532 net
maintained lines. The remaining binary suite passes 8/8. Atlas is untouched.
CultMesh contains no JSON-derived operator snapshot, coordinator receipt,
Hands gate, role-review event, unauthenticated Odin/Eve provider row, or daemon
tool directory. Native runtime and keyed Mind receipts remain the owners; old
@ -49,7 +215,7 @@ social, memory, and Body inputs without that fiction.
The operator surface has no unconsumed view-lens registry, legacy acceptance
bundle builder, test-only raw-JSON result interpreter, or second CRRC
recommendation algorithm. Typed runtime-result interpretation remains shared by
recommendation algorithm. Typed outcome interpretation remains shared by
current work and reorientation. The keyed status composer is the sole CRRC
recommendation projection; its DTO does not confer routing authority.
@ -212,11 +378,146 @@ Body/domain binding, and canonical Modeling request; validation reads those
facts from their owning immutable documents instead of copying them into the
join record.
Proposal-Modeling launch provenance is also a narrow join. Exact `6831eb69`
reduces its persisted binding from twelve fields to schema, request ID, job ID,
and exact launch-document digest. The request owns proposal/runtime/thread; the
launch owns role/binding/document; the binding key derives from the job ID.
Fulfillment and archival read that exact key rather than scanning every binding.
Proposal-Modeling has no second launch record. Exact `b0a4978d` deletes the
persisted binding after its minimized form proved to own nothing. The immutable
worker launch carries the typed request reference, job, role, binding, and sealed
document; current-work, retry counting, fulfillment, coordinator actuation, and
archival all read that owner directly. Family-specific semantic requests remain
mandatory.
Planning and PlanMind likewise have no second launch record. Exact `b55e96ea`
deletes both persisted binding families and derives contiguous attempts and the
current job from the family references on immutable worker launches. A retry is
authorized only by the exact typed failed result and its failure review; the
launch transaction now strongly reads those documents and every prior launch.
The semantic Planning and PlanMind requests, sealed projections, result
validation, and two-stage adoption boundary remain family-owned.
Body Modeling, Imagination consideration, and Reorientation also have no
second launch record. Exact `8dbfcf63` deletes all three persisted binding
families. Canonical worker job identities now encode contiguous attempt order;
the immutable worker launch carries the exact family reference, role, binding,
and typed launch projection. Body retry CAS reads the prior launches, latest
job, exact structured result, and matching admission refusal. Reorientation
retry CAS reads the prior launches, latest job, typed pass failure, and exact
terminal runtime result. Imagination retry CAS reads every prior launch. Family
requests, sealed reasoning projections, result validation, substrate grants,
typed failures, and Mind admission remain with their existing owners. Runtime
writable state is v19. This cut does not touch Model Atlas or narrow the still-
unrun inter-swarm path.
Verdict-driven Modeling has one writer. Exact `3ed1d564` deletes the public
`commit_repo_frontier_modeling_request` API that production never called.
Accepted Verification already commits its audit, Soul verdict, and canonical
frontier-Modeling request atomically through the Mind owner; the deleted API
reopened the store and offered a second mutation path solely for one OpenAI
test fixture. That test now launches ordinary Body Modeling through current
work and still proves exact sealed input, structured completion, typed result
and job terminality, provider failure, and output-contract failure. Its
fabricated frontier, route, Verification, and Soul tableau is gone.
Coordinator terminal state is not an artifact catalogue. Exact `63d2991e`
advances the receipt to v1 and removes its unread step count, model-provider
echo, runtime-store path, artifact and sealed-artifact lists, arbitrary
metadata, and final-job echo. The receipt retains its terminal status/action/
reason, time, session/thread identity, plan-or-execute mode, and the exact
resident grant, launch, policy, argv, objective, release, manifest, and
executable provenance continuity authenticates. Operator artifacts remain in
the operator summary only. The published JSON schema now matches the Rust
contract instead of rejecting the live resident fields it omitted. Runtime
writable state is v20.
Runtime session and job documents are lifecycle state, not duplicate result
records. Sessions retain only grouping identity plus active/completed state;
jobs retain only identity plus Queued/Completed/Failed state. Exact structured
role/reorientation outcomes own semantic decision cargo, native model receipts
or typed pass failures own model terminality, and worker process claims own
process death. There is no generic terminal-result document or summary-bearing
lifecycle record.
Runtime physiology is created only by its real owners. Exact `cd2177e8` first
deleted `ensure_runtime_session`, whose only consumers were fixtures and whose
active-ID path returned a pre-existing session without comparing its payload.
Exact `fdefb889` later deletes the remaining generic session/job writers after
proving every caller was also a fixture. Family launch CAS, model-execution
opening, and coordinator opening now own creation. Runtime schema is unchanged.
Current typed work has one assembler. Exact `0a97eef8` deletes the separate
Body-only and unresolved-Body projectors, which reopened Body, RepoModel, and
runtime state to reconstruct subsets already owned by `project_current_work`.
It also deletes three unused per-job continuation wrappers that each rebuilt
the entire projection. Live coordinator review selectors consume the one shared
slice directly. The retained lifecycle proof confirms projection remains
thread-free and performs no live Body scan.
Exact `5a047944` removes the callerless generic helper shelf rather than
preserving API because it looks reusable: JSON ledger status, role binding and
owner lookups, unique-string accumulation, and a successful-coordinator
receipt wrapper. Typed owners and the exact coordinator binding validator stay
where live callers use them. No replacement utility layer exists.
Exact `6ccaf937` removes three more unowned mouths. Repository Body consumers
read the assembled typed Mind view rather than a one-field forwarding helper.
Persona quarantine remains keyed typed state rather than gaining a second
sorted-list projection. Substrate Gate no longer exposes a generic repo-work
planning grant that no admission or actuator consumed; exact coordinator and
worker grant constructors remain with their live authority paths. Nothing is
inserted between those owners and their consumers.
Exact `f6a2ad7f` deletes two exported readers with no workspace consumer.
Packaged-release authentication remains exact-ID based; its current-head
document remains publication state, not a general query API. Idunn
provider-health admission continues to consume and validate typed trust
anchors supplied at its boundary; it no longer owns an unused file-loading
convenience path. The signed admission, continuity, and substitution laws are
unchanged.
Imagination consideration no longer writes a review-shaped cul-de-sac. Exact
`be611f24` deletes the producerless review request, its durable type, schema
constant, export, and runtime registry entry. The retained candidate remains a
typed proposal-only outcome; a real adopted Modeling proposal follows the
separate proposal lifecycle rather than this unread document. Runtime epoch
v22 refuses v21 stores instead of silently carrying the retired type forward.
Resident Self no longer admits a receipt family that no process could produce
or consume. Exact `3140d305` removes `ResidentSelfRuntimeReceipt`, its schema
constant, and its registration/decoding branches. Resident Self state v2
refuses v1 stores without mutation. The unrelated definition-only coordinator
result-status enum is also gone; current role-board status remains the one live
display projection. Session and worker retention continue through their actual
runtime-spine owners.
Model inference no longer writes mutable adapter-status shadows. Exact
`0091e0e1` removes both generic and OpenAI-specific status documents, their
constructors/writers, runtime registrations, published schemas, and the test
that only restated the synthetic OpenRouter status fields. Provider/model
identity remains in the exact native and provider requests; credential and
transport attachment is proved where the transport is constructed and used.
Runtime epoch v23 refuses v22 stores. The local published catalog now contains
twenty live or portable contracts.
Mind no longer contains a second, uninhabited planning ontology. Exact
`2e3489c4` removes planning-capture, backlog-item, roadmap-stream, and
objective-draft document families plus their 256-line generic state-model
vocabulary. No live path could construct or admit one, so every reasoning
projection received the same empty structure. Frontier planning requests,
candidates, Mind review, adopted decisions, checkpoints, and consequence
receipts remain the sole planning lifecycle. Mind/runtime epochs v9/v24 refuse
the retired writable shape.
Blocked Persona turns have one durable consequence record. Exact `a6f73fc4`
deletes the duplicate quarantine-pressure document that was written beside the
terminal receipt but never read. The terminal receipt retains exact blocked
crossing evidence and mention digest; keyed mentions retain `quarantined`
status plus terminal-receipt identity. No second queue or pressure projection
reconstructs the same decision.
Reorientation likewise has one decision record. Exact `fed4b857` deletes the
entire continuity-gateway module and its unread recovery receipt, which copied
the persisted runtime result into a companion beside the typed Mind decision.
The sealed basis/context, exact result, strong reads, Mind decision or failure,
and commit receipt own replay and audit. Reorientation now uses the ordinary
Mind commit primitive. Runtime epoch v25 refuses v24 stores.
Bifrost owns its private feedback-signing identity. Exact `94098223` deletes
Epiphany's host-identity executable and the private signer, persisted identity,
@ -258,9 +559,9 @@ Epiphany-authored facsimile.
Exact `25d4d1fa` deletes a second fictional Hands branch: the callerless route-
relinquishment writer, its refusal receipt, its Mind receipt, and both schema
registrations. No scheduler, coordinator, actuator, admission owner, or test
consumed it. The live path is narrower: exact intent/review/grant authority,
then patch/command/commit receipts, with the commit atomically creating the
Verification request. Epiphany has no durable Hands-relinquishment lifecycle
consumed it. That intermediate path was narrower: exact intent/review/grant
authority, then patch/command/commit receipts, with the commit atomically
creating the Verification request. Epiphany has no durable Hands-relinquishment lifecycle
until a real consequence owner requires and verifies one.
Hands also does not record operator-authored descriptions as if they were
@ -272,6 +573,17 @@ receipt-chain admission and Verification projection remain ready for a future
actuator that performs the operation and emits receipts from its own observed
effects.
Exact `a01c6842` removes the remaining counterfeit review. The coordinator
created `HandsActionReview` as approved in the same function and instant as its
intent and grant; no independent reviewer or refusal producer existed. The
live chain is now adopted route plus Substrate Gate grant, minimal intent, and
`RepoFrontierHandsAuthority`, followed by exact patch, command, and commit
receipts. Receipt cargo derives job, grant, and route identity through the
intent and authority rather than copying them. Commit admission requires one
exact patch and one successful command receipt for the intent; a competing
receipt refuses instead of winning a timestamp race. Runtime is v46 and the
sealed Verification projection contract is v2.
The handoff from Hands to Soul has no transient aggregate. Exact `6ccc7dd2`
deletes the 20-field receipt-chain summary and derives the Verification request
directly from the persisted patch, command, and commit receipts plus the
@ -285,11 +597,12 @@ producer or consumer. Exact tool definitions sealed into model requests plus
runtime execution bindings and governed invocation receipts own the actual
path. The catalog retains tool intent/receipt and drops to 42 schemas.
Continuity publishes only the document it owns. Exact `ca2d2cf2` removes packet,
compaction-checkpoint, stale-turn-repair, and refusal contract rows that had no
typed documents or runtime path. Reorientation still emits the exact persisted
`ContinuityRecoveryReceipt`; compaction and handoff remain native state
physiology rather than advertised CultNet fiction.
Continuity owns no second reorientation archive. Exact `ca2d2cf2` removes
packet, compaction-checkpoint, stale-turn-repair, and refusal contract rows that
had no typed documents or runtime path. Exact `fed4b857` later removes the
unread recovery-receipt copy. The typed reorientation Mind decision/failure,
sealed context, exact result, and commit receipt own the live path; compaction
and handoff remain native state physiology.
Soul likewise publishes only its real result. Exact `f8974d52` removes generic
verification-request, invariant-check, regression, review, and refusal contract
@ -297,11 +610,12 @@ rows that had no typed documents or runtime path. The keyed repo-frontier
verification request owns the live obligation, and `SoulVerdictReceipt` owns the
persisted audit result consumed by Modeling/Mind.
Eyes publishes only concrete observation evidence. Exact `201fa192` removes
generic evidence-request, review, and refusal rows that had no typed documents
or runtime path. Authenticated source lookup receipts prove what was inspected;
the typed research decision becomes the exact persisted evidence packet consumed
by current work and Modeling.
Eyes owns one concrete admission path. Exact `201fa192` removes generic
evidence-request, review, and refusal rows that had no typed documents or
runtime path. Exact `83611f9b` then removes the never-persisted source-lookup
DTO and the duplicate evidence packet. The real grant/tool receipt chain is
authenticated in place; the structured Research decision becomes keyed Mind
evidence/observation writes under one atomic `Eyes.frontier_research` commit.
Idunn deployment truth is not mirrored into invented Epiphany-local envelopes.
Odin/Idunn owns `gamecult.idunn.deployment_manifest.v3` and signed daemon-health
@ -346,6 +660,184 @@ Codex-derived code remains only where an OpenAI provider needs its earned
authentication or transport. Neither owns Epiphany Mind, scheduler, route, or
interface authority.
Exact `a6cf9383` first made that boundary physical in the package graph. The pure
`epiphany-openai-runtime` library opens the exact model execution, derives the
provider request, admits canonical native model events, emits governed tool
intents, and records terminal physiology/audit without depending on a Codex
crate. One shared source module at the release-bundle edge owns concrete
Codex/OpenRouter authentication, credential reading, and network calls for the
model and Persona entrypoints. Persona keeps its three-stage family executor;
the service owns its concrete runner. Workers have one governed tool-capable
route, not a tool route plus a parallel no-tools opinion.
The package boundary remains earned after the `a721b763` audit. It is one
optional library target containing model/Persona orchestration, pulled only by
the two model-backed release binaries behind `openai-runtime`. The root release
package owns nine production binaries; folding the library into that package
would spread model-runtime compilation or feature scaffolding across seven
unrelated entrypoints. Package count is not improved when compile fan-out and
ownership legibility get worse.
One logical state judgment consumes one pulled snapshot. Exact `7374be5e`
makes Mind assembly, keyed RepoModel assembly, current-work projection,
reasoning-basis construction, and Reorientation reuse the same CultCache.
Exact `5f1bea39` extends that rule through immutable typed-fulfillment replay:
family admission, PlanMind continuation, worker archival, and Resident Self do
not reopen runtime state while validating the result they just selected. The
provider runtime retains one path-based role-result CAS writer; read-only
replay consumes an explicit `&CultCache` and cannot silently rebase model
output against a newer store head.
Exact `2bcbf268` makes that projection the sole coordinator scheduling input.
The accepted manual-regather obligation lives beside the other current work;
operator status and Resident Self derive from the same value. Status has no
second Mind assembly, Resident Self has no hard-coded Continue override, and
the deleted `EpiphanyCrrcAction` enum cannot impersonate a parallel state
machine. Reorientation decisions remain durable Mind documents; the boolean is
only their deterministic scheduling projection.
Exact `125d77a2` removes the residual CRRC module. Operator review opens one
runtime cache and reads both the job latch and exact typed Reorientation result
from it. Pending, Completed, and Failed are the complete live vocabulary.
Storage failure is an error, and completed-without-result is a broken invariant;
neither is laundered into a synthetic lifecycle state.
Exact `dfe3757b` carries terminal failure disposition out of that same
projection. The coordinator does not reopen the runtime job to rediscover a
fact the projection already observed; success and failure admission owners
still enforce their own exact strong dependencies.
The target boundary is independent `GameCult/CodexConnector`; exact
`6dc80f6d266db4d82566d2434adcc55a48e8ecad` owns one typed multi-caller
transport package and one public daemon binary without linking a Codex crate.
With no features the library exposes only the typed provider/result contract.
The default `client` feature adds authenticated encryption, framing, and socket
transport. Auth, raw Responses transport, CultCache replay, and Ureq/Rustls sit
behind the `daemon` feature.
Each consumer internally derives its exact typed provider request. The daemon
verifies those exact bytes, returns a digest-bound transport receipt plus typed
events, and uses a private digest-pinned official `codex app-server` child only
to own credential refresh. The child never receives cognitive input or output.
Replay is one keyed CultCache document per caller/request identity in an owned
Redb store. `Active` is durable before provider I/O; the exact encrypted
`Completed` response is durable before socket reply. After restart, completed
work replays byte-identically and ambiguous active work refuses without
consuming unrelated live capacity. RAM retains only the current process's live
execution set.
Exact Epiphany `ed7357a2` completes its source-side consumer cut. The model and
Persona entrypoints cross the shared client ABI; the complete embedded Codex
spine, compiled Codex graph, direct auth/HTTP/SSE path, and Codex-home/readiness
plumbing are gone. OpenRouter remains a separate direct provider edge. Durable
model receipts retain Connector caller identity, exact native/provider digests,
and cached-token usage without requiring a transcript. Redacted CultMesh/Odin
readiness and one real Epiphany round trip remain open. Yggdrasil now runs the
independent service and Idunn target for Ghostlight; Ghostlight can consume the
ABI but cannot mutate Connector's source, package, service, credential, or
rollback authority.
Exact `c37cae8b` closes the remaining request-authorship split. The consumer
adapter turns the canonical native request directly into one closed durable
provider-request variant before inference. For Codex, that variant contains the
exact `CodexProviderRequest` sent across the socket. The release edge may add
only Connector invocation physiology: caller identity, native-request digest,
expiry, encryption, and framing. It cannot lower schemas, rename tool calls,
choose tools, or author a second provider request. OpenRouter retains its own
exact direct request variant. Decision contexts and runtime model execution
persist the same provider-request bytes; runtime v47 and decision context v3
refuse the old writable shape.
Exact `b83c0416` deletes the transient provider-specific request-input, tool,
event, payload, and receipt vocabulary. CodexConnector and OpenRouter both emit
the canonical `EpiphanyModelStreamEvent` family directly, whose completed event
carries the canonical `EpiphanyModelReceipt`. No isomorphic normalization pass
or provider-receipt field copy remains.
Exact `a721b763` deletes the `epiphany-openai-adapter` Cargo package. Pure
provider lowering and OpenRouter response projection are modules of
`epiphany-model-adapter`, beside the native contracts they translate. Network
transport remains outside: OpenRouter at the release edge and Codex in the
independent daemon.
Exact `8086d66f` makes packaged-release ownership physical. The pure release
constructor and its witness/file verifier live beneath Core's existing
`packaged_release` module, beside the CultMesh publication and authentication
path that consumes them. The single-consumer `epiphany-release-construction`
package, manifest, workspace target, and dependency edge are gone. The source
body moved byte-for-byte; exact target, cache, substitution, and tamper proofs
remain module tests under the owning Core library.
Exact `1e66e770` removes the three Hands receipt convenience constructors. A
receipt is an explicit typed consequence, not an object whose provenance should
be hidden behind a test-only factory. The retained writers are the admission
owner: patch and command identities are immutable; commit validates the exact
route, intent, Substrate Gate grant, patch, successful command, current frontier,
and atomically creates the Verification obligation. The current-work lifecycle
still proves disjoint Persona and Hands commits merge.
Exact `fab6a59a` deletes the producerless Idunn runtime-health projection. No
production path called its aggregate calculator, signer, or RUDP publisher;
only its own tests did. Idunn owns deployed daemon health outside Epiphany, and
Epiphany's swarm derives startup permission through `resident_readiness`. A
second dormant publisher was not redundancy; it was an unauthenticated claim
of physiology.
Exact `200732e9` deletes the adjacent producerless Idunn provider-health store.
No production process issued its query, verified a candidate through it, or
read its monotonic admission documents; only six module-local tests did. The
release swarm compiles without that vocabulary and continues to authenticate
its exact packaged release, brake, process owner, and resident provider through
`resident_readiness`. Idunn's deployment health remains an external receipt,
not an Epiphany-owned shadow store.
Exact `1e4fa533` narrows packaged-release API to actual consumers. Authentication
opens its typed witness directly; construction-private binary enumeration and
witness I/O no longer masquerade as root APIs; validation remains visible only
to the parent publication/authentication owner. Package, publish, inspect,
authenticate, and exact digest operations remain the external contract.
Exact `e905dcd9` removes the canonical brake-surface helper as a separate API.
The seven protected surfaces are construction data of the default brake, not a
second callable authority. Engage, release, load, write, validation, and the
typed brake document remain unchanged.
Exact `f962c0bc` removes the test-only `ResidentProviderHealth` aggregate and
its authenticator. Resident startup has one decision path:
`derive_resident_cognition_readiness` loads the exact provider readiness row,
probes its process singleton, and asks `provider_authority_failures` for the
concrete failed predicates through `provider_is_fresh`. The dead boolean
wrapper and duplicate release/freshness/owner test are gone; exact predicate,
clock-skew, release/process/brake, CAS, and private-state proofs remain.
Exact `d0a317bd` removes five one-caller review-job-ID wrappers. One coordinator
review dispatch pulls one current-work snapshot and derives Research,
Verification, proposal Modeling, Body Modeling, or frontier-verdict Modeling
identity from that same state. The shared attempt projection exposes its job ID
only for Review; every family still enters its own exact result and admission
owner. No generic role registry or second scheduling projection exists.
Exact `1a9dec56` collapses the adjacent duplicated review consequences. A
private target names the already-selected family and exact job; the coordinator
then performs one typed result read and one operator-stop path. Automatic review
dispatches to the existing family-specific admission function, emits the same
family event cargo, and refreshes status once. The target is in-process control
flow only: it owns no durable state, scheduling, callback table, or CAS law.
Exact `fca516f5` narrows the adjacent inspection surface. Resident-provider
documents are validated and loaded only inside their readiness owner. Resident
Self exposes no generic terminal-receipt reader; the consequential verifier
reads the exact typed envelope. RepoModel basis currentness is checked through
the cache-owned complete-view validator, with no second path-based assembler.
Exact `e7b66b35` makes causal request-ID construction an internal Core organ.
The six family functions still share the one length-framed SHA-256 primitive;
the crate root carries them only at crate visibility, not as downstream ABI.
Exact `9f189871` applies the same boundary to adjacent cognition helpers. The
admitted-direction prompt is called through its owning module, supersession is
crate-only, and Resident Self policy/death helpers are same-module private.
## Canonical authority map
| Owner | Inputs | Outputs | Invariant |
@ -354,15 +846,14 @@ interface authority.
| `reasoning_context.rs` Mind commit owner | invariant-owned strong reads and complete typed writes | atomic batch CAS plus `EpiphanyMindCommitReceipt` | Disjoint identities merge; same-identity or changed-strong-read conflicts refuse without partial mutation. |
| runtime bootstrap | immutable runtime identity envelope | exact identity strong read plus keyed session/job/launch writes | Worker launch replays the original identity and existing session envelopes byte-for-byte; it cannot refresh a singleton timestamp or capability mirror and thereby serialize unrelated work. Native runtime kind and supported document types are derived for CultNet publication. |
| concrete family admission owners | sealed decision context, exact family request/result chain, affected semantic documents | one family-specific `MindMutation` | The model cannot choose which stale state is safe to ignore. |
| `current_work.rs` | keyed Mind view and exact runtime request/job/result/decision families | pure family scheduling and continuation projections | Events, timestamps, role lanes, and thread provenance cannot create or suppress work. |
| `current_work.rs` | one pulled CultCache snapshot containing keyed Mind and exact runtime request/job/result/decision families | pure family scheduling and continuation projections | Mind, RepoModel, Research, Planning, Hands readiness, and launch projections derive from the same envelope set. Events, timestamps, role lanes, thread provenance, and later live reads cannot create or suppress work. |
| coordinator policy/status | keyed Mind presence, exact current work, and the accepted continuity decision | one action/reason pair plus operator-readable role lanes | Coordinator presence is derived; no mutable coordinator head, pressure tableau, or duplicate policy wrapper exists. |
| runtime worker attempt owner | immutable launch, exact process claim, typed result, job result, archival evidence | one terminal attempt authority | Scheduling, process liveness, and semantic admission remain distinct authorities. |
| model-provider boundary | sealed native model request plus explicit provider configuration and injected credential | exact internally derived provider request and transport result | Provider selection cannot author a second request truth or admit Mind state. |
| model-provider connector | one configured caller's encrypted expiring native request over bounded loopback CultNet | ordered public model events, terminal receipt, and redacted CultMesh capability status | A consumer may borrow Codex transport and physical concurrency without borrowing Epiphany's Mind, credentials, swarm lifecycle, or state-admission authority. |
| runtime worker attempt owner | sealed typed launch, exact process claim, semantic result or typed failure, job result, archival evidence | one terminal attempt authority | The launch document solely owns live family identity. The result cannot repeat routing identity; after live deletion the archive owns exact request kind/ID beside the preserved structured decision. Scheduling, process liveness, and semantic admission remain distinct authorities. |
| model-provider boundary | sealed native model request plus explicit provider configuration | one closed durable provider-request variant and transport result | The consumer adapter owns provider lowering. OpenRouter remains a release-owned direct boundary. For Codex, the persisted request is the exact `CodexProviderRequest` crossing the lean Connector client ABI; the release edge adds only authenticated invocation physiology. The daemon owns caller admission, credential refresh, exact transport, and durable replay. No transport layer can author a second request truth or admit Mind state. |
| OpenAI Responses schema projector | full native typed output schema | one provider-legal strict generation schema | Provider formatting preserves useful supported constraints but never replaces native decoding or Mind admission. |
| model-pass terminal owner | sealed reasoning basis/context and typed failure class | exact transport closure plus `EpiphanyModelPassFailure` and terminal session/job result in one batch CAS | The caller cannot nominate a job/session; the context-derived binding closes role, reorient, and Persona failures without granting generic transport results decision authority. |
| Substrate Gate | exact worker/job authority and requested operation | scoped grant or refusal | Access permission does not admit Mind state. |
| Eyes | explicit external-evidence obligation plus governed source receipts | typed evidence packet and Mind observations | Eyes gathers outside evidence; it does not gate Modeling over the Body. |
| Eyes | explicit external-evidence obligation plus governed grant/tool receipts | keyed Mind evidence/observations plus exact commit receipt | Eyes gathers outside evidence; it does not gate Modeling over the Body. |
| Modeling | Body basis, keyed RepoModel view, verified consequences, and explicit proposals | typed graph/frontier mutations | Modeling processes the Body directly and owns no external-source permission. |
| Hands | adopted route/plan plus exact capability receipts | typed consequences | A claimed intention is not a consequence. |
| Soul | exact consequence and invariant/evidence obligations | verification audit or refusal | Work is not true merely because it ran. |
@ -651,12 +1142,14 @@ Coordinator priority is a pure projection:
No default Modeling job, latest lane, accepted-at comparison, runtime event, or
generic interrupt can manufacture work.
There is no persisted generic runtime-event document. A runtime session or job
owns its own lifecycle state; `EpiphanyRuntimeJobResult`,
`EpiphanyModelPassFailure`, `EpiphanyCoordinatorRunReceipt`, and
`EpiphanyCoordinatorDeathRecovery` own their exact terminal facts. CultNet and
operator views derive display from those documents. No paired event may become
a second terminality, archival, replay, or recovery authority.
There is no persisted generic runtime-event or generic job-result document. A
runtime session or job owns only its lifecycle latch;
`EpiphanyModelPassFailure`, typed role/reorientation outcomes,
`EpiphanyCoordinatorRunReceipt`, exact process claims, and
`EpiphanyCoordinatorDeathRecovery` own their respective terminal facts.
CultNet and operator views derive display from those documents. No paired event
or summary record may become a second terminality, archival, replay, or
recovery authority.
```mermaid
flowchart TD
@ -677,6 +1170,16 @@ flowchart TD
Eyes may create evidence that creates Modeling work. Eyes acceptance never
suppresses or authorizes ordinary Body Modeling.
Exact `edb5c3a3` deletes the eight optional family request-ID mirrors from the
generic worker launch and heartbeat options, plus their terminal-result echoes,
the copied Body basis, and the copied frontier route. Every current-work,
admission, retry, schema, and tool path derives live family identity from the
sealed typed launch. A successful terminal result must contain the semantic
payload required by that context; a typed failure contains one nonempty error
and no success cargo. Retained attempts carry request kind/ID explicitly because
retention deletes the live launch. Runtime is v39 and the role-result contract
is v5; no old-store reader exists.
Each simple unresolved model-pass family embeds one
`EpiphanyAgentPassAttemptProjection`: continuation action plus the exact latest
runtime job identity when one exists. Body, proposal and frontier-verdict
@ -706,9 +1209,87 @@ These are distinct authorities and must not be collapsed into one state enum:
- Mind owns adoption of a successful structured family result.
- Retention owns deletion only after all live authorities are terminal.
The attempt aggregate centralizes exact request association and typed terminal
classification. Archived fulfilled attempts must retain recoverable decision
authority, not merely an ID/digest tombstone.
Execution bindings own edges, not copied request content. Exact `ce6bff12`
reduces a model binding to request, session, and job identity; provider, source
worker, and reasoning basis are read from the exact native model request. A
tool binding contains only intent, session, and job identity; its optional
model ancestry is read from the exact typed tool intent. Validation and
retention reload those owners. No binding timestamp participates in causality.
Runtime writable state is v40.
Exact `cabdd6c3` separates coordinator incarnation audit from model-session
admission. `EpiphanyRuntimeSession` now owns only a grouping identity and the
active/completed latch that prevents new model/tool work after closure.
Coordinator runs never inhabit that aggregate. Their immutable
`EpiphanyCoordinatorRunBasis` records thread, resident launch identity,
objective, and start time. Terminal success/failure receipts and exact process
death recovery both compete through one deterministic
`EpiphanyCoordinatorRunTerminality` identity; the winner and its full typed
authority are inserted atomically while the exact basis is replayed
byte-identically. Unrelated document commits do not participate. Timestamp
ordering, coordinator notes, whole-store snapshots, and generic archived status
do not own terminality.
Exact `dbb00c2b` finishes the model-session audit without deleting a real phase
boundary. A model session can have terminal jobs between tool rounds, so
binding-derived job terminality cannot say that the session has been explicitly
closed. The live active/completed latch owns that decision. Retention later
deletes the bindings and exact tool/model documents; the private archived
session therefore owns the retired identity set and deletion-chain digest that
prevents resurrection. Six echo-only fields leave the in-memory model-turn
summary, and the one-field closure options wrapper is gone: 38 net lines.
Exact `a46725a1` reduces the generic runtime job to the only transition it
actually performs. A job is inserted `Queued` and terminalizes as `Completed`
or `Failed`; no writer ever produced `Running`, `WaitingForReview`, or
`Cancelled`, so those states and both creation/update clocks are deleted.
Frontier Research no longer sorts attempts by creation time. It evaluates the
exact set: more than one live or successful authority refuses, one live attempt
waits, one successful attempt becomes reviewable, and terminal failures require
their exact job result before retry. Runtime writable state is v41. The next
audit boundary is the remaining job role/session join and generic terminal-result
cargo, not another lifecycle layer.
Exact `10a70afc` removes the remaining generic job ownership mirrors.
`EpiphanyRuntimeJob` now owns only its semantic identity and the mutable
Queued/Completed/Failed latch. The sealed outer-worker launch owns role;
`EpiphanyRuntimeModelExecutionBinding` owns model-session membership; outer
workers belong to the root session by construction. Session closure, retention,
and tool admission derive those exact joins. Generic job results no longer copy
role/session and lose their always-empty metadata map. A job snapshot requires
zero or one exact terminal result and refuses multiplicity rather than choosing
by completion time. Runtime writable state is v42.
Exact `53e869c0` removes evidence and artifact cargo from the generic terminal
job result. Those values belong only to the structured role or reorientation
outcome that authored them. Generic transport/process physiology retains
verdict, terse summary and next move, completion time, and optional exact
decision-context identity. Runtime writable state is v43.
Exact `8eaa96c5` removes the generic terminal document itself from structured
worker completion. A typed role/reorientation outcome is strong-read and
byte-identically replayed in the same CAS that terminalizes its job and process
claim. Failed typed outcomes close as `Failed`/`TerminalFailure`; successful
outcomes close as `Completed`/`TerminalResult`. Archives carry context plus the
full typed role result, including typed failures. The generic result family is
deleted. Runtime/Mind writable state is v45/v11; archived worker attempts are
v3.
The sealed typed launch owns exact live request association. The terminal role
result owns only semantic decision cargo or typed failure. The archived attempt
owns historical request kind/ID and typed terminal classification after the
live launch is deleted. Archived attempts retain the structured role result
and decision context—not an ID/digest tombstone—and preserve typed failure
cargo when the terminal process status is failure.
Verification receives its complete Hands consequence chain in the sealed typed
projection: route, authority, intent, patch, command, and commit. Exact
`24023265` deletes the second input path that exposed a governed tool for live
runtime-store rereads of the same three receipts. The MCP schema/dispatch,
prompt mandate, three public readers and exports, source executor store
argument, stale Substrate Gate allowance, and advertisement assertion are gone.
Tool observations remain for facts genuinely outside the sealed basis. The
durable receipt chain and atomic commit-to-Verification admission are unchanged.
The heartbeat scheduler does not exist. Resident Self directly selects the
oldest pending pressure when no grant, prepared launch, or active lease exists.
@ -727,6 +1308,15 @@ the mention pending with the terminal receipt bound into its next deterministic
request identity. Resident Self cannot launch, block, or terminalize Persona
work.
Delivered speech has one local consequence owner. Exact `b3e9b229` deletes the
standalone Persona delivery-evidence document and the conversation receipt's
copied evidence-ID vector. Bifrost keeps the durable signed crossing request and
receipt; the Persona social terminal folds that exact receipt into message ID,
crossing receipt ID, and digest. Retention reloads and verifies the provider-
owned pair and matches it against the terminal. Runtime is v37; the terminal
and conversation execution schemas are v2. This is not an Atlas proof: live
inter-swarm collaboration remains unrun.
## RepoModel projection
RepoModel persistence is keyed by semantic identity: identity/body binding,
@ -765,12 +1355,22 @@ receipts, and their direct context links.
Runtime retention may remove SSE frames, deltas, intermediate requests,
provider events, and tool scaffolding only after the terminal context is sealed
and the retained decision can still reach its basis/context without them.
The v1 worker-attempt archive embeds the exact typed role result and its
context-bound generic result family. The read-only decision audit consumes the
same durable records before and after archival.
The v3 worker-attempt archive embeds exact request kind/ID, decision context,
and the full typed role outcome when one exists. The read-only decision audit
consumes the same durable records before and after archival; it never
reconstructs family identity from terminal-result echoes or prose summaries.
## Verification and open gates
Focused acceptance at exact `edb5c3a3` proves the complete keyed state-flow
across Body and both frontier Modeling families, Planning/PlanMind,
Hands/Verification, and Eyes/Research without thread-state or result-echo
authority. Content-addressed basis/context substitution, exact provider/tool
bytes, transcript-free audit, typed failure, disjoint merge, same-identity
conflict, and v38 writable-store refusal all pass. Core/OpenAI libraries and the
release-owned model binary compile natively. The cut removes 443 maintained
lines net; it does not claim a fresh package, Atlas Gate, or live collaboration.
Accepted through the local `5b799b12` source boundary:
- core library `494/494`;
@ -828,11 +1428,12 @@ Accepted through the local `5b799b12` source boundary:
Open before Model Atlas Gate 1 resumes:
1. let Idunn compile, test, and seal exact build-affecting source `6831eb69`;
2. use that exact package in private fresh-store Ox17; stop after three
provider failures total, and prove direct Body Modeling, typed
refusal-to-retry if exercised, Hands through Verification, exact context
audit, and restart/re-entry without public speech;
1. construct the renamed Capstone 17 runtime package from the exact current
Epiphany commit and deploy it with new empty private volumes;
2. verify Connector `80de3acc` plus the bounded `1eeea5f` bridge before
inference, reset the failure budget for that new root, and prove direct Body Modeling,
typed refusal-to-retry if exercised, Hands through Verification, exact
context audit, and restart/re-entry without public speech or Hands mutation;
3. only then restart Model Atlas Gate 1 from a new
external root.

View file

@ -7,14 +7,13 @@ of app-server routes.
## Objective
Build an inspectable native organism whose typed state, runtime, organ gates,
memory, scheduling, interfaces, and social crossings can operate without Codex
owning Epiphany cognition. Retain Codex-derived code only where it provides an
earned OpenAI-compatible authentication or model-transport capability.
memory, scheduling, interfaces, and social crossings operate without Codex
owning Epiphany cognition. OpenAI subscription authentication and model
transport belong to the independent CodexConnector daemon, not Epiphany's body.
## Current Mechanism
- `epiphany-state-model` owns shared typed semantic contracts; durable Mind is keyed CultCache owned by `epiphany-core`.
- `epiphany-core` owns keyed Mind admission, runtime spine, organ gates,
- `epiphany-core` owns the semantic contracts, keyed Mind admission, runtime spine, organ gates,
Resident Self physiology, Persona, and CultMesh integration. Coordinator policy,
status, and every migrated work family consume keyed Mind plus exact runtime
receipts. Persisted `EpiphanyThreadStateEntry`, the in-memory
@ -26,8 +25,8 @@ earned OpenAI-compatible authentication or model-transport capability.
evidence sources until a typed reasoning consumer earns them.
- CultCache stores typed state and receipts; CultMesh/CultNet carry typed local
and federated projections.
- Vendored Codex exposes no Epiphany route, DTO, thread-state field, rollout
migration, scheduler, watcher, or bridge crate.
- Codex implementation source is absent from Epiphany. The typed Connector
contract is the only Codex-facing compile dependency.
## Invariants
@ -68,7 +67,8 @@ earned OpenAI-compatible authentication or model-transport capability.
### 0. Federate Decision-Auditable Mind
Status: source-complete; exact-package capstone open. The persisted thread head,
Status: source-complete; historical Ox17 checksum-sealed and retired; renamed
Capstone 17 image construction and fresh run active. The persisted thread head,
global revision transaction, aggregate RepoModel, generic Mind gateway, and
dual readers are deleted. Concrete invariant owners submit exact-envelope
`MindMutation` plans; CultCache batch CAS merges disjoint identities and refuses
@ -223,17 +223,93 @@ Delete or demote:
## Immediate Next Action
Keep the five-day shakedown, Ox17 deployment, and Model Atlas operational Gate
1 paused. Continue the source-subtraction and test-authority audit from exact
`6831eb69`. The current sequence has deleted the semantic-memory/workspace-coverage
projection system, its local daemon supervisor, and the surviving generic
RepoModel profile, impossible lifecycle, producerless receipt/summary, and
decorative scoring axes. Modeling consumes typed Body and keyed RepoModel state
directly. Domains are seed-owned; model passes mutate only live keyed operations.
Persona consumes keyed memory and social state directly. No
Epiphany-owned Rider or Unity bridge exists: Brokkr owns Unity through typed
Eve/CultMesh capabilities, and a future Rider daemon will own Rider. Do not
admit a fresh package until the remaining production surfaces and tests have
earned their authority under the same audit.
1 paused. Exact `a721b763` leaves Epiphany with one native model event/receipt
family and one package owning native contracts plus pure provider lowering.
The library-only `epiphany-openai-runtime` package remains earned: its one
optional target isolates model/Persona orchestration from seven unrelated root
release binaries. Exact `8086d66f` deletes the single-consumer release-
construction package and moves its pure constructor beneath Core's existing
packaged-release owner. Exact `1e66e770` deletes the producerless Hands receipt
constructors while retaining the consequence writers that protect the actual
route/grant/Verification chain. Exact `fab6a59a` deletes the never-called Idunn
runtime-health signer/aggregate/RUDP module. Exact `200732e9` deletes its
equally isolated provider-health query/admission store. The release swarm still
uses `resident_readiness`; Idunn's deployed health authority remains external.
Exact `1e4fa533` then narrows packaged-release API to its real package,
publish, inspect, authentication, and digest consumers. Exact `e905dcd9`
inlines the single-caller canonical brake-surface list without changing brake
authority. Exact `f962c0bc` deletes the test-only resident-provider aggregate,
its dead boolean predicate wrapper, and one duplicate test while preserving the
single live readiness path and its exact failed-predicate evidence. Audit the
five one-caller current-work review-ID wrappers next: derive review identity
from one already-owned projection if the coordinator has it, and delete the
repeated store reads instead of introducing a generic review registry. Exact
`d0a317bd` lands that cut. Continue through the adjacent five duplicated review
consequence branches: select one private typed family target, execute the common
result/stop/admit/status skeleton once, and retain family-specific admission
without adding persisted or registry state. Exact `1a9dec56` lands that cut.
Continue with a class audit of test-only public readers and validators; demote
interfaces needed only by consequential Core proofs, and delete those that
merely manufacture a second authority. Exact `fca516f5` lands that class cut:
the terminal-receipt proof now reads the typed envelope in its test, RepoModel
currentness uses the live cache validator, and resident readiness internals are
private. Continue at the crate-root export boundary; deterministic causal-ID
helpers used only inside Core should not masquerade as a public library API.
Exact `e7b66b35` demotes that entire causal-ID surface without changing the
digest law. Continue the same class audit through internal prompt rendering,
supersession, and Resident Self policy/completion helpers; keep entrypoints that
are actually consumed by release binaries or sibling packages public.
Exact `9f189871` lands that cut and removes the dead prompt root alias. Exact
`ed4ca58c` then deletes `RepoFrontierAdoptedPlan::effective_action` and
`effective_command`; the operator projection and Hands consequence validation
read the owning public fields directly. A bounded closing audit found no
further unearned public function across `runtime_spine`, `state_model`,
`current_work`, `resident_readiness`, and `resident_self`; every survivor has a
production consumer, and the sole low-fanout candidate protects resident
objective authentication. Source subtraction has reached diminishing returns.
Construct and run the fresh provider-neutral Capstone 17 package next;
`state/map.yaml` owns its current direction.
Resident startup now has one health decision. `derive_resident_cognition_readiness`
loads the exact provider row, probes the process singleton, and obtains the
concrete failed authority predicates through `provider_is_fresh`. No parallel
aggregate or boolean wrapper can reinterpret those facts for another caller.
Coordinator family review now consumes one `EpiphanyCurrentWorkProjection`.
Five path-based job-ID APIs are gone; the shared attempt projection exposes a
job ID only in Review state. Research keeps its specialized lifecycle and the
three Modeling families keep their established priority. The remaining repeated
result-read/operator-stop/admission blocks are gone. One private target now
selects the family, and one common pipeline reads, pauses, admits, emits, and
refreshes. The target cannot persist, schedule, or admit anything itself.
Preserve exact provider-request sealing, the lean Connector client ABI,
OpenRouter's separate temporary release edge, keyed Mind audit/CAS guarantees,
direct Body-to-Modeling flow, Persona state, and the complete unrun Model Atlas
slice. No Epiphany-owned Rider or Unity bridge exists: Brokkr owns Unity through
typed Eve/CultMesh capabilities, and a future Rider daemon owns Rider.
The provider decision is the independent `GameCult/CodexConnector` service;
exact `6dc80f6d266db4d82566d2434adcc55a48e8ecad` owns the v2 multi-caller contract,
exact native/provider digest binding, typed tool and terminal receipts, the
pinned official Codex credential child, raw Responses transport, and durable
keyed replay. No features yields only the typed contract; default `client` adds
authenticated framing and socket transport; `daemon` adds service authority.
It links no Codex crate. Active replay identity is written before network
access; the exact encrypted completion is durable before reply; restart
ambiguity refuses rather than re-executing. Yggdrasil now runs Connector as an
independent service and Idunn target for Ghostlight. One digest-bound Epiphany
round trip remains unaccepted while Epiphany's selected edge is OpenRouter.
Epiphany pins the contract-only surface in its adapter and enables the client
only at its release edge. Exact `87ea81db` also deletes Epiphany's obsolete
public schema for the extinct generic provider request; the shared Codex
contract belongs to Connector, while Epiphany's wrapper remains private Mind
state. Ghostlight exact `8e7d980` pins the same Connector source; its live Ygg
deployment consumes the independent service without owning it. Exact `7a125d78` removes the excluded 3,592-file
Codex implementation tree and its obsolete keeper notes; gamecult-ops exact
`cb63e77` repairs the independent deployment ownership map. No dual daemon or
refresh writer is an acceptable transition state.
Model Atlas has component proofs but no live inter-swarm collaboration run.
Preserve its publication, trust, transport, projection, Soul verification, Self
@ -241,6 +317,47 @@ admission, wake, brake, and Eve path until Gate 1 exercises the complete crossin
Streamlining may remove duplicate authors and process shells; it may not spend
the only end-to-end collaboration path before it has carried real traffic.
Exact `6f5d6600` reduces the immutable repository-domain receipt to the
canonical organizational repository name and exact authenticated Body hash.
Runtime/swarm/workspace belong to the Body route; envelope type/key/schema
belong to CultCache; binding time and fixed prose own nothing. The typed receipt
still prevents deployment input from relabeling the Body after admission.
Exact `952dcd9f` reduces operator-objective intake to the human assertion and
its actual provenance. CultCache owns envelope identity and schema; the
singleton Mind objective owns active state; the exact Mind commit receipt owns
atomic admission. One coordinator-binary test that only repeated the library
proof is gone, while byte-idempotency, replacement refusal, and resident-grant
provenance remain covered.
Exact `718ce9c1` applies the same ownership rule across the complete durable
repository-frontier chain. Thirteen Proposal, Planning, Research, PlanMind,
Hands, Verification, Soul, and Modeling documents no longer persist fixed
schema/version labels or contract slogans already owned by CultCache and the
runtime epoch. Their semantic identities, strong dependencies, exact source
versions, decision contexts, evidence, dispositions, and consequences remain.
Sealed worker contexts retain explicit schema and contract fields because they
are exact model input. Runtime advances to v33. Atlas is untouched and remains
protected until a real inter-swarm collaboration run exercises the complete
crossing.
Exact `a4356d1f` roots runtime schema authority in runtime identity plus swarm
binding and removes fixed schema-version echoes from thirteen child physiology
documents. Sessions, jobs, execution bindings, launches, process claims,
results, coordinator receipts, death recovery, and archives retain their exact
causal and lifecycle payloads. Runtime advances to v34; old-store refusal and
all affected consequence paths remain proven. No compatibility layer replaces
the deleted fields. Atlas and external crossing contracts are unchanged.
Exact `e60b924f` deletes the model runtime's callerless preflight JSON
self-certificate and registered-type catalog accessor. Exact `3b991e40`
collapses the packaged model runtime onto admitted `run-worker` plus
`list-decisions` and `audit-decision`. The three uncalled direct transport/tool
CLI paths, their parser/option types, and their self-contained ingress tests are
gone. Provider execution and tool continuation now have one production entry
path; transcript-free decision inspection remains. The two cuts remove 532 net
maintained lines without changing Mind/runtime schemas or Atlas.
Resident Self now solely imports authenticated Bifrost deliveries; exact
`3dda58a5` deletes the standalone Persona feedback ingress and its old Starfire
snapshot seam. Exact `56267201` deletes the three unadmitted Atlas daemon
@ -295,17 +412,135 @@ Cut in this order:
projection assembled from exact keyed Mind document versions. The final
model request must render that projection; citing keyed sources while
rendering the old thread snapshot is explicitly forbidden.
2. **Body admission landed at `2eb95df6`; current-work projection landed at `e42788c9`:** one pure typed projection now derives Body Modeling, Eyes continuation, proposal Modeling, frontier planning, and Hands readiness from keyed Mind/runtime receipts.
2. **Body admission landed at `2eb95df6`; current-work projection landed at `e42788c9`, consumes one exact snapshot at `7374be5e`, typed fulfillment reuses it at `5f1bea39`, coordinator scheduling becomes its sole consumer projection at `2bcbf268`, reorientation result review becomes a single-cache typed read at `125d77a2`, and terminal disposition stays in that projection at `dfe3757b`:** one pure typed projection derives Body Modeling, Eyes continuation, proposal Modeling, frontier planning, Hands readiness, accepted manual-regather obligation, and exact Reorientation success/failure disposition from one pulled keyed Mind/runtime cache. Immutable result replay, family admission, PlanMind continuation, worker archival, Resident Self completion, and operator status validate or route against that same projection rather than reopening runtime state or accepting a parallel CRRC action. Reorientation result review likewise reads its job and structured result from one runtime snapshot and exposes no synthetic backend lifecycle.
Modeling must derive from the current Body/RepoModel obligation; Eyes only
from explicit external-evidence obligations. Body-generation Modeling
identity, typed Body-to-Mind admission, sealed reasoning, and atomic decision
admission are landed. Current-work no longer reads the external Body store;
status consumes the shared projection.
status consumes the shared projection. Exact `0a97eef8` deletes the later
callerless Body-only/unresolved-Body reconstruction paths and three unused
per-job continuation wrappers, leaving `project_current_work` as the sole
slice assembler.
Exact `5a047944` also removes the callerless JSON ledger-status helper,
generic role lookup helpers, unique-string wrapper, and successful-receipt
wrapper. Live typed owners remain; no generic utility replacement is added.
Exact `6ccaf937` removes the one-field repository Body Mind-view alias, the
unconsumed sorted Persona-quarantine projection, and the unconsumed generic
repo-work planning grant. Typed Mind/quarantine owners and exact live
coordinator/worker grants remain.
Exact `f6a2ad7f` removes the callerless packaged-release-head and Idunn
trust-anchor file readers. Exact release authentication and typed signed
health admission remain with their live consumers.
Exact `be611f24` removes the producerless Imagination consideration-review
writer and durable contract together. Candidates remain proposal-only
outcomes; actual adopted proposal Modeling stays in its existing lifecycle.
Runtime writable epoch is v22.
Exact `3140d305` removes the producerless Resident Self runtime-receipt
document and the definition-only coordinator role-result enum. Resident Self
state v2 refuses v1 stores; actual terminal receipts and runtime retention
remain with their live owners.
Exact `0091e0e1` removes the unread generic/OpenAI adapter-status documents,
their per-pass writers and construction helpers, one self-affirming test,
two schemas, and both runtime registrations. Exact request/provider context
and live transport construction remain. Runtime writable epoch is v23.
Exact `2e3489c4` removes four uninhabited generic planning document families,
their empty reasoning-projection field, and the 256-line issue-tracker DTO
vocabulary beneath them. Actual frontier planning and adoption remain.
Mind/runtime writable epochs are v9/v24.
Exact `a6f73fc4` removes the write-only Persona quarantine-pressure document.
The terminal turn receipt and receipt-bound quarantined mentions remain the
single blocked-consequence authority.
Exact `fed4b857` removes the unread Continuity recovery-receipt companion and
its entire module. Exact reorientation result, typed Mind decision/failure,
sealed context, and commit receipt remain the sole recovery audit path.
Runtime writable epoch is v25.
Exact `a602fbdc` removes fifteen unconsumed state-model DTO types, two
callerless OpenAI-runtime mouths, and eight definition-only type constants.
Exact keyed Mind/runtime receipts, RepoModel documents, pass-local reasoning,
and the release-owned observed worker route remain. No schema epoch changes
because none of the deleted vocabulary was registered or persisted.
Exact `9abadfe7` removes the unconsumed provider-frame observation DTO and
callback APIs from both transports, plus the provider-specific transcript
reader used only by one test assertion. Typed stream events/receipts remain;
no audit or admission path depends on frame previews or transcript text.
Exact `2a435eb5` advances Persona memory to v2 and tool receipts to v1 while
deleting duplicate effect-event provenance, an always-empty relationship,
and an always-empty raw-result reference. Mind/runtime epochs are v10/v26;
old writable stores refuse without migration.
Exact `ffd91c20` deletes three more derived fields: the fixed Research role
from the then-live Eyes packets, the fixed Epiphany Persona agent from queued
mentions,
and the unread Resident Self retention timestamp. Their owning request/
context, Persona queue/turn request, and chained retention digest remain.
Persona mention v2, Resident Self state v3/retention head v1, and runtime v27
formed that hard cut; exact `83611f9b` later deletes the Eyes packet family.
Model Atlas remains intact for its first
live inter-swarm collaboration run; component proofs are not acceptance.
Exact `fe118e13` makes provider events and receipts transient transport
values and deletes their duplicate CultCache/CultNet authority. One native
model stream/receipt family now owns continuation, reconstruction, terminal
validation, and retention; the exact provider request remains durable audit
input. Runtime advances to v28, the catalog drops to 18 schemas, and the
model-runtime include shell is gone. Atlas remains unchanged and unaccepted.
Exact `dbed11b3` and `8bb0719b` delete the remaining hand-maintained schema
shadow registry. Runtime-local CultCache documents are discovered from the
native registration or projected by their owning provider; they are not
republished as stale JSON. The catalog now contains only the exact provider
request plus the portable Persona and work-organ contracts. The cut removes
850 net maintained lines. Atlas remains unchanged and unaccepted.
Exact `695af6c6` deletes thirteen unowned dependency declarations and the
final tracked marker for the extinct OpenAI auth-spine crate. The lock graph
contracts from 757 to 732 packages; all nine production entrypoints and the
affected library/test targets compile individually. Package-scoped cleanup
removes 124.2 GiB of reproducible artifacts while preserving the prebuilt
state tool. Atlas remains unchanged and unaccepted.
Exact `65bf044f` removes redundant schema/prose cargo from the three Hands
consequence receipts, fourteen unused shadow type constants, and one
callerless reorient display enum. CultCache owns document type/schema
identity; exact intent, review, grant, job, consequence, and immutable
receipt identity remain. Runtime advances to v29. The concurrent
Persona/Hands-to-Verification proof and old-epoch refusal pass; package
cleanup removes 678.6 MiB. Atlas remains unchanged and unaccepted.
Exact `d9a196a0` makes the Proposal-Modeling request the sole owner of
autonomous proposal origin. The companion binding document, schema,
registration, third CAS write, collision branch, and copied worker hashes
are gone; exact direction-result, option-ordinal, and worker-job identities
live on the request. Runtime advances to v30. The full keyed lifecycle and
old-store refusal pass; package cleanup removes 677.6 MiB. Atlas remains
unchanged and unaccepted.
Exact `fdefb889` deletes the generic session/job writers after proving every
caller was a fixture. Family launch CAS, model execution, and coordinator
opening are the only runtime-physiology creation owners. The cut removes 96
net maintained lines without a schema or Atlas change.
Exact `b3e9b229` deletes Persona's standalone delivery-evidence document and
the conversation receipt's copied evidence-ID vector. Bifrost owns the
durable signed crossing receipt; the Persona social terminal owns the local
delivered consequence and folds the exact message, crossing, and digest
identities into one receipt. Retention verifies that provider-owned receipt
directly. Runtime advances to v36 and both changed Persona documents make a
v2 hard cut. Atlas remains untouched and operationally unaccepted.
Exact `ce6bff12` reduces runtime execution bindings to their owned
request-or-intent to session/job edges. Provider, worker, reasoning-basis,
and model-request ancestry are loaded from the native request or typed tool
intent instead of copied into bindings; non-causal binding timestamps and
their API arguments are gone. Runtime advances to v37. The cut removes 67
net source lines. One focused OpenAI test generated 12.713 GiB across 19,134
files, so its provider/runtime dependency boundary is the next source-shape
audit. Atlas remains protected for its first live inter-swarm run.
Exact `a6cf9383` resolves that boundary: concrete Codex/OpenRouter auth,
credential reading, and transport move to one release-entrypoint source
module, while `epiphany-openai-runtime` retains pure exact-request opening,
event admission, tool intent emission, and durable audit with no Codex
dependency. Persona's concrete runner moves to the Persona service. The
duplicate no-tools worker route, DTOs, CLI mode, and coordinator switch are
deleted; one governed tool-capable path remains. Provider strict-shape tests
move to the release owner. The cut removes 176 net source lines with no new
target, process, schema, or epoch. The same runtime consequence proof now
uses 1.869 GiB/2,942 files instead of 12.713 GiB/19,134. The release edge
still reaches 10.473 GiB/16,483 files through `codex-login`, which is the
next provider-edge liability. Atlas remains untouched and unaccepted.
3. **Baseline transaction landed at `9f7b164f`; operator routing at `587c56d2`; Resident continuation and keyed acceptance at `478fb923`:** baseline Body Modeling is now thread-free from unresolved Body obligation through Launch/Wait/Review and exact Mind admission. Resident and operator callers consume the same family projection and acceptance owner. Thread ID remains immutable pass provenance only.
4. **Proposal Modeling landed at `d1b031cb`:** its immutable request, exact launch binding, runtime attempt/result, and `Modeling.proposal_frontier` commit now own Launch/Wait/Review and admission. The aggregate launcher refuses proposal cargo; the old selector, validator, context builder, and generic-launch hint are deleted. Body and Proposal share only the exact-envelope launch CAS invariant, not a mutable family registry.
Exact `6831eb69` later reduces that launch binding from twelve mirrored fields
to request ID, job ID, and exact launch digest under its schema; validators
fetch the deterministic job-derived key directly.
4. **Proposal Modeling landed at `d1b031cb`:** its immutable request, worker launch, runtime attempt/result, and `Modeling.proposal_frontier` commit now own Launch/Wait/Review and admission. The aggregate launcher refuses proposal cargo; the old selector, validator, context builder, and generic-launch hint are deleted. Body and Proposal share only the exact-envelope launch CAS invariant, not a mutable family registry. Exact `b0a4978d` deletes the later duplicate launch-binding document entirely; the worker launch is the sole launch owner.
5. **Continuation vocabulary landed at `e404c105` and was named truthfully at `fc5d1a4a`:** concrete pass families share one pure `EpiphanyAgentPassContinuationAction`; their semantic requests, projections, validators, and admission owners remain separate.
6. **Dormant claim-repair authority deleted at `20dd66c8`:** no request/binding/context schema, runtime carrier, coordinator branch, result cargo, export, or model lowering survives. The independent Eyes challenge fact remains. The same pass fixed two-snapshot Body/Proposal launch derivation and stress-proved single Proposal launch 20/20.
7. **Frontier-verdict Modeling landed at `d367e525`:** one exact Soul verdict
@ -318,7 +553,7 @@ Cut in this order:
8. **Frontier Research landed at `c7412998`:** the v3 request seals the exact
frontier/dependency document closure, while the full RepoModel projection is
audit cargo only. One deterministic attempt, terminal decision context,
request-bound Eyes packet, and `Eyes.frontier_research` Mind mutation own
structured Research result and `Eyes.frontier_research` Mind mutation own
Launch/Wait/Review/admission. Stale terminal decisions remain durable, exact
strong-state changes refuse admission byte-identically, and disjoint keyed
writes merge. Accepted Eyes, role lanes, timestamps, generic regather, and
@ -343,7 +578,10 @@ Cut in this order:
planning conflicts without turning unrelated graph writes into a global
head. Thread provenance, generic coordinator validators/constructors,
aggregate role lanes, and accepted-at/latest-result behavior no longer own
this family.
this family. Exact `b55e96ea` deletes both later duplicate launch-binding
document families. Immutable worker launches now own attempt identity and
current jobs; exact failed results plus typed failure reviews own retry
ancestry and are strong reads of the retry launch transaction.
11. **Reorientation landed at `d5df53ae`:** one keyed request seals the exact
continuity projection and source document versions; deterministic attempts
bind an exact reasoning basis and terminal decision context. The family
@ -352,7 +590,40 @@ Cut in this order:
coordinator routing use the same current-work projection. Aggregate
launch/acceptance, checkpoint/freshness recommendation, latest binding,
global revision acceptance, and accepted-at comparison no longer own this
family.
family. Exact `8dbfcf63` deletes its later duplicate launch-binding document;
the immutable worker launch owns request/job/attempt identity, while retry
CAS strongly reads the typed failure and exact terminal runtime result.
The same cut deletes the duplicate Body Modeling and Imagination
consideration binding families. Canonical job identities and immutable
family-referenced worker launches now own all three attempt sequences.
Their semantic requests, sealed projections, result admission, substrate
grants, typed failures, and refusal/retry invariants remain family-owned.
Runtime writable state advances to v19. Model Atlas and the unrun
inter-swarm collaboration path are unchanged.
Exact `3ed1d564` then deletes the standalone public writer for the derived
frontier-Modeling request. Accepted Verification is the sole writer and
atomically commits its audit, Soul verdict, and request. The deleted path had
no production caller and survived only because an OpenAI test fabricated a
parallel Verification/Soul tableau; that test now uses ordinary Body
Modeling while preserving provider, contract, structured-result, and job-
terminality proofs.
Exact `63d2991e` then reduces the coordinator terminal receipt to the
terminal Self decision and exact resident-launch provenance continuity
consumes. Operator artifact inventory, provider/store echoes, arbitrary
metadata, step count, and final-job echo stay in their existing owners or
disappear when unread. Receipt/schema version is v1 and runtime writable
state is v20; the published schema now describes the live resident fields.
Exact `196222d9` then makes runtime sessions/jobs minimal lifecycle records:
session metadata and job metadata, summary, and artifact refs disappear
because no reader consumed them and the terminal job result already owns
consequential summary/artifact truth. Launch options no longer accept those
echoes. Runtime writable state is v21 and the closed session/job catalog
schemas are v1. Model Atlas remains intact for the unrun live inter-swarm
Gate 1.
Exact `cd2177e8` then deletes the duplicate `ensure_runtime_session` writer.
Its two test consumers now use strict session creation; a reused active ID
can no longer substitute a different objective, creation time, or
coordinator note. No persisted shape changes and runtime remains v21.
12. **Landed at `1662a012`, `07d891ba`, and `ec1431ff`:** coordinator policy now
projects keyed current work; persisted `EpiphanyThreadStateEntry`,
`coordinator_state_transaction`, generic coordinator services, the
@ -587,6 +858,86 @@ Cut in this order:
run summary loses its fixed adapter/schema catalogue and caller-supplied
store echo. Exact authority remains the typed receipt; stdout returns only
intent ID, receipt ID, and terminal status.
47. **The sealed launch owns pass-family identity at `edb5c3a3`:** eight
optional request-ID mirrors are deleted from worker launch/runtime options,
and terminal role results lose the same family echoes plus copied Body basis
and frontier route. Live routing decodes the typed launch; retained history
uses archived request kind/ID beside the preserved structured decision.
Success requires the family semantic payload and typed failure forbids
success cargo. Runtime is v39 and role-result output is v5 with no legacy
reader.
48. **Coordinator incarnation and model-session authority split at
`cabdd6c3`:** model sessions retain only identity plus active/completed
admission state. Coordinator start provenance is an immutable typed basis;
one deterministic terminality identity admits either the exact full run
receipt or exact death recovery, so competing outcomes conflict while
unrelated document inserts still merge. Runtime is v40. The generic session
loses objective, timestamps, notes, and four unused states; the redundant
one-field open options and whole-snapshot append helper are deleted. The next
subtraction target is the generic runtime job aggregate, not a lifecycle
registry.
49. **Runtime jobs carry lifecycle, not chronology, at `a46725a1`:** every
production job is inserted queued and terminalizes completed or failed.
Unwritten running/review/cancelled states and creation/update clocks are
deleted. Research derives its exact attempt set, refuses multiple live or
successful authorities, and never selects work by timestamp order. Runtime
is v41; the cut removes 67 maintained lines net. The remaining audit target
is duplicated role/session identity and generic terminal-result cargo.
50. **Runtime job ownership is derived at `10a70afc`:** the generic job is only
job identity plus Queued/Completed/Failed status. Sealed launches own
outer-worker role, model bindings own model-session membership, and root
membership is structural. Jobs/results lose role/session mirrors, result
metadata disappears, launch options lose three unused fields, and duplicate
terminal results refuse instead of sorting by time. Runtime is v42; 73
maintained lines are removed net. Next audit generic result cargo by terminal
family so structured mirrors disappear without deleting the only failure
account.
51. **Generic job results do not own decision evidence at `53e869c0`:** evidence
and artifact mirrors are deleted from the result contract and completion
API. Typed role/reorientation outcomes remain their sole owners. The generic
result retains only terminal physiology and the terse failure/display account
needed where no structured outcome exists. Runtime is v43; 51 maintained
lines are removed net. Next audit remaining summary, next-move, time, and
context cargo by terminal family.
52. **Structured decisions have one durable owner at `8eaa96c5`:** completing a
typed role/reorientation outcome no longer persists a generic result copy.
The exact typed envelope is a byte-identical strong dependency in the atomic
job/process terminalization CAS. Fulfilled archives retain the context and
full typed role result only. Runtime is v44; archived attempts are v3. The
source delta is +7 lines for the CAS fence while one durable duplicate family
disappears. Next determine whether the residual failure/transport result
contract can be deleted entirely.
53. **Generic job results are deleted at `2c3335dc`:** typed role and
reorientation outcomes atomically close their own job/process physiology;
native model receipts or typed model-pass failures own model terminality;
exact process claims own process death. Reorientation failure admission and
decision audit bind the typed failure directly. One shared outcome rule
prevents typed failures from claiming successful process fulfillment, and
failed archives retain the full typed failure. Runtime/Mind advance to
v45/v11 with no compatibility reader. The cut removes 361 maintained lines
net.
54. **Model-session retirement is retained but stripped at `dbb00c2b`:** the
live session latch owns explicit closure between tool rounds; the private
archive owns retired identity collision refusal after bindings are deleted.
Six model-turn display echoes and the one-field closure wrapper are gone:
38 net lines. No registry or replacement aggregate is added.
55. **Verification has one exact Hands input at `24023265`:** its sealed typed
projection already carries the complete consequence chain. The live
`read_hands_receipt` tool, three runtime readers, exports, tool/prompt/gate
cargo, duplicate source-store argument, and advertisement assertion are
deleted: 78 net lines. The consequence receipts and atomic Hands-to-
Verification admission remain. Next map the missing production Hands
actuator as an ABI boundary and demote only speculative public mouths.
56. **Hands review authority is deleted at `a01c6842`:** the coordinator's
same-function always-approved `HandsActionReview` had no independent reviewer
or refusal producer. Adopted route, Substrate Gate grant, minimal intent,
and frontier authority now form the complete authorization chain. Patch,
command, and commit consequences cite the intent and derive the remaining
identities. Commit admission requires exactly one patch and one successful
command; ambiguous competing receipts refuse instead of being ordered by
timestamp. Runtime advances to v46 and the sealed Verification projection
to v2. The cut removes 381 maintained lines net; core 152/152, coordinator
4/4, and OpenAI runtime 21/21 pass.
Do not preserve the aggregate for compatibility, manufacture a bootstrap
thread, release autonomous scheduling, register topology in `gamecult-ops`,

View file

@ -0,0 +1,901 @@
# The body plane for oversize answers: cut map
Date: 2026-09-22. Imagination output (Opus). Nothing here is committed and no
repository was edited. Self places and commits this file; the natural homes
are a section of `F:\Projects\Epiphany\notes\eureka-pipeline-state-cut.md`
(the campaign that owns the ruling) with the CultLib half copied or linked
into `F:\Projects\CultLib\docs\`, because the cut spans the organ and
CultMesh.
Status: cut map for three cuts, **BP-1** (CultLib: the content plane in the
Rust runtime), **BP-2** (the pin move that lets Huginn see it) and **BP-3**
(Huginn: an oversize answer is deferred to the body plane).
**2026-09-29 (Self):** both questions in section 8 are ruled (Q-BP1 A,
Q-BP2 A, 2026-09-22). Selection Cut 1 merged to CultLib `main` at `dbee044`,
so section 6's collision is gone. **BP-1 is in Hands** (Sonnet) on CultLib
`cultmesh/rust-content-plane`, based on `268e0ef`, with two overrides to
section 3: no committed mutation entries (the operator retired hand-written
harnesses on 2026-09-22; each rule's test is shown red once by a scratch
mutant instead), and verification on Yggdrasil through the stopgap rather
than on the workstation. The Pins table below is as of 2026-09-22.
**BP-1 landed** (Sonnet, 2026-09-29) on `cultmesh/rust-content-plane` at
`29c3e4c`, `11485fb` and `8a66b1d`. `cultnet-rs` went from 264 to 279 tests,
plus 3 C# vector tests, and all 22 listed mutants went red. Size: +486 source
lines against ~330 estimated, and +778 test lines against ~350.
**Soul (Opus) will merge it after a fix batch, now in Hands:**
- F1: a flood of zero-size chunks makes the client send unbounded requests;
- F2: malformed requests are refused at decode, so a Rust server answers
nothing where the reference answers `found:false`. **This was the map's
defect**: section 3 ordered decode-time request validation, which
contradicts "a failure is an answer, never silence". Refusals belong to the
answer function;
- F3: `BTreeMap` metadata breaks byte identity for keys that are not sorted;
- F4: hashes are normalised only after requests have gone out;
- F5: one error spelling differs from the reference;
- F6: 8 surviving mutants.
Held: byte identity in both directions beyond the fixture, verification,
serving, validation order, and one path. The empty-body deviation (one
zero-length chunk) is the reference's behaviour, so the spec was wrong there too.
**The ruling this map is written under** (operator, 2026-09-17, recorded in
the Epiphany map at about line 368): an answer too large for the control
plane travels over CultMesh's content/body transfer. The control plane
carries a summary and a reference to the body. Raising the window
(1,228,800 bytes) and bounding documents smaller were both rejected. **The
typed oversize refusal stays as the backstop.** The ruling says the cut "spans
the organ and CultMesh", and nothing about it was in Cut 10 except the refusal.
## Pins
| Repo | Branch | HEAD | State |
|---|---|---|---|
| CultLib | `main` (checked out; the operator's shell is on `main`, and the gitStatus snapshot's `cultnet/selection-cut1` is the locked worktree `.claude/worktrees/agent-af580008561975a4c`, also at `b3d9cf7`) | `b3d9cf7` | Dirty in `native/GameCult.Mesh.Quic.Native/*` and `scripts/mutate-cultmesh.mjs` from the QUIC campaign. **Not touched.** `git diff --stat a0813c6 b3d9cf7 -- packages/cultcache-rs packages/cultnet-rs packages/cultmesh-rs` is **empty**, so every Rust anchor below holds at Huginn's pin as well. |
| CultLib selection Cut 1 | `cultnet/selection-cut1` | `b3d9cf7` + uncommitted work in the locked worktree | In Hands (another agent). The collision is in section 6. |
| Huginn | `eureka/memory-organ` | `202e5e3` | Clean. Cut 10 and its two fix batches have landed. |
| Epiphany | `codex/eureka-pipeline-state` | `2876a827` | Map read only. The leaf `epiphany-pipeline` is pinned by Huginn at `5cda0886` and pins `cultcache-rs` at `a0813c6`. |
## 0b. Model page: identity, lifecycle and authority
This table was written before any cut was mapped. Every persistent or
long-lived kind the cut touches has a row, and no cell is empty.
| Kind | Identity (what names it) | Lifecycle (what happens to it over time) | Authority (who decides) |
|---|---|---|---|
| **Deferred body**: the encoded bytes of one `HuginnMindResponse` that did not fit one send | The SHA-256 of those bytes, lowercase hex: the manifest's `contentHash`. Two identical answers are one body. | Created when an answer's encoded envelope exceeds the window and the body is within the deferral bound. A second deferral of the same bytes refreshes it; it is never duplicated. Every chunk served also refreshes it (sliding expiry). It expires when it has been untouched for the TTL, and it is evicted least-recently-touched first when the retained total exceeds the budget. It is lost when the process exits, and the client asks again. It is never written to the mind, a file or any `.cc` store. | The daemon's serve module (`huginn-daemon`) decides to defer and holds the retention store. The mind decides what the answer *is* and knows nothing about deferral. |
| **Chunk** | The SHA-256 of the chunk's bytes. Its record key is `mesh:cdn:chunk:<hash>`, which is the C# reference's spelling (`CultMeshCdnArtifactChunk.CreateRecordKey`, `CultMeshCdn.cs:120`). | It lives exactly as long as some retained body references it. A chunk shared by two bodies is stored once and stays until both are gone. | Chunking (the boundaries and the hash) belongs to `cultnet-rs` `pack_content`. Retention belongs to the daemon's store. |
| **Reference (manifest)** | `gamecult.mesh.cdn_artifact_manifest.v1`, the reference's type, reused and not reinvented. `contentHash` identifies the body; `artifactId` and the other fields describe it and never identify it. | Transient: minted with the body, carried once inside the deferred response, and never stored by the daemon or the client. It stays valid while the body is retained. After that a chunk request answers `found: false` and the client re-asks the operation. | `cultnet-rs` owns the shape and the validation rules, with parity to `CultMeshCdn.ValidateManifestShape` (`CultMeshCdn.cs:499-526`). The daemon fills it in. |
| **Deferred response**: the control-plane answer that stands in for an oversize one | Correlated by the operation envelope's `messageId`, exactly like every other answer. | One per request, sent once, and never deferred itself. It always fits: at the deferral bound it is about 41 KB, far under the window. | Only the daemon's serve module produces it. `Mind` never does. |
| **Content chunk wire messages** `cultmesh.content_chunk_request.v1` / `.response.v1` | Their `schemaVersion` strings, both already defined by the reference (`CultNetSchemaMessages.cs:153,157`, classes `:1414-1446`). | Stateless: one request, one response. | The C# reference owns the spelling. `cultnet-rs` gains it with byte identity, pinned by vectors in both directions. |
| **Vector fixtures** `contracts/cultmesh/content-vectors.cs-written.json` and `.rs-written.json` | File path plus the `label` of each vector. | Committed. Each writer regenerates its own file under `CULTNET_WRITE_VECTORS=1`. The file is expected to grow and is not pinned as final. | Each runtime owns its written file. Both runtimes' tests read both files. |
| **Mind documents and receipts** | Unchanged: leaf keys and receipt ids. | Unchanged. **No body-plane state enters the mind.** | Unchanged: `huginn-mind` admission. |
Forks this page surfaced, which are sections 8's Q-BP1 and Q-BP2:
- **Authority over the physical path.** CultMesh's own doctrine forbids
schema messages carrying bulk bodies and names the RUDP content path as
legacy. That is Q-BP1.
- **What the summary is.** The ruling says "a summary". Whether that means the
reference's own facts or a per-operation typed summary decides whether
`huginn-mind` grows new types. That is Q-BP2.
**Identity cannot be the request.** Re-deriving a body per chunk request from
the mind would need no retention at all. But a chunk request names only a
hash, so the daemon could not know which answer to recompute without a
Huginn-specific request shape, and that would give up wire reuse with the
reference. Rejected for that reason, not asked.
## 1. Body facts
Each fact is backed by a source read at the pins above or by a probe that
ran code. The probes are in the scratchpad and nothing was committed:
- `bodyplane-cs/`: a net10.0 console against the built reference DLLs
`bin/GameCult.{Networking,Mesh}/Debug/netstandard2.1/*.dll`, dated
2026-09-16. The sources it depends on last moved at `f622ee1` (2026-08-18),
so the DLLs match `b3d9cf7`. It emitted real reference bytes to
`bodyplane-cs/out/`.
- `bodyplane-rs/`: a crate against `packages/cultnet-rs` by path, release
build. `src/main.rs` is the transport probe and `src/bin/parity.rs` the
byte-parity probe.
- `pinprobe/`: `cargo tree -d` over mixed CultLib git revisions.
### F1. Only the C# reference implements body or content transfer. The Rust client Huginn uses has no path today.
- **Source.** Across `packages/` there is no occurrence of
`content_chunk_request`, `body_read_request`, `cdn_artifact_manifest`,
`ContentChunk*` or `BodyRead*`, and no `cdn`, `chunkHash` or
`artifact_manifest` in any `.rs`, `.ts`, `.py` or `.kt` source.
`cultnet-rs` does not contain the word "body". What the other runtimes have
is the *local* stream-body negotiation enum (`SharedMemory`,
`CultCachePage`, `InlineBytes`, and so on): `cultmesh-rs/src/lib.rs:191-200`,
`cultnet-py/.../cultmesh_contracts.py:434`, and in `cultmesh-ts` and
`cultmesh-kotlin`. That is same-machine body mapping, not network transfer.
- **Probe A (decode).** `cultnet_rs::decode_cultnet_message_from_slice` over
bytes the C# reference emitted:
- `content_chunk_request` gives *unknown variant
`cultmesh.content_chunk_request.v1`*, because the closed enum
(`contracts.rs:252-392`) lists 20 `cultnet.*` variants.
- `content_chunk_response`, `body_read_response` and a real 256 KiB chunk
response give *invalid type: byte array, expected any valid JSON value*.
The non-raw decode path converts through `serde_json::Value`
(`contracts.rs:404-411`), which cannot hold MessagePack `bin`. So adding
the variants alone is not enough: both must ride the raw path that
`document_put_raw` uses (`contracts.rs:771-854`).
- **Probe E (on the wire).** A Rust hub sent the reference's own 262,357-byte
chunk response raw on the `schema` channel. The transport delivered it, and
the Rust client refused it in `receive_schema_message_once` with the same
byte-array error.
**Conclusion:** no Rust runtime can request, answer, decode or verify a body
or a content chunk today. Rust gets its first content-plane code in BP-1.
### F2. The reference has two planes, and only one of them fits this class of content.
- **The content plane.** It carries large immutable artifacts
(`src/GameCult.Mesh/docs/content-sessions.md:1-3`). The reference is a
`CultMeshCdnArtifactManifest` (`CultMeshCdn.cs:127-190`) listing
content-addressed chunks (default 4 MiB, `CultMeshCdnPackOptions`
`:44`). The chunk protocol is one bounded response per chunk
(`content-sessions.md:44-46`). The transfer owner
(`CultMeshContentTransferService`, `CultMeshContentTransfer.cs:115`) is the
only owner of chunk verification, whole-body SHA-256 verification, resume
and promotion. The preferred connector is TCP: a typed header, then raw
bytes outside the envelope (`transport-planes.md:22-26`). It is plaintext
and **restricted to explicit loopback development**
(`transport-planes.md:103-106`). The RUDP content server
(`CultMeshLegacyRudpContentServer`, `CultMeshContentSessions.cs:14-87`) is
"basement priority and exists for compatibility and parity measurement"
(`transport-planes.md:93-96`), and its connector is never installed by
default (`CultMeshContentSessions.cs:97-116`, priority 10,000).
- **The body plane proper.** It carries live, lease-bound generations of
mutable bodies: `CultMeshNetworkBodyStore` (`CultMeshBodySessions.cs:19-219`,
a 16 MiB default bound, 8 retained generations, HMAC capability tokens,
expiry by lease). It is served **whole in one response**
(`CultMeshBodyServer.HandleAsync`, `:241-272`, payload inline). There is no
chunking.
- **Why that decides it.** A pipeline answer is an immutable artifact, which
is the content plane's stated class. The body plane's single response meets
exactly the wall the ruling is routing around: probe B's whole-body send
failed with `RUDP reliable send queue is full`. The operator's phrase
"content/body transfer" covers both planes, and the source says which one
this is. **This map uses the content plane's chunk protocol and manifest.**
It is recorded as a reading of the source, not asked, because the body
plane cannot carry this answer on any transport Huginn has.
### F3. What the wire carries. Decoded from real bytes and pinned at byte level.
| Message | Reference encoding (probe, hex prefix) | Fields in order |
|---|---|---|
| `cultmesh.content_chunk_request.v1` | named map `0x85`, 169 bytes for the sample | `schemaVersion`, `messageId`, `chunkHash` (64 hex), `recordKey` (may be empty), `expectedSizeBytes` (int32) |
| `cultmesh.content_chunk_response.v1` | named map `0x87`, 179 bytes for a 4-byte payload | `schemaVersion`, `messageId`, `found`, `chunkHash`, `sizeBytes` (int32), `payload` (**MessagePack `bin`**), `error` (string, empty on success) |
| `gamecult.mesh.cdn_artifact_manifest.v1` | **array** `0x9A` (10 integer-keyed fields), 1,097 bytes for 6 chunks | `[artifactId, kind, version, contentHash, sizeBytes(int64), mimeType, createdAtUtc, chunks[[chunkHash, offset(int64), sizeBytes(int32), recordKey]], tags[], metadata{}]` |
| `cultmesh.body_read_response.v1` (not used) | named map `0x8B`, 11 fields | whole generation in `payload` |
Measured:
- A 256 KiB chunk response encodes to **262,357 bytes**, 213 bytes of
overhead, so four fit in Huginn's 1,228,800-byte window.
- The manifest for the real 1,315,551-byte case at 256 KiB chunks is 6 refs
and 1,097 bytes.
- **Probe parity:**
- A Rust `rmpv` map written in the reference's key order is **byte-identical**
to the reference's request (169 = 169) and response (179 = 179).
- A Rust tuple-shaped manifest decodes the reference's bytes and re-encodes
them **byte-identically**.
- A Rust re-pack of the same body with SHA-256 at 256 KiB reproduces every
chunk reference (hash, offset, size, record key) and the content hash.
- `rmp_serde::to_vec_named`, the encoder Huginn uses for responses, still
writes a tuple-shaped type as an array (`0x9A`). So the reference's array
form survives inside Huginn's named-map payload **only if** the Rust type
serialises as a tuple.
### F4. Chunked delivery works on Huginn's exact transport. Pipelining past the window does not.
Probes B, C and D used a hub configured exactly as Huginn's `serve::bind`
(`serve.rs:135-143`: fragment 1200, pending 1024, one settled session) and the
real 1,315,551-byte body, with chunks carried as base64 inside operation
responses, the only bytes-bearing message Rust has today.
- **One reply carrying the whole body** (a 1,754,254-byte envelope):
`RUDP reliable send queue is full`, returned to the sender only.
- **Sequential request, then reply, one chunk at a time** at 256, 512 and 768
KiB: delivered whole and SHA-equal, in 1.56 s, 1.54 s and 1.45 s on loopback
(about 0.85 MB/s, paced by the 32-packet send window,
`rudp.rs:34`). **Chunk size barely moves throughput.**
- **900 KiB** as base64 is a 1,228,983-byte envelope, which exceeds the window,
so the send fails.
- **Six 256 KiB chunks sent without waiting:** four accepted, two refused
immediately, and all four accepted ones were delivered. This is Cut 10's F2
(pipelined reads) reproduced at chunk scale. The window is per session and
counts unacknowledged packets.
**Consequence for the design:** the fetch is sequential, one chunk in
flight, at 256 KiB. With `bin` payloads, as opposed to this probe's base64,
one chunk reply is 262,357 bytes, which leaves room for a control-plane
answer on the same session.
### F5. Moving one CultLib pin duplicates `cultcache-rs` even when its content is identical.
`pinprobe` (`cultcache-rs` at `a0813c6` beside `cultnet-rs` at `b3d9cf7`,
whose `cultcache-rs` content is byte-identical) reports **two**
`cultcache-rs v0.2.0` and two `cultcache-rs-derive` in `cargo tree -d`,
because Cargo keys a git source by revision.
Huginn has three pins at `a0813c6`: `huginn-mind`'s `cultcache-rs`,
`huginn-daemon`'s `cultnet-rs`, and the Epiphany leaf `epiphany-pipeline`'s
`cultcache-rs` (at leaf `5cda0886`). All three must move together, or "one
crate pins one revision" breaks (see `huginn-daemon/Cargo.toml` and the Cut 8
build check `cargo tree -p huginn-mind -e normal -d` with zero duplicates).
That is BP-2.
### F6. Huginn today.
The anchors in `huginn-daemon/src/serve.rs` at `202e5e3`:
- The module doc's stated limit is at `:10-17`, and the pipelined-read
paragraphs are at `:32-44`.
- `MAX_FRAGMENT_BYTES`, `MAX_PENDING_RELIABLE_PACKETS` and `MAX_RESPONSE_BYTES`
are at `:124-131`, and `bind` is at `:135-143`.
- `answer` (`:165-194`) routes `OperationRequest` and `SchemaCatalogRequest`,
and answers every other family with `Error` (`:188-192`).
- **`within_window` (`:217-251`) is the single owner of the size decision
today.** It encodes the envelope, admits it at `<= MAX_RESPONSE_BYTES`, and
otherwise answers `Refused(ResponseTooLarge { bytes, limit })`.
- `run` (`:257-305`) reads the clock once per frame, at `:294`.
Elsewhere in the organ:
- `MindRefusal::ResponseTooLarge` is at `huginn-mind/src/refusal.rs:57-65`, and
its doc names the daemon as owner.
- `HuginnMindResponse` is at `huginn-mind/src/wire.rs:81-90`, and `status` is
at `:97-102`.
- The published schema is `schemas/cultnet/huginn.mind_response.v1.schema.json`,
pinned byte for byte by `published_wire_schemas_match_derivation`
(`wire.rs:210-216`).
- The tests that pin the gate are
`an_answer_too_large_for_one_send_is_a_typed_refusal_that_reaches_the_client`
(`serve.rs:548-656`) and
`the_gates_boundary_is_the_transports_boundary_byte_for_byte`
(`serve.rs:749-777`).
- The mutation entries D20, D20L through D20L5, D21 and D21L are in
`tools/eureka-cut10-mutations.psd1:250-330`.
- `huginn-mind` has **no** `cultnet-rs` dependency today. The selection
consumer cut adds one, because `Query { instance, selection }` carries the
substrate's type (`docs/cultnet-selection-cut.md` §12).
- `eureka-state/src/lib.rs` is empty. Cut 13 has not been built.
## 2. The cut order
```
[selection Cut 1, Rust commit 3 lands on CultLib main] (in Hands now, not this map)
│
▼
BP-1 CultLib the content plane in cultnet-rs (wire, manifest, pack, answer, verified fetch) + vectors both ways
│
▼
BP-2 Epiphany leaf + Huginn one pin move to a CultLib rev carrying selection Cut 1 and BP-1
│
├──► [Huginn selection consumer cut] (Epiphany map, unnumbered, not this map)
▼
BP-3 Huginn an oversize answer is deferred to the body plane; the refusal becomes the backstop
│
▼
Cut 11 / Cut 13 (Cut 13's spec gains: resolve a Deferred answer)
```
BP-3 comes **after** the Huginn selection consumer cut by default, for two
reasons. That cut already adds `cultnet-rs` to `huginn-mind`, which BP-3's
response variant needs. It also deletes `open_items` and `history`, so BP-3
tests fewer read operations. If the operator orders BP-3 first, BP-3 adds
that dependency itself (section 7, BP-3 "Adds") and nothing else changes.
## 3. Cut BP-1. The content plane in `cultnet-rs`
- **Repo and branch:** CultLib, `cultmesh/rust-content-plane`, from `main`
**after selection Cut 1's Rust commit (its commit 3) has landed on `main`**.
It depends on nothing else. See section 6 for why it cannot branch now.
- **First:**
- Record `packages/cultnet-rs` baseline tests (51 in `tests/cultnet.rs`
and 7 in `tests/rudp_server_hub.rs` at `b3d9cf7`; recount at the actual
base).
- Record the target-dir path count and size.
- Capture the reference's bytes for every vector with the C# writer
before writing any Rust.
- **Deletes first:** none. This cut fills an absence. No Rust code carries a
body today, so nothing is stood beside, and there is no compensator to cut.
This is stated rather than padded.
- **Keeps (untouched):**
- Every C# source file. The reference does not change: its chunk classes,
serializer arms (`CultNetSchemaMessageSerialization.cs:79-80`), CDN pack
and validation, and servers stay as they are.
- `CultNetDatabaseSubscribeMessage` and its `bodyIds`/`supportedBodyTransports`.
- `CultMeshBodyDemand.cs` and `CultMesh.cs`.
- `contracts/cultnet/*.schema.json` and `CultNetSchemaRegistry.cs`.
- `cultmesh-rs` (Huginn does not depend on it, and this cut does not make it).
- `rudp.rs` (the transport carries these as ordinary schema frames).
- **Adds:**
| Add | Owner | Live consumer | Protected invariant | Why an existing owner cannot serve |
|---|---|---|---|---|
| `contracts.rs`: `CultNetMessage::ContentChunkRequest { message_id, chunk_hash, record_key, expected_size_bytes: i32 }` and `ContentChunkResponse { message_id, found, chunk_hash, size_bytes: i32, payload: Vec<u8>, error }`, renamed to the reference's `schemaVersion`s, **both on the raw path** | `cultnet-rs` | Huginn's daemon (BP-3), `fetch_content` | **byte identity with the reference in both directions** | The closed enum cannot carry them (F1). The JSON-value path cannot carry `bin` (F1). |
| `src/content.rs`: `CultMeshCdnArtifactManifest` and `CultMeshCdnChunkRef` (the reference's names; named Rust fields; serde `into`/`from` a private tuple mirror, so every encoder writes the reference's array, F3) | `cultnet-rs` | BP-3's deferred response, the fetch | one reference vocabulary across runtimes; the array form survives `to_vec_named` | A Huginn-local reference type would be a second vocabulary for one thing |
| `normalize_hash` (mirrors `CultMeshCdn.NormalizeHash`, `CultMeshCdn.cs:549-560`: an optional case-insensitive `sha256:` prefix is stripped, then trimmed, then lowercased; empty is refused) | `cultnet-rs` | answer, fetch | one hash spelling | — |
| `pack_content(artifact_id, kind, version, mime_type, created_at_utc, bytes, chunk_size) -> (CultMeshCdnArtifactManifest, Vec<CultMeshCdnChunk>)` (mirrors `CultMeshCdn.PackArtifact`, `:232-310`) | `cultnet-rs` | BP-3's store | **the same bytes and options produce the reference's manifest byte for byte** | — |
| `validate_manifest` (mirrors `ValidateManifestShape`, `:499-526`, **including its sort by offset before the contiguity check**) and a caller-supplied `max_bytes` refused **before any fetch** | `cultnet-rs` | `fetch_content` | a hostile or corrupt manifest never makes a client allocate or ask | — |
| `answer_content_chunk_request(request, lookup: impl Fn(&str) -> Option<&[u8]>) -> CultNetMessage` (mirrors `CultMeshLegacyRudpContentServer.HandleAsync`, `CultMeshContentSessions.cs:43-86`, **in its validation order**: message id, hash normalisation, record-key agreement with `mesh:cdn:chunk:<hash>`, lookup, size and hash of what is served; a failure is `found: false`, an empty payload, and `error` = `<ExceptionKind>: <message>` in the reference's spelling) | `cultnet-rs` | BP-3's `serve::answer` | a server never serves bytes whose hash is not the requested one, and never answers a failure as silence | Rust has no `ICultNetSchemaServer`, so a pure function is the whole server |
| `fetch_content(manifest, max_bytes, ask: impl FnMut(CultNetMessage) -> Result<CultNetMessage>) -> Result<Vec<u8>>`: sequential, one chunk in flight, in manifest-offset order; verifies each response's correlation id, `found`, size and SHA-256 against the reference, then the whole body's size and SHA-256 against `contentHash` | `cultnet-rs` | Huginn's tests now, `eureka-state` (Cut 13) later | **one owner of verification on the receiving side**, as `CultMeshContentTransferService` is in the reference; no caller re-derives a check | The reference's transfer service is durable and file-promoting, and a C# owner does not serve Rust. This is its in-memory subset, with no resume, no failover and no files |
| `lib.rs:21-39`: `mod content; pub use content::*;` | `cultnet-rs` | — | — | — |
| `tests/content.rs` (new) | `cultnet-rs` | CI | the rules below | — |
| `contracts/cultmesh/content-vectors.cs-written.json` and `content-vectors.rs-written.json`: request, response found, response not found, and manifest vectors. A **deterministic** body (`byte[i] = i % 251`, 1,315,551 bytes, the real oversize case) at 256 KiB. **The fixture must carry:** a hash given with an uppercase `SHA256:` prefix; a manifest whose chunk list is **out of offset order**, which the reference accepts; a final chunk shorter than the rest; a zero-length body (0 chunks); and a chunk response whose `chunkHash` differs from its payload's hash in the **last** hex digit only. Otherwise a loosening that compares prefixes cannot fail. | each runtime writes its own | `tests/content.rs`, the C# test below | **vectors the reference writes decode and re-encode identically in Rust, and the other way round** | — |
| `tests/GameCult.Mesh.Tests/CultMeshContentVectorTests.cs` (**a new file**, not `NetworkingTests.cs`, which the selection cut is rewriting): writes the cs vectors under `CULTNET_WRITE_VECTORS=1`; asserts the committed cs vectors equal a fresh encode; decodes the rs-written vectors, asserts field equality and byte-identical re-encode; asserts `CultMeshCdn.PackArtifact` of the fixture body equals the fixture manifest | C# tests | CI | parity from the reference's side | — |
| Docs: `src/GameCult.Mesh/docs/transport-planes.md` cut line (`:93-101`) and `content-sessions.md` gain one paragraph each naming the Rust runtime's content path and its transport (the wording depends on **Q-BP1**); `docs/runtime-parity-scope.md` gets one row | docs | — | describe the live system | — |
No new dependency: `sha2`, `serde_bytes` and `rmpv` are already in
`cultnet-rs/Cargo.toml:14-31`. No new package, binary, transport, store
format or JSON schema file (see section 5).
- **Per-file changes** in `packages/cultnet-rs/src/contracts.rs` at `b3d9cf7`.
Re-anchor at the base, because selection Cut 1 moves this file.
- **`:252-392`**, the enum: two variants after `OperationResponse`.
`payload` is `#[serde(with = "serde_bytes")]`, although the raw path
never serialises it through serde.
- **`:398-418`**, `parse_cultnet_message`: both `schemaVersion`s join the
raw arm at `:405`.
- **`:420-436`**, `encode_cultnet_message_for_wire`: both join the raw arm
at `:427`.
- **`:458-643`**, `validate_message`: two arms.
- Request: `messageId` non-empty; `chunkHash` non-empty after
normalisation; `expectedSizeBytes >= 0`.
- Response: `messageId` non-empty. `found` implies
`payload.len() == sizeBytes` and an empty `error`. Not `found` implies
an empty payload.
- These are the reference's own invariants, as its constructors and
`HandleAsync` produce them. They are not new policy.
- **`:771-810`**, `parse_raw_cultnet_schema_message`: two arms, reading
`bin` via `as_slice` and ints via `as_i64` range-checked to `i32`.
- **`:812-854`**, `encode_raw_cultnet_schema_message`: two arms writing keys
**in the reference's declaration order** (F3), with the payload as
`rmpv::Value::Binary`.
- **`:964-1138`**, the `gamecult.networking.v0` path: unchanged. Its `_`
arms already refuse. A test pins that refusal.
- **Authority map:**
- **Owner:** `cultnet-rs::content` owns chunking, hashing, manifest shape,
manifest validation, answering a chunk request and verifying a fetched
body, for every Rust runtime. `contracts.rs` owns the two messages'
spelling.
- **Inputs:** bytes plus pack options (packing); a chunk request plus a
lookup (answering); a manifest, a size cap and a transport closure
(fetching).
- **Outputs:** a manifest and chunks; a chunk response message; verified
bytes or a typed error.
- **Derived state:** `recordKey` is derived from the hash and is never an
independent identity. `sizeBytes` and `contentHash` are derived from the
bytes.
- **Forbidden writers:** no consumer (Huginn, `eureka-state`) hashes,
chunks, validates a manifest or checks a chunk itself. No consumer
re-orders or retries chunks behind `fetch_content`.
- **Shared paths:** the daemon's answer and the client's fetch both call
`normalize_hash`. `pack_content` and `validate_manifest` share the one
contiguity rule.
- **Deletion line:** none. This is the first owner.
- **Verification:**
- **Builds** (PowerShell, `$env:CARGO_TARGET_DIR='C:\Users\Meta\.cargo-target-codex'`):
`cargo test -p cultnet-rs --tests` from `packages/cultnet-rs`; then
`dotnet test tests/GameCult.Mesh.Tests --filter CultMeshContentVectorTests`.
- **Tests, each with the rule it pins and the mutants that must kill it**
(committed in `scripts/mutate-cultmesh.mjs`, CultLib's own runner, which
owes the Eureka harness contract by name, **once the QUIC campaign has
released that file**; see section 6):
- `reference_vectors_encode_byte_identically` pins byte identity of the
two messages and the manifest.
Mutants:
- swap two keys in `encode_raw`'s request arm (revert-shaped);
- write `payload` as a `str` or an array of ints (loosening);
- encode the manifest through a named-field struct, i.e. drop the tuple
mirror (loosening, killed because `to_vec_named` then writes a map).
- `rust_vectors_decode_in_the_reference` (C#) pins the direction the
reference reads.
Mutant: Rust writes `sizeBytes` as a `u64`, which MessagePack-CSharp
rejects into `int`.
- `a_chunk_response_carries_bin_through_the_raw_path` pins F1's failure
mode.
Mutant: remove the response from the raw arm at `:405`. It must fail
with the byte-array error.
- `normalize_hash_matches_the_reference` pins the reference's
normalisation.
Mutants:
- case-sensitive prefix strip;
- no lowercasing;
- no trim (each killed by the fixture's `SHA256:` uppercase vector).
- `a_manifest_out_of_offset_order_is_accepted_as_the_reference_accepts_it`
pins parity of the sort.
Mutant: drop the sort. It must fail, because the fixture's chunk list
is shuffled.
- `a_manifest_over_the_cap_is_refused_before_any_request` pins that the
cap precedes fetching.
Mutants:
- check the cap after the first chunk (the test counts closure calls,
expecting 0);
- compare `>` instead of `>=` at the boundary.
The cap's boundary vector is exactly `max_bytes` accepted and one byte
over refused. **At least one mutant must be a function of the input**:
`max_bytes * 2`. The probe sizes sit where doubling would admit them.
- `a_chunk_whose_hash_differs_in_the_last_digit_is_refused` pins whole-hash
comparison.
Mutants:
- compare the first 63 characters;
- compare lengths;
- compare prefixes.
All are killed only because the fixture pair differs in the last digit.
- `a_body_whose_chunks_verify_but_whole_hash_does_not_is_refused` pins the
whole-body check.
Mutant: skip the final `contentHash` comparison. The fixture substitutes
a manifest whose chunk refs are valid but whose `contentHash` is another
body's.
- `a_response_for_another_message_id_is_refused` pins correlation.
Mutant: skip the id check.
- `answer_refuses_a_record_key_that_disagrees_with_its_hash` pins parity
with `HandleAsync`'s third check.
Mutant: delete the record-key check.
- `answer_serves_found_false_with_the_reference_error_spelling` pins that a
failure is an answer.
Mutant: return `Error` instead.
- `gamecult_networking_contract_refuses_content_messages` pins that the
legacy contract does not grow them.
Mutant: add them to its encoder.
- A **no-op control** rewrites each target through the runner's I/O path
and must leave everything green.
- **Negative checks:**
- `git diff --stat main -- src/GameCult.Networking src/GameCult.Mesh/*.cs contracts/cultnet packages/cultmesh-rs packages/cultnet-rs/src/rudp.rs`
is empty.
- `rg -n "untagged" packages/cultnet-rs/src/content.rs` is empty.
- **Operator:** nothing to click. This cut's evidence is the two vector files.
- **Subtraction estimate:** removes 0 lines. Adds about 330: `content.rs`
about 190, `contracts.rs` about 90, `lib.rs` 2, and docs about 20. Tests add
about 260 Rust and 90 C#, plus two vector files. There are no dependencies,
targets or formats to remove or add. **The delta is net positive because it
buys an explicitly ruled capability with the smallest surface that carries
it: the reference's own wire and reference type, not a new one.**
- **Build budget:**
- Host and target are both the Windows workstation, debug.
- Rust: `cultnet-rs` library and tests only, in the warm codex target dir.
The probe's release build of the same crate by path added 945 files and
0.32 GiB (12.31 to 12.63 GiB) cold. The expected debug delta is up to
+1,000 paths and about +0.4 GiB. Nothing is cleaned.
- C#: `GameCult.Mesh.Tests`, which rebuilds Networking, Mesh and Caching as
dependencies with no source change in them.
- No native build, no TypeScript, no Python, no workspace-wide build.
- C: had 258 GB free.
- The Linux target (Yggdrasil) is not exercised here. Nothing in this cut
is platform-specific, and Cut 14 builds on Yggdrasil.
## 4. Cut BP-2. One pin move
- **Repo and branch:** Epiphany `codex/eureka-pipeline-state` and Huginn
`eureka/memory-organ`. It depends on BP-1 **and** selection Cut 1 being on
CultLib `main`, pushed.
- **Why it is its own cut:** F5. Moving `cultnet-rs` alone duplicates
`cultcache-rs`. The selection consumer cut needs the same move, so **one**
move to one revision carrying both CultLib changes serves both Huginn cuts.
That saves a second leaf release and a second Huginn bump. Self schedules
it once, whichever Huginn cut runs first.
- **Changes:**
- Epiphany `epiphany-pipeline/Cargo.toml` `cultcache-rs` rev: from
`a0813c6…` to the new CultLib SHA. This is one commit. The leaf's own
tests must stay green. `cultcache-rs` content is unchanged across
`a0813c6..b3d9cf7`, so any change after that is itself a finding.
- Huginn: `crates/huginn-mind/Cargo.toml` `cultcache-rs` rev and
`epiphany-pipeline` rev to the new leaf SHA, and
`crates/huginn-daemon/Cargo.toml` `cultnet-rs` rev. This is one commit.
- **Verification:**
- `cargo tree -p huginn-daemon -e normal -d` shows zero `cultcache-rs` or
`cultnet-rs` duplicates.
- The full Huginn suite is green: 74 tests at `202e5e3`.
- The cut8, cut9 and cut10 mutation suites rerun clean.
- `rg "a0813c6" F:\Projects\Huginn\crates F:\Projects\Epiphany\epiphany-pipeline`
is empty.
- **Subtraction:** 0 or 0. Only four revision strings change.
- **Build budget:** Huginn workspace check and tests, and `epiphany-pipeline`
library tests. Each is a cold compile of three CultLib crates at a new
revision, about +300 to +600 paths. The same host as before.
## 5. Cut BP-3. Huginn defers an oversize answer to the body plane
- **Repo and branch:** Huginn `eureka/memory-organ`. It depends on BP-2 and,
by default, on the Huginn selection consumer cut (section 2). It needs
**Q-BP2** answered, and **Q-BP1** decides one paragraph of its module
documentation.
- **First:** rerun
`an_answer_too_large_for_one_send_is_a_typed_refusal_that_reaches_the_client`
and record the two sizes it prints. At `202e5e3` they are 1,315,551 bytes
for the wide view and 1,189,495 for the fitting one. They are the
before-and-after witness.
- **Deletes first:**
- `serve.rs:217-251` `within_window` is **deleted whole**. It is the one
owner of the size decision, and it is replaced by `deliver` (below), not
wrapped. A gate that refuses and a second gate that defers would be two
owners of one decision.
- `serve.rs:10-17`, the stated limit ("This is the current bound and not a
design target… expect the number to move"), is deleted. The number no
longer moves, because the window stops being the organ's delivery limit.
- In `tools/eureka-cut10-mutations.psd1:250-330`, entries D20, D20L, D20L2,
D20L3, D20L4, D20L5, D21 and D21L are **deleted from the cut10 suite and
re-authored in BP-3's suite** against the new spelling. Their `Old`
anchors name `within_window`'s lines, which no longer exist, so leaving
them would make the cut10 runner fail on anchor-not-found rather than on
a rule.
- **Keeps:**
- `MAX_FRAGMENT_BYTES`, `MAX_PENDING_RELIABLE_PACKETS` and
`MAX_RESPONSE_BYTES` (`:124-131`): the window is still what one send
carries.
- `MindRefusal::ResponseTooLarge { bytes, limit }` keeps its name and
shape; **its doc is re-scoped** (below).
- The pipelined-read paragraphs `:32-44`, unchanged and still true:
deferral does not fix F2.
- `encode_or_fail` (`:197-215`).
- The `Daemon` dispatch. `daemon.rs` is untouched: **the mind and the pure
dispatch know nothing about deferral.**
- **Adds:**
| Add | Owner | Live consumer | Protected invariant | Why an existing owner cannot serve |
|---|---|---|---|---|
| `huginn-mind/src/wire.rs:81-90`: `HuginnMindResponse::Deferred(DeferredAnswer)` with `DeferredAnswer { manifest: CultMeshCdnArtifactManifest }` under Q-BP2 A, or plus the typed summary under B; `status()` (`:97-102`) answers `accepted` for it. The manifest's schema is embedded through a `#[schemars(schema_with)]` field attribute returning a `$ref` to its published schema, as the selection consumer cut now does for `Selection` (corrected 2026-09-22: a hand-written `impl JsonSchema` for a foreign type is `E0117`, the orphan rule). `schemas/cultnet/huginn.mind_response.v1.schema.json` is regenerated. **The schema id stays `v1`**, because no client of it exists (Cut 13 is unbuilt) | `huginn-mind` (types only) | the daemon, Cut 13 | the reference rides the response schema, so there is no second vocabulary for a client | `HuginnMindResponse` is the only response a client decodes |
| `huginn-daemon/src/bodies.rs` (new), `DeferredBodies`: a map from chunk hash to bytes with reference counts, and a map from content hash to `(chunk hashes, bytes, last_touched)`. `retain(manifest, chunks, now)`, which is idempotent on the content hash and refreshes it; `chunk(hash, now) -> Option<&[u8]>`, which refreshes the owning bodies; `expire(now)`; eviction least-recently-touched first when the total is over budget. Constants: `DEFERRED_CHUNK_BYTES = 256 * 1024`; `MAX_DEFERRED_BODY_BYTES = 64 MiB`; `DEFERRED_BUDGET_BYTES = 256 MiB`; `DEFERRED_TTL = 60 s`, which is twice `session_timeout`, and is a field of `ServeOptions` (`:70-79`) with that default | `huginn-daemon::serve` | `deliver`, `answer` | retention is bounded in bytes and time, and a body is never evicted while its chunks are being served within the TTL | The mind must not hold transport artifacts. `cultnet-rs` owns no retention policy, and should not, because the policy is this service's memory budget |
| `serve.rs`: `deliver(reply, message_id, operation, runtime_id, bodies, now)` replaces `within_window`. It encodes the envelope. If the envelope is `<= MAX_RESPONSE_BYTES`, the reply is sent unchanged. Otherwise it takes the **payload bytes**, the named MessagePack of the response. If they are `<= MAX_DEFERRED_BODY_BYTES`, it calls `pack_content("huginn.mind_response", "package", "", "application/vnd.gamecult.huginn.mind-response+msgpack", now, payload, DEFERRED_CHUNK_BYTES)`, then `bodies.retain`, and answers `Deferred`. Otherwise it answers `Refused(ResponseTooLarge { bytes: payload_len, limit: MAX_DEFERRED_BODY_BYTES })` | `huginn-daemon::serve` | `answer` | **the one owner of how an answer is delivered.** Three outcomes, in that order, and nothing else decides | — |
| `serve.rs:165-194`, `answer`: a new `CultNetMessage::ContentChunkRequest` arm calls `cultnet_rs::answer_content_chunk_request(request, \|h\| bodies.chunk(h, now))`. The error text at `:188-192` names the third family. The signature gains `bodies: &mut DeferredBodies`, and `run` (`:257-305`) owns one and passes it | `huginn-daemon::serve` | the client's fetch | a chunk is answered on the requester's own session, like every other frame | — |
| `refusal.rs:57-65`, doc only: `ResponseTooLarge` is now "the answer exceeds the largest body the organ will deliver by any plane", `limit` is `MAX_DEFERRED_BODY_BYTES`, and `bytes` is the encoded answer (the payload, not the envelope). It is still the daemon's, and still never raised by the mind | `huginn-mind` | clients | **the backstop stays, per the ruling** | — |
- **Authority map:**
- **Owner:** `huginn-daemon::serve::deliver` decides, per answer, between
delivering it whole, deferring it, or refusing it. `DeferredBodies` owns
retention. `cultnet-rs::content` owns chunking, hashing, the manifest and
chunk answering.
- **Inputs:** the encoded reply, the window constants, the deferral bound,
the store's state and the per-frame clock read (`serve.rs:294`, still the
crate's only one).
- **Outputs:**
- The reply, unchanged.
- `Deferred { manifest }` plus retained chunks.
- `Refused(ResponseTooLarge)`.
- `ContentChunkResponse` for each chunk request.
- **Derived state:**
- The manifest and the chunk hashes are derived from the payload bytes.
- The deferred body is **cache-only** (ephemeral transport state, like
the reference's `CultMeshNetworkBodyStore`, `CultMeshBodySessions.cs:15-18`,
"not a CultCache document store and not world truth").
- The mind is not a writer or a reader of any of it.
- **Forbidden writers:**
- `Daemon::handle` and `Mind` never produce `Deferred` and never read the
window.
- No second gate: `within_window` is gone, and nothing else compares
against `MAX_RESPONSE_BYTES`.
- The client never re-derives a check: it calls `fetch_content`.
- No pagination or truncation behind the caller's back (the Cut 10 F1
ruling still holds).
- **Shared paths:** every operation's reply passes through `deliver`,
including `Whoami` and `Admit`. There is no per-operation path to
deferral. Direct replies and chunk replies leave through the same
`hub.send_schema_message` at `:295`.
- **Deletion line:** `within_window` and the stated-limit paragraph are
deleted before `deliver` is added, and the cut10 entries that anchored
on them are removed in the same commit.
- **Verification:**
- **Builds** (PowerShell, the codex target dir):
- `cargo test -p huginn-mind --lib`;
- `cargo test -p huginn-daemon --lib`;
- `cargo check --workspace`;
- `cargo tree -p huginn-daemon -e normal -d`, which must show no
`cultcache-rs` or `cultnet-rs` duplicate.
- **Tests, each with its rule and mutants.** They are committed in a new
`tools/eureka-bodyplane-mutations.psd1` and run by the Eureka harness
`C:\Users\Meta\.claude\skills\eureka\tools\eureka-mutations.ps1 -Repo F:\Projects\Huginn`,
with a no-op control.
- `an_oversize_answer_arrives_deferred_and_fetches_to_the_same_answer`
rewrites `serve.rs:548-656` over a real loopback hub and a real mind.
The wide view, at 1,315,551 bytes, comes back `Deferred`. The client
runs `cultnet_rs::fetch_content` over the **same session**, one chunk
in flight, and decodes the bytes as `HuginnMindResponse`. The result
**equals** `daemon.handle` of the same request, and is `View(Some)` and
not `Deferred`. The fitting view and `Whoami` still arrive directly.
This pins the ruling.
Mutants:
- `deliver` refuses instead of deferring (revert to Cut 10's
behaviour);
- `deliver` defers at `MAX_RESPONSE_BYTES * 2` (a function of the input:
the wide answer sits between 1x and 2x, so doubling lets it through
whole and the send fails silently, and the test times out with no
reply).
- `the_delivery_boundary_is_the_transports_boundary_byte_for_byte`
rewrites `serve.rs:749-777`. An envelope of exactly `MAX_RESPONSE_BYTES`
goes direct; one byte more is `Deferred`, **not** refused. This pins
that the threshold is the window.
Mutants:
- `<` for `<=` (off by one);
- measure the payload and not the envelope (the old D20L5, re-authored);
- `MAX_RESPONSE_BYTES - MAX_FRAGMENT_BYTES` (a function of the constant
that a "safety margin" edit would produce).
- `an_answer_over_the_deferral_bound_is_refused_by_name` uses a synthetic
response sized exactly `MAX_DEFERRED_BODY_BYTES` (deferred) and one
byte more (refused, with `bytes` = payload length and `limit` =
`MAX_DEFERRED_BODY_BYTES`). This pins that the backstop stays and is
measured against the deferral bound.
Mutants:
- `if true` (never refuse);
- `limit: MAX_RESPONSE_BYTES` (the old limit leaking through);
- `bytes: envelope_len` (the wrong measure);
- `MAX_DEFERRED_BODY_BYTES * 4` (a function of the input).
- `a_deferred_answer_is_never_itself_deferred` pins a rule written
*inside* `deliver`: the deferred reply is always under the window, and
a body never decodes to `Deferred`. Encode the `Deferred` reply at the
bound (about 256 manifest refs) and assert it is `<= MAX_RESPONSE_BYTES`.
Mutant: recurse `deliver` on the deferred reply.
- `retention_is_idempotent_and_sliding` exercises `DeferredBodies` alone
with an injected `now`. The same body deferred twice is stored once, and
its budget use is counted once. A chunk served at `t = TTL - 1`
refreshes the body, so it survives to `2 * TTL - 2`. An untouched body
is gone at `TTL`.
Mutants:
- expiry from creation rather than last touch;
- `>` rather than `>=` at the TTL boundary;
- a TTL that is a function of the input (`ttl / 2`);
- count bytes per retain instead of per body.
- `eviction_is_least_recently_touched_and_bounded_in_bytes` uses three
bodies over the budget. The one touched longest ago goes first, and a
shared chunk survives while any owner lives.
Mutants:
- evict the newest;
- evict by insertion order (killed because the fixture touches the
oldest-inserted body last);
- free a shared chunk with its first owner.
- `an_evicted_body_answers_found_false_and_the_client_can_ask_again`
pins lifecycle end to end.
Mutant: `chunk()` returns stale bytes after eviction. That is
impossible by construction, so the mutant used is "do not refresh on
serve" at a TTL shorter than a fetch.
- `the_mind_never_sees_a_deferral` is a negative test:
`rg -n "Deferred|DeferredBodies|MAX_RESPONSE_BYTES" F:\Projects\Huginn\crates\huginn-mind\src`
matches only the `wire.rs` variant and its schema test.
- `published_wire_schemas_match_derivation` (`wire.rs:210-216`, kept)
pins that the regenerated schema equals the derivation.
- **Scenario sited at the decision:** the loopback test holds its client
on the `Deferred` reply **before** fetching and asserts the manifest's
`sizeBytes` equals the direct `daemon.handle` payload's length. That
observation is at the decision, not downstream of the fetch (the
skill's "put the observation where the rule is decided").
- **Negative checks:**
- `rg -n "fn within_window" F:\Projects\Huginn\crates` is empty.
- `rg -n "sha2|Sha256" F:\Projects\Huginn\crates\huginn-daemon\src` is
empty, because hashing belongs to `cultnet-rs`.
- Verified to not collide: `huginn-mind`'s `sha2` is for receipts, in
another crate.
- **Operator:** none until Cut 14. The real-network check (WireGuard to
Yggdrasil, an oversize read end to end) is Cut 14's runbook step and is
recorded there.
- **Subtraction estimate:**
- Removes about 35 lines (`within_window` 35, the stated-limit doc 8) and
8 mutation entries.
- Adds about 190 lines: `bodies.rs` about 110, `deliver` about 35, the
`answer` arm 8, the wire variant with its `schema_with` `$ref` about
35.
- Tests are about +220 net over the rewritten two.
- Entries: 8 removed, about 20 added.
- No dependency is added: `cultnet-rs` is already a daemon dependency, and
`huginn-mind` already has it after the selection consumer cut. If BP-3
runs first, it adds that dependency to `huginn-mind`, and that is the one
listed cost.
- **Build budget:**
- `huginn-mind` and `huginn-daemon` library and tests, warm after BP-2.
- Expected +50 to +200 paths.
- Host and target are both the workstation. The unix arm is not exercised,
and Cut 14 builds on Yggdrasil.
## 6. Interaction with CultNet selection Cut 1, and sequencing
Selection Cut 1 is in Hands on `cultnet/selection-cut1` in the locked worktree
`.claude/worktrees/agent-af580008561975a4c`, based at `b3d9cf7`.
**What it touches, from its own map:**
- `CultNetDatabaseSubscribeMessage`: v1 classes beside v0. It **keeps** the
body-plane demand fields `bodyIds` and `supportedBodyTransports`, which its
§5 lists as not selection.
- `CultMeshBodyDemand.cs:177-221` and `CultMesh.cs` body and snapshot
selector construction.
- `CultNetSchemaRegistry.cs:360-385`.
- `CultNetSchemaMessageSerialization.cs:9-85`.
- `NetworkingTests.cs`.
- In Rust: `packages/cultnet-rs/src/contracts.rs:314-331`, which gains
`SnapshotRequestV1`, `DatabaseSubscribeV1` and `SnapshotResponseRawV1`. The
last is a **raw** message, so it edits the same `parse_raw` and
`encode_raw` dispatch this map edits.
- `lib.rs` re-exports, the new `selection.rs`, and the vector writer pattern
under `CULTNET_WRITE_VECTORS=1`.
**Semantic interaction: none.** `bodyIds` and `supportedBodyTransports` on the
subscribe message are demand for *live* bodies, the reference's body plane
proper (F2). This cut uses the content plane and never touches that message,
its schema, the body-demand code or the database subscription server and
client. A Huginn read is an operation request and not a subscription, so the
two cuts share no rule.
**Textual collision: certain, in five places:**
- the `CultNetMessage` enum;
- `parse_cultnet_message`'s raw arm;
- `encode_cultnet_message_for_wire`'s raw arm;
- `parse_raw` and `encode_raw`;
- `validate_message`;
- and `lib.rs`.
Two Hands in one working tree is forbidden, and two branches editing one
dispatch list is a merge that falls to whoever lands second.
**Sequence:**
1. Selection Cut 1 lands its Rust commit (its commit 3) on `main`.
2. BP-1 branches from that `main`. The anchors in section 3 are re-taken at
that base, and the discrepancy is reported.
3. BP-1 lands.
4. BP-2 moves the pins once to a revision carrying both.
5. Then the Huginn selection consumer cut and BP-3, in that order by
default.
BP-1 does **not** add JSON schema files or registry entries, so it does not
touch `CultNetSchemaRegistry.cs` or `contracts/cultnet/`, which selection
Cut 1 is editing. The absence of published schemas for the content messages
predates both cuts and is a follow-up (section 7).
**The mutation runner:** `scripts/mutate-cultmesh.mjs` is modified in the
working tree by the live QUIC campaign. BP-1 adds its entries only after that
file is committed or released, and never onto someone else's uncommitted
edit. If the QUIC campaign still holds it when BP-1 is ready, BP-1 carries
its entries in a new `scripts/mutate-cultnet-content.mjs` under the same
contract, and Self collapses the two later. That is a default, not a
question.
## 7. Deliberately not in scope
- **Pipelined chunk fetch, and Cut 10's F2 (pipelined reads).** The fetch is
sequential. F4 shows the throughput barely depends on chunk size, and
pipelining is the known way to lose a reply silently. A window-aware sender
is a transport change in `rudp.rs`, not this cut.
- **Throughput.** About 0.85 MB/s on loopback (F4), set by the 32-packet
pacing window. A 64 MiB body takes about 75 s. Recorded, not addressed.
- **A TCP or QUIC content connector in Rust, and Rust session identity with
Odin-signed route certificates.** These are the reference's
`CultMeshSessionIdentityClient` and `transport-planes.md:73-86`. Huginn's
control plane has none of that today, and the body rides the same session
with the same (absent) trust. **Trust for both planes is Cut 14's** (D8, the
trust boundary).
- **Durable, resumable or file-promoted fetch** (the reference transfer
service's resume state and `<content-hash>.body` promotion). An answer is
re-asked, not resumed.
- **Publishing JSON schemas for `cultmesh.content_chunk_*`.** The reference
never published them: there is no file in `contracts/cultnet/` and no entry
at `CultNetSchemaRegistry.cs:360-385`. Follow-up: a CultLib parity fill for
every runtime, after selection Cut 1 has released the registry.
- **The TypeScript, Python and Kotlin content plane.** It follows when one of
them has a caller, the same rule the selection cut ruled.
- **The live body plane** (`body_read_*`, `CultMeshNetworkBodyStore`, body
demand) in Rust.
- **Persisting deferred bodies across a daemon restart.**
- **Per-operation summaries,** unless Q-BP2 B is ruled. Row-level summaries
are the selection vocabulary's projections.
- **Cut 13's client handling of `Deferred`.** Consequence for Cut 13's spec,
to be carried into its refresh: every read tool resolves `Deferred` through
`cultnet_rs::fetch_content` with a cap no larger than
`MAX_DEFERRED_BODY_BYTES`, and treats `ResponseTooLarge` as "narrow the
selection".
- **Consequence for Cut 14's spec:** the daemon's memory budget on Yggdrasil
grows by up to `DEFERRED_BUDGET_BYTES` (256 MiB).
## 8. Operator questions
Two real forks. Every other choice above (the content plane over the body
plane per F2, the reference's manifest, the ephemeral store, the constants,
the order) is a default with its reason stated. It is recorded, not asked.
**Q-BP1: RULED A by the operator, 2026-09-22 ("A is fine here").**
- Chunk requests and responses travel on the session the client already
holds, byte-identical on the wire to the reference's
`CultMeshLegacyRudpContentServer`.
- BP-1 amends `transport-planes.md` to name this as Rust's *explicit* content
path, never an implicit default.
- It lasts until Rust has an authenticated content connector, TCP+TLS or
QUIC. That later move sits behind the same `fetch_content` owner and needs
no change to the reference type.
*History, the question as asked:* **Q-BP1. Which physical path carries the body?**
CultMesh's own transport doctrine says "Schema messages must not carry bulk
bodies" and that the RUDP content path is legacy, "compatibility and parity
measurement" only (`transport-planes.md:90-96`). But the only session
Huginn and its client share is CultNet RUDP, and the doctrine's preferred
content plane (TCP byte stream) is plaintext and loopback-only until TLS
(`:103-106`), while Huginn serves remote clients over WireGuard.
- **A. Chunk request and response on the session the client already holds.**
This is wire-identical to the reference's `CultMeshLegacyRudpContentServer`.
`transport-planes.md` is amended to name it as the Rust runtime's
*explicit* content path, never an implicit default, until Rust has an
authenticated content connector. Cost: it re-legitimises, for Rust, a path
the C# doctrine retired, and it inherits RUDP's pacing (F4).
- **B. Port the TCP content plane to Rust** (`cultmesh-content+tcp://`, a
typed header and raw bytes). Faster, and outside the envelope. It is
blocked for Huginn by the doctrine's own loopback-only gate until TLS
exists, so it needs a TLS content connector as well: a much larger cut, and
Huginn waits on it.
- **C. Wait for authenticated QUIC in Rust**, which does not exist. Huginn's
oversize answers stay refused until then.
**Recommended: A.** It is the only option that delivers what the ruling
asked for on the substrate Huginn has. It reuses the reference's wire byte
for byte, and the amendment keeps the doctrine honest rather than quietly
contradicted. B and C are where the body moves later, behind the same
`fetch_content` owner and without changing the wire's reference type.
**What depends on it:** BP-1's documentation paragraph, whether BP-1 grows a
TCP connector (B), and whether BP-3 can land before a TLS campaign (B and C
block it).
**Q-BP2: RULED A by the operator, 2026-09-22 ("I accept your
recommendations").** The deferred answer carries the reference's own facts:
the manifest (`sizeBytes`, `contentHash`, the chunk list) and the operation.
It carries no per-operation summary. Row-level facts come from asking again
with a header projection. `huginn-mind` gains no summary types. B can be added
later on the selection projection if Cut 13's tools show the need.
*History, the question as asked:* **Q-BP2. What is "the summary" the control plane carries?**
- **A. The reference's own facts.** The manifest carries `sizeBytes`,
`contentHash` and the chunk list, and the envelope carries the operation.
A caller decides whether to fetch on size alone. Anything row-level (ids,
statuses, `matched`) comes from asking again with a selection projection,
which the selection cut already provides.
- **B. A typed per-operation summary produced by the mind:** for a view, the
id, kind, status and admission facts without the document; for a page,
`matched` and row references. The caller can act without fetching at all.
Cost: new summary types and derivations in `huginn-mind`, one per read
operation, re-specified whenever the selection consumer cut reshapes the
reads, plus schema growth.
**Recommended: A.** It keeps deferral a pure transport decision owned by one
function that knows nothing about operations. A per-operation summary is a
second projection vocabulary beside the one the operator just ruled into
CultNet. If Cut 13's tools show that agents need to decide without fetching,
B can be added then, on the selection projection rather than beside it.
**What depends on it:** `DeferredAnswer`'s shape, whether `huginn-mind`
gains types, BP-3's size (B adds about 120 lines and a test per operation),
and Cut 13's tool output.
## 9. What was probed, what was only read, and what is not settled
- **Probed by running code:** F1 (decode refusals, plus the on-wire refusal),
F3 (real reference bytes, byte identity of both messages and the manifest
in Rust, the Rust re-pack equal to the reference's), F4 (whole-body send
failure; sequential delivery at three chunk sizes with SHA equality and
timings; the 900 KiB failure; the pipelining loss), and F5 (duplicate
`cultcache-rs` under mixed revisions).
- **Read only, not run:**
- C# `ValidateManifestShape` sorting and contiguity, `NormalizeHash`, and
the legacy server's validation order. These are the rules the vectors
will pin in BP-1.
- The reference's TCP connector behaviour and the loopback-only gate, from
`transport-planes.md`, not from running `CultMeshTcpContentTransport`.
- Selection Cut 1's exact edit sites in the worktree. I read its map, not
its uncommitted diff, and did not open the locked worktree's changes, to
avoid reading another agent's working tree mid-edit.
- **Not probed:**
- Throughput over WireGuard or to Yggdrasil (loopback only).
- The 60 s TTL against a real slow link.
- C# decoding **Rust-written** bytes. Byte identity makes it follow, but
BP-1's C# test is the first thing that will actually run it.
- Whether a hostile chunk flood can starve the control plane on one
session (the window is per session, so the likely answer is that it can
starve only the flooding session). Recorded for Cut 14's trust pass.
- **Substrate missing, recorded per the skill:** typed pipeline state. This
map, its model page and its questions live in markdown because Huginn,
which would hold them, is the thing being built.

Some files were not shown because too many files have changed in this diff Show more