@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
|
@@ -33,7 +33,7 @@ use crate::event_log::EventLog;
|
|
|
33
33
|
use crate::model::paths::runtime_dir;
|
|
34
34
|
use crate::state::identity::{migrate_state_identity, SystemEnv};
|
|
35
35
|
use crate::state::json_truthy;
|
|
36
|
-
use crate::state::projection::team_state_key;
|
|
36
|
+
use crate::state::projection::{team_state_key, CURRENT_TEAM_ALIAS};
|
|
37
37
|
use crate::state::StateError;
|
|
38
38
|
|
|
39
39
|
/// `state.py:26-29`:agent session-state 归一字段(setdefault None)。
|
|
@@ -205,7 +205,24 @@ impl Drop for RuntimeLock {
|
|
|
205
205
|
/// `save_runtime_state`(bug-084)。`state` 是 state.json 的内存 Value(插入序保留)。
|
|
206
206
|
/// 注:Python 在此还调 `_migrate_state_identity`(identity slice 落地后接入;本 slice 不改 state 内容)。
|
|
207
207
|
pub fn save_runtime_state(workspace: &Path, state: &Value) -> Result<(), StateError> {
|
|
208
|
-
save_runtime_state_with_merge_options(workspace, state, &[], None, &[], &[])
|
|
208
|
+
save_runtime_state_with_merge_options(workspace, state, &[], None, &[], None, &[], None)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
pub(crate) fn save_runtime_state_with_receiver_authority(
|
|
212
|
+
workspace: &Path,
|
|
213
|
+
state: &Value,
|
|
214
|
+
receiver_team_key: &str,
|
|
215
|
+
) -> Result<(), StateError> {
|
|
216
|
+
save_runtime_state_with_merge_options(
|
|
217
|
+
workspace,
|
|
218
|
+
state,
|
|
219
|
+
&[],
|
|
220
|
+
None,
|
|
221
|
+
&[],
|
|
222
|
+
None,
|
|
223
|
+
&[],
|
|
224
|
+
Some(receiver_team_key),
|
|
225
|
+
)
|
|
209
226
|
}
|
|
210
227
|
|
|
211
228
|
pub(crate) fn save_runtime_state_reapplying_after_conflict<F>(
|
|
@@ -230,9 +247,19 @@ where
|
|
|
230
247
|
pub(crate) fn save_runtime_state_with_lifecycle_topology_authority(
|
|
231
248
|
workspace: &Path,
|
|
232
249
|
state: &Value,
|
|
250
|
+
topology_team_key: &str,
|
|
233
251
|
agent_ids: &[&str],
|
|
234
252
|
) -> Result<(), StateError> {
|
|
235
|
-
save_runtime_state_with_merge_options(
|
|
253
|
+
save_runtime_state_with_merge_options(
|
|
254
|
+
workspace,
|
|
255
|
+
state,
|
|
256
|
+
&[],
|
|
257
|
+
None,
|
|
258
|
+
&[],
|
|
259
|
+
Some(topology_team_key),
|
|
260
|
+
agent_ids,
|
|
261
|
+
None,
|
|
262
|
+
)
|
|
236
263
|
}
|
|
237
264
|
|
|
238
265
|
pub(crate) fn save_runtime_state_with_lifecycle_topology_authority_and_capture_backfill_skip(
|
|
@@ -248,7 +275,9 @@ pub(crate) fn save_runtime_state_with_lifecycle_topology_authority_and_capture_b
|
|
|
248
275
|
&[],
|
|
249
276
|
Some(skip_capture_backfill_team_key),
|
|
250
277
|
skip_capture_backfill_agent_ids,
|
|
278
|
+
Some(skip_capture_backfill_team_key),
|
|
251
279
|
topology_agent_ids,
|
|
280
|
+
None,
|
|
252
281
|
)
|
|
253
282
|
}
|
|
254
283
|
|
|
@@ -257,7 +286,16 @@ pub(crate) fn save_runtime_state_with_deleted_agents(
|
|
|
257
286
|
state: &Value,
|
|
258
287
|
deleted_agent_ids: &[&str],
|
|
259
288
|
) -> Result<(), StateError> {
|
|
260
|
-
save_runtime_state_with_merge_options(
|
|
289
|
+
save_runtime_state_with_merge_options(
|
|
290
|
+
workspace,
|
|
291
|
+
state,
|
|
292
|
+
deleted_agent_ids,
|
|
293
|
+
None,
|
|
294
|
+
&[],
|
|
295
|
+
None,
|
|
296
|
+
&[],
|
|
297
|
+
None,
|
|
298
|
+
)
|
|
261
299
|
}
|
|
262
300
|
|
|
263
301
|
pub(crate) fn save_runtime_state_with_team_tombstoned_agents(
|
|
@@ -272,7 +310,9 @@ pub(crate) fn save_runtime_state_with_team_tombstoned_agents(
|
|
|
272
310
|
&[],
|
|
273
311
|
Some(tombstoned_team_key),
|
|
274
312
|
tombstoned_agent_ids,
|
|
313
|
+
None,
|
|
275
314
|
&[],
|
|
315
|
+
None,
|
|
276
316
|
)
|
|
277
317
|
}
|
|
278
318
|
|
|
@@ -288,7 +328,9 @@ pub(crate) fn save_runtime_state_with_team_tombstone_lifecycle_topology_authorit
|
|
|
288
328
|
&[],
|
|
289
329
|
Some(tombstoned_team_key),
|
|
290
330
|
agent_ids,
|
|
331
|
+
Some(tombstoned_team_key),
|
|
291
332
|
agent_ids,
|
|
333
|
+
None,
|
|
292
334
|
)
|
|
293
335
|
}
|
|
294
336
|
|
|
@@ -298,7 +340,9 @@ fn save_runtime_state_with_merge_options(
|
|
|
298
340
|
deleted_agent_ids: &[&str],
|
|
299
341
|
skip_capture_backfill_team_key: Option<&str>,
|
|
300
342
|
skip_capture_backfill_agent_ids: &[&str],
|
|
343
|
+
topology_update_team_key: Option<&str>,
|
|
301
344
|
topology_update_agent_ids: &[&str],
|
|
345
|
+
receiver_update_team_key: Option<&str>,
|
|
302
346
|
) -> Result<(), StateError> {
|
|
303
347
|
let path = runtime_state_path(workspace);
|
|
304
348
|
// Python `state.py:497`:先对入参 state 跑 `_migrate_state_identity`(就地填缺失 leader uuid)。
|
|
@@ -362,10 +406,19 @@ fn save_runtime_state_with_merge_options(
|
|
|
362
406
|
.filter(|id| !id.is_empty())
|
|
363
407
|
.map(str::to_string)
|
|
364
408
|
.collect::<BTreeSet<_>>();
|
|
365
|
-
let topology_updates =
|
|
366
|
-
.
|
|
367
|
-
.
|
|
368
|
-
|
|
409
|
+
let topology_updates = topology_update_team_key
|
|
410
|
+
.into_iter()
|
|
411
|
+
.flat_map(|team_key| {
|
|
412
|
+
topology_update_agent_ids
|
|
413
|
+
.iter()
|
|
414
|
+
.copied()
|
|
415
|
+
.filter(|id| !id.is_empty())
|
|
416
|
+
.map(move |agent_id| (team_key.to_string(), agent_id.to_string()))
|
|
417
|
+
})
|
|
418
|
+
.collect::<BTreeSet<_>>();
|
|
419
|
+
let receiver_updates = receiver_update_team_key
|
|
420
|
+
.into_iter()
|
|
421
|
+
.filter(|team_key| !team_key.is_empty())
|
|
369
422
|
.map(str::to_string)
|
|
370
423
|
.collect::<BTreeSet<_>>();
|
|
371
424
|
apply_persist_merge_contract(
|
|
@@ -375,6 +428,7 @@ fn save_runtime_state_with_merge_options(
|
|
|
375
428
|
skip_capture_backfill_team_key,
|
|
376
429
|
&skip_capture_backfill,
|
|
377
430
|
&topology_updates,
|
|
431
|
+
&receiver_updates,
|
|
378
432
|
)?;
|
|
379
433
|
}
|
|
380
434
|
// Stage 3 save-output strip second pass (defence-in-depth): after the
|
|
@@ -440,7 +494,8 @@ fn apply_persist_merge_contract(
|
|
|
440
494
|
deleted_agent_ids: &BTreeSet<String>,
|
|
441
495
|
skip_capture_backfill_team_key: Option<&str>,
|
|
442
496
|
skip_capture_backfill_agent_ids: &BTreeSet<String>,
|
|
443
|
-
|
|
497
|
+
topology_updates: &BTreeSet<(String, String)>,
|
|
498
|
+
receiver_updates: &BTreeSet<String>,
|
|
444
499
|
) -> Result<(), StateError> {
|
|
445
500
|
// A0/R1: the projection gate only guards the TOP-LEVEL passes (top-level agents and
|
|
446
501
|
// the top-level<->active-team cross projections depend on which team is active); the
|
|
@@ -448,10 +503,19 @@ fn apply_persist_merge_contract(
|
|
|
448
503
|
// even when another process flipped session_name/active_team_key between this
|
|
449
504
|
// writer's load and save.
|
|
450
505
|
let projection_matches = same_runtime_projection(incoming, latest);
|
|
451
|
-
let top_level_team =
|
|
506
|
+
let top_level_team = incoming
|
|
507
|
+
.get("active_team_key")
|
|
508
|
+
.and_then(Value::as_str)
|
|
509
|
+
.filter(|key| !key.is_empty())
|
|
510
|
+
.map(str::to_string)
|
|
511
|
+
.or_else(|| Some(team_state_key(incoming)))
|
|
512
|
+
.or_else(|| Some(team_state_key(latest)));
|
|
452
513
|
let skip_top_level_capture_backfill =
|
|
453
514
|
should_skip_capture_backfill(top_level_team.as_deref(), skip_capture_backfill_team_key);
|
|
454
515
|
if projection_matches {
|
|
516
|
+
let topology_update_agent_ids =
|
|
517
|
+
topology_updates_for_team(topology_updates, top_level_team.as_deref());
|
|
518
|
+
preserve_latest_retired_agents(incoming, latest, &topology_update_agent_ids)?;
|
|
455
519
|
// 0.5.26 (§7.3): top-level projection uses the incoming root state's
|
|
456
520
|
// implicit "alive" shape (top-level agents are always the active team).
|
|
457
521
|
// Passing `None` for team lifecycle keeps historical behavior when the
|
|
@@ -463,7 +527,7 @@ fn apply_persist_merge_contract(
|
|
|
463
527
|
deleted_agent_ids,
|
|
464
528
|
skip_top_level_capture_backfill,
|
|
465
529
|
skip_capture_backfill_agent_ids,
|
|
466
|
-
topology_update_agent_ids,
|
|
530
|
+
&topology_update_agent_ids,
|
|
467
531
|
None,
|
|
468
532
|
)?;
|
|
469
533
|
// Stage 3c (identity-boundary unified plan, architect direction
|
|
@@ -500,6 +564,21 @@ fn apply_persist_merge_contract(
|
|
|
500
564
|
let Some(incoming_entry) = incoming_teams.get_mut(team) else {
|
|
501
565
|
continue;
|
|
502
566
|
};
|
|
567
|
+
// Intentional semantic split with
|
|
568
|
+
// `projection::resolve_runtime_team_scope`: that selector checks
|
|
569
|
+
// alive/terminal state, but this lock-held merge cannot depend on
|
|
570
|
+
// the mutable alive view. A change to either CURRENT_TEAM_ALIAS
|
|
571
|
+
// mapping must evaluate the other. The legacy physical alias gets
|
|
572
|
+
// active-team authority; every real sibling remains self-scoped.
|
|
573
|
+
let authority_team = if team == CURRENT_TEAM_ALIAS {
|
|
574
|
+
top_level_team.as_deref()
|
|
575
|
+
} else {
|
|
576
|
+
Some(team.as_str())
|
|
577
|
+
};
|
|
578
|
+
let topology_update_agent_ids =
|
|
579
|
+
topology_updates_for_team(topology_updates, authority_team);
|
|
580
|
+
let receiver_update_authoritative =
|
|
581
|
+
authority_team.is_some_and(|team_key| receiver_updates.contains(team_key));
|
|
503
582
|
let projection = format!("teams.{team}.agents");
|
|
504
583
|
// 0.5.26 (`.team/artifacts/stale-team-saveconflict-locate.md` §7.3):
|
|
505
584
|
// decide the containing team's aliveness from BOTH sides — a shutdown
|
|
@@ -514,6 +593,11 @@ fn apply_persist_merge_contract(
|
|
|
514
593
|
.map(|_| crate::state::projection::team_is_alive_candidate(incoming_entry))
|
|
515
594
|
.unwrap_or(true),
|
|
516
595
|
);
|
|
596
|
+
preserve_latest_retired_agents(
|
|
597
|
+
incoming_entry,
|
|
598
|
+
latest_entry,
|
|
599
|
+
&topology_update_agent_ids,
|
|
600
|
+
)?;
|
|
517
601
|
merge_agent_projection(
|
|
518
602
|
&projection,
|
|
519
603
|
incoming_entry.get_mut("agents"),
|
|
@@ -521,16 +605,79 @@ fn apply_persist_merge_contract(
|
|
|
521
605
|
deleted_agent_ids,
|
|
522
606
|
should_skip_capture_backfill(Some(team), skip_capture_backfill_team_key),
|
|
523
607
|
skip_capture_backfill_agent_ids,
|
|
524
|
-
topology_update_agent_ids,
|
|
608
|
+
&topology_update_agent_ids,
|
|
525
609
|
team_alive,
|
|
526
610
|
)?;
|
|
527
|
-
preserve_latest_ownership_fields(
|
|
611
|
+
preserve_latest_ownership_fields(
|
|
612
|
+
incoming_entry,
|
|
613
|
+
latest_entry,
|
|
614
|
+
receiver_update_authoritative,
|
|
615
|
+
);
|
|
528
616
|
preserve_latest_endpoint_convergence_fields(incoming_entry, latest_entry);
|
|
529
617
|
}
|
|
530
618
|
}
|
|
531
619
|
Ok(())
|
|
532
620
|
}
|
|
533
621
|
|
|
622
|
+
fn topology_updates_for_team(
|
|
623
|
+
topology_updates: &BTreeSet<(String, String)>,
|
|
624
|
+
team_key: Option<&str>,
|
|
625
|
+
) -> BTreeSet<String> {
|
|
626
|
+
topology_updates
|
|
627
|
+
.iter()
|
|
628
|
+
.filter(|(authority_team, _)| Some(authority_team.as_str()) == team_key)
|
|
629
|
+
.map(|(_, agent_id)| agent_id.clone())
|
|
630
|
+
.collect()
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
fn preserve_latest_retired_agents(
|
|
634
|
+
incoming: &mut Value,
|
|
635
|
+
latest: &Value,
|
|
636
|
+
topology_update_agent_ids: &BTreeSet<String>,
|
|
637
|
+
) -> Result<(), StateError> {
|
|
638
|
+
// A persisted retirement is lifecycle authority, so an ordinary stale
|
|
639
|
+
// writer (notably a coordinator tick captured before remove-agent) cannot
|
|
640
|
+
// resurrect its old roster row. Explicit lifecycle topology writers are
|
|
641
|
+
// the only override: AddAgent uses that existing authority to consume the
|
|
642
|
+
// tombstone, while remove itself supplies the newer tombstone as incoming.
|
|
643
|
+
let retired = latest
|
|
644
|
+
.get("agent_lifecycle")
|
|
645
|
+
.and_then(Value::as_object)
|
|
646
|
+
.into_iter()
|
|
647
|
+
.flat_map(|entries| entries.iter())
|
|
648
|
+
.filter(|(agent_id, entry)| {
|
|
649
|
+
!topology_update_agent_ids.contains(*agent_id)
|
|
650
|
+
&& entry.get("state").and_then(Value::as_str) == Some("retired")
|
|
651
|
+
})
|
|
652
|
+
.map(|(agent_id, entry)| (agent_id.clone(), entry.clone()))
|
|
653
|
+
.collect::<Vec<_>>();
|
|
654
|
+
if retired.is_empty() {
|
|
655
|
+
return Ok(());
|
|
656
|
+
}
|
|
657
|
+
let Some(incoming) = incoming.as_object_mut() else {
|
|
658
|
+
return Err(StateError::SaveFailed(
|
|
659
|
+
"incoming runtime projection is not an object".to_string(),
|
|
660
|
+
));
|
|
661
|
+
};
|
|
662
|
+
if let Some(agents) = incoming.get_mut("agents").and_then(Value::as_object_mut) {
|
|
663
|
+
for (agent_id, _) in &retired {
|
|
664
|
+
agents.remove(agent_id);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
let lifecycle = incoming
|
|
668
|
+
.entry("agent_lifecycle".to_string())
|
|
669
|
+
.or_insert_with(|| serde_json::json!({}));
|
|
670
|
+
let Some(lifecycle) = lifecycle.as_object_mut() else {
|
|
671
|
+
return Err(StateError::SaveFailed(
|
|
672
|
+
"incoming agent_lifecycle is not an object".to_string(),
|
|
673
|
+
));
|
|
674
|
+
};
|
|
675
|
+
for (agent_id, entry) in retired {
|
|
676
|
+
lifecycle.insert(agent_id, entry);
|
|
677
|
+
}
|
|
678
|
+
Ok(())
|
|
679
|
+
}
|
|
680
|
+
|
|
534
681
|
/// 0.5.x Windows portability Batch 7 F6: copy `latest.transport.shim`
|
|
535
682
|
/// into `incoming` if the incoming save omitted it. The coordinator's
|
|
536
683
|
/// `conpty_shim::finalize` writes `state.transport.shim = {pid,
|
|
@@ -578,8 +725,12 @@ fn should_skip_capture_backfill(
|
|
|
578
725
|
}
|
|
579
726
|
}
|
|
580
727
|
|
|
581
|
-
fn preserve_latest_ownership_fields(
|
|
582
|
-
|
|
728
|
+
fn preserve_latest_ownership_fields(
|
|
729
|
+
incoming: &mut Value,
|
|
730
|
+
latest: &Value,
|
|
731
|
+
receiver_update_authoritative: bool,
|
|
732
|
+
) {
|
|
733
|
+
if !latest_has_preferable_ownership(incoming, latest, receiver_update_authoritative) {
|
|
583
734
|
return;
|
|
584
735
|
}
|
|
585
736
|
let Some(incoming_obj) = incoming.as_object_mut() else {
|
|
@@ -643,13 +794,19 @@ fn endpoint_convergence_epoch(state: &Value) -> Option<u64> {
|
|
|
643
794
|
.and_then(Value::as_u64)
|
|
644
795
|
}
|
|
645
796
|
|
|
646
|
-
fn latest_has_preferable_ownership(
|
|
797
|
+
fn latest_has_preferable_ownership(
|
|
798
|
+
incoming: &Value,
|
|
799
|
+
latest: &Value,
|
|
800
|
+
receiver_update_authoritative: bool,
|
|
801
|
+
) -> bool {
|
|
647
802
|
let latest_epoch = ownership_epoch(latest);
|
|
648
803
|
let incoming_epoch = ownership_epoch(incoming);
|
|
649
804
|
if latest_epoch > incoming_epoch {
|
|
650
805
|
return true;
|
|
651
806
|
}
|
|
652
|
-
latest_epoch == incoming_epoch
|
|
807
|
+
latest_epoch == incoming_epoch
|
|
808
|
+
&& !receiver_update_authoritative
|
|
809
|
+
&& (ownership_attached(latest) || !ownership_attached(incoming))
|
|
653
810
|
}
|
|
654
811
|
|
|
655
812
|
fn ownership_epoch(state: &Value) -> u64 {
|
|
@@ -1809,8 +1966,13 @@ mod tests {
|
|
|
1809
1966
|
"other": {"agent_id": "other", "provider": "codex", "window": "old-other", "pane_id": "%old", "spawn_epoch": 1}
|
|
1810
1967
|
},
|
|
1811
1968
|
});
|
|
1812
|
-
let err = save_runtime_state_with_lifecycle_topology_authority(
|
|
1813
|
-
|
|
1969
|
+
let err = save_runtime_state_with_lifecycle_topology_authority(
|
|
1970
|
+
&ws,
|
|
1971
|
+
&incoming,
|
|
1972
|
+
"team-a",
|
|
1973
|
+
&["target"],
|
|
1974
|
+
)
|
|
1975
|
+
.expect_err("authorized target must not hide other agent conflict");
|
|
1814
1976
|
assert!(matches!(err, StateError::SaveConflict(_)));
|
|
1815
1977
|
assert!(err.to_string().contains("agent_id=other"));
|
|
1816
1978
|
}
|
|
@@ -1835,7 +1997,13 @@ mod tests {
|
|
|
1835
1997
|
"target": {"agent_id": "target", "provider": "codex", "status": "stopped"}
|
|
1836
1998
|
},
|
|
1837
1999
|
});
|
|
1838
|
-
save_runtime_state_with_lifecycle_topology_authority(
|
|
2000
|
+
save_runtime_state_with_lifecycle_topology_authority(
|
|
2001
|
+
&ws,
|
|
2002
|
+
&incoming,
|
|
2003
|
+
"team-a",
|
|
2004
|
+
&["target"],
|
|
2005
|
+
)
|
|
2006
|
+
.unwrap();
|
|
1839
2007
|
let target = read_state(&ws).pointer("/agents/target").cloned().unwrap();
|
|
1840
2008
|
assert!(
|
|
1841
2009
|
target.get("pane_id").is_none(),
|
|
@@ -2418,4 +2586,98 @@ mod tests {
|
|
|
2418
2586
|
"0.4.6: partial poisoned latest must not resurrect cleared session_id; got {alpha}"
|
|
2419
2587
|
);
|
|
2420
2588
|
}
|
|
2589
|
+
|
|
2590
|
+
fn ownership_projection(session: &str, tty: &str) -> Value {
|
|
2591
|
+
let entry = json!({
|
|
2592
|
+
"session_name": "team-authority-merge",
|
|
2593
|
+
"owner_epoch": 10,
|
|
2594
|
+
"team_owner": {"status": "attached", "owner_epoch": 10, "pane_id": "%7"},
|
|
2595
|
+
"leader_receiver": {
|
|
2596
|
+
"status": "attached",
|
|
2597
|
+
"owner_epoch": 10,
|
|
2598
|
+
"pane_id": "%7",
|
|
2599
|
+
"session_name": session,
|
|
2600
|
+
"pane_tty": tty
|
|
2601
|
+
}
|
|
2602
|
+
});
|
|
2603
|
+
let mut state = json!({
|
|
2604
|
+
"session_name": "team-authority-merge",
|
|
2605
|
+
"active_team_key": "team-authority-merge",
|
|
2606
|
+
"teams": {
|
|
2607
|
+
"team-authority-merge": entry.clone(),
|
|
2608
|
+
"sibling": entry.clone()
|
|
2609
|
+
}
|
|
2610
|
+
});
|
|
2611
|
+
state["teams"][CURRENT_TEAM_ALIAS] = entry;
|
|
2612
|
+
state
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
#[test]
|
|
2616
|
+
fn equal_epoch_ordinary_writer_preserves_latest_receiver_observation() {
|
|
2617
|
+
let mut incoming = ownership_projection("stale-session", "/dev/stale");
|
|
2618
|
+
let latest = ownership_projection("refreshed-session", "/dev/refreshed");
|
|
2619
|
+
apply_persist_merge_contract(
|
|
2620
|
+
&mut incoming,
|
|
2621
|
+
&latest,
|
|
2622
|
+
&BTreeSet::new(),
|
|
2623
|
+
None,
|
|
2624
|
+
&BTreeSet::new(),
|
|
2625
|
+
&BTreeSet::new(),
|
|
2626
|
+
&BTreeSet::new(),
|
|
2627
|
+
)
|
|
2628
|
+
.unwrap();
|
|
2629
|
+
|
|
2630
|
+
assert_eq!(
|
|
2631
|
+
incoming.pointer("/teams/team-authority-merge/leader_receiver/session_name"),
|
|
2632
|
+
Some(&json!("refreshed-session"))
|
|
2633
|
+
);
|
|
2634
|
+
assert_eq!(
|
|
2635
|
+
incoming.pointer("/teams/team-authority-merge/leader_receiver/pane_tty"),
|
|
2636
|
+
Some(&json!("/dev/refreshed"))
|
|
2637
|
+
);
|
|
2638
|
+
assert_eq!(
|
|
2639
|
+
incoming["teams"][CURRENT_TEAM_ALIAS]["leader_receiver"]["session_name"],
|
|
2640
|
+
json!("refreshed-session")
|
|
2641
|
+
);
|
|
2642
|
+
assert_eq!(
|
|
2643
|
+
incoming["teams"]["sibling"]["leader_receiver"]["session_name"],
|
|
2644
|
+
json!("refreshed-session")
|
|
2645
|
+
);
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
#[test]
|
|
2649
|
+
fn equal_epoch_receiver_authority_keeps_incoming_refresh() {
|
|
2650
|
+
let mut incoming = ownership_projection("refreshed-session", "/dev/refreshed");
|
|
2651
|
+
let latest = ownership_projection("stale-session", "/dev/stale");
|
|
2652
|
+
let receiver_updates = BTreeSet::from(["team-authority-merge".to_string()]);
|
|
2653
|
+
apply_persist_merge_contract(
|
|
2654
|
+
&mut incoming,
|
|
2655
|
+
&latest,
|
|
2656
|
+
&BTreeSet::new(),
|
|
2657
|
+
None,
|
|
2658
|
+
&BTreeSet::new(),
|
|
2659
|
+
&BTreeSet::new(),
|
|
2660
|
+
&receiver_updates,
|
|
2661
|
+
)
|
|
2662
|
+
.unwrap();
|
|
2663
|
+
|
|
2664
|
+
assert_eq!(
|
|
2665
|
+
incoming.pointer("/teams/team-authority-merge/leader_receiver/session_name"),
|
|
2666
|
+
Some(&json!("refreshed-session"))
|
|
2667
|
+
);
|
|
2668
|
+
assert_eq!(
|
|
2669
|
+
incoming.pointer("/teams/team-authority-merge/leader_receiver/pane_tty"),
|
|
2670
|
+
Some(&json!("/dev/refreshed"))
|
|
2671
|
+
);
|
|
2672
|
+
assert_eq!(
|
|
2673
|
+
incoming["teams"][CURRENT_TEAM_ALIAS]["leader_receiver"]["session_name"],
|
|
2674
|
+
json!("refreshed-session"),
|
|
2675
|
+
"current alias must inherit the active team's receiver authority"
|
|
2676
|
+
);
|
|
2677
|
+
assert_eq!(
|
|
2678
|
+
incoming["teams"]["sibling"]["leader_receiver"]["session_name"],
|
|
2679
|
+
json!("stale-session"),
|
|
2680
|
+
"team-scoped receiver authority must not overwrite a sibling's latest tuple"
|
|
2681
|
+
);
|
|
2682
|
+
}
|
|
2421
2683
|
}
|