cluxion-agentplugin-preprocessing 0.3.14__tar.gz → 0.3.15__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 (95) hide show
  1. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/PKG-INFO +1 -1
  2. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/pyproject.toml +1 -1
  3. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/core/context_compress.py +75 -13
  4. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/tests/runtime/test_context_compress_llm_forget.py +49 -0
  5. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/.github/profile/README.md +0 -0
  6. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/.gitignore +0 -0
  7. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/Docs/README.md +0 -0
  8. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/LICENSE +0 -0
  9. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/README.md +0 -0
  10. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/adapters/claude/.claude-plugin/plugin.json +0 -0
  11. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/adapters/claude/skills/preprocess/SKILL.md +0 -0
  12. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/adapters/codex/config-snippet.toml +0 -0
  13. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/cluxion-Docs/README.md +0 -0
  14. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/cluxion-Docs/architecture.md +0 -0
  15. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/cluxion-Docs/harness-logic.md +0 -0
  16. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/cluxion-Docs/honesty-preprocessing.md +0 -0
  17. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/cluxion-Docs/install-and-operations.md +0 -0
  18. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/cluxion-Docs/security.md +0 -0
  19. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/rust/cluxion_queue/Cargo.lock +0 -0
  20. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/rust/cluxion_queue/Cargo.toml +0 -0
  21. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/rust/cluxion_queue/pyproject.toml +0 -0
  22. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/rust/cluxion_queue/src/context.rs +0 -0
  23. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/rust/cluxion_queue/src/dispatch.rs +0 -0
  24. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/rust/cluxion_queue/src/guard.rs +0 -0
  25. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/rust/cluxion_queue/src/lib.rs +0 -0
  26. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/rust/cluxion_queue/src/main.rs +0 -0
  27. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/rust/cluxion_queue/src/queue.rs +0 -0
  28. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/rust/cluxion_queue/src/types.rs +0 -0
  29. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_agentplugin_preprocessing/__init__.py +0 -0
  30. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_agentplugin_preprocessing/cli.py +0 -0
  31. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_agentplugin_preprocessing/doctor/__init__.py +0 -0
  32. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_agentplugin_preprocessing/doctor/catalog.json +0 -0
  33. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_agentplugin_preprocessing/doctor/framework.py +0 -0
  34. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_agentplugin_preprocessing/doctor/probes.py +0 -0
  35. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_agentplugin_preprocessing/guard_watch.py +0 -0
  36. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_agentplugin_preprocessing/hermes_config.py +0 -0
  37. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_agentplugin_preprocessing/plugin.py +0 -0
  38. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_agentplugin_preprocessing/plugin.yaml +0 -0
  39. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_agentplugin_preprocessing/runner.py +0 -0
  40. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_agentplugin_preprocessing/schemas.py +0 -0
  41. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/__init__.py +0 -0
  42. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/__main__.py +0 -0
  43. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/adapters/__init__.py +0 -0
  44. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/adapters/contract.py +0 -0
  45. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/adapters/grok_build.py +0 -0
  46. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/adapters/hermes.py +0 -0
  47. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/adapters/spec.py +0 -0
  48. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/bootstrap.py +0 -0
  49. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/cli.py +0 -0
  50. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/core/__init__.py +0 -0
  51. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/core/clarification.py +0 -0
  52. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/core/dispatch_store.py +0 -0
  53. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/core/harness.py +0 -0
  54. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/core/hybrid_forget.py +0 -0
  55. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/core/intent.py +0 -0
  56. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/core/ledger.py +0 -0
  57. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/core/ledger_codec.py +0 -0
  58. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/core/llm_compress.py +0 -0
  59. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/core/plan_codec.py +0 -0
  60. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/core/preprocess.py +0 -0
  61. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/core/types.py +0 -0
  62. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/core/work_queue.py +0 -0
  63. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/guard_daemon_host.py +0 -0
  64. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/models/__init__.py +0 -0
  65. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/models/supervisor.py +0 -0
  66. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/models/vllm_mlx.py +0 -0
  67. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/resources/__init__.py +0 -0
  68. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/resources/guard_bridge.py +0 -0
  69. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/resources/py_queue.py +0 -0
  70. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/resources/queue_bridge.py +0 -0
  71. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/resources/rust_bridge.py +0 -0
  72. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/web/__init__.py +0 -0
  73. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/src/cluxion_runtime/web/browser_bridge.py +0 -0
  74. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/tests/runtime/test_auto_compress_middleware.py +0 -0
  75. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/tests/runtime/test_browser_bridge.py +0 -0
  76. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/tests/runtime/test_clarification.py +0 -0
  77. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/tests/runtime/test_cluxion_runtime_spine.py +0 -0
  78. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/tests/runtime/test_context_compress.py +0 -0
  79. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/tests/runtime/test_contract.py +0 -0
  80. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/tests/runtime/test_dispatch_store.py +0 -0
  81. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/tests/runtime/test_guard.py +0 -0
  82. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/tests/runtime/test_guard_daemon_host.py +0 -0
  83. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/tests/runtime/test_ledger.py +0 -0
  84. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/tests/runtime/test_py_queue_concurrency.py +0 -0
  85. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/tests/runtime/test_queue_backends.py +0 -0
  86. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/tests/runtime/test_runtime_adapter_cli.py +0 -0
  87. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/tests/runtime/test_rust_queue.py +0 -0
  88. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/tests/runtime/test_supervisor.py +0 -0
  89. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/tests/test_bootstrap.py +0 -0
  90. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/tests/test_doctor.py +0 -0
  91. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/tests/test_guard_watch.py +0 -0
  92. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/tests/test_hermes_config.py +0 -0
  93. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/tests/test_packaging_policy.py +0 -0
  94. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/tests/test_plugin.py +0 -0
  95. {cluxion_agentplugin_preprocessing-0.3.14 → cluxion_agentplugin_preprocessing-0.3.15}/tests/test_runner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cluxion-agentplugin-preprocessing
