cluxion-agentplugin-preprocessing 0.3.12__tar.gz → 0.3.14__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 (97) hide show
  1. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/PKG-INFO +1 -1
  2. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/pyproject.toml +1 -1
  3. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/rust/cluxion_queue/src/guard.rs +260 -35
  4. cluxion_agentplugin_preprocessing-0.3.14/src/cluxion_agentplugin_preprocessing/__init__.py +12 -0
  5. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_agentplugin_preprocessing/guard_watch.py +5 -0
  6. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_agentplugin_preprocessing/plugin.py +64 -0
  7. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/core/context_compress.py +97 -14
  8. cluxion_agentplugin_preprocessing-0.3.14/src/cluxion_runtime/core/hybrid_forget.py +186 -0
  9. cluxion_agentplugin_preprocessing-0.3.14/src/cluxion_runtime/core/llm_compress.py +138 -0
  10. cluxion_agentplugin_preprocessing-0.3.14/src/cluxion_runtime/guard_daemon_host.py +259 -0
  11. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/resources/guard_bridge.py +15 -5
  12. cluxion_agentplugin_preprocessing-0.3.14/tests/runtime/test_auto_compress_middleware.py +118 -0
  13. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/runtime/test_cluxion_runtime_spine.py +4 -0
  14. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/runtime/test_context_compress.py +7 -0
  15. cluxion_agentplugin_preprocessing-0.3.14/tests/runtime/test_context_compress_llm_forget.py +202 -0
  16. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/runtime/test_guard.py +30 -0
  17. cluxion_agentplugin_preprocessing-0.3.14/tests/runtime/test_guard_daemon_host.py +236 -0
  18. cluxion_agentplugin_preprocessing-0.3.12/src/cluxion_agentplugin_preprocessing/__init__.py +0 -7
  19. cluxion_agentplugin_preprocessing-0.3.12/src/cluxion_runtime/guard_daemon_host.py +0 -28
  20. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/.github/profile/README.md +0 -0
  21. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/.gitignore +0 -0
  22. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/Docs/README.md +0 -0
  23. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/LICENSE +0 -0
  24. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/README.md +0 -0
  25. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/adapters/claude/.claude-plugin/plugin.json +0 -0
  26. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/adapters/claude/skills/preprocess/SKILL.md +0 -0
  27. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/adapters/codex/config-snippet.toml +0 -0
  28. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/cluxion-Docs/README.md +0 -0
  29. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/cluxion-Docs/architecture.md +0 -0
  30. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/cluxion-Docs/harness-logic.md +0 -0
  31. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/cluxion-Docs/honesty-preprocessing.md +0 -0
  32. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/cluxion-Docs/install-and-operations.md +0 -0
  33. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/cluxion-Docs/security.md +0 -0
  34. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/rust/cluxion_queue/Cargo.lock +0 -0
  35. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/rust/cluxion_queue/Cargo.toml +0 -0
  36. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/rust/cluxion_queue/pyproject.toml +0 -0
  37. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/rust/cluxion_queue/src/context.rs +0 -0
  38. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/rust/cluxion_queue/src/dispatch.rs +0 -0
  39. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/rust/cluxion_queue/src/lib.rs +0 -0
  40. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/rust/cluxion_queue/src/main.rs +0 -0
  41. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/rust/cluxion_queue/src/queue.rs +0 -0
  42. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/rust/cluxion_queue/src/types.rs +0 -0
  43. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_agentplugin_preprocessing/cli.py +0 -0
  44. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_agentplugin_preprocessing/doctor/__init__.py +0 -0
  45. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_agentplugin_preprocessing/doctor/catalog.json +0 -0
  46. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_agentplugin_preprocessing/doctor/framework.py +0 -0
  47. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_agentplugin_preprocessing/doctor/probes.py +0 -0
  48. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_agentplugin_preprocessing/hermes_config.py +0 -0
  49. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_agentplugin_preprocessing/plugin.yaml +0 -0
  50. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_agentplugin_preprocessing/runner.py +0 -0
  51. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_agentplugin_preprocessing/schemas.py +0 -0
  52. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/__init__.py +0 -0
  53. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/__main__.py +0 -0
  54. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/adapters/__init__.py +0 -0
  55. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/adapters/contract.py +0 -0
  56. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/adapters/grok_build.py +0 -0
  57. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/adapters/hermes.py +0 -0
  58. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/adapters/spec.py +0 -0
  59. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/bootstrap.py +0 -0
  60. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/cli.py +0 -0
  61. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/core/__init__.py +0 -0
  62. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/core/clarification.py +0 -0
  63. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/core/dispatch_store.py +0 -0
  64. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/core/harness.py +0 -0
  65. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/core/intent.py +0 -0
  66. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/core/ledger.py +0 -0
  67. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/core/ledger_codec.py +0 -0
  68. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/core/plan_codec.py +0 -0
  69. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/core/preprocess.py +0 -0
  70. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/core/types.py +0 -0
  71. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/core/work_queue.py +0 -0
  72. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/models/__init__.py +0 -0
  73. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/models/supervisor.py +0 -0
  74. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/models/vllm_mlx.py +0 -0
  75. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/resources/__init__.py +0 -0
  76. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/resources/py_queue.py +0 -0
  77. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/resources/queue_bridge.py +0 -0
  78. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/resources/rust_bridge.py +0 -0
  79. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/web/__init__.py +0 -0
  80. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/src/cluxion_runtime/web/browser_bridge.py +0 -0
  81. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/runtime/test_browser_bridge.py +0 -0
  82. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/runtime/test_clarification.py +0 -0
  83. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/runtime/test_contract.py +0 -0
  84. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/runtime/test_dispatch_store.py +0 -0
  85. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/runtime/test_ledger.py +0 -0
  86. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/runtime/test_py_queue_concurrency.py +0 -0
  87. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/runtime/test_queue_backends.py +0 -0
  88. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/runtime/test_runtime_adapter_cli.py +0 -0
  89. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/runtime/test_rust_queue.py +0 -0
  90. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/runtime/test_supervisor.py +0 -0
  91. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/test_bootstrap.py +0 -0
  92. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/test_doctor.py +0 -0
  93. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/test_guard_watch.py +0 -0
  94. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/test_hermes_config.py +0 -0
  95. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/test_packaging_policy.py +0 -0
  96. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/tests/test_plugin.py +0 -0
  97. {cluxion_agentplugin_preprocessing-0.3.12 → cluxion_agentplugin_preprocessing-0.3.14}/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.12
3
+ Version: 0.3.14
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.12"
7
+ version = "0.3.14"
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"
@@ -19,9 +19,13 @@ const DEFAULT_CPU_SAMPLE_MS: u64 = 100;
19
19
  const MAX_REPORTED_PIDS: usize = 50;
20
20
  const DEFAULT_CPU_HOT_THRESHOLD: f64 = 50.0;
21
21
  const DEFAULT_RSS_HOT_THRESHOLD_MB: u64 = 1024;
22
- pub const DEFAULT_DAEMON_INTERVAL_MS: u64 = 200;
23
- pub const DEFAULT_DAEMON_WINDOW: usize = 25;
22
+ pub const DEFAULT_DAEMON_INTERVAL_MS: u64 = 1000;
23
+ pub const DEFAULT_DAEMON_WINDOW: usize = 10;
24
+ pub const PROC_SCAN_EVERY_N_TICKS: u64 = 5;
24
25
  pub const STATE_FILE_NAME: &str = "guard_state.json";
26
+ pub const HEARTBEAT_FILE_NAME: &str = "guard_heartbeat";
27
+ pub const PID_FILE_NAME: &str = "guard_daemon.pid";
28
+ pub const DEFAULT_IDLE_TTL_MS: u64 = 600_000;
25
29
 
26
30
  fn epoch_ms() -> u64 {
27
31
  SystemTime::now()
@@ -30,6 +34,26 @@ fn epoch_ms() -> u64 {
30
34
  .unwrap_or(0)
31
35
  }
32
36
 
37
+ fn idle_ttl_ms() -> u64 {
38
+ std::env::var("CLUXION_GUARD_IDLE_TTL_MS")
39
+ .ok()
40
+ .and_then(|raw| raw.parse::<u64>().ok())
41
+ .unwrap_or(DEFAULT_IDLE_TTL_MS)
42
+ }
43
+
44
+ /// True when the heartbeat mtime is older than `ttl_ms` relative to `now_ms`.
45
+ pub fn is_idle(heartbeat_mtime_ms: u64, now_ms: u64, ttl_ms: u64) -> bool {
46
+ now_ms.saturating_sub(heartbeat_mtime_ms) > ttl_ms
47
+ }
48
+
49
+ fn heartbeat_mtime_ms(path: &Path) -> Option<u64> {
50
+ let modified = std::fs::metadata(path).ok()?.modified().ok()?;
51
+ modified
52
+ .duration_since(UNIX_EPOCH)
53
+ .ok()
54
+ .map(|duration| duration.as_millis() as u64)
55
+ }
56
+
33
57
  fn uint_field(payload: &Value, key: &str, default: u64) -> u64 {
34
58
  payload.get(key).and_then(Value::as_u64).unwrap_or(default)
35
59
  }
@@ -48,7 +72,14 @@ pub fn sample(payload: &Value) -> Result<Value, QueueError> {
48
72
  Ok(sample_from(&sys))
49
73
  }
50
74
 
51
- fn sample_from(sys: &System) -> Value {
75
+ #[derive(Clone, Debug, PartialEq, Eq)]
76
+ struct ProcessScanCache {
77
+ process_count: usize,
78
+ zombie_count: usize,
79
+ zombie_pids: Vec<u64>,
80
+ }
81
+
82
+ fn scan_process_fields(sys: &System) -> ProcessScanCache {
52
83
  let mut zombie_pids: Vec<u64> = sys
53
84
  .processes()
54
85
  .iter()
@@ -58,19 +89,88 @@ fn sample_from(sys: &System) -> Value {
58
89
  zombie_pids.sort_unstable();
59
90
  let zombie_count = zombie_pids.len();
60
91
  zombie_pids.truncate(MAX_REPORTED_PIDS);
92
+ ProcessScanCache {
93
+ process_count: sys.processes().len(),
94
+ zombie_count,
95
+ zombie_pids,
96
+ }
97
+ }
98
+
99
+ fn build_current_snapshot(sys: &System, process_cache: &ProcessScanCache) -> Value {
61
100
  json!({
62
101
  "ok": true,
63
102
  "total_ram_mb": sys.total_memory() / 1_048_576,
64
103
  "available_ram_mb": sys.available_memory() / 1_048_576,
65
104
  "swap_used_mb": sys.used_swap() / 1_048_576,
66
105
  "cpu_percent": f64::from(sys.global_cpu_usage()),
67
- "process_count": sys.processes().len(),
68
- "zombie_count": zombie_count,
69
- "zombie_pids": zombie_pids,
106
+ "process_count": process_cache.process_count,
107
+ "zombie_count": process_cache.zombie_count,
108
+ "zombie_pids": process_cache.zombie_pids.clone(),
70
109
  "sampled_at_ms": epoch_ms(),
71
110
  })
72
111
  }
73
112
 
113
+ fn sample_from(sys: &System) -> Value {
114
+ build_current_snapshot(sys, &scan_process_fields(sys))
115
+ }
116
+
117
+ fn push_window_sample(
118
+ cpu_window: &mut Vec<f64>,
119
+ ram_window: &mut Vec<u64>,
120
+ window: usize,
121
+ cpu: f64,
122
+ ram: u64,
123
+ ) {
124
+ if cpu_window.len() == window {
125
+ cpu_window.remove(0);
126
+ ram_window.remove(0);
127
+ }
128
+ cpu_window.push(cpu);
129
+ ram_window.push(ram);
130
+ }
131
+
132
+ fn build_daemon_state(
133
+ current: &Value,
134
+ cpu_window: &[f64],
135
+ ram_window: &[u64],
136
+ interval_ms: u64,
137
+ ) -> Value {
138
+ json!({
139
+ "ok": true,
140
+ "current": current,
141
+ "window": {
142
+ "samples": cpu_window.len(),
143
+ "cpu_avg": cpu_window.iter().sum::<f64>() / cpu_window.len() as f64,
144
+ "cpu_peak": cpu_window.iter().cloned().fold(0.0, f64::max),
145
+ "min_available_ram_mb": ram_window.iter().min().copied().unwrap_or(0),
146
+ },
147
+ "interval_ms": interval_ms,
148
+ "updated_at_ms": epoch_ms(),
149
+ })
150
+ }
151
+
152
+ fn daemon_tick(
153
+ sys: &mut System,
154
+ process_cache: &mut ProcessScanCache,
155
+ cpu_window: &mut Vec<f64>,
156
+ ram_window: &mut Vec<u64>,
157
+ window: usize,
158
+ interval_ms: u64,
159
+ tick: u64,
160
+ ) -> Value {
161
+ if tick % PROC_SCAN_EVERY_N_TICKS == 0 {
162
+ sys.refresh_processes(ProcessesToUpdate::All, true);
163
+ *process_cache = scan_process_fields(sys);
164
+ }
165
+ sys.refresh_memory();
166
+ sys.refresh_cpu_usage();
167
+ let current = build_current_snapshot(sys, process_cache);
168
+ let cpu = current["cpu_percent"].as_f64().unwrap_or(0.0);
169
+ let ram = current["available_ram_mb"].as_u64().unwrap_or(0);
170
+ push_window_sample(cpu_window, ram_window, window, cpu, ram);
171
+ build_daemon_state(&current, cpu_window, ram_window, interval_ms)
172
+ }
173
+
74
174
  /// Scan processes against registered owner roots. A process is `owned`
75
175
  /// only when its parent lineage reaches one of `owned_roots`; everything
76
176
  /// else — including processes whose lineage cannot be walked — is
@@ -157,50 +257,53 @@ fn is_owned(pid: u64, owned_roots: &[u64], parents: &HashMap<u64, u64>) -> bool
157
257
  }
158
258
 
159
259
  /// Polling daemon: refresh, fold into a rolling window, publish state
160
- /// atomically (write to a temp file, then rename). Runs until killed.
260
+ /// atomically (write to a temp file, then rename). Self-exits when the
261
+ /// heartbeat file is stale; otherwise runs until killed.
262
+ ///
263
+ /// Cheap ticks refresh memory/CPU every `interval_ms`; full process scans run
264
+ /// every [`PROC_SCAN_EVERY_N_TICKS`] ticks and their results are cached for
265
+ /// intervening cheap ticks.
161
266
  pub fn run_daemon(store_dir: &Path, interval_ms: u64, window: usize) -> Result<(), QueueError> {
162
267
  std::fs::create_dir_all(store_dir)?;
163
268
  let state_path = store_dir.join(STATE_FILE_NAME);
164
269
  let tmp_path = store_dir.join(format!("{STATE_FILE_NAME}.tmp"));
270
+ let heartbeat_path = store_dir.join(HEARTBEAT_FILE_NAME);
271
+ let pid_path = store_dir.join(PID_FILE_NAME);
272
+ let idle_ttl = idle_ttl_ms();
165
273
  let interval = std::time::Duration::from_millis(interval_ms.max(100));
166
274
  let window = window.max(1);
275
+ let interval_ms = interval.as_millis() as u64;
167
276
 
168
277
  let mut sys = System::new();
169
- sys.refresh_memory();
170
- sys.refresh_cpu_usage();
278
+ let mut process_cache = ProcessScanCache {
279
+ process_count: 0,
280
+ zombie_count: 0,
281
+ zombie_pids: Vec::new(),
282
+ };
171
283
  let mut cpu_window: Vec<f64> = Vec::with_capacity(window);
172
284
  let mut ram_window: Vec<u64> = Vec::with_capacity(window);
285
+ let mut tick: u64 = 0;
173
286
 
174
287
  loop {
175
- std::thread::sleep(interval);
176
- sys.refresh_memory();
177
- sys.refresh_cpu_usage();
178
- sys.refresh_processes(ProcessesToUpdate::All, true);
179
- let current = sample_from(&sys);
180
-
181
- let cpu = current["cpu_percent"].as_f64().unwrap_or(0.0);
182
- let ram = current["available_ram_mb"].as_u64().unwrap_or(0);
183
- if cpu_window.len() == window {
184
- cpu_window.remove(0);
185
- ram_window.remove(0);
288
+ if let Some(mtime) = heartbeat_mtime_ms(&heartbeat_path) {
289
+ if is_idle(mtime, epoch_ms(), idle_ttl) {
290
+ let _ = std::fs::remove_file(&pid_path);
291
+ return Ok(());
292
+ }
186
293
  }
187
- cpu_window.push(cpu);
188
- ram_window.push(ram);
189
-
190
- let state = json!({
191
- "ok": true,
192
- "current": current,
193
- "window": {
194
- "samples": cpu_window.len(),
195
- "cpu_avg": cpu_window.iter().sum::<f64>() / cpu_window.len() as f64,
196
- "cpu_peak": cpu_window.iter().cloned().fold(0.0, f64::max),
197
- "min_available_ram_mb": ram_window.iter().min().copied().unwrap_or(0),
198
- },
199
- "interval_ms": interval.as_millis() as u64,
200
- "updated_at_ms": epoch_ms(),
201
- });
294
+ let state = daemon_tick(
295
+ &mut sys,
296
+ &mut process_cache,
297
+ &mut cpu_window,
298
+ &mut ram_window,
299
+ window,
300
+ interval_ms,
301
+ tick,
302
+ );
202
303
  std::fs::write(&tmp_path, serde_json::to_vec(&state)?)?;
203
304
  std::fs::rename(&tmp_path, &state_path)?;
305
+ tick += 1;
306
+ std::thread::sleep(interval);
204
307
  }
205
308
  }
206
309
 
@@ -262,4 +365,126 @@ mod tests {
262
365
  // No roots registered -> nothing is owned.
263
366
  assert!(!is_owned(10, &[], &parents));
264
367
  }
368
+
369
+ #[test]
370
+ fn daemon_tick_caches_process_fields_between_scans() {
371
+ let mut sys = System::new();
372
+ sys.refresh_processes(ProcessesToUpdate::All, true);
373
+ let mut process_cache = scan_process_fields(&sys);
374
+ let mut cpu_window: Vec<f64> = Vec::with_capacity(3);
375
+ let mut ram_window: Vec<u64> = Vec::with_capacity(3);
376
+
377
+ let scan_tick_state = daemon_tick(
378
+ &mut sys,
379
+ &mut process_cache,
380
+ &mut cpu_window,
381
+ &mut ram_window,
382
+ 3,
383
+ 1000,
384
+ 0,
385
+ );
386
+ let scan_process_count = scan_tick_state["current"]["process_count"]
387
+ .as_u64()
388
+ .expect("process_count");
389
+ assert!(scan_process_count > 0);
390
+
391
+ let stale_cache = ProcessScanCache {
392
+ process_count: 1,
393
+ zombie_count: 2,
394
+ zombie_pids: vec![99, 100],
395
+ };
396
+ process_cache = stale_cache.clone();
397
+
398
+ let cheap_tick_state = daemon_tick(
399
+ &mut sys,
400
+ &mut process_cache,
401
+ &mut cpu_window,
402
+ &mut ram_window,
403
+ 3,
404
+ 1000,
405
+ 1,
406
+ );
407
+ assert_eq!(
408
+ cheap_tick_state["current"]["process_count"].as_u64(),
409
+ Some(1)
410
+ );
411
+ assert_eq!(
412
+ cheap_tick_state["current"]["zombie_count"].as_u64(),
413
+ Some(2)
414
+ );
415
+ assert_eq!(
416
+ cheap_tick_state["current"]["zombie_pids"],
417
+ json!([99, 100])
418
+ );
419
+
420
+ let rescan_tick_state = daemon_tick(
421
+ &mut sys,
422
+ &mut process_cache,
423
+ &mut cpu_window,
424
+ &mut ram_window,
425
+ 3,
426
+ 1000,
427
+ PROC_SCAN_EVERY_N_TICKS,
428
+ );
429
+ assert_ne!(process_cache, stale_cache);
430
+ assert!(process_cache.process_count > 0);
431
+ assert_eq!(
432
+ rescan_tick_state["current"]["process_count"].as_u64(),
433
+ Some(process_cache.process_count as u64)
434
+ );
435
+ }
436
+
437
+ #[test]
438
+ fn is_idle_detects_stale_and_fresh_heartbeats() {
439
+ let ttl = 600_000;
440
+ let now = 1_700_000_000_000u64;
441
+ assert!(is_idle(now - ttl - 1, now, ttl));
442
+ assert!(!is_idle(now - ttl, now, ttl));
443
+ assert!(!is_idle(now - 1, now, ttl));
444
+ }
445
+
446
+ #[test]
447
+ fn daemon_state_json_has_required_keys() {
448
+ let mut sys = System::new();
449
+ sys.refresh_processes(ProcessesToUpdate::All, true);
450
+ let mut process_cache = scan_process_fields(&sys);
451
+ let mut cpu_window: Vec<f64> = Vec::with_capacity(2);
452
+ let mut ram_window: Vec<u64> = Vec::with_capacity(2);
453
+
454
+ let state = daemon_tick(
455
+ &mut sys,
456
+ &mut process_cache,
457
+ &mut cpu_window,
458
+ &mut ram_window,
459
+ 2,
460
+ 1000,
461
+ 0,
462
+ );
463
+
464
+ for key in ["ok", "current", "window", "interval_ms", "updated_at_ms"] {
465
+ assert!(state.get(key).is_some(), "missing top-level key: {key}");
466
+ }
467
+ for key in [
468
+ "ok",
469
+ "total_ram_mb",
470
+ "available_ram_mb",
471
+ "swap_used_mb",
472
+ "cpu_percent",
473
+ "process_count",
474
+ "zombie_count",
475
+ "zombie_pids",
476
+ "sampled_at_ms",
477
+ ] {
478
+ assert!(
479
+ state["current"].get(key).is_some(),
480
+ "missing current key: {key}"
481
+ );
482
+ }
483
+ for key in ["samples", "cpu_avg", "cpu_peak", "min_available_ram_mb"] {
484
+ assert!(
485
+ state["window"].get(key).is_some(),
486
+ "missing window key: {key}"
487
+ );
488
+ }
489
+ }
265
490
  }
@@ -0,0 +1,12 @@
1
+ """Universal Cluxion preprocessing agent plugin."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib.metadata import PackageNotFoundError, version
6
+
7
+ try:
8
+ __version__ = version("cluxion-agentplugin-preprocessing")
9
+ except PackageNotFoundError: # pragma: no cover
10
+ __version__ = "0.3.13"
11
+
12
+ __all__ = ["__version__"]
@@ -6,6 +6,7 @@ stderr and never raised into the host agent.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import contextlib
9
10
  import os
10
11
  import sys
11
12
  import threading
@@ -35,6 +36,7 @@ def on_session_start(**_: Any) -> None:
35
36
  return
36
37
  try:
37
38
  result = guard_bridge.start_daemon()
39
+ guard_bridge.touch_heartbeat()
38
40
  except Exception as exc:
39
41
  _warn(f"cluxion guard autostart failed: {exc}")
40
42
  return
@@ -53,6 +55,9 @@ def post_tool_call(**_: Any) -> None:
53
55
  """
54
56
  global _last_warning_at, _last_watch_at
55
57
 
58
+ with contextlib.suppress(Exception):
59
+ guard_bridge.touch_heartbeat()
60
+
56
61
  now = time.monotonic()
57
62
  try:
58
63
  with _lock:
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import importlib.resources
6
6
  import json
7
+ import os
7
8
  from pathlib import Path
8
9
  from typing import TYPE_CHECKING
9
10
 
@@ -26,6 +27,12 @@ from cluxion_agentplugin_preprocessing.schemas import (
26
27
  SERVE_LOCAL_SCHEMA,
27
28
  WEB_SEARCH_SCHEMA,
28
29
  )
30
+ from cluxion_runtime.core.context_compress import (
31
+ DEFAULT_TRIGGER_RATIO,
32
+ _resolve_context_limit,
33
+ compress,
34
+ )
35
+ from cluxion_runtime.core.preprocess import estimate_tokens
29
36
 
30
37
  if TYPE_CHECKING:
31
38
  from collections.abc import Callable
@@ -38,6 +45,10 @@ def register(ctx: object) -> None:
38
45
  register_hook("on_session_start", guard_watch.on_session_start)
39
46
  register_hook("post_tool_call", guard_watch.post_tool_call)
40
47
 
48
+ register_mw = getattr(ctx, "register_middleware", None)
49
+ if callable(register_mw):
50
+ register_mw("llm_request", _auto_compress_middleware)
51
+
41
52
  ctx.register_tool(
42
53
  name="cluxion_plan",
43
54
  toolset="cluxion",
@@ -275,4 +286,57 @@ def _json_result(callback: Callable[[], str]) -> str:
275
286
  return json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False, sort_keys=True)
276
287
 
277
288
 
289
+ def _autocompress_enabled() -> bool:
290
+ value = os.environ.get("CLUXION_PREPROCESS_AUTOCOMPRESS", "1").strip().lower()
291
+ return value not in ("0", "false", "no", "off")
292
+
293
+
294
+ def _auto_compress_middleware(
295
+ request: dict[str, object],
296
+ original_request: object = None,
297
+ model: str | None = None,
298
+ **_: object,
299
+ ) -> dict[str, object] | None:
300
+ del original_request
301
+ if os.environ.get("CLUXION_PREPROCESS_IN_COMPRESS") == "1":
302
+ return None
303
+ if not _autocompress_enabled():
304
+ return None
305
+
306
+ messages_key = "messages"
307
+ raw_messages = request.get("messages")
308
+ if not isinstance(raw_messages, list):
309
+ raw_messages = request.get("input")
310
+ messages_key = "input"
311
+ if not isinstance(raw_messages, list):
312
+ return None
313
+
314
+ try:
315
+ total_tokens = 0
316
+ for raw in raw_messages:
317
+ if isinstance(raw, dict):
318
+ total_tokens += estimate_tokens(str(raw.get("content", "")))
319
+ else:
320
+ total_tokens += estimate_tokens(str(raw))
321
+
322
+ limit_payload: dict[str, object] = {}
323
+ if model:
324
+ limit_payload["model"] = model
325
+ ctx_limit = request.get("context_limit_tokens")
326
+ if isinstance(ctx_limit, int) and not isinstance(ctx_limit, bool) and ctx_limit > 0:
327
+ limit_payload["context_limit_tokens"] = ctx_limit
328
+ context_limit = _resolve_context_limit(limit_payload)
329
+ if total_tokens / context_limit < DEFAULT_TRIGGER_RATIO:
330
+ return None
331
+
332
+ payload: dict[str, object] = {"messages": raw_messages, **limit_payload}
333
+ result = compress(payload)
334
+ shrunk = result.get("messages")
335
+ if not isinstance(shrunk, list):
336
+ return None
337
+ return {"request": {**request, messages_key: shrunk}, "source": "preprocessing"}
338
+ except Exception:
339
+ return None
340
+
341
+
278
342
  __all__ = ["register"]
@@ -1,17 +1,16 @@
1
- """Deterministic context compression: stage 1 of the 70% -> 30% pipeline.
1
+ """Context compression: 70% trigger -> 30% target pipeline.
2
2
 
3
- Pure-Python mirror of ``rust/cluxion_queue/src/context.rs`` — every
3
+ Stage 1 (deterministic) mirrors ``rust/cluxion_queue/src/context.rs`` — every
4
4
  constant, threshold, ordering rule, and the token estimator must stay in
5
- lockstep so the three backends produce identical output (parity-tested).
5
+ lockstep so the three backends produce identical Stage-1 output (parity-tested).
6
+
7
+ Stages 2 (LLM summarization via ``hermes -z``) and 3 (hybrid forgetting) are
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.
6
11
 
7
12
  What stays untouched: pinned messages (explicit ``pinned``, the first
8
13
  user message = task intent, the most recent ``keep_recent`` turns).
9
- Stages run oldest-first and stop as soon as usage reaches the target:
10
- A. truncate long messages (head + tail excerpt)
11
- B. drop exact duplicates (trimmed-content match)
12
- C. fold remaining old turns into one-line digests
13
- If the target is still not met the result carries ``ai_summary_request``
14
- telling the host AI which messages to summarize and what to preserve.
15
14
  """
16
15
 
17
16
  from __future__ import annotations
@@ -19,6 +18,8 @@ from __future__ import annotations
19
18
  from dataclasses import dataclass
20
19
  from typing import TYPE_CHECKING
21
20
 
21
+ from cluxion_runtime.core.hybrid_forget import apply_hybrid_forget
22
+ from cluxion_runtime.core.llm_compress import hermes_available, summarize_messages
22
23
  from cluxion_runtime.core.preprocess import estimate_tokens
23
24
 
24
25
  if TYPE_CHECKING:
@@ -70,6 +71,12 @@ def compress(payload: Mapping[str, object]) -> dict[str, object]:
70
71
  trigger_ratio = _ratio(payload, "trigger_ratio", DEFAULT_TRIGGER_RATIO)
71
72
  target_ratio = _ratio(payload, "target_ratio", DEFAULT_TARGET_RATIO)
72
73
  keep_recent = _uint(payload, "keep_recent_turns", DEFAULT_KEEP_RECENT)
74
+ enable_llm = _bool_flag(payload, "enable_llm_summary", True)
75
+ enable_forget = _bool_flag(payload, "enable_forget", True)
76
+ model = payload.get("model") if isinstance(payload.get("model"), str) else None
77
+ session_id = payload.get("session_id") if isinstance(payload.get("session_id"), str) else None
78
+ timeout_raw = payload.get("llm_timeout_s")
79
+ timeout_s = float(timeout_raw) if isinstance(timeout_raw, (int, float)) and not isinstance(timeout_raw, bool) else 120.0
73
80
 
74
81
  tokens_before = sum(estimate_tokens(m.content) for m in messages)
75
82
  usage_before = tokens_before / context_limit
@@ -102,11 +109,69 @@ def compress(payload: Mapping[str, object]) -> dict[str, object]:
102
109
  if changed:
103
110
  stages.append("digest")
104
111
 
105
- summary_request = None
106
- if total > target_tokens:
112
+ summary_request: dict[str, object] | None = None
113
+ dropped_without_backup = False
114
+ over_target_pinned_only = False
115
+ forced_over_target = False
116
+
117
+ if total > target_tokens and enable_llm and hermes_available():
107
118
  summary_request = _build_summary_request(messages, pinned, total, target_tokens)
119
+ indices = summary_request["summarize_indices"]
120
+ if isinstance(indices, list) and indices:
121
+ summaries = summarize_messages(
122
+ messages,
123
+ indices, # type: ignore[arg-type]
124
+ str(summary_request.get("instructions", "")),
125
+ model=model,
126
+ timeout_s=timeout_s,
127
+ )
128
+ if summaries is not None:
129
+ for idx, summary in summaries.items():
130
+ if idx in pinned or idx < 0 or idx >= len(messages):
131
+ continue
132
+ old_tokens = estimate_tokens(messages[idx].content)
133
+ messages[idx].content = summary
134
+ total = total - old_tokens + estimate_tokens(summary)
135
+ stages.append("llm_summary")
136
+ summary_request = None
137
+ # fail-safe: keep Stage-1 output and ai_summary_request on LLM failure
138
+
139
+ if total > target_tokens and enable_forget:
140
+ forget_result = apply_hybrid_forget(
141
+ messages,
142
+ pinned,
143
+ total,
144
+ target_tokens,
145
+ session_id=session_id,
146
+ )
147
+ messages = forget_result.messages
148
+ total = forget_result.tokens_after
149
+ dropped_without_backup = forget_result.dropped_without_backup
150
+ over_target_pinned_only = forget_result.over_target_pinned_only
151
+ if forget_result.dropped_indices:
152
+ stages.append("forget")
153
+ pinned = _pinned_indices(messages, keep_recent)
108
154
 
109
- return _result_payload(messages, tokens_before, total, context_limit, stages, summary_request, pinned)
155
+ if total > target_tokens:
156
+ if summary_request is None:
157
+ 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:
161
+ forced_over_target = True
162
+
163
+ return _result_payload(
164
+ messages,
165
+ tokens_before,
166
+ total,
167
+ context_limit,
168
+ stages,
169
+ summary_request,
170
+ pinned,
171
+ dropped_without_backup=dropped_without_backup,
172
+ over_target_pinned_only=over_target_pinned_only,
173
+ forced_over_target=forced_over_target,
174
+ )
110
175
 
111
176
 
112
177
  def _resolve_context_limit(payload: Mapping[str, object]) -> int:
@@ -136,6 +201,13 @@ def _uint(payload: Mapping[str, object], key: str, default: int) -> int:
136
201
  return default
137
202
 
138
203
 
204
+ def _bool_flag(payload: Mapping[str, object], key: str, default: bool) -> bool:
205
+ value = payload.get(key)
206
+ if isinstance(value, bool):
207
+ return value
208
+ return default
209
+
210
+
139
211
  def _pinned_indices(messages: list[_Msg], keep_recent: int) -> list[int]:
140
212
  pinned = [idx for idx, msg in enumerate(messages) if msg.pinned]
141
213
  first_user = next((idx for idx, msg in enumerate(messages) if msg.role == "user"), None)
@@ -235,8 +307,12 @@ def _result_payload(
235
307
  stages: list[str],
236
308
  summary_request: dict[str, object] | None,
237
309
  pinned: list[int],
310
+ *,
311
+ dropped_without_backup: bool = False,
312
+ over_target_pinned_only: bool = False,
313
+ forced_over_target: bool = False,
238
314
  ) -> dict[str, object]:
239
- return {
315
+ result: dict[str, object] = {
240
316
  "ok": True,
241
317
  "compressed": bool(stages),
242
318
  "tokens_before": tokens_before,
@@ -249,6 +325,13 @@ def _result_payload(
249
325
  "messages": [{"role": m.role, "content": m.content, "pinned": m.pinned} for m in messages],
250
326
  "ai_summary_request": summary_request,
251
327
  }
328
+ if dropped_without_backup:
329
+ result["dropped_without_backup"] = True
330
+ if over_target_pinned_only:
331
+ result["over_target_pinned_only"] = True
332
+ if forced_over_target:
333
+ result["forced_over_target"] = True
334
+ return result
252
335
 
253
336
 
254
- __all__ = ["compress", "estimate_tokens"]
337
+ __all__ = ["compress", "estimate_tokens"]