Embedder::embed takes the bound it must answer within. The query embed gets
QUERY_EMBED_TIMEOUT, derived from SEARCH_DEADLINE, which is also the serve
loop's default search timeout. The bulk flush embed keeps 300 s
(BULK_EMBED_TIMEOUT); it does not block serving.
Projector::embed_verified reads the model identity and the collection label,
embeds, re-reads the identity and refuses on change. Search and flush both use
it; the inline copies and step()'s separate identity read are gone. Flush now
also refuses a collection that is no longer this mind's.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The three searches now carry their own text, so the test asserts which two were
embedded, in what order, not only how many.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
cargo-mutants on 11b's diff: three timeouts (a drain loop that never settled
under Projector::reconcile -> Ok(()), a dead worker leaving thirty-second
waits to add up past the harness deadline) and three survivors. Now:
- drain is bounded, the status and gate waits are ten seconds, so a mutant
fails inside the harness deadline instead of hanging it;
- the worker's first step is due immediately rather than one interval after
a startup that is itself a verification, and an idle worker is pinned to
one recheck per interval (a recheck that did not move its own deadline
would list the collection in a loop);
- the log line for hits the mind does not hold is a function with a test.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Operator ruling 2026-09-30: while the index is rebuilding (a re-pulled model,
a lost collection, Reconciling or Behind) a semantic search refuses with a
typed Unavailable naming the state and the pending count, at the sink and
again in Projector::search, and never answers from a half-filled collection.
Soul's findings on 11b:
- a failed search clears the reconciliation, so whoami stops reporting Current
and the worker verifies again; the idle recheck is an absolute time that
searches no longer push out;
- the queue holds at most SEARCHES_QUEUED_MAX and refuses the rest by name;
the worker serves one search per turn and returns to its writes; the serve
loop takes back a search it timed out (IndexSink::abandon), so a queued one
is never embedded;
- the model identity is read again after the query is embedded, and the
collection's label is re-read before it: a search is one decision;
- a failure report's pending count is refreshed under the lock that takes the
inbox (absorb);
- wait_status has one overall deadline, and the test gate and worker-stop
waits are bounded, so a mutant fails instead of hanging.
Tests pin each rule: ticket uniqueness, searched() returning exactly what the
worker answered, the refuse_searches call site, serving between batches, the
bound, abandonment, the rechecks and the label.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Daemon::unheld called mind.view once per hit, and every call decodes the
whole mind: 3.27 s on the loop thread at 2000 documents and 200 hits, to feed
one log line, and a second judge of whether the mind holds a document. rank
already reads the rows once and is the only judge now: it returns the hits it
found no document for in Ranked { page, dropped }, and Daemon::finish reports
a count and the first five.
Also in rank: candidates are filtered to held documents before they are cut to
the best 200 by score, so an id the mind lacks cannot take a place from one it
holds and select never sees more than it can answer (its own cut is by
ordinal). A semantic selection with descending is refused SelectionInvalid,
as cursor is: the answer's order is the score's and was silently ignored.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The narrowed selection cleared a cursor that semantic_selection has already
refused, and an empty cursor is no cursor to the evaluator; the mutation run
showed the clear could be deleted with every test still green. A test pins
that an empty cursor still ranks.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Qdrant supplies candidate ids and scores and nothing else. Mind::rank drops
every hit the mind does not hold, then lets CultNet's selection decide
membership over the whole mind with its keys narrowed to the candidates, so
in_force and cited mean what they mean in query. Order is score then ordinal;
the page is cut to the selection's limit and top_k; next is never set. A
cursor with semantic, blank text and an out-of-range top_k are refused by name
before anything is embedded. Mind::query loses its semantic parameter and the
"index is not wired" refusal; the published request schema's description of
SemanticQuery follows.
The serve loop never waits on the embedder: a semantic query becomes a Search
held beside its session, the worker embeds (behind the model's query
instruction) and searches between batches, and the loop answers when the
worker has or when ServeOptions::search_timeout passes. A worker that is
Failing or Refused refuses at once, typed Unavailable naming its own error.
VectorIndex gains search; Qdrant's adapter sends no filter.
Model identity is re-read before every flush and before every search: a model
re-pulled under the same dimensions but a new digest forgets the
reconciliation instead of mixing vectors, and the next step rebuilds. An idle
worker forgets the reconciliation every Backoff::recheck (60 s), so a lost
collection is found without a write. Both go through Projector::reconcile,
still the one judge.
Also fixes a race in the worker loop found by the mutation runs: entries were
taken from the inbox under one lock and published as wanted under another, so
whoami could read a stale Current with nothing queued while the worker held
them. Taking, absorbing and publishing are now one critical section.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
huginn-mind::index defines each kind's search text (pure). The daemon gains
Ollama and Qdrant adapters (ureq 3, no TLS) behind Embedder/VectorIndex
ports, and one worker thread whose Projector owns reconcile. Admission
never waits on the index. Reconcile is the only judge of missing points,
and it runs again after any failure. The collection is labelled with the
mind's genesis receipt id and keyed on model identity (name, digest,
dims, text version), never the URL. whoami reports IndexStatus. The live
smoke passed against Qdrant on Yggdrasil and Raven's Ollama
(qwen3-embedding:0.6b, 1024 dims).
Soul closed it after one fix batch. For Cut 11b: every search hit is
joined back through the mind and filtered by in-force; model identity is
re-checked per flush. Noted limit: Ollama truncates at 4096 tokens.
mind 96, daemon 47.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
F1 the 30 s to 10 min backoff curve is pinned by a pure test.
F2 any failed step forgets the reconciliation; the projector keeps every entry
it was given so a lost collection or a changed model is rebuilt and refilled
without a restart.
F3 the collection label carries the mind genesis receipt id; a collection of
another mind with the same instance name is refused and untouched. A mind with
no receipt yet reconciles nothing.
F4 the effective input limit (model context, not 16 KiB) and the Cut 11b join
requirement are recorded in the module docs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The instance name is only a label; two stores may share it. The receipt of the
first admission never changes and digests the first batch.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mutation survivors from the first cargo-mutants pass; the equivalent ones
(point-id bit shaping, a byte-limit boundary) are left.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
An answer too large for one send becomes HuginnMindResponse::Deferred,
carrying only the reference's CultMeshCdnArtifactManifest (Q-BP2 A). The
client fetches the body over the session it already holds with
cultnet-rs fetch_content (Q-BP1 A) and gets bytes identical to the mind's
answer. Retention is in memory only: 256 MiB counted by distinct chunk,
a sliding 60 s TTL, and LRU eviction. ResponseTooLarge stays the backstop
above 64 MiB. Every echoed field is bounded at 256 bytes, so every reply
fits the window. A deferred envelope carries its answer's status.
Soul closed it after one fix batch. Named limit: there is no per-peer
budget, so a client can evict another client's body mid-fetch. That is
acceptable for trusted WireGuard clients. mind 91, daemon 27.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A reply echoes the request's message_id (and on failures its service_id,
operation and payload_schema; on chunk answers its chunk_hash). answer now
refuses any of them over MAX_ECHOED_FIELD_BYTES (256) at the door with a
transport Error that carries none, so every reply, including the largest
Deferred and ResponseTooLarge, fits the window. A deferred answer's envelope
carries the status of the answer it stands for. The retention docs name the
missing per-peer budget and that expiry runs only when a frame arrives.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
huginn-mind::index owns what of a document is searchable: index_text is a
pure function per kind, Mind::index_entries builds the entries with the
writing receipt's ordinal, INDEX_TEXT_VERSION says when the rule changed.
huginn-daemon::index owns the projection. A Projector reconciles the mind
against the collection at startup (the only place a point is judged
missing: set difference on point ids, rebuild when the model name, digest,
dimensions or text version differ, refuse a collection that is not this
instance's) and one worker thread embeds and upserts in batches of 32,
retrying with a 30 s to 10 min backoff. IndexSink::committed builds entries
and leaves them in an inbox; admission never waits on the embedder or on
Qdrant. Whoami reports the worker's health as IndexStatus. The Ollama and
Qdrant adapters are the only files holding HTTP or JSON (ureq 3, no TLS).
The daemon now requires --qdrant-url, --ollama-url and --embedding-model;
NoIndex leaves the binary.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
deliver replaces within_window as the one owner of how an answer leaves:
whole when the envelope fits the window, deferred when the payload is within
MAX_DEFERRED_BODY_BYTES (64 MiB), refused by name past it. A deferred answer
is HuginnMindResponse::Deferred(DeferredAnswer { manifest }), the reference's
CultMeshCdnArtifactManifest; the chunks are asked for on the session the client
holds and answered by cultnet-rs's answer_content_chunk_request. The body is
held by DeferredBodies (256 MiB budget, sliding 60 s TTL, least-recently-
touched eviction, shared chunks stored once), in memory only. The mind knows
nothing of deferral.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Each kind declares exactly its aliases and roles, role targets are the field's
typed kinds, and no alias is numeric. Found by cargo-mutants over the RS-3
diff: declared_on, declared_roles and is_numeric survived.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The four hand-written filters, the order and the cap are deleted:
PipelineQuery, PipelineQueryPage, QUERY_LIMIT_MAX, Reader::matches, ordered,
repo_matches. Mind::query(selection, semantic) validates through
cultnet_rs::validate, closes the values door (rows.rs refuse_values, S6),
takes the snapshot from the cursor's own asOf (an asOf beyond the head is
CursorInvalid), builds Reader::at(as_of) so every derivation runs over the
documents admitted at or before it, and evaluates through cultnet_rs::select.
The page carries matched, asOf, the next cursor, headers or whole views by
projection, and typed edges when the selection hops.
rows.rs is the organ's vocabulary: SelectionRow (a Row), Vocabulary (a
RowSet: thirteen schemas, eleven aliases, fifteen roles) and the value door.
huginn-mind links cultnet-rs at the rev the daemon pins. Selection refusals
map totally onto SelectionInvalid, CursorInvalid or Unavailable. The wire's
Query carries CultLib's selection by $ref, and both published schemas are
regenerated. The relations RS-2 unpinned are re-pinned as selections.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
docs.rs::citations and CitationRole are the one edge list: the fifteen
roles, one per referring leaf field. Admission's A7 references() is a map
over them, carrying the Missing classification by role and the hand-off
source-side filter, with the same refusals in the same order. kind_of_id and
outcome_references leave admission; the CitesResolvedDocument check reads
outcome_citations.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
One pin move: huginn-mind pins cultcache-rs at 6251cbc and the leaf at
4891f2e5; huginn-daemon pins cultnet-rs at 6251cbc. cargo tree shows one
cultcache-rs and one cultnet-rs per crate.
Adopting RS-L, which the new leaf carries:
- Campaign, cut-spec, question and ruling fixtures carry a Title; the
question and ruling fixtures gain the new required title field.
- The epoch literals in daemon and envelope tests move v1 -> v2. A store
written at an earlier epoch is refused at open (ForeignEpoch); no key
migration.
- Derived stewardship, hand-off and resolution keys are now the lowercased
OrgRepo::identity(); test key literals follow.
- The two published wire schemas are regenerated (Title, OrgRepo bound and
description); the byte-for-byte pin test passes.
CultLib 6251cbc gives CultNetMessage::Error code and details; the four
construction sites and one pattern in huginn-daemon take None and `..`. No
behaviour change: None is skipped on the wire. mind 70 and daemon 18 pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The test-support feature comment in huginn-mind/Cargo.toml named only
store_path_for; it also gates open_with. The RS-1 grammar-door test's
doc called open_with "the door open shares" -- open and open_with
both call open_checked, which is the actual shared construction site.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The doc on open_with claimed it never compiles into a release binary.
That is only true for `cargo build --release -p huginn-daemon`; the
test-support feature is unified in by `--all-targets` and `cargo
test`, which do produce a huginn-daemon binary carrying open_with.
Checked F:\Projects\gamecult-ops\deployment\idunn-bindings and
F:\Projects\gamecult-ops\idunn for a Huginn deploy recipe: none
exists yet, so nothing currently pins which command ships. State the
actual condition instead of a release-only guarantee no recipe backs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every caller of Mind::receipts lives inside huginn-mind (admission.rs,
query.rs, receipt.rs); nothing outside the crate needs raw,
unchecked receipt ordinals. Narrowing it to pub(crate) closes the
only door that let a caller read ordinals without going through
receipt::head's density check, which is what head's own doc claimed
already held. Update that doc to say so explicitly instead of
asserting a door was closed that was, in fact, still open.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The two fixtures pinning the epoch-first gate order used
"epiphany.pipeline.campaign.v1" as their foreign type. That id is the
pinned leaf's real, currently-registered campaign kind, so
is_known_type accepts it and a gate-order mutant that ran the type
gate first still passed both fixtures by accident (confirmed on
Yggdrasil: both hand-applied mutants survived 70/70 before this fix).
Re-point both at a new NEVER_A_KNOWN_TYPE constant that cannot collide
with any real PipelineKind id at any epoch this leaf pin carries, so
the fixtures actually exercise the epoch-first order.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Better to have nothing than a harness that punishes refactoring
(operator ruling, 2026-09-22). Remove tools/eureka-cut8-mutations.psd1
(825 lines), eureka-cut9-mutations.psd1 (371), eureka-cut10-mutations.psd1
(994) and eureka-readside-mutations.psd1 (225). cut9, cut10 and readside
carried uncommitted edits that are now moot and are dropped with the
files. The harness that ran them, eureka-mutations.ps1, was already
deleted from the Eureka skill (37c80e4).
Code comments in crates/huginn-mind/src/mind.rs and
crates/huginn-daemon/src/serve.rs that cited entry ids from the
deleted suites (R5b, R5c, N1, N3, S2, S4, S3wide, Residue S1) are
reworded to drop the id or name the test that pins the rule instead;
each comment already sits beside that test. Comments citing F- and
other S/A-prefixed labels that do not match any entry in the deleted
files (F2-F6, S1, S5, A1, and the D3/D4/D5/D8 in query.rs, which name
a different rule than the D3/D4/D5/D8 entries in the deleted files)
are untouched.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
F1: AdmissionIndex::build now calls receipt::head before copying any
ordinal, so query and view refuse a receipt chain that is not exactly
{1..=N} (duplicate, gap, zero, or above-N) instead of trusting a stored
ordinal unchecked. New test query::tests::query_and_view_refuse_a_receipt_chain_that_is_not_dense
pins it behaviourally: it plants each broken chain directly (bypassing
admission) and asserts query and view both refuse. The schema's ordinal
gets minimum: 1 (was 0), matched by #[schemars(range(min = 1))] on
AdmissionFacts::ordinal. head's doc now says AdmissionIndex::build asks
it too.
F2: Mind::open_with is renamed to an internal open_checked, and a
test-only Mind::open_with is gated behind cfg(test) or the test-support
feature, exactly like store_path_for. Production reaches open_checked
only through Mind::open, which takes the store's per-path lock first.
mind::tests::a_mind_has_one_owner_at_a_time (pre-existing) still pins
that a second Mind::open on the same path is refused. The full store
seal against a second in-process owner over an arbitrary MindStore stays
a recorded follow-up. lib.rs's crate doc now names Mind::open as the
owner of "may this store be this instance's mind".
F3: R5 is not binary. Two loosenings are pinned as R5b and R5c in
mind.rs, with behavioural fixtures: a foreign type with no epoch record
at all still refuses MissingIdentity, not ForeignStore (R5b); an epoch
record at the current key with a foreign value, beside a foreign type,
still refuses ForeignEpoch, not ForeignStore (R5c).
F5: gate step numbering in mind.rs's doc comment and its test comments
now read 2 (epoch), 3 (type), matching the code's actual order. docs.rs
no longer credits resolutions_of/assignments_of with reading history,
which RS-2 deleted. The two-door test in query.rs is renamed from
a_ref_whose_kind_and_id_disagree_is_refused_by_both_doors to
..._by_view, since view is the only door left.
Per the operator's ruling retiring the anchor-based mutation harness,
this batch carries no .psd1 changes; the cut9/cut10/readside mutation
files have uncommitted edits from the earlier pass (stale references to
the old test names, and R3L's target) left for a later pass to fold in
or drop.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Per eureka-read-side-cut.md. The receipt goes to v2 (type id and schema
version both huginn.mind_commit_receipt.v2) and gains ordinal: u64 at key
7. candidate takes the ordinal; digest stays unchanged, so an exact
replay keeps the ordinal its first admission assigned. validate refuses
ordinal == 0. New receipt::head(mind) decodes the receipts and requires
their ordinals to be exactly {1..=N}, refusing Unavailable otherwise;
admission is the only caller, passing head(self)? + 1, and no other code
computes an ordinal. AdmissionFacts gains ordinal, filled by
AdmissionIndex::build; the order query.rs sorts by is unchanged (RS-3
deletes it, not this cut).
mind.rs's opener now calls refuse_foreign_epoch before
refuse_foreign_types, so a store written at a foreign epoch is refused
as ForeignEpoch rather than ForeignStore: a real epoch bump always moves
every type id, so the type gate would otherwise fire first and the
epoch gate would never be reached for the one case it exists for. The
doc comment is updated to match. Regenerated both published wire
schemas.
New tools/eureka-readside-mutations.psd1 (R1-R5, R1 and R3 with L
variants, R1 also with an off-by-one-head R1L2; R2 and R5 documented as
having no loosening, on the same grounds cut10's D9 and D12 give). Five
new tests carry them: ordinals_are_admission_order_not_clock_or_id_order
and an_exact_replay_keeps_its_ordinal in admission.rs,
a_chain_that_is_not_dense_refuses_admission_and_reads_alike in
receipt.rs, and a_v1_receipt_store_is_refused_at_open plus
a_store_written_at_the_previous_epoch_is_refused_by_the_epoch_gate in
mind.rs. cut8's H6 is re-anchored on the new gate order (its own rule,
every gate before attach, is unchanged); cut8 (67), cut9 (27) and cut10
(65) all rerun clean, because candidate's signature and the opener's
order both changed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Subtraction only, per eureka-read-side-cut.md. Deletes PipelineOpenItems,
HistoryScope, open_items, history, and the admitted_after/admitted_before
window from the read side, their wire variants and arms, the daemon arms
and tests that exercised only these, the OpenItems/History lines from the
instance-gate tests (each keeps its query and view cases), and the README
clause. serve.rs's N4 gate fixtures use whoami in place of open_items as
the long operation name; the case is kept.
Deleted the now-dead Reader::views() helper and the 'a lifetime it forced
on Reader, and stripped the History-only assertions from two tests the
map did not name (a_ref_whose_kind_and_id_disagree_is_refused_by_both_doors
and views_read_admitted_at_from_the_receipt_not_stored_at) that also called
the deleted API. Regenerated both published wire schemas.
Removes cut9 V9-V15, V22 and V22L (12 entries incl. V13L/V14L), and cut10
D19, D19L, D23, D23L, S1 and S1d (6 entries). D13 and D13L are re-anchored
on the surviving view/query arms; D13L uses a near-miss spelling rather
than a pure casing change, since PowerShell's default -eq made a casing
mutant read as "changes nothing" to the harness's own guard. V24 collapses
from a two-door Edits array to a single edit now that view is the only
door left. cut9 (27 entries) and cut10 (65 entries) both rerun clean with
M0 green.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Epiphany codex/eureka-pipeline-state b3ae787b extends the leaf's door
test with the 64-byte whole-name and per-label bounds and NUL, adds
three matching mutation entries, and corrects the entries file header
that still named the deleted repo-local harness path. One cultcache-rs
throughout: epiphany-pipeline, cultnet-rs and huginn-mind all still
resolve to CultLib a0813c6e.
Verified: cargo test --workspace (huginn-mind 65/65, huginn-daemon
20/20, eureka-state 0/0), cargo tree -i cultcache-rs shows one node.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
S3: Mind::path_for bypassed the grammar door and the daemon's own tests
constructed stores through it directly. It is now private; a test-only
Mind::store_path_for validates through require_grammatical_slug first,
gated behind cfg(test) here and a test-support feature dev-dependents
opt into (huginn-daemon's Cargo.toml). The four daemon test call sites
and one huginn-mind test call site now go through the door.
S2: N1's door, require_grammatical_slug, had no mutation entry for two
of its three callers (open_with and require_instance survived deleting
it; open already had N1's own escape test). Added a test per caller and
a mutation entry for each of the three deletions (N1open, N1openwith,
N1requireinstance) in tools/eureka-cut10-mutations.psd1.
S4: a Unicode hyphen (U+2010) folded to ASCII '-' before the grammar
check survived. Added a fixture and mutation entry (S4fold) pinning
that no such fold runs before Slug::validate_slug.
S1: the size-gate fixtures varied operation and runtime id together
(4/16, 4/16, 10/19), so a formula tracking one and dropping the other
still passed. Added a fixture varying them independently (10/16 and
4/19) and two mutation entries (N4c, N4d) for the two formulas that
each drop one term.
Verified: cargo test -p huginn-mind --lib (65/65), cargo test -p
huginn-daemon --lib (20/20, one more than before: the new
independent-axis gate test). Mutation suites through the Eureka
harness: Cut 8 67/67 killed, Cut 9 38/38 killed, Cut 10 77/77 killed
(71 before this batch, +6: N1open, N1openwith, N1requireinstance,
S4fold, N4c, N4d). M0 green in every suite.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
operation and runtime id
N3 (medium-low). One generated test, mind::tests::every_separator_variant_
of_a_declared_name_is_foreign_not_the_mind, replaces the separator-only
fixture pairs' narrower coverage. A base name carrying all three separator
bytes a Slug permits at three non-adjacent positions yields 26 same-length
variants by cartesian substitution, plus 3 dash-padded and 1 doubled-dash
variant (30 total); every one must be refused as ForeignInstance. Kills
four mutants of require_instance's comparison: folding `_`/`.` into each
other on both sides, folding `_` to `-` on the mind's own side alone,
trim_matches('-'), and collapsing `--` to `-` (N3a-d).
N4 (low). The size-gate boundary fixtures (the_gates_boundary_* in
serve.rs) held operation and source_runtime_id fixed at "view" and
"huginn-yggdrasil", varying only message_id -- so a formula of message_id's
length alone, shaped to fit exactly the suite's two lengths, would never be
wrong there. synthetic_with_id and sized_exactly_with_id are now thin
wrappers over synthetic_full/sized_exactly_full, which also take operation
and runtime_id; a new test, the_gates_boundary_holds_across_operation_and_
runtime_id, holds the boundary at view and open_items over two runtime
ids. Kills two coincidental overhead formulas that fit the old fixture's
two message_id lengths by construction: payload+228+id.len()+(id.len()>31)
(N4a) and payload+228+id.len()*38/37 (N4b).
All three suites rerun clean: cut8 67/67, cut9 38/38, cut10 71/71 (65 plus
N3a-d and N4a-b), every M0 green, restores hash-verified. cargo test
--workspace: huginn-mind 62/62, huginn-daemon 19/19.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
N1 (medium-low). Moves the epiphany-pipeline pin to the leaf's new commit
(9d3a3efd), which opens Slug::validate_slug. Huginn's own cultcache-rs pin
is untouched; cargo tree -d still carries exactly one cultcache-rs.
Mind::require_instance now validates the declared name's grammar before
comparing identity, so admission's A1 and every read refuse a bad name
identically, through one door. Mind::open and open_with validate the
declared instance before path_for ever runs, so a Slug carrying `..` and a
path separator (Soul's outer/inner probe) is refused before any store is
created, not after it has already escaped the state root.
require_grammatical_instance and its daemon-side branch in daemon.rs are
deleted: the throwaway PipelineInstance wrapper it built is no longer the
only door onto the leaf's grammar. The refusal now names the field a
caller actually held the declared name under (declared, or instance) and
the whole declared name, not the old wrapper's instance.instance field or
a single failing label.
Adds a leaf-escape regression test pinning Soul's probe and asserting no
store is created, and a fullwidth-instance test showing Admit and Query
refuse identically. Updates the cut10 grammar-fold mutant (S3wide) to the
new function shape and the affected daemon test to the new field name.
N5 and N6, prose only. The cut10 entries gain a note by S1 and S1d: neither
mutant "dies on all three" faults its shared fixture carries, contrary to
an earlier claim; S1d exempts the orphan fault by construction and is
killed by the undecodable-document block instead, the next one the test
body reaches. The cut9 header's self-contradiction at lines 18-25 is
resolved (nothing in the suite exercises the door, not "nothing reaches
it"), its self-reference now points at store.rs:18-19, and its line-1
range is corrected to V1-V24L.
All three suites rerun clean after the pin move: cut8 67/67, cut9 38/38,
cut10 65/65, every M0 green, restores hash-verified. cargo test --workspace:
huginn-mind 61/61 (was 60), huginn-daemon 18/18 (was 17).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
F7: store.rs claimed "no non-test code outside this crate touches a row
directly." False: MindStore's supertrait CacheBackingStore and the public
compare_and_swap_batch already let it. Rewrite the comment to say so, and
name the re-export honestly -- two traits, CacheBackingStore and
DatabaseEntry, plus DatabaseEntry's own derive macro riding along under the
same name in a different namespace. Sealing the store so admission is its
only write path stays a recorded follow-up, its own cut; nothing here seals
it.
The cut9 entries header rested the same false premise: "no behaviour this
Body can reach" for assignments_of and resolutions_of's two weakenings
assumed admission is the only write path. Corrected to "not yet reached,"
with the reason -- a row planted through the store port directly bypasses
A5's instance check and the leaf's kind/id grammar alike.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
F5: cut8's entries header still named the deleted
F:\Projects\Epiphany\tools\eureka-mutations.ps1. The harness moved to
C:\Users\Meta\.claude\skills\eureka\tools\eureka-mutations.ps1 on 2026-09-17;
cut9 and cut10's headers already needed the same fix and are corrected
alongside their own entries in this batch.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
F2: the boundary test always used message_id "m-0", so a gate that measures
payload.len() + 231 -- exactly that id's fixed overhead -- passed unnoticed.
message_id rides the encoded envelope and echoes the client's own value, so a
longer id changes the overhead the gate must measure honestly.
Parametrize synthetic/sized_exactly by message_id and add a second boundary
test at a 40-byte id: 1,228,800 bytes still passes and 1,228,801 still
refuses. Add mutation entry D20L6, the "+231" formula, which dies against the
new test and survived the old one.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
F1: two mutation entries against Mind::require_instance, folding `_` and `.`
into `-` before comparing. Both die against
require_instance_is_the_one_check_admission_and_the_daemon_share's new
separator-fold assertions (the prior commit); neither had an entry before,
since no fixture pair differed by separator alone.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
F3: the only existing fault for the open-items dispatch arm (an orphaned
instance document) sits outside the campaign a request asks about, so two
loosenings survive unnoticed: one that swallows an Unavailable naming a
document in the requested campaign, and one that swallows every Unavailable
except a missing receipt.
Add open_items_refuses_on_a_fault_inside_the_requested_campaign, driving a
missing receipt, an undecodable stored document (docs.rs:46) and a document
written by two receipts (query.rs:145), each on a cut spec rooted in the
requested campaign. Add mutation entries S1 and S1d against
Mind::open_items; both die against the new test.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
F6: no declared instance Slug was ever checked against Slug's own grammar
before reaching Mind::require_instance's byte comparison, so a name outside
the grammar read as merely a foreign mind rather than being refused by its
own name.
Slug's grammar check is a crate-private trait in epiphany_pipeline, reachable
only through a real document, so require_grammatical_instance wraps the
declared name in a throwaway PipelineInstance and reads the answer back
through PipelineDocument::validate, the leaf's own public door; nothing here
re-derives the grammar. Daemon::handle asks it on the read path, before
Mind::require_instance, for every request that names an instance.
Add a_declared_instance_outside_the_grammar_is_refused_by_name_before_the_identity_check,
driving a fullwidth Latin small letter y, and mutation entry S3wide, which
folds a fullwidth character to plain ASCII before validating and dies
against it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
F1: thought-cage, thought_cage and thought.cage are three legal Slug names,
not one, so a comparison that folds `_` or `.` into `-` at
Mind::require_instance answers one mind for another. Add the fixture pair
and a mind opened under a separator-bearing name to
require_instance_is_the_one_check_admission_and_the_daemon_share, and two
mutation entries, D1L6 and D1L7, that fold each separator into `-`; both die
against the new assertions.
F4: fixtures.rs described Label's grammar on the CASED_INSTANCE comment as
though it were Slug's. Corrected to say what Slug actually is at the pinned
Epiphany revision: dot-joined labels, bounded to 64 bytes whole.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every foreign fixture differed from the mind's name in its bytes, so a check
folding case passed both suites. A slug label is `[A-Za-z0-9_-]{1,64}`:
uppercase is legal, `Yggdrasil` and `yggdrasil` are two names a client may
declare and two minds a host may hold, and a comparison that folds case
answers one of them for the other. Soul found the mutant alive.
`CASED_INSTANCE` stands beside the near and prefixed names in the mind
fixtures and `CASED` beside them in the daemon's, and each test asserts the
relation it relies on before using it: the cased name lowercases to the mind's
and is not equal to it.
`D1L5` folds case against the mind suite and `D1DL2` against the daemon suite,
so both fixture sets are load-bearing rather than only the one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The document test measures two real answers, one over the window and one
39,305 bytes under it. That gap is wide enough to hide two mutants, and Soul
found both alive: a gate measuring the payload string rather than the encoded
envelope, 231 bytes smaller, and a gate comparing `<` where the transport
compares `<=`. Neither is reachable by a test whose answers sit tens of
thousands of bytes from the edge.
The boundary is known exactly, so a test sits on it. A synthetic reply sized
to exactly `MAX_RESPONSE_BYTES` passes the gate unchanged and is accepted by a
hub `bind` configured; one byte more is refused by the gate, by name and with
both numbers, and refused by the transport with its own queue error. The
refusal is measured too -- 299 bytes, one packet -- so the answer that says an
answer did not fit always fits itself, and a one-byte reply after the accepted
one is refused, which shows the accepted answer filled the window it was
measured against. This is Soul's probe, with the assertions its `eprintln`s
were standing in for.
`D20L4` stops the gate one byte short of the limit and `D20L5` measures the
payload inside the envelope rather than the envelope. Both survived the suite
before this commit.
The session is settled before anything is sent, and the module documentation
now says why: the hub's own accept is unacknowledged reliable traffic, so a
session that has just connected holds 1023 packets rather than 1024 until the
client's acknowledgement arrives. Nothing in the loop depends on it -- on
loopback the acknowledgement precedes the first request -- but a boundary test
written without it measures 1023 and fails for the wrong reason.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`open_items` was the one read arm carrying no mutation entry. The reasoning
was that it raises no refusal of its own and its only failing paths are store
integrity inside the reader, which nothing on the daemon's surface can drive.
The second half was wrong, and Soul falsified it: a mind whose commit receipt
has been deleted still opens, because the opener reads the store's types,
epoch and identity and not its receipts, and then a read that joins a document
to its receipt refuses by name over the orphan. That store is reachable from
this crate's own surface.
Soul's probe lands as the test, unchanged in substance. It admits an identity,
drops the daemon, deletes the receipt row, reopens, and asserts the open-items
answer is the reader's own sentence whole:
`Unavailable { detail: "document epiphany.pipeline.instance.v1/yggdrasil:instance:self has no commit receipt" }`.
Reaching a row needs CultCache's `CacheBackingStore` and `DatabaseEntry`, which
`huginn-daemon` could not name. It does not gain a second `cultcache-rs` entry
for them: `huginn-mind::store` re-exports the two traits beside the store type
it already re-exports, for the reason stated there, so the workspace still pins
one revision through one dependency. That is a widening of `huginn-mind`'s
public surface by two re-exports, and no non-test code outside that crate
touches a row.
`D23` rewraps the arm's refusal as unavailable and `D23L` swallows it into an
empty open set, which is what a healthy campaign with nothing open answers.
Both survived the whole suite before this commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`D21` reverted the refusal to a failure envelope and had no weaker sibling.
`D21L` keeps the response schema and drops the client's correlation key, which
leaves a refusal the client cannot match to the request that earned it. It
dies, and the suite's 52 entries and M0 are green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A reply larger than one send could carry was handed to the hub anyway. The hub
refused it, the loop printed the error and served past, and the client waited
for an answer nothing would ever send. A real-sized read was lost in silence,
which is the one thing this surface promises not to do: a refusal is typed, and
that has to hold at every size.
`within_window` now encodes the reply and measures it against the window this
module configures, before a send it can already see will fail. Over the limit,
the answer becomes `MindRefusal::ResponseTooLarge { bytes, limit }` on the
response schema, reaching the client exactly as every other refusal does, with
the encoded size and the limit so a caller can narrow its own request. Nothing
is truncated, paginated or retried.
The arithmetic, measured rather than estimated. One `PipelineCutSpec` with every
list at the leaf's bound -- 256 file changes, 64 deletes, keeps, adds, builds,
tests, negative checks and operator checks, a full authority map and structural
delta -- is about 980,000 bytes of field content. As a `View` response, base64
in the operation envelope and MessagePack around that, it encodes to 1,315,551
bytes. The window is `MAX_PENDING_RELIABLE_PACKETS` (1024) times
`MAX_FRAGMENT_BYTES` (1200), so 1,228,800. A single document at the leaf's own
bounds therefore does not fit, by 86,751 bytes.
The window's values are unchanged by ruling: what a list read should return is
an open question, and raising the window would bake multi-megabyte answers in
as ordinary traffic before that question is answered. The module documentation
states 1,228,800 as the current bound rather than as a design target.
The test admits two cut specs into a real mind: one at 256 file changes and one
narrowed to 180, whose view encodes to 1,189,495 bytes. Over one loopback
session the query, the wide view and the open items are each refused by name,
and the narrow view is then delivered whole. Both halves are load-bearing: the
delivered answer sits 39,305 bytes under the limit, so `D22` and `D22L`, which
shrink the configured window, break it.
`D20` removes the gate, `D20L` measures against a larger number than the hub
carries, `D20L2` and `D20L3` collapse the two fields the refusal reports, `D21`
sends it as a failure envelope instead, and `D22`, `D22L` and `D22L2` shrink
each half of the window the gate trusts. All eight die. The whole suite, 51
entries and M0, is green.
Two reads pipelined on one session are still not both answered, and the module
documentation says so: the window is per session and counts what is
unacknowledged, so a second large reply sent before the first is acknowledged
fails to send even though each passes the gate alone. The gate measures one
answer against the window, not against what the session still has room for.
That is not fixed here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The dispatch was honest and nothing forbade it flattening a refusal the mind
raised: mutants that rewrapped a view refusal as unavailable, swallowed one
into an empty answer, and rewrapped a query refusal all survived both suites.
Flattening is the failure this cut exists to prevent, so it is now a test.
The new daemon test drives each read method into a refusal the method itself
raises, through `Daemon::handle`, with the shapes Soul reached over the wire:
a reference whose kind and id disagree for `view` and for a subject `history`,
and a semantic query for `query`, which is unavailable until Cut 11 wires an
index. Each must arrive as the mind spelled it. The same reads answer when
asked something answerable, so the arms are refusing on the mind's word rather
than on their own shape.
`D17`-`D19` rewrap each refusal and `D17L`-`D19L` swallow each into the empty
answer of its own kind.
`open_items` is not pinned and says so rather than carrying a weak entry: it
raises no refusal of its own, and its only failing paths are store integrity
inside the reader, which nothing on the daemon's surface can drive without a
corrupt store. A mutant against that arm could not fail.
The operation comparison is pinned in the same pass, because it shares this
entries file. Its mismatch case compared two words that differ under any
comparison, so a case-insensitive comparison survived; `Admit` now stands
beside `query` as a malformed envelope, and `D5L2` folds case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>