cluxion-agentplugin-preprocessing 0.3.11__tar.gz → 0.3.12__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 (90) hide show
  1. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/PKG-INFO +1 -1
  2. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/pyproject.toml +1 -1
  3. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/rust/cluxion_queue/Cargo.lock +12 -1
  4. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/rust/cluxion_queue/Cargo.toml +2 -1
  5. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/rust/cluxion_queue/pyproject.toml +1 -1
  6. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/rust/cluxion_queue/src/dispatch.rs +93 -66
  7. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/rust/cluxion_queue/src/queue.rs +14 -5
  8. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_agentplugin_preprocessing/__init__.py +1 -1
  9. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/.github/profile/README.md +0 -0
  10. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/.gitignore +0 -0
  11. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/Docs/README.md +0 -0
  12. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/LICENSE +0 -0
  13. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/README.md +0 -0
  14. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/adapters/claude/.claude-plugin/plugin.json +0 -0
  15. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/adapters/claude/skills/preprocess/SKILL.md +0 -0
  16. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/adapters/codex/config-snippet.toml +0 -0
  17. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/cluxion-Docs/README.md +0 -0
  18. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/cluxion-Docs/architecture.md +0 -0
  19. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/cluxion-Docs/harness-logic.md +0 -0
  20. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/cluxion-Docs/honesty-preprocessing.md +0 -0
  21. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/cluxion-Docs/install-and-operations.md +0 -0
  22. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/cluxion-Docs/security.md +0 -0
  23. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/rust/cluxion_queue/src/context.rs +0 -0
  24. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/rust/cluxion_queue/src/guard.rs +0 -0
  25. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/rust/cluxion_queue/src/lib.rs +0 -0
  26. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/rust/cluxion_queue/src/main.rs +0 -0
  27. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/rust/cluxion_queue/src/types.rs +0 -0
  28. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_agentplugin_preprocessing/cli.py +0 -0
  29. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_agentplugin_preprocessing/doctor/__init__.py +0 -0
  30. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_agentplugin_preprocessing/doctor/catalog.json +0 -0
  31. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_agentplugin_preprocessing/doctor/framework.py +0 -0
  32. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_agentplugin_preprocessing/doctor/probes.py +0 -0
  33. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_agentplugin_preprocessing/guard_watch.py +0 -0
  34. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_agentplugin_preprocessing/hermes_config.py +0 -0
  35. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_agentplugin_preprocessing/plugin.py +0 -0
  36. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_agentplugin_preprocessing/plugin.yaml +0 -0
  37. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_agentplugin_preprocessing/runner.py +0 -0
  38. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_agentplugin_preprocessing/schemas.py +0 -0
  39. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_runtime/__init__.py +0 -0
  40. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_runtime/__main__.py +0 -0
  41. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_runtime/adapters/__init__.py +0 -0
  42. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_runtime/adapters/contract.py +0 -0
  43. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_runtime/adapters/grok_build.py +0 -0
  44. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_runtime/adapters/hermes.py +0 -0
  45. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_runtime/adapters/spec.py +0 -0
  46. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_runtime/bootstrap.py +0 -0
  47. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_runtime/cli.py +0 -0
  48. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_runtime/core/__init__.py +0 -0
  49. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_runtime/core/clarification.py +0 -0
  50. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_runtime/core/context_compress.py +0 -0
  51. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_runtime/core/dispatch_store.py +0 -0
  52. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_runtime/core/harness.py +0 -0
  53. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_runtime/core/intent.py +0 -0
  54. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_runtime/core/ledger.py +0 -0
  55. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_runtime/core/ledger_codec.py +0 -0
  56. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_runtime/core/plan_codec.py +0 -0
  57. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_runtime/core/preprocess.py +0 -0
  58. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_runtime/core/types.py +0 -0
  59. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_runtime/core/work_queue.py +0 -0
  60. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_runtime/guard_daemon_host.py +0 -0
  61. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_runtime/models/__init__.py +0 -0
  62. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_runtime/models/supervisor.py +0 -0
  63. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_runtime/models/vllm_mlx.py +0 -0
  64. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_runtime/resources/__init__.py +0 -0
  65. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_runtime/resources/guard_bridge.py +0 -0
  66. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_runtime/resources/py_queue.py +0 -0
  67. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_runtime/resources/queue_bridge.py +0 -0
  68. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_runtime/resources/rust_bridge.py +0 -0
  69. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_runtime/web/__init__.py +0 -0
  70. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/src/cluxion_runtime/web/browser_bridge.py +0 -0
  71. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/tests/runtime/test_browser_bridge.py +0 -0
  72. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/tests/runtime/test_clarification.py +0 -0
  73. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/tests/runtime/test_cluxion_runtime_spine.py +0 -0
  74. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/tests/runtime/test_context_compress.py +0 -0
  75. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/tests/runtime/test_contract.py +0 -0
  76. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/tests/runtime/test_dispatch_store.py +0 -0
  77. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/tests/runtime/test_guard.py +0 -0
  78. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/tests/runtime/test_ledger.py +0 -0
  79. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/tests/runtime/test_py_queue_concurrency.py +0 -0
  80. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/tests/runtime/test_queue_backends.py +0 -0
  81. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/tests/runtime/test_runtime_adapter_cli.py +0 -0
  82. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/tests/runtime/test_rust_queue.py +0 -0
  83. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/tests/runtime/test_supervisor.py +0 -0
  84. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/tests/test_bootstrap.py +0 -0
  85. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/tests/test_doctor.py +0 -0
  86. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/tests/test_guard_watch.py +0 -0
  87. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/tests/test_hermes_config.py +0 -0
  88. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/tests/test_packaging_policy.py +0 -0
  89. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/tests/test_plugin.py +0 -0
  90. {cluxion_agentplugin_preprocessing-0.3.11 → cluxion_agentplugin_preprocessing-0.3.12}/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.12
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.12"
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
  }
@@ -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__"]