11 KiB
Watchdog reporting postmortem
Campaign idunn-watchdog, 2026-09-30 to 2026-10-01. This was the first Eureka
campaign whose state was typed in Huginn's mind (instance eureka) rather than
kept in a prose map. Document ids below are idunn-watchdog:<kind>:<local>.
Read them with view; this page does not restate them.
Summary
Idunn is now a watchdog that reaches the operator on Discord.
- Idunn opens a durable incident at the site where a continuity budget is
exhausted (since
idunn-reincarnationcut 1 this is a paging threshold only; continuity keeps restarting). The incident is written toincidents.cc, a world-readable file in its projection directory with a private 0600 lock, and closes when the target runs again and its restart window is no longer exhausted. - Bifrost's watchdog-notice reader runs from a one-minute timer. It sends one opening DM and at most one closure DM per incident, keeps its own journal, and retries a failed send forever with capped backoff.
- Scale: 8 cuts across Idunn, Bifrost and gamecult-ops: 13 spec revisions, 17 Hands reports, 17 Soul verdicts and 55 findings. Measured against the in-force estimates:
| Repo | Estimate (lines) | Landed (lines) |
|---|---|---|
| Idunn | +930/−101 | +2618/−354 |
| Bifrost | +246/−335 | +1182/−429 |
| gamecult-ops | +280/−35 | +505/−104 |
- Installed on Yggdrasil:
- Idunn
a909f93at 2026-09-30 23:21 UTC, thencb60965at 2026-10-01 02:02 UTC. - The Bifrost reader at
160fac20with CultLib36ea08d3, at 2026-10-01 08:04 UTC.
- Idunn
- First live notice: the incident's first real input was raven-muninn, which exhausted its budget minutes after the incident cut was installed. Its opening DM was delivered at 08:04:41 UTC, and the operator confirmed it.
- Still owed: the closure DM for that same incident
(
follow_up:closure-dm-observed). It depends on raven-muninn recovering, and raven-muninn's own fault lies outside this campaign.
Scope and invariants
Target r4. Its invariants are:
incident-ownerrecord-before-deliverydelivery-ownersurvival-independentone-per-incidentno-sensitive-egresstick-lock-private
The operator directions are ruling:operator-watchdog-direction and
ruling:operator-bifrost-bridge. The second made Bifrost the bridge to
Discord, so Idunn holds no credential and speaks no Discord API.
Excluded:
- Odin's store condition and B5
OperatorRequired. Neither can page yet. Odin's O2 cut is unbuilt, and B5 has no operator exit (follow_up:b5-operator-required-no-exit). - VoidBot's
notify_owner. - The Persona Discord crossing.
- Eve lowering.
The earlier attempt was Bifrost c063f27 (2026-09-04): an alarm was a command
written into Bifrost's store. It never ran, because no pump was deployed and
the deployed pump lacked the DM verb. It was also reversed by ruling:
ruling:delivery-crossing has Bifrost read Idunn's projection, so no write
crosses an owner boundary.
Timeline
| Cut | Reports | Soul passes | Notable finding |
|---|---|---|---|
| ops-retire-alarm | h1 | 1 | none |
| bifrost-retire-alarm | h1–h2 | 2 | silent-skip: a stray discord-dm command sat pending forever |
| bifrost-suite | h1 | 1 | suite-cultcache-scope (pre-existing): 9 of 17 tests failed at base on a CultLib rename |
| bifrost-notice-reader | h1–h3 | 3 | prune-keyed-on-presence, bridge-exit-is-failed: duplicate DMs and blocked closures |
| bifrost-notice-retry | h1–h2 | 2 | future-stamp-stalls-retry |
| ops-notice-deploy | h1–h3 | 3 | retry-window-five-minutes: a 5-minute outage dropped a notice for good |
| idunn-incident | h1–h3 | 3 | lock-stalls-tick (Blocker); lock-acl-world-openable (High) |
| idunn-topology-lock | h1–h2 | 2 | contended-publish-aborts; the live stall it fixed was pre-existing |
Structural delta
The landed code was about 2.5× its estimate, and the misses were not explained cut by cut as the skill asks.
- idunn-incident: +1813 against +700. Its fix batches added the lock primitive, load-independent lock tests, and per-operation fault keys.
- bifrost-notice-reader: +891, against r3's estimate of +140. That r3 figure described only the revision's own delta, not the cut, so r3 carried the wrong baseline.
Deleted:
- Bifrost's
publish-idunn-alarmand the pump'sdiscord-dmverb. - gamecult-ops' dead
--operator-alarm-commandscripts. - Idunn's
publish_projection_modeand its 0644-lock test. - The reader's prune-on-absence logic and its
WATCHDOG_NOTICE_NOW_MSproduction clock seam.
Added:
idunn.operator_incident.v1(incidents.cc) andbifrost.watchdog_notice_execution.v1(the journal).bifrost-watchdog-notice.{service,timer}.
Nothing was parked.
What Soul caught
Green suites passed every defect below.
- Survival and locking.
- Incident writes took a blocking flock inside Idunn's scheduler tick, on
a world-readable lock. Any local uid could freeze continuity for every
target, and
WatchdogUSec=0meant nothing would restart Idunn (s1.lock-stalls-tick). - After the fix, the live directory's default ACL still made the lock 0644,
so any uid could stop incidents opening. The test ran in a temp directory
with no ACL (
s2.lock-acl-world-openable). - The same probe exposed a pre-existing live hazard:
topology.cc.lock0644 with a blocking write in the tick, measured at 5.07 s at base (s2.topology-lock-stalls-tick). The operator ruled it into the campaign; after the topology cut the tick takes 0.09 s.
- Incident writes took a blocking flock inside Idunn's scheduler tick, on
a world-readable lock. Any local uid could freeze continuity for every
target, and
- Split authority.
- The reader pruned its "already sent" journal whenever a key was absent from Idunn's store. Restoring a backup would have re-sent old DMs.
- The reader decided subject validity with its own grammar, narrower than Idunn's.
- Retention was decided by two predicates in Idunn.
- Lifecycle.
- One running observation closed an exhaustion incident. A release that
crashes after each restart would have sent about 12 DMs an hour
(
s1.flap-reopens). - A bridge that died after Discord accepted a message was retried up to 5 times, and the closure was then blocked forever.
- Five attempts on a one-minute timer dropped a notice after a 5-minute Discord outage.
- One running observation closed an exhaustion incident. A release that
crashes after each restart would have sent about 12 DMs an hour
(
- Decorative checks.
- A compare-exchange expected
Noneon a key that embedsnow, so it guarded nothing (s1.cas-decorative). - A 2-second wall-clock bound on a lock test flaked under load and counted equivalent mutants as caught.
- A compare-exchange expected
- The verifier itself. Soul's s3 found that the stopgap forced one
CARGO_TARGET_DIRon every job, so cargo-mutants' parallel workers tested each other's binaries. Two runs of the same two mutants disagreed (s3.mutants-share-target-dir). Every parallel mutation count reported through the stopgap before 2026-10-01 is unproven, in both sessions' work.
Operator corrections
- "Bifrost is indeed intended to be the bridge to external services like
Discord." The agents had found two delivery paths: VoidBot's
notify_ownerand Bifrost's command store. The ownership boundary was missing from both repos' summaries. - "Stewardship is currently universal…" Self had filed a gap treating follow-up repos as needing stewardship. It was withdrawn; the rule was not written in the skill.
- "…we seem to be saving some tokens… but we're very very slow." Self blamed cold builds, measured, and was wrong. Cold builds took about 51 s and warm builds 29 s. The cost was slot queueing, repeated verify jobs, a test that hung a slot for 39 minutes, and a 4-hour job from the other session holding a fifth of capacity.
- "Yep, fixing Mind issues is highest priority, immediately once this
campaign closes" (
ruling:mind-fixes-next).
Incidents
- Network outages (ENOTFOUND). Two outages killed three agents
mid-pass. Each was resumed by
SendMessagewith context intact, and nothing was lost, because each agent's state was in commits and the mind. - A shared stopgap target directory made mutation evidence noise. It is
fixed in Eureka
a755a60. - Self's own brief inflated runtime. It told Hands to mutate serially to dodge a flake, and was corrected to skipping the flaky test by name.
- A hung test held a verify slot for 39 minutes
(
s2.route-connect-test-hangs). It is fixed. - A Hands test helper overwrote the container's
/usr/bin/nodethrough a symlink. It was caught by the agent's own baseline and then confined (s2.test-clobbers-usr-bin-node). - A worktree directory stays locked by a finished process
(
F:\Projects\gamecult-ops-watchdog-deploy, empty). Removing it is left to the operator.
What worked
- Soul attacks the layer where the rule fails, not the test. Every High/Blocker finding came from a probe the shipped tests could not see: a separate-process lock holder, the live directory's ACL, or a restored store.
- Point Soul at the load-bearing invariant in the brief. Naming "can the lock hang the tick" produced the Blocker on the first pass.
- Read before touching live state. Self installed only after checking the installed revision, the in-flight transactions and the unit diff. The repo unit carried a BOM the live one did not.
- One verify job per pass. Once briefs required it, a reader fix batch took 11 minutes, where the first reader Hands took 38.
What to change in the pipeline
Already changed in the skill:
- The substrate gaps moved from a table to the mind.
- Imagination owns the model page.
- Soul pays for each rerun once.
- The rust image uses sccache.
- The stopgap no longer forces
CARGO_TARGET_DIR.
Still to change:
- Explain every ledger miss in the next report's deviations. This campaign ran at about 2.5× its estimate and nobody explained it per cut.
- Make a revision's
estimatethe whole cut. r3's estimate covered only its own delta. - Verify the merged tree of a long-lived branch before pushing. Self did this once, for the incident cut over B5, and it should be the rule.
- Briefs name the standard sibling build command. The stopgap cannot
provision siblings (
follow_up:gap-verify-no-sibling-repos), and one hand-rolled build produced four false failures.
Open follow-ups
Fifty follow-ups are in force. Twenty are substrate gaps (gap-*): the work
the operator ruled next. Query them with the substrate-gaps recipe in the
skill. Seven findings remain unresolved:
cut-idunn-incident.s3.mutants-share-target-dir. It is fixed in the stopgap but awaits a Soul mutation run under the fixed script.- Six findings whose resolution keys exceed 64 bytes
(
follow_up:gap-unresolvable-finding-key). Their substance is recorded infollow_up:notice-retry-hardening,follow_up:ops-refusal-test-hardening,follow_up:bifrost-pump-input-hygieneandfollow_up:contended-publish-bounds.
Operational follow-ups that matter most:
closure-dm-observednotice-unknown-settlecontended-publish-boundsidunn-load-flaky-testscultcache-rs-lock-modecultcache-ts-readonly-pull