@team-agent/installer 0.5.77 → 0.5.80
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 +393 -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/runtime.rs +3 -1
- 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/tests.rs +37 -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/coordinator/steps/abnormal.rs +122 -2
- 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 +197 -88
- 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 +86 -23
- 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/fixtures/pi_list_models_spaced_ids.stdout.txt +9 -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_catalog_red.rs +71 -0
- 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 +221 -80
- package/crates/team-agent/src/messaging/leader_channel.rs +56 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +127 -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/provider/session_scan/common/tests.rs +204 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +24 -2
- package/crates/team-agent/src/state/abnormal_watch/tests/mod.rs +248 -0
- package/crates/team-agent/src/state/abnormal_watch.rs +144 -0
- package/crates/team-agent/src/state/mod.rs +1 -0
- package/crates/team-agent/src/state/persist.rs +102 -4
- package/crates/team-agent/src/state/projection.rs +7 -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 +149 -4
- package/crates/team-agent/src/tmux_backend.rs +239 -127
- package/crates/team-agent/src/transport.rs +78 -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};
|
|
@@ -219,31 +220,128 @@ pub fn deliver_pending_message(
|
|
|
219
220
|
turn_verification: None,
|
|
220
221
|
});
|
|
221
222
|
};
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
223
|
+
match prepare_message_state(
|
|
224
|
+
workspace, store, message_id, event_log, state, &message, false,
|
|
225
|
+
)? {
|
|
226
|
+
Ok(prepared) => deliver_prepared_message(
|
|
227
|
+
workspace, store, transport, message_id, event_log, state, message, prepared,
|
|
228
|
+
),
|
|
229
|
+
Err(outcome) => Ok(outcome),
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Thread-local observation only; no production cache or scheduling hook.
|
|
234
|
+
#[cfg(test)]
|
|
235
|
+
pub(super) mod preparation_probe {
|
|
236
|
+
std::thread_local! {
|
|
237
|
+
pub static COUNTS: std::cell::Cell<(usize, usize, usize)> = const { std::cell::Cell::new((0, 0, 0)) };
|
|
238
|
+
pub static BEFORE_CLAIM: std::cell::RefCell<Option<Box<dyn FnOnce()>>> = const { std::cell::RefCell::new(None) };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
struct PreparedMessageState {
|
|
243
|
+
scoped: Option<serde_json::Value>,
|
|
244
|
+
canonical_owner_team_id: Option<String>,
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
fn prepare_message_state(
|
|
248
|
+
workspace: &Path,
|
|
249
|
+
store: &MessageStore,
|
|
250
|
+
message_id: &str,
|
|
251
|
+
event_log: &EventLog,
|
|
252
|
+
state: &serde_json::Value,
|
|
253
|
+
message: &PendingMessage,
|
|
254
|
+
batch: bool,
|
|
255
|
+
) -> Result<Result<PreparedMessageState, DeliveryOutcome>, MessagingError> {
|
|
256
|
+
#[cfg(test)]
|
|
257
|
+
preparation_probe::COUNTS.with(|counts| {
|
|
258
|
+
let (prepare, project, claim) = counts.get();
|
|
259
|
+
counts.set((prepare + 1, project, claim));
|
|
260
|
+
});
|
|
261
|
+
let mut prepared = PreparedMessageState {
|
|
262
|
+
scoped: None,
|
|
263
|
+
canonical_owner_team_id: message.owner_team_id.clone(),
|
|
264
|
+
};
|
|
265
|
+
if let Some(team) = message
|
|
266
|
+
.owner_team_id
|
|
267
|
+
.as_deref()
|
|
268
|
+
.filter(|team| !team.is_empty())
|
|
269
|
+
{
|
|
270
|
+
match project_state_for_owner_team(
|
|
271
|
+
workspace,
|
|
272
|
+
team,
|
|
273
|
+
state,
|
|
274
|
+
Some(store),
|
|
275
|
+
Some(message_id),
|
|
276
|
+
Some(event_log),
|
|
277
|
+
)? {
|
|
278
|
+
OwnerTeamProjection::Projected {
|
|
229
279
|
state,
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
Some(
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
280
|
+
canonical_team,
|
|
281
|
+
} => {
|
|
282
|
+
prepared.scoped = Some(state);
|
|
283
|
+
prepared.canonical_owner_team_id = Some(canonical_team);
|
|
284
|
+
}
|
|
285
|
+
OwnerTeamProjection::Refused(outcome) => {
|
|
286
|
+
if batch {
|
|
287
|
+
let _ = event_log.write(
|
|
288
|
+
"delivery.projection_refused",
|
|
289
|
+
serde_json::json!({
|
|
290
|
+
"message_id": message_id,
|
|
291
|
+
"owner_team_id": team,
|
|
292
|
+
"status": super::helpers::status_wire(outcome.status),
|
|
293
|
+
"verification": outcome.verification,
|
|
294
|
+
"reason": format!("{:?}", outcome.reason),
|
|
295
|
+
}),
|
|
296
|
+
);
|
|
241
297
|
}
|
|
242
|
-
|
|
298
|
+
return Ok(Err(outcome));
|
|
243
299
|
}
|
|
244
300
|
}
|
|
245
|
-
|
|
246
|
-
|
|
301
|
+
}
|
|
302
|
+
if batch
|
|
303
|
+
&& recipient_is_busy(
|
|
304
|
+
prepared.scoped.as_ref().unwrap_or(state),
|
|
305
|
+
&message.recipient,
|
|
306
|
+
)
|
|
307
|
+
{
|
|
308
|
+
event_log.write(
|
|
309
|
+
"send.deferred_busy",
|
|
310
|
+
serde_json::json!({
|
|
311
|
+
"message_id": message_id,
|
|
312
|
+
"sender": message.sender,
|
|
313
|
+
"recipient": message.recipient,
|
|
314
|
+
"reason": "recipient_busy",
|
|
315
|
+
}),
|
|
316
|
+
)?;
|
|
317
|
+
return Ok(Err(DeliveryOutcome {
|
|
318
|
+
ok: false,
|
|
319
|
+
status: DeliveryStatus::Queued,
|
|
320
|
+
message_status: MessageStatusShadow(message.status.clone()),
|
|
321
|
+
message_id: Some(message_id.to_string()),
|
|
322
|
+
verification: None,
|
|
323
|
+
stage: None,
|
|
324
|
+
reason: None,
|
|
325
|
+
channel: None,
|
|
326
|
+
ack_forced_off: false,
|
|
327
|
+
turn_verification: None,
|
|
328
|
+
}));
|
|
329
|
+
}
|
|
330
|
+
Ok(Ok(prepared))
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
fn deliver_prepared_message(
|
|
334
|
+
workspace: &Path,
|
|
335
|
+
store: &MessageStore,
|
|
336
|
+
transport: &dyn Transport,
|
|
337
|
+
message_id: &str,
|
|
338
|
+
event_log: &EventLog,
|
|
339
|
+
fallback: &serde_json::Value,
|
|
340
|
+
message: PendingMessage,
|
|
341
|
+
prepared: PreparedMessageState,
|
|
342
|
+
) -> Result<DeliveryOutcome, MessagingError> {
|
|
343
|
+
let canonical_owner_team_id = prepared.canonical_owner_team_id;
|
|
344
|
+
let state = prepared.scoped.as_ref().unwrap_or(fallback);
|
|
247
345
|
if message.recipient == "leader"
|
|
248
346
|
&& matches!(
|
|
249
347
|
message.status.as_str(),
|
|
@@ -290,6 +388,18 @@ pub fn deliver_pending_message(
|
|
|
290
388
|
}),
|
|
291
389
|
)?;
|
|
292
390
|
}
|
|
391
|
+
#[cfg(test)]
|
|
392
|
+
{
|
|
393
|
+
preparation_probe::BEFORE_CLAIM.with(|hook| {
|
|
394
|
+
if let Some(hook) = hook.borrow_mut().take() {
|
|
395
|
+
hook();
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
preparation_probe::COUNTS.with(|counts| {
|
|
399
|
+
let (prepare, project, claim) = counts.get();
|
|
400
|
+
counts.set((prepare, project, claim + 1));
|
|
401
|
+
});
|
|
402
|
+
}
|
|
293
403
|
let attempt = if store.claim_for_delivery(message_id)? {
|
|
294
404
|
message.delivery_attempts.saturating_add(1)
|
|
295
405
|
} else {
|
|
@@ -572,11 +682,11 @@ pub fn deliver_pending_message(
|
|
|
572
682
|
canonical_owner_team_id.as_deref(),
|
|
573
683
|
resolved.metadata.as_ref(),
|
|
574
684
|
);
|
|
575
|
-
let
|
|
685
|
+
let recipient_cursor = recipient_is_cursor_agent(state, &message.recipient);
|
|
576
686
|
let inject_attempt = crate::tmux_backend::with_paste_to_submit_floor(
|
|
577
687
|
paste_to_submit_floor_for_recipient(state, &message.recipient),
|
|
578
688
|
|| {
|
|
579
|
-
crate::tmux_backend::with_cursor_single_enter(
|
|
689
|
+
crate::tmux_backend::with_cursor_single_enter(recipient_cursor, || {
|
|
580
690
|
transport.inject_with_submit_observer(
|
|
581
691
|
&target,
|
|
582
692
|
&payload,
|
|
@@ -592,9 +702,7 @@ pub fn deliver_pending_message(
|
|
|
592
702
|
// grok 忙时第一次回车只入显式队列。默认 send-now:只重按回车顶出去。
|
|
593
703
|
// 无 `Enter:send now` 时零次额外按键,claude/codex 行为不变。
|
|
594
704
|
// cursor:第二下 Enter 打断进行中回合,不走 flush。
|
|
595
|
-
if !crate::provider::submit_now::keep_provider_queue_requested()
|
|
596
|
-
&& recipient_allows_explicit_queue_flush(state, &message.recipient)
|
|
597
|
-
{
|
|
705
|
+
if !crate::provider::submit_now::keep_provider_queue_requested() && !recipient_cursor {
|
|
598
706
|
match crate::provider::submit_now::flush_explicit_queue(transport, &target) {
|
|
599
707
|
Ok(flush) if flush.extra_enters > 0 => {
|
|
600
708
|
let _ = event_log.write(
|
|
@@ -1947,10 +2055,7 @@ fn merge_inject_readback_fields(
|
|
|
1947
2055
|
};
|
|
1948
2056
|
let inject_pane = target.and_then(inject_pane_id);
|
|
1949
2057
|
if let Some(pane) = inject_pane {
|
|
1950
|
-
obj.insert(
|
|
1951
|
-
"inject_target_pane_id".to_string(),
|
|
1952
|
-
serde_json::json!(pane),
|
|
1953
|
-
);
|
|
2058
|
+
obj.insert("inject_target_pane_id".to_string(), serde_json::json!(pane));
|
|
1954
2059
|
}
|
|
1955
2060
|
let resolved_pane = metadata.map(|meta| meta.target_pane_id.as_str());
|
|
1956
2061
|
obj.insert(
|
|
@@ -1999,6 +2104,36 @@ fn merge_inject_readback_fields(
|
|
|
1999
2104
|
.map(|d| d.last_capture_outcome.as_str())
|
|
2000
2105
|
.unwrap_or(CaptureSampleOutcome::NotAttempted.as_str())),
|
|
2001
2106
|
);
|
|
2107
|
+
obj.insert(
|
|
2108
|
+
"consumption_reason".to_string(),
|
|
2109
|
+
serde_json::json!(diag
|
|
2110
|
+
.map(|d| d.consumption_reason.as_str())
|
|
2111
|
+
.unwrap_or("unknown")),
|
|
2112
|
+
);
|
|
2113
|
+
obj.insert(
|
|
2114
|
+
"before_busy_signal_kind".to_string(),
|
|
2115
|
+
serde_json::json!(diag.and_then(|d| d.before_busy_signal_kind.map(|kind| kind.as_str()))),
|
|
2116
|
+
);
|
|
2117
|
+
obj.insert(
|
|
2118
|
+
"busy_signal_kind".to_string(),
|
|
2119
|
+
serde_json::json!(diag.and_then(|d| d.busy_signal_kind.map(|kind| kind.as_str()))),
|
|
2120
|
+
);
|
|
2121
|
+
obj.insert(
|
|
2122
|
+
"busy_line_from_bottom".to_string(),
|
|
2123
|
+
serde_json::json!(diag.and_then(|d| d.busy_line_from_bottom)),
|
|
2124
|
+
);
|
|
2125
|
+
obj.insert(
|
|
2126
|
+
"current_marker_in_bottom_15".to_string(),
|
|
2127
|
+
serde_json::json!(diag.and_then(|d| d.current_marker_in_bottom_15)),
|
|
2128
|
+
);
|
|
2129
|
+
obj.insert(
|
|
2130
|
+
"paste_identity_in_composer".to_string(),
|
|
2131
|
+
serde_json::json!(diag.and_then(|d| d.paste_identity_in_composer)),
|
|
2132
|
+
);
|
|
2133
|
+
obj.insert(
|
|
2134
|
+
"consumption_from_capture_result".to_string(),
|
|
2135
|
+
serde_json::json!(diag.and_then(|d| d.consumption_from_capture_result)),
|
|
2136
|
+
);
|
|
2002
2137
|
obj.insert(
|
|
2003
2138
|
"pane_command_basename".to_string(),
|
|
2004
2139
|
diag.and_then(|d| d.pane_command_basename.as_deref())
|
|
@@ -2161,59 +2296,30 @@ pub fn deliver_pending_messages(
|
|
|
2161
2296
|
};
|
|
2162
2297
|
let mut delivered = Vec::new();
|
|
2163
2298
|
for message_id in message_ids {
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
OwnerTeamProjection::Refused(outcome) => {
|
|
2181
|
-
let _ = event_log.write(
|
|
2182
|
-
"delivery.projection_refused",
|
|
2183
|
-
serde_json::json!({
|
|
2184
|
-
"message_id": message_id.as_str(),
|
|
2185
|
-
"owner_team_id": team,
|
|
2186
|
-
"status": super::helpers::status_wire(outcome.status),
|
|
2187
|
-
"verification": outcome.verification,
|
|
2188
|
-
"reason": format!("{:?}", outcome.reason),
|
|
2189
|
-
}),
|
|
2190
|
-
);
|
|
2191
|
-
continue;
|
|
2192
|
-
}
|
|
2193
|
-
}
|
|
2194
|
-
}
|
|
2195
|
-
_ => state,
|
|
2196
|
-
};
|
|
2197
|
-
if recipient_is_busy(state, &message.recipient) {
|
|
2198
|
-
event_log.write(
|
|
2199
|
-
"send.deferred_busy",
|
|
2200
|
-
serde_json::json!({
|
|
2201
|
-
"message_id": message_id,
|
|
2202
|
-
"sender": message.sender,
|
|
2203
|
-
"recipient": message.recipient,
|
|
2204
|
-
"reason": "recipient_busy",
|
|
2205
|
-
}),
|
|
2206
|
-
)?;
|
|
2207
|
-
continue;
|
|
2208
|
-
}
|
|
2209
|
-
}
|
|
2210
|
-
let outcome = match deliver_pending_message(
|
|
2299
|
+
let Some(message) = message_for_delivery(&store, &message_id)? else {
|
|
2300
|
+
continue;
|
|
2301
|
+
};
|
|
2302
|
+
let prepared = match prepare_message_state(
|
|
2303
|
+
workspace,
|
|
2304
|
+
&store,
|
|
2305
|
+
&message_id,
|
|
2306
|
+
event_log,
|
|
2307
|
+
state,
|
|
2308
|
+
&message,
|
|
2309
|
+
true,
|
|
2310
|
+
)? {
|
|
2311
|
+
Ok(prepared) => prepared,
|
|
2312
|
+
Err(_) => continue,
|
|
2313
|
+
};
|
|
2314
|
+
let outcome = match deliver_prepared_message(
|
|
2211
2315
|
workspace,
|
|
2212
2316
|
&store,
|
|
2213
2317
|
transport,
|
|
2214
2318
|
&message_id,
|
|
2215
2319
|
event_log,
|
|
2216
2320
|
state,
|
|
2321
|
+
message,
|
|
2322
|
+
prepared,
|
|
2217
2323
|
) {
|
|
2218
2324
|
Ok(outcome) => outcome,
|
|
2219
2325
|
Err(error) => {
|
|
@@ -2357,6 +2463,13 @@ fn recipient_provider(state: &serde_json::Value, recipient: &str) -> Option<Prov
|
|
|
2357
2463
|
.and_then(parse_canonical_provider)
|
|
2358
2464
|
}
|
|
2359
2465
|
|
|
2466
|
+
fn recipient_is_cursor_agent(state: &serde_json::Value, recipient: &str) -> bool {
|
|
2467
|
+
matches!(
|
|
2468
|
+
recipient_provider(state, recipient),
|
|
2469
|
+
Some(Provider::CursorAgent)
|
|
2470
|
+
)
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2360
2473
|
/// ---
|
|
2361
2474
|
/// purpose: paste→Enter 地板按收件人 provider 选择
|
|
2362
2475
|
/// contract: CursorAgent 与 Grok 为 1s;其余(含 claude)为 ZERO
|
|
@@ -2927,6 +3040,11 @@ fn project_state_for_owner_team(
|
|
|
2927
3040
|
message_id: Option<&str>,
|
|
2928
3041
|
event_log: Option<&EventLog>,
|
|
2929
3042
|
) -> Result<OwnerTeamProjection, MessagingError> {
|
|
3043
|
+
#[cfg(test)]
|
|
3044
|
+
preparation_probe::COUNTS.with(|counts| {
|
|
3045
|
+
let (prepare, project, claim) = counts.get();
|
|
3046
|
+
counts.set((prepare, project + 1, claim));
|
|
3047
|
+
});
|
|
2930
3048
|
let raw = crate::state::persist::load_runtime_state(workspace)?;
|
|
2931
3049
|
let fallback_has_teams = fallback
|
|
2932
3050
|
.get("teams")
|
|
@@ -3391,6 +3509,13 @@ mod paste_floor_tests {
|
|
|
3391
3509
|
turn_verification: crate::transport::TurnVerification::NotYetObserved,
|
|
3392
3510
|
attempts: 1,
|
|
3393
3511
|
submit_diagnostics: Some(crate::transport::SubmitDiagnostics {
|
|
3512
|
+
consumption_reason: SubmitConsumptionReason::Unverified,
|
|
3513
|
+
before_busy_signal_kind: Some(BusySignalKind::Working),
|
|
3514
|
+
busy_signal_kind: Some(BusySignalKind::SpinnerGlyph),
|
|
3515
|
+
busy_line_from_bottom: Some(2),
|
|
3516
|
+
current_marker_in_bottom_15: Some(false),
|
|
3517
|
+
paste_identity_in_composer: None,
|
|
3518
|
+
consumption_from_capture_result: Some(false),
|
|
3394
3519
|
last_capture_outcome: CaptureSampleOutcome::Failed,
|
|
3395
3520
|
capture_err_count: 1,
|
|
3396
3521
|
capture_sample_count: 1,
|
|
@@ -3419,6 +3544,22 @@ mod paste_floor_tests {
|
|
|
3419
3544
|
None,
|
|
3420
3545
|
);
|
|
3421
3546
|
assert_eq!(event["last_capture_outcome"], serde_json::json!("failed"));
|
|
3547
|
+
assert_eq!(event["consumption_reason"], serde_json::json!("unverified"));
|
|
3548
|
+
assert_eq!(
|
|
3549
|
+
event["before_busy_signal_kind"],
|
|
3550
|
+
serde_json::json!("working")
|
|
3551
|
+
);
|
|
3552
|
+
assert_eq!(event["busy_signal_kind"], serde_json::json!("spinner_glyph"));
|
|
3553
|
+
assert_eq!(event["busy_line_from_bottom"], serde_json::json!(2));
|
|
3554
|
+
assert_eq!(
|
|
3555
|
+
event["current_marker_in_bottom_15"],
|
|
3556
|
+
serde_json::json!(false)
|
|
3557
|
+
);
|
|
3558
|
+
assert_eq!(event["paste_identity_in_composer"], serde_json::Value::Null);
|
|
3559
|
+
assert_eq!(
|
|
3560
|
+
event["consumption_from_capture_result"],
|
|
3561
|
+
serde_json::json!(false)
|
|
3562
|
+
);
|
|
3422
3563
|
assert_eq!(event["token_seen_after_paste"], serde_json::Value::Null);
|
|
3423
3564
|
assert_eq!(event["pane_command_basename"], serde_json::json!("grok"));
|
|
3424
3565
|
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
|
+
}
|