@team-agent/installer 0.5.49 → 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 +71 -5
- package/crates/team-agent/src/cli/send.rs +438 -518
- 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 +9 -7
- package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -3
- package/crates/team-agent/src/cli/tests/status_send.rs +17 -33
- package/crates/team-agent/src/cli/types.rs +4 -8
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +1 -1
- 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,13 +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
77
|
// `args.team` is a selector and may be a legacy session/team-dir alias.
|
|
148
78
|
// All downstream membership and DB scope must use the canonical key that
|
|
149
79
|
// resolve_active_team returned, never the original selector spelling.
|
|
150
80
|
opts.team = Some(TeamKey::new(selected.team_key.clone()));
|
|
151
|
-
let content = args.message.join(" ");
|
|
152
81
|
// CR-061/N27 routing-ambiguous: a single positional with no `--to`/`--targets` and an
|
|
153
82
|
// empty message body is a prompt-only invocation (`team-agent send "fix the build"`).
|
|
154
83
|
// The lone positional is CONTENT, not a target — reject with `routing_ambiguous`
|
|
@@ -161,11 +90,12 @@ pub fn cmd_send(args: &SendArgs) -> Result<CmdResult, CliError> {
|
|
|
161
90
|
if let Some(value) = dirty_topology_refusal_value(&selected, args.team.as_deref()) {
|
|
162
91
|
return Ok(cmd_send_result(value, args.json));
|
|
163
92
|
}
|
|
164
|
-
let coordinator_ensure =
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
+
};
|
|
169
99
|
if let Some(value) =
|
|
170
100
|
coordinator_ensure_unavailable_value(coordinator_ensure.as_ref(), &target, &content, &opts)
|
|
171
101
|
{
|
|
@@ -391,253 +321,393 @@ fn coordinator_health_status_wire(
|
|
|
391
321
|
}
|
|
392
322
|
}
|
|
393
323
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
/// `lifecycle_worker_tmux_backend_for_selected_state` below builds the
|
|
403
|
-
/// transport from the SELECTED team's persisted endpoint. For cross-
|
|
404
|
-
/// workspace delivery, use `--to-name` / `--to-leader` instead; there
|
|
405
|
-
/// is intentionally no `--socket` flag.
|
|
406
|
-
fn send_to_pane_direct(
|
|
407
|
-
workspace: &Path,
|
|
408
|
-
pane_id: &str,
|
|
409
|
-
content: &str,
|
|
410
|
-
sender: &str,
|
|
411
|
-
task_id: Option<&str>,
|
|
412
|
-
team: Option<&str>,
|
|
413
|
-
json: bool,
|
|
414
|
-
) -> Result<serde_json::Value, CliError> {
|
|
415
|
-
use crate::messaging::delivery::render_message;
|
|
416
|
-
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
|
+
}
|
|
417
332
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
+
})
|
|
441
419
|
})
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
"
|
|
446
|
-
|
|
447
|
-
|
|
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
|
+
}));
|
|
448
432
|
}
|
|
449
|
-
let
|
|
450
|
-
let
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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),
|
|
469
482
|
);
|
|
470
|
-
|
|
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!({
|
|
471
568
|
"ok": ok,
|
|
472
|
-
"
|
|
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,
|
|
473
575
|
"message_id": message_id,
|
|
474
|
-
"
|
|
475
|
-
"inject_verification": format!("{:?}", report.inject_verification),
|
|
476
|
-
"in_team": in_team,
|
|
576
|
+
"results": results,
|
|
477
577
|
}))
|
|
478
578
|
}
|
|
479
579
|
|
|
480
|
-
fn
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
resolved: &crate::cli::named_address::ResolvedNamedAddress,
|
|
580
|
+
fn resolution_refusal_json(
|
|
581
|
+
error: &crate::cli::named_address::NamedAddressError,
|
|
582
|
+
logical_to: &str,
|
|
484
583
|
content: &str,
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
)
|
|
489
|
-
|
|
490
|
-
|
|
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
|
+
}
|
|
491
602
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
if
|
|
498
|
-
|
|
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;
|
|
499
615
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
+
}
|
|
508
625
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
.
|
|
512
|
-
.
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
))
|
|
517
|
-
})?;
|
|
518
|
-
let target_kind = named_target_kind_wire(resolved.target_kind);
|
|
519
|
-
let event = serde_json::json!({
|
|
520
|
-
"to_name": resolved.raw_name,
|
|
521
|
-
"target_kind": target_kind,
|
|
522
|
-
"sender": sender,
|
|
523
|
-
"sender_workspace": sender_run_workspace.display().to_string(),
|
|
524
|
-
"target_workspace": resolved.target_workspace.display().to_string(),
|
|
525
|
-
"team_key": resolved.team_key,
|
|
526
|
-
"agent_id": resolved.agent_id,
|
|
527
|
-
"pane_id": resolved.pane_id,
|
|
528
|
-
"session_name": resolved.session_name,
|
|
529
|
-
"window_name": resolved.window_name,
|
|
530
|
-
"tmux_endpoint": resolved.tmux_endpoint,
|
|
531
|
-
"state_pane_id": resolved.state_pane_id,
|
|
532
|
-
"state_pane_stale": resolved.state_pane_stale,
|
|
533
|
-
"agent_status": resolved.agent_status,
|
|
534
|
-
"warning": resolved.warning,
|
|
535
|
-
"message_id": message_id,
|
|
536
|
-
"submit_verification": crate::transport::submit_verification_wire(report.submit_verification),
|
|
537
|
-
"inject_verification": format!("{:?}", report.inject_verification),
|
|
538
|
-
});
|
|
539
|
-
let _ = event_log.write("send.name_direct", event.clone());
|
|
540
|
-
let ok = matches!(
|
|
541
|
-
report.submit_verification,
|
|
542
|
-
crate::transport::SubmitVerification::EnterSentWithoutPlaceholderCheck
|
|
543
|
-
| crate::transport::SubmitVerification::PastedContentPromptAbsentAfterSubmit
|
|
544
|
-
| crate::transport::SubmitVerification::KeySentAfterVisibleToken { .. }
|
|
545
|
-
);
|
|
546
|
-
let mut value = event;
|
|
547
|
-
if let Some(obj) = value.as_object_mut() {
|
|
548
|
-
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.");
|
|
549
633
|
}
|
|
550
|
-
Ok(value)
|
|
551
634
|
}
|
|
552
635
|
|
|
553
|
-
fn
|
|
554
|
-
event_log: &crate::event_log::EventLog,
|
|
636
|
+
fn logical_recipient_id(
|
|
555
637
|
resolved: &crate::cli::named_address::ResolvedNamedAddress,
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
let target_kind = named_target_kind_wire(resolved.target_kind);
|
|
568
|
-
let base_event = serde_json::json!({
|
|
569
|
-
"to_name": resolved.raw_name,
|
|
570
|
-
"target_kind": target_kind,
|
|
571
|
-
"sender": sender,
|
|
572
|
-
"sender_workspace": resolved.sender_workspace.display().to_string(),
|
|
573
|
-
"target_workspace": resolved.target_workspace.display().to_string(),
|
|
574
|
-
"team_key": resolved.team_key,
|
|
575
|
-
"agent_id": resolved.agent_id,
|
|
576
|
-
"transport_kind": "codex_app_server",
|
|
577
|
-
"socket": binding.socket,
|
|
578
|
-
"thread_id": binding.thread_id,
|
|
579
|
-
"message_id": message_id,
|
|
580
|
-
});
|
|
581
|
-
match crate::codex_app_server::submit_to_bound_thread(&binding, message_id, rendered) {
|
|
582
|
-
Ok(submit) => {
|
|
583
|
-
let event = merge_json(
|
|
584
|
-
base_event.clone(),
|
|
585
|
-
serde_json::json!({
|
|
586
|
-
"ok": true,
|
|
587
|
-
"turn_id": submit.turn_id,
|
|
588
|
-
"turn_status": submit.turn_status,
|
|
589
|
-
}),
|
|
590
|
-
);
|
|
591
|
-
let _ = event_log.write("send.name_app_server", event.clone());
|
|
592
|
-
Ok(event)
|
|
593
|
-
}
|
|
594
|
-
Err(crate::codex_app_server::AppServerError::LeaderBusy(message)) => {
|
|
595
|
-
let event = merge_json(
|
|
596
|
-
base_event.clone(),
|
|
597
|
-
serde_json::json!({
|
|
598
|
-
"ok": false,
|
|
599
|
-
"status": "retry_scheduled",
|
|
600
|
-
"reason": "leader_busy",
|
|
601
|
-
"channel": "leader_busy",
|
|
602
|
-
"error": message,
|
|
603
|
-
}),
|
|
604
|
-
);
|
|
605
|
-
let _ = event_log.write("send.name_app_server", event.clone());
|
|
606
|
-
Ok(event)
|
|
607
|
-
}
|
|
608
|
-
Err(error) => {
|
|
609
|
-
let event = merge_json(
|
|
610
|
-
base_event.clone(),
|
|
611
|
-
serde_json::json!({
|
|
612
|
-
"ok": false,
|
|
613
|
-
"status": "refused",
|
|
614
|
-
"reason": error.code(),
|
|
615
|
-
"channel": "rebind_required",
|
|
616
|
-
"error": error.to_string(),
|
|
617
|
-
"action": "run team-agent attach-app-server-leader for the target team",
|
|
618
|
-
}),
|
|
619
|
-
);
|
|
620
|
-
let _ = event_log.write("send.name_app_server", event.clone());
|
|
621
|
-
Ok(event)
|
|
622
|
-
}
|
|
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
|
+
)),
|
|
623
649
|
}
|
|
624
650
|
}
|
|
625
651
|
|
|
626
|
-
fn
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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));
|
|
630
671
|
}
|
|
631
672
|
}
|
|
632
|
-
|
|
673
|
+
Ok(value)
|
|
633
674
|
}
|
|
634
675
|
|
|
635
|
-
fn
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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);
|
|
640
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)
|
|
641
711
|
}
|
|
642
712
|
|
|
643
713
|
fn routing_ambiguous_value(
|
|
@@ -1220,183 +1290,33 @@ pub fn send_to_canonical_leader_target(
|
|
|
1220
1290
|
sender_workspace: &std::path::Path,
|
|
1221
1291
|
name: &str,
|
|
1222
1292
|
content: &str,
|
|
1223
|
-
sender: &
|
|
1293
|
+
sender: &TrustedSender,
|
|
1224
1294
|
task_id: Option<&str>,
|
|
1225
1295
|
) -> Result<serde_json::Value, CliError> {
|
|
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
|
-
|
|
1251
|
-
"stable_qualified_name": null,
|
|
1252
|
-
"channel": "leader_mailbox",
|
|
1253
|
-
"delivered": false,
|
|
1254
|
-
"message_status": "queued_until_leader_attach",
|
|
1255
|
-
"action": "run `team-agent leaders` to see registered leaders; inspect queued leader messages with `team-agent inbox`; retry with a qualified name",
|
|
1256
|
-
"registry_stale": false,
|
|
1257
|
-
}));
|
|
1258
|
-
}
|
|
1259
|
-
if candidates_all.len() > 1 {
|
|
1260
|
-
let cand_json: Vec<serde_json::Value> = candidates_all
|
|
1261
|
-
.iter()
|
|
1262
|
-
.map(|e| {
|
|
1263
|
-
serde_json::json!({
|
|
1264
|
-
"name": e.qualified_name,
|
|
1265
|
-
"workspace": e.workspace.display().to_string(),
|
|
1266
|
-
"team_key": e.team_key,
|
|
1267
|
-
"workspace_hash": e.workspace_hash,
|
|
1268
|
-
"stable_qualified_name": e.stable_qualified_name,
|
|
1269
|
-
})
|
|
1270
|
-
})
|
|
1271
|
-
.collect();
|
|
1272
|
-
return Ok(serde_json::json!({
|
|
1273
|
-
"ok": false,
|
|
1274
|
-
"status": "refused",
|
|
1275
|
-
"reason": "name_ambiguous",
|
|
1276
|
-
"requested_name": name,
|
|
1277
|
-
"resolved_via": "host_leader_registry",
|
|
1278
|
-
"candidates": cand_json,
|
|
1279
|
-
"channel": "leader_mailbox",
|
|
1280
|
-
"delivered": false,
|
|
1281
|
-
"action": "run `team-agent leaders` and retry with the qualified name",
|
|
1282
|
-
}));
|
|
1283
|
-
}
|
|
1284
|
-
let entry = candidates_all.into_iter().next().ok_or_else(|| {
|
|
1285
|
-
CliError::Runtime("internal: candidate list must have at least one entry".to_string())
|
|
1286
|
-
})?;
|
|
1287
|
-
// Canonical-validate the entry against target workspace state. Send
|
|
1288
|
-
// through the same E6 --to-name path so live inject and mailbox both
|
|
1289
|
-
// funnel through one code path.
|
|
1290
|
-
let (status, reason) = crate::leader::registry::classify(&entry);
|
|
1291
|
-
if status == "STALE" {
|
|
1292
|
-
// Check whether the underlying team is still alive — if so we
|
|
1293
|
-
// may still queue via the E6 mailbox path (leader-not-attached
|
|
1294
|
-
// shape). If the workspace/team is gone we refuse `registry_stale`.
|
|
1295
|
-
let state = crate::state::persist::load_runtime_state(&entry.workspace).ok();
|
|
1296
|
-
let team_alive = state
|
|
1297
|
-
.as_ref()
|
|
1298
|
-
.and_then(|s| s.get("teams"))
|
|
1299
|
-
.and_then(|v| v.as_object())
|
|
1300
|
-
.and_then(|teams| teams.get(&entry.team_key))
|
|
1301
|
-
.and_then(|t| t.get("status"))
|
|
1302
|
-
.and_then(serde_json::Value::as_str)
|
|
1303
|
-
.map(|s| s == "alive" || s.is_empty())
|
|
1304
|
-
.unwrap_or(false);
|
|
1305
|
-
if !team_alive {
|
|
1306
|
-
return Ok(serde_json::json!({
|
|
1307
|
-
"ok": false,
|
|
1308
|
-
"status": "refused",
|
|
1309
|
-
"reason": "registry_stale",
|
|
1310
|
-
"requested_name": name,
|
|
1311
|
-
"resolved_via": "host_leader_registry",
|
|
1312
|
-
"stale_reason": reason,
|
|
1313
|
-
"workspace_hash": entry.workspace_hash,
|
|
1314
|
-
"stable_qualified_name": entry.stable_qualified_name,
|
|
1315
|
-
"channel": "leader_mailbox",
|
|
1316
|
-
"delivered": false,
|
|
1317
|
-
"action": "target team is not alive; run `team-agent leaders` for current state",
|
|
1318
|
-
}));
|
|
1319
|
-
}
|
|
1320
|
-
// Team alive but leader unattached → E6 mailbox.
|
|
1321
|
-
let event_log = crate::event_log::EventLog::new(&entry.workspace);
|
|
1322
|
-
let task = task_id.map(|s| crate::model::ids::TaskId::new(s.to_string()));
|
|
1323
|
-
let outcome = crate::messaging::enqueue_leader_mailbox_until_attach(
|
|
1324
|
-
&entry.workspace,
|
|
1325
|
-
&entry.team_key,
|
|
1326
|
-
content,
|
|
1327
|
-
task.as_ref(),
|
|
1328
|
-
sender,
|
|
1329
|
-
&event_log,
|
|
1330
|
-
)
|
|
1331
|
-
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
1332
|
-
return Ok(serde_json::json!({
|
|
1333
|
-
"ok": true,
|
|
1334
|
-
"status": "queued_until_leader_attach",
|
|
1335
|
-
"message_status": "queued_until_leader_attach",
|
|
1336
|
-
"channel": "leader_mailbox",
|
|
1337
|
-
"delivered": false,
|
|
1338
|
-
"resolved_via": "host_leader_registry",
|
|
1339
|
-
"requested_name": name,
|
|
1340
|
-
"to_leader": entry.qualified_name,
|
|
1341
|
-
"target_workspace": entry.workspace.display().to_string(),
|
|
1342
|
-
"workspace_hash": entry.workspace_hash,
|
|
1343
|
-
"stable_qualified_name": entry.stable_qualified_name,
|
|
1344
|
-
"team_key": entry.team_key,
|
|
1345
|
-
"message_id": outcome.message_id,
|
|
1346
|
-
}));
|
|
1347
|
-
}
|
|
1348
|
-
// LIVE: canonical-validated. Delegate to the E6 --to-name path via a
|
|
1349
|
-
// synthesized `<workspace>::<team_key>/leader` name so live inject +
|
|
1350
|
-
// mailbox both go through one code path.
|
|
1351
|
-
let to_name = format!("{}::{}/leader", entry.workspace.display(), entry.team_key);
|
|
1352
|
-
// 0.5.45 naming-addressing (design §3.1 / §4.1): internal
|
|
1353
|
-
// registry-to-E6 delegation MUST pass None for bare_team_scope.
|
|
1354
|
-
// The synthesized name above is a full `workspace::team/leader`
|
|
1355
|
-
// form, and the caller's `--team` flag (if any) must not
|
|
1356
|
-
// override the registry's authoritative team_key.
|
|
1357
|
-
let (resolved, transport) =
|
|
1358
|
-
match crate::cli::named_address::resolve_name_for_cli(sender_workspace, &to_name, None) {
|
|
1359
|
-
Ok(r) => r,
|
|
1360
|
-
Err(err) => {
|
|
1361
|
-
// Named-address refusal — surface it verbatim but tag as
|
|
1362
|
-
// registry-resolved so callers can trace the origin.
|
|
1363
|
-
let mut body = err.to_json();
|
|
1364
|
-
if let Some(obj) = body.as_object_mut() {
|
|
1365
|
-
obj.insert(
|
|
1366
|
-
"resolved_via".to_string(),
|
|
1367
|
-
serde_json::Value::String("host_leader_registry".to_string()),
|
|
1368
|
-
);
|
|
1369
|
-
obj.insert("delivered".to_string(), serde_json::Value::Bool(false));
|
|
1370
|
-
}
|
|
1371
|
-
return Ok(body);
|
|
1372
|
-
}
|
|
1373
|
-
};
|
|
1374
|
-
let mut value = send_to_named_pane_direct(
|
|
1375
|
-
sender_workspace,
|
|
1376
|
-
transport.as_ref(),
|
|
1377
|
-
&resolved,
|
|
1378
|
-
content,
|
|
1379
|
-
sender,
|
|
1380
|
-
task_id,
|
|
1381
|
-
true,
|
|
1382
|
-
)?;
|
|
1383
|
-
if let Some(obj) = value.as_object_mut() {
|
|
1384
|
-
obj.insert(
|
|
1385
|
-
"resolved_via".to_string(),
|
|
1386
|
-
serde_json::Value::String("host_leader_registry".to_string()),
|
|
1387
|
-
);
|
|
1388
|
-
obj.insert(
|
|
1389
|
-
"to_leader".to_string(),
|
|
1390
|
-
serde_json::Value::String(entry.qualified_name.clone()),
|
|
1391
|
-
);
|
|
1392
|
-
// Honest delivered marker. `send_to_named_pane_direct` sets `ok`
|
|
1393
|
-
// to whether physical inject verified — mirror that as
|
|
1394
|
-
// `delivered`.
|
|
1395
|
-
let ok = obj
|
|
1396
|
-
.get("ok")
|
|
1397
|
-
.and_then(serde_json::Value::as_bool)
|
|
1398
|
-
.unwrap_or(false);
|
|
1399
|
-
obj.insert("delivered".to_string(), serde_json::Value::Bool(ok));
|
|
1400
|
-
}
|
|
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);
|
|
1401
1321
|
Ok(value)
|
|
1402
1322
|
}
|