@team-agent/installer 0.5.60 → 0.5.61
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 +44 -1
- package/crates/team-agent/src/cli/diagnose.rs +183 -0
- package/crates/team-agent/src/cli/emit.rs +63 -14
- package/crates/team-agent/src/cli/leader.rs +8 -1
- package/crates/team-agent/src/cli/mod.rs +263 -5
- package/crates/team-agent/src/cli/named_address.rs +18 -1
- package/crates/team-agent/src/cli/send/presentation.rs +2 -0
- package/crates/team-agent/src/cli/spec.rs +4 -1
- package/crates/team-agent/src/cli/status_port/store.rs +7 -3
- package/crates/team-agent/src/cli/tests/named_address.rs +65 -0
- package/crates/team-agent/src/cli/types.rs +33 -0
- package/crates/team-agent/src/communication_mode/mod.rs +53 -0
- package/crates/team-agent/src/compiler/tests.rs +5 -5
- package/crates/team-agent/src/compiler.rs +53 -1
- package/crates/team-agent/src/db/message_store.rs +45 -1
- package/crates/team-agent/src/fake_worker.rs +21 -1
- package/crates/team-agent/src/leader/start.rs +732 -94
- package/crates/team-agent/src/leader/tests/identity.rs +64 -57
- package/crates/team-agent/src/leader/tests/identity_session_names.rs +42 -0
- package/crates/team-agent/src/lib.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +29 -11
- package/crates/team-agent/src/mcp_server/normalize.rs +1 -0
- package/crates/team-agent/src/mcp_server/tests/send.rs +26 -0
- package/crates/team-agent/src/mcp_server/tests/wire.rs +1 -1
- package/crates/team-agent/src/mcp_server/tools.rs +100 -66
- package/crates/team-agent/src/mcp_server/types.rs +5 -0
- package/crates/team-agent/src/mcp_server/wire.rs +27 -21
- package/crates/team-agent/src/messaging/delivery.rs +105 -38
- package/crates/team-agent/src/messaging/leader_channel.rs +33 -10
- package/crates/team-agent/src/messaging/leader_receiver.rs +40 -23
- package/crates/team-agent/src/messaging/presentation.rs +109 -0
- package/crates/team-agent/src/messaging/results.rs +165 -17
- package/crates/team-agent/src/messaging/send.rs +94 -81
- package/crates/team-agent/src/messaging/tests/leader_channel.rs +6 -6
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +187 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +13 -6
- package/crates/team-agent/src/messaging/types.rs +5 -0
- package/crates/team-agent/src/messaging/watchers.rs +5 -0
- package/crates/team-agent/src/model/mod.rs +1 -0
- package/crates/team-agent/src/model/pane_authority_refusal.rs +358 -0
- package/crates/team-agent/src/model/spec.rs +16 -0
- package/crates/team-agent/src/provider/session/capture.rs +40 -18
- package/crates/team-agent/src/provider/session_scan/claude.rs +92 -3
- package/crates/team-agent/src/provider/session_scan/common.rs +23 -7
- package/crates/team-agent/src/provider/session_scan.rs +5 -0
- package/crates/team-agent/src/topology.rs +3 -0
- package/package.json +4 -4
- package/skills/team-agent/command-coverage.json +379 -0
|
@@ -17,17 +17,17 @@ use crate::state::persist::{
|
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
// ── REUSE: step 11 messaging delegate surface ───────────────────────────────
|
|
20
|
-
use crate::messaging::{self, MessageTarget, SendOptions, TrustedSender};
|
|
20
|
+
use crate::messaging::{self, DeliveryStatus, MessageTarget, SendOptions, TrustedSender};
|
|
21
21
|
|
|
22
22
|
use super::helpers::{
|
|
23
23
|
current_reportable_message_for, delivery_outcome_value, direct_message_attribution_for,
|
|
24
|
-
ensure_object, enum_value,
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
ensure_object, enum_value, is_worker_recipient, json_dumps_default, latest_task_for_assignee,
|
|
25
|
+
non_empty_string, object_fields, requires_ack_for_target, tool_runtime_error,
|
|
26
|
+
DirectMessageAttribution,
|
|
27
27
|
};
|
|
28
28
|
use super::normalize::{
|
|
29
|
-
compact_tool_result, normalize_report_envelope,
|
|
30
|
-
|
|
29
|
+
compact_tool_result, normalize_report_envelope, report_result_integrity_warnings,
|
|
30
|
+
validate_test_evidence_schema,
|
|
31
31
|
};
|
|
32
32
|
use super::types::{
|
|
33
33
|
Scope, SendOutcome, ToolError, ToolErrorReason, ToolOk, ToolResult, VisiblePeers,
|
|
@@ -116,6 +116,22 @@ impl TeamOrchestratorTools {
|
|
|
116
116
|
"ValueError",
|
|
117
117
|
));
|
|
118
118
|
};
|
|
119
|
+
if let Some(route) = task.get("result_route") {
|
|
120
|
+
let Some(route) = route.as_str() else {
|
|
121
|
+
return Err(ToolError::new(
|
|
122
|
+
ToolErrorReason::InvalidToolArguments,
|
|
123
|
+
"assign_task task.result_route must be 'leader' or 'pipeline'",
|
|
124
|
+
"ValueError",
|
|
125
|
+
));
|
|
126
|
+
};
|
|
127
|
+
if crate::messaging::results::ResultRoute::parse(route).is_none() {
|
|
128
|
+
return Err(ToolError::new(
|
|
129
|
+
ToolErrorReason::InvalidToolArguments,
|
|
130
|
+
format!("assign_task task.result_route has unknown value: {route}"),
|
|
131
|
+
"ValueError",
|
|
132
|
+
));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
119
135
|
|
|
120
136
|
let task_value = Value::Object(task_obj.clone());
|
|
121
137
|
let recovery = task_recovery_marker(&task_value);
|
|
@@ -185,6 +201,7 @@ impl TeamOrchestratorTools {
|
|
|
185
201
|
requires_ack,
|
|
186
202
|
scope_override,
|
|
187
203
|
None,
|
|
204
|
+
None,
|
|
188
205
|
)
|
|
189
206
|
}
|
|
190
207
|
|
|
@@ -195,17 +212,20 @@ impl TeamOrchestratorTools {
|
|
|
195
212
|
task_id: Option<&str>,
|
|
196
213
|
requires_ack: Option<bool>,
|
|
197
214
|
scope_override: Option<Scope>,
|
|
215
|
+
mailbox: Option<&Value>,
|
|
198
216
|
presentation: Option<&Value>,
|
|
199
217
|
) -> Result<SendOutcome, ToolError> {
|
|
200
|
-
let
|
|
201
|
-
crate::messaging::presentation::
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
218
|
+
let normalized =
|
|
219
|
+
crate::messaging::presentation::normalize_send_presentation(mailbox, presentation)
|
|
220
|
+
.map_err(|error| {
|
|
221
|
+
ToolError::new(
|
|
222
|
+
ToolErrorReason::InvalidToolArguments,
|
|
223
|
+
format!("invalid send routing: {error}"),
|
|
224
|
+
"PresentationError",
|
|
225
|
+
)
|
|
226
|
+
})?;
|
|
227
|
+
let presentation = normalized.request;
|
|
228
|
+
let deprecation = normalized.deprecation;
|
|
209
229
|
let canonical_owner_team = self.canonical_owner_team_key()?;
|
|
210
230
|
if matches!(scope_override, Some(Scope::Workspace)) {
|
|
211
231
|
return Err(self.rpc_scope_refused(
|
|
@@ -261,6 +281,21 @@ impl TeamOrchestratorTools {
|
|
|
261
281
|
if is_worker_recipient(to) {
|
|
262
282
|
let out = messaging::send_message(&self.workspace, to, content, &opts)
|
|
263
283
|
.map_err(tool_runtime_error)?;
|
|
284
|
+
if matches!(out.status, DeliveryStatus::StoredOnly) {
|
|
285
|
+
let value = delivery_outcome_value(&out);
|
|
286
|
+
let mut ok = compact_tool_result(&value)?;
|
|
287
|
+
if let Some(verification) = out.verification.as_deref() {
|
|
288
|
+
ok.fields.insert(
|
|
289
|
+
"verification".to_string(),
|
|
290
|
+
Value::String(verification.to_string()),
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
if let Some(deprecation) = deprecation {
|
|
294
|
+
ok.fields
|
|
295
|
+
.insert("warning".to_string(), Value::String(deprecation));
|
|
296
|
+
}
|
|
297
|
+
return Ok(SendOutcome::Direct(ok));
|
|
298
|
+
}
|
|
264
299
|
// tools.py:175-181 — accepted+poll_via ONLY for a REAL message_id; any other
|
|
265
300
|
// outcome falls back to the compacted direct result. Never invent an
|
|
266
301
|
// `mcp_<timestamp>` id: it does not exist in the store and makes the
|
|
@@ -276,12 +311,25 @@ impl TeamOrchestratorTools {
|
|
|
276
311
|
return Ok(SendOutcome::WorkerAccepted {
|
|
277
312
|
poll_via: format!("team-agent inbox {message_id}"),
|
|
278
313
|
message_id,
|
|
314
|
+
warning: deprecation,
|
|
279
315
|
});
|
|
280
316
|
}
|
|
281
317
|
let out = messaging::send_message(&self.workspace, to, content, &opts)
|
|
282
318
|
.map_err(tool_runtime_error)?;
|
|
283
319
|
let value = delivery_outcome_value(&out);
|
|
284
|
-
let ok = compact_tool_result(&value)?;
|
|
320
|
+
let mut ok = compact_tool_result(&value)?;
|
|
321
|
+
if matches!(out.status, DeliveryStatus::StoredOnly) {
|
|
322
|
+
if let Some(verification) = out.verification.as_deref() {
|
|
323
|
+
ok.fields.insert(
|
|
324
|
+
"verification".to_string(),
|
|
325
|
+
Value::String(verification.to_string()),
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if let Some(deprecation) = deprecation {
|
|
330
|
+
ok.fields
|
|
331
|
+
.insert("warning".to_string(), Value::String(deprecation));
|
|
332
|
+
}
|
|
285
333
|
Ok(SendOutcome::Direct(ok))
|
|
286
334
|
}
|
|
287
335
|
|
|
@@ -352,10 +400,11 @@ impl TeamOrchestratorTools {
|
|
|
352
400
|
task_id: Option<&str>,
|
|
353
401
|
agent_id: Option<&str>,
|
|
354
402
|
) -> ToolResult {
|
|
403
|
+
let status = enum_value(status);
|
|
355
404
|
self.report_result_with_presentation(
|
|
356
405
|
envelope,
|
|
357
406
|
summary,
|
|
358
|
-
status,
|
|
407
|
+
status.as_str(),
|
|
359
408
|
changes,
|
|
360
409
|
tests,
|
|
361
410
|
risks,
|
|
@@ -372,7 +421,7 @@ impl TeamOrchestratorTools {
|
|
|
372
421
|
&self,
|
|
373
422
|
envelope: Option<&Value>,
|
|
374
423
|
summary: Option<&str>,
|
|
375
|
-
status:
|
|
424
|
+
status: Option<&str>,
|
|
376
425
|
changes: Option<&[Value]>,
|
|
377
426
|
tests: Option<&[Value]>,
|
|
378
427
|
risks: Option<&[Value]>,
|
|
@@ -404,7 +453,10 @@ impl TeamOrchestratorTools {
|
|
|
404
453
|
);
|
|
405
454
|
}
|
|
406
455
|
if !obj.contains_key("status") {
|
|
407
|
-
obj.insert(
|
|
456
|
+
obj.insert(
|
|
457
|
+
"status".to_string(),
|
|
458
|
+
Value::String(status.unwrap_or("success").to_string()),
|
|
459
|
+
);
|
|
408
460
|
}
|
|
409
461
|
if !obj.contains_key("task_id") {
|
|
410
462
|
// Blocker-1 (prerelease 0.4.0): scoped-team task inference +
|
|
@@ -499,29 +551,36 @@ impl TeamOrchestratorTools {
|
|
|
499
551
|
obj.insert("agent_id".to_string(), Value::String(resolved));
|
|
500
552
|
}
|
|
501
553
|
if !obj.contains_key("changes") {
|
|
502
|
-
|
|
554
|
+
obj.insert(
|
|
555
|
+
"changes".to_string(),
|
|
556
|
+
Value::Array(changes.unwrap_or(&[]).to_vec()),
|
|
557
|
+
);
|
|
503
558
|
}
|
|
504
559
|
if !obj.contains_key("tests") {
|
|
505
|
-
|
|
560
|
+
obj.insert(
|
|
561
|
+
"tests".to_string(),
|
|
562
|
+
Value::Array(tests.unwrap_or(&[]).to_vec()),
|
|
563
|
+
);
|
|
506
564
|
}
|
|
507
565
|
if !obj.contains_key("risks") {
|
|
508
|
-
|
|
566
|
+
obj.insert(
|
|
567
|
+
"risks".to_string(),
|
|
568
|
+
Value::Array(risks.unwrap_or(&[]).to_vec()),
|
|
569
|
+
);
|
|
509
570
|
}
|
|
510
571
|
if !obj.contains_key("artifacts") {
|
|
511
|
-
|
|
572
|
+
obj.insert(
|
|
573
|
+
"artifacts".to_string(),
|
|
574
|
+
Value::Array(artifacts.unwrap_or(&[]).to_vec()),
|
|
575
|
+
);
|
|
512
576
|
}
|
|
513
577
|
if !obj.contains_key("next_actions") {
|
|
514
|
-
|
|
578
|
+
obj.insert(
|
|
579
|
+
"next_actions".to_string(),
|
|
580
|
+
Value::Array(next_actions.unwrap_or(&[]).to_vec()),
|
|
581
|
+
);
|
|
515
582
|
}
|
|
516
583
|
}
|
|
517
|
-
// T3-1 cr verdict (refined): an unknown non-empty status literal normalizes to
|
|
518
|
-
// Partial and must be OBSERVABLE at this ingestion boundary (the envelope-borne
|
|
519
|
-
// path; the wire `status` arg path emits at dispatch). Never a silent swallow.
|
|
520
|
-
if let Some(raw) =
|
|
521
|
-
normalize_result_status_observed(base.get("status").and_then(Value::as_str)).1
|
|
522
|
-
{
|
|
523
|
-
self.note_unknown_result_status(&raw);
|
|
524
|
-
}
|
|
525
584
|
if let Err(error) = validate_test_evidence_schema(base.get("tests")) {
|
|
526
585
|
return Err(ToolError::new(
|
|
527
586
|
ToolErrorReason::InvalidToolArguments,
|
|
@@ -538,8 +597,15 @@ impl TeamOrchestratorTools {
|
|
|
538
597
|
));
|
|
539
598
|
}
|
|
540
599
|
let warnings = report_result_integrity_warnings(&base, &normalized);
|
|
541
|
-
let mut env_value =
|
|
542
|
-
|
|
600
|
+
let mut env_value = base;
|
|
601
|
+
if let Some(obj) = env_value.as_object_mut() {
|
|
602
|
+
obj.entry("schema_version")
|
|
603
|
+
.or_insert_with(|| Value::String("result_envelope_v1".to_string()));
|
|
604
|
+
obj.insert(
|
|
605
|
+
"presentation".to_string(),
|
|
606
|
+
serde_json::to_value(&normalized.presentation).unwrap_or(Value::Null),
|
|
607
|
+
);
|
|
608
|
+
}
|
|
543
609
|
if !warnings.is_empty() {
|
|
544
610
|
if let Some(obj) = env_value.as_object_mut() {
|
|
545
611
|
obj.insert("warnings".to_string(), Value::Array(warnings));
|
|
@@ -555,20 +621,6 @@ impl TeamOrchestratorTools {
|
|
|
555
621
|
.and_then(|value| compact_tool_result(&value))
|
|
556
622
|
}
|
|
557
623
|
|
|
558
|
-
/// T3-1 cr verdict: the observable record of an unknown→Partial status
|
|
559
|
-
/// normalization (`provider.result.unknown_status_normalized`, raw literal
|
|
560
|
-
/// included) — MUST-NOT-13: the swallow is never silent.
|
|
561
|
-
pub(crate) fn note_unknown_result_status(&self, raw: &str) {
|
|
562
|
-
let _ = EventLog::new(&self.workspace).write(
|
|
563
|
-
"provider.result.unknown_status_normalized",
|
|
564
|
-
serde_json::json!({
|
|
565
|
-
"agent_id": self.agent_id.as_ref().map(AgentId::as_str),
|
|
566
|
-
"raw_status": raw,
|
|
567
|
-
"normalized": "partial",
|
|
568
|
-
}),
|
|
569
|
-
);
|
|
570
|
-
}
|
|
571
|
-
|
|
572
624
|
/// `update_state` (`tools.py:316-325`): delegated through the lifecycle tools
|
|
573
625
|
/// facade. S0 preserves the old placeholder behavior.
|
|
574
626
|
pub fn update_state(&self, note: &str) -> ToolResult {
|
|
@@ -1179,24 +1231,6 @@ fn merge_object_fields(existing: &mut Value, incoming: &Value) {
|
|
|
1179
1231
|
}
|
|
1180
1232
|
}
|
|
1181
1233
|
|
|
1182
|
-
fn copy_report_attribution_fields(source: &Value, target: &mut Value) {
|
|
1183
|
-
let Some(src) = source.as_object() else {
|
|
1184
|
-
return;
|
|
1185
|
-
};
|
|
1186
|
-
let Some(dst) = target.as_object_mut() else {
|
|
1187
|
-
return;
|
|
1188
|
-
};
|
|
1189
|
-
for key in [
|
|
1190
|
-
"attributed_message_id",
|
|
1191
|
-
"attribution_scope",
|
|
1192
|
-
"task_id_source",
|
|
1193
|
-
] {
|
|
1194
|
-
if let Some(value) = src.get(key) {
|
|
1195
|
-
dst.entry(key.to_string()).or_insert(value.clone());
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
1234
|
fn push_report_warning(obj: &mut serde_json::Map<String, Value>, warning: Value) {
|
|
1201
1235
|
let Some(code) = warning.get("code").and_then(Value::as_str) else {
|
|
1202
1236
|
return;
|
|
@@ -322,6 +322,7 @@ pub enum SendOutcome {
|
|
|
322
322
|
message_id: String,
|
|
323
323
|
/// Byte-stable `"team-agent inbox <message_id>"`.
|
|
324
324
|
poll_via: String,
|
|
325
|
+
warning: Option<String>,
|
|
325
326
|
},
|
|
326
327
|
/// Compacted delegate result (leader / `*` / broadcast / fanout).
|
|
327
328
|
Direct(ToolOk),
|
|
@@ -335,12 +336,16 @@ impl SendOutcome {
|
|
|
335
336
|
SendOutcome::WorkerAccepted {
|
|
336
337
|
message_id,
|
|
337
338
|
poll_via,
|
|
339
|
+
warning,
|
|
338
340
|
} => {
|
|
339
341
|
let mut obj = serde_json::Map::new();
|
|
340
342
|
obj.insert("status".to_string(), Value::String("accepted".to_string()));
|
|
341
343
|
obj.insert("delivery_pending".to_string(), Value::Bool(true));
|
|
342
344
|
obj.insert("poll_via".to_string(), Value::String(poll_via.clone()));
|
|
343
345
|
obj.insert("message_id".to_string(), Value::String(message_id.clone()));
|
|
346
|
+
if let Some(warning) = warning {
|
|
347
|
+
obj.insert("warning".to_string(), Value::String(warning.clone()));
|
|
348
|
+
}
|
|
344
349
|
Value::Object(obj)
|
|
345
350
|
}
|
|
346
351
|
SendOutcome::Direct(ok) => Value::Object(ok.fields.clone()),
|
|
@@ -9,7 +9,6 @@ use crate::event_log::EventLog;
|
|
|
9
9
|
use crate::messaging::MessageTarget;
|
|
10
10
|
|
|
11
11
|
use super::helpers::{json_dumps_default, object_fields};
|
|
12
|
-
use super::normalize::normalize_result_status;
|
|
13
12
|
use super::tools::TeamOrchestratorTools;
|
|
14
13
|
use super::types::{
|
|
15
14
|
McpError, McpTool, RpcError, RpcId, RpcMethod, RpcResponse, Scope, SendOutcome,
|
|
@@ -369,7 +368,7 @@ fn python_repr(value: &str) -> String {
|
|
|
369
368
|
fn tool_contract(tool: McpTool) -> Value {
|
|
370
369
|
let (description, required) = match tool {
|
|
371
370
|
McpTool::SendMessage => (
|
|
372
|
-
"Send a message to a teammate, the leader, or '*' for all other team members.
|
|
371
|
+
"Send a message to a teammate, the leader, or '*' for all other team members. mailbox=true stores durably without live injection; the default is live delivery.",
|
|
373
372
|
vec!["to", "content"],
|
|
374
373
|
),
|
|
375
374
|
McpTool::AssignTask => ("Assign or update a task in the team graph and deliver it to its assignee.", vec!["task"]),
|
|
@@ -404,11 +403,7 @@ fn tool_properties(tool: McpTool) -> serde_json::Map<String, Value> {
|
|
|
404
403
|
let mut properties = serde_json::Map::new();
|
|
405
404
|
match tool {
|
|
406
405
|
McpTool::AssignTask => {
|
|
407
|
-
insert_property(
|
|
408
|
-
&mut properties,
|
|
409
|
-
"task",
|
|
410
|
-
object_property("Task object to add or update."),
|
|
411
|
-
);
|
|
406
|
+
insert_property(&mut properties, "task", task_property());
|
|
412
407
|
insert_property(
|
|
413
408
|
&mut properties,
|
|
414
409
|
"message",
|
|
@@ -424,8 +419,10 @@ fn tool_properties(tool: McpTool) -> serde_json::Map<String, Value> {
|
|
|
424
419
|
insert_property(&mut properties, "content", string_property("Message body."));
|
|
425
420
|
insert_property(
|
|
426
421
|
&mut properties,
|
|
427
|
-
"
|
|
428
|
-
|
|
422
|
+
"mailbox",
|
|
423
|
+
boolean_property(
|
|
424
|
+
"Set true to store durably without live injection; omit for default live delivery.",
|
|
425
|
+
),
|
|
429
426
|
);
|
|
430
427
|
}
|
|
431
428
|
McpTool::ReportResult => {
|
|
@@ -603,6 +600,25 @@ fn object_property(description: &str) -> Value {
|
|
|
603
600
|
serde_json::json!({"type": "object", "description": description, "additionalProperties": true})
|
|
604
601
|
}
|
|
605
602
|
|
|
603
|
+
fn task_property() -> Value {
|
|
604
|
+
serde_json::json!({
|
|
605
|
+
"type": "object",
|
|
606
|
+
"description": "Task object to add or update.",
|
|
607
|
+
"properties": {
|
|
608
|
+
"result_route": {
|
|
609
|
+
"type": "string",
|
|
610
|
+
"enum": crate::messaging::results::ResultRoute::ALL
|
|
611
|
+
.iter()
|
|
612
|
+
.map(|route| route.as_str())
|
|
613
|
+
.collect::<Vec<_>>(),
|
|
614
|
+
"default": crate::messaging::results::ResultRoute::default().as_str(),
|
|
615
|
+
"description": "Result destination chosen by the task assigner."
|
|
616
|
+
}
|
|
617
|
+
},
|
|
618
|
+
"additionalProperties": true
|
|
619
|
+
})
|
|
620
|
+
}
|
|
621
|
+
|
|
606
622
|
fn presentation_property(description: &str) -> Value {
|
|
607
623
|
serde_json::json!({
|
|
608
624
|
"type": "object",
|
|
@@ -646,6 +662,7 @@ pub(crate) fn dispatch_tool(
|
|
|
646
662
|
None,
|
|
647
663
|
None,
|
|
648
664
|
None,
|
|
665
|
+
args.get("mailbox"),
|
|
649
666
|
args.get("presentation"),
|
|
650
667
|
)?;
|
|
651
668
|
match outcome {
|
|
@@ -658,18 +675,7 @@ pub(crate) fn dispatch_tool(
|
|
|
658
675
|
McpTool::ReportResult => tools.report_result_with_presentation(
|
|
659
676
|
args.get("envelope"),
|
|
660
677
|
args.get("summary").and_then(Value::as_str),
|
|
661
|
-
|
|
662
|
-
// Partial and is OBSERVABLE at this ingestion boundary, never silent.
|
|
663
|
-
{
|
|
664
|
-
let (status, unknown) =
|
|
665
|
-
crate::mcp_server::normalize::normalize_result_status_observed(
|
|
666
|
-
args.get("status").and_then(Value::as_str),
|
|
667
|
-
);
|
|
668
|
-
if let Some(raw) = unknown {
|
|
669
|
-
tools.note_unknown_result_status(&raw);
|
|
670
|
-
}
|
|
671
|
-
status
|
|
672
|
-
},
|
|
678
|
+
args.get("status").and_then(Value::as_str),
|
|
673
679
|
args.get("changes")
|
|
674
680
|
.and_then(Value::as_array)
|
|
675
681
|
.map(Vec::as_slice),
|
|
@@ -220,7 +220,12 @@ pub fn deliver_pending_message(
|
|
|
220
220
|
}
|
|
221
221
|
_ => state,
|
|
222
222
|
};
|
|
223
|
-
if message.recipient == "leader"
|
|
223
|
+
if message.recipient == "leader"
|
|
224
|
+
&& matches!(
|
|
225
|
+
message.status.as_str(),
|
|
226
|
+
"submitted_pending_acceptance" | "injected_awaiting_receipt"
|
|
227
|
+
)
|
|
228
|
+
{
|
|
224
229
|
return observe_pending_leader_acceptance(store, event_log, state, message_id);
|
|
225
230
|
}
|
|
226
231
|
if message.recipient == "leader" && message.status == "queued_until_leader_attach" {
|
|
@@ -341,6 +346,12 @@ pub fn deliver_pending_message(
|
|
|
341
346
|
.get("status")
|
|
342
347
|
.and_then(serde_json::Value::as_str)
|
|
343
348
|
.unwrap_or("unbound");
|
|
349
|
+
let refusal = match &reason {
|
|
350
|
+
crate::messaging::LeaderChannelUnbound::PaneWorkspaceMismatch(_) => {
|
|
351
|
+
DeliveryRefusal::PaneWorkspaceMismatch
|
|
352
|
+
}
|
|
353
|
+
_ => DeliveryRefusal::LeaderNotAttached,
|
|
354
|
+
};
|
|
344
355
|
store.mark(message_id, "failed", Some("leader_not_attached"))?;
|
|
345
356
|
event_log.write(
|
|
346
357
|
"leader_receiver.delivery_blocked",
|
|
@@ -349,7 +360,7 @@ pub fn deliver_pending_message(
|
|
|
349
360
|
"sender": message.sender,
|
|
350
361
|
"reason": "leader_receiver_not_attached",
|
|
351
362
|
"error": "leader_not_attached",
|
|
352
|
-
"channel_reason":
|
|
363
|
+
"channel_reason": reason.reason_code(),
|
|
353
364
|
"channel": "rebind_required",
|
|
354
365
|
"action": "run team-agent attach-leader or team-agent takeover",
|
|
355
366
|
"status": receiver_status,
|
|
@@ -364,7 +375,7 @@ pub fn deliver_pending_message(
|
|
|
364
375
|
"run team-agent attach-leader or team-agent takeover".to_string(),
|
|
365
376
|
),
|
|
366
377
|
stage: None,
|
|
367
|
-
reason: Some(
|
|
378
|
+
reason: Some(refusal),
|
|
368
379
|
channel: Some("rebind_required".to_string()),
|
|
369
380
|
});
|
|
370
381
|
}
|
|
@@ -711,19 +722,30 @@ pub fn deliver_pending_message(
|
|
|
711
722
|
}
|
|
712
723
|
if is_leader_recipient {
|
|
713
724
|
store.record_delivery_submission(message_id, readback_verified)?;
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
"message_id": message_id,
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
message_id,
|
|
725
|
-
|
|
726
|
-
|
|
725
|
+
match observe_leader_receipt(state, message_id) {
|
|
726
|
+
LeaderReceiptObservation::SourceUnavailable => {
|
|
727
|
+
store.mark_injected_awaiting_receipt(message_id)?;
|
|
728
|
+
event_log.write(
|
|
729
|
+
"leader_receiver.receipt_source_unavailable",
|
|
730
|
+
serde_json::json!({"message_id": message_id}),
|
|
731
|
+
)?;
|
|
732
|
+
return Ok(leader_receipt_source_unavailable_outcome(message_id));
|
|
733
|
+
}
|
|
734
|
+
LeaderReceiptObservation::TokenAbsent => {
|
|
735
|
+
store.mark(message_id, "submitted_pending_acceptance", None)?;
|
|
736
|
+
event_log.write(
|
|
737
|
+
"leader_receiver.acceptance_pending",
|
|
738
|
+
serde_json::json!({
|
|
739
|
+
"message_id": message_id,
|
|
740
|
+
"reason": "provider_receipt_not_observed",
|
|
741
|
+
}),
|
|
742
|
+
)?;
|
|
743
|
+
return Ok(leader_acceptance_pending_outcome(
|
|
744
|
+
message_id,
|
|
745
|
+
"submitted_pending_acceptance",
|
|
746
|
+
));
|
|
747
|
+
}
|
|
748
|
+
LeaderReceiptObservation::TokenObserved => {}
|
|
727
749
|
}
|
|
728
750
|
store.mark_delivered_with_receipt(message_id)?;
|
|
729
751
|
} else {
|
|
@@ -2041,15 +2063,36 @@ fn leader_rollout_path(state: &serde_json::Value) -> Option<std::path::PathBuf>
|
|
|
2041
2063
|
.map(std::path::PathBuf::from)
|
|
2042
2064
|
}
|
|
2043
2065
|
|
|
2044
|
-
|
|
2066
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
2067
|
+
pub(crate) enum LeaderReceiptObservation {
|
|
2068
|
+
SourceUnavailable,
|
|
2069
|
+
TokenAbsent,
|
|
2070
|
+
TokenObserved,
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
pub(crate) fn observe_leader_receipt(
|
|
2074
|
+
state: &serde_json::Value,
|
|
2075
|
+
message_id: &str,
|
|
2076
|
+
) -> LeaderReceiptObservation {
|
|
2045
2077
|
let Some(path) = leader_rollout_path(state) else {
|
|
2046
|
-
return
|
|
2078
|
+
return LeaderReceiptObservation::SourceUnavailable;
|
|
2047
2079
|
};
|
|
2048
|
-
|
|
2080
|
+
let Ok(observed) = rollout_tail_contains_result(
|
|
2049
2081
|
&path,
|
|
2050
2082
|
&format!("[team-agent-token:{message_id}]"),
|
|
2051
2083
|
64 * 1024,
|
|
2052
|
-
)
|
|
2084
|
+
) else {
|
|
2085
|
+
return LeaderReceiptObservation::SourceUnavailable;
|
|
2086
|
+
};
|
|
2087
|
+
if observed {
|
|
2088
|
+
LeaderReceiptObservation::TokenObserved
|
|
2089
|
+
} else {
|
|
2090
|
+
LeaderReceiptObservation::TokenAbsent
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
pub(crate) fn leader_transcript_has_token(state: &serde_json::Value, message_id: &str) -> bool {
|
|
2095
|
+
observe_leader_receipt(state, message_id) == LeaderReceiptObservation::TokenObserved
|
|
2053
2096
|
}
|
|
2054
2097
|
|
|
2055
2098
|
fn leader_acceptance_pending_outcome(message_id: &str, status: &str) -> DeliveryOutcome {
|
|
@@ -2065,17 +2108,41 @@ fn leader_acceptance_pending_outcome(message_id: &str, status: &str) -> Delivery
|
|
|
2065
2108
|
}
|
|
2066
2109
|
}
|
|
2067
2110
|
|
|
2111
|
+
pub(crate) fn leader_receipt_source_unavailable_outcome(message_id: &str) -> DeliveryOutcome {
|
|
2112
|
+
DeliveryOutcome {
|
|
2113
|
+
ok: true,
|
|
2114
|
+
status: DeliveryStatus::Blocked,
|
|
2115
|
+
message_status: MessageStatusShadow("injected_awaiting_receipt".to_string()),
|
|
2116
|
+
message_id: Some(message_id.to_string()),
|
|
2117
|
+
verification: Some("leader_receipt_source_unavailable".to_string()),
|
|
2118
|
+
stage: Some(DeliveryStage::Submit),
|
|
2119
|
+
reason: None,
|
|
2120
|
+
channel: Some("leader_receipt_source_unavailable".to_string()),
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2068
2124
|
fn observe_pending_leader_acceptance(
|
|
2069
2125
|
store: &MessageStore,
|
|
2070
2126
|
event_log: &EventLog,
|
|
2071
2127
|
state: &serde_json::Value,
|
|
2072
2128
|
message_id: &str,
|
|
2073
2129
|
) -> Result<DeliveryOutcome, MessagingError> {
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
message_id
|
|
2077
|
-
|
|
2078
|
-
|
|
2130
|
+
match observe_leader_receipt(state, message_id) {
|
|
2131
|
+
LeaderReceiptObservation::SourceUnavailable => {
|
|
2132
|
+
store.mark_injected_awaiting_receipt(message_id)?;
|
|
2133
|
+
event_log.write(
|
|
2134
|
+
"leader_receiver.receipt_source_unavailable",
|
|
2135
|
+
serde_json::json!({"message_id": message_id}),
|
|
2136
|
+
)?;
|
|
2137
|
+
return Ok(leader_receipt_source_unavailable_outcome(message_id));
|
|
2138
|
+
}
|
|
2139
|
+
LeaderReceiptObservation::TokenAbsent => {
|
|
2140
|
+
return Ok(leader_acceptance_pending_outcome(
|
|
2141
|
+
message_id,
|
|
2142
|
+
"submitted_pending_acceptance",
|
|
2143
|
+
));
|
|
2144
|
+
}
|
|
2145
|
+
LeaderReceiptObservation::TokenObserved => {}
|
|
2079
2146
|
}
|
|
2080
2147
|
store.mark_delivered_with_receipt(message_id)?;
|
|
2081
2148
|
event_log.write(
|
|
@@ -2744,22 +2811,22 @@ fn claude_recipient_rollout(
|
|
|
2744
2811
|
/// Silent on read errors — callers treat missing/unreadable as "token not
|
|
2745
2812
|
/// present" which forces the unverified path.
|
|
2746
2813
|
fn rollout_tail_contains(path: &std::path::Path, needle: &str, tail_bytes: u64) -> bool {
|
|
2814
|
+
rollout_tail_contains_result(path, needle, tail_bytes).unwrap_or(false)
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
fn rollout_tail_contains_result(
|
|
2818
|
+
path: &std::path::Path,
|
|
2819
|
+
needle: &str,
|
|
2820
|
+
tail_bytes: u64,
|
|
2821
|
+
) -> std::io::Result<bool> {
|
|
2747
2822
|
use std::io::{Read, Seek, SeekFrom};
|
|
2748
|
-
let
|
|
2749
|
-
|
|
2750
|
-
};
|
|
2751
|
-
let Ok(metadata) = file.metadata() else {
|
|
2752
|
-
return false;
|
|
2753
|
-
};
|
|
2823
|
+
let mut file = std::fs::File::open(path)?;
|
|
2824
|
+
let metadata = file.metadata()?;
|
|
2754
2825
|
let len = metadata.len();
|
|
2755
2826
|
let start = len.saturating_sub(tail_bytes);
|
|
2756
|
-
|
|
2757
|
-
return false;
|
|
2758
|
-
}
|
|
2827
|
+
file.seek(SeekFrom::Start(start))?;
|
|
2759
2828
|
let mut buf = Vec::with_capacity(tail_bytes.min(len) as usize);
|
|
2760
|
-
|
|
2761
|
-
return false;
|
|
2762
|
-
}
|
|
2829
|
+
file.take(tail_bytes).read_to_end(&mut buf)?;
|
|
2763
2830
|
let haystack = String::from_utf8_lossy(&buf);
|
|
2764
|
-
haystack.contains(needle)
|
|
2831
|
+
Ok(haystack.contains(needle))
|
|
2765
2832
|
}
|