LearnNewsExamplesServices
Frontmatter
id16636
titleraise-ceiling is reachable only by stores, and sanctioned by no ADR
stateClosed
labels
enhancementaiarchitectureagent-os
assigneesneo-opus-vega
createdAtAug 7, 2026, 4:11 PM
updatedAtAug 9, 2026, 1:09 AM
githubUrlhttps://github.com/neomjs/neo/issues/16636
authorneo-opus-vega
commentsCount5
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[ ] 16596 A store at its memory ceiling is told to shed load it cannot shed
blocking[]
closedAtAug 8, 2026, 4:21 PM

raise-ceiling is reachable only by stores, and sanctioned by no ADR

Closed Backlog/active-chunk-13 enhancementaiarchitectureagent-os
neo-opus-vega
neo-opus-vega commented on Aug 7, 2026, 4:11 PM

Rewritten 2026-08-08T09:47Z — current facts only. Prior body was 25,033 bytes and carried three correction sections, a resolved fork analysis, and scoping narrative for work now shipped. Provenance is in git history, the PR, and the comment trail.

Reconcile the container-health action vocabulary with what the actuator can do, and route the service-side ceiling case. Delivered-half sibling of #16630.

Ground state on dev

ADR-0026 §2.4 action matrix after #16596:

target kind admitted actions
supervised task restart, warm-provider
compose service (transient) + reconfigure
compose service (store) + raise-ceiling
deploy target redeploy

raise-ceiling is store-only (AC-12); its executor shipped in #16637.

The three things this closes

1. throttle-shed is a diagnosis class the lifecycle actuator cannot perform. ContainerHealthDiagnosisService:739 prescribes it for container resource exhaustion, but the only implementation of that name is collection-keyed in ADR-0027's data-recovery world. A lifecycle throttle-shed is a forward declaration consumed by nothing. Disclosed in ADR-0026 §2.4 :93.

2. The service-side ceiling case needs a knob, not a new action. FALSIFIED 2026-08-08 — the channel cannot carry the payload. I claimed reconfigure's restart coupling was "required here rather than a defect" because a V8 ceiling is a process-start argument. That reasoning assumed a restart re-reads the composition. It does not: docker-compose.yml:117 interpolates ${NEO_KB_SERVER_HEAP_MB} into Config.Cmd at create time, and RecoveryActuatorService.mjs:586restartComposeServiceapplyLifecycle({operation: 'restart'}) re-runs that baked command. Prescribing reconfigure would have been a no-op reporting success. Delivering a heap ceiling needs container recreation, a privilege change. Found by @neo-gpt-emmy reviewing PR #16663; verified in source before adoption. The successor question lives in #16676 AC-0.

Resource distinction, measured: raise-ceiling mutates HostConfig.Memory (widens the cgroup envelope); a heap raise changes --max-old-space-size and leaves it unchanged — spending already-authorised memory. The discriminator is headroom, not service class.

3. The undeclared-ceiling record needs a home that is not the facts array. selectEvidenceFacts(facts, …) is called with the whole array at :676, :691, :731, :741, :752, :780, so any new member becomes candidate evidence for every class — this broke 3 assertions when tried in #16634. It lives in deployment-state-bridge-diagnostics instead, observe-only by construction.

Knob shape

Two knobs, not one. serviceKey is singular and the actuator refuses a knob/target mismatch against it.

No min/max. 768 has no derivation in the repo (three compose occurrences, no working-set arithmetic). Both bounds are relationships instead, per the registry's rule that a bound against a leaf it does not change is expressed against the live value, never as a constant:

bound expressed against
raise-not-lower the declared ceiling being changed
strictly-below the live container limit (HostConfig.Memory)

The container limit is runtime context, so a config-only channel fails closed on these knobs. Measured: no compose deploy.resources value has an AiConfig leaf anywhere, and there is no deploy.* subtree in configBase.mjs. Cgroup limits are observed at runtime by design. Failing closed is correct — a controller blind to the container limit must not raise a ceiling past it.

Leaf unit is MB, the unit --max-old-space-size takes, so the one MB→bytes conversion happens once where both units meet.

Contract Ledger Matrix

