You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Follow-up to #4458 and #4486. #4486 removed the fabricated capacity and the two terminal gates. This issue takes the remaining step: the runtime stops estimating context fit altogether. Every "does it fit" question is answered by a provider, every trigger by a real number.
I plan to implement this as a single PR. The design was discussed on #4458 with @Astro-Han and @likun666661; the one contested point is called out at the end.
Principles
Fit is the provider's answer. Maka neither estimates, nor judges, nor terminates. The only irreversible outcome is the provider's own rejection.
The Maka window is a target, not a limit. It is the compaction trigger the user configures. If the user has not set one, there is none.
Real numbers only. Maka uses provider-reported usage and user-declared settings. charsPerToken takes part in no decision.
Reversible first. Maka's only proactive actions are a fold (reversible) and a hint.
Maka does not decide for the user. It surfaces facts (the model's declared window, the last accepted usage); they take effect only when the user applies them.
Three windows
Source
Role
When unset / unknown
Maka window
User setting, the only source
Proactive fold trigger
No threshold, no proactive fold
Metadata window
Provider /models or models.dev
A hint next to the setting, one click to apply
No hint
Provider window
Unknown to Maka
Hard limit
Acts through rejection / truncated reply / eviction
What "new content" is
Everything appended between two provider requests, not only the user's message:
produced by the model previous step's text, tool_call arguments, reasoning (wire-dependent)
size known: previous usage.outputTokens
from outside the model tool results, user message, images, steering, tool-schema changes
size unknown; text tool results are already capped at 8 KB each
baseline = input_prev + output_prev, both provider-counted. The only unknown is the outside part, and it does not need measuring: the provider will.
Main flow
appended content
│
▼
[A] content policy (not a judgment)
a text tool result over 8 KB → archived to an artifact, a placeholder goes on the wire
│
▼
[B] proactive fold (the only proactive trigger)
Maka window unset, or no valid baseline → skip
baseline > Maka window → foldOnce(); send regardless of outcome
previous step finished with `length` → foldOnce() once
│
▼
[C] send ── the provider decides, six branches ────────────────────────────┐
│ │
├─ accepted, usage present and grew → record baseline, continue │
├─ accepted, usage flat or lower → hint: provider is dropping or rewriting context; set a Maka window
├─ accepted, usage missing → hint: this provider reports no usage; only the rejection path remains
├─ accepted, finish = length → reply was cut; mark, fold once before the next step
├─ rejected, classified ContextLength │
│ not yet folded this send → foldOnce() → resend once ─────────────────┘
│ already folded → surface the provider's error verbatim; the message stays editable
│ and baseline < Maka window → hint: window is larger than the provider's; suggest input_n, one click
│ and Maka window unset → hint: set it to input_n?, one click
└─ rejected, unclassified → error + a "compact and retry" action; the user decides
At most two folds per send: one in [B], one on rejection. No loop.
foldOnce
range structurally chosen largest safe prefix: no partial, never splits a tool pair, one pair kept as tail
pre_turn (step 0): fold prior turns, the user message pinned into the tail
mid_turn (step ≥ 1): fold prior turns plus this turn's completed steps;
tail = user message + the in-flight tool pair
summary maxOutputTokens = 8k
summarizer says input_too_large → halve the range → retry; down to the tail alone and still rejected → give up, send anyway
summarizer finishes with `length` → one shorten pass through the existing repair path
quality judged on the summarizer call's real in/out: a large fold that yields a few hundred tokens is rejected
no word limit in the prompt (measured summaries are ~1K; the failure direction is too short)
write checkpoint; replay = summary + everything after the boundary, no size-based selection
after baseline cleared; the next accepted request is the first measurement
The tail needs no token bound: text results are capped by [A], images are counted by the provider, an oversized user message goes through the rejection path and is edited by the user.
Baseline validity, including resume
source the session's newest persisted token_usage input/output (lastRequestAnchor from #4486)
valid same model, same connection (run-header check)
invalid model or connection changed → discard; after any fold → clear; usage not persisted before a crash → none
too high user deleted messages / branched back → keep; at worst one unnecessary fold, reversible
shrunk user lowered the Maka window → baseline > new window → fold before the next append; same path, no error
Rule: rather none than a guess. The cost of no baseline is one possible rejection on the first request.
Every number in the design
Number
Origin
A judgment?
input / output
provider
—
Maka window
user
no, a target
8 KB
content policy for one tool result
no
8k
summary output cap
no; the summarizer's provider truncates, then a shorten pass
With no window set, the only Maka-owned numbers left are the two content-policy constants.
Against the current main
Removed
maxHistoryEstimatedTokens and its five consumers; the 32,000 fallback
the quarter-window / 16,384 reserve (retired outright)
estimateNextRequestTokens; midTurnRequestPayloadChars (a JSON.stringify of the whole request every step)
lastRequestPayloadChars, forcedTriggerEstimate, the capacity re-entry, lastShapeFailure
the payloadChars half of lastRequestAnchor
the estimated input fit in fitHistoryCompactMessages
chars/4 in summary-quality validation
size-based replay selection (ai-sdk-backend.ts ~4878)
the metadata window taking effect as capacity on its own
Changed
baseline gains output_prev (reasoning tokens counted per wire)
retreat halves the range instead of dropping one event, with a give-up exit
the Maka window reads the user setting only; the metadata window becomes a hint
Added
maxOutputTokens wired into the summarizer call (history-compact-summarizer.ts:44 is declared and never passed)
finish = length → fold once before the next step
silent-eviction detection (append-only step, usage did not grow)
missing-usage hint
window-too-large / window-unset hints with one-click apply of input_n
unclassified rejection → "compact and retry" action
metadata window shown next to the setting with one-click apply
composer usage indicator: percentage with a window, absolute tokens without
Open points
Reasoning tokens in the baseline. Anthropic resends thinking inside a turn's tool loop; OpenAI does not. Per wire, or always counted (high, safe)?
Out-of-the-box behaviour. With no window set the runtime is purely reactive: the first time a session fills up costs one round trip and possibly one cut reply. Acceptable, or is the one-click hint on first rejection enough?
Manual /compact. Same foldOnce; after it clears the baseline, the indicator shows "unmeasured since compaction"?
The delta, contested.fix(runtime): let the provider decide fit, anchor estimates on real usage, retire the local verdict #4486 keeps a signed char/4 delta so step 0 can fold before a large new message or image goes out. My case for dropping it: the delta prices an image at a constant and under-reads CJK by about half, so the round trip it saves is the one it is most likely to misjudge; large text is already capped by [A]; and removing it takes the whole payload ruler and the per-step JSON.stringify with it. Happy to be argued back on the PR.
English
Follow-up to #4458 and #4486. #4486 removed the fabricated capacity and the two terminal gates. This issue takes the remaining step: the runtime stops estimating context fit altogether. Every "does it fit" question is answered by a provider, every trigger by a real number.
I plan to implement this as a single PR. The design was discussed on #4458 with @Astro-Han and @likun666661; the one contested point is called out at the end.
Principles
charsPerTokentakes part in no decision.Three windows
/modelsor models.devWhat "new content" is
Everything appended between two provider requests, not only the user's message:
baseline = input_prev + output_prev, both provider-counted. The only unknown is the outside part, and it does not need measuring: the provider will.Main flow
At most two folds per send: one in [B], one on rejection. No loop.
foldOnce
The tail needs no token bound: text results are capped by [A], images are counted by the provider, an oversized user message goes through the rejection path and is edited by the user.
Baseline validity, including resume
Rule: rather none than a guess. The cost of no baseline is one possible rejection on the first request.
Every number in the design
With no window set, the only Maka-owned numbers left are the two content-policy constants.
Against the current
mainRemoved
Changed
Added
Open points
/compact. SamefoldOnce; after it clears the baseline, the indicator shows "unmeasured since compaction"?JSON.stringifywith it. Happy to be argued back on the PR.Refs #4458, #4486, #4283.
简体中文
#4458 与 #4486 的后续。#4486 删掉了编造的容量和两道终止闸门;本 issue 走完剩下的一步:runtime 不再估算上下文放不放得下。所有「放不放得下」由某个供应商回答,所有触发由真实数字驱动。
我计划用一个 PR 实现。设计已在 #4458 上与 @Astro-Han、@likun666661 讨论;唯一有争议的点放在最后。
原则
charsPerToken不参与任何决策。三个窗口
/models或 models.dev「新内容」是什么
两次供应商请求之间追加的一切,不只是用户消息:
baseline = input_prev + output_prev,两个都是供应商数的。唯一的未知是模型之外的部分,它不需要量,供应商会量。主流程
每次发送最多两次折叠:[B] 一次 + 拒绝路径一次,无循环。
foldOnce
尾巴不需要 token 上界:文本被 [A] 压住,图片由供应商数,巨大用户消息走拒绝路径由用户编辑。
baseline 有效性(含恢复)
规则:宁可没有,不要猜。没有 baseline 的代价是第一次请求可能被拒一次。
每个数字的来历
未设窗口时,Maka 自己的数只剩后两个内容策略常量。
相比当前
main删
改
加
开放问题
/compact。 同一个 foldOnce;清空 baseline 后指示器显示「压缩后未测量」?JSON.stringify。欢迎在 PR 上说服我。Refs #4458, #4486, #4283。