@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
|
@@ -65,10 +65,11 @@ use crate::provider::wire::{parse_provider, provider_wire};
|
|
|
65
65
|
use crate::transport::{
|
|
66
66
|
command_basename, normalize_capture, tmux_capture_argv, tmux_empty_inject_argv,
|
|
67
67
|
tmux_inject_text_argv, tmux_query_argv, tmux_send_keys_argv, tmux_send_submit_argv,
|
|
68
|
-
tmux_spawn_argv, AttachOutcome, BackendKind,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
tmux_spawn_argv, AttachOutcome, BackendKind, BusySignalKind, CaptureRange,
|
|
69
|
+
CaptureSampleOutcome, CapturedText, InjectPayload, InjectReport, InjectStage,
|
|
70
|
+
InjectVerification, InputSurfaceProbe, Key, PaneField, PaneId, PaneInfo, PaneMode,
|
|
71
|
+
QueryOutcome, SessionName, SetEnvOutcome, SpawnResult, SubmitAttemptObservation,
|
|
72
|
+
SubmitConsumptionReason, SubmitDiagnostics, SubmitObserver, SubmitVerification, Target,
|
|
72
73
|
Transport, TransportError, TurnVerification, WindowName,
|
|
73
74
|
};
|
|
74
75
|
|
|
@@ -242,8 +243,6 @@ impl TmuxSocketEndpoint {
|
|
|
242
243
|
pub(crate) enum RuntimeTmuxEndpointSource {
|
|
243
244
|
StateTmuxEndpoint,
|
|
244
245
|
StateTmuxSocket,
|
|
245
|
-
LeaderReceiverTmuxEndpoint,
|
|
246
|
-
LeaderReceiverTmuxSocket,
|
|
247
246
|
WorkspaceFallback,
|
|
248
247
|
}
|
|
249
248
|
|
|
@@ -257,8 +256,6 @@ impl RuntimeTmuxEndpointSource {
|
|
|
257
256
|
match self {
|
|
258
257
|
Self::StateTmuxEndpoint => "state.tmux_endpoint",
|
|
259
258
|
Self::StateTmuxSocket => "state.tmux_socket",
|
|
260
|
-
Self::LeaderReceiverTmuxEndpoint => "leader_receiver.tmux_endpoint",
|
|
261
|
-
Self::LeaderReceiverTmuxSocket => "leader_receiver.tmux_socket",
|
|
262
259
|
Self::WorkspaceFallback => "workspace_fallback",
|
|
263
260
|
}
|
|
264
261
|
}
|
|
@@ -671,50 +668,6 @@ fn runtime_tmux_endpoint_from_state(
|
|
|
671
668
|
})
|
|
672
669
|
}
|
|
673
670
|
|
|
674
|
-
/// ---
|
|
675
|
-
/// purpose: 读取 selected team's persisted leader receiver endpoint
|
|
676
|
-
/// params:
|
|
677
|
-
/// state: 运行期全量或 selected-team 投影 JSON
|
|
678
|
-
/// returns: nested selected receiver 优先,其次投影顶层 receiver;二者都缺则 None
|
|
679
|
-
/// boundary: 只读 owning receiver,不读取普通 tmux_endpoint,不探活
|
|
680
|
-
/// ---
|
|
681
|
-
pub(crate) fn owning_tmux_endpoint_from_state(
|
|
682
|
-
state: &serde_json::Value,
|
|
683
|
-
) -> Option<(&str, RuntimeTmuxEndpointSource)> {
|
|
684
|
-
fn receiver_endpoint(state: &serde_json::Value) -> Option<(&str, RuntimeTmuxEndpointSource)> {
|
|
685
|
-
state
|
|
686
|
-
.pointer("/leader_receiver/tmux_endpoint")
|
|
687
|
-
.and_then(serde_json::Value::as_str)
|
|
688
|
-
.filter(|endpoint| !endpoint.is_empty())
|
|
689
|
-
.map(|endpoint| {
|
|
690
|
-
(
|
|
691
|
-
endpoint,
|
|
692
|
-
RuntimeTmuxEndpointSource::LeaderReceiverTmuxEndpoint,
|
|
693
|
-
)
|
|
694
|
-
})
|
|
695
|
-
.or_else(|| {
|
|
696
|
-
state
|
|
697
|
-
.pointer("/leader_receiver/tmux_socket")
|
|
698
|
-
.and_then(serde_json::Value::as_str)
|
|
699
|
-
.filter(|endpoint| !endpoint.is_empty())
|
|
700
|
-
.map(|endpoint| {
|
|
701
|
-
(
|
|
702
|
-
endpoint,
|
|
703
|
-
RuntimeTmuxEndpointSource::LeaderReceiverTmuxSocket,
|
|
704
|
-
)
|
|
705
|
-
})
|
|
706
|
-
})
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
state
|
|
710
|
-
.get("active_team_key")
|
|
711
|
-
.and_then(serde_json::Value::as_str)
|
|
712
|
-
.filter(|team_key| !team_key.is_empty())
|
|
713
|
-
.and_then(|team_key| state.get("teams").and_then(|teams| teams.get(team_key)))
|
|
714
|
-
.and_then(receiver_endpoint)
|
|
715
|
-
.or_else(|| receiver_endpoint(state))
|
|
716
|
-
}
|
|
717
|
-
|
|
718
671
|
/// CP-1 socket name: SHORT + DETERMINISTIC per canonical workspace path. `ta-` + 12 hex chars of a
|
|
719
672
|
/// stable FNV-1a hash over the canonicalized path. AF_UNIX `sun_path` is ~104 chars and the socket
|
|
720
673
|
/// lives at `/tmp/tmux-<uid>/<name>`, so we must NOT use the (~88-char) session name. §10: a
|
|
@@ -758,10 +711,6 @@ pub fn runtime_tmux_endpoint_from_state_pub(
|
|
|
758
711
|
let src = match source {
|
|
759
712
|
RuntimeTmuxEndpointSource::StateTmuxEndpoint => "state.tmux_endpoint",
|
|
760
713
|
RuntimeTmuxEndpointSource::StateTmuxSocket => "state.tmux_socket",
|
|
761
|
-
RuntimeTmuxEndpointSource::LeaderReceiverTmuxEndpoint => {
|
|
762
|
-
"leader_receiver.tmux_endpoint"
|
|
763
|
-
}
|
|
764
|
-
RuntimeTmuxEndpointSource::LeaderReceiverTmuxSocket => "leader_receiver.tmux_socket",
|
|
765
714
|
RuntimeTmuxEndpointSource::WorkspaceFallback => "workspace_fallback",
|
|
766
715
|
};
|
|
767
716
|
(endpoint.to_string(), src)
|
|
@@ -1530,6 +1479,14 @@ fn submit_diagnostics_for_inject(
|
|
|
1530
1479
|
surface: InjectTargetSurface,
|
|
1531
1480
|
) -> SubmitDiagnostics {
|
|
1532
1481
|
SubmitDiagnostics {
|
|
1482
|
+
consumption_reason: SubmitConsumptionReason::Unknown,
|
|
1483
|
+
// No pre-paste capture exists on this path; keep the diagnostic null.
|
|
1484
|
+
before_busy_signal_kind: None,
|
|
1485
|
+
busy_signal_kind: None,
|
|
1486
|
+
busy_line_from_bottom: None,
|
|
1487
|
+
current_marker_in_bottom_15: None,
|
|
1488
|
+
paste_identity_in_composer: None,
|
|
1489
|
+
consumption_from_capture_result: None,
|
|
1533
1490
|
appear_gate_elapsed_ms: 0,
|
|
1534
1491
|
appear_gate_matched: false,
|
|
1535
1492
|
total_elapsed_ms,
|
|
@@ -2118,6 +2075,56 @@ pub(crate) fn consumption_from_capture(
|
|
|
2118
2075
|
}
|
|
2119
2076
|
}
|
|
2120
2077
|
|
|
2078
|
+
#[derive(Debug, Clone, Copy, Default)]
|
|
2079
|
+
struct ConsumptionCaptureObservation {
|
|
2080
|
+
busy: Option<BusySignalMatch>,
|
|
2081
|
+
marker_in_bottom_15: Option<bool>,
|
|
2082
|
+
paste_identity_in_composer: Option<bool>,
|
|
2083
|
+
consumption_from_capture_result: Option<bool>,
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
fn consumption_capture_observation(
|
|
2087
|
+
text: &str,
|
|
2088
|
+
marker: Option<&str>,
|
|
2089
|
+
token_ever_visible: bool,
|
|
2090
|
+
tracked: Option<PasteLatch>,
|
|
2091
|
+
) -> ConsumptionCaptureObservation {
|
|
2092
|
+
let Some(marker) = marker else {
|
|
2093
|
+
return ConsumptionCaptureObservation::default();
|
|
2094
|
+
};
|
|
2095
|
+
ConsumptionCaptureObservation {
|
|
2096
|
+
busy: provider_busy_signal_match(text),
|
|
2097
|
+
marker_in_bottom_15: Some(token_in_bottom_n(text, marker, 15)),
|
|
2098
|
+
paste_identity_in_composer: tracked
|
|
2099
|
+
.map(|tracked| this_paste_identity_in_composer(text, marker, Some(tracked))),
|
|
2100
|
+
consumption_from_capture_result: consumption_from_capture(
|
|
2101
|
+
text,
|
|
2102
|
+
marker,
|
|
2103
|
+
token_ever_visible,
|
|
2104
|
+
tracked,
|
|
2105
|
+
),
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
#[derive(Debug, Clone, Copy, Default)]
|
|
2110
|
+
struct ConsumptionDiagnostics {
|
|
2111
|
+
busy_signal_kind: Option<BusySignalKind>,
|
|
2112
|
+
busy_line_from_bottom: Option<u8>,
|
|
2113
|
+
current_marker_in_bottom_15: Option<bool>,
|
|
2114
|
+
paste_identity_in_composer: Option<bool>,
|
|
2115
|
+
consumption_from_capture_result: Option<bool>,
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
impl ConsumptionDiagnostics {
|
|
2119
|
+
fn record(&mut self, observation: ConsumptionCaptureObservation) {
|
|
2120
|
+
self.busy_signal_kind = observation.busy.map(|match_| match_.kind);
|
|
2121
|
+
self.busy_line_from_bottom = observation.busy.map(|match_| match_.line_from_bottom);
|
|
2122
|
+
self.current_marker_in_bottom_15 = observation.marker_in_bottom_15;
|
|
2123
|
+
self.paste_identity_in_composer = observation.paste_identity_in_composer;
|
|
2124
|
+
self.consumption_from_capture_result = observation.consumption_from_capture_result;
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2121
2128
|
/// ---
|
|
2122
2129
|
/// purpose: Gone 分支在判已消费前复核 composer 占位符(PasteLatch 身份)
|
|
2123
2130
|
/// contract: 锁定身份仍在,或未锁定但 grok `[Pasted: …]` 仍在 ⇒ Some(false)
|
|
@@ -2147,37 +2154,67 @@ fn marker_position_from_bottom(text: &str, marker: &str) -> Option<u32> {
|
|
|
2147
2154
|
None
|
|
2148
2155
|
}
|
|
2149
2156
|
|
|
2150
|
-
|
|
2157
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
2158
|
+
struct BusySignalMatch {
|
|
2159
|
+
kind: BusySignalKind,
|
|
2160
|
+
line_from_bottom: u8,
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
fn busy_signal_kind_in_line(line: &str) -> Option<BusySignalKind> {
|
|
2164
|
+
let lower = line.to_ascii_lowercase();
|
|
2165
|
+
if lower.contains("working") {
|
|
2166
|
+
return Some(BusySignalKind::Working);
|
|
2167
|
+
}
|
|
2168
|
+
if lower.contains("thinking") {
|
|
2169
|
+
return Some(BusySignalKind::Thinking);
|
|
2170
|
+
}
|
|
2171
|
+
if lower.contains("processing") {
|
|
2172
|
+
return Some(BusySignalKind::Processing);
|
|
2173
|
+
}
|
|
2174
|
+
if lower.contains("esc to interrupt") {
|
|
2175
|
+
return Some(BusySignalKind::EscToInterrupt);
|
|
2176
|
+
}
|
|
2177
|
+
if line.contains('●')
|
|
2178
|
+
|| line.contains('⏳')
|
|
2179
|
+
|| line.contains('⠋')
|
|
2180
|
+
|| line.contains('⠙')
|
|
2181
|
+
|| line.contains('⠹')
|
|
2182
|
+
|| line.contains('⠸')
|
|
2183
|
+
|| line.contains('⠼')
|
|
2184
|
+
|| line.contains('⠴')
|
|
2185
|
+
|| line.contains('⠦')
|
|
2186
|
+
|| line.contains('⠧')
|
|
2187
|
+
|| line.contains('⠇')
|
|
2188
|
+
|| line.contains('⠏')
|
|
2189
|
+
|| line.contains('✶')
|
|
2190
|
+
|| line.contains('✢')
|
|
2191
|
+
|| line.contains('✻')
|
|
2192
|
+
|| line.contains('✽')
|
|
2193
|
+
|| line.contains('✳')
|
|
2194
|
+
{
|
|
2195
|
+
return Some(BusySignalKind::SpinnerGlyph);
|
|
2196
|
+
}
|
|
2197
|
+
None
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
fn provider_busy_signal_match(text: &str) -> Option<BusySignalMatch> {
|
|
2151
2201
|
text.lines()
|
|
2152
2202
|
.rev()
|
|
2153
2203
|
.filter(|line| !line.trim().is_empty())
|
|
2154
2204
|
.take(15)
|
|
2155
|
-
.
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|| line.contains('●')
|
|
2162
|
-
|| line.contains('⏳')
|
|
2163
|
-
|| line.contains('⠋')
|
|
2164
|
-
|| line.contains('⠙')
|
|
2165
|
-
|| line.contains('⠹')
|
|
2166
|
-
|| line.contains('⠸')
|
|
2167
|
-
|| line.contains('⠼')
|
|
2168
|
-
|| line.contains('⠴')
|
|
2169
|
-
|| line.contains('⠦')
|
|
2170
|
-
|| line.contains('⠧')
|
|
2171
|
-
|| line.contains('⠇')
|
|
2172
|
-
|| line.contains('⠏')
|
|
2173
|
-
|| line.contains('✶')
|
|
2174
|
-
|| line.contains('✢')
|
|
2175
|
-
|| line.contains('✻')
|
|
2176
|
-
|| line.contains('✽')
|
|
2177
|
-
|| line.contains('✳')
|
|
2205
|
+
.enumerate()
|
|
2206
|
+
.find_map(|(line_from_bottom, line)| {
|
|
2207
|
+
busy_signal_kind_in_line(line).map(|kind| BusySignalMatch {
|
|
2208
|
+
kind,
|
|
2209
|
+
line_from_bottom: line_from_bottom as u8,
|
|
2210
|
+
})
|
|
2178
2211
|
})
|
|
2179
2212
|
}
|
|
2180
2213
|
|
|
2214
|
+
fn provider_busy_signal_in_tail(text: &str) -> bool {
|
|
2215
|
+
provider_busy_signal_match(text).is_some()
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2181
2218
|
/// ---
|
|
2182
2219
|
/// purpose: 入箱 vs 开跑(G4)。给定最近一次 pane 尾,回答席位是否因此开跑。
|
|
2183
2220
|
/// params:
|
|
@@ -2619,24 +2656,22 @@ fn append_caller_context_assignments(parts: &mut Vec<String>, provider: Option<&
|
|
|
2619
2656
|
));
|
|
2620
2657
|
}
|
|
2621
2658
|
|
|
2622
|
-
///
|
|
2623
|
-
/// `leader_shell_wrapper_command` and the substring detected by
|
|
2624
|
-
/// `leader_provider_health` MUST share this prefix exactly. Format:
|
|
2659
|
+
/// Public exit-marker text emitted by `leader_shell_wrapper_command`. Format:
|
|
2625
2660
|
/// `"[team-agent] {provider_label} exited with {rc}"`.
|
|
2626
2661
|
pub const LEADER_PROVIDER_EXIT_MARKER_PREFIX: &str = "[team-agent]";
|
|
2627
2662
|
pub const LEADER_PROVIDER_EXIT_MARKER_SUFFIX: &str = "exited with";
|
|
2663
|
+
const PROVIDER_EXIT_STATUS_OPTION: &str = "@team-agent-provider-exit-status";
|
|
2628
2664
|
|
|
2629
2665
|
/// 0.4.x (CR R6): build the leader exit marker text for `provider_label`.
|
|
2630
|
-
/// Used by
|
|
2631
|
-
/// (capture substring) so they cannot drift.
|
|
2666
|
+
/// Used by the shell wrapper for human-readable output.
|
|
2632
2667
|
/// ---
|
|
2633
2668
|
/// purpose: 单一来源地拼出 leader pane 的 provider 退出标记文本
|
|
2634
2669
|
/// params:
|
|
2635
2670
|
/// provider_label: 人类可读的 provider 名,原样嵌入标记
|
|
2636
2671
|
/// returns: 前缀 + provider 名 + 后缀 拼成的标记(不含退出码,退出码由 shell 的 printf 补)
|
|
2637
2672
|
/// boundary:
|
|
2638
|
-
/// -
|
|
2639
|
-
/// -
|
|
2673
|
+
/// - shell wrapper 统一使用此显示标记,进程状态另由监督 shell 收据证明
|
|
2674
|
+
/// - 仅供显示;历史正文可包含相同文本,不得据此判定进程退出
|
|
2640
2675
|
/// ---
|
|
2641
2676
|
pub fn leader_provider_exit_marker(provider_label: &str) -> String {
|
|
2642
2677
|
format!(
|
|
@@ -2868,6 +2903,11 @@ pub fn leader_shell_wrapper_command(
|
|
|
2868
2903
|
parts.push(key.clone());
|
|
2869
2904
|
parts.push("&&".to_string());
|
|
2870
2905
|
}
|
|
2906
|
+
// Clear any prior receipt before invoking the provider. Terminal content,
|
|
2907
|
+
// including replayed sessions, must never be treated as an exit receipt.
|
|
2908
|
+
parts.push(format!(
|
|
2909
|
+
"{{ tmux -S \"${{TMUX%%,*}}\" set-option -p -t \"$TMUX_PANE\" {PROVIDER_EXIT_STATUS_OPTION} running >/dev/null 2>&1 || :; }} &&"
|
|
2910
|
+
));
|
|
2871
2911
|
// 3. env exports + provider (NO `exec` so the provider is a child).
|
|
2872
2912
|
// 0.4.x ordering fix: skip keys present in env_unset so KEY=val does not
|
|
2873
2913
|
// re-introduce a just-unset variable from the inherited env map.
|
|
@@ -2881,6 +2921,9 @@ pub fn leader_shell_wrapper_command(
|
|
|
2881
2921
|
parts.push(";".to_string());
|
|
2882
2922
|
// 4. exit marker + inert shell tail
|
|
2883
2923
|
parts.push("rc=$?;".to_string());
|
|
2924
|
+
parts.push(format!(
|
|
2925
|
+
"tmux -S \"${{TMUX%%,*}}\" set-option -p -t \"$TMUX_PANE\" {PROVIDER_EXIT_STATUS_OPTION} \"$rc\" >/dev/null 2>&1;"
|
|
2926
|
+
));
|
|
2884
2927
|
parts.push("printf".to_string());
|
|
2885
2928
|
// CR R6: marker text comes from single-source `leader_provider_exit_marker`.
|
|
2886
2929
|
parts.push(shell_quote(&format!(
|
|
@@ -2893,8 +2936,8 @@ pub fn leader_shell_wrapper_command(
|
|
|
2893
2936
|
}
|
|
2894
2937
|
|
|
2895
2938
|
fn inert_pane_tail_command() -> String {
|
|
2896
|
-
// `sh` is deliberate:
|
|
2897
|
-
//
|
|
2939
|
+
// `sh` is deliberate: exited-wrapper input classifiers recognize shell
|
|
2940
|
+
// basenames. `sh -c` does not read commands from
|
|
2898
2941
|
// stdin; disabling echo also prevents input from appearing accepted.
|
|
2899
2942
|
let script = r#"trap '' INT QUIT; stty -echo 2>/dev/null; printf "%s\n" "[team-agent] Provider exited; this pane no longer accepts input. Restart from another pane with the appropriate team-agent start command."; while :; do sleep 3600 & wait "$!"; done"#;
|
|
2900
2943
|
format!("exec /bin/sh -c {}", shell_quote(script))
|
|
@@ -3227,6 +3270,8 @@ impl Transport for TmuxBackend {
|
|
|
3227
3270
|
let mut consumption_attempts: u32 = 0;
|
|
3228
3271
|
let mut capture_retries: u32 = 0;
|
|
3229
3272
|
let mut consumed: Option<bool> = None;
|
|
3273
|
+
let mut consumption_reason = SubmitConsumptionReason::Unknown;
|
|
3274
|
+
let mut consumption_diagnostics = ConsumptionDiagnostics::default();
|
|
3230
3275
|
let mut attempts_detail: Vec<SubmitAttemptObservation> = Vec::new();
|
|
3231
3276
|
let mut any_attempt_matched = false;
|
|
3232
3277
|
|
|
@@ -3267,6 +3312,13 @@ impl Transport for TmuxBackend {
|
|
|
3267
3312
|
token_seen_after_enter = Some(false);
|
|
3268
3313
|
}
|
|
3269
3314
|
tracked_paste = latch_paste(&cap.text, tracked_paste);
|
|
3315
|
+
let observation = consumption_capture_observation(
|
|
3316
|
+
&cap.text,
|
|
3317
|
+
marker,
|
|
3318
|
+
token_ever_visible,
|
|
3319
|
+
tracked_paste,
|
|
3320
|
+
);
|
|
3321
|
+
consumption_diagnostics.record(observation);
|
|
3270
3322
|
let obs = submit_attempt_observation(
|
|
3271
3323
|
attempt_index,
|
|
3272
3324
|
&cap,
|
|
@@ -3279,18 +3331,16 @@ impl Transport for TmuxBackend {
|
|
|
3279
3331
|
attempts_detail.push(obs);
|
|
3280
3332
|
// NeverSeen 不得把 token 缺席写成 consumed;
|
|
3281
3333
|
// 见过再消失,或本次占位符身份离开 composer,才停手。
|
|
3282
|
-
if
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
token_ever_visible,
|
|
3286
|
-
tracked_paste,
|
|
3287
|
-
) == Some(true)
|
|
3288
|
-
{
|
|
3334
|
+
if observation.consumption_from_capture_result == Some(true) {
|
|
3335
|
+
consumption_reason =
|
|
3336
|
+
SubmitConsumptionReason::ConsumptionFromCapture;
|
|
3289
3337
|
consumed = Some(true);
|
|
3290
3338
|
break;
|
|
3291
3339
|
}
|
|
3292
3340
|
if cursor_single_enter_enabled() {
|
|
3293
|
-
if
|
|
3341
|
+
if observation.busy.is_some() {
|
|
3342
|
+
consumption_reason =
|
|
3343
|
+
SubmitConsumptionReason::CursorPollingBusy;
|
|
3294
3344
|
consumed = Some(true);
|
|
3295
3345
|
break;
|
|
3296
3346
|
}
|
|
@@ -3375,6 +3425,13 @@ impl Transport for TmuxBackend {
|
|
|
3375
3425
|
token_seen_after_enter = Some(false);
|
|
3376
3426
|
}
|
|
3377
3427
|
tracked_paste = latch_paste(&cap.text, tracked_paste);
|
|
3428
|
+
let observation = consumption_capture_observation(
|
|
3429
|
+
&cap.text,
|
|
3430
|
+
marker,
|
|
3431
|
+
token_ever_visible,
|
|
3432
|
+
tracked_paste,
|
|
3433
|
+
);
|
|
3434
|
+
consumption_diagnostics.record(observation);
|
|
3378
3435
|
let obs = submit_attempt_observation(
|
|
3379
3436
|
attempt_index,
|
|
3380
3437
|
&cap,
|
|
@@ -3385,24 +3442,22 @@ impl Transport for TmuxBackend {
|
|
|
3385
3442
|
any_attempt_matched = true;
|
|
3386
3443
|
}
|
|
3387
3444
|
attempts_detail.push(obs);
|
|
3388
|
-
if
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
token_ever_visible,
|
|
3392
|
-
tracked_paste,
|
|
3393
|
-
) == Some(true)
|
|
3394
|
-
{
|
|
3445
|
+
if observation.consumption_from_capture_result == Some(true) {
|
|
3446
|
+
consumption_reason =
|
|
3447
|
+
SubmitConsumptionReason::ConsumptionFromCapture;
|
|
3395
3448
|
found_consumed = true;
|
|
3396
3449
|
break;
|
|
3397
3450
|
}
|
|
3398
|
-
if cursor_single_enter_enabled()
|
|
3399
|
-
|
|
3400
|
-
|
|
3451
|
+
if cursor_single_enter_enabled() && observation.busy.is_some() {
|
|
3452
|
+
consumption_reason =
|
|
3453
|
+
SubmitConsumptionReason::CursorPollingBusy;
|
|
3401
3454
|
found_consumed = true;
|
|
3402
3455
|
break;
|
|
3403
3456
|
}
|
|
3404
3457
|
}
|
|
3405
3458
|
Err(_) => {
|
|
3459
|
+
consumption_diagnostics
|
|
3460
|
+
.record(ConsumptionCaptureObservation::default());
|
|
3406
3461
|
capture_tally.record_err();
|
|
3407
3462
|
capture_retries = capture_retries.saturating_add(1);
|
|
3408
3463
|
consecutive_capture_failures =
|
|
@@ -3426,7 +3481,8 @@ impl Transport for TmuxBackend {
|
|
|
3426
3481
|
}
|
|
3427
3482
|
} else {
|
|
3428
3483
|
// Non-token payload: single Enter, no consumption check
|
|
3429
|
-
// (0.3.27).
|
|
3484
|
+
// (0.3.27). Keep the explicit no-marker reason.
|
|
3485
|
+
consumption_reason = SubmitConsumptionReason::NoMarker;
|
|
3430
3486
|
consumed = Some(true);
|
|
3431
3487
|
break;
|
|
3432
3488
|
}
|
|
@@ -3451,21 +3507,38 @@ impl Transport for TmuxBackend {
|
|
|
3451
3507
|
Some(false) => match self.capture(target, CaptureRange::Tail(15)) {
|
|
3452
3508
|
Ok(cap) => {
|
|
3453
3509
|
last_turn_text = Some(cap.text.clone());
|
|
3510
|
+
let observation = consumption_capture_observation(
|
|
3511
|
+
&cap.text,
|
|
3512
|
+
marker,
|
|
3513
|
+
token_ever_visible,
|
|
3514
|
+
tracked_paste,
|
|
3515
|
+
);
|
|
3516
|
+
consumption_diagnostics.record(observation);
|
|
3454
3517
|
attempts_detail.push(submit_attempt_observation(
|
|
3455
3518
|
consumption_attempts.max(1),
|
|
3456
3519
|
&cap,
|
|
3457
3520
|
marker,
|
|
3458
3521
|
inject_start.elapsed().as_millis() as u64,
|
|
3459
3522
|
));
|
|
3460
|
-
if
|
|
3523
|
+
if observation.busy.is_some() {
|
|
3524
|
+
consumption_reason = SubmitConsumptionReason::FallbackBusy;
|
|
3461
3525
|
SubmitVerification::EnterSentWithoutPlaceholderCheck
|
|
3462
3526
|
} else {
|
|
3527
|
+
consumption_reason = SubmitConsumptionReason::Unverified;
|
|
3463
3528
|
SubmitVerification::SubmitConsumptionUnverified
|
|
3464
3529
|
}
|
|
3465
3530
|
}
|
|
3466
|
-
Err(_) =>
|
|
3531
|
+
Err(_) => {
|
|
3532
|
+
consumption_diagnostics
|
|
3533
|
+
.record(ConsumptionCaptureObservation::default());
|
|
3534
|
+
consumption_reason = SubmitConsumptionReason::Unverified;
|
|
3535
|
+
SubmitVerification::SubmitConsumptionUnverified
|
|
3536
|
+
}
|
|
3467
3537
|
},
|
|
3468
|
-
None =>
|
|
3538
|
+
None => {
|
|
3539
|
+
consumption_reason = SubmitConsumptionReason::Unverified;
|
|
3540
|
+
SubmitVerification::SubmitConsumptionUnverified
|
|
3541
|
+
}
|
|
3469
3542
|
};
|
|
3470
3543
|
// Unverified + 折行缺口:只补一颗 C-m。A 的 should_resubmit_enter
|
|
3471
3544
|
// 已为真(latch/底15 token,含打满 cap)时 B 不介入。身份拿不到
|
|
@@ -3511,6 +3584,13 @@ impl Transport for TmuxBackend {
|
|
|
3511
3584
|
token_seen_after_enter = Some(false);
|
|
3512
3585
|
}
|
|
3513
3586
|
tracked_paste = latch_paste(&cap.text, tracked_paste);
|
|
3587
|
+
let observation = consumption_capture_observation(
|
|
3588
|
+
&cap.text,
|
|
3589
|
+
marker,
|
|
3590
|
+
token_ever_visible,
|
|
3591
|
+
tracked_paste,
|
|
3592
|
+
);
|
|
3593
|
+
consumption_diagnostics.record(observation);
|
|
3514
3594
|
let obs = submit_attempt_observation(
|
|
3515
3595
|
consumption_attempts.max(1),
|
|
3516
3596
|
&cap,
|
|
@@ -3519,20 +3599,22 @@ impl Transport for TmuxBackend {
|
|
|
3519
3599
|
);
|
|
3520
3600
|
attempts_detail.push(obs);
|
|
3521
3601
|
last_turn_text = Some(cap.text.clone());
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
) == Some(true)
|
|
3528
|
-
|| provider_busy_signal_in_tail(&cap.text)
|
|
3529
|
-
{
|
|
3602
|
+
let capture_consumed = observation
|
|
3603
|
+
.consumption_from_capture_result
|
|
3604
|
+
== Some(true);
|
|
3605
|
+
let busy = observation.busy.is_some();
|
|
3606
|
+
if capture_consumed || busy {
|
|
3530
3607
|
// Gone+无 latch 会把折行 token 写成已消费;身份仍在则不算。
|
|
3531
3608
|
if !this_paste_identity_in_composer(
|
|
3532
3609
|
&cap.text,
|
|
3533
3610
|
m,
|
|
3534
3611
|
tracked_paste,
|
|
3535
3612
|
) {
|
|
3613
|
+
consumption_reason = if capture_consumed {
|
|
3614
|
+
SubmitConsumptionReason::ConsumptionFromCapture
|
|
3615
|
+
} else {
|
|
3616
|
+
SubmitConsumptionReason::FallbackBusy
|
|
3617
|
+
};
|
|
3536
3618
|
found_consumed = true;
|
|
3537
3619
|
break;
|
|
3538
3620
|
}
|
|
@@ -3542,11 +3624,14 @@ impl Transport for TmuxBackend {
|
|
|
3542
3624
|
m,
|
|
3543
3625
|
tracked_paste,
|
|
3544
3626
|
) {
|
|
3627
|
+
consumption_reason = SubmitConsumptionReason::Unknown;
|
|
3545
3628
|
found_consumed = true;
|
|
3546
3629
|
break;
|
|
3547
3630
|
}
|
|
3548
3631
|
}
|
|
3549
3632
|
Err(_) => {
|
|
3633
|
+
consumption_diagnostics
|
|
3634
|
+
.record(ConsumptionCaptureObservation::default());
|
|
3550
3635
|
capture_tally.record_err();
|
|
3551
3636
|
break;
|
|
3552
3637
|
}
|
|
@@ -3562,6 +3647,24 @@ impl Transport for TmuxBackend {
|
|
|
3562
3647
|
}
|
|
3563
3648
|
let total_elapsed_ms = inject_start.elapsed().as_millis() as u64;
|
|
3564
3649
|
let surface = probe_inject_target_surface(self, target);
|
|
3650
|
+
let mut submit_diagnostics = submit_diagnostics_for_inject(
|
|
3651
|
+
total_elapsed_ms,
|
|
3652
|
+
attempts_detail,
|
|
3653
|
+
&capture_tally,
|
|
3654
|
+
token_seen_after_paste,
|
|
3655
|
+
token_seen_after_enter,
|
|
3656
|
+
surface,
|
|
3657
|
+
);
|
|
3658
|
+
submit_diagnostics.consumption_reason = consumption_reason;
|
|
3659
|
+
submit_diagnostics.busy_signal_kind = consumption_diagnostics.busy_signal_kind;
|
|
3660
|
+
submit_diagnostics.busy_line_from_bottom =
|
|
3661
|
+
consumption_diagnostics.busy_line_from_bottom;
|
|
3662
|
+
submit_diagnostics.current_marker_in_bottom_15 =
|
|
3663
|
+
consumption_diagnostics.current_marker_in_bottom_15;
|
|
3664
|
+
submit_diagnostics.paste_identity_in_composer =
|
|
3665
|
+
consumption_diagnostics.paste_identity_in_composer;
|
|
3666
|
+
submit_diagnostics.consumption_from_capture_result =
|
|
3667
|
+
consumption_diagnostics.consumption_from_capture_result;
|
|
3565
3668
|
return Ok(InjectReport {
|
|
3566
3669
|
stage_reached: InjectStage::Submit,
|
|
3567
3670
|
inject_verification: inject_verification_after_readback(
|
|
@@ -3574,14 +3677,7 @@ impl Transport for TmuxBackend {
|
|
|
3574
3677
|
last_turn_text.as_deref(),
|
|
3575
3678
|
),
|
|
3576
3679
|
attempts: consumption_attempts.saturating_add(capture_retries),
|
|
3577
|
-
submit_diagnostics: Some(
|
|
3578
|
-
total_elapsed_ms,
|
|
3579
|
-
attempts_detail,
|
|
3580
|
-
&capture_tally,
|
|
3581
|
-
token_seen_after_paste,
|
|
3582
|
-
token_seen_after_enter,
|
|
3583
|
-
surface,
|
|
3584
|
-
)),
|
|
3680
|
+
submit_diagnostics: Some(submit_diagnostics),
|
|
3585
3681
|
});
|
|
3586
3682
|
}
|
|
3587
3683
|
}
|
|
@@ -3653,6 +3749,22 @@ impl Transport for TmuxBackend {
|
|
|
3653
3749
|
Ok(Some(output.stdout.trim().to_string()))
|
|
3654
3750
|
}
|
|
3655
3751
|
|
|
3752
|
+
fn provider_exit_status(&self, pane: &PaneId) -> Result<Option<u8>, TransportError> {
|
|
3753
|
+
let argv = self.tmux_argv(&[
|
|
3754
|
+
"tmux".to_string(),
|
|
3755
|
+
"display-message".to_string(),
|
|
3756
|
+
"-p".to_string(),
|
|
3757
|
+
"-t".to_string(),
|
|
3758
|
+
pane.as_str().to_string(),
|
|
3759
|
+
format!("#{{{PROVIDER_EXIT_STATUS_OPTION}}}"),
|
|
3760
|
+
]);
|
|
3761
|
+
let output = self.runner.run(&argv)?;
|
|
3762
|
+
if !output.success {
|
|
3763
|
+
return Ok(None);
|
|
3764
|
+
}
|
|
3765
|
+
Ok(output.stdout.trim().parse::<u8>().ok())
|
|
3766
|
+
}
|
|
3767
|
+
|
|
3656
3768
|
fn liveness(&self, pane: &PaneId) -> Result<PaneLiveness, TransportError> {
|
|
3657
3769
|
let argv = self.tmux_argv(&[
|
|
3658
3770
|
"tmux".to_string(),
|