@team-agent/installer 0.5.77 → 0.5.78
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 +161 -29
- package/crates/team-agent/src/cli/diagnose.rs +333 -68
- package/crates/team-agent/src/cli/emit.rs +24 -8
- package/crates/team-agent/src/cli/mod.rs +543 -98
- package/crates/team-agent/src/cli/models.rs +362 -32
- package/crates/team-agent/src/cli/named_address.rs +19 -5
- package/crates/team-agent/src/cli/profile.rs +35 -4
- package/crates/team-agent/src/cli/spec.rs +7 -2
- package/crates/team-agent/src/cli/status_port/compact.rs +12 -8
- package/crates/team-agent/src/cli/status_port/snapshot.rs +15 -12
- package/crates/team-agent/src/cli/status_port/store.rs +1 -1
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +62 -8
- package/crates/team-agent/src/cli/tests/named_address.rs +70 -0
- package/crates/team-agent/src/cli/tests/verb_profile.rs +81 -6
- package/crates/team-agent/src/cli/types.rs +1 -0
- package/crates/team-agent/src/leader/lease.rs +492 -33
- package/crates/team-agent/src/leader/owner_bind.rs +40 -12
- package/crates/team-agent/src/leader/rediscover/tests.rs +65 -0
- package/crates/team-agent/src/leader/rediscover.rs +36 -6
- package/crates/team-agent/src/leader/start.rs +151 -14
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +15 -9
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +28 -227
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +0 -18
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +94 -227
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +59 -10
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +65 -18
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1449 -282
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +349 -4
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +11 -11
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +35 -3
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +16 -0
- package/crates/team-agent/src/lifecycle/launch.rs +30 -7
- package/crates/team-agent/src/lifecycle/profile_launch.rs +37 -3
- package/crates/team-agent/src/lifecycle/restart/agent.rs +112 -100
- package/crates/team-agent/src/lifecycle/restart/common.rs +82 -52
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +996 -66
- package/crates/team-agent/src/lifecycle/restart.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +15 -0
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +5 -0
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +11 -13
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +119 -4
- package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +12 -0
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +22 -0
- package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +12 -0
- package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +265 -198
- package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +14 -30
- package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +2 -5
- package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +10 -0
- package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +58 -35
- package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +10 -0
- package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +9 -0
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +3 -2
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +0 -168
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +145 -212
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +16 -2
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +54 -9
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +78 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +51 -10
- package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +50 -7
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +137 -0
- package/crates/team-agent/src/lifecycle/types.rs +8 -0
- package/crates/team-agent/src/mcp_server/wire.rs +140 -11
- package/crates/team-agent/src/messaging/delivery.rs +67 -8
- package/crates/team-agent/src/messaging/leader_channel.rs +56 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +10 -2
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +2 -5
- package/crates/team-agent/src/state/persist.rs +97 -1
- package/crates/team-agent/src/state/repository.rs +29 -0
- package/crates/team-agent/src/test_support/models_timeout_fixture.rs +43 -21
- package/crates/team-agent/src/tmux_backend/tests.rs +121 -4
- package/crates/team-agent/src/tmux_backend.rs +208 -118
- package/crates/team-agent/src/transport.rs +72 -0
- package/crates/team-agent/src/transport_factory.rs +53 -0
- package/package.json +4 -5
- package/skills/team-agent/SKILL.md +45 -17
- package/skills/team-agent/command-coverage.json +4 -29
- package/npm/check-skill-links.mjs +0 -88
- package/skills/team-agent/references/team-agent-operator.md +0 -672
|
@@ -185,7 +185,7 @@ fn run_stdio_loop<R: BufRead, W: Write>(
|
|
|
185
185
|
let mut report = ServerRunReport::default();
|
|
186
186
|
let marker = McpServerLifecycleMarker::from_env(workspace);
|
|
187
187
|
marker.write_started();
|
|
188
|
-
let result = run_stdio_loop_inner(&tools, reader, &mut writer, &mut report);
|
|
188
|
+
let result = run_stdio_loop_inner(&tools, reader, &mut writer, &mut report, &marker);
|
|
189
189
|
match &result {
|
|
190
190
|
Ok(_) => marker.write_exit("stdin_eof", None),
|
|
191
191
|
Err(error) => marker.write_exit("fatal_error", Some(&error.to_string())),
|
|
@@ -198,16 +198,20 @@ fn run_stdio_loop_inner<R: BufRead, W: Write>(
|
|
|
198
198
|
reader: R,
|
|
199
199
|
writer: &mut W,
|
|
200
200
|
report: &mut ServerRunReport,
|
|
201
|
+
marker: &McpServerLifecycleMarker,
|
|
201
202
|
) -> Result<ServerRunReport, McpError> {
|
|
202
203
|
for line in reader.lines() {
|
|
203
204
|
let line = line?;
|
|
204
205
|
report.requests_read = report.requests_read.saturating_add(1);
|
|
205
|
-
let frame = handle_stdin_line(tools, &line, report)?;
|
|
206
|
+
let (frame, tools_list_success) = handle_stdin_line(tools, &line, report)?;
|
|
206
207
|
if let Some(value) = frame {
|
|
207
208
|
let value = crate::redaction::redact_external_value(&value);
|
|
208
209
|
serde_json::to_writer(&mut *writer, &value)?;
|
|
209
210
|
writer.write_all(b"\n")?;
|
|
210
211
|
writer.flush()?;
|
|
212
|
+
if tools_list_success {
|
|
213
|
+
marker.write_tools_list_response_written();
|
|
214
|
+
}
|
|
211
215
|
report.responses_written = report.responses_written.saturating_add(1);
|
|
212
216
|
}
|
|
213
217
|
}
|
|
@@ -219,29 +223,34 @@ fn handle_stdin_line(
|
|
|
219
223
|
tools: &TeamOrchestratorTools,
|
|
220
224
|
line: &str,
|
|
221
225
|
report: &mut ServerRunReport,
|
|
222
|
-
) -> Result<Option<Value>, McpError> {
|
|
226
|
+
) -> Result<(Option<Value>, bool), McpError> {
|
|
223
227
|
let request: Value = match serde_json::from_str(line) {
|
|
224
228
|
Ok(value) => value,
|
|
225
229
|
Err(err) => {
|
|
226
230
|
report.error_frames = report.error_frames.saturating_add(1);
|
|
227
|
-
return Ok(
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
231
|
+
return Ok((
|
|
232
|
+
Some(error_response_value(
|
|
233
|
+
RpcId::Null,
|
|
234
|
+
-32700,
|
|
235
|
+
format!("parse error: {err}"),
|
|
236
|
+
)),
|
|
237
|
+
false,
|
|
238
|
+
));
|
|
232
239
|
}
|
|
233
240
|
};
|
|
234
241
|
if request.get("jsonrpc").and_then(Value::as_str) == Some("2.0") {
|
|
242
|
+
let is_tools_list = request.get("method").and_then(Value::as_str) == Some("tools/list");
|
|
235
243
|
match handle_mcp(tools, &request)? {
|
|
236
244
|
Some(response) => {
|
|
245
|
+
let tools_list_success = is_tools_list && response.error.is_none();
|
|
237
246
|
if response.error.is_some() {
|
|
238
247
|
report.error_frames = report.error_frames.saturating_add(1);
|
|
239
248
|
}
|
|
240
|
-
Ok(Some(serde_json::to_value(response)?))
|
|
249
|
+
Ok((Some(serde_json::to_value(response)?), tools_list_success))
|
|
241
250
|
}
|
|
242
251
|
None => {
|
|
243
252
|
report.notifications_skipped = report.notifications_skipped.saturating_add(1);
|
|
244
|
-
Ok(None)
|
|
253
|
+
Ok((None, false))
|
|
245
254
|
}
|
|
246
255
|
}
|
|
247
256
|
} else {
|
|
@@ -252,7 +261,7 @@ fn handle_stdin_line(
|
|
|
252
261
|
err.to_envelope()
|
|
253
262
|
}
|
|
254
263
|
};
|
|
255
|
-
Ok(Some(value))
|
|
264
|
+
Ok((Some(value), false))
|
|
256
265
|
}
|
|
257
266
|
}
|
|
258
267
|
|
|
@@ -301,6 +310,23 @@ impl McpServerLifecycleMarker {
|
|
|
301
310
|
self.write("mcp.server_started", None, None);
|
|
302
311
|
}
|
|
303
312
|
|
|
313
|
+
fn write_tools_list_response_written(&self) {
|
|
314
|
+
let _ = EventLog::new(&self.workspace).write(
|
|
315
|
+
"mcp.tools_list_response_written",
|
|
316
|
+
serde_json::json!({
|
|
317
|
+
"method": "tools/list",
|
|
318
|
+
"success": true,
|
|
319
|
+
"agent_id": self.agent_id.as_deref(),
|
|
320
|
+
"owner_team_id": self.owner_team_id.as_deref(),
|
|
321
|
+
"workspace": self.workspace.display().to_string(),
|
|
322
|
+
"pid": self.pid,
|
|
323
|
+
"ppid": self.ppid,
|
|
324
|
+
"spawn_epoch": null,
|
|
325
|
+
"spawn_epoch_source": "unavailable",
|
|
326
|
+
}),
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
304
330
|
fn write_exit(&self, reason: &str, error: Option<&str>) {
|
|
305
331
|
self.write("mcp.server_exit", Some(reason), error);
|
|
306
332
|
}
|
|
@@ -798,6 +824,109 @@ mod e23_lifecycle_marker_tests {
|
|
|
798
824
|
path
|
|
799
825
|
}
|
|
800
826
|
|
|
827
|
+
#[test]
|
|
828
|
+
fn stdio_loop_logs_tools_list_after_flush_once_per_response() {
|
|
829
|
+
let ws = marker_ws("tools-list-success");
|
|
830
|
+
let input = std::io::Cursor::new(
|
|
831
|
+
br#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}
|
|
832
|
+
{"jsonrpc":"2.0","id":2,"method":"tools/list"}
|
|
833
|
+
"#.to_vec(),
|
|
834
|
+
);
|
|
835
|
+
let mut output = Vec::new();
|
|
836
|
+
|
|
837
|
+
let report = run_stdio_loop(&ws, input, &mut output).unwrap();
|
|
838
|
+
|
|
839
|
+
assert_eq!(report.responses_written, 2);
|
|
840
|
+
assert_eq!(output.iter().filter(|byte| **byte == b'\n').count(), 2);
|
|
841
|
+
let events = EventLog::new(&ws).tail(0).unwrap();
|
|
842
|
+
let tool_events: Vec<_> = events
|
|
843
|
+
.iter()
|
|
844
|
+
.filter(|event| event["event"] == serde_json::json!("mcp.tools_list_response_written"))
|
|
845
|
+
.collect();
|
|
846
|
+
assert_eq!(tool_events.len(), 2, "one event per successful tools/list response");
|
|
847
|
+
for event in tool_events {
|
|
848
|
+
assert_eq!(event["method"], serde_json::json!("tools/list"));
|
|
849
|
+
assert_eq!(event["success"], serde_json::json!(true));
|
|
850
|
+
assert_eq!(event["workspace"], serde_json::json!(ws.display().to_string()));
|
|
851
|
+
assert_eq!(event["pid"], serde_json::json!(std::process::id()));
|
|
852
|
+
assert_eq!(event["spawn_epoch"], serde_json::Value::Null);
|
|
853
|
+
}
|
|
854
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
#[test]
|
|
858
|
+
fn stdio_loop_does_not_log_tools_list_when_write_fails() {
|
|
859
|
+
struct FailingWriter;
|
|
860
|
+
impl Write for FailingWriter {
|
|
861
|
+
fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
|
|
862
|
+
Err(std::io::Error::other("simulated stdout write failure"))
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
fn flush(&mut self) -> std::io::Result<()> {
|
|
866
|
+
Ok(())
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
let ws = marker_ws("tools-list-write-fail");
|
|
871
|
+
let input = std::io::Cursor::new(
|
|
872
|
+
br#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}"#.to_vec(),
|
|
873
|
+
);
|
|
874
|
+
let error = run_stdio_loop(&ws, input, FailingWriter).expect_err("write must fail");
|
|
875
|
+
assert!(error.to_string().contains("simulated stdout write failure"));
|
|
876
|
+
let events = EventLog::new(&ws).tail(0).unwrap();
|
|
877
|
+
assert!(!events
|
|
878
|
+
.iter()
|
|
879
|
+
.any(|event| event["event"] == serde_json::json!("mcp.tools_list_response_written")));
|
|
880
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
#[test]
|
|
884
|
+
fn stdio_loop_does_not_log_tools_list_when_flush_fails() {
|
|
885
|
+
struct FlushFailWriter(Vec<u8>);
|
|
886
|
+
impl Write for FlushFailWriter {
|
|
887
|
+
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
|
888
|
+
self.0.extend_from_slice(buf);
|
|
889
|
+
Ok(buf.len())
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
fn flush(&mut self) -> std::io::Result<()> {
|
|
893
|
+
Err(std::io::Error::other("simulated stdout flush failure"))
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
let ws = marker_ws("tools-list-flush-fail");
|
|
898
|
+
let input = std::io::Cursor::new(
|
|
899
|
+
br#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}"#.to_vec(),
|
|
900
|
+
);
|
|
901
|
+
let error = run_stdio_loop(&ws, input, FlushFailWriter(Vec::new()))
|
|
902
|
+
.expect_err("flush must fail");
|
|
903
|
+
assert!(error.to_string().contains("simulated stdout flush failure"));
|
|
904
|
+
let events = EventLog::new(&ws).tail(0).unwrap();
|
|
905
|
+
assert!(!events
|
|
906
|
+
.iter()
|
|
907
|
+
.any(|event| event["event"] == serde_json::json!("mcp.tools_list_response_written")));
|
|
908
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
#[test]
|
|
912
|
+
fn stdio_loop_does_not_log_tools_list_for_initialize_or_notification() {
|
|
913
|
+
let ws = marker_ws("tools-list-not-applicable");
|
|
914
|
+
let input = std::io::Cursor::new(
|
|
915
|
+
br#"{"jsonrpc":"2.0","id":1,"method":"initialize"}
|
|
916
|
+
{"jsonrpc":"2.0","method":"notifications/initialized"}
|
|
917
|
+
"#.to_vec(),
|
|
918
|
+
);
|
|
919
|
+
let mut output = Vec::new();
|
|
920
|
+
|
|
921
|
+
run_stdio_loop(&ws, input, &mut output).unwrap();
|
|
922
|
+
|
|
923
|
+
let events = EventLog::new(&ws).tail(0).unwrap();
|
|
924
|
+
assert!(!events
|
|
925
|
+
.iter()
|
|
926
|
+
.any(|event| event["event"] == serde_json::json!("mcp.tools_list_response_written")));
|
|
927
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
928
|
+
}
|
|
929
|
+
|
|
801
930
|
#[test]
|
|
802
931
|
fn stdio_loop_writes_started_and_stdin_eof_exit_markers() {
|
|
803
932
|
let ws = marker_ws("eof");
|
|
@@ -31,9 +31,10 @@ use crate::provider::wire::{
|
|
|
31
31
|
is_claude_family, parse_canonical_provider, parse_provider, provider_wire,
|
|
32
32
|
};
|
|
33
33
|
use crate::transport::{
|
|
34
|
-
submit_verification_wire, turn_verification_wire,
|
|
35
|
-
InjectPayload, InjectReport, InjectVerification, InputSurfaceProbe, Key,
|
|
36
|
-
|
|
34
|
+
submit_verification_wire, turn_verification_wire, BusySignalKind, CaptureRange,
|
|
35
|
+
CaptureSampleOutcome, InjectPayload, InjectReport, InjectVerification, InputSurfaceProbe, Key,
|
|
36
|
+
PaneId, PaneInfo, QueryOutcome, SessionName, SubmitConsumptionReason, SubmitObserver,
|
|
37
|
+
SubmitVerification, Target, Transport, WindowName,
|
|
37
38
|
};
|
|
38
39
|
|
|
39
40
|
use super::helpers::{message_exists, MessageStatusShadow};
|
|
@@ -572,11 +573,11 @@ pub fn deliver_pending_message(
|
|
|
572
573
|
canonical_owner_team_id.as_deref(),
|
|
573
574
|
resolved.metadata.as_ref(),
|
|
574
575
|
);
|
|
575
|
-
let
|
|
576
|
+
let recipient_cursor = recipient_is_cursor_agent(state, &message.recipient);
|
|
576
577
|
let inject_attempt = crate::tmux_backend::with_paste_to_submit_floor(
|
|
577
578
|
paste_to_submit_floor_for_recipient(state, &message.recipient),
|
|
578
579
|
|| {
|
|
579
|
-
crate::tmux_backend::with_cursor_single_enter(
|
|
580
|
+
crate::tmux_backend::with_cursor_single_enter(recipient_cursor, || {
|
|
580
581
|
transport.inject_with_submit_observer(
|
|
581
582
|
&target,
|
|
582
583
|
&payload,
|
|
@@ -592,9 +593,7 @@ pub fn deliver_pending_message(
|
|
|
592
593
|
// grok 忙时第一次回车只入显式队列。默认 send-now:只重按回车顶出去。
|
|
593
594
|
// 无 `Enter:send now` 时零次额外按键,claude/codex 行为不变。
|
|
594
595
|
// cursor:第二下 Enter 打断进行中回合,不走 flush。
|
|
595
|
-
if !crate::provider::submit_now::keep_provider_queue_requested()
|
|
596
|
-
&& recipient_allows_explicit_queue_flush(state, &message.recipient)
|
|
597
|
-
{
|
|
596
|
+
if !crate::provider::submit_now::keep_provider_queue_requested() && !recipient_cursor {
|
|
598
597
|
match crate::provider::submit_now::flush_explicit_queue(transport, &target) {
|
|
599
598
|
Ok(flush) if flush.extra_enters > 0 => {
|
|
600
599
|
let _ = event_log.write(
|
|
@@ -1999,6 +1998,36 @@ fn merge_inject_readback_fields(
|
|
|
1999
1998
|
.map(|d| d.last_capture_outcome.as_str())
|
|
2000
1999
|
.unwrap_or(CaptureSampleOutcome::NotAttempted.as_str())),
|
|
2001
2000
|
);
|
|
2001
|
+
obj.insert(
|
|
2002
|
+
"consumption_reason".to_string(),
|
|
2003
|
+
serde_json::json!(diag
|
|
2004
|
+
.map(|d| d.consumption_reason.as_str())
|
|
2005
|
+
.unwrap_or("unknown")),
|
|
2006
|
+
);
|
|
2007
|
+
obj.insert(
|
|
2008
|
+
"before_busy_signal_kind".to_string(),
|
|
2009
|
+
serde_json::json!(diag.and_then(|d| d.before_busy_signal_kind.map(|kind| kind.as_str()))),
|
|
2010
|
+
);
|
|
2011
|
+
obj.insert(
|
|
2012
|
+
"busy_signal_kind".to_string(),
|
|
2013
|
+
serde_json::json!(diag.and_then(|d| d.busy_signal_kind.map(|kind| kind.as_str()))),
|
|
2014
|
+
);
|
|
2015
|
+
obj.insert(
|
|
2016
|
+
"busy_line_from_bottom".to_string(),
|
|
2017
|
+
serde_json::json!(diag.and_then(|d| d.busy_line_from_bottom)),
|
|
2018
|
+
);
|
|
2019
|
+
obj.insert(
|
|
2020
|
+
"current_marker_in_bottom_15".to_string(),
|
|
2021
|
+
serde_json::json!(diag.and_then(|d| d.current_marker_in_bottom_15)),
|
|
2022
|
+
);
|
|
2023
|
+
obj.insert(
|
|
2024
|
+
"paste_identity_in_composer".to_string(),
|
|
2025
|
+
serde_json::json!(diag.and_then(|d| d.paste_identity_in_composer)),
|
|
2026
|
+
);
|
|
2027
|
+
obj.insert(
|
|
2028
|
+
"consumption_from_capture_result".to_string(),
|
|
2029
|
+
serde_json::json!(diag.and_then(|d| d.consumption_from_capture_result)),
|
|
2030
|
+
);
|
|
2002
2031
|
obj.insert(
|
|
2003
2032
|
"pane_command_basename".to_string(),
|
|
2004
2033
|
diag.and_then(|d| d.pane_command_basename.as_deref())
|
|
@@ -2357,6 +2386,13 @@ fn recipient_provider(state: &serde_json::Value, recipient: &str) -> Option<Prov
|
|
|
2357
2386
|
.and_then(parse_canonical_provider)
|
|
2358
2387
|
}
|
|
2359
2388
|
|
|
2389
|
+
fn recipient_is_cursor_agent(state: &serde_json::Value, recipient: &str) -> bool {
|
|
2390
|
+
matches!(
|
|
2391
|
+
recipient_provider(state, recipient),
|
|
2392
|
+
Some(Provider::CursorAgent)
|
|
2393
|
+
)
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2360
2396
|
/// ---
|
|
2361
2397
|
/// purpose: paste→Enter 地板按收件人 provider 选择
|
|
2362
2398
|
/// contract: CursorAgent 与 Grok 为 1s;其余(含 claude)为 ZERO
|
|
@@ -3391,6 +3427,13 @@ mod paste_floor_tests {
|
|
|
3391
3427
|
turn_verification: crate::transport::TurnVerification::NotYetObserved,
|
|
3392
3428
|
attempts: 1,
|
|
3393
3429
|
submit_diagnostics: Some(crate::transport::SubmitDiagnostics {
|
|
3430
|
+
consumption_reason: SubmitConsumptionReason::Unverified,
|
|
3431
|
+
before_busy_signal_kind: Some(BusySignalKind::Working),
|
|
3432
|
+
busy_signal_kind: Some(BusySignalKind::SpinnerGlyph),
|
|
3433
|
+
busy_line_from_bottom: Some(2),
|
|
3434
|
+
current_marker_in_bottom_15: Some(false),
|
|
3435
|
+
paste_identity_in_composer: None,
|
|
3436
|
+
consumption_from_capture_result: Some(false),
|
|
3394
3437
|
last_capture_outcome: CaptureSampleOutcome::Failed,
|
|
3395
3438
|
capture_err_count: 1,
|
|
3396
3439
|
capture_sample_count: 1,
|
|
@@ -3419,6 +3462,22 @@ mod paste_floor_tests {
|
|
|
3419
3462
|
None,
|
|
3420
3463
|
);
|
|
3421
3464
|
assert_eq!(event["last_capture_outcome"], serde_json::json!("failed"));
|
|
3465
|
+
assert_eq!(event["consumption_reason"], serde_json::json!("unverified"));
|
|
3466
|
+
assert_eq!(
|
|
3467
|
+
event["before_busy_signal_kind"],
|
|
3468
|
+
serde_json::json!("working")
|
|
3469
|
+
);
|
|
3470
|
+
assert_eq!(event["busy_signal_kind"], serde_json::json!("spinner_glyph"));
|
|
3471
|
+
assert_eq!(event["busy_line_from_bottom"], serde_json::json!(2));
|
|
3472
|
+
assert_eq!(
|
|
3473
|
+
event["current_marker_in_bottom_15"],
|
|
3474
|
+
serde_json::json!(false)
|
|
3475
|
+
);
|
|
3476
|
+
assert_eq!(event["paste_identity_in_composer"], serde_json::Value::Null);
|
|
3477
|
+
assert_eq!(
|
|
3478
|
+
event["consumption_from_capture_result"],
|
|
3479
|
+
serde_json::json!(false)
|
|
3480
|
+
);
|
|
3422
3481
|
assert_eq!(event["token_seen_after_paste"], serde_json::Value::Null);
|
|
3423
3482
|
assert_eq!(event["pane_command_basename"], serde_json::json!("grok"));
|
|
3424
3483
|
assert_eq!(event["input_surface"], serde_json::json!("input"));
|
|
@@ -229,3 +229,59 @@ fn string_field<'a>(value: &'a Value, key: &str) -> Option<&'a str> {
|
|
|
229
229
|
.and_then(Value::as_str)
|
|
230
230
|
.filter(|value| !value.is_empty())
|
|
231
231
|
}
|
|
232
|
+
|
|
233
|
+
#[cfg(test)]
|
|
234
|
+
mod claim_rework_live_tests {
|
|
235
|
+
use super::*;
|
|
236
|
+
use crate::transport::test_support::OfflineTransport;
|
|
237
|
+
use crate::transport::{PaneId, PaneInfo, SessionName};
|
|
238
|
+
use serde_json::json;
|
|
239
|
+
use std::path::PathBuf;
|
|
240
|
+
|
|
241
|
+
#[test]
|
|
242
|
+
fn pending_receiver_is_not_a_live_channel() {
|
|
243
|
+
let workspace = PathBuf::from("/tmp/claim-rework-a3-pending");
|
|
244
|
+
let receiver = json!({
|
|
245
|
+
"mode": "direct_tmux",
|
|
246
|
+
"status": "pending",
|
|
247
|
+
"pane_id": "%1",
|
|
248
|
+
"tmux_socket": "/tmp/ta-offline.sock"
|
|
249
|
+
});
|
|
250
|
+
let transport = OfflineTransport::default().with_tmux_endpoint("/tmp/ta-offline.sock");
|
|
251
|
+
match resolve_live_leader_channel(&workspace, &receiver, &transport) {
|
|
252
|
+
LeaderChannelResolution::Unbound(LeaderChannelUnbound::ReceiverNotAttached) => {}
|
|
253
|
+
other => panic!("pending seed must not be live; got {other:?}"),
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
#[test]
|
|
258
|
+
fn dead_pane_with_matching_endpoint_is_pane_not_live() {
|
|
259
|
+
let workspace = PathBuf::from("/tmp/claim-rework-a3-dead");
|
|
260
|
+
let endpoint = "/tmp/ta-claim-rework-a3.sock";
|
|
261
|
+
let receiver = json!({
|
|
262
|
+
"mode": "direct_tmux",
|
|
263
|
+
"status": "attached",
|
|
264
|
+
"pane_id": "%missing",
|
|
265
|
+
"tmux_socket": endpoint
|
|
266
|
+
});
|
|
267
|
+
let transport = OfflineTransport::default()
|
|
268
|
+
.with_tmux_endpoint(endpoint)
|
|
269
|
+
.with_targets(vec![PaneInfo {
|
|
270
|
+
pane_id: PaneId::new("%other"),
|
|
271
|
+
session: SessionName::new("team"),
|
|
272
|
+
window_index: None,
|
|
273
|
+
window_name: None,
|
|
274
|
+
pane_index: None,
|
|
275
|
+
tty: None,
|
|
276
|
+
current_command: None,
|
|
277
|
+
current_path: None,
|
|
278
|
+
active: true,
|
|
279
|
+
pane_pid: None,
|
|
280
|
+
leader_env: Default::default(),
|
|
281
|
+
}]);
|
|
282
|
+
match resolve_live_leader_channel(&workspace, &receiver, &transport) {
|
|
283
|
+
LeaderChannelResolution::Unbound(LeaderChannelUnbound::PaneNotLive) => {}
|
|
284
|
+
other => panic!("matching endpoint + missing pane must be PaneNotLive; got {other:?}"),
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
@@ -3376,10 +3376,18 @@ fn gate054_status_surfaces_pending_leader_notifications() {
|
|
|
3376
3376
|
entry.get("channel").and_then(serde_json::Value::as_str),
|
|
3377
3377
|
Some("rebind_required")
|
|
3378
3378
|
);
|
|
3379
|
-
|
|
3379
|
+
let action = entry
|
|
3380
3380
|
.get("action")
|
|
3381
3381
|
.and_then(serde_json::Value::as_str)
|
|
3382
|
-
.
|
|
3382
|
+
.unwrap_or("");
|
|
3383
|
+
assert!(
|
|
3384
|
+
action.contains("historical leader mailbox debt"),
|
|
3385
|
+
"historical debt action must stay debt-scoped; action={action}"
|
|
3386
|
+
);
|
|
3387
|
+
assert!(
|
|
3388
|
+
!action.contains("attach-leader") && !action.contains("takeover") && !action.contains("claim-leader"),
|
|
3389
|
+
"historical debt must not authorize bind commands; action={action}"
|
|
3390
|
+
);
|
|
3383
3391
|
}
|
|
3384
3392
|
|
|
3385
3393
|
#[test]
|
|
@@ -15,10 +15,8 @@
|
|
|
15
15
|
//! `.team/scripts/cursor_seat.sh` 实测一致:
|
|
16
16
|
//! `--trust --sandbox disabled --workspace <物理路径> [--force] [--model]`
|
|
17
17
|
//! Role 不入 argv,写 `<workspace>/.cursor/rules/*.mdc` + `alwaysApply: true`。
|
|
18
|
-
//! MCP 无 `--mcp-config`;身份必须写进
|
|
19
|
-
//! `provider-config/<id>/cursor/.cursor/mcp.json` 的 env 表
|
|
18
|
+
//! MCP 无 `--mcp-config`;身份必须写进 `.cursor/mcp.json` 的 env 表
|
|
20
19
|
//! (cursor 不把父进程 TEAM_AGENT_* 传给 MCP 子进程)。
|
|
21
|
-
//! spawn 把 `--workspace` 指到该工程根,并用 `--add-dir` 挂上真 workspace。
|
|
22
20
|
//!
|
|
23
21
|
//! 隐藏 flag `--system-prompt` / `--allowed-tools` 实测存在但 help 未列,
|
|
24
22
|
//! 随时可能消失。可以在注释里记录,代码里不许调。
|
|
@@ -75,8 +73,7 @@ pub(crate) fn cursor_agent_base_command(
|
|
|
75
73
|
let _ = effort;
|
|
76
74
|
// system_prompt 不入 argv(help 无 --rules / --append-system-prompt)。
|
|
77
75
|
// launch 写 `<workspace>/.cursor/rules/team-agent-role-<agent_id>.mdc`。
|
|
78
|
-
// mcp_config 也不入 argv(无 --mcp-config)。launch 写
|
|
79
|
-
// `.team/runtime/provider-config/<id>/cursor/.cursor/mcp.json`
|
|
76
|
+
// mcp_config 也不入 argv(无 --mcp-config)。launch 写 `.cursor/mcp.json`
|
|
80
77
|
// 并 `agent mcp enable team_orchestrator`。这里收下以免静默丢弃。
|
|
81
78
|
let _ = (adapter, auth_mode, mcp_config, managed_mcp_config, system_prompt);
|
|
82
79
|
argv.push("--workspace".to_string());
|
|
@@ -377,6 +377,7 @@ fn save_runtime_state_with_merge_options(
|
|
|
377
377
|
exact_owner_seed_to_clear,
|
|
378
378
|
None,
|
|
379
379
|
None,
|
|
380
|
+
None,
|
|
380
381
|
)
|
|
381
382
|
}
|
|
382
383
|
|
|
@@ -399,6 +400,7 @@ pub(crate) fn save_runtime_state_with_receiver_authority_and_expected(
|
|
|
399
400
|
None,
|
|
400
401
|
Some((expected_owner, expected_receiver)),
|
|
401
402
|
None,
|
|
403
|
+
None,
|
|
402
404
|
)
|
|
403
405
|
}
|
|
404
406
|
|
|
@@ -421,6 +423,38 @@ pub(crate) fn save_runtime_state_with_exact_owner_receiver_cleanup(
|
|
|
421
423
|
None,
|
|
422
424
|
None,
|
|
423
425
|
Some((team_key, expected_owner, expected_receiver)),
|
|
426
|
+
None,
|
|
427
|
+
)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
pub(crate) fn save_runtime_state_with_exact_owner_receiver_restore(
|
|
431
|
+
workspace: &Path,
|
|
432
|
+
state: &Value,
|
|
433
|
+
team_key: &str,
|
|
434
|
+
expected_owner: &Value,
|
|
435
|
+
expected_receiver: &Value,
|
|
436
|
+
previous_owner: &Value,
|
|
437
|
+
previous_receiver: &Value,
|
|
438
|
+
) -> Result<(), StateError> {
|
|
439
|
+
persist_runtime_state_with_merge_options_and_expected(
|
|
440
|
+
workspace,
|
|
441
|
+
state,
|
|
442
|
+
&[],
|
|
443
|
+
None,
|
|
444
|
+
&[],
|
|
445
|
+
None,
|
|
446
|
+
&[],
|
|
447
|
+
Some(team_key),
|
|
448
|
+
None,
|
|
449
|
+
None,
|
|
450
|
+
None,
|
|
451
|
+
Some((
|
|
452
|
+
team_key,
|
|
453
|
+
expected_owner,
|
|
454
|
+
expected_receiver,
|
|
455
|
+
previous_owner,
|
|
456
|
+
previous_receiver,
|
|
457
|
+
)),
|
|
424
458
|
)
|
|
425
459
|
}
|
|
426
460
|
|
|
@@ -436,6 +470,7 @@ fn persist_runtime_state_with_merge_options_and_expected(
|
|
|
436
470
|
exact_owner_seed_to_clear: Option<&Value>,
|
|
437
471
|
expected_owner_receiver: Option<(&Value, &Value)>,
|
|
438
472
|
exact_owner_receiver_to_clear: Option<(&str, &Value, &Value)>,
|
|
473
|
+
exact_owner_receiver_to_restore: Option<(&str, &Value, &Value, &Value, &Value)>,
|
|
439
474
|
) -> Result<(), StateError> {
|
|
440
475
|
let path = runtime_state_path(workspace);
|
|
441
476
|
// Python `state.py:497`:先对入参 state 跑 `_migrate_state_identity`(就地填缺失 leader uuid)。
|
|
@@ -459,7 +494,8 @@ fn persist_runtime_state_with_merge_options_and_expected(
|
|
|
459
494
|
// and leave the seed, or skip copying a concurrent owner.
|
|
460
495
|
let skip_pre_lock_fast_path = exact_owner_seed_to_clear.is_some()
|
|
461
496
|
|| expected_owner_receiver.is_some()
|
|
462
|
-
|| exact_owner_receiver_to_clear.is_some()
|
|
497
|
+
|| exact_owner_receiver_to_clear.is_some()
|
|
498
|
+
|| exact_owner_receiver_to_restore.is_some();
|
|
463
499
|
if !skip_pre_lock_fast_path && cache_equals(&path, &migrated) {
|
|
464
500
|
return Ok(());
|
|
465
501
|
}
|
|
@@ -544,6 +580,7 @@ fn persist_runtime_state_with_merge_options_and_expected(
|
|
|
544
580
|
&receiver_updates,
|
|
545
581
|
receiver_update_team_key.zip(exact_owner_seed_to_clear),
|
|
546
582
|
exact_owner_receiver_to_clear,
|
|
583
|
+
exact_owner_receiver_to_restore,
|
|
547
584
|
)?;
|
|
548
585
|
}
|
|
549
586
|
// Stage 3 save-output strip second pass (defence-in-depth): after the
|
|
@@ -613,6 +650,7 @@ fn apply_persist_merge_contract(
|
|
|
613
650
|
receiver_updates: &BTreeSet<String>,
|
|
614
651
|
exact_owner_seed_to_clear: Option<(&str, &Value)>,
|
|
615
652
|
exact_owner_receiver_to_clear: Option<(&str, &Value, &Value)>,
|
|
653
|
+
exact_owner_receiver_to_restore: Option<(&str, &Value, &Value, &Value, &Value)>,
|
|
616
654
|
) -> Result<(), StateError> {
|
|
617
655
|
// A0/R1: the projection gate only guards the TOP-LEVEL passes (top-level agents and
|
|
618
656
|
// the top-level<->active-team cross projections depend on which team is active); the
|
|
@@ -745,6 +783,25 @@ fn apply_persist_merge_contract(
|
|
|
745
783
|
);
|
|
746
784
|
}
|
|
747
785
|
}
|
|
786
|
+
if let Some((
|
|
787
|
+
restore_team,
|
|
788
|
+
expected_owner,
|
|
789
|
+
expected_receiver,
|
|
790
|
+
previous_owner,
|
|
791
|
+
previous_receiver,
|
|
792
|
+
)) = exact_owner_receiver_to_restore
|
|
793
|
+
{
|
|
794
|
+
if team == restore_team {
|
|
795
|
+
apply_lock_held_exact_owner_receiver_restore(
|
|
796
|
+
incoming_entry,
|
|
797
|
+
latest_entry,
|
|
798
|
+
expected_owner,
|
|
799
|
+
expected_receiver,
|
|
800
|
+
previous_owner,
|
|
801
|
+
previous_receiver,
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
748
805
|
preserve_latest_endpoint_convergence_fields(incoming_entry, latest_entry);
|
|
749
806
|
}
|
|
750
807
|
}
|
|
@@ -919,6 +976,43 @@ fn apply_lock_held_exact_owner_receiver_cleanup(
|
|
|
919
976
|
}
|
|
920
977
|
}
|
|
921
978
|
|
|
979
|
+
fn apply_lock_held_exact_owner_receiver_restore(
|
|
980
|
+
incoming: &mut Value,
|
|
981
|
+
latest: &Value,
|
|
982
|
+
expected_owner: &Value,
|
|
983
|
+
expected_receiver: &Value,
|
|
984
|
+
previous_owner: &Value,
|
|
985
|
+
previous_receiver: &Value,
|
|
986
|
+
) {
|
|
987
|
+
let latest_owner = latest.get("team_owner");
|
|
988
|
+
let latest_receiver = latest.get("leader_receiver");
|
|
989
|
+
let Some(incoming_obj) = incoming.as_object_mut() else {
|
|
990
|
+
return;
|
|
991
|
+
};
|
|
992
|
+
if latest_owner == Some(expected_owner) && latest_receiver == Some(expected_receiver) {
|
|
993
|
+
incoming_obj.insert("team_owner".to_string(), previous_owner.clone());
|
|
994
|
+
incoming_obj.insert("leader_receiver".to_string(), previous_receiver.clone());
|
|
995
|
+
if let Some(epoch) = previous_owner
|
|
996
|
+
.get("owner_epoch")
|
|
997
|
+
.cloned()
|
|
998
|
+
.or_else(|| latest.get("owner_epoch").cloned())
|
|
999
|
+
{
|
|
1000
|
+
incoming_obj.insert("owner_epoch".to_string(), epoch);
|
|
1001
|
+
}
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
for key in ["team_owner", "leader_receiver", "owner_epoch"] {
|
|
1005
|
+
match latest.get(key) {
|
|
1006
|
+
Some(value) => {
|
|
1007
|
+
incoming_obj.insert(key.to_string(), value.clone());
|
|
1008
|
+
}
|
|
1009
|
+
None => {
|
|
1010
|
+
incoming_obj.remove(key);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
922
1016
|
fn preserve_latest_endpoint_convergence_fields(incoming: &mut Value, latest: &Value) {
|
|
923
1017
|
if !latest_has_preferable_endpoint_convergence(incoming, latest) {
|
|
924
1018
|
return;
|
|
@@ -3113,6 +3207,7 @@ mod tests {
|
|
|
3113
3207
|
&BTreeSet::new(),
|
|
3114
3208
|
None,
|
|
3115
3209
|
None,
|
|
3210
|
+
None,
|
|
3116
3211
|
)
|
|
3117
3212
|
.unwrap();
|
|
3118
3213
|
|
|
@@ -3149,6 +3244,7 @@ mod tests {
|
|
|
3149
3244
|
&receiver_updates,
|
|
3150
3245
|
None,
|
|
3151
3246
|
None,
|
|
3247
|
+
None,
|
|
3152
3248
|
)
|
|
3153
3249
|
.unwrap();
|
|
3154
3250
|
|
|
@@ -52,6 +52,8 @@ use super::persist::{
|
|
|
52
52
|
as helper_write_root_with_receiver_authority_and_expected,
|
|
53
53
|
save_runtime_state_with_exact_owner_receiver_cleanup
|
|
54
54
|
as helper_write_root_with_exact_owner_receiver_cleanup,
|
|
55
|
+
save_runtime_state_with_exact_owner_receiver_restore
|
|
56
|
+
as helper_write_root_with_exact_owner_receiver_restore,
|
|
55
57
|
save_runtime_state_with_team_tombstone_lifecycle_topology_authority as helper_write_root_with_team_tombstone_lifecycle_topology_authority,
|
|
56
58
|
save_runtime_state_with_team_tombstoned_agents as helper_write_root_with_team_tombstoned_agents,
|
|
57
59
|
save_runtime_state_without_migrations as helper_write_root_without_migrations,
|
|
@@ -224,9 +226,20 @@ pub enum StateWriteIntent<'a> {
|
|
|
224
226
|
seed: &'a Value,
|
|
225
227
|
expected_receiver: &'a Value,
|
|
226
228
|
},
|
|
229
|
+
RestoreExactTeamOwnerAndReceiver {
|
|
230
|
+
team_key: &'a str,
|
|
231
|
+
expected_owner: &'a Value,
|
|
232
|
+
expected_receiver: &'a Value,
|
|
233
|
+
previous_owner: &'a Value,
|
|
234
|
+
previous_receiver: &'a Value,
|
|
235
|
+
},
|
|
227
236
|
LeaderBindingRestoreNonTargetTeams {
|
|
228
237
|
target_team_key: &'a str,
|
|
229
238
|
},
|
|
239
|
+
/// Strip extra keys written by a failed or degraded quick-start bind attempt.
|
|
240
|
+
QuickStartBindingCleanup {
|
|
241
|
+
team_key: &'a str,
|
|
242
|
+
},
|
|
230
243
|
LeaderStartBinding {
|
|
231
244
|
team_key: &'a str,
|
|
232
245
|
transport_kind: &'a str,
|
|
@@ -428,6 +441,22 @@ fn route_direct(
|
|
|
428
441
|
seed,
|
|
429
442
|
expected_receiver,
|
|
430
443
|
),
|
|
444
|
+
StateWriteIntent::RestoreExactTeamOwnerAndReceiver {
|
|
445
|
+
team_key,
|
|
446
|
+
expected_owner,
|
|
447
|
+
expected_receiver,
|
|
448
|
+
previous_owner,
|
|
449
|
+
previous_receiver,
|
|
450
|
+
} => helper_write_root_with_exact_owner_receiver_restore(
|
|
451
|
+
workspace,
|
|
452
|
+
state,
|
|
453
|
+
team_key,
|
|
454
|
+
expected_owner,
|
|
455
|
+
expected_receiver,
|
|
456
|
+
previous_owner,
|
|
457
|
+
previous_receiver,
|
|
458
|
+
),
|
|
459
|
+
StateWriteIntent::QuickStartBindingCleanup { .. } => helper_write_root(workspace, state),
|
|
431
460
|
StateWriteIntent::LeaderBindingRestoreNonTargetTeams { .. } => {
|
|
432
461
|
helper_write_root_without_migrations(workspace, state)
|
|
433
462
|
}
|