@team-agent/installer 0.3.1 → 0.3.3
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 +34 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +234 -26
- package/crates/team-agent/src/cli/diagnose.rs +144 -10
- package/crates/team-agent/src/cli/emit.rs +289 -54
- package/crates/team-agent/src/cli/leader.rs +37 -8
- package/crates/team-agent/src/cli/mod.rs +1281 -196
- package/crates/team-agent/src/cli/status_port.rs +195 -46
- package/crates/team-agent/src/cli/tests/divergence.rs +1 -2
- package/crates/team-agent/src/cli/tests/lane_c.rs +23 -13
- package/crates/team-agent/src/cli/tests/main_preserved.rs +2 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +59 -3
- package/crates/team-agent/src/cli/types.rs +18 -0
- package/crates/team-agent/src/compiler.rs +15 -5
- package/crates/team-agent/src/coordinator/health.rs +95 -17
- package/crates/team-agent/src/coordinator/mod.rs +4 -0
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +500 -0
- package/crates/team-agent/src/coordinator/runtime_observation.rs +58 -0
- package/crates/team-agent/src/coordinator/tick.rs +222 -69
- package/crates/team-agent/src/coordinator/types.rs +15 -3
- package/crates/team-agent/src/db/schema.rs +37 -2
- package/crates/team-agent/src/diagnose/comms.rs +226 -0
- package/crates/team-agent/src/diagnose/mod.rs +45 -0
- package/crates/team-agent/src/diagnose/orphans.rs +658 -0
- package/crates/team-agent/src/fake_worker.rs +146 -3
- package/crates/team-agent/src/leader/start.rs +121 -23
- package/crates/team-agent/src/leader/types.rs +44 -1
- package/crates/team-agent/src/lib.rs +3 -0
- package/crates/team-agent/src/lifecycle/display.rs +645 -47
- package/crates/team-agent/src/lifecycle/launch.rs +1061 -146
- package/crates/team-agent/src/lifecycle/mod.rs +2 -0
- package/crates/team-agent/src/lifecycle/profile_launch.rs +810 -0
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +522 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +99 -23
- package/crates/team-agent/src/lifecycle/restart/common.rs +183 -24
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +498 -22
- package/crates/team-agent/src/lifecycle/restart/remove.rs +27 -7
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +19 -0
- package/crates/team-agent/src/lifecycle/restart.rs +24 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +5 -5
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +37 -7
- package/crates/team-agent/src/lifecycle/types.rs +19 -0
- package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +341 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +10 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +158 -0
- package/crates/team-agent/src/mcp_server/mod.rs +3 -74
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -1
- package/crates/team-agent/src/mcp_server/tests/send.rs +6 -5
- package/crates/team-agent/src/mcp_server/tools.rs +312 -111
- package/crates/team-agent/src/mcp_server/types.rs +6 -4
- package/crates/team-agent/src/mcp_server/wire.rs +19 -7
- package/crates/team-agent/src/message_store.rs +21 -4
- package/crates/team-agent/src/messaging/delivery.rs +470 -59
- package/crates/team-agent/src/messaging/mod.rs +9 -6
- package/crates/team-agent/src/messaging/results.rs +353 -63
- package/crates/team-agent/src/messaging/selftest.rs +199 -12
- package/crates/team-agent/src/messaging/send.rs +35 -3
- package/crates/team-agent/src/messaging/tests/runtime.rs +19 -4
- package/crates/team-agent/src/messaging/types.rs +11 -3
- package/crates/team-agent/src/os_probe.rs +119 -0
- package/crates/team-agent/src/packaging/migrate.rs +10 -2
- package/crates/team-agent/src/packaging/tests.rs +23 -0
- package/crates/team-agent/src/provider/adapter.rs +564 -63
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +1 -7
- package/crates/team-agent/src/provider/classify.rs +51 -4
- package/crates/team-agent/src/provider/helpers.rs +10 -1
- package/crates/team-agent/src/provider/startup_prompt.rs +94 -0
- package/crates/team-agent/src/provider/types.rs +47 -0
- package/crates/team-agent/src/session_capture.rs +616 -0
- package/crates/team-agent/src/state/persist.rs +170 -1
- package/crates/team-agent/src/state/projection.rs +141 -8
- package/crates/team-agent/src/state/selector.rs +5 -2
- package/crates/team-agent/src/tmux_backend.rs +161 -64
- package/crates/team-agent/src/transport/test_support.rs +9 -0
- package/crates/team-agent/src/transport/tests/wire.rs +4 -0
- package/crates/team-agent/src/transport.rs +13 -2
- package/package.json +4 -4
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
//! (`identity::migrate_state_identity`,补 leader_session_uuid)→ `_migrate_active_team_key`
|
|
21
21
|
//! (seed active 指针);任一改动 → `save_runtime_state` 回写。不存在且命中缓存 → 返回缓存 deepcopy。
|
|
22
22
|
|
|
23
|
-
use std::collections::HashMap;
|
|
23
|
+
use std::collections::{BTreeSet, HashMap};
|
|
24
24
|
use std::io;
|
|
25
25
|
use std::path::{Path, PathBuf};
|
|
26
26
|
use std::sync::{LazyLock, Mutex};
|
|
@@ -170,6 +170,14 @@ impl Drop for RuntimeLock {
|
|
|
170
170
|
/// `save_runtime_state`(bug-084)。`state` 是 state.json 的内存 Value(插入序保留)。
|
|
171
171
|
/// 注:Python 在此还调 `_migrate_state_identity`(identity slice 落地后接入;本 slice 不改 state 内容)。
|
|
172
172
|
pub fn save_runtime_state(workspace: &Path, state: &Value) -> Result<(), StateError> {
|
|
173
|
+
save_runtime_state_with_deleted_agents(workspace, state, &[])
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
pub(crate) fn save_runtime_state_with_deleted_agents(
|
|
177
|
+
workspace: &Path,
|
|
178
|
+
state: &Value,
|
|
179
|
+
deleted_agent_ids: &[&str],
|
|
180
|
+
) -> Result<(), StateError> {
|
|
173
181
|
let path = runtime_state_path(workspace);
|
|
174
182
|
if cache_equals(&path, state) {
|
|
175
183
|
return Ok(());
|
|
@@ -203,6 +211,15 @@ pub fn save_runtime_state(workspace: &Path, state: &Value) -> Result<(), StateEr
|
|
|
203
211
|
if let Some(parent) = path.parent() {
|
|
204
212
|
std::fs::create_dir_all(parent)?;
|
|
205
213
|
}
|
|
214
|
+
if let Some(latest) = read_latest_state_under_lock(workspace, &path) {
|
|
215
|
+
let deleted = deleted_agent_ids
|
|
216
|
+
.iter()
|
|
217
|
+
.copied()
|
|
218
|
+
.filter(|id| !id.is_empty())
|
|
219
|
+
.map(str::to_string)
|
|
220
|
+
.collect::<BTreeSet<_>>();
|
|
221
|
+
preserve_latest_roster_entries(&mut migrated, &latest, &deleted);
|
|
222
|
+
}
|
|
206
223
|
// 字节对拍 Python json.dumps(indent=2, ensure_ascii=False)(无尾换行)。
|
|
207
224
|
let payload = serde_json::to_string_pretty(&migrated)?;
|
|
208
225
|
let delays = [0.05_f64, 0.2, 0.5];
|
|
@@ -243,6 +260,158 @@ pub fn save_runtime_state(workspace: &Path, state: &Value) -> Result<(), StateEr
|
|
|
243
260
|
Err(StateError::SaveFailed("retry loop exhausted without return".to_string()))
|
|
244
261
|
}
|
|
245
262
|
|
|
263
|
+
fn read_latest_state_under_lock(workspace: &Path, path: &Path) -> Option<Value> {
|
|
264
|
+
let text = std::fs::read_to_string(path).ok()?;
|
|
265
|
+
let mut latest = serde_json::from_str::<Value>(&text).ok()?;
|
|
266
|
+
normalize_agent_session_state(&mut latest);
|
|
267
|
+
let _ = migrate_state_identity(&mut latest, &SystemEnv, workspace);
|
|
268
|
+
let _ = migrate_active_team_key(&mut latest);
|
|
269
|
+
Some(latest)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
fn preserve_latest_roster_entries(incoming: &mut Value, latest: &Value, deleted_agent_ids: &BTreeSet<String>) {
|
|
273
|
+
if !same_runtime_projection(incoming, latest) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
preserve_missing_agents(incoming.get_mut("agents"), latest.get("agents"), deleted_agent_ids);
|
|
277
|
+
preserve_latest_ownership_fields(incoming, latest);
|
|
278
|
+
|
|
279
|
+
let active_team = active_team_key(incoming).or_else(|| active_team_key(latest));
|
|
280
|
+
if let Some(active_team) = active_team.as_deref() {
|
|
281
|
+
let latest_active_agents = latest
|
|
282
|
+
.get("teams")
|
|
283
|
+
.and_then(Value::as_object)
|
|
284
|
+
.and_then(|teams| teams.get(active_team))
|
|
285
|
+
.and_then(|entry| entry.get("agents"));
|
|
286
|
+
preserve_missing_agents(incoming.get_mut("agents"), latest_active_agents, deleted_agent_ids);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
let latest_teams = latest.get("teams").and_then(Value::as_object);
|
|
290
|
+
let Some(incoming_teams) = incoming.get_mut("teams").and_then(Value::as_object_mut) else {
|
|
291
|
+
return;
|
|
292
|
+
};
|
|
293
|
+
if let Some(latest_teams) = latest_teams {
|
|
294
|
+
for (team, latest_entry) in latest_teams {
|
|
295
|
+
let Some(incoming_entry) = incoming_teams.get_mut(team) else {
|
|
296
|
+
continue;
|
|
297
|
+
};
|
|
298
|
+
preserve_missing_agents(
|
|
299
|
+
incoming_entry.get_mut("agents"),
|
|
300
|
+
latest_entry.get("agents"),
|
|
301
|
+
deleted_agent_ids,
|
|
302
|
+
);
|
|
303
|
+
preserve_latest_ownership_fields(incoming_entry, latest_entry);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if let Some(active_team) = active_team.as_deref() {
|
|
307
|
+
let latest_top_agents = latest.get("agents");
|
|
308
|
+
if let Some(incoming_entry) = incoming_teams.get_mut(active_team) {
|
|
309
|
+
preserve_missing_agents(incoming_entry.get_mut("agents"), latest_top_agents, deleted_agent_ids);
|
|
310
|
+
preserve_latest_ownership_fields(incoming_entry, latest);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
fn preserve_latest_ownership_fields(incoming: &mut Value, latest: &Value) {
|
|
316
|
+
if !latest_has_preferable_ownership(incoming, latest) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
let Some(incoming_obj) = incoming.as_object_mut() else {
|
|
320
|
+
return;
|
|
321
|
+
};
|
|
322
|
+
for key in ["leader_receiver", "team_owner", "owner_epoch"] {
|
|
323
|
+
if let Some(value) = latest.get(key).filter(|value| json_truthy(value)) {
|
|
324
|
+
incoming_obj.insert(key.to_string(), value.clone());
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
fn latest_has_preferable_ownership(incoming: &Value, latest: &Value) -> bool {
|
|
330
|
+
let latest_epoch = ownership_epoch(latest);
|
|
331
|
+
let incoming_epoch = ownership_epoch(incoming);
|
|
332
|
+
if latest_epoch > incoming_epoch {
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
latest_epoch == incoming_epoch
|
|
336
|
+
&& !ownership_attached(incoming)
|
|
337
|
+
&& ownership_attached(latest)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
fn ownership_epoch(state: &Value) -> u64 {
|
|
341
|
+
state
|
|
342
|
+
.get("owner_epoch")
|
|
343
|
+
.and_then(Value::as_u64)
|
|
344
|
+
.or_else(|| {
|
|
345
|
+
state
|
|
346
|
+
.get("team_owner")
|
|
347
|
+
.and_then(|owner| owner.get("owner_epoch"))
|
|
348
|
+
.and_then(Value::as_u64)
|
|
349
|
+
})
|
|
350
|
+
.or_else(|| {
|
|
351
|
+
state
|
|
352
|
+
.get("leader_receiver")
|
|
353
|
+
.and_then(|receiver| receiver.get("owner_epoch"))
|
|
354
|
+
.and_then(Value::as_u64)
|
|
355
|
+
})
|
|
356
|
+
.unwrap_or(0)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
fn ownership_attached(state: &Value) -> bool {
|
|
360
|
+
["leader_receiver", "team_owner"].into_iter().any(|key| {
|
|
361
|
+
state
|
|
362
|
+
.get(key)
|
|
363
|
+
.and_then(|value| value.get("pane_id"))
|
|
364
|
+
.and_then(Value::as_str)
|
|
365
|
+
.is_some_and(|pane| !pane.is_empty() && pane != "__team_agent_unbound__")
|
|
366
|
+
})
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
fn preserve_missing_agents(
|
|
370
|
+
incoming_agents: Option<&mut Value>,
|
|
371
|
+
latest_agents: Option<&Value>,
|
|
372
|
+
deleted_agent_ids: &BTreeSet<String>,
|
|
373
|
+
) {
|
|
374
|
+
let Some(incoming_agents) = incoming_agents else {
|
|
375
|
+
return;
|
|
376
|
+
};
|
|
377
|
+
let Some(incoming_map) = incoming_agents.as_object_mut() else {
|
|
378
|
+
return;
|
|
379
|
+
};
|
|
380
|
+
let Some(latest_map) = latest_agents.and_then(Value::as_object) else {
|
|
381
|
+
return;
|
|
382
|
+
};
|
|
383
|
+
for (agent_id, latest_agent) in latest_map {
|
|
384
|
+
if deleted_agent_ids.contains(agent_id) {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
incoming_map
|
|
388
|
+
.entry(agent_id.clone())
|
|
389
|
+
.or_insert_with(|| latest_agent.clone());
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
fn same_runtime_projection(left: &Value, right: &Value) -> bool {
|
|
394
|
+
let left_session = left.get("session_name").and_then(Value::as_str);
|
|
395
|
+
let right_session = right.get("session_name").and_then(Value::as_str);
|
|
396
|
+
if left_session.is_some() && right_session.is_some() && left_session != right_session {
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
let left_team = active_team_key(left);
|
|
400
|
+
let right_team = active_team_key(right);
|
|
401
|
+
if left_team.is_some() && right_team.is_some() && left_team != right_team {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
true
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
fn active_team_key(state: &Value) -> Option<String> {
|
|
408
|
+
state
|
|
409
|
+
.get("active_team_key")
|
|
410
|
+
.and_then(Value::as_str)
|
|
411
|
+
.filter(|team| !team.is_empty() && *team != "current")
|
|
412
|
+
.map(str::to_string)
|
|
413
|
+
}
|
|
414
|
+
|
|
246
415
|
/// `_self_heal_runtime_state`:重建 inode(heal-tmp + backup-rename),绝不 in-place truncate。
|
|
247
416
|
fn self_heal(
|
|
248
417
|
workspace: &Path,
|
|
@@ -14,11 +14,18 @@ use std::path::Path;
|
|
|
14
14
|
use serde_json::{json, Map, Value};
|
|
15
15
|
|
|
16
16
|
use super::StateError;
|
|
17
|
-
use crate::state::persist::{load_runtime_state,
|
|
17
|
+
use crate::state::persist::{load_runtime_state, save_runtime_state_with_deleted_agents};
|
|
18
18
|
|
|
19
19
|
/// `team_state_key`(`state.py:93`):从 team_dir(.name)/spec_path(.parent.name)派生 team key,
|
|
20
20
|
/// 跳过 `.team`/`runtime`;兜底 `session_name` 或 `"current"`。
|
|
21
21
|
pub fn team_state_key(state: &Value) -> String {
|
|
22
|
+
if let Some(team_key) = state
|
|
23
|
+
.get("team_key")
|
|
24
|
+
.and_then(Value::as_str)
|
|
25
|
+
.filter(|key| !key.is_empty())
|
|
26
|
+
{
|
|
27
|
+
return team_key.to_string();
|
|
28
|
+
}
|
|
22
29
|
for field in ["team_dir", "spec_path"] {
|
|
23
30
|
// Python `if not value: continue` —— None/空串 falsy 跳过。
|
|
24
31
|
let value = match state.get(field).and_then(Value::as_str) {
|
|
@@ -43,6 +50,105 @@ pub fn team_state_key(state: &Value) -> String {
|
|
|
43
50
|
.map_or_else(|| "current".to_string(), str::to_string)
|
|
44
51
|
}
|
|
45
52
|
|
|
53
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
54
|
+
pub enum OwnerTeamResolution {
|
|
55
|
+
Canonical(String),
|
|
56
|
+
LegacyAlias { requested: String, canonical: String },
|
|
57
|
+
Unresolved { requested: String },
|
|
58
|
+
Ambiguous { requested: String, matches: Vec<String> },
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
impl OwnerTeamResolution {
|
|
62
|
+
pub fn canonical_key(&self) -> Option<&str> {
|
|
63
|
+
match self {
|
|
64
|
+
OwnerTeamResolution::Canonical(key)
|
|
65
|
+
| OwnerTeamResolution::LegacyAlias { canonical: key, .. } => Some(key),
|
|
66
|
+
OwnerTeamResolution::Unresolved { .. } | OwnerTeamResolution::Ambiguous { .. } => None,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
pub fn resolve_owner_team_id(state: &Value, owner_team_id: &str) -> OwnerTeamResolution {
|
|
72
|
+
let requested = owner_team_id.trim();
|
|
73
|
+
if requested.is_empty() {
|
|
74
|
+
return OwnerTeamResolution::Unresolved { requested: owner_team_id.to_string() };
|
|
75
|
+
}
|
|
76
|
+
let teams = state.get("teams").and_then(Value::as_object);
|
|
77
|
+
if teams.is_some_and(|teams| teams.contains_key(requested)) {
|
|
78
|
+
return OwnerTeamResolution::Canonical(requested.to_string());
|
|
79
|
+
}
|
|
80
|
+
if teams.is_none_or(Map::is_empty) {
|
|
81
|
+
let active = state.get("active_team_key").and_then(Value::as_str).unwrap_or("");
|
|
82
|
+
let derived = team_state_key(state);
|
|
83
|
+
if active == requested || derived == requested {
|
|
84
|
+
return OwnerTeamResolution::Canonical(requested.to_string());
|
|
85
|
+
}
|
|
86
|
+
if !active.is_empty() {
|
|
87
|
+
return OwnerTeamResolution::LegacyAlias {
|
|
88
|
+
requested: requested.to_string(),
|
|
89
|
+
canonical: active.to_string(),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if derived != "current" {
|
|
93
|
+
return OwnerTeamResolution::LegacyAlias {
|
|
94
|
+
requested: requested.to_string(),
|
|
95
|
+
canonical: derived,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return OwnerTeamResolution::Canonical(requested.to_string());
|
|
99
|
+
}
|
|
100
|
+
let Some(teams) = teams else {
|
|
101
|
+
return OwnerTeamResolution::Unresolved { requested: requested.to_string() };
|
|
102
|
+
};
|
|
103
|
+
let mut matches = Vec::new();
|
|
104
|
+
for (key, entry) in teams {
|
|
105
|
+
if legacy_owner_team_aliases(entry).any(|alias| alias == requested) {
|
|
106
|
+
matches.push(key.clone());
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
matches.sort();
|
|
110
|
+
matches.dedup();
|
|
111
|
+
match matches.len() {
|
|
112
|
+
0 => OwnerTeamResolution::Unresolved { requested: requested.to_string() },
|
|
113
|
+
1 => OwnerTeamResolution::LegacyAlias {
|
|
114
|
+
requested: requested.to_string(),
|
|
115
|
+
canonical: matches.remove(0),
|
|
116
|
+
},
|
|
117
|
+
_ => OwnerTeamResolution::Ambiguous { requested: requested.to_string(), matches },
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
fn legacy_owner_team_aliases(entry: &Value) -> impl Iterator<Item = String> + '_ {
|
|
122
|
+
let scalar_paths = [
|
|
123
|
+
"/team/name",
|
|
124
|
+
"/team/id",
|
|
125
|
+
"/name",
|
|
126
|
+
"/team_name",
|
|
127
|
+
"/team_id",
|
|
128
|
+
"/spec_name",
|
|
129
|
+
"/legacy_owner_team_id",
|
|
130
|
+
"/legacy_team_id",
|
|
131
|
+
"/legacy_team_name",
|
|
132
|
+
"/legacy_alias",
|
|
133
|
+
];
|
|
134
|
+
let list_paths = ["/legacy_aliases", "/legacy_team_aliases", "/legacy_owner_team_ids", "/aliases"];
|
|
135
|
+
let scalars = scalar_paths
|
|
136
|
+
.into_iter()
|
|
137
|
+
.filter_map(|path| entry.pointer(path).and_then(Value::as_str));
|
|
138
|
+
let lists = list_paths.into_iter().flat_map(|path| {
|
|
139
|
+
entry
|
|
140
|
+
.pointer(path)
|
|
141
|
+
.and_then(Value::as_array)
|
|
142
|
+
.into_iter()
|
|
143
|
+
.flatten()
|
|
144
|
+
.filter_map(Value::as_str)
|
|
145
|
+
});
|
|
146
|
+
scalars
|
|
147
|
+
.chain(lists)
|
|
148
|
+
.filter(|alias| !alias.is_empty())
|
|
149
|
+
.map(str::to_string)
|
|
150
|
+
}
|
|
151
|
+
|
|
46
152
|
/// `compact_team_state`(`state.py:105`):剔除 `teams`(team entry 不嵌套全量 teams),保序。
|
|
47
153
|
pub fn compact_team_state(state: &Value) -> Value {
|
|
48
154
|
match state.as_object() {
|
|
@@ -241,11 +347,7 @@ pub fn select_runtime_state(workspace: &Path, team: Option<&str>) -> Result<Valu
|
|
|
241
347
|
}
|
|
242
348
|
let matches: Vec<&String> = alive
|
|
243
349
|
.iter()
|
|
244
|
-
.filter(|(key, value)|
|
|
245
|
-
let session = value.get("session_name").and_then(Value::as_str).unwrap_or("");
|
|
246
|
-
let dir = value.get("team_dir").and_then(Value::as_str).unwrap_or("");
|
|
247
|
-
team == key.as_str() || team == session || team == dir
|
|
248
|
-
})
|
|
350
|
+
.filter(|(key, value)| team_selector_matches(team, key, value))
|
|
249
351
|
.map(|(k, _)| k)
|
|
250
352
|
.collect();
|
|
251
353
|
if matches.len() == 1 {
|
|
@@ -282,6 +384,29 @@ pub fn select_runtime_state(workspace: &Path, team: Option<&str>) -> Result<Valu
|
|
|
282
384
|
))
|
|
283
385
|
}
|
|
284
386
|
|
|
387
|
+
fn team_selector_matches(team: &str, key: &str, value: &Value) -> bool {
|
|
388
|
+
if team == key {
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
let session = value.get("session_name").and_then(Value::as_str).unwrap_or("");
|
|
392
|
+
if team == session {
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
if let Some(stripped) = session.strip_prefix("team-") {
|
|
396
|
+
if team == stripped {
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
let dir = value.get("team_dir").and_then(Value::as_str).unwrap_or("");
|
|
401
|
+
if team == dir {
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
std::path::Path::new(dir)
|
|
405
|
+
.file_name()
|
|
406
|
+
.and_then(|name| name.to_str())
|
|
407
|
+
.is_some_and(|name| team == name)
|
|
408
|
+
}
|
|
409
|
+
|
|
285
410
|
/// `ambiguous_team_target_result`(`state.py:226`):无显式 team 且多候选 → 拒绝 dict;否则 None。
|
|
286
411
|
pub fn ambiguous_team_target_result(state: &Value) -> Option<Value> {
|
|
287
412
|
let alive = team_state_candidates(state);
|
|
@@ -339,6 +464,14 @@ pub fn resolve_team_scoped_state(
|
|
|
339
464
|
/// 纯 `save_runtime_state`(字节等价);多 team 时把本 team 落到 `teams[target_key]=compact(...)`,顶层
|
|
340
465
|
/// 视图按 golden 的 `existing_primary_key` 逻辑择 incoming/existing。§10:无 unwrap/panic。
|
|
341
466
|
pub fn save_team_scoped_state(workspace: &Path, team_state: &Value) -> Result<(), StateError> {
|
|
467
|
+
save_team_scoped_state_with_deleted_agents(workspace, team_state, &[])
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
pub(crate) fn save_team_scoped_state_with_deleted_agents(
|
|
471
|
+
workspace: &Path,
|
|
472
|
+
team_state: &Value,
|
|
473
|
+
deleted_agent_ids: &[&str],
|
|
474
|
+
) -> Result<(), StateError> {
|
|
342
475
|
let target_key = team_state_key(team_state);
|
|
343
476
|
let existing = load_runtime_state(workspace)?;
|
|
344
477
|
// existing_primary_key = team_state_key(existing) if existing.get("session_name") else None
|
|
@@ -367,7 +500,7 @@ pub fn save_team_scoped_state(workspace: &Path, team_state: &Value) -> Result<()
|
|
|
367
500
|
// not existing_teams and existing_primary_key == target_key → 纯 save(剔 teams)。
|
|
368
501
|
if existing_teams.is_empty() && existing_primary_key.as_deref() == Some(target_key.as_str()) {
|
|
369
502
|
let merged = compact_team_state(team_state);
|
|
370
|
-
return
|
|
503
|
+
return save_runtime_state_with_deleted_agents(workspace, &merged, deleted_agent_ids);
|
|
371
504
|
}
|
|
372
505
|
// teams = deepcopy(incoming_teams or existing_teams)
|
|
373
506
|
let mut teams = match incoming_teams {
|
|
@@ -387,7 +520,7 @@ pub fn save_team_scoped_state(workspace: &Path, team_state: &Value) -> Result<()
|
|
|
387
520
|
if merged.get("teams").and_then(Value::as_object).is_some_and(Map::is_empty) {
|
|
388
521
|
merged.remove("teams");
|
|
389
522
|
}
|
|
390
|
-
|
|
523
|
+
save_runtime_state_with_deleted_agents(workspace, &Value::Object(merged), deleted_agent_ids)
|
|
391
524
|
}
|
|
392
525
|
|
|
393
526
|
// ---- helpers ----
|
|
@@ -98,8 +98,11 @@ fn spec_workspace_from_state(state: &Value) -> Option<PathBuf> {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
fn selected_team_key(state: &Value, team: Option<&str>) -> String {
|
|
101
|
-
|
|
101
|
+
state
|
|
102
|
+
.get("active_team_key")
|
|
103
|
+
.and_then(Value::as_str)
|
|
104
|
+
.filter(|s| !s.is_empty())
|
|
102
105
|
.map(ToString::to_string)
|
|
103
|
-
.or_else(||
|
|
106
|
+
.or_else(|| team.filter(|s| !s.is_empty()).map(ToString::to_string))
|
|
104
107
|
.unwrap_or_else(|| team_state_key(state))
|
|
105
108
|
}
|