@team-agent/installer 0.5.100 → 0.5.101
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 +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/diagnose.rs +29 -7
- package/crates/team-agent/src/cli/mod.rs +24 -8
- package/crates/team-agent/src/compiler.rs +3 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +211 -16
- package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +15 -5
- package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +89 -6
- package/crates/team-agent/src/lifecycle/launch/role_source.rs +168 -32
- package/crates/team-agent/src/lifecycle/launch.rs +11 -3
- package/crates/team-agent/src/lifecycle/profile_launch.rs +23 -8
- package/crates/team-agent/src/lifecycle/restart/agent.rs +2 -2
- package/crates/team-agent/src/lifecycle/restart/avatar.rs +1806 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +114 -1
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +8 -2
- package/crates/team-agent/src/lifecycle/restart/selection.rs +14 -2
- package/crates/team-agent/src/lifecycle/restart.rs +11 -4
- package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +1398 -0
- package/crates/team-agent/src/lifecycle/tests.rs +1 -0
- package/crates/team-agent/src/lifecycle/types.rs +10 -2
- package/crates/team-agent/src/model/spec.rs +1 -0
- package/crates/team-agent/src/provider/types.rs +2 -0
- package/crates/team-agent/src/state/persist.rs +58 -2
- package/crates/team-agent/src/state/projection.rs +5 -1
- package/crates/team-agent/src/state/repository.rs +125 -4
- package/crates/team-agent/src/transport/test_support.rs +23 -3
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +17 -46
- package/skills/team-agent/references/team-agent-operator.md +4 -2
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -94,6 +94,14 @@ pub(crate) fn classify_tmux_server_error(error_text: &str) -> &'static str {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
pub(crate) fn diagnose_runtime(state: &Value, backend: &dyn Transport) -> (Value, Value) {
|
|
97
|
+
diagnose_runtime_with_workspace(None, state, backend)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fn diagnose_runtime_with_workspace(
|
|
101
|
+
workspace: Option<&std::path::Path>,
|
|
102
|
+
state: &Value,
|
|
103
|
+
backend: &dyn Transport,
|
|
104
|
+
) -> (Value, Value) {
|
|
97
105
|
let mut issues = Vec::new();
|
|
98
106
|
let mut repairs = Vec::new();
|
|
99
107
|
|
|
@@ -248,12 +256,25 @@ pub(crate) fn diagnose_runtime(state: &Value, backend: &dyn Transport) -> (Value
|
|
|
248
256
|
.and_then(Value::as_str)
|
|
249
257
|
.filter(|path| !path.is_empty())
|
|
250
258
|
.map(crate::provider::RolloutPath::new);
|
|
251
|
-
let
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
259
|
+
let agent_identity = crate::model::ids::AgentId::new(agent_id.clone());
|
|
260
|
+
let identity_probe = workspace
|
|
261
|
+
.map(|workspace| {
|
|
262
|
+
crate::lifecycle::restart::session_identity_probe_for_agent_with_fork_ancestry(
|
|
263
|
+
workspace,
|
|
264
|
+
state,
|
|
265
|
+
&agent_identity,
|
|
266
|
+
agent_state,
|
|
267
|
+
provider,
|
|
268
|
+
rollout_path.as_ref(),
|
|
269
|
+
)
|
|
270
|
+
})
|
|
271
|
+
.unwrap_or_else(|| {
|
|
272
|
+
crate::lifecycle::restart::session_identity_probe_for_agent(
|
|
273
|
+
&agent_identity,
|
|
274
|
+
provider,
|
|
275
|
+
rollout_path.as_ref(),
|
|
276
|
+
)
|
|
277
|
+
});
|
|
257
278
|
if identity_probe.identity_ok == Some(false) {
|
|
258
279
|
let issue = format!("session_identity_mismatch:{agent_id}");
|
|
259
280
|
issues.push(json!(issue));
|
|
@@ -304,7 +325,8 @@ pub(crate) fn diagnose_runtime_for_workspace(
|
|
|
304
325
|
selected_team_key: Option<&str>,
|
|
305
326
|
coordinator_health: &crate::coordinator::HealthReport,
|
|
306
327
|
) -> (Value, Value) {
|
|
307
|
-
let (mut issues, mut repairs) =
|
|
328
|
+
let (mut issues, mut repairs) =
|
|
329
|
+
diagnose_runtime_with_workspace(Some(workspace), state, backend);
|
|
308
330
|
append_live_leader_workspace_mismatch_issue(
|
|
309
331
|
workspace,
|
|
310
332
|
state,
|
|
@@ -2926,14 +2926,30 @@ pub mod lifecycle_port {
|
|
|
2926
2926
|
let source = crate::model::ids::AgentId::new(source_agent);
|
|
2927
2927
|
let dest = crate::model::ids::AgentId::new(as_agent_id);
|
|
2928
2928
|
match crate::lifecycle::fork_agent(workspace, &source, &dest, label, true, team) {
|
|
2929
|
-
Ok(report) =>
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2929
|
+
Ok(report) => {
|
|
2930
|
+
let mut value = json!({
|
|
2931
|
+
"ok": true,
|
|
2932
|
+
"source_agent_id": report.source_agent_id.as_str(),
|
|
2933
|
+
"new_agent_id": report.new_agent_id.as_str(),
|
|
2934
|
+
"session_id": report.session_id.as_ref().map(|session| session.as_str()),
|
|
2935
|
+
"new_session_id": report.session_id.as_ref().map(|session| session.as_str()),
|
|
2936
|
+
"backing_state": report.backing_state,
|
|
2937
|
+
});
|
|
2938
|
+
if let (Some(details), Some(object)) = (report.pi_fork, value.as_object_mut()) {
|
|
2939
|
+
object.insert("status".to_string(), json!("forked"));
|
|
2940
|
+
object.insert("target".to_string(), json!(report.new_agent_id.as_str()));
|
|
2941
|
+
object.insert(
|
|
2942
|
+
"source_session_id".to_string(),
|
|
2943
|
+
json!(details.source_session_id.as_str()),
|
|
2944
|
+
);
|
|
2945
|
+
object.insert("session_id".to_string(), json!(details.session_id.as_str()));
|
|
2946
|
+
object.insert(
|
|
2947
|
+
"backing_path".to_string(),
|
|
2948
|
+
json!(details.backing_path.0.to_string_lossy().to_string()),
|
|
2949
|
+
);
|
|
2950
|
+
}
|
|
2951
|
+
Ok(value)
|
|
2952
|
+
}
|
|
2937
2953
|
Err(e) => Ok(error_value(e)),
|
|
2938
2954
|
}
|
|
2939
2955
|
}
|
|
@@ -555,6 +555,9 @@ fn compile_role_agent_with_mode(
|
|
|
555
555
|
]),
|
|
556
556
|
),
|
|
557
557
|
];
|
|
558
|
+
if let Some(label) = string_field(&meta, "label") {
|
|
559
|
+
agent_items.insert(1, ("label", Value::Str(label)));
|
|
560
|
+
}
|
|
558
561
|
if let Some(profile) = string_field(&meta, "profile") {
|
|
559
562
|
agent_items.push(("profile", Value::Str(profile)));
|
|
560
563
|
}
|
|
@@ -177,6 +177,216 @@ pub(super) fn upsert_agent_state_from_role(
|
|
|
177
177
|
"runtime state agents is not an object".to_string(),
|
|
178
178
|
));
|
|
179
179
|
};
|
|
180
|
+
let entry = starting_agent_entry(workspace, agent_id, meta, dynamic_role_file);
|
|
181
|
+
agent_map.insert(agent_id.as_str().to_string(), entry);
|
|
182
|
+
crate::lifecycle::restart::remove::clear_agent_retirement_in_state(&mut state, agent_id);
|
|
183
|
+
save_launched_team_state_for_key(
|
|
184
|
+
workspace,
|
|
185
|
+
&state,
|
|
186
|
+
Some(canonical_team_key),
|
|
187
|
+
Some(agent_id.as_str()),
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// ---
|
|
192
|
+
/// purpose: 首次登记 Pi fork 席位,原子带入目标角色与完整 fork capture seed
|
|
193
|
+
/// returns: 成功后 TARGET 已按 ForkAgent typed intent 登记为 starting
|
|
194
|
+
/// errors: 重复席位、无效 capture seed 或 state 写入失败时拒绝
|
|
195
|
+
/// ---
|
|
196
|
+
pub(crate) fn fork_upsert_agent_state_from_role(
|
|
197
|
+
workspace: &Path,
|
|
198
|
+
canonical_team_key: &str,
|
|
199
|
+
agent_id: &AgentId,
|
|
200
|
+
meta: &Value,
|
|
201
|
+
compiled_agent: &Value,
|
|
202
|
+
dynamic_role_file: &Path,
|
|
203
|
+
seed: &ForkCaptureSeed,
|
|
204
|
+
) -> Result<(), LifecycleError> {
|
|
205
|
+
if agent_id == &seed.source_agent_id {
|
|
206
|
+
return Err(LifecycleError::RequirementUnmet(
|
|
207
|
+
"fork target must differ from source".to_string(),
|
|
208
|
+
));
|
|
209
|
+
}
|
|
210
|
+
if seed.captured.captured_via != crate::provider::CaptureVia::ForkSnapshot
|
|
211
|
+
|| seed.captured.attribution_confidence != crate::provider::Confidence::High
|
|
212
|
+
|| seed.captured.session_id.is_none()
|
|
213
|
+
|| seed.captured.rollout_path.is_none()
|
|
214
|
+
|| seed.captured_at.is_empty()
|
|
215
|
+
{
|
|
216
|
+
return Err(LifecycleError::RequirementUnmet(
|
|
217
|
+
"fork capture seed is incomplete or not a verified Pi snapshot".to_string(),
|
|
218
|
+
));
|
|
219
|
+
}
|
|
220
|
+
if compiled_agent.get("id").and_then(Value::as_str) != Some(agent_id.as_str()) {
|
|
221
|
+
return Err(LifecycleError::Compile(
|
|
222
|
+
"compiled fork role id does not match target".to_string(),
|
|
223
|
+
));
|
|
224
|
+
}
|
|
225
|
+
let sessions_root = std::fs::canonicalize(&seed.pi_sessions_root)
|
|
226
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
227
|
+
let backing = seed.captured.rollout_path.as_ref().ok_or_else(|| {
|
|
228
|
+
LifecycleError::RequirementUnmet("fork backing path is missing".to_string())
|
|
229
|
+
})?;
|
|
230
|
+
let backing_path = std::fs::canonicalize(&backing.0)
|
|
231
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
232
|
+
if !backing_path.starts_with(&sessions_root) {
|
|
233
|
+
return Err(LifecycleError::RequirementUnmet(
|
|
234
|
+
"fork backing is outside the target Pi session root".to_string(),
|
|
235
|
+
));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let mut state =
|
|
239
|
+
crate::state::projection::select_runtime_state(workspace, Some(canonical_team_key))
|
|
240
|
+
.map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
|
|
241
|
+
if !state.is_object() {
|
|
242
|
+
state = serde_json::json!({});
|
|
243
|
+
}
|
|
244
|
+
let root = state.as_object_mut().ok_or_else(|| {
|
|
245
|
+
LifecycleError::StatePersist("runtime state root is not an object".to_string())
|
|
246
|
+
})?;
|
|
247
|
+
let agents = root
|
|
248
|
+
.entry("agents".to_string())
|
|
249
|
+
.or_insert_with(|| serde_json::json!({}));
|
|
250
|
+
if !agents.is_object() {
|
|
251
|
+
*agents = serde_json::json!({});
|
|
252
|
+
}
|
|
253
|
+
let agent_map = agents.as_object_mut().ok_or_else(|| {
|
|
254
|
+
LifecycleError::StatePersist("runtime state agents is not an object".to_string())
|
|
255
|
+
})?;
|
|
256
|
+
if agent_map.contains_key(agent_id.as_str()) {
|
|
257
|
+
return Err(LifecycleError::RequirementUnmet(format!(
|
|
258
|
+
"agent id already exists in selected team: {}",
|
|
259
|
+
agent_id
|
|
260
|
+
)));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let source_profile_dir = agent_map
|
|
264
|
+
.get(seed.source_agent_id.as_str())
|
|
265
|
+
.and_then(serde_json::Value::as_object)
|
|
266
|
+
.and_then(|source| source.get("_profile_dir"))
|
|
267
|
+
.and_then(serde_json::Value::as_str)
|
|
268
|
+
.filter(|path| !path.is_empty())
|
|
269
|
+
.map(|path| serde_json::json!(path));
|
|
270
|
+
let mut entry = starting_agent_entry(workspace, agent_id, meta, dynamic_role_file);
|
|
271
|
+
let entry_object = entry.as_object_mut().ok_or_else(|| {
|
|
272
|
+
LifecycleError::StatePersist("fork target entry is not an object".to_string())
|
|
273
|
+
})?;
|
|
274
|
+
for field in [
|
|
275
|
+
"provider",
|
|
276
|
+
"auth_mode",
|
|
277
|
+
"role",
|
|
278
|
+
"model",
|
|
279
|
+
"effort",
|
|
280
|
+
"profile",
|
|
281
|
+
"tools",
|
|
282
|
+
"system_prompt",
|
|
283
|
+
"output_contract",
|
|
284
|
+
"communication_mode",
|
|
285
|
+
"working_directory",
|
|
286
|
+
"cwd",
|
|
287
|
+
"config",
|
|
288
|
+
"name",
|
|
289
|
+
"dangerously_skip_permissions",
|
|
290
|
+
] {
|
|
291
|
+
if let Some(value) = compiled_agent.get(field) {
|
|
292
|
+
entry_object.insert(field.to_string(), yaml_value_to_json(value));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if compiled_agent.get("model").is_some() && !entry_object.contains_key("model_source") {
|
|
296
|
+
entry_object.insert("model_source".to_string(), serde_json::json!("team"));
|
|
297
|
+
}
|
|
298
|
+
entry_object.insert("agent_id".to_string(), serde_json::json!(agent_id.as_str()));
|
|
299
|
+
entry_object.insert("label".to_string(), serde_json::json!(agent_id.as_str()));
|
|
300
|
+
entry_object.insert(
|
|
301
|
+
"owner_team_id".to_string(),
|
|
302
|
+
serde_json::json!(canonical_team_key),
|
|
303
|
+
);
|
|
304
|
+
entry_object.insert(
|
|
305
|
+
"claude_projects_root".to_string(),
|
|
306
|
+
serde_json::json!(sessions_root.to_string_lossy().to_string()),
|
|
307
|
+
);
|
|
308
|
+
entry_object.insert(
|
|
309
|
+
"forked_from".to_string(),
|
|
310
|
+
serde_json::json!(seed.source_agent_id.as_str()),
|
|
311
|
+
);
|
|
312
|
+
entry_object.insert(
|
|
313
|
+
"spawn_cwd".to_string(),
|
|
314
|
+
serde_json::json!(seed.captured.spawn_cwd.to_string_lossy().to_string()),
|
|
315
|
+
);
|
|
316
|
+
entry_object.insert(
|
|
317
|
+
"session_id".to_string(),
|
|
318
|
+
serde_json::json!(seed.captured.session_id.as_ref().map(|id| id.as_str())),
|
|
319
|
+
);
|
|
320
|
+
let backing = seed.captured.rollout_path.as_ref().ok_or_else(|| {
|
|
321
|
+
LifecycleError::RequirementUnmet("fork backing path is missing".to_string())
|
|
322
|
+
})?;
|
|
323
|
+
entry_object.insert(
|
|
324
|
+
"rollout_path".to_string(),
|
|
325
|
+
serde_json::json!(backing.0.to_string_lossy().to_string()),
|
|
326
|
+
);
|
|
327
|
+
entry_object.insert(
|
|
328
|
+
"captured_at".to_string(),
|
|
329
|
+
serde_json::json!(seed.captured_at.as_str()),
|
|
330
|
+
);
|
|
331
|
+
entry_object.insert(
|
|
332
|
+
"captured_via".to_string(),
|
|
333
|
+
serde_json::to_value(seed.captured.captured_via)
|
|
334
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?,
|
|
335
|
+
);
|
|
336
|
+
entry_object.insert(
|
|
337
|
+
"attribution_confidence".to_string(),
|
|
338
|
+
serde_json::to_value(seed.captured.attribution_confidence)
|
|
339
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?,
|
|
340
|
+
);
|
|
341
|
+
entry_object.insert("capture_state".to_string(), serde_json::json!("captured"));
|
|
342
|
+
if let Some(profile_dir) = source_profile_dir {
|
|
343
|
+
entry_object.insert("_profile_dir".to_string(), profile_dir);
|
|
344
|
+
} else {
|
|
345
|
+
entry_object.insert(
|
|
346
|
+
"_profile_dir".to_string(),
|
|
347
|
+
serde_json::json!(seed.profile_dir.to_string_lossy().to_string()),
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
agent_map.insert(agent_id.as_str().to_string(), entry);
|
|
351
|
+
crate::lifecycle::restart::remove::clear_agent_retirement_in_state(&mut state, agent_id);
|
|
352
|
+
crate::state::repository::StateRepository::new(workspace)
|
|
353
|
+
.save(
|
|
354
|
+
crate::state::repository::StateWriteIntent::ForkAgent {
|
|
355
|
+
team_key: canonical_team_key,
|
|
356
|
+
agent_id: agent_id.as_str(),
|
|
357
|
+
},
|
|
358
|
+
&state,
|
|
359
|
+
)
|
|
360
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
fn yaml_value_to_json(value: &Value) -> serde_json::Value {
|
|
364
|
+
match value {
|
|
365
|
+
Value::Null => serde_json::Value::Null,
|
|
366
|
+
Value::Bool(value) => serde_json::json!(value),
|
|
367
|
+
Value::Int(value) => serde_json::json!(value),
|
|
368
|
+
Value::Float(value) => serde_json::Number::from_f64(*value)
|
|
369
|
+
.map(serde_json::Value::Number)
|
|
370
|
+
.unwrap_or(serde_json::Value::Null),
|
|
371
|
+
Value::Str(value) => serde_json::json!(value),
|
|
372
|
+
Value::List(values) => {
|
|
373
|
+
serde_json::Value::Array(values.iter().map(yaml_value_to_json).collect())
|
|
374
|
+
}
|
|
375
|
+
Value::Map(values) => serde_json::Value::Object(
|
|
376
|
+
values
|
|
377
|
+
.iter()
|
|
378
|
+
.map(|(key, value)| (key.clone(), yaml_value_to_json(value)))
|
|
379
|
+
.collect(),
|
|
380
|
+
),
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
fn starting_agent_entry(
|
|
385
|
+
workspace: &Path,
|
|
386
|
+
agent_id: &AgentId,
|
|
387
|
+
meta: &Value,
|
|
388
|
+
dynamic_role_file: &Path,
|
|
389
|
+
) -> serde_json::Value {
|
|
180
390
|
let provider = meta
|
|
181
391
|
.get("provider")
|
|
182
392
|
.and_then(Value::as_str)
|
|
@@ -189,10 +399,6 @@ pub(super) fn upsert_agent_state_from_role(
|
|
|
189
399
|
.get("role")
|
|
190
400
|
.and_then(Value::as_str)
|
|
191
401
|
.unwrap_or_else(|| agent_id.as_str());
|
|
192
|
-
// E42 (0.3.24 P0, double-spec deadlock): persist the initial state row as
|
|
193
|
-
// "starting" (not "running"). The caller (add_agent_with_transport_at_paths)
|
|
194
|
-
// promotes to "running" only after start_agent_at_paths returns Running.
|
|
195
|
-
// If the spawn fails, the rollback below removes the entry entirely.
|
|
196
402
|
let mut entry = serde_json::json!({
|
|
197
403
|
"provider": provider,
|
|
198
404
|
"auth_mode": auth_mode,
|
|
@@ -221,9 +427,6 @@ pub(super) fn upsert_agent_state_from_role(
|
|
|
221
427
|
}
|
|
222
428
|
}
|
|
223
429
|
}
|
|
224
|
-
// 0.4.x provider effort MVP step 8 (dynamic add-agent): persist effort
|
|
225
|
-
// from the role doc front matter (compiler.rs validates syntax/semantics
|
|
226
|
-
// at compile; add-agent path validates here too in case of direct YAML).
|
|
227
430
|
if let Some(effort_str) = meta.get("effort").and_then(Value::as_str) {
|
|
228
431
|
if !effort_str.is_empty() {
|
|
229
432
|
if let Some(obj) = entry.as_object_mut() {
|
|
@@ -232,7 +435,6 @@ pub(super) fn upsert_agent_state_from_role(
|
|
|
232
435
|
}
|
|
233
436
|
}
|
|
234
437
|
if let Some(obj) = entry.as_object_mut() {
|
|
235
|
-
// 0.5.66 bypass 单源:policy 从 role 文档字段派生,不再用全队 `DangerousApproval`。
|
|
236
438
|
let meta_provider = meta
|
|
237
439
|
.get("provider")
|
|
238
440
|
.and_then(Value::as_str)
|
|
@@ -240,14 +442,7 @@ pub(super) fn upsert_agent_state_from_role(
|
|
|
240
442
|
.unwrap_or(Provider::Codex);
|
|
241
443
|
persist_effective_approval_policy_from_yaml_agent(obj, meta, meta_provider);
|
|
242
444
|
}
|
|
243
|
-
|
|
244
|
-
crate::lifecycle::restart::remove::clear_agent_retirement_in_state(&mut state, agent_id);
|
|
245
|
-
save_launched_team_state_for_key(
|
|
246
|
-
workspace,
|
|
247
|
-
&state,
|
|
248
|
-
Some(canonical_team_key),
|
|
249
|
-
Some(agent_id.as_str()),
|
|
250
|
-
)
|
|
445
|
+
entry
|
|
251
446
|
}
|
|
252
447
|
|
|
253
448
|
/// ---
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
//! ---
|
|
2
|
-
//! purpose:
|
|
2
|
+
//! purpose: Pi 以完整会话快照创建独立席位;已验证的其他 provider 保持窗口内分身
|
|
3
3
|
//! contract:
|
|
4
4
|
//! provides:
|
|
5
5
|
//! - name: fork_agent_with_transport
|
|
6
|
-
//! what:
|
|
6
|
+
//! what: Pi 走锁内快照新席位;已验证的其他 provider 保持窗口内斜杠 fork
|
|
7
7
|
//! - name: in_window_fork
|
|
8
8
|
//! what: 已验证的 provider+subscription 给出 command+screen_mark;未验证返回 None
|
|
9
9
|
//! depends:
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
//! - crate::lifecycle::profile_launch
|
|
13
13
|
//! - crate::transport::Transport
|
|
14
14
|
//! boundary:
|
|
15
|
-
//! -
|
|
16
|
-
//! -
|
|
15
|
+
//! - 仅 Pi 的锁内专用管道读取 exact backing、注册并启动新席位
|
|
16
|
+
//! - 非 Pi 不 spawn 新 pane,不改 TEAM_AGENT_ID / MCP / 席位名
|
|
17
17
|
//! - 重试只重按回车,不重粘 /fork
|
|
18
18
|
//! - 不把 pane 锁超时从 200ms 调大
|
|
19
19
|
//! - 未验证的 provider 不猜斜杠命令
|
|
@@ -99,7 +99,7 @@ pub fn fork_agent_with_transport(
|
|
|
99
99
|
workspace: &Path,
|
|
100
100
|
source_agent_id: &AgentId,
|
|
101
101
|
as_agent_id: &AgentId,
|
|
102
|
-
|
|
102
|
+
label: Option<&str>,
|
|
103
103
|
_open_display: bool,
|
|
104
104
|
team: Option<&str>,
|
|
105
105
|
transport: &dyn Transport,
|
|
@@ -142,6 +142,15 @@ pub fn fork_agent_with_transport(
|
|
|
142
142
|
.unwrap_or("subscription");
|
|
143
143
|
let provider = parse_provider(provider_raw).unwrap_or(Provider::Grok);
|
|
144
144
|
let auth = parse_auth_mode(auth_raw).unwrap_or(AuthMode::Subscription);
|
|
145
|
+
if provider == Provider::Pi {
|
|
146
|
+
return crate::lifecycle::restart::fork_pi_new_seat_locked(
|
|
147
|
+
&selected,
|
|
148
|
+
source_agent_id,
|
|
149
|
+
as_agent_id,
|
|
150
|
+
label,
|
|
151
|
+
transport,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
145
154
|
let Some(spec) = in_window_fork(provider, auth) else {
|
|
146
155
|
return Err(refuse_missing_in_window_fork(provider, provider_raw));
|
|
147
156
|
};
|
|
@@ -201,6 +210,7 @@ pub fn fork_agent_with_transport(
|
|
|
201
210
|
},
|
|
202
211
|
session_id: None,
|
|
203
212
|
backing_state: ForkBackingState::Verified,
|
|
213
|
+
pi_fork: None,
|
|
204
214
|
})
|
|
205
215
|
}
|
|
206
216
|
|
|
@@ -44,11 +44,39 @@ pub fn fork_agent(
|
|
|
44
44
|
.map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
|
|
45
45
|
// Fork-agent routes to the selected live team's persisted endpoint, not
|
|
46
46
|
// the workspace-hash fallback socket.
|
|
47
|
-
let
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
let is_pi = selected
|
|
48
|
+
.state
|
|
49
|
+
.get("agents")
|
|
50
|
+
.and_then(|agents| agents.get(source_agent_id.as_str()))
|
|
51
|
+
.and_then(|agent| agent.get("provider"))
|
|
52
|
+
.and_then(serde_json::Value::as_str)
|
|
53
|
+
== Some("pi");
|
|
54
|
+
let transport: Box<dyn crate::transport::Transport> = if is_pi {
|
|
55
|
+
#[cfg(test)]
|
|
56
|
+
{
|
|
57
|
+
test_fork_transport(&selected, source_agent_id)
|
|
58
|
+
}
|
|
59
|
+
#[cfg(not(test))]
|
|
60
|
+
{
|
|
61
|
+
Box::new(
|
|
62
|
+
crate::lifecycle::restart::lifecycle_worker_tmux_backend_selection_for_state(
|
|
63
|
+
&selected.run_workspace,
|
|
64
|
+
&selected.state,
|
|
65
|
+
)?
|
|
66
|
+
.backend,
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
Box::new(
|
|
71
|
+
crate::lifecycle::restart::lifecycle_worker_tmux_backend_for_selected_state(
|
|
72
|
+
&selected.run_workspace,
|
|
73
|
+
Some(selected.team_key.as_str()),
|
|
74
|
+
)
|
|
75
|
+
.unwrap_or_else(|_| {
|
|
76
|
+
crate::tmux_backend::TmuxBackend::for_workspace(&selected.run_workspace)
|
|
77
|
+
}),
|
|
78
|
+
)
|
|
79
|
+
};
|
|
52
80
|
fork_agent_with_transport(
|
|
53
81
|
workspace,
|
|
54
82
|
source_agent_id,
|
|
@@ -56,6 +84,61 @@ pub fn fork_agent(
|
|
|
56
84
|
label,
|
|
57
85
|
open_display,
|
|
58
86
|
team,
|
|
59
|
-
|
|
87
|
+
transport.as_ref(),
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#[cfg(test)]
|
|
92
|
+
fn test_fork_transport(
|
|
93
|
+
selected: &crate::state::selector::SelectedTeam,
|
|
94
|
+
source_agent_id: &AgentId,
|
|
95
|
+
) -> Box<dyn crate::transport::Transport> {
|
|
96
|
+
let state = &selected.state;
|
|
97
|
+
let session = state
|
|
98
|
+
.get("session_name")
|
|
99
|
+
.and_then(serde_json::Value::as_str)
|
|
100
|
+
.filter(|value| !value.is_empty())
|
|
101
|
+
.map(crate::transport::SessionName::new)
|
|
102
|
+
.unwrap_or_else(|| crate::transport::SessionName::new("offline"));
|
|
103
|
+
let source = state
|
|
104
|
+
.get("agents")
|
|
105
|
+
.and_then(|agents| agents.get(source_agent_id.as_str()));
|
|
106
|
+
let targets = source
|
|
107
|
+
.and_then(|agent| agent.get("pane_id"))
|
|
108
|
+
.and_then(serde_json::Value::as_str)
|
|
109
|
+
.filter(|pane| !pane.is_empty())
|
|
110
|
+
.map(|pane| {
|
|
111
|
+
let window = source
|
|
112
|
+
.and_then(|agent| agent.get("window"))
|
|
113
|
+
.and_then(serde_json::Value::as_str)
|
|
114
|
+
.filter(|value| !value.is_empty())
|
|
115
|
+
.unwrap_or(source_agent_id.as_str());
|
|
116
|
+
vec![crate::transport::PaneInfo {
|
|
117
|
+
pane_id: crate::transport::PaneId::new(pane),
|
|
118
|
+
session: session.clone(),
|
|
119
|
+
window_index: None,
|
|
120
|
+
window_name: Some(crate::transport::WindowName::new(window)),
|
|
121
|
+
pane_index: None,
|
|
122
|
+
tty: None,
|
|
123
|
+
current_command: None,
|
|
124
|
+
current_path: None,
|
|
125
|
+
active: false,
|
|
126
|
+
pane_pid: None,
|
|
127
|
+
leader_env: std::collections::BTreeMap::new(),
|
|
128
|
+
}]
|
|
129
|
+
})
|
|
130
|
+
.unwrap_or_default();
|
|
131
|
+
let endpoint = state
|
|
132
|
+
.get("tmux_endpoint")
|
|
133
|
+
.or_else(|| state.get("tmux_socket"))
|
|
134
|
+
.and_then(serde_json::Value::as_str)
|
|
135
|
+
.filter(|value| !value.is_empty())
|
|
136
|
+
.map(str::to_string)
|
|
137
|
+
.unwrap_or_else(|| "offline".to_string());
|
|
138
|
+
Box::new(
|
|
139
|
+
crate::transport::test_support::OfflineTransport::new()
|
|
140
|
+
.with_session_present(true)
|
|
141
|
+
.with_targets(targets)
|
|
142
|
+
.with_tmux_endpoint(endpoint),
|
|
60
143
|
)
|
|
61
144
|
}
|