switchroom 0.21.15 → 0.21.16

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 (25) hide show
  1. package/bin/rules-sentinel-hook.sh +101 -0
  2. package/dist/agent-scheduler/index.js +1 -0
  3. package/dist/auth-broker/index.js +1 -0
  4. package/dist/cli/notion-write-pretool.mjs +1 -0
  5. package/dist/cli/switchroom.js +1924 -841
  6. package/dist/host-control/main.js +2 -1
  7. package/dist/vault/approvals/kernel-server.js +1 -0
  8. package/dist/vault/broker/server.js +1 -0
  9. package/package.json +1 -1
  10. package/profiles/_shared/delegation-golden-rule.md.hbs +2 -0
  11. package/skills/mental-model-curator/SKILL.md +187 -56
  12. package/telegram-plugin/dist/gateway/gateway.js +5 -4
  13. package/vendor/hindsight-memory/hooks/hooks.json +10 -0
  14. package/vendor/hindsight-memory/scripts/lib/recall_buffer.py +236 -0
  15. package/vendor/hindsight-memory/scripts/prefetch.py +156 -0
  16. package/vendor/hindsight-memory/scripts/recall.py +185 -0
  17. package/vendor/hindsight-memory/scripts/retain.py +132 -49
  18. package/vendor/hindsight-memory/scripts/tests/test_prefetch_pipeline.py +247 -0
  19. package/vendor/hindsight-memory/scripts/tests/test_recall_buffer.py +143 -0
  20. package/vendor/hindsight-memory/scripts/tests/test_recall_buffer_join.py +193 -0
  21. package/vendor/hindsight-memory/scripts/tests/test_recall_cap_truncation.py +133 -0
  22. package/vendor/hindsight-memory/scripts/tests/test_recall_junk_gate.py +168 -0
  23. package/vendor/hindsight-memory/scripts/tests/test_recall_no_score_floor.py +124 -0
  24. package/vendor/hindsight-memory/scripts/tests/test_retain_delta.py +304 -0
  25. package/vendor/hindsight-memory/scripts/tests/test_retain_stop_hook_prefetch_gate.py +109 -0