Target Surface Authority Behaviour Fallback Evidence
deployment-service-stateinspect.declaredHeapCeilingMb this ticket publish the --max-old-space-size parsed from Config.Cmd null when none; 'unknown' when declarations diverge summarizeInspect() :1001 dropped Config.Cmd
inspect.nodeCommand this ticket whether the command launches Node false read from the command, never the image name
stats.memoryLimitBytes existing unchanged summarizeStats() :1027 already publishes it
deployment-state-bridge-diagnosticsundeclaredHeapCeilingServices this ticket names Node services with no declared ceiling; record-terminal stable empty array when all declare — the key is always present recordType ~:476, observe-only :79, emitted unconditionally at :525
divergence between declared and actual this ticket out of scope no V8-scoped metric exists for a sibling container (#16630)

Decision Record impact: none — UNRESOLVED, and that is the correction. The none rested entirely on reconfigure being an already-admitted class, which is moot now that it cannot deliver the effect (see finding 2). A recreation-class path is a privilege change requiring §2.4 accounting; whether one is acceptable is #16676 AC-0, which may conclude that none is.

Acceptance criteria

  • Every member of CONTAINER_HEALTH_ACTION_CLASSES is accounted for in ADR-0026 §2.4 — asserted over a curated enum→matrix mapping, not a grep (the ADR spells throttle/shed separately while the enum carries hyphenated throttle-shed).
  • Both server container limits parameterised in compose (NEO_KB_SERVER_MEMORY_LIMIT / NEO_MC_SERVER_MEMORY_LIMIT, default 1g) — the prerequisite that makes the strictly-below bound expressible.
  • The two service-heap knobs MOVED OUT to #16695, and that is their disposition here. They were written and are not deliverable: no writer or effect path exists (heapCeilingMb occurs only in the registry and the bridge), and the channel they were designed for cannot apply them — a restart re-runs the baked Config.Cmd. Declaring actuator authority for an operation nothing can perform is the exact defect finding 1 of this ticket names in throttle-shed, so shipping them here would have closed the ticket by committing its own thesis. #16695 owns the descriptors, their tests and the recreation-class question; a negative answer there closes them as withdrawn.
  • A Node service with no declared ceiling produces an undeclaredHeapCeilingServices record; record-terminal, no action, no privilege, no restart.
  • That record is absent from evidenceFacts at every branch — true by construction, since it is not a fact type; the spec guards the construction.
  • Negative control: a service declaring a ceiling, and a non-Node service, produce no finding. Plus a positive control, because four rows asserting [] are jointly vacuous if the filter is broken.
  • Mutation-proven by name, each mutation's application checked by occurrence count before the run.
  • The heap knobs cannot sanction a cgroup envelope move. Ceiling leaves declare the resource they govern (container-memory vs v8-heap) and the envelope guard requires the former; a bandless envelope leaf refuses instead of comparing against undefined. Mutation-proven.

Routing moved to #16676

Adding these knobs widened a privilege boundary before it narrowed one, and that is why the extra AC above exists. The envelope guard admitted any service declared by a role: 'ceiling' leaf, so both servers became legal targets for update-memory-limit — the cgroup move #16636 puts out of scope. The band gate went vacuous with them too: these knobs carry no min/max by design, and value < undefined || value > undefined is NaN-false both ways, so the cap that terminates the autonomous ratchet disappeared rather than tightening.

Routing itself is a placement decision, not a branch. The decision point consumes facts, but the condition is a config relationship this ticket deliberately keeps out of that array — selectEvidenceFacts(facts, …) takes the whole array at six sites, which is what broke 3 assertions in #16634. The fork, its three candidates and their falsifiers live in #16676.

Out of scope

  • A raise-ceiling executor for services (B1-privilege work on a restart path).
  • The reactive controller for any action class — no controller consumes container-health decisions yet, so "routes to X" means prescribed in the recorded diagnosis, never actuated.
  • Slice B of #16630 — dimension-matched V8 observation.
  • Chroma's ceiling — #16595 / #16596 / #16637.

Related

#16630 (parent) · #16596 / #16637 (store side, the §2.4 amendment this rebases onto) · #16463 · #16374 · ADR-0025 / 0026 / 0027

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

Retrieval Hint: query_raw_memories("service heap ceiling knob reconfigure not raise-ceiling headroom condition") · recoveryKnobRegistry.mjs · ADR-0026 §2.4 :93

tobiu referenced in commit 82e2629 - "feat: A store's ceiling is raisable — bounded knob, live update, no restart (#16637) (#16638) on Aug 7, 2026, 7:21 PM
tobiu referenced in commit 5f11053 - "The declared heap ceiling becomes observable, and the envelope guard names its resource (#16636) (#16697) on Aug 8, 2026, 4:21 PM
tobiu closed this issue on Aug 8, 2026, 4:21 PM