cluxion-agentplugin-preprocessing 0.3.17__tar.gz → 0.3.18__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.17 → cluxion_agentplugin_preprocessing-0.3.18}/PKG-INFO +1 -1
  2. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/pyproject.toml +1 -1
  3. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_agentplugin_preprocessing/guard_watch.py +7 -1
  4. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_agentplugin_preprocessing/plugin.py +1 -0
  5. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_agentplugin_preprocessing/plugin.yaml +12 -3
  6. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_agentplugin_preprocessing/runner.py +14 -1
  7. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/cli.py +18 -3
  8. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/core/llm_compress.py +18 -1
  9. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/guard_daemon_host.py +44 -6
  10. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/resources/queue_bridge.py +61 -4
  11. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/tests/runtime/test_context_compress.py +53 -1
  12. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/tests/runtime/test_context_compress_llm_forget.py +45 -0
  13. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/tests/runtime/test_guard_daemon_host.py +26 -3
  14. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/tests/test_guard_watch.py +1 -0
  15. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/tests/test_runner.py +8 -0
  16. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/.github/profile/README.md +0 -0
  17. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/.gitignore +0 -0
  18. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/Docs/README.md +0 -0
  19. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/LICENSE +0 -0
  20. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/README.md +0 -0
  21. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/adapters/claude/.claude-plugin/plugin.json +0 -0
  22. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/adapters/claude/skills/preprocess/SKILL.md +0 -0
  23. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/adapters/codex/config-snippet.toml +0 -0
  24. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/cluxion-Docs/README.md +0 -0
  25. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/cluxion-Docs/architecture.md +0 -0
  26. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/cluxion-Docs/harness-logic.md +0 -0
  27. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/cluxion-Docs/honesty-preprocessing.md +0 -0
  28. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/cluxion-Docs/install-and-operations.md +0 -0
  29. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/cluxion-Docs/security.md +0 -0
  30. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/rust/cluxion_queue/Cargo.lock +0 -0
  31. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/rust/cluxion_queue/Cargo.toml +0 -0
  32. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/rust/cluxion_queue/pyproject.toml +0 -0
  33. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/rust/cluxion_queue/src/context.rs +0 -0
  34. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/rust/cluxion_queue/src/dispatch.rs +0 -0
  35. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/rust/cluxion_queue/src/guard.rs +0 -0
  36. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/rust/cluxion_queue/src/lib.rs +0 -0
  37. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/rust/cluxion_queue/src/main.rs +0 -0
  38. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/rust/cluxion_queue/src/queue.rs +0 -0
  39. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/rust/cluxion_queue/src/types.rs +0 -0
  40. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_agentplugin_preprocessing/__init__.py +0 -0
  41. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_agentplugin_preprocessing/cli.py +0 -0
  42. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_agentplugin_preprocessing/doctor/__init__.py +0 -0
  43. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_agentplugin_preprocessing/doctor/catalog.json +0 -0
  44. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_agentplugin_preprocessing/doctor/framework.py +0 -0
  45. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_agentplugin_preprocessing/doctor/probes.py +0 -0
  46. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_agentplugin_preprocessing/hermes_config.py +0 -0
  47. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_agentplugin_preprocessing/schemas.py +0 -0
  48. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/__init__.py +0 -0
  49. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/__main__.py +0 -0
  50. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/adapters/__init__.py +0 -0
  51. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/adapters/contract.py +0 -0
  52. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/adapters/grok_build.py +0 -0
  53. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/adapters/hermes.py +0 -0
  54. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/adapters/spec.py +0 -0
  55. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/bootstrap.py +0 -0
  56. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/core/__init__.py +0 -0
  57. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/core/clarification.py +0 -0
  58. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/core/context_compress.py +0 -0
  59. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/core/dispatch_store.py +0 -0
  60. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/core/harness.py +0 -0
  61. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/core/hybrid_forget.py +0 -0
  62. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/core/intent.py +0 -0
  63. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/core/ledger.py +0 -0
  64. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/core/ledger_codec.py +0 -0
  65. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/core/plan_codec.py +0 -0
  66. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/core/preprocess.py +0 -0
  67. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/core/types.py +0 -0
  68. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/core/work_queue.py +0 -0
  69. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/models/__init__.py +0 -0
  70. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/models/supervisor.py +0 -0
  71. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/models/vllm_mlx.py +0 -0
  72. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/resources/__init__.py +0 -0
  73. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/resources/guard_bridge.py +0 -0
  74. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/resources/py_queue.py +0 -0
  75. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/resources/rust_bridge.py +0 -0
  76. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/web/__init__.py +0 -0
  77. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/src/cluxion_runtime/web/browser_bridge.py +0 -0
  78. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/tests/runtime/test_auto_compress_middleware.py +0 -0
  79. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/tests/runtime/test_browser_bridge.py +0 -0
  80. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/tests/runtime/test_clarification.py +0 -0
  81. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/tests/runtime/test_cluxion_runtime_spine.py +0 -0
  82. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/tests/runtime/test_contract.py +0 -0
  83. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/tests/runtime/test_dispatch_store.py +0 -0
  84. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/tests/runtime/test_guard.py +0 -0
  85. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/tests/runtime/test_ledger.py +0 -0
  86. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/tests/runtime/test_py_queue_concurrency.py +0 -0
  87. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/tests/runtime/test_queue_backends.py +0 -0
  88. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/tests/runtime/test_runtime_adapter_cli.py +0 -0
  89. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/tests/runtime/test_rust_queue.py +0 -0
  90. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/tests/runtime/test_supervisor.py +0 -0
  91. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/tests/test_bootstrap.py +0 -0
  92. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/tests/test_doctor.py +0 -0
  93. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/tests/test_hermes_config.py +0 -0
  94. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/tests/test_packaging_policy.py +0 -0
  95. {cluxion_agentplugin_preprocessing-0.3.17 → cluxion_agentplugin_preprocessing-0.3.18}/tests/test_plugin.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cluxion-agentplugin-preprocessing
