@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.
Files changed (178) hide show
  1. package/Cargo.lock +8 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +1 -0
  4. package/crates/team-agent/src/cli/adapters.rs +5 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +156 -0
  6. package/crates/team-agent/src/cli/emit.rs +6 -5
  7. package/crates/team-agent/src/cli/grok_slot.rs +299 -0
  8. package/crates/team-agent/src/cli/leader.rs +22 -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 +3 -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/missing_subcommands.rs +129 -19
  17. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +387 -6
  18. package/crates/team-agent/src/cli/tests/status_send.rs +79 -10
  19. package/crates/team-agent/src/communication_mode/mod.rs +6 -4
  20. package/crates/team-agent/src/compiler.rs +121 -5
  21. package/crates/team-agent/src/coordinator/backoff.rs +55 -0
  22. package/crates/team-agent/src/coordinator/conpty_shim.rs +82 -0
  23. package/crates/team-agent/src/coordinator/health.rs +173 -0
  24. package/crates/team-agent/src/coordinator/mod.rs +31 -0
  25. package/crates/team-agent/src/coordinator/orphan.rs +31 -0
  26. package/crates/team-agent/src/coordinator/runtime_detectors.rs +31 -0
  27. package/crates/team-agent/src/coordinator/runtime_observation.rs +25 -0
  28. package/crates/team-agent/src/coordinator/steps/abnormal.rs +59 -0
  29. package/crates/team-agent/src/coordinator/steps/delivery.rs +10 -0
  30. package/crates/team-agent/src/coordinator/steps/health_sync.rs +10 -0
  31. package/crates/team-agent/src/coordinator/steps/mod.rs +22 -0
  32. package/crates/team-agent/src/coordinator/steps/persist.rs +10 -0
  33. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +10 -0
  34. package/crates/team-agent/src/coordinator/steps/session_gate.rs +10 -0
  35. package/crates/team-agent/src/coordinator/tick.rs +133 -22
  36. package/crates/team-agent/src/coordinator/types.rs +49 -0
  37. package/crates/team-agent/src/db/message_store.rs +39 -5
  38. package/crates/team-agent/src/layout/worker_env.rs +20 -3
  39. package/crates/team-agent/src/layout/worker_window_helpers.rs +3 -0
  40. package/crates/team-agent/src/leader/provider_attribution.rs +53 -0
  41. package/crates/team-agent/src/leader/registry.rs +65 -0
  42. package/crates/team-agent/src/leader/start.rs +973 -75
  43. package/crates/team-agent/src/lifecycle/display.rs +56 -0
  44. package/crates/team-agent/src/lifecycle/helpers.rs +57 -0
  45. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +398 -26
  46. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +80 -0
  47. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +60 -0
  48. package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +101 -11
  49. package/crates/team-agent/src/lifecycle/launch/cursor_create_chat.rs +229 -0
  50. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +548 -0
  51. package/crates/team-agent/src/lifecycle/launch/cursor_mcp_iso.rs +120 -0
  52. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +208 -456
  53. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +24 -0
  54. package/crates/team-agent/src/lifecycle/launch/grok_per_seat.rs +260 -0
  55. package/crates/team-agent/src/lifecycle/launch/identity.rs +146 -0
  56. package/crates/team-agent/src/lifecycle/launch/layout.rs +60 -0
  57. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +112 -0
  58. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +482 -1
  59. package/crates/team-agent/src/lifecycle/launch/ownership.rs +40 -0
  60. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +830 -0
  61. package/crates/team-agent/src/lifecycle/launch/plan.rs +31 -0
  62. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +66 -0
  63. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +87 -3
  64. package/crates/team-agent/src/lifecycle/launch/readiness.rs +85 -38
  65. package/crates/team-agent/src/lifecycle/launch/role_source.rs +44 -44
  66. package/crates/team-agent/src/lifecycle/launch/spawn.rs +127 -25
  67. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +140 -0
  68. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +109 -0
  69. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +214 -1
  70. package/crates/team-agent/src/lifecycle/launch.rs +107 -29
  71. package/crates/team-agent/src/lifecycle/lock.rs +42 -1
  72. package/crates/team-agent/src/lifecycle/mod.rs +11 -0
  73. package/crates/team-agent/src/lifecycle/pane_input_lock.rs +161 -0
  74. package/crates/team-agent/src/lifecycle/profile_launch.rs +99 -0
  75. package/crates/team-agent/src/lifecycle/profile_smoke.rs +52 -1
  76. package/crates/team-agent/src/lifecycle/restart/agent.rs +214 -160
  77. package/crates/team-agent/src/lifecycle/restart/common.rs +513 -37
  78. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +27 -0
  79. package/crates/team-agent/src/lifecycle/restart/preflight.rs +25 -0
  80. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +106 -0
  81. package/crates/team-agent/src/lifecycle/restart/remove.rs +233 -6
  82. package/crates/team-agent/src/lifecycle/restart/selection.rs +72 -0
  83. package/crates/team-agent/src/lifecycle/restart/team_state.rs +22 -0
  84. package/crates/team-agent/src/lifecycle/restart.rs +29 -0
  85. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +2 -3
  86. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +24 -3
  87. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +309 -0
  88. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +150 -157
  89. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +2 -2
  90. package/crates/team-agent/src/lifecycle/tests/core.rs +4 -0
  91. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +464 -0
  92. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +247 -0
  93. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +353 -0
  94. package/crates/team-agent/src/lifecycle/tests/fixtures/pi_list_models_g0.stdout.txt +222 -0
  95. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +787 -0
  96. package/crates/team-agent/src/lifecycle/tests/gate_fixtures.rs +562 -0
  97. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +239 -0
  98. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +485 -0
  99. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +250 -0
  100. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +293 -0
  101. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +1 -1
  102. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +63 -38
  103. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +3 -2
  104. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +16 -8
  105. package/crates/team-agent/src/lifecycle/tests/mcp_tool_name_format_red.rs +69 -0
  106. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +20 -19
  107. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +97 -61
  108. package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +97 -0
  109. package/crates/team-agent/src/lifecycle/tests/pi_command_red.rs +128 -0
  110. package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +207 -0
  111. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +838 -0
  112. package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +320 -0
  113. package/crates/team-agent/src/lifecycle/tests/pi_lifecycle_doctor_red.rs +98 -0
  114. package/crates/team-agent/src/lifecycle/tests/pi_session_delivery_red.rs +216 -0
  115. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +3 -0
  116. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +40 -7
  117. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +59 -1
  118. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +69 -26
  119. package/crates/team-agent/src/lifecycle/tests.rs +30 -13
  120. package/crates/team-agent/src/lifecycle/types.rs +61 -0
  121. package/crates/team-agent/src/lifecycle/worker_command_context.rs +143 -12
  122. package/crates/team-agent/src/mcp_server/tests/scoped.rs +100 -1
  123. package/crates/team-agent/src/mcp_server/tests.rs +196 -2
  124. package/crates/team-agent/src/messaging/delivery.rs +430 -41
  125. package/crates/team-agent/src/messaging/helpers.rs +2 -0
  126. package/crates/team-agent/src/messaging/leader_receiver.rs +53 -1
  127. package/crates/team-agent/src/messaging/results.rs +4 -0
  128. package/crates/team-agent/src/messaging/send.rs +34 -0
  129. package/crates/team-agent/src/messaging/tests/dup_inject.rs +406 -0
  130. package/crates/team-agent/src/messaging/tests/e23.rs +9 -0
  131. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +66 -0
  132. package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
  133. package/crates/team-agent/src/messaging/types.rs +4 -1
  134. package/crates/team-agent/src/model/enums.rs +17 -6
  135. package/crates/team-agent/src/model/permissions.rs +3 -0
  136. package/crates/team-agent/src/os_probe.rs +73 -2
  137. package/crates/team-agent/src/provider/adapter.rs +259 -6
  138. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +89 -0
  139. package/crates/team-agent/src/provider/adapters/grok.rs +157 -0
  140. package/crates/team-agent/src/provider/adapters/mod.rs +3 -0
  141. package/crates/team-agent/src/provider/adapters/pi.rs +152 -0
  142. package/crates/team-agent/src/provider/bypass_flags.rs +47 -2
  143. package/crates/team-agent/src/provider/classify.rs +4 -2
  144. package/crates/team-agent/src/provider/faults.rs +2 -1
  145. package/crates/team-agent/src/provider/mod.rs +11 -0
  146. package/crates/team-agent/src/provider/session/capture.rs +158 -20
  147. package/crates/team-agent/src/provider/session/context_fork/claude.rs +38 -1
  148. package/crates/team-agent/src/provider/session/context_fork/codex.rs +72 -1
  149. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +57 -1
  150. package/crates/team-agent/src/provider/session/context_fork.rs +78 -3
  151. package/crates/team-agent/src/provider/session/mod.rs +18 -0
  152. package/crates/team-agent/src/provider/session/resume.rs +57 -0
  153. package/crates/team-agent/src/provider/session_scan/claude.rs +125 -1
  154. package/crates/team-agent/src/provider/session_scan/codex.rs +94 -1
  155. package/crates/team-agent/src/provider/session_scan/common.rs +205 -2
  156. package/crates/team-agent/src/provider/session_scan/copilot.rs +33 -1
  157. package/crates/team-agent/src/provider/session_scan/cursor.rs +538 -0
  158. package/crates/team-agent/src/provider/session_scan/grok.rs +288 -0
  159. package/crates/team-agent/src/provider/session_scan/pi.rs +172 -0
  160. package/crates/team-agent/src/provider/session_scan.rs +12 -0
  161. package/crates/team-agent/src/provider/submit_now.rs +94 -0
  162. package/crates/team-agent/src/provider/tests/adapter.rs +384 -0
  163. package/crates/team-agent/src/provider/types.rs +4 -2
  164. package/crates/team-agent/src/provider/wire.rs +29 -1
  165. package/crates/team-agent/src/state/persist.rs +301 -8
  166. package/crates/team-agent/src/state/repository.rs +5 -0
  167. package/crates/team-agent/src/tmux_backend/tests.rs +1737 -42
  168. package/crates/team-agent/src/tmux_backend.rs +1116 -75
  169. package/crates/team-agent/src/transport/tests/wire.rs +28 -2
  170. package/crates/team-agent/src/transport.rs +248 -1
  171. package/npm/install.mjs +129 -73
  172. package/package.json +4 -4
  173. package/skills/team-agent/SKILL.md +32 -239
  174. package/skills/team-agent/command-coverage.json +31 -0
  175. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +0 -59
  176. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +0 -488
  177. package/crates/team-agent/src/lifecycle/launch/fork_pending.rs +0 -109
  178. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +0 -447
