2026-06-05 12:23:59 +01:00
|
|
|
# Huginn
|
2026-05-06 03:11:02 +01:00
|
|
|
|
2026-09-16 15:12:18 +02:00
|
|
|
Huginn is the GameCult memory organ. It owns each agent instance's mind as
|
|
|
|
|
typed state: what an instance remembers, which of that memory is admitted to
|
|
|
|
|
steer its next action, and the provenance of every admitted claim.
|
2026-05-06 03:11:02 +01:00
|
|
|
|
2026-06-21 15:51:46 +01:00
|
|
|
Upstream: `https://github.com/GameCult/Huginn.git`
|
|
|
|
|
|
2026-06-05 12:42:30 +01:00
|
|
|
## Authority
|
2026-05-06 03:11:02 +01:00
|
|
|
|
2026-09-16 15:12:18 +02:00
|
|
|
- An instance owns its mind; Huginn owns the state. Huginn is the single
|
|
|
|
|
writer of an instance's memory documents. No other service, script, or agent
|
|
|
|
|
writes them.
|
2026-09-16 23:00:28 +02:00
|
|
|
- Memory documents are CultCache state. `huginn-mind` persists them through
|
|
|
|
|
CultLib's Rust CultCache into a redb store at
|
|
|
|
|
`<state_root>/minds/<instance>/mind.redb`. `huginn-daemon` serves one such
|
|
|
|
|
mind over CultNet RUDP: admission and the read side ride
|
|
|
|
|
`cultnet.operation_request.v0` and `cultnet.operation_response.v0`, and the
|
|
|
|
|
two wire schemas answer a schema catalog request.
|
Close the epoch record, the minted leaf refusal and the nested resolution
Soul's Cut 8 pass found three rules the code only half held and one
document that promised more than the crate does.
The opener's step 3 said "exactly one epoch record at the current epoch"
and checked neither the count nor the key: a record keyed anything at all
was read as the epoch record, and a second one was never looked at, so a
current record beside a foreign one passed or refused on nothing but the
order the store returned them in. Step 3 now counts before it reads,
refuses a record under any key but the epoch, and names the defect in
`found`: the stored epoch, the offending key, or the count.
The campaign row minted `PipelineRefusal::FieldBound` for an empty
`repos`. The organ does not get to invent the leaf's refusals; an empty
`repos` is admission's own rule and is now `MindRefusal::EmptyRepos`.
The matrix admitted `Withdrawn` on a resolution, which made a subject
resolvable twice over: withdraw the resolution and the subject is open
again. A subject resolves at most once, and a resolution is not itself a
subject, so `resolution` joins `campaign`, `cut_report`, `verdict`,
`instance` and `hand_off` as not resolvable. The key grammar still spells
a resolution of a resolution; admission refuses it here. Self's default
ruling on Q17, standing until the operator rules.
`README.md` and `AGENTS.md` claimed CultNet publication and a Qdrant
connection in the present tense and called the store `.cc`. The daemon is
a stub, nothing connects to Qdrant, and the live crate writes
`mind.redb`. They say so now.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 17:53:08 +02:00
|
|
|
- Retrieval is to depend on Qdrant directly, refusing loudly when Qdrant is
|
|
|
|
|
unreachable rather than falling back to a second store or a second writer.
|
|
|
|
|
No crate opens that connection yet.
|
2026-09-16 15:12:18 +02:00
|
|
|
- Mind state is not version-controlled. It lives in Huginn's store, not in
|
|
|
|
|
any repository.
|
2026-06-21 15:51:46 +01:00
|
|
|
|
2026-09-16 15:12:18 +02:00
|
|
|
Generic `.cc` inspection is not Huginn's job. CultCache Studio inspects and
|
|
|
|
|
edits `.cc` state. Huginn reads and writes minds.
|
2026-06-21 15:51:46 +01:00
|
|
|
|
2026-09-16 15:12:18 +02:00
|
|
|
## Layout
|
2026-05-06 04:41:11 +01:00
|
|
|
|
2026-09-16 15:12:18 +02:00
|
|
|
A Cargo workspace of three crates:
|
2026-05-06 04:41:11 +01:00
|
|
|
|
Read a mind: views, queries, open items and history
`query.rs` is the read side, over a `&Mind<S>` with no store file, no
daemon and no index. A `PipelineDocumentView` is four fields: the id,
because a `PipelineDocument` does not carry its key and a client must
never derive one; the document; the facts of its admission; and its
status.
The facts come from the commit receipts and from their `writes` alone,
so a document a later batch cited as a strong read keeps the receipt
that wrote it, and `admitted_at` is that receipt's `committed_at` -- the
`now` admission was passed -- never the store's `stored_at`. A pipeline
document the image holds and no receipt wrote is an integrity fault and
refuses; the store has one writer and the receipt is its proof.
Status is derived per call through `docs::Docs`, the same owner the
admission rules ask, so a withdrawn resolution reopens its subject for a
reader exactly when it reopens it for a rule. `closing_resolution` is
`in_force_unless`'s own body asked for the resolution rather than the
bool, so there is no second derivation to drift.
Four reads: `view` by id, `query` over the filters (root, repo, cut,
kinds, in force, faculty, an admission window), `open_items` for one
campaign's unanswered questions, standing findings, follow-ups,
unreported specs and unjudged reports, and `history` for every
resolution a subject ever had or every assignment of a repo to this
mind, withdrawn ones included with their reasons.
A query is ordered by `(admitted_at, id)`, capped at 200, and carries
`matched`, the count before the cap, so a truncation is never a short
page that reads like the whole answer. `semantic` is accepted by the
type and refused typed until Cut 11 owns the index; no substring filter
lives here to become a second answer to "the text of a document".
`cargo test -p huginn-mind --lib`: 56 passed, 0 failed, 0 warnings
(46 + the ten tests). `cargo check --workspace` clean. No dependency, no
target, no kind, no schema, no epoch; `Cargo.toml` and `Cargo.lock`
untouched.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 21:22:41 +02:00
|
|
|
- `crates/huginn-mind`: storage, identity, admission, and queries and derived
|
|
|
|
|
status over memory documents.
|
2026-09-16 23:00:28 +02:00
|
|
|
- `crates/huginn-daemon`: the CultNet surface. The socket, the sessions, the
|
|
|
|
|
process and the operation envelope, and no rule.
|
Pin the reopened subject, the retaken repo, and six blind spots
Two tests for the rules the last commit landed.
`a_withdrawn_resolution_reopens_its_subject_and_stays_readable` withdraws a
closure, admits the next one at `n2`, reads the first back with its outcome
intact, refuses a third while `n2` stands, refuses the withdrawal of the
withdrawal, and has a ruling answer the reopened question -- the derived
resolution lands at `n2`, not `n1`.
`a_repo_is_stewarded_once_at_a_time_and_again_after_a_transfer` refuses a
second assignment while the first stands and a second claiming the taken
sequence, hands the repo away, finds nothing stewarding it, hands it back and
gets `n2` with the second `handed_on`, the first still readable by key.
Entries H40-H46 carry the mutants: the two sequence rules, the recursive
in-force, `AlreadyStewarded`, the Q19 cap, and `derive`'s two sequences.
Then the six places Soul found the code right and the suite blind, each a
fixture and an entry (H47-H51): a handed-away repo is not stewarded, so
`stewardship_of`'s in-force half is pinned; a document two batch documents
cite is pinned once rather than cancelled; what a derived write cites in the
image is pinned, which is H32's surviving half; a finding may cite the
vocabulary of the target landing beside it; `Answered` coherence is checked
against a ruling in the image as against one in the batch; a branch differing
only in case is a mismatch.
H32 is re-anchored rather than left to rot. Its A4 half is unreachable now: a
derived write takes the sequence after the batch's own records, so a derived
identity cannot collide with one the batch holds. What survives is that
derived writes are validated, staged and resolved like any other, and the
transfer test's strong read is what fails when they are not.
README stops claiming the two stub crates do anything.
H1-H51 all killed, M0 green. 33 tests, 0 warnings; `cargo check --workspace`
clean; `git diff --stat 4094e68 -- .voidbot` empty.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 19:44:15 +02:00
|
|
|
- `crates/eureka-state`: a stub. It will carry typed state for the Eureka
|
|
|
|
|
pipeline; today it holds none.
|
2026-05-06 04:41:11 +01:00
|
|
|
|
Open a mind fail-closed over an owned redb store
Cut 8, Huginn half, commit 1 of 4: the store port, the opener and the
identity rules. `huginn-mind` gains its dependencies (`cultcache-rs` at
Epiphany's pin `a0813c6`, `epiphany-pipeline` at `a65c6420`, the Epiphany
half of this cut), `store.rs` with the one narrow `MindStore` trait that
injects compare-and-swap, `mind.rs` with `Mind::open` and `open_with`, and
`refusal.rs` with the organ's refusal vocabulary.
The opener refuses in order and attaches nothing until every gate passes:
a foreign type, a foreign epoch, a missing identity, an identity naming
another instance. The path under the state root is derived from the slug;
the `instance` document is the authority, so a store copied into another
instance's directory is refused. The owned store's exclusive lock is the
single-writer mechanism: a second `Mind::open` of the same path is
`MindAlreadyOwned`.
The receipt type and the two provenance shapes (`Faculty`,
`PipelineProvenance`) land here rather than in the later commits because
the opener registers the receipt's type id, and their only consumer is the
receipt; the commit primitive itself is the next commit. `MindRefusal`
carries a serde remote definition of the leaf's `PipelineRefusal`, which
derives neither `Serialize` nor `JsonSchema` at the pinned rev; it is
compiled against the leaf's type so a leaf change breaks the build rather
than drifting.
`Cargo.lock` is regenerated and gains 90 packages. The map predicted a
CRLF-to-LF rewrite of the whole file; the file was already LF in the index
and stays LF, so the diff is additions only.
README and AGENTS now say `huginn-mind` is live and the other two crates
are stubs.
Builds: `cargo check -p huginn-mind --lib --tests` and `cargo test -p
huginn-mind --lib` (4 tests) pass. This commit carries dead-code warnings
on the store and cache accessors and the refusing test store; commits 2 and
3 consume them.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 17:04:23 +02:00
|
|
|
`huginn-mind` is live: it opens one instance's store (an owned redb CultCache
|
|
|
|
|
at `<state_root>/minds/<instance>/mind.redb`, locked for the mind's lifetime),
|
|
|
|
|
and refuses a store whose `instance` document names another instance, whose
|
Admit a batch through one ordered path with every rule in it
Cut 8, Huginn half, commit 3 of 4: `admission.rs`. `Mind::admit`
prepares typed documents through the leaf and calls `admit_prepared`,
the single commit path for this cut, Cut 10's sink, Cut 12's hand-off and
import, and Cut 13's tools. The steps run in order and the first failing
step is the outcome: the batch names this mind (A1), one to sixty-four
envelopes (A2), the leaf's bounds, formats and key recomputation (A3),
unique identities (A4), every instance-carrying document names this mind
(A5), an empty mind's first batch carries exactly one `instance` document
and a batch carrying one derives the epoch record (A6), every reference
resolves in image or batch by `(kind, id)` (A7), the derived writes pass
A3-A7 themselves, the per-kind rules and the resolution matrix (A8), an
exact replay answers with the stored receipt only after validation (A9),
no write collides with the image (A10), the commit (A11).
Derived writes: a ruling that answers derives the `Answered` resolution
of its question; a hand-off derives the stewardship withdrawal on its
source side and the stewardship assignment on its receiving side. "In
force" is computed at rule time from the resolutions in image and batch
and never stored; the two derivations are the only resolutions a rule
ignores when judging its own document.
Two places the leaf is thinner than the spec assumed. `FindingWithoutRange`
is unreachable: a finding envelope without `range` fails the leaf's decode
as `Document(InvalidFormat { field: "payload" })` before any organ rule,
and the test asserts that. A campaign with no repos has no refusal of its
own in the spec's list; it is refused as `Document(FieldBound)` on
`campaign.repos` with a minimum of one. `PipelineAdmissionBatch` cannot
derive `JsonSchema` because the leaf's `PipelineDocument` does not; the
outcome and refusal types do.
The epoch record is derived whenever the batch carries the `instance`
document, which only the first batch may, so a replay of the first batch
digests to the same receipt and answers `AlreadyAdmitted` instead of
colliding.
Tests: 26 (the spec's 25 plus the replay-planting test, the receipt's
deliberate second construction site). `cargo check -p huginn-mind --lib
--tests` and `cargo test -p huginn-mind --lib` pass with zero warnings;
`cargo check --workspace` passes. README says what the crate does now.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 17:13:44 +02:00
|
|
|
epoch record is foreign, or whose types are not a mind's. It admits batches
|
|
|
|
|
of pipeline documents through one commit path: the leaf's bounds, formats and
|
|
|
|
|
keys, then the organ's cross-document rules (references, in-force status,
|
|
|
|
|
the resolution matrix, derived resolutions and stewardships), then one
|
|
|
|
|
compare-and-swap that lands the batch whole with a receipt naming the exact
|
|
|
|
|
bytes it read and wrote. An exact replay answers with the stored receipt.
|
Read a mind: views, queries, open items and history
`query.rs` is the read side, over a `&Mind<S>` with no store file, no
daemon and no index. A `PipelineDocumentView` is four fields: the id,
because a `PipelineDocument` does not carry its key and a client must
never derive one; the document; the facts of its admission; and its
status.
The facts come from the commit receipts and from their `writes` alone,
so a document a later batch cited as a strong read keeps the receipt
that wrote it, and `admitted_at` is that receipt's `committed_at` -- the
`now` admission was passed -- never the store's `stored_at`. A pipeline
document the image holds and no receipt wrote is an integrity fault and
refuses; the store has one writer and the receipt is its proof.
Status is derived per call through `docs::Docs`, the same owner the
admission rules ask, so a withdrawn resolution reopens its subject for a
reader exactly when it reopens it for a rule. `closing_resolution` is
`in_force_unless`'s own body asked for the resolution rather than the
bool, so there is no second derivation to drift.
Four reads: `view` by id, `query` over the filters (root, repo, cut,
kinds, in force, faculty, an admission window), `open_items` for one
campaign's unanswered questions, standing findings, follow-ups,
unreported specs and unjudged reports, and `history` for every
resolution a subject ever had or every assignment of a repo to this
mind, withdrawn ones included with their reasons.
A query is ordered by `(admitted_at, id)`, capped at 200, and carries
`matched`, the count before the cap, so a truncation is never a short
page that reads like the whole answer. `semantic` is accepted by the
type and refused typed until Cut 11 owns the index; no substring filter
lives here to become a second answer to "the text of a document".
`cargo test -p huginn-mind --lib`: 56 passed, 0 failed, 0 warnings
(46 + the ten tests). `cargo check --workspace` clean. No dependency, no
target, no kind, no schema, no epoch; `Cargo.toml` and `Cargo.lock`
untouched.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 21:22:41 +02:00
|
|
|
It reads them back the same way: a document with the facts of its admission
|
|
|
|
|
joined from that receipt and its status derived at read time, typed queries
|
|
|
|
|
over one mind, a campaign's open work, and a subject's or a repo's history.
|
|
|
|
|
Status is never stored, and the views derive it through the same rules
|
|
|
|
|
admission does. Document shape and keys come from `epiphany-pipeline`; the
|
|
|
|
|
store is CultLib's Rust CultCache.
|
2026-09-16 23:00:28 +02:00
|
|
|
|
|
|
|
|
`huginn-daemon` opens one mind, binds one UDP socket in that order, and
|
|
|
|
|
answers every frame on the session it arrived on: one operation per `Mind`
|
|
|
|
|
method plus `whoami`, whose payload is the mind's own types as named
|
|
|
|
|
MessagePack. A refusal is an answer with a `rejected` status, never a
|
|
|
|
|
transport error; an envelope that does not decode is answered with a typed
|
|
|
|
|
failure and reaches no mind. The two schemas it publishes live in
|
|
|
|
|
`schemas/cultnet/` and are pinned to their derivation by a test. There is no
|
|
|
|
|
index yet.
|
|
|
|
|
|
|
|
|
|
`eureka-state` is a stub; the campaign's cut map in
|
|
|
|
|
`Epiphany/notes/eureka-pipeline-state-cut.md` owns what each crate must do
|
|
|
|
|
next.
|
2026-06-21 15:51:46 +01:00
|
|
|
|
|
|
|
|
```powershell
|
2026-09-16 15:12:18 +02:00
|
|
|
cargo check --workspace
|
2026-09-16 23:00:28 +02:00
|
|
|
cargo run -p huginn-daemon -- --state-root <abs> --instance <slug> --bind 127.0.0.1:17872
|
2026-06-21 15:51:46 +01:00
|
|
|
```
|
|
|
|
|
|
2026-09-16 15:12:18 +02:00
|
|
|
## Persona
|
|
|
|
|
|
|
|
|
|
Huginn's legacy repo Persona lives under `.voidbot/`. Its `state/huginn.cc`
|
|
|
|
|
holds legacy `void.*` documents and its `voice/identity.json` names the
|
|
|
|
|
Persona. The two disagree about jurisdiction; that migration belongs to the
|
|
|
|
|
portable-Persona work, not to this workspace, and nothing here reads or writes
|
|
|
|
|
`.voidbot/`.
|