@team-agent/installer 0.5.48 → 0.5.50
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 -1
- package/crates/team-agent/src/cli/emit.rs +89 -38
- package/crates/team-agent/src/cli/mod.rs +1 -1
- package/crates/team-agent/src/cli/named_address.rs +127 -25
- package/crates/team-agent/src/cli/send.rs +442 -521
- package/crates/team-agent/src/cli/spec.rs +1 -1
- package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -1
- package/crates/team-agent/src/cli/tests/named_address.rs +32 -7
- package/crates/team-agent/src/cli/tests/run_delegation.rs +2 -4
- package/crates/team-agent/src/cli/tests/status_send.rs +59 -33
- package/crates/team-agent/src/cli/types.rs +4 -8
- package/crates/team-agent/src/lifecycle/launch.rs +23 -9
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +33 -16
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +2 -2
- package/crates/team-agent/src/mcp_server/mod.rs +1 -1
- package/crates/team-agent/src/mcp_server/tests/send.rs +16 -5
- package/crates/team-agent/src/mcp_server/tests/wire.rs +6 -0
- package/crates/team-agent/src/mcp_server/tools.rs +12 -10
- package/crates/team-agent/src/mcp_server/wire.rs +2 -18
- package/crates/team-agent/src/messaging/mod.rs +1 -0
- package/crates/team-agent/src/messaging/send.rs +34 -11
- package/package.json +4 -4
|
@@ -7,133 +7,63 @@ 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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
// mailbox (`queued_until_leader_attach` / `leader_mailbox`) both
|
|
16
|
-
// funnel through one code path. Mutually exclusive with
|
|
17
|
-
// `--to-name`, TARGET/--to, `--pane`.
|
|
18
|
-
if args.to_name.is_some()
|
|
19
|
-
|| args.pane.is_some()
|
|
20
|
-
|| args.target.is_some()
|
|
10
|
+
// F1 (0.3.26, cross-team send): --pane <pane_id> direct targeting.
|
|
11
|
+
// Mutually exclusive with target / --to (agent-name routing).
|
|
12
|
+
if let Some(ref pane_id) = args.pane {
|
|
13
|
+
warn_send_alias("--pane");
|
|
14
|
+
if args.target.is_some()
|
|
21
15
|
|| args.targets.is_some()
|
|
16
|
+
|| args.to_name.is_some()
|
|
17
|
+
|| args.to_leader.is_some()
|
|
22
18
|
{
|
|
19
|
+
let message = if args.to_name.is_some() {
|
|
20
|
+
"--to-name and --pane/TARGET/--to are mutually exclusive"
|
|
21
|
+
} else {
|
|
22
|
+
"--pane and TARGET/--to are mutually exclusive; --pane also conflicts with --to-leader"
|
|
23
|
+
};
|
|
23
24
|
return Err(CliError::Usage(
|
|
24
|
-
|
|
25
|
-
--to-leader resolves a host leader delivery name via the leader registry"
|
|
26
|
-
.to_string(),
|
|
25
|
+
message.to_string(),
|
|
27
26
|
));
|
|
28
27
|
}
|
|
29
28
|
let content = args.message.join(" ");
|
|
30
29
|
if content.is_empty() {
|
|
31
30
|
return Err(CliError::Usage(
|
|
32
|
-
"--
|
|
31
|
+
"--pane requires a non-empty message".to_string(),
|
|
33
32
|
));
|
|
34
33
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
&content,
|
|
39
|
-
&args.sender,
|
|
40
|
-
args.task.as_deref(),
|
|
41
|
-
)?;
|
|
42
|
-
return Ok(cmd_send_result(value, args.json));
|
|
34
|
+
return Err(CliError::Usage(format!(
|
|
35
|
+
"--pane {pane_id} is deprecated and sunset; use a logical TARGET so the message is persisted before delivery"
|
|
36
|
+
)));
|
|
43
37
|
}
|
|
44
|
-
if
|
|
45
|
-
|
|
46
|
-
return Err(CliError::Usage(
|
|
47
|
-
"--to-name and --pane/TARGET/--to are mutually exclusive: \
|
|
48
|
-
--to-name resolves a stable workspace/team/name to a live pane"
|
|
49
|
-
.to_string(),
|
|
50
|
-
));
|
|
51
|
-
}
|
|
52
|
-
let content = args.message.join(" ");
|
|
53
|
-
if content.is_empty() {
|
|
54
|
-
return Err(CliError::Usage(
|
|
55
|
-
"--to-name requires a non-empty message".to_string(),
|
|
56
|
-
));
|
|
57
|
-
}
|
|
58
|
-
// 0.5.45 naming-addressing (design §3.1, RED-1): thread
|
|
59
|
-
// `--team` down to resolver so bare `--to-name agent --team T`
|
|
60
|
-
// scopes to `T` BEFORE workspace scanning. Qualified addresses
|
|
61
|
-
// (`team/agent`, `workspace::team/agent`) ignore the scope
|
|
62
|
-
// per §1 priority ladder.
|
|
63
|
-
let (resolved, transport) =
|
|
64
|
-
match crate::cli::named_address::resolve_name_for_cli(
|
|
65
|
-
&args.workspace,
|
|
66
|
-
to_name,
|
|
67
|
-
args.team.as_deref(),
|
|
68
|
-
) {
|
|
69
|
-
Ok(resolved) => resolved,
|
|
70
|
-
Err(error) => {
|
|
71
|
-
// E6 (0.5.9 offline-mailbox-toname-design §3.1/§6.2): when
|
|
72
|
-
// the resolver refuses with `leader_not_attached`, the team
|
|
73
|
-
// itself may still be alive (worker + coordinator running
|
|
74
|
-
// without a bound leader). Third-party senders in that
|
|
75
|
-
// shape must land in the offline mailbox — same
|
|
76
|
-
// canonical team.db row + queued_until_leader_attach
|
|
77
|
-
// status the coordinator/attach hook replay through the
|
|
78
|
-
// existing pipeline exactly once. Owner-scope refusals
|
|
79
|
-
// (same workspace as target) keep the actionable attach
|
|
80
|
-
// hint — E6 owner copy is documented as
|
|
81
|
-
// `run team-agent attach-leader`.
|
|
82
|
-
if let Some(mut value) = maybe_enqueue_offline_leader_mailbox(
|
|
83
|
-
&args.workspace,
|
|
84
|
-
to_name,
|
|
85
|
-
&content,
|
|
86
|
-
&args.sender,
|
|
87
|
-
args.task.as_deref(),
|
|
88
|
-
&error,
|
|
89
|
-
)? {
|
|
90
|
-
add_send_reminder_if_ok(&mut value);
|
|
91
|
-
return Ok(cmd_send_result(value, args.json));
|
|
92
|
-
}
|
|
93
|
-
if args.json {
|
|
94
|
-
return Ok(CmdResult::from_json(error.to_json(), args.json));
|
|
95
|
-
}
|
|
96
|
-
return Err(CliError::Usage(error.n38_message()));
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
let mut value = send_to_named_pane_direct(
|
|
100
|
-
&args.workspace,
|
|
101
|
-
transport.as_ref(),
|
|
102
|
-
&resolved,
|
|
103
|
-
&content,
|
|
104
|
-
&args.sender,
|
|
105
|
-
args.task.as_deref(),
|
|
106
|
-
args.json,
|
|
107
|
-
)?;
|
|
108
|
-
add_send_reminder_if_ok(&mut value);
|
|
109
|
-
return Ok(cmd_send_result(value, args.json));
|
|
38
|
+
if args.targets.is_some() {
|
|
39
|
+
warn_send_alias("--targets");
|
|
110
40
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
))
|
|
41
|
+
if args.to_name.is_some() {
|
|
42
|
+
warn_send_alias("--to-name");
|
|
43
|
+
}
|
|
44
|
+
if args.to_leader.is_some() {
|
|
45
|
+
warn_send_alias("--to-leader");
|
|
46
|
+
}
|
|
47
|
+
let host_leader_alias = if let Some(name) = args.to_leader.as_deref() {
|
|
48
|
+
match resolve_host_leader_alias(name) {
|
|
49
|
+
Ok(resolved) => Some(resolved),
|
|
50
|
+
Err(value) => return Ok(cmd_send_result(value, args.json)),
|
|
121
51
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
52
|
+
} else {
|
|
53
|
+
None
|
|
54
|
+
};
|
|
55
|
+
let logical_to = logical_to_from_args(
|
|
56
|
+
args,
|
|
57
|
+
host_leader_alias
|
|
58
|
+
.as_ref()
|
|
59
|
+
.map(|(logical_to, _)| logical_to.as_str()),
|
|
60
|
+
)?;
|
|
61
|
+
let content = args.message.join(" ");
|
|
62
|
+
if !logical_to.is_empty() && logical_to != "*" && !content.is_empty() {
|
|
63
|
+
let mut value = send_to_logical_to(args, &logical_to, &content)?;
|
|
64
|
+
if let Some((_, entry)) = host_leader_alias.as_ref() {
|
|
65
|
+
decorate_host_leader_alias(&mut value, entry);
|
|
127
66
|
}
|
|
128
|
-
let mut value = send_to_pane_direct(
|
|
129
|
-
&args.workspace,
|
|
130
|
-
pane_id,
|
|
131
|
-
&content,
|
|
132
|
-
&args.sender,
|
|
133
|
-
args.task.as_deref(),
|
|
134
|
-
args.team.as_deref(),
|
|
135
|
-
args.json,
|
|
136
|
-
)?;
|
|
137
67
|
add_send_reminder_if_ok(&mut value);
|
|
138
68
|
return Ok(cmd_send_result(value, args.json));
|
|
139
69
|
}
|
|
@@ -142,12 +72,12 @@ pub fn cmd_send(args: &SendArgs) -> Result<CmdResult, CliError> {
|
|
|
142
72
|
args.team.as_deref(),
|
|
143
73
|
crate::state::selector::SelectorMode::RuntimeOnly,
|
|
144
74
|
)?;
|
|
145
|
-
let target = send_target(
|
|
75
|
+
let target = send_target(None, Some(logical_to.as_str()));
|
|
146
76
|
let mut opts = send_options_from_args(args);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
77
|
+
// `args.team` is a selector and may be a legacy session/team-dir alias.
|
|
78
|
+
// All downstream membership and DB scope must use the canonical key that
|
|
79
|
+
// resolve_active_team returned, never the original selector spelling.
|
|
80
|
+
opts.team = Some(TeamKey::new(selected.team_key.clone()));
|
|
151
81
|
// CR-061/N27 routing-ambiguous: a single positional with no `--to`/`--targets` and an
|
|
152
82
|
// empty message body is a prompt-only invocation (`team-agent send "fix the build"`).
|
|
153
83
|
// The lone positional is CONTENT, not a target — reject with `routing_ambiguous`
|
|
@@ -160,11 +90,12 @@ pub fn cmd_send(args: &SendArgs) -> Result<CmdResult, CliError> {
|
|
|
160
90
|
if let Some(value) = dirty_topology_refusal_value(&selected, args.team.as_deref()) {
|
|
161
91
|
return Ok(cmd_send_result(value, args.json));
|
|
162
92
|
}
|
|
163
|
-
let coordinator_ensure =
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
93
|
+
let coordinator_ensure =
|
|
94
|
+
if target_has_known_worker(&selected.state, &target, opts.sender.as_str()) {
|
|
95
|
+
loud_ensure_coordinator(&selected)?
|
|
96
|
+
} else {
|
|
97
|
+
None
|
|
98
|
+
};
|
|
168
99
|
if let Some(value) =
|
|
169
100
|
coordinator_ensure_unavailable_value(coordinator_ensure.as_ref(), &target, &content, &opts)
|
|
170
101
|
{
|
|
@@ -390,253 +321,393 @@ fn coordinator_health_status_wire(
|
|
|
390
321
|
}
|
|
391
322
|
}
|
|
392
323
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
/// `lifecycle_worker_tmux_backend_for_selected_state` below builds the
|
|
402
|
-
/// transport from the SELECTED team's persisted endpoint. For cross-
|
|
403
|
-
/// workspace delivery, use `--to-name` / `--to-leader` instead; there
|
|
404
|
-
/// is intentionally no `--socket` flag.
|
|
405
|
-
fn send_to_pane_direct(
|
|
406
|
-
workspace: &Path,
|
|
407
|
-
pane_id: &str,
|
|
408
|
-
content: &str,
|
|
409
|
-
sender: &str,
|
|
410
|
-
task_id: Option<&str>,
|
|
411
|
-
team: Option<&str>,
|
|
412
|
-
json: bool,
|
|
413
|
-
) -> Result<serde_json::Value, CliError> {
|
|
414
|
-
use crate::messaging::delivery::render_message;
|
|
415
|
-
use crate::transport::{InjectPayload, Key, PaneId, Target};
|
|
324
|
+
fn warn_send_alias(flag: &str) {
|
|
325
|
+
let spec = crate::cli::spec::command_spec("send");
|
|
326
|
+
let sunset = spec.and_then(|spec| spec.sunset).unwrap_or("next compatibility release");
|
|
327
|
+
let action = spec
|
|
328
|
+
.and_then(|spec| spec.action)
|
|
329
|
+
.unwrap_or("use positional logical TARGET addressing");
|
|
330
|
+
eprintln!("warning: {flag} is deprecated; sunset: {sunset}; action: {action}");
|
|
331
|
+
}
|
|
416
332
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
333
|
+
fn logical_to_from_args(args: &SendArgs, host_leader_to: Option<&str>) -> Result<String, CliError> {
|
|
334
|
+
if args.to_name.is_some()
|
|
335
|
+
&& (args.target.is_some() || args.targets.is_some() || args.to_leader.is_some())
|
|
336
|
+
{
|
|
337
|
+
return Err(CliError::Usage(
|
|
338
|
+
"--to-name and --pane/TARGET/--to are mutually exclusive".to_string(),
|
|
339
|
+
));
|
|
340
|
+
}
|
|
341
|
+
let supplied = [
|
|
342
|
+
args.target.is_some(),
|
|
343
|
+
args.targets.is_some(),
|
|
344
|
+
args.to_name.is_some(),
|
|
345
|
+
args.to_leader.is_some(),
|
|
346
|
+
]
|
|
347
|
+
.into_iter()
|
|
348
|
+
.filter(|present| *present)
|
|
349
|
+
.count();
|
|
350
|
+
if supplied > 1 {
|
|
351
|
+
return Err(CliError::Usage(
|
|
352
|
+
"TARGET, --targets, --to-name, and --to-leader are mutually exclusive".to_string(),
|
|
353
|
+
));
|
|
354
|
+
}
|
|
355
|
+
let logical_to = if args.to_leader.is_some() {
|
|
356
|
+
host_leader_to.unwrap_or_default().to_string()
|
|
357
|
+
} else if let Some(name) = args.to_name.as_deref() {
|
|
358
|
+
name.to_string()
|
|
359
|
+
} else if let Some(targets) = args.targets.as_deref() {
|
|
360
|
+
targets.to_string()
|
|
361
|
+
} else {
|
|
362
|
+
args.target.clone().unwrap_or_default()
|
|
363
|
+
};
|
|
364
|
+
if args.target.is_none() && supplied > 0 && args.message.is_empty() {
|
|
365
|
+
if args.to_name.is_some() {
|
|
366
|
+
return Err(CliError::Usage(
|
|
367
|
+
"--to-name requires a non-empty message".to_string(),
|
|
368
|
+
));
|
|
369
|
+
}
|
|
370
|
+
return Err(CliError::Usage(
|
|
371
|
+
"send requires a non-empty message after logical TO".to_string(),
|
|
372
|
+
));
|
|
373
|
+
}
|
|
374
|
+
Ok(logical_to)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
fn resolve_host_leader_alias(
|
|
378
|
+
name: &str,
|
|
379
|
+
) -> Result<(String, crate::leader::registry::LeaderRegistryEntry), Value> {
|
|
380
|
+
let classified = crate::leader::registry::list_validated_no_gc();
|
|
381
|
+
let candidates = classified
|
|
382
|
+
.iter()
|
|
383
|
+
.filter(|(entry, _, _)| {
|
|
384
|
+
entry.delivery_name == name
|
|
385
|
+
|| entry.qualified_name == name
|
|
386
|
+
|| entry.stable_qualified_name == name
|
|
387
|
+
|| entry.aliases.iter().any(|alias| alias == name)
|
|
388
|
+
})
|
|
389
|
+
.map(|(entry, _, _)| entry.clone())
|
|
390
|
+
.collect::<Vec<_>>();
|
|
391
|
+
if candidates.is_empty() {
|
|
392
|
+
return Err(json!({
|
|
393
|
+
"ok": false,
|
|
394
|
+
"status": "refused",
|
|
395
|
+
"reason": "leader_name_not_found",
|
|
396
|
+
"requested_name": name,
|
|
397
|
+
"resolved_via": "host_leader_registry",
|
|
398
|
+
"candidates": Vec::<Value>::new(),
|
|
399
|
+
"workspace_hash": null,
|
|
400
|
+
"stable_qualified_name": null,
|
|
401
|
+
"channel": "leader_mailbox",
|
|
402
|
+
"delivered": false,
|
|
403
|
+
"message_status": "queued_until_leader_attach",
|
|
404
|
+
"action": "run `team-agent leaders` to see registered leaders; inspect queued leader messages with `team-agent inbox`; retry with a qualified name",
|
|
405
|
+
"registry_stale": false,
|
|
406
|
+
}));
|
|
407
|
+
}
|
|
408
|
+
if candidates.len() > 1 {
|
|
409
|
+
let candidates = candidates
|
|
410
|
+
.iter()
|
|
411
|
+
.map(|entry| {
|
|
412
|
+
json!({
|
|
413
|
+
"name": entry.qualified_name,
|
|
414
|
+
"workspace": entry.workspace.display().to_string(),
|
|
415
|
+
"team_key": entry.team_key,
|
|
416
|
+
"workspace_hash": entry.workspace_hash,
|
|
417
|
+
"stable_qualified_name": entry.stable_qualified_name,
|
|
418
|
+
})
|
|
440
419
|
})
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
"
|
|
445
|
-
|
|
446
|
-
|
|
420
|
+
.collect::<Vec<_>>();
|
|
421
|
+
return Err(json!({
|
|
422
|
+
"ok": false,
|
|
423
|
+
"status": "refused",
|
|
424
|
+
"reason": "name_ambiguous",
|
|
425
|
+
"requested_name": name,
|
|
426
|
+
"resolved_via": "host_leader_registry",
|
|
427
|
+
"candidates": candidates,
|
|
428
|
+
"channel": "leader_mailbox",
|
|
429
|
+
"delivered": false,
|
|
430
|
+
"action": "run `team-agent leaders` and retry with the qualified name",
|
|
431
|
+
}));
|
|
447
432
|
}
|
|
448
|
-
let
|
|
449
|
-
let
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
433
|
+
let entry = candidates[0].clone();
|
|
434
|
+
let (status, reason) = crate::leader::registry::classify(&entry);
|
|
435
|
+
if status == "STALE" {
|
|
436
|
+
let team_alive = crate::state::persist::load_runtime_state(&entry.workspace)
|
|
437
|
+
.ok()
|
|
438
|
+
.and_then(|state| {
|
|
439
|
+
state
|
|
440
|
+
.get("teams")
|
|
441
|
+
.and_then(Value::as_object)
|
|
442
|
+
.and_then(|teams| teams.get(&entry.team_key))
|
|
443
|
+
.and_then(|team| team.get("status"))
|
|
444
|
+
.and_then(Value::as_str)
|
|
445
|
+
.map(|status| status == "alive" || status.is_empty())
|
|
446
|
+
})
|
|
447
|
+
.unwrap_or(false);
|
|
448
|
+
if !team_alive {
|
|
449
|
+
return Err(json!({
|
|
450
|
+
"ok": false,
|
|
451
|
+
"status": "refused",
|
|
452
|
+
"reason": "registry_stale",
|
|
453
|
+
"requested_name": name,
|
|
454
|
+
"resolved_via": "host_leader_registry",
|
|
455
|
+
"stale_reason": reason,
|
|
456
|
+
"workspace_hash": entry.workspace_hash,
|
|
457
|
+
"stable_qualified_name": entry.stable_qualified_name,
|
|
458
|
+
"channel": "leader_mailbox",
|
|
459
|
+
"delivered": false,
|
|
460
|
+
"action": "target team is not alive; run `team-agent leaders` for current state",
|
|
461
|
+
}));
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
let logical_to = format!("{}::{}/leader", entry.workspace.display(), entry.team_key);
|
|
465
|
+
Ok((logical_to, entry))
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
fn decorate_host_leader_alias(
|
|
469
|
+
value: &mut Value,
|
|
470
|
+
entry: &crate::leader::registry::LeaderRegistryEntry,
|
|
471
|
+
) {
|
|
472
|
+
let Some(object) = value.as_object_mut() else {
|
|
473
|
+
return;
|
|
474
|
+
};
|
|
475
|
+
object.insert("resolved_via".to_string(), json!("host_leader_registry"));
|
|
476
|
+
object.insert("to_leader".to_string(), json!(entry.qualified_name));
|
|
477
|
+
object.insert("requested_name".to_string(), json!(entry.delivery_name));
|
|
478
|
+
object.insert("workspace_hash".to_string(), json!(entry.workspace_hash));
|
|
479
|
+
object.insert(
|
|
480
|
+
"stable_qualified_name".to_string(),
|
|
481
|
+
json!(entry.stable_qualified_name),
|
|
468
482
|
);
|
|
469
|
-
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
fn send_to_logical_to(args: &SendArgs, logical_to: &str, content: &str) -> Result<Value, CliError> {
|
|
486
|
+
let names = logical_to
|
|
487
|
+
.split(',')
|
|
488
|
+
.map(str::trim)
|
|
489
|
+
.filter(|name| !name.is_empty())
|
|
490
|
+
.collect::<Vec<_>>();
|
|
491
|
+
if names.is_empty() || names.len() != logical_to.split(',').count() {
|
|
492
|
+
return Err(CliError::Usage(
|
|
493
|
+
"logical TO comma-list contains an empty recipient".to_string(),
|
|
494
|
+
));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
let mut resolved = Vec::with_capacity(names.len());
|
|
498
|
+
for name in names {
|
|
499
|
+
match crate::cli::named_address::resolve_name_for_cli(
|
|
500
|
+
&args.workspace,
|
|
501
|
+
name,
|
|
502
|
+
args.team.as_deref(),
|
|
503
|
+
) {
|
|
504
|
+
Ok((recipient, _transport)) => resolved.push(recipient),
|
|
505
|
+
Err(mut error) => {
|
|
506
|
+
adapt_positional_bare_error(args, name, &mut error);
|
|
507
|
+
if matches!(
|
|
508
|
+
error.kind,
|
|
509
|
+
crate::cli::named_address::NamedAddressErrorKind::StateNotFound
|
|
510
|
+
) {
|
|
511
|
+
return Ok(resolution_refusal_json(
|
|
512
|
+
&error,
|
|
513
|
+
logical_to,
|
|
514
|
+
content,
|
|
515
|
+
args,
|
|
516
|
+
));
|
|
517
|
+
}
|
|
518
|
+
if resolved.is_empty() && !logical_to.contains(',') {
|
|
519
|
+
if let Some(value) = maybe_enqueue_offline_leader_mailbox(
|
|
520
|
+
&args.workspace,
|
|
521
|
+
name,
|
|
522
|
+
content,
|
|
523
|
+
args.sender.as_str(),
|
|
524
|
+
args.task.as_deref(),
|
|
525
|
+
&error,
|
|
526
|
+
)? {
|
|
527
|
+
return Ok(value);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if args.json {
|
|
531
|
+
return Ok(error.to_json());
|
|
532
|
+
}
|
|
533
|
+
return Err(CliError::Usage(error.n38_message()));
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if resolved.len() == 1 {
|
|
539
|
+
return send_to_resolved_name(args, &resolved[0], content);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
let first = &resolved[0];
|
|
543
|
+
let one_scope = resolved.iter().all(|recipient| {
|
|
544
|
+
recipient.target_workspace == first.target_workspace && recipient.team_key == first.team_key
|
|
545
|
+
});
|
|
546
|
+
if one_scope {
|
|
547
|
+
let recipients = resolved
|
|
548
|
+
.iter()
|
|
549
|
+
.map(logical_recipient_id)
|
|
550
|
+
.collect::<Result<Vec<_>, _>>()?;
|
|
551
|
+
let target = MessageTarget::Fanout(recipients);
|
|
552
|
+
return persist_resolved_target(args, first, &target, content);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
let mut results = Vec::with_capacity(resolved.len());
|
|
556
|
+
for recipient in &resolved {
|
|
557
|
+
results.push(send_to_resolved_name(args, recipient, content)?);
|
|
558
|
+
}
|
|
559
|
+
let ok = results
|
|
560
|
+
.iter()
|
|
561
|
+
.all(|value| value.get("ok").and_then(Value::as_bool) == Some(true));
|
|
562
|
+
let message_id = results
|
|
563
|
+
.iter()
|
|
564
|
+
.rev()
|
|
565
|
+
.find_map(|value| value.get("message_id").and_then(Value::as_str))
|
|
566
|
+
.map(str::to_string);
|
|
567
|
+
Ok(json!({
|
|
470
568
|
"ok": ok,
|
|
471
|
-
"
|
|
569
|
+
"status": if ok { "fanout_delivered" } else { "fanout_partial" },
|
|
570
|
+
"delivery_status": if ok { "pending" } else { "fanout_partial" },
|
|
571
|
+
"delivered": false,
|
|
572
|
+
"target": logical_to.split(',').map(str::trim).collect::<Vec<_>>(),
|
|
573
|
+
"content_length_bytes": content.len(),
|
|
574
|
+
"sender": args.sender,
|
|
472
575
|
"message_id": message_id,
|
|
473
|
-
"
|
|
474
|
-
"inject_verification": format!("{:?}", report.inject_verification),
|
|
475
|
-
"in_team": in_team,
|
|
576
|
+
"results": results,
|
|
476
577
|
}))
|
|
477
578
|
}
|
|
478
579
|
|
|
479
|
-
fn
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
resolved: &crate::cli::named_address::ResolvedNamedAddress,
|
|
580
|
+
fn resolution_refusal_json(
|
|
581
|
+
error: &crate::cli::named_address::NamedAddressError,
|
|
582
|
+
logical_to: &str,
|
|
483
583
|
content: &str,
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
)
|
|
488
|
-
|
|
489
|
-
|
|
584
|
+
args: &SendArgs,
|
|
585
|
+
) -> Value {
|
|
586
|
+
let mut value = error.to_json();
|
|
587
|
+
if let Some(object) = value.as_object_mut() {
|
|
588
|
+
object.insert("delivery_status".to_string(), json!("refused"));
|
|
589
|
+
object.insert("delivered".to_string(), json!(false));
|
|
590
|
+
object.insert("target".to_string(), json!(logical_to));
|
|
591
|
+
object.insert("agent_id".to_string(), json!(logical_to));
|
|
592
|
+
object.insert("content_length_bytes".to_string(), json!(content.len()));
|
|
593
|
+
object.insert("sender".to_string(), json!(args.sender));
|
|
594
|
+
object.insert("message_id".to_string(), Value::Null);
|
|
595
|
+
object.insert("message_status".to_string(), json!("refused"));
|
|
596
|
+
object.insert("verification".to_string(), Value::Null);
|
|
597
|
+
object.insert("stage".to_string(), Value::Null);
|
|
598
|
+
object.insert("channel".to_string(), Value::Null);
|
|
599
|
+
}
|
|
600
|
+
value
|
|
601
|
+
}
|
|
490
602
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
if
|
|
497
|
-
|
|
603
|
+
fn adapt_positional_bare_error(
|
|
604
|
+
args: &SendArgs,
|
|
605
|
+
name: &str,
|
|
606
|
+
error: &mut crate::cli::named_address::NamedAddressError,
|
|
607
|
+
) {
|
|
608
|
+
if args.target.as_deref() != Some(name)
|
|
609
|
+
|| args.team.is_none()
|
|
610
|
+
|| name.contains('/')
|
|
611
|
+
|| name.contains(':')
|
|
612
|
+
|| name.contains(',')
|
|
613
|
+
{
|
|
614
|
+
return;
|
|
498
615
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
616
|
+
error.requested_name = Some(name.to_string());
|
|
617
|
+
for candidate in &mut error.candidates {
|
|
618
|
+
let agent_id = candidate
|
|
619
|
+
.get("agent_id")
|
|
620
|
+
.and_then(Value::as_str)
|
|
621
|
+
.map(str::to_string);
|
|
622
|
+
if let (Some(object), Some(agent_id)) = (candidate.as_object_mut(), agent_id) {
|
|
623
|
+
object.insert("name".to_string(), json!(agent_id));
|
|
624
|
+
}
|
|
507
625
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
.
|
|
511
|
-
.
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
))
|
|
516
|
-
})?;
|
|
517
|
-
let target_kind = named_target_kind_wire(resolved.target_kind);
|
|
518
|
-
let event = serde_json::json!({
|
|
519
|
-
"to_name": resolved.raw_name,
|
|
520
|
-
"target_kind": target_kind,
|
|
521
|
-
"sender": sender,
|
|
522
|
-
"sender_workspace": sender_run_workspace.display().to_string(),
|
|
523
|
-
"target_workspace": resolved.target_workspace.display().to_string(),
|
|
524
|
-
"team_key": resolved.team_key,
|
|
525
|
-
"agent_id": resolved.agent_id,
|
|
526
|
-
"pane_id": resolved.pane_id,
|
|
527
|
-
"session_name": resolved.session_name,
|
|
528
|
-
"window_name": resolved.window_name,
|
|
529
|
-
"tmux_endpoint": resolved.tmux_endpoint,
|
|
530
|
-
"state_pane_id": resolved.state_pane_id,
|
|
531
|
-
"state_pane_stale": resolved.state_pane_stale,
|
|
532
|
-
"agent_status": resolved.agent_status,
|
|
533
|
-
"warning": resolved.warning,
|
|
534
|
-
"message_id": message_id,
|
|
535
|
-
"submit_verification": crate::transport::submit_verification_wire(report.submit_verification),
|
|
536
|
-
"inject_verification": format!("{:?}", report.inject_verification),
|
|
537
|
-
});
|
|
538
|
-
let _ = event_log.write("send.name_direct", event.clone());
|
|
539
|
-
let ok = matches!(
|
|
540
|
-
report.submit_verification,
|
|
541
|
-
crate::transport::SubmitVerification::EnterSentWithoutPlaceholderCheck
|
|
542
|
-
| crate::transport::SubmitVerification::PastedContentPromptAbsentAfterSubmit
|
|
543
|
-
| crate::transport::SubmitVerification::KeySentAfterVisibleToken { .. }
|
|
544
|
-
);
|
|
545
|
-
let mut value = event;
|
|
546
|
-
if let Some(obj) = value.as_object_mut() {
|
|
547
|
-
obj.insert("ok".to_string(), serde_json::json!(ok));
|
|
626
|
+
error.suggested_name = error
|
|
627
|
+
.suggested_name
|
|
628
|
+
.as_deref()
|
|
629
|
+
.and_then(|suggested| suggested.rsplit('/').next())
|
|
630
|
+
.map(str::to_string);
|
|
631
|
+
if let Some(suggested) = error.suggested_name.as_deref() {
|
|
632
|
+
error.action = format!("Did you mean `{suggested}`? Retry with `{suggested}` as TO.");
|
|
548
633
|
}
|
|
549
|
-
Ok(value)
|
|
550
634
|
}
|
|
551
635
|
|
|
552
|
-
fn
|
|
553
|
-
event_log: &crate::event_log::EventLog,
|
|
636
|
+
fn logical_recipient_id(
|
|
554
637
|
resolved: &crate::cli::named_address::ResolvedNamedAddress,
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
let target_kind = named_target_kind_wire(resolved.target_kind);
|
|
567
|
-
let base_event = serde_json::json!({
|
|
568
|
-
"to_name": resolved.raw_name,
|
|
569
|
-
"target_kind": target_kind,
|
|
570
|
-
"sender": sender,
|
|
571
|
-
"sender_workspace": resolved.sender_workspace.display().to_string(),
|
|
572
|
-
"target_workspace": resolved.target_workspace.display().to_string(),
|
|
573
|
-
"team_key": resolved.team_key,
|
|
574
|
-
"agent_id": resolved.agent_id,
|
|
575
|
-
"transport_kind": "codex_app_server",
|
|
576
|
-
"socket": binding.socket,
|
|
577
|
-
"thread_id": binding.thread_id,
|
|
578
|
-
"message_id": message_id,
|
|
579
|
-
});
|
|
580
|
-
match crate::codex_app_server::submit_to_bound_thread(&binding, message_id, rendered) {
|
|
581
|
-
Ok(submit) => {
|
|
582
|
-
let event = merge_json(
|
|
583
|
-
base_event.clone(),
|
|
584
|
-
serde_json::json!({
|
|
585
|
-
"ok": true,
|
|
586
|
-
"turn_id": submit.turn_id,
|
|
587
|
-
"turn_status": submit.turn_status,
|
|
588
|
-
}),
|
|
589
|
-
);
|
|
590
|
-
let _ = event_log.write("send.name_app_server", event.clone());
|
|
591
|
-
Ok(event)
|
|
592
|
-
}
|
|
593
|
-
Err(crate::codex_app_server::AppServerError::LeaderBusy(message)) => {
|
|
594
|
-
let event = merge_json(
|
|
595
|
-
base_event.clone(),
|
|
596
|
-
serde_json::json!({
|
|
597
|
-
"ok": false,
|
|
598
|
-
"status": "retry_scheduled",
|
|
599
|
-
"reason": "leader_busy",
|
|
600
|
-
"channel": "leader_busy",
|
|
601
|
-
"error": message,
|
|
602
|
-
}),
|
|
603
|
-
);
|
|
604
|
-
let _ = event_log.write("send.name_app_server", event.clone());
|
|
605
|
-
Ok(event)
|
|
606
|
-
}
|
|
607
|
-
Err(error) => {
|
|
608
|
-
let event = merge_json(
|
|
609
|
-
base_event.clone(),
|
|
610
|
-
serde_json::json!({
|
|
611
|
-
"ok": false,
|
|
612
|
-
"status": "refused",
|
|
613
|
-
"reason": error.code(),
|
|
614
|
-
"channel": "rebind_required",
|
|
615
|
-
"error": error.to_string(),
|
|
616
|
-
"action": "run team-agent attach-app-server-leader for the target team",
|
|
617
|
-
}),
|
|
618
|
-
);
|
|
619
|
-
let _ = event_log.write("send.name_app_server", event.clone());
|
|
620
|
-
Ok(event)
|
|
621
|
-
}
|
|
638
|
+
) -> Result<String, CliError> {
|
|
639
|
+
match resolved.target_kind {
|
|
640
|
+
crate::cli::named_address::NamedTargetKind::Worker => resolved
|
|
641
|
+
.agent_id
|
|
642
|
+
.clone()
|
|
643
|
+
.ok_or_else(|| CliError::Runtime("resolved worker is missing agent id".to_string())),
|
|
644
|
+
crate::cli::named_address::NamedTargetKind::Leader => Ok("leader".to_string()),
|
|
645
|
+
crate::cli::named_address::NamedTargetKind::SessionWindow => Err(CliError::Usage(
|
|
646
|
+
"named session/window delivery is sunset; use a logical agent or leader name"
|
|
647
|
+
.to_string(),
|
|
648
|
+
)),
|
|
622
649
|
}
|
|
623
650
|
}
|
|
624
651
|
|
|
625
|
-
fn
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
652
|
+
fn send_to_resolved_name(
|
|
653
|
+
args: &SendArgs,
|
|
654
|
+
resolved: &crate::cli::named_address::ResolvedNamedAddress,
|
|
655
|
+
content: &str,
|
|
656
|
+
) -> Result<Value, CliError> {
|
|
657
|
+
let recipient = logical_recipient_id(resolved)?;
|
|
658
|
+
if let Some(warning) = &resolved.warning {
|
|
659
|
+
eprintln!("warning: {warning}");
|
|
660
|
+
}
|
|
661
|
+
let target = MessageTarget::Single(recipient);
|
|
662
|
+
let mut value = persist_resolved_target(args, resolved, &target, content)?;
|
|
663
|
+
if args.to_name.is_some() || args.to_leader.is_some() {
|
|
664
|
+
if let Some(obj) = value.as_object_mut() {
|
|
665
|
+
obj.insert("to_name".to_string(), json!(resolved.raw_name));
|
|
666
|
+
obj.insert(
|
|
667
|
+
"target_workspace".to_string(),
|
|
668
|
+
json!(resolved.target_workspace.display().to_string()),
|
|
669
|
+
);
|
|
670
|
+
obj.insert("team_key".to_string(), json!(resolved.team_key));
|
|
629
671
|
}
|
|
630
672
|
}
|
|
631
|
-
|
|
673
|
+
Ok(value)
|
|
632
674
|
}
|
|
633
675
|
|
|
634
|
-
fn
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
676
|
+
fn persist_resolved_target(
|
|
677
|
+
args: &SendArgs,
|
|
678
|
+
resolved: &crate::cli::named_address::ResolvedNamedAddress,
|
|
679
|
+
target: &MessageTarget,
|
|
680
|
+
content: &str,
|
|
681
|
+
) -> Result<Value, CliError> {
|
|
682
|
+
let selected = crate::state::selector::resolve_active_team(
|
|
683
|
+
&resolved.target_workspace,
|
|
684
|
+
resolved.team_key.as_deref(),
|
|
685
|
+
crate::state::selector::SelectorMode::RuntimeOnly,
|
|
686
|
+
)?;
|
|
687
|
+
if let Some(value) = dirty_topology_refusal_value(&selected, resolved.team_key.as_deref()) {
|
|
688
|
+
return Ok(value);
|
|
639
689
|
}
|
|
690
|
+
let mut opts = send_options_from_args(args);
|
|
691
|
+
opts.route_task_id = args.to_name.is_none() && args.to_leader.is_none();
|
|
692
|
+
opts.team = Some(TeamKey::new(selected.team_key.clone()));
|
|
693
|
+
let coordinator_ensure = if target_has_known_worker(&selected.state, target, opts.sender.as_str())
|
|
694
|
+
{
|
|
695
|
+
loud_ensure_coordinator(&selected)?
|
|
696
|
+
} else {
|
|
697
|
+
None
|
|
698
|
+
};
|
|
699
|
+
if let Some(value) = coordinator_ensure_unavailable_value(
|
|
700
|
+
coordinator_ensure.as_ref(),
|
|
701
|
+
target,
|
|
702
|
+
content,
|
|
703
|
+
&opts,
|
|
704
|
+
) {
|
|
705
|
+
return Ok(value);
|
|
706
|
+
}
|
|
707
|
+
let outcome = messaging::send_message(&selected.run_workspace, target, content, &opts)?;
|
|
708
|
+
let mut value = delivery_outcome_json(&outcome, target, content, &opts);
|
|
709
|
+
append_loud_ensure_fields(&mut value, coordinator_ensure.as_ref());
|
|
710
|
+
Ok(value)
|
|
640
711
|
}
|
|
641
712
|
|
|
642
713
|
fn routing_ambiguous_value(
|
|
@@ -1219,183 +1290,33 @@ pub fn send_to_canonical_leader_target(
|
|
|
1219
1290
|
sender_workspace: &std::path::Path,
|
|
1220
1291
|
name: &str,
|
|
1221
1292
|
content: &str,
|
|
1222
|
-
sender: &
|
|
1293
|
+
sender: &TrustedSender,
|
|
1223
1294
|
task_id: Option<&str>,
|
|
1224
1295
|
) -> Result<serde_json::Value, CliError> {
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
"stable_qualified_name": null,
|
|
1251
|
-
"channel": "leader_mailbox",
|
|
1252
|
-
"delivered": false,
|
|
1253
|
-
"message_status": "queued_until_leader_attach",
|
|
1254
|
-
"action": "run `team-agent leaders` to see registered leaders; inspect queued leader messages with `team-agent inbox`; retry with a qualified name",
|
|
1255
|
-
"registry_stale": false,
|
|
1256
|
-
}));
|
|
1257
|
-
}
|
|
1258
|
-
if candidates_all.len() > 1 {
|
|
1259
|
-
let cand_json: Vec<serde_json::Value> = candidates_all
|
|
1260
|
-
.iter()
|
|
1261
|
-
.map(|e| {
|
|
1262
|
-
serde_json::json!({
|
|
1263
|
-
"name": e.qualified_name,
|
|
1264
|
-
"workspace": e.workspace.display().to_string(),
|
|
1265
|
-
"team_key": e.team_key,
|
|
1266
|
-
"workspace_hash": e.workspace_hash,
|
|
1267
|
-
"stable_qualified_name": e.stable_qualified_name,
|
|
1268
|
-
})
|
|
1269
|
-
})
|
|
1270
|
-
.collect();
|
|
1271
|
-
return Ok(serde_json::json!({
|
|
1272
|
-
"ok": false,
|
|
1273
|
-
"status": "refused",
|
|
1274
|
-
"reason": "name_ambiguous",
|
|
1275
|
-
"requested_name": name,
|
|
1276
|
-
"resolved_via": "host_leader_registry",
|
|
1277
|
-
"candidates": cand_json,
|
|
1278
|
-
"channel": "leader_mailbox",
|
|
1279
|
-
"delivered": false,
|
|
1280
|
-
"action": "run `team-agent leaders` and retry with the qualified name",
|
|
1281
|
-
}));
|
|
1282
|
-
}
|
|
1283
|
-
let entry = candidates_all.into_iter().next().ok_or_else(|| {
|
|
1284
|
-
CliError::Runtime("internal: candidate list must have at least one entry".to_string())
|
|
1285
|
-
})?;
|
|
1286
|
-
// Canonical-validate the entry against target workspace state. Send
|
|
1287
|
-
// through the same E6 --to-name path so live inject and mailbox both
|
|
1288
|
-
// funnel through one code path.
|
|
1289
|
-
let (status, reason) = crate::leader::registry::classify(&entry);
|
|
1290
|
-
if status == "STALE" {
|
|
1291
|
-
// Check whether the underlying team is still alive — if so we
|
|
1292
|
-
// may still queue via the E6 mailbox path (leader-not-attached
|
|
1293
|
-
// shape). If the workspace/team is gone we refuse `registry_stale`.
|
|
1294
|
-
let state = crate::state::persist::load_runtime_state(&entry.workspace).ok();
|
|
1295
|
-
let team_alive = state
|
|
1296
|
-
.as_ref()
|
|
1297
|
-
.and_then(|s| s.get("teams"))
|
|
1298
|
-
.and_then(|v| v.as_object())
|
|
1299
|
-
.and_then(|teams| teams.get(&entry.team_key))
|
|
1300
|
-
.and_then(|t| t.get("status"))
|
|
1301
|
-
.and_then(serde_json::Value::as_str)
|
|
1302
|
-
.map(|s| s == "alive" || s.is_empty())
|
|
1303
|
-
.unwrap_or(false);
|
|
1304
|
-
if !team_alive {
|
|
1305
|
-
return Ok(serde_json::json!({
|
|
1306
|
-
"ok": false,
|
|
1307
|
-
"status": "refused",
|
|
1308
|
-
"reason": "registry_stale",
|
|
1309
|
-
"requested_name": name,
|
|
1310
|
-
"resolved_via": "host_leader_registry",
|
|
1311
|
-
"stale_reason": reason,
|
|
1312
|
-
"workspace_hash": entry.workspace_hash,
|
|
1313
|
-
"stable_qualified_name": entry.stable_qualified_name,
|
|
1314
|
-
"channel": "leader_mailbox",
|
|
1315
|
-
"delivered": false,
|
|
1316
|
-
"action": "target team is not alive; run `team-agent leaders` for current state",
|
|
1317
|
-
}));
|
|
1318
|
-
}
|
|
1319
|
-
// Team alive but leader unattached → E6 mailbox.
|
|
1320
|
-
let event_log = crate::event_log::EventLog::new(&entry.workspace);
|
|
1321
|
-
let task = task_id.map(|s| crate::model::ids::TaskId::new(s.to_string()));
|
|
1322
|
-
let outcome = crate::messaging::enqueue_leader_mailbox_until_attach(
|
|
1323
|
-
&entry.workspace,
|
|
1324
|
-
&entry.team_key,
|
|
1325
|
-
content,
|
|
1326
|
-
task.as_ref(),
|
|
1327
|
-
sender,
|
|
1328
|
-
&event_log,
|
|
1329
|
-
)
|
|
1330
|
-
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
1331
|
-
return Ok(serde_json::json!({
|
|
1332
|
-
"ok": true,
|
|
1333
|
-
"status": "queued_until_leader_attach",
|
|
1334
|
-
"message_status": "queued_until_leader_attach",
|
|
1335
|
-
"channel": "leader_mailbox",
|
|
1336
|
-
"delivered": false,
|
|
1337
|
-
"resolved_via": "host_leader_registry",
|
|
1338
|
-
"requested_name": name,
|
|
1339
|
-
"to_leader": entry.qualified_name,
|
|
1340
|
-
"target_workspace": entry.workspace.display().to_string(),
|
|
1341
|
-
"workspace_hash": entry.workspace_hash,
|
|
1342
|
-
"stable_qualified_name": entry.stable_qualified_name,
|
|
1343
|
-
"team_key": entry.team_key,
|
|
1344
|
-
"message_id": outcome.message_id,
|
|
1345
|
-
}));
|
|
1346
|
-
}
|
|
1347
|
-
// LIVE: canonical-validated. Delegate to the E6 --to-name path via a
|
|
1348
|
-
// synthesized `<workspace>::<team_key>/leader` name so live inject +
|
|
1349
|
-
// mailbox both go through one code path.
|
|
1350
|
-
let to_name = format!("{}::{}/leader", entry.workspace.display(), entry.team_key);
|
|
1351
|
-
// 0.5.45 naming-addressing (design §3.1 / §4.1): internal
|
|
1352
|
-
// registry-to-E6 delegation MUST pass None for bare_team_scope.
|
|
1353
|
-
// The synthesized name above is a full `workspace::team/leader`
|
|
1354
|
-
// form, and the caller's `--team` flag (if any) must not
|
|
1355
|
-
// override the registry's authoritative team_key.
|
|
1356
|
-
let (resolved, transport) =
|
|
1357
|
-
match crate::cli::named_address::resolve_name_for_cli(sender_workspace, &to_name, None) {
|
|
1358
|
-
Ok(r) => r,
|
|
1359
|
-
Err(err) => {
|
|
1360
|
-
// Named-address refusal — surface it verbatim but tag as
|
|
1361
|
-
// registry-resolved so callers can trace the origin.
|
|
1362
|
-
let mut body = err.to_json();
|
|
1363
|
-
if let Some(obj) = body.as_object_mut() {
|
|
1364
|
-
obj.insert(
|
|
1365
|
-
"resolved_via".to_string(),
|
|
1366
|
-
serde_json::Value::String("host_leader_registry".to_string()),
|
|
1367
|
-
);
|
|
1368
|
-
obj.insert("delivered".to_string(), serde_json::Value::Bool(false));
|
|
1369
|
-
}
|
|
1370
|
-
return Ok(body);
|
|
1371
|
-
}
|
|
1372
|
-
};
|
|
1373
|
-
let mut value = send_to_named_pane_direct(
|
|
1374
|
-
sender_workspace,
|
|
1375
|
-
transport.as_ref(),
|
|
1376
|
-
&resolved,
|
|
1377
|
-
content,
|
|
1378
|
-
sender,
|
|
1379
|
-
task_id,
|
|
1380
|
-
true,
|
|
1381
|
-
)?;
|
|
1382
|
-
if let Some(obj) = value.as_object_mut() {
|
|
1383
|
-
obj.insert(
|
|
1384
|
-
"resolved_via".to_string(),
|
|
1385
|
-
serde_json::Value::String("host_leader_registry".to_string()),
|
|
1386
|
-
);
|
|
1387
|
-
obj.insert(
|
|
1388
|
-
"to_leader".to_string(),
|
|
1389
|
-
serde_json::Value::String(entry.qualified_name.clone()),
|
|
1390
|
-
);
|
|
1391
|
-
// Honest delivered marker. `send_to_named_pane_direct` sets `ok`
|
|
1392
|
-
// to whether physical inject verified — mirror that as
|
|
1393
|
-
// `delivered`.
|
|
1394
|
-
let ok = obj
|
|
1395
|
-
.get("ok")
|
|
1396
|
-
.and_then(serde_json::Value::as_bool)
|
|
1397
|
-
.unwrap_or(false);
|
|
1398
|
-
obj.insert("delivered".to_string(), serde_json::Value::Bool(ok));
|
|
1399
|
-
}
|
|
1296
|
+
let (logical_to, entry) = match resolve_host_leader_alias(name) {
|
|
1297
|
+
Ok(resolved) => resolved,
|
|
1298
|
+
Err(value) => return Ok(value),
|
|
1299
|
+
};
|
|
1300
|
+
let args = SendArgs {
|
|
1301
|
+
target: Some(logical_to.clone()),
|
|
1302
|
+
message: vec![content.to_string()],
|
|
1303
|
+
targets: None,
|
|
1304
|
+
workspace: sender_workspace.to_path_buf(),
|
|
1305
|
+
team: None,
|
|
1306
|
+
task: task_id.map(str::to_string),
|
|
1307
|
+
sender: sender.clone(),
|
|
1308
|
+
no_ack: false,
|
|
1309
|
+
no_wait: true,
|
|
1310
|
+
watch_result: false,
|
|
1311
|
+
timeout: 0.0,
|
|
1312
|
+
confirm_human: false,
|
|
1313
|
+
json: true,
|
|
1314
|
+
message_id: None,
|
|
1315
|
+
pane: None,
|
|
1316
|
+
to_name: None,
|
|
1317
|
+
to_leader: None,
|
|
1318
|
+
};
|
|
1319
|
+
let mut value = send_to_logical_to(&args, &logical_to, content)?;
|
|
1320
|
+
decorate_host_leader_alias(&mut value, &entry);
|
|
1400
1321
|
Ok(value)
|
|
1401
1322
|
}
|