@team-agent/installer 0.5.0 → 0.5.2

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 (40) hide show
  1. package/Cargo.lock +118 -9
  2. package/Cargo.toml +2 -2
  3. package/crates/team-agent/Cargo.toml +1 -0
  4. package/crates/team-agent/src/app_server_test_support.rs +258 -0
  5. package/crates/team-agent/src/cli/adapters.rs +32 -3
  6. package/crates/team-agent/src/cli/attach_app_server_leader.rs +16 -0
  7. package/crates/team-agent/src/cli/diagnose.rs +14 -5
  8. package/crates/team-agent/src/cli/emit.rs +111 -3
  9. package/crates/team-agent/src/cli/mod.rs +72 -25
  10. package/crates/team-agent/src/cli/named_address.rs +975 -0
  11. package/crates/team-agent/src/cli/send.rs +200 -1
  12. package/crates/team-agent/src/cli/status_port.rs +44 -6
  13. package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -0
  14. package/crates/team-agent/src/cli/tests/mod.rs +1 -0
  15. package/crates/team-agent/src/cli/tests/named_address.rs +586 -0
  16. package/crates/team-agent/src/cli/tests/run_delegation.rs +2 -0
  17. package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
  18. package/crates/team-agent/src/cli/types.rs +21 -0
  19. package/crates/team-agent/src/codex_app_server.rs +488 -0
  20. package/crates/team-agent/src/conpty/backend.rs +710 -0
  21. package/crates/team-agent/src/conpty/mod.rs +32 -0
  22. package/crates/team-agent/src/coordinator/backoff.rs +45 -6
  23. package/crates/team-agent/src/diagnose/orphans.rs +11 -3
  24. package/crates/team-agent/src/leader/lease.rs +144 -7
  25. package/crates/team-agent/src/leader/owner_bind.rs +5 -2
  26. package/crates/team-agent/src/leader/start.rs +18 -5
  27. package/crates/team-agent/src/leader/tests/lease_api.rs +172 -0
  28. package/crates/team-agent/src/lib.rs +23 -0
  29. package/crates/team-agent/src/lifecycle/launch.rs +127 -3
  30. package/crates/team-agent/src/lifecycle/restart/common.rs +70 -0
  31. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +7 -0
  32. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +90 -0
  33. package/crates/team-agent/src/messaging/delivery.rs +329 -9
  34. package/crates/team-agent/src/messaging/leader_receiver.rs +17 -138
  35. package/crates/team-agent/src/messaging/mod.rs +2 -2
  36. package/crates/team-agent/src/messaging/results.rs +78 -21
  37. package/crates/team-agent/src/messaging/tests/runtime.rs +232 -0
  38. package/crates/team-agent/src/tmux_backend.rs +31 -0
  39. package/crates/team-agent/src/transport_factory.rs +632 -0
  40. package/package.json +4 -4
