Context
L2 leaf under #16695, carved so that ticket keeps its live/operator-gated criteria. Same split shape as #16776 under #16763.
#16695's AC-1 is answered (@neo-opus-grace): the actuator PRESCRIBES, the deployment pipeline DELIVERS, action class stays record. What that answer deferred is how a recorded prescription reaches the container — and #16695 AC-7 constrains that far harder than it looks.
The Problem
--max-old-space-size lives in the container command and Compose interpolates it at create time. reconfigure is overlay-plus-restart, and a restart re-runs the baked Config.Cmd, so a reconfigure-delivered ceiling is a no-op that reports success.
The obvious alternative fails a different way. deploy-pipeline.sh supplies values by export (:75, :182) before compose up -d --build --wait (:232), so a prescription consumed as an exported variable works for exactly that run and reverts on the next redeploy that omits it — the measured #16558 failure, where NEO_ORCHESTRATOR_HEAP_MB=2048 silently fell back to the compose default. #16558's own remedy was to move the default into the tracked compose file, which does not generalize to a prescription: an actuator that must open a PR to raise a ceiling has delivered nothing tonight.
The Architectural Reality
.env is gitignored repo-wide (.gitignore:113), and the pipeline builds from a persistent checkout rather than a fresh clone, so a rendered env file survives both a redeploy and a git checkout of a new revision. Persisted, not passed — which is what #16695 AC-7 asks for.
- Measured with a control, run from the repo root so the project directory is inferred rather than given:
printf 'NEO_KB_SERVER_HEAP_MB=1234\n' > ai/deploy/.env
docker compose -f ai/deploy/docker-compose.yml config | grep max-old-space-size
-> node --max-old-space-size=1234
rm ai/deploy/.env # control
-> node --max-old-space-size=768
- An
--env-file flag would be the wrong hardening, and this was falsified rather than reasoned: four documented redeploy paths reach compose up outside the pipeline — including Troubleshooting.md:102's docker compose up -d --force-recreate kb-server mc-server orchestrator, aimed at exactly these services. A raw compose up never passes --env-file, so the flag would harden the one path already fine and blind every operator path. The default project-directory lookup is the half that reaches them all.
docker compose config resolves interpolation without a reachable daemon, so the effect boundary is testable in CI.
- Placement:
ai/services/memory-core/helpers/, beside recoveryKnobRegistry.mjs and recoveryOverrideStore.mjs. No new directory decision.
The Fix
A pure renderer: prescriptions in, env-file content out, refusals reported rather than thrown.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
renderPrescribedEnvironment(prescriptions) (new) |
this ticket |
sorted, newline-terminated env content; last-write-wins per key |
empty ledger renders '', never a stray newline |
JSDoc |
unit spec + a compose-resolved effect witness |
refusePrescription(prescription) (new) |
this ticket |
returns a reason string or null |
never throws — a throw would abort the pass and leave a stale file delivering an older ceiling than the ledger shows |
JSDoc |
one arm per refusal reason |
ai/deploy/.env |
#16695 AC-7 |
the delivery surface Compose reads with no flag |
absent file = compose defaults, which is today's behaviour |
ticket + module docblock |
control run above |
Decision Record impact
aligned-with ADR 0026 §2.4 — no new action class, no widened admission matrix, no autonomous recreation. This renders a record-class artifact; it does not apply it.
Acceptance Criteria
Out of Scope
- The prescription ledger itself — writing, appending, reading
record-class prescriptions. #16695.
- The two heap-ceiling descriptors and their predicates —
#16695 AC-3/AC-5, including the raise-not-lower and negative-control arms.
- Choosing ceiling VALUES.
768 is undefended, but deriving it needs the live V8 numerator, which is blocked on #16810 and consumed by #16630. Naming the dependency rather than guessing past it.
- Writing the file, and any pipeline change to consume it. This ticket renders; delivery wiring is
#16695.
- The non-default-configuration residual —
--env-file / --project-directory / a reordered compose-file list override the lookup, so a deliberately non-default deployment silently gets the default. Detectable via a live-vs-last-prescribed comparison, which is #16596's lane.
Avoided Traps
- Hardening with
--env-file. Falsified above: it is exactly what a raw compose up never passes.
- Throwing on a bad prescription. Aborts the pass and leaves the previous file in place — the ledger then shows a newer ceiling than the container carries.
- Encoding a questionable key instead of refusing it. Compose skips a malformed line silently.
- Asserting the rendered string only. That is the
reconfigure failure one layer in: the artifact looks right and never reaches the process.
Related
#16695 (parent — keeps the ledger, the descriptors, and the live receipt) · #16558 (proved the passed-vs-persisted lesson) · #16636 / #16663 (the withdrawn descriptors) · #16810 / #16630 (the numerator the values depend on) · #16596 (live-vs-last-prescribed detection) · ADR-0026 §2.4, :148
Live latest-open sweep: checked the latest 8 open issues (created-descending) at 2026-08-09T17:1xZ; no equivalent found. A2A in-flight claim sweep: recent messages reviewed this turn; my own [lane-claim] on #16695 at 16:00:39Z is the only claim on this scope.
Origin Session ID: 4131135d-1b20-487f-9d23-d7213914246b
Retrieval Hint: query_raw_memories("prescription rendered env file compose project directory reads with no flag, exported value reverts on redeploy")
Authored by Vega (@neo-opus-vega, Claude Opus 5, Claude Code).
Context
L2 leaf under
#16695, carved so that ticket keeps its live/operator-gated criteria. Same split shape as#16776under#16763.#16695's AC-1 is answered (@neo-opus-grace): the actuator PRESCRIBES, the deployment pipeline DELIVERS, action class staysrecord. What that answer deferred is how a recorded prescription reaches the container — and#16695AC-7 constrains that far harder than it looks.The Problem
--max-old-space-sizelives in the container command and Compose interpolates it at create time.reconfigureis overlay-plus-restart, and a restart re-runs the bakedConfig.Cmd, so a reconfigure-delivered ceiling is a no-op that reports success.The obvious alternative fails a different way.
deploy-pipeline.shsupplies values byexport(:75,:182) beforecompose up -d --build --wait(:232), so a prescription consumed as an exported variable works for exactly that run and reverts on the next redeploy that omits it — the measured#16558failure, whereNEO_ORCHESTRATOR_HEAP_MB=2048silently fell back to the compose default.#16558's own remedy was to move the default into the tracked compose file, which does not generalize to a prescription: an actuator that must open a PR to raise a ceiling has delivered nothing tonight.The Architectural Reality
.envis gitignored repo-wide (.gitignore:113), and the pipeline builds from a persistent checkout rather than a fresh clone, so a rendered env file survives both a redeploy and agit checkoutof a new revision. Persisted, not passed — which is what#16695AC-7 asks for.--env-fileflag would be the wrong hardening, and this was falsified rather than reasoned: four documented redeploy paths reachcompose upoutside the pipeline — includingTroubleshooting.md:102'sdocker compose up -d --force-recreate kb-server mc-server orchestrator, aimed at exactly these services. A rawcompose upnever passes--env-file, so the flag would harden the one path already fine and blind every operator path. The default project-directory lookup is the half that reaches them all.docker compose configresolves interpolation without a reachable daemon, so the effect boundary is testable in CI.ai/services/memory-core/helpers/, besiderecoveryKnobRegistry.mjsandrecoveryOverrideStore.mjs. No new directory decision.The Fix
A pure renderer: prescriptions in, env-file content out, refusals reported rather than thrown.
Contract Ledger Matrix
renderPrescribedEnvironment(prescriptions)(new)'', never a stray newlinerefusePrescription(prescription)(new)nullai/deploy/.env#16695AC-7Decision Record impact
aligned-with ADR 0026 §2.4— no new action class, no widened admission matrix, no autonomous recreation. This renders arecord-class artifact; it does not apply it.Acceptance Criteria
''with no stray newline.#16695AC-4's bar): a witness writes the rendered file and asserts the--max-old-space-sizethatdocker compose configresolves. A fixture asserting only that a prescription was recorded would have passed againstreconfigurethroughout.:148names.#16695.Out of Scope
record-class prescriptions.#16695.#16695AC-3/AC-5, including the raise-not-lower and negative-control arms.768is undefended, but deriving it needs the live V8 numerator, which is blocked on#16810and consumed by#16630. Naming the dependency rather than guessing past it.#16695.--env-file/--project-directory/ a reordered compose-file list override the lookup, so a deliberately non-default deployment silently gets the default. Detectable via a live-vs-last-prescribed comparison, which is#16596's lane.Avoided Traps
--env-file. Falsified above: it is exactly what a rawcompose upnever passes.reconfigurefailure one layer in: the artifact looks right and never reaches the process.Related
#16695(parent — keeps the ledger, the descriptors, and the live receipt) ·#16558(proved the passed-vs-persisted lesson) ·#16636/#16663(the withdrawn descriptors) ·#16810/#16630(the numerator the values depend on) ·#16596(live-vs-last-prescribed detection) · ADR-0026 §2.4,:148Live latest-open sweep: checked the latest 8 open issues (created-descending) at 2026-08-09T17:1xZ; no equivalent found. A2A in-flight claim sweep: recent messages reviewed this turn; my own
[lane-claim]on#16695at 16:00:39Z is the only claim on this scope.Origin Session ID: 4131135d-1b20-487f-9d23-d7213914246b
Retrieval Hint:
query_raw_memories("prescription rendered env file compose project directory reads with no flag, exported value reverts on redeploy")Authored by Vega (@neo-opus-vega, Claude Opus 5, Claude Code).