@team-agent/installer 0.5.99 → 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.
Files changed (47) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +1 -156
  4. package/crates/team-agent/src/cli/diagnose.rs +32 -10
  5. package/crates/team-agent/src/cli/emit.rs +6 -185
  6. package/crates/team-agent/src/cli/mod.rs +32 -182
  7. package/crates/team-agent/src/cli/send/coordinator.rs +2 -2
  8. package/crates/team-agent/src/cli/spec.rs +41 -67
  9. package/crates/team-agent/src/cli/tests/main_preserved.rs +2 -185
  10. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +9 -9
  11. package/crates/team-agent/src/cli/triage.rs +1 -1
  12. package/crates/team-agent/src/cli/types.rs +0 -49
  13. package/crates/team-agent/src/compiler.rs +3 -0
  14. package/crates/team-agent/src/leader/lease.rs +1 -1
  15. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +211 -16
  16. package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +1 -0
  17. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +15 -5
  18. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +89 -6
  19. package/crates/team-agent/src/lifecycle/launch/role_source.rs +168 -32
  20. package/crates/team-agent/src/lifecycle/launch.rs +11 -3
  21. package/crates/team-agent/src/lifecycle/profile_launch.rs +23 -8
  22. package/crates/team-agent/src/lifecycle/restart/agent.rs +2 -2
  23. package/crates/team-agent/src/lifecycle/restart/avatar.rs +1806 -0
  24. package/crates/team-agent/src/lifecycle/restart/common.rs +114 -1
  25. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +14 -8
  26. package/crates/team-agent/src/lifecycle/restart/selection.rs +14 -2
  27. package/crates/team-agent/src/lifecycle/restart.rs +11 -4
  28. package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +2 -2
  29. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +1 -1
  30. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +9 -2
  31. package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +1398 -0
  32. package/crates/team-agent/src/lifecycle/tests/realmachine_clusters_1_6_red.rs +0 -60
  33. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +2 -2
  34. package/crates/team-agent/src/lifecycle/tests.rs +1 -0
  35. package/crates/team-agent/src/lifecycle/types.rs +10 -2
  36. package/crates/team-agent/src/messaging/send.rs +2 -2
  37. package/crates/team-agent/src/model/spec.rs +1 -0
  38. package/crates/team-agent/src/provider/types.rs +3 -1
  39. package/crates/team-agent/src/state/persist.rs +58 -2
  40. package/crates/team-agent/src/state/projection.rs +5 -1
  41. package/crates/team-agent/src/state/repository.rs +125 -9
  42. package/crates/team-agent/src/transport/test_support.rs +23 -3
  43. package/package.json +4 -4
  44. package/skills/team-agent/SKILL.md +17 -46
  45. package/skills/team-agent/references/recovery-runbook.md +2 -2
  46. package/skills/team-agent/references/team-agent-operator.md +4 -2
  47. package/skills/team-agent/references/team-in-team.md +0 -6
@@ -272,66 +272,6 @@ fn c1_runtime_commands_resolve_active_team_for_status_send_and_collect() {
272
272
  );
273
273
  }
274
274
 
