@team-agent/installer 0.5.59 → 0.5.61
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/adapters.rs +44 -1
- package/crates/team-agent/src/cli/diagnose.rs +183 -0
- package/crates/team-agent/src/cli/emit.rs +63 -14
- package/crates/team-agent/src/cli/leader.rs +8 -1
- package/crates/team-agent/src/cli/mod.rs +263 -5
- package/crates/team-agent/src/cli/named_address.rs +18 -1
- package/crates/team-agent/src/cli/send/presentation.rs +2 -0
- package/crates/team-agent/src/cli/spec.rs +4 -1
- package/crates/team-agent/src/cli/status_port/store.rs +7 -3
- package/crates/team-agent/src/cli/tests/named_address.rs +65 -0
- package/crates/team-agent/src/cli/types.rs +33 -0
- package/crates/team-agent/src/communication_mode/mod.rs +53 -0
- package/crates/team-agent/src/compiler/tests.rs +5 -5
- package/crates/team-agent/src/compiler.rs +53 -1
- package/crates/team-agent/src/coordinator/tick.rs +21 -8
- package/crates/team-agent/src/db/message_store.rs +45 -1
- package/crates/team-agent/src/fake_worker.rs +21 -1
- package/crates/team-agent/src/leader/start.rs +732 -94
- package/crates/team-agent/src/leader/tests/identity.rs +64 -57
- package/crates/team-agent/src/leader/tests/identity_session_names.rs +42 -0
- package/crates/team-agent/src/lib.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +58 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +57 -51
- package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +148 -8
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +5 -0
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +35 -1
- package/crates/team-agent/src/lifecycle/types.rs +1 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +29 -11
- package/crates/team-agent/src/mcp_server/normalize.rs +1 -0
- package/crates/team-agent/src/mcp_server/tests/send.rs +26 -0
- package/crates/team-agent/src/mcp_server/tests/wire.rs +1 -1
- package/crates/team-agent/src/mcp_server/tools.rs +100 -66
- package/crates/team-agent/src/mcp_server/types.rs +5 -0
- package/crates/team-agent/src/mcp_server/wire.rs +27 -21
- package/crates/team-agent/src/messaging/delivery.rs +105 -38
- package/crates/team-agent/src/messaging/leader_channel.rs +33 -10
- package/crates/team-agent/src/messaging/leader_receiver.rs +40 -23
- package/crates/team-agent/src/messaging/presentation.rs +109 -0
- package/crates/team-agent/src/messaging/results.rs +165 -17
- package/crates/team-agent/src/messaging/send.rs +94 -81
- package/crates/team-agent/src/messaging/tests/leader_channel.rs +6 -6
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +187 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +13 -6
- package/crates/team-agent/src/messaging/types.rs +5 -0
- package/crates/team-agent/src/messaging/watchers.rs +5 -0
- package/crates/team-agent/src/model/mod.rs +1 -0
- package/crates/team-agent/src/model/pane_authority_refusal.rs +358 -0
- package/crates/team-agent/src/model/spec.rs +16 -0
- package/crates/team-agent/src/provider/adapter.rs +57 -0
- package/crates/team-agent/src/provider/adapters/claude_fork.rs +10 -2
- package/crates/team-agent/src/provider/session/capture.rs +69 -34
- package/crates/team-agent/src/provider/session/context_fork/codex.rs +386 -9
- package/crates/team-agent/src/provider/session/context_fork/outcome.rs +3 -14
- package/crates/team-agent/src/provider/session/context_fork.rs +7 -2
- package/crates/team-agent/src/provider/session/mod.rs +3 -2
- package/crates/team-agent/src/provider/session_scan/claude.rs +92 -3
- package/crates/team-agent/src/provider/session_scan/codex.rs +28 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +34 -8
- package/crates/team-agent/src/provider/session_scan.rs +8 -0
- package/crates/team-agent/src/topology.rs +3 -0
- package/package.json +4 -4
- package/skills/team-agent/command-coverage.json +379 -0
|
@@ -130,7 +130,7 @@ fn leader_start_plan_external_leader_keeps_exec_provider_in_tmux() {
|
|
|
130
130
|
std::fs::create_dir_all(&ws).unwrap();
|
|
131
131
|
|
|
132
132
|
let provider_args = vec!["--".to_string(), "--model".to_string(), "opus".to_string()];
|
|
133
|
-
let plan =
|
|
133
|
+
let plan = crate::leader::start::leader_start_plan_after_ambient_authority_check(
|
|
134
134
|
Provider::Fake,
|
|
135
135
|
&provider_args,
|
|
136
136
|
&ws,
|
|
@@ -202,7 +202,7 @@ fn managed_leader_uses_dedicated_leader_session_independent_of_state_session_nam
|
|
|
202
202
|
|
|
203
203
|
#[test]
|
|
204
204
|
#[serial_test::serial(env)]
|
|
205
|
-
fn
|
|
205
|
+
fn in_tmux_default_leader_uses_managed_client_and_external_opt_out_keeps_exec_provider() {
|
|
206
206
|
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
207
207
|
let ws = std::env::temp_dir().join(format!("ta_rs_lsp_switch_{}", std::process::id()));
|
|
208
208
|
std::fs::create_dir_all(&ws).unwrap();
|
|
@@ -210,26 +210,65 @@ fn in_tmux_default_leader_runs_provider_in_current_pane() {
|
|
|
210
210
|
let endpoint = format!("/private/tmp/tmux-501/{socket},88432,187");
|
|
211
211
|
let _e = EnvGuard::apply(&[("TMUX", Some(&endpoint)), ("TMUX_PANE", Some("%7"))]);
|
|
212
212
|
|
|
213
|
-
let
|
|
213
|
+
let managed = crate::leader::start::leader_start_plan_after_ambient_authority_check(
|
|
214
|
+
Provider::Fake,
|
|
215
|
+
&[],
|
|
216
|
+
&ws,
|
|
217
|
+
false,
|
|
218
|
+
false,
|
|
219
|
+
None,
|
|
220
|
+
false,
|
|
221
|
+
)
|
|
222
|
+
.unwrap();
|
|
214
223
|
|
|
215
|
-
assert_eq!(
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
224
|
+
assert_eq!(
|
|
225
|
+
managed.mode,
|
|
226
|
+
LeaderStartMode::ManagedTmuxClient,
|
|
227
|
+
"N41 managed default: an in-tmux caller on the workspace server must use the managed client path"
|
|
228
|
+
);
|
|
229
|
+
assert!(!managed.is_external_leader);
|
|
230
|
+
assert!(managed.session_name.is_some());
|
|
231
|
+
assert_eq!(
|
|
232
|
+
managed.leader_window.as_ref().map(WindowName::as_str),
|
|
233
|
+
Some("fake")
|
|
234
|
+
);
|
|
220
235
|
assert!(
|
|
221
|
-
|
|
236
|
+
managed.argv.iter().any(|arg| arg == "switch-client")
|
|
237
|
+
&& !managed.argv.iter().any(|arg| arg == "attach-session"),
|
|
238
|
+
"N41 managed default: same-server dispatch must switch the existing client, not nested-attach: {:?}",
|
|
239
|
+
managed.argv
|
|
240
|
+
);
|
|
241
|
+
assert_eq!(managed.provider_argv, vec!["fake".to_string()]);
|
|
242
|
+
|
|
243
|
+
let external = crate::leader::start::leader_start_plan_after_ambient_authority_check(
|
|
244
|
+
Provider::Fake,
|
|
245
|
+
&[],
|
|
246
|
+
&ws,
|
|
247
|
+
false,
|
|
248
|
+
false,
|
|
249
|
+
None,
|
|
250
|
+
true,
|
|
251
|
+
)
|
|
252
|
+
.unwrap();
|
|
253
|
+
|
|
254
|
+
assert_eq!(external.mode, LeaderStartMode::ExecProvider);
|
|
255
|
+
assert!(external.is_external_leader);
|
|
256
|
+
assert_eq!(external.leader_window, None);
|
|
257
|
+
assert_eq!(external.argv, vec!["fake".to_string()]);
|
|
258
|
+
assert_eq!(external.provider_argv, vec!["fake".to_string()]);
|
|
259
|
+
assert!(
|
|
260
|
+
!external
|
|
222
261
|
.argv
|
|
223
262
|
.iter()
|
|
224
263
|
.any(|arg| arg == "switch-client" || arg == "attach-session"),
|
|
225
|
-
"
|
|
226
|
-
|
|
264
|
+
"N41 external opt-out: explicit external topology must remain a direct provider path: {:?}",
|
|
265
|
+
external.argv
|
|
227
266
|
);
|
|
228
267
|
}
|
|
229
268
|
|
|
230
269
|
#[test]
|
|
231
270
|
#[serial_test::serial(env)]
|
|
232
|
-
fn
|
|
271
|
+
fn in_tmux_external_opt_out_keeps_exec_provider_across_server_boundary() {
|
|
233
272
|
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
234
273
|
let _e = EnvGuard::apply(&[
|
|
235
274
|
("TMUX", Some("/private/tmp/tmux-501/default,88432,187")),
|
|
@@ -238,12 +277,21 @@ fn in_tmux_default_leader_does_not_refuse_different_tmux_server() {
|
|
|
238
277
|
let ws = std::env::temp_dir().join(format!("ta_rs_lsp_refuse_{}", std::process::id()));
|
|
239
278
|
std::fs::create_dir_all(&ws).unwrap();
|
|
240
279
|
|
|
241
|
-
let plan =
|
|
280
|
+
let plan = crate::leader::start::leader_start_plan_after_ambient_authority_check(
|
|
281
|
+
Provider::Fake,
|
|
282
|
+
&[],
|
|
283
|
+
&ws,
|
|
284
|
+
false,
|
|
285
|
+
false,
|
|
286
|
+
None,
|
|
287
|
+
true,
|
|
288
|
+
)
|
|
289
|
+
.unwrap();
|
|
242
290
|
|
|
243
291
|
assert_eq!(plan.mode, LeaderStartMode::ExecProvider);
|
|
244
|
-
assert!(
|
|
245
|
-
assert!(plan.session_name.is_none());
|
|
292
|
+
assert!(plan.is_external_leader);
|
|
246
293
|
assert_eq!(plan.argv, vec!["fake".to_string()]);
|
|
294
|
+
assert_eq!(plan.provider_argv, vec!["fake".to_string()]);
|
|
247
295
|
}
|
|
248
296
|
|
|
249
297
|
#[test]
|
|
@@ -420,45 +468,4 @@ fn p2_leader_detect_divergence_catches_owner_uuid_split() {
|
|
|
420
468
|
// refactor must also update these tests.
|
|
421
469
|
// ═══════════════════════════════════════════════════════════════════════
|
|
422
470
|
|
|
423
|
-
|
|
424
|
-
fn unit0_leader_session_prefix_constant_is_stable() {
|
|
425
|
-
// Pinned: the literal leader session prefix that gates "is this a
|
|
426
|
-
// leader session" decisions everywhere in the runtime.
|
|
427
|
-
assert_eq!(
|
|
428
|
-
crate::leader::start::LEADER_SESSION_PREFIX,
|
|
429
|
-
"team-agent-leader-"
|
|
430
|
-
);
|
|
431
|
-
// The layout module re-exports the same value (kept in sync via
|
|
432
|
-
// const re-export).
|
|
433
|
-
assert_eq!(
|
|
434
|
-
crate::layout::sessions::LEADER_SESSION_PREFIX,
|
|
435
|
-
crate::leader::start::LEADER_SESSION_PREFIX
|
|
436
|
-
);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
#[test]
|
|
440
|
-
fn unit0_leader_prefixed_name_must_never_be_taken_as_worker_session() {
|
|
441
|
-
// Pinned invariant fed into unit-1 typed identity: any session
|
|
442
|
-
// name starting with `LEADER_SESSION_PREFIX` is a leader launcher
|
|
443
|
-
// session, never a worker session. unit-1 will wrap this rule in
|
|
444
|
-
// typed constructors (`WorkerSession::new` rejects the prefix,
|
|
445
|
-
// `LeaderLauncherSession::new` requires it).
|
|
446
|
-
let leader = "team-agent-leader-claude-x-aaaaaaaaaaaa";
|
|
447
|
-
let worker = "team-real-worker";
|
|
448
|
-
let prefix = crate::leader::start::LEADER_SESSION_PREFIX;
|
|
449
|
-
assert!(leader.starts_with(prefix));
|
|
450
|
-
assert!(!worker.starts_with(prefix));
|
|
451
|
-
// Symmetry: the runtime today uses this exact check
|
|
452
|
-
// (cli/mod.rs:261 `starts_with(LEADER_SESSION_PREFIX)`) to spare
|
|
453
|
-
// leader sessions during shutdown.
|
|
454
|
-
let leader_name = crate::transport::SessionName::new(leader);
|
|
455
|
-
let worker_name = crate::transport::SessionName::new(worker);
|
|
456
|
-
assert!(
|
|
457
|
-
crate::layout::sessions::is_leader_session(&leader_name),
|
|
458
|
-
"is_leader_session must accept a leader-prefixed session",
|
|
459
|
-
);
|
|
460
|
-
assert!(
|
|
461
|
-
!crate::layout::sessions::is_leader_session(&worker_name),
|
|
462
|
-
"is_leader_session must reject a non-prefixed session",
|
|
463
|
-
);
|
|
464
|
-
}
|
|
471
|
+
include!("identity_session_names.rs");
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#[test]
|
|
2
|
+
fn unit0_leader_session_prefix_constant_is_stable() {
|
|
3
|
+
// Pinned: the literal leader session prefix that gates "is this a
|
|
4
|
+
// leader session" decisions everywhere in the runtime.
|
|
5
|
+
assert_eq!(
|
|
6
|
+
crate::leader::start::LEADER_SESSION_PREFIX,
|
|
7
|
+
"team-agent-leader-"
|
|
8
|
+
);
|
|
9
|
+
// The layout module re-exports the same value (kept in sync via
|
|
10
|
+
// const re-export).
|
|
11
|
+
assert_eq!(
|
|
12
|
+
crate::layout::sessions::LEADER_SESSION_PREFIX,
|
|
13
|
+
crate::leader::start::LEADER_SESSION_PREFIX
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#[test]
|
|
18
|
+
fn unit0_leader_prefixed_name_must_never_be_taken_as_worker_session() {
|
|
19
|
+
// Pinned invariant fed into unit-1 typed identity: any session
|
|
20
|
+
// name starting with `LEADER_SESSION_PREFIX` is a leader launcher
|
|
21
|
+
// session, never a worker session. unit-1 will wrap this rule in
|
|
22
|
+
// typed constructors (`WorkerSession::new` rejects the prefix,
|
|
23
|
+
// `LeaderLauncherSession::new` requires it).
|
|
24
|
+
let leader = "team-agent-leader-claude-x-aaaaaaaaaaaa";
|
|
25
|
+
let worker = "team-real-worker";
|
|
26
|
+
let prefix = crate::leader::start::LEADER_SESSION_PREFIX;
|
|
27
|
+
assert!(leader.starts_with(prefix));
|
|
28
|
+
assert!(!worker.starts_with(prefix));
|
|
29
|
+
// Symmetry: the runtime today uses this exact check
|
|
30
|
+
// (cli/mod.rs:261 `starts_with(LEADER_SESSION_PREFIX)`) to spare
|
|
31
|
+
// leader sessions during shutdown.
|
|
32
|
+
let leader_name = crate::transport::SessionName::new(leader);
|
|
33
|
+
let worker_name = crate::transport::SessionName::new(worker);
|
|
34
|
+
assert!(
|
|
35
|
+
crate::layout::sessions::is_leader_session(&leader_name),
|
|
36
|
+
"is_leader_session must accept a leader-prefixed session",
|
|
37
|
+
);
|
|
38
|
+
assert!(
|
|
39
|
+
!crate::layout::sessions::is_leader_session(&worker_name),
|
|
40
|
+
"is_leader_session must reject a non-prefixed session",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -50,6 +50,10 @@ pub mod state;
|
|
|
50
50
|
// step 6 (compiler) — TEAM.md + agents/*.md → 规范 team.spec dict(doc→spec 纯变换)。
|
|
51
51
|
pub mod compiler;
|
|
52
52
|
|
|
53
|
+
// Official communication-mode catalog. Configuration compilation and every
|
|
54
|
+
// future projection consumer must use this typed single source of truth.
|
|
55
|
+
pub mod communication_mode;
|
|
56
|
+
|
|
53
57
|
// step 7 (message_store) — team.db 上的核心消息生命周期(create/claim/mark/通知去重)。
|
|
54
58
|
// unit-10 (Stage 4) compat shim. Physical home is now `crate::db::message_store`.
|
|
55
59
|
pub use crate::db::message_store;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
use super::super::*;
|
|
2
|
+
|
|
3
|
+
pub(super) struct CompleteForkInput<'a> {
|
|
4
|
+
pub workspace: &'a Path,
|
|
5
|
+
pub team_key: &'a str,
|
|
6
|
+
pub agent_id: &'a AgentId,
|
|
7
|
+
pub spawn: &'a crate::transport::SpawnResult,
|
|
8
|
+
pub window: &'a WindowName,
|
|
9
|
+
pub transport: &'a dyn Transport,
|
|
10
|
+
pub session_name: &'a SessionName,
|
|
11
|
+
pub mcp_config_path: &'a Path,
|
|
12
|
+
pub profile_launch: &'a crate::provider::ProviderProfileLaunch,
|
|
13
|
+
pub materialized_role: &'a mut MaterializedRole,
|
|
14
|
+
pub claude_fork:
|
|
15
|
+
&'a mut Option<crate::provider::adapters::claude_fork::ClaudeForkMaterialization>,
|
|
16
|
+
pub copilot_fork:
|
|
17
|
+
&'a mut Option<crate::provider::adapters::copilot_fork::CopilotForkMaterialization>,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
pub(super) fn complete_fork(input: CompleteForkInput<'_>) -> Result<bool, LifecycleError> {
|
|
21
|
+
if let Err(error) = verify_fork_registration(
|
|
22
|
+
input.workspace,
|
|
23
|
+
input.team_key,
|
|
24
|
+
input.agent_id,
|
|
25
|
+
input.spawn,
|
|
26
|
+
input.window,
|
|
27
|
+
) {
|
|
28
|
+
rollback_fork_after_spawn(
|
|
29
|
+
input.workspace,
|
|
30
|
+
input.transport,
|
|
31
|
+
input.session_name,
|
|
32
|
+
input.window,
|
|
33
|
+
input.mcp_config_path,
|
|
34
|
+
input.agent_id,
|
|
35
|
+
input.profile_launch,
|
|
36
|
+
input.team_key,
|
|
37
|
+
);
|
|
38
|
+
return Err(error);
|
|
39
|
+
}
|
|
40
|
+
let coordinator_started = start_fork_coordinator(ForkCoordinatorInput {
|
|
41
|
+
workspace: input.workspace,
|
|
42
|
+
team_key: input.team_key,
|
|
43
|
+
agent_id: input.agent_id,
|
|
44
|
+
transport: input.transport,
|
|
45
|
+
session_name: input.session_name,
|
|
46
|
+
window: input.window,
|
|
47
|
+
mcp_config_path: input.mcp_config_path,
|
|
48
|
+
profile_launch: input.profile_launch,
|
|
49
|
+
})?;
|
|
50
|
+
input.materialized_role.keep();
|
|
51
|
+
if let Some(materialized) = input.claude_fork.as_mut() {
|
|
52
|
+
materialized.keep();
|
|
53
|
+
}
|
|
54
|
+
if let Some(materialized) = input.copilot_fork.as_mut() {
|
|
55
|
+
materialized.keep();
|
|
56
|
+
}
|
|
57
|
+
Ok(coordinator_started)
|
|
58
|
+
}
|
|
@@ -1,16 +1,7 @@
|
|
|
1
1
|
use super::*;
|
|
2
|
-
use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest};
|
|
3
2
|
use crate::lifecycle::profile_launch::parse_provider;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
use crate::model::ids::AgentId;
|
|
7
|
-
use crate::model::permissions::{self, AgentPermissionInput};
|
|
8
|
-
use crate::model::yaml::{self, Value};
|
|
9
|
-
use crate::state::persist::load_runtime_state;
|
|
10
|
-
use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
|
|
11
|
-
use std::collections::{BTreeMap, BTreeSet};
|
|
12
|
-
use std::path::{Path, PathBuf};
|
|
13
|
-
use std::process::Command;
|
|
3
|
+
|
|
4
|
+
mod completion;
|
|
14
5
|
|
|
15
6
|
pub fn fork_agent_with_transport(
|
|
16
7
|
workspace: &Path,
|
|
@@ -135,7 +126,7 @@ pub fn fork_agent_with_transport(
|
|
|
135
126
|
new_agent,
|
|
136
127
|
Some(as_agent_id.as_str()),
|
|
137
128
|
provider,
|
|
138
|
-
)
|
|
129
|
+
)?;
|
|
139
130
|
let system_prompt =
|
|
140
131
|
crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
|
|
141
132
|
let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
|
|
@@ -239,6 +230,23 @@ pub fn fork_agent_with_transport(
|
|
|
239
230
|
as_agent_id.as_str(),
|
|
240
231
|
Some(&fork_team),
|
|
241
232
|
);
|
|
233
|
+
let spawned_at = spawn_timestamp_for_agent(1);
|
|
234
|
+
let mut fork_backing = adapter
|
|
235
|
+
.materialize_fork_backing(
|
|
236
|
+
&source_backing,
|
|
237
|
+
&session_id,
|
|
238
|
+
source_agent_id.as_str(),
|
|
239
|
+
as_agent_id.as_str(),
|
|
240
|
+
&mut plan,
|
|
241
|
+
)
|
|
242
|
+
.map_err(|error| {
|
|
243
|
+
let _ = std::fs::write(&spec_path, text.as_bytes());
|
|
244
|
+
cleanup_fork_mcp_artifacts(&workspace, as_agent_id, &mcp_config_path, &profile_launch);
|
|
245
|
+
LifecycleError::Provider(error.to_string())
|
|
246
|
+
})?;
|
|
247
|
+
let mut expected_backing_path = fork_backing
|
|
248
|
+
.as_ref()
|
|
249
|
+
.map(|materialized| materialized.path().to_path_buf());
|
|
242
250
|
if matches!(provider, Provider::Claude | Provider::ClaudeCode) {
|
|
243
251
|
plan.provider_projects_root = source_backing.parent().map(Path::to_path_buf);
|
|
244
252
|
}
|
|
@@ -248,12 +256,19 @@ pub fn fork_agent_with_transport(
|
|
|
248
256
|
&plan,
|
|
249
257
|
&source_backing,
|
|
250
258
|
&session_id,
|
|
259
|
+
source_agent_id,
|
|
260
|
+
as_agent_id,
|
|
251
261
|
)
|
|
252
262
|
.map_err(|error| {
|
|
253
263
|
let _ = std::fs::write(&spec_path, text.as_bytes());
|
|
254
264
|
cleanup_fork_mcp_artifacts(&workspace, as_agent_id, &mcp_config_path, &profile_launch);
|
|
255
265
|
error
|
|
256
266
|
})?;
|
|
267
|
+
if expected_backing_path.is_none() {
|
|
268
|
+
expected_backing_path = claude_fork
|
|
269
|
+
.as_ref()
|
|
270
|
+
.map(|materialized| materialized.path().to_path_buf());
|
|
271
|
+
}
|
|
257
272
|
// The framework-created Claude snapshot is only fork input, not provider
|
|
258
273
|
// proof. Observe changes made after materialization so a spawn-only
|
|
259
274
|
// provider cannot turn the copied source backing into a Verified result.
|
|
@@ -307,7 +322,6 @@ pub fn fork_agent_with_transport(
|
|
|
307
322
|
// Release the metadata lock before per-seat provider convergence; finalize reacquires it.
|
|
308
323
|
drop(_lock);
|
|
309
324
|
let spawn_epoch = 1_u64;
|
|
310
|
-
let spawned_at = spawn_timestamp_for_agent(u32::try_from(spawn_epoch).unwrap_or(u32::MAX));
|
|
311
325
|
let spawn_result = if session_live {
|
|
312
326
|
transport.spawn_into_with_env_unset(
|
|
313
327
|
&session_name,
|
|
@@ -343,6 +357,9 @@ pub fn fork_agent_with_transport(
|
|
|
343
357
|
return Err(LifecycleError::Transport(error.to_string()));
|
|
344
358
|
}
|
|
345
359
|
};
|
|
360
|
+
if let Some(materialized) = fork_backing.as_mut() {
|
|
361
|
+
materialized.handoff();
|
|
362
|
+
}
|
|
346
363
|
ensure_fork_spawn_live(ForkPostSpawnInput {
|
|
347
364
|
workspace: &workspace,
|
|
348
365
|
transport,
|
|
@@ -361,7 +378,8 @@ pub fn fork_agent_with_transport(
|
|
|
361
378
|
&session_id,
|
|
362
379
|
&plan,
|
|
363
380
|
&backing_before,
|
|
364
|
-
|
|
381
|
+
expected_backing_path.as_deref(),
|
|
382
|
+
source_agent_id.as_str(),
|
|
365
383
|
as_agent_id.as_str(),
|
|
366
384
|
&workspace,
|
|
367
385
|
&spawned_at,
|
|
@@ -414,7 +432,7 @@ pub fn fork_agent_with_transport(
|
|
|
414
432
|
}
|
|
415
433
|
};
|
|
416
434
|
if let Some(context_proof) = context_proof.as_ref() {
|
|
417
|
-
|
|
435
|
+
let finalized = match finalize_fork_state(ForkFinalizeInput {
|
|
418
436
|
workspace: &workspace,
|
|
419
437
|
team_key: &fork_team,
|
|
420
438
|
source_agent_id,
|
|
@@ -430,51 +448,39 @@ pub fn fork_agent_with_transport(
|
|
|
430
448
|
spawned_at: &spawned_at,
|
|
431
449
|
spawn_epoch,
|
|
432
450
|
}) {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
transport,
|
|
452
|
-
&session_name,
|
|
453
|
-
&window,
|
|
454
|
-
&mcp_config_path,
|
|
455
|
-
as_agent_id,
|
|
456
|
-
&profile_launch,
|
|
457
|
-
&fork_team,
|
|
458
|
-
);
|
|
459
|
-
return Err(error);
|
|
451
|
+
Ok(finalized) => finalized,
|
|
452
|
+
Err(error) => {
|
|
453
|
+
rollback_fork_after_spawn(
|
|
454
|
+
&workspace,
|
|
455
|
+
transport,
|
|
456
|
+
&session_name,
|
|
457
|
+
&window,
|
|
458
|
+
&mcp_config_path,
|
|
459
|
+
as_agent_id,
|
|
460
|
+
&profile_launch,
|
|
461
|
+
&fork_team,
|
|
462
|
+
);
|
|
463
|
+
return Err(error);
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
finalized
|
|
467
|
+
.write_audit(&crate::event_log::EventLog::new(&workspace))
|
|
468
|
+
.map_err(|error| LifecycleError::StatePersist(error.to_string()))?;
|
|
460
469
|
}
|
|
461
|
-
let coordinator_started =
|
|
470
|
+
let coordinator_started = completion::complete_fork(completion::CompleteForkInput {
|
|
462
471
|
workspace: &workspace,
|
|
463
472
|
team_key: &fork_team,
|
|
464
473
|
agent_id: as_agent_id,
|
|
474
|
+
spawn: &spawn,
|
|
475
|
+
window: &window,
|
|
465
476
|
transport,
|
|
466
477
|
session_name: &session_name,
|
|
467
|
-
window: &window,
|
|
468
478
|
mcp_config_path: &mcp_config_path,
|
|
469
479
|
profile_launch: &profile_launch,
|
|
480
|
+
materialized_role: &mut materialized_role,
|
|
481
|
+
claude_fork: &mut claude_fork,
|
|
482
|
+
copilot_fork: &mut copilot_fork,
|
|
470
483
|
})?;
|
|
471
|
-
materialized_role.keep();
|
|
472
|
-
if let Some(materialized) = claude_fork.as_mut() {
|
|
473
|
-
materialized.keep();
|
|
474
|
-
}
|
|
475
|
-
if let Some(materialized) = copilot_fork.as_mut() {
|
|
476
|
-
materialized.keep();
|
|
477
|
-
}
|
|
478
484
|
let backing_state = if context_proof.is_some() {
|
|
479
485
|
ForkBackingState::Verified
|
|
480
486
|
} else {
|
|
@@ -44,6 +44,8 @@ pub(super) fn prepare_claude_fork_backing(
|
|
|
44
44
|
plan: &crate::provider::CommandPlan,
|
|
45
45
|
source_backing: &Path,
|
|
46
46
|
source_session_id: &crate::provider::SessionId,
|
|
47
|
+
source_agent_id: &AgentId,
|
|
48
|
+
target_agent_id: &AgentId,
|
|
47
49
|
) -> Result<Option<crate::provider::adapters::claude_fork::ClaudeForkMaterialization>, LifecycleError>
|
|
48
50
|
{
|
|
49
51
|
if !matches!(provider, Provider::Claude | Provider::ClaudeCode) {
|
|
@@ -56,6 +58,8 @@ pub(super) fn prepare_claude_fork_backing(
|
|
|
56
58
|
source_backing,
|
|
57
59
|
source_session_id,
|
|
58
60
|
target_session_id,
|
|
61
|
+
source_agent_id.as_str(),
|
|
62
|
+
target_agent_id.as_str(),
|
|
59
63
|
)
|
|
60
64
|
.map(Some)
|
|
61
65
|
.map_err(|error| {
|
|
@@ -122,7 +126,75 @@ pub(super) struct ForkFinalizeInput<'a> {
|
|
|
122
126
|
pub spawn_epoch: u64,
|
|
123
127
|
}
|
|
124
128
|
|
|
125
|
-
|
|
129
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
130
|
+
pub(crate) struct ContextForkFinalized {
|
|
131
|
+
source_agent_id: String,
|
|
132
|
+
agent_id: String,
|
|
133
|
+
session_id: String,
|
|
134
|
+
rollout_path: String,
|
|
135
|
+
captured_via: String,
|
|
136
|
+
attribution_confidence: String,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
impl ContextForkFinalized {
|
|
140
|
+
fn new(
|
|
141
|
+
source_agent_id: &str,
|
|
142
|
+
agent_id: &str,
|
|
143
|
+
session_id: &str,
|
|
144
|
+
rollout_path: &Path,
|
|
145
|
+
captured_via: &str,
|
|
146
|
+
attribution_confidence: &str,
|
|
147
|
+
) -> Self {
|
|
148
|
+
Self {
|
|
149
|
+
source_agent_id: source_agent_id.to_string(),
|
|
150
|
+
agent_id: agent_id.to_string(),
|
|
151
|
+
session_id: session_id.to_string(),
|
|
152
|
+
rollout_path: rollout_path.to_string_lossy().to_string(),
|
|
153
|
+
captured_via: captured_via.to_string(),
|
|
154
|
+
attribution_confidence: attribution_confidence.to_string(),
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fn from_captured(
|
|
159
|
+
source_agent_id: &str,
|
|
160
|
+
agent_id: &str,
|
|
161
|
+
captured: &crate::provider::CapturedSession,
|
|
162
|
+
) -> Option<Self> {
|
|
163
|
+
let captured_via = serde_json::to_value(captured.captured_via).ok()?;
|
|
164
|
+
let attribution_confidence = serde_json::to_value(captured.attribution_confidence).ok()?;
|
|
165
|
+
Some(Self::new(
|
|
166
|
+
source_agent_id,
|
|
167
|
+
agent_id,
|
|
168
|
+
captured.session_id.as_ref()?.as_str(),
|
|
169
|
+
captured.rollout_path.as_ref()?.as_path(),
|
|
170
|
+
captured_via.as_str()?,
|
|
171
|
+
attribution_confidence.as_str()?,
|
|
172
|
+
))
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
pub(crate) fn write_audit(
|
|
176
|
+
&self,
|
|
177
|
+
event_log: &crate::event_log::EventLog,
|
|
178
|
+
) -> Result<(), crate::event_log::EventLogError> {
|
|
179
|
+
event_log
|
|
180
|
+
.write(
|
|
181
|
+
crate::lifecycle::types::event_names::CONTEXT_FORK,
|
|
182
|
+
serde_json::json!({
|
|
183
|
+
"source_agent_id": self.source_agent_id,
|
|
184
|
+
"agent_id": self.agent_id,
|
|
185
|
+
"session_id": self.session_id,
|
|
186
|
+
"rollout_path": self.rollout_path,
|
|
187
|
+
"captured_via": self.captured_via,
|
|
188
|
+
"attribution_confidence": self.attribution_confidence,
|
|
189
|
+
}),
|
|
190
|
+
)
|
|
191
|
+
.map(|_| ())
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
pub(super) fn finalize_fork_state(
|
|
196
|
+
input: ForkFinalizeInput<'_>,
|
|
197
|
+
) -> Result<ContextForkFinalized, LifecycleError> {
|
|
126
198
|
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
127
199
|
workspace: input.workspace,
|
|
128
200
|
operation: "fork-agent-finalize",
|
|
@@ -169,7 +241,15 @@ pub(super) fn finalize_fork_state(input: ForkFinalizeInput<'_>) -> Result<(), Li
|
|
|
169
241
|
},
|
|
170
242
|
&next_state,
|
|
171
243
|
)
|
|
172
|
-
.map_err(|error| LifecycleError::StatePersist(error.to_string()))
|
|
244
|
+
.map_err(|error| LifecycleError::StatePersist(error.to_string()))?;
|
|
245
|
+
Ok(ContextForkFinalized::new(
|
|
246
|
+
input.source_agent_id.as_str(),
|
|
247
|
+
input.agent_id.as_str(),
|
|
248
|
+
input.context_proof.new_session_id.as_str(),
|
|
249
|
+
&input.context_proof.backing_path,
|
|
250
|
+
&input.context_proof.captured_via,
|
|
251
|
+
&input.context_proof.attribution_confidence,
|
|
252
|
+
))
|
|
173
253
|
}
|
|
174
254
|
|
|
175
255
|
pub(super) struct ForkPendingFinalizeInput<'a> {
|
|
@@ -233,20 +313,36 @@ pub(super) fn finalize_pending_fork_state(
|
|
|
233
313
|
pub(crate) fn finalize_pending_fork_capture(
|
|
234
314
|
agent: &mut serde_json::Map<String, serde_json::Value>,
|
|
235
315
|
captured: &crate::provider::CapturedSession,
|
|
236
|
-
) ->
|
|
316
|
+
) -> Option<ContextForkFinalized> {
|
|
237
317
|
let Some(session_id) = captured.session_id.as_ref() else {
|
|
238
|
-
return
|
|
318
|
+
return None;
|
|
239
319
|
};
|
|
240
320
|
let Some(rollout_path) = captured.rollout_path.as_ref() else {
|
|
241
|
-
return
|
|
321
|
+
return None;
|
|
242
322
|
};
|
|
243
323
|
if agent
|
|
244
324
|
.get("fork_source_session_id")
|
|
245
325
|
.and_then(serde_json::Value::as_str)
|
|
246
326
|
== Some(session_id.as_str())
|
|
247
327
|
{
|
|
248
|
-
return
|
|
328
|
+
return None;
|
|
249
329
|
}
|
|
330
|
+
let finalized = ContextForkFinalized::from_captured(
|
|
331
|
+
agent
|
|
332
|
+
.get("forked_from")
|
|
333
|
+
.and_then(serde_json::Value::as_str)
|
|
334
|
+
.unwrap_or_default(),
|
|
335
|
+
agent
|
|
336
|
+
.get("agent_id")
|
|
337
|
+
.and_then(serde_json::Value::as_str)
|
|
338
|
+
.or_else(|| {
|
|
339
|
+
agent
|
|
340
|
+
.get("pending_target_agent")
|
|
341
|
+
.and_then(serde_json::Value::as_str)
|
|
342
|
+
})
|
|
343
|
+
.unwrap_or_default(),
|
|
344
|
+
captured,
|
|
345
|
+
)?;
|
|
250
346
|
agent.insert(
|
|
251
347
|
"session_id".to_string(),
|
|
252
348
|
serde_json::json!(session_id.as_str()),
|
|
@@ -269,11 +365,55 @@ pub(crate) fn finalize_pending_fork_capture(
|
|
|
269
365
|
);
|
|
270
366
|
agent.remove("_pending_session_id");
|
|
271
367
|
agent.remove("attribution_ambiguous");
|
|
272
|
-
agent.remove("fork_source_session_id");
|
|
273
368
|
agent.remove("pending_target_agent");
|
|
274
369
|
agent.remove("pending_grace_secs");
|
|
275
370
|
agent.insert("capture_state".to_string(), serde_json::json!("captured"));
|
|
276
|
-
|
|
371
|
+
Some(finalized)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
#[cfg(test)]
|
|
375
|
+
mod tests {
|
|
376
|
+
use super::*;
|
|
377
|
+
|
|
378
|
+
#[test]
|
|
379
|
+
fn context_fork_finalized_writes_catalog_event_and_canonical_fields() {
|
|
380
|
+
let workspace = std::env::temp_dir().join(format!(
|
|
381
|
+
"team-agent-context-fork-audit-{}",
|
|
382
|
+
std::process::id()
|
|
383
|
+
));
|
|
384
|
+
let _ = std::fs::remove_dir_all(&workspace);
|
|
385
|
+
std::fs::create_dir_all(&workspace).expect("create audit workspace");
|
|
386
|
+
let event_log = crate::event_log::EventLog::new(&workspace);
|
|
387
|
+
ContextForkFinalized::new(
|
|
388
|
+
"source",
|
|
389
|
+
"target",
|
|
390
|
+
"target-session",
|
|
391
|
+
Path::new("/tmp/target.jsonl"),
|
|
392
|
+
"context_fork_verified",
|
|
393
|
+
"high",
|
|
394
|
+
)
|
|
395
|
+
.write_audit(&event_log)
|
|
396
|
+
.expect("write context fork audit");
|
|
397
|
+
|
|
398
|
+
let events = event_log.tail(0).expect("read context fork audit");
|
|
399
|
+
let event = events.last().expect("context fork audit event");
|
|
400
|
+
assert_eq!(
|
|
401
|
+
event.get("event").and_then(serde_json::Value::as_str),
|
|
402
|
+
Some(crate::lifecycle::types::event_names::CONTEXT_FORK)
|
|
403
|
+
);
|
|
404
|
+
assert_eq!(
|
|
405
|
+
event
|
|
406
|
+
.get("source_agent_id")
|
|
407
|
+
.and_then(serde_json::Value::as_str),
|
|
408
|
+
Some("source")
|
|
409
|
+
);
|
|
410
|
+
assert_eq!(
|
|
411
|
+
event.get("agent_id").and_then(serde_json::Value::as_str),
|
|
412
|
+
Some("target")
|
|
413
|
+
);
|
|
414
|
+
assert!(event.get("prompt").is_none());
|
|
415
|
+
let _ = std::fs::remove_dir_all(workspace);
|
|
416
|
+
}
|
|
277
417
|
}
|
|
278
418
|
|
|
279
419
|
pub(super) fn verify_fork_registration(
|
|
@@ -354,6 +354,11 @@ pub(super) fn upsert_forked_agent_state(
|
|
|
354
354
|
"forked_from".to_string(),
|
|
355
355
|
serde_json::json!(source_agent_id.as_str()),
|
|
356
356
|
);
|
|
357
|
+
entry.insert(
|
|
358
|
+
"fork_source_session_id".to_string(),
|
|
359
|
+
serde_json::json!(context_proof.source_session_id.as_str()),
|
|
360
|
+
);
|
|
361
|
+
entry.insert("capture_state".to_string(), serde_json::json!("captured"));
|
|
357
362
|
entry.insert(
|
|
358
363
|
"dynamic_role_file".to_string(),
|
|
359
364
|
serde_json::json!(dynamic_role_file.to_string_lossy().to_string()),
|