@@ -1,4 +1,42 @@
1
- //!
1
+ //! ---
2
+ //! purpose: 会话捕获通道——把「席位有 pending id 但还没有权威会话」这件事,经扫描候选 + 分配器,收敛成 state 上的原子四元组(session_id + rollout_path + captured_at + captured_via),收敛不了就写明确的诊断态
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: capture_missing_provider_sessions_once
6
+ //! what: 单次捕获通道:挑 pending 席位 → adapter 扫候选 → 分配 → 写四元组或写 ambiguous/identity_mismatch/transcript_missing
7
+ //! - name: converge_missing_provider_sessions
8
+ //! what: 破坏性生命周期门用的有界收敛屏障:显式 deadline + poll_interval,反复调上一函数直到无缺口或超预算
9
+ //! - name: recover_resume_session_from_events
10
+ //! what: 从 event log 的 session.captured 记录补回缺失的四元组,captured_via=event_log_repair
11
+ //! - name: incomplete_resumable_agent_ids
12
+ //! what: 列出「provider 支持 resume 但四元组不完整」的席位 id,已排序
13
+ //! - name: incomplete_interacted_resumable_agent_ids
14
+ //! what: 上一条再筛「有过 first_send_at」的子集
15
+ //! - name: CapturePassReport
16
+ //! what: 单次捕获的结果面:changed/pending/assigned/ambiguous/identity_mismatches/capture_failures/candidate_count_by_agent/transcript_missing
17
+ //! - name: SessionConvergence
18
+ //! what: 收敛结果:converged 是否达成、changed、剩余 missing、预算与实耗
19
+ //! - name: SessionConvergenceProgress
20
+ //! what: 每轮回调给调用方的进度快照
21
+ //! - name: SESSION_CAPTURE_CONVERGENCE_DEADLINE_MS
22
+ //! what: 默认收敛预算 12s(restart 复用同一常量)
23
+ //! - name: SESSION_CAPTURE_CONVERGENCE_POLL_MS
24
+ //! what: 默认轮询间隔 250ms
25
+ //! requires:
26
+ //! - name: crate::provider::ProviderAdapter
27
+ //! what: 候选扫描一律经 adapter.capture_session_candidates,本文件不直接读 provider 存档
28
+ //! - name: crate::state::identity_keys::SessionAttributionKey
29
+ //! what: 分配器的 claimed 键空间(Session/Transcript × team-scoped/global)
30
+ //! - name: crate::event_log::EventLog
31
+ //! what: event-log 修复通道的事实源
32
+ //! boundary:
33
+ //! - 四元组是原子的:缺 session_id 或 rollout_path 就整条不写,宁可留 pending 也不写半条
34
+ //! - 不发事件——transcript_missing 只进报告,事件由 coordinator tick 节流发出
35
+ //! - 不启动/销毁席位,不决定 restart 走向;只提供事实与诊断态
36
+ //! - capture_state 只是诊断字段,写它不等于写权威四元组
37
+ //! - 歧义只在 finalize_ambiguous=true 的那一趟才落盘;收敛超时的那趟不 finalize
38
+ //! maturity: wired
39
+ //! ---
2
40
  use std::collections::{BTreeMap, BTreeSet};
