@team-agent/installer 0.5.65 → 0.5.68
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 +8 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/Cargo.toml +6 -0
- package/crates/team-agent/src/cli/adapters.rs +6 -1
- package/crates/team-agent/src/cli/diagnose.rs +82 -0
- package/crates/team-agent/src/cli/emit.rs +3 -3
- package/crates/team-agent/src/cli/grok_slot.rs +299 -0
- package/crates/team-agent/src/cli/leader.rs +21 -7
- package/crates/team-agent/src/cli/leaders.rs +125 -1
- package/crates/team-agent/src/cli/mod.rs +20 -0
- package/crates/team-agent/src/cli/send/presentation.rs +7 -1
- package/crates/team-agent/src/cli/spec.rs +2 -0
- package/crates/team-agent/src/cli/status_port/compact.rs +28 -0
- package/crates/team-agent/src/cli/status_port/snapshot.rs +25 -1
- package/crates/team-agent/src/cli/tests/base.rs +26 -3
- package/crates/team-agent/src/cli/tests/compile.rs +1 -1
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +129 -19
- package/crates/team-agent/src/cli/tests/mod.rs +2 -2
- package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -1
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +387 -6
- package/crates/team-agent/src/cli/tests/status_send.rs +79 -10
- package/crates/team-agent/src/cli/tests/verb_validate.rs +1 -1
- package/crates/team-agent/src/communication_mode/mod.rs +6 -4
- package/crates/team-agent/src/compiler/tests.rs +23 -20
- package/crates/team-agent/src/compiler.rs +84 -5
- package/crates/team-agent/src/conpty/backend.rs +16 -0
- package/crates/team-agent/src/coordinator/backoff.rs +55 -0
- package/crates/team-agent/src/coordinator/conpty_shim.rs +82 -0
- package/crates/team-agent/src/coordinator/health.rs +173 -0
- package/crates/team-agent/src/coordinator/mod.rs +31 -0
- package/crates/team-agent/src/coordinator/orphan.rs +31 -0
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +30 -0
- package/crates/team-agent/src/coordinator/runtime_observation.rs +25 -0
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +59 -0
- package/crates/team-agent/src/coordinator/steps/delivery.rs +10 -0
- package/crates/team-agent/src/coordinator/steps/health_sync.rs +10 -0
- package/crates/team-agent/src/coordinator/steps/mod.rs +22 -0
- package/crates/team-agent/src/coordinator/steps/persist.rs +10 -0
- package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +10 -0
- package/crates/team-agent/src/coordinator/steps/session_gate.rs +10 -0
- package/crates/team-agent/src/coordinator/tick.rs +133 -22
- package/crates/team-agent/src/coordinator/types.rs +49 -0
- package/crates/team-agent/src/db/message_store.rs +39 -5
- package/crates/team-agent/src/layout/worker_env.rs +20 -3
- package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
- package/crates/team-agent/src/leader/provider_attribution.rs +53 -0
- package/crates/team-agent/src/leader/registry.rs +65 -0
- package/crates/team-agent/src/leader/start.rs +1080 -58
- package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/display.rs +56 -0
- package/crates/team-agent/src/lifecycle/helpers.rs +57 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +332 -14
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +69 -2
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +196 -2
- package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +101 -11
- package/crates/team-agent/src/lifecycle/launch/cursor_create_chat.rs +229 -0
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +332 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +204 -457
- package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +24 -0
- package/crates/team-agent/src/lifecycle/launch/grok_per_seat.rs +260 -0
- package/crates/team-agent/src/lifecycle/launch/identity.rs +146 -0
- package/crates/team-agent/src/lifecycle/launch/layout.rs +60 -0
- package/crates/team-agent/src/lifecycle/launch/leader_context.rs +112 -0
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +530 -0
- package/crates/team-agent/src/lifecycle/launch/ownership.rs +40 -0
- package/crates/team-agent/src/lifecycle/launch/plan.rs +31 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +66 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +78 -0
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +85 -38
- package/crates/team-agent/src/lifecycle/launch/role_source.rs +44 -44
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +101 -4
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +244 -48
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +109 -2
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +214 -1
- package/crates/team-agent/src/lifecycle/launch.rs +209 -43
- package/crates/team-agent/src/lifecycle/lock.rs +42 -1
- package/crates/team-agent/src/lifecycle/mod.rs +11 -0
- package/crates/team-agent/src/lifecycle/pane_input_lock.rs +161 -0
- package/crates/team-agent/src/lifecycle/profile_launch.rs +98 -0
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +51 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +213 -3
- package/crates/team-agent/src/lifecycle/restart/common.rs +373 -18
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +27 -0
- package/crates/team-agent/src/lifecycle/restart/preflight.rs +25 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +165 -15
- package/crates/team-agent/src/lifecycle/restart/remove.rs +196 -5
- package/crates/team-agent/src/lifecycle/restart/selection.rs +72 -0
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +22 -0
- package/crates/team-agent/src/lifecycle/restart.rs +29 -0
- package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +3 -4
- package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +4 -4
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +309 -0
- package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +159 -174
- package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +15 -13
- package/crates/team-agent/src/lifecycle/tests/core.rs +4 -18
- package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +287 -0
- package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +229 -0
- package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +353 -0
- package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +784 -0
- package/crates/team-agent/src/lifecycle/tests/gate_fixtures.rs +562 -0
- package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +239 -0
- package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +498 -0
- package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +250 -0
- package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +293 -0
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +3 -3
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +65 -40
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +20 -32
- package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +17 -9
- package/crates/team-agent/src/lifecycle/tests/mcp_tool_name_format_red.rs +69 -0
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +24 -23
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +98 -62
- package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +3 -1
- package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +41 -8
- package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +3 -3
- package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +60 -2
- package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +69 -26
- package/crates/team-agent/src/lifecycle/tests.rs +23 -15
- package/crates/team-agent/src/lifecycle/types.rs +61 -2
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +200 -33
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +100 -1
- package/crates/team-agent/src/mcp_server/tests.rs +196 -2
- package/crates/team-agent/src/messaging/delivery.rs +405 -40
- package/crates/team-agent/src/messaging/helpers.rs +2 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +53 -1
- package/crates/team-agent/src/messaging/results.rs +4 -0
- package/crates/team-agent/src/messaging/send.rs +34 -0
- package/crates/team-agent/src/messaging/tests/dup_inject.rs +406 -0
- package/crates/team-agent/src/messaging/tests/e23.rs +9 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +66 -0
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +1 -1
- package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
- package/crates/team-agent/src/messaging/types.rs +4 -1
- package/crates/team-agent/src/model/enums.rs +14 -5
- package/crates/team-agent/src/model/permissions.rs +3 -0
- package/crates/team-agent/src/model/spec.rs +129 -6
- package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +3 -1
- package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +3 -1
- package/crates/team-agent/src/model/testdata/team.spec.yaml +3 -1
- package/crates/team-agent/src/os_probe.rs +73 -2
- package/crates/team-agent/src/provider/adapter.rs +231 -4
- package/crates/team-agent/src/provider/adapters/claude.rs +5 -2
- package/crates/team-agent/src/provider/adapters/codex.rs +5 -2
- package/crates/team-agent/src/provider/adapters/copilot.rs +5 -2
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +86 -0
- package/crates/team-agent/src/provider/adapters/grok.rs +157 -0
- package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
- package/crates/team-agent/src/provider/bypass_flags.rs +124 -0
- package/crates/team-agent/src/provider/classify.rs +4 -2
- package/crates/team-agent/src/provider/faults.rs +2 -1
- package/crates/team-agent/src/provider/mod.rs +13 -0
- package/crates/team-agent/src/provider/session/capture.rs +158 -20
- package/crates/team-agent/src/provider/session/context_fork/claude.rs +38 -1
- package/crates/team-agent/src/provider/session/context_fork/codex.rs +72 -1
- package/crates/team-agent/src/provider/session/context_fork/outcome.rs +57 -1
- package/crates/team-agent/src/provider/session/context_fork.rs +77 -3
- package/crates/team-agent/src/provider/session/mod.rs +18 -0
- package/crates/team-agent/src/provider/session/resume.rs +57 -0
- package/crates/team-agent/src/provider/session_scan/claude.rs +125 -1
- package/crates/team-agent/src/provider/session_scan/codex.rs +94 -1
- package/crates/team-agent/src/provider/session_scan/common.rs +204 -2
- package/crates/team-agent/src/provider/session_scan/copilot.rs +33 -1
- package/crates/team-agent/src/provider/session_scan/cursor.rs +538 -0
- package/crates/team-agent/src/provider/session_scan/grok.rs +288 -0
- package/crates/team-agent/src/provider/session_scan.rs +8 -0
- package/crates/team-agent/src/provider/submit_now.rs +94 -0
- package/crates/team-agent/src/provider/tests/adapter.rs +304 -0
- package/crates/team-agent/src/provider/types.rs +4 -2
- package/crates/team-agent/src/provider/wire.rs +23 -1
- package/crates/team-agent/src/state/persist.rs +301 -8
- package/crates/team-agent/src/state/repository.rs +5 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +1737 -42
- package/crates/team-agent/src/tmux_backend.rs +1083 -75
- package/crates/team-agent/src/transport/test_support.rs +20 -0
- package/crates/team-agent/src/transport/tests/wire.rs +28 -2
- package/crates/team-agent/src/transport.rs +265 -1
- package/npm/install.mjs +129 -73
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +33 -238
- package/skills/team-agent/command-coverage.json +31 -0
- package/crates/team-agent/src/lifecycle/launch/approval.rs +0 -134
- package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +0 -59
- package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +0 -483
- package/crates/team-agent/src/lifecycle/launch/fork_pending.rs +0 -109
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +0 -447
- package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +0 -1187
- package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +0 -327
|
@@ -882,6 +882,16 @@ fn merge_agent_projection(
|
|
|
882
882
|
if !tombstoned_for_projection
|
|
883
883
|
&& latest_has_protected_live_topology(latest_agent, team_alive)
|
|
884
884
|
{
|
|
885
|
+
// A lifecycle-authoritative writer may have loaded its whole
|
|
886
|
+
// projection before this peer finalized. Preserve the complete
|
|
887
|
+
// latest peer row rather than treating that untouched omission
|
|
888
|
+
// as an intentional delete. Ordinary saves (with no topology
|
|
889
|
+
// authority) retain the conflict so omission cannot clobber a
|
|
890
|
+
// live row.
|
|
891
|
+
if !topology_update_agent_ids.is_empty() {
|
|
892
|
+
slot.insert(latest_agent.clone());
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
885
895
|
return Err(save_conflict(
|
|
886
896
|
projection,
|
|
887
897
|
agent_id,
|
|
@@ -896,7 +906,20 @@ fn merge_agent_projection(
|
|
|
896
906
|
if !tombstoned_for_projection && !topology_update_agent_ids.contains(agent_id) {
|
|
897
907
|
let fields = topology_conflict_fields(existing.get(), latest_agent, team_alive);
|
|
898
908
|
if !fields.is_empty() {
|
|
899
|
-
|
|
909
|
+
if !topology_update_agent_ids.is_empty()
|
|
910
|
+
&& lifecycle_peer_snapshot_is_stale(existing.get(), latest_agent)
|
|
911
|
+
{
|
|
912
|
+
// A lifecycle writer may have loaded a complete
|
|
913
|
+
// peer row before that peer finalized. Its own
|
|
914
|
+
// topology authority is limited to the IDs it
|
|
915
|
+
// explicitly reports; preserve the latest peer
|
|
916
|
+
// row instead of surfacing a stale self-conflict.
|
|
917
|
+
preserve_latest_topology(existing.get_mut(), latest_agent);
|
|
918
|
+
} else if lifecycle_placeholder(existing.get()) {
|
|
919
|
+
preserve_latest_topology(existing.get_mut(), latest_agent);
|
|
920
|
+
} else {
|
|
921
|
+
return Err(save_conflict(projection, agent_id, fields));
|
|
922
|
+
}
|
|
900
923
|
}
|
|
901
924
|
}
|
|
902
925
|
if !skip_capture_backfill || !skip_capture_backfill_agent_ids.contains(agent_id) {
|
|
@@ -908,6 +931,58 @@ fn merge_agent_projection(
|
|
|
908
931
|
Ok(())
|
|
909
932
|
}
|
|
910
933
|
|
|
934
|
+
fn lifecycle_placeholder(agent: &Value) -> bool {
|
|
935
|
+
matches!(
|
|
936
|
+
agent.get("status").and_then(Value::as_str),
|
|
937
|
+
Some("reserved" | "starting")
|
|
938
|
+
)
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
fn lifecycle_peer_snapshot_is_stale(incoming: &Value, latest: &Value) -> bool {
|
|
942
|
+
// A concurrent clone/restart can have promoted its peer to `running`
|
|
943
|
+
// while its capture tuple is still pending. That row is still a
|
|
944
|
+
// lifecycle snapshot, not a completed topology observation; preserving
|
|
945
|
+
// the latest peer avoids making the concurrent writer lose on a pane
|
|
946
|
+
// binding it never owned. A completed row remains epoch-ordered below,
|
|
947
|
+
// so a newer/equal completed topology still conflicts.
|
|
948
|
+
if incoming
|
|
949
|
+
.get("_pending_session_id")
|
|
950
|
+
.and_then(Value::as_str)
|
|
951
|
+
.is_some_and(|session| !session.is_empty())
|
|
952
|
+
{
|
|
953
|
+
return true;
|
|
954
|
+
}
|
|
955
|
+
let Some(incoming_epoch) = incoming.get("spawn_epoch").and_then(Value::as_u64) else {
|
|
956
|
+
return false;
|
|
957
|
+
};
|
|
958
|
+
let Some(latest_epoch) = latest.get("spawn_epoch").and_then(Value::as_u64) else {
|
|
959
|
+
return false;
|
|
960
|
+
};
|
|
961
|
+
incoming_epoch < latest_epoch
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
fn preserve_latest_topology(incoming: &mut Value, latest: &Value) {
|
|
965
|
+
let Some(incoming) = incoming.as_object_mut() else {
|
|
966
|
+
return;
|
|
967
|
+
};
|
|
968
|
+
// A stale add/clone writer can carry a placeholder row (`starting`) after
|
|
969
|
+
// a peer has completed its spawn. Preserve the completed lifecycle and
|
|
970
|
+
// capture tuple together with topology; otherwise a later peer save can
|
|
971
|
+
// regress the finalized row back to `starting` while returning success.
|
|
972
|
+
if latest.get("status").and_then(Value::as_str) == Some("running") {
|
|
973
|
+
// The finalized row is authoritative as a whole. Copying a hand-picked
|
|
974
|
+
// field list can leave stale capture/metadata keys from the placeholder
|
|
975
|
+
// and is not equivalent to preserving the peer that already finalized.
|
|
976
|
+
*incoming = latest.as_object().cloned().unwrap_or_default();
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
for field in LIVE_TOPOLOGY_FIELDS {
|
|
980
|
+
if let Some(value) = latest.get(field).filter(|value| json_truthy(value)) {
|
|
981
|
+
incoming.insert(field.to_string(), value.clone());
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
911
986
|
fn save_conflict(projection: &str, agent_id: &str, fields: Vec<&'static str>) -> StateError {
|
|
912
987
|
StateError::SaveConflict(format!(
|
|
913
988
|
"agent_id={agent_id} projection={projection} conflicting_fields={}",
|
|
@@ -2003,13 +2078,8 @@ mod tests {
|
|
|
2003
2078
|
"target": {"agent_id": "target", "provider": "codex", "status": "stopped"}
|
|
2004
2079
|
},
|
|
2005
2080
|
});
|
|
2006
|
-
save_runtime_state_with_lifecycle_topology_authority(
|
|
2007
|
-
|
|
2008
|
-
&incoming,
|
|
2009
|
-
"team-a",
|
|
2010
|
-
&["target"],
|
|
2011
|
-
)
|
|
2012
|
-
.unwrap();
|
|
2081
|
+
save_runtime_state_with_lifecycle_topology_authority(&ws, &incoming, "team-a", &["target"])
|
|
2082
|
+
.unwrap();
|
|
2013
2083
|
let target = read_state(&ws).pointer("/agents/target").cloned().unwrap();
|
|
2014
2084
|
assert!(
|
|
2015
2085
|
target.get("pane_id").is_none(),
|
|
@@ -2021,6 +2091,229 @@ mod tests {
|
|
|
2021
2091
|
);
|
|
2022
2092
|
}
|
|
2023
2093
|
|
|
2094
|
+
#[test]
|
|
2095
|
+
fn lifecycle_authority_preserves_stale_omitted_finalized_peer() {
|
|
2096
|
+
let latest_state = || {
|
|
2097
|
+
json!({
|
|
2098
|
+
"session_name": "team-a",
|
|
2099
|
+
"active_team_key": "team-a",
|
|
2100
|
+
"agents": {
|
|
2101
|
+
"f1": {
|
|
2102
|
+
"agent_id": "f1",
|
|
2103
|
+
"provider": "codex",
|
|
2104
|
+
"role": "Developer",
|
|
2105
|
+
"model": "gpt-5.5",
|
|
2106
|
+
"status": "running",
|
|
2107
|
+
"session_id": "session-f1",
|
|
2108
|
+
"rollout_path": "/tmp/f1.jsonl",
|
|
2109
|
+
"captured_at": "2026-08-28T00:00:00Z",
|
|
2110
|
+
"captured_via": "contract-fixture",
|
|
2111
|
+
"attribution_confidence": "high",
|
|
2112
|
+
"capture_state": "complete",
|
|
2113
|
+
"spawn_cwd": "/tmp/f1",
|
|
2114
|
+
"owner_team_id": "team-a",
|
|
2115
|
+
"pane_id": "%1",
|
|
2116
|
+
"pane_pid": 101,
|
|
2117
|
+
"window": "f1",
|
|
2118
|
+
"spawned_at": "2026-08-28T00:00:01Z",
|
|
2119
|
+
"spawn_epoch": 1
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
})
|
|
2123
|
+
};
|
|
2124
|
+
let f2 = json!({
|
|
2125
|
+
"agent_id": "f2",
|
|
2126
|
+
"provider": "codex",
|
|
2127
|
+
"role": "Developer",
|
|
2128
|
+
"model": "gpt-5.5",
|
|
2129
|
+
"status": "running",
|
|
2130
|
+
"session_id": "session-f2",
|
|
2131
|
+
"rollout_path": "/tmp/f2.jsonl",
|
|
2132
|
+
"captured_at": "2026-08-28T00:00:02Z",
|
|
2133
|
+
"captured_via": "contract-fixture",
|
|
2134
|
+
"attribution_confidence": "high",
|
|
2135
|
+
"capture_state": "complete",
|
|
2136
|
+
"spawn_cwd": "/tmp/f2",
|
|
2137
|
+
"owner_team_id": "team-a",
|
|
2138
|
+
"pane_id": "%2",
|
|
2139
|
+
"pane_pid": 102,
|
|
2140
|
+
"window": "f2",
|
|
2141
|
+
"spawned_at": "2026-08-28T00:00:03Z",
|
|
2142
|
+
"spawn_epoch": 1
|
|
2143
|
+
});
|
|
2144
|
+
|
|
2145
|
+
// A stale f2 finalization omitted the peer that finalized after its
|
|
2146
|
+
// snapshot. Lifecycle authority preserves f1 byte-for-byte and writes f2.
|
|
2147
|
+
let ws = temp_ws();
|
|
2148
|
+
let latest = latest_state();
|
|
2149
|
+
write_state(&ws, &latest);
|
|
2150
|
+
let incoming = json!({
|
|
2151
|
+
"session_name": "team-a",
|
|
2152
|
+
"active_team_key": "team-a",
|
|
2153
|
+
"agents": {"f2": f2.clone()}
|
|
2154
|
+
});
|
|
2155
|
+
save_runtime_state_with_lifecycle_topology_authority(&ws, &incoming, "team-a", &["f2"])
|
|
2156
|
+
.unwrap();
|
|
2157
|
+
let saved = read_state(&ws);
|
|
2158
|
+
assert_eq!(saved["agents"]["f1"], latest["agents"]["f1"]);
|
|
2159
|
+
assert_eq!(saved["agents"]["f2"], f2);
|
|
2160
|
+
|
|
2161
|
+
// Captured R4 f1/f2 shape: the writer includes a stale, already
|
|
2162
|
+
// running f1 snapshot while finalizing f2. Lifecycle authority must
|
|
2163
|
+
// preserve f1 byte-for-byte rather than report SaveConflict for its
|
|
2164
|
+
// five topology fields.
|
|
2165
|
+
let ws = temp_ws();
|
|
2166
|
+
let latest = latest_state();
|
|
2167
|
+
write_state(&ws, &latest);
|
|
2168
|
+
let incoming = json!({
|
|
2169
|
+
"session_name": "team-a",
|
|
2170
|
+
"active_team_key": "team-a",
|
|
2171
|
+
"agents": {
|
|
2172
|
+
"f1": {
|
|
2173
|
+
"agent_id": "f1",
|
|
2174
|
+
"provider": "codex",
|
|
2175
|
+
"status": "running",
|
|
2176
|
+
"session_id": "session-f1",
|
|
2177
|
+
"rollout_path": "/tmp/f1.jsonl",
|
|
2178
|
+
"captured_at": "2026-08-28T00:00:00Z",
|
|
2179
|
+
"captured_via": "contract-fixture",
|
|
2180
|
+
"attribution_confidence": "high",
|
|
2181
|
+
"capture_state": "complete",
|
|
2182
|
+
"spawn_cwd": "/tmp/f1",
|
|
2183
|
+
"owner_team_id": "team-a",
|
|
2184
|
+
"pane_id": "%stale",
|
|
2185
|
+
"pane_pid": 999,
|
|
2186
|
+
"window": "f1-stale",
|
|
2187
|
+
"spawned_at": "2026-08-27T23:59:59Z",
|
|
2188
|
+
"spawn_epoch": 0
|
|
2189
|
+
},
|
|
2190
|
+
"f2": f2.clone()
|
|
2191
|
+
}
|
|
2192
|
+
});
|
|
2193
|
+
save_runtime_state_with_lifecycle_topology_authority(&ws, &incoming, "team-a", &["f2"])
|
|
2194
|
+
.unwrap();
|
|
2195
|
+
let saved = read_state(&ws);
|
|
2196
|
+
assert_eq!(saved["agents"]["f1"], latest["agents"]["f1"]);
|
|
2197
|
+
assert_eq!(saved["agents"]["f2"], f2);
|
|
2198
|
+
|
|
2199
|
+
// The same omission without lifecycle authority remains a conflict.
|
|
2200
|
+
let ws = temp_ws();
|
|
2201
|
+
let latest = latest_state();
|
|
2202
|
+
write_state(&ws, &latest);
|
|
2203
|
+
let incoming = json!({
|
|
2204
|
+
"session_name": "team-a",
|
|
2205
|
+
"active_team_key": "team-a",
|
|
2206
|
+
"agents": {"f2": f2.clone()}
|
|
2207
|
+
});
|
|
2208
|
+
assert!(matches!(
|
|
2209
|
+
save_runtime_state(&ws, &incoming),
|
|
2210
|
+
Err(StateError::SaveConflict(_))
|
|
2211
|
+
));
|
|
2212
|
+
assert_eq!(read_state(&ws), latest);
|
|
2213
|
+
|
|
2214
|
+
// Explicit deletion remains authoritative and must not be resurrected.
|
|
2215
|
+
let ws = temp_ws();
|
|
2216
|
+
let latest = latest_state();
|
|
2217
|
+
write_state(&ws, &latest);
|
|
2218
|
+
let incoming = json!({
|
|
2219
|
+
"session_name": "team-a",
|
|
2220
|
+
"active_team_key": "team-a",
|
|
2221
|
+
"agents": {"f2": f2}
|
|
2222
|
+
});
|
|
2223
|
+
save_runtime_state_with_deleted_agents(&ws, &incoming, &["f1"]).unwrap();
|
|
2224
|
+
let saved = read_state(&ws);
|
|
2225
|
+
assert!(saved["agents"].get("f1").is_none());
|
|
2226
|
+
assert!(saved["agents"].get("f2").is_some());
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
#[test]
|
|
2230
|
+
fn lifecycle_authority_preserves_complete_latest_row_for_starting_peer() {
|
|
2231
|
+
let ws = temp_ws();
|
|
2232
|
+
let latest = json!({
|
|
2233
|
+
"session_name": "team-a",
|
|
2234
|
+
"active_team_key": "team-a",
|
|
2235
|
+
"agents": {
|
|
2236
|
+
"f1": {
|
|
2237
|
+
"agent_id": "f1",
|
|
2238
|
+
"provider": "codex",
|
|
2239
|
+
"status": "running",
|
|
2240
|
+
"session_id": "session-f1",
|
|
2241
|
+
"rollout_path": "/tmp/f1.jsonl",
|
|
2242
|
+
"captured_at": "2026-08-28T00:00:00Z",
|
|
2243
|
+
"captured_via": "contract-fixture",
|
|
2244
|
+
"attribution_confidence": "high",
|
|
2245
|
+
"capture_state": "complete",
|
|
2246
|
+
"spawn_cwd": "/tmp/f1",
|
|
2247
|
+
"owner_team_id": "team-a",
|
|
2248
|
+
"pane_id": "%1",
|
|
2249
|
+
"pane_pid": 101,
|
|
2250
|
+
"window": "f1",
|
|
2251
|
+
"spawned_at": "2026-08-28T00:00:01Z",
|
|
2252
|
+
"spawn_epoch": 1
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
});
|
|
2256
|
+
write_state(&ws, &latest);
|
|
2257
|
+
let incoming = json!({
|
|
2258
|
+
"session_name": "team-a",
|
|
2259
|
+
"active_team_key": "team-a",
|
|
2260
|
+
"agents": {
|
|
2261
|
+
"f1": {
|
|
2262
|
+
"agent_id": "f1",
|
|
2263
|
+
"provider": "codex",
|
|
2264
|
+
"status": "starting",
|
|
2265
|
+
"stale_metadata": "must-not-survive"
|
|
2266
|
+
},
|
|
2267
|
+
"f2": {"agent_id": "f2", "status": "starting"}
|
|
2268
|
+
}
|
|
2269
|
+
});
|
|
2270
|
+
save_runtime_state_with_lifecycle_topology_authority(&ws, &incoming, "team-a", &["f2"])
|
|
2271
|
+
.unwrap();
|
|
2272
|
+
assert_eq!(read_state(&ws)["agents"]["f1"], latest["agents"]["f1"]);
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
#[test]
|
|
2276
|
+
fn lifecycle_authority_still_conflicts_on_different_completed_topology() {
|
|
2277
|
+
let ws = temp_ws();
|
|
2278
|
+
let latest = json!({
|
|
2279
|
+
"session_name": "team-a",
|
|
2280
|
+
"active_team_key": "team-a",
|
|
2281
|
+
"agents": {
|
|
2282
|
+
"f1": {
|
|
2283
|
+
"agent_id": "f1",
|
|
2284
|
+
"status": "running",
|
|
2285
|
+
"pane_id": "%1",
|
|
2286
|
+
"pane_pid": 101,
|
|
2287
|
+
"window": "f1",
|
|
2288
|
+
"spawned_at": "2026-08-28T00:00:01Z",
|
|
2289
|
+
"spawn_epoch": 1
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
});
|
|
2293
|
+
write_state(&ws, &latest);
|
|
2294
|
+
let incoming = json!({
|
|
2295
|
+
"session_name": "team-a",
|
|
2296
|
+
"active_team_key": "team-a",
|
|
2297
|
+
"agents": {
|
|
2298
|
+
"f1": {
|
|
2299
|
+
"agent_id": "f1",
|
|
2300
|
+
"status": "running",
|
|
2301
|
+
"pane_id": "%9",
|
|
2302
|
+
"pane_pid": 109,
|
|
2303
|
+
"window": "f1-new",
|
|
2304
|
+
"spawned_at": "2026-08-28T00:00:09Z",
|
|
2305
|
+
"spawn_epoch": 9
|
|
2306
|
+
},
|
|
2307
|
+
"f2": {"agent_id": "f2", "status": "running"}
|
|
2308
|
+
}
|
|
2309
|
+
});
|
|
2310
|
+
let err =
|
|
2311
|
+
save_runtime_state_with_lifecycle_topology_authority(&ws, &incoming, "team-a", &["f2"])
|
|
2312
|
+
.expect_err("two completed topologies for f1 must conflict");
|
|
2313
|
+
assert!(matches!(err, StateError::SaveConflict(_)));
|
|
2314
|
+
assert!(err.to_string().contains("agent_id=f1"));
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2024
2317
|
#[test]
|
|
2025
2318
|
fn owner_epoch_preserve_is_explicit_and_bounded_to_team_entry() {
|
|
2026
2319
|
let ws = temp_ws();
|
|
@@ -235,6 +235,7 @@ pub enum StateWriteIntent<'a> {
|
|
|
235
235
|
MessagingTurnArm {
|
|
236
236
|
owner_team_id: Option<&'a str>,
|
|
237
237
|
},
|
|
238
|
+
MessagingSessionCapture,
|
|
238
239
|
ResultCollection {
|
|
239
240
|
owner_team_id: Option<&'a str>,
|
|
240
241
|
},
|
|
@@ -415,6 +416,10 @@ fn route_direct(
|
|
|
415
416
|
},
|
|
416
417
|
// MessagingTurnArm -> root save (messaging/activity.rs:83).
|
|
417
418
|
StateWriteIntent::MessagingTurnArm { .. } => helper_write_root(workspace, state),
|
|
419
|
+
// MessagingSessionCapture -> root save; the post-send capture backfill
|
|
420
|
+
// at messaging/send.rs used the root helper directly before it was
|
|
421
|
+
// migrated onto the repository.
|
|
422
|
+
StateWriteIntent::MessagingSessionCapture => helper_write_root(workspace, state),
|
|
418
423
|
// ResultCollection direct-write path uses `save_team_scoped_state`
|
|
419
424
|
// when an owner_team_id is bound and `save_runtime_state` at the root
|
|
420
425
|
// otherwise (messaging/results.rs:201/211). The reapply-shaped
|