3
- Version: 0.3.17
3
+ Version: 0.3.18
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.17"
7
+ version = "0.3.18"
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"
@@ -26,6 +26,12 @@ _last_watch_at: float | None = None
26
26
  _last_warning_at: float | None = None
27
27
 
28
28
 
29
+ def on_session_end(**_: Any) -> None:
30
+ """Stop the guard daemon on clean session end so orphans do not linger."""
31
+ with contextlib.suppress(Exception):
32
+ guard_bridge.stop_daemon()
33
+
34
+
29
35
  def on_session_start(**_: Any) -> None:
30
36
  """Start the guard daemon unless ``CLUXION_GUARD_AUTOSTART=0`` or ``false``.
31
37
 
@@ -111,4 +117,4 @@ def _warn(message: str) -> None:
111
117
  print(message, file=sys.stderr)
112
118
 
113
119
 
114
- __all__ = ["on_session_start", "post_tool_call"]
120
+ __all__ = ["on_session_end", "on_session_start", "post_tool_call"]
@@ -43,6 +43,7 @@ def register(ctx: object) -> None:
43
43
  register_hook = getattr(ctx, "register_hook", None)
44
44
  if callable(register_hook):
45
45
  register_hook("on_session_start", guard_watch.on_session_start)
46
+ register_hook("on_session_end", guard_watch.on_session_end)
46
47
  register_hook("post_tool_call", guard_watch.post_tool_call)
47
48
 
48
49
  register_mw = getattr(ctx, "register_middleware", None)
@@ -1,13 +1,22 @@
1
- name: hermes-cluxion
2
- version: 0.1.9
1
+ name: cluxion-agentplugin-preprocessing
2
+ version: 0.3.18
3
3
  description: "Universal agent preprocessing plugin: honesty contracts, clarification, Rust work queue, resource-aware harness handoff. Connected AI calls cluxion tools; plugin does not own models."
4
4
  author: cluxion
5
5
  kind: standalone
6
6
  provides_tools:
7
- - cluxion_bootstrap
8
7
  - cluxion_plan
8
+ - cluxion_clarify
9
+ - cluxion_bootstrap
9
10
  - cluxion_serve_local
10
11
  - cluxion_hermes_config
11
12
  - cluxion_queue_next
12
13
  - cluxion_queue_record
13
14
  - cluxion_queue_brief
15
+ - cluxion_context_compress
16
+ - cluxion_guard
17
+ - cluxion_web_search
18
+ - cluxion_browser_open
19
+ - cluxion_browser_extract
20
+ - cluxion_browser_click
21
+ - cluxion_browser_type
22
+ - cluxion_doctor
@@ -247,10 +247,23 @@ def _execute_json(command: Sequence[str], stdin: str | None, command_runner: Com
247
247
  except OSError as exc:
248
248
  return RuntimeResult(False, tuple(command), {"error": str(exc)})
249
249
  if completed.returncode != 0:
250
+ error = "cluxion-runtime failed"
251
+ stderr = completed.stderr.strip()
252
+ stdout = completed.stdout.strip()
253
+ for payload_text in (stderr, stdout):
254
+ if not payload_text:
255
+ continue
256
+ try:
257
+ parsed_err = json.loads(payload_text)
258
+ except json.JSONDecodeError:
259
+ continue
260
+ if isinstance(parsed_err, dict) and parsed_err.get("error"):
261
+ error = str(parsed_err["error"])
262
+ break
250
263
  return RuntimeResult(
251
264
  False,
252
265
  tuple(command),
253
- {"error": "cluxion-runtime failed", "stderr": completed.stderr.strip(), "returncode": completed.returncode},
266
+ {"error": error, "stderr": stderr, "returncode": completed.returncode},
254
267
  )
255
268
  try:
256
269
  parsed = json.loads(completed.stdout)
@@ -206,11 +206,27 @@ def _run_context_compress(_: argparse.Namespace) -> int:
206
206
  return 0
207
207
 
208
208
 
209
+ _VALID_GUARD_ACTIONS = frozenset({"status", "start", "stop", "enforce", "auto-enforce"})
210
+
211
+
212
+ def _guard_action_error(action: str) -> str:
213
+ return (
214
+ f"unknown guard action: {action} "
215
+ "(expected: status|start|stop|enforce|auto-enforce)"
216
+ )
217
+
218
+
209
219
  def _run_guard(_: argparse.Namespace) -> int:
210
220
  from cluxion_runtime.resources import guard_bridge
211
221
 
212
222
  payload = _payload_from_stdin()
213
223
  action = str(payload.get("action", "status"))
224
+ if action not in _VALID_GUARD_ACTIONS:
225
+ print(
226
+ json.dumps({"ok": False, "error": _guard_action_error(action)}, ensure_ascii=False, sort_keys=True),
227
+ file=sys.stderr,
228
+ )
229
+ return 1
214
230
  try:
215
231
  if action == "start":
216
232
  result = guard_bridge.start_daemon(
@@ -256,9 +272,8 @@ def _run_guard(_: argparse.Namespace) -> int:
256
272
  result["scan"] = guard_bridge.scan([int(pid) for pid in owned_roots])
257
273
  else:
258
274
  print(
259
- json.dumps(
260
- {"ok": False, "error": f"unknown guard action: {action}"}, ensure_ascii=False, sort_keys=True
261
- )
275
+ json.dumps({"ok": False, "error": _guard_action_error(action)}, ensure_ascii=False, sort_keys=True),
276
+ file=sys.stderr,
262
277
  )
263
278
  return 1
264
279
  except RuntimeError as exc:
@@ -35,13 +35,15 @@ _SUMMARY_INSTRUCTIONS = (
35
35
 
36
36
  _HARD_TOKEN_RE = re.compile(
37
37
  r"\b(?:"
38
- r"\d+(?:\.\d+)?(?:k|m|만|억)?"
38
+ r"\d+(?:[._]\d+)+(?:k|m|만|억)?"
39
+ r"|\d+(?:\.\d+)?(?:k|m|만|억)?"
39
40
  r"|[A-Za-z][\w.-]*\d[\w.-]*"
40
41
  r"|\d[\w.-]+"
41
42
  r")\b",
42
43
  re.IGNORECASE,
43
44
  )
44
45
  _NUMERIC_SUFFIX_RE = re.compile(r"^(\d+(?:\.\d+)?)(k|m|만|억)?$", re.IGNORECASE)
46
+ _MULTI_GROUP_NUMERIC_RE = re.compile(r"^\d+(?:[._]\d+)+$")
45
47
  _STRIP_LABEL_PREFIX_RE = re.compile(r"(?:\w+:\s*)+", re.IGNORECASE)
46
48
  _SUFFIX_MULTIPLIERS = {"k": 1000, "m": 1_000_000, "만": 10_000, "억": 100_000_000}
47
49
 
@@ -129,10 +131,25 @@ def _numeric_variants(token: str) -> set[str]:
129
131
  return variants
130
132
 
131
133
 
134
+ def _bounded_numeric_in_source(norm_token: str, norm_source: str) -> bool:
135
+ """Match a dotted/underscored numeric token only as a complete identifier."""
136
+ pattern = rf"(?<![\d._]){re.escape(norm_token)}(?![\d._])"
137
+ return re.search(pattern, norm_source) is not None
138
+
139
+
132
140
  def _token_traceable_in_source(token: str, source: str) -> bool:
133
141
  norm_source = _normalize_for_match(source)
134
142
  norm_token = _normalize_for_match(token)
135
143
 
144
+ # Version/IP/port-like tokens must match atomically — never as a substring prefix
145
+ # of a longer value (e.g. 0.3.1 must not trace to source 0.3.17).
146
+ if _MULTI_GROUP_NUMERIC_RE.match(norm_token):
147
+ if _bounded_numeric_in_source(norm_token, norm_source):
148
+ return True
149
+ return any(
150
+ _bounded_numeric_in_source(variant, norm_source) for variant in _numeric_variants(token)
151
+ )
152
+
136
153
  if norm_token in norm_source:
137
154
  return True
138
155
 
@@ -15,8 +15,9 @@ import psutil
15
15
  STATE_FILE_NAME = "guard_state.json"
16
16
  HEARTBEAT_FILE_NAME = "guard_heartbeat"
17
17
  PID_FILE_NAME = "guard_daemon.pid"
18
- DEFAULT_IDLE_TTL_MS = 600_000
18
+ DEFAULT_IDLE_TTL_MS = 120_000
19
19
  PROC_SCAN_EVERY_N_TICKS = 5
20
+ STATE_WRITE_EVERY_N_TICKS = 30
20
21
  _MAX_REPORTED_PIDS = 50
21
22
  _MIN_INTERVAL_MS = 100
22
23
  _MIN_WINDOW = 1
@@ -145,6 +146,35 @@ def _write_state_atomically(store_dir: Path, state: dict[str, Any]) -> None:
145
146
  os.replace(tmp_path, state_path)
146
147
 
147
148
 
149
+ def _state_write_fingerprint(state: dict[str, Any]) -> str:
150
+ """Stable snapshot key excluding volatile timestamps."""
151
+ current = dict(state.get("current", {}))
152
+ current.pop("sampled_at_ms", None)
153
+ window = state.get("window")
154
+ payload = {
155
+ "current": current,
156
+ "window": window,
157
+ "interval_ms": state.get("interval_ms"),
158
+ }
159
+ return json.dumps(payload, separators=(",", ":"), sort_keys=True)
160
+
161
+
162
+ def _write_state_if_changed(
163
+ store_dir: Path,
164
+ state: dict[str, Any],
165
+ *,
166
+ last_fingerprint: str | None,
167
+ tick: int,
168
+ ) -> str:
169
+ fingerprint = _state_write_fingerprint(state)
170
+ if (
171
+ fingerprint != last_fingerprint
172
+ or tick % STATE_WRITE_EVERY_N_TICKS == 0
173
+ ):
174
+ _write_state_atomically(store_dir, state)
175
+ return fingerprint
176
+
177
+
148
178
  def _idle_ttl_ms() -> int:
149
179
  raw = os.environ.get("CLUXION_GUARD_IDLE_TTL_MS")
150
180
  if raw is None:
@@ -192,10 +222,11 @@ def _daemon_loop_step(
192
222
  interval_ms: int,
193
223
  tick: int,
194
224
  idle_ttl_ms: int,
225
+ last_fingerprint: str | None = None,
195
226
  now_ms: int | None = None,
196
- ) -> tuple[bool, ProcessScanCache]:
227
+ ) -> tuple[bool, ProcessScanCache, str]:
197
228
  if _check_idle_exit(base, idle_ttl_ms, now_ms=now_ms):
198
- return False, process_cache
229
+ return False, process_cache, last_fingerprint or ""
199
230
  state, process_cache = _python_daemon_tick(
200
231
  process_cache,
201
232
  cpu_window,
@@ -204,8 +235,13 @@ def _daemon_loop_step(
204
235
  interval_ms,
205
236
  tick,
206
237
  )
207
- _write_state_atomically(base, state)
208
- return True, process_cache
238
+ fingerprint = _write_state_if_changed(
239
+ base,
240
+ state,
241
+ last_fingerprint=last_fingerprint,
242
+ tick=tick,
243
+ )
244
+ return True, process_cache, fingerprint
209
245
 
210
246
 
211
247
  def _run_python_daemon(store_dir: str, interval_ms: int, window: int) -> None:
@@ -219,9 +255,10 @@ def _run_python_daemon(store_dir: str, interval_ms: int, window: int) -> None:
219
255
  cpu_window: list[float] = []
220
256
  ram_window: list[int] = []
221
257
  tick = 0
258
+ last_fingerprint: str | None = None
222
259
 
223
260
  while True:
224
- keep_running, process_cache = _daemon_loop_step(
261
+ keep_running, process_cache, last_fingerprint = _daemon_loop_step(
225
262
  base,
226
263
  process_cache=process_cache,
227
264
  cpu_window=cpu_window,
@@ -230,6 +267,7 @@ def _run_python_daemon(store_dir: str, interval_ms: int, window: int) -> None:
230
267
  interval_ms=interval_ms,
231
268
  tick=tick,
232
269
  idle_ttl_ms=idle_ttl_ms,
270
+ last_fingerprint=last_fingerprint,
233
271
  )
234
272
  if not keep_running:
235
273
  return
@@ -114,15 +114,72 @@ def queue_status(*, store_dir: Path | None = None) -> dict[str, object]:
114
114
 
115
115
  def compress_context(payload: Mapping[str, object]) -> dict[str, object]:
116
116
  """Deterministic context compression — pure function, no store_dir involved."""
117
+ from cluxion_runtime.core import context_compress
118
+
117
119
  body = dict(payload)
118
120
  backend = resolve_backend()
121
+ if backend == "python":
122
+ return context_compress.compress(body)
119
123
  if backend == "native":
120
- return _invoke_native("context-compress", body)
121
- if backend == "subprocess":
122
- return _invoke_subprocess("context-compress", body)
124
+ stage1 = _invoke_native("context-compress", body)
125
+ else:
126
+ stage1 = _invoke_subprocess("context-compress", body)
127
+ return _finalize_context_compress(body, stage1)
128
+
129
+
130
+ def _context_trigger_ratio(payload: Mapping[str, object]) -> float:
131
+ from cluxion_runtime.core.context_compress import DEFAULT_TRIGGER_RATIO
132
+
133
+ value = payload.get("trigger_ratio")
134
+ if isinstance(value, (int, float)) and not isinstance(value, bool) and 0.0 < float(value) < 1.0:
135
+ return float(value)
136
+ return DEFAULT_TRIGGER_RATIO
137
+
138
+
139
+ def _context_bool_flag(payload: Mapping[str, object], key: str, default: bool) -> bool:
140
+ value = payload.get(key)
141
+ if isinstance(value, bool):
142
+ return value
143
+ return default
144
+
145
+
146
+ def _finalize_context_compress(body: Mapping[str, object], stage1: dict[str, object]) -> dict[str, object]:
147
+ """After Rust Stage-1, continue the Python pipeline when still above trigger."""
123
148
  from cluxion_runtime.core import context_compress
124
149
 
125
- return context_compress.compress(body)
150
+ trigger_ratio = _context_trigger_ratio(body)
151
+ usage_after = float(stage1.get("usage_after", 0))
152
+ if usage_after <= trigger_ratio:
153
+ stage1["reached_target"] = True
154
+ return stage1
155
+
156
+ enable_llm = _context_bool_flag(body, "enable_llm_summary", True)
157
+ enable_forget = _context_bool_flag(body, "enable_forget", True)
158
+ if not enable_llm and not enable_forget:
159
+ stage1["reached_target"] = False
160
+ if stage1.get("ai_summary_request"):
161
+ stage1["requires_summary"] = True
162
+ return stage1
163
+
164
+ continued_body = dict(body)
165
+ messages = stage1.get("messages")
166
+ if isinstance(messages, list):
167
+ continued_body["messages"] = messages
168
+ continued = context_compress.compress(continued_body)
169
+ continued_usage = float(continued.get("usage_after", 1.0))
170
+ continued["reached_target"] = continued_usage <= trigger_ratio
171
+ if not continued["reached_target"] and continued.get("ai_summary_request"):
172
+ continued["requires_summary"] = True
173
+
174
+ stage1_stages = stage1.get("stages_applied")
175
+ continued_stages = continued.get("stages_applied")
176
+ if isinstance(stage1_stages, list) and isinstance(continued_stages, list):
177
+ merged = list(stage1_stages)
178
+ for stage in continued_stages:
179
+ if stage not in merged:
180
+ merged.append(stage)
181
+ continued["stages_applied"] = merged
182
+ return continued
126
183
 
127
184
 
128
185
  def _invoke(command: str, payload: Mapping[str, object], *, store_dir: Path | None) -> dict[str, object]:
@@ -121,7 +121,59 @@ def test_model_registry_resolution(backend) -> None:
121
121
  def test_backend_matches_python_reference(backend) -> None:
122
122
  reference = context_compress.compress(dict(_COMPRESSIBLE))
123
123
  result = queue_bridge.compress_context(_COMPRESSIBLE)
124
- assert result == reference
124
+ if backend == "python":
125
+ assert result == reference
126
+ else:
127
+ # Stage-1 backends may add reached_target when continuing the pipeline.
128
+ result_compare = {k: v for k, v in result.items() if k not in {"reached_target", "requires_summary"}}
129
+ reference_compare = dict(reference)
130
+ assert result_compare == reference_compare
131
+
132
+
133
+ def test_tool_path_continues_when_stage1_above_trigger(monkeypatch) -> None:
134
+ """Rust Stage-1-only output above trigger must continue into Python pipeline."""
135
+ messages = [
136
+ {"role": "user", "content": "task intent"},
137
+ {"role": "assistant", "content": _long(80_000)},
138
+ {"role": "tool", "content": _long(80_000)},
139
+ {"role": "user", "content": "recent question"},
140
+ ]
141
+ full_payload = {
142
+ "messages": messages,
143
+ "context_limit_tokens": 40_000,
144
+ "keep_recent_turns": 1,
145
+ "enable_llm_summary": False,
146
+ "enable_forget": True,
147
+ }
148
+ stage1 = {
149
+ "ok": True,
150
+ "compressed": True,
151
+ "tokens_before": 90_000,
152
+ "tokens_after": 34_000,
153
+ "usage_before": 2.25,
154
+ "usage_after": 0.85,
155
+ "context_limit": 40_000,
156
+ "stages_applied": ["truncate", "digest"],
157
+ "pinned_indices": [0, 3],
158
+ "messages": messages,
159
+ "ai_summary_request": {
160
+ "reason": "deterministic stages insufficient",
161
+ "current_tokens": 34_000,
162
+ "target_tokens": 12_000,
163
+ "summarize_indices": [1, 2],
164
+ "instructions": "summarize",
165
+ },
166
+ }
167
+ assert float(stage1["usage_after"]) > 0.70
168
+
169
+ monkeypatch.setenv(queue_bridge.QUEUE_BACKEND_ENV, "native")
170
+ monkeypatch.setattr(queue_bridge, "_invoke_native", lambda *a, **k: dict(stage1))
171
+ monkeypatch.setattr(context_compress, "hermes_available", lambda: False)
172
+
173
+ result = queue_bridge.compress_context(full_payload)
174
+ trigger = 0.70
175
+ assert result.get("reached_target") is True
176
+ assert float(result["usage_after"]) <= trigger
125
177
 
126
178
 
127
179
  def test_missing_messages_raises(backend) -> None:
@@ -2,8 +2,11 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import json
5
6
  import subprocess
6
7
 
8
+ import pytest
9
+
7
10
  from cluxion_runtime.core import context_compress
8
11
  from cluxion_runtime.core.context_compress import _Msg
9
12
  from cluxion_runtime.core.hybrid_forget import _cold_demote, apply_hybrid_forget
@@ -270,6 +273,48 @@ def test_hallucination_guard_keeps_korean_normalized_number(monkeypatch) -> None
270
273
  assert "5433" in result[0]
271
274
 
272
275
 
276
+ @pytest.mark.parametrize(
277
+ "fabricated",
278
+ ["0.3.7", "0.3.1", "3.17", "0.17", "0.31.7", "1.3.17"],
279
+ )
280
+ def test_hallucination_guard_strips_fabricated_version_parts(fabricated: str, monkeypatch) -> None:
281
+ from cluxion_runtime.core import llm_compress
282
+
283
+ source = "deployed version 0.3.17 of the package"
284
+ llm_json = json.dumps({"0": f"Package version {fabricated} deployed."})
285
+ monkeypatch.setattr(llm_compress, "hermes_available", lambda: True)
286
+ monkeypatch.setattr(llm_compress, "_call_hermes_oneshot", lambda *a, **k: llm_json)
287
+
288
+ result = llm_compress.summarize_messages([_msg(source)], [0])
289
+ assert result is not None
290
+ assert fabricated not in result[0]
291
+ assert "0.3.17" in result[0] or "version" in result[0].lower()
292
+
293
+
294
+ def test_hallucination_guard_keeps_exact_version(monkeypatch) -> None:
295
+ from cluxion_runtime.core import llm_compress
296
+
297
+ source = "deployed version 0.3.17 of the package"
298
+ llm_json = '{"0": "Deployed version 0.3.17."}'
299
+ monkeypatch.setattr(llm_compress, "hermes_available", lambda: True)
300
+ monkeypatch.setattr(llm_compress, "_call_hermes_oneshot", lambda *a, **k: llm_json)
301
+
302
+ result = llm_compress.summarize_messages([_msg(source)], [0])
303
+ assert result is not None
304
+ assert "0.3.17" in result[0]
305
+
306
+
307
+ def test_apply_hallucination_guard_strips_invented_version() -> None:
308
+ from cluxion_runtime.core import llm_compress
309
+
310
+ source = "Current package is version 0.3.17 only."
311
+ summary = "We bumped to version 1.2.3 for this release."
312
+ guarded, stripped = llm_compress._apply_hallucination_guard(summary, source)
313
+ assert stripped >= 1
314
+ assert guarded is not None
315
+ assert "1.2.3" not in guarded
316
+
317
+
273
318
  def test_hallucination_guard_all_fabricated_returns_none(monkeypatch) -> None:
274
319
  from cluxion_runtime.core import llm_compress
275
320
 
@@ -11,13 +11,16 @@ from typing import Any
11
11
  import pytest
12
12
 
13
13
  from cluxion_runtime.guard_daemon_host import (
14
+ DEFAULT_IDLE_TTL_MS,
14
15
  HEARTBEAT_FILE_NAME,
15
16
  PID_FILE_NAME,
16
17
  PROC_SCAN_EVERY_N_TICKS,
18
+ STATE_WRITE_EVERY_N_TICKS,
17
19
  ProcessScanCache,
18
20
  _daemon_loop_step,
19
21
  _python_daemon_tick,
20
22
  _run_python_daemon,
23
+ _write_state_if_changed,
21
24
  is_idle,
22
25
  )
23
26
 
@@ -178,7 +181,7 @@ def test_daemon_loop_step_exits_idle_and_removes_pidfile(tmp_path: Path) -> None
178
181
  pid_path.write_text("4242", encoding="utf-8")
179
182
 
180
183
  process_cache = ProcessScanCache(process_count=0, zombie_count=0, zombie_pids=[])
181
- keep_running, _ = _daemon_loop_step(
184
+ keep_running, _, _ = _daemon_loop_step(
182
185
  tmp_path,
183
186
  process_cache=process_cache,
184
187
  cpu_window=[],
@@ -218,7 +221,7 @@ def test_daemon_loop_step_keeps_running_with_fresh_heartbeat(tmp_path: Path) ->
218
221
  pid_path.write_text("4242", encoding="utf-8")
219
222
 
220
223
  process_cache = ProcessScanCache(process_count=0, zombie_count=0, zombie_pids=[])
221
- keep_running, refreshed_cache = _daemon_loop_step(
224
+ keep_running, refreshed_cache, _ = _daemon_loop_step(
222
225
  tmp_path,
223
226
  process_cache=process_cache,
224
227
  cpu_window=[],
@@ -233,4 +236,24 @@ def test_daemon_loop_step_keeps_running_with_fresh_heartbeat(tmp_path: Path) ->
233
236
  assert keep_running is True
234
237
  assert pid_path.exists()
235
238
  assert refreshed_cache.process_count > 0
236
- assert (tmp_path / "guard_state.json").exists()
239
+ assert (tmp_path / "guard_state.json").exists()
240
+
241
+
242
+ def test_default_idle_ttl_is_two_minutes() -> None:
243
+ assert DEFAULT_IDLE_TTL_MS == 120_000
244
+
245
+
246
+ def test_write_state_if_changed_skips_identical_fingerprint(tmp_path: Path) -> None:
247
+ process_cache = ProcessScanCache(process_count=0, zombie_count=0, zombie_pids=[])
248
+ state, _ = _python_daemon_tick(process_cache, [], [], 5, 1000, tick=0)
249
+ state_path = tmp_path / "guard_state.json"
250
+
251
+ fp = _write_state_if_changed(tmp_path, state, last_fingerprint=None, tick=1)
252
+ first_mtime = state_path.stat().st_mtime
253
+ assert state_path.exists()
254
+
255
+ _write_state_if_changed(tmp_path, state, last_fingerprint=fp, tick=2)
256
+ assert state_path.stat().st_mtime == first_mtime
257
+
258
+ _write_state_if_changed(tmp_path, state, last_fingerprint=fp, tick=STATE_WRITE_EVERY_N_TICKS)
259
+ assert state_path.stat().st_mtime >= first_mtime
@@ -43,6 +43,7 @@ def test_register_adds_guard_hooks_when_supported() -> None:
43
43
 
44
44
  assert ctx.hooks == {
45
45
  "on_session_start": guard_watch.on_session_start,
46
+ "on_session_end": guard_watch.on_session_end,
46
47
  "post_tool_call": guard_watch.post_tool_call,
47
48
  }
48
49
  assert "cluxion_guard" in ctx.tools
@@ -205,6 +205,14 @@ def test_context_compress_rejects_malformed_messages() -> None:
205
205
  raise AssertionError("expected ValueError")
206
206
 
207
207
 
208
+ def test_guard_unknown_action_returns_clear_error() -> None:
209
+ result = runner.guard({"action": "sample"})
210
+ payload = json.loads(result.to_json())
211
+ assert result.ok is False
212
+ assert "unknown guard action: sample" in payload["error"]
213
+ assert "status|start|stop|enforce|auto-enforce" in payload["error"]
214
+
215
+
208
216
  def test_context_compress_accepts_valid_messages_structure() -> None:
209
217
  try:
210
218
  runner.context_compress({"messages": [{"content": "hello world"}]})