275
- #[test]
276
- #[ignore = "real-machine: command-file gate uses real team-agent binary/lifecycle"]
277
- fn c2_stop_team_level_command_exists_and_keeps_state_while_stopping_runtime() {
278
- let ws = tmp_dir("c2-stop");
279
- seed_runtime_state(&ws);
280
-
281
- let out = run(
282
- &["stop", "--workspace", ws.to_str().unwrap(), "--json"],
283
- &ws,
284
- );
285
- let err = String::from_utf8_lossy(&out.stderr);
286
- assert!(
287
- !err.contains("invalid choice: 'stop'"),
288
- "CR-005: canonical `team-agent stop --workspace <ws> --json` must be a registered team-level stop verb, not argparse invalid-choice; stderr={err:?}"
289
- );
290
- assert_eq!(
291
- out.status.code(),
292
- Some(0),
293
- "CR-005: stop should stop the runtime while keeping state on a valid workspace; stdout={} stderr={}",
294
- String::from_utf8_lossy(&out.stdout),
295
- err
296
- );
297
- let value = stdout_json(&out);
298
- assert_eq!(
299
- value["ok"],
300
- json!(true),
301
- "stop must return a successful JSON envelope"
302
- );
303
- assert!(
304
- team_agent::state::persist::runtime_state_path(&ws).exists(),
305
- "stop keeps runtime state for later restart"
306
- );
307
- }
308
-
309
- #[test]
310
- #[ignore = "real-machine: command-file gate uses real team-agent binary/lifecycle"]
311
- fn c2_canonical_verbs_expose_help_instead_of_invalid_choice() {
312
- let cwd = tmp_dir("c2-help-verbs");
313
- let mut failures = Vec::new();
314
- for verb in ["start", "restart-agent", "purge-agent"] {
315
- let out = run(&[verb, "--help"], &cwd);
316
- let stdout = String::from_utf8_lossy(&out.stdout);
317
- let stderr = String::from_utf8_lossy(&out.stderr);
318
- if out.status.code() != Some(0)
319
- || stderr.contains("invalid choice:")
320
- || !(stdout.contains("usage") || stderr.contains("usage"))
321
- {
322
- failures.push(format!(
323
- "{verb}: code={:?} stdout={stdout:?} stderr={stderr:?}",
324
- out.status.code()
325
- ));
326
- }
327
- }
328
- assert!(
329
- failures.is_empty(),
330
- "CR-030/032/035/063: canonical lifecycle verbs must be registered and expose help, not invalid-choice:\n{}",
331
- failures.join("\n")
332
- );
333
- }
334
-
335
275
  #[test]
336
276
  #[ignore = "real-machine: command-file gate uses real team-agent binary/lifecycle"]
