@team-agent/installer 0.4.11 → 0.5.1
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 +1 -0
- package/crates/team-agent/src/cli/diagnose.rs +2 -15
- package/crates/team-agent/src/cli/emit.rs +98 -5
- package/crates/team-agent/src/cli/mod.rs +2 -0
- package/crates/team-agent/src/cli/named_address.rs +864 -0
- package/crates/team-agent/src/cli/send.rs +104 -0
- package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -0
- package/crates/team-agent/src/cli/tests/mod.rs +1 -0
- package/crates/team-agent/src/cli/tests/named_address.rs +455 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +4 -0
- package/crates/team-agent/src/compiler.rs +11 -20
- package/crates/team-agent/src/coordinator/orphan.rs +2 -2
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +5 -4
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +1776 -2
- package/crates/team-agent/src/coordinator/steps/mod.rs +19 -0
- package/crates/team-agent/src/coordinator/tests/a0_lostupdate.rs +78 -7
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +195 -0
- package/crates/team-agent/src/coordinator/tick.rs +31 -932
- package/crates/team-agent/src/diagnose/orphans.rs +66 -8
- package/crates/team-agent/src/layout/worker_window_helpers.rs +5 -7
- package/crates/team-agent/src/leader/provider_attribution.rs +7 -5
- package/crates/team-agent/src/leader/rediscover.rs +1 -11
- package/crates/team-agent/src/lifecycle/launch/plan.rs +19 -8
- package/crates/team-agent/src/lifecycle/launch.rs +135 -58
- package/crates/team-agent/src/lifecycle/lock.rs +302 -0
- package/crates/team-agent/src/lifecycle/mod.rs +1 -0
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +1 -11
- package/crates/team-agent/src/lifecycle/restart/agent.rs +169 -113
- package/crates/team-agent/src/lifecycle/restart/common.rs +108 -17
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +165 -32
- package/crates/team-agent/src/lifecycle/restart/remove.rs +16 -2
- package/crates/team-agent/src/lifecycle/restart/selection.rs +2 -1
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +6 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +3 -3
- package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +321 -0
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +35 -5
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +802 -0
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +675 -0
- package/crates/team-agent/src/lifecycle/tests.rs +3 -0
- package/crates/team-agent/src/lifecycle/types.rs +8 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +24 -9
- package/crates/team-agent/src/mcp_server/tools.rs +157 -68
- package/crates/team-agent/src/messaging/activity.rs +43 -18
- package/crates/team-agent/src/messaging/delivery.rs +161 -97
- package/crates/team-agent/src/messaging/helpers.rs +1 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +22 -1
- package/crates/team-agent/src/messaging/results.rs +34 -9
- package/crates/team-agent/src/messaging/scheduler.rs +52 -9
- package/crates/team-agent/src/model/spec.rs +10 -6
- package/crates/team-agent/src/provider/adapter.rs +10 -1038
- package/crates/team-agent/src/provider/adapters/claude.rs +3 -8
- package/crates/team-agent/src/provider/adapters/copilot.rs +2 -5
- package/crates/team-agent/src/provider/classify.rs +23 -48
- package/crates/team-agent/src/provider/command.rs +16 -7
- package/crates/team-agent/src/provider/faults.rs +93 -53
- package/crates/team-agent/src/provider/session/capture.rs +4 -15
- package/crates/team-agent/src/provider/session_scan/claude.rs +309 -0
- package/crates/team-agent/src/provider/session_scan/codex.rs +40 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +350 -0
- package/crates/team-agent/src/provider/session_scan/copilot.rs +202 -0
- package/crates/team-agent/src/provider/session_scan.rs +65 -27
- package/crates/team-agent/src/provider/tests/faults.rs +80 -0
- package/crates/team-agent/src/provider/types.rs +33 -1
- package/crates/team-agent/src/provider/wire.rs +171 -1
- package/crates/team-agent/src/state/identity.rs +242 -57
- package/crates/team-agent/src/state/identity_keys.rs +9 -12
- package/crates/team-agent/src/state/mod.rs +5 -1
- package/crates/team-agent/src/state/owner_gate.rs +59 -15
- package/crates/team-agent/src/state/ownership.rs +12 -11
- package/crates/team-agent/src/state/paths.rs +13 -6
- package/crates/team-agent/src/state/persist.rs +671 -128
- package/crates/team-agent/src/state/projection.rs +240 -49
- package/crates/team-agent/src/state/selector.rs +11 -5
- package/crates/team-agent/src/tmux_backend/tests.rs +51 -2
- package/crates/team-agent/src/tmux_backend.rs +42 -5
- package/crates/team-agent/src/transport/test_support.rs +55 -6
- package/package.json +4 -4
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
use std::path::{Path, PathBuf};
|
|
2
|
+
use std::time::Duration;
|
|
3
|
+
|
|
4
|
+
use super::*;
|
|
5
|
+
use crate::lifecycle::lock::{
|
|
6
|
+
acquire_agent_lifecycle_lock, acquire_agent_lifecycle_lock_for_test, LifecycleLockRequest,
|
|
7
|
+
AGENT_LIFECYCLE_LOCK_NAME, LIFECYCLE_LOCK_HELD_LONG, LIFECYCLE_LOCK_TIMEOUT,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
#[test]
|
|
11
|
+
fn lifecycle_lock_timeout_error_contract_and_constants() {
|
|
12
|
+
assert_eq!(AGENT_LIFECYCLE_LOCK_NAME, "agent-lifecycle");
|
|
13
|
+
assert_eq!(LIFECYCLE_LOCK_TIMEOUT, Duration::from_secs(30));
|
|
14
|
+
assert_eq!(LIFECYCLE_LOCK_HELD_LONG, Duration::from_secs(5));
|
|
15
|
+
|
|
16
|
+
let ws = temp_ws();
|
|
17
|
+
let err = LifecycleError::LifecycleLockTimeout {
|
|
18
|
+
lock_path: crate::model::paths::runtime_dir(&ws).join("agent-lifecycle.lock"),
|
|
19
|
+
log_path: crate::model::paths::logs_dir(&ws).join("events.jsonl"),
|
|
20
|
+
operation: "reset-agent".to_string(),
|
|
21
|
+
waited_ms: 30_000,
|
|
22
|
+
};
|
|
23
|
+
assert_eq!(
|
|
24
|
+
err.to_string(),
|
|
25
|
+
format!(
|
|
26
|
+
"error: lifecycle lock timeout after 30s\naction: retry or check for hung reset/start\nlog: {}",
|
|
27
|
+
crate::model::paths::logs_dir(&ws).join("events.jsonl").display()
|
|
28
|
+
)
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#[test]
|
|
33
|
+
fn lifecycle_lock_lifecycle_rollback_releases_after_timeout() {
|
|
34
|
+
let ws = temp_ws();
|
|
35
|
+
let agent = aid("alpha");
|
|
36
|
+
let held = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
37
|
+
workspace: &ws,
|
|
38
|
+
operation: "reset-agent",
|
|
39
|
+
team: Some("alpha"),
|
|
40
|
+
agent_id: Some(&agent),
|
|
41
|
+
})
|
|
42
|
+
.expect("hold lifecycle lock");
|
|
43
|
+
|
|
44
|
+
let waiter_ws = ws.clone();
|
|
45
|
+
let waiter_agent = aid("beta");
|
|
46
|
+
let result = std::thread::spawn(move || {
|
|
47
|
+
acquire_agent_lifecycle_lock_for_test(
|
|
48
|
+
LifecycleLockRequest {
|
|
49
|
+
workspace: &waiter_ws,
|
|
50
|
+
operation: "start-agent",
|
|
51
|
+
team: Some("alpha"),
|
|
52
|
+
agent_id: Some(&waiter_agent),
|
|
53
|
+
},
|
|
54
|
+
Duration::from_millis(200),
|
|
55
|
+
Duration::from_secs(5),
|
|
56
|
+
)
|
|
57
|
+
})
|
|
58
|
+
.join()
|
|
59
|
+
.expect("waiter thread");
|
|
60
|
+
|
|
61
|
+
assert!(
|
|
62
|
+
matches!(result, Err(LifecycleError::LifecycleLockTimeout { operation, .. }) if operation == "start-agent"),
|
|
63
|
+
"second lifecycle mutator must time out while lock is held"
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
drop(held);
|
|
67
|
+
acquire_agent_lifecycle_lock_for_test(
|
|
68
|
+
LifecycleLockRequest {
|
|
69
|
+
workspace: &ws,
|
|
70
|
+
operation: "stop-agent",
|
|
71
|
+
team: Some("alpha"),
|
|
72
|
+
agent_id: Some(&agent),
|
|
73
|
+
},
|
|
74
|
+
Duration::from_millis(200),
|
|
75
|
+
Duration::from_secs(5),
|
|
76
|
+
)
|
|
77
|
+
.expect("lock released after guard drop");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#[test]
|
|
81
|
+
fn lifecycle_lock_long_held_event_records_holder_and_waiter() {
|
|
82
|
+
let ws = temp_ws();
|
|
83
|
+
let holder_agent = aid("holder");
|
|
84
|
+
let waiter_agent = aid("waiter");
|
|
85
|
+
let _held = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
86
|
+
workspace: &ws,
|
|
87
|
+
operation: "restart",
|
|
88
|
+
team: Some("alpha"),
|
|
89
|
+
agent_id: Some(&holder_agent),
|
|
90
|
+
})
|
|
91
|
+
.expect("hold lifecycle lock");
|
|
92
|
+
|
|
93
|
+
let waiter_ws = ws.clone();
|
|
94
|
+
let result = std::thread::spawn(move || {
|
|
95
|
+
acquire_agent_lifecycle_lock_for_test(
|
|
96
|
+
LifecycleLockRequest {
|
|
97
|
+
workspace: &waiter_ws,
|
|
98
|
+
operation: "reset-agent",
|
|
99
|
+
team: Some("alpha"),
|
|
100
|
+
agent_id: Some(&waiter_agent),
|
|
101
|
+
},
|
|
102
|
+
Duration::from_millis(220),
|
|
103
|
+
Duration::from_millis(50),
|
|
104
|
+
)
|
|
105
|
+
})
|
|
106
|
+
.join()
|
|
107
|
+
.expect("waiter thread");
|
|
108
|
+
assert!(matches!(
|
|
109
|
+
result,
|
|
110
|
+
Err(LifecycleError::LifecycleLockTimeout { .. })
|
|
111
|
+
));
|
|
112
|
+
|
|
113
|
+
let events = read_events(&ws);
|
|
114
|
+
let event = events
|
|
115
|
+
.iter()
|
|
116
|
+
.find(|event| {
|
|
117
|
+
event.get("event").and_then(serde_json::Value::as_str)
|
|
118
|
+
== Some("lifecycle.lock_held_long")
|
|
119
|
+
})
|
|
120
|
+
.unwrap_or_else(|| panic!("missing lifecycle.lock_held_long event: {events:?}"));
|
|
121
|
+
assert_eq!(event["lock_name"], json!("agent-lifecycle"));
|
|
122
|
+
assert_eq!(event["operation"], json!("reset-agent"));
|
|
123
|
+
assert_eq!(event["holder"]["operation"], json!("restart"));
|
|
124
|
+
assert_eq!(event["holder"]["agent_id"], json!("holder"));
|
|
125
|
+
assert_eq!(event["threshold_ms"], json!(50));
|
|
126
|
+
assert_eq!(event["timeout_ms"], json!(220));
|
|
127
|
+
assert!(
|
|
128
|
+
event["blocked_queue_len"].as_u64().unwrap_or(0) >= 1,
|
|
129
|
+
"waiter sidecar should be visible while long-held event is emitted"
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#[test]
|
|
134
|
+
fn lifecycle_lock_phase_b_golden_source_guard() {
|
|
135
|
+
r2_lifecycle_lock_exists_precondition();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
#[test]
|
|
139
|
+
fn r2_lifecycle_lock_exists_precondition() {
|
|
140
|
+
let src = manifest_src();
|
|
141
|
+
let lock = read_src("lifecycle/lock.rs");
|
|
142
|
+
assert!(
|
|
143
|
+
lock.contains("AGENT_LIFECYCLE_LOCK_NAME: &str = \"agent-lifecycle\""),
|
|
144
|
+
"lifecycle lock name constant must remain explicit"
|
|
145
|
+
);
|
|
146
|
+
assert!(
|
|
147
|
+
lock.contains("LIFECYCLE_LOCK_TIMEOUT: Duration = Duration::from_secs(30)"),
|
|
148
|
+
"lifecycle lock timeout constant must remain 30s"
|
|
149
|
+
);
|
|
150
|
+
assert!(
|
|
151
|
+
lock.contains("write_lock_held_long_event") && lock.contains("lifecycle.lock_held_long"),
|
|
152
|
+
"long-held lifecycle lock event must remain wired"
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
let state_hits = source_files(&src.join("state"))
|
|
156
|
+
.into_iter()
|
|
157
|
+
.filter(|path| {
|
|
158
|
+
let text = std::fs::read_to_string(path).unwrap();
|
|
159
|
+
text.contains("RuntimeLock::acquire") && text.contains("\"state-save\"")
|
|
160
|
+
})
|
|
161
|
+
.collect::<Vec<_>>();
|
|
162
|
+
assert_eq!(
|
|
163
|
+
state_hits,
|
|
164
|
+
vec![src.join("state").join("persist.rs")],
|
|
165
|
+
"state-save lock must stay owned by state/persist.rs"
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
for path in source_files(&src.join("state")) {
|
|
169
|
+
let text = std::fs::read_to_string(&path).unwrap();
|
|
170
|
+
assert!(
|
|
171
|
+
!text.contains("LifecycleLock")
|
|
172
|
+
&& !text.contains("acquire_agent_lifecycle_lock")
|
|
173
|
+
&& !text.contains("agent-lifecycle"),
|
|
174
|
+
"state module must not acquire or know lifecycle lock: {}",
|
|
175
|
+
path.display()
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
for path in source_files(&src.join("coordinator")) {
|
|
180
|
+
let text = std::fs::read_to_string(&path).unwrap();
|
|
181
|
+
assert!(
|
|
182
|
+
!text.contains("acquire_agent_lifecycle_lock") && !text.contains("agent-lifecycle"),
|
|
183
|
+
"coordinator tick path must not block on lifecycle lock: {}",
|
|
184
|
+
path.display()
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for path in source_files(&src) {
|
|
189
|
+
if path == src.join("state").join("persist.rs") || is_test_source(&path) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
let text = std::fs::read_to_string(&path).unwrap();
|
|
193
|
+
assert!(
|
|
194
|
+
!text.contains("RuntimeLock::acquire"),
|
|
195
|
+
"production source outside state/persist.rs must not acquire runtime locks directly: {}",
|
|
196
|
+
path.display()
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
for path in source_files(&src) {
|
|
201
|
+
if path.starts_with(src.join("state")) || path.starts_with(src.join("lifecycle")) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
let text = std::fs::read_to_string(&path).unwrap();
|
|
205
|
+
let lifecycle_topology_authority_entries = [
|
|
206
|
+
"save_runtime_state_with_lifecycle_topology_authority",
|
|
207
|
+
"save_team_scoped_state_with_lifecycle_topology_authority",
|
|
208
|
+
"save_runtime_state_with_team_tombstone_lifecycle_topology_authority",
|
|
209
|
+
"save_team_scoped_state_with_tombstone_lifecycle_topology_authority",
|
|
210
|
+
];
|
|
211
|
+
assert!(
|
|
212
|
+
!lifecycle_topology_authority_entries
|
|
213
|
+
.iter()
|
|
214
|
+
.any(|entry| text.contains(entry)),
|
|
215
|
+
"lifecycle topology authority save entry must not be referenced outside lifecycle/state modules: {}",
|
|
216
|
+
path.display()
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let agent = read_src("lifecycle/restart/agent.rs");
|
|
221
|
+
assert_public_operation(&agent, "start-agent");
|
|
222
|
+
assert_public_operation(&agent, "stop-agent");
|
|
223
|
+
assert_public_operation(&agent, "reset-agent");
|
|
224
|
+
assert_body_is_unlocked(&agent, "pub(crate) fn start_agent_at_paths");
|
|
225
|
+
assert_body_is_unlocked(&agent, "pub(super) fn stop_agent_at_paths");
|
|
226
|
+
assert_body_is_unlocked(&agent, "fn reset_agent_at_paths");
|
|
227
|
+
|
|
228
|
+
let remove = read_src("lifecycle/restart/remove.rs");
|
|
229
|
+
assert_public_operation(&remove, "remove-agent");
|
|
230
|
+
assert_body_is_unlocked(&remove, "fn remove_agent_at_paths");
|
|
231
|
+
assert!(
|
|
232
|
+
!remove.contains("start_agent_with_transport"),
|
|
233
|
+
"remove rollback runs while remove-agent holds the lifecycle lock; it must use lock-free start_agent_at_paths"
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
let launch = read_src("lifecycle/launch.rs");
|
|
237
|
+
assert_public_operation(&launch, "add-agent");
|
|
238
|
+
assert_public_operation(&launch, "fork-agent");
|
|
239
|
+
assert_body_is_unlocked(&launch, "fn add_agent_with_transport_at_paths");
|
|
240
|
+
|
|
241
|
+
let rebuild = read_src("lifecycle/restart/rebuild.rs");
|
|
242
|
+
assert_public_operation(&rebuild, "restart");
|
|
243
|
+
let drop_idx = rebuild.find("drop(lifecycle_lock);").unwrap();
|
|
244
|
+
let coordinator_idx = rebuild
|
|
245
|
+
.find("start_coordinator_for_workspace(&selected.run_workspace)")
|
|
246
|
+
.unwrap();
|
|
247
|
+
let readiness_idx = rebuild.find("wait_restart_readiness_or_timeout(").unwrap();
|
|
248
|
+
assert!(
|
|
249
|
+
rebuild.contains("let lifecycle_lock = acquire_agent_lifecycle_lock")
|
|
250
|
+
&& drop_idx < coordinator_idx
|
|
251
|
+
&& coordinator_idx < readiness_idx,
|
|
252
|
+
"restart must release lifecycle lock before coordinator readiness wait"
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
fn is_test_source(path: &Path) -> bool {
|
|
257
|
+
path.components()
|
|
258
|
+
.any(|component| component.as_os_str() == "tests")
|
|
259
|
+
|| path
|
|
260
|
+
.file_name()
|
|
261
|
+
.and_then(|name| name.to_str())
|
|
262
|
+
.is_some_and(|name| name == "tests.rs" || name.ends_with("_tests.rs"))
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
fn read_events(workspace: &Path) -> Vec<serde_json::Value> {
|
|
266
|
+
let path = crate::model::paths::logs_dir(workspace).join("events.jsonl");
|
|
267
|
+
std::fs::read_to_string(path)
|
|
268
|
+
.unwrap_or_default()
|
|
269
|
+
.lines()
|
|
270
|
+
.filter_map(|line| serde_json::from_str(line).ok())
|
|
271
|
+
.collect()
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
fn manifest_src() -> PathBuf {
|
|
275
|
+
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src")
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
fn read_src(path: &str) -> String {
|
|
279
|
+
std::fs::read_to_string(manifest_src().join(path)).unwrap()
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
fn source_files(root: &Path) -> Vec<PathBuf> {
|
|
283
|
+
let mut out = Vec::new();
|
|
284
|
+
collect_rs_files(root, &mut out);
|
|
285
|
+
out.sort();
|
|
286
|
+
out
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
fn collect_rs_files(root: &Path, out: &mut Vec<PathBuf>) {
|
|
290
|
+
for entry in std::fs::read_dir(root).unwrap() {
|
|
291
|
+
let path = entry.unwrap().path();
|
|
292
|
+
if path.is_dir() {
|
|
293
|
+
collect_rs_files(&path, out);
|
|
294
|
+
} else if path.extension().and_then(|ext| ext.to_str()) == Some("rs") {
|
|
295
|
+
out.push(path);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
fn assert_public_operation(source: &str, operation: &str) {
|
|
301
|
+
assert!(
|
|
302
|
+
source.contains("acquire_agent_lifecycle_lock")
|
|
303
|
+
&& source.contains(&format!("operation: \"{operation}\"")),
|
|
304
|
+
"public lifecycle entry must acquire lifecycle lock for {operation}"
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
fn assert_body_is_unlocked(source: &str, signature: &str) {
|
|
309
|
+
let body = source
|
|
310
|
+
.split_once(signature)
|
|
311
|
+
.unwrap_or_else(|| panic!("missing signature: {signature}"))
|
|
312
|
+
.1;
|
|
313
|
+
let body = body
|
|
314
|
+
.split_once("\n}\n")
|
|
315
|
+
.map(|(body, _)| body)
|
|
316
|
+
.unwrap_or(body);
|
|
317
|
+
assert!(
|
|
318
|
+
!body.contains("acquire_agent_lifecycle_lock"),
|
|
319
|
+
"{signature} must remain lock-free to avoid nested reset deadlocks"
|
|
320
|
+
);
|
|
321
|
+
}
|
|
@@ -33,6 +33,7 @@ type RecordedSpawns = LaneSpawns;
|
|
|
33
33
|
/// every spawn (kind + argv) like `RecordingLaunchTransport`.
|
|
34
34
|
struct SessionProbeRecordingTransport {
|
|
35
35
|
spawns: RecordedSpawns,
|
|
36
|
+
spawn_targets: std::sync::Mutex<Vec<(String, String, String)>>,
|
|
36
37
|
session_exists: bool,
|
|
37
38
|
// 0.4.10+ reset hard-gate: record killed panes so liveness/has_pane
|
|
38
39
|
// reflect the kill (the gate checks "did stop actually remove the
|
|
@@ -47,11 +48,22 @@ impl crate::transport::Transport for SessionProbeRecordingTransport {
|
|
|
47
48
|
}
|
|
48
49
|
fn spawn_first(&self, session: &crate::transport::SessionName, window: &crate::transport::WindowName, argv: &[String], _cwd: &std::path::Path, _env: &std::collections::BTreeMap<String, String>) -> Result<crate::transport::SpawnResult, crate::transport::TransportError> {
|
|
49
50
|
self.spawns.lock().unwrap().push(("spawn_first".to_string(), argv.to_vec()));
|
|
51
|
+
self.spawn_targets.lock().unwrap().push((
|
|
52
|
+
session.as_str().to_string(),
|
|
53
|
+
window.as_str().to_string(),
|
|
54
|
+
"%0".to_string(),
|
|
55
|
+
));
|
|
50
56
|
Ok(crate::transport::SpawnResult { pane_id: crate::transport::PaneId::new("%0"), session: session.clone(), window: window.clone(), child_pid: Some(6856) })
|
|
51
57
|
}
|
|
52
58
|
fn spawn_into(&self, session: &crate::transport::SessionName, window: &crate::transport::WindowName, argv: &[String], _cwd: &std::path::Path, _env: &std::collections::BTreeMap<String, String>) -> Result<crate::transport::SpawnResult, crate::transport::TransportError> {
|
|
53
59
|
self.spawns.lock().unwrap().push(("spawn_into".to_string(), argv.to_vec()));
|
|
54
|
-
|
|
60
|
+
let pane = format!("%{}", window.as_str());
|
|
61
|
+
self.spawn_targets.lock().unwrap().push((
|
|
62
|
+
session.as_str().to_string(),
|
|
63
|
+
window.as_str().to_string(),
|
|
64
|
+
pane.clone(),
|
|
65
|
+
));
|
|
66
|
+
Ok(crate::transport::SpawnResult { pane_id: crate::transport::PaneId::new(pane), session: session.clone(), window: window.clone(), child_pid: Some(6856) })
|
|
55
67
|
}
|
|
56
68
|
fn inject(&self, _t: &crate::transport::Target, _p: &crate::transport::InjectPayload, _s: crate::transport::Key, _b: bool) -> Result<crate::transport::InjectReport, crate::transport::TransportError> {
|
|
57
69
|
unimplemented!("not reached by start_agent respawn")
|
|
@@ -84,7 +96,25 @@ impl crate::transport::Transport for SessionProbeRecordingTransport {
|
|
|
84
96
|
Ok(())
|
|
85
97
|
}
|
|
86
98
|
fn list_targets(&self) -> Result<Vec<crate::transport::PaneInfo>, crate::transport::TransportError> {
|
|
87
|
-
Ok(
|
|
99
|
+
Ok(self
|
|
100
|
+
.spawn_targets
|
|
101
|
+
.lock()
|
|
102
|
+
.unwrap()
|
|
103
|
+
.iter()
|
|
104
|
+
.map(|(session, window, pane)| crate::transport::PaneInfo {
|
|
105
|
+
pane_id: crate::transport::PaneId::new(pane.as_str()),
|
|
106
|
+
session: crate::transport::SessionName::new(session.as_str()),
|
|
107
|
+
window_index: None,
|
|
108
|
+
window_name: Some(crate::transport::WindowName::new(window.as_str())),
|
|
109
|
+
pane_index: None,
|
|
110
|
+
tty: None,
|
|
111
|
+
current_command: Some("node".to_string()),
|
|
112
|
+
current_path: None,
|
|
113
|
+
active: false,
|
|
114
|
+
pane_pid: Some(6856),
|
|
115
|
+
leader_env: std::collections::BTreeMap::new(),
|
|
116
|
+
})
|
|
117
|
+
.collect())
|
|
88
118
|
}
|
|
89
119
|
fn has_session(&self, _s: &crate::transport::SessionName) -> Result<bool, crate::transport::TransportError> {
|
|
90
120
|
Ok(self.session_exists)
|
|
@@ -133,7 +163,7 @@ fn start_agent_respawn_into_dead_session_uses_new_session_not_new_window() {
|
|
|
133
163
|
let ws = respawn_ws_one_resumable_worker();
|
|
134
164
|
let spawns = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
|
|
135
165
|
// session_exists=false models the dead CP-1 `-L` server after --discard-session.
|
|
136
|
-
let transport = SessionProbeRecordingTransport { spawns: std::sync::Arc::clone(&spawns), session_exists: false, killed_panes: std::sync::Mutex::new(std::collections::BTreeSet::new()) };
|
|
166
|
+
let transport = SessionProbeRecordingTransport { spawns: std::sync::Arc::clone(&spawns), spawn_targets: std::sync::Mutex::new(Vec::new()), session_exists: false, killed_panes: std::sync::Mutex::new(std::collections::BTreeSet::new()) };
|
|
137
167
|
let _ = start_agent_with_transport(&ws, &AgentId::new("alpha"), false, false, false, None, &transport);
|
|
138
168
|
let recorded = spawns.lock().unwrap().clone();
|
|
139
169
|
assert_eq!(recorded.len(), 1, "exactly one respawn for alpha; got {recorded:?}");
|
|
@@ -165,7 +195,7 @@ fn reset_agent_discard_session_rebuilds_window_via_start_respawn() {
|
|
|
165
195
|
let ws = restart_ws_two_resumable_workers(); // compiled spec + state(alpha,bravo running) + seeded coordinator
|
|
166
196
|
let spawns = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
|
|
167
197
|
// live session present (other workers keep it alive) but alpha's window was discarded.
|
|
168
|
-
let transport = SessionProbeRecordingTransport { spawns: std::sync::Arc::clone(&spawns), session_exists: true, killed_panes: std::sync::Mutex::new(std::collections::BTreeSet::new()) };
|
|
198
|
+
let transport = SessionProbeRecordingTransport { spawns: std::sync::Arc::clone(&spawns), spawn_targets: std::sync::Mutex::new(Vec::new()), session_exists: true, killed_panes: std::sync::Mutex::new(std::collections::BTreeSet::new()) };
|
|
169
199
|
let _ = reset_agent_with_transport(&ws, &AgentId::new("alpha"), true, false, None, &transport);
|
|
170
200
|
let recorded = spawns.lock().unwrap().clone();
|
|
171
201
|
assert!(
|
|
@@ -203,7 +233,7 @@ fn reset_agent_discard_session_syncs_projection_epoch_inputs_for_restart_agent_c
|
|
|
203
233
|
|
|
204
234
|
let before = chrono::Utc::now();
|
|
205
235
|
let spawns = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
|
|
206
|
-
let transport = SessionProbeRecordingTransport { spawns: std::sync::Arc::clone(&spawns), session_exists: true, killed_panes: std::sync::Mutex::new(std::collections::BTreeSet::new()) };
|
|
236
|
+
let transport = SessionProbeRecordingTransport { spawns: std::sync::Arc::clone(&spawns), spawn_targets: std::sync::Mutex::new(Vec::new()), session_exists: true, killed_panes: std::sync::Mutex::new(std::collections::BTreeSet::new()) };
|
|
207
237
|
let outcome = reset_agent_with_transport(&ws, &AgentId::new("alpha"), true, false, None, &transport)
|
|
208
238
|
.expect("reset-agent alpha --discard-session --no-display");
|
|
209
239
|
assert!(
|