3
41
  use std::path::{Path, PathBuf};
4
42
 
@@ -30,7 +68,6 @@ pub struct CapturePassReport {
30
68
  /// (coordinator tick) emits a throttled
31
69
  /// `provider.session.transcript_missing` event for each entry.
32
70
  pub transcript_missing: Vec<TranscriptMissing>,
33
- pub(crate) context_forks: Vec<crate::lifecycle::launch::ContextForkFinalized>,
34
71
  }
35
72
 
36
73
  #[derive(Debug, Clone, PartialEq, Eq)]
@@ -97,6 +134,26 @@ pub struct SessionConvergenceProgress {
97
134
  /// unbounded wait: callers must pass an explicit `deadline` and `poll_interval`.
98
135
  /// Each poll runs the shared allocator once, reports progress, and sleeps until
99
136
  /// either all required agents have provider sessions or the deadline expires.
137
+ /// ---
138
+ /// purpose: 有界收敛屏障——在显式预算内反复跑单次捕获,直到调用方指定的必需席位都拿到会话,或预算耗尽
139
+ /// params:
140
+ /// state: 就地修改的 state JSON;每轮的写入都直接落在它上面
141
+ /// adapter_for: provider → adapter 的工厂
142
+ /// deadline: 总预算;绝不无限等待
143
+ /// poll_interval: 轮询间隔;同时用作单次捕获的 timeout(取秒数,至少 1)
144
+ /// missing_agent_ids: 由调用方定义「谁必须有会话」——本函数不自己判定必需集
145
+ /// progress: 每轮回调;返回 Err 即整体中止并把该错误上抛
146
+ /// returns: converged=true 表示 missing 已空;converged=false 表示预算到期仍有缺口,两者都是正常返回而非错误
147
+ /// errors: 单次捕获的 ProviderError 与 progress 回调的错误都被转成 String 上抛
148
+ /// contract:
149
+ /// provides:
150
+ /// - name: converge_missing_provider_sessions
151
+ /// what: 只有 missing 清空后才会补跑一趟 finalize_ambiguous=true;超时路径不补跑
152
+ /// boundary:
153
+ /// - 不是机会式单次捕获,也不是无界等待——预算必须由调用方显式给出
154
+ /// - 超时返回时歧义态未被 finalize:行上不会出现 attribution_ambiguous 标记;capture_state 保持前值(可能缺失)——非歧义的 pending 席位仍会被单次捕获写成 pending_first_turn / pending_context_fork / transcript_missing,这不受 finalize 开关控制
155
+ /// - progress 只报告不决策;是否继续由预算决定
156
+ /// ---
100
157
  pub fn converge_missing_provider_sessions<F, M, P>(
101
158
  state: &mut Value,
102
159
  adapter_for: &mut F,
@@ -168,6 +225,25 @@ where
168
225
  }
169
226
  }
170
227
 
228
+ /// ---
229
+ /// purpose: 跑一趟捕获——为每个还缺权威会话的席位扫候选、经分配器定归属,把能确定的写成原子四元组,把不能确定的写成诊断态
230
+ /// params:
231
+ /// state: 就地修改的 state JSON;无 agents 对象时直接返回空报告
232
+ /// adapter_for: provider → adapter 的工厂;扫描一律经 adapter
233
+ /// finalize_ambiguous: false 时歧义只进报告不落盘;true 时才把 attribution_ambiguous / capture_state 写进 agent 行
234
+ /// timeout_s: 传给 adapter.capture_session_candidates 的单席位扫描预算
235
+ /// returns: CapturePassReport;changed 表示 state 被改过,调用方据此决定是否持久化
236
+ /// errors: 当前实现不产生 Err(Result 签名为与调用方同形);取不到 agents 视图返回空报告;单席位扫描失败进 capture_failures 且该席位保持 pending,不中断整趟
237
+ /// contract:
238
+ /// provides:
239
+ /// - name: capture_missing_provider_sessions_once
240
+ /// what: 写权威四元组的唯一常规通道(另一条是 event-log 修复)
241
+ /// boundary:
242
+ /// - 候选的 embedded worker id 与本席位不符 → 记 identity_mismatch 并跳过,绝不写会话
243
+ /// - 候选 session id 与 pending expected id 不符 → 记 ambiguous 并跳过,绝不写会话
244
+ /// - _pending_session_id 与行上 session_id 不一致时不 stamp captured——重开在途,旧会话不得被宣告为新席位的权威
245
+ /// - 不发事件;transcript_missing 只进报告
246
+ /// ---
171
247
  pub fn capture_missing_provider_sessions_once<F>(
172
248
  state: &mut Value,
173
249
  adapter_for: &mut F,
@@ -317,12 +393,9 @@ where
317
393
  // partial candidates (no session_id or no rollout_path). When
318
394
  // it returns false, the row stays unchanged; capture re-runs
319
395
  // on the next coordinator tick.
320
- if let Some(applied) = apply_captured_session(agent_obj, &candidate.captured) {
396
+ if apply_captured_session(agent_obj, &candidate.captured) {
321
397
  report.changed = true;
322
398
  report.assigned.push(item.agent_id);
323
- if let AppliedCapture::ContextFork(context_fork) = applied {
324
- report.context_forks.push(context_fork);
325
- }
326
399
  }
327
400
  continue;
328
401
  }
@@ -517,6 +590,19 @@ fn classify_pending_capture_state(
517
590
  }
518
591
  }
