@team-agent/installer 0.3.2 → 0.3.4
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 +34 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +196 -19
- package/crates/team-agent/src/cli/diagnose.rs +145 -11
- package/crates/team-agent/src/cli/emit.rs +287 -53
- package/crates/team-agent/src/cli/leader.rs +37 -8
- package/crates/team-agent/src/cli/mod.rs +807 -316
- package/crates/team-agent/src/cli/status_port.rs +25 -2
- package/crates/team-agent/src/cli/tests/divergence.rs +1 -2
- package/crates/team-agent/src/cli/tests/lane_c.rs +23 -13
- package/crates/team-agent/src/cli/tests/main_preserved.rs +2 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +57 -3
- package/crates/team-agent/src/cli/types.rs +17 -0
- package/crates/team-agent/src/compiler/tests.rs +2 -2
- package/crates/team-agent/src/compiler.rs +16 -6
- package/crates/team-agent/src/coordinator/health.rs +89 -20
- package/crates/team-agent/src/coordinator/mod.rs +4 -0
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +500 -0
- package/crates/team-agent/src/coordinator/runtime_observation.rs +58 -0
- package/crates/team-agent/src/coordinator/tests/watch.rs +4 -2
- package/crates/team-agent/src/coordinator/tick.rs +222 -69
- package/crates/team-agent/src/coordinator/types.rs +15 -3
- package/crates/team-agent/src/db/schema.rs +37 -2
- package/crates/team-agent/src/diagnose/comms.rs +226 -0
- package/crates/team-agent/src/diagnose/mod.rs +45 -0
- package/crates/team-agent/src/diagnose/orphans.rs +658 -0
- package/crates/team-agent/src/fake_worker.rs +146 -3
- package/crates/team-agent/src/leader/start.rs +121 -23
- package/crates/team-agent/src/leader/types.rs +44 -1
- package/crates/team-agent/src/lib.rs +3 -0
- package/crates/team-agent/src/lifecycle/display.rs +648 -50
- package/crates/team-agent/src/lifecycle/launch.rs +1048 -264
- package/crates/team-agent/src/lifecycle/mod.rs +3 -0
- package/crates/team-agent/src/lifecycle/profile_launch.rs +810 -0
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +522 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +113 -26
- package/crates/team-agent/src/lifecycle/restart/common.rs +189 -102
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +465 -25
- package/crates/team-agent/src/lifecycle/restart/remove.rs +22 -6
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +19 -0
- package/crates/team-agent/src/lifecycle/restart.rs +4 -1
- package/crates/team-agent/src/lifecycle/tests/core.rs +4 -4
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +5 -5
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +39 -9
- package/crates/team-agent/src/lifecycle/types.rs +23 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +326 -0
- package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +341 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +10 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +158 -0
- package/crates/team-agent/src/mcp_server/mod.rs +3 -74
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -1
- package/crates/team-agent/src/mcp_server/tests/send.rs +6 -5
- package/crates/team-agent/src/mcp_server/tools.rs +312 -111
- package/crates/team-agent/src/mcp_server/types.rs +6 -4
- package/crates/team-agent/src/mcp_server/wire.rs +19 -7
- package/crates/team-agent/src/message_store.rs +21 -4
- package/crates/team-agent/src/messaging/delivery.rs +87 -37
- package/crates/team-agent/src/messaging/mod.rs +9 -6
- package/crates/team-agent/src/messaging/results.rs +153 -16
- package/crates/team-agent/src/messaging/selftest.rs +199 -12
- package/crates/team-agent/src/messaging/send.rs +35 -3
- package/crates/team-agent/src/messaging/tests/runtime.rs +19 -4
- package/crates/team-agent/src/messaging/types.rs +11 -3
- package/crates/team-agent/src/os_probe.rs +119 -0
- package/crates/team-agent/src/packaging/migrate.rs +10 -2
- package/crates/team-agent/src/packaging/tests.rs +23 -0
- package/crates/team-agent/src/provider/adapter.rs +483 -67
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +1 -7
- package/crates/team-agent/src/provider/classify.rs +51 -4
- package/crates/team-agent/src/provider/startup_prompt.rs +94 -0
- package/crates/team-agent/src/provider/types.rs +47 -0
- package/crates/team-agent/src/session_capture.rs +616 -0
- package/crates/team-agent/src/state/persist.rs +57 -0
- package/crates/team-agent/src/state/projection.rs +32 -23
- package/crates/team-agent/src/state/selector.rs +5 -2
- package/crates/team-agent/src/tmux_backend.rs +151 -60
- package/crates/team-agent/src/transport/test_support.rs +9 -0
- package/crates/team-agent/src/transport/tests/wire.rs +4 -0
- package/crates/team-agent/src/transport.rs +13 -2
- package/package.json +4 -4
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
|
|
32
32
|
use std::path::Path;
|
|
33
33
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
34
|
-
use std::time::{SystemTime, UNIX_EPOCH};
|
|
34
|
+
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|
35
35
|
|
|
36
36
|
use rusqlite::{params, OptionalExtension};
|
|
37
37
|
use thiserror::Error;
|
|
@@ -100,8 +100,25 @@ impl MessageStore {
|
|
|
100
100
|
let runtime_dir = workspace.join(".team").join("runtime");
|
|
101
101
|
std::fs::create_dir_all(&runtime_dir)?;
|
|
102
102
|
let path = runtime_dir.join("team.db");
|
|
103
|
+
let existed = path.exists();
|
|
103
104
|
let conn = crate::db::schema::open_db(&path)?;
|
|
104
|
-
|
|
105
|
+
if existed {
|
|
106
|
+
conn.busy_timeout(Duration::from_millis(5))?;
|
|
107
|
+
let version = conn.query_row("pragma user_version", [], |row| row.get::<_, i64>(0));
|
|
108
|
+
conn.busy_timeout(Duration::from_millis(30_000))?;
|
|
109
|
+
match version {
|
|
110
|
+
Ok(version) if version == crate::db::schema::SCHEMA_VERSION => {}
|
|
111
|
+
Ok(_) => crate::db::schema::initialize_schema(&conn, Some(&path))?,
|
|
112
|
+
Err(rusqlite::Error::SqliteFailure(err, _))
|
|
113
|
+
if matches!(
|
|
114
|
+
err.code,
|
|
115
|
+
rusqlite::ErrorCode::DatabaseBusy | rusqlite::ErrorCode::DatabaseLocked
|
|
116
|
+
) => {}
|
|
117
|
+
Err(error) => return Err(error.into()),
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
crate::db::schema::initialize_schema(&conn, Some(&path))?;
|
|
121
|
+
}
|
|
105
122
|
Ok(Self { path })
|
|
106
123
|
}
|
|
107
124
|
|
|
@@ -220,12 +237,12 @@ impl MessageStore {
|
|
|
220
237
|
end,
|
|
221
238
|
updated_at = ?3,
|
|
222
239
|
delivered_at = case
|
|
223
|
-
when ?2 in ('injected', 'visible', 'submitted', '
|
|
240
|
+
when ?2 in ('injected', 'visible', 'submitted', 'delivered')
|
|
224
241
|
then ?3
|
|
225
242
|
else delivered_at
|
|
226
243
|
end,
|
|
227
244
|
acknowledged_at = case when ?2 = 'acknowledged' then ?3 else acknowledged_at end,
|
|
228
|
-
error = coalesce(?4, error)
|
|
245
|
+
error = case when ?2 = 'delivered' then null else coalesce(?4, error) end
|
|
229
246
|
where message_id = ?1",
|
|
230
247
|
params![message_id, status, now, error],
|
|
231
248
|
)?;
|
|
@@ -115,18 +115,6 @@ pub fn deliver_pending_message(
|
|
|
115
115
|
});
|
|
116
116
|
}
|
|
117
117
|
let message = message_for_delivery(store, message_id)?;
|
|
118
|
-
if !store.claim_for_delivery(message_id)? {
|
|
119
|
-
return Ok(DeliveryOutcome {
|
|
120
|
-
ok: false,
|
|
121
|
-
status: DeliveryStatus::Refused,
|
|
122
|
-
message_status: MessageStatusShadow("target_resolved".to_string()),
|
|
123
|
-
message_id: Some(message_id.to_string()),
|
|
124
|
-
verification: None,
|
|
125
|
-
stage: None,
|
|
126
|
-
reason: Some(DeliveryRefusal::MessageAlreadyClaimed),
|
|
127
|
-
channel: None,
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
118
|
let Some(message) = message else {
|
|
131
119
|
return Ok(DeliveryOutcome {
|
|
132
120
|
ok: false,
|
|
@@ -154,6 +142,18 @@ pub fn deliver_pending_message(
|
|
|
154
142
|
}
|
|
155
143
|
_ => state,
|
|
156
144
|
};
|
|
145
|
+
if !store.claim_for_delivery(message_id)? && message.status != "target_resolved" {
|
|
146
|
+
return Ok(DeliveryOutcome {
|
|
147
|
+
ok: false,
|
|
148
|
+
status: DeliveryStatus::Refused,
|
|
149
|
+
message_status: MessageStatusShadow("target_resolved".to_string()),
|
|
150
|
+
message_id: Some(message_id.to_string()),
|
|
151
|
+
verification: None,
|
|
152
|
+
stage: None,
|
|
153
|
+
reason: Some(DeliveryRefusal::MessageAlreadyClaimed),
|
|
154
|
+
channel: None,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
157
|
if message.recipient == "leader" && leader_receiver_has_noncanonical_tmux_socket(state) {
|
|
158
158
|
store.mark(message_id, "failed", Some("leader_not_attached"))?;
|
|
159
159
|
event_log.write(
|
|
@@ -244,7 +244,6 @@ pub fn deliver_pending_message(
|
|
|
244
244
|
&message.content,
|
|
245
245
|
message_id,
|
|
246
246
|
);
|
|
247
|
-
let rendered_len = rendered.len();
|
|
248
247
|
let inject_report = match transport.inject(
|
|
249
248
|
&target,
|
|
250
249
|
&InjectPayload::Text(rendered),
|
|
@@ -282,7 +281,7 @@ pub fn deliver_pending_message(
|
|
|
282
281
|
return Err(error.into());
|
|
283
282
|
}
|
|
284
283
|
};
|
|
285
|
-
if !inject_submit_verified(&inject_report
|
|
284
|
+
if !inject_submit_verified(&inject_report) {
|
|
286
285
|
let reason = format!(
|
|
287
286
|
"submit_unverified:{}",
|
|
288
287
|
submit_verification_wire(inject_report.submit_verification)
|
|
@@ -340,19 +339,13 @@ pub fn deliver_pending_message(
|
|
|
340
339
|
Ok(outcome)
|
|
341
340
|
}
|
|
342
341
|
|
|
343
|
-
fn inject_submit_verified(
|
|
344
|
-
report: &InjectReport,
|
|
345
|
-
payload_len: usize,
|
|
346
|
-
sender: &str,
|
|
347
|
-
recipient: &str,
|
|
348
|
-
) -> bool {
|
|
342
|
+
fn inject_submit_verified(report: &InjectReport) -> bool {
|
|
349
343
|
match report.submit_verification {
|
|
350
344
|
SubmitVerification::SendKeysFailed => false,
|
|
345
|
+
SubmitVerification::PastedContentPromptStillPresentAfterSubmit => false,
|
|
351
346
|
SubmitVerification::PastedContentPromptAbsentAfterSubmit => true,
|
|
352
347
|
SubmitVerification::KeySentAfterVisibleToken { .. } => true,
|
|
353
|
-
SubmitVerification::EnterSentWithoutPlaceholderCheck =>
|
|
354
|
-
recipient == "leader" || matches!(sender, "leader" | "Leader") || payload_len < 80
|
|
355
|
-
}
|
|
348
|
+
SubmitVerification::EnterSentWithoutPlaceholderCheck => true,
|
|
356
349
|
}
|
|
357
350
|
}
|
|
358
351
|
|
|
@@ -450,13 +443,60 @@ fn delivery_transport_for_recipient<'a>(
|
|
|
450
443
|
if recipient != "leader" {
|
|
451
444
|
return DeliveryTransport::Borrowed(product_transport);
|
|
452
445
|
}
|
|
446
|
+
let pane_id = leader_receiver_pane_id(state);
|
|
453
447
|
let Some(socket) = leader_receiver_tmux_socket(state) else {
|
|
448
|
+
if let Some(pane_id) = pane_id {
|
|
449
|
+
let in_workspace = product_transport
|
|
450
|
+
.list_targets()
|
|
451
|
+
.unwrap_or_default()
|
|
452
|
+
.iter()
|
|
453
|
+
.any(|target| target.pane_id.as_str() == pane_id);
|
|
454
|
+
if !in_workspace {
|
|
455
|
+
let default_backend = crate::tmux_backend::TmuxBackend::new();
|
|
456
|
+
if default_backend
|
|
457
|
+
.list_targets()
|
|
458
|
+
.unwrap_or_default()
|
|
459
|
+
.iter()
|
|
460
|
+
.any(|target| target.pane_id.as_str() == pane_id)
|
|
461
|
+
{
|
|
462
|
+
return DeliveryTransport::Owned(default_backend);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
454
466
|
return DeliveryTransport::Borrowed(product_transport);
|
|
455
467
|
};
|
|
456
468
|
if socket == crate::tmux_backend::socket_name_for_workspace(workspace) {
|
|
457
469
|
DeliveryTransport::Borrowed(product_transport)
|
|
458
470
|
} else {
|
|
459
|
-
|
|
471
|
+
let endpoint_backend = crate::tmux_backend::TmuxBackend::for_tmux_endpoint(socket);
|
|
472
|
+
if let Some(pane_id) = pane_id {
|
|
473
|
+
if endpoint_backend
|
|
474
|
+
.list_targets()
|
|
475
|
+
.unwrap_or_default()
|
|
476
|
+
.iter()
|
|
477
|
+
.any(|target| target.pane_id.as_str() == pane_id)
|
|
478
|
+
{
|
|
479
|
+
return DeliveryTransport::Owned(endpoint_backend);
|
|
480
|
+
}
|
|
481
|
+
if product_transport
|
|
482
|
+
.list_targets()
|
|
483
|
+
.unwrap_or_default()
|
|
484
|
+
.iter()
|
|
485
|
+
.any(|target| target.pane_id.as_str() == pane_id)
|
|
486
|
+
{
|
|
487
|
+
return DeliveryTransport::Borrowed(product_transport);
|
|
488
|
+
}
|
|
489
|
+
let default_backend = crate::tmux_backend::TmuxBackend::new();
|
|
490
|
+
if default_backend
|
|
491
|
+
.list_targets()
|
|
492
|
+
.unwrap_or_default()
|
|
493
|
+
.iter()
|
|
494
|
+
.any(|target| target.pane_id.as_str() == pane_id)
|
|
495
|
+
{
|
|
496
|
+
return DeliveryTransport::Owned(default_backend);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
DeliveryTransport::Owned(endpoint_backend)
|
|
460
500
|
}
|
|
461
501
|
}
|
|
462
502
|
|
|
@@ -466,7 +506,7 @@ fn leader_receiver_pane_id_in_state(state: &serde_json::Value) -> Option<&str> {
|
|
|
466
506
|
.get(key)
|
|
467
507
|
.and_then(|r| r.get("pane_id"))
|
|
468
508
|
.and_then(serde_json::Value::as_str)
|
|
469
|
-
.filter(|s| !s.is_empty())
|
|
509
|
+
.filter(|s| !s.is_empty() && *s != "__team_agent_unbound__")
|
|
470
510
|
})
|
|
471
511
|
}
|
|
472
512
|
|
|
@@ -528,7 +568,7 @@ pub fn deliver_pending_messages(
|
|
|
528
568
|
let conn = crate::db::schema::open_db(store.db_path())?;
|
|
529
569
|
let mut stmt = conn.prepare(
|
|
530
570
|
"select message_id from messages
|
|
531
|
-
where status in ('pending', 'accepted')
|
|
571
|
+
where status in ('pending', 'accepted', 'target_resolved')
|
|
532
572
|
order by created_at, message_id",
|
|
533
573
|
)?;
|
|
534
574
|
let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
|
|
@@ -577,6 +617,7 @@ struct PendingMessage {
|
|
|
577
617
|
content: String,
|
|
578
618
|
task_id: Option<String>,
|
|
579
619
|
owner_team_id: Option<String>,
|
|
620
|
+
status: String,
|
|
580
621
|
}
|
|
581
622
|
|
|
582
623
|
fn message_for_delivery(
|
|
@@ -586,7 +627,7 @@ fn message_for_delivery(
|
|
|
586
627
|
let conn = crate::db::schema::open_db(store.db_path())?;
|
|
587
628
|
let message = conn
|
|
588
629
|
.query_row(
|
|
589
|
-
"select sender, recipient, content, task_id, owner_team_id from messages where message_id = ?1",
|
|
630
|
+
"select sender, recipient, content, task_id, owner_team_id, status from messages where message_id = ?1",
|
|
590
631
|
params![message_id],
|
|
591
632
|
|row| {
|
|
592
633
|
Ok(PendingMessage {
|
|
@@ -595,6 +636,7 @@ fn message_for_delivery(
|
|
|
595
636
|
content: row.get::<_, String>(2)?,
|
|
596
637
|
task_id: row.get::<_, Option<String>>(3)?,
|
|
597
638
|
owner_team_id: row.get::<_, Option<String>>(4)?,
|
|
639
|
+
status: row.get::<_, String>(5)?,
|
|
598
640
|
})
|
|
599
641
|
},
|
|
600
642
|
)
|
|
@@ -603,10 +645,11 @@ fn message_for_delivery(
|
|
|
603
645
|
}
|
|
604
646
|
|
|
605
647
|
/// Pre-inject gate (Contract B): peek the recipient pane and answer "is there an
|
|
606
|
-
/// actionable
|
|
607
|
-
/// the SHARED provider/startup_prompt
|
|
608
|
-
/// API calls. Returns `false` if capture fails so
|
|
609
|
-
/// without the trust-menu shape) keep flowing through
|
|
648
|
+
/// actionable provider startup prompt right now (trust menu or update prompt)" using
|
|
649
|
+
/// the SHARED provider/startup_prompt recognizers — no second classifier, no provider
|
|
650
|
+
/// API calls. Returns `false` if capture fails so providers without a startup
|
|
651
|
+
/// recognizer (or any pane without the trust-menu shape) keep flowing through
|
|
652
|
+
/// normal delivery.
|
|
610
653
|
fn recipient_pane_has_actionable_startup_prompt(
|
|
611
654
|
transport: &dyn Transport,
|
|
612
655
|
state: &serde_json::Value,
|
|
@@ -620,7 +663,7 @@ fn recipient_pane_has_actionable_startup_prompt(
|
|
|
620
663
|
let provider = agent
|
|
621
664
|
.and_then(|agent| agent.get("provider"))
|
|
622
665
|
.and_then(serde_json::Value::as_str);
|
|
623
|
-
if !matches!(provider, Some("codex")) {
|
|
666
|
+
if !matches!(provider, Some("codex" | "claude" | "claude_code")) {
|
|
624
667
|
return false;
|
|
625
668
|
}
|
|
626
669
|
// step2-retry/scrollback root-cause (rt binary 6c9c6c1c): once the agent's
|
|
@@ -643,11 +686,18 @@ fn recipient_pane_has_actionable_startup_prompt(
|
|
|
643
686
|
Ok(Ok(captured)) => captured.text,
|
|
644
687
|
_ => return false,
|
|
645
688
|
};
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
689
|
+
match provider {
|
|
690
|
+
Some("codex") => matches!(
|
|
691
|
+
crate::provider::classify_codex_startup_screen(&captured),
|
|
692
|
+
crate::provider::StartupScreenDecision::AnswerWorkspaceTrust
|
|
693
|
+
| crate::provider::StartupScreenDecision::SkipUpdatePrompt
|
|
694
|
+
),
|
|
695
|
+
Some("claude" | "claude_code") => matches!(
|
|
696
|
+
crate::provider::classify_claude_startup_screen(&captured),
|
|
697
|
+
crate::provider::StartupScreenDecision::AnswerWorkspaceTrust
|
|
698
|
+
),
|
|
699
|
+
_ => false,
|
|
700
|
+
}
|
|
651
701
|
}
|
|
652
702
|
|
|
653
703
|
fn recipient_is_busy(state: &serde_json::Value, recipient: &str) -> bool {
|
|
@@ -86,18 +86,21 @@ pub use leader_receiver::{
|
|
|
86
86
|
claim_leader_receiver, mirror_peer_message_to_leader, send_to_leader_receiver,
|
|
87
87
|
};
|
|
88
88
|
pub use peers::allow_peer_talk;
|
|
89
|
-
pub use results::{
|
|
89
|
+
pub use results::{
|
|
90
|
+
collect, collect_for_team, collect_results_and_notify_watchers, report_result,
|
|
91
|
+
report_result_for_owner_team,
|
|
92
|
+
};
|
|
90
93
|
pub use scheduler::{detect_stuck_agents, fire_due_scheduled_events, stuck_cancel, stuck_list};
|
|
91
94
|
pub use selftest::{evaluate_idle_behavior, run_comms_selftest, CommsSelftestDriver};
|
|
92
95
|
pub use send::{apply_worker_sender_bypass, send_message, session_drift_refusal, MessageTarget, SendOptions};
|
|
93
96
|
pub use trust::{attempt_trust_auto_answer, TrustAnswerOutcome};
|
|
94
97
|
pub use types::{
|
|
95
98
|
ActivityStatus, AgentActivity, AlertSnapshot, AlertSuppression, AlertType, CheckEvidence,
|
|
96
|
-
CheckKind, CheckStatus, DeliveryOutcome, DeliveryRefusal, DeliveryStage,
|
|
97
|
-
IdleEvaluation, LeaderNotificationKey, LeaderReceiver, PaneWidthQuery,
|
|
98
|
-
ReceiverMode, ScheduledKind, SelftestCheck, SelftestReport, SendEventPayload,
|
|
99
|
-
WatcherNotice, RESULT_DELIVERY_MAX_ATTEMPTS, SEND_RETRY_MAX_ATTEMPTS,
|
|
100
|
-
TRUST_RETRY_MAX_ATTEMPTS,
|
|
99
|
+
CheckKind, CheckStatus, ContractSuiteCheck, DeliveryOutcome, DeliveryRefusal, DeliveryStage,
|
|
100
|
+
DeliveryStatus, IdleEvaluation, LeaderNotificationKey, LeaderReceiver, PaneWidthQuery,
|
|
101
|
+
ProviderSdkCalls, ReceiverMode, ScheduledKind, SelftestCheck, SelftestReport, SendEventPayload,
|
|
102
|
+
TrustRetryPayload, WatcherNotice, RESULT_DELIVERY_MAX_ATTEMPTS, SEND_RETRY_MAX_ATTEMPTS,
|
|
103
|
+
TRUST_RETRY_BACKOFF_SECONDS, TRUST_RETRY_MAX_ATTEMPTS,
|
|
101
104
|
};
|
|
102
105
|
pub use watchers::{
|
|
103
106
|
delivered_result_message, format_result_watcher_notification, notify_result_watchers,
|
|
@@ -6,6 +6,7 @@ use rusqlite::params;
|
|
|
6
6
|
|
|
7
7
|
use crate::event_log::EventLog;
|
|
8
8
|
use crate::message_store::MessageStore;
|
|
9
|
+
use crate::transport::{InjectPayload, Key, PaneId, Target, Transport};
|
|
9
10
|
|
|
10
11
|
use super::helpers::{next_result_id, required_str, validate_result_envelope};
|
|
11
12
|
use super::types::SEND_RETRY_MAX_ATTEMPTS;
|
|
@@ -98,11 +99,13 @@ fn collect_scoped(
|
|
|
98
99
|
let mut collected = Vec::new();
|
|
99
100
|
let mut collected_results = Vec::new();
|
|
100
101
|
let mut invalid_results = Vec::new();
|
|
102
|
+
let mut fatal_invalid_results = 0usize;
|
|
101
103
|
let mut state_dirty = false;
|
|
102
104
|
for row in rows {
|
|
103
105
|
let envelope: serde_json::Value = match serde_json::from_str(&row.envelope) {
|
|
104
106
|
Ok(envelope) => envelope,
|
|
105
107
|
Err(error) => {
|
|
108
|
+
fatal_invalid_results = fatal_invalid_results.saturating_add(1);
|
|
106
109
|
record_invalid_result(
|
|
107
110
|
&conn,
|
|
108
111
|
&mut invalid_results,
|
|
@@ -114,6 +117,7 @@ fn collect_scoped(
|
|
|
114
117
|
}
|
|
115
118
|
};
|
|
116
119
|
if let Err(error) = validate_result_envelope(&envelope) {
|
|
120
|
+
fatal_invalid_results = fatal_invalid_results.saturating_add(1);
|
|
117
121
|
record_invalid_result(
|
|
118
122
|
&conn,
|
|
119
123
|
&mut invalid_results,
|
|
@@ -128,6 +132,9 @@ fn collect_scoped(
|
|
|
128
132
|
} else if is_message_scoped_result(&conn, &row.task_id, &row.agent_id, owner_team_id)? {
|
|
129
133
|
"message"
|
|
130
134
|
} else {
|
|
135
|
+
if result_file.is_some() || row.task_id != "manual" {
|
|
136
|
+
fatal_invalid_results = fatal_invalid_results.saturating_add(1);
|
|
137
|
+
}
|
|
131
138
|
record_invalid_result(
|
|
132
139
|
&conn,
|
|
133
140
|
&mut invalid_results,
|
|
@@ -189,7 +196,7 @@ fn collect_scoped(
|
|
|
189
196
|
}
|
|
190
197
|
let counts = result_counts(&conn, owner_team_id)?;
|
|
191
198
|
Ok(serde_json::json!({
|
|
192
|
-
"ok":
|
|
199
|
+
"ok": fatal_invalid_results == 0,
|
|
193
200
|
"collected": collected,
|
|
194
201
|
"collected_results": collected_results,
|
|
195
202
|
"delivered_messages": [],
|
|
@@ -478,6 +485,14 @@ fn count_results(
|
|
|
478
485
|
pub fn report_result(
|
|
479
486
|
workspace: &Path,
|
|
480
487
|
envelope: &serde_json::Value,
|
|
488
|
+
) -> Result<serde_json::Value, MessagingError> {
|
|
489
|
+
report_result_for_owner_team(workspace, envelope, None)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
pub fn report_result_for_owner_team(
|
|
493
|
+
workspace: &Path,
|
|
494
|
+
envelope: &serde_json::Value,
|
|
495
|
+
explicit_owner_team: Option<&str>,
|
|
481
496
|
) -> Result<serde_json::Value, MessagingError> {
|
|
482
497
|
validate_result_envelope(envelope)?;
|
|
483
498
|
let store = MessageStore::open(workspace)?;
|
|
@@ -497,7 +512,10 @@ pub fn report_result(
|
|
|
497
512
|
let conn = crate::db::schema::open_db(store.db_path())?;
|
|
498
513
|
let state_for_owner = crate::state::persist::load_runtime_state(workspace)
|
|
499
514
|
.unwrap_or(serde_json::json!({}));
|
|
500
|
-
let owner_team =
|
|
515
|
+
let owner_team = explicit_owner_team
|
|
516
|
+
.filter(|team| !team.is_empty())
|
|
517
|
+
.map(str::to_string)
|
|
518
|
+
.unwrap_or_else(|| super::leader_receiver::active_team_key(workspace, &state_for_owner));
|
|
501
519
|
let inserted = insert_result_if_absent(
|
|
502
520
|
&conn,
|
|
503
521
|
&result_id,
|
|
@@ -513,7 +531,7 @@ pub fn report_result(
|
|
|
513
531
|
"mcp.report_result_duplicate_ignored",
|
|
514
532
|
serde_json::json!({
|
|
515
533
|
"notification_status": "duplicate_ignored",
|
|
516
|
-
"owner_team_id":
|
|
534
|
+
"owner_team_id": owner_team,
|
|
517
535
|
"result_id": result_id,
|
|
518
536
|
}),
|
|
519
537
|
)?;
|
|
@@ -545,7 +563,7 @@ pub fn report_result(
|
|
|
545
563
|
// legacy path was MUST-8 / I-3 violating (the deferred notification status was returned
|
|
546
564
|
// to the caller as "success" while leader actually never saw the result text).
|
|
547
565
|
let content = format_report_result_notification(&result_id, task_id, agent_id, status, envelope);
|
|
548
|
-
let state =
|
|
566
|
+
let state = report_owner_state(&state_for_owner, &owner_team);
|
|
549
567
|
let event_log = EventLog::new(workspace);
|
|
550
568
|
let mut outcome = super::leader_receiver::send_to_leader_receiver(
|
|
551
569
|
workspace,
|
|
@@ -558,19 +576,72 @@ pub fn report_result(
|
|
|
558
576
|
Some(&result_id),
|
|
559
577
|
&event_log,
|
|
560
578
|
)?;
|
|
561
|
-
if
|
|
562
|
-
if let Some(message_id) = outcome.message_id.clone() {
|
|
579
|
+
if let Some(message_id) = outcome.message_id.clone() {
|
|
563
580
|
let store = MessageStore::open(workspace)?;
|
|
564
581
|
let transport = crate::tmux_backend::TmuxBackend::for_workspace(workspace);
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
&message_id,
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
582
|
+
let delivery_state_raw = crate::state::persist::load_runtime_state(workspace)
|
|
583
|
+
.unwrap_or_else(|_| state_for_owner.clone());
|
|
584
|
+
let delivery_state = report_owner_state(&delivery_state_raw, &owner_team);
|
|
585
|
+
for attempt in 0..3 {
|
|
586
|
+
let _ = store.mark(&message_id, "accepted", None);
|
|
587
|
+
outcome = super::delivery::deliver_pending_message(
|
|
588
|
+
workspace,
|
|
589
|
+
&store,
|
|
590
|
+
&transport,
|
|
591
|
+
&message_id,
|
|
592
|
+
&event_log,
|
|
593
|
+
&delivery_state,
|
|
594
|
+
)?;
|
|
595
|
+
if outcome.ok {
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
let delivered = super::delivery::deliver_pending_messages(
|
|
599
|
+
workspace,
|
|
600
|
+
&delivery_state,
|
|
601
|
+
&transport,
|
|
602
|
+
&event_log,
|
|
603
|
+
)?;
|
|
604
|
+
if delivered.iter().any(|delivered_id| delivered_id == &message_id) {
|
|
605
|
+
outcome = crate::messaging::DeliveryOutcome {
|
|
606
|
+
ok: true,
|
|
607
|
+
status: crate::messaging::DeliveryStatus::Delivered,
|
|
608
|
+
message_status: super::helpers::MessageStatusShadow("delivered".to_string()),
|
|
609
|
+
message_id: Some(message_id.clone()),
|
|
610
|
+
verification: None,
|
|
611
|
+
stage: None,
|
|
612
|
+
reason: None,
|
|
613
|
+
channel: Some("leader_receiver".to_string()),
|
|
614
|
+
};
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
if attempt < 2 {
|
|
618
|
+
std::thread::sleep(std::time::Duration::from_millis(50));
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
match inject_leader_notification_direct(workspace, &delivery_state, &content, &message_id) {
|
|
622
|
+
Ok(()) => {
|
|
623
|
+
store.mark(&message_id, "delivered", None)?;
|
|
624
|
+
outcome = crate::messaging::DeliveryOutcome {
|
|
625
|
+
ok: true,
|
|
626
|
+
status: crate::messaging::DeliveryStatus::Delivered,
|
|
627
|
+
message_status: super::helpers::MessageStatusShadow("delivered".to_string()),
|
|
628
|
+
message_id: Some(message_id),
|
|
629
|
+
verification: None,
|
|
630
|
+
stage: None,
|
|
631
|
+
reason: None,
|
|
632
|
+
channel: Some("leader_receiver".to_string()),
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
Err(reason) => {
|
|
636
|
+
event_log.write(
|
|
637
|
+
"leader_receiver.direct_inject_skipped",
|
|
638
|
+
serde_json::json!({
|
|
639
|
+
"message_id": message_id,
|
|
640
|
+
"reason": reason,
|
|
641
|
+
}),
|
|
642
|
+
)?;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
574
645
|
}
|
|
575
646
|
let leader_notified = outcome.ok;
|
|
576
647
|
let notification_status_wire = if outcome.ok {
|
|
@@ -590,7 +661,7 @@ pub fn report_result(
|
|
|
590
661
|
"notification_channel": channel,
|
|
591
662
|
"notification_message_id": outcome.message_id,
|
|
592
663
|
"notification_status": notification_status_wire,
|
|
593
|
-
"owner_team_id":
|
|
664
|
+
"owner_team_id": owner_team,
|
|
594
665
|
"result_id": result_id,
|
|
595
666
|
}),
|
|
596
667
|
)?;
|
|
@@ -624,6 +695,72 @@ pub fn report_result(
|
|
|
624
695
|
Ok(serde_json::Value::Object(out))
|
|
625
696
|
}
|
|
626
697
|
|
|
698
|
+
fn report_owner_state(state: &serde_json::Value, owner_team: &str) -> serde_json::Value {
|
|
699
|
+
let mut state = match crate::state::projection::resolve_owner_team_id(state, owner_team)
|
|
700
|
+
.canonical_key()
|
|
701
|
+
{
|
|
702
|
+
Some(team) => crate::state::projection::project_top_level_view(state, team),
|
|
703
|
+
None => state.clone(),
|
|
704
|
+
};
|
|
705
|
+
if let Some(obj) = state.as_object_mut() {
|
|
706
|
+
obj.insert(
|
|
707
|
+
"active_team_key".to_string(),
|
|
708
|
+
serde_json::Value::String(owner_team.to_string()),
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
state
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
fn inject_leader_notification_direct(
|
|
715
|
+
workspace: &Path,
|
|
716
|
+
state: &serde_json::Value,
|
|
717
|
+
content: &str,
|
|
718
|
+
message_id: &str,
|
|
719
|
+
) -> Result<(), String> {
|
|
720
|
+
let Some(pane_id) = state
|
|
721
|
+
.get("leader_receiver")
|
|
722
|
+
.or_else(|| state.get("team_owner"))
|
|
723
|
+
.and_then(|receiver| receiver.get("pane_id"))
|
|
724
|
+
.and_then(serde_json::Value::as_str)
|
|
725
|
+
.filter(|pane| !pane.is_empty() && *pane != "__team_agent_unbound__")
|
|
726
|
+
else {
|
|
727
|
+
return Err("leader_direct_inject_failed:no_bound_pane".to_string());
|
|
728
|
+
};
|
|
729
|
+
let rendered = format!(
|
|
730
|
+
"Team Agent message from leader_receiver:\n\n{content}\n\n[team-agent-token:{message_id}]"
|
|
731
|
+
);
|
|
732
|
+
let target = Target::Pane(PaneId::new(pane_id));
|
|
733
|
+
if let Some(socket) = state
|
|
734
|
+
.get("leader_receiver")
|
|
735
|
+
.and_then(|receiver| receiver.get("tmux_socket"))
|
|
736
|
+
.and_then(serde_json::Value::as_str)
|
|
737
|
+
.filter(|socket| !socket.is_empty())
|
|
738
|
+
{
|
|
739
|
+
let backend = crate::tmux_backend::TmuxBackend::for_tmux_endpoint(socket);
|
|
740
|
+
if backend
|
|
741
|
+
.inject(&target, &InjectPayload::Text(rendered.clone()), Key::Enter, true)
|
|
742
|
+
.is_ok()
|
|
743
|
+
{
|
|
744
|
+
return Ok(());
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
let workspace_backend = crate::tmux_backend::TmuxBackend::for_workspace(workspace);
|
|
748
|
+
if workspace_backend
|
|
749
|
+
.inject(&target, &InjectPayload::Text(rendered.clone()), Key::Enter, true)
|
|
750
|
+
.is_ok()
|
|
751
|
+
{
|
|
752
|
+
return Ok(());
|
|
753
|
+
}
|
|
754
|
+
let default_backend = crate::tmux_backend::TmuxBackend::new();
|
|
755
|
+
if default_backend
|
|
756
|
+
.inject(&target, &InjectPayload::Text(rendered), Key::Enter, true)
|
|
757
|
+
.is_ok()
|
|
758
|
+
{
|
|
759
|
+
return Ok(());
|
|
760
|
+
}
|
|
761
|
+
Err(format!("leader_direct_inject_failed:pane={pane_id}"))
|
|
762
|
+
}
|
|
763
|
+
|
|
627
764
|
fn insert_result_if_absent(
|
|
628
765
|
conn: &rusqlite::Connection,
|
|
629
766
|
result_id: &str,
|