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.
- package/dist/agent-scheduler/index.js +20 -7
- package/dist/auth-broker/index.js +93 -28
- package/dist/cli/autoaccept-poll.js +0 -1
- package/dist/cli/drive-write-pretool.mjs +5 -0
- package/dist/cli/ms-365-write-pretool.mjs +5 -0
- package/dist/cli/notion-write-pretool.mjs +20 -6
- package/dist/cli/switchroom.js +3091 -1435
- package/dist/host-control/main.js +92 -29
- package/dist/vault/approvals/kernel-server.js +92 -28
- package/dist/vault/broker/server.js +258 -71
- package/examples/switchroom.yaml +1 -1
- package/package.json +1 -1
- package/profiles/_base/cron-session.sh.hbs +6 -0
- package/profiles/_base/start.sh.hbs +92 -17
- package/skills/switchroom-health/SKILL.md +19 -0
- package/skills/switchroom-status/SKILL.md +1 -1
- package/telegram-plugin/auth-snapshot-format.ts +9 -2
- package/telegram-plugin/dist/gateway/gateway.js +6981 -6747
- package/telegram-plugin/gateway/gateway.ts +53 -52
- package/telegram-plugin/gateway/periodic-sweep-guard.ts +86 -0
- package/telegram-plugin/gateway/status-pin-retarget.ts +144 -0
- package/telegram-plugin/quota-bar-format.ts +4 -1
- package/telegram-plugin/status-no-truncate.ts +49 -0
- package/telegram-plugin/status-pin-driver.ts +28 -0
- package/telegram-plugin/status-pin.ts +33 -4
- package/telegram-plugin/tests/auth-snapshot-format.test.ts +42 -0
- package/telegram-plugin/tests/card-type-distinguishability.test.ts +268 -0
- package/telegram-plugin/tests/periodic-sweep-guard.test.ts +151 -0
- package/telegram-plugin/tests/pinned-card-collapse.test.ts +29 -18
- package/telegram-plugin/tests/quota-bar-format.test.ts +50 -0
- package/telegram-plugin/tests/secret-detect-false-positives.test.ts +1 -1
- package/telegram-plugin/tests/status-pin-retarget.test.ts +216 -0
- package/telegram-plugin/tests/status-pin-shutdown-wiring.test.ts +94 -0
- package/telegram-plugin/tests/status-pin-store.test.ts +87 -21
- package/telegram-plugin/tests/status-pin.test.ts +128 -2
- package/telegram-plugin/tests/worker-activity-feed.test.ts +10 -10
- package/telegram-plugin/tests/worker-feed-coalesce.test.ts +37 -21
- package/telegram-plugin/tests/worker-visibility-prose-silent-harness.test.ts +1 -1
- package/telegram-plugin/tier-downgrade.ts +3 -2
- package/telegram-plugin/tool-activity-summary.ts +61 -18
- package/telegram-plugin/uat/assertions.ts +21 -2
- package/telegram-plugin/uat/feed-matcher.test.ts +29 -0
- package/telegram-plugin/uat/scenarios/jtbd-liveness-narration-channel.test.ts +9 -2
- package/telegram-plugin/uat/scenarios/jtbd-liveness-narration-dm.test.ts +9 -2
- package/telegram-plugin/worker-activity-feed.ts +38 -17
- package/vendor/hindsight-memory/scripts/lib/config.py +61 -19
- package/vendor/hindsight-memory/scripts/lib/content.py +376 -1
- package/vendor/hindsight-memory/scripts/lib/english_words.txt +10799 -0
- package/vendor/hindsight-memory/scripts/recall.py +503 -252
- package/vendor/hindsight-memory/scripts/tests/test_recall_bank_slots.py +509 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_envelope_strip_telemetry.py +22 -5
- package/vendor/hindsight-memory/scripts/tests/test_recall_error_text.py +147 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_hook_budget.py +266 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_integration.py +0 -401
- package/vendor/hindsight-memory/scripts/tests/test_recall_no_lexical_gate.py +261 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_query_shaping.py +473 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_request_timeout.py +241 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_transcript_fallback.py +25 -8
- 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()
|