Repository navigation
feat(OMN-19507): the dev-200 deploy agent defers a deploy while .200's model servers need the host; .101 is out of the deploy pool - #4128
Conversation
…e overlay on the 42xxx block Onex-Lane: dev-200-lane Onex-Session: 50705c397acd4413bdee8c1379652cf8
…, dev-200 declared routed-nothing, one launchd template for macOS lab hosts Onex-Lane: dev-200-lane Onex-Session: 50705c397acd4413bdee8c1379652cf8
…0-load-gate # Conflicts: # deploy/lane-census/lane-manifest.yaml # scripts/generate_claude_lane_block.py # tests/unit/scripts/test_lane_census_manifest_parity.py
… servers need the host; .101 is an excluded host A load_gate: block on a routing-table instance makes its agent defer, not refuse, a command for itself (and an idle converge) while load1 per cpu or the memory headroom a rebuild would leave (free less the Docker VM's growth less the model server's unmapped working set) is past the declared threshold. Nothing is committed or published; the partition is paused and resumed once a re-check opens the gate. An unreadable input defers by name. Only dev-200 declares a gate, so the .201 and .202 agents are unchanged. excluded_hosts: in the table names .101 (hostname stickybeatz) per operator RULING omni_home ledger:3893 item (c); no instance may name it and no agent may run as it, even with DEPLOY_AGENT_INSTANCE set. Onex-Lane: lab-200-deploy-83 Onex-Session: c2d0be217b2d4bc7b90c63f1e06e5adc
… from the measured baseline Onex-Lane: lab-200-deploy-83 Onex-Session: c2d0be217b2d4bc7b90c63f1e06e5adc
… gate after logging is configured Onex-Lane: lab-200-deploy-83 Onex-Session: c2d0be217b2d4bc7b90c63f1e06e5adc
…ainst its own env store, not the host's workstation file The first real dev-200 job (lab proof, .200, 2026-09-25T13:32Z) failed at preflight: validate_llm_endpoint_env_contract always read ~/.omnibase/.env, which on .200 is the operator's workstation file (LLM_EMBEDDING_URL on port 8100) that the lane never reads, while the lane's own store dev-200.env carried the canonical 8002. The store the unit declares (DEPLOY_AGENT_ENV_FILE) is now the file checked; .201 declares ~/.omnibase/.env itself, so it is unchanged. Onex-Lane: lab-200-deploy-83 Onex-Session: c2d0be217b2d4bc7b90c63f1e06e5adc
Resolves the routing table against #4105 (verify: blocks) and #4116 (the dev-200 lane declaration): dev-202 keeps its lane: and verify: blocks, and dev-200 gets a verify: block on its 42xxx ports with receipt lane compose-dev-200 (EnumLabLane.COMPOSE_DEV_200, the same hunk as #4118). No route names dev-200: no runner carries host-200 yet. Onex-Lane: lab-200-deploy-83 Onex-Session: c2d0be217b2d4bc7b90c63f1e06e5adc
…fy targets match its overlay The routing table describes an instance twice: the lane: block the deploy agent composes from (OMN-19543) and the verify: block the per-merge verify job reads (#4105). Both name the receipt lane, compose project and containers, so they are pinned together, and dev-200's verify targets are pinned to docker-compose.dev-200.yml as dev-202's are to its overlay. No route may name dev-200 while no runner carries host-200. Onex-Lane: lab-200-deploy-83 Onex-Session: c2d0be217b2d4bc7b90c63f1e06e5adc
…ibase_infra#4128 (#11294) * evidence(OMN-19507): author OCC companion for OmniNode-ai/omnibase_infra#4128 OCC companion by node_pr_lifecycle_fix_effect (OMN-13317 F1 / OMN-13990 / OMN-14285). Product PR head b01886427756621efc57043b4e4c6f1cf2023a47. * evidence(OMN-19507): self-bind OCC#11294 + rebind contract_sha256 --------- Co-authored-by: omnimarket-bot <bot@omninode.ai>
✅ Hostile Reviewer — REVIEWEDCritical findings: 2
Semantics (OMN-17492 — the model finds, thread resolution gates)
Powered by omniintelligence.review_pairing.cli_review — multi-model adversarial review: qwen3-review, qwen3-review-b, glm-review (OMN-8468/OMN-8524/OMN-17492) |
… first real dev-200 deploy The per-merge verify job derives its ceiling from the routed lane's declaration, so a lane without one refuses at the budget read. dev-200's boots are measured on the lab-proof job fec4e429 at omnibase_infra f43e7d9: omninode-runtime /health after 358.7s, runtime-effects after 712s from compose-up with one verify recreate, both inside the same 900s compose-dev and compose-dev-202 declare. Onex-Lane: lab-200-deploy-83 Onex-Session: c2d0be217b2d4bc7b90c63f1e06e5adc
There was a problem hiding this comment.
Hostile Reviewer — adversarial findings (OMN-17492)
Models succeeded: qwen3-review, qwen3-review-b, glm-review
Models failed: none
New finding threads: 2
Deduped (already posted on this PR): 0
Below quorum (one model only, reported not threaded): 14
Nit-level findings suppressed: 1
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: 14 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]
scripts/deploy-agent/deploy_agent/consumer.py, _maybe_resume_after_load_gate and poll_and_accept(qwen3-review) — Load gate deferral leaves partition paused indefinitely if recheck clock is not advanced | Inconsumer.py, when a command is deferred by the load gate, the partition is paused andload_gate_paused - [MAJOR]
scripts/deploy-agent/deploy_agent/load_gate.py, decide function(qwen3-review) — Load gate memory calculation does not account for memory already allocated to the Docker VM | The load gate's memory headroom calculation is: `headroom = free - build_reserve - max(0, model_reserve - - [MAJOR]
scripts/deploy-agent/deploy_agent/consumer.py(qwen3-review-b) — Load gate deferral does not handle coalescing or multiple pending records | When the load gate defers a command, the consumer seeks back to the current record's offset and pauses the partition. Howeve - [MAJOR]
scripts/deploy-agent/deploy_agent/routing.py(qwen3-review-b) — resolve_instance checks excluded_hosts before DEPLOY_AGENT_INSTANCE, breaking explicit instance selection on excluded hosts | The new code in resolve_instance checks if the host is in excluded_hosts B - [MINOR]
scripts/deploy-agent/deploy_agent/consumer.py, load_gate_paused field and _maybe_resume_after_load_gate(qwen3-review) — Missing test for load gate deferral when multiple partitions are paused | The load gate pauses a single partition when a command is deferred. However, if multiple commands are in flight and multiple p - [MINOR]
scripts/deploy-agent/deploy_agent/load_gate.py, probe_host_load and _run(qwen3-review) — Load gate probe runs subprocesses on every check, which can be expensive | Theprobe_host_loadfunction runssysctl,memory_pressure, andioregsubprocesses on every check. The `recheck_second - [MINOR]
scripts/deploy-agent/deploy_agent/routing.py, resolve_instance(qwen3-review) — Excluded host check uses first hostname label, which may not match the operator's intent | Theexcluded_hostskey is the lowercased first hostname label. The comment says.101has hostname `Sticky - [MINOR]
scripts/deploy-agent/tests/unit/test_load_gate_omn19507.py, test_load_gate_consumer_resumes_the_partition_once_a_recheck_opens(qwen3-review) — Test for load gate deferral does not verify that the partition is not resumed when the gate is still shut | The testtest_load_gate_consumer_resumes_the_partition_once_a_recheck_opensverifies that - [MINOR]
scripts/deploy-agent/deploy_agent/agent.py, _idle_converge_gate_open(qwen3-review) — The load gate is only checked for the consumer's own instance, but the idle converge does not check the gate for the routed instance | The load gate is checked in the consumer's_process_messagefor - [MINOR]
config/deploy_lane_routing.yaml, routes(qwen3-review) — The dev-200 instance is declared but has no route, so it will never receive commands | Thedev-200instance is declared in the routing table, but no route names it. This means that no command will e - [MINOR]
scripts/deploy-agent/deploy_agent/load_gate.py(qwen3-review-b) — Load gate check spawns subprocesses on every poll cycle when gate is open | The LoadGate.check() method is called on every poll cycle when the gate is not paused (via _maybe_resume_after_load_gate or - [MINOR]
scripts/deploy-agent/tests/unit/test_load_gate_omn19507.py(qwen3-review-b) — Missing test for load gate interaction with host_slot busy state | The load gate check (step 5b) occurs after the host_slot busy check (step 5a). The tests cover the case where the gate defers and whe - [MINOR]
scripts/deploy-agent/deploy_agent/executor.py(qwen3-review-b) — DEV_INSTANCE_LANE_CONFIGS is built at import time from the committed table, creating a hidden dependency on file system state | The module-level constant DEV_INSTANCE_LANE_CONFIGS is built by calling - [MINOR]
scripts/deploy-agent/tests/unit/test_deploy_agent_instances_as_data_omn19543.py(qwen3-review-b) — Test for health bind host uses subprocess to import module, which is fragile and slow | The test test_the_health_bind_defaults_to_every_interface uses subprocess.run to import the agent module in a fr
Findings demoted from threads (anchor rejected)
-
[MAJOR] hostile-reviewer (qwen3-review)
LaunchAgent plist exposes Kafka SASL credentials in plaintext environment variables | The
deploy-agent-instance.plist.templatesetsKAFKA_SASL_MECHANISM=SCRAM-SHA-256andKAFKA_SECURITY_PROTOCOL=SASL_PLAINTEXTas environment variables. While the actual SASL username and password are not in the plist (they come from the env file), the security protocol and mechanism are exposed. More critically, theDEPLOY_AGENT_ENV_PROTECTEDlist includesKAFKA_SASL_ENV_PREFIXbut not the actual SASL credentials,Resolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MAJOR] hostile-reviewer (qwen3-review-b)
LaunchAgent plist exposes internal network topology and credentials in plaintext | The rendered LaunchAgent plist contains hardcoded internal IP addresses (192.168.86.201:19092 for Kafka) and SASL mechanism details. While the plist is rendered to a user-local path (~/.omnibase/...), the template itself is committed to the repository. The KAFKA_BOOTSTRAP_SERVERS value is a static internal IP. If the repository is ever exposed or if the plist is accidentally committed with sensitive values, this leaks interna
Resolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492).
There was a problem hiding this comment.
Hostile Reviewer — adversarial findings (OMN-17492)
Models succeeded: qwen3-review, qwen3-review-b, glm-review
Models failed: none
New finding threads: 6
Deduped (already posted on this PR): 0
Below quorum (one model only, reported not threaded): 10
Nit-level findings suppressed: 1
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: 10 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]
scripts/deploy-agent/deploy_agent/agent.py(qwen3-review) — Health endpoint binds to 0.0.0.0 by default | TheHEALTH_BIND_HOSTdefaults to0.0.0.0ifDEPLOY_AGENT_BIND_HOSTis not set. While the launchd plist explicitly sets it to127.0.0.1, the system - [MAJOR]
scripts/deploy-agent/deploy_agent/consumer.py(qwen3-review) — Load gate deferral does not handle coalescing of newer commands | The load gate defers a command by seeking the partition back to the current offset and pausing it. If a newer command for the same cor - [MAJOR]
scripts/deploy-agent/deploy/launchd/deploy-agent-instance.plist.template(qwen3-review-b) — Kafka SASL credentials exposed in LaunchAgent plist template | The newdeploy-agent-instance.plist.templatehardcodesKAFKA_BOOTSTRAP_SERVERS,KAFKA_SECURITY_PROTOCOL,KAFKA_SASL_MECHANISM, an - [MAJOR]
scripts/deploy-agent/deploy_agent/load_gate.py(qwen3-review-b) — Load gate memory calculation uses free percent, not absolute free bytes, leading to inaccurate headroom | Thedecidefunction calculates `free_gib = reading.memory_total_gib * reading.memory_free_pe - [MAJOR]
scripts/deploy-agent/deploy_agent/load_gate.py(qwen3-review-b) — Load gate probe spawns 4 subprocesses on every check, with no caching | Each call toLoadGate.check()invokesprobe_host_load(), which spawns up to 4 subprocesses:os.getloadavg()(no subprocess - [MAJOR]
scripts/deploy-agent/deploy_agent/routing.py(qwen3-review-b) — Excluded host check uses first hostname label, which may not match the operator's intent | Theresolve_instancefunction extracts the first label of the hostname: `host = (hostname or socket.gethost - [MAJOR]
scripts/deploy-agent/tests/unit/test_load_gate_omn19507.py(qwen3-review-b) — No test for load gate deferral timeout or escalation | The tests for the load gate cover the decision logic, the consumer's deferral behavior, and the idle converge's wait. However, there is no test f - [MINOR]
scripts/deploy-agent/deploy_agent/load_gate.py(qwen3-review) — Load gate probe runs subprocesses on every check | Theprobe_host_loadfunction runssysctl,memory_pressure, andioregsubprocesses on every call toLoadGate.check(). This is called on ever - [MINOR]
scripts/deploy-agent/tests/unit/test_load_gate_omn19507.py(qwen3-review) — Missing test for load gate deferral with multiple messages in a batch | The tests for the load gate consumer deferral only test the case where a single message is deferred. There is no test for the ca - [MINOR]
scripts/deploy-agent/deploy_agent/load_gate.py(qwen3-review-b) — Load gaterecheck_dueusestime.monotonic, which is not affected by system clock changes, but the recheck interval is not configurable per instance | Therecheck_secondsis a field in `ModelLoad
Findings demoted from threads (anchor rejected)
-
[CRITICAL] hostile-reviewer (qwen3-review)
Load gate deferral loses pending command on partition resume | When the load gate defers a command, the consumer seeks the partition back to the deferred record's offset and pauses it. However, when the gate later opens and the partition is resumed, the consumer does not re-process the deferred record. The record was never committed, so it remains at the paused offset. Upon resume, the consumer will fetch from that offset, but the code path in
poll_and_acceptthat handles the deferred record is not re-entResolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[CRITICAL] hostile-reviewer (qwen3-review-b)
Load gate deferral leaves partition paused indefinitely if gate never opens | When the load gate defers a command, the consumer seeks back to the current offset and pauses the partition. The partition is only resumed when
_maybe_resume_after_load_gatedetects the gate is open. However, if the host remains under load indefinitely (e.g., a model server is stuck in a high-memory state), the partition stays paused forever. The agent will never process any commands for that instance, and there is no timeout orResolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MINOR] hostile-reviewer (qwen3-review)
Instance lanes are loaded at module import time | The
DEV_INSTANCE_LANE_CONFIGSis built at module import time by callingbuild_dev_instance_lane_configs(load_instance_lanes(AGENT_CLONE_ROOT)). This means the routing table is read from disk when the module is imported. If the routing table is updated while the agent is running, the agent will not see the new configuration until it is restarted. This is consistent with the existing behavior for the routing table, but it means that adding a new instance rResolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MINOR] hostile-reviewer (qwen3-review)
Test for health bind host uses subprocess | The test
test_the_health_bind_defaults_to_every_interfaceuses a subprocess to import the agent module and check theHEALTH_BIND_HOSTvalue. This is because the module reads the environment variable at import time. This is a valid approach, but it is slow and fragile. It relies on the Python interpreter being available and the module being importable in the subprocess. This could be flaky in CI environments where the Python path is not set up correctly. | EvidResolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MINOR] hostile-reviewer (qwen3-review-b)
Instance lane configs are built at module import time, preventing dynamic reconfiguration | The
DEV_INSTANCE_LANE_CONFIGSdict is built at module import time by callingbuild_dev_instance_lane_configs(load_instance_lanes(AGENT_CLONE_ROOT)). This means the instance compositions are fixed at agent start. If the routing table is updated (e.g., a new instance is added), the agent must be restarted to pick up the change. This is consistent with the existing design (the router is also read at start), but it mResolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492). -
[MINOR] hostile-reviewer (qwen3-review-b)
Test for dev-200 composition agreement with overlay does not verify port conflicts | The test
test_dev_200_composition_agrees_with_its_overlayverifies that the health ports in the lane config match the ports published by the overlay. However, it does not verify that these ports do not conflict with other services on the host. The comment says 'its port block is 42xxx, below the macOS ephemeral range', but there is no test that verifies the ports are not already in use by other services. The `preflight_poResolve this thread when addressed — the
Hostile Review Thread Gateblocks while hostile-reviewer threads are unresolved (OMN-17492).
What
Ticket OMN-19507, epic OMN-19500. Authority: omni_home ledger RULING 2026-09-25T12:40:36Z (ledger:3893), items (b) "a deploy lane on .200 with a load gate" and (c) "leave .101 out of the deploy pool and leave its dogfood copy as is".
This branch carries #4117 (deploy-agent instances as data, dev-200 declared and routed nothing) merged with current dev. #4117 is DIRTY against dev since #4105 landed. This PR resolves that conflict, so landing it lands #4117's two commits (
f7e80c80b,63376a979) too, and #4117 can then be closed as superseded. #4118 (compose-dev-200 receipt lane reader) stays stacked on top and carries the sameEnumLabLane.COMPOSE_DEV_200hunk, so it merges cleanly after this.Summary (first draft by lab delegation run
c1dd9e9d-6315-4622-9c3c-3a9bd578e92efrom a fact list, then corrected against the diff):Load gate. The new module
scripts/deploy-agent/deploy_agent/load_gate.pygives an instance a gate. When the instance declaresload_gate:inconfig/deploy_lane_routing.yaml, its agent does not accept a rebuild command, and does not start an idle converge, while the gate is shut. A deferral commits no offset and publishes nothing, so the command waits on the topic. The gate has two rules:max_load1_per_cpudefers;headroom = free - build_reserve - max(0, model_reserve - gpu_in_use)belowmin_headroom_gibdefers.An input the agent cannot read defers the job, and the deferral names that input. Only dev-200 declares a gate.
.101 out of the pool.
excluded_hosts:in the routing table names.101(first hostname labelstickybeatz; the .200 host isstickybeatz-studio), with the ruling cited in the value. The parser refuses an instance that names it. An agent on that host refuses to start, even withDEPLOY_AGENT_INSTANCEset. The .101 dogfood copy is not touched.dev-200 verify targets. Since feat(OMN-19507): the per-merge verify jobs read the lane the deploy agent routed the merge to #4105, every instance needs a
verify:block. dev-200 now has one on its 42xxx ports: receipt lanecompose-dev-200, runner labelsself-hosted, omnibase-verify, host-200.EnumLabLane.COMPOSE_DEV_200is added outsideANY_OF_DEFAULT_LANES.No route names dev-200. No runner carries
host-200. The org runners API, read at 2026-09-25T14:58Z, lists 71 runners and none on .200. A test pins this, and it is the one test that changes when a verify runner is registered there and a route is added.A fix found by the first real dev-200 job (2026-09-25T13:32Z, job
9abe33a6). The LLM endpoint env contract check always read~/.omnibase/.env. On .200 that is the operator's workstation file (embedding port 8100), which the lane never reads. It now checks the store the agent was launched against (DEPLOY_AGENT_ENV_FILE). On .201 that declared store is~/.omnibase/.envitself, so nothing changes there.The agent logs its routing instance and load gate after logging is configured. Before this change, the dev-200 bring-up found no instance line in the log (ledger 2026-09-25T11:23:31Z, finding 2).
Measured baselines for the .200 thresholds
The .200 host is Stickybeatz-Studio: 24 cpu (
sysctl hw.ncpu) and 192 GiB unified memory. It also runs the orchestration sessions and the gpt-oss-120bllama-serveron :8130, with every layer on the GPU. On this machine GPU memory is system memory.build_reserve_gibdocker infoMemTotal 33333612544). The running dev-200 lane holds about 7.3 GiB of it (docker stats, ledger 2026-09-25T11:23:31Z).model_reserve_gibllama-serverwas serving (ioreg -c IOAccelerator12:49:08Z).min_headroom_gibmax_load1_per_cpullama-server /metrics12:57Z), against 68.6 tok/s at 12:42Z. The sessions-and-provers baseline peaked at load1 54.31, which is 2.26 per cpu (uptime13:04Z).Tests
scripts/deploy-agent/tests/unit/test_load_gate_omn19507.py: 23 tests. They cover the decision, the macOS readers (memory_pressure,ioreg), the probe never raising, recheck cadence, the table parsing, the committed table gating dev-200 only, the excluded-host refusals, the consumer deferring without commit or publish and resuming once a recheck opens, and idle converge waiting.test_llm_endpoint_env_contract_guard.pycovers the env-store fix.tests/ci/test_dev_200_verify_targets_omn19507.py(new, 4 tests): eachlane:block agrees with itsverify:block, dev-200's verify targets agree withdocker-compose.dev-200.yml, no route names dev-200, and compose-dev-200 declares a settle budget inside its bounds (config/lab_pass_settle_budget.yaml, boots measured on the lab job below).053d2305f), 2 fail:KeyError: 'verify', and0 instances emit receipt lane 'compose-dev-200'. Against the pre-image settle file, the budget read raisesSettleBudgetError ... declares no settle budget for lane 'compose-dev-200'.Lab (rule 24)
Run on .200 under ledger HOLD
2026-09-25T15:25:07Z-lab-200-deploy-83(surface 200-compose-dev; it extends2026-09-25T13:05:42Z-lab-200-deploy-83). The agent clone~/onex-lanes/dev-200/omnibase_infraruns this branch atb01886427. After the LaunchAgent kickstart, its log readsDeploy agent routing instance: dev-200, consumer group onex-deploy-agent-dev-200, 0 route(s), default dev-201, thenDeploy agent load gate: {max_load1_per_cpu 3.0, build_reserve_gib 24, model_reserve_gib 70, min_headroom_gib 16, recheck_seconds 60}andloaded_code_sha b01886427(15:34:03Z).The gate deferred, then opened, on the real thresholds. The proof ran one real deploy of the omnibase_infra dev head
f43e7d9b3a38through the agent's own job code (DeployAgent._execute_command), with the gate checked first and the two bus publishes stubbed, so the shared dev bus carries no lab-proof terminal event.fec4e429-f9fa-4818-a1b9-1919138255e9, accepted 16:00:31Z and finishedsuccessat 16:21:49Z. Its phases were preflight, git, compose_gen, runtime and verification, all success; seed and core were skipped. The agent's/job/<id>endpoint reads the same record.9abe33a6) had failed there.omninode-runtimeanswered /health after 358.7s.runtime-effectsneeded the agent's one verify recreate and recovered after 241s.org.opencontainers.image.revision=f43e7d9b3a38. omnimarket is vendored at11ad3ddccad1, the omnimarket dev head.llama-server(gpt-oss-120b, :8130) was never stopped or restarted. A read-only sampler polled it every 30s. Across the job's build and compose-up, from 16:08:01Z to 16:21:49Z, it took 28 samples: every /health answered 200 in at most 5.0 ms, host free memory held at 86-87%, and load1 ran from 45 to 76.Receipts. The lane's verify job ran locally with
lab_pass_receipt.py(converge, probe-lane, emit, verify), reading loopback URLs instead ofhost.docker.internal, which does not resolve on the host itself. Both receipts are FAIL, on one check each:f43e7d9b3a3850b551c654fc54a211ad3ddccad1533a914339f2fb4dhealth_dimensions:discovery_errors, unhealthy across the full 900s budget. The runtime log names the cause:contract 'projection_lab_lane_health' is scoped to runtime lanes [compose-dev, onex-lab, onex-lab-k3s] and this runtime cannot name its lane (ONEX_RUNTIME_LANE='compose-dev-200' is not a registered lane).Runtime classification
This PR is runtime-affecting because it changes the deploy agent. On .201 nothing changes: dev-201 declares no gate and no lane block, and the table routes nothing to a new instance. On .202 nothing changes either: dev-202 declares no gate. SOLO, with the .201 agent idle.
Evidence-Ticket: OMN-19507
Evidence-Source: OCC#11294