oh-my-codex 0.21.16 → 0.21.18

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 (58) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/crates/omx-runtime/src/main.rs +14 -2
  4. package/crates/omx-runtime/tests/execution.rs +63 -1
  5. package/crates/omx-runtime-core/src/dispatch.rs +53 -0
  6. package/crates/omx-runtime-core/src/engine.rs +637 -35
  7. package/crates/omx-runtime-core/src/lib.rs +14 -0
  8. package/crates/omx-runtime-core/src/mailbox.rs +168 -38
  9. package/dist/cli/__tests__/doctor-invalid-config.test.js +57 -0
  10. package/dist/cli/__tests__/doctor-invalid-config.test.js.map +1 -1
  11. package/dist/cli/__tests__/plugin-cache-live-pin-3790.test.js +1 -1
  12. package/dist/cli/__tests__/setup-refresh.test.js +54 -0
  13. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  14. package/dist/compat/__tests__/rust-runtime-compat.test.js +3 -3
  15. package/dist/config/__tests__/generator-idempotent.test.js +64 -1
  16. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  17. package/dist/config/generator.d.ts.map +1 -1
  18. package/dist/config/generator.js +66 -21
  19. package/dist/config/generator.js.map +1 -1
  20. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +1006 -10
  21. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  22. package/dist/mcp/state-server.d.ts +1 -1
  23. package/dist/runtime/__tests__/bridge.test.js +91 -8
  24. package/dist/runtime/__tests__/bridge.test.js.map +1 -1
  25. package/dist/runtime/bridge.d.ts +13 -2
  26. package/dist/runtime/bridge.d.ts.map +1 -1
  27. package/dist/runtime/bridge.js +94 -41
  28. package/dist/runtime/bridge.js.map +1 -1
  29. package/dist/scripts/notify-hook/team-dispatch.d.ts +12 -0
  30. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  31. package/dist/scripts/notify-hook/team-dispatch.js +127 -27
  32. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  33. package/dist/team/__tests__/api-interop.test.js +2 -2
  34. package/dist/team/__tests__/delivery-e2e-smoke.test.js +19 -5
  35. package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
  36. package/dist/team/__tests__/runtime.test.js +21 -1
  37. package/dist/team/__tests__/runtime.test.js.map +1 -1
  38. package/dist/team/__tests__/state.test.js +1532 -11
  39. package/dist/team/__tests__/state.test.js.map +1 -1
  40. package/dist/team/mcp-comm.js +2 -2
  41. package/dist/team/mcp-comm.js.map +1 -1
  42. package/dist/team/state/dispatch-recovery.d.ts +8 -0
  43. package/dist/team/state/dispatch-recovery.d.ts.map +1 -0
  44. package/dist/team/state/dispatch-recovery.js +88 -0
  45. package/dist/team/state/dispatch-recovery.js.map +1 -0
  46. package/dist/team/state/dispatch.d.ts.map +1 -1
  47. package/dist/team/state/dispatch.js +94 -69
  48. package/dist/team/state/dispatch.js.map +1 -1
  49. package/dist/team/state/mailbox.d.ts +9 -2
  50. package/dist/team/state/mailbox.d.ts.map +1 -1
  51. package/dist/team/state/mailbox.js +186 -68
  52. package/dist/team/state/mailbox.js.map +1 -1
  53. package/dist/team/state.d.ts.map +1 -1
  54. package/dist/team/state.js +116 -30
  55. package/dist/team/state.js.map +1 -1
  56. package/package.json +1 -1
  57. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  58. package/src/scripts/notify-hook/team-dispatch.ts +117 -26
@@ -218,36 +218,67 @@ impl RuntimeEngine {
218
218
  }
219
219
  RuntimeCommand::CaptureSnapshot => RuntimeEvent::SnapshotCaptured,
220
220
  RuntimeCommand::CreateMailboxMessage {
221
+ team_name,
221
222
  message_id,
222
223
  from_worker,
223
224
  to_worker,
224
225
  body,
225
226
  } => {
226
- self.mailbox
227
- .create(&message_id, &from_worker, &to_worker, &body);
227
+ self.mailbox.create(
228
+ team_name.clone(),
229
+ &message_id,
230
+ &from_worker,
231
+ &to_worker,
232
+ &body,
233
+ )?;
228
234
  RuntimeEvent::MailboxMessageCreated {
235
+ team_name,
229
236
  message_id,
230
237
  from_worker,
231
238
  to_worker,
232
239
  body: Some(body),
233
240
  }
234
241
  }
