@team-agent/installer 0.4.10 → 0.5.0
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 +62 -4
- 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/leader/start.rs +169 -25
- 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 +436 -121
- 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 +193 -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 +54 -15
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +802 -0
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +648 -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/lifecycle/worker_command_context.rs +24 -34
- 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
|
@@ -59,7 +59,10 @@ pub fn detect_idle_fallbacks(
|
|
|
59
59
|
let idle_workers = stmt
|
|
60
60
|
.query_map(params![team.as_str()], |row| row.get::<_, String>(0))?
|
|
61
61
|
.collect::<Result<Vec<_>, _>>()?;
|
|
62
|
-
let worker_set = workers
|
|
62
|
+
let worker_set = workers
|
|
63
|
+
.iter()
|
|
64
|
+
.cloned()
|
|
65
|
+
.collect::<std::collections::BTreeSet<_>>();
|
|
63
66
|
let idle_set = idle_workers
|
|
64
67
|
.iter()
|
|
65
68
|
.cloned()
|
|
@@ -74,8 +77,16 @@ pub fn detect_idle_fallbacks(
|
|
|
74
77
|
}
|
|
75
78
|
let now = chrono::Utc::now().to_rfc3339();
|
|
76
79
|
let mut next_state = state.clone();
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
let suppression_snapshots =
|
|
81
|
+
idle_fallback_suppression_snapshots(&next_state, store, &team, &idle_workers)?;
|
|
82
|
+
register_idle_fallback_suppression(&mut next_state, &team, &now, &suppression_snapshots);
|
|
83
|
+
crate::state::persist::save_runtime_state_reapplying_after_conflict(
|
|
84
|
+
workspace,
|
|
85
|
+
&next_state,
|
|
86
|
+
|latest| {
|
|
87
|
+
register_idle_fallback_suppression(latest, &team, &now, &suppression_snapshots);
|
|
88
|
+
},
|
|
89
|
+
)?;
|
|
79
90
|
let alert_count = idle_workers.len();
|
|
80
91
|
let content = format!(
|
|
81
92
|
"Idle fallback: all workers idle while {obligation_count} undelivered obligation(s) remain."
|
|
@@ -213,7 +224,11 @@ fn active_team_key(workspace: &Path, state: &Value) -> String {
|
|
|
213
224
|
.and_then(Value::as_str)
|
|
214
225
|
.filter(|team| !team.is_empty())
|
|
215
226
|
.map(ToString::to_string)
|
|
216
|
-
.or_else(||
|
|
227
|
+
.or_else(|| {
|
|
228
|
+
workspace
|
|
229
|
+
.file_name()
|
|
230
|
+
.map(|name| name.to_string_lossy().to_string())
|
|
231
|
+
})
|
|
217
232
|
.unwrap_or_else(|| "current".to_string())
|
|
218
233
|
}
|
|
219
234
|
|
|
@@ -298,14 +313,13 @@ fn idle_fallback_suppressed(
|
|
|
298
313
|
&& delivered == delivered_message_ids(store, team, agent_id)?)
|
|
299
314
|
}
|
|
300
315
|
|
|
301
|
-
fn
|
|
302
|
-
state: &
|
|
316
|
+
fn idle_fallback_suppression_snapshots(
|
|
317
|
+
state: &Value,
|
|
303
318
|
store: &MessageStore,
|
|
304
319
|
team: &str,
|
|
305
320
|
idle_workers: &[String],
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
let snapshots = idle_workers
|
|
321
|
+
) -> Result<Vec<(String, Vec<String>, Vec<String>)>, MessagingError> {
|
|
322
|
+
idle_workers
|
|
309
323
|
.iter()
|
|
310
324
|
.map(|agent_id| {
|
|
311
325
|
Ok((
|
|
@@ -314,23 +328,31 @@ fn register_idle_fallback_suppression(
|
|
|
314
328
|
delivered_message_ids(store, team, agent_id)?,
|
|
315
329
|
))
|
|
316
330
|
})
|
|
317
|
-
.collect::<Result<Vec<_>, MessagingError>>()
|
|
331
|
+
.collect::<Result<Vec<_>, MessagingError>>()
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
fn register_idle_fallback_suppression(
|
|
335
|
+
state: &mut Value,
|
|
336
|
+
team: &str,
|
|
337
|
+
now: &str,
|
|
338
|
+
snapshots: &[(String, Vec<String>, Vec<String>)],
|
|
339
|
+
) {
|
|
318
340
|
let Some(root) = state.as_object_mut() else {
|
|
319
|
-
return
|
|
341
|
+
return;
|
|
320
342
|
};
|
|
321
343
|
let Some(coordinator) = root
|
|
322
344
|
.entry("coordinator")
|
|
323
345
|
.or_insert_with(|| serde_json::json!({}))
|
|
324
346
|
.as_object_mut()
|
|
325
347
|
else {
|
|
326
|
-
return
|
|
348
|
+
return;
|
|
327
349
|
};
|
|
328
350
|
let Some(last) = coordinator
|
|
329
351
|
.entry("idle_fallback_last_fired_at")
|
|
330
352
|
.or_insert_with(|| serde_json::json!({}))
|
|
331
353
|
.as_object_mut()
|
|
332
354
|
else {
|
|
333
|
-
return
|
|
355
|
+
return;
|
|
334
356
|
};
|
|
335
357
|
last.insert(team.to_string(), serde_json::Value::String(now.to_string()));
|
|
336
358
|
let Some(all) = coordinator
|
|
@@ -338,18 +360,18 @@ fn register_idle_fallback_suppression(
|
|
|
338
360
|
.or_insert_with(|| serde_json::json!({}))
|
|
339
361
|
.as_object_mut()
|
|
340
362
|
else {
|
|
341
|
-
return
|
|
363
|
+
return;
|
|
342
364
|
};
|
|
343
365
|
let Some(team_map) = all
|
|
344
366
|
.entry(team.to_string())
|
|
345
367
|
.or_insert_with(|| serde_json::json!({}))
|
|
346
368
|
.as_object_mut()
|
|
347
369
|
else {
|
|
348
|
-
return
|
|
370
|
+
return;
|
|
349
371
|
};
|
|
350
372
|
for (agent_id, assigned, delivered) in snapshots {
|
|
351
373
|
let Some(agent_map) = team_map
|
|
352
|
-
.entry(agent_id)
|
|
374
|
+
.entry(agent_id.clone())
|
|
353
375
|
.or_insert_with(|| serde_json::json!({}))
|
|
354
376
|
.as_object_mut()
|
|
355
377
|
else {
|
|
@@ -367,7 +389,6 @@ fn register_idle_fallback_suppression(
|
|
|
367
389
|
}),
|
|
368
390
|
);
|
|
369
391
|
}
|
|
370
|
-
Ok(())
|
|
371
392
|
}
|
|
372
393
|
|
|
373
394
|
fn timestamp_in_future(ts: &str) -> bool {
|
|
@@ -407,7 +428,11 @@ fn assigned_task_ids(state: &Value, agent_id: &str) -> Vec<String> {
|
|
|
407
428
|
tasks
|
|
408
429
|
.iter()
|
|
409
430
|
.filter(|task| task.get("assignee").and_then(Value::as_str) == Some(agent_id))
|
|
410
|
-
.filter_map(|task|
|
|
431
|
+
.filter_map(|task| {
|
|
432
|
+
task.get("id")
|
|
433
|
+
.and_then(Value::as_str)
|
|
434
|
+
.map(ToString::to_string)
|
|
435
|
+
})
|
|
411
436
|
.collect::<Vec<_>>()
|
|
412
437
|
})
|
|
413
438
|
.unwrap_or_default();
|
|
@@ -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() {
|