@team-agent/installer 0.3.1 → 0.3.3
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 +234 -26
- package/crates/team-agent/src/cli/diagnose.rs +144 -10
- package/crates/team-agent/src/cli/emit.rs +289 -54
- package/crates/team-agent/src/cli/leader.rs +37 -8
- package/crates/team-agent/src/cli/mod.rs +1281 -196
- package/crates/team-agent/src/cli/status_port.rs +195 -46
- 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 +59 -3
- package/crates/team-agent/src/cli/types.rs +18 -0
- package/crates/team-agent/src/compiler.rs +15 -5
- package/crates/team-agent/src/coordinator/health.rs +95 -17
- 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/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 +645 -47
- package/crates/team-agent/src/lifecycle/launch.rs +1061 -146
- package/crates/team-agent/src/lifecycle/mod.rs +2 -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 +99 -23
- package/crates/team-agent/src/lifecycle/restart/common.rs +183 -24
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +498 -22
- package/crates/team-agent/src/lifecycle/restart/remove.rs +27 -7
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +19 -0
- package/crates/team-agent/src/lifecycle/restart.rs +24 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +5 -5
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +37 -7
- package/crates/team-agent/src/lifecycle/types.rs +19 -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 +470 -59
- package/crates/team-agent/src/messaging/mod.rs +9 -6
- package/crates/team-agent/src/messaging/results.rs +353 -63
- 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 +564 -63
- 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/helpers.rs +10 -1
- 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 +170 -1
- package/crates/team-agent/src/state/projection.rs +141 -8
- package/crates/team-agent/src/state/selector.rs +5 -2
- package/crates/team-agent/src/tmux_backend.rs +161 -64
- 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
|
@@ -9,13 +9,17 @@ use crate::event_log::EventLog;
|
|
|
9
9
|
use crate::message_store::MessageStore;
|
|
10
10
|
use crate::model::enums::{PaneLiveness, Provider};
|
|
11
11
|
use crate::model::ids::TeamKey;
|
|
12
|
-
use crate::transport::{
|
|
12
|
+
use crate::transport::{
|
|
13
|
+
submit_verification_wire, InjectPayload, InjectReport, Key, PaneId, SessionName,
|
|
14
|
+
SubmitVerification, Target, Transport, WindowName,
|
|
15
|
+
};
|
|
13
16
|
|
|
14
17
|
use super::helpers::{message_exists, MessageStatusShadow};
|
|
15
18
|
use super::{
|
|
16
19
|
DeliveryOutcome, DeliveryRefusal, DeliveryStage, DeliveryStatus, MessagingError,
|
|
17
20
|
PaneWidthQuery, TrustRetryPayload,
|
|
18
21
|
};
|
|
22
|
+
use crate::state::projection::OwnerTeamResolution;
|
|
19
23
|
|
|
20
24
|
// ===========================================================================
|
|
21
25
|
// internal_delivery.py — coordinator/调度器侧 thin wrapper (card §65)
|
|
@@ -111,30 +115,45 @@ pub fn deliver_pending_message(
|
|
|
111
115
|
});
|
|
112
116
|
}
|
|
113
117
|
let message = message_for_delivery(store, message_id)?;
|
|
114
|
-
|
|
118
|
+
let Some(message) = message else {
|
|
115
119
|
return Ok(DeliveryOutcome {
|
|
116
120
|
ok: false,
|
|
117
|
-
status: DeliveryStatus::
|
|
118
|
-
message_status: MessageStatusShadow("
|
|
121
|
+
status: DeliveryStatus::Failed,
|
|
122
|
+
message_status: MessageStatusShadow("failed".to_string()),
|
|
119
123
|
message_id: Some(message_id.to_string()),
|
|
120
124
|
verification: None,
|
|
121
125
|
stage: None,
|
|
122
|
-
reason: Some(DeliveryRefusal::
|
|
126
|
+
reason: Some(DeliveryRefusal::UnknownRecipient),
|
|
123
127
|
channel: None,
|
|
124
128
|
});
|
|
125
|
-
}
|
|
126
|
-
let
|
|
129
|
+
};
|
|
130
|
+
let mut canonical_owner_team_id = message.owner_team_id.clone();
|
|
131
|
+
let scoped_state;
|
|
132
|
+
let state = match message.owner_team_id.as_deref() {
|
|
133
|
+
Some(team) if !team.is_empty() => {
|
|
134
|
+
match project_state_for_owner_team(workspace, team, state, Some(store), Some(message_id), Some(event_log))? {
|
|
135
|
+
OwnerTeamProjection::Projected { state, canonical_team } => {
|
|
136
|
+
canonical_owner_team_id = Some(canonical_team);
|
|
137
|
+
scoped_state = state;
|
|
138
|
+
&scoped_state
|
|
139
|
+
}
|
|
140
|
+
OwnerTeamProjection::Refused(outcome) => return Ok(outcome),
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
_ => state,
|
|
144
|
+
};
|
|
145
|
+
if !store.claim_for_delivery(message_id)? && message.status != "target_resolved" {
|
|
127
146
|
return Ok(DeliveryOutcome {
|
|
128
147
|
ok: false,
|
|
129
|
-
status: DeliveryStatus::
|
|
130
|
-
message_status: MessageStatusShadow("
|
|
148
|
+
status: DeliveryStatus::Refused,
|
|
149
|
+
message_status: MessageStatusShadow("target_resolved".to_string()),
|
|
131
150
|
message_id: Some(message_id.to_string()),
|
|
132
151
|
verification: None,
|
|
133
152
|
stage: None,
|
|
134
|
-
reason: Some(DeliveryRefusal::
|
|
153
|
+
reason: Some(DeliveryRefusal::MessageAlreadyClaimed),
|
|
135
154
|
channel: None,
|
|
136
155
|
});
|
|
137
|
-
}
|
|
156
|
+
}
|
|
138
157
|
if message.recipient == "leader" && leader_receiver_has_noncanonical_tmux_socket(state) {
|
|
139
158
|
store.mark(message_id, "failed", Some("leader_not_attached"))?;
|
|
140
159
|
event_log.write(
|
|
@@ -225,39 +244,68 @@ pub fn deliver_pending_message(
|
|
|
225
244
|
&message.content,
|
|
226
245
|
message_id,
|
|
227
246
|
);
|
|
228
|
-
|
|
247
|
+
let inject_report = match transport.inject(
|
|
229
248
|
&target,
|
|
230
249
|
&InjectPayload::Text(rendered),
|
|
231
250
|
Key::Enter,
|
|
232
251
|
true,
|
|
233
252
|
) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
"
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
253
|
+
Ok(report) => report,
|
|
254
|
+
Err(error) => {
|
|
255
|
+
if message.recipient == "leader" {
|
|
256
|
+
store.mark(message_id, "failed", Some("leader_not_attached"))?;
|
|
257
|
+
event_log.write(
|
|
258
|
+
"leader_receiver.delivery_blocked",
|
|
259
|
+
serde_json::json!({
|
|
260
|
+
"message_id": message_id,
|
|
261
|
+
"sender": message.sender,
|
|
262
|
+
"reason": "leader_not_attached",
|
|
263
|
+
"channel": "rebind_required",
|
|
264
|
+
"action": "run team-agent claim-leader or team-agent takeover",
|
|
265
|
+
"error": error.to_string(),
|
|
266
|
+
}),
|
|
267
|
+
)?;
|
|
268
|
+
return Ok(DeliveryOutcome {
|
|
269
|
+
ok: false,
|
|
270
|
+
status: DeliveryStatus::Refused,
|
|
271
|
+
message_status: MessageStatusShadow("failed".to_string()),
|
|
272
|
+
message_id: Some(message_id.to_string()),
|
|
273
|
+
verification: Some(
|
|
274
|
+
"run team-agent claim-leader or team-agent takeover".to_string(),
|
|
275
|
+
),
|
|
276
|
+
stage: None,
|
|
277
|
+
reason: Some(DeliveryRefusal::LeaderNotAttached),
|
|
278
|
+
channel: Some("rebind_required".to_string()),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
return Err(error.into());
|
|
259
282
|
}
|
|
260
|
-
|
|
283
|
+
};
|
|
284
|
+
if !inject_submit_verified(&inject_report) {
|
|
285
|
+
let reason = format!(
|
|
286
|
+
"submit_unverified:{}",
|
|
287
|
+
submit_verification_wire(inject_report.submit_verification)
|
|
288
|
+
);
|
|
289
|
+
store.mark(message_id, "submitted_unverified", Some(&reason))?;
|
|
290
|
+
event_log.write(
|
|
291
|
+
"send.unverified",
|
|
292
|
+
serde_json::json!({
|
|
293
|
+
"message_id": message_id,
|
|
294
|
+
"recipient": message.recipient,
|
|
295
|
+
"reason": reason,
|
|
296
|
+
"attempts": inject_report.attempts,
|
|
297
|
+
}),
|
|
298
|
+
)?;
|
|
299
|
+
return Ok(DeliveryOutcome {
|
|
300
|
+
ok: false,
|
|
301
|
+
status: DeliveryStatus::Failed,
|
|
302
|
+
message_status: MessageStatusShadow("submitted_unverified".to_string()),
|
|
303
|
+
message_id: Some(message_id.to_string()),
|
|
304
|
+
verification: Some(reason),
|
|
305
|
+
stage: Some(DeliveryStage::Submit),
|
|
306
|
+
reason: None,
|
|
307
|
+
channel: None,
|
|
308
|
+
});
|
|
261
309
|
}
|
|
262
310
|
store.mark(message_id, "delivered", None)?;
|
|
263
311
|
event_log.write(
|
|
@@ -274,18 +322,33 @@ pub fn deliver_pending_message(
|
|
|
274
322
|
reason: None,
|
|
275
323
|
channel: None,
|
|
276
324
|
};
|
|
277
|
-
|
|
278
|
-
|
|
325
|
+
stamp_first_send_at_if_leader_to_worker_scoped(
|
|
326
|
+
workspace,
|
|
327
|
+
&message.sender,
|
|
328
|
+
&message.recipient,
|
|
329
|
+
canonical_owner_team_id.as_deref(),
|
|
330
|
+
)?;
|
|
331
|
+
record_turn_open_if_leader_to_worker_scoped(
|
|
279
332
|
workspace,
|
|
280
|
-
state,
|
|
281
333
|
&message.sender,
|
|
282
334
|
&message.recipient,
|
|
283
335
|
&outcome,
|
|
284
336
|
event_log,
|
|
337
|
+
canonical_owner_team_id.as_deref(),
|
|
285
338
|
)?;
|
|
286
339
|
Ok(outcome)
|
|
287
340
|
}
|
|
288
341
|
|
|
342
|
+
fn inject_submit_verified(report: &InjectReport) -> bool {
|
|
343
|
+
match report.submit_verification {
|
|
344
|
+
SubmitVerification::SendKeysFailed => false,
|
|
345
|
+
SubmitVerification::PastedContentPromptStillPresentAfterSubmit => false,
|
|
346
|
+
SubmitVerification::PastedContentPromptAbsentAfterSubmit => true,
|
|
347
|
+
SubmitVerification::KeySentAfterVisibleToken { .. } => true,
|
|
348
|
+
SubmitVerification::EnterSentWithoutPlaceholderCheck => true,
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
289
352
|
/// Render a message into the worker-facing protocol block (port of `rust_core.py:render_message`,
|
|
290
353
|
/// golden-verified): `Team Agent message from {sender}[ for {task_id}]:\n\n{content}\n\n
|
|
291
354
|
/// [team-agent-token:{message_id}]`. The worker (fake or real provider) only builds a result_envelope
|
|
@@ -380,13 +443,60 @@ fn delivery_transport_for_recipient<'a>(
|
|
|
380
443
|
if recipient != "leader" {
|
|
381
444
|
return DeliveryTransport::Borrowed(product_transport);
|
|
382
445
|
}
|
|
446
|
+
let pane_id = leader_receiver_pane_id(state);
|
|
383
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
|
+
}
|
|
384
466
|
return DeliveryTransport::Borrowed(product_transport);
|
|
385
467
|
};
|
|
386
468
|
if socket == crate::tmux_backend::socket_name_for_workspace(workspace) {
|
|
387
469
|
DeliveryTransport::Borrowed(product_transport)
|
|
388
470
|
} else {
|
|
389
|
-
|
|
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)
|
|
390
500
|
}
|
|
391
501
|
}
|
|
392
502
|
|
|
@@ -396,7 +506,7 @@ fn leader_receiver_pane_id_in_state(state: &serde_json::Value) -> Option<&str> {
|
|
|
396
506
|
.get(key)
|
|
397
507
|
.and_then(|r| r.get("pane_id"))
|
|
398
508
|
.and_then(serde_json::Value::as_str)
|
|
399
|
-
.filter(|s| !s.is_empty())
|
|
509
|
+
.filter(|s| !s.is_empty() && *s != "__team_agent_unbound__")
|
|
400
510
|
})
|
|
401
511
|
}
|
|
402
512
|
|
|
@@ -458,7 +568,7 @@ pub fn deliver_pending_messages(
|
|
|
458
568
|
let conn = crate::db::schema::open_db(store.db_path())?;
|
|
459
569
|
let mut stmt = conn.prepare(
|
|
460
570
|
"select message_id from messages
|
|
461
|
-
where status in ('pending', 'accepted')
|
|
571
|
+
where status in ('pending', 'accepted', 'target_resolved')
|
|
462
572
|
order by created_at, message_id",
|
|
463
573
|
)?;
|
|
464
574
|
let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
|
|
@@ -467,6 +577,19 @@ pub fn deliver_pending_messages(
|
|
|
467
577
|
let mut delivered = Vec::new();
|
|
468
578
|
for message_id in message_ids {
|
|
469
579
|
if let Some(message) = message_for_delivery(&store, &message_id)? {
|
|
580
|
+
let scoped_state;
|
|
581
|
+
let state = match message.owner_team_id.as_deref() {
|
|
582
|
+
Some(team) if !team.is_empty() => {
|
|
583
|
+
match project_state_for_owner_team(workspace, team, state, Some(&store), Some(&message_id), Some(event_log))? {
|
|
584
|
+
OwnerTeamProjection::Projected { state, .. } => {
|
|
585
|
+
scoped_state = state;
|
|
586
|
+
&scoped_state
|
|
587
|
+
}
|
|
588
|
+
OwnerTeamProjection::Refused(_) => continue,
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
_ => state,
|
|
592
|
+
};
|
|
470
593
|
if recipient_is_busy(state, &message.recipient) {
|
|
471
594
|
event_log.write(
|
|
472
595
|
"send.deferred_busy",
|
|
@@ -493,6 +616,8 @@ struct PendingMessage {
|
|
|
493
616
|
recipient: String,
|
|
494
617
|
content: String,
|
|
495
618
|
task_id: Option<String>,
|
|
619
|
+
owner_team_id: Option<String>,
|
|
620
|
+
status: String,
|
|
496
621
|
}
|
|
497
622
|
|
|
498
623
|
fn message_for_delivery(
|
|
@@ -502,7 +627,7 @@ fn message_for_delivery(
|
|
|
502
627
|
let conn = crate::db::schema::open_db(store.db_path())?;
|
|
503
628
|
let message = conn
|
|
504
629
|
.query_row(
|
|
505
|
-
"select sender, recipient, content, task_id from messages where message_id = ?1",
|
|
630
|
+
"select sender, recipient, content, task_id, owner_team_id, status from messages where message_id = ?1",
|
|
506
631
|
params![message_id],
|
|
507
632
|
|row| {
|
|
508
633
|
Ok(PendingMessage {
|
|
@@ -510,6 +635,8 @@ fn message_for_delivery(
|
|
|
510
635
|
recipient: row.get::<_, String>(1)?,
|
|
511
636
|
content: row.get::<_, String>(2)?,
|
|
512
637
|
task_id: row.get::<_, Option<String>>(3)?,
|
|
638
|
+
owner_team_id: row.get::<_, Option<String>>(4)?,
|
|
639
|
+
status: row.get::<_, String>(5)?,
|
|
513
640
|
})
|
|
514
641
|
},
|
|
515
642
|
)
|
|
@@ -518,10 +645,11 @@ fn message_for_delivery(
|
|
|
518
645
|
}
|
|
519
646
|
|
|
520
647
|
/// Pre-inject gate (Contract B): peek the recipient pane and answer "is there an
|
|
521
|
-
/// actionable
|
|
522
|
-
/// the SHARED provider/startup_prompt
|
|
523
|
-
/// API calls. Returns `false` if capture fails so
|
|
524
|
-
/// 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.
|
|
525
653
|
fn recipient_pane_has_actionable_startup_prompt(
|
|
526
654
|
transport: &dyn Transport,
|
|
527
655
|
state: &serde_json::Value,
|
|
@@ -535,7 +663,7 @@ fn recipient_pane_has_actionable_startup_prompt(
|
|
|
535
663
|
let provider = agent
|
|
536
664
|
.and_then(|agent| agent.get("provider"))
|
|
537
665
|
.and_then(serde_json::Value::as_str);
|
|
538
|
-
if !matches!(provider, Some("codex")) {
|
|
666
|
+
if !matches!(provider, Some("codex" | "claude" | "claude_code")) {
|
|
539
667
|
return false;
|
|
540
668
|
}
|
|
541
669
|
// step2-retry/scrollback root-cause (rt binary 6c9c6c1c): once the agent's
|
|
@@ -558,11 +686,18 @@ fn recipient_pane_has_actionable_startup_prompt(
|
|
|
558
686
|
Ok(Ok(captured)) => captured.text,
|
|
559
687
|
_ => return false,
|
|
560
688
|
};
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
+
}
|
|
566
701
|
}
|
|
567
702
|
|
|
568
703
|
fn recipient_is_busy(state: &serde_json::Value, recipient: &str) -> bool {
|
|
@@ -666,10 +801,28 @@ pub fn record_turn_open_if_leader_to_worker(
|
|
|
666
801
|
event_log: &EventLog,
|
|
667
802
|
) -> Result<(), MessagingError> {
|
|
668
803
|
let _ = state;
|
|
804
|
+
record_turn_open_if_leader_to_worker_scoped(
|
|
805
|
+
workspace,
|
|
806
|
+
sender,
|
|
807
|
+
recipient,
|
|
808
|
+
delivered,
|
|
809
|
+
event_log,
|
|
810
|
+
None,
|
|
811
|
+
)
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
fn record_turn_open_if_leader_to_worker_scoped(
|
|
815
|
+
workspace: &Path,
|
|
816
|
+
sender: &str,
|
|
817
|
+
recipient: &str,
|
|
818
|
+
delivered: &DeliveryOutcome,
|
|
819
|
+
event_log: &EventLog,
|
|
820
|
+
owner_team_id: Option<&str>,
|
|
821
|
+
) -> Result<(), MessagingError> {
|
|
669
822
|
if !delivered.ok || !matches!(sender, "leader" | "Leader") || recipient == "leader" {
|
|
670
823
|
return Ok(());
|
|
671
824
|
}
|
|
672
|
-
let mut state =
|
|
825
|
+
let mut state = scoped_state_for_write(workspace, owner_team_id)?;
|
|
673
826
|
let Some(root) = state.as_object_mut() else {
|
|
674
827
|
return Ok(());
|
|
675
828
|
};
|
|
@@ -682,7 +835,7 @@ pub fn record_turn_open_if_leader_to_worker(
|
|
|
682
835
|
serde_json::json!({"armed": true, "node_id": recipient, "turn_id": delivered.message_id}),
|
|
683
836
|
);
|
|
684
837
|
}
|
|
685
|
-
|
|
838
|
+
save_scoped_state(workspace, &state, owner_team_id)?;
|
|
686
839
|
event_log.write(
|
|
687
840
|
"turn_open.armed_after_delivery",
|
|
688
841
|
serde_json::json!({"agent_id": recipient, "message_id": delivered.message_id}),
|
|
@@ -699,10 +852,19 @@ pub fn stamp_first_send_at_if_leader_to_worker(
|
|
|
699
852
|
recipient: &str,
|
|
700
853
|
) -> Result<(), MessagingError> {
|
|
701
854
|
let _ = state;
|
|
855
|
+
stamp_first_send_at_if_leader_to_worker_scoped(workspace, sender, recipient, None)
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
fn stamp_first_send_at_if_leader_to_worker_scoped(
|
|
859
|
+
workspace: &Path,
|
|
860
|
+
sender: &str,
|
|
861
|
+
recipient: &str,
|
|
862
|
+
owner_team_id: Option<&str>,
|
|
863
|
+
) -> Result<(), MessagingError> {
|
|
702
864
|
if !matches!(sender, "leader" | "Leader") || recipient == "leader" {
|
|
703
865
|
return Ok(());
|
|
704
866
|
}
|
|
705
|
-
let mut state =
|
|
867
|
+
let mut state = scoped_state_for_write(workspace, owner_team_id)?;
|
|
706
868
|
let now = chrono::Utc::now().to_rfc3339();
|
|
707
869
|
if let Some(agent) = state
|
|
708
870
|
.get_mut("agents")
|
|
@@ -712,12 +874,261 @@ pub fn stamp_first_send_at_if_leader_to_worker(
|
|
|
712
874
|
{
|
|
713
875
|
if !agent.contains_key("first_send_at") || agent.get("first_send_at").is_some_and(serde_json::Value::is_null) {
|
|
714
876
|
agent.insert("first_send_at".to_string(), serde_json::Value::String(now));
|
|
715
|
-
|
|
877
|
+
save_scoped_state(workspace, &state, owner_team_id)?;
|
|
716
878
|
}
|
|
717
879
|
}
|
|
718
880
|
Ok(())
|
|
719
881
|
}
|
|
720
882
|
|
|
883
|
+
fn scoped_state_for_write(
|
|
884
|
+
workspace: &Path,
|
|
885
|
+
owner_team_id: Option<&str>,
|
|
886
|
+
) -> Result<serde_json::Value, MessagingError> {
|
|
887
|
+
match owner_team_id.filter(|team| !team.is_empty()) {
|
|
888
|
+
Some(team) => {
|
|
889
|
+
let raw = crate::state::persist::load_runtime_state(workspace)?;
|
|
890
|
+
match project_state_for_owner_team_value(&raw, team) {
|
|
891
|
+
Some(projected) => Ok(projected),
|
|
892
|
+
None => Ok(raw),
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
None => Ok(crate::state::persist::load_runtime_state(workspace)?),
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
fn save_scoped_state(
|
|
900
|
+
workspace: &Path,
|
|
901
|
+
state: &serde_json::Value,
|
|
902
|
+
owner_team_id: Option<&str>,
|
|
903
|
+
) -> Result<(), MessagingError> {
|
|
904
|
+
if owner_team_id.filter(|team| !team.is_empty()).is_some() {
|
|
905
|
+
if state
|
|
906
|
+
.get("teams")
|
|
907
|
+
.and_then(serde_json::Value::as_object)
|
|
908
|
+
.is_some_and(|teams| {
|
|
909
|
+
owner_team_id
|
|
910
|
+
.and_then(|team| crate::state::projection::resolve_owner_team_id(state, team).canonical_key().map(str::to_string))
|
|
911
|
+
.is_some_and(|team| teams.contains_key(&team))
|
|
912
|
+
})
|
|
913
|
+
{
|
|
914
|
+
crate::state::projection::save_team_scoped_state(workspace, state)?;
|
|
915
|
+
} else {
|
|
916
|
+
crate::state::persist::save_runtime_state(workspace, state)?;
|
|
917
|
+
}
|
|
918
|
+
} else {
|
|
919
|
+
crate::state::persist::save_runtime_state(workspace, state)?;
|
|
920
|
+
}
|
|
921
|
+
Ok(())
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
enum OwnerTeamProjection {
|
|
925
|
+
Projected { state: serde_json::Value, canonical_team: String },
|
|
926
|
+
Refused(DeliveryOutcome),
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
fn project_state_for_owner_team(
|
|
930
|
+
workspace: &Path,
|
|
931
|
+
team: &str,
|
|
932
|
+
fallback: &serde_json::Value,
|
|
933
|
+
store: Option<&MessageStore>,
|
|
934
|
+
message_id: Option<&str>,
|
|
935
|
+
event_log: Option<&EventLog>,
|
|
936
|
+
) -> Result<OwnerTeamProjection, MessagingError> {
|
|
937
|
+
let raw = crate::state::persist::load_runtime_state(workspace)?;
|
|
938
|
+
let fallback_has_teams = fallback
|
|
939
|
+
.get("teams")
|
|
940
|
+
.and_then(serde_json::Value::as_object)
|
|
941
|
+
.is_some_and(|teams| !teams.is_empty());
|
|
942
|
+
let (mut projection_source, mut resolution) = if fallback_has_teams {
|
|
943
|
+
(fallback, crate::state::projection::resolve_owner_team_id(fallback, team))
|
|
944
|
+
} else {
|
|
945
|
+
(&raw, crate::state::projection::resolve_owner_team_id(&raw, team))
|
|
946
|
+
};
|
|
947
|
+
if !fallback_has_teams && matches!(resolution, OwnerTeamResolution::Unresolved { .. }) {
|
|
948
|
+
let fallback_resolution = crate::state::projection::resolve_owner_team_id(fallback, team);
|
|
949
|
+
if !matches!(fallback_resolution, OwnerTeamResolution::Unresolved { .. }) {
|
|
950
|
+
resolution = fallback_resolution;
|
|
951
|
+
projection_source = fallback;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
let canonical_team = match resolution {
|
|
955
|
+
OwnerTeamResolution::Canonical(canonical) => canonical,
|
|
956
|
+
OwnerTeamResolution::LegacyAlias { requested, canonical } => {
|
|
957
|
+
normalize_owner_team_id_rows(workspace, &requested, &canonical, message_id, event_log)?;
|
|
958
|
+
canonical
|
|
959
|
+
}
|
|
960
|
+
OwnerTeamResolution::Unresolved { requested } => {
|
|
961
|
+
let outcome = refuse_owner_team_resolution(
|
|
962
|
+
store,
|
|
963
|
+
message_id,
|
|
964
|
+
event_log,
|
|
965
|
+
"owner_team_unresolved",
|
|
966
|
+
serde_json::json!({"owner_team_id": requested}),
|
|
967
|
+
DeliveryRefusal::UnknownRecipient,
|
|
968
|
+
)?;
|
|
969
|
+
return Ok(OwnerTeamProjection::Refused(outcome));
|
|
970
|
+
}
|
|
971
|
+
OwnerTeamResolution::Ambiguous { requested, matches } => {
|
|
972
|
+
let outcome = refuse_owner_team_resolution(
|
|
973
|
+
store,
|
|
974
|
+
message_id,
|
|
975
|
+
event_log,
|
|
976
|
+
"owner_team_ambiguous",
|
|
977
|
+
serde_json::json!({"owner_team_id": requested, "matches": matches}),
|
|
978
|
+
DeliveryRefusal::Ambiguous,
|
|
979
|
+
)?;
|
|
980
|
+
return Ok(OwnerTeamProjection::Refused(outcome));
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
if top_level_state_matches_owner_team(fallback, &canonical_team) {
|
|
984
|
+
let mut state = fallback.clone();
|
|
985
|
+
carry_top_level_leader_binding(&mut state, &raw);
|
|
986
|
+
return Ok(OwnerTeamProjection::Projected {
|
|
987
|
+
state,
|
|
988
|
+
canonical_team,
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
if top_level_state_matches_owner_team(&raw, &canonical_team) {
|
|
992
|
+
return Ok(OwnerTeamProjection::Projected {
|
|
993
|
+
state: raw,
|
|
994
|
+
canonical_team,
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
if state_has_no_team_entries(projection_source) {
|
|
998
|
+
let mut state = projection_source.clone();
|
|
999
|
+
carry_top_level_leader_binding(&mut state, &raw);
|
|
1000
|
+
return Ok(OwnerTeamProjection::Projected {
|
|
1001
|
+
state,
|
|
1002
|
+
canonical_team,
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
let mut state = project_state_for_owner_team_value(projection_source, &canonical_team)
|
|
1006
|
+
.ok_or_else(|| MessagingError::Routing(format!("owner_team_unresolved: {canonical_team}")))?;
|
|
1007
|
+
carry_top_level_leader_binding(&mut state, projection_source);
|
|
1008
|
+
carry_top_level_leader_binding(&mut state, &raw);
|
|
1009
|
+
Ok(OwnerTeamProjection::Projected { state, canonical_team })
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
fn carry_top_level_leader_binding(projected: &mut serde_json::Value, raw: &serde_json::Value) {
|
|
1013
|
+
let Some(projected_obj) = projected.as_object_mut() else {
|
|
1014
|
+
return;
|
|
1015
|
+
};
|
|
1016
|
+
for key in ["leader_receiver", "team_owner", "owner_epoch"] {
|
|
1017
|
+
if projected_obj.contains_key(key) {
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
if let Some(value) = raw.get(key) {
|
|
1021
|
+
projected_obj.insert(key.to_string(), value.clone());
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
fn state_has_no_team_entries(state: &serde_json::Value) -> bool {
|
|
1027
|
+
state
|
|
1028
|
+
.get("teams")
|
|
1029
|
+
.and_then(serde_json::Value::as_object)
|
|
1030
|
+
.is_none_or(serde_json::Map::is_empty)
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
pub(crate) fn normalize_owner_team_id_rows(
|
|
1034
|
+
workspace: &Path,
|
|
1035
|
+
requested: &str,
|
|
1036
|
+
canonical: &str,
|
|
1037
|
+
message_id: Option<&str>,
|
|
1038
|
+
event_log: Option<&EventLog>,
|
|
1039
|
+
) -> Result<(), MessagingError> {
|
|
1040
|
+
if requested == canonical {
|
|
1041
|
+
return Ok(());
|
|
1042
|
+
}
|
|
1043
|
+
let store = MessageStore::open(workspace)?;
|
|
1044
|
+
let conn = crate::db::schema::open_db(store.db_path())?;
|
|
1045
|
+
for table in [
|
|
1046
|
+
"messages",
|
|
1047
|
+
"results",
|
|
1048
|
+
"scheduled_events",
|
|
1049
|
+
"agent_health",
|
|
1050
|
+
"result_watchers",
|
|
1051
|
+
"leader_notification_log",
|
|
1052
|
+
] {
|
|
1053
|
+
let sql = format!("update or ignore {table} set owner_team_id = ?1 where owner_team_id = ?2");
|
|
1054
|
+
conn.execute(&sql, params![canonical, requested])?;
|
|
1055
|
+
}
|
|
1056
|
+
if let Some(event_log) = event_log {
|
|
1057
|
+
event_log.write(
|
|
1058
|
+
"owner_team_id.compatibility_alias_migrated",
|
|
1059
|
+
serde_json::json!({
|
|
1060
|
+
"requested_owner_team_id": requested,
|
|
1061
|
+
"canonical_owner_team_id": canonical,
|
|
1062
|
+
"message_id": message_id,
|
|
1063
|
+
}),
|
|
1064
|
+
)?;
|
|
1065
|
+
}
|
|
1066
|
+
Ok(())
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
fn refuse_owner_team_resolution(
|
|
1070
|
+
store: Option<&MessageStore>,
|
|
1071
|
+
message_id: Option<&str>,
|
|
1072
|
+
event_log: Option<&EventLog>,
|
|
1073
|
+
error: &str,
|
|
1074
|
+
details: serde_json::Value,
|
|
1075
|
+
refusal: DeliveryRefusal,
|
|
1076
|
+
) -> Result<DeliveryOutcome, MessagingError> {
|
|
1077
|
+
if let (Some(store), Some(message_id)) = (store, message_id) {
|
|
1078
|
+
store.mark(message_id, "failed", Some(error))?;
|
|
1079
|
+
}
|
|
1080
|
+
if let Some(event_log) = event_log {
|
|
1081
|
+
event_log.write(
|
|
1082
|
+
"owner_team_id.resolution_failed",
|
|
1083
|
+
serde_json::json!({
|
|
1084
|
+
"message_id": message_id,
|
|
1085
|
+
"error": error,
|
|
1086
|
+
"details": details,
|
|
1087
|
+
}),
|
|
1088
|
+
)?;
|
|
1089
|
+
}
|
|
1090
|
+
Ok(DeliveryOutcome {
|
|
1091
|
+
ok: false,
|
|
1092
|
+
status: DeliveryStatus::Refused,
|
|
1093
|
+
message_status: MessageStatusShadow("failed".to_string()),
|
|
1094
|
+
message_id: message_id.map(str::to_string),
|
|
1095
|
+
verification: Some(error.to_string()),
|
|
1096
|
+
stage: None,
|
|
1097
|
+
reason: Some(refusal),
|
|
1098
|
+
channel: Some("owner_team_resolution".to_string()),
|
|
1099
|
+
})
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
fn project_state_for_owner_team_value(
|
|
1103
|
+
raw: &serde_json::Value,
|
|
1104
|
+
team: &str,
|
|
1105
|
+
) -> Option<serde_json::Value> {
|
|
1106
|
+
if let Some(projected) = raw
|
|
1107
|
+
.get("teams")
|
|
1108
|
+
.and_then(serde_json::Value::as_object)
|
|
1109
|
+
.is_some_and(|teams| teams.contains_key(team))
|
|
1110
|
+
.then(|| crate::state::projection::project_top_level_view(raw, team))
|
|
1111
|
+
{
|
|
1112
|
+
return Some(projected);
|
|
1113
|
+
}
|
|
1114
|
+
if top_level_state_matches_owner_team(raw, team) {
|
|
1115
|
+
return None;
|
|
1116
|
+
}
|
|
1117
|
+
None
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
fn top_level_state_matches_owner_team(state: &serde_json::Value, team: &str) -> bool {
|
|
1121
|
+
state
|
|
1122
|
+
.get("active_team_key")
|
|
1123
|
+
.and_then(serde_json::Value::as_str)
|
|
1124
|
+
.is_some_and(|value| value == team)
|
|
1125
|
+
|| crate::state::projection::team_state_key(state) == team
|
|
1126
|
+
|| state
|
|
1127
|
+
.get("session_name")
|
|
1128
|
+
.and_then(serde_json::Value::as_str)
|
|
1129
|
+
.is_some_and(|session| session == team || session.strip_prefix("team-") == Some(team))
|
|
1130
|
+
}
|
|
1131
|
+
|
|
721
1132
|
/// `retry_injection_after_trust_auto_answer` (`trust_auto_answer.py`):leader 路径 trust 应答
|
|
722
1133
|
/// 后重注入 (查 pane_width fail-safe + attempt_trust_auto_answer + 等 dismissal + 重 inject)。
|
|
723
1134
|
pub fn retry_injection_after_trust_auto_answer(
|