@team-agent/installer 0.5.120 → 0.5.122
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/intent/runtime.rs +46 -10
- package/crates/team-agent/src/cli/intent/runtime_tests.rs +114 -0
- package/crates/team-agent/src/cli/mod.rs +4 -1
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +56 -0
- package/crates/team-agent/src/coordinator/tests/health_sync.rs +27 -8
- package/crates/team-agent/src/db/message_store.rs +65 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +8 -0
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +111 -4
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp_iso.rs +244 -82
- package/crates/team-agent/src/lifecycle/launch.rs +1 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +3 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +10 -1
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +5 -0
- package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +16 -3
- package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +60 -17
- package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +162 -8
- package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +55 -6
- package/crates/team-agent/src/mcp_server/mod.rs +2 -0
- package/crates/team-agent/src/mcp_server/readiness.rs +495 -0
- package/crates/team-agent/src/mcp_server/readiness_tests.rs +272 -0
- package/crates/team-agent/src/mcp_server/wire.rs +96 -11
- package/crates/team-agent/src/messaging/delivery.rs +62 -0
- package/crates/team-agent/src/messaging/delivery_consumption_tests.rs +12 -1
- package/crates/team-agent/src/messaging/tests/dup_inject.rs +89 -5
- package/crates/team-agent/src/messaging/tests/spine.rs +45 -14
- package/crates/team-agent/src/provider/adapter.rs +7 -0
- package/package.json +4 -4
|
@@ -28,6 +28,68 @@ use crate::transport::{
|
|
|
28
28
|
TurnVerification, WindowName,
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
+
#[test]
|
|
32
|
+
#[cfg(unix)]
|
|
33
|
+
fn eager_mcp_waits_without_spending_input_attempts_then_delivers_the_same_message() {
|
|
34
|
+
let mut case = WorkerCase::new("mcp-handshake", "cursor_agent");
|
|
35
|
+
let carrier = crate::mcp_server::readiness::test_carrier_pid().unwrap();
|
|
36
|
+
let config = case.workspace.join(".team/runtime/mcp/w1.json");
|
|
37
|
+
std::fs::create_dir_all(config.parent().unwrap()).unwrap();
|
|
38
|
+
std::fs::write(&config, serde_json::to_vec(&serde_json::json!({
|
|
39
|
+
"mcpServers": {"team_orchestrator": {
|
|
40
|
+
"command": std::env::current_exe().unwrap().canonicalize().unwrap(),
|
|
41
|
+
"env": {"TEAM_AGENT_WORKSPACE": case.workspace, "TEAM_AGENT_ID": "w1", "TEAM_AGENT_OWNER_TEAM_ID": "dup-team"}
|
|
42
|
+
}}
|
|
43
|
+
})).unwrap()).unwrap();
|
|
44
|
+
case.state["team_key"] = serde_json::json!("dup-team");
|
|
45
|
+
case.state["agents"]["w1"]["mcp_config"] = serde_json::json!(config);
|
|
46
|
+
case.state["agents"]["w1"]["pane_pid"] = serde_json::json!(carrier);
|
|
47
|
+
case.state["agents"]["w1"]["spawned_at"] = serde_json::json!((chrono::Utc::now() - chrono::Duration::seconds(10)).to_rfc3339());
|
|
48
|
+
case.state["teams"] = serde_json::json!({"dup-team": case.state.clone()});
|
|
49
|
+
crate::state::persist::save_runtime_state(&case.workspace, &case.state).unwrap();
|
|
50
|
+
let transport = RecordingTransport { carrier: Some(carrier), ..RecordingTransport::new("") };
|
|
51
|
+
let message_id = case.store.create_message(None, "leader", "w1", "handshake canary", None, false, Some("dup-team")).unwrap();
|
|
52
|
+
for _ in 0..3 {
|
|
53
|
+
let outcome = deliver_pending_message(&case.workspace, &case.store, &transport,
|
|
54
|
+
&message_id, &case.event_log, &case.state).unwrap();
|
|
55
|
+
assert!(!outcome.ok);
|
|
56
|
+
assert!(outcome.verification.as_deref().unwrap().starts_with("mcp_handshake_"));
|
|
57
|
+
assert_eq!(transport.inject_count(), 0);
|
|
58
|
+
}
|
|
59
|
+
let connection = rusqlite::Connection::open(case.store.db_path()).unwrap();
|
|
60
|
+
let attempts: i64 = connection.query_row("select delivery_attempts from messages where message_id=?1", [&message_id], |row| row.get(0)).unwrap();
|
|
61
|
+
assert_eq!(attempts, 0, "waiting is not a physical delivery attempt");
|
|
62
|
+
crate::mcp_server::readiness::publish(&case.workspace, "dup-team", "w1", chrono::Utc::now()).unwrap();
|
|
63
|
+
let outcome = deliver_pending_message(&case.workspace, &case.store, &transport,
|
|
64
|
+
&message_id, &case.event_log, &case.state).unwrap();
|
|
65
|
+
assert_eq!(outcome.message_id.as_deref(), Some(message_id.as_str()));
|
|
66
|
+
assert_eq!(transport.inject_count(), 1, "same queued row proceeds after the real receipt");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#[test]
|
|
70
|
+
#[cfg(unix)]
|
|
71
|
+
fn issue345_missing_eager_config_defers_without_input_or_attempts() {
|
|
72
|
+
let mut case = WorkerCase::new("missing-mcp-config", "cursor_agent");
|
|
73
|
+
let carrier = crate::mcp_server::readiness::test_carrier_pid().unwrap();
|
|
74
|
+
case.state["team_key"] = serde_json::json!("dup-team");
|
|
75
|
+
case.state["agents"]["w1"]["pane_pid"] = serde_json::json!(carrier);
|
|
76
|
+
case.state["agents"]["w1"]["spawned_at"] = serde_json::json!(chrono::Utc::now().to_rfc3339());
|
|
77
|
+
case.state["teams"] = serde_json::json!({"dup-team": case.state.clone()});
|
|
78
|
+
crate::state::persist::save_runtime_state(&case.workspace, &case.state).unwrap();
|
|
79
|
+
let transport = RecordingTransport { carrier: Some(carrier), ..RecordingTransport::new("") };
|
|
80
|
+
let message_id = case.store.create_message(None, "leader", "w1", "missing config canary", None, false, Some("dup-team")).unwrap();
|
|
81
|
+
for _ in 0..3 {
|
|
82
|
+
let outcome = deliver_pending_message(&case.workspace, &case.store, &transport,
|
|
83
|
+
&message_id, &case.event_log, &case.state).unwrap();
|
|
84
|
+
assert!(!outcome.ok);
|
|
85
|
+
assert!(outcome.verification.as_deref().unwrap().starts_with("mcp_handshake_"));
|
|
86
|
+
assert_eq!(transport.inject_count(), 0, "missing metadata must never reach input");
|
|
87
|
+
}
|
|
88
|
+
let connection = rusqlite::Connection::open(case.store.db_path()).unwrap();
|
|
89
|
+
let attempts: i64 = connection.query_row("select delivery_attempts from messages where message_id=?1", [&message_id], |row| row.get(0)).unwrap();
|
|
90
|
+
assert_eq!(attempts, 0, "metadata wait is not an input attempt");
|
|
91
|
+
}
|
|
92
|
+
|
|
31
93
|
#[test]
|
|
32
94
|
fn error_retry_claim_is_single_winner_across_two_delivers() {
|
|
33
95
|
let case = WorkerCase::new("claim-gate", "fake");
|
|
@@ -103,10 +165,30 @@ fn observer_state_save_failure_after_enter_does_not_return_err() {
|
|
|
103
165
|
|
|
104
166
|
#[test]
|
|
105
167
|
fn pane_token_refuses_second_inject_for_claude_grok_cursor() {
|
|
106
|
-
for provider in ["claude", "grok", "
|
|
107
|
-
let case = WorkerCase::new(&format!("token-{provider}"), provider);
|
|
108
|
-
|
|
109
|
-
let
|
|
168
|
+
for provider in ["claude", "grok", "cursor_agent"] {
|
|
169
|
+
let mut case = WorkerCase::new(&format!("token-{provider}"), provider);
|
|
170
|
+
#[cfg(unix)]
|
|
171
|
+
let carrier = crate::mcp_server::readiness::test_carrier_pid().unwrap();
|
|
172
|
+
#[cfg(not(unix))]
|
|
173
|
+
let carrier = std::process::id();
|
|
174
|
+
let config = case.workspace.join(".team/runtime/mcp/w1.json");
|
|
175
|
+
std::fs::create_dir_all(config.parent().unwrap()).unwrap();
|
|
176
|
+
std::fs::write(&config, serde_json::to_vec(&serde_json::json!({
|
|
177
|
+
"mcpServers": {"team_orchestrator": {
|
|
178
|
+
"command": std::env::current_exe().unwrap().canonicalize().unwrap(),
|
|
179
|
+
"env": {"TEAM_AGENT_WORKSPACE": case.workspace, "TEAM_AGENT_ID": "w1", "TEAM_AGENT_OWNER_TEAM_ID": "dup-team"}
|
|
180
|
+
}}
|
|
181
|
+
})).unwrap()).unwrap();
|
|
182
|
+
case.state["team_key"] = serde_json::json!("dup-team");
|
|
183
|
+
case.state["agents"]["w1"]["mcp_config"] = serde_json::json!(config);
|
|
184
|
+
case.state["agents"]["w1"]["pane_pid"] = serde_json::json!(carrier);
|
|
185
|
+
case.state["agents"]["w1"]["spawned_at"] = serde_json::json!((chrono::Utc::now() - chrono::Duration::seconds(10)).to_rfc3339());
|
|
186
|
+
case.state["teams"] = serde_json::json!({"dup-team": case.state.clone()});
|
|
187
|
+
crate::state::persist::save_runtime_state(&case.workspace, &case.state).unwrap();
|
|
188
|
+
#[cfg(unix)]
|
|
189
|
+
crate::mcp_server::readiness::publish(&case.workspace, "dup-team", "w1", chrono::Utc::now()).unwrap();
|
|
190
|
+
let transport = RecordingTransport { carrier: Some(carrier), ..RecordingTransport::new("") };
|
|
191
|
+
let message_id = case.store.create_message(None, "leader", "w1", &format!("{provider} canary"), None, false, Some("dup-team")).unwrap();
|
|
110
192
|
let first = deliver_pending_message(
|
|
111
193
|
&case.workspace,
|
|
112
194
|
&case.store,
|
|
@@ -273,6 +355,7 @@ fn poison_state_file(workspace: &Path) {
|
|
|
273
355
|
struct RecordingTransport {
|
|
274
356
|
injects: Arc<AtomicUsize>,
|
|
275
357
|
capture: Arc<Mutex<String>>,
|
|
358
|
+
carrier: Option<u32>,
|
|
276
359
|
}
|
|
277
360
|
|
|
278
361
|
impl RecordingTransport {
|
|
@@ -280,6 +363,7 @@ impl RecordingTransport {
|
|
|
280
363
|
Self {
|
|
281
364
|
injects: Arc::new(AtomicUsize::new(0)),
|
|
282
365
|
capture: Arc::new(Mutex::new(capture.to_string())),
|
|
366
|
+
carrier: None,
|
|
283
367
|
}
|
|
284
368
|
}
|
|
285
369
|
|
|
@@ -370,7 +454,7 @@ impl Transport for RecordingTransport {
|
|
|
370
454
|
current_command: Some("sleep".to_string()),
|
|
371
455
|
current_path: None,
|
|
372
456
|
active: true,
|
|
373
|
-
pane_pid:
|
|
457
|
+
pane_pid: self.carrier,
|
|
374
458
|
leader_env: BTreeMap::new(),
|
|
375
459
|
}])
|
|
376
460
|
}
|
|
@@ -169,7 +169,10 @@ fn p2_scheduler_unknown_kind_surfaces_error() {
|
|
|
169
169
|
|
|
170
170
|
/// A delivery transport whose `inject` RECORDS-and-succeeds (the real deliver loop legitimately
|
|
171
171
|
/// injects; the §84-guard NoopTransport panics). Only `inject` is reached by deliver_pending_message.
|
|
172
|
-
|
|
172
|
+
#[derive(Default)]
|
|
173
|
+
struct DeliverOkTransport {
|
|
174
|
+
targets: Vec<PaneInfo>,
|
|
175
|
+
}
|
|
173
176
|
impl Transport for DeliverOkTransport {
|
|
174
177
|
fn kind(&self) -> BackendKind {
|
|
175
178
|
BackendKind::Tmux
|
|
@@ -232,7 +235,7 @@ impl Transport for DeliverOkTransport {
|
|
|
232
235
|
Ok(PaneLiveness::Unknown)
|
|
233
236
|
}
|
|
234
237
|
fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
|
|
235
|
-
Ok(
|
|
238
|
+
Ok(self.targets.clone())
|
|
236
239
|
}
|
|
237
240
|
fn has_session(&self, _s: &SessionName) -> Result<bool, TransportError> {
|
|
238
241
|
Ok(true)
|
|
@@ -333,9 +336,9 @@ fn spine_delivery_skips_queued_statuses() {
|
|
|
333
336
|
.unwrap();
|
|
334
337
|
set_message_status(&store, &qidle, "queued_until_idle");
|
|
335
338
|
set_message_status(&store, &qstart, "queued_until_start");
|
|
336
|
-
let state = serde_json::json!({"agents": {"w1": {}}});
|
|
339
|
+
let state = serde_json::json!({"agents": {"w1": {"provider": "fake"}}});
|
|
337
340
|
|
|
338
|
-
let _ = deliver_pending_messages(&ws, &state, &DeliverOkTransport, &log).unwrap();
|
|
341
|
+
let _ = deliver_pending_messages(&ws, &state, &DeliverOkTransport::default(), &log).unwrap();
|
|
339
342
|
|
|
340
343
|
assert_eq!(
|
|
341
344
|
message_status_of(&store, &qidle),
|
|
@@ -359,9 +362,9 @@ fn spine_delivery_busy_recipient_is_injected_without_idle_deferral() {
|
|
|
359
362
|
let mid = store
|
|
360
363
|
.create_message(Some("t"), "leader", "w1", "hi", None, true, None)
|
|
361
364
|
.unwrap();
|
|
362
|
-
let state = serde_json::json!({"agents": {"w1": {"status": "busy"}}});
|
|
365
|
+
let state = serde_json::json!({"agents": {"w1": {"provider": "fake", "status": "busy"}}});
|
|
363
366
|
|
|
364
|
-
let delivered = deliver_pending_messages(&ws, &state, &DeliverOkTransport, &log).unwrap();
|
|
367
|
+
let delivered = deliver_pending_messages(&ws, &state, &DeliverOkTransport::default(), &log).unwrap();
|
|
365
368
|
|
|
366
369
|
assert!(delivered.contains(&mid));
|
|
367
370
|
assert_eq!(message_status_of(&store, &mid), "delivered");
|
|
@@ -380,11 +383,39 @@ fn spine_delivery_pi_cursor_uses_v113_direct_input_without_idle_scope() {
|
|
|
380
383
|
let store = store_for(&ws);
|
|
381
384
|
let log = EventLog::new(&ws);
|
|
382
385
|
let mid = store
|
|
383
|
-
.create_message(Some("t"), "leader", "w1", "hi", None, true,
|
|
386
|
+
.create_message(Some("t"), "leader", "w1", "hi", None, true, Some("spine-team"))
|
|
384
387
|
.unwrap();
|
|
385
|
-
|
|
386
|
-
let
|
|
387
|
-
|
|
388
|
+
#[cfg(unix)]
|
|
389
|
+
let carrier = crate::mcp_server::readiness::test_carrier_pid().unwrap();
|
|
390
|
+
#[cfg(not(unix))]
|
|
391
|
+
let carrier = std::process::id();
|
|
392
|
+
let config = ws.join(".team/runtime/mcp/w1.json");
|
|
393
|
+
std::fs::create_dir_all(config.parent().unwrap()).unwrap();
|
|
394
|
+
std::fs::write(&config, serde_json::to_vec(&serde_json::json!({
|
|
395
|
+
"mcpServers": {"team_orchestrator": {
|
|
396
|
+
"command": std::env::current_exe().unwrap().canonicalize().unwrap(),
|
|
397
|
+
"env": {"TEAM_AGENT_WORKSPACE": ws, "TEAM_AGENT_ID": "w1", "TEAM_AGENT_OWNER_TEAM_ID": "spine-team"}
|
|
398
|
+
}}
|
|
399
|
+
})).unwrap()).unwrap();
|
|
400
|
+
let mut state = serde_json::json!({"team_key": "spine-team", "active_team_key": "spine-team", "session_name": "spine", "agents": {"w1": {
|
|
401
|
+
"provider": provider, "status": "running", "window": "w1", "mcp_config": config,
|
|
402
|
+
"spawned_at": (chrono::Utc::now() - chrono::Duration::seconds(10)).to_rfc3339(),
|
|
403
|
+
"pane_id": "%spine", "pane_pid": carrier
|
|
404
|
+
}}});
|
|
405
|
+
state["teams"] = serde_json::json!({"spine-team": state.clone()});
|
|
406
|
+
crate::state::persist::save_runtime_state(&ws, &state).unwrap();
|
|
407
|
+
#[cfg(unix)]
|
|
408
|
+
if provider == "cursor_agent" {
|
|
409
|
+
crate::mcp_server::readiness::publish(&ws, "spine-team", "w1", chrono::Utc::now()).unwrap();
|
|
410
|
+
}
|
|
411
|
+
let transport = DeliverOkTransport { targets: vec![PaneInfo {
|
|
412
|
+
pane_id: PaneId::new("%spine"), session: SessionName::new("spine"),
|
|
413
|
+
window_index: None, window_name: Some(WindowName::new("w1")), pane_index: None, tty: None,
|
|
414
|
+
current_command: None, current_path: None, active: true,
|
|
415
|
+
pane_pid: Some(carrier), leader_env: BTreeMap::new(),
|
|
416
|
+
}] };
|
|
417
|
+
let delivered = deliver_pending_messages(&ws, &state, &transport, &log).unwrap();
|
|
418
|
+
assert!(delivered.contains(&mid), "{provider}: {:?}", read_event_log(&ws));
|
|
388
419
|
assert_eq!(message_status_of(&store, &mid), "delivered");
|
|
389
420
|
}
|
|
390
421
|
}
|
|
@@ -405,9 +436,9 @@ fn contract_alive_worker_with_working_health_is_deliverable_not_deferred() {
|
|
|
405
436
|
.create_message(Some("t"), "leader", "w1", "hi", None, true, None)
|
|
406
437
|
.unwrap();
|
|
407
438
|
seed_agent_health(&store, "w1", "WORKING"); // turn-level state — must NOT gate delivery
|
|
408
|
-
let state = serde_json::json!({"agents": {"w1": {"status": "running"}}}); // lifecycle: alive
|
|
439
|
+
let state = serde_json::json!({"agents": {"w1": {"provider": "fake", "status": "running"}}}); // lifecycle: alive
|
|
409
440
|
|
|
410
|
-
let delivered = deliver_pending_messages(&ws, &state, &DeliverOkTransport, &log).unwrap();
|
|
441
|
+
let delivered = deliver_pending_messages(&ws, &state, &DeliverOkTransport::default(), &log).unwrap();
|
|
411
442
|
|
|
412
443
|
let events = read_event_log(&ws);
|
|
413
444
|
let deferred: Vec<_> = events
|
|
@@ -530,7 +561,7 @@ fn spine_delivery_injects_resolvable_target_not_bare_agent_pane() {
|
|
|
530
561
|
// in-team, non-busy agent -> deliver proceeds; the session-qualified resolution uses session_name + window.
|
|
531
562
|
let state = serde_json::json!({
|
|
532
563
|
"session_name": "team-x",
|
|
533
|
-
"agents": {"w1": {"status": "idle", "window": "w1"}}
|
|
564
|
+
"agents": {"w1": {"provider": "fake", "status": "idle", "window": "w1"}}
|
|
534
565
|
});
|
|
535
566
|
let transport = OfflineTransport::new();
|
|
536
567
|
|
|
@@ -585,7 +616,7 @@ fn spine_delivery_injects_rendered_protocol_block_not_raw_content() {
|
|
|
585
616
|
.unwrap();
|
|
586
617
|
let state = serde_json::json!({
|
|
587
618
|
"session_name": "team-x",
|
|
588
|
-
"agents": {"w1": {"status": "idle", "window": "w1"}}
|
|
619
|
+
"agents": {"w1": {"provider": "fake", "status": "idle", "window": "w1"}}
|
|
589
620
|
});
|
|
590
621
|
let transport = OfflineTransport::new();
|
|
591
622
|
|
|
@@ -214,6 +214,13 @@ pub trait ProviderAdapter {
|
|
|
214
214
|
/// compatible_api 走 `ensure_compatible_claude_mcp_config`)。
|
|
215
215
|
fn mcp_config(&self, auth_mode: AuthMode) -> Result<McpConfig, ProviderError>;
|
|
216
216
|
|
|
217
|
+
/// Eager stdio clients must publish their initial MCP handshake before input.
|
|
218
|
+
/// Pi owns admission in its runtime registry hooks; Fake has no MCP client.
|
|
219
|
+
/// The process-stamp receipt port is currently POSIX-only, not a Windows claim.
|
|
220
|
+
fn requires_worker_mcp_handshake(&self) -> bool {
|
|
221
|
+
cfg!(unix) && !matches!(self.provider(), Provider::Fake | Provider::Pi)
|
|
222
|
+
}
|
|
223
|
+
|
|
217
224
|
/// 安装 MCP server。gemini 写全局 `~/.gemini/settings.json` 并备份/还原
|
|
218
225
|
/// (`gemini.py:40-78`)——有副作用 I/O,失败可还原 → Result。
|
|
219
226
|
fn install_mcp(&self, config: &McpConfig) -> Result<(), ProviderError>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.122",
|
|
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.122",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.122",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.122"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|