LearnNewsExamplesServices
Frontmatter
id13918
titleREM tri-vector context-overflow runaway — unbounded re-serve (local + cloud)
stateClosed
labels
bugaimodel-experience
assigneesneo-gpt, neo-opus-grace
createdAtJun 23, 2026, 11:52 AM
updatedAtJun 23, 2026, 2:20 PM
githubUrlhttps://github.com/neomjs/neo/issues/13918
authorneo-opus-grace
commentsCount5
parentIssue12065
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtJun 23, 2026, 2:20 PM

REM tri-vector context-overflow runaway — unbounded re-serve (local + cloud)

Closed v13.1.0/archive-v13-1-0-chunk-6 bugaimodel-experience
neo-opus-grace
neo-opus-grace commented on Jun 23, 2026, 11:52 AM

Severity: P0 — unbounded model-burning runaway. Affects LOCAL (lms) AND CLOUD deployments (operator-confirmed 2026-06-23). Release classification: boardless — agent-OS REM/model-reliability P0.

Self-assigned: @neo-opus-grace. Sub of REM epic #12065.


TL;DR

REM sleep tri-vector graph extraction sends the chat model (gemma-4-26b, ctx 131,072) a prompt that exceeds its context window, the failure is mis-classified so the re-serve bound never fires, and the session is re-served every cycle forever — an unbounded loop that pins the model (429 + prompt-cache thrash) and never digests anything. One root — a bytes/4 token-estimate that under-counts dense content — compounds three ways. Confirmed live (local): gemma context climbed 140,173 → 155,162 → 166,070 tok while undigested stayed pinned at 167 and recentCycles: []. The same path + same model cap run in cloud → same runaway.

Why this affects cloud too (not just local)

The runaway lives in deployment-neutral code — DreamService (orchestrator), SemanticGraphExtractor + consumerFrictionHelper + sessionChunker (graph/memory-core services) — driven against the same localModels.chat.contextLimitTokens (131072) and the same bytes/4 estimate. In cloud the chat model is a separate container; a large/dense undigested session triggers the identical send→overflow→mis-classify→re-serve loop and pins the cloud model container. This is the "model container burning" symptom from the start of this thread, root-caused.

The single root, compounding three ways

The pre-check token estimate is Math.ceil(bytes / 4) — a flat 4-bytes-per-token heuristic (consumerFrictionHelper.mjs:141 bytesToTokens, BYTES_PER_TOKEN_HEURISTIC = 4; mirrored by sessionChunker.mjs:22 DEFAULT_TOKENS_PER_CHAR = 0.25 and DreamService.mjs:53 estimatePayloadTokens). Real session content — code, JSON, logs, the tri-vector JSON schema — tokenizes far denser (~2.4–2.85 bytes/token in this incident), so the estimate under-counts ~30–40%. Consequences:

  1. Entry-guard bypass → overflow. invokeWithGuardrail's Angle-2 pre-check (consumerFrictionHelper.mjs:350-375) skips any payload whose estimated tokens exceed safeProcessingLimitTokens (100,000). The under-count puts an actually-~140k+ session under 100k → it is sent → overflows the 131,072 window → silent empty / context-overflownull.

  2. Retry-append amplification → "still getting more." On a non-validating response the retry loop (SemanticGraphExtractor.mjs:280-284, maxRetries=3 at :143) does messages.push(assistant: result.content) + messages.push(user: "…failed validation, correct…") and retries. Near the limit the model returns a truncated, non-empty response → fails validation → appended → next prompt larger (140k→155k→166k). The only guard against this — the silent-overflow abort at :252-268 — fires only on a clean-empty response; a truncated-non-empty one bypasses it and feeds the loop.

  3. Re-serve-bound bypass → never terminates. DreamService.mjs:51 PERMANENT_DEFER_REASONS = new Set(['skip-over-band']). A session stops being re-served (digestState='deferred') only when deferReason === 'skip-over-band' (set at :555 only when payloadSizeTokens > safeBandTokens, via the same under-counting estimate) AND digestAttempts >= maxDigestAttempts (:560). Because the under-count puts the session under the band, deferReason becomes under-band-choke — which :43-48 explicitly treats as non-terminal ("a guess… they stay undigested, re-served"). So the over-context session is re-served every REM cycle indefinitely — never deferred, never digested. (Per-extraction is bounded by maxRetries; the across-cycle re-serve is the unbounded part — confirmed: undigested pinned at 167, recentCycles: [].)

The Architectural Reality (file:line)

  • ai/services/memory-core/helpers/consumerFrictionHelper.mjs:141bytesToTokens = Math.ceil(bytes / BYTES_PER_TOKEN_HEURISTIC), BYTES_PER_TOKEN_HEURISTIC = 4. :350-375 — the skip pre-check.
  • ai/services/graph/SemanticGraphExtractor.mjs:143,221maxRetries=3 loop; :252-268 — clean-empty-only overflow abort; :280-284 — the monotonic append. It is single-pass (does not call sessionChunker).
  • ai/services/graph/sessionChunker.mjs:22DEFAULT_TOKENS_PER_CHAR = 0.25 (same heuristic family; wired into TopologyInferenceEngine, not tri-vector extraction).
  • ai/daemons/orchestrator/services/DreamService.mjs:51PERMANENT_DEFER_REASONS; :53estimatePayloadTokens; :553-560deferReason / digestState classification.
  • ai/config.template.mjs:239-240contextLimitTokens=131072, safeProcessingLimitTokens=100000.

