@team-agent/installer 0.5.51 → 0.5.53
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/adapters.rs +18 -2
- package/crates/team-agent/src/cli/emit.rs +17 -0
- package/crates/team-agent/src/cli/mod.rs +155 -4
- package/crates/team-agent/src/cli/named_address.rs +35 -67
- package/crates/team-agent/src/cli/spec.rs +3 -0
- package/crates/team-agent/src/cli/tests/main_preserved.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +11 -0
- package/crates/team-agent/src/diagnose/orphans.rs +24 -3
- package/crates/team-agent/src/kill_audit.rs +67 -0
- package/crates/team-agent/src/leader/lease.rs +526 -73
- package/crates/team-agent/src/leader/rediscover/tests.rs +3 -0
- package/crates/team-agent/src/leader/rediscover.rs +6 -0
- package/crates/team-agent/src/leader/start.rs +144 -23
- package/crates/team-agent/src/leader/tests/idle.rs +3 -0
- package/crates/team-agent/src/leader/types.rs +6 -0
- package/crates/team-agent/src/lib.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +12 -1
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +106 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +216 -208
- package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +36 -0
- package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +238 -0
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +101 -5
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/role_source.rs +170 -0
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +8 -8
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch.rs +14 -2
- package/crates/team-agent/src/lifecycle/restart/agent.rs +20 -4
- package/crates/team-agent/src/lifecycle/restart/common.rs +12 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +113 -4
- package/crates/team-agent/src/lifecycle/restart/remove.rs +73 -10
- package/crates/team-agent/src/lifecycle/restart.rs +25 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +441 -12
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +20 -2
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +1 -0
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +3 -1
- package/crates/team-agent/src/lifecycle/types.rs +17 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +40 -5
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -1
- package/crates/team-agent/src/mcp_server/tests/tools.rs +18 -1
- package/crates/team-agent/src/mcp_server/tests/wire.rs +243 -167
- package/crates/team-agent/src/mcp_server/tests.rs +11 -4
- package/crates/team-agent/src/mcp_server/tools.rs +18 -4
- package/crates/team-agent/src/mcp_server/types.rs +3 -0
- package/crates/team-agent/src/mcp_server/wire.rs +30 -7
- package/crates/team-agent/src/messaging/delivery.rs +126 -147
- package/crates/team-agent/src/messaging/leader_channel.rs +198 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +68 -21
- package/crates/team-agent/src/messaging/mod.rs +5 -0
- package/crates/team-agent/src/messaging/tests/leader_channel.rs +169 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +34 -23
- package/crates/team-agent/src/provider/adapter.rs +54 -13
- package/crates/team-agent/src/provider/adapters/claude_fork.rs +122 -0
- package/crates/team-agent/src/provider/adapters/copilot_fork.rs +306 -0
- package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
- package/crates/team-agent/src/provider/session/capture.rs +321 -52
- package/crates/team-agent/src/provider/session/context_fork.rs +499 -0
- package/crates/team-agent/src/provider/session/mod.rs +6 -0
- package/crates/team-agent/src/provider/session_scan/claude.rs +1 -1
- package/crates/team-agent/src/provider/session_scan/codex.rs +144 -27
- package/crates/team-agent/src/provider/session_scan/common.rs +39 -6
- package/crates/team-agent/src/provider/session_scan/copilot.rs +47 -3
- package/crates/team-agent/src/provider/session_scan.rs +3 -3
- package/crates/team-agent/src/provider/tests/copilot_fork.rs +191 -0
- package/crates/team-agent/src/provider/tests.rs +1 -0
- package/crates/team-agent/src/state/persist.rs +282 -20
- package/crates/team-agent/src/state/projection.rs +245 -30
- package/crates/team-agent/src/state/repository/intent.rs +13 -0
- package/crates/team-agent/src/state/repository.rs +36 -12
- package/crates/team-agent/src/state/selector.rs +9 -18
- package/crates/team-agent/src/tmux_backend/tests.rs +51 -16
- package/crates/team-agent/src/tmux_backend.rs +29 -2
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +8 -3
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -876,8 +876,10 @@ fn fake_shutdown(workspace: &Path) -> Result<Value, CliError> {
|
|
|
876
876
|
}
|
|
877
877
|
}
|
|
878
878
|
}
|
|
879
|
-
crate::state::repository::StateRepository::new(workspace)
|
|
880
|
-
|
|
879
|
+
crate::state::repository::StateRepository::new(workspace).save(
|
|
880
|
+
crate::state::repository::StateWriteIntent::FakeE2eSeed,
|
|
881
|
+
&state,
|
|
882
|
+
)?;
|
|
881
883
|
Ok(json!({
|
|
882
884
|
"ok": true,
|
|
883
885
|
"session_name": state.get("session_name").cloned().unwrap_or(Value::Null),
|
|
@@ -1375,6 +1377,20 @@ pub fn cmd_fork_agent(args: &ForkAgentArgs) -> Result<CmdResult, CliError> {
|
|
|
1375
1377
|
))
|
|
1376
1378
|
}
|
|
1377
1379
|
|
|
1380
|
+
pub fn cmd_clone_agent(args: &CloneAgentArgs) -> Result<CmdResult, CliError> {
|
|
1381
|
+
Ok(CmdResult::from_json(
|
|
1382
|
+
lifecycle_port::clone_agent(
|
|
1383
|
+
&args.workspace,
|
|
1384
|
+
&args.source_agent,
|
|
1385
|
+
&args.as_agent,
|
|
1386
|
+
args.label.as_deref(),
|
|
1387
|
+
!args.no_display,
|
|
1388
|
+
args.team.as_deref(),
|
|
1389
|
+
)?,
|
|
1390
|
+
args.json,
|
|
1391
|
+
))
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1378
1394
|
/// `cmd_remove_agent`(`commands.py:394`)。
|
|
1379
1395
|
pub fn cmd_remove_agent(args: &RemoveAgentArgs) -> Result<CmdResult, CliError> {
|
|
1380
1396
|
Ok(CmdResult::from_json(
|
|
@@ -115,6 +115,7 @@ fn dispatch(command: &str, args: &[String], cwd: &Path) -> Result<ExitCode, CliE
|
|
|
115
115
|
"stop-agent" => cmd_stop_agent(&stop_agent_args(args, cwd)?).map(emit_result),
|
|
116
116
|
"reset-agent" => cmd_reset_agent(&reset_agent_args(args, cwd)?).map(emit_result),
|
|
117
117
|
"add-agent" => cmd_add_agent(&add_agent_args(args, cwd)?).map(emit_result),
|
|
118
|
+
"clone-agent" => cmd_clone_agent(&clone_agent_args(args, cwd)?).map(emit_result),
|
|
118
119
|
"fork-agent" => cmd_fork_agent(&fork_agent_args(args, cwd)?).map(emit_result),
|
|
119
120
|
"remove-agent" => cmd_remove_agent(&remove_agent_args(args, cwd)?).map(emit_result),
|
|
120
121
|
"stuck-list" => cmd_stuck_list(&stuck_list_args(args, cwd)).map(emit_result),
|
|
@@ -330,6 +331,7 @@ fn command_help(command: Option<&str>) -> String {
|
|
|
330
331
|
Some("start-agent") => "usage: team-agent start-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--force] [--allow-fresh] [--no-display] [--json]".to_string(),
|
|
331
332
|
Some("stop-agent") => "usage: team-agent stop-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
|
|
332
333
|
Some("add-agent") => "usage: team-agent add-agent AGENT --role-file FILE [--force] [--workspace WORKSPACE] [--team TEAM] [--no-display] [--json]".to_string(),
|
|
334
|
+
Some("clone-agent") => "usage: team-agent clone-agent SOURCE_AGENT --as AGENT [--label LABEL] [--workspace WORKSPACE] [--team TEAM] [--no-display] [--json]".to_string(),
|
|
333
335
|
Some("fork-agent") => "usage: team-agent fork-agent SOURCE_AGENT --as AGENT [--label LABEL] [--workspace WORKSPACE] [--team TEAM] [--no-display] [--json]".to_string(),
|
|
334
336
|
Some("remove-agent") => "usage: team-agent remove-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--from-spec] [--confirm] [--force] [--json]".to_string(),
|
|
335
337
|
// 0.5.26 (§7.6): removed from help; dispatch was never wired.
|
|
@@ -1366,6 +1368,21 @@ fn fork_agent_args(args: &[String], cwd: &Path) -> Result<ForkAgentArgs, CliErro
|
|
|
1366
1368
|
})
|
|
1367
1369
|
}
|
|
1368
1370
|
|
|
1371
|
+
fn clone_agent_args(args: &[String], cwd: &Path) -> Result<CloneAgentArgs, CliError> {
|
|
1372
|
+
let parsed = parse_args(args);
|
|
1373
|
+
Ok(CloneAgentArgs {
|
|
1374
|
+
source_agent: required_pos(&parsed, 0, "source_agent")?,
|
|
1375
|
+
workspace: workspace(&parsed, cwd),
|
|
1376
|
+
team: parsed.team,
|
|
1377
|
+
as_agent: parsed
|
|
1378
|
+
.as_agent
|
|
1379
|
+
.ok_or_else(|| CliError::Usage("missing --as".to_string()))?,
|
|
1380
|
+
label: parsed.label,
|
|
1381
|
+
no_display: parsed.no_display,
|
|
1382
|
+
json: parsed.json,
|
|
1383
|
+
})
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1369
1386
|
fn remove_agent_args(args: &[String], cwd: &Path) -> Result<RemoveAgentArgs, CliError> {
|
|
1370
1387
|
let parsed = parse_args(args);
|
|
1371
1388
|
let workspace = workspace(&parsed, cwd);
|
|
@@ -374,6 +374,19 @@ pub mod lifecycle_port {
|
|
|
374
374
|
);
|
|
375
375
|
let mut error = None;
|
|
376
376
|
for session in &sessions {
|
|
377
|
+
let targets = vec![session.as_str().to_string()];
|
|
378
|
+
if let Err(audit_error) = crate::kill_audit::pre_kill_audit(
|
|
379
|
+
event_log,
|
|
380
|
+
transport,
|
|
381
|
+
"shutdown.shared_socket_individual",
|
|
382
|
+
crate::kill_audit::KILL_SESSION,
|
|
383
|
+
&targets,
|
|
384
|
+
) {
|
|
385
|
+
error.get_or_insert_with(|| {
|
|
386
|
+
format!("pre-kill audit failed: {audit_error}")
|
|
387
|
+
});
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
377
390
|
if let Err(err) = transport.kill_session(session) {
|
|
378
391
|
if !tmux_absent_error(&err.to_string()) {
|
|
379
392
|
error.get_or_insert_with(|| err.to_string());
|
|
@@ -386,7 +399,20 @@ pub mod lifecycle_port {
|
|
|
386
399
|
error,
|
|
387
400
|
};
|
|
388
401
|
}
|
|
389
|
-
let
|
|
402
|
+
let targets = sessions
|
|
403
|
+
.iter()
|
|
404
|
+
.map(|session| session.as_str().to_string())
|
|
405
|
+
.collect::<Vec<_>>();
|
|
406
|
+
let error = match crate::kill_audit::pre_kill_audit(
|
|
407
|
+
event_log,
|
|
408
|
+
transport,
|
|
409
|
+
"shutdown.exclusive_socket",
|
|
410
|
+
crate::kill_audit::KILL_SERVER,
|
|
411
|
+
&targets,
|
|
412
|
+
) {
|
|
413
|
+
Ok(()) => transport.kill_server().err().map(|error| error.to_string()),
|
|
414
|
+
Err(error) => Some(format!("pre-kill audit failed: {error}")),
|
|
415
|
+
};
|
|
390
416
|
ShutdownSocketCleanup {
|
|
391
417
|
killed_sessions: sessions,
|
|
392
418
|
spared_sessions: Vec::new(),
|
|
@@ -406,6 +432,18 @@ pub mod lifecycle_port {
|
|
|
406
432
|
}
|
|
407
433
|
let mut error = None;
|
|
408
434
|
for session in &to_kill {
|
|
435
|
+
let targets = vec![session.as_str().to_string()];
|
|
436
|
+
if let Err(audit_error) = crate::kill_audit::pre_kill_audit(
|
|
437
|
+
event_log,
|
|
438
|
+
transport,
|
|
439
|
+
"shutdown.selected_session",
|
|
440
|
+
crate::kill_audit::KILL_SESSION,
|
|
441
|
+
&targets,
|
|
442
|
+
) {
|
|
443
|
+
error
|
|
444
|
+
.get_or_insert_with(|| format!("pre-kill audit failed: {audit_error}"));
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
409
447
|
if let Err(err) = transport.kill_session(session) {
|
|
410
448
|
if !tmux_absent_error(&err.to_string()) {
|
|
411
449
|
error.get_or_insert_with(|| err.to_string());
|
|
@@ -421,6 +459,59 @@ pub mod lifecycle_port {
|
|
|
421
459
|
}
|
|
422
460
|
}
|
|
423
461
|
|
|
462
|
+
#[cfg(test)]
|
|
463
|
+
mod kill_audit_tests {
|
|
464
|
+
use std::collections::BTreeMap;
|
|
465
|
+
|
|
466
|
+
use super::*;
|
|
467
|
+
use crate::transport::test_support::OfflineTransport;
|
|
468
|
+
use crate::transport::{PaneId, PaneInfo, SessionName};
|
|
469
|
+
|
|
470
|
+
#[test]
|
|
471
|
+
fn audit_write_failure_skips_destructive_kill() {
|
|
472
|
+
let root =
|
|
473
|
+
std::env::temp_dir().join(format!("ta-kill-audit-fail-{}", std::process::id()));
|
|
474
|
+
let blocked_parent = root.join("not-a-directory");
|
|
475
|
+
std::fs::create_dir_all(&root).unwrap();
|
|
476
|
+
std::fs::write(&blocked_parent, b"file").unwrap();
|
|
477
|
+
let event_log = crate::event_log::EventLog::at(blocked_parent.join("events.jsonl"));
|
|
478
|
+
let session = SessionName::new("team-audit-fail");
|
|
479
|
+
let transport = OfflineTransport::new()
|
|
480
|
+
.with_session_present(true)
|
|
481
|
+
.with_targets(vec![PaneInfo {
|
|
482
|
+
pane_id: PaneId::new("%1"),
|
|
483
|
+
session: session.clone(),
|
|
484
|
+
window_index: Some(0),
|
|
485
|
+
window_name: None,
|
|
486
|
+
pane_index: Some(0),
|
|
487
|
+
tty: None,
|
|
488
|
+
current_command: None,
|
|
489
|
+
current_path: None,
|
|
490
|
+
active: true,
|
|
491
|
+
pane_pid: None,
|
|
492
|
+
leader_env: BTreeMap::new(),
|
|
493
|
+
}]);
|
|
494
|
+
let cleanup = bare_shutdown_socket_cleanup(
|
|
495
|
+
&transport,
|
|
496
|
+
&json!({
|
|
497
|
+
"is_external_leader": true,
|
|
498
|
+
"tmux_socket_source": "leader_env",
|
|
499
|
+
}),
|
|
500
|
+
&event_log,
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
assert!(
|
|
504
|
+
!transport.calls().contains(&"kill_session"),
|
|
505
|
+
"an unaudited destructive call must not execute"
|
|
506
|
+
);
|
|
507
|
+
assert!(cleanup
|
|
508
|
+
.error
|
|
509
|
+
.as_deref()
|
|
510
|
+
.is_some_and(|error| error.contains("pre-kill audit failed")));
|
|
511
|
+
let _ = std::fs::remove_dir_all(root);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
424
515
|
fn state_uses_external_leader(state: &Value) -> bool {
|
|
425
516
|
crate::state::projection::state_is_external_leader(state)
|
|
426
517
|
}
|
|
@@ -501,6 +592,17 @@ pub mod lifecycle_port {
|
|
|
501
592
|
);
|
|
502
593
|
continue;
|
|
503
594
|
}
|
|
595
|
+
let targets = vec![session.as_str().to_string()];
|
|
596
|
+
if let Err(audit_error) = crate::kill_audit::pre_kill_audit(
|
|
597
|
+
event_log,
|
|
598
|
+
transport,
|
|
599
|
+
"shutdown.managed_worker_session",
|
|
600
|
+
crate::kill_audit::KILL_SESSION,
|
|
601
|
+
&targets,
|
|
602
|
+
) {
|
|
603
|
+
error.get_or_insert_with(|| format!("pre-kill audit failed: {audit_error}"));
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
504
606
|
if let Err(err) = transport.kill_session(session) {
|
|
505
607
|
if !tmux_absent_error(&err.to_string()) {
|
|
506
608
|
error.get_or_insert_with(|| err.to_string());
|
|
@@ -598,6 +700,16 @@ pub mod lifecycle_port {
|
|
|
598
700
|
);
|
|
599
701
|
return cleanup;
|
|
600
702
|
}
|
|
703
|
+
if let Err(error) = crate::kill_audit::pre_kill_audit(
|
|
704
|
+
event_log,
|
|
705
|
+
transport,
|
|
706
|
+
"shutdown.empty_owned_endpoint",
|
|
707
|
+
crate::kill_audit::KILL_SERVER,
|
|
708
|
+
&[],
|
|
709
|
+
) {
|
|
710
|
+
cleanup.error = Some(format!("pre-kill audit failed: {error}"));
|
|
711
|
+
return cleanup;
|
|
712
|
+
}
|
|
601
713
|
if let Err(error) = transport.kill_server() {
|
|
602
714
|
cleanup.error = Some(error.to_string());
|
|
603
715
|
}
|
|
@@ -748,11 +860,24 @@ pub mod lifecycle_port {
|
|
|
748
860
|
let _ =
|
|
749
861
|
crate::lifecycle::display::close_team_display_backends(&run_workspace, session);
|
|
750
862
|
} else {
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
863
|
+
let targets = vec![session.as_str().to_string()];
|
|
864
|
+
let event_log = crate::event_log::EventLog::new(&run_workspace);
|
|
865
|
+
if let Err(error) = crate::kill_audit::pre_kill_audit(
|
|
866
|
+
&event_log,
|
|
867
|
+
transport,
|
|
868
|
+
"shutdown.team_session",
|
|
869
|
+
crate::kill_audit::KILL_SESSION,
|
|
870
|
+
&targets,
|
|
871
|
+
) {
|
|
872
|
+
kill_error = Some(format!("pre-kill audit failed: {error}"));
|
|
873
|
+
} else if let Err(error) = transport.kill_session(session) {
|
|
874
|
+
if tmux_absent_error(&error.to_string()) {
|
|
875
|
+
push_unique_session(&mut killed_sessions, session.clone());
|
|
876
|
+
} else {
|
|
754
877
|
kill_error = Some(error.to_string());
|
|
755
878
|
}
|
|
879
|
+
} else {
|
|
880
|
+
push_unique_session(&mut killed_sessions, session.clone());
|
|
756
881
|
}
|
|
757
882
|
let _ =
|
|
758
883
|
crate::lifecycle::display::close_team_display_backends(&run_workspace, session);
|
|
@@ -2357,10 +2482,36 @@ pub mod lifecycle_port {
|
|
|
2357
2482
|
"ok": true,
|
|
2358
2483
|
"source_agent_id": report.source_agent_id.as_str(),
|
|
2359
2484
|
"new_agent_id": report.new_agent_id.as_str(),
|
|
2485
|
+
"session_id": report.session_id.as_ref().map(|session| session.as_str()),
|
|
2486
|
+
"new_session_id": report.session_id.as_ref().map(|session| session.as_str()),
|
|
2360
2487
|
})),
|
|
2361
2488
|
Err(e) => Ok(error_value(e)),
|
|
2362
2489
|
}
|
|
2363
2490
|
}
|
|
2491
|
+
pub fn clone_agent(
|
|
2492
|
+
workspace: &Path,
|
|
2493
|
+
source_agent: &str,
|
|
2494
|
+
as_agent_id: &str,
|
|
2495
|
+
label: Option<&str>,
|
|
2496
|
+
open_display: bool,
|
|
2497
|
+
team: Option<&str>,
|
|
2498
|
+
) -> Result<Value, CliError> {
|
|
2499
|
+
let source = crate::model::ids::AgentId::new(source_agent);
|
|
2500
|
+
let dest = crate::model::ids::AgentId::new(as_agent_id);
|
|
2501
|
+
match crate::lifecycle::clone_agent(workspace, &source, &dest, label, open_display, team) {
|
|
2502
|
+
Ok(report) => Ok(json!({
|
|
2503
|
+
"ok": true,
|
|
2504
|
+
"status": "cloned",
|
|
2505
|
+
"source_agent_id": report.source_agent_id.as_str(),
|
|
2506
|
+
"new_agent_id": report.new_agent_id.as_str(),
|
|
2507
|
+
"session_id": report.session_id.as_ref().map(|session| session.as_str()),
|
|
2508
|
+
"new_session_id": report.session_id.as_ref().map(|session| session.as_str()),
|
|
2509
|
+
"backing_path": report.backing_path.as_ref().map(|path| path.to_string_lossy()),
|
|
2510
|
+
"backing_state": report.backing_state,
|
|
2511
|
+
})),
|
|
2512
|
+
Err(error) => Ok(error_value(error)),
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2364
2515
|
/// `runtime.remove_agent`(`cmd_remove_agent`;`--from-spec` 须配 `--confirm`)。
|
|
2365
2516
|
pub fn remove_agent(
|
|
2366
2517
|
workspace: &Path,
|
|
@@ -939,103 +939,71 @@ fn resolve_leader(
|
|
|
939
939
|
);
|
|
940
940
|
return Err(err);
|
|
941
941
|
}
|
|
942
|
-
if crate::codex_app_server::receiver_is_app_server(receiver) {
|
|
943
|
-
return resolve_app_server_leader(
|
|
944
|
-
sender_workspace,
|
|
945
|
-
target_workspace,
|
|
946
|
-
team,
|
|
947
|
-
parsed,
|
|
948
|
-
receiver,
|
|
949
|
-
);
|
|
950
|
-
}
|
|
951
|
-
let pane_id = string_field(receiver, "pane_id")
|
|
952
|
-
.filter(|pane| !pane.is_empty())
|
|
953
|
-
.ok_or_else(|| {
|
|
954
|
-
let mut err = leader_not_attached_third_party(
|
|
955
|
-
target_workspace,
|
|
956
|
-
team,
|
|
957
|
-
None,
|
|
958
|
-
None,
|
|
959
|
-
None,
|
|
960
|
-
transport.tmux_endpoint(),
|
|
961
|
-
);
|
|
962
|
-
err.candidates =
|
|
963
|
-
leader_advisory_candidates(state, team, transport, "state_recorded_socket");
|
|
964
|
-
err
|
|
965
|
-
})?;
|
|
966
942
|
let session = string_field(receiver, "session_name");
|
|
967
943
|
let window = string_field(receiver, "window_name");
|
|
968
944
|
let socket = string_field(receiver, "tmux_socket")
|
|
969
945
|
.map(str::to_string)
|
|
970
946
|
.or_else(|| transport.tmux_endpoint());
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
.window_name
|
|
980
|
-
.as_ref()
|
|
981
|
-
.is_some_and(|name| name.as_str() == expected)
|
|
982
|
-
})
|
|
983
|
-
})
|
|
984
|
-
.collect::<Vec<_>>();
|
|
985
|
-
let stale_pane_seen = targets
|
|
986
|
-
.iter()
|
|
987
|
-
.any(|target| target.pane_id.as_str() == pane_id);
|
|
988
|
-
match matches.len() {
|
|
989
|
-
1 => Ok(ResolvedNamedAddress {
|
|
947
|
+
match crate::messaging::resolve_live_leader_channel(
|
|
948
|
+
target_workspace,
|
|
949
|
+
receiver,
|
|
950
|
+
transport,
|
|
951
|
+
) {
|
|
952
|
+
crate::messaging::LeaderChannelResolution::Live(
|
|
953
|
+
crate::messaging::LiveLeaderChannel::DirectTmux(channel),
|
|
954
|
+
) => Ok(ResolvedNamedAddress {
|
|
990
955
|
raw_name: parsed.display_name(),
|
|
991
956
|
target_kind: NamedTargetKind::Leader,
|
|
992
957
|
sender_workspace: sender_workspace.to_path_buf(),
|
|
993
958
|
target_workspace: target_workspace.to_path_buf(),
|
|
994
959
|
team_key: Some(team.to_string()),
|
|
995
960
|
agent_id: None,
|
|
996
|
-
pane_id: pane_id.
|
|
961
|
+
pane_id: channel.pane_id.clone(),
|
|
997
962
|
session_name: session.map(str::to_string),
|
|
998
963
|
window_name: window.map(str::to_string),
|
|
999
964
|
tmux_endpoint: socket,
|
|
1000
965
|
transport_kind: Some("direct_tmux".to_string()),
|
|
1001
966
|
app_server: None,
|
|
1002
|
-
state_pane_id: Some(pane_id
|
|
967
|
+
state_pane_id: Some(channel.pane_id),
|
|
1003
968
|
state_pane_stale: false,
|
|
1004
969
|
agent_status: None,
|
|
1005
|
-
warning:
|
|
970
|
+
warning: (!channel.metadata_drift.is_empty()).then(|| {
|
|
971
|
+
format!(
|
|
972
|
+
"leader_channel_metadata_drift:{}",
|
|
973
|
+
channel.metadata_drift.join(",")
|
|
974
|
+
)
|
|
975
|
+
}),
|
|
1006
976
|
}),
|
|
1007
|
-
|
|
977
|
+
crate::messaging::LeaderChannelResolution::Live(
|
|
978
|
+
crate::messaging::LiveLeaderChannel::CodexAppServer(_),
|
|
979
|
+
) => resolve_app_server_leader(sender_workspace, target_workspace, team, parsed, receiver),
|
|
980
|
+
crate::messaging::LeaderChannelResolution::Unbound(reason) => {
|
|
981
|
+
let pane_id = string_field(receiver, "pane_id");
|
|
1008
982
|
let mut err = leader_not_attached_third_party(
|
|
1009
983
|
target_workspace,
|
|
1010
984
|
team,
|
|
1011
|
-
|
|
985
|
+
pane_id,
|
|
1012
986
|
session,
|
|
1013
987
|
window,
|
|
1014
988
|
socket,
|
|
1015
989
|
);
|
|
1016
|
-
|
|
1017
|
-
err.log = format!("{} stale_tuple_mismatch=true", err.log);
|
|
1018
|
-
}
|
|
990
|
+
err.log = format!("{} channel_unbound={reason:?}", err.log);
|
|
1019
991
|
err.candidates =
|
|
1020
992
|
leader_advisory_candidates(state, team, transport, "state_recorded_socket");
|
|
1021
993
|
Err(err)
|
|
1022
994
|
}
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
})
|
|
1036
|
-
})
|
|
1037
|
-
.collect(),
|
|
1038
|
-
)),
|
|
995
|
+
crate::messaging::LeaderChannelResolution::ProbeFailed(error) => {
|
|
996
|
+
let mut err = leader_not_attached_third_party(
|
|
997
|
+
target_workspace,
|
|
998
|
+
team,
|
|
999
|
+
string_field(receiver, "pane_id"),
|
|
1000
|
+
session,
|
|
1001
|
+
window,
|
|
1002
|
+
socket,
|
|
1003
|
+
);
|
|
1004
|
+
err.log = format!("{} channel_probe_failed={error}", err.log);
|
|
1005
|
+
Err(err)
|
|
1006
|
+
}
|
|
1039
1007
|
}
|
|
1040
1008
|
}
|
|
1041
1009
|
|
|
@@ -55,6 +55,7 @@ pub(crate) enum DispatchKind {
|
|
|
55
55
|
StopAgent,
|
|
56
56
|
ResetAgent,
|
|
57
57
|
AddAgent,
|
|
58
|
+
CloneAgent,
|
|
58
59
|
ForkAgent,
|
|
59
60
|
RemoveAgent,
|
|
60
61
|
StuckList,
|
|
@@ -98,6 +99,7 @@ pub(crate) const ALL_DISPATCH_KINDS: &[DispatchKind] = &[
|
|
|
98
99
|
DispatchKind::StopAgent,
|
|
99
100
|
DispatchKind::ResetAgent,
|
|
100
101
|
DispatchKind::AddAgent,
|
|
102
|
+
DispatchKind::CloneAgent,
|
|
101
103
|
DispatchKind::ForkAgent,
|
|
102
104
|
DispatchKind::RemoveAgent,
|
|
103
105
|
DispatchKind::StuckList,
|
|
@@ -183,6 +185,7 @@ pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
|
|
|
183
185
|
CommandSpec { name: "attach-app-server-leader", tier: CommandTier::Guided, category: CommandCategory::GuidedRecovery, kind: CommandKind::Dispatch(DispatchKind::AttachAppServerLeader), summary: "bind an app-server leader receiver", usage: "usage: team-agent attach-app-server-leader [--workspace WORKSPACE] [--team TEAM] --socket unix:///path.sock --thread-id THREAD_ID [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
184
186
|
CommandSpec { name: "remove-agent", tier: CommandTier::Secondary, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::RemoveAgent), summary: "remove a worker", usage: "usage: team-agent remove-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--from-spec] [--confirm] [--force] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
185
187
|
CommandSpec { name: "fork-agent", tier: CommandTier::Secondary, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::ForkAgent), summary: "fork a worker", usage: "usage: team-agent fork-agent SOURCE_AGENT --as AGENT [--label LABEL] [--workspace WORKSPACE] [--team TEAM] [--no-display] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
|
|
188
|
+
CommandSpec { name: "clone-agent", tier: CommandTier::Secondary, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::CloneAgent), summary: "clone a worker role into a fresh session", usage: "usage: team-agent clone-agent SOURCE_AGENT --as AGENT [--label LABEL] [--workspace WORKSPACE] [--team TEAM] [--no-display] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
|
|
186
189
|
CommandSpec { name: "allow-peer-talk", tier: CommandTier::Secondary, category: CommandCategory::Setup, kind: CommandKind::Dispatch(DispatchKind::AllowPeerTalk), summary: "allow direct worker peer talk", usage: "usage: team-agent allow-peer-talk A B [--workspace WORKSPACE] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: Some(GovernanceNote { decision: "secondary", reason: "capability/config surface" }) },
|
|
187
190
|
CommandSpec { name: "approvals", tier: CommandTier::Secondary, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Approvals), summary: "inspect provider approval state", usage: "usage: team-agent approvals [AGENT] [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: Some(GovernanceNote { decision: "secondary", reason: "approval diagnostics" }) },
|
|
188
191
|
CommandSpec { name: "profile", tier: CommandTier::Secondary, category: CommandCategory::Setup, kind: CommandKind::Dispatch(DispatchKind::Profile), summary: "manage provider profiles", usage: "usage: team-agent profile COMMAND NAME [--workspace WORKSPACE] [--team TEAM] [--auth-mode MODE] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: Some(GovernanceNote { decision: "secondary", reason: "provider/profile setup" }) },
|
|
@@ -400,6 +400,7 @@ fn ux_wait_ready_does_not_report_ready_true_without_ready_runtime_state() {
|
|
|
400
400
|
crate::state::persist::save_runtime_state(
|
|
401
401
|
&ws,
|
|
402
402
|
&json!({
|
|
403
|
+
"active_team_key": "wait-ready",
|
|
403
404
|
"agents": {"w1": {"status": "starting"}},
|
|
404
405
|
"tasks": [{"id": "t1", "assignee": "w1", "status": "pending"}],
|
|
405
406
|
"leader_receiver": {"status": "attached"},
|
|
@@ -514,6 +514,17 @@ pub struct ForkAgentArgs {
|
|
|
514
514
|
pub json: bool,
|
|
515
515
|
}
|
|
516
516
|
|
|
517
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
518
|
+
pub struct CloneAgentArgs {
|
|
519
|
+
pub source_agent: String,
|
|
520
|
+
pub workspace: PathBuf,
|
|
521
|
+
pub team: Option<String>,
|
|
522
|
+
pub as_agent: String,
|
|
523
|
+
pub label: Option<String>,
|
|
524
|
+
pub no_display: bool,
|
|
525
|
+
pub json: bool,
|
|
526
|
+
}
|
|
527
|
+
|
|
517
528
|
/// `remove-agent`(`parser.py:414`)。`--from-spec` 须配 `--confirm`。
|
|
518
529
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
519
530
|
pub struct RemoveAgentArgs {
|
|
@@ -48,6 +48,14 @@ impl OrphanScanScope {
|
|
|
48
48
|
canonical_current_workspace: canonicalize_workspace(workspace),
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
+
|
|
52
|
+
fn workspace(&self) -> &Path {
|
|
53
|
+
match self {
|
|
54
|
+
Self::CurrentWorkspace {
|
|
55
|
+
canonical_current_workspace,
|
|
56
|
+
} => canonical_current_workspace,
|
|
57
|
+
}
|
|
58
|
+
}
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
fn canonicalize_workspace(workspace: &Path) -> PathBuf {
|
|
@@ -262,7 +270,7 @@ fn cleanup_report(report: ScanReport, scope: &OrphanScanScope) -> CleanupReport
|
|
|
262
270
|
}
|
|
263
271
|
continue;
|
|
264
272
|
}
|
|
265
|
-
if kill_tmux_session(orphan) {
|
|
273
|
+
if kill_tmux_session(orphan, scope.workspace()) {
|
|
266
274
|
killed.push(orphan_value(orphan, "killed"));
|
|
267
275
|
} else {
|
|
268
276
|
failed.push(orphan_value(orphan, "failed"));
|
|
@@ -790,14 +798,27 @@ fn orphan_value(orphan: &OrphanRecord, action: &str) -> Value {
|
|
|
790
798
|
value
|
|
791
799
|
}
|
|
792
800
|
|
|
793
|
-
fn kill_tmux_session(orphan: &OrphanRecord) -> bool {
|
|
801
|
+
fn kill_tmux_session(orphan: &OrphanRecord, audit_workspace: &Path) -> bool {
|
|
794
802
|
let (Some(socket), Some(session)) = (&orphan.tmux_socket, &orphan.session) else {
|
|
795
803
|
return false;
|
|
796
804
|
};
|
|
797
805
|
// Phase 1d Batch 6: factory tmux-socket-name helper. Explicit
|
|
798
806
|
// tmux-only kill; conpty shim orphans are NOT routed here (design
|
|
799
807
|
// §Batch 6).
|
|
800
|
-
crate::transport_factory::tmux_socket_name_transport(socket)
|
|
808
|
+
let transport = crate::transport_factory::tmux_socket_name_transport(socket);
|
|
809
|
+
let targets = vec![session.clone()];
|
|
810
|
+
if crate::kill_audit::pre_kill_audit(
|
|
811
|
+
&crate::event_log::EventLog::new(audit_workspace),
|
|
812
|
+
&transport,
|
|
813
|
+
"diagnose.orphans.tmux_session",
|
|
814
|
+
crate::kill_audit::KILL_SESSION,
|
|
815
|
+
&targets,
|
|
816
|
+
)
|
|
817
|
+
.is_err()
|
|
818
|
+
{
|
|
819
|
+
return false;
|
|
820
|
+
}
|
|
821
|
+
transport
|
|
801
822
|
.kill_session(&SessionName::new(session.clone()))
|
|
802
823
|
.is_ok()
|
|
803
824
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
//! Durable, fail-closed observation immediately before destructive transport calls.
|
|
2
|
+
|
|
3
|
+
use std::path::Path;
|
|
4
|
+
|
|
5
|
+
use serde_json::json;
|
|
6
|
+
|
|
7
|
+
use crate::event_log::{EventLog, EventLogError};
|
|
8
|
+
use crate::transport::{BackendKind, Transport};
|
|
9
|
+
|
|
10
|
+
pub(crate) const KILL_SERVER: &str = "kill-server";
|
|
11
|
+
pub(crate) const KILL_SESSION: &str = "kill-session";
|
|
12
|
+
|
|
13
|
+
pub(crate) fn pre_kill_audit(
|
|
14
|
+
event_log: &EventLog,
|
|
15
|
+
transport: &dyn Transport,
|
|
16
|
+
caller: &str,
|
|
17
|
+
action: &str,
|
|
18
|
+
targets: &[String],
|
|
19
|
+
) -> Result<(), EventLogError> {
|
|
20
|
+
let endpoint = transport.tmux_endpoint();
|
|
21
|
+
let argv = destructive_argv(transport.kind(), endpoint.as_deref(), action, targets);
|
|
22
|
+
event_log.write(
|
|
23
|
+
"transport.pre_kill_audit",
|
|
24
|
+
json!({
|
|
25
|
+
"phase": "pre_call",
|
|
26
|
+
"caller": caller,
|
|
27
|
+
"caller_pid": std::process::id(),
|
|
28
|
+
"action": action,
|
|
29
|
+
"argv": argv,
|
|
30
|
+
"endpoint": endpoint,
|
|
31
|
+
"targets": targets,
|
|
32
|
+
}),
|
|
33
|
+
)?;
|
|
34
|
+
Ok(())
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fn destructive_argv(
|
|
38
|
+
kind: BackendKind,
|
|
39
|
+
endpoint: Option<&str>,
|
|
40
|
+
action: &str,
|
|
41
|
+
targets: &[String],
|
|
42
|
+
) -> Vec<String> {
|
|
43
|
+
let mut argv = if kind == BackendKind::Tmux {
|
|
44
|
+
let mut argv = vec!["tmux".to_string()];
|
|
45
|
+
if let Some(endpoint) = endpoint.filter(|endpoint| !endpoint.is_empty()) {
|
|
46
|
+
if endpoint != "default" {
|
|
47
|
+
argv.push(if Path::new(endpoint).is_absolute() {
|
|
48
|
+
"-S".to_string()
|
|
49
|
+
} else {
|
|
50
|
+
"-L".to_string()
|
|
51
|
+
});
|
|
52
|
+
argv.push(endpoint.to_string());
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
argv
|
|
56
|
+
} else {
|
|
57
|
+
vec![format!("{kind:?}").to_ascii_lowercase()]
|
|
58
|
+
};
|
|
59
|
+
argv.push(action.to_string());
|
|
60
|
+
if action == KILL_SESSION {
|
|
61
|
+
for target in targets {
|
|
62
|
+
argv.push("-t".to_string());
|
|
63
|
+
argv.push(target.clone());
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
argv
|
|
67
|
+
}
|