switchroom 0.19.24 → 0.19.26

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 (59) hide show
  1. package/dist/agent-scheduler/index.js +20 -7
  2. package/dist/auth-broker/index.js +93 -28
  3. package/dist/cli/autoaccept-poll.js +0 -1
  4. package/dist/cli/drive-write-pretool.mjs +5 -0
  5. package/dist/cli/ms-365-write-pretool.mjs +5 -0
  6. package/dist/cli/notion-write-pretool.mjs +20 -6
  7. package/dist/cli/switchroom.js +3091 -1435
  8. package/dist/host-control/main.js +92 -29
  9. package/dist/vault/approvals/kernel-server.js +92 -28
  10. package/dist/vault/broker/server.js +258 -71
  11. package/examples/switchroom.yaml +1 -1
  12. package/package.json +1 -1
  13. package/profiles/_base/cron-session.sh.hbs +6 -0
  14. package/profiles/_base/start.sh.hbs +92 -17
  15. package/skills/switchroom-health/SKILL.md +19 -0
  16. package/skills/switchroom-status/SKILL.md +1 -1
  17. package/telegram-plugin/auth-snapshot-format.ts +9 -2
  18. package/telegram-plugin/dist/gateway/gateway.js +6981 -6747
  19. package/telegram-plugin/gateway/gateway.ts +53 -52
  20. package/telegram-plugin/gateway/periodic-sweep-guard.ts +86 -0
  21. package/telegram-plugin/gateway/status-pin-retarget.ts +144 -0
  22. package/telegram-plugin/quota-bar-format.ts +4 -1
  23. package/telegram-plugin/status-no-truncate.ts +49 -0
  24. package/telegram-plugin/status-pin-driver.ts +28 -0
  25. package/telegram-plugin/status-pin.ts +33 -4
  26. package/telegram-plugin/tests/auth-snapshot-format.test.ts +42 -0
  27. package/telegram-plugin/tests/card-type-distinguishability.test.ts +268 -0
  28. package/telegram-plugin/tests/periodic-sweep-guard.test.ts +151 -0
  29. package/telegram-plugin/tests/pinned-card-collapse.test.ts +29 -18
  30. package/telegram-plugin/tests/quota-bar-format.test.ts +50 -0
  31. package/telegram-plugin/tests/secret-detect-false-positives.test.ts +1 -1
  32. package/telegram-plugin/tests/status-pin-retarget.test.ts +216 -0
  33. package/telegram-plugin/tests/status-pin-shutdown-wiring.test.ts +94 -0
  34. package/telegram-plugin/tests/status-pin-store.test.ts +87 -21
  35. package/telegram-plugin/tests/status-pin.test.ts +128 -2
  36. package/telegram-plugin/tests/worker-activity-feed.test.ts +10 -10
  37. package/telegram-plugin/tests/worker-feed-coalesce.test.ts +37 -21
  38. package/telegram-plugin/tests/worker-visibility-prose-silent-harness.test.ts +1 -1
  39. package/telegram-plugin/tier-downgrade.ts +3 -2
  40. package/telegram-plugin/tool-activity-summary.ts +61 -18
  41. package/telegram-plugin/uat/assertions.ts +21 -2
  42. package/telegram-plugin/uat/feed-matcher.test.ts +29 -0
  43. package/telegram-plugin/uat/scenarios/jtbd-liveness-narration-channel.test.ts +9 -2
  44. package/telegram-plugin/uat/scenarios/jtbd-liveness-narration-dm.test.ts +9 -2
  45. package/telegram-plugin/worker-activity-feed.ts +38 -17
  46. package/vendor/hindsight-memory/scripts/lib/config.py +61 -19
  47. package/vendor/hindsight-memory/scripts/lib/content.py +376 -1
  48. package/vendor/hindsight-memory/scripts/lib/english_words.txt +10799 -0
  49. package/vendor/hindsight-memory/scripts/recall.py +503 -252
  50. package/vendor/hindsight-memory/scripts/tests/test_recall_bank_slots.py +509 -0
  51. package/vendor/hindsight-memory/scripts/tests/test_recall_envelope_strip_telemetry.py +22 -5
  52. package/vendor/hindsight-memory/scripts/tests/test_recall_error_text.py +147 -0
  53. package/vendor/hindsight-memory/scripts/tests/test_recall_hook_budget.py +266 -0
  54. package/vendor/hindsight-memory/scripts/tests/test_recall_integration.py +0 -401
  55. package/vendor/hindsight-memory/scripts/tests/test_recall_no_lexical_gate.py +261 -0
  56. package/vendor/hindsight-memory/scripts/tests/test_recall_query_shaping.py +473 -0
  57. package/vendor/hindsight-memory/scripts/tests/test_recall_request_timeout.py +241 -0
  58. package/vendor/hindsight-memory/scripts/tests/test_recall_transcript_fallback.py +25 -8
  59. package/vendor/hindsight-memory/tests/test_content.py +218 -0
