@team-agent/installer 0.5.65 → 0.5.68

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.
Files changed (208) hide show
  1. package/Cargo.lock +8 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +6 -0
  4. package/crates/team-agent/src/cli/adapters.rs +6 -1
  5. package/crates/team-agent/src/cli/diagnose.rs +82 -0
  6. package/crates/team-agent/src/cli/emit.rs +3 -3
  7. package/crates/team-agent/src/cli/grok_slot.rs +299 -0
  8. package/crates/team-agent/src/cli/leader.rs +21 -7
  9. package/crates/team-agent/src/cli/leaders.rs +125 -1
  10. package/crates/team-agent/src/cli/mod.rs +20 -0
  11. package/crates/team-agent/src/cli/send/presentation.rs +7 -1
  12. package/crates/team-agent/src/cli/spec.rs +2 -0
  13. package/crates/team-agent/src/cli/status_port/compact.rs +28 -0
  14. package/crates/team-agent/src/cli/status_port/snapshot.rs +25 -1
  15. package/crates/team-agent/src/cli/tests/base.rs +26 -3
  16. package/crates/team-agent/src/cli/tests/compile.rs +1 -1
  17. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +129 -19
  18. package/crates/team-agent/src/cli/tests/mod.rs +2 -2
  19. package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -1
  20. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +387 -6
  21. package/crates/team-agent/src/cli/tests/status_send.rs +79 -10
  22. package/crates/team-agent/src/cli/tests/verb_validate.rs +1 -1
  23. package/crates/team-agent/src/communication_mode/mod.rs +6 -4
  24. package/crates/team-agent/src/compiler/tests.rs +23 -20
  25. package/crates/team-agent/src/compiler.rs +84 -5
  26. package/crates/team-agent/src/conpty/backend.rs +16 -0
  27. package/crates/team-agent/src/coordinator/backoff.rs +55 -0
  28. package/crates/team-agent/src/coordinator/conpty_shim.rs +82 -0
  29. package/crates/team-agent/src/coordinator/health.rs +173 -0
  30. package/crates/team-agent/src/coordinator/mod.rs +31 -0
  31. package/crates/team-agent/src/coordinator/orphan.rs +31 -0
  32. package/crates/team-agent/src/coordinator/runtime_detectors.rs +30 -0
  33. package/crates/team-agent/src/coordinator/runtime_observation.rs +25 -0
  34. package/crates/team-agent/src/coordinator/steps/abnormal.rs +59 -0
  35. package/crates/team-agent/src/coordinator/steps/delivery.rs +10 -0
  36. package/crates/team-agent/src/coordinator/steps/health_sync.rs +10 -0
  37. package/crates/team-agent/src/coordinator/steps/mod.rs +22 -0
  38. package/crates/team-agent/src/coordinator/steps/persist.rs +10 -0
  39. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +10 -0
  40. package/crates/team-agent/src/coordinator/steps/session_gate.rs +10 -0
  41. package/crates/team-agent/src/coordinator/tick.rs +133 -22
  42. package/crates/team-agent/src/coordinator/types.rs +49 -0
  43. package/crates/team-agent/src/db/message_store.rs +39 -5
  44. package/crates/team-agent/src/layout/worker_env.rs +20 -3
  45. package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
  46. package/crates/team-agent/src/leader/provider_attribution.rs +53 -0
  47. package/crates/team-agent/src/leader/registry.rs +65 -0
  48. package/crates/team-agent/src/leader/start.rs +1080 -58
  49. package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1 -1
  50. package/crates/team-agent/src/lifecycle/display.rs +56 -0
  51. package/crates/team-agent/src/lifecycle/helpers.rs +57 -0
  52. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +332 -14
  53. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +69 -2
  54. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +196 -2
  55. package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +101 -11
  56. package/crates/team-agent/src/lifecycle/launch/cursor_create_chat.rs +229 -0
  57. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +332 -0
  58. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +204 -457
  59. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +24 -0
  60. package/crates/team-agent/src/lifecycle/launch/grok_per_seat.rs +260 -0
  61. package/crates/team-agent/src/lifecycle/launch/identity.rs +146 -0
  62. package/crates/team-agent/src/lifecycle/launch/layout.rs +60 -0
  63. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +112 -0
  64. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +530 -0
  65. package/crates/team-agent/src/lifecycle/launch/ownership.rs +40 -0
  66. package/crates/team-agent/src/lifecycle/launch/plan.rs +31 -0
  67. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +66 -0
  68. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +78 -0
  69. package/crates/team-agent/src/lifecycle/launch/readiness.rs +85 -38
  70. package/crates/team-agent/src/lifecycle/launch/role_source.rs +44 -44
  71. package/crates/team-agent/src/lifecycle/launch/spawn.rs +101 -4
  72. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +244 -48
  73. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +109 -2
  74. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +214 -1
  75. package/crates/team-agent/src/lifecycle/launch.rs +209 -43
  76. package/crates/team-agent/src/lifecycle/lock.rs +42 -1
  77. package/crates/team-agent/src/lifecycle/mod.rs +11 -0
  78. package/crates/team-agent/src/lifecycle/pane_input_lock.rs +161 -0
  79. package/crates/team-agent/src/lifecycle/profile_launch.rs +98 -0
  80. package/crates/team-agent/src/lifecycle/profile_smoke.rs +51 -1
  81. package/crates/team-agent/src/lifecycle/restart/agent.rs +213 -3
  82. package/crates/team-agent/src/lifecycle/restart/common.rs +373 -18
  83. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +27 -0
  84. package/crates/team-agent/src/lifecycle/restart/preflight.rs +25 -0
  85. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +165 -15
  86. package/crates/team-agent/src/lifecycle/restart/remove.rs +196 -5
  87. package/crates/team-agent/src/lifecycle/restart/selection.rs +72 -0
  88. package/crates/team-agent/src/lifecycle/restart/team_state.rs +22 -0
  89. package/crates/team-agent/src/lifecycle/restart.rs +29 -0
  90. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1 -1
  91. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +3 -4
  92. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +1 -1
  93. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +2 -2
  94. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +4 -4
  95. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +309 -0
  96. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +159 -174
  97. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +2 -2
  98. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +15 -13
  99. package/crates/team-agent/src/lifecycle/tests/core.rs +4 -18
  100. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +1 -1
  101. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +287 -0
  102. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +229 -0
  103. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +353 -0
  104. package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +1 -1
  105. package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +1 -1
  106. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +784 -0
  107. package/crates/team-agent/src/lifecycle/tests/gate_fixtures.rs +562 -0
  108. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +239 -0
  109. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +498 -0
  110. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +250 -0
  111. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +293 -0
  112. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +3 -0
  113. package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +3 -3
  114. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +65 -40
  115. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +20 -32
  116. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +17 -9
  117. package/crates/team-agent/src/lifecycle/tests/mcp_tool_name_format_red.rs +69 -0
  118. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +24 -23
  119. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +98 -62
  120. package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +2 -2
  121. package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +1 -1
  122. package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +1 -1
  123. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +3 -1
  124. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -2
  125. package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +1 -1
  126. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +1 -1
  127. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +41 -8
  128. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +3 -3
  129. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1 -1
  130. package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +1 -1
  131. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +1 -1
  132. package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1 -1
  133. package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1 -1
  134. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +1 -1
  135. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +60 -2
  136. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +1 -1
  137. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +2 -2
  138. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +69 -26
  139. package/crates/team-agent/src/lifecycle/tests.rs +23 -15
  140. package/crates/team-agent/src/lifecycle/types.rs +61 -2
  141. package/crates/team-agent/src/lifecycle/worker_command_context.rs +200 -33
  142. package/crates/team-agent/src/mcp_server/tests/scoped.rs +100 -1
  143. package/crates/team-agent/src/mcp_server/tests.rs +196 -2
  144. package/crates/team-agent/src/messaging/delivery.rs +405 -40
  145. package/crates/team-agent/src/messaging/helpers.rs +2 -0
  146. package/crates/team-agent/src/messaging/leader_receiver.rs +53 -1
  147. package/crates/team-agent/src/messaging/results.rs +4 -0
  148. package/crates/team-agent/src/messaging/send.rs +34 -0
  149. package/crates/team-agent/src/messaging/tests/dup_inject.rs +406 -0
  150. package/crates/team-agent/src/messaging/tests/e23.rs +9 -0
  151. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +66 -0
  152. package/crates/team-agent/src/messaging/tests/main_preserved.rs +1 -1
  153. package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
  154. package/crates/team-agent/src/messaging/types.rs +4 -1
  155. package/crates/team-agent/src/model/enums.rs +14 -5
  156. package/crates/team-agent/src/model/permissions.rs +3 -0
  157. package/crates/team-agent/src/model/spec.rs +129 -6
  158. package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +3 -1
  159. package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +3 -1
  160. package/crates/team-agent/src/model/testdata/team.spec.yaml +3 -1
  161. package/crates/team-agent/src/os_probe.rs +73 -2
  162. package/crates/team-agent/src/provider/adapter.rs +231 -4
  163. package/crates/team-agent/src/provider/adapters/claude.rs +5 -2
  164. package/crates/team-agent/src/provider/adapters/codex.rs +5 -2
  165. package/crates/team-agent/src/provider/adapters/copilot.rs +5 -2
  166. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +86 -0
  167. package/crates/team-agent/src/provider/adapters/grok.rs +157 -0
  168. package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
  169. package/crates/team-agent/src/provider/bypass_flags.rs +124 -0
  170. package/crates/team-agent/src/provider/classify.rs +4 -2
  171. package/crates/team-agent/src/provider/faults.rs +2 -1
  172. package/crates/team-agent/src/provider/mod.rs +13 -0
  173. package/crates/team-agent/src/provider/session/capture.rs +158 -20
  174. package/crates/team-agent/src/provider/session/context_fork/claude.rs +38 -1
  175. package/crates/team-agent/src/provider/session/context_fork/codex.rs +72 -1
  176. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +57 -1
  177. package/crates/team-agent/src/provider/session/context_fork.rs +77 -3
  178. package/crates/team-agent/src/provider/session/mod.rs +18 -0
  179. package/crates/team-agent/src/provider/session/resume.rs +57 -0
  180. package/crates/team-agent/src/provider/session_scan/claude.rs +125 -1
  181. package/crates/team-agent/src/provider/session_scan/codex.rs +94 -1
  182. package/crates/team-agent/src/provider/session_scan/common.rs +204 -2
  183. package/crates/team-agent/src/provider/session_scan/copilot.rs +33 -1
  184. package/crates/team-agent/src/provider/session_scan/cursor.rs +538 -0
  185. package/crates/team-agent/src/provider/session_scan/grok.rs +288 -0
  186. package/crates/team-agent/src/provider/session_scan.rs +8 -0
  187. package/crates/team-agent/src/provider/submit_now.rs +94 -0
  188. package/crates/team-agent/src/provider/tests/adapter.rs +304 -0
  189. package/crates/team-agent/src/provider/types.rs +4 -2
  190. package/crates/team-agent/src/provider/wire.rs +23 -1
  191. package/crates/team-agent/src/state/persist.rs +301 -8
  192. package/crates/team-agent/src/state/repository.rs +5 -0
  193. package/crates/team-agent/src/tmux_backend/tests.rs +1737 -42
  194. package/crates/team-agent/src/tmux_backend.rs +1083 -75
  195. package/crates/team-agent/src/transport/test_support.rs +20 -0
  196. package/crates/team-agent/src/transport/tests/wire.rs +28 -2
  197. package/crates/team-agent/src/transport.rs +265 -1
  198. package/npm/install.mjs +129 -73
  199. package/package.json +4 -4
  200. package/skills/team-agent/SKILL.md +33 -238
  201. package/skills/team-agent/command-coverage.json +31 -0
  202. package/crates/team-agent/src/lifecycle/launch/approval.rs +0 -134
  203. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +0 -59
  204. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +0 -483
  205. package/crates/team-agent/src/lifecycle/launch/fork_pending.rs +0 -109
  206. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +0 -447
  207. package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +0 -1187
  208. package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +0 -327
