@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.
- package/Cargo.lock +118 -9
- package/Cargo.toml +2 -2
- package/crates/team-agent/Cargo.toml +1 -0
- package/crates/team-agent/src/app_server_test_support.rs +258 -0
- package/crates/team-agent/src/cli/adapters.rs +32 -3
- package/crates/team-agent/src/cli/attach_app_server_leader.rs +16 -0
- package/crates/team-agent/src/cli/diagnose.rs +14 -5
- package/crates/team-agent/src/cli/emit.rs +111 -3
- package/crates/team-agent/src/cli/mod.rs +72 -25
- package/crates/team-agent/src/cli/named_address.rs +975 -0
- package/crates/team-agent/src/cli/send.rs +200 -1
- package/crates/team-agent/src/cli/status_port.rs +44 -6
- 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 +586 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +2 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +21 -0
- package/crates/team-agent/src/codex_app_server.rs +488 -0
- package/crates/team-agent/src/conpty/backend.rs +710 -0
- package/crates/team-agent/src/conpty/mod.rs +32 -0
- package/crates/team-agent/src/coordinator/backoff.rs +45 -6
- package/crates/team-agent/src/diagnose/orphans.rs +11 -3
- package/crates/team-agent/src/leader/lease.rs +144 -7
- package/crates/team-agent/src/leader/owner_bind.rs +5 -2
- package/crates/team-agent/src/leader/start.rs +18 -5
- package/crates/team-agent/src/leader/tests/lease_api.rs +172 -0
- package/crates/team-agent/src/lib.rs +23 -0
- package/crates/team-agent/src/lifecycle/launch.rs +127 -3
- package/crates/team-agent/src/lifecycle/restart/common.rs +70 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +7 -0
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +90 -0
- package/crates/team-agent/src/messaging/delivery.rs +329 -9
- package/crates/team-agent/src/messaging/leader_receiver.rs +17 -138
- package/crates/team-agent/src/messaging/mod.rs +2 -2
- package/crates/team-agent/src/messaging/results.rs +78 -21
- package/crates/team-agent/src/messaging/tests/runtime.rs +232 -0
- package/crates/team-agent/src/tmux_backend.rs +31 -0
- package/crates/team-agent/src/transport_factory.rs +632 -0
- 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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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> {
|