switchroom 0.21.14 → 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.
- package/bin/rules-sentinel-hook.sh +101 -0
- package/dist/agent-scheduler/index.js +7 -2
- package/dist/auth-broker/index.js +7 -2
- package/dist/cli/notion-write-pretool.mjs +7 -2
- package/dist/cli/switchroom.js +2601 -1009
- package/dist/host-control/main.js +8 -3
- package/dist/vault/approvals/kernel-server.js +7 -2
- package/dist/vault/broker/server.js +7 -2
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +9 -0
- package/profiles/_shared/delegation-golden-rule.md.hbs +2 -0
- package/skills/mental-model-curator/SKILL.md +187 -56
- package/telegram-plugin/dist/gateway/gateway.js +11 -6
- package/vendor/hindsight-memory/hooks/hooks.json +10 -0
- package/vendor/hindsight-memory/scripts/lib/client.py +14 -0
- package/vendor/hindsight-memory/scripts/lib/config.py +22 -0
- package/vendor/hindsight-memory/scripts/lib/directives.py +45 -7
- package/vendor/hindsight-memory/scripts/lib/recall_buffer.py +236 -0
- package/vendor/hindsight-memory/scripts/lib/watermark.py +27 -0
- package/vendor/hindsight-memory/scripts/prefetch.py +156 -0
- package/vendor/hindsight-memory/scripts/recall.py +520 -5
- package/vendor/hindsight-memory/scripts/reconcile_tail.py +4 -12
- package/vendor/hindsight-memory/scripts/retain.py +167 -28
- package/vendor/hindsight-memory/scripts/tests/test_config_retain_tool_calls_env.py +98 -0
- package/vendor/hindsight-memory/scripts/tests/test_directives.py +52 -0
- package/vendor/hindsight-memory/scripts/tests/test_incremental_sweep.py +293 -0
- package/vendor/hindsight-memory/scripts/tests/test_prefetch_pipeline.py +247 -0
- package/vendor/hindsight-memory/scripts/tests/test_profile_capture_nudge.py +335 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_buffer.py +143 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_buffer_join.py +193 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_cap_truncation.py +133 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_junk_gate.py +168 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_no_score_floor.py +124 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_query_timestamp.py +376 -0
- package/vendor/hindsight-memory/scripts/tests/test_retain_delta.py +304 -0
- package/vendor/hindsight-memory/scripts/tests/test_retain_stop_hook_prefetch_gate.py +109 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""M4 P-REC test (e) — cap truncation, OUTCOME test.
|
|
2
|
+
|
|
3
|
+
epic §M4 TDD plan (e): "candidate sets larger than 8/4096 are truncated at
|
|
4
|
+
injection, logged truthfully." The truncation mechanism itself
|
|
5
|
+
(``recallMaxMemories`` head-slice / bank-slot reservation, ``recall.py``
|
|
6
|
+
~2757-2795) already exists pre-M4; this is the M4-scoped regression guard
|
|
7
|
+
that asserts it on RENDERED output and the truthful log row, per the
|
|
8
|
+
red-team's "assert on transport/rendered output, not a flag" rule — not
|
|
9
|
+
merely that ``capped`` was set internally.
|
|
10
|
+
|
|
11
|
+
Stdlib-only; drives ``recall.main()`` end to end against a fake client, no
|
|
12
|
+
network. Harness mirrors ``tests/test_recall_min_score.py``.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import io
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import shutil
|
|
19
|
+
import sys
|
|
20
|
+
import tempfile
|
|
21
|
+
import unittest
|
|
22
|
+
from unittest.mock import patch
|
|
23
|
+
|
|
24
|
+
SCRIPTS_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
25
|
+
if SCRIPTS_DIR not in sys.path:
|
|
26
|
+
sys.path.insert(0, SCRIPTS_DIR)
|
|
27
|
+
|
|
28
|
+
import recall # noqa: E402
|
|
29
|
+
|
|
30
|
+
OWN = "test-bank"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _memory(text, mem_id, score):
|
|
34
|
+
return {"text": text, "type": "fact", "mentioned_at": "2026-01-01", "id": mem_id,
|
|
35
|
+
"scores": {"final": score}}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _Client:
|
|
39
|
+
def __init__(self, results):
|
|
40
|
+
self._results = results
|
|
41
|
+
|
|
42
|
+
def list_directives(self, bank_id, active_only=True, timeout=2):
|
|
43
|
+
return {"items": []}
|
|
44
|
+
|
|
45
|
+
def recall(self, bank_id, query, **kwargs):
|
|
46
|
+
return {"results": [dict(m) for m in self._results]}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class CapTruncationTests(unittest.TestCase):
|
|
50
|
+
def setUp(self):
|
|
51
|
+
self._tmpdir = tempfile.mkdtemp(prefix="recall-cap-test-")
|
|
52
|
+
self._prev = os.environ.get("CLAUDE_PLUGIN_DATA")
|
|
53
|
+
os.environ["CLAUDE_PLUGIN_DATA"] = self._tmpdir
|
|
54
|
+
|
|
55
|
+
def tearDown(self):
|
|
56
|
+
shutil.rmtree(self._tmpdir, ignore_errors=True)
|
|
57
|
+
if self._prev is None:
|
|
58
|
+
os.environ.pop("CLAUDE_PLUGIN_DATA", None)
|
|
59
|
+
else:
|
|
60
|
+
os.environ["CLAUDE_PLUGIN_DATA"] = self._prev
|
|
61
|
+
|
|
62
|
+
def _log_row(self):
|
|
63
|
+
path = os.path.join(self._tmpdir, "state", "recall_log.jsonl")
|
|
64
|
+
with open(path, encoding="utf-8") as fh:
|
|
65
|
+
rows = [json.loads(line) for line in fh if line.strip()]
|
|
66
|
+
self.assertTrue(rows, "no recall_log row was written")
|
|
67
|
+
return rows[-1]
|
|
68
|
+
|
|
69
|
+
def _run(self, n_candidates, max_memories):
|
|
70
|
+
# Distinct scores so ordering (and therefore which 8 survive) is
|
|
71
|
+
# deterministic: candidate 0 highest score .. candidate N-1 lowest.
|
|
72
|
+
results = [
|
|
73
|
+
_memory(f"candidate {i}", f"m{i}", score=1.0 - (i * 0.01))
|
|
74
|
+
for i in range(n_candidates)
|
|
75
|
+
]
|
|
76
|
+
client = _Client(results)
|
|
77
|
+
hook_input = {
|
|
78
|
+
"prompt": "what do you remember about the deploy pipeline",
|
|
79
|
+
"session_id": "test-session",
|
|
80
|
+
"transcript_path": "",
|
|
81
|
+
"cwd": "/tmp",
|
|
82
|
+
}
|
|
83
|
+
config = {
|
|
84
|
+
"autoRecall": True,
|
|
85
|
+
"bankId": OWN,
|
|
86
|
+
"recallMaxTokens": 4096,
|
|
87
|
+
"recallMaxMemories": max_memories,
|
|
88
|
+
"recallBudget": "mid",
|
|
89
|
+
"recallContextTurns": 1,
|
|
90
|
+
"recallMaxQueryChars": 800,
|
|
91
|
+
"recallPromptPreamble": "",
|
|
92
|
+
"recallParallelDeadlineSeconds": 5,
|
|
93
|
+
"directivesCacheTtlSeconds": 0,
|
|
94
|
+
}
|
|
95
|
+
stdout = io.StringIO()
|
|
96
|
+
with patch("recall.load_config", return_value=config), \
|
|
97
|
+
patch("recall.get_api_url", return_value="http://fake"), \
|
|
98
|
+
patch("recall.HindsightClient", return_value=client), \
|
|
99
|
+
patch("recall.ensure_bank_mission"), \
|
|
100
|
+
patch("sys.stdin", io.StringIO(json.dumps(hook_input))), \
|
|
101
|
+
patch("sys.stdout", stdout):
|
|
102
|
+
recall.main()
|
|
103
|
+
out = stdout.getvalue()
|
|
104
|
+
ctx = None
|
|
105
|
+
if out:
|
|
106
|
+
parsed = json.loads(out)
|
|
107
|
+
ctx = parsed.get("hookSpecificOutput", {}).get("additionalContext")
|
|
108
|
+
return ctx
|
|
109
|
+
|
|
110
|
+
def test_candidate_set_above_cap_is_truncated_in_rendered_output(self):
|
|
111
|
+
ctx = self._run(n_candidates=12, max_memories=8)
|
|
112
|
+
self.assertIsNotNone(ctx)
|
|
113
|
+
injected = sum(1 for i in range(12) if f"candidate {i}" in ctx)
|
|
114
|
+
self.assertEqual(injected, 8, "rendered output must contain exactly the cap's worth of memories")
|
|
115
|
+
|
|
116
|
+
def test_log_row_reports_capped_true_with_truthful_pre_cap_count(self):
|
|
117
|
+
self._run(n_candidates=12, max_memories=8)
|
|
118
|
+
row = self._log_row()
|
|
119
|
+
self.assertTrue(row["capped"])
|
|
120
|
+
self.assertEqual(row["pre_cap_count"], 12)
|
|
121
|
+
|
|
122
|
+
def test_candidate_set_at_or_below_cap_is_not_truncated(self):
|
|
123
|
+
ctx = self._run(n_candidates=8, max_memories=8)
|
|
124
|
+
self.assertIsNotNone(ctx)
|
|
125
|
+
for i in range(8):
|
|
126
|
+
self.assertIn(f"candidate {i}", ctx)
|
|
127
|
+
row = self._log_row()
|
|
128
|
+
self.assertFalse(row["capped"])
|
|
129
|
+
self.assertEqual(row["pre_cap_count"], 8)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if __name__ == "__main__":
|
|
133
|
+
unittest.main()
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""M4 P-REC test (d) — junk gate for `<task-notification>` envelopes.
|
|
2
|
+
|
|
3
|
+
carve-M4.md packet P-REC, test (d): a `<task-notification>`-prefixed prompt
|
|
4
|
+
must not run the noisy NON-directive recall — no `client.recall`, and no
|
|
5
|
+
non-directive memory in the emitted transport.
|
|
6
|
+
|
|
7
|
+
#4756 F2 directive exemption: DIRECTIVES are the one memory class that must
|
|
8
|
+
SURVIVE the gate — an agent's standing rules apply on every turn, synthetic or
|
|
9
|
+
not. So a gated turn STILL fetches + injects the `<active_directives>` block
|
|
10
|
+
(`client.list_directives`), while the `recall` result set stays suppressed.
|
|
11
|
+
Asserts on TRANSPORT (what reaches stdout + which client methods are touched),
|
|
12
|
+
not on an internal flag, per the red-team's "assert transport, not a flag" rule.
|
|
13
|
+
|
|
14
|
+
Distinct from the gateway's `<channel source=...>` envelope (a real human
|
|
15
|
+
message) — this only matches the CLI-native task-notification wrapper.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import io
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import shutil
|
|
22
|
+
import sys
|
|
23
|
+
import tempfile
|
|
24
|
+
import unittest
|
|
25
|
+
from unittest.mock import patch
|
|
26
|
+
|
|
27
|
+
SCRIPTS_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
28
|
+
if SCRIPTS_DIR not in sys.path:
|
|
29
|
+
sys.path.insert(0, SCRIPTS_DIR)
|
|
30
|
+
|
|
31
|
+
import recall # noqa: E402
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class _DirectiveExemptClient:
|
|
35
|
+
"""Models a gated task-notification turn after the #4756 F2 fix.
|
|
36
|
+
|
|
37
|
+
`list_directives` answers with one active directive (which MUST survive the
|
|
38
|
+
gate and be injected). `recall` returns a real memory but records that it
|
|
39
|
+
was called — the gate must NEVER call it, so a hit on `recall_called` (or
|
|
40
|
+
the memory text appearing in transport) is a regression to the pre-fix
|
|
41
|
+
behavior where the gate dropped directives too.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self):
|
|
45
|
+
self.recall_called = False
|
|
46
|
+
|
|
47
|
+
def list_directives(self, bank_id, active_only=True, timeout=2):
|
|
48
|
+
return {"items": [{
|
|
49
|
+
"id": "d1", "name": "always-sign-off", "priority": 10,
|
|
50
|
+
"content": "always end replies with a wave",
|
|
51
|
+
}]}
|
|
52
|
+
|
|
53
|
+
def recall(self, bank_id, query, **kwargs):
|
|
54
|
+
self.recall_called = True
|
|
55
|
+
return {"results": [{
|
|
56
|
+
"text": "a real memory", "type": "fact", "mentioned_at": "2026-01-01",
|
|
57
|
+
"id": "m1", "scores": {"final": 0.9},
|
|
58
|
+
}]}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class _AnsweringClient:
|
|
62
|
+
"""A normal client that DOES answer, used to prove the gate did NOT fire."""
|
|
63
|
+
|
|
64
|
+
def list_directives(self, bank_id, active_only=True, timeout=2):
|
|
65
|
+
return {"items": []}
|
|
66
|
+
|
|
67
|
+
def recall(self, bank_id, query, **kwargs):
|
|
68
|
+
return {"results": [{
|
|
69
|
+
"text": "a real memory", "type": "fact", "mentioned_at": "2026-01-01",
|
|
70
|
+
"id": "m1", "scores": {"final": 0.9},
|
|
71
|
+
}]}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class JunkGateTests(unittest.TestCase):
|
|
75
|
+
def setUp(self):
|
|
76
|
+
self._tmpdir = tempfile.mkdtemp(prefix="recall-junk-gate-test-")
|
|
77
|
+
self._prev = os.environ.get("CLAUDE_PLUGIN_DATA")
|
|
78
|
+
os.environ["CLAUDE_PLUGIN_DATA"] = self._tmpdir
|
|
79
|
+
|
|
80
|
+
def tearDown(self):
|
|
81
|
+
shutil.rmtree(self._tmpdir, ignore_errors=True)
|
|
82
|
+
if self._prev is None:
|
|
83
|
+
os.environ.pop("CLAUDE_PLUGIN_DATA", None)
|
|
84
|
+
else:
|
|
85
|
+
os.environ["CLAUDE_PLUGIN_DATA"] = self._prev
|
|
86
|
+
|
|
87
|
+
def _run(self, prompt, client):
|
|
88
|
+
hook_input = {
|
|
89
|
+
"prompt": prompt,
|
|
90
|
+
"session_id": "test-session",
|
|
91
|
+
"transcript_path": "",
|
|
92
|
+
"cwd": "/tmp",
|
|
93
|
+
}
|
|
94
|
+
config = {
|
|
95
|
+
"autoRecall": True,
|
|
96
|
+
"bankId": "test-bank",
|
|
97
|
+
"recallMaxTokens": 4096,
|
|
98
|
+
"recallBudget": "mid",
|
|
99
|
+
"recallContextTurns": 1,
|
|
100
|
+
"recallMaxQueryChars": 800,
|
|
101
|
+
"recallPromptPreamble": "",
|
|
102
|
+
"recallParallelDeadlineSeconds": 5,
|
|
103
|
+
"directivesCacheTtlSeconds": 0,
|
|
104
|
+
}
|
|
105
|
+
stdout = io.StringIO()
|
|
106
|
+
with patch("recall.load_config", return_value=config), \
|
|
107
|
+
patch("recall.get_api_url", return_value="http://fake"), \
|
|
108
|
+
patch("recall.HindsightClient", return_value=client), \
|
|
109
|
+
patch("recall.ensure_bank_mission"), \
|
|
110
|
+
patch("sys.stdin", io.StringIO(json.dumps(hook_input))), \
|
|
111
|
+
patch("sys.stdout", stdout):
|
|
112
|
+
recall.main()
|
|
113
|
+
return stdout.getvalue()
|
|
114
|
+
|
|
115
|
+
def test_task_notification_gate_injects_directives_but_suppresses_recall(self):
|
|
116
|
+
# #4756 F2: on a gated task-notification turn, DIRECTIVES must be
|
|
117
|
+
# injected while the non-directive `recall` result set is suppressed.
|
|
118
|
+
# A test that would pass under the OLD bug (gate emits nothing) is
|
|
119
|
+
# invalid — so assert the directive text is PRESENT (the bug emitted
|
|
120
|
+
# ""), the memory text is ABSENT, and `recall` was never called.
|
|
121
|
+
client = _DirectiveExemptClient()
|
|
122
|
+
out = self._run(
|
|
123
|
+
"<task-notification>a background task finished</task-notification>",
|
|
124
|
+
client,
|
|
125
|
+
)
|
|
126
|
+
self.assertTrue(out, "gated turn must still emit the directives block")
|
|
127
|
+
ctx = json.loads(out)["hookSpecificOutput"]["additionalContext"]
|
|
128
|
+
self.assertIn("<active_directives>", ctx)
|
|
129
|
+
self.assertIn("always end replies with a wave", ctx)
|
|
130
|
+
self.assertNotIn(
|
|
131
|
+
"a real memory", ctx,
|
|
132
|
+
"gate must suppress the non-directive recall result set",
|
|
133
|
+
)
|
|
134
|
+
self.assertFalse(
|
|
135
|
+
client.recall_called,
|
|
136
|
+
"gated turn must never run the memory recall HTTP call",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def test_task_notification_gate_with_no_directives_emits_nothing(self):
|
|
140
|
+
# No active directives → no empty wrapper, no recall: the gate stays a
|
|
141
|
+
# true no-op on the transport (byte-identical to pre-fix when the bank
|
|
142
|
+
# has nothing to inject).
|
|
143
|
+
client = _AnsweringClient() # list_directives returns items: []
|
|
144
|
+
out = self._run(
|
|
145
|
+
"<task-notification>a background task finished</task-notification>",
|
|
146
|
+
client,
|
|
147
|
+
)
|
|
148
|
+
self.assertEqual(
|
|
149
|
+
out, "",
|
|
150
|
+
"gated turn with no directives must emit no transport output",
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def test_ordinary_prompt_with_task_notification_substring_midstring_is_not_gated(self):
|
|
154
|
+
# Only a PREFIX match gates — a real user prompt that happens to
|
|
155
|
+
# mention the phrase must still recall normally and reach the
|
|
156
|
+
# network (proven here by using an ANSWERING client and asserting
|
|
157
|
+
# the real memory shows up in the rendered transport).
|
|
158
|
+
out = self._run(
|
|
159
|
+
"what does a <task-notification> even look like in our hooks?",
|
|
160
|
+
_AnsweringClient(),
|
|
161
|
+
)
|
|
162
|
+
self.assertTrue(out)
|
|
163
|
+
ctx = json.loads(out)["hookSpecificOutput"]["additionalContext"]
|
|
164
|
+
self.assertIn("a real memory", ctx)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
if __name__ == "__main__":
|
|
168
|
+
unittest.main()
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""M4 P-REC test (f) — no score floor applied outside degraded mode.
|
|
2
|
+
|
|
3
|
+
epic §M4 TDD plan (f): "a regression test asserting no score floor is
|
|
4
|
+
applied outside degraded mode (guards against the refused recommendation
|
|
5
|
+
sneaking in)." E-13 measured a real rank-1 hit scoring ~0.001 and any floor
|
|
6
|
+
emptied 28% of healthy recalls (#3761) — the floor MUST stay scoped to
|
|
7
|
+
degraded turns only (``recallMinScoreScope`` default ``"degraded"``).
|
|
8
|
+
|
|
9
|
+
This is a regression TRIPWIRE, not new behaviour: ``recall.py`` already
|
|
10
|
+
scopes the floor correctly (``2731-2735``). Per the red-team audit (Finding:
|
|
11
|
+
test (f) leaned on the ``min_score_applied`` flag alone), this asserts on
|
|
12
|
+
BOTH the flag AND the rendered injected content — the full candidate set
|
|
13
|
+
must actually reach ``additionalContext`` on a healthy turn even when a
|
|
14
|
+
floor is configured, so a future "add a real floor" PR that drops rows
|
|
15
|
+
while leaving the flag mis-set still fails RED.
|
|
16
|
+
|
|
17
|
+
Stdlib-only; drives ``recall.main()`` end to end. Reuses the harness shape
|
|
18
|
+
from ``tests/test_recall_min_score.py`` (kept in a dedicated M4 file per the
|
|
19
|
+
carve's named test-file convention).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import io
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import shutil
|
|
26
|
+
import sys
|
|
27
|
+
import tempfile
|
|
28
|
+
import unittest
|
|
29
|
+
from unittest.mock import patch
|
|
30
|
+
|
|
31
|
+
SCRIPTS_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
32
|
+
if SCRIPTS_DIR not in sys.path:
|
|
33
|
+
sys.path.insert(0, SCRIPTS_DIR)
|
|
34
|
+
|
|
35
|
+
import recall # noqa: E402
|
|
36
|
+
|
|
37
|
+
OWN = "test-bank"
|
|
38
|
+
NOISE = 0.0006 # E-13's measured rank-1-hit-that-scores-almost-zero regime
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _memory(text, mem_id, score):
|
|
42
|
+
return {"text": text, "type": "fact", "mentioned_at": "2026-01-01", "id": mem_id,
|
|
43
|
+
"scores": {"final": score}}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class _Client:
|
|
47
|
+
def __init__(self, results):
|
|
48
|
+
self._results = results
|
|
49
|
+
|
|
50
|
+
def list_directives(self, bank_id, active_only=True, timeout=2):
|
|
51
|
+
return {"items": []}
|
|
52
|
+
|
|
53
|
+
def recall(self, bank_id, query, **kwargs):
|
|
54
|
+
return {"results": [dict(m) for m in self._results]}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class NoScoreFloorOutsideDegradedTests(unittest.TestCase):
|
|
58
|
+
def setUp(self):
|
|
59
|
+
self._tmpdir = tempfile.mkdtemp(prefix="recall-no-floor-test-")
|
|
60
|
+
self._prev = os.environ.get("CLAUDE_PLUGIN_DATA")
|
|
61
|
+
os.environ["CLAUDE_PLUGIN_DATA"] = self._tmpdir
|
|
62
|
+
|
|
63
|
+
def tearDown(self):
|
|
64
|
+
shutil.rmtree(self._tmpdir, ignore_errors=True)
|
|
65
|
+
if self._prev is None:
|
|
66
|
+
os.environ.pop("CLAUDE_PLUGIN_DATA", None)
|
|
67
|
+
else:
|
|
68
|
+
os.environ["CLAUDE_PLUGIN_DATA"] = self._prev
|
|
69
|
+
|
|
70
|
+
def _log_row(self):
|
|
71
|
+
path = os.path.join(self._tmpdir, "state", "recall_log.jsonl")
|
|
72
|
+
with open(path, encoding="utf-8") as fh:
|
|
73
|
+
rows = [json.loads(line) for line in fh if line.strip()]
|
|
74
|
+
self.assertTrue(rows)
|
|
75
|
+
return rows[-1]
|
|
76
|
+
|
|
77
|
+
def test_healthy_turn_ignores_a_configured_floor_flag_and_rendered_output(self):
|
|
78
|
+
# 6 noise-scoring candidates, a floor set well above their score, own
|
|
79
|
+
# bank answers cleanly (no timeout/error) — a HEALTHY turn.
|
|
80
|
+
results = [_memory(f"noise fact {i}", f"m{i}", NOISE) for i in range(6)]
|
|
81
|
+
client = _Client(results)
|
|
82
|
+
hook_input = {
|
|
83
|
+
"prompt": "what did we decide about the auth flow",
|
|
84
|
+
"session_id": "test-session",
|
|
85
|
+
"transcript_path": "",
|
|
86
|
+
"cwd": "/tmp",
|
|
87
|
+
}
|
|
88
|
+
config = {
|
|
89
|
+
"autoRecall": True,
|
|
90
|
+
"bankId": OWN,
|
|
91
|
+
"recallMaxTokens": 4096,
|
|
92
|
+
"recallMinScore": 0.5, # configured floor, well above NOISE
|
|
93
|
+
"recallBudget": "mid",
|
|
94
|
+
"recallContextTurns": 1,
|
|
95
|
+
"recallMaxQueryChars": 800,
|
|
96
|
+
"recallPromptPreamble": "",
|
|
97
|
+
"recallParallelDeadlineSeconds": 5,
|
|
98
|
+
"directivesCacheTtlSeconds": 0,
|
|
99
|
+
}
|
|
100
|
+
stdout = io.StringIO()
|
|
101
|
+
with patch("recall.load_config", return_value=config), \
|
|
102
|
+
patch("recall.get_api_url", return_value="http://fake"), \
|
|
103
|
+
patch("recall.HindsightClient", return_value=client), \
|
|
104
|
+
patch("recall.ensure_bank_mission"), \
|
|
105
|
+
patch("sys.stdin", io.StringIO(json.dumps(hook_input))), \
|
|
106
|
+
patch("sys.stdout", stdout):
|
|
107
|
+
recall.main()
|
|
108
|
+
out = stdout.getvalue()
|
|
109
|
+
self.assertTrue(out)
|
|
110
|
+
ctx = json.loads(out)["hookSpecificOutput"]["additionalContext"]
|
|
111
|
+
|
|
112
|
+
# Rendered-output assertion (the redteam-mandated strengthening): the
|
|
113
|
+
# FULL pre-floor candidate set reaches the prompt, not just a flag.
|
|
114
|
+
for i in range(6):
|
|
115
|
+
self.assertIn(f"noise fact {i}", ctx,
|
|
116
|
+
"a score floor silently dropped a healthy-turn candidate")
|
|
117
|
+
|
|
118
|
+
row = self._log_row()
|
|
119
|
+
self.assertFalse(row["min_score_applied"])
|
|
120
|
+
self.assertEqual(row["dropped_below_min_score"], 0)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
if __name__ == "__main__":
|
|
124
|
+
unittest.main()
|