235
- RuntimeCommand::MarkMailboxNotified { message_id } => {
236
- self.mailbox.mark_notified(&message_id)?;
237
- RuntimeEvent::MailboxNotified { message_id }
242
+ RuntimeCommand::MarkMailboxNotified {
243
+ message_id,
244
+ team_name,
245
+ } => {
246
+ let team_name = self
247
+ .mailbox
248
+ .mark_notified(&message_id, team_name.as_deref())?;
249
+ RuntimeEvent::MailboxNotified {
250
+ team_name,
251
+ message_id,
252
+ }
238
253
  }
239
- RuntimeCommand::MarkMailboxDelivered { message_id } => {
240
- self.mailbox.mark_delivered(&message_id)?;
241
- RuntimeEvent::MailboxDelivered { message_id }
254
+ RuntimeCommand::MarkMailboxDelivered {
255
+ message_id,
256
+ team_name,
257
+ } => {
258
+ let team_name = self
259
+ .mailbox
260
+ .mark_delivered(&message_id, team_name.as_deref())?;
261
+ RuntimeEvent::MailboxDelivered {
262
+ team_name,
263
+ message_id,
264
+ }
242
265
  }
243
266
  };
244
267
 
245
- if !matches!(event, RuntimeEvent::DispatchRecordsRemoved { .. }) {
268
+ // Projection refreshes do not change domain state or need replay history.
269
+ if !matches!(
270
+ event,
271
+ RuntimeEvent::DispatchRecordsRemoved { .. } | RuntimeEvent::SnapshotCaptured
272
+ ) {
246
273
  self.event_log.push(event.clone());
247
274
  }
248
275
  Ok(event)
249
276
  }
250
277
 
278
+ pub fn dispatch_records(&self) -> &[crate::DispatchRecord] {
279
+ self.dispatch.records()
280
+ }
281
+
251
282
  pub fn snapshot(&self) -> RuntimeSnapshot {
252
283
  RuntimeSnapshot {
253
284
  schema_version: RUNTIME_SCHEMA_VERSION,
@@ -362,9 +393,11 @@ impl RuntimeEngine {
362
393
  let events_path = dir.join("events.json");
363
394
  let events_json = std::fs::read_to_string(&events_path)?;
364
395
  let mut events: Vec<RuntimeEvent> = serde_json::from_str(&events_json)?;
365
- let mailbox = std::fs::read_to_string(dir.join("mailbox.json"))
366
- .ok()
367
- .and_then(|mailbox_json| serde_json::from_str::<MailboxLog>(&mailbox_json).ok());
396
+ let mailbox = match std::fs::read_to_string(dir.join("mailbox.json")) {
397
+ Ok(mailbox_json) => Some(serde_json::from_str::<MailboxLog>(&mailbox_json)?),
398
+ Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
399
+ Err(error) => return Err(error.into()),
400
+ };
368
401
  let ledger = match std::fs::read_to_string(dir.join(DISPATCH_SEEN_LEDGER_FILE)) {
369
402
  Ok(json) => parse_dispatch_seen_ledger(&json)?,
370
403
  Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
@@ -376,14 +409,13 @@ impl RuntimeEngine {
376
409
  }
377
410
  Err(error) => return Err(error.into()),
378
411
  };
379
- let persisted_dispatch_ids = match std::fs::read_to_string(dir.join("dispatch.json")) {
380
- Ok(json) => {
381
- let dispatch: DispatchLog = serde_json::from_str(&json)?;
382
- collect_unique_dispatch_ids(dispatch.request_ids())?
383
- }
384
- Err(error) if error.kind() == std::io::ErrorKind::NotFound => BTreeSet::new(),
412
+ let dispatch_projection = match std::fs::read_to_string(dir.join("dispatch.json")) {
413
+ Ok(json) => serde_json::from_str::<DispatchLog>(&json)?,
414
+ Err(error) if error.kind() == std::io::ErrorKind::NotFound => DispatchLog::new(),
385
415
  Err(error) => return Err(error.into()),
386
416
  };
417
+ let persisted_dispatch_ids =
418
+ collect_unique_dispatch_ids(dispatch_projection.request_ids())?;
387
419
 
388
420
  drop(lock_file);
389
421
 
@@ -403,29 +435,64 @@ impl RuntimeEngine {
403
435
  .into());
404
436
  }
405
437
  engine.seen_dispatch_ids = ledger;
406
-
407
- if let Some(mailbox_state) = mailbox {
408
- let body_by_message_id: std::collections::HashMap<&str, &str> = mailbox_state
409
- .records()
410
- .iter()
411
- .map(|record| (record.message_id.as_str(), record.body.as_str()))
412
- .collect();
438
+ engine
439
+ .dispatch
440
+ .recover_pending_hook_attempts(&dispatch_projection);
441
+
442
+ if let Some(mut mailbox_state) = mailbox {
443
+ let mut body_by_message = std::collections::HashMap::new();
444
+ for record in mailbox_state.records() {
445
+ body_by_message
446
+ .entry((record.team_name.clone(), record.message_id.clone()))
447
+ .and_modify(|body| *body = None)
448
+ .or_insert(Some(record.body.clone()));
449
+ }
413
450
 
414
451
  for event in &mut events {
415
452
  if let RuntimeEvent::MailboxMessageCreated {
416
- message_id, body, ..
453
+ team_name,
454
+ message_id,
455
+ body,
456
+ ..
417
457
  } = event
418
458
  {
419
459
  if body.is_none() {
420
- if let Some(record_body) = body_by_message_id.get(message_id.as_str()) {
460
+ if let Some(Some(record_body)) =
461
+ body_by_message.get(&(team_name.clone(), message_id.clone()))
462
+ {
421
463
  if !record_body.is_empty() {
422
- *body = Some((*record_body).to_string());
464
+ *body = Some(record_body.clone());
423
465
  }
424
466
  }
425
467
  }
468
+ if let Some(body) = body.as_deref() {
469
+ mailbox_state.recover_body(team_name.as_deref(), message_id, body);
470
+ }
426
471
  }
427
472
  }
428
473
  engine.mailbox = mailbox_state;
474
+ for event in &events {
475
+ match event {
476
+ RuntimeEvent::MailboxMessageCreated {
477
+ team_name,
478
+ message_id,
479
+ ..
480
+ } if !engine.mailbox.records().iter().any(|record| {
481
+ record.message_id == *message_id
482
+ && record.team_name.as_deref() == team_name.as_deref()
483
+ }) =>
484
+ {
485
+ replay_event(&mut engine, event)?
486
+ }
487
+ RuntimeEvent::MailboxNotified { .. }
488
+ | RuntimeEvent::MailboxDelivered { .. }
489
+ if !mailbox_acknowledgement_is_reflected(&engine.mailbox, event) =>
490
+ {
491
+ replay_event(&mut engine, event)?;
492
+ }
493
+ _ => {}
494
+ }
495
+ }
429
496
  }
430
497
 
431
498
  engine.event_log = events;
@@ -611,23 +678,51 @@ fn replay_event(engine: &mut RuntimeEngine, event: &RuntimeEvent) -> Result<(),
611
678
  engine.replay.request_replay(cursor.clone());
612
679
  }
613
680
  RuntimeEvent::MailboxMessageCreated {
681
+ team_name,
614
682
  message_id,
615
683
  from_worker,
616
684
  to_worker,
617
685
  body,
618
686
  } => {
619
- engine.mailbox.create(
687
+ engine.mailbox.replay_create(
688
+ team_name.clone(),
620
689
  message_id,
621
690
  from_worker,
622
691
  to_worker,
623
692
  body.as_deref().unwrap_or(""),
624
693
  );
625
694
  }
626
- RuntimeEvent::MailboxNotified { message_id } => {
627
- let _ = engine.mailbox.mark_notified(message_id);
695
+ RuntimeEvent::MailboxNotified {
696
+ message_id,
697
+ team_name,
628
698
  }
629
- RuntimeEvent::MailboxDelivered { message_id } => {
630
- let _ = engine.mailbox.mark_delivered(message_id);
699
+ | RuntimeEvent::MailboxDelivered {
700
+ message_id,
701
+ team_name,
702
+ } => {
703
+ // Older acknowledgement events have no scope and are safe only for a unique ID.
704
+ let replay_team = team_name.clone().or_else(|| {
705
+ let mut records = engine
706
+ .mailbox
707
+ .records()
708
+ .iter()
709
+ .filter(|record| record.message_id == *message_id);
710
+ let first = records.next()?;
711
+ if records.next().is_none() {
712
+ first.team_name.clone()
713
+ } else {
714
+ None
715
+ }
716
+ });
717
+ if matches!(event, RuntimeEvent::MailboxNotified { .. }) {
718
+ let _ = engine
719
+ .mailbox
720
+ .mark_notified(message_id, replay_team.as_deref());
721
+ } else {
722
+ let _ = engine
723
+ .mailbox
724
+ .mark_delivered(message_id, replay_team.as_deref());
725
+ }
631
726
  }
632
727
  RuntimeEvent::DispatchRecordsRemoved { .. } | RuntimeEvent::SnapshotCaptured => {}
633
728
  event => replay_dispatch_event(&mut engine.dispatch, event)?,
@@ -635,6 +730,66 @@ fn replay_event(engine: &mut RuntimeEngine, event: &RuntimeEvent) -> Result<(),
635
730
  Ok(())
636
731
  }
637
732
 
733
+ fn mailbox_acknowledgement_is_reflected(mailbox: &MailboxLog, event: &RuntimeEvent) -> bool {
734
+ let (message_id, team_name, delivered) = match event {
735
+ RuntimeEvent::MailboxNotified {
736
+ message_id,
737
+ team_name,
738
+ } => (message_id.as_str(), team_name.as_deref(), false),
739
+ RuntimeEvent::MailboxDelivered {
740
+ message_id,
741
+ team_name,
742
+ } => (message_id.as_str(), team_name.as_deref(), true),
743
+ _ => return false,
744
+ };
745
+
746
+ let record = if let Some(team_name) = team_name {
747
+ let mut scoped = mailbox.records().iter().filter(|record| {
748
+ record.message_id == message_id && record.team_name.as_deref() == Some(team_name)
749
+ });
750
+ match scoped.next() {
751
+ Some(record) => {
752
+ if scoped.next().is_none() {
753
+ Some(record)
754
+ } else {
755
+ None
756
+ }
757
+ }
758
+ None => {
759
+ let mut matches = mailbox
760
+ .records()
761
+ .iter()
762
+ .filter(|record| record.message_id == message_id);
763
+ let first = matches.next();
764
+ if matches.next().is_none() {
765
+ first.filter(|record| record.team_name.is_none())
766
+ } else {
767
+ None
768
+ }
769
+ }
770
+ }
771
+ } else {
772
+ let mut matches = mailbox
773
+ .records()
774
+ .iter()
775
+ .filter(|record| record.message_id == message_id);
776
+ let first = matches.next();
777
+ if matches.next().is_none() {
778
+ first
779
+ } else {
780
+ None
781
+ }
782
+ };
783
+
784
+ record.is_some_and(|record| {
785
+ if delivered {
786
+ record.delivered_at.is_some()
787
+ } else {
788
+ record.notified_at.is_some() || record.delivered_at.is_some()
789
+ }
790
+ })
791
+ }
792
+
638
793
  pub fn derive_readiness(
639
794
  authority: &AuthorityLease,
640
795
  _dispatch: &DispatchLog,
@@ -668,6 +823,80 @@ pub fn derive_readiness(
668
823
  #[cfg(test)]
669
824
  mod tests {
670
825
  use super::*;
826
+ use crate::DispatchStatus;
827
+
828
+ #[test]
829
+ fn load_preserves_pending_hook_attempts_across_teams_without_overriding_events() {
830
+ let dir = std::env::temp_dir().join(format!("omx-hook-recovery-{}", std::process::id()));
831
+ let _ = std::fs::remove_dir_all(&dir);
832
+ let mut engine = RuntimeEngine::new().with_state_dir(&dir);
833
+ for (id, team) in [
834
+ ("alpha", "alpha"),
835
+ ("beta", "beta"),
836
+ ("notified", "alpha"),
837
+ ("wrong-team", "alpha"),
838
+ ("wrong-target", "alpha"),
839
+ ("terminal-projection", "alpha"),
840
+ ] {
841
+ engine.process(RuntimeCommand::QueueDispatch {
842
+ request_id: id.into(), target: "worker-1".into(),
843
+ metadata: Some(serde_json::json!({"team_name": team, "attempt_count": 0, "trigger_message": "original"})),
844
+ }).unwrap();
845
+ }
846
+ engine
847
+ .process(RuntimeCommand::MarkNotified {
848
+ request_id: "notified".into(),
849
+ channel: "runtime".into(),
850
+ })
851
+ .unwrap();
852
+ engine.persist().unwrap();
853
+ let path = dir.join("dispatch.json");
854
+ let mut projection: serde_json::Value =
855
+ serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
856
+ for record in projection["records"].as_array_mut().unwrap() {
857
+ record["status"] = serde_json::json!("pending");
858
+ record["reason"] = serde_json::json!("tmux_send_keys_unconfirmed");
859
+ record["metadata"]["attempt_count"] = serde_json::json!(2);
860
+ record["metadata"]["last_attempt_at"] = serde_json::json!("2026-10-16T00:00:00Z");
861
+ record["metadata"]["trigger_message"] = serde_json::json!("corrupted");
862
+ match record["request_id"].as_str().unwrap() {
863
+ "wrong-team" => record["metadata"]["team_name"] = serde_json::json!("beta"),
864
+ "wrong-target" => record["target"] = serde_json::json!("worker-2"),
865
+ "terminal-projection" => record["status"] = serde_json::json!("delivered"),
866
+ _ => {}
867
+ }
868
+ }
869
+ std::fs::write(&path, serde_json::to_string(&projection).unwrap()).unwrap();
870
+ let mut loaded = RuntimeEngine::load(&dir).unwrap();
871
+ loaded.process(RuntimeCommand::CaptureSnapshot).unwrap();
872
+ loaded.persist().unwrap();
873
+ let reloaded = RuntimeEngine::load(&dir).unwrap();
874
+ for record in reloaded.dispatch.records() {
875
+ let metadata = record.metadata.as_ref().unwrap();
876
+ assert_eq!(metadata["trigger_message"], "original");
877
+ assert_eq!(record.target, "worker-1");
878
+ if record.request_id == "alpha" || record.request_id == "beta" {
879
+ assert_eq!(metadata["attempt_count"], 2);
880
+ assert_eq!(metadata["last_attempt_at"], "2026-10-16T00:00:00Z");
881
+ assert_eq!(record.reason.as_deref(), Some("tmux_send_keys_unconfirmed"));
882
+ assert_eq!(record.status, DispatchStatus::Pending);
883
+ } else {
884
+ assert_eq!(metadata["attempt_count"], 0);
885
+ assert!(metadata.get("last_attempt_at").is_none());
886
+ assert_eq!(metadata["team_name"], "alpha");
887
+ assert_eq!(
888
+ record.status,
889
+ if record.request_id == "notified" {
890
+ DispatchStatus::Notified
891
+ } else {
892
+ DispatchStatus::Pending
893
+ }
894
+ );
895
+ assert_ne!(record.reason.as_deref(), Some("tmux_send_keys_unconfirmed"));
896
+ }
897
+ }
898
+ let _ = std::fs::remove_dir_all(&dir);
899
+ }
671
900
 
672
901
  #[test]
673
902
  fn process_acquire_authority() {
@@ -740,11 +969,11 @@ mod tests {
740
969
  }
741
970
 
742
971
  #[test]
743
- fn event_log_accumulates() {
972
+ fn snapshot_refresh_does_not_accumulate_events() {
744
973
  let mut engine = RuntimeEngine::new();
745
974
  engine.process(RuntimeCommand::CaptureSnapshot).unwrap();
746
975
  engine.process(RuntimeCommand::CaptureSnapshot).unwrap();
747
- assert_eq!(engine.event_log().len(), 2);
976
+ assert_eq!(engine.event_log().len(), 0);
748
977
  }
749
978
 
750
979
  #[test]
@@ -779,6 +1008,223 @@ mod tests {
779
1008
  let _ = std::fs::remove_dir_all(&dir);
780
1009
  }
781
1010
 
1011
+ #[test]
1012
+ fn mailbox_mutation_selects_scoped_record_after_legacy_id_collision() {
1013
+ let dir =
1014
+ std::env::temp_dir().join(format!("omx-mailbox-collision-{}", std::process::id()));
1015
+ let _ = std::fs::remove_dir_all(&dir);
1016
+ let mut engine = RuntimeEngine::new().with_state_dir(&dir);
1017
+ for team_name in [None, Some("beta".to_string())] {
1018
+ let event = RuntimeEvent::MailboxMessageCreated {
1019
+ team_name,
1020
+ message_id: "collision".into(),
1021
+ from_worker: "leader-fixed".into(),
1022
+ to_worker: "worker-1".into(),
1023
+ body: Some("payload".into()),
1024
+ };
1025
+ replay_event(&mut engine, &event).unwrap();
1026
+ engine.event_log.push(event);
1027
+ }
1028
+ engine
1029
+ .process(RuntimeCommand::MarkMailboxNotified {
1030
+ team_name: Some("beta".into()),
1031
+ message_id: "collision".into(),
1032
+ })
1033
+ .unwrap();
1034
+ engine
1035
+ .process(RuntimeCommand::MarkMailboxDelivered {
1036
+ team_name: Some("beta".into()),
1037
+ message_id: "collision".into(),
1038
+ })
1039
+ .unwrap();
1040
+ engine.persist().unwrap();
1041
+ std::fs::remove_file(dir.join("mailbox.json")).unwrap();
1042
+ let engine = RuntimeEngine::load(&dir).unwrap();
1043
+ assert!(engine.mailbox.records()[0].notified_at.is_none());
1044
+ assert!(engine.mailbox.records()[0].delivered_at.is_none());
1045
+ assert!(engine.mailbox.records()[1].notified_at.is_some());
1046
+ assert!(engine.mailbox.records()[1].delivered_at.is_some());
1047
+ let _ = std::fs::remove_dir_all(&dir);
1048
+ }
1049
+
1050
+ #[test]
1051
+ fn old_mailbox_acknowledgements_replay_only_for_unique_ids() {
1052
+ for scopes in [
1053
+ vec![None],
1054
+ vec![Some("alpha".to_string())],
1055
+ vec![None, None],
1056
+ vec![None, Some("beta".to_string())],
1057
+ ] {
1058
+ let mut engine = RuntimeEngine::new();
1059
+ for scope in &scopes {
1060
+ engine.mailbox.replay_create(
1061
+ scope.clone(),
1062
+ "legacy-event",
1063
+ "leader-fixed",
1064
+ "worker-1",
1065
+ "body",
1066
+ );
1067
+ }
1068
+ for event in ["MailboxNotified", "MailboxDelivered"] {
1069
+ let event: RuntimeEvent = serde_json::from_value(serde_json::json!({
1070
+ "event": event, "message_id": "legacy-event"
1071
+ }))
1072
+ .unwrap();
1073
+ replay_event(&mut engine, &event).unwrap();
1074
+ }
1075
+ for record in engine.mailbox.records() {
1076
+ assert_eq!(record.notified_at.is_some(), scopes.len() == 1);
1077
+ assert_eq!(record.delivered_at.is_some(), scopes.len() == 1);
1078
+ }
1079
+ }
1080
+ }
1081
+
1082
+ #[test]
1083
+ fn mailbox_team_check_guards_the_record_mutated_when_ids_repeat() {
1084
+ let mut engine = RuntimeEngine::new();
1085
+ for team in ["alpha", "beta"] {
1086
+ engine
1087
+ .process(RuntimeCommand::CreateMailboxMessage {
1088
+ team_name: Some(team.into()),
1089
+ message_id: "repeated".into(),
1090
+ from_worker: "leader-fixed".into(),
1091
+ to_worker: "worker-1".into(),
1092
+ body: team.into(),
1093
+ })
1094
+ .unwrap();
1095
+ }
1096
+ assert!(engine
1097
+ .process(RuntimeCommand::MarkMailboxNotified {
1098
+ team_name: Some("beta".into()),
1099
+ message_id: "repeated".into(),
1100
+ })
1101
+ .is_ok());
1102
+ assert!(engine
1103
+ .process(RuntimeCommand::MarkMailboxDelivered {
1104
+ team_name: Some("beta".into()),
1105
+ message_id: "repeated".into(),
1106
+ })
1107
+ .is_ok());
1108
+ assert!(engine.mailbox.records()[0].notified_at.is_none());
1109
+ assert!(engine.mailbox.records()[0].delivered_at.is_none());
1110
+ assert!(engine.mailbox.records()[1].notified_at.is_some());
1111
+ assert!(engine.mailbox.records()[1].delivered_at.is_some());
1112
+ engine
1113
+ .process(RuntimeCommand::MarkMailboxDelivered {
1114
+ team_name: Some("alpha".into()),
1115
+ message_id: "repeated".into(),
1116
+ })
1117
+ .unwrap();
1118
+ assert!(engine.mailbox.records()[0].delivered_at.is_some());
1119
+ assert!(engine.mailbox.records()[1].delivered_at.is_some());
1120
+ }
1121
+
1122
+ #[test]
1123
+ fn mailbox_team_scope_survives_reload_and_replay_and_guards_mutations() {
1124
+ let dir =
1125
+ std::env::temp_dir().join(format!("omx-mailbox-team-scope-{}", std::process::id()));
1126
+ let _ = std::fs::remove_dir_all(&dir);
1127
+ let mut engine = RuntimeEngine::new().with_state_dir(&dir);
1128
+ engine
1129
+ .process(RuntimeCommand::CreateMailboxMessage {
1130
+ team_name: Some("alpha".into()),
1131
+ message_id: "scoped".into(),
1132
+ from_worker: "leader-fixed".into(),
1133
+ to_worker: "worker-1".into(),
1134
+ body: "alpha only".into(),
1135
+ })
1136
+ .unwrap();
1137
+ engine.persist().unwrap();
1138
+ let mut loaded = RuntimeEngine::load(&dir).unwrap();
1139
+ assert_eq!(
1140
+ loaded.mailbox.records()[0].team_name.as_deref(),
1141
+ Some("alpha")
1142
+ );
1143
+ for team_name in [None, Some("beta".to_string())] {
1144
+ assert!(loaded
1145
+ .process(RuntimeCommand::MarkMailboxNotified {
1146
+ team_name: team_name.clone(),
1147
+ message_id: "scoped".into(),
1148
+ })
1149
+ .is_err());
1150
+ assert!(loaded
1151
+ .process(RuntimeCommand::MarkMailboxDelivered {
1152
+ team_name,
1153
+ message_id: "scoped".into(),
1154
+ })
1155
+ .is_err());
1156
+ }
1157
+ assert!(loaded.mailbox.records()[0].notified_at.is_none());
1158
+ assert!(loaded.mailbox.records()[0].delivered_at.is_none());
1159
+ std::fs::remove_file(dir.join("mailbox.json")).unwrap();
1160
+ let mut replayed = RuntimeEngine::load(&dir).unwrap();
1161
+ assert_eq!(
1162
+ replayed.mailbox.records()[0].team_name.as_deref(),
1163
+ Some("alpha")
1164
+ );
1165
+ replayed
1166
+ .process(RuntimeCommand::MarkMailboxNotified {
1167
+ team_name: Some("alpha".into()),
1168
+ message_id: "scoped".into(),
1169
+ })
1170
+ .unwrap();
1171
+ replayed
1172
+ .process(RuntimeCommand::MarkMailboxDelivered {
1173
+ team_name: Some("alpha".into()),
1174
+ message_id: "scoped".into(),
1175
+ })
1176
+ .unwrap();
1177
+ let _ = std::fs::remove_dir_all(&dir);
1178
+ }
1179
+
1180
+ #[test]
1181
+ fn mailbox_event_log_preserves_acknowledgement_over_a_stale_snapshot() {
1182
+ let dir =
1183
+ std::env::temp_dir().join(format!("omx-mailbox-stale-snapshot-{}", std::process::id()));
1184
+ let _ = std::fs::remove_dir_all(&dir);
1185
+ let mut engine = RuntimeEngine::new().with_state_dir(&dir);
1186
+ engine
1187
+ .process(RuntimeCommand::CreateMailboxMessage {
1188
+ team_name: Some("alpha".into()),
1189
+ message_id: "stale-snapshot".into(),
1190
+ from_worker: "leader-fixed".into(),
1191
+ to_worker: "worker-1".into(),
1192
+ body: "delivery survives a stale projection".into(),
1193
+ })
1194
+ .unwrap();
1195
+ engine.persist().unwrap();
1196
+ let pending_snapshot = std::fs::read_to_string(dir.join("mailbox.json")).unwrap();
1197
+
1198
+ engine
1199
+ .process(RuntimeCommand::MarkMailboxDelivered {
1200
+ team_name: Some("alpha".into()),
1201
+ message_id: "stale-snapshot".into(),
1202
+ })
1203
+ .unwrap();
1204
+ engine.persist().unwrap();
1205
+ std::fs::write(dir.join("mailbox.json"), pending_snapshot).unwrap();
1206
+
1207
+ let mut loaded = RuntimeEngine::load(&dir).unwrap();
1208
+ assert!(loaded.mailbox.records()[0].delivered_at.is_some());
1209
+ let recovered_timestamp = loaded.mailbox.records()[0].delivered_at.clone();
1210
+ loaded
1211
+ .process(RuntimeCommand::MarkMailboxDelivered {
1212
+ team_name: Some("alpha".into()),
1213
+ message_id: "stale-snapshot".into(),
1214
+ })
1215
+ .unwrap();
1216
+ loaded.persist().unwrap();
1217
+ let persisted: MailboxLog =
1218
+ serde_json::from_str(&std::fs::read_to_string(dir.join("mailbox.json")).unwrap())
1219
+ .unwrap();
1220
+ assert_eq!(persisted.records()[0].delivered_at, recovered_timestamp);
1221
+ assert_eq!(
1222
+ RuntimeEngine::load(&dir).unwrap().mailbox.records()[0].delivered_at,
1223
+ recovered_timestamp
1224
+ );
1225
+ let _ = std::fs::remove_dir_all(&dir);
1226
+ }
1227
+
782
1228
  #[test]
783
1229
  fn persist_and_load_round_trip_preserves_mailbox_body() {
784
1230
  let dir = std::env::temp_dir().join("omx-runtime-test-mailbox-body");
@@ -787,6 +1233,7 @@ mod tests {
787
1233
  let mut engine = RuntimeEngine::new().with_state_dir(&dir);
788
1234
  engine
789
1235
  .process(RuntimeCommand::CreateMailboxMessage {
1236
+ team_name: Some("alpha".into()),
790
1237
  message_id: "msg-1".into(),
791
1238
  from_worker: "worker-1".into(),
792
1239
  to_worker: "leader-fixed".into(),
@@ -817,6 +1264,7 @@ mod tests {
817
1264
  std::fs::write(
818
1265
  dir.join("events.json"),
819
1266
  serde_json::to_string_pretty(&vec![RuntimeEvent::MailboxMessageCreated {
1267
+ team_name: None,
820
1268
  message_id: "msg-legacy".into(),
821
1269
  from_worker: "worker-1".into(),
822
1270
  to_worker: "leader-fixed".into(),
@@ -868,6 +1316,160 @@ mod tests {
868
1316
  let _ = std::fs::remove_dir_all(&dir);
869
1317
  }
870
1318
 
1319
+ #[test]
1320
+ fn load_backfills_empty_snapshot_body_from_creation_event() {
1321
+ let dir = std::env::temp_dir().join(format!(
1322
+ "omx-runtime-test-mailbox-event-body-{}",
1323
+ std::process::id()
1324
+ ));
1325
+ let _ = std::fs::remove_dir_all(&dir);
1326
+ std::fs::create_dir_all(&dir).unwrap();
1327
+ std::fs::write(
1328
+ dir.join("events.json"),
1329
+ serde_json::to_string_pretty(&vec![RuntimeEvent::MailboxMessageCreated {
1330
+ team_name: Some("alpha".into()),
1331
+ message_id: "msg-scoped".into(),
1332
+ from_worker: "worker-1".into(),
1333
+ to_worker: "leader-fixed".into(),
1334
+ body: Some("event body".into()),
1335
+ }])
1336
+ .unwrap(),
1337
+ )
1338
+ .unwrap();
1339
+ std::fs::write(
1340
+ dir.join("mailbox.json"),
1341
+ serde_json::to_string_pretty(&serde_json::json!({
1342
+ "records": [{
1343
+ "team_name": "alpha",
1344
+ "message_id": "msg-scoped",
1345
+ "from_worker": "worker-1",
1346
+ "to_worker": "leader-fixed",
1347
+ "body": "",
1348
+ "created_at": "2026-04-04T00:00:00.000Z",
1349
+ "notified_at": null,
1350
+ "delivered_at": null
1351
+ }]
1352
+ }))
1353
+ .unwrap(),
1354
+ )
1355
+ .unwrap();
1356
+ std::fs::write(
1357
+ dir.join(DISPATCH_SEEN_LEDGER_FILE),
1358
+ r#"{"schema_version":2,"ledger_epoch":1,"request_ids":[]}"#,
1359
+ )
1360
+ .unwrap();
1361
+ std::fs::write(dir.join("engine.lock"), "").unwrap();
1362
+
1363
+ let loaded = RuntimeEngine::load(&dir).unwrap();
1364
+ loaded.persist().unwrap();
1365
+ let mailbox_json = std::fs::read_to_string(dir.join("mailbox.json")).unwrap();
1366
+ let mailbox: serde_json::Value = serde_json::from_str(&mailbox_json).unwrap();
1367
+ assert_eq!(mailbox["records"][0]["body"], "event body");
1368
+ let _ = std::fs::remove_dir_all(&dir);
1369
+ }
1370
+
1371
+ #[test]
1372
+ fn load_rejects_an_unreadable_mailbox_snapshot() {
1373
+ let dir = std::env::temp_dir().join(format!(
1374
+ "omx-runtime-test-mailbox-corrupt-{}",
1375
+ std::process::id()
1376
+ ));
1377
+ let _ = std::fs::remove_dir_all(&dir);
1378
+ std::fs::create_dir_all(&dir).unwrap();
1379
+ std::fs::write(
1380
+ dir.join("events.json"),
1381
+ serde_json::to_string_pretty(&vec![RuntimeEvent::MailboxMessageCreated {
1382
+ team_name: None,
1383
+ message_id: "msg-legacy".into(),
1384
+ from_worker: "worker-1".into(),
1385
+ to_worker: "leader-fixed".into(),
1386
+ body: None,
1387
+ }])
1388
+ .unwrap(),
1389
+ )
1390
+ .unwrap();
1391
+ std::fs::write(dir.join("mailbox.json"), "{").unwrap();
1392
+ std::fs::write(
1393
+ dir.join(DISPATCH_SEEN_LEDGER_FILE),
1394
+ r#"{"schema_version":2,"ledger_epoch":1,"request_ids":[]}"#,
1395
+ )
1396
+ .unwrap();
1397
+ std::fs::write(dir.join("engine.lock"), "").unwrap();
1398
+
1399
+ assert!(RuntimeEngine::load(&dir).is_err());
1400
+ assert_eq!(
1401
+ std::fs::read_to_string(dir.join("mailbox.json")).unwrap(),
1402
+ "{"
1403
+ );
1404
+ let _ = std::fs::remove_dir_all(&dir);
1405
+ }
1406
+
1407
+ #[test]
1408
+ fn mailbox_body_backfill_respects_team_and_rejects_duplicate_keys() {
1409
+ let dir =
1410
+ std::env::temp_dir().join(format!("omx-mailbox-backfill-scope-{}", std::process::id()));
1411
+ let _ = std::fs::remove_dir_all(&dir);
1412
+ let mut engine = RuntimeEngine::new().with_state_dir(&dir);
1413
+ for (team_name, message_id, body) in [
1414
+ (None, "collision", "legacy"),
1415
+ (Some("beta"), "collision", "beta secret"),
1416
+ (Some("alpha"), "collision", "alpha secret"),
1417
+ (Some("alpha"), "collision", "ambiguous alpha"),
1418
+ (None, "duplicate-legacy", "first legacy"),
1419
+ (None, "duplicate-legacy", "second legacy"),
1420
+ (Some("gamma"), "scoped-only", "gamma secret"),
1421
+ ] {
1422
+ let event = RuntimeEvent::MailboxMessageCreated {
1423
+ team_name: team_name.map(String::from),
1424
+ message_id: message_id.into(),
1425
+ from_worker: "leader-fixed".into(),
1426
+ to_worker: "worker-1".into(),
1427
+ body: Some(body.into()),
1428
+ };
1429
+ replay_event(&mut engine, &event).unwrap();
1430
+ engine.event_log.push(event);
1431
+ }
1432
+ engine.persist().unwrap();
1433
+ for event in &mut engine.event_log {
1434
+ if let RuntimeEvent::MailboxMessageCreated {
1435
+ team_name,
1436
+ message_id,
1437
+ body,
1438
+ ..
1439
+ } = event
1440
+ {
1441
+ *body = None;
1442
+ if message_id == "scoped-only" {
1443
+ *team_name = None;
1444
+ }
1445
+ }
1446
+ }
1447
+ engine.persist().unwrap();
1448
+ let loaded = RuntimeEngine::load(&dir).unwrap();
1449
+ loaded.persist().unwrap();
1450
+ let expected = [
1451
+ Some("legacy"),
1452
+ Some("beta secret"),
1453
+ None,
1454
+ None,
1455
+ None,
1456
+ None,
1457
+ None,
1458
+ ];
1459
+ assert_eq!(loaded.event_log.len(), expected.len());
1460
+ for (event, expected_body) in loaded.event_log.iter().zip(expected) {
1461
+ assert!(
1462
+ matches!(event, RuntimeEvent::MailboxMessageCreated {body, ..} if body.as_deref() == expected_body)
1463
+ );
1464
+ }
1465
+ std::fs::remove_file(dir.join("mailbox.json")).unwrap();
1466
+ let replayed = RuntimeEngine::load(&dir).unwrap();
1467
+ for (record, expected_body) in replayed.mailbox.records().iter().zip(expected) {
1468
+ assert_eq!(record.body, expected_body.unwrap_or(""));
1469
+ }
1470
+ let _ = std::fs::remove_dir_all(&dir);
1471
+ }
1472
+
871
1473
  #[test]
872
1474
  fn derive_readiness_stale_authority() {
873
1475
  let mut authority = AuthorityLease::new();