@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
|
@@ -18,13 +18,15 @@ pub fn start_agent(
|
|
|
18
18
|
team: Option<&str>,
|
|
19
19
|
) -> Result<StartAgentOutcome, LifecycleError> {
|
|
20
20
|
let paths = lifecycle_paths(workspace, team)?;
|
|
21
|
+
let canonical_team = paths.canonical_team(team).map(str::to_string);
|
|
22
|
+
let team = canonical_team.as_deref();
|
|
21
23
|
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
22
24
|
workspace: &paths.run_workspace,
|
|
23
25
|
operation: "start-agent",
|
|
24
26
|
team,
|
|
25
27
|
agent_id: Some(agent_id),
|
|
26
28
|
})?;
|
|
27
|
-
let transport =
|
|
29
|
+
let transport = paths.tmux_backend()?;
|
|
28
30
|
start_agent_at_paths(
|
|
29
31
|
&paths.run_workspace,
|
|
30
32
|
&paths.spec_workspace,
|
|
@@ -54,9 +56,12 @@ pub fn start_agent_with_transport(
|
|
|
54
56
|
Err(_) if team.is_none() => LifecyclePaths {
|
|
55
57
|
run_workspace: workspace.to_path_buf(),
|
|
56
58
|
spec_workspace: workspace.to_path_buf(),
|
|
59
|
+
selected: None,
|
|
57
60
|
},
|
|
58
61
|
Err(error) => return Err(error),
|
|
59
62
|
};
|
|
63
|
+
let canonical_team = paths.canonical_team(team).map(str::to_string);
|
|
64
|
+
let team = canonical_team.as_deref();
|
|
60
65
|
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
61
66
|
workspace: &paths.run_workspace,
|
|
62
67
|
operation: "start-agent",
|
|
@@ -778,13 +783,15 @@ pub fn stop_agent(
|
|
|
778
783
|
team: Option<&str>,
|
|
779
784
|
) -> Result<StopAgentReport, LifecycleError> {
|
|
780
785
|
let paths = lifecycle_paths(workspace, team)?;
|
|
786
|
+
let canonical_team = paths.canonical_team(team).map(str::to_string);
|
|
787
|
+
let team = canonical_team.as_deref();
|
|
781
788
|
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
782
789
|
workspace: &paths.run_workspace,
|
|
783
790
|
operation: "stop-agent",
|
|
784
791
|
team,
|
|
785
792
|
agent_id: Some(agent_id),
|
|
786
793
|
})?;
|
|
787
|
-
let transport =
|
|
794
|
+
let transport = paths.tmux_backend()?;
|
|
788
795
|
stop_agent_at_paths(
|
|
789
796
|
&paths.run_workspace,
|
|
790
797
|
&paths.spec_workspace,
|
|
@@ -801,6 +808,8 @@ pub fn stop_agent_with_transport(
|
|
|
801
808
|
transport: &dyn crate::transport::Transport,
|
|
802
809
|
) -> Result<StopAgentReport, LifecycleError> {
|
|
803
810
|
let paths = lifecycle_paths(workspace, team)?;
|
|
811
|
+
let canonical_team = paths.canonical_team(team).map(str::to_string);
|
|
812
|
+
let team = canonical_team.as_deref();
|
|
804
813
|
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
805
814
|
workspace: &paths.run_workspace,
|
|
806
815
|
operation: "stop-agent",
|
|
@@ -840,9 +849,12 @@ pub(crate) fn start_agent_at_paths_for_recovery(
|
|
|
840
849
|
Err(_) if team.is_none() => LifecyclePaths {
|
|
841
850
|
run_workspace: workspace.to_path_buf(),
|
|
842
851
|
spec_workspace: workspace.to_path_buf(),
|
|
852
|
+
selected: None,
|
|
843
853
|
},
|
|
844
854
|
Err(error) => return Err(RecoveryError::Lifecycle(error.to_string())),
|
|
845
855
|
};
|
|
856
|
+
let canonical_team = paths.canonical_team(team).map(str::to_string);
|
|
857
|
+
let team = canonical_team.as_deref();
|
|
846
858
|
// Best-effort stop: if the live pane still exists we tear it down so the
|
|
847
859
|
// subsequent start creates a fresh provider process instead of a Noop.
|
|
848
860
|
// Stop errors are absorbed into the start attempt result — the important
|
|
@@ -884,26 +896,38 @@ pub(super) fn stop_agent_at_paths(
|
|
|
884
896
|
transport: &dyn crate::transport::Transport,
|
|
885
897
|
) -> Result<StopAgentReport, LifecycleError> {
|
|
886
898
|
// golden operations.py:64-66: resolve_team_scoped_state -> owner gate, BEFORE the unknown-worker raise.
|
|
887
|
-
let
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
let
|
|
892
|
-
let
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
.
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
899
|
+
let seat = super::remove::resolve_seat(workspace, spec_workspace, agent_id, team, transport)?;
|
|
900
|
+
if seat.consistency == super::remove::SeatConsistency::Absent {
|
|
901
|
+
return Err(unknown_worker(agent_id));
|
|
902
|
+
}
|
|
903
|
+
let mut state = seat.state;
|
|
904
|
+
let spec = seat.spec;
|
|
905
|
+
let state_agent = state.get("agents").and_then(|v| v.get(agent_id.as_str()));
|
|
906
|
+
// Persisted-only seats must remain stoppable. Build the minimal provider
|
|
907
|
+
// projection consumed by mark_agent_stopped instead of requiring a spec
|
|
908
|
+
// row that is precisely what recovery is repairing.
|
|
909
|
+
let fallback_agent = YamlValue::Map(vec![(
|
|
910
|
+
"provider".to_string(),
|
|
911
|
+
YamlValue::Str(
|
|
912
|
+
state_agent
|
|
913
|
+
.and_then(|agent| agent.get("provider"))
|
|
914
|
+
.and_then(|value| value.as_str())
|
|
915
|
+
.unwrap_or("codex")
|
|
916
|
+
.to_string(),
|
|
917
|
+
),
|
|
918
|
+
)]);
|
|
919
|
+
let agent = find_spec_agent(&spec, agent_id).unwrap_or(&fallback_agent);
|
|
920
|
+
let session_name = seat.session;
|
|
921
|
+
let window = seat.window;
|
|
922
|
+
let pane_id = seat.physical.map(|pane| pane.pane_id).or_else(|| {
|
|
923
|
+
state
|
|
924
|
+
.get("agents")
|
|
925
|
+
.and_then(|agents| agents.get(agent_id.as_str()))
|
|
926
|
+
.and_then(|agent| agent.get("pane_id"))
|
|
927
|
+
.and_then(serde_json::Value::as_str)
|
|
928
|
+
.filter(|pane| !pane.is_empty())
|
|
929
|
+
.map(crate::transport::PaneId::new)
|
|
930
|
+
});
|
|
907
931
|
let target_str = pane_id
|
|
908
932
|
.as_ref()
|
|
909
933
|
.map(|pane| pane.as_str().to_string())
|
|
@@ -995,12 +1019,15 @@ pub(super) fn stop_agent_at_paths(
|
|
|
995
1019
|
}
|
|
996
1020
|
close_agent_display(&mut state, agent_id);
|
|
997
1021
|
mark_agent_stopped(&mut state, agent_id, agent, &window)?;
|
|
1022
|
+
let team_key = restart_projection_team_key(&state, team);
|
|
998
1023
|
// golden operations.py:95: save_team_scoped_state (team projection) — NOT a raw save, so a
|
|
999
1024
|
// multi-team workspace keeps the other teams' persisted runtime state instead of being clobbered.
|
|
1000
|
-
crate::state::
|
|
1001
|
-
|
|
1025
|
+
crate::state::repository::StateRepository::new(workspace).save(
|
|
1026
|
+
crate::state::repository::StateWriteIntent::StopAgent {
|
|
1027
|
+
team_key: &team_key,
|
|
1028
|
+
agent_id: agent_id.as_str(),
|
|
1029
|
+
},
|
|
1002
1030
|
&state,
|
|
1003
|
-
&[agent_id.as_str()],
|
|
1004
1031
|
)
|
|
1005
1032
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
1006
1033
|
// golden operations.py:96-99: snapshot (side-effect), then state_file = write_team_state path.
|
|
@@ -1202,13 +1229,15 @@ pub fn reset_agent(
|
|
|
1202
1229
|
});
|
|
1203
1230
|
}
|
|
1204
1231
|
let paths = lifecycle_paths(workspace, team)?;
|
|
1232
|
+
let canonical_team = paths.canonical_team(team).map(str::to_string);
|
|
1233
|
+
let team = canonical_team.as_deref();
|
|
1205
1234
|
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
1206
1235
|
workspace: &paths.run_workspace,
|
|
1207
1236
|
operation: "reset-agent",
|
|
1208
1237
|
team,
|
|
1209
1238
|
agent_id: Some(agent_id),
|
|
1210
1239
|
})?;
|
|
1211
|
-
let transport =
|
|
1240
|
+
let transport = paths.tmux_backend()?;
|
|
1212
1241
|
reset_agent_at_paths(
|
|
1213
1242
|
&paths.run_workspace,
|
|
1214
1243
|
&paths.spec_workspace,
|
|
@@ -1234,6 +1263,8 @@ pub fn reset_agent_with_transport(
|
|
|
1234
1263
|
});
|
|
1235
1264
|
}
|
|
1236
1265
|
let paths = lifecycle_paths(workspace, team)?;
|
|
1266
|
+
let canonical_team = paths.canonical_team(team).map(str::to_string);
|
|
1267
|
+
let team = canonical_team.as_deref();
|
|
1237
1268
|
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
1238
1269
|
workspace: &paths.run_workspace,
|
|
1239
1270
|
operation: "reset-agent",
|
|
@@ -1447,10 +1478,13 @@ fn reset_agent_at_paths(
|
|
|
1447
1478
|
sync_restart_team_projections(&mut state, &team_key);
|
|
1448
1479
|
// golden operations.py (reset): save_team_scoped_state on the team projection — same multi-team
|
|
1449
1480
|
// preservation as stop, not a raw save_runtime_state.
|
|
1450
|
-
crate::state::
|
|
1451
|
-
|
|
1481
|
+
crate::state::repository::StateRepository::new(workspace).save(
|
|
1482
|
+
crate::state::repository::StateWriteIntent::ResetAgent {
|
|
1483
|
+
team_key: &team_key,
|
|
1484
|
+
agent_id: agent_id.as_str(),
|
|
1485
|
+
discard_session,
|
|
1486
|
+
},
|
|
1452
1487
|
&state,
|
|
1453
|
-
&[agent_id.as_str()],
|
|
1454
1488
|
)
|
|
1455
1489
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
1456
1490
|
// golden operations.py:125: write_team_state after the discard-save (the intermediate stopped snapshot).
|
|
@@ -373,8 +373,8 @@ pub(super) fn spawn_agent_window(
|
|
|
373
373
|
let tmux_endpoint = transport.tmux_endpoint();
|
|
374
374
|
let event_log = crate::event_log::EventLog::new(workspace);
|
|
375
375
|
let _ = event_log.write(
|
|
376
|
-
|
|
377
|
-
serde_json::json!({
|
|
376
|
+
crate::event_log::PROVIDER_WORKER_SPAWN_ARGV,
|
|
377
|
+
crate::event_log::provider_worker_spawn_argv_fields(serde_json::json!({
|
|
378
378
|
"agent_id": agent_id.as_str(),
|
|
379
379
|
"provider": provider,
|
|
380
380
|
"argv": plan.argv,
|
|
@@ -388,7 +388,7 @@ pub(super) fn spawn_agent_window(
|
|
|
388
388
|
"source": "restart",
|
|
389
389
|
"tmux_endpoint": tmux_endpoint,
|
|
390
390
|
"tmux_endpoint_source": tmux_endpoint_source.unwrap_or("transport"),
|
|
391
|
-
}),
|
|
391
|
+
})),
|
|
392
392
|
);
|
|
393
393
|
}
|
|
394
394
|
|
|
@@ -817,11 +817,13 @@ pub(super) fn save_restart_projected_state_with_capture_backfill_skip(
|
|
|
817
817
|
topology_authority_agent_ids: &[&str],
|
|
818
818
|
) -> Result<(), LifecycleError> {
|
|
819
819
|
sync_restart_team_projections(state, team_key);
|
|
820
|
-
crate::state::
|
|
821
|
-
|
|
820
|
+
crate::state::repository::StateRepository::new(workspace).save(
|
|
821
|
+
crate::state::repository::StateWriteIntent::RestartTeam {
|
|
822
|
+
team_key,
|
|
823
|
+
topology_authority_agent_ids,
|
|
824
|
+
skip_capture_backfill_agent_ids,
|
|
825
|
+
},
|
|
822
826
|
state,
|
|
823
|
-
skip_capture_backfill_agent_ids,
|
|
824
|
-
topology_authority_agent_ids,
|
|
825
827
|
)
|
|
826
828
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))
|
|
827
829
|
}
|
|
@@ -1316,21 +1318,16 @@ pub(crate) fn converge_missing_provider_sessions(
|
|
|
1316
1318
|
poll_interval,
|
|
1317
1319
|
restart_required_missing_session_agent_ids,
|
|
1318
1320
|
|progress| {
|
|
1319
|
-
let pending_agent_ids = progress.pending_agent_ids.clone();
|
|
1320
1321
|
write_session_convergence_progress_event(
|
|
1321
1322
|
workspace,
|
|
1322
1323
|
serde_json::json!({
|
|
1323
|
-
"ts": chrono::Utc::now().to_rfc3339(),
|
|
1324
|
-
"event": "provider.session.converging",
|
|
1325
1324
|
"iteration": progress.iteration,
|
|
1326
1325
|
"elapsed_ms": progress.elapsed_ms,
|
|
1327
1326
|
"deadline_ms": progress.deadline_ms,
|
|
1328
1327
|
"changed": progress.changed,
|
|
1329
1328
|
"assigned": progress.assigned,
|
|
1330
1329
|
"missing": progress.missing,
|
|
1331
|
-
"required_missing": progress.required_missing_agent_ids.clone(),
|
|
1332
1330
|
"required_missing_agent_ids": progress.required_missing_agent_ids,
|
|
1333
|
-
"pending": pending_agent_ids,
|
|
1334
1331
|
"pending_agent_ids": progress.pending_agent_ids,
|
|
1335
1332
|
"candidate_count_by_agent": progress.candidate_count_by_agent,
|
|
1336
1333
|
"remaining_ms": progress.remaining_ms,
|
|
@@ -1344,22 +1341,11 @@ pub(crate) fn converge_missing_provider_sessions(
|
|
|
1344
1341
|
|
|
1345
1342
|
fn write_session_convergence_progress_event(
|
|
1346
1343
|
workspace: &Path,
|
|
1347
|
-
|
|
1344
|
+
fields: serde_json::Value,
|
|
1348
1345
|
) -> Result<(), String> {
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
if let Some(parent) = path.parent() {
|
|
1353
|
-
std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
|
|
1354
|
-
}
|
|
1355
|
-
let line = serde_json::to_string(&event).map_err(|e| e.to_string())?;
|
|
1356
|
-
let mut file = std::fs::OpenOptions::new()
|
|
1357
|
-
.create(true)
|
|
1358
|
-
.append(true)
|
|
1359
|
-
.open(path)
|
|
1360
|
-
.map_err(|e| e.to_string())?;
|
|
1361
|
-
file.write_all(line.as_bytes())
|
|
1362
|
-
.and_then(|_| file.write_all(b"\n"))
|
|
1346
|
+
crate::event_log::EventLog::new(workspace)
|
|
1347
|
+
.write(crate::event_log::PROVIDER_SESSION_CONVERGING, fields)
|
|
1348
|
+
.map(|_| ())
|
|
1363
1349
|
.map_err(|e| e.to_string())
|
|
1364
1350
|
}
|
|
1365
1351
|
|
|
@@ -1837,6 +1823,46 @@ mod e36_transcript_backing_tests {
|
|
|
1837
1823
|
}
|
|
1838
1824
|
}
|
|
1839
1825
|
|
|
1826
|
+
#[test]
|
|
1827
|
+
fn convergence_event_uses_central_scrub_and_preserves_required_failure() {
|
|
1828
|
+
let scratch = ScratchDir::new("event-log-central");
|
|
1829
|
+
let marker = "synthetic-convergence-marker";
|
|
1830
|
+
write_session_convergence_progress_event(
|
|
1831
|
+
scratch.path(),
|
|
1832
|
+
serde_json::json!({
|
|
1833
|
+
"diagnostic": format!("mcp_servers.demo.env.OPENAI_API_KEY=\"{marker}\""),
|
|
1834
|
+
"required_missing_agent_ids": ["worker"],
|
|
1835
|
+
"pending_agent_ids": ["worker"],
|
|
1836
|
+
}),
|
|
1837
|
+
)
|
|
1838
|
+
.expect("central EventLog write");
|
|
1839
|
+
let bytes = std::fs::read_to_string(
|
|
1840
|
+
scratch
|
|
1841
|
+
.path()
|
|
1842
|
+
.join(".team")
|
|
1843
|
+
.join("logs")
|
|
1844
|
+
.join("events.jsonl"),
|
|
1845
|
+
)
|
|
1846
|
+
.expect("events");
|
|
1847
|
+
assert!(!bytes.contains(marker));
|
|
1848
|
+
assert!(bytes.contains("[REDACTED]"));
|
|
1849
|
+
|
|
1850
|
+
let blocked = ScratchDir::new("event-log-required");
|
|
1851
|
+
std::fs::create_dir_all(
|
|
1852
|
+
blocked
|
|
1853
|
+
.path()
|
|
1854
|
+
.join(".team")
|
|
1855
|
+
.join("logs")
|
|
1856
|
+
.join("events.jsonl"),
|
|
1857
|
+
)
|
|
1858
|
+
.expect("occupy event path with directory");
|
|
1859
|
+
assert!(write_session_convergence_progress_event(
|
|
1860
|
+
blocked.path(),
|
|
1861
|
+
serde_json::json!({"pending_agent_ids": ["worker"]}),
|
|
1862
|
+
)
|
|
1863
|
+
.is_err());
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1840
1866
|
// E36 fix-B RED→GREEN: a real Claude worker that sent a message has its session
|
|
1841
1867
|
// transcript landed at <projects_root>/<slug>/<session_id>.jsonl, but neither
|
|
1842
1868
|
// rollout_path was persisted to state nor a session.captured event was logged.
|
|
@@ -1841,11 +1841,13 @@ fn save_restart_state_with_lifecycle_topology_authority_and_capture_backfill_ski
|
|
|
1841
1841
|
.map(String::as_str)
|
|
1842
1842
|
.collect::<Vec<_>>();
|
|
1843
1843
|
sync_restart_team_projections(state, team_key);
|
|
1844
|
-
crate::state::
|
|
1845
|
-
|
|
1844
|
+
crate::state::repository::StateRepository::new(workspace).save(
|
|
1845
|
+
crate::state::repository::StateWriteIntent::RestartTeam {
|
|
1846
|
+
team_key,
|
|
1847
|
+
topology_authority_agent_ids: &topology_agent_ids,
|
|
1848
|
+
skip_capture_backfill_agent_ids: &skip_capture_backfill_agent_ids,
|
|
1849
|
+
},
|
|
1846
1850
|
state,
|
|
1847
|
-
&skip_capture_backfill_agent_ids,
|
|
1848
|
-
&topology_agent_ids,
|
|
1849
1851
|
)
|
|
1850
1852
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))
|
|
1851
1853
|
}
|
|
@@ -1858,7 +1860,11 @@ fn save_restart_session_repairs(
|
|
|
1858
1860
|
) -> Result<(), LifecycleError> {
|
|
1859
1861
|
let repairs = collect_session_repair_fields(state, agent_ids);
|
|
1860
1862
|
sync_restart_team_projections(state, team_key);
|
|
1861
|
-
|
|
1863
|
+
let repository = crate::state::repository::StateRepository::new(workspace);
|
|
1864
|
+
match repository.save(
|
|
1865
|
+
crate::state::repository::StateWriteIntent::RestartSessionRepair { team_key },
|
|
1866
|
+
state,
|
|
1867
|
+
) {
|
|
1862
1868
|
Ok(()) => Ok(()),
|
|
1863
1869
|
Err(crate::state::StateError::SaveConflict(_)) => {
|
|
1864
1870
|
let mut latest =
|
|
@@ -1868,7 +1874,11 @@ fn save_restart_session_repairs(
|
|
|
1868
1874
|
apply_session_repair_fields(&mut latest, agent_id, repair);
|
|
1869
1875
|
}
|
|
1870
1876
|
sync_restart_team_projections(&mut latest, team_key);
|
|
1871
|
-
|
|
1877
|
+
repository
|
|
1878
|
+
.save(
|
|
1879
|
+
crate::state::repository::StateWriteIntent::RestartSessionRepair { team_key },
|
|
1880
|
+
&latest,
|
|
1881
|
+
)
|
|
1872
1882
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
1873
1883
|
*state = latest;
|
|
1874
1884
|
Ok(())
|
|
@@ -2266,8 +2276,8 @@ fn write_fake_harness_spawn_argv_event(
|
|
|
2266
2276
|
tmux_endpoint_source: Option<&str>,
|
|
2267
2277
|
) {
|
|
2268
2278
|
let _ = crate::event_log::EventLog::new(workspace).write(
|
|
2269
|
-
|
|
2270
|
-
serde_json::json!({
|
|
2279
|
+
crate::event_log::PROVIDER_WORKER_SPAWN_ARGV,
|
|
2280
|
+
crate::event_log::provider_worker_spawn_argv_fields(serde_json::json!({
|
|
2271
2281
|
"agent_id": decision.agent_id.as_str(),
|
|
2272
2282
|
"provider": agent_provider(agent),
|
|
2273
2283
|
"argv": [],
|
|
@@ -2277,7 +2287,7 @@ fn write_fake_harness_spawn_argv_event(
|
|
|
2277
2287
|
"source": "restart",
|
|
2278
2288
|
"tmux_endpoint": transport.tmux_endpoint(),
|
|
2279
2289
|
"tmux_endpoint_source": tmux_endpoint_source.unwrap_or("transport"),
|
|
2280
|
-
}),
|
|
2290
|
+
})),
|
|
2281
2291
|
);
|
|
2282
2292
|
}
|
|
2283
2293
|
|
|
@@ -2924,15 +2934,7 @@ fn write_restart_resume_decision_event(
|
|
|
2924
2934
|
forced_fresh_convergence: Option<&crate::session_capture::SessionConvergence>,
|
|
2925
2935
|
unresumable: Option<&crate::lifecycle::types::UnresumableWorker>,
|
|
2926
2936
|
) -> Result<(), LifecycleError> {
|
|
2927
|
-
use std::io::Write as _;
|
|
2928
|
-
|
|
2929
|
-
let path = workspace.join(".team").join("logs").join("events.jsonl");
|
|
2930
|
-
if let Some(parent) = path.parent() {
|
|
2931
|
-
std::fs::create_dir_all(parent).map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
2932
|
-
}
|
|
2933
2937
|
let mut event = serde_json::json!({
|
|
2934
|
-
"ts": chrono::Utc::now().to_rfc3339(),
|
|
2935
|
-
"event": crate::lifecycle::types::event_names::RESTART_RESUME_DECISION,
|
|
2936
2938
|
"worker_id": worker_id,
|
|
2937
2939
|
"has_first_send_at": first_send_at.is_some(),
|
|
2938
2940
|
"has_session_id": session_id.is_some(),
|
|
@@ -3039,15 +3041,12 @@ fn write_restart_resume_decision_event(
|
|
|
3039
3041
|
}
|
|
3040
3042
|
}
|
|
3041
3043
|
}
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
.
|
|
3048
|
-
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
3049
|
-
file.write_all(line.as_bytes())
|
|
3050
|
-
.and_then(|_| file.write_all(b"\n"))
|
|
3044
|
+
crate::event_log::EventLog::new(workspace)
|
|
3045
|
+
.write(
|
|
3046
|
+
crate::lifecycle::types::event_names::RESTART_RESUME_DECISION,
|
|
3047
|
+
event,
|
|
3048
|
+
)
|
|
3049
|
+
.map(|_| ())
|
|
3051
3050
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))
|
|
3052
3051
|
}
|
|
3053
3052
|
|
|
@@ -3120,10 +3119,16 @@ fn restart_candidate_from_state(
|
|
|
3120
3119
|
.filter(|s| !s.is_empty())
|
|
3121
3120
|
.map(SessionName::new)
|
|
3122
3121
|
.unwrap_or_else(|| SessionName::new(team_name));
|
|
3122
|
+
let retired = retired_agent_ids(state);
|
|
3123
3123
|
let mut agents = state
|
|
3124
3124
|
.get("agents")
|
|
3125
3125
|
.and_then(serde_json::Value::as_object)
|
|
3126
|
-
.map(|map|
|
|
3126
|
+
.map(|map| {
|
|
3127
|
+
map.keys()
|
|
3128
|
+
.filter(|agent_id| !retired.contains(agent_id.as_str()))
|
|
3129
|
+
.map(AgentId::new)
|
|
3130
|
+
.collect::<Vec<_>>()
|
|
3131
|
+
})
|
|
3127
3132
|
.unwrap_or_default();
|
|
3128
3133
|
agents.sort_by(|a, b| a.as_str().cmp(b.as_str()));
|
|
3129
3134
|
RestartCandidate {
|
|
@@ -3136,6 +3141,19 @@ fn restart_candidate_from_state(
|
|
|
3136
3141
|
}
|
|
3137
3142
|
}
|
|
3138
3143
|
|
|
3144
|
+
fn retired_agent_ids(state: &serde_json::Value) -> std::collections::BTreeSet<String> {
|
|
3145
|
+
state
|
|
3146
|
+
.get("agent_lifecycle")
|
|
3147
|
+
.and_then(serde_json::Value::as_object)
|
|
3148
|
+
.into_iter()
|
|
3149
|
+
.flat_map(|lifecycle| lifecycle.iter())
|
|
3150
|
+
.filter(|(_, entry)| {
|
|
3151
|
+
entry.get("state").and_then(serde_json::Value::as_str) == Some("retired")
|
|
3152
|
+
})
|
|
3153
|
+
.map(|(agent_id, _)| agent_id.clone())
|
|
3154
|
+
.collect()
|
|
3155
|
+
}
|
|
3156
|
+
|
|
3139
3157
|
/// E5 task#3 / RC-A6a:每次 restart 都以**角色定义**(team_dir 的 TEAM.md+agents/*.md)
|
|
3140
3158
|
/// compile_team 重建 runtime spec(覆盖),保留运行期 override(session_name 必须延续,
|
|
3141
3159
|
/// 否则 tmux session 对不上)。写到 .team/runtime/<team_key>/team.spec.yaml。
|
|
@@ -3205,6 +3223,7 @@ fn rebuild_runtime_spec_from_roles(
|
|
|
3205
3223
|
// 静态 team_dir/agents/*.md + state 记录的 dynamic role source。缺文件 fail-closed
|
|
3206
3224
|
// 三行式,不静默 prune 已 live 的 helper(persist SaveConflict 保护继续生效)。
|
|
3207
3225
|
merge_state_dynamic_role_files(&mut spec, run_workspace, &team_dir, team_key, state)?;
|
|
3226
|
+
filter_retired_agents_from_spec(&mut spec, &retired_agent_ids(state));
|
|
3208
3227
|
// 写 runtime spec(覆盖,原子 tmp+rename;Bug2)。
|
|
3209
3228
|
let spec_path = crate::model::paths::runtime_spec_path(run_workspace, team_key);
|
|
3210
3229
|
crate::lifecycle::launch::write_spec_atomic(&spec_path, &spec)?;
|
|
@@ -3217,6 +3236,15 @@ fn rebuild_runtime_spec_from_roles(
|
|
|
3217
3236
|
Ok(spec)
|
|
3218
3237
|
}
|
|
3219
3238
|
|
|
3239
|
+
fn filter_retired_agents_from_spec(
|
|
3240
|
+
spec: &mut YamlValue,
|
|
3241
|
+
retired: &std::collections::BTreeSet<String>,
|
|
3242
|
+
) {
|
|
3243
|
+
for agent_id in retired {
|
|
3244
|
+
*spec = super::remove::spec_without_agent(spec, &AgentId::new(agent_id));
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
|
|
3220
3248
|
/// 0.5.30 (`add-agent-restart-saveconflict-locate.md` §4/§11): 把 add-agent
|
|
3221
3249
|
/// 写入 `state.agents.<id>.dynamic_role_file` 的动态 role 文档合并回 restart
|
|
3222
3250
|
/// 重建 spec。规则:
|
|
@@ -3419,6 +3447,118 @@ mod tests {
|
|
|
3419
3447
|
TransportError,
|
|
3420
3448
|
};
|
|
3421
3449
|
|
|
3450
|
+
#[test]
|
|
3451
|
+
fn retired_filter_clears_every_spec_reference_via_remove_primitive() {
|
|
3452
|
+
let mut spec = crate::model::yaml::loads(
|
|
3453
|
+
r#"
|
|
3454
|
+
name: retirement-filter
|
|
3455
|
+
agents:
|
|
3456
|
+
- id: gone
|
|
3457
|
+
- id: keep
|
|
3458
|
+
runtime:
|
|
3459
|
+
startup_order:
|
|
3460
|
+
- gone
|
|
3461
|
+
- keep
|
|
3462
|
+
routing:
|
|
3463
|
+
default_assignee: gone
|
|
3464
|
+
rules:
|
|
3465
|
+
- match: gone-task
|
|
3466
|
+
assign_to: gone
|
|
3467
|
+
- match: keep-task
|
|
3468
|
+
assign_to: keep
|
|
3469
|
+
tasks:
|
|
3470
|
+
- title: gone-task
|
|
3471
|
+
assignee: gone
|
|
3472
|
+
- title: keep-task
|
|
3473
|
+
assignee: keep
|
|
3474
|
+
"#,
|
|
3475
|
+
)
|
|
3476
|
+
.expect("fixture spec");
|
|
3477
|
+
let retired = std::collections::BTreeSet::from(["gone".to_string()]);
|
|
3478
|
+
|
|
3479
|
+
filter_retired_agents_from_spec(&mut spec, &retired);
|
|
3480
|
+
|
|
3481
|
+
let agents = spec
|
|
3482
|
+
.get("agents")
|
|
3483
|
+
.and_then(YamlValue::as_list)
|
|
3484
|
+
.expect("agents");
|
|
3485
|
+
assert!(agents.iter().all(|agent| {
|
|
3486
|
+
agent.get("id").and_then(YamlValue::as_str) != Some("gone")
|
|
3487
|
+
}));
|
|
3488
|
+
let startup_order = spec
|
|
3489
|
+
.get("runtime")
|
|
3490
|
+
.and_then(|runtime| runtime.get("startup_order"))
|
|
3491
|
+
.and_then(YamlValue::as_list)
|
|
3492
|
+
.expect("startup_order");
|
|
3493
|
+
assert!(startup_order
|
|
3494
|
+
.iter()
|
|
3495
|
+
.all(|agent_id| agent_id.as_str() != Some("gone")));
|
|
3496
|
+
let routing = spec.get("routing").expect("routing");
|
|
3497
|
+
assert_ne!(
|
|
3498
|
+
routing
|
|
3499
|
+
.get("default_assignee")
|
|
3500
|
+
.and_then(YamlValue::as_str),
|
|
3501
|
+
Some("gone"),
|
|
3502
|
+
"spec_default_assignee must not return a retired agent"
|
|
3503
|
+
);
|
|
3504
|
+
let rules = routing
|
|
3505
|
+
.get("rules")
|
|
3506
|
+
.and_then(YamlValue::as_list)
|
|
3507
|
+
.expect("routing rules");
|
|
3508
|
+
assert!(rules.iter().all(|rule| {
|
|
3509
|
+
rule.get("assign_to").and_then(YamlValue::as_str) != Some("gone")
|
|
3510
|
+
}));
|
|
3511
|
+
let tasks = spec
|
|
3512
|
+
.get("tasks")
|
|
3513
|
+
.and_then(YamlValue::as_list)
|
|
3514
|
+
.expect("tasks");
|
|
3515
|
+
assert!(tasks.iter().all(|task| {
|
|
3516
|
+
task.get("assignee").and_then(YamlValue::as_str) != Some("gone")
|
|
3517
|
+
}));
|
|
3518
|
+
}
|
|
3519
|
+
|
|
3520
|
+
#[test]
|
|
3521
|
+
fn resume_decision_event_uses_central_scrub_and_preserves_required_failure() {
|
|
3522
|
+
let marker = "synthetic-resume-decision-marker";
|
|
3523
|
+
let workspace =
|
|
3524
|
+
std::env::temp_dir().join(format!("ta-resume-decision-event-{}", std::process::id()));
|
|
3525
|
+
let _ = std::fs::remove_dir_all(&workspace);
|
|
3526
|
+
write_restart_resume_decision_event(
|
|
3527
|
+
&workspace,
|
|
3528
|
+
"worker",
|
|
3529
|
+
Some(format!("mcp_servers.demo.env.OPENAI_API_KEY=\"{marker}\"")),
|
|
3530
|
+
None,
|
|
3531
|
+
false,
|
|
3532
|
+
"fresh_start",
|
|
3533
|
+
false,
|
|
3534
|
+
None,
|
|
3535
|
+
None,
|
|
3536
|
+
)
|
|
3537
|
+
.expect("central EventLog write");
|
|
3538
|
+
let bytes =
|
|
3539
|
+
std::fs::read_to_string(workspace.join(".team").join("logs").join("events.jsonl"))
|
|
3540
|
+
.expect("events");
|
|
3541
|
+
assert!(!bytes.contains(marker));
|
|
3542
|
+
assert!(bytes.contains("[REDACTED]"));
|
|
3543
|
+
let _ = std::fs::remove_dir_all(&workspace);
|
|
3544
|
+
|
|
3545
|
+
std::fs::create_dir_all(workspace.join(".team").join("logs").join("events.jsonl"))
|
|
3546
|
+
.expect("occupy event path with directory");
|
|
3547
|
+
let result = write_restart_resume_decision_event(
|
|
3548
|
+
&workspace,
|
|
3549
|
+
"worker",
|
|
3550
|
+
None,
|
|
3551
|
+
None,
|
|
3552
|
+
false,
|
|
3553
|
+
"fresh_start",
|
|
3554
|
+
false,
|
|
3555
|
+
None,
|
|
3556
|
+
None,
|
|
3557
|
+
);
|
|
3558
|
+
assert!(matches!(result, Err(LifecycleError::StatePersist(_))));
|
|
3559
|
+
let _ = std::fs::remove_dir_all(workspace);
|
|
3560
|
+
}
|
|
3561
|
+
|
|
3422
3562
|
struct RespawnEpochTransport;
|
|
3423
3563
|
|
|
3424
3564
|
impl Transport for RespawnEpochTransport {
|