@@ -546,59 +546,6 @@ class AckShortCircuitTests(unittest.TestCase):
546
546
  self.assertIsNotNone(ctx)
547
547
 
548
548
 
549
- class ContainmentOverlapUnitTests(unittest.TestCase):
550
- """Switchroom #475 / #3541: pure-function tests for the relevance helpers."""
551
-
552
- def test_identical_text_is_full_overlap(self):
553
- # Modulo stop-word stripping (`is`, `the`, `a`, `to` removed).
554
- self.assertEqual(
555
- recall.containment_overlap("deploy the staging server", "deploy the staging server"),
556
- 1.0,
557
- )
558
-
559
- def test_disjoint_text_is_zero(self):
560
- self.assertEqual(
561
- recall.containment_overlap("deploy staging server", "vegan dinner recipes"),
562
- 0.0,
563
- )
564
-
565
- def test_partial_overlap_is_between(self):
566
- score = recall.containment_overlap(
567
- "deploy staging server",
568
- "deploy production server",
569
- )
570
- # {deploy, staging, server} vs {deploy, production, server}
571
- # → intersection 2, |M| = 3 → 0.666…
572
- self.assertAlmostEqual(score, 2 / 3, places=2)
573
-
574
- def test_stopwords_dont_inflate_overlap(self):
575
- # "the" / "is" / "a" present in both shouldn't count.
576
- score = recall.containment_overlap("the cat is a pet", "the dog is a pet")
577
- # Real tokens after stopword strip: {cat, pet} vs {dog, pet}
578
- # → intersection 1, |M| = 2 → 0.5
579
- self.assertAlmostEqual(score, 0.5, places=2)
580
-
581
- def test_empty_text_yields_zero(self):
582
- self.assertEqual(recall.containment_overlap("", "anything at all"), 0.0)
583
- self.assertEqual(recall.containment_overlap("query", ""), 0.0)
584
-
585
- def test_non_string_inputs_yield_zero(self):
586
- self.assertEqual(recall.containment_overlap(None, "x"), 0.0)
587
- self.assertEqual(recall.containment_overlap("x", None), 0.0)
588
-
589
- def test_case_insensitive(self):
590
- self.assertEqual(
591
- recall.containment_overlap("DEPLOY Server", "deploy server"),
592
- 1.0,
593
- )
594
-
595
- def test_punctuation_stripped(self):
596
- self.assertEqual(
597
- recall.containment_overlap("deploy, server!", "deploy server"),
598
- 1.0,
599
- )
600
-
601
-
602
549
  class InjectedScoreStatsUnitTests(unittest.TestCase):
603
550
  """#3541 — aggregates for the injected set. Must be total and non-fatal:
604
551
  telemetry can never take recall down."""
@@ -646,354 +593,6 @@ class InjectedScoreStatsUnitTests(unittest.TestCase):
646
593
  )
647
594
 
648
595
 
