@team-agent/installer 0.5.74 → 0.5.77
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/README.md +9 -0
- package/README.zh.md +8 -0
- package/crates/team-agent/src/cli/adapters.rs +23 -96
- package/crates/team-agent/src/cli/emit.rs +35 -7
- package/crates/team-agent/src/cli/mod.rs +63 -42
- package/crates/team-agent/src/cli/models.rs +138 -27
- package/crates/team-agent/src/cli/status_port/brief.rs +1137 -0
- package/crates/team-agent/src/cli/status_port.rs +8 -0
- package/crates/team-agent/src/cli/tests/mod.rs +1 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +29 -24
- package/crates/team-agent/src/cli/tests/status_brief.rs +691 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +8 -2
- package/crates/team-agent/src/layout/worker_env.rs +366 -0
- package/crates/team-agent/src/leader/lease.rs +274 -40
- package/crates/team-agent/src/leader/registry.rs +177 -19
- package/crates/team-agent/src/leader/types.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +93 -5
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1820 -60
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +138 -13
- package/crates/team-agent/src/lifecycle/restart/common.rs +3 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +11 -4
- package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +45 -14
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +1 -0
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +24 -28
- package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +22 -10
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +110 -31
- package/crates/team-agent/src/lifecycle/types.rs +4 -0
- package/crates/team-agent/src/messaging/delivery.rs +200 -29
- package/crates/team-agent/src/messaging/leader_channel.rs +12 -3
- package/crates/team-agent/src/messaging/watchers.rs +48 -4
- package/crates/team-agent/src/state/paths.rs +17 -2
- package/crates/team-agent/src/state/persist.rs +166 -6
- package/crates/team-agent/src/state/projection.rs +18 -2
- package/crates/team-agent/src/state/repository.rs +36 -0
- package/crates/team-agent/src/state/selector.rs +23 -3
- package/crates/team-agent/src/test_support/models_timeout_fixture.rs +122 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +350 -7
- package/crates/team-agent/src/tmux_backend.rs +217 -26
- package/crates/team-agent/src/transport/test_support.rs +18 -3
- package/crates/team-agent/src/transport.rs +99 -0
- package/package.json +4 -4
|
@@ -26,6 +26,46 @@ use super::{
|
|
|
26
26
|
|
|
27
27
|
// ── leader::lease — attach / claim / takeover / autobind / readopt 统一 CAS 路径 ──
|
|
28
28
|
|
|
29
|
+
#[derive(Debug, Clone)]
|
|
30
|
+
pub(crate) struct AppliedLeaderGrant {
|
|
31
|
+
pub(crate) owner: Value,
|
|
32
|
+
pub(crate) receiver: Value,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#[derive(Debug, Default)]
|
|
36
|
+
struct AttachRecovery {
|
|
37
|
+
blocked_counts: crate::message_store::BlockedLeaderRequeueCounts,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#[derive(Debug)]
|
|
41
|
+
pub(crate) struct AttachLeaderError {
|
|
42
|
+
source: LeaderError,
|
|
43
|
+
applied_grant: Option<AppliedLeaderGrant>,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
impl AttachLeaderError {
|
|
47
|
+
fn with_applied(source: LeaderError, applied_grant: Option<AppliedLeaderGrant>) -> Self {
|
|
48
|
+
Self {
|
|
49
|
+
source,
|
|
50
|
+
applied_grant,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
pub(crate) fn into_parts(self) -> (LeaderError, Option<AppliedLeaderGrant>) {
|
|
55
|
+
(self.source, self.applied_grant)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fn into_error(self) -> LeaderError {
|
|
59
|
+
self.source
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
impl From<LeaderError> for AttachLeaderError {
|
|
64
|
+
fn from(source: LeaderError) -> Self {
|
|
65
|
+
Self::with_applied(source, None)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
29
69
|
/// `attach_leader`(card §42;`__init__.py:19`)。手动 CLI attach;持 `LEADER_OWNERSHIP_LOCK`
|
|
30
70
|
/// 整段临界区做 state 变更 + 事件 + 双写 + requeue exhausted watchers。
|
|
31
71
|
pub fn attach_leader(
|
|
@@ -91,8 +131,11 @@ pub fn attach_leader(
|
|
|
91
131
|
if let Some(endpoint) = target.endpoint.as_deref() {
|
|
92
132
|
quiet_fake_leader_pane_echo(provider, &target.info, endpoint);
|
|
93
133
|
}
|
|
94
|
-
let
|
|
134
|
+
let recovery =
|
|
95
135
|
requeue_exhausted_watchers_after_attach(workspace, &state, &event_log, &pane_id)?;
|
|
136
|
+
let team_id = TeamKey::new(crate::state::projection::team_state_key(&state));
|
|
137
|
+
let attach_window_failures =
|
|
138
|
+
attach_window_failure_value(recovery.blocked_counts, &team_id, &pane_id);
|
|
96
139
|
return Ok(LeaseResult {
|
|
97
140
|
ok: true,
|
|
98
141
|
status: LeaseStatus::Claimed,
|
|
@@ -106,6 +149,7 @@ pub fn attach_leader(
|
|
|
106
149
|
.map(str::to_string),
|
|
107
150
|
bound_pane_id: Some(pane_id),
|
|
108
151
|
topology_convergence: None,
|
|
152
|
+
attach_window_failures,
|
|
109
153
|
});
|
|
110
154
|
}
|
|
111
155
|
event_log.write(
|
|
@@ -130,7 +174,10 @@ pub fn attach_leader(
|
|
|
130
174
|
if let Some(endpoint) = target.endpoint.as_deref() {
|
|
131
175
|
quiet_fake_leader_pane_echo(provider, &target.info, endpoint);
|
|
132
176
|
}
|
|
133
|
-
let
|
|
177
|
+
let recovery = requeue_exhausted_watchers_after_attach(workspace, &state, &event_log, &pane_id)?;
|
|
178
|
+
let team_id = TeamKey::new(crate::state::projection::team_state_key(&state));
|
|
179
|
+
let attach_window_failures =
|
|
180
|
+
attach_window_failure_value(recovery.blocked_counts, &team_id, &pane_id);
|
|
134
181
|
return Ok(LeaseResult {
|
|
135
182
|
ok: true,
|
|
136
183
|
status: LeaseStatus::AlreadyBound,
|
|
@@ -141,6 +188,7 @@ pub fn attach_leader(
|
|
|
141
188
|
action: None,
|
|
142
189
|
bound_pane_id: Some(pane_id),
|
|
143
190
|
topology_convergence: None,
|
|
191
|
+
attach_window_failures,
|
|
144
192
|
});
|
|
145
193
|
}
|
|
146
194
|
let identity = leader_identity_context(workspace, None, Some(&state))?;
|
|
@@ -157,7 +205,10 @@ pub fn attach_leader(
|
|
|
157
205
|
if let Some(endpoint) = target.endpoint.as_deref() {
|
|
158
206
|
quiet_fake_leader_pane_echo(provider, &target.info, endpoint);
|
|
159
207
|
}
|
|
160
|
-
let
|
|
208
|
+
let recovery = requeue_exhausted_watchers_after_attach(workspace, &state, &event_log, &pane_id)?;
|
|
209
|
+
let team_id = TeamKey::new(crate::state::projection::team_state_key(&state));
|
|
210
|
+
let attach_window_failures =
|
|
211
|
+
attach_window_failure_value(recovery.blocked_counts, &team_id, &pane_id);
|
|
161
212
|
Ok(LeaseResult {
|
|
162
213
|
ok: true,
|
|
163
214
|
status: LeaseStatus::Claimed,
|
|
@@ -168,6 +219,7 @@ pub fn attach_leader(
|
|
|
168
219
|
action: None,
|
|
169
220
|
bound_pane_id: Some(pane_id),
|
|
170
221
|
topology_convergence: None,
|
|
222
|
+
attach_window_failures,
|
|
171
223
|
})
|
|
172
224
|
}
|
|
173
225
|
|
|
@@ -406,17 +458,34 @@ fn requeue_exhausted_watchers_after_attach(
|
|
|
406
458
|
state: &Value,
|
|
407
459
|
event_log: &crate::event_log::EventLog,
|
|
408
460
|
pane_id: &PaneId,
|
|
409
|
-
) -> Result<
|
|
461
|
+
) -> Result<AttachRecovery, LeaderError> {
|
|
410
462
|
let store = MessageStore::open(workspace)?;
|
|
411
463
|
let team_id = TeamKey::new(crate::state::projection::team_state_key(state));
|
|
412
|
-
let notices =
|
|
413
|
-
|
|
414
|
-
|
|
464
|
+
let (notices, blocked_counts) =
|
|
465
|
+
crate::messaging::watchers::requeue_delivery_exhausted_watchers_with_counts(
|
|
466
|
+
workspace, &store, event_log, &team_id, pane_id,
|
|
467
|
+
)?;
|
|
415
468
|
event_log.write(
|
|
416
469
|
super::LeaderEvent::ReceiverRequeuedExhaustedWatchers.name(),
|
|
417
470
|
requeued_exhausted_watchers_event_payload(pane_id, &team_id, ¬ices),
|
|
418
471
|
)?;
|
|
419
|
-
Ok(
|
|
472
|
+
Ok(AttachRecovery { blocked_counts })
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
fn attach_window_failure_value(
|
|
476
|
+
counts: crate::message_store::BlockedLeaderRequeueCounts,
|
|
477
|
+
team_id: &TeamKey,
|
|
478
|
+
pane_id: &PaneId,
|
|
479
|
+
) -> Option<Value> {
|
|
480
|
+
(counts.blocked_leader_unbound > 0).then(|| {
|
|
481
|
+
json!({
|
|
482
|
+
"team_id": team_id.as_str(),
|
|
483
|
+
"pane_id": pane_id.as_str(),
|
|
484
|
+
"reason": "leader_not_attached",
|
|
485
|
+
"count": counts.blocked_leader_unbound,
|
|
486
|
+
"status": "requeued_pending_physical_retry",
|
|
487
|
+
})
|
|
488
|
+
})
|
|
420
489
|
}
|
|
421
490
|
|
|
422
491
|
/// R8 D4 (decoupled for offline byte-lock — c-lite): build the `leader_receiver.requeued_exhausted_watchers`
|
|
@@ -454,63 +523,140 @@ pub(crate) fn attach_leader_to_state(
|
|
|
454
523
|
source: LeaseSource,
|
|
455
524
|
require_current: bool,
|
|
456
525
|
) -> Result<(LeaderReceiver, Value), LeaderError> {
|
|
457
|
-
|
|
526
|
+
attach_leader_to_state_with_target_and_controls(
|
|
527
|
+
workspace,
|
|
528
|
+
state,
|
|
529
|
+
pane,
|
|
530
|
+
provider,
|
|
531
|
+
event_log,
|
|
532
|
+
source,
|
|
533
|
+
require_current,
|
|
534
|
+
None,
|
|
535
|
+
None,
|
|
536
|
+
None,
|
|
537
|
+
None,
|
|
538
|
+
)
|
|
539
|
+
.map_err(AttachLeaderError::into_error)
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
#[allow(clippy::too_many_arguments)]
|
|
543
|
+
pub(crate) fn attach_leader_to_state_with_target(
|
|
544
|
+
workspace: &Path,
|
|
545
|
+
state: &mut Value,
|
|
546
|
+
pane: Option<&PaneId>,
|
|
547
|
+
provider: Provider,
|
|
548
|
+
event_log: &crate::event_log::EventLog,
|
|
549
|
+
source: LeaseSource,
|
|
550
|
+
require_current: bool,
|
|
551
|
+
caller_target: Option<&PaneInfo>,
|
|
552
|
+
) -> Result<(LeaderReceiver, Value), LeaderError> {
|
|
553
|
+
attach_leader_to_state_with_target_and_controls(
|
|
554
|
+
workspace,
|
|
555
|
+
state,
|
|
556
|
+
pane,
|
|
557
|
+
provider,
|
|
558
|
+
event_log,
|
|
559
|
+
source,
|
|
560
|
+
require_current,
|
|
561
|
+
caller_target,
|
|
562
|
+
None,
|
|
563
|
+
None,
|
|
564
|
+
None,
|
|
565
|
+
)
|
|
566
|
+
.map_err(AttachLeaderError::into_error)
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
#[allow(clippy::too_many_arguments)]
|
|
570
|
+
pub(crate) fn attach_leader_to_state_with_target_and_controls(
|
|
571
|
+
workspace: &Path,
|
|
572
|
+
state: &mut Value,
|
|
573
|
+
pane: Option<&PaneId>,
|
|
574
|
+
provider: Provider,
|
|
575
|
+
event_log: &crate::event_log::EventLog,
|
|
576
|
+
source: LeaseSource,
|
|
577
|
+
require_current: bool,
|
|
578
|
+
caller_target: Option<&PaneInfo>,
|
|
579
|
+
expected_owner: Option<&Value>,
|
|
580
|
+
expected_receiver: Option<&Value>,
|
|
581
|
+
nonce_writer: Option<&dyn Fn(&str, &PaneId, &str) -> Result<String, LeaderError>>,
|
|
582
|
+
) -> Result<(LeaderReceiver, Value), AttachLeaderError> {
|
|
583
|
+
if expected_owner.is_some() != expected_receiver.is_some() {
|
|
584
|
+
return Err(AttachLeaderError::from(LeaderError::Validation(
|
|
585
|
+
"fresh caller CAS requires both expected owner and receiver".to_string(),
|
|
586
|
+
)));
|
|
587
|
+
}
|
|
458
588
|
let pane_id = pane
|
|
459
589
|
.cloned()
|
|
460
590
|
.ok_or_else(|| LeaderError::Validation("tmux pane not found".to_string()))?;
|
|
461
591
|
let non_empty_pane_id = NonEmptyPaneId::try_from_pane(&pane_id)?;
|
|
462
592
|
let identity = leader_identity_context(workspace, None, Some(state))?;
|
|
463
593
|
let epoch = current_owner_epoch(state);
|
|
464
|
-
let
|
|
594
|
+
let has_owner = state.get("team_owner").is_some();
|
|
595
|
+
let binding_epoch = if has_owner {
|
|
596
|
+
epoch
|
|
597
|
+
} else {
|
|
598
|
+
OwnerEpoch(epoch.0.saturating_add(1))
|
|
599
|
+
};
|
|
600
|
+
let mut receiver = make_receiver(
|
|
465
601
|
provider,
|
|
466
602
|
&non_empty_pane_id,
|
|
467
603
|
&identity.leader_session_uuid,
|
|
468
|
-
|
|
604
|
+
binding_epoch,
|
|
469
605
|
Discovery::EnvPane,
|
|
470
606
|
None,
|
|
471
607
|
);
|
|
472
|
-
|
|
608
|
+
authorize_fresh_caller_receiver(
|
|
609
|
+
workspace,
|
|
610
|
+
&mut receiver,
|
|
611
|
+
&non_empty_pane_id,
|
|
612
|
+
source,
|
|
613
|
+
require_current,
|
|
614
|
+
caller_target,
|
|
615
|
+
nonce_writer,
|
|
616
|
+
)?;
|
|
617
|
+
let receiver_value = serde_json::to_value(&receiver).map_err(LeaderError::from)?;
|
|
618
|
+
if has_owner {
|
|
473
619
|
write_receiver_to_state(state, &receiver)?;
|
|
474
620
|
} else {
|
|
475
|
-
let
|
|
476
|
-
let receiver = make_receiver(
|
|
477
|
-
provider,
|
|
478
|
-
&non_empty_pane_id,
|
|
479
|
-
&identity.leader_session_uuid,
|
|
480
|
-
next_epoch,
|
|
481
|
-
Discovery::EnvPane,
|
|
482
|
-
None,
|
|
483
|
-
);
|
|
484
|
-
let owner = make_owner(provider, &non_empty_pane_id, &identity, next_epoch);
|
|
621
|
+
let owner = make_owner(provider, &non_empty_pane_id, &identity, binding_epoch);
|
|
485
622
|
write_binding_to_state(state, &receiver, &owner)?;
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
json!({"pane_id": pane_id.as_str(), "owner_epoch": next_epoch.0}),
|
|
490
|
-
)?;
|
|
491
|
-
let team_id = TeamKey::new(crate::state::projection::team_state_key(state));
|
|
492
|
-
crate::messaging::watchers::recover_watchers_for_incident(
|
|
623
|
+
}
|
|
624
|
+
if let (Some(expected_owner), Some(expected_receiver)) = (expected_owner, expected_receiver) {
|
|
625
|
+
write_lease_dual_state_with_expected(
|
|
493
626
|
workspace,
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
LeaderIncident::LeaderAttached,
|
|
627
|
+
state,
|
|
628
|
+
expected_owner,
|
|
629
|
+
expected_receiver,
|
|
498
630
|
)?;
|
|
499
|
-
|
|
631
|
+
} else {
|
|
632
|
+
write_lease_dual_state(workspace, state)?;
|
|
500
633
|
}
|
|
501
|
-
|
|
502
|
-
|
|
634
|
+
let applied_grant = state
|
|
635
|
+
.get("team_owner")
|
|
636
|
+
.cloned()
|
|
637
|
+
.map(|owner| AppliedLeaderGrant {
|
|
638
|
+
owner,
|
|
639
|
+
receiver: receiver_value,
|
|
640
|
+
});
|
|
641
|
+
if let Err(error) = event_log.write(
|
|
503
642
|
super::LeaderEvent::ReceiverAttached.name(),
|
|
504
|
-
json!({"pane_id": pane_id.as_str(), "owner_epoch":
|
|
505
|
-
)
|
|
643
|
+
json!({"pane_id": pane_id.as_str(), "owner_epoch": binding_epoch.0}),
|
|
644
|
+
) {
|
|
645
|
+
return Err(AttachLeaderError::with_applied(
|
|
646
|
+
error.into(),
|
|
647
|
+
applied_grant.clone(),
|
|
648
|
+
));
|
|
649
|
+
}
|
|
506
650
|
let team_id = TeamKey::new(crate::state::projection::team_state_key(state));
|
|
507
|
-
crate::messaging::watchers::recover_watchers_for_incident(
|
|
651
|
+
if let Err(error) = crate::messaging::watchers::recover_watchers_for_incident(
|
|
508
652
|
workspace,
|
|
509
653
|
event_log,
|
|
510
654
|
&team_id,
|
|
511
655
|
&pane_id,
|
|
512
656
|
LeaderIncident::LeaderAttached,
|
|
513
|
-
)
|
|
657
|
+
) {
|
|
658
|
+
return Err(AttachLeaderError::with_applied(error.into(), applied_grant));
|
|
659
|
+
}
|
|
514
660
|
Ok((receiver, json!({"ok": true})))
|
|
515
661
|
}
|
|
516
662
|
|
|
@@ -837,6 +983,7 @@ fn claim_lease_no_incident_with_target(
|
|
|
837
983
|
action: None,
|
|
838
984
|
bound_pane_id: Some(caller_pane.clone()),
|
|
839
985
|
topology_convergence,
|
|
986
|
+
attach_window_failures: None,
|
|
840
987
|
});
|
|
841
988
|
}
|
|
842
989
|
let owner_live = bound_pane_id.as_deref().is_some_and(|pane| {
|
|
@@ -1082,6 +1229,7 @@ fn claim_lease_no_incident_with_target(
|
|
|
1082
1229
|
action: None,
|
|
1083
1230
|
bound_pane_id: Some(caller_pane.clone()),
|
|
1084
1231
|
topology_convergence,
|
|
1232
|
+
attach_window_failures: None,
|
|
1085
1233
|
})
|
|
1086
1234
|
}
|
|
1087
1235
|
|
|
@@ -1340,6 +1488,7 @@ fn convergence_persistence_refusal(
|
|
|
1340
1488
|
),
|
|
1341
1489
|
bound_pane_id,
|
|
1342
1490
|
topology_convergence,
|
|
1491
|
+
attach_window_failures: None,
|
|
1343
1492
|
}
|
|
1344
1493
|
}
|
|
1345
1494
|
|
|
@@ -1363,6 +1512,7 @@ fn refused(
|
|
|
1363
1512
|
},
|
|
1364
1513
|
bound_pane_id,
|
|
1365
1514
|
topology_convergence: None,
|
|
1515
|
+
attach_window_failures: None,
|
|
1366
1516
|
}
|
|
1367
1517
|
}
|
|
1368
1518
|
|
|
@@ -1870,6 +2020,72 @@ fn emit_lease_refusal(
|
|
|
1870
2020
|
Ok(())
|
|
1871
2021
|
}
|
|
1872
2022
|
|
|
2023
|
+
fn authorize_fresh_caller_receiver(
|
|
2024
|
+
workspace: &Path,
|
|
2025
|
+
receiver: &mut LeaderReceiver,
|
|
2026
|
+
pane: &NonEmptyPaneId,
|
|
2027
|
+
source: LeaseSource,
|
|
2028
|
+
require_current: bool,
|
|
2029
|
+
caller_target: Option<&PaneInfo>,
|
|
2030
|
+
nonce_writer: Option<&dyn Fn(&str, &PaneId, &str) -> Result<String, LeaderError>>,
|
|
2031
|
+
) -> Result<(), LeaderError> {
|
|
2032
|
+
if !matches!(source, LeaseSource::QuickStart) || !require_current {
|
|
2033
|
+
return Ok(());
|
|
2034
|
+
}
|
|
2035
|
+
let Some(target) = caller_target else {
|
|
2036
|
+
return Ok(());
|
|
2037
|
+
};
|
|
2038
|
+
if target.pane_id.as_str() != pane.as_pane_id().as_str() || !target.active {
|
|
2039
|
+
return Err(LeaderError::Validation(
|
|
2040
|
+
"fresh caller pane observation does not match the bound pane".to_string(),
|
|
2041
|
+
));
|
|
2042
|
+
}
|
|
2043
|
+
let Some(current_path) = target.current_path.as_deref() else {
|
|
2044
|
+
return Ok(());
|
|
2045
|
+
};
|
|
2046
|
+
if crate::messaging::leader_channel::path_is_in_workspace(current_path, workspace) {
|
|
2047
|
+
return Ok(());
|
|
2048
|
+
}
|
|
2049
|
+
let Some(endpoint) = receiver
|
|
2050
|
+
.tmux_socket
|
|
2051
|
+
.as_deref()
|
|
2052
|
+
.filter(|endpoint| !endpoint.is_empty())
|
|
2053
|
+
else {
|
|
2054
|
+
return Err(LeaderError::Validation(
|
|
2055
|
+
"fresh cross-workspace caller requires a tmux endpoint".to_string(),
|
|
2056
|
+
));
|
|
2057
|
+
};
|
|
2058
|
+
if !Path::new(endpoint).is_absolute() {
|
|
2059
|
+
return Err(LeaderError::Validation(
|
|
2060
|
+
"fresh cross-workspace caller requires an absolute tmux endpoint".to_string(),
|
|
2061
|
+
));
|
|
2062
|
+
}
|
|
2063
|
+
let pane_id = pane.as_pane_id();
|
|
2064
|
+
let live_nonce = if let Some(live_nonce) = target
|
|
2065
|
+
.leader_env
|
|
2066
|
+
.get(crate::tmux_backend::PANE_BINDING_NONCE_METADATA_KEY)
|
|
2067
|
+
.filter(|nonce| !nonce.is_empty())
|
|
2068
|
+
.cloned()
|
|
2069
|
+
{
|
|
2070
|
+
live_nonce
|
|
2071
|
+
} else {
|
|
2072
|
+
let nonce = pane_binding_nonce_for_claim(workspace, endpoint, pane_id, None);
|
|
2073
|
+
if let Some(writer) = nonce_writer {
|
|
2074
|
+
writer(endpoint, pane_id, &nonce)?
|
|
2075
|
+
} else {
|
|
2076
|
+
tmux_backend_for_endpoint(endpoint)
|
|
2077
|
+
.set_pane_binding_nonce_if_unset(pane_id, &nonce)
|
|
2078
|
+
.map_err(|error| LeaderError::Tmux(error.to_string()))?
|
|
2079
|
+
}
|
|
2080
|
+
};
|
|
2081
|
+
receiver.scope_authority = Some(
|
|
2082
|
+
crate::messaging::leader_channel::FRESH_CALLER_SCOPE_AUTHORITY.to_string(),
|
|
2083
|
+
);
|
|
2084
|
+
receiver.authorized_team_workspace = Some(canonical_workspace(workspace));
|
|
2085
|
+
receiver.binding_nonce = Some(live_nonce);
|
|
2086
|
+
Ok(())
|
|
2087
|
+
}
|
|
2088
|
+
|
|
1873
2089
|
fn make_receiver(
|
|
1874
2090
|
provider: Provider,
|
|
1875
2091
|
pane: &NonEmptyPaneId,
|
|
@@ -2237,6 +2453,24 @@ fn state_owner(state: &Value) -> Option<TeamOwner> {
|
|
|
2237
2453
|
/// now persists ONLY the canonical root state; retaining the public
|
|
2238
2454
|
/// name for 0.5.x call-site stability. The B0 legacy snapshot is
|
|
2239
2455
|
/// diagnostic-only via `lifecycle::save_team_runtime_snapshot`.
|
|
2456
|
+
pub(crate) fn write_lease_dual_state_with_expected(
|
|
2457
|
+
workspace: &Path,
|
|
2458
|
+
state: &Value,
|
|
2459
|
+
expected_owner: &Value,
|
|
2460
|
+
expected_receiver: &Value,
|
|
2461
|
+
) -> Result<(), LeaderError> {
|
|
2462
|
+
let team_key = canonical_owner_write_key(state);
|
|
2463
|
+
crate::state::repository::StateRepository::new(workspace).save(
|
|
2464
|
+
crate::state::repository::StateWriteIntent::ClaimLeaderFreshCaller {
|
|
2465
|
+
team_key: team_key.as_str(),
|
|
2466
|
+
expected_owner,
|
|
2467
|
+
expected_receiver,
|
|
2468
|
+
},
|
|
2469
|
+
state,
|
|
2470
|
+
)?;
|
|
2471
|
+
Ok(())
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2240
2474
|
pub fn write_lease_dual_state(workspace: &Path, state: &Value) -> Result<(), LeaderError> {
|
|
2241
2475
|
// 0.5.42 S1b (s1b-writer-cluster-locate.md §4.4): terminal root
|
|
2242
2476
|
// save routes through `StateRepository` with the `ClaimLeader`
|