@team-agent/installer 0.4.11 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +1 -0
- package/crates/team-agent/src/cli/diagnose.rs +2 -15
- package/crates/team-agent/src/cli/emit.rs +98 -5
- package/crates/team-agent/src/cli/mod.rs +2 -0
- package/crates/team-agent/src/cli/named_address.rs +864 -0
- package/crates/team-agent/src/cli/send.rs +104 -0
- package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -0
- package/crates/team-agent/src/cli/tests/mod.rs +1 -0
- package/crates/team-agent/src/cli/tests/named_address.rs +455 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +4 -0
- package/crates/team-agent/src/compiler.rs +11 -20
- package/crates/team-agent/src/coordinator/orphan.rs +2 -2
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +5 -4
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +1776 -2
- package/crates/team-agent/src/coordinator/steps/mod.rs +19 -0
- package/crates/team-agent/src/coordinator/tests/a0_lostupdate.rs +78 -7
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +195 -0
- package/crates/team-agent/src/coordinator/tick.rs +31 -932
- package/crates/team-agent/src/diagnose/orphans.rs +66 -8
- package/crates/team-agent/src/layout/worker_window_helpers.rs +5 -7
- package/crates/team-agent/src/leader/provider_attribution.rs +7 -5
- package/crates/team-agent/src/leader/rediscover.rs +1 -11
- package/crates/team-agent/src/lifecycle/launch/plan.rs +19 -8
- package/crates/team-agent/src/lifecycle/launch.rs +135 -58
- package/crates/team-agent/src/lifecycle/lock.rs +302 -0
- package/crates/team-agent/src/lifecycle/mod.rs +1 -0
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +1 -11
- package/crates/team-agent/src/lifecycle/restart/agent.rs +169 -113
- package/crates/team-agent/src/lifecycle/restart/common.rs +108 -17
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +165 -32
- package/crates/team-agent/src/lifecycle/restart/remove.rs +16 -2
- package/crates/team-agent/src/lifecycle/restart/selection.rs +2 -1
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +6 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +3 -3
- package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +321 -0
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +35 -5
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +802 -0
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +675 -0
- package/crates/team-agent/src/lifecycle/tests.rs +3 -0
- package/crates/team-agent/src/lifecycle/types.rs +8 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +24 -9
- package/crates/team-agent/src/mcp_server/tools.rs +157 -68
- package/crates/team-agent/src/messaging/activity.rs +43 -18
- package/crates/team-agent/src/messaging/delivery.rs +161 -97
- package/crates/team-agent/src/messaging/helpers.rs +1 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +22 -1
- package/crates/team-agent/src/messaging/results.rs +34 -9
- package/crates/team-agent/src/messaging/scheduler.rs +52 -9
- package/crates/team-agent/src/model/spec.rs +10 -6
- package/crates/team-agent/src/provider/adapter.rs +10 -1038
- package/crates/team-agent/src/provider/adapters/claude.rs +3 -8
- package/crates/team-agent/src/provider/adapters/copilot.rs +2 -5
- package/crates/team-agent/src/provider/classify.rs +23 -48
- package/crates/team-agent/src/provider/command.rs +16 -7
- package/crates/team-agent/src/provider/faults.rs +93 -53
- package/crates/team-agent/src/provider/session/capture.rs +4 -15
- package/crates/team-agent/src/provider/session_scan/claude.rs +309 -0
- package/crates/team-agent/src/provider/session_scan/codex.rs +40 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +350 -0
- package/crates/team-agent/src/provider/session_scan/copilot.rs +202 -0
- package/crates/team-agent/src/provider/session_scan.rs +65 -27
- package/crates/team-agent/src/provider/tests/faults.rs +80 -0
- package/crates/team-agent/src/provider/types.rs +33 -1
- package/crates/team-agent/src/provider/wire.rs +171 -1
- package/crates/team-agent/src/state/identity.rs +242 -57
- package/crates/team-agent/src/state/identity_keys.rs +9 -12
- package/crates/team-agent/src/state/mod.rs +5 -1
- package/crates/team-agent/src/state/owner_gate.rs +59 -15
- package/crates/team-agent/src/state/ownership.rs +12 -11
- package/crates/team-agent/src/state/paths.rs +13 -6
- package/crates/team-agent/src/state/persist.rs +671 -128
- package/crates/team-agent/src/state/projection.rs +240 -49
- package/crates/team-agent/src/state/selector.rs +11 -5
- package/crates/team-agent/src/tmux_backend/tests.rs +51 -2
- package/crates/team-agent/src/tmux_backend.rs +42 -5
- package/crates/team-agent/src/transport/test_support.rs +55 -6
- package/package.json +4 -4
|
@@ -9,6 +9,9 @@ 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::provider::wire::{
|
|
13
|
+
is_claude_family, parse_canonical_provider, parse_provider, provider_wire,
|
|
14
|
+
};
|
|
12
15
|
use crate::transport::{
|
|
13
16
|
submit_verification_wire, InjectPayload, InjectReport, InjectVerification, Key, PaneId,
|
|
14
17
|
PaneInfo, SessionName, SubmitVerification, Target, Transport, WindowName,
|
|
@@ -302,76 +305,63 @@ pub fn deliver_pending_message(
|
|
|
302
305
|
} else {
|
|
303
306
|
InjectPayload::Text(rendered)
|
|
304
307
|
};
|
|
305
|
-
let inject_report =
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
store.mark(message_id, "failed", Some("leader_not_attached"))?;
|
|
312
|
-
event_log.write(
|
|
313
|
-
"leader_receiver.delivery_blocked",
|
|
314
|
-
serde_json::json!({
|
|
315
|
-
"message_id": message_id,
|
|
316
|
-
"sender": message.sender,
|
|
317
|
-
"reason": "leader_not_attached",
|
|
318
|
-
"channel": "rebind_required",
|
|
319
|
-
"action": "run team-agent claim-leader or team-agent takeover",
|
|
320
|
-
"error": error.to_string(),
|
|
321
|
-
}),
|
|
322
|
-
)?;
|
|
323
|
-
return Ok(DeliveryOutcome {
|
|
324
|
-
ok: false,
|
|
325
|
-
status: DeliveryStatus::Refused,
|
|
326
|
-
message_status: MessageStatusShadow("failed".to_string()),
|
|
327
|
-
message_id: Some(message_id.to_string()),
|
|
328
|
-
verification: Some(
|
|
329
|
-
"run team-agent claim-leader or team-agent takeover".to_string(),
|
|
330
|
-
),
|
|
331
|
-
stage: None,
|
|
332
|
-
reason: Some(DeliveryRefusal::LeaderNotAttached),
|
|
333
|
-
channel: Some("rebind_required".to_string()),
|
|
334
|
-
});
|
|
335
|
-
}
|
|
308
|
+
let inject_report = match transport.inject(&target, &payload, Key::Enter, true) {
|
|
309
|
+
Ok(report) => report,
|
|
310
|
+
Err(error) => {
|
|
311
|
+
let reason = format!("inject_failed:{error}");
|
|
312
|
+
if message.recipient == "leader" {
|
|
313
|
+
store.mark(message_id, "failed", Some("leader_not_attached"))?;
|
|
336
314
|
event_log.write(
|
|
337
|
-
"
|
|
315
|
+
"leader_receiver.delivery_blocked",
|
|
338
316
|
serde_json::json!({
|
|
339
317
|
"message_id": message_id,
|
|
340
|
-
"
|
|
341
|
-
"
|
|
342
|
-
"
|
|
318
|
+
"sender": message.sender,
|
|
319
|
+
"reason": "leader_not_attached",
|
|
320
|
+
"channel": "rebind_required",
|
|
321
|
+
"action": "run team-agent claim-leader or team-agent takeover",
|
|
343
322
|
"error": error.to_string(),
|
|
344
323
|
}),
|
|
345
324
|
)?;
|
|
346
|
-
if attempt >= u32::from(SEND_RETRY_MAX_ATTEMPTS) {
|
|
347
|
-
store.mark(message_id, "failed", Some("send_inject_exhausted"))?;
|
|
348
|
-
emit_send_failed_exhausted(
|
|
349
|
-
workspace,
|
|
350
|
-
state,
|
|
351
|
-
event_log,
|
|
352
|
-
message_id,
|
|
353
|
-
&message.recipient,
|
|
354
|
-
attempt,
|
|
355
|
-
"send_inject_exhausted",
|
|
356
|
-
&reason,
|
|
357
|
-
None,
|
|
358
|
-
)?;
|
|
359
|
-
return Ok(DeliveryOutcome {
|
|
360
|
-
ok: false,
|
|
361
|
-
status: DeliveryStatus::Failed,
|
|
362
|
-
message_status: MessageStatusShadow("failed".to_string()),
|
|
363
|
-
message_id: Some(message_id.to_string()),
|
|
364
|
-
verification: Some(reason),
|
|
365
|
-
stage: Some(DeliveryStage::Inject),
|
|
366
|
-
reason: None,
|
|
367
|
-
channel: None,
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
store.mark(message_id, "target_resolved", Some(&reason))?;
|
|
371
325
|
return Ok(DeliveryOutcome {
|
|
372
326
|
ok: false,
|
|
373
|
-
status: DeliveryStatus::
|
|
374
|
-
message_status: MessageStatusShadow("
|
|
327
|
+
status: DeliveryStatus::Refused,
|
|
328
|
+
message_status: MessageStatusShadow("failed".to_string()),
|
|
329
|
+
message_id: Some(message_id.to_string()),
|
|
330
|
+
verification: Some(
|
|
331
|
+
"run team-agent claim-leader or team-agent takeover".to_string(),
|
|
332
|
+
),
|
|
333
|
+
stage: None,
|
|
334
|
+
reason: Some(DeliveryRefusal::LeaderNotAttached),
|
|
335
|
+
channel: Some("rebind_required".to_string()),
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
event_log.write(
|
|
339
|
+
"send.inject_failed",
|
|
340
|
+
serde_json::json!({
|
|
341
|
+
"message_id": message_id,
|
|
342
|
+
"recipient": message.recipient,
|
|
343
|
+
"attempts": attempt,
|
|
344
|
+
"max_attempts": SEND_RETRY_MAX_ATTEMPTS,
|
|
345
|
+
"error": error.to_string(),
|
|
346
|
+
}),
|
|
347
|
+
)?;
|
|
348
|
+
if attempt >= u32::from(SEND_RETRY_MAX_ATTEMPTS) {
|
|
349
|
+
store.mark(message_id, "failed", Some("send_inject_exhausted"))?;
|
|
350
|
+
emit_send_failed_exhausted(
|
|
351
|
+
workspace,
|
|
352
|
+
state,
|
|
353
|
+
event_log,
|
|
354
|
+
message_id,
|
|
355
|
+
&message.recipient,
|
|
356
|
+
attempt,
|
|
357
|
+
"send_inject_exhausted",
|
|
358
|
+
&reason,
|
|
359
|
+
None,
|
|
360
|
+
)?;
|
|
361
|
+
return Ok(DeliveryOutcome {
|
|
362
|
+
ok: false,
|
|
363
|
+
status: DeliveryStatus::Failed,
|
|
364
|
+
message_status: MessageStatusShadow("failed".to_string()),
|
|
375
365
|
message_id: Some(message_id.to_string()),
|
|
376
366
|
verification: Some(reason),
|
|
377
367
|
stage: Some(DeliveryStage::Inject),
|
|
@@ -379,6 +369,18 @@ pub fn deliver_pending_message(
|
|
|
379
369
|
channel: None,
|
|
380
370
|
});
|
|
381
371
|
}
|
|
372
|
+
store.mark(message_id, "target_resolved", Some(&reason))?;
|
|
373
|
+
return Ok(DeliveryOutcome {
|
|
374
|
+
ok: false,
|
|
375
|
+
status: DeliveryStatus::Degraded,
|
|
376
|
+
message_status: MessageStatusShadow("target_resolved".to_string()),
|
|
377
|
+
message_id: Some(message_id.to_string()),
|
|
378
|
+
verification: Some(reason),
|
|
379
|
+
stage: Some(DeliveryStage::Inject),
|
|
380
|
+
reason: None,
|
|
381
|
+
channel: None,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
382
384
|
};
|
|
383
385
|
let submit_verified = inject_submit_verified(&inject_report);
|
|
384
386
|
let readback_verified = pane_readback_verified(&inject_report);
|
|
@@ -404,8 +406,7 @@ pub fn deliver_pending_message(
|
|
|
404
406
|
// legacy keys (`message_id` / `recipient` / `reason` / `attempts`)
|
|
405
407
|
// are preserved byte-for-byte for grep compatibility; new keys are
|
|
406
408
|
// ADDITIONAL.
|
|
407
|
-
let submit_attempts_detail =
|
|
408
|
-
render_submit_diagnostics(&inject_report);
|
|
409
|
+
let submit_attempts_detail = render_submit_diagnostics(&inject_report);
|
|
409
410
|
event_log.write(
|
|
410
411
|
"send.unverified",
|
|
411
412
|
serde_json::json!({
|
|
@@ -461,14 +462,15 @@ pub fn deliver_pending_message(
|
|
|
461
462
|
// per-delivery check / short grace window. Phase-1 Claude only —
|
|
462
463
|
// codex/copilot keep the pre-fix behaviour (will be addressed in
|
|
463
464
|
// phase-2 once Claude phase-1 is field-validated).
|
|
464
|
-
if let Some((rollout_path, provider_wire_str)) =
|
|
465
|
+
if let Some((rollout_path, provider_wire_str)) =
|
|
466
|
+
claude_recipient_rollout(state, &message.recipient)
|
|
467
|
+
{
|
|
465
468
|
let token_marker = format!("[team-agent-token:{message_id}]");
|
|
466
469
|
let grace = std::time::Duration::from_millis(200);
|
|
467
470
|
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(500);
|
|
468
471
|
let mut transcript_has_token = false;
|
|
469
472
|
loop {
|
|
470
|
-
transcript_has_token =
|
|
471
|
-
rollout_tail_contains(&rollout_path, &token_marker, 64 * 1024);
|
|
473
|
+
transcript_has_token = rollout_tail_contains(&rollout_path, &token_marker, 64 * 1024);
|
|
472
474
|
if transcript_has_token || std::time::Instant::now() >= deadline {
|
|
473
475
|
break;
|
|
474
476
|
}
|
|
@@ -589,7 +591,12 @@ pub(crate) fn pane_readback_verified(report: &InjectReport) -> bool {
|
|
|
589
591
|
/// when it sees this block + extracts the token — the bare content gives WORKING but never a report
|
|
590
592
|
/// (rt-host-a loop #4). token == message_id (exactly-once correlation).
|
|
591
593
|
/// F1 (0.3.26): promoted to `pub` for direct pane send in cli/send.rs.
|
|
592
|
-
pub fn render_message(
|
|
594
|
+
pub fn render_message(
|
|
595
|
+
sender: &str,
|
|
596
|
+
task_id: Option<&str>,
|
|
597
|
+
content: &str,
|
|
598
|
+
message_id: &str,
|
|
599
|
+
) -> String {
|
|
593
600
|
let mut header = format!("Team Agent message from {sender}");
|
|
594
601
|
if let Some(task_id) = task_id.filter(|t| !t.is_empty()) {
|
|
595
602
|
header.push_str(&format!(" for {task_id}"));
|
|
@@ -717,8 +724,7 @@ fn pane_conflicts_on_same_socket(
|
|
|
717
724
|
worker_socket: Option<&str>,
|
|
718
725
|
leader: PaneSocketBinding<'_>,
|
|
719
726
|
) -> bool {
|
|
720
|
-
leader.pane_id == pane_id
|
|
721
|
-
&& !tmux_sockets_known_different(worker_socket, leader.tmux_socket)
|
|
727
|
+
leader.pane_id == pane_id && !tmux_sockets_known_different(worker_socket, leader.tmux_socket)
|
|
722
728
|
}
|
|
723
729
|
|
|
724
730
|
fn tmux_sockets_known_different(left: Option<&str>, right: Option<&str>) -> bool {
|
|
@@ -784,7 +790,10 @@ fn leader_receiver_pane_is_usable(
|
|
|
784
790
|
let Some(pane_id) = leader_receiver_pane_id(state) else {
|
|
785
791
|
return false;
|
|
786
792
|
};
|
|
787
|
-
if live_targets
|
|
793
|
+
if live_targets
|
|
794
|
+
.iter()
|
|
795
|
+
.any(|target| target.pane_id.as_str() == pane_id)
|
|
796
|
+
{
|
|
788
797
|
return true;
|
|
789
798
|
}
|
|
790
799
|
!matches!(
|
|
@@ -1228,11 +1237,12 @@ fn recipient_pane_has_actionable_startup_prompt(
|
|
|
1228
1237
|
.and_then(|agents| agents.get(recipient));
|
|
1229
1238
|
let provider = agent
|
|
1230
1239
|
.and_then(|agent| agent.get("provider"))
|
|
1231
|
-
.and_then(serde_json::Value::as_str)
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1240
|
+
.and_then(serde_json::Value::as_str)
|
|
1241
|
+
.and_then(parse_canonical_provider);
|
|
1242
|
+
let Some(provider) = provider else {
|
|
1243
|
+
return false;
|
|
1244
|
+
};
|
|
1245
|
+
if matches!(provider, Provider::GeminiCli | Provider::Fake) {
|
|
1236
1246
|
return false;
|
|
1237
1247
|
}
|
|
1238
1248
|
// step2-retry/scrollback root-cause (rt binary 6c9c6c1c): once the agent's
|
|
@@ -1264,20 +1274,20 @@ fn recipient_pane_has_actionable_startup_prompt(
|
|
|
1264
1274
|
_ => return false,
|
|
1265
1275
|
};
|
|
1266
1276
|
match provider {
|
|
1267
|
-
|
|
1277
|
+
Provider::Codex => matches!(
|
|
1268
1278
|
crate::provider::classify_codex_startup_screen(&captured),
|
|
1269
1279
|
crate::provider::StartupScreenDecision::AnswerWorkspaceTrust
|
|
1270
1280
|
| crate::provider::StartupScreenDecision::SkipUpdatePrompt
|
|
1271
1281
|
),
|
|
1272
|
-
|
|
1282
|
+
Provider::Claude | Provider::ClaudeCode => matches!(
|
|
1273
1283
|
crate::provider::classify_claude_startup_screen(&captured),
|
|
1274
1284
|
crate::provider::StartupScreenDecision::AnswerWorkspaceTrust
|
|
1275
1285
|
),
|
|
1276
|
-
|
|
1286
|
+
Provider::Copilot => matches!(
|
|
1277
1287
|
crate::provider::classify_copilot_startup_screen(&captured),
|
|
1278
1288
|
crate::provider::StartupScreenDecision::AnswerWorkspaceTrust
|
|
1279
1289
|
),
|
|
1280
|
-
|
|
1290
|
+
Provider::GeminiCli | Provider::Fake => false,
|
|
1281
1291
|
}
|
|
1282
1292
|
}
|
|
1283
1293
|
|
|
@@ -1410,8 +1420,20 @@ fn record_turn_open_if_leader_to_worker_scoped(
|
|
|
1410
1420
|
return Ok(());
|
|
1411
1421
|
}
|
|
1412
1422
|
let mut state = scoped_state_for_write(workspace, owner_team_id)?;
|
|
1423
|
+
arm_turn_open(&mut state, recipient, &delivered.message_id);
|
|
1424
|
+
save_scoped_state_reapplying_after_conflict(workspace, &state, owner_team_id, |latest| {
|
|
1425
|
+
arm_turn_open(latest, recipient, &delivered.message_id);
|
|
1426
|
+
})?;
|
|
1427
|
+
event_log.write(
|
|
1428
|
+
"turn_open.armed_after_delivery",
|
|
1429
|
+
serde_json::json!({"agent_id": recipient, "message_id": delivered.message_id}),
|
|
1430
|
+
)?;
|
|
1431
|
+
Ok(())
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
fn arm_turn_open(state: &mut serde_json::Value, recipient: &str, message_id: &Option<String>) {
|
|
1413
1435
|
let Some(root) = state.as_object_mut() else {
|
|
1414
|
-
return
|
|
1436
|
+
return;
|
|
1415
1437
|
};
|
|
1416
1438
|
let coordinator = root
|
|
1417
1439
|
.entry("coordinator")
|
|
@@ -1419,15 +1441,9 @@ fn record_turn_open_if_leader_to_worker_scoped(
|
|
|
1419
1441
|
if let Some(obj) = coordinator.as_object_mut() {
|
|
1420
1442
|
obj.insert(
|
|
1421
1443
|
"turn_open".to_string(),
|
|
1422
|
-
serde_json::json!({"armed": true, "node_id": recipient, "turn_id":
|
|
1444
|
+
serde_json::json!({"armed": true, "node_id": recipient, "turn_id": message_id}),
|
|
1423
1445
|
);
|
|
1424
1446
|
}
|
|
1425
|
-
save_scoped_state(workspace, &state, owner_team_id)?;
|
|
1426
|
-
event_log.write(
|
|
1427
|
-
"turn_open.armed_after_delivery",
|
|
1428
|
-
serde_json::json!({"agent_id": recipient, "message_id": delivered.message_id}),
|
|
1429
|
-
)?;
|
|
1430
|
-
Ok(())
|
|
1431
1447
|
}
|
|
1432
1448
|
|
|
1433
1449
|
/// `_stamp_first_send_at_if_leader_to_worker` (`delivery.py:380`):首次 leader→worker 投递戳
|
|
@@ -1453,6 +1469,15 @@ fn stamp_first_send_at_if_leader_to_worker_scoped(
|
|
|
1453
1469
|
}
|
|
1454
1470
|
let mut state = scoped_state_for_write(workspace, owner_team_id)?;
|
|
1455
1471
|
let now = chrono::Utc::now().to_rfc3339();
|
|
1472
|
+
if stamp_first_send_at(&mut state, recipient, &now) {
|
|
1473
|
+
save_scoped_state_reapplying_after_conflict(workspace, &state, owner_team_id, |latest| {
|
|
1474
|
+
let _ = stamp_first_send_at(latest, recipient, &now);
|
|
1475
|
+
})?;
|
|
1476
|
+
}
|
|
1477
|
+
Ok(())
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
fn stamp_first_send_at(state: &mut serde_json::Value, recipient: &str, now: &str) -> bool {
|
|
1456
1481
|
if let Some(agent) = state
|
|
1457
1482
|
.get_mut("agents")
|
|
1458
1483
|
.and_then(serde_json::Value::as_object_mut)
|
|
@@ -1464,11 +1489,14 @@ fn stamp_first_send_at_if_leader_to_worker_scoped(
|
|
|
1464
1489
|
.get("first_send_at")
|
|
1465
1490
|
.is_some_and(serde_json::Value::is_null)
|
|
1466
1491
|
{
|
|
1467
|
-
agent.insert(
|
|
1468
|
-
|
|
1492
|
+
agent.insert(
|
|
1493
|
+
"first_send_at".to_string(),
|
|
1494
|
+
serde_json::Value::String(now.to_string()),
|
|
1495
|
+
);
|
|
1496
|
+
return true;
|
|
1469
1497
|
}
|
|
1470
1498
|
}
|
|
1471
|
-
|
|
1499
|
+
false
|
|
1472
1500
|
}
|
|
1473
1501
|
|
|
1474
1502
|
fn scoped_state_for_write(
|
|
@@ -1516,6 +1544,40 @@ fn save_scoped_state(
|
|
|
1516
1544
|
Ok(())
|
|
1517
1545
|
}
|
|
1518
1546
|
|
|
1547
|
+
fn save_scoped_state_reapplying_after_conflict<F>(
|
|
1548
|
+
workspace: &Path,
|
|
1549
|
+
state: &serde_json::Value,
|
|
1550
|
+
owner_team_id: Option<&str>,
|
|
1551
|
+
reapply: F,
|
|
1552
|
+
) -> Result<(), MessagingError>
|
|
1553
|
+
where
|
|
1554
|
+
F: FnOnce(&mut serde_json::Value),
|
|
1555
|
+
{
|
|
1556
|
+
if owner_team_id.filter(|team| !team.is_empty()).is_some()
|
|
1557
|
+
&& state
|
|
1558
|
+
.get("teams")
|
|
1559
|
+
.and_then(serde_json::Value::as_object)
|
|
1560
|
+
.is_some_and(|teams| {
|
|
1561
|
+
owner_team_id
|
|
1562
|
+
.and_then(|team| {
|
|
1563
|
+
crate::state::projection::resolve_owner_team_id(state, team)
|
|
1564
|
+
.canonical_key()
|
|
1565
|
+
.map(str::to_string)
|
|
1566
|
+
})
|
|
1567
|
+
.is_some_and(|team| teams.contains_key(&team))
|
|
1568
|
+
})
|
|
1569
|
+
{
|
|
1570
|
+
crate::state::projection::save_team_scoped_state_reapplying_after_conflict(
|
|
1571
|
+
workspace, state, reapply,
|
|
1572
|
+
)?;
|
|
1573
|
+
} else {
|
|
1574
|
+
crate::state::persist::save_runtime_state_reapplying_after_conflict(
|
|
1575
|
+
workspace, state, reapply,
|
|
1576
|
+
)?;
|
|
1577
|
+
}
|
|
1578
|
+
Ok(())
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1519
1581
|
enum OwnerTeamProjection {
|
|
1520
1582
|
Projected {
|
|
1521
1583
|
state: serde_json::Value,
|
|
@@ -1765,12 +1827,14 @@ fn claude_recipient_rollout(
|
|
|
1765
1827
|
recipient: &str,
|
|
1766
1828
|
) -> Option<(std::path::PathBuf, &'static str)> {
|
|
1767
1829
|
let agent = state.get("agents")?.get(recipient)?;
|
|
1768
|
-
let provider = agent
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1830
|
+
let provider = agent
|
|
1831
|
+
.get("provider")
|
|
1832
|
+
.and_then(serde_json::Value::as_str)
|
|
1833
|
+
.and_then(parse_provider)?;
|
|
1834
|
+
if !is_claude_family(provider) {
|
|
1835
|
+
return None;
|
|
1836
|
+
}
|
|
1837
|
+
let provider_wire_str = provider_wire(provider);
|
|
1774
1838
|
let rollout = agent
|
|
1775
1839
|
.get("rollout_path")
|
|
1776
1840
|
.and_then(serde_json::Value::as_str)
|
|
@@ -131,6 +131,7 @@ pub(crate) fn working_seconds(scrollback: &str) -> Option<u64> {
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
pub(crate) fn non_provider_command(command: &str) -> Option<&str> {
|
|
134
|
+
// Activity command grammar, not provider identity parsing.
|
|
134
135
|
let base = command.rsplit('/').next().unwrap_or(command);
|
|
135
136
|
let normalized = base.to_ascii_lowercase();
|
|
136
137
|
match normalized.as_str() {
|
|
@@ -269,7 +269,28 @@ pub fn claim_leader_receiver(
|
|
|
269
269
|
.with_leader_receiver(receiver_value)
|
|
270
270
|
.with_owner_epoch(next_epoch);
|
|
271
271
|
crate::state::ownership::write_owner(state, &team_key, record);
|
|
272
|
-
crate::state::persist::
|
|
272
|
+
crate::state::persist::save_runtime_state_reapplying_after_conflict(
|
|
273
|
+
workspace,
|
|
274
|
+
state,
|
|
275
|
+
|latest| {
|
|
276
|
+
let team_key = crate::state::projection::team_state_key(latest);
|
|
277
|
+
let record = crate::state::ownership::OwnershipWrite::new()
|
|
278
|
+
.with_team_owner(
|
|
279
|
+
state
|
|
280
|
+
.get("team_owner")
|
|
281
|
+
.cloned()
|
|
282
|
+
.unwrap_or_else(|| serde_json::json!({})),
|
|
283
|
+
)
|
|
284
|
+
.with_leader_receiver(
|
|
285
|
+
state
|
|
286
|
+
.get("leader_receiver")
|
|
287
|
+
.cloned()
|
|
288
|
+
.unwrap_or_else(|| serde_json::json!({})),
|
|
289
|
+
)
|
|
290
|
+
.with_owner_epoch(next_epoch);
|
|
291
|
+
crate::state::ownership::write_owner(latest, &team_key, record);
|
|
292
|
+
},
|
|
293
|
+
)?;
|
|
273
294
|
event_log.write(
|
|
274
295
|
"leader_receiver.claimed",
|
|
275
296
|
serde_json::json!({"owner_epoch": next_epoch, "candidate": candidate}),
|
|
@@ -108,6 +108,7 @@ fn collect_scoped(
|
|
|
108
108
|
let mut invalid_results = Vec::new();
|
|
109
109
|
let mut fatal_invalid_results = 0usize;
|
|
110
110
|
let mut state_dirty = false;
|
|
111
|
+
let mut task_updates = Vec::new();
|
|
111
112
|
for row in rows {
|
|
112
113
|
let envelope: serde_json::Value = match serde_json::from_str(&row.envelope) {
|
|
113
114
|
Ok(envelope) => envelope,
|
|
@@ -167,6 +168,7 @@ fn collect_scoped(
|
|
|
167
168
|
}
|
|
168
169
|
if scope == "task" {
|
|
169
170
|
mark_task_done(&mut state, &row.task_id, &row.result_id);
|
|
171
|
+
task_updates.push((row.task_id.clone(), row.result_id.clone()));
|
|
170
172
|
state_dirty = true;
|
|
171
173
|
}
|
|
172
174
|
log.write(
|
|
@@ -196,9 +198,25 @@ fn collect_scoped(
|
|
|
196
198
|
}
|
|
197
199
|
if state_dirty {
|
|
198
200
|
if owner_team_id.is_some() {
|
|
199
|
-
crate::state::projection::
|
|
201
|
+
crate::state::projection::save_team_scoped_state_reapplying_after_conflict(
|
|
202
|
+
&paths.run_workspace,
|
|
203
|
+
&state,
|
|
204
|
+
|latest| {
|
|
205
|
+
for (task_id, result_id) in &task_updates {
|
|
206
|
+
mark_task_done(latest, task_id, result_id);
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
)?;
|
|
200
210
|
} else {
|
|
201
|
-
crate::state::persist::
|
|
211
|
+
crate::state::persist::save_runtime_state_reapplying_after_conflict(
|
|
212
|
+
&paths.run_workspace,
|
|
213
|
+
&state,
|
|
214
|
+
|latest| {
|
|
215
|
+
for (task_id, result_id) in &task_updates {
|
|
216
|
+
mark_task_done(latest, task_id, result_id);
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
)?;
|
|
202
220
|
}
|
|
203
221
|
}
|
|
204
222
|
let counts = result_counts(&conn, owner_team_id)?;
|
|
@@ -708,8 +726,10 @@ fn report_result_for_owner_team_inner(
|
|
|
708
726
|
Ok(outcome) => outcome,
|
|
709
727
|
Err(error) => {
|
|
710
728
|
let message_id = format!("fallback:{result_id}");
|
|
711
|
-
let fallback_error =
|
|
712
|
-
|
|
729
|
+
let fallback_error = fallback_primary_error_text(
|
|
730
|
+
fallback_primary_error,
|
|
731
|
+
format!("leader_funnel_error:{error}"),
|
|
732
|
+
);
|
|
713
733
|
super::leader_receiver::deliver_to_leader_fallback_pane(
|
|
714
734
|
workspace,
|
|
715
735
|
&state,
|
|
@@ -1027,9 +1047,11 @@ fn format_report_result_changes(envelope: &serde_json::Value) -> Option<String>
|
|
|
1027
1047
|
.filter_map(|change| {
|
|
1028
1048
|
let path = report_field_any(change, &["path", "file", "filepath", "filename"])?;
|
|
1029
1049
|
let kind = report_field_any(change, &["kind", "type", "action"]).unwrap_or("changed");
|
|
1030
|
-
let description =
|
|
1031
|
-
|
|
1032
|
-
|
|
1050
|
+
let description = report_field_any(
|
|
1051
|
+
change,
|
|
1052
|
+
&["description", "summary", "detail", "details", "message"],
|
|
1053
|
+
)
|
|
1054
|
+
.unwrap_or(path);
|
|
1033
1055
|
Some(format!("{kind} {path}: {description}"))
|
|
1034
1056
|
})
|
|
1035
1057
|
.collect::<Vec<_>>();
|
|
@@ -1078,8 +1100,11 @@ fn format_report_result_next_actions(envelope: &serde_json::Value) -> Option<Str
|
|
|
1078
1100
|
let parts = report_result_array(envelope, "next_actions")?
|
|
1079
1101
|
.iter()
|
|
1080
1102
|
.filter_map(|action| {
|
|
1081
|
-
report_field_any(
|
|
1082
|
-
|
|
1103
|
+
report_field_any(
|
|
1104
|
+
action,
|
|
1105
|
+
&["description", "summary", "action", "todo", "message"],
|
|
1106
|
+
)
|
|
1107
|
+
.map(|text| text.to_string())
|
|
1083
1108
|
})
|
|
1084
1109
|
.collect::<Vec<_>>();
|
|
1085
1110
|
if parts.is_empty() {
|
|
@@ -10,7 +10,9 @@ use crate::transport::{PaneId, Transport};
|
|
|
10
10
|
|
|
11
11
|
use super::delivery::{deliver_stored_message, handle_trust_retry_needed};
|
|
12
12
|
use super::helpers::{parse_scheduled_kind, status_wire};
|
|
13
|
-
use super::{
|
|
13
|
+
use super::{
|
|
14
|
+
AlertType, MessagingError, ScheduledKind, TrustRetryPayload, TRUST_RETRY_MAX_ATTEMPTS,
|
|
15
|
+
};
|
|
14
16
|
|
|
15
17
|
/// `_fire_due_scheduled_events` (`scheduler.py:41`):coordinator tick 的调度器心脏 —— 分派到期
|
|
16
18
|
/// `send`/`health_ping`/`trust_retry` ([`ScheduledKind`] 穷尽 match),send 失败有界重试。
|
|
@@ -50,9 +52,15 @@ pub fn fire_due_scheduled_events(
|
|
|
50
52
|
let outcome = deliver_stored_message(
|
|
51
53
|
workspace,
|
|
52
54
|
Some(&target),
|
|
53
|
-
payload
|
|
55
|
+
payload
|
|
56
|
+
.get("content")
|
|
57
|
+
.and_then(|v| v.as_str())
|
|
58
|
+
.unwrap_or(""),
|
|
54
59
|
None,
|
|
55
|
-
payload
|
|
60
|
+
payload
|
|
61
|
+
.get("sender")
|
|
62
|
+
.and_then(|v| v.as_str())
|
|
63
|
+
.unwrap_or("leader"),
|
|
56
64
|
payload
|
|
57
65
|
.get("requires_ack")
|
|
58
66
|
.and_then(|v| v.as_bool())
|
|
@@ -61,7 +69,10 @@ pub fn fire_due_scheduled_events(
|
|
|
61
69
|
.get("wait_visible")
|
|
62
70
|
.and_then(|v| v.as_bool())
|
|
63
71
|
.unwrap_or(false),
|
|
64
|
-
payload
|
|
72
|
+
payload
|
|
73
|
+
.get("timeout")
|
|
74
|
+
.and_then(|v| v.as_f64())
|
|
75
|
+
.unwrap_or(30.0),
|
|
65
76
|
None,
|
|
66
77
|
)?;
|
|
67
78
|
serde_json::json!({
|
|
@@ -114,7 +125,10 @@ pub fn fire_due_scheduled_events(
|
|
|
114
125
|
|
|
115
126
|
let (status, result_json) = match fire_one() {
|
|
116
127
|
Ok(result) => {
|
|
117
|
-
let ok = result
|
|
128
|
+
let ok = result
|
|
129
|
+
.get("ok")
|
|
130
|
+
.and_then(serde_json::Value::as_bool)
|
|
131
|
+
.unwrap_or(false);
|
|
118
132
|
(if ok { "done" } else { "failed" }, result.to_string())
|
|
119
133
|
}
|
|
120
134
|
Err(error) => {
|
|
@@ -242,7 +256,26 @@ pub fn stuck_cancel(
|
|
|
242
256
|
},
|
|
243
257
|
);
|
|
244
258
|
}
|
|
245
|
-
crate::state::persist::
|
|
259
|
+
crate::state::persist::save_runtime_state_reapplying_after_conflict(
|
|
260
|
+
workspace,
|
|
261
|
+
&state,
|
|
262
|
+
|latest| {
|
|
263
|
+
for kind in &alert_types {
|
|
264
|
+
upsert_suppression(
|
|
265
|
+
latest,
|
|
266
|
+
SuppressionRecord {
|
|
267
|
+
team: &team,
|
|
268
|
+
agent_id,
|
|
269
|
+
alert_type: kind,
|
|
270
|
+
suppressed_by,
|
|
271
|
+
suppressed_at: &now,
|
|
272
|
+
assigned_task_ids: assigned.clone(),
|
|
273
|
+
delivered_message_ids: delivered.clone(),
|
|
274
|
+
},
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
)?;
|
|
246
279
|
crate::event_log::EventLog::new(workspace).write(
|
|
247
280
|
"coordinator.idle_alert_suppressed",
|
|
248
281
|
serde_json::json!({
|
|
@@ -273,7 +306,11 @@ fn active_team_key(workspace: &Path, state: &serde_json::Value) -> String {
|
|
|
273
306
|
.and_then(serde_json::Value::as_str)
|
|
274
307
|
.filter(|team| !team.is_empty())
|
|
275
308
|
.map(ToString::to_string)
|
|
276
|
-
.or_else(||
|
|
309
|
+
.or_else(|| {
|
|
310
|
+
workspace
|
|
311
|
+
.file_name()
|
|
312
|
+
.map(|name| name.to_string_lossy().to_string())
|
|
313
|
+
})
|
|
277
314
|
.unwrap_or_else(|| "current".to_string())
|
|
278
315
|
}
|
|
279
316
|
|
|
@@ -284,8 +321,14 @@ fn assigned_task_ids(state: &serde_json::Value, agent_id: &str) -> Vec<String> {
|
|
|
284
321
|
.map(|tasks| {
|
|
285
322
|
tasks
|
|
286
323
|
.iter()
|
|
287
|
-
.filter(|task|
|
|
288
|
-
|
|
324
|
+
.filter(|task| {
|
|
325
|
+
task.get("assignee").and_then(serde_json::Value::as_str) == Some(agent_id)
|
|
326
|
+
})
|
|
327
|
+
.filter_map(|task| {
|
|
328
|
+
task.get("id")
|
|
329
|
+
.and_then(serde_json::Value::as_str)
|
|
330
|
+
.map(ToString::to_string)
|
|
331
|
+
})
|
|
289
332
|
.collect()
|
|
290
333
|
})
|
|
291
334
|
.unwrap_or_default();
|