3
- Version: 0.3.14
3
+ Version: 0.3.15
4
4
  Summary: Universal agent plugin for Cluxion preprocessing, honesty contracts, clarification, Rust work queue, and resource-aware harness handoff.
5
5
  Project-URL: Homepage, https://github.com/cluxion/cluxion-Agentplugin-preprocessing
6
6
  Project-URL: Repository, https://github.com/cluxion/cluxion-Agentplugin-preprocessing
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cluxion-agentplugin-preprocessing"
7
- version = "0.3.14"
7
+ version = "0.3.15"
8
8
  description = "Universal agent plugin for Cluxion preprocessing, honesty contracts, clarification, Rust work queue, and resource-aware harness handoff."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -6,8 +6,10 @@ lockstep so the three backends produce identical Stage-1 output (parity-tested).
6
6
 
7
7
  Stages 2 (LLM summarization via ``hermes -z``) and 3 (hybrid forgetting) are
8
8
  Python-only; the Rust mirror intentionally does not replicate LLM or forgetforge
9
- calls. Disable them with ``enable_llm_summary`` / ``enable_forget`` for Stage-1
10
- parity.
9
+ calls. Stage 4 (last-resort truncation of pinned recent turns) is also
10
+ Python-only — it runs when every remaining message is pinned yet still exceeds
11
+ the target. Disable stages 2-4 with ``enable_llm_summary`` / ``enable_forget``
12
+ for Stage-1 parity.
11
13
 
12
14
  What stays untouched: pinned messages (explicit ``pinned``, the first
13
15
  user message = task intent, the most recent ``keep_recent`` turns).
@@ -152,13 +154,26 @@ def compress(payload: Mapping[str, object]) -> dict[str, object]:
152
154
  stages.append("forget")
153
155
  pinned = _pinned_indices(messages, keep_recent)
154
156
 
157
+ intent_idx = _first_user_index(messages)
158
+ if total > target_tokens and (
159
+ over_target_pinned_only or not any(idx not in pinned for idx in range(len(messages)))
160
+ ):
161
+ total, changed = _stage_truncate_pinned_recent(
162
+ messages, keep_recent, total, target_tokens, intent_idx=intent_idx
163
+ )
164
+ if changed:
165
+ stages.append("truncate_pinned_recent")
166
+ over_target_pinned_only = False
167
+
155
168
  if total > target_tokens:
156
169
  if summary_request is None:
157
170
  summary_request = _build_summary_request(messages, pinned, total, target_tokens)
158
- if over_target_pinned_only or not any(idx not in pinned for idx in range(len(messages))):
159
- over_target_pinned_only = True
160
- if total / context_limit > trigger_ratio:
171
+ intent_tokens = (
172
+ estimate_tokens(messages[intent_idx].content) if intent_idx is not None else 0
173
+ )
174
+ if intent_tokens > target_tokens:
161
175
  forced_over_target = True
176
+ over_target_pinned_only = True
162
177
 
163
178
  return _result_payload(
164
179
  messages,
@@ -208,6 +223,10 @@ def _bool_flag(payload: Mapping[str, object], key: str, default: bool) -> bool:
208
223
  return default
209
224
 
210
225
 
226
+ def _first_user_index(messages: list[_Msg]) -> int | None:
227
+ return next((idx for idx, msg in enumerate(messages) if msg.role == "user"), None)
228
+
229
+
211
230
  def _pinned_indices(messages: list[_Msg], keep_recent: int) -> list[int]:
212
231
  pinned = [idx for idx, msg in enumerate(messages) if msg.pinned]
213
232
  first_user = next((idx for idx, msg in enumerate(messages) if msg.role == "user"), None)
@@ -221,6 +240,17 @@ def _pinned_indices(messages: list[_Msg], keep_recent: int) -> list[int]:
221
240
  return pinned
222
241
 
223
242
 
243
+ def _apply_head_tail_truncate(content: str) -> str | None:
244
+ if estimate_tokens(content) <= TRUNCATE_MIN_TOKENS:
245
+ return None
246
+ if len(content) <= TRUNCATE_HEAD_CHARS + TRUNCATE_TAIL_CHARS:
247
+ return None
248
+ elided = len(content) - TRUNCATE_HEAD_CHARS - TRUNCATE_TAIL_CHARS
249
+ head = content[:TRUNCATE_HEAD_CHARS]
250
+ tail = content[len(content) - TRUNCATE_TAIL_CHARS :]
251
+ return f"{head}\n[...cluxion: {elided} chars elided...]\n{tail}"
252
+
253
+
224
254
  def _stage_truncate(messages: list[_Msg], pinned: list[int], total: int, target: int) -> tuple[int, bool]:
225
255
  changed = False
226
256
  for idx, msg in enumerate(messages):
@@ -228,21 +258,53 @@ def _stage_truncate(messages: list[_Msg], pinned: list[int], total: int, target:
228
258
  break
229
259
  if idx in pinned:
230
260
  continue
231
- tokens = estimate_tokens(msg.content)
232
- if tokens <= TRUNCATE_MIN_TOKENS:
261
+ replacement = _apply_head_tail_truncate(msg.content)
262
+ if replacement is None:
233
263
  continue
234
- if len(msg.content) <= TRUNCATE_HEAD_CHARS + TRUNCATE_TAIL_CHARS:
235
- continue
236
- elided = len(msg.content) - TRUNCATE_HEAD_CHARS - TRUNCATE_TAIL_CHARS
237
- head = msg.content[:TRUNCATE_HEAD_CHARS]
238
- tail = msg.content[len(msg.content) - TRUNCATE_TAIL_CHARS :]
239
- replacement = f"{head}\n[...cluxion: {elided} chars elided...]\n{tail}"
264
+ tokens = estimate_tokens(msg.content)
240
265
  total = total - tokens + estimate_tokens(replacement)
241
266
  msg.content = replacement
242
267
  changed = True
243
268
  return total, changed
244
269
 
245
270
 
271
+ def _pinned_recent_indices(messages: list[_Msg], keep_recent: int, intent_idx: int | None) -> list[int]:
272
+ recent_start = max(0, len(messages) - keep_recent)
273
+ return [idx for idx in range(recent_start, len(messages)) if idx != intent_idx]
274
+
275
+
276
+ def _stage_truncate_pinned_recent(
277
+ messages: list[_Msg],
278
+ keep_recent: int,
279
+ total: int,
280
+ target: int,
281
+ *,
282
+ intent_idx: int | None,
283
+ ) -> tuple[int, bool]:
284
+ """Last-resort: truncate pinned recent turns (never intent) until total <= target."""
285
+ if total <= target:
286
+ return total, False
287
+
288
+ candidates = _pinned_recent_indices(messages, keep_recent, intent_idx)
289
+ changed = False
290
+ while total > target:
291
+ progressed = False
292
+ for idx in candidates:
293
+ if total <= target:
294
+ break
295
+ replacement = _apply_head_tail_truncate(messages[idx].content)
296
+ if replacement is None:
297
+ continue
298
+ tokens = estimate_tokens(messages[idx].content)
299
+ total = total - tokens + estimate_tokens(replacement)
300
+ messages[idx].content = replacement
301
+ changed = True
302
+ progressed = True
303
+ if not progressed:
304
+ break
305
+ return total, changed
306
+
307
+
246
308
  def _stage_dedup(messages: list[_Msg], pinned: list[int], total: int, target: int) -> tuple[int, bool]:
247
309
  changed = False
248
310
  seen: dict[str, int] = {}
@@ -179,6 +179,55 @@ def test_summarize_messages_returns_none_on_bad_json(monkeypatch) -> None:
179
179
  assert llm_compress.summarize_messages([type("M", (), {"role": "user", "content": "hi"})()], [0]) is None
180
180
 
181
181
 
182
+ def test_pinned_recent_last_resort_brings_under_target(monkeypatch) -> None:
183
+ """Live edge case: all messages pinned and huge — intent preserved, usage <= target."""
184
+ monkeypatch.setattr(context_compress, "hermes_available", lambda: False)
185
+ intent = "TASK_INTENT: implement pinned-overflow guard"
186
+ payload = {
187
+ "messages": [
188
+ {"role": "user", "content": intent + _long(160_000)},
189
+ {"role": "assistant", "content": _long(160_000)},
190
+ {"role": "tool", "content": _long(160_000)},
191
+ {"role": "assistant", "content": _long(160_000)},
192
+ {"role": "user", "content": _long(160_000)},
193
+ ],
194
+ # 5 x ~40k tokens ~ 200k total -> usage 0.80 at this limit (live edge case).
195
+ "context_limit_tokens": 250_000,
196
+ "keep_recent_turns": 4,
197
+ "enable_llm_summary": False,
198
+ "enable_forget": True,
199
+ }
200
+ result = context_compress.compress(payload)
201
+ target = int(0.30 * result["context_limit"])
202
+ assert result["usage_before"] >= 0.70
203
+ assert result["messages"][0]["content"].startswith(intent)
204
+ assert result["tokens_after"] <= target
205
+ assert result.get("over_target_pinned_only") is not True
206
+ assert "truncate_pinned_recent" in result["stages_applied"]
207
+
208
+
209
+ def test_lone_giant_intent_forced_over_target(monkeypatch) -> None:
210
+ """When intent alone exceeds target, truncate everything else and flag forced_over_target."""
211
+ monkeypatch.setattr(context_compress, "hermes_available", lambda: False)
212
+ intent = "GIANT_INTENT"
213
+ payload = {
214
+ "messages": [
215
+ {"role": "user", "content": intent + _long(12_000)},
216
+ {"role": "assistant", "content": _long(12_000)},
217
+ ],
218
+ "context_limit_tokens": 1000,
219
+ "keep_recent_turns": 2,
220
+ "enable_llm_summary": False,
221
+ "enable_forget": True,
222
+ }
223
+ result = context_compress.compress(payload)
224
+ assert result["messages"][0]["content"].startswith(intent)
225
+ assert result.get("forced_over_target") is True
226
+ assert result.get("over_target_pinned_only") is True
227
+ assert "[...cluxion:" in result["messages"][1]["content"]
228
+ assert result["tokens_after"] > int(0.30 * result["context_limit"])
229
+
230
+
182
231
  def test_korean_decision_survives_stage3() -> None:
183
232
  body = _long(4000)
184
233
  digest = f"[cluxion digest] tool: {body[:80]} [900 tokens elided]"