@team-agent/installer 0.5.39 → 0.5.40
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
package/Cargo.toml
CHANGED
|
@@ -3339,6 +3339,30 @@ pub mod lifecycle_port {
|
|
|
3339
3339
|
"invalid": invalid.iter().map(|w| w.worker_id.as_str()).collect::<Vec<_>>(),
|
|
3340
3340
|
"reminder": crate::cli::QUICK_START_REMINDER,
|
|
3341
3341
|
}),
|
|
3342
|
+
crate::lifecycle::RestartReport::RefusedBuildBeforeDestroyRequired {
|
|
3343
|
+
session_name,
|
|
3344
|
+
live_agents,
|
|
3345
|
+
error,
|
|
3346
|
+
} => {
|
|
3347
|
+
let repair_team = team
|
|
3348
|
+
.filter(|team| !team.is_empty())
|
|
3349
|
+
.unwrap_or(session_name.as_str());
|
|
3350
|
+
let shutdown_scoped =
|
|
3351
|
+
format!("team-agent shutdown --team {repair_team}");
|
|
3352
|
+
json!({
|
|
3353
|
+
"ok": false,
|
|
3354
|
+
"status": "refused_build_before_destroy",
|
|
3355
|
+
"reason": "build_before_destroy_required",
|
|
3356
|
+
"session_name": session_name,
|
|
3357
|
+
"live_agents": live_agents,
|
|
3358
|
+
"error": error,
|
|
3359
|
+
"next_actions": [
|
|
3360
|
+
shutdown_scoped,
|
|
3361
|
+
"team-agent restart",
|
|
3362
|
+
],
|
|
3363
|
+
"reminder": crate::cli::QUICK_START_REMINDER,
|
|
3364
|
+
})
|
|
3365
|
+
}
|
|
3342
3366
|
crate::lifecycle::RestartReport::RefusedDirtyTopology {
|
|
3343
3367
|
session_name,
|
|
3344
3368
|
reason,
|
|
@@ -410,22 +410,27 @@ fn restart_with_selected_team_and_transport(
|
|
|
410
410
|
}
|
|
411
411
|
}
|
|
412
412
|
let session_name = state_session_name(&state);
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
413
|
+
// 0.5.40 Slice 3 (tmux-server-death-locate §7 Slice 3, first-version
|
|
414
|
+
// build-before-destroy). Pre-0.5.40 code UNCONDITIONALLY killed the
|
|
415
|
+
// live worker session here, then ran the spawn loop against a fresh
|
|
416
|
+
// session — which loses provider session state (Case B) and, under
|
|
417
|
+
// upstream tmux 3.6a private-server bugs, can cascade into whole-
|
|
418
|
+
// server death (Case A). Locate §7 Slice 3 requires: "restart must
|
|
419
|
+
// build/prove the replacement before retiring a currently live
|
|
420
|
+
// worker session." First-version narrow shape: defer the kill until
|
|
421
|
+
// AFTER the spawn loop proves the replacement is minimally viable,
|
|
422
|
+
// and refuse the kill on failure so the original session/state stay
|
|
423
|
+
// authoritative. Discriminator = `collect_live_agents_from_state`
|
|
424
|
+
// (state marks agents running with real pane_ids). When state has
|
|
425
|
+
// no authoritative live agents, the previous behavior (pre-spawn
|
|
426
|
+
// teardown of a stale empty session) is preserved so tests that
|
|
427
|
+
// seed a fake-live session without live agent rows still work.
|
|
428
|
+
let live_worker_session_deferred_teardown =
|
|
429
|
+
session_live_or_default(transport, &session_name, false)
|
|
430
|
+
&& !collect_live_agents_from_state(&state).is_empty();
|
|
431
|
+
if session_live_or_default(transport, &session_name, false)
|
|
432
|
+
&& !live_worker_session_deferred_teardown
|
|
433
|
+
{
|
|
429
434
|
eprintln!(
|
|
430
435
|
"team_agent::layout restart_precondition_warning worker_session=`{}` action=killing \
|
|
431
436
|
(post-Step-7 will refuse and direct user to recover; safe today because Step 2 \
|
|
@@ -451,6 +456,20 @@ fn restart_with_selected_team_and_transport(
|
|
|
451
456
|
}
|
|
452
457
|
phase_timer.emit(&selected.run_workspace, "restart.phase", "teardown");
|
|
453
458
|
phase_timer.emit(&selected.run_workspace, "restart.phase", "spawn_all");
|
|
459
|
+
// 0.5.40 Slice 3 (tmux-server-death-locate §7 Slice 3): when the
|
|
460
|
+
// pre-spawn teardown was DEFERRED (live worker session with
|
|
461
|
+
// authoritative running agents), snapshot the pre-spawn agent rows
|
|
462
|
+
// so we can restore them if the replacement build fails. This
|
|
463
|
+
// enforces the "old session/state stay authoritative on failure"
|
|
464
|
+
// invariant without threading a buffer through every state
|
|
465
|
+
// mutation. On success (all spawns viable) the mutations already
|
|
466
|
+
// written win; on failure the snapshot restores the original rows.
|
|
467
|
+
let build_before_destroy_agents_snapshot: Option<serde_json::Value> =
|
|
468
|
+
if live_worker_session_deferred_teardown {
|
|
469
|
+
state.get("agents").cloned()
|
|
470
|
+
} else {
|
|
471
|
+
None
|
|
472
|
+
};
|
|
454
473
|
let mut successful_agents: Vec<RestartedAgent> = Vec::new();
|
|
455
474
|
let mut failed_agents: Vec<RestartFailedAgent> = Vec::new();
|
|
456
475
|
let mut fatal_resume_failure = false;
|
|
@@ -599,6 +618,37 @@ fn restart_with_selected_team_and_transport(
|
|
|
599
618
|
};
|
|
600
619
|
let mut session_live = session_live_or_default(transport, &session_name, false);
|
|
601
620
|
if !session_live {
|
|
621
|
+
// 0.5.40 Slice 3: when we deferred the pre-spawn teardown
|
|
622
|
+
// because the original session was live with authoritative
|
|
623
|
+
// running agents, a mid-flight session disappearance
|
|
624
|
+
// classifies as tmux_server_crashed (0539 §11.1 B) — the
|
|
625
|
+
// whole server died out from under us. Do NOT cascade-pop
|
|
626
|
+
// previously-successful agents into `session_disappeared_after_spawn`:
|
|
627
|
+
// that path (a) rewrites state.agents.<popped>.status to
|
|
628
|
+
// restart_failed (losing the authoritative old row R3
|
|
629
|
+
// requires stays untouched), and (b) emits the exact
|
|
630
|
+
// cascade error string R3 forbids. Instead, refuse the
|
|
631
|
+
// build-before-destroy cohort: leave `successful_agents`
|
|
632
|
+
// alone (their live spawn already happened but the server
|
|
633
|
+
// died before we could prove them viable), stop the loop,
|
|
634
|
+
// and let the terminal Failed report attribute the failure
|
|
635
|
+
// to tmux_server_crashed via `restart_failure_phase`. The
|
|
636
|
+
// legacy path still runs when the pre-spawn teardown was
|
|
637
|
+
// taken (empty session case), because
|
|
638
|
+
// `live_worker_session_deferred_teardown` is false there.
|
|
639
|
+
if live_worker_session_deferred_teardown {
|
|
640
|
+
fatal_resume_failure = true;
|
|
641
|
+
let error = format!(
|
|
642
|
+
"tmux_server_crashed: session {} disappeared during replacement build; refusing to touch original agent state",
|
|
643
|
+
session_name.as_str()
|
|
644
|
+
);
|
|
645
|
+
failed_agents.push(restart_failed_agent(
|
|
646
|
+
decision,
|
|
647
|
+
"tmux_server_crashed",
|
|
648
|
+
error,
|
|
649
|
+
));
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
602
652
|
if let Some(previous) = successful_agents.pop() {
|
|
603
653
|
let error = format!(
|
|
604
654
|
"session_disappeared_after_spawn: provider_resume_exited for {}; session {} disappeared before spawning {}",
|
|
@@ -750,6 +800,38 @@ fn restart_with_selected_team_and_transport(
|
|
|
750
800
|
})
|
|
751
801
|
.map(|agent| agent.agent_id.as_str().to_string())
|
|
752
802
|
.collect::<Vec<_>>();
|
|
803
|
+
// 0.5.40 Slice 3 (tmux-server-death-locate §7 Slice 3): restore the
|
|
804
|
+
// pre-spawn agents snapshot when we deferred the teardown AND the
|
|
805
|
+
// replacement build failed. This is the "old session/state stay
|
|
806
|
+
// authoritative on failure" invariant made concrete — the mid-loop
|
|
807
|
+
// mark_agent_respawned/mark_agent_restart_failed calls already
|
|
808
|
+
// mutated `state.agents` in memory; restoring the snapshot rewinds
|
|
809
|
+
// those mutations so `save_restart_state_...` below persists rows
|
|
810
|
+
// byte-identical to what was loaded. Only rewind when
|
|
811
|
+
// `fatal_resume_failure` (whole cohort refused) OR when NO agent
|
|
812
|
+
// survived (`successful_agents.is_empty()` with failures) — on
|
|
813
|
+
// partial success the surviving replacements are legitimately new
|
|
814
|
+
// and the pre-restart rows are stale.
|
|
815
|
+
if let Some(snapshot) = build_before_destroy_agents_snapshot.as_ref() {
|
|
816
|
+
let replacement_failed =
|
|
817
|
+
fatal_resume_failure || (!failed_agents.is_empty() && successful_agents.is_empty());
|
|
818
|
+
if replacement_failed {
|
|
819
|
+
if let Some(state_obj) = state.as_object_mut() {
|
|
820
|
+
state_obj.insert("agents".to_string(), snapshot.clone());
|
|
821
|
+
if let Some(teams) = state_obj
|
|
822
|
+
.get_mut("teams")
|
|
823
|
+
.and_then(serde_json::Value::as_object_mut)
|
|
824
|
+
{
|
|
825
|
+
if let Some(team_entry) = teams
|
|
826
|
+
.get_mut(selected.team_key.as_str())
|
|
827
|
+
.and_then(serde_json::Value::as_object_mut)
|
|
828
|
+
{
|
|
829
|
+
team_entry.insert("agents".to_string(), snapshot.clone());
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
753
835
|
save_restart_state_with_lifecycle_topology_authority_and_capture_backfill_skip(
|
|
754
836
|
&selected.run_workspace,
|
|
755
837
|
&mut state,
|
|
@@ -3073,6 +3155,32 @@ fn load_endpoint_convergence_runtime_spec(
|
|
|
3073
3155
|
Ok(Some(spec))
|
|
3074
3156
|
}
|
|
3075
3157
|
|
|
3158
|
+
/// 0.5.40 Slice 3: collect the set of agent ids that state marks as
|
|
3159
|
+
/// running with a real pane_id. "Running with pane_id" is the
|
|
3160
|
+
/// discriminator between "state has an authoritative live team the
|
|
3161
|
+
/// user would lose on teardown" and "state is fresh / all stopped and
|
|
3162
|
+
/// the live session is just leftover empty topology". Sorted for
|
|
3163
|
+
/// deterministic report output.
|
|
3164
|
+
fn collect_live_agents_from_state(state: &serde_json::Value) -> Vec<String> {
|
|
3165
|
+
let Some(agents) = state.get("agents").and_then(serde_json::Value::as_object) else {
|
|
3166
|
+
return Vec::new();
|
|
3167
|
+
};
|
|
3168
|
+
let mut live: Vec<String> = agents
|
|
3169
|
+
.iter()
|
|
3170
|
+
.filter_map(|(agent_id, agent)| {
|
|
3171
|
+
let status = agent.get("status").and_then(serde_json::Value::as_str)?;
|
|
3172
|
+
let pane_id = agent
|
|
3173
|
+
.get("pane_id")
|
|
3174
|
+
.and_then(serde_json::Value::as_str)
|
|
3175
|
+
.filter(|s| !s.is_empty())?;
|
|
3176
|
+
let _ = pane_id;
|
|
3177
|
+
(status == "running").then(|| agent_id.clone())
|
|
3178
|
+
})
|
|
3179
|
+
.collect();
|
|
3180
|
+
live.sort();
|
|
3181
|
+
live
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3076
3184
|
fn has_endpoint_convergence_marker(state: &serde_json::Value) -> bool {
|
|
3077
3185
|
state
|
|
3078
3186
|
.get("topology_convergence")
|
|
@@ -745,6 +745,27 @@ pub enum RestartReport {
|
|
|
745
745
|
allow_fresh: bool,
|
|
746
746
|
error: String,
|
|
747
747
|
},
|
|
748
|
+
/// 0.5.40 Slice 3 (tmux-server-death-locate §7 Slice 3, first-version
|
|
749
|
+
/// all-or-refuse): the previous worker session is still live AND state
|
|
750
|
+
/// carries running agents with real pane_ids. Restart today can only
|
|
751
|
+
/// proceed by killing the live session first — which loses provider
|
|
752
|
+
/// session state in the worker panes and, worse, can cascade into
|
|
753
|
+
/// server death under Case B. Until the build-before-destroy path
|
|
754
|
+
/// (later car: temporary session + minimal-viability proof + swap)
|
|
755
|
+
/// lands, restart refuses this shape *before* touching tmux or state.
|
|
756
|
+
/// **nothing created or killed; state.agents rows are byte-identical
|
|
757
|
+
/// to what was seeded before the call.**
|
|
758
|
+
RefusedBuildBeforeDestroyRequired {
|
|
759
|
+
/// Live worker session name from state that would need destructive
|
|
760
|
+
/// rebuild.
|
|
761
|
+
session_name: String,
|
|
762
|
+
/// Snapshot of agent ids currently running with real pane_ids —
|
|
763
|
+
/// this is the "authoritative old state" the refusal preserves.
|
|
764
|
+
live_agents: Vec<String>,
|
|
765
|
+
/// Human-readable error pointing the user at the canonical recovery
|
|
766
|
+
/// path (bare shutdown then restart).
|
|
767
|
+
error: String,
|
|
768
|
+
},
|
|
748
769
|
/// unit-3 (Stage 1): session-identity preflight refused — `state.session_name`
|
|
749
770
|
/// is a leader launcher session (`team-agent-leader-*`). Proceeding would
|
|
750
771
|
/// tear down the leader pane (E49 / 0.3.39). **nothing created or killed**.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.40",
|
|
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.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.5.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.5.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.5.40",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.40",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.40"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|