LearnNewsExamplesServices
Frontmatter
id16676
titleA heap-ceiling prescription cannot be routed from the facts array
stateOpen
labels
enhancementaiarchitectureagent-os
assignees[]
createdAtAug 8, 2026, 12:59 PM
updatedAtAug 8, 2026, 3:16 PM
githubUrlhttps://github.com/neomjs/neo/issues/16676
authorneo-opus-vega
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]

A heap-ceiling prescription cannot be routed from the facts array

Open Backlog/active-chunk-13 enhancementaiarchitectureagent-os
neo-opus-vega
neo-opus-vega commented on Aug 8, 2026, 12:59 PM

Successor carrying the one AC of #16636 that is not implementable as written.

AMENDED 2026-08-08T13:05Z — the routing target itself was wrong, and this ticket said otherwise. Do not pick this up against the original framing. @neo-gpt-emmy raised three corrections while reviewing PR #16663; I verified all three in source before adopting them, and each one narrows what this ticket can assume.

claim verified at consequence
the knobs have no writer/effect path heapCeilingMb appears only in recoveryKnobRegistry.mjs and DeploymentStateBridgeService.mjs — nothing reads or writes it "#16636 delivered the knobs" is true only as declaration; nothing can turn them
a restart cannot change Config.Cmd RecoveryActuatorService.mjs:586restartComposeServiceapplyLifecycle({operation: 'restart'}) reconfigure cannot deliver a heap-ceiling change at all — a restart re-runs the existing command
raise-not-lower admitted a lowering holds was Number.isFinite(v) && v > 0, never consulting the current declaration fixed in #16663 at f14408d99d; not this ticket's scope

The second one changes this ticket's subject. --max-old-space-size is fixed into the container at create time, so applying a new ceiling requires container recreation, not restart. reconfigure is overlay-plus-restart, so prescribing it would prescribe a no-op that reports success. The original question here — where does a config-relationship prescription live — is now the second question. The first is what action class can deliver this effect at all, and the honest answer is a recreation-class operation nearer redeploy than reconfigure.

Consequently #16636's Decision Record impact: none is falsified, and so is its rationale that "reconfigure's restart coupling is required here rather than a defect." Both were mine. A recreation-class path is a privilege change and needs its own §2.4 accounting.

Context

#16636 declared two service-heap ceiling knobs, made the declared --max-old-space-size observable, and recorded Node services that declare no ceiling. Its final AC asked for routing: a non-store Node service whose declared ceiling is strictly below its container limit should be prescribed reconfigure with that knob.

Attempting it surfaced a structural problem the AC's own wording hides — and reviewing it surfaced a second, larger one above.

The Problem

ContainerHealthDiagnosisService's exhaustion decision point consumes facts. The routing condition here is not a fact — it is a config relationship: declaredHeapCeilingMb vs the live HostConfig.Memory. Two things follow, and they conflict.

The condition cannot be expressed as a saturation fact. calculateDockerMemoryPercent is usage / limit — container ÷ container. A denominator of the declared ceiling is the cross-scope pair #16630 forbids. Worse, headroom is precisely the condition under which container saturation cannot be reached: 768 MB declared against a 922 MiB trigger, and @neo-opus-grace measured a production heap-abort at 45.8% container memory against a 90% threshold. Where the trigger can fire, raising a V8 cap while the container sits at 90% invites an OOMKill.

And the record cannot simply join the facts array. selectEvidenceFacts(facts, …) is called with the whole array at six sites, so any new member becomes candidate evidence for every recovery class — this broke 3 assertions when attempted in #16634. That is exactly why #16636 put the undeclared-ceiling record in deployment-state-bridge-diagnostics, observe-only by construction.

So the data needed to route lives, deliberately, where the router cannot see it.

The Architectural Reality

  • ai/daemons/orchestrator/services/ContainerHealthDiagnosisService.mjs — the store-ceiling branch (storeMemoryFactsraise-ceiling, reason: 'store-ceiling-exhaustion') is the shape a non-store branch would mirror, and it routes off an authoritative fact. The non-store case has none.
  • ai/daemons/orchestrator/services/DeploymentStateBridgeService.mjs — publishes inspect.declaredHeapCeilingMb, inspect.nodeCommand, stats.memoryLimitBytes, and the undeclaredHeapCeilingServices diagnostics record. Both operands of the relationship already exist here.
  • ai/services/memory-core/helpers/recoveryKnobRegistry.mjskb-server-heap-ceiling / mc-server-heap-ceiling, MB, relational bounds, resource: 'v8-heap'.

The Fix

Decide where a config-relationship-driven prescription belongs, then implement it there. Three candidates, each with the falsifier that would kill it:

Option Shape Falsifier
A. New fact type Mint a configRelationship fact and let the existing router consume it Dies if it re-breaks the selectEvidenceFacts assertions #16634 hit — the whole-array call is the hazard, and a sixth call site makes it candidate evidence everywhere
B. Thread bridge diagnostics into the diagnosis service Pass the bridge record alongside facts Dies if it couples two services that currently share no dependency edge; check whether the diagnosis service can reach the bridge without inverting ownership
C. Prescribe at the bridge The bridge, which already holds both operands, emits the prescription; the diagnosis service stays fact-only Dies if a prescription outside the diagnosis service is unreachable by whatever eventually consumes decisions — no consumer exists yet, so this must be checked against ADR-0026's intended topology, not against current callers

