@team-agent/installer 0.5.75 → 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/README.md +9 -0
- package/README.zh.md +8 -0
- package/crates/team-agent/src/cli/adapters.rs +184 -125
- package/crates/team-agent/src/cli/diagnose.rs +333 -68
- package/crates/team-agent/src/cli/emit.rs +59 -15
- package/crates/team-agent/src/cli/mod.rs +600 -134
- package/crates/team-agent/src/cli/models.rs +485 -44
- 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/brief.rs +1137 -0
- 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/status_port.rs +8 -0
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +62 -8
- package/crates/team-agent/src/cli/tests/mod.rs +1 -0
- package/crates/team-agent/src/cli/tests/named_address.rs +70 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +29 -24
- package/crates/team-agent/src/cli/tests/status_brief.rs +691 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +8 -2
- 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 +538 -41
- 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/leader/types.rs +4 -0
- 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 +138 -14
- 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 +1002 -65
- 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/b0_reader_hideone_audit_contract.rs +45 -14
- 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 -3
- 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 +4 -3
- 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 +69 -28
- 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/upgrade_compat_0211_red.rs +22 -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 +12 -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/messaging/watchers.rs +48 -4
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +2 -5
- package/crates/team-agent/src/state/paths.rs +17 -2
- package/crates/team-agent/src/state/persist.rs +97 -1
- package/crates/team-agent/src/state/projection.rs +18 -2
- package/crates/team-agent/src/state/repository.rs +29 -0
- package/crates/team-agent/src/state/selector.rs +23 -3
- package/crates/team-agent/src/test_support/models_timeout_fixture.rs +144 -0
- 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]
|
|
@@ -653,12 +653,30 @@ pub fn recover_watchers_for_incident(
|
|
|
653
653
|
/// row for watcher-backed messages. A `submitted_pending_acceptance` row has
|
|
654
654
|
/// already crossed the transport boundary and is claim-immutable. Any future
|
|
655
655
|
/// retry belongs to a separate typed recovery arm, not attach/claim convergence.
|
|
656
|
+
/// Compatibility helper retained for callers that only need the total.
|
|
657
|
+
/// The typed recovery path below is the single writer; this wrapper performs
|
|
658
|
+
/// no second requeue and only maps the first result to `total()`.
|
|
656
659
|
pub(crate) fn requeue_blocked_leader_messages(
|
|
657
660
|
store: &MessageStore,
|
|
658
661
|
event_log: &EventLog,
|
|
659
662
|
owner_team_id: &TeamKey,
|
|
660
663
|
claimed_pane_id: &PaneId,
|
|
661
664
|
) -> Result<usize, MessagingError> {
|
|
665
|
+
requeue_blocked_leader_messages_with_counts(
|
|
666
|
+
store,
|
|
667
|
+
event_log,
|
|
668
|
+
owner_team_id,
|
|
669
|
+
claimed_pane_id,
|
|
670
|
+
)
|
|
671
|
+
.map(crate::message_store::BlockedLeaderRequeueCounts::total)
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
pub(crate) fn requeue_blocked_leader_messages_with_counts(
|
|
675
|
+
store: &MessageStore,
|
|
676
|
+
event_log: &EventLog,
|
|
677
|
+
owner_team_id: &TeamKey,
|
|
678
|
+
claimed_pane_id: &PaneId,
|
|
679
|
+
) -> Result<crate::message_store::BlockedLeaderRequeueCounts, MessagingError> {
|
|
662
680
|
// E6 (0.5.9 offline-mailbox §6.5): also requeue rows that a third-party
|
|
663
681
|
// sender left in `queued_until_leader_attach` via the leader mailbox.
|
|
664
682
|
// Same idempotent requeue funnel (row/message_id/leader_notification_log
|
|
@@ -690,7 +708,7 @@ pub(crate) fn requeue_blocked_leader_messages(
|
|
|
690
708
|
}),
|
|
691
709
|
)?;
|
|
692
710
|
}
|
|
693
|
-
Ok(
|
|
711
|
+
Ok(counts)
|
|
694
712
|
}
|
|
695
713
|
|
|
696
714
|
/// `requeue_delivery_exhausted_watchers`: attach-leader 成功后把已经耗尽投递
|
|
@@ -702,12 +720,33 @@ pub(crate) fn requeue_blocked_leader_messages(
|
|
|
702
720
|
/// `deliver_pending_messages` never re-picks — the new leader would never see the
|
|
703
721
|
/// pending notification.
|
|
704
722
|
pub fn requeue_delivery_exhausted_watchers(
|
|
705
|
-
|
|
723
|
+
workspace: &Path,
|
|
706
724
|
store: &MessageStore,
|
|
707
725
|
event_log: &EventLog,
|
|
708
726
|
owner_team_id: &TeamKey,
|
|
709
727
|
claimed_pane_id: &PaneId,
|
|
710
728
|
) -> Result<Vec<WatcherNotice>, MessagingError> {
|
|
729
|
+
requeue_delivery_exhausted_watchers_with_counts(
|
|
730
|
+
workspace,
|
|
731
|
+
store,
|
|
732
|
+
event_log,
|
|
733
|
+
owner_team_id,
|
|
734
|
+
claimed_pane_id,
|
|
735
|
+
)
|
|
736
|
+
.map(|(notices, _)| notices)
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/// Attach-leader's typed recovery result. The counts come from the same
|
|
740
|
+
/// owner-team scoped attach-recovery call (the watcher and message updates
|
|
741
|
+
/// remain their existing transactions), so an attach caller can expose only
|
|
742
|
+
/// this operation's debt rather than scanning arbitrary historical events.
|
|
743
|
+
pub(crate) fn requeue_delivery_exhausted_watchers_with_counts(
|
|
744
|
+
_workspace: &Path,
|
|
745
|
+
store: &MessageStore,
|
|
746
|
+
event_log: &EventLog,
|
|
747
|
+
owner_team_id: &TeamKey,
|
|
748
|
+
claimed_pane_id: &PaneId,
|
|
749
|
+
) -> Result<(Vec<WatcherNotice>, crate::message_store::BlockedLeaderRequeueCounts), MessagingError> {
|
|
711
750
|
let conn = crate::db::schema::open_db(store.db_path())?;
|
|
712
751
|
let mut stmt = conn.prepare(
|
|
713
752
|
"select watcher_id, result_id, status from result_watchers
|
|
@@ -748,8 +787,13 @@ pub fn requeue_delivery_exhausted_watchers(
|
|
|
748
787
|
)?;
|
|
749
788
|
}
|
|
750
789
|
drop(stmt);
|
|
751
|
-
let
|
|
752
|
-
|
|
790
|
+
let counts = requeue_blocked_leader_messages_with_counts(
|
|
791
|
+
store,
|
|
792
|
+
event_log,
|
|
793
|
+
owner_team_id,
|
|
794
|
+
claimed_pane_id,
|
|
795
|
+
)?;
|
|
796
|
+
Ok((out, counts))
|
|
753
797
|
}
|
|
754
798
|
|
|
755
799
|
/// `delivered_result_message` (`result_delivery.py:394`):内容级去重 —— 查某 result_id 是否已有
|
|
@@ -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());
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
use std::path::{Path, PathBuf};
|
|
28
28
|
|
|
29
29
|
use crate::model::paths::{runtime_dir, runtime_spec_path};
|
|
30
|
-
use crate::state::persist::load_runtime_state;
|
|
30
|
+
use crate::state::persist::{load_runtime_state, load_runtime_state_without_migrations};
|
|
31
31
|
use crate::state::projection::team_state_candidates;
|
|
32
32
|
|
|
33
33
|
/// The identity of a team within a workspace. Carries the *workspace root*
|
|
@@ -193,10 +193,25 @@ impl CommandScope {
|
|
|
193
193
|
/// On any I/O error the empty case is returned — destructive commands
|
|
194
194
|
/// will run their own selector and surface the real error.
|
|
195
195
|
pub fn resolve(workspace: &Path, requested_team: Option<&str>) -> Self {
|
|
196
|
+
Self::resolve_from_state(requested_team, load_runtime_state(workspace))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// Ambiguity gate for read-only commands. Must not migrate or persist state.
|
|
200
|
+
pub fn resolve_readonly(workspace: &Path, requested_team: Option<&str>) -> Self {
|
|
201
|
+
Self::resolve_from_state(
|
|
202
|
+
requested_team,
|
|
203
|
+
load_runtime_state_without_migrations(workspace),
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
fn resolve_from_state(
|
|
208
|
+
requested_team: Option<&str>,
|
|
209
|
+
loaded: Result<serde_json::Value, crate::state::StateError>,
|
|
210
|
+
) -> Self {
|
|
196
211
|
if let Some(team) = requested_team.filter(|t| !t.is_empty()) {
|
|
197
212
|
return Self::Resolved(team.to_string());
|
|
198
213
|
}
|
|
199
|
-
let Ok(state) =
|
|
214
|
+
let Ok(state) = loaded else {
|
|
200
215
|
return Self::EmptyWorkspace;
|
|
201
216
|
};
|
|
202
217
|
let alive = team_state_candidates(&state);
|