@team-agent/installer 0.3.39 → 0.4.0

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 (61) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +7 -0
  4. package/crates/team-agent/src/cli/adapters.rs +8 -3
  5. package/crates/team-agent/src/cli/diagnose.rs +12 -3
  6. package/crates/team-agent/src/cli/emit.rs +1 -0
  7. package/crates/team-agent/src/cli/mod.rs +250 -9
  8. package/crates/team-agent/src/cli/send.rs +11 -2
  9. package/crates/team-agent/src/cli/status_port.rs +39 -4
  10. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +189 -6
  11. package/crates/team-agent/src/cli/tests/status_send.rs +189 -0
  12. package/crates/team-agent/src/cli/tests/verb_settle.rs +2 -0
  13. package/crates/team-agent/src/cli/types.rs +5 -0
  14. package/crates/team-agent/src/coordinator/mod.rs +1 -0
  15. package/crates/team-agent/src/coordinator/steps/abnormal.rs +4 -0
  16. package/crates/team-agent/src/coordinator/steps/delivery.rs +4 -0
  17. package/crates/team-agent/src/coordinator/steps/health_sync.rs +4 -0
  18. package/crates/team-agent/src/coordinator/steps/mod.rs +83 -0
  19. package/crates/team-agent/src/coordinator/steps/persist.rs +4 -0
  20. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +4 -0
  21. package/crates/team-agent/src/coordinator/steps/session_gate.rs +5 -0
  22. package/crates/team-agent/src/coordinator/tests/health_sync.rs +156 -0
  23. package/crates/team-agent/src/coordinator/tick.rs +126 -30
  24. package/crates/team-agent/src/{message_store.rs → db/message_store.rs} +6 -0
  25. package/crates/team-agent/src/db/mod.rs +1 -0
  26. package/crates/team-agent/src/layout/mod.rs +7 -0
  27. package/crates/team-agent/src/layout/runtime_sessions.rs +312 -0
  28. package/crates/team-agent/src/layout/tmux_endpoint.rs +162 -0
  29. package/crates/team-agent/src/leader/start.rs +27 -1
  30. package/crates/team-agent/src/leader/tests/identity.rs +50 -0
  31. package/crates/team-agent/src/lib.rs +6 -2
  32. package/crates/team-agent/src/lifecycle/launch/readiness.rs +32 -0
  33. package/crates/team-agent/src/lifecycle/launch/spawn.rs +32 -0
  34. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +51 -0
  35. package/crates/team-agent/src/lifecycle/launch.rs +34 -0
  36. package/crates/team-agent/src/lifecycle/restart/agent.rs +17 -0
  37. package/crates/team-agent/src/lifecycle/restart/common.rs +143 -23
  38. package/crates/team-agent/src/lifecycle/restart/preflight.rs +87 -0
  39. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +308 -0
  40. package/crates/team-agent/src/lifecycle/restart/selection.rs +87 -22
  41. package/crates/team-agent/src/lifecycle/restart.rs +6 -0
  42. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +6 -1
  43. package/crates/team-agent/src/lifecycle/tests/restart.rs +187 -0
  44. package/crates/team-agent/src/lifecycle/tests.rs +1 -0
  45. package/crates/team-agent/src/lifecycle/types.rs +20 -1
  46. package/crates/team-agent/src/mcp_server/helpers.rs +72 -1
  47. package/crates/team-agent/src/mcp_server/tools.rs +29 -4
  48. package/crates/team-agent/src/provider/adapter.rs +31 -1
  49. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +34 -0
  50. package/crates/team-agent/src/provider/classify.rs +138 -0
  51. package/crates/team-agent/src/provider/command.rs +71 -0
  52. package/crates/team-agent/src/provider/mod.rs +3 -0
  53. package/crates/team-agent/src/{session_capture.rs → provider/session/capture.rs} +139 -6
  54. package/crates/team-agent/src/provider/session/mod.rs +13 -0
  55. package/crates/team-agent/src/provider/session/resume.rs +417 -0
  56. package/crates/team-agent/src/provider/session_scan.rs +63 -0
  57. package/crates/team-agent/src/provider/types.rs +5 -0
  58. package/crates/team-agent/src/state/persist.rs +238 -0
  59. package/crates/team-agent/src/tmux_backend.rs +25 -0
  60. package/npm/install.mjs +27 -1
  61. package/package.json +4 -4
