A long session (v0.43.2, MC-managed mode) froze at 100% context after an
app restart + session resume. The historian was healthy (pointer
advanced to the tail, zero failed runs); the problem was entirely in
two MC-internal paths. Timestamps UTC; session id redacted.
1. Dangling boundary → silent infinite defer (prefix-trim path).
The session's compaction boundary (a message id in session meta) was
not present in the in-memory "current messages" list after a session
resume — the resumed list held 58–115 of the session's ~574 messages,
and the boundary anchored a message outside that window in every app
instance across ~3 h (surviving multiple restarts). Log:
[09-25 23:46:02.224Z] [ses_…] transform stage: stage=findSessionId elapsed=0.3ms messages=58
[09-25 23:46:02.275Z] [ses_…] transform: first pass reset — percentage=45.3% — clearing stale usage state
[09-25 23:46:02.494Z] [ses_…] prefix trim: boundary msg_0a5836bd… absent from current messages; pass=priced; no in-pass trim applied
…94 consecutive passes, identical shape…
[09-26 02:24:48.635Z] [ses_…] prefix trim: boundary msg_0d5f1939… absent from current messages; pass=defer; no in-pass trim applied
Untrimmed, the wire carried the full raw history, which grew each turn
to the context limit (stored tokens.total: 158,958 → 160,268 →
165,394 → 169,812 → 262,145; limit 262,144). A fresh restart re-aligned
the in-memory window and the first pass trimmed normally:
[09-26 02:49:20.759Z] [ses_…] transform: final-wire telemetry estimate=160884 trusted=true conversation=59723 …
[09-26 02:50:06.672Z] [ses_…] event message.updated: totalInputTokens=80538 contextLimit=258978 percentage=31.1%
The code (v0.43.2 dist, index-g6hpfkqa.js):
// :35357-35366 — the prefix-trim path: a single failed lookup, refused, on every pass
} else {
const index = findBoundaryIndex(options.sessionId, options.messages, boundary);
if (index >= 0) {
options.messages.splice(0, index + 1);
status = "applied";
} else {
sessionLog(..., `prefix trim: boundary ${boundary} absent from current
messages; pass=…; no in-pass trim applied`);
status = "refused"; // silently terminal — no counter, no re-anchor
}
}
Note: this is the same failure shape as #264 ("Degraded mode in
inject-compartments has no recovery path when boundary is outside the
visible window", fixed in v0.33.1) — but in a path the fix did not
reach. The sibling inject-compartments path in v0.43.2 ships the full
recovery ladder, in the same file:
// :33772-33799 — inject-compartments path (the #264 fix): counted, re-anchors, falls back
} else {
const degradedCount = noteDegradedRebuild(sessionId);
if (degradedCount === 1) reconcileForkOrphanedCompactionMarkers(db, sessionId);
let reAnchored = false;
if (degradedCount >= REANCHOR_MIN_DEGRADED_PASSES && isCacheBusting) {
…
logReanchorOnce(…`compartment injection re-anchored: natural boundary
${trimEndMessageId} not visible for ${degradedCount} passes; splicing
at visible compartment boundary ${resultEndMessageId}`);
}
if (!reAnchored) {
needsFreshMaterialization = true;
logReanchorOnce(…`compartment injection degraded: … requesting fresh
materialization to re-cut the baseline`);
}
}
Suggested behavior: port that ladder to the prefix-trim path — a
failed lookup is a counted degraded state, not a terminal refusal.
Secondary hardening, same direction: make the cut total by using the
boundary's position in MC's own persistent order (the ordinal space
the historian reads — on the session above the pointer reached 497 of
574 while loaded windows held only 58–115) as the cut coordinate, so a
window that starts entirely after the boundary yields [summary] + [whole window] instead of "absent"; the boundary ID then serves as an
integrity check (no longer resolves → visible "boundary stale" +
re-compaction from list head). (The existing prefixTrimSourceOrder
position-cut mechanism is a close primitive, but it is armed only when
a deferred history refresh is pending and captures the live window as
its coordinate system — which, when the window starts after the
boundary, refuses just as silently.)
2. Pressure escalation runs on incremental event-payload usage, not
final cumulative usage.
The v0.43.2 pressure formula does include cache.write
(index.js:40923: input + cache.read + cache.write) — the undercount
is in the payload it reads. Verbatim, the final call of the frozen
window (same timestamp, two sources):
[09-26 02:29:10.414Z] [ses_…] event message.updated: … hasUsageTokens=true
tokens.input=2398 cache.read=165393 cache.write=0
[09-26 02:29:10.414Z] [ses_…] event message.updated: totalInputTokens=167791
contextLimit=214982 percentage=78.0%
[09-26 02:29:10.521Z] [ses_…] transform scheduler: percentage=78.0% inputTokens=167791 … decision=execute
versus the final stored usage for that same call (opencode store):
tokens.input=89,167, tokens.total=262,145 — the true prompt size,
≈100% of the 262,144 limit. The message.updated payload is an
incremental reading (new input this call + cache hits), while the
stored usage is the cumulative prompt; MC's pressure state is built
from the former (index.js:40980-40985 → contextUsageMap → scheduler
→ latch):
// index.js:40923-40934
const totalInputTokens = (info.tokens?.input ?? 0)
+ (info.tokens?.cache?.read ?? 0) + (info.tokens?.cache?.write ?? 0);
const pressureInputTokens = usageReadingValid ? totalInputTokens : 0;
// :40980-40985
const percentage = contextLimit > 0 ? pressureInputTokens / contextLimit * 100 : 0;
deps.contextUsageMap.set(info.sessionID, { usage: { percentage, inputTokens: pressureInputTokens }, … });
The emergency drain latch arms at max(85, threshold+2) percent
(index.js:39898) — at the true ~100% it should have armed and
bypassed the drain budget; at the frozen 78% it never did
(emergency_drain_active stayed 0 for the whole window). Compounding:
limit-resolution drift (the same session resolved 214,982, then
258,978, then 262,144 across passes) makes the percentage
pass-dependent.
Suggested behavior: build the pressure/escalation state from the
cumulative final usage of completed calls (the value the host stores
per message — tokens.total above), with the message.updated
streaming payload kept for live display only; the
terminalAssistantUpdate completion hook (index.js:40897) is the
natural point to fetch it.
This cannot cause spurious compaction: the cumulative total is the
ground-truth prompt size — the reading moves toward reality, never
above it (the non-cached portion persists as cache on the next turn,
so the cumulative total is a floor on next turn's pressure, not a
transient spike). And the denominator is already floored
(index.js:40978: contextLimit = max(contextLimit, provenSafeInputTokens)), so the limit cannot resolve below a
proven-safe input and manufacture a high-percentage reading; the
residual limit drift is in the under-escalation direction (safe). The
reading can only rise to the true value, and it fires only in the
regime that needs the rescue.
A long session (v0.43.2, MC-managed mode) froze at 100% context after an
app restart + session resume. The historian was healthy (pointer
advanced to the tail, zero failed runs); the problem was entirely in
two MC-internal paths. Timestamps UTC; session id redacted.
1. Dangling boundary → silent infinite defer (prefix-trim path).
The session's compaction boundary (a message id in session meta) was
not present in the in-memory "current messages" list after a session
resume — the resumed list held 58–115 of the session's ~574 messages,
and the boundary anchored a message outside that window in every app
instance across ~3 h (surviving multiple restarts). Log:
Untrimmed, the wire carried the full raw history, which grew each turn
to the context limit (stored
tokens.total: 158,958 → 160,268 →165,394 → 169,812 → 262,145; limit 262,144). A fresh restart re-aligned
the in-memory window and the first pass trimmed normally:
The code (v0.43.2 dist,
index-g6hpfkqa.js):Note: this is the same failure shape as #264 ("Degraded mode in
inject-compartments has no recovery path when boundary is outside the
visible window", fixed in v0.33.1) — but in a path the fix did not
reach. The sibling inject-compartments path in v0.43.2 ships the full
recovery ladder, in the same file:
Suggested behavior: port that ladder to the prefix-trim path — a
failed lookup is a counted degraded state, not a terminal refusal.
Secondary hardening, same direction: make the cut total by using the
boundary's position in MC's own persistent order (the ordinal space
the historian reads — on the session above the pointer reached 497 of
574 while loaded windows held only 58–115) as the cut coordinate, so a
window that starts entirely after the boundary yields
[summary] + [whole window]instead of "absent"; the boundary ID then serves as anintegrity check (no longer resolves → visible "boundary stale" +
re-compaction from list head). (The existing
prefixTrimSourceOrderposition-cut mechanism is a close primitive, but it is armed only when
a deferred history refresh is pending and captures the live window as
its coordinate system — which, when the window starts after the
boundary, refuses just as silently.)
2. Pressure escalation runs on incremental event-payload usage, not
final cumulative usage.
The v0.43.2 pressure formula does include cache.write
(
index.js:40923:input + cache.read + cache.write) — the undercountis in the payload it reads. Verbatim, the final call of the frozen
window (same timestamp, two sources):
versus the final stored usage for that same call (opencode store):
tokens.input=89,167,tokens.total=262,145— the true prompt size,≈100% of the 262,144 limit. The
message.updatedpayload is anincremental reading (new input this call + cache hits), while the
stored usage is the cumulative prompt; MC's pressure state is built
from the former (
index.js:40980-40985→contextUsageMap→ scheduler→ latch):
The emergency drain latch arms at
max(85, threshold+2)percent(
index.js:39898) — at the true ~100% it should have armed andbypassed the drain budget; at the frozen 78% it never did
(
emergency_drain_activestayed 0 for the whole window). Compounding:limit-resolution drift (the same session resolved 214,982, then
258,978, then 262,144 across passes) makes the percentage
pass-dependent.
Suggested behavior: build the pressure/escalation state from the
cumulative final usage of completed calls (the value the host stores
per message —
tokens.totalabove), with themessage.updatedstreaming payload kept for live display only; the
terminalAssistantUpdatecompletion hook (index.js:40897) is thenatural point to fetch it.
This cannot cause spurious compaction: the cumulative total is the
ground-truth prompt size — the reading moves toward reality, never
above it (the non-cached portion persists as cache on the next turn,
so the cumulative total is a floor on next turn's pressure, not a
transient spike). And the denominator is already floored
(
index.js:40978:contextLimit = max(contextLimit, provenSafeInputTokens)), so the limit cannot resolve below aproven-safe input and manufacture a high-percentage reading; the
residual limit drift is in the under-escalation direction (safe). The
reading can only rise to the true value, and it fires only in the
regime that needs the rescue.