Add the AGPL-3.0 text as LICENSE and set the workspace license to
AGPL-3.0-only (rulings license-split, leaf-relicensed-agpl); the leaf's
provenance (Epiphany ef956865, PolyForm Noncommercial, relicensed by its sole
copyright holder 2026-10-09) is recorded in its Cargo.toml and README.
One [workspace.dependencies] entry per CultLib crate now owns the revision;
the crates inherit it. index.json lists Huginn's two wire schemas too, and the
catalogue test drops its huginn. prefix exclusion: every schema file is listed.
README and AGENTS stop naming Epiphany notes as the owner of the cut.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Package and lib become eureka-pipeline / eureka_pipeline; the crate joins the
workspace; huginn-mind's git dependency on GameCult/Epiphany is replaced by
the path, and Cargo.lock is regenerated (the Epiphany source entry is gone).
Every epiphany_pipeline mention in huginn-mind, huginn-daemon and eureka-state
is renamed. The schema-publication test reads ../../schemas/cultnet. The
leaf's license-file key (../LICENSE, which Huginn has no file for) is dropped.
Type ids, the epoch and every schema_id string are untouched. Schemas README
and the pipeline section of schemas/cultnet/README.md are rewritten for the
new home; README and AGENTS no longer say the leaf is pinned by git. New test:
the catalogue lists exactly the schema files beside it.
Not yet built or tested; verification follows on Yggdrasil.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
eureka-state on Windows could not reach the daemon: the cultnet-rs RUDP
client bound 127.0.0.1, and a loopback-bound socket cannot send to
10.77.0.1 (os error 10051). CultLib 8fc74c7 binds the unspecified address
for a remote endpoint. The daemon and the client move together, because
the ack cuts between the revs changed the RUDP handshake.
epiphany-pipeline moves to Epiphany 73edee5, which pins the same CultLib,
so one cultcache-rs is in the graph and the pipeline's CultCache types
stay Huginn's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MAX_DEFERRED_BODY_BYTES was the daemon's constant, but a client caps its
content fetch at the same number, and the client crate cannot depend on the
daemon. It moves to huginn-mind::wire next to the response schema it bounds;
the daemon imports it, the pin test stays in bodies.rs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
envelope.rs (encode/decode request and response, OperationFailure,
FAILURE_SCHEMA) leaves huginn-daemon for huginn-mind, next to the wire types
it wraps. The daemon imports it and no longer re-exports it. base64 moves
with it; the daemon keeps base64 because serve.rs measures and decodes
payloads. schemars leaves the daemon, whose only user was the envelope's
OperationFailure derive. The two envelope tests run unchanged from their new
home on huginn-mind fixtures.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two mutants survived Soul pass 4: dropping `self.settle()` from reconcile,
and dropping `!projector.stale()` from take_search. One test each.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Losing a collection the worker found whole at start-up is a rebuild. The
mutation that left Build::Unbuilt in place on a matching label survived,
because nothing observed the difference until the collection was lost.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test read the queued search's due moment through the sink's lock right
after asking, but the worker can take the search first: it failed under the
load of cargo-mutants' baseline run. The stamp is pinned without a worker in
the_sink_owns_the_searchs_deadline_and_tells_the_loop, which is the test that
owns it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
S1. serve_search absorbed the inbox and judged pending under one lock and
popped the search under a second, so an admission acknowledged before the
search was asked could be missed. The absorb, the judgement and the take are
now one critical section, and take_search never takes a search while the inbox
is non-empty or anything ahead of it is owed.
S2. The projector carries Build (Unbuilt, Built, Rebuilding). A collection made
again (label mismatch, or lost after it was once built) is Rebuilding until the
entries it owes are written; settle() is the one place that clears it, so a
failure mid-refill does not end the rebuild. Rebuilding reports Reconciling.
unsearchable stays the only refusal predicate.
S3. A mind whose index was never built is Unbuilt: its first build reports
Behind, so the search waits and answers whichever side looked first. Only a
rebuild of an index that exists refuses.
S5. ServeOptions::search_timeout is deleted. The sink owns the deadline
(Backoff::search_deadline, stamped on every search) and the serve loop asks
the daemon for it (IndexSink::search_deadline), so the loop's wait and the
worker's bound are one value. A mind with no identity and documents owed is
Reconciling, so a search is taken and refused rather than left for a spinning
worker.
S4. A test pins that a later call's bound shrinks as the search spends time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The refusal names the pending count; with every document written the count is
zero and the test proved nothing about it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Soul pass 2 follow-ups F1, F2, F4, F5 on the semantic index.
F1: every call a search makes (identity reads, label read, embed, query) is
bounded by what remains of one Budget, and the queued ticket carries the moment
SEARCH_DEADLINE after it was asked. The ports take the bound per call
(model_identity, describe, search); QUERY_EMBED_TIMEOUT is gone, since the
ticket's deadline is the one number. A hung identity read frees the worker
within the search's time.
F2: one refusal predicate. Behind is lag and no longer refuses; only
Reconciling, Failing and Refused do, for the sink and the projector alike. The
worker absorbs the inbox before taking a search and leaves the search queued
while the writes ahead of it are unfinished, so a client that admits and then
searches reads its own writes. An index being rebuilt is still refused.
F4: the model-changed refusal and the worker's failure answer to queued
searches name the state and the pending count.
F5: a search that fails for its own reasons no longer forgets the
reconciliation; only a changed model or label does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Every fixture pair differed in length, so a comparison of lengths, of first
bytes, or one gated on the declared name being the longer passed both suites.
The check was pinned by the fixtures' luck, not by the test.
Two foreign names now stand beside `thought-cage` in the mind test and the
daemon test: `yggdrasix`, which is `yggdrasil`'s own length and differs in its
last byte alone, and `yggdrasil-two`, which carries the whole name as its
prefix. The near name kills the three comparisons above; the prefixed one kills
a `starts_with`.
`D1L` is the loosening the brief asked for, comparing lengths, restored to that
shape; `D1L2` compares first bytes, `D1L3` runs the gate only when the declared
name is longer, `D1L4` asks whether one name begins with the other, and `D1DL`
runs the length comparison against the daemon suite so the daemon test's own
fixtures are load-bearing. The refusal-naming mutant that had taken `D1L`'s
place is kept as `D1N`; it attacks a weaker rule and is no longer standing in
for one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`eureka-cut10-mutations.psd1` carries thirty-one entries over sixteen rules:
each rule has a revert that removes it and, where the rule can be weakened
rather than removed, a loosening that leaves it standing but wrong. The
connection id and the startup order say plainly why they have only the one
mutation each. `D1` and `D1D` are one edit run against both suites, because
the instance check has one owner and two callers.
Three tests grew to hold their mutants. The instance test now admits a batch
whose documents name no instance, so nothing between the wire and A1 can
quietly rewrite the declaration into the mind's own. The malformed-envelope
test now sends a refused read through the whole answer path, so a refusal
must stay on the response schema. The startup test now holds the mind and the
port at once, so a startup that binds first reports the port rather than the
mind it never asked.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`huginn-daemon` becomes a library and a binary. `Daemon` holds one mind and
dispatches a `HuginnMindRequest` to the `Mind` method that answers it, with no
socket and no clock: `now` is the caller's. Every read that names an instance
is put to `Mind::require_instance` before the method runs, so ruling 14 is
checked once, by its owner, whichever side of the wire the request came from.
`IndexSink` is where Cut 11 attaches; its error is logged and the outcome is
what the mind decided.
`envelope` carries the vocabulary in CultNet's `cultnet.operation_request.v0`
and `cultnet.operation_response.v0` as base64 of named MessagePack, which is
what the C# reference and Eve's plugin ABI speak. The envelope's operation
string is checked against the decoded payload rather than trusted. A message
that does not decode is answered with a typed failure naming its code, and
reaches no mind.
`serve` binds one non-blocking socket on CultNet's operation connection id and
answers each frame on the session it arrived on. A refusal is an answer with a
`rejected` status, never a transport error. The two wire schemas go out on a
schema catalog request. A hostile datagram and a departed peer are logged and
served past. `startup` is the one place the mind opens and the socket binds,
in that order, so a mind that will not open leaves the port free.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three stub crates following Odin's layout: huginn-mind and eureka-state as
libraries, huginn-daemon as the binary. Edition, license and publish are
workspace-owned. .gitignore gains target/ and drops the four entries that
named build outputs no longer produced here.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>