337
277
  fn c2_quick_start_help_is_zero_token_help_only_and_does_not_compile_workspace() {
@@ -80,7 +80,7 @@ fn quick_start_workspace_override_is_not_dropped_before_runtime_state_write() {
80
80
  let quick_start_args = source_section(
81
81
  &cli_types,
82
82
  "pub struct QuickStartArgs",
83
- "pub struct InitArgs",
83
+ "pub struct SendArgs",
84
84
  );
85
85
  assert!(
86
86
  quick_start_args.contains("pub workspace: PathBuf"),
@@ -97,7 +97,7 @@ fn quick_start_workspace_override_is_not_dropped_before_runtime_state_write() {
97
97
 
98
98
  let emit =
99
99
  std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/src/cli/emit.rs")).unwrap();
100
- let quick_start_args_fn = source_section(&emit, "fn quick_start_args", "fn init_args");
100
+ let quick_start_args_fn = source_section(&emit, "fn quick_start_args", "fn resolve_cli_path");
101
101
  assert!(
102
102
  quick_start_args_fn.contains("workspace: workspace.clone()")
103
103
  || quick_start_args_fn.contains("workspace,"),
@@ -82,6 +82,7 @@ mod pi_command_red;
82
82
  mod pi_compiler_red;
83
83
  mod pi_dual_entry_red;
84
84
  mod pi_executable_mcp_red;
85
+ mod pi_fork_session_inheritance_red;
85
86
  mod pi_lifecycle_doctor_red;
86
87
  mod pi_session_delivery_red;
87
88
  mod quick_start_worker_readiness_red;
@@ -605,8 +605,8 @@ pub struct AddAgentReport {
605
605
  pub role_file: PathBuf,
606
606
  }
607
607
 
608
- /// `fork_agent(...)` 结果(`operations.py:402`)。native session fork(provider 须
609
- /// supports_session_fork ∧ auth_mode!=compatible_api)。
608
+ /// `fork_agent(...)` 结果。Pi 的 snapshot fork 附带可审计的 source/target backing 细节;
609
+ /// 已支持的其他 provider 保持既有报告字段。
610
610
  #[derive(Debug, Clone, PartialEq, Eq)]
611
611
  pub struct ForkAgentReport {
612
612
  pub source_agent_id: AgentId,
@@ -614,6 +614,14 @@ pub struct ForkAgentReport {
614
614
  pub env: AgentActionEnvelope,
615
615
  pub session_id: Option<SessionId>,
616
616
  pub backing_state: ForkBackingState,
617
+ pub pi_fork: Option<PiForkDetails>,
618
+ }
619
+
620
+ #[derive(Debug, Clone, PartialEq, Eq)]
621
+ pub struct PiForkDetails {
622
+ pub source_session_id: SessionId,
623
+ pub session_id: SessionId,
624
+ pub backing_path: RolloutPath,
617
625
  }
618
626
 
619
627
  #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
@@ -551,11 +551,11 @@ fn coordinator_unavailable_outcome(
551
551
  );
552
552
  let warning = if persist {
553
553
  format!(
554
- "coordinator is not running; message was queued for {recipient} and will retry. Run `team-agent diagnose` or restart the team."
554
+ "coordinator is not running; message was queued for {recipient} and will retry. Run `team-agent doctor` or restart the team."
555
555
  )
556
556
  } else {
557
557
  format!(
558
- "coordinator protocol or schema is incompatible; message was not queued for {recipient}. Run `team-agent diagnose` or upgrade the runtime."
558
+ "coordinator protocol or schema is incompatible; message was not queued for {recipient}. Run `team-agent doctor` or upgrade the runtime."
559
559
  )
560
560
  };
561
561
  event_log.write(
@@ -406,6 +406,7 @@ fn check_agent(agent: &Yaml, path: &str, errors: &mut Vec<String>) {
406
406
  ];
407
407
  let allowed = &[
408
408
  "id",
409
+ "label",
409
410
  "role",
410
411
  "provider",
411
412
  "model",
@@ -132,6 +132,8 @@ pub enum CaptureVia {
132
132
  FsWatch,
133
133
  FsMtimeFallback,
134
134
  FsRepair,
135
+ SessionScan,
136
+ ForkSnapshot,
135
137
  }
136
138
 
137
139
  /// attribution confidence(doc §56)。bug-085 fallback 固定 `Low`。
@@ -172,7 +174,7 @@ pub enum AuthHintStatus {
172
174
  pub enum NoPingReason {
173
175
  /// worker 从未 open 过 turn(`idle_predicate.py:61`)。
174
176
  NotArmedNoWorkerTurn,
175
- /// suppress flag 置位(acknowledge-idle 后,`idle_predicate.py:63`)。
177
+ /// Suppress flag from a legacy idle acknowledgement (`idle_predicate.py:63`).
176
178
  Acknowledged,
177
179
  /// armed 但 elapsed < debounce(`idle_predicate.py:65`)。
178
180
  DebounceActive,
@@ -232,6 +232,7 @@ pub fn save_runtime_state(workspace: &Path, state: &Value) -> Result<(), StateEr
232
232
  &[],
233
233
  None,
234
234
  None,
235
+ false,
235
236
  )
236
237
  }
237
238
 
@@ -251,6 +252,7 @@ pub(crate) fn save_runtime_state_with_receiver_authority(
251
252
  &[],
252
253
  Some(receiver_team_key),
253
254
  exact_owner_seed_to_clear,
255
+ false,
254
256
  )
255
257
  }
256
258
 
@@ -289,6 +291,7 @@ pub(crate) fn save_runtime_state_with_lifecycle_topology_authority(
289
291
  agent_ids,
290
292
  None,
291
293
  None,
294
+ false,
292
295
  )
293
296
  }
294
297
 
@@ -309,6 +312,7 @@ pub(crate) fn save_runtime_state_with_lifecycle_topology_authority_and_capture_b
309
312
  topology_agent_ids,
310
313
  None,
311
314
  None,
315
+ false,
312
316
  )
313
317
  }
314
318
 
@@ -327,6 +331,7 @@ pub(crate) fn save_runtime_state_with_deleted_agents(
327
331
  &[],
328
332
  None,
329
333
  None,
334
+ true,
330
335
  )
331
336
  }
332
337
 
@@ -346,6 +351,7 @@ pub(crate) fn save_runtime_state_with_team_tombstoned_agents(
346
351
  &[],
347
352
  None,
348
353
  None,
354
+ false,
349
355
  )
350
356
  }
351
357
 
@@ -365,6 +371,7 @@ pub(crate) fn save_runtime_state_with_team_tombstone_lifecycle_topology_authorit
365
371
  agent_ids,
366
372
  None,
367
373
  None,
374
+ false,
368
375
  )
369
376
  }
370
377
 
