You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by as...@apache.org on 2026/08/23 07:32:44 UTC
(maka) branch fix/3542-unify-context-compaction updated (c0c59d01d -> 152d87f1e)
This is an automated email from the ASF dual-hosted git repository.
Astro-Han pushed a change to branch fix/3542-unify-context-compaction
in repository https://gitbox.apache.org/repos/asf/maka.git
omit c0c59d01d fix: unify context compaction
add 3f94fe847 fix(runtime-host): keep folded steering entries decodable in the followup queue (#3532)
add 04680fe0b fix(cli): show model display names in picker (#3493)
add c9fa58a53 fix(desktop): reconcile goal arm across reconnects (#3363)
add ce0e5931a feat(desktop): let the user choose or import the app icon (#3431)
add b9dfa99c2 feat(runtime-host): attribute Windows startup stalls (#3454)
add 46caf842a docs: turn the Simplified Chinese README link into a 中文文档 badge (#3547)
add d60498784 fix(cli): reconcile tool results while turns are live (#3524)
add 4ee7be566 fix(runtime-host): reconcile retired fallback models (#3517)
add ebc1e1817 fix(core): classify IPv6 literals by parsed bytes in the private-range SSRF gate (#3502)
add bfcd3da68 fix(cli): let /session detach from a running Turn instead of trapping the client (#3498)
add e665bc4ad fix(core): scope the missing-default-model health warning to the workspace default (#3514)
add e05308be7 feat(providers): add Alibaba (China) provider for the dashscope China endpoint (#3329)
new dca393495 fix: unify context compaction
new 152d87f1e refactor: remove context policy environment overrides
This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version. This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:
* -- * -- B -- O -- O -- O (c0c59d01d)
\
N -- N -- N refs/heads/fix/3542-unify-context-compaction (152d87f1e)
You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.
Any revisions marked "omit" are not gone; other references still
refer to them. Any revisions marked "discard" are gone forever.
The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
CHANGELOG.md | 4 +
README.md | 2 +-
README.zh-CN.md | 2 +-
apps/desktop/assets/app-icons/alpine.png | Bin 0 -> 28246 bytes
apps/desktop/assets/app-icons/cyan.png | Bin 0 -> 25907 bytes
apps/desktop/assets/app-icons/dusk.png | Bin 0 -> 28401 bytes
apps/desktop/assets/app-icons/forest.png | Bin 0 -> 28599 bytes
apps/desktop/assets/app-icons/graphite.png | Bin 0 -> 28591 bytes
apps/desktop/assets/app-icons/ice.png | Bin 0 -> 26690 bytes
apps/desktop/assets/app-icons/ink.png | Bin 0 -> 27190 bytes
apps/desktop/assets/app-icons/mono.png | Bin 0 -> 348468 bytes
apps/desktop/assets/app-icons/night.png | Bin 0 -> 27999 bytes
apps/desktop/assets/app-icons/pale-inverted.png | Bin 0 -> 26392 bytes
apps/desktop/assets/app-icons/paper.png | Bin 0 -> 27824 bytes
apps/desktop/assets/app-icons/pencil-kraft.png | Bin 0 -> 27457 bytes
apps/desktop/assets/app-icons/pencil-navy.png | Bin 0 -> 27366 bytes
apps/desktop/assets/app-icons/pencil-sky.png | Bin 0 -> 27900 bytes
apps/desktop/assets/app-icons/sky.png | Bin 0 -> 25999 bytes
.../src/main/__tests__/app-icon-ipc.test.ts | 223 ++++
apps/desktop/src/main/__tests__/app-icon.test.ts | 134 +++
.../main/__tests__/client-settings-effects.test.ts | 33 +
.../main/__tests__/custom-app-icon-store.test.ts | 90 ++
.../src/main/__tests__/desktop-assets.test.ts | 6 +-
.../src/main/__tests__/goal-arm-outcome.test.ts | 100 ++
.../desktop/src/main/__tests__/goal-dialog.test.ts | 270 +++++
.../src/main/__tests__/goal-preload-bridge.test.ts | 89 ++
.../src/main/__tests__/image-header.test.ts | 87 ++
.../runtime-host-reconnecting-ipc-main.test.ts | 268 ++++-
.../runtime-host-session-domains-ipc-main.test.ts | 435 +++++++-
.../__tests__/settings-ownership-app-icon.test.ts | 49 +
apps/desktop/src/main/app-icon-ipc.ts | 219 ++++
apps/desktop/src/main/app-icon-surface.ts | 174 ++++
apps/desktop/src/main/app-icon.ts | 68 ++
apps/desktop/src/main/client-settings-effects.ts | 22 +-
apps/desktop/src/main/custom-app-icon-store.ts | 92 ++
apps/desktop/src/main/custom-app-icons.ts | 128 +++
apps/desktop/src/main/desktop-assets.ts | 5 +
.../desktop/src/main/desktop-shell-presentation.ts | 24 +-
apps/desktop/src/main/image-header.ts | 82 ++
apps/desktop/src/main/ipc-reconnect-policy.ts | 51 +
apps/desktop/src/main/main-window.ts | 21 +-
apps/desktop/src/main/runtime-host-boot.ts | 20 +
.../src/main/runtime-host-desktop-candidate.ts | 45 +-
.../src/main/runtime-host-desktop-manager.ts | 2 +-
.../src/main/runtime-host-permissions-ipc-main.ts | 10 +-
.../src/main/runtime-host-reconnecting-ipc-main.ts | 203 +++-
.../main/runtime-host-session-domains-ipc-main.ts | 104 +-
apps/desktop/src/preload/bridge-contract.d.ts | 53 +-
apps/desktop/src/preload/preload.ts | 66 +-
.../src/preload/projected-session-runtime-host.ts | 76 ++
apps/desktop/src/renderer/goal-arm-outcome.ts | 50 +
apps/desktop/src/renderer/goal-dialog.tsx | 62 +-
.../src/renderer/locales/settings-health-copy.ts | 23 +-
.../renderer/locales/settings-preferences-copy.ts | 50 +-
apps/desktop/src/renderer/locales/shell-copy.ts | 43 +
.../renderer/settings/appearance-settings-page.tsx | 231 ++++-
.../src/renderer/settings/provider-brand-marks.tsx | 1 +
.../src/renderer/settings/provider-display-copy.ts | 8 +
.../src/renderer/settings/settings-surface.tsx | 1 +
.../src/renderer/styles/settings/theme-preview.css | 9 +
apps/desktop/src/shared/goal-arm.ts | 11 +
apps/desktop/src/shared/settings-ownership.ts | 13 +-
.../stories/settings/settings-pages.stories.tsx | 31 +
.../llm-compaction-events-log-projection-draft.md | 36 +-
...compaction-events-log-projection-draft.zh-CN.md | 36 +-
docs/astryx-surface-file-inventory.md | 2 +-
packages/cli/src/__tests__/pi-tui-runner.test.ts | 396 +++++++-
.../src/__tests__/runtime-host-onboarding.test.ts | 8 +-
.../__tests__/runtime-host-session-driver.test.ts | 34 +
packages/cli/src/pi-tui-contracts.ts | 2 +
packages/cli/src/pi-tui-pickers.ts | 23 +-
packages/cli/src/pi-tui-runner.ts | 183 +++-
packages/cli/src/runtime-host-cli-context.ts | 2 +-
packages/cli/src/runtime-host-onboarding.ts | 1 +
packages/cli/src/runtime-host-session-channel.ts | 4 +
packages/cli/src/runtime-host-session-driver.ts | 18 +
packages/cli/src/session-driver.ts | 2 +-
packages/core/src/__tests__/health.test.ts | 75 ++
.../src/__tests__/mcp-host-classification.test.ts | 113 +++
packages/core/src/__tests__/model-catalog.test.ts | 24 +
.../core/src/__tests__/model-web-search.test.ts | 4 +
packages/core/src/__tests__/settings.test.ts | 60 +-
packages/core/src/health.ts | 64 +-
packages/core/src/mcp.ts | 152 ++-
packages/core/src/model-metadata.generated.ts | 1070 ++++++++++++++++++++
packages/core/src/model-web-search.ts | 2 +
packages/core/src/provider-registry.ts | 32 +
packages/core/src/runtime-policy.ts | 1 +
packages/core/src/settings.ts | 99 ++
.../mcp/src/__tests__/transport-security.test.ts | 40 +
.../src/__tests__/bootstrap-runtime-policy.test.ts | 47 +-
.../src/__tests__/handshake-compatibility.test.ts | 72 +-
.../runtime-host/src/__tests__/host-kernel.test.ts | 287 +++++-
.../src/__tests__/message-coordinator.test.ts | 39 +
.../src/__tests__/owned-candidate.test.ts | 7 +
.../src/__tests__/startup-error.test.ts | 71 +-
.../runtime-host/src/client/connect-or-spawn.ts | 196 +++-
packages/runtime-host/src/client/connection.ts | 87 +-
packages/runtime-host/src/client/index.ts | 1 +
packages/runtime-host/src/client/launcher.ts | 4 +-
packages/runtime-host/src/client/startup-error.ts | 14 +-
.../src/server/bootstrap-runtime-policy.ts | 6 +
.../runtime-host/src/server/message-coordinator.ts | 15 +-
.../context-budget-mid-turn-policy.test.ts | 137 +--
.../__tests__/context-budget-model-facts.test.ts | 5 +-
.../context-budget-retired-settings.test.ts | 49 -
.../__tests__/mid-turn-capacity-backend.test.ts | 58 +-
.../src/__tests__/responses-wire-contract.test.ts | 7 +
packages/runtime/src/context-budget-policy.ts | 175 +---
.../src/telemetry/model-pricing.generated.ts | 177 ++++
.../runtime-policy/connection-catalog-document.ts | 36 +-
scripts/sync-model-metadata.mjs | 1 +
scripts/verify-packaged-app-icons.test.mjs | 99 ++
scripts/verify-packaged-app.mjs | 26 +
scripts/verify-windows-x64.mjs | 1 +
115 files changed, 7261 insertions(+), 629 deletions(-)
create mode 100644 apps/desktop/assets/app-icons/alpine.png
create mode 100644 apps/desktop/assets/app-icons/cyan.png
create mode 100644 apps/desktop/assets/app-icons/dusk.png
create mode 100644 apps/desktop/assets/app-icons/forest.png
create mode 100644 apps/desktop/assets/app-icons/graphite.png
create mode 100644 apps/desktop/assets/app-icons/ice.png
create mode 100644 apps/desktop/assets/app-icons/ink.png
create mode 100644 apps/desktop/assets/app-icons/mono.png
create mode 100644 apps/desktop/assets/app-icons/night.png
create mode 100644 apps/desktop/assets/app-icons/pale-inverted.png
create mode 100644 apps/desktop/assets/app-icons/paper.png
create mode 100644 apps/desktop/assets/app-icons/pencil-kraft.png
create mode 100644 apps/desktop/assets/app-icons/pencil-navy.png
create mode 100644 apps/desktop/assets/app-icons/pencil-sky.png
create mode 100644 apps/desktop/assets/app-icons/sky.png
create mode 100644 apps/desktop/src/main/__tests__/app-icon-ipc.test.ts
create mode 100644 apps/desktop/src/main/__tests__/app-icon.test.ts
create mode 100644 apps/desktop/src/main/__tests__/custom-app-icon-store.test.ts
create mode 100644 apps/desktop/src/main/__tests__/goal-arm-outcome.test.ts
create mode 100644 apps/desktop/src/main/__tests__/goal-dialog.test.ts
create mode 100644 apps/desktop/src/main/__tests__/goal-preload-bridge.test.ts
create mode 100644 apps/desktop/src/main/__tests__/image-header.test.ts
create mode 100644 apps/desktop/src/main/__tests__/settings-ownership-app-icon.test.ts
create mode 100644 apps/desktop/src/main/app-icon-ipc.ts
create mode 100644 apps/desktop/src/main/app-icon-surface.ts
create mode 100644 apps/desktop/src/main/app-icon.ts
create mode 100644 apps/desktop/src/main/custom-app-icon-store.ts
create mode 100644 apps/desktop/src/main/custom-app-icons.ts
create mode 100644 apps/desktop/src/main/image-header.ts
create mode 100644 apps/desktop/src/preload/projected-session-runtime-host.ts
create mode 100644 apps/desktop/src/renderer/goal-arm-outcome.ts
create mode 100644 packages/core/src/__tests__/mcp-host-classification.test.ts
delete mode 100644 packages/runtime/src/__tests__/context-budget-retired-settings.test.ts
create mode 100644 scripts/verify-packaged-app-icons.test.mjs
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
(maka) 02/02: refactor: remove context policy environment overrides
Posted by as...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
Astro-Han pushed a commit to branch fix/3542-unify-context-compaction
in repository https://gitbox.apache.org/repos/asf/maka.git
commit 152d87f1ed9cd06642fb846e8fa381406a0958b2
Author: AstroHan <le...@outlook.com>
AuthorDate: Sun Aug 23 15:32:27 2026 +0800
refactor: remove context policy environment overrides
Make context management a fixed Runtime-owned policy derived from model facts, remove unsupported configuration states, and document on-demand ArchiveRead behavior.
Generated-by: Codex
---
CHANGELOG.md | 4 +
.../llm-compaction-events-log-projection-draft.md | 36 ++---
...compaction-events-log-projection-draft.zh-CN.md | 36 ++---
.../context-budget-mid-turn-policy.test.ts | 137 ++++++----------
.../__tests__/context-budget-model-facts.test.ts | 5 +-
.../context-budget-retired-settings.test.ts | 49 ------
.../__tests__/mid-turn-capacity-backend.test.ts | 58 +------
packages/runtime/src/context-budget-policy.ts | 175 +++------------------
8 files changed, 105 insertions(+), 395 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 355478484..d612438ee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,6 +26,10 @@
- Added `/transcript` to browse long TUI sessions without depending on terminal
scrollback, with line, page, and first/last navigation.
+### Changed
+
+- Unified context management under one Runtime-owned policy. `MAKA_CONTEXT_*` environment overrides no longer tune or disable compaction and Tool Result pruning; model-visible archive placeholders are read on demand through bounded `ArchiveRead` calls instead of eager hydration.
+
## 0.1.11 - 2026-08-18
### Highlights
diff --git a/docs/architecture/llm-compaction-events-log-projection-draft.md b/docs/architecture/llm-compaction-events-log-projection-draft.md
index 6c0d696dd..9070ed180 100644
--- a/docs/architecture/llm-compaction-events-log-projection-draft.md
+++ b/docs/architecture/llm-compaction-events-log-projection-draft.md
@@ -175,30 +175,26 @@ First, compaction happens inside **model-history projection**, not inside the Ru
Second, the checkpoint is not itself a canonical RuntimeEvent. Coverage and tail selection return the selected checkpoint explicitly alongside the projected RuntimeEvents. A V2 checkpoint also materializes as the familiar system-authored text block so ordinary replay planning can consume it. A compatible V3 checkpoint creates no synthetic text: the provider materializer prepends its assistant `openai.compaction` custom part directly. Neither representation is written back to the RuntimeE [...]
-## Triggering: high water decides when to project
+## Triggering ends before compaction begins
-Automatic history compaction is governed by `ContextBudgetPolicy`. The current default policy:
+Runtime derives one capacity from the selected model's metadata. For a known context window it reserves one quarter of the window, capped at 16,384 tokens; most providers without a known window use a 32,000-token history budget plus the classic 16,384-token reserve.
-- derives the prior-history budget from the selected model's context window and reserves 16,384 tokens by default;
-- uses a 32,000-token history budget for most providers when no context window can be resolved;
-- enters compaction when compactable history exceeds `maxHistoryEstimatedTokens × highWaterRatio`;
-- defaults `highWaterRatio` to `1`;
-- targets a raw tail of no more than 16,384 estimated tokens;
-- asks for at least three recent Turns, while token-capped tail selection may retain fewer when a Turn is very large;
-- defaults to at most 1,024 estimated tokens for one checkpoint and 2,048 for the compact-projection budget.
+Trigger owners use that capacity but do not participate in compaction:
-These numbers are current policy defaults, not protocol constants. Environment variables and model metadata may change them. The stable semantics are:
+- the pre-turn and active-turn evaluators emit a Compact command when the projected request crosses the derived capacity;
+- provider-overflow recovery emits the same command after a real overflow;
+- manual `context.compact` emits it directly, without manufacturing a high-water crossing.
+
+Once emitted, the command always enters the same transaction. The planner receives no force flag, context window, reserve, next-request estimate, high-water ratio, or minimum-recent-Turn policy:
```text
-source log grows past high water
- → select an older prefix to fold
- → preserve a recent raw tail
- → accept projection only if checkpoint + tail fits current limits
+trigger owner emits Compact command
+ → select the largest safe completed prefix
+ → generate and validate one rolling replacement
+ → append one checkpoint or leave durable state unchanged
```
-Tail selection works backward over complete Turns to avoid splitting an ordinary Tool Call/Result pair. If the newest Turn alone exceeds the tail budget, the current implementation falls back to the latest complete function call/response pair in that Turn. If no pair exists, it retains at least the final event.
-
-This boundary matters: `minRecentTurns` is a preference, not permission to violate the context cap.
+Safe-prefix selection never crosses a partial event, pinned live event, or Tool Call/Result pair. Trigger-specific callers may reserve a small verbatim successor tail, but one completed Turn remains compactable regardless of how many Agent Loop steps it contains. Context management has no environment-variable policy surface; model facts and these Runtime invariants are the only inputs.
## What the LLM does—and does not do
@@ -416,7 +412,7 @@ Desktop `sessions:compact` does not silently rewrite a database. It creates a Tu
- the Run ends through normal terminal events and completed/cancelled state;
- a new checkpoint still commits through the same `history_compact_checkpoint_recorded` path.
-The manual policy lowers high water to almost zero and reduces the retained-tail target so even a small history can produce a projection. It changes the trigger, not source, coverage, durability, or replay invariants.
+Manual compaction emits the same Compact command directly. It does not create a special policy or alter a high-water threshold; only the trigger differs from automatic and overflow recovery.
## V2 and V3: one bounded checkpoint with two projection values
@@ -446,6 +442,8 @@ Both preserve canonical source, but they do not create parallel compaction autho
Active Tool Result Prune remains a deterministic non-LLM rewrite. It archives an eligible raw Tool Result before replacing that result in the current provider request. It neither summarizes a span nor creates a checkpoint, and the later history-compaction planner reads the canonical RuntimeEvents rather than treating prune placeholders as source authority.
+The placeholder carries a bounded `maka://archive/...` address and instructions for `ArchiveRead`; model replay deterministically reconstructs that address for legacy placeholders. Runtime does not eagerly expand the archived body back into every request. The model calls `ArchiveRead` only when it needs the detail, and the Host validates the Session, hash, and byte size before returning a bounded inspect or query result. A later checkpoint replaces covered placeholders with its summary a [...]
+
## What compaction is not
### It is not Memory
@@ -511,7 +509,7 @@ Read the current implementation from these locations:
6. `packages/runtime/src/history-compact-ledger.ts`: bounded-projection lookup, ledger recovery, and checkpoint selection;
7. `packages/runtime/src/runtime-kernel.ts`: serialized checkpoint writes and manual-compaction lifecycle;
8. `packages/storage/src/agent-run-store.ts`: commit ordering between canonical append and bounded event projection;
-9. `packages/runtime/src/context-budget-policy.ts`: default budgets, environment controls, and the manual lookup overlay;
+9. `packages/runtime/src/context-budget-policy.ts`: model-capacity derivation and fixed Runtime policy;
10. `packages/runtime/src/openai-codex-history-compactor.ts`: Codex compact-output validation and rolling provider-state input;
11. `packages/runtime-host/src/server/execution-model-composition.ts`: default provider-specific compactor selection.
diff --git a/docs/architecture/llm-compaction-events-log-projection-draft.zh-CN.md b/docs/architecture/llm-compaction-events-log-projection-draft.zh-CN.md
index 920d43f92..8fc9f520d 100644
--- a/docs/architecture/llm-compaction-events-log-projection-draft.zh-CN.md
+++ b/docs/architecture/llm-compaction-events-log-projection-draft.zh-CN.md
@@ -175,30 +175,26 @@ V2 中模型主要看到 `summary`;V3 中 provider 看到自己的 opaque comp
第二,checkpoint 也不是 canonical RuntimeEvent。coverage 与 tail selection 会把选中的 checkpoint 和投影后的 RuntimeEvents 一起显式返回。V2 checkpoint 还会物化为熟悉的 system-authored 文本块,供普通 replay planning 使用;兼容的 V3 checkpoint 不创建任何 synthetic 文本,由 provider materializer 直接在请求头部加入 assistant `openai.compaction` custom part。两种表示都不会伪装成原始交互事件写回 RuntimeEvent ledger。
-## 什么时候触发:High water 决定是否投影
+## Trigger 在 compaction 开始前结束
-自动 history compaction 由 `ContextBudgetPolicy` 驱动。当前默认策略会:
+Runtime 从所选模型的 metadata 推导唯一 capacity。已知 context window 时,reserve 取 window 的四分之一且上限为 16,384 tokens;无法得到 window 时,多数 provider 使用 32,000-token history budget 加经典的 16,384-token reserve。
-- 根据所选模型 context window 计算 prior-history budget,并默认预留 16,384 tokens;
-- 无法得到 context window 时,多数 provider 使用 32,000 tokens 的默认 history budget;
-- 当可 compact 的历史估算超过 `maxHistoryEstimatedTokens × highWaterRatio` 时进入 compaction;
-- 默认 `highWaterRatio = 1`;
-- 默认希望保留不超过 16,384 estimated tokens 的 raw tail;
-- 请求至少保留 3 个最近 Turn,但 tail selection 以 token cap 为先,极大 Turn 可能使实际保留更少;
-- 单个 checkpoint 默认最多 1,024 estimated tokens,compact projection 总预算默认最多 2,048。
+Trigger owner 使用这个 capacity,但不参与 compaction:
-这些数字是当前默认 policy,不是协议常量。环境变量和模型 metadata 可以改变它们。真正稳定的是触发语义:
+- pre-turn 与 active-turn evaluator 在 projected request 越过推导出的 capacity 时发出 Compact command;
+- provider-overflow recovery 在真实 overflow 后发出同一 command;
+- 手动 `context.compact` 直接发出 command,不伪造 high-water crossing。
+
+Command 一旦发出就进入同一个 transaction。Planner 不再接收 force flag、context window、reserve、next-request estimate、high-water ratio 或 minimum-recent-Turn policy:
```text
-source log grows past high water
- → select an older prefix to fold
- → preserve a recent raw tail
- → accept projection only if checkpoint + tail fits current limits
+trigger owner emits Compact command
+ → select the largest safe completed prefix
+ → generate and validate one rolling replacement
+ → append one checkpoint or leave durable state unchanged
```
-Tail selection 按完整 Turn 从新到旧选择,避免把普通 tool call/result pair 从中间劈开。如果单个最新 Turn 本身已经超过 tail budget,当前实现会退而保留该 Turn 中最新的完整 function call/response pair;找不到 pair 时至少保留最后一条事件。
-
-这是一条重要边界:`minRecentTurns` 是期望,不是允许突破上下文上限的承诺。
+Safe-prefix selection 不会跨越 partial event、pinned live event 或 Tool Call/Result pair。Trigger-specific caller 可以保留一小段 verbatim successor tail,但一个 completed Turn 无论包含多少 Agent Loop steps 都可以 compact。Context management 不再提供环境变量 policy surface;模型事实与这些 Runtime invariant 是唯一输入。
## LLM 在这里做什么,也不做什么
@@ -416,7 +412,7 @@ Compaction 跨越 token estimation、LLM call、schema construction、durable ap
- Run 最终以正常 terminal event 与 completed/cancelled 状态收尾;
- 新 checkpoint 仍通过同一 `history_compact_checkpoint_recorded` durable path 提交。
-Manual policy 把 high-water 降到接近零并把 retained tail 目标降到最小,使小历史也能主动产生 projection。它改变触发条件,不改变 source、coverage、durability 和 replay invariants。
+Manual compaction 直接发出同一个 Compact command。它不创建特殊 policy,也不修改 high-water threshold;与 automatic 和 overflow recovery 的差别只在 trigger。
## V2 与 V3:一种 bounded checkpoint、两种 projection value
@@ -446,6 +442,8 @@ Maka 只有一套 LLM compaction 机制,以及一个相邻的 current-request
Active Tool Result Prune 仍是 deterministic、非 LLM 的 rewrite。它先归档 eligible raw Tool Result,再替换 current provider request 中的该结果。它既不总结 span,也不创建 checkpoint;后续 history-compaction planner 读取 canonical RuntimeEvents,不会把 prune placeholder 当成 source authority。
+Placeholder 携带 bounded `maka://archive/...` 地址和 `ArchiveRead` 指令;model replay 会为 legacy placeholder 确定性补回这个地址。Runtime 不再把 archived body eager-expand 回每次请求。只有模型确实需要细节时才调用 `ArchiveRead`,Host 在返回 bounded inspect/query 结果前校验 Session、hash 与 byte size。后续 checkpoint 会用 summary 替换已覆盖的 placeholder,并且有意不携带 archive roots。完整 Tool Result 仍在 canonical RuntimeEvent ledger 中,但 model reachability 不会变成永久的 cross-checkpoint authority。
+
## Compaction 不是什么
### 它不是 Memory
@@ -511,7 +509,7 @@ LLM 在 summary 中写“测试已通过”仍然只是对 source events 的概
6. `packages/runtime/src/history-compact-ledger.ts`:bounded projection lookup、ledger recovery 与 checkpoint selection;
7. `packages/runtime/src/runtime-kernel.ts`:checkpoint write serialization 与 manual compaction lifecycle;
8. `packages/storage/src/agent-run-store.ts`:canonical append 与 bounded event projection 的提交顺序;
-9. `packages/runtime/src/context-budget-policy.ts`:默认预算、环境开关与 manual lookup overlay;
+9. `packages/runtime/src/context-budget-policy.ts`:model-capacity derivation 与固定 Runtime policy;
10. `packages/runtime/src/openai-codex-history-compactor.ts`:Codex compact output 校验与 rolling provider-state input;
11. `packages/runtime-host/src/server/execution-model-composition.ts`:默认 provider-specific compactor 选择。
diff --git a/packages/runtime/src/__tests__/context-budget-mid-turn-policy.test.ts b/packages/runtime/src/__tests__/context-budget-mid-turn-policy.test.ts
index afa0099b7..ef483bedb 100644
--- a/packages/runtime/src/__tests__/context-budget-mid-turn-policy.test.ts
+++ b/packages/runtime/src/__tests__/context-budget-mid-turn-policy.test.ts
@@ -25,43 +25,43 @@ import {
resolveContextBudgetCapacity,
} from '../context-budget-policy.js';
-describe('mid-turn history compact policy env plumbing', () => {
- test('defaults on with no compaction env at all (the runtime, not the surface, owns it)', () => {
- const policy = buildDefaultContextBudgetPolicy(connection(), { env: {} });
- assert.equal(policy?.historyCompact?.enabled, true);
- assert.deepEqual(policy?.historyCompact?.midTurn, { enabled: true, reserveTokens: 16_384 });
- });
-
- test('honors explicit reserve and tail-event overrides', () => {
- const policy = buildDefaultContextBudgetPolicy(connection(), {
- env: {
- MAKA_CONTEXT_HISTORY_COMPACT_RESERVE_TOKENS: '8000',
- MAKA_CONTEXT_HISTORY_COMPACT_MID_TURN_TAIL_EVENTS: '2',
- },
- });
- assert.deepEqual(policy?.historyCompact?.midTurn, {
- enabled: true,
- reserveTokens: 8_000,
- reserveTailEvents: 2,
- });
- });
+test('context policy is independent of process environment overrides', () => {
+ const overrides = {
+ MAKA_CONTEXT_BUDGET: 'off',
+ MAKA_CONTEXT_HISTORY_COMPACT: 'off',
+ MAKA_CONTEXT_HISTORY_COMPACT_HIGH_WATER_NAME: 'custom-high-water',
+ MAKA_CONTEXT_HISTORY_COMPACT_MID_TURN: 'off',
+ MAKA_CONTEXT_HISTORY_COMPACT_MID_TURN_TAIL_EVENTS: '99',
+ MAKA_CONTEXT_HISTORY_COMPACT_RESERVE_TOKENS: '1',
+ MAKA_CONTEXT_STALE_TOOL_RESULT_PRUNE: 'off',
+ MAKA_CONTEXT_STALE_TOOL_RESULT_MAX_TOKENS: '1',
+ MAKA_CONTEXT_STALE_TOOL_RESULT_MIN_RECENT_TURNS: '99',
+ MAKA_CONTEXT_ACTIVE_TOOL_RESULT_PRUNE: 'off',
+ MAKA_CONTEXT_ACTIVE_TOOL_RESULT_MAX_ESTIMATED_TOKENS: '1',
+ MAKA_CONTEXT_ACTIVE_TOOL_RESULT_MIN_STEP_NUMBER: '99',
+ MAKA_CONTEXT_ACTIVE_TOOL_RESULT_MIN_SUPERSEDED_TOKENS: '1',
+ } as const;
+ const previous = Object.fromEntries(
+ Object.keys(overrides).map((name) => [name, process.env[name]]),
+ );
+ try {
+ for (const name of Object.keys(overrides)) delete process.env[name];
+ const baseline = buildDefaultContextBudgetPolicy(connection());
+ Object.assign(process.env, overrides);
+ assert.deepEqual(buildDefaultContextBudgetPolicy(connection()), baseline);
+ } finally {
+ for (const [name, value] of Object.entries(previous)) {
+ if (value === undefined) delete process.env[name];
+ else process.env[name] = value;
+ }
+ }
+});
- test('MAKA_CONTEXT_HISTORY_COMPACT_MID_TURN=off is the escape hatch even with history compact on', () => {
- const policy = buildDefaultContextBudgetPolicy(connection(), {
- env: {
- MAKA_CONTEXT_HISTORY_COMPACT: 'on',
- MAKA_CONTEXT_HISTORY_COMPACT_MID_TURN: 'off',
- },
- });
+describe('mid-turn history compact policy', () => {
+ test('is owned by the runtime default', () => {
+ const policy = buildDefaultContextBudgetPolicy(connection());
assert.equal(policy?.historyCompact?.enabled, true);
- assert.equal(policy?.historyCompact?.midTurn, undefined);
- });
-
- test('an explicit MAKA_CONTEXT_HISTORY_COMPACT=off disables history compaction and midTurn with it', () => {
- const policy = buildDefaultContextBudgetPolicy(connection(), {
- env: { MAKA_CONTEXT_HISTORY_COMPACT: 'off' },
- });
- assert.equal(policy?.historyCompact, undefined);
+ assert.deepEqual(policy?.historyCompact?.midTurn, { enabled: true, reserveTokens: 16_384 });
});
});
@@ -73,13 +73,13 @@ describe('window-bounded reserve derivation (issue #882 PR 3 review P2)', () =>
// summarizer for a checkpoint that could never pass the replay gate.
// The default reserve must be bounded by the KNOWN window: a quarter of
// the window, capped at the classic 16384.
- const policy = buildDefaultContextBudgetPolicy(gpt4Connection(), { env: {}, modelId: 'gpt-4' });
+ const policy = buildDefaultContextBudgetPolicy(gpt4Connection(), { modelId: 'gpt-4' });
assert.equal(policy?.maxHistoryEstimatedTokens, 8192 - 2048);
assert.deepEqual(policy?.historyCompact?.midTurn, { enabled: true, reserveTokens: 2048 });
});
test('uses the official Agent Plan default-model window instead of the unknown-model fallback', () => {
- const policy = buildDefaultContextBudgetPolicy(agentPlanConnection(), { env: {} });
+ const policy = buildDefaultContextBudgetPolicy(agentPlanConnection());
assert.equal(policy?.maxHistoryEstimatedTokens, 256_000 - 16_384);
assert.deepEqual(policy?.historyCompact?.midTurn, { enabled: true, reserveTokens: 16_384 });
});
@@ -91,7 +91,7 @@ describe('window-bounded reserve derivation (issue #882 PR 3 review P2)', () =>
defaultModel: 'custom-model',
models: [{ id: 'custom-model' }],
} as LlmConnection,
- { env: {}, modelId: 'custom-model' },
+ { modelId: 'custom-model' },
);
// No window: the flat 32_000 fallback budget and the classic reserve.
assert.equal(policy?.maxHistoryEstimatedTokens, 32_000);
@@ -109,20 +109,11 @@ describe('window-bounded reserve derivation (issue #882 PR 3 review P2)', () =>
{ tokens: 48_384, source: 'policy_fallback' },
);
});
-
- test('respects an explicit reserve override verbatim even on a small window', () => {
- const policy = buildDefaultContextBudgetPolicy(gpt4Connection(), {
- env: { MAKA_CONTEXT_HISTORY_COMPACT_RESERVE_TOKENS: '6000' },
- modelId: 'gpt-4',
- });
- assert.equal(policy?.maxHistoryEstimatedTokens, 8192 - 6000);
- assert.deepEqual(policy?.historyCompact?.midTurn, { enabled: true, reserveTokens: 6000 });
- });
});
-describe('tool-result prune policy env plumbing', () => {
- test('provides bounded active and stale defaults', () => {
- const policy = buildDefaultContextBudgetPolicy(connection(), { env: {} });
+describe('tool-result prune policy', () => {
+ test('uses bounded runtime defaults', () => {
+ const policy = buildDefaultContextBudgetPolicy(connection());
assert.deepEqual(policy?.activeToolResultPrune, {
enabled: true,
maxCurrentResultEstimatedTokens: 2_048,
@@ -135,45 +126,6 @@ describe('tool-result prune policy env plumbing', () => {
minRecentTurnsFull: 2,
});
});
-
- test('applies explicit active and stale tool-result bounds', () => {
- const policy = buildDefaultContextBudgetPolicy(connection(), {
- env: {
- MAKA_CONTEXT_ACTIVE_TOOL_RESULT_MAX_ESTIMATED_TOKENS: '4096',
- MAKA_CONTEXT_ACTIVE_TOOL_RESULT_MIN_SUPERSEDED_TOKENS: '384',
- MAKA_CONTEXT_ACTIVE_TOOL_RESULT_MIN_STEP_NUMBER: '3',
- MAKA_CONTEXT_STALE_TOOL_RESULT_MAX_TOKENS: '8192',
- MAKA_CONTEXT_STALE_TOOL_RESULT_MIN_RECENT_TURNS: '5',
- },
- });
- assert.equal(policy?.activeToolResultPrune?.maxCurrentResultEstimatedTokens, 4_096);
- assert.equal(policy?.activeToolResultPrune?.minSupersededResultEstimatedTokens, 384);
- assert.equal(policy?.activeToolResultPrune?.minStepNumber, 3);
- assert.equal(policy?.staleToolResultPrune?.maxResultEstimatedTokens, 8_192);
- assert.equal(policy?.staleToolResultPrune?.minRecentTurnsFull, 5);
- });
-
- test('honors explicit disablement and rejects malformed booleans', () => {
- const disabled = buildDefaultContextBudgetPolicy(connection(), {
- env: {
- MAKA_CONTEXT_ACTIVE_TOOL_RESULT_PRUNE: 'false',
- MAKA_CONTEXT_STALE_TOOL_RESULT_PRUNE: 'off',
- },
- });
- assert.equal(disabled?.activeToolResultPrune, undefined);
- assert.equal(disabled?.staleToolResultPrune, undefined);
- assert.equal(
- buildDefaultContextBudgetPolicy(connection(), { env: { MAKA_CONTEXT_BUDGET: 'off' } }),
- undefined,
- );
- assert.throws(
- () =>
- buildDefaultContextBudgetPolicy(connection(), {
- env: { MAKA_CONTEXT_STALE_TOOL_RESULT_PRUNE: 'maybe' },
- }),
- /MAKA_CONTEXT_STALE_TOOL_RESULT_PRUNE must be a boolean/,
- );
- });
});
function gpt4Connection(): LlmConnection {
@@ -215,7 +167,7 @@ describe('declared relay context window', () => {
models: [{ id: 'reasoner-32k', contextWindow: 8_192 }],
relayModelProfiles: { 'reasoner-32k': { contextWindow: 131_072 } },
};
- const policy = buildDefaultContextBudgetPolicy(relay, { env: {}, modelId: 'reasoner-32k' });
+ const policy = buildDefaultContextBudgetPolicy(relay, { modelId: 'reasoner-32k' });
// Declared 131_072 wins: reserve 131_072/4 caps at 16_384. The fetched
// 8_192 row would have yielded 8_192 − 2_048, and no declaration at all
// would have fallen to the 32_000 unknown-model default.
@@ -223,7 +175,6 @@ describe('declared relay context window', () => {
// Clearing the declaration falls back to the fetched row's window.
const undeclared: LlmConnection = { ...relay, relayModelProfiles: undefined };
const fallback = buildDefaultContextBudgetPolicy(undeclared, {
- env: {},
modelId: 'reasoner-32k',
});
assert.equal(fallback?.maxHistoryEstimatedTokens, 8_192 - 2_048);
@@ -247,12 +198,12 @@ describe('declared relay context window', () => {
models: [{ id: 'reasoner-32k', contextWindow: 8_192 }],
relayModelProfiles: { 'reasoner-32k': { contextWindow: 131_072 } },
};
- const policy = buildDefaultContextBudgetPolicy(other, { env: {}, modelId: 'reasoner-32k' });
+ const policy = buildDefaultContextBudgetPolicy(other, { modelId: 'reasoner-32k' });
assert.equal(policy?.maxHistoryEstimatedTokens, 131_072 - 16_384);
// Absent stays absent: an undeclared model still reads the stored row.
const undeclared: LlmConnection = { ...other, relayModelProfiles: undefined };
assert.equal(
- buildDefaultContextBudgetPolicy(undeclared, { env: {}, modelId: 'reasoner-32k' })
+ buildDefaultContextBudgetPolicy(undeclared, { modelId: 'reasoner-32k' })
?.maxHistoryEstimatedTokens,
8_192 - 2_048,
);
diff --git a/packages/runtime/src/__tests__/context-budget-model-facts.test.ts b/packages/runtime/src/__tests__/context-budget-model-facts.test.ts
index 0208f14a8..d472ad935 100644
--- a/packages/runtime/src/__tests__/context-budget-model-facts.test.ts
+++ b/packages/runtime/src/__tests__/context-budget-model-facts.test.ts
@@ -33,10 +33,7 @@ test('context budgeting prefers a model input limit over its context window', ()
};
assert.equal(resolveSelectedModelContextWindow(connection, undefined), 600);
- assert.equal(
- buildDefaultContextBudgetPolicy(connection, { env: {} })?.maxHistoryEstimatedTokens,
- 450,
- );
+ assert.equal(buildDefaultContextBudgetPolicy(connection)?.maxHistoryEstimatedTokens, 450);
});
test('invalid zero input limits do not disable the context-window fallback', () => {
diff --git a/packages/runtime/src/__tests__/context-budget-retired-settings.test.ts b/packages/runtime/src/__tests__/context-budget-retired-settings.test.ts
deleted file mode 100644
index d45245c49..000000000
--- a/packages/runtime/src/__tests__/context-budget-retired-settings.test.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import assert from 'node:assert/strict';
-import { describe, test } from 'node:test';
-import type { LlmConnection } from '@maka/core/llm-connections';
-import { buildDefaultContextBudgetPolicy } from '../context-budget-policy.js';
-
-describe('retired context policy settings', () => {
- test('retired semantic compact environment settings do not change the runtime policy', () => {
- const baseline = buildDefaultContextBudgetPolicy(connection(), { env: {} });
- const configured = buildDefaultContextBudgetPolicy(connection(), {
- env: {
- MAKA_CONTEXT_SEMANTIC_COMPACT: 'on',
- MAKA_CONTEXT_SEMANTIC_COMPACT_MODE: 'replace',
- MAKA_CONTEXT_SEMANTIC_COMPACT_MAX_ACTIVE_ESTIMATED_TOKENS: '4096',
- },
- });
- assert.deepEqual(configured, baseline);
- });
-});
-
-function connection(): LlmConnection {
- return {
- slug: 'anthropic-main',
- name: 'Anthropic',
- providerType: 'anthropic',
- defaultModel: 'claude-sonnet-4-5-20250929',
- enabled: true,
- createdAt: 1,
- updatedAt: 1,
- };
-}
diff --git a/packages/runtime/src/__tests__/mid-turn-capacity-backend.test.ts b/packages/runtime/src/__tests__/mid-turn-capacity-backend.test.ts
index e909c9b4b..e747d512e 100644
--- a/packages/runtime/src/__tests__/mid-turn-capacity-backend.test.ts
+++ b/packages/runtime/src/__tests__/mid-turn-capacity-backend.test.ts
@@ -107,10 +107,8 @@ interface MidTurnFixtureOptions {
/**
* Derive the policy from the runtime default (buildDefaultContextBudgetPolicy)
* instead of the hand-built one, so a test can exercise the shipped default.
- */
+ */
useRuntimeDefaultPolicy?: boolean;
- /** Exercise the supported explicit history-compaction escape hatch. */
- historyCompactOff?: boolean;
reserveTokens?: number;
summarize?: (
input: HistoryCompactSummaryInput,
@@ -488,18 +486,6 @@ function buildFixture(options: MidTurnFixtureOptions = {}): MidTurnFixture {
{
name: 'runtime-default-mid-turn',
modelId: 'mock-model-id',
- // Every value is the shipped runtime default (including the
- // default-on midTurn derivation and the window-bounded reserve
- // under test); a test may still size the reserve to its toy window
- // through the first-class env knob by passing reserveTokens.
- env: {
- ...(options.reserveTokens !== undefined
- ? {
- MAKA_CONTEXT_HISTORY_COMPACT_RESERVE_TOKENS: String(options.reserveTokens),
- }
- : {}),
- ...(options.historyCompactOff ? { MAKA_CONTEXT_HISTORY_COMPACT: 'off' } : {}),
- },
},
)
: {
@@ -1587,52 +1573,10 @@ describe('mid-turn capacity default-on safety guards (issue #882 PR 3)', () => {
assert.equal(promptJson(fixture, 0).includes('OVERSIZED_TOOL_RESULT_'), false);
});
- test('rejects an oversized prior turn when history compaction is disabled', async () => {
- const fixture = buildFixture({
- useRuntimeDefaultPolicy: true,
- withoutContextWindow: true,
- historyCompactOff: true,
- priorChars: 180_000,
- priorShape: 'tool_heavy',
- });
- await runFixtureTurn(fixture);
-
- assert.equal(fixture.summarizerCalls, 0);
- assert.equal(fixture.model.doStreamCalls.length, 0);
- const complete = fixture.events.find((event) => event.type === 'complete');
- assert.equal(complete?.type, 'complete');
- if (complete?.type !== 'complete') return;
- assert.equal(complete.stopReason, 'context_budget_exhausted');
- assert.equal(complete.contextBudgetExhaustedDetail, 'no_safe_completed_span');
- });
-
- test('rejects an oversized penultimate turn when history compaction is disabled', async () => {
- const fixture = buildFixture({
- useRuntimeDefaultPolicy: true,
- withoutContextWindow: true,
- historyCompactOff: true,
- priorChars: 180_000,
- priorShape: 'tool_heavy',
- });
- fixture.priorEvents.push(
- runtimeTextEvent('latest-user', 'turn-latest', 'user', 'small latest question'),
- runtimeTextEvent('latest-model', 'turn-latest', 'model', 'small latest answer'),
- );
- await runFixtureTurn(fixture);
-
- assert.equal(fixture.model.doStreamCalls.length, 0);
- const complete = fixture.events.find((event) => event.type === 'complete');
- assert.equal(complete?.type, 'complete');
- if (complete?.type !== 'complete') return;
- assert.equal(complete.stopReason, 'context_budget_exhausted');
- assert.equal(complete.contextBudgetExhaustedDetail, 'no_safe_completed_span');
- });
-
test('keeps multiple bounded recent turns below the model capacity', async () => {
const fixture = buildFixture({
useRuntimeDefaultPolicy: true,
contextWindow: 100_000,
- historyCompactOff: true,
priorChars: 40_000,
});
fixture.priorEvents.push(
diff --git a/packages/runtime/src/context-budget-policy.ts b/packages/runtime/src/context-budget-policy.ts
index 1c44bdd7a..2c210c7fd 100644
--- a/packages/runtime/src/context-budget-policy.ts
+++ b/packages/runtime/src/context-budget-policy.ts
@@ -25,18 +25,15 @@ import { finitePositive } from './context-budget-helpers.js';
export interface BuildDefaultContextBudgetPolicyOptions {
name?: string;
- env?: Record<string, string | undefined>;
modelId?: string;
}
export function buildDefaultContextBudgetPolicy(
connection: RuntimeExecutionConnection,
options: BuildDefaultContextBudgetPolicyOptions = {},
-): ContextBudgetPolicy | undefined {
- const env = options.env ?? process.env;
- if (env.MAKA_CONTEXT_BUDGET === 'off') return undefined;
+): ContextBudgetPolicy {
const contextWindow = resolveSelectedModelContextWindow(connection, options.modelId);
- const reserveTokens = defaultCompactReserveTokens(env, contextWindow);
+ const reserveTokens = defaultCompactReserveTokens(contextWindow);
const maxHistoryEstimatedTokens = defaultHistoryBudgetTokens(
connection,
contextWindow,
@@ -46,111 +43,25 @@ export function buildDefaultContextBudgetPolicy(
/-default-history-budget$/,
'',
);
- const staleToolResultPrune = buildStaleToolResultPrunePolicy(env);
- const historyCompact = buildHistoryCompactPolicy(
- env,
- `${surfaceName}-history-compact`,
- reserveTokens,
- );
- const activeToolResultPrune = buildActiveToolResultPrunePolicy(env);
- if (
- maxHistoryEstimatedTokens === undefined &&
- staleToolResultPrune === undefined &&
- historyCompact === undefined &&
- activeToolResultPrune === undefined
- ) {
- return undefined;
- }
return {
name: options.name ?? 'default-history-budget',
...(maxHistoryEstimatedTokens !== undefined ? { maxHistoryEstimatedTokens } : {}),
- ...(staleToolResultPrune !== undefined ? { staleToolResultPrune } : {}),
- ...(historyCompact !== undefined ? { historyCompact } : {}),
- ...(activeToolResultPrune !== undefined ? { activeToolResultPrune } : {}),
- };
-}
-
-function buildStaleToolResultPrunePolicy(
- env: Record<string, string | undefined>,
-): NonNullable<ContextBudgetPolicy['staleToolResultPrune']> | undefined {
- const enabled = parseOptionalBoolean(
- env.MAKA_CONTEXT_STALE_TOOL_RESULT_PRUNE,
- 'MAKA_CONTEXT_STALE_TOOL_RESULT_PRUNE',
- );
- if (enabled === false) return undefined;
- return {
- enabled: true,
- maxResultEstimatedTokens: parsePositiveInt(env.MAKA_CONTEXT_STALE_TOOL_RESULT_MAX_TOKENS, 2048),
- minRecentTurnsFull: parsePositiveInt(env.MAKA_CONTEXT_STALE_TOOL_RESULT_MIN_RECENT_TURNS, 2),
- };
-}
-
-function buildActiveToolResultPrunePolicy(
- env: Record<string, string | undefined>,
-): NonNullable<ContextBudgetPolicy['activeToolResultPrune']> | undefined {
- const enabled = parseOptionalBoolean(
- env.MAKA_CONTEXT_ACTIVE_TOOL_RESULT_PRUNE,
- 'MAKA_CONTEXT_ACTIVE_TOOL_RESULT_PRUNE',
- );
- if (enabled === false) return undefined;
- return {
- enabled: true,
- maxCurrentResultEstimatedTokens: parsePositiveInt(
- env.MAKA_CONTEXT_ACTIVE_TOOL_RESULT_MAX_ESTIMATED_TOKENS,
- 2048,
- ),
- minSupersededResultEstimatedTokens: parsePositiveInt(
- env.MAKA_CONTEXT_ACTIVE_TOOL_RESULT_MIN_SUPERSEDED_TOKENS,
- 256,
- ),
- minStepNumber:
- parseOptionalNonNegativeInt(env.MAKA_CONTEXT_ACTIVE_TOOL_RESULT_MIN_STEP_NUMBER) ?? 1,
- };
-}
-
-function buildHistoryCompactPolicy(
- env: Record<string, string | undefined>,
- defaultHighWaterName: string,
- reserveTokens: number,
-): NonNullable<ContextBudgetPolicy['historyCompact']> | undefined {
- const enabled = parseOptionalBoolean(
- env.MAKA_CONTEXT_HISTORY_COMPACT,
- 'MAKA_CONTEXT_HISTORY_COMPACT',
- );
- if (enabled === false) return undefined;
- const midTurn = buildHistoryCompactMidTurnPolicy(env, reserveTokens);
- return {
- enabled: true,
- highWaterName: env.MAKA_CONTEXT_HISTORY_COMPACT_HIGH_WATER_NAME ?? defaultHighWaterName,
- ...(midTurn !== undefined ? { midTurn } : {}),
- };
-}
-
-// Mid-turn capacity compaction is a runtime-owned default (issue #882 PR 3):
-// whenever history compaction is enabled, the runtime derives midTurn from the
-// selected model's window (`contextWindow - reserveTokens`, the same reserve as
-// the turn-boundary budget) so every surface inherits the invariant without
-// copying config. `MAKA_CONTEXT_HISTORY_COMPACT_MID_TURN=off` stays as the
-// explicit escape hatch. The backend still gates activation on the checkpoint
-// seams, the persisted head anchor, and a KNOWN context window, so a session
-// without model metadata (or a child with no anchor seam) never misfires even
-// though the default is on.
-function buildHistoryCompactMidTurnPolicy(
- env: Record<string, string | undefined>,
- reserveTokens: number,
-): NonNullable<NonNullable<ContextBudgetPolicy['historyCompact']>['midTurn']> | undefined {
- const enabled = parseOptionalBoolean(
- env.MAKA_CONTEXT_HISTORY_COMPACT_MID_TURN,
- 'MAKA_CONTEXT_HISTORY_COMPACT_MID_TURN',
- );
- if (enabled === false) return undefined;
- const reserveTailEvents = parseOptionalNonNegativeInt(
- env.MAKA_CONTEXT_HISTORY_COMPACT_MID_TURN_TAIL_EVENTS,
- );
- return {
- enabled: true,
- reserveTokens,
- ...(reserveTailEvents !== undefined ? { reserveTailEvents } : {}),
+ staleToolResultPrune: {
+ enabled: true,
+ maxResultEstimatedTokens: 2_048,
+ minRecentTurnsFull: 2,
+ },
+ historyCompact: {
+ enabled: true,
+ highWaterName: `${surfaceName}-history-compact`,
+ midTurn: { enabled: true, reserveTokens },
+ },
+ activeToolResultPrune: {
+ enabled: true,
+ maxCurrentResultEstimatedTokens: 2_048,
+ minSupersededResultEstimatedTokens: 256,
+ minStepNumber: 1,
+ },
};
}
@@ -160,15 +71,9 @@ function buildHistoryCompactMidTurnPolicy(
// summarizer for a checkpoint the replay gate could never admit. The default
// is therefore bounded by the KNOWN window (a quarter of it, capped at 16384;
// peers bound the same way: opencode caps its buffer by the model's output
-// limit, gemini-cli triggers at a window fraction). An explicit
-// MAKA_CONTEXT_HISTORY_COMPACT_RESERVE_TOKENS is respected verbatim, and an
-// unknown window keeps the classic constant.
-function defaultCompactReserveTokens(
- env: Record<string, string | undefined>,
- contextWindow: number | undefined,
-): number {
- const explicit = parseOptionalPositiveInt(env.MAKA_CONTEXT_HISTORY_COMPACT_RESERVE_TOKENS);
- if (explicit !== undefined) return explicit;
+// limit, gemini-cli triggers at a window fraction). An unknown window keeps
+// the classic constant.
+function defaultCompactReserveTokens(contextWindow: number | undefined): number {
if (contextWindow === undefined) return 16_384;
return Math.min(16_384, Math.max(1, Math.floor(contextWindow / 4)));
}
@@ -235,41 +140,3 @@ export function resolveContextBudgetCapacity(
if (historyBudget === undefined || reserveTokens === undefined) return undefined;
return { tokens: historyBudget + reserveTokens, source: 'policy_fallback' };
}
-
-function parsePositiveInt(value: string | undefined, fallback: number): number {
- const parsed = parseOptionalPositiveInt(value);
- return parsed ?? fallback;
-}
-
-function parseOptionalPositiveInt(value: string | undefined): number | undefined {
- if (!value) return undefined;
- const parsed = Number.parseInt(value, 10);
- return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
-}
-
-function parseOptionalNonNegativeInt(value: string | undefined): number | undefined {
- if (!value) return undefined;
- const parsed = Number.parseInt(value, 10);
- return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
-}
-
-function parseOptionalBoolean(value: string | undefined, name: string): boolean | undefined {
- const normalized = value?.trim().toLowerCase();
- if (!normalized) return undefined;
- switch (normalized) {
- case '1':
- case 'true':
- case 'yes':
- case 'on':
- case 'enabled':
- return true;
- case '0':
- case 'false':
- case 'no':
- case 'off':
- case 'disabled':
- return false;
- default:
- throw new Error(`${name} must be a boolean, got ${JSON.stringify(value)}`);
- }
-}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
(maka) 01/02: fix: unify context compaction
Posted by as...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
Astro-Han pushed a commit to branch fix/3542-unify-context-compaction
in repository https://gitbox.apache.org/repos/asf/maka.git
commit dca393495509a71981d785d87500d54ab1fcf6ce
Author: AstroHan <le...@outlook.com>
AuthorDate: Sun Aug 23 11:25:28 2026 +0800
fix: unify context compaction
Use the RuntimeEvent ledger and latest durable checkpoint as the single context compaction authority across manual, pre-turn, active-turn, and overflow triggers. Allow a completed single turn to compact, return one typed outcome, and remove semantic compaction plus the legacy block, synthesis cache, archive retrieval, history search, rewrite, and flat diagnostic paths.
Fixes #3542
Fixes #3550
Generated-by: Codex
---
.../__tests__/app-shell-command-actions.test.ts | 23 +-
.../__tests__/history-compact-artifacts.test.ts | 500 -----
.../runtime-host-session-execution-ipc-main.ts | 3 +-
apps/desktop/src/preload/bridge-contract.d.ts | 2 +-
apps/desktop/src/preload/preload.ts | 2 +-
.../src/renderer/app-shell-command-actions.ts | 23 +
apps/desktop/src/renderer/app-shell.tsx | 15 +-
.../src/renderer/locales/conversation-copy.ts | 4 +-
apps/desktop/src/renderer/locales/shell-copy.ts | 12 +
.../llm-compaction-events-log-projection-draft.md | 64 +-
...compaction-events-log-projection-draft.zh-CN.md | 62 +-
packages/cli/src/__tests__/pi-transcript.test.ts | 32 +
packages/cli/src/pi-transcript.ts | 31 +-
.../usage-record-schema-retired-fields.test.ts | 55 +
packages/core/src/agent-run.ts | 1 -
packages/core/src/backend-types.ts | 4 +-
packages/core/src/events.ts | 7 +
packages/core/src/usage-ledger-merge.ts | 7 +-
packages/core/src/usage-record-schema.ts | 252 +--
packages/core/src/usage-stats/types.ts | 87 +-
.../src/__tests__/context-coordinator.test.ts | 62 +-
.../src/__tests__/context-protocol.test.ts | 58 +-
.../src/__tests__/desktop-e2e-execution.test.ts | 4 +-
.../__tests__/execution-model-composition.test.ts | 3 +-
.../runtime-host/src/__tests__/protocol.test.ts | 4 +
.../src/__tests__/root-turn-coordinator.test.ts | 25 +-
packages/runtime-host/src/protocol/context.ts | 31 +-
packages/runtime-host/src/protocol/index.ts | 5 +-
packages/runtime-host/src/protocol/turn.ts | 41 +-
.../src/server/canonical-turn-snapshot.ts | 20 +
.../runtime-host/src/server/context-coordinator.ts | 33 +-
.../src/server/execution-model-composition.ts | 1 -
.../src/test-only/desktop-e2e-execution.ts | 2 +-
packages/runtime/package.json | 1 -
.../src/__tests__/active-compaction-kernel.test.ts | 388 ----
.../src/__tests__/active-tool-result-prune.test.ts | 35 +-
.../__tests__/agent-graph-supervisor-wake.test.ts | 28 +-
.../runtime/src/__tests__/ai-sdk-backend.test.ts | 2297 +++-----------------
.../context-budget-mid-turn-policy.test.ts | 3 +-
.../context-budget-retired-settings.test.ts | 49 +
.../context-budget-semantic-compact-policy.test.ts | 71 -
.../runtime/src/__tests__/context-budget.test.ts | 2012 +----------------
.../src/__tests__/conversation-copy.test.ts | 4 +-
packages/runtime/src/__tests__/cost.test.ts | 2 +-
.../src/__tests__/deferred-tools-backend.test.ts | 45 -
.../__tests__/history-compact-artifacts.test.ts | 167 --
.../__tests__/history-compact-checkpoint.test.ts | 20 +-
.../history-compact-mid-turn-checkpoint.test.ts | 4 +-
...-compact.test.ts => history-compaction.test.ts} | 119 +-
.../runtime/src/__tests__/memory-artifact-store.ts | 64 -
.../__tests__/mid-turn-capacity-backend.test.ts | 62 +-
.../__tests__/overflow-reactive-recovery.test.ts | 2 -
.../src/__tests__/runtime-event-read-model.test.ts | 31 +
.../runtime/src/__tests__/semantic-compact.test.ts | 1158 ----------
.../runtime/src/__tests__/session-manager.test.ts | 9 +-
.../runtime/src/__tests__/session-recap.test.ts | 73 +
.../__tests__/synthesis-cache-artifacts.test.ts | 338 ---
packages/runtime/src/active-compaction-kernel.ts | 948 --------
packages/runtime/src/active-tool-result-prune.ts | 35 -
.../runtime/src/agent-graph-supervisor-wake.ts | 26 +-
packages/runtime/src/agent-run.ts | 22 -
packages/runtime/src/ai-sdk-backend.ts | 486 +----
packages/runtime/src/ai-sdk-compaction-contract.ts | 111 +-
packages/runtime/src/ai-sdk-compaction.ts | 1188 ++--------
packages/runtime/src/ai-sdk-flow.ts | 3 +
packages/runtime/src/compaction-boundary.ts | 74 +-
packages/runtime/src/context-budget-helpers.ts | 46 +-
packages/runtime/src/context-budget-policy.ts | 302 +--
packages/runtime/src/context-budget.ts | 366 +---
packages/runtime/src/context-source-ref.ts | 117 -
packages/runtime/src/conversation-copy.ts | 8 +-
packages/runtime/src/history-compact-artifacts.ts | 262 ---
packages/runtime/src/history-compact.ts | 1024 ---------
packages/runtime/src/history-compaction.ts | 573 +++++
packages/runtime/src/memory-extraction.ts | 2 +-
packages/runtime/src/mid-turn-capacity-compact.ts | 419 ----
packages/runtime/src/model-adapter.ts | 74 +-
packages/runtime/src/model-history.ts | 3 +-
packages/runtime/src/request-projection.ts | 19 +-
.../runtime/src/runtime-event-history-search.ts | 169 --
packages/runtime/src/runtime-kernel.ts | 14 +-
packages/runtime/src/semantic-compact.ts | 1593 --------------
packages/runtime/src/session-manager.ts | 19 +-
packages/runtime/src/session-recap.ts | 34 +-
packages/runtime/src/synthesis-cache-artifacts.ts | 189 --
packages/runtime/src/synthesis-cache.ts | 778 -------
packages/runtime/src/tool-result-archive.ts | 204 +-
87 files changed, 2098 insertions(+), 15467 deletions(-)
diff --git a/apps/desktop/src/main/__tests__/app-shell-command-actions.test.ts b/apps/desktop/src/main/__tests__/app-shell-command-actions.test.ts
index cbdbbd31b..f4c98f7b6 100644
--- a/apps/desktop/src/main/__tests__/app-shell-command-actions.test.ts
+++ b/apps/desktop/src/main/__tests__/app-shell-command-actions.test.ts
@@ -19,7 +19,10 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
-import { resolveManualDiagnosticTarget } from '../../renderer/app-shell-command-actions.js';
+import {
+ contextCompactionNotice,
+ resolveManualDiagnosticTarget,
+} from '../../renderer/app-shell-command-actions.js';
test('targets manual diagnostics to the current task or new-task Host profile', () => {
assert.deepEqual(
@@ -61,3 +64,21 @@ test('targets manual diagnostics to the current task or new-task Host profile',
undefined,
);
});
+
+test('presents every successful-frame context compaction outcome', () => {
+ assert.deepEqual(contextCompactionNotice({ kind: 'compacted', checkpointId: 'checkpoint-1' }), {
+ level: 'success',
+ title: 'Context compacted',
+ description: 'Older context was replaced with a checkpoint summary.',
+ });
+ assert.deepEqual(contextCompactionNotice({ kind: 'unchanged', reason: 'already_compacted' }), {
+ level: 'info',
+ title: 'Nothing to compact',
+ description: 'The task already uses the latest checkpoint.',
+ });
+ assert.deepEqual(contextCompactionNotice({ kind: 'failed', reason: 'write_failed' }), {
+ level: 'error',
+ title: 'Compaction failed',
+ description: 'write_failed',
+ });
+});
diff --git a/apps/desktop/src/main/__tests__/history-compact-artifacts.test.ts b/apps/desktop/src/main/__tests__/history-compact-artifacts.test.ts
deleted file mode 100644
index 4c0cbfce6..000000000
--- a/apps/desktop/src/main/__tests__/history-compact-artifacts.test.ts
+++ /dev/null
@@ -1,500 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import assert from 'node:assert/strict';
-import { createHash } from 'node:crypto';
-import { mkdtemp, rm } from 'node:fs/promises';
-import { tmpdir } from 'node:os';
-import { join } from 'node:path';
-import { describe, test } from 'node:test';
-import type { RuntimeEvent } from '@maka/core/runtime-event';
-import {
- applyRuntimeEventHistoryCompact,
- buildHistoryCompactBlockFromSummary,
- type HistoryCompactBlock,
-} from '@maka/runtime/context-budget';
-import { type HistoryCompactWriteInput } from '@maka/runtime/ai-sdk-compaction-contract';
-import {
- createSqliteArtifactStore,
- type ArtifactStore,
-} from '@maka/storage';
-import {
- loadHistoryCompactBlocksFromArtifacts,
- persistHistoryCompactBlocksToArtifacts,
-} from '@maka/runtime/history-compact-artifacts';
-
-describe('desktop history compact artifact lifecycle', () => {
- test('persists archived RuntimeEvent sources and a compact block', async () => {
- await withStore(async (store) => {
- const foldedEvents = [
- textEvent('old-1', 'turn-1', 'alpha fact'),
- textEvent('old-2', 'turn-2', 'beta fact'),
- ];
- const input: HistoryCompactWriteInput = {
- sessionId: 'session-1',
- turnId: 'turn-write',
- source: {
- draftBlock: historyCompactBlock(foldedEvents, 'deterministic fallback'),
- foldedRuntimeEvents: foldedEvents,
- },
- limits: {
- maxBlocks: 1,
- maxBlockEstimatedTokens: 1_024,
- maxEstimatedTokens: 2_048,
- charsPerToken: 4,
- },
- };
-
- const createdArtifacts: string[] = [];
- const write = await persistHistoryCompactBlocksToArtifacts(store, input, {
- now: () => 1_800_000_000_100,
- summarize: () => 'host summary alpha beta',
- onArtifactCreated: (artifact) => {
- createdArtifacts.push(artifact.id);
- },
- });
-
- assert.equal(write.blocks.length, 1);
- assert.match(write.blocks[0]?.summary ?? '', /host summary alpha beta/);
- assert.equal(write.blocks[0]?.sourceArchiveRefs?.length, 2);
- assert.equal(write.blocks[0]?.sourceArchiveRefs?.[0]?.bodySha256, sha256(JSON.stringify({ kind: 'text', text: 'alpha fact' })));
- assert.equal(createdArtifacts.length, 3);
-
- const records = await store.list('session-1');
- assert.equal(records.filter((record) => record.source === 'history_compact_source').length, 2);
- assert.equal(records.filter((record) => record.source === 'history_compact_block').length, 1);
-
- const loaded = await loadHistoryCompactBlocksFromArtifacts(store, {
- sessionId: 'session-1',
- maxBlocks: 1,
- maxEstimatedTokens: 2_048,
- maxBytes: 16_384,
- });
- assert.equal(loaded.blocks.length, 1);
- assert.equal(loaded.blocks[0]?.blockId, write.blocks[0]?.blockId);
- assert.equal(loaded.skipped, undefined);
- });
- });
-
- test('aborted writes do not create a replayable compact block', async () => {
- await withStore(async (store) => {
- const foldedEvents = [
- textEvent('old-1', 'turn-1', 'alpha fact'),
- textEvent('old-2', 'turn-2', 'beta fact'),
- ];
- const controller = new AbortController();
- const input: HistoryCompactWriteInput = {
- sessionId: 'session-1',
- turnId: 'turn-write',
- source: {
- draftBlock: historyCompactBlock(foldedEvents, 'deterministic fallback'),
- foldedRuntimeEvents: foldedEvents,
- },
- limits: {
- maxBlocks: 1,
- maxBlockEstimatedTokens: 1_024,
- maxEstimatedTokens: 2_048,
- charsPerToken: 4,
- },
- abortSignal: controller.signal,
- };
-
- await assert.rejects(
- persistHistoryCompactBlocksToArtifacts(store, input, {
- now: () => 1_800_000_000_100,
- summarize: () => 'host summary alpha beta',
- onArtifactCreated: (artifact) => {
- if (artifact.source === 'history_compact_source') controller.abort();
- },
- }),
- /history compact write aborted|This operation was aborted/,
- );
-
- const liveRecords = await store.list('session-1');
- assert.equal(liveRecords.filter((record) => record.source === 'history_compact_source').length, 0);
- assert.equal(liveRecords.filter((record) => record.source === 'history_compact_block').length, 0);
- const allRecords = await store.list('session-1', { includeDeleted: true });
- assert.equal(allRecords.filter((record) => record.source === 'history_compact_source' && record.status === 'deleted').length, 1);
- });
- });
-
- test('aborting during the final compact block write removes the replayable block', async () => {
- await withStore(async (store) => {
- const foldedEvents = [
- textEvent('old-1', 'turn-1', 'alpha fact'),
- textEvent('old-2', 'turn-2', 'beta fact'),
- ];
- const controller = new AbortController();
- const input: HistoryCompactWriteInput = {
- sessionId: 'session-1',
- turnId: 'turn-write',
- source: {
- draftBlock: historyCompactBlock(foldedEvents, 'deterministic fallback'),
- foldedRuntimeEvents: foldedEvents,
- },
- limits: {
- maxBlocks: 1,
- maxBlockEstimatedTokens: 1_024,
- maxEstimatedTokens: 2_048,
- charsPerToken: 4,
- },
- abortSignal: controller.signal,
- };
-
- await assert.rejects(
- persistHistoryCompactBlocksToArtifacts({
- create: async (createInput) => {
- const artifact = await store.create(createInput);
- if (artifact.source === 'history_compact_block') controller.abort();
- return artifact;
- },
- delete: (artifactId) => store.delete(artifactId),
- }, input, {
- now: () => 1_800_000_000_100,
- summarize: () => 'host summary alpha beta',
- }),
- /history compact write aborted|This operation was aborted/,
- );
-
- const liveRecords = await store.list('session-1');
- assert.equal(liveRecords.filter((record) => record.source === 'history_compact_source').length, 0);
- assert.equal(liveRecords.filter((record) => record.source === 'history_compact_block').length, 0);
- const allRecords = await store.list('session-1', { includeDeleted: true });
- assert.equal(allRecords.filter((record) => record.source === 'history_compact_source' && record.status === 'deleted').length, 2);
- assert.equal(allRecords.filter((record) => record.source === 'history_compact_block' && record.status === 'deleted').length, 1);
- });
- });
-
- test('replayed loaded compact blocks keep persisted source archive refs', async () => {
- await withStore(async (store) => {
- const foldedEvents = [
- textEventWithReorderedContent('old-1', 'turn-1', 'alpha fact'),
- textEventWithReorderedContent('old-2', 'turn-2', 'beta fact'),
- ];
- const input: HistoryCompactWriteInput = {
- sessionId: 'session-1',
- turnId: 'turn-write',
- source: {
- draftBlock: historyCompactBlock(foldedEvents, 'deterministic fallback'),
- foldedRuntimeEvents: foldedEvents,
- },
- limits: {
- maxBlocks: 1,
- maxBlockEstimatedTokens: 1_024,
- maxEstimatedTokens: 2_048,
- charsPerToken: 4,
- },
- };
-
- const write = await persistHistoryCompactBlocksToArtifacts(store, input, {
- now: () => 1_800_000_000_100,
- summarize: () => 'host summary alpha beta',
- });
- const replay = applyRuntimeEventHistoryCompact(
- [
- ...foldedEvents,
- textEvent('recent', 'turn-3', 'recent retained fact'),
- ],
- {
- historyCompact: {
- enabled: true,
- mode: 'lookup',
- highWaterRatio: 0.000001,
- tailEstimatedTokens: 1,
- minRecentTurns: 1,
- archiveRequired: true,
- blocks: write.blocks,
- },
- charsPerToken: 4,
- maxHistoryEstimatedTokens: 2_048,
- },
- );
-
- assert.equal(replay.blocks.length, 1);
- assert.equal(replay.blocks[0]?.sourceArchiveRefs?.length, 2);
- assert.equal(replay.diagnosticPatch.historyCompactSkipped, undefined);
- });
- });
-
- test('does not leave source artifacts when the compact block exceeds the block token limit', async () => {
- await withStore(async (store) => {
- const foldedEvents = [
- textEvent('old-1', 'turn-1', 'alpha fact'),
- textEvent('old-2', 'turn-2', 'beta fact'),
- ];
- const input: HistoryCompactWriteInput = {
- sessionId: 'session-1',
- turnId: 'turn-write',
- source: {
- draftBlock: historyCompactBlock(foldedEvents, 'deterministic fallback'),
- foldedRuntimeEvents: foldedEvents,
- },
- limits: {
- maxBlocks: 1,
- maxBlockEstimatedTokens: 1,
- maxEstimatedTokens: 2_048,
- charsPerToken: 4,
- },
- };
-
- const createdArtifacts: string[] = [];
- const write = await persistHistoryCompactBlocksToArtifacts(store, input, {
- now: () => 1_800_000_000_100,
- summarize: () => 'summary that cannot fit in one estimated token',
- onArtifactCreated: (artifact) => {
- createdArtifacts.push(artifact.id);
- },
- });
-
- assert.equal(write.blocks.length, 0);
- assert.equal(write.skipped, 1);
- assert.deepEqual(write.skippedReasonCounts, { max_block_tokens: 1 });
- assert.deepEqual(createdArtifacts, []);
- assert.deepEqual(await store.list('session-1'), []);
- });
- });
-
- test('does not leave source artifacts when the compact block exceeds the total token limit', async () => {
- await withStore(async (store) => {
- const foldedEvents = [
- textEvent('old-1', 'turn-1', 'alpha fact'),
- textEvent('old-2', 'turn-2', 'beta fact'),
- ];
- const input: HistoryCompactWriteInput = {
- sessionId: 'session-1',
- turnId: 'turn-write',
- source: {
- draftBlock: historyCompactBlock(foldedEvents, 'deterministic fallback'),
- foldedRuntimeEvents: foldedEvents,
- },
- limits: {
- maxBlocks: 1,
- maxBlockEstimatedTokens: 1_024,
- maxEstimatedTokens: 1,
- charsPerToken: 4,
- },
- };
-
- const createdArtifacts: string[] = [];
- const write = await persistHistoryCompactBlocksToArtifacts(store, input, {
- now: () => 1_800_000_000_100,
- summarize: () => 'summary that cannot fit in one estimated token',
- onArtifactCreated: (artifact) => {
- createdArtifacts.push(artifact.id);
- },
- });
-
- assert.equal(write.blocks.length, 0);
- assert.equal(write.skipped, 1);
- assert.deepEqual(write.skippedReasonCounts, { max_total_tokens: 1 });
- assert.deepEqual(createdArtifacts, []);
- assert.deepEqual(await store.list('session-1'), []);
- });
- });
-
- test('rejects deleted, wrong-source, wrong-session, malformed, wrong-version, and oversized blocks, and ignores deleted source artifacts', async () => {
- await withStore(async (store) => {
- await store.create({
- id: 'valid',
- sessionId: 'session-1',
- turnId: 'turn-1',
- name: 'valid.json',
- kind: 'file',
- content: JSON.stringify(historyCompactBlock([textEvent('old-1', 'turn-1', 'alpha')], 'valid')),
- mimeType: 'application/json',
- source: 'history_compact_block',
- now: 100,
- });
- await store.create({
- id: 'wrong-source',
- sessionId: 'session-1',
- turnId: 'turn-1',
- name: 'wrong-source.json',
- kind: 'file',
- content: JSON.stringify(historyCompactBlock([textEvent('old-2', 'turn-2', 'beta')], 'wrong-source')),
- mimeType: 'application/json',
- source: 'tool_result_archive',
- now: 110,
- });
- await store.create({
- id: 'wrong-session',
- sessionId: 'session-1',
- turnId: 'turn-1',
- name: 'wrong-session.json',
- kind: 'file',
- content: JSON.stringify({
- ...historyCompactBlock([textEvent('old-3', 'turn-3', 'gamma')], 'wrong-session'),
- sessionId: 'session-other',
- }),
- mimeType: 'application/json',
- source: 'history_compact_block',
- now: 120,
- });
- await store.create({
- id: 'invalid-json',
- sessionId: 'session-1',
- turnId: 'turn-1',
- name: 'invalid-json.json',
- kind: 'file',
- content: '{not-json',
- mimeType: 'application/json',
- source: 'history_compact_block',
- now: 130,
- });
- await store.create({
- id: 'wrong-version',
- sessionId: 'session-1',
- turnId: 'turn-1',
- name: 'wrong-version.json',
- kind: 'file',
- content: JSON.stringify({ ...historyCompactBlock([textEvent('old-4', 'turn-4', 'delta')], 'wrong-version'), version: 2 }),
- mimeType: 'application/json',
- source: 'history_compact_block',
- now: 140,
- });
- await store.create({
- id: 'bad-estimate',
- sessionId: 'session-1',
- turnId: 'turn-1',
- name: 'bad-estimate.json',
- kind: 'file',
- content: JSON.stringify({
- ...historyCompactBlock([textEvent('old-7', 'turn-7', 'eta')], 'bad-estimate'),
- estimatedTokens: 'tiny',
- }),
- mimeType: 'application/json',
- source: 'history_compact_block',
- now: 145,
- });
- await store.create({
- id: 'oversized',
- sessionId: 'session-1',
- turnId: 'turn-1',
- name: 'oversized.json',
- kind: 'file',
- content: JSON.stringify({
- ...historyCompactBlock([textEvent('old-5', 'turn-5', 'epsilon')], 'oversized'),
- // impl recomputes tokens from the rendered text and deliberately
- // ignores the persisted estimate, so force an oversized render to
- // exceed maxEstimatedTokens and trip max_total_tokens.
- summary: 'oversized '.repeat(500),
- }),
- mimeType: 'application/json',
- source: 'history_compact_block',
- now: 150,
- });
- await store.create({
- id: 'deleted',
- sessionId: 'session-1',
- turnId: 'turn-1',
- name: 'deleted.json',
- kind: 'file',
- content: JSON.stringify(historyCompactBlock([textEvent('old-6', 'turn-6', 'zeta')], 'deleted')),
- mimeType: 'application/json',
- source: 'history_compact_block',
- now: 160,
- });
- await store.delete('deleted');
- // A deleted history_compact_source (left behind by an aborted write)
- // must NOT inflate the compact-block loader's deleted skip count.
- await store.create({
- id: 'deleted-source',
- sessionId: 'session-1',
- turnId: 'turn-1',
- name: 'history-compact-source-deleted.json',
- kind: 'file',
- content: '{}',
- mimeType: 'application/json',
- source: 'history_compact_source',
- now: 165,
- });
- await store.delete('deleted-source');
-
- const loaded = await loadHistoryCompactBlocksFromArtifacts(store, {
- sessionId: 'session-1',
- maxBlocks: 10,
- maxEstimatedTokens: 1_000,
- maxBytes: 20_000,
- });
-
- assert.deepEqual(loaded.blocks.map((block) => block.summary), ['valid']);
- assert.equal(loaded.skippedReasonCounts?.deleted, 1);
- assert.equal(loaded.skippedReasonCounts?.session_mismatch, 1);
- assert.equal(loaded.skippedReasonCounts?.invalid_json, 1);
- assert.equal(loaded.skippedReasonCounts?.invalid_schema_version, 2);
- assert.equal(loaded.skippedReasonCounts?.max_total_tokens, 1);
- });
- });
-});
-
-async function withStore(
- fn: (store: ArtifactStore, workspaceRoot: string) => Promise<void>,
-): Promise<void> {
- const workspaceRoot = await mkdtemp(join(tmpdir(), 'maka-history-compact-artifacts-'));
- try {
- await fn(createSqliteArtifactStore(workspaceRoot), workspaceRoot);
- } finally {
- await rm(workspaceRoot, { recursive: true, force: true });
- }
-}
-
-function textEvent(id: string, turnId: string, text: string): RuntimeEvent {
- return {
- id,
- sessionId: 'session-1',
- runId: 'run-1',
- turnId,
- invocationId: 'invocation-1',
- ts: 1_800_000_000_000,
- partial: false,
- role: 'user',
- author: 'user',
- content: { kind: 'text', text },
- };
-}
-
-function textEventWithReorderedContent(id: string, turnId: string, text: string): RuntimeEvent {
- return {
- ...textEvent(id, turnId, text),
- content: { text, kind: 'text' } as RuntimeEvent['content'],
- };
-}
-
-function historyCompactBlock(
- events: RuntimeEvent[],
- summary: string,
- overrides: Partial<HistoryCompactBlock> = {},
-): HistoryCompactBlock {
- return {
- ...buildHistoryCompactBlockFromSummary({
- sessionId: 'session-1',
- foldedRuntimeEvents: events,
- summary,
- highWaterName: 'test-history-compact',
- highWaterSeq: 1,
- now: 1_800_000_000_000,
- charsPerToken: 4,
- }),
- ...overrides,
- };
-}
-
-function sha256(text: string): string {
- return createHash('sha256').update(text).digest('hex');
-}
diff --git a/apps/desktop/src/main/runtime-host-session-execution-ipc-main.ts b/apps/desktop/src/main/runtime-host-session-execution-ipc-main.ts
index 09fda5765..f513bf8be 100644
--- a/apps/desktop/src/main/runtime-host-session-execution-ipc-main.ts
+++ b/apps/desktop/src/main/runtime-host-session-execution-ipc-main.ts
@@ -506,8 +506,9 @@ export function registerRuntimeHostSessionExecutionIpc(
ipcMain.handle("sessions:compact", async (_event, sessionId: string) => {
const turnId = newId();
- await deps.client.compactContext({ sessionId, turnId });
+ const result = await deps.client.compactContext({ sessionId, turnId });
deps.emitSessionsChanged("status-change", sessionId, { turnId });
+ return result;
});
ipcMain.handle("sessions:resumeLatest", async (_event, sessionId: string) => {
const plan = await deps.client.queryTurnResume({ sessionId });
diff --git a/apps/desktop/src/preload/bridge-contract.d.ts b/apps/desktop/src/preload/bridge-contract.d.ts
index 7175f8e7c..d309922dd 100644
--- a/apps/desktop/src/preload/bridge-contract.d.ts
+++ b/apps/desktop/src/preload/bridge-contract.d.ts
@@ -742,7 +742,7 @@ export interface MakaBridge {
): () => void;
listTurns(sessionId: string): Promise<TurnRecord[]>;
listTurnLandmarks(sessionId: string): Promise<OperationOutput<'session.turn_landmarks.query'>>;
- compact(sessionId: string): Promise<void>;
+ compact(sessionId: string): Promise<OperationOutput<'context.compact'>>;
resumeLatest(sessionId: string): Promise<
| { disposition: 'started'; runId: string; turnId: string }
| { disposition: 'park'; rejectionReasons: string[]; diagnostics: unknown[] }
diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts
index a37f9cf99..c91408216 100644
--- a/apps/desktop/src/preload/preload.ts
+++ b/apps/desktop/src/preload/preload.ts
@@ -1491,7 +1491,7 @@ const makaBridge = {
}
return send(command);
},
- compact(sessionId: string): Promise<void> {
+ compact(sessionId: string): Promise<OperationOutput<'context.compact'>> {
return invokeSessionRuntimeHost('sessions:compact', sessionId);
},
resumeLatest(sessionId: string): Promise<
diff --git a/apps/desktop/src/renderer/app-shell-command-actions.ts b/apps/desktop/src/renderer/app-shell-command-actions.ts
index 1ea78b8dc..c2e2c85de 100644
--- a/apps/desktop/src/renderer/app-shell-command-actions.ts
+++ b/apps/desktop/src/renderer/app-shell-command-actions.ts
@@ -19,6 +19,7 @@
import { useMemo, useRef } from "react";
import type { DailyReviewSummary } from '@maka/core/daily-review';
+import type { ContextCompactionOutcome } from '@maka/core/events';
import type { LlmConnection } from '@maka/core/llm-connections';
import type { PermissionMode } from '@maka/core/permission';
import type { SessionStartMode } from '@maka/core/explore-agent';
@@ -58,6 +59,28 @@ type ToastApi = {
): void;
};
+export function contextCompactionNotice(
+ outcome: ContextCompactionOutcome,
+ uiLocale: UiLocale = 'en',
+): { level: 'success' | 'info' | 'error'; title: string; description: string } {
+ const copy = getShellCopy(uiLocale).app;
+ if (outcome.kind === 'compacted') {
+ return {
+ level: 'success',
+ title: copy.compactSuccessTitle,
+ description: copy.compactSuccessDescription,
+ };
+ }
+ if (outcome.kind === 'unchanged') {
+ return {
+ level: 'info',
+ title: copy.compactUnchangedTitle,
+ description: copy.compactUnchangedDescription,
+ };
+ }
+ return { level: 'error', title: copy.compactErrorTitle, description: outcome.reason };
+}
+
type ComposerImportOwner = {
sessionId: string | undefined;
navSection: NavSelection["section"];
diff --git a/apps/desktop/src/renderer/app-shell.tsx b/apps/desktop/src/renderer/app-shell.tsx
index bcdfc6644..64c8b26f5 100644
--- a/apps/desktop/src/renderer/app-shell.tsx
+++ b/apps/desktop/src/renderer/app-shell.tsx
@@ -146,7 +146,10 @@ import {
readSessionListViewMode,
} from './session-list-layout';
import { modelSetupToastCopy } from './model-connection-errors';
-import type { AppShellCommandListOptions } from './app-shell-command-actions';
+import {
+ contextCompactionNotice,
+ type AppShellCommandListOptions,
+} from './app-shell-command-actions';
import { AppShellTopbarActions } from './app-shell-chrome-actions';
import { updateReminderFromStatus } from './app-shell-app-update';
import { useBuildStamp } from './app-shell-build-stamp';
@@ -2048,7 +2051,15 @@ function AppShellContent({
const sessionId = activeIdRef.current;
if (!sessionId) return true;
try {
- await window.maka.sessions.compact(sessionId);
+ const result = await window.maka.sessions.compact(sessionId);
+ if (result.kind === 'finished') {
+ const notice = contextCompactionNotice(result.outcome, uiLocale);
+ if (notice.level === 'error') {
+ showSessionError(sessionId, notice.title, notice.description);
+ return false;
+ }
+ toastApi[notice.level](notice.title, notice.description);
+ }
return true;
} catch (error) {
if (activeIdRef.current !== sessionId) return false;
diff --git a/apps/desktop/src/renderer/locales/conversation-copy.ts b/apps/desktop/src/renderer/locales/conversation-copy.ts
index 17cafea2f..ee1efd980 100644
--- a/apps/desktop/src/renderer/locales/conversation-copy.ts
+++ b/apps/desktop/src/renderer/locales/conversation-copy.ts
@@ -328,8 +328,8 @@ export interface DesktopConversationCopy {
*
* The call-kind tables are typed against the core union, so a kind added to the
* runtime fails this file at compile time instead of reaching a Chinese panel
- * as `daily_review`. The runtime fallthrough stays for data written by an older
- * schema, which the type system cannot reach.
+ * as `daily_review`. The table also labels decode-only historical kinds such as
+ * `semantic_compact`; the Runtime no longer emits them.
*/
type CallKindCopy = Record<Exclude<ModelCallKind, 'main'>, string>;
diff --git a/apps/desktop/src/renderer/locales/shell-copy.ts b/apps/desktop/src/renderer/locales/shell-copy.ts
index 16a1c8905..5211ea17e 100644
--- a/apps/desktop/src/renderer/locales/shell-copy.ts
+++ b/apps/desktop/src/renderer/locales/shell-copy.ts
@@ -437,6 +437,10 @@ type ShellCopy = {
loadingWorkbar: string;
useSkillPrompt(skillName: string): string;
newConversation: string;
+ compactSuccessTitle: string;
+ compactSuccessDescription: string;
+ compactUnchangedTitle: string;
+ compactUnchangedDescription: string;
compactErrorTitle: string;
compactErrorFallback: string;
slashCommands: Record<SlashCommandIdForSurface<'desktop'>, {
@@ -1175,6 +1179,10 @@ const SHELL_COPY_BY_LOCALE = {
loadingWorkbar: '正在加载任务工作栏…',
useSkillPrompt: (skillName: string) => `使用 ${skillName} 技能:`,
newConversation: '新建任务',
+ compactSuccessTitle: '上下文已压缩',
+ compactSuccessDescription: '较早的上下文已替换为检查点摘要。',
+ compactUnchangedTitle: '无需压缩',
+ compactUnchangedDescription: '任务已使用最新的检查点。',
compactErrorTitle: '压缩失败',
compactErrorFallback: '任务暂时无法压缩,请稍后重试。',
slashCommands: {
@@ -1734,6 +1742,10 @@ const SHELL_COPY_BY_LOCALE = {
loadingWorkbar: 'Loading task workbar…',
useSkillPrompt: (skillName: string) => `Use the ${skillName} skill: `,
newConversation: 'New task',
+ compactSuccessTitle: 'Context compacted',
+ compactSuccessDescription: 'Older context was replaced with a checkpoint summary.',
+ compactUnchangedTitle: 'Nothing to compact',
+ compactUnchangedDescription: 'The task already uses the latest checkpoint.',
compactErrorTitle: 'Compaction failed',
compactErrorFallback: 'The task could not be compacted. Try again later.',
slashCommands: {
diff --git a/docs/architecture/llm-compaction-events-log-projection-draft.md b/docs/architecture/llm-compaction-events-log-projection-draft.md
index 1b60345c2..6c0d696dd 100644
--- a/docs/architecture/llm-compaction-events-log-projection-draft.md
+++ b/docs/architecture/llm-compaction-events-log-projection-draft.md
@@ -36,9 +36,9 @@ owners:
This chapter builds on Chapter 1's log-first Runtime and Chapter 2's distinction between compressing context and compressing evidence. It is for Runtime engineers changing history compaction, context budgets, checkpoint persistence, or recovery. The first half establishes the mental model. The complete chapter should let a reader locate text-summary and provider-native checkpoint generation, validation, rolling updates, replay, and failure recovery.
-The primary subject is **prior-history LLM compaction**: a compactor produces either a continuation summary or provider-native compact state, the checkpoint covers a prefix of older RuntimeEvents, and later requests use that projection in place of the prefix. The chapter does not fully cover active or stale pruning of individual Tool Results, and it does not treat current-Turn `semanticCompact` as the same mechanism. Both reduce provider messages, but their durable sources, lifecycles, a [...]
+The primary subject is **RuntimeEvent history compaction**: a compactor produces either a continuation summary or provider-native compact state, the checkpoint covers a safe prefix of RuntimeEvents, and later requests use that projection in place of the prefix. The same planner and checkpoint transaction serve manual, pre-turn, mid-turn, and overflow triggers. The chapter does not fully cover active or stale pruning of individual Tool Results; those reduce provider messages without creat [...]
-This chapter describes the implementation current as of 2026-08-14. The V1 artifact-backed `HistoryCompactBlock` remains as a read-only compatibility path. Ledger-backed checkpoints use schema V2 for text summaries and schema V3 for provider-native state. OpenAI Codex subscription models use Codex remote compaction V2 by default; other providers retain text-summary behavior.
+This chapter describes the implementation current as of 2026-08-23. Ledger-backed checkpoints use schema V2 for text summaries and schema V3 for provider-native state. OpenAI Codex subscription models use Codex remote compaction V2 by default; other providers retain text-summary behavior.
## Start with a long-running Session
@@ -112,7 +112,7 @@ flowchart LR
L --> R["Replay / audit / new projection"]
```
-Read the diagram left to right. A successful compaction does not rewrite the log on the left. The checkpoint and raw tail in the middle jointly form the historical prefix for the next request. The lower branch shows that a debugger, history search, or future compactor can still consume the same log. The diagram omits the system prompt, tool schemas, and current user message. They also shape the final request, but they are outside history-compaction source coverage.
+Read the diagram left to right. A successful compaction does not rewrite the log on the left. The checkpoint and raw tail in the middle jointly form the historical prefix for the next request. The lower branch shows that a debugger or future compactor can still consume the same log. The diagram omits the system prompt, tool schemas, and current user message. They also shape the final request, but they are outside history-compaction source coverage.
In database terms, the checkpoint is closer to a materialized view or snapshot than to WAL truncation. It can accelerate reads, carry a version, become invalid, and be rebuilt from its source log. It cannot declare the source log obsolete.
@@ -161,14 +161,13 @@ The prior-history path for a normal Send begins in `AiSdkBackend.buildPriorMessa
1. Exclude the current `turnId` to obtain the prior Runtime context.
2. Prepare the context-budget policy.
-3. Load the latest ledger-backed checkpoint, falling back to V1 blocks only when needed.
+3. Load the latest compatible ledger-backed checkpoint.
4. Handle stale oversized Tool Results first.
5. Calculate the history-compaction high water and retained tail.
6. Validate whether an existing checkpoint exactly matches the source prefix.
7. If the old checkpoint does not cover the new fold, call an LLM to create a rolling successor.
8. Project a V2 checkpoint as a synthetic text RuntimeEvent, or carry a V3 checkpoint as explicit projection metadata, then append the uncovered raw tail.
-9. Apply history search, archive retrieval, or synthesis cache when configured.
-10. Build the provider replay plan and only then materialize `ModelMessage[]`.
+9. Build the provider replay plan and only then materialize `ModelMessage[]`.
The ordering reveals two properties.
@@ -281,7 +280,7 @@ sequenceDiagram
B->>P: synthetic checkpoint event + raw tail
```
-Read this diagram from top to bottom. The critical commit point is `history_compact_checkpoint_recorded`. A new summary enters the same provider request as a replacement checkpoint only after the durable recorder succeeds. The diagram omits V1 artifact compatibility and final replay-plan materialization.
+Read this diagram from top to bottom. The critical commit point is `history_compact_checkpoint_recorded`. A new summary enters the same provider request as a replacement checkpoint only after the durable recorder succeeds. The diagram omits final replay-plan materialization.
Rolling does not mean a summary can only advance forever. The same coverage may be explicitly rewritten, but the candidate must name the current checkpoint in `previousCheckpointId` and preserve the same source digest, through boundary, and Turn/Event counts. This is a compare-and-swap rewrite of one materialized view, not permission for an arbitrary late writer to overwrite it.
@@ -404,7 +403,7 @@ The correct interpretation is:
> Fail open to a safe source-derived context, not to an invented summary.
-The model may temporarily see fewer old details, but the source log remains intact. Maka can compact again, use history search, read archives, or build a new projection under another policy later.
+The model may temporarily see fewer old details, but the source log remains intact. Maka can compact again or build a new projection under another policy later.
## Manual compaction is also a Runtime operation
@@ -419,48 +418,33 @@ Desktop `sessions:compact` does not silently rewrite a database. It creates a Tu
The manual policy lowers high water to almost zero and reduces the retained-tail target so even a small history can produce a projection. It changes the trigger, not source, coverage, durability, or replay invariants.
-## V1, V2, and V3: bounded checkpoints with two projection values
+## V2 and V3: one bounded checkpoint with two projection values
-The older V1 `HistoryCompactBlock` stores a `sourceRef` for every folded RuntimeEvent and may create a separate artifact for each source event. Its provenance is explicit, but in real long Sessions the block JSON grows linearly with event count and can reach multiple megabytes. The projection itself is no longer bounded.
-
-V2 `HistoryCompactCheckpoint` replaces that fan-out with fixed-size prefix metadata: Event/Turn counts, a through boundary, and one digest over the complete ordered prefix. The canonical ledger already preserves the original RuntimeEvents; a second fan-out JSON copy is not required to prove source identity.
+V2 `HistoryCompactCheckpoint` uses fixed-size prefix metadata: Event/Turn counts, a through boundary, and one digest over the complete ordered prefix. The canonical ledger already preserves the original RuntimeEvents; a second fan-out JSON copy is not required to prove source identity.
V3 keeps the same bounded source, coverage, lineage, durability, and replay checks. It changes only the projection value from a text summary to a closed provider-state variant. The union currently contains one variant, `openai_codex_remote_v2`; adding another provider requires a concrete validated variant, not an arbitrary JSON registry.
The current state is:
-- schema V2 and V3 checkpoints load first from the AgentRun ledger and its bounded projection;
-- V1 artifact blocks are read only as a compatibility fallback;
-- after a ledger-backed checkpoint validates coverage over the same prefix, Runtime may validate and reclaim the corresponding legacy block/source artifacts;
-- cleanup is reclaim-only and cannot affect replay on failure.
+- schema V2 and V3 checkpoints load from the AgentRun ledger and their bounded projection;
+- the latest compatible durable checkpoint is the only checkpoint authority;
+- V1 block/source artifact paths have been removed rather than retained as a second representation;
+- a failed generation, validation, or append leaves the RuntimeEvent ledger unchanged.
This evolution does not reduce provenance. It puts provenance at the correct layer: the RuntimeEvent ledger preserves source facts, while the checkpoint preserves only the bounded identity required to validate a source prefix.
-## Do not collapse three kinds of “compact” into one feature
+## One compaction mechanism, one adjacent prune
-Maka currently has at least three context-reduction mechanisms:
+Maka has one LLM compaction mechanism and one adjacent current-request rewrite:
| Mechanism | Source | When it runs | Durable result | Role in this chapter |
|---|---|---|---|---|
-| History LLM compaction | Older RuntimeEvent prefix | Between Turns, while building prior history | Schema V2 or V3 checkpoint recorded in the AgentRun event ledger | Primary subject |
+| History LLM compaction | Safe RuntimeEvent prefix | Manual request, pre-turn capacity, active-turn capacity, or provider overflow | Schema V2 or V3 checkpoint recorded in the AgentRun event ledger | Primary subject |
| Active Tool Result Prune | Provider-visible Tool Result in the current Turn | Before the next step in the same Turn | Raw result is archived first; placeholder changes only current messages | Primary subject of Chapter 2 |
-| `semanticCompact` | The completed middle span after the current-user head anchor and before the open protocol tail, plus available Turn RuntimeEvent refs | During `prepareStep` | V2 `SemanticCompactBlock` is a best-effort diagnostic record; accepted messages live in the current backend projection | Attention shaping; adjacent, not equivalent to V2 checkpoint |
-
-They share the direction “do not mutate canonical source,” but their guarantees are not identical.
-
-Current `semanticCompact` treats the original current-user message as an immutable head anchor. The Runtime selects only a contiguous completed span after that anchor, grouping complete assistant steps with all corresponding tool call/result pairs. The newest completed execution episode and the first incomplete provider episode plus everything after it remain an exact tail. The LLM emits only a bounded continuation delta: established findings, decisions, failed paths, partial work produc [...]
-
-The exact head anchor remains a `user` message. The LLM-authored continuation projection is rendered as an `assistant` message, because it represents the model's own compressed execution history rather than a new user instruction. This avoids turning `head anchor + projection` into two consecutive user messages, which can cause OpenAI-compatible models to restart task discovery. No runtime-inferred state card is injected into this message.
-
-Active tool-result pruning also preserves the complete results of the newest completed provider step. `prepareStep` runs before the first request that can expose those results to the model, so pruning them there would archive evidence before the model had ever consumed it. They become eligible only after a newer step completes; semantic compaction may still summarize the fresh result because its LLM projection carries the selected evidence forward.
-
-Once a semantic projection has been accepted, later active pruning is only a representation change in its already-covered source. The Runtime matches a raw Tool Result and its archive placeholder by stable lineage identity (tool call, tool name, payload shape, and the original body hash), rather than requiring their provider-visible message bytes to remain equal. Every other source field remains exact-match. This keeps the accepted projection alive across pruning without allowing unrelat [...]
-
-The provider-visible continuation contains only the exact head anchor, the LLM-authored continuation delta, the newest completed execution episode, and the exact open protocol tail. Runtime-inferred state cards are not generated or rendered by `semanticCompact`. Coverage, source refs, archive refs, and preserved-tail indexes remain diagnostic side-channel data on `SemanticCompactBlock`; they are not injected into the continuation. When rolling forward from a legacy block, the Runtime re- [...]
-This mechanism deliberately pays for a prefix rewrite to shape model attention. Signed token savings, compact-call cost, and cache misses are diagnostics rather than rejection gates in attention mode. Source/head/protocol validation, non-truncated output, and the complete provider-visible projection budget are hard acceptance gates. A V2 block records the exact head identity, predecessor, new coverage, and cumulative digest, but recording remains best-effort: recorder failure does not ro [...]
+Both preserve canonical source, but they do not create parallel compaction authorities. History compaction always selects a safe RuntimeEvent prefix, generates and validates one replacement, then persists one checkpoint before replay. Trigger-specific code may pin the live head or reserve a verbatim tail; it does not own another planner, summary format, controller, or durable block.
-If all LLM compaction is eventually unified under the “Events Log projection” model, the important work is not sharing one summarization prompt. Active-loop sources would need complete, stable, verifiable event coverage and an explicit durability contract. That is an architectural direction, not a claim about the current implementation.
+Active Tool Result Prune remains a deterministic non-LLM rewrite. It archives an eligible raw Tool Result before replacing that result in the current provider request. It neither summarizes a span nor creates a checkpoint, and the later history-compaction planner reads the canonical RuntimeEvents rather than treating prune placeholders as source authority.
## What compaction is not
@@ -525,20 +509,18 @@ Read the current implementation from these locations:
4. `packages/runtime/src/ai-sdk-backend.ts`: request-projection pipeline, manual compaction, writes, and fallback semantics;
5. `packages/runtime/src/agent-run.ts`: durable `history_compact_checkpoint_recorded` event;
6. `packages/runtime/src/history-compact-ledger.ts`: bounded-projection lookup, ledger recovery, and checkpoint selection;
-7. `packages/runtime/src/runtime-kernel.ts`: serialized checkpoint writes, cache, and legacy-cleanup scheduling;
+7. `packages/runtime/src/runtime-kernel.ts`: serialized checkpoint writes and manual-compaction lifecycle;
8. `packages/storage/src/agent-run-store.ts`: commit ordering between canonical append and bounded event projection;
-9. `packages/runtime/src/history-compact-artifacts.ts`: V1 artifact-backed compatibility path;
-10. `packages/runtime/src/history-compact-cleanup.ts`: legacy reclaim after V2 coverage validation;
-11. `packages/runtime/src/context-budget-policy.ts`: default budgets, environment controls, and the manual lookup overlay;
-12. `packages/runtime/src/openai-codex-history-compactor.ts`: Codex compact-output validation and rolling provider-state input;
-13. `packages/runtime-host/src/server/execution-model-composition.ts`: default provider-specific compactor selection.
+9. `packages/runtime/src/context-budget-policy.ts`: default budgets, environment controls, and the manual lookup overlay;
+10. `packages/runtime/src/openai-codex-history-compactor.ts`: Codex compact-output validation and rolling provider-state input;
+11. `packages/runtime-host/src/server/execution-model-composition.ts`: default provider-specific compactor selection.
Important tests include:
- `history-compact-checkpoint.test.ts`: bounded 10K-event coverage, prefix digest, ledger recovery, and policy replay;
- `history-compact-summarizer.test.ts`: Tool-bearing summarizer input, failure, and rolling updates;
- `context-budget.test.ts`: high water, tail cap, Tool pair preservation, archive gates, and loaded blocks;
-- `ai-sdk-backend.test.ts`: same-request replacement, checkpoint reuse, fail-open, manual compaction, and V1 fallback;
+- `ai-sdk-backend.test.ts`: same-request replacement, checkpoint reuse, fail-open, and manual compaction;
- `session-manager.test.ts`: manual-compaction Run lifecycle, stop, and concurrency;
- `agent-run-store.test.ts`: atomic ordering and repair safety for canonical events and bounded projections.
diff --git a/docs/architecture/llm-compaction-events-log-projection-draft.zh-CN.md b/docs/architecture/llm-compaction-events-log-projection-draft.zh-CN.md
index fa7df562f..920d43f92 100644
--- a/docs/architecture/llm-compaction-events-log-projection-draft.zh-CN.md
+++ b/docs/architecture/llm-compaction-events-log-projection-draft.zh-CN.md
@@ -36,9 +36,9 @@ owners:
本文承接第一章的 log-first Runtime,也承接第二章“压缩上下文、不要压缩证据”的区分。本文面向需要修改 history compaction、上下文预算、checkpoint 持久化或恢复链路的 Runtime 工程师。读完前半部分,读者应该能建立正确心智模型;读完整章,应该能定位文本摘要与 provider-native checkpoint 的生成、校验、滚动更新、回放与故障恢复路径。
-本文主要讨论 **prior-history LLM compaction**:compactor 生成 continuation summary 或 provider-native compact state,checkpoint 覆盖若干旧 RuntimeEvents,并在以后请求中用该投影替代前缀。它不完整展开单个 Tool Result 的 active/stale prune,也不把当前 Turn 内的 `semanticCompact` 当作同一机制。二者都会缩小 provider messages,但 durable source、生命周期和安全边界不同,后文会专门区分。
+本文主要讨论 **RuntimeEvent history compaction**:compactor 生成 continuation summary 或 provider-native compact state,checkpoint 覆盖一段安全的 RuntimeEvent 前缀,并在以后请求中用该投影替代前缀。手动、pre-turn、mid-turn 与 overflow 触发器共用同一个 planner 和 checkpoint transaction。本文不完整展开单个 Tool Result 的 active/stale prune;它们会缩小 provider messages,但不会形成另一套 LLM compaction 机制。
-本文描述截至 2026-08-14 的当前实现。V1 artifact-backed `HistoryCompactBlock` 仍作为只读兼容路径存在;ledger-backed checkpoint 中,schema V2 保存文本摘要,schema V3 保存 provider-native state。OpenAI Codex 订阅模型默认使用 Codex remote compaction V2,其他 provider 维持文本摘要行为。
+本文描述截至 2026-08-23 的当前实现。ledger-backed checkpoint 中,schema V2 保存文本摘要,schema V3 保存 provider-native state。OpenAI Codex 订阅模型默认使用 Codex remote compaction V2,其他 provider 维持文本摘要行为。
## 从一个长期会话开始
@@ -161,14 +161,13 @@ V2 中模型主要看到 `summary`;V3 中 provider 看到自己的 opaque comp
1. 排除当前 `turnId`,得到 prior Runtime context;
2. 准备 context budget policy;
-3. 加载最新 ledger-backed checkpoint,必要时才回退到 V1 blocks;
+3. 加载最新且兼容的 ledger-backed checkpoint;
4. 先处理 stale oversized tool results;
5. 计算 history compact 的 high-water 与 retained tail;
6. 校验已有 checkpoint 是否恰好匹配 source prefix;
7. 如果旧 checkpoint 不足以覆盖新的 fold,调用 LLM 滚动生成 successor;
8. V2 checkpoint 投影为 synthetic text RuntimeEvent;V3 checkpoint 则作为显式 projection metadata 传递,然后拼接未覆盖 raw tail;
-9. 在需要时做 history search、archive retrieval 或 synthesis cache;
-10. 建立 provider replay plan,最后才物化成 `ModelMessage[]`。
+9. 建立 provider replay plan,最后才物化成 `ModelMessage[]`。
这条顺序说明两件事。
@@ -281,7 +280,7 @@ sequenceDiagram
B->>P: synthetic checkpoint event + raw tail
```
-这张图从上向下读,关键提交点是 `history_compact_checkpoint_recorded`。新 summary 只有在 durable recorder 成功之后,才会作为 replacement checkpoint 进入同一次 provider request。图中省略了 V1 artifact compatibility 与后续 replay-plan materialization。
+这张图从上向下读,关键提交点是 `history_compact_checkpoint_recorded`。新 summary 只有在 durable recorder 成功之后,才会作为 replacement checkpoint 进入同一次 provider request。图中省略了后续 replay-plan materialization。
Rolling 并不意味着 summary 永远只进不退。同一 coverage 可以被明确重写,但 candidate 必须把当前 checkpoint 放在 `previousCheckpointId`,并保持相同 source digest、through boundary 与 Turn/Event 计数。这相当于对同一 materialized view 做 compare-and-swap,而不是让任意迟到写入覆盖它。
@@ -404,7 +403,7 @@ Compaction 跨越 token estimation、LLM call、schema construction、durable ap
> Fail open to a safe source-derived context, not to an invented summary.
-模型可能暂时少看到一些旧细节,但 source log 没有丢。以后可以重新 compact、history search、读取 archive,或在不同 policy 下生成新 projection。
+模型可能暂时少看到一些旧细节,但 source log 没有丢。以后可以重新 compact,或在不同 policy 下生成新 projection。
## Manual compaction 也是一次 Runtime operation
@@ -419,48 +418,33 @@ Compaction 跨越 token estimation、LLM call、schema construction、durable ap
Manual policy 把 high-water 降到接近零并把 retained tail 目标降到最小,使小历史也能主动产生 projection。它改变触发条件,不改变 source、coverage、durability 和 replay invariants。
-## V1、V2 与 V3:两种 projection value 共用 bounded checkpoint
+## V2 与 V3:一种 bounded checkpoint、两种 projection value
-旧 V1 `HistoryCompactBlock` 为每条 folded RuntimeEvent 保存 `sourceRefs`,还可以为每个 source event 创建独立 artifact。它的优点是 provenance 显式,但真实长 Session 中 block JSON 会随 event count 线性膨胀,甚至达到数 MB;projection 自身不再 bounded。
-
-V2 `HistoryCompactCheckpoint` 改成固定大小的 prefix metadata:event/Turn count、through boundary 和一个覆盖完整有序前缀的 digest。原始 RuntimeEvents 已经在 canonical ledger 中,不必为了证明来源再复制一份 fan-out JSON。
+V2 `HistoryCompactCheckpoint` 使用固定大小的 prefix metadata:event/Turn count、through boundary 和一个覆盖完整有序前缀的 digest。原始 RuntimeEvents 已经在 canonical ledger 中,不必为了证明来源再复制一份 fan-out JSON。
V3 保持完全相同的 bounded source、coverage、lineage、durability 与 replay check,只把 projection value 从文本摘要换成 closed provider-state variant。当前 union 只有 `openai_codex_remote_v2`;未来新增 provider 必须加入具体、可校验的 variant,而不是引入任意 JSON registry。
当前状态是:
-- schema V2 与 V3 checkpoint 优先从 AgentRun ledger 与 bounded projection 加载;
-- V1 artifact blocks 只作为兼容 fallback 读取;
-- 当 ledger-backed checkpoint 已验证覆盖同一前缀时,Runtime 可以校验并回收对应 legacy block/source artifacts;
-- cleanup 是 reclaim-only,失败不影响 replay。
+- schema V2 与 V3 checkpoint 从 AgentRun ledger 与 bounded projection 加载;
+- latest compatible durable checkpoint 是唯一 checkpoint authority;
+- V1 block/source artifact 路径已经删除,不再保留第二种表示;
+- generation、validation 或 append 任一失败时,RuntimeEvent ledger 保持不变。
这次演进不是减少 provenance,而是把 provenance 放回正确层次:source facts 由 RuntimeEvent ledger 保存,checkpoint 只保存验证 source prefix 所需的 bounded identity。
-## 不要把三种“compact”混成一个功能
+## 一套 compaction 机制,一个相邻 prune
-Maka 当前至少有三类上下文缩减:
+Maka 只有一套 LLM compaction 机制,以及一个相邻的 current-request rewrite:
| 机制 | Source | 发生时机 | Durable result | 本章定位 |
|---|---|---|---|---|
-| History LLM compaction | 旧 RuntimeEvent prefix | Turn 之间、构建 prior history 时 | Schema V2 或 V3 checkpoint 记录进 AgentRun event ledger | 本章主体 |
+| History LLM compaction | 安全的 RuntimeEvent prefix | 手动请求、pre-turn capacity、active-turn capacity 或 provider overflow | Schema V2 或 V3 checkpoint 记录进 AgentRun event ledger | 本章主体 |
| Active Tool Result Prune | 当前 Turn 的 provider-visible tool result | 同一 Turn 的下一 step 前 | raw result 先归档;placeholder 只改当前 messages | 第二章主体 |
-| `semanticCompact` | 当前 user head anchor 之后、open protocol tail 之前的 completed middle span,加可用的 Turn RuntimeEvent refs | `prepareStep` | V2 `SemanticCompactBlock` 是 best-effort diagnostic record;accepted messages 保存在当前 backend projection | Attention shaping,相邻机制,不等同于 V2 checkpoint |
-
-它们共享“不要修改 canonical source”的方向,但保证强度不同。
-
-当前 `semanticCompact` 把原始 current-user message 当作不可改写的 head anchor。Runtime 只选择 anchor 之后由完整 assistant step 与完整 tool call/result pairs 组成的连续 completed span;最新一个 completed execution episode,以及第一个未完成 provider episode 及其后内容,都保持为 exact tail。LLM 只生成 bounded continuation delta:已经确认的发现、已经做出的决策、失败路径、部分工作产物和正在执行的动作;不重复 exact head anchor 已经携带的目标与约束。attention compaction 在总 active context 超过 128K estimated tokens 前保持休眠;越过该水位后,completed span 还需达到 4K,rolling successor 也需新增 4K completed raw history。
-
-精确 head anchor 始终保持为 `user` message。LLM 生成的 continuation projection 以 `assistant` message 注入,因为它表达的是模型自身已经完成的执行历史,而不是一条新的用户指令。这样可以避免把 `head anchor + projection` 变成连续两条 user message,进而导致 OpenAI-compatible 模型重新开始任务发现。该 message 不注入任何 runtime 推导的 state card。
-
-Active tool-result pruning 还必须完整保留最新 completed provider step 的结果。`prepareStep` 发生在这些结果第一次可能发送给模型之前;如果此时就 prune,相当于模型尚未消费证据,证据便已被归档。只有更新的 step 完成后,前一步结果才进入 prune 候选;semantic compaction 仍可总结最新结果,因为它的 LLM projection 会继续携带被选中的证据。
-
-一旦 semantic projection 被接纳,后续 active pruning 对它已覆盖的 source 只是表示形式变化。Runtime 不再要求 provider-visible message 的字节级一致,而是通过稳定 lineage identity(tool call、tool name、payload shape 和原始 body hash)将 raw Tool Result 与 archive placeholder 识别为同一条 source。其他 source 字段仍须精确匹配。这样既能让 accepted projection 穿过 pruning 继续生效,也不会让无关的历史变更冒充同一段 coverage。
-
-Provider 可见的 continuation 只包含 exact head anchor、LLM 生成的 continuation delta、最新一个 completed execution episode 和 exact open protocol tail。`semanticCompact` 不再生成或渲染 Runtime 推断的 state cards。Coverage、source refs、archive refs 与 preserved-tail indexes 只保留在 `SemanticCompactBlock` 的诊断旁路中,不注入 continuation。rolling successor 遇到旧 block 时,会重新渲染其中的 LLM summary 并忽略 legacy state cards,而不是原样回放旧的 provider message。
-该机制主动用 prefix rewrite 换取模型注意力。Signed token savings、compact-call cost 与 cache miss 是诊断数据,不是 attention 模式的拒绝条件;source/head/protocol validation、非截断输出与完整 provider-visible projection budget 才是 hard acceptance gates。V2 block 记录 exact head identity、predecessor、new coverage 与 cumulative digest,但 recorder 仍是 best-effort:写入失败不会撤销 invocation-local accepted projection。因此它仍不是 V2 history checkpoint 那种“durable append 成功后才进入 replay”的同一协议。
+两者都保留 canonical source,但不会形成并行的 compaction authority。History compaction 始终选择安全 RuntimeEvent prefix,生成并校验一个 replacement,再先持久化一个 checkpoint、后进入 replay。trigger-specific 代码可以固定 live head 或保留 verbatim tail,但不再拥有另一套 planner、summary format、controller 或 durable block。
-如果未来要把所有 LLM compaction 统一到“Events Log projection”模型,关键不是复用一个 summary prompt,而是让 active-loop source 也获得完整、稳定、可验证的 event coverage 与明确的 durability contract。这是架构方向,不应被描述成当前已经完成。
+Active Tool Result Prune 仍是 deterministic、非 LLM 的 rewrite。它先归档 eligible raw Tool Result,再替换 current provider request 中的该结果。它既不总结 span,也不创建 checkpoint;后续 history-compaction planner 读取 canonical RuntimeEvents,不会把 prune placeholder 当成 source authority。
## Compaction 不是什么
@@ -525,20 +509,18 @@ LLM 在 summary 中写“测试已通过”仍然只是对 source events 的概
4. `packages/runtime/src/ai-sdk-backend.ts`:请求投影主链、manual compact、write/fallback 语义;
5. `packages/runtime/src/agent-run.ts`:`history_compact_checkpoint_recorded` durable event;
6. `packages/runtime/src/history-compact-ledger.ts`:bounded projection lookup、ledger recovery 与 checkpoint selection;
-7. `packages/runtime/src/runtime-kernel.ts`:checkpoint write serialization、cache 与 legacy cleanup 调度;
+7. `packages/runtime/src/runtime-kernel.ts`:checkpoint write serialization 与 manual compaction lifecycle;
8. `packages/storage/src/agent-run-store.ts`:canonical append 与 bounded event projection 的提交顺序;
-9. `packages/runtime/src/history-compact-artifacts.ts`:V1 artifact-backed compatibility path;
-10. `packages/runtime/src/history-compact-cleanup.ts`:V2 覆盖验证后的 legacy reclaim;
-11. `packages/runtime/src/context-budget-policy.ts`:默认预算、环境开关与 manual lookup overlay;
-12. `packages/runtime/src/openai-codex-history-compactor.ts`:Codex compact output 校验与 rolling provider-state input;
-13. `packages/runtime-host/src/server/execution-model-composition.ts`:默认 provider-specific compactor 选择。
+9. `packages/runtime/src/context-budget-policy.ts`:默认预算、环境开关与 manual lookup overlay;
+10. `packages/runtime/src/openai-codex-history-compactor.ts`:Codex compact output 校验与 rolling provider-state input;
+11. `packages/runtime-host/src/server/execution-model-composition.ts`:默认 provider-specific compactor 选择。
重点测试包括:
- `history-compact-checkpoint.test.ts`:10K event bounded coverage、prefix digest、ledger recovery、policy replay;
- `history-compact-summarizer.test.ts`:tool-bearing summary input、failure 与 rolling update;
- `context-budget.test.ts`:high-water、tail cap、tool pair preservation、archive gate 与 loaded block;
-- `ai-sdk-backend.test.ts`:same-request replacement、checkpoint reuse、fail-open、manual compact 与 V1 fallback;
+- `ai-sdk-backend.test.ts`:same-request replacement、checkpoint reuse、fail-open 与 manual compact;
- `session-manager.test.ts`:manual compaction 的 Run lifecycle、stop 与 concurrency;
- `agent-run-store.test.ts`:canonical event 和 bounded projection 的原子顺序与 repair safety。
diff --git a/packages/cli/src/__tests__/pi-transcript.test.ts b/packages/cli/src/__tests__/pi-transcript.test.ts
index e1cab2d10..7ba93397d 100644
--- a/packages/cli/src/__tests__/pi-transcript.test.ts
+++ b/packages/cli/src/__tests__/pi-transcript.test.ts
@@ -35,11 +35,43 @@ import {
renderMakaPiTranscript,
reconcileToolsWithStoredMessages,
replaceTranscriptWithStoredMessages,
+ submitCompactToTranscript,
toggleAllThinkingExpansion,
toggleAllToolExpansion,
} from '../pi-transcript.js';
describe('Maka Pi TUI transcript', () => {
+ test('renders manual compaction from the typed terminal outcome', async () => {
+ for (const [outcome, expected] of [
+ [{ kind: 'compacted' as const, checkpointId: 'checkpoint-1' }, 'Context compacted.'],
+ [{ kind: 'unchanged' as const, reason: 'already_compacted' }, 'Nothing to compact.'],
+ [
+ { kind: 'failed' as const, reason: 'write_failed' },
+ 'Context compaction failed: write_failed.',
+ ],
+ ] as const) {
+ const state = createMakaPiTranscriptState();
+ await submitCompactToTranscript({
+ state,
+ driver: {
+ compactSession: async function* () {
+ yield {
+ type: 'complete' as const,
+ id: 'complete-1',
+ turnId: 'compact-1',
+ ts: 1,
+ stopReason: 'end_turn' as const,
+ contextCompactionOutcome: outcome,
+ };
+ },
+ },
+ });
+ const notice = state.entries.at(-1);
+ assert.equal(notice?.kind, 'notice');
+ assert.equal(notice?.kind === 'notice' ? notice.text : '', expected);
+ }
+ });
+
test('renders stored legacy Automation prompts as read-only provenance', () => {
const state = createMakaPiTranscriptState();
replaceTranscriptWithStoredMessages(state, [
diff --git a/packages/cli/src/pi-transcript.ts b/packages/cli/src/pi-transcript.ts
index b5f3e1745..bc00aee7e 100644
--- a/packages/cli/src/pi-transcript.ts
+++ b/packages/cli/src/pi-transcript.ts
@@ -509,21 +509,24 @@ export async function submitCompactToTranscript(input: {
driver: Pick<MakaSessionDriver, 'compactSession'>;
onChange?: () => void;
}): Promise<void> {
- let completed = false;
- let sawCompactionNotice = false;
+ let outcome: Extract<SessionEvent, { type: 'complete' }>['contextCompactionOutcome'];
try {
for await (const event of input.driver.compactSession()) {
- if (event.type === 'token_usage' && contextBudgetOutcomeNotice(event.contextBudget))
- sawCompactionNotice = true;
- if (event.type === 'complete' && event.stopReason === 'end_turn') completed = true;
- applyMakaSessionEventToTranscript(input.state, event);
+ if (event.type === 'complete') outcome = event.contextCompactionOutcome;
+ if (event.type === 'token_usage') accumulateUsage(input.state.usage, event);
+ else applyMakaSessionEventToTranscript(input.state, event);
input.onChange?.();
}
- if (completed && !sawCompactionNotice) {
+ if (outcome) {
input.state.entries.push({
kind: 'notice',
- level: 'info',
- text: 'Nothing to compact.',
+ level: outcome.kind === 'failed' ? 'error' : 'info',
+ text:
+ outcome.kind === 'compacted'
+ ? 'Context compacted.'
+ : outcome.kind === 'unchanged'
+ ? 'Nothing to compact.'
+ : `Context compaction failed: ${outcome.reason}.`,
});
input.onChange?.();
}
@@ -1060,15 +1063,11 @@ function contextBudgetNoticeText(
(candidate) => candidate.decision === 'replaced',
);
if (!contextBudget || !decision) return undefined;
- const kind = decision.boundaryKind ?? contextBudget.highWaterReason ?? 'context';
- const coveredTurns = decision.coveredTurns ?? contextBudget.historyCompactedTurns;
- const coveredEvents = decision.coveredRuntimeEvents ?? contextBudget.historyCompactedEvents;
+ const kind = decision.boundaryKind ?? 'context';
+ const coveredTurns = decision.coveredTurns;
+ const coveredEvents = decision.coveredRuntimeEvents;
const savedTokens =
decision.estimatedTokensSaved ??
- tokenDelta(
- contextBudget.historyCompactedEstimatedTokensBefore,
- contextBudget.historyCompactedEstimatedTokensAfter,
- ) ??
tokenDelta(contextBudget.estimatedTokensBefore, contextBudget.estimatedTokensAfter);
const parts = [`Context compacted: ${kind}`];
if (coveredTurns !== undefined || coveredEvents !== undefined) {
diff --git a/packages/core/src/__tests__/usage-record-schema-retired-fields.test.ts b/packages/core/src/__tests__/usage-record-schema-retired-fields.test.ts
new file mode 100644
index 000000000..c78cf8458
--- /dev/null
+++ b/packages/core/src/__tests__/usage-record-schema-retired-fields.test.ts
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import assert from 'node:assert/strict';
+import test from 'node:test';
+import { isContextBudgetDiagnostic } from '../usage-record-schema.js';
+import type { ContextBudgetDiagnostic } from '../usage-stats/types.js';
+
+const currentDiagnostic: ContextBudgetDiagnostic = {
+ enabled: true,
+ estimatedTokensBefore: 10,
+ estimatedTokensAfter: 5,
+ keptTurns: 1,
+ droppedTurns: 1,
+ keptEvents: 2,
+ droppedEvents: 2,
+};
+
+// Retired telemetry remains readable but cannot be produced through the current contract.
+const retiredDiagnostic: ContextBudgetDiagnostic = {
+ ...currentDiagnostic,
+ // @ts-expect-error semanticCompactEnabled is a decode-only historical key.
+ semanticCompactEnabled: true,
+};
+void retiredDiagnostic;
+
+test('accepts retired context-budget keys only as inert historical data', () => {
+ assert.equal(
+ isContextBudgetDiagnostic({
+ ...currentDiagnostic,
+ semanticCompactEnabled: true,
+ synthesisCacheEnabled: true,
+ historyCompactBlocksWritten: 1,
+ historyRewriteVersion: 'legacy',
+ }),
+ true,
+ );
+ assert.equal(isContextBudgetDiagnostic({ ...currentDiagnostic, unknownFutureKey: true }), false);
+});
diff --git a/packages/core/src/agent-run.ts b/packages/core/src/agent-run.ts
index 4fcff6051..e47bdaa76 100644
--- a/packages/core/src/agent-run.ts
+++ b/packages/core/src/agent-run.ts
@@ -408,7 +408,6 @@ export const AGENT_RUN_EVENT_TYPES = [
'provider_request_attempt_recorded',
'model_call_attempt_recorded',
'history_compact_checkpoint_recorded',
- 'semantic_compact_block_recorded',
'task_gate_decided',
'abort_requested',
'run_completed',
diff --git a/packages/core/src/backend-types.ts b/packages/core/src/backend-types.ts
index d37b3ece6..8990812e8 100644
--- a/packages/core/src/backend-types.ts
+++ b/packages/core/src/backend-types.ts
@@ -29,6 +29,7 @@
import type {
AttachmentRef,
+ ContextCompactionOutcome,
MessageContent,
QuoteRef,
SessionEvent,
@@ -166,11 +167,10 @@ export interface BackendCompactHistoryInput {
*/
runId: string;
runtimeContext: readonly RuntimeEvent[];
- /** Override the configured recent-turn tail for an explicit recovery compaction. */
- minRecentTurns?: number;
}
export interface BackendCompactHistoryResult {
+ outcome: ContextCompactionOutcome;
contextBudget?: ContextBudgetDiagnostic;
}
diff --git a/packages/core/src/events.ts b/packages/core/src/events.ts
index 18311065d..4896c7641 100644
--- a/packages/core/src/events.ts
+++ b/packages/core/src/events.ts
@@ -1155,8 +1155,15 @@ export interface CompleteEvent extends BaseEvent {
* outcome, not a provider context-length error.
*/
contextBudgetExhaustedDetail?: ContextBudgetExhaustedDetail;
+ /** Durable result of an explicit context-compaction execution. */
+ contextCompactionOutcome?: ContextCompactionOutcome;
}
+export type ContextCompactionOutcome =
+ | { kind: 'compacted'; checkpointId: string }
+ | { kind: 'unchanged'; reason: string }
+ | { kind: 'failed'; reason: string };
+
export type ContextBudgetExhaustedDetail =
| 'no_safe_completed_span'
| 'summarizer_failed'
diff --git a/packages/core/src/usage-ledger-merge.ts b/packages/core/src/usage-ledger-merge.ts
index 0081a1385..8399be92c 100644
--- a/packages/core/src/usage-ledger-merge.ts
+++ b/packages/core/src/usage-ledger-merge.ts
@@ -42,9 +42,10 @@ import type {
* says how much came from where, until the old table ages out of the queried
* range and `legacyRecords` reaches zero on its own.
*
- * Model calls that have not yet been routed through the canonical seam —
- * `semantic_compact` and `history_compact` — still write only to the old table,
- * so this merge is what keeps them counted in the meantime.
+ * History compaction calls have not yet been routed through the canonical seam
+ * and still write only to the old table, so this merge keeps them counted in
+ * the meantime. Historical records may also contain the retired
+ * `semantic_compact` call kind.
*/
export interface UsageProvenance {
/** Classification of the canonical records behind this result. */
diff --git a/packages/core/src/usage-record-schema.ts b/packages/core/src/usage-record-schema.ts
index b586f418b..1dee218a3 100644
--- a/packages/core/src/usage-record-schema.ts
+++ b/packages/core/src/usage-record-schema.ts
@@ -70,7 +70,7 @@ const COMPACTION_DECISION_SHAPE = defineObjectShape<CompactionDecisionDiagnostic
],
);
-const CONTEXT_BUDGET_SHAPE = defineObjectShape<ContextBudgetDiagnostic>()(
+const CURRENT_CONTEXT_BUDGET_SHAPE = defineObjectShape<ContextBudgetDiagnostic>()(
[
'enabled',
'estimatedTokensBefore',
@@ -83,7 +83,6 @@ const CONTEXT_BUDGET_SHAPE = defineObjectShape<ContextBudgetDiagnostic>()(
[
'policyName',
'maxHistoryEstimatedTokens',
- 'maxHistoryTurns',
'prunedToolResults',
'prunedToolResultEstimatedTokensBefore',
'prunedToolResultEstimatedTokensAfter',
@@ -96,78 +95,92 @@ const CONTEXT_BUDGET_SHAPE = defineObjectShape<ContextBudgetDiagnostic>()(
'activeDuplicateToolResults',
'activeArchiveFailures',
'activeEstimatedTokensSaved',
- 'semanticCompactEnabled',
- 'semanticCompactMode',
'compactionDecisions',
- 'archiveRetrievalMode',
- 'archiveRetrievalEligibleTurns',
- 'retrievedArchiveToolResults',
- 'retrievedArchiveEstimatedTokens',
- 'archiveRetrievalSkipped',
- 'archiveRetrievalFailures',
- 'archiveRetrievalSkippedReasonCounts',
- 'archiveRetrievalFailureReasonCounts',
- 'historySearchMatches',
- 'historyAroundRetrievedEvents',
- 'historyAroundEstimatedTokens',
- 'historyAroundSkippedEvents',
- 'synthesisCacheEnabled',
- 'synthesisCacheMode',
- 'synthesisCacheBlocksLoaded',
- 'synthesisCacheLoadSkipped',
- 'synthesisCacheLoadSkippedReasonCounts',
- 'synthesisCacheLoadFailures',
- 'synthesisCacheBlocksAvailable',
- 'synthesisCacheBlocksSelected',
- 'synthesisCacheBlockIds',
- 'synthesisCacheEstimatedTokens',
- 'synthesisCacheSkipped',
- 'synthesisCacheSkippedReasonCounts',
- 'synthesisCacheInvalidated',
- 'synthesisCacheInvalidationReasonCounts',
- 'synthesisCacheWritesAttempted',
- 'synthesisCacheBlocksWritten',
- 'synthesisCacheWrittenBlockIds',
- 'synthesisCacheWriteEstimatedTokens',
- 'synthesisCacheWriteSkipped',
- 'synthesisCacheWriteSkippedReasonCounts',
- 'synthesisCacheWriteFailures',
- 'synthesisCacheEvicted',
- 'synthesisCacheEvictionReasonCounts',
- 'historyCompactEnabled',
- 'historyCompactMode',
- 'historyCompactBlocksLoaded',
- 'historyCompactLoadSkipped',
- 'historyCompactLoadSkippedReasonCounts',
- 'historyCompactLoadFailures',
- 'historyCompactBlocksAvailable',
- 'historyCompactBlocksSelected',
- 'historyCompactBlockIds',
- 'historyCompactedTurns',
- 'historyCompactedEvents',
- 'historyCompactedEstimatedTokensBefore',
- 'historyCompactedEstimatedTokensAfter',
- 'historyCompactSkipped',
- 'historyCompactSkippedReasonCounts',
- 'historyCompactCoverageHashes',
- 'historyCompactWritesAttempted',
- 'historyCompactBlocksWritten',
- 'historyCompactWrittenBlockIds',
- 'historyCompactWriteEstimatedTokens',
- 'historyCompactWriteSkipped',
- 'historyCompactWriteSkippedReasonCounts',
- 'historyCompactWriteFailures',
- 'highWaterName',
- 'highWaterSeq',
- 'highWaterReason',
- 'highWaterRequestShapeHashBefore',
- 'highWaterRequestShapeHashAfter',
- 'historyRewriteVersion',
- 'historyRewriteResetReason',
- 'historyRewriteGate',
],
);
+/**
+ * Keys written by retired context-budget implementations. They remain
+ * accepted only so persisted usage records stay readable; current code cannot
+ * produce them through ContextBudgetDiagnostic.
+ */
+const RETIRED_CONTEXT_BUDGET_KEYS = [
+ 'maxHistoryTurns',
+ 'semanticCompactEnabled',
+ 'semanticCompactMode',
+ 'archiveRetrievalMode',
+ 'archiveRetrievalEligibleTurns',
+ 'retrievedArchiveToolResults',
+ 'retrievedArchiveEstimatedTokens',
+ 'archiveRetrievalSkipped',
+ 'archiveRetrievalFailures',
+ 'archiveRetrievalSkippedReasonCounts',
+ 'archiveRetrievalFailureReasonCounts',
+ 'historySearchMatches',
+ 'historyAroundRetrievedEvents',
+ 'historyAroundEstimatedTokens',
+ 'historyAroundSkippedEvents',
+ 'synthesisCacheEnabled',
+ 'synthesisCacheMode',
+ 'synthesisCacheBlocksLoaded',
+ 'synthesisCacheLoadSkipped',
+ 'synthesisCacheLoadSkippedReasonCounts',
+ 'synthesisCacheLoadFailures',
+ 'synthesisCacheBlocksAvailable',
+ 'synthesisCacheBlocksSelected',
+ 'synthesisCacheBlockIds',
+ 'synthesisCacheEstimatedTokens',
+ 'synthesisCacheSkipped',
+ 'synthesisCacheSkippedReasonCounts',
+ 'synthesisCacheInvalidated',
+ 'synthesisCacheInvalidationReasonCounts',
+ 'synthesisCacheWritesAttempted',
+ 'synthesisCacheBlocksWritten',
+ 'synthesisCacheWrittenBlockIds',
+ 'synthesisCacheWriteEstimatedTokens',
+ 'synthesisCacheWriteSkipped',
+ 'synthesisCacheWriteSkippedReasonCounts',
+ 'synthesisCacheWriteFailures',
+ 'synthesisCacheEvicted',
+ 'synthesisCacheEvictionReasonCounts',
+ 'historyCompactEnabled',
+ 'historyCompactMode',
+ 'historyCompactBlocksLoaded',
+ 'historyCompactLoadSkipped',
+ 'historyCompactLoadSkippedReasonCounts',
+ 'historyCompactLoadFailures',
+ 'historyCompactBlocksAvailable',
+ 'historyCompactBlocksSelected',
+ 'historyCompactBlockIds',
+ 'historyCompactedTurns',
+ 'historyCompactedEvents',
+ 'historyCompactedEstimatedTokensBefore',
+ 'historyCompactedEstimatedTokensAfter',
+ 'historyCompactSkipped',
+ 'historyCompactSkippedReasonCounts',
+ 'historyCompactCoverageHashes',
+ 'historyCompactWritesAttempted',
+ 'historyCompactBlocksWritten',
+ 'historyCompactWrittenBlockIds',
+ 'historyCompactWriteEstimatedTokens',
+ 'historyCompactWriteSkipped',
+ 'historyCompactWriteSkippedReasonCounts',
+ 'historyCompactWriteFailures',
+ 'highWaterName',
+ 'highWaterSeq',
+ 'highWaterReason',
+ 'highWaterRequestShapeHashBefore',
+ 'highWaterRequestShapeHashAfter',
+ 'historyRewriteVersion',
+ 'historyRewriteResetReason',
+ 'historyRewriteGate',
+] as const;
+
+const CONTEXT_BUDGET_SHAPE = {
+ required: CURRENT_CONTEXT_BUDGET_SHAPE.required,
+ allowed: new Set([...CURRENT_CONTEXT_BUDGET_SHAPE.allowed, ...RETIRED_CONTEXT_BUDGET_KEYS]),
+};
+
const PROMPT_SEGMENT_KINDS = new Set([
'system_prompt',
'tool_schema',
@@ -233,60 +246,9 @@ const CONTEXT_NUMBERS = [
'activeDuplicateToolResults',
'activeArchiveFailures',
'activeEstimatedTokensSaved',
- 'archiveRetrievalEligibleTurns',
- 'retrievedArchiveToolResults',
- 'retrievedArchiveEstimatedTokens',
- 'archiveRetrievalSkipped',
- 'archiveRetrievalFailures',
- 'historySearchMatches',
- 'historyAroundRetrievedEvents',
- 'historyAroundEstimatedTokens',
- 'historyAroundSkippedEvents',
- 'synthesisCacheBlocksLoaded',
- 'synthesisCacheLoadSkipped',
- 'synthesisCacheLoadFailures',
- 'synthesisCacheBlocksAvailable',
- 'synthesisCacheBlocksSelected',
- 'synthesisCacheEstimatedTokens',
- 'synthesisCacheSkipped',
- 'synthesisCacheInvalidated',
- 'synthesisCacheWritesAttempted',
- 'synthesisCacheBlocksWritten',
- 'synthesisCacheWriteEstimatedTokens',
- 'synthesisCacheWriteSkipped',
- 'synthesisCacheWriteFailures',
- 'synthesisCacheEvicted',
- 'historyCompactBlocksLoaded',
- 'historyCompactLoadSkipped',
- 'historyCompactLoadFailures',
- 'historyCompactBlocksAvailable',
- 'historyCompactBlocksSelected',
- 'historyCompactedTurns',
- 'historyCompactedEvents',
- 'historyCompactedEstimatedTokensBefore',
- 'historyCompactedEstimatedTokensAfter',
- 'historyCompactSkipped',
- 'historyCompactWritesAttempted',
- 'historyCompactBlocksWritten',
- 'historyCompactWriteEstimatedTokens',
- 'historyCompactWriteSkipped',
- 'historyCompactWriteFailures',
- 'highWaterSeq',
] as const;
-const CONTEXT_REASON_COUNTS = [
- 'archivePlaceholderReasonCounts',
- 'archiveRetrievalSkippedReasonCounts',
- 'archiveRetrievalFailureReasonCounts',
- 'synthesisCacheLoadSkippedReasonCounts',
- 'synthesisCacheSkippedReasonCounts',
- 'synthesisCacheInvalidationReasonCounts',
- 'synthesisCacheWriteSkippedReasonCounts',
- 'synthesisCacheEvictionReasonCounts',
- 'historyCompactLoadSkippedReasonCounts',
- 'historyCompactSkippedReasonCounts',
- 'historyCompactWriteSkippedReasonCounts',
-] as const;
+const CONTEXT_REASON_COUNTS = ['archivePlaceholderReasonCounts'] as const;
/**
* Token-usage field bundle shared by the runtime-event, message, and event
@@ -376,53 +338,9 @@ export function isContextBudgetDiagnostic(value: unknown): value is ContextBudge
}
return (
isOptionalString(value.policyName) &&
- (value.semanticCompactEnabled === undefined ||
- typeof value.semanticCompactEnabled === 'boolean') &&
- (value.semanticCompactMode === undefined ||
- ['off', 'validate_only', 'prepare_step_dry_run', 'replace'].includes(
- value.semanticCompactMode as string,
- )) &&
(value.compactionDecisions === undefined ||
(Array.isArray(value.compactionDecisions) &&
- value.compactionDecisions.every(isCompactionDecisionDiagnostic))) &&
- (value.archiveRetrievalMode === undefined ||
- value.archiveRetrievalMode === 'eager' ||
- value.archiveRetrievalMode === 'history_search_gated') &&
- (value.synthesisCacheEnabled === undefined ||
- typeof value.synthesisCacheEnabled === 'boolean') &&
- (value.synthesisCacheMode === undefined ||
- ['off', 'lookup', 'read_write', 'write_only', 'fallback_archive_retrieval'].includes(
- value.synthesisCacheMode as string,
- )) &&
- optionalStringArray(value.synthesisCacheBlockIds) &&
- optionalStringArray(value.synthesisCacheWrittenBlockIds) &&
- (value.historyCompactEnabled === undefined ||
- typeof value.historyCompactEnabled === 'boolean') &&
- (value.historyCompactMode === undefined ||
- ['off', 'deterministic', 'lookup', 'read_write'].includes(
- value.historyCompactMode as string,
- )) &&
- optionalStringArray(value.historyCompactBlockIds) &&
- optionalStringArray(value.historyCompactCoverageHashes) &&
- optionalStringArray(value.historyCompactWrittenBlockIds) &&
- isOptionalString(value.highWaterName) &&
- (value.highWaterReason === undefined ||
- [
- 'archive_prune',
- 'history_search_gated_retrieval',
- 'synthesis_cache_write',
- 'synthesis_cache_select',
- 'history_compact',
- 'manual_reset',
- 'system_change',
- 'tools_change',
- 'log_rewrite',
- ].includes(value.highWaterReason as string)) &&
- isOptionalString(value.highWaterRequestShapeHashBefore) &&
- isOptionalString(value.highWaterRequestShapeHashAfter) &&
- isOptionalString(value.historyRewriteVersion) &&
- isOptionalString(value.historyRewriteResetReason) &&
- isOptionalString(value.historyRewriteGate)
+ value.compactionDecisions.every(isCompactionDecisionDiagnostic)))
);
}
diff --git a/packages/core/src/usage-stats/types.ts b/packages/core/src/usage-stats/types.ts
index 75d4ce282..f7542ccd8 100644
--- a/packages/core/src/usage-stats/types.ts
+++ b/packages/core/src/usage-stats/types.ts
@@ -17,6 +17,10 @@
* under the License.
*/
+/**
+ * Stored model-call kinds. `semantic_compact` is decode-only for historical
+ * usage records; the Runtime no longer has a writer or execution path for it.
+ */
export const MODEL_CALL_KINDS = [
'main',
'semantic_compact',
@@ -281,7 +285,6 @@ export interface ContextBudgetDiagnostic {
enabled: boolean;
policyName?: string;
maxHistoryEstimatedTokens?: number;
- maxHistoryTurns?: number;
estimatedTokensBefore: number;
estimatedTokensAfter: number;
keptTurns: number;
@@ -300,89 +303,7 @@ export interface ContextBudgetDiagnostic {
activeDuplicateToolResults?: number;
activeArchiveFailures?: number;
activeEstimatedTokensSaved?: number;
- semanticCompactEnabled?: boolean;
- semanticCompactMode?: 'off' | 'validate_only' | 'prepare_step_dry_run' | 'replace';
compactionDecisions?: CompactionDecisionDiagnostic[];
- archiveRetrievalMode?: 'eager' | 'history_search_gated';
- archiveRetrievalEligibleTurns?: number;
- retrievedArchiveToolResults?: number;
- retrievedArchiveEstimatedTokens?: number;
- archiveRetrievalSkipped?: number;
- archiveRetrievalFailures?: number;
- archiveRetrievalSkippedReasonCounts?: Record<string, number>;
- archiveRetrievalFailureReasonCounts?: Record<string, number>;
- historySearchMatches?: number;
- historyAroundRetrievedEvents?: number;
- historyAroundEstimatedTokens?: number;
- historyAroundSkippedEvents?: number;
- synthesisCacheEnabled?: boolean;
- synthesisCacheMode?:
- | 'off'
- | 'lookup'
- | 'read_write'
- | 'write_only'
- | 'fallback_archive_retrieval';
- synthesisCacheBlocksLoaded?: number;
- synthesisCacheLoadSkipped?: number;
- synthesisCacheLoadSkippedReasonCounts?: Record<string, number>;
- synthesisCacheLoadFailures?: number;
- synthesisCacheBlocksAvailable?: number;
- synthesisCacheBlocksSelected?: number;
- synthesisCacheBlockIds?: string[];
- synthesisCacheEstimatedTokens?: number;
- synthesisCacheSkipped?: number;
- synthesisCacheSkippedReasonCounts?: Record<string, number>;
- synthesisCacheInvalidated?: number;
- synthesisCacheInvalidationReasonCounts?: Record<string, number>;
- synthesisCacheWritesAttempted?: number;
- synthesisCacheBlocksWritten?: number;
- synthesisCacheWrittenBlockIds?: string[];
- synthesisCacheWriteEstimatedTokens?: number;
- synthesisCacheWriteSkipped?: number;
- synthesisCacheWriteSkippedReasonCounts?: Record<string, number>;
- synthesisCacheWriteFailures?: number;
- synthesisCacheEvicted?: number;
- synthesisCacheEvictionReasonCounts?: Record<string, number>;
- historyCompactEnabled?: boolean;
- historyCompactMode?: 'off' | 'deterministic' | 'lookup' | 'read_write';
- historyCompactBlocksLoaded?: number;
- historyCompactLoadSkipped?: number;
- historyCompactLoadSkippedReasonCounts?: Record<string, number>;
- historyCompactLoadFailures?: number;
- historyCompactBlocksAvailable?: number;
- historyCompactBlocksSelected?: number;
- historyCompactBlockIds?: string[];
- historyCompactedTurns?: number;
- historyCompactedEvents?: number;
- historyCompactedEstimatedTokensBefore?: number;
- historyCompactedEstimatedTokensAfter?: number;
- historyCompactSkipped?: number;
- historyCompactSkippedReasonCounts?: Record<string, number>;
- historyCompactCoverageHashes?: string[];
- historyCompactWritesAttempted?: number;
- historyCompactBlocksWritten?: number;
- historyCompactWrittenBlockIds?: string[];
- historyCompactWriteEstimatedTokens?: number;
- historyCompactWriteSkipped?: number;
- historyCompactWriteSkippedReasonCounts?: Record<string, number>;
- historyCompactWriteFailures?: number;
- highWaterName?: string;
- highWaterSeq?: number;
- highWaterReason?:
- | 'archive_prune'
- | 'history_search_gated_retrieval'
- | 'synthesis_cache_write'
- | 'synthesis_cache_select'
- | 'history_compact'
- | 'manual_reset'
- | 'system_change'
- | 'tools_change'
- | 'log_rewrite';
- highWaterRequestShapeHashBefore?: string;
- highWaterRequestShapeHashAfter?: string;
- historyRewriteVersion?: string;
- historyRewriteResetReason?: string;
- historyRewriteGate?: string;
}
export interface ToolInvocationResultSummary {
diff --git a/packages/runtime-host/src/__tests__/context-coordinator.test.ts b/packages/runtime-host/src/__tests__/context-coordinator.test.ts
index 2e7320cc0..cc152adaa 100644
--- a/packages/runtime-host/src/__tests__/context-coordinator.test.ts
+++ b/packages/runtime-host/src/__tests__/context-coordinator.test.ts
@@ -43,6 +43,7 @@ test('context compaction waits for terminal execution cleanup before preparing',
runId: 'run-compact',
status: 'completed' as const,
terminalEventId: 'event-compact',
+ contextCompactionOutcome: { kind: 'compacted' as const, checkpointId: 'checkpoint-1' },
};
const prepare = (): HostedExecutionPreparation => {
prepareCalls += 1;
@@ -100,6 +101,65 @@ test('context compaction waits for terminal execution cleanup before preparing',
releaseCleanup();
const outcome = await compaction;
assert.equal(outcome.ok, true);
- if (outcome.ok) assert.deepEqual(outcome.result, compacted);
+ if (outcome.ok) {
+ assert.deepEqual(outcome.result, {
+ kind: 'finished',
+ turn: compacted,
+ outcome: compacted.contextCompactionOutcome,
+ });
+ }
assert.equal(prepareCalls, 2);
});
+
+test('completed legacy compaction without an outcome returns a typed failure without draining', async () => {
+ const completed = {
+ sessionId: 'session-context',
+ turnId: 'turn-compact',
+ runId: 'run-compact',
+ status: 'completed' as const,
+ terminalEventId: 'event-compact',
+ };
+ let drainRequests = 0;
+ const coordinator = new HostContextCoordinator({
+ runtime: {
+ compactSession: async function* () {},
+ getContextDiagnostics: async () => ({}) as never,
+ listTurns: async () => [],
+ preflightContextCompaction: async () => {},
+ },
+ executions: {
+ lookup: async () => ({
+ sessionId: completed.sessionId,
+ turnId: completed.turnId,
+ runId: completed.runId,
+ userMessageId: null,
+ descriptor: { kind: 'context_compact' as const },
+ }),
+ prepare: () => assert.fail('existing execution must not prepare another root'),
+ reconcile: async () => assert.fail('existing execution must not reconcile'),
+ admit: async () => completedHostedExecutionAdmission(completed),
+ },
+ sessions: {
+ readHeaderSnapshot: async () =>
+ ({ status: 'active', isArchived: false }) as unknown as SessionHeader,
+ },
+ requestDrain: () => {
+ drainRequests += 1;
+ },
+ });
+
+ const outcome = await coordinator.handlers['context.compact'](
+ { sessionId: completed.sessionId, turnId: completed.turnId },
+ {} as never,
+ );
+
+ assert.deepEqual(outcome, {
+ ok: true,
+ result: {
+ kind: 'finished',
+ turn: completed,
+ outcome: { kind: 'failed', reason: 'missing_durable_outcome' },
+ },
+ });
+ assert.equal(drainRequests, 0);
+});
diff --git a/packages/runtime-host/src/__tests__/context-protocol.test.ts b/packages/runtime-host/src/__tests__/context-protocol.test.ts
index 33b5b36e6..2e555ff50 100644
--- a/packages/runtime-host/src/__tests__/context-protocol.test.ts
+++ b/packages/runtime-host/src/__tests__/context-protocol.test.ts
@@ -105,10 +105,13 @@ test('context operations preserve bounded exact wire values', () => {
operation: 'context.compact',
ok: true,
result: {
- sessionId: 'session-1',
- turnId: 'compact-1',
- runId: 'run-compact-1',
- status: 'running',
+ kind: 'started',
+ turn: {
+ sessionId: 'session-1',
+ turnId: 'compact-1',
+ runId: 'run-compact-1',
+ status: 'running',
+ },
},
}),
{
@@ -116,10 +119,49 @@ test('context operations preserve bounded exact wire values', () => {
operation: 'context.compact',
ok: true,
result: {
- sessionId: 'session-1',
- turnId: 'compact-1',
- runId: 'run-compact-1',
- status: 'running',
+ kind: 'started',
+ turn: {
+ sessionId: 'session-1',
+ turnId: 'compact-1',
+ runId: 'run-compact-1',
+ status: 'running',
+ },
+ },
+ },
+ );
+ assert.deepEqual(
+ decodeHostFrame({
+ requestId: 'request-compact-finished',
+ operation: 'context.compact',
+ ok: true,
+ result: {
+ kind: 'finished',
+ turn: {
+ sessionId: 'session-1',
+ turnId: 'compact-1',
+ runId: 'run-compact-1',
+ status: 'completed',
+ terminalEventId: 'event-compact-1',
+ contextCompactionOutcome: { kind: 'compacted', checkpointId: 'checkpoint-1' },
+ },
+ outcome: { kind: 'compacted', checkpointId: 'checkpoint-1' },
+ },
+ }),
+ {
+ requestId: 'request-compact-finished',
+ operation: 'context.compact',
+ ok: true,
+ result: {
+ kind: 'finished',
+ turn: {
+ sessionId: 'session-1',
+ turnId: 'compact-1',
+ runId: 'run-compact-1',
+ status: 'completed',
+ terminalEventId: 'event-compact-1',
+ contextCompactionOutcome: { kind: 'compacted', checkpointId: 'checkpoint-1' },
+ },
+ outcome: { kind: 'compacted', checkpointId: 'checkpoint-1' },
},
},
);
diff --git a/packages/runtime-host/src/__tests__/desktop-e2e-execution.test.ts b/packages/runtime-host/src/__tests__/desktop-e2e-execution.test.ts
index 2721c316c..92130a1ad 100644
--- a/packages/runtime-host/src/__tests__/desktop-e2e-execution.test.ts
+++ b/packages/runtime-host/src/__tests__/desktop-e2e-execution.test.ts
@@ -63,10 +63,12 @@ test('Desktop E2E compaction records a deterministic checkpoint', async () => {
runtimeContext: [userEvent()],
});
- assert.deepEqual(result, {});
assert.equal(recorded.length, 1);
assert.equal(recorded[0]?.turnId, 'turn-1');
const checkpoint = recorded[0]!.checkpoint;
+ assert.deepEqual(result, {
+ outcome: { kind: 'compacted', checkpointId: checkpoint.checkpointId },
+ });
assert.equal(checkpoint.version, 2);
if (checkpoint.version !== 2) return;
// The fixture is shaped like a real sectioned checkpoint so the builder's
diff --git a/packages/runtime-host/src/__tests__/execution-model-composition.test.ts b/packages/runtime-host/src/__tests__/execution-model-composition.test.ts
index 4dd5d094a..af100b503 100644
--- a/packages/runtime-host/src/__tests__/execution-model-composition.test.ts
+++ b/packages/runtime-host/src/__tests__/execution-model-composition.test.ts
@@ -402,7 +402,6 @@ test('Codex OAuth history compaction uses the provider-native route and preserve
turnId: 'turn-compact',
runId: 'run-compact',
runtimeContext,
- minRecentTurns: 1,
});
assert.equal(requests.length, 1, JSON.stringify(result));
@@ -410,7 +409,7 @@ test('Codex OAuth history compaction uses the provider-native route and preserve
const requestText = JSON.stringify(requests[0]!.body);
assert.match(requestText, /"type":"compaction_trigger"/);
assert.doesNotMatch(requestText, /context summarization assistant/i);
- assert.equal(result.contextBudget?.historyCompactWriteFailures, 1);
+ assert.deepEqual(result.outcome, { kind: 'failed', reason: 'provider_error' });
assert.equal(result.contextBudget?.compactionDecisions?.[0]?.decision, 'failedOpen');
assert.equal(attempts.length, 1);
assert.equal(attempts[0]?.callKind, 'history_compact');
diff --git a/packages/runtime-host/src/__tests__/protocol.test.ts b/packages/runtime-host/src/__tests__/protocol.test.ts
index 624b55463..8ca26bcaf 100644
--- a/packages/runtime-host/src/__tests__/protocol.test.ts
+++ b/packages/runtime-host/src/__tests__/protocol.test.ts
@@ -205,6 +205,10 @@ describe('Runtime Host bootstrap protocol', () => {
);
});
+ test('publishes a new compatibility epoch for typed context compaction outcomes', () => {
+ assert.ok(RUNTIME_HOST_COMPATIBILITY_EPOCH > 39);
+ });
+
test('selects the highest mutually supported protocol and rejects a gap', () => {
assert.equal(negotiateProtocol({ min: 0, max: 0 }, { min: 0, max: 0 }), 0);
assert.equal(negotiateProtocol({ min: 1, max: 3 }, { min: 2, max: 4 }), 3);
diff --git a/packages/runtime-host/src/__tests__/root-turn-coordinator.test.ts b/packages/runtime-host/src/__tests__/root-turn-coordinator.test.ts
index 01df0d01f..231c1aba9 100644
--- a/packages/runtime-host/src/__tests__/root-turn-coordinator.test.ts
+++ b/packages/runtime-host/src/__tests__/root-turn-coordinator.test.ts
@@ -1656,7 +1656,6 @@ test('Agent Graph context recovery fences competing root turns while compaction
backend?.releaseCompact();
assert.equal(await recovery, undefined);
assert.equal(backend?.compactInput?.turnId, compactTurnId);
- assert.equal(backend?.compactInput?.minRecentTurns, 0);
assert.deepEqual(fixture.coordinator.readRootState(fixture.sessionId), {
kind: 'idle',
});
@@ -1697,14 +1696,15 @@ test('manual context compact uses durable root query, stop, and exact retry auth
);
assert.equal(started.ok, true);
if (!started.ok) return;
- assert.equal(started.result.sessionId, fixture.sessionId);
- assert.equal(started.result.turnId, turnId);
+ assert.equal(started.result.kind, 'started');
+ assert.equal(started.result.turn.sessionId, fixture.sessionId);
+ assert.equal(started.result.turn.turnId, turnId);
await backend?.compactStarted.promise;
assert.deepEqual(fixture.coordinator.readRootState(fixture.sessionId), {
kind: 'active',
sessionId: fixture.sessionId,
turnId,
- runId: started.result.runId,
+ runId: started.result.turn.runId,
});
const queried = await fixture.turnControl.handlers['turn.query'](
@@ -1712,13 +1712,13 @@ test('manual context compact uses durable root query, stop, and exact retry auth
context,
);
assert.equal(queried.ok, true);
- if (queried.ok) assert.equal(queried.result.runId, started.result.runId);
+ if (queried.ok) assert.equal(queried.result.runId, started.result.turn.runId);
const stopped = await fixture.turnControl.handlers['turn.stop'](
{
sessionId: fixture.sessionId,
turnId,
- runId: started.result.runId,
+ runId: started.result.turn.runId,
},
context,
);
@@ -1730,7 +1730,14 @@ test('manual context compact uses durable root query, stop, and exact retry auth
{ sessionId: fixture.sessionId, turnId },
context,
);
- assert.deepEqual(retried, stopped);
+ assert.deepEqual(retried, {
+ ok: true,
+ result: {
+ kind: 'finished',
+ turn: stopped.result,
+ outcome: { kind: 'failed', reason: stopped.result.abortSource },
+ },
+ });
const admission = await fixture.stores.agentRunStore.readRootTurnAdmission(
fixture.sessionId,
turnId,
@@ -5178,7 +5185,7 @@ class BlockingContextRecoveryBackend implements AgentBackend {
this.compactInput = input;
this.compactStarted.resolve();
await this.#compactReleased.promise;
- return {};
+ return { outcome: { kind: 'unchanged' as const, reason: 'test' } };
}
releaseCompact(): void {
@@ -5245,7 +5252,7 @@ class GraphFollowupRecoveryBackend implements AgentBackend {
this.compactStartedCount += 1;
this.compactStarted.resolve();
await this.#compactReleased.promise;
- return {};
+ return { outcome: { kind: 'unchanged' as const, reason: 'test' } };
}
releaseGraphTurn(): void {
diff --git a/packages/runtime-host/src/protocol/context.ts b/packages/runtime-host/src/protocol/context.ts
index 360ea2449..6d0e18c56 100644
--- a/packages/runtime-host/src/protocol/context.ts
+++ b/packages/runtime-host/src/protocol/context.ts
@@ -26,7 +26,8 @@ import {
} from './codec.js';
import { invalidProtocolFrame } from './errors.js';
import { defineOperation } from './operation-spec.js';
-import { decodeTurnSnapshot, type TurnSnapshot } from './turn.js';
+import { decodeContextCompactionOutcome, decodeTurnSnapshot, type TurnSnapshot } from './turn.js';
+import type { ContextCompactionOutcome } from '@maka/core/events';
export interface ContextDiagnosticsQueryInput {
readonly sessionId: string;
@@ -37,7 +38,13 @@ export interface ContextCompactInput {
readonly turnId: string;
}
-export type ContextCompactResult = TurnSnapshot;
+export type ContextCompactResult =
+ | { readonly kind: 'started'; readonly turn: TurnSnapshot }
+ | {
+ readonly kind: 'finished';
+ readonly turn: TurnSnapshot;
+ readonly outcome: ContextCompactionOutcome;
+ };
export interface ContextDiagnosticsSegment {
readonly kind: 'system_instructions' | 'tool_definitions' | 'messages' | 'other';
@@ -114,9 +121,9 @@ export const CONTEXT_OPERATION_SPECS = {
availability: 'ready',
errors: [...QUERY_ERRORS, 'session_archived', 'session_busy', 'operation_conflict'] as const,
decodeInput: decodeContextCompactInput,
- decodeOutput: decodeTurnSnapshot,
+ decodeOutput: decodeContextCompactResult,
assertOutputForInput: (input, output) => {
- if (input.sessionId !== output.sessionId || input.turnId !== output.turnId) {
+ if (input.sessionId !== output.turn.sessionId || input.turnId !== output.turn.turnId) {
throw invalidProtocolFrame('Context compact changed operation identity');
}
},
@@ -136,6 +143,22 @@ function decodeContextCompactInput(value: unknown): ContextCompactInput {
};
}
+function decodeContextCompactResult(value: unknown): ContextCompactResult {
+ const result = requireShapedRecord(
+ value,
+ 'Context compact result',
+ ['kind', 'turn'],
+ ['outcome'],
+ );
+ const kind = requireString(result.kind, 'kind', 32);
+ const turn = decodeTurnSnapshot(result.turn);
+ if (kind === 'started' && result.outcome === undefined) return { kind, turn };
+ if (kind === 'finished' && result.outcome !== undefined) {
+ return { kind, turn, outcome: decodeContextCompactionOutcome(result.outcome) };
+ }
+ throw invalidProtocolFrame('Invalid context compact result');
+}
+
function decodeContextDiagnosticsResult(value: unknown): ContextDiagnosticsResult {
const record = requireShapedRecord(
value,
diff --git a/packages/runtime-host/src/protocol/index.ts b/packages/runtime-host/src/protocol/index.ts
index a05afbcbd..32467f852 100644
--- a/packages/runtime-host/src/protocol/index.ts
+++ b/packages/runtime-host/src/protocol/index.ts
@@ -91,7 +91,10 @@ export const RUNTIME_HOST_REGISTRATION_SCHEMA_VERSION = 1 as const;
export const RUNTIME_HOST_PROTOCOL_VERSION = 0 as const;
// Increment when the same protocol version no longer guarantees safe Client-Host
// interoperability. Mismatches are rejected before domain commands are admitted.
-export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 39 as const;
+export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 40 as const;
+// 40: Context compaction returns a typed terminal outcome on both Turn
+// snapshots and context.compact results. Epoch-39 peers reject these closed
+// shapes after admission, so mixed peers must fail during the handshake.
// 39: Client Capability tool descriptors carry trusted activity semantics and
// invocations can stream bounded progress frames.
// 38: `execute` is no longer a permission mode. Frame decoders reject it, so a
diff --git a/packages/runtime-host/src/protocol/turn.ts b/packages/runtime-host/src/protocol/turn.ts
index a0d4ff063..5ee9829c2 100644
--- a/packages/runtime-host/src/protocol/turn.ts
+++ b/packages/runtime-host/src/protocol/turn.ts
@@ -21,6 +21,7 @@ import { MAX_ATTACHMENT_BYTES, MAX_ATTACHMENT_COUNT } from '@maka/core/attachmen
import {
decodeMessageContent as decodeCanonicalMessageContent,
isCanonicalAttachmentRef,
+ type ContextCompactionOutcome,
type MessageContent,
type ProviderRetryReason,
} from '@maka/core/events';
@@ -182,7 +183,11 @@ export type LiveTurnSnapshot = TurnSnapshotBase & {
export type TurnSnapshot =
| LiveTurnSnapshot
- | (TurnSnapshotBase & { status: 'completed'; terminalEventId: string })
+ | (TurnSnapshotBase & {
+ status: 'completed';
+ terminalEventId: string;
+ contextCompactionOutcome?: ContextCompactionOutcome;
+ })
| (TurnSnapshotBase & {
status: 'failed';
terminalEventId: string;
@@ -631,17 +636,23 @@ export function decodeTurnSnapshot(value: unknown): TurnSnapshot {
};
const status = requireTurnRunStatus(record.status);
if (status === 'completed') {
- assertExactKeys(record, 'completed Turn snapshot', [
- 'sessionId',
- 'turnId',
- 'runId',
- 'status',
- 'terminalEventId',
- ]);
+ requireShapedRecord(
+ record,
+ 'completed Turn snapshot',
+ ['sessionId', 'turnId', 'runId', 'status', 'terminalEventId'],
+ ['contextCompactionOutcome'],
+ );
return {
...base,
status,
terminalEventId: requireId(record.terminalEventId, 'terminalEventId'),
+ ...(record.contextCompactionOutcome !== undefined
+ ? {
+ contextCompactionOutcome: decodeContextCompactionOutcome(
+ record.contextCompactionOutcome,
+ ),
+ }
+ : {}),
};
}
if (status === 'failed') {
@@ -699,6 +710,20 @@ export function decodeTurnSnapshot(value: unknown): TurnSnapshot {
};
}
+export function decodeContextCompactionOutcome(value: unknown): ContextCompactionOutcome {
+ const record = requireRecord(value, 'Context compaction outcome');
+ const kind = requireString(record.kind, 'kind', 32);
+ if (kind === 'compacted') {
+ assertExactKeys(record, 'compacted context outcome', ['kind', 'checkpointId']);
+ return { kind, checkpointId: requireEntityId(record.checkpointId, 'checkpointId') };
+ }
+ if (kind === 'unchanged' || kind === 'failed') {
+ assertExactKeys(record, `${kind} context outcome`, ['kind', 'reason']);
+ return { kind, reason: requireString(record.reason, 'reason', 256) };
+ }
+ throw invalidProtocolFrame('Invalid context compaction outcome kind');
+}
+
export function decodeTurnProviderRetry(value: unknown): TurnProviderRetry {
const record = requireRecord(value, 'Turn provider retry');
const phase = record.phase;
diff --git a/packages/runtime-host/src/server/canonical-turn-snapshot.ts b/packages/runtime-host/src/server/canonical-turn-snapshot.ts
index 7b0d2f344..24cf1f3e7 100644
--- a/packages/runtime-host/src/server/canonical-turn-snapshot.ts
+++ b/packages/runtime-host/src/server/canonical-turn-snapshot.ts
@@ -18,6 +18,7 @@
*/
import type { AgentRunHeader } from '@maka/core/agent-run';
+import type { ContextCompactionOutcome } from '@maka/core/events';
import { truncateUtf8 } from '@maka/core/diagnostic-log';
import { redactSecrets } from '@maka/core/redaction';
import { classifyTerminalRuntimeLedger } from '@maka/runtime/terminal-run-commit';
@@ -55,12 +56,16 @@ export async function readCanonicalTurnSnapshot(
if (terminal.kind === 'fact') {
const fact = terminal.fact;
if (fact.runStatus === 'completed') {
+ const contextCompactionOutcome = readContextCompactionOutcome(
+ fact.terminalEvent.actions?.stateDelta?.contextCompactionOutcome,
+ );
return {
sessionId,
turnId,
runId,
status: 'completed',
terminalEventId: fact.terminalEvent.id,
+ ...(contextCompactionOutcome ? { contextCompactionOutcome } : {}),
};
}
if (fact.runStatus === 'failed') {
@@ -105,6 +110,21 @@ export async function readCanonicalTurnSnapshot(
return { sessionId, turnId, runId, status: run.status };
}
+function readContextCompactionOutcome(value: unknown): ContextCompactionOutcome | undefined {
+ if (!value || typeof value !== 'object') return undefined;
+ const outcome = value as Record<string, unknown>;
+ if (outcome.kind === 'compacted' && typeof outcome.checkpointId === 'string') {
+ return { kind: 'compacted', checkpointId: outcome.checkpointId };
+ }
+ if (
+ (outcome.kind === 'unchanged' || outcome.kind === 'failed') &&
+ typeof outcome.reason === 'string'
+ ) {
+ return { kind: outcome.kind, reason: outcome.reason };
+ }
+ return undefined;
+}
+
/** Maximizes the encoded size of a protocol-valid failed Turn snapshot. */
export function worstCaseFailedTurnSnapshot(identity: CanonicalTurnIdentity): TurnSnapshot {
return {
diff --git a/packages/runtime-host/src/server/context-coordinator.ts b/packages/runtime-host/src/server/context-coordinator.ts
index f693715bc..706ff0c5e 100644
--- a/packages/runtime-host/src/server/context-coordinator.ts
+++ b/packages/runtime-host/src/server/context-coordinator.ts
@@ -28,6 +28,7 @@ import { type SessionManager } from '@maka/runtime/session-manager';
import { isSessionNotFoundError } from '@maka/storage/execution-stores';
import type {
ContextCompactInput,
+ ContextCompactResult,
ContextDiagnosticsQueryInput,
OperationOutcome,
} from '../protocol/index.js';
@@ -108,7 +109,7 @@ export class HostContextCoordinator {
const admitted = await this.#executions.admit(this.#admission(input, existing));
return {
ok: true,
- result: admitted.snapshot,
+ result: this.#compactResult(admitted.snapshot),
};
}
@@ -141,7 +142,7 @@ export class HostContextCoordinator {
const admitted = await prepared.admit(this.#admission(input, identity));
return {
ok: true,
- result: admitted.snapshot,
+ result: this.#compactResult(admitted.snapshot),
};
} catch (error) {
if (error instanceof RuntimeHostedRootConflictError) {
@@ -157,6 +158,34 @@ export class HostContextCoordinator {
}
}
+ #compactResult(turn: import('../protocol/index.js').TurnSnapshot): ContextCompactResult {
+ if (turn.status === 'completed') {
+ if (!turn.contextCompactionOutcome) {
+ return {
+ kind: 'finished',
+ turn,
+ outcome: { kind: 'failed', reason: 'missing_durable_outcome' },
+ };
+ }
+ return {
+ kind: 'finished',
+ turn,
+ outcome: turn.contextCompactionOutcome,
+ };
+ }
+ if (turn.status === 'failed' || turn.status === 'cancelled') {
+ return {
+ kind: 'finished',
+ turn,
+ outcome: {
+ kind: 'failed',
+ reason: turn.status === 'failed' ? turn.failureClass : turn.abortSource,
+ },
+ };
+ }
+ return { kind: 'started', turn };
+ }
+
async #preflight(
input: ContextCompactInput,
): Promise<OperationOutcome<'context.compact'> | undefined> {
diff --git a/packages/runtime-host/src/server/execution-model-composition.ts b/packages/runtime-host/src/server/execution-model-composition.ts
index 5c0f3acda..015e9b644 100644
--- a/packages/runtime-host/src/server/execution-model-composition.ts
+++ b/packages/runtime-host/src/server/execution-model-composition.ts
@@ -396,7 +396,6 @@ export async function createHostAiSdkBackend(input: HostAiSdkBackendInput): Prom
recordHistoryCompactCheckpoint: input.context.recordHistoryCompactCheckpoint,
loadTurnRuntimeEvents: input.context.loadTurnRuntimeEvents,
allowMidTurnHistoryCompaction: input.context.allowMidTurnHistoryCompaction,
- recordSemanticCompactBlock: input.context.recordSemanticCompactBlock,
recordRunTrace: input.context.recordRunTrace,
...(commitRunComposition
? {
diff --git a/packages/runtime-host/src/test-only/desktop-e2e-execution.ts b/packages/runtime-host/src/test-only/desktop-e2e-execution.ts
index 5ac43c4b9..65754a5f3 100644
--- a/packages/runtime-host/src/test-only/desktop-e2e-execution.ts
+++ b/packages/runtime-host/src/test-only/desktop-e2e-execution.ts
@@ -61,7 +61,7 @@ export class DesktopE2eBackend extends FakeBackend {
].join('\n'),
});
await recordCheckpoint(checkpoint, input.turnId);
- return {};
+ return { outcome: { kind: 'compacted', checkpointId: checkpoint.checkpointId } };
}
}
diff --git a/packages/runtime/package.json b/packages/runtime/package.json
index ad2df56c7..79915e1a1 100644
--- a/packages/runtime/package.json
+++ b/packages/runtime/package.json
@@ -59,7 +59,6 @@
"./goal-state": "./dist/goal-state.js",
"./goal-tools": "./dist/goal-tools.js",
"./goal-turn-lifecycle": "./dist/goal-turn-lifecycle.js",
- "./history-compact-artifacts": "./dist/history-compact-artifacts.js",
"./history-compact-checkpoint": "./dist/history-compact-checkpoint.js",
"./history-compact-ledger": "./dist/history-compact-ledger.js",
"./history-compact-summarizer": "./dist/history-compact-summarizer.js",
diff --git a/packages/runtime/src/__tests__/active-compaction-kernel.test.ts b/packages/runtime/src/__tests__/active-compaction-kernel.test.ts
deleted file mode 100644
index 50d048d1a..000000000
--- a/packages/runtime/src/__tests__/active-compaction-kernel.test.ts
+++ /dev/null
@@ -1,388 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import assert from 'node:assert/strict';
-import test from 'node:test';
-import type { RuntimeEvent } from '@maka/core/runtime-event';
-
-import {
- activeCompactionCoverageFromEntries,
- buildActiveCompactionHeadAnchor,
- buildActiveCompactionSourceIndex,
- selectActiveCompactionSafeSpan,
- validateActiveCompactionCoverageForSourceIndex,
-} from '../active-compaction-kernel.js';
-import type { ModelMessage } from '../model-protocol.js';
-import {
- ACTIVE_ARCHIVED_TOOL_RESULT_PLACEHOLDER_KIND,
- type ActiveArchivedToolResultPlaceholder,
-} from '../active-tool-result-prune.js';
-import { ARCHIVED_TOOL_RESULT_REWRITE_VERSION } from '../tool-result-archive.js';
-
-test('active compaction kernel selects only complete provider episodes after the head anchor', () => {
- const messages = fixtureMessages();
- const index = buildActiveCompactionSourceIndex({
- sessionId: 'session-1',
- turnId: 'turn-1',
- messages,
- stepNumber: 2,
- charsPerToken: 1,
- });
-
- const selection = selectActiveCompactionSafeSpan({
- index,
- messages,
- headAnchor: buildActiveCompactionHeadAnchor(messages, 0, 1),
- policy: {
- enabled: true,
- minStepNumber: 1,
- highWaterRatio: 0.1,
- maxActiveEstimatedTokens: 1,
- minSafePrefixEstimatedTokens: 0,
- preserveRecentCompletedEpisodes: 1,
- },
- });
-
- assert.equal(selection.decision, 'selected');
- if (selection.decision !== 'selected') return;
- assert.equal(selection.startMessageIndex, 1);
- assert.equal(selection.endMessageIndex, 2);
- assert.deepEqual(selection.coverage.toolCallIds, ['call-1']);
- assert.equal(
- validateActiveCompactionCoverageForSourceIndex(selection.coverage, index).valid,
- true,
- );
-});
-
-test('active compaction kernel fails closed on hash drift and split tool pairs', () => {
- const messages = fixtureMessages();
- const index = buildActiveCompactionSourceIndex({
- sessionId: 'session-1',
- turnId: 'turn-1',
- messages,
- });
- const callEntry = index.entries.find((entry) => entry.contentKind === 'function_call');
- assert.ok(callEntry);
-
- const splitCoverage = activeCompactionCoverageFromEntries([callEntry]);
- const splitValidation = validateActiveCompactionCoverageForSourceIndex(splitCoverage, index);
- assert.equal(splitValidation.valid, false);
- assert.ok(splitValidation.reasons.includes('tool_pair_split'));
-
- const hashValidation = validateActiveCompactionCoverageForSourceIndex(
- { ...splitCoverage, bodySha256: ['not-the-source-hash'] },
- index,
- );
- assert.equal(hashValidation.valid, false);
- assert.ok(hashValidation.reasons.includes('source_hash_mismatch'));
-});
-
-test('active compaction kernel does not relabel a tool result from a call-only runtime match', () => {
- const index = buildActiveCompactionSourceIndex({
- sessionId: 'session-1',
- turnId: 'turn-1',
- messages: fixtureMessages(),
- runtimeEvents: [
- {
- id: 'event-call-1',
- invocationId: 'invocation-1',
- runId: 'run-1',
- sessionId: 'session-1',
- turnId: 'turn-1',
- ts: 1,
- partial: false,
- role: 'model',
- author: 'agent',
- content: {
- kind: 'function_call',
- id: 'call-1',
- name: 'Read',
- args: { path: 'README.md' },
- },
- } satisfies RuntimeEvent,
- ],
- });
-
- const resultEntry = index.entries.find((entry) => entry.messageIndex === 2);
- assert.ok(resultEntry);
- assert.equal(resultEntry.contentKind, 'tool_result');
- assert.equal(resultEntry.runtimeEventId, undefined);
-});
-
-test('active compaction kernel binds runtime events by role exactly once', () => {
- const sharedText = 'same provider-visible text';
- const messages = [
- { role: 'user', content: sharedText },
- { role: 'assistant', content: sharedText },
- { role: 'assistant', content: sharedText },
- ] as ModelMessage[];
- const index = buildActiveCompactionSourceIndex({
- sessionId: 'session-1',
- turnId: 'turn-1',
- messages,
- runtimeEvents: [
- runtimeTextEvent('event-user', 'user', sharedText),
- runtimeTextEvent('event-model', 'model', sharedText),
- ],
- });
-
- assert.equal(index.entries[0]?.runtimeEventId, 'event-user');
- assert.equal(index.entries[1]?.runtimeEventId, 'event-model');
- assert.equal(index.entries[2]?.runtimeEventId, undefined);
-});
-
-test('active compaction kernel leaves ambiguous runtime matches unbound', () => {
- const sharedText = 'ambiguous model text';
- const index = buildActiveCompactionSourceIndex({
- sessionId: 'session-1',
- turnId: 'turn-1',
- messages: [{ role: 'assistant', content: sharedText }] as ModelMessage[],
- runtimeEvents: [
- runtimeTextEvent('event-model-1', 'model', sharedText),
- runtimeTextEvent('event-model-2', 'model', sharedText),
- ],
- });
-
- assert.equal(index.entries[0]?.runtimeEventId, undefined);
-});
-
-test('active compaction kernel fails open on duplicate or missing tool identities', () => {
- for (const messages of malformedToolEpisodes()) {
- const index = buildActiveCompactionSourceIndex({
- sessionId: 'session-1',
- turnId: 'turn-1',
- messages,
- stepNumber: 2,
- charsPerToken: 1,
- });
- const selection = selectActiveCompactionSafeSpan({
- index,
- messages,
- headAnchor: buildActiveCompactionHeadAnchor(messages, 0, 1),
- policy: {
- enabled: true,
- minStepNumber: 1,
- highWaterRatio: 0.1,
- maxActiveEstimatedTokens: 1,
- minSafePrefixEstimatedTokens: 0,
- },
- });
-
- assert.ok(selection.decision === 'failedOpen');
- assert.equal(selection.reason, 'tool_pair_split');
- }
-});
-
-test('active compaction kernel preserves archived placeholder provenance with archiveRequired', () => {
- const placeholder = activePlaceholder();
- const messages = [
- { role: 'user', content: 'inspect the archive' },
- {
- role: 'assistant',
- content: [
- {
- type: 'tool-call',
- toolCallId: placeholder.toolCallId,
- toolName: placeholder.toolName,
- input: { path: 'notes.md' },
- },
- ],
- },
- {
- role: 'tool',
- content: [
- {
- type: 'tool-result',
- toolCallId: placeholder.toolCallId,
- toolName: placeholder.toolName,
- output: { type: 'text', value: JSON.stringify(placeholder) },
- },
- ],
- },
- ] as ModelMessage[];
- const index = buildActiveCompactionSourceIndex({
- sessionId: 'session-1',
- turnId: 'turn-1',
- messages,
- stepNumber: 2,
- charsPerToken: 1,
- runtimeEvents: [runtimeToolCallEvent(placeholder.toolCallId, placeholder.toolName)],
- });
- const archived = index.entries.find(
- (entry) => entry.contentKind === 'active_archive_placeholder',
- );
-
- assert.ok(archived);
- assert.equal(archived.archiveRef?.artifactId, placeholder.artifactId);
- assert.equal(archived.archiveRef?.bodySha256, placeholder.bodySha256);
- assert.equal(archived.archiveRef?.originalEstimatedTokens, placeholder.originalEstimatedTokens);
- assert.equal(archived.archiveRef?.originalBytes, placeholder.originalBytes);
- assert.equal(archived.toolCallId, placeholder.toolCallId);
-
- const selection = selectActiveCompactionSafeSpan({
- index,
- messages,
- headAnchor: buildActiveCompactionHeadAnchor(messages, 0, 1),
- policy: {
- enabled: true,
- minStepNumber: 1,
- highWaterRatio: 0.1,
- maxActiveEstimatedTokens: 1,
- minSafePrefixEstimatedTokens: 0,
- archiveRequired: true,
- },
- });
- assert.equal(selection.decision, 'selected');
-});
-
-test('active compaction kernel binds safe-span selection to the exact user head', () => {
- const messages = fixtureMessages();
- const headAnchor = buildActiveCompactionHeadAnchor(messages, 0);
- const changedMessages: ModelMessage[] = [
- { role: 'user', content: 'changed request' } as ModelMessage,
- ...messages.slice(1),
- ];
- const index = buildActiveCompactionSourceIndex({
- sessionId: 'session-1',
- turnId: 'turn-1',
- messages: changedMessages,
- });
-
- const selection = selectActiveCompactionSafeSpan({
- index,
- messages: changedMessages,
- headAnchor,
- policy: { enabled: true, minStepNumber: 0 },
- });
-
- assert.equal(selection.decision, 'failedOpen');
- assert.equal(selection.reason, 'head_anchor_mismatch');
-});
-
-function fixtureMessages(): ModelMessage[] {
- return [
- { role: 'user', content: 'inspect the repository' },
- {
- role: 'assistant',
- content: [
- {
- type: 'tool-call',
- toolCallId: 'call-1',
- toolName: 'Read',
- input: { path: 'README.md' },
- },
- ],
- },
- {
- role: 'tool',
- content: [
- {
- type: 'tool-result',
- toolCallId: 'call-1',
- toolName: 'Read',
- output: { type: 'text', value: 'contents' },
- },
- ],
- },
- { role: 'assistant', content: 'The repository is ready.' },
- ] as ModelMessage[];
-}
-
-function runtimeTextEvent(id: string, role: RuntimeEvent['role'], text: string): RuntimeEvent {
- return {
- id,
- invocationId: 'invocation-1',
- runId: 'run-1',
- sessionId: 'session-1',
- turnId: 'turn-1',
- ts: 1,
- partial: false,
- role,
- author: role === 'user' ? 'user' : 'agent',
- content: { kind: 'text', text },
- };
-}
-
-function runtimeToolCallEvent(toolCallId: string, toolName: string): RuntimeEvent {
- return {
- id: 'event-call',
- invocationId: 'invocation-1',
- runId: 'run-1',
- sessionId: 'session-1',
- turnId: 'turn-1',
- ts: 1,
- partial: false,
- role: 'model',
- author: 'agent',
- content: {
- kind: 'function_call',
- id: toolCallId,
- name: toolName,
- args: { path: 'notes.md' },
- },
- };
-}
-
-function activePlaceholder(): ActiveArchivedToolResultPlaceholder {
- return {
- kind: ACTIVE_ARCHIVED_TOOL_RESULT_PLACEHOLDER_KIND,
- rewriteVersion: ARCHIVED_TOOL_RESULT_REWRITE_VERSION,
- artifactId: 'artifact-tool-1',
- turnId: 'turn-1',
- toolCallId: 'call-archive-1',
- toolName: 'Read',
- bodySha256: 'a'.repeat(64),
- originalEstimatedTokens: 123,
- originalBytes: 456,
- reason: 'active_current_turn_tool_result_pruned_before_next_step',
- };
-}
-
-function malformedToolEpisodes(): ModelMessage[][] {
- const head = { role: 'user', content: 'inspect' } as ModelMessage;
- const call = (toolCallId?: string) =>
- ({
- role: 'assistant',
- content: [
- {
- type: 'tool-call',
- ...(toolCallId ? { toolCallId } : {}),
- toolName: 'Read',
- input: { path: 'README.md' },
- },
- ],
- }) as ModelMessage;
- const result = (toolCallId: string) =>
- ({
- role: 'tool',
- content: [
- {
- type: 'tool-result',
- toolCallId,
- toolName: 'Read',
- output: { type: 'text', value: 'contents' },
- },
- ],
- }) as ModelMessage;
- return [
- [head, call('duplicate'), call('duplicate'), result('duplicate')],
- [head, call('duplicate'), result('duplicate'), result('duplicate')],
- [head, call(), result('missing')],
- ];
-}
diff --git a/packages/runtime/src/__tests__/active-tool-result-prune.test.ts b/packages/runtime/src/__tests__/active-tool-result-prune.test.ts
index aae79e767..db8673ab1 100644
--- a/packages/runtime/src/__tests__/active-tool-result-prune.test.ts
+++ b/packages/runtime/src/__tests__/active-tool-result-prune.test.ts
@@ -22,10 +22,7 @@ import { describe, test } from 'node:test';
import { z } from 'zod';
import type { ModelMessage } from '../model-protocol.js';
-import {
- activeToolResultLineageIdentity,
- rewriteActiveToolResultsInMessages,
-} from '../active-tool-result-prune.js';
+import { rewriteActiveToolResultsInMessages } from '../active-tool-result-prune.js';
import { planActiveToolResultSupersession } from '../active-tool-result-working-set.js';
import { composeRequestProjection } from '../request-projection.js';
import { ToolAvailabilityRuntime, LOAD_TOOLS_NAME } from '../tool-availability.js';
@@ -273,36 +270,6 @@ describe('active current-turn tool-result pruning', () => {
assert.deepEqual(second.messages, first.messages);
});
- test('raw result and archive placeholder share a stable lineage identity', async () => {
- const messages = [largeTextToolMessage('Read', 'tool-1', 'SECRET'.repeat(20))];
- const rawPart = (messages[0] as Extract<ModelMessage, { role: 'tool' }>).content[0];
- const rewritten = await rewriteActiveToolResultsInMessages({
- messages,
- policy: { enabled: true, maxCurrentResultEstimatedTokens: 1 },
- stepNumber: 1,
- turnId: 'turn-1',
- charsPerToken: 1,
- archiveToolResult: () => ({ artifactId: 'artifact-tool-1' }),
- });
- const placeholderPart = (rewritten.messages[0] as Extract<ModelMessage, { role: 'tool' }>)
- .content[0];
- const differentPart = (
- largeTextToolMessage('Read', 'tool-1', 'DIFFERENT'.repeat(20)) as Extract<
- ModelMessage,
- { role: 'tool' }
- >
- ).content[0];
-
- assert.deepEqual(
- activeToolResultLineageIdentity(placeholderPart),
- activeToolResultLineageIdentity(rawPart),
- );
- assert.notDeepEqual(
- activeToolResultLineageIdentity(differentPart),
- activeToolResultLineageIdentity(rawPart),
- );
- });
-
test('returns active prune diagnostics for rewritten and failed archives', async () => {
const success = await rewriteActiveToolResultsInMessages({
messages: [largeToolMessage('Read', 'tool-1', 'SECRET'.repeat(200))],
diff --git a/packages/runtime/src/__tests__/agent-graph-supervisor-wake.test.ts b/packages/runtime/src/__tests__/agent-graph-supervisor-wake.test.ts
index e0cb7731d..1fd22eefc 100644
--- a/packages/runtime/src/__tests__/agent-graph-supervisor-wake.test.ts
+++ b/packages/runtime/src/__tests__/agent-graph-supervisor-wake.test.ts
@@ -32,8 +32,8 @@ import type { AgentGraphClientSnapshot } from '../stream-graph-read-model.js';
import type { AgentGraphScheduleReconciliationResult } from '../stream-graph-schedule-reconcile.js';
describe('Agent Graph supervisor wake delivery', () => {
- test('uses the shared aggressive compaction adapter for overflow recovery', async () => {
- const calls: Array<{ sessionId: string; turnId: string; minRecentTurns: number }> = [];
+ test('uses the shared compaction transaction for overflow recovery', async () => {
+ const calls: Array<{ sessionId: string; turnId: string }> = [];
const recovery = await recoverAgentGraphSupervisorContextOverflow({
rootSessionId: 'root-session',
compactTurnId: 'compact-turn',
@@ -55,23 +55,27 @@ describe('Agent Graph supervisor wake delivery', () => {
droppedTurns: 20,
keptEvents: 4,
droppedEvents: 80,
- historyCompactedEvents: 75,
- historyCompactBlocksWritten: 1,
+ compactionDecisions: [
+ {
+ stage: 'priorReplay',
+ sourceKind: 'runtimeEvents',
+ decision: 'replaced',
+ boundaryKind: 'historyCompact',
+ boundaryIds: ['checkpoint-1'],
+ },
+ ],
},
};
},
});
- assert.deepEqual(calls, [
- { sessionId: 'root-session', turnId: 'compact-turn', minRecentTurns: 0 },
- ]);
+ assert.deepEqual(calls, [{ sessionId: 'root-session', turnId: 'compact-turn' }]);
assert.deepEqual(recovery, {
estimatedTokensBefore: 700_000,
estimatedTokensAfter: 12_000,
droppedTurns: 20,
droppedEvents: 80,
- historyCompactedEvents: 75,
- historyCompactBlocksWritten: 1,
+ outcome: { kind: 'compacted', checkpointId: 'checkpoint-1' },
});
});
@@ -197,8 +201,7 @@ describe('Agent Graph supervisor wake delivery', () => {
estimatedTokensBefore: 700_000,
estimatedTokensAfter: 12_000,
droppedEvents: 80,
- historyCompactedEvents: 75,
- historyCompactBlocksWritten: 1,
+ outcome: { kind: 'compacted', checkpointId: 'checkpoint-1' },
};
},
newId: sequentialIds(),
@@ -227,8 +230,7 @@ describe('Agent Graph supervisor wake delivery', () => {
estimatedTokensBefore: 700_000,
estimatedTokensAfter: 12_000,
droppedEvents: 80,
- historyCompactedEvents: 75,
- historyCompactBlocksWritten: 1,
+ outcome: { kind: 'compacted', checkpointId: 'checkpoint-1' },
},
});
} finally {
diff --git a/packages/runtime/src/__tests__/ai-sdk-backend.test.ts b/packages/runtime/src/__tests__/ai-sdk-backend.test.ts
index cbf8a5e2c..f4123c838 100644
--- a/packages/runtime/src/__tests__/ai-sdk-backend.test.ts
+++ b/packages/runtime/src/__tests__/ai-sdk-backend.test.ts
@@ -62,22 +62,13 @@ import {
ARCHIVED_TOOL_RESULT_PLACEHOLDER_KIND,
ARCHIVED_TOOL_RESULT_REWRITE_VERSION,
applyRuntimeEventContextBudget,
- buildHistoryCompactBlockFromSummary,
- type HistoryCompactBlock,
- type SynthesisCacheBlock,
} from '../context-budget.js';
import {
buildHistoryCompactCheckpoint,
type HistoryCompactCheckpoint,
} from '../history-compact-checkpoint.js';
-import {
- loadHistoryCompactBlocksFromArtifacts,
- persistHistoryCompactBlocksToArtifacts,
-} from '../history-compact-artifacts.js';
import { buildDefaultContextBudgetPolicy } from '../context-budget-policy.js';
-import { memoryArtifactStore } from './memory-artifact-store.js';
import { buildRuntimeEventModelReplayPlan, buildSteeringEnvelope } from '../model-history.js';
-import type { SemanticCompactBlock } from '../semantic-compact.js';
import { HistoryCompactSummarizerError } from '../history-compact-summarizer.js';
import type { SandboxDiagnosticsSnapshot } from '../sandbox/diagnostics.js';
import { SandboxCommandError } from '../sandbox/errors.js';
@@ -546,10 +537,6 @@ describe('AiSdkBackend Memory Extraction triggers', () => {
charsPerToken: 1,
historyCompact: {
enabled: true,
- mode: 'read_write',
- highWaterRatio: 0.01,
- tailEstimatedTokens: 20,
- maxSummaryEstimatedTokens: 500,
},
},
summarizeHistoryCompact: async () => structuredSummary('AUTOMATIC_MEMORY_SUMMARY'),
@@ -637,7 +624,7 @@ describe('AiSdkBackend Memory Extraction triggers', () => {
contextBudget: {
maxHistoryEstimatedTokens: 1_500,
charsPerToken: 1,
- historyCompact: { enabled: true, mode: 'read_write' },
+ historyCompact: { enabled: true },
},
summarizeHistoryCompact: async () => structuredSummary('must not summarize'),
recordHistoryCompactCheckpoint: (checkpoint) => {
@@ -700,10 +687,6 @@ describe('AiSdkBackend Memory Extraction triggers', () => {
charsPerToken: 1,
historyCompact: {
enabled: true,
- mode: 'read_write',
- highWaterRatio: 0.01,
- tailEstimatedTokens: 20,
- maxSummaryEstimatedTokens: 500,
},
},
summarizeHistoryCompact: async () => structuredSummary('DENIED_MEMORY_SUMMARY'),
@@ -777,10 +760,6 @@ describe('AiSdkBackend Memory Extraction triggers', () => {
charsPerToken: 1,
historyCompact: {
enabled: true,
- mode: 'read_write',
- highWaterRatio: 0.01,
- tailEstimatedTokens: 20,
- maxSummaryEstimatedTokens: 500,
},
},
summarizeHistoryCompact: async () => structuredSummary('UNAVAILABLE_MEMORY_SUMMARY'),
@@ -4207,10 +4186,9 @@ describe('AiSdkBackend model history', () => {
assert.equal(prompt.includes(newResult.body), false);
});
- test('history search does not re-add stale full tool results after archive pruning', async () => {
- const model = completionModel();
- const events: SessionEvent[] = [];
- const oldResult = { body: 'SECRET_PAYLOAD_SHOULD_NOT_RETURN'.repeat(20) };
+ test('manual compactHistory writes a V2 checkpoint without the legacy artifact writer', async () => {
+ const recorded: HistoryCompactCheckpoint[] = [];
+ let memoryDispatches = 0;
const backend = createTestAiSdkBackend({
sessionId: 'session-1',
header: header(),
@@ -4218,89 +4196,70 @@ describe('AiSdkBackend model history', () => {
connection: connection(),
apiKey: 'sk-test',
modelId: 'mock-model-id',
- modelFactory: () => model,
+ modelFactory: () => completionModel(),
tools: [],
newId: idGenerator(),
now: monotonicClock(),
contextBudget: {
- name: 'archive-search-test',
- maxHistoryTurns: 1,
- minRecentTurns: 0,
- staleToolResultPrune: {
- enabled: true,
- maxResultEstimatedTokens: 1,
- minRecentTurnsFull: 0,
- },
- historySearch: {
- enabled: true,
- maxResults: 1,
- around: 1,
- maxEstimatedTokens: 4096,
- },
+ name: 'manual-v2-compact-test',
+ maxHistoryEstimatedTokens: 10_000,
charsPerToken: 1,
},
- toolResultArchive: testToolResultArchive({
- archiveToolResult: async (event) => ({ artifactId: `artifact-${event.runtimeEventId}` }),
- }),
+ summarizeHistoryCompact: async () => structuredSummary('MANUAL_V2_HISTORY_COMPACT_SENTINEL'),
+ recordHistoryCompactCheckpoint: (checkpoint) => {
+ recorded.push(checkpoint);
+ },
+ memoryExtraction: {
+ gate: async () => ({ allowed: true }),
+ remember: async () => ({ status: 'unavailable', requestedItems: [] }),
+ extract: () => {
+ memoryDispatches += 1;
+ },
+ },
});
- for await (const event of backend.send({
- turnId: 'turn-current',
- text: 'Find SECRET_PAYLOAD_SHOULD_NOT_RETURN',
- context: [],
+ const result = await backend.compactHistory({
+ turnId: 'turn-compact',
+ runId: 'run-1',
runtimeContext: [
- runtimeEvent({
- id: 'rt-call',
- turnId: 'turn-old',
+ runtimeTextEvent({
+ id: 'manual-v2-old-1',
+ turnId: 'turn-old-1',
+ role: 'user',
+ author: 'user',
+ text: 'manual v2 old alpha '.repeat(100),
+ }),
+ runtimeTextEvent({
+ id: 'manual-v2-old-2',
+ turnId: 'turn-old-2',
role: 'model',
author: 'agent',
- content: {
- kind: 'function_call',
- id: 'tool-1',
- name: 'Read',
- args: { path: 'secret.txt' },
- },
- }),
- runtimeEvent({
- id: 'rt-result',
- turnId: 'turn-old',
- role: 'tool',
- author: 'tool',
- content: {
- kind: 'function_response',
- id: 'tool-1',
- name: 'Read',
- result: oldResult,
- isError: false,
- },
+ text: 'manual v2 old beta '.repeat(100),
}),
- runtimeEvent({
- id: 'rt-new',
- turnId: 'turn-new',
+ runtimeTextEvent({
+ id: 'manual-v2-recent',
+ turnId: 'turn-recent',
role: 'user',
author: 'user',
- content: { kind: 'text', text: 'newer retained context' },
+ text: 'manual v2 recent retained context',
}),
],
- })) {
- events.push(event);
- }
+ });
- const prompt = JSON.stringify(compactPrompt(model));
- assert.equal(prompt.includes(oldResult.body), false);
- const usage = events.find(
- (event): event is Extract<SessionEvent, { type: 'token_usage' }> =>
- event.type === 'token_usage',
+ assert.equal(recorded.length, 1);
+ assert.equal(
+ recorded[0]?.version === 2 ? recorded[0].summary : undefined,
+ structuredSummary('MANUAL_V2_HISTORY_COMPACT_SENTINEL'),
);
- assert.equal(usage?.contextBudget?.archivePlaceholders, 1);
- assert.equal(usage?.contextBudget?.historySearchMatches, 0);
+ assert.deepEqual(recorded[0]?.coverage.eventCount, 3);
+ assert.equal(recorded[0]?.memoryExtractionBoundary, undefined);
+ assert.equal(memoryDispatches, 0);
+ assert.equal(result.outcome.kind, 'compacted');
+ assert.equal(result.contextBudget?.compactionDecisions?.[0]?.decision, 'replaced');
});
- test('hydrates archived RuntimeEvent tool results for model replay when retrieval is enabled', async () => {
- const model = completionModel();
- const events: SessionEvent[] = [];
- let archivedBody = '';
- const oldResult = { body: 'retrieved 中文 archived payload 🙂'.repeat(3) };
+ test('manual compactHistory compacts one completed turn with multiple agent steps', async () => {
+ const recorded: HistoryCompactCheckpoint[] = [];
const backend = createTestAiSdkBackend({
sessionId: 'session-1',
header: header(),
@@ -4308,146 +4267,81 @@ describe('AiSdkBackend model history', () => {
connection: connection(),
apiKey: 'sk-test',
modelId: 'mock-model-id',
- modelFactory: () => model,
+ modelFactory: () => completionModel(),
tools: [],
newId: idGenerator(),
now: monotonicClock(),
contextBudget: {
- name: 'archive-retrieval-test',
- staleToolResultPrune: {
- enabled: true,
- maxResultEstimatedTokens: 1,
- minRecentTurnsFull: 0,
- },
- archiveRetrieval: {
- enabled: true,
- maxResults: 1,
- maxEstimatedTokens: 1024,
- maxBytes: 1024,
- },
+ name: 'manual-single-turn-compact-test',
+ maxHistoryEstimatedTokens: 10_000,
charsPerToken: 1,
},
- toolResultArchive: testToolResultArchive({
- archiveToolResult: async (event) => {
- archivedBody = event.serializedResult;
- return { artifactId: `artifact-${event.runtimeEventId}` };
- },
- readToolResultArchive: async (event) => {
- if (event.originalBytes !== utf8Bytes(archivedBody)) {
- return { ok: false, reason: 'size_mismatch' };
- }
- return {
- ok: true,
- serializedResult: event.bodySha256 === sha256(archivedBody) ? archivedBody : 'tampered',
- };
- },
- }),
+ summarizeHistoryCompact: async () => structuredSummary('MANUAL_SINGLE_TURN_SENTINEL'),
+ recordHistoryCompactCheckpoint: (checkpoint) => {
+ recorded.push(checkpoint);
+ },
});
- for await (const event of backend.send({
- turnId: 'turn-current',
- text: 'current user',
- context: [],
+ const result = await backend.compactHistory({
+ turnId: 'turn-compact',
+ runId: 'run-1',
runtimeContext: [
- runtimeEvent({
- id: 'rt-call',
- turnId: 'turn-prev',
+ runtimeTextEvent({
+ id: 'single-turn-user',
+ turnId: 'turn-work',
+ role: 'user',
+ author: 'user',
+ text: 'Implement the requested change.',
+ }),
+ runtimeTextEvent({
+ id: 'single-turn-agent-step-1',
+ turnId: 'turn-work',
role: 'model',
author: 'agent',
- content: {
- kind: 'function_call',
- id: 'tool-1',
- name: 'Read',
- args: { path: 'package.json' },
- },
+ text: 'Inspected the current implementation. '.repeat(40),
}),
- runtimeEvent({
- id: 'rt-result',
- turnId: 'turn-prev',
- role: 'tool',
- author: 'tool',
- content: {
- kind: 'function_response',
- id: 'tool-1',
- name: 'Read',
- result: oldResult,
- isError: false,
- },
+ runtimeTextEvent({
+ id: 'single-turn-agent-step-2',
+ turnId: 'turn-work',
+ role: 'model',
+ author: 'agent',
+ text: 'Completed and verified the change. '.repeat(40),
}),
],
- })) {
- events.push(event);
- }
-
- const prompt = JSON.stringify(compactPrompt(model));
- assert.match(prompt, /retrieved 中文 archived payload/);
- assert.equal(prompt.includes('maka.archived_tool_result'), false);
- const usage = events.find(
- (event): event is Extract<SessionEvent, { type: 'token_usage' }> =>
- event.type === 'token_usage',
- );
- assert.equal(usage?.contextBudget?.retrievedArchiveToolResults, 1);
- assert.equal(usage?.contextBudget?.archiveRetrievalFailures, 0);
- });
-
- test('gates archive hydration to RuntimeEvent turns selected by history search', async () => {
- const selectedResult = { body: 'PHASE7_SELECTED_SENTINEL'.repeat(20) };
- const unselectedResult = { body: 'PHASE7_UNSELECTED_SENTINEL'.repeat(20) };
- const result = await runArchiveGatedReplay({
- query: 'Find needle-a and recover its archived result',
- selectedResult,
- unselectedResult,
- selectedPath: 'needle-a.txt',
- unselectedPath: 'needle-b.txt',
});
- assert.deepEqual(result.readRuntimeEventIds, ['rt-result-a']);
- assert.match(result.prompt, /PHASE7_SELECTED_SENTINEL/);
- assert.equal(result.prompt.includes('PHASE7_UNSELECTED_SENTINEL'), false);
- assert.equal(result.usage?.contextBudget?.archiveRetrievalMode, 'history_search_gated');
- assert.equal(result.usage?.contextBudget?.archiveRetrievalEligibleTurns, 1);
- assert.equal(result.usage?.contextBudget?.retrievedArchiveToolResults, 1);
- assert.equal(result.usage?.contextBudget?.historySearchMatches, 1);
- });
-
- test('gates archive hydration using searchable original tool result content', async () => {
- const selectedResult = { body: 'zzztokenbodyonly777'.repeat(20) };
- const unselectedResult = { body: 'other_payload'.repeat(20) };
- const result = await runArchiveGatedReplay({
- query: 'zzztokenbodyonly777',
- selectedResult,
- unselectedResult,
- selectedPath: 'selected.txt',
- unselectedPath: 'unselected.txt',
- selectedAfterUnselected: true,
- });
-
- assert.deepEqual(result.readRuntimeEventIds, ['rt-result-a']);
- assert.match(result.prompt, /zzztokenbodyonly777/);
- assert.equal(result.prompt.includes('other_payload'), false);
- assert.equal(result.usage?.contextBudget?.archiveRetrievalMode, 'history_search_gated');
- assert.equal(result.usage?.contextBudget?.retrievedArchiveToolResults, 1);
- assert.equal(result.usage?.contextBudget?.historySearchMatches, 1);
+ assert.equal(recorded.length, 1);
+ assert.equal(recorded[0]?.coverage.turnCount, 1);
+ assert.equal(recorded[0]?.coverage.eventCount, 3);
+ assert.equal(result.contextBudget?.compactionDecisions?.[0]?.decision, 'replaced');
});
- test('uses selected synthesis block instead of hydrating covered archived payload', async () => {
- const model = completionModel();
- const events: SessionEvent[] = [];
- const archivedBodies = new Map<string, string>();
- const readRuntimeEventIds: string[] = [];
- const writeInputs: Array<{ turnId: string; query: string }> = [];
- const oldResult = { body: 'RAW_SYNTHESIS_ARCHIVE_PAYLOAD'.repeat(20) };
- const serialized = JSON.stringify(oldResult);
- const block = synthesisBlock({
- queryKey: 'key-alpha',
- turnId: 'turn-alpha',
- runtimeEventId: 'rt-result-alpha',
- toolCallId: 'tool-alpha',
- artifactId: 'artifact-rt-result-alpha',
- bodySha256: sha256(serialized),
- originalEstimatedTokens: serialized.length,
- originalBytes: utf8Bytes(serialized),
+ test('manual compactHistory rolls forward from the previous V2 checkpoint', async () => {
+ const oldEvents = [
+ runtimeTextEvent({
+ id: 'manual-v2-roll-old-1',
+ turnId: 'manual-v2-roll-turn-1',
+ role: 'user',
+ author: 'user',
+ text: 'manual v2 roll old alpha '.repeat(12),
+ }),
+ runtimeTextEvent({
+ id: 'manual-v2-roll-old-2',
+ turnId: 'manual-v2-roll-turn-2',
+ role: 'model',
+ author: 'agent',
+ text: 'manual v2 roll old beta '.repeat(12),
+ }),
+ ];
+ const previous = buildHistoryCompactCheckpoint({
+ sessionId: 'session-1',
+ coveredRuntimeEvents: oldEvents.slice(0, 1),
+ summary: 'MANUAL_V2_PREVIOUS_SUMMARY',
+ summaryFormat: 'legacy_freeform',
+ charsPerToken: 1,
});
+ const summaryInputs: Array<{ previous?: string; newlyFoldedIds: string[] }> = [];
+ const recorded: HistoryCompactCheckpoint[] = [];
const backend = createTestAiSdkBackend({
sessionId: 'session-1',
header: header(),
@@ -4455,958 +4349,52 @@ describe('AiSdkBackend model history', () => {
connection: connection(),
apiKey: 'sk-test',
modelId: 'mock-model-id',
- modelFactory: () => model,
+ modelFactory: () => completionModel(),
tools: [],
newId: idGenerator(),
now: monotonicClock(),
contextBudget: {
- name: 'synthesis-cache-test',
- maxHistoryTurns: 1,
- minRecentTurns: 0,
- staleToolResultPrune: {
- enabled: true,
- maxResultEstimatedTokens: 1,
- minRecentTurnsFull: 0,
- },
- archiveRetrieval: {
- enabled: true,
- mode: 'history_search_gated',
- maxResults: 1,
- maxEstimatedTokens: 4096,
- maxBytes: 4096,
- },
- historySearch: {
- enabled: true,
- maxResults: 1,
- around: 1,
- maxEstimatedTokens: 4096,
- },
- synthesisCache: {
- enabled: true,
- mode: 'read_write',
- blocks: [block],
- },
+ name: 'manual-v2-roll-test',
+ maxHistoryEstimatedTokens: 10_000,
charsPerToken: 1,
},
- toolResultArchive: testToolResultArchive({
- archiveToolResult: async (event) => {
- archivedBodies.set(event.runtimeEventId, event.serializedResult);
- return { artifactId: `artifact-${event.runtimeEventId}` };
- },
- readToolResultArchive: async (event) => {
- readRuntimeEventIds.push(event.runtimeEventId);
- const body = archivedBodies.get(event.runtimeEventId);
- return body ? { ok: true, serializedResult: body } : { ok: false, reason: 'not_found' };
- },
- }),
- writeSynthesisCache: async (event) => {
- writeInputs.push({ turnId: event.turnId, query: event.source.query });
- return { blocks: [] };
+ loadHistoryCompactCheckpoint: () => previous,
+ summarizeHistoryCompact: async (input) => {
+ summaryInputs.push({
+ previous:
+ input.previousCheckpoint?.version === 2 ? input.previousCheckpoint.summary : undefined,
+ newlyFoldedIds: (input.newlyFoldedRuntimeEvents ?? []).map((event) => event.id),
+ });
+ return structuredSummary('MANUAL_V2_ROLLED_SUMMARY');
+ },
+ recordHistoryCompactCheckpoint: (checkpoint) => {
+ recorded.push(checkpoint);
},
});
- for await (const event of backend.send({
- turnId: 'turn-current',
- text: 'Recover key-alpha',
- context: [],
+ await backend.compactHistory({
+ turnId: 'turn-compact',
+ runId: 'run-1',
runtimeContext: [
- runtimeEvent({
- id: 'rt-call-alpha',
- turnId: 'turn-alpha',
- role: 'model',
- author: 'agent',
- content: {
- kind: 'function_call',
- id: 'tool-alpha',
- name: 'Read',
- args: { path: 'key-alpha.txt' },
- },
- }),
- runtimeEvent({
- id: 'rt-result-alpha',
- turnId: 'turn-alpha',
- role: 'tool',
- author: 'tool',
- content: {
- kind: 'function_response',
- id: 'tool-alpha',
- name: 'Read',
- result: oldResult,
- isError: false,
- },
- }),
+ ...oldEvents,
runtimeTextEvent({
- id: 'rt-new',
- turnId: 'turn-new',
+ id: 'manual-v2-roll-recent',
+ turnId: 'manual-v2-roll-recent-turn',
role: 'user',
author: 'user',
- text: 'newer retained context',
+ text: 'manual v2 roll retained context',
}),
],
- })) {
- events.push(event);
- }
-
- assert.deepEqual(readRuntimeEventIds, []);
- assert.deepEqual(writeInputs, []);
- const prompt = JSON.stringify(compactPrompt(model));
- assert.match(prompt, /maka_synthesis_cache_block/);
- assert.match(prompt, /SYNTHESIS_SENTINEL_KEY_ALPHA/);
- assert.equal(prompt.includes('RAW_SYNTHESIS_ARCHIVE_PAYLOAD'), false);
- const usage = events.find(
- (event): event is Extract<SessionEvent, { type: 'token_usage' }> =>
- event.type === 'token_usage',
- );
- assert.equal(usage?.contextBudget?.synthesisCacheBlocksSelected, 1);
- assert.deepEqual(usage?.contextBudget?.synthesisCacheBlockIds, ['synth-key-alpha']);
- assert.equal(usage?.contextBudget?.retrievedArchiveToolResults, undefined);
- });
-
- test('A/B: synthesis cache cuts replay tokens vs hydrating the archived payload', async () => {
- // Deterministic economic-mechanism benchmark for #578. The same replay is
- // projected twice against a fake model: the baseline re-hydrates the full
- // archived tool result; the arm injects the compact synthesis block instead.
- // The prompt the model actually receives is ground truth, so its size delta
- // is the synthesis cache's replay-token saving — no live model or network,
- // so the number is reproducible in CI. (Live Terminal-Bench runs can't drive
- // this: their single-long-turn shape never archives-then-retrieves a turn, so
- // the write path stays source_missing — see docs/archive/economic-mechanisms-benchmark.md.)
- const CHARS_PER_TOKEN = 1; // 1:1 so prompt chars are directly the token estimate
- const archivedResult = { body: 'RAW_SYNTHESIS_ARCHIVE_PAYLOAD '.repeat(80).trim() };
- const serialized = JSON.stringify(archivedResult);
- const block = synthesisBlock({
- queryKey: 'key-alpha',
- turnId: 'turn-alpha',
- runtimeEventId: 'rt-result-alpha',
- toolCallId: 'tool-alpha',
- artifactId: 'artifact-rt-result-alpha',
- bodySha256: sha256(serialized),
- originalEstimatedTokens: serialized.length,
- originalBytes: utf8Bytes(serialized),
- });
-
- async function projectReplayPromptChars(useSynthesisCache: boolean): Promise<string> {
- const model = completionModel();
- const archivedBodies = new Map<string, string>();
- const backend = createTestAiSdkBackend({
- sessionId: 'session-1',
- header: header(),
- appendMessage: async () => {},
- connection: connection(),
- apiKey: 'sk-test',
- modelId: 'mock-model-id',
- modelFactory: () => model,
- tools: [],
- newId: idGenerator(),
- now: monotonicClock(),
- contextBudget: {
- name: 'synthesis-cache-benchmark',
- maxHistoryTurns: 1,
- minRecentTurns: 0,
- staleToolResultPrune: {
- enabled: true,
- maxResultEstimatedTokens: 1,
- minRecentTurnsFull: 0,
- },
- archiveRetrieval: {
- enabled: true,
- mode: 'history_search_gated',
- maxResults: 1,
- maxEstimatedTokens: 8192,
- maxBytes: 8192,
- },
- historySearch: { enabled: true, maxResults: 1, around: 1, maxEstimatedTokens: 8192 },
- ...(useSynthesisCache
- ? { synthesisCache: { enabled: true, mode: 'read_write', blocks: [block] } }
- : {}),
- charsPerToken: CHARS_PER_TOKEN,
- },
- toolResultArchive: testToolResultArchive({
- archiveToolResult: async (event) => {
- archivedBodies.set(event.runtimeEventId, event.serializedResult);
- return { artifactId: `artifact-${event.runtimeEventId}` };
- },
- readToolResultArchive: async (event) => {
- const body = archivedBodies.get(event.runtimeEventId);
- return body ? { ok: true, serializedResult: body } : { ok: false, reason: 'not_found' };
- },
- }),
- writeSynthesisCache: async () => ({ blocks: [] }),
- });
- for await (const _event of backend.send({
- turnId: 'turn-current',
- text: 'Recover key-alpha',
- context: [],
- runtimeContext: [
- runtimeEvent({
- id: 'rt-call-alpha',
- turnId: 'turn-alpha',
- role: 'model',
- author: 'agent',
- content: {
- kind: 'function_call',
- id: 'tool-alpha',
- name: 'Read',
- args: { path: 'key-alpha.txt' },
- },
- }),
- runtimeEvent({
- id: 'rt-result-alpha',
- turnId: 'turn-alpha',
- role: 'tool',
- author: 'tool',
- content: {
- kind: 'function_response',
- id: 'tool-alpha',
- name: 'Read',
- result: archivedResult,
- isError: false,
- },
- }),
- runtimeTextEvent({
- id: 'rt-new',
- turnId: 'turn-new',
- role: 'user',
- author: 'user',
- text: 'newer retained context',
- }),
- ],
- })) {
- // drain the stream so the request is issued to the mock model
- }
- return JSON.stringify(compactPrompt(model));
- }
-
- const baseline = await projectReplayPromptChars(false);
- const arm = await projectReplayPromptChars(true);
- const baselineTokens = Math.ceil(baseline.length / CHARS_PER_TOKEN);
- const armTokens = Math.ceil(arm.length / CHARS_PER_TOKEN);
-
- // Ground-truth: baseline hydrates the raw payload; the arm swaps in the block.
- assert.ok(
- baseline.includes('RAW_SYNTHESIS_ARCHIVE_PAYLOAD'),
- 'baseline should hydrate the archived payload',
- );
- assert.ok(arm.includes('maka_synthesis_cache_block'), 'arm should inject the synthesis block');
- assert.equal(
- arm.includes('RAW_SYNTHESIS_ARCHIVE_PAYLOAD'),
- false,
- 'arm must not hydrate the raw payload',
- );
- assert.ok(armTokens < baselineTokens, 'synthesis cache must reduce replay tokens');
- });
-
- test('loads synthesis blocks before archive retrieval', async () => {
- const model = completionModel();
- const events: SessionEvent[] = [];
- const archivedBodies = new Map<string, string>();
- const readRuntimeEventIds: string[] = [];
- const oldResult = { body: 'RAW_LOADED_SYNTHESIS_ARCHIVE_PAYLOAD'.repeat(20) };
- const serialized = JSON.stringify(oldResult);
- const block = synthesisBlock({
- queryKey: 'key-alpha',
- turnId: 'turn-alpha',
- runtimeEventId: 'rt-result-alpha',
- toolCallId: 'tool-alpha',
- artifactId: 'artifact-rt-result-alpha',
- bodySha256: sha256(serialized),
- originalEstimatedTokens: serialized.length,
- originalBytes: utf8Bytes(serialized),
- });
- let loadCalls = 0;
- const backend = createTestAiSdkBackend({
- sessionId: 'session-1',
- header: header(),
- appendMessage: async () => {},
- connection: connection(),
- apiKey: 'sk-test',
- modelId: 'mock-model-id',
- modelFactory: () => model,
- tools: [],
- newId: idGenerator(),
- now: monotonicClock(),
- contextBudget: {
- name: 'synthesis-cache-load-test',
- maxHistoryTurns: 1,
- minRecentTurns: 0,
- staleToolResultPrune: {
- enabled: true,
- maxResultEstimatedTokens: 1,
- minRecentTurnsFull: 0,
- },
- archiveRetrieval: {
- enabled: true,
- mode: 'history_search_gated',
- maxResults: 1,
- maxEstimatedTokens: 4096,
- maxBytes: 4096,
- },
- historySearch: {
- enabled: true,
- maxResults: 1,
- around: 1,
- maxEstimatedTokens: 4096,
- },
- synthesisCache: {
- enabled: true,
- maxBlocks: 1,
- },
- charsPerToken: 1,
- },
- toolResultArchive: testToolResultArchive({
- archiveToolResult: async (event) => {
- archivedBodies.set(event.runtimeEventId, event.serializedResult);
- return { artifactId: `artifact-${event.runtimeEventId}` };
- },
- readToolResultArchive: async (event) => {
- readRuntimeEventIds.push(event.runtimeEventId);
- const body = archivedBodies.get(event.runtimeEventId);
- return body ? { ok: true, serializedResult: body } : { ok: false, reason: 'not_found' };
- },
- }),
- loadSynthesisCache: async () => {
- loadCalls += 1;
- return { blocks: [block] };
- },
- });
-
- for await (const event of backend.send({
- turnId: 'turn-current',
- text: 'Recover key-alpha',
- context: [],
- runtimeContext: [
- runtimeEvent({
- id: 'rt-call-alpha',
- turnId: 'turn-alpha',
- role: 'model',
- author: 'agent',
- content: {
- kind: 'function_call',
- id: 'tool-alpha',
- name: 'Read',
- args: { path: 'key-alpha.txt' },
- },
- }),
- runtimeEvent({
- id: 'rt-result-alpha',
- turnId: 'turn-alpha',
- role: 'tool',
- author: 'tool',
- content: {
- kind: 'function_response',
- id: 'tool-alpha',
- name: 'Read',
- result: oldResult,
- isError: false,
- },
- }),
- runtimeTextEvent({
- id: 'rt-new',
- turnId: 'turn-new',
- role: 'user',
- author: 'user',
- text: 'newer retained context',
- }),
- ],
- })) {
- events.push(event);
- }
-
- assert.equal(loadCalls, 1);
- assert.deepEqual(readRuntimeEventIds, []);
- const prompt = JSON.stringify(compactPrompt(model));
- assert.match(prompt, /maka_synthesis_cache_block/);
- assert.equal(prompt.includes('RAW_LOADED_SYNTHESIS_ARCHIVE_PAYLOAD'), false);
- const usage = events.find(
- (event): event is Extract<SessionEvent, { type: 'token_usage' }> =>
- event.type === 'token_usage',
- );
- assert.equal(usage?.contextBudget?.synthesisCacheBlocksLoaded, 1);
- assert.equal(usage?.contextBudget?.synthesisCacheBlocksSelected, 1);
- });
-
- test('writes synthesis cache after successful gated archive retrieval without injecting it into the same request', async () => {
- const model = completionModel();
- const events: SessionEvent[] = [];
- const archivedBodies = new Map<string, string>();
- const writeInputs: Array<{ sourceRefCount: number; hydratedHasRaw: boolean }> = [];
- const oldResult = { body: 'RAW_WRITE_SYNTHESIS_ARCHIVE_PAYLOAD'.repeat(20), key: 'key-alpha' };
- const serialized = JSON.stringify(oldResult);
- const writtenBlock = synthesisBlock({
- queryKey: 'key-alpha',
- turnId: 'turn-alpha',
- runtimeEventId: 'rt-result-alpha',
- toolCallId: 'tool-alpha',
- artifactId: 'artifact-rt-result-alpha',
- bodySha256: sha256(serialized),
- originalEstimatedTokens: serialized.length,
- originalBytes: utf8Bytes(serialized),
- });
- const backend = createTestAiSdkBackend({
- sessionId: 'session-1',
- header: header(),
- appendMessage: async () => {},
- connection: connection(),
- apiKey: 'sk-test',
- modelId: 'mock-model-id',
- modelFactory: () => model,
- tools: [],
- newId: idGenerator(),
- now: monotonicClock(),
- contextBudget: {
- name: 'synthesis-cache-write-test',
- maxHistoryTurns: 1,
- minRecentTurns: 0,
- staleToolResultPrune: {
- enabled: true,
- maxResultEstimatedTokens: 1,
- minRecentTurnsFull: 0,
- },
- archiveRetrieval: {
- enabled: true,
- mode: 'history_search_gated',
- maxResults: 1,
- maxEstimatedTokens: 4096,
- maxBytes: 4096,
- },
- historySearch: {
- enabled: true,
- maxResults: 1,
- around: 1,
- maxEstimatedTokens: 4096,
- },
- synthesisCache: {
- enabled: true,
- mode: 'read_write',
- maxBlocks: 1,
- },
- charsPerToken: 1,
- },
- toolResultArchive: testToolResultArchive({
- archiveToolResult: async (event) => {
- archivedBodies.set(event.runtimeEventId, event.serializedResult);
- return { artifactId: `artifact-${event.runtimeEventId}` };
- },
- readToolResultArchive: async (event) => {
- const body = archivedBodies.get(event.runtimeEventId);
- return body ? { ok: true, serializedResult: body } : { ok: false, reason: 'not_found' };
- },
- }),
- writeSynthesisCache: async (input) => {
- writeInputs.push({
- sourceRefCount: input.source.retrievedArchiveRefs.length,
- hydratedHasRaw: JSON.stringify(input.source.hydratedRuntimeEvents).includes(
- 'RAW_WRITE_SYNTHESIS_ARCHIVE_PAYLOAD',
- ),
- });
- return { blocks: [writtenBlock] };
- },
- });
-
- for await (const event of backend.send({
- turnId: 'turn-current',
- text: 'Recover key-alpha',
- context: [],
- runtimeContext: [
- runtimeEvent({
- id: 'rt-call-alpha',
- turnId: 'turn-alpha',
- role: 'model',
- author: 'agent',
- content: {
- kind: 'function_call',
- id: 'tool-alpha',
- name: 'Read',
- args: { path: 'key-alpha.txt' },
- },
- }),
- runtimeEvent({
- id: 'rt-result-alpha',
- turnId: 'turn-alpha',
- role: 'tool',
- author: 'tool',
- content: {
- kind: 'function_response',
- id: 'tool-alpha',
- name: 'Read',
- result: oldResult,
- isError: false,
- },
- }),
- runtimeTextEvent({
- id: 'rt-new',
- turnId: 'turn-new',
- role: 'user',
- author: 'user',
- text: 'newer retained context',
- }),
- ],
- })) {
- events.push(event);
- }
-
- assert.deepEqual(writeInputs, [{ sourceRefCount: 1, hydratedHasRaw: true }]);
- const prompt = JSON.stringify(compactPrompt(model));
- assert.match(prompt, /RAW_WRITE_SYNTHESIS_ARCHIVE_PAYLOAD/);
- assert.equal(prompt.includes('SYNTHESIS_SENTINEL_KEY_ALPHA'), false);
- const usage = events.find(
- (event): event is Extract<SessionEvent, { type: 'token_usage' }> =>
- event.type === 'token_usage',
- );
- assert.equal(usage?.contextBudget?.synthesisCacheWritesAttempted, 1);
- assert.equal(usage?.contextBudget?.synthesisCacheBlocksWritten, 1);
- assert.deepEqual(usage?.contextBudget?.synthesisCacheWrittenBlockIds, ['synth-key-alpha']);
- });
-
- test('falls back to gated archive retrieval when synthesis request asks for evidence', async () => {
- const model = completionModel();
- const events: SessionEvent[] = [];
- const archivedBodies = new Map<string, string>();
- const readRuntimeEventIds: string[] = [];
- const writeInputs: Array<{ turnId: string; query: string }> = [];
- const oldResult = { body: 'RAW_SYNTHESIS_ARCHIVE_PAYLOAD'.repeat(20) };
- const serialized = JSON.stringify(oldResult);
- const block = synthesisBlock({
- queryKey: 'key-alpha',
- turnId: 'turn-alpha',
- runtimeEventId: 'rt-result-alpha',
- toolCallId: 'tool-alpha',
- artifactId: 'artifact-rt-result-alpha',
- bodySha256: sha256(serialized),
- originalEstimatedTokens: serialized.length,
- originalBytes: utf8Bytes(serialized),
- });
- const backend = createTestAiSdkBackend({
- sessionId: 'session-1',
- header: header(),
- appendMessage: async () => {},
- connection: connection(),
- apiKey: 'sk-test',
- modelId: 'mock-model-id',
- modelFactory: () => model,
- tools: [],
- newId: idGenerator(),
- now: monotonicClock(),
- contextBudget: {
- name: 'synthesis-cache-raw-test',
- maxHistoryTurns: 1,
- minRecentTurns: 0,
- staleToolResultPrune: {
- enabled: true,
- maxResultEstimatedTokens: 1,
- minRecentTurnsFull: 0,
- },
- archiveRetrieval: {
- enabled: true,
- mode: 'history_search_gated',
- maxResults: 1,
- maxEstimatedTokens: 4096,
- maxBytes: 4096,
- },
- historySearch: {
- enabled: true,
- maxResults: 1,
- around: 1,
- maxEstimatedTokens: 4096,
- },
- synthesisCache: {
- enabled: true,
- mode: 'read_write',
- blocks: [block],
- },
- charsPerToken: 1,
- },
- toolResultArchive: testToolResultArchive({
- archiveToolResult: async (event) => {
- archivedBodies.set(event.runtimeEventId, event.serializedResult);
- return { artifactId: `artifact-${event.runtimeEventId}` };
- },
- readToolResultArchive: async (event) => {
- readRuntimeEventIds.push(event.runtimeEventId);
- const body = archivedBodies.get(event.runtimeEventId);
- return body ? { ok: true, serializedResult: body } : { ok: false, reason: 'not_found' };
- },
- }),
- writeSynthesisCache: async (event) => {
- writeInputs.push({ turnId: event.turnId, query: event.source.query });
- return { blocks: [] };
- },
- });
-
- for await (const event of backend.send({
- turnId: 'turn-current',
- text: 'Show raw evidence for key-alpha',
- context: [],
- runtimeContext: [
- runtimeEvent({
- id: 'rt-call-alpha',
- turnId: 'turn-alpha',
- role: 'model',
- author: 'agent',
- content: {
- kind: 'function_call',
- id: 'tool-alpha',
- name: 'Read',
- args: { path: 'key-alpha.txt' },
- },
- }),
- runtimeEvent({
- id: 'rt-result-alpha',
- turnId: 'turn-alpha',
- role: 'tool',
- author: 'tool',
- content: {
- kind: 'function_response',
- id: 'tool-alpha',
- name: 'Read',
- result: oldResult,
- isError: false,
- },
- }),
- runtimeTextEvent({
- id: 'rt-new',
- turnId: 'turn-new',
- role: 'user',
- author: 'user',
- text: 'newer retained context',
- }),
- ],
- })) {
- events.push(event);
- }
-
- assert.deepEqual(readRuntimeEventIds, ['rt-result-alpha']);
- assert.deepEqual(writeInputs, []);
- const prompt = JSON.stringify(compactPrompt(model));
- assert.match(prompt, /RAW_SYNTHESIS_ARCHIVE_PAYLOAD/);
- assert.equal(prompt.includes('maka_synthesis_cache_block'), false);
- const usage = events.find(
- (event): event is Extract<SessionEvent, { type: 'token_usage' }> =>
- event.type === 'token_usage',
- );
- assert.equal(usage?.contextBudget?.synthesisCacheBlocksSelected, 0);
- assert.deepEqual(usage?.contextBudget?.synthesisCacheSkippedReasonCounts, {
- raw_evidence_requested: 1,
- });
- assert.equal(usage?.contextBudget?.retrievedArchiveToolResults, 1);
- assert.equal(usage?.contextBudget?.synthesisCacheWriteSkipped, 1);
- assert.deepEqual(usage?.contextBudget?.synthesisCacheWriteSkippedReasonCounts, {
- raw_evidence_requested: 1,
- });
- });
-
- test('manual compactHistory writes shared history compact artifacts and returns diagnostics', async () => {
- const writeInputs: Array<{ turnId: string; foldedIds: string[] }> = [];
- const oldEvents = [
- runtimeTextEvent({
- id: 'manual-compact-old-1',
- turnId: 'turn-old-1',
- role: 'user',
- author: 'user',
- text: 'manual alpha compact source '.repeat(12),
- }),
- runtimeTextEvent({
- id: 'manual-compact-old-2',
- turnId: 'turn-old-2',
- role: 'model',
- author: 'agent',
- text: 'manual beta compact source '.repeat(12),
- }),
- ];
- const backend = createTestAiSdkBackend({
- sessionId: 'session-1',
- header: header(),
- appendMessage: async () => {},
- connection: connection(),
- apiKey: 'sk-test',
- modelId: 'mock-model-id',
- modelFactory: () => completionModel(),
- tools: [],
- newId: idGenerator(),
- now: monotonicClock(),
- contextBudget: {
- name: 'manual-compact-test',
- maxHistoryEstimatedTokens: 10_000,
- minRecentTurns: 1,
- charsPerToken: 1,
- },
- writeHistoryCompact: async (input) => {
- writeInputs.push({
- turnId: input.turnId,
- foldedIds: input.source.foldedRuntimeEvents.map((event) => event.id),
- });
- return {
- blocks: [
- buildHistoryCompactBlockFromSummary({
- sessionId: input.sessionId,
- foldedRuntimeEvents: input.source.foldedRuntimeEvents,
- summary: 'MANUAL_HISTORY_COMPACT_SENTINEL',
- highWaterName: input.source.draftBlock.highWaterName,
- highWaterSeq: input.source.draftBlock.highWaterSeq,
- charsPerToken: input.limits.charsPerToken,
- }),
- ],
- };
- },
- });
-
- const recentEvent = runtimeTextEvent({
- id: 'manual-compact-recent',
- turnId: 'turn-recent',
- role: 'user',
- author: 'user',
- text: 'manual recent retained context',
- });
- const runtimeContext = [...oldEvents, recentEvent];
- const result = await backend.compactHistory({
- turnId: 'turn-compact',
- runId: 'run-1',
- runtimeContext,
- });
-
- assert.deepEqual(writeInputs, [
- {
- turnId: 'turn-compact',
- foldedIds: ['manual-compact-old-1', 'manual-compact-old-2'],
- },
- ]);
- assert.equal(result.contextBudget?.historyCompactWritesAttempted, 1);
- assert.equal(result.contextBudget?.historyCompactBlocksWritten, 1);
- assert.equal(result.contextBudget?.compactionDecisions?.[0]?.decision, 'replaced');
- assert.equal(result.contextBudget?.compactionDecisions?.[0]?.boundaryKind, 'historyCompact');
-
- await backend.compactHistory({
- turnId: 'turn-overflow-recovery',
- runId: 'run-1',
- runtimeContext,
- minRecentTurns: 0,
- });
- assert.deepEqual(writeInputs[1], {
- turnId: 'turn-overflow-recovery',
- foldedIds: ['manual-compact-old-1', 'manual-compact-old-2', 'manual-compact-recent'],
- });
- });
-
- test('manual compactHistory still folds small histories with the default automatic compact policy', async () => {
- const writeInputs: string[][] = [];
- const backend = createTestAiSdkBackend({
- sessionId: 'session-1',
- header: header(),
- appendMessage: async () => {},
- connection: connection(),
- apiKey: 'sk-test',
- modelId: 'claude-sonnet-4-5-20250929',
- modelFactory: () => completionModel(),
- tools: [],
- newId: idGenerator(),
- now: monotonicClock(),
- contextBudget: buildDefaultContextBudgetPolicy(connection(), {
- name: 'cli-default-history-budget',
- modelId: 'claude-sonnet-4-5-20250929',
- }),
- writeHistoryCompact: async (input) => {
- writeInputs.push(input.source.foldedRuntimeEvents.map((event) => event.id));
- return {
- blocks: [
- buildHistoryCompactBlockFromSummary({
- sessionId: input.sessionId,
- foldedRuntimeEvents: input.source.foldedRuntimeEvents,
- summary: 'DEFAULT_POLICY_MANUAL_HISTORY_COMPACT_SENTINEL',
- highWaterName: input.source.draftBlock.highWaterName,
- highWaterSeq: input.source.draftBlock.highWaterSeq,
- charsPerToken: input.limits.charsPerToken,
- }),
- ],
- };
- },
- });
-
- const result = await backend.compactHistory({
- turnId: 'turn-compact',
- runId: 'run-1',
- runtimeContext: [
- runtimeTextEvent({
- id: 'default-policy-manual-old-1',
- turnId: 'turn-old-1',
- role: 'user',
- author: 'user',
- text: 'default policy manual old alpha '.repeat(10),
- }),
- runtimeTextEvent({
- id: 'default-policy-manual-old-2',
- turnId: 'turn-old-2',
- role: 'model',
- author: 'agent',
- text: 'default policy manual old beta '.repeat(10),
- }),
- runtimeTextEvent({
- id: 'default-policy-manual-recent',
- turnId: 'turn-recent',
- role: 'user',
- author: 'user',
- text: 'default policy manual recent retained context',
- }),
- ],
- });
-
- assert.deepEqual(writeInputs, [['default-policy-manual-old-1', 'default-policy-manual-old-2']]);
- assert.equal(result.contextBudget?.historyCompactBlocksWritten, 1);
- assert.equal(result.contextBudget?.compactionDecisions?.[0]?.decision, 'replaced');
- });
-
- test('manual compactHistory writes a V2 checkpoint without the legacy artifact writer', async () => {
- const recorded: HistoryCompactCheckpoint[] = [];
- let memoryDispatches = 0;
- const backend = createTestAiSdkBackend({
- sessionId: 'session-1',
- header: header(),
- appendMessage: async () => {},
- connection: connection(),
- apiKey: 'sk-test',
- modelId: 'mock-model-id',
- modelFactory: () => completionModel(),
- tools: [],
- newId: idGenerator(),
- now: monotonicClock(),
- contextBudget: {
- name: 'manual-v2-compact-test',
- maxHistoryEstimatedTokens: 10_000,
- minRecentTurns: 1,
- charsPerToken: 1,
- },
- summarizeHistoryCompact: async () => structuredSummary('MANUAL_V2_HISTORY_COMPACT_SENTINEL'),
- recordHistoryCompactCheckpoint: (checkpoint) => {
- recorded.push(checkpoint);
- },
- memoryExtraction: {
- gate: async () => ({ allowed: true }),
- remember: async () => ({ status: 'unavailable', requestedItems: [] }),
- extract: () => {
- memoryDispatches += 1;
- },
- },
- });
-
- const result = await backend.compactHistory({
- turnId: 'turn-compact',
- runId: 'run-1',
- runtimeContext: [
- runtimeTextEvent({
- id: 'manual-v2-old-1',
- turnId: 'turn-old-1',
- role: 'user',
- author: 'user',
- text: 'manual v2 old alpha '.repeat(100),
- }),
- runtimeTextEvent({
- id: 'manual-v2-old-2',
- turnId: 'turn-old-2',
- role: 'model',
- author: 'agent',
- text: 'manual v2 old beta '.repeat(100),
- }),
- runtimeTextEvent({
- id: 'manual-v2-recent',
- turnId: 'turn-recent',
- role: 'user',
- author: 'user',
- text: 'manual v2 recent retained context',
- }),
- ],
- });
-
- assert.equal(recorded.length, 1);
- assert.equal(
- recorded[0]?.version === 2 ? recorded[0].summary : undefined,
- structuredSummary('MANUAL_V2_HISTORY_COMPACT_SENTINEL'),
- );
- assert.deepEqual(recorded[0]?.coverage.eventCount, 2);
- assert.equal(recorded[0]?.memoryExtractionBoundary, undefined);
- assert.equal(memoryDispatches, 0);
- assert.equal(result.contextBudget?.historyCompactBlocksWritten, 1);
- assert.equal(result.contextBudget?.compactionDecisions?.[0]?.decision, 'replaced');
- });
-
- test('manual compactHistory rolls forward from the previous V2 checkpoint', async () => {
- const oldEvents = [
- runtimeTextEvent({
- id: 'manual-v2-roll-old-1',
- turnId: 'manual-v2-roll-turn-1',
- role: 'user',
- author: 'user',
- text: 'manual v2 roll old alpha '.repeat(12),
- }),
- runtimeTextEvent({
- id: 'manual-v2-roll-old-2',
- turnId: 'manual-v2-roll-turn-2',
- role: 'model',
- author: 'agent',
- text: 'manual v2 roll old beta '.repeat(12),
- }),
- ];
- const previous = buildHistoryCompactCheckpoint({
- sessionId: 'session-1',
- coveredRuntimeEvents: oldEvents.slice(0, 1),
- summary: 'MANUAL_V2_PREVIOUS_SUMMARY',
- summaryFormat: 'legacy_freeform',
- charsPerToken: 1,
- });
- const summaryInputs: Array<{ previous?: string; newlyFoldedIds: string[] }> = [];
- const recorded: HistoryCompactCheckpoint[] = [];
- const backend = createTestAiSdkBackend({
- sessionId: 'session-1',
- header: header(),
- appendMessage: async () => {},
- connection: connection(),
- apiKey: 'sk-test',
- modelId: 'mock-model-id',
- modelFactory: () => completionModel(),
- tools: [],
- newId: idGenerator(),
- now: monotonicClock(),
- contextBudget: {
- name: 'manual-v2-roll-test',
- maxHistoryEstimatedTokens: 10_000,
- minRecentTurns: 1,
- charsPerToken: 1,
- },
- loadHistoryCompactCheckpoint: () => previous,
- summarizeHistoryCompact: async (input) => {
- summaryInputs.push({
- previous:
- input.previousCheckpoint?.version === 2 ? input.previousCheckpoint.summary : undefined,
- newlyFoldedIds: (input.newlyFoldedRuntimeEvents ?? []).map((event) => event.id),
- });
- return structuredSummary('MANUAL_V2_ROLLED_SUMMARY');
- },
- recordHistoryCompactCheckpoint: (checkpoint) => {
- recorded.push(checkpoint);
- },
- });
-
- await backend.compactHistory({
- turnId: 'turn-compact',
- runId: 'run-1',
- runtimeContext: [
- ...oldEvents,
- runtimeTextEvent({
- id: 'manual-v2-roll-recent',
- turnId: 'manual-v2-roll-recent-turn',
- role: 'user',
- author: 'user',
- text: 'manual v2 roll retained context',
- }),
- ],
- });
+ });
assert.deepEqual(summaryInputs, [
{
previous: 'MANUAL_V2_PREVIOUS_SUMMARY',
- newlyFoldedIds: ['manual-v2-roll-old-2'],
+ newlyFoldedIds: ['manual-v2-roll-old-2', 'manual-v2-roll-recent'],
},
]);
assert.equal(recorded[0]?.previousCheckpointId, previous.checkpointId);
- assert.equal(recorded[0]?.coverage.eventCount, 2);
+ assert.equal(recorded[0]?.coverage.eventCount, 3);
});
test('manual compactHistory reuses a checkpoint that already covers the full fold', async () => {
@@ -5426,9 +4414,16 @@ describe('AiSdkBackend model history', () => {
text: 'manual v2 reuse old beta '.repeat(12),
}),
];
+ const recentEvent = runtimeTextEvent({
+ id: 'manual-v2-reuse-recent',
+ turnId: 'manual-v2-reuse-recent-turn',
+ role: 'user',
+ author: 'user',
+ text: 'manual v2 reuse retained context',
+ });
const previous = buildHistoryCompactCheckpoint({
sessionId: 'session-1',
- coveredRuntimeEvents: oldEvents,
+ coveredRuntimeEvents: [...oldEvents, recentEvent],
summary: 'MANUAL_V2_REUSED_SUMMARY',
summaryFormat: 'legacy_freeform',
charsPerToken: 1,
@@ -5449,7 +4444,6 @@ describe('AiSdkBackend model history', () => {
contextBudget: {
name: 'manual-v2-reuse-test',
maxHistoryEstimatedTokens: 10_000,
- minRecentTurns: 1,
charsPerToken: 1,
},
loadHistoryCompactCheckpoint: () => previous,
@@ -5466,21 +4460,12 @@ describe('AiSdkBackend model history', () => {
const result = await backend.compactHistory({
turnId: 'turn-compact',
runId: 'run-1',
- runtimeContext: [
- ...oldEvents,
- runtimeTextEvent({
- id: 'manual-v2-reuse-recent',
- turnId: 'manual-v2-reuse-recent-turn',
- role: 'user',
- author: 'user',
- text: 'manual v2 reuse retained context',
- }),
- ],
+ runtimeContext: [...oldEvents, recentEvent],
});
assert.equal(summarizeCalls, 0);
assert.equal(recordCalls, 0);
- assert.equal(result.contextBudget?.historyCompactWriteFailures ?? 0, 0);
+ assert.deepEqual(result.outcome, { kind: 'unchanged', reason: 'already_compacted' });
assert.equal(result.contextBudget?.compactionDecisions?.[0]?.decision, 'unchanged');
assert.equal(result.contextBudget?.compactionDecisions?.[0]?.reason, 'already_compacted');
});
@@ -5511,8 +4496,8 @@ describe('AiSdkBackend model history', () => {
});
for (const limits of [
- { maxHistoryEstimatedTokens: 10_000, maxBlockEstimatedTokens: 500 },
- { maxHistoryEstimatedTokens: 1_400, maxBlockEstimatedTokens: 10_000 },
+ { maxHistoryEstimatedTokens: 10_000 },
+ { maxHistoryEstimatedTokens: 1_400 },
]) {
let summarizeCalls = 0;
const recorded: HistoryCompactCheckpoint[] = [];
@@ -5530,12 +4515,8 @@ describe('AiSdkBackend model history', () => {
contextBudget: {
name: 'manual-v2-refit-test',
maxHistoryEstimatedTokens: limits.maxHistoryEstimatedTokens,
- minRecentTurns: 1,
charsPerToken: 1,
- historyCompact: {
- enabled: true,
- maxBlockEstimatedTokens: limits.maxBlockEstimatedTokens,
- },
+ historyCompact: { enabled: true },
},
loadHistoryCompactCheckpoint: () => previous,
summarizeHistoryCompact: async () => {
@@ -5583,10 +4564,9 @@ describe('AiSdkBackend model history', () => {
now: monotonicClock(),
contextBudget: {
name: 'manual-v2-envelope-budget-test',
- maxHistoryEstimatedTokens: 10_000,
- minRecentTurns: 1,
+ maxHistoryEstimatedTokens: 100,
charsPerToken: 1,
- historyCompact: { enabled: true, maxBlockEstimatedTokens: 100, maxEstimatedTokens: 10_000 },
+ historyCompact: { enabled: true },
},
summarizeHistoryCompact: async () => structuredSummary('TINY_SUMMARY'),
recordHistoryCompactCheckpoint: () => {
@@ -5623,7 +4603,7 @@ describe('AiSdkBackend model history', () => {
});
assert.equal(recordCalls, 0);
- assert.equal(result.contextBudget?.historyCompactWriteFailures, 1);
+ assert.deepEqual(result.outcome, { kind: 'failed', reason: 'prefix_over_budget' });
});
test('manual compactHistory rejects a complete summary that makes the full replay larger', async () => {
@@ -5642,7 +4622,6 @@ describe('AiSdkBackend model history', () => {
contextBudget: {
name: 'manual-v2-larger-replacement-test',
maxHistoryEstimatedTokens: 10_000,
- minRecentTurns: 1,
charsPerToken: 1,
historyCompact: { enabled: true },
},
@@ -5681,7 +4660,7 @@ describe('AiSdkBackend model history', () => {
});
assert.equal(recordCalls, 0);
- assert.equal(result.contextBudget?.historyCompactWriteFailures, 1);
+ assert.deepEqual(result.outcome, { kind: 'failed', reason: 'replacement_not_smaller' });
assert.equal(
result.contextBudget?.compactionDecisions?.[0]?.failOpenReason,
'replacement_not_smaller',
@@ -5736,9 +4715,7 @@ describe('AiSdkBackend model history', () => {
});
assert.equal(result.contextBudget?.compactionDecisions?.[0]?.failOpenReason, 'output_length');
- assert.deepEqual(result.contextBudget?.historyCompactWriteSkippedReasonCounts, {
- output_length: 1,
- });
+ assert.deepEqual(result.outcome, { kind: 'failed', reason: 'output_length' });
});
test('the checkpoint write gate rejects a malformed summary from any producer', async () => {
@@ -5757,144 +4734,51 @@ describe('AiSdkBackend model history', () => {
newId: idGenerator(),
now: monotonicClock(),
contextBudget: {
- name: 'manual-v2-write-gate-test',
- maxHistoryEstimatedTokens: 10_000,
- charsPerToken: 1,
- historyCompact: { enabled: true },
- },
- // Returns (not throws) a section-less fragment, bypassing the
- // summarizer's own generate-time validation.
- summarizeHistoryCompact: async () => '这次会话主要讨论了以下内容,然后:',
- recordHistoryCompactCheckpoint: () => {
- throw new Error('must not persist');
- },
- });
-
- const result = await backend.compactHistory({
- turnId: 'turn-compact',
- runId: 'run-1',
- runtimeContext: [
- runtimeTextEvent({
- id: 'write-gate-old',
- turnId: 'old',
- role: 'user',
- author: 'user',
- text: 'old '.repeat(100),
- }),
- runtimeTextEvent({
- id: 'write-gate-recent',
- turnId: 'recent',
- role: 'user',
- author: 'user',
- text: 'recent',
- }),
- ],
- });
-
- assert.equal(
- result.contextBudget?.compactionDecisions?.[0]?.failOpenReason,
- 'malformed_summary_missing_section',
- );
- assert.deepEqual(result.contextBudget?.historyCompactWriteSkippedReasonCounts, {
- malformed_summary_missing_section: 1,
- });
- });
-
- test('manual compactHistory writes the current fold instead of reusing a loaded prefix block', async () => {
- const covered = [
- runtimeTextEvent({
- id: 'manual-prefix-old-1',
- turnId: 'turn-old-1',
- role: 'user',
- author: 'user',
- text: 'manual prefix alpha '.repeat(12),
- }),
- runtimeTextEvent({
- id: 'manual-prefix-old-2',
- turnId: 'turn-old-2',
- role: 'model',
- author: 'agent',
- text: 'manual prefix beta '.repeat(12),
- }),
- ];
- const loadedBlock = buildHistoryCompactBlockFromSummary({
- sessionId: 'session-1',
- foldedRuntimeEvents: covered,
- summary: 'OLD_MANUAL_HISTORY_COMPACT_SENTINEL',
- highWaterName: 'loaded-manual-compact',
- highWaterSeq: 1,
- charsPerToken: 1,
- });
- let loadCalls = 0;
- const writeInputs: string[][] = [];
- const backend = createTestAiSdkBackend({
- sessionId: 'session-1',
- header: header(),
- appendMessage: async () => {},
- connection: connection(),
- apiKey: 'sk-test',
- modelId: 'mock-model-id',
- modelFactory: () => completionModel(),
- tools: [],
- newId: idGenerator(),
- now: monotonicClock(),
- contextBudget: {
- name: 'manual-compact-test',
+ name: 'manual-v2-write-gate-test',
maxHistoryEstimatedTokens: 10_000,
- minRecentTurns: 1,
charsPerToken: 1,
+ historyCompact: { enabled: true },
},
- loadHistoryCompact: async () => {
- loadCalls += 1;
- return { blocks: [loadedBlock] };
- },
- writeHistoryCompact: async (input) => {
- writeInputs.push(input.source.foldedRuntimeEvents.map((event) => event.id));
- return {
- blocks: [
- buildHistoryCompactBlockFromSummary({
- sessionId: input.sessionId,
- foldedRuntimeEvents: input.source.foldedRuntimeEvents,
- summary: 'NEW_MANUAL_HISTORY_COMPACT_SENTINEL',
- highWaterName: input.source.draftBlock.highWaterName,
- highWaterSeq: input.source.draftBlock.highWaterSeq,
- charsPerToken: input.limits.charsPerToken,
- }),
- ],
- };
+ // Returns (not throws) a section-less fragment, bypassing the
+ // summarizer's own generate-time validation.
+ summarizeHistoryCompact: async () => '这次会话主要讨论了以下内容,然后:',
+ recordHistoryCompactCheckpoint: () => {
+ throw new Error('must not persist');
},
});
- await backend.compactHistory({
+ const result = await backend.compactHistory({
turnId: 'turn-compact',
runId: 'run-1',
runtimeContext: [
- ...covered,
runtimeTextEvent({
- id: 'manual-prefix-former-tail',
- turnId: 'turn-former-tail',
+ id: 'write-gate-old',
+ turnId: 'old',
role: 'user',
author: 'user',
- text: 'manual former retained tail now foldable '.repeat(8),
+ text: 'old '.repeat(100),
}),
runtimeTextEvent({
- id: 'manual-prefix-recent',
- turnId: 'turn-recent',
- role: 'model',
- author: 'agent',
- text: 'manual recent retained context',
+ id: 'write-gate-recent',
+ turnId: 'recent',
+ role: 'user',
+ author: 'user',
+ text: 'recent',
}),
],
});
- assert.equal(loadCalls, 0);
- assert.deepEqual(writeInputs, [
- ['manual-prefix-old-1', 'manual-prefix-old-2', 'manual-prefix-former-tail'],
- ]);
+ assert.equal(
+ result.contextBudget?.compactionDecisions?.[0]?.failOpenReason,
+ 'malformed_summary_missing_section',
+ );
+ assert.deepEqual(result.outcome, {
+ kind: 'failed',
+ reason: 'malformed_summary_missing_section',
+ });
});
test('manual compactHistory is a no-op when context budget is disabled', async () => {
- let writes = 0;
const backend = createTestAiSdkBackend({
sessionId: 'session-1',
header: header(),
@@ -5906,10 +4790,6 @@ describe('AiSdkBackend model history', () => {
tools: [],
newId: idGenerator(),
now: monotonicClock(),
- writeHistoryCompact: async () => {
- writes += 1;
- return { blocks: [] };
- },
});
const result = await backend.compactHistory({
@@ -5933,8 +4813,7 @@ describe('AiSdkBackend model history', () => {
],
});
- assert.deepEqual(result, {});
- assert.equal(writes, 0);
+ assert.deepEqual(result.outcome, { kind: 'unchanged', reason: 'operation_unavailable' });
});
test('manual compactHistory is a no-op when no durable writer is configured', async () => {
@@ -5952,7 +4831,6 @@ describe('AiSdkBackend model history', () => {
contextBudget: {
name: 'manual-compact-test',
maxHistoryEstimatedTokens: 10_000,
- minRecentTurns: 1,
charsPerToken: 1,
},
});
@@ -5978,7 +4856,7 @@ describe('AiSdkBackend model history', () => {
],
});
- assert.deepEqual(result, {});
+ assert.deepEqual(result.outcome, { kind: 'unchanged', reason: 'operation_unavailable' });
});
test('manual compactHistory does not report replaced when durable write fails', async () => {
@@ -6019,10 +4897,10 @@ describe('AiSdkBackend model history', () => {
contextBudget: {
name: 'manual-compact-test',
maxHistoryEstimatedTokens: 10_000,
- minRecentTurns: 1,
charsPerToken: 1,
},
- writeHistoryCompact: async () => {
+ summarizeHistoryCompact: async () => structuredSummary('WRITE_FAILURE_SUMMARY'),
+ recordHistoryCompactCheckpoint: async () => {
throw new Error('artifact write failed');
},
});
@@ -6033,11 +4911,7 @@ describe('AiSdkBackend model history', () => {
runtimeContext: oldEvents,
});
- assert.equal(result.contextBudget?.historyCompactWriteFailures, 1);
- assert.equal(result.contextBudget?.historyCompactBlockIds, undefined);
- assert.equal(result.contextBudget?.historyCompactBlocksSelected, undefined);
- assert.equal(result.contextBudget?.historyCompactedEvents, undefined);
- assert.equal(result.contextBudget?.highWaterReason, undefined);
+ assert.deepEqual(result.outcome, { kind: 'failed', reason: 'write_failed' });
assert.deepEqual(
result.contextBudget?.compactionDecisions?.map((decision) => decision.decision),
['failedOpen'],
@@ -6045,13 +4919,12 @@ describe('AiSdkBackend model history', () => {
assert.equal(result.contextBudget?.compactionDecisions?.[0]?.failOpenReason, 'write_failed');
});
- test('stopping manual compactHistory does not poison the next backend turn', async () => {
- const writeGate = makeGate();
- let writeStarted: (() => void) | undefined;
- let writeAbortSignal: AbortSignal | undefined;
- const writeStartedPromise = new Promise<void>((resolve) => {
- writeStarted = resolve;
+ test('stopping manual compactHistory aborts the transaction without poisoning the next turn', async () => {
+ let summarizeStarted!: () => void;
+ const started = new Promise<void>((resolve) => {
+ summarizeStarted = resolve;
});
+ let recordCalls = 0;
const backend = createTestAiSdkBackend({
sessionId: 'session-1',
header: header(),
@@ -6064,229 +4937,47 @@ describe('AiSdkBackend model history', () => {
newId: idGenerator(),
now: monotonicClock(),
contextBudget: {
- name: 'manual-compact-test',
+ name: 'manual-compact-abort-test',
maxHistoryEstimatedTokens: 10_000,
- minRecentTurns: 1,
charsPerToken: 1,
},
- writeHistoryCompact: async (input) => {
- writeAbortSignal = input.abortSignal;
- writeStarted?.();
- await writeGate.promise;
- return {
- blocks: [
- buildHistoryCompactBlockFromSummary({
- sessionId: input.sessionId,
- foldedRuntimeEvents: input.source.foldedRuntimeEvents,
- summary: 'MANUAL_HISTORY_COMPACT_SENTINEL',
- highWaterName: input.source.draftBlock.highWaterName,
- highWaterSeq: input.source.draftBlock.highWaterSeq,
- charsPerToken: input.limits.charsPerToken,
- }),
- ],
- };
- },
- });
-
- const compactPromise = backend.compactHistory({
- turnId: 'turn-compact',
- runId: 'run-1',
- runtimeContext: [
- runtimeTextEvent({
- id: 'old-1',
- turnId: 'turn-old-1',
- role: 'user',
- author: 'user',
- text: 'old alpha '.repeat(20),
- }),
- runtimeTextEvent({
- id: 'old-2',
- turnId: 'turn-old-2',
- role: 'model',
- author: 'agent',
- text: 'old beta '.repeat(20),
- }),
- runtimeTextEvent({
- id: 'recent',
- turnId: 'turn-recent',
- role: 'user',
- author: 'user',
- text: 'recent',
+ summarizeHistoryCompact: ({ abortSignal }) =>
+ new Promise((resolve) => {
+ summarizeStarted();
+ abortSignal?.addEventListener(
+ 'abort',
+ () => resolve(structuredSummary('ABORTED_SUMMARY')),
+ { once: true },
+ );
}),
- ],
- });
- await writeStartedPromise;
- assert.equal(writeAbortSignal?.aborted, false);
- await backend.stop('user_stop');
- assert.equal(writeAbortSignal?.aborted, true);
- writeGate.release();
- await compactPromise;
-
- const events: SessionEvent[] = [];
- for await (const event of backend.send({ turnId: 'turn-next', text: 'next', context: [] })) {
- events.push(event);
- }
-
- assert.equal(
- events.some((event) => event.type === 'text_delta' && event.text === 'NEXT_OK'),
- true,
- );
- });
-
- test('stopped manual compactHistory stays suppressed when the next send starts before the writer returns', async () => {
- const writeGate = makeGate();
- let writeStarted: (() => void) | undefined;
- const writeStartedPromise = new Promise<void>((resolve) => {
- writeStarted = resolve;
- });
- const backend = createTestAiSdkBackend({
- sessionId: 'session-1',
- header: header(),
- appendMessage: async () => {},
- connection: connection(),
- apiKey: 'sk-test',
- modelId: 'mock-model-id',
- modelFactory: () => textCompletionModel('NEXT_OK'),
- tools: [],
- newId: idGenerator(),
- now: monotonicClock(),
- contextBudget: {
- name: 'manual-compact-test',
- maxHistoryEstimatedTokens: 10_000,
- minRecentTurns: 1,
- charsPerToken: 1,
- },
- writeHistoryCompact: async (input) => {
- writeStarted?.();
- await writeGate.promise;
- return {
- blocks: [
- buildHistoryCompactBlockFromSummary({
- sessionId: input.sessionId,
- foldedRuntimeEvents: input.source.foldedRuntimeEvents,
- summary: 'MANUAL_HISTORY_COMPACT_SENTINEL',
- highWaterName: input.source.draftBlock.highWaterName,
- highWaterSeq: input.source.draftBlock.highWaterSeq,
- charsPerToken: input.limits.charsPerToken,
- }),
- ],
- };
+ recordHistoryCompactCheckpoint: () => {
+ recordCalls += 1;
},
});
- const compactPromise = backend.compactHistory({
+ const compact = backend.compactHistory({
turnId: 'turn-compact',
runId: 'run-1',
runtimeContext: [
runtimeTextEvent({
- id: 'old-1',
- turnId: 'turn-old-1',
- role: 'user',
- author: 'user',
- text: 'old alpha '.repeat(20),
- }),
- runtimeTextEvent({
- id: 'old-2',
- turnId: 'turn-old-2',
- role: 'model',
- author: 'agent',
- text: 'old beta '.repeat(20),
- }),
- runtimeTextEvent({
- id: 'recent',
- turnId: 'turn-recent',
+ id: 'abort-old',
+ turnId: 'turn-old',
role: 'user',
author: 'user',
- text: 'recent',
+ text: 'old '.repeat(100),
}),
],
});
- await writeStartedPromise;
+ await started;
await backend.stop('user_stop');
- const sendEventsPromise = (async () => {
- const events: SessionEvent[] = [];
- for await (const event of backend.send({ turnId: 'turn-next', text: 'next', context: [] })) {
- events.push(event);
- }
- return events;
- })();
- writeGate.release();
-
- const [compactResult, events] = await Promise.all([compactPromise, sendEventsPromise]);
- assert.equal(compactResult.contextBudget, undefined);
- assert.equal(
- events.some((event) => event.type === 'text_delta' && event.text === 'NEXT_OK'),
- true,
- );
- });
-
- test('stopping after manual compactHistory returns does not poison the next backend turn', async () => {
- const backend = createTestAiSdkBackend({
- sessionId: 'session-1',
- header: header(),
- appendMessage: async () => {},
- connection: connection(),
- apiKey: 'sk-test',
- modelId: 'mock-model-id',
- modelFactory: () => textCompletionModel('NEXT_OK'),
- tools: [],
- newId: idGenerator(),
- now: monotonicClock(),
- contextBudget: {
- name: 'manual-compact-test',
- maxHistoryEstimatedTokens: 10_000,
- minRecentTurns: 1,
- charsPerToken: 1,
- },
- writeHistoryCompact: async (input) => ({
- blocks: [
- buildHistoryCompactBlockFromSummary({
- sessionId: input.sessionId,
- foldedRuntimeEvents: input.source.foldedRuntimeEvents,
- summary: 'MANUAL_HISTORY_COMPACT_SENTINEL',
- highWaterName: input.source.draftBlock.highWaterName,
- highWaterSeq: input.source.draftBlock.highWaterSeq,
- charsPerToken: input.limits.charsPerToken,
- }),
- ],
- }),
- });
-
- await backend.compactHistory({
- turnId: 'turn-compact',
- runId: 'run-1',
- runtimeContext: [
- runtimeTextEvent({
- id: 'old-1',
- turnId: 'turn-old-1',
- role: 'user',
- author: 'user',
- text: 'old alpha '.repeat(20),
- }),
- runtimeTextEvent({
- id: 'old-2',
- turnId: 'turn-old-2',
- role: 'model',
- author: 'agent',
- text: 'old beta '.repeat(20),
- }),
- runtimeTextEvent({
- id: 'recent',
- turnId: 'turn-recent',
- role: 'user',
- author: 'user',
- text: 'recent',
- }),
- ],
- });
- await backend.stop('user_stop');
+ assert.deepEqual(await compact, { outcome: { kind: 'failed', reason: 'aborted' } });
+ assert.equal(recordCalls, 0);
const events: SessionEvent[] = [];
for await (const event of backend.send({ turnId: 'turn-next', text: 'next', context: [] })) {
events.push(event);
}
-
assert.equal(
events.some((event) => event.type === 'text_delta' && event.text === 'NEXT_OK'),
true,
@@ -6477,161 +5168,46 @@ describe('AiSdkBackend model history', () => {
const assistants: AssistantMessage[] = [];
const backend = createTestAiSdkBackend({
sessionId: 'session-1',
- header: header(),
- appendMessage: async (message) => {
- if (message.type === 'assistant') assistants.push(message);
- },
- connection: connection(),
- apiKey: 'sk-test',
- modelId: 'mock-model-id',
- modelFactory: () => model,
- tools: [],
- newId: idGenerator(),
- now: monotonicClock(),
- });
-
- const events: SessionEvent[] = [];
- for await (const event of backend.send({ turnId: 'turn-1', text: 'hi', context: [] })) {
- events.push(event);
- if (event.type === 'text_delta' && event.text === 'partial answer') gate.release();
- }
-
- // The streamed partial persists as this step's AssistantMessage.
- assert.equal(assistants.length, 1);
- assert.equal(assistants[0]!.text, 'partial answer');
- // And the turn still closes as an error, not a false success.
- const failure = events.find((event) => event.type === 'error');
- assert.equal(
- failure?.message,
- 'provider exploded mid-step (code=provider_error, requestId=req-mid-step)',
- );
- const completes = events.filter((event) => event.type === 'complete');
- assert.equal(completes.length > 0, true);
- assert.equal(
- completes.every((event) => (event as { stopReason?: string }).stopReason === 'error'),
- true,
- );
- });
-
- test('writes host history compact block and replays the host summary in the same request', async () => {
- const model = completionModel();
- const events: SessionEvent[] = [];
- const storedMessages: StoredMessage[] = [];
- const writeInputs: Array<{ draftSummary: string; foldedIds: string[] }> = [];
- const oldEvents = [
- runtimeTextEvent({
- id: 'compact-old-1',
- turnId: 'turn-old-1',
- role: 'user',
- author: 'user',
- text: 'alpha compact source '.repeat(12),
- }),
- runtimeTextEvent({
- id: 'compact-old-2',
- turnId: 'turn-old-2',
- role: 'model',
- author: 'agent',
- text: 'beta compact source '.repeat(12),
- }),
- ];
- const backend = createTestAiSdkBackend({
- sessionId: 'session-1',
- header: header(),
- appendMessage: async (message) => {
- storedMessages.push(message);
- },
- connection: connection(),
- apiKey: 'sk-test',
- modelId: 'mock-model-id',
- modelFactory: () => model,
- tools: [],
- newId: idGenerator(),
- now: monotonicClock(),
- contextBudget: {
- name: 'history-compact-write-test',
- maxHistoryEstimatedTokens: 1500,
- minRecentTurns: 1,
- charsPerToken: 1,
- historyCompact: {
- enabled: true,
- mode: 'read_write',
- highWaterRatio: 0.01,
- tailEstimatedTokens: 44,
- minRecentTurns: 1,
- maxSummaryEstimatedTokens: 120,
- },
- },
- writeHistoryCompact: async (input) => {
- writeInputs.push({
- draftSummary: input.source.draftBlock.summary,
- foldedIds: input.source.foldedRuntimeEvents.map((event) => event.id),
- });
- return {
- blocks: [
- buildHistoryCompactBlockFromSummary({
- sessionId: input.sessionId,
- foldedRuntimeEvents: input.source.foldedRuntimeEvents,
- summary: 'HOST_HISTORY_COMPACT_SENTINEL',
- highWaterName: input.source.draftBlock.highWaterName,
- highWaterSeq: input.source.draftBlock.highWaterSeq,
- charsPerToken: input.limits.charsPerToken,
- }),
- ],
- };
+ header: header(),
+ appendMessage: async (message) => {
+ if (message.type === 'assistant') assistants.push(message);
},
+ connection: connection(),
+ apiKey: 'sk-test',
+ modelId: 'mock-model-id',
+ modelFactory: () => model,
+ tools: [],
+ newId: idGenerator(),
+ now: monotonicClock(),
});
- for await (const event of backend.send({
- turnId: 'turn-current',
- text: 'continue after compact',
- context: [],
- runtimeContext: [
- ...oldEvents,
- runtimeTextEvent({
- id: 'compact-recent',
- turnId: 'turn-recent',
- role: 'user',
- author: 'user',
- text: 'recent retained context',
- }),
- ],
- })) {
+ const events: SessionEvent[] = [];
+ for await (const event of backend.send({ turnId: 'turn-1', text: 'hi', context: [] })) {
events.push(event);
+ if (event.type === 'text_delta' && event.text === 'partial answer') gate.release();
}
- assert.deepEqual(
- writeInputs.map((input) => input.foldedIds),
- [['compact-old-1', 'compact-old-2']],
- );
- assert.match(writeInputs[0]?.draftSummary ?? '', /Compacted 2 older turns/);
- const prompt = JSON.stringify(compactPrompt(model));
- assert.match(prompt, /HOST_HISTORY_COMPACT_SENTINEL/);
- assert.equal(prompt.includes('alpha compact source'), false);
- assert.match(prompt, /recent retained context/);
- const usage = events.find(
- (event): event is Extract<SessionEvent, { type: 'token_usage' }> =>
- event.type === 'token_usage',
- );
- assert.equal(usage?.contextBudget?.historyCompactWritesAttempted, 1);
- assert.equal(usage?.contextBudget?.historyCompactBlocksWritten, 1);
- assert.equal(usage?.contextBudget?.highWaterReason, 'history_compact');
- assert.deepEqual(
- usage?.contextBudget?.compactionDecisions?.[0]?.boundaryIds,
- usage?.contextBudget?.historyCompactWrittenBlockIds,
+ // The streamed partial persists as this step's AssistantMessage.
+ assert.equal(assistants.length, 1);
+ assert.equal(assistants[0]!.text, 'partial answer');
+ // And the turn still closes as an error, not a false success.
+ const failure = events.find((event) => event.type === 'error');
+ assert.equal(
+ failure?.message,
+ 'provider exploded mid-step (code=provider_error, requestId=req-mid-step)',
);
- assert.equal(usage?.contextBudget?.compactionDecisions?.[0]?.decision, 'replaced');
- assert.equal(usage?.contextBudget?.compactionDecisions?.[0]?.boundaryKind, 'historyCompact');
+ const completes = events.filter((event) => event.type === 'complete');
+ assert.equal(completes.length > 0, true);
assert.equal(
- storedMessages.some(
- (message) => message.type === 'system_note' && message.kind === 'context_compacted',
- ),
+ completes.every((event) => (event as { stopReason?: string }).stopReason === 'error'),
true,
);
});
- test('V2 blank summary fails open to the retained tail and emits one visible notice', async () => {
+ test('blank summary preserves history and stops before an oversized request', async () => {
const model = completionModel();
const storedMessages: StoredMessage[] = [];
+ const events: SessionEvent[] = [];
let recordCalls = 0;
const backend = createTestAiSdkBackend({
sessionId: 'session-1',
@@ -6651,10 +5227,6 @@ describe('AiSdkBackend model history', () => {
charsPerToken: 1,
historyCompact: {
enabled: true,
- mode: 'read_write',
- highWaterRatio: 0.01,
- tailEstimatedTokens: 20,
- maxSummaryEstimatedTokens: 500,
},
},
summarizeHistoryCompact: async () => ' ',
@@ -6662,49 +5234,43 @@ describe('AiSdkBackend model history', () => {
recordCalls += 1;
},
});
- await drain(
- backend.send({
- turnId: 'turn-current',
- text: 'continue',
- context: [],
- runtimeContext: [
- runtimeTextEvent({
- id: 'blank-old-1',
- turnId: 'blank-turn-1',
- role: 'user',
- author: 'user',
- text: 'blank old source one '.repeat(30),
- }),
- runtimeTextEvent({
- id: 'blank-old-2',
- turnId: 'blank-turn-2',
- role: 'model',
- author: 'agent',
- text: 'blank old source two '.repeat(50),
- }),
- runtimeTextEvent({
- id: 'blank-recent',
- turnId: 'blank-recent-turn',
- role: 'user',
- author: 'user',
- text: 'BLANK_RETAINED_TAIL',
- }),
- ],
- }),
- );
+ for await (const event of backend.send({
+ turnId: 'turn-current',
+ text: 'continue',
+ context: [],
+ runtimeContext: [
+ runtimeTextEvent({
+ id: 'blank-old-1',
+ turnId: 'blank-turn-1',
+ role: 'user',
+ author: 'user',
+ text: 'blank old source one '.repeat(30),
+ }),
+ runtimeTextEvent({
+ id: 'blank-old-2',
+ turnId: 'blank-turn-2',
+ role: 'model',
+ author: 'agent',
+ text: 'blank old source two '.repeat(50),
+ }),
+ runtimeTextEvent({
+ id: 'blank-recent',
+ turnId: 'blank-recent-turn',
+ role: 'user',
+ author: 'user',
+ text: 'BLANK_RETAINED_TAIL',
+ }),
+ ],
+ })) {
+ events.push(event);
+ }
- const prompt = JSON.stringify(compactPrompt(model));
- assert.equal(prompt.includes('blank old source one'), false);
- assert.equal(prompt.includes('blank old source two'), false);
- assert.match(prompt, /BLANK_RETAINED_TAIL/);
+ assert.equal(model.doStreamCalls.length, 0);
assert.equal(recordCalls, 0);
- assert.equal(
- storedMessages.filter(
- (message) =>
- message.type === 'system_note' && message.kind === 'context_compaction_failed_open',
- ).length,
- 1,
+ const terminal = events.find(
+ (event): event is Extract<SessionEvent, { type: 'complete' }> => event.type === 'complete',
);
+ assert.equal(terminal?.stopReason, 'context_budget_exhausted');
});
test('replays a matching Codex V3 checkpoint as native provider state', async () => {
@@ -6752,7 +5318,7 @@ describe('AiSdkBackend model history', () => {
tools: [],
contextBudget: {
maxHistoryEstimatedTokens: 100_000,
- historyCompact: { enabled: true, mode: 'lookup' },
+ historyCompact: { enabled: true },
},
loadHistoryCompactCheckpoint: () => checkpoint,
});
@@ -6821,8 +5387,7 @@ describe('AiSdkBackend model history', () => {
tools: [],
contextBudget: {
maxHistoryEstimatedTokens: 100_000,
- minRecentTurns: 0,
- historyCompact: { enabled: true, mode: 'lookup', minRecentTurns: 0 },
+ historyCompact: { enabled: true },
},
loadHistoryCompactCheckpoint: () => checkpoint,
});
@@ -6915,7 +5480,7 @@ describe('AiSdkBackend model history', () => {
tools: [],
contextBudget: {
maxHistoryEstimatedTokens: 100_000,
- historyCompact: { enabled: true, mode: 'lookup' },
+ historyCompact: { enabled: true },
},
loadHistoryCompactCheckpoint: () => checkpoint,
});
@@ -7009,7 +5574,7 @@ describe('AiSdkBackend model history', () => {
tools: [],
contextBudget: {
maxHistoryEstimatedTokens: 100_000,
- historyCompact: { enabled: true, mode: 'lookup' },
+ historyCompact: { enabled: true },
},
loadHistoryCompactCheckpoint: () => checkpoint,
});
@@ -9090,161 +7655,6 @@ describe('AiSdkBackend usage telemetry', () => {
assert.equal(usageMessage?.contextBudget?.activeDuplicateToolResults, undefined);
});
- test('semantic compact projects a recorded block through the real backend request pipeline', async () => {
- const durable = durableTurnHarness('turn-1', 'inspect the repository');
- const messages: unknown[] = [];
- const events: SessionEvent[] = [];
- const recordedBlocks: SemanticCompactBlock[] = [];
- const largeBody = 'SEMANTIC_RAW_TOOL_OUTPUT'.repeat(200);
- let streamCalls = 0;
- const model = new MockLanguageModelV4({
- doGenerate: {
- content: [
- {
- type: 'text',
- text: JSON.stringify({
- established_findings: ['The repository inspection completed.'],
- decisions: [],
- failed_paths: [],
- partial_work_product: ['The earlier Read result was inspected.'],
- action_in_progress: 'Continue from the preserved latest tool episode.',
- }),
- },
- ],
- finishReason: { unified: 'stop', raw: 'stop' },
- usage: {
- inputTokens: { total: 10, noCache: 10, cacheRead: 0, cacheWrite: 0 },
- outputTokens: { total: 12, text: 12, reasoning: 0 },
- },
- warnings: [],
- },
- doStream: async () => {
- streamCalls += 1;
- const toolCall = (toolCallId: string, toolName: string, input: unknown) =>
- [
- { type: 'stream-start', warnings: [] },
- { type: 'tool-call', toolCallId, toolName, input: JSON.stringify(input) },
- {
- type: 'finish',
- finishReason: { unified: 'tool-calls', raw: 'tool_calls' },
- usage: {
- inputTokens: { total: 1, noCache: 1, cacheRead: 0, cacheWrite: 0 },
- outputTokens: { total: 1, text: 1, reasoning: 0 },
- },
- },
- ] as LanguageModelV4StreamPart[];
- const chunks: LanguageModelV4StreamPart[] =
- streamCalls === 1
- ? toolCall('tool-read', 'Read', { path: 'notes.md' })
- : streamCalls === 2
- ? toolCall('tool-bash', 'Bash', { cmd: 'continue' })
- : [
- { type: 'stream-start', warnings: [] },
- {
- type: 'finish',
- finishReason: { unified: 'stop', raw: 'stop' },
- usage: {
- inputTokens: { total: 1, noCache: 1, cacheRead: 0, cacheWrite: 0 },
- outputTokens: { total: 1, text: 1, reasoning: 0 },
- },
- },
- ];
- return {
- stream: simulateReadableStream({ chunks, initialDelayInMs: null, chunkDelayInMs: null }),
- };
- },
- });
- const backend = createTestAiSdkBackend({
- sessionId: 'session-1',
- header: header(),
- appendMessage: async (message) => {
- messages.push(message);
- },
- connection: connection(),
- apiKey: 'sk-test',
- modelId: 'mock-model-id',
- modelFactory: () => model,
- tools: [
- {
- name: 'Read',
- description: 'Read description',
- parameters: z.object({ path: z.string() }),
- impl: async () => ({ body: largeBody }),
- },
- {
- name: 'Bash',
- description: 'Bash description',
- parameters: z.object({ cmd: z.string() }),
- impl: async () => ({ body: 'latest tool episode' }),
- },
- ],
- contextBudget: {
- charsPerToken: 1,
- activeToolResultPrune: { enabled: true, maxCurrentResultEstimatedTokens: 1 },
- semanticCompact: {
- enabled: true,
- mode: 'replace',
- minStepNumber: 1,
- highWaterRatio: 0.1,
- maxActiveEstimatedTokens: 1,
- minSafePrefixEstimatedTokens: 1,
- maxAcceptedProjectionEstimatedTokens: 2048,
- minSavingsTokens: 1,
- minSavingsRatio: 0,
- },
- },
- toolResultArchive: testToolResultArchive({
- archiveToolResult: async () => ({ artifactId: 'artifact-tool-read' }),
- }),
- loadTurnRuntimeEvents: durable.loadTurnRuntimeEvents,
- recordSemanticCompactBlock: (block) => {
- recordedBlocks.push(block);
- },
- newId: idGenerator(),
- now: monotonicClock(),
- });
-
- for await (const event of backend.send(durable.input())) {
- durable.record(event);
- events.push(event);
- }
- await Promise.resolve();
-
- assert.equal(streamCalls, 3);
- assert.equal(model.doGenerateCalls.length, 1);
- assert.equal(recordedBlocks.length, 1);
- assert.equal(recordedBlocks[0]?.kind, 'maka.semantic_compact_block');
- assert.equal(recordedBlocks[0]?.projection?.format, 'structured');
- const projectedPrompt = JSON.stringify(model.doStreamCalls[2]?.prompt ?? []);
- assert.match(projectedPrompt, /maka_semantic_compact_block/);
- assert.match(projectedPrompt, /Continue from the preserved latest tool episode/);
- assert.doesNotMatch(projectedPrompt, /SEMANTIC_RAW_TOOL_OUTPUT/);
- assert.match(projectedPrompt, /latest tool episode/);
-
- const usageMessage = messages.find(
- (message) => (message as { type?: string }).type === 'token_usage',
- ) as { contextBudget?: Record<string, unknown> } | undefined;
- const usageEvent = events.find((event) => event.type === 'token_usage') as
- | (Extract<SessionEvent, { type: 'token_usage' }> & {
- contextBudget?: Record<string, unknown>;
- })
- | undefined;
- for (const contextBudget of [usageMessage?.contextBudget, usageEvent?.contextBudget]) {
- const decisions = contextBudget?.compactionDecisions as
- | Array<Record<string, unknown>>
- | undefined;
- assert.equal(
- decisions?.some(
- (decision) =>
- decision.boundaryKind === 'semanticCompact' && decision.decision === 'replaced',
- ),
- true,
- );
- assert.equal(typeof contextBudget?.highWaterRequestShapeHashBefore, 'string');
- assert.equal(typeof contextBudget?.highWaterRequestShapeHashAfter, 'string');
- }
- });
-
test('normalizes cache and reasoning tokens to messages, events, and telemetry', async () => {
const messages: unknown[] = [];
const events: SessionEvent[] = [];
@@ -9784,8 +8194,7 @@ describe('AiSdkBackend context budget and prompt attribution', () => {
turnTailPrompt: 'volatile tail',
contextBudget: {
name: 'test-budget',
- maxHistoryEstimatedTokens: 40,
- minRecentTurns: 1,
+ maxHistoryEstimatedTokens: 1_000,
charsPerToken: 1,
},
});
@@ -9830,6 +8239,8 @@ describe('AiSdkBackend context budget and prompt attribution', () => {
assert.deepEqual(compactPrompt(model), [
{ role: 'system', content: 'durable system' },
+ { role: 'user', content: [{ type: 'text', text: 'old user text' }] },
+ { role: 'assistant', content: [{ type: 'text', text: 'old assistant text' }] },
{ role: 'user', content: [{ type: 'text', text: 'new user text' }] },
{ role: 'assistant', content: [{ type: 'text', text: 'new assistant text' }] },
{ role: 'user', content: [{ type: 'text', text: 'current user\n\nvolatile tail' }] },
@@ -9840,7 +8251,7 @@ describe('AiSdkBackend context budget and prompt attribution', () => {
);
assert.ok(usage);
assert.equal(usage.contextBudget?.policyName, 'test-budget');
- assert.equal(usage.contextBudget?.droppedTurns, 1);
+ assert.equal(usage.contextBudget?.droppedTurns, 0);
assert.equal(
usage.promptSegments?.some((segment) => segment.kind === 'prior_history'),
true,
@@ -13809,140 +12220,12 @@ describe('AiSdkBackend thinking persistence', () => {
});
});
-async function runArchiveGatedReplay(input: {
- query: string;
- selectedResult: unknown;
- unselectedResult: unknown;
- selectedPath: string;
- unselectedPath: string;
- selectedAfterUnselected?: boolean;
-}): Promise<{
- prompt: string;
- readRuntimeEventIds: string[];
- usage: Extract<SessionEvent, { type: 'token_usage' }> | undefined;
-}> {
- const model = completionModel();
- const events: SessionEvent[] = [];
- const archivedBodies = new Map<string, string>();
- const readRuntimeEventIds: string[] = [];
- const backend = createTestAiSdkBackend({
- sessionId: 'session-1',
- header: header(),
- appendMessage: async () => {},
- connection: connection(),
- apiKey: 'sk-test',
- modelId: 'mock-model-id',
- modelFactory: () => model,
- tools: [],
- newId: idGenerator(),
- now: monotonicClock(),
- contextBudget: {
- name: 'archive-retrieval-gated-test',
- maxHistoryTurns: 1,
- minRecentTurns: 0,
- staleToolResultPrune: {
- enabled: true,
- maxResultEstimatedTokens: 1,
- minRecentTurnsFull: 0,
- },
- archiveRetrieval: {
- enabled: true,
- mode: 'history_search_gated',
- maxResults: 2,
- maxEstimatedTokens: 4096,
- maxBytes: 4096,
- },
- historySearch: {
- enabled: true,
- maxResults: 1,
- around: 1,
- maxEstimatedTokens: 4096,
- },
- charsPerToken: 1,
- },
- toolResultArchive: testToolResultArchive({
- archiveToolResult: async (event) => {
- archivedBodies.set(event.runtimeEventId, event.serializedResult);
- return { artifactId: `artifact-${event.runtimeEventId}` };
- },
- readToolResultArchive: async (event) => {
- readRuntimeEventIds.push(event.runtimeEventId);
- const body = archivedBodies.get(event.runtimeEventId);
- assert.ok(body);
- return event.bodySha256 === sha256(body)
- ? { ok: true, serializedResult: body }
- : { ok: false, reason: 'corrupt' };
- },
- }),
- });
- const selectedEvents = archiveGatedTurnEvents('a', input.selectedPath, input.selectedResult);
- const unselectedEvents = archiveGatedTurnEvents(
- 'b',
- input.unselectedPath,
- input.unselectedResult,
- );
- const runtimeContext = [
- ...(input.selectedAfterUnselected ? unselectedEvents : selectedEvents),
- ...(input.selectedAfterUnselected ? selectedEvents : unselectedEvents),
- runtimeTextEvent({
- id: 'rt-new',
- turnId: 'turn-new',
- role: 'user',
- author: 'user',
- text: 'newer retained context',
- }),
- ];
-
- for await (const event of backend.send({
- turnId: 'turn-current',
- text: input.query,
- context: [],
- runtimeContext,
- })) {
- events.push(event);
- }
-
- const prompt = JSON.stringify(compactPrompt(model));
- if (typeof prompt !== 'string') assert.fail('model prompt was not captured');
- const usage = events.find(
- (event): event is Extract<SessionEvent, { type: 'token_usage' }> =>
- event.type === 'token_usage',
- );
- return { prompt, readRuntimeEventIds, usage };
-}
-
-// The checkpoint write gate validates summary structure (#3029), so stub
// summaries must be shaped like real checkpoints while keeping their
// sentinel text greppable.
function structuredSummary(body: string): string {
return `## Goal\n${body}\n\n## Progress\n- done\n\n## Next Steps\n1. continue\n\n## Critical Context\n- (none)`;
}
-function archiveGatedTurnEvents(suffix: 'a' | 'b', path: string, result: unknown): RuntimeEvent[] {
- return [
- runtimeEvent({
- id: `rt-call-${suffix}`,
- turnId: `turn-${suffix}`,
- role: 'model',
- author: 'agent',
- content: { kind: 'function_call', id: `tool-${suffix}`, name: 'Read', args: { path } },
- }),
- runtimeEvent({
- id: `rt-result-${suffix}`,
- turnId: `turn-${suffix}`,
- role: 'tool',
- author: 'tool',
- content: {
- kind: 'function_response',
- id: `tool-${suffix}`,
- name: 'Read',
- result,
- isError: false,
- },
- }),
- ];
-}
-
describe('AiSdkBackend steering durability and identity', () => {
const steeringBackend = (
model: MockLanguageModelV4,
@@ -15210,54 +13493,6 @@ function runtimeEvent(input: {
};
}
-function synthesisBlock(input: {
- queryKey: string;
- turnId: string;
- runtimeEventId: string;
- toolCallId: string;
- artifactId: string;
- bodySha256: string;
- originalEstimatedTokens: number;
- originalBytes: number;
-}): SynthesisCacheBlock {
- return {
- kind: 'maka.synthesis_cache_block',
- version: 1,
- blockId: `synth-${input.queryKey}`,
- sessionId: 'session-1',
- createdAt: 2,
- highWaterName: `after-gated-${input.queryKey}`,
- highWaterSeq: 1,
- coverage: {
- queryKeys: [input.queryKey],
- turnIds: [input.turnId],
- runtimeEventIds: [input.runtimeEventId],
- toolNames: ['Read'],
- toolCallIds: [input.toolCallId],
- artifactIds: [input.artifactId],
- bodySha256: [input.bodySha256],
- },
- summary: 'SYNTHESIS_SENTINEL_KEY_ALPHA',
- limitations: ['Does not include raw tool output.'],
- sourceRefs: [
- {
- kind: 'archived_tool_result',
- sessionId: 'session-1',
- turnId: input.turnId,
- runtimeEventId: input.runtimeEventId,
- toolCallId: input.toolCallId,
- toolName: 'Read',
- artifactId: input.artifactId,
- bodySha256: input.bodySha256,
- originalEstimatedTokens: input.originalEstimatedTokens,
- originalBytes: input.originalBytes,
- placeholderReason: 'stale_tool_result_pruned_before_compact',
- },
- ],
- createdFrom: 'gated_archive_retrieval',
- };
-}
-
function compactPrompt(model: MockLanguageModelV4): unknown {
return model.doStreamCalls[0]?.prompt.map((message) => ({
role: message.role,
diff --git a/packages/runtime/src/__tests__/context-budget-mid-turn-policy.test.ts b/packages/runtime/src/__tests__/context-budget-mid-turn-policy.test.ts
index c84bc4153..afa0099b7 100644
--- a/packages/runtime/src/__tests__/context-budget-mid-turn-policy.test.ts
+++ b/packages/runtime/src/__tests__/context-budget-mid-turn-policy.test.ts
@@ -136,14 +136,13 @@ describe('tool-result prune policy env plumbing', () => {
});
});
- test('applies explicit bounds and stale recent-turn precedence', () => {
+ test('applies explicit active and stale tool-result bounds', () => {
const policy = buildDefaultContextBudgetPolicy(connection(), {
env: {
MAKA_CONTEXT_ACTIVE_TOOL_RESULT_MAX_ESTIMATED_TOKENS: '4096',
MAKA_CONTEXT_ACTIVE_TOOL_RESULT_MIN_SUPERSEDED_TOKENS: '384',
MAKA_CONTEXT_ACTIVE_TOOL_RESULT_MIN_STEP_NUMBER: '3',
MAKA_CONTEXT_STALE_TOOL_RESULT_MAX_TOKENS: '8192',
- MAKA_CONTEXT_MIN_RECENT_TURNS: '4',
MAKA_CONTEXT_STALE_TOOL_RESULT_MIN_RECENT_TURNS: '5',
},
});
diff --git a/packages/runtime/src/__tests__/context-budget-retired-settings.test.ts b/packages/runtime/src/__tests__/context-budget-retired-settings.test.ts
new file mode 100644
index 000000000..d45245c49
--- /dev/null
+++ b/packages/runtime/src/__tests__/context-budget-retired-settings.test.ts
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import assert from 'node:assert/strict';
+import { describe, test } from 'node:test';
+import type { LlmConnection } from '@maka/core/llm-connections';
+import { buildDefaultContextBudgetPolicy } from '../context-budget-policy.js';
+
+describe('retired context policy settings', () => {
+ test('retired semantic compact environment settings do not change the runtime policy', () => {
+ const baseline = buildDefaultContextBudgetPolicy(connection(), { env: {} });
+ const configured = buildDefaultContextBudgetPolicy(connection(), {
+ env: {
+ MAKA_CONTEXT_SEMANTIC_COMPACT: 'on',
+ MAKA_CONTEXT_SEMANTIC_COMPACT_MODE: 'replace',
+ MAKA_CONTEXT_SEMANTIC_COMPACT_MAX_ACTIVE_ESTIMATED_TOKENS: '4096',
+ },
+ });
+ assert.deepEqual(configured, baseline);
+ });
+});
+
+function connection(): LlmConnection {
+ return {
+ slug: 'anthropic-main',
+ name: 'Anthropic',
+ providerType: 'anthropic',
+ defaultModel: 'claude-sonnet-4-5-20250929',
+ enabled: true,
+ createdAt: 1,
+ updatedAt: 1,
+ };
+}
diff --git a/packages/runtime/src/__tests__/context-budget-semantic-compact-policy.test.ts b/packages/runtime/src/__tests__/context-budget-semantic-compact-policy.test.ts
deleted file mode 100644
index 468ba4275..000000000
--- a/packages/runtime/src/__tests__/context-budget-semantic-compact-policy.test.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import assert from 'node:assert/strict';
-import { describe, test } from 'node:test';
-import type { LlmConnection } from '@maka/core/llm-connections';
-import { buildDefaultContextBudgetPolicy } from '../context-budget-policy.js';
-
-describe('semantic compaction policy env plumbing (issue #882 PR 3)', () => {
- test('defaults off: the #986 experiment is opt-in, not part of the runtime default', () => {
- const policy = buildDefaultContextBudgetPolicy(connection(), { env: {} });
- // The rest of the default budget still exists (history compaction stays on),
- // proving the policy is built but simply omits the experiment.
- assert.equal(policy?.historyCompact?.enabled, true);
- assert.equal(policy?.semanticCompact, undefined);
- });
-
- test('honors an explicit MAKA_CONTEXT_SEMANTIC_COMPACT=on opt-in', () => {
- const policy = buildDefaultContextBudgetPolicy(connection(), {
- env: { MAKA_CONTEXT_SEMANTIC_COMPACT: 'on' },
- });
- assert.equal(policy?.semanticCompact?.enabled, true);
- assert.equal(policy?.semanticCompact?.mode, 'replace');
- assert.equal(policy?.semanticCompact?.maxActiveEstimatedTokens, 131_072);
- assert.equal(policy?.semanticCompact?.highWaterRatio, 1);
- assert.equal(policy?.semanticCompact?.minSafePrefixEstimatedTokens, 4_096);
- });
-
- test('honors an explicit mode as an opt-in even without the boolean flag', () => {
- const policy = buildDefaultContextBudgetPolicy(connection(), {
- env: { MAKA_CONTEXT_SEMANTIC_COMPACT_MODE: 'validate_only' },
- });
- assert.equal(policy?.semanticCompact?.enabled, true);
- assert.equal(policy?.semanticCompact?.mode, 'validate_only');
- });
-
- test('an explicit mode of off keeps it disabled', () => {
- const policy = buildDefaultContextBudgetPolicy(connection(), {
- env: { MAKA_CONTEXT_SEMANTIC_COMPACT_MODE: 'off' },
- });
- assert.equal(policy?.semanticCompact, undefined);
- });
-});
-
-function connection(): LlmConnection {
- return {
- slug: 'anthropic-main',
- name: 'Anthropic',
- providerType: 'anthropic',
- defaultModel: 'claude-sonnet-4-5-20250929',
- enabled: true,
- createdAt: 1,
- updatedAt: 1,
- };
-}
diff --git a/packages/runtime/src/__tests__/context-budget.test.ts b/packages/runtime/src/__tests__/context-budget.test.ts
index 0448dc98b..887c2cf15 100644
--- a/packages/runtime/src/__tests__/context-budget.test.ts
+++ b/packages/runtime/src/__tests__/context-budget.test.ts
@@ -18,2012 +18,42 @@
*/
import assert from 'node:assert/strict';
-import { Buffer } from 'node:buffer';
-import { createHash } from 'node:crypto';
-import { describe, test } from 'node:test';
+import { test } from 'node:test';
import type { RuntimeEvent } from '@maka/core/runtime-event';
-import {
- ARCHIVED_TOOL_RESULT_PLACEHOLDER_KIND,
- ARCHIVED_TOOL_RESULT_REWRITE_VERSION,
- applyRuntimeEventContextBudget,
- buildHistoryCompactBlockFromSummary,
- collectStaleToolResultArchiveCandidates,
- buildSynthesisCacheBlocksFromHydratedArchives,
- deserializeToolResultArchive,
- renderHistoryCompactBlock,
- retrieveArchivedToolResultsForReplay,
- retrieveRuntimeEventHistoryAround,
- serializeToolResultForArchive,
- validateHistoryCompactBlockShape,
- validateSynthesisCacheBlockShape,
- type SynthesisCacheBlock,
-} from '../context-budget.js';
-import { applyRuntimeEventHistoryCompact } from '../history-compact.js';
-import { renderSynthesisCacheBlock, selectSynthesisCacheForReplay } from '../synthesis-cache.js';
-import { historyCompactBlockToCompactionBoundary } from '../compaction-boundary.js';
-import {
- estimateRuntimeEventChars,
- estimateRuntimeEventsTokens,
-} from '../context-budget-helpers.js';
+import { applyRuntimeEventContextBudget } from '../context-budget.js';
+import { estimateRuntimeEventsTokens } from '../context-budget-helpers.js';
test('estimates only model-visible provider context', () => {
- const visible = textEvent('visible-user', 'turn-1', 'visible context');
- const hiddenCall = {
- ...toolCall('hidden-call', 'turn-1', 'nested-call'),
- origin: 'code_mode' as const,
- modelVisibility: 'hidden' as const,
- };
- const hiddenResult = {
- ...toolResult('hidden-result', 'turn-1', 'nested-call', {
- text: 'nested result must not consume provider context',
- }),
- origin: 'code_mode' as const,
- modelVisibility: 'hidden' as const,
- };
-
+ const visible = textEvent('visible', 'visible context');
+ const hidden = { ...textEvent('hidden', 'hidden context'), modelVisibility: 'hidden' as const };
assert.equal(
- estimateRuntimeEventsTokens([visible, hiddenCall, hiddenResult], 1),
+ estimateRuntimeEventsTokens([visible, hidden], 1),
estimateRuntimeEventsTokens([visible], 1),
);
});
-describe('context-budget archive retrieval', () => {
- test('does not archive hidden nested tool results', () => {
- const hiddenResult = toolResult('hidden-result', 'turn-old', 'hidden-tool', {
- text: 'DO_NOT_ARCHIVE_NESTED_RESULT'.repeat(20),
- });
- hiddenResult.modelVisibility = 'hidden';
- const visibleResult = toolResult('visible-result', 'turn-old', 'visible-tool', {
- text: 'archive this result'.repeat(20),
- });
-
- const candidates = collectStaleToolResultArchiveCandidates([hiddenResult, visibleResult], {
- staleToolResultPrune: {
- enabled: true,
- maxResultEstimatedTokens: 1,
- minRecentTurnsFull: 0,
- },
- charsPerToken: 1,
- minRecentTurns: 0,
- });
-
- assert.deepEqual(
- candidates.map((candidate) => candidate.runtimeEventId),
- ['visible-result'],
- );
- });
-
- test('counts provider-native replay payloads and never archives only their display projection', () => {
- const rawProviderOutput = [{ encryptedContent: 'x'.repeat(10_000) }];
- const event = {
- ...toolResult('result-native', 'turn-native', 'search-native', {
- kind: 'web_search',
- provider: 'model',
- query: 'latest',
- rows: [],
- }),
- content: {
- kind: 'function_response' as const,
- id: 'search-native',
- name: 'WebSearch',
- result: { kind: 'web_search', provider: 'model', query: 'latest', rows: [] },
- providerExecuted: true,
- providerOutput: rawProviderOutput,
- },
- };
-
- assert.ok(estimateRuntimeEventChars(event) >= 10_000);
- const budgeted = applyRuntimeEventContextBudget([event], {
- staleToolResultPrune: {
- enabled: true,
- maxResultEstimatedTokens: 1,
- minRecentTurnsFull: 0,
- archiveRefs: [],
- },
- charsPerToken: 1,
- });
- assert.ok(budgeted);
- assert.equal(budgeted.diagnostic.prunedToolResults ?? 0, 0);
- assert.deepEqual(budgeted.events[0], event);
- });
-
- test('prunes the newest turn when the full-result protection window is zero', () => {
- const sentinel = 'newest-turn-full-result-must-not-be-reinjected';
- const originalResult = { kind: 'text', text: sentinel.repeat(4) };
- const serialized = serializeToolResultForArchive(originalResult);
- const events = [
- toolCall('call-newest', 'turn-newest', 'tool-newest'),
- toolResult('result-newest', 'turn-newest', 'tool-newest', originalResult),
- ];
-
- const budgeted = applyRuntimeEventContextBudget(events, {
- staleToolResultPrune: {
- enabled: true,
- maxResultEstimatedTokens: 1,
- minRecentTurnsFull: 0,
- archiveRefs: [
- {
- runtimeEventId: 'result-newest',
- toolCallId: 'tool-newest',
- toolName: 'Read',
- artifactId: 'artifact-newest',
- bodySha256: sha256(serialized),
- originalEstimatedTokens: serialized.length,
- originalBytes: utf8Bytes(serialized),
- rewriteVersion: ARCHIVED_TOOL_RESULT_REWRITE_VERSION,
- reason: 'stale_tool_result_pruned_before_compact',
- },
- ],
- },
- charsPerToken: 1,
- });
-
- assert.ok(budgeted);
- assert.equal(budgeted.diagnostic.prunedToolResults, 1);
- const result = budgeted.events.find((event) => event.id === 'result-newest');
- assert.equal(
- result?.content?.kind === 'function_response'
- ? (result.content.result as { kind?: string }).kind
- : undefined,
- ARCHIVED_TOOL_RESULT_PLACEHOLDER_KIND,
- );
- assert.equal(JSON.stringify(budgeted.events).includes(sentinel), false);
- });
-
- test('deserializes JSON, undefined, and fallback strings', () => {
- assert.deepEqual(deserializeToolResultArchive('{"kind":"text","text":"ok"}'), {
- kind: 'text',
- text: 'ok',
- });
- assert.equal(deserializeToolResultArchive('undefined'), undefined);
- assert.equal(deserializeToolResultArchive('plain fallback'), 'plain fallback');
- });
-
- test('hydrates archived placeholders for replay only after hash validation', async () => {
- const originalResult = { kind: 'text', text: 'old archived payload' };
- const serialized = serializeToolResultForArchive(originalResult);
- const events = [
- toolCall('call-old', 'turn-old', 'tool-old'),
- toolResult('result-old', 'turn-old', 'tool-old', originalResult),
- toolCall('call-new', 'turn-new', 'tool-new'),
- toolResult('result-new', 'turn-new', 'tool-new', { kind: 'text', text: 'new full payload' }),
- ];
- const budgeted = applyRuntimeEventContextBudget(events, {
- staleToolResultPrune: {
- enabled: true,
- maxResultEstimatedTokens: 1,
- minRecentTurnsFull: 1,
- archiveRefs: [
- {
- runtimeEventId: 'result-old',
- toolCallId: 'tool-old',
- toolName: 'Read',
- artifactId: 'artifact-old',
- bodySha256: sha256(serialized),
- originalEstimatedTokens: serialized.length,
- originalBytes: utf8Bytes(serialized),
- rewriteVersion: ARCHIVED_TOOL_RESULT_REWRITE_VERSION,
- reason: 'stale_tool_result_pruned_before_compact',
- },
- ],
- },
- archiveRetrieval: { enabled: true, maxResults: 1, maxEstimatedTokens: 1024, maxBytes: 1024 },
- minRecentTurns: 2,
- charsPerToken: 1,
- });
-
- assert.ok(budgeted);
- const retrieval = await retrieveArchivedToolResultsForReplay(
- budgeted.events,
- { enabled: true, maxResults: 1, maxEstimatedTokens: 1024, maxBytes: 1024 },
- async () => ({ ok: true, serializedResult: serialized }),
- { sessionId: 'session-1', charsPerToken: 1 },
- );
-
- const originalBudgeted = budgeted.events.find((event) => event.id === 'result-old');
- assert.equal(
- originalBudgeted?.content?.kind === 'function_response'
- ? (originalBudgeted.content.result as { kind?: string }).kind
- : undefined,
- ARCHIVED_TOOL_RESULT_PLACEHOLDER_KIND,
- );
- const hydrated = retrieval.events.find((event) => event.id === 'result-old');
- assert.deepEqual(
- hydrated?.content?.kind === 'function_response' ? hydrated.content.result : undefined,
- originalResult,
- );
- assert.equal(retrieval.diagnosticPatch.retrievedArchiveToolResults, 1);
- assert.equal(retrieval.diagnosticPatch.retrievedArchiveEstimatedTokens, serialized.length);
- assert.deepEqual(retrieval.retrievedSourceRefs, [
- {
- kind: 'archived_tool_result',
- sessionId: 'session-1',
- turnId: 'turn-old',
- runtimeEventId: 'result-old',
- toolCallId: 'tool-old',
- toolName: 'Read',
- artifactId: 'artifact-old',
- bodySha256: sha256(serialized),
- originalEstimatedTokens: serialized.length,
- originalBytes: utf8Bytes(serialized),
- placeholderReason: 'stale_tool_result_pruned_before_compact',
- },
- ]);
- });
-
- test('uses UTF-8 byte length for non-ASCII archive size validation', async () => {
- const originalResult = { kind: 'text', text: '旧归档 payload 🙂'.repeat(3) };
- const serialized = serializeToolResultForArchive(originalResult);
- assert.ok(utf8Bytes(serialized) > serialized.length);
- const events = [
- toolCall('call-old', 'turn-old', 'tool-old'),
- toolResult('result-old', 'turn-old', 'tool-old', originalResult),
- toolCall('call-new', 'turn-new', 'tool-new'),
- toolResult('result-new', 'turn-new', 'tool-new', { kind: 'text', text: 'new full payload' }),
- ];
- const budgeted = applyRuntimeEventContextBudget(events, {
- staleToolResultPrune: {
- enabled: true,
- maxResultEstimatedTokens: 1,
- minRecentTurnsFull: 1,
- archiveRefs: [
- {
- runtimeEventId: 'result-old',
- toolCallId: 'tool-old',
- toolName: 'Read',
- artifactId: 'artifact-old',
- bodySha256: sha256(serialized),
- originalEstimatedTokens: serialized.length,
- originalBytes: utf8Bytes(serialized),
- rewriteVersion: ARCHIVED_TOOL_RESULT_REWRITE_VERSION,
- reason: 'stale_tool_result_pruned_before_compact',
- },
- ],
- },
- archiveRetrieval: { enabled: true, maxResults: 1, maxEstimatedTokens: 1024, maxBytes: 1024 },
- minRecentTurns: 1,
- charsPerToken: 1,
- });
-
- assert.ok(budgeted);
- assert.equal(budgeted.diagnostic.prunedToolResults, 1);
- const retrieval = await retrieveArchivedToolResultsForReplay(
- budgeted.events,
- { enabled: true, maxResults: 1, maxEstimatedTokens: 1024, maxBytes: 1024 },
- async (input) =>
- input.originalBytes === utf8Bytes(serialized)
- ? { ok: true, serializedResult: serialized }
- : { ok: false, reason: 'size_mismatch' },
- { sessionId: 'session-1', charsPerToken: 1 },
- );
-
- const hydrated = retrieval.events.find((event) => event.id === 'result-old');
- assert.deepEqual(
- hydrated?.content?.kind === 'function_response' ? hydrated.content.result : undefined,
- originalResult,
- );
- assert.equal(retrieval.diagnosticPatch.retrievedArchiveToolResults, 1);
- assert.equal(retrieval.diagnosticPatch.archiveRetrievalFailures, 0);
- });
-
- test('rejects archive refs whose body hash does not match the current result', () => {
- const currentResult = { kind: 'text', text: 'alpha payload sentinel' };
- const staleResult = { kind: 'text', text: 'bravo payload sentinel' };
- const currentSerialized = serializeToolResultForArchive(currentResult);
- const staleSerialized = serializeToolResultForArchive(staleResult);
- assert.equal(currentSerialized.length, staleSerialized.length);
- assert.equal(utf8Bytes(currentSerialized), utf8Bytes(staleSerialized));
- const events = [
- toolCall('call-old', 'turn-old', 'tool-old'),
- toolResult('result-old', 'turn-old', 'tool-old', currentResult),
- toolCall('call-new', 'turn-new', 'tool-new'),
- toolResult('result-new', 'turn-new', 'tool-new', { kind: 'text', text: 'new full payload' }),
- ];
-
- const budgeted = applyRuntimeEventContextBudget(events, {
- staleToolResultPrune: {
- enabled: true,
- maxResultEstimatedTokens: 1,
- minRecentTurnsFull: 1,
- archiveRefs: [
- {
- runtimeEventId: 'result-old',
- toolCallId: 'tool-old',
- toolName: 'Read',
- artifactId: 'artifact-stale',
- bodySha256: sha256(staleSerialized),
- originalEstimatedTokens: currentSerialized.length,
- originalBytes: utf8Bytes(currentSerialized),
- rewriteVersion: ARCHIVED_TOOL_RESULT_REWRITE_VERSION,
- reason: 'stale_tool_result_pruned_before_compact',
- },
- ],
- },
- minRecentTurns: 1,
- charsPerToken: 1,
- });
-
- assert.ok(budgeted);
- assert.equal(budgeted.diagnostic.prunedToolResults ?? 0, 0);
- assert.equal(budgeted.diagnostic.archiveWriteFailures, 1);
- const result = budgeted.events.find((event) => event.id === 'result-old');
- assert.deepEqual(
- result?.content?.kind === 'function_response' ? result.content.result : undefined,
- currentResult,
- );
- });
-
- test('keeps placeholders and records corrupt/missing archive diagnostics', async () => {
- const serialized = serializeToolResultForArchive({ kind: 'text', text: 'body' });
- const events = [
- toolCall('call-1', 'turn-1', 'tool-1'),
- archivedResult('result-1', 'turn-1', 'tool-1', {
- artifactId: 'artifact-1',
- bodySha256: sha256(serialized),
- originalEstimatedTokens: serialized.length,
- originalBytes: utf8Bytes(serialized),
- }),
- ];
-
- const corrupt = await retrieveArchivedToolResultsForReplay(
- events,
- { enabled: true, maxResults: 1, maxEstimatedTokens: 1024, maxBytes: 1024 },
- async () => ({ ok: true, serializedResult: 'tampered' }),
- { sessionId: 'session-1' },
- );
- assert.equal(corrupt.diagnosticPatch.archiveRetrievalFailures, 1);
- assert.deepEqual(corrupt.diagnosticPatch.archiveRetrievalFailureReasonCounts, { corrupt: 1 });
- assert.equal(
- corrupt.events[1]?.content?.kind === 'function_response'
- ? (corrupt.events[1].content.result as { kind?: string }).kind
- : undefined,
- ARCHIVED_TOOL_RESULT_PLACEHOLDER_KIND,
- );
-
- const missing = await retrieveArchivedToolResultsForReplay(
- events,
- { enabled: true, maxResults: 1, maxEstimatedTokens: 1024, maxBytes: 1024 },
- async () => ({ ok: false, reason: 'not_found' }),
- { sessionId: 'session-1' },
- );
- assert.deepEqual(missing.diagnosticPatch.archiveRetrievalFailureReasonCounts, { not_found: 1 });
- });
-
- test('fails open when retrieval is disabled or no reader is available', async () => {
- const serialized = serializeToolResultForArchive({ kind: 'text', text: 'body' });
- const events = [
- archivedResult('result-1', 'turn-1', 'tool-1', {
- artifactId: 'artifact-1',
- bodySha256: sha256(serialized),
- originalEstimatedTokens: serialized.length,
- originalBytes: utf8Bytes(serialized),
- }),
- ];
-
- const disabled = await retrieveArchivedToolResultsForReplay(
- events,
- { enabled: false, maxResults: 1, maxEstimatedTokens: 1024, maxBytes: 1024 },
- async () => ({ ok: true, serializedResult: serialized }),
- { sessionId: 'session-1' },
- );
- assert.notEqual(disabled.events, events);
- assert.deepEqual(disabled.events, events);
- assert.deepEqual(disabled.diagnosticPatch, {});
-
- const noReader = await retrieveArchivedToolResultsForReplay(
- events,
- { enabled: true, maxResults: 1, maxEstimatedTokens: 1024, maxBytes: 1024 },
- undefined,
- { sessionId: 'session-1' },
- );
- assert.deepEqual(noReader.events, events);
- assert.deepEqual(noReader.diagnosticPatch, {});
- });
-
- test('skips oversized candidates before reading archives', async () => {
- const small = serializeToolResultForArchive({ kind: 'text', text: 'small' });
- const big = serializeToolResultForArchive({ kind: 'text', text: 'big' });
- const events = [
- archivedResult('result-big', 'turn-1', 'tool-big', {
- artifactId: 'artifact-big',
- bodySha256: sha256(big),
- originalEstimatedTokens: 500,
- originalBytes: 5000,
- }),
- archivedResult('result-small', 'turn-2', 'tool-small', {
- artifactId: 'artifact-small',
- bodySha256: sha256(small),
- originalEstimatedTokens: small.length,
- originalBytes: utf8Bytes(small),
- }),
- ];
- const readIds: string[] = [];
-
- const result = await retrieveArchivedToolResultsForReplay(
- events,
- { enabled: true, maxResults: 2, maxEstimatedTokens: 100, maxBytes: 100 },
- async (input) => {
- readIds.push(input.runtimeEventId);
- return { ok: true, serializedResult: small };
- },
- { sessionId: 'session-1' },
- );
-
- assert.deepEqual(readIds, ['result-small']);
- assert.equal(result.diagnosticPatch.retrievedArchiveToolResults, 1);
- assert.equal(result.diagnosticPatch.archiveRetrievalSkipped, 1);
- assert.deepEqual(result.diagnosticPatch.archiveRetrievalSkippedReasonCounts, { max_bytes: 1 });
- });
-
- test('selects newest placeholders first and obeys max result bounds', async () => {
- const first = serializeToolResultForArchive({ kind: 'text', text: 'first' });
- const second = serializeToolResultForArchive({ kind: 'text', text: 'second' });
- const events = [
- toolCall('call-1', 'turn-1', 'tool-1'),
- archivedResult('result-1', 'turn-1', 'tool-1', {
- artifactId: 'artifact-1',
- bodySha256: sha256(first),
- originalEstimatedTokens: first.length,
- originalBytes: utf8Bytes(first),
- }),
- toolCall('call-2', 'turn-2', 'tool-2'),
- archivedResult('result-2', 'turn-2', 'tool-2', {
- artifactId: 'artifact-2',
- bodySha256: sha256(second),
- originalEstimatedTokens: second.length,
- originalBytes: utf8Bytes(second),
- }),
- ];
- const seen: string[] = [];
-
- const retrieved = await retrieveArchivedToolResultsForReplay(
- events,
- { enabled: true, maxResults: 1, maxEstimatedTokens: 1024, maxBytes: 1024 },
- async (input) => {
- seen.push(input.runtimeEventId);
- return { ok: true, serializedResult: input.runtimeEventId === 'result-2' ? second : first };
- },
- { sessionId: 'session-1' },
- );
-
- assert.deepEqual(seen, ['result-2']);
- assert.equal(retrieved.diagnosticPatch.retrievedArchiveToolResults, 1);
- assert.deepEqual(
- retrieved.events[3]?.content?.kind === 'function_response'
- ? retrieved.events[3].content.result
- : undefined,
- { kind: 'text', text: 'second' },
- );
- });
-
- test('gates archive reads to history-selected turns when requested', async () => {
- const selected = serializeToolResultForArchive({ kind: 'text', text: 'selected' });
- const unselected = serializeToolResultForArchive({ kind: 'text', text: 'unselected' });
- const events = [
- archivedResult('result-selected', 'turn-selected', 'tool-selected', {
- artifactId: 'artifact-selected',
- bodySha256: sha256(selected),
- originalEstimatedTokens: selected.length,
- originalBytes: utf8Bytes(selected),
- }),
- archivedResult('result-unselected', 'turn-unselected', 'tool-unselected', {
- artifactId: 'artifact-unselected',
- bodySha256: sha256(unselected),
- originalEstimatedTokens: unselected.length,
- originalBytes: utf8Bytes(unselected),
- }),
- ];
- const reads: string[] = [];
-
- const retrieved = await retrieveArchivedToolResultsForReplay(
- events,
- {
- enabled: true,
- mode: 'history_search_gated',
- maxResults: 2,
- maxEstimatedTokens: 1024,
- maxBytes: 1024,
- },
- async (input) => {
- reads.push(input.runtimeEventId);
- return {
- ok: true,
- serializedResult: input.runtimeEventId === 'result-selected' ? selected : unselected,
- };
- },
- { sessionId: 'session-1', allowedTurnIds: new Set(['turn-selected']) },
- );
-
- assert.deepEqual(reads, ['result-selected']);
- assert.equal(retrieved.diagnosticPatch.archiveRetrievalMode, 'history_search_gated');
- assert.equal(retrieved.diagnosticPatch.archiveRetrievalEligibleTurns, 1);
- assert.equal(retrieved.diagnosticPatch.retrievedArchiveToolResults, 1);
- assert.equal(retrieved.diagnosticPatch.archiveRetrievalSkipped, 1);
- assert.deepEqual(retrieved.diagnosticPatch.archiveRetrievalSkippedReasonCounts, {
- history_search_gate: 1,
- });
- assert.deepEqual(
- retrieved.events[0]?.content?.kind === 'function_response'
- ? retrieved.events[0].content.result
- : undefined,
- { kind: 'text', text: 'selected' },
- );
- assert.equal(
- retrieved.events[1]?.content?.kind === 'function_response'
- ? (retrieved.events[1].content.result as { kind?: string }).kind
- : undefined,
- ARCHIVED_TOOL_RESULT_PLACEHOLDER_KIND,
- );
- });
-});
-
-describe('context-budget synthesis cache', () => {
- test('selects a source-bearing synthesis block and injects it for replay only', () => {
- const serialized = serializeToolResultForArchive({ text: 'raw archived key-alpha payload' });
- const events = [
- toolCall('call-alpha', 'turn-alpha', 'tool-alpha'),
- archivedResult('result-alpha', 'turn-alpha', 'tool-alpha', {
- artifactId: 'artifact-alpha',
- bodySha256: sha256(serialized),
- originalEstimatedTokens: serialized.length,
- originalBytes: utf8Bytes(serialized),
- }),
- textEvent('recent', 'turn-recent', 'newer retained context'),
- ];
- const block = synthesisBlock({
- queryKey: 'key-alpha',
- turnId: 'turn-alpha',
- runtimeEventId: 'result-alpha',
- toolCallId: 'tool-alpha',
- artifactId: 'artifact-alpha',
- bodySha256: sha256(serialized),
- originalEstimatedTokens: serialized.length,
- originalBytes: utf8Bytes(serialized),
- });
-
- const result = selectSynthesisCacheForReplay(
- events,
- 'Recover key-alpha',
- { enabled: true, blocks: [block] },
- { sessionId: 'session-1', charsPerToken: 1 },
- );
-
- assert.equal(result.selectedBlocks.length, 1);
- assert.equal(result.diagnosticPatch.synthesisCacheMode, 'lookup');
- assert.deepEqual(result.diagnosticPatch.synthesisCacheBlockIds, ['synth-key-alpha']);
- assert.equal(result.diagnosticPatch.highWaterName, 'after-gated-key-alpha');
- assert.equal(
- events.some((event) => event.id === 'result-alpha'),
- true,
- );
- assert.equal(
- result.events.some((event) => event.id === 'result-alpha'),
- false,
- );
- assert.equal(
- result.events.some((event) => event.id === 'recent'),
- true,
- );
- const synthetic = result.events.find((event) => event.id === 'synthesis-cache:synth-key-alpha');
- assert.equal(synthetic?.role, 'model');
- assert.match(
- synthetic?.content?.kind === 'text' ? synthetic.content.text : '',
- /<maka_synthesis_cache_block/,
- );
- assert.match(renderSynthesisCacheBlock(block), /artifact-alpha/);
- });
-
- test('retains same-turn events not covered by a selected synthesis block', () => {
- const serialized = serializeToolResultForArchive({ text: 'raw archived key-alpha payload' });
- const events = [
- textEvent('prompt-alpha', 'turn-alpha', 'please inspect key-alpha'),
- toolCall('call-alpha', 'turn-alpha', 'tool-alpha'),
- archivedResult('result-alpha', 'turn-alpha', 'tool-alpha', {
- artifactId: 'artifact-alpha',
- bodySha256: sha256(serialized),
- originalEstimatedTokens: serialized.length,
- originalBytes: utf8Bytes(serialized),
- }),
- toolCall('call-beta', 'turn-alpha', 'tool-beta'),
- toolResult('result-beta', 'turn-alpha', 'tool-beta', { text: 'unrelated same-turn payload' }),
- textEvent('recent', 'turn-recent', 'newer retained context'),
- ];
- const block = synthesisBlock({
- queryKey: 'key-alpha',
- turnId: 'turn-alpha',
- runtimeEventId: 'result-alpha',
- toolCallId: 'tool-alpha',
- artifactId: 'artifact-alpha',
- bodySha256: sha256(serialized),
- originalEstimatedTokens: serialized.length,
- originalBytes: utf8Bytes(serialized),
- });
-
- const result = selectSynthesisCacheForReplay(
- events,
- 'Recover key-alpha',
- { enabled: true, blocks: [block] },
- { sessionId: 'session-1', charsPerToken: 1 },
- );
-
- assert.deepEqual(
- result.events.map((event) => event.id),
- ['prompt-alpha', 'synthesis-cache:synth-key-alpha', 'call-beta', 'result-beta', 'recent'],
- );
- });
-
- test('inserts a selected synthesis block at the covered event position', () => {
- const serialized = serializeToolResultForArchive({ text: 'raw archived key-alpha payload' });
- const events = [
- textEvent('before', 'turn-before', 'older retained context'),
- toolCall('call-alpha', 'turn-alpha', 'tool-alpha'),
- archivedResult('result-alpha', 'turn-alpha', 'tool-alpha', {
- artifactId: 'artifact-alpha',
- bodySha256: sha256(serialized),
- originalEstimatedTokens: serialized.length,
- originalBytes: utf8Bytes(serialized),
- }),
- textEvent('after', 'turn-after', 'newer retained context'),
- ];
- const block = synthesisBlock({
- queryKey: 'key-alpha',
- turnId: 'turn-alpha',
- runtimeEventId: 'result-alpha',
- toolCallId: 'tool-alpha',
- artifactId: 'artifact-alpha',
- bodySha256: sha256(serialized),
- originalEstimatedTokens: serialized.length,
- originalBytes: utf8Bytes(serialized),
- });
-
- const result = selectSynthesisCacheForReplay(
- events,
- 'Recover key-alpha',
- { enabled: true, blocks: [block] },
- { sessionId: 'session-1', charsPerToken: 1 },
- );
-
- assert.deepEqual(
- result.events.map((event) => event.id),
- ['before', 'synthesis-cache:synth-key-alpha', 'after'],
- );
- });
-
- test('inserts multiple selected synthesis blocks at their covered event positions', () => {
- const alpha = serializeToolResultForArchive({ text: 'raw archived key-alpha payload' });
- const beta = serializeToolResultForArchive({ text: 'raw archived key-beta payload' });
- const events = [
- textEvent('before', 'turn-before', 'older retained context'),
- toolCall('call-alpha', 'turn-alpha', 'tool-alpha'),
- archivedResult('result-alpha', 'turn-alpha', 'tool-alpha', {
- artifactId: 'artifact-alpha',
- bodySha256: sha256(alpha),
- originalEstimatedTokens: alpha.length,
- originalBytes: utf8Bytes(alpha),
- }),
- textEvent('middle', 'turn-middle', 'retained middle context'),
- toolCall('call-beta', 'turn-beta', 'tool-beta'),
- archivedResult('result-beta', 'turn-beta', 'tool-beta', {
- artifactId: 'artifact-beta',
- bodySha256: sha256(beta),
- originalEstimatedTokens: beta.length,
- originalBytes: utf8Bytes(beta),
- }),
- textEvent('after', 'turn-after', 'newer retained context'),
- ];
- const alphaBlock = synthesisBlock({
- queryKey: 'key-alpha',
- turnId: 'turn-alpha',
- runtimeEventId: 'result-alpha',
- toolCallId: 'tool-alpha',
- artifactId: 'artifact-alpha',
- bodySha256: sha256(alpha),
- originalEstimatedTokens: alpha.length,
- originalBytes: utf8Bytes(alpha),
- });
- const betaBlock = synthesisBlock({
- queryKey: 'key-beta',
- turnId: 'turn-beta',
- runtimeEventId: 'result-beta',
- toolCallId: 'tool-beta',
- artifactId: 'artifact-beta',
- bodySha256: sha256(beta),
- originalEstimatedTokens: beta.length,
- originalBytes: utf8Bytes(beta),
- });
-
- const result = selectSynthesisCacheForReplay(
- events,
- 'Recover key-alpha and key-beta',
- { enabled: true, blocks: [alphaBlock, betaBlock], maxBlocks: 2 },
- { sessionId: 'session-1', charsPerToken: 1 },
- );
-
- assert.deepEqual(
- result.events.map((event) => event.id),
- [
- 'before',
- 'synthesis-cache:synth-key-alpha',
- 'middle',
- 'synthesis-cache:synth-key-beta',
- 'after',
- ],
- );
- });
-
- test('does not select synthesis when raw evidence is requested', () => {
- const serialized = serializeToolResultForArchive({ text: 'raw archived key-alpha payload' });
- const events = [
- archivedResult('result-alpha', 'turn-alpha', 'tool-alpha', {
- artifactId: 'artifact-alpha',
- bodySha256: sha256(serialized),
- originalEstimatedTokens: serialized.length,
- originalBytes: utf8Bytes(serialized),
- }),
- ];
- const block = synthesisBlock({
- queryKey: 'key-alpha',
- turnId: 'turn-alpha',
- runtimeEventId: 'result-alpha',
- toolCallId: 'tool-alpha',
- artifactId: 'artifact-alpha',
- bodySha256: sha256(serialized),
- originalEstimatedTokens: serialized.length,
- originalBytes: utf8Bytes(serialized),
- });
-
- const result = selectSynthesisCacheForReplay(
- events,
- 'Show raw archive evidence for key-alpha',
- { enabled: true, blocks: [block] },
- { sessionId: 'session-1' },
- );
-
- assert.equal(result.selectedBlocks.length, 0);
- assert.equal(result.diagnosticPatch.synthesisCacheMode, 'fallback_archive_retrieval');
- assert.deepEqual(result.diagnosticPatch.synthesisCacheSkippedReasonCounts, {
- raw_evidence_requested: 1,
- });
- assert.deepEqual(result.events, events);
- });
-
- test('does not select synthesis for a longer uncovered key that shares a prefix', () => {
- const serialized = serializeToolResultForArchive({ text: 'raw archived key-alpha payload' });
- const events = [
- archivedResult('result-alpha', 'turn-alpha', 'tool-alpha', {
- artifactId: 'artifact-alpha',
- bodySha256: sha256(serialized),
- originalEstimatedTokens: serialized.length,
- originalBytes: utf8Bytes(serialized),
- }),
- ];
- const block = synthesisBlock({
- queryKey: 'key-alpha',
- turnId: 'turn-alpha',
- runtimeEventId: 'result-alpha',
- toolCallId: 'tool-alpha',
- artifactId: 'artifact-alpha',
- bodySha256: sha256(serialized),
- originalEstimatedTokens: serialized.length,
- originalBytes: utf8Bytes(serialized),
- });
-
- const result = selectSynthesisCacheForReplay(
- events,
- 'Recover key-alpha-noise-01',
- { enabled: true, blocks: [block] },
- { sessionId: 'session-1' },
- );
-
- assert.equal(result.selectedBlocks.length, 0);
- assert.deepEqual(result.diagnosticPatch.synthesisCacheSkippedReasonCounts, {
- coverage_miss: 1,
- });
- });
-
- test('does not append synthesis when a source event is missing', () => {
- const serialized = serializeToolResultForArchive({ text: 'raw archived key-alpha payload' });
- const events = [textEvent('recent', 'turn-recent', 'newer retained context')];
- const block = synthesisBlock({
- queryKey: 'key-alpha',
- turnId: 'turn-alpha',
- runtimeEventId: 'result-alpha',
- toolCallId: 'tool-alpha',
- artifactId: 'artifact-alpha',
- bodySha256: sha256(serialized),
- originalEstimatedTokens: serialized.length,
- originalBytes: utf8Bytes(serialized),
- });
-
- const result = selectSynthesisCacheForReplay(
- events,
- 'Recover key-alpha',
- { enabled: true, blocks: [block] },
- { sessionId: 'session-1' },
- );
-
- assert.equal(result.selectedBlocks.length, 0);
- assert.deepEqual(
- result.events.map((event) => event.id),
- ['recent'],
- );
- assert.deepEqual(result.diagnosticPatch.synthesisCacheInvalidationReasonCounts, {
- source_missing: 1,
- });
- });
-
- test('invalidates synthesis when source placeholder hashes do not match', () => {
- const serialized = serializeToolResultForArchive({ text: 'raw archived key-alpha payload' });
- const events = [
- archivedResult('result-alpha', 'turn-alpha', 'tool-alpha', {
- artifactId: 'artifact-alpha',
- bodySha256: sha256(serialized),
- originalEstimatedTokens: serialized.length,
- originalBytes: utf8Bytes(serialized),
- }),
- ];
- const block = synthesisBlock({
- queryKey: 'key-alpha',
- turnId: 'turn-alpha',
- runtimeEventId: 'result-alpha',
- toolCallId: 'tool-alpha',
- artifactId: 'artifact-alpha',
- bodySha256: 'sha256:mismatch',
- originalEstimatedTokens: serialized.length,
- originalBytes: utf8Bytes(serialized),
- });
-
- const result = selectSynthesisCacheForReplay(
- events,
- 'Recover key-alpha',
- { enabled: true, blocks: [block] },
- { sessionId: 'session-1' },
- );
-
- assert.equal(result.selectedBlocks.length, 0);
- assert.deepEqual(result.diagnosticPatch.synthesisCacheInvalidationReasonCounts, {
- source_hash_mismatch: 1,
- });
- });
-
- test('invalidates synthesis when a newer matching tool result exists', () => {
- const serialized = serializeToolResultForArchive({ text: 'raw archived key-alpha payload' });
- const events = [
- archivedResult('result-alpha', 'turn-alpha', 'tool-alpha', {
- artifactId: 'artifact-alpha',
- bodySha256: sha256(serialized),
- originalEstimatedTokens: serialized.length,
- originalBytes: utf8Bytes(serialized),
- }),
- {
- ...archivedResult('result-newer', 'turn-newer', 'tool-newer', {
- artifactId: 'artifact-newer',
- bodySha256: sha256(serialized),
- originalEstimatedTokens: serialized.length,
- originalBytes: utf8Bytes(serialized),
- }),
- ts: 1_800_000_000_010,
- content: {
- kind: 'function_response' as const,
- id: 'tool-newer',
- name: 'Read',
- result: { text: 'new key-alpha payload' },
- isError: false,
- },
- },
- ];
- const block = synthesisBlock({
- queryKey: 'key-alpha',
- turnId: 'turn-alpha',
- runtimeEventId: 'result-alpha',
- toolCallId: 'tool-alpha',
- artifactId: 'artifact-alpha',
- bodySha256: sha256(serialized),
- originalEstimatedTokens: serialized.length,
- originalBytes: utf8Bytes(serialized),
- });
-
- const result = selectSynthesisCacheForReplay(
- events,
- 'Recover key-alpha',
- { enabled: true, blocks: [block] },
- { sessionId: 'session-1' },
- );
-
- assert.equal(result.selectedBlocks.length, 0);
- assert.deepEqual(result.diagnosticPatch.synthesisCacheInvalidationReasonCounts, {
- new_relevant_tool_result: 1,
- });
- });
-
- test('enforces synthesis block count and token budgets', () => {
- const serialized = serializeToolResultForArchive({ text: 'raw archived key-alpha payload' });
- const events = [
- archivedResult('result-alpha', 'turn-alpha', 'tool-alpha', {
- artifactId: 'artifact-alpha',
- bodySha256: sha256(serialized),
- originalEstimatedTokens: serialized.length,
- originalBytes: utf8Bytes(serialized),
- }),
- archivedResult('result-beta', 'turn-beta', 'tool-beta', {
- artifactId: 'artifact-beta',
- bodySha256: sha256(serialized),
- originalEstimatedTokens: serialized.length,
- originalBytes: utf8Bytes(serialized),
- }),
- ];
- const first = synthesisBlock({
- queryKey: 'key-alpha',
- turnId: 'turn-alpha',
- runtimeEventId: 'result-alpha',
- toolCallId: 'tool-alpha',
- artifactId: 'artifact-alpha',
- bodySha256: sha256(serialized),
- originalEstimatedTokens: serialized.length,
- originalBytes: utf8Bytes(serialized),
- });
- const second = {
- ...synthesisBlock({
- queryKey: 'key-alpha',
- turnId: 'turn-beta',
- runtimeEventId: 'result-beta',
- toolCallId: 'tool-beta',
- artifactId: 'artifact-beta',
- bodySha256: sha256(serialized),
- originalEstimatedTokens: serialized.length,
- originalBytes: utf8Bytes(serialized),
- }),
- blockId: 'synth-key-alpha-second',
- };
-
- const maxBlocks = selectSynthesisCacheForReplay(
- events,
- 'Recover key-alpha',
- { enabled: true, blocks: [first, second], maxBlocks: 1, maxEstimatedTokens: 10_000 },
- { sessionId: 'session-1' },
- );
- assert.equal(maxBlocks.selectedBlocks.length, 1);
- assert.deepEqual(maxBlocks.diagnosticPatch.synthesisCacheSkippedReasonCounts, {
- max_blocks: 1,
- });
-
- const maxBlockTokens = selectSynthesisCacheForReplay(
- events,
- 'Recover key-alpha',
- { enabled: true, blocks: [{ ...first, estimatedTokens: 99 }], maxBlockEstimatedTokens: 10 },
- { sessionId: 'session-1' },
- );
- assert.equal(maxBlockTokens.selectedBlocks.length, 0);
- assert.deepEqual(maxBlockTokens.diagnosticPatch.synthesisCacheSkippedReasonCounts, {
- max_block_tokens: 1,
- });
-
- const maxTotalTokens = selectSynthesisCacheForReplay(
- events,
- 'Recover key-alpha',
- {
- enabled: true,
- blocks: [
- { ...first, estimatedTokens: 7 },
- { ...second, estimatedTokens: 7 },
- ],
- maxBlocks: 2,
- maxEstimatedTokens: 10,
- },
- { sessionId: 'session-1' },
- );
- assert.equal(maxTotalTokens.selectedBlocks.length, 1);
- assert.deepEqual(maxTotalTokens.diagnosticPatch.synthesisCacheSkippedReasonCounts, {
- max_total_tokens: 1,
- });
- });
-
- test('builds stable bounded synthesis blocks from hydrated archive refs', () => {
- const resultBody = { text: 'key-alpha stable sentinel SYNTHESIS_BUILDER_SENTINEL' };
- const serialized = serializeToolResultForArchive(resultBody);
- const placeholderEvents = [
- archivedResult('result-alpha', 'turn-alpha', 'tool-alpha', {
- artifactId: 'artifact-alpha',
- bodySha256: sha256(serialized),
- originalEstimatedTokens: serialized.length,
- originalBytes: utf8Bytes(serialized),
- }),
- ];
- const hydratedEvents = [
- {
- ...placeholderEvents[0]!,
- content: {
- kind: 'function_response' as const,
- id: 'tool-alpha',
- name: 'Read',
- result: resultBody,
- },
- },
- ];
- const sourceRefs = [
- {
- kind: 'archived_tool_result' as const,
- sessionId: 'session-1',
- turnId: 'turn-alpha',
- runtimeEventId: 'result-alpha',
- toolCallId: 'tool-alpha',
- toolName: 'Read',
- artifactId: 'artifact-alpha',
- bodySha256: sha256(serialized),
- originalEstimatedTokens: serialized.length,
- originalBytes: utf8Bytes(serialized),
- placeholderReason: 'stale_tool_result_pruned_before_compact' as const,
- },
- ];
-
- const first = buildSynthesisCacheBlocksFromHydratedArchives({
- sessionId: 'session-1',
- query: 'Recover key-alpha',
- hydratedRuntimeEvents: hydratedEvents,
- retrievedArchiveRefs: sourceRefs,
- archiveRetrievalMode: 'history_search_gated',
- limits: {
- maxBlocks: 1,
- maxBlockEstimatedTokens: 1024,
- maxEstimatedTokens: 1024,
- charsPerToken: 4,
- },
- now: 1_800_000_000_100,
- });
- const second = buildSynthesisCacheBlocksFromHydratedArchives({
- sessionId: 'session-1',
- query: 'Recover key-alpha',
- hydratedRuntimeEvents: hydratedEvents,
- retrievedArchiveRefs: sourceRefs,
- archiveRetrievalMode: 'history_search_gated',
- limits: {
- maxBlocks: 1,
- maxBlockEstimatedTokens: 1024,
- maxEstimatedTokens: 1024,
- charsPerToken: 4,
- },
- now: 1_800_000_000_100,
- });
-
- assert.equal(first.blocks.length, 1);
- assert.equal(first.blocks[0]?.blockId, second.blocks[0]?.blockId);
- assert.equal(first.blocks[0]?.createdFrom, 'gated_archive_retrieval');
- assert.deepEqual(first.blocks[0]?.coverage.artifactIds, ['artifact-alpha']);
- assert.equal(first.blocks[0]?.coverage.queryKeys.includes('key-alpha'), true);
- for (const genericKey of ['context', 'for', 'json', 'kind', 'lookup', 'the', 'tools']) {
- assert.equal(first.blocks[0]?.coverage.queryKeys.includes(genericKey), false, genericKey);
- }
- assert.match(first.blocks[0]?.summary ?? '', /SYNTHESIS_BUILDER_SENTINEL/);
- assert.equal(validateSynthesisCacheBlockShape(first.blocks[0], 'session-1'), true);
-
- const genericMiss = selectSynthesisCacheForReplay(
- placeholderEvents,
- 'Recover an unrelated lookup key',
- { enabled: true, blocks: first.blocks },
- { sessionId: 'session-1' },
- );
- assert.equal(genericMiss.selectedBlocks.length, 0);
- assert.deepEqual(genericMiss.diagnosticPatch.synthesisCacheSkippedReasonCounts, {
- coverage_miss: 1,
- });
-
- const tooSmall = buildSynthesisCacheBlocksFromHydratedArchives({
- sessionId: 'session-1',
- query: 'Recover key-alpha',
- hydratedRuntimeEvents: hydratedEvents,
- retrievedArchiveRefs: sourceRefs,
- archiveRetrievalMode: 'history_search_gated',
- limits: { maxBlocks: 1, maxBlockEstimatedTokens: 1, maxEstimatedTokens: 1, charsPerToken: 1 },
- });
- assert.equal(tooSmall.blocks.length, 0);
- assert.deepEqual(tooSmall.skippedReasonCounts, { max_block_tokens: 1 });
- });
-
- test('does not synthesize hidden archived nested results', () => {
- const hiddenBody = { text: 'DO_NOT_REPLAY_HIDDEN_ARCHIVE' };
- const hiddenSerialized = serializeToolResultForArchive(hiddenBody);
- const hiddenEvent = {
- ...toolResult('hidden-synthesis-result', 'turn-hidden', 'hidden-synthesis-tool', hiddenBody),
- modelVisibility: 'hidden' as const,
- };
- const result = buildSynthesisCacheBlocksFromHydratedArchives({
- sessionId: 'session-1',
- query: 'hidden archive',
- hydratedRuntimeEvents: [hiddenEvent],
- retrievedArchiveRefs: [
- {
- kind: 'archived_tool_result' as const,
- sessionId: 'session-1',
- turnId: 'turn-hidden',
- runtimeEventId: hiddenEvent.id,
- toolCallId: 'hidden-synthesis-tool',
- toolName: 'Read',
- artifactId: 'hidden-artifact',
- bodySha256: sha256(hiddenSerialized),
- originalEstimatedTokens: hiddenSerialized.length,
- originalBytes: utf8Bytes(hiddenSerialized),
- placeholderReason: 'stale_tool_result_pruned_before_compact' as const,
- },
- ],
- archiveRetrievalMode: 'history_search_gated',
- limits: {
- maxBlocks: 1,
- maxBlockEstimatedTokens: 1024,
- maxEstimatedTokens: 1024,
- charsPerToken: 4,
- },
- });
-
- assert.deepEqual(result.blocks, []);
- assert.equal(result.skippedReasonCounts?.source_missing, 1);
- });
-});
-
-describe('context-budget history compact', () => {
- test('folds older turns into a source-bearing compact block at high water', () => {
- const events = [
- textEvent('old-1', 'turn-1', 'alpha context '.repeat(12)),
- textEvent('old-2', 'turn-2', 'beta context '.repeat(12)),
- toolCall('old-call', 'turn-3', 'tool-old'),
- toolResult('old-result', 'turn-3', 'tool-old', { text: 'gamma tool payload '.repeat(8) }),
- textEvent('recent-1', 'turn-4', 'recent user context'),
- textEvent('recent-2', 'turn-5', 'latest user context'),
- ];
-
- const result = applyRuntimeEventContextBudget(events, {
- maxHistoryEstimatedTokens: 1600,
- minRecentTurns: 2,
- charsPerToken: 1,
- historyCompact: {
- enabled: true,
- highWaterName: 'reasonix-lowfreq-v1',
- highWaterRatio: 0.01,
- tailEstimatedTokens: 48,
- minRecentTurns: 2,
- maxSummaryEstimatedTokens: 90,
- },
- });
-
- assert.ok(result);
- assert.equal(result.diagnostic.highWaterReason, 'history_compact');
- assert.equal(result.diagnostic.highWaterName, 'reasonix-lowfreq-v1');
- assert.equal(result.diagnostic.historyCompactMode, 'deterministic');
- assert.equal(result.diagnostic.historyCompactedTurns, 3);
- assert.equal(result.diagnostic.historyCompactedEvents, 4);
- assert.equal(result.diagnostic.droppedTurns, 3);
- assert.equal(result.diagnostic.droppedEvents, 3);
- assert.equal(result.diagnostic.historyCompactBlockIds?.length, 1);
- assert.deepEqual(result.diagnostic.compactionDecisions, [
- {
- stage: 'priorReplay',
- sourceKind: 'runtimeEvents',
- decision: 'replaced',
- boundaryKind: 'historyCompact',
- boundaryIds: result.diagnostic.historyCompactBlockIds,
- coveredTurns: 3,
- coveredRuntimeEvents: 4,
- coverageHashes: result.diagnostic.historyCompactCoverageHashes,
- estimatedTokensBefore: result.diagnostic.historyCompactedEstimatedTokensBefore,
- estimatedTokensAfter: result.diagnostic.historyCompactedEstimatedTokensAfter,
- estimatedTokensSaved: Math.max(
- 0,
- (result.diagnostic.historyCompactedEstimatedTokensBefore ?? 0) -
- (result.diagnostic.historyCompactedEstimatedTokensAfter ?? 0),
- ),
- },
- ]);
- assert.equal(
- result.events.some((event) => event.id === 'old-1'),
- false,
- );
- assert.equal(
- result.events.some((event) => event.id === 'old-result'),
- false,
- );
- assert.equal(
- result.events.some((event) => event.id === 'recent-1'),
- true,
- );
- assert.equal(
- result.events.some((event) => event.id === 'recent-2'),
- true,
- );
-
- const synthetic = result.events.find((event) => event.id.startsWith('history-compact:'));
- assert.equal(synthetic?.role, 'user');
- assert.equal(synthetic?.author, 'system');
- const compactText = synthetic?.content?.kind === 'text' ? synthetic.content.text : '';
- assert.match(compactText, /<maka_history_compact_block/);
- assert.match(compactText, /coverage: 4 runtime events across 3 turns/);
- assert.doesNotMatch(compactText, /runtimeEventIds=\[/);
- assert.equal(
- events.some((event) => event.id === 'old-1'),
- true,
- 'input events remain unchanged',
- );
- });
-
- test('lookup mode uses loaded blocks but does not synthesize a fallback block', () => {
- const events = [
- textEvent('old-1', 'turn-1', 'alpha context '.repeat(12)),
- textEvent('old-2', 'turn-2', 'beta context '.repeat(12)),
- textEvent('recent-1', 'turn-3', 'recent tail'),
- ];
-
- const result = applyRuntimeEventContextBudget(events, {
- maxHistoryEstimatedTokens: 1000,
- minRecentTurns: 1,
- charsPerToken: 1,
- historyCompact: {
- enabled: true,
- mode: 'lookup',
- highWaterRatio: 0.01,
- targetRatio: 0.2,
- },
- });
-
- assert.ok(result);
- assert.deepEqual(
- result.events.map((event) => event.id),
- events.map((event) => event.id),
- );
- assert.equal(result.historyCompactBlocks?.length ?? 0, 0);
- assert.deepEqual(result.diagnostic.historyCompactSkippedReasonCounts, { lookup_miss: 1 });
- assert.deepEqual(result.diagnostic.compactionDecisions, [
- {
- stage: 'priorReplay',
- sourceKind: 'runtimeEvents',
- decision: 'unchanged',
- boundaryKind: 'historyCompact',
- reason: 'lookup_miss',
- skippedReasonCounts: { lookup_miss: 1 },
- },
- ]);
- });
-
- test('keeps prior history unchanged below high water', () => {
- const events = [
- textEvent('short-1', 'turn-1', 'short context'),
- textEvent('short-2', 'turn-2', 'still short'),
- ];
-
- const result = applyRuntimeEventContextBudget(events, {
- maxHistoryEstimatedTokens: 1000,
- charsPerToken: 1,
- historyCompact: {
- enabled: true,
- highWaterRatio: 0.8,
- },
- });
-
- assert.ok(result);
- assert.deepEqual(
- result.events.map((event) => event.id),
- ['short-1', 'short-2'],
- );
- assert.equal(result.diagnostic.historyCompactEnabled, true);
- assert.deepEqual(result.diagnostic.historyCompactSkippedReasonCounts, { below_high_water: 1 });
- assert.deepEqual(result.diagnostic.compactionDecisions, [
- {
- stage: 'priorReplay',
- sourceKind: 'runtimeEvents',
- decision: 'unchanged',
- boundaryKind: 'historyCompact',
- reason: 'below_high_water',
- skippedReasonCounts: { below_high_water: 1 },
- },
- ]);
- assert.equal(result.diagnostic.highWaterReason, undefined);
- });
-
- test('honors the history compact tail cap before the requested recent-turn count', () => {
- const events = [
- textEvent('old-1', 'turn-1', 'old context '.repeat(30)),
- textEvent('old-2', 'turn-2', 'older context '.repeat(30)),
- textEvent('recent-1', 'turn-3', 'recent one '.repeat(40)),
- textEvent('recent-2', 'turn-4', 'recent two '.repeat(40)),
- textEvent('latest', 'turn-5', 'latest tail'),
- ];
-
- const result = applyRuntimeEventContextBudget(events, {
- maxHistoryEstimatedTokens: 2000,
- minRecentTurns: 3,
- charsPerToken: 1,
- historyCompact: {
- enabled: true,
- highWaterRatio: 0.1,
- minRecentTurns: 3,
- tailEstimatedTokens: 25,
- maxSummaryEstimatedTokens: 120,
- },
- });
-
- assert.ok(result);
- assert.equal(
- result.events.some((event) => event.id === 'recent-1'),
- false,
- );
- assert.equal(
- result.events.some((event) => event.id === 'recent-2'),
- false,
- );
- assert.equal(
- result.events.some((event) => event.id === 'latest'),
- true,
- );
- assert.equal(result.diagnostic.historyCompactedTurns, 4);
- });
-
- test('V2 checkpoint compaction retains only the latest complete turn', () => {
- const events = [
- textEvent('old-1', 'turn-1', 'old context '.repeat(40)),
- textEvent('tail-2', 'turn-2', 'tail two'),
- textEvent('tail-3', 'turn-3', 'tail three'),
- textEvent('tail-4', 'turn-4', 'tail four'),
- textEvent('tail-5', 'turn-5', 'tail five'),
- ];
-
- const result = applyRuntimeEventContextBudget(
- events,
- {
- maxHistoryEstimatedTokens: 2000,
- minRecentTurns: 2,
- charsPerToken: 1,
- historyCompact: {
- enabled: true,
- highWaterRatio: 0.1,
- minRecentTurns: 2,
- tailEstimatedTokens: 100,
- maxSummaryEstimatedTokens: 120,
- },
- },
- { historyCompactProtocol: 'checkpoint_v2' },
- );
-
- assert.ok(result);
- assert.deepEqual(
- result.events
- .filter((event) => !event.id.startsWith('history-compact:'))
- .map((event) => event.id),
- ['tail-5'],
- );
- assert.equal(result.diagnostic.historyCompactedTurns, 4);
- });
-
- test('overflow recovery may fold the latest complete turn with no raw tail', () => {
- const events = [
- toolCall('latest-call', 'turn-latest', 'tool-1'),
- toolResult('latest-result', 'turn-latest', 'tool-1', {
- text: 'oversized latest result '.repeat(40),
- }),
- ];
-
- const result = applyRuntimeEventContextBudget(
- events,
- {
- maxHistoryEstimatedTokens: 2000,
- minRecentTurns: 0,
- charsPerToken: 1,
- historyCompact: {
- enabled: true,
- highWaterRatio: 0.1,
- minRecentTurns: 0,
- maxSummaryEstimatedTokens: 120,
- },
- },
- { historyCompactProtocol: 'checkpoint_v2' },
- );
-
- assert.ok(result);
- assert.deepEqual(
- result.events
- .filter((event) => !event.id.startsWith('history-compact:'))
- .map((event) => event.id),
- [],
- );
- assert.equal(result.diagnostic.historyCompactedTurns, 1);
- });
-
- test('keeps the complete latest turn as the continuation seam', () => {
- const events = [
- textEvent('old-1', 'turn-1', 'old context '.repeat(30)),
- toolCall('latest-call-1', 'turn-2', 'tool-1'),
- toolResult('latest-result-1', 'turn-2', 'tool-1', { text: 'first huge result '.repeat(20) }),
- toolCall('latest-call-2', 'turn-2', 'tool-2'),
- toolResult('latest-result-2', 'turn-2', 'tool-2', { text: 'second huge result '.repeat(20) }),
- ];
-
- const result = applyRuntimeEventContextBudget(
- events,
- {
- maxHistoryEstimatedTokens: 2500,
- minRecentTurns: 3,
- charsPerToken: 1,
- historyCompact: {
- enabled: true,
- highWaterRatio: 0.1,
- minRecentTurns: 3,
- tailEstimatedTokens: 10,
- maxSummaryEstimatedTokens: 120,
- },
- },
- { historyCompactProtocol: 'checkpoint_v2' },
- );
-
- assert.ok(result);
- assert.equal(
- result.events.some((event) => event.id === 'old-1'),
- false,
- );
- assert.equal(
- result.events.some((event) => event.id === 'latest-call-1'),
- true,
- );
- assert.equal(
- result.events.some((event) => event.id === 'latest-result-1'),
- true,
- );
- assert.equal(
- result.events.some((event) => event.id === 'latest-call-2'),
- true,
- );
- assert.equal(
- result.events.some((event) => event.id === 'latest-result-2'),
- true,
- );
- assert.equal(result.diagnostic.historyCompactedTurns, 1);
- });
-
- test('falls back to normal pruning when a generated compact replay exceeds budget', () => {
- const maxHistoryEstimatedTokens = 700;
- const events = [
- textEvent('old-1', 'turn-1', 'old compact source alpha '.repeat(40)),
- textEvent('old-2', 'turn-2', 'old compact source beta '.repeat(40)),
- toolCall('latest-call', 'turn-3', 'tool-latest'),
- toolResult('latest-result', 'turn-3', 'tool-latest', {
- text: 'latest retained tool result '.repeat(12),
- }),
- ];
-
- const result = applyRuntimeEventContextBudget(events, {
- maxHistoryEstimatedTokens,
- minRecentTurns: 1,
- charsPerToken: 1,
- historyCompact: {
- enabled: true,
- highWaterRatio: 0.5,
- tailEstimatedTokens: 10,
- minRecentTurns: 1,
- maxSummaryEstimatedTokens: 120,
- },
- });
-
- assert.ok(result);
- assert.equal(result.historyCompactBlocks, undefined);
- assert.equal(result.diagnostic.estimatedTokensAfter <= maxHistoryEstimatedTokens, true);
- assert.deepEqual(result.diagnostic.historyCompactSkippedReasonCounts, {
- replay_over_budget: 1,
- });
- assert.equal(
- result.events.some((event) => event.id.startsWith('history-compact:')),
- false,
- );
- assert.deepEqual(
- result.events.map((event) => event.id),
- ['latest-call', 'latest-result'],
- );
- });
-
- test('skips compaction when archive-before-project is required but missing', () => {
- const events = [
- textEvent('old-1', 'turn-1', 'missing archive source '.repeat(20)),
- textEvent('old-2', 'turn-2', 'also missing archive '.repeat(20)),
- textEvent('recent-1', 'turn-3', 'recent tail'),
- ];
-
- const result = applyRuntimeEventContextBudget(events, {
- maxHistoryEstimatedTokens: 500,
- minRecentTurns: 1,
- charsPerToken: 1,
- historyCompact: {
- enabled: true,
- archiveRequired: true,
- highWaterRatio: 0.5,
- targetRatio: 0.2,
- },
- });
-
- assert.ok(result);
- assert.equal(
- result.events.some((event) => event.id.startsWith('history-compact:')),
- false,
- );
- assert.equal(result.diagnostic.historyCompactSkipped, 1);
- assert.deepEqual(result.diagnostic.historyCompactSkippedReasonCounts, { archive_missing: 1 });
- assert.deepEqual(result.diagnostic.compactionDecisions, [
- {
- stage: 'priorReplay',
- sourceKind: 'runtimeEvents',
- decision: 'failedOpen',
- boundaryKind: 'historyCompact',
- reason: 'archive_missing',
- failOpenReason: 'archive_missing',
- skippedReasonCounts: { archive_missing: 1 },
- },
- ]);
- assert.equal(result.diagnostic.highWaterReason, undefined);
- });
-
- test('builds stable compact blocks with explicit render and shape validation', () => {
- const events = [
- textEvent('old-1', 'turn-1', 'stable alpha '.repeat(15)),
- textEvent('old-2', 'turn-2', 'stable beta '.repeat(15)),
- textEvent('recent-1', 'turn-3', 'recent tail'),
- ];
- const policy = {
- maxHistoryEstimatedTokens: 1500,
- charsPerToken: 1,
- historyCompact: {
- enabled: true,
- highWaterRatio: 0.01,
- tailEstimatedTokens: 36,
- minRecentTurns: 1,
- maxSummaryEstimatedTokens: 120,
- },
- } as const;
-
- const first = applyRuntimeEventHistoryCompact(
- events,
- policy.historyCompact,
- policy.charsPerToken,
- policy.maxHistoryEstimatedTokens,
- { charsPerToken: 1, maxHistoryEstimatedTokens: 1500 },
- );
- const second = applyRuntimeEventHistoryCompact(
- events,
- policy.historyCompact,
- policy.charsPerToken,
- policy.maxHistoryEstimatedTokens,
- { charsPerToken: 1, maxHistoryEstimatedTokens: 1500 },
- );
-
- assert.equal(first.blocks.length, 1);
- assert.equal(first.blocks[0]?.blockId, second.blocks[0]?.blockId);
- assert.equal(validateHistoryCompactBlockShape(first.blocks[0], 'session-1'), true);
- const rendered = renderHistoryCompactBlock(first.blocks[0]!);
- assert.match(rendered, /coverage: 2 runtime events across 2 turns/);
- assert.doesNotMatch(rendered, /bodySha256=/);
- assert.ok(
- (first.blocks[0]?.coverage.bodySha256.length ?? 0) > 0,
- 'hashes stay in the block JSON',
- );
- assert.equal(first.events[0]?.id, `history-compact:${first.blocks[0]?.blockId}`);
- const boundary = historyCompactBlockToCompactionBoundary(first.blocks[0]!, {
- renderedText: renderHistoryCompactBlock(first.blocks[0]!),
- preservedAnchor: { tailTurnIds: ['turn-3'] },
- validationStatus: 'valid',
- });
- assert.equal(boundary.kind, 'historyCompact');
- assert.equal(boundary.stage, 'priorReplay');
- assert.equal(boundary.boundaryId, first.blocks[0]?.blockId);
- assert.deepEqual(boundary.coverage.runtimeEventIds, ['old-1', 'old-2']);
- assert.deepEqual(boundary.preservedAnchor?.tailTurnIds, ['turn-3']);
- assert.equal(boundary.validationStatus, 'valid');
- });
-
- test('excludes hidden events from compact summaries and coverage', () => {
- const hidden = {
- ...textEvent('hidden-nested', 'turn-1', 'DO_NOT_REPLAY_NESTED_ARGUMENT'),
- modelVisibility: 'hidden' as const,
- };
- const result = applyRuntimeEventHistoryCompact(
- [
- textEvent('visible-old', 'turn-1', 'visible prior context'),
- hidden,
- textEvent('visible-recent', 'turn-2', 'visible recent context'),
- ],
- {
- enabled: true,
- highWaterRatio: 0.001,
- minRecentTurns: 0,
- maxSummaryEstimatedTokens: 1_000,
- },
- 1,
- 5_000,
- { charsPerToken: 1, maxHistoryEstimatedTokens: 5_000 },
- );
-
- assert.equal(result.blocks.length, 1);
- assert.doesNotMatch(result.blocks[0]?.summary ?? '', /DO_NOT_REPLAY_NESTED_ARGUMENT/);
- assert.equal(result.blocks[0]?.coverage.runtimeEventIds.includes(hidden.id), false);
- });
-
- test('selects a loaded compact block instead of rebuilding the folded region', () => {
- const folded = [
- textEvent('old-1', 'turn-1', 'loaded alpha '.repeat(15)),
- textEvent('old-2', 'turn-2', 'loaded beta '.repeat(15)),
- ];
- const loadedBlock = buildHistoryCompactBlockFromSummary({
- sessionId: 'session-1',
- foldedRuntimeEvents: folded,
- summary: 'LOADED_CONTEXT_COMPACT_SENTINEL',
- highWaterName: 'loaded-compact',
- highWaterSeq: 9,
- charsPerToken: 1,
- });
-
- const result = applyRuntimeEventContextBudget(
- [...folded, textEvent('recent-1', 'turn-3', 'recent tail')],
- {
- maxHistoryEstimatedTokens: 10_000,
- charsPerToken: 1,
- historyCompact: {
- enabled: true,
- mode: 'lookup',
- highWaterRatio: 0.000001,
- targetRatio: 0.2,
- tailEstimatedTokens: 1,
- minRecentTurns: 1,
- blocks: [loadedBlock],
- },
- },
- );
-
- assert.ok(result);
- assert.equal(result.historyCompactBlocks?.[0]?.blockId, loadedBlock.blockId);
- assert.equal(result.diagnostic.historyCompactBlocksAvailable, 1);
- assert.equal(result.diagnostic.historyCompactBlocksSelected, 1);
- assert.deepEqual(result.diagnostic.historyCompactBlockIds, [loadedBlock.blockId]);
- assert.equal(result.diagnostic.compactionDecisions?.[0]?.decision, 'replaced');
- assert.deepEqual(result.diagnostic.compactionDecisions?.[0]?.boundaryIds, [
- loadedBlock.blockId,
- ]);
- assert.match(
- result.events[0]?.content?.kind === 'text' ? result.events[0].content.text : '',
- /LOADED_CONTEXT_COMPACT_SENTINEL/,
- );
- });
-
- test('lookup history compact keeps matching a loaded prefix after newer turns extend the fold', () => {
- const covered = [
- textEvent('old-1', 'turn-1', 'loaded alpha '.repeat(15)),
- textEvent('old-2', 'turn-2', 'loaded beta '.repeat(15)),
- ];
- const loadedBlock = buildHistoryCompactBlockFromSummary({
- sessionId: 'session-1',
- foldedRuntimeEvents: covered,
- summary: 'LOADED_CONTEXT_COMPACT_SENTINEL',
- highWaterName: 'loaded-compact',
- highWaterSeq: 9,
- charsPerToken: 1,
- });
- const uncoveredFolded = textEvent('former-tail', 'turn-3', 'former retained tail now foldable');
- const retainedOne = textEvent('recent-1', 'turn-4', 'recent retained one');
- const retainedTwo = textEvent('recent-2', 'turn-5', 'recent retained two');
-
- const result = applyRuntimeEventContextBudget(
- [...covered, uncoveredFolded, retainedOne, retainedTwo],
- {
- maxHistoryEstimatedTokens: 10_000,
- charsPerToken: 1,
- historyCompact: {
- enabled: true,
- mode: 'lookup',
- highWaterRatio: 0.000001,
- tailEstimatedTokens: 1,
- minRecentTurns: 2,
- blocks: [loadedBlock],
- },
- },
- );
-
- assert.ok(result);
- assert.equal(result.historyCompactBlocks?.[0]?.blockId, loadedBlock.blockId);
- assert.deepEqual(
- result.events.map((event) => event.id),
- [`history-compact:${loadedBlock.blockId}`, 'former-tail', 'recent-1', 'recent-2'],
- );
- assert.equal(result.diagnostic.compactionDecisions?.[0]?.decision, 'replaced');
- });
-
- test('lookup prefix compact falls back to normal pruning when uncovered suffix exceeds budget', () => {
- const covered = [
- textEvent('old-1', 'turn-1', 'loaded alpha '.repeat(15)),
- textEvent('old-2', 'turn-2', 'loaded beta '.repeat(15)),
- ];
- const loadedBlock = buildHistoryCompactBlockFromSummary({
- sessionId: 'session-1',
- foldedRuntimeEvents: covered,
- summary: 'LOADED_CONTEXT_COMPACT_SENTINEL',
- highWaterName: 'loaded-compact',
- highWaterSeq: 9,
- charsPerToken: 1,
- });
-
- const result = applyRuntimeEventContextBudget(
- [
- ...covered,
- textEvent('huge-former-tail', 'turn-3', 'huge uncovered suffix '.repeat(50)),
- textEvent('recent', 'turn-4', 'recent retained'),
- ],
- {
- maxHistoryEstimatedTokens: 80,
- minRecentTurns: 1,
- charsPerToken: 1,
- historyCompact: {
- enabled: true,
- mode: 'lookup',
- highWaterRatio: 0.000001,
- tailEstimatedTokens: 1,
- minRecentTurns: 1,
- blocks: [loadedBlock],
- },
- },
- );
-
- assert.ok(result);
- assert.equal(result.historyCompactBlocks, undefined);
- assert.deepEqual(
- result.events.map((event) => event.id),
- ['recent'],
- );
- assert.deepEqual(result.diagnostic.historyCompactSkippedReasonCounts, {
- prefix_over_budget: 1,
- });
- });
-
- test('ignores diagnostic-only runtime turns when retaining recent turns', () => {
- const result = applyRuntimeEventContextBudget(
- [
- textEvent('old-1', 'turn-1', 'old context that should be dropped'),
- textEvent('recent-1', 'turn-2', 'recent real context'),
- diagnosticRuntimeEvent('manual-compact-terminal', 'turn-compact'),
- ],
- {
- maxHistoryTurns: 1,
- minRecentTurns: 1,
- charsPerToken: 1,
- },
- );
-
- assert.ok(result);
- assert.deepEqual(
- result.events.map((event) => event.id),
- ['recent-1'],
- );
- assert.equal(result.diagnostic.keptTurns, 1);
- });
-
- test('ignores diagnostic-only runtime turns when matching loaded compact blocks', () => {
- const folded = [
- textEvent('old-1', 'turn-1', 'loaded alpha '.repeat(15)),
- textEvent('old-2', 'turn-2', 'loaded beta '.repeat(15)),
- ];
- const loadedBlock = buildHistoryCompactBlockFromSummary({
- sessionId: 'session-1',
- foldedRuntimeEvents: folded,
- summary: 'LOADED_CONTEXT_COMPACT_SENTINEL',
- highWaterName: 'loaded-compact',
- highWaterSeq: 9,
- charsPerToken: 1,
- });
-
- const result = applyRuntimeEventContextBudget(
- [
- ...folded,
- textEvent('recent-1', 'turn-3', 'recent retained one'),
- textEvent('recent-2', 'turn-4', 'recent retained two'),
- diagnosticRuntimeEvent('manual-compact-terminal', 'turn-compact'),
- ],
- {
- maxHistoryEstimatedTokens: 10_000,
- charsPerToken: 1,
- historyCompact: {
- enabled: true,
- mode: 'lookup',
- highWaterRatio: 0.000001,
- targetRatio: 0.2,
- tailEstimatedTokens: 1,
- minRecentTurns: 2,
- blocks: [loadedBlock],
- },
- },
- );
-
- assert.ok(result);
- assert.equal(result.historyCompactBlocks?.[0]?.blockId, loadedBlock.blockId);
- assert.deepEqual(result.diagnostic.historyCompactSkippedReasonCounts, undefined);
- assert.equal(
- result.events.some((event) => event.id === 'manual-compact-terminal'),
- false,
- );
- assert.equal(
- result.events.some((event) => event.id === 'recent-1'),
- true,
- );
- assert.equal(
- result.events.some((event) => event.id === 'recent-2'),
- true,
- );
- });
-});
-
-describe('context-budget search and rewrite diagnostics', () => {
- test('retrieves bounded around-context for deterministic RuntimeEvent search hits', () => {
- const events = [
- textEvent('before', 'turn-1', 'setup context'),
- textEvent('target', 'turn-2', 'needle project archive detail'),
- textEvent('after', 'turn-3', 'follow-up context'),
- textEvent('far', 'turn-4', 'unrelated'),
- ];
-
- const result = retrieveRuntimeEventHistoryAround(
- events,
- 'please find needle',
- { enabled: true, maxResults: 1, around: 1, maxEstimatedTokens: 1000 },
- { charsPerToken: 1 },
- );
-
- assert.deepEqual(
- result.events.map((event) => event.id),
- ['before', 'target', 'after'],
- );
- assert.equal(result.diagnosticPatch.historySearchMatches, 1);
- assert.equal(result.diagnosticPatch.historyAroundRetrievedEvents, 3);
- });
-
- test('records named history rewrite gate version and reset reason', () => {
- const budgeted = applyRuntimeEventContextBudget([textEvent('event-1', 'turn-1', 'hello')], {
- historyRewrite: {
- enabled: true,
- name: 'phase6-high-water',
- historyRewriteVersion: 'phase6-v1',
- resetReason: 'explicit_test_reset',
- },
- });
-
- assert.ok(budgeted);
- assert.equal(budgeted.diagnostic.historyRewriteGate, 'phase6-high-water');
- assert.equal(budgeted.diagnostic.historyRewriteVersion, 'phase6-v1');
- assert.equal(budgeted.diagnostic.historyRewriteResetReason, 'explicit_test_reset');
+test('capacity policy keeps the canonical ledger until a checkpoint replaces it', () => {
+ const events = [textEvent('user', 'large history '.repeat(100))];
+ const result = applyRuntimeEventContextBudget(events, {
+ maxHistoryEstimatedTokens: 1,
+ historyCompact: { enabled: true },
});
+ assert.deepEqual(result?.events, events);
});
-function textEvent(id: string, turnId: string, text: string): RuntimeEvent {
- return baseEvent({
- id,
- turnId,
- role: 'user',
- author: 'user',
- content: { kind: 'text', text },
- });
-}
-
-function diagnosticRuntimeEvent(id: string, turnId: string): RuntimeEvent {
- return baseEvent({
- id,
- turnId,
- role: 'system',
- author: 'system',
- status: 'completed',
- actions: { endInvocation: true },
- });
-}
-
-function toolCall(id: string, turnId: string, toolCallId: string): RuntimeEvent {
- return baseEvent({
- id,
- turnId,
- role: 'model',
- author: 'agent',
- content: {
- kind: 'function_call',
- id: toolCallId,
- name: 'Read',
- args: { path: `${toolCallId}.txt` },
- },
- });
-}
-
-function toolResult(id: string, turnId: string, toolCallId: string, result: unknown): RuntimeEvent {
- return baseEvent({
- id,
- turnId,
- role: 'tool',
- author: 'tool',
- content: { kind: 'function_response', id: toolCallId, name: 'Read', result },
- });
-}
-
-function archivedResult(
- id: string,
- turnId: string,
- toolCallId: string,
- archive: {
- artifactId: string;
- bodySha256: string;
- originalEstimatedTokens: number;
- originalBytes: number;
- },
-): RuntimeEvent {
- return toolResult(id, turnId, toolCallId, {
- kind: ARCHIVED_TOOL_RESULT_PLACEHOLDER_KIND,
- rewriteVersion: ARCHIVED_TOOL_RESULT_REWRITE_VERSION,
- runtimeEventId: id,
- toolCallId,
- toolName: 'Read',
- reason: 'stale_tool_result_pruned_before_compact',
- ...archive,
- });
-}
-
-function synthesisBlock(input: {
- queryKey: string;
- turnId: string;
- runtimeEventId: string;
- toolCallId: string;
- artifactId: string;
- bodySha256: string;
- originalEstimatedTokens: number;
- originalBytes: number;
-}): SynthesisCacheBlock {
- return {
- kind: 'maka.synthesis_cache_block',
- version: 1,
- blockId: `synth-${input.queryKey}`,
- sessionId: 'session-1',
- createdAt: 1_800_000_000_001,
- highWaterName: `after-gated-${input.queryKey}`,
- highWaterSeq: 1,
- coverage: {
- queryKeys: [input.queryKey],
- turnIds: [input.turnId],
- runtimeEventIds: [input.runtimeEventId],
- toolNames: ['Read'],
- toolCallIds: [input.toolCallId],
- artifactIds: [input.artifactId],
- bodySha256: [input.bodySha256],
- },
- summary: `The stable answer for ${input.queryKey} is SYNTH_SENTINEL.`,
- limitations: ['Does not include raw tool output.'],
- sourceRefs: [
- {
- kind: 'archived_tool_result',
- sessionId: 'session-1',
- turnId: input.turnId,
- runtimeEventId: input.runtimeEventId,
- toolCallId: input.toolCallId,
- toolName: 'Read',
- artifactId: input.artifactId,
- bodySha256: input.bodySha256,
- originalEstimatedTokens: input.originalEstimatedTokens,
- originalBytes: input.originalBytes,
- placeholderReason: 'stale_tool_result_pruned_before_compact',
- },
- ],
- createdFrom: 'gated_archive_retrieval',
- };
-}
-
-function baseEvent(overrides: Partial<RuntimeEvent>): RuntimeEvent {
+function textEvent(id: string, text: string): RuntimeEvent {
return {
- id: 'event',
+ id,
+ invocationId: 'invocation-1',
sessionId: 'session-1',
runId: 'run-1',
turnId: 'turn-1',
- invocationId: 'invocation-1',
- ts: 1_800_000_000_000,
+ ts: 1,
partial: false,
- role: 'system',
- author: 'system',
- ...overrides,
+ role: 'user',
+ author: 'user',
+ status: 'completed',
+ modelVisibility: 'visible',
+ content: { kind: 'text', text },
};
}
-
-function sha256(text: string): string {
- return createHash('sha256').update(text).digest('hex');
-}
-
-function utf8Bytes(text: string): number {
- return Buffer.byteLength(text, 'utf8');
-}
diff --git a/packages/runtime/src/__tests__/conversation-copy.test.ts b/packages/runtime/src/__tests__/conversation-copy.test.ts
index 7f194753a..6819c7f36 100644
--- a/packages/runtime/src/__tests__/conversation-copy.test.ts
+++ b/packages/runtime/src/__tests__/conversation-copy.test.ts
@@ -46,7 +46,7 @@ import {
matchHistoryCompactCheckpointPrefix,
validateHistoryCompactCheckpointShape,
} from '../history-compact-checkpoint.js';
-import { isHistoryCompactContentEvent } from '../history-compact.js';
+import { isHistoryCompactContentEvent } from '../history-compaction.js';
import { RuntimeReadModel, type RuntimeReadModelSessionView } from '../runtime-read-model.js';
import { buildToolOperationId } from '../runtime-commit-sink.js';
import { buildToolResultArchiveResourceRef } from '../tool-result-archive-resource.js';
@@ -1077,7 +1077,7 @@ test('conversation copy clones one terminal Runtime ledger with new owned identi
turnId: 'turn-1',
ts: 2.7,
data: { blockId: 'semantic-source', block: { sourceOwnedHash: true } },
- });
+ } as unknown as EmittedAgentRunEvent);
await runStore.appendEvent('session-source', 'run-source', {
type: 'history_compact_checkpoint_recorded',
id: 'checkpoint-source',
diff --git a/packages/runtime/src/__tests__/cost.test.ts b/packages/runtime/src/__tests__/cost.test.ts
index b2f0b43b7..698340ac4 100644
--- a/packages/runtime/src/__tests__/cost.test.ts
+++ b/packages/runtime/src/__tests__/cost.test.ts
@@ -146,7 +146,7 @@ describe('recordLlmCall', () => {
{
sessionId: 'session-1',
turnId: 'turn-1',
- callKind: 'semantic_compact',
+ callKind: 'history_compact',
callId: 'compact-1',
connectionSlug: 'connection-1',
providerId: 'anthropic',
diff --git a/packages/runtime/src/__tests__/deferred-tools-backend.test.ts b/packages/runtime/src/__tests__/deferred-tools-backend.test.ts
index 7fdb6cb02..d2bfdf30d 100644
--- a/packages/runtime/src/__tests__/deferred-tools-backend.test.ts
+++ b/packages/runtime/src/__tests__/deferred-tools-backend.test.ts
@@ -289,51 +289,6 @@ describe('AiSdkBackend deferred tool loading', () => {
);
});
- test('high-water "after" hash stays consistent with the final recorded requestShapeHash across a same-turn load', async () => {
- const records: SendDiagnostics[] = [];
- const implCalls: string[] = [];
- const be = backend(loadBrowserThenFinishModel(), implCalls, {
- recordSendDiagnostics: (r) => records.push(r),
- });
- // Real high-water reasons only arise from the synthesis-cache subsystem
- // (selectSynthesisCacheForReplay needs valid cache blocks + matching
- // history). Inject just the marker by wrapping buildPriorMessages so this
- // targets the diagnostics-consistency invariant, not that subsystem.
- type PriorReplayish = { contextBudget?: Record<string, unknown> };
- const patch = be as unknown as {
- buildPriorMessages: (scope: unknown, input: unknown) => Promise<PriorReplayish>;
- };
- const realBuildPriorMessages = patch.buildPriorMessages.bind(be);
- patch.buildPriorMessages = async (scope: unknown, input: unknown) => {
- const prior = await realBuildPriorMessages(scope, input);
- return {
- ...prior,
- contextBudget: {
- ...(prior.contextBudget ?? {}),
- highWaterReason: 'synthesis_cache_select',
- },
- };
- };
-
- await drain(be.send({ turnId: 'turn-1', text: 'load browser', context: [] }));
-
- assert.equal(records.length, 1, 'one llm-call record for the turn');
- const cb = records[0]?.contextBudget;
- assert.ok(cb?.highWaterRequestShapeHashAfter, 'a high-water "after" hash was recorded');
- // The same-turn load makes the final active set differ from step-0, so this
- // equality fails if "after" is left at the step-0 hash (the bug).
- assert.equal(
- cb.highWaterRequestShapeHashAfter,
- records[0]?.requestShapeHash,
- 'high-water "after" must equal the final recorded requestShapeHash',
- );
- assert.equal(
- cb.highWaterRequestShapeHashBefore,
- undefined,
- 'first turn has no pre-turn baseline',
- );
- });
-
test('economy off: every tool stays advertised, no connector', async () => {
const captured: string[][] = [];
const implCalls: string[] = [];
diff --git a/packages/runtime/src/__tests__/history-compact-artifacts.test.ts b/packages/runtime/src/__tests__/history-compact-artifacts.test.ts
deleted file mode 100644
index 8b8490c16..000000000
--- a/packages/runtime/src/__tests__/history-compact-artifacts.test.ts
+++ /dev/null
@@ -1,167 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import assert from 'node:assert/strict';
-import { describe, test } from 'node:test';
-import type { RuntimeEvent } from '@maka/core/runtime-event';
-import { buildHistoryCompactBlockFromSummary } from '../context-budget.js';
-import type { HistoryCompactWriteInput } from '../ai-sdk-compaction-contract.js';
-import {
- loadHistoryCompactBlocksFromArtifacts,
- persistHistoryCompactBlocksToArtifacts,
-} from '../history-compact-artifacts.js';
-import { memoryArtifactStore } from './memory-artifact-store.js';
-
-describe('history compact artifacts', () => {
- test('persists a compact block within default limits when the fold covers many events', async () => {
- const store = memoryArtifactStore();
- const foldedEvents = Array.from({ length: 60 }, (_, index) =>
- textEvent(`old-${index}`, `turn-${Math.floor(index / 4)}`, `folded fact number ${index}`),
- );
- const input: HistoryCompactWriteInput = {
- sessionId: 'session-1',
- turnId: 'turn-write',
- source: {
- draftBlock: buildHistoryCompactBlockFromSummary({
- sessionId: 'session-1',
- foldedRuntimeEvents: foldedEvents,
- summary: 'deterministic fallback',
- highWaterName: 'test-history-compact',
- highWaterSeq: 1,
- now: 1_800_000_000_000,
- charsPerToken: 4,
- }),
- foldedRuntimeEvents: foldedEvents,
- },
- limits: {
- maxBlocks: 1,
- maxBlockEstimatedTokens: 1_024,
- maxEstimatedTokens: 2_048,
- charsPerToken: 4,
- },
- };
-
- const write = await persistHistoryCompactBlocksToArtifacts(store, input, {
- now: () => 1_800_000_000_100,
- summarize: () => 'short summary of a long session',
- });
-
- assert.deepEqual(write.skippedReasonCounts, undefined);
- assert.equal(write.blocks.length, 1);
- assert.ok((write.blocks[0]?.estimatedTokens ?? 0) <= 1_024);
- assert.equal(write.blocks[0]?.sourceArchiveRefs?.length, 60);
- });
-
- test('loads a metadata-heavy block without an explicit byte cap', async () => {
- const store = memoryArtifactStore();
- const foldedEvents = Array.from({ length: 60 }, (_, index) =>
- textEvent(`old-${index}`, `turn-${Math.floor(index / 4)}`, `folded fact number ${index}`),
- );
- const write = await persistHistoryCompactBlocksToArtifacts(
- store,
- {
- sessionId: 'session-1',
- turnId: 'turn-write',
- source: {
- draftBlock: buildHistoryCompactBlockFromSummary({
- sessionId: 'session-1',
- foldedRuntimeEvents: foldedEvents,
- summary: 'deterministic fallback',
- highWaterName: 'test-history-compact',
- highWaterSeq: 1,
- now: 1_800_000_000_000,
- charsPerToken: 4,
- }),
- foldedRuntimeEvents: foldedEvents,
- },
- limits: {
- maxBlocks: 1,
- maxBlockEstimatedTokens: 1_024,
- maxEstimatedTokens: 2_048,
- charsPerToken: 4,
- },
- },
- {
- now: () => 1_800_000_000_100,
- summarize: () => 'short summary of a long session',
- },
- );
- assert.equal(write.blocks.length, 1);
-
- const loaded = await loadHistoryCompactBlocksFromArtifacts(store, {
- sessionId: 'session-1',
- maxBlocks: 1,
- maxEstimatedTokens: 2_048,
- });
-
- assert.deepEqual(loaded.skippedReasonCounts, undefined);
- assert.equal(loaded.blocks[0]?.blockId, write.blocks[0]?.blockId);
- });
-
- test('skips a block whose persisted token estimate understates its rendered size', async () => {
- const store = memoryArtifactStore();
- const foldedEvents = [textEvent('old-0', 'turn-0', 'folded fact')];
- const block = buildHistoryCompactBlockFromSummary({
- sessionId: 'session-1',
- foldedRuntimeEvents: foldedEvents,
- summary: 'short summary',
- highWaterName: 'test-history-compact',
- highWaterSeq: 1,
- now: 1_800_000_000_000,
- charsPerToken: 4,
- });
- await store.create({
- sessionId: 'session-1',
- turnId: 'turn-write',
- name: `history-compact-${block.blockId}.json`,
- kind: 'file',
- content: JSON.stringify({
- ...block,
- summary: 'oversized '.repeat(4_000),
- estimatedTokens: 1,
- }),
- mimeType: 'application/json',
- source: 'history_compact_block',
- });
-
- const loaded = await loadHistoryCompactBlocksFromArtifacts(store, {
- sessionId: 'session-1',
- maxBlocks: 1,
- maxEstimatedTokens: 2_048,
- });
-
- assert.equal(loaded.blocks.length, 0);
- assert.deepEqual(loaded.skippedReasonCounts, { max_total_tokens: 1 });
- });
-});
-
-function textEvent(id: string, turnId: string, text: string): RuntimeEvent {
- return {
- id,
- sessionId: 'session-1',
- runId: 'run-1',
- turnId,
- invocationId: 'invocation-1',
- ts: 1_800_000_000_000,
- partial: false,
- role: 'user',
- author: 'user',
- content: { kind: 'text', text },
- };
-}
diff --git a/packages/runtime/src/__tests__/history-compact-checkpoint.test.ts b/packages/runtime/src/__tests__/history-compact-checkpoint.test.ts
index 51240d0d0..e36c35017 100644
--- a/packages/runtime/src/__tests__/history-compact-checkpoint.test.ts
+++ b/packages/runtime/src/__tests__/history-compact-checkpoint.test.ts
@@ -37,7 +37,7 @@ import {
loadLatestHistoryCompactCheckpointFromRunLedger,
} from '../history-compact-ledger.js';
import { estimateRuntimeEventsTokens } from '../context-budget.js';
-import { applyRuntimeEventHistoryCompact } from '../history-compact.js';
+import { applyRuntimeEventHistoryCompact } from '../history-compaction.js';
// Satisfies the sectioned summary contract for marked-checkpoint fixtures.
const STRUCTURED_SUMMARY = [
@@ -864,10 +864,7 @@ describe('history compact checkpoint', () => {
events,
{
enabled: true,
- mode: 'read_write',
checkpoint,
- highWaterRatio: 0.01,
- tailEstimatedTokens: 1,
},
1,
1_000,
@@ -906,7 +903,7 @@ describe('history compact checkpoint', () => {
// resurrects the covered raw prefix.
const replay = applyRuntimeEventHistoryCompact(
events,
- { enabled: true, mode: 'read_write', checkpoint },
+ { enabled: true, checkpoint },
4,
1_000_000,
);
@@ -916,8 +913,7 @@ describe('history compact checkpoint', () => {
replay.events.map((event) => event.id),
[`history-compact:${checkpoint.checkpointId}`, 'event-4', 'event-5'],
);
- assert.equal(replay.diagnosticPatch.historyCompactBlocksSelected, 1);
- assert.equal(replay.diagnosticPatch.historyCompactSkippedReasonCounts, undefined);
+ assert.equal(replay.diagnosticPatch.compactionDecisions?.[0]?.decision, 'replaced');
});
test('accepts a complete checkpoint above legacy block limits when the full replay fits', () => {
@@ -941,12 +937,7 @@ describe('history compact checkpoint', () => {
events,
{
enabled: true,
- mode: 'read_write',
checkpoint,
- maxBlockEstimatedTokens: 10_000,
- maxEstimatedTokens: 100,
- highWaterRatio: 0.000001,
- tailEstimatedTokens: 1,
},
1,
10_000,
@@ -981,12 +972,7 @@ describe('history compact checkpoint', () => {
events,
{
enabled: true,
- mode: 'read_write',
checkpoint,
- maxBlockEstimatedTokens: 10_000,
- maxEstimatedTokens: 10_000,
- highWaterRatio: 0.000001,
- tailEstimatedTokens: 1,
},
1,
10_000,
diff --git a/packages/runtime/src/__tests__/history-compact-mid-turn-checkpoint.test.ts b/packages/runtime/src/__tests__/history-compact-mid-turn-checkpoint.test.ts
index d8f05dcbe..bfcae155d 100644
--- a/packages/runtime/src/__tests__/history-compact-mid-turn-checkpoint.test.ts
+++ b/packages/runtime/src/__tests__/history-compact-mid-turn-checkpoint.test.ts
@@ -27,7 +27,7 @@ import {
projectHistoryCompactCheckpointReplay,
validateHistoryCompactCheckpointShape,
} from '../history-compact-checkpoint.js';
-import { applyRuntimeEventHistoryCompact } from '../history-compact.js';
+import { applyRuntimeEventHistoryCompact } from '../history-compaction.js';
describe('mid-turn history compact checkpoint', () => {
test('builds a mid_turn checkpoint that re-renders the covered head anchor verbatim', () => {
@@ -300,7 +300,7 @@ describe('mid-turn history compact checkpoint', () => {
// covered raw span may never be re-injected on recovery.
const replay = applyRuntimeEventHistoryCompact(
events,
- { enabled: true, mode: 'read_write', checkpoint },
+ { enabled: true, checkpoint },
1,
10_000,
);
diff --git a/packages/runtime/src/__tests__/mid-turn-capacity-compact.test.ts b/packages/runtime/src/__tests__/history-compaction.test.ts
similarity index 82%
rename from packages/runtime/src/__tests__/mid-turn-capacity-compact.test.ts
rename to packages/runtime/src/__tests__/history-compaction.test.ts
index f633ca64c..d019a2a91 100644
--- a/packages/runtime/src/__tests__/mid-turn-capacity-compact.test.ts
+++ b/packages/runtime/src/__tests__/history-compaction.test.ts
@@ -24,15 +24,15 @@ import {
estimateNextRequestTokens,
exceedsContextWindow,
exceedsHighWater,
- planMidTurnCapacityCompaction,
- selectMidTurnSafeBoundary,
- type PlanMidTurnCapacityCompactionInput,
-} from '../mid-turn-capacity-compact.js';
+ applyRuntimeEventHistoryCompact,
+ planHistoryCompaction,
+ selectSafeCompactionPrefix,
+ type PlanHistoryCompactionInput,
+} from '../history-compaction.js';
import { HistoryCompactSummarizerError } from '../history-compact-summarizer.js';
-import { applyRuntimeEventHistoryCompact } from '../history-compact.js';
import { matchHistoryCompactCheckpointPrefix } from '../history-compact-checkpoint.js';
-describe('mid-turn capacity trigger measurement', () => {
+describe('context compaction trigger measurement', () => {
test('anchors on real provider usage plus a tail char/4 delta', () => {
// last step: 100 input + 40 output real tokens, then 400 chars of new tool results
assert.equal(
@@ -72,7 +72,7 @@ describe('mid-turn capacity trigger measurement', () => {
});
});
-describe('mid-turn safe boundary selection', () => {
+describe('safe compaction prefix selection', () => {
test('folds the largest immutable non-partial prefix, leaving the reserved tail', () => {
const events = [
user('anchor', 'turn-1'),
@@ -80,7 +80,7 @@ describe('mid-turn safe boundary selection', () => {
model('m2', 'turn-1'),
model('m3', 'turn-1'),
];
- const boundary = selectMidTurnSafeBoundary(events, { reserveTailEvents: 1 });
+ const boundary = selectSafeCompactionPrefix(events, { reserveTailEvents: 1 });
assert.deepEqual(boundary, { ok: true, coveredCount: 3 });
});
@@ -91,7 +91,7 @@ describe('mid-turn safe boundary selection', () => {
{ ...model('m2-partial', 'turn-1'), partial: true },
];
// Reserving 0 tail would cut after the partial; it must retreat to m1.
- const boundary = selectMidTurnSafeBoundary(events, { reserveTailEvents: 0 });
+ const boundary = selectSafeCompactionPrefix(events, { reserveTailEvents: 0 });
assert.deepEqual(boundary, { ok: true, coveredCount: 2 });
});
@@ -106,7 +106,7 @@ describe('mid-turn safe boundary selection', () => {
// reserveTail=2 would cut at index 3, between call-2 and its result → retreat to 3? No:
// index 3 straddles call-2(3)/result-2(4)? call at 3 >= 3, result at 4 >= 3, both outside → safe.
// Force a straddle: reserveTail=1 → maxCut=4 straddles nothing (call-2 at 3<4, result-2 at 4>=4) → straddle, retreat to 3.
- const boundary = selectMidTurnSafeBoundary(events, { reserveTailEvents: 1 });
+ const boundary = selectSafeCompactionPrefix(events, { reserveTailEvents: 1 });
assert.deepEqual(boundary, { ok: true, coveredCount: 3 });
});
@@ -119,7 +119,7 @@ describe('mid-turn safe boundary selection', () => {
// With no reserved tail the largest cut ends on the immutable m-final, but
// the prefix would still span the partial snapshot — coverage must stop
// strictly before the first partial.
- const boundary = selectMidTurnSafeBoundary(events, { reserveTailEvents: 0 });
+ const boundary = selectSafeCompactionPrefix(events, { reserveTailEvents: 0 });
assert.deepEqual(boundary, { ok: true, coveredCount: 1 });
});
@@ -131,19 +131,19 @@ describe('mid-turn safe boundary selection', () => {
];
// Even with no reserved tail, covering the open call would orphan the
// response that lands after compaction — the cut must stop before it.
- const boundary = selectMidTurnSafeBoundary(events, { reserveTailEvents: 0 });
+ const boundary = selectSafeCompactionPrefix(events, { reserveTailEvents: 0 });
assert.deepEqual(boundary, { ok: true, coveredCount: 2 });
});
test('reports no safe completed span when the whole pool is one atomic pair', () => {
const events = [call('c1', 'call-1', 'turn-1'), result('r1', 'call-1', 'turn-1')];
// Reserving 1 tail forces maxCut=1, which straddles the only pair → no safe span.
- const boundary = selectMidTurnSafeBoundary(events, { reserveTailEvents: 1 });
+ const boundary = selectSafeCompactionPrefix(events, { reserveTailEvents: 1 });
assert.deepEqual(boundary, { ok: false, reason: 'no_safe_completed_span' });
});
});
-describe('plan mid-turn capacity compaction', () => {
+describe('plan context compaction', () => {
// A long turn: two prior turns folded already conceptually, plus the current
// turn's head anchor and several completed steps.
function longTurnEvents(): RuntimeEvent[] {
@@ -162,16 +162,11 @@ describe('plan mid-turn capacity compaction', () => {
function structuredSummary(body: string): string {
return `## Goal\n${body}\n\n## Progress\n- done\n\n## Next Steps\n1. continue\n\n## Critical Context\n- (none)`;
}
- function planInput(
- over: Partial<PlanMidTurnCapacityCompactionInput> = {},
- ): PlanMidTurnCapacityCompactionInput {
+ function planInput(over: Partial<PlanHistoryCompactionInput> = {}): PlanHistoryCompactionInput {
return {
sessionId: 'session-1',
orderedEvents: longTurnEvents(),
headAnchor: { runtimeEventId: 'anchor', turnId: 'turn-1' },
- estimatedNextRequestTokens: 120_000,
- contextWindow: 128_000,
- reserveTokens: 16_384,
reserveTailEvents: 1,
charsPerToken: 4,
now: 1_800_000_010_000,
@@ -180,15 +175,13 @@ describe('plan mid-turn capacity compaction', () => {
};
}
- test('skips below the high-water threshold', async () => {
- const result = await planMidTurnCapacityCompaction(
- planInput({ estimatedNextRequestTokens: 100_000 }),
- );
- assert.deepEqual(result, { decision: 'skip', reason: 'below_high_water' });
+ test('compacts whenever the caller has emitted a compaction command', async () => {
+ const result = await planHistoryCompaction(planInput());
+ assert.equal(result.decision, 'compacted');
});
test('compacts a safe prefix, keeps the head anchor verbatim, and continues with the tail', async () => {
- const result = await planMidTurnCapacityCompaction(planInput());
+ const result = await planHistoryCompaction(planInput());
assert.equal(result.decision, 'compacted');
if (result.decision !== 'compacted') return;
assert.equal(result.checkpoint.phase, 'mid_turn');
@@ -208,7 +201,7 @@ describe('plan mid-turn capacity compaction', () => {
test('step-0 recovery folds prior history without covering the current user anchor', async () => {
const events = longTurnEvents();
- const result = await planMidTurnCapacityCompaction(
+ const result = await planHistoryCompaction(
planInput({ phase: 'pre_turn', orderedEvents: events }),
);
assert.equal(result.decision, 'compacted');
@@ -226,9 +219,44 @@ describe('plan mid-turn capacity compaction', () => {
assert.deepEqual(result.replacementEvents[1], events[2]);
});
+ test('backs the safe prefix down locally when the full summary input does not fit', async () => {
+ const events = [
+ user('old-user', 'old-turn'),
+ model('old-model', 'old-turn', 'old result'),
+ user('recent-user', 'recent-turn'),
+ model('recent-model', 'recent-turn', 'recent result'),
+ ];
+ const attemptedCoverage: string[][] = [];
+ const result = await planHistoryCompaction(
+ planInput({
+ phase: 'standalone',
+ orderedEvents: events,
+ reserveTailEvents: 0,
+ summarize: ({ coveredRuntimeEvents }) => {
+ attemptedCoverage.push(coveredRuntimeEvents.map((event) => event.id));
+ if (coveredRuntimeEvents.length === events.length) {
+ throw new HistoryCompactSummarizerError('input_too_large');
+ }
+ return structuredSummary('A bounded automatic summary.');
+ },
+ }),
+ );
+
+ assert.equal(result.decision, 'compacted');
+ if (result.decision !== 'compacted') return;
+ assert.deepEqual(attemptedCoverage, [
+ ['old-user', 'old-model', 'recent-user', 'recent-model'],
+ ['old-user', 'old-model', 'recent-user'],
+ ]);
+ assert.deepEqual(
+ result.tailRuntimeEvents.map((event) => event.id),
+ ['recent-model'],
+ );
+ });
+
test('persisted checkpoint replay-validates against the same ledger prefix (recovery)', async () => {
const events = longTurnEvents();
- const result = await planMidTurnCapacityCompaction(planInput({ orderedEvents: events }));
+ const result = await planHistoryCompaction(planInput({ orderedEvents: events }));
assert.equal(result.decision, 'compacted');
if (result.decision !== 'compacted') return;
@@ -243,7 +271,7 @@ describe('plan mid-turn capacity compaction', () => {
// re-injects the replaced raw span.
const replay = applyRuntimeEventHistoryCompact(
events,
- { enabled: true, mode: 'read_write', checkpoint: result.checkpoint },
+ { enabled: true, checkpoint: result.checkpoint },
4,
1_000_000,
);
@@ -254,10 +282,9 @@ describe('plan mid-turn capacity compaction', () => {
assert.equal(replayIds.includes('call-a'), false);
});
- test('fails open below the window when the summarizer fails', async () => {
- const result = await planMidTurnCapacityCompaction(
+ test('fails open when the summarizer throws', async () => {
+ const result = await planHistoryCompaction(
planInput({
- estimatedNextRequestTokens: 120_000, // over high-water, under window
summarize: () => {
throw new Error('summarizer down');
},
@@ -267,7 +294,7 @@ describe('plan mid-turn capacity compaction', () => {
});
test('preserves a typed summarizer failure as the mid-turn diagnostic reason', async () => {
- const result = await planMidTurnCapacityCompaction(
+ const result = await planHistoryCompaction(
planInput({
summarize: () => {
throw new HistoryCompactSummarizerError('provider_error');
@@ -281,12 +308,9 @@ describe('plan mid-turn capacity compaction', () => {
});
});
- test('fails open (never terminates) above the window when the summarizer fails', async () => {
- // The engine is a pure shaper: the over-window pass/terminate verdict is
- // issued by the backend's final-request estimate owner, never here.
- const result = await planMidTurnCapacityCompaction(
+ test('fails open when the summarizer returns no checkpoint', async () => {
+ const result = await planHistoryCompaction(
planInput({
- estimatedNextRequestTokens: 130_000, // over the window itself
summarize: () => '',
}),
);
@@ -296,7 +320,7 @@ describe('plan mid-turn capacity compaction', () => {
test('the write gate rejects a malformed summary from any producer', async () => {
// The default summarizer validates its own completions; the write gate
// enforces the same invariant for any other producer (#3029).
- const result = await planMidTurnCapacityCompaction(
+ const result = await planHistoryCompaction(
planInput({ summarize: () => '这不是结构化摘要,而是一段自由文本。' }),
);
assert.deepEqual(result, {
@@ -314,17 +338,16 @@ describe('plan mid-turn capacity compaction', () => {
call('c', 'c1', 'turn-1'),
result('r', 'c1', 'turn-1'),
];
- const outcome = await planMidTurnCapacityCompaction(
+ const outcome = await planHistoryCompaction(
planInput({
orderedEvents: events,
- estimatedNextRequestTokens: 130_000,
reserveTailEvents: 1,
}),
);
assert.deepEqual(outcome, { decision: 'fail_open', reason: 'no_safe_completed_span' });
});
- test('still shapes when the estimate exceeds the window — no post-fold window verdict here', async () => {
+ test('does not issue a post-fold window verdict', async () => {
// Review round-3 finding A: a post-fold re-estimate that subtracts the
// RAW covered span is wrong on a rolling (second) compaction — the
// previous request was already `[block, anchor, tail]` and never carried
@@ -332,13 +355,7 @@ describe('plan mid-turn capacity compaction', () => {
// still-over-window request. The engine therefore makes NO window claim
// after folding: it returns the shape and the backend owner re-measures
// the actual replacement payload.
- const outcome = await planMidTurnCapacityCompaction(
- planInput({
- estimatedNextRequestTokens: 10_000,
- contextWindow: 1_000,
- reserveTokens: 100,
- }),
- );
+ const outcome = await planHistoryCompaction(planInput());
assert.equal(outcome.decision, 'compacted');
});
@@ -349,7 +366,7 @@ describe('plan mid-turn capacity compaction', () => {
test('rolls forward from a matching previous checkpoint (only the new span is summarized)', async () => {
const events = longTurnEvents();
- const first = await planMidTurnCapacityCompaction(
+ const first = await planHistoryCompaction(
planInput({
orderedEvents: events.slice(0, 5), // fold through res-a
}),
@@ -358,7 +375,7 @@ describe('plan mid-turn capacity compaction', () => {
if (first.decision !== 'compacted') return;
let seenNewlyFolded: string[] = [];
- const second = await planMidTurnCapacityCompaction(
+ const second = await planHistoryCompaction(
planInput({
orderedEvents: events,
previousCheckpoint: first.checkpoint,
diff --git a/packages/runtime/src/__tests__/memory-artifact-store.ts b/packages/runtime/src/__tests__/memory-artifact-store.ts
deleted file mode 100644
index 790da3316..000000000
--- a/packages/runtime/src/__tests__/memory-artifact-store.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { Buffer } from 'node:buffer';
-import type { ArtifactRecord } from '@maka/core/artifacts';
-import type { HistoryCompactArtifactStore } from '../history-compact-artifacts.js';
-
-// Shared in-memory HistoryCompactArtifactStore for tests. Not a test file
-// itself (no .test. suffix), so the runner glob skips it.
-export function memoryArtifactStore(): HistoryCompactArtifactStore {
- const records = new Map<string, { record: ArtifactRecord; content: string }>();
- return {
- async create(input) {
- const id = input.id ?? `artifact-${records.size + 1}`;
- const record: ArtifactRecord = {
- id,
- sessionId: input.sessionId,
- turnId: input.turnId,
- createdAt: input.now ?? 0,
- name: input.name,
- kind: input.kind,
- relativePath: input.name,
- sizeBytes: Buffer.byteLength(input.content, 'utf8'),
- mimeType: input.mimeType,
- source: input.source,
- summary: input.summary,
- status: 'live',
- };
- records.set(id, { record, content: input.content });
- return record;
- },
- async delete(artifactId) {
- const entry = records.get(artifactId);
- if (entry) entry.record.status = 'deleted';
- },
- async purge(artifactIds) {
- for (const artifactId of artifactIds) records.delete(artifactId);
- },
- async list() {
- return [...records.values()].map((entry) => entry.record);
- },
- async readText(artifactId) {
- const entry = records.get(artifactId);
- if (!entry) return { ok: false, reason: 'not_found' };
- return { ok: true, text: entry.content };
- },
- };
-}
diff --git a/packages/runtime/src/__tests__/mid-turn-capacity-backend.test.ts b/packages/runtime/src/__tests__/mid-turn-capacity-backend.test.ts
index 3cc01c47b..e909c9b4b 100644
--- a/packages/runtime/src/__tests__/mid-turn-capacity-backend.test.ts
+++ b/packages/runtime/src/__tests__/mid-turn-capacity-backend.test.ts
@@ -38,7 +38,7 @@ import {
} from '../ai-sdk-flow.js';
import type { InvocationContext } from '../invocation-context.js';
import { applyRuntimeEventContextBudget } from '../context-budget.js';
-import { evaluateHistoryCompactCheckpointReplay } from '../history-compact.js';
+import { evaluateHistoryCompactCheckpointReplay } from '../history-compaction.js';
import type {
HistoryCompactCheckpoint,
HistoryCompactProviderState,
@@ -111,7 +111,6 @@ interface MidTurnFixtureOptions {
useRuntimeDefaultPolicy?: boolean;
/** Exercise the supported explicit history-compaction escape hatch. */
historyCompactOff?: boolean;
- historyBudgetTokens?: number;
reserveTokens?: number;
summarize?: (
input: HistoryCompactSummaryInput,
@@ -127,8 +126,6 @@ interface MidTurnFixtureOptions {
withoutPriorTurns?: boolean;
/** Enable the default-on active tool-result prune with a tiny threshold. */
activeToolResultPrune?: boolean;
- /** Enable semantic compaction so it competes with the capacity hook. */
- semanticCompact?: boolean;
/**
* Summarize through the real `buildLlmHistorySummarizer` against a mock
* provider, so the compaction settles a canonical record instead of the
@@ -502,34 +499,19 @@ function buildFixture(options: MidTurnFixtureOptions = {}): MidTurnFixture {
}
: {}),
...(options.historyCompactOff ? { MAKA_CONTEXT_HISTORY_COMPACT: 'off' } : {}),
- ...(options.historyBudgetTokens !== undefined
- ? { MAKA_CONTEXT_HISTORY_BUDGET_TOKENS: String(options.historyBudgetTokens) }
- : {}),
},
},
)
: {
name: 'mid-turn-test',
maxHistoryEstimatedTokens: 100_000,
- minRecentTurns: 1,
historyCompact: {
enabled: true,
- mode: 'read_write',
midTurn: { enabled: true, reserveTokens },
},
...(options.activeToolResultPrune
? { activeToolResultPrune: { enabled: true, maxCurrentResultEstimatedTokens: 30 } }
: {}),
- ...(options.semanticCompact
- ? {
- semanticCompact: {
- enabled: true,
- mode: 'replace' as const,
- minStepNumber: 2,
- maxActiveEstimatedTokens: 1,
- },
- }
- : {}),
},
...(options.activeToolResultPrune
? {
@@ -843,8 +825,7 @@ function defineMidTurnSuite(consumer: ConsumerMode): void {
// never re-injected, even though the raw history is below the high water.
const replay = applyRuntimeEventContextBudget([...fixture.priorEvents, ...fixture.ledger], {
maxHistoryEstimatedTokens: 100_000,
- minRecentTurns: 1,
- historyCompact: { enabled: true, mode: 'read_write', checkpoint },
+ historyCompact: { enabled: true, checkpoint },
});
assert.ok(replay);
@@ -956,12 +937,6 @@ function defineMidTurnSuite(consumer: ConsumerMode): void {
(decision) => decision.phase === 'mid_turn' && decision.decision === 'failedOpen',
);
assert.equal(failedOpen?.failOpenReason, 'write_failed');
- // The recorder WAS invoked and failed: exactly that is what the counters say.
- const usageEvent = fixture.events.find((event) => event.type === 'token_usage') as
- | { contextBudget?: ContextBudgetDiagnostic }
- | undefined;
- assert.equal(usageEvent?.contextBudget?.historyCompactWritesAttempted, 1);
- assert.equal(usageEvent?.contextBudget?.historyCompactWriteFailures, 1);
});
test('a malformed summarizer completion fails open end-to-end with its granular reason', async () => {
@@ -1019,8 +994,6 @@ function defineMidTurnSuite(consumer: ConsumerMode): void {
(decision) => decision.phase === 'mid_turn' && decision.reason === 'context_budget_exhausted',
);
assert.equal(exhaustedDecision?.skippedReasonCounts?.write_failed, 1);
- assert.equal(lastCall?.contextBudget?.historyCompactWritesAttempted, 1);
- assert.equal(lastCall?.contextBudget?.historyCompactWriteFailures, 1);
});
test('fails closed before provider dispatch when the durable ledger read fails', async () => {
@@ -1062,29 +1035,6 @@ function defineMidTurnSuite(consumer: ConsumerMode): void {
assert.match(thirdPrompt, /active_current_turn_tool_result_pruned_before_next_step/);
});
- test('semantic compaction yields on the step the capacity hook replaced', async () => {
- const fixture = buildFixture({ semanticCompact: true });
- await runFixtureTurn(fixture, consumer);
-
- // The capacity projection won the replaced step.
- assert.equal(fixture.model.doStreamCalls.length, 3);
- assert.equal(fixture.recorded.length, 1);
- assert.match(promptJson(fixture, 2), /maka_history_compact_checkpoint/);
-
- // Deterministic priority: semantic compaction was skipped for that step
- // with an explicit decision — one step never runs two summarizers.
- const yielded = compactionDecisions(fixture).find(
- (decision) => decision.reason === 'mid_turn_capacity_precedence',
- );
- assert.equal(yielded?.decision, 'unchanged');
- assert.equal(fixture.summarizerCalls, 1);
- // No semantic summary model call was ever made.
- assert.equal(
- fixture.events.some((event) => event.type === 'error'),
- false,
- );
- });
-
test('a rolling second compaction that still exceeds the window ends explicitly (review finding A)', async () => {
// Review round-3 finding A: the old post-fold re-estimate subtracted the
// RAW covered span from a usage estimate anchored to the ALREADY-compacted
@@ -1247,11 +1197,6 @@ function defineMidTurnSuite(consumer: ConsumerMode): void {
// before any high-water check, so a persisted runaway block would poison
// every later projection even though this step correctly refused it.
assert.equal(fixture.recorded.length, 0);
- // The recorder was never reached, so the diagnostics claim no write.
- const usageEvent = fixture.events.find((event) => event.type === 'token_usage') as
- | { contextBudget?: ContextBudgetDiagnostic }
- | undefined;
- assert.equal(usageEvent?.contextBudget?.historyCompactWritesAttempted, undefined);
});
test("the usage baseline is the last request's INPUT tokens — output is not double-counted (review finding 1)", async () => {
@@ -1683,12 +1628,11 @@ describe('mid-turn capacity default-on safety guards (issue #882 PR 3)', () => {
assert.equal(complete.contextBudgetExhaustedDetail, 'no_safe_completed_span');
});
- test('keeps multiple bounded recent turns when only their aggregate exceeds the history budget', async () => {
+ test('keeps multiple bounded recent turns below the model capacity', async () => {
const fixture = buildFixture({
useRuntimeDefaultPolicy: true,
contextWindow: 100_000,
historyCompactOff: true,
- historyBudgetTokens: 32_000,
priorChars: 40_000,
});
fixture.priorEvents.push(
diff --git a/packages/runtime/src/__tests__/overflow-reactive-recovery.test.ts b/packages/runtime/src/__tests__/overflow-reactive-recovery.test.ts
index 4a00448cc..6b4e9e401 100644
--- a/packages/runtime/src/__tests__/overflow-reactive-recovery.test.ts
+++ b/packages/runtime/src/__tests__/overflow-reactive-recovery.test.ts
@@ -602,10 +602,8 @@ function buildReactiveFixture(options: ReactiveFixtureOptions): ReactiveFixture
contextBudget: {
name: 'reactive-test',
maxHistoryEstimatedTokens: 100_000,
- minRecentTurns: 1,
historyCompact: {
enabled: true,
- mode: 'read_write',
...(midTurnEnabled ? { midTurn: { enabled: true, reserveTokens } } : {}),
},
...(options.activeToolResultPrune
diff --git a/packages/runtime/src/__tests__/runtime-event-read-model.test.ts b/packages/runtime/src/__tests__/runtime-event-read-model.test.ts
index 419a4486f..ca7f35a79 100644
--- a/packages/runtime/src/__tests__/runtime-event-read-model.test.ts
+++ b/packages/runtime/src/__tests__/runtime-event-read-model.test.ts
@@ -17,6 +17,7 @@
* under the License.
*/
+import assert from 'node:assert/strict';
import { describe, test } from 'node:test';
import type { AgentRunHeader } from '@maka/core/agent-run';
import type { CreateSessionInput, SessionListFilter } from '@maka/core/runtime-inputs';
@@ -742,6 +743,36 @@ describe('projectRuntimeEventsToStoredMessages', () => {
expect(out.diagnostics.map((diag) => diag.code)).toEqual(['archived_tool_result_placeholder']);
});
+ test('legacy archived tool-result placeholders gain a deterministic ArchiveRead ref for replay', () => {
+ const events = baseEvents();
+ const toolResult = events.find((event) => event.id === 'evt-tool-result');
+ if (toolResult?.content?.kind !== 'function_response')
+ throw new Error('fixture missing tool result');
+ toolResult.content.result = {
+ kind: 'maka.archived_tool_result',
+ rewriteVersion: 1,
+ artifactId: 'artifact-tool-result',
+ runtimeEventId: 'evt-tool-result',
+ toolCallId: 'tool-1',
+ toolName: 'Read',
+ bodySha256: 'a'.repeat(64),
+ originalEstimatedTokens: 200,
+ originalBytes: 800,
+ reason: 'stale_tool_result_pruned_before_compact',
+ };
+
+ const replay = buildRuntimeEventModelReplayPlan(events);
+ const result = replay.items.find(
+ (item) => item.kind === 'tool_result' && item.toolCallId === 'tool-1',
+ );
+ assert.equal(
+ result?.kind === 'tool_result' && typeof result.output === 'object' && result.output !== null
+ ? (result.output as { resourceRef?: string }).resourceRef
+ : undefined,
+ `maka://archive/artifact-tool-result/${'a'.repeat(64)}/800`,
+ );
+ });
+
test('archive status wrapper can project missing and corrupt rows without changing sync defaults', () => {
const events = baseEvents();
const toolResult = events.find((event) => event.id === 'evt-tool-result');
diff --git a/packages/runtime/src/__tests__/semantic-compact.test.ts b/packages/runtime/src/__tests__/semantic-compact.test.ts
deleted file mode 100644
index ad1fdfebc..000000000
--- a/packages/runtime/src/__tests__/semantic-compact.test.ts
+++ /dev/null
@@ -1,1158 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import assert from 'node:assert/strict';
-import { describe, test } from 'node:test';
-import type { ModelMessage } from '../model-protocol.js';
-import { buildActiveCompactionHeadAnchor } from '../active-compaction-kernel.js';
-
-import {
- renderSemanticCompactBlock,
- rewriteSemanticCompactInMessages,
- semanticCompactBlockToCompactionBoundary,
- type SemanticCompactSummaryRequest,
-} from '../semantic-compact.js';
-
-describe('semantic compact', () => {
- test('replaces older completed episodes while preserving the newest completed episode', async () => {
- let requestSeen: SemanticCompactSummaryRequest | undefined;
- const messages = semanticFixtureMessages();
-
- const result = await rewriteSemanticCompactInMessages({
- sessionId: 'session-1',
- turnId: 'turn-1',
- messages,
- stepNumber: 2,
- charsPerToken: 1,
- policy: {
- enabled: true,
- maxActiveEstimatedTokens: 1,
- highWaterRatio: 0.1,
- minSafePrefixEstimatedTokens: 1,
- maxAcceptedProjectionEstimatedTokens: 2048,
- minSavingsTokens: 1,
- minSavingsRatio: 0,
- },
- requestShapeHashForMessages: (stepMessages) =>
- `shape:${stepMessages.length}:${JSON.stringify(stepMessages).length}`,
- summarizer: (request) => {
- requestSeen = request;
- assert.equal('tools' in request, false);
- assert.equal('toolChoice' in request, false);
- assert.match(JSON.stringify(request.messages), /OLD_BUILD_LOG/);
- assert.doesNotMatch(JSON.stringify(request.messages), /recent-result/);
- assert.match(JSON.stringify(request.messages), /Return ONLY a valid JSON object/);
- assert.doesNotMatch(JSON.stringify(request.messages), /source_manifest/);
- assert.doesNotMatch(JSON.stringify(request.messages), /restoration_cards/);
- return {
- text: semanticSummary({
- finding: 'Solve the task while keeping the service running.',
- actionInProgress: 'Continue from the preserved recent tool result.',
- partialWorkProduct: ['Earlier output showed a large build log.'],
- }),
- usage: {
- inputTokens: 10,
- outputTokens: 12,
- cacheHitInputTokens: 0,
- cacheMissInputTokens: 10,
- cacheMissInputSource: 'explicit',
- cacheWriteInputTokens: 0,
- reasoningTokens: 0,
- totalTokens: 22,
- cachedInputTokens: 0,
- },
- finishReason: 'stop',
- };
- },
- });
-
- assert.equal(result.decision, 'replaced');
- assert.ok(requestSeen, 'expected injected summarizer to be called');
- assert.equal(requestSeen.maxOutputTokens, 4096);
- assert.equal(result.block?.kind, 'maka.semantic_compact_block');
- assert.equal(result.block?.blockId, 'semcompact-311e98c7e686a4ac360adc140a644165');
- assert.equal(
- result.block?.cumulativeCoverageDigest,
- 'edc54105a90e20f39f8f7c184c7162f4f187da295076393399a73d2b78ae2d2a',
- );
- assert.equal(result.block?.stateCards, undefined);
- assert.equal(result.block?.acceptance.decision, 'accepted');
- assert.ok((result.block?.estimatedTokensSavedSigned ?? 0) > 0);
- assert.deepEqual(result.block?.preservedTail.toolCallIds, ['tool-recent']);
- assert.equal(
- result.messages.some(
- (message) =>
- message.role === 'assistant' &&
- JSON.stringify(message.content).includes('maka_semantic_compact_block'),
- ),
- true,
- );
- assert.deepEqual(
- result.messages[0],
- messages[0],
- 'the exact current-user head anchor must stay first',
- );
- assert.equal(result.messages.filter((message) => message.role === 'user').length, 1);
- assert.equal(
- result.messages.some(
- (message) =>
- message.role === 'tool' && JSON.stringify(message.content).includes('recent-result'),
- ),
- true,
- );
-
- const decisions = result.diagnosticPatch.compactionDecisions ?? [];
- assert.equal(decisions[0]?.boundaryKind, 'semanticCompact');
- assert.equal(decisions[0]?.decision, 'replaced');
- assert.equal(decisions[0]?.compactCallInputTokens, 10);
- assert.equal(decisions[0]?.compactCallOutputTokens, 12);
- assert.equal(decisions[0]?.compactCallTotalTokens, 22);
- assert.equal(typeof result.diagnosticPatch.highWaterRequestShapeHashBefore, 'string');
- assert.equal(typeof result.diagnosticPatch.highWaterRequestShapeHashAfter, 'string');
- });
-
- test('accepts with a warning when signed savings do not meet the configured margin', async () => {
- const messages = semanticFixtureMessages();
- const result = await rewriteSemanticCompactInMessages({
- sessionId: 'session-1',
- turnId: 'turn-1',
- messages,
- stepNumber: 2,
- charsPerToken: 1,
- policy: {
- enabled: true,
- maxActiveEstimatedTokens: 1,
- highWaterRatio: 0.1,
- minSafePrefixEstimatedTokens: 1,
- maxAcceptedProjectionEstimatedTokens: 2048,
- minSavingsTokens: 50_000,
- minSavingsRatio: 0,
- },
- summarizer: () => ({
- text: semanticSummary({
- finding: 'Solve the task.',
- actionInProgress: 'Continue from preserved context.',
- }),
- usage: {
- inputTokens: 5,
- outputTokens: 6,
- cacheHitInputTokens: 0,
- cacheMissInputTokens: 5,
- cacheMissInputSource: 'explicit',
- cacheWriteInputTokens: 0,
- reasoningTokens: 0,
- totalTokens: 11,
- cachedInputTokens: 0,
- },
- }),
- });
-
- assert.equal(result.decision, 'replaced');
- assert.equal(result.reason, 'below_min_savings_tokens');
- assert.notDeepEqual(result.messages, messages);
- assert.equal(result.block?.acceptance.decision, 'accepted');
- assert.equal(result.block?.acceptance.reason, 'below_min_savings_tokens');
- const decision = result.diagnosticPatch.compactionDecisions?.[0];
- assert.equal(decision?.boundaryKind, 'semanticCompact');
- assert.equal(decision?.decision, 'replaced');
- assert.equal(decision?.reason, 'below_min_savings_tokens');
- assert.deepEqual(decision?.skippedReasonCounts, { below_min_savings_tokens: 1 });
- assert.equal(typeof decision?.estimatedTokensSaved, 'number');
- assert.equal(decision?.compactCallInputTokens, 5);
- assert.equal(decision?.compactCallTotalTokens, 11);
- });
-
- test('rejects summaries that newly surface private verifier material', async () => {
- const result = await rewriteSemanticCompactInMessages({
- sessionId: 'session-1',
- turnId: 'turn-1',
- messages: semanticFixtureMessages(),
- stepNumber: 2,
- charsPerToken: 1,
- policy: {
- enabled: true,
- maxActiveEstimatedTokens: 1,
- highWaterRatio: 0.1,
- minSafePrefixEstimatedTokens: 1,
- maxAcceptedProjectionEstimatedTokens: 2048,
- minSavingsTokens: 1,
- minSavingsRatio: 0,
- },
- summarizer: () => ({
- text: semanticSummary({
- finding: 'The hidden verifier says this will pass.',
- actionInProgress: 'Continue.',
- }),
- usage: {
- inputTokens: 3,
- outputTokens: 4,
- cacheHitInputTokens: 0,
- cacheMissInputTokens: 3,
- cacheMissInputSource: 'explicit',
- cacheWriteInputTokens: 0,
- reasoningTokens: 0,
- totalTokens: 7,
- cachedInputTokens: 0,
- },
- }),
- });
-
- assert.equal(result.decision, 'unchanged');
- assert.equal(result.reason, 'private_verifier_surface');
- assert.equal(
- result.diagnosticPatch.compactionDecisions?.[0]?.reason,
- 'private_verifier_surface',
- );
- assert.equal(result.block, undefined);
- assert.equal(result.diagnosticPatch.compactionDecisions?.[0]?.compactCallTotalTokens, 7);
- });
-
- test('falls back to bounded complete summary text when JSON does not satisfy the schema contract', async () => {
- const baseInput = {
- sessionId: 'session-1',
- turnId: 'turn-1',
- messages: semanticFixtureMessages(),
- stepNumber: 2,
- charsPerToken: 1,
- policy: {
- enabled: true,
- maxActiveEstimatedTokens: 1,
- highWaterRatio: 0.1,
- minSafePrefixEstimatedTokens: 1,
- maxAcceptedProjectionEstimatedTokens: 2048,
- minSavingsTokens: 1,
- minSavingsRatio: 0,
- },
- } as const;
-
- const missingAction = await rewriteSemanticCompactInMessages({
- ...baseInput,
- summarizer: () => ({ text: JSON.stringify({ established_findings: ['Build configured.'] }) }),
- });
- assert.equal(missingAction.decision, 'replaced');
- assert.equal(missingAction.block?.acceptance.decision, 'accepted');
- assert.equal(missingAction.block?.acceptance.reason, 'summary_missing_action_in_progress');
- assert.equal(
- missingAction.diagnosticPatch.compactionDecisions?.[0]?.reason,
- 'summary_missing_action_in_progress',
- );
-
- const emptyAction = await rewriteSemanticCompactInMessages({
- ...baseInput,
- summarizer: () => ({ text: JSON.stringify({ action_in_progress: '' }) }),
- });
- assert.equal(emptyAction.decision, 'replaced');
- assert.equal(emptyAction.block?.acceptance.decision, 'accepted');
- assert.equal(emptyAction.block?.acceptance.reason, 'summary_missing_action_in_progress');
- assert.equal(
- emptyAction.diagnosticPatch.compactionDecisions?.[0]?.reason,
- 'summary_missing_action_in_progress',
- );
- assert.match(
- renderSemanticCompactBlock(emptyAction.block!),
- /bounded text fallback|continuation_notes/,
- );
- });
-
- test('rejects an unbounded non-structured fallback without a complete sentence or line', async () => {
- const messages = semanticFixtureMessages();
- const result = await rewriteSemanticCompactInMessages({
- sessionId: 'session-fallback-boundary',
- turnId: 'turn-fallback-boundary',
- messages,
- headAnchor: buildActiveCompactionHeadAnchor(messages, 0, 1),
- stepNumber: 2,
- charsPerToken: 1,
- policy: { ...attentionTestPolicy(), maxAcceptedProjectionEstimatedTokens: 128 },
- summarizer: () => ({ text: 'unbounded '.repeat(2_000) }),
- });
-
- assert.equal(result.decision, 'unchanged');
- assert.equal(result.reason, 'fallback_projection_empty');
- assert.deepEqual(result.messages, messages);
- });
-
- test('does not reject valid summaries just because they exceed the soft summary target', async () => {
- const result = await rewriteSemanticCompactInMessages({
- sessionId: 'session-1',
- turnId: 'turn-1',
- messages: semanticFixtureMessages(),
- stepNumber: 2,
- charsPerToken: 1,
- policy: {
- enabled: true,
- maxActiveEstimatedTokens: 1,
- highWaterRatio: 0.1,
- minSafePrefixEstimatedTokens: 1,
- maxSummaryEstimatedTokens: 1,
- maxAcceptedProjectionEstimatedTokens: 2048,
- minSavingsTokens: 1,
- minSavingsRatio: 0,
- },
- summarizer: () => ({
- text: semanticSummary({
- finding: 'Continue after compact with complete continuity state.',
- actionInProgress: 'Resume with the preserved recent tool result.',
- partialWorkProduct: [
- 'Earlier output showed a large build log that does not need to remain verbatim.',
- ],
- }),
- }),
- });
-
- assert.equal(result.decision, 'replaced');
- assert.equal(result.block?.acceptance.decision, 'accepted');
- assert.notEqual(result.reason, 'summary_too_large');
- });
-
- test('accepts compact with a warning when provider savings do not beat compact-call token cost', async () => {
- const result = await rewriteSemanticCompactInMessages({
- sessionId: 'session-1',
- turnId: 'turn-1',
- messages: semanticFixtureMessages(),
- stepNumber: 2,
- charsPerToken: 1,
- policy: {
- enabled: true,
- maxActiveEstimatedTokens: 1,
- highWaterRatio: 0.1,
- minSafePrefixEstimatedTokens: 1,
- maxAcceptedProjectionEstimatedTokens: 2048,
- minSavingsTokens: 1,
- minNetSavingsTokens: 1,
- compactCallTokenCostWeight: 1,
- minSavingsRatio: 0,
- },
- summarizer: () => ({
- text: semanticSummary({
- finding: 'Continue after compact.',
- actionInProgress: 'Resume with preserved tail.',
- }),
- usage: {
- inputTokens: 999_999,
- outputTokens: 1,
- cacheHitInputTokens: 0,
- cacheMissInputTokens: 999_999,
- cacheMissInputSource: 'explicit',
- cacheWriteInputTokens: 0,
- reasoningTokens: 0,
- totalTokens: 1_000_000,
- cachedInputTokens: 0,
- },
- }),
- });
-
- assert.equal(result.decision, 'replaced');
- assert.equal(result.block?.acceptance.decision, 'accepted');
- assert.equal(result.block?.acceptance.reason, 'below_min_net_savings_tokens');
- assert.equal(
- result.diagnosticPatch.compactionDecisions?.[0]?.reason,
- 'below_min_net_savings_tokens',
- );
- assert.ok((result.block?.estimatedNetTokensSavedSigned ?? 0) < 0);
- });
-
- test('does not brake semantic compact calls after malformed summary fallbacks', async () => {
- const controllerState = {
- consecutiveInvalidSummaries: 0,
- totalInvalidSummaries: 0,
- compactCallCount: 0,
- compactCallTotalTokens: 0,
- acceptedEstimatedTokensSaved: 0,
- };
- let calls = 0;
- const policy = {
- enabled: true,
- maxActiveEstimatedTokens: 1,
- highWaterRatio: 0.1,
- minSafePrefixEstimatedTokens: 1,
- maxAcceptedProjectionEstimatedTokens: 2048,
- minSavingsTokens: 1,
- minSavingsRatio: 0,
- maxConsecutiveInvalidSummaries: 1,
- invalidSummaryCooldownSteps: 3,
- } as const;
-
- const invalid = await rewriteSemanticCompactInMessages({
- sessionId: 'session-1',
- turnId: 'turn-1',
- messages: semanticFixtureMessages(),
- stepNumber: 2,
- charsPerToken: 1,
- policy,
- controllerState,
- summarizer: () => {
- calls += 1;
- return { text: JSON.stringify({ established_findings: ['Build configured.'] }) };
- },
- });
- assert.equal(invalid.decision, 'replaced');
- assert.equal(invalid.block?.acceptance.reason, 'summary_missing_action_in_progress');
- assert.equal(calls, 1);
- assert.equal(controllerState.consecutiveInvalidSummaries, 0);
-
- const cooled = await rewriteSemanticCompactInMessages({
- sessionId: 'session-1',
- turnId: 'turn-1',
- messages: semanticFixtureMessages(),
- stepNumber: 3,
- charsPerToken: 1,
- policy,
- controllerState,
- summarizer: () => {
- calls += 1;
- return {
- text: semanticSummary({
- finding: 'Should not run.',
- actionInProgress: 'Should not run.',
- }),
- };
- },
- });
- assert.equal(cooled.decision, 'replaced');
- assert.notEqual(cooled.reason, 'semantic_compact_cooldown');
- assert.equal(calls, 2);
-
- const resumed = await rewriteSemanticCompactInMessages({
- sessionId: 'session-1',
- turnId: 'turn-1',
- messages: semanticFixtureMessages(),
- stepNumber: 6,
- charsPerToken: 1,
- policy,
- controllerState,
- summarizer: () => {
- calls += 1;
- return {
- text: semanticSummary({ finding: 'Runs after cooldown.', actionInProgress: 'Continue.' }),
- };
- },
- });
- assert.notEqual(resumed.reason, 'semantic_compact_cooldown');
- assert.equal(calls, 3);
- });
-
- test('brakes semantic compact calls after repeated summarizer failures', async () => {
- const controllerState = {
- consecutiveInvalidSummaries: 0,
- totalInvalidSummaries: 0,
- compactCallCount: 0,
- compactCallTotalTokens: 0,
- acceptedEstimatedTokensSaved: 0,
- };
- let calls = 0;
- const policy = {
- enabled: true,
- maxActiveEstimatedTokens: 1,
- highWaterRatio: 0.1,
- minSafePrefixEstimatedTokens: 1,
- maxAcceptedProjectionEstimatedTokens: 2048,
- minSavingsTokens: 1,
- minSavingsRatio: 0,
- maxConsecutiveInvalidSummaries: 1,
- invalidSummaryCooldownSteps: 3,
- } as const;
-
- const failed = await rewriteSemanticCompactInMessages({
- sessionId: 'session-1',
- turnId: 'turn-1',
- messages: semanticFixtureMessages(),
- stepNumber: 2,
- charsPerToken: 1,
- policy,
- controllerState,
- summarizer: () => {
- calls += 1;
- throw new Error('boom');
- },
- });
- assert.equal(failed.reason, 'summarizer_failed');
- assert.equal(controllerState.consecutiveInvalidSummaries, 1);
-
- const cooled = await rewriteSemanticCompactInMessages({
- sessionId: 'session-1',
- turnId: 'turn-1',
- messages: semanticFixtureMessages(),
- stepNumber: 3,
- charsPerToken: 1,
- policy,
- controllerState,
- summarizer: () => {
- calls += 1;
- return {
- text: semanticSummary({
- finding: 'Should not run.',
- actionInProgress: 'Should not run.',
- }),
- };
- },
- });
- assert.equal(cooled.decision, 'unchanged');
- assert.equal(cooled.reason, 'semantic_compact_cooldown');
- assert.equal(calls, 1);
- });
-
- test('keeps runtime evidence in the diagnostic block without injecting it into the LLM projection', async () => {
- const result = await rewriteSemanticCompactInMessages({
- sessionId: 'session-1',
- turnId: 'turn-1',
- messages: semanticFixtureMessages(),
- stepNumber: 2,
- charsPerToken: 1,
- policy: {
- enabled: true,
- maxActiveEstimatedTokens: 1,
- highWaterRatio: 0.1,
- minSafePrefixEstimatedTokens: 1,
- maxAcceptedProjectionEstimatedTokens: 2048,
- minSavingsTokens: 1,
- minSavingsRatio: 0,
- },
- summarizer: () => ({
- text: semanticSummary({
- finding: 'Continue after compact.',
- actionInProgress: 'Resume with preserved tail.',
- }),
- }),
- });
-
- assert.equal(result.decision, 'replaced');
- assert.ok((result.block?.sourceRefs.length ?? 0) > 0);
- assert.ok((result.block?.coverage.providerMessageSourceIds.length ?? 0) > 0);
- assert.equal(result.block?.stateCards, undefined);
- const rendered = renderSemanticCompactBlock(result.block!);
- assert.match(rendered, /maka_semantic_compact_block/);
- assert.match(rendered, /resume action_in_progress instead of restarting task discovery/);
- assert.match(rendered, /action_in_progress: Resume with preserved tail\./);
- assert.doesNotMatch(rendered, /restoration_state_cards/);
- assert.doesNotMatch(rendered, /durable_archives_available/);
- assert.doesNotMatch(rendered, /durable_coverage/);
- assert.doesNotMatch(rendered, /preserved_tail:/);
- assert.doesNotMatch(rendered, /providerSourceIds=/);
- });
-
- test('preserves prior replay and the exact multimodal current-user head anchor', async () => {
- const messages = [
- { role: 'user', content: 'prior user' },
- { role: 'assistant', content: 'prior answer' },
- {
- role: 'user',
- content: [
- { type: 'text', text: 'Fix the current task exactly.' },
- { type: 'image', image: 'data:image/png;base64,AAAA' },
- ],
- providerOptions: { test: { stable: true } },
- },
- {
- role: 'assistant',
- content: [
- { type: 'reasoning', text: 'completed reasoning '.repeat(600) },
- {
- type: 'tool-call',
- toolCallId: 'anchor-earlier',
- toolName: 'Bash',
- input: { command: 'ls' },
- },
- ],
- },
- {
- role: 'tool',
- content: [
- { type: 'tool-result', toolCallId: 'anchor-earlier', toolName: 'Bash', result: 'files' },
- ],
- },
- {
- role: 'assistant',
- content: [
- {
- type: 'tool-call',
- toolCallId: 'anchor-recent',
- toolName: 'Bash',
- input: { command: 'pwd' },
- },
- ],
- },
- {
- role: 'tool',
- content: [
- {
- type: 'tool-result',
- toolCallId: 'anchor-recent',
- toolName: 'Bash',
- result: 'workspace ready',
- },
- ],
- },
- ] as unknown as ModelMessage[];
- const headAnchor = buildActiveCompactionHeadAnchor(messages, 2, 1);
- const result = await rewriteSemanticCompactInMessages({
- sessionId: 'session-anchor',
- turnId: 'turn-anchor',
- messages,
- headAnchor,
- stepNumber: 2,
- charsPerToken: 1,
- policy: attentionTestPolicy(),
- summarizer: () => ({
- text: semanticSummary({
- finding: 'Fix the current task exactly.',
- actionInProgress: 'Continue.',
- }),
- }),
- });
-
- assert.equal(result.decision, 'replaced');
- assert.deepEqual(result.messages.slice(0, 3), messages.slice(0, 3));
- assert.equal(result.block?.headAnchor?.messageSignature, headAnchor.messageSignature);
- assert.equal(result.block?.headAnchor?.bodySha256, headAnchor.bodySha256);
- assert.equal(result.block?.version, 2);
- const boundary = semanticCompactBlockToCompactionBoundary(result.block!);
- assert.equal(boundary.predecessorBoundaryId, undefined);
- assert.deepEqual(
- boundary.preservedAnchor?.headProviderMessageSourceIds,
- result.block?.headAnchor?.sourceIds,
- );
- assert.equal(
- result.block?.coverage.providerMessageSourceIds.some((id) => id.startsWith('provider:2:')),
- false,
- );
- });
-
- test('keeps an incomplete multi-tool episode in the exact tail', async () => {
- const messages = [
- { role: 'user', content: 'Current task' },
- {
- role: 'assistant',
- content: [
- { type: 'reasoning', text: 'earlier completed reasoning '.repeat(500) },
- {
- type: 'tool-call',
- toolCallId: 'call-earlier',
- toolName: 'Bash',
- input: { command: 'earlier' },
- },
- ],
- },
- {
- role: 'tool',
- content: [
- {
- type: 'tool-result',
- toolCallId: 'call-earlier',
- toolName: 'Bash',
- result: 'earlier-done',
- },
- ],
- },
- {
- role: 'assistant',
- content: [
- { type: 'reasoning', text: 'completed reasoning '.repeat(500) },
- {
- type: 'tool-call',
- toolCallId: 'call-complete',
- toolName: 'Bash',
- input: { command: 'done' },
- },
- ],
- },
- {
- role: 'tool',
- content: [
- { type: 'tool-result', toolCallId: 'call-complete', toolName: 'Bash', result: 'done' },
- ],
- },
- { role: 'assistant', content: [{ type: 'reasoning', text: 'open episode reasoning' }] },
- {
- role: 'assistant',
- content: [
- { type: 'tool-call', toolCallId: 'call-a', toolName: 'Bash', input: { command: 'a' } },
- { type: 'tool-call', toolCallId: 'call-b', toolName: 'Bash', input: { command: 'b' } },
- ],
- },
- {
- role: 'tool',
- content: [
- { type: 'tool-result', toolCallId: 'call-a', toolName: 'Bash', result: 'done-a' },
- ],
- },
- ] as unknown as ModelMessage[];
- const result = await rewriteSemanticCompactInMessages({
- sessionId: 'session-tail',
- turnId: 'turn-tail',
- messages,
- headAnchor: buildActiveCompactionHeadAnchor(messages, 0, 1),
- stepNumber: 2,
- charsPerToken: 1,
- policy: attentionTestPolicy(),
- summarizer: () => ({
- text: semanticSummary({ finding: 'Current task', actionInProgress: 'Finish tools.' }),
- }),
- });
-
- assert.equal(result.decision, 'replaced');
- assert.deepEqual(result.messages.slice(-5), messages.slice(-5));
- assert.deepEqual(result.block?.preservedTail.toolCallIds, [
- 'call-a',
- 'call-b',
- 'call-complete',
- ]);
- assert.doesNotMatch(renderSemanticCompactBlock(result.block!), /open episode reasoning/);
- });
-
- test('requires both total high water and a 4K completed safe span', async () => {
- let calls = 0;
- const messages = [
- { role: 'user', content: 'large exact instruction '.repeat(500) },
- {
- role: 'assistant',
- content: [
- { type: 'text', text: 'small completed step' },
- {
- type: 'tool-call',
- toolCallId: 'threshold-earlier',
- toolName: 'Bash',
- input: { command: 'true' },
- },
- ],
- },
- {
- role: 'tool',
- content: [
- { type: 'tool-result', toolCallId: 'threshold-earlier', toolName: 'Bash', result: 'ok' },
- ],
- },
- {
- role: 'assistant',
- content: [
- {
- type: 'tool-call',
- toolCallId: 'threshold-recent',
- toolName: 'Bash',
- input: { command: 'pwd' },
- },
- ],
- },
- {
- role: 'tool',
- content: [
- { type: 'tool-result', toolCallId: 'threshold-recent', toolName: 'Bash', result: 'ok' },
- ],
- },
- ] as ModelMessage[];
- const result = await rewriteSemanticCompactInMessages({
- sessionId: 'session-threshold',
- turnId: 'turn-threshold',
- messages,
- headAnchor: buildActiveCompactionHeadAnchor(messages, 0, 1),
- stepNumber: 2,
- charsPerToken: 1,
- policy: {
- ...attentionTestPolicy(),
- maxActiveEstimatedTokens: 16_000,
- highWaterRatio: 0.5,
- minSafePrefixEstimatedTokens: 4_096,
- },
- summarizer: () => {
- calls += 1;
- return { text: semanticSummary({ finding: 'unused', actionInProgress: 'unused' }) };
- },
- });
-
- assert.equal(result.decision, 'unchanged');
- assert.equal(result.reason, 'below_min_safe_prefix');
- assert.equal(calls, 0);
- assert.deepEqual(result.messages[0], messages[0]);
- });
-
- test('does not call the summarizer before the configured 128K attention high water', async () => {
- let calls = 0;
- const messages = [
- { role: 'user', content: 'Exact task instruction' },
- {
- role: 'assistant',
- content: [
- { type: 'text', text: 'older completed work '.repeat(2_000) },
- {
- type: 'tool-call',
- toolCallId: 'water-earlier',
- toolName: 'Bash',
- input: { command: 'true' },
- },
- ],
- },
- {
- role: 'tool',
- content: [
- { type: 'tool-result', toolCallId: 'water-earlier', toolName: 'Bash', result: 'ok' },
- ],
- },
- {
- role: 'assistant',
- content: [
- {
- type: 'tool-call',
- toolCallId: 'water-recent',
- toolName: 'Bash',
- input: { command: 'pwd' },
- },
- ],
- },
- {
- role: 'tool',
- content: [
- {
- type: 'tool-result',
- toolCallId: 'water-recent',
- toolName: 'Bash',
- result: 'workspace',
- },
- ],
- },
- ] as unknown as ModelMessage[];
- const result = await rewriteSemanticCompactInMessages({
- sessionId: 'session-128k-water',
- turnId: 'turn-128k-water',
- messages,
- headAnchor: buildActiveCompactionHeadAnchor(messages, 0, 1),
- stepNumber: 2,
- charsPerToken: 1,
- policy: {
- ...attentionTestPolicy(),
- maxActiveEstimatedTokens: 131_072,
- highWaterRatio: 1,
- },
- summarizer: () => {
- calls += 1;
- return { text: semanticSummary({ finding: 'unused', actionInProgress: 'unused' }) };
- },
- });
-
- assert.equal(result.decision, 'unchanged');
- assert.equal(result.reason, 'below_high_water');
- assert.equal(calls, 0);
- });
-
- test('rolls predecessor plus newly completed raw history into one V2 successor', async () => {
- const original = [
- {
- role: 'user',
- content: [
- { type: 'text', text: 'Exact task instruction' },
- { type: 'file', data: 'data:text/plain;base64,QQ==', mediaType: 'text/plain' },
- ],
- providerOptions: { test: { anchor: 'stable' } },
- },
- {
- role: 'assistant',
- content: [
- { type: 'text', text: 'first completed history '.repeat(500) },
- {
- type: 'tool-call',
- toolCallId: 'roll-earlier-1',
- toolName: 'Bash',
- input: { command: 'ls' },
- },
- ],
- },
- {
- role: 'tool',
- content: [
- { type: 'tool-result', toolCallId: 'roll-earlier-1', toolName: 'Bash', result: 'files' },
- ],
- },
- {
- role: 'assistant',
- content: [
- {
- type: 'tool-call',
- toolCallId: 'roll-recent-1',
- toolName: 'Bash',
- input: { command: 'pwd' },
- },
- ],
- },
- {
- role: 'tool',
- content: [
- {
- type: 'tool-result',
- toolCallId: 'roll-recent-1',
- toolName: 'Bash',
- result: 'workspace ready',
- },
- ],
- },
- ] as unknown as ModelMessage[];
- const headAnchor = buildActiveCompactionHeadAnchor(original, 0, 1);
- const first = await rewriteSemanticCompactInMessages({
- sessionId: 'session-roll',
- turnId: 'turn-roll',
- messages: original,
- headAnchor,
- stepNumber: 2,
- charsPerToken: 1,
- policy: attentionTestPolicy(),
- summarizer: () => ({
- text: semanticSummary({
- finding: 'Exact task instruction',
- actionInProgress: 'Second step.',
- }),
- }),
- });
- assert.equal(first.decision, 'replaced');
-
- first.block!.stateCards = [
- {
- kind: 'process',
- text: 'Let me re-read every source file before writing the VM.',
- sourceIds: ['legacy-source'],
- },
- ];
- const legacyProjection = first.messages.find((message) =>
- JSON.stringify(message.content).includes('<maka_semantic_compact_block'),
- );
- assert.ok(legacyProjection);
- legacyProjection.content = `${String(legacyProjection.content)}\nrestoration_state_cards:\n- Let me re-read every source file before writing the VM.`;
-
- const withNewHistory = [
- ...first.messages,
- {
- role: 'assistant',
- content: [
- { type: 'text', text: 'second completed history '.repeat(500) },
- {
- type: 'tool-call',
- toolCallId: 'roll-earlier-2',
- toolName: 'Bash',
- input: { command: 'git diff' },
- },
- ],
- } as ModelMessage,
- {
- role: 'tool',
- content: [
- { type: 'tool-result', toolCallId: 'roll-earlier-2', toolName: 'Bash', result: 'diff' },
- ],
- } as unknown as ModelMessage,
- {
- role: 'assistant',
- content: [
- {
- type: 'tool-call',
- toolCallId: 'roll-recent-2',
- toolName: 'Bash',
- input: { command: 'git status' },
- },
- ],
- } as ModelMessage,
- {
- role: 'tool',
- content: [
- { type: 'tool-result', toolCallId: 'roll-recent-2', toolName: 'Bash', result: 'clean' },
- ],
- } as unknown as ModelMessage,
- ];
- const second = await rewriteSemanticCompactInMessages({
- sessionId: 'session-roll',
- turnId: 'turn-roll',
- messages: withNewHistory,
- headAnchor,
- predecessorBlock: first.block,
- stepNumber: 3,
- charsPerToken: 1,
- policy: { ...attentionTestPolicy(), minNewPrefixEstimatedTokens: 1 },
- summarizer: (request) => {
- const renderedRequest = JSON.stringify(request.messages);
- assert.match(renderedRequest, new RegExp(first.block!.blockId));
- assert.doesNotMatch(renderedRequest, /restoration_state_cards/);
- assert.doesNotMatch(renderedRequest, /Let me re-read every source file/);
- return {
- text: semanticSummary({ finding: 'Exact task instruction', actionInProgress: 'Finish.' }),
- };
- },
- });
-
- assert.equal(second.decision, 'replaced');
- assert.equal(second.block?.predecessorBlockId, first.block?.blockId);
- assert.notEqual(second.block?.cumulativeCoverageDigest, first.block?.cumulativeCoverageDigest);
- assert.equal(
- second.messages.filter((message) =>
- JSON.stringify(message.content).includes('<maka_semantic_compact_block'),
- ).length,
- 1,
- );
- assert.deepEqual(second.messages[0], original[0]);
- });
-
- test('rejects empty and token-limit-truncated summarizer output', async () => {
- const messages = semanticFixtureMessages();
- const input = {
- sessionId: 'session-reject',
- turnId: 'turn-reject',
- messages,
- headAnchor: buildActiveCompactionHeadAnchor(messages, 0, 1),
- stepNumber: 2,
- charsPerToken: 1,
- policy: attentionTestPolicy(),
- } as const;
- const empty = await rewriteSemanticCompactInMessages({
- ...input,
- summarizer: () => ({ text: '' }),
- });
- const truncated = await rewriteSemanticCompactInMessages({
- ...input,
- summarizer: () => ({ text: '{"action_in_progress":"partial', finishReason: 'max_tokens' }),
- });
- assert.equal(empty.decision, 'unchanged');
- assert.equal(empty.reason, 'summary_missing');
- assert.equal(truncated.decision, 'unchanged');
- assert.equal(truncated.reason, 'summary_truncated');
- });
-
- test('bounds the complete provider-visible projection block', async () => {
- const messages = semanticFixtureMessages();
- const result = await rewriteSemanticCompactInMessages({
- sessionId: 'session-budget',
- turnId: 'turn-budget',
- messages,
- headAnchor: buildActiveCompactionHeadAnchor(messages, 0, 4),
- stepNumber: 2,
- charsPerToken: 4,
- policy: {
- ...attentionTestPolicy(),
- maxAcceptedProjectionEstimatedTokens: 768,
- },
- summarizer: () => ({
- text: semanticSummary({
- finding: 'Keep the objective focused. '.repeat(40),
- actionInProgress: 'Continue with the next exact action. '.repeat(40),
- partialWorkProduct: Array.from(
- { length: 8 },
- (_, index) => `command-${index} ${'output '.repeat(80)}`,
- ),
- }),
- }),
- });
- assert.equal(result.decision, 'replaced');
- assert.ok(Math.ceil(renderSemanticCompactBlock(result.block!).length / 4) <= 768);
- assert.ok((result.block?.projection?.estimatedTokens ?? 769) <= 768);
-
- const fallback = await rewriteSemanticCompactInMessages({
- sessionId: 'session-budget-fallback',
- turnId: 'turn-budget-fallback',
- messages,
- headAnchor: buildActiveCompactionHeadAnchor(messages, 0, 4),
- stepNumber: 2,
- charsPerToken: 4,
- policy: { ...attentionTestPolicy(), maxAcceptedProjectionEstimatedTokens: 768 },
- summarizer: () => ({
- text: 'The build is configured and the service is running. '.repeat(400),
- }),
- });
- assert.equal(fallback.decision, 'replaced');
- assert.equal(fallback.block?.projection?.format, 'bounded_text_fallback');
- assert.ok(Math.ceil(renderSemanticCompactBlock(fallback.block!).length / 4) <= 768);
- assert.ok((fallback.block?.projection?.estimatedTokens ?? 769) <= 768);
- });
-});
-
-function attentionTestPolicy() {
- return {
- enabled: true,
- maxActiveEstimatedTokens: 1,
- highWaterRatio: 0.1,
- minSafePrefixEstimatedTokens: 1,
- minNewPrefixEstimatedTokens: 1,
- maxAcceptedProjectionEstimatedTokens: 2048,
- minSavingsTokens: 1,
- minSavingsRatio: 0,
- } as const;
-}
-
-function semanticFixtureMessages(): ModelMessage[] {
- return [
- {
- role: 'user',
- content: 'Please fix the build and keep the service running. '.repeat(80),
- } as ModelMessage,
- {
- role: 'assistant',
- content: 'I ran configure and saw a linker failure. '.repeat(80),
- } as ModelMessage,
- {
- role: 'assistant',
- content: [
- {
- type: 'tool-call',
- toolCallId: 'tool-old',
- toolName: 'Bash',
- input: { command: 'make test' },
- },
- ],
- } as ModelMessage,
- {
- role: 'tool',
- content: [
- {
- type: 'tool-result',
- toolCallId: 'tool-old',
- toolName: 'Bash',
- result: { body: 'OLD_BUILD_LOG '.repeat(800) },
- },
- ],
- } as unknown as ModelMessage,
- {
- role: 'assistant',
- content: [
- {
- type: 'tool-call',
- toolCallId: 'tool-recent',
- toolName: 'Bash',
- input: { command: 'ps aux' },
- },
- ],
- } as ModelMessage,
- {
- role: 'tool',
- content: [
- {
- type: 'tool-result',
- toolCallId: 'tool-recent',
- toolName: 'Bash',
- result: { body: 'recent-result service still running' },
- },
- ],
- } as unknown as ModelMessage,
- ];
-}
-
-function semanticSummary(input: {
- finding: string;
- actionInProgress: string;
- partialWorkProduct?: string[];
-}): string {
- return JSON.stringify({
- established_findings: [input.finding],
- decisions: [],
- failed_paths: [],
- partial_work_product: input.partialWorkProduct ?? [],
- action_in_progress: input.actionInProgress,
- });
-}
diff --git a/packages/runtime/src/__tests__/session-manager.test.ts b/packages/runtime/src/__tests__/session-manager.test.ts
index d0180caf6..73c4fa77f 100644
--- a/packages/runtime/src/__tests__/session-manager.test.ts
+++ b/packages/runtime/src/__tests__/session-manager.test.ts
@@ -3917,6 +3917,9 @@ describe('SessionManager manual compaction and quiescent session changes', () =>
const usage = events[0];
if (usage?.type !== 'token_usage') throw new Error('expected token_usage');
expect(usage.contextBudget?.compactionDecisions?.[0]?.decision).toBe('replaced');
+ const complete = events[1];
+ if (complete?.type !== 'complete') throw new Error('expected complete');
+ expect(complete.contextCompactionOutcome?.kind).toBe('compacted');
const messages = await store.readMessages(session.id);
expect(
@@ -3943,7 +3946,7 @@ describe('SessionManager manual compaction and quiescent session changes', () =>
expect(runStore.operations).toEqual(['terminalRuntimeEvent', 'completedRunHeader']);
expect(
(await runStore.readRuntimeEvents(session.id, compactRun!.runId)).some(
- (event) => event.actions?.tokenUsage?.contextBudget,
+ (event) => event.actions?.stateDelta?.contextCompactionOutcome,
),
).toBe(true);
await manager.stopSession(session.id, { source: 'stop_button' });
@@ -4018,7 +4021,6 @@ describe('SessionManager manual compaction and quiescent session changes', () =>
contextBudget: {
name: 'manual-compact-accounting',
maxHistoryEstimatedTokens: 10_000,
- minRecentTurns: 1,
charsPerToken: 1,
},
summarizeHistoryCompact: buildLlmHistorySummarizer({
@@ -16171,6 +16173,7 @@ class ActiveTurnBackend extends TestBackend {
function compactHistoryResult() {
return {
+ outcome: { kind: 'compacted' as const, checkpointId: 'checkpoint-1' },
contextBudget: {
enabled: true,
policyName: 'unit-budget',
@@ -16195,6 +16198,7 @@ function compactHistoryResult() {
function compactHistoryFailOpenResult() {
return {
+ outcome: { kind: 'failed' as const, reason: 'write_failed' },
contextBudget: {
enabled: true,
policyName: 'unit-budget',
@@ -16204,7 +16208,6 @@ function compactHistoryFailOpenResult() {
droppedTurns: 1,
keptEvents: 1,
droppedEvents: 1,
- historyCompactWriteFailures: 1,
compactionDecisions: [
{
stage: 'priorReplay' as const,
diff --git a/packages/runtime/src/__tests__/session-recap.test.ts b/packages/runtime/src/__tests__/session-recap.test.ts
new file mode 100644
index 000000000..25d97bc6a
--- /dev/null
+++ b/packages/runtime/src/__tests__/session-recap.test.ts
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import assert from 'node:assert/strict';
+import { test } from 'node:test';
+import type { RuntimeEvent } from '@maka/core/runtime-event';
+import type { LlmConnection } from '@maka/core/llm-connections';
+import { buildSessionRecapMessages } from '../session-recap.js';
+
+test('session recap bounds its request to the newest complete turns', () => {
+ const events: RuntimeEvent[] = [];
+ for (let index = 0; index < 20; index += 1) {
+ const turnId = `turn-${index}`;
+ events.push(textEvent(`user-${index}`, turnId, 'user', `user-${index} ${'u'.repeat(2_000)}`));
+ events.push(
+ textEvent(`model-${index}`, turnId, 'model', `model-${index} ${'m'.repeat(2_000)}`),
+ );
+ }
+
+ const messages = buildSessionRecapMessages({
+ events,
+ connection: connection(),
+ modelId: 'gpt-4',
+ });
+ const serialized = JSON.stringify(messages);
+
+ assert.equal(serialized.includes('user-0 '), false);
+ assert.equal(serialized.includes('model-19 '), true);
... 10730 lines suppressed ...
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org