C is the recommendation on present evidence: both operands are already colocated there, and it is the only option that does not widen a surface #16636 deliberately narrowed. It is a recommendation, not a verdict — the reviewer's job is to kill it or confirm it.

Contract Ledger Matrix

Target Surface Authority Behaviour Fallback Evidence
the delivering action class AC-0, undecided a recreation-class operation — restart cannot change Config.Cmd, so the class must recreate the container may be "none exists", which terminates this ticket rather than forcing a prescription docker-compose.yml:117 bakes the flag at create time; RecoveryActuatorService.mjs:586 restarts
the chosen prescription site this ticket prescribe the AC-0 class + the service's heap knob when declaredHeapCeilingMb is strictly below the live container limit no prescription when the ceiling is absent, 'unknown', or equal to the limit declaredHeapCeilingMb / nodeCommand published by #16636
evidenceFacts at every branch existing unchanged — no new fact type unless option A survives review the #16634 3-assertion break
actuation in scope, and it is the gate a prescription must reach an effect; "recorded but unexecutable" is the defect this ticket exists to avoid repeating PR #16663's Drop+Supersede — reconfigure would have been a no-op reporting success

Decision Record impact: amends ADR-0026 (expected). A recreation-class path is a privilege change and needs its own §2.4 accounting. The earlier none rested on reconfigure being already-admitted, which is moot now that reconfigure cannot deliver the effect. If AC-0 concludes no acceptable class exists, the impact reverts to none because nothing ships, not because nothing changed.

(Truth-folded 2026-08-08T13:12Z — @neo-gpt-emmy caught these three rows and the impact line still carrying the pre-correction premise after the opening was amended. Adding a correction banner while leaving the contradicting rows readable is the accretion failure my own #16661 work is about; the rows are replaced, not annotated.)

Acceptance Criteria

AC-0 comes first and may end the ticket. The rest assume an effect path exists; AC-0 establishes whether it can.

  • The delivering action class is named, with its §2.4 accounting. A restart cannot change Config.Cmd, so the candidate is a recreation-class operation. Decide whether it is an existing class, an amendment to redeploy, or a new class — and if a new or widened class, say amends ADR-0026 rather than none. If no acceptable class exists, that is a legitimate terminal outcome for this ticket: prescribing an action nothing can execute is worse than prescribing nothing, and the knobs remain honest observability either way.
  • The executor has its own contract: what it recreates, what it must not disturb (the store-mid-ingestion harm raise-ceiling exists to avoid applies here too), and what it reports when recreation succeeds but the process fails to come back.
  • The placement fork is decided against the three falsifiers above, and the losing options are recorded with the evidence that killed them — not silently dropped.
  • A non-store Node service whose declared ceiling is strictly below its container limit is prescribed the action class AC-0 named with its heap knob.
  • Negative control: a service at a ceiling equal to its container limit is not prescribed, and neither is a non-Node service. The config relationship fires, not service class.
  • No memory-saturation fact is consulted — asserted by a fixture that prescribes while reporting healthy memory, so a saturation-coupled implementation fails.
  • evidenceFacts is unchanged at every branch unless option A is chosen and its §2.4 impact is declared.
  • The prescription is proven to reach an effect, not merely to be recorded. A test asserting only that a diagnosis names an action would have passed against reconfigure throughout — that is exactly how this defect survived until review.
  • Mutation-proven by name, each mutation's application checked by occurrence count before the run.

Out of Scope

  • A raise-ceiling executor for services (B1-privilege work on a restart path). Note this is not the same as AC-0's recreation path, which this ticket does own.
  • The reactive controller for any action class.
  • Slice B of #16630 — dimension-matched V8 observation.
  • Chroma's ceiling — #16595 / #16596 / #16637.

Avoided Traps

Mirroring the store branch. The obvious move is to copy storeMemoryFactsraise-ceiling into a non-store arm. It fails silently: that branch routes off an authoritative saturation fact, and the non-store heap case produces none by construction — a copied shape would compile, pass a naive fixture, and never fire in production.

Treating the AC as a small implementation. #16636's routing AC reads like one branch. It is a placement decision with a documented prior failure (#16634).

Related

#16636 (parent — every other AC delivered) · #16630 · #16634 (the selectEvidenceFacts break) · #16596 / #16637 (store side) · #16463 · ADR-0026 §2.4

Live latest-open sweep: checked latest 20 open issues at 2026-08-08T10:57:45Z; no equivalent found. A2A in-flight claim sweep: 30 most recent messages, no overlapping [lane-claim].

Origin Session ID: 4141258c-36d3-4788-b0c2-ab3ebe0867be

Retrieval Hint: query_raw_memories("config relationship prescription has no home in the facts array, heap ceiling routing") · ContainerHealthDiagnosisService.mjs storeMemoryFacts branch