@@ -262,3 +262,159 @@ fn contract_working_worker_stays_alive_and_deliverable_in_real_tick() {
262
262
  deferred_busy (golden delivers; fake workers are permanently WORKING). got {events:?}"
263
263
  );
264
264
  }
265
+
266
+ // ═══════════════════════════════════════════════════════════════════════════
267
+ // RM-039-STAT-001 third-round RED contracts (architect verdict 2026-06-22).
268
+ //
269
+ // The provider JSONL classifier already recognizes Codex
270
+ // `event_msg/task_started` as an open turn (verified at b6807ba via
271
+ // `p2_codex_event_msg_task_started_is_open_turn`). The third-round bug is
272
+ // in the COORDINATOR ACTIVITY REFRESH BOUNDARY: JSONL classification was
273
+ // gated behind pane-target/window checks and warm-idle suppression, so a
274
+ // stale pane-fallback `idle_prompt` could become the persisted activity
275
+ // truth and then survive across ticks because warm-idle suppression
276
+ // blocked re-classification — even when the rollout grew and the worker
277
+ // process was alive.
278
+ //
279
+ // Fix: JSONL has the floor. If it returns a definite activity, write
280
+ // activity + agent_health BEFORE any pane work and BYPASS warm-idle
281
+ // suppression for that agent on that tick.
282
+ // ═══════════════════════════════════════════════════════════════════════════
283
+
284
+ /// Write a real-shape Codex rollout prefix to `path`. The prefix ends
285
+ /// after `event_msg/task_started` (no `task_complete` follows), matching
286
+ /// the architect's specification.
287
+ fn write_codex_rollout_prefix_task_started(path: &std::path::Path) {
288
+ if let Some(parent) = path.parent() {
289
+ std::fs::create_dir_all(parent).unwrap();
290
+ }
291
+ // session_meta + task_started — minimal but realistic shape per
292
+ // architect §2 ("real rollout uses modern Codex records: session_meta,
293
+ // event_msg, response_item, turn_context"). The classifier needs only
294
+ // task_started to declare an open turn (provider/classify.rs:364-398).
295
+ let body = "{\"type\":\"session_meta\",\"session_id\":\"019eeda2-7e2f-77a1-bc66-f8d485c8d2c8\",\"created\":\"2026-06-22T12:41:49Z\"}\n\
296
+ {\"type\":\"event_msg\",\"payload\":{\"type\":\"task_started\"},\"ts\":\"2026-06-22T12:42:54Z\"}\n";
297
+ std::fs::write(path, body).unwrap();
298
+ }
299
+
300
+ /// Write a real-shape Codex rollout ending with `event_msg/task_complete`
301
+ /// (full turn close). The classifier MUST return Idle with rationale
302
+ /// `provider_jsonl:task_complete`.
303
+ fn write_codex_rollout_task_complete(path: &std::path::Path) {
304
+ if let Some(parent) = path.parent() {
305
+ std::fs::create_dir_all(parent).unwrap();
306
+ }
307
+ let body = "{\"type\":\"session_meta\",\"session_id\":\"019eeda2-7e2f-77a1-bc66-f8d485c8d2c8\",\"created\":\"2026-06-22T12:41:49Z\"}\n\
308
+ {\"type\":\"event_msg\",\"payload\":{\"type\":\"task_started\"},\"ts\":\"2026-06-22T12:42:54Z\"}\n\
309
+ {\"type\":\"event_msg\",\"payload\":{\"type\":\"task_complete\"},\"ts\":\"2026-06-22T12:46:08Z\"}\n";
310
+ std::fs::write(path, body).unwrap();
311
+ }
312
+
313
+ fn agent_activity_rationale(dir: &std::path::Path, agent: &str) -> Option<String> {
314
+ let state = crate::state::persist::load_runtime_state(dir).ok()?;
315
+ state
316
+ .get("agents")?
317
+ .get(agent)?
318
+ .get("activity")?
319
+ .get("rationale")?
320
+ .as_str()
321
+ .map(str::to_string)
322
+ }
323
+
324
+ // RED #2 (architect §6.2): an agent currently marked idle with
325
+ // `coordinator_idle_capture_next_at` in the future MUST still get
326
+ // re-classified as `working` once its rollout JSONL records
327
+ // `event_msg/task_started`. The JSONL classifier must beat warm-idle
328
+ // suppression.
329
+ #[test]
330
+ fn rm039_stat001_jsonl_overrides_warm_idle_suppression_for_open_turn() {
331
+ use std::sync::atomic::{AtomicU64, Ordering};
332
+ static N: AtomicU64 = AtomicU64::new(0);
333
+ let n = N.fetch_add(1, Ordering::Relaxed);
334
+ let rollout = std::env::temp_dir().join(format!(
335
+ "ta-rs-rm039-r3-rollout-open-{}-{}.jsonl",
336
+ std::process::id(),
337
+ n
338
+ ));
339
+ write_codex_rollout_prefix_task_started(&rollout);
340
+ // future timestamp — would have suppressed pane re-capture
341
+ let next_at = (chrono::Utc::now() + chrono::Duration::seconds(120)).to_rfc3339();
342
+ let agents = serde_json::json!({
343
+ "stat-worker": {
344
+ "provider": "codex",
345
+ "window": "w1",
346
+ "pane_id": "%1",
347
+ "status": "running",
348
+ "session_id": "019eeda2-7e2f-77a1-bc66-f8d485c8d2c8",
349
+ "rollout_path": rollout.to_string_lossy(),
350
+ "activity": {"status": "idle", "confidence": 0.9, "rationale": "idle_prompt"},
351
+ "coordinator_idle_capture_next_at": next_at,
352
+ "pane_pid_status": "pid_running:99999",
353
+ }
354
+ });
355
+ // Stale pane scrollback that pane-fallback would classify as idle.
356
+ let (coord, dir) = seeded_health_coord(agents, "previous output\n❯\n");
357
+ coord.tick().expect("tick");
358
+ let status = agent_activity_status(&dir, "stat-worker");
359
+ assert_eq!(
360
+ status.as_deref(),
361
+ Some("working"),
362
+ "RM-039-STAT-001 third-round: JSONL `task_started` must override warm-idle \
363
+ suppression even when the pane scrollback still shows the idle prompt; got {status:?}"
364
+ );
365
+ let rationale = agent_activity_rationale(&dir, "stat-worker");
366
+ assert!(
367
+ rationale
368
+ .as_deref()
369
+ .is_some_and(|r| r.starts_with("provider_jsonl:")),
370
+ "rationale must come from the provider JSONL path (provider_jsonl:*), not pane fallback; \
371
+ got {rationale:?}"
372
+ );
373
+ }
374
+
375
+ // RED #4 (architect §6.4): a full rollout ending in `event_msg/task_complete`
376
+ // MUST return/persist idle with rationale `provider_jsonl:task_complete`.
377
+ // This preserves the E47 stale-working fix and proves the JSONL-first
378
+ // reorder did not regress the close-turn classification.
379
+ #[test]
380
+ fn rm039_stat001_jsonl_close_turn_preserves_e47_idle_classification() {
381
+ use std::sync::atomic::{AtomicU64, Ordering};
382
+ static N: AtomicU64 = AtomicU64::new(0);
383
+ let n = N.fetch_add(1, Ordering::Relaxed);
384
+ let rollout = std::env::temp_dir().join(format!(
385
+ "ta-rs-rm039-r3-rollout-close-{}-{}.jsonl",
386
+ std::process::id(),
387
+ n
388
+ ));
389
+ write_codex_rollout_task_complete(&rollout);
390
+ let agents = serde_json::json!({
391
+ "stat-worker": {
392
+ "provider": "codex",
393
+ "window": "w1",
394
+ "pane_id": "%1",
395
+ "status": "running",
396
+ "session_id": "019eeda2-7e2f-77a1-bc66-f8d485c8d2c8",
397
+ "rollout_path": rollout.to_string_lossy(),
398
+ "pane_pid_status": "pid_running:99999",
399
+ }
400
+ });
401
+ // The pane Tail(40) still shows the live working spinner — the E47
402
+ // stale-working scenario. The JSONL `task_complete` MUST close the
403
+ // turn anyway.
404
+ let (coord, dir) =
405
+ seeded_health_coord(agents, "Working (5s · esc to interrupt)\n• Working\n");
406
+ coord.tick().expect("tick");
407
+ let status = agent_activity_status(&dir, "stat-worker");
408
+ assert_eq!(
409
+ status.as_deref(),
410
+ Some("idle"),
411
+ "RM-039-STAT-001 third-round must preserve E47: `task_complete` closes the turn \
412
+ even when pane scrollback still shows a working spinner; got {status:?}"
413
+ );
414
+ let rationale = agent_activity_rationale(&dir, "stat-worker");
415
+ assert_eq!(
416
+ rationale.as_deref(),
417
+ Some("provider_jsonl:task_complete"),
418
+ "rationale must be `provider_jsonl:task_complete`, not stale pane working; got {rationale:?}"
419
+ );
420
+ }
@@ -99,6 +99,33 @@ pub enum TickError {
99
99
  Panic(String),
100
100
  }
101
101
 
102
+ /// Issue 2 (Round 3b gate review §6): when the runtime state has
103
+ /// `active_team_key` AND `teams.<key>` is a populated object, return the
104
+ /// team-scoped projection so the coordinator's tick reads `session_name` /
105
+ /// `agents` / `leader_receiver` from the team's nested entry rather than
106
+ /// the top-level (often stale) view. When the projection cannot be derived,
107
+ /// returns `None` and the tick falls back to the raw state — preserving
108
+ /// behavior for legacy single-team workspaces and tests that don't seed
109
+ /// `teams.<key>`. Sibling teams under `state.teams.*` are NOT touched.
110
+ fn coordinator_team_scoped_state(
111
+ workspace: &std::path::Path,
112
+ raw_state: &Value,
113
+ ) -> Option<Value> {
114
+ let active = raw_state
115
+ .get("active_team_key")
116
+ .and_then(Value::as_str)
117
+ .filter(|s| !s.is_empty())?;
118
+ let has_team_entry = raw_state
119
+ .get("teams")
120
+ .and_then(Value::as_object)
121
+ .map(|teams| teams.contains_key(active))
122
+ .unwrap_or(false);
123
+ if !has_team_entry {
124
+ return None;
125
+ }
126
+ crate::state::projection::select_runtime_state(workspace, Some(active)).ok()
127
+ }
128
+
102
129
  // ===========================================================================
103
130
  // Coordinator struct(daemon lifecycle + tick orchestration)
104
131
  // ===========================================================================
@@ -190,7 +217,18 @@ impl Coordinator {
190
217
  /// 副作用 ORDER 测试断言固定序列)。生产两者均 `None`,零开销。
191
218
  pub fn tick(&self) -> Result<TickReport, TickError> {
192
219
  self.record_step("load_state");
193
- let mut state = crate::state::persist::load_runtime_state(self.workspace.as_path())?;
220
+ let raw_state = crate::state::persist::load_runtime_state(self.workspace.as_path())?;
221
+ // Issue 2 (Round 3b gate review §6): when the runtime carries
222
+ // `active_team_key` AND `teams.<key>` exists, project the team-scoped
223
+ // view (session_name / agents / leader_receiver come from the team's
224
+ // nested object) instead of the raw top-level state. Otherwise the
225
+ // coordinator would probe the wrong tmux session (e.g. stale
226
+ // `session_name=team-tmp` while the real team session is
227
+ // `team-prerelease-040-round3b`) and emit `coordinator.session_missing`
228
+ // even though the right session is alive. Fall back to raw state when
229
+ // no team scope can be derived (legacy single-team workspaces).
230
+ let mut state = coordinator_team_scoped_state(self.workspace.as_path(), &raw_state)
231
+ .unwrap_or(raw_state);
194
232
  let store = crate::message_store::MessageStore::open(self.workspace.as_path())?;
195
233
  let event_log = EventLog::new(self.workspace.as_path());
196
234
  increment_coordinator_tick_iteration_count(&self.workspace);
@@ -406,6 +444,29 @@ impl Coordinator {
406
444
  true,
407
445
  0,
408
446
  )?;
447
+ // RM-039-STAT-001 third-round fix (architect verdict 2026-06-22):
448
+ // when capture_missing_sessions assigns a new rollout_path to an
449
+ // agent, clear that agent's `coordinator_idle_capture_next_at` so
450
+ // the JSONL classifier is not gated by stale warm-idle suppression
451
+ // on the SAME tick that the rollout becomes readable. Without this,
452
+ // an agent that was IDLE before attribution would carry the old
453
+ // suppression timestamp into sync_agent_health and only the
454
+ // expensive pane Tail(40) path would observe its working state.
455
+ if !report.assigned.is_empty() {
456
+ if let Some(agents) = state
457
+ .get_mut("agents")
458
+ .and_then(serde_json::Value::as_object_mut)
459
+ {
460
+ for assigned_agent_id in &report.assigned {
461
+ if let Some(agent_obj) = agents
462
+ .get_mut(assigned_agent_id)
463
+ .and_then(serde_json::Value::as_object_mut)
464
+ {
465
+ agent_obj.remove("coordinator_idle_capture_next_at");
466
+ }
467
+ }
468
+ }
469
+ }
409
470
  for failure in report.capture_failures {
410
471
  event_log.write(
411
472
  "coordinator.tick.capture_missing_failed",
@@ -466,6 +527,38 @@ impl Coordinator {
466
527
  if !agent_probe_base_eligible(agent) {
467
528
  continue;
468
529
  }
530
+ // RM-039-STAT-001 third-round fix (architect verdict 2026-06-22):
531
+ // try the provider JSONL classifier FIRST, BEFORE any pane-target
532
+ // / window checks and BEFORE warm_idle_capture_suppressed. The
533
+ // historical chain ran JSONL only inside the pane-capture branch,
534
+ // so a stale pane-fallback `idle_prompt` could land first
535
+ // (before session attribution completed), then
536
+ // `warm_idle_capture_suppressed` would gate subsequent ticks for
537
+ // IDLE_HEALTH_CAPTURE_INTERVAL_SECS even after JSONL gained a
538
+ // `task_started` record + the worker process was alive. Now: if
539
+ // the JSONL classifier returns a definite activity (not Unknown),
540
+ // we write activity + agent_health here and bypass the warm-idle
541
+ // suppression for the rest of this iteration. Pane capture still
542
+ // runs so the CapturedRuntimeFact map stays populated for
543
+ // downstream runtime detectors; only the activity classify
544
+ // happens once.
545
+ let jsonl_first = jsonl_activity_for_agent(agent);
546
+ if let Some(activity) = jsonl_first.as_ref() {
547
+ remember_idle_capture_schedule(agent, activity);
548
+ write_activity(agent, activity, false);
549
+ let last_output_at_now = agent
550
+ .get("last_output_at")
551
+ .and_then(Value::as_str)
552
+ .map(str::to_string);
553
+ write_agent_health(
554
+ store,
555
+ &team,
556
+ agent_id,
557
+ agent,
558
+ activity,
559
+ last_output_at_now.as_deref(),
560
+ )?;
561
+ }
469
562
  let Some((session, window, target)) =
470
563
  capture_window_target(agent, session_name.as_deref())
471
564
  else {
@@ -495,7 +588,13 @@ impl Coordinator {
495
588
  if !windows.iter().any(|known| known == &window) {
496
589
  continue;
497
590
  }
498
- if warm_idle_capture_suppressed(agent, has_work_obligation) {
591
+ // Warm-idle suppression still gates pane fallback ONLY. When
592
+ // JSONL above produced a definite activity, we already wrote it,
593
+ // so warm-idle gating no longer matters for the activity path —
594
+ // but we still want CapturedRuntimeFact populated for downstream
595
+ // detectors, so fall through into the pane capture block below
596
+ // (skipping the pane-classify activity write at the bottom).
597
+ if jsonl_first.is_none() && warm_idle_capture_suppressed(agent, has_work_obligation) {
499
598
  continue;
500
599
  }
501
600
  let captured = match self
@@ -544,38 +643,35 @@ impl Coordinator {
544
643
  .get("last_output_at")
545
644
  .and_then(Value::as_str)
546
645
  .map(str::to_string);
547
- // E47 (0.3.24 P0, idle/busy 假阳): consult the AUTHORITATIVE
548
- // provider JSONL classifier BEFORE the TUI keyword scan. The
549
- // scrollback Tail(40) keeps historical `• Working (...· esc to
550
- // interrupt)` + spinners within the 40-line window long after a
551
- // turn completed, so `classify_agent_activity`'s rfind-recency
552
- // grep flips a truly-idle worker to Working/Stuck. The provider
553
- // JSONL (codex task_complete / claude end_turn) is the single
554
- // source of truth for "turn closed". Only fall back to TUI
555
- // scanning when the JSONL is unreadable or no lifecycle fact has
556
- // been written yet (TurnState::Unknown) — never silently fall
557
- // through into the leaky grep on a CONFIRMED IDLE.
558
- let activity_from_jsonl = jsonl_activity_for_agent(agent);
559
- let activity = activity_from_jsonl.unwrap_or_else(|| {
560
- crate::messaging::classify_agent_activity(
646
+ // E47 (0.3.24 P0, idle/busy 假阳): the provider JSONL classifier
647
+ // already ran at the top of this iteration. If it returned a
648
+ // definite fact (Some), activity + agent_health were written
649
+ // there; we MUST NOT classify-and-overwrite here, otherwise the
650
+ // pane fallback would flip the JSONL truth back to a stale
651
+ // idle_prompt. Only run the pane fallback when JSONL returned
652
+ // None (no readable rollout, TurnState::Unknown, or unparseable
653
+ // log). RM-039-STAT-001 third-round fix (architect verdict
654
+ // 2026-06-22).
655
+ let last_output_at = last_output_at_now;
656
+ if jsonl_first.is_none() {
657
+ let activity = crate::messaging::classify_agent_activity(
561
658
  &snapshot,
562
659
  &captured.text,
563
660
  pane_in_mode,
564
661
  current_command.as_deref(),
565
- last_output_at_now.as_deref(),
566
- )
567
- });
568
- remember_idle_capture_schedule(agent, &activity);
569
- write_activity(agent, &activity, false);
570
- let last_output_at = last_output_at_now;
571
- write_agent_health(
572
- store,
573
- &team,
574
- agent_id,
575
- agent,
576
- &activity,
577
- last_output_at.as_deref(),
578
- )?;
662
+ last_output_at.as_deref(),
663
+ );
664
+ remember_idle_capture_schedule(agent, &activity);
665
+ write_activity(agent, &activity, false);
666
+ write_agent_health(
667
+ store,
668
+ &team,
669
+ agent_id,
670
+ agent,
671
+ &activity,
672
+ last_output_at.as_deref(),
673
+ )?;
674
+ }
579
675
  let pane_info = matching_capture_pane_info(agent, &session, &window, pane_infos);
580
676
  let pane_id = pane_info
581
677
  .as_ref()
@@ -1,3 +1,9 @@
1
+ // unit-10 (Stage 4): moved from crate::message_store to crate::db::message_store.
2
+ // The historical body uses unwrap/expect/panic for SQL-row failures; the parent
3
+ // db/ module denies these lints, so re-allow them here so the move is a pure
4
+ // physical relocation (zero behavior change).
5
+ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
6
+
1
7
  //! step 7 · message_store — core message lifecycle over `team.db`.
2
8
  //!
3
9
  //! Truth source (READ-ONLY): `team-agent-public` @ v0.2.11,
@@ -9,6 +9,7 @@
9
9
  //! §10:db 层无 unwrap/expect/panic;rusqlite 错误经 [`DbError`] 传播。
10
10
  #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
11
11
 
12
+ pub mod message_store;
12
13
  pub mod migration;
13
14
  pub mod schema;
14
15
 
@@ -27,3 +27,10 @@ pub mod worker_env;
27
27
  pub mod placement;
28
28
  pub mod recovery;
29
29
  pub mod overlay;
30
+ pub mod runtime_sessions;
31
+ pub mod tmux_endpoint;
32
+
33
+ pub use runtime_sessions::{
34
+ LeaderLauncherSession, LeaderLauncherSessionError, RuntimeSessionAnomaly, RuntimeSessions,
35
+ WorkerSession, WorkerSessionError,
36
+ };