switchroom 0.21.12 → 0.21.14
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 +23 -0
- package/dist/auth-broker/index.js +24 -1
- package/dist/cli/notion-write-pretool.mjs +23 -0
- package/dist/cli/switchroom.js +3763 -1897
- package/dist/host-control/main.js +40 -15
- package/dist/vault/approvals/kernel-server.js +24 -1
- package/dist/vault/broker/server.js +150 -10
- package/package.json +1 -1
- package/profiles/_shared/agent-self-service.md.hbs +32 -86
- package/profiles/_shared/vault-protocol.md.hbs +17 -62
- package/profiles/default/CLAUDE.md.hbs +76 -74
- package/skills/switchroom-architecture/SKILL.md +11 -7
- package/skills/switchroom-architecture/cascade.md +29 -10
- package/skills/switchroom-architecture/sub-agents.md +27 -26
- package/skills/switchroom-architecture/telegram.md +44 -17
- package/skills/switchroom-runtime/SKILL.md +32 -0
- package/skills/switchroom-status/SKILL.md +17 -18
- package/skills/switchroom-status/scripts/status.sh +8 -17
- package/skills/telegram-test-harness/SKILL.md +6 -5
- package/telegram-plugin/dist/gateway/gateway.js +133 -5
- package/telegram-plugin/gateway/gateway.ts +2 -0
- package/telegram-plugin/shared/utf8-sanitize.ts +269 -0
- package/telegram-plugin/tests/utf8-sanitize-wire.test.ts +588 -0
- package/vendor/hindsight-memory/scripts/lib/directives.py +18 -0
- package/vendor/hindsight-memory/scripts/recall.py +14 -0
- package/vendor/hindsight-memory/scripts/tests/test_directives.py +46 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_integration.py +53 -0
|
@@ -28,6 +28,7 @@ from lib.directives import ( # noqa: E402
|
|
|
28
28
|
fetch_active_directives,
|
|
29
29
|
fetch_active_directives_cached,
|
|
30
30
|
format_active_directives_block,
|
|
31
|
+
injected_directive_ids,
|
|
31
32
|
invalidate_directives_cache,
|
|
32
33
|
parse_active_directives_block,
|
|
33
34
|
rule_already_captured,
|
|
@@ -260,6 +261,51 @@ class FormatActiveDirectivesBlockTests(unittest.TestCase):
|
|
|
260
261
|
# Honours a custom cap the same way the formatter does.
|
|
261
262
|
self.assertEqual(count_omitted_directives(directives, max_directives=5), total - 5)
|
|
262
263
|
|
|
264
|
+
def test_injected_directive_ids_matches_the_rendered_block(self):
|
|
265
|
+
"""Memory-redesign step 1 (E-45 recommendation (b)): the id list
|
|
266
|
+
`recall.py` puts on the recall_log row must name exactly the
|
|
267
|
+
directives `format_active_directives_block` actually rendered —
|
|
268
|
+
not the full fetched set, and in the same priority order."""
|
|
269
|
+
total = MAX_DIRECTIVES + 4
|
|
270
|
+
directives = [
|
|
271
|
+
_directive(f"d{i}", f"c{i}", priority=total - i) for i in range(total)
|
|
272
|
+
]
|
|
273
|
+
with patch("sys.stderr", new=StringIO()):
|
|
274
|
+
out = format_active_directives_block(directives)
|
|
275
|
+
ids = injected_directive_ids(directives)
|
|
276
|
+
self.assertEqual(len(ids), MAX_DIRECTIVES)
|
|
277
|
+
# Real, concrete values — the head-slice in priority order, not a
|
|
278
|
+
# placeholder or a count.
|
|
279
|
+
self.assertEqual(ids, [f"id-d{i}" for i in range(MAX_DIRECTIVES)])
|
|
280
|
+
# Every injected id is actually present in the rendered block...
|
|
281
|
+
for i in range(MAX_DIRECTIVES):
|
|
282
|
+
self.assertIn(f"d{i}: c{i}", out)
|
|
283
|
+
# ...and the omitted tail's ids are excluded.
|
|
284
|
+
for i in range(MAX_DIRECTIVES, total):
|
|
285
|
+
self.assertNotIn(f"id-d{i}", ids)
|
|
286
|
+
self.assertNotIn(f"d{i}: c{i}", out)
|
|
287
|
+
|
|
288
|
+
def test_injected_directive_ids_under_cap_returns_all(self):
|
|
289
|
+
directives = [_directive(f"d{i}", f"c{i}", priority=5 - i) for i in range(3)]
|
|
290
|
+
self.assertEqual(injected_directive_ids(directives), ["id-d0", "id-d1", "id-d2"])
|
|
291
|
+
|
|
292
|
+
def test_injected_directive_ids_empty_list(self):
|
|
293
|
+
self.assertEqual(injected_directive_ids([]), [])
|
|
294
|
+
|
|
295
|
+
def test_injected_directive_ids_skips_malformed_entries(self):
|
|
296
|
+
directives = [
|
|
297
|
+
_directive("good", "content", priority=9),
|
|
298
|
+
{"priority": 5}, # no id — must not crash or contribute a None
|
|
299
|
+
"not-a-dict",
|
|
300
|
+
]
|
|
301
|
+
self.assertEqual(injected_directive_ids(directives), ["id-good"])
|
|
302
|
+
|
|
303
|
+
def test_injected_directive_ids_honours_custom_cap(self):
|
|
304
|
+
directives = [_directive(f"d{i}", f"c{i}", priority=10 - i) for i in range(5)]
|
|
305
|
+
self.assertEqual(
|
|
306
|
+
injected_directive_ids(directives, max_directives=2), ["id-d0", "id-d1"]
|
|
307
|
+
)
|
|
308
|
+
|
|
263
309
|
def test_no_warning_when_nothing_is_truncated(self):
|
|
264
310
|
directives = [_directive("only", "single", priority=5)]
|
|
265
311
|
with patch("sys.stderr", new=StringIO()) as fake_err:
|
|
@@ -470,6 +470,59 @@ class RecallTelemetryLogTests(unittest.TestCase):
|
|
|
470
470
|
self.assertIsNone(e["injected_score_median"])
|
|
471
471
|
self.assertIsNone(e["injected_score_max"])
|
|
472
472
|
|
|
473
|
+
def test_logs_injected_directive_ids(self):
|
|
474
|
+
"""Memory-redesign step 1 (E-45 recommendation (b)): the recall_log
|
|
475
|
+
row must name WHICH directives were injected, not just how many.
|
|
476
|
+
`directive_count` alone can't answer "which directives were never
|
|
477
|
+
once injected" — this makes that queryable.
|
|
478
|
+
"""
|
|
479
|
+
directives = [
|
|
480
|
+
_directive("first", "content one", priority=10),
|
|
481
|
+
_directive("second", "content two", priority=5),
|
|
482
|
+
]
|
|
483
|
+
client = _FakeClient(directives=directives, memories=[])
|
|
484
|
+
_run_main_with(client)
|
|
485
|
+
e = self._read_log()[0]
|
|
486
|
+
self.assertEqual(e["directive_count"], 2)
|
|
487
|
+
self.assertEqual(e["directive_ids"], ["id-first", "id-second"])
|
|
488
|
+
|
|
489
|
+
def test_directive_ids_preserve_priority_order_across_more_than_a_few(self):
|
|
490
|
+
"""Real values across several directives, in the rendered priority
|
|
491
|
+
order — not just a two-item happy path."""
|
|
492
|
+
directives = [
|
|
493
|
+
_directive(f"d{i}", f"c{i}", priority=10 - i) for i in range(6)
|
|
494
|
+
]
|
|
495
|
+
client = _FakeClient(directives=directives, memories=[])
|
|
496
|
+
_run_main_with(client)
|
|
497
|
+
e = self._read_log()[0]
|
|
498
|
+
self.assertEqual(e["directive_count"], 6)
|
|
499
|
+
self.assertEqual(
|
|
500
|
+
e["directive_ids"],
|
|
501
|
+
["id-d0", "id-d1", "id-d2", "id-d3", "id-d4", "id-d5"],
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
def test_directive_ids_null_on_cache_hit(self):
|
|
505
|
+
"""A cache hit replays a formatted context block, not a fetched
|
|
506
|
+
directive list — `directive_ids` must be null (schema-uniform with
|
|
507
|
+
`directive_count`/`directives_omitted`), never a stale prior value.
|
|
508
|
+
|
|
509
|
+
Forces the cache-HIT branch directly (rather than relying on a
|
|
510
|
+
second `main()` call actually persisting a cache entry — the
|
|
511
|
+
integration harness patches `write_state` to a no-op, so
|
|
512
|
+
`_cache_store` never lands between calls)."""
|
|
513
|
+
client = _FakeClient(directives=[_directive("only", "content", priority=5)], memories=[])
|
|
514
|
+
with patch.object(recall, "_cache_lookup", return_value="[Hindsight] cached context"), \
|
|
515
|
+
patch.dict(os.environ, {"HINDSIGHT_RECALL_CACHE_TTL_SECS": "300"}):
|
|
516
|
+
_run_main_with(client)
|
|
517
|
+
entries = self._read_log()
|
|
518
|
+
self.assertEqual(len(entries), 1)
|
|
519
|
+
e = entries[0]
|
|
520
|
+
self.assertTrue(e["cache_hit"])
|
|
521
|
+
self.assertIsNone(e["directive_ids"])
|
|
522
|
+
# Schema-uniform with the fields it mirrors.
|
|
523
|
+
self.assertIsNone(e["directive_count"])
|
|
524
|
+
self.assertIsNone(e["directives_omitted"])
|
|
525
|
+
|
|
473
526
|
def test_no_log_when_plugin_data_unset(self):
|
|
474
527
|
# If CLAUDE_PLUGIN_DATA isn't set, the writer no-ops silently —
|
|
475
528
|
# we don't want a stray log file in the working directory.
|