52 KiB
Huginn wired in: cut map for Cuts 11-15
Date: 2026-09-29. Imagination output (Opus). Nothing here is committed, and no
repository or live service was written to. Self places this map. Its natural
home is a section of F:\Projects\Epiphany\notes\eureka-pipeline-state-cut.md
that replaces the old Cuts 11-15 (lines 5490-5853). Mark those lines as history
the same day, so that no reader sees two live designs.
Q-H1 reversed, 2026-09-30 (operator). Self laid out the five things 14-I still needs and recommended the interim unit. The operator: "So, what's blocking Huginn? I want us to be gathering evidence on Eureka ASAP" and then "Go for it." Q-H1 is now A. Cut 14 builds the interim unit as mapped, and its deletion line stays 14-I. Self deploys it under the operator's standing deployment authority, which replaces "the operator deploys" in Cut 14. Q-H2 has moved on: authority backups now run to Raven (weekly tier added 2026-09-30), so Cut 14's backup items apply under Q-H2 A. Cut 15 follows once 14 is live. The Q-H1 B text below is history.
Cut 15 merged, 2026-09-30 (Self). GameCult/Eureka main is at bdb9dfd (051e1fc, 2a0e9f7, 272f5ca,
f0ff484, bdb9dfd).
- Soul took three passes. The first found the Soul brief had stopped reading operator rulings (High). That is fixed: Soul claims against every cited ruling and every direction in force, independent of Hands' promises.
- Live checks passed against the mind.
eureka-statewas redeployed on CultLib8fc74c70, and checks 3 and 6 now pass from Starfire. - The substrate gaps are tabulated in
references/campaign-state.md, and each becomes afollow_upin the first campaign. - Operator, on moving to typed state: "how are we going to move to Huginn mid-campaign? Methinks we don't." Campaigns already in flight finish in prose. The first typed campaign starts fresh in a new session: Idunn as a watchdog that reports to the operator on Discord (store-recovery Q-R4).
Cut 14 status, 2026-09-30 (Self). Huginn is live on Yggdrasil: huginn.service on 10.77.0.1:17872/udp,
instance eureka, started 13:24 CEST.
- The release is
0c4e410(theeureka/memory-organhead), with binary sha256a02ae1f5.... - gamecult-ops
a0320e4/14df7ce, Huginn READMEdcb85de. The ufw rule admits Starfire only. - Checks 1, 2, 4, 5 (at the firewall counters) and 7 pass. Check 4 admitted the mind's identity document
eureka:instance:self(receiptmind-commit-379a2a34...), and it survived a restart. - Check 8 waits for the 04:00 CEST backup on 1 October. The map's freeze-loop script no longer exists. The
non-freezing state-backup producer covers
/var/lib/gamecult, so no freeze loop was added. - Check 3 fails, and Cut 15 is blocked on it.
eureka-stateon Windows cannot reach the daemon. The cultnet-rs client binds127.0.0.1by default (cultmesh.rs:256), and on Windows a loopback-bound socket cannot send to10.77.0.1(os error 10051). This is CultLib's defect, and it is fixed there (hands/rudp-client-bind). After that come Huginn's pin bump, the Windows rebuild ofeureka-state, and a rerun of checks 3 and 6. - Self default: the derived Qdrant collection
huginn_mind_eurekajoins the backup'sQDRANT_SKIP, because the collection is disposable and the mind file is the truth.
Operator rulings, 2026-09-29, each asked on its own:
- Q-H1: B, wait for Idunn. Cut 14's interim systemd unit is not
built. The organ deploys through Idunn after route continuity (Idunn
docs/route-continuity-cut.md) lands. Cut 14 is remapped as 14-I once that campaign settles. A Rust runtime-presence implementation shared with Idunn, rather than a third copy, is its prerequisite, and where it lives is a fork to bring then. Cuts 11a, 11b, 13a and 13b proceed. They run against a hand-started daemon in tests only. - Q-H2: C, no backup yet. Nothing on Yggdrasil is backed up today (the authority backup unit was never installed). That is recorded as a standing risk, not solved here.
- Q-H4: one mind, instance
eureka. The operator asked first: "Wouldn't a single instance be bad for concurrency? ... can multiple Eureka instances share a Mind on Huginn?" Self answered from the Body. The daemon is the mind's only writer, and any number of sessions are its clients. Racing admissions get typed refusals. Focus comes from scoped queries, not from mind size. Self's addition for Cut 13, from that exchange: receipts name the instance but not the session that admitted a batch. With several sessions sharing one mind, the admitting session is recorded on the receipt (declared attribution, like the instance). - Q-H5: park Cut 12, tag and note, not deleted.
- Q-H6: A, the hybrid, with a standing direction. The operator: "every bit of prose we're passing around should be viewed as a potential liability, especially if it's too big to just shove the whole thing into context. We should be looking for ways to turn what remains into typed state as well." Every Imagination pass looks for the next prose surface that could become typed (body facts, the model page, the subtraction ledger), and proposes it rather than adding prose.
Objective (operator, 2026-09-29): "Huginn wired in". Eureka's pipeline runs on Huginn's typed state. In scope: Cuts 11, 13, 14 and 15. Cut 12 is recommended for parking (see Q-H5). Out of scope: Cut 16 (the proof campaign), the Verse-presence cut (Q23 A), a public or stranger-ready skill, and the landing site.
What lands ahead of these cuts (mapped elsewhere, not re-mapped here):
- BP-1: CultLib content plane. It is in Hands on
cultmesh/rust-content-plane, with three commits pushed. - BP-2: the pin move.
- RS-3:
Mind::query(selection, semantic). It addscultnet-rstohuginn-mind, and deletesPipelineQueryandQUERY_LIMIT_MAX. - BP-3:
HuginnMindResponse::Deferred,MAX_DEFERRED_BODY_BYTES, and a deferral budget of 256 MiB.
Every Huginn anchor below is against eureka/memory-organ at 8c5f784, which
is clean. RS-3 and BP-3 move query.rs, wire.rs and serve.rs, so each
brief re-takes its anchors after they land.
Pins
| Repo | Branch | HEAD | Used for |
|---|---|---|---|
| Huginn | eureka/memory-organ |
8c5f784 |
Cuts 11 and 13 |
| Epiphany | codex/eureka-pipeline-state |
4f784d04 |
the leaf's text fields (epiphany-pipeline/src/lib.rs:510-578), read only |
| Eureka skill | main |
19487f0 |
Cut 15 (~/.claude/skills/eureka) |
| gamecult-ops | main |
8554e60 |
Cut 14 |
| Idunn | (current) | 5bbb461 |
read only. The route-continuity target and cut map are the dependency for 14-I |
Probe facts (run 2026-09-29, read-only except as noted)
- P1. Qdrant on Yggdrasil is
qdrant 1.17.1, on127.0.0.1:6333.- It holds exactly two collections, both voidbot's:
voidbot_repository_source_chunks: 116,877 points;voidbot_discord_history_chunks: 40,788 points.
- Both are 1,024-dimensional,
Cosine,on_disk: true, with keyword payload indexes (sourceId,corpusKind,repoName,pathPrefixes, …). config.metadatais null on both. They were restored, not created by voidbot's code. So voidbot's embedder-compatibility check (VoidBot/packages/rag/src/qdrant-vector-store.ts:458-477) has nothing to compare against in production.
- It holds exactly two collections, both voidbot's:
- P2. Raven's Ollama is live, and voidbot already uses it.
/srv/voidbot/retrieval/voidbot.envreadsRAG_OLLAMA_BASE_URL=http://10.77.0.4:11434andRAG_OLLAMA_MODEL=qwen3-embedding:0.6b. So the 2026-09-26 cutover has happened.gamecult-ops/inventory.md:669andrunbooks/voidbot-retrieval-recovery-yggdrasil.md:40-62still name Nightwing. They are stale.- Nightwing (
10.77.0.3:11434) did not answer. - Epiphany's old Yggdrasil-local endpoint (
10.77.0.1:11435) did not answer.
- P3. What Raven serves. Ollama
0.21.1. Tags:qwen3-embedding:0.6b: digestac6da0dfba84…, Q8_0;bge-m3:latest.- One embed call per model, from Yggdrasil, returned 1,024 dimensions for both. Each call took 5-6 s cold.
/api/psreports a context of 4,096. - Side effect: this probe loaded
bge-m3into Raven's VRAM until its 5-minute keep-alive expired.
- P4. How voidbot uses Qdrant, which is the convention to reuse (
qdrant-vector-store.ts):- The point id is the first 16 bytes of
sha256(chunk id), shaped into a v5-style UUID (:264-280). The real id travels in the payload. - Collections are created with
metadata {managedBy, corpusKind, embedderId, vectorLength, schemaVersion}(:203-216). embedderIdisollama:<baseUrl>:<model>(ollama-embedder.ts:29-31). Moving the host therefore reads as a model change, which is the hazard the 2026-09-26 memory warns about. Huginn keys compatibility on model name plus digest, not on the URL.- Query text is prefixed
Instruct: …\nQuery: …when an instruction is configured (:42-48).
- The point id is the first 16 bytes of
- P5. WireGuard exists.
- Starfire has the adapter
wg-gamecult-starfireat10.77.0.2, and it is Up. - Yggdrasil has
wg-gamecultat10.77.0.1/24, reachable from Starfire. 10.77.0.1:17872/udpis unbound. Ports in use today: 17871 (Odin, via nginx), 17873, 17876 and 17890 (Idunn's host-actuator hub, which is not in R21's list).- ufw admits UDP on wg only per port: 3075, 17871, 17876 (from
10.77.0.2only) and 17890. 17872 needs a rule. - Starfire's 17877 RUDP requester is not listening today. So a Rust RUDP client on Windows over WireGuard has a precedent, but it is not proven live right now.
- Starfire has the adapter
- P6.
crates/eureka-stateis an empty stub:src/lib.rshas 0 lines, andCargo.tomlhas no dependencies. - P7. The daemon serves one mind per process.
serve.rs:91-111takes exactly--state-root,--instanceand--bind.Mind::opentakes a lifetime-exclusive redb lock (mind.rs:127-139).IndexSink::committedruns synchronously insideDaemon::handle, before the reply (daemon.rs:74-83).runis a single-threaded poll loop (serve.rs:257-297). Any blocking call made there stalls every session.
- P8. The hand-off already works one side at a time. Admission derives:
- the source side's withdrawal when
from_instance == mind(admission.rs:331-346); - the target side's stewardship when
to_instance == mind(:347-353). So a hand-off is two ordinary admissions, one per mind.
- the source side's withdrawal when
- P9. Idunn v2's contract for a routed stateful target, read from source:
- The state slot kinds are only
cultcache-fileandexternal-file(Idunn/src/deployment.rs:182-187). There is nocultcache-directory. - A dependency on non-Idunn infrastructure is
kind = "external-operator-binding". It is satisfied by the binding'sexternal_capabilities(deployment.rs:1359-1383,deployment_plan.rs:146-249).shared-infrastructureneeds a managed provider with Odin correlation. - The route challenge for
rudpis a CultNetSnapshotRequestforgamecult.runtime_presence_health.v2, keyed by the target (drivers.rs:5524-5545). The service must answer it with signed presence, signed with the Idunn activation credential. It must also adopt the process write lease. - Huginn implements none of this.
- No shared Rust implementation exists.
cultnet-rshas the contracts and signers (runtime_authority_contracts.rs, 3,309 lines). Each Rust service writes its own participant: Ghostlight'sidunn_health.rsis 2,001 lines, and Odin has its own. - Unit names are
idunn-<target>-<sha256>per incarnation. - There are 391 failed
idunn-*transient units on Yggdrasil today.
- The state slot kinds are only
- P10. No backup runs on Yggdrasil.
gamecult-authority-backup.serviceis not installed. The only backup timer isdpkg-db-backup.timer.- The script
scripts/backup-gamecult-authority-yggdrasil.sh:74-76would refuse at its first step anyway: it freezesepiphany.serviceandepiphany-swarm.service, which are absent (freeze_unitreturns 1 when a unit is inactive). - Its path list does not name any Idunn target's state root either, including
odinandstreampixels-service.
- P11. How Claude Code reaches MCP servers today (
~/.claude.json):voidbotat:17875is the Yggdrasil-hosted MCP, reached through the SSH tunnel (start-yggdrasil-tunnel.ps1:24).aquasynth_dingsat:17878runs locally.- The target's "Shape decided" says
eureka-stateis a local stdio server. That governs here. The HTTP convention recorded in memory is not a ruling.
Not probed:
- whether
cultnet-rs's RUDP client works on Windows; - whether Claude Code accepts a tool schema that
$refs a remote$id; - whether Qdrant accepts
.in a collection name; - whether
ureqwithout TLS covers both adapters; - which connection id Idunn's RUDP challenge uses.
Each is assigned to a cut's first step below.
Standing rulings, checked against the maps
| Ruling | Where it is recorded | Holds? |
|---|---|---|
| An instance owns its mind; a service owns the state | target 14 and 15; map rulings 14 and 15 | yes |
| The organ depends on Qdrant directly, not on voidbot | target 16; map 16 | yes |
| The organ shares voidbot's Qdrant, with the coupling declared | map 21 (Q9 A) | yes. "Declared" means an Idunn dependency, which the interim deploy (Q-H1 A) cannot express. The runbook states the coupling until 14-I. |
| Identity is declared, not authenticated | map 18 (Q6 A) | yes |
| The workstation reaches the organ over WireGuard | map 19 (Q7 A) | yes. P5 confirms the mesh; the ufw rule is Cut 14's. |
| Eureka has no Persona | target 6; map 9 | yes |
| Embeddings: Raven's Ollama (operator, 2026-09-26, for voidbot) | memory nightwing-embedding-migration-2026-09-26.md |
the target disagrees. Target ruling 16 still says "Epiphany's own service embeds against a Yggdrasil-local endpoint … only voidbot uses Nightwing". Both endpoints are dead (P2). Self sweeps that sentence. Huginn uses the endpoint voidbot uses, with the same model. |
open_items and history are deleted, not kept as presets |
read-side map (2026-09-17) | yes. Cut 13's old seven tools shrink to four. |
Q13 A: the client depends on huginn-mind for types only |
map ~3931 | yes |
| No committed mutation harness | operator, 2026-09-22 | yes. Old Cut 11 and 12's "Mutations:" lines are void. Soul runs cargo-mutants on each diff. |
0b. Model page
One row per persistent or long-lived kind these cuts touch. A bold cell is empty or disputed.
| Kind | What names it | What happens to it over time | Who decides |
|---|---|---|---|
| Index point: one per indexable document | A v5-shaped UUID from sha256(document id) (P4's function, reused). The payload holds doc_id, kind, root, ordinal and text_sha256. |
Written after the document's batch commits, off the serve loop. Never updated, because documents are immutable. Never deleted, except when its collection is rebuilt. A missing point is recreated by reconciliation. | Only the daemon's index worker writes one. huginn-mind::index_text decides what the text is. Qdrant decides nothing. |
| Collection, one per mind | huginn_mind_<instance>. Its metadata is {managed_by: "huginn", instance, model, model_digest, dimensions, index_text_version}. |
Created on the first reconciliation. Rebuilt whole when its metadata disagrees with the running configuration. A collection whose mind is retired has no owner. With one instance this is a recorded follow-up, not a cut. | The daemon serving that instance. It never touches a collection whose managed_by or instance is not its own. |
| Index text version | huginn_mind::index::INDEX_TEXT_VERSION |
Bumped whenever index_text changes. A bump makes every collection disagree, so each is rebuilt. |
huginn-mind |
| Embedding model binding | Model name plus the digest from /api/tags. Never the base URL (P4). |
Changes when the operator changes the configuration, or when Raven re-pulls the model and the digest changes. Either one rebuilds the collection. | The operator, through the unit's arguments. The daemon verifies the binding at each reconciliation. |
| Index health (behind or unreachable) | none: in-memory in the worker | Lost on restart, and recomputed by reconciliation at startup. Never persisted, never written to the mind. | The worker |
| Pending semantic reply | (session, message_id) in the serve loop |
Lives until the worker answers or the session times out. Lost on restart, and the client asks again. | serve |
| Eureka instance | A slug. The operator names it (Q-H4). | Created by the first admission of its instance document, which Self writes at the first campaign. Never renamed: a rename is a new mind. |
The operator names it. Admission enforces it (A1, require_instance). |
| MCP registration | eureka-state at user scope in ~/.claude.json, with the environment variables EUREKA_INSTANCE and HUGINN_ENDPOINT |
One stdio child per Claude Code session. It dies with the session and holds nothing. | The operator, because it is configuration |
| Deferred body (BP-3), client side | The manifest's contentHash |
Fetched and verified per call, then dropped. Never written by eureka-state. |
cultnet_rs::fetch_content owns verification. |
| Deployed daemon identity | Interim (Q-H1 A): the systemd unit huginn.service on Yggdrasil, with runtime id huginn-<slug> (daemon.rs:54-56). Later: idunn-huginn-<sha>. |
Interim: systemd restarts the unit, which is continuity. The operator installs a new binary, which is deployment, and no brake gates it (Q-H1). | Interim: the operator. After 14-I: Idunn. |
| Mind store | /var/lib/gamecult/huginn/minds/<slug>/mind.redb |
Preserved forever. It is the only truth. No backup owner runs (P10), so this is Q-H2. | The daemon is the single writer, through the redb lock. |
| Firewall admission | A ufw rule: 10.77.0.1 17872/udp on wg-gamecult from 10.77.0.2 |
Installed once. It dies in 14-I if Idunn's route owns it. | Interim: the operator |
| Hand-off / imported mind | The leaf hand_off key |
Admitted separately into each mind (P8). Import has no consumer (Q-H5). | huginn-mind admission |
Operator questions (one batch)
Q-H1. How does Huginn get onto Yggdrasil this week?
- A. An interim operator-installed systemd unit, retired into Idunn by a named later cut (14-I).
- The operator builds the binary in the pinned Rust image, at an exact revision, on Yggdrasil.
- It binds
10.77.0.1:17872directly, with no nginx in the path. - The state root is chosen now so that 14-I is a unit swap and no data moves.
- Costs:
- it contradicts the old authority map's "no operator script deploys Huginn directly";
- it has no deployment brake;
- the Qdrant coupling is written in a runbook rather than declared to Idunn.
- B. Wait for Idunn. Huginn is deployed only through Idunn, after two things land: route continuity's route-proof class (its Q1 b) and a shared Rust runtime participant in
cultnet-rs. "Wired in" does not close this week. - C. Idunn now, against today's contract.
- Huginn writes a third copy of the service-side presence, activation and lease code (Ghostlight's copy is 2,001 lines).
- It also has to answer the route challenge and pass Odin-correlated readiness.
- Its RUDP route goes through nginx UDP, whose reload loop is exactly what route continuity exists to fix. Every reload breaks RUDP sessions (route-continuity target).
- All of this is reworked when that campaign lands.
- Recommended: A, with 14-I named, dated and blocked on route continuity plus the shared participant.
- Neither B nor C gives a working organ on a path that will survive.
- C would build the thing route continuity is deleting.
- A's authority gap is real. The operator, not a map, should accept it.
- Depends on it: Cut 14's whole shape, whether this week closes, and whether 14-I exists.
Q-H2. Who backs up the mind? The target requires a named backup owner. None runs (P10).
- A. Revive the authority backup.
- Edit the freeze list: drop the two absent Epiphany units and add
huginn.service. - Add
var/lib/gamecult/huginnto the path list. - Install the timer.
- Side effect: daily backups resume for voidbot, Bifrost and Idunn configuration, including a daily pause of five containers.
- Edit the freeze list: drop the two absent Epiphany units and add
- B. A Huginn-only timer: freeze
huginn.service, tarminds/, thaw. This makes a second backup mechanism. - C. Ship with no backup, and record it as a blocking follow-up.
- Recommended: A. One owner of backup, and the script is already that owner. It is simply not running. The operator must accept the side effects, and should know that Idunn targets' state (
odin,streampixels-service) is in no backup either. - Depends on it: Cut 14's gamecult-ops diff, and one verification step.
Q-H4. What is the Eureka instance called, and how many are there?
- A. One instance for all Claude Code Eureka work, for example
eureka, with hoststarfire. - B. One per workstation or per operator, for example
metacrat.starfire. - Recommended: A. Ruling 14 lets one instance steward several repos. Stewardship, not the instance, is what separates repos.
- Depends on it: the unit's
--instance, the state path, the collection name, and the MCP environment. It is cheap now and a new mind later.
Q-H5. Park Cut 12? (the consumer audit)
- Nothing that "wired in" needs calls a cross-mind hand-off or an import:
- there is one instance (Q-H4);
- no second mind exists;
- Epiphany's adoption is the second campaign.
- Old Cut 12's
hand_off(&mut Mind from, &mut Mind to)cannot be built as specified:- each mind is locked by its own daemon process (P7);
- "both minds or neither" would need a cross-process protocol.
- The hand-off already works as two ordinary admissions, one per mind, each derived and idempotent (P8).
- Recommended: park it. The map keeps a note: the hand-off is two admissions, sender first. Import waits for a consumer. No tag is needed, because no code exists.
- Depends on it: whether Cut 12 is built at all.
Q-H6. What does Eureka keep in typed state, and what stays in prose?
- A. A hybrid.
- Typed documents hold the questions, rulings,
cut_spec(Hands' contract),cut_report, verdicts, findings, follow-ups and resolutions. - The status header becomes a query.
- The committed map keeps only what has no kind: body facts, the model page, rationale, and the subtraction ledger narrative.
- Per-cut specs live only as
cut_spec, so no spec exists twice.
- Typed documents hold the questions, rulings,
- B. Typed state for records only. Specs stay in the prose map, and
cut_specgoes unused. - C. Retire the markdown map entirely (old Cut 15).
- Recommended: A.
- B leaves the kind the leaf spent most of its bounds on without a consumer.
- C discards body facts and the model page, which no kind carries. Step 0b is what stopped re-cutting.
- Depends on it: Cut 15's text, and the output format of every future Imagination pass, this one included.
Cut order
[BP-1 → BP-2 → RS-3 → BP-3] (in flight, other maps)
│
BP-2 ──► 11a Huginn: the index write side (text, adapters, worker, reconcile)
│ (after BP-2; may land before or after RS-3, serially on the branch)
▼
BP-3 ──► 11b Huginn: semantic read (rank the hits through the one evaluator; non-blocking)
▼
13a Huginn: envelope moves to huginn-mind; eureka-state client core
▼
13b Huginn: the MCP surface, Windows build and install
│
11a ──► 14 gamecult-ops: the interim unit, ufw, runbook, backup (Q-H1 A, Q-H2); the operator deploys
▼
15 Eureka skill: typed-state wiring (Q-H6); after 13b is registered and 14 is live
---
12 parked (Q-H5) 14-I Idunn path, blocked (see the end)
Cut 11a. The index write side
- Repo/branch: Huginn,
eureka/memory-organ, after BP-2. The worktree is named by Self. - First: Hands checks, in
ureq3's own Cargo manifest, thatdefault-features = falsegives plain HTTP with no TLS. If it does not, Hands usesreqwestwithblockingand no TLS, and reports which. - Deletes first:
NoIndex(daemon.rs:20-27) as the binary's index. It survives only in tests, or goes if the new worker's mock serves them.- The "No crate opens that connection yet" line in
README.md.
Adds, huginn-mind/src/index.rs. This is pure: no network and no JSON.
-
pub const INDEX_TEXT_VERSION: u32. -
pub struct IndexEntry { id: PipelineRef, text: String, text_sha256: String, ordinal: u64 }. -
pub fn index_text(&PipelineDocument) -> Option<String>. It reads the leaf's fields (lib.rs:510-578), joined by newlines and bounded atINDEX_TEXT_MAX_BYTES(16 KiB, cut at a char boundary):Kind Fields campaign titletarget invariants[].statement,not_in_scope,canonical_implementationsquestion title,question,options[].textruling title,ruling,operator_quotecut_spec title,first,adds,keeps_movescut_report commits[].subject,deviations(what,why),undone,promises[].textverdict claims[].claimfinding claim,failure_scenario,evidence[].resultfollow_up item,why_it_can_waitresolution rationale, withdrawn ones included (the Q17 B obligation)hand_off reasoninstance, stewardship none -
impl Mind { pub fn index_entries(&self, only: Option<&[PipelineRef]>) -> Vec<IndexEntry> }. The ordinal comes from the writing receipt (RS-1). -
Owner:
huginn-mind, because "which fields are text" is a rule about the leaf's documents, and RS-3 names Cut 11 its owner.- Live consumer: the daemon's worker.
- Protected invariant: the text is derived, never stored, and never includes receipts, provenance or status.
- Why no existing owner serves: the leaf owns shape, not retrieval text, and the daemon owns no rule.
Adds, huginn-daemon/src/index/. Three files: mod.rs, ollama.rs, qdrant.rs. The daemon depends on ureq (see First) and serde_json, and serde_json is used only in the two adapter files: that is the xenos boundary.
-
Ports, which are narrow traits so tests mock them:
Embedder { model_identity() -> ModelIdentity{name, digest, dimensions}; embed(&[String]) -> Vec<Vec<f32>> };VectorIndex { describe(name) -> Option<CollectionMeta>; recreate(name, meta); ids(name) -> BTreeSet<String>; upsert(name, points) }.
searchis 11b's. -
The worker is one thread that owns both ports and receives jobs over a channel.
IndexSink::committedbuilds the entries (local and cheap) and only enqueues them. It never blocks and never waits on the network. This is the fix for P7.- At startup, one reconciliation:
- read the model identity;
describethe collection;- if it is absent, or its metadata disagrees,
recreateit; - if the metadata names another
managed_byorinstance, refuse and log, and never recreate; - take
ids, subtract, then embed and upsert what is missing, in batches of 32, withwait=true.
- On failure: log, keep the failed entries in memory, and retry with a backoff that starts at 30 s and is capped at 10 min.
-
Options:
--qdrant-url,--ollama-urland--embedding-modeljoinparse_options(serve.rs:96). All three are required, so there is no index-less mode. An unreachable index is a degraded projection, not a configuration. -
The query instruction constant for qwen3 lives beside the Ollama adapter. 11b uses it.
Authority map.
- Owner: the typed mind owns truth, and the worker owns the projection.
- Inputs:
IndexEntrys built byhuginn-mind, and the configuration. - Outputs: Qdrant points and collection metadata.
- Derived state: the whole collection and the worker's in-memory health.
- Forbidden writers: nothing else writes
huginn_mind_*; voidbot never does; admission never waits on the index; the mind never stores index state; there is nopending_indexdocument (old Cut 11 is superseded). - Shared paths:
IndexSink::committed(live writes) and startup reconciliation share one embed-and-upsert routine. - Deletion line:
NoIndexleaves the binary.
Verification (Yggdrasil, stopgap, Rust image): ygg-verify.sh F:/Projects/Huginn <sha> rust 'cargo test --locked --workspace', then cargo mutants --in-diff on the cut's range.
| Test (behaviour) | Rule |
|---|---|
an_admission_replies_while_the_embedder_is_blocked (the mock embedder waits on a barrier; handle returns Committed before the barrier is released) |
The index never delays or refuses admission |
reconcile_indexes_exactly_the_indexable_documents_the_collection_lacks |
The projection converges by set difference |
a_model_digest_change_rebuilds_the_collection (same name, different digest) |
Compatibility is keyed on model and digest, never on the URL. At least one fixture pair must differ only in URL and expect no rebuild. |
a_collection_owned_by_another_instance_is_never_recreated |
No cross-mind writes |
index_text_ignores_admission_facts_and_indexes_withdrawn_resolutions |
Text is derived from the document alone, and Q17's obligation holds |
point_ids_are_a_pure_function_of_the_document_id (a fixed vector, computed by P4's algorithm) |
Stable ids across restarts, with voidbot's convention |
- Negative greps:
rg -n "ureq|reqwest|serde_json|qdrant|ollama" crates/huginn-mind/srcis empty;rg -n "thread::spawn" crates/huginn-daemon/srchits onlyindex/.
- Live adapter check (operator-approved, because it writes to the shared Qdrant): a scratch instance
verify.<rand>on Yggdrasil admits two documents; its collection appears with P1-style parameters and the metadata above; then the collection is deleted by hand.
Estimate: about 650 lines added, most of them tests. +ureq, +serde_json (daemon only). −NoIndex from the binary.
Build budget: the Huginn workspace, on Yggdrasil only, with one release build for the live check.
Hands context: about 150k tokens: daemon.rs, serve.rs and mind.rs headers plus the new code.
Cut 11b. Semantic read
Added 2026-09-29 (Self), from Soul's pass on 11a:
-
The mind judges every hit. The index never deletes points. Withdrawn and superseded documents are indexed on purpose, and point payloads carry no status. So Qdrant supplies candidate ids and scores, and every hit is joined back through the mind: ids the mind doesn't hold are dropped (a restored mind, or a second store), and the rest are filtered by RS-3's in-force derivation, through selection. A hit is never shown on the index's word alone.
-
The effective embedding bound is the model's context, not 16 KiB.
qwen3-embedding:0.6btakes 4096 tokens and Ollama truncates beyond that silently. Long documents rank on their first ~4k tokens. -
Model identity is re-checked on each flush. A model re-pulled with the same dimensions and a new digest must not mix vectors until a restart. An idle mind whose collection is lost also stays
Currentuntil the next upsert fails. Found by Soul on 11a's closing batch; 11b owns both. -
11a merged into
eureka/memory-organ. It was: Soul closed it on conditions. A fix batch is in Hands: pin the backoff, reconcile again after any failure, label the collection with the mind's identity, and merge BP-3. -
Repo/branch: Huginn, after RS-3 and BP-3. Anchors are re-taken on their tip.
-
Deletes first:
- the refusal "semantic query: the index is not wired (Cut 11)", at both sites (
query.rs:296-299, and the daemon test atdaemon.rs:534-541, re-taken); - the test
semantic_query_refuses_typed_until_wired. Its rule dies; the replacements are below.
- the refusal "semantic query: the index is not wired (Cut 11)", at both sites (
Adds.
huginn-mind:Mind::rank(&self, selection: &Selection, hits: &[(PipelineRef, f32)]) -> Result<PipelineSelectionPage, MindRefusal>. Its rules:- A selection with a
cursor, together withsemantic, is refusedSelectionInvalid(field: "cursor"), because semantic answers are not pageable. - The rows are restricted to hit ids present in the image as of
head. A hit with no document is dropped and logged by the caller, never fabricated. - RS-3's own path runs:
cultnet_rs::validate, the value door, thenselectover the restricted rows. The one evaluator decides membership. Qdrant never filters. - Order is score descending, with ties broken by ordinal.
- Truncate to
limit. next: None.
- A selection with a
huginn-daemon:- A
Querywithsemanticis not answered inline.servestores(session, message_id, request)and sends aSearch { text, top_k }job to the worker. - The worker embeds with the query instruction and searches the collection, with no filter and an oversample of
min(top_k × 4, 200). - The loop drains worker replies on each poll, calls
rank, and replies on the stored session. - When the worker is unhealthy, or the call fails:
Refused(Unavailable { detail }), and the detail names the endpoint.
- A
VectorIndexgainssearch.
Authority map: the owner of membership is rank, over RS-3's evaluator. Qdrant supplies only a candidate set and scores. Forbidden: lowering a FieldPredicate into a Qdrant filter, because that would be a second evaluator; RS-3 §8 had left this to Cut 11, and this cut refuses it.
Verification:
| Test | Rule |
|---|---|
semantic_hits_pass_through_the_selections_own_predicates (a hit of an excluded kind is absent) |
One evaluator |
semantic_results_are_ordered_by_score_not_ordinal |
Ranking |
a_hit_without_a_document_is_dropped |
Qdrant is not truth |
an_unreachable_embedder_is_refused_unavailable_never_an_empty_page |
Honest availability |
another_session_is_answered_while_a_semantic_search_is_outstanding (blocked mock embedder) |
The loop never waits on the network |
cursor_with_semantic_is_refused |
Not pageable |
- Live (operator-approved): on the scratch instance, a paraphrase query returns a document that shares no exact words with it, and the hit resolves through the same page type.
Estimate: about 350 lines added. No new dependencies.
Hands context: about 150k tokens.
Cut 13a. The envelope moves to its owner; the client core
- Repo/branch: Huginn, after 11b.
- Moves (ownership only; one commit that builds on its own):
huginn-daemon/src/envelope.rs(223 lines) moves tohuginn-mind/src/envelope.rs:encode_request,decode_request, the response pair,OperationFailureandFAILURE_SCHEMA.huginn-mindgainsbase64. It already hascultnet-rsthrough RS-3.- The daemon imports the module from
huginn-mind. - Rule: one spelling of the operation envelope. The client and the daemon cannot drift apart.
- Verified by the daemon's existing envelope tests, unchanged, run from their new home.
- Adds,
crates/eureka-state(library part).Cargo.toml:huginn-mind(types, envelope),cultnet-rs,anyhow,serde,serde_json.HuginnClient { endpoint: SocketAddr, instance: Slug, timeout }:- its one method is
call(HuginnMindRequest) -> Result<HuginnMindResponse, ClientError>; - the transport is
CultNetRudpSocketTransportConnectiononCULTNET_OPERATION_CONNECTION_ID, channelschema, as the daemon expects (serve.rs:283); - it resolves
Deferredthroughcultnet_rs::fetch_content, capped at BP-3'sMAX_DEFERRED_BODY_BYTES(imported, not copied); ClientError::Unavailable { endpoint, detail }is the only transport error.
- its one method is
- Owner: none over state; the client is a transport shim.
- Forbidden: opening a store, calling
Mind::open,admitoradmit_prepared, deriving status, or writing any file.
Verification (Yggdrasil):
| Test | Rule |
|---|---|
every_operation_round_trips_against_a_live_daemon (an in-process daemon on 127.0.0.1:0 with a temp mind) |
Pipeline smoke |
a_closed_port_is_unavailable_naming_the_endpoint_within_the_timeout |
Loud unavailability. The timeout is a function of the configured value, so probe two values. |
a_deferred_answer_resolves_to_the_same_page_as_a_direct_one |
BP-3's client side |
a_foreign_instance_is_an_answer_not_an_error |
Refusals are data |
- Negative grep:
rg -n "Mind::open|\.admit\(|admit_prepared|OwnedRedb|std::fs::(write|create)" crates/eureka-state/srcis empty.
Estimate: about 300 lines added. The move is net 0.
Hands context: about 120k tokens.
Cut 13b. The MCP surface, the build and the install
- Landed before it (2026-09-30, Self): Cut 11b and its follow-ups merged to
eureka/memory-organate3e0c38andff39d80. Cut 13a merged at33641f4(Soul: no blocker; fixes at3dc50ee). 13a's client now has two errors:ClientError::Unavailable(transport, timeout, out-of-protocol answer) andClientError::Rejected { endpoint, code, detail }(an envelope-levelOperationFailure). Itstimeoutbounds the whole call; answers are checked against the request's id and operation; every call disconnects its session. 13b mapsRejectedlikeUnavailable: a result withisError: trueand the typed body, so an MCP caller can tell a permanent mismatch from a down daemon. - Built (2026-09-30):
hands/cut13bat120e0a6, green on Yggdrasil; Soul pass running. Windows binaryC:\Users\Meta\.eureka\bin\eureka-state.exe(from63ac503). Registered by the operator (user scope), 2026-09-30. In Windows PowerShell the documented formclaude mcp add ... -e K=V -- <exe>fails with "missing required argument 'commandOrUrl'": the variadic-eswallows the name and path. The form that worked puts positionals first:claude mcp add eureka-state <exe> --scope user -e EUREKA_INSTANCE=eureka -e HUGINN_ENDPOINT=rudp://10.77.0.1:17872.whoamianswersreachable: falseuntil Cut 14. - Repo/branch: Huginn, after 13a.
- First:
- Hands builds a 20-line scratch rmcp server whose tool schema carries a
$defs-internal$ref, and confirmsclaude mcplists and calls it. It is not committed. - Hands confirms the
claude mcp addsyntax fromclaude mcp add --help.
- Hands builds a 20-line scratch rmcp server whose tool schema carries a
- Adds.
-
The binary
eureka-state(src/main.rs) usesrmcp = "2.2.0", the version Epiphany's lockfile already resolves, withserver,macrosandtransport-io, plustokio(rt,macros,io-std) andschemars. -
Configuration comes from the environment:
EUREKA_INSTANCEandHUGINN_ENDPOINT(rudp://10.77.0.1:17872). Missing or invalid configuration does not stop startup.whoamireports it, and every other tool refuses it, typed. An MCP server that dies at startup vanishes silently from Claude Code. -
Four tools, 1:1 with the wire. The old
open_items,rulings_in_force,stewardshipandhistorytools are deleted designs. Their recipes move to the skill (Cut 15).Tool Input Output whoami{}{ instance, endpoint, reachable, status: Option<MindStatus> }admit{ faculty, agent, session, documents }PipelineAdmissionOutcome.instanceandprovenance.tool = "eureka-state"are filled in by the server, never taken from input.view{ id }Option<PipelineDocumentView>query{ selection, semantic? }PipelineSelectionPage -
Refusals are successful tool results carrying the typed refusal.
Unavailableis a result withisError: trueand the typed body. Malformed input isinvalid_params. -
Input schemas are self-contained: schemars output with the CultLib selection schema bundled into
$defs, and no remote$idreference.
-
Verification.
- On Yggdrasil, a JSON-RPC exchange piped into the Linux build:
initialize,notifications/initialized, thentools/list, which lists 4 tools and contains no$refoutside#/$defs. Thenwhoamiagainst a closed port, which givesreachable: falseand no JSON-RPC error. - Test
admit_ignores_any_instance_in_input. The rule: declared identity is configuration, not a per-call argument. - Windows (Starfire; Windows-only work, one job, nothing else running):
cargo install --locked --path crates/eureka-state --root C:\Users\Meta\.eureka. Hands reports the binary path and the build log's target directory. - Registration is the operator's:
claude mcp add --scope user eureka-state -e EUREKA_INSTANCE=<Q-H4> -e HUGINN_ENDPOINT=rudp://10.77.0.1:17872 -- C:\Users\Meta\.eureka\bin\eureka-state.exe, in the syntax confirmed by First. - Operator only, after Cut 14:
whoamifrom a Claude Code session answersreachable: true. This is the only proof of the Windows RUDP path over WireGuard.
Estimate: about 400 lines added. +1 binary, +rmcp, +tokio.
Hands context: about 180k tokens. rmcp's API is the unknown.
Cut 14. Interim deployment (under Q-H1 A and Q-H2 A)
- Repos: gamecult-ops
main, plus one line in Huginn'sREADME.mdfor the run command. Depends on 11a, because the options exist from then. - Deletes first: from
scripts/backup-gamecult-authority-yggdrasil.sh:74-76, the absentepiphany-swarm.serviceandepiphany.servicein the freeze loop (under Q-H2 A).
Adds, gamecult-ops.
-
systemd/huginn.service:User=huginn, with a system user created by the runbook;ExecStart=/opt/gamecult/huginn/current/huginn-daemon --state-root /var/lib/gamecult/huginn --instance <Q-H4> --bind 10.77.0.1:17872 --qdrant-url http://127.0.0.1:6333 --ollama-url http://10.77.0.4:11434 --embedding-model qwen3-embedding:0.6b;Restart=on-failure;After=/Requires=wg-quick@wg-gamecult.service, because the bind needs the interface;- no ordering on Qdrant or Raven, because the index is a projection;
StateDirectory/ReadWritePathslimited to/var/lib/gamecult/huginn;ProtectSystem=strict;MemoryMax=768M, which covers BP-3's 256 MiB deferral budget plus the mind.
Hands confirms the WireGuard unit's real name with
systemctl list-unitsbefore writingRequires=. -
runbooks/huginn-yggdrasil.md:- Authority: the mind file is truth; the collection is disposable; Qdrant is shared with voidbot, and this is the undeclared coupling ruling 21 wanted declared; the embedder is Raven.
- Release body: build in
rust@sha256:4c2fd73e…(the Odin binding's pinned image) at an exact revision, withcargo build --locked --release -p huginn-daemon. Install to/opt/gamecult/huginn/releases/<sha>/, then flipcurrent. - Install: user, directories, unit, and the ufw rule
ufw allow in on wg-gamecult from 10.77.0.2 to 10.77.0.1 port 17872 proto udp comment 'Huginn mind RUDP from Starfire'. - Verification.
- Recovery: stop the unit, delete
huginn_mind_<slug>to force a rebuild, restoreminds/from backup. - Retirement: this unit, the ufw rule and the release section die in 14-I.
-
inventory.md: allocate 17872 to Huginn, and add a Huginn line to the Yggdrasil services list. The stale Nightwing embedding text (P2) belongs to the embedding migration's owner. Hands does not rewrite it. Hands reports it. -
Backup (under Q-H2 A):
- add
var/lib/gamecult/huginnto the tar list; - add
huginn.serviceto the freeze loop; - install and enable
gamecult-authority-backup.timer, which is operator-run.
- add
Authority map (interim).
- Owner: the operator owns deployment and continuity until 14-I. Huginn owns its state.
- Forbidden writers: nothing but the daemon writes
/var/lib/gamecult/huginn. Nothing but the runbook's release step writes/opt/gamecult/huginn. - Deletion line: 14-I.
Verification (operator only; Hands does not deploy):
- The unit reaches
active. - The daemon's stderr says it is serving
10.77.0.1:17872. - From Starfire,
eureka-statewhoamianswersreachable: true. - Restart preserves state: admit, run
systemctl restart huginn, thenviewthe document. - Negative: a
whoamifrom Raven (10.77.0.4) times out, because ufw admits Starfire only. - Negative: stop the unit. The next
admitisUnavailablenaming the endpoint, and no file appears on Starfire. - The collection
huginn_mind_<slug>exists, with its metadata. - Backup: run the backup service once.
tar --listshowsvar/lib/gamecult/huginn/minds/<slug>/mind.redb.
Estimate: about 150 lines of ops configuration and prose. +1 unit, +1 ufw rule, +1 port, +1 backup path, and backups resume.
Cut 15. Skill wiring (under Q-H6 A)
- Repo:
GameCult/Eureka,mainat19487f0(~/.claude/skills/eureka). Depends on 13b being registered and 14 being live. - The old Cut 15's anchors are stale:
SKILL.mdis now 533 lines, and "Mind Steward" is now Life. Hands re-takes them. The sections to change:
| Where | Change |
|---|---|
SKILL.md:8-19 (the substrate paragraph) |
Eureka keeps questions, rulings, cut specs, reports, verdicts, findings, follow-ups and resolutions as typed documents in the instance's mind (Huginn), through the eureka-state tools. The committed target and map keep what has no kind. Delete "committed docs … memory files" as the store. |
§0 (:58-72) |
Self calls whoami first. If the organ is unreachable, stop and say so. Then Self admits stewardship (if absent), campaign and target. |
§0b and §1 (:74-165) |
Imagination admits questions and cut_specs. The map document keeps body facts, the model page and rationale, and has no per-cut spec sections. "Commit the map" applies to that prose only. |
§2 (:167-189) |
A ruling is admitted with answers, choice and operator_quote. Supersession is a resolution, never an edit. |
§3 (:190-300) |
The Hands brief names the cut_spec id and the rulings in force. Hands admits a cut_report. cargo-mutants survivor triage goes into cut_report.deviations, or into a follow-up. |
§4 (:304-335) |
Soul admits one verdict plus its findings. Admitting is a Mind write, not a Body edit, so Soul stays read-only on repos. |
§5 and §6 (:337-399) |
Triage outcomes are resolutions and follow_ups. The status header becomes a set of selection recipes. Self reconciles the ledger from cut_report.structural_delta. |
Self's discipline (:401-451) |
Never restate typed state in prose. When the organ is unreachable, stop; never keep a second copy (the target's invariant). Keep "Record the substrate you needed and did not have". |
references/briefs.md |
Each faculty's "read first" becomes selection recipes. Hands gets view <cut_spec id>. A new Rehydrate section: whoami, then the recipes. |
references/cut-map.md |
Rewrite it as "Campaign state": the document set, the keys, the resolution matrix, and selection recipes in RS-3's vocabulary (below). The prose-map template shrinks to body facts, model page and rationale. |
references/changelog.md |
A dated entry with this map's evidence. |
Selection recipes to write. These replace the deleted presets, spelled in RS-3's aliases:
- rulings in force:
schemas: [epiphany.pipeline.ruling.v2],in_force = true,root = <campaign>; - open items: questions, findings and follow-ups with
in_force = true; - specs with no report:
cut_specs, with the hopcut_specinbound and absent (Hands must confirm that the substrate can express absence; if it cannot, this recipe is two queries and says so); - a subject's history:
resolutions withsubject= X,descending.
Verification (Soul, by reading plus one live exercise).
rg -n "status header|WriterLeaseHeld|lease|open_items|rulings_in_force|Mind Steward" ~/.claude/skills/eurekahits only changelog and history lines.- Every leaf kind is named by at least one brief.
- No brief asks for prose relay of a typed artifact.
- Live: a fresh agent given only the skill and the instance slug runs
whoamiand the rulings-in-force recipe against the live organ, and both return typed answers.
Estimate: prose shrinks. cut-map.md is replaced, not extended.
Cut 12. Parked (Q-H5)
Recorded, not built:
- A hand-off is two admissions, one per mind, sender first. Each side's derivation already exists (P8).
- Import waits for a consumer, most likely Epiphany's adoption campaign.
- Old Cut 12's in-process
hand_off(&mut from, &mut to)is infeasible under one daemon per mind (P7).
14-I. Idunn path (blocked; not this week)
Named so that the interim unit has a deletion line. Its dependencies:
- Route continuity (
Idunn/docs/route-continuity-target.md, invariants 1, 2 and 5, and its Q1 b):- Huginn declares no
odin.verse-rendezvousdependency, so it is a route-proof target. - No nginx reload on observation, because every reload breaks RUDP sessions.
- Stateful deadlines resolve (its Q3).
- Huginn declares no
- A shared Rust runtime participant in
cultnet-rs: presence signing, activation credential, lease adoption and the challenge answer. Otherwise Huginn writes a third copy (P9). The owner is CultLib. - Idunn has no path from a selected dependency's endpoint to a service argument. Qdrant and Ollama will be
external-operator-bindingdependencies whose endpoints are duplicated inargument_bindings, as Ghostlight duplicates Odin's. Route this to Idunn. - The slot for
minds/<slug>/mind.redbisexternal-file, because redb is not Idunn's single-file CultCache format. It is one slot per instance, so the recipe names the instance. - Whether the huginn-daemon hub (
serve.rs:138, which drops other connection ids) receives Idunn's RUDP challenge. Not probed.
The later Verse-presence cut (Q23 A) comes after 14-I.
Subtraction ledger (estimates; Self reconciles at each landing)
| Cut | Removed | Added | Dependencies, targets, formats |
|---|---|---|---|
| 11a | NoIndex from the binary |
~650 | +ureq, +serde_json (daemon); +1 collection per mind |
| 11b | the unwired refusal and its test | ~350 | none |
| 13a | 223 (moved) | ~300 | +base64 in huginn-mind (moved) |
| 13b | the old design's 3 preset tools | ~400 | +1 binary; +rmcp, +tokio |
| 12 | the whole cut (parked) | 0 | none |
| 14 | 2 dead freeze entries | ~150 ops | +1 unit, +1 ufw rule, +1 port; backups resume |
| 15 | per-cut prose sections, status headers | prose shrinks | cut-map.md replaced |
What in the old specs (cut map lines 5490-5866) is now wrong
- Cut 11's
pending_indexdocument. A mind admits exactly fifteen types (mind.rs:84-88), and this would be derived state in the truth store. It is replaced by reconciliation by set difference. - Cut 11's "indexing at admission, in the same call". It blocks the single-threaded serve loop (P7). A cold embed takes 5-6 s (P3).
- Cut 11's flags "following
epiphany.service's spelling (R16)" and its10.77.0.1:11435precedent. Both endpoints are dead (P2). - The compatibility record ported from voidbot. Its
embedderIdincludes the host URL (P4). Compatibility keys on model and digest instead. - One shared collection with an
instancepayload. It becomes one collection per mind. - Cut 12's two-mind in-process hand-off. It is infeasible (P7), and the hand-off already works per mind (P8).
- Cut 13's seven tools. They become four, after the operator deleted
open_itemsandhistoryas presets. - Cut 13's "tool schemas equal the published schemas". The published request schema will
$refCultLib's$id(RS-3 F9). MCP tool schemas must be self-contained. - Cut 13 is missing Deferred handling (BP-3 §7).
- Cut 14's
kind = cultcache-directory. No such kind exists (P9). - Cut 14's "
shared-infrastructuredependency on Qdrant, Ghostlight's shape (:168-187)". Ghostlight declares only Odin, and non-Idunn infrastructure isexternal-operator-binding(P9). - Cut 14's
idunn-huginnunit name in the backup freeze loop. Units are named per incarnation,idunn-<target>-<sha>(P9). - Cut 14's "Named owner:
gamecult-authority-backup.service, daily at 03:20 UTC". It is not installed, and it would refuse to run (P10). - Cut 14's route through nginx
rudp://10.77.0.1:17872with presence health. Huginn has no runtime participant, and route continuity is rewriting that path (P9). - Cut 15's anchors, and its "Mind Steward" name. Its
open_itemsplusquerystatus header and its rehydration sequence both use deleted tools. - Q14 C (move the mutation harness into the skill repo). It is moot: the harnesses were retired.