@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,3 +1,22 @@
|
|
|
1
|
+
//! ---
|
|
2
|
+
//! purpose: 单个席位在 runtime state 里的行的构造,含 MCP 配置落盘与审批策略投影
|
|
3
|
+
//! contract:
|
|
4
|
+
//! provides:
|
|
5
|
+
//! - name: running_agent_state
|
|
6
|
+
//! what: 构造 running 席位的 state 行,同时写出该席位的 MCP 配置文件
|
|
7
|
+
//! - name: effective_approval_policy
|
|
8
|
+
//! what: 把 bypass 审批结论投影成 state 里的 JSON
|
|
9
|
+
//! - name: effective_approval_policy_from_agent
|
|
10
|
+
//! what: 由席位行自己的 dangerously_skip_permissions 算审批策略
|
|
11
|
+
//! depends:
|
|
12
|
+
//! - crate::provider
|
|
13
|
+
//! - crate::provider::bypass_flags
|
|
14
|
+
//! - crate::lifecycle::launch::mcp_config
|
|
15
|
+
//! boundary:
|
|
16
|
+
//! - 只构造与落 MCP 配置,不 spawn 也不改窗口
|
|
17
|
+
//! - 审批结论只认席位自己的声明,不做全队继承
|
|
18
|
+
//! maturity: wired
|
|
19
|
+
//! ---
|
|
1
20
|
use std::collections::{BTreeMap, BTreeSet};
|
|
2
21
|
use std::path::{Path, PathBuf};
|
|
3
22
|
use std::process::Command;
|
|
@@ -14,6 +33,15 @@ use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest}
|
|
|
14
33
|
|
|
15
34
|
use super::*;
|
|
16
35
|
|
|
36
|
+
/// ---
|
|
37
|
+
/// purpose: 构造某席位 running 状态的 state 行
|
|
38
|
+
/// params:
|
|
39
|
+
/// spawn_cwd: 该席位进程的工作目录
|
|
40
|
+
/// started_agent: 本次 spawn 结果,用于取布局窗口
|
|
41
|
+
/// pane_id: 该席位的 pane,空串时不写入
|
|
42
|
+
/// returns: 含状态、provider、模型、auth_mode、窗口、MCP 配置路径、spawn 时间与 pane 信息的行
|
|
43
|
+
/// errors: 取 provider MCP 配置失败返回 Provider,写 MCP 配置文件失败返回 StatePersist
|
|
44
|
+
/// ---
|
|
17
45
|
pub(super) fn running_agent_state(
|
|
18
46
|
agent: &Value,
|
|
19
47
|
id: &str,
|
|
@@ -141,6 +169,10 @@ pub(super) fn running_agent_state(
|
|
|
141
169
|
Ok(serde_json::Value::Object(state))
|
|
142
170
|
}
|
|
143
171
|
|
|
172
|
+
/// ---
|
|
173
|
+
/// purpose: 把 bypass 审批结论投影成落盘用的 JSON
|
|
174
|
+
/// returns: 含 enabled、来源、是否继承与显式确认位;来源为 runtime_config 且启用时视为已显式确认
|
|
175
|
+
/// ---
|
|
144
176
|
pub(crate) fn effective_approval_policy(safety: &DangerousApproval) -> serde_json::Value {
|
|
145
177
|
serde_json::json!({
|
|
146
178
|
"enabled": safety.enabled,
|
|
@@ -153,6 +185,11 @@ pub(crate) fn effective_approval_policy(safety: &DangerousApproval) -> serde_jso
|
|
|
153
185
|
})
|
|
154
186
|
}
|
|
155
187
|
|
|
188
|
+
/// ---
|
|
189
|
+
/// purpose: 把审批策略写进席位 state 行
|
|
190
|
+
/// params:
|
|
191
|
+
/// agent_state: 就地写入 effective_approval_policy 字段
|
|
192
|
+
/// ---
|
|
156
193
|
pub(crate) fn persist_effective_approval_policy(
|
|
157
194
|
agent_state: &mut serde_json::Map<String, serde_json::Value>,
|
|
158
195
|
safety: &DangerousApproval,
|
|
@@ -163,6 +200,10 @@ pub(crate) fn persist_effective_approval_policy(
|
|
|
163
200
|
);
|
|
164
201
|
}
|
|
165
202
|
|
|
203
|
+
/// ---
|
|
204
|
+
/// purpose: 给出审批来源的稳定 wire 字符串
|
|
205
|
+
/// returns: runtime_config、leader_process 或 disabled
|
|
206
|
+
/// ---
|
|
166
207
|
pub(super) fn dangerous_approval_source_str(source: DangerousApprovalSource) -> &'static str {
|
|
167
208
|
match source {
|
|
168
209
|
DangerousApprovalSource::RuntimeConfig => "runtime_config",
|
|
@@ -171,6 +212,11 @@ pub(super) fn dangerous_approval_source_str(source: DangerousApprovalSource) ->
|
|
|
171
212
|
}
|
|
172
213
|
}
|
|
173
214
|
|
|
215
|
+
/// ---
|
|
216
|
+
/// purpose: 由 JSON 席位行自己的 bypass 声明算出审批策略
|
|
217
|
+
/// returns: 未声明时全为关闭;声明为真时记 runtime_config 来源并带该 provider 的 bypass argv
|
|
218
|
+
/// contract_id: lifecycle.agent_state.approval_policy_from_agent
|
|
219
|
+
/// ---
|
|
174
220
|
/// 0.5.66 bypass 单源:从 agent 行 + provider 构造 approval policy JSON
|
|
175
221
|
/// (restart/fork/add 落盘用;取代全队 `DangerousApproval` 派生)。
|
|
176
222
|
pub(crate) fn effective_approval_policy_from_agent(
|
|
@@ -211,10 +257,17 @@ fn provider_wire_str(provider: Provider) -> &'static str {
|
|
|
211
257
|
Provider::Codex => "codex",
|
|
212
258
|
Provider::Copilot => "copilot",
|
|
213
259
|
Provider::GeminiCli => "gemini_cli",
|
|
260
|
+
Provider::Grok => "grok",
|
|
261
|
+
Provider::CursorAgent => "cursor_agent",
|
|
214
262
|
Provider::Fake => "fake",
|
|
215
263
|
}
|
|
216
264
|
}
|
|
217
265
|
|
|
266
|
+
/// ---
|
|
267
|
+
/// purpose: 由席位行自身算出审批策略并写回同一行
|
|
268
|
+
/// params:
|
|
269
|
+
/// agent_state: 既是来源也是写入目标
|
|
270
|
+
/// ---
|
|
218
271
|
/// 0.5.66 bypass 单源:从 agent 行(state Map)+ provider 落盘 approval policy。
|
|
219
272
|
/// 目标与来源是同一 agent 行。
|
|
220
273
|
pub(crate) fn persist_effective_approval_policy_from_agent(
|
|
@@ -228,6 +281,12 @@ pub(crate) fn persist_effective_approval_policy_from_agent(
|
|
|
228
281
|
);
|
|
229
282
|
}
|
|
230
283
|
|
|
284
|
+
/// ---
|
|
285
|
+
/// purpose: 由 YAML spec 里的 agent 声明算出审批策略并写进 state 行
|
|
286
|
+
/// params:
|
|
287
|
+
/// agent: YAML 侧的 agent 节点,提供 bypass 声明
|
|
288
|
+
/// agent_state: 写入目标
|
|
289
|
+
/// ---
|
|
231
290
|
/// 0.5.66 bypass 单源:yaml spec agent 版(launch/state_projection 用)。
|
|
232
291
|
pub(crate) fn persist_effective_approval_policy_from_yaml_agent(
|
|
233
292
|
agent_state: &mut serde_json::Map<String, serde_json::Value>,
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
//! ---
|
|
2
|
+
//! purpose: 零上下文重读角色文件再起一席,不继承源会话
|
|
3
|
+
//! contract:
|
|
4
|
+
//! provides:
|
|
5
|
+
//! - name: clone_agent
|
|
6
|
+
//! what: 用源席最新角色文件 add-agent,保留源工具集
|
|
7
|
+
//! boundary:
|
|
8
|
+
//! - 不走 fork 的窗口内命令
|
|
9
|
+
//! - 不把 clone 与 fork 合成一条路径
|
|
10
|
+
//! maturity: wired
|
|
11
|
+
//! ---
|
|
1
12
|
use crate::lifecycle::*;
|
|
2
13
|
use crate::model::ids::AgentId;
|
|
3
14
|
use crate::provider::SessionId;
|
|
@@ -5,6 +16,15 @@ use std::path::Path;
|
|
|
5
16
|
|
|
6
17
|
use super::*;
|
|
7
18
|
|
|
19
|
+
/// ---
|
|
20
|
+
/// purpose: 以源席最新角色文件另起一席,不继承源会话
|
|
21
|
+
/// params:
|
|
22
|
+
/// source_agent_id: 源席位,只借它的角色文件与工具集
|
|
23
|
+
/// as_agent_id: 新席位名
|
|
24
|
+
/// label: 新席位的角色标签
|
|
25
|
+
/// returns: clone 报告
|
|
26
|
+
/// errors: 选不到 team 返回 TeamSelect,源席不存在返回 RequirementUnmet,角色物化与加席错误透传
|
|
27
|
+
/// ---
|
|
8
28
|
pub fn clone_agent(
|
|
9
29
|
workspace: &Path,
|
|
10
30
|
source_agent_id: &AgentId,
|
|
@@ -40,16 +60,10 @@ pub fn clone_agent(
|
|
|
40
60
|
as_agent_id,
|
|
41
61
|
label,
|
|
42
62
|
)?;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
let spec = crate::model::yaml::loads(
|
|
48
|
-
&std::fs::read_to_string(spec_path)
|
|
49
|
-
.map_err(|error| LifecycleError::Compile(error.to_string()))?,
|
|
50
|
-
)
|
|
51
|
-
.map_err(|error| LifecycleError::Compile(error.to_string()))?;
|
|
52
|
-
clamp_materialized_role_to_leader(materialized.path(), &spec)?;
|
|
63
|
+
// The materialized role carries the source role's DECLARED tools verbatim —
|
|
64
|
+
// a clone must preserve the source seat's full tools set (add-agent does
|
|
65
|
+
// the same; no leader-ceiling clamp). See role_source.rs for the removed
|
|
66
|
+
// clamp_materialized_role_to_leader.
|
|
53
67
|
let added = add_agent(
|
|
54
68
|
&selected.run_workspace,
|
|
55
69
|
as_agent_id,
|
|
@@ -63,6 +77,15 @@ pub fn clone_agent(
|
|
|
63
77
|
as_agent_id,
|
|
64
78
|
source_session_id.as_ref(),
|
|
65
79
|
);
|
|
80
|
+
if let Some((session_id, backing_path)) = verified.as_ref() {
|
|
81
|
+
persist_clone_session(
|
|
82
|
+
&selected.run_workspace,
|
|
83
|
+
selected.team_key.as_str(),
|
|
84
|
+
as_agent_id,
|
|
85
|
+
session_id,
|
|
86
|
+
backing_path,
|
|
87
|
+
)?;
|
|
88
|
+
}
|
|
66
89
|
let (session_id, backing_path, backing_state) = match verified {
|
|
67
90
|
Some((session_id, backing_path)) => (
|
|
68
91
|
Some(session_id),
|
|
@@ -94,13 +117,80 @@ fn read_agent_session(
|
|
|
94
117
|
.and_then(|agents| agents.get(agent_id.as_str()))?;
|
|
95
118
|
let session = agent
|
|
96
119
|
.get("session_id")
|
|
120
|
+
.or_else(|| agent.get("_pending_session_id"))
|
|
97
121
|
.and_then(serde_json::Value::as_str)
|
|
98
122
|
.filter(|value| !value.is_empty())?;
|
|
99
123
|
let backing = agent
|
|
100
124
|
.get("rollout_path")
|
|
101
125
|
.and_then(serde_json::Value::as_str)
|
|
102
126
|
.filter(|value| !value.is_empty())
|
|
103
|
-
.map(std::path::PathBuf::from)
|
|
127
|
+
.map(std::path::PathBuf::from)
|
|
128
|
+
.or_else(|| find_claude_backing(agent, session))?;
|
|
104
129
|
let distinct = source_session_id.is_none_or(|source| source.as_str() != session);
|
|
105
130
|
(distinct && backing.is_file()).then(|| (SessionId::new(session), backing))
|
|
106
131
|
}
|
|
132
|
+
|
|
133
|
+
fn find_claude_backing(agent: &serde_json::Value, session: &str) -> Option<std::path::PathBuf> {
|
|
134
|
+
let root = agent
|
|
135
|
+
.get("claude_projects_root")
|
|
136
|
+
.and_then(serde_json::Value::as_str)
|
|
137
|
+
.map(std::path::PathBuf::from)
|
|
138
|
+
.or_else(|| {
|
|
139
|
+
std::env::var_os("HOME")
|
|
140
|
+
.map(|home| std::path::PathBuf::from(home).join(".claude/projects"))
|
|
141
|
+
})?;
|
|
142
|
+
let target = format!("{session}.jsonl");
|
|
143
|
+
let mut pending = vec![root];
|
|
144
|
+
while let Some(dir) = pending.pop() {
|
|
145
|
+
let entries = std::fs::read_dir(&dir).ok()?;
|
|
146
|
+
for entry in entries.flatten() {
|
|
147
|
+
let path = entry.path();
|
|
148
|
+
if path.is_dir() {
|
|
149
|
+
pending.push(path);
|
|
150
|
+
} else if path.file_name().and_then(|name| name.to_str()) == Some(target.as_str()) {
|
|
151
|
+
return Some(path);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
None
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fn persist_clone_session(
|
|
159
|
+
workspace: &Path,
|
|
160
|
+
team_key: &str,
|
|
161
|
+
agent_id: &AgentId,
|
|
162
|
+
session_id: &SessionId,
|
|
163
|
+
backing_path: &Path,
|
|
164
|
+
) -> Result<(), LifecycleError> {
|
|
165
|
+
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
166
|
+
workspace,
|
|
167
|
+
operation: "clone-session-bind",
|
|
168
|
+
team: Some(team_key),
|
|
169
|
+
agent_id: Some(agent_id),
|
|
170
|
+
})?;
|
|
171
|
+
let mut state = crate::state::projection::select_runtime_state(workspace, Some(team_key))
|
|
172
|
+
.map_err(|error| LifecycleError::TeamSelect(error.to_string()))?;
|
|
173
|
+
let Some(agent) = state
|
|
174
|
+
.get_mut("agents")
|
|
175
|
+
.and_then(serde_json::Value::as_object_mut)
|
|
176
|
+
.and_then(|agents| agents.get_mut(agent_id.as_str()))
|
|
177
|
+
.and_then(serde_json::Value::as_object_mut)
|
|
178
|
+
else {
|
|
179
|
+
return Ok(());
|
|
180
|
+
};
|
|
181
|
+
agent.insert(
|
|
182
|
+
"session_id".to_string(),
|
|
183
|
+
serde_json::json!(session_id.as_str()),
|
|
184
|
+
);
|
|
185
|
+
agent.insert(
|
|
186
|
+
"rollout_path".to_string(),
|
|
187
|
+
serde_json::json!(backing_path.to_string_lossy().to_string()),
|
|
188
|
+
);
|
|
189
|
+
agent.insert("captured_via".to_string(), serde_json::json!("clone-agent"));
|
|
190
|
+
crate::lifecycle::launch::save_launched_team_state_for_key(
|
|
191
|
+
workspace,
|
|
192
|
+
&state,
|
|
193
|
+
Some(team_key),
|
|
194
|
+
Some(agent_id.as_str()),
|
|
195
|
+
)
|
|
196
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
//! ---
|
|
2
|
+
//! purpose: cursor 候选 A——`agent create-chat` 解析首行 chatId 后按记录 PID 杀
|
|
3
|
+
//! contract:
|
|
4
|
+
//! provides:
|
|
5
|
+
//! - name: apply_cursor_create_chat_plan
|
|
6
|
+
//! what: 无 pending 时把 CLI chatId 写入 expected_session_id 并 `--resume` 该 id
|
|
7
|
+
//! - name: create_cursor_chat_id
|
|
8
|
+
//! what: 跑 create-chat,读第一行 UUID,只杀本调用记录的 PID
|
|
9
|
+
//! requires:
|
|
10
|
+
//! - name: cursor-agent-cli
|
|
11
|
+
//! what: PATH 上的 `agent`;不存在或首行非法则返回 None(降级候选 B)
|
|
12
|
+
//! boundary:
|
|
13
|
+
//! - 不发明 --session-id;不 pkill/killall;不 wait 子进程自行退出
|
|
14
|
+
//! - 不读 chats 正文、不打印 proxy 值
|
|
15
|
+
//! - 已有 expected_session_id 或 argv 已含 --resume 则不动
|
|
16
|
+
//! - 2026-08-21 活体:create-chat 后 `--resume` 的存档在 pane stop 后被清;
|
|
17
|
+
//! TUI 自建会话能留盘。fresh 路径不调用本函数(走 scan 唯一 hex)。
|
|
18
|
+
//! maturity: wired
|
|
19
|
+
//! ---
|
|
20
|
+
|
|
21
|
+
use std::io::{BufRead, BufReader};
|
|
22
|
+
use std::path::Path;
|
|
23
|
+
use std::process::{Command, Stdio};
|
|
24
|
+
use std::sync::mpsc;
|
|
25
|
+
use std::thread;
|
|
26
|
+
use std::time::Duration;
|
|
27
|
+
|
|
28
|
+
use crate::provider::types::{CommandPlan, SessionId};
|
|
29
|
+
|
|
30
|
+
const FIRST_LINE_TIMEOUT: Duration = Duration::from_secs(5);
|
|
31
|
+
const TERM_GRACE: Duration = Duration::from_millis(200);
|
|
32
|
+
|
|
33
|
+
/// ---
|
|
34
|
+
/// purpose: 冷启/无 session 的 cursor 计划绑定 create-chat 得到的 chatId
|
|
35
|
+
/// params:
|
|
36
|
+
/// plan: build_command_plan 产出;workspace: 物理 cwd
|
|
37
|
+
/// returns: 成功则 expected_session_id 与 argv `--resume <id>` 齐;失败则计划不变
|
|
38
|
+
/// contract:
|
|
39
|
+
/// provides:
|
|
40
|
+
/// - name: apply_cursor_create_chat_plan
|
|
41
|
+
/// what: 只在无 pending 且 argv 无 --resume 时写入 CLI chatId
|
|
42
|
+
/// boundary:
|
|
43
|
+
/// - 库测试(cfg(test))不调真实 agent,避免订阅/挂起;具名测试打 create_cursor_chat_id
|
|
44
|
+
/// - 不在 launch/restart fresh 调用:活体证实 `--resume` 空聊存档停 pane 后消失
|
|
45
|
+
/// ---
|
|
46
|
+
pub(crate) fn apply_cursor_create_chat_plan(plan: &mut CommandPlan, workspace: &Path) {
|
|
47
|
+
if cfg!(test) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if plan.expected_session_id.is_some() {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if plan.argv.iter().any(|flag| flag == "--resume") {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
let Some(chat_id) = create_cursor_chat_id(workspace) else {
|
|
57
|
+
return;
|
|
58
|
+
};
|
|
59
|
+
plan.expected_session_id = Some(SessionId::new(chat_id.clone()));
|
|
60
|
+
plan.argv.push("--resume".to_string());
|
|
61
|
+
plan.argv.push(chat_id);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// ---
|
|
65
|
+
/// purpose: 跑 `agent create-chat`,解析首行 UUID,按记录 PID 终止挂起进程
|
|
66
|
+
/// params:
|
|
67
|
+
/// workspace: create-chat 的 cwd(物理 workspace)
|
|
68
|
+
/// returns: 合法 UUID;命令缺失/超时/非法首行则 None
|
|
69
|
+
/// contract:
|
|
70
|
+
/// provides:
|
|
71
|
+
/// - name: create_cursor_chat_id
|
|
72
|
+
/// what: 读一行就停;不 wait 退出;只杀本 Child.id()
|
|
73
|
+
/// boundary:
|
|
74
|
+
/// - 禁 pkill/killall;pid 0/1 不杀
|
|
75
|
+
/// ---
|
|
76
|
+
pub(crate) fn create_cursor_chat_id(workspace: &Path) -> Option<String> {
|
|
77
|
+
create_cursor_chat_id_with_command("agent", workspace)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fn create_cursor_chat_id_with_command(program: &str, workspace: &Path) -> Option<String> {
|
|
81
|
+
let mut child = Command::new(program)
|
|
82
|
+
.arg("create-chat")
|
|
83
|
+
.current_dir(workspace)
|
|
84
|
+
.stdin(Stdio::null())
|
|
85
|
+
.stdout(Stdio::piped())
|
|
86
|
+
.stderr(Stdio::null())
|
|
87
|
+
.spawn()
|
|
88
|
+
.ok()?;
|
|
89
|
+
let pid = child.id();
|
|
90
|
+
let stdout = child.stdout.take()?;
|
|
91
|
+
let (tx, rx) = mpsc::channel();
|
|
92
|
+
thread::spawn(move || {
|
|
93
|
+
let mut reader = BufReader::new(stdout);
|
|
94
|
+
let mut line = String::new();
|
|
95
|
+
let result = reader.read_line(&mut line).ok().and_then(|_| parse_chat_id_line(&line));
|
|
96
|
+
let _ = tx.send(result);
|
|
97
|
+
});
|
|
98
|
+
let parsed = match rx.recv_timeout(FIRST_LINE_TIMEOUT) {
|
|
99
|
+
Ok(value) => value,
|
|
100
|
+
Err(_) => None,
|
|
101
|
+
};
|
|
102
|
+
kill_recorded_pid(pid);
|
|
103
|
+
let _ = child.wait();
|
|
104
|
+
parsed
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
fn parse_chat_id_line(line: &str) -> Option<String> {
|
|
108
|
+
let token = line
|
|
109
|
+
.trim()
|
|
110
|
+
.split_whitespace()
|
|
111
|
+
.next()
|
|
112
|
+
.unwrap_or("")
|
|
113
|
+
.trim_matches(['"', '\'']);
|
|
114
|
+
is_chat_uuid(token).then(|| token.to_string())
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fn is_chat_uuid(value: &str) -> bool {
|
|
118
|
+
let bytes = value.as_bytes();
|
|
119
|
+
if bytes.len() != 36 {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
for (i, b) in bytes.iter().enumerate() {
|
|
123
|
+
if i == 8 || i == 13 || i == 18 || i == 23 {
|
|
124
|
+
if *b != b'-' {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
} else if !b.is_ascii_hexdigit() {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
true
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
fn kill_recorded_pid(pid: u32) {
|
|
135
|
+
if pid == 0 || pid == 1 {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
#[cfg(unix)]
|
|
139
|
+
{
|
|
140
|
+
let raw = pid as i32;
|
|
141
|
+
unsafe {
|
|
142
|
+
libc::kill(raw, libc::SIGTERM);
|
|
143
|
+
}
|
|
144
|
+
thread::sleep(TERM_GRACE);
|
|
145
|
+
unsafe {
|
|
146
|
+
libc::kill(raw, libc::SIGKILL);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
#[cfg(not(unix))]
|
|
150
|
+
{
|
|
151
|
+
let _ = pid;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
#[cfg(test)]
|
|
156
|
+
mod tests {
|
|
157
|
+
#![allow(clippy::unwrap_used)]
|
|
158
|
+
|
|
159
|
+
use super::*;
|
|
160
|
+
use std::os::unix::fs::PermissionsExt;
|
|
161
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
162
|
+
|
|
163
|
+
fn tmp_root(tag: &str) -> std::path::PathBuf {
|
|
164
|
+
static CTR: AtomicU64 = AtomicU64::new(0);
|
|
165
|
+
let dir = std::env::temp_dir().join(format!(
|
|
166
|
+
"ta-d117-create-chat-{}-{}-{}",
|
|
167
|
+
tag,
|
|
168
|
+
std::process::id(),
|
|
169
|
+
CTR.fetch_add(1, Ordering::Relaxed)
|
|
170
|
+
));
|
|
171
|
+
let _ = std::fs::remove_dir_all(&dir);
|
|
172
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
173
|
+
dir
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fn write_stub(dir: &Path, body: &str) -> std::path::PathBuf {
|
|
177
|
+
let path = dir.join("agent");
|
|
178
|
+
std::fs::write(&path, body).unwrap();
|
|
179
|
+
let mut perm = std::fs::metadata(&path).unwrap().permissions();
|
|
180
|
+
perm.set_mode(0o755);
|
|
181
|
+
std::fs::set_permissions(&path, perm).unwrap();
|
|
182
|
+
path
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#[test]
|
|
186
|
+
fn create_chat_reads_first_line_and_kills_recorded_pid() {
|
|
187
|
+
let base = tmp_root("ok");
|
|
188
|
+
let ws = base.join("ws");
|
|
189
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
190
|
+
let bin = write_stub(
|
|
191
|
+
&base,
|
|
192
|
+
"#!/bin/sh\necho '681c8837-d564-44fa-98bf-4ff5a66592e2'\nkill -STOP $$\n",
|
|
193
|
+
);
|
|
194
|
+
let got = create_cursor_chat_id_with_command(bin.to_str().unwrap(), &ws);
|
|
195
|
+
assert_eq!(
|
|
196
|
+
got.as_deref(),
|
|
197
|
+
Some("681c8837-d564-44fa-98bf-4ff5a66592e2")
|
|
198
|
+
);
|
|
199
|
+
let _ = std::fs::remove_dir_all(&base);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
#[test]
|
|
203
|
+
fn create_chat_rejects_non_uuid_first_line() {
|
|
204
|
+
let base = tmp_root("bad");
|
|
205
|
+
let ws = base.join("ws");
|
|
206
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
207
|
+
let bin = write_stub(&base, "#!/bin/sh\necho not-a-uuid\nkill -STOP $$\n");
|
|
208
|
+
let got = create_cursor_chat_id_with_command(bin.to_str().unwrap(), &ws);
|
|
209
|
+
assert!(got.is_none());
|
|
210
|
+
let _ = std::fs::remove_dir_all(&base);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
#[test]
|
|
214
|
+
fn apply_is_noop_when_resume_already_present() {
|
|
215
|
+
let mut plan = CommandPlan {
|
|
216
|
+
argv: vec![
|
|
217
|
+
"agent".into(),
|
|
218
|
+
"--resume".into(),
|
|
219
|
+
"already-bound".into(),
|
|
220
|
+
],
|
|
221
|
+
expected_session_id: None,
|
|
222
|
+
provider_projects_root: None,
|
|
223
|
+
managed_mcp_config: false,
|
|
224
|
+
};
|
|
225
|
+
apply_cursor_create_chat_plan(&mut plan, Path::new("/tmp"));
|
|
226
|
+
assert!(plan.expected_session_id.is_none());
|
|
227
|
+
assert_eq!(plan.argv.iter().filter(|a| *a == "--resume").count(), 1);
|
|
228
|
+
}
|
|
229
|
+
}
|