@team-agent/installer 0.5.49 → 0.5.51

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.
Files changed (90) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +6 -4
  4. package/crates/team-agent/src/cli/emit.rs +91 -39
  5. package/crates/team-agent/src/cli/mod.rs +33 -15
  6. package/crates/team-agent/src/cli/named_address.rs +82 -53
  7. package/crates/team-agent/src/cli/send/coordinator.rs +163 -0
  8. package/crates/team-agent/src/cli/send/mailbox.rs +99 -0
  9. package/crates/team-agent/src/cli/send/persist.rs +154 -0
  10. package/crates/team-agent/src/cli/send/presentation.rs +333 -0
  11. package/crates/team-agent/src/cli/send/resolve.rs +361 -0
  12. package/crates/team-agent/src/cli/send.rs +103 -1308
  13. package/crates/team-agent/src/cli/spec.rs +2 -2
  14. package/crates/team-agent/src/cli/status_port/agents.rs +358 -0
  15. package/crates/team-agent/src/cli/status_port/approvals.rs +79 -0
  16. package/crates/team-agent/src/cli/status_port/compact.rs +207 -0
  17. package/crates/team-agent/src/cli/status_port/format.rs +145 -0
  18. package/crates/team-agent/src/cli/status_port/inbox.rs +36 -0
  19. package/crates/team-agent/src/cli/status_port/runtime.rs +195 -0
  20. package/crates/team-agent/src/cli/status_port/snapshot.rs +181 -0
  21. package/crates/team-agent/src/cli/status_port/store.rs +412 -0
  22. package/crates/team-agent/src/cli/status_port/tests.rs +54 -0
  23. package/crates/team-agent/src/cli/status_port.rs +47 -1548
  24. package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -1
  25. package/crates/team-agent/src/cli/tests/named_address.rs +9 -7
  26. package/crates/team-agent/src/cli/tests/run_delegation.rs +2 -3
  27. package/crates/team-agent/src/cli/tests/status_send.rs +17 -33
  28. package/crates/team-agent/src/cli/types.rs +5 -8
  29. package/crates/team-agent/src/coordinator/conpty_shim.rs +34 -30
  30. package/crates/team-agent/src/coordinator/steps/abnormal.rs +135 -13
  31. package/crates/team-agent/src/coordinator/tick.rs +37 -0
  32. package/crates/team-agent/src/db/agent_health_capture.rs +18 -13
  33. package/crates/team-agent/src/db/message_store.rs +154 -44
  34. package/crates/team-agent/src/event_log.rs +73 -0
  35. package/crates/team-agent/src/leader/start.rs +28 -4
  36. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +424 -0
  37. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +297 -0
  38. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +160 -0
  39. package/crates/team-agent/src/lifecycle/launch/approval.rs +134 -0
  40. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +492 -0
  41. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +297 -0
  42. package/crates/team-agent/src/lifecycle/launch/identity.rs +372 -0
  43. package/crates/team-agent/src/lifecycle/launch/layout.rs +313 -0
  44. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +478 -0
  45. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +201 -0
  46. package/crates/team-agent/src/lifecycle/launch/ownership.rs +66 -0
  47. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +477 -0
  48. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +278 -0
  49. package/crates/team-agent/src/lifecycle/launch/readiness.rs +123 -0
  50. package/crates/team-agent/src/lifecycle/launch/spawn.rs +377 -0
  51. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +434 -0
  52. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +499 -0
  53. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +438 -0
  54. package/crates/team-agent/src/lifecycle/launch.rs +119 -5351
  55. package/crates/team-agent/src/lifecycle/restart/agent.rs +44 -26
  56. package/crates/team-agent/src/lifecycle/restart/common.rs +53 -27
  57. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +67 -26
  58. package/crates/team-agent/src/lifecycle/restart/remove.rs +435 -72
  59. package/crates/team-agent/src/lifecycle/restart.rs +1 -1
  60. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +575 -17
  61. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +55 -2
  62. package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +24 -1
  63. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +1 -1
  64. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +30 -7
  65. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +8 -4
  66. package/crates/team-agent/src/mcp_server/mod.rs +2 -2
  67. package/crates/team-agent/src/mcp_server/tests/send.rs +22 -15
  68. package/crates/team-agent/src/mcp_server/tests/wire.rs +6 -0
  69. package/crates/team-agent/src/mcp_server/tools.rs +26 -15
  70. package/crates/team-agent/src/mcp_server/wire.rs +2 -18
  71. package/crates/team-agent/src/messaging/activity.rs +4 -2
  72. package/crates/team-agent/src/messaging/address.rs +86 -0
  73. package/crates/team-agent/src/messaging/delivery.rs +165 -39
  74. package/crates/team-agent/src/messaging/helpers.rs +17 -13
  75. package/crates/team-agent/src/messaging/leader_receiver.rs +60 -35
  76. package/crates/team-agent/src/messaging/mod.rs +11 -2
  77. package/crates/team-agent/src/messaging/persist.rs +309 -0
  78. package/crates/team-agent/src/messaging/results.rs +16 -24
  79. package/crates/team-agent/src/messaging/scheduler.rs +4 -2
  80. package/crates/team-agent/src/messaging/selftest.rs +19 -12
  81. package/crates/team-agent/src/messaging/send.rs +133 -58
  82. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +305 -0
  83. package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
  84. package/crates/team-agent/src/messaging/tests/runtime.rs +38 -17
  85. package/crates/team-agent/src/messaging/watchers.rs +13 -3
  86. package/crates/team-agent/src/redaction.rs +72 -2
  87. package/crates/team-agent/src/state/persist.rs +2 -1
  88. package/crates/team-agent/src/state/repository/tests.rs +47 -0
  89. package/crates/team-agent/src/state/repository.rs +59 -16
  90. package/package.json +4 -4