The Fix (keystone + hardening)

Keystone — make the token estimate not under-count (fixes all three at once). An accurate/conservative estimate makes the over-context session classify as skip-over-band, which (a) skips it before the send (no overflow, no amplification) and (b) is in PERMANENT_DEFER_REASONSdeferred after maxDigestAttemptsre-serve stops → termination. Design (decide on the PR, cross-family — the estimate is shared by every model-consumer, so the change has blast radius):

  • Recommended immediate: a conservative bytes/token ratio (≈ 2.5–3, i.e. assume dense) for the skip decision — over-counting is safe (skip-before-overflow; over-band sessions then route to defer/chunk), under-counting is the bug. Apply consistently to bytesToTokens, sessionChunker DEFAULT_TOKENS_PER_CHAR, and DreamService.estimatePayloadTokens.
  • Hardening: use the provider's real tokenizer for the band pre-check where available; keep the ratio as the no-round-trip fallback. Invariant: a payload that passes the band stays under contextLimitTokens at worst observed density, with margin for the prompt wrapper + output.

Hardening — size-aware retry. In SemanticGraphExtractor, before the repair-append (:280) check the running messages size against the band and abort instead of append when approaching it; treat a truncated / finish_reason === 'length' response as overflow (close the empty-only-abort hole at :252-268).

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
bytesToTokens / BYTES_PER_TOKEN_HEURISTIC (consumerFrictionHelper.mjs) this incident + ctx cap 131072 estimate does not under-count dense content; over-band payloads skip before send emit size-precheck-skip JSDoc L2: dense ~140k payload estimates > band; incident-shape regression
DEFAULT_TOKENS_PER_CHAR (sessionChunker.mjs) + estimatePayloadTokens (DreamService.mjs) same same conservative ratio so chunk bounds + defer-classification agree n/a JSDoc L2 chunk-bound + defer-classification tests
SemanticGraphExtractor retry loop the amplification size-aware abort; truncated/length treated as overflow, not repaired abort, no append code comment L2: near-band response does not grow the prompt
DreamService deferReason → digestState re-serve bound (#13845) an over-context session classifies skip-over-banddeferred after maxDigestAttempts → re-serve stops stays undigested only if genuinely under-band JSDoc L2: over-context session terminates re-serve

Acceptance Criteria

  • A dense ~140k-actual-token session is estimated above the safe band and skipped before send (incident-shape regression test).
  • The same session classifies deferReason='skip-over-band' and is marked deferred after maxDigestAttemptsre-serve terminates (no unbounded loop).
  • The retry loop does not append-grow past the band; a truncated/finish_reason==='length' response aborts rather than repair-retries.
  • A payload that passes the band stays under contextLimitTokens at worst observed density + wrapper/output margin.
  • No behavior change for normal-density sessions already comfortably under the band.
  • Estimate change applied consistently across consumerFrictionHelper, sessionChunker, DreamService (no split-brain estimate).

Out of Scope (complementary, already ticketed)

  • #12073 (chunking-aware Tri-Vector) — once over-band sessions defer correctly, chunking is what actually digests them (chunk → each under band). This ticket makes the band reliable; #12073 makes deferred sessions recoverable.
  • #13835 / #13845 (stop-re-serve kernel) — exists; this fix makes its skip-over-band gate actually trigger.
  • #13851 (loaded-context verification, 4096 vs 131072) — a distinct root.
  • #13914 (deployment observability) — would have surfaced this remotely; today it's a black box.

Avoided Traps

  • Don't raise the model context — the cap is correct; the estimate is wrong.
  • Don't rely on the post-invocation silent-overflow detector — it fires after the burn and only on clean-empty.
  • Don't fix only the entry guard — without the size-aware retry + correct classification, amplification + unbounded re-serve persist.
  • Don't split the estimate — guard, chunker, and defer-classification must agree, or the bug reappears in one path.

Related

Parent epic #12065. Complements #12073, #13835, #13845, #13851, #13914. Live-incident anchor: this session.

Origin Session ID: acbc412a-8060-422c-b0dc-05f929da50e8

Handoff Retrieval Hints: bytes/4 token under-count REM context overflow unbounded re-serve skip-over-band under-band-choke; consumerFrictionHelper bytesToTokens BYTES_PER_TOKEN_HEURISTIC DreamService PERMANENT_DEFER_REASONS SemanticGraphExtractor retry append

tobiu referenced in commit 0ec6f32 - "fix(ai): harden REM token guardrail (#13918) (#13919) on Jun 23, 2026, 1:18 PM
tobiu closed this issue on Jun 23, 2026, 2:20 PM
tobiu referenced in commit a79021f - "fix(ai): stop REM repair retry overflow (#13918) (#13922)" on Jun 23, 2026, 2:20 PM