@team-agent/installer 0.3.23 → 0.3.25
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/src/coordinator/tick.rs +55 -7
- package/crates/team-agent/src/leader/lease.rs +34 -13
- package/crates/team-agent/src/leader/tests/lease_api.rs +79 -0
- package/crates/team-agent/src/lifecycle/helpers.rs +11 -3
- package/crates/team-agent/src/lifecycle/launch.rs +219 -35
- package/crates/team-agent/src/lifecycle/restart/agent.rs +61 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +66 -1
- package/crates/team-agent/src/lifecycle/restart.rs +19 -2
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +994 -0
- package/crates/team-agent/src/messaging/delivery.rs +86 -27
- package/crates/team-agent/src/messaging/helpers.rs +64 -24
- package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
- package/crates/team-agent/src/messaging/tests/spine.rs +157 -4
- package/crates/team-agent/src/messaging/tests/wave2.rs +689 -0
- package/crates/team-agent/src/state/projection.rs +5 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +297 -0
- package/crates/team-agent/src/tmux_backend.rs +145 -5
- package/crates/team-agent/src/transport/test_support.rs +79 -6
- package/crates/team-agent/src/transport.rs +26 -1
- package/npm/install.mjs +90 -25
- package/package.json +4 -4
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -544,13 +544,27 @@ impl Coordinator {
|
|
|
544
544
|
.get("last_output_at")
|
|
545
545
|
.and_then(Value::as_str)
|
|
546
546
|
.map(str::to_string);
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
)
|
|
547
|
+
// E47 (0.3.24 P0, idle/busy 假阳): consult the AUTHORITATIVE
|
|
548
|
+
// provider JSONL classifier BEFORE the TUI keyword scan. The
|
|
549
|
+
// scrollback Tail(40) keeps historical `• Working (...· esc to
|
|
550
|
+
// interrupt)` + spinners within the 40-line window long after a
|
|
551
|
+
// turn completed, so `classify_agent_activity`'s rfind-recency
|
|
552
|
+
// grep flips a truly-idle worker to Working/Stuck. The provider
|
|
553
|
+
// JSONL (codex task_complete / claude end_turn) is the single
|
|
554
|
+
// source of truth for "turn closed". Only fall back to TUI
|
|
555
|
+
// scanning when the JSONL is unreadable or no lifecycle fact has
|
|
556
|
+
// been written yet (TurnState::Unknown) — never silently fall
|
|
557
|
+
// through into the leaky grep on a CONFIRMED IDLE.
|
|
558
|
+
let activity_from_jsonl = jsonl_activity_for_agent(agent);
|
|
559
|
+
let activity = activity_from_jsonl.unwrap_or_else(|| {
|
|
560
|
+
crate::messaging::classify_agent_activity(
|
|
561
|
+
&snapshot,
|
|
562
|
+
&captured.text,
|
|
563
|
+
pane_in_mode,
|
|
564
|
+
current_command.as_deref(),
|
|
565
|
+
last_output_at_now.as_deref(),
|
|
566
|
+
)
|
|
567
|
+
});
|
|
554
568
|
remember_idle_capture_schedule(agent, &activity);
|
|
555
569
|
write_activity(agent, &activity, false);
|
|
556
570
|
let last_output_at = last_output_at_now;
|
|
@@ -2519,6 +2533,40 @@ fn agent_rollout_path(agent: &Value) -> Option<PathBuf> {
|
|
|
2519
2533
|
.map(PathBuf::from)
|
|
2520
2534
|
}
|
|
2521
2535
|
|
|
2536
|
+
/// E47 (0.3.24 P0, idle/busy 假阳): consult the authoritative provider JSONL
|
|
2537
|
+
/// classifier and map to neutral `AgentActivity`. Returns `None` when the
|
|
2538
|
+
/// classifier reports `TurnState::Unknown` (unreadable JSONL / no lifecycle
|
|
2539
|
+
/// fact yet) so the caller falls back to the TUI scan — this honours the
|
|
2540
|
+
/// IRON LAW (activity.rs:3 / bug-071/077/085): no-signal = Uncertain (not
|
|
2541
|
+
/// silently coerced to Idle); but here Unknown means "JSONL gave no signal",
|
|
2542
|
+
/// so we hand off to the TUI scanner which has its OWN no-signal → Uncertain
|
|
2543
|
+
/// path. Copilot/Gemini/Fake providers (which don't have JSONL — classify.rs
|
|
2544
|
+
/// returns Unknown for them) thus keep using TUI scanning unchanged.
|
|
2545
|
+
fn jsonl_activity_for_agent(agent: &Value) -> Option<crate::messaging::AgentActivity> {
|
|
2546
|
+
let rollout_path = agent_rollout_path(agent)?;
|
|
2547
|
+
let provider = agent
|
|
2548
|
+
.get("provider")
|
|
2549
|
+
.and_then(Value::as_str)
|
|
2550
|
+
.and_then(parse_provider)?;
|
|
2551
|
+
let log_text = std::fs::read_to_string(&rollout_path).ok()?;
|
|
2552
|
+
let process = explicit_process_liveness(agent).unwrap_or(ProcessLiveness::Unverifiable);
|
|
2553
|
+
let result = crate::provider::classify(provider, &log_text, process, 0.0).ok()?;
|
|
2554
|
+
use crate::messaging::{ActivityStatus, AgentActivity};
|
|
2555
|
+
use crate::provider::types::TurnState;
|
|
2556
|
+
let status = match result.state {
|
|
2557
|
+
TurnState::Idle => ActivityStatus::Idle,
|
|
2558
|
+
TurnState::IdleInterrupted => ActivityStatus::Idle,
|
|
2559
|
+
TurnState::Working => ActivityStatus::Working,
|
|
2560
|
+
TurnState::BlockedOnHuman | TurnState::Abnormal => ActivityStatus::Uncertain,
|
|
2561
|
+
TurnState::Unknown => return None,
|
|
2562
|
+
};
|
|
2563
|
+
Some(AgentActivity {
|
|
2564
|
+
status,
|
|
2565
|
+
confidence: 0.95,
|
|
2566
|
+
rationale: format!("provider_jsonl:{}", result.reason),
|
|
2567
|
+
})
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2522
2570
|
fn runtime_approval_target(
|
|
2523
2571
|
agent: &Value,
|
|
2524
2572
|
session_name: Option<&str>,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//! + 双写 / 分叉检测。
|
|
3
3
|
|
|
4
4
|
use std::collections::BTreeSet;
|
|
5
|
-
use std::path::Path;
|
|
5
|
+
use std::path::{Path, PathBuf};
|
|
6
6
|
|
|
7
7
|
use serde_json::{json, Value};
|
|
8
8
|
|
|
@@ -1024,14 +1024,27 @@ fn state_owner(state: &Value) -> Option<TeamOwner> {
|
|
|
1024
1024
|
pub fn write_lease_dual_state(workspace: &Path, state: &Value) -> Result<(), LeaderError> {
|
|
1025
1025
|
crate::state::persist::save_runtime_state(workspace, state)?;
|
|
1026
1026
|
if let Some(session_name) = state.get("session_name").and_then(Value::as_str) {
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1027
|
+
write_team_snapshot_atomic(workspace, session_name, state)?;
|
|
1028
|
+
}
|
|
1029
|
+
Ok(())
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
fn write_team_snapshot_atomic(
|
|
1033
|
+
workspace: &Path,
|
|
1034
|
+
session_name: &str,
|
|
1035
|
+
state: &Value,
|
|
1036
|
+
) -> Result<(), LeaderError> {
|
|
1037
|
+
let snap_path = crate::lifecycle::helpers::team_snapshot_path(workspace, session_name);
|
|
1038
|
+
let parent = snap_path
|
|
1039
|
+
.parent()
|
|
1040
|
+
.ok_or_else(|| LeaderError::Validation("team snapshot path has no parent".to_string()))?;
|
|
1041
|
+
std::fs::create_dir_all(parent)?;
|
|
1042
|
+
let tmp = parent.join(format!("state.json.tmp-{}", std::process::id()));
|
|
1043
|
+
let data = serde_json::to_vec_pretty(state)?;
|
|
1044
|
+
std::fs::write(&tmp, data)?;
|
|
1045
|
+
if let Err(error) = std::fs::rename(&tmp, &snap_path) {
|
|
1046
|
+
let _ = std::fs::remove_file(&tmp);
|
|
1047
|
+
return Err(error.into());
|
|
1035
1048
|
}
|
|
1036
1049
|
Ok(())
|
|
1037
1050
|
}
|
|
@@ -1113,10 +1126,7 @@ pub fn detect_dual_state_divergence(
|
|
|
1113
1126
|
let Some(session_name) = state.get("session_name").and_then(Value::as_str) else {
|
|
1114
1127
|
return Ok(None);
|
|
1115
1128
|
};
|
|
1116
|
-
let snap_path =
|
|
1117
|
-
.join("teams")
|
|
1118
|
-
.join(session_name)
|
|
1119
|
-
.join("state.json");
|
|
1129
|
+
let snap_path = readable_team_snapshot_path(workspace, session_name);
|
|
1120
1130
|
if !snap_path.exists() {
|
|
1121
1131
|
return Ok(None);
|
|
1122
1132
|
}
|
|
@@ -1150,6 +1160,17 @@ pub fn detect_dual_state_divergence(
|
|
|
1150
1160
|
})))
|
|
1151
1161
|
}
|
|
1152
1162
|
|
|
1163
|
+
fn readable_team_snapshot_path(workspace: &Path, session_name: &str) -> PathBuf {
|
|
1164
|
+
let safe_path = crate::lifecycle::helpers::team_snapshot_path(workspace, session_name);
|
|
1165
|
+
if safe_path.exists() {
|
|
1166
|
+
return safe_path;
|
|
1167
|
+
}
|
|
1168
|
+
crate::model::paths::runtime_dir(workspace)
|
|
1169
|
+
.join("teams")
|
|
1170
|
+
.join(session_name)
|
|
1171
|
+
.join("state.json")
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1153
1174
|
#[cfg(test)]
|
|
1154
1175
|
mod tests {
|
|
1155
1176
|
use super::*;
|
|
@@ -145,6 +145,85 @@ use super::*;
|
|
|
145
145
|
);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
#[test]
|
|
149
|
+
fn lease_divergence_reads_restart_snapshot_for_special_session_names() {
|
|
150
|
+
let ws = std::env::temp_dir().join(format!("ta_rs_dual_special_{}", std::process::id()));
|
|
151
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
152
|
+
let state = serde_json::json!({
|
|
153
|
+
"session_name": "team:proj-中文",
|
|
154
|
+
"team_owner": {"pane_id": "%1", "owner_epoch": 2, "leader_session_uuid": "uuuu"},
|
|
155
|
+
"leader_receiver": {"pane_id": "%1", "owner_epoch": 2},
|
|
156
|
+
});
|
|
157
|
+
write_lease_dual_state(&ws, &state).unwrap();
|
|
158
|
+
let safe_path = crate::lifecycle::helpers::team_snapshot_path(&ws, "team:proj-中文");
|
|
159
|
+
let raw_path = crate::model::paths::runtime_dir(&ws)
|
|
160
|
+
.join("teams")
|
|
161
|
+
.join("team:proj-中文")
|
|
162
|
+
.join("state.json");
|
|
163
|
+
assert!(
|
|
164
|
+
safe_path.exists(),
|
|
165
|
+
"lease snapshot must use the restart-safe path: {}",
|
|
166
|
+
safe_path.display()
|
|
167
|
+
);
|
|
168
|
+
assert!(
|
|
169
|
+
!raw_path.exists(),
|
|
170
|
+
"new lease writes must not create the legacy raw session path: {}",
|
|
171
|
+
raw_path.display()
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
let restart_state = serde_json::json!({
|
|
175
|
+
"session_name": "team:proj-中文",
|
|
176
|
+
"team_owner": {"pane_id": "%9", "owner_epoch": 2, "leader_session_uuid": "uuuu"},
|
|
177
|
+
"leader_receiver": {"pane_id": "%9", "owner_epoch": 2},
|
|
178
|
+
});
|
|
179
|
+
let restart_path = crate::lifecycle::save_team_runtime_snapshot(&ws, &restart_state).unwrap();
|
|
180
|
+
assert_eq!(restart_path, safe_path);
|
|
181
|
+
assert!(
|
|
182
|
+
restart_path.exists(),
|
|
183
|
+
"restart snapshot must be written to a real file: {}",
|
|
184
|
+
restart_path.display()
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
let d = detect_dual_state_divergence(&ws, &state)
|
|
188
|
+
.unwrap()
|
|
189
|
+
.expect("divergence detector must read the restart-written snapshot");
|
|
190
|
+
assert_eq!(d["workspace_owner_pane"], serde_json::json!("%1"));
|
|
191
|
+
assert_eq!(d["team_owner_pane"], serde_json::json!("%9"));
|
|
192
|
+
assert_eq!(d["workspace_receiver_pane"], serde_json::json!("%1"));
|
|
193
|
+
assert_eq!(d["team_receiver_pane"], serde_json::json!("%9"));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
#[test]
|
|
197
|
+
fn detect_dual_state_divergence_reads_legacy_raw_snapshot_when_safe_absent() {
|
|
198
|
+
let ws = std::env::temp_dir().join(format!("ta_rs_dual_legacy_{}", std::process::id()));
|
|
199
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
200
|
+
let state = serde_json::json!({
|
|
201
|
+
"session_name": "team:legacy-中文",
|
|
202
|
+
"team_owner": {"pane_id": "%1", "leader_session_uuid": "uuuu", "owner_epoch": 2},
|
|
203
|
+
"leader_receiver": {"pane_id": "%1", "owner_epoch": 2},
|
|
204
|
+
});
|
|
205
|
+
let safe_path = crate::lifecycle::helpers::team_snapshot_path(&ws, "team:legacy-中文");
|
|
206
|
+
assert!(!safe_path.exists(), "test setup requires safe path absent");
|
|
207
|
+
let raw_dir = crate::model::paths::runtime_dir(&ws)
|
|
208
|
+
.join("teams")
|
|
209
|
+
.join("team:legacy-中文");
|
|
210
|
+
std::fs::create_dir_all(&raw_dir).unwrap();
|
|
211
|
+
let snap = serde_json::json!({
|
|
212
|
+
"session_name": "team:legacy-中文",
|
|
213
|
+
"team_owner": {"pane_id": "%8", "leader_session_uuid": "uuuu", "owner_epoch": 2},
|
|
214
|
+
"leader_receiver": {"pane_id": "%8", "owner_epoch": 2},
|
|
215
|
+
});
|
|
216
|
+
std::fs::write(raw_dir.join("state.json"), serde_json::to_string(&snap).unwrap()).unwrap();
|
|
217
|
+
|
|
218
|
+
let d = detect_dual_state_divergence(&ws, &state)
|
|
219
|
+
.unwrap()
|
|
220
|
+
.expect("legacy raw snapshot fallback must preserve old teams");
|
|
221
|
+
assert_eq!(d["workspace_owner_pane"], serde_json::json!("%1"));
|
|
222
|
+
assert_eq!(d["team_owner_pane"], serde_json::json!("%8"));
|
|
223
|
+
assert_eq!(d["workspace_receiver_pane"], serde_json::json!("%1"));
|
|
224
|
+
assert_eq!(d["team_receiver_pane"], serde_json::json!("%8"));
|
|
225
|
+
}
|
|
226
|
+
|
|
148
227
|
// detect_dual_state_divergence:无 session_name → None(__init__.py:560-561);unimplemented → RED。
|
|
149
228
|
#[test]
|
|
150
229
|
fn detect_dual_state_divergence_none_without_session_name() {
|
|
@@ -16,14 +16,15 @@ pub fn save_team_runtime_snapshot(
|
|
|
16
16
|
.get("session_name")
|
|
17
17
|
.and_then(|v| v.as_str())
|
|
18
18
|
.ok_or_else(|| LifecycleError::StatePersist("session_name is required".to_string()))?;
|
|
19
|
-
let safe = safe_snapshot_name(session_name);
|
|
20
19
|
// golden restart/snapshot.py:46-47 — team_runtime_snapshot_dir = runtime_dir(workspace)/teams/<safe>,
|
|
21
20
|
// and paths.py:25-26 runtime_dir = workspace/.team/runtime. Use the crate path helper so the snapshot
|
|
22
21
|
// lands at <ws>/.team/runtime/teams/<safe>, matching golden AND the rest of the crate (not the
|
|
23
22
|
// ".team"-less <ws>/runtime/teams that the original port wrote).
|
|
24
|
-
let
|
|
23
|
+
let path = team_snapshot_path(workspace, session_name);
|
|
24
|
+
let dir = path
|
|
25
|
+
.parent()
|
|
26
|
+
.ok_or_else(|| LifecycleError::StatePersist("snapshot path has no parent".to_string()))?;
|
|
25
27
|
fs::create_dir_all(&dir).map_err(|e| persist_err("create snapshot dir", &e))?;
|
|
26
|
-
let path = dir.join("state.json");
|
|
27
28
|
let tmp = dir.join("state.json.tmp");
|
|
28
29
|
let data = serde_json::to_vec_pretty(state)
|
|
29
30
|
.map_err(|e| LifecycleError::StatePersist(format!("serialize snapshot: {e}")))?;
|
|
@@ -32,6 +33,13 @@ pub fn save_team_runtime_snapshot(
|
|
|
32
33
|
Ok(path)
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
pub fn team_snapshot_path(workspace: &Path, session_name: &str) -> PathBuf {
|
|
37
|
+
crate::model::paths::runtime_dir(workspace)
|
|
38
|
+
.join("teams")
|
|
39
|
+
.join(safe_snapshot_name(session_name))
|
|
40
|
+
.join("state.json")
|
|
41
|
+
}
|
|
42
|
+
|
|
35
43
|
fn persist_err(action: &str, err: &io::Error) -> LifecycleError {
|
|
36
44
|
LifecycleError::StatePersist(format!("{action}: {err}"))
|
|
37
45
|
}
|