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.
Files changed (165) hide show
  1. {deepstrike-0.2.7 → deepstrike-0.2.9}/Cargo.lock +5 -5
  2. {deepstrike-0.2.7 → deepstrike-0.2.9}/Cargo.toml +2 -2
  3. {deepstrike-0.2.7 → deepstrike-0.2.9}/PKG-INFO +1 -1
  4. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/context/compression.rs +130 -0
  5. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/context/manager.rs +258 -18
  6. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/context/mod.rs +0 -1
  7. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/context/pressure.rs +8 -0
  8. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/context/task_state.rs +90 -0
  9. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/mm/handle.rs +124 -43
  10. deepstrike-0.2.9/crates/deepstrike-core/src/orchestration/mod.rs +8 -0
  11. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/orchestration/task_graph.rs +25 -0
  12. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/orchestration/workflow.rs +137 -3
  13. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/runtime/kernel.rs +59 -224
  14. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/runtime/mod.rs +2 -0
  15. deepstrike-0.2.9/crates/deepstrike-core/src/runtime/snapshot.rs +507 -0
  16. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/scheduler/state_machine.rs +397 -283
  17. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/scheduler/state_machine_tests.rs +607 -35
  18. deepstrike-0.2.9/crates/deepstrike-core/src/scheduler/tcb.rs +1007 -0
  19. deepstrike-0.2.9/crates/deepstrike-core/src/scheduler/workflow_run.rs +909 -0
  20. deepstrike-0.2.9/crates/deepstrike-core/src/types/result.rs +53 -0
  21. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/types/task.rs +1 -1
  22. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-py/src/lib.rs +0 -283
  23. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/__init__.py +0 -7
  24. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/providers/__init__.py +8 -0
  25. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/providers/base.py +14 -0
  26. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/providers/deepseek.py +2 -2
  27. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/providers/minimax.py +2 -2
  28. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/providers/openai.py +24 -10
  29. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/providers/replay.py +18 -0
  30. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/providers/replay_validator.py +52 -21
  31. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/__init__.py +4 -0
  32. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/provider_replay.py +13 -0
  33. {deepstrike-0.2.7 → deepstrike-0.2.9}/pyproject.toml +1 -1
  34. deepstrike-0.2.7/crates/deepstrike-core/src/context/llm_summarizer.rs +0 -240
  35. deepstrike-0.2.7/crates/deepstrike-core/src/orchestration/loop_until_done.rs +0 -281
  36. deepstrike-0.2.7/crates/deepstrike-core/src/orchestration/mod.rs +0 -7
  37. deepstrike-0.2.7/crates/deepstrike-core/src/scheduler/tcb.rs +0 -416
  38. deepstrike-0.2.7/crates/deepstrike-core/src/scheduler/workflow_run.rs +0 -375
  39. deepstrike-0.2.7/crates/deepstrike-core/src/types/result.rs +0 -37
  40. deepstrike-0.2.7/deepstrike/workflow.py +0 -54
  41. {deepstrike-0.2.7 → deepstrike-0.2.9}/README.md +0 -0
  42. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/Cargo.toml +0 -0
  43. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/context/config.rs +0 -0
  44. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/context/dashboard.rs +0 -0
  45. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/context/partitions.rs +0 -0
  46. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/context/renderer.rs +0 -0
  47. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/context/renewal.rs +0 -0
  48. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/context/sections.rs +0 -0
  49. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/context/skill_catalog.rs +0 -0
  50. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/context/snapshot.rs +0 -0
  51. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/context/summarizer.rs +0 -0
  52. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/context/text.rs +0 -0
  53. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/context/token_engine.rs +0 -0
  54. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/governance/audit.rs +0 -0
  55. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/governance/constraint.rs +0 -0
  56. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/governance/mod.rs +0 -0
  57. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/governance/permission.rs +0 -0
  58. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/governance/pipeline.rs +0 -0
  59. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/governance/quota.rs +0 -0
  60. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/governance/rate_limit.rs +0 -0
  61. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/governance/sandbox.rs +0 -0
  62. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/governance/tool_decision.rs +0 -0
  63. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/governance/veto.rs +0 -0
  64. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/harness/eval_pipeline.rs +0 -0
  65. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/harness/mod.rs +0 -0
  66. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/lib.rs +0 -0
  67. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/memory/curator.rs +0 -0
  68. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/memory/durable.rs +0 -0
  69. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/memory/extractor.rs +0 -0
  70. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/memory/idle_pipeline.rs +0 -0
  71. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/memory/mod.rs +0 -0
  72. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/memory/runtime.rs +0 -0
  73. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/memory/semantic.rs +0 -0
  74. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/memory/session.rs +0 -0
  75. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/memory/synthesis.rs +0 -0
  76. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/memory/trace_analyzer.rs +0 -0
  77. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/memory/working.rs +0 -0
  78. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/mm/memory.rs +0 -0
  79. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/mm/mod.rs +0 -0
  80. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/orchestration/executor.rs +0 -0
  81. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/orchestration/gen_eval.rs +0 -0
  82. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/orchestration/planner.rs +0 -0
  83. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/orchestration/tournament.rs +0 -0
  84. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/proc/mod.rs +0 -0
  85. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/runtime/event_log.rs +0 -0
  86. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/runtime/repair.rs +0 -0
  87. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/runtime/replay.rs +0 -0
  88. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/runtime/session.rs +0 -0
  89. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/scheduler/milestone.rs +0 -0
  90. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/scheduler/mod.rs +0 -0
  91. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/scheduler/policy.rs +0 -0
  92. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/scheduler/rollback.rs +0 -0
  93. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/signals/attention.rs +0 -0
  94. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/signals/mod.rs +0 -0
  95. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/signals/queue.rs +0 -0
  96. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/signals/router.rs +0 -0
  97. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/syscall/mod.rs +0 -0
  98. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/types/agent.rs +0 -0
  99. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/types/capability.rs +0 -0
  100. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/types/contract.rs +0 -0
  101. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/types/error.rs +0 -0
  102. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/types/message.rs +0 -0
  103. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/types/milestone.rs +0 -0
  104. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/types/mod.rs +0 -0
  105. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/types/model.rs +0 -0
  106. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/types/policy.rs +0 -0
  107. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/types/signal.rs +0 -0
  108. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-core/src/types/skill.rs +0 -0
  109. {deepstrike-0.2.7 → deepstrike-0.2.9}/crates/deepstrike-py/Cargo.toml +0 -0
  110. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/collaboration/__init__.py +0 -0
  111. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/collaboration/contract.py +0 -0
  112. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/collaboration/handoff.py +0 -0
  113. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/collaboration/harness.py +0 -0
  114. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/collaboration/modes.py +0 -0
  115. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/collaboration/pool.py +0 -0
  116. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/governance.py +0 -0
  117. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/harness/__init__.py +0 -0
  118. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/harness/harness.py +0 -0
  119. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/kernel/__init__.py +0 -0
  120. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/knowledge/__init__.py +0 -0
  121. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/knowledge/source.py +0 -0
  122. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/memory/__init__.py +0 -0
  123. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/memory/agent.py +0 -0
  124. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/memory/protocols.py +0 -0
  125. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/memory/working.py +0 -0
  126. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/providers/anthropic.py +0 -0
  127. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/providers/gemini.py +0 -0
  128. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/providers/glm.py +0 -0
  129. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/providers/kimi.py +0 -0
  130. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/providers/ollama.py +0 -0
  131. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/providers/qwen.py +0 -0
  132. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/providers/stream.py +0 -0
  133. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/archive.py +0 -0
  134. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/credential_vault.py +0 -0
  135. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/execution_plane.py +0 -0
  136. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/filtered_plane.py +0 -0
  137. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/kernel_event_log.py +0 -0
  138. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/kernel_step.py +0 -0
  139. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/large_result_spool.py +0 -0
  140. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/mcp_proxy_plane.py +0 -0
  141. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/os_profile.py +0 -0
  142. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/os_snapshot.py +0 -0
  143. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/process_sandbox_plane.py +0 -0
  144. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/remote_vpc_plane.py +0 -0
  145. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/replay_sanitize.py +0 -0
  146. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/runner.py +0 -0
  147. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/session_log.py +0 -0
  148. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/session_repair.py +0 -0
  149. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/runtime/sub_agent_orchestrator.py +0 -0
  150. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/safety/__init__.py +0 -0
  151. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/safety/permissions.py +0 -0
  152. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/signals/__init__.py +0 -0
  153. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/signals/gateway.py +0 -0
  154. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/signals/scheduled.py +0 -0
  155. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/signals/types.py +0 -0
  156. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/skills/__init__.py +0 -0
  157. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/skills/registry.py +0 -0
  158. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/skills/watcher.py +0 -0
  159. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/tools/__init__.py +0 -0
  160. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/tools/builtin/__init__.py +0 -0
  161. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/tools/builtin/read_file.py +0 -0
  162. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/tools/execution.py +0 -0
  163. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/tools/registry.py +0 -0
  164. {deepstrike-0.2.7 → deepstrike-0.2.9}/deepstrike/types/__init__.py +0 -0
  165. {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.7"
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.7"
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.7"
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.7"
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.7"
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.7"
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.7" }
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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepstrike
3
- Version: 0.2.7
3
+ Version: 0.2.9
4
4
  Requires-Dist: httpx>=0.27
5
5
  Requires-Dist: pyyaml>=6.0
6
6
  Requires-Dist: anyio>=4.0
@@ -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
- let ids: Vec<HandleId> = self
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
- .map(|h| h.id)
128
- .collect();
129
- let cutoff = ids.len().saturating_sub(keep);
130
- for (i, id) in ids.iter().enumerate() {
131
- if let Some(handle) = self.handles.get_mut(*id) {
132
- // Only toggle the reversible Resident<->Collapsed axis; never clobber a handle
133
- // that has been spooled or paged out.
134
- if matches!(handle.residency, Residency::Resident | Residency::Collapsed) {
135
- handle.residency = if collapse && i < cutoff {
136
- Residency::Collapsed
137
- } else {
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.pressure(&self.partitions, &self.engine, self.last_observed_prompt_tokens)
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
- self.pressure.recommend(self.rho())
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
  }
@@ -13,4 +13,3 @@ pub mod summarizer;
13
13
  pub mod task_state;
14
14
  pub mod text;
15
15
  pub mod token_engine;
16
- pub mod llm_summarizer;
@@ -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,