@team-agent/installer 0.5.58 → 0.5.60
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/emit.rs +30 -2
- package/crates/team-agent/src/cli/send/mailbox.rs +68 -25
- package/crates/team-agent/src/cli/spec.rs +1 -1
- package/crates/team-agent/src/coordinator/tick.rs +21 -8
- package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +58 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +56 -50
- 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.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/mcp_server/normalize.rs +53 -1
- package/crates/team-agent/src/mcp_server/tools.rs +8 -1
- 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 +29 -16
- 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/codex.rs +28 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +11 -1
- package/crates/team-agent/src/provider/session_scan.rs +3 -0
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +1 -0
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
use super::spec::{command_spec, CommandKind, CommandTier, ALL_DISPATCH_KINDS, COMMAND_SPECS};
|
|
5
5
|
use super::*;
|
|
6
|
-
use std::io::Write as _;
|
|
6
|
+
use std::io::{ErrorKind, Write as _};
|
|
7
7
|
|
|
8
8
|
/// `emit`(`helpers.py:12-23`):`--json`→`json.dumps(indent=2, ensure_ascii=False, sort_keys=True)`;
|
|
9
9
|
/// 否则 dict 逐键 `key: value`(嵌套 dict/list 内联 compact json,`ensure_ascii=False`)、非 dict 直接 print。
|
|
@@ -77,12 +77,40 @@ pub fn run(argv: &[String], cwd: &Path) -> ExitCode {
|
|
|
77
77
|
/// Print a handler's CmdResult to stdout (emit formats json/human), then surface its exit code.
|
|
78
78
|
/// (parser.py: `print(emit(result, as_json))` then the ok→exit mapping.)
|
|
79
79
|
fn emit_result(r: CmdResult) -> ExitCode {
|
|
80
|
+
let persisted_message_id = match &r.output {
|
|
81
|
+
CmdOutput::Json(value) => value
|
|
82
|
+
.get("message_id")
|
|
83
|
+
.and_then(Value::as_str)
|
|
84
|
+
.map(ToString::to_string),
|
|
85
|
+
_ => None,
|
|
86
|
+
};
|
|
80
87
|
if let Some(text) = emit(&r.output, r.as_json) {
|
|
81
|
-
|
|
88
|
+
if let Err(error) = write_stdout_line(&text) {
|
|
89
|
+
if let Some(message_id) = persisted_message_id {
|
|
90
|
+
let stderr = std::io::stderr();
|
|
91
|
+
let mut stderr = stderr.lock();
|
|
92
|
+
let _ = writeln!(
|
|
93
|
+
stderr,
|
|
94
|
+
"stdout unavailable after durable send; persisted_message_id={message_id}"
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return if error.kind() == ErrorKind::BrokenPipe {
|
|
98
|
+
r.exit
|
|
99
|
+
} else {
|
|
100
|
+
ExitCode::Error
|
|
101
|
+
};
|
|
102
|
+
}
|
|
82
103
|
}
|
|
83
104
|
r.exit
|
|
84
105
|
}
|
|
85
106
|
|
|
107
|
+
fn write_stdout_line(text: &str) -> std::io::Result<()> {
|
|
108
|
+
let stdout = std::io::stdout();
|
|
109
|
+
let mut stdout = stdout.lock();
|
|
110
|
+
stdout.write_all(text.as_bytes())?;
|
|
111
|
+
stdout.write_all(b"\n")
|
|
112
|
+
}
|
|
113
|
+
|
|
86
114
|
fn dispatch(command: &str, args: &[String], cwd: &Path) -> Result<ExitCode, CliError> {
|
|
87
115
|
let Some(spec) = command_spec(command) else {
|
|
88
116
|
return Ok(emit_unknown_subcommand_usage(command));
|
|
@@ -37,8 +37,8 @@ pub(super) fn maybe_enqueue_offline_leader_mailbox(
|
|
|
37
37
|
Ok(s) => s,
|
|
38
38
|
Err(_) => return Ok(None),
|
|
39
39
|
};
|
|
40
|
-
let
|
|
41
|
-
if
|
|
40
|
+
let attachment_history = target_team_mailbox_history(&state, &team_key);
|
|
41
|
+
if attachment_history == MailboxAttachmentHistory::NotEligible {
|
|
42
42
|
return Ok(None);
|
|
43
43
|
}
|
|
44
44
|
let event_log = crate::event_log::EventLog::new(&target_workspace);
|
|
@@ -53,47 +53,90 @@ pub(super) fn maybe_enqueue_offline_leader_mailbox(
|
|
|
53
53
|
)
|
|
54
54
|
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
55
55
|
let message_id = outcome.message_id.clone().unwrap_or_else(|| "".to_string());
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
56
|
+
let receipt = match attachment_history {
|
|
57
|
+
MailboxAttachmentHistory::NeverAttached => json!({
|
|
58
|
+
"ok": true,
|
|
59
|
+
"status": "deferred",
|
|
60
|
+
"deferred_reason": "never_attached",
|
|
61
|
+
"message_status": "queued_until_leader_attach",
|
|
62
|
+
"channel": "leader_mailbox",
|
|
63
|
+
"delivered": false,
|
|
64
|
+
"to_name": to_name,
|
|
65
|
+
"target_workspace": target_workspace.display().to_string(),
|
|
66
|
+
"team_key": team_key,
|
|
67
|
+
"recipient": "leader",
|
|
68
|
+
"leader_attached": false,
|
|
69
|
+
"message_id": message_id,
|
|
70
|
+
}),
|
|
71
|
+
MailboxAttachmentHistory::PreviouslyAttached => json!({
|
|
72
|
+
"ok": true,
|
|
73
|
+
"status": "queued_until_leader_attach",
|
|
74
|
+
"deferred_reason": "leader_currently_unattached",
|
|
75
|
+
"message_status": "queued_until_leader_attach",
|
|
76
|
+
"channel": "leader_mailbox",
|
|
77
|
+
"delivered": false,
|
|
78
|
+
"to_name": to_name,
|
|
79
|
+
"target_workspace": target_workspace.display().to_string(),
|
|
80
|
+
"team_key": team_key,
|
|
81
|
+
"recipient": "leader",
|
|
82
|
+
"leader_attached": false,
|
|
83
|
+
"message_id": message_id,
|
|
84
|
+
}),
|
|
85
|
+
MailboxAttachmentHistory::NotEligible => unreachable!("refused before persistence"),
|
|
86
|
+
};
|
|
87
|
+
Ok(Some(receipt))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
91
|
+
enum MailboxAttachmentHistory {
|
|
92
|
+
PreviouslyAttached,
|
|
93
|
+
NeverAttached,
|
|
94
|
+
NotEligible,
|
|
69
95
|
}
|
|
70
96
|
|
|
71
|
-
/// Positive-source
|
|
97
|
+
/// Positive-source mailbox eligibility per offline-mailbox-toname-design.md §4:
|
|
72
98
|
/// - target workspace has state and the team key is present + not archived/down;
|
|
73
|
-
/// - AND
|
|
74
|
-
///
|
|
99
|
+
/// - AND a persisted session name establishes a durable replay target;
|
|
100
|
+
/// - a persisted receiver attachment timestamp distinguishes previously attached
|
|
101
|
+
/// teams from teams whose first successful leader attachment is still pending.
|
|
75
102
|
///
|
|
76
103
|
/// We deliberately do NOT poll coordinator health here — enqueuing is
|
|
77
104
|
/// safe even when the coordinator is transiently down; attach-leader
|
|
78
105
|
/// itself replays via `requeue_blocked_leader_messages` regardless.
|
|
79
|
-
|
|
106
|
+
fn target_team_mailbox_history(state: &Value, team_key: &str) -> MailboxAttachmentHistory {
|
|
80
107
|
let team = state
|
|
81
108
|
.get("teams")
|
|
82
109
|
.and_then(|v| v.as_object())
|
|
83
110
|
.and_then(|teams| teams.get(team_key));
|
|
84
111
|
let Some(team) = team else {
|
|
85
|
-
return
|
|
112
|
+
return MailboxAttachmentHistory::NotEligible;
|
|
86
113
|
};
|
|
87
114
|
let status = team
|
|
88
115
|
.get("status")
|
|
89
116
|
.and_then(|v| v.as_str())
|
|
90
117
|
.unwrap_or("alive");
|
|
91
118
|
if matches!(status, "archived" | "down" | "stopped") {
|
|
92
|
-
return
|
|
119
|
+
return MailboxAttachmentHistory::NotEligible;
|
|
93
120
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
team.get("session_name")
|
|
121
|
+
let has_session = team
|
|
122
|
+
.get("session_name")
|
|
97
123
|
.and_then(|v| v.as_str())
|
|
98
|
-
.is_some_and(|s| !s.is_empty())
|
|
124
|
+
.is_some_and(|s| !s.is_empty());
|
|
125
|
+
if !has_session {
|
|
126
|
+
return MailboxAttachmentHistory::NotEligible;
|
|
127
|
+
}
|
|
128
|
+
let previously_attached = team
|
|
129
|
+
.get("leader_receiver")
|
|
130
|
+
.and_then(Value::as_object)
|
|
131
|
+
.is_some_and(|receiver| {
|
|
132
|
+
receiver
|
|
133
|
+
.get("attached_at")
|
|
134
|
+
.and_then(Value::as_str)
|
|
135
|
+
.is_some_and(|value| !value.is_empty())
|
|
136
|
+
});
|
|
137
|
+
if previously_attached {
|
|
138
|
+
MailboxAttachmentHistory::PreviouslyAttached
|
|
139
|
+
} else {
|
|
140
|
+
MailboxAttachmentHistory::NeverAttached
|
|
141
|
+
}
|
|
99
142
|
}
|
|
@@ -155,7 +155,7 @@ pub(crate) struct CommandSpec {
|
|
|
155
155
|
#[rustfmt::skip]
|
|
156
156
|
pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
|
|
157
157
|
CommandSpec { name: "quick-start", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::Dispatch(DispatchKind::QuickStart), summary: "start or attach a team from TEAM.md", usage: "usage: team-agent quick-start [TEAMDIR] [--workspace WORKSPACE] [--name NAME] [--team-id TEAM|--team TEAM] [--yes] [--no-display] [--backend tmux|conpty] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
|
|
158
|
-
CommandSpec { name: "send", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Send), summary: "persist a message for
|
|
158
|
+
CommandSpec { name: "send", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Send), summary: "persist a message for an in-team short name or fully-qualified logical recipient", usage: "usage: team-agent send TO MESSAGE... [--workspace WORKSPACE] [--team TEAM] [--presentation-sink leader|casefile|silent --message-class CLASS [--case-id CASE]] [--json]\nTO forms are co-equal: an in-team short name (for example `team-agent send reviewer \"Review\"`) or `<workspace>::<team>/<agent>`.", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: Some("next compatibility release"), action: Some("use positional logical TO and the returned message id"), governance: None },
|
|
159
159
|
CommandSpec { name: "status", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Status), summary: "show current team status", usage: "usage: team-agent status [AGENT] [--workspace WORKSPACE] [--team TEAM] [--summary|--json] [--detail]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
160
160
|
CommandSpec { name: "collect", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Collect), summary: "collect reported results", usage: "usage: team-agent collect [--workspace WORKSPACE] [--team TEAM] [--result-file FILE] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
161
161
|
CommandSpec { name: "restart", tier: CommandTier::Core, category: CommandCategory::TeamLifecycle, kind: CommandKind::Dispatch(DispatchKind::Restart), summary: "restart the selected team", usage: "usage: team-agent restart [WORKSPACE] [--team TEAM] [--allow-fresh] [--session-converge-deadline SECONDS] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
|
|
@@ -269,12 +269,17 @@ impl Coordinator {
|
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
self.record_step("capture_missing");
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
272
|
+
let pending_context_fork_audits =
|
|
273
|
+
match self.capture_missing_sessions(&mut state, &event_log) {
|
|
274
|
+
Ok(audits) => audits,
|
|
275
|
+
Err(error) => {
|
|
276
|
+
let _ = event_log.write(
|
|
277
|
+
"coordinator.tick.capture_missing_failed",
|
|
278
|
+
serde_json::json!({"error": error.to_string()}),
|
|
279
|
+
);
|
|
280
|
+
Vec::new()
|
|
281
|
+
}
|
|
282
|
+
};
|
|
278
283
|
|
|
279
284
|
// Slice 1 energy gate: one pane snapshot per tick feeds probe eligibility,
|
|
280
285
|
// health sync, and abnormal-exit detection. Missing panes are filtered
|
|
@@ -454,6 +459,14 @@ impl Coordinator {
|
|
|
454
459
|
collections,
|
|
455
460
|
));
|
|
456
461
|
}
|
|
462
|
+
for context_fork in &pending_context_fork_audits {
|
|
463
|
+
context_fork.write_audit(&event_log).map_err(|error| {
|
|
464
|
+
eprintln!(
|
|
465
|
+
"[coordinator] context_fork audit publish failed after state commit: {error}"
|
|
466
|
+
);
|
|
467
|
+
TickError::EventLog(error)
|
|
468
|
+
})?;
|
|
469
|
+
}
|
|
457
470
|
|
|
458
471
|
// 0.5.36 (`.team/artifacts/supermarket-api-error-recovery-locate.md` §7.3):
|
|
459
472
|
// post-save recovery step. Reloads fresh state, consumes due
|
|
@@ -487,7 +500,7 @@ impl Coordinator {
|
|
|
487
500
|
&self,
|
|
488
501
|
state: &mut Value,
|
|
489
502
|
event_log: &EventLog,
|
|
490
|
-
) -> Result<
|
|
503
|
+
) -> Result<Vec<crate::lifecycle::launch::ContextForkFinalized>, TickError> {
|
|
491
504
|
let report = crate::session_capture::capture_missing_provider_sessions_once(
|
|
492
505
|
state,
|
|
493
506
|
&mut |provider| self.provider_registry.adapter_for(provider),
|
|
@@ -651,7 +664,7 @@ impl Coordinator {
|
|
|
651
664
|
}),
|
|
652
665
|
)?;
|
|
653
666
|
}
|
|
654
|
-
Ok(
|
|
667
|
+
Ok(report.context_forks)
|
|
655
668
|
}
|
|
656
669
|
|
|
657
670
|
fn sync_agent_health(
|
|
@@ -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,
|
|
@@ -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 {
|