@team-agent/installer 0.5.3 → 0.5.4
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/cli/diagnose.rs +191 -31
- package/crates/team-agent/src/cli/mod.rs +93 -34
- package/crates/team-agent/src/coordinator/tick.rs +13 -0
- package/crates/team-agent/src/lifecycle/launch.rs +14 -6
- package/crates/team-agent/src/lifecycle/restart/agent.rs +15 -1
- package/crates/team-agent/src/lifecycle/restart/common.rs +53 -2
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +108 -6
- package/crates/team-agent/src/lifecycle/restart/selection.rs +47 -18
- package/crates/team-agent/src/lifecycle/restart.rs +16 -6
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +287 -53
- package/crates/team-agent/src/lifecycle/tests/restart.rs +144 -5
- package/crates/team-agent/src/lifecycle/types.rs +1 -0
- package/crates/team-agent/src/provider/adapter.rs +103 -41
- package/crates/team-agent/src/provider/session/capture.rs +328 -66
- package/crates/team-agent/src/provider/session/resume.rs +30 -28
- package/crates/team-agent/src/provider/session_scan/common.rs +117 -1
- package/crates/team-agent/src/provider/session_scan/copilot.rs +1 -0
- package/crates/team-agent/src/provider/session_scan.rs +1 -0
- package/crates/team-agent/src/state/persist.rs +17 -0
- package/crates/team-agent/src/state/projection.rs +59 -4
- package/package.json +4 -4
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
//! diagnose/preflight/wait-ready CLI helpers.
|
|
2
2
|
use super::*;
|
|
3
|
-
use crate::provider::wire::{command_name, provider_wire};
|
|
3
|
+
use crate::provider::wire::{command_name, parse_provider, provider_wire};
|
|
4
4
|
use crate::transport::Transport;
|
|
5
5
|
|
|
6
6
|
pub(crate) fn diagnose_runtime(state: &Value, backend: &dyn Transport) -> (Value, Value) {
|
|
7
7
|
let mut issues = Vec::new();
|
|
8
8
|
let mut repairs = Vec::new();
|
|
9
9
|
|
|
10
|
-
if let Some(session_name) = state
|
|
11
|
-
|
|
10
|
+
if let Some(session_name) = state
|
|
11
|
+
.get("session_name")
|
|
12
|
+
.and_then(Value::as_str)
|
|
13
|
+
.filter(|s| !s.is_empty())
|
|
14
|
+
{
|
|
15
|
+
match backend.has_session(&crate::transport::SessionName::new(
|
|
16
|
+
session_name.to_string(),
|
|
17
|
+
)) {
|
|
12
18
|
Ok(true) => {}
|
|
13
19
|
Ok(false) => {
|
|
14
20
|
issues.push(json!("tmux_session_missing"));
|
|
@@ -69,13 +75,55 @@ pub(crate) fn diagnose_runtime(state: &Value, backend: &dyn Transport) -> (Value
|
|
|
69
75
|
}
|
|
70
76
|
}
|
|
71
77
|
|
|
72
|
-
if let Some(session_name) = state
|
|
73
|
-
|
|
78
|
+
if let Some(session_name) = state
|
|
79
|
+
.get("session_name")
|
|
80
|
+
.and_then(Value::as_str)
|
|
81
|
+
.filter(|s| !s.is_empty())
|
|
82
|
+
{
|
|
83
|
+
if let Ok(windows) = backend.list_windows(&crate::transport::SessionName::new(
|
|
84
|
+
session_name.to_string(),
|
|
85
|
+
)) {
|
|
74
86
|
if let Some(agents) = state.get("agents").and_then(Value::as_object) {
|
|
75
87
|
for (agent_id, agent_state) in agents {
|
|
88
|
+
let provider = agent_state
|
|
89
|
+
.get("provider")
|
|
90
|
+
.and_then(Value::as_str)
|
|
91
|
+
.and_then(parse_provider)
|
|
92
|
+
.unwrap_or(crate::provider::Provider::Codex);
|
|
93
|
+
let rollout_path = agent_state
|
|
94
|
+
.get("rollout_path")
|
|
95
|
+
.and_then(Value::as_str)
|
|
96
|
+
.filter(|path| !path.is_empty())
|
|
97
|
+
.map(crate::provider::RolloutPath::new);
|
|
98
|
+
let identity_probe =
|
|
99
|
+
crate::lifecycle::restart::session_identity_probe_for_agent(
|
|
100
|
+
&crate::model::ids::AgentId::new(agent_id.clone()),
|
|
101
|
+
provider,
|
|
102
|
+
rollout_path.as_ref(),
|
|
103
|
+
);
|
|
104
|
+
if identity_probe.identity_ok == Some(false) {
|
|
105
|
+
let issue = format!("session_identity_mismatch:{agent_id}");
|
|
106
|
+
issues.push(json!(issue));
|
|
107
|
+
repairs.push(json!({
|
|
108
|
+
"issue": format!("session_identity_mismatch:{agent_id}"),
|
|
109
|
+
"action": format!(
|
|
110
|
+
"run `team-agent restart --allow-fresh` to discard the poisoned session tuple for `{agent_id}`"
|
|
111
|
+
),
|
|
112
|
+
"expected_agent_id": agent_id,
|
|
113
|
+
"embedded_agent_id": identity_probe.embedded_agent_id,
|
|
114
|
+
"rollout_path": identity_probe
|
|
115
|
+
.rollout_path
|
|
116
|
+
.map(|path| path.to_string_lossy().to_string()),
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
76
119
|
let window = ["window", "window_name"]
|
|
77
120
|
.iter()
|
|
78
|
-
.find_map(|key|
|
|
121
|
+
.find_map(|key| {
|
|
122
|
+
agent_state
|
|
123
|
+
.get(*key)
|
|
124
|
+
.and_then(Value::as_str)
|
|
125
|
+
.filter(|s| !s.is_empty())
|
|
126
|
+
})
|
|
79
127
|
.unwrap_or(agent_id);
|
|
80
128
|
if !windows.iter().any(|w| w.as_str() == window) {
|
|
81
129
|
issues.push(json!(format!("worker_window_missing:{agent_id}")));
|
|
@@ -131,6 +179,80 @@ fn leader_receiver_attached(state: &Value) -> bool {
|
|
|
131
179
|
mode_direct && status_attached && pane_present
|
|
132
180
|
}
|
|
133
181
|
|
|
182
|
+
#[cfg(test)]
|
|
183
|
+
mod tests {
|
|
184
|
+
use super::*;
|
|
185
|
+
|
|
186
|
+
fn write_codex_identity_rollout(
|
|
187
|
+
workspace: &std::path::Path,
|
|
188
|
+
session_id: &str,
|
|
189
|
+
embedded_agent_id: &str,
|
|
190
|
+
) -> std::path::PathBuf {
|
|
191
|
+
let path = workspace.join("rollout-poison.jsonl");
|
|
192
|
+
let text = format!(
|
|
193
|
+
"{{\"session_meta\":{{\"payload\":{{\"id\":\"{session_id}\",\"cwd\":\"{}\"}}}}}}\n\
|
|
194
|
+
{{\"type\":\"turn_context\",\"payload\":{{}}}}\n\
|
|
195
|
+
{{\"type\":\"response_item\",\"payload\":{{\"content\":[{{\"type\":\"input_text\",\"text\":\"You are Team Agent worker `{embedded_agent_id}` with role `fixture`.\"}}]}}}}\n",
|
|
196
|
+
workspace.to_string_lossy()
|
|
197
|
+
);
|
|
198
|
+
std::fs::write(&path, text).expect("write rollout");
|
|
199
|
+
path
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
#[test]
|
|
203
|
+
fn diagnose_surfaces_codex_session_identity_mismatch() {
|
|
204
|
+
let workspace =
|
|
205
|
+
std::env::temp_dir().join(format!("ta-diagnose-crossbind-{}", std::process::id()));
|
|
206
|
+
let _ = std::fs::remove_dir_all(&workspace);
|
|
207
|
+
std::fs::create_dir_all(&workspace).unwrap();
|
|
208
|
+
let rollout = write_codex_identity_rollout(
|
|
209
|
+
&workspace,
|
|
210
|
+
"019f3327-c35a-7023-b3cd-1bea93a7a157",
|
|
211
|
+
"ios-dev",
|
|
212
|
+
);
|
|
213
|
+
let state = json!({
|
|
214
|
+
"session_name": "team-fixture",
|
|
215
|
+
"leader_receiver": {
|
|
216
|
+
"mode": "direct_tmux",
|
|
217
|
+
"status": "attached",
|
|
218
|
+
"pane_id": "%leader",
|
|
219
|
+
"provider": "codex"
|
|
220
|
+
},
|
|
221
|
+
"agents": {
|
|
222
|
+
"frontend": {
|
|
223
|
+
"provider": "codex",
|
|
224
|
+
"status": "running",
|
|
225
|
+
"session_id": "019f3327-c35a-7023-b3cd-1bea93a7a157",
|
|
226
|
+
"rollout_path": rollout.to_string_lossy(),
|
|
227
|
+
"captured_at": "2026-07-05T17:04:04Z",
|
|
228
|
+
"captured_via": "fs_watch",
|
|
229
|
+
"spawn_cwd": workspace.to_string_lossy(),
|
|
230
|
+
"window": "frontend"
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
let backend = crate::transport::test_support::OfflineTransport::new()
|
|
235
|
+
.with_session_present(true)
|
|
236
|
+
.with_windows(vec![crate::transport::WindowName::new("frontend")]);
|
|
237
|
+
let (issues, repairs) = diagnose_runtime(&state, &backend);
|
|
238
|
+
assert!(
|
|
239
|
+
issues
|
|
240
|
+
.as_array()
|
|
241
|
+
.is_some_and(|items| items.iter().any(|item| item.as_str()
|
|
242
|
+
== Some("session_identity_mismatch:frontend"))),
|
|
243
|
+
"diagnose must surface session_identity_mismatch for poisoned Codex tuples; issues={issues}"
|
|
244
|
+
);
|
|
245
|
+
assert!(
|
|
246
|
+
repairs.as_array().is_some_and(|items| items.iter().any(|item| {
|
|
247
|
+
item.get("issue").and_then(Value::as_str)
|
|
248
|
+
== Some("session_identity_mismatch:frontend")
|
|
249
|
+
})),
|
|
250
|
+
"diagnose should include an explicit repair hint for the mismatched tuple; repairs={repairs}"
|
|
251
|
+
);
|
|
252
|
+
let _ = std::fs::remove_dir_all(&workspace);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
134
256
|
pub(crate) fn build_preflight_report(team: &std::path::Path) -> Result<Value, CliError> {
|
|
135
257
|
let mut checks = Vec::new();
|
|
136
258
|
let mut next_actions = Vec::new();
|
|
@@ -150,7 +272,9 @@ pub(crate) fn build_preflight_report(team: &std::path::Path) -> Result<Value, Cl
|
|
|
150
272
|
"ok": false,
|
|
151
273
|
"error": error.to_string(),
|
|
152
274
|
}));
|
|
153
|
-
next_actions.push(json!(
|
|
275
|
+
next_actions.push(json!(
|
|
276
|
+
"fix TEAM.md and role front matter, then run preflight again"
|
|
277
|
+
));
|
|
154
278
|
None
|
|
155
279
|
}
|
|
156
280
|
};
|
|
@@ -183,7 +307,11 @@ pub(crate) fn build_preflight_report(team: &std::path::Path) -> Result<Value, Cl
|
|
|
183
307
|
|
|
184
308
|
let workspace = crate::model::paths::team_workspace(team)
|
|
185
309
|
.map_err(|error| CliError::Runtime(error.to_string()))?;
|
|
186
|
-
let profile_dir_exists = workspace
|
|
310
|
+
let profile_dir_exists = workspace
|
|
311
|
+
.join(".team")
|
|
312
|
+
.join("current")
|
|
313
|
+
.join("profiles")
|
|
314
|
+
.exists()
|
|
187
315
|
|| team.join("profiles").exists();
|
|
188
316
|
let profile_dir_check = json!({
|
|
189
317
|
"name": "profile_dir",
|
|
@@ -229,7 +357,10 @@ pub(crate) fn build_preflight_report(team: &std::path::Path) -> Result<Value, Cl
|
|
|
229
357
|
let summary = if ok {
|
|
230
358
|
"preflight passed".to_string()
|
|
231
359
|
} else {
|
|
232
|
-
format!(
|
|
360
|
+
format!(
|
|
361
|
+
"preflight found blockers: {}",
|
|
362
|
+
blocker_names(&blockers).join(", ")
|
|
363
|
+
)
|
|
233
364
|
};
|
|
234
365
|
let checks_value = Value::Array(checks);
|
|
235
366
|
let details_log = write_details_log(
|
|
@@ -255,7 +386,9 @@ pub(crate) fn build_preflight_report(team: &std::path::Path) -> Result<Value, Cl
|
|
|
255
386
|
Ok(report)
|
|
256
387
|
}
|
|
257
388
|
|
|
258
|
-
pub(crate) fn build_profile_smoke_check_for_team(
|
|
389
|
+
pub(crate) fn build_profile_smoke_check_for_team(
|
|
390
|
+
team: &std::path::Path,
|
|
391
|
+
) -> Result<Value, CliError> {
|
|
259
392
|
let workspace = crate::model::paths::team_workspace(team)
|
|
260
393
|
.map_err(|error| CliError::Runtime(error.to_string()))?;
|
|
261
394
|
let spec = match crate::compiler::compile_team(team) {
|
|
@@ -376,10 +509,14 @@ pub(crate) fn build_wait_ready_report(
|
|
|
376
509
|
}));
|
|
377
510
|
}
|
|
378
511
|
};
|
|
379
|
-
let timeout = if timeout.is_finite() && timeout > 0.0 {
|
|
512
|
+
let timeout = if timeout.is_finite() && timeout > 0.0 {
|
|
513
|
+
timeout
|
|
514
|
+
} else {
|
|
515
|
+
0.0
|
|
516
|
+
};
|
|
380
517
|
let deadline = std::time::Instant::now() + std::time::Duration::from_secs_f64(timeout);
|
|
381
518
|
let mut readiness;
|
|
382
|
-
let mut state_read_error: Option<String
|
|
519
|
+
let mut state_read_error: Option<String>;
|
|
383
520
|
loop {
|
|
384
521
|
let mut state = match crate::state::projection::select_runtime_state(
|
|
385
522
|
&selected.run_workspace,
|
|
@@ -419,7 +556,9 @@ pub(crate) fn build_wait_ready_report(
|
|
|
419
556
|
"error",
|
|
420
557
|
"state_read_error",
|
|
421
558
|
"runtime state could not be read",
|
|
422
|
-
vec![json!(
|
|
559
|
+
vec![json!(
|
|
560
|
+
"inspect .team/runtime/state.json (corrupt or unreadable) and retry"
|
|
561
|
+
)],
|
|
423
562
|
)
|
|
424
563
|
} else if awaiting_trust {
|
|
425
564
|
(
|
|
@@ -427,11 +566,17 @@ pub(crate) fn build_wait_ready_report(
|
|
|
427
566
|
"pending",
|
|
428
567
|
"awaiting_trust_prompt",
|
|
429
568
|
"workers are awaiting trust prompt",
|
|
430
|
-
vec![json!(
|
|
569
|
+
vec![json!(
|
|
570
|
+
"answer the provider trust prompt, then run wait-ready again"
|
|
571
|
+
)],
|
|
431
572
|
)
|
|
432
573
|
} else if ready {
|
|
433
574
|
(true, "ready", "ready", "workers ready", Vec::new())
|
|
434
|
-
} else if readiness
|
|
575
|
+
} else if readiness
|
|
576
|
+
.get("session_capture_complete")
|
|
577
|
+
.and_then(Value::as_bool)
|
|
578
|
+
== Some(false)
|
|
579
|
+
{
|
|
435
580
|
(
|
|
436
581
|
false,
|
|
437
582
|
"pending",
|
|
@@ -445,7 +590,9 @@ pub(crate) fn build_wait_ready_report(
|
|
|
445
590
|
"timeout",
|
|
446
591
|
"workers_not_ready",
|
|
447
592
|
"workers not ready before timeout",
|
|
448
|
-
vec![json!(
|
|
593
|
+
vec![json!(
|
|
594
|
+
"inspect team-agent diagnose output and worker terminals"
|
|
595
|
+
)],
|
|
449
596
|
)
|
|
450
597
|
};
|
|
451
598
|
let details_log = write_details_log(
|
|
@@ -479,7 +626,11 @@ fn inject_tmux_session_present(workspace: &std::path::Path, state: &mut Value) {
|
|
|
479
626
|
// runtime actually uses (state.tmux_endpoint / tmux_socket), not the
|
|
480
627
|
// workspace-hash socket. Otherwise readiness reports `process_started=false`
|
|
481
628
|
// even though the session is alive on the persisted socket.
|
|
482
|
-
let Some(session_name) = state
|
|
629
|
+
let Some(session_name) = state
|
|
630
|
+
.get("session_name")
|
|
631
|
+
.and_then(Value::as_str)
|
|
632
|
+
.filter(|s| !s.is_empty())
|
|
633
|
+
else {
|
|
483
634
|
return;
|
|
484
635
|
};
|
|
485
636
|
let session_name_owned = session_name.to_string();
|
|
@@ -519,14 +670,11 @@ pub(crate) fn wait_readiness(state: &Value) -> Value {
|
|
|
519
670
|
.get("leader_receiver")
|
|
520
671
|
.and_then(Value::as_object)
|
|
521
672
|
.is_some_and(|receiver| {
|
|
522
|
-
receiver
|
|
523
|
-
.get("status")
|
|
524
|
-
.and_then(Value::as_str)
|
|
525
|
-
== Some("attached")
|
|
673
|
+
receiver.get("status").and_then(Value::as_str) == Some("attached")
|
|
526
674
|
|| receiver
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
675
|
+
.get("pane_id")
|
|
676
|
+
.and_then(Value::as_str)
|
|
677
|
+
.is_some_and(|pane| !pane.is_empty() && pane != "__team_agent_unbound__")
|
|
530
678
|
});
|
|
531
679
|
|
|
532
680
|
if let Some(agents) = agents {
|
|
@@ -544,8 +692,7 @@ pub(crate) fn wait_readiness(state: &Value) -> Value {
|
|
|
544
692
|
Some("running" | "busy" | "ready")
|
|
545
693
|
)
|
|
546
694
|
});
|
|
547
|
-
mcp_ready = !agents.is_empty()
|
|
548
|
-
&& agents.values().all(agent_mcp_ready);
|
|
695
|
+
mcp_ready = !agents.is_empty() && agents.values().all(agent_mcp_ready);
|
|
549
696
|
task_prompt_delivered = !agents.is_empty()
|
|
550
697
|
&& (message_counts_positive(state.get("messages"))
|
|
551
698
|
|| agents.values().all(|agent| {
|
|
@@ -560,7 +707,8 @@ pub(crate) fn wait_readiness(state: &Value) -> Value {
|
|
|
560
707
|
.and_then(Value::as_str)
|
|
561
708
|
== Some("awaiting_trust_prompt")
|
|
562
709
|
});
|
|
563
|
-
incomplete_sessions =
|
|
710
|
+
incomplete_sessions =
|
|
711
|
+
crate::session_capture::incomplete_interacted_resumable_agent_ids(state);
|
|
564
712
|
}
|
|
565
713
|
let all_resumable_have_session = incomplete_sessions.is_empty();
|
|
566
714
|
let session_capture_incomplete = !all_resumable_have_session;
|
|
@@ -591,7 +739,9 @@ fn inject_message_counts(workspace: &std::path::Path, state: &mut Value) -> Resu
|
|
|
591
739
|
let mut stmt = conn
|
|
592
740
|
.prepare("select status, count(*) from messages group by status order by status")
|
|
593
741
|
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
594
|
-
let mut rows = stmt
|
|
742
|
+
let mut rows = stmt
|
|
743
|
+
.query([])
|
|
744
|
+
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
595
745
|
let mut messages = Map::new();
|
|
596
746
|
while let Some(row) = rows.next().map_err(|e| CliError::Runtime(e.to_string()))? {
|
|
597
747
|
let status: String = row.get(0).map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
@@ -619,7 +769,10 @@ fn message_counts_positive(value: Option<&Value>) -> bool {
|
|
|
619
769
|
fn legacy_process_started(agents: &serde_json::Map<String, Value>) -> bool {
|
|
620
770
|
!agents.is_empty()
|
|
621
771
|
&& agents.values().all(|agent| {
|
|
622
|
-
agent
|
|
772
|
+
agent
|
|
773
|
+
.get("pane_id")
|
|
774
|
+
.and_then(Value::as_str)
|
|
775
|
+
.is_some_and(|s| !s.is_empty())
|
|
623
776
|
|| agent.get("pid").and_then(Value::as_i64).is_some()
|
|
624
777
|
|| agent.get("process_started").and_then(Value::as_bool) == Some(true)
|
|
625
778
|
|| fake_agent_started(agent)
|
|
@@ -717,7 +870,10 @@ fn preflight_blockers(checks: &[Value]) -> Vec<Value> {
|
|
|
717
870
|
.iter()
|
|
718
871
|
.filter(|check| check.get("ok").and_then(Value::as_bool) == Some(false))
|
|
719
872
|
.map(|check| {
|
|
720
|
-
let name = check
|
|
873
|
+
let name = check
|
|
874
|
+
.get("name")
|
|
875
|
+
.and_then(Value::as_str)
|
|
876
|
+
.unwrap_or("unknown");
|
|
721
877
|
let reason = check
|
|
722
878
|
.get("error")
|
|
723
879
|
.or_else(|| check.get("reason"))
|
|
@@ -747,7 +903,11 @@ fn yaml_path_str<'a>(value: &'a crate::model::yaml::Value, keys: &[&str]) -> Opt
|
|
|
747
903
|
current.as_str()
|
|
748
904
|
}
|
|
749
905
|
|
|
750
|
-
fn write_details_log(
|
|
906
|
+
fn write_details_log(
|
|
907
|
+
workspace: &std::path::Path,
|
|
908
|
+
prefix: &str,
|
|
909
|
+
value: &Value,
|
|
910
|
+
) -> Result<std::path::PathBuf, CliError> {
|
|
751
911
|
let logs = workspace.join(".team").join("logs");
|
|
752
912
|
std::fs::create_dir_all(&logs)?;
|
|
753
913
|
let path = logs.join(format!("{prefix}-{}.json", timestamp_slug()));
|
|
@@ -210,14 +210,22 @@ pub mod lifecycle_port {
|
|
|
210
210
|
/// `&*box`. Factory refusal falls back to the workspace tmux
|
|
211
211
|
/// backend byte-equivalent to today so daemon liveness paths
|
|
212
212
|
/// don't crash.
|
|
213
|
-
pub fn shutdown(
|
|
213
|
+
pub fn shutdown(
|
|
214
|
+
workspace: &Path,
|
|
215
|
+
keep_logs: bool,
|
|
216
|
+
team: Option<&str>,
|
|
217
|
+
) -> Result<Value, CliError> {
|
|
214
218
|
let run_ws = crate::model::paths::canonical_run_workspace(workspace)
|
|
215
219
|
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
216
220
|
let state = shutdown_state_for_team(&run_ws, team)?;
|
|
217
221
|
if let Some(endpoint) = legacy_worker_tmux_endpoint(&state) {
|
|
218
222
|
let transport = shutdown_transport_for_endpoint(endpoint);
|
|
219
223
|
return shutdown_with_transport_and_state(
|
|
220
|
-
workspace,
|
|
224
|
+
workspace,
|
|
225
|
+
keep_logs,
|
|
226
|
+
team,
|
|
227
|
+
&transport,
|
|
228
|
+
Some(state),
|
|
221
229
|
);
|
|
222
230
|
}
|
|
223
231
|
let boxed: Box<dyn crate::transport::Transport> =
|
|
@@ -352,11 +360,7 @@ pub mod lifecycle_port {
|
|
|
352
360
|
let anchor_sessions = anchor_sessions_from_state(state, &pane_targets, event_log);
|
|
353
361
|
match sessions_to_kill(&sessions, &anchor_sessions) {
|
|
354
362
|
KillDecision::KillServerExclusive => {
|
|
355
|
-
if state
|
|
356
|
-
.get("tmux_socket_source")
|
|
357
|
-
.and_then(Value::as_str)
|
|
358
|
-
== Some("leader_env")
|
|
359
|
-
{
|
|
363
|
+
if state.get("tmux_socket_source").and_then(Value::as_str) == Some("leader_env") {
|
|
360
364
|
let _ = event_log.write(
|
|
361
365
|
"shutdown.kill_server_skipped_shared_socket",
|
|
362
366
|
json!({
|
|
@@ -463,10 +467,7 @@ pub mod lifecycle_port {
|
|
|
463
467
|
.cloned()
|
|
464
468
|
.collect::<Vec<_>>();
|
|
465
469
|
if let Some(anchored) = target_spared_for_anchor {
|
|
466
|
-
if !spared
|
|
467
|
-
.iter()
|
|
468
|
-
.any(|s| s.as_str() == anchored.as_str())
|
|
469
|
-
{
|
|
470
|
+
if !spared.iter().any(|s| s.as_str() == anchored.as_str()) {
|
|
470
471
|
spared.push(anchored);
|
|
471
472
|
}
|
|
472
473
|
}
|
|
@@ -486,7 +487,10 @@ pub mod lifecycle_port {
|
|
|
486
487
|
// (this function) computes its own `to_kill` from `target`. Any
|
|
487
488
|
// leader-prefixed entry here is a topology violation introduced
|
|
488
489
|
// somewhere upstream. Log loudly and skip the kill.
|
|
489
|
-
if session
|
|
490
|
+
if session
|
|
491
|
+
.as_str()
|
|
492
|
+
.starts_with(crate::leader::LEADER_SESSION_PREFIX)
|
|
493
|
+
{
|
|
490
494
|
eprintln!(
|
|
491
495
|
"team_agent::layout shutdown_invariant_violation kind=KillListContainsLeaderSession \
|
|
492
496
|
session=`{}` action=skipping_kill (post-Step-9 will hard-fail)",
|
|
@@ -723,11 +727,7 @@ pub mod lifecycle_port {
|
|
|
723
727
|
if crate::state::projection::state_is_managed_leader(&state)
|
|
724
728
|
&& session_has_leader_anchor
|
|
725
729
|
{
|
|
726
|
-
let _ = event_log_write_session_spared(
|
|
727
|
-
&run_workspace,
|
|
728
|
-
session,
|
|
729
|
-
&leader_anchor_ids,
|
|
730
|
-
);
|
|
730
|
+
let _ = event_log_write_session_spared(&run_workspace, session, &leader_anchor_ids);
|
|
731
731
|
push_unique_session(&mut spared_sessions, session.clone());
|
|
732
732
|
let worker_panes = collect_session_worker_panes(
|
|
733
733
|
&state,
|
|
@@ -742,10 +742,8 @@ pub mod lifecycle_port {
|
|
|
742
742
|
}
|
|
743
743
|
}
|
|
744
744
|
}
|
|
745
|
-
let _ =
|
|
746
|
-
&run_workspace,
|
|
747
|
-
session,
|
|
748
|
-
);
|
|
745
|
+
let _ =
|
|
746
|
+
crate::lifecycle::display::close_team_display_backends(&run_workspace, session);
|
|
749
747
|
} else {
|
|
750
748
|
push_unique_session(&mut killed_sessions, session.clone());
|
|
751
749
|
if let Err(error) = transport.kill_session(session) {
|
|
@@ -753,10 +751,8 @@ pub mod lifecycle_port {
|
|
|
753
751
|
kill_error = Some(error.to_string());
|
|
754
752
|
}
|
|
755
753
|
}
|
|
756
|
-
let _ =
|
|
757
|
-
&run_workspace,
|
|
758
|
-
session,
|
|
759
|
-
);
|
|
754
|
+
let _ =
|
|
755
|
+
crate::lifecycle::display::close_team_display_backends(&run_workspace, session);
|
|
760
756
|
}
|
|
761
757
|
}
|
|
762
758
|
deadline.check("reap_workspace_residuals")?;
|
|
@@ -953,7 +949,7 @@ pub mod lifecycle_port {
|
|
|
953
949
|
// byte-preserving. The Unix behavior is a no-op (no shim
|
|
954
950
|
// concept there); adding a placeholder key would just noise
|
|
955
951
|
// up the fixtures.
|
|
956
|
-
let
|
|
952
|
+
let response = json!({
|
|
957
953
|
"ok": ok,
|
|
958
954
|
"status": status,
|
|
959
955
|
"phase": phase,
|
|
@@ -979,6 +975,8 @@ pub mod lifecycle_port {
|
|
|
979
975
|
},
|
|
980
976
|
});
|
|
981
977
|
#[cfg(windows)]
|
|
978
|
+
let mut response = response;
|
|
979
|
+
#[cfg(windows)]
|
|
982
980
|
{
|
|
983
981
|
if let Some(obj) = response.as_object_mut() {
|
|
984
982
|
obj.insert(
|
|
@@ -1291,10 +1289,7 @@ pub mod lifecycle_port {
|
|
|
1291
1289
|
// probes when the primary transport is ConPTY; the primary
|
|
1292
1290
|
// transport check above already covered the honest question
|
|
1293
1291
|
// ("does the shim still have this session?").
|
|
1294
|
-
let primary_is_conpty = matches!(
|
|
1295
|
-
transport.kind(),
|
|
1296
|
-
crate::transport::BackendKind::ConPty
|
|
1297
|
-
);
|
|
1292
|
+
let primary_is_conpty = matches!(transport.kind(), crate::transport::BackendKind::ConPty);
|
|
1298
1293
|
if !primary_is_conpty {
|
|
1299
1294
|
let workspace_transport = shutdown_workspace_transport(workspace);
|
|
1300
1295
|
match crate::transport::Transport::has_session(&workspace_transport, session) {
|
|
@@ -2938,14 +2933,38 @@ pub mod lifecycle_port {
|
|
|
2938
2933
|
);
|
|
2939
2934
|
}
|
|
2940
2935
|
}
|
|
2936
|
+
if let crate::provider::session::ResumeRefusalReason::SessionIdentityMismatch {
|
|
2937
|
+
expected_agent_id,
|
|
2938
|
+
embedded_agent_id,
|
|
2939
|
+
session_id,
|
|
2940
|
+
rollout_path,
|
|
2941
|
+
} = reason
|
|
2942
|
+
{
|
|
2943
|
+
entry.insert(
|
|
2944
|
+
"expected_agent_id".to_string(),
|
|
2945
|
+
json!(expected_agent_id),
|
|
2946
|
+
);
|
|
2947
|
+
entry.insert(
|
|
2948
|
+
"embedded_agent_id".to_string(),
|
|
2949
|
+
json!(embedded_agent_id),
|
|
2950
|
+
);
|
|
2951
|
+
entry.insert(
|
|
2952
|
+
"poisoned_session_id".to_string(),
|
|
2953
|
+
json!(session_id),
|
|
2954
|
+
);
|
|
2955
|
+
if let Some(path) = rollout_path {
|
|
2956
|
+
entry.insert(
|
|
2957
|
+
"rollout_path".to_string(),
|
|
2958
|
+
json!(path.to_string_lossy()),
|
|
2959
|
+
);
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2941
2962
|
}
|
|
2942
2963
|
Value::Object(entry)
|
|
2943
2964
|
})
|
|
2944
2965
|
.collect();
|
|
2945
|
-
let unresumable_ids: Vec<&str> =
|
|
2946
|
-
.iter()
|
|
2947
|
-
.map(|w| w.agent_id.as_str())
|
|
2948
|
-
.collect();
|
|
2966
|
+
let unresumable_ids: Vec<&str> =
|
|
2967
|
+
unresumable.iter().map(|w| w.agent_id.as_str()).collect();
|
|
2949
2968
|
// Layer 2 self-healing (leader follow-up 2026-06-22): when
|
|
2950
2969
|
// EVERY unresumable worker shares the same structured
|
|
2951
2970
|
// refusal_reason wire string, refine the top-level `status`
|
|
@@ -3036,6 +3055,46 @@ pub mod lifecycle_port {
|
|
|
3036
3055
|
unresumable_ids.join(", "),
|
|
3037
3056
|
),
|
|
3038
3057
|
),
|
|
3058
|
+
"session_identity_mismatch" => {
|
|
3059
|
+
let mut lines = Vec::new();
|
|
3060
|
+
for w in unresumable.iter() {
|
|
3061
|
+
if let Some(
|
|
3062
|
+
crate::provider::session::ResumeRefusalReason::SessionIdentityMismatch {
|
|
3063
|
+
expected_agent_id,
|
|
3064
|
+
embedded_agent_id,
|
|
3065
|
+
session_id,
|
|
3066
|
+
rollout_path,
|
|
3067
|
+
},
|
|
3068
|
+
) = w.refusal_reason.as_ref()
|
|
3069
|
+
{
|
|
3070
|
+
let path = rollout_path
|
|
3071
|
+
.as_ref()
|
|
3072
|
+
.map(|p| p.to_string_lossy().into_owned())
|
|
3073
|
+
.unwrap_or_else(|| "<unknown>".to_string());
|
|
3074
|
+
lines.push(format!(
|
|
3075
|
+
" {}: session {} points to transcript identity {} at {} (expected {})",
|
|
3076
|
+
w.agent_id.as_str(),
|
|
3077
|
+
session_id,
|
|
3078
|
+
embedded_agent_id,
|
|
3079
|
+
path,
|
|
3080
|
+
expected_agent_id
|
|
3081
|
+
));
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
let mut msg = format!(
|
|
3085
|
+
"restart refused: provider session identity mismatch for {} worker(s) ({}). Pass --allow-fresh to discard the poisoned tuple and start fresh.",
|
|
3086
|
+
unresumable.len(),
|
|
3087
|
+
unresumable_ids.join(", "),
|
|
3088
|
+
);
|
|
3089
|
+
if !lines.is_empty() {
|
|
3090
|
+
msg.push_str("\nMismatched sessions:");
|
|
3091
|
+
for line in &lines {
|
|
3092
|
+
msg.push('\n');
|
|
3093
|
+
msg.push_str(line);
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
("refused_session_identity_mismatch".to_string(), msg)
|
|
3097
|
+
}
|
|
3039
3098
|
_ => ("refused_resume_atomicity".to_string(), error.clone()),
|
|
3040
3099
|
}
|
|
3041
3100
|
} else {
|
|
@@ -553,6 +553,19 @@ impl Coordinator {
|
|
|
553
553
|
}),
|
|
554
554
|
)?;
|
|
555
555
|
}
|
|
556
|
+
for mismatch in &report.identity_mismatches {
|
|
557
|
+
event_log.write(
|
|
558
|
+
"provider.session.identity_mismatch",
|
|
559
|
+
serde_json::json!({
|
|
560
|
+
"agent_id": mismatch.agent_id,
|
|
561
|
+
"expected_agent_id": mismatch.expected_agent_id,
|
|
562
|
+
"embedded_agent_id": mismatch.embedded_agent_id,
|
|
563
|
+
"session_id": mismatch.session_id,
|
|
564
|
+
"rollout_path": mismatch.rollout_path,
|
|
565
|
+
"spawn_cwd": mismatch.spawn_cwd,
|
|
566
|
+
}),
|
|
567
|
+
)?;
|
|
568
|
+
}
|
|
556
569
|
for ambiguous in report.ambiguous {
|
|
557
570
|
let candidate_count = report
|
|
558
571
|
.candidate_count_by_agent
|
|
@@ -374,6 +374,8 @@ fn spawn_agents(
|
|
|
374
374
|
);
|
|
375
375
|
}
|
|
376
376
|
}
|
|
377
|
+
let spawn_epoch = u64::try_from(started.len()).unwrap_or(u64::MAX);
|
|
378
|
+
let spawned_at = spawn_timestamp_for_agent(u32::try_from(spawn_epoch).unwrap_or(u32::MAX));
|
|
377
379
|
// E6 层1 实证3 + 诊断留痕:落最终 worker argv(spawn 前的真实形态)。
|
|
378
380
|
// 任何"--session-id 预定 UUID 没生效"必须能从 events.jsonl 回答:argv 里到底有没有它。
|
|
379
381
|
// 抽出 --session-id 值单列,方便和盘上 ~/.claude/projects/<cwd> 实际落的 UUID 对账。
|
|
@@ -393,6 +395,10 @@ fn spawn_agents(
|
|
|
393
395
|
"argv": plan.argv,
|
|
394
396
|
"session_id_in_argv": session_id_in_argv,
|
|
395
397
|
"expected_session_id": plan.expected_session_id.as_ref().map(|s| s.as_str()),
|
|
398
|
+
"spawn_cwd": workspace.to_string_lossy(),
|
|
399
|
+
"spawned_at": spawned_at.as_str(),
|
|
400
|
+
"source": "launch",
|
|
401
|
+
"spawn_epoch": spawn_epoch,
|
|
396
402
|
}),
|
|
397
403
|
);
|
|
398
404
|
}
|
|
@@ -473,6 +479,7 @@ fn spawn_agents(
|
|
|
473
479
|
agent_id,
|
|
474
480
|
start_mode: StartMode::Fresh,
|
|
475
481
|
target: spawn.pane_id.as_str().to_string(),
|
|
482
|
+
spawned_at,
|
|
476
483
|
session_id: None,
|
|
477
484
|
rollout_path: None,
|
|
478
485
|
pending_session_id: plan.expected_session_id.clone(),
|
|
@@ -872,7 +879,9 @@ fn persist_spawn_agent_state(
|
|
|
872
879
|
continue;
|
|
873
880
|
}
|
|
874
881
|
let pane_pid = pane_pids_by_agent.get(id).copied();
|
|
875
|
-
let spawned_at =
|
|
882
|
+
let spawned_at = started_agent
|
|
883
|
+
.map(|started| started.spawned_at.clone())
|
|
884
|
+
.unwrap_or_else(|| spawn_timestamp_for_agent(spawn_index));
|
|
876
885
|
spawn_index = spawn_index.saturating_add(1);
|
|
877
886
|
agents.insert(
|
|
878
887
|
id.to_string(),
|
|
@@ -2970,11 +2979,10 @@ pub fn quick_start_in_workspace_with_display_and_backend(
|
|
|
2970
2979
|
// `state.transport.shim.pipe_ready = true` marker so its
|
|
2971
2980
|
// `conpty_pipe_ready` gate opens.
|
|
2972
2981
|
#[cfg(windows)]
|
|
2973
|
-
let state_value =
|
|
2974
|
-
&workspace
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
.and_then(|t| serde_json::from_str::<serde_json::Value>(&t).ok());
|
|
2982
|
+
let state_value =
|
|
2983
|
+
std::fs::read_to_string(crate::state::persist::runtime_state_path(&workspace))
|
|
2984
|
+
.ok()
|
|
2985
|
+
.and_then(|t| serde_json::from_str::<serde_json::Value>(&t).ok());
|
|
2978
2986
|
#[cfg(windows)]
|
|
2979
2987
|
let input = match state_value.as_ref() {
|
|
2980
2988
|
Some(v) => input.with_state(Some(v)),
|