@team-agent/installer 0.5.66 → 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.
- 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 +82 -0
- package/crates/team-agent/src/cli/emit.rs +3 -3
- package/crates/team-agent/src/cli/grok_slot.rs +299 -0
- package/crates/team-agent/src/cli/leader.rs +21 -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 +2 -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 +59 -0
- 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 +30 -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 +2 -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 +920 -63
- 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 +332 -12
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +62 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +59 -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 +332 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +204 -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 +530 -0
- package/crates/team-agent/src/lifecycle/launch/ownership.rs +40 -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 +78 -0
- 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 +62 -2
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +139 -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 +99 -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 +98 -0
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +51 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +124 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +369 -11
- 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 +196 -5
- 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/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 +287 -0
- package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +229 -0
- package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +353 -0
- package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +784 -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 +498 -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/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/restart_rebind_hotfix_252_red.rs +2 -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 +23 -13
- package/crates/team-agent/src/lifecycle/types.rs +61 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +85 -11
- 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 +405 -40
- 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 +14 -5
- 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 +231 -4
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +86 -0
- package/crates/team-agent/src/provider/adapters/grok.rs +157 -0
- package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
- package/crates/team-agent/src/provider/bypass_flags.rs +46 -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 +77 -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 +204 -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.rs +8 -0
- package/crates/team-agent/src/provider/submit_now.rs +94 -0
- package/crates/team-agent/src/provider/tests/adapter.rs +304 -0
- package/crates/team-agent/src/provider/types.rs +4 -2
- package/crates/team-agent/src/provider/wire.rs +23 -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 +1064 -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 +33 -238
- 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
|
@@ -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::
|
|
72
|
+
Provider::Copilot | Provider::Grok | Provider::CursorAgent | 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,7 @@ 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::Grok | Provider::CursorAgent | Provider::GeminiCli | Provider::Fake => home,
|
|
182
256
|
}
|
|
183
257
|
}
|
|
184
258
|
|
|
@@ -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
|
//!
|
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
//! ---
|
|
2
|
+
//! purpose: 把 restart resume 门的「为什么拒绝」从两个不透明字符串升级成闭合枚举,并携带操作者可用的恢复线索
|
|
3
|
+
//! contract:
|
|
4
|
+
//! provides:
|
|
5
|
+
//! - name: ResumeRefusalReason
|
|
6
|
+
//! what: resume 拒绝原因闭合枚举(7 变体),新原因必须改代码才能加
|
|
7
|
+
//! - name: wire
|
|
8
|
+
//! what: 枚举 → 历史 UnresumableWorker.reason 稳定串,供 JSON/日志沿用旧形状
|
|
9
|
+
//! - name: from_legacy
|
|
10
|
+
//! what: 历史字符串 → 枚举的逆映射,未识别串一律落进 Other{legacy_reason}
|
|
11
|
+
//! - name: RecoveryHint
|
|
12
|
+
//! what: 缺 backing 时给操作者的 provider/name/cwd 三元线索
|
|
13
|
+
//! - name: picker_hint
|
|
14
|
+
//! what: 把 RecoveryHint 渲染成一行人读文本
|
|
15
|
+
//! requires:
|
|
16
|
+
//! - name: std::path::PathBuf
|
|
17
|
+
//! what: checked_paths 与 spawn_cwd 的载体
|
|
18
|
+
//! boundary:
|
|
19
|
+
//! - 只做「原因的分类与措辞」:不探测 backing 是否存在,不读磁盘,不发事件
|
|
20
|
+
//! - RecoveryHint 只呈现给人,绝不被自动 resume 消费(自动恢复需 Layer 3 多键过滤+backing 复验)
|
|
21
|
+
//! - 判定 resume 是否可行的调用点在 lifecycle/restart,不在本文件
|
|
22
|
+
//! maturity: wired
|
|
23
|
+
//! ---
|
|
1
24
|
//!
|
|
2
25
|
//! unit-5 (Stage 2) — closed `ResumeRefusalReason` enum and recovery hints.
|
|
3
26
|
//!
|
|
@@ -47,6 +70,16 @@ impl RecoveryHint {
|
|
|
47
70
|
/// Build a human-readable picker hint (one line). Layer 2 surfaces
|
|
48
71
|
/// this in the CLI refusal message and in the
|
|
49
72
|
/// `session.recovery.candidate_hint` event payload.
|
|
73
|
+
/// ---
|
|
74
|
+
/// purpose: 把 provider/name/cwd 三元线索拼成一行人读文本,供 CLI 拒绝信息与事件载荷使用
|
|
75
|
+
/// returns: 四种措辞之一,按 name/cwd 各自是否存在退化;两者都缺时退到 "<provider> session"
|
|
76
|
+
/// contract:
|
|
77
|
+
/// provides:
|
|
78
|
+
/// - name: picker_hint
|
|
79
|
+
/// what: 纯格式化,不查磁盘、不校验 cwd 是否还在
|
|
80
|
+
/// boundary:
|
|
81
|
+
/// - 输出只给人看,不做机器解析的契约,调用方不得据此自动 resume
|
|
82
|
+
/// ---
|
|
50
83
|
pub fn picker_hint(&self) -> String {
|
|
51
84
|
match (&self.provider_session_name_hint, &self.spawn_cwd) {
|
|
52
85
|
(Some(name), Some(cwd)) => format!(
|
|
@@ -118,6 +151,17 @@ impl ResumeRefusalReason {
|
|
|
118
151
|
/// `UnresumableWorker.reason` values. Use this when emitting JSON or
|
|
119
152
|
/// log fields so downstream consumers see the same strings they
|
|
120
153
|
/// always have.
|
|
154
|
+
/// ---
|
|
155
|
+
/// purpose: 把结构化拒绝原因压回历史 UnresumableWorker.reason 稳定串
|
|
156
|
+
/// returns: 六个 canonical 串之一;Other 一律折回 "session_unresumable"——未 taxonomize 的新失败类型对外与历史大杂烩不可区分
|
|
157
|
+
/// contract:
|
|
158
|
+
/// provides:
|
|
159
|
+
/// - name: wire
|
|
160
|
+
/// what: 枚举 → 稳定串的全函数映射,不丢字段但丢细节
|
|
161
|
+
/// boundary:
|
|
162
|
+
/// - 不携带 checked_paths / recovery_hint / drift 的 expected-actual 等负载,只给分类名
|
|
163
|
+
/// - 与 from_legacy 只在五个 canonical 串上互逆;SessionDrift 有 wire 串但 from_legacy 无对应臂,"session_drift" 会落进 Other,该变体不可往返
|
|
164
|
+
/// ---
|
|
121
165
|
pub fn wire(&self) -> &'static str {
|
|
122
166
|
match self {
|
|
123
167
|
ResumeRefusalReason::NoSessionId => "no_persisted_session_id",
|
|
@@ -136,6 +180,19 @@ impl ResumeRefusalReason {
|
|
|
136
180
|
|
|
137
181
|
/// Lift a legacy free-form `reason` string into the structured enum.
|
|
138
182
|
/// Round-trip-safe with `wire()` for the canonical names.
|
|
183
|
+
/// ---
|
|
184
|
+
/// purpose: 把历史自由串抬升成结构化枚举,保证旧持久化数据不丢分类
|
|
185
|
+
/// params:
|
|
186
|
+
/// reason: 历史 UnresumableWorker.reason 串;不在识别表内的任意值都合法
|
|
187
|
+
/// returns: 匹配到的变体(负载字段一律填空,因为串里没有这些事实);未匹配则 Other{legacy_reason=原串}
|
|
188
|
+
/// contract:
|
|
189
|
+
/// provides:
|
|
190
|
+
/// - name: from_legacy
|
|
191
|
+
/// what: 全函数、不失败、不 panic 的逆映射
|
|
192
|
+
/// boundary:
|
|
193
|
+
/// - 不还原 checked_paths / recovery_hint / provider 名等负载——它们在串里本就不存在
|
|
194
|
+
/// - 识别表缺 "session_drift" 臂,该串会落到 Other 而非 SessionDrift
|
|
195
|
+
/// ---
|
|
139
196
|
pub fn from_legacy(reason: &str) -> Self {
|
|
140
197
|
match reason {
|
|
141
198
|
"no_persisted_session_id" => ResumeRefusalReason::NoSessionId,
|
|
@@ -1,4 +1,33 @@
|
|
|
1
|
-
//!
|
|
1
|
+
//! ---
|
|
2
|
+
//! purpose: claude 家族的会话归属过滤——按 expected id 直达 ~/.claude/projects/<编码 cwd>/<sid>.jsonl 读头验身份,并为通用扫描提供 leader-transcript 与 cwd 字段两道排除判据
|
|
3
|
+
//! contract:
|
|
4
|
+
//! provides:
|
|
5
|
+
//! - name: projects_dir_for_cwd
|
|
6
|
+
//! what: 由 HOME + spawn_cwd 推出 claude 的 projects 目录(非字母数字字符一律替成 '-')
|
|
7
|
+
//! - name: encode_projects_dir
|
|
8
|
+
//! what: claude 的目录名编码规则本身
|
|
9
|
+
//! - name: scan_expected_session
|
|
10
|
+
//! what: 有 pending id 时的直达捕获:读头 64KB,验 sessionId 一致 + 有 user/assistant 记录 + 无 leader marker + 有 cwd 字段,四条全过才出候选
|
|
11
|
+
//! - name: rollout_path_has_leader_marker
|
|
12
|
+
//! what: 判某条 transcript 是不是 leader 的(customTitle/agentName == "claude leader")
|
|
13
|
+
//! - name: records_have_leader_marker
|
|
14
|
+
//! what: 上一条的记录级判据
|
|
15
|
+
//! - name: has_cwd_field
|
|
16
|
+
//! what: 记录里有没有 cwd 字段——claude transcript 的最低可信度门槛
|
|
17
|
+
//! - name: apply_expected_session_filter
|
|
18
|
+
//! what: 通用扫描结果的收窄:expected 命中则独取,未命中则只留身份/路径阳性的
|
|
19
|
+
//! requires:
|
|
20
|
+
//! - name: super::common
|
|
21
|
+
//! what: 读头、解析记录、embedded 身份、时间窗过滤都复用 common
|
|
22
|
+
//! - name: crate::provider::helpers::find_session_id
|
|
23
|
+
//! what: 从记录里取 sessionId 的统一入口
|
|
24
|
+
//! boundary:
|
|
25
|
+
//! - 只服务 Provider::Claude / ClaudeCode;rollout_path_has_leader_marker 对其它 provider 恒 false
|
|
26
|
+
//! - 不写盘、不改 state、不发事件
|
|
27
|
+
//! - 直达路径不做同 cwd「最新文件」回落——有 pending id 就只认那一个文件名
|
|
28
|
+
//! - leader transcript 一律排除,防止 worker 席位绑上 leader 的会话
|
|
29
|
+
//! maturity: wired
|
|
30
|
+
//! ---
|
|
2
31
|
use std::path::{Path, PathBuf};
|
|
3
32
|
|
|
4
33
|
use crate::provider::helpers::find_session_id;
|
|
@@ -7,6 +36,20 @@ use crate::provider::Provider;
|
|
|
7
36
|
|
|
8
37
|
use super::{CaptureSessionContext, CapturedSessionCandidate};
|
|
9
38
|
|
|
39
|
+
/// ---
|
|
40
|
+
/// purpose: 由 HOME 与席位 cwd 推出 claude 存放该工作目录 transcript 的 projects 子目录
|
|
41
|
+
/// params:
|
|
42
|
+
/// home: HOME 根;调用方决定是真实 HOME 还是隔离根
|
|
43
|
+
/// spawn_cwd: 席位工作目录;先 canonicalize,失败则原样使用
|
|
44
|
+
/// returns: <home>/.claude/projects/<编码后的 cwd>;编码结果为空串时 None
|
|
45
|
+
/// contract:
|
|
46
|
+
/// provides:
|
|
47
|
+
/// - name: projects_dir_for_cwd
|
|
48
|
+
/// what: 只拼路径,不创建目录、不判存在性
|
|
49
|
+
/// boundary:
|
|
50
|
+
/// - 不枚举目录内容、不读任何文件
|
|
51
|
+
/// - canonicalize 失败不报错,退回原路径——编码结果因此可能与 claude 实际用的目录不同
|
|
52
|
+
/// ---
|
|
10
53
|
pub(crate) fn projects_dir_for_cwd(home: &Path, spawn_cwd: &Path) -> Option<PathBuf> {
|
|
11
54
|
let canonical = std::fs::canonicalize(spawn_cwd).unwrap_or_else(|_| spawn_cwd.to_path_buf());
|
|
12
55
|
let encoded = encode_projects_dir(&canonical.to_string_lossy());
|
|
@@ -16,6 +59,19 @@ pub(crate) fn projects_dir_for_cwd(home: &Path, spawn_cwd: &Path) -> Option<Path
|
|
|
16
59
|
Some(home.join(".claude").join("projects").join(encoded))
|
|
17
60
|
}
|
|
18
61
|
|
|
62
|
+
/// ---
|
|
63
|
+
/// purpose: 复刻 claude 的 projects 目录名编码:非 ASCII 字母数字的字符一律替成单个 '-'
|
|
64
|
+
/// params:
|
|
65
|
+
/// path: 待编码的路径文本
|
|
66
|
+
/// returns: 等长的编码串;输入为空则空串
|
|
67
|
+
/// contract:
|
|
68
|
+
/// provides:
|
|
69
|
+
/// - name: encode_projects_dir
|
|
70
|
+
/// what: 纯字符映射,逐字符一对一,不折叠连续分隔符
|
|
71
|
+
/// boundary:
|
|
72
|
+
/// - 有损且不可逆:不同路径可以编出同一个目录名
|
|
73
|
+
/// - 不做长度截断、不做大小写归一
|
|
74
|
+
/// ---
|
|
19
75
|
pub(super) fn encode_projects_dir(path: &str) -> String {
|
|
20
76
|
let mut out = String::with_capacity(path.len());
|
|
21
77
|
for c in path.chars() {
|
|
@@ -28,6 +84,21 @@ pub(super) fn encode_projects_dir(path: &str) -> String {
|
|
|
28
84
|
out
|
|
29
85
|
}
|
|
30
86
|
|
|
87
|
+
/// ---
|
|
88
|
+
/// purpose: 有 pending id 时直达那一个 transcript 文件,读头验明身份后给出唯一候选
|
|
89
|
+
/// params:
|
|
90
|
+
/// context: 需要 expected_session_id;projects 根优先取 provider_projects_root,否则退到 HOME/.claude/projects;spawn_cwd 决定编码后的子目录
|
|
91
|
+
/// returns: 四道校验全过则一条 FsWatch/High 候选(带 embedded 身份与是否与本席位一致);任一条不过则空向量
|
|
92
|
+
/// contract:
|
|
93
|
+
/// provides:
|
|
94
|
+
/// - name: scan_expected_session
|
|
95
|
+
/// what: 只读该文件头 64KB;不遍历目录、不比较 mtime
|
|
96
|
+
/// boundary:
|
|
97
|
+
/// - 无 expected_session_id / 无法确定 projects 根 / 编码为空 / 文件读不出来 → 空向量
|
|
98
|
+
/// - 四道校验:sessionId 与 expected 相等、存在 user 或 assistant 记录、不含 leader marker、至少一条记录有 cwd 字段
|
|
99
|
+
/// - embedded 身份与本席位不符时仍返回候选(positive_agent_id_match=false),是否拒绝交给上游分配器判定
|
|
100
|
+
/// - agent_path_match 恒 false:直达路径下文件名就是 uuid,不含席位名
|
|
101
|
+
/// ---
|
|
31
102
|
pub(super) fn scan_expected_session(
|
|
32
103
|
context: &CaptureSessionContext,
|
|
33
104
|
) -> Vec<CapturedSessionCandidate> {
|
|
@@ -87,6 +158,20 @@ pub(super) fn scan_expected_session(
|
|
|
87
158
|
}]
|
|
88
159
|
}
|
|
89
160
|
|
|
161
|
+
/// ---
|
|
162
|
+
/// purpose: 判断一条 transcript 是不是 leader 的会话,供捕获与 event-log 修复两条通道共用排除
|
|
163
|
+
/// params:
|
|
164
|
+
/// provider: 非 Claude/ClaudeCode 一律直接判否
|
|
165
|
+
/// rollout_path: 待判定的 transcript 路径
|
|
166
|
+
/// returns: 读得到头且头部记录里出现 leader marker 才为 true
|
|
167
|
+
/// contract:
|
|
168
|
+
/// provides:
|
|
169
|
+
/// - name: rollout_path_has_leader_marker
|
|
170
|
+
/// what: 只读头 64KB
|
|
171
|
+
/// boundary:
|
|
172
|
+
/// - 文件打不开、解析不出记录一律返回 false —— 判据是 fail-open 的:读不到不等于不是 leader
|
|
173
|
+
/// - marker 只在头窗口内查;超出 64KB 之后才出现的 marker 看不见
|
|
174
|
+
/// ---
|
|
90
175
|
pub(crate) fn rollout_path_has_leader_marker(provider: Provider, rollout_path: &Path) -> bool {
|
|
91
176
|
if !matches!(provider, Provider::Claude | Provider::ClaudeCode) {
|
|
92
177
|
return false;
|
|
@@ -99,6 +184,18 @@ pub(crate) fn rollout_path_has_leader_marker(provider: Provider, rollout_path: &
|
|
|
99
184
|
records_have_leader_marker(&records)
|
|
100
185
|
}
|
|
101
186
|
|
|
187
|
+
/// ---
|
|
188
|
+
/// purpose: 在已解析的记录里找 leader 身份 marker
|
|
189
|
+
/// params:
|
|
190
|
+
/// records: 已解析的 transcript 记录切片
|
|
191
|
+
/// returns: 任一记录的 customTitle 或 agentName 小写后等于 "claude leader" 即 true
|
|
192
|
+
/// contract:
|
|
193
|
+
/// provides:
|
|
194
|
+
/// - name: records_have_leader_marker
|
|
195
|
+
/// what: 纯内存判定,无 I/O
|
|
196
|
+
/// boundary:
|
|
197
|
+
/// - 判据是精确串相等(仅大小写不敏感),不做包含匹配、不认其它别名
|
|
198
|
+
/// ---
|
|
102
199
|
pub(super) fn records_have_leader_marker(records: &[serde_json::Value]) -> bool {
|
|
103
200
|
records.iter().any(|record| {
|
|
104
201
|
let custom_title = record
|
|
@@ -114,10 +211,37 @@ pub(super) fn records_have_leader_marker(records: &[serde_json::Value]) -> bool
|
|
|
114
211
|
})
|
|
115
212
|
}
|
|
116
213
|
|
|
214
|
+
/// ---
|
|
215
|
+
/// purpose: 判断一条 claude 记录是否带 cwd 字段——用作 transcript 是否够格当候选的最低门槛
|
|
216
|
+
/// params:
|
|
217
|
+
/// record: 单条已解析记录
|
|
218
|
+
/// returns: common::record_cwd 能取到值即 true
|
|
219
|
+
/// contract:
|
|
220
|
+
/// provides:
|
|
221
|
+
/// - name: has_cwd_field
|
|
222
|
+
/// what: 只判字段有无,不比较 cwd 是否等于席位 cwd
|
|
223
|
+
/// boundary:
|
|
224
|
+
/// - 不做路径等价判定;是否同 cwd 由调用方另行判断
|
|
225
|
+
/// ---
|
|
117
226
|
pub(super) fn has_cwd_field(record: &serde_json::Value) -> bool {
|
|
118
227
|
super::common::record_cwd(record).is_some()
|
|
119
228
|
}
|
|
120
229
|
|
|
230
|
+
/// ---
|
|
231
|
+
/// purpose: 用 pending id 收窄通用扫描的结果,把「可能是它」压成「就是它」或「至少身份阳性」
|
|
232
|
+
/// params:
|
|
233
|
+
/// context: 有 expected_session_id 才做收窄;否则退到时间窗过滤
|
|
234
|
+
/// out: 待收窄的候选列表,按值传入
|
|
235
|
+
/// returns: expected 命中则只留那一条;未命中则只留 positive_agent_id_match 或 agent_path_match 为真的;无 expected 则原表经唯一时间窗过滤后返回
|
|
236
|
+
/// errors: 当前实现不产生 Err;返回 Result 是为与其它 provider 过滤器同形
|
|
237
|
+
/// contract:
|
|
238
|
+
/// provides:
|
|
239
|
+
/// - name: apply_expected_session_filter
|
|
240
|
+
/// what: 纯过滤,不读盘(时间窗分支会取候选文件 mtime)
|
|
241
|
+
/// boundary:
|
|
242
|
+
/// - 未命中 expected 时不返回空而是返回身份阳性子集——弱于「必须命中」,允许分配器再判
|
|
243
|
+
/// - 不排序;expected 优先排序由 common::sort_expected_first_if_needed 另做
|
|
244
|
+
/// ---
|
|
121
245
|
pub(super) fn apply_expected_session_filter(
|
|
122
246
|
context: &CaptureSessionContext,
|
|
123
247
|
mut out: Vec<CapturedSessionCandidate>,
|