switchroom 0.19.22 → 0.19.23

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 (37) hide show
  1. package/dist/agent-scheduler/index.js +2 -1
  2. package/dist/auth-broker/index.js +68 -1
  3. package/dist/cli/notion-write-pretool.mjs +2 -1
  4. package/dist/cli/switchroom.js +552 -320
  5. package/dist/host-control/main.js +69 -2
  6. package/dist/vault/approvals/kernel-server.js +71 -4
  7. package/dist/vault/broker/server.js +71 -4
  8. package/package.json +5 -4
  9. package/profiles/_base/start.sh.hbs +101 -0
  10. package/profiles/_shared/agent-self-service.md.hbs +64 -109
  11. package/profiles/_shared/delegation-golden-rule.md.hbs +5 -5
  12. package/profiles/_shared/dev-protocol.md.hbs +13 -42
  13. package/profiles/_shared/execution-discipline.md.hbs +7 -14
  14. package/profiles/coding/CLAUDE.md.hbs +0 -6
  15. package/profiles/default/CLAUDE.md.hbs +21 -50
  16. package/skills/dev-protocol/SKILL.md +90 -107
  17. package/telegram-plugin/bunfig.toml +10 -0
  18. package/telegram-plugin/dist/gateway/gateway.js +108 -16
  19. package/telegram-plugin/gateway/backstop-delivery.ts +97 -16
  20. package/telegram-plugin/gateway/captured-answer-resume.ts +46 -17
  21. package/telegram-plugin/gateway/gateway.ts +9 -7
  22. package/telegram-plugin/gateway/outbound-send-path.ts +8 -1
  23. package/telegram-plugin/gateway/stream-render.ts +6 -0
  24. package/telegram-plugin/gateway/turn-record-status.ts +19 -0
  25. package/telegram-plugin/gateway/turns-jsonl-rotate.ts +65 -0
  26. package/telegram-plugin/tests/agent-state-dir-preload.test.ts +33 -0
  27. package/telegram-plugin/tests/backstop-delivery.test.ts +204 -7
  28. package/telegram-plugin/tests/backstop-readback-probe.test.ts +12 -0
  29. package/telegram-plugin/tests/captured-answer-resume.test.ts +104 -0
  30. package/telegram-plugin/tests/turns-jsonl-rotate.test.ts +92 -1
  31. package/vendor/hindsight-memory/scripts/drain_pending.py +113 -11
  32. package/vendor/hindsight-memory/scripts/lib/pending.py +802 -65
  33. package/vendor/hindsight-memory/scripts/lib/retain_split.py +54 -7
  34. package/vendor/hindsight-memory/scripts/tests/test_pending_drops.py +1445 -11
  35. package/vendor/hindsight-memory/scripts/tests/test_retain_split.py +78 -6
  36. package/vendor/hindsight-memory/tests/test_drain_pending.py +17 -2
  37. package/vendor/hindsight-memory/tests/test_pending.py +12 -4
@@ -17,6 +17,7 @@ otherwise undo:
17
17
  * the stall guard does not overshoot under concurrency.
