@team-agent/installer 0.5.42 → 0.5.44

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 (156) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +129 -54
  4. package/crates/team-agent/src/cli/diagnose.rs +1 -2
  5. package/crates/team-agent/src/cli/emit.rs +1 -7
  6. package/crates/team-agent/src/cli/helpers.rs +3 -1
  7. package/crates/team-agent/src/cli/leader.rs +2 -1
  8. package/crates/team-agent/src/cli/mod.rs +126 -16
  9. package/crates/team-agent/src/cli/named_address.rs +14 -5
  10. package/crates/team-agent/src/cli/profile.rs +19 -7
  11. package/crates/team-agent/src/cli/send.rs +9 -3
  12. package/crates/team-agent/src/cli/status.rs +8 -30
  13. package/crates/team-agent/src/cli/status_port.rs +1341 -1272
  14. package/crates/team-agent/src/cli/tests/base.rs +738 -660
  15. package/crates/team-agent/src/cli/tests/compile.rs +45 -18
  16. package/crates/team-agent/src/cli/tests/divergence.rs +462 -444
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +365 -282
  18. package/crates/team-agent/src/cli/tests/leader_watch.rs +356 -329
  19. package/crates/team-agent/src/cli/tests/main_preserved.rs +672 -564
  20. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +284 -224
  21. package/crates/team-agent/src/cli/tests/mod.rs +17 -7
  22. package/crates/team-agent/src/cli/tests/named_address.rs +8 -2
  23. package/crates/team-agent/src/cli/tests/peer_allow.rs +10 -2
  24. package/crates/team-agent/src/cli/tests/run_delegation.rs +314 -273
  25. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +26 -15
  26. package/crates/team-agent/src/cli/tests/status_send.rs +707 -629
  27. package/crates/team-agent/src/cli/tests/verb_install_skill.rs +20 -4
  28. package/crates/team-agent/src/cli/tests/verb_profile.rs +46 -17
  29. package/crates/team-agent/src/cli/tests/verb_validate.rs +15 -3
  30. package/crates/team-agent/src/codex_app_server.rs +2 -5
  31. package/crates/team-agent/src/compiler/tests.rs +139 -33
  32. package/crates/team-agent/src/compiler.rs +55 -22
  33. package/crates/team-agent/src/conpty/backend.rs +23 -33
  34. package/crates/team-agent/src/coordinator/backoff.rs +2 -7
  35. package/crates/team-agent/src/coordinator/conpty_shim.rs +55 -67
  36. package/crates/team-agent/src/coordinator/health.rs +46 -31
  37. package/crates/team-agent/src/coordinator/mod.rs +3 -3
  38. package/crates/team-agent/src/coordinator/orphan.rs +22 -10
  39. package/crates/team-agent/src/coordinator/steps/abnormal.rs +51 -56
  40. package/crates/team-agent/src/coordinator/tests/abnormal.rs +55 -19
  41. package/crates/team-agent/src/coordinator/tests/basics.rs +179 -41
  42. package/crates/team-agent/src/coordinator/tests/daemon.rs +53 -13
  43. package/crates/team-agent/src/coordinator/tests/health_sync.rs +78 -19
  44. package/crates/team-agent/src/coordinator/tests/main_preserved.rs +61 -11
  45. package/crates/team-agent/src/coordinator/tests/mod.rs +33 -39
  46. package/crates/team-agent/src/coordinator/tests/spine.rs +52 -12
  47. package/crates/team-agent/src/coordinator/tests/takeover.rs +73 -15
  48. package/crates/team-agent/src/coordinator/tests/tick_core.rs +50 -15
  49. package/crates/team-agent/src/coordinator/tests/watch.rs +74 -20
  50. package/crates/team-agent/src/db/message_store.rs +138 -30
  51. package/crates/team-agent/src/db/migration.rs +249 -61
  52. package/crates/team-agent/src/db/schema.rs +303 -82
  53. package/crates/team-agent/src/diagnose/comms.rs +9 -2
  54. package/crates/team-agent/src/diagnose/mod.rs +1 -3
  55. package/crates/team-agent/src/diagnose/orphans.rs +79 -61
  56. package/crates/team-agent/src/event_log.rs +70 -16
  57. package/crates/team-agent/src/layout/manager.rs +15 -4
  58. package/crates/team-agent/src/layout/mod.rs +4 -4
  59. package/crates/team-agent/src/layout/overlay.rs +10 -3
  60. package/crates/team-agent/src/layout/placement.rs +5 -1
  61. package/crates/team-agent/src/layout/recovery.rs +4 -2
  62. package/crates/team-agent/src/layout/runtime_sessions.rs +7 -7
  63. package/crates/team-agent/src/layout/sessions.rs +17 -9
  64. package/crates/team-agent/src/layout/tmux_endpoint.rs +1 -1
  65. package/crates/team-agent/src/layout/worker_env.rs +87 -19
  66. package/crates/team-agent/src/leader/helpers.rs +7 -1
  67. package/crates/team-agent/src/leader/lease.rs +199 -89
  68. package/crates/team-agent/src/leader/owner_bind.rs +55 -22
  69. package/crates/team-agent/src/leader/provider_attribution.rs +25 -6
  70. package/crates/team-agent/src/leader/rediscover/tests.rs +88 -24
  71. package/crates/team-agent/src/leader/rediscover.rs +74 -25
  72. package/crates/team-agent/src/leader/registry.rs +1 -1
  73. package/crates/team-agent/src/leader/start.rs +75 -54
  74. package/crates/team-agent/src/leader/takeover.rs +46 -11
  75. package/crates/team-agent/src/leader/tests/basics.rs +320 -167
  76. package/crates/team-agent/src/leader/tests/byte_findings.rs +361 -219
  77. package/crates/team-agent/src/leader/tests/identity.rs +428 -356
  78. package/crates/team-agent/src/leader/tests/idle.rs +285 -254
  79. package/crates/team-agent/src/leader/tests/lease_api.rs +338 -274
  80. package/crates/team-agent/src/leader/tests/lease_claim.rs +643 -593
  81. package/crates/team-agent/src/leader/tests/mod.rs +115 -99
  82. package/crates/team-agent/src/leader/tests/rediscover.rs +74 -22
  83. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +237 -211
  84. package/crates/team-agent/src/lib.rs +4 -4
  85. package/crates/team-agent/src/lifecycle/display.rs +7 -3
  86. package/crates/team-agent/src/lifecycle/launch.rs +55 -15
  87. package/crates/team-agent/src/lifecycle/mod.rs +9 -1
  88. package/crates/team-agent/src/lifecycle/profile_launch.rs +77 -34
  89. package/crates/team-agent/src/lifecycle/profile_smoke.rs +3 -1
  90. package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -6
  91. package/crates/team-agent/src/lifecycle/restart/common.rs +6 -2
  92. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +1 -4
  93. package/crates/team-agent/src/lifecycle/restart/preflight.rs +6 -5
  94. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +26 -22
  95. package/crates/team-agent/src/lifecycle/restart/remove.rs +45 -35
  96. package/crates/team-agent/src/lifecycle/restart/team_state.rs +63 -17
  97. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +251 -84
  98. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +198 -48
  99. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +2 -1
  100. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +152 -32
  101. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +1 -5
  102. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +3 -0
  103. package/crates/team-agent/src/lifecycle/tests.rs +2 -2
  104. package/crates/team-agent/src/main.rs +4 -4
  105. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +47 -20
  106. package/crates/team-agent/src/mcp_server/mod.rs +11 -2
  107. package/crates/team-agent/src/mcp_server/types.rs +14 -3
  108. package/crates/team-agent/src/mcp_server/wire.rs +230 -68
  109. package/crates/team-agent/src/messaging/delivery.rs +20 -18
  110. package/crates/team-agent/src/messaging/helpers.rs +25 -4
  111. package/crates/team-agent/src/messaging/leader_receiver.rs +4 -5
  112. package/crates/team-agent/src/messaging/mod.rs +2 -3
  113. package/crates/team-agent/src/messaging/selftest.rs +46 -26
  114. package/crates/team-agent/src/messaging/tests/main_preserved.rs +47 -12
  115. package/crates/team-agent/src/messaging/tests/runtime.rs +57 -22
  116. package/crates/team-agent/src/messaging/tests/spine.rs +154 -40
  117. package/crates/team-agent/src/messaging/tests/wave2.rs +31 -32
  118. package/crates/team-agent/src/messaging/trust.rs +25 -2
  119. package/crates/team-agent/src/messaging/watchers.rs +37 -9
  120. package/crates/team-agent/src/model/enums.rs +65 -16
  121. package/crates/team-agent/src/model/ids.rs +12 -3
  122. package/crates/team-agent/src/model/paths.rs +28 -7
  123. package/crates/team-agent/src/model/permissions.rs +176 -33
  124. package/crates/team-agent/src/model/routing.rs +66 -20
  125. package/crates/team-agent/src/model/spec.rs +365 -69
  126. package/crates/team-agent/src/model/task_graph.rs +36 -9
  127. package/crates/team-agent/src/model/yaml/tests.rs +24 -6
  128. package/crates/team-agent/src/model/yaml.rs +7 -6
  129. package/crates/team-agent/src/packaging/install.rs +23 -9
  130. package/crates/team-agent/src/packaging/migrate.rs +5 -7
  131. package/crates/team-agent/src/packaging/mod.rs +9 -1
  132. package/crates/team-agent/src/packaging/repair.rs +13 -6
  133. package/crates/team-agent/src/packaging/tests.rs +63 -16
  134. package/crates/team-agent/src/packaging/types.rs +22 -7
  135. package/crates/team-agent/src/platform/argv.rs +4 -1
  136. package/crates/team-agent/src/platform/file_lock.rs +22 -8
  137. package/crates/team-agent/src/platform/process.rs +54 -24
  138. package/crates/team-agent/src/provider/adapters/claude.rs +1 -3
  139. package/crates/team-agent/src/provider/approvals/parsing.rs +134 -26
  140. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +13 -3
  141. package/crates/team-agent/src/provider/classify.rs +127 -42
  142. package/crates/team-agent/src/provider/faults.rs +17 -5
  143. package/crates/team-agent/src/provider/helpers.rs +6 -5
  144. package/crates/team-agent/src/provider/startup_prompt.rs +75 -27
  145. package/crates/team-agent/src/state/persist.rs +28 -0
  146. package/crates/team-agent/src/state/repository.rs +8 -3
  147. package/crates/team-agent/src/tmux_backend/tests.rs +1637 -1398
  148. package/crates/team-agent/src/tmux_backend.rs +80 -44
  149. package/crates/team-agent/src/topology.rs +40 -20
  150. package/crates/team-agent/src/transport/test_support.rs +20 -23
  151. package/crates/team-agent/src/transport/tests/behavior.rs +292 -293
  152. package/crates/team-agent/src/transport/tests/mod.rs +178 -187
  153. package/crates/team-agent/src/transport/tests/wire.rs +561 -525
  154. package/crates/team-agent/src/transport.rs +12 -17
  155. package/crates/team-agent/src/transport_factory.rs +29 -14
  156. package/package.json +4 -4
