Repository navigation
feat(OMN-18867): a durable lab alarm on FAIL receipts, restart loops and growing consumer lag - #3930
Conversation
…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
…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>
|
| 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
There was a problem hiding this comment.
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 onchannel.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
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
StartCalendarIntervalagent, 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
Hourkey, deliberately: aMinute-only interval matches every hour, and adding anHourwould turn this into a daily job that still reads as periodic, leaving detection bound at 24 hours instead of one.RunAtLoadandKeepAliveare 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
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_nothingis 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:
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 intoOKis 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
INDETERMINATEdoes 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
dockerwould 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,
/readysecond, and the evidence names which surface answered.The one recorded skip, and why it is safe
config/skip_count_baseline.yamlgrows 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
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.sqland 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.devhas since landed both halves, so this branch carries neither anddocker/migrationsis byte-identical todev. 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
bash scripts/launchd/install-lab-alarm.sh(the installer prints the exact command and both readback checks).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