@@ -378,6 +385,7 @@ fn save_runtime_state_with_merge_options(
378
385
  topology_update_agent_ids: &[&str],
379
386
  receiver_update_team_key: Option<&str>,
380
387
  exact_owner_seed_to_clear: Option<&Value>,
388
+ preserve_latest_fork_targets: bool,
381
389
  ) -> Result<(), StateError> {
382
390
  persist_runtime_state_with_merge_options_and_expected(
383
391
  workspace,
@@ -392,6 +400,7 @@ fn save_runtime_state_with_merge_options(
392
400
  None,
393
401
  None,
394
402
  None,
403
+ preserve_latest_fork_targets,
395
404
  )
396
405
  }
397
406
 
@@ -415,6 +424,7 @@ pub(crate) fn save_runtime_state_with_receiver_authority_and_expected(
415
424
  Some((expected_owner, expected_receiver)),
416
425
  None,
417
426
  None,
427
+ false,
418
428
  )
419
429
  }
420
430
 
@@ -438,6 +448,7 @@ pub(crate) fn save_runtime_state_with_exact_owner_receiver_cleanup(
438
448
  None,
439
449
  Some((team_key, expected_owner, expected_receiver)),
440
450
  None,
451
+ false,
441
452
  )
442
453
  }
443
454
 
@@ -469,6 +480,7 @@ pub(crate) fn save_runtime_state_with_exact_owner_receiver_restore(
469
480
  previous_owner,
470
481
  previous_receiver,
471
482
  )),
483
+ false,
472
484
  )
473
485
  }
474
486
 
