deepstrike 0.2.7__tar.gz → 0.2.9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {deepstrike-0.2.7 → deepstrike-0.2.9}/Cargo.lock +5 -5
- {deepstrike-0.2.7 → deepstrike-0.2.9}/Cargo.toml +2 -2
- {deepstrike-0.2.7 → deepstrike-0.2.9}/PKG-INFO +1 -1
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/context/compression.rs +130 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/context/manager.rs +258 -18
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/context/mod.rs +0 -1
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/context/pressure.rs +8 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/context/task_state.rs +90 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/mm/handle.rs +124 -43
- deepstrike-0.2.9/crates/deepstrike-core/src/orchestration/mod.rs +8 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/orchestration/task_graph.rs +25 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/orchestration/workflow.rs +137 -3
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/runtime/kernel.rs +59 -224
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/runtime/mod.rs +2 -0
- deepstrike-0.2.9/crates/deepstrike-core/src/runtime/snapshot.rs +507 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/scheduler/state_machine.rs +397 -283
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/scheduler/state_machine_tests.rs +607 -35
- deepstrike-0.2.9/crates/deepstrike-core/src/scheduler/tcb.rs +1007 -0
- deepstrike-0.2.9/crates/deepstrike-core/src/scheduler/workflow_run.rs +909 -0
- deepstrike-0.2.9/crates/deepstrike-core/src/types/result.rs +53 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/types/task.rs +1 -1
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-py/src/lib.rs +0 -283
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/__init__.py +0 -7
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/providers/__init__.py +8 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/providers/base.py +14 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/providers/deepseek.py +2 -2
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/providers/minimax.py +2 -2
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/providers/openai.py +24 -10
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/providers/replay.py +18 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/providers/replay_validator.py +52 -21
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/__init__.py +4 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/provider_replay.py +13 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/pyproject.toml +1 -1
- deepstrike-0.2.7/crates/deepstrike-core/src/context/llm_summarizer.rs +0 -240
- deepstrike-0.2.7/crates/deepstrike-core/src/orchestration/loop_until_done.rs +0 -281
- deepstrike-0.2.7/crates/deepstrike-core/src/orchestration/mod.rs +0 -7
- deepstrike-0.2.7/crates/deepstrike-core/src/scheduler/tcb.rs +0 -416
- deepstrike-0.2.7/crates/deepstrike-core/src/scheduler/workflow_run.rs +0 -375
- deepstrike-0.2.7/crates/deepstrike-core/src/types/result.rs +0 -37
- deepstrike-0.2.7/deepstrike/workflow.py +0 -54
- {deepstrike-0.2.7 → deepstrike-0.2.9}/README.md +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/Cargo.toml +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/context/config.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/context/dashboard.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/context/partitions.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/context/renderer.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/context/renewal.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/context/sections.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/context/skill_catalog.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/context/snapshot.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/context/summarizer.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/context/text.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/context/token_engine.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/governance/audit.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/governance/constraint.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/governance/mod.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/governance/permission.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/governance/pipeline.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/governance/quota.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/governance/rate_limit.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/governance/sandbox.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/governance/tool_decision.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/governance/veto.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/harness/eval_pipeline.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/harness/mod.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/lib.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/memory/curator.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/memory/durable.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/memory/extractor.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/memory/idle_pipeline.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/memory/mod.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/memory/runtime.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/memory/semantic.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/memory/session.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/memory/synthesis.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/memory/trace_analyzer.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/memory/working.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/mm/memory.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/mm/mod.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/orchestration/executor.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/orchestration/gen_eval.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/orchestration/planner.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/orchestration/tournament.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/proc/mod.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/runtime/event_log.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/runtime/repair.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/runtime/replay.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/runtime/session.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/scheduler/milestone.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/scheduler/mod.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/scheduler/policy.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/scheduler/rollback.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/signals/attention.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/signals/mod.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/signals/queue.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/signals/router.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/syscall/mod.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/types/agent.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/types/capability.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/types/contract.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/types/error.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/types/message.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/types/milestone.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/types/mod.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/types/model.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/types/policy.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/types/signal.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/types/skill.rs +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-py/Cargo.toml +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/collaboration/__init__.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/collaboration/contract.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/collaboration/handoff.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/collaboration/harness.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/collaboration/modes.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/collaboration/pool.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/governance.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/harness/__init__.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/harness/harness.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/kernel/__init__.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/knowledge/__init__.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/knowledge/source.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/memory/__init__.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/memory/agent.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/memory/protocols.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/memory/working.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/providers/anthropic.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/providers/gemini.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/providers/glm.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/providers/kimi.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/providers/ollama.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/providers/qwen.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/providers/stream.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/archive.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/credential_vault.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/execution_plane.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/filtered_plane.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/kernel_event_log.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/kernel_step.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/large_result_spool.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/mcp_proxy_plane.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/os_profile.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/os_snapshot.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/process_sandbox_plane.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/remote_vpc_plane.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/replay_sanitize.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/runner.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/session_log.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/session_repair.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/sub_agent_orchestrator.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/safety/__init__.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/safety/permissions.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/signals/__init__.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/signals/gateway.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/signals/scheduled.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/signals/types.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/skills/__init__.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/skills/registry.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/skills/watcher.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/tools/__init__.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/tools/builtin/__init__.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/tools/builtin/read_file.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/tools/execution.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/tools/registry.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/types/__init__.py +0 -0
- {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/types/agent.py +0 -0
|
@@ -194,7 +194,7 @@ dependencies = [
|
|
|
194
194
|
|
|
195
195
|
[[package]]
|
|
196
196
|
name = "deepstrike-core"
|
|
197
|
-
version = "0.2.
|
|
197
|
+
version = "0.2.9"
|
|
198
198
|
dependencies = [
|
|
199
199
|
"compact_str",
|
|
200
200
|
"pretty_assertions",
|
|
@@ -206,7 +206,7 @@ dependencies = [
|
|
|
206
206
|
|
|
207
207
|
[[package]]
|
|
208
208
|
name = "deepstrike-node"
|
|
209
|
-
version = "0.2.
|
|
209
|
+
version = "0.2.9"
|
|
210
210
|
dependencies = [
|
|
211
211
|
"compact_str",
|
|
212
212
|
"deepstrike-core",
|
|
@@ -218,7 +218,7 @@ dependencies = [
|
|
|
218
218
|
|
|
219
219
|
[[package]]
|
|
220
220
|
name = "deepstrike-py"
|
|
221
|
-
version = "0.2.
|
|
221
|
+
version = "0.2.9"
|
|
222
222
|
dependencies = [
|
|
223
223
|
"compact_str",
|
|
224
224
|
"deepstrike-core",
|
|
@@ -229,7 +229,7 @@ dependencies = [
|
|
|
229
229
|
|
|
230
230
|
[[package]]
|
|
231
231
|
name = "deepstrike-sdk"
|
|
232
|
-
version = "0.2.
|
|
232
|
+
version = "0.2.9"
|
|
233
233
|
dependencies = [
|
|
234
234
|
"async-stream",
|
|
235
235
|
"async-trait",
|
|
@@ -263,7 +263,7 @@ dependencies = [
|
|
|
263
263
|
|
|
264
264
|
[[package]]
|
|
265
265
|
name = "deepstrike-wasm"
|
|
266
|
-
version = "0.2.
|
|
266
|
+
version = "0.2.9"
|
|
267
267
|
dependencies = [
|
|
268
268
|
"compact_str",
|
|
269
269
|
"deepstrike-core",
|
|
@@ -3,13 +3,13 @@ resolver = "2"
|
|
|
3
3
|
members = ["crates/deepstrike-core", "crates/deepstrike-py"]
|
|
4
4
|
|
|
5
5
|
[workspace.package]
|
|
6
|
-
version = "0.2.
|
|
6
|
+
version = "0.2.9"
|
|
7
7
|
edition = "2024"
|
|
8
8
|
license = "MIT"
|
|
9
9
|
repository = "https://github.com/kongusen/deepstrike"
|
|
10
10
|
|
|
11
11
|
[workspace.dependencies]
|
|
12
|
-
deepstrike-core = { path = "crates/deepstrike-core", version = "0.2.
|
|
12
|
+
deepstrike-core = { path = "crates/deepstrike-core", version = "0.2.9" }
|
|
13
13
|
notify = "6"
|
|
14
14
|
serde = { version = "1", features = ["derive"] }
|
|
15
15
|
serde_json = "1"
|
|
@@ -663,6 +663,136 @@ mod tests {
|
|
|
663
663
|
assert!(ctx.task_state.compression_log.iter().any(|e| e.action == "auto_compact"));
|
|
664
664
|
}
|
|
665
665
|
|
|
666
|
+
// ─── W1-1 characterization baseline ────────────────────────────────────────
|
|
667
|
+
// Locks the CURRENT compaction behavior (tokens_saved / archived count / summary)
|
|
668
|
+
// across all four pressure levels + the cascade, so the upcoming compactor→executor
|
|
669
|
+
// refactor (EvictionOp vocab + cache-aware planner) is provably behavior-preserving.
|
|
670
|
+
// These are golden-master pins: the values describe what the pipeline does TODAY, not
|
|
671
|
+
// an independent derivation. If a future change moves a number here, that is a behavior
|
|
672
|
+
// change and must be justified, not blindly re-pinned.
|
|
673
|
+
|
|
674
|
+
use crate::types::message::Role;
|
|
675
|
+
use compact_str::CompactString;
|
|
676
|
+
|
|
677
|
+
/// Deterministic fixture: 4 oversized text turns + 2 tool-result messages, explicit token
|
|
678
|
+
/// counts so the cascade math is reproducible under `char_approx`.
|
|
679
|
+
fn baseline_partitions() -> ContextPartitions {
|
|
680
|
+
let cfg = config();
|
|
681
|
+
let mut ctx = ContextPartitions::new(&cfg);
|
|
682
|
+
// Oversized text turns (trigger Snip / Collapse / Auto).
|
|
683
|
+
ctx.history.push(Message::user("u0 ".repeat(120)), 300);
|
|
684
|
+
ctx.history.push(Message::assistant("a0 ".repeat(120)), 300);
|
|
685
|
+
// Tool-result message (trigger Micro).
|
|
686
|
+
ctx.history.messages.push(Message {
|
|
687
|
+
role: Role::Tool,
|
|
688
|
+
content: Content::Parts(vec![ContentPart::ToolResult {
|
|
689
|
+
call_id: CompactString::new("call_1"),
|
|
690
|
+
output: serde_json::json!({"rows": 42, "ok": true, "name": "alpha"}).to_string()
|
|
691
|
+
+ &"-pad".repeat(400),
|
|
692
|
+
is_error: false,
|
|
693
|
+
}]),
|
|
694
|
+
tool_calls: vec![],
|
|
695
|
+
token_count: Some(400),
|
|
696
|
+
});
|
|
697
|
+
ctx.history.token_count += 400;
|
|
698
|
+
ctx.history.push(Message::user("u1 ".repeat(120)), 300);
|
|
699
|
+
ctx.history.push(Message::assistant("a1 ".repeat(120)), 300);
|
|
700
|
+
ctx.history.messages.push(Message {
|
|
701
|
+
role: Role::Tool,
|
|
702
|
+
content: Content::Parts(vec![ContentPart::ToolResult {
|
|
703
|
+
call_id: CompactString::new("call_2"),
|
|
704
|
+
output: "y".repeat(1600),
|
|
705
|
+
is_error: false,
|
|
706
|
+
}]),
|
|
707
|
+
tool_calls: vec![],
|
|
708
|
+
token_count: Some(400),
|
|
709
|
+
});
|
|
710
|
+
ctx.history.token_count += 400;
|
|
711
|
+
ctx
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/// Run the pipeline on a fresh baseline fixture at one action level.
|
|
715
|
+
/// Returns `(before, saved, summary, archived_len, msgs_after, total_after)`.
|
|
716
|
+
fn run_baseline(action: PressureAction) -> (u32, u32, Option<String>, usize, usize, u32) {
|
|
717
|
+
let mut ctx = baseline_partitions();
|
|
718
|
+
let before = ctx.total_tokens(&engine());
|
|
719
|
+
let (saved, summary, archived) =
|
|
720
|
+
CompressionPipeline::new(&config()).compress(&mut ctx, action, MAX, 500, &engine());
|
|
721
|
+
let archived_len = archived.len();
|
|
722
|
+
let msgs_after = ctx.history.messages.len();
|
|
723
|
+
let total_after = ctx.total_tokens(&engine());
|
|
724
|
+
(before, saved, summary, archived_len, msgs_after, total_after)
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
#[test]
|
|
728
|
+
fn baseline_snip_only_caps_text_no_archival() {
|
|
729
|
+
// SnipCompact runs only the Snip stage: caps oversized text messages in place; never
|
|
730
|
+
// archives or summarizes. Cascade stops above target (snip alone can't reach 500).
|
|
731
|
+
let (before, saved, summary, archived, msgs, total) = run_baseline(PressureAction::SnipCompact);
|
|
732
|
+
assert_eq!(before, 2001);
|
|
733
|
+
assert_eq!(saved, 1000);
|
|
734
|
+
assert_eq!(archived, 0);
|
|
735
|
+
assert!(summary.is_none());
|
|
736
|
+
assert_eq!(msgs, 6, "snip mutates in place, drops no messages");
|
|
737
|
+
assert_eq!(total, 1001);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
#[test]
|
|
741
|
+
fn baseline_micro_excerpts_tool_results() {
|
|
742
|
+
// MicroCompact runs Snip then Micro: tool results excerpted to placeholders. Still no
|
|
743
|
+
// archival/summary; messages stay in place.
|
|
744
|
+
let (before, saved, summary, archived, msgs, total) = run_baseline(PressureAction::MicroCompact);
|
|
745
|
+
assert_eq!(before, 2001);
|
|
746
|
+
assert_eq!(saved, 1362);
|
|
747
|
+
assert_eq!(archived, 0);
|
|
748
|
+
assert!(summary.is_none());
|
|
749
|
+
assert_eq!(msgs, 6);
|
|
750
|
+
assert_eq!(total, 639);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
#[test]
|
|
754
|
+
fn baseline_collapse_drops_oldest_and_summarizes() {
|
|
755
|
+
// ContextCollapse runs Snip→Micro→Collapse: oldest messages drained to `archived` with a
|
|
756
|
+
// summary, down to the preserve-recent floor (4 msgs kept).
|
|
757
|
+
let (before, saved, summary, archived, msgs, total) =
|
|
758
|
+
run_baseline(PressureAction::ContextCollapse);
|
|
759
|
+
assert_eq!(before, 2001);
|
|
760
|
+
assert_eq!(saved, 1462);
|
|
761
|
+
assert_eq!(archived, 2, "drops the 2 oldest messages above the preserve floor");
|
|
762
|
+
assert_eq!(msgs, 4, "preserve_recent_turns=2 → 4 messages kept");
|
|
763
|
+
assert_eq!(total, 539);
|
|
764
|
+
let summary = summary.expect("collapse summarizes archived messages");
|
|
765
|
+
assert!(
|
|
766
|
+
summary.contains("[Compressed: context_collapse]"),
|
|
767
|
+
"summary routes the collapse action: {summary}"
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
#[test]
|
|
772
|
+
fn baseline_auto_matches_collapse_at_preserve_floor() {
|
|
773
|
+
// AutoCompact runs all 4 stages, but on this fixture Snip→Micro→Collapse already hit the
|
|
774
|
+
// preserve floor, so the Auto stage archives nothing extra: identical outcome to Collapse.
|
|
775
|
+
let (before, saved, summary, archived, msgs, total) = run_baseline(PressureAction::AutoCompact);
|
|
776
|
+
assert_eq!(before, 2001);
|
|
777
|
+
assert_eq!(saved, 1462);
|
|
778
|
+
assert_eq!(archived, 2);
|
|
779
|
+
assert_eq!(msgs, 4);
|
|
780
|
+
assert_eq!(total, 539);
|
|
781
|
+
assert!(summary.is_some());
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
#[test]
|
|
785
|
+
fn baseline_saved_is_monotonic_in_action_level() {
|
|
786
|
+
// The cross-level contract the refactor must preserve: heavier pressure never frees less.
|
|
787
|
+
let snip = run_baseline(PressureAction::SnipCompact).1;
|
|
788
|
+
let micro = run_baseline(PressureAction::MicroCompact).1;
|
|
789
|
+
let collapse = run_baseline(PressureAction::ContextCollapse).1;
|
|
790
|
+
let auto = run_baseline(PressureAction::AutoCompact).1;
|
|
791
|
+
assert!(snip <= micro, "{snip} <= {micro}");
|
|
792
|
+
assert!(micro <= collapse, "{micro} <= {collapse}");
|
|
793
|
+
assert!(collapse <= auto, "{collapse} <= {auto}");
|
|
794
|
+
}
|
|
795
|
+
|
|
666
796
|
#[test]
|
|
667
797
|
fn pipeline_stops_cascade_when_target_reached() {
|
|
668
798
|
let cfg = ContextConfig {
|
|
@@ -119,29 +119,57 @@ impl ContextManager {
|
|
|
119
119
|
pub fn recompute_handle_residency(&mut self) {
|
|
120
120
|
let collapse = self.rho() >= self.config.collapse_threshold;
|
|
121
121
|
let keep = self.config.preserve_recent_msgs;
|
|
122
|
-
|
|
122
|
+
// Single mutable pass in insertion order. The previous implementation collected ids then
|
|
123
|
+
// re-found each via `get_mut` (a linear scan), making this O(handles²) **every turn** — the
|
|
124
|
+
// long-session cliff. `tool_result_handles_mut().enumerate()` yields the same set in the
|
|
125
|
+
// same order, so `i`/`cutoff` are bit-identical to the old logic, now in O(handles).
|
|
126
|
+
let total = self
|
|
123
127
|
.handles
|
|
124
128
|
.all()
|
|
125
129
|
.iter()
|
|
126
130
|
.filter(|h| matches!(h.kind, HandleKind::ToolResult))
|
|
127
|
-
.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
Residency::Resident
|
|
139
|
-
};
|
|
140
|
-
}
|
|
131
|
+
.count();
|
|
132
|
+
let cutoff = total.saturating_sub(keep);
|
|
133
|
+
for (i, handle) in self.handles.tool_result_handles_mut().enumerate() {
|
|
134
|
+
// Only toggle the reversible Resident<->Collapsed axis; never clobber a handle
|
|
135
|
+
// that has been spooled or paged out.
|
|
136
|
+
if matches!(handle.residency, Residency::Resident | Residency::Collapsed) {
|
|
137
|
+
handle.residency = if collapse && i < cutoff {
|
|
138
|
+
Residency::Collapsed
|
|
139
|
+
} else {
|
|
140
|
+
Residency::Resident
|
|
141
|
+
};
|
|
141
142
|
}
|
|
142
143
|
}
|
|
143
144
|
}
|
|
144
145
|
|
|
146
|
+
/// Drop handles whose anchored source message no longer lives in `partitions.history` — i.e.
|
|
147
|
+
/// archived by a compaction or dropped on renewal. Without this the handle table grows with
|
|
148
|
+
/// total session length (a handle per tool result, never removed), which also inflates the
|
|
149
|
+
/// per-turn `recompute_handle_residency` scan. Called at compaction/renewal boundaries, so the
|
|
150
|
+
/// table tracks the working set, not the whole session. Handles with no `source` anchor (future
|
|
151
|
+
/// non-tool-result kinds) are always kept — they can't be orphaned by this check.
|
|
152
|
+
pub fn prune_orphaned_handles(&mut self) {
|
|
153
|
+
let live: std::collections::HashSet<CompactString> = self
|
|
154
|
+
.partitions
|
|
155
|
+
.history
|
|
156
|
+
.messages
|
|
157
|
+
.iter()
|
|
158
|
+
.flat_map(|m| match &m.content {
|
|
159
|
+
Content::Parts(parts) => parts
|
|
160
|
+
.iter()
|
|
161
|
+
.filter_map(|p| match p {
|
|
162
|
+
ContentPart::ToolResult { call_id, .. } => Some(call_id.clone()),
|
|
163
|
+
_ => None,
|
|
164
|
+
})
|
|
165
|
+
.collect::<Vec<_>>(),
|
|
166
|
+
_ => Vec::new(),
|
|
167
|
+
})
|
|
168
|
+
.collect();
|
|
169
|
+
self.handles
|
|
170
|
+
.retain(|h| h.source.as_ref().is_none_or(|s| live.contains(s)));
|
|
171
|
+
}
|
|
172
|
+
|
|
145
173
|
/// Mark the handle anchored to `call_id` as spooled to disk (Layer 1): the SDK persists the
|
|
146
174
|
/// full output, working context keeps only the preview. Keeps the handle out of the
|
|
147
175
|
/// Resident↔Collapsed projection cycle. No-op if no handle is anchored to `call_id`.
|
|
@@ -159,8 +187,32 @@ impl ContextManager {
|
|
|
159
187
|
|
|
160
188
|
// ── Pressure ──────────────────────────────────────────────────────────────
|
|
161
189
|
|
|
190
|
+
/// **Raw** rho — full partition weight (or provider-observed tokens when available). This is the
|
|
191
|
+
/// projection-decision rho: [`Self::recompute_handle_residency`] marks the Resident↔Collapsed set
|
|
192
|
+
/// from *this* value, so it must NOT discount paged content (else collapse → rho drops →
|
|
193
|
+
/// un-collapse would oscillate). Compaction/renewal triggers use [`Self::effective_rho`] instead.
|
|
162
194
|
pub fn rho(&self) -> f64 {
|
|
163
|
-
self.pressure
|
|
195
|
+
self.pressure
|
|
196
|
+
.pressure(&self.partitions, &self.engine, self.last_observed_prompt_tokens)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// **Effective** rho — the pressure that actually drives compaction/renewal, made paging-aware.
|
|
200
|
+
///
|
|
201
|
+
/// When provider usage is authoritative (`observed_prompt_tokens` set), the rendered prompt was
|
|
202
|
+
/// already collapsed (the renderer emits previews for `Collapsed` handles), so the observed count
|
|
203
|
+
/// already reflects paging — raw rho is exact and returned as-is. In the **estimate** path
|
|
204
|
+
/// (no observed tokens) we estimate from `partitions`, which still carry the full weight of
|
|
205
|
+
/// paged-out tool results (collapse is non-destructive); we subtract the non-resident handle
|
|
206
|
+
/// tokens so that collapsing/spooling a result immediately relieves pressure, rather than only
|
|
207
|
+
/// after the next provider round-trip. With no paged handles this equals [`Self::rho`], so the
|
|
208
|
+
/// pre-paging behavior is preserved exactly.
|
|
209
|
+
pub fn effective_rho(&self) -> f64 {
|
|
210
|
+
if self.max_tokens == 0 || self.last_observed_prompt_tokens.is_some() {
|
|
211
|
+
return self.rho();
|
|
212
|
+
}
|
|
213
|
+
let total = self.partitions.total_tokens(&self.engine);
|
|
214
|
+
let effective = total.saturating_sub(self.handles.non_resident_tokens());
|
|
215
|
+
effective as f64 / self.max_tokens as f64
|
|
164
216
|
}
|
|
165
217
|
|
|
166
218
|
pub fn set_observed_prompt_tokens(&mut self, tokens: u32) {
|
|
@@ -168,7 +220,10 @@ impl ContextManager {
|
|
|
168
220
|
}
|
|
169
221
|
|
|
170
222
|
pub fn should_compress(&self) -> PressureAction {
|
|
171
|
-
|
|
223
|
+
// Use effective (paging-aware) rho: once tool results are collapsed/spooled they no longer
|
|
224
|
+
// occupy working context, so they must not keep driving compaction. Equals raw rho when
|
|
225
|
+
// nothing is paged (behavior-preserving) and when provider usage is authoritative.
|
|
226
|
+
self.pressure.recommend(self.effective_rho())
|
|
172
227
|
}
|
|
173
228
|
|
|
174
229
|
pub fn compress(&mut self, action: PressureAction) -> (u32, Option<String>, Vec<Message>) {
|
|
@@ -194,6 +249,11 @@ impl ContextManager {
|
|
|
194
249
|
self.last_compact_ms = Some(ts);
|
|
195
250
|
}
|
|
196
251
|
|
|
252
|
+
// Archived messages have left history — drop their now-orphaned handles (bounds the table).
|
|
253
|
+
if !result.2.is_empty() {
|
|
254
|
+
self.prune_orphaned_handles();
|
|
255
|
+
}
|
|
256
|
+
|
|
197
257
|
result
|
|
198
258
|
}
|
|
199
259
|
|
|
@@ -201,7 +261,47 @@ impl ContextManager {
|
|
|
201
261
|
if self.sections.is_partition_pinned(ContextSectionPartition::History) {
|
|
202
262
|
return (0, None, vec![]);
|
|
203
263
|
}
|
|
204
|
-
self.compression.compress(&mut self.partitions, PressureAction::AutoCompact, self.max_tokens, 0, &self.engine)
|
|
264
|
+
let result = self.compression.compress(&mut self.partitions, PressureAction::AutoCompact, self.max_tokens, 0, &self.engine);
|
|
265
|
+
if !result.2.is_empty() {
|
|
266
|
+
self.prune_orphaned_handles();
|
|
267
|
+
}
|
|
268
|
+
result
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/// W1-1 收口: run one compaction `action` toward an **explicit** `target_tokens`, instead of
|
|
272
|
+
/// re-deriving the target from config. This is what lets `EvictionOp::Collapse { target_tokens }`
|
|
273
|
+
/// flow from the planner (the single decision point) straight to the executor — the compactor no
|
|
274
|
+
/// longer re-decides the target. `compress_with_time` remains the config-derived convenience used
|
|
275
|
+
/// by the other layers (Snip/Micro), whose target equals `config.target_tokens(max_tokens)`.
|
|
276
|
+
pub fn compress_with_target(
|
|
277
|
+
&mut self,
|
|
278
|
+
action: PressureAction,
|
|
279
|
+
target_tokens: u32,
|
|
280
|
+
now_ms: Option<u64>,
|
|
281
|
+
) -> (u32, Option<String>, Vec<Message>) {
|
|
282
|
+
if self.sections.is_partition_pinned(ContextSectionPartition::History) {
|
|
283
|
+
return (0, None, vec![]);
|
|
284
|
+
}
|
|
285
|
+
let result =
|
|
286
|
+
self.compression
|
|
287
|
+
.compress(&mut self.partitions, action, self.max_tokens, target_tokens, &self.engine);
|
|
288
|
+
if let Some(ts) = now_ms {
|
|
289
|
+
self.last_compact_ms = Some(ts);
|
|
290
|
+
}
|
|
291
|
+
if !result.2.is_empty() {
|
|
292
|
+
self.prune_orphaned_handles();
|
|
293
|
+
}
|
|
294
|
+
result
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/// W1-1 收口: the truthful compaction parameters the planner stamps into the [`EvictionPlan`],
|
|
298
|
+
/// read once from config so the ops carry real values (not magic-number placeholders) and the
|
|
299
|
+
/// executor stays a pure executor. Returns `(target_tokens, preserve_recent_turns)`.
|
|
300
|
+
pub fn plan_compaction_params(&self) -> (u32, usize) {
|
|
301
|
+
(
|
|
302
|
+
self.config.target_tokens(self.max_tokens),
|
|
303
|
+
self.config.preserve_recent_turns,
|
|
304
|
+
)
|
|
205
305
|
}
|
|
206
306
|
|
|
207
307
|
// ── Renewal ───────────────────────────────────────────────────────────────
|
|
@@ -216,6 +316,8 @@ impl ContextManager {
|
|
|
216
316
|
self.partitions = renewed;
|
|
217
317
|
self.last_handoff = Some(artifact);
|
|
218
318
|
self.sprint += 1;
|
|
319
|
+
// History was rebuilt wholesale — drop handles anchored to messages it no longer carries.
|
|
320
|
+
self.prune_orphaned_handles();
|
|
219
321
|
}
|
|
220
322
|
|
|
221
323
|
// ── Render ────────────────────────────────────────────────────────────────
|
|
@@ -284,6 +386,13 @@ impl ContextManager {
|
|
|
284
386
|
self.partitions.signals.push(text);
|
|
285
387
|
}
|
|
286
388
|
|
|
389
|
+
/// Record a durable user directive in the (non-compressible, renewal-carried) task_state, so a
|
|
390
|
+
/// mid-task user command keeps its salience across compaction/renewal — unlike the ephemeral
|
|
391
|
+
/// signal channel, which is cleared on renewal.
|
|
392
|
+
pub fn record_directive(&mut self, text: impl Into<String>) {
|
|
393
|
+
self.partitions.task_state.record_directive(text);
|
|
394
|
+
}
|
|
395
|
+
|
|
287
396
|
// ── Task state ────────────────────────────────────────────────────────────
|
|
288
397
|
|
|
289
398
|
pub fn init_task(&mut self, goal: String, criteria: Vec<String>) {
|
|
@@ -651,4 +760,135 @@ mod tests {
|
|
|
651
760
|
mgr.push_history(Message::user("hello"), 5);
|
|
652
761
|
assert_eq!(mgr.handles.all().len(), 1);
|
|
653
762
|
}
|
|
763
|
+
|
|
764
|
+
// ── W1-3: handle-table GC (prune orphaned handles + bounded recompute) ──
|
|
765
|
+
|
|
766
|
+
fn tool_result_msg(call_id: &str, output: &str) -> Message {
|
|
767
|
+
Message::tool(vec![ContentPart::ToolResult {
|
|
768
|
+
call_id: call_id.into(),
|
|
769
|
+
output: output.to_string(),
|
|
770
|
+
is_error: false,
|
|
771
|
+
}])
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
#[test]
|
|
775
|
+
fn effective_rho_discounts_paged_out_handles() {
|
|
776
|
+
let mut mgr = ContextManager::new(1_000);
|
|
777
|
+
// A large tool-result output so its handle carries a real token weight.
|
|
778
|
+
let big = "data ".repeat(200);
|
|
779
|
+
let tok = mgr.engine.count(&big);
|
|
780
|
+
mgr.push_history(tool_result_msg("c0", &big), tok);
|
|
781
|
+
mgr.push_history(Message::user("u"), 50);
|
|
782
|
+
|
|
783
|
+
let raw = mgr.rho();
|
|
784
|
+
// Everything resident → effective equals raw (behavior-preserving when nothing is paged).
|
|
785
|
+
assert_eq!(mgr.handles.non_resident_tokens(), 0);
|
|
786
|
+
assert!((mgr.effective_rho() - raw).abs() < f64::EPSILON);
|
|
787
|
+
|
|
788
|
+
// Page the tool result out of working context.
|
|
789
|
+
mgr.mark_spooled("c0", "disk://c0");
|
|
790
|
+
let paged = mgr.handles.non_resident_tokens();
|
|
791
|
+
assert!(paged > 0, "handle is now non-resident with a real token weight");
|
|
792
|
+
|
|
793
|
+
// Raw rho is unchanged (partitions are untouched by the non-destructive projection)...
|
|
794
|
+
assert!((mgr.rho() - raw).abs() < f64::EPSILON, "raw rho unchanged by paging");
|
|
795
|
+
// ...but effective rho drops by exactly the paged tokens — paging relieves pressure now.
|
|
796
|
+
let total = mgr.partitions.total_tokens(&mgr.engine);
|
|
797
|
+
let expected = total.saturating_sub(paged) as f64 / 1_000.0;
|
|
798
|
+
assert!((mgr.effective_rho() - expected).abs() < f64::EPSILON);
|
|
799
|
+
assert!(mgr.effective_rho() < raw, "effective pressure relieved by paging");
|
|
800
|
+
|
|
801
|
+
// When provider usage is authoritative, the rendered prompt was already collapsed, so
|
|
802
|
+
// effective falls back to raw (no double-discount).
|
|
803
|
+
mgr.set_observed_prompt_tokens(900);
|
|
804
|
+
assert!((mgr.effective_rho() - mgr.rho()).abs() < f64::EPSILON);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
#[test]
|
|
808
|
+
fn prune_orphaned_handles_drops_handles_whose_message_left_history() {
|
|
809
|
+
let mut mgr = ContextManager::new(10_000);
|
|
810
|
+
mgr.push_history(tool_result_msg("c0", "out 0"), 20);
|
|
811
|
+
mgr.push_history(tool_result_msg("c1", "out 1"), 20);
|
|
812
|
+
assert_eq!(mgr.handles.all().len(), 2);
|
|
813
|
+
|
|
814
|
+
// Simulate compaction archiving the oldest tool-result message out of history.
|
|
815
|
+
mgr.partitions.history.messages.remove(0);
|
|
816
|
+
mgr.prune_orphaned_handles();
|
|
817
|
+
|
|
818
|
+
// The handle for the evicted message is gone; the live one is retained.
|
|
819
|
+
assert_eq!(mgr.handles.all().len(), 1);
|
|
820
|
+
assert!(mgr.handles.residency_for_source("c0").is_none());
|
|
821
|
+
assert_eq!(
|
|
822
|
+
mgr.handles.residency_for_source("c1"),
|
|
823
|
+
Some(&Residency::Resident)
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
#[test]
|
|
828
|
+
fn autocompact_prunes_handles_for_archived_tool_results() {
|
|
829
|
+
let mut mgr = ContextManager::new(1_000);
|
|
830
|
+
// Enough oversized tool results to force AutoCompact to archive some.
|
|
831
|
+
for i in 0..30 {
|
|
832
|
+
mgr.push_history(tool_result_msg(&format!("c{i}"), &"x".repeat(200)), 80);
|
|
833
|
+
}
|
|
834
|
+
assert_eq!(mgr.handles.all().len(), 30);
|
|
835
|
+
|
|
836
|
+
let (saved, _, archived) = mgr.compress(PressureAction::AutoCompact);
|
|
837
|
+
assert!(saved > 0 && !archived.is_empty(), "expected archival");
|
|
838
|
+
|
|
839
|
+
// After compaction the table tracks only the tool results still in working history —
|
|
840
|
+
// not the whole session. (No handle outlives its backing message.)
|
|
841
|
+
let live_tool_results = mgr
|
|
842
|
+
.partitions
|
|
843
|
+
.history
|
|
844
|
+
.messages
|
|
845
|
+
.iter()
|
|
846
|
+
.filter(|m| matches!(&m.content, Content::Parts(p)
|
|
847
|
+
if p.iter().any(|x| matches!(x, ContentPart::ToolResult { .. }))))
|
|
848
|
+
.count();
|
|
849
|
+
assert_eq!(mgr.handles.all().len(), live_tool_results);
|
|
850
|
+
assert!(mgr.handles.all().len() < 30, "table must shrink with archival");
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
#[test]
|
|
854
|
+
fn renew_prunes_handles_for_dropped_history() {
|
|
855
|
+
let mut mgr = ContextManager::new(1_000);
|
|
856
|
+
mgr.init_task("g".to_string(), vec![]);
|
|
857
|
+
for i in 0..20 {
|
|
858
|
+
mgr.push_history(tool_result_msg(&format!("c{i}"), "data"), 60);
|
|
859
|
+
}
|
|
860
|
+
mgr.renew();
|
|
861
|
+
// Every retained handle must still be anchored to a message present in the renewed history.
|
|
862
|
+
for h in mgr.handles.all() {
|
|
863
|
+
if let Some(src) = h.source.as_ref() {
|
|
864
|
+
assert!(
|
|
865
|
+
mgr.handles.residency_for_source(src).is_some(),
|
|
866
|
+
"no dangling handle survives renewal"
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
assert!(mgr.handles.all().len() <= 20);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
#[test]
|
|
874
|
+
fn recompute_residency_index_semantics_with_spooled_in_the_middle() {
|
|
875
|
+
// Locks the O(n)-rewrite's index/cutoff semantics against the old id+get_mut version:
|
|
876
|
+
// a spooled handle still occupies an index position but is never toggled.
|
|
877
|
+
let mut mgr = ContextManager::new(1_000);
|
|
878
|
+
for i in 0..6 {
|
|
879
|
+
mgr.push_history(tool_result_msg(&format!("c{i}"), &"y".repeat(40)), 40);
|
|
880
|
+
}
|
|
881
|
+
mgr.mark_spooled("c2", "disk://c2");
|
|
882
|
+
|
|
883
|
+
mgr.set_observed_prompt_tokens(950); // rho >= collapse_threshold
|
|
884
|
+
mgr.recompute_handle_residency();
|
|
885
|
+
|
|
886
|
+
// Spooled stays spooled; the most recent preserve_recent_msgs stay resident; older collapse.
|
|
887
|
+
assert_eq!(
|
|
888
|
+
mgr.handles.residency_for_source("c2"),
|
|
889
|
+
Some(&Residency::SpooledOut { r: "disk://c2".to_string() })
|
|
890
|
+
);
|
|
891
|
+
assert_eq!(mgr.handles.residency_for_source("c0"), Some(&Residency::Collapsed));
|
|
892
|
+
assert_eq!(mgr.handles.residency_for_source("c5"), Some(&Residency::Resident));
|
|
893
|
+
}
|
|
654
894
|
}
|
|
@@ -30,6 +30,14 @@ impl PressureMonitor {
|
|
|
30
30
|
|
|
31
31
|
/// Current pressure rho ∈ [0, +∞).
|
|
32
32
|
/// Uses provider-reported prompt tokens when available; otherwise estimates from partitions.
|
|
33
|
+
///
|
|
34
|
+
/// This is the **raw** rho (full partition weight). Making rho paging-aware — i.e. subtracting
|
|
35
|
+
/// non-resident (`Collapsed`/`SpooledOut`/`PagedOut`) handle tokens so paging immediately relieves
|
|
36
|
+
/// pressure — is **not** a drop-in here: [`crate::context::manager::ContextManager::recompute_handle_residency`]
|
|
37
|
+
/// decides the Resident↔Collapsed projection from this very rho, so subtracting collapsed tokens
|
|
38
|
+
/// would drop rho below `collapse_threshold` and immediately un-collapse (oscillation). That needs
|
|
39
|
+
/// a deliberate split into *raw* rho (drives the collapse decision) vs *effective* rho (drives
|
|
40
|
+
/// further compaction/renewal), tracked as remaining W1-1 design work — see `ContextManager::rho`.
|
|
33
41
|
pub fn pressure(
|
|
34
42
|
&self,
|
|
35
43
|
partitions: &ContextPartitions,
|