switchroom 0.13.53 → 0.13.55
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 +53 -1
- package/dist/auth-broker/index.js +53 -1
- package/dist/cli/ms-365-write-pretool.mjs +259 -0
- package/dist/cli/notion-write-pretool.mjs +13388 -0
- package/dist/cli/switchroom.js +1601 -380
- package/dist/host-control/main.js +53 -1
- package/dist/vault/approvals/kernel-server.js +54 -2
- package/dist/vault/broker/server.js +54 -2
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +17 -0
- package/profiles/_shared/telegram-style.md.hbs +2 -0
- package/skills/notion/SKILL.md +144 -0
- package/telegram-plugin/dist/gateway/gateway.js +406 -43
- package/telegram-plugin/gateway/gateway.ts +227 -17
- package/telegram-plugin/gateway/ipc-protocol.ts +37 -0
- package/telegram-plugin/gateway/ipc-server.ts +59 -0
- package/telegram-plugin/gateway/ms365-write-approval.test.ts +314 -0
- package/telegram-plugin/gateway/ms365-write-approval.ts +335 -0
- package/telegram-plugin/tests/ipc-validator.test.ts +61 -0
- package/telegram-plugin/tests/slash-command-smart-split.test.ts +115 -0
- package/vendor/hindsight-memory/scripts/lib/gateway_ipc.py +35 -0
- package/vendor/hindsight-memory/scripts/recall.py +164 -4
- package/vendor/hindsight-memory/scripts/retain.py +52 -0
- package/vendor/hindsight-memory/scripts/tests/test_gateway_ipc.py +42 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_topic_filter.py +139 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""PR6 — unit tests for recall.py's topic-aware helpers.
|
|
2
|
+
|
|
3
|
+
Covers:
|
|
4
|
+
- _filter_by_active_topic — drops memories whose metadata.thread_id
|
|
5
|
+
differs from the active prompt's thread_id; passes through untagged
|
|
6
|
+
legacy memories.
|
|
7
|
+
- _summarise_source_topics — distribution counts used in the recall
|
|
8
|
+
log for binding-failure instrumentation.
|
|
9
|
+
- _topic_filter_mode — env-var parsing with safe default.
|
|
10
|
+
- _cache_key — active_thread_id participates in hash (cross-topic
|
|
11
|
+
prompts in supergroup mode mustn't collide on the cache).
|
|
12
|
+
|
|
13
|
+
Stdlib-only.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
import unittest
|
|
19
|
+
|
|
20
|
+
SCRIPTS_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
21
|
+
if SCRIPTS_DIR not in sys.path:
|
|
22
|
+
sys.path.insert(0, SCRIPTS_DIR)
|
|
23
|
+
|
|
24
|
+
from recall import ( # noqa: E402
|
|
25
|
+
_cache_key,
|
|
26
|
+
_filter_by_active_topic,
|
|
27
|
+
_summarise_source_topics,
|
|
28
|
+
_topic_filter_mode,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _mem(thread_id):
|
|
33
|
+
"""Fake memory record with the metadata shape recall sees."""
|
|
34
|
+
return {"id": f"m{thread_id}", "metadata": {"thread_id": thread_id}}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _untagged_mem(suffix=""):
|
|
38
|
+
return {"id": f"u{suffix}", "metadata": {"retained_at": "2026-01-01"}}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class FilterByActiveTopicTests(unittest.TestCase):
|
|
42
|
+
def test_drops_cross_topic_when_active_set(self):
|
|
43
|
+
results = [_mem("17"), _mem("31"), _mem("17")]
|
|
44
|
+
kept, dropped = _filter_by_active_topic(results, "17")
|
|
45
|
+
self.assertEqual(len(kept), 2)
|
|
46
|
+
self.assertEqual(dropped, 1)
|
|
47
|
+
self.assertTrue(all(m["metadata"]["thread_id"] == "17" for m in kept))
|
|
48
|
+
|
|
49
|
+
def test_passes_through_untagged_memories(self):
|
|
50
|
+
# Legacy memories (pre-PR6 retain) have no thread_id — must
|
|
51
|
+
# never be dropped, regardless of active topic.
|
|
52
|
+
results = [_mem("17"), _untagged_mem(), _mem("31"), _untagged_mem("b")]
|
|
53
|
+
kept, dropped = _filter_by_active_topic(results, "17")
|
|
54
|
+
# Kept: the 17 tagged + both untagged.
|
|
55
|
+
self.assertEqual(len(kept), 3)
|
|
56
|
+
self.assertEqual(dropped, 1)
|
|
57
|
+
|
|
58
|
+
def test_no_active_thread_is_passthrough(self):
|
|
59
|
+
# DM / fleet-shared agents have no active_thread_id; the
|
|
60
|
+
# filter is a no-op regardless of mode.
|
|
61
|
+
results = [_mem("17"), _mem("31"), _untagged_mem()]
|
|
62
|
+
kept, dropped = _filter_by_active_topic(results, None)
|
|
63
|
+
self.assertEqual(len(kept), 3)
|
|
64
|
+
self.assertEqual(dropped, 0)
|
|
65
|
+
|
|
66
|
+
def test_str_int_equivalence(self):
|
|
67
|
+
# Metadata can carry thread_ids as either string or int
|
|
68
|
+
# depending on how retain serialized them. The active_thread_id
|
|
69
|
+
# is always string (envelope is parsed as text). Compare as
|
|
70
|
+
# strings.
|
|
71
|
+
results = [{"id": "m", "metadata": {"thread_id": 17}}] # numeric
|
|
72
|
+
kept, dropped = _filter_by_active_topic(results, "17")
|
|
73
|
+
self.assertEqual(len(kept), 1)
|
|
74
|
+
self.assertEqual(dropped, 0)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class SummariseSourceTopicsTests(unittest.TestCase):
|
|
78
|
+
def test_counts_by_thread(self):
|
|
79
|
+
results = [_mem("17"), _mem("17"), _mem("31")]
|
|
80
|
+
self.assertEqual(_summarise_source_topics(results), {"17": 2, "31": 1})
|
|
81
|
+
|
|
82
|
+
def test_untagged_bucket(self):
|
|
83
|
+
results = [_mem("17"), _untagged_mem(), _untagged_mem()]
|
|
84
|
+
summary = _summarise_source_topics(results)
|
|
85
|
+
self.assertEqual(summary["17"], 1)
|
|
86
|
+
self.assertEqual(summary["__no_thread__"], 2)
|
|
87
|
+
|
|
88
|
+
def test_missing_metadata_bucket(self):
|
|
89
|
+
results = [{"id": "x"}] # no metadata key at all
|
|
90
|
+
self.assertEqual(_summarise_source_topics(results), {"__untagged__": 1})
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class TopicFilterModeTests(unittest.TestCase):
|
|
94
|
+
def setUp(self):
|
|
95
|
+
self._saved = os.environ.get("HINDSIGHT_TOPIC_FILTER_MODE")
|
|
96
|
+
os.environ.pop("HINDSIGHT_TOPIC_FILTER_MODE", None)
|
|
97
|
+
|
|
98
|
+
def tearDown(self):
|
|
99
|
+
if self._saved is None:
|
|
100
|
+
os.environ.pop("HINDSIGHT_TOPIC_FILTER_MODE", None)
|
|
101
|
+
else:
|
|
102
|
+
os.environ["HINDSIGHT_TOPIC_FILTER_MODE"] = self._saved
|
|
103
|
+
|
|
104
|
+
def test_default_is_soft_preamble(self):
|
|
105
|
+
self.assertEqual(_topic_filter_mode(), "soft-preamble")
|
|
106
|
+
|
|
107
|
+
def test_hard_filter_env(self):
|
|
108
|
+
os.environ["HINDSIGHT_TOPIC_FILTER_MODE"] = "hard-filter"
|
|
109
|
+
self.assertEqual(_topic_filter_mode(), "hard-filter")
|
|
110
|
+
|
|
111
|
+
def test_unknown_value_falls_back_to_default(self):
|
|
112
|
+
# Operator typos shouldn't silently enable a strict mode.
|
|
113
|
+
os.environ["HINDSIGHT_TOPIC_FILTER_MODE"] = "strict"
|
|
114
|
+
self.assertEqual(_topic_filter_mode(), "soft-preamble")
|
|
115
|
+
|
|
116
|
+
def test_case_insensitive(self):
|
|
117
|
+
os.environ["HINDSIGHT_TOPIC_FILTER_MODE"] = "Hard-Filter"
|
|
118
|
+
self.assertEqual(_topic_filter_mode(), "hard-filter")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class CacheKeyIncludesActiveTopicTests(unittest.TestCase):
|
|
122
|
+
def test_same_prompt_different_topic_misses(self):
|
|
123
|
+
k1 = _cache_key("sess", "what's up?", "bank", [], "17")
|
|
124
|
+
k2 = _cache_key("sess", "what's up?", "bank", [], "31")
|
|
125
|
+
self.assertNotEqual(k1, k2)
|
|
126
|
+
|
|
127
|
+
def test_backward_compat_no_topic(self):
|
|
128
|
+
# Pre-PR6 callers (none after this PR, but the param is
|
|
129
|
+
# optional so they couldn't break) get a stable key.
|
|
130
|
+
k1 = _cache_key("sess", "p", "bank", [])
|
|
131
|
+
k2 = _cache_key("sess", "p", "bank", [], None)
|
|
132
|
+
k3 = _cache_key("sess", "p", "bank", [], "")
|
|
133
|
+
# All three collapse to the empty-thread case → same hash.
|
|
134
|
+
self.assertEqual(k1, k2)
|
|
135
|
+
self.assertEqual(k1, k3)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
if __name__ == "__main__":
|
|
139
|
+
unittest.main()
|