package/Cargo.lock CHANGED
@@ -575,7 +575,7 @@ dependencies = [
575
575
 
576
576
  [[package]]
577
577
  name = "team-agent"
578
- version = "0.5.49"
578
+ version = "0.5.51"
579
579
  dependencies = [
580
580
  "anyhow",
581
581
  "chrono",
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent", "crates/win-conpty-phase0", "crates/conpty-trans
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.5.49"
12
+ version = "0.5.51"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -721,7 +721,7 @@ fn run_fake_e2e(workspace: &Path) -> Result<Value, CliError> {
721
721
  &SendOptions {
722
722
  task_id: Some(TaskId::new("task_impl")),
723
723
  route_task_id: false,
724
- sender: "leader".to_string(),
724
+ sender: messaging::TrustedSender::leader(),
725
725
  requires_ack: true,
726
726
  ..SendOptions::default()
727
727
  },
@@ -841,8 +841,8 @@ fn provider_command(provider: &str) -> &str {
841
841
  }
842
842
 
843
843
  fn seed_fake_e2e_state(workspace: &Path) -> Result<(), CliError> {
844
- crate::state::persist::save_runtime_state(
845
- workspace,
844
+ crate::state::repository::StateRepository::new(workspace).save(
845
+ crate::state::repository::StateWriteIntent::FakeE2eSeed,
846
846
  &json!({
847
847
  "leader": {"id": "leader"},
848
848
  "session_name": "team-agent-fake-e2e",
@@ -876,7 +876,8 @@ fn fake_shutdown(workspace: &Path) -> Result<Value, CliError> {
876
876
  }
877
877
  }
878
878
  }
879
- crate::state::persist::save_runtime_state(workspace, &state)?;
879
+ crate::state::repository::StateRepository::new(workspace)
880
+ .save(crate::state::repository::StateWriteIntent::FakeE2eSeed, &state)?;
880
881
  Ok(json!({
881
882
  "ok": true,
882
883
  "session_name": state.get("session_name").cloned().unwrap_or(Value::Null),
@@ -1353,6 +1354,7 @@ pub fn cmd_add_agent(args: &AddAgentArgs) -> Result<CmdResult, CliError> {
1353
1354
  &args.role_file,
1354
1355
  !args.no_display,
1355
1356
  args.team.as_deref(),
1357
+ args.force,
1356
1358
  )?,
1357
1359
  args.json,
1358
1360
  ))
@@ -315,21 +315,9 @@ fn command_help(command: Option<&str>) -> String {
315
315
  Some("start") => compat_hidden_help("start", "usage: team-agent start [TEAMDIR] [--yes] [--fresh] [--json]"),
316
316
  Some("compile") => "usage: team-agent compile --team TEAM [--out FILE] [--json]".to_string(),
317
317
  Some("send") => concat!(
318
- "usage: team-agent send TARGET MESSAGE... ",
319
- "[--workspace WORKSPACE] [--team TEAM] [--targets AGENTS] ",
320
- "[--to-name NAME] [--pane PANE] [--task TASK] [--sender SENDER] ",
321
- "[--watch-result] [--requires-ack|--no-ack] [--no-wait] ",
322
- "[--timeout SECONDS] [--confirm-human] [--message-id ID] [--json]\n\n",
323
- "TARGET is a short id scoped by --team; MCP `to` is a short id scoped ",
324
- "by the worker's owner team.\n",
325
- "--to-name accepts: --to-name AGENT, --to-name TEAM/AGENT, or ",
326
- "--to-name WORKSPACE::TEAM/AGENT.\n",
327
- "--team scopes only a bare --to-name AGENT; qualified forms keep the ",
328
- "scope in the address.\n",
329
- "TEAM/AGENT and WORKSPACE::TEAM/AGENT are not valid positional TARGET ",
330
- "or MCP `to` values.\n\n",
331
- "MVP: name-based cross-workspace addressing assumes trusted local ",
332
- "caller; no auth gate."
318
+ "usage: team-agent send TO MESSAGE... ",
319
+ "[--workspace WORKSPACE] [--team TEAM] [--json]\n\n",
320
+ "TO is a logical recipient; send returns after the message is persisted."
333
321
  )
334
322
  .to_string(),
335
323
  Some("allow-peer-talk") => "usage: team-agent allow-peer-talk A B [--workspace WORKSPACE] [--json]".to_string(),
@@ -341,7 +329,7 @@ fn command_help(command: Option<&str>) -> String {
341
329
  Some("reset-agent") => "usage: team-agent reset-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--discard-session] [--no-display] [--json]".to_string(),
342
330
  Some("start-agent") => "usage: team-agent start-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--force] [--allow-fresh] [--no-display] [--json]".to_string(),
343
331
  Some("stop-agent") => "usage: team-agent stop-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
344
- Some("add-agent") => "usage: team-agent add-agent AGENT --role-file FILE [--workspace WORKSPACE] [--team TEAM] [--no-display] [--json]".to_string(),
332
+ Some("add-agent") => "usage: team-agent add-agent AGENT --role-file FILE [--force] [--workspace WORKSPACE] [--team TEAM] [--no-display] [--json]".to_string(),
345
333
  Some("fork-agent") => "usage: team-agent fork-agent SOURCE_AGENT --as AGENT [--label LABEL] [--workspace WORKSPACE] [--team TEAM] [--no-display] [--json]".to_string(),
346
334
  Some("remove-agent") => "usage: team-agent remove-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--from-spec] [--confirm] [--force] [--json]".to_string(),
347
335
  // 0.5.26 (§7.6): removed from help; dispatch was never wired.
@@ -732,7 +720,6 @@ struct ParsedArgs {
732
720
  team_id: Option<String>,
733
721
  targets: Option<String>,
734
722
  task: Option<String>,
735
- sender: Option<String>,
736
723
  watch_result: bool,
737
724
  requires_ack: bool,
738
725
  no_ack: bool,
@@ -816,7 +803,6 @@ fn parse_args(args: &[String]) -> ParsedArgs {
816
803
  "--targets" | "--target" | "--to" => parsed.targets = next_arg(args, &mut i),
817
804
  "--task" => parsed.task = next_arg(args, &mut i),
818
805
  "--task-id" => parsed.task_id = next_arg(args, &mut i),
819
- "--sender" => parsed.sender = next_arg(args, &mut i),
820
806
  "--agent-id" => parsed.agent_id = next_arg(args, &mut i),
821
807
  "--watch-result" => parsed.watch_result = true,
822
808
  "--requires-ack" => parsed.requires_ack = true,
@@ -1027,6 +1013,8 @@ fn resolve_cli_path(cwd: &Path, path: &Path) -> PathBuf {
1027
1013
  }
1028
1014
 
1029
1015
  fn send_args(args: &[String], cwd: &Path) -> Result<SendArgs, CliError> {
1016
+ validate_send_flags(args)?;
1017
+ warn_send_legacy_delivery_flags(args);
1030
1018
  let parsed = parse_args(args);
1031
1019
  let target = if parsed.targets.is_some()
1032
1020
  || parsed.pane.is_some()
@@ -1050,21 +1038,96 @@ fn send_args(args: &[String], cwd: &Path) -> Result<SendArgs, CliError> {
1050
1038
  targets: parsed.targets,
1051
1039
  workspace,
1052
1040
  team: parsed.team,
1053
- task: parsed.task,
1054
- sender: parsed.sender.unwrap_or_else(|| "leader".to_string()),
1055
- no_ack: parsed.no_ack && !parsed.requires_ack,
1056
- no_wait: parsed.no_wait,
1057
- watch_result: parsed.watch_result,
1058
- timeout: parsed.timeout.unwrap_or(30.0),
1059
- confirm_human: parsed.confirm_human,
1041
+ task: None,
1042
+ sender: trusted_cli_sender(),
1043
+ no_ack: false,
1044
+ no_wait: true,
1045
+ watch_result: false,
1046
+ timeout: 0.0,
1047
+ confirm_human: false,
1060
1048
  json: parsed.json,
1061
- message_id: parsed.message_id,
1049
+ message_id: None,
1062
1050
  pane: parsed.pane.clone(),
1063
1051
  to_name: parsed.to_name.clone(),
1064
1052
  to_leader: parsed.to_leader.clone(),
1065
1053
  })
1066
1054
  }
1067
1055
 
1056
+ fn trusted_cli_sender() -> TrustedSender {
1057
+ std::env::var("TEAM_AGENT_ID")
1058
+ .ok()
1059
+ .map(|value| value.trim().to_string())
1060
+ .filter(|value| !value.is_empty())
1061
+ .map(crate::model::ids::AgentId::new)
1062
+ .map(TrustedSender::from_runtime_identity)
1063
+ .unwrap_or_else(TrustedSender::leader)
1064
+ }
1065
+
1066
+ fn warn_send_legacy_delivery_flags(args: &[String]) {
1067
+ const FLAGS: &[&str] = &[
1068
+ "--task",
1069
+ "--watch-result",
1070
+ "--requires-ack",
1071
+ "--no-ack",
1072
+ "--no-wait",
1073
+ "--timeout",
1074
+ "--confirm-human",
1075
+ "--message-id",
1076
+ ];
1077
+ let spec = command_spec("send");
1078
+ let sunset = spec
1079
+ .and_then(|spec| spec.sunset)
1080
+ .unwrap_or("next compatibility release");
1081
+ let action = spec
1082
+ .and_then(|spec| spec.action)
1083
+ .unwrap_or("use positional logical TO and the returned message id");
1084
+ for flag in FLAGS {
1085
+ if args
1086
+ .iter()
1087
+ .any(|arg| arg == flag || arg.starts_with(&format!("{flag}=")))
1088
+ {
1089
+ eprintln!("warning: {flag} is deprecated; sunset: {sunset}; action: {action}");
1090
+ }
1091
+ }
1092
+ }
1093
+
1094
+ fn validate_send_flags(args: &[String]) -> Result<(), CliError> {
1095
+ const ALLOWED: &[&str] = &[
1096
+ "--workspace",
1097
+ "--team",
1098
+ "--targets",
1099
+ "--target",
1100
+ "--to",
1101
+ "--to-name",
1102
+ "--to-leader",
1103
+ "--pane",
1104
+ "--task",
1105
+ "--watch-result",
1106
+ "--requires-ack",
1107
+ "--no-ack",
1108
+ "--no-wait",
1109
+ "--timeout",
1110
+ "--confirm-human",
1111
+ "--message-id",
1112
+ "--json",
1113
+ "-h",
1114
+ "--help",
1115
+ ];
1116
+ const ALLOWED_PREFIXES: &[&str] = &["--team=", "--pane=", "--to-name=", "--to-leader="];
1117
+ if let Some(flag) = args.iter().find(|arg| {
1118
+ arg.starts_with('-')
1119
+ && !ALLOWED.contains(&arg.as_str())
1120
+ && !ALLOWED_PREFIXES
1121
+ .iter()
1122
+ .any(|prefix| arg.starts_with(prefix))
1123
+ }) {
1124
+ return Err(CliError::Usage(format!(
1125
+ "unrecognized argument for `send`: {flag}"
1126
+ )));
1127
+ }
1128
+ Ok(())
1129
+ }
1130
+
1068
1131
  /// Stage 4 of identity-boundary unified plan (architect direction
1069
1132
  /// 2026-06-24, .team/artifacts/identity-boundary-unified-plan.md §2 Stage
1070
1133
  /// 4): destructive command ambiguity gate. When the workspace has 2+
@@ -1282,6 +1345,7 @@ fn add_agent_args(args: &[String], cwd: &Path) -> Result<AddAgentArgs, CliError>
1282
1345
  role_file: parsed
1283
1346
  .role_file
1284
1347
  .ok_or_else(|| CliError::Usage("missing --role-file".to_string()))?,
1348
+ force: parsed.force,
1285
1349
  no_display: parsed.no_display,
1286
1350
  json: parsed.json,
1287
1351
  })
@@ -1760,19 +1824,7 @@ mod tests {
1760
1824
  "quick-start",
1761
1825
  &["--workspace", "--team-id", "--yes", "--json"][..],
1762
1826
  ),
1763
- (
1764
- "send",
1765
- &[
1766
- "--workspace",
1767
- "--team",
1768
- "--targets",
1769
- "--to-name",
1770
- "--pane",
1771
- "--watch-result",
1772
- "--timeout",
1773
- "--json",
1774
- ][..],
1775
- ),
1827
+ ("send", &["--workspace", "--team", "--json"][..]),
1776
1828
  (
1777
1829
  "status",
1778
1830
  &["--workspace", "--team", "--summary", "--json", "--detail"][..],
@@ -44,7 +44,7 @@ use serde_json::{json, Map, Value};
44
44
  use thiserror::Error;
45
45
 
46
46
  // REUSE in-tree(只 import,不 redefine):
47
- use crate::messaging::{self, AlertType, MessageTarget, SendOptions};
47
+ use crate::messaging::{self, AlertType, MessageTarget, SendOptions, TrustedSender};
48
48
  use crate::model::ids::{TaskId, TeamKey};
49
49
 
50
50
  pub(crate) const COMMS_BOUNDARY_TEXT: &str = "validates live pane binding consistency and zero-token comms contracts. Does NOT perform live runtime message round-trip. (zero token, zero pollution)";
@@ -884,7 +884,13 @@ pub mod lifecycle_port {
884
884
  if session_killed && !verification_degraded {
885
885
  mark_active_team_shutdown(&mut state, team_shutdown_status);
886
886
  }
887
- crate::state::projection::save_team_scoped_state(&run_workspace, &state)?;
887
+ crate::state::repository::StateRepository::new(&run_workspace).save(
888
+ crate::state::repository::StateWriteIntent::ShutdownTeam {
889
+ team_key: team,
890
+ clean: session_killed && !verification_degraded,
891
+ },
892
+ &state,
893
+ )?;
888
894
  promote_live_sibling_after_scoped_shutdown(&run_workspace, &state)?;
889
895
  } else {
890
896
  let _changed_keys = mark_matching_session_teams_stopped(
@@ -893,7 +899,13 @@ pub mod lifecycle_port {
893
899
  session_killed && !verification_degraded,
894
900
  team_shutdown_status,
895
901
  );
896
- crate::state::persist::save_runtime_state(&run_workspace, &state)?;
902
+ crate::state::repository::StateRepository::new(&run_workspace).save(
903
+ crate::state::repository::StateWriteIntent::ShutdownTeam {
904
+ team_key: None,
905
+ clean: session_killed && !verification_degraded,
906
+ },
907
+ &state,
908
+ )?;
897
909
  }
898
910
  let coordinator_status = if coordinator_timeout {
899
911
  "timeout"
@@ -2310,14 +2322,16 @@ pub mod lifecycle_port {
2310
2322
  role_file: &str,
2311
2323
  open_display: bool,
2312
2324
  team: Option<&str>,
2325
+ force: bool,
2313
2326
  ) -> Result<Value, CliError> {
2314
2327
  let agent_id = crate::model::ids::AgentId::new(agent);
2315
- match crate::lifecycle::add_agent(
2328
+ match crate::lifecycle::add_agent_force(
2316
2329
  workspace,
2317
2330
  &agent_id,
2318
2331
  Path::new(role_file),
2319
2332
  open_display,
2320
2333
  team,
2334
+ force,
2321
2335
  ) {
2322
2336
  Ok(report) => Ok(json!({
2323
2337
  "ok": true,
@@ -2372,7 +2386,7 @@ pub mod lifecycle_port {
2372
2386
  ));
2373
2387
  }
2374
2388
  }
2375
- Err(error) if confirm => return Ok(error_value(error)),
2389
+ Err(error) if confirm && !force => return Ok(error_value(error)),
2376
2390
  Err(_) => {}
2377
2391
  }
2378
2392
  if !confirm {
@@ -2548,7 +2562,13 @@ pub mod lifecycle_port {
2548
2562
  .and_then(|agents| agents.keys().next().cloned())
2549
2563
  .map(Value::String)
2550
2564
  .unwrap_or(Value::Null);
2551
- crate::state::persist::save_runtime_state(workspace, &state)
2565
+ crate::state::repository::StateRepository::new(workspace)
2566
+ .save(
2567
+ crate::state::repository::StateWriteIntent::IdleAck {
2568
+ team_key: Some(&team),
2569
+ },
2570
+ &state,
2571
+ )
2552
2572
  .map_err(|e| CliError::Runtime(e.to_string()))?;
2553
2573
  crate::event_log::EventLog::new(workspace)
2554
2574
  .write(
@@ -3553,7 +3573,13 @@ pub mod lifecycle_port {
3553
3573
  return Ok(());
3554
3574
  };
3555
3575
  let promoted = crate::state::projection::project_top_level_view(&raw, next_key);
3556
- crate::state::persist::save_runtime_state(workspace, &promoted)?;
3576
+ crate::state::repository::StateRepository::new(workspace).save(
3577
+ crate::state::repository::StateWriteIntent::PromoteLiveSiblingAfterShutdown {
3578
+ stopped_team_key: stopped_key,
3579
+ promoted_team_key: next_key,
3580
+ },
3581
+ &promoted,
3582
+ )?;
3557
3583
  Ok(())
3558
3584
  }
3559
3585
 
@@ -3966,14 +3992,6 @@ pub mod diagnose_port {
3966
3992
  format!("{:012x}", now & 0xffffffffffff)
3967
3993
  }
3968
3994
 
3969
- fn read_runtime_state(workspace: &Path) -> Value {
3970
- let path = workspace.join(".team").join("runtime").join("state.json");
3971
- std::fs::read_to_string(path)
3972
- .ok()
3973
- .and_then(|s| serde_json::from_str(&s).ok())
3974
- .unwrap_or_else(|| json!({}))
3975
- }
3976
-
3977
3995
  fn which_path(binary: &str) -> Option<String> {
3978
3996
  let path = std::env::var_os("PATH")?;
3979
3997
  for dir in std::env::split_paths(&path) {
@@ -505,57 +505,20 @@ enum ParsedTarget {
505
505
  }
506
506
 
507
507
  fn parse_named_address(raw_name: &str) -> Result<ParsedNamedAddress, NamedAddressError> {
508
- let raw = raw_name.trim();
509
- if raw.is_empty() {
510
- return Err(name_invalid("name is empty"));
511
- }
512
- let (workspace, name) = if let Some((workspace, rest)) = raw.split_once("::") {
513
- if workspace.trim().is_empty() || rest.trim().is_empty() {
514
- return Err(name_invalid(
515
- "workspace-qualified name must include workspace and target",
516
- ));
517
- }
518
- (Some(PathBuf::from(workspace)), rest.trim())
519
- } else {
520
- (None, raw)
521
- };
522
-
523
- if name.contains("//") {
524
- return Err(name_invalid("name contains an empty path segment"));
525
- }
526
-
527
- let target = if name.contains('/') {
528
- let parts = name.split('/').collect::<Vec<_>>();
529
- if parts.len() != 2 || !valid_component(parts[0]) || !valid_component(parts[1]) {
530
- return Err(name_invalid("expected <team>/<agent> or <team>/leader"));
508
+ let parsed = crate::messaging::parse_logical_address(raw_name).map_err(name_invalid)?;
509
+ let target = match parsed.target {
510
+ crate::messaging::LogicalAddressTarget::Worker(agent) => ParsedTarget::BareAgent(agent),
511
+ crate::messaging::LogicalAddressTarget::TeamEntity { team, entity } => {
512
+ ParsedTarget::TeamEntity { team, entity }
531
513
  }
532
- ParsedTarget::TeamEntity {
533
- team: parts[0].to_string(),
534
- entity: parts[1].to_string(),
514
+ crate::messaging::LogicalAddressTarget::SessionWindow { session, window } => {
515
+ ParsedTarget::SessionWindow { session, window }
535
516
  }
536
- } else if name.contains(':') {
537
- let parts = name.split(':').collect::<Vec<_>>();
538
- if parts.len() != 2 || parts[0].trim().is_empty() || parts[1].trim().is_empty() {
539
- return Err(name_invalid("expected <session>:<window>"));
540
- }
541
- ParsedTarget::SessionWindow {
542
- session: parts[0].to_string(),
543
- window: parts[1].to_string(),
544
- }
545
- } else {
546
- if !valid_component(name) {
547
- return Err(name_invalid("expected a non-empty agent id"));
548
- }
549
- ParsedTarget::BareAgent(name.to_string())
550
517
  };
551
- Ok(ParsedNamedAddress { workspace, target })
552
- }
553
-
554
- fn valid_component(raw: &str) -> bool {
555
- !raw.trim().is_empty()
556
- && !raw.contains(char::is_whitespace)
557
- && !raw.contains('/')
558
- && !raw.contains(':')
518
+ Ok(ParsedNamedAddress {
519
+ workspace: parsed.workspace,
520
+ target,
521
+ })
559
522
  }
560
523
 
561
524
  fn resolve_workspace(
@@ -680,12 +643,38 @@ fn resolve_worker(
680
643
  .with_scoped_suggestions_for_agent(team_entry, team, agent, parsed));
681
644
  }
682
645
  };
683
- let session = string_field(team_entry, "session_name")
684
- .ok_or_else(|| name_not_resolvable("team is missing session_name"))?;
646
+ let Some(session) = string_field(team_entry, "session_name") else {
647
+ return Ok(resolved_worker_without_live(
648
+ sender_workspace,
649
+ target_workspace,
650
+ team,
651
+ agent,
652
+ parsed,
653
+ agent_entry,
654
+ None,
655
+ agent,
656
+ transport.tmux_endpoint(),
657
+ ));
658
+ };
685
659
  let window = string_field(agent_entry, "window")
686
660
  .or_else(|| string_field(agent_entry, "window_name"))
687
661
  .unwrap_or(agent);
688
- let targets = list_targets(transport)?;
662
+ let targets = match list_targets(transport) {
663
+ Ok(targets) => targets,
664
+ Err(_) => {
665
+ return Ok(resolved_worker_without_live(
666
+ sender_workspace,
667
+ target_workspace,
668
+ team,
669
+ agent,
670
+ parsed,
671
+ agent_entry,
672
+ Some(session),
673
+ window,
674
+ transport.tmux_endpoint(),
675
+ ));
676
+ }
677
+ };
689
678
  let matches = matching_session_window(&targets, session, window);
690
679
  match matches.len() {
691
680
  1 => {
@@ -720,10 +709,14 @@ fn resolve_worker(
720
709
  warning,
721
710
  })
722
711
  }
723
- 0 => Err(name_not_live_worker(
712
+ 0 => Ok(resolved_worker_without_live(
713
+ sender_workspace,
714
+ target_workspace,
724
715
  team,
725
716
  agent,
726
- session,
717
+ parsed,
718
+ agent_entry,
719
+ Some(session),
727
720
  window,
728
721
  transport.tmux_endpoint(),
729
722
  )),
@@ -747,6 +740,42 @@ fn resolve_worker(
747
740
  }
748
741
  }
749
742
 
743
+ #[allow(clippy::too_many_arguments)]
744
+ fn resolved_worker_without_live(
745
+ sender_workspace: &Path,
746
+ target_workspace: &Path,
747
+ team: &str,
748
+ agent: &str,
749
+ parsed: &ParsedNamedAddress,
750
+ agent_entry: &Value,
751
+ session: Option<&str>,
752
+ window: &str,
753
+ tmux_endpoint: Option<String>,
754
+ ) -> ResolvedNamedAddress {
755
+ let state_pane_id = string_field(agent_entry, "pane_id").map(str::to_string);
756
+ ResolvedNamedAddress {
757
+ raw_name: parsed.display_name(),
758
+ target_kind: NamedTargetKind::Worker,
759
+ sender_workspace: sender_workspace.to_path_buf(),
760
+ target_workspace: target_workspace.to_path_buf(),
761
+ team_key: Some(team.to_string()),
762
+ agent_id: Some(agent.to_string()),
763
+ pane_id: state_pane_id.clone().unwrap_or_default(),
764
+ session_name: session.map(str::to_string),
765
+ window_name: Some(window.to_string()),
766
+ tmux_endpoint,
767
+ transport_kind: Some("direct_tmux".to_string()),
768
+ app_server: None,
769
+ state_pane_id,
770
+ state_pane_stale: true,
771
+ agent_status: string_field(agent_entry, "status").map(str::to_string),
772
+ warning: Some(
773
+ "agent has no live pane; message will be persisted for standard delivery recovery"
774
+ .to_string(),
775
+ ),
776
+ }
777
+ }
778
+
750
779
  fn resolve_bare_agent(
751
780
  sender_workspace: &Path,
752
781
  target_workspace: &Path,