@@ -0,0 +1,304 @@
1
+ """M4 P-RET — per-turn delta retain, OUTCOME tests (red-team Finding A/A2).
2
+
3
+ Red-team verdict (redteam-M4.md, BINDING): as the carve drafted P-RET, a
4
+ per-turn delta retain at ``retainEveryNTurns==1`` would slice the transcript
5
+ tail and POST it under ``document_id={session_id}`` — the SAME id the
6
+ full-session path uses — which UPSERTS (overwrites) the whole session's
7
+ stored memory with just the latest turn on every successful turn. This is the
8
+ single most dangerous defect the red-team found (worse than the crash seam
9
+ the carve flagged as "the whole ballgame").
10
+
11
+ The fix under test: ``run_retain(hook_input, delta=True)`` must ALWAYS post
12
+ under a distinct, content-derived ``document_id`` (mirroring the existing
13
+ ``retainEveryNTurns>1`` chunked-mode strategy) — never ``{session_id}`` — so
14
+ each delta lands as its own document and prior turns are never truncated.
15
+
16
+ Stdlib-only (``python3 -m unittest discover tests/``). Every test drives the
17
+ real ``retain.run_retain`` against a FAKE in-process daemon (no network, no
18
+ LLM) and asserts OUTCOMES (what actually landed in the bank), never a code
19
+ path or an internal flag — per the M4 test-harness rule.
20
+ """
21
+
22
+ import json
23
+ import os
24
+ import shutil
25
+ import sys
26
+ import tempfile
27
+ import unittest
28
+ from unittest import mock
29
+
30
+ SCRIPTS_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
31
+ if SCRIPTS_DIR not in sys.path:
32
+ sys.path.insert(0, SCRIPTS_DIR)
33
+
34
+ import retain # noqa: E402
35
+ from lib import watermark # noqa: E402
36
+ from lib.client import HindsightClient # noqa: E402
37
+
38
+
39
+ class FakeDaemon:
40
+ """Records retained documents by document_id with upsert semantics —
41
+ exactly the hazard under test: a bad document_id choice silently
42
+ overwrites (upserts) a prior document instead of creating a new one."""
43
+
44
+ def __init__(self):
45
+ self.docs = {} # document_id -> content
46
+ self.posts = [] # [(document_id, content)]
47
+
48
+ def retain(self, bank_id, content, document_id="conversation", context=None,
49
+ metadata=None, tags=None, timeout=15, async_processing=True,
50
+ observation_scopes=None):
51
+ self.posts.append((document_id, content))
52
+ self.docs[document_id] = content
53
+ return {"ok": True}
54
+
55
+ def all_content(self) -> str:
56
+ return "\n".join(self.docs.values())
57
+
58
+
59
+ def _write_transcript(path, n_turns, prefix, start=0):
60
+ lines = []
61
+ for i in range(start, start + n_turns):
62
+ lines.append(json.dumps(
63
+ {"role": "user", "content": f"user turn {i}", "uuid": f"{prefix}-u{i}"}))
64
+ lines.append(json.dumps(
65
+ {"role": "assistant", "content": f"assistant turn {i}", "uuid": f"{prefix}-a{i}"}))
66
+ with open(path, "w", encoding="utf-8") as f:
67
+ f.write("\n".join(lines))
68
+
69
+
70
+ def _append_turn(path, i, prefix):
71
+ with open(path, "a", encoding="utf-8") as f:
72
+ f.write("\n")
73
+ f.write(json.dumps({"role": "user", "content": f"user turn {i}", "uuid": f"{prefix}-u{i}"}))
74
+ f.write("\n")
75
+ f.write(json.dumps({"role": "assistant", "content": f"assistant turn {i}", "uuid": f"{prefix}-a{i}"}))
76
+
77
+
78
+ class DeltaRetainBase(unittest.TestCase):
79
+ def setUp(self):
80
+ self.tmp = tempfile.mkdtemp(prefix="hs-m4-delta-")
81
+ self.plugin_root = os.path.join(self.tmp, "plugin_root")
82
+ self.home = os.path.join(self.tmp, "home")
83
+ self.data = os.path.join(self.tmp, "data")
84
+ self.transcripts = os.path.join(self.tmp, "transcripts")
85
+ for d in (self.plugin_root, self.home, self.data, self.transcripts):
86
+ os.makedirs(d)
87
+
88
+ self._write_settings(every_n_turns=1, prefetch_enabled=True)
89
+
90
+ self.env = mock.patch.dict(os.environ, {
91
+ "CLAUDE_PLUGIN_ROOT": self.plugin_root,
92
+ "CLAUDE_PLUGIN_DATA": self.data,
93
+ "HOME": self.home,
94
+ "HINDSIGHT_PENDING_DIR": os.path.join(self.home, ".hindsight", "pending-retains"),
95
+ "HINDSIGHT_RETAINED_DIR": os.path.join(self.home, ".hindsight", "retained"),
96
+ "HINDSIGHT_INFLIGHT_LOCK": os.path.join(self.home, ".hindsight", "retain-inflight.lock"),
97
+ "HINDSIGHT_TRANSCRIPTS_DIR": self.transcripts,
98
+ }, clear=False)
99
+ self.env.start()
100
+ for k in list(os.environ):
101
+ if k.startswith("HINDSIGHT_") and k not in (
102
+ "HINDSIGHT_PENDING_DIR", "HINDSIGHT_RETAINED_DIR",
103
+ "HINDSIGHT_INFLIGHT_LOCK", "HINDSIGHT_TRANSCRIPTS_DIR",
104
+ ):
105
+ os.environ.pop(k, None)
106
+
107
+ self.daemon = FakeDaemon()
108
+ self._patches = [
109
+ mock.patch.object(HindsightClient, "retain", self._fake_retain),
110
+ mock.patch("retain.get_api_url", return_value="http://fake"),
111
+ ]
112
+ for p in self._patches:
113
+ p.start()
114
+
115
+ def _write_settings(self, every_n_turns: int, prefetch_enabled: bool):
116
+ settings = {
117
+ "autoRetain": True,
118
+ "retainMode": "chunked",
119
+ "retainEveryNTurns": every_n_turns,
120
+ "retainOverlapTurns": 0,
121
+ "bankId": "test-bank",
122
+ "memoryPrefetchEnabled": prefetch_enabled,
123
+ }
124
+ with open(os.path.join(self.plugin_root, "settings.json"), "w") as f:
125
+ json.dump(settings, f)
126
+
127
+ def _fake_retain(self, *a, **kw):
128
+ return self.daemon.retain(*a, **kw)
129
+
130
+ def tearDown(self):
131
+ for p in self._patches:
132
+ p.stop()
133
+ self.env.stop()
134
+ shutil.rmtree(self.tmp, ignore_errors=True)
135
+
136
+ def _hook(self, session):
137
+ return {
138
+ "session_id": session,
139
+ "transcript_path": os.path.join(self.transcripts, f"{session}.jsonl"),
140
+ "cwd": "/x",
141
+ }
142
+
143
+
144
+ class TestDeltaRetainDoesNotTruncate(DeltaRetainBase):
145
+ """The RED test for red-team Finding A: retain turn N, then retain turn
146
+ N+1 — BOTH facts must survive, not just the latest. As the carve drafted
147
+ it (document_id={session_id} at every_n==1), the second delta would
148
+ upsert-overwrite the first and this test would fail."""
149
+
150
+ def test_round_trip_both_turns_survive_delta_retain(self):
151
+ session = "sessDelta"
152
+ path = self._hook(session)["transcript_path"]
153
+
154
+ # Turn N: state a novel fact.
155
+ _write_transcript(path, 1, session, start=0)
156
+ result_n = retain.run_retain(self._hook(session), force=False, delta=True)
157
+ self.assertEqual(result_n.get("status"), "ok")
158
+
159
+ # Turn N+1: state a second, distinct fact.
160
+ _append_turn(path, 1, session)
161
+ result_n1 = retain.run_retain(self._hook(session), force=False, delta=True)
162
+ self.assertEqual(result_n1.get("status"), "ok")
163
+
164
+ # The failing-first assertion: BOTH facts survive across the whole
165
+ # set of documents stored for this session — not just the latest.
166
+ blob = self.daemon.all_content()
167
+ self.assertIn("user turn 0", blob,
168
+ "turn N's fact was destroyed by the delta retain at turn N+1 "
169
+ "(document_id collision / truncating overwrite — Finding A)")
170
+ self.assertIn("user turn 1", blob)
171
+
172
+ def test_delta_document_id_is_never_the_bare_session_id(self):
173
+ # The load-bearing requirement: delta retain must NEVER post under
174
+ # document_id == session_id (that id is reserved for the
175
+ # full-session / non-delta path and upserts destructively).
176
+ session = "sessDeltaDocId"
177
+ path = self._hook(session)["transcript_path"]
178
+ _write_transcript(path, 1, session, start=0)
179
+ retain.run_retain(self._hook(session), force=False, delta=True)
180
+
181
+ self.assertNotIn(session, self.daemon.docs,
182
+ "delta retain posted under the bare {session_id} document_id "
183
+ "— this OVERWRITES the whole-session document on every turn")
184
+ self.assertEqual(len(self.daemon.docs), 1)
185
+
186
+ def test_second_delta_document_id_differs_from_first(self):
187
+ session = "sessDeltaDistinctIds"
188
+ path = self._hook(session)["transcript_path"]
189
+ _write_transcript(path, 1, session, start=0)
190
+ retain.run_retain(self._hook(session), force=False, delta=True)
191
+ first_ids = set(self.daemon.docs.keys())
192
+
193
+ _append_turn(path, 1, session)
194
+ retain.run_retain(self._hook(session), force=False, delta=True)
195
+ second_ids = set(self.daemon.docs.keys())
196
+
197
+ self.assertEqual(len(second_ids), 2, "each delta must land as its OWN document")
198
+ self.assertTrue(first_ids.issubset(second_ids))
199
+
200
+
201
+ class TestDeltaRetainSlicing(DeltaRetainBase):
202
+ def test_delta_slices_only_tail_after_watermark(self):
203
+ # The second delta's OWN document must contain only the NEW turn,
204
+ # not a re-post of the first turn's content (proves it's an
205
+ # incremental slice, not a full-window resend under a fresh id).
206
+ session = "sessDeltaSlice"
207
+ path = self._hook(session)["transcript_path"]
208
+ _write_transcript(path, 1, session, start=0)
209
+ retain.run_retain(self._hook(session), force=False, delta=True)
210
+ first_doc_content = list(self.daemon.docs.values())[0]
211
+
212
+ _append_turn(path, 1, session)
213
+ retain.run_retain(self._hook(session), force=False, delta=True)
214
+
215
+ newest_doc_id, newest_content = self.daemon.posts[-1]
216
+ self.assertIn("user turn 1", newest_content)
217
+ self.assertNotIn("user turn 0", newest_content,
218
+ "second delta re-sent the first turn — not an incremental slice")
219
+
220
+ def test_delta_bypasses_every_n_throttle(self):
221
+ # Delta mode is a distinct per-turn mechanism (called every turn by
222
+ # prefetch.py) — it must fire regardless of retainEveryNTurns, and
223
+ # must NOT consume/advance the every-Nth turn-count throttle.
224
+ self._write_settings(every_n_turns=5, prefetch_enabled=True)
225
+ session = "sessDeltaThrottle"
226
+ path = self._hook(session)["transcript_path"]
227
+ _write_transcript(path, 1, session, start=0)
228
+
229
+ result = retain.run_retain(self._hook(session), force=False, delta=True)
230
+ self.assertEqual(result.get("status"), "ok",
231
+ "delta retain must fire even when every_n_turns=5 and this "
232
+ "would be throttled on the normal (non-delta) path")
233
+
234
+ def test_delta_watermark_read_failure_degrades_to_full_window_no_raise(self):
235
+ # Mirrors the existing §4.3 hazard discipline: a watermark read
236
+ # failure on the delta path must degrade to a full-window slice
237
+ # (never raise, never emit an empty/lost slice).
238
+ session = "sessDeltaBoom"
239
+ path = self._hook(session)["transcript_path"]
240
+ _write_transcript(path, 2, session, start=0)
241
+
242
+ def _boom(_):
243
+ raise RuntimeError("simulated watermark read explosion")
244
+
245
+ with mock.patch("retain.watermark.load", side_effect=_boom):
246
+ try:
247
+ result = retain.run_retain(self._hook(session), force=False, delta=True)
248
+ except Exception as e: # noqa: BLE001
249
+ self.fail(f"delta retain propagated a watermark read failure: {e!r}")
250
+
251
+ self.assertEqual(result.get("status"), "ok")
252
+ blob = self.daemon.all_content()
253
+ self.assertIn("user turn 0", blob)
254
+ self.assertIn("user turn 1", blob)
255
+
256
+
257
+ class TestSessionEndInterplayAtCadenceOne(DeltaRetainBase):
258
+ """Finding A2: the SessionEnd force sweep only used the watermark at
259
+ n>1. On a flipped (every_n==1, prefetch-enabled) agent it must ALSO use
260
+ the watermark, so the SessionEnd sweep doesn't blindly re-post the whole
261
+ session under {session_id} on top of the per-turn delta documents."""
262
+
263
+ def test_force_sweep_at_cadence_one_is_incremental_when_prefetch_enabled(self):
264
+ self._write_settings(every_n_turns=1, prefetch_enabled=True)
265
+ session = "sessEndN1"
266
+ path = self._hook(session)["transcript_path"]
267
+ _write_transcript(path, 1, session, start=0)
268
+ retain.run_retain(self._hook(session), force=False, delta=True)
269
+
270
+ _append_turn(path, 1, session)
271
+ result = retain.run_retain(self._hook(session), force=True)
272
+ self.assertEqual(result.get("status"), "ok")
273
+
274
+ # The forced sweep's OWN post must be incremental (only the new
275
+ # tail), not a full-session resend that duplicates turn 0.
276
+ newest_doc_id, newest_content = self.daemon.posts[-1]
277
+ self.assertIn("user turn 1", newest_content)
278
+ self.assertNotIn("user turn 0", newest_content,
279
+ "SessionEnd force sweep at cadence-1 with prefetch enabled "
280
+ "ignored the watermark and re-swept the whole session")
281
+
282
+ def test_force_sweep_at_cadence_one_is_full_session_when_prefetch_disabled(self):
283
+ # Byte-identical-to-today guard: prefetch OFF (the fleet default)
284
+ # must keep the existing full-session force-sweep behaviour even if
285
+ # a watermark happens to exist.
286
+ self._write_settings(every_n_turns=1, prefetch_enabled=False)
287
+ session = "sessEndN1Off"
288
+ path = self._hook(session)["transcript_path"]
289
+ _write_transcript(path, 2, session, start=0)
290
+ watermark.commit(session, f"{session}-a0", "doc-prior",
291
+ ordered_uuids=[f"{session}-u0", f"{session}-a0",
292
+ f"{session}-u1", f"{session}-a1"])
293
+
294
+ result = retain.run_retain(self._hook(session), force=True)
295
+ self.assertEqual(result.get("status"), "ok")
296
+ self.assertIn(session, self.daemon.docs,
297
+ "prefetch-disabled n==1 sweep must post under the bare {session_id} id")
298
+ content = self.daemon.docs[session]
299
+ self.assertIn("user turn 0", content)
300
+ self.assertIn("user turn 1", content)
301
+
302
+
303
+ if __name__ == "__main__":
304
+ unittest.main()
@@ -0,0 +1,109 @@
1
+ """M4 P-WIRE — retain.py's own Stop-hook entrypoint must not double-retain
2
+ when `prefetch.py` (the new Stop-hook entry, hooks/hooks.json) is already
3
+ doing a delta retain every turn for this agent.
4
+
5
+ Outcome asserted: with `memoryPrefetchEnabled` on, `retain.main()` must NOT
6
+ reach `run_retain` at all this turn (transport-level: the daemon fake
7
+ receives zero retain calls). With the flag off (every agent today, the
8
+ default), `retain.main()` is untouched — proven against the SAME assertion
9
+ inverted, so a future accidental regression of the gate (always skipping)
10
+ would fail this file too.
11
+ """
12
+
13
+ import io
14
+ import json
15
+ import os
16
+ import shutil
17
+ import sys
18
+ import tempfile
19
+ import unittest
20
+ from unittest import mock
21
+
22
+ SCRIPTS_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
23
+ if SCRIPTS_DIR not in sys.path:
24
+ sys.path.insert(0, SCRIPTS_DIR)
25
+
26
+ import retain # noqa: E402
27
+ from lib.client import HindsightClient # noqa: E402
28
+
29
+
30
+ class FakeDaemon:
31
+ def __init__(self):
32
+ self.retain_calls = 0
33
+
34
+ def retain(self, *a, **kw):
35
+ self.retain_calls += 1
36
+ return {"status": "ok"}
37
+
38
+
39
+ class StopHookPrefetchGateBase(unittest.TestCase):
40
+ def setUp(self):
41
+ self.tmp = tempfile.mkdtemp(prefix="hs-m4-stopgate-")
42
+ self.plugin_root = os.path.join(self.tmp, "plugin_root")
43
+ self.home = os.path.join(self.tmp, "home")
44
+ self.data = os.path.join(self.tmp, "data")
45
+ for d in (self.plugin_root, self.home, self.data):
46
+ os.makedirs(d)
47
+
48
+ self.transcript_path = os.path.join(self.tmp, "t.jsonl")
49
+ with open(self.transcript_path, "w", encoding="utf-8") as f:
50
+ f.write(json.dumps({"role": "user", "content": "hello", "uuid": "u0"}) + "\n")
51
+ f.write(json.dumps({"role": "assistant", "content": "hi", "uuid": "a0"}) + "\n")
52
+
53
+ self.env = mock.patch.dict(os.environ, {
54
+ "CLAUDE_PLUGIN_ROOT": self.plugin_root,
55
+ "CLAUDE_PLUGIN_DATA": self.data,
56
+ "HOME": self.home,
57
+ }, clear=False)
58
+ self.env.start()
59
+
60
+ self.daemon = FakeDaemon()
61
+ self._patches = [
62
+ mock.patch.object(HindsightClient, "retain", self.daemon.retain),
63
+ mock.patch("retain.get_api_url", return_value="http://fake"),
64
+ ]
65
+ for p in self._patches:
66
+ p.start()
67
+
68
+ def tearDown(self):
69
+ for p in self._patches:
70
+ p.stop()
71
+ self.env.stop()
72
+ shutil.rmtree(self.tmp, ignore_errors=True)
73
+
74
+ def _write_settings(self, prefetch_enabled: bool):
75
+ settings = {
76
+ "autoRetain": True,
77
+ "bankId": "test-bank",
78
+ "retainMode": "chunked",
79
+ "retainEveryNTurns": 1,
80
+ "retainOverlapTurns": 0,
81
+ "memoryPrefetchEnabled": prefetch_enabled,
82
+ }
83
+ with open(os.path.join(self.plugin_root, "settings.json"), "w") as f:
84
+ json.dump(settings, f)
85
+
86
+ def _run_main(self):
87
+ hook_input = {"session_id": "s1", "transcript_path": self.transcript_path, "cwd": "/tmp"}
88
+ with mock.patch("sys.stdin", io.StringIO(json.dumps(hook_input))):
89
+ retain.main()
90
+
91
+
92
+ class PrefetchEnabledSkipsOwnRetainTests(StopHookPrefetchGateBase):
93
+ def test_flag_on_the_stop_hooks_own_retain_never_fires(self):
94
+ self._write_settings(prefetch_enabled=True)
95
+ self._run_main()
96
+ self.assertEqual(self.daemon.retain_calls, 0,
97
+ "retain.py's own Stop-hook retain must be skipped when prefetch.py owns retaining")
98
+
99
+
100
+ class PrefetchDisabledIsByteIdenticalTests(StopHookPrefetchGateBase):
101
+ def test_flag_off_the_stop_hooks_own_retain_runs_as_before(self):
102
+ self._write_settings(prefetch_enabled=False)
103
+ self._run_main()
104
+ self.assertEqual(self.daemon.retain_calls, 1,
105
+ "the default (flag off) path must retain exactly as it did pre-M4")
106
+
107
+
108
+ if __name__ == "__main__":
109
+ unittest.main()