@@ -7,6 +7,40 @@ use crate::messaging::{DeliveryOutcome, DeliveryRefusal, DeliveryStage, Delivery
7
7
  /// `cmd_send`(`commands.py:164`)。解析 target(`--to` fanout / 单 target / `*`)→ [`MessageTarget`],
8
8
  /// 拼 [`SendOptions`](no_ack→requires_ack 取反、no_wait→wait_visible 取反等)→ `messaging::send_message`。
9
9
  pub fn cmd_send(args: &SendArgs) -> Result<CmdResult, CliError> {
10
+ if let Some(ref to_name) = args.to_name {
11
+ if args.pane.is_some() || args.target.is_some() || args.targets.is_some() {
12
+ return Err(CliError::Usage(
13
+ "--to-name and --pane/TARGET/--to are mutually exclusive: \
14
+ --to-name resolves a stable workspace/team/name to a live pane"
15
+ .to_string(),
16
+ ));
17
+ }
18
+ let content = args.message.join(" ");
19
+ if content.is_empty() {
20
+ return Err(CliError::Usage(
21
+ "--to-name requires a non-empty message".to_string(),
22
+ ));
23
+ }
24
+ let (resolved, transport) =
25
+ match crate::cli::named_address::resolve_name_for_cli(&args.workspace, to_name) {
26
+ Ok(resolved) => resolved,
27
+ Err(error) if args.json => {
28
+ return Ok(CmdResult::from_json(error.to_json(), args.json));
29
+ }
30
+ Err(error) => return Err(CliError::Usage(error.n38_message())),
31
+ };
32
+ let mut value = send_to_named_pane_direct(
33
+ &args.workspace,
34
+ transport.as_ref(),
35
+ &resolved,
36
+ &content,
37
+ &args.sender,
38
+ args.task.as_deref(),
39
+ args.json,
40
+ )?;
41
+ add_send_reminder_if_ok(&mut value);
42
+ return Ok(CmdResult::from_json(value, args.json));
43
+ }
10
44
  // F1 (0.3.26, cross-team send): --pane <pane_id> direct targeting.
11
45
  // Mutually exclusive with target / --to (agent-name routing).
12
46
  if let Some(ref pane_id) = args.pane {
@@ -20,7 +54,9 @@ pub fn cmd_send(args: &SendArgs) -> Result<CmdResult, CliError> {
20
54
  }
21
55
  let content = args.message.join(" ");
22
56
  if content.is_empty() {
23
- return Err(CliError::Usage("--pane requires a non-empty message".to_string()));
57
+ return Err(CliError::Usage(
58
+ "--pane requires a non-empty message".to_string(),
59
+ ));
24
60
  }
25
61
  let mut value = send_to_pane_direct(
26
62
  &args.workspace,
@@ -145,6 +181,169 @@ fn send_to_pane_direct(
145
181
  }))
146
182
  }
147
183
 
184
+ fn send_to_named_pane_direct(
185
+ sender_workspace: &Path,
186
+ transport: &dyn crate::transport::Transport,
187
+ resolved: &crate::cli::named_address::ResolvedNamedAddress,
188
+ content: &str,
189
+ sender: &str,
190
+ task_id: Option<&str>,
191
+ _json: bool,
192
+ ) -> Result<serde_json::Value, CliError> {
193
+ use crate::messaging::delivery::render_message;
194
+ use crate::transport::{InjectPayload, Key, PaneId, Target};
195
+
196
+ let message_id = format!("named_send_{}", chrono::Utc::now().timestamp_millis());
197
+ let rendered = render_message(sender, task_id, content, &message_id);
198
+ let sender_run_workspace = crate::model::paths::canonical_run_workspace(sender_workspace)
199
+ .unwrap_or_else(|_| sender_workspace.to_path_buf());
200
+ let event_log = crate::event_log::EventLog::new(&sender_run_workspace);
201
+ if let Some(warning) = &resolved.warning {
202
+ eprintln!("warning: {warning}");
203
+ }
204
+ if resolved.transport_kind.as_deref() == Some("codex_app_server") {
205
+ return send_to_named_app_server_leader(
206
+ &event_log,
207
+ resolved,
208
+ &message_id,
209
+ &rendered,
210
+ sender,
211
+ );
212
+ }
213
+ let target = Target::Pane(PaneId::new(&resolved.pane_id));
214
+ let report = transport
215
+ .inject(&target, &InjectPayload::Text(rendered), Key::Enter, true)
216
+ .map_err(|e| {
217
+ CliError::Runtime(format!(
218
+ "inject to named target {} pane {} failed: {e}",
219
+ resolved.raw_name, resolved.pane_id
220
+ ))
221
+ })?;
222
+ let target_kind = named_target_kind_wire(resolved.target_kind);
223
+ let event = serde_json::json!({
224
+ "to_name": resolved.raw_name,
225
+ "target_kind": target_kind,
226
+ "sender": sender,
227
+ "sender_workspace": sender_run_workspace.display().to_string(),
228
+ "target_workspace": resolved.target_workspace.display().to_string(),
229
+ "team_key": resolved.team_key,
230
+ "agent_id": resolved.agent_id,
231
+ "pane_id": resolved.pane_id,
232
+ "session_name": resolved.session_name,
233
+ "window_name": resolved.window_name,
234
+ "tmux_endpoint": resolved.tmux_endpoint,
235
+ "state_pane_id": resolved.state_pane_id,
236
+ "state_pane_stale": resolved.state_pane_stale,
237
+ "agent_status": resolved.agent_status,
238
+ "warning": resolved.warning,
239
+ "message_id": message_id,
240
+ "submit_verification": crate::transport::submit_verification_wire(report.submit_verification),
241
+ "inject_verification": format!("{:?}", report.inject_verification),
242
+ });
243
+ let _ = event_log.write("send.name_direct", event.clone());
244
+ let ok = matches!(
245
+ report.submit_verification,
246
+ crate::transport::SubmitVerification::EnterSentWithoutPlaceholderCheck
247
+ | crate::transport::SubmitVerification::PastedContentPromptAbsentAfterSubmit
248
+ | crate::transport::SubmitVerification::KeySentAfterVisibleToken { .. }
249
+ );
250
+ let mut value = event;
251
+ if let Some(obj) = value.as_object_mut() {
252
+ obj.insert("ok".to_string(), serde_json::json!(ok));
253
+ }
254
+ Ok(value)
255
+ }
256
+
257
+ fn send_to_named_app_server_leader(
258
+ event_log: &crate::event_log::EventLog,
259
+ resolved: &crate::cli::named_address::ResolvedNamedAddress,
260
+ message_id: &str,
261
+ rendered: &str,
262
+ sender: &str,
263
+ ) -> Result<serde_json::Value, CliError> {
264
+ let receiver = serde_json::json!({
265
+ "mode": "codex_app_server",
266
+ "transport_kind": "codex_app_server",
267
+ "app_server": resolved.app_server.clone().unwrap_or(serde_json::Value::Null),
268
+ });
269
+ let binding = crate::codex_app_server::binding_from_receiver(&receiver)
270
+ .map_err(|error| CliError::Runtime(format!("invalid app-server named leader: {error}")))?;
271
+ let target_kind = named_target_kind_wire(resolved.target_kind);
272
+ let base_event = serde_json::json!({
273
+ "to_name": resolved.raw_name,
274
+ "target_kind": target_kind,
275
+ "sender": sender,
276
+ "sender_workspace": resolved.sender_workspace.display().to_string(),
277
+ "target_workspace": resolved.target_workspace.display().to_string(),
278
+ "team_key": resolved.team_key,
279
+ "agent_id": resolved.agent_id,
280
+ "transport_kind": "codex_app_server",
281
+ "socket": binding.socket,
282
+ "thread_id": binding.thread_id,
283
+ "message_id": message_id,
284
+ });
285
+ match crate::codex_app_server::submit_to_bound_thread(&binding, message_id, rendered) {
286
+ Ok(submit) => {
287
+ let event = merge_json(
288
+ base_event.clone(),
289
+ serde_json::json!({
290
+ "ok": true,
291
+ "turn_id": submit.turn_id,
292
+ "turn_status": submit.turn_status,
293
+ }),
294
+ );
295
+ let _ = event_log.write("send.name_app_server", event.clone());
296
+ Ok(event)
297
+ }
298
+ Err(crate::codex_app_server::AppServerError::LeaderBusy(message)) => {
299
+ let event = merge_json(
300
+ base_event.clone(),
301
+ serde_json::json!({
302
+ "ok": false,
303
+ "status": "retry_scheduled",
304
+ "reason": "leader_busy",
305
+ "channel": "leader_busy",
306
+ "error": message,
307
+ }),
308
+ );
309
+ let _ = event_log.write("send.name_app_server", event.clone());
310
+ Ok(event)
311
+ }
312
+ Err(error) => {
313
+ let event = merge_json(
314
+ base_event.clone(),
315
+ serde_json::json!({
316
+ "ok": false,
317
+ "status": "refused",
318
+ "reason": error.code(),
319
+ "channel": "rebind_required",
320
+ "error": error.to_string(),
321
+ "action": "run team-agent attach-app-server-leader for the target team",
322
+ }),
323
+ );
324
+ let _ = event_log.write("send.name_app_server", event.clone());
325
+ Ok(event)
326
+ }
327
+ }
328
+ }
329
+
330
+ fn merge_json(mut left: serde_json::Value, right: serde_json::Value) -> serde_json::Value {
331
+ if let (Some(left), Some(right)) = (left.as_object_mut(), right.as_object()) {
332
+ for (key, value) in right {
333
+ left.insert(key.clone(), value.clone());
334
+ }
335
+ }
336
+ left
337
+ }
338
+
339
+ fn named_target_kind_wire(kind: crate::cli::named_address::NamedTargetKind) -> &'static str {
340
+ match kind {
341
+ crate::cli::named_address::NamedTargetKind::Worker => "worker",
342
+ crate::cli::named_address::NamedTargetKind::Leader => "leader",
343
+ crate::cli::named_address::NamedTargetKind::SessionWindow => "session_window",
344
+ }
345
+ }
346
+
148
347
  pub fn cmd_fallback_send_leader(args: &FallbackSendLeaderArgs) -> Result<CmdResult, CliError> {
149
348
  if let Some(value) = fallback_business_refusal(&args.primary_error, args.json) {
150
349
  return Ok(value);
@@ -280,7 +280,29 @@ use rusqlite::params;
280
280
  if let (Some(session), Some(agents)) = (session, state.get("agents").and_then(Value::as_object)) {
281
281
  let run_ws = crate::model::paths::canonical_run_workspace(workspace)
282
282
  .unwrap_or_else(|_| workspace.to_path_buf());
283
- let backend = crate::tmux_backend::TmuxBackend::for_workspace(&run_ws);
283
+ // 0.5.x Phase 1d Batch 3: use the factory-resolved backend
284
+ // so conpty teams get their scrollback from the shim rather
285
+ // than a fake tmux capture that always returns empty. Tmux
286
+ // teams take the same code path as before (byte-equivalent).
287
+ let resolved = crate::transport_factory::resolve_read_only_transport(
288
+ &run_ws,
289
+ Some(state),
290
+ crate::transport_factory::TransportPurpose::Status,
291
+ );
292
+ let backend: Box<dyn crate::transport::Transport> = match resolved {
293
+ Ok(r) => r.backend,
294
+ Err(_) => {
295
+ // Read-path fallback: refused factory means we don't
296
+ // try to inspect approval prompts. Empty vec = no
297
+ // waiting approvals, honest.
298
+ return Ok(json!({
299
+ "ok": true,
300
+ "waiting": false,
301
+ "waiting_count": 0,
302
+ "approvals": [],
303
+ }));
304
+ }
305
+ };
284
306
  for (agent_id, agent_state) in agents {
285
307
  if agent.is_some_and(|wanted| wanted != agent_id) {
286
308
  continue;
@@ -469,14 +491,30 @@ use rusqlite::params;
469
491
  }
470
492
  let run_ws = crate::model::paths::canonical_run_workspace(workspace)
471
493
  .unwrap_or_else(|_| workspace.to_path_buf());
472
- let selection = crate::tmux_backend::tmux_backend_for_runtime_state_or_workspace(
494
+ // 0.5.x Phase 1d Batch 3: route through the factory so a
495
+ // conpty team does NOT get its `has_session` probe served by a
496
+ // tmux backend (which would always return false and drive the
497
+ // reader into a false `tmux_session_missing` state — design
498
+ // §Batch 3 Verification anchor). Tmux teams see byte-equivalent
499
+ // behavior because factory Layer 3 (legacy tmux endpoint) uses
500
+ // the same `tmux_backend_for_runtime_state_or_workspace` shape.
501
+ let resolved = crate::transport_factory::resolve_read_only_transport(
473
502
  &run_ws,
474
503
  Some(state),
504
+ crate::transport_factory::TransportPurpose::Status,
475
505
  );
476
- selection
477
- .backend
478
- .has_session(&crate::transport::SessionName::new(name))
479
- .unwrap_or(false)
506
+ match resolved {
507
+ Ok(r) => r
508
+ .backend
509
+ .has_session(&crate::transport::SessionName::new(name))
510
+ .unwrap_or(false),
511
+ Err(_) => {
512
+ // Factory refused (e.g. explicit conpty without a
513
+ // resolvable team_key). Honest: return false rather
514
+ // than pretend a tmux session exists.
515
+ false
516
+ }
517
+ }
480
518
  }
481
519
 
482
520
  fn message_counts(conn: &rusqlite::Connection, owner_team_id: Option<&str>) -> Result<Value, CliError> {
@@ -50,6 +50,7 @@ use super::*;
50
50
  json: true,
51
51
  message_id: None,
52
52
  pane: None,
53
+ to_name: None,
53
54
  };
54
55
  let _ = cmd_send(&args);
55
56
 
@@ -87,6 +87,7 @@ mod divergence;
87
87
  mod lane_c;
88
88
  mod leader_watch;
89
89
  mod missing_subcommands;
90
+ mod named_address;
90
91
  mod repair_state_byte_lock;
91
92
  mod peer_allow;
92
93
  mod run_delegation;