@team-agent/installer 0.5.66 → 0.5.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +8 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/Cargo.toml +1 -0
- package/crates/team-agent/src/cli/adapters.rs +5 -0
- package/crates/team-agent/src/cli/diagnose.rs +156 -0
- package/crates/team-agent/src/cli/emit.rs +6 -5
- package/crates/team-agent/src/cli/grok_slot.rs +299 -0
- package/crates/team-agent/src/cli/leader.rs +22 -7
- package/crates/team-agent/src/cli/leaders.rs +125 -1
- package/crates/team-agent/src/cli/mod.rs +20 -0
- package/crates/team-agent/src/cli/send/presentation.rs +7 -1
- package/crates/team-agent/src/cli/spec.rs +3 -0
- package/crates/team-agent/src/cli/status_port/compact.rs +28 -0
- package/crates/team-agent/src/cli/status_port/snapshot.rs +25 -1
- package/crates/team-agent/src/cli/tests/base.rs +26 -3
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +129 -19
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +387 -6
- package/crates/team-agent/src/cli/tests/status_send.rs +79 -10
- package/crates/team-agent/src/communication_mode/mod.rs +6 -4
- package/crates/team-agent/src/compiler.rs +121 -5
- package/crates/team-agent/src/coordinator/backoff.rs +55 -0
- package/crates/team-agent/src/coordinator/conpty_shim.rs +82 -0
- package/crates/team-agent/src/coordinator/health.rs +173 -0
- package/crates/team-agent/src/coordinator/mod.rs +31 -0
- package/crates/team-agent/src/coordinator/orphan.rs +31 -0
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +31 -0
- package/crates/team-agent/src/coordinator/runtime_observation.rs +25 -0
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +59 -0
- package/crates/team-agent/src/coordinator/steps/delivery.rs +10 -0
- package/crates/team-agent/src/coordinator/steps/health_sync.rs +10 -0
- package/crates/team-agent/src/coordinator/steps/mod.rs +22 -0
- package/crates/team-agent/src/coordinator/steps/persist.rs +10 -0
- package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +10 -0
- package/crates/team-agent/src/coordinator/steps/session_gate.rs +10 -0
- package/crates/team-agent/src/coordinator/tick.rs +133 -22
- package/crates/team-agent/src/coordinator/types.rs +49 -0
- package/crates/team-agent/src/db/message_store.rs +39 -5
- package/crates/team-agent/src/layout/worker_env.rs +20 -3
- package/crates/team-agent/src/layout/worker_window_helpers.rs +3 -0
- package/crates/team-agent/src/leader/provider_attribution.rs +53 -0
- package/crates/team-agent/src/leader/registry.rs +65 -0
- package/crates/team-agent/src/leader/start.rs +973 -75
- package/crates/team-agent/src/lifecycle/display.rs +56 -0
- package/crates/team-agent/src/lifecycle/helpers.rs +57 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +398 -26
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +80 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +60 -0
- package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +101 -11
- package/crates/team-agent/src/lifecycle/launch/cursor_create_chat.rs +229 -0
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +548 -0
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp_iso.rs +120 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +208 -456
- package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +24 -0
- package/crates/team-agent/src/lifecycle/launch/grok_per_seat.rs +260 -0
- package/crates/team-agent/src/lifecycle/launch/identity.rs +146 -0
- package/crates/team-agent/src/lifecycle/launch/layout.rs +60 -0
- package/crates/team-agent/src/lifecycle/launch/leader_context.rs +112 -0
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +482 -1
- package/crates/team-agent/src/lifecycle/launch/ownership.rs +40 -0
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +830 -0
- package/crates/team-agent/src/lifecycle/launch/plan.rs +31 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +66 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +87 -3
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +85 -38
- package/crates/team-agent/src/lifecycle/launch/role_source.rs +44 -44
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +127 -25
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +140 -0
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +109 -0
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +214 -1
- package/crates/team-agent/src/lifecycle/launch.rs +107 -29
- package/crates/team-agent/src/lifecycle/lock.rs +42 -1
- package/crates/team-agent/src/lifecycle/mod.rs +11 -0
- package/crates/team-agent/src/lifecycle/pane_input_lock.rs +161 -0
- package/crates/team-agent/src/lifecycle/profile_launch.rs +99 -0
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +52 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +214 -160
- package/crates/team-agent/src/lifecycle/restart/common.rs +513 -37
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +27 -0
- package/crates/team-agent/src/lifecycle/restart/preflight.rs +25 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +106 -0
- package/crates/team-agent/src/lifecycle/restart/remove.rs +233 -6
- package/crates/team-agent/src/lifecycle/restart/selection.rs +72 -0
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +22 -0
- package/crates/team-agent/src/lifecycle/restart.rs +29 -0
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +2 -3
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +24 -3
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +309 -0
- package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +150 -157
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/core.rs +4 -0
- package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +464 -0
- package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +247 -0
- package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +353 -0
- package/crates/team-agent/src/lifecycle/tests/fixtures/pi_list_models_g0.stdout.txt +222 -0
- package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +787 -0
- package/crates/team-agent/src/lifecycle/tests/gate_fixtures.rs +562 -0
- package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +239 -0
- package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +485 -0
- package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +250 -0
- package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +293 -0
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +63 -38
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +3 -2
- package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +16 -8
- package/crates/team-agent/src/lifecycle/tests/mcp_tool_name_format_red.rs +69 -0
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +20 -19
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +97 -61
- package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +97 -0
- package/crates/team-agent/src/lifecycle/tests/pi_command_red.rs +128 -0
- package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +207 -0
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +838 -0
- package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +320 -0
- package/crates/team-agent/src/lifecycle/tests/pi_lifecycle_doctor_red.rs +98 -0
- package/crates/team-agent/src/lifecycle/tests/pi_session_delivery_red.rs +216 -0
- package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +40 -7
- package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +59 -1
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +69 -26
- package/crates/team-agent/src/lifecycle/tests.rs +30 -13
- package/crates/team-agent/src/lifecycle/types.rs +61 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +143 -12
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +100 -1
- package/crates/team-agent/src/mcp_server/tests.rs +196 -2
- package/crates/team-agent/src/messaging/delivery.rs +430 -41
- package/crates/team-agent/src/messaging/helpers.rs +2 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +53 -1
- package/crates/team-agent/src/messaging/results.rs +4 -0
- package/crates/team-agent/src/messaging/send.rs +34 -0
- package/crates/team-agent/src/messaging/tests/dup_inject.rs +406 -0
- package/crates/team-agent/src/messaging/tests/e23.rs +9 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +66 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
- package/crates/team-agent/src/messaging/types.rs +4 -1
- package/crates/team-agent/src/model/enums.rs +17 -6
- package/crates/team-agent/src/model/permissions.rs +3 -0
- package/crates/team-agent/src/os_probe.rs +73 -2
- package/crates/team-agent/src/provider/adapter.rs +259 -6
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +89 -0
- package/crates/team-agent/src/provider/adapters/grok.rs +157 -0
- package/crates/team-agent/src/provider/adapters/mod.rs +3 -0
- package/crates/team-agent/src/provider/adapters/pi.rs +152 -0
- package/crates/team-agent/src/provider/bypass_flags.rs +47 -2
- package/crates/team-agent/src/provider/classify.rs +4 -2
- package/crates/team-agent/src/provider/faults.rs +2 -1
- package/crates/team-agent/src/provider/mod.rs +11 -0
- package/crates/team-agent/src/provider/session/capture.rs +158 -20
- package/crates/team-agent/src/provider/session/context_fork/claude.rs +38 -1
- package/crates/team-agent/src/provider/session/context_fork/codex.rs +72 -1
- package/crates/team-agent/src/provider/session/context_fork/outcome.rs +57 -1
- package/crates/team-agent/src/provider/session/context_fork.rs +78 -3
- package/crates/team-agent/src/provider/session/mod.rs +18 -0
- package/crates/team-agent/src/provider/session/resume.rs +57 -0
- package/crates/team-agent/src/provider/session_scan/claude.rs +125 -1
- package/crates/team-agent/src/provider/session_scan/codex.rs +94 -1
- package/crates/team-agent/src/provider/session_scan/common.rs +205 -2
- package/crates/team-agent/src/provider/session_scan/copilot.rs +33 -1
- package/crates/team-agent/src/provider/session_scan/cursor.rs +538 -0
- package/crates/team-agent/src/provider/session_scan/grok.rs +288 -0
- package/crates/team-agent/src/provider/session_scan/pi.rs +172 -0
- package/crates/team-agent/src/provider/session_scan.rs +12 -0
- package/crates/team-agent/src/provider/submit_now.rs +94 -0
- package/crates/team-agent/src/provider/tests/adapter.rs +384 -0
- package/crates/team-agent/src/provider/types.rs +4 -2
- package/crates/team-agent/src/provider/wire.rs +29 -1
- package/crates/team-agent/src/state/persist.rs +301 -8
- package/crates/team-agent/src/state/repository.rs +5 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +1737 -42
- package/crates/team-agent/src/tmux_backend.rs +1116 -75
- package/crates/team-agent/src/transport/tests/wire.rs +28 -2
- package/crates/team-agent/src/transport.rs +248 -1
- package/npm/install.mjs +129 -73
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +32 -239
- package/skills/team-agent/command-coverage.json +31 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +0 -59
- package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +0 -488
- package/crates/team-agent/src/lifecycle/launch/fork_pending.rs +0 -109
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +0 -447
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
//! ---
|
|
2
|
+
//! purpose: 按 pending uuid 捕获 ~/.grok/sessions/<urlencoded-cwd>/<uuid>/,不读会话正文
|
|
3
|
+
//! contract:
|
|
4
|
+
//! provides:
|
|
5
|
+
//! - name: grok_session_dir
|
|
6
|
+
//! what: 由 spawn cwd + session id 派生磁盘存档目录
|
|
7
|
+
//! - name: grok_session_archive_present
|
|
8
|
+
//! what: 只凭 marker 文件存在性判定 backing,不打开正文
|
|
9
|
+
//! - name: scan_session_store
|
|
10
|
+
//! what: expected_session_id 命中存档则返回唯一候选,否则空
|
|
11
|
+
//! requires:
|
|
12
|
+
//! - name: expected_session_id
|
|
13
|
+
//! what: 无 pending id 不扫描、不拾取同 cwd 其它会话
|
|
14
|
+
//! boundary:
|
|
15
|
+
//! - 只服务 Provider::Grok
|
|
16
|
+
//! - 不读 chat_history/events 正文,不改 claude/codex/copilot 扫描
|
|
17
|
+
//! maturity: wired
|
|
18
|
+
//! ---
|
|
19
|
+
|
|
20
|
+
use std::path::{Path, PathBuf};
|
|
21
|
+
|
|
22
|
+
use crate::provider::types::{CaptureVia, CapturedSession, Confidence, RolloutPath};
|
|
23
|
+
|
|
24
|
+
use super::{CaptureSessionContext, CapturedSessionCandidate};
|
|
25
|
+
|
|
26
|
+
/// RFC 3986 unreserved set — same as Python `urllib.parse.quote(path, safe='')`.
|
|
27
|
+
fn percent_encode_unreserved(raw: &str) -> String {
|
|
28
|
+
let mut out = String::with_capacity(raw.len() * 3);
|
|
29
|
+
for &byte in raw.as_bytes() {
|
|
30
|
+
match byte {
|
|
31
|
+
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
|
|
32
|
+
out.push(byte as char);
|
|
33
|
+
}
|
|
34
|
+
_ => {
|
|
35
|
+
out.push('%');
|
|
36
|
+
const HEX: &[u8; 16] = b"0123456789ABCDEF";
|
|
37
|
+
out.push(HEX[(byte >> 4) as usize] as char);
|
|
38
|
+
out.push(HEX[(byte & 0x0F) as usize] as char);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
out
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// ---
|
|
46
|
+
/// purpose: 由 spawn cwd 与 session id 派生 ~/.grok/sessions 下的存档目录
|
|
47
|
+
/// params:
|
|
48
|
+
/// spawn_cwd: 席位工作目录;canonicalize 后再 percent-encode
|
|
49
|
+
/// session_id: 框架预定 uuid;空或含路径分隔则拒绝
|
|
50
|
+
/// returns: HOME 下对应目录路径;HOME 缺失或 id 非法则 None
|
|
51
|
+
/// contract:
|
|
52
|
+
/// provides:
|
|
53
|
+
/// - name: grok_session_dir
|
|
54
|
+
/// what: 只派生路径,不创建目录、不读存档
|
|
55
|
+
/// boundary:
|
|
56
|
+
/// - 不枚举其它 uuid,不读会话文件
|
|
57
|
+
/// ---
|
|
58
|
+
pub(crate) fn grok_session_dir(spawn_cwd: &Path, session_id: &str) -> Option<PathBuf> {
|
|
59
|
+
if session_id.is_empty()
|
|
60
|
+
|| session_id.contains('/')
|
|
61
|
+
|| session_id.contains('\\')
|
|
62
|
+
|| session_id.contains('\0')
|
|
63
|
+
{
|
|
64
|
+
return None;
|
|
65
|
+
}
|
|
66
|
+
let home = std::env::var_os("HOME").map(PathBuf::from)?;
|
|
67
|
+
let canonical = std::fs::canonicalize(spawn_cwd).unwrap_or_else(|_| spawn_cwd.to_path_buf());
|
|
68
|
+
let encoded = percent_encode_unreserved(&canonical.to_string_lossy());
|
|
69
|
+
if encoded.is_empty() {
|
|
70
|
+
return None;
|
|
71
|
+
}
|
|
72
|
+
Some(
|
|
73
|
+
home.join(".grok")
|
|
74
|
+
.join("sessions")
|
|
75
|
+
.join(encoded)
|
|
76
|
+
.join(session_id),
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// ---
|
|
81
|
+
/// purpose: 只凭 marker 文件存在性判断 grok 会话存档是否可 resume
|
|
82
|
+
/// params:
|
|
83
|
+
/// dir: grok_session_dir 给出的目录
|
|
84
|
+
/// returns: 目录存在且含 events.jsonl 或 chat_history.jsonl 或 summary.json
|
|
85
|
+
/// contract:
|
|
86
|
+
/// provides:
|
|
87
|
+
/// - name: grok_session_archive_present
|
|
88
|
+
/// what: 用 is_file 判 backing,不打开正文
|
|
89
|
+
/// boundary:
|
|
90
|
+
/// - 不读 jsonl / summary 内容
|
|
91
|
+
/// ---
|
|
92
|
+
pub(crate) fn grok_session_archive_present(dir: &Path) -> bool {
|
|
93
|
+
if !dir.is_dir() {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
["events.jsonl", "chat_history.jsonl", "summary.json"]
|
|
97
|
+
.iter()
|
|
98
|
+
.any(|name| dir.join(name).is_file())
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// ---
|
|
102
|
+
/// purpose: 按 pending expected_session_id 捕获唯一 grok 磁盘存档
|
|
103
|
+
/// params:
|
|
104
|
+
/// context: 含 spawn_cwd 与 expected_session_id 的捕获上下文
|
|
105
|
+
/// returns: 命中则一条 FsWatch 候选,否则空向量
|
|
106
|
+
/// contract:
|
|
107
|
+
/// provides:
|
|
108
|
+
/// - name: scan_session_store
|
|
109
|
+
/// what: expected id 与目录对齐才捕获
|
|
110
|
+
/// boundary:
|
|
111
|
+
/// - 无 pending id 不扫描;不拾取同 cwd 其它会话
|
|
112
|
+
/// ---
|
|
113
|
+
pub(super) fn scan_session_store(context: &CaptureSessionContext) -> Vec<CapturedSessionCandidate> {
|
|
114
|
+
let Some(expected) = context.expected_session_id.as_ref() else {
|
|
115
|
+
return Vec::new();
|
|
116
|
+
};
|
|
117
|
+
let Some(dir) = grok_session_dir(&context.spawn_cwd, expected.as_str()) else {
|
|
118
|
+
return Vec::new();
|
|
119
|
+
};
|
|
120
|
+
if !grok_session_archive_present(&dir) {
|
|
121
|
+
return Vec::new();
|
|
122
|
+
}
|
|
123
|
+
vec![CapturedSessionCandidate {
|
|
124
|
+
captured: CapturedSession {
|
|
125
|
+
session_id: Some(expected.clone()),
|
|
126
|
+
rollout_path: Some(RolloutPath::new(dir)),
|
|
127
|
+
captured_via: CaptureVia::FsWatch,
|
|
128
|
+
attribution_confidence: Confidence::High,
|
|
129
|
+
spawn_cwd: context.spawn_cwd.clone(),
|
|
130
|
+
},
|
|
131
|
+
embedded_agent_id: None,
|
|
132
|
+
positive_agent_id_match: false,
|
|
133
|
+
agent_path_match: false,
|
|
134
|
+
}]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#[cfg(test)]
|
|
138
|
+
mod tests {
|
|
139
|
+
#![allow(clippy::unwrap_used)]
|
|
140
|
+
|
|
141
|
+
use super::*;
|
|
142
|
+
use crate::provider::types::SessionId;
|
|
143
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
144
|
+
|
|
145
|
+
struct HomeGuard {
|
|
146
|
+
prev: Option<std::ffi::OsString>,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
impl HomeGuard {
|
|
150
|
+
fn set(home: &Path) -> Self {
|
|
151
|
+
let prev = std::env::var_os("HOME");
|
|
152
|
+
std::env::set_var("HOME", home);
|
|
153
|
+
Self { prev }
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
impl Drop for HomeGuard {
|
|
158
|
+
fn drop(&mut self) {
|
|
159
|
+
match &self.prev {
|
|
160
|
+
Some(v) => std::env::set_var("HOME", v),
|
|
161
|
+
None => std::env::remove_var("HOME"),
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
fn tmp_root(tag: &str) -> PathBuf {
|
|
167
|
+
static CTR: AtomicU64 = AtomicU64::new(0);
|
|
168
|
+
let dir = std::env::temp_dir().join(format!(
|
|
169
|
+
"ta-d111-grok-scan-{}-{}-{}",
|
|
170
|
+
tag,
|
|
171
|
+
std::process::id(),
|
|
172
|
+
CTR.fetch_add(1, Ordering::Relaxed)
|
|
173
|
+
));
|
|
174
|
+
let _ = std::fs::remove_dir_all(&dir);
|
|
175
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
176
|
+
std::fs::canonicalize(&dir).unwrap()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fn seed_archive(home: &Path, cwd: &Path, session_id: &str) -> PathBuf {
|
|
180
|
+
let dir = grok_session_dir(cwd, session_id).expect("dir");
|
|
181
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
182
|
+
std::fs::write(dir.join("events.jsonl"), b"").unwrap();
|
|
183
|
+
dir
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
fn ctx(cwd: &Path, expected: Option<&str>) -> CaptureSessionContext {
|
|
187
|
+
CaptureSessionContext {
|
|
188
|
+
agent_id: "w1".to_string(),
|
|
189
|
+
spawn_cwd: cwd.to_path_buf(),
|
|
190
|
+
pane_id: None,
|
|
191
|
+
pane_pid: None,
|
|
192
|
+
spawned_at: None,
|
|
193
|
+
expected_session_id: expected.map(SessionId::new),
|
|
194
|
+
provider_projects_root: None,
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
#[test]
|
|
199
|
+
fn grok_cwd_encoding_matches_python_quote_empty_safe() {
|
|
200
|
+
assert_eq!(
|
|
201
|
+
percent_encode_unreserved("/private/tmp/ta-t105-repro-83327/ws"),
|
|
202
|
+
"%2Fprivate%2Ftmp%2Fta-t105-repro-83327%2Fws"
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
#[test]
|
|
207
|
+
#[serial_test::serial(env)]
|
|
208
|
+
fn grok_expected_id_captures_archive_without_reading_body() {
|
|
209
|
+
let base = tmp_root("hit");
|
|
210
|
+
let home = base.join("home");
|
|
211
|
+
let cwd = base.join("ws");
|
|
212
|
+
std::fs::create_dir_all(&home).unwrap();
|
|
213
|
+
std::fs::create_dir_all(&cwd).unwrap();
|
|
214
|
+
let sid = "75fce302-0ce9-4635-a788-f4aec11a3f6a";
|
|
215
|
+
let _guard = HomeGuard::set(&home);
|
|
216
|
+
let dir = seed_archive(&home, &cwd, sid);
|
|
217
|
+
std::fs::write(
|
|
218
|
+
dir.join("chat_history.jsonl"),
|
|
219
|
+
b"THIS-BODY-MUST-NOT-BE-PARSED\n",
|
|
220
|
+
)
|
|
221
|
+
.unwrap();
|
|
222
|
+
let out = scan_session_store(&ctx(&cwd, Some(sid)));
|
|
223
|
+
assert_eq!(out.len(), 1);
|
|
224
|
+
assert_eq!(out[0].captured.session_id.as_ref().unwrap().as_str(), sid);
|
|
225
|
+
assert_eq!(
|
|
226
|
+
out[0]
|
|
227
|
+
.captured
|
|
228
|
+
.rollout_path
|
|
229
|
+
.as_ref()
|
|
230
|
+
.map(|p| p.as_path().to_path_buf()),
|
|
231
|
+
Some(dir)
|
|
232
|
+
);
|
|
233
|
+
let _ = std::fs::remove_dir_all(&base);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
#[test]
|
|
237
|
+
#[serial_test::serial(env)]
|
|
238
|
+
fn grok_expected_id_does_not_steal_foreign_cwd_archive() {
|
|
239
|
+
let base = tmp_root("foreign");
|
|
240
|
+
let home = base.join("home");
|
|
241
|
+
let cwd = base.join("ws");
|
|
242
|
+
let foreign = base.join("other");
|
|
243
|
+
std::fs::create_dir_all(&home).unwrap();
|
|
244
|
+
std::fs::create_dir_all(&cwd).unwrap();
|
|
245
|
+
std::fs::create_dir_all(&foreign).unwrap();
|
|
246
|
+
let sid = "4e7b5a6e-e09a-4291-9c75-563107105185";
|
|
247
|
+
let _guard = HomeGuard::set(&home);
|
|
248
|
+
seed_archive(&home, &foreign, sid);
|
|
249
|
+
let out = scan_session_store(&ctx(&cwd, Some(sid)));
|
|
250
|
+
assert!(
|
|
251
|
+
out.is_empty(),
|
|
252
|
+
"foreign cwd archive must not bind; got {out:?}"
|
|
253
|
+
);
|
|
254
|
+
let _ = std::fs::remove_dir_all(&base);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
#[test]
|
|
258
|
+
#[serial_test::serial(env)]
|
|
259
|
+
fn grok_scan_without_expected_id_returns_empty() {
|
|
260
|
+
let base = tmp_root("no-pending");
|
|
261
|
+
let home = base.join("home");
|
|
262
|
+
let cwd = base.join("ws");
|
|
263
|
+
std::fs::create_dir_all(&home).unwrap();
|
|
264
|
+
std::fs::create_dir_all(&cwd).unwrap();
|
|
265
|
+
let _guard = HomeGuard::set(&home);
|
|
266
|
+
seed_archive(&home, &cwd, "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa");
|
|
267
|
+
let out = scan_session_store(&ctx(&cwd, None));
|
|
268
|
+
assert!(out.is_empty());
|
|
269
|
+
let _ = std::fs::remove_dir_all(&base);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
#[test]
|
|
273
|
+
#[serial_test::serial(env)]
|
|
274
|
+
fn grok_scan_empty_dir_without_marker_is_not_backing() {
|
|
275
|
+
let base = tmp_root("empty");
|
|
276
|
+
let home = base.join("home");
|
|
277
|
+
let cwd = base.join("ws");
|
|
278
|
+
std::fs::create_dir_all(&home).unwrap();
|
|
279
|
+
std::fs::create_dir_all(&cwd).unwrap();
|
|
280
|
+
let sid = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb";
|
|
281
|
+
let _guard = HomeGuard::set(&home);
|
|
282
|
+
let dir = grok_session_dir(&cwd, sid).unwrap();
|
|
283
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
284
|
+
assert!(!grok_session_archive_present(&dir));
|
|
285
|
+
assert!(scan_session_store(&ctx(&cwd, Some(sid))).is_empty());
|
|
286
|
+
let _ = std::fs::remove_dir_all(&base);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
//! ---
|
|
2
|
+
//! purpose: 在记录的 Pi 席位 session root 内按真实 header 捕获 exact JSONL backing
|
|
3
|
+
//! contract:
|
|
4
|
+
//! provides:
|
|
5
|
+
//! - name: scan_session_store
|
|
6
|
+
//! what: 只读每个 JSONL 的首个完整 header,并按 id/cwd/spawned_at 返回候选
|
|
7
|
+
//! - name: validate_exact_backing
|
|
8
|
+
//! what: restart 前重验 exact path 的 id/cwd header
|
|
9
|
+
//! boundary:
|
|
10
|
+
//! - 只递归 recorded provider_projects_root,不扫描 HOME 或消息正文
|
|
11
|
+
//! - 不以文件名、mtime 或唯一文件替代 header 匹配
|
|
12
|
+
//! maturity: wired
|
|
13
|
+
//! ---
|
|
14
|
+
|
|
15
|
+
use std::io::BufRead;
|
|
16
|
+
use std::path::{Path, PathBuf};
|
|
17
|
+
|
|
18
|
+
use crate::provider::types::{CaptureVia, CapturedSession, Confidence, RolloutPath, SessionId};
|
|
19
|
+
use crate::provider::ProviderError;
|
|
20
|
+
|
|
21
|
+
use super::{CaptureSessionContext, CapturedSessionCandidate};
|
|
22
|
+
|
|
23
|
+
#[derive(serde::Deserialize)]
|
|
24
|
+
struct PiSessionHeader {
|
|
25
|
+
#[serde(rename = "type")]
|
|
26
|
+
record_type: String,
|
|
27
|
+
version: u64,
|
|
28
|
+
id: String,
|
|
29
|
+
cwd: PathBuf,
|
|
30
|
+
timestamp: String,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fn paths_equal(left: &Path, right: &Path) -> bool {
|
|
34
|
+
let left = std::fs::canonicalize(left).unwrap_or_else(|_| left.to_path_buf());
|
|
35
|
+
let right = std::fs::canonicalize(right).unwrap_or_else(|_| right.to_path_buf());
|
|
36
|
+
left == right
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
fn read_header(path: &Path) -> Option<PiSessionHeader> {
|
|
40
|
+
let file = std::fs::File::open(path).ok()?;
|
|
41
|
+
let mut first_line = String::new();
|
|
42
|
+
let bytes = std::io::BufReader::new(file)
|
|
43
|
+
.read_line(&mut first_line)
|
|
44
|
+
.ok()?;
|
|
45
|
+
if bytes == 0 || !first_line.ends_with('\n') {
|
|
46
|
+
return None;
|
|
47
|
+
}
|
|
48
|
+
let header: PiSessionHeader = serde_json::from_str(first_line.trim_end()).ok()?;
|
|
49
|
+
(header.record_type == "session" && header.version == 3).then_some(header)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fn header_matches(
|
|
53
|
+
header: &PiSessionHeader,
|
|
54
|
+
expected_session_id: &SessionId,
|
|
55
|
+
spawn_cwd: &Path,
|
|
56
|
+
spawned_at: Option<&str>,
|
|
57
|
+
) -> bool {
|
|
58
|
+
if header.id != expected_session_id.as_str() || !paths_equal(&header.cwd, spawn_cwd) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
let Ok(header_timestamp) = chrono::DateTime::parse_from_rfc3339(&header.timestamp) else {
|
|
62
|
+
return false;
|
|
63
|
+
};
|
|
64
|
+
let Some(spawned_at) = spawned_at else {
|
|
65
|
+
return true;
|
|
66
|
+
};
|
|
67
|
+
let Ok(spawned_at) = chrono::DateTime::parse_from_rfc3339(spawned_at) else {
|
|
68
|
+
return false;
|
|
69
|
+
};
|
|
70
|
+
header_timestamp >= spawned_at
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fn collect_jsonl(root: &Path, out: &mut Vec<PathBuf>) -> Result<(), ProviderError> {
|
|
74
|
+
let entries = std::fs::read_dir(root)
|
|
75
|
+
.map_err(|error| ProviderError::Io(format!("{}: {error}", root.display())))?;
|
|
76
|
+
for entry in entries {
|
|
77
|
+
let entry =
|
|
78
|
+
entry.map_err(|error| ProviderError::Io(format!("{}: {error}", root.display())))?;
|
|
79
|
+
let file_type = entry
|
|
80
|
+
.file_type()
|
|
81
|
+
.map_err(|error| ProviderError::Io(format!("{}: {error}", entry.path().display())))?;
|
|
82
|
+
if file_type.is_symlink() {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
let path = entry.path();
|
|
86
|
+
if file_type.is_dir() {
|
|
87
|
+
collect_jsonl(&path, out)?;
|
|
88
|
+
} else if file_type.is_file() && path.extension().is_some_and(|ext| ext == "jsonl") {
|
|
89
|
+
out.push(path);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
Ok(())
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// ---
|
|
96
|
+
/// purpose: restart 前重验调用方给出的 exact Pi JSONL backing
|
|
97
|
+
/// returns: header type/version/id/cwd 全匹配时成功
|
|
98
|
+
/// errors: 文件缺失、header 不完整或 identity 不匹配时返回 ResumeUnavailable
|
|
99
|
+
/// ---
|
|
100
|
+
pub(crate) fn validate_exact_backing(
|
|
101
|
+
path: &Path,
|
|
102
|
+
expected_session_id: &SessionId,
|
|
103
|
+
spawn_cwd: &Path,
|
|
104
|
+
) -> Result<(), ProviderError> {
|
|
105
|
+
if !path.is_file() {
|
|
106
|
+
return Err(ProviderError::ResumeUnavailable(format!(
|
|
107
|
+
"Pi exact session backing is missing: {}",
|
|
108
|
+
path.display()
|
|
109
|
+
)));
|
|
110
|
+
}
|
|
111
|
+
let header = read_header(path).ok_or_else(|| {
|
|
112
|
+
ProviderError::ResumeUnavailable(format!(
|
|
113
|
+
"Pi exact session backing has no complete session header: {}",
|
|
114
|
+
path.display()
|
|
115
|
+
))
|
|
116
|
+
})?;
|
|
117
|
+
if !header_matches(&header, expected_session_id, spawn_cwd, None) {
|
|
118
|
+
return Err(ProviderError::ResumeUnavailable(format!(
|
|
119
|
+
"Pi exact session backing header does not match id/cwd: {}",
|
|
120
|
+
path.display()
|
|
121
|
+
)));
|
|
122
|
+
}
|
|
123
|
+
Ok(())
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/// ---
|
|
127
|
+
/// purpose: 仅在 recorded seat root 内递归捕获 Pi session header 候选
|
|
128
|
+
/// returns: 每个 id/cwd/timestamp 精确匹配的 JSONL 各返回一个候选
|
|
129
|
+
/// errors: root 内目录枚举失败时返回 ProviderError
|
|
130
|
+
/// ---
|
|
131
|
+
pub(super) fn scan_session_store(
|
|
132
|
+
context: &CaptureSessionContext,
|
|
133
|
+
) -> Result<Vec<CapturedSessionCandidate>, ProviderError> {
|
|
134
|
+
let (Some(root), Some(expected_session_id), Some(spawned_at)) = (
|
|
135
|
+
context.provider_projects_root.as_deref(),
|
|
136
|
+
context.expected_session_id.as_ref(),
|
|
137
|
+
context.spawned_at.as_deref(),
|
|
138
|
+
) else {
|
|
139
|
+
return Ok(Vec::new());
|
|
140
|
+
};
|
|
141
|
+
if !root.is_dir() {
|
|
142
|
+
return Ok(Vec::new());
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let mut paths = Vec::new();
|
|
146
|
+
collect_jsonl(root, &mut paths)?;
|
|
147
|
+
paths.sort();
|
|
148
|
+
Ok(paths
|
|
149
|
+
.into_iter()
|
|
150
|
+
.filter_map(|path| {
|
|
151
|
+
let header = read_header(&path)?;
|
|
152
|
+
header_matches(
|
|
153
|
+
&header,
|
|
154
|
+
expected_session_id,
|
|
155
|
+
&context.spawn_cwd,
|
|
156
|
+
Some(spawned_at),
|
|
157
|
+
)
|
|
158
|
+
.then(|| CapturedSessionCandidate {
|
|
159
|
+
captured: CapturedSession {
|
|
160
|
+
session_id: Some(expected_session_id.clone()),
|
|
161
|
+
rollout_path: Some(RolloutPath::new(path)),
|
|
162
|
+
captured_via: CaptureVia::FsWatch,
|
|
163
|
+
attribution_confidence: Confidence::High,
|
|
164
|
+
spawn_cwd: context.spawn_cwd.clone(),
|
|
165
|
+
},
|
|
166
|
+
embedded_agent_id: None,
|
|
167
|
+
positive_agent_id_match: false,
|
|
168
|
+
agent_path_match: false,
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
.collect())
|
|
172
|
+
}
|
|
@@ -14,6 +14,9 @@ pub(crate) mod claude;
|
|
|
14
14
|
pub(crate) mod codex;
|
|
15
15
|
pub(crate) mod common;
|
|
16
16
|
pub(crate) mod copilot;
|
|
17
|
+
pub(crate) mod cursor;
|
|
18
|
+
pub(crate) mod grok;
|
|
19
|
+
pub(crate) mod pi;
|
|
17
20
|
|
|
18
21
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
19
22
|
pub struct CaptureSessionContext {
|
|
@@ -41,6 +44,15 @@ pub(crate) fn scan_session_candidates_once(
|
|
|
41
44
|
if matches!(provider, Provider::Copilot) {
|
|
42
45
|
return Ok(copilot::scan_session_store(context));
|
|
43
46
|
}
|
|
47
|
+
if matches!(provider, Provider::Grok) {
|
|
48
|
+
return Ok(grok::scan_session_store(context));
|
|
49
|
+
}
|
|
50
|
+
if matches!(provider, Provider::CursorAgent) {
|
|
51
|
+
return Ok(cursor::scan_session_store(context));
|
|
52
|
+
}
|
|
53
|
+
if matches!(provider, Provider::Pi) {
|
|
54
|
+
return pi::scan_session_store(context);
|
|
55
|
+
}
|
|
44
56
|
if matches!(provider, Provider::Claude | Provider::ClaudeCode)
|
|
45
57
|
&& context.expected_session_id.is_some()
|
|
46
58
|
{
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
//! ---
|
|
2
|
+
//! purpose: grok 忙时默认把显式队列顶出去(再按回车)
|
|
3
|
+
//! contract:
|
|
4
|
+
//! provides:
|
|
5
|
+
//! - name: flush_explicit_queue
|
|
6
|
+
//! what: 屏幕出现 grok `Enter:send now` 时只重按回车直到消失;默认不认 cursor 页脚
|
|
7
|
+
//! depends:
|
|
8
|
+
//! - crate::transport::Transport
|
|
9
|
+
//! boundary:
|
|
10
|
+
//! - 不重粘文本、不用 Escape、不用 Ctrl-C、不加自动重投
|
|
11
|
+
//! - 不按忙闲决定发不发;只在已经注入之后确认队列态
|
|
12
|
+
//! - 无标记时不改 claude/codex 提交行为
|
|
13
|
+
//! - cursor 第二下 Enter 会打断进行中回合;生产 flush 不带 cursor 标记
|
|
14
|
+
//! maturity: wired
|
|
15
|
+
//! ---
|
|
16
|
+
|
|
17
|
+
use std::time::Duration;
|
|
18
|
+
|
|
19
|
+
use crate::transport::{CaptureRange, Key, Target, Transport, TransportError};
|
|
20
|
+
|
|
21
|
+
/// grok 1.0.4 实测:页脚出现这串就是显式队列。
|
|
22
|
+
pub const GROK_SEND_NOW_MARK: &str = "Enter:send now";
|
|
23
|
+
|
|
24
|
+
/// 2026-08-18 真机,cursor-agent 2026.08.11-e8db854。
|
|
25
|
+
/// 忙时 follow-ups 盒页脚原文(大小写按屏上):
|
|
26
|
+
/// `enter send now · ↑ select/edit · esc cancel`
|
|
27
|
+
/// 出处:`.team/scripts/cursor_send.sh` 的 `QUEUE_MARK='enter send now'`
|
|
28
|
+
/// (脚本头 2026-08-18 真机段)。与 grok 的 `Enter:send now`(大写 E、冒号)
|
|
29
|
+
/// 不是同一串。
|
|
30
|
+
pub const CURSOR_SEND_NOW_MARK: &str = "enter send now";
|
|
31
|
+
|
|
32
|
+
/// 与 grok_send.sh / cursor_send.sh 对齐:最多再按 8 次回车。
|
|
33
|
+
pub const GROK_SEND_NOW_MAX_ENTERS: u32 = 8;
|
|
34
|
+
|
|
35
|
+
pub struct FlushReport {
|
|
36
|
+
pub extra_enters: u32,
|
|
37
|
+
pub mark_cleared: bool,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// After paste+first Enter, if the pane still shows a queue footer,
|
|
41
|
+
/// press Enter only until the mark is gone. No mark ⇒ no keys (claude/codex).
|
|
42
|
+
pub fn flush_explicit_queue(
|
|
43
|
+
transport: &dyn Transport,
|
|
44
|
+
target: &Target,
|
|
45
|
+
) -> Result<FlushReport, TransportError> {
|
|
46
|
+
flush_explicit_queue_for(transport, target, &[GROK_SEND_NOW_MARK])
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// One detector path at a time. Production flush is grok-only.
|
|
50
|
+
/// Tests enable a single mark so a false-positive cannot hide behind the other path.
|
|
51
|
+
pub fn flush_explicit_queue_for(
|
|
52
|
+
transport: &dyn Transport,
|
|
53
|
+
target: &Target,
|
|
54
|
+
marks: &[&str],
|
|
55
|
+
) -> Result<FlushReport, TransportError> {
|
|
56
|
+
let mut extra_enters = 0;
|
|
57
|
+
for _ in 0..GROK_SEND_NOW_MAX_ENTERS {
|
|
58
|
+
let captured = transport.capture(target, CaptureRange::Tail(40))?;
|
|
59
|
+
if !queue_mark_visible_for(&captured.text, marks) {
|
|
60
|
+
return Ok(FlushReport {
|
|
61
|
+
extra_enters,
|
|
62
|
+
mark_cleared: true,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
transport.send_keys(target, &[Key::Enter])?;
|
|
66
|
+
extra_enters = extra_enters.saturating_add(1);
|
|
67
|
+
std::thread::sleep(retry_pause());
|
|
68
|
+
}
|
|
69
|
+
let captured = transport.capture(target, CaptureRange::Tail(40))?;
|
|
70
|
+
Ok(FlushReport {
|
|
71
|
+
extra_enters,
|
|
72
|
+
mark_cleared: !queue_mark_visible_for(&captured.text, marks),
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
pub fn keep_provider_queue_requested() -> bool {
|
|
77
|
+
matches!(
|
|
78
|
+
std::env::var("TEAM_AGENT_KEEP_PROVIDER_QUEUE").as_deref(),
|
|
79
|
+
Ok("1") | Ok("true") | Ok("yes")
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
pub fn queue_mark_visible(text: &str) -> bool {
|
|
84
|
+
queue_mark_visible_for(text, &[GROK_SEND_NOW_MARK])
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
pub fn queue_mark_visible_for(text: &str, marks: &[&str]) -> bool {
|
|
88
|
+
marks.iter().any(|mark| text.contains(mark))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#[allow(dead_code)]
|
|
92
|
+
fn retry_pause() -> Duration {
|
|
93
|
+
Duration::from_millis(50)
|
|
94
|
+
}
|