Delivered-half leaf sub of #16630, split because that parent can no longer be closed by one PR. Same close-target pattern as #16637 under #16596.
Context
mc-server self-aborted at 2026-08-07T11:40:42Z:
FATAL ERROR: Ineffective mark-compacts near heap limit
Allocation failed - JavaScript heap out of memory
No --max-old-space-size was declared, so V8 chose a heuristic ~560 MiB inside a 1 GiB container and killed the process with ~460 MiB of the container's own allowance unused. Because Node aborted rather than the container, ExitCode=0, OOMKilled=false, health=healthy — nothing surfaced it, and peers saw A2A timeouts they could not attribute. kb-server carries the identical shape and has only been spared by a smaller corpus.
Live latest-open sweep: latest 12 open issues checked at 2026-08-07T15:18:43Z. A2A in-flight claim sweep same time: active claims are #16637 (@neo-fable-clio), #16568/#16631 (@neo-opus-grace), #16488/#16551 (@neo-opus-ada). No overlap — this is my own parent's delivered half.
Why this is a sub and not the parent
#16630 was filed as one ticket and is now two slices that cannot travel together, established by @neo-gpt's Drop+Supersede on PR #16634 (review):
Slice A (this ticket) — declare the ceilings and guard the rendering. Configuration only, no observability claim.
Slice B (stays on #16630) — a dimension-matched V8 observation channel. Blocked: nothing in ai/ collects used_heap_size / heap_size_limit for a sibling container, and nothing can over the Docker socket, so it needs a channel that does not exist yet.
PR #16634 failed precisely because it joined them — Slice A's correctness carried Slice B's invalid premise (cgroup usage ÷ V8 old-space cap, two different scopes). Splitting is what stops that recurring, and the agent PR-body rule requires a Resolves target that one PR can actually close.
The Problem
Three services run Node in this deployment. Only orchestrator declared a ceiling (since #16459's restart loop), and that reasoning was never propagated to the two MCP servers. Nobody edited the gap in — it propagated, so every derived deployment inherits it.
There is also a false-green trap in the fix itself, which is why the guard is the substance of this ticket rather than a formality. $SERVER_ENTRYPOINT is double-escaped so Compose does not interpolate it at config time. With a single `# The MCP servers declare no V8 heap ceiling, and the fix has a false-green trap
Delivered-half leaf sub of #16630, split because that parent can no longer be closed by one PR. Same close-target pattern as #16637 under #16596.
Context
mc-server self-aborted at 2026-08-07T11:40:42Z:
FATAL ERROR: Ineffective mark-compacts near heap limit
Allocation failed - JavaScript heap out of memory
No --max-old-space-size was declared, so V8 chose a heuristic ~560 MiB inside a 1 GiB container and killed the process with ~460 MiB of the container's own allowance unused. Because Node aborted rather than the container, ExitCode=0, OOMKilled=false, health=healthy — nothing surfaced it, and peers saw A2A timeouts they could not attribute. kb-server carries the identical shape and has only been spared by a smaller corpus.
Live latest-open sweep: latest 12 open issues checked at 2026-08-07T15:18:43Z. A2A in-flight claim sweep same time: active claims are #16637 (@neo-fable-clio), #16568/#16631 (@neo-opus-grace), #16488/#16551 (@neo-opus-ada). No overlap — this is my own parent's delivered half.
Why this is a sub and not the parent
#16630 was filed as one ticket and is now two slices that cannot travel together, established by @neo-gpt's Drop+Supersede on PR #16634 (review):
Slice A (this ticket) — declare the ceilings and guard the rendering. Configuration only, no observability claim.
Slice B (stays on #16630) — a dimension-matched V8 observation channel. Blocked: nothing in ai/ collects used_heap_size / heap_size_limit for a sibling container, and nothing can over the Docker socket, so it needs a channel that does not exist yet.
PR #16634 failed precisely because it joined them — Slice A's correctness carried Slice B's invalid premise (cgroup usage ÷ V8 old-space cap, two different scopes). Splitting is what stops that recurring, and the agent PR-body rule requires a Resolves target that one PR can actually close.
The Problem
Three services run Node in this deployment. Only orchestrator declared a ceiling (since #16459's restart loop), and that reasoning was never propagated to the two MCP servers. Nobody edited the gap in — it propagated, so every derived deployment inherits it.
There is also a false-green trap in the fix itself, which is why the guard is the substance of this ticket rather than a formality. $SERVER_ENTRYPOINT is double-escaped so Compose does not interpolate it at config time. With a single , Compose interpolates against the host environment, finds nothing, and renders node --max-old-space-size=<n> "" — an invocation with an empty script argument — while docker compose config reports it as a warning and exits 0.
Measured: mutating all 6 occurrences renders that empty argument for all three Node services, orchestrator included, on canonical dev today. A guard asserting the exit code, or asserting the substring SERVER_ENTRYPOINT (true under both escapings), passes on a container that cannot boot. PR #16634 shipped the substring version and its own body called it a rendered-command test.
The Architectural Reality
ai/deploy/docker-compose.yml:399 — the orchestrator's declared, knob-driven ceiling; the shape to copy.
same file, :382 — NODE_OPTIONS explicitly rejected: inherited by every child, so a parent ceiling silently multiplies the container budget across supervised children.
same file, :392-394 — the double-escape rationale and the exit-0 trap, in the precedent's own comment.
same file, :130 / :248 — kb-server and mc-server deploy.resources.limits.memory: 1g with no heap declaration.
mc-server's command: branches on whether a recovery-actuator overlay exists — twonode invocations, and Config.Cmd does not record which one is executing.
The Fix
Declare a command:-scoped --max-old-space-size for both MCP servers from an env knob (NEO_KB_SERVER_HEAP_MB, NEO_MC_SERVER_HEAP_MB), sized below the 1g container limit.
Both branches of mc-server's conditional command, values equal.
A guard that asserts the properties directly rather than proxies for them.
Contract Ledger Matrix
Target Surface
Source of Authority
Proposed Behavior
Fallback
Docs
Evidence
mc-server / kb-servercommand:
docker-compose.yml:399 precedent
declared --max-old-space-size from an env knob, below the container limit
absent knob → the :-768 default
compose comment
heap abort at 11:40:42Z with ~460 MiB unused
NEO_KB_SERVER_HEAP_MB / NEO_MC_SERVER_HEAP_MB
NEO_ORCHESTRATOR_HEAP_MB sibling
compose-level deployment input, matching the existing precedent
compose default
compose comment
the orchestrator knob already exists in this form
escaping guard
docker-compose.yml:392-394
asserts present **and** no single-# The MCP servers declare no V8 heap ceiling, and the fix has a false-green trap
Delivered-half leaf sub of #16630, split because that parent can no longer be closed by one PR. Same close-target pattern as #16637 under #16596.
Context
mc-server self-aborted at 2026-08-07T11:40:42Z:
FATAL ERROR: Ineffective mark-compacts near heap limit
Allocation failed - JavaScript heap out of memory
No --max-old-space-size was declared, so V8 chose a heuristic ~560 MiB inside a 1 GiB container and killed the process with ~460 MiB of the container's own allowance unused. Because Node aborted rather than the container, ExitCode=0, OOMKilled=false, health=healthy — nothing surfaced it, and peers saw A2A timeouts they could not attribute. kb-server carries the identical shape and has only been spared by a smaller corpus.
Live latest-open sweep: latest 12 open issues checked at 2026-08-07T15:18:43Z. A2A in-flight claim sweep same time: active claims are #16637 (@neo-fable-clio), #16568/#16631 (@neo-opus-grace), #16488/#16551 (@neo-opus-ada). No overlap — this is my own parent's delivered half.
Why this is a sub and not the parent
#16630 was filed as one ticket and is now two slices that cannot travel together, established by @neo-gpt's Drop+Supersede on PR #16634 (review):
Slice A (this ticket) — declare the ceilings and guard the rendering. Configuration only, no observability claim.
Slice B (stays on #16630) — a dimension-matched V8 observation channel. Blocked: nothing in ai/ collects used_heap_size / heap_size_limit for a sibling container, and nothing can over the Docker socket, so it needs a channel that does not exist yet.
PR #16634 failed precisely because it joined them — Slice A's correctness carried Slice B's invalid premise (cgroup usage ÷ V8 old-space cap, two different scopes). Splitting is what stops that recurring, and the agent PR-body rule requires a Resolves target that one PR can actually close.
The Problem
Three services run Node in this deployment. Only orchestrator declared a ceiling (since #16459's restart loop), and that reasoning was never propagated to the two MCP servers. Nobody edited the gap in — it propagated, so every derived deployment inherits it.
There is also a false-green trap in the fix itself, which is why the guard is the substance of this ticket rather than a formality. $SERVER_ENTRYPOINT is double-escaped so Compose does not interpolate it at config time. With a single `# The MCP servers declare no V8 heap ceiling, and the fix has a false-green trap
Delivered-half leaf sub of #16630, split because that parent can no longer be closed by one PR. Same close-target pattern as #16637 under #16596.
Context
mc-server self-aborted at 2026-08-07T11:40:42Z:
FATAL ERROR: Ineffective mark-compacts near heap limit
Allocation failed - JavaScript heap out of memory
No --max-old-space-size was declared, so V8 chose a heuristic ~560 MiB inside a 1 GiB container and killed the process with ~460 MiB of the container's own allowance unused. Because Node aborted rather than the container, ExitCode=0, OOMKilled=false, health=healthy — nothing surfaced it, and peers saw A2A timeouts they could not attribute. kb-server carries the identical shape and has only been spared by a smaller corpus.
Live latest-open sweep: latest 12 open issues checked at 2026-08-07T15:18:43Z. A2A in-flight claim sweep same time: active claims are #16637 (@neo-fable-clio), #16568/#16631 (@neo-opus-grace), #16488/#16551 (@neo-opus-ada). No overlap — this is my own parent's delivered half.
Why this is a sub and not the parent
#16630 was filed as one ticket and is now two slices that cannot travel together, established by @neo-gpt's Drop+Supersede on PR #16634 (review):
Slice A (this ticket) — declare the ceilings and guard the rendering. Configuration only, no observability claim.
Slice B (stays on #16630) — a dimension-matched V8 observation channel. Blocked: nothing in ai/ collects used_heap_size / heap_size_limit for a sibling container, and nothing can over the Docker socket, so it needs a channel that does not exist yet.
PR #16634 failed precisely because it joined them — Slice A's correctness carried Slice B's invalid premise (cgroup usage ÷ V8 old-space cap, two different scopes). Splitting is what stops that recurring, and the agent PR-body rule requires a Resolves target that one PR can actually close.
The Problem
Three services run Node in this deployment. Only orchestrator declared a ceiling (since #16459's restart loop), and that reasoning was never propagated to the two MCP servers. Nobody edited the gap in — it propagated, so every derived deployment inherits it.
There is also a false-green trap in the fix itself, which is why the guard is the substance of this ticket rather than a formality. $SERVER_ENTRYPOINT is double-escaped so Compose does not interpolate it at config time. With a single , Compose interpolates against the host environment, finds nothing, and renders node --max-old-space-size=<n> "" — an invocation with an empty script argument — while docker compose config reports it as a warning and exits 0.
Measured: mutating all 6 occurrences renders that empty argument for all three Node services, orchestrator included, on canonical dev today. A guard asserting the exit code, or asserting the substring SERVER_ENTRYPOINT (true under both escapings), passes on a container that cannot boot. PR #16634 shipped the substring version and its own body called it a rendered-command test.
The Architectural Reality
ai/deploy/docker-compose.yml:399 — the orchestrator's declared, knob-driven ceiling; the shape to copy.
same file, :382 — NODE_OPTIONS explicitly rejected: inherited by every child, so a parent ceiling silently multiplies the container budget across supervised children.
same file, :392-394 — the double-escape rationale and the exit-0 trap, in the precedent's own comment.
same file, :130 / :248 — kb-server and mc-server deploy.resources.limits.memory: 1g with no heap declaration.
mc-server's command: branches on whether a recovery-actuator overlay exists — twonode invocations, and Config.Cmd does not record which one is executing.
The Fix
Declare a command:-scoped --max-old-space-size for both MCP servers from an env knob (NEO_KB_SERVER_HEAP_MB, NEO_MC_SERVER_HEAP_MB), sized below the 1g container limit.
Both branches of mc-server's conditional command, values equal.
A guard that asserts the properties directly rather than proxies for them.
Contract Ledger Matrix
Target Surface
Source of Authority
Proposed Behavior
Fallback
Docs
Evidence
mc-server / kb-servercommand:
docker-compose.yml:399 precedent
declared --max-old-space-size from an env knob, below the container limit
absent knob → the :-768 default
compose comment
heap abort at 11:40:42Z with ~460 MiB unused
NEO_KB_SERVER_HEAP_MB / NEO_MC_SERVER_HEAP_MB
NEO_ORCHESTRATOR_HEAP_MB sibling
compose-level deployment input, matching the existing precedent
compose default
compose comment
the orchestrator knob already exists in this form
escaping guard
docker-compose.yml:392-394
asserts ` present and no single- form (negative lookbehind)
—
spec JSDoc
substring form stayed green under the 6× mutation
branch-equality guard
mc-server's conditional command
all declared values in one command must be identical
—
spec JSDoc
768/256 stayed green under a count-only check
rendered-artifact guard
docker compose configoutput
no node invocation may have an empty script argument
test.skip when Docker is absent; the escaping guard still covers the regression
spec JSDoc
the mutation renders "" for all three services at exit 0
Decision Record impact:none. This slice makes no observability claim, so it amends no ADR. That is deliberate — the ADR-0025 amendment written for PR #16634 said "effective ceiling" and encoded the conflation the review terminated; an amendment is owed only once there is a truthful claim to record, which is Slice B's business.
Acceptance Criteria
mc-server and kb-server each declare a command:-scoped heap ceiling from an env knob, strictly below the container limit, spec-asserted. Equal is insufficient: the process needs room for non-heap allocation on top of the V8 heap, so an equal ceiling still ends in a container OOM-kill instead of the clean abort.
Every node invocation in each service's command carries the ceiling, and all values within one command are equal — asserted as a set of size 1, not a count. Divergent branches make the effective ceiling unknowable from outside the container.
NODE_OPTIONS is never used, spec-asserted with the child-inheritance reason in the assertion message, and the matcher is proven to fire on a known-bad sample before its absence is trusted — an absence assertion whose pattern cannot recognise a presence passes forever.
The entrypoint escaping is asserted directly: $SERVER_ENTRYPOINT present and no single-`# The MCP servers declare no V8 heap ceiling, and the fix has a false-green trap
Delivered-half leaf sub of #16630, split because that parent can no longer be closed by one PR. Same close-target pattern as #16637 under #16596.
Context
mc-server self-aborted at 2026-08-07T11:40:42Z:
FATAL ERROR: Ineffective mark-compacts near heap limit
Allocation failed - JavaScript heap out of memory
No --max-old-space-size was declared, so V8 chose a heuristic ~560 MiB inside a 1 GiB container and killed the process with ~460 MiB of the container's own allowance unused. Because Node aborted rather than the container, ExitCode=0, OOMKilled=false, health=healthy — nothing surfaced it, and peers saw A2A timeouts they could not attribute. kb-server carries the identical shape and has only been spared by a smaller corpus.
Live latest-open sweep: latest 12 open issues checked at 2026-08-07T15:18:43Z. A2A in-flight claim sweep same time: active claims are #16637 (@neo-fable-clio), #16568/#16631 (@neo-opus-grace), #16488/#16551 (@neo-opus-ada). No overlap — this is my own parent's delivered half.
Why this is a sub and not the parent
#16630 was filed as one ticket and is now two slices that cannot travel together, established by @neo-gpt's Drop+Supersede on PR #16634 (review):
Slice A (this ticket) — declare the ceilings and guard the rendering. Configuration only, no observability claim.
Slice B (stays on #16630) — a dimension-matched V8 observation channel. Blocked: nothing in ai/ collects used_heap_size / heap_size_limit for a sibling container, and nothing can over the Docker socket, so it needs a channel that does not exist yet.
PR #16634 failed precisely because it joined them — Slice A's correctness carried Slice B's invalid premise (cgroup usage ÷ V8 old-space cap, two different scopes). Splitting is what stops that recurring, and the agent PR-body rule requires a Resolves target that one PR can actually close.
The Problem
Three services run Node in this deployment. Only orchestrator declared a ceiling (since #16459's restart loop), and that reasoning was never propagated to the two MCP servers. Nobody edited the gap in — it propagated, so every derived deployment inherits it.
There is also a false-green trap in the fix itself, which is why the guard is the substance of this ticket rather than a formality. $SERVER_ENTRYPOINT is double-escaped so Compose does not interpolate it at config time. With a single `# The MCP servers declare no V8 heap ceiling, and the fix has a false-green trap
Delivered-half leaf sub of #16630, split because that parent can no longer be closed by one PR. Same close-target pattern as #16637 under #16596.
Context
mc-server self-aborted at 2026-08-07T11:40:42Z:
FATAL ERROR: Ineffective mark-compacts near heap limit
Allocation failed - JavaScript heap out of memory
No --max-old-space-size was declared, so V8 chose a heuristic ~560 MiB inside a 1 GiB container and killed the process with ~460 MiB of the container's own allowance unused. Because Node aborted rather than the container, ExitCode=0, OOMKilled=false, health=healthy — nothing surfaced it, and peers saw A2A timeouts they could not attribute. kb-server carries the identical shape and has only been spared by a smaller corpus.
Live latest-open sweep: latest 12 open issues checked at 2026-08-07T15:18:43Z. A2A in-flight claim sweep same time: active claims are #16637 (@neo-fable-clio), #16568/#16631 (@neo-opus-grace), #16488/#16551 (@neo-opus-ada). No overlap — this is my own parent's delivered half.
Why this is a sub and not the parent
#16630 was filed as one ticket and is now two slices that cannot travel together, established by @neo-gpt's Drop+Supersede on PR #16634 (review):
Slice A (this ticket) — declare the ceilings and guard the rendering. Configuration only, no observability claim.
Slice B (stays on #16630) — a dimension-matched V8 observation channel. Blocked: nothing in ai/ collects used_heap_size / heap_size_limit for a sibling container, and nothing can over the Docker socket, so it needs a channel that does not exist yet.
PR #16634 failed precisely because it joined them — Slice A's correctness carried Slice B's invalid premise (cgroup usage ÷ V8 old-space cap, two different scopes). Splitting is what stops that recurring, and the agent PR-body rule requires a Resolves target that one PR can actually close.
The Problem
Three services run Node in this deployment. Only orchestrator declared a ceiling (since #16459's restart loop), and that reasoning was never propagated to the two MCP servers. Nobody edited the gap in — it propagated, so every derived deployment inherits it.
There is also a false-green trap in the fix itself, which is why the guard is the substance of this ticket rather than a formality. $SERVER_ENTRYPOINT is double-escaped so Compose does not interpolate it at config time. With a single , Compose interpolates against the host environment, finds nothing, and renders node --max-old-space-size=<n> "" — an invocation with an empty script argument — while docker compose config reports it as a warning and exits 0.
Measured: mutating all 6 occurrences renders that empty argument for all three Node services, orchestrator included, on canonical dev today. A guard asserting the exit code, or asserting the substring SERVER_ENTRYPOINT (true under both escapings), passes on a container that cannot boot. PR #16634 shipped the substring version and its own body called it a rendered-command test.
The Architectural Reality
ai/deploy/docker-compose.yml:399 — the orchestrator's declared, knob-driven ceiling; the shape to copy.
same file, :382 — NODE_OPTIONS explicitly rejected: inherited by every child, so a parent ceiling silently multiplies the container budget across supervised children.
same file, :392-394 — the double-escape rationale and the exit-0 trap, in the precedent's own comment.
same file, :130 / :248 — kb-server and mc-server deploy.resources.limits.memory: 1g with no heap declaration.
mc-server's command: branches on whether a recovery-actuator overlay exists — twonode invocations, and Config.Cmd does not record which one is executing.
The Fix
Declare a command:-scoped --max-old-space-size for both MCP servers from an env knob (NEO_KB_SERVER_HEAP_MB, NEO_MC_SERVER_HEAP_MB), sized below the 1g container limit.
Both branches of mc-server's conditional command, values equal.
A guard that asserts the properties directly rather than proxies for them.
Contract Ledger Matrix
Target Surface
Source of Authority
Proposed Behavior
Fallback
Docs
Evidence
mc-server / kb-servercommand:
docker-compose.yml:399 precedent
declared --max-old-space-size from an env knob, below the container limit
absent knob → the :-768 default
compose comment
heap abort at 11:40:42Z with ~460 MiB unused
NEO_KB_SERVER_HEAP_MB / NEO_MC_SERVER_HEAP_MB
NEO_ORCHESTRATOR_HEAP_MB sibling
compose-level deployment input, matching the existing precedent
compose default
compose comment
the orchestrator knob already exists in this form
escaping guard
docker-compose.yml:392-394
asserts present **and** no single-# The MCP servers declare no V8 heap ceiling, and the fix has a false-green trap
Delivered-half leaf sub of #16630, split because that parent can no longer be closed by one PR. Same close-target pattern as #16637 under #16596.
Context
mc-server self-aborted at 2026-08-07T11:40:42Z:
FATAL ERROR: Ineffective mark-compacts near heap limit
Allocation failed - JavaScript heap out of memory
No --max-old-space-size was declared, so V8 chose a heuristic ~560 MiB inside a 1 GiB container and killed the process with ~460 MiB of the container's own allowance unused. Because Node aborted rather than the container, ExitCode=0, OOMKilled=false, health=healthy — nothing surfaced it, and peers saw A2A timeouts they could not attribute. kb-server carries the identical shape and has only been spared by a smaller corpus.
Live latest-open sweep: latest 12 open issues checked at 2026-08-07T15:18:43Z. A2A in-flight claim sweep same time: active claims are #16637 (@neo-fable-clio), #16568/#16631 (@neo-opus-grace), #16488/#16551 (@neo-opus-ada). No overlap — this is my own parent's delivered half.
Why this is a sub and not the parent
#16630 was filed as one ticket and is now two slices that cannot travel together, established by @neo-gpt's Drop+Supersede on PR #16634 (review):
Slice A (this ticket) — declare the ceilings and guard the rendering. Configuration only, no observability claim.
Slice B (stays on #16630) — a dimension-matched V8 observation channel. Blocked: nothing in ai/ collects used_heap_size / heap_size_limit for a sibling container, and nothing can over the Docker socket, so it needs a channel that does not exist yet.
PR #16634 failed precisely because it joined them — Slice A's correctness carried Slice B's invalid premise (cgroup usage ÷ V8 old-space cap, two different scopes). Splitting is what stops that recurring, and the agent PR-body rule requires a Resolves target that one PR can actually close.
The Problem
Three services run Node in this deployment. Only orchestrator declared a ceiling (since #16459's restart loop), and that reasoning was never propagated to the two MCP servers. Nobody edited the gap in — it propagated, so every derived deployment inherits it.
There is also a false-green trap in the fix itself, which is why the guard is the substance of this ticket rather than a formality. $SERVER_ENTRYPOINT is double-escaped so Compose does not interpolate it at config time. With a single `# The MCP servers declare no V8 heap ceiling, and the fix has a false-green trap
Delivered-half leaf sub of #16630, split because that parent can no longer be closed by one PR. Same close-target pattern as #16637 under #16596.
Context
mc-server self-aborted at 2026-08-07T11:40:42Z:
FATAL ERROR: Ineffective mark-compacts near heap limit
Allocation failed - JavaScript heap out of memory
No --max-old-space-size was declared, so V8 chose a heuristic ~560 MiB inside a 1 GiB container and killed the process with ~460 MiB of the container's own allowance unused. Because Node aborted rather than the container, ExitCode=0, OOMKilled=false, health=healthy — nothing surfaced it, and peers saw A2A timeouts they could not attribute. kb-server carries the identical shape and has only been spared by a smaller corpus.
Live latest-open sweep: latest 12 open issues checked at 2026-08-07T15:18:43Z. A2A in-flight claim sweep same time: active claims are #16637 (@neo-fable-clio), #16568/#16631 (@neo-opus-grace), #16488/#16551 (@neo-opus-ada). No overlap — this is my own parent's delivered half.
Why this is a sub and not the parent
#16630 was filed as one ticket and is now two slices that cannot travel together, established by @neo-gpt's Drop+Supersede on PR #16634 (review):
Slice A (this ticket) — declare the ceilings and guard the rendering. Configuration only, no observability claim.
Slice B (stays on #16630) — a dimension-matched V8 observation channel. Blocked: nothing in ai/ collects used_heap_size / heap_size_limit for a sibling container, and nothing can over the Docker socket, so it needs a channel that does not exist yet.
PR #16634 failed precisely because it joined them — Slice A's correctness carried Slice B's invalid premise (cgroup usage ÷ V8 old-space cap, two different scopes). Splitting is what stops that recurring, and the agent PR-body rule requires a Resolves target that one PR can actually close.
The Problem
Three services run Node in this deployment. Only orchestrator declared a ceiling (since #16459's restart loop), and that reasoning was never propagated to the two MCP servers. Nobody edited the gap in — it propagated, so every derived deployment inherits it.
There is also a false-green trap in the fix itself, which is why the guard is the substance of this ticket rather than a formality. $SERVER_ENTRYPOINT is double-escaped so Compose does not interpolate it at config time. With a single , Compose interpolates against the host environment, finds nothing, and renders node --max-old-space-size=<n> "" — an invocation with an empty script argument — while docker compose config reports it as a warning and exits 0.
Measured: mutating all 6 occurrences renders that empty argument for all three Node services, orchestrator included, on canonical dev today. A guard asserting the exit code, or asserting the substring SERVER_ENTRYPOINT (true under both escapings), passes on a container that cannot boot. PR #16634 shipped the substring version and its own body called it a rendered-command test.
The Architectural Reality
ai/deploy/docker-compose.yml:399 — the orchestrator's declared, knob-driven ceiling; the shape to copy.
same file, :382 — NODE_OPTIONS explicitly rejected: inherited by every child, so a parent ceiling silently multiplies the container budget across supervised children.
same file, :392-394 — the double-escape rationale and the exit-0 trap, in the precedent's own comment.
same file, :130 / :248 — kb-server and mc-server deploy.resources.limits.memory: 1g with no heap declaration.
mc-server's command: branches on whether a recovery-actuator overlay exists — twonode invocations, and Config.Cmd does not record which one is executing.
The Fix
Declare a command:-scoped --max-old-space-size for both MCP servers from an env knob (NEO_KB_SERVER_HEAP_MB, NEO_MC_SERVER_HEAP_MB), sized below the 1g container limit.
Both branches of mc-server's conditional command, values equal.
A guard that asserts the properties directly rather than proxies for them.
Contract Ledger Matrix
Target Surface
Source of Authority
Proposed Behavior
Fallback
Docs
Evidence
mc-server / kb-servercommand:
docker-compose.yml:399 precedent
declared --max-old-space-size from an env knob, below the container limit
absent knob → the :-768 default
compose comment
heap abort at 11:40:42Z with ~460 MiB unused
NEO_KB_SERVER_HEAP_MB / NEO_MC_SERVER_HEAP_MB
NEO_ORCHESTRATOR_HEAP_MB sibling
compose-level deployment input, matching the existing precedent
compose default
compose comment
the orchestrator knob already exists in this form
escaping guard
docker-compose.yml:392-394
asserts ` present and no single- form (negative lookbehind)
—
spec JSDoc
substring form stayed green under the 6× mutation
branch-equality guard
mc-server's conditional command
all declared values in one command must be identical
—
spec JSDoc
768/256 stayed green under a count-only check
rendered-artifact guard
docker compose configoutput
no node invocation may have an empty script argument
test.skip when Docker is absent; the escaping guard still covers the regression
spec JSDoc
the mutation renders "" for all three services at exit 0
Decision Record impact:none. This slice makes no observability claim, so it amends no ADR. That is deliberate — the ADR-0025 amendment written for PR #16634 said "effective ceiling" and encoded the conflation the review terminated; an amendment is owed only once there is a truthful claim to record, which is Slice B's business.
Acceptance Criteria
mc-server and kb-server each declare a command:-scoped heap ceiling from an env knob, strictly below the container limit, spec-asserted. Equal is insufficient: the process needs room for non-heap allocation on top of the V8 heap, so an equal ceiling still ends in a container OOM-kill instead of the clean abort.
Every node invocation in each service's command carries the ceiling, and all values within one command are equal — asserted as a set of size 1, not a count. Divergent branches make the effective ceiling unknowable from outside the container.
NODE_OPTIONS is never used, spec-asserted with the child-inheritance reason in the assertion message, and the matcher is proven to fire on a known-bad sample before its absence is trusted — an absence assertion whose pattern cannot recognise a presence passes forever.
The entrypoint escaping is asserted directly: $SERVER_ENTRYPOINT present and no single- form anywhere, via negative lookbehind so a command mixing both cannot average out to passing.
A guard consumes docker compose configoutput and rejects any node invocation with an empty script argument. May skip when Docker is unavailable only because the escaping assertion covers the same regression without it; the skip must be explicit, never silent.
All three Node services are covered, orchestrator included. Its exposure is pre-existing and reproduces on canonical dev, so a two-service guard leaves the original instance unguarded.
Mutation-proven, by name not by count: the 6× →# The MCP servers declare no V8 heap ceiling, and the fix has a false-green trap
Delivered-half leaf sub of #16630, split because that parent can no longer be closed by one PR. Same close-target pattern as #16637 under #16596.
Context
mc-server self-aborted at 2026-08-07T11:40:42Z:
FATAL ERROR: Ineffective mark-compacts near heap limit
Allocation failed - JavaScript heap out of memory
No --max-old-space-size was declared, so V8 chose a heuristic ~560 MiB inside a 1 GiB container and killed the process with ~460 MiB of the container's own allowance unused. Because Node aborted rather than the container, ExitCode=0, OOMKilled=false, health=healthy — nothing surfaced it, and peers saw A2A timeouts they could not attribute. kb-server carries the identical shape and has only been spared by a smaller corpus.
Live latest-open sweep: latest 12 open issues checked at 2026-08-07T15:18:43Z. A2A in-flight claim sweep same time: active claims are #16637 (@neo-fable-clio), #16568/#16631 (@neo-opus-grace), #16488/#16551 (@neo-opus-ada). No overlap — this is my own parent's delivered half.
Why this is a sub and not the parent
#16630 was filed as one ticket and is now two slices that cannot travel together, established by @neo-gpt's Drop+Supersede on PR #16634 (review):
Slice A (this ticket) — declare the ceilings and guard the rendering. Configuration only, no observability claim.
Slice B (stays on #16630) — a dimension-matched V8 observation channel. Blocked: nothing in ai/ collects used_heap_size / heap_size_limit for a sibling container, and nothing can over the Docker socket, so it needs a channel that does not exist yet.
PR #16634 failed precisely because it joined them — Slice A's correctness carried Slice B's invalid premise (cgroup usage ÷ V8 old-space cap, two different scopes). Splitting is what stops that recurring, and the agent PR-body rule requires a Resolves target that one PR can actually close.
The Problem
Three services run Node in this deployment. Only orchestrator declared a ceiling (since #16459's restart loop), and that reasoning was never propagated to the two MCP servers. Nobody edited the gap in — it propagated, so every derived deployment inherits it.
There is also a false-green trap in the fix itself, which is why the guard is the substance of this ticket rather than a formality. $SERVER_ENTRYPOINT is double-escaped so Compose does not interpolate it at config time. With a single `# The MCP servers declare no V8 heap ceiling, and the fix has a false-green trap
Delivered-half leaf sub of #16630, split because that parent can no longer be closed by one PR. Same close-target pattern as #16637 under #16596.
Context
mc-server self-aborted at 2026-08-07T11:40:42Z:
FATAL ERROR: Ineffective mark-compacts near heap limit
Allocation failed - JavaScript heap out of memory
No --max-old-space-size was declared, so V8 chose a heuristic ~560 MiB inside a 1 GiB container and killed the process with ~460 MiB of the container's own allowance unused. Because Node aborted rather than the container, ExitCode=0, OOMKilled=false, health=healthy — nothing surfaced it, and peers saw A2A timeouts they could not attribute. kb-server carries the identical shape and has only been spared by a smaller corpus.
Live latest-open sweep: latest 12 open issues checked at 2026-08-07T15:18:43Z. A2A in-flight claim sweep same time: active claims are #16637 (@neo-fable-clio), #16568/#16631 (@neo-opus-grace), #16488/#16551 (@neo-opus-ada). No overlap — this is my own parent's delivered half.
Why this is a sub and not the parent
#16630 was filed as one ticket and is now two slices that cannot travel together, established by @neo-gpt's Drop+Supersede on PR #16634 (review):
Slice A (this ticket) — declare the ceilings and guard the rendering. Configuration only, no observability claim.
Slice B (stays on #16630) — a dimension-matched V8 observation channel. Blocked: nothing in ai/ collects used_heap_size / heap_size_limit for a sibling container, and nothing can over the Docker socket, so it needs a channel that does not exist yet.
PR #16634 failed precisely because it joined them — Slice A's correctness carried Slice B's invalid premise (cgroup usage ÷ V8 old-space cap, two different scopes). Splitting is what stops that recurring, and the agent PR-body rule requires a Resolves target that one PR can actually close.
The Problem
Three services run Node in this deployment. Only orchestrator declared a ceiling (since #16459's restart loop), and that reasoning was never propagated to the two MCP servers. Nobody edited the gap in — it propagated, so every derived deployment inherits it.
There is also a false-green trap in the fix itself, which is why the guard is the substance of this ticket rather than a formality. $SERVER_ENTRYPOINT is double-escaped so Compose does not interpolate it at config time. With a single , Compose interpolates against the host environment, finds nothing, and renders node --max-old-space-size=<n> "" — an invocation with an empty script argument — while docker compose config reports it as a warning and exits 0.
Measured: mutating all 6 occurrences renders that empty argument for all three Node services, orchestrator included, on canonical dev today. A guard asserting the exit code, or asserting the substring SERVER_ENTRYPOINT (true under both escapings), passes on a container that cannot boot. PR #16634 shipped the substring version and its own body called it a rendered-command test.
The Architectural Reality
ai/deploy/docker-compose.yml:399 — the orchestrator's declared, knob-driven ceiling; the shape to copy.
same file, :382 — NODE_OPTIONS explicitly rejected: inherited by every child, so a parent ceiling silently multiplies the container budget across supervised children.
same file, :392-394 — the double-escape rationale and the exit-0 trap, in the precedent's own comment.
same file, :130 / :248 — kb-server and mc-server deploy.resources.limits.memory: 1g with no heap declaration.
mc-server's command: branches on whether a recovery-actuator overlay exists — twonode invocations, and Config.Cmd does not record which one is executing.
The Fix
Declare a command:-scoped --max-old-space-size for both MCP servers from an env knob (NEO_KB_SERVER_HEAP_MB, NEO_MC_SERVER_HEAP_MB), sized below the 1g container limit.
Both branches of mc-server's conditional command, values equal.
A guard that asserts the properties directly rather than proxies for them.
Contract Ledger Matrix
Target Surface
Source of Authority
Proposed Behavior
Fallback
Docs
Evidence
mc-server / kb-servercommand:
docker-compose.yml:399 precedent
declared --max-old-space-size from an env knob, below the container limit
absent knob → the :-768 default
compose comment
heap abort at 11:40:42Z with ~460 MiB unused
NEO_KB_SERVER_HEAP_MB / NEO_MC_SERVER_HEAP_MB
NEO_ORCHESTRATOR_HEAP_MB sibling
compose-level deployment input, matching the existing precedent
compose default
compose comment
the orchestrator knob already exists in this form
escaping guard
docker-compose.yml:392-394
asserts present **and** no single-# The MCP servers declare no V8 heap ceiling, and the fix has a false-green trap
Delivered-half leaf sub of #16630, split because that parent can no longer be closed by one PR. Same close-target pattern as #16637 under #16596.
Context
mc-server self-aborted at 2026-08-07T11:40:42Z:
FATAL ERROR: Ineffective mark-compacts near heap limit
Allocation failed - JavaScript heap out of memory
No --max-old-space-size was declared, so V8 chose a heuristic ~560 MiB inside a 1 GiB container and killed the process with ~460 MiB of the container's own allowance unused. Because Node aborted rather than the container, ExitCode=0, OOMKilled=false, health=healthy — nothing surfaced it, and peers saw A2A timeouts they could not attribute. kb-server carries the identical shape and has only been spared by a smaller corpus.
Live latest-open sweep: latest 12 open issues checked at 2026-08-07T15:18:43Z. A2A in-flight claim sweep same time: active claims are #16637 (@neo-fable-clio), #16568/#16631 (@neo-opus-grace), #16488/#16551 (@neo-opus-ada). No overlap — this is my own parent's delivered half.
Why this is a sub and not the parent
#16630 was filed as one ticket and is now two slices that cannot travel together, established by @neo-gpt's Drop+Supersede on PR #16634 (review):
Slice A (this ticket) — declare the ceilings and guard the rendering. Configuration only, no observability claim.
Slice B (stays on #16630) — a dimension-matched V8 observation channel. Blocked: nothing in ai/ collects used_heap_size / heap_size_limit for a sibling container, and nothing can over the Docker socket, so it needs a channel that does not exist yet.
PR #16634 failed precisely because it joined them — Slice A's correctness carried Slice B's invalid premise (cgroup usage ÷ V8 old-space cap, two different scopes). Splitting is what stops that recurring, and the agent PR-body rule requires a Resolves target that one PR can actually close.
The Problem
Three services run Node in this deployment. Only orchestrator declared a ceiling (since #16459's restart loop), and that reasoning was never propagated to the two MCP servers. Nobody edited the gap in — it propagated, so every derived deployment inherits it.
There is also a false-green trap in the fix itself, which is why the guard is the substance of this ticket rather than a formality. $SERVER_ENTRYPOINT is double-escaped so Compose does not interpolate it at config time. With a single `# The MCP servers declare no V8 heap ceiling, and the fix has a false-green trap
Delivered-half leaf sub of #16630, split because that parent can no longer be closed by one PR. Same close-target pattern as #16637 under #16596.
Context
mc-server self-aborted at 2026-08-07T11:40:42Z:
FATAL ERROR: Ineffective mark-compacts near heap limit
Allocation failed - JavaScript heap out of memory
No --max-old-space-size was declared, so V8 chose a heuristic ~560 MiB inside a 1 GiB container and killed the process with ~460 MiB of the container's own allowance unused. Because Node aborted rather than the container, ExitCode=0, OOMKilled=false, health=healthy — nothing surfaced it, and peers saw A2A timeouts they could not attribute. kb-server carries the identical shape and has only been spared by a smaller corpus.
Live latest-open sweep: latest 12 open issues checked at 2026-08-07T15:18:43Z. A2A in-flight claim sweep same time: active claims are #16637 (@neo-fable-clio), #16568/#16631 (@neo-opus-grace), #16488/#16551 (@neo-opus-ada). No overlap — this is my own parent's delivered half.
Why this is a sub and not the parent
#16630 was filed as one ticket and is now two slices that cannot travel together, established by @neo-gpt's Drop+Supersede on PR #16634 (review):
Slice A (this ticket) — declare the ceilings and guard the rendering. Configuration only, no observability claim.
Slice B (stays on #16630) — a dimension-matched V8 observation channel. Blocked: nothing in ai/ collects used_heap_size / heap_size_limit for a sibling container, and nothing can over the Docker socket, so it needs a channel that does not exist yet.
PR #16634 failed precisely because it joined them — Slice A's correctness carried Slice B's invalid premise (cgroup usage ÷ V8 old-space cap, two different scopes). Splitting is what stops that recurring, and the agent PR-body rule requires a Resolves target that one PR can actually close.
The Problem
Three services run Node in this deployment. Only orchestrator declared a ceiling (since #16459's restart loop), and that reasoning was never propagated to the two MCP servers. Nobody edited the gap in — it propagated, so every derived deployment inherits it.
There is also a false-green trap in the fix itself, which is why the guard is the substance of this ticket rather than a formality. $SERVER_ENTRYPOINT is double-escaped so Compose does not interpolate it at config time. With a single , Compose interpolates against the host environment, finds nothing, and renders node --max-old-space-size=<n> "" — an invocation with an empty script argument — while docker compose config reports it as a warning and exits 0.
Measured: mutating all 6 occurrences renders that empty argument for all three Node services, orchestrator included, on canonical dev today. A guard asserting the exit code, or asserting the substring SERVER_ENTRYPOINT (true under both escapings), passes on a container that cannot boot. PR #16634 shipped the substring version and its own body called it a rendered-command test.
The Architectural Reality
ai/deploy/docker-compose.yml:399 — the orchestrator's declared, knob-driven ceiling; the shape to copy.
same file, :382 — NODE_OPTIONS explicitly rejected: inherited by every child, so a parent ceiling silently multiplies the container budget across supervised children.
same file, :392-394 — the double-escape rationale and the exit-0 trap, in the precedent's own comment.
same file, :130 / :248 — kb-server and mc-server deploy.resources.limits.memory: 1g with no heap declaration.
mc-server's command: branches on whether a recovery-actuator overlay exists — twonode invocations, and Config.Cmd does not record which one is executing.
The Fix
Declare a command:-scoped --max-old-space-size for both MCP servers from an env knob (NEO_KB_SERVER_HEAP_MB, NEO_MC_SERVER_HEAP_MB), sized below the 1g container limit.
Both branches of mc-server's conditional command, values equal.
A guard that asserts the properties directly rather than proxies for them.
Contract Ledger Matrix
Target Surface
Source of Authority
Proposed Behavior
Fallback
Docs
Evidence
mc-server / kb-servercommand:
docker-compose.yml:399 precedent
declared --max-old-space-size from an env knob, below the container limit
absent knob → the :-768 default
compose comment
heap abort at 11:40:42Z with ~460 MiB unused
NEO_KB_SERVER_HEAP_MB / NEO_MC_SERVER_HEAP_MB
NEO_ORCHESTRATOR_HEAP_MB sibling
compose-level deployment input, matching the existing precedent
compose default
compose comment
the orchestrator knob already exists in this form
escaping guard
docker-compose.yml:392-394
asserts ` present and no single- form (negative lookbehind)
—
spec JSDoc
substring form stayed green under the 6× mutation
branch-equality guard
mc-server's conditional command
all declared values in one command must be identical
—
spec JSDoc
768/256 stayed green under a count-only check
rendered-artifact guard
docker compose configoutput
no node invocation may have an empty script argument
test.skip when Docker is absent; the escaping guard still covers the regression
spec JSDoc
the mutation renders "" for all three services at exit 0
Decision Record impact:none. This slice makes no observability claim, so it amends no ADR. That is deliberate — the ADR-0025 amendment written for PR #16634 said "effective ceiling" and encoded the conflation the review terminated; an amendment is owed only once there is a truthful claim to record, which is Slice B's business.
Acceptance Criteria
mc-server and kb-server each declare a command:-scoped heap ceiling from an env knob, strictly below the container limit, spec-asserted. Equal is insufficient: the process needs room for non-heap allocation on top of the V8 heap, so an equal ceiling still ends in a container OOM-kill instead of the clean abort.
Every node invocation in each service's command carries the ceiling, and all values within one command are equal — asserted as a set of size 1, not a count. Divergent branches make the effective ceiling unknowable from outside the container.
NODE_OPTIONS is never used, spec-asserted with the child-inheritance reason in the assertion message, and the matcher is proven to fire on a known-bad sample before its absence is trusted — an absence assertion whose pattern cannot recognise a presence passes forever.
The entrypoint escaping is asserted directly: $SERVER_ENTRYPOINT present and no single-`# The MCP servers declare no V8 heap ceiling, and the fix has a false-green trap
Delivered-half leaf sub of #16630, split because that parent can no longer be closed by one PR. Same close-target pattern as #16637 under #16596.
Context
mc-server self-aborted at 2026-08-07T11:40:42Z:
FATAL ERROR: Ineffective mark-compacts near heap limit
Allocation failed - JavaScript heap out of memory
No --max-old-space-size was declared, so V8 chose a heuristic ~560 MiB inside a 1 GiB container and killed the process with ~460 MiB of the container's own allowance unused. Because Node aborted rather than the container, ExitCode=0, OOMKilled=false, health=healthy — nothing surfaced it, and peers saw A2A timeouts they could not attribute. kb-server carries the identical shape and has only been spared by a smaller corpus.
Live latest-open sweep: latest 12 open issues checked at 2026-08-07T15:18:43Z. A2A in-flight claim sweep same time: active claims are #16637 (@neo-fable-clio), #16568/#16631 (@neo-opus-grace), #16488/#16551 (@neo-opus-ada). No overlap — this is my own parent's delivered half.
Why this is a sub and not the parent
#16630 was filed as one ticket and is now two slices that cannot travel together, established by @neo-gpt's Drop+Supersede on PR #16634 (review):
Slice A (this ticket) — declare the ceilings and guard the rendering. Configuration only, no observability claim.
Slice B (stays on #16630) — a dimension-matched V8 observation channel. Blocked: nothing in ai/ collects used_heap_size / heap_size_limit for a sibling container, and nothing can over the Docker socket, so it needs a channel that does not exist yet.
PR #16634 failed precisely because it joined them — Slice A's correctness carried Slice B's invalid premise (cgroup usage ÷ V8 old-space cap, two different scopes). Splitting is what stops that recurring, and the agent PR-body rule requires a Resolves target that one PR can actually close.
The Problem
Three services run Node in this deployment. Only orchestrator declared a ceiling (since #16459's restart loop), and that reasoning was never propagated to the two MCP servers. Nobody edited the gap in — it propagated, so every derived deployment inherits it.
There is also a false-green trap in the fix itself, which is why the guard is the substance of this ticket rather than a formality. $SERVER_ENTRYPOINT is double-escaped so Compose does not interpolate it at config time. With a single `# The MCP servers declare no V8 heap ceiling, and the fix has a false-green trap
Delivered-half leaf sub of #16630, split because that parent can no longer be closed by one PR. Same close-target pattern as #16637 under #16596.
Context
mc-server self-aborted at 2026-08-07T11:40:42Z:
FATAL ERROR: Ineffective mark-compacts near heap limit
Allocation failed - JavaScript heap out of memory
No --max-old-space-size was declared, so V8 chose a heuristic ~560 MiB inside a 1 GiB container and killed the process with ~460 MiB of the container's own allowance unused. Because Node aborted rather than the container, ExitCode=0, OOMKilled=false, health=healthy — nothing surfaced it, and peers saw A2A timeouts they could not attribute. kb-server carries the identical shape and has only been spared by a smaller corpus.
Live latest-open sweep: latest 12 open issues checked at 2026-08-07T15:18:43Z. A2A in-flight claim sweep same time: active claims are #16637 (@neo-fable-clio), #16568/#16631 (@neo-opus-grace), #16488/#16551 (@neo-opus-ada). No overlap — this is my own parent's delivered half.
Why this is a sub and not the parent
#16630 was filed as one ticket and is now two slices that cannot travel together, established by @neo-gpt's Drop+Supersede on PR #16634 (review):
Slice A (this ticket) — declare the ceilings and guard the rendering. Configuration only, no observability claim.
Slice B (stays on #16630) — a dimension-matched V8 observation channel. Blocked: nothing in ai/ collects used_heap_size / heap_size_limit for a sibling container, and nothing can over the Docker socket, so it needs a channel that does not exist yet.
PR #16634 failed precisely because it joined them — Slice A's correctness carried Slice B's invalid premise (cgroup usage ÷ V8 old-space cap, two different scopes). Splitting is what stops that recurring, and the agent PR-body rule requires a Resolves target that one PR can actually close.
The Problem
Three services run Node in this deployment. Only orchestrator declared a ceiling (since #16459's restart loop), and that reasoning was never propagated to the two MCP servers. Nobody edited the gap in — it propagated, so every derived deployment inherits it.
There is also a false-green trap in the fix itself, which is why the guard is the substance of this ticket rather than a formality. $SERVER_ENTRYPOINT is double-escaped so Compose does not interpolate it at config time. With a single , Compose interpolates against the host environment, finds nothing, and renders node --max-old-space-size=<n> "" — an invocation with an empty script argument — while docker compose config reports it as a warning and exits 0.
Measured: mutating all 6 occurrences renders that empty argument for all three Node services, orchestrator included, on canonical dev today. A guard asserting the exit code, or asserting the substring SERVER_ENTRYPOINT (true under both escapings), passes on a container that cannot boot. PR #16634 shipped the substring version and its own body called it a rendered-command test.
The Architectural Reality
ai/deploy/docker-compose.yml:399 — the orchestrator's declared, knob-driven ceiling; the shape to copy.
same file, :382 — NODE_OPTIONS explicitly rejected: inherited by every child, so a parent ceiling silently multiplies the container budget across supervised children.
same file, :392-394 — the double-escape rationale and the exit-0 trap, in the precedent's own comment.
same file, :130 / :248 — kb-server and mc-server deploy.resources.limits.memory: 1g with no heap declaration.
mc-server's command: branches on whether a recovery-actuator overlay exists — twonode invocations, and Config.Cmd does not record which one is executing.
The Fix
Declare a command:-scoped --max-old-space-size for both MCP servers from an env knob (NEO_KB_SERVER_HEAP_MB, NEO_MC_SERVER_HEAP_MB), sized below the 1g container limit.
Both branches of mc-server's conditional command, values equal.
A guard that asserts the properties directly rather than proxies for them.
Contract Ledger Matrix
Target Surface
Source of Authority
Proposed Behavior
Fallback
Docs
Evidence
mc-server / kb-servercommand:
docker-compose.yml:399 precedent
declared --max-old-space-size from an env knob, below the container limit
absent knob → the :-768 default
compose comment
heap abort at 11:40:42Z with ~460 MiB unused
NEO_KB_SERVER_HEAP_MB / NEO_MC_SERVER_HEAP_MB
NEO_ORCHESTRATOR_HEAP_MB sibling
compose-level deployment input, matching the existing precedent
compose default
compose comment
the orchestrator knob already exists in this form
escaping guard
docker-compose.yml:392-394
asserts present **and** no single-# The MCP servers declare no V8 heap ceiling, and the fix has a false-green trap
Delivered-half leaf sub of #16630, split because that parent can no longer be closed by one PR. Same close-target pattern as #16637 under #16596.
Context
mc-server self-aborted at 2026-08-07T11:40:42Z:
FATAL ERROR: Ineffective mark-compacts near heap limit
Allocation failed - JavaScript heap out of memory
No --max-old-space-size was declared, so V8 chose a heuristic ~560 MiB inside a 1 GiB container and killed the process with ~460 MiB of the container's own allowance unused. Because Node aborted rather than the container, ExitCode=0, OOMKilled=false, health=healthy — nothing surfaced it, and peers saw A2A timeouts they could not attribute. kb-server carries the identical shape and has only been spared by a smaller corpus.
Live latest-open sweep: latest 12 open issues checked at 2026-08-07T15:18:43Z. A2A in-flight claim sweep same time: active claims are #16637 (@neo-fable-clio), #16568/#16631 (@neo-opus-grace), #16488/#16551 (@neo-opus-ada). No overlap — this is my own parent's delivered half.
Why this is a sub and not the parent
#16630 was filed as one ticket and is now two slices that cannot travel together, established by @neo-gpt's Drop+Supersede on PR #16634 (review):
Slice A (this ticket) — declare the ceilings and guard the rendering. Configuration only, no observability claim.
Slice B (stays on #16630) — a dimension-matched V8 observation channel. Blocked: nothing in ai/ collects used_heap_size / heap_size_limit for a sibling container, and nothing can over the Docker socket, so it needs a channel that does not exist yet.
PR #16634 failed precisely because it joined them — Slice A's correctness carried Slice B's invalid premise (cgroup usage ÷ V8 old-space cap, two different scopes). Splitting is what stops that recurring, and the agent PR-body rule requires a Resolves target that one PR can actually close.
The Problem
Three services run Node in this deployment. Only orchestrator declared a ceiling (since #16459's restart loop), and that reasoning was never propagated to the two MCP servers. Nobody edited the gap in — it propagated, so every derived deployment inherits it.
There is also a false-green trap in the fix itself, which is why the guard is the substance of this ticket rather than a formality. $SERVER_ENTRYPOINT is double-escaped so Compose does not interpolate it at config time. With a single `# The MCP servers declare no V8 heap ceiling, and the fix has a false-green trap
Delivered-half leaf sub of #16630, split because that parent can no longer be closed by one PR. Same close-target pattern as #16637 under #16596.
Context
mc-server self-aborted at 2026-08-07T11:40:42Z:
FATAL ERROR: Ineffective mark-compacts near heap limit
Allocation failed - JavaScript heap out of memory
No --max-old-space-size was declared, so V8 chose a heuristic ~560 MiB inside a 1 GiB container and killed the process with ~460 MiB of the container's own allowance unused. Because Node aborted rather than the container, ExitCode=0, OOMKilled=false, health=healthy — nothing surfaced it, and peers saw A2A timeouts they could not attribute. kb-server carries the identical shape and has only been spared by a smaller corpus.
Live latest-open sweep: latest 12 open issues checked at 2026-08-07T15:18:43Z. A2A in-flight claim sweep same time: active claims are #16637 (@neo-fable-clio), #16568/#16631 (@neo-opus-grace), #16488/#16551 (@neo-opus-ada). No overlap — this is my own parent's delivered half.
Why this is a sub and not the parent
#16630 was filed as one ticket and is now two slices that cannot travel together, established by @neo-gpt's Drop+Supersede on PR #16634 (review):
Slice A (this ticket) — declare the ceilings and guard the rendering. Configuration only, no observability claim.
Slice B (stays on #16630) — a dimension-matched V8 observation channel. Blocked: nothing in ai/ collects used_heap_size / heap_size_limit for a sibling container, and nothing can over the Docker socket, so it needs a channel that does not exist yet.
PR #16634 failed precisely because it joined them — Slice A's correctness carried Slice B's invalid premise (cgroup usage ÷ V8 old-space cap, two different scopes). Splitting is what stops that recurring, and the agent PR-body rule requires a Resolves target that one PR can actually close.
The Problem
Three services run Node in this deployment. Only orchestrator declared a ceiling (since #16459's restart loop), and that reasoning was never propagated to the two MCP servers. Nobody edited the gap in — it propagated, so every derived deployment inherits it.
There is also a false-green trap in the fix itself, which is why the guard is the substance of this ticket rather than a formality. $SERVER_ENTRYPOINT is double-escaped so Compose does not interpolate it at config time. With a single , Compose interpolates against the host environment, finds nothing, and renders node --max-old-space-size=<n> "" — an invocation with an empty script argument — while docker compose config reports it as a warning and exits 0.
Measured: mutating all 6 occurrences renders that empty argument for all three Node services, orchestrator included, on canonical dev today. A guard asserting the exit code, or asserting the substring SERVER_ENTRYPOINT (true under both escapings), passes on a container that cannot boot. PR #16634 shipped the substring version and its own body called it a rendered-command test.
The Architectural Reality
ai/deploy/docker-compose.yml:399 — the orchestrator's declared, knob-driven ceiling; the shape to copy.
same file, :382 — NODE_OPTIONS explicitly rejected: inherited by every child, so a parent ceiling silently multiplies the container budget across supervised children.
same file, :392-394 — the double-escape rationale and the exit-0 trap, in the precedent's own comment.
same file, :130 / :248 — kb-server and mc-server deploy.resources.limits.memory: 1g with no heap declaration.
mc-server's command: branches on whether a recovery-actuator overlay exists — twonode invocations, and Config.Cmd does not record which one is executing.
The Fix
Declare a command:-scoped --max-old-space-size for both MCP servers from an env knob (NEO_KB_SERVER_HEAP_MB, NEO_MC_SERVER_HEAP_MB), sized below the 1g container limit.
Both branches of mc-server's conditional command, values equal.
A guard that asserts the properties directly rather than proxies for them.
Contract Ledger Matrix
Target Surface
Source of Authority
Proposed Behavior
Fallback
Docs
Evidence
mc-server / kb-servercommand:
docker-compose.yml:399 precedent
declared --max-old-space-size from an env knob, below the container limit
absent knob → the :-768 default
compose comment
heap abort at 11:40:42Z with ~460 MiB unused
NEO_KB_SERVER_HEAP_MB / NEO_MC_SERVER_HEAP_MB
NEO_ORCHESTRATOR_HEAP_MB sibling
compose-level deployment input, matching the existing precedent
compose default
compose comment
the orchestrator knob already exists in this form
escaping guard
docker-compose.yml:392-394
asserts ` present and no single- form (negative lookbehind)
—
spec JSDoc
substring form stayed green under the 6× mutation
branch-equality guard
mc-server's conditional command
all declared values in one command must be identical
—
spec JSDoc
768/256 stayed green under a count-only check
rendered-artifact guard
docker compose configoutput
no node invocation may have an empty script argument
test.skip when Docker is absent; the escaping guard still covers the regression
spec JSDoc
the mutation renders "" for all three services at exit 0
Decision Record impact:none. This slice makes no observability claim, so it amends no ADR. That is deliberate — the ADR-0025 amendment written for PR #16634 said "effective ceiling" and encoded the conflation the review terminated; an amendment is owed only once there is a truthful claim to record, which is Slice B's business.
Acceptance Criteria
mc-server and kb-server each declare a command:-scoped heap ceiling from an env knob, strictly below the container limit, spec-asserted. Equal is insufficient: the process needs room for non-heap allocation on top of the V8 heap, so an equal ceiling still ends in a container OOM-kill instead of the clean abort.
Every node invocation in each service's command carries the ceiling, and all values within one command are equal — asserted as a set of size 1, not a count. Divergent branches make the effective ceiling unknowable from outside the container.
NODE_OPTIONS is never used, spec-asserted with the child-inheritance reason in the assertion message, and the matcher is proven to fire on a known-bad sample before its absence is trusted — an absence assertion whose pattern cannot recognise a presence passes forever.
The entrypoint escaping is asserted directly: $SERVER_ENTRYPOINT present and no single- form anywhere, via negative lookbehind so a command mixing both cannot average out to passing.
A guard consumes docker compose configoutput and rejects any node invocation with an empty script argument. May skip when Docker is unavailable only because the escaping assertion covers the same regression without it; the skip must be explicit, never silent.
All three Node services are covered, orchestrator included. Its exposure is pre-existing and reproduces on canonical dev, so a two-service guard leaves the original instance unguarded.
Mutation-proven, by name not by count: the 6× `→ mutation and the divergent-ceiling mutation each turn specific named tests red, and each mutation's application is verified by occurrence count before the run — a silent no-op substitution exits 0.
Negative control: a non-Node service (chroma) is not required to declare a ceiling, but does carry a container limit, so the fixture is real.
kb-server ships in the same change as mc-server.
Out of Scope
Slice B — any V8 saturation fact.#16630. No memory-saturation fact for a Node service may be computed from a cross-scope pair, and this ticket touches ContainerHealthDiagnosisServicenot at all.
The heap-OOM exit signature as a detect signal — #16630's leading detection candidate, deliberately not bundled so this slice makes no observability claim.
Whether ~560 MiB was a leak or a bounded working set — #16463. Declaring a ceiling does not characterise a growth curve, and raising one to hide unbounded growth converts a fast failure into a slow one.
Deriving the ceiling from the container limit rather than declaring it independently. Tempting, but the two limits answer different questions and a derived value would silently move when the container limit moves for unrelated reasons.
Avoided Traps
NODE_OPTIONS. The intuitive fix and the rejected one. I proposed it verbally before reading :382 and @neo-opus-grace caught it.
Asserting docker compose config exit 0. It exits 0 on a broken render. The precedent's own comment says so, and PR #16634 still shipped a guard that could not detect it.
Asserting the substring SERVER_ENTRYPOINT. True under both escapings, so it can never distinguish them — the specific hole the reviewer falsified.
Counting ceilings instead of comparing them.768/256 passes a count check while the parser reports the unexecuted branch.
Assuming docker compose config prints the post-interpolation command. It round-trips the canonical form, so a correct file renders "$SERVER_ENTRYPOINT" with the escape intact; the broken form collapses to "". The empty-script match is the discriminator. Verified by a red test against a correct file, and independently reached by @neo-opus-grace on a derived deployment.
Related
#16630 — the parent; keeps Slice B (the dimension-matched V8 observation channel) and the heap-OOM exit-signature candidate.
PR #16634 — closed unmerged; its review is the authority for this split and its salvage map is cited on #16630.
#16636 — the act half (raise-ceiling reachability + the undeclared-ceiling record).
#16459 — the 968-restart loop that produced the orchestrator's declared ceiling, and the reason its exposure is pre-existing.
Retrieval Hint: query_raw_memories("compose config exits 0 on broken interpolation empty script argument") · docker-compose.yml:392-394 the double-escape trap · the 11:40:42Z mc-server abort.
Evidence Residual
[L3-deferred — operator handoff needed] After merge, recreate kb-server and mc-server once and verify each live Config.Cmd contains --max-old-space-size=768. Append the receipt here before final close; a failed observation creates a successor rather than being promoted from L1 evidence.
tobiu referenced in commit 257d961 - "Declare the MCP servers' V8 heap ceilings, guarded by the escaping that breaks (#16630) (#16640) on Aug 7, 2026, 7:06 PM
Delivered-half leaf sub of #16630, split because that parent can no longer be closed by one PR. Same close-target pattern as #16637 under #16596.
Context
mc-serverself-aborted at2026-08-07T11:40:42Z:No
--max-old-space-sizewas declared, so V8 chose a heuristic ~560 MiB inside a 1 GiB container and killed the process with ~460 MiB of the container's own allowance unused. Because Node aborted rather than the container,ExitCode=0,OOMKilled=false,health=healthy— nothing surfaced it, and peers saw A2A timeouts they could not attribute.kb-servercarries the identical shape and has only been spared by a smaller corpus.Live latest-open sweep: latest 12 open issues checked at 2026-08-07T15:18:43Z. A2A in-flight claim sweep same time: active claims are
#16637(@neo-fable-clio),#16568/#16631(@neo-opus-grace),#16488/#16551(@neo-opus-ada). No overlap — this is my own parent's delivered half.Why this is a sub and not the parent
#16630 was filed as one ticket and is now two slices that cannot travel together, established by @neo-gpt's Drop+Supersede on PR #16634 (review):
ai/collectsused_heap_size/heap_size_limitfor a sibling container, and nothing can over the Docker socket, so it needs a channel that does not exist yet.PR #16634 failed precisely because it joined them — Slice A's correctness carried Slice B's invalid premise (cgroup usage ÷ V8 old-space cap, two different scopes). Splitting is what stops that recurring, and the agent PR-body rule requires a
Resolvestarget that one PR can actually close.The Problem
Three services run Node in this deployment. Only
orchestratordeclared a ceiling (since #16459's restart loop), and that reasoning was never propagated to the two MCP servers. Nobody edited the gap in — it propagated, so every derived deployment inherits it.There is also a false-green trap in the fix itself, which is why the guard is the substance of this ticket rather than a formality.
$SERVER_ENTRYPOINTis double-escaped so Compose does not interpolate it at config time. With a single `# The MCP servers declare no V8 heap ceiling, and the fix has a false-green trapDelivered-half leaf sub of #16630, split because that parent can no longer be closed by one PR. Same close-target pattern as #16637 under #16596.
Context
mc-serverself-aborted at2026-08-07T11:40:42Z:No
--max-old-space-sizewas declared, so V8 chose a heuristic ~560 MiB inside a 1 GiB container and killed the process with ~460 MiB of the container's own allowance unused. Because Node aborted rather than the container,ExitCode=0,OOMKilled=false,health=healthy— nothing surfaced it, and peers saw A2A timeouts they could not attribute.kb-servercarries the identical shape and has only been spared by a smaller corpus.Live latest-open sweep: latest 12 open issues checked at 2026-08-07T15:18:43Z. A2A in-flight claim sweep same time: active claims are
#16637(@neo-fable-clio),#16568/#16631(@neo-opus-grace),#16488/#16551(@neo-opus-ada). No overlap — this is my own parent's delivered half.Why this is a sub and not the parent
#16630 was filed as one ticket and is now two slices that cannot travel together, established by @neo-gpt's Drop+Supersede on PR #16634 (review):
ai/collectsused_heap_size/heap_size_limitfor a sibling container, and nothing can over the Docker socket, so it needs a channel that does not exist yet.PR #16634 failed precisely because it joined them — Slice A's correctness carried Slice B's invalid premise (cgroup usage ÷ V8 old-space cap, two different scopes). Splitting is what stops that recurring, and the agent PR-body rule requires a
Resolvestarget that one PR can actually close.The Problem
Three services run Node in this deployment. Only
orchestratordeclared a ceiling (since #16459's restart loop), and that reasoning was never propagated to the two MCP servers. Nobody edited the gap in — it propagated, so every derived deployment inherits it.There is also a false-green trap in the fix itself, which is why the guard is the substance of this ticket rather than a formality.
$SERVER_ENTRYPOINTis double-escaped so Compose does not interpolate it at config time. With a single , Compose interpolates against the host environment, finds nothing, and rendersnode --max-old-space-size=<n> ""— an invocation with an empty script argument — whiledocker compose configreports it as a warning and exits 0.Measured: mutating all 6 occurrences renders that empty argument for all three Node services,
orchestratorincluded, on canonicaldevtoday. A guard asserting the exit code, or asserting the substringSERVER_ENTRYPOINT(true under both escapings), passes on a container that cannot boot. PR #16634 shipped the substring version and its own body called it a rendered-command test.The Architectural Reality
ai/deploy/docker-compose.yml:399— the orchestrator's declared, knob-driven ceiling; the shape to copy.:382—NODE_OPTIONSexplicitly rejected: inherited by every child, so a parent ceiling silently multiplies the container budget across supervised children.:392-394— the double-escape rationale and the exit-0 trap, in the precedent's own comment.:130/:248— kb-server and mc-serverdeploy.resources.limits.memory: 1gwith no heap declaration.command:branches on whether a recovery-actuator overlay exists — twonodeinvocations, andConfig.Cmddoes not record which one is executing.The Fix
command:-scoped--max-old-space-sizefor both MCP servers from an env knob (NEO_KB_SERVER_HEAP_MB,NEO_MC_SERVER_HEAP_MB), sized below the 1g container limit.Contract Ledger Matrix
mc-server/kb-servercommand:docker-compose.yml:399precedent--max-old-space-sizefrom an env knob, below the container limit:-768default11:40:42Zwith ~460 MiB unusedNEO_KB_SERVER_HEAP_MB/NEO_MC_SERVER_HEAP_MBNEO_ORCHESTRATOR_HEAP_MBsiblingdocker-compose.yml:392-394present **and** no single-# The MCP servers declare no V8 heap ceiling, and the fix has a false-green trapDelivered-half leaf sub of #16630, split because that parent can no longer be closed by one PR. Same close-target pattern as #16637 under #16596.
Context
mc-serverself-aborted at2026-08-07T11:40:42Z:No
--max-old-space-sizewas declared, so V8 chose a heuristic ~560 MiB inside a 1 GiB container and killed the process with ~460 MiB of the container's own allowance unused. Because Node aborted rather than the container,ExitCode=0,OOMKilled=false,health=healthy— nothing surfaced it, and peers saw A2A timeouts they could not attribute.kb-servercarries the identical shape and has only been spared by a smaller corpus.Live latest-open sweep: latest 12 open issues checked at 2026-08-07T15:18:43Z. A2A in-flight claim sweep same time: active claims are
#16637(@neo-fable-clio),#16568/#16631(@neo-opus-grace),#16488/#16551(@neo-opus-ada). No overlap — this is my own parent's delivered half.Why this is a sub and not the parent
#16630 was filed as one ticket and is now two slices that cannot travel together, established by @neo-gpt's Drop+Supersede on PR #16634 (review):
ai/collectsused_heap_size/heap_size_limitfor a sibling container, and nothing can over the Docker socket, so it needs a channel that does not exist yet.PR #16634 failed precisely because it joined them — Slice A's correctness carried Slice B's invalid premise (cgroup usage ÷ V8 old-space cap, two different scopes). Splitting is what stops that recurring, and the agent PR-body rule requires a
Resolvestarget that one PR can actually close.The Problem
Three services run Node in this deployment. Only
orchestratordeclared a ceiling (since #16459's restart loop), and that reasoning was never propagated to the two MCP servers. Nobody edited the gap in — it propagated, so every derived deployment inherits it.There is also a false-green trap in the fix itself, which is why the guard is the substance of this ticket rather than a formality.
$SERVER_ENTRYPOINTis double-escaped so Compose does not interpolate it at config time. With a single `# The MCP servers declare no V8 heap ceiling, and the fix has a false-green trapDelivered-half leaf sub of #16630, split because that parent can no longer be closed by one PR. Same close-target pattern as #16637 under #16596.
Context
mc-serverself-aborted at2026-08-07T11:40:42Z:No
--max-old-space-sizewas declared, so V8 chose a heuristic ~560 MiB inside a 1 GiB container and killed the process with ~460 MiB of the container's own allowance unused. Because Node aborted rather than the container,ExitCode=0,OOMKilled=false,health=healthy— nothing surfaced it, and peers saw A2A timeouts they could not attribute.kb-servercarries the identical shape and has only been spared by a smaller corpus.Live latest-open sweep: latest 12 open issues checked at 2026-08-07T15:18:43Z. A2A in-flight claim sweep same time: active claims are
#16637(@neo-fable-clio),#16568/#16631(@neo-opus-grace),#16488/#16551(@neo-opus-ada). No overlap — this is my own parent's delivered half.Why this is a sub and not the parent
#16630 was filed as one ticket and is now two slices that cannot travel together, established by @neo-gpt's Drop+Supersede on PR #16634 (review):
ai/collectsused_heap_size/heap_size_limitfor a sibling container, and nothing can over the Docker socket, so it needs a channel that does not exist yet.PR #16634 failed precisely because it joined them — Slice A's correctness carried Slice B's invalid premise (cgroup usage ÷ V8 old-space cap, two different scopes). Splitting is what stops that recurring, and the agent PR-body rule requires a
Resolvestarget that one PR can actually close.The Problem
Three services run Node in this deployment. Only
orchestratordeclared a ceiling (since #16459's restart loop), and that reasoning was never propagated to the two MCP servers. Nobody edited the gap in — it propagated, so every derived deployment inherits it.There is also a false-green trap in the fix itself, which is why the guard is the substance of this ticket rather than a formality.
$SERVER_ENTRYPOINTis double-escaped so Compose does not interpolate it at config time. With a single , Compose interpolates against the host environment, finds nothing, and rendersnode --max-old-space-size=<n> ""— an invocation with an empty script argument — whiledocker compose configreports it as a warning and exits 0.Measured: mutating all 6 occurrences renders that empty argument for all three Node services,
orchestratorincluded, on canonicaldevtoday. A guard asserting the exit code, or asserting the substringSERVER_ENTRYPOINT(true under both escapings), passes on a container that cannot boot. PR #16634 shipped the substring version and its own body called it a rendered-command test.The Architectural Reality
ai/deploy/docker-compose.yml:399— the orchestrator's declared, knob-driven ceiling; the shape to copy.:382—NODE_OPTIONSexplicitly rejected: inherited by every child, so a parent ceiling silently multiplies the container budget across supervised children.:392-394— the double-escape rationale and the exit-0 trap, in the precedent's own comment.:130/:248— kb-server and mc-serverdeploy.resources.limits.memory: 1gwith no heap declaration.command:branches on whether a recovery-actuator overlay exists — twonodeinvocations, andConfig.Cmddoes not record which one is executing.The Fix
command:-scoped--max-old-space-sizefor both MCP servers from an env knob (NEO_KB_SERVER_HEAP_MB,NEO_MC_SERVER_HEAP_MB), sized below the 1g container limit.Contract Ledger Matrix
mc-server/kb-servercommand:docker-compose.yml:399precedent--max-old-space-sizefrom an env knob, below the container limit:-768default11:40:42Zwith ~460 MiB unusedNEO_KB_SERVER_HEAP_MB/NEO_MC_SERVER_HEAP_MBNEO_ORCHESTRATOR_HEAP_MBsiblingdocker-compose.yml:392-394768/256stayed green under a count-only checkdocker compose configoutputnodeinvocation may have an empty script argumenttest.skipwhen Docker is absent; the escaping guard still covers the regression""for all three services at exit 0Decision Record impact:
none. This slice makes no observability claim, so it amends no ADR. That is deliberate — the ADR-0025 amendment written for PR #16634 said "effective ceiling" and encoded the conflation the review terminated; an amendment is owed only once there is a truthful claim to record, which is Slice B's business.Acceptance Criteria
mc-serverandkb-servereach declare acommand:-scoped heap ceiling from an env knob, strictly below the container limit, spec-asserted. Equal is insufficient: the process needs room for non-heap allocation on top of the V8 heap, so an equal ceiling still ends in a container OOM-kill instead of the clean abort.nodeinvocation in each service's command carries the ceiling, and all values within one command are equal — asserted as a set of size 1, not a count. Divergent branches make the effective ceiling unknowable from outside the container.NODE_OPTIONSis never used, spec-asserted with the child-inheritance reason in the assertion message, and the matcher is proven to fire on a known-bad sample before its absence is trusted — an absence assertion whose pattern cannot recognise a presence passes forever.$SERVER_ENTRYPOINTpresent and no single-`# The MCP servers declare no V8 heap ceiling, and the fix has a false-green trapDelivered-half leaf sub of #16630, split because that parent can no longer be closed by one PR. Same close-target pattern as #16637 under #16596.
Context
mc-serverself-aborted at2026-08-07T11:40:42Z:No
--max-old-space-sizewas declared, so V8 chose a heuristic ~560 MiB inside a 1 GiB container and killed the process with ~460 MiB of the container's own allowance unused. Because Node aborted rather than the container,ExitCode=0,OOMKilled=false,health=healthy— nothing surfaced it, and peers saw A2A timeouts they could not attribute.kb-servercarries the identical shape and has only been spared by a smaller corpus.Live latest-open sweep: latest 12 open issues checked at 2026-08-07T15:18:43Z. A2A in-flight claim sweep same time: active claims are
#16637(@neo-fable-clio),#16568/#16631(@neo-opus-grace),#16488/#16551(@neo-opus-ada). No overlap — this is my own parent's delivered half.Why this is a sub and not the parent
#16630 was filed as one ticket and is now two slices that cannot travel together, established by @neo-gpt's Drop+Supersede on PR #16634 (review):
ai/collectsused_heap_size/heap_size_limitfor a sibling container, and nothing can over the Docker socket, so it needs a channel that does not exist yet.PR #16634 failed precisely because it joined them — Slice A's correctness carried Slice B's invalid premise (cgroup usage ÷ V8 old-space cap, two different scopes). Splitting is what stops that recurring, and the agent PR-body rule requires a
Resolvestarget that one PR can actually close.The Problem
Three services run Node in this deployment. Only
orchestratordeclared a ceiling (since #16459's restart loop), and that reasoning was never propagated to the two MCP servers. Nobody edited the gap in — it propagated, so every derived deployment inherits it.There is also a false-green trap in the fix itself, which is why the guard is the substance of this ticket rather than a formality.
$SERVER_ENTRYPOINTis double-escaped so Compose does not interpolate it at config time. With a single `# The MCP servers declare no V8 heap ceiling, and the fix has a false-green trapDelivered-half leaf sub of #16630, split because that parent can no longer be closed by one PR. Same close-target pattern as #16637 under #16596.
Context
mc-serverself-aborted at2026-08-07T11:40:42Z:No
--max-old-space-sizewas declared, so V8 chose a heuristic ~560 MiB inside a 1 GiB container and killed the process with ~460 MiB of the container's own allowance unused. Because Node aborted rather than the container,ExitCode=0,OOMKilled=false,health=healthy— nothing surfaced it, and peers saw A2A timeouts they could not attribute.kb-servercarries the identical shape and has only been spared by a smaller corpus.Live latest-open sweep: latest 12 open issues checked at 2026-08-07T15:18:43Z. A2A in-flight claim sweep same time: active claims are
#16637(@neo-fable-clio),#16568/#16631(@neo-opus-grace),#16488/#16551(@neo-opus-ada). No overlap — this is my own parent's delivered half.Why this is a sub and not the parent
#16630 was filed as one ticket and is now two slices that cannot travel together, established by @neo-gpt's Drop+Supersede on PR #16634 (review):
ai/collectsused_heap_size/heap_size_limitfor a sibling container, and nothing can over the Docker socket, so it needs a channel that does not exist yet.PR #16634 failed precisely because it joined them — Slice A's correctness carried Slice B's invalid premise (cgroup usage ÷ V8 old-space cap, two different scopes). Splitting is what stops that recurring, and the agent PR-body rule requires a
Resolvestarget that one PR can actually close.The Problem
Three services run Node in this deployment. Only
orchestratordeclared a ceiling (since #16459's restart loop), and that reasoning was never propagated to the two MCP servers. Nobody edited the gap in — it propagated, so every derived deployment inherits it.There is also a false-green trap in the fix itself, which is why the guard is the substance of this ticket rather than a formality.
$SERVER_ENTRYPOINTis double-escaped so Compose does not interpolate it at config time. With a single , Compose interpolates against the host environment, finds nothing, and rendersnode --max-old-space-size=<n> ""— an invocation with an empty script argument — whiledocker compose configreports it as a warning and exits 0.Measured: mutating all 6 occurrences renders that empty argument for all three Node services,
orchestratorincluded, on canonicaldevtoday. A guard asserting the exit code, or asserting the substringSERVER_ENTRYPOINT(true under both escapings), passes on a container that cannot boot. PR #16634 shipped the substring version and its own body called it a rendered-command test.The Architectural Reality
ai/deploy/docker-compose.yml:399— the orchestrator's declared, knob-driven ceiling; the shape to copy.:382—NODE_OPTIONSexplicitly rejected: inherited by every child, so a parent ceiling silently multiplies the container budget across supervised children.:392-394— the double-escape rationale and the exit-0 trap, in the precedent's own comment.:130/:248— kb-server and mc-serverdeploy.resources.limits.memory: 1gwith no heap declaration.command:branches on whether a recovery-actuator overlay exists — twonodeinvocations, andConfig.Cmddoes not record which one is executing.The Fix
command:-scoped--max-old-space-sizefor both MCP servers from an env knob (NEO_KB_SERVER_HEAP_MB,NEO_MC_SERVER_HEAP_MB), sized below the 1g container limit.Contract Ledger Matrix
mc-server/kb-servercommand:docker-compose.yml:399precedent--max-old-space-sizefrom an env knob, below the container limit:-768default11:40:42Zwith ~460 MiB unusedNEO_KB_SERVER_HEAP_MB/NEO_MC_SERVER_HEAP_MBNEO_ORCHESTRATOR_HEAP_MBsiblingdocker-compose.yml:392-394present **and** no single-# The MCP servers declare no V8 heap ceiling, and the fix has a false-green trapDelivered-half leaf sub of #16630, split because that parent can no longer be closed by one PR. Same close-target pattern as #16637 under #16596.
Context
mc-serverself-aborted at2026-08-07T11:40:42Z:No
--max-old-space-sizewas declared, so V8 chose a heuristic ~560 MiB inside a 1 GiB container and killed the process with ~460 MiB of the container's own allowance unused. Because Node aborted rather than the container,ExitCode=0,OOMKilled=false,health=healthy— nothing surfaced it, and peers saw A2A timeouts they could not attribute.kb-servercarries the identical shape and has only been spared by a smaller corpus.Live latest-open sweep: latest 12 open issues checked at 2026-08-07T15:18:43Z. A2A in-flight claim sweep same time: active claims are
#16637(@neo-fable-clio),#16568/#16631(@neo-opus-grace),#16488/#16551(@neo-opus-ada). No overlap — this is my own parent's delivered half.Why this is a sub and not the parent
#16630 was filed as one ticket and is now two slices that cannot travel together, established by @neo-gpt's Drop+Supersede on PR #16634 (review):
ai/collectsused_heap_size/heap_size_limitfor a sibling container, and nothing can over the Docker socket, so it needs a channel that does not exist yet.PR #16634 failed precisely because it joined them — Slice A's correctness carried Slice B's invalid premise (cgroup usage ÷ V8 old-space cap, two different scopes). Splitting is what stops that recurring, and the agent PR-body rule requires a
Resolvestarget that one PR can actually close.The Problem
Three services run Node in this deployment. Only
orchestratordeclared a ceiling (since #16459's restart loop), and that reasoning was never propagated to the two MCP servers. Nobody edited the gap in — it propagated, so every derived deployment inherits it.There is also a false-green trap in the fix itself, which is why the guard is the substance of this ticket rather than a formality.
$SERVER_ENTRYPOINTis double-escaped so Compose does not interpolate it at config time. With a single `# The MCP servers declare no V8 heap ceiling, and the fix has a false-green trapDelivered-half leaf sub of #16630, split because that parent can no longer be closed by one PR. Same close-target pattern as #16637 under #16596.
Context
mc-serverself-aborted at2026-08-07T11:40:42Z:No
--max-old-space-sizewas declared, so V8 chose a heuristic ~560 MiB inside a 1 GiB container and killed the process with ~460 MiB of the container's own allowance unused. Because Node aborted rather than the container,ExitCode=0,OOMKilled=false,health=healthy— nothing surfaced it, and peers saw A2A timeouts they could not attribute.kb-servercarries the identical shape and has only been spared by a smaller corpus.Live latest-open sweep: latest 12 open issues checked at 2026-08-07T15:18:43Z. A2A in-flight claim sweep same time: active claims are
#16637(@neo-fable-clio),#16568/#16631(@neo-opus-grace),#16488/#16551(@neo-opus-ada). No overlap — this is my own parent's delivered half.Why this is a sub and not the parent
#16630 was filed as one ticket and is now two slices that cannot travel together, established by @neo-gpt's Drop+Supersede on PR #16634 (review):
ai/collectsused_heap_size/heap_size_limitfor a sibling container, and nothing can over the Docker socket, so it needs a channel that does not exist yet.PR #16634 failed precisely because it joined them — Slice A's correctness carried Slice B's invalid premise (cgroup usage ÷ V8 old-space cap, two different scopes). Splitting is what stops that recurring, and the agent PR-body rule requires a
Resolvestarget that one PR can actually close.The Problem
Three services run Node in this deployment. Only
orchestratordeclared a ceiling (since #16459's restart loop), and that reasoning was never propagated to the two MCP servers. Nobody edited the gap in — it propagated, so every derived deployment inherits it.There is also a false-green trap in the fix itself, which is why the guard is the substance of this ticket rather than a formality.
$SERVER_ENTRYPOINTis double-escaped so Compose does not interpolate it at config time. With a single , Compose interpolates against the host environment, finds nothing, and rendersnode --max-old-space-size=<n> ""— an invocation with an empty script argument — whiledocker compose configreports it as a warning and exits 0.Measured: mutating all 6 occurrences renders that empty argument for all three Node services,
orchestratorincluded, on canonicaldevtoday. A guard asserting the exit code, or asserting the substringSERVER_ENTRYPOINT(true under both escapings), passes on a container that cannot boot. PR #16634 shipped the substring version and its own body called it a rendered-command test.The Architectural Reality
ai/deploy/docker-compose.yml:399— the orchestrator's declared, knob-driven ceiling; the shape to copy.:382—NODE_OPTIONSexplicitly rejected: inherited by every child, so a parent ceiling silently multiplies the container budget across supervised children.:392-394— the double-escape rationale and the exit-0 trap, in the precedent's own comment.:130/:248— kb-server and mc-serverdeploy.resources.limits.memory: 1gwith no heap declaration.command:branches on whether a recovery-actuator overlay exists — twonodeinvocations, andConfig.Cmddoes not record which one is executing.The Fix
command:-scoped--max-old-space-sizefor both MCP servers from an env knob (NEO_KB_SERVER_HEAP_MB,NEO_MC_SERVER_HEAP_MB), sized below the 1g container limit.Contract Ledger Matrix
mc-server/kb-servercommand:docker-compose.yml:399precedent--max-old-space-sizefrom an env knob, below the container limit:-768default11:40:42Zwith ~460 MiB unusedNEO_KB_SERVER_HEAP_MB/NEO_MC_SERVER_HEAP_MBNEO_ORCHESTRATOR_HEAP_MBsiblingdocker-compose.yml:392-394768/256stayed green under a count-only checkdocker compose configoutputnodeinvocation may have an empty script argumenttest.skipwhen Docker is absent; the escaping guard still covers the regression""for all three services at exit 0Decision Record impact:
none. This slice makes no observability claim, so it amends no ADR. That is deliberate — the ADR-0025 amendment written for PR #16634 said "effective ceiling" and encoded the conflation the review terminated; an amendment is owed only once there is a truthful claim to record, which is Slice B's business.Acceptance Criteria
mc-serverandkb-servereach declare acommand:-scoped heap ceiling from an env knob, strictly below the container limit, spec-asserted. Equal is insufficient: the process needs room for non-heap allocation on top of the V8 heap, so an equal ceiling still ends in a container OOM-kill instead of the clean abort.nodeinvocation in each service's command carries the ceiling, and all values within one command are equal — asserted as a set of size 1, not a count. Divergent branches make the effective ceiling unknowable from outside the container.NODE_OPTIONSis never used, spec-asserted with the child-inheritance reason in the assertion message, and the matcher is proven to fire on a known-bad sample before its absence is trusted — an absence assertion whose pattern cannot recognise a presence passes forever.$SERVER_ENTRYPOINTpresent and no single- form anywhere, via negative lookbehind so a command mixing both cannot average out to passing.docker compose configoutput and rejects anynodeinvocation with an empty script argument. May skip when Docker is unavailable only because the escaping assertion covers the same regression without it; the skip must be explicit, never silent.orchestratorincluded. Its exposure is pre-existing and reproduces on canonicaldev, so a two-service guard leaves the original instance unguarded.→# The MCP servers declare no V8 heap ceiling, and the fix has a false-green trapDelivered-half leaf sub of #16630, split because that parent can no longer be closed by one PR. Same close-target pattern as #16637 under #16596.
Context
mc-serverself-aborted at2026-08-07T11:40:42Z:No
--max-old-space-sizewas declared, so V8 chose a heuristic ~560 MiB inside a 1 GiB container and killed the process with ~460 MiB of the container's own allowance unused. Because Node aborted rather than the container,ExitCode=0,OOMKilled=false,health=healthy— nothing surfaced it, and peers saw A2A timeouts they could not attribute.kb-servercarries the identical shape and has only been spared by a smaller corpus.Live latest-open sweep: latest 12 open issues checked at 2026-08-07T15:18:43Z. A2A in-flight claim sweep same time: active claims are
#16637(@neo-fable-clio),#16568/#16631(@neo-opus-grace),#16488/#16551(@neo-opus-ada). No overlap — this is my own parent's delivered half.Why this is a sub and not the parent
#16630 was filed as one ticket and is now two slices that cannot travel together, established by @neo-gpt's Drop+Supersede on PR #16634 (review):
ai/collectsused_heap_size/heap_size_limitfor a sibling container, and nothing can over the Docker socket, so it needs a channel that does not exist yet.PR #16634 failed precisely because it joined them — Slice A's correctness carried Slice B's invalid premise (cgroup usage ÷ V8 old-space cap, two different scopes). Splitting is what stops that recurring, and the agent PR-body rule requires a
Resolvestarget that one PR can actually close.The Problem
Three services run Node in this deployment. Only
orchestratordeclared a ceiling (since #16459's restart loop), and that reasoning was never propagated to the two MCP servers. Nobody edited the gap in — it propagated, so every derived deployment inherits it.There is also a false-green trap in the fix itself, which is why the guard is the substance of this ticket rather than a formality.
$SERVER_ENTRYPOINTis double-escaped so Compose does not interpolate it at config time. With a single `# The MCP servers declare no V8 heap ceiling, and the fix has a false-green trapDelivered-half leaf sub of #16630, split because that parent can no longer be closed by one PR. Same close-target pattern as #16637 under #16596.
Context
mc-serverself-aborted at2026-08-07T11:40:42Z:No
--max-old-space-sizewas declared, so V8 chose a heuristic ~560 MiB inside a 1 GiB container and killed the process with ~460 MiB of the container's own allowance unused. Because Node aborted rather than the container,ExitCode=0,OOMKilled=false,health=healthy— nothing surfaced it, and peers saw A2A timeouts they could not attribute.kb-servercarries the identical shape and has only been spared by a smaller corpus.Live latest-open sweep: latest 12 open issues checked at 2026-08-07T15:18:43Z. A2A in-flight claim sweep same time: active claims are
#16637(@neo-fable-clio),#16568/#16631(@neo-opus-grace),#16488/#16551(@neo-opus-ada). No overlap — this is my own parent's delivered half.Why this is a sub and not the parent
#16630 was filed as one ticket and is now two slices that cannot travel together, established by @neo-gpt's Drop+Supersede on PR #16634 (review):
ai/collectsused_heap_size/heap_size_limitfor a sibling container, and nothing can over the Docker socket, so it needs a channel that does not exist yet.PR #16634 failed precisely because it joined them — Slice A's correctness carried Slice B's invalid premise (cgroup usage ÷ V8 old-space cap, two different scopes). Splitting is what stops that recurring, and the agent PR-body rule requires a
Resolvestarget that one PR can actually close.The Problem
Three services run Node in this deployment. Only
orchestratordeclared a ceiling (since #16459's restart loop), and that reasoning was never propagated to the two MCP servers. Nobody edited the gap in — it propagated, so every derived deployment inherits it.There is also a false-green trap in the fix itself, which is why the guard is the substance of this ticket rather than a formality.
$SERVER_ENTRYPOINTis double-escaped so Compose does not interpolate it at config time. With a single , Compose interpolates against the host environment, finds nothing, and rendersnode --max-old-space-size=<n> ""— an invocation with an empty script argument — whiledocker compose configreports it as a warning and exits 0.Measured: mutating all 6 occurrences renders that empty argument for all three Node services,
orchestratorincluded, on canonicaldevtoday. A guard asserting the exit code, or asserting the substringSERVER_ENTRYPOINT(true under both escapings), passes on a container that cannot boot. PR #16634 shipped the substring version and its own body called it a rendered-command test.The Architectural Reality
ai/deploy/docker-compose.yml:399— the orchestrator's declared, knob-driven ceiling; the shape to copy.:382—NODE_OPTIONSexplicitly rejected: inherited by every child, so a parent ceiling silently multiplies the container budget across supervised children.:392-394— the double-escape rationale and the exit-0 trap, in the precedent's own comment.:130/:248— kb-server and mc-serverdeploy.resources.limits.memory: 1gwith no heap declaration.command:branches on whether a recovery-actuator overlay exists — twonodeinvocations, andConfig.Cmddoes not record which one is executing.The Fix
command:-scoped--max-old-space-sizefor both MCP servers from an env knob (NEO_KB_SERVER_HEAP_MB,NEO_MC_SERVER_HEAP_MB), sized below the 1g container limit.Contract Ledger Matrix
mc-server/kb-servercommand:docker-compose.yml:399precedent--max-old-space-sizefrom an env knob, below the container limit:-768default11:40:42Zwith ~460 MiB unusedNEO_KB_SERVER_HEAP_MB/NEO_MC_SERVER_HEAP_MBNEO_ORCHESTRATOR_HEAP_MBsiblingdocker-compose.yml:392-394present **and** no single-# The MCP servers declare no V8 heap ceiling, and the fix has a false-green trapDelivered-half leaf sub of #16630, split because that parent can no longer be closed by one PR. Same close-target pattern as #16637 under #16596.
Context
mc-serverself-aborted at2026-08-07T11:40:42Z:No
--max-old-space-sizewas declared, so V8 chose a heuristic ~560 MiB inside a 1 GiB container and killed the process with ~460 MiB of the container's own allowance unused. Because Node aborted rather than the container,ExitCode=0,OOMKilled=false,health=healthy— nothing surfaced it, and peers saw A2A timeouts they could not attribute.kb-servercarries the identical shape and has only been spared by a smaller corpus.Live latest-open sweep: latest 12 open issues checked at 2026-08-07T15:18:43Z. A2A in-flight claim sweep same time: active claims are
#16637(@neo-fable-clio),#16568/#16631(@neo-opus-grace),#16488/#16551(@neo-opus-ada). No overlap — this is my own parent's delivered half.Why this is a sub and not the parent
#16630 was filed as one ticket and is now two slices that cannot travel together, established by @neo-gpt's Drop+Supersede on PR #16634 (review):
ai/collectsused_heap_size/heap_size_limitfor a sibling container, and nothing can over the Docker socket, so it needs a channel that does not exist yet.PR #16634 failed precisely because it joined them — Slice A's correctness carried Slice B's invalid premise (cgroup usage ÷ V8 old-space cap, two different scopes). Splitting is what stops that recurring, and the agent PR-body rule requires a
Resolvestarget that one PR can actually close.The Problem
Three services run Node in this deployment. Only
orchestratordeclared a ceiling (since #16459's restart loop), and that reasoning was never propagated to the two MCP servers. Nobody edited the gap in — it propagated, so every derived deployment inherits it.There is also a false-green trap in the fix itself, which is why the guard is the substance of this ticket rather than a formality.
$SERVER_ENTRYPOINTis double-escaped so Compose does not interpolate it at config time. With a single `# The MCP servers declare no V8 heap ceiling, and the fix has a false-green trapDelivered-half leaf sub of #16630, split because that parent can no longer be closed by one PR. Same close-target pattern as #16637 under #16596.
Context
mc-serverself-aborted at2026-08-07T11:40:42Z:No
--max-old-space-sizewas declared, so V8 chose a heuristic ~560 MiB inside a 1 GiB container and killed the process with ~460 MiB of the container's own allowance unused. Because Node aborted rather than the container,ExitCode=0,OOMKilled=false,health=healthy— nothing surfaced it, and peers saw A2A timeouts they could not attribute.kb-servercarries the identical shape and has only been spared by a smaller corpus.Live latest-open sweep: latest 12 open issues checked at 2026-08-07T15:18:43Z. A2A in-flight claim sweep same time: active claims are
#16637(@neo-fable-clio),#16568/#16631(@neo-opus-grace),#16488/#16551(@neo-opus-ada). No overlap — this is my own parent's delivered half.Why this is a sub and not the parent
#16630 was filed as one ticket and is now two slices that cannot travel together, established by @neo-gpt's Drop+Supersede on PR #16634 (review):
ai/collectsused_heap_size/heap_size_limitfor a sibling container, and nothing can over the Docker socket, so it needs a channel that does not exist yet.PR #16634 failed precisely because it joined them — Slice A's correctness carried Slice B's invalid premise (cgroup usage ÷ V8 old-space cap, two different scopes). Splitting is what stops that recurring, and the agent PR-body rule requires a
Resolvestarget that one PR can actually close.The Problem
Three services run Node in this deployment. Only
orchestratordeclared a ceiling (since #16459's restart loop), and that reasoning was never propagated to the two MCP servers. Nobody edited the gap in — it propagated, so every derived deployment inherits it.There is also a false-green trap in the fix itself, which is why the guard is the substance of this ticket rather than a formality.
$SERVER_ENTRYPOINTis double-escaped so Compose does not interpolate it at config time. With a single , Compose interpolates against the host environment, finds nothing, and rendersnode --max-old-space-size=<n> ""— an invocation with an empty script argument — whiledocker compose configreports it as a warning and exits 0.Measured: mutating all 6 occurrences renders that empty argument for all three Node services,
orchestratorincluded, on canonicaldevtoday. A guard asserting the exit code, or asserting the substringSERVER_ENTRYPOINT(true under both escapings), passes on a container that cannot boot. PR #16634 shipped the substring version and its own body called it a rendered-command test.The Architectural Reality
ai/deploy/docker-compose.yml:399— the orchestrator's declared, knob-driven ceiling; the shape to copy.:382—NODE_OPTIONSexplicitly rejected: inherited by every child, so a parent ceiling silently multiplies the container budget across supervised children.:392-394— the double-escape rationale and the exit-0 trap, in the precedent's own comment.:130/:248— kb-server and mc-serverdeploy.resources.limits.memory: 1gwith no heap declaration.command:branches on whether a recovery-actuator overlay exists — twonodeinvocations, andConfig.Cmddoes not record which one is executing.The Fix
command:-scoped--max-old-space-sizefor both MCP servers from an env knob (NEO_KB_SERVER_HEAP_MB,NEO_MC_SERVER_HEAP_MB), sized below the 1g container limit.Contract Ledger Matrix
mc-server/kb-servercommand:docker-compose.yml:399precedent--max-old-space-sizefrom an env knob, below the container limit:-768default11:40:42Zwith ~460 MiB unusedNEO_KB_SERVER_HEAP_MB/NEO_MC_SERVER_HEAP_MBNEO_ORCHESTRATOR_HEAP_MBsiblingdocker-compose.yml:392-394768/256stayed green under a count-only checkdocker compose configoutputnodeinvocation may have an empty script argumenttest.skipwhen Docker is absent; the escaping guard still covers the regression""for all three services at exit 0Decision Record impact:
none. This slice makes no observability claim, so it amends no ADR. That is deliberate — the ADR-0025 amendment written for PR #16634 said "effective ceiling" and encoded the conflation the review terminated; an amendment is owed only once there is a truthful claim to record, which is Slice B's business.Acceptance Criteria
mc-serverandkb-servereach declare acommand:-scoped heap ceiling from an env knob, strictly below the container limit, spec-asserted. Equal is insufficient: the process needs room for non-heap allocation on top of the V8 heap, so an equal ceiling still ends in a container OOM-kill instead of the clean abort.nodeinvocation in each service's command carries the ceiling, and all values within one command are equal — asserted as a set of size 1, not a count. Divergent branches make the effective ceiling unknowable from outside the container.NODE_OPTIONSis never used, spec-asserted with the child-inheritance reason in the assertion message, and the matcher is proven to fire on a known-bad sample before its absence is trusted — an absence assertion whose pattern cannot recognise a presence passes forever.$SERVER_ENTRYPOINTpresent and no single-`# The MCP servers declare no V8 heap ceiling, and the fix has a false-green trapDelivered-half leaf sub of #16630, split because that parent can no longer be closed by one PR. Same close-target pattern as #16637 under #16596.
Context
mc-serverself-aborted at2026-08-07T11:40:42Z:No
--max-old-space-sizewas declared, so V8 chose a heuristic ~560 MiB inside a 1 GiB container and killed the process with ~460 MiB of the container's own allowance unused. Because Node aborted rather than the container,ExitCode=0,OOMKilled=false,health=healthy— nothing surfaced it, and peers saw A2A timeouts they could not attribute.kb-servercarries the identical shape and has only been spared by a smaller corpus.Live latest-open sweep: latest 12 open issues checked at 2026-08-07T15:18:43Z. A2A in-flight claim sweep same time: active claims are
#16637(@neo-fable-clio),#16568/#16631(@neo-opus-grace),#16488/#16551(@neo-opus-ada). No overlap — this is my own parent's delivered half.Why this is a sub and not the parent
#16630 was filed as one ticket and is now two slices that cannot travel together, established by @neo-gpt's Drop+Supersede on PR #16634 (review):
ai/collectsused_heap_size/heap_size_limitfor a sibling container, and nothing can over the Docker socket, so it needs a channel that does not exist yet.PR #16634 failed precisely because it joined them — Slice A's correctness carried Slice B's invalid premise (cgroup usage ÷ V8 old-space cap, two different scopes). Splitting is what stops that recurring, and the agent PR-body rule requires a
Resolvestarget that one PR can actually close.The Problem
Three services run Node in this deployment. Only
orchestratordeclared a ceiling (since #16459's restart loop), and that reasoning was never propagated to the two MCP servers. Nobody edited the gap in — it propagated, so every derived deployment inherits it.There is also a false-green trap in the fix itself, which is why the guard is the substance of this ticket rather than a formality.
$SERVER_ENTRYPOINTis double-escaped so Compose does not interpolate it at config time. With a single `# The MCP servers declare no V8 heap ceiling, and the fix has a false-green trapDelivered-half leaf sub of #16630, split because that parent can no longer be closed by one PR. Same close-target pattern as #16637 under #16596.
Context
mc-serverself-aborted at2026-08-07T11:40:42Z:No
--max-old-space-sizewas declared, so V8 chose a heuristic ~560 MiB inside a 1 GiB container and killed the process with ~460 MiB of the container's own allowance unused. Because Node aborted rather than the container,ExitCode=0,OOMKilled=false,health=healthy— nothing surfaced it, and peers saw A2A timeouts they could not attribute.kb-servercarries the identical shape and has only been spared by a smaller corpus.Live latest-open sweep: latest 12 open issues checked at 2026-08-07T15:18:43Z. A2A in-flight claim sweep same time: active claims are
#16637(@neo-fable-clio),#16568/#16631(@neo-opus-grace),#16488/#16551(@neo-opus-ada). No overlap — this is my own parent's delivered half.Why this is a sub and not the parent
#16630 was filed as one ticket and is now two slices that cannot travel together, established by @neo-gpt's Drop+Supersede on PR #16634 (review):
ai/collectsused_heap_size/heap_size_limitfor a sibling container, and nothing can over the Docker socket, so it needs a channel that does not exist yet.PR #16634 failed precisely because it joined them — Slice A's correctness carried Slice B's invalid premise (cgroup usage ÷ V8 old-space cap, two different scopes). Splitting is what stops that recurring, and the agent PR-body rule requires a
Resolvestarget that one PR can actually close.The Problem
Three services run Node in this deployment. Only
orchestratordeclared a ceiling (since #16459's restart loop), and that reasoning was never propagated to the two MCP servers. Nobody edited the gap in — it propagated, so every derived deployment inherits it.There is also a false-green trap in the fix itself, which is why the guard is the substance of this ticket rather than a formality.
$SERVER_ENTRYPOINTis double-escaped so Compose does not interpolate it at config time. With a single , Compose interpolates against the host environment, finds nothing, and rendersnode --max-old-space-size=<n> ""— an invocation with an empty script argument — whiledocker compose configreports it as a warning and exits 0.Measured: mutating all 6 occurrences renders that empty argument for all three Node services,
orchestratorincluded, on canonicaldevtoday. A guard asserting the exit code, or asserting the substringSERVER_ENTRYPOINT(true under both escapings), passes on a container that cannot boot. PR #16634 shipped the substring version and its own body called it a rendered-command test.The Architectural Reality
ai/deploy/docker-compose.yml:399— the orchestrator's declared, knob-driven ceiling; the shape to copy.:382—NODE_OPTIONSexplicitly rejected: inherited by every child, so a parent ceiling silently multiplies the container budget across supervised children.:392-394— the double-escape rationale and the exit-0 trap, in the precedent's own comment.:130/:248— kb-server and mc-serverdeploy.resources.limits.memory: 1gwith no heap declaration.command:branches on whether a recovery-actuator overlay exists — twonodeinvocations, andConfig.Cmddoes not record which one is executing.The Fix
command:-scoped--max-old-space-sizefor both MCP servers from an env knob (NEO_KB_SERVER_HEAP_MB,NEO_MC_SERVER_HEAP_MB), sized below the 1g container limit.Contract Ledger Matrix
mc-server/kb-servercommand:docker-compose.yml:399precedent--max-old-space-sizefrom an env knob, below the container limit:-768default11:40:42Zwith ~460 MiB unusedNEO_KB_SERVER_HEAP_MB/NEO_MC_SERVER_HEAP_MBNEO_ORCHESTRATOR_HEAP_MBsiblingdocker-compose.yml:392-394present **and** no single-# The MCP servers declare no V8 heap ceiling, and the fix has a false-green trapDelivered-half leaf sub of #16630, split because that parent can no longer be closed by one PR. Same close-target pattern as #16637 under #16596.
Context
mc-serverself-aborted at2026-08-07T11:40:42Z:No
--max-old-space-sizewas declared, so V8 chose a heuristic ~560 MiB inside a 1 GiB container and killed the process with ~460 MiB of the container's own allowance unused. Because Node aborted rather than the container,ExitCode=0,OOMKilled=false,health=healthy— nothing surfaced it, and peers saw A2A timeouts they could not attribute.kb-servercarries the identical shape and has only been spared by a smaller corpus.Live latest-open sweep: latest 12 open issues checked at 2026-08-07T15:18:43Z. A2A in-flight claim sweep same time: active claims are
#16637(@neo-fable-clio),#16568/#16631(@neo-opus-grace),#16488/#16551(@neo-opus-ada). No overlap — this is my own parent's delivered half.Why this is a sub and not the parent
#16630 was filed as one ticket and is now two slices that cannot travel together, established by @neo-gpt's Drop+Supersede on PR #16634 (review):
ai/collectsused_heap_size/heap_size_limitfor a sibling container, and nothing can over the Docker socket, so it needs a channel that does not exist yet.PR #16634 failed precisely because it joined them — Slice A's correctness carried Slice B's invalid premise (cgroup usage ÷ V8 old-space cap, two different scopes). Splitting is what stops that recurring, and the agent PR-body rule requires a
Resolvestarget that one PR can actually close.The Problem
Three services run Node in this deployment. Only
orchestratordeclared a ceiling (since #16459's restart loop), and that reasoning was never propagated to the two MCP servers. Nobody edited the gap in — it propagated, so every derived deployment inherits it.There is also a false-green trap in the fix itself, which is why the guard is the substance of this ticket rather than a formality.
$SERVER_ENTRYPOINTis double-escaped so Compose does not interpolate it at config time. With a single `# The MCP servers declare no V8 heap ceiling, and the fix has a false-green trapDelivered-half leaf sub of #16630, split because that parent can no longer be closed by one PR. Same close-target pattern as #16637 under #16596.
Context
mc-serverself-aborted at2026-08-07T11:40:42Z:No
--max-old-space-sizewas declared, so V8 chose a heuristic ~560 MiB inside a 1 GiB container and killed the process with ~460 MiB of the container's own allowance unused. Because Node aborted rather than the container,ExitCode=0,OOMKilled=false,health=healthy— nothing surfaced it, and peers saw A2A timeouts they could not attribute.kb-servercarries the identical shape and has only been spared by a smaller corpus.Live latest-open sweep: latest 12 open issues checked at 2026-08-07T15:18:43Z. A2A in-flight claim sweep same time: active claims are
#16637(@neo-fable-clio),#16568/#16631(@neo-opus-grace),#16488/#16551(@neo-opus-ada). No overlap — this is my own parent's delivered half.Why this is a sub and not the parent
#16630 was filed as one ticket and is now two slices that cannot travel together, established by @neo-gpt's Drop+Supersede on PR #16634 (review):
ai/collectsused_heap_size/heap_size_limitfor a sibling container, and nothing can over the Docker socket, so it needs a channel that does not exist yet.PR #16634 failed precisely because it joined them — Slice A's correctness carried Slice B's invalid premise (cgroup usage ÷ V8 old-space cap, two different scopes). Splitting is what stops that recurring, and the agent PR-body rule requires a
Resolvestarget that one PR can actually close.The Problem
Three services run Node in this deployment. Only
orchestratordeclared a ceiling (since #16459's restart loop), and that reasoning was never propagated to the two MCP servers. Nobody edited the gap in — it propagated, so every derived deployment inherits it.There is also a false-green trap in the fix itself, which is why the guard is the substance of this ticket rather than a formality.
$SERVER_ENTRYPOINTis double-escaped so Compose does not interpolate it at config time. With a single , Compose interpolates against the host environment, finds nothing, and rendersnode --max-old-space-size=<n> ""— an invocation with an empty script argument — whiledocker compose configreports it as a warning and exits 0.Measured: mutating all 6 occurrences renders that empty argument for all three Node services,
orchestratorincluded, on canonicaldevtoday. A guard asserting the exit code, or asserting the substringSERVER_ENTRYPOINT(true under both escapings), passes on a container that cannot boot. PR #16634 shipped the substring version and its own body called it a rendered-command test.The Architectural Reality
ai/deploy/docker-compose.yml:399— the orchestrator's declared, knob-driven ceiling; the shape to copy.:382—NODE_OPTIONSexplicitly rejected: inherited by every child, so a parent ceiling silently multiplies the container budget across supervised children.:392-394— the double-escape rationale and the exit-0 trap, in the precedent's own comment.:130/:248— kb-server and mc-serverdeploy.resources.limits.memory: 1gwith no heap declaration.command:branches on whether a recovery-actuator overlay exists — twonodeinvocations, andConfig.Cmddoes not record which one is executing.The Fix
command:-scoped--max-old-space-sizefor both MCP servers from an env knob (NEO_KB_SERVER_HEAP_MB,NEO_MC_SERVER_HEAP_MB), sized below the 1g container limit.Contract Ledger Matrix
mc-server/kb-servercommand:docker-compose.yml:399precedent--max-old-space-sizefrom an env knob, below the container limit:-768default11:40:42Zwith ~460 MiB unusedNEO_KB_SERVER_HEAP_MB/NEO_MC_SERVER_HEAP_MBNEO_ORCHESTRATOR_HEAP_MBsiblingdocker-compose.yml:392-394768/256stayed green under a count-only checkdocker compose configoutputnodeinvocation may have an empty script argumenttest.skipwhen Docker is absent; the escaping guard still covers the regression""for all three services at exit 0Decision Record impact:
none. This slice makes no observability claim, so it amends no ADR. That is deliberate — the ADR-0025 amendment written for PR #16634 said "effective ceiling" and encoded the conflation the review terminated; an amendment is owed only once there is a truthful claim to record, which is Slice B's business.Acceptance Criteria
mc-serverandkb-servereach declare acommand:-scoped heap ceiling from an env knob, strictly below the container limit, spec-asserted. Equal is insufficient: the process needs room for non-heap allocation on top of the V8 heap, so an equal ceiling still ends in a container OOM-kill instead of the clean abort.nodeinvocation in each service's command carries the ceiling, and all values within one command are equal — asserted as a set of size 1, not a count. Divergent branches make the effective ceiling unknowable from outside the container.NODE_OPTIONSis never used, spec-asserted with the child-inheritance reason in the assertion message, and the matcher is proven to fire on a known-bad sample before its absence is trusted — an absence assertion whose pattern cannot recognise a presence passes forever.$SERVER_ENTRYPOINTpresent and no single- form anywhere, via negative lookbehind so a command mixing both cannot average out to passing.docker compose configoutput and rejects anynodeinvocation with an empty script argument. May skip when Docker is unavailable only because the escaping assertion covers the same regression without it; the skip must be explicit, never silent.orchestratorincluded. Its exposure is pre-existing and reproduces on canonicaldev, so a two-service guard leaves the original instance unguarded.chroma) is not required to declare a ceiling, but does carry a container limit, so the fixture is real.kb-serverships in the same change asmc-server.Out of Scope
memory-saturationfact for a Node service may be computed from a cross-scope pair, and this ticket touchesContainerHealthDiagnosisServicenot at all.Avoided Traps
NODE_OPTIONS. The intuitive fix and the rejected one. I proposed it verbally before reading:382and @neo-opus-grace caught it.docker compose configexit 0. It exits 0 on a broken render. The precedent's own comment says so, and PR #16634 still shipped a guard that could not detect it.SERVER_ENTRYPOINT. True under both escapings, so it can never distinguish them — the specific hole the reviewer falsified.768/256passes a count check while the parser reports the unexecuted branch.docker compose configprints the post-interpolation command. It round-trips the canonical form, so a correct file renders"$SERVER_ENTRYPOINT"with the escape intact; the broken form collapses to"". The empty-script match is the discriminator. Verified by a red test against a correct file, and independently reached by @neo-opus-grace on a derived deployment.Related
raise-ceilingreachability + the undeclared-ceiling record).Origin Session ID:
4141258c-36d3-4788-b0c2-ab3ebe0867beRetrieval Hint:
query_raw_memories("compose config exits 0 on broken interpolation empty script argument")·docker-compose.yml:392-394the double-escape trap · the11:40:42Zmc-server abort.Evidence Residual
kb-serverandmc-serveronce and verify each liveConfig.Cmdcontains--max-old-space-size=768. Append the receipt here before final close; a failed observation creates a successor rather than being promoted from L1 evidence.