@team-agent/installer 0.5.50 → 0.5.52
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 +5 -3
- package/crates/team-agent/src/cli/emit.rs +2 -1
- package/crates/team-agent/src/cli/mod.rs +32 -14
- package/crates/team-agent/src/cli/named_address.rs +46 -115
- package/crates/team-agent/src/cli/send/coordinator.rs +163 -0
- package/crates/team-agent/src/cli/send/mailbox.rs +99 -0
- package/crates/team-agent/src/cli/send/persist.rs +154 -0
- package/crates/team-agent/src/cli/send/presentation.rs +333 -0
- package/crates/team-agent/src/cli/send/resolve.rs +361 -0
- package/crates/team-agent/src/cli/send.rs +44 -1169
- package/crates/team-agent/src/cli/spec.rs +1 -1
- package/crates/team-agent/src/cli/status_port/agents.rs +358 -0
- package/crates/team-agent/src/cli/status_port/approvals.rs +79 -0
- package/crates/team-agent/src/cli/status_port/compact.rs +207 -0
- package/crates/team-agent/src/cli/status_port/format.rs +145 -0
- package/crates/team-agent/src/cli/status_port/inbox.rs +36 -0
- package/crates/team-agent/src/cli/status_port/runtime.rs +195 -0
- package/crates/team-agent/src/cli/status_port/snapshot.rs +181 -0
- package/crates/team-agent/src/cli/status_port/store.rs +412 -0
- package/crates/team-agent/src/cli/status_port/tests.rs +54 -0
- package/crates/team-agent/src/cli/status_port.rs +47 -1548
- package/crates/team-agent/src/cli/tests/main_preserved.rs +1 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +1 -0
- package/crates/team-agent/src/coordinator/conpty_shim.rs +34 -30
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +135 -13
- package/crates/team-agent/src/coordinator/tick.rs +37 -0
- package/crates/team-agent/src/db/agent_health_capture.rs +18 -13
- package/crates/team-agent/src/db/message_store.rs +154 -44
- package/crates/team-agent/src/event_log.rs +73 -0
- package/crates/team-agent/src/leader/lease.rs +202 -16
- package/crates/team-agent/src/leader/start.rs +28 -4
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +424 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +303 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +160 -0
- package/crates/team-agent/src/lifecycle/launch/approval.rs +134 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +492 -0
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +297 -0
- package/crates/team-agent/src/lifecycle/launch/identity.rs +372 -0
- package/crates/team-agent/src/lifecycle/launch/layout.rs +313 -0
- package/crates/team-agent/src/lifecycle/launch/leader_context.rs +478 -0
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +201 -0
- package/crates/team-agent/src/lifecycle/launch/ownership.rs +66 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +477 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +278 -0
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +123 -0
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +377 -0
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +434 -0
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +499 -0
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +438 -0
- package/crates/team-agent/src/lifecycle/launch.rs +119 -5351
- package/crates/team-agent/src/lifecycle/restart/agent.rs +63 -29
- package/crates/team-agent/src/lifecycle/restart/common.rs +53 -27
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +167 -27
- package/crates/team-agent/src/lifecycle/restart/remove.rs +503 -77
- package/crates/team-agent/src/lifecycle/restart.rs +26 -2
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +906 -17
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +67 -2
- package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +24 -1
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +30 -7
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +8 -4
- package/crates/team-agent/src/mcp_server/mod.rs +1 -1
- package/crates/team-agent/src/mcp_server/tests/send.rs +6 -10
- package/crates/team-agent/src/mcp_server/tests/tools.rs +18 -1
- package/crates/team-agent/src/mcp_server/tests.rs +11 -4
- package/crates/team-agent/src/mcp_server/tools.rs +14 -5
- package/crates/team-agent/src/messaging/activity.rs +4 -2
- package/crates/team-agent/src/messaging/address.rs +86 -0
- package/crates/team-agent/src/messaging/delivery.rs +288 -183
- package/crates/team-agent/src/messaging/helpers.rs +17 -13
- package/crates/team-agent/src/messaging/leader_channel.rs +171 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +60 -35
- package/crates/team-agent/src/messaging/mod.rs +15 -2
- package/crates/team-agent/src/messaging/persist.rs +309 -0
- package/crates/team-agent/src/messaging/results.rs +16 -24
- package/crates/team-agent/src/messaging/scheduler.rs +4 -2
- package/crates/team-agent/src/messaging/selftest.rs +19 -12
- package/crates/team-agent/src/messaging/send.rs +101 -49
- package/crates/team-agent/src/messaging/tests/leader_channel.rs +169 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +305 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +2 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +38 -17
- package/crates/team-agent/src/messaging/watchers.rs +13 -3
- package/crates/team-agent/src/redaction.rs +72 -2
- package/crates/team-agent/src/state/persist.rs +284 -21
- 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/tests.rs +47 -0
- package/crates/team-agent/src/state/repository.rs +94 -27
- package/crates/team-agent/src/state/selector.rs +9 -18
- package/package.json +4 -4
|
@@ -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)。
|
|
@@ -47,7 +47,7 @@ const SESSION_STATE_FIELDS: [&str; 6] = [
|
|
|
47
47
|
];
|
|
48
48
|
const LIVE_TOPOLOGY_FIELDS: [&str; 5] =
|
|
49
49
|
["pane_id", "pane_pid", "window", "spawned_at", "spawn_epoch"];
|
|
50
|
-
const ROSTER_STUB_ALLOWLIST: [&str;
|
|
50
|
+
const ROSTER_STUB_ALLOWLIST: [&str; 16] = [
|
|
51
51
|
"agent_id",
|
|
52
52
|
"provider",
|
|
53
53
|
"auth_mode",
|
|
@@ -57,6 +57,7 @@ const ROSTER_STUB_ALLOWLIST: [&str; 15] = [
|
|
|
57
57
|
"profile",
|
|
58
58
|
"_profile_dir",
|
|
59
59
|
"dynamic_role_file",
|
|
60
|
+
"role_source_ownership",
|
|
60
61
|
"effort",
|
|
61
62
|
"forked_from",
|
|
62
63
|
"managed_mcp_config",
|
|
@@ -204,7 +205,24 @@ impl Drop for RuntimeLock {
|
|
|
204
205
|
/// `save_runtime_state`(bug-084)。`state` 是 state.json 的内存 Value(插入序保留)。
|
|
205
206
|
/// 注:Python 在此还调 `_migrate_state_identity`(identity slice 落地后接入;本 slice 不改 state 内容)。
|
|
206
207
|
pub fn save_runtime_state(workspace: &Path, state: &Value) -> Result<(), StateError> {
|
|
207
|
-
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
|
+
)
|
|
208
226
|
}
|
|
209
227
|
|
|
210
228
|
pub(crate) fn save_runtime_state_reapplying_after_conflict<F>(
|
|
@@ -229,9 +247,19 @@ where
|
|
|
229
247
|
pub(crate) fn save_runtime_state_with_lifecycle_topology_authority(
|
|
230
248
|
workspace: &Path,
|
|
231
249
|
state: &Value,
|
|
250
|
+
topology_team_key: &str,
|
|
232
251
|
agent_ids: &[&str],
|
|
233
252
|
) -> Result<(), StateError> {
|
|
234
|
-
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
|
+
)
|
|
235
263
|
}
|
|
236
264
|
|
|
237
265
|
pub(crate) fn save_runtime_state_with_lifecycle_topology_authority_and_capture_backfill_skip(
|
|
@@ -247,7 +275,9 @@ pub(crate) fn save_runtime_state_with_lifecycle_topology_authority_and_capture_b
|
|
|
247
275
|
&[],
|
|
248
276
|
Some(skip_capture_backfill_team_key),
|
|
249
277
|
skip_capture_backfill_agent_ids,
|
|
278
|
+
Some(skip_capture_backfill_team_key),
|
|
250
279
|
topology_agent_ids,
|
|
280
|
+
None,
|
|
251
281
|
)
|
|
252
282
|
}
|
|
253
283
|
|
|
@@ -256,7 +286,16 @@ pub(crate) fn save_runtime_state_with_deleted_agents(
|
|
|
256
286
|
state: &Value,
|
|
257
287
|
deleted_agent_ids: &[&str],
|
|
258
288
|
) -> Result<(), StateError> {
|
|
259
|
-
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
|
+
)
|
|
260
299
|
}
|
|
261
300
|
|
|
262
301
|
pub(crate) fn save_runtime_state_with_team_tombstoned_agents(
|
|
@@ -271,7 +310,9 @@ pub(crate) fn save_runtime_state_with_team_tombstoned_agents(
|
|
|
271
310
|
&[],
|
|
272
311
|
Some(tombstoned_team_key),
|
|
273
312
|
tombstoned_agent_ids,
|
|
313
|
+
None,
|
|
274
314
|
&[],
|
|
315
|
+
None,
|
|
275
316
|
)
|
|
276
317
|
}
|
|
277
318
|
|
|
@@ -287,7 +328,9 @@ pub(crate) fn save_runtime_state_with_team_tombstone_lifecycle_topology_authorit
|
|
|
287
328
|
&[],
|
|
288
329
|
Some(tombstoned_team_key),
|
|
289
330
|
agent_ids,
|
|
331
|
+
Some(tombstoned_team_key),
|
|
290
332
|
agent_ids,
|
|
333
|
+
None,
|
|
291
334
|
)
|
|
292
335
|
}
|
|
293
336
|
|
|
@@ -297,7 +340,9 @@ fn save_runtime_state_with_merge_options(
|
|
|
297
340
|
deleted_agent_ids: &[&str],
|
|
298
341
|
skip_capture_backfill_team_key: Option<&str>,
|
|
299
342
|
skip_capture_backfill_agent_ids: &[&str],
|
|
343
|
+
topology_update_team_key: Option<&str>,
|
|
300
344
|
topology_update_agent_ids: &[&str],
|
|
345
|
+
receiver_update_team_key: Option<&str>,
|
|
301
346
|
) -> Result<(), StateError> {
|
|
302
347
|
let path = runtime_state_path(workspace);
|
|
303
348
|
// Python `state.py:497`:先对入参 state 跑 `_migrate_state_identity`(就地填缺失 leader uuid)。
|
|
@@ -361,10 +406,19 @@ fn save_runtime_state_with_merge_options(
|
|
|
361
406
|
.filter(|id| !id.is_empty())
|
|
362
407
|
.map(str::to_string)
|
|
363
408
|
.collect::<BTreeSet<_>>();
|
|
364
|
-
let topology_updates =
|
|
365
|
-
.
|
|
366
|
-
.
|
|
367
|
-
|
|
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())
|
|
368
422
|
.map(str::to_string)
|
|
369
423
|
.collect::<BTreeSet<_>>();
|
|
370
424
|
apply_persist_merge_contract(
|
|
@@ -374,6 +428,7 @@ fn save_runtime_state_with_merge_options(
|
|
|
374
428
|
skip_capture_backfill_team_key,
|
|
375
429
|
&skip_capture_backfill,
|
|
376
430
|
&topology_updates,
|
|
431
|
+
&receiver_updates,
|
|
377
432
|
)?;
|
|
378
433
|
}
|
|
379
434
|
// Stage 3 save-output strip second pass (defence-in-depth): after the
|
|
@@ -439,7 +494,8 @@ fn apply_persist_merge_contract(
|
|
|
439
494
|
deleted_agent_ids: &BTreeSet<String>,
|
|
440
495
|
skip_capture_backfill_team_key: Option<&str>,
|
|
441
496
|
skip_capture_backfill_agent_ids: &BTreeSet<String>,
|
|
442
|
-
|
|
497
|
+
topology_updates: &BTreeSet<(String, String)>,
|
|
498
|
+
receiver_updates: &BTreeSet<String>,
|
|
443
499
|
) -> Result<(), StateError> {
|
|
444
500
|
// A0/R1: the projection gate only guards the TOP-LEVEL passes (top-level agents and
|
|
445
501
|
// the top-level<->active-team cross projections depend on which team is active); the
|
|
@@ -447,10 +503,19 @@ fn apply_persist_merge_contract(
|
|
|
447
503
|
// even when another process flipped session_name/active_team_key between this
|
|
448
504
|
// writer's load and save.
|
|
449
505
|
let projection_matches = same_runtime_projection(incoming, latest);
|
|
450
|
-
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)));
|
|
451
513
|
let skip_top_level_capture_backfill =
|
|
452
514
|
should_skip_capture_backfill(top_level_team.as_deref(), skip_capture_backfill_team_key);
|
|
453
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)?;
|
|
454
519
|
// 0.5.26 (§7.3): top-level projection uses the incoming root state's
|
|
455
520
|
// implicit "alive" shape (top-level agents are always the active team).
|
|
456
521
|
// Passing `None` for team lifecycle keeps historical behavior when the
|
|
@@ -462,7 +527,7 @@ fn apply_persist_merge_contract(
|
|
|
462
527
|
deleted_agent_ids,
|
|
463
528
|
skip_top_level_capture_backfill,
|
|
464
529
|
skip_capture_backfill_agent_ids,
|
|
465
|
-
topology_update_agent_ids,
|
|
530
|
+
&topology_update_agent_ids,
|
|
466
531
|
None,
|
|
467
532
|
)?;
|
|
468
533
|
// Stage 3c (identity-boundary unified plan, architect direction
|
|
@@ -499,6 +564,21 @@ fn apply_persist_merge_contract(
|
|
|
499
564
|
let Some(incoming_entry) = incoming_teams.get_mut(team) else {
|
|
500
565
|
continue;
|
|
501
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));
|
|
502
582
|
let projection = format!("teams.{team}.agents");
|
|
503
583
|
// 0.5.26 (`.team/artifacts/stale-team-saveconflict-locate.md` §7.3):
|
|
504
584
|
// decide the containing team's aliveness from BOTH sides — a shutdown
|
|
@@ -513,6 +593,11 @@ fn apply_persist_merge_contract(
|
|
|
513
593
|
.map(|_| crate::state::projection::team_is_alive_candidate(incoming_entry))
|
|
514
594
|
.unwrap_or(true),
|
|
515
595
|
);
|
|
596
|
+
preserve_latest_retired_agents(
|
|
597
|
+
incoming_entry,
|
|
598
|
+
latest_entry,
|
|
599
|
+
&topology_update_agent_ids,
|
|
600
|
+
)?;
|
|
516
601
|
merge_agent_projection(
|
|
517
602
|
&projection,
|
|
518
603
|
incoming_entry.get_mut("agents"),
|
|
@@ -520,16 +605,79 @@ fn apply_persist_merge_contract(
|
|
|
520
605
|
deleted_agent_ids,
|
|
521
606
|
should_skip_capture_backfill(Some(team), skip_capture_backfill_team_key),
|
|
522
607
|
skip_capture_backfill_agent_ids,
|
|
523
|
-
topology_update_agent_ids,
|
|
608
|
+
&topology_update_agent_ids,
|
|
524
609
|
team_alive,
|
|
525
610
|
)?;
|
|
526
|
-
preserve_latest_ownership_fields(
|
|
611
|
+
preserve_latest_ownership_fields(
|
|
612
|
+
incoming_entry,
|
|
613
|
+
latest_entry,
|
|
614
|
+
receiver_update_authoritative,
|
|
615
|
+
);
|
|
527
616
|
preserve_latest_endpoint_convergence_fields(incoming_entry, latest_entry);
|
|
528
617
|
}
|
|
529
618
|
}
|
|
530
619
|
Ok(())
|
|
531
620
|
}
|
|
532
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
|
+
|
|
533
681
|
/// 0.5.x Windows portability Batch 7 F6: copy `latest.transport.shim`
|
|
534
682
|
/// into `incoming` if the incoming save omitted it. The coordinator's
|
|
535
683
|
/// `conpty_shim::finalize` writes `state.transport.shim = {pid,
|
|
@@ -577,8 +725,12 @@ fn should_skip_capture_backfill(
|
|
|
577
725
|
}
|
|
578
726
|
}
|
|
579
727
|
|
|
580
|
-
fn preserve_latest_ownership_fields(
|
|
581
|
-
|
|
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) {
|
|
582
734
|
return;
|
|
583
735
|
}
|
|
584
736
|
let Some(incoming_obj) = incoming.as_object_mut() else {
|
|
@@ -642,13 +794,19 @@ fn endpoint_convergence_epoch(state: &Value) -> Option<u64> {
|
|
|
642
794
|
.and_then(Value::as_u64)
|
|
643
795
|
}
|
|
644
796
|
|
|
645
|
-
fn latest_has_preferable_ownership(
|
|
797
|
+
fn latest_has_preferable_ownership(
|
|
798
|
+
incoming: &Value,
|
|
799
|
+
latest: &Value,
|
|
800
|
+
receiver_update_authoritative: bool,
|
|
801
|
+
) -> bool {
|
|
646
802
|
let latest_epoch = ownership_epoch(latest);
|
|
647
803
|
let incoming_epoch = ownership_epoch(incoming);
|
|
648
804
|
if latest_epoch > incoming_epoch {
|
|
649
805
|
return true;
|
|
650
806
|
}
|
|
651
|
-
latest_epoch == incoming_epoch
|
|
807
|
+
latest_epoch == incoming_epoch
|
|
808
|
+
&& !receiver_update_authoritative
|
|
809
|
+
&& (ownership_attached(latest) || !ownership_attached(incoming))
|
|
652
810
|
}
|
|
653
811
|
|
|
654
812
|
fn ownership_epoch(state: &Value) -> u64 {
|
|
@@ -1808,8 +1966,13 @@ mod tests {
|
|
|
1808
1966
|
"other": {"agent_id": "other", "provider": "codex", "window": "old-other", "pane_id": "%old", "spawn_epoch": 1}
|
|
1809
1967
|
},
|
|
1810
1968
|
});
|
|
1811
|
-
let err = save_runtime_state_with_lifecycle_topology_authority(
|
|
1812
|
-
|
|
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");
|
|
1813
1976
|
assert!(matches!(err, StateError::SaveConflict(_)));
|
|
1814
1977
|
assert!(err.to_string().contains("agent_id=other"));
|
|
1815
1978
|
}
|
|
@@ -1834,7 +1997,13 @@ mod tests {
|
|
|
1834
1997
|
"target": {"agent_id": "target", "provider": "codex", "status": "stopped"}
|
|
1835
1998
|
},
|
|
1836
1999
|
});
|
|
1837
|
-
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();
|
|
1838
2007
|
let target = read_state(&ws).pointer("/agents/target").cloned().unwrap();
|
|
1839
2008
|
assert!(
|
|
1840
2009
|
target.get("pane_id").is_none(),
|
|
@@ -2417,4 +2586,98 @@ mod tests {
|
|
|
2417
2586
|
"0.4.6: partial poisoned latest must not resurrect cleared session_id; got {alpha}"
|
|
2418
2587
|
);
|
|
2419
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
|
+
}
|
|
2420
2683
|
}
|