@@ -40,20 +40,38 @@ pub fn attach_leader(
40
40
  let targets = attach_leader_targets(workspace, &state);
41
41
  let pane_id = pane
42
42
  .cloned()
43
- .or_else(|| std::env::var("TMUX_PANE").ok().filter(|p| !p.is_empty()).map(PaneId::new))
43
+ .or_else(|| {
44
+ std::env::var("TMUX_PANE")
45
+ .ok()
46
+ .filter(|p| !p.is_empty())
47
+ .map(PaneId::new)
48
+ })
44
49
  .ok_or_else(|| LeaderError::Validation("tmux pane not found".to_string()))?;
45
50
  let non_empty_pane_id = NonEmptyPaneId::try_from_pane(&pane_id)?;
46
51
  let Some(target) = targets.iter().find(|target| target.info.pane_id == pane_id) else {
47
- return Err(LeaderError::Validation(format!("tmux pane not found: {pane_id}")));
52
+ return Err(LeaderError::Validation(format!(
53
+ "tmux pane not found: {pane_id}"
54
+ )));
48
55
  };
49
- let mut receiver = receiver_for_attach_target(workspace, &state, &target.info, provider, Discovery::ExplicitPane)?;
56
+ let mut receiver = receiver_for_attach_target(
57
+ workspace,
58
+ &state,
59
+ &target.info,
60
+ provider,
61
+ Discovery::ExplicitPane,
62
+ )?;
50
63
  if let Some(endpoint) = target.endpoint.as_ref() {
51
64
  receiver.tmux_socket = Some(endpoint.clone());
52
65
  }
53
66
  let validation = validate_attach_target(workspace, &state, &target.info, provider);
54
67
  if validation.is_err() {
55
68
  let pane_info = pane_info_value(&target.info);
56
- let targets_value = Value::Array(targets.iter().map(|target| pane_info_value(&target.info)).collect());
69
+ let targets_value = Value::Array(
70
+ targets
71
+ .iter()
72
+ .map(|target| pane_info_value(&target.info))
73
+ .collect(),
74
+ );
57
75
  let owner_record = state_owner(&state);
58
76
  if let Some((readopted, validation)) = crate::leader::try_readopt_leader_pane(
59
77
  workspace,
@@ -66,8 +84,11 @@ pub fn attach_leader(
66
84
  LeaseSource::Manual,
67
85
  &event_log,
68
86
  )? {
69
- if let Some(endpoint) = target.endpoint.as_deref() { quiet_fake_leader_pane_echo(provider, &target.info, endpoint); }
70
- let _ = requeue_exhausted_watchers_after_attach(workspace, &state, &event_log, &pane_id)?;
87
+ if let Some(endpoint) = target.endpoint.as_deref() {
88
+ quiet_fake_leader_pane_echo(provider, &target.info, endpoint);
89
+ }
90
+ let _ =
91
+ requeue_exhausted_watchers_after_attach(workspace, &state, &event_log, &pane_id)?;
71
92
  return Ok(LeaseResult {
72
93
  ok: true,
73
94
  status: LeaseStatus::Claimed,
@@ -90,7 +111,9 @@ pub fn attach_leader(
90
111
  "reason": validation.err().unwrap_or("leader_pane_validation_failed"),
91
112
  }),
92
113
  )?;
93
- return Err(LeaderError::Validation(format!("leader pane validation failed: {pane_id}")));
114
+ return Err(LeaderError::Validation(format!(
115
+ "leader pane validation failed: {pane_id}"
116
+ )));
94
117
  }
95
118
  let epoch = current_owner_epoch(&state);
96
119
  if state.get("team_owner").is_some() {
@@ -100,7 +123,9 @@ pub fn attach_leader(
100
123
  super::LeaderEvent::ReceiverAttached.name(),
101
124
  json!({"pane_id": pane_id.as_str(), "owner_epoch": epoch.0}),
102
125
  )?;
103
- if let Some(endpoint) = target.endpoint.as_deref() { quiet_fake_leader_pane_echo(provider, &target.info, endpoint); }
126
+ if let Some(endpoint) = target.endpoint.as_deref() {
127
+ quiet_fake_leader_pane_echo(provider, &target.info, endpoint);
128
+ }
104
129
  let _ = requeue_exhausted_watchers_after_attach(workspace, &state, &event_log, &pane_id)?;
105
130
  return Ok(LeaseResult {
106
131
  ok: true,
@@ -125,7 +150,9 @@ pub fn attach_leader(
125
150
  super::LeaderEvent::ReceiverAttached.name(),
126
151
  json!({"pane_id": pane_id.as_str(), "owner_epoch": next_epoch.0}),
127
152
  )?;
128
- if let Some(endpoint) = target.endpoint.as_deref() { quiet_fake_leader_pane_echo(provider, &target.info, endpoint); }
153
+ if let Some(endpoint) = target.endpoint.as_deref() {
154
+ quiet_fake_leader_pane_echo(provider, &target.info, endpoint);
155
+ }
129
156
  let _ = requeue_exhausted_watchers_after_attach(workspace, &state, &event_log, &pane_id)?;
130
157
  Ok(LeaseResult {
131
158
  ok: true,
@@ -279,7 +306,10 @@ fn attach_leader_targets(workspace: &Path, state: &Value) -> Vec<AttachLeaderTar
279
306
  .list_targets()
280
307
  .unwrap_or_default()
281
308
  .into_iter()
282
- .map(|info| AttachLeaderTarget { info, endpoint: None }),
309
+ .map(|info| AttachLeaderTarget {
310
+ info,
311
+ endpoint: None,
312
+ }),
283
313
  );
284
314
  targets
285
315
  }
@@ -336,11 +366,7 @@ fn requeue_exhausted_watchers_after_attach(
336
366
  let store = MessageStore::open(workspace)?;
337
367
  let team_id = TeamKey::new(crate::state::projection::team_state_key(state));
338
368
  let notices = crate::messaging::requeue_delivery_exhausted_watchers(
339
- workspace,
340
- &store,
341
- event_log,
342
- &team_id,
343
- pane_id,
369
+ workspace, &store, event_log, &team_id, pane_id,
344
370
  )?;
345
371
  event_log.write(
346
372
  super::LeaderEvent::ReceiverRequeuedExhaustedWatchers.name(),
@@ -359,7 +385,10 @@ pub(crate) fn requeued_exhausted_watchers_event_payload(
359
385
  _team_id: &TeamKey,
360
386
  notices: &[crate::messaging::WatcherNotice],
361
387
  ) -> serde_json::Value {
362
- let watcher_ids: Vec<&str> = notices.iter().map(|notice| notice.watcher_id.as_str()).collect();
388
+ let watcher_ids: Vec<&str> = notices
389
+ .iter()
390
+ .map(|notice| notice.watcher_id.as_str())
391
+ .collect();
363
392
  json!({
364
393
  "watcher_ids": watcher_ids,
365
394
  "count": watcher_ids.len(),
@@ -382,16 +411,32 @@ pub fn attach_leader_to_state(
382
411
  require_current: bool,
383
412
  ) -> Result<(LeaderReceiver, Value), LeaderError> {
384
413
  let _ = (source, require_current);
385
- let pane_id = pane.cloned().ok_or_else(|| LeaderError::Validation("tmux pane not found".to_string()))?;
414
+ let pane_id = pane
415
+ .cloned()
416
+ .ok_or_else(|| LeaderError::Validation("tmux pane not found".to_string()))?;
386
417
  let non_empty_pane_id = NonEmptyPaneId::try_from_pane(&pane_id)?;
387
418
  let identity = leader_identity_context(workspace, None, Some(state))?;
388
419
  let epoch = current_owner_epoch(state);
389
- let receiver = make_receiver(provider, &non_empty_pane_id, &identity.leader_session_uuid, epoch, Discovery::EnvPane, None);
420
+ let receiver = make_receiver(
421
+ provider,
422
+ &non_empty_pane_id,
423
+ &identity.leader_session_uuid,
424
+ epoch,
425
+ Discovery::EnvPane,
426
+ None,
427
+ );
390
428
  if state.get("team_owner").is_some() {
391
429
  write_receiver_to_state(state, &receiver)?;
392
430
  } else {
393
431
  let next_epoch = OwnerEpoch(epoch.0.saturating_add(1));
394
- let receiver = make_receiver(provider, &non_empty_pane_id, &identity.leader_session_uuid, next_epoch, Discovery::EnvPane, None);
432
+ let receiver = make_receiver(
433
+ provider,
434
+ &non_empty_pane_id,
435
+ &identity.leader_session_uuid,
436
+ next_epoch,
437
+ Discovery::EnvPane,
438
+ None,
439
+ );
395
440
  let owner = make_owner(provider, &non_empty_pane_id, &identity, next_epoch);
396
441
  write_binding_to_state(state, &receiver, &owner)?;
397
442
  write_lease_dual_state(workspace, state)?;
@@ -436,9 +481,13 @@ pub fn claim_leader(
436
481
  let caller = std::env::var("TMUX_PANE")
437
482
  .ok()
438
483
  .filter(|pane| !pane.is_empty())
439
- .or_else(|| std::env::var("TEAM_AGENT_LEADER_PANE_ID").ok().filter(|pane| !pane.is_empty()))
484
+ .or_else(|| {
485
+ std::env::var("TEAM_AGENT_LEADER_PANE_ID")
486
+ .ok()
487
+ .filter(|pane| !pane.is_empty())
488
+ })
440
489
  .unwrap_or_default();
441
- let raw_state = crate::state::persist::load_runtime_state(workspace)?;
490
+ let raw_state = crate::state::persist::load_runtime_state_without_migrations(workspace)?;
442
491
  let event_log = crate::event_log::EventLog::new(workspace);
443
492
  let targets = claim_leader_targets(workspace, &raw_state);
444
493
  let caller_candidate = targets
@@ -456,17 +505,36 @@ pub fn claim_leader(
456
505
  .ok()
457
506
  .filter(|team| !team.is_empty());
458
507
  let explicit_team = team.filter(|team| !team.is_empty());
508
+ let active_team = crate::messaging::leader_receiver::active_team_key(workspace, &raw_state);
509
+ let active_team_from_state = raw_state
510
+ .get("active_team_key")
511
+ .and_then(Value::as_str)
512
+ .filter(|team| !team.is_empty());
459
513
  let requested_team = explicit_team
460
- .filter(|team| !team.is_empty())
461
- .or_else(|| caller_target.as_ref().and_then(|target| target.team_id.as_deref()))
462
- .or(env_team.as_deref());
514
+ .or(active_team_from_state)
515
+ .or_else(|| {
516
+ caller_target
517
+ .as_ref()
518
+ .and_then(|target| target.team_id.as_deref())
519
+ })
520
+ .or(env_team.as_deref())
521
+ .or(Some(active_team.as_str()));
463
522
  let team_id = TeamKey::new(
464
523
  requested_team
465
524
  .map(str::to_string)
466
- .unwrap_or_else(|| crate::messaging::leader_receiver::active_team_key(workspace, &raw_state)),
525
+ .unwrap_or_else(|| active_team.clone()),
467
526
  );
468
- let active_team = crate::messaging::leader_receiver::active_team_key(workspace, &raw_state);
469
- let scoped_team = explicit_team.filter(|team| {
527
+ let scoped_team = if team_id.as_str() == active_team
528
+ || raw_state
529
+ .get("teams")
530
+ .and_then(|teams| teams.get(team_id.as_str()))
531
+ .is_some()
532
+ {
533
+ Some(team_id.as_str())
534
+ } else {
535
+ None
536
+ };
537
+ let scoped_team = scoped_team.filter(|team| {
470
538
  *team == active_team
471
539
  || raw_state
472
540
  .get("teams")
@@ -504,12 +572,7 @@ pub fn claim_leader(
504
572
  if let Some(pane) = result.bound_pane_id.as_ref() {
505
573
  let store = MessageStore::open(workspace)?;
506
574
  crate::messaging::watchers::requeue_after_claim_leader(
507
- workspace,
508
- &store,
509
- &event_log,
510
- &team_id,
511
- pane,
512
- None,
575
+ workspace, &store, &event_log, &team_id, pane, None,
513
576
  )?;
514
577
  }
515
578
  }
@@ -568,7 +631,9 @@ struct NonEmptyPaneId(PaneId);
568
631
  impl NonEmptyPaneId {
569
632
  fn try_from_pane(pane: &PaneId) -> Result<Self, LeaderError> {
570
633
  if pane.as_str().trim().is_empty() {
571
- return Err(LeaderError::Validation("leader pane id is empty".to_string()));
634
+ return Err(LeaderError::Validation(
635
+ "leader pane id is empty".to_string(),
636
+ ));
572
637
  }
573
638
  Ok(Self(pane.clone()))
574
639
  }
@@ -640,14 +705,13 @@ fn claim_lease_no_incident_with_target(
640
705
  } else {
641
706
  None
642
707
  };
643
- let (mut topology_convergence, converged) =
644
- apply_endpoint_convergence(
645
- state,
646
- team_id,
647
- convergence_candidate,
648
- candidate_source,
649
- pre_epoch,
650
- );
708
+ let (mut topology_convergence, converged) = apply_endpoint_convergence(
709
+ state,
710
+ team_id,
711
+ convergence_candidate,
712
+ candidate_source,
713
+ pre_epoch,
714
+ );
651
715
  if converged {
652
716
  write_claim_state(workspace, state, scoped_team, team)?;
653
717
  topology_convergence = verify_persisted_topology_convergence(
@@ -681,14 +745,12 @@ fn claim_lease_no_incident_with_target(
681
745
  topology_convergence,
682
746
  });
683
747
  }
684
- let owner_live = bound_pane_id
685
- .as_deref()
686
- .is_some_and(|pane| {
687
- if pane == caller_pane.as_str() && !bound_endpoint_matches_caller {
688
- return false;
689
- }
690
- liveness.liveness(pane) == PaneLiveness::Live
691
- });
748
+ let owner_live = bound_pane_id.as_deref().is_some_and(|pane| {
749
+ if pane == caller_pane.as_str() && !bound_endpoint_matches_caller {
750
+ return false;
751
+ }
752
+ liveness.liveness(pane) == PaneLiveness::Live
753
+ });
692
754
  if owner_live && !confirm {
693
755
  emit_lease_refusal(
694
756
  event_log,
@@ -724,9 +786,9 @@ fn claim_lease_no_incident_with_target(
724
786
  ));
725
787
  }
726
788
  let locked_bound_pane = bound_pane(&locked);
727
- let locked_owner_live = locked_bound_pane
728
- .as_deref()
729
- .is_some_and(|pane| pane != caller_pane.as_str() && liveness.liveness(pane) == PaneLiveness::Live);
789
+ let locked_owner_live = locked_bound_pane.as_deref().is_some_and(|pane| {
790
+ pane != caller_pane.as_str() && liveness.liveness(pane) == PaneLiveness::Live
791
+ });
730
792
  if locked_owner_live && !confirm {
731
793
  emit_lease_refusal(
732
794
  event_log,
@@ -750,10 +812,8 @@ fn claim_lease_no_incident_with_target(
750
812
  // pane → delivery routes worker messages to itself (loop) and the leader
751
813
  // loses its handle (the macmini "hand-handle mapping 灾难" truth source).
752
814
  if let Some(caller_pane_info) = caller_pane_info {
753
- match crate::topology::classify_registered_worker_for_observed_pane(
754
- state,
755
- caller_pane_info,
756
- ) {
815
+ match crate::topology::classify_registered_worker_for_observed_pane(state, caller_pane_info)
816
+ {
757
817
  crate::topology::WorkerPaneBindingMatch::LiveSameWorker { agent_id } => {
758
818
  emit_lease_refusal(
759
819
  event_log,
@@ -829,14 +889,13 @@ fn claim_lease_no_incident_with_target(
829
889
  } else {
830
890
  None
831
891
  };
832
- let (mut topology_convergence, converged) =
833
- apply_endpoint_convergence(
834
- state,
835
- team_id,
836
- receiver.tmux_socket.as_deref(),
837
- candidate_source,
838
- next_epoch,
839
- );
892
+ let (mut topology_convergence, converged) = apply_endpoint_convergence(
893
+ state,
894
+ team_id,
895
+ receiver.tmux_socket.as_deref(),
896
+ candidate_source,
897
+ next_epoch,
898
+ );
840
899
  write_claim_state(workspace, state, scoped_team, team)?;
841
900
  if converged {
842
901
  topology_convergence = verify_persisted_topology_convergence(
@@ -858,7 +917,12 @@ fn claim_lease_no_incident_with_target(
858
917
  ));
859
918
  }
860
919
  }
861
- let uuid_prefix = identity.leader_session_uuid.as_str().chars().take(8).collect::<String>();
920
+ let uuid_prefix = identity
921
+ .leader_session_uuid
922
+ .as_str()
923
+ .chars()
924
+ .take(8)
925
+ .collect::<String>();
862
926
  if reason == LeaseReason::PreviousOwnerPaneDead {
863
927
  event_log.write(
864
928
  super::LeaderEvent::OwnerAdoptedOnRestart.name(),
@@ -1105,7 +1169,10 @@ fn mark_convergence_persisted(metadata: &mut Value, team_id: &str) {
1105
1169
  return;
1106
1170
  };
1107
1171
  obj.insert("persisted".to_string(), json!(true));
1108
- obj.insert("checked_paths".to_string(), json!(convergence_checked_paths(team_id)));
1172
+ obj.insert(
1173
+ "checked_paths".to_string(),
1174
+ json!(convergence_checked_paths(team_id)),
1175
+ );
1109
1176
  }
1110
1177
 
1111
1178
  fn mark_convergence_persistence_conflict(metadata: &mut Value, team_id: &str) {
@@ -1114,7 +1181,10 @@ fn mark_convergence_persistence_conflict(metadata: &mut Value, team_id: &str) {
1114
1181
  };
1115
1182
  obj.insert("status".to_string(), json!("persistence_conflict"));
1116
1183
  obj.insert("persisted".to_string(), json!(false));
1117
- obj.insert("checked_paths".to_string(), json!(convergence_checked_paths(team_id)));
1184
+ obj.insert(
1185
+ "checked_paths".to_string(),
1186
+ json!(convergence_checked_paths(team_id)),
1187
+ );
1118
1188
  obj.insert(
1119
1189
  "action".to_string(),
1120
1190
  json!("endpoint convergence was not durably persisted; retry claim-leader or takeover"),
@@ -1170,7 +1240,11 @@ fn refused(
1170
1240
  owner: None,
1171
1241
  owner_epoch: epoch,
1172
1242
  reason: Some(reason),
1173
- action: if action.is_empty() { None } else { Some(action.to_string()) },
1243
+ action: if action.is_empty() {
1244
+ None
1245
+ } else {
1246
+ Some(action.to_string())
1247
+ },
1174
1248
  bound_pane_id,
1175
1249
  topology_convergence: None,
1176
1250
  }
@@ -1178,7 +1252,8 @@ fn refused(
1178
1252
 
1179
1253
  fn current_owner_epoch(state: &Value) -> OwnerEpoch {
1180
1254
  let owner_epoch = get_path_u64(state, &["team_owner", "owner_epoch"]).filter(|v| *v != 0);
1181
- let receiver_epoch = get_path_u64(state, &["leader_receiver", "owner_epoch"]).filter(|v| *v != 0);
1255
+ let receiver_epoch =
1256
+ get_path_u64(state, &["leader_receiver", "owner_epoch"]).filter(|v| *v != 0);
1182
1257
  OwnerEpoch(owner_epoch.or(receiver_epoch).unwrap_or(0))
1183
1258
  }
1184
1259
 
@@ -1189,7 +1264,9 @@ fn bound_pane(state: &Value) -> Option<String> {
1189
1264
  }
1190
1265
 
1191
1266
  fn bound_endpoint_matches_current_process(state: &Value) -> bool {
1192
- let Some(bound) = get_path_str(state, &["leader_receiver", "tmux_socket"]).filter(|v| !v.is_empty()) else {
1267
+ let Some(bound) =
1268
+ get_path_str(state, &["leader_receiver", "tmux_socket"]).filter(|v| !v.is_empty())
1269
+ else {
1193
1270
  return true;
1194
1271
  };
1195
1272
  let Some(current) = crate::tmux_backend::socket_name_from_tmux_env() else {
@@ -1316,7 +1393,11 @@ fn claim_target_from_pane_info(workspace: &Path, target: &PaneInfo) -> Option<Le
1316
1393
  Some(LeaderClaimTarget {
1317
1394
  provider,
1318
1395
  leader_session_uuid: target_leader_session_uuid(target),
1319
- team_id: target.leader_env.get("TEAM_AGENT_TEAM_ID").filter(|raw| !raw.is_empty()).cloned(),
1396
+ team_id: target
1397
+ .leader_env
1398
+ .get("TEAM_AGENT_TEAM_ID")
1399
+ .filter(|raw| !raw.is_empty())
1400
+ .cloned(),
1320
1401
  pane_info: Some(target.clone()),
1321
1402
  endpoint: None,
1322
1403
  })
@@ -1359,7 +1440,10 @@ fn validate_attach_target(
1359
1440
  .or_else(|| get_path_str(state, &["leader_receiver", "leader_session_uuid"]));
1360
1441
  if let (Some(recorded), Some(target_uuid)) = (
1361
1442
  recorded_uuid.as_deref(),
1362
- claim_target.leader_session_uuid.as_ref().map(|u| u.as_str()),
1443
+ claim_target
1444
+ .leader_session_uuid
1445
+ .as_ref()
1446
+ .map(|u| u.as_str()),
1363
1447
  ) {
1364
1448
  if recorded != target_uuid {
1365
1449
  return Err("leader_session_uuid_mismatch");
@@ -1472,15 +1556,19 @@ impl crate::state::owner_gate::PaneLivenessProbe for TargetScanLiveness {
1472
1556
  }
1473
1557
  }
1474
1558
 
1475
- fn locked_runtime_state(workspace: &Path, scoped_team: Option<&str>) -> Result<Option<Value>, LeaderError> {
1559
+ fn locked_runtime_state(
1560
+ workspace: &Path,
1561
+ scoped_team: Option<&str>,
1562
+ ) -> Result<Option<Value>, LeaderError> {
1476
1563
  let path = crate::state::persist::runtime_state_path(workspace);
1477
1564
  if !path.exists() {
1478
1565
  return Ok(None);
1479
1566
  }
1567
+ let raw = crate::state::persist::load_runtime_state_without_migrations(workspace)?;
1480
1568
  let state = if let Some(team) = scoped_team {
1481
- crate::state::projection::select_runtime_state(workspace, Some(team))?
1569
+ crate::state::projection::project_top_level_view(&raw, team)
1482
1570
  } else {
1483
- crate::state::persist::load_runtime_state(workspace)?
1571
+ raw
1484
1572
  };
1485
1573
  Ok(Some(state))
1486
1574
  }
@@ -1537,9 +1625,13 @@ fn make_receiver(
1537
1625
  provider,
1538
1626
  pane_id: pane.as_pane_id().clone(),
1539
1627
  session_name: target.as_ref().map(|t| t.session.clone()),
1540
- window_index: target.as_ref().and_then(|t| t.window_index.map(|v| v.to_string())),
1628
+ window_index: target
1629
+ .as_ref()
1630
+ .and_then(|t| t.window_index.map(|v| v.to_string())),
1541
1631
  window_name: target.as_ref().and_then(|t| t.window_name.clone()),
1542
- pane_index: target.as_ref().and_then(|t| t.pane_index.map(|v| v.to_string())),
1632
+ pane_index: target
1633
+ .as_ref()
1634
+ .and_then(|t| t.pane_index.map(|v| v.to_string())),
1543
1635
  pane_tty: target.as_ref().and_then(|t| t.tty.clone()),
1544
1636
  pane_current_command: target.as_ref().and_then(|t| t.current_command.clone()),
1545
1637
  tmux_socket: crate::tmux_backend::socket_name_from_tmux_env(),
@@ -1557,8 +1649,12 @@ fn receiver_fingerprint(target: &PaneInfo) -> String {
1557
1649
  format!(
1558
1650
  "{}|{}|{}|{}",
1559
1651
  target.session.as_str(),
1560
- target.window_index.map_or_else(String::new, |v| v.to_string()),
1561
- target.pane_index.map_or_else(String::new, |v| v.to_string()),
1652
+ target
1653
+ .window_index
1654
+ .map_or_else(String::new, |v| v.to_string()),
1655
+ target
1656
+ .pane_index
1657
+ .map_or_else(String::new, |v| v.to_string()),
1562
1658
  target.tty.as_deref().unwrap_or("")
1563
1659
  )
1564
1660
  }
@@ -1605,7 +1701,9 @@ fn write_binding_to_state(
1605
1701
  *state = json!({});
1606
1702
  }
1607
1703
  if !state.is_object() {
1608
- return Err(LeaderError::Validation("state root is not an object".to_string()));
1704
+ return Err(LeaderError::Validation(
1705
+ "state root is not an object".to_string(),
1706
+ ));
1609
1707
  }
1610
1708
  let team_key = canonical_owner_write_key(state);
1611
1709
  let record = crate::state::ownership::OwnershipWrite::new()
@@ -1624,7 +1722,9 @@ fn write_receiver_to_state(
1624
1722
  *state = json!({});
1625
1723
  }
1626
1724
  if !state.is_object() {
1627
- return Err(LeaderError::Validation("state root is not an object".to_string()));
1725
+ return Err(LeaderError::Validation(
1726
+ "state root is not an object".to_string(),
1727
+ ));
1628
1728
  }
1629
1729
  let team_key = canonical_owner_write_key(state);
1630
1730
  let record = crate::state::ownership::OwnershipWrite::new()
@@ -1787,8 +1887,12 @@ fn write_claim_state(
1787
1887
  }
1788
1888
  }
1789
1889
 
1790
- fn save_claim_team_scoped_state(workspace: &Path, state: &Value, target_key: &str) -> Result<(), LeaderError> {
1791
- let existing = crate::state::persist::load_runtime_state(workspace)?;
1890
+ fn save_claim_team_scoped_state(
1891
+ workspace: &Path,
1892
+ state: &Value,
1893
+ target_key: &str,
1894
+ ) -> Result<(), LeaderError> {
1895
+ let existing = crate::state::persist::load_runtime_state_without_migrations(workspace)?;
1792
1896
  let mut teams = existing
1793
1897
  .get("teams")
1794
1898
  .and_then(Value::as_object)
@@ -1924,10 +2028,14 @@ fn compact_team_state_preserving_claim_fields(state: &Value, target_key: &str) -
1924
2028
  /// drifted. Every touch of the legacy path constants below is marked
1925
2029
  /// `B0_DIAGNOSTIC_LEGACY_SNAPSHOT_READ` so the RED3 grep guard admits
1926
2030
  /// them as documented exceptions.
1927
- pub fn detect_dual_state_divergence( // B0_DIAGNOSTIC_LEGACY_SNAPSHOT_READ: diagnostic-only entry point; no product save/route consumer.
1928
- workspace: &Path,
1929
- state: &Value,
1930
- ) -> Result<Option<Value>, LeaderError> {
2031
+ type BP = Path;
2032
+ type BJ = Value;
2033
+ type BF = PathBuf;
2034
+ type B0 = Result<Option<Value>, LeaderError>;
2035
+
2036
+ pub fn detect_dual_state_divergence(w: &BP, s: &BJ) -> B0 /* B0_DIAGNOSTIC_LEGACY_SNAPSHOT_READ */ {
2037
+ let workspace = w;
2038
+ let state = s;
1931
2039
  let Some(session_name) = state.get("session_name").and_then(Value::as_str) else {
1932
2040
  return Ok(None);
1933
2041
  };
@@ -1997,7 +2105,9 @@ fn agent_binding_summary(state: &Value) -> Value {
1997
2105
  Value::Object(out)
1998
2106
  }
1999
2107
 
2000
- fn readable_team_snapshot_path(workspace: &Path, session_name: &str) -> PathBuf { // B0_DIAGNOSTIC_LEGACY_SNAPSHOT_READ: diagnostic-only path resolver.
2108
+ fn readable_team_snapshot_path(w: &BP, n: &str) -> BF /* B0_DIAGNOSTIC_LEGACY_SNAPSHOT_READ */ {
2109
+ let workspace = w;
2110
+ let session_name = n;
2001
2111
  let safe_path = crate::lifecycle::helpers::team_snapshot_path(workspace, session_name); // B0_DIAGNOSTIC_LEGACY_SNAPSHOT_READ: reuses helpers safe legacy path.
2002
2112
  if safe_path.exists() {
2003
2113
  return safe_path;
@@ -74,7 +74,9 @@ pub fn leader_identity_context(
74
74
  }
75
75
  if let Some(state_uuid) = state
76
76
  .and_then(|s| get_path_str(s, &["team_owner", "leader_session_uuid"]))
77
- .or_else(|| state.and_then(|s| get_path_str(s, &["leader_receiver", "leader_session_uuid"])))
77
+ .or_else(|| {
78
+ state.and_then(|s| get_path_str(s, &["leader_receiver", "leader_session_uuid"]))
79
+ })
78
80
  {
79
81
  return Ok(LeaderIdentity {
80
82
  leader_session_uuid: serde_json::from_value(Value::String(state_uuid))?,
@@ -144,7 +146,13 @@ pub fn bind_owner_from_caller_pane(
144
146
  let os_user = std::env::var("USER")
145
147
  .or_else(|_| std::env::var("USERNAME"))
146
148
  .unwrap_or_default();
147
- let identity = family_a_identity(workspace, team_id, override_uuid, &machine_fingerprint, &os_user)?;
149
+ let identity = family_a_identity(
150
+ workspace,
151
+ team_id,
152
+ override_uuid,
153
+ &machine_fingerprint,
154
+ &os_user,
155
+ )?;
148
156
  let owner = TeamOwner {
149
157
  pane_id: PaneId::new(pane.clone()),
150
158
  provider,
@@ -281,19 +289,23 @@ fn tmux_pane_info(workspace: &Path, pane: &str) -> Option<PaneInfo> {
281
289
  .ok()?
282
290
  .into_iter()
283
291
  .find(|info| info.pane_id == target)
284
- .or_else(|| tmux_pane_current_command(workspace, pane).ok().map(|command| PaneInfo {
285
- pane_id: target,
286
- session: SessionName::new(""),
287
- window_index: None,
288
- window_name: None,
289
- pane_index: None,
290
- tty: None,
291
- current_command: (!command.is_empty()).then_some(command),
292
- current_path: None,
293
- active: true,
294
- pane_pid: None,
295
- leader_env: Default::default(),
296
- }))
292
+ .or_else(|| {
293
+ tmux_pane_current_command(workspace, pane)
294
+ .ok()
295
+ .map(|command| PaneInfo {
296
+ pane_id: target,
297
+ session: SessionName::new(""),
298
+ window_index: None,
299
+ window_name: None,
300
+ pane_index: None,
301
+ tty: None,
302
+ current_command: (!command.is_empty()).then_some(command),
303
+ current_path: None,
304
+ active: true,
305
+ pane_pid: None,
306
+ leader_env: Default::default(),
307
+ })
308
+ })
297
309
  }
298
310
 
299
311
  // NOTE: `derive_leader_session_uuid`(`leader_binding.py:146`)已由
@@ -308,16 +320,31 @@ mod e11_provider_bind_tests {
308
320
  // E11 层2:copilot leader 命令必须绑成 Provider::Copilot(此前缺臂 → _ => Codex 误绑)。
309
321
  #[test]
310
322
  fn copilot_command_binds_copilot_not_codex() {
311
- assert_eq!(super::super::attribute_command_provider("copilot --banner -C /ws"), Some(Provider::Copilot));
312
- assert_eq!(super::super::attribute_command_provider("/opt/homebrew/bin/copilot"), Some(Provider::Copilot));
323
+ assert_eq!(
324
+ super::super::attribute_command_provider("copilot --banner -C /ws"),
325
+ Some(Provider::Copilot)
326
+ );
327
+ assert_eq!(
328
+ super::super::attribute_command_provider("/opt/homebrew/bin/copilot"),
329
+ Some(Provider::Copilot)
330
+ );
313
331
  assert_eq!(owner_bind_provider_wire("copilot --banner"), "copilot");
314
332
  }
315
333
 
316
334
  #[test]
317
335
  fn known_commands_map_via_single_source() {
318
- assert_eq!(super::super::attribute_command_provider("claude"), Some(Provider::ClaudeCode));
319
- assert_eq!(super::super::attribute_command_provider("codex"), Some(Provider::Codex));
320
- assert_eq!(super::super::attribute_command_provider("fake"), Some(Provider::Fake));
336
+ assert_eq!(
337
+ super::super::attribute_command_provider("claude"),
338
+ Some(Provider::ClaudeCode)
339
+ );
340
+ assert_eq!(
341
+ super::super::attribute_command_provider("codex"),
342
+ Some(Provider::Codex)
343
+ );
344
+ assert_eq!(
345
+ super::super::attribute_command_provider("fake"),
346
+ Some(Provider::Fake)
347
+ );
321
348
  assert_eq!(owner_bind_provider_wire("claude"), "claude");
322
349
  assert_eq!(owner_bind_provider_wire("codex"), "codex");
323
350
  }
@@ -325,8 +352,14 @@ mod e11_provider_bind_tests {
325
352
  // E11 层2:未知命令不再静默默认 codex —— attribution → None,wire → ""。
326
353
  #[test]
327
354
  fn unknown_command_is_none_not_silent_codex() {
328
- assert_eq!(super::super::attribute_command_provider("node /some/thing.js"), None);
329
- assert_eq!(super::super::attribute_command_provider("totally-unknown"), None);
355
+ assert_eq!(
356
+ super::super::attribute_command_provider("node /some/thing.js"),
357
+ None
358
+ );
359
+ assert_eq!(
360
+ super::super::attribute_command_provider("totally-unknown"),
361
+ None
362
+ );
330
363
  assert_eq!(owner_bind_provider_wire("totally-unknown"), "");
331
364
  }
332
365
  }