@team-agent/installer 0.5.64 → 0.5.66
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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/Cargo.toml +5 -0
- package/crates/team-agent/src/cli/adapters.rs +6 -1
- package/crates/team-agent/src/cli/emit.rs +15 -0
- package/crates/team-agent/src/cli/mod.rs +9 -8
- package/crates/team-agent/src/cli/send/presentation.rs +1 -0
- package/crates/team-agent/src/cli/spec.rs +3 -0
- package/crates/team-agent/src/cli/tests/compile.rs +1 -1
- package/crates/team-agent/src/cli/tests/mod.rs +33 -2
- package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -1
- package/crates/team-agent/src/cli/tests/status_send.rs +13 -3
- package/crates/team-agent/src/cli/tests/verb_validate.rs +1 -1
- package/crates/team-agent/src/cli/types.rs +7 -0
- package/crates/team-agent/src/compiler/tests.rs +23 -20
- package/crates/team-agent/src/compiler.rs +25 -5
- package/crates/team-agent/src/conpty/backend.rs +16 -0
- package/crates/team-agent/src/coordinator/health.rs +17 -0
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +55 -2
- package/crates/team-agent/src/db/migration.rs +2 -1
- package/crates/team-agent/src/db/schema.rs +16 -8
- package/crates/team-agent/src/leader/lease.rs +1 -2
- package/crates/team-agent/src/leader/start.rs +175 -10
- package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +0 -2
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +7 -2
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +137 -2
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +1 -2
- package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +5 -0
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +39 -2
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +105 -48
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +0 -2
- package/crates/team-agent/src/lifecycle/launch.rs +117 -21
- package/crates/team-agent/src/lifecycle/restart/agent.rs +89 -3
- package/crates/team-agent/src/lifecycle/restart/common.rs +9 -12
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +59 -15
- package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +4 -4
- package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +4 -4
- package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +24 -32
- package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +13 -11
- package/crates/team-agent/src/lifecycle/tests/core.rs +0 -18
- package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +3 -3
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +17 -30
- package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +4 -4
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +11 -7
- package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +3 -3
- package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests.rs +11 -7
- package/crates/team-agent/src/lifecycle/types.rs +0 -2
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +115 -22
- package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
- package/crates/team-agent/src/messaging/delivery.rs +29 -0
- package/crates/team-agent/src/messaging/helpers.rs +2 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +12 -0
- package/crates/team-agent/src/messaging/mod.rs +2 -0
- package/crates/team-agent/src/messaging/results.rs +5 -0
- package/crates/team-agent/src/messaging/send.rs +9 -0
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +1 -1
- package/crates/team-agent/src/messaging/tests/runtime.rs +32 -0
- package/crates/team-agent/src/messaging/types.rs +2 -0
- package/crates/team-agent/src/messaging/wait.rs +366 -0
- package/crates/team-agent/src/messaging/watchers.rs +153 -6
- package/crates/team-agent/src/model/spec.rs +129 -6
- package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +3 -1
- package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +3 -1
- package/crates/team-agent/src/model/testdata/team.spec.yaml +3 -1
- package/crates/team-agent/src/provider/adapters/claude.rs +5 -2
- package/crates/team-agent/src/provider/adapters/codex.rs +5 -2
- package/crates/team-agent/src/provider/adapters/copilot.rs +5 -2
- package/crates/team-agent/src/provider/bypass_flags.rs +80 -0
- package/crates/team-agent/src/provider/mod.rs +2 -0
- package/crates/team-agent/src/tmux_backend.rs +35 -12
- package/crates/team-agent/src/transport/test_support.rs +20 -0
- package/crates/team-agent/src/transport.rs +28 -11
- package/package.json +4 -4
- package/crates/team-agent/src/lifecycle/launch/approval.rs +0 -134
- package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +0 -1187
- package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +0 -327
|
@@ -214,6 +214,7 @@ pub fn send_message(
|
|
|
214
214
|
stage: None,
|
|
215
215
|
reason: Some(crate::messaging::DeliveryRefusal::EmptyTargetList),
|
|
216
216
|
channel: None,
|
|
217
|
+
ack_forced_off: false,
|
|
217
218
|
});
|
|
218
219
|
}
|
|
219
220
|
MessageTarget::Fanout(recipients) => {
|
|
@@ -351,6 +352,7 @@ pub fn send_message(
|
|
|
351
352
|
stage: None,
|
|
352
353
|
reason: None,
|
|
353
354
|
channel: None,
|
|
355
|
+
ack_forced_off: false,
|
|
354
356
|
})
|
|
355
357
|
}
|
|
356
358
|
|
|
@@ -404,6 +406,7 @@ fn persist_stored_only_send(
|
|
|
404
406
|
stage: None,
|
|
405
407
|
reason: None,
|
|
406
408
|
channel: Some(presentation.effective_sink.as_str().to_string()),
|
|
409
|
+
ack_forced_off: false,
|
|
407
410
|
})
|
|
408
411
|
}
|
|
409
412
|
|
|
@@ -451,6 +454,7 @@ fn refused_outcome_with_id(reason: DeliveryRefusal, message_id: Option<String>)
|
|
|
451
454
|
stage: None,
|
|
452
455
|
reason: Some(reason),
|
|
453
456
|
channel: None,
|
|
457
|
+
ack_forced_off: false,
|
|
454
458
|
}
|
|
455
459
|
}
|
|
456
460
|
|
|
@@ -467,6 +471,7 @@ fn refused_outcome_with_verification(
|
|
|
467
471
|
stage: None,
|
|
468
472
|
reason: Some(reason),
|
|
469
473
|
channel: None,
|
|
474
|
+
ack_forced_off: false,
|
|
470
475
|
}
|
|
471
476
|
}
|
|
472
477
|
|
|
@@ -549,6 +554,7 @@ fn coordinator_unavailable_outcome(
|
|
|
549
554
|
stage: None,
|
|
550
555
|
reason: Some(DeliveryRefusal::CoordinatorUnavailable),
|
|
551
556
|
channel: Some("coordinator_unavailable".to_string()),
|
|
557
|
+
ack_forced_off: false,
|
|
552
558
|
},
|
|
553
559
|
}))
|
|
554
560
|
}
|
|
@@ -573,6 +579,7 @@ fn rebind_required_outcome_with_verification(
|
|
|
573
579
|
stage: None,
|
|
574
580
|
reason: Some(DeliveryRefusal::LeaderNotAttached),
|
|
575
581
|
channel: Some("rebind_required".to_string()),
|
|
582
|
+
ack_forced_off: false,
|
|
576
583
|
}
|
|
577
584
|
}
|
|
578
585
|
|
|
@@ -804,6 +811,7 @@ pub(crate) fn session_drift_refusal(
|
|
|
804
811
|
stage: None,
|
|
805
812
|
reason: Some(DeliveryRefusal::SessionDrift),
|
|
806
813
|
channel: None,
|
|
814
|
+
ack_forced_off: false,
|
|
807
815
|
}))
|
|
808
816
|
}
|
|
809
817
|
|
|
@@ -924,5 +932,6 @@ fn fanout_send(
|
|
|
924
932
|
} else {
|
|
925
933
|
channel_label.to_string()
|
|
926
934
|
}),
|
|
935
|
+
ack_forced_off: false,
|
|
927
936
|
})
|
|
928
937
|
}
|
|
@@ -94,7 +94,7 @@ fn collect_result_file_ingests_valid_known_task_envelope_into_results() {
|
|
|
94
94
|
.unwrap();
|
|
95
95
|
std::fs::write(
|
|
96
96
|
team.join("agents").join("w1.md"),
|
|
97
|
-
"---\nname: w1\nrole: Worker\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nW1.\n",
|
|
97
|
+
"---\nname: w1\nrole: Worker\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ndangerously_skip_permissions: false\ntools:\n - mcp_team\n---\n\nW1.\n",
|
|
98
98
|
)
|
|
99
99
|
.unwrap();
|
|
100
100
|
let spec = crate::compiler::compile_team(&team).expect("compile collect team");
|
|
@@ -709,6 +709,38 @@ fn report_result_invalid_envelope_errors_validation() {
|
|
|
709
709
|
);
|
|
710
710
|
}
|
|
711
711
|
|
|
712
|
+
#[test]
|
|
713
|
+
fn leader_receiver_reports_requires_ack_forced_off() {
|
|
714
|
+
let ws = tmp_ws("ack-forced-off");
|
|
715
|
+
let log = EventLog::new(&ws);
|
|
716
|
+
let state = json(serde_json::json!({
|
|
717
|
+
"active_team_key": "team-a",
|
|
718
|
+
"teams": {"team-a": {}}
|
|
719
|
+
}));
|
|
720
|
+
|
|
721
|
+
let outcome = send_to_leader_receiver(
|
|
722
|
+
&ws,
|
|
723
|
+
&state,
|
|
724
|
+
"leader",
|
|
725
|
+
"ack contract probe",
|
|
726
|
+
None,
|
|
727
|
+
"worker",
|
|
728
|
+
true,
|
|
729
|
+
None,
|
|
730
|
+
&log,
|
|
731
|
+
)
|
|
732
|
+
.unwrap();
|
|
733
|
+
|
|
734
|
+
let value = crate::mcp_server::helpers::delivery_outcome_value(&outcome);
|
|
735
|
+
assert_eq!(
|
|
736
|
+
value
|
|
737
|
+
.get("ack_forced_off")
|
|
738
|
+
.and_then(serde_json::Value::as_bool),
|
|
739
|
+
Some(true),
|
|
740
|
+
"requires_ack=true must report that the unavailable ack guarantee was forced off"
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
|
|
712
744
|
// ════════════════════════════════════════════════════════════════════════
|
|
713
745
|
// GROUP N — notify_result_watchers dedupe (exactly-once, Gap 32/38).
|
|
714
746
|
// result_delivery.py:38-132. superseded for duplicate watchers same result.
|
|
@@ -295,6 +295,8 @@ pub enum PaneWidthQuery {
|
|
|
295
295
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
296
296
|
pub struct DeliveryOutcome {
|
|
297
297
|
pub ok: bool,
|
|
298
|
+
/// Whether a caller-requested ack guarantee was unavailable and forced off.
|
|
299
|
+
pub ack_forced_off: bool,
|
|
298
300
|
/// 投递层结果态 (≠ 行态)。
|
|
299
301
|
pub status: DeliveryStatus,
|
|
300
302
|
/// step 7 `messages.status` 行态原文 (shadow;leader 集成时收口为 step 7 enum)。
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
use std::path::Path;
|
|
2
|
+
|
|
3
|
+
use serde_json::Value;
|
|
4
|
+
|
|
5
|
+
use super::MessagingError;
|
|
6
|
+
|
|
7
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
8
|
+
pub struct WaitResult {
|
|
9
|
+
pub task_id: String,
|
|
10
|
+
pub result_id: String,
|
|
11
|
+
pub waited: bool,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
impl WaitResult {
|
|
15
|
+
pub fn to_json(&self) -> Value {
|
|
16
|
+
serde_json::json!({
|
|
17
|
+
"ok": true,
|
|
18
|
+
"status": "completed",
|
|
19
|
+
"task_id": self.task_id,
|
|
20
|
+
"result_id": self.result_id,
|
|
21
|
+
"waited": self.waited,
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#[cfg(not(unix))]
|
|
27
|
+
pub fn wait_for_result(_workspace: &Path, _task_id: &str) -> Result<WaitResult, MessagingError> {
|
|
28
|
+
Err(MessagingError::Validation(
|
|
29
|
+
"wait --task requires POSIX FIFO support".to_string(),
|
|
30
|
+
))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#[cfg(unix)]
|
|
34
|
+
mod unix {
|
|
35
|
+
use std::ffi::CString;
|
|
36
|
+
use std::fs::{File, OpenOptions};
|
|
37
|
+
use std::io::{self, Read};
|
|
38
|
+
use std::os::fd::{AsRawFd, RawFd};
|
|
39
|
+
use std::os::unix::fs::OpenOptionsExt;
|
|
40
|
+
use std::path::{Path, PathBuf};
|
|
41
|
+
use std::sync::atomic::{AtomicI32, Ordering};
|
|
42
|
+
|
|
43
|
+
use rusqlite::{params, OptionalExtension};
|
|
44
|
+
|
|
45
|
+
use crate::event_log::EventLog;
|
|
46
|
+
use crate::message_store::MessageStore;
|
|
47
|
+
|
|
48
|
+
use super::{MessagingError, WaitResult};
|
|
49
|
+
|
|
50
|
+
static CAUGHT_SIGNAL: AtomicI32 = AtomicI32::new(0);
|
|
51
|
+
|
|
52
|
+
enum Registration {
|
|
53
|
+
Completed(String),
|
|
54
|
+
Pending,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
struct FifoEndpoints {
|
|
58
|
+
path: PathBuf,
|
|
59
|
+
reader: File,
|
|
60
|
+
_keepalive_writer: File,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
impl Drop for FifoEndpoints {
|
|
64
|
+
fn drop(&mut self) {
|
|
65
|
+
let _ = std::fs::remove_file(&self.path);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
struct RegistrationCleanup {
|
|
70
|
+
db_path: PathBuf,
|
|
71
|
+
watcher_id: String,
|
|
72
|
+
active: bool,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
impl RegistrationCleanup {
|
|
76
|
+
fn cleanup(&mut self) -> Result<(), MessagingError> {
|
|
77
|
+
if self.active {
|
|
78
|
+
let conn = crate::db::schema::open_db(&self.db_path)?;
|
|
79
|
+
conn.execute(
|
|
80
|
+
"delete from result_watchers where watcher_id = ?1",
|
|
81
|
+
params![self.watcher_id],
|
|
82
|
+
)?;
|
|
83
|
+
self.active = false;
|
|
84
|
+
}
|
|
85
|
+
Ok(())
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
impl Drop for RegistrationCleanup {
|
|
90
|
+
fn drop(&mut self) {
|
|
91
|
+
if self.active {
|
|
92
|
+
if let Ok(conn) = crate::db::schema::open_db(&self.db_path) {
|
|
93
|
+
let _ = conn.execute(
|
|
94
|
+
"delete from result_watchers where watcher_id = ?1",
|
|
95
|
+
params![self.watcher_id],
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
struct SignalGuard {
|
|
103
|
+
old_int: libc::sigaction,
|
|
104
|
+
old_term: libc::sigaction,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
impl SignalGuard {
|
|
108
|
+
fn install() -> io::Result<Self> {
|
|
109
|
+
CAUGHT_SIGNAL.store(0, Ordering::SeqCst);
|
|
110
|
+
let mut action: libc::sigaction = unsafe { std::mem::zeroed() };
|
|
111
|
+
action.sa_sigaction = record_signal as *const () as libc::sighandler_t;
|
|
112
|
+
action.sa_flags = 0;
|
|
113
|
+
unsafe {
|
|
114
|
+
libc::sigemptyset(&mut action.sa_mask);
|
|
115
|
+
}
|
|
116
|
+
let mut old_int: libc::sigaction = unsafe { std::mem::zeroed() };
|
|
117
|
+
let mut old_term: libc::sigaction = unsafe { std::mem::zeroed() };
|
|
118
|
+
if unsafe { libc::sigaction(libc::SIGINT, &action, &mut old_int) } != 0 {
|
|
119
|
+
return Err(io::Error::last_os_error());
|
|
120
|
+
}
|
|
121
|
+
if unsafe { libc::sigaction(libc::SIGTERM, &action, &mut old_term) } != 0 {
|
|
122
|
+
let error = io::Error::last_os_error();
|
|
123
|
+
unsafe {
|
|
124
|
+
libc::sigaction(libc::SIGINT, &old_int, std::ptr::null_mut());
|
|
125
|
+
}
|
|
126
|
+
return Err(error);
|
|
127
|
+
}
|
|
128
|
+
Ok(Self { old_int, old_term })
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
impl Drop for SignalGuard {
|
|
133
|
+
fn drop(&mut self) {
|
|
134
|
+
unsafe {
|
|
135
|
+
libc::sigaction(libc::SIGINT, &self.old_int, std::ptr::null_mut());
|
|
136
|
+
libc::sigaction(libc::SIGTERM, &self.old_term, std::ptr::null_mut());
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
extern "C" fn record_signal(signal: libc::c_int) {
|
|
142
|
+
CAUGHT_SIGNAL.store(signal, Ordering::SeqCst);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
pub fn wait_for_result(workspace: &Path, task_id: &str) -> Result<WaitResult, MessagingError> {
|
|
146
|
+
if task_id.trim().is_empty() {
|
|
147
|
+
return Err(MessagingError::Validation(
|
|
148
|
+
"wait --task requires a non-empty task id".to_string(),
|
|
149
|
+
));
|
|
150
|
+
}
|
|
151
|
+
let _signals = SignalGuard::install()?;
|
|
152
|
+
let store = MessageStore::open(workspace)?;
|
|
153
|
+
let workspace = std::fs::canonicalize(workspace)?;
|
|
154
|
+
let watcher_id = format!("wait-{}", random_suffix()?);
|
|
155
|
+
let fifo_path = fifo_path(&workspace, task_id, &watcher_id)?;
|
|
156
|
+
let fifo = create_fifo(&fifo_path)?;
|
|
157
|
+
let conn = crate::db::schema::open_db(store.db_path())?;
|
|
158
|
+
let registration = register(&conn, &watcher_id, task_id, &fifo_path)?;
|
|
159
|
+
if let Registration::Completed(result_id) = registration {
|
|
160
|
+
return Ok(WaitResult {
|
|
161
|
+
task_id: task_id.to_string(),
|
|
162
|
+
result_id,
|
|
163
|
+
waited: false,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let mut cleanup = RegistrationCleanup {
|
|
168
|
+
db_path: store.db_path().to_path_buf(),
|
|
169
|
+
watcher_id: watcher_id.clone(),
|
|
170
|
+
active: true,
|
|
171
|
+
};
|
|
172
|
+
EventLog::new(&workspace).write(
|
|
173
|
+
"result_wake.registered",
|
|
174
|
+
serde_json::json!({
|
|
175
|
+
"task_id": task_id,
|
|
176
|
+
"watcher_id": watcher_id,
|
|
177
|
+
"recipient": fifo_path,
|
|
178
|
+
}),
|
|
179
|
+
)?;
|
|
180
|
+
let result_id = read_wake_line(fifo.reader.as_raw_fd())?;
|
|
181
|
+
cleanup.cleanup()?;
|
|
182
|
+
Ok(WaitResult {
|
|
183
|
+
task_id: task_id.to_string(),
|
|
184
|
+
result_id,
|
|
185
|
+
waited: true,
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
fn register(
|
|
190
|
+
conn: &rusqlite::Connection,
|
|
191
|
+
watcher_id: &str,
|
|
192
|
+
task_id: &str,
|
|
193
|
+
fifo_path: &Path,
|
|
194
|
+
) -> Result<Registration, MessagingError> {
|
|
195
|
+
conn.execute_batch("BEGIN IMMEDIATE")?;
|
|
196
|
+
let operation = (|| -> Result<Registration, MessagingError> {
|
|
197
|
+
let completed = conn
|
|
198
|
+
.query_row(
|
|
199
|
+
"select result_id from results
|
|
200
|
+
where task_id = ?1 order by created_at, result_id limit 1",
|
|
201
|
+
params![task_id],
|
|
202
|
+
|row| row.get::<_, String>(0),
|
|
203
|
+
)
|
|
204
|
+
.optional()?;
|
|
205
|
+
if let Some(result_id) = completed {
|
|
206
|
+
return Ok(Registration::Completed(result_id));
|
|
207
|
+
}
|
|
208
|
+
conn.execute(
|
|
209
|
+
"insert into result_watchers(
|
|
210
|
+
watcher_id, owner_team_id, task_id, agent_id, message_id,
|
|
211
|
+
leader_id, recipient, status, created_at
|
|
212
|
+
) values (?1, null, ?2, null, null, 'team-agent', ?3, 'pending', ?4)",
|
|
213
|
+
params![
|
|
214
|
+
watcher_id,
|
|
215
|
+
task_id,
|
|
216
|
+
fifo_path.to_string_lossy().as_ref(),
|
|
217
|
+
chrono::Utc::now().to_rfc3339(),
|
|
218
|
+
],
|
|
219
|
+
)?;
|
|
220
|
+
Ok(Registration::Pending)
|
|
221
|
+
})();
|
|
222
|
+
match operation {
|
|
223
|
+
Ok(registration) => match conn.execute_batch("COMMIT") {
|
|
224
|
+
Ok(()) => Ok(registration),
|
|
225
|
+
Err(error) => {
|
|
226
|
+
let _ = conn.execute_batch("ROLLBACK");
|
|
227
|
+
Err(error.into())
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
Err(error) => {
|
|
231
|
+
let _ = conn.execute_batch("ROLLBACK");
|
|
232
|
+
Err(error)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
fn fifo_path(
|
|
238
|
+
workspace: &Path,
|
|
239
|
+
task_id: &str,
|
|
240
|
+
watcher_id: &str,
|
|
241
|
+
) -> Result<PathBuf, MessagingError> {
|
|
242
|
+
let dir = workspace.join(".team/runtime/wake");
|
|
243
|
+
std::fs::create_dir_all(&dir)?;
|
|
244
|
+
let task = task_id
|
|
245
|
+
.chars()
|
|
246
|
+
.map(|ch| {
|
|
247
|
+
if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_') {
|
|
248
|
+
ch
|
|
249
|
+
} else {
|
|
250
|
+
'_'
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
.take(80)
|
|
254
|
+
.collect::<String>();
|
|
255
|
+
let task = if task.is_empty() { "task" } else { &task };
|
|
256
|
+
Ok(dir.join(format!("{task}-{watcher_id}.fifo")))
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
fn random_suffix() -> Result<String, MessagingError> {
|
|
260
|
+
let mut bytes = [0_u8; 16];
|
|
261
|
+
File::open("/dev/urandom")?.read_exact(&mut bytes)?;
|
|
262
|
+
Ok(bytes.iter().map(|byte| format!("{byte:02x}")).collect())
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
fn create_fifo(path: &Path) -> Result<FifoEndpoints, MessagingError> {
|
|
266
|
+
let c_path = CString::new(path.as_os_str().as_encoded_bytes())
|
|
267
|
+
.map_err(|_| MessagingError::Validation("FIFO path contains a NUL byte".to_string()))?;
|
|
268
|
+
if unsafe { libc::mkfifo(c_path.as_ptr(), 0o600) } != 0 {
|
|
269
|
+
return Err(io::Error::last_os_error().into());
|
|
270
|
+
}
|
|
271
|
+
let reader = match OpenOptions::new()
|
|
272
|
+
.read(true)
|
|
273
|
+
.custom_flags(libc::O_NONBLOCK)
|
|
274
|
+
.open(path)
|
|
275
|
+
{
|
|
276
|
+
Ok(reader) => reader,
|
|
277
|
+
Err(error) => {
|
|
278
|
+
let _ = std::fs::remove_file(path);
|
|
279
|
+
return Err(error.into());
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
let keepalive_writer = match OpenOptions::new()
|
|
283
|
+
.write(true)
|
|
284
|
+
.custom_flags(libc::O_NONBLOCK)
|
|
285
|
+
.open(path)
|
|
286
|
+
{
|
|
287
|
+
Ok(writer) => writer,
|
|
288
|
+
Err(error) => {
|
|
289
|
+
let _ = std::fs::remove_file(path);
|
|
290
|
+
return Err(error.into());
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
set_blocking(reader.as_raw_fd())?;
|
|
294
|
+
Ok(FifoEndpoints {
|
|
295
|
+
path: path.to_path_buf(),
|
|
296
|
+
reader,
|
|
297
|
+
_keepalive_writer: keepalive_writer,
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
fn set_blocking(fd: RawFd) -> io::Result<()> {
|
|
302
|
+
let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
|
|
303
|
+
if flags < 0 {
|
|
304
|
+
return Err(io::Error::last_os_error());
|
|
305
|
+
}
|
|
306
|
+
if unsafe { libc::fcntl(fd, libc::F_SETFL, flags & !libc::O_NONBLOCK) } < 0 {
|
|
307
|
+
return Err(io::Error::last_os_error());
|
|
308
|
+
}
|
|
309
|
+
Ok(())
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
fn read_wake_line(fd: RawFd) -> Result<String, MessagingError> {
|
|
313
|
+
let mut line = Vec::new();
|
|
314
|
+
loop {
|
|
315
|
+
if let Some(signal) = caught_signal() {
|
|
316
|
+
return Err(interrupted(signal).into());
|
|
317
|
+
}
|
|
318
|
+
let mut bytes = [0_u8; 256];
|
|
319
|
+
let count =
|
|
320
|
+
unsafe { libc::read(fd, bytes.as_mut_ptr().cast::<libc::c_void>(), bytes.len()) };
|
|
321
|
+
if count > 0 {
|
|
322
|
+
line.extend_from_slice(&bytes[..count as usize]);
|
|
323
|
+
if let Some(newline) = line.iter().position(|byte| *byte == b'\n') {
|
|
324
|
+
line.truncate(newline);
|
|
325
|
+
let result_id = String::from_utf8(line).map_err(|error| {
|
|
326
|
+
MessagingError::Validation(format!("invalid FIFO wake line: {error}"))
|
|
327
|
+
})?;
|
|
328
|
+
let result_id = result_id.trim().to_string();
|
|
329
|
+
if result_id.is_empty() {
|
|
330
|
+
return Err(MessagingError::Validation(
|
|
331
|
+
"FIFO wake line did not contain a result id".to_string(),
|
|
332
|
+
));
|
|
333
|
+
}
|
|
334
|
+
return Ok(result_id);
|
|
335
|
+
}
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if count == 0 {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
let error = io::Error::last_os_error();
|
|
342
|
+
if error.kind() == io::ErrorKind::Interrupted {
|
|
343
|
+
if let Some(signal) = caught_signal() {
|
|
344
|
+
return Err(interrupted(signal).into());
|
|
345
|
+
}
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
return Err(error.into());
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
fn caught_signal() -> Option<i32> {
|
|
353
|
+
let signal = CAUGHT_SIGNAL.load(Ordering::SeqCst);
|
|
354
|
+
(signal != 0).then_some(signal)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
fn interrupted(signal: i32) -> io::Error {
|
|
358
|
+
io::Error::new(
|
|
359
|
+
io::ErrorKind::Interrupted,
|
|
360
|
+
format!("wait interrupted by signal {signal}"),
|
|
361
|
+
)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
#[cfg(unix)]
|
|
366
|
+
pub use unix::wait_for_result;
|
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
use std::path::Path;
|
|
4
4
|
|
|
5
|
+
#[cfg(unix)]
|
|
6
|
+
use std::fs::OpenOptions;
|
|
7
|
+
#[cfg(unix)]
|
|
8
|
+
use std::io::Write as _;
|
|
9
|
+
#[cfg(unix)]
|
|
10
|
+
use std::os::unix::fs::OpenOptionsExt;
|
|
11
|
+
|
|
5
12
|
use rusqlite::{params, OptionalExtension};
|
|
6
13
|
|
|
7
14
|
use crate::event_log::EventLog;
|
|
@@ -119,6 +126,128 @@ fn watcher_matches(
|
|
|
119
126
|
task_matches && agent_matches
|
|
120
127
|
}
|
|
121
128
|
|
|
129
|
+
pub(crate) fn notify_fifo_result_watchers(
|
|
130
|
+
conn: &rusqlite::Connection,
|
|
131
|
+
event_log: &EventLog,
|
|
132
|
+
task_id: &str,
|
|
133
|
+
result_id: &str,
|
|
134
|
+
) -> Result<(), MessagingError> {
|
|
135
|
+
let mut stmt = conn.prepare(
|
|
136
|
+
"select watcher_id, recipient from result_watchers
|
|
137
|
+
where task_id = ?1 and status = 'pending' and recipient is not null
|
|
138
|
+
order by created_at, watcher_id",
|
|
139
|
+
)?;
|
|
140
|
+
let rows = stmt
|
|
141
|
+
.query_map(params![task_id], |row| {
|
|
142
|
+
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
|
|
143
|
+
})?
|
|
144
|
+
.collect::<Result<Vec<_>, _>>()?;
|
|
145
|
+
drop(stmt);
|
|
146
|
+
for (watcher_id, recipient) in rows {
|
|
147
|
+
let _ = notify_fifo_waiter(conn, event_log, &watcher_id, task_id, result_id, &recipient)?;
|
|
148
|
+
}
|
|
149
|
+
Ok(())
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#[cfg(unix)]
|
|
153
|
+
fn notify_fifo_waiter(
|
|
154
|
+
conn: &rusqlite::Connection,
|
|
155
|
+
event_log: &EventLog,
|
|
156
|
+
watcher_id: &str,
|
|
157
|
+
task_id: &str,
|
|
158
|
+
result_id: &str,
|
|
159
|
+
recipient: &str,
|
|
160
|
+
) -> Result<WatcherNotice, MessagingError> {
|
|
161
|
+
let write = (|| -> std::io::Result<()> {
|
|
162
|
+
let mut fifo = OpenOptions::new()
|
|
163
|
+
.write(true)
|
|
164
|
+
.custom_flags(libc::O_NONBLOCK)
|
|
165
|
+
.open(recipient)?;
|
|
166
|
+
fifo.write_all(format!("{result_id}\n").as_bytes())
|
|
167
|
+
})();
|
|
168
|
+
match write {
|
|
169
|
+
Ok(()) => {
|
|
170
|
+
event_log.write(
|
|
171
|
+
"result_wake.notified",
|
|
172
|
+
serde_json::json!({
|
|
173
|
+
"task_id": task_id,
|
|
174
|
+
"result_id": result_id,
|
|
175
|
+
"watcher_id": watcher_id,
|
|
176
|
+
"recipient": recipient,
|
|
177
|
+
}),
|
|
178
|
+
)?;
|
|
179
|
+
Ok(WatcherNotice {
|
|
180
|
+
watcher_id: watcher_id.to_string(),
|
|
181
|
+
result_id: Some(result_id.to_string()),
|
|
182
|
+
ok: true,
|
|
183
|
+
status: Some("notified".to_string()),
|
|
184
|
+
notified_message_id: None,
|
|
185
|
+
primary_watcher_id: None,
|
|
186
|
+
prior_state: None,
|
|
187
|
+
error: None,
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
Err(error)
|
|
191
|
+
if matches!(
|
|
192
|
+
error.raw_os_error(),
|
|
193
|
+
Some(code) if code == libc::ENXIO || code == libc::ENOENT
|
|
194
|
+
) =>
|
|
195
|
+
{
|
|
196
|
+
let reason = if error.raw_os_error() == Some(libc::ENXIO) {
|
|
197
|
+
"ENXIO"
|
|
198
|
+
} else {
|
|
199
|
+
"ENOENT"
|
|
200
|
+
};
|
|
201
|
+
conn.execute(
|
|
202
|
+
"delete from result_watchers where watcher_id = ?1",
|
|
203
|
+
params![watcher_id],
|
|
204
|
+
)?;
|
|
205
|
+
event_log.write(
|
|
206
|
+
"result_wake.notify_failed",
|
|
207
|
+
serde_json::json!({
|
|
208
|
+
"task_id": task_id,
|
|
209
|
+
"result_id": result_id,
|
|
210
|
+
"watcher_id": watcher_id,
|
|
211
|
+
"recipient": recipient,
|
|
212
|
+
"reason": reason,
|
|
213
|
+
}),
|
|
214
|
+
)?;
|
|
215
|
+
Ok(WatcherNotice {
|
|
216
|
+
watcher_id: watcher_id.to_string(),
|
|
217
|
+
result_id: Some(result_id.to_string()),
|
|
218
|
+
ok: false,
|
|
219
|
+
status: Some("removed".to_string()),
|
|
220
|
+
notified_message_id: None,
|
|
221
|
+
primary_watcher_id: None,
|
|
222
|
+
prior_state: None,
|
|
223
|
+
error: Some(reason.to_string()),
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
Err(error) => Err(error.into()),
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
#[cfg(not(unix))]
|
|
231
|
+
fn notify_fifo_waiter(
|
|
232
|
+
_conn: &rusqlite::Connection,
|
|
233
|
+
_event_log: &EventLog,
|
|
234
|
+
watcher_id: &str,
|
|
235
|
+
_task_id: &str,
|
|
236
|
+
result_id: &str,
|
|
237
|
+
_recipient: &str,
|
|
238
|
+
) -> Result<WatcherNotice, MessagingError> {
|
|
239
|
+
Ok(WatcherNotice {
|
|
240
|
+
watcher_id: watcher_id.to_string(),
|
|
241
|
+
result_id: Some(result_id.to_string()),
|
|
242
|
+
ok: false,
|
|
243
|
+
status: Some("unsupported".to_string()),
|
|
244
|
+
notified_message_id: None,
|
|
245
|
+
primary_watcher_id: None,
|
|
246
|
+
prior_state: None,
|
|
247
|
+
error: Some("POSIX FIFO support unavailable".to_string()),
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
122
251
|
fn deliver_primary_watcher(
|
|
123
252
|
workspace: &Path,
|
|
124
253
|
conn: &rusqlite::Connection,
|
|
@@ -143,6 +272,20 @@ fn deliver_primary_watcher(
|
|
|
143
272
|
"missing_result_id",
|
|
144
273
|
);
|
|
145
274
|
};
|
|
275
|
+
if let Some(recipient) = watcher
|
|
276
|
+
.get("recipient")
|
|
277
|
+
.and_then(|value| value.as_str())
|
|
278
|
+
.filter(|recipient| Path::new(recipient).is_absolute())
|
|
279
|
+
{
|
|
280
|
+
return notify_fifo_waiter(
|
|
281
|
+
conn,
|
|
282
|
+
event_log,
|
|
283
|
+
watcher_id,
|
|
284
|
+
result_task.unwrap_or_default(),
|
|
285
|
+
result_id,
|
|
286
|
+
recipient,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
146
289
|
if let Some(existing) = delivered_result_message(
|
|
147
290
|
store,
|
|
148
291
|
result_id,
|
|
@@ -196,7 +339,10 @@ fn deliver_primary_watcher(
|
|
|
196
339
|
.get("leader_id")
|
|
197
340
|
.and_then(|v| v.as_str())
|
|
198
341
|
.unwrap_or("team-agent"),
|
|
199
|
-
|
|
342
|
+
watcher
|
|
343
|
+
.get("recipient")
|
|
344
|
+
.and_then(|value| value.as_str())
|
|
345
|
+
.unwrap_or("leader"),
|
|
200
346
|
&content,
|
|
201
347
|
None,
|
|
202
348
|
false,
|
|
@@ -332,7 +478,7 @@ pub fn retry_result_deliveries(
|
|
|
332
478
|
// delivery path with dedupe/attempt bounds); a watcher is never flipped to
|
|
333
479
|
// `notified` without a delivery. Missing result rows are skipped (still retryable).
|
|
334
480
|
let mut stmt = conn.prepare(
|
|
335
|
-
"select watcher_id, owner_team_id, task_id, agent_id, leader_id, status, created_at,
|
|
481
|
+
"select watcher_id, owner_team_id, task_id, agent_id, leader_id, recipient, status, created_at,
|
|
336
482
|
result_id, notified_message_id
|
|
337
483
|
from result_watchers
|
|
338
484
|
where status in ('pending', 'notify_failed')
|
|
@@ -346,10 +492,11 @@ pub fn retry_result_deliveries(
|
|
|
346
492
|
"task_id": row.get::<_, Option<String>>(2)?,
|
|
347
493
|
"agent_id": row.get::<_, Option<String>>(3)?,
|
|
348
494
|
"leader_id": row.get::<_, Option<String>>(4)?,
|
|
349
|
-
"
|
|
350
|
-
"
|
|
351
|
-
"
|
|
352
|
-
"
|
|
495
|
+
"recipient": row.get::<_, Option<String>>(5)?,
|
|
496
|
+
"status": row.get::<_, Option<String>>(6)?,
|
|
497
|
+
"created_at": row.get::<_, Option<String>>(7)?,
|
|
498
|
+
"result_id": row.get::<_, Option<String>>(8)?,
|
|
499
|
+
"notified_message_id": row.get::<_, Option<String>>(9)?,
|
|
353
500
|
}))
|
|
354
501
|
})?
|
|
355
502
|
.collect::<Result<Vec<_>, _>>()?;
|