@@ -0,0 +1,538 @@
1
+ //! ---
2
+ //! purpose: 捕获 ~/.cursor/chats/ 存档:pending chatId 或 workspace hex 下唯一新目录
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: cursor_session_dir
6
+ //! what: 由 HOME + chatId 派生磁盘存档目录(直达或一层 hex 子目录)
7
+ //! - name: cursor_session_archive_present
8
+ //! what: 只凭 marker 文件存在性判定 backing,不打开正文
9
+ //! - name: scan_session_store
10
+ //! what: pending 命中则返回该 id;否则仅当 spawn_cwd 的 md5 hex 下唯一新 marker 目录才捕获
11
+ //! requires:
12
+ //! - name: workspace-hex-or-pending
13
+ //! what: 无 pending 时只看 ~/.cursor/chats/<md5(canonical cwd)>/,不扫全库
14
+ //! boundary:
15
+ //! - 只服务 Provider::CursorAgent
16
+ //! - 不发明 --session-id;不读 store.db/meta.json 正文
17
+ //! - 0 或 ≥2 个新目录 → 空向量,不拾取邻席
18
+ //! - 不改 grok/claude/codex/copilot 扫描
19
+ //! maturity: wired
20
+ //! ---
21
+
22
+ use std::path::{Path, PathBuf};
23
+
24
+ use crate::provider::types::{CaptureVia, CapturedSession, Confidence, RolloutPath, SessionId};
25
+
26
+ use super::{CaptureSessionContext, CapturedSessionCandidate};
27
+
28
+ fn session_id_legal(session_id: &str) -> bool {
29
+ !session_id.is_empty()
30
+ && !session_id.contains('/')
31
+ && !session_id.contains('\\')
32
+ && !session_id.contains('\0')
33
+ }
34
+
35
+ /// Marker names from 2026-08-21 U-02 `ls` of `~/.cursor/chats/<hex>/<uuid>/`
36
+ /// (names only). Existence is backing; contents are not read.
37
+ const MARKERS: &[&str] = &["store.db", "meta.json"];
38
+
39
+ /// ---
40
+ /// purpose: 只凭 marker 文件存在性判断 cursor 会话存档是否可 resume
41
+ /// params:
42
+ /// dir: cursor_session_dir 给出的目录
43
+ /// returns: 目录存在且含 store.db 或 meta.json
44
+ /// contract:
45
+ /// provides:
46
+ /// - name: cursor_session_archive_present
47
+ /// what: 用 is_file 判 backing,不打开正文
48
+ /// boundary:
49
+ /// - 不读 json / sqlite 内容
50
+ /// ---
51
+ pub(crate) fn cursor_session_archive_present(dir: &Path) -> bool {
52
+ if !dir.is_dir() {
53
+ return false;
54
+ }
55
+ MARKERS.iter().any(|name| dir.join(name).is_file())
56
+ }
57
+
58
+ fn first_present_dir(candidates: impl IntoIterator<Item = PathBuf>) -> Option<PathBuf> {
59
+ candidates.into_iter().find(|dir| cursor_session_archive_present(dir))
60
+ }
61
+
62
+ /// ---
63
+ /// purpose: 由 chatId 派生 ~/.cursor/chats 下的存档目录
64
+ /// params:
65
+ /// session_id: CLI chatId;空或含路径分隔则拒绝
66
+ /// returns: HOME 下含 marker 的目录;HOME 缺失或 id 非法则 None
67
+ /// contract:
68
+ /// provides:
69
+ /// - name: cursor_session_dir
70
+ /// what: 只派生并核 marker 存在性,不创建目录、不读存档
71
+ /// boundary:
72
+ /// - 无 pending 时不枚举其它 chatId
73
+ /// ---
74
+ pub(crate) fn cursor_session_dir(session_id: &str) -> Option<PathBuf> {
75
+ if !session_id_legal(session_id) {
76
+ return None;
77
+ }
78
+ let home = std::env::var_os("HOME").map(PathBuf::from)?;
79
+ let chats = home.join(".cursor").join("chats");
80
+ let direct = chats.join(session_id);
81
+ if let Some(dir) = first_present_dir([direct.clone()]) {
82
+ return Some(dir);
83
+ }
84
+ // U-02 layout: ~/.cursor/chats/<hex32>/<uuid>/store.db
85
+ if direct.is_dir() {
86
+ if let Ok(entries) = std::fs::read_dir(&direct) {
87
+ for entry in entries.flatten() {
88
+ let child = entry.path();
89
+ if cursor_session_archive_present(&child) {
90
+ return Some(child);
91
+ }
92
+ }
93
+ }
94
+ }
95
+ if let Ok(entries) = std::fs::read_dir(&chats) {
96
+ for entry in entries.flatten() {
97
+ let nested = entry.path().join(session_id);
98
+ if cursor_session_archive_present(&nested) {
99
+ return Some(nested);
100
+ }
101
+ }
102
+ }
103
+ None
104
+ }
105
+
106
+ /// ---
107
+ /// purpose: 由 spawn_cwd 的 md5 hex + chatId 直达存档,避免全库枚举失败
108
+ /// params:
109
+ /// session_id: CLI chatId;spawn_cwd: 席位物理 workspace
110
+ /// returns: hex/uuid 含 marker 则该目录,否则回落 cursor_session_dir
111
+ /// contract:
112
+ /// provides:
113
+ /// - name: cursor_session_dir_for_cwd
114
+ /// what: 优先 ~/.cursor/chats/<md5(cwd)>/<chatId>/,再全库回落
115
+ /// boundary:
116
+ /// - 不读正文;id 非法则 None
117
+ /// ---
118
+ pub(crate) fn cursor_session_dir_for_cwd(session_id: &str, spawn_cwd: &Path) -> Option<PathBuf> {
119
+ if !session_id_legal(session_id) {
120
+ return None;
121
+ }
122
+ let home = std::env::var_os("HOME").map(PathBuf::from)?;
123
+ let hex = workspace_chats_hex(spawn_cwd);
124
+ let nested = home
125
+ .join(".cursor")
126
+ .join("chats")
127
+ .join(hex)
128
+ .join(session_id);
129
+ if cursor_session_archive_present(&nested) {
130
+ return Some(nested);
131
+ }
132
+ cursor_session_dir(session_id)
133
+ }
134
+
135
+ fn candidate_for(
136
+ session_id: SessionId,
137
+ dir: PathBuf,
138
+ spawn_cwd: PathBuf,
139
+ ) -> CapturedSessionCandidate {
140
+ CapturedSessionCandidate {
141
+ captured: CapturedSession {
142
+ session_id: Some(session_id),
143
+ rollout_path: Some(RolloutPath::new(dir)),
144
+ captured_via: CaptureVia::FsWatch,
145
+ attribution_confidence: Confidence::High,
146
+ spawn_cwd,
147
+ },
148
+ embedded_agent_id: None,
149
+ positive_agent_id_match: false,
150
+ agent_path_match: false,
151
+ }
152
+ }
153
+
154
+ fn workspace_chats_hex(spawn_cwd: &Path) -> String {
155
+ let canonical = std::fs::canonicalize(spawn_cwd).unwrap_or_else(|_| spawn_cwd.to_path_buf());
156
+ format!("{:x}", md5::compute(canonical.to_string_lossy().as_bytes()))
157
+ }
158
+
159
+ fn dir_mtime_passes(dir: &Path, spawned_at: Option<&str>) -> bool {
160
+ let Some(raw) = spawned_at else {
161
+ return true;
162
+ };
163
+ let Ok(parsed) = chrono::DateTime::parse_from_rfc3339(raw) else {
164
+ return true;
165
+ };
166
+ let threshold = parsed.with_timezone(&chrono::Utc) - chrono::Duration::seconds(2);
167
+ let Ok(meta) = dir.metadata() else {
168
+ return false;
169
+ };
170
+ let Ok(mtime) = meta.modified() else {
171
+ return false;
172
+ };
173
+ chrono::DateTime::<chrono::Utc>::from(mtime) >= threshold
174
+ }
175
+
176
+ /// ---
177
+ /// purpose: 无 pending 时按 spawn_cwd 的 md5 hex 捕获唯一新 cursor 存档
178
+ /// params:
179
+ /// context: spawn_cwd + 可选 spawned_at
180
+ /// returns: hex 下恰好一个通过时间窗的 marker 目录则一条候选,否则空
181
+ /// contract:
182
+ /// provides:
183
+ /// - name: scan_unique_workspace_chat
184
+ /// what: 只枚举 ~/.cursor/chats/<md5(cwd)>/,0 或 ≥2 不拾取
185
+ /// boundary:
186
+ /// - 不扫其它 hex;不读 marker 正文
187
+ /// ---
188
+ fn scan_unique_workspace_chat(context: &CaptureSessionContext) -> Vec<CapturedSessionCandidate> {
189
+ let Some(home) = std::env::var_os("HOME").map(PathBuf::from) else {
190
+ return Vec::new();
191
+ };
192
+ let hex = workspace_chats_hex(&context.spawn_cwd);
193
+ if hex.len() != 32 {
194
+ return Vec::new();
195
+ }
196
+ let root = home.join(".cursor").join("chats").join(&hex);
197
+ if !root.is_dir() {
198
+ return Vec::new();
199
+ }
200
+ let mut hits: Vec<(String, PathBuf)> = Vec::new();
201
+ let Ok(entries) = std::fs::read_dir(&root) else {
202
+ return Vec::new();
203
+ };
204
+ for entry in entries.flatten() {
205
+ let child = entry.path();
206
+ if !child.is_dir() {
207
+ continue;
208
+ }
209
+ let Some(name) = child.file_name().and_then(|n| n.to_str()) else {
210
+ continue;
211
+ };
212
+ if !session_id_legal(name) {
213
+ continue;
214
+ }
215
+ if !cursor_session_archive_present(&child) {
216
+ continue;
217
+ }
218
+ if !dir_mtime_passes(&child, context.spawned_at.as_deref()) {
219
+ continue;
220
+ }
221
+ hits.push((name.to_string(), child));
222
+ }
223
+ if hits.len() != 1 {
224
+ return Vec::new();
225
+ }
226
+ let (sid, dir) = hits.remove(0);
227
+ vec![candidate_for(
228
+ SessionId::new(sid),
229
+ dir,
230
+ context.spawn_cwd.clone(),
231
+ )]
232
+ }
233
+
234
+ /// ---
235
+ /// purpose: 按 pending chatId 或 workspace hex 唯一新目录捕获 cursor 存档
236
+ /// params:
237
+ /// context: 含 spawn_cwd;可选 expected_session_id / spawned_at
238
+ /// returns: 命中则一条 FsWatch 候选,否则空向量
239
+ /// contract:
240
+ /// provides:
241
+ /// - name: scan_session_store
242
+ /// what: pending 优先;否则唯一新 hex 目录
243
+ /// boundary:
244
+ /// - 非唯一不拾取既有 hex 会话
245
+ /// ---
246
+ pub(super) fn scan_session_store(context: &CaptureSessionContext) -> Vec<CapturedSessionCandidate> {
247
+ if let Some(expected) = context.expected_session_id.as_ref() {
248
+ let Some(dir) = cursor_session_dir(expected.as_str()) else {
249
+ return Vec::new();
250
+ };
251
+ return vec![candidate_for(
252
+ expected.clone(),
253
+ dir,
254
+ context.spawn_cwd.clone(),
255
+ )];
256
+ }
257
+ scan_unique_workspace_chat(context)
258
+ }
259
+
260
+ #[cfg(test)]
261
+ mod tests {
262
+ #![allow(clippy::unwrap_used)]
263
+
264
+ use super::*;
265
+ use crate::provider::types::SessionId;
266
+ use std::sync::atomic::{AtomicU64, Ordering};
267
+
268
+ struct HomeGuard {
269
+ prev: Option<std::ffi::OsString>,
270
+ }
271
+
272
+ impl HomeGuard {
273
+ fn set(home: &Path) -> Self {
274
+ let prev = std::env::var_os("HOME");
275
+ std::env::set_var("HOME", home);
276
+ Self { prev }
277
+ }
278
+ }
279
+
280
+ impl Drop for HomeGuard {
281
+ fn drop(&mut self) {
282
+ match &self.prev {
283
+ Some(v) => std::env::set_var("HOME", v),
284
+ None => std::env::remove_var("HOME"),
285
+ }
286
+ }
287
+ }
288
+
289
+ fn tmp_root(tag: &str) -> PathBuf {
290
+ static CTR: AtomicU64 = AtomicU64::new(0);
291
+ let dir = std::env::temp_dir().join(format!(
292
+ "ta-d115-cursor-scan-{}-{}-{}",
293
+ tag,
294
+ std::process::id(),
295
+ CTR.fetch_add(1, Ordering::Relaxed)
296
+ ));
297
+ let _ = std::fs::remove_dir_all(&dir);
298
+ std::fs::create_dir_all(&dir).unwrap();
299
+ std::fs::canonicalize(&dir).unwrap()
300
+ }
301
+
302
+ fn seed_direct(home: &Path, session_id: &str) -> PathBuf {
303
+ let dir = home.join(".cursor").join("chats").join(session_id);
304
+ std::fs::create_dir_all(&dir).unwrap();
305
+ std::fs::write(dir.join("store.db"), b"").unwrap();
306
+ dir
307
+ }
308
+
309
+ fn seed_nested(home: &Path, outer: &str, session_id: &str) -> PathBuf {
310
+ let dir = home
311
+ .join(".cursor")
312
+ .join("chats")
313
+ .join(outer)
314
+ .join(session_id);
315
+ std::fs::create_dir_all(&dir).unwrap();
316
+ std::fs::write(dir.join("meta.json"), b"THIS-BODY-MUST-NOT-BE-PARSED\n").unwrap();
317
+ dir
318
+ }
319
+
320
+ fn ctx(cwd: &Path, expected: Option<&str>) -> CaptureSessionContext {
321
+ CaptureSessionContext {
322
+ agent_id: "w1".to_string(),
323
+ spawn_cwd: cwd.to_path_buf(),
324
+ pane_id: None,
325
+ pane_pid: None,
326
+ spawned_at: None,
327
+ expected_session_id: expected.map(SessionId::new),
328
+ provider_projects_root: None,
329
+ }
330
+ }
331
+
332
+ #[test]
333
+ #[serial_test::serial(env)]
334
+ fn cursor_unique_workspace_hex_dir_captures_without_pending() {
335
+ let base = tmp_root("unique-hex");
336
+ let home = base.join("home");
337
+ let cwd = base.join("ws");
338
+ std::fs::create_dir_all(&home).unwrap();
339
+ std::fs::create_dir_all(&cwd).unwrap();
340
+ let cwd = std::fs::canonicalize(&cwd).unwrap();
341
+ let _guard = HomeGuard::set(&home);
342
+ let sid = "09d2aeaa-33ef-4148-a2e6-83a7c3775caa";
343
+ let hex = workspace_chats_hex(&cwd);
344
+ assert_eq!(hex.len(), 32);
345
+ let dir = seed_nested(&home, &hex, sid);
346
+ let out = scan_session_store(&ctx(&cwd, None));
347
+ assert_eq!(out.len(), 1, "unique hex uuid must capture; got {out:?}");
348
+ assert_eq!(out[0].captured.session_id.as_ref().unwrap().as_str(), sid);
349
+ assert_eq!(
350
+ out[0]
351
+ .captured
352
+ .rollout_path
353
+ .as_ref()
354
+ .map(|p| p.as_path().to_path_buf()),
355
+ Some(dir)
356
+ );
357
+ let _ = std::fs::remove_dir_all(&base);
358
+ }
359
+
360
+ #[test]
361
+ #[serial_test::serial(env)]
362
+ fn cursor_two_workspace_hex_dirs_do_not_steal() {
363
+ let base = tmp_root("two-hex");
364
+ let home = base.join("home");
365
+ let cwd = base.join("ws");
366
+ std::fs::create_dir_all(&home).unwrap();
367
+ std::fs::create_dir_all(&cwd).unwrap();
368
+ let cwd = std::fs::canonicalize(&cwd).unwrap();
369
+ let _guard = HomeGuard::set(&home);
370
+ let hex = workspace_chats_hex(&cwd);
371
+ seed_nested(&home, &hex, "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa");
372
+ seed_nested(&home, &hex, "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb");
373
+ let out = scan_session_store(&ctx(&cwd, None));
374
+ assert!(
375
+ out.is_empty(),
376
+ "two marker dirs under cwd hex must not bind; got {out:?}"
377
+ );
378
+ let _ = std::fs::remove_dir_all(&base);
379
+ }
380
+
381
+ #[test]
382
+ #[serial_test::serial(env)]
383
+ fn cursor_session_dir_for_cwd_hits_hex_layout() {
384
+ let base = tmp_root("for-cwd");
385
+ let home = base.join("home");
386
+ let cwd = base.join("ws");
387
+ std::fs::create_dir_all(&home).unwrap();
388
+ std::fs::create_dir_all(&cwd).unwrap();
389
+ let cwd = std::fs::canonicalize(&cwd).unwrap();
390
+ let _guard = HomeGuard::set(&home);
391
+ let sid = "09d2aeaa-33ef-4148-a2e6-83a7c3775caa";
392
+ let hex = workspace_chats_hex(&cwd);
393
+ let dir = seed_nested(&home, &hex, sid);
394
+ assert_eq!(
395
+ cursor_session_dir_for_cwd(sid, &cwd).as_deref(),
396
+ Some(dir.as_path())
397
+ );
398
+ let _ = std::fs::remove_dir_all(&base);
399
+ }
400
+
401
+ #[test]
402
+ #[serial_test::serial(env)]
403
+ fn cursor_foreign_hex_is_ignored_when_cwd_hex_empty() {
404
+ let base = tmp_root("foreign-hex");
405
+ let home = base.join("home");
406
+ let cwd = base.join("ws");
407
+ std::fs::create_dir_all(&home).unwrap();
408
+ std::fs::create_dir_all(&cwd).unwrap();
409
+ let cwd = std::fs::canonicalize(&cwd).unwrap();
410
+ let _guard = HomeGuard::set(&home);
411
+ seed_nested(
412
+ &home,
413
+ "ffffffffffffffffffffffffffffffff",
414
+ "cccccccc-cccc-4ccc-8ccc-cccccccccccc",
415
+ );
416
+ let out = scan_session_store(&ctx(&cwd, None));
417
+ assert!(
418
+ out.is_empty(),
419
+ "foreign hex must not bind empty cwd hex; got {out:?}"
420
+ );
421
+ let _ = std::fs::remove_dir_all(&base);
422
+ }
423
+
424
+ #[test]
425
+ #[serial_test::serial(env)]
426
+ fn cursor_expected_id_captures_store_db_without_reading_body() {
427
+ let base = tmp_root("hit");
428
+ let home = base.join("home");
429
+ let cwd = base.join("ws");
430
+ std::fs::create_dir_all(&home).unwrap();
431
+ std::fs::create_dir_all(&cwd).unwrap();
432
+ let sid = "00a437742b92089861da7821a62f232a";
433
+ let _guard = HomeGuard::set(&home);
434
+ let dir = seed_direct(&home, sid);
435
+ std::fs::write(dir.join("store.db"), b"THIS-BODY-MUST-NOT-BE-PARSED\n").unwrap();
436
+ let out = scan_session_store(&ctx(&cwd, Some(sid)));
437
+ assert_eq!(out.len(), 1);
438
+ assert_eq!(out[0].captured.session_id.as_ref().unwrap().as_str(), sid);
439
+ assert_eq!(
440
+ out[0]
441
+ .captured
442
+ .rollout_path
443
+ .as_ref()
444
+ .map(|p| p.as_path().to_path_buf()),
445
+ Some(dir)
446
+ );
447
+ let _ = std::fs::remove_dir_all(&base);
448
+ }
449
+
450
+ #[test]
451
+ #[serial_test::serial(env)]
452
+ fn cursor_nested_hex_uuid_layout_captures_expected_id() {
453
+ let base = tmp_root("nested");
454
+ let home = base.join("home");
455
+ let cwd = base.join("ws");
456
+ std::fs::create_dir_all(&home).unwrap();
457
+ std::fs::create_dir_all(&cwd).unwrap();
458
+ let outer = "00a437742b92089861da7821a62f232a";
459
+ let sid = "502896a1-72ba-4c53-9a86-b2da28780806";
460
+ let _guard = HomeGuard::set(&home);
461
+ let dir = seed_nested(&home, outer, sid);
462
+ let out = scan_session_store(&ctx(&cwd, Some(sid)));
463
+ assert_eq!(out.len(), 1);
464
+ assert_eq!(out[0].captured.session_id.as_ref().unwrap().as_str(), sid);
465
+ assert_eq!(
466
+ out[0]
467
+ .captured
468
+ .rollout_path
469
+ .as_ref()
470
+ .map(|p| p.as_path().to_path_buf()),
471
+ Some(dir)
472
+ );
473
+ let _ = std::fs::remove_dir_all(&base);
474
+ }
475
+
476
+ #[test]
477
+ #[serial_test::serial(env)]
478
+ fn cursor_scan_without_expected_id_does_not_steal_existing_hex() {
479
+ let base = tmp_root("no-pending");
480
+ let home = base.join("home");
481
+ let cwd = base.join("ws");
482
+ std::fs::create_dir_all(&home).unwrap();
483
+ std::fs::create_dir_all(&cwd).unwrap();
484
+ let _guard = HomeGuard::set(&home);
485
+ seed_direct(&home, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
486
+ let out = scan_session_store(&ctx(&cwd, None));
487
+ assert!(
488
+ out.is_empty(),
489
+ "no pending must not pick existing chats; got {out:?}"
490
+ );
491
+ let _ = std::fs::remove_dir_all(&base);
492
+ }
493
+
494
+ #[test]
495
+ #[serial_test::serial(env)]
496
+ fn cursor_expected_id_does_not_steal_foreign_chat() {
497
+ let base = tmp_root("foreign");
498
+ let home = base.join("home");
499
+ let cwd = base.join("ws");
500
+ std::fs::create_dir_all(&home).unwrap();
501
+ std::fs::create_dir_all(&cwd).unwrap();
502
+ let _guard = HomeGuard::set(&home);
503
+ seed_direct(&home, "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
504
+ let out = scan_session_store(&ctx(&cwd, Some("cccccccccccccccccccccccccccccccc")));
505
+ assert!(out.is_empty(), "foreign chat must not bind; got {out:?}");
506
+ let _ = std::fs::remove_dir_all(&base);
507
+ }
508
+
509
+ #[test]
510
+ #[serial_test::serial(env)]
511
+ fn cursor_empty_or_path_id_is_rejected() {
512
+ let base = tmp_root("illegal");
513
+ let home = base.join("home");
514
+ std::fs::create_dir_all(&home).unwrap();
515
+ let _guard = HomeGuard::set(&home);
516
+ assert!(cursor_session_dir("").is_none());
517
+ assert!(cursor_session_dir("../escape").is_none());
518
+ assert!(cursor_session_dir("a\\b").is_none());
519
+ let _ = std::fs::remove_dir_all(&base);
520
+ }
521
+
522
+ #[test]
523
+ #[serial_test::serial(env)]
524
+ fn cursor_empty_dir_without_marker_is_not_backing() {
525
+ let base = tmp_root("empty");
526
+ let home = base.join("home");
527
+ let cwd = base.join("ws");
528
+ std::fs::create_dir_all(&home).unwrap();
529
+ std::fs::create_dir_all(&cwd).unwrap();
530
+ let sid = "dddddddddddddddddddddddddddddddd";
531
+ let _guard = HomeGuard::set(&home);
532
+ let dir = home.join(".cursor").join("chats").join(sid);
533
+ std::fs::create_dir_all(&dir).unwrap();
534
+ assert!(!cursor_session_archive_present(&dir));
535
+ assert!(scan_session_store(&ctx(&cwd, Some(sid))).is_empty());
536
+ let _ = std::fs::remove_dir_all(&base);
537
+ }
538
+ }