519
592
 
593
+ /// ---
594
+ /// purpose: 列出「provider 本可 resume、但会话事实还不完整」的席位——即捕获通道该继续照看的那批
595
+ /// params:
596
+ /// state: 只读的 state JSON;无 agents 对象时返回空
597
+ /// returns: 按 agent id 升序的清单
598
+ /// contract:
599
+ /// provides:
600
+ /// - name: incomplete_resumable_agent_ids
601
+ /// what: 只读判定,不修改 state
602
+ /// boundary:
603
+ /// - 用全局 get_adapter 取 adapter,不接受注入——测试要替身请走 capture 通道那两个函数
604
+ /// - 不区分「从没捕获过」与「捕获过但四元组残缺」,两者都在清单里
605
+ /// ---
520
606
  pub fn incomplete_resumable_agent_ids(state: &Value) -> Vec<String> {
521
607
  let Some(agents) = state.get("agents").and_then(Value::as_object) else {
522
608
  return Vec::new();
@@ -535,6 +621,28 @@ pub fn incomplete_resumable_agent_ids(state: &Value) -> Vec<String> {
535
621
  out
536
622
  }
537
623
 
624
+ /// ---
625
+ /// purpose: 会话四元组缺失时的第二条修复通道——倒序翻 event log,用该席位最近一条可信的 session.captured 记录把行补全
626
+ /// params:
627
+ /// workspace: event log 所在工作区
628
+ /// agent_id: 目标席位;事件的 agent_id 或 worker_id 命中即算该席位
629
+ /// previous: 修复前的 agent 行;provider 字段决定要不要套 claude leader-marker 过滤
630
+ /// adapter: 用于二次确认候选会话确实可 resume
631
+ /// auth_mode: 传给 session_is_resumable 的鉴权模式
632
+ /// exclude_session_ids: 调用方给出的黑名单(例如刚被判为不可用的会话)
633
+ /// returns: Some(补好四元组的新行) 表示找到可用记录;None 表示没找到,或先撞上 discard.session_tombstone 而主动放弃
634
+ /// errors: 读 event log 的 Io 错误、以及 session_is_resumable 的 ProviderError
635
+ /// contract:
636
+ /// provides:
637
+ /// - name: recover_resume_session_from_events
638
+ /// what: 修复结果一定是完整四元组;captured_at 缺事件 ts 时退到当前时间,captured_via 固定 event_log_repair
639
+ /// boundary:
640
+ /// - 只读 event log 与 rollout 路径存在性,不扫 provider 存档、不调分配器
641
+ /// - 不走分配器的 claimed-key 冲突检查,只看 exclude_session_ids 与 tombstone
642
+ /// - rollout_path 存在性用 exists()——对目录型 backing(grok/cursor)只要目录在就为真
643
+ /// - claude 家族额外过滤 leader-marker transcript;其它 provider 无等价归属过滤
644
+ /// - 修复成功会清掉 attribution_ambiguous 标记
645
+ /// ---
538
646
  pub fn recover_resume_session_from_events(
539
647
  workspace: &Path,
540
648
  agent_id: &str,
@@ -651,6 +759,19 @@ fn event_rollout_path(event: &Value) -> Option<PathBuf> {
651
759
  .map(PathBuf::from)
652
760
  }
653
761
 
762
+ /// ---
763
+ /// purpose: 在「会话不完整」清单上再筛出真正被用过的席位——有 first_send_at 才算交互过
764
+ /// params:
765
+ /// state: 只读的 state JSON
766
+ /// returns: 按 agent id 升序的子集
767
+ /// contract:
768
+ /// provides:
769
+ /// - name: incomplete_interacted_resumable_agent_ids
770
+ /// what: incomplete_resumable_agent_ids 的过滤视图,只读
771
+ /// boundary:
772
+ /// - 交互判据只看 first_send_at 非空,不看 last_result_at / pane 输出
773
+ /// - 从没派过活的席位不进清单——沉默席位不算捕获失败
774
+ /// ---
654
775
  pub fn incomplete_interacted_resumable_agent_ids(state: &Value) -> Vec<String> {
655
776
  let mut out = incomplete_resumable_agent_ids(state)
656
777
  .into_iter()
@@ -1170,11 +1291,6 @@ fn candidate_key(owner: &PendingSessionCapture, candidate: &CapturedSessionCandi
1170
1291
  .join("|")
1171
1292
  }
1172
1293
 
1173
- enum AppliedCapture {
1174
- Session,
1175
- ContextFork(crate::lifecycle::launch::ContextForkFinalized),
1176
- }
1177
-
1178
1294
  /// 0.4.6 tuple-atomic contract (audit §Capture 修改清单, line 111): writes
1179
1295
  /// the authoritative session tuple if and only if the candidate carries
1180
1296
  /// BOTH `session_id` and `rollout_path`. A partial candidate (e.g.
@@ -1185,16 +1301,12 @@ enum AppliedCapture {
1185
1301
  fn apply_captured_session(
1186
1302
  agent_obj: &mut serde_json::Map<String, Value>,
1187
1303
  captured: &CapturedSession,
1188
- ) -> Option<AppliedCapture> {
1189
- if agent_obj.get("capture_state").and_then(Value::as_str) == Some("pending_context_fork") {
1190
- return crate::lifecycle::finalize_pending_fork_capture(agent_obj, captured)
1191
- .map(AppliedCapture::ContextFork);
1192
- }
1304
+ ) -> bool {
1193
1305
  let Some(session_id) = captured.session_id.as_ref() else {
1194
- return None;
1306
+ return false;
1195
1307
  };
1196
1308
  let Some(rollout_path) = captured.rollout_path.as_ref() else {
1197
- return None;
1309
+ return false;
1198
1310
  };
1199
1311
  agent_obj.insert(
1200
1312
  "session_id".to_string(),
@@ -1224,7 +1336,7 @@ fn apply_captured_session(
1224
1336
  // diagnose/status observability.
1225
1337
  agent_obj.remove("_pending_session_id");
1226
1338
  agent_obj.insert("capture_state".to_string(), serde_json::json!("captured"));
1227
- Some(AppliedCapture::Session)
1339
+ true
1228
1340
  }
1229
1341
 
1230
1342
  fn claimed_provider_session_keys(
@@ -1410,6 +1522,20 @@ pub(crate) mod test_support {
1410
1522
  }
1411
1523
 
1412
1524
  impl CaptureCandidatesAdapter {
1525
+ /// ---
1526
+ /// purpose: 造一个可控的测试替身 adapter:指定 provider,并可让某个席位的候选扫描必定失败
1527
+ /// params:
1528
+ /// provider: 替身自称的 provider
1529
+ /// fail_agent_id: 该席位的 capture_session_candidates 返回错误;None 表示都不失败
1530
+ /// error: 失败时携带的错误文本
1531
+ /// returns: 未挂候选表的替身;候选表另经 with_candidates 注入
1532
+ /// contract:
1533
+ /// provides:
1534
+ /// - name: new
1535
+ /// what: 纯构造,无 I/O
1536
+ /// boundary:
1537
+ /// - 仅 cfg(test) 存在,不进产品二进制
1538
+ /// ---
1413
1539
  pub(crate) fn new(provider: Provider, fail_agent_id: Option<&str>, error: &str) -> Self {
1414
1540
  Self {
1415
1541
  provider,
@@ -1419,6 +1545,18 @@ pub(crate) mod test_support {
1419
1545
  }
1420
1546
  }
1421
1547
 
1548
+ /// ---
1549
+ /// purpose: 给测试替身挂一张 agent_id → 候选列表的表,用来构造分配器层面的交叉归属回归
1550
+ /// params:
1551
+ /// candidates_by_agent: 按席位 id 索引的候选;未列入的席位仍返回空列表
1552
+ /// returns: 挂好表的自身(builder 风格,消费 self)
1553
+ /// contract:
1554
+ /// provides:
1555
+ /// - name: with_candidates
1556
+ /// what: 只改替身内部状态,不触发任何扫描
1557
+ /// boundary:
1558
+ /// - 仅 cfg(test) 存在,不进产品二进制
1559
+ /// ---
1422
1560
  pub(crate) fn with_candidates(
1423
1561
  mut self,
1424
1562
  candidates_by_agent: BTreeMap<String, Vec<CapturedSessionCandidate>>,
@@ -2653,7 +2791,7 @@ mod u1_tests {
2653
2791
  spawn_cwd: PathBuf::from("/tmp/cwd"),
2654
2792
  };
2655
2793
  let written = super::apply_captured_session(&mut agent, &captured);
2656
- assert!(written.is_some(), "apply must accept valid captured");
2794
+ assert!(written, "apply must accept valid captured");
2657
2795
  assert!(
2658
2796
  agent.get("_pending_session_id").is_none(),
2659
2797
  "S1-CAPTURE-001: _pending_session_id must be removed after capture \
@@ -1,6 +1,43 @@
1
- //!
1
+ //! ---
2
+ //! purpose: claude 家族的 fork 验证——在预算内轮询「精确快照路径」与「provider 侧 projects 路径」两处,任一处出现可读的新 backing 即出证明
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: verify_claude_fork
6
+ //! what: 双路径轮询验证,成功给 ContextForkProof(captured_via=context_fork_verified)
7
+ //! requires:
8
+ //! - name: crate::provider::session_scan::claude::projects_dir_for_cwd
9
+ //! what: 第二条探测路径由 HOME + spawn_cwd 推导出 ~/.claude/projects 下的目录
10
+ //! - name: super::ContextBackingSnapshot
11
+ //! what: 判「文件变过」的基线
12
+ //! boundary:
13
+ //! - 只服务 Provider::Claude / ClaudeCode
14
+ //! - 文件名必须精确等于 <expected_session_id>.jsonl,不做同目录最新文件回落
15
+ //! - 不解析会话正文语义,只验「首行可解析 JSON」与「sessionId 记录(若有)一致」
16
+ //! - 未在 deadline 内满足条件即 Timeout,不降级放行
17
+ //! maturity: wired
18
+ //! ---
2
19
  use super::*;
3
20
 
21
+ /// ---
22
+ /// purpose: 轮询等待 claude fork 的新 transcript 落盘并验明身份
23
+ /// params:
24
+ /// provider: 写进证明的 provider 标记(Claude 或 ClaudeCode)
25
+ /// source_session_id: 源会话;新会话等于它即不出证明
26
+ /// plan: 必须带 expected_session_id,否则立即拒绝
27
+ /// before: spawn 前基线,用于判 stamp 变化
28
+ /// expected_backing_path: 必需的精确快照路径;文件名不等于 <expected>.jsonl 即拒绝
29
+ /// spawn_cwd: 用于推导第二条 provider 侧探测路径
30
+ /// deadline: 轮询预算,每轮间隔 50ms
31
+ /// returns: 证明中 backing_path 是命中的那一条路径,attribution_confidence 固定 "high"
32
+ /// errors: Rejected(CaptureFailed) 当 plan 缺 expected id / 缺快照路径 / 文件名不匹配;Timeout 当预算内两条路径都没满足
33
+ /// contract:
34
+ /// provides:
35
+ /// - name: verify_claude_fork
36
+ /// what: 只读文件元数据与首行/sessionId 记录,不写任何文件
37
+ /// boundary:
38
+ /// - 两条路径的严格程度不同:快照路径用 is_none_or(无 sessionId 记录时视为匹配),provider 路径用 is_some_and(必须读到 sessionId)
39
+ /// - 不排除 .team/logs/events.jsonl 之外的无关写入方——硬绑定只有「文件名等于 expected uuid」这一条
40
+ /// ---
4
41
  pub(super) fn verify_claude_fork(
5
42
  provider: Provider,
6
43
  source_session_id: &SessionId,
@@ -1,4 +1,25 @@
1
- //!
1
+ //! ---
2
+ //! purpose: codex 的 fork 两件事——把源 rollout 改写成新身份的快照物化出来(materialize),再验证 CLI 确实认领了它(verify)
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: materialize_codex_fork
6
+ //! what: 读源 rollout 完整记录 → 改 session_meta.id 与 worker 身份 marker → no-clobber 硬链接发布 → 读回自证 → 把 plan 从 `codex fork` 改写成 `codex resume <新 id>`
7
+ //! - name: CodexForkMaterialization
8
+ //! what: 物化产物的 RAII 句柄:未 handoff 就 Drop 时删除目标文件
9
+ //! - name: verify_codex_fork
10
+ //! what: 有 expected id 时按精确路径+embedded 身份认领;无 expected id 时退到 legacy「唯一变过的新 rollout」路径
11
+ //! requires:
12
+ //! - name: crate::provider::session_scan
13
+ //! what: 认领新 rollout 靠一次性候选扫描,不自己解析目录
14
+ //! - name: crate::provider::adapter::ForkBackingMaterialization
15
+ //! what: 句柄实现的对外 trait(path/handoff)
16
+ //! boundary:
17
+ //! - 改写只动 session_meta.payload.id 与恰好一处 worker 身份 marker;二者数量不是恰好各 1 就整体拒绝,绝不部分改写
18
+ //! - 发布用 create_new 临时文件 + hard_link,绝不覆盖已存在的目标路径
19
+ //! - 只服务 Provider::Codex
20
+ //! - 新 session id 是本地生成的 uuid-v7 形状串,不问 CLI 要
21
+ //! maturity: wired
22
+ //! ---
2
23
  use super::*;
3
24
  use std::collections::BTreeSet;
4
25
  use std::fs::{File, OpenOptions};
@@ -11,6 +32,16 @@ pub(crate) struct CodexForkMaterialization {
11
32
  }
12
33
 
13
34
  impl CodexForkMaterialization {
35
+ /// ---
36
+ /// purpose: 暴露已物化的目标 rollout 路径,供调用方拼 resume 参数与做 fork 验证
37
+ /// returns: 物化目标文件的绝对路径;句柄还活着时该路径必然存在
38
+ /// contract:
39
+ /// provides:
40
+ /// - name: path
41
+ /// what: 只借出路径,不转移所有权、不影响 Drop 时的删除决定
42
+ /// boundary:
43
+ /// - 拿到路径不等于拿到保留承诺——不调 handoff 就仍会在 Drop 时被删
44
+ /// ---
14
45
  pub(crate) fn path(&self) -> &Path {
15
46
  &self.path
16
47
  }
@@ -38,6 +69,25 @@ impl Drop for CodexForkMaterialization {
38
69
  }
39
70
  }
40
71
 
72
+ /// ---
73
+ /// purpose: 为 codex fork 物化一份改写了会话身份的新 rollout,并把命令计划改成对该新会话的精确 resume
74
+ /// params:
75
+ /// source_path: 源 rollout 文件;新文件发布在它的同一父目录
76
+ /// source_session_id: 源会话 id;源快照的 session_meta.payload.id 必须与之相等,否则拒绝
77
+ /// source_agent_id: 源席位 id,用于定位待替换的 worker 身份 marker
78
+ /// target_agent_id: 目标席位 id,替换后的 marker 内容,并参与读回自证
79
+ /// plan: 就地改写;要求形如 `codex fork ... <source_session_id>`,会被改成 `codex resume ... <新 id>` 并设上 expected_session_id
80
+ /// returns: RAII 句柄——未 handoff 即 Drop 时删除新文件,避免留下半成品存档
81
+ /// errors: Io(源无父目录/源无完整 JSONL 记录/源某行非法 JSON/session_meta 缺 payload.id/id 与源不符/session_meta 或 marker 命中数不是恰好各 1/发布失败/读回身份不符);Command(plan 形状不是可转换的 codex fork)
82
+ /// contract:
83
+ /// provides:
84
+ /// - name: materialize_codex_fork
85
+ /// what: 写盘发生在此;读回校验失败会先删目标文件再返回错误
86
+ /// boundary:
87
+ /// - 只截到源文件最后一个换行处,绝不把半条正在写入的记录带进新快照
88
+ /// - 不改源文件
89
+ /// - 不启动进程、不调 codex CLI——只准备好 backing 与 argv
90
+ /// ---
41
91
  pub(crate) fn materialize_codex_fork(
42
92
  source_path: &Path,
43
93
  source_session_id: &SessionId,
@@ -283,6 +333,27 @@ fn codex_session_v7() -> String {
283
333
  )
284
334
  }
285
335
 
336
+ /// ---
337
+ /// purpose: 验证 codex 已经在物化出来的那条 rollout 上开出了新会话,且该会话的 embedded 身份就是目标席位
338
+ /// params:
339
+ /// source_session_id: 源会话;目标等于它即拒绝
340
+ /// plan: 无 expected_session_id 时整体退到 legacy 路径(靠「唯一一条变过的新 rollout」认领)
341
+ /// before: spawn 前基线;legacy 路径用它排除源会话所在文件与未变文件
342
+ /// expected_backing_path: 有 expected id 时必需——候选路径必须逐字节等于它,不做同目录择新
343
+ /// agent_id: 候选的 embedded worker id 必须等于它,且 positive_agent_id_match 必须为真
344
+ /// spawn_cwd: 构造扫描上下文
345
+ /// spawned_at: 扫描的时间边界
346
+ /// deadline: 轮询预算,每轮间隔 50ms
347
+ /// returns: ContextForkProof,captured_via="context_fork_verified"、attribution_confidence="high"
348
+ /// errors: Rejected(CaptureFailed) 当目标等于源 / 缺物化目标路径;Rejected 亦透传扫描期的 ProviderError;Timeout 当预算内没有满足全部条件的候选
349
+ /// contract:
350
+ /// provides:
351
+ /// - name: verify_codex_fork
352
+ /// what: 只读;认领判据是路径精确相等 + session id 等于 expected + embedded 身份等于 agent_id 三条同时成立
353
+ /// boundary:
354
+ /// - 不写 backing、不改 plan
355
+ /// - legacy 路径(无 expected id)不做身份比对,只要求「不是源会话、不是基线里未变的文件、且全场恰好一条」——多于一条即不认领
356
+ /// ---
286
357
  pub(super) fn verify_codex_fork(
287
358
  source_session_id: &SessionId,
288
359
  plan: &CommandPlan,
@@ -1,4 +1,24 @@
1
- //!
1
+ //! ---
2
+ //! purpose: 把 verify_context_fork 的 Result 翻译成三态结局(Verified/Pending/Rejected),并给 pending 态一条推进到失败的判据
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: ContextForkOutcome
6
+ //! what: fork 三态:Verified(带证明) / Pending(带可交回捕获通道的扫描上下文) / Rejected(provider 错误)
7
+ //! - name: PendingContextFork
8
+ //! what: 超时未验证时保留的续查材料:源会话 id、目标席位、spawned_at、CaptureSessionContext
9
+ //! - name: observe_context_fork
10
+ //! what: 调 verify_context_fork 并把 Timeout 折成 Pending 而不是错误
11
+ //! - name: transition_pending_context_fork
12
+ //! what: 纯判据——pending_context_fork 是否该被推进成 transcript_missing
13
+ //! requires:
14
+ //! - name: crate::provider::session_scan::CaptureSessionContext
15
+ //! what: Pending 携带的续查上下文类型
16
+ //! boundary:
17
+ //! - 不轮询、不读磁盘;所有 I/O 都在被调用的 verify_context_fork 里
18
+ //! - 不写 state、不发事件——只产出结局值,落盘由捕获通道做
19
+ //! - Pending 不是成功也不是失败,绝不折进另外两态
20
+ //! maturity: wired
21
+ //! ---
2
22
  use super::*;
3
23
 
4
24
  #[derive(Debug, Clone)]
@@ -21,6 +41,20 @@ pub(crate) enum ContextForkPendingFailure {
21
41
  TranscriptMissing,
22
42
  }
23
43
 
44
+ /// ---
45
+ /// purpose: 判定一个停在 pending_context_fork 的席位是否该被改判为 transcript_missing
46
+ /// params:
47
+ /// triggered: 该席位是否已出现过触发事件(有过交互/结果/pane 输出)
48
+ /// grace_expired: 宽限窗口是否已过
49
+ /// returns: 两者同时为真才给 Some(TranscriptMissing);否则 None = 继续 pending
50
+ /// contract:
51
+ /// provides:
52
+ /// - name: transition_pending_context_fork
53
+ /// what: 纯布尔判据,无 I/O、无时钟
54
+ /// boundary:
55
+ /// - 不自己计算 triggered / grace_expired——两个事实由调用方(capture.rs)从 agent 行算好后传入
56
+ /// - 只表达「该改判」,不负责写 capture_state
57
+ /// ---
24
58
  pub(crate) fn transition_pending_context_fork(
25
59
  triggered: bool,
26
60
  grace_expired: bool,
@@ -28,6 +62,28 @@ pub(crate) fn transition_pending_context_fork(
28
62
  (triggered && grace_expired).then_some(ContextForkPendingFailure::TranscriptMissing)
29
63
  }
30
64
 
65
+ /// ---
66
+ /// purpose: 跑一次 fork 验证并把结果收敛成三态,超时不当错误而是留成可续查的 Pending
67
+ /// params:
68
+ /// provider: 转交 verify_context_fork 分派
69
+ /// source_session_id: 源会话 id,同时写进 Pending 供后续比对
70
+ /// plan: expected_session_id 与 provider_projects_root 会被复制进 Pending 的扫描上下文
71
+ /// before: spawn 前 backing 基线
72
+ /// expected_backing_path: 精确快照路径,透传
73
+ /// source_agent_id: 透传给 verify_context_fork(当前该参数不参与判定)
74
+ /// agent_id: 目标席位 id;既用于 codex 身份比对,也写进 Pending.target_agent
75
+ /// spawn_cwd: 目标席位工作目录,透传并写进 Pending 的扫描上下文
76
+ /// spawned_at: 时间边界,透传并写进 Pending
77
+ /// deadline: 轮询预算
78
+ /// returns: Verified(proof) / Pending(续查材料) / Rejected(ProviderError)
79
+ /// contract:
80
+ /// provides:
81
+ /// - name: observe_context_fork
82
+ /// what: 只做结果翻译与 Pending 材料装配,不新增任何判定
83
+ /// boundary:
84
+ /// - Pending 里的 CaptureSessionContext 一律 pane_id=None、pane_pid=None——本函数不掌握 pane 事实
85
+ /// - 不重试、不写状态;是否再验由捕获通道决定
86
+ /// ---
31
87
  pub(crate) fn observe_context_fork(
32
88
  provider: Provider,
33
89
  source_session_id: &SessionId,
@@ -1,4 +1,29 @@
1
- //!
1
+ //! ---
2
+ //! purpose: context-fork 的验证总闸——按 provider 分派「新会话 backing 确实生成了」的证明,拿不到证明就超时或拒绝,绝不假绿
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: ContextBackingSnapshot
6
+ //! what: spawn 前对 provider backing 根下 .jsonl 的 (len, mtime) 基线快照,用于判「变过」
7
+ //! - name: verify_context_fork
8
+ //! what: 按 provider 路由到 copilot/codex/claude 三条验证路径,成功给 ContextForkProof
9
+ //! - name: context_fork_convergence_deadline
10
+ //! what: 每个 provider 各自的收敛预算(claude 45s / codex 10s / 其余 5s)
11
+ //! - name: ContextForkProof
12
+ //! what: 已验证的 fork 证明:新旧 session id、backing 路径、captured_via、归属置信度
13
+ //! - name: ContextForkTermination
14
+ //! what: 两种失败:Timeout(未在预算内看到新 backing) 与 Rejected(provider 侧错误)
15
+ //! requires:
16
+ //! - name: crate::provider::session_scan
17
+ //! what: codex 分支复用一次性候选扫描来认领新 rollout
18
+ //! - name: crate::provider::CommandPlan
19
+ //! what: expected_session_id 与 provider_projects_root 两个关键输入都来自 plan
20
+ //! boundary:
21
+ //! - 只回答「fork 有没有产生一个可读的、不等于源会话的新 backing」,不回答会话内容对不对
22
+ //! - 不创建/改写 backing(codex 的物化改写在 context_fork/codex.rs,不在本文件)
23
+ //! - 未证明即 Timeout/Rejected,绝不退化成「假定成功」
24
+ //! - grok / cursor / gemini / fake 没有可验证 backing,一律直接 Rejected
25
+ //! maturity: wired
26
+ //! ---
2
27
  use std::collections::BTreeMap;
3
28
  use std::path::{Path, PathBuf};
4
29
  use std::time::{Duration, SystemTime};
@@ -27,12 +52,25 @@ pub(crate) use outcome::{
27
52
  observe_context_fork, transition_pending_context_fork, ContextForkOutcome, PendingContextFork,
28
53
  };
29
54
 
55
+ /// ---
56
+ /// purpose: 给出该 provider 等待 fork backing 落盘的收敛预算
57
+ /// params:
58
+ /// provider: 目标 provider;全枚举穷举,无兜底臂
59
+ /// returns: Claude/ClaudeCode 45s、Codex 10s、其余(Copilot/Grok/CursorAgent/GeminiCli/Fake) 5s
60
+ /// contract:
61
+ /// provides:
62
+ /// - name: context_fork_convergence_deadline
63
+ /// what: 纯查表,不读时钟、不读磁盘
64
+ /// boundary:
65
+ /// - 只给预算数值,不负责在预算内轮询;超时语义由 ContextForkOutcome::Pending 承接
66
+ /// ---
30
67
  pub(crate) fn context_fork_convergence_deadline(provider: Provider) -> Duration {
31
68
  // Expiration is consumed by ContextForkOutcome::Pending(PendingContextFork).
32
69
  match provider {
33
70
  Provider::Claude | Provider::ClaudeCode => Duration::from_secs(45),
34
71
  Provider::Codex => Duration::from_secs(10),
35
- Provider::Copilot | Provider::GeminiCli | Provider::Fake => Duration::from_secs(5),
72
+ Provider::Copilot | Provider::Grok | Provider::CursorAgent | Provider::Pi | Provider::GeminiCli
73
+ | Provider::Fake => Duration::from_secs(5),
36
74
  }
37
75
  }
38
76
 
@@ -59,6 +97,20 @@ struct FileStamp {
59
97
  }
60
98
 
61
99
  impl ContextBackingSnapshot {
100
+ /// ---
101
+ /// purpose: 在 fork spawn 之前对 provider backing 根做一次 .jsonl 基线快照,事后据此判断哪些文件「变过」
102
+ /// params:
103
+ /// provider: 决定 backing 根的默认位置(plan 未给 provider_projects_root 时按 HOME 推导)
104
+ /// plan: 优先取 plan.provider_projects_root;隔离根存在时不碰用户全局目录
105
+ /// returns: 根下递归到底的 path → (len, modified) 映射;根不可读时是空映射,不报错
106
+ /// contract:
107
+ /// provides:
108
+ /// - name: capture
109
+ /// what: 只读元数据(len+mtime),不打开文件正文
110
+ /// boundary:
111
+ /// - 只收 .jsonl 后缀;copilot 的 session-store.db 不在快照内,其证明走 sqlite 查询另算
112
+ /// - 目录读失败静默跳过——快照是「变没变」的参照物,不是完整性断言
113
+ /// ---
62
114
  pub(crate) fn capture(provider: Provider, plan: &CommandPlan) -> Self {
63
115
  let root = provider_backing_root(provider, plan);
64
116
  let files = jsonl_files(&root);
@@ -66,6 +118,28 @@ impl ContextBackingSnapshot {
66
118
  }
67
119
  }
68
120
 
121
+ /// ---
122
+ /// purpose: fork 后按 provider 分派验证,只有拿到「新会话 backing 可读且不等于源会话」的实证才返回证明
123
+ /// params:
124
+ /// provider: 决定走 copilot(sqlite 行存在) / codex(候选扫描认领) / claude(轮询双路径) 三条路之一
125
+ /// source_session_id: 被 fork 的源会话 id;新会话等于它即判失败
126
+ /// plan: 提供 expected_session_id 与隔离 backing 根
127
+ /// before: spawn 前的 ContextBackingSnapshot 基线,用于判「文件变过」
128
+ /// expected_backing_path: 精确快照路径。claude 必需,缺失即拒绝;codex 仅在 plan 带 expected id 时必需(无 expected id 走 legacy「唯一变过的新 rollout」认领,可为 None);两者都不做同目录猜测
129
+ /// spawn_cwd: codex 分支据此构造扫描上下文;claude 分支据此推导 provider 侧 projects 目录
130
+ /// spawned_at: codex 候选扫描的时间边界
131
+ /// deadline: 轮询预算,来自 context_fork_convergence_deadline
132
+ /// returns: ContextForkProof——新旧 session id、backing 路径、captured_via、attribution_confidence
133
+ /// errors: Timeout 表示预算内没看到可验证的新 backing;Rejected 包装 ProviderError(缺 expected id、路径不匹配、无可验证 backing 的 provider)
134
+ /// contract:
135
+ /// provides:
136
+ /// - name: verify_context_fork
137
+ /// what: 分派 + 兜底拒绝;本函数自身不轮询,轮询在各 provider 分支内
138
+ /// boundary:
139
+ /// - 不修改 backing、不写 state、不发事件
140
+ /// - _source_agent_id 当前不参与任何判定(仅 codex 用 agent_id 做 embedded 身份比对)
141
+ /// - 未列入三条路径的 provider 一律 CaptureFailed,绝不返回「无法验证但放行」
142
+ /// ---
69
143
  pub(crate) fn verify_context_fork(
70
144
  provider: Provider,
71
145
  source_session_id: &SessionId,
@@ -178,7 +252,8 @@ fn provider_backing_root(provider: Provider, plan: &CommandPlan) -> PathBuf {
178
252
  Provider::Claude | Provider::ClaudeCode => home.join(".claude").join("projects"),
179
253
  Provider::Codex => home.join(".codex").join("sessions"),
180
254
  Provider::Copilot => home.join(".copilot").join("session-state"),
181
- Provider::GeminiCli | Provider::Fake => home,
255
+ Provider::Pi => PathBuf::new(),
256
+ Provider::Grok | Provider::CursorAgent | Provider::GeminiCli | Provider::Fake => home,
182
257
  }
183
258
  }
184
259
 
@@ -1,3 +1,21 @@
1
+ //! ---
2
+ //! purpose: provider 会话域的命名空间——把「捕获 / resume 拒绝判定 / context-fork」三块聚合成一个出口
3
+ //! contract:
4
+ //! provides:
5
+ //! - name: capture
6
+ //! what: pending id → 扫描候选 → 分配 → 写会话四元组的捕获通道(pub 子模块)
7
+ //! - name: resume
8
+ //! what: ResumeRefusalReason 闭合枚举与 RecoveryHint(pub 子模块)
9
+ //! - name: ContextForkProof
10
+ //! what: context-fork 验证通过后的证明结构,由私有 context_fork 子模块 re-export
11
+ //! requires:
12
+ //! - name: crate::provider::session_scan
13
+ //! what: 磁盘候选扫描不在本命名空间内实现,由 session_scan 提供
14
+ //! boundary:
15
+ //! - 只做子模块聚合与 re-export,本文件不含任何逻辑
16
+ //! - 不决定何时重启/销毁席位——那是 lifecycle 的判断
17
+ //! maturity: wired
18
+ //! ---
1
19
  //!
2
20
  //! unit-6 (Stage 2) — provider session namespace.
3
21
  //!