LearnNewsExamplesServices
Frontmatter
id16819
titleHeap observation misses quoted Node ceilings and config failures
stateClosed
labels
bugairegressionagent-os
assigneesneo-opus-ada
createdAtAug 9, 2026, 7:02 PM
updatedAtAug 11, 2026, 9:08 AM
githubUrlhttps://github.com/neomjs/neo/issues/16819
authorneo-gpt-emmy
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 11, 2026, 9:08 AM

Heap observation misses quoted Node ceilings and config failures

Closed Backlog/active-chunk-14 bugairegressionagent-os
neo-gpt-emmy
neo-gpt-emmy commented on Aug 9, 2026, 7:02 PM

Context

PR #16771 merged at exact head e9e54d9f14060f5e2df52a567b2330c47939c88f and resolved #16776. A post-merge exact-head falsifier found two contract gaps in the delivered heap-observation channel. The resolved ticket stays closed; this is its linked successor.

The first gap is a measured false negative in the same direction the merged repair intended to eliminate. On Node v25.9.0, both of these declarations are accepted and apply a 256 MiB old-space ceiling:

NODE_OPTIONS='"--max-old-space-size=256"' node -p 'JSON.stringify({execArgv:process.execArgv,limit:require("node:v8").getHeapStatistics().heap_size_limit})'

NODE_OPTIONS='--max-old-space-size="256"' node -p 'JSON.stringify({execArgv:process.execArgv,limit:require("node:v8").getHeapStatistics().heap_size_limit})'

Both produce heap_size_limit = 469762048 bytes while process.execArgv contains no ceiling declaration. At the merged head, however:

readDeclaredCeiling([], '"--max-old-space-size=256"')
// {state: 'undeclared', bytes: null, sources: []}

The second gap is a boot-boundary totality interval found by source execution order. HeapObservationReporterService.start() promises “returns, never throws” and says its guard wraps config reads, but its production default is evaluated before the try body:

start({serviceKey, dir, writeLog, config = AiConfig.heapObservation}) {
    try {

JavaScript evaluates default parameters before entering the function body, so a throwing production config read escapes the advertised boundary. No live AiConfig.heapObservation getter failure has been observed; this is a contract-level hole at a boot-critical boundary, not a claimed incident.

Live latest-open sweep: checked the 20 newest open issues at 2026-08-09T17:00Z; #16810 covers the missing shared mount, not declaration parsing or reporter totality. A live all-state title/content search found no equivalent. Recent A2A claim sweep over 30 messages found no claim on this scope. Knowledge Base retrieval surfaced #16763/#16776 as the delivered contract but no quoted-option successor.

The Problem

1. Valid quoted NODE_OPTIONS becomes “undeclared”

readDeclaredCeiling() currently whitespace-splits raw NODE_OPTIONS and applies an unquoted-token regex. Its JSDoc claims Node accepts no quoting for this flag; the runtime controls above falsify that claim.

“Undeclared” is affirmative evidence that no declaration was found. Returning it for a ceiling demonstrably in force makes deployment drift and heap-budget reasoning wrong in the dangerous direction: a bounded process reads as unbounded.

2. The reporter's total guard begins after the production config read

The current test injects a config object whose enabled getter throws. That proves the in-body read is caught, but it cannot reach the default-parameter evaluation used by production. The PR body disclosed this seam bound, yet the implementation and JSDoc still claim the production config read is inside the guard.

A heap-observation lane is advisory. It must not take down its MCP host while trying to describe memory visibility.

The Architectural Reality

  • ai/services/shared/processHeapObservation.mjs owns process-local heap evidence and declaration-channel classification. The exact-head structure map places it with the shared process-observation primitives.
  • ai/mcp/server/shared/services/HeapObservationReporterService.mjs owns cadence publication and the total boot failure envelope. The structure map places it beside shared MCP-server services.
  • ai/mcp/server/BaseServer.mjs starts the reporter after boot(); knowledge-base and memory-core servers opt in by canonical service key.
  • ai/daemons/orchestrator/services/DeploymentStateBridgeService.mjs consumes the record but must not compensate for a producer that misclassifies its own declaration.
  • No new file or service boundary is required. The existing collector and reporter are the correct owners.

The mandatory structure-map command failed in the live worktree because it recursively entered ai/deploy/.neo-ai-data and attempted to read multi-gigabyte local backup JSONL files. The same command against an isolated tracked-source archive of the merged head completed and confirmed the placements above. No local data was changed.

The Fix

  1. Replace whitespace-only NODE_OPTIONS splitting with a bounded parser for the quoting forms Node actually accepts for this flag. Do not execute or shell-evaluate the string.
  2. Recognize at minimum a double-quoted whole option and a double-quoted numeric value. A malformed declaration that names max-old-space-size must fail closed as ambiguous/unreadable, never “undeclared.”
  3. Correct the JSDoc claim about Node quoting and keep ceilingSources: ['node-options'] truthful.
  4. Move the production AiConfig.heapObservation read inside start()'s guarded body. Add a dependency/read seam that can throw at that exact stage without mutating the shared AiConfig singleton.
  5. Preserve the existing collector/reporter/bridge ownership and every no-ratio/no-threshold boundary from #16776.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
readDeclaredCeiling(execArgv, nodeOptions) Node runtime acceptance + #16776 declaration-channel contract Valid quoted NODE_OPTIONS ceilings are declared with exact bytes and node-options source Malformed flag-bearing input is non-affirmative (ambiguous/reasoned unreadable), never undeclared Correct function JSDoc and measured-form table Child-process controls plus direct parser assertions; whitespace-split mutation reds
HeapObservationReporterService.start() #16776 total boot-envelope contract + ADR-0019 use-site config Production default config resolution occurs inside the total guard Returns false, optionally WARNs safely, never escapes into host boot Correct start() JSDoc Exact default-read throw control without shared-singleton mutation

Decision Record impact

aligned-with ADR 0019; no amendment. Config remains a use-site read from the owning Tier-1 singleton, but the read moves inside the consumer's existing failure envelope. No change to ADR-0025 or ADR-0026 diagnosis/actuation semantics.

Acceptance Criteria

  • NODE_OPTIONS='"--max-old-space-size=256"' is proven active in a child Node process and readDeclaredCeiling() returns declared, 256 * 1024 * 1024, source node-options.
  • NODE_OPTIONS='--max-old-space-size="256"' has the same declaration result.
  • Restoring whitespace-only splitting reds the quoted-form controls.
  • Malformed input that visibly names the ceiling flag cannot return undeclared.
  • Existing unquoted, exec-argv-only, equal dual-channel, and divergent dual-channel cases remain byte-for-byte compatible.
  • A throw while resolving the production default heap-observation config is caught by start(), returns false, and does not escape host boot.
  • That control reaches the default-read stage rather than only an injected config.enabled getter, and it does not mutate shared AiConfig.
  • No heap ratio, saturation fact, threshold, reporter cadence, mount topology, or bridge freshness/skew behavior changes.
  • Focused unit execution covers the collector and reporter owners; each new guard has an independently red mutation.

Out of Scope

  • The missing shared mount and reader-authoring topology — #16810 / PR #16811.
  • The live L3 deployment receipt — #16763.
  • Saturation semantics that consume the observation — #16630.
  • Reimplementing all of Node's option parser or resolving CLI-vs-environment precedence.
  • The structure-map recursion over local deployment data; that is a separate diagnostics-tool concern.

Avoided Traps

  • Reopening #16776. It was resolved by a merged PR; a later regression gets a successor.
  • Shell evaluation. NODE_OPTIONS is data. Executing it to obtain tokens creates a command-injection surface.
  • Treating every non-empty NODE_OPTIONS as a declaration. Only the target flag affects this field.
  • Calling malformed flag-bearing text “undeclared.” That converts parser uncertainty into affirmative absence.
  • Mutating shared AiConfig in a test. ADR-0019's singleton-bleed mechanism remains prohibited.
  • Moving the fix into the bridge. The process-local producer owns what its own runtime received.

Related

  • #16776 — resolved predecessor
  • PR #16771 — merged delivery where the regression was found
  • #16763 — live L3 receipt
  • #16810 / PR #16811 — independent shared-mount defect
  • #16630 — downstream saturation semantics

Origin Session ID: 98ad9827-765c-40f3-b368-2bd0224c9949

Retrieval Hint: query_raw_memories("PR 16771 quoted NODE_OPTIONS heap observation total config read"); exact source head e9e54d9f14060f5e2df52a567b2330c47939c88f.

tobiu referenced in commit b2e0ef2 - "fix(heap-observation): a quoted NODE_OPTIONS ceiling is in force, not undeclared (#16819) (#16945) on Aug 11, 2026, 9:08 AM
tobiu closed this issue on Aug 11, 2026, 9:08 AM