@team-agent/installer 0.5.66 → 0.5.68
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 +8 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/Cargo.toml +1 -0
- package/crates/team-agent/src/cli/adapters.rs +5 -0
- package/crates/team-agent/src/cli/diagnose.rs +82 -0
- package/crates/team-agent/src/cli/emit.rs +3 -3
- package/crates/team-agent/src/cli/grok_slot.rs +299 -0
- package/crates/team-agent/src/cli/leader.rs +21 -7
- package/crates/team-agent/src/cli/leaders.rs +125 -1
- package/crates/team-agent/src/cli/mod.rs +20 -0
- package/crates/team-agent/src/cli/send/presentation.rs +7 -1
- package/crates/team-agent/src/cli/spec.rs +2 -0
- package/crates/team-agent/src/cli/status_port/compact.rs +28 -0
- package/crates/team-agent/src/cli/status_port/snapshot.rs +25 -1
- package/crates/team-agent/src/cli/tests/base.rs +26 -3
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +129 -19
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +387 -6
- package/crates/team-agent/src/cli/tests/status_send.rs +79 -10
- package/crates/team-agent/src/communication_mode/mod.rs +6 -4
- package/crates/team-agent/src/compiler.rs +59 -0
- package/crates/team-agent/src/coordinator/backoff.rs +55 -0
- package/crates/team-agent/src/coordinator/conpty_shim.rs +82 -0
- package/crates/team-agent/src/coordinator/health.rs +173 -0
- package/crates/team-agent/src/coordinator/mod.rs +31 -0
- package/crates/team-agent/src/coordinator/orphan.rs +31 -0
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +30 -0
- package/crates/team-agent/src/coordinator/runtime_observation.rs +25 -0
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +59 -0
- package/crates/team-agent/src/coordinator/steps/delivery.rs +10 -0
- package/crates/team-agent/src/coordinator/steps/health_sync.rs +10 -0
- package/crates/team-agent/src/coordinator/steps/mod.rs +22 -0
- package/crates/team-agent/src/coordinator/steps/persist.rs +10 -0
- package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +10 -0
- package/crates/team-agent/src/coordinator/steps/session_gate.rs +10 -0
- package/crates/team-agent/src/coordinator/tick.rs +133 -22
- package/crates/team-agent/src/coordinator/types.rs +49 -0
- package/crates/team-agent/src/db/message_store.rs +39 -5
- package/crates/team-agent/src/layout/worker_env.rs +20 -3
- package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
- package/crates/team-agent/src/leader/provider_attribution.rs +53 -0
- package/crates/team-agent/src/leader/registry.rs +65 -0
- package/crates/team-agent/src/leader/start.rs +920 -63
- package/crates/team-agent/src/lifecycle/display.rs +56 -0
- package/crates/team-agent/src/lifecycle/helpers.rs +57 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +332 -12
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +62 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +59 -0
- package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +101 -11
- package/crates/team-agent/src/lifecycle/launch/cursor_create_chat.rs +229 -0
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +332 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +204 -456
- package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +24 -0
- package/crates/team-agent/src/lifecycle/launch/grok_per_seat.rs +260 -0
- package/crates/team-agent/src/lifecycle/launch/identity.rs +146 -0
- package/crates/team-agent/src/lifecycle/launch/layout.rs +60 -0
- package/crates/team-agent/src/lifecycle/launch/leader_context.rs +112 -0
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +530 -0
- package/crates/team-agent/src/lifecycle/launch/ownership.rs +40 -0
- package/crates/team-agent/src/lifecycle/launch/plan.rs +31 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +66 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +78 -0
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +85 -38
- package/crates/team-agent/src/lifecycle/launch/role_source.rs +44 -44
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +62 -2
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +139 -0
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +109 -0
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +214 -1
- package/crates/team-agent/src/lifecycle/launch.rs +99 -29
- package/crates/team-agent/src/lifecycle/lock.rs +42 -1
- package/crates/team-agent/src/lifecycle/mod.rs +11 -0
- package/crates/team-agent/src/lifecycle/pane_input_lock.rs +161 -0
- package/crates/team-agent/src/lifecycle/profile_launch.rs +98 -0
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +51 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +124 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +369 -11
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +27 -0
- package/crates/team-agent/src/lifecycle/restart/preflight.rs +25 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +106 -0
- package/crates/team-agent/src/lifecycle/restart/remove.rs +196 -5
- package/crates/team-agent/src/lifecycle/restart/selection.rs +72 -0
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +22 -0
- package/crates/team-agent/src/lifecycle/restart.rs +29 -0
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +2 -3
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +309 -0
- package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +150 -157
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/core.rs +4 -0
- package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +287 -0
- package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +229 -0
- package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +353 -0
- package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +784 -0
- package/crates/team-agent/src/lifecycle/tests/gate_fixtures.rs +562 -0
- package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +239 -0
- package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +498 -0
- package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +250 -0
- package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +293 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +63 -38
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +3 -2
- package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +16 -8
- package/crates/team-agent/src/lifecycle/tests/mcp_tool_name_format_red.rs +69 -0
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +20 -19
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +97 -61
- package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +2 -0
- package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +40 -7
- package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +59 -1
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +69 -26
- package/crates/team-agent/src/lifecycle/tests.rs +23 -13
- package/crates/team-agent/src/lifecycle/types.rs +61 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +85 -11
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +100 -1
- package/crates/team-agent/src/mcp_server/tests.rs +196 -2
- package/crates/team-agent/src/messaging/delivery.rs +405 -40
- package/crates/team-agent/src/messaging/helpers.rs +2 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +53 -1
- package/crates/team-agent/src/messaging/results.rs +4 -0
- package/crates/team-agent/src/messaging/send.rs +34 -0
- package/crates/team-agent/src/messaging/tests/dup_inject.rs +406 -0
- package/crates/team-agent/src/messaging/tests/e23.rs +9 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +66 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
- package/crates/team-agent/src/messaging/types.rs +4 -1
- package/crates/team-agent/src/model/enums.rs +14 -5
- package/crates/team-agent/src/model/permissions.rs +3 -0
- package/crates/team-agent/src/os_probe.rs +73 -2
- package/crates/team-agent/src/provider/adapter.rs +231 -4
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +86 -0
- package/crates/team-agent/src/provider/adapters/grok.rs +157 -0
- package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
- package/crates/team-agent/src/provider/bypass_flags.rs +46 -2
- package/crates/team-agent/src/provider/classify.rs +4 -2
- package/crates/team-agent/src/provider/faults.rs +2 -1
- package/crates/team-agent/src/provider/mod.rs +11 -0
- package/crates/team-agent/src/provider/session/capture.rs +158 -20
- package/crates/team-agent/src/provider/session/context_fork/claude.rs +38 -1
- package/crates/team-agent/src/provider/session/context_fork/codex.rs +72 -1
- package/crates/team-agent/src/provider/session/context_fork/outcome.rs +57 -1
- package/crates/team-agent/src/provider/session/context_fork.rs +77 -3
- package/crates/team-agent/src/provider/session/mod.rs +18 -0
- package/crates/team-agent/src/provider/session/resume.rs +57 -0
- package/crates/team-agent/src/provider/session_scan/claude.rs +125 -1
- package/crates/team-agent/src/provider/session_scan/codex.rs +94 -1
- package/crates/team-agent/src/provider/session_scan/common.rs +204 -2
- package/crates/team-agent/src/provider/session_scan/copilot.rs +33 -1
- package/crates/team-agent/src/provider/session_scan/cursor.rs +538 -0
- package/crates/team-agent/src/provider/session_scan/grok.rs +288 -0
- package/crates/team-agent/src/provider/session_scan.rs +8 -0
- package/crates/team-agent/src/provider/submit_now.rs +94 -0
- package/crates/team-agent/src/provider/tests/adapter.rs +304 -0
- package/crates/team-agent/src/provider/types.rs +4 -2
- package/crates/team-agent/src/provider/wire.rs +23 -1
- package/crates/team-agent/src/state/persist.rs +301 -8
- package/crates/team-agent/src/state/repository.rs +5 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +1737 -42
- package/crates/team-agent/src/tmux_backend.rs +1064 -75
- package/crates/team-agent/src/transport/tests/wire.rs +28 -2
- package/crates/team-agent/src/transport.rs +248 -1
- package/npm/install.mjs +129 -73
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +33 -238
- package/skills/team-agent/command-coverage.json +31 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +0 -59
- package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +0 -488
- package/crates/team-agent/src/lifecycle/launch/fork_pending.rs +0 -109
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +0 -447
|
@@ -295,6 +295,7 @@ pub(crate) fn fail_leader_delivery(
|
|
|
295
295
|
reason: Some(reason),
|
|
296
296
|
channel: Some("fallback_log".to_string()),
|
|
297
297
|
ack_forced_off: false,
|
|
298
|
+
turn_verification: None,
|
|
298
299
|
});
|
|
299
300
|
};
|
|
300
301
|
store.mark(&message_id, "failed", error)?;
|
|
@@ -312,6 +313,7 @@ pub(crate) fn fail_leader_delivery(
|
|
|
312
313
|
reason: Some(reason),
|
|
313
314
|
channel: Some("fallback_inbox".to_string()),
|
|
314
315
|
ack_forced_off: false,
|
|
316
|
+
turn_verification: None,
|
|
315
317
|
})
|
|
316
318
|
}
|
|
317
319
|
|
|
@@ -134,6 +134,7 @@ pub fn send_to_leader_receiver_with_presentation(
|
|
|
134
134
|
reason: Some(DeliveryRefusal::Duplicate),
|
|
135
135
|
channel: Some("leader_receiver".to_string()),
|
|
136
136
|
ack_forced_off: requires_ack,
|
|
137
|
+
turn_verification: None,
|
|
137
138
|
});
|
|
138
139
|
}
|
|
139
140
|
PersistResolution::Persisted(persisted) => persisted.message_id,
|
|
@@ -176,6 +177,7 @@ pub fn send_to_leader_receiver_with_presentation(
|
|
|
176
177
|
reason: None,
|
|
177
178
|
channel: Some("leader_receiver".to_string()),
|
|
178
179
|
ack_forced_off: requires_ack,
|
|
180
|
+
turn_verification: None,
|
|
179
181
|
});
|
|
180
182
|
}
|
|
181
183
|
}
|
|
@@ -213,6 +215,7 @@ pub fn send_to_leader_receiver_with_presentation(
|
|
|
213
215
|
reason: None,
|
|
214
216
|
channel: Some("leader_receiver".to_string()),
|
|
215
217
|
ack_forced_off: requires_ack,
|
|
218
|
+
turn_verification: None,
|
|
216
219
|
})
|
|
217
220
|
}
|
|
218
221
|
|
|
@@ -259,6 +262,7 @@ pub fn deliver_to_leader_fallback_pane(
|
|
|
259
262
|
reason: None,
|
|
260
263
|
channel: Some("leader_receiver".to_string()),
|
|
261
264
|
ack_forced_off: false,
|
|
265
|
+
turn_verification: None,
|
|
262
266
|
});
|
|
263
267
|
}
|
|
264
268
|
|
|
@@ -274,6 +278,7 @@ pub fn deliver_to_leader_fallback_pane(
|
|
|
274
278
|
reason: None,
|
|
275
279
|
channel: Some("leader_receiver".to_string()),
|
|
276
280
|
ack_forced_off: false,
|
|
281
|
+
turn_verification: None,
|
|
277
282
|
});
|
|
278
283
|
}
|
|
279
284
|
|
|
@@ -316,6 +321,7 @@ pub fn deliver_to_leader_fallback_pane(
|
|
|
316
321
|
reason: Some(DeliveryRefusal::LeaderNotAttached),
|
|
317
322
|
channel: Some("fallback_pane".to_string()),
|
|
318
323
|
ack_forced_off: false,
|
|
324
|
+
turn_verification: None,
|
|
319
325
|
});
|
|
320
326
|
};
|
|
321
327
|
|
|
@@ -390,9 +396,40 @@ pub fn deliver_to_leader_fallback_pane(
|
|
|
390
396
|
reason: Some(DeliveryRefusal::LeaderNotAttached),
|
|
391
397
|
channel: Some("rebind_required".to_string()),
|
|
392
398
|
ack_forced_off: false,
|
|
399
|
+
turn_verification: None,
|
|
393
400
|
});
|
|
394
401
|
};
|
|
395
402
|
let target = Target::Pane(PaneId::new(channel.pane_id));
|
|
403
|
+
if super::delivery::token_already_visible(
|
|
404
|
+
transport.as_ref(),
|
|
405
|
+
state,
|
|
406
|
+
"leader",
|
|
407
|
+
&target,
|
|
408
|
+
message_id,
|
|
409
|
+
) {
|
|
410
|
+
event_log.write(
|
|
411
|
+
"leader_receiver.duplicate_token_refused",
|
|
412
|
+
serde_json::json!({
|
|
413
|
+
"message_id": message_id,
|
|
414
|
+
"result_id": result_id,
|
|
415
|
+
"pane_id": pane_id,
|
|
416
|
+
"reason": "message_token_already_visible",
|
|
417
|
+
"delivered_via": "fallback_pane",
|
|
418
|
+
}),
|
|
419
|
+
)?;
|
|
420
|
+
return Ok(DeliveryOutcome {
|
|
421
|
+
ok: true,
|
|
422
|
+
status: DeliveryStatus::AlreadyDelivered,
|
|
423
|
+
message_status: MessageStatusShadow("delivered".to_string()),
|
|
424
|
+
message_id: Some(message_id.to_string()),
|
|
425
|
+
verification: Some("message_token_already_visible".to_string()),
|
|
426
|
+
stage: Some(DeliveryStage::Submit),
|
|
427
|
+
reason: None,
|
|
428
|
+
channel: Some("fallback_pane".to_string()),
|
|
429
|
+
ack_forced_off: false,
|
|
430
|
+
turn_verification: None,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
396
433
|
let inject_result: Result<InjectReport, TransportError> =
|
|
397
434
|
transport.inject(&target, &payload, Key::Enter, true);
|
|
398
435
|
|
|
@@ -443,6 +480,7 @@ pub fn deliver_to_leader_fallback_pane(
|
|
|
443
480
|
reason: None,
|
|
444
481
|
channel: Some("leader_acceptance_pending".to_string()),
|
|
445
482
|
ack_forced_off: false,
|
|
483
|
+
turn_verification: None,
|
|
446
484
|
});
|
|
447
485
|
}
|
|
448
486
|
super::delivery::LeaderReceiptObservation::TokenObserved => {}
|
|
@@ -470,6 +508,7 @@ pub fn deliver_to_leader_fallback_pane(
|
|
|
470
508
|
reason: None,
|
|
471
509
|
channel: Some("fallback_pane".to_string()),
|
|
472
510
|
ack_forced_off: false,
|
|
511
|
+
turn_verification: None,
|
|
473
512
|
})
|
|
474
513
|
} else {
|
|
475
514
|
let reason = format!(
|
|
@@ -496,6 +535,7 @@ pub fn deliver_to_leader_fallback_pane(
|
|
|
496
535
|
reason: None,
|
|
497
536
|
channel: Some("fallback_pane".to_string()),
|
|
498
537
|
ack_forced_off: false,
|
|
538
|
+
turn_verification: None,
|
|
499
539
|
})
|
|
500
540
|
}
|
|
501
541
|
}
|
|
@@ -538,6 +578,7 @@ pub fn deliver_to_leader_fallback_pane(
|
|
|
538
578
|
reason: Some(DeliveryRefusal::TmuxTargetMissing),
|
|
539
579
|
channel: Some(channel),
|
|
540
580
|
ack_forced_off: false,
|
|
581
|
+
turn_verification: None,
|
|
541
582
|
})
|
|
542
583
|
}
|
|
543
584
|
}
|
|
@@ -656,7 +697,17 @@ fn message_already_delivered(
|
|
|
656
697
|
.optional()?;
|
|
657
698
|
Ok(matches!(
|
|
658
699
|
status.as_deref(),
|
|
659
|
-
Some(
|
|
700
|
+
Some(
|
|
701
|
+
"delivered"
|
|
702
|
+
| "acknowledged"
|
|
703
|
+
| "submitted"
|
|
704
|
+
| "submitted_unverified"
|
|
705
|
+
| "submitted_pending_acceptance"
|
|
706
|
+
| "injected_awaiting_receipt"
|
|
707
|
+
| "visible"
|
|
708
|
+
| "injected"
|
|
709
|
+
| "consumed"
|
|
710
|
+
)
|
|
660
711
|
))
|
|
661
712
|
}
|
|
662
713
|
|
|
@@ -730,5 +781,6 @@ pub fn enqueue_leader_mailbox_until_attach(
|
|
|
730
781
|
reason: None,
|
|
731
782
|
channel: Some("leader_mailbox".to_string()),
|
|
732
783
|
ack_forced_off: false,
|
|
784
|
+
turn_verification: None,
|
|
733
785
|
})
|
|
734
786
|
}
|
|
@@ -940,6 +940,7 @@ fn report_result_for_owner_team_inner(
|
|
|
940
940
|
reason: Some(crate::messaging::DeliveryRefusal::LeaderNotAttached),
|
|
941
941
|
channel: Some("codex_app_server".to_string()),
|
|
942
942
|
ack_forced_off: false,
|
|
943
|
+
turn_verification: None,
|
|
943
944
|
}
|
|
944
945
|
}
|
|
945
946
|
Err(error) => {
|
|
@@ -1005,6 +1006,7 @@ fn report_result_for_owner_team_inner(
|
|
|
1005
1006
|
reason: None,
|
|
1006
1007
|
channel: Some("leader_receiver".to_string()),
|
|
1007
1008
|
ack_forced_off: false,
|
|
1009
|
+
turn_verification: None,
|
|
1008
1010
|
};
|
|
1009
1011
|
break;
|
|
1010
1012
|
}
|
|
@@ -1035,6 +1037,7 @@ fn report_result_for_owner_team_inner(
|
|
|
1035
1037
|
reason: None,
|
|
1036
1038
|
channel: Some("leader_receiver".to_string()),
|
|
1037
1039
|
ack_forced_off: false,
|
|
1040
|
+
turn_verification: None,
|
|
1038
1041
|
};
|
|
1039
1042
|
break;
|
|
1040
1043
|
}
|
|
@@ -1051,6 +1054,7 @@ fn report_result_for_owner_team_inner(
|
|
|
1051
1054
|
reason: None,
|
|
1052
1055
|
channel: Some("leader_receiver".to_string()),
|
|
1053
1056
|
ack_forced_off: false,
|
|
1057
|
+
turn_verification: None,
|
|
1054
1058
|
};
|
|
1055
1059
|
break;
|
|
1056
1060
|
}
|
|
@@ -215,6 +215,7 @@ pub fn send_message(
|
|
|
215
215
|
reason: Some(crate::messaging::DeliveryRefusal::EmptyTargetList),
|
|
216
216
|
channel: None,
|
|
217
217
|
ack_forced_off: false,
|
|
218
|
+
turn_verification: None,
|
|
218
219
|
});
|
|
219
220
|
}
|
|
220
221
|
MessageTarget::Fanout(recipients) => {
|
|
@@ -316,6 +317,7 @@ pub fn send_message(
|
|
|
316
317
|
None,
|
|
317
318
|
);
|
|
318
319
|
}
|
|
320
|
+
capture_missing_sessions_after_send(workspace);
|
|
319
321
|
if let Some(CoordinatorUnavailableGate {
|
|
320
322
|
mut outcome,
|
|
321
323
|
persist: true,
|
|
@@ -353,9 +355,34 @@ pub fn send_message(
|
|
|
353
355
|
reason: None,
|
|
354
356
|
channel: None,
|
|
355
357
|
ack_forced_off: false,
|
|
358
|
+
turn_verification: None,
|
|
356
359
|
})
|
|
357
360
|
}
|
|
358
361
|
|
|
362
|
+
fn capture_missing_sessions_after_send(workspace: &Path) {
|
|
363
|
+
let Ok(mut state) = crate::state::persist::load_runtime_state(workspace) else {
|
|
364
|
+
return;
|
|
365
|
+
};
|
|
366
|
+
let Ok(report) = crate::session_capture::capture_missing_provider_sessions_once(
|
|
367
|
+
&mut state,
|
|
368
|
+
&mut crate::provider::get_adapter,
|
|
369
|
+
false,
|
|
370
|
+
0,
|
|
371
|
+
) else {
|
|
372
|
+
return;
|
|
373
|
+
};
|
|
374
|
+
if report.changed || !report.assigned.is_empty() {
|
|
375
|
+
// The post-send capture backfill routes through `StateRepository` with
|
|
376
|
+
// the `MessagingSessionCapture` intent; dispatch is the same root
|
|
377
|
+
// helper this callsite used directly before, and the error is still
|
|
378
|
+
// ignored because the capture is best-effort.
|
|
379
|
+
let _ = crate::state::repository::StateRepository::new(workspace).save(
|
|
380
|
+
crate::state::repository::StateWriteIntent::MessagingSessionCapture,
|
|
381
|
+
&state,
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
359
386
|
fn persist_stored_only_send(
|
|
360
387
|
workspace: &Path,
|
|
361
388
|
recipient: LogicalRecipient,
|
|
@@ -407,6 +434,7 @@ fn persist_stored_only_send(
|
|
|
407
434
|
reason: None,
|
|
408
435
|
channel: Some(presentation.effective_sink.as_str().to_string()),
|
|
409
436
|
ack_forced_off: false,
|
|
437
|
+
turn_verification: None,
|
|
410
438
|
})
|
|
411
439
|
}
|
|
412
440
|
|
|
@@ -455,6 +483,7 @@ fn refused_outcome_with_id(reason: DeliveryRefusal, message_id: Option<String>)
|
|
|
455
483
|
reason: Some(reason),
|
|
456
484
|
channel: None,
|
|
457
485
|
ack_forced_off: false,
|
|
486
|
+
turn_verification: None,
|
|
458
487
|
}
|
|
459
488
|
}
|
|
460
489
|
|
|
@@ -472,6 +501,7 @@ fn refused_outcome_with_verification(
|
|
|
472
501
|
reason: Some(reason),
|
|
473
502
|
channel: None,
|
|
474
503
|
ack_forced_off: false,
|
|
504
|
+
turn_verification: None,
|
|
475
505
|
}
|
|
476
506
|
}
|
|
477
507
|
|
|
@@ -555,6 +585,7 @@ fn coordinator_unavailable_outcome(
|
|
|
555
585
|
reason: Some(DeliveryRefusal::CoordinatorUnavailable),
|
|
556
586
|
channel: Some("coordinator_unavailable".to_string()),
|
|
557
587
|
ack_forced_off: false,
|
|
588
|
+
turn_verification: None,
|
|
558
589
|
},
|
|
559
590
|
}))
|
|
560
591
|
}
|
|
@@ -580,6 +611,7 @@ fn rebind_required_outcome_with_verification(
|
|
|
580
611
|
reason: Some(DeliveryRefusal::LeaderNotAttached),
|
|
581
612
|
channel: Some("rebind_required".to_string()),
|
|
582
613
|
ack_forced_off: false,
|
|
614
|
+
turn_verification: None,
|
|
583
615
|
}
|
|
584
616
|
}
|
|
585
617
|
|
|
@@ -812,6 +844,7 @@ pub(crate) fn session_drift_refusal(
|
|
|
812
844
|
reason: Some(DeliveryRefusal::SessionDrift),
|
|
813
845
|
channel: None,
|
|
814
846
|
ack_forced_off: false,
|
|
847
|
+
turn_verification: None,
|
|
815
848
|
}))
|
|
816
849
|
}
|
|
817
850
|
|
|
@@ -933,5 +966,6 @@ fn fanout_send(
|
|
|
933
966
|
channel_label.to_string()
|
|
934
967
|
}),
|
|
935
968
|
ack_forced_off: false,
|
|
969
|
+
turn_verification: None,
|
|
936
970
|
})
|
|
937
971
|
}
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
//! ---
|
|
2
|
+
//! purpose: 消息双投三机制回归——claim 闸、observer 落盘失败、token 幂等
|
|
3
|
+
//! contract:
|
|
4
|
+
//! provides:
|
|
5
|
+
//! - name: dup-inject-unit
|
|
6
|
+
//! what: 同 message_id 二次 deliver 不得再 inject;observer 失败不返 Err 重投
|
|
7
|
+
//! requires:
|
|
8
|
+
//! - name: recording-transport
|
|
9
|
+
//! what: 捕获 pane 文本与 inject 次数
|
|
10
|
+
//! boundary:
|
|
11
|
+
//! - 不作为活体验收;活体三臂装置在 scripts/repro/repro_dup_inject.sh
|
|
12
|
+
//! maturity: wired
|
|
13
|
+
//! ---
|
|
14
|
+
|
|
15
|
+
use super::*;
|
|
16
|
+
use std::collections::BTreeMap;
|
|
17
|
+
use std::path::{Path, PathBuf};
|
|
18
|
+
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
19
|
+
use std::sync::{Arc, Mutex};
|
|
20
|
+
|
|
21
|
+
use crate::event_log::EventLog;
|
|
22
|
+
use crate::message_store::MessageStore;
|
|
23
|
+
use crate::messaging::leader_receiver::deliver_to_leader_fallback_pane;
|
|
24
|
+
use crate::transport::{
|
|
25
|
+
AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport,
|
|
26
|
+
InjectStage, InjectVerification, Key, PaneField, PaneId, PaneInfo, PaneLiveness, SessionName,
|
|
27
|
+
SetEnvOutcome, SpawnResult, SubmitVerification, Target, Transport, TransportError,
|
|
28
|
+
TurnVerification, WindowName,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
#[test]
|
|
32
|
+
fn error_retry_claim_is_single_winner_across_two_delivers() {
|
|
33
|
+
let case = WorkerCase::new("claim-gate", "fake");
|
|
34
|
+
let transport = RecordingTransport::new("");
|
|
35
|
+
let message_id = case.seed_leader_to_worker("claim canary");
|
|
36
|
+
case.store
|
|
37
|
+
.mark(&message_id, "target_resolved", Some("probe_failed"))
|
|
38
|
+
.unwrap();
|
|
39
|
+
|
|
40
|
+
let first = deliver_pending_message(
|
|
41
|
+
&case.workspace,
|
|
42
|
+
&case.store,
|
|
43
|
+
&transport,
|
|
44
|
+
&message_id,
|
|
45
|
+
&case.event_log,
|
|
46
|
+
&case.state,
|
|
47
|
+
)
|
|
48
|
+
.expect("first retry after error");
|
|
49
|
+
assert!(
|
|
50
|
+
first.ok || first.status == DeliveryStatus::AlreadyDelivered,
|
|
51
|
+
"{first:?}"
|
|
52
|
+
);
|
|
53
|
+
assert_eq!(
|
|
54
|
+
transport.inject_count(),
|
|
55
|
+
1,
|
|
56
|
+
"first claim winner injects once"
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
case.store
|
|
60
|
+
.mark(&message_id, "target_resolved", Some("probe_failed"))
|
|
61
|
+
.unwrap();
|
|
62
|
+
let second = deliver_pending_message(
|
|
63
|
+
&case.workspace,
|
|
64
|
+
&case.store,
|
|
65
|
+
&transport,
|
|
66
|
+
&message_id,
|
|
67
|
+
&case.event_log,
|
|
68
|
+
&case.state,
|
|
69
|
+
)
|
|
70
|
+
.expect("second retry after plant");
|
|
71
|
+
assert_eq!(
|
|
72
|
+
transport.inject_count(),
|
|
73
|
+
1,
|
|
74
|
+
"token already in pane must refuse the planted second inject: {second:?}"
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
#[test]
|
|
79
|
+
fn observer_state_save_failure_after_enter_does_not_return_err() {
|
|
80
|
+
let case = WorkerCase::new("observer-degrade", "fake");
|
|
81
|
+
let transport = RecordingTransport::new("");
|
|
82
|
+
let message_id = case.seed_leader_to_worker("observer canary");
|
|
83
|
+
poison_state_file(&case.workspace);
|
|
84
|
+
|
|
85
|
+
let outcome = deliver_pending_message(
|
|
86
|
+
&case.workspace,
|
|
87
|
+
&case.store,
|
|
88
|
+
&transport,
|
|
89
|
+
&message_id,
|
|
90
|
+
&case.event_log,
|
|
91
|
+
&case.state,
|
|
92
|
+
)
|
|
93
|
+
.expect("physical Enter already sent; observer save must not reverse that into Err");
|
|
94
|
+
assert_eq!(transport.inject_count(), 1);
|
|
95
|
+
assert!(
|
|
96
|
+
case.events().contains("delivery.post_submit_state_degraded")
|
|
97
|
+
|| outcome.ok
|
|
98
|
+
|| outcome.status == DeliveryStatus::AlreadyDelivered,
|
|
99
|
+
"observer/state save failure must degrade, not reopen inject. outcome={outcome:?} events={}",
|
|
100
|
+
case.events()
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#[test]
|
|
105
|
+
fn pane_token_refuses_second_inject_for_claude_grok_cursor() {
|
|
106
|
+
for provider in ["claude", "grok", "cursor"] {
|
|
107
|
+
let case = WorkerCase::new(&format!("token-{provider}"), provider);
|
|
108
|
+
let transport = RecordingTransport::new("");
|
|
109
|
+
let message_id = case.seed_leader_to_worker(&format!("{provider} canary"));
|
|
110
|
+
let first = deliver_pending_message(
|
|
111
|
+
&case.workspace,
|
|
112
|
+
&case.store,
|
|
113
|
+
&transport,
|
|
114
|
+
&message_id,
|
|
115
|
+
&case.event_log,
|
|
116
|
+
&case.state,
|
|
117
|
+
)
|
|
118
|
+
.expect("first inject");
|
|
119
|
+
assert_eq!(
|
|
120
|
+
transport.inject_count(),
|
|
121
|
+
1,
|
|
122
|
+
"provider={provider} first inject failed: {first:?}"
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
case.store
|
|
126
|
+
.mark(&message_id, "target_resolved", Some("probe_failed"))
|
|
127
|
+
.unwrap();
|
|
128
|
+
let second = deliver_pending_message(
|
|
129
|
+
&case.workspace,
|
|
130
|
+
&case.store,
|
|
131
|
+
&transport,
|
|
132
|
+
&message_id,
|
|
133
|
+
&case.event_log,
|
|
134
|
+
&case.state,
|
|
135
|
+
)
|
|
136
|
+
.expect("second deliver");
|
|
137
|
+
assert_eq!(
|
|
138
|
+
transport.inject_count(),
|
|
139
|
+
1,
|
|
140
|
+
"provider={provider} pane token must refuse second inject: {second:?}"
|
|
141
|
+
);
|
|
142
|
+
assert!(
|
|
143
|
+
second.status == DeliveryStatus::AlreadyDelivered
|
|
144
|
+
|| second.reason == Some(DeliveryRefusal::MessageAlreadyClaimed)
|
|
145
|
+
|| case.events().contains("delivery.duplicate_token_refused"),
|
|
146
|
+
"provider={provider} missing duplicate refuse. outcome={second:?} events={}",
|
|
147
|
+
case.events()
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#[test]
|
|
153
|
+
fn transcript_token_refuses_second_inject() {
|
|
154
|
+
let case = WorkerCase::new("transcript", "claude");
|
|
155
|
+
let message_id = case.seed_leader_to_worker("transcript canary");
|
|
156
|
+
std::fs::write(
|
|
157
|
+
&case.rollout,
|
|
158
|
+
format!("user\n[team-agent-token:{message_id}]\n"),
|
|
159
|
+
)
|
|
160
|
+
.unwrap();
|
|
161
|
+
let transport = RecordingTransport::new("unrelated pane text");
|
|
162
|
+
case.store
|
|
163
|
+
.mark(&message_id, "target_resolved", Some("probe_failed"))
|
|
164
|
+
.unwrap();
|
|
165
|
+
let outcome = deliver_pending_message(
|
|
166
|
+
&case.workspace,
|
|
167
|
+
&case.store,
|
|
168
|
+
&transport,
|
|
169
|
+
&message_id,
|
|
170
|
+
&case.event_log,
|
|
171
|
+
&case.state,
|
|
172
|
+
)
|
|
173
|
+
.expect("transcript hit");
|
|
174
|
+
assert_eq!(
|
|
175
|
+
transport.inject_count(),
|
|
176
|
+
0,
|
|
177
|
+
"transcript token must refuse inject: {outcome:?}"
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
#[test]
|
|
182
|
+
fn fallback_already_delivered_uses_live_status_set() {
|
|
183
|
+
let workspace = tmp_ws("fallback-status");
|
|
184
|
+
let store = MessageStore::open(&workspace).unwrap();
|
|
185
|
+
let mid = store
|
|
186
|
+
.create_message(None, "w1", "leader", "fallback canary", None, false, None)
|
|
187
|
+
.unwrap();
|
|
188
|
+
store
|
|
189
|
+
.mark(&mid, "submitted_pending_acceptance", None)
|
|
190
|
+
.unwrap();
|
|
191
|
+
|
|
192
|
+
let event_log = EventLog::new(&workspace);
|
|
193
|
+
let state = serde_json::json!({
|
|
194
|
+
"active_team_key": "t",
|
|
195
|
+
"leader_receiver": {"pane_id": "%leader", "status": "attached"}
|
|
196
|
+
});
|
|
197
|
+
crate::state::persist::save_runtime_state(&workspace, &state).unwrap();
|
|
198
|
+
let outcome = deliver_to_leader_fallback_pane(
|
|
199
|
+
&workspace,
|
|
200
|
+
&state,
|
|
201
|
+
&mid,
|
|
202
|
+
None,
|
|
203
|
+
"fallback canary",
|
|
204
|
+
false,
|
|
205
|
+
Some("primary failed"),
|
|
206
|
+
&event_log,
|
|
207
|
+
)
|
|
208
|
+
.expect("fallback short-circuit");
|
|
209
|
+
assert_eq!(outcome.status, DeliveryStatus::AlreadyDelivered);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
struct WorkerCase {
|
|
213
|
+
workspace: PathBuf,
|
|
214
|
+
rollout: PathBuf,
|
|
215
|
+
store: MessageStore,
|
|
216
|
+
event_log: EventLog,
|
|
217
|
+
state: serde_json::Value,
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
impl WorkerCase {
|
|
221
|
+
fn new(tag: &str, provider: &str) -> Self {
|
|
222
|
+
let workspace = tmp_ws(&format!("dup-inject-{tag}"));
|
|
223
|
+
let rollout = workspace.join(format!("{provider}-rollout.jsonl"));
|
|
224
|
+
std::fs::write(&rollout, "").unwrap();
|
|
225
|
+
let state = serde_json::json!({
|
|
226
|
+
"active_team_key": "dup-team",
|
|
227
|
+
"session_name": "team-dup",
|
|
228
|
+
"agents": {
|
|
229
|
+
"w1": {
|
|
230
|
+
"pane_id": "%w1",
|
|
231
|
+
"window": "w1",
|
|
232
|
+
"provider": provider,
|
|
233
|
+
"rollout_path": rollout,
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
crate::state::persist::save_runtime_state(&workspace, &state).unwrap();
|
|
238
|
+
let store = MessageStore::open(&workspace).unwrap();
|
|
239
|
+
let event_log = EventLog::new(&workspace);
|
|
240
|
+
Self {
|
|
241
|
+
workspace,
|
|
242
|
+
rollout,
|
|
243
|
+
store,
|
|
244
|
+
event_log,
|
|
245
|
+
state,
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
fn seed_leader_to_worker(&self, content: &str) -> String {
|
|
250
|
+
self.store
|
|
251
|
+
.create_message(None, "leader", "w1", content, None, false, None)
|
|
252
|
+
.unwrap()
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
fn events(&self) -> String {
|
|
256
|
+
self.event_log
|
|
257
|
+
.tail(0)
|
|
258
|
+
.unwrap()
|
|
259
|
+
.into_iter()
|
|
260
|
+
.map(|event| event.to_string())
|
|
261
|
+
.collect::<Vec<_>>()
|
|
262
|
+
.join("\n")
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
fn poison_state_file(workspace: &Path) {
|
|
267
|
+
let path = crate::state::persist::runtime_state_path(workspace);
|
|
268
|
+
let _ = std::fs::remove_file(&path);
|
|
269
|
+
std::fs::create_dir_all(&path).unwrap();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
#[derive(Clone)]
|
|
273
|
+
struct RecordingTransport {
|
|
274
|
+
injects: Arc<AtomicUsize>,
|
|
275
|
+
capture: Arc<Mutex<String>>,
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
impl RecordingTransport {
|
|
279
|
+
fn new(capture: &str) -> Self {
|
|
280
|
+
Self {
|
|
281
|
+
injects: Arc::new(AtomicUsize::new(0)),
|
|
282
|
+
capture: Arc::new(Mutex::new(capture.to_string())),
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
fn inject_count(&self) -> usize {
|
|
287
|
+
self.injects.load(Ordering::Relaxed)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
impl Transport for RecordingTransport {
|
|
292
|
+
fn kind(&self) -> BackendKind {
|
|
293
|
+
BackendKind::Tmux
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
fn spawn_first(
|
|
297
|
+
&self,
|
|
298
|
+
_session: &SessionName,
|
|
299
|
+
_window: &WindowName,
|
|
300
|
+
_argv: &[String],
|
|
301
|
+
_cwd: &Path,
|
|
302
|
+
_env: &BTreeMap<String, String>,
|
|
303
|
+
) -> Result<SpawnResult, TransportError> {
|
|
304
|
+
unreachable!("dup-inject tests do not spawn")
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
fn spawn_into(
|
|
308
|
+
&self,
|
|
309
|
+
_session: &SessionName,
|
|
310
|
+
_window: &WindowName,
|
|
311
|
+
_argv: &[String],
|
|
312
|
+
_cwd: &Path,
|
|
313
|
+
_env: &BTreeMap<String, String>,
|
|
314
|
+
) -> Result<SpawnResult, TransportError> {
|
|
315
|
+
unreachable!("dup-inject tests do not spawn")
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
fn inject(
|
|
319
|
+
&self,
|
|
320
|
+
_target: &Target,
|
|
321
|
+
payload: &InjectPayload,
|
|
322
|
+
_submit: Key,
|
|
323
|
+
_bracketed_paste: bool,
|
|
324
|
+
) -> Result<InjectReport, TransportError> {
|
|
325
|
+
self.injects.fetch_add(1, Ordering::Relaxed);
|
|
326
|
+
if let Some(text) = payload.text() {
|
|
327
|
+
self.capture.lock().unwrap().push_str(text);
|
|
328
|
+
}
|
|
329
|
+
Ok(InjectReport {
|
|
330
|
+
stage_reached: InjectStage::Submit,
|
|
331
|
+
inject_verification: InjectVerification::CaptureContainsToken,
|
|
332
|
+
submit_verification: SubmitVerification::EnterSentWithoutPlaceholderCheck,
|
|
333
|
+
turn_verification: TurnVerification::NotYetObserved,
|
|
334
|
+
attempts: 1,
|
|
335
|
+
submit_diagnostics: None,
|
|
336
|
+
})
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
fn send_keys(&self, _target: &Target, _keys: &[Key]) -> Result<(), TransportError> {
|
|
340
|
+
Ok(())
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
fn capture(
|
|
344
|
+
&self,
|
|
345
|
+
_target: &Target,
|
|
346
|
+
range: CaptureRange,
|
|
347
|
+
) -> Result<CapturedText, TransportError> {
|
|
348
|
+
Ok(CapturedText {
|
|
349
|
+
text: self.capture.lock().unwrap().clone(),
|
|
350
|
+
range,
|
|
351
|
+
})
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
fn query(&self, _target: &Target, _field: PaneField) -> Result<Option<String>, TransportError> {
|
|
355
|
+
Ok(None)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
fn liveness(&self, _pane: &PaneId) -> Result<PaneLiveness, TransportError> {
|
|
359
|
+
Ok(PaneLiveness::Live)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
|
|
363
|
+
Ok(vec![PaneInfo {
|
|
364
|
+
pane_id: PaneId::new("%w1"),
|
|
365
|
+
session: SessionName::new("team-dup"),
|
|
366
|
+
window_index: None,
|
|
367
|
+
window_name: Some(WindowName::new("w1")),
|
|
368
|
+
pane_index: None,
|
|
369
|
+
tty: None,
|
|
370
|
+
current_command: Some("sleep".to_string()),
|
|
371
|
+
current_path: None,
|
|
372
|
+
active: true,
|
|
373
|
+
pane_pid: None,
|
|
374
|
+
leader_env: BTreeMap::new(),
|
|
375
|
+
}])
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
|
|
379
|
+
Ok(true)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
fn list_windows(&self, _session: &SessionName) -> Result<Vec<WindowName>, TransportError> {
|
|
383
|
+
Ok(vec![WindowName::new("w1")])
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
fn set_session_env(
|
|
387
|
+
&self,
|
|
388
|
+
_session: &SessionName,
|
|
389
|
+
_key: &str,
|
|
390
|
+
_value: &str,
|
|
391
|
+
) -> Result<SetEnvOutcome, TransportError> {
|
|
392
|
+
Ok(SetEnvOutcome::Applied)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
|
|
396
|
+
Ok(())
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
fn kill_window(&self, _target: &Target) -> Result<(), TransportError> {
|
|
400
|
+
Ok(())
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
fn attach_session(&self, _session: &SessionName) -> Result<AttachOutcome, TransportError> {
|
|
404
|
+
Ok(AttachOutcome::Attached)
|
|
405
|
+
}
|
|
406
|
+
}
|