switchroom 0.15.43 → 0.15.45
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 +37 -1
- package/dist/auth-broker/index.js +37 -1
- package/dist/cli/notion-write-pretool.mjs +37 -1
- package/dist/cli/switchroom.js +239 -38
- package/dist/cli/ui/index.html +1 -1
- package/dist/host-control/main.js +37 -1
- package/dist/vault/approvals/kernel-server.js +41 -2
- package/dist/vault/broker/server.js +41 -2
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +6 -0
- package/profiles/default/CLAUDE.md.hbs +3 -3
- package/telegram-plugin/dist/gateway/gateway.js +43 -7
- package/vendor/hindsight-memory/scripts/lib/gateway_ipc.py +29 -0
- package/vendor/hindsight-memory/scripts/recall.py +65 -2
- package/vendor/hindsight-memory/scripts/tests/test_sender_routing.py +127 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Unit tests for per-speaker memory routing (Switchroom).
|
|
2
|
+
|
|
3
|
+
RFC reference/rfcs/per-speaker-memory-routing.md. Covers:
|
|
4
|
+
- extract_user_from_prompt: sender (`user=`) extraction from the
|
|
5
|
+
<channel ...> envelope (username, numeric id, quote styles, missing).
|
|
6
|
+
- _cache_key: the sender is part of the key, so two speakers sending the
|
|
7
|
+
same prompt in one session don't collide on the recall cache.
|
|
8
|
+
|
|
9
|
+
Stdlib-only.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import unittest
|
|
15
|
+
|
|
16
|
+
SCRIPTS_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
17
|
+
if SCRIPTS_DIR not in sys.path:
|
|
18
|
+
sys.path.insert(0, SCRIPTS_DIR)
|
|
19
|
+
|
|
20
|
+
from lib.gateway_ipc import extract_user_from_prompt # noqa: E402
|
|
21
|
+
import recall # noqa: E402
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ExtractUserTests(unittest.TestCase):
|
|
25
|
+
def test_username_present(self):
|
|
26
|
+
p = '<channel source="telegram" chat_id="-100" user="lisa" ts="1">hi</channel>'
|
|
27
|
+
self.assertEqual(extract_user_from_prompt(p), "lisa")
|
|
28
|
+
|
|
29
|
+
def test_numeric_id_when_no_username(self):
|
|
30
|
+
p = '<channel source="telegram" chat_id="-100" user="987654321">hi</channel>'
|
|
31
|
+
self.assertEqual(extract_user_from_prompt(p), "987654321")
|
|
32
|
+
|
|
33
|
+
def test_single_quotes(self):
|
|
34
|
+
p = "<channel source='telegram' chat_id='-100' user='ken'>hi</channel>"
|
|
35
|
+
self.assertEqual(extract_user_from_prompt(p), "ken")
|
|
36
|
+
|
|
37
|
+
def test_no_envelope_returns_none(self):
|
|
38
|
+
self.assertIsNone(extract_user_from_prompt("a plain interactive prompt"))
|
|
39
|
+
|
|
40
|
+
def test_empty_user_returns_none(self):
|
|
41
|
+
p = '<channel source="telegram" chat_id="-100" user="">hi</channel>'
|
|
42
|
+
self.assertIsNone(extract_user_from_prompt(p))
|
|
43
|
+
|
|
44
|
+
def test_non_string_returns_none(self):
|
|
45
|
+
self.assertIsNone(extract_user_from_prompt(None))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class CacheKeySenderTests(unittest.TestCase):
|
|
49
|
+
"""The sender must be part of the cache key so a multi-user session
|
|
50
|
+
doesn't serve one speaker's recall to another."""
|
|
51
|
+
|
|
52
|
+
def test_different_senders_different_keys(self):
|
|
53
|
+
k_ken = recall._cache_key("s1", "what's on today", "clerk", [], None, "ken")
|
|
54
|
+
k_lisa = recall._cache_key("s1", "what's on today", "clerk", [], None, "lisa")
|
|
55
|
+
self.assertNotEqual(k_ken, k_lisa)
|
|
56
|
+
|
|
57
|
+
def test_same_sender_same_key(self):
|
|
58
|
+
a = recall._cache_key("s1", "p", "clerk", [], None, "ken")
|
|
59
|
+
b = recall._cache_key("s1", "p", "clerk", [], None, "ken")
|
|
60
|
+
self.assertEqual(a, b)
|
|
61
|
+
|
|
62
|
+
def test_no_sender_backward_compatible(self):
|
|
63
|
+
# Omitting the sender (DM / fleet-shared, single user) collapses to
|
|
64
|
+
# the empty string — stable, and distinct from any named sender.
|
|
65
|
+
none1 = recall._cache_key("s1", "p", "clerk", [], None)
|
|
66
|
+
none2 = recall._cache_key("s1", "p", "clerk", [], None, None)
|
|
67
|
+
self.assertEqual(none1, none2)
|
|
68
|
+
self.assertNotEqual(
|
|
69
|
+
none1, recall._cache_key("s1", "p", "clerk", [], None, "ken")
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ResolveSenderBankTests(unittest.TestCase):
|
|
74
|
+
"""The map lookup. The gateway emits a BARE username (from.username),
|
|
75
|
+
while operators naturally write `@handle` in config — both must resolve.
|
|
76
|
+
Plus additive append, dup/self skip, and fail-safe behaviour."""
|
|
77
|
+
|
|
78
|
+
def test_at_keyed_map_resolves_bare_sender(self):
|
|
79
|
+
# The headline case: operator keys "@lisa", gateway emits "lisa".
|
|
80
|
+
out = recall._resolve_sender_bank('{"@lisa": "lisa-profile"}', "lisa", "clerk", [])
|
|
81
|
+
self.assertEqual(out, ["lisa-profile"])
|
|
82
|
+
|
|
83
|
+
def test_bare_keyed_map_resolves_bare_sender(self):
|
|
84
|
+
out = recall._resolve_sender_bank('{"lisa": "lisa-profile"}', "lisa", "clerk", [])
|
|
85
|
+
self.assertEqual(out, ["lisa-profile"])
|
|
86
|
+
|
|
87
|
+
def test_numeric_id_key(self):
|
|
88
|
+
out = recall._resolve_sender_bank('{"123456789": "ken-profile"}', "123456789", "clerk", [])
|
|
89
|
+
self.assertEqual(out, ["ken-profile"])
|
|
90
|
+
|
|
91
|
+
def test_additive_keeps_existing_extra_banks(self):
|
|
92
|
+
out = recall._resolve_sender_bank('{"@lisa": "lisa-profile"}', "lisa", "clerk", ["shared"])
|
|
93
|
+
self.assertEqual(out, ["shared", "lisa-profile"])
|
|
94
|
+
|
|
95
|
+
def test_skips_self_bank(self):
|
|
96
|
+
out = recall._resolve_sender_bank('{"@lisa": "clerk"}', "lisa", "clerk", [])
|
|
97
|
+
self.assertEqual(out, [])
|
|
98
|
+
|
|
99
|
+
def test_skips_duplicate(self):
|
|
100
|
+
out = recall._resolve_sender_bank('{"@lisa": "lisa-profile"}', "lisa", "clerk", ["lisa-profile"])
|
|
101
|
+
self.assertEqual(out, ["lisa-profile"])
|
|
102
|
+
|
|
103
|
+
def test_unmapped_sender_unchanged(self):
|
|
104
|
+
out = recall._resolve_sender_bank('{"@lisa": "lisa-profile"}', "stranger", "clerk", ["shared"])
|
|
105
|
+
self.assertEqual(out, ["shared"])
|
|
106
|
+
|
|
107
|
+
def test_no_sender_unchanged(self):
|
|
108
|
+
self.assertEqual(recall._resolve_sender_bank('{"@lisa": "x"}', None, "clerk", []), [])
|
|
109
|
+
|
|
110
|
+
def test_empty_env_unchanged(self):
|
|
111
|
+
self.assertEqual(recall._resolve_sender_bank("", "lisa", "clerk", ["a"]), ["a"])
|
|
112
|
+
|
|
113
|
+
def test_bad_json_unchanged(self):
|
|
114
|
+
self.assertEqual(recall._resolve_sender_bank("{not json", "lisa", "clerk", []), [])
|
|
115
|
+
|
|
116
|
+
def test_non_dict_json_unchanged(self):
|
|
117
|
+
self.assertEqual(recall._resolve_sender_bank('["lisa"]', "lisa", "clerk", []), [])
|
|
118
|
+
|
|
119
|
+
def test_does_not_mutate_input_list(self):
|
|
120
|
+
original = ["shared"]
|
|
121
|
+
out = recall._resolve_sender_bank('{"@lisa": "lisa-profile"}', "lisa", "clerk", original)
|
|
122
|
+
self.assertEqual(original, ["shared"]) # input untouched
|
|
123
|
+
self.assertEqual(out, ["shared", "lisa-profile"])
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__":
|
|
127
|
+
unittest.main()
|