@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
|
@@ -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()),
|
|
@@ -295,8 +295,8 @@ mod fork_state;
|
|
|
295
295
|
pub(super) use fork_state::*;
|
|
296
296
|
|
|
297
297
|
mod fork_finalize;
|
|
298
|
-
pub(crate) use fork_finalize::finalize_pending_fork_capture;
|
|
299
298
|
pub(super) use fork_finalize::*;
|
|
299
|
+
pub(crate) use fork_finalize::{finalize_pending_fork_capture, ContextForkFinalized};
|
|
300
300
|
|
|
301
301
|
mod role_source;
|
|
302
302
|
pub(super) use role_source::*;
|
|
@@ -1178,7 +1178,29 @@ pub(super) fn fork_ws(alpha_role: &str) -> PathBuf {
|
|
|
1178
1178
|
std::process::id(),
|
|
1179
1179
|
n
|
|
1180
1180
|
));
|
|
1181
|
-
std::fs::write(
|
|
1181
|
+
std::fs::write(
|
|
1182
|
+
&rollout,
|
|
1183
|
+
format!(
|
|
1184
|
+
"{}\n{}\n",
|
|
1185
|
+
json!({
|
|
1186
|
+
"type": "session_meta",
|
|
1187
|
+
"payload": {
|
|
1188
|
+
"id": "sess-a",
|
|
1189
|
+
"cwd": ws,
|
|
1190
|
+
}
|
|
1191
|
+
}),
|
|
1192
|
+
json!({
|
|
1193
|
+
"type": "response_item",
|
|
1194
|
+
"payload": {
|
|
1195
|
+
"content": [{
|
|
1196
|
+
"type": "input_text",
|
|
1197
|
+
"text": "You are Team Agent worker `alpha` with role `fixture`."
|
|
1198
|
+
}]
|
|
1199
|
+
}
|
|
1200
|
+
})
|
|
1201
|
+
),
|
|
1202
|
+
)
|
|
1203
|
+
.expect("seed identity-complete fork source rollout");
|
|
1182
1204
|
crate::state::persist::save_runtime_state(
|
|
1183
1205
|
&ws,
|
|
1184
1206
|
&json!({
|
|
@@ -1669,6 +1691,18 @@ fn lanea_fork_gate_error_text_and_spec_rollback_on_adapter_arm() {
|
|
|
1669
1691
|
fn lanea_fork_report_session_id_is_not_pane_id() {
|
|
1670
1692
|
let _home = LaneHomeGuard::enter("fork-report");
|
|
1671
1693
|
let ws = fork_ws(DELEG_ROLE_ALPHA); // codex+subscription -> native fork supported -> full success path
|
|
1694
|
+
let mut state = crate::state::persist::load_runtime_state(&ws).unwrap();
|
|
1695
|
+
let source = PathBuf::from(state["agents"]["alpha"]["rollout_path"].as_str().unwrap());
|
|
1696
|
+
let codex_root = std::env::var_os("HOME")
|
|
1697
|
+
.map(PathBuf::from)
|
|
1698
|
+
.unwrap()
|
|
1699
|
+
.join(".codex/sessions/lane-fixture");
|
|
1700
|
+
std::fs::create_dir_all(&codex_root).unwrap();
|
|
1701
|
+
let discoverable_source = codex_root.join("rollout-sess-a.jsonl");
|
|
1702
|
+
std::fs::copy(source, &discoverable_source).unwrap();
|
|
1703
|
+
state["agents"]["alpha"]["rollout_path"] =
|
|
1704
|
+
json!(discoverable_source.to_string_lossy().to_string());
|
|
1705
|
+
crate::state::persist::save_runtime_state(&ws, &state).unwrap();
|
|
1672
1706
|
let tx = LaneTransport::new("team-laneateam", &[]);
|
|
1673
1707
|
let report =
|
|
1674
1708
|
fork_agent_with_transport(&ws, &aid("alpha"), &aid("newfork"), None, false, None, &tx)
|
|
@@ -36,6 +36,7 @@ pub mod event_names {
|
|
|
36
36
|
pub const ADD_FAILED: &str = "lifecycle.add_failed";
|
|
37
37
|
pub const REMOVE_STEP_COMPLETED: &str = "lifecycle.remove_step_completed";
|
|
38
38
|
pub const REMOVE_ROLLED_BACK: &str = "lifecycle.remove_rolled_back";
|
|
39
|
+
pub const CONTEXT_FORK: &str = "context_fork";
|
|
39
40
|
// restart 决策事件(Route B audit 契约必发)。
|
|
40
41
|
pub const RESTART_RESUME_DECISION: &str = "restart.resume_decision";
|
|
41
42
|
pub const RESTART_ATOMIC_REFUSAL: &str = "restart.atomic_refusal";
|
|
@@ -336,9 +336,20 @@ pub(crate) fn normalize_tests(value: Option<&Value>) -> Vec<NormalizedTest> {
|
|
|
336
336
|
.or_else(|| obj.get("name"))
|
|
337
337
|
.or_else(|| obj.get("test"))
|
|
338
338
|
.and_then(text_of_value)?;
|
|
339
|
+
let status =
|
|
340
|
+
match normalize_token(obj.get("status").and_then(Value::as_str)).as_str() {
|
|
341
|
+
"executed" => {
|
|
342
|
+
if obj.get("exit_code").and_then(Value::as_i64) == Some(0) {
|
|
343
|
+
TestStatus::Passed
|
|
344
|
+
} else {
|
|
345
|
+
TestStatus::Failed
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
_ => normalize_test_status(obj.get("status").and_then(Value::as_str)),
|
|
349
|
+
};
|
|
339
350
|
Some(NormalizedTest {
|
|
340
351
|
command,
|
|
341
|
-
status
|
|
352
|
+
status,
|
|
342
353
|
detail: obj
|
|
343
354
|
.get("detail")
|
|
344
355
|
.or_else(|| obj.get("output"))
|
|
@@ -346,6 +357,7 @@ pub(crate) fn normalize_tests(value: Option<&Value>) -> Vec<NormalizedTest> {
|
|
|
346
357
|
.or_else(|| obj.get("stderr"))
|
|
347
358
|
.or_else(|| obj.get("summary"))
|
|
348
359
|
.or_else(|| obj.get("message"))
|
|
360
|
+
.or_else(|| obj.get("log_path"))
|
|
349
361
|
.and_then(text_of_value),
|
|
350
362
|
})
|
|
351
363
|
}
|
|
@@ -358,6 +370,46 @@ pub(crate) fn normalize_tests(value: Option<&Value>) -> Vec<NormalizedTest> {
|
|
|
358
370
|
.collect()
|
|
359
371
|
}
|
|
360
372
|
|
|
373
|
+
pub(crate) fn validate_test_evidence_schema(value: Option<&Value>) -> Result<(), String> {
|
|
374
|
+
const ALLOWED: &str = r#"allowed schema: {"status":"executed","command":string,"exit_code":integer,"log_path":string}"#;
|
|
375
|
+
for (index, item) in items_from_value(value).iter().enumerate() {
|
|
376
|
+
let Value::Object(obj) = item else {
|
|
377
|
+
continue;
|
|
378
|
+
};
|
|
379
|
+
let status = normalize_token(obj.get("status").and_then(Value::as_str));
|
|
380
|
+
if status == "executed" {
|
|
381
|
+
let valid = obj.get("command").and_then(Value::as_str).is_some()
|
|
382
|
+
&& obj.get("exit_code").and_then(Value::as_i64).is_some()
|
|
383
|
+
&& obj.get("log_path").and_then(Value::as_str).is_some();
|
|
384
|
+
if !valid {
|
|
385
|
+
return Err(format!(
|
|
386
|
+
"unsupported_test_evidence_schema at tests[{index}]; {ALLOWED}"
|
|
387
|
+
));
|
|
388
|
+
}
|
|
389
|
+
} else if !status.is_empty()
|
|
390
|
+
&& !matches!(
|
|
391
|
+
status.as_str(),
|
|
392
|
+
"passed"
|
|
393
|
+
| "pass"
|
|
394
|
+
| "ok"
|
|
395
|
+
| "success"
|
|
396
|
+
| "failed"
|
|
397
|
+
| "fail"
|
|
398
|
+
| "error"
|
|
399
|
+
| "skipped"
|
|
400
|
+
| "skip"
|
|
401
|
+
| "not_run"
|
|
402
|
+
| "notrun"
|
|
403
|
+
)
|
|
404
|
+
{
|
|
405
|
+
return Err(format!(
|
|
406
|
+
"unsupported_test_evidence_schema at tests[{index}]; {ALLOWED}"
|
|
407
|
+
));
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
Ok(())
|
|
411
|
+
}
|
|
412
|
+
|
|
361
413
|
pub(crate) fn normalize_risks(value: Option<&Value>) -> Vec<NormalizedRisk> {
|
|
362
414
|
items_from_value(value)
|
|
363
415
|
.iter()
|
|
@@ -27,7 +27,7 @@ use super::helpers::{
|
|
|
27
27
|
};
|
|
28
28
|
use super::normalize::{
|
|
29
29
|
compact_tool_result, normalize_report_envelope, normalize_result_status_observed,
|
|
30
|
-
report_result_integrity_warnings,
|
|
30
|
+
report_result_integrity_warnings, validate_test_evidence_schema,
|
|
31
31
|
};
|
|
32
32
|
use super::types::{
|
|
33
33
|
Scope, SendOutcome, ToolError, ToolErrorReason, ToolOk, ToolResult, VisiblePeers,
|
|
@@ -522,6 +522,13 @@ impl TeamOrchestratorTools {
|
|
|
522
522
|
{
|
|
523
523
|
self.note_unknown_result_status(&raw);
|
|
524
524
|
}
|
|
525
|
+
if let Err(error) = validate_test_evidence_schema(base.get("tests")) {
|
|
526
|
+
return Err(ToolError::new(
|
|
527
|
+
ToolErrorReason::InvalidToolArguments,
|
|
528
|
+
error,
|
|
529
|
+
"UnsupportedTestEvidenceSchema",
|
|
530
|
+
));
|
|
531
|
+
}
|
|
525
532
|
let normalized = normalize_report_envelope(&base);
|
|
526
533
|
if let Some(error) = normalized.presentation_error.as_deref() {
|
|
527
534
|
return Err(ToolError::new(
|
|
@@ -12,6 +12,11 @@ use super::{AuthMode, Provider};
|
|
|
12
12
|
|
|
13
13
|
pub use crate::provider::session_scan::{CaptureSessionContext, CapturedSessionCandidate};
|
|
14
14
|
|
|
15
|
+
pub trait ForkBackingMaterialization {
|
|
16
|
+
fn path(&self) -> &Path;
|
|
17
|
+
fn handoff(&mut self);
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
// ===========================================================================
|
|
16
21
|
// TRAIT: ProviderAdapter (method SIGNATURES only — 无 body)
|
|
17
22
|
// ===========================================================================
|
|
@@ -187,6 +192,24 @@ pub trait ProviderAdapter {
|
|
|
187
192
|
.map(CommandPlan::argv_only)
|
|
188
193
|
}
|
|
189
194
|
|
|
195
|
+
fn materialize_fork_backing(
|
|
196
|
+
&self,
|
|
197
|
+
source_path: &Path,
|
|
198
|
+
source_session_id: &SessionId,
|
|
199
|
+
source_agent_id: &str,
|
|
200
|
+
target_agent_id: &str,
|
|
201
|
+
plan: &mut CommandPlan,
|
|
202
|
+
) -> Result<Option<Box<dyn ForkBackingMaterialization>>, ProviderError> {
|
|
203
|
+
let _ = (
|
|
204
|
+
source_path,
|
|
205
|
+
source_session_id,
|
|
206
|
+
source_agent_id,
|
|
207
|
+
target_agent_id,
|
|
208
|
+
plan,
|
|
209
|
+
);
|
|
210
|
+
Ok(None)
|
|
211
|
+
}
|
|
212
|
+
|
|
190
213
|
/// 计算本 provider 该用的 MCP server 配置(`adapter.py` mcp_config;claude
|
|
191
214
|
/// compatible_api 走 `ensure_compatible_claude_mcp_config`)。
|
|
192
215
|
fn mcp_config(&self, auth_mode: AuthMode) -> Result<McpConfig, ProviderError>;
|
|
@@ -549,6 +572,29 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
549
572
|
}
|
|
550
573
|
}
|
|
551
574
|
|
|
575
|
+
fn materialize_fork_backing(
|
|
576
|
+
&self,
|
|
577
|
+
source_path: &Path,
|
|
578
|
+
source_session_id: &SessionId,
|
|
579
|
+
source_agent_id: &str,
|
|
580
|
+
target_agent_id: &str,
|
|
581
|
+
plan: &mut CommandPlan,
|
|
582
|
+
) -> Result<Option<Box<dyn ForkBackingMaterialization>>, ProviderError> {
|
|
583
|
+
match self.provider {
|
|
584
|
+
Provider::Codex => crate::provider::session::materialize_codex_fork(
|
|
585
|
+
source_path,
|
|
586
|
+
source_session_id,
|
|
587
|
+
source_agent_id,
|
|
588
|
+
target_agent_id,
|
|
589
|
+
plan,
|
|
590
|
+
)
|
|
591
|
+
.map(|materialized| {
|
|
592
|
+
Some(Box::new(materialized) as Box<dyn ForkBackingMaterialization>)
|
|
593
|
+
}),
|
|
594
|
+
_ => Ok(None),
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
552
598
|
fn capture_session_id(
|
|
553
599
|
&self,
|
|
554
600
|
agent_id: &str,
|
|
@@ -1198,3 +1244,14 @@ pub(crate) fn next_session_token() -> String {
|
|
|
1198
1244
|
bytes[15],
|
|
1199
1245
|
)
|
|
1200
1246
|
}
|
|
1247
|
+
|
|
1248
|
+
#[cfg(test)]
|
|
1249
|
+
mod fork_materialization_source_guard {
|
|
1250
|
+
#[test]
|
|
1251
|
+
fn lifecycle_uses_provider_neutral_fork_materialization_dispatch() {
|
|
1252
|
+
let source = include_str!("../lifecycle/launch/fork_agent.rs");
|
|
1253
|
+
assert!(source.contains(".materialize_fork_backing("));
|
|
1254
|
+
assert!(!source.contains("materialize_codex_fork"));
|
|
1255
|
+
assert!(!source.contains("codex_fork"));
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
@@ -30,6 +30,8 @@ pub(crate) fn materialize_claude_fork(
|
|
|
30
30
|
source_path: &Path,
|
|
31
31
|
source_session_id: &SessionId,
|
|
32
32
|
target_session_id: &SessionId,
|
|
33
|
+
source_agent_id: &str,
|
|
34
|
+
target_agent_id: &str,
|
|
33
35
|
) -> Result<ClaudeForkMaterialization, ProviderError> {
|
|
34
36
|
let parent = source_path.parent().ok_or_else(|| {
|
|
35
37
|
ProviderError::Io(format!(
|
|
@@ -46,7 +48,12 @@ pub(crate) fn materialize_claude_fork(
|
|
|
46
48
|
}
|
|
47
49
|
let source = std::fs::read_to_string(source_path)
|
|
48
50
|
.map_err(|error| ProviderError::Io(error.to_string()))?;
|
|
49
|
-
let rewritten = source
|
|
51
|
+
let rewritten = source
|
|
52
|
+
.replace(source_session_id.as_str(), target_session_id.as_str())
|
|
53
|
+
.replace(
|
|
54
|
+
&format!("You are Team Agent worker `{source_agent_id}`"),
|
|
55
|
+
&format!("You are Team Agent worker `{target_agent_id}`"),
|
|
56
|
+
);
|
|
50
57
|
validate_jsonl(&rewritten)?;
|
|
51
58
|
let temp = parent.join(format!(
|
|
52
59
|
".{}.tmp-{}",
|
|
@@ -108,7 +115,8 @@ mod tests {
|
|
|
108
115
|
std::fs::write(&source_path, &source).unwrap();
|
|
109
116
|
|
|
110
117
|
let mut materialized =
|
|
111
|
-
materialize_claude_fork(&source_path, &source_id, &target_id)
|
|
118
|
+
materialize_claude_fork(&source_path, &source_id, &target_id, "source", "target")
|
|
119
|
+
.unwrap();
|
|
112
120
|
let target_path = dir.join(format!("{}.jsonl", target_id.as_str()));
|
|
113
121
|
let target = std::fs::read_to_string(&target_path).unwrap();
|
|
114
122
|
assert!(!target.contains(source_id.as_str()));
|
|
@@ -29,6 +29,7 @@ pub struct CapturePassReport {
|
|
|
29
29
|
/// (coordinator tick) emits a throttled
|
|
30
30
|
/// `provider.session.transcript_missing` event for each entry.
|
|
31
31
|
pub transcript_missing: Vec<TranscriptMissing>,
|
|
32
|
+
pub(crate) context_forks: Vec<crate::lifecycle::launch::ContextForkFinalized>,
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
@@ -306,9 +307,12 @@ where
|
|
|
306
307
|
// partial candidates (no session_id or no rollout_path). When
|
|
307
308
|
// it returns false, the row stays unchanged; capture re-runs
|
|
308
309
|
// on the next coordinator tick.
|
|
309
|
-
if apply_captured_session(agent_obj, &candidate.captured) {
|
|
310
|
+
if let Some(applied) = apply_captured_session(agent_obj, &candidate.captured) {
|
|
310
311
|
report.changed = true;
|
|
311
312
|
report.assigned.push(item.agent_id);
|
|
313
|
+
if let AppliedCapture::ContextFork(context_fork) = applied {
|
|
314
|
+
report.context_forks.push(context_fork);
|
|
315
|
+
}
|
|
312
316
|
}
|
|
313
317
|
continue;
|
|
314
318
|
}
|
|
@@ -676,6 +680,17 @@ where
|
|
|
676
680
|
.and_then(Value::as_str)
|
|
677
681
|
.filter(|value| !value.is_empty())
|
|
678
682
|
.map(str::to_string);
|
|
683
|
+
let expected_session_id = agent
|
|
684
|
+
.get("_pending_session_id")
|
|
685
|
+
.and_then(Value::as_str)
|
|
686
|
+
.filter(|value| !value.is_empty())
|
|
687
|
+
.map(SessionId::new);
|
|
688
|
+
if matches!(provider, Provider::Codex)
|
|
689
|
+
&& agent.get("capture_state").and_then(Value::as_str) == Some("pending_context_fork")
|
|
690
|
+
&& expected_session_id.is_none()
|
|
691
|
+
{
|
|
692
|
+
return None;
|
|
693
|
+
}
|
|
679
694
|
Some(PendingSessionCapture {
|
|
680
695
|
agent_id: agent_id.to_string(),
|
|
681
696
|
provider,
|
|
@@ -699,15 +714,7 @@ where
|
|
|
699
714
|
.and_then(Value::as_u64)
|
|
700
715
|
.and_then(|pid| u32::try_from(pid).ok()),
|
|
701
716
|
spawned_at,
|
|
702
|
-
expected_session_id
|
|
703
|
-
None
|
|
704
|
-
} else {
|
|
705
|
-
agent
|
|
706
|
-
.get("_pending_session_id")
|
|
707
|
-
.and_then(Value::as_str)
|
|
708
|
-
.filter(|value| !value.is_empty())
|
|
709
|
-
.map(SessionId::new)
|
|
710
|
-
},
|
|
717
|
+
expected_session_id,
|
|
711
718
|
provider_projects_root: agent
|
|
712
719
|
.get("claude_projects_root")
|
|
713
720
|
.and_then(Value::as_str)
|
|
@@ -1144,6 +1151,11 @@ fn candidate_key(owner: &PendingSessionCapture, candidate: &CapturedSessionCandi
|
|
|
1144
1151
|
.join("|")
|
|
1145
1152
|
}
|
|
1146
1153
|
|
|
1154
|
+
enum AppliedCapture {
|
|
1155
|
+
Session,
|
|
1156
|
+
ContextFork(crate::lifecycle::launch::ContextForkFinalized),
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1147
1159
|
/// 0.4.6 tuple-atomic contract (audit §Capture 修改清单, line 111): writes
|
|
1148
1160
|
/// the authoritative session tuple if and only if the candidate carries
|
|
1149
1161
|
/// BOTH `session_id` and `rollout_path`. A partial candidate (e.g.
|
|
@@ -1154,15 +1166,16 @@ fn candidate_key(owner: &PendingSessionCapture, candidate: &CapturedSessionCandi
|
|
|
1154
1166
|
fn apply_captured_session(
|
|
1155
1167
|
agent_obj: &mut serde_json::Map<String, Value>,
|
|
1156
1168
|
captured: &CapturedSession,
|
|
1157
|
-
) ->
|
|
1169
|
+
) -> Option<AppliedCapture> {
|
|
1158
1170
|
if agent_obj.get("capture_state").and_then(Value::as_str) == Some("pending_context_fork") {
|
|
1159
|
-
return crate::lifecycle::finalize_pending_fork_capture(agent_obj, captured)
|
|
1171
|
+
return crate::lifecycle::finalize_pending_fork_capture(agent_obj, captured)
|
|
1172
|
+
.map(AppliedCapture::ContextFork);
|
|
1160
1173
|
}
|
|
1161
1174
|
let Some(session_id) = captured.session_id.as_ref() else {
|
|
1162
|
-
return
|
|
1175
|
+
return None;
|
|
1163
1176
|
};
|
|
1164
1177
|
let Some(rollout_path) = captured.rollout_path.as_ref() else {
|
|
1165
|
-
return
|
|
1178
|
+
return None;
|
|
1166
1179
|
};
|
|
1167
1180
|
agent_obj.insert(
|
|
1168
1181
|
"session_id".to_string(),
|
|
@@ -1192,7 +1205,7 @@ fn apply_captured_session(
|
|
|
1192
1205
|
// diagnose/status observability.
|
|
1193
1206
|
agent_obj.remove("_pending_session_id");
|
|
1194
1207
|
agent_obj.insert("capture_state".to_string(), serde_json::json!("captured"));
|
|
1195
|
-
|
|
1208
|
+
Some(AppliedCapture::Session)
|
|
1196
1209
|
}
|
|
1197
1210
|
|
|
1198
1211
|
fn claimed_provider_session_keys(
|
|
@@ -2621,7 +2634,7 @@ mod u1_tests {
|
|
|
2621
2634
|
spawn_cwd: PathBuf::from("/tmp/cwd"),
|
|
2622
2635
|
};
|
|
2623
2636
|
let written = super::apply_captured_session(&mut agent, &captured);
|
|
2624
|
-
assert!(written, "apply must
|
|
2637
|
+
assert!(written.is_some(), "apply must accept valid captured");
|
|
2625
2638
|
assert!(
|
|
2626
2639
|
agent.get("_pending_session_id").is_none(),
|
|
2627
2640
|
"S1-CAPTURE-001: _pending_session_id must be removed after capture \
|