18
18
  """
19
19
 
20
+ import errno
20
21
  import io
21
22
  import json
22
23
  import os
@@ -95,6 +96,9 @@ class _QueueTempDirMixin:
95
96
  "HINDSIGHT_RETAIN_CLIENT_DEADLINE_S",
96
97
  # The retain content bound, which decides whether `enqueue` splits.
97
98
  "HINDSIGHT_RETAIN_MAX_CONTENT_CHARS",
99
+ "HINDSIGHT_PENDING_DUPLICATE_DIR",
100
+ "HINDSIGHT_PENDING_DEAD_DIR",
101
+ "HINDSIGHT_PENDING_RESPLIT_DIR",
98
102
  )
99
103
 
100
104
  def setUp(self):
@@ -253,9 +257,9 @@ class EvictionTest(_QueueTempDirMixin, unittest.TestCase):
253
257
  def test_eviction_is_logged_to_the_ledger(self):
254
258
  """Eviction is not silent loss, but it IS loss -- doctor reads this."""
255
259
  pending.MAX_ENTRIES = 1
256
- pending.enqueue(_payload(doc="d0"), RuntimeError("x"))
260
+ pending.enqueue(_payload(content="m0", doc="d0"), RuntimeError("x"))
257
261
  with redirect_stderr(io.StringIO()):
258
- pending.enqueue(_payload(doc="d1"), RuntimeError("x"))
262
+ pending.enqueue(_payload(content="m1", doc="d1"), RuntimeError("x"))
259
263
  with open(pending.evictions_log_path(), encoding="utf-8") as f:
260
264
  line = f.read().strip()
261
265
  self.assertIn("evicted=", line)
@@ -290,7 +294,9 @@ class EvictionTest(_QueueTempDirMixin, unittest.TestCase):
290
294
  with unittest.mock.patch.object(pending.time, "time", lambda: next(clock)):
291
295
  with redirect_stderr(io.StringIO()):
292
296
  paths = [
293
- pending.enqueue(_payload(doc=f"d{i}"), RuntimeError("x"))
297
+ pending.enqueue(
298
+ _payload(content=f"m{i}", doc=f"d{i}"), RuntimeError("x")
299
+ )
294
300
  for i in range(5)
295
301
  ]
296
302
  evicted_names = [os.path.basename(p) for p in paths[:-1]]
@@ -320,9 +326,9 @@ class EvictionTest(_QueueTempDirMixin, unittest.TestCase):
320
326
  prev = (pending.EVICTIONS_LOG_MAX_BYTES, pending.EVICTIONS_LOG_KEEP_LINES)
321
327
  pending.EVICTIONS_LOG_MAX_BYTES, pending.EVICTIONS_LOG_KEEP_LINES = 1, 3
322
328
  try:
323
- pending.enqueue(_payload(doc="d0"), RuntimeError("x"))
329
+ pending.enqueue(_payload(content="m0", doc="d0"), RuntimeError("x"))
324
330
  with redirect_stderr(io.StringIO()):
325
- pending.enqueue(_payload(doc="d1"), RuntimeError("x"))
331
+ pending.enqueue(_payload(content="m1", doc="d1"), RuntimeError("x"))
326
332
  finally:
327
333
  pending.EVICTIONS_LOG_MAX_BYTES, pending.EVICTIONS_LOG_KEEP_LINES = prev
328
334
 
@@ -476,14 +482,341 @@ class DedupeTest(_QueueTempDirMixin, unittest.TestCase):
476
482
  pending.enqueue(_payload(bank="bank-b", content="same"), RuntimeError("x"))
477
483
  self.assertEqual(pending.count(), 2)
478
484
 
479
- def test_entry_without_document_id_is_always_kept(self):
480
- """No document_id => identity cannot be established => never merge."""
485
+ def test_entry_without_content_is_always_kept(self):
486
+ """No content => identity cannot be established => never merge.
487
+
488
+ This used to key off ``document_id``. It does not any more
489
+ (switchroom #3688): the id varies per enqueue for the producer that
490
+ dominates this queue, so it was never a usable identity. Content is.
491
+ """
481
492
  p = _payload()
482
- p.pop("document_id")
493
+ p.pop("content")
483
494
  pending.enqueue(dict(p), RuntimeError("x"))
484
495
  pending.enqueue(dict(p), RuntimeError("x"))
485
496
  self.assertEqual(pending.count(), 2)
486
497
 
498
+ def test_entry_without_document_id_still_dedupes_on_content(self):
499
+ """The id is not required for identity — the content carries it."""
500
+ p = _payload(content="same")
501
+ p.pop("document_id")
502
+ a = pending.enqueue(dict(p), RuntimeError("x"))
503
+ b = pending.enqueue(dict(p), RuntimeError("x"))
504
+ self.assertEqual(a, b)
505
+ self.assertEqual(pending.count(), 1)
506
+
507
+ def test_same_content_under_a_fresh_document_id_is_ONE_entry(self):
508
+ """THE bug (switchroom #3688), pinned as an outcome.
509
+
510
+ Measured on the live fleet 2026-07-26: 1,060 queued files, ~368
511
+ distinct ``(bank_id, part_position, sha256(content))`` groups. The top
512
+ group held
513
+ 32 files carrying ONE byte-identical 45,000-char part under 32
514
+ DIFFERENT document ids, because ``subagent_retain.py`` stamps the
515
+ sub-agent's own session id into the id
516
+ (``{parent}-sub-{agent_id}-r{start}-{end}``) and every SubagentStop
517
+ mints a new one. With ``document_id`` in the dedupe key not one of
518
+ those 32 matched, so the queue refilled itself faster than it
519
+ drained and the same LLM extraction was paid for 32 times.
520
+
521
+ Restore ``document_id`` to ``_dupe_key`` and this goes red.
522
+ """
523
+ parent = "34c4bbeb-f805-4dd3-b085-46ac2eb57ae9"
524
+ slice_ids = f"r{_uuid(7)}-{_uuid(8)}"
525
+ content = "the same sub-agent work log, byte for byte" * 50
526
+ for agent_id in ("aa2170a03a351e0", "a8857997182e1e9", "a836dfc39b5eb94"):
527
+ pending.enqueue(
528
+ _payload(content=content, doc=f"{parent}-sub-{agent_id}-{slice_ids}"),
529
+ RuntimeError("timed out"),
530
+ )
531
+ self.assertEqual(
532
+ pending.count(),
533
+ 1,
534
+ "re-enqueuing identical content under a fresh document_id must be "
535
+ "a no-op, not a new queue entry",
536
+ )
537
+
538
+ def test_the_surviving_entry_keeps_a_usable_document_id(self):
539
+ """Dedupe must not leave an entry the drain cannot act on."""
540
+ doc = _cd_doc(3)
541
+ pending.enqueue(_payload(content="c", doc=doc), RuntimeError("x"))
542
+ pending.enqueue(_payload(content="c", doc=_cd_doc(4)), RuntimeError("x"))
543
+ (_, entry), = pending.iter_entries()
544
+ self.assertEqual(entry["document_id"], doc, "the FIRST entry survives")
545
+ self.assertTrue(pending.is_content_derived_document_id(entry["document_id"]))
546
+
547
+ def test_identical_parts_of_ONE_split_are_never_merged(self):
548
+ """The parts of a split are positions, not copies.
549
+
550
+ ``split_retain_content`` cuts on a character bound, so repetitive
551
+ content yields byte-identical parts. Merging them would leave the
552
+ document missing every position but one — content loss, not a
553
+ redundant copy. Drop ``_part_position`` from the key and this goes
554
+ red: 10 parts collapse to 1.
555
+ """
556
+ pending.MAX_ENTRIES = 20
557
+ pending.MAX_BYTES = 10**9
558
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "3000"
559
+ try:
560
+ pending.enqueue(
561
+ _payload(content="z" * 30_000, doc=_cd_doc(5)), RuntimeError("x")
562
+ )
563
+ finally:
564
+ os.environ.pop("HINDSIGHT_RETAIN_MAX_CONTENT_CHARS", None)
565
+
566
+ entries = [e for _p, e in pending.iter_entries()]
567
+ self.assertEqual(len(entries), 10, "one queued entry per position")
568
+ self.assertEqual(
569
+ len({e["content"] for e in entries}),
570
+ 1,
571
+ "fixture check: the parts really are byte-identical",
572
+ )
573
+
574
+ def test_the_same_position_under_a_GROWING_total_still_dedupes(self):
575
+ """The measured 32x group carried -p7of15, -p7of16 and -p7of18.
576
+
577
+ The enclosing transcript kept growing between SubagentStops while
578
+ part 7 itself did not change. Key on the total as well as the index
579
+ and that group splits three ways and collapses nothing.
580
+ """
581
+ base = _cd_doc(6)
582
+ for total in (15, 16, 18):
583
+ pending.enqueue(
584
+ _payload(content="part-seven", doc=f"{base}-p7of{total}"),
585
+ RuntimeError("x"),
586
+ )
587
+ self.assertEqual(pending.count(), 1, "one memory, one queue entry")
588
+
589
+ def test_a_part_position_is_read_off_the_id_not_the_metadata(self):
590
+ """Entries queued by older builds may carry no part metadata."""
591
+ self.assertEqual(pending._part_position({"document_id": "abc"}), "")
592
+ self.assertEqual(pending._part_position({"document_id": "abc-p7of16"}), "7")
593
+ self.assertEqual(
594
+ pending._part_position({"document_id": "abc-p2of5-p1of2"}),
595
+ "2.1",
596
+ "innermost split reads last",
597
+ )
598
+ self.assertEqual(pending._part_position({"document_id": None}), "")
599
+
600
+
601
+ class CollapseDuplicatesTest(_QueueTempDirMixin, unittest.TestCase):
602
+ """``collapse_duplicates`` — the other half of content-keyed identity.
603
+
604
+ ``enqueue``'s filename key stops NEW duplicates. It cannot touch the
605
+ ones already on disk: those were written with the old id-bearing key, so
606
+ a fresh enqueue of their content computes a different key and never
607
+ matches them. This pass recomputes identity from CONTENT, which makes it
608
+ generation-agnostic, and the backlog drain runs it before spending a
609
+ single LLM call.
610
+ """
611
+
612
+ def _duplicate_names(self):
613
+ try:
614
+ return sorted(os.listdir(pending.duplicate_dir()))
615
+ except OSError:
616
+ return []
617
+
618
+ def _write_legacy(self, name, content, bank="bank-a", doc=None, attempts=1):
619
+ """Write an entry the way an OLDER build would have named it.
620
+
621
+ ``<unix-ms>-<uuid>.json`` with no key segment at all — the shape
622
+ that predates #3596 — so nothing about the filename can be doing
623
+ the identity work in these cases.
624
+ """
625
+ os.makedirs(self._dir, mode=0o700, exist_ok=True)
626
+ entry = dict(_payload(bank=bank, content=content, doc=doc or _cd_doc(1)))
627
+ entry["attempt_count"] = attempts
628
+ p = os.path.join(self._dir, name)
629
+ with open(p, "w", encoding="utf-8") as f:
630
+ json.dump(entry, f)
631
+ return p
632
+
633
+ def test_duplicates_collapse_to_one_live_entry(self):
634
+ for i in range(5):
635
+ self._write_legacy(f"{1000 + i:013d}-aaaaaaaaaaa{i}.json", "same-memory")
636
+ self.assertEqual(pending.count(), 5)
637
+
638
+ with redirect_stderr(io.StringIO()):
639
+ collapsed = pending.collapse_duplicates()
640
+
641
+ self.assertEqual(collapsed, 4)
642
+ self.assertEqual(pending.count(), 1)
643
+
644
+ def test_collapse_archives_the_losers_and_never_deletes_them(self):
645
+ for i in range(3):
646
+ self._write_legacy(f"{1000 + i:013d}-aaaaaaaaaaa{i}.json", "same-memory")
647
+
648
+ with redirect_stderr(io.StringIO()):
649
+ pending.collapse_duplicates()
650
+
651
+ archived = self._duplicate_names()
652
+ self.assertEqual(len(archived), 2, "losers are MOVED, not removed")
653
+ for name in archived:
654
+ with open(os.path.join(pending.duplicate_dir(), name)) as f:
655
+ self.assertEqual(json.load(f)["content"], "same-memory")
656
+
657
+ def test_the_copy_with_the_fewest_attempts_survives(self):
658
+ """Durability-first survivor selection.
659
+
660
+ Every copy carries identical content, so any of them delivers the
661
+ same memory — but only the one with attempts left can still get
662
+ there before MAX_ATTEMPTS. Keeping the OLDEST outright would
663
+ systematically keep the most-attempted copy, which is backwards.
664
+ """
665
+ self._write_legacy("0000000001000-aaaaaaaaaaa0.json", "same", attempts=4)
666
+ keep = self._write_legacy("0000000002000-aaaaaaaaaaa1.json", "same", attempts=1)
667
+ self._write_legacy("0000000003000-aaaaaaaaaaa2.json", "same", attempts=3)
668
+
669
+ with redirect_stderr(io.StringIO()):
670
+ pending.collapse_duplicates()
671
+
672
+ survivors = [p for p, _ in pending.iter_entries()]
673
+ self.assertEqual(survivors, [keep])
674
+
675
+ def test_distinct_memories_are_never_collapsed(self):
676
+ self._write_legacy("0000000001000-aaaaaaaaaaa0.json", "memory-one")
677
+ self._write_legacy("0000000002000-aaaaaaaaaaa1.json", "memory-two")
678
+ self._write_legacy("0000000003000-aaaaaaaaaaa2.json", "memory-three")
679
+
680
+ with redirect_stderr(io.StringIO()):
681
+ self.assertEqual(pending.collapse_duplicates(), 0)
682
+ self.assertEqual(pending.count(), 3)
683
+
684
+ def test_identical_parts_of_ONE_split_are_never_collapsed(self):
685
+ """The backlog is full of split parts; do not eat a document's tail.
686
+
687
+ A legacy 3-part entry whose parts happen to be byte-identical must
688
+ survive the sweep intact. Only the position keeps them apart.
689
+ """
690
+ base = _cd_doc(2)
691
+ for i in range(1, 4):
692
+ self._write_legacy(
693
+ f"000000000{i}000-aaaaaaaaaaa{i}.json",
694
+ "zzz",
695
+ doc=f"{base}-p{i}of3",
696
+ )
697
+
698
+ with redirect_stderr(io.StringIO()):
699
+ self.assertEqual(pending.collapse_duplicates(), 0)
700
+ self.assertEqual(pending.count(), 3, "every position kept")
701
+ self.assertEqual(self._duplicate_names(), [], "nothing archived")
702
+
703
+ def test_the_same_position_from_different_runs_IS_collapsed(self):
704
+ """The complement: same index, different totals and ids -> one entry."""
705
+ for i, total in enumerate((15, 16, 18), start=1):
706
+ self._write_legacy(
707
+ f"000000000{i}000-aaaaaaaaaaa{i}.json",
708
+ "zzz",
709
+ doc=f"{_cd_doc(i)}-p7of{total}",
710
+ )
711
+
712
+ with redirect_stderr(io.StringIO()):
713
+ self.assertEqual(pending.collapse_duplicates(), 2)
714
+ self.assertEqual(pending.count(), 1)
715
+
716
+ def test_same_content_in_different_banks_is_never_collapsed(self):
717
+ self._write_legacy("0000000001000-aaaaaaaaaaa0.json", "same", bank="bank-a")
718
+ self._write_legacy("0000000002000-aaaaaaaaaaa1.json", "same", bank="bank-b")
719
+
720
+ with redirect_stderr(io.StringIO()):
721
+ self.assertEqual(pending.collapse_duplicates(), 0)
722
+ self.assertEqual(pending.count(), 2)
723
+
724
+ def test_entries_without_content_are_never_grouped(self):
725
+ os.makedirs(self._dir, mode=0o700, exist_ok=True)
726
+ for i in range(2):
727
+ p = os.path.join(self._dir, f"{1000 + i:013d}-aaaaaaaaaaa{i}.json")
728
+ e = dict(_payload())
729
+ e.pop("content")
730
+ with open(p, "w", encoding="utf-8") as f:
731
+ json.dump(e, f)
732
+
733
+ with redirect_stderr(io.StringIO()):
734
+ self.assertEqual(pending.collapse_duplicates(), 0)
735
+ self.assertEqual(pending.count(), 2)
736
+
737
+ def test_an_unarchivable_duplicate_stays_queued_and_is_not_counted(self):
738
+ """Archiving never falls back to a delete, and never over-reports."""
739
+ for i in range(3):
740
+ self._write_legacy(f"{1000 + i:013d}-aaaaaaaaaaa{i}.json", "same")
741
+
742
+ with unittest.mock.patch.object(
743
+ pending.shutil, "move", side_effect=OSError("ENOSPC")
744
+ ):
745
+ with redirect_stderr(io.StringIO()) as err:
746
+ collapsed = pending.collapse_duplicates()
747
+
748
+ self.assertEqual(collapsed, 0, "a retire that did not happen is not counted")
749
+ self.assertEqual(pending.count(), 3, "the entries are STILL QUEUED")
750
+ self.assertIn("STAYS QUEUED", err.getvalue())
751
+
752
+ def test_the_duplicate_archive_is_itself_bounded(self):
753
+ prev = pending.DUPLICATE_MAX_ENTRIES
754
+ pending.DUPLICATE_MAX_ENTRIES = 2
755
+ try:
756
+ for i in range(6):
757
+ self._write_legacy(f"{1000 + i:013d}-aaaaaaaaaaa{i}.json", "same")
758
+ with redirect_stderr(io.StringIO()):
759
+ pending.collapse_duplicates()
760
+ self.assertLessEqual(len(self._duplicate_names()), 2)
761
+ finally:
762
+ pending.DUPLICATE_MAX_ENTRIES = prev
763
+
764
+ def test_the_backlog_drain_collapses_before_paying_for_extraction(self):
765
+ """The user-visible outcome: one memory costs ONE extraction.
766
+
767
+ Thirty-two byte-identical copies is what the live queue actually
768
+ held. At the measured ~168 s per phase-2 extraction, draining them
769
+ all is ~90 minutes of shared LLM lane time to persist one memory.
770
+ """
771
+ for i in range(32):
772
+ self._write_legacy(f"{1000 + i:013d}-aaaaaaaaaaa{i:02d}.json", "one-memory")
773
+
774
+ posted = []
775
+ with unittest.mock.patch.object(
776
+ drain_pending, "_document_state", lambda e, timeout=30: bool(posted)
777
+ ):
778
+ with unittest.mock.patch.object(
779
+ drain_pending,
780
+ "_retry_one",
781
+ lambda e, timeout: posted.append(e["document_id"]),
782
+ ):
783
+ with redirect_stderr(io.StringIO()):
784
+ summary = drain_pending.drain_backlog(CONFIG)
785
+
786
+ self.assertEqual(summary["collapsed"], 31)
787
+ self.assertEqual(len(posted), 1, "one memory must cost exactly one retain")
788
+ self.assertEqual(pending.count(), 0)
789
+
790
+ def test_the_in_hook_drain_does_not_collapse(self):
791
+ """The SessionStart drain's contract is a hard latency ceiling.
792
+
793
+ Collapsing reads every queued entry to recompute identity from
794
+ content; that belongs out of hook. New duplicates cannot accumulate
795
+ in-hook anyway — ``enqueue``'s filename key stops those.
796
+ """
797
+ for i in range(4):
798
+ self._write_legacy(f"{1000 + i:013d}-aaaaaaaaaaa{i}.json", "same")
799
+
800
+ with unittest.mock.patch.object(
801
+ drain_pending, "_document_state", lambda e, timeout=30: None
802
+ ):
803
+ with unittest.mock.patch.object(
804
+ drain_pending, "_retry_one", lambda e, timeout: None
805
+ ):
806
+ with redirect_stderr(io.StringIO()):
807
+ summary = drain_pending.drain(CONFIG)
808
+
809
+ self.assertEqual(summary["collapsed"], 0)
810
+ self.assertEqual(len(self._duplicate_names()), 0)
811
+
812
+ def test_a_dry_run_collapses_nothing(self):
813
+ for i in range(3):
814
+ self._write_legacy(f"{1000 + i:013d}-aaaaaaaaaaa{i}.json", "same")
815
+ with redirect_stderr(io.StringIO()):
816
+ summary = drain_pending.drain_backlog(CONFIG, dry_run=True)
817
+ self.assertEqual(summary["collapsed"], 0)
818
+ self.assertEqual(pending.count(), 3)
819
+
487
820
 
488
821
  class DropLedgerTest(_QueueTempDirMixin, unittest.TestCase):
489
822
  """Residual drops -- the entry could not be written even after eviction."""
@@ -1091,8 +1424,12 @@ class BacklogDrainTest(_QueueTempDirMixin, unittest.TestCase):
1091
1424
  )
1092
1425
 
1093
1426
  self.assertEqual(summary["dead"], 1)
1094
- self.assertTrue(os.path.exists(path + ".dead"))
1427
+ # The marker lands in the pending-dead/ archive, NOT beside the live
1428
+ # entries -- see `pending.dead_dir`.
1429
+ self.assertTrue(os.path.exists(
1430
+ os.path.join(pending.dead_dir(), os.path.basename(path) + ".dead")))
1095
1431
  self.assertFalse(os.path.exists(path))
1432
+ self.assertFalse(os.path.exists(path + ".dead"))
1096
1433
 
1097
1434
  def test_extraction_500_past_max_attempts_never_kills_the_memory(self):
1098
1435
  """The regression this gate exists for.
@@ -1237,8 +1574,10 @@ class TransportFailureEndToEndTest(_QueueTempDirMixin, unittest.TestCase):
1237
1574
  path = self._exhaust_attempts()
1238
1575
  summary = self._drain_with_urlopen(self._http_error(400))
1239
1576
  self.assertEqual(summary["dead"], 1, "a 4xx rejection must still retire")
1240
- self.assertTrue(os.path.exists(path + ".dead"))
1577
+ self.assertTrue(os.path.exists(
1578
+ os.path.join(pending.dead_dir(), os.path.basename(path) + ".dead")))
1241
1579
  self.assertFalse(os.path.exists(path))
1580
+ self.assertFalse(os.path.exists(path + ".dead"))
1242
1581
 
1243
1582
 
1244
1583
  class ArchiveFailureNeverDeletesTest(_QueueTempDirMixin, unittest.TestCase):
@@ -1861,7 +2200,10 @@ class ClampAndEnvKnobBoundaryTest(_QueueTempDirMixin, unittest.TestCase):
1861
2200
  )
1862
2201
  self.assertEqual(
1863
2202
  retain_split.retain_content_limit(),
1864
- 3000 * int(600 // 18.4),
2203
+ # Same input, less the #3693 safety fraction: the bound is a
2204
+ # FRACTION of the deadline, not all of it, so a maximally-sized
2205
+ # part still has headroom when the drain waits exactly 600s.
2206
+ 3000 * int((600 * retain_split.retain_deadline_safety()) // 18.4),
1865
2207
  "and the content bound, from the same input",
1866
2208
  )
1867
2209
  finally:
@@ -2271,5 +2613,1097 @@ class EvictionsLogRotationBoundaryTest(_QueueTempDirMixin, unittest.TestCase):
2271
2613
  self.assertEqual(len(self._lines()), 1, "over the cap, rotation runs")
2272
2614
 
2273
2615
 
2616
+ class DeadMarkersLeaveTheLiveQueueTest(_QueueTempDirMixin, unittest.TestCase):
2617
+ """A `.dead` marker is the ONLY copy of its memory, so it must not sit
2618
+ in the directory external janitors sweep.
2619
+
2620
+ Live evidence: a host cron on this fleet ran
2621
+ ``find <queue> -name '*.dead' -mtime +14 -delete`` (6 markers were live
2622
+ in the queue when this was measured on 2026-07-26, each on a countdown
2623
+ to permanent deletion). Fixing that one cron is
2624
+ not a fix -- the next janitor has the same shape. These tests pin the
2625
+ product-level invariant instead: THE LIVE QUEUE DIRECTORY CONTAINS ONLY
2626
+ LIVE ENTRIES.
2627
+ """
2628
+
2629
+ def _dead_names(self):
2630
+ try:
2631
+ return sorted(os.listdir(pending.dead_dir()))
2632
+ except OSError:
2633
+ return []
2634
+
2635
+ def test_a_janitor_globbing_the_live_queue_cannot_match_a_memory(self):
2636
+ path = pending.enqueue(_payload(content="doomed"), RuntimeError("x"))
2637
+ pending.mark_dead(path, dict(_payload(content="doomed")))
2638
+
2639
+ # This is literally what the host cron matched on.
2640
+ leftovers = [n for n in os.listdir(self._dir) if n.endswith(".dead")]
2641
+ self.assertEqual(leftovers, [], "no .dead file may remain in the queue dir")
2642
+ self.assertEqual(len(self._dead_names()), 1, "and the memory still exists")
2643
+
2644
+ def test_the_marker_still_carries_the_whole_memory(self):
2645
+ path = pending.enqueue(_payload(content="precious"), RuntimeError("x"))
2646
+ marker = pending.mark_dead(path, dict(_payload(content="precious")))
2647
+ with open(marker, encoding="utf-8") as f:
2648
+ self.assertEqual(json.load(f)["content"], "precious")
2649
+ self.assertTrue(marker.startswith(pending.dead_dir()))
2650
+
2651
+ def test_an_unwritable_dead_archive_leaves_the_entry_QUEUED(self):
2652
+ """Failure to archive is never a licence to delete."""
2653
+ path = pending.enqueue(_payload(content="stays"), RuntimeError("x"))
2654
+ blocker = os.path.join(self._tmp, "blocked")
2655
+ with open(blocker, "w", encoding="utf-8") as f:
2656
+ f.write("not a directory")
2657
+ os.environ["HINDSIGHT_PENDING_DEAD_DIR"] = os.path.join(blocker, "dead")
2658
+
2659
+ self.assertIsNone(pending.mark_dead(path, dict(_payload(content="stays"))))
2660
+ self.assertTrue(os.path.exists(path), "the live entry must survive")
2661
+ self.assertEqual(pending.count(), 1)
2662
+
2663
+ def test_a_failed_REPLACE_also_leaves_the_entry_QUEUED(self):
2664
+ """The other half of "FAILURE IS NOT DELETION".
2665
+
2666
+ `test_an_unwritable_dead_archive_leaves_the_entry_QUEUED` fails at
2667
+ `os.makedirs`, so the tmp never exists and the cleanup block's
2668
+ `os.unlink(tmp)` raises before anything else in it can run -- which
2669
+ shields a stray `os.unlink(path)` placed AFTER it from ever being
2670
+ observed. Here the tmp write SUCCEEDS and only `os.replace` fails, so
2671
+ every statement in the cleanup block executes.
2672
+ """
2673
+ path = pending.enqueue(_payload(content="stays"), RuntimeError("x"))
2674
+ real_replace = os.replace
2675
+
2676
+ def _fail_replace(src, dst, *a, **kw):
2677
+ if str(dst).endswith(".dead"):
2678
+ raise OSError("replace refused")
2679
+ return real_replace(src, dst, *a, **kw)
2680
+
2681
+ with unittest.mock.patch("os.replace", side_effect=_fail_replace):
2682
+ self.assertIsNone(
2683
+ pending.mark_dead(path, dict(_payload(content="stays")))
2684
+ )
2685
+
2686
+ self.assertTrue(os.path.exists(path), "the live entry must survive")
2687
+ self.assertEqual(pending.count(), 1)
2688
+ self.assertEqual(self._dead_names(), [], "no marker, so no copy exists")
2689
+ # And the orphaned tmp really was cleaned up -- that is what the
2690
+ # `os.unlink(tmp)` in the cleanup block is FOR.
2691
+ self.assertEqual(
2692
+ [n for n in os.listdir(pending.dead_dir()) if n.endswith(".tmp")], []
2693
+ )
2694
+
2695
+ def test_legacy_markers_in_the_queue_dir_are_relocated_not_deleted(self):
2696
+ os.makedirs(self._dir, mode=0o700, exist_ok=True)
2697
+ legacy = []
2698
+ for i in range(3):
2699
+ name = os.path.join(self._dir, f"100{i}000-abc-{i}.json.dead")
2700
+ with open(name, "w", encoding="utf-8") as f:
2701
+ json.dump(_payload(content=f"old-{i}"), f)
2702
+ legacy.append(name)
2703
+
2704
+ self.assertEqual(pending.sweep_legacy_dead_markers(), 3)
2705
+ self.assertEqual(
2706
+ [n for n in os.listdir(self._dir) if n.endswith(".dead")], []
2707
+ )
2708
+ self.assertEqual(len(self._dead_names()), 3)
2709
+ for name in legacy:
2710
+ self.assertFalse(os.path.exists(name))
2711
+ # Content survived the move -- this is the whole point.
2712
+ moved = os.path.join(pending.dead_dir(), self._dead_names()[0])
2713
+ with open(moved, encoding="utf-8") as f:
2714
+ self.assertEqual(json.load(f)["content"], "old-0")
2715
+
2716
+ def test_the_sweep_is_idempotent_and_never_overwrites(self):
2717
+ os.makedirs(self._dir, mode=0o700, exist_ok=True)
2718
+ name = os.path.join(self._dir, "1000000-abc-0.json.dead")
2719
+ with open(name, "w", encoding="utf-8") as f:
2720
+ json.dump(_payload(content="first"), f)
2721
+ self.assertEqual(pending.sweep_legacy_dead_markers(), 1)
2722
+ self.assertEqual(pending.sweep_legacy_dead_markers(), 0)
2723
+
2724
+ # A same-named marker reappearing must not clobber the archived one.
2725
+ with open(name, "w", encoding="utf-8") as f:
2726
+ json.dump(_payload(content="second"), f)
2727
+ self.assertEqual(pending.sweep_legacy_dead_markers(), 0)
2728
+ with open(os.path.join(pending.dead_dir(), os.path.basename(name)),
2729
+ encoding="utf-8") as f:
2730
+ self.assertEqual(json.load(f)["content"], "first")
2731
+
2732
+ def test_the_sweep_leaves_live_entries_alone(self):
2733
+ pending.enqueue(_payload(content="alive"), RuntimeError("x"))
2734
+ self.assertEqual(pending.sweep_legacy_dead_markers(), 0)
2735
+ self.assertEqual(pending.count(), 1)
2736
+
2737
+ def test_a_dead_marker_is_STRUCTURALLY_UNTRIMMABLE(self):
2738
+ """The deliberate exception: a dead marker is the only copy left.
2739
+
2740
+ Every other archive holds a redundant copy and is capped. This one
2741
+ holds unrecovered memories, so a cap would be a delete by another
2742
+ name -- exactly the loss channel this change closes. The guarantee
2743
+ is not "we remember not to call the trim": `_trim_dir` selects
2744
+ `*.json` and a marker is `<entry>.json.dead`, so pointing a trim
2745
+ straight at the dead archive with a zero cap still removes nothing.
2746
+ """
2747
+ for i in range(40):
2748
+ path = pending.enqueue(_payload(content=f"dead-{i}"), RuntimeError("x"))
2749
+ pending.mark_dead(path, dict(_payload(content=f"dead-{i}")))
2750
+ self.assertEqual(len(self._dead_names()), 40)
2751
+
2752
+ with redirect_stderr(io.StringIO()):
2753
+ dropped = pending._trim_dir(pending.dead_dir(), 0, 0)
2754
+ self.assertEqual(dropped, 0)
2755
+ self.assertEqual(len(self._dead_names()), 40, "a memory was trimmed away")
2756
+
2757
+ def _write_legacy_markers(self, n: int) -> list:
2758
+ os.makedirs(self._dir, mode=0o700, exist_ok=True)
2759
+ names = []
2760
+ for i in range(n):
2761
+ name = f"100{i}000-abc-{i}.json.dead"
2762
+ with open(os.path.join(self._dir, name), "w", encoding="utf-8") as f:
2763
+ json.dump(_payload(content=f"old-{i}"), f)
2764
+ names.append(name)
2765
+ return names
2766
+
2767
+ def _legacy_leftovers(self):
2768
+ return sorted(n for n in os.listdir(self._dir) if n.endswith(".dead"))
2769
+
2770
+ def test_an_uncreatable_dead_archive_reports_ZERO_relocated(self):
2771
+ """The count is a claim about the LIVE QUEUE, not an intention.
2772
+
2773
+ Phase 0b turns it straight into `summary["dead_relocated"]` and the
2774
+ line "the queue now holds only live entries, so no janitor glob over
2775
+ it can match a memory". When `dead_dir()` cannot be created not one
2776
+ marker moved, so returning `len(names)` would print that sentence
2777
+ over a queue directory still holding every marker -- the exact
2778
+ condition this change exists to end, now with an operator told it was
2779
+ fixed.
2780
+ """
2781
+ self._write_legacy_markers(3)
2782
+
2783
+ with unittest.mock.patch.object(
2784
+ pending.os, "makedirs",
2785
+ side_effect=OSError(errno.EACCES, "Permission denied"),
2786
+ ):
2787
+ with redirect_stderr(io.StringIO()) as err:
2788
+ moved = pending.sweep_legacy_dead_markers()
2789
+
2790
+ self.assertEqual(moved, 0, "nothing moved, so nothing may be claimed")
2791
+ self.assertEqual(
2792
+ len(self._legacy_leftovers()), 3,
2793
+ "and every marker is still in the janitor's path",
2794
+ )
2795
+ self.assertEqual(self._dead_names(), [])
2796
+ self.assertIn("STAY in the live queue", err.getvalue())
2797
+
2798
+ def test_a_marker_that_could_not_be_MOVED_is_not_counted(self):
2799
+ """One marker per `except OSError`, and a `pass` there counts a lie.
2800
+
2801
+ The loop swallows a per-marker failure deliberately -- one unwritable
2802
+ marker must not abandon the other nine -- but swallowing it is not
2803
+ the same as having moved it. Dropping the `continue` leaves the
2804
+ marker in the live queue directory AND reports it relocated.
2805
+ """
2806
+ names = self._write_legacy_markers(2)
2807
+ real_move = shutil.move
2808
+
2809
+ def refuse_the_second(src, dst):
2810
+ if str(src).endswith(names[1]):
2811
+ raise OSError(errno.EACCES, "Permission denied")
2812
+ return real_move(src, dst)
2813
+
2814
+ with unittest.mock.patch.object(pending.shutil, "move", refuse_the_second):
2815
+ with redirect_stderr(io.StringIO()) as err:
2816
+ moved = pending.sweep_legacy_dead_markers()
2817
+
2818
+ self.assertEqual(moved, 1, "only the marker that actually moved counts")
2819
+ self.assertEqual(
2820
+ self._legacy_leftovers(), [names[1]],
2821
+ "the refused one is still in the live queue directory",
2822
+ )
2823
+ self.assertEqual(self._dead_names(), [names[0]])
2824
+ self.assertIn("relocated 1 legacy .dead marker(s)", err.getvalue())
2825
+
2826
+ def test_the_backlog_drain_relocates_legacy_markers(self):
2827
+ """The migration only happens if the drain actually calls it.
2828
+
2829
+ Nothing else on the fleet runs `sweep_legacy_dead_markers`, so an
2830
+ unwired phase 0b leaves every existing marker sitting in the janitor's
2831
+ path -- the exact condition this change exists to end. Asserting the
2832
+ function works in isolation does not catch that; this does.
2833
+ """
2834
+ path = pending.enqueue(_payload(content="legacy"), RuntimeError("x"))
2835
+ legacy = path + ".dead"
2836
+ os.replace(path, legacy)
2837
+
2838
+ summary = drain_pending.drain_backlog(CONFIG, phase="reconcile")
2839
+
2840
+ self.assertEqual(summary["dead_relocated"], 1)
2841
+ self.assertFalse(os.path.exists(legacy), "marker left in the live queue dir")
2842
+ self.assertEqual(self._dead_names(), [os.path.basename(legacy)])
2843
+
2844
+ def test_a_dry_run_relocates_nothing(self):
2845
+ path = pending.enqueue(_payload(content="legacy"), RuntimeError("x"))
2846
+ legacy = path + ".dead"
2847
+ os.replace(path, legacy)
2848
+
2849
+ summary = drain_pending.drain_backlog(CONFIG, phase="reconcile", dry_run=True)
2850
+
2851
+ self.assertEqual(summary["dead_relocated"], 0)
2852
+ self.assertTrue(os.path.exists(legacy))
2853
+
2854
+
2855
+ class ResplitOverBoundEntriesTest(_QueueTempDirMixin, unittest.TestCase):
2856
+ """An entry over the retain bound is RECOVERABLE, so it must never die.
2857
+
2858
+ Such an entry needs more sequential extraction calls than fit the client
2859
+ deadline, so every POST is guaranteed waste; if the server rejects the
2860
+ body as a 4xx it is (correctly) classified permanent and the memory goes
2861
+ `.dead`. Splitting it is the difference between a lost memory and a slow
2862
+ one. Two ways one gets into a queue and both are live on this fleet: an
2863
+ older build enqueued it unsplit, or the BOUND MOVED under it.
2864
+ """
2865
+
2866
+ def _resplit_names(self):
2867
+ try:
2868
+ return sorted(os.listdir(pending.resplit_dir()))
2869
+ except OSError:
2870
+ return []
2871
+
2872
+ def test_an_over_bound_entry_becomes_drainable_parts(self):
2873
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "100000"
2874
+ path = pending.enqueue(_payload(content="z" * 90_000), RuntimeError("x"))
2875
+ self.assertEqual(pending.count(), 1, "queued unsplit under the old bound")
2876
+
2877
+ # The bound MOVES -- exactly what a deadline or safety-factor change
2878
+ # does. The entry is now unretainable as-is.
2879
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "3000"
2880
+ entries, parts = pending.resplit_over_bound_entries()
2881
+
2882
+ self.assertEqual(entries, 1)
2883
+ self.assertGreaterEqual(parts, 30)
2884
+ self.assertFalse(os.path.exists(path), "the original left the queue")
2885
+ self.assertEqual(len(self._resplit_names()), 1, "archived, not deleted")
2886
+ for _p, entry in pending.iter_entries():
2887
+ self.assertLessEqual(len(entry["content"]), 3000)
2888
+
2889
+ def test_no_line_of_the_memory_is_lost_across_the_resplit(self):
2890
+ """Every line survives -- that is the guarantee that matters.
2891
+
2892
+ `split_retain_content` promises to lose no MESSAGE, not to be
2893
+ byte-exact: it splits on line boundaries and the boundary newline
2894
+ itself is not carried into either part (measured: 29 newlines lost
2895
+ over a 30-way split). That is a pre-existing property of the splitter
2896
+ that the LIVE enqueue path already has; this test pins the property a
2897
+ re-split must not go beyond -- no line of the memory may vanish.
2898
+
2899
+ Order is deliberately not asserted: the parts are independent queue
2900
+ entries whose filenames carry a random suffix, so `iter_entries()`
2901
+ returns them in filename order, not part order. Reassembly is by the
2902
+ part metadata, not by queue position.
2903
+ """
2904
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "100000"
2905
+ body = "".join(f"line {i}\n" for i in range(9000))
2906
+ pending.enqueue(_payload(content=body), RuntimeError("x"))
2907
+
2908
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "3000"
2909
+ pending.resplit_over_bound_entries()
2910
+ rejoined = "\n".join(e["content"] for _p, e in pending.iter_entries())
2911
+ surviving = [ln for ln in rejoined.split("\n") if ln]
2912
+ expected = [f"line {i}" for i in range(9000)]
2913
+ # Cheap, readable failure first: name the lines that went missing
2914
+ # rather than dumping a 9000-element list diff.
2915
+ missing = sorted(set(expected) - set(surviving))
2916
+ self.assertEqual(missing[:10], [], f"{len(missing)} line(s) lost in the re-split")
2917
+ self.assertEqual(len(surviving), len(expected), "a line was duplicated or dropped")
2918
+
2919
+ def test_the_resplit_archive_is_itself_bounded(self):
2920
+ """The archived original is redundant, so it must not eat the disk.
2921
+
2922
+ It is only moved AFTER its parts are queued, so unlike
2923
+ `pending-dead/` (which holds the only copy of an unrecovered memory
2924
+ and is deliberately uncapped) this archive can be trimmed on the
2925
+ same terms as `pending-duplicate/`.
2926
+ """
2927
+ prev = pending.RESPLIT_MAX_ENTRIES
2928
+ pending.RESPLIT_MAX_ENTRIES = 2
2929
+ try:
2930
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "100000"
2931
+ for i in range(5):
2932
+ pending.enqueue(_payload(content=f"{i}" * 40_000), RuntimeError("x"))
2933
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "3000"
2934
+ with redirect_stderr(io.StringIO()):
2935
+ resplit, _parts = pending.resplit_over_bound_entries()
2936
+ self.assertEqual(resplit, 5)
2937
+ self.assertLessEqual(len(self._resplit_names()), 2)
2938
+ finally:
2939
+ pending.RESPLIT_MAX_ENTRIES = prev
2940
+
2941
+ def test_the_archived_original_is_still_readable(self):
2942
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "100000"
2943
+ pending.enqueue(_payload(content="q" * 50_000), RuntimeError("x"))
2944
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "3000"
2945
+ pending.resplit_over_bound_entries()
2946
+
2947
+ archived = os.path.join(pending.resplit_dir(), self._resplit_names()[0])
2948
+ with open(archived, encoding="utf-8") as f:
2949
+ self.assertEqual(len(json.load(f)["content"]), 50_000)
2950
+
2951
+ def test_an_entry_within_the_bound_is_untouched(self):
2952
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "100000"
2953
+ path = pending.enqueue(_payload(content="small"), RuntimeError("x"))
2954
+ self.assertEqual(pending.resplit_over_bound_entries(), (0, 0))
2955
+ self.assertTrue(os.path.exists(path))
2956
+ self.assertEqual(self._resplit_names(), [])
2957
+
2958
+ def test_resplitting_is_idempotent(self):
2959
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "100000"
2960
+ pending.enqueue(_payload(content="w" * 40_000), RuntimeError("x"))
2961
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "3000"
2962
+ pending.resplit_over_bound_entries()
2963
+ depth = pending.count()
2964
+ self.assertEqual(pending.resplit_over_bound_entries(), (0, 0))
2965
+ self.assertEqual(pending.count(), depth, "a second pass is a no-op")
2966
+
2967
+ def test_an_entry_that_cannot_be_split_STAYS_QUEUED(self):
2968
+ """No part written => the original is left exactly where it was."""
2969
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "100000"
2970
+ path = pending.enqueue(_payload(content="v" * 40_000), RuntimeError("x"))
2971
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "3000"
2972
+
2973
+ with unittest.mock.patch.object(pending, "_enqueue_one", return_value=None):
2974
+ entries, parts = pending.resplit_over_bound_entries()
2975
+
2976
+ self.assertEqual((entries, parts), (0, 0))
2977
+ self.assertTrue(os.path.exists(path), "never deleted on failure")
2978
+ self.assertEqual(self._resplit_names(), [])
2979
+
2980
+ #: The exact membership of `pending._ENTRY_ONLY_FIELDS`, spelled out
2981
+ #: here rather than read off the module under test.
2982
+ #:
2983
+ #: This literal is deliberate, and it is what makes the strip testable at
2984
+ #: all. Only THREE members are observable in the resulting parts:
2985
+ #: `attempt_count` (`_build_entry` uses `setdefault`), `last_attempt_at`
2986
+ #: and `dead_at` (`_build_entry` never writes them). The other four --
2987
+ #: `schema`, `failed_at`, `error_class`, `error_message` -- are assigned
2988
+ #: UNCONDITIONALLY by `_build_entry`, so deleting any of them from the
2989
+ #: frozenset changes no entry on disk and no outcome assertion can see
2990
+ #: it. They are in the set as defence-in-depth (see the comment on
2991
+ #: `_ENTRY_ONLY_FIELDS`), and defence-in-depth that is asserted against
2992
+ #: the module's own value is asserted against nothing.
2993
+ ENTRY_ONLY_FIELDS = frozenset(
2994
+ {
2995
+ "schema",
2996
+ "failed_at",
2997
+ "error_class",
2998
+ "error_message",
2999
+ "attempt_count",
3000
+ "last_attempt_at",
3001
+ "dead_at",
3002
+ }
3003
+ )
3004
+
3005
+ def test_entry_only_fields_is_every_field_build_entry_adds(self):
3006
+ """Pin the membership itself; the strip's outcome cannot.
3007
+
3008
+ `_build_entry` is the only writer of the first four, and it writes
3009
+ them unconditionally -- so a member dropped from the frozenset is
3010
+ invisible in every part on disk. Asserting it here is the only
3011
+ thing that fails when one goes missing.
3012
+ """
3013
+ self.assertEqual(pending._ENTRY_ONLY_FIELDS, self.ENTRY_ONLY_FIELDS)
3014
+ built = pending._build_entry(_payload(), RuntimeError("x"))
3015
+ self.assertEqual(
3016
+ self.ENTRY_ONLY_FIELDS - set(_payload()) - {"last_attempt_at", "dead_at"},
3017
+ set(built) - set(_payload()),
3018
+ "the set drifted from what `_build_entry` actually stamps; "
3019
+ "`last_attempt_at`/`dead_at` come from `update_attempt`/`mark_dead`",
3020
+ )
3021
+
3022
+ def test_the_parts_carry_no_stale_attempt_metadata(self):
3023
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "100000"
3024
+ path = pending.enqueue(_payload(content="u" * 40_000), RuntimeError("x"))
3025
+ for _ in range(4):
3026
+ with open(path, encoding="utf-8") as f:
3027
+ entry = json.load(f)
3028
+ pending.update_attempt(path, entry, RuntimeError("again"))
3029
+ stale_stamp = "2020-01-01T00:00:00Z"
3030
+ with open(path, encoding="utf-8") as f:
3031
+ stale = json.load(f)
3032
+ stale["failed_at"] = stale_stamp
3033
+ stale["dead_at"] = stale_stamp
3034
+ with open(path, "w", encoding="utf-8") as f:
3035
+ json.dump(stale, f)
3036
+ # Guard the fixture: the assertions below are vacuous unless the
3037
+ # original really is carrying EVERY entry-only field, since what is
3038
+ # under test is that all of them are stripped off the payload.
3039
+ self.assertEqual(stale["attempt_count"], 5)
3040
+ self.assertIn("last_attempt_at", stale)
3041
+ self.assertEqual(stale["error_class"], "RuntimeError")
3042
+ self.assertEqual(stale["error_message"], "again")
3043
+ self.assertEqual(
3044
+ self.ENTRY_ONLY_FIELDS - set(stale), frozenset(),
3045
+ "the fixture must carry every entry-only field",
3046
+ )
3047
+
3048
+ # The load-bearing assertion for the four fields `_build_entry`
3049
+ # overwrites: catch the payload ON THE WAY IN. "The entry on disk has
3050
+ # a fresh `failed_at`" is true whether or not `failed_at` was ever
3051
+ # stripped; "the dict handed to `enqueue_parts` has no `failed_at`"
3052
+ # is not.
3053
+ seen: list[dict] = []
3054
+ real_enqueue_parts = pending.enqueue_parts
3055
+
3056
+ def spy(payload, error, **kwargs):
3057
+ seen.append(dict(payload))
3058
+ return real_enqueue_parts(payload, error, **kwargs)
3059
+
3060
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "3000"
3061
+ with unittest.mock.patch.object(pending, "enqueue_parts", spy):
3062
+ with redirect_stderr(io.StringIO()):
3063
+ pending.resplit_over_bound_entries()
3064
+
3065
+ self.assertEqual(len(seen), 1, "the over-bound entry was re-enqueued")
3066
+ self.assertEqual(
3067
+ sorted(set(seen[0]) & self.ENTRY_ONLY_FIELDS), [],
3068
+ "`enqueue()` takes a PAYLOAD, not an entry read off disk -- every "
3069
+ "entry-only field must be stripped before it goes back in",
3070
+ )
3071
+ self.assertEqual(
3072
+ seen[0]["content"], "u" * 40_000,
3073
+ "and the payload still carries the memory it is stripping around",
3074
+ )
3075
+
3076
+ entries = pending.iter_entries()
3077
+ self.assertGreaterEqual(len(entries), 13, "the entry really was split")
3078
+ for _p, entry in entries:
3079
+ # These three are the ones the strip alone decides: `_build_entry`
3080
+ # `setdefault`s `attempt_count` and never writes the other two.
3081
+ self.assertEqual(
3082
+ entry["attempt_count"], 1,
3083
+ "a re-split part starts its own budget -- inheriting an "
3084
+ "exhausted one would send it straight back toward .dead",
3085
+ )
3086
+ self.assertNotIn(
3087
+ "dead_at", entry,
3088
+ "a fresh part is not born already dead",
3089
+ )
3090
+ # `update_attempt` stamps `last_attempt_at`, so it is entry-only
3091
+ # metadata too. Round-tripping it pairs a timestamp from the old
3092
+ # entry's last retry with a fresh `attempt_count: 1` -- an entry
3093
+ # that claims it was last attempted before it was ever queued.
3094
+ self.assertNotIn(
3095
+ "last_attempt_at", entry,
3096
+ "a fresh part has never been attempted",
3097
+ )
3098
+ # The rest are `_build_entry` guarantees, not strip guarantees:
3099
+ # they hold even if the field were left on the payload. Kept as
3100
+ # a check on `_build_entry`, labelled so no one reads them as
3101
+ # coverage of `_ENTRY_ONLY_FIELDS`.
3102
+ self.assertEqual(entry["error_class"], "RuntimeError")
3103
+ self.assertIn("re-split", entry["error_message"])
3104
+ self.assertNotEqual(entry["failed_at"], stale_stamp)
3105
+ self.assertEqual(entry["schema"], pending.SCHEMA)
3106
+
3107
+ # --- The archive is only earned by a NEW file -------------------------
3108
+ #
3109
+ # `_enqueue_one` returns the path of an ALREADY-QUEUED identical entry on
3110
+ # a dedupe hit, so "enqueue handed back a path" is not "a part was
3111
+ # written". Both cases below are the same bug from two angles: a re-split
3112
+ # that wrote nothing must not archive the original, because the archive
3113
+ # is capped (`_trim_dir`) and the original is then the only copy.
3114
+
3115
+ def test_a_resplit_that_dedupes_onto_its_own_original_STAYS_QUEUED(self):
3116
+ """The real repro, no patching: ONE part, so `enqueue` re-enqueues
3117
+ the payload unchanged, the dupe key matches the entry being re-split,
3118
+ and the "written" path IS the original.
3119
+
3120
+ A JSON transcript is re-serialised compactly by the splitter, so a
3121
+ pretty-printed one over the bound can compact to a single part under
3122
+ it. That is not exotic -- it is any indented transcript whose
3123
+ whitespace is most of its size.
3124
+ """
3125
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "100000"
3126
+ blocks = [{"type": "text", "text": f"line {i}"} for i in range(60)]
3127
+ content = json.dumps([{"role": "user", "content": blocks}], indent=8)
3128
+ path = pending.enqueue(_payload(content=content), RuntimeError("x"))
3129
+ self.assertEqual(pending.count(), 1)
3130
+
3131
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "3000"
3132
+ self.assertGreater(len(content), 3000, "the entry is over the bound")
3133
+ self.assertEqual(
3134
+ len(retain_split.split_retain_content(content)), 1,
3135
+ "the splitter yields a SINGLE part -- the condition under test",
3136
+ )
3137
+
3138
+ with redirect_stderr(io.StringIO()):
3139
+ entries, parts = pending.resplit_over_bound_entries()
3140
+
3141
+ self.assertEqual((entries, parts), (0, 0))
3142
+ self.assertTrue(os.path.exists(path), "the ONLY copy was archived away")
3143
+ self.assertEqual(pending.count(), 1)
3144
+ self.assertEqual(self._resplit_names(), [], "nothing was earned")
3145
+
3146
+ def test_a_deduped_part_is_not_counted_as_a_written_part(self):
3147
+ """`_enqueue_one` returns an EXISTING path and writes no new file.
3148
+
3149
+ Distinct from `test_an_entry_that_cannot_be_split_STAYS_QUEUED`,
3150
+ which returns None and so makes "no path came back" and "no file
3151
+ appeared" true at once -- it cannot tell the two guards apart.
3152
+ """
3153
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "100000"
3154
+ path = pending.enqueue(_payload(content="d" * 40_000), RuntimeError("x"))
3155
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "3000"
3156
+
3157
+ # Non-None, already on disk, and no new file is created.
3158
+ with unittest.mock.patch.object(pending, "_enqueue_one", return_value=path):
3159
+ with redirect_stderr(io.StringIO()):
3160
+ entries, parts = pending.resplit_over_bound_entries()
3161
+
3162
+ self.assertEqual((entries, parts), (0, 0))
3163
+ self.assertTrue(os.path.exists(path), "never deleted on a no-op split")
3164
+ self.assertEqual(pending.count(), 1)
3165
+ self.assertEqual(self._resplit_names(), [])
3166
+
3167
+ def test_the_part_count_is_not_a_queue_depth_delta(self):
3168
+ """Parts WRITTEN, not queue growth.
3169
+
3170
+ When the queue is near its cap, `_evict_to_fit` sheds an older entry
3171
+ for each part it makes room for, so the depth barely moves while N
3172
+ parts are genuinely written. A depth delta under-reports that (and at
3173
+ the extreme reads 0, which is the "stays queued" signal) -- so the
3174
+ count is taken from what `_enqueue_one` handed back.
3175
+ """
3176
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "100000"
3177
+ for i in range(10):
3178
+ pending.enqueue(
3179
+ _payload(content=f"filler-{i}", doc=_cd_doc(i + 50)),
3180
+ RuntimeError("x"),
3181
+ )
3182
+ path = pending.enqueue(_payload(content="y" * 40_000), RuntimeError("x"))
3183
+ self.assertEqual(pending.count(), 11)
3184
+
3185
+ # 11 queued + 14 parts does not fit, so writing the parts evicts.
3186
+ pending.MAX_ENTRIES = 20
3187
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "3000"
3188
+ with redirect_stderr(io.StringIO()):
3189
+ entries, parts = pending.resplit_over_bound_entries()
3190
+
3191
+ self.assertEqual(entries, 1)
3192
+ # 20 at the cap, less the original once it is archived out.
3193
+ self.assertEqual(pending.count(), 19, "the cap held, so eviction fired")
3194
+ self.assertLess(
3195
+ 19 - 11, 14, "the depth grew by less than the parts written"
3196
+ )
3197
+ self.assertEqual(
3198
+ parts, 14,
3199
+ "every part written is counted, even the ones whose room was "
3200
+ "made by evicting an older entry",
3201
+ )
3202
+ self.assertFalse(os.path.exists(path), "the original was archived")
3203
+
3204
+ # --- A refusal the caller absorbs is not a permanent loss -------------
3205
+
3206
+ def _refused_resplit_leaves_the_ledger_clean(self, cap: str, value: int):
3207
+ """`record_drop` means the memory is GONE; here it is still queued.
3208
+
3209
+ `switchroom doctor` FAILS on a non-zero drop ledger, and phase 0c
3210
+ retries on every backlog drain (10 min on this fleet), so stamping a
3211
+ refusal turns a safely-queued memory into a permanent, monotonically
3212
+ climbing doctor failure.
3213
+ """
3214
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "100000"
3215
+ path = pending.enqueue(_payload(content="r" * 40_000), RuntimeError("x"))
3216
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "3000"
3217
+ setattr(pending, cap, value)
3218
+
3219
+ with redirect_stderr(io.StringIO()):
3220
+ for _ in range(3): # every drain retries the same entry
3221
+ self.assertEqual(pending.resplit_over_bound_entries(), (0, 0))
3222
+
3223
+ self.assertEqual(
3224
+ pending.read_drops(), {},
3225
+ "nothing was lost -- the original is still queued and retryable",
3226
+ )
3227
+ self.assertTrue(os.path.exists(path))
3228
+ self.assertEqual(self._resplit_names(), [])
3229
+
3230
+ def test_a_resplit_refused_on_the_entry_cap_stamps_no_drop(self):
3231
+ self._refused_resplit_leaves_the_ledger_clean("MAX_ENTRIES", 2)
3232
+
3233
+ def test_a_resplit_refused_on_the_byte_cap_stamps_no_drop(self):
3234
+ self._refused_resplit_leaves_the_ledger_clean("MAX_BYTES", 4096)
3235
+
3236
+ def test_the_producer_path_still_stamps_a_refused_memory_as_dropped(self):
3237
+ """The suppression is scoped to the re-split caller, not global.
3238
+
3239
+ `session_end` / `retain` have no other copy of the memory, so when
3240
+ `enqueue` refuses one the turn really is lost and the ledger is the
3241
+ only record of it.
3242
+ """
3243
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "3000"
3244
+ pending.MAX_BYTES = 4096 # smaller than the split memory as a whole
3245
+ with redirect_stderr(io.StringIO()):
3246
+ self.assertIsNone(
3247
+ pending.enqueue(_payload(content="e" * 40_000), RuntimeError("x"))
3248
+ )
3249
+ self.assertEqual(pending.read_drops().get("count"), 1)
3250
+ self.assertEqual(pending.count(), 0)
3251
+
3252
+ # --- ... and neither is a PER-PART failure the caller absorbs ---------
3253
+ #
3254
+ # The cap refusals above are the whole-memory door. `_enqueue_one` has a
3255
+ # second one: its own MAX_BYTES guard and the post-eviction write failure
3256
+ # both `record_drop`. When EVERY part fails there, `enqueue_parts` hands
3257
+ # back nothing, `written == 0`, and the original correctly STAYS QUEUED --
3258
+ # so those drops describe a memory that was not lost, on an entry phase 0c
3259
+ # retries every backlog drain (10 min on this fleet), climbing forever and
3260
+ # never resetting once the disk is fixed. Same failure mode, same fix.
3261
+
3262
+ @staticmethod
3263
+ def _enospc_on_every_rename():
3264
+ """A full disk, at the exact syscall `_enqueue_one` fails on."""
3265
+ return unittest.mock.patch.object(
3266
+ pending.os, "rename",
3267
+ side_effect=OSError(errno.ENOSPC, "No space left on device"),
3268
+ )
3269
+
3270
+ def test_a_resplit_whose_every_part_fails_to_write_stamps_no_drop(self):
3271
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "100000"
3272
+ path = pending.enqueue(_payload(content="p" * 40_000), RuntimeError("x"))
3273
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "3000"
3274
+
3275
+ with self._enospc_on_every_rename():
3276
+ with redirect_stderr(io.StringIO()):
3277
+ for _ in range(3): # every backlog drain retries this entry
3278
+ self.assertEqual(pending.resplit_over_bound_entries(), (0, 0))
3279
+
3280
+ self.assertEqual(
3281
+ pending.read_drops(), {},
3282
+ "nothing was lost -- not one part was written, so the original "
3283
+ "is still queued and the next drain retries the whole re-split",
3284
+ )
3285
+ self.assertTrue(os.path.exists(path), "the original never left")
3286
+ self.assertEqual(pending.count(), 1)
3287
+ self.assertEqual(self._resplit_names(), [])
3288
+
3289
+ def test_a_part_that_fails_is_stamped_once_the_original_is_retired(self):
3290
+ """The deferral is not a suppression.
3291
+
3292
+ One part of fourteen hits ENOSPC, thirteen land, so the original IS
3293
+ archived -- nothing will ever retry part five again. That IS the
3294
+ permanent loss `record_drop` exists to make visible.
3295
+ """
3296
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "100000"
3297
+ path = pending.enqueue(_payload(content="q" * 40_000), RuntimeError("x"))
3298
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "3000"
3299
+
3300
+ real_rename = os.rename
3301
+ seen = {"n": 0}
3302
+
3303
+ def flaky(src, dst):
3304
+ seen["n"] += 1
3305
+ if seen["n"] == 5: # part five of fourteen
3306
+ raise OSError(errno.ENOSPC, "No space left on device")
3307
+ return real_rename(src, dst)
3308
+
3309
+ with unittest.mock.patch.object(pending.os, "rename", flaky):
3310
+ with redirect_stderr(io.StringIO()):
3311
+ entries, parts = pending.resplit_over_bound_entries()
3312
+
3313
+ self.assertEqual((entries, parts), (1, 13), "thirteen of fourteen")
3314
+ self.assertFalse(os.path.exists(path), "the original was archived")
3315
+ self.assertEqual(
3316
+ pending.read_drops().get("count"), 1,
3317
+ "the part that never landed is gone for good and must be visible",
3318
+ )
3319
+ self.assertEqual(
3320
+ pending.read_drops().get("last_error_class"), "OSError",
3321
+ )
3322
+
3323
+ def test_the_producer_path_still_stamps_a_failed_part_as_dropped(self):
3324
+ """The per-part deferral is scoped to the re-split caller too.
3325
+
3326
+ `session_end` / `retain` hold no other copy, so every part that
3327
+ cannot be written really is a lost slice of the turn.
3328
+ """
3329
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "3000"
3330
+ with self._enospc_on_every_rename():
3331
+ with redirect_stderr(io.StringIO()):
3332
+ self.assertIsNone(
3333
+ pending.enqueue(_payload(content="f" * 40_000), RuntimeError("x"))
3334
+ )
3335
+ self.assertEqual(pending.count(), 0, "not one part landed")
3336
+ self.assertGreaterEqual(
3337
+ pending.read_drops().get("count", 0), 14,
3338
+ "one drop per part the producer could not write",
3339
+ )
3340
+
3341
+ # --- An original evicted out from under its own re-split --------------
3342
+
3343
+ @staticmethod
3344
+ def _clock_frozen_at(when: float):
3345
+ """Freeze the millisecond `_enqueue_one` stamps into entry names.
3346
+
3347
+ Entry names are `<unix-ms>-<dupe-key>-<uuid>.json` and FIFO order is
3348
+ their lexicographic sort, so entries written inside ONE millisecond
3349
+ tie-break on the CONTENT-DERIVED dupe key -- stable and total, but
3350
+ arbitrary. Production never hits that on the re-split path: the
3351
+ original was queued by an EARLIER process, milliseconds to days
3352
+ before its parts. A test that enqueues both inside one tick does hit
3353
+ it, and then `_evict_to_fit` sheds whichever name happens to sort
3354
+ first -- measured 6 failures in 40 isolated runs of the case below
3355
+ before this pin. So pin the age relationship the product guarantees
3356
+ instead of racing the clock for it.
3357
+ """
3358
+ return unittest.mock.patch.object(pending.time, "time", lambda: when)
3359
+
3360
+ def _nth_part_write_fails(self, n: int):
3361
+ """ENOSPC on the `n`-th part write, and on nothing else.
3362
+
3363
+ Counting every `os.rename` would be wrong: `shutil.move` renames too
3364
+ when source and destination share a filesystem, so an eviction inside
3365
+ the same loop shifts the count. Only the tmp -> `<entry>.json` rename
3366
+ inside the LIVE QUEUE dir is a part landing.
3367
+ """
3368
+ real_rename = os.rename
3369
+ seen = {"n": 0}
3370
+
3371
+ def flaky(src, dst):
3372
+ landing = str(dst).endswith(".json") and os.path.dirname(
3373
+ os.path.abspath(str(dst))
3374
+ ) == os.path.abspath(self._dir)
3375
+ if landing:
3376
+ seen["n"] += 1
3377
+ if seen["n"] == n:
3378
+ raise OSError(errno.ENOSPC, "No space left on device")
3379
+ return real_rename(src, dst)
3380
+
3381
+ return unittest.mock.patch.object(pending.os, "rename", flaky)
3382
+
3383
+ def _an_original_older_than_its_parts(self, content: str) -> str:
3384
+ """Enqueue `content` stamped an hour ago; return its queue path."""
3385
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "100000"
3386
+ with self._clock_frozen_at(time.time() - 3600):
3387
+ path = pending.enqueue(_payload(content=content), RuntimeError("x"))
3388
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "3000"
3389
+ return path
3390
+
3391
+ def test_a_resplit_that_evicts_its_own_original_says_so(self):
3392
+ """The parts fill the queue and FIFO-shed the entry being re-split.
3393
+
3394
+ `total > MAX_ENTRIES` is false at exactly 14 parts and a cap of 14,
3395
+ so there is no refusal: part fourteen calls `_evict_to_fit`, which
3396
+ sheds the OLDEST entry -- the original. `shutil.move` then raises
3397
+ `FileNotFoundError`. Nothing is lost (fourteen parts plus the evicted
3398
+ copy), but the old line told the operator the original "STAYS QUEUED"
3399
+ when it is in `pending-evicted/`, and returned (0, 0) after writing
3400
+ fourteen parts.
3401
+ """
3402
+ path = self._an_original_older_than_its_parts("z" * 40_000)
3403
+ name = os.path.basename(path)
3404
+ pending.MAX_ENTRIES = 14
3405
+
3406
+ err = io.StringIO()
3407
+ with redirect_stderr(err):
3408
+ entries, parts = pending.resplit_over_bound_entries()
3409
+
3410
+ self.assertEqual(len(retain_split.split_retain_content("z" * 40_000)), 14)
3411
+ self.assertFalse(os.path.exists(path), "the original was evicted")
3412
+ self.assertIn(name, self._archive_names(), "...into pending-evicted/")
3413
+ self.assertEqual(self._resplit_names(), [], "it was never archived")
3414
+ self.assertEqual(pending.count(), 14, "fourteen parts, at the cap")
3415
+
3416
+ log = err.getvalue()
3417
+ self.assertNotIn(
3418
+ "STAYS QUEUED", log,
3419
+ "it is NOT queued -- that is the line an operator reads under "
3420
+ "exactly this pressure",
3421
+ )
3422
+ self.assertIn(pending.evicted_dir(), log, "say where it actually is")
3423
+
3424
+ def test_the_evicted_branch_counts_the_parts_it_actually_wrote(self):
3425
+ """The return tuple alone, pinned on its own.
3426
+
3427
+ This is the assertion that keeps the `continue` off the evicted
3428
+ branch: with it restored the drain writes fourteen parts and reports
3429
+ (0, 0), so phase 0c's `_blog` line and `summary["resplit"]` both go
3430
+ silent on a re-split that happened. It is deliberately free of log
3431
+ wording and of every archive-location assertion, so no rewording of
3432
+ the operator line can retire the guard by accident.
3433
+ """
3434
+ self._an_original_older_than_its_parts("z" * 40_000)
3435
+ pending.MAX_ENTRIES = 14
3436
+
3437
+ with redirect_stderr(io.StringIO()):
3438
+ entries, parts = pending.resplit_over_bound_entries()
3439
+
3440
+ self.assertEqual(
3441
+ (entries, parts), (1, 14),
3442
+ "fourteen parts were genuinely written and are queued, so phase "
3443
+ "0c's summary and `_blog` line must report them",
3444
+ )
3445
+ self.assertEqual(pending.count(), 14, "and all fourteen really are live")
3446
+
3447
+ def test_a_part_shed_to_make_room_is_not_counted_as_queued(self):
3448
+ """The other side of the eviction race, and an accounting lie.
3449
+
3450
+ FIFO sheds the OLDEST name, which is the original only while the
3451
+ original IS the oldest. Freeze the parts into an older millisecond
3452
+ and part fourteen evicts a PART instead: `shutil.move` then SUCCEEDS,
3453
+ the original is archived under `pending-resplit/`, and the drain used
3454
+ to report "into 14 queued part(s)" with thirteen queued and one
3455
+ sitting in `pending-evicted/`. Nothing re-drains that directory (see
3456
+ `drain_pending`, `probe.ts`, `doctor.ts` -- all of them only count or
3457
+ trim it), so the shed part is a slice of the memory that will not
3458
+ reach the bank, and the operator line must not paper over it.
3459
+ """
3460
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "100000"
3461
+ path = pending.enqueue(_payload(content="y" * 40_000), RuntimeError("x"))
3462
+ name = os.path.basename(path)
3463
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "3000"
3464
+ pending.MAX_ENTRIES = 14
3465
+
3466
+ err = io.StringIO()
3467
+ # The parts land an hour "before" the original, so the oldest name in
3468
+ # the queue when part fourteen needs room is a part, not the original.
3469
+ with self._clock_frozen_at(time.time() - 3600):
3470
+ with redirect_stderr(err):
3471
+ entries, parts = pending.resplit_over_bound_entries()
3472
+
3473
+ self.assertEqual(pending.count(), 13, "thirteen parts survived")
3474
+ self.assertEqual(
3475
+ self._resplit_names(), [name], "the original WAS archived here"
3476
+ )
3477
+ shed = self._archive_names()
3478
+ self.assertEqual(len(shed), 1, "one part was shed to make room")
3479
+ self.assertNotIn(name, shed, "and it was a part, not the original")
3480
+
3481
+ self.assertEqual(
3482
+ (entries, parts), (1, 13),
3483
+ "a part in pending-evicted/ is not a queued part -- nothing "
3484
+ "re-drains that directory",
3485
+ )
3486
+ log = err.getvalue()
3487
+ self.assertIn("into 13 queued part(s)", log)
3488
+ self.assertNotIn("into 14 queued part(s)", log)
3489
+ # The eviction itself already logs "evicted OLDEST ... into <dir>", so
3490
+ # asserting on `evicted_dir()` alone here passes with no re-split line
3491
+ # at all -- vacuous. Pin the re-split's OWN line: the operator reading
3492
+ # "the original is archived" has to be told, in the same breath, that
3493
+ # a part went the other way.
3494
+ self.assertRegex(
3495
+ log,
3496
+ r"re-split \S+ wrote 14 part\(s\).*13 part\(s\) of this memory "
3497
+ r"are live",
3498
+ "the re-split must reconcile 14 written against 13 live",
3499
+ )
3500
+ self.assertIn(
3501
+ f"FIFO-evicted into {pending.evicted_dir()}", log,
3502
+ "and say where the shed part went",
3503
+ )
3504
+ self.assertIn(shed[0], log, "named, so it can be found")
3505
+
3506
+ def _resplit_with_the_last_part_failing(self):
3507
+ """Original evicted AND one part unwritten, the two together.
3508
+
3509
+ Part fourteen is the one that triggers `_evict_to_fit` (the queue is
3510
+ at its cap of fourteen), so failing THAT part's write is the only
3511
+ arrangement where the original leaves the queue and a part is
3512
+ genuinely lost in the same drain. Returns `(entries, parts, log)`.
3513
+ """
3514
+ path = self._an_original_older_than_its_parts("w" * 40_000)
3515
+ pending.MAX_ENTRIES = 14
3516
+
3517
+ err = io.StringIO()
3518
+ with self._nth_part_write_fails(14):
3519
+ with redirect_stderr(err):
3520
+ entries, parts = pending.resplit_over_bound_entries()
3521
+
3522
+ self.assertFalse(os.path.exists(path), "the original left the queue")
3523
+ self.assertIn(
3524
+ os.path.basename(path), self._archive_names(),
3525
+ "by eviction -- pending-resplit/ never got it",
3526
+ )
3527
+ self.assertEqual(self._resplit_names(), [])
3528
+ self.assertEqual((entries, parts), (1, 13), "thirteen of fourteen")
3529
+ return entries, parts, err.getvalue()
3530
+
3531
+ def test_a_part_that_fails_is_stamped_when_the_original_is_EVICTED(self):
3532
+ """The archived branch is not the only one that retires the original.
3533
+
3534
+ `record_deferred_drops` is reached on BOTH branches below the move,
3535
+ and only the archived one was covered: gating the stamp on the
3536
+ original having reached `pending-resplit/` left this drain silent
3537
+ about part fourteen, which nothing will ever retry -- the original is
3538
+ in `pending-evicted/` and no consumer re-drains that directory.
3539
+ """
3540
+ self._resplit_with_the_last_part_failing()
3541
+
3542
+ self.assertEqual(
3543
+ pending.read_drops().get("count"), 1,
3544
+ "the part that never landed is gone for good and must be visible",
3545
+ )
3546
+ self.assertEqual(pending.read_drops().get("last_error_class"), "OSError")
3547
+
3548
+ def test_the_evicted_branch_does_not_reassure_when_a_part_was_lost(self):
3549
+ """Two contradictory lines in one drain is a log-lie, not a nuance.
3550
+
3551
+ The evicted branch printed "(the parts carry the whole memory)"
3552
+ unconditionally and `record_deferred_drops` then printed "permanently
3553
+ lost" for the same entry, in the same drain. A non-empty `deferred`
3554
+ is EXACTLY the case where the parts do not carry the whole memory.
3555
+ """
3556
+ _entries, _parts, log = self._resplit_with_the_last_part_failing()
3557
+
3558
+ self.assertNotIn(
3559
+ "carry the whole memory", log,
3560
+ "one part never got written, so they demonstrably do not",
3561
+ )
3562
+ self.assertIn("permanently lost", log, "and the loss is still said")
3563
+ self.assertIn(pending.evicted_dir(), log, "with the original's location")
3564
+
3565
+ def test_a_concurrent_reconcile_is_not_reported_as_an_eviction(self):
3566
+ """"Gone from the queue" is resolved, not assumed.
3567
+
3568
+ Nothing serialises this drain against an in-hook `drain()` --
3569
+ `lib/pacing.py`'s `retain-inflight.lock` is a per-POST storm guard and
3570
+ `drain_pending` takes no lock -- so the original can be RECONCILED out
3571
+ from under the archive move. Attributing that to eviction told the
3572
+ operator the memory was shed when it had just been confirmed durable
3573
+ upstream, and stamped a permanent DROP for a part of a memory that
3574
+ reached the bank whole.
3575
+ """
3576
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "100000"
3577
+ path = pending.enqueue(_payload(content="v" * 40_000), RuntimeError("x"))
3578
+ name = os.path.basename(path)
3579
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "3000"
3580
+
3581
+ real_move = shutil.move
3582
+
3583
+ def reconcile_first(src, dst):
3584
+ """The other process wins the race, between two syscalls here."""
3585
+ if os.path.abspath(src) == os.path.abspath(path):
3586
+ # Exactly what `archive_reconciled` does in the other
3587
+ # process, minus the recursion back through this patch.
3588
+ os.makedirs(pending.reconciled_dir(), mode=0o700, exist_ok=True)
3589
+ real_move(src, os.path.join(pending.reconciled_dir(), name))
3590
+ raise OSError(errno.ENOENT, "No such file or directory")
3591
+ return real_move(src, dst)
3592
+
3593
+ err = io.StringIO()
3594
+ with self._nth_part_write_fails(5): # part five never lands
3595
+ with unittest.mock.patch.object(pending.shutil, "move", reconcile_first):
3596
+ with redirect_stderr(err):
3597
+ entries, parts = pending.resplit_over_bound_entries()
3598
+
3599
+ self.assertEqual(self._reconciled_names(), [name], "that is where it went")
3600
+ self.assertEqual(self._archive_names(), [], "it was never evicted")
3601
+ self.assertEqual((entries, parts), (1, 13))
3602
+
3603
+ log = err.getvalue()
3604
+ self.assertIn(pending.reconciled_dir(), log, "say where it actually is")
3605
+ self.assertNotIn("it was itself evicted", log)
3606
+ self.assertEqual(
3607
+ pending.read_drops(), {},
3608
+ "the whole memory is durable upstream, so the part that never "
3609
+ "got written is a redundant copy, not a lost turn",
3610
+ )
3611
+
3612
+ def test_content_exactly_at_the_bound_is_not_re_split(self):
3613
+ """`<= limit` is the bound, and the equal case is the whole point.
3614
+
3615
+ `split_retain_content` passes content of exactly `limit` chars
3616
+ through unsplit, so re-splitting it here is pure churn: the payload
3617
+ dedupes straight back onto the original and the drain prints "could
3618
+ not re-split" about an entry that never needed splitting. At `<`
3619
+ every drain does that for every at-the-bound entry.
3620
+ """
3621
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "3000"
3622
+ path = pending.enqueue(_payload(content="b" * 3000), RuntimeError("x"))
3623
+ self.assertEqual(pending.count(), 1, "it was queued whole, not split")
3624
+
3625
+ err = io.StringIO()
3626
+ with redirect_stderr(err):
3627
+ entries, parts = pending.resplit_over_bound_entries()
3628
+
3629
+ self.assertEqual((entries, parts), (0, 0))
3630
+ self.assertTrue(os.path.exists(path), "untouched")
3631
+ self.assertEqual(
3632
+ err.getvalue(), "",
3633
+ "an entry AT the bound is not even considered -- one char over "
3634
+ "is over, one char at is not",
3635
+ )
3636
+
3637
+ def test_an_archive_failure_that_leaves_the_original_queued_still_says_so(self):
3638
+ """The other branch: the move fails but the entry is still there."""
3639
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "100000"
3640
+ path = pending.enqueue(_payload(content="a" * 40_000), RuntimeError("x"))
3641
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "3000"
3642
+
3643
+ err = io.StringIO()
3644
+ with unittest.mock.patch.object(
3645
+ pending.shutil, "move",
3646
+ side_effect=OSError(errno.EACCES, "Permission denied"),
3647
+ ):
3648
+ with redirect_stderr(err):
3649
+ entries, parts = pending.resplit_over_bound_entries()
3650
+
3651
+ self.assertEqual((entries, parts), (0, 0), "the original is still queued")
3652
+ self.assertTrue(os.path.exists(path))
3653
+ self.assertIn("STAYS QUEUED", err.getvalue())
3654
+
3655
+ # --- The (first, paths) contract at total <= 1 ------------------------
3656
+
3657
+ def test_a_single_part_enqueue_returns_the_path_it_wrote(self):
3658
+ """`enqueue_parts` exists so a caller holding a copy can decide when
3659
+ to retire it. That decision reads the LIST, so the list must carry
3660
+ the single-part write too -- returning `(path, [])` would tell every
3661
+ such caller "nothing was queued" for the commonest payload there is.
3662
+ """
3663
+ first, paths = pending.enqueue_parts(
3664
+ _payload(content="under the bound"), RuntimeError("x")
3665
+ )
3666
+ self.assertIsNotNone(first)
3667
+ self.assertEqual(
3668
+ len(retain_split.split_retain_content("under the bound")), 1,
3669
+ "the single-part branch is the one under test",
3670
+ )
3671
+ self.assertEqual(
3672
+ paths, [first],
3673
+ "one part written is one path handed back",
3674
+ )
3675
+ self.assertEqual(pending.count(), 1)
3676
+
3677
+ def test_a_single_part_that_could_not_be_written_returns_no_paths(self):
3678
+ """The other half of the contract: `None` means nothing to retire."""
3679
+ with self._enospc_on_every_rename():
3680
+ with redirect_stderr(io.StringIO()):
3681
+ first, paths = pending.enqueue_parts(
3682
+ _payload(content="under the bound"), RuntimeError("x")
3683
+ )
3684
+ self.assertIsNone(first)
3685
+ self.assertEqual(paths, [])
3686
+
3687
+ def test_the_backlog_drain_resplits_before_paying_for_extraction(self):
3688
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "100000"
3689
+ pending.enqueue(_payload(content="t" * 40_000), RuntimeError("x"))
3690
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "3000"
3691
+
3692
+ summary = drain_pending.drain_backlog(CONFIG, phase="reconcile")
3693
+ self.assertEqual(summary["resplit"], 1)
3694
+ self.assertGreaterEqual(summary["resplit_parts"], 14)
3695
+
3696
+ def test_a_dry_run_resplits_nothing(self):
3697
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "100000"
3698
+ path = pending.enqueue(_payload(content="s" * 40_000), RuntimeError("x"))
3699
+ os.environ["HINDSIGHT_RETAIN_MAX_CONTENT_CHARS"] = "3000"
3700
+
3701
+ summary = drain_pending.drain_backlog(
3702
+ CONFIG, phase="reconcile", dry_run=True
3703
+ )
3704
+ self.assertEqual(summary["resplit"], 0)
3705
+ self.assertTrue(os.path.exists(path))
3706
+
3707
+
2274
3708
  if __name__ == "__main__":
2275
3709
  unittest.main()