switchroom 0.17.6 → 0.18.3
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/bin/workspace-dynamic-hook.sh +12 -13
- package/dist/agent-scheduler/index.js +65 -5
- package/dist/auth-broker/index.js +6623 -514
- package/dist/cli/notion-write-pretool.mjs +64 -4
- package/dist/cli/switchroom.js +1888 -1162
- package/dist/host-control/main.js +6306 -162
- package/dist/vault/approvals/kernel-server.js +6014 -202
- package/dist/vault/broker/server.js +6741 -940
- package/package.json +1 -1
- package/profiles/_base/settings.json.hbs +2 -2
- package/profiles/_base/start.sh.hbs +218 -25
- package/profiles/coding/CLAUDE.md.hbs +1 -1
- package/profiles/default/CLAUDE.md +116 -0
- package/profiles/default/CLAUDE.md.hbs +2 -2
- package/profiles/executive-assistant/CLAUDE.md.hbs +1 -1
- package/profiles/health-coach/CLAUDE.md.hbs +1 -1
- package/skills/mental-model-curator/SKILL.md +162 -0
- package/telegram-plugin/auth-snapshot-format.ts +22 -24
- package/telegram-plugin/bridge/bridge.ts +80 -1
- package/telegram-plugin/bridge/ipc-client.ts +19 -0
- package/telegram-plugin/bridge/permission-ledger.ts +61 -0
- package/telegram-plugin/consolidation-legibility.ts +279 -0
- package/telegram-plugin/context-exhaustion.ts +124 -0
- package/telegram-plugin/dist/bridge/bridge.js +85 -1
- package/telegram-plugin/dist/gateway/gateway.js +25802 -8488
- package/telegram-plugin/dist/server.js +86 -2
- package/telegram-plugin/feed-heartbeat-climb.ts +206 -0
- package/telegram-plugin/gateway/activity-card-store.ts +369 -0
- package/telegram-plugin/gateway/gateway.ts +1861 -172
- package/telegram-plugin/gateway/inbound-delivery-gate.ts +26 -0
- package/telegram-plugin/gateway/inbound-spool.ts +22 -0
- package/telegram-plugin/gateway/mental-model-propose-card.ts +69 -0
- package/telegram-plugin/gateway/mental-model-propose-diff.ts +171 -0
- package/telegram-plugin/gateway/mental-model-propose-inbound-builders.ts +147 -0
- package/telegram-plugin/gateway/mental-model-propose-resolve.ts +201 -0
- package/telegram-plugin/gateway/missed-approvals-card.ts +161 -0
- package/telegram-plugin/gateway/missed-approvals-store.ts +167 -0
- package/telegram-plugin/gateway/model-command.ts +70 -10
- package/telegram-plugin/gateway/permission-rearm.ts +115 -0
- package/telegram-plugin/gateway/scoped-grant-store.ts +89 -0
- package/telegram-plugin/memory-legibility.ts +217 -0
- package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/telegram-plugin/package.json +6 -0
- package/telegram-plugin/quota-watch.ts +4 -6
- package/telegram-plugin/registry/turns-schema.test.ts +97 -0
- package/telegram-plugin/registry/turns-schema.ts +78 -0
- package/telegram-plugin/render/ir.ts +209 -0
- package/telegram-plugin/render/parse.ts +363 -0
- package/telegram-plugin/render/render.ts +440 -0
- package/telegram-plugin/render/rich-render.ts +72 -0
- package/telegram-plugin/scoped-approval.ts +59 -0
- package/telegram-plugin/silent-end.ts +78 -0
- package/telegram-plugin/stream-controller.ts +14 -3
- package/telegram-plugin/subagent-watcher.ts +60 -6
- package/telegram-plugin/tests/activity-card-store.test.ts +530 -0
- package/telegram-plugin/tests/activity-card-wiring.test.ts +88 -0
- package/telegram-plugin/tests/auth-command-format2.test.ts +1 -1
- package/telegram-plugin/tests/auth-snapshot-format.test.ts +30 -16
- package/telegram-plugin/tests/claude-code-event-contract.test.ts +48 -0
- package/telegram-plugin/tests/consolidation-legibility.test.ts +224 -0
- package/telegram-plugin/tests/emission-authority-facade.test.ts +25 -10
- package/telegram-plugin/tests/feed-heartbeat-liveness-open.test.ts +44 -9
- package/telegram-plugin/tests/feed-survival.test.ts +39 -0
- package/telegram-plugin/tests/gateway-boot-marker-clear.test.ts +3 -3
- package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +81 -0
- package/telegram-plugin/tests/inbound-emit-after-intercepts.test.ts +82 -0
- package/telegram-plugin/tests/inbound-spool.test.ts +105 -0
- package/telegram-plugin/tests/liveness-tracker.test.ts +228 -0
- package/telegram-plugin/tests/memory-legibility.test.ts +216 -0
- package/telegram-plugin/tests/mental-model-propose-callback-gate.test.ts +67 -0
- package/telegram-plugin/tests/mental-model-propose-card.test.ts +56 -0
- package/telegram-plugin/tests/mental-model-propose-diff.test.ts +201 -0
- package/telegram-plugin/tests/mental-model-propose-inbound-builders.test.ts +68 -0
- package/telegram-plugin/tests/mental-model-propose-resolve.test.ts +157 -0
- package/telegram-plugin/tests/missed-approvals-card.test.ts +145 -0
- package/telegram-plugin/tests/missed-approvals-store.test.ts +147 -0
- package/telegram-plugin/tests/missed-approvals-wiring.test.ts +89 -0
- package/telegram-plugin/tests/model-command.test.ts +193 -16
- package/telegram-plugin/tests/narrative-render.test.ts +125 -0
- package/telegram-plugin/tests/orphaned-reply-rearm.test.ts +123 -163
- package/telegram-plugin/tests/permission-ledger.test.ts +166 -0
- package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +1 -1
- package/telegram-plugin/tests/permission-rearm-wiring.test.ts +175 -0
- package/telegram-plugin/tests/permission-rearm.test.ts +126 -0
- package/telegram-plugin/tests/quota-watch.test.ts +1 -4
- package/telegram-plugin/tests/rapid-fire-delivery-ordering.test.ts +149 -0
- package/telegram-plugin/tests/render/parse-torture.test.ts +136 -0
- package/telegram-plugin/tests/render/parse.test.ts +393 -0
- package/telegram-plugin/tests/render/render.test.ts +436 -0
- package/telegram-plugin/tests/render/rich-render.test.ts +85 -0
- package/telegram-plugin/tests/scoped-grant-persist.test.ts +223 -0
- package/telegram-plugin/tests/silent-end-transport.test.ts +290 -0
- package/telegram-plugin/tests/silent-turn-climb-transport.test.ts +337 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +139 -0
- package/telegram-plugin/tests/telegram-activity-visibility-integration.test.ts +155 -1
- package/telegram-plugin/tests/worktree-watch-cwds.test.ts +198 -0
- package/telegram-plugin/turn-liveness-floor.ts +35 -1
- package/telegram-plugin/uat/assertions.ts +88 -4
- package/telegram-plugin/uat/feed-matcher.test.ts +69 -0
- package/telegram-plugin/uat/scenarios/fuzz-liveness-climb-dm.test.ts +155 -0
- package/telegram-plugin/uat/scenarios/jtbd-directive-capture-nudge-dm.test.ts +185 -0
- package/telegram-plugin/uat/scenarios/jtbd-liveness-climb-channel.test.ts +192 -0
- package/telegram-plugin/uat/scenarios/jtbd-liveness-climb-dm.test.ts +220 -0
- package/telegram-plugin/uat/scenarios/jtbd-liveness-narration-channel.test.ts +137 -0
- package/telegram-plugin/uat/scenarios/jtbd-liveness-narration-dm.test.ts +148 -0
- package/telegram-plugin/uat/scenarios/jtbd-memory-legibility-channel.test.ts +66 -0
- package/telegram-plugin/uat/scenarios/jtbd-memory-legibility-dm.test.ts +61 -0
- package/telegram-plugin/uat/scenarios/jtbd-rich-formatting-render-dm.test.ts +99 -7
- package/telegram-plugin/uat/scenarios/silent-end-recovery-channel.test.ts +136 -0
- package/telegram-plugin/uat/scenarios/silent-end-recovery-dm.test.ts +24 -2
- package/telegram-plugin/worktree-watch-cwds.ts +135 -0
- package/vendor/hindsight-memory/hooks/hooks.json +9 -0
- package/vendor/hindsight-memory/scripts/__pycache__/directive_verify.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/drain_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/recall.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/retain.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/session_end.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/directive_verify.py +445 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/bank.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/client.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/config.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/content.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/daemon.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/directives.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/gateway_ipc.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/llm.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/state.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/switchroom_envelope.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/client.py +11 -1
- package/vendor/hindsight-memory/scripts/lib/config.py +46 -2
- package/vendor/hindsight-memory/scripts/lib/directives.py +88 -0
- package/vendor/hindsight-memory/scripts/lib/switchroom_envelope.py +77 -0
- package/vendor/hindsight-memory/scripts/recall.py +217 -10
- package/vendor/hindsight-memory/scripts/retain.py +17 -0
- package/vendor/hindsight-memory/scripts/setup_hooks.py +9 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/__init__.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_config_client_casts.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_config_client_casts.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_capture_nudge.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_capture_nudge.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_verify.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_verify.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directives.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directives.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_gateway_ipc.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_gateway_ipc.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_context_slice.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_context_slice.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_integration.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_integration.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_tag_filters.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_tag_filters.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_topic_filter.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_topic_filter.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_trivial_skip.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_trivial_skip.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_retain_window.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_retain_window.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_sender_routing.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_sender_routing.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_switchroom_envelope.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/test_directive_capture_nudge.py +185 -0
- package/vendor/hindsight-memory/scripts/tests/test_directive_verify.py +516 -0
- package/vendor/hindsight-memory/scripts/tests/test_directives.py +49 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_integration.py +1 -0
- package/vendor/hindsight-memory/scripts/tests/test_retain_window.py +66 -1
- package/vendor/hindsight-memory/scripts/tests/test_switchroom_envelope.py +69 -0
- package/vendor/hindsight-memory/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/conftest.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_bank.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_bank.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_client.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_client.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313-pytest-9.0.3.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_content.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_content.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_drain_pending.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_drain_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_hooks.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_hooks.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_manifest.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_manifest.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_pending.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_recall_exit_codes.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_recall_exit_codes.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_session_end_pending.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_session_end_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_state.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_state.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/test_client.py +43 -0
- package/vendor/hindsight-memory/tests/test_recall_exit_codes.py +49 -2
- package/vendor/hindsight-memory/tests/test_recall_precision.py +114 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Switchroom #2903 Fix 6.3 — the `<channel source="...">` envelope grammar is
|
|
2
|
+
extracted from directive_verify.py into lib/switchroom_envelope.py so the
|
|
3
|
+
coupling between the vendored Python guard and the switchroom gateway's TS wire
|
|
4
|
+
format is explicit and testable in ONE place.
|
|
5
|
+
|
|
6
|
+
Pins the grammar so a TS-side format change that breaks these regexes is caught
|
|
7
|
+
by a failing Python test rather than silently disabling the synthetic-inbound
|
|
8
|
+
guard.
|
|
9
|
+
|
|
10
|
+
Stdlib-only.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
import unittest
|
|
16
|
+
|
|
17
|
+
SCRIPTS_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
18
|
+
if SCRIPTS_DIR not in sys.path:
|
|
19
|
+
sys.path.insert(0, SCRIPTS_DIR)
|
|
20
|
+
|
|
21
|
+
from lib.switchroom_envelope import ( # noqa: E402
|
|
22
|
+
HUMAN_INBOUND_SOURCES,
|
|
23
|
+
envelope_source,
|
|
24
|
+
is_synthetic_inbound,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TestEnvelopeSource(unittest.TestCase):
|
|
29
|
+
def test_extracts_open_form(self):
|
|
30
|
+
self.assertEqual(
|
|
31
|
+
envelope_source('<channel source="telegram" chat_id="1">hi</channel>'),
|
|
32
|
+
"telegram",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def test_extracts_self_closing_form(self):
|
|
36
|
+
self.assertEqual(envelope_source('<channel source="reaction"/>'), "reaction")
|
|
37
|
+
|
|
38
|
+
def test_lowercases(self):
|
|
39
|
+
self.assertEqual(envelope_source('<channel source="Cron">x</channel>'), "cron")
|
|
40
|
+
|
|
41
|
+
def test_none_when_no_envelope(self):
|
|
42
|
+
self.assertIsNone(envelope_source("From now on, use British spelling."))
|
|
43
|
+
|
|
44
|
+
def test_none_on_non_string(self):
|
|
45
|
+
for bad in (None, 123, [], {}):
|
|
46
|
+
self.assertIsNone(envelope_source(bad))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TestSyntheticClassification(unittest.TestCase):
|
|
50
|
+
def test_telegram_is_human(self):
|
|
51
|
+
self.assertIn("telegram", HUMAN_INBOUND_SOURCES)
|
|
52
|
+
self.assertFalse(is_synthetic_inbound('<channel source="telegram">hi</channel>'))
|
|
53
|
+
|
|
54
|
+
def test_no_envelope_is_human(self):
|
|
55
|
+
self.assertFalse(is_synthetic_inbound("hello there"))
|
|
56
|
+
|
|
57
|
+
def test_new_source_defaults_to_synthetic(self):
|
|
58
|
+
# Whitelist posture: an UNKNOWN/new source is treated as machine-authored
|
|
59
|
+
# by default, not accidentally classified human.
|
|
60
|
+
self.assertTrue(is_synthetic_inbound('<channel source="brand_new_channel">x</channel>'))
|
|
61
|
+
|
|
62
|
+
def test_known_synthetic_sources(self):
|
|
63
|
+
for src in ("cron", "reaction", "resume_interrupted", "vault_grant_approved"):
|
|
64
|
+
with self.subTest(src=src):
|
|
65
|
+
self.assertTrue(is_synthetic_inbound(f'<channel source="{src}">x</channel>'))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
unittest.main()
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/vendor/hindsight-memory/tests/__pycache__/test_drain_pending.cpython-313-pytest-9.1.1.pyc
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -274,6 +274,49 @@ class TestHindsightClientRecallTagFilters:
|
|
|
274
274
|
assert "tag_groups" not in captured["body"]
|
|
275
275
|
|
|
276
276
|
|
|
277
|
+
class TestHindsightClientPreferObservations:
|
|
278
|
+
"""Switchroom Phase-1 — prefer_observations forwarded in the recall body."""
|
|
279
|
+
|
|
280
|
+
def test_forwards_prefer_observations_true(self):
|
|
281
|
+
c = HindsightClient("http://localhost:9077")
|
|
282
|
+
captured = {}
|
|
283
|
+
|
|
284
|
+
def fake_open(req, timeout=None):
|
|
285
|
+
captured["body"] = json.loads(req.data.decode())
|
|
286
|
+
return FakeResp({"results": []})
|
|
287
|
+
|
|
288
|
+
with patch("urllib.request.urlopen", side_effect=fake_open):
|
|
289
|
+
c.recall("bank", "query", prefer_observations=True)
|
|
290
|
+
|
|
291
|
+
assert captured["body"]["prefer_observations"] is True
|
|
292
|
+
|
|
293
|
+
def test_forwards_prefer_observations_false(self):
|
|
294
|
+
c = HindsightClient("http://localhost:9077")
|
|
295
|
+
captured = {}
|
|
296
|
+
|
|
297
|
+
def fake_open(req, timeout=None):
|
|
298
|
+
captured["body"] = json.loads(req.data.decode())
|
|
299
|
+
return FakeResp({"results": []})
|
|
300
|
+
|
|
301
|
+
with patch("urllib.request.urlopen", side_effect=fake_open):
|
|
302
|
+
c.recall("bank", "query", prefer_observations=False)
|
|
303
|
+
|
|
304
|
+
assert captured["body"]["prefer_observations"] is False
|
|
305
|
+
|
|
306
|
+
def test_omits_prefer_observations_when_unset(self):
|
|
307
|
+
c = HindsightClient("http://localhost:9077")
|
|
308
|
+
captured = {}
|
|
309
|
+
|
|
310
|
+
def fake_open(req, timeout=None):
|
|
311
|
+
captured["body"] = json.loads(req.data.decode())
|
|
312
|
+
return FakeResp({"results": []})
|
|
313
|
+
|
|
314
|
+
with patch("urllib.request.urlopen", side_effect=fake_open):
|
|
315
|
+
c.recall("bank", "query")
|
|
316
|
+
|
|
317
|
+
assert "prefer_observations" not in captured["body"]
|
|
318
|
+
|
|
319
|
+
|
|
277
320
|
class TestRequestTimeoutOverride:
|
|
278
321
|
"""Upstream 55ef70679 — the constructor override replaces the per-call
|
|
279
322
|
timeout that recall/retain/_request would otherwise use. When unset,
|
|
@@ -37,8 +37,10 @@ executes.
|
|
|
37
37
|
|
|
38
38
|
import json
|
|
39
39
|
import os
|
|
40
|
+
import shutil
|
|
40
41
|
import subprocess
|
|
41
42
|
import sys
|
|
43
|
+
import tempfile
|
|
42
44
|
import textwrap
|
|
43
45
|
|
|
44
46
|
|
|
@@ -48,6 +50,52 @@ SCRIPTS_DIR = os.path.abspath(
|
|
|
48
50
|
RECALL_PY = os.path.join(SCRIPTS_DIR, "recall.py")
|
|
49
51
|
|
|
50
52
|
|
|
53
|
+
def _dir_is_exec_capable(path):
|
|
54
|
+
"""Return True iff a file created under ``path`` can be executed.
|
|
55
|
+
|
|
56
|
+
pytest's ``tmp_path`` defaults to the system temp dir, which on
|
|
57
|
+
hardened hosts / CI containers is frequently mounted ``noexec``
|
|
58
|
+
(verify with ``findmnt -T /tmp``). recall.py invokes the
|
|
59
|
+
``switchroom`` CLI *by name* via ``subprocess.run`` — that resolves
|
|
60
|
+
to our on-PATH shim, which must therefore live on an
|
|
61
|
+
exec-capable filesystem or the call dies with PermissionError and
|
|
62
|
+
the shim never runs. Probe the real invariant rather than assuming.
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
probe = os.path.join(path, ".exec_probe.sh")
|
|
66
|
+
with open(probe, "w") as fh:
|
|
67
|
+
fh.write("#!/usr/bin/env bash\nexit 0\n")
|
|
68
|
+
os.chmod(probe, 0o755)
|
|
69
|
+
rc = subprocess.run([probe], capture_output=True).returncode
|
|
70
|
+
os.unlink(probe)
|
|
71
|
+
return rc == 0
|
|
72
|
+
except (OSError, PermissionError):
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _make_exec_bindir(tmp_path):
|
|
77
|
+
"""Create the shim ``bin`` directory on an exec-capable filesystem.
|
|
78
|
+
|
|
79
|
+
Prefer ``tmp_path`` itself (keeps everything colocated); fall back
|
|
80
|
+
to a self-cleaning temp dir next to this test file — which lives in
|
|
81
|
+
the repo checkout, an exec-capable ext4/overlay mount — when the
|
|
82
|
+
default temp root is ``noexec``.
|
|
83
|
+
"""
|
|
84
|
+
if _dir_is_exec_capable(str(tmp_path)):
|
|
85
|
+
bindir = tmp_path / "bin"
|
|
86
|
+
bindir.mkdir(exist_ok=True)
|
|
87
|
+
return bindir
|
|
88
|
+
fallback_root = tempfile.mkdtemp(
|
|
89
|
+
prefix="sw-recall-bin-", dir=os.path.dirname(__file__)
|
|
90
|
+
)
|
|
91
|
+
import atexit
|
|
92
|
+
|
|
93
|
+
atexit.register(shutil.rmtree, fallback_root, ignore_errors=True)
|
|
94
|
+
import pathlib
|
|
95
|
+
|
|
96
|
+
return pathlib.Path(fallback_root)
|
|
97
|
+
|
|
98
|
+
|
|
51
99
|
def _run_recall_with_injected_fault(
|
|
52
100
|
tmp_path,
|
|
53
101
|
debug=False,
|
|
@@ -105,8 +153,7 @@ def _run_recall_with_injected_fault(
|
|
|
105
153
|
# Path manipulation for the switchroom shim. We always isolate
|
|
106
154
|
# PATH so the test doesn't accidentally invoke a real `switchroom`
|
|
107
155
|
# on the host — that would write to a real state dir.
|
|
108
|
-
bindir = tmp_path
|
|
109
|
-
bindir.mkdir(exist_ok=True)
|
|
156
|
+
bindir = _make_exec_bindir(tmp_path)
|
|
110
157
|
if switchroom_shim is not None:
|
|
111
158
|
sw = bindir / "switchroom"
|
|
112
159
|
sw.write_text(switchroom_shim)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Retrieval-precision measurement for the Switchroom Phase-1 memory changes.
|
|
2
|
+
|
|
3
|
+
Per the RFC's outcome-UAT rule (reference/rfcs/hindsight-memory-reimagined.md,
|
|
4
|
+
"Measurement"), the bank-starvation fix ships behind a check that compares
|
|
5
|
+
recalled-set relevance before/after the sort. These tests assert the two
|
|
6
|
+
load-bearing properties of the fix:
|
|
7
|
+
|
|
8
|
+
1. The merged multi-bank result set is ordered by the engine's real
|
|
9
|
+
relevance score (`scores.final`) descending.
|
|
10
|
+
2. A high-relevance memory from an ADDITIONAL bank is not starved by the
|
|
11
|
+
count cap when the own bank supplies enough lower-relevance hits to
|
|
12
|
+
fill it — the pre-fix bug (append-then-slice) would drop it.
|
|
13
|
+
|
|
14
|
+
`_sort_by_final_score` is the in-place sort applied just before the cap in
|
|
15
|
+
`process_recall`; the cap itself is a plain head-slice, reproduced here so the
|
|
16
|
+
before/after relevance of the capped set is directly comparable.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from recall import _result_final_score, _sort_by_final_score
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _mem(mem_id, final, bank):
|
|
23
|
+
"""A recall result as the engine returns it: text + a scores object."""
|
|
24
|
+
return {
|
|
25
|
+
"id": mem_id,
|
|
26
|
+
"text": f"memory {mem_id} from {bank}",
|
|
27
|
+
"bank": bank,
|
|
28
|
+
"scores": {"final": final, "semantic": final, "keyword": final},
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _capped_relevance(results, cap):
|
|
33
|
+
"""Mirror process_recall: head-slice at the cap, return kept scores."""
|
|
34
|
+
kept = results[:cap] if cap > 0 else results
|
|
35
|
+
return [_result_final_score(m) for m in kept]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TestFinalScoreExtraction:
|
|
39
|
+
def test_reads_final_score(self):
|
|
40
|
+
assert _result_final_score(_mem("a", 0.9, "own")) == 0.9
|
|
41
|
+
|
|
42
|
+
def test_missing_scores_sorts_last(self):
|
|
43
|
+
assert _result_final_score({"id": "x", "text": "t"}) == float("-inf")
|
|
44
|
+
|
|
45
|
+
def test_null_scores_sorts_last(self):
|
|
46
|
+
assert _result_final_score({"id": "x", "scores": None}) == float("-inf")
|
|
47
|
+
|
|
48
|
+
def test_missing_final_sorts_last(self):
|
|
49
|
+
assert _result_final_score({"id": "x", "scores": {"semantic": 0.5}}) == float("-inf")
|
|
50
|
+
|
|
51
|
+
def test_bool_is_not_a_score(self):
|
|
52
|
+
# True is an int subclass; it must not be mistaken for a 1.0 score.
|
|
53
|
+
assert _result_final_score({"id": "x", "scores": {"final": True}}) == float("-inf")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TestSortByFinalScore:
|
|
57
|
+
def test_orders_descending(self):
|
|
58
|
+
results = [_mem("a", 0.2, "own"), _mem("b", 0.9, "own"), _mem("c", 0.5, "own")]
|
|
59
|
+
_sort_by_final_score(results)
|
|
60
|
+
assert [m["id"] for m in results] == ["b", "c", "a"]
|
|
61
|
+
|
|
62
|
+
def test_stable_on_ties_preserves_own_bank_first(self):
|
|
63
|
+
# Own-bank appears before additional-bank in insertion order; on a
|
|
64
|
+
# score tie the stable sort must keep own-bank ahead.
|
|
65
|
+
results = [_mem("own1", 0.5, "own"), _mem("extra1", 0.5, "profile")]
|
|
66
|
+
_sort_by_final_score(results)
|
|
67
|
+
assert [m["id"] for m in results] == ["own1", "extra1"]
|
|
68
|
+
|
|
69
|
+
def test_scoreless_entries_sink_to_the_bottom(self):
|
|
70
|
+
results = [{"id": "noscore", "text": "t"}, _mem("scored", 0.1, "own")]
|
|
71
|
+
_sort_by_final_score(results)
|
|
72
|
+
assert results[0]["id"] == "scored"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class TestCrossBankStarvationRegression:
|
|
76
|
+
"""The bug the fix targets: a relevant additional-bank memory dropped at
|
|
77
|
+
the cap because own-bank hits were appended first."""
|
|
78
|
+
|
|
79
|
+
def test_high_relevance_additional_bank_memory_survives_cap(self):
|
|
80
|
+
# Own bank fills the cap with mediocre hits; the profile bank has the
|
|
81
|
+
# single most relevant memory. Pre-fix (append own, then extra, then
|
|
82
|
+
# head-slice) the profile hit lands at index 2 and is sliced off.
|
|
83
|
+
cap = 2
|
|
84
|
+
own = [_mem("own_lo1", 0.30, "own"), _mem("own_lo2", 0.25, "own")]
|
|
85
|
+
extra = [_mem("profile_hi", 0.95, "profile")]
|
|
86
|
+
merged = own + extra # exactly the pre-fix append order
|
|
87
|
+
|
|
88
|
+
# Before: append-then-slice starves the profile bank.
|
|
89
|
+
pre_fix_ids = [m["id"] for m in merged[:cap]]
|
|
90
|
+
assert "profile_hi" not in pre_fix_ids
|
|
91
|
+
|
|
92
|
+
# After: sort by scores.final before the slice keeps the best memory.
|
|
93
|
+
_sort_by_final_score(merged)
|
|
94
|
+
post_fix_ids = [m["id"] for m in merged[:cap]]
|
|
95
|
+
assert "profile_hi" in post_fix_ids
|
|
96
|
+
assert post_fix_ids[0] == "profile_hi"
|
|
97
|
+
|
|
98
|
+
def test_capped_set_relevance_is_no_worse_after_sort(self):
|
|
99
|
+
# Measurement: summed relevance of the capped set must not decrease.
|
|
100
|
+
cap = 3
|
|
101
|
+
merged = [
|
|
102
|
+
_mem("own1", 0.4, "own"),
|
|
103
|
+
_mem("own2", 0.35, "own"),
|
|
104
|
+
_mem("own3", 0.3, "own"),
|
|
105
|
+
_mem("profile1", 0.9, "profile"),
|
|
106
|
+
_mem("shared1", 0.8, "shared"),
|
|
107
|
+
]
|
|
108
|
+
before = sum(_capped_relevance(list(merged), cap))
|
|
109
|
+
_sort_by_final_score(merged)
|
|
110
|
+
after = sum(_capped_relevance(merged, cap))
|
|
111
|
+
assert after >= before
|
|
112
|
+
# And concretely, the two top additional-bank hits are now retained.
|
|
113
|
+
kept_ids = [m["id"] for m in merged[:cap]]
|
|
114
|
+
assert "profile1" in kept_ids and "shared1" in kept_ids
|