Skip to content

feat(OMN-18867): a durable lab alarm on FAIL receipts, restart loops and growing consumer lag - #3930

Merged
jonahgabriel merged 4 commits into
devfrom
jonah/omn-18867-durable-lab-alarm
Sep 21, 2026
Merged

jonahgabriel merged 4 commits into
devfrom
jonah/omn-18867-durable-lab-alarm

Conversation

@jonahgabriel

@jonahgabriel jonahgabriel commented Sep 21, 2026 •

Copy link
Copy Markdown
Collaborator

Detection time is the defect

Three lab conditions ran unobserved in one window. The savings writer crash-looping, frozen nine days at lag 498 (OMN-18851). The runtime crash-looping on a projection grant from a stale pin (omnibase_infra#3857). Compose-dev receipts failing for hours while a release lane waited on one.

Every one was found by a person looking, and the first nine days late. The conditions are not the problem; nothing was watching, and nothing carried what it saw to anyone.

The timer

A launchd StartCalendarInterval agent, hourly, rendered at install time rather than committed so no machine's paths land in source. Session-scoped scheduling dies with the session and this has to survive one.

The plist carries no Hour key, deliberately: a Minute-only interval matches every hour, and adding an Hour would turn this into a daily job that still reads as periodic, leaving detection bound at 24 hours instead of one. RunAtLoad and KeepAlive are both false, the second because a reader restarted the instant it exits would sample lag continuously and destroy the two-samples-straddling-real-time property the growth condition rests on.

The OMN-17173 correction applies: launchd does fire on this Mac, the earlier ticks had been bootout'd. So the installer directs the operator at both halves of the readback, a non-zero PID column and a fresh log entry, because a loaded-but-bootout'd job looks installed and does nothing.

Three conditions, each with a declared bound

condition alarms when
lab-pass receipt the compose-dev receipt for the lane's delivered sha is FAIL
container restarts a container passes its own declared bound, not one fleet number
consumer group lag a declared group's lag grew across two consecutive samples

Growth and only growth, and that is the decisive choice. 498 held flat for nine days is a number a generous bound admits and a tight bound flags on every healthy busy group as well. That noise is what gets an alarm muted, and a muted alarm is worse than none because it reads as coverage. test_a_high_but_flat_lag_raises_nothing is the control that pins it.

Delivery, and what authorizes it

An earlier revision of this pull request shipped no sender at all and said so. That was correct at the time and is no longer the state: an operator ruling on 2026-09-21 authorizes this alarm to post to one named channel using the lab's existing bot token by reference.

No agent message is consent. Not a dispatch brief, not this ticket, not the plan. The only thing that authorizes a send is a durable row in the rolling work ledger, resolved at run time on every run, and each run records the citation it resolved or the fact that it resolved nothing. A row that is later deleted or edited disables delivery rather than silently keeping it.

The resolver accepts two shapes. Rule 18's canonical row with two labelled scope lists, and the dated RULING the real authorization was actually written as. It requires the four substantive facts and not the two label strings — the consent token, an approved_by, the destination channel, and an explicit exclusion — because a gate that refused a real, dated, operator-attributed, channel-naming authorization over two missing labels is a spelling test rather than an authorization control, and the next lane routes around it. Five refusal controls keep that from being a rubber stamp: no approver, no channel, a different channel, no exclusion clause, and a row that is not a consent row at all. An unreadable ledger resolves nothing.

The credential is named, never valued. It is read out of an env file by NAME at send time, held for one request, and never logged, never written to the run record, never put on a command line and never included in an error message — a failure names the variable and the file instead. An env file rather than the process environment because this runs under launchd, which inherits no shell, so a design assuming the token is in the agent's environment sends nothing and says it succeeded.

Delivery is per newly raised alarm, so the edge-triggering below is also the channel's deduplication: a condition that stays bad posts once, not once an hour.

Proven live, not described

One injected FAIL receipt, then an immediate repeat of the same condition, with only the three conditions' inputs faked so the lane itself was never touched:

RUN 1  posting: authorized by .../ROLLING_WORK_LEDGER.md:3523 (approved_by=operator);
       delivered 1/1 to #omninode-notifications; delivery ids 44e64dab...@1790011496.610959
RUN 2  raised nothing new this run
       posting: authorized by .../ROLLING_WORK_LEDGER.md:3523 (approved_by=operator);
       nothing new to deliver this run

The second line is the deduplication, measured rather than asserted. A note was posted in-thread on the delivered message saying it was an injected proof, so the channel is not left holding a false alarm.

Three outcomes, because two would lie

OK, ALARM, INDETERMINATE. A docker socket that will not answer, a broker that refuses, an artifact surface that errors, and a lag condition with no previous sample are all conditions the run could not evaluate. Folding those into OK is how a monitor reports green over an outage.

The exit code carries the verdict too, so a run cannot print a finding and pass: 0 all clear, 1 alarm raised, 2 indeterminate.

Every run appends a record naming all three conditions, whether or not anything fired, because a silent run and a run that never happened are otherwise indistinguishable. The record model refuses to describe a two-condition run as a run.

Edge-triggered. One subject raises once and not again until it recovers. An INDETERMINATE does not clear an active alarm, because "I could not tell" is not "it recovered".

Two things measured during the build that changed it

The lane is not on this host. A local docker daemon cannot see the lane's containers at all, so a bare docker would report every container unreadable forever, on a healthy lane and an unhealthy one alike. Docker is rewritten through ssh at the runner seam, which is already the seam every condition is tested through.

The ready endpoint carries no commit. It answers healthy with a package version and no sha anywhere in its payload, so a resolver built on it alone leaves the receipt condition permanently blind. The delivered sha comes from the deploy agent first, /ready second, and the evidence names which surface answered.

The one recorded skip, and why it is safe

config/skip_count_baseline.yaml grows by one id, 557 to 558: the live-ledger consent readback. It resolves the real row out of the shared registry ledger through the same resolver the alarm calls, and that file lives outside this repository, so CI has no ledger to read. It skips with a named reason rather than passing, because a green meaning I could not look is what this ticket is about.

Unlike the other entries in that list there is no second job that executes it, and the addendum says so plainly. What makes the skip safe is a property of the alarm rather than a promise: consent is resolved at run time, so a vanished row disables delivery instead of enabling it, and six hermetic refusal controls plus a falsifier asserting nothing is sent without a resolvable row all run everywhere.

What this does not claim

  • It blocks nothing, and that is the design. This is a detector on a timer; it turns a nine-day detection time into about an hour and now into a message. The enforcement it carries is that conditions are declared rather than judged, refusals are typed, the whole-run record is mandatory, delivery requires a cited durable grant, and the exit status cannot report success over a finding. A pre-merge gate is not available for a condition that only exists on a running lane.
  • A green is not node liveness. A group can read Stable with zero lag while the node behind it refuses every message (OMN-18881), so the lag condition measures lag and its growth and the evidence names the fact it read.
  • Container status is recorded, not graded. The bound is on restarts; grading status needs its own per-container declaration, since the migration gate exits on purpose every boot. Pinned by a test so it stays deliberate.
  • No dead-letter condition. The boundary writes to a derived per-class topic, so a probe on the declared topic sees a permanent zero on any lane.

A note on one thing this branch briefly carried

For part of its life this branch also carried omnimarket's node_projection_consumer_flow/0005_add_node_id_ingest_index.sql and its declaration row, because two pre-commit hooks in this repository block each other's remedy: the vendor-sync hook refuses every commit until a new omnimarket node migration is mirrored, and the declaration validator then refuses the mirror until it is declared. Reproduced on an unrelated branch, so it was repo-wide rather than mine.

dev has since landed both halves, so this branch carries neither and docker/migrations is byte-identical to dev. Keeping the duplicate would have been a second identical declaration, which CI refuses in three places at once. The friction is recorded rather than dropped: the arrangement that made a lane with no stake in that migration carry it is unchanged, and the next lane to commit here before a sync lands will carry it again.

Operator actions

  1. Install and take the first run by hand, then read back both halves: bash scripts/launchd/install-lab-alarm.sh (the installer prints the exact command and both readback checks).
  2. The lag condition stays indeterminate until the broker credential is present in the agent's environment. Nothing else is blocked by it.

No required context is added or changed by this pull request, and none is requested.

Ticket: OMN-18867. Parent: OMN-18864. Cause: OMN-18851, OMN-18836 sibling.

Evidence-Ticket: OMN-18867
Evidence-Source: OCC#10695

…sed once each

Three lab conditions ran unobserved in one window: the savings writer frozen
nine days at lag 498, the runtime crash-looping on a stale pin, and compose-dev
receipts failing for hours while a release lane waited. Every one was found by
a person looking. Detection time is the defect, not the conditions.

A launchd StartCalendarInterval agent, hourly, because session scheduling dies
with the session. The plist is rendered at install time rather than committed,
carries no Hour key so a Minute-only interval cannot silently become daily, and
the installer directs the operator at both halves of the OMN-17173 readback,
since a loaded-but-bootout'd job looks installed and does nothing.

Three conditions with declared bounds: a FAIL receipt for the delivered sha, a
container past its own per-container restart bound, and a consumer group whose
lag GREW across two consecutive samples. Growth and only growth: 498 held flat
for nine days is a number a generous bound admits and a tight bound flags on
every healthy busy group, and that noise is what gets an alarm muted.

Edge-triggered, so one subject raises once until it recovers. Three outcomes,
because a docker socket that will not answer is not a healthy lane. Every run
appends a record naming all three conditions whether or not anything fired, and
the exit code carries the verdict so the run does not print a finding and pass.

The lane is not on this host, so docker is rewritten through ssh at the runner
seam, which is already the tested seam for every condition. The delivered sha
comes from the deploy agent first: measured today, this lane's ready endpoint
answers healthy with a package version and no commit at all, so a resolver
built on it alone would leave the receipt condition permanently blind.

Nothing is sent anywhere. There is no network client in the module and a test
asserts its absence. The consent resolver a future sender must pass ships now,
with its refusal tested against a missing row, a row missing its out-of-scope
half, a row naming another channel, an unreadable ledger, and the live ledger.

Live, read-only, on the brew interpreter the agent will use: the receipt
condition read a real PASS receipt over 11 checks for the lane's own delivered
sha, the restart condition read twelve real containers, and the lag condition
returned INDETERMINATE naming all six unreadable groups with no broker
credential present, which is the negative control working.

Claude-Session: https://claude.ai/code/session_011WaS5jrEdJDstEEtiJuRvW
@jonahgabriel
jonahgabriel enabled auto-merge (squash) September 21, 2026 16:16
jonahgabriel pushed a commit to OmniNode-ai/onex_change_control that referenced this pull request Sep 21, 2026
…ibase_infra#3930 (#10695)

* evidence(OMN-18867): author OCC companion for OmniNode-ai/omnibase_infra#3930

OCC companion by node_pr_lifecycle_fix_effect (OMN-13317 F1 / OMN-13990 / OMN-14285). Product PR head 44e64dab8e4bf9867acc6297b110d49d031c560f.

* evidence(OMN-18867): self-bind OCC#10695 + rebind contract_sha256

---------

Co-authored-by: omnimarket-bot <bot@omninode.ai>
@github-actions

github-actions Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ Hostile Reviewer — DEGRADED (informational)

Critical findings: 0
Major findings: 0
Total findings: 0
Models succeeded: glm-review

Note: Fewer than 2 reviewer models succeeded. Degraded results are informational (OMN-8468/OMN-8524) and do not block merge. Error: cli_review exit 2 (fewer than 2 models succeeded — partial/total outage)


Semantics (OMN-17492 — the model finds, thread resolution gates)

Surface Meaning Blocks merge?
Review threads Per-finding, posted by the reviewer No (informational)
Hostile Review Thread Gate Deterministic: unresolved hostile-reviewer threads exist Fails until resolved (not yet a required context)
degraded verdict Fewer than 2 models succeeded (infra) No

Powered by omniintelligence.review_pairing.cli_review — multi-model adversarial review: qwen3-review, qwen3-review-b, glm-review (OMN-8468/OMN-8524/OMN-17492)

…onsented to

A detector nobody reads is the failure OMN-16030 already paid for once, and
the durable local artifact this alarm writes is exactly that unless something
carries a raised alarm to a person. An operator ruling of 2026-09-21T14:46:45Z
authorizes one channel, #omninode-notifications, using the lab bot token by
reference. No agent message is consent: the only thing that authorizes a send
is a durable ledger row, resolved at run time, and every run records the
citation it resolved or the fact that it resolved nothing.

The credential is named, never valued. It is read out of an env file by NAME
at send time, held for one request, and never logged, never written to the run
record, never put on a command line and never included in an error message --
a failure names the variable and the file instead. An env file rather than the
process environment because this runs under launchd, which inherits no shell.

Delivery is per NEWLY RAISED alarm, so the edge-triggering is also the
channel's deduplication: a condition that stays bad posts once, not once an
hour. A failed send is recorded as a failure rather than swallowed.

The consent resolver now accepts the shape the real authorization was recorded
in -- a dated RULING carrying the OPERATOR-CONSENT token, an approved_by, the
channel and an exclusion clause -- as well as rule 18's two-labelled-list row.
It requires the substance and not the label strings, because a gate that
refused a real dated operator-attributed channel-naming authorization over two
missing labels is a spelling test and the next lane routes around it. Five
refusal controls keep that from being a rubber stamp: no approver, no channel,
a different channel, no exclusion clause, and not a consent row at all.

Eight new controls in all, including the falsifier that a posting path is
reachable with no resolvable row, that the token reaches neither the run
record nor the rendering nor a delivery id, that a failed send is recorded,
and that a repeated condition delivers nothing.

Proven live rather than described. One injected FAIL receipt delivered 1/1 to
#omninode-notifications citing ROLLING_WORK_LEDGER.md:3523; the immediate
repeat of the same condition recorded "nothing new to deliver this run". The
lane was never touched: only the three conditions' inputs were injected.

config/skip_count_baseline.yaml records the live-ledger consent readback,
557 -> 558, with provenance. It reads a file outside this repository, so CI
has no ledger to read and it skips with a named reason rather than passing.
A skip is safe there and the addendum says why: consent is resolved at run
time, so a vanished row disables delivery instead of silently enabling it.

ALSO IN THIS COMMIT, and not this ticket's work: the vendored copy of
omnimarket's node_projection_consumer_flow/0005_add_node_id_ingest_index.sql
plus its row in docker/migrations/forward/_ledger/application-migrations.tsv.
That migration landed in omnimarket dev today and the vendor-sync pre-commit
hook refuses every infra commit until it is mirrored 1:1, while the
declaration validator then refuses the mirror until it is declared. The pair
is mechanical -- the file is copied byte for byte and the row carries the
same owner, source and omninode_internal domain as the five sibling
consumer_flow rows, with the checksum taken from the copied file -- and it is
carried here only because the two hooks together block any other commit in
this repository. Recorded as friction rather than absorbed silently.

Claude-Session: https://claude.ai/code/session_01EYUMfw1eY4umCKvhQvS8ZF
Onex-Lane: omn18868-67-seam-gates-restart
Onex-Session: 54dc70ab29954d59a53b739d50029c02

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hostile Reviewer — adversarial findings (OMN-17492)

Models succeeded: glm-review
Models failed: codex
New finding threads: 0
Deduped (already posted on this PR): 0
Below quorum (one model only, reported not threaded): 9
Nit-level findings suppressed: 2

The model is the FINDER, never the gate: merge is gated only by the
deterministic Hostile Review Thread Gate, which blocks while
hostile-reviewer threads are unresolved. Resolve each thread after
addressing (or rejecting, with a reply) its finding.

Below quorum: 9 finding(s) raised by one model only (OMN-18479)

These are reported and NOT dropped, but they get no thread and do not block: a single model's finding no other model reproduced is not evidence enough to stop a merge. Read them; act on them if they are right.

  • [MAJOR] docker/migrations/forward/nodes/node_projection_consumer_flow/0005_add_node_id_ingest_index.sql (glm-review) — CREATE INDEX CONCURRENTLY failure leaves an invalid index and IF NOT EXISTS masks it | If the CONCURRENTLY build fails or is interrupted, Postgres leaves an INVALID index with the given name. On every
  • [MAJOR] scripts/lab_alarm.py, run_once (glm-review) — Failed Slack delivery permanently suppresses the alarm | select_new_alarms marks each raised alarm active in state before delivery is attempted, and the state is saved before any post_alarm call. When
  • [MAJOR] scripts/lab_alarm.py, resolve_posting_consent (glm-review) — Consent channel match is unanchored substring containment | Both the labelled and ruling consent paths authorize on channel.lower() in body.lower(). A row that grants channel A and explicitly exclud
  • [MAJOR] scripts/lab_alarm.py, evaluate_consumer_group_lag (glm-review) — One unreadable group suppresses alarms for comparable groups | In evaluate_consumer_group_lag, any unreadable group returns INDETERMINATE before the growth comparison, discarding the previous sample's
  • [MINOR] scripts/lab_alarm.py, ModelAlarmState.load (glm-review) — Corrupt lag_sample values crash state loading despite the documented fallback | ModelAlarmState.load documents that an unreadable state file is treated as empty, but the comprehension {str(k): int(v)
  • [MINOR] scripts/lab_alarm.py, ModelAlarmState.save (glm-review) — State file writes are not atomic | state.save uses path.write_text directly. A crash or power loss mid-write leaves a truncated state.json, which load() treats as empty, dropping every active alarm an
  • [MINOR] scripts/lab_alarm.py, run_once; scripts/launchd/install-lab-alarm.sh (glm-review) — No inter-process locking on the state directory | The installer's readback instructs the operator to run the alarm by hand at the same state_dir the launch agent uses. If a manual run overlaps the hou
  • [MINOR] scripts/lab_alarm.py, post_alarm (glm-review) — PostingError messages may leak urllib exception internals into the durable log | post_alarm catches Exception and formats str(exc) into PostingError, which run_once embeds verbatim into the posting st
  • [MINOR] config/lab_alarm.json; scripts/lab_alarm.py, evaluate_container_restarts (glm-review) — Restart bound is cumulative and never reset relative to deploys | docker's RestartCount is cumulative since container creation. The config comment asserts omninode-runtime is 'recreated on every deplo

…g; dev landed it

The vendor sync and its declaration reached dev while this branch was open, so
the branch's own copy became a second identical declaration and CI refused it
in three places at once: 'FATAL: double migration declaration for artifact
nodes/node_projection_consumer_flow/0005_add_node_id_ingest_index.sql', which
took the Node Migration Declaration Check, two test shards and the fence
parity pair with it.

dev's copy is kept and this branch's is removed, so docker/migrations is now
byte-identical to dev. The friction stands and is recorded: the two hooks that
forced a lane with no stake in that migration to carry it are still arranged
that way, and the next lane to commit here before a sync lands will carry it
again.

Claude-Session: https://claude.ai/code/session_01EYUMfw1eY4umCKvhQvS8ZF
Onex-Lane: omn18868-67-seam-gates-restart
Onex-Session: 54dc70ab29954d59a53b739d50029c02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant