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.
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/PKG-INFO +1 -1
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/pyproject.toml +1 -1
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/rust/cluxion_queue/Cargo.lock +12 -1
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/rust/cluxion_queue/Cargo.toml +2 -1
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/rust/cluxion_queue/pyproject.toml +1 -1
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/rust/cluxion_queue/src/dispatch.rs +93 -66
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/rust/cluxion_queue/src/guard.rs +260 -35
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/rust/cluxion_queue/src/queue.rs +14 -5
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_agentplugin_preprocessing/__init__.py +1 -1
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_agentplugin_preprocessing/guard_watch.py +5 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_agentplugin_preprocessing/plugin.py +64 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/core/context_compress.py +97 -14
- cluxion_agentplugin_preprocessing-0.3.13/src/cluxion_runtime/core/hybrid_forget.py +186 -0
- cluxion_agentplugin_preprocessing-0.3.13/src/cluxion_runtime/core/llm_compress.py +138 -0
- cluxion_agentplugin_preprocessing-0.3.13/src/cluxion_runtime/guard_daemon_host.py +259 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/resources/guard_bridge.py +15 -5
- cluxion_agentplugin_preprocessing-0.3.13/tests/runtime/test_auto_compress_middleware.py +118 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/runtime/test_cluxion_runtime_spine.py +4 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/runtime/test_context_compress.py +7 -0
- cluxion_agentplugin_preprocessing-0.3.13/tests/runtime/test_context_compress_llm_forget.py +202 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/runtime/test_guard.py +30 -0
- cluxion_agentplugin_preprocessing-0.3.13/tests/runtime/test_guard_daemon_host.py +236 -0
- cluxion_agentplugin_preprocessing-0.3.11/src/cluxion_runtime/guard_daemon_host.py +0 -28
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/.github/profile/README.md +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/.gitignore +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/Docs/README.md +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/LICENSE +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/README.md +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/adapters/claude/.claude-plugin/plugin.json +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/adapters/claude/skills/preprocess/SKILL.md +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/adapters/codex/config-snippet.toml +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/cluxion-Docs/README.md +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/cluxion-Docs/architecture.md +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/cluxion-Docs/harness-logic.md +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/cluxion-Docs/honesty-preprocessing.md +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/cluxion-Docs/install-and-operations.md +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/cluxion-Docs/security.md +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/rust/cluxion_queue/src/context.rs +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/rust/cluxion_queue/src/lib.rs +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/rust/cluxion_queue/src/main.rs +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/rust/cluxion_queue/src/types.rs +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_agentplugin_preprocessing/cli.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_agentplugin_preprocessing/doctor/__init__.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_agentplugin_preprocessing/doctor/catalog.json +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_agentplugin_preprocessing/doctor/framework.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_agentplugin_preprocessing/doctor/probes.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_agentplugin_preprocessing/hermes_config.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_agentplugin_preprocessing/plugin.yaml +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_agentplugin_preprocessing/runner.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_agentplugin_preprocessing/schemas.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/__init__.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/__main__.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/adapters/__init__.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/adapters/contract.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/adapters/grok_build.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/adapters/hermes.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/adapters/spec.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/bootstrap.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/cli.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/core/__init__.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/core/clarification.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/core/dispatch_store.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/core/harness.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/core/intent.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/core/ledger.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/core/ledger_codec.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/core/plan_codec.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/core/preprocess.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/core/types.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/core/work_queue.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/models/__init__.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/models/supervisor.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/models/vllm_mlx.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/resources/__init__.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/resources/py_queue.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/resources/queue_bridge.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/resources/rust_bridge.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/web/__init__.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/src/cluxion_runtime/web/browser_bridge.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/runtime/test_browser_bridge.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/runtime/test_clarification.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/runtime/test_contract.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/runtime/test_dispatch_store.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/runtime/test_ledger.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/runtime/test_py_queue_concurrency.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/runtime/test_queue_backends.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/runtime/test_runtime_adapter_cli.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/runtime/test_rust_queue.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/runtime/test_supervisor.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/test_bootstrap.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/test_doctor.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/test_guard_watch.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/test_hermes_config.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/test_packaging_policy.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/test_plugin.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/tests/test_runner.py +0 -0
{cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cluxion-agentplugin-preprocessing
|
|
3
|
-
Version: 0.3.
|
|
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
|
{cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.13}/pyproject.toml
RENAMED
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cluxion-agentplugin-preprocessing"
|
|
7
|
-
version = "0.3.
|
|
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.
|
|
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.
|
|
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 = []
|
|
@@ -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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
27
|
-
|
|
28
|
-
let
|
|
29
|
-
{
|
|
30
|
-
let
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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":
|
|
47
|
-
"step":
|
|
48
|
-
"remaining":
|
|
49
|
-
"synthesis_ready":
|
|
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
|
|
72
|
-
|
|
73
|
-
let
|
|
74
|
-
{
|
|
75
|
-
let
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
step
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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 =
|
|
23
|
-
pub const DEFAULT_DAEMON_WINDOW: usize =
|
|
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
|
-
|
|
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":
|
|
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(¤t, 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).
|
|
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
|
-
|
|
170
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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) =>
|
|
133
|
-
"
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
}
|