@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.
Files changed (29) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/intent/runtime.rs +46 -10
  4. package/crates/team-agent/src/cli/intent/runtime_tests.rs +114 -0
  5. package/crates/team-agent/src/cli/mod.rs +4 -1
  6. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +56 -0
  7. package/crates/team-agent/src/coordinator/tests/health_sync.rs +27 -8
  8. package/crates/team-agent/src/db/message_store.rs +65 -0
  9. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +8 -0
  10. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +111 -4
  11. package/crates/team-agent/src/lifecycle/launch/cursor_mcp_iso.rs +244 -82
  12. package/crates/team-agent/src/lifecycle/launch.rs +1 -0
  13. package/crates/team-agent/src/lifecycle/restart/agent.rs +3 -0
  14. package/crates/team-agent/src/lifecycle/restart/common.rs +10 -1
  15. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +5 -0
  16. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +16 -3
  17. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +60 -17
  18. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +162 -8
  19. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +55 -6
  20. package/crates/team-agent/src/mcp_server/mod.rs +2 -0
  21. package/crates/team-agent/src/mcp_server/readiness.rs +495 -0
  22. package/crates/team-agent/src/mcp_server/readiness_tests.rs +272 -0
  23. package/crates/team-agent/src/mcp_server/wire.rs +96 -11
  24. package/crates/team-agent/src/messaging/delivery.rs +62 -0
  25. package/crates/team-agent/src/messaging/delivery_consumption_tests.rs +12 -1
  26. package/crates/team-agent/src/messaging/tests/dup_inject.rs +89 -5
  27. package/crates/team-agent/src/messaging/tests/spine.rs +45 -14
  28. package/crates/team-agent/src/provider/adapter.rs +7 -0
  29. 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", "cursor"] {
107
- let case = WorkerCase::new(&format!("token-{provider}"), provider);
108
- let transport = RecordingTransport::new("");
109
- let message_id = case.seed_leader_to_worker(&format!("{provider} canary"));
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: None,
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
- struct DeliverOkTransport;
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(Vec::new())
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, None)
386
+ .create_message(Some("t"), "leader", "w1", "hi", None, true, Some("spine-team"))
384
387
  .unwrap();
385
- let state = serde_json::json!({"agents": {"w1": {"provider": provider, "status": "running"}}});
386
- let delivered = deliver_pending_messages(&ws, &state, &DeliverOkTransport, &log).unwrap();
387
- assert!(delivered.contains(&mid), "{provider}");
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.120",
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.120",
24
- "@team-agent/cli-darwin-x64": "0.5.120",
25
- "@team-agent/cli-linux-x64": "0.5.120"
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",