@team-agent/installer 0.5.26 → 0.5.28

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 CHANGED
@@ -575,7 +575,7 @@ dependencies = [
575
575
 
576
576
  [[package]]
577
577
  name = "team-agent"
578
- version = "0.5.26"
578
+ version = "0.5.28"
579
579
  dependencies = [
580
580
  "anyhow",
581
581
  "chrono",
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent", "crates/win-conpty-phase0", "crates/conpty-trans
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.5.26"
12
+ version = "0.5.28"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -3463,9 +3463,16 @@ pub mod lifecycle_port {
3463
3463
  out
3464
3464
  }
3465
3465
 
3466
- /// 0.5.26 (`.team/artifacts/stale-team-saveconflict-locate.md` §7.2):
3467
- /// scoped shutdown 只有一队,`state.active_team_key` 指向被关的那队。
3466
+ /// 0.5.26 (`.team/artifacts/stale-team-saveconflict-locate.md` §7.2) /
3467
+ /// 0.5.27 (`.team/artifacts/0526-supermarket-shutdown-status-real-fail-locate.md` §5.1):
3468
+ /// scoped shutdown 只有一队,`state.active_team_key` 指向被关的那队;
3468
3469
  /// 只在 session_killed && !verification_degraded 时被调用。
3470
+ ///
3471
+ /// 同时 stamp 顶层 `state["status"]` 与 `teams[active_key]["status"]`:
3472
+ /// scoped 关队走 `save_team_scoped_state`,反写盘的源是 `compact(top-level)`
3473
+ /// (见 `state/persist.rs::save_team_scoped_state` +
3474
+ /// `projection::compact_team_state`),只标 nested 会被顶层视图 clobber 丢盘。
3475
+ /// 保留 nested 写入以维持同 tick 内的 in-memory 一致(其他 read-back 立即看到)。
3469
3476
  fn mark_active_team_shutdown(state: &mut Value, status: &str) {
3470
3477
  let Some(active_key) = state
3471
3478
  .get("active_team_key")
@@ -3474,6 +3481,9 @@ pub mod lifecycle_port {
3474
3481
  else {
3475
3482
  return;
3476
3483
  };
3484
+ if let Some(obj) = state.as_object_mut() {
3485
+ obj.insert("status".to_string(), json!(status));
3486
+ }
3477
3487
  let Some(teams) = state.get_mut("teams").and_then(Value::as_object_mut) else {
3478
3488
  return;
3479
3489
  };
@@ -331,6 +331,129 @@ fn scoped_shutdown_keeps_owned_endpoint_when_sibling_session_remains() {
331
331
  );
332
332
  }
333
333
 
334
+ #[test]
335
+ fn scoped_clean_shutdown_persists_team_shutdown_status_after_disk_roundtrip() {
336
+ let ws = tmp_shutdown_workspace("scoped-clean-status-roundtrip");
337
+ crate::state::persist::save_runtime_state(
338
+ &ws,
339
+ &json!({
340
+ "schema_version": 1,
341
+ "active_team_key": "current",
342
+ "team_key": "current",
343
+ "session_name": "team-current",
344
+ "team_dir": ws.display().to_string(),
345
+ "workspace": ws.display().to_string(),
346
+ "agents": {
347
+ "adminweb": {
348
+ "agent_id": "adminweb",
349
+ "status": "running",
350
+ "provider": "fake",
351
+ "window": "adminweb"
352
+ }
353
+ },
354
+ "teams": {
355
+ "current": {
356
+ "team_key": "current",
357
+ "session_name": "team-current",
358
+ "team_dir": ws.display().to_string(),
359
+ "workspace": ws.display().to_string(),
360
+ "agents": {
361
+ "adminweb": {
362
+ "agent_id": "adminweb",
363
+ "status": "running",
364
+ "provider": "fake",
365
+ "window": "adminweb"
366
+ }
367
+ }
368
+ }
369
+ }
370
+ }),
371
+ )
372
+ .unwrap();
373
+
374
+ let out = crate::cli::lifecycle_port::shutdown_with_transport(
375
+ &ws,
376
+ true,
377
+ Some("current"),
378
+ &CleanShutdownTransport::new(),
379
+ )
380
+ .expect("shutdown should complete");
381
+
382
+ assert_eq!(out["ok"], json!(true), "shutdown report: {out}");
383
+ assert_eq!(out["session_killed"], json!(true), "shutdown report: {out}");
384
+ assert_eq!(out["verification_degraded"], json!(false), "shutdown report: {out}");
385
+ let saved = crate::state::persist::load_runtime_state(&ws).unwrap();
386
+ assert_eq!(
387
+ saved.pointer("/teams/current/status").and_then(serde_json::Value::as_str),
388
+ Some("shutdown"),
389
+ "0.5.27 RED: clean scoped shutdown returned session_killed=true and not degraded, \
390
+ so disk state must persist teams.current.status=shutdown. The compact projected \
391
+ save path must not drop the tombstone; saved={saved}"
392
+ );
393
+ assert_eq!(
394
+ saved
395
+ .pointer("/teams/current/agents/adminweb/status")
396
+ .and_then(serde_json::Value::as_str),
397
+ Some("stopped"),
398
+ "0.5.27 RED: clean shutdown must also persist stopped agents under the retained team; saved={saved}"
399
+ );
400
+ }
401
+
402
+ #[test]
403
+ fn scoped_degraded_shutdown_does_not_persist_team_shutdown_status() {
404
+ let ws = tmp_shutdown_workspace("scoped-degraded-no-status");
405
+ crate::state::persist::save_runtime_state(
406
+ &ws,
407
+ &json!({
408
+ "schema_version": 1,
409
+ "active_team_key": "current",
410
+ "team_key": "current",
411
+ "session_name": "team-current",
412
+ "agents": {
413
+ "adminweb": {
414
+ "agent_id": "adminweb",
415
+ "status": "running",
416
+ "provider": "fake",
417
+ "window": "adminweb"
418
+ }
419
+ },
420
+ "teams": {
421
+ "current": {
422
+ "team_key": "current",
423
+ "session_name": "team-current",
424
+ "agents": {
425
+ "adminweb": {
426
+ "agent_id": "adminweb",
427
+ "status": "running",
428
+ "provider": "fake",
429
+ "window": "adminweb"
430
+ }
431
+ }
432
+ }
433
+ }
434
+ }),
435
+ )
436
+ .unwrap();
437
+
438
+ let out = crate::cli::lifecycle_port::shutdown_with_transport(
439
+ &ws,
440
+ true,
441
+ Some("current"),
442
+ &CleanShutdownTransport::new().with_probe_timeout("ps_table"),
443
+ )
444
+ .expect("shutdown should complete");
445
+
446
+ assert_eq!(out["session_killed"], json!(true), "shutdown report: {out}");
447
+ assert_eq!(out["verification_degraded"], json!(true), "shutdown report: {out}");
448
+ let saved = crate::state::persist::load_runtime_state(&ws).unwrap();
449
+ assert_ne!(
450
+ saved.pointer("/teams/current/status").and_then(serde_json::Value::as_str),
451
+ Some("shutdown"),
452
+ "degraded shutdown must remain dirty/diagnostic and must not stamp \
453
+ teams.current.status=shutdown; saved={saved}"
454
+ );
455
+ }
456
+
334
457
  #[test]
335
458
  fn repeated_owned_endpoint_shutdowns_leave_no_socket_file_growth() {
336
459
  let ws = tmp_shutdown_workspace("owned-loop-no-growth");
@@ -672,9 +672,16 @@ pub(super) fn sync_restart_team_projections(state: &mut serde_json::Value, team_
672
672
  else {
673
673
  return;
674
674
  };
675
+ // 0.5.28 (`.team/artifacts/0527-realfail-layer2-leader-locate.md` §3):
676
+ // 显式 team_key = 操作目标,始终允许覆盖 / 新建(该 helper 的正当职责)。
677
+ // active/derived/"current" 三种别名兜底 = alias-identity 家族第三例,只有
678
+ // 「盘上已有该 alias 条目」且「其身份与 compact 不冲突」时才允许写,
679
+ // 避免 0.5.26 起死 sibling 保留时被活队 compact 硬克隆。禁止 alias 新建条目。
675
680
  let mut keys = Vec::new();
681
+ let mut explicit = false;
676
682
  if !team_key.is_empty() {
677
683
  keys.push(team_key.to_string());
684
+ explicit = true;
678
685
  }
679
686
  if let Some(active_key) = active_key {
680
687
  keys.push(active_key);
@@ -688,8 +695,41 @@ pub(super) fn sync_restart_team_projections(state: &mut serde_json::Value, team_
688
695
  keys.sort();
689
696
  keys.dedup();
690
697
  for key in keys {
691
- teams.insert(key, compact.clone());
698
+ let is_operation_target = explicit && key == team_key;
699
+ if is_operation_target {
700
+ teams.insert(key, compact.clone());
701
+ continue;
702
+ }
703
+ let Some(existing) = teams.get(&key) else {
704
+ // 别名条目不存在:不新建,避免把 hack alias 变成真队。
705
+ continue;
706
+ };
707
+ if json_team_identity_matches(existing, &compact) {
708
+ teams.insert(key, compact.clone());
709
+ }
710
+ }
711
+ }
712
+
713
+ /// 0.5.28: 别名同步身份门。比较 existing 条目与 compact(即将写入)四个身份字段:
714
+ /// `team_key`/`session_name`/`team_dir`/`spec_path`。legacy 缺字段容忍(两侧任一
715
+ /// 缺则该字段不参与判定);已有字段必须相等,冲突即拒绝覆盖。
716
+ fn json_team_identity_matches(existing: &serde_json::Value, compact: &serde_json::Value) -> bool {
717
+ const FIELDS: &[&str] = &["team_key", "session_name", "team_dir", "spec_path"];
718
+ for field in FIELDS {
719
+ let lhs = existing
720
+ .get(field)
721
+ .and_then(serde_json::Value::as_str)
722
+ .filter(|s| !s.is_empty());
723
+ let rhs = compact
724
+ .get(field)
725
+ .and_then(serde_json::Value::as_str)
726
+ .filter(|s| !s.is_empty());
727
+ match (lhs, rhs) {
728
+ (Some(a), Some(b)) if a != b => return false,
729
+ _ => {}
730
+ }
692
731
  }
732
+ true
693
733
  }
694
734
 
695
735
  pub(super) fn state_session_name(state: &serde_json::Value) -> SessionName {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.5.26",
3
+ "version": "0.5.28",
4
4
  "description": "npx installer for Team Agent",
5
5
  "keywords": [
6
6
  "codex",
@@ -20,9 +20,9 @@
20
20
  "team-agent-installer": "npm/install.mjs"
21
21
  },
22
22
  "optionalDependencies": {
23
- "@team-agent/cli-darwin-arm64": "0.5.26",
24
- "@team-agent/cli-darwin-x64": "0.5.26",
25
- "@team-agent/cli-linux-x64": "0.5.26"
23
+ "@team-agent/cli-darwin-arm64": "0.5.28",
24
+ "@team-agent/cli-darwin-x64": "0.5.28",
25
+ "@team-agent/cli-linux-x64": "0.5.28"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",