cluxion-agentplugin-preprocessing 0.3.11__tar.gz → 0.3.13__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 (96) hide show
  1. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/PKG-INFO +1 -1
  2. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/pyproject.toml +1 -1
  3. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/rust/cluxion_queue/Cargo.lock +12 -1
  4. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/rust/cluxion_queue/Cargo.toml +2 -1
  5. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/rust/cluxion_queue/pyproject.toml +1 -1
  6. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/rust/cluxion_queue/src/dispatch.rs +93 -66
  7. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/rust/cluxion_queue/src/guard.rs +260 -35
  8. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/rust/cluxion_queue/src/queue.rs +14 -5
  9. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_agentplugin_preprocessing/__init__.py +1 -1
  10. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_agentplugin_preprocessing/guard_watch.py +5 -0
  11. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_agentplugin_preprocessing/plugin.py +64 -0
  12. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/core/context_compress.py +97 -14
  13. cluxion_agentplugin_preprocessing-0.3.13/src/cluxion_runtime/core/hybrid_forget.py +186 -0
  14. cluxion_agentplugin_preprocessing-0.3.13/src/cluxion_runtime/core/llm_compress.py +138 -0
  15. cluxion_agentplugin_preprocessing-0.3.13/src/cluxion_runtime/guard_daemon_host.py +259 -0
  16. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/resources/guard_bridge.py +15 -5
  17. cluxion_agentplugin_preprocessing-0.3.13/tests/runtime/test_auto_compress_middleware.py +118 -0
  18. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/runtime/test_cluxion_runtime_spine.py +4 -0
  19. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/runtime/test_context_compress.py +7 -0
  20. cluxion_agentplugin_preprocessing-0.3.13/tests/runtime/test_context_compress_llm_forget.py +202 -0
  21. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/runtime/test_guard.py +30 -0
  22. cluxion_agentplugin_preprocessing-0.3.13/tests/runtime/test_guard_daemon_host.py +236 -0
  23. cluxion_agentplugin_preprocessing-0.3.11/src/cluxion_runtime/guard_daemon_host.py +0 -28
  24. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/.github/profile/README.md +0 -0
  25. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/.gitignore +0 -0
  26. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/Docs/README.md +0 -0
  27. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/LICENSE +0 -0
  28. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/README.md +0 -0
  29. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/adapters/claude/.claude-plugin/plugin.json +0 -0
  30. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/adapters/claude/skills/preprocess/SKILL.md +0 -0
  31. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/adapters/codex/config-snippet.toml +0 -0
  32. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/cluxion-Docs/README.md +0 -0
  33. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/cluxion-Docs/architecture.md +0 -0
  34. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/cluxion-Docs/harness-logic.md +0 -0
  35. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/cluxion-Docs/honesty-preprocessing.md +0 -0
  36. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/cluxion-Docs/install-and-operations.md +0 -0
  37. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/cluxion-Docs/security.md +0 -0
  38. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/rust/cluxion_queue/src/context.rs +0 -0
  39. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/rust/cluxion_queue/src/lib.rs +0 -0
  40. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/rust/cluxion_queue/src/main.rs +0 -0
  41. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/rust/cluxion_queue/src/types.rs +0 -0
  42. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_agentplugin_preprocessing/cli.py +0 -0
  43. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_agentplugin_preprocessing/doctor/__init__.py +0 -0
  44. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_agentplugin_preprocessing/doctor/catalog.json +0 -0
  45. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_agentplugin_preprocessing/doctor/framework.py +0 -0
  46. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_agentplugin_preprocessing/doctor/probes.py +0 -0
  47. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_agentplugin_preprocessing/hermes_config.py +0 -0
  48. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_agentplugin_preprocessing/plugin.yaml +0 -0
  49. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_agentplugin_preprocessing/runner.py +0 -0
  50. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_agentplugin_preprocessing/schemas.py +0 -0
  51. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/__init__.py +0 -0
  52. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/__main__.py +0 -0
  53. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/adapters/__init__.py +0 -0
  54. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/adapters/contract.py +0 -0
  55. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/adapters/grok_build.py +0 -0
  56. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/adapters/hermes.py +0 -0
  57. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/adapters/spec.py +0 -0
  58. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/bootstrap.py +0 -0
  59. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/cli.py +0 -0
  60. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/core/__init__.py +0 -0
  61. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/core/clarification.py +0 -0
  62. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/core/dispatch_store.py +0 -0
  63. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/core/harness.py +0 -0
  64. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/core/intent.py +0 -0
  65. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/core/ledger.py +0 -0
  66. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/core/ledger_codec.py +0 -0
  67. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/core/plan_codec.py +0 -0
  68. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/core/preprocess.py +0 -0
  69. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/core/types.py +0 -0
  70. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/core/work_queue.py +0 -0
  71. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/models/__init__.py +0 -0
  72. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/models/supervisor.py +0 -0
  73. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/models/vllm_mlx.py +0 -0
  74. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/resources/__init__.py +0 -0
  75. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/resources/py_queue.py +0 -0
  76. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/resources/queue_bridge.py +0 -0
  77. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/resources/rust_bridge.py +0 -0
  78. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/web/__init__.py +0 -0
  79. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/web/browser_bridge.py +0 -0
  80. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/runtime/test_browser_bridge.py +0 -0
  81. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/runtime/test_clarification.py +0 -0
  82. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/runtime/test_contract.py +0 -0
  83. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/runtime/test_dispatch_store.py +0 -0
  84. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/runtime/test_ledger.py +0 -0
  85. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/runtime/test_py_queue_concurrency.py +0 -0
  86. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/runtime/test_queue_backends.py +0 -0
  87. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/runtime/test_runtime_adapter_cli.py +0 -0
  88. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/runtime/test_rust_queue.py +0 -0
  89. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/runtime/test_supervisor.py +0 -0
  90. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/test_bootstrap.py +0 -0
  91. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/test_doctor.py +0 -0
  92. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/test_guard_watch.py +0 -0
  93. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/test_hermes_config.py +0 -0
  94. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/test_packaging_policy.py +0 -0
  95. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/test_plugin.py +0 -0
  96. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/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.11
3
+ Version: 0.3.13
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.11"
7
+ version = "0.3.13"
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"
@@ -53,8 +53,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
53
53
 
54
54
  [[package]]
55
55
  name = "cluxion_queue"
56
- version = "0.2.0"
56
+ version = "0.2.1"
57
57
  dependencies = [
58
+ "fs2",
58
59
  "pyo3",
59
60
  "rusqlite",
60
61
  "serde",
@@ -148,6 +149,16 @@ version = "0.1.9"
148
149
  source = "registry+https://github.com/rust-lang/crates.io-index"
149
150
  checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
150
151
 
152
+ [[package]]
153
+ name = "fs2"
154
+ version = "0.4.3"
155
+ source = "registry+https://github.com/rust-lang/crates.io-index"
156
+ checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
157
+ dependencies = [
158
+ "libc",
159
+ "winapi",
160
+ ]
161
+
151
162
  [[package]]
152
163
  name = "generic-array"
153
164
  version = "0.14.7"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "cluxion_queue"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  edition = "2021"
5
5
  description = "Durable work queue and dispatch store for Cluxion agent preprocessing"
6
6
 
@@ -20,6 +20,7 @@ rusqlite = { version = "0.32", features = ["bundled"] }
20
20
  sysinfo = "0.33"
21
21
  thiserror = "2"
22
22
  pyo3 = { version = "0.23", features = ["abi3-py311"], optional = true }
23
+ fs2 = "0.4"
23
24
 
24
25
  [features]
25
26
  default = []
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "cluxion-queue-native"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "In-process Rust queue engine for cluxion-agentplugin-preprocessing"
9
9
  requires-python = ">=3.11"
10
10
  license = "Apache-2.0"
@@ -1,6 +1,7 @@
1
- use std::fs;
1
+ use std::fs::{self, OpenOptions};
2
2
  use std::path::{Path, PathBuf};
3
3
 
4
+ use fs2::FileExt;
4
5
  use serde_json::{json, Value};
5
6
 
6
7
  use crate::types::{ok_payload, require_str, QueueError};
@@ -14,49 +15,55 @@ pub fn persist_bundle(store_dir: &Path, payload: &Value) -> Result<Value, QueueE
14
15
  let dispatch_dir = dispatch_dir(store_dir);
15
16
  fs::create_dir_all(&dispatch_dir)?;
16
17
  let path = bundle_path(&dispatch_dir, work_id)?;
17
- write_atomic_json(&path, &bundle)?;
18
- Ok(ok_payload(json!({
19
- "stored": true,
20
- "path": path.to_string_lossy(),
21
- })))
18
+ with_dispatch_lock(&dispatch_dir, || {
19
+ write_atomic_json(&path, &bundle)?;
20
+ Ok(ok_payload(json!({
21
+ "stored": true,
22
+ "path": path.to_string_lossy(),
23
+ })))
24
+ })
22
25
  }
23
26
 
24
27
  pub fn next_step(store_dir: &Path, payload: &Value) -> Result<Value, QueueError> {
25
28
  let work_id = require_str(payload, "work_id")?;
26
- let path = bundle_path(&dispatch_dir(store_dir), work_id)?;
27
- let mut bundle = read_bundle(&path)?;
28
- let mut selected: Option<Value> = None;
29
- {
30
- let steps = steps_mut(&mut bundle)?;
31
- for step in steps.iter_mut() {
32
- let status = step.get("status").and_then(Value::as_str).unwrap_or("");
33
- if status == "queued" || status == "retry_wait" {
34
- step["status"] = json!("running");
35
- step["updated_at"] = json!(now_secs());
36
- selected = Some(public_step(step));
37
- break;
29
+ let dispatch_dir = dispatch_dir(store_dir);
30
+ fs::create_dir_all(&dispatch_dir)?;
31
+ let path = bundle_path(&dispatch_dir, work_id)?;
32
+ with_dispatch_lock(&dispatch_dir, || {
33
+ let mut bundle = read_bundle(&path)?;
34
+ let mut selected: Option<Value> = None;
35
+ {
36
+ let steps = steps_mut(&mut bundle)?;
37
+ for step in steps.iter_mut() {
38
+ let status = step.get("status").and_then(Value::as_str).unwrap_or("");
39
+ if status == "queued" || status == "retry_wait" {
40
+ step["status"] = json!("running");
41
+ step["updated_at"] = json!(now_secs());
42
+ selected = Some(public_step(step));
43
+ break;
44
+ }
38
45
  }
39
46
  }
40
- }
41
- if let Some(step) = selected {
42
- write_atomic_json(&path, &bundle)?;
43
- let remaining = remaining_count(steps_ref(&bundle)?);
44
- return Ok(ok_payload(json!({
47
+ if let Some(step) = selected {
48
+ write_atomic_json(&path, &bundle)?;
49
+ let remaining = remaining_count(steps_ref(&bundle)?);
50
+ return Ok(ok_payload(json!({
51
+ "work_id": work_id,
52
+ "ready": true,
53
+ "step": step,
54
+ "remaining": remaining,
55
+ "synthesis_ready": false,
56
+ })));
57
+ }
58
+ let steps = steps_ref(&bundle)?;
59
+ Ok(ok_payload(json!({
45
60
  "work_id": work_id,
46
- "ready": true,
47
- "step": step,
48
- "remaining": remaining,
49
- "synthesis_ready": false,
50
- })));
51
- }
52
- let steps = steps_ref(&bundle)?;
53
- Ok(ok_payload(json!({
54
- "work_id": work_id,
55
- "ready": false,
56
- "step": json!({}),
57
- "remaining": remaining_count(steps),
58
- "synthesis_ready": steps.iter().all(|step| step.get("status") == Some(&json!("succeeded"))),
59
- })))
61
+ "ready": false,
62
+ "step": json!({}),
63
+ "remaining": remaining_count(steps),
64
+ "synthesis_ready": steps.iter().all(|step| step.get("status") == Some(&json!("succeeded"))),
65
+ })))
66
+ })
60
67
  }
61
68
 
62
69
  pub fn record_step(store_dir: &Path, payload: &Value) -> Result<Value, QueueError> {
@@ -68,37 +75,41 @@ pub fn record_step(store_dir: &Path, payload: &Value) -> Result<Value, QueueErro
68
75
  .get("failed")
69
76
  .and_then(Value::as_bool)
70
77
  .unwrap_or(false);
71
- let path = bundle_path(&dispatch_dir(store_dir), work_id)?;
72
- let mut bundle = read_bundle(&path)?;
73
- let mut recorded_status = None;
74
- {
75
- let steps = steps_mut(&mut bundle)?;
76
- for step in steps.iter_mut() {
77
- if step.get("step_id") == Some(&json!(step_id)) {
78
- step["status"] = json!(if failed { "failed" } else { "succeeded" });
79
- step["result"] = json!(result);
80
- step["error"] = json!(error);
81
- step["updated_at"] = json!(now_secs());
82
- recorded_status = step.get("status").cloned();
83
- break;
78
+ let dispatch_dir = dispatch_dir(store_dir);
79
+ fs::create_dir_all(&dispatch_dir)?;
80
+ let path = bundle_path(&dispatch_dir, work_id)?;
81
+ with_dispatch_lock(&dispatch_dir, || {
82
+ let mut bundle = read_bundle(&path)?;
83
+ let mut recorded_status = None;
84
+ {
85
+ let steps = steps_mut(&mut bundle)?;
86
+ for step in steps.iter_mut() {
87
+ if step.get("step_id") == Some(&json!(step_id)) {
88
+ step["status"] = json!(if failed { "failed" } else { "succeeded" });
89
+ step["result"] = json!(result);
90
+ step["error"] = json!(error);
91
+ step["updated_at"] = json!(now_secs());
92
+ recorded_status = step.get("status").cloned();
93
+ break;
94
+ }
84
95
  }
85
96
  }
86
- }
87
- if let Some(status) = recorded_status {
88
- write_atomic_json(&path, &bundle)?;
89
- let steps = steps_ref(&bundle)?;
90
- return Ok(ok_payload(json!({
91
- "work_id": work_id,
92
- "step_id": step_id,
93
- "recorded": true,
94
- "status": status,
95
- "remaining": remaining_count(steps),
96
- "synthesis_ready": steps.iter().all(|item| item.get("status") == Some(&json!("succeeded"))),
97
- })));
98
- }
99
- Err(QueueError::Store(format!(
100
- "dispatch step not found: {work_id}/{step_id}"
101
- )))
97
+ if let Some(status) = recorded_status {
98
+ write_atomic_json(&path, &bundle)?;
99
+ let steps = steps_ref(&bundle)?;
100
+ return Ok(ok_payload(json!({
101
+ "work_id": work_id,
102
+ "step_id": step_id,
103
+ "recorded": true,
104
+ "status": status,
105
+ "remaining": remaining_count(steps),
106
+ "synthesis_ready": steps.iter().all(|item| item.get("status") == Some(&json!("succeeded"))),
107
+ })));
108
+ }
109
+ Err(QueueError::Store(format!(
110
+ "dispatch step not found: {work_id}/{step_id}"
111
+ )))
112
+ })
102
113
  }
103
114
 
104
115
  pub fn build_brief(store_dir: &Path, payload: &Value) -> Result<Value, QueueError> {
@@ -133,6 +144,22 @@ pub fn build_brief(store_dir: &Path, payload: &Value) -> Result<Value, QueueErro
133
144
  })))
134
145
  }
135
146
 
147
+ fn with_dispatch_lock<T>(
148
+ dispatch_dir: &Path,
149
+ f: impl FnOnce() -> Result<T, QueueError>,
150
+ ) -> Result<T, QueueError> {
151
+ let lock_path = dispatch_dir.join(".dispatch.lock");
152
+ let lockfile = OpenOptions::new()
153
+ .read(true)
154
+ .write(true)
155
+ .create(true)
156
+ .open(&lock_path)?;
157
+ lockfile.lock_exclusive()?;
158
+ let result = f();
159
+ let _ = lockfile.unlock();
160
+ result
161
+ }
162
+
136
163
  fn dispatch_dir(store_dir: &Path) -> PathBuf {
137
164
  store_dir.join("dispatch")
138
165
  }
@@ -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
  }
@@ -95,6 +95,8 @@ pub fn enqueue(store_dir: &Path, payload: &Value) -> Result<Value, QueueError> {
95
95
  pub fn dequeue(store_dir: &Path, _payload: &Value) -> Result<Value, QueueError> {
96
96
  let now = now_secs();
97
97
  with_db(store_dir, |conn| {
98
+ // Use BEGIN IMMEDIATE for atomic SELECT-then-UPDATE claim
99
+ conn.execute_batch("BEGIN IMMEDIATE;")?;
98
100
  let row = conn.query_row(
99
101
  "SELECT work_id, prompt, surface, priority, metadata_json
100
102
  FROM work_queue
@@ -118,6 +120,7 @@ pub fn dequeue(store_dir: &Path, _payload: &Value) -> Result<Value, QueueError>
118
120
  "UPDATE work_queue SET status='running', updated_at=?2 WHERE work_id=?1",
119
121
  params![work_id, now],
120
122
  )?;
123
+ conn.execute_batch("COMMIT;")?;
121
124
  Ok(ok_payload(json!({
122
125
  "ready": true,
123
126
  "item": {
@@ -129,11 +132,17 @@ pub fn dequeue(store_dir: &Path, _payload: &Value) -> Result<Value, QueueError>
129
132
  }
130
133
  })))
131
134
  }
132
- Err(rusqlite::Error::QueryReturnedNoRows) => Ok(ok_payload(json!({
133
- "ready": false,
134
- "item": Value::Null,
135
- }))),
136
- Err(err) => Err(QueueError::Sqlite(err)),
135
+ Err(rusqlite::Error::QueryReturnedNoRows) => {
136
+ conn.execute_batch("COMMIT;")?;
137
+ Ok(ok_payload(json!({
138
+ "ready": false,
139
+ "item": Value::Null,
140
+ })))
141
+ }
142
+ Err(err) => {
143
+ let _ = conn.execute_batch("ROLLBACK;");
144
+ Err(QueueError::Sqlite(err))
145
+ }
137
146
  }
138
147
  })
139
148
  }
@@ -2,6 +2,6 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.3.11"
5
+ __version__ = "0.3.12"
6
6
 
7
7
  __all__ = ["__version__"]