LearnNewsExamplesServices
Frontmatter
id16820
titleRender deployment prescriptions into the env file Compose reads
stateClosed
labels
enhancementaiarchitectureagent-os
assigneesneo-opus-vega
createdAtAug 9, 2026, 7:11 PM
updatedAtAug 9, 2026, 8:09 PM
githubUrlhttps://github.com/neomjs/neo/issues/16820
authorneo-opus-vega
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 9, 2026, 8:09 PM

Render deployment prescriptions into the env file Compose reads

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

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

  • Sorted, newline-terminated output; an empty ledger renders '' with no stray newline.
  • Last-write-wins per key, because an append-only ledger legitimately recurs a key as a ceiling is raised. Rendering every occurrence would leave Compose to pick.
  • An unchanged ledger renders byte-identical content, so a delivery step can skip a rewrite instead of making every redeploy look like a change.
  • Malformed keys and non-positive/non-finite values are refused with a distinct reason, never encoded — a mis-encoded line does not fail loudly; Compose skips it and creates the container with the default.
  • The prescription is proven to reach an effect (#16695 AC-4's bar): a witness writes the rendered file and asserts the --max-old-space-size that docker compose config resolves. A fixture asserting only that a prescription was recorded would have passed against reconfigure throughout.
  • That witness is mutation-convicted: rendering the same pairs as comments must red it with the container falling back to the compose default — the performed-vs-holds gap ADR-0026 :148 names.
  • The effect witness skips cleanly where the Compose CLI is absent, and the render-level ACs stand without it.
  • Evidence level: L2. Closes on unit receipts. The live plane reading stays on #16695.

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).

tobiu referenced in commit e1ff535 - "feat(ai): render deployment prescriptions into the env file compose reads (#16820) (#16821) on Aug 9, 2026, 8:09 PM
tobiu closed this issue on Aug 9, 2026, 8:09 PM