649
- class OverlapFilterUnitTests(unittest.TestCase):
650
- """Switchroom #475: _filter_by_overlap behaviour."""
651
-
652
- def test_threshold_zero_passthrough(self):
653
- results = [_memory("totally unrelated text")]
654
- kept, dropped = recall._filter_by_overlap(results, "deploy server", 0.0)
655
- self.assertEqual(kept, results)
656
- self.assertEqual(dropped, 0)
657
-
658
- def test_high_threshold_drops_weak_matches(self):
659
- results = [
660
- _memory("deploy server staging"), # full overlap
661
- _memory("vegan dinner recipes"), # zero overlap
662
- ]
663
- kept, dropped = recall._filter_by_overlap(results, "deploy server staging", 0.5)
664
- self.assertEqual(len(kept), 1)
665
- self.assertEqual(dropped, 1)
666
- self.assertEqual(kept[0]["text"], "deploy server staging")
667
-
668
- def test_threshold_keeps_partial_match_at_or_above(self):
669
- results = [_memory("deploy production server")]
670
- kept, dropped = recall._filter_by_overlap(results, "deploy staging server", 0.5)
671
- # 2/|M| = 2/3 = 0.666… ≥ 0.5 → kept
672
- self.assertEqual(len(kept), 1)
673
- self.assertEqual(dropped, 0)
674
-
675
- def test_threshold_drops_partial_match_below(self):
676
- results = [_memory("deploy production server")]
677
- # 2/|M| = 2/3 = 0.666… < 0.7 → dropped
678
- kept, dropped = recall._filter_by_overlap(results, "deploy staging server", 0.7)
679
- self.assertEqual(len(kept), 0)
680
- self.assertEqual(dropped, 1)
681
-
682
-
683
- class OverlapGateQueryLengthInvarianceTests(unittest.TestCase):
684
- """Switchroom #3541 regression: the gate must not be decided by prompt length.
685
-
686
- Production telemetry (1548 recall_log.jsonl rows, ts >= 2026-07-18) showed
687
- the survival rate through this gate collapsing monotonically as the prompt
688
- grew — 7.3% at <200 query chars down to 0.9% at 600-750 chars — with a
689
- 93% zero-result rate at the long end. That is the Jaccard union term, not a
690
- relevance judgement. These tests pin the containment metric that fixes it.
691
- """
692
-
693
- # A realistic recall query: the memory-bearing sentence plus the long
694
- # unrelated prior-context preamble the UserPromptSubmit hook prepends.
695
- CORE_QUERY = "where did we land on the postgres connection pool sizing"
696
- PREAMBLE = (
697
- "Prior context: the operator asked about telegram card rendering, then "
698
- "about vault broker grants, then about the nightly digest schedule and "
699
- "the reaction dispatch wiring, and separately about docker image "
700
- "promotion, canary holdback, worktree hygiene, changelog discipline, "
701
- "eval harness thresholds, skill authoring quotas and mental model "
702
- "refresh cadence across the whole fleet of agents on this host. "
703
- )
704
- MEMORY = "Postgres connection pool sizing was settled at min 5 max 100."
705
-
706
- @staticmethod
707
- def _filler(n_words):
708
- """Generate `n_words` DISTINCT unrelated content words.
709
-
710
- Distinctness matters: the metrics operate on token *sets*, so
711
- repeating the same paragraph would not grow ``|Q|`` and a
712
- length-sensitivity test built on it would silently pass even under
713
- the buggy Jaccard metric.
714
-
715
- The words must also be purely alphabetic — ``_overlap_tokens``
716
- splits on every non-alpha character, so a digit-suffixed word like
717
- ``filler7word`` collapses to the two tokens ``filler``/``word`` no
718
- matter how many are emitted.
719
- """
720
- letters = "abcdefghijklmnopqrstuvwxyz"
721
- return " ".join(
722
- "zz" + letters[i // 26 % 26] + letters[i % 26] for i in range(n_words)
723
- )
724
-
725
- def test_extra_preamble_does_not_change_the_score(self):
726
- """In the production regime (query longer than the memory — always
727
- true for the UserPromptSubmit preamble), piling on more unrelated
728
- preamble must NOT move the score. Under Jaccard these two differ,
729
- because the union term grows with the prompt; under containment the
730
- denominator is the memory, so they are identical."""
731
- short = recall.containment_overlap(
732
- self._filler(20) + " " + self.CORE_QUERY, self.MEMORY
733
- )
734
- long_ = recall.containment_overlap(
735
- self._filler(200) + " " + self.CORE_QUERY, self.MEMORY
736
- )
737
- self.assertGreater(short, 0.0, "sanity: the memory does overlap the query")
738
- self.assertAlmostEqual(
739
- short,
740
- long_,
741
- places=6,
742
- msg="prompt length changed the gate score — the union-term artifact is back",
743
- )
744
-
745
- def test_relevant_memory_survives_a_production_length_prompt(self):
746
- """At the fleet default threshold (0.10, scaffold.ts), a clearly
747
- relevant memory must survive a production-p50-length prompt."""
748
- query = self.PREAMBLE + self.CORE_QUERY
749
- self.assertGreater(
750
- len(query), 400, "sanity: this is a production-shaped long prompt"
751
- )
752
- kept, dropped = recall._filter_by_overlap([_memory(self.MEMORY)], query, 0.10)
753
- self.assertEqual(
754
- len(kept), 1, "relevant memory was dropped from a long prompt"
755
- )
756
- self.assertEqual(dropped, 0)
757
-
758
- def test_gate_still_drops_irrelevant_memories_on_a_long_prompt(self):
759
- """The floor must still bite: a length-invariant metric must not
760
- become a passthrough. An unrelated memory is still dropped."""
761
- query = self.PREAMBLE + self.CORE_QUERY
762
- kept, dropped = recall._filter_by_overlap(
763
- [_memory("Ken prefers oat milk in his flat white.")], query, 0.10
764
- )
765
- self.assertEqual(len(kept), 0, "gate no longer filters — floor is dead")
766
- self.assertEqual(dropped, 1)
767
-
768
- # --- switchroom #3541 review F4: what the floor does and does NOT do ---
769
- #
770
- # `test_gate_still_drops_irrelevant_memories_on_a_long_prompt` above uses a
771
- # memory with ZERO shared terms, so it scores 0.000 and would pass at a
772
- # threshold of 0.001. It proves the gate is not a total no-op; it does not
773
- # characterise the floor. These two do.
774
-
775
- IRRELEVANT_BUT_OVERLAPPING = (
776
- "The nightly digest schedule and the reaction dispatch wiring "
777
- "were changed."
778
- )
779
-
780
- def test_partially_overlapping_irrelevant_memory_is_KEPT_at_the_default(self):
781
- """Characterisation, not aspiration: at the 0.10 fleet default the gate
782
- KEEPS an off-topic memory that merely reuses words from the prompt's
783
- prior-context preamble.
784
-
785
- This memory has nothing to do with the actual question (postgres pool
786
- sizing) but shares most of its content tokens with the preamble, so it
787
- scores ~0.857 — HIGHER than the genuinely relevant memory (~0.571).
788
- Containment is therefore NOT monotone in relevance on a preamble-heavy
789
- prompt, which is precisely why the threshold is not raised to try to
790
- exclude this: any threshold that drops it drops the relevant memory
791
- first. Precision here is the engine reranker's job, not the gate's.
792
-
793
- If this test ever starts failing because the memory is now dropped,
794
- the gate's selectivity changed — re-measure before accepting it.
795
- """
796
- query = self.PREAMBLE + self.CORE_QUERY
797
- off_topic = recall.containment_overlap(query, self.IRRELEVANT_BUT_OVERLAPPING)
798
- relevant = recall.containment_overlap(query, self.MEMORY)
799
- self.assertGreater(
800
- off_topic,
801
- relevant,
802
- "the relevance inversion this test documents is gone — "
803
- "re-derive the threshold recommendation",
804
- )
805
- kept, dropped = recall._filter_by_overlap(
806
- [_memory(self.IRRELEVANT_BUT_OVERLAPPING)], query, 0.10
807
- )
808
- self.assertEqual(len(kept), 1, "gate selectivity changed — re-measure")
809
- self.assertEqual(dropped, 0)
810
-
811
- def test_floor_drops_a_long_memory_with_only_incidental_overlap(self):
812
- """Where the floor DOES bite: because the denominator is the memory's
813
- own token count, a long memory sharing a single incidental term with
814
- the prompt scores below 0.10 and is dropped.
815
-
816
- This is the real content of the floor after #3541 — it removes
817
- candidates with near-zero lexical relationship to the prompt, and
818
- (deliberately) little else.
819
- """
820
- query = self.PREAMBLE + self.CORE_QUERY
821
- # Exactly 1 shared content token ("docker") out of 17 -> 0.059 < 0.10
822
- long_incidental = (
823
- "docker buildx bake emits oci manifests whose provenance "
824
- "attestations confuse older registries during garbage collection "
825
- "sweeps"
826
- )
827
- score = recall.containment_overlap(query, long_incidental)
828
- self.assertLess(score, 0.10, "fixture no longer scores below the floor")
829
- self.assertGreater(score, 0.0, "sanity: there IS some incidental overlap")
830
- kept, dropped = recall._filter_by_overlap(
831
- [_memory(long_incidental)], query, 0.10
832
- )
833
- self.assertEqual(len(kept), 0, "floor is dead — nothing is being dropped")
834
- self.assertEqual(dropped, 1)
835
-
836
- # --- switchroom #3541 review F5: the |Q| < |M| regime ---
837
-
838
- def test_short_prompt_uses_the_memory_as_denominator(self):
839
- """A short prompt must NOT be scored against itself.
840
-
841
- With the textbook overlap coefficient (`min(|Q|, |M|)`) a prompt
842
- shorter than the memory flips the denominator to the QUERY, and the
843
- metric silently becomes "what fraction of the prompt is in the
844
- memory" — a one-word prompt then scores 1.0 against any memory
845
- containing that word, re-introducing the query-length dependence
846
- #3541 exists to remove. Dividing by `|M|` unconditionally has no such
847
- discontinuity.
848
- """
849
- memory = "Postgres connection pool sizing was settled at min 5 max 100."
850
- n_mem = len(recall._overlap_tokens(memory))
851
- query = "docker" # 1 content token, far shorter than the memory
852
- self.assertLess(
853
- len(recall._overlap_tokens(query)),
854
- n_mem,
855
- "sanity: this is the |Q| < |M| regime",
856
- )
857
- # A min()-denominator would score this 1.0 (1 shared token / |Q| = 1);
858
- # dividing by the memory gives 1/2.
859
- self.assertEqual(recall.containment_overlap(query, "docker image"), 0.5)
860
-
861
- # And a short prompt overlapping the memory scores by the memory:
862
- q2 = "postgres pool"
863
- expected = len(
864
- recall._overlap_tokens(q2) & recall._overlap_tokens(memory)
865
- ) / n_mem
866
- self.assertAlmostEqual(
867
- recall.containment_overlap(q2, memory), expected, places=6
868
- )
869
-
870
- def test_never_stricter_than_the_jaccard_gate_it_replaces(self):
871
- """Safety property vs what production runs today: because
872
- `|Q u M| >= |M|`, containment >= Jaccard for every pair. At a fixed
873
- threshold this gate admits a SUPERSET of what the deployed Jaccard
874
- gate admits, so no memory that survives today can be dropped by this
875
- change. Asserted over the fixtures rather than argued in prose.
876
- """
877
- query = self.PREAMBLE + self.CORE_QUERY
878
- cases = [
879
- self.MEMORY,
880
- self.IRRELEVANT_BUT_OVERLAPPING,
881
- "Ken prefers oat milk in his flat white.",
882
- "docker",
883
- "postgres pool sizing",
884
- self.PREAMBLE,
885
- ]
886
- for mem in cases:
887
- a = recall._overlap_tokens(query)
888
- b = recall._overlap_tokens(mem)
889
- jac = (len(a & b) / len(a | b)) if (a and b) else 0.0
890
- score = recall.containment_overlap(query, mem)
891
- self.assertGreaterEqual(
892
- score + 1e-12,
893
- jac,
894
- f"containment scored BELOW jaccard for {mem[:40]!r} — "
895
- f"this change could drop a memory production keeps",
896
- )
897
- # Upper bound + exact value. Review finding: the lower bound
898
- # alone is unfalsifiable for ANY implementation dividing by |M|
899
- # (a constant `return 1.0` satisfies it), so it survived every
900
- # mutation and proved nothing. These two pin the metric.
901
- self.assertLessEqual(score, 1.0, f"containment exceeded 1.0 for {mem[:40]!r}")
902
- expected = (len(a & b) / len(b)) if b else 0.0
903
- self.assertAlmostEqual(
904
- score, expected, places=9,
905
- msg=f"containment is not |Q n M| / |M| for {mem[:40]!r}",
906
- )
907
-
908
- def test_strict_subset_memory_scores_strictly_below_one(self):
909
- """A memory carrying a term the query does NOT have must score < 1.0.
910
-
911
- The companion upper bound to the superset property above: it is what
912
- actually falsifies a degenerate `return 1.0` implementation.
913
- """
914
- query = self.PREAMBLE + self.CORE_QUERY
915
- # Every token of this memory except `quokka` appears in the query.
916
- mem = self.CORE_QUERY + " quokka"
917
- score = recall.containment_overlap(query, mem)
918
- self.assertLess(score, 1.0)
919
- self.assertGreater(score, 0.0)
920
- # And a fully-contained memory reaches exactly 1.0, so the < 1.0
921
- # above is a property of the missing term, not a ceiling artefact.
922
- self.assertAlmostEqual(
923
- recall.containment_overlap(query, self.CORE_QUERY), 1.0, places=9
924
- )
925
-
926
- def test_survival_does_not_degrade_as_the_prompt_grows(self):
927
- """Sweep prompt length the way production does and assert the relevant
928
- memory survives at every length — the monotonic collapse is the bug."""
929
- for n_filler in (0, 20, 50, 100, 200, 400):
930
- query = self._filler(n_filler) + " " + self.CORE_QUERY
931
- kept, _ = recall._filter_by_overlap(
932
- [_memory(self.MEMORY)], query, 0.10
933
- )
934
- self.assertEqual(
935
- len(kept),
936
- 1,
937
- f"relevant memory dropped at {n_filler} filler words "
938
- f"({len(query)} query chars)",
939
- )
940
-
941
-
942
- class OverlapGateIntegrationTests(unittest.TestCase):
943
- """Switchroom #475: gate wired through main()."""
944
-
945
- def test_default_off_passes_everything_through(self):
946
- # No recallMinOverlap in config → behaves as before.
947
- client = _FakeClient(
948
- directives=[],
949
- memories=[
950
- _memory("deploy staging server"),
951
- _memory("vegan dinner recipes"),
952
- ],
953
- )
954
- ctx, _ = _run_main_with(client, prompt="how do we deploy staging?")
955
- self.assertIsNotNone(ctx)
956
- self.assertIn("deploy staging server", ctx)
957
- self.assertIn("vegan dinner recipes", ctx)
958
-
959
- def test_high_threshold_drops_irrelevant_memories(self):
960
- client = _FakeClient(
961
- directives=[],
962
- memories=[
963
- _memory("deploy staging server"),
964
- _memory("vegan dinner recipes"),
965
- ],
966
- )
967
- ctx, _ = _run_main_with(
968
- client,
969
- prompt="how do we deploy staging server",
970
- config_extra={"recallMinOverlap": 0.5},
971
- )
972
- # Relevant survives, junk doesn't.
973
- self.assertIsNotNone(ctx)
974
- self.assertIn("deploy staging server", ctx)
975
- self.assertNotIn("vegan", ctx)
976
-
977
- def test_threshold_emits_no_block_when_all_dropped(self):
978
- # All memories below threshold → no <hindsight_memories> block.
979
- # Telemetry still records the dropped count.
980
- client = _FakeClient(
981
- directives=[],
982
- memories=[
983
- _memory("vegan dinner recipes"),
984
- _memory("totally unrelated chatter"),
985
- ],
986
- )
987
- ctx, _ = _run_main_with(
988
- client,
989
- prompt="how do we deploy staging server",
990
- config_extra={"recallMinOverlap": 0.5},
991
- )
992
- # No memories survived; with no directives either, we expect no
993
- # additionalContext at all.
994
- self.assertIsNone(ctx)
995
-
996
-
997
596
  class RecallTagFilterIntegrationTests(unittest.TestCase):
998
597
  """Upstream 962140eef port — tag filters flow through main() to each
999
598
  per-bank recall call, composed with our additional-banks routing."""
@@ -0,0 +1,261 @@
1
+ """Switchroom — the lexical-overlap recall gate is gone, and stays gone.
2
+
3
+ Between the engine's reranker and the ``recallMaxMemories`` head-slice, recall
4
+ used to run a ``recallMinOverlap`` gate: any candidate whose containment
5
+ overlap ``|Q n M| / |M|`` with the prompt fell under a threshold was discarded.
6
+ It was removed outright (no replacement score floor) because measurement showed
7
+ it was pure loss:
8
+
9
+ * On healthy production rows (non-timeout, non-error, ts >= 2026-07-20,
10
+ n=212 fleet-wide) it discarded 6026 of 7549 post-reranker candidates —
11
+ 79.8% fleet-wide, 94.4% for overlord, 91.0% for klanker.
12
+ * Replaying 330 real logged queries against the live engine, it dropped the
13
+ engine's OWN top-ranked candidate on 31.2% of queries.
14
+ * It filtered no measurable noise: the rate at which the best injected
15
+ memory scored below 1e-3 was 27.0% with the gate and 28.2% with no gate.
16
+
17
+ The root cause is the tokenizer: ``_overlap_tokens`` keeps only alphabetic
18
+ tokens of length > 1, so digits, identifiers, version numbers, PR numbers and
19
+ file paths are invisible to it. For a fleet that talks in identifiers that is
20
+ close to worst case.
21
+
22
+ So these tests assert OUTCOMES, not code paths. Each one fails if candidates
23
+ are dropped for lexical reasons again — including via a stale
24
+ ``recallMinOverlap`` key left behind in an operator's settings.json, which is
25
+ exactly how a removed knob comes back to life.
26
+
27
+ Stdlib-only (unittest + mock); runs under ``python3 -m unittest discover
28
+ tests/``. Harness mirrors ``test_recall_integration.py``.
29
+ """
30
+
31
+ import io
32
+ import json
33
+ import os
34
+ import shutil
35
+ import sys
36
+ import tempfile
37
+ import unittest
38
+ from unittest.mock import patch
39
+
40
+ SCRIPTS_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
41
+ if SCRIPTS_DIR not in sys.path:
42
+ sys.path.insert(0, SCRIPTS_DIR)
43
+
44
+ import recall # noqa: E402
45
+
46
+ BANK = "test-bank"
47
+
48
+ # A production-shaped recall query: the memory-bearing sentence plus the long
49
+ # unrelated prior-context preamble the UserPromptSubmit hook prepends. Query
50
+ # length is what made the old gate collapse, so the fixtures keep it realistic.
51
+ PREAMBLE = (
52
+ "Prior context: the operator asked about telegram card rendering, then "
53
+ "about vault broker grants, then about the nightly digest schedule and "
54
+ "the reaction dispatch wiring, and separately about docker image "
55
+ "promotion, canary holdback, worktree hygiene and changelog discipline. "
56
+ )
57
+
58
+
59
+ def _memory(text, score=0.5, mem_id=None):
60
+ return {
61
+ "text": text,
62
+ "type": "fact",
63
+ "mentioned_at": "2026-01-01",
64
+ "id": mem_id or text,
65
+ "scores": {"final": score},
66
+ }
67
+
68
+
69
+ class _FakeClient:
70
+ def __init__(self, memories, directives=None):
71
+ self._memories = memories
72
+ self._directives = directives or []
73
+
74
+ def list_directives(self, bank_id, active_only=True, timeout=2):
75
+ return {"items": list(self._directives)}
76
+
77
+ def recall(self, bank_id, query, **kwargs):
78
+ return {"results": [dict(m) for m in self._memories]}
79
+
80
+
81
+ def _run_main_with(client, prompt="what did we decide", config_extra=None):
82
+ hook_input = {
83
+ "prompt": prompt,
84
+ "session_id": "test-session",
85
+ "transcript_path": "",
86
+ "cwd": "/tmp",
87
+ }
88
+ config = {
89
+ "autoRecall": True,
90
+ "bankId": BANK,
91
+ "recallMaxTokens": 4096,
92
+ "recallBudget": "mid",
93
+ "recallContextTurns": 1,
94
+ "recallMaxQueryChars": 800,
95
+ "recallPromptPreamble": "",
96
+ "directivesCacheTtlSeconds": 0,
97
+ }
98
+ if config_extra:
99
+ config.update(config_extra)
100
+
101
+ stdout = io.StringIO()
102
+ stderr = io.StringIO()
103
+ with patch.object(recall, "load_config", return_value=config), patch.object(
104
+ recall, "get_api_url", return_value="http://localhost:18888"
105
+ ), patch.object(recall, "HindsightClient", return_value=client), patch.object(
106
+ recall, "ensure_bank_mission", return_value=None
107
+ ), patch.object(recall, "write_state", return_value=None), patch(
108
+ "sys.stdin", new=io.StringIO(json.dumps(hook_input))
109
+ ), patch("sys.stdout", new=stdout), patch("sys.stderr", new=stderr):
110
+ recall.main()
111
+
112
+ raw = stdout.getvalue()
113
+ if not raw.strip():
114
+ return None, raw
115
+ return json.loads(raw)["hookSpecificOutput"]["additionalContext"], raw
116
+
117
+
118
+ class GateHelpersAreGoneTests(unittest.TestCase):
119
+ """The gate's machinery must not exist to be re-wired by accident."""
120
+
121
+ def test_gate_helpers_are_removed(self):
122
+ self.assertFalse(hasattr(recall, "containment_overlap"))
123
+ self.assertFalse(hasattr(recall, "_filter_by_overlap"))
124
+
125
+ def test_tokenizer_survives_for_the_transcript_fallback(self):
126
+ """`_overlap_tokens` is still load-bearing for
127
+ `_build_transcript_fallback`'s keyword match — deleting it with the
128
+ gate would silently break the #3369 fallback."""
129
+ self.assertEqual(
130
+ recall._overlap_tokens("Deploy the staging server!"),
131
+ {"deploy", "staging", "server"},
132
+ )
133
+
134
+
135
+ class NoLexicalDroppingIntegrationTests(unittest.TestCase):
136
+ """Wired through main(): what the engine returns is what gets injected."""
137
+
138
+ def test_zero_word_overlap_memory_is_still_injected(self):
139
+ """The gate's headline behaviour, inverted. A memory sharing no
140
+ content word with the prompt was dropped outright; the reranker
141
+ ranked it, so it must now survive to the head-slice."""
142
+ client = _FakeClient(
143
+ [
144
+ _memory("deploy staging server", score=0.9),
145
+ _memory("vegan dinner recipes", score=0.8),
146
+ ]
147
+ )
148
+ ctx, _ = _run_main_with(client, prompt="how do we deploy staging server")
149
+ self.assertIsNotNone(ctx)
150
+ self.assertIn("deploy staging server", ctx)
151
+ self.assertIn("vegan dinner recipes", ctx)
152
+
153
+ def test_identifier_only_match_survives(self):
154
+ """The measured root cause: `_overlap_tokens` drops digits and
155
+ 1-char tokens, so a memory whose ONLY relationship to the prompt is
156
+ an identifier scored 0.0 and was always discarded. Assert directly
157
+ that the tokenizer still cannot see the identifier, AND that the
158
+ memory is injected anyway."""
159
+ query = PREAMBLE + "did PR #3541 land, and what did v0.19.24 change"
160
+ mem = "#3541 shipped in 0.19.24"
161
+ self.assertEqual(
162
+ recall._overlap_tokens(query) & recall._overlap_tokens(mem),
163
+ set(),
164
+ "fixture drifted: it must share no ALPHABETIC token with the query",
165
+ )
166
+ ctx, _ = _run_main_with(_FakeClient([_memory(mem, score=0.95)]), prompt=query)
167
+ self.assertIsNotNone(ctx)
168
+ self.assertIn(mem, ctx)
169
+
170
+ def test_long_prompt_does_not_starve_recall(self):
171
+ """#3541's failure shape: survival collapsed as the prompt grew, to
172
+ 93% zero-result past 600 query chars. Length must now be inert."""
173
+ for repeat in (1, 4, 8):
174
+ query = PREAMBLE * repeat + "where did the connection pool sizing land"
175
+ with self.subTest(query_chars=len(query)):
176
+ ctx, _ = _run_main_with(
177
+ _FakeClient([_memory(f"m-{i}", score=0.9 - i * 0.01) for i in range(6)]),
178
+ prompt=query,
179
+ )
180
+ self.assertIsNotNone(ctx)
181
+ for i in range(6):
182
+ self.assertIn(f"m-{i}", ctx)
183
+
184
+ def test_stale_min_overlap_key_is_inert(self):
185
+ """A removed knob comes back to life through leftover config. An
186
+ operator settings.json still carrying `recallMinOverlap: 0.5` must
187
+ drop nothing — under the old code this emptied the block entirely."""
188
+ client = _FakeClient(
189
+ [
190
+ _memory("vegan dinner recipes", score=0.9),
191
+ _memory("totally unrelated chatter", score=0.8),
192
+ ]
193
+ )
194
+ ctx, _ = _run_main_with(
195
+ client,
196
+ prompt="how do we deploy staging server",
197
+ config_extra={"recallMinOverlap": 0.5},
198
+ )
199
+ self.assertIsNotNone(ctx)
200
+ self.assertIn("vegan dinner recipes", ctx)
201
+ self.assertIn("totally unrelated chatter", ctx)
202
+
203
+ def test_relevance_order_and_cap_still_apply(self):
204
+ """Removing the gate must not remove the controls that replaced it:
205
+ the engine's `scores.final` sort plus the head-slice ARE the
206
+ precision mechanism now."""
207
+ mems = [_memory(f"m-{i}", score=i / 10.0) for i in range(10)]
208
+ ctx, _ = _run_main_with(
209
+ _FakeClient(mems),
210
+ config_extra={"recallMaxMemories": 3},
211
+ )
212
+ self.assertIsNotNone(ctx)
213
+ for keep in ("m-9", "m-8", "m-7"):
214
+ self.assertIn(keep, ctx)
215
+ for drop in ("m-0", "m-1", "m-2"):
216
+ self.assertNotIn(drop, ctx)
217
+
218
+
219
+ class RecallLogSchemaTests(unittest.TestCase):
220
+ """`overlap_dropped` measured a filter that no longer exists; a
221
+ permanently-zero field reads as "the gate is fine" on every dashboard."""
222
+
223
+ def setUp(self):
224
+ self._tmpdir = tempfile.mkdtemp(prefix="recall-nogate-test-")
225
+ self._prev = os.environ.get("CLAUDE_PLUGIN_DATA")
226
+ os.environ["CLAUDE_PLUGIN_DATA"] = self._tmpdir
227
+
228
+ def tearDown(self):
229
+ shutil.rmtree(self._tmpdir, ignore_errors=True)
230
+ if self._prev is None:
231
+ os.environ.pop("CLAUDE_PLUGIN_DATA", None)
232
+ else:
233
+ os.environ["CLAUDE_PLUGIN_DATA"] = self._prev
234
+
235
+ def test_log_row_drops_the_overlap_dropped_field(self):
236
+ _run_main_with(_FakeClient([_memory("a fact", score=0.9)]))
237
+ path = os.path.join(self._tmpdir, "state", "recall_log.jsonl")
238
+ self.assertTrue(os.path.isfile(path), "no recall_log.jsonl written")
239
+ with open(path, encoding="utf-8") as f:
240
+ rows = [json.loads(line) for line in f if line.strip()]
241
+ self.assertTrue(rows)
242
+ for row in rows:
243
+ self.assertNotIn("overlap_dropped", row)
244
+ # The quality fields that now carry the load must still be there.
245
+ self.assertIn("injected_score_median", row)
246
+ self.assertIn("pre_cap_count", row)
247
+
248
+ def test_every_candidate_reaches_the_cap_stage(self):
249
+ """Volume proof: nothing is lost between the engine response and the
250
+ head-slice. `pre_cap_count` == what the bank returned."""
251
+ mems = [_memory(f"m-{i}", score=0.9 - i * 0.01) for i in range(12)]
252
+ _run_main_with(_FakeClient(mems), config_extra={"recallMaxMemories": 4})
253
+ path = os.path.join(self._tmpdir, "state", "recall_log.jsonl")
254
+ with open(path, encoding="utf-8") as f:
255
+ rows = [json.loads(line) for line in f if line.strip()]
256
+ self.assertEqual(rows[-1]["pre_cap_count"], 12)
257
+ self.assertEqual(rows[-1]["result_count"], 4)
258
+
259
+
260
+ if __name__ == "__main__":
261
+ unittest.main()