Watchdog reporting postmortem

First typed Eureka campaign (idunn-watchdog): 8 cuts, 17 Soul verdicts,
55 findings; landed about 2.5x its estimate. Idunn pages the operator on
Discord through Bifrost; closure DM still owed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-01 10:25:36 +02:00
commit 6667f0f9a6

View file

@ -0,0 +1,244 @@
# 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. The incident is written to `incidents.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 `a909f93` at 2026-09-30 23:21 UTC, then `cb60965` at 2026-10-01
02:02 UTC.
- The Bifrost reader at `160fac20` with CultLib `36ea08d3`, at 2026-10-01
08:04 UTC.
- **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-owner`
- `record-before-delivery`
- `delivery-owner`
- `survival-independent`
- `one-per-incident`
- `no-sensitive-egress`
- `tick-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-alarm` and the pump's `discord-dm` verb.
- gamecult-ops' dead `--operator-alarm-command` scripts.
- Idunn's `publish_projection_mode` and its 0644-lock test.
- The reader's prune-on-absence logic and its `WATCHDOG_NOTICE_NOW_MS`
production clock seam.
Added:
- `idunn.operator_incident.v1` (`incidents.cc`) and
`bifrost.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=0` meant 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.lock` 0644
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.
- **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.
- **Decorative checks.**
- A compare-exchange expected `None` on a key that embeds `now`, 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.
- **The verifier itself.** Soul's s3 found that the stopgap forced one
`CARGO_TARGET_DIR` on 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_owner` and 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 `SendMessage` with 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/node`** through 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 `estimate` the 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 in
`follow_up:notice-retry-hardening`, `follow_up:ops-refusal-test-hardening`,
`follow_up:bifrost-pump-input-hygiene` and
`follow_up:contended-publish-bounds`.
Operational follow-ups that matter most:
- `closure-dm-observed`
- `notice-unknown-settle`
- `contended-publish-bounds`
- `idunn-load-flaky-tests`
- `cultcache-rs-lock-mode`
- `cultcache-ts-readonly-pull`