@@ -485,6 +497,7 @@ fn persist_runtime_state_with_merge_options_and_expected(
485
497
  expected_owner_receiver: Option<(&Value, &Value)>,
486
498
  exact_owner_receiver_to_clear: Option<(&str, &Value, &Value)>,
487
499
  exact_owner_receiver_to_restore: Option<(&str, &Value, &Value, &Value, &Value)>,
500
+ preserve_latest_fork_targets: bool,
488
501
  ) -> Result<(), StateError> {
489
502
  let path = runtime_state_path(workspace);
490
503
  // Python `state.py:497`:先对入参 state 跑 `_migrate_state_identity`(就地填缺失 leader uuid)。
@@ -585,7 +598,7 @@ fn persist_runtime_state_with_merge_options_and_expected(
585
598
  .filter(|team_key| !team_key.is_empty())
586
599
  .map(str::to_string)
587
600
  .collect::<BTreeSet<_>>();
588
- apply_persist_merge_contract(
601
+ apply_persist_merge_contract_with_fork_target_preservation(
589
602
  &mut migrated,
590
603
  latest,
591
604
  &deleted,
@@ -596,6 +609,7 @@ fn persist_runtime_state_with_merge_options_and_expected(
596
609
  receiver_update_team_key.zip(exact_owner_seed_to_clear),
597
610
  exact_owner_receiver_to_clear,
598
611
  exact_owner_receiver_to_restore,
612
+ preserve_latest_fork_targets,
599
613
  )?;
600
614
  }
601
615
  super::abnormal_watch::compact_after_merge(&mut migrated, latest.as_ref(), deleted_agent_ids);
@@ -707,6 +721,34 @@ fn apply_persist_merge_contract(
707
721
  exact_owner_seed_to_clear: Option<(&str, &Value)>,
708
722
  exact_owner_receiver_to_clear: Option<(&str, &Value, &Value)>,
709
723
  exact_owner_receiver_to_restore: Option<(&str, &Value, &Value, &Value, &Value)>,
724
+ ) -> Result<(), StateError> {
725
+ apply_persist_merge_contract_with_fork_target_preservation(
726
+ incoming,
727
+ latest,
728
+ deleted_agent_ids,
729
+ skip_capture_backfill_team_key,
730
+ skip_capture_backfill_agent_ids,
731
+ topology_updates,
732
+ receiver_updates,
733
+ exact_owner_seed_to_clear,
734
+ exact_owner_receiver_to_clear,
735
+ exact_owner_receiver_to_restore,
736
+ false,
737
+ )
738
+ }
739
+
740
+ fn apply_persist_merge_contract_with_fork_target_preservation(
741
+ incoming: &mut Value,
742
+ latest: &Value,
743
+ deleted_agent_ids: &BTreeSet<String>,
744
+ skip_capture_backfill_team_key: Option<&str>,
745
+ skip_capture_backfill_agent_ids: &BTreeSet<String>,
746
+ topology_updates: &BTreeSet<(String, String)>,
747
+ receiver_updates: &BTreeSet<String>,
748
+ exact_owner_seed_to_clear: Option<(&str, &Value)>,
749
+ exact_owner_receiver_to_clear: Option<(&str, &Value, &Value)>,
750
+ exact_owner_receiver_to_restore: Option<(&str, &Value, &Value, &Value, &Value)>,
751
+ preserve_latest_fork_targets: bool,
710
752
  ) -> Result<(), StateError> {
711
753
  // A0/R1: the projection gate only guards the TOP-LEVEL passes (top-level agents and
712
754
  // the top-level<->active-team cross projections depend on which team is active); the
@@ -740,6 +782,7 @@ fn apply_persist_merge_contract(
740
782
  skip_capture_backfill_agent_ids,
741
783
  &topology_update_agent_ids,
742
784
  None,
785
+ preserve_latest_fork_targets,
743
786
  )?;
744
787
  // Stage 3c (identity-boundary unified plan, architect direction
745
788
  // 2026-06-23): top-level owner copy-back removed. Pre-3c this
@@ -818,6 +861,7 @@ fn apply_persist_merge_contract(
818
861
  skip_capture_backfill_agent_ids,
819
862
  &topology_update_agent_ids,
820
863
  team_alive,
864
+ preserve_latest_fork_targets,
821
865
  )?;
822
866
  preserve_latest_ownership_fields(
823
867
  incoming_entry,
@@ -1196,6 +1240,7 @@ fn merge_agent_projection(
1196
1240
  // the merge must NOT protect stale topology so a live sibling's write
1197
1241
  // can go through.
1198
1242
  team_alive: Option<bool>,
1243
+ preserve_latest_fork_targets: bool,
1199
1244
  ) -> Result<(), StateError> {
1200
1245
  let Some(incoming_agents) = incoming_agents else {
1201
1246
  return Ok(());
@@ -1212,8 +1257,17 @@ fn merge_agent_projection(
1212
1257
  }
1213
1258
  let tombstoned_for_projection =
1214
1259
  skip_capture_backfill && skip_capture_backfill_agent_ids.contains(agent_id);
1260
+ let fork_target = preserve_latest_fork_targets
1261
+ && latest_agent
1262
+ .get("forked_from")
1263
+ .and_then(Value::as_str)
1264
+ .is_some_and(|source| !source.is_empty());
1215
1265
  match incoming_map.entry(agent_id.clone()) {
1216
1266
  serde_json::map::Entry::Vacant(slot) => {
1267
+ if fork_target {
1268
+ slot.insert(latest_agent.clone());
1269
+ continue;
1270
+ }
1217
1271
  if topology_update_agent_ids.contains(agent_id) {
1218
1272
  continue;
1219
1273
  }
@@ -1244,7 +1298,9 @@ fn merge_agent_projection(
1244
1298
  if !tombstoned_for_projection && !topology_update_agent_ids.contains(agent_id) {
1245
1299
  let fields = topology_conflict_fields(existing.get(), latest_agent, team_alive);
1246
1300
  if !fields.is_empty() {
1247
- if !topology_update_agent_ids.is_empty()
1301
+ if fork_target {
1302
+ preserve_latest_topology(existing.get_mut(), latest_agent);
1303
+ } else if !topology_update_agent_ids.is_empty()
1248
1304
  && lifecycle_peer_snapshot_is_stale(existing.get(), latest_agent)
1249
1305
  {
1250
1306
  // A lifecycle writer may have loaded a complete
@@ -972,7 +972,11 @@ fn save_team_scoped_state_with_merge_options(
972
972
  );
973
973
  }
974
974
  if tombstoned_agent_ids.is_empty() {
975
- return save_runtime_state_with_deleted_agents(workspace, &merged, deleted_agent_ids);
975
+ return save_runtime_state_with_deleted_agents(
976
+ workspace,
977
+ &merged,
978
+ deleted_agent_ids,
979
+ );
976
980
  }
977
981
  return save_runtime_state_with_team_tombstoned_agents(
978
982
  workspace,
@@ -128,6 +128,9 @@ impl<'a> StateRepository<'a> {
128
128
  /// forward call to the same legacy helper family that the caller used
129
129
  /// before S1a; the intent selects the family, not the merge semantics.
130
130
  pub fn save(&self, intent: StateWriteIntent<'_>, state: &Value) -> Result<(), StateError> {
131
+ if let StateWriteIntent::CoordinatorTick { team_key } = &intent {
132
+ return self.save_coordinator_tick(team_key, state);
133
+ }
131
134
  route_direct(self.workspace, intent, state)
132
135
  }
133
136
 
@@ -149,8 +152,8 @@ impl<'a> StateRepository<'a> {
149
152
  route_reapply(self.workspace, intent, state, reapply)
150
153
  }
151
154
 
152
- /// Commit a task/note delta once against latest under state-save. Return
153
- /// the committed Team view for callers that produce Markdown afterwards.
155
+ /// Commit a bounded Team delta once against latest under state-save.
156
+ /// Return the committed Team view for callers that produce Markdown afterwards.
154
157
  pub fn commit<F>(&self, intent: StateWriteIntent<'_>, update: F) -> Result<Value, StateError>
155
158
  where
156
159
  F: FnOnce(&mut Value),
@@ -163,7 +166,12 @@ impl<'a> StateRepository<'a> {
163
166
  StateWriteIntent::McpAssignTask { .. } => None,
164
167
  StateWriteIntent::McpUpdateStateNote { team_key } => team_key,
165
168
  StateWriteIntent::ResultCollection { owner_team_id } => owner_team_id,
166
- _ => return Err(StateError::SaveFailed("intent does not accept a task/note delta".into())),
169
+ StateWriteIntent::ForkAgent { team_key, .. } => Some(team_key),
170
+ _ => {
171
+ return Err(StateError::SaveFailed(
172
+ "intent does not accept a bounded Team delta".into(),
173
+ ))
174
+ }
167
175
  };
168
176
  let committed = super::persist::update_runtime_state(self.workspace, |latest| {
169
177
  let mut selected = bounded_team_view(latest, team_key)?;
@@ -176,6 +184,64 @@ impl<'a> StateRepository<'a> {
176
184
  bounded_team_view(&committed, team_key)
177
185
  }
178
186
 
187
+ /// Atomically replace only the fork target row in the selected team.
188
+ pub(crate) fn commit_fork_agent(
189
+ &self,
190
+ intent: StateWriteIntent<'_>,
191
+ target_row: &Value,
192
+ ) -> Result<(), StateError> {
193
+ let (team_key, agent_id) = match &intent {
194
+ StateWriteIntent::ForkAgent {
195
+ team_key,
196
+ agent_id,
197
+ } => (*team_key, *agent_id),
198
+ _ => {
199
+ return Err(StateError::SaveFailed(
200
+ "fork target delta requires ForkAgent intent".into(),
201
+ ))
202
+ }
203
+ };
204
+ let target_row = target_row.clone();
205
+ let mut found = false;
206
+ self.commit(
207
+ StateWriteIntent::ForkAgent { team_key, agent_id },
208
+ |selected| {
209
+ if let Some(row) = selected
210
+ .get_mut("agents")
211
+ .and_then(Value::as_object_mut)
212
+ .and_then(|agents| agents.get_mut(agent_id))
213
+ {
214
+ *row = target_row;
215
+ found = true;
216
+ }
217
+ },
218
+ )?;
219
+ if found {
220
+ Ok(())
221
+ } else {
222
+ Err(StateError::SaveFailed(
223
+ "ForkAgent target row is missing from selected team".into(),
224
+ ))
225
+ }
226
+ }
227
+
228
+ fn save_coordinator_tick(&self, team_key: &str, state: &Value) -> Result<(), StateError> {
229
+ let incoming = state.clone();
230
+ super::persist::update_runtime_state(self.workspace, |latest| {
231
+ let mut incoming_team = bounded_team_view(&incoming, Some(team_key))?;
232
+ let latest_team = bounded_team_view(latest, Some(team_key))?;
233
+ preserve_latest_fork_rows(&mut incoming_team, &latest_team);
234
+ let mut checked =
235
+ super::projection::merge_committed_team(latest, &incoming_team, team_key);
236
+ super::persist::merge_ordinary_state(&mut checked, latest)?;
237
+ let checked = bounded_team_view(&checked, Some(team_key))?;
238
+ Ok(super::projection::merge_committed_team(
239
+ latest, &checked, team_key,
240
+ ))
241
+ })?;
242
+ Ok(())
243
+ }
244
+
179
245
  /// Fence the sampled incarnation with the existing persist guards, then
180
246
  /// commit only this tick's agent observations and coordinator bookkeeping.
181
247
  pub fn commit_observations(
@@ -201,6 +267,7 @@ impl<'a> StateRepository<'a> {
201
267
  ));
202
268
  }
203
269
  let mut checked = observed.clone();
270
+ preserve_latest_fork_rows(&mut checked, latest);
204
271
  super::persist::merge_ordinary_state(&mut checked, latest)?;
205
272
  if !legacy_single_team_state(&checked)
206
273
  || sampled_identity != legacy_team_identity(&checked)
@@ -222,7 +289,13 @@ impl<'a> StateRepository<'a> {
222
289
  }
223
290
  return Ok(selected);
224
291
  }
225
- let mut checked = super::projection::merge_committed_team(latest, observed, team_key);
292
+ let mut observed_with_delta = observed.clone();
293
+ sync_team_observation_delta(before, &mut observed_with_delta, team_key);
294
+ let mut observed_team = bounded_team_view(&observed_with_delta, Some(team_key))?;
295
+ let latest_team = bounded_team_view(latest, Some(team_key))?;
296
+ preserve_latest_fork_rows(&mut observed_team, &latest_team);
297
+ let mut checked =
298
+ super::projection::merge_committed_team(latest, &observed_team, team_key);
226
299
  super::persist::merge_ordinary_state(&mut checked, latest)?;
227
300
  let checked = bounded_team_view(&checked, Some(team_key))?;
228
301
  let mut selected = bounded_team_view(latest, Some(team_key))?;
@@ -231,12 +304,32 @@ impl<'a> StateRepository<'a> {
231
304
  apply_observation_delta(&before[field], &checked[field], &mut selected[field]);
232
305
  }
233
306
  }
307
+ sync_team_observation_delta(before, &mut selected, team_key);
234
308
  Ok(super::projection::merge_committed_team(latest, &selected, team_key))
235
309
  })?;
236
310
  Ok(())
237
311
  }
238
312
  }
239
313
 
314
+ // Coordinator observations do not own fork-target lifecycle rows.
315
+ fn preserve_latest_fork_rows(incoming: &mut Value, latest: &Value) {
316
+ let Some(latest_agents) = latest.get("agents").and_then(Value::as_object) else {
317
+ return;
318
+ };
319
+ let Some(incoming_agents) = incoming.get_mut("agents").and_then(Value::as_object_mut) else {
320
+ return;
321
+ };
322
+ for (agent_id, latest_agent) in latest_agents {
323
+ if latest_agent
324
+ .get("forked_from")
325
+ .and_then(Value::as_str)
326
+ .is_some_and(|source| !source.is_empty())
327
+ {
328
+ incoming_agents.insert(agent_id.clone(), latest_agent.clone());
329
+ }
330
+ }
331
+ }
332
+
240
333
  fn legacy_single_team_state(state: &Value) -> bool {
241
334
  match state.get("teams") {
242
335
  None => true,
@@ -292,6 +385,34 @@ fn apply_observation_delta(before: &Value, observed: &Value, latest: &mut Value)
292
385
  }
293
386
  }
294
387
 
388
+ // The coordinator observes a projected team view, while its nested `teams`
389
+ // snapshot remains stale. Carry only observation deltas back to the canonical
390
+ // team entry before `bounded_team_view` projects it again.
391
+ fn sync_team_observation_delta(before: &Value, observed: &mut Value, team_key: &str) {
392
+ for field in ["agents", "coordinator"] {
393
+ let before_value = before.get(field).cloned().unwrap_or(Value::Null);
394
+ let observed_value = observed.get(field).cloned().unwrap_or(Value::Null);
395
+ if before_value == observed_value {
396
+ continue;
397
+ }
398
+ let Some(team_entry) = observed
399
+ .get_mut("teams")
400
+ .and_then(Value::as_object_mut)
401
+ .and_then(|teams| teams.get_mut(team_key))
402
+ .and_then(Value::as_object_mut)
403
+ else {
404
+ continue;
405
+ };
406
+ if field == "coordinator" && !team_entry.contains_key(field) {
407
+ team_entry.insert(field.to_string(), serde_json::json!({}));
408
+ }
409
+ let slot = team_entry
410
+ .entry(field.to_string())
411
+ .or_insert_with(|| before_value.clone());
412
+ apply_observation_delta(&before_value, &observed_value, slot);
413
+ }
414
+ }
415
+
295
416
  /// Closed semantic catalog of workspace state writes. Every allowlisted
296
417
  /// callsite maps to exactly one variant, and there is intentionally no
297
418
  /// generic escape bucket. Any new state write must earn its own variant so
@@ -419,9 +540,6 @@ pub enum StateWriteIntent<'a> {
419
540
  SchedulerSuppression {
420
541
  owner_team_id: Option<&'a str>,
421
542
  },
422
- IdleAck {
423
- team_key: Option<&'a str>,
424
- },
425
543
  TaskRepair {
426
544
  team_key: Option<&'a str>,
427
545
  },
@@ -650,8 +768,6 @@ fn route_direct(
650
768
  },
651
769
  // SchedulerSuppression -> root save (messaging/scheduler.rs:259).
652
770
  StateWriteIntent::SchedulerSuppression { .. } => helper_write_root(workspace, state),
653
- // IdleAck -> root save (cli/mod.rs:2544).
654
- StateWriteIntent::IdleAck { .. } => helper_write_root(workspace, state),
655
771
  // TaskRepair -> `save_team_scoped_state` (cli/adapters.rs:675).
656
772
  StateWriteIntent::TaskRepair { .. } => helper_write_team_scoped(workspace, state),
657
773
  // FakeE2eSeed / SelfMigration are diagnostic seams. The current
@@ -360,7 +360,20 @@ impl OfflineTransport {
360
360
  if kind == "spawn_first" && !state.session_absent_after_spawn_first {
361
361
  state.session_present = true;
362
362
  }
363
- let pane_index = state.spawns.len().saturating_sub(1);
363
+ let mut pane_index = state.spawns.len().saturating_sub(1);
364
+ // Existing sessions reserve `%0` for the already-running pane.
365
+ if kind == "spawn_into" {
366
+ pane_index = pane_index.max(1);
367
+ }
368
+ while state.pane_presence.contains_key(&format!("%{pane_index}"))
369
+ || state
370
+ .targets
371
+ .iter()
372
+ .chain(state.target_snapshots.iter().flatten())
373
+ .any(|target| target.pane_id.as_str() == format!("%{pane_index}"))
374
+ {
375
+ pane_index = pane_index.saturating_add(1);
376
+ }
364
377
  state
365
378
  .pane_presence
366
379
  .insert(format!("%{pane_index}"), state.spawned_panes_addressable);
@@ -541,10 +554,17 @@ impl Transport for OfflineTransport {
541
554
  });
542
555
  }
543
556
  Ok(self.with_state(|state| {
544
- state
557
+ let targets = state
545
558
  .target_snapshots
546
559
  .pop_front()
547
- .unwrap_or_else(|| state.targets.clone())
560
+ .unwrap_or_else(|| state.targets.clone());
561
+ for target in &targets {
562
+ state
563
+ .pane_presence
564
+ .entry(target.pane_id.as_str().to_string())
565
+ .or_insert(true);
566
+ }
567
+ targets
548
568
  }))
549
569
  }
550
570
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.5.99",
3
+ "version": "0.5.101",
4
4
  "description": "npx installer for Team Agent",
5
5
  "keywords": [
6
6
  "codex",
@@ -20,9 +20,9 @@
20
20
  "team-agent-installer": "npm/install.mjs"
21
21
  },
22
22
  "optionalDependencies": {
23
- "@team-agent/cli-darwin-arm64": "0.5.99",
24
- "@team-agent/cli-darwin-x64": "0.5.99",
25
- "@team-agent/cli-linux-x64": "0.5.99"
23
+ "@team-agent/cli-darwin-arm64": "0.5.101",
24
+ "@team-agent/cli-darwin-x64": "0.5.101",
25
+ "@team-agent/cli-linux-x64": "0.5.101"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",