switchroom 0.21.13 → 0.21.15
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 +6 -2
- package/dist/auth-broker/index.js +6 -2
- package/dist/cli/notion-write-pretool.mjs +6 -2
- package/dist/cli/switchroom.js +1608 -699
- package/dist/host-control/main.js +22 -16
- package/dist/vault/approvals/kernel-server.js +6 -2
- package/dist/vault/broker/server.js +132 -11
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +9 -0
- 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-runtime/SKILL.md +32 -0
- package/telegram-plugin/dist/gateway/gateway.js +10 -6
- 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 +63 -7
- package/vendor/hindsight-memory/scripts/lib/watermark.py +27 -0
- package/vendor/hindsight-memory/scripts/recall.py +349 -5
- package/vendor/hindsight-memory/scripts/reconcile_tail.py +4 -12
- package/vendor/hindsight-memory/scripts/retain.py +59 -3
- package/vendor/hindsight-memory/scripts/tests/test_config_retain_tool_calls_env.py +98 -0
- package/vendor/hindsight-memory/scripts/tests/test_directives.py +98 -0
- package/vendor/hindsight-memory/scripts/tests/test_incremental_sweep.py +293 -0
- package/vendor/hindsight-memory/scripts/tests/test_profile_capture_nudge.py +335 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_integration.py +53 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_query_timestamp.py +376 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""Switchroom P2 (memory-redesign RFC §5) — pass a `query_timestamp` anchor
|
|
2
|
+
from the recall hook when the inbound prompt is time-relative.
|
|
3
|
+
|
|
4
|
+
`query_timestamp` is an ISO 8601 datetime naming when the query is being asked
|
|
5
|
+
(https://hindsight.vectorize.io/developer/api/recall). The engine uses it to
|
|
6
|
+
resolve relative temporal expressions in the query ("last week", "yesterday",
|
|
7
|
+
"on the 12th") and to anchor recency scoring. A REST probe on the live engine
|
|
8
|
+
(2026-08-17) confirmed the field is ACCEPTED and HONOURED by the recall body:
|
|
9
|
+
a malformed value 400s ("Invalid query_timestamp format. Expected ISO format")
|
|
10
|
+
and a different anchor changes the returned result ordering — so it is neither
|
|
11
|
+
ignored nor rejected.
|
|
12
|
+
|
|
13
|
+
The guarantees under test are OUTCOMES on the wire body and the recall_log
|
|
14
|
+
row, never a branch taken:
|
|
15
|
+
|
|
16
|
+
1. The date parser maps representative temporal phrases to a deterministic
|
|
17
|
+
anchor (the injected `now`, ISO 8601) and returns None for text with no
|
|
18
|
+
temporal phrase — a pure, IO-free, model-free regex.
|
|
19
|
+
2. Absent a temporal phrase the field is NEVER added to the recall body, so
|
|
20
|
+
the wire body is byte-identical to a pre-P2 client.
|
|
21
|
+
3. Present a temporal phrase, the exact anchor reaches the wire.
|
|
22
|
+
4. The recall_log row carries `query_timestamp` (the ISO value when it fired,
|
|
23
|
+
null otherwise) so its firing rate is measurable from day one.
|
|
24
|
+
5. The `recallQueryTimestamp: false` gate suppresses the field even on a
|
|
25
|
+
temporal prompt (the rollback lever).
|
|
26
|
+
|
|
27
|
+
Stdlib-only (unittest + mock); runs under ``python3 -m unittest discover
|
|
28
|
+
tests/``. Wire-body harness mirrors ``test_observation_scopes.py``; the
|
|
29
|
+
end-to-end harness mirrors ``test_recall_min_score.py``.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import io
|
|
33
|
+
import json
|
|
34
|
+
import os
|
|
35
|
+
import shutil
|
|
36
|
+
import sys
|
|
37
|
+
import tempfile
|
|
38
|
+
import unittest
|
|
39
|
+
from datetime import datetime, timedelta, timezone
|
|
40
|
+
from unittest.mock import patch
|
|
41
|
+
|
|
42
|
+
SCRIPTS_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
43
|
+
if SCRIPTS_DIR not in sys.path:
|
|
44
|
+
sys.path.insert(0, SCRIPTS_DIR)
|
|
45
|
+
|
|
46
|
+
import recall # noqa: E402
|
|
47
|
+
from recall import detect_query_timestamp # noqa: E402
|
|
48
|
+
from lib.client import HindsightClient # noqa: E402
|
|
49
|
+
|
|
50
|
+
# A fixed anchor so every parser assertion is byte-exact and clock-independent.
|
|
51
|
+
FIXED_NOW = datetime(2026, 8, 17, 12, 0, 0, tzinfo=timezone.utc)
|
|
52
|
+
FIXED_ISO = FIXED_NOW.isoformat()
|
|
53
|
+
|
|
54
|
+
OWN = "test-bank"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class DetectQueryTimestampParser(unittest.TestCase):
|
|
58
|
+
"""The deterministic temporal-expression parser (pure function)."""
|
|
59
|
+
|
|
60
|
+
# Representative phrases across the families the regex covers. Each MUST
|
|
61
|
+
# map to the injected anchor — the value is always "now" by the field's
|
|
62
|
+
# documented semantics (the engine resolves the phrase against the anchor).
|
|
63
|
+
TEMPORAL = [
|
|
64
|
+
"what did we work on last week",
|
|
65
|
+
"remind me what happened yesterday",
|
|
66
|
+
"did we ship it this month",
|
|
67
|
+
"what's on for next week",
|
|
68
|
+
"what did I say a couple of days ago",
|
|
69
|
+
"we discussed this 3 weeks ago",
|
|
70
|
+
"what did we decide on the 12th",
|
|
71
|
+
"the incident on tuesday",
|
|
72
|
+
"what did we do last tuesday",
|
|
73
|
+
"the plan from last night",
|
|
74
|
+
"back in june we agreed something",
|
|
75
|
+
"the deploy earlier today",
|
|
76
|
+
"the other day you mentioned a bug",
|
|
77
|
+
"the release two months ago",
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
# Ordinary sentences whose tokens brush temporal words but carry no
|
|
81
|
+
# actual temporal expression — the negative guard must keep the field off.
|
|
82
|
+
NON_TEMPORAL = [
|
|
83
|
+
"how do I restart the hindsight container",
|
|
84
|
+
"explain the recall cache key",
|
|
85
|
+
"may I ask about the auth flow", # bare "may" (modal), not a month
|
|
86
|
+
"the friday deploy script is broken", # bare weekday, no preposition
|
|
87
|
+
"august is a bank name here", # bare month, no preposition
|
|
88
|
+
"summarise the current architecture",
|
|
89
|
+
"what is 2 plus 2",
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
def test_temporal_phrases_map_to_the_injected_anchor(self):
|
|
93
|
+
for phrase in self.TEMPORAL:
|
|
94
|
+
with self.subTest(phrase=phrase):
|
|
95
|
+
self.assertEqual(
|
|
96
|
+
detect_query_timestamp(phrase, now=FIXED_NOW),
|
|
97
|
+
FIXED_ISO,
|
|
98
|
+
f"expected anchor for temporal phrase: {phrase!r}",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def test_non_temporal_text_returns_none(self):
|
|
102
|
+
for phrase in self.NON_TEMPORAL:
|
|
103
|
+
with self.subTest(phrase=phrase):
|
|
104
|
+
self.assertIsNone(
|
|
105
|
+
detect_query_timestamp(phrase, now=FIXED_NOW),
|
|
106
|
+
f"unexpected anchor for non-temporal phrase: {phrase!r}",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def test_empty_and_non_string_return_none(self):
|
|
110
|
+
self.assertIsNone(detect_query_timestamp("", now=FIXED_NOW))
|
|
111
|
+
self.assertIsNone(detect_query_timestamp(" ", now=FIXED_NOW))
|
|
112
|
+
self.assertIsNone(detect_query_timestamp(None, now=FIXED_NOW))
|
|
113
|
+
self.assertIsNone(detect_query_timestamp(42, now=FIXED_NOW))
|
|
114
|
+
|
|
115
|
+
def test_default_anchor_is_local_wall_clock_not_utc(self):
|
|
116
|
+
# No injected now → real clock. The anchor must carry the PROCESS-LOCAL
|
|
117
|
+
# offset (datetime.now().astimezone()), never a hardcoded UTC. A
|
|
118
|
+
# UTC-stamped anchor tells the engine the operator is in UTC and
|
|
119
|
+
# resolves "yesterday"/"last week"/"on the 12th" against the wrong
|
|
120
|
+
# calendar day for a Melbourne query — the exact off-by-one P2 serves.
|
|
121
|
+
out = detect_query_timestamp("what did we do yesterday")
|
|
122
|
+
self.assertIsNotNone(out)
|
|
123
|
+
parsed = datetime.fromisoformat(out)
|
|
124
|
+
self.assertIsNotNone(parsed.tzinfo, "anchor must be timezone-aware")
|
|
125
|
+
# The offset is the process-local one, whatever the CI TZ — not an
|
|
126
|
+
# assumed UTC. In the Melbourne container this is +10/+11, never +00:00.
|
|
127
|
+
self.assertEqual(
|
|
128
|
+
parsed.utcoffset(),
|
|
129
|
+
datetime.now().astimezone().utcoffset(),
|
|
130
|
+
"anchor offset must be the process-local offset, not UTC",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def test_injected_local_instant_keeps_the_local_day_not_the_utc_day(self):
|
|
134
|
+
# Pin a Melbourne-morning instant (UTC+10) whose UTC calendar day is
|
|
135
|
+
# the PREVIOUS day. The returned anchor must keep the LOCAL day (17th),
|
|
136
|
+
# because that is the operator's real "today"; a UTC conversion would
|
|
137
|
+
# slip it to the 16th and mis-resolve every relative phrase by a day.
|
|
138
|
+
aest = timezone(timedelta(hours=10))
|
|
139
|
+
local_now = datetime(2026, 8, 17, 9, 0, 0, tzinfo=aest)
|
|
140
|
+
# Guard the fixture itself: local and UTC really are different days.
|
|
141
|
+
self.assertEqual(local_now.date().isoformat(), "2026-08-17")
|
|
142
|
+
self.assertEqual(
|
|
143
|
+
local_now.astimezone(timezone.utc).date().isoformat(), "2026-08-16"
|
|
144
|
+
)
|
|
145
|
+
out = detect_query_timestamp("what did we do yesterday", now=local_now)
|
|
146
|
+
parsed = datetime.fromisoformat(out)
|
|
147
|
+
self.assertEqual(parsed.date().isoformat(), "2026-08-17")
|
|
148
|
+
self.assertEqual(parsed.utcoffset(), timedelta(hours=10))
|
|
149
|
+
|
|
150
|
+
def test_parser_is_pure_and_deterministic(self):
|
|
151
|
+
# Same input + same now → identical output, every call.
|
|
152
|
+
a = detect_query_timestamp("what did we work on last week", now=FIXED_NOW)
|
|
153
|
+
b = detect_query_timestamp("what did we work on last week", now=FIXED_NOW)
|
|
154
|
+
self.assertEqual(a, b)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class _RecordingClient(HindsightClient):
|
|
158
|
+
"""Captures request bodies instead of putting them on a socket."""
|
|
159
|
+
|
|
160
|
+
def __init__(self, *a, **kw):
|
|
161
|
+
super().__init__(*a, **kw)
|
|
162
|
+
self.bodies = []
|
|
163
|
+
|
|
164
|
+
def _request(self, method, path, body=None, timeout=30):
|
|
165
|
+
self.bodies.append(body)
|
|
166
|
+
return {"results": []}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class WireBody(unittest.TestCase):
|
|
170
|
+
"""What actually goes on the recall wire — the additive-field invariant."""
|
|
171
|
+
|
|
172
|
+
def setUp(self):
|
|
173
|
+
self.client = _RecordingClient("http://hindsight.invalid")
|
|
174
|
+
|
|
175
|
+
def test_unset_omits_the_key_entirely(self):
|
|
176
|
+
self.client.recall("bank", "a query")
|
|
177
|
+
# Not present-and-null — ABSENT. A pre-P2 body simply had no such key,
|
|
178
|
+
# so the engine's own current-time anchor stands.
|
|
179
|
+
self.assertNotIn("query_timestamp", self.client.bodies[0])
|
|
180
|
+
|
|
181
|
+
def test_explicit_none_omits_the_key_entirely(self):
|
|
182
|
+
self.client.recall("bank", "a query", query_timestamp=None)
|
|
183
|
+
self.assertNotIn("query_timestamp", self.client.bodies[0])
|
|
184
|
+
|
|
185
|
+
def test_unset_body_is_identical_to_a_pre_field_body(self):
|
|
186
|
+
# The literal body a pre-P2 client would have posted.
|
|
187
|
+
expected = {"query": "a query", "max_tokens": 1024, "budget": "mid"}
|
|
188
|
+
self.client.recall("bank", "a query")
|
|
189
|
+
self.assertEqual(self.client.bodies[0], expected)
|
|
190
|
+
|
|
191
|
+
def test_set_reaches_the_wire_verbatim(self):
|
|
192
|
+
self.client.recall("bank", "a query", query_timestamp=FIXED_ISO)
|
|
193
|
+
self.assertEqual(self.client.bodies[0]["query_timestamp"], FIXED_ISO)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class _E2EClient:
|
|
197
|
+
"""Fake client that records the query_timestamp kwarg each bank sees."""
|
|
198
|
+
|
|
199
|
+
def __init__(self):
|
|
200
|
+
self.recall_kwargs = []
|
|
201
|
+
|
|
202
|
+
def list_directives(self, bank_id, active_only=True, timeout=2):
|
|
203
|
+
return {"items": []}
|
|
204
|
+
|
|
205
|
+
def recall(self, bank_id, query, **kwargs):
|
|
206
|
+
self.recall_kwargs.append(kwargs.get("query_timestamp"))
|
|
207
|
+
return {"results": []}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class _StrictSignatureClient:
|
|
211
|
+
"""A pre-P2 client: recall() takes the exact old keyword set, with NO
|
|
212
|
+
query_timestamp and NO **kwargs. Handing it the kwarg would TypeError on
|
|
213
|
+
bind (before the body), so `calls` only increments on a clean pre-P2 call.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
def __init__(self):
|
|
217
|
+
self.calls = 0
|
|
218
|
+
|
|
219
|
+
def list_directives(self, bank_id, active_only=True, timeout=2):
|
|
220
|
+
return {"items": []}
|
|
221
|
+
|
|
222
|
+
def recall(
|
|
223
|
+
self,
|
|
224
|
+
bank_id,
|
|
225
|
+
query,
|
|
226
|
+
max_tokens=1024,
|
|
227
|
+
budget="mid",
|
|
228
|
+
types=None,
|
|
229
|
+
tags=None,
|
|
230
|
+
tags_match=None,
|
|
231
|
+
tag_groups=None,
|
|
232
|
+
prefer_observations=None,
|
|
233
|
+
timeout=10,
|
|
234
|
+
):
|
|
235
|
+
self.calls += 1
|
|
236
|
+
return {"results": []}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class RecallLogRow(unittest.TestCase):
|
|
240
|
+
"""Drives recall.main() end to end with an isolated recall log."""
|
|
241
|
+
|
|
242
|
+
def setUp(self):
|
|
243
|
+
self._tmpdir = tempfile.mkdtemp(prefix="recall-qts-test-")
|
|
244
|
+
self._prev = os.environ.get("CLAUDE_PLUGIN_DATA")
|
|
245
|
+
os.environ["CLAUDE_PLUGIN_DATA"] = self._tmpdir
|
|
246
|
+
|
|
247
|
+
def tearDown(self):
|
|
248
|
+
shutil.rmtree(self._tmpdir, ignore_errors=True)
|
|
249
|
+
if self._prev is None:
|
|
250
|
+
os.environ.pop("CLAUDE_PLUGIN_DATA", None)
|
|
251
|
+
else:
|
|
252
|
+
os.environ["CLAUDE_PLUGIN_DATA"] = self._prev
|
|
253
|
+
|
|
254
|
+
def _log_row(self):
|
|
255
|
+
path = os.path.join(self._tmpdir, "state", "recall_log.jsonl")
|
|
256
|
+
with open(path, encoding="utf-8") as fh:
|
|
257
|
+
rows = [json.loads(line) for line in fh if line.strip()]
|
|
258
|
+
self.assertTrue(rows, "no recall_log row was written")
|
|
259
|
+
return rows[-1]
|
|
260
|
+
|
|
261
|
+
def _run(self, prompt, client, config_extra=None, cache_hit_context=None):
|
|
262
|
+
hook_input = {
|
|
263
|
+
"prompt": prompt,
|
|
264
|
+
"session_id": "test-session",
|
|
265
|
+
"transcript_path": "",
|
|
266
|
+
"cwd": "/tmp",
|
|
267
|
+
}
|
|
268
|
+
config = {
|
|
269
|
+
"autoRecall": True,
|
|
270
|
+
"bankId": OWN,
|
|
271
|
+
"recallMaxTokens": 1024,
|
|
272
|
+
"recallBudget": "mid",
|
|
273
|
+
"recallContextTurns": 1,
|
|
274
|
+
"recallMaxQueryChars": 800,
|
|
275
|
+
"recallPromptPreamble": "",
|
|
276
|
+
"recallParallelDeadlineSeconds": 5,
|
|
277
|
+
"directivesCacheTtlSeconds": 0,
|
|
278
|
+
}
|
|
279
|
+
if config_extra:
|
|
280
|
+
config.update(config_extra)
|
|
281
|
+
# Cache-hit path: a positive TTL plus a lookup that returns context
|
|
282
|
+
# makes run_recall take the early cache-hit branch (no bank runs).
|
|
283
|
+
cache_ttl = 300 if cache_hit_context is not None else 0
|
|
284
|
+
stdout = io.StringIO()
|
|
285
|
+
stderr = io.StringIO()
|
|
286
|
+
with patch.object(recall, "load_config", return_value=config), patch.object(
|
|
287
|
+
recall, "get_api_url", return_value="http://localhost:18888"
|
|
288
|
+
), patch.object(recall, "HindsightClient", return_value=client), patch.object(
|
|
289
|
+
recall, "ensure_bank_mission", return_value=None
|
|
290
|
+
), patch.object(recall, "write_state", return_value=None), patch.object(
|
|
291
|
+
recall, "_cache_ttl_secs", return_value=cache_ttl
|
|
292
|
+
), patch.object(
|
|
293
|
+
recall, "_cache_lookup", return_value=cache_hit_context
|
|
294
|
+
), patch.object(recall, "_cache_store", return_value=None), patch(
|
|
295
|
+
"sys.stdin", new=io.StringIO(json.dumps(hook_input))
|
|
296
|
+
), patch("sys.stdout", new=stdout), patch("sys.stderr", new=stderr):
|
|
297
|
+
recall.main()
|
|
298
|
+
|
|
299
|
+
def test_temporal_prompt_logs_and_sends_an_anchor(self):
|
|
300
|
+
client = _E2EClient()
|
|
301
|
+
self._run("what did we work on last week", client)
|
|
302
|
+
row = self._log_row()
|
|
303
|
+
self.assertIn("query_timestamp", row)
|
|
304
|
+
self.assertIsNotNone(row["query_timestamp"], "temporal turn must log an anchor")
|
|
305
|
+
# And the same anchor reached the bank on the wire.
|
|
306
|
+
self.assertEqual(client.recall_kwargs, [row["query_timestamp"]])
|
|
307
|
+
# It is a parseable, tz-aware ISO string (engine 400s otherwise).
|
|
308
|
+
parsed = datetime.fromisoformat(row["query_timestamp"])
|
|
309
|
+
self.assertIsNotNone(parsed.tzinfo)
|
|
310
|
+
|
|
311
|
+
def test_non_temporal_prompt_logs_null_and_sends_nothing(self):
|
|
312
|
+
client = _E2EClient()
|
|
313
|
+
self._run("how do I restart the hindsight container", client)
|
|
314
|
+
row = self._log_row()
|
|
315
|
+
self.assertIn("query_timestamp", row)
|
|
316
|
+
self.assertIsNone(row["query_timestamp"])
|
|
317
|
+
# No anchor on the wire — byte-identical to pre-P2 behaviour.
|
|
318
|
+
self.assertEqual(client.recall_kwargs, [None])
|
|
319
|
+
|
|
320
|
+
def test_gate_off_suppresses_the_field_on_a_temporal_prompt(self):
|
|
321
|
+
client = _E2EClient()
|
|
322
|
+
self._run(
|
|
323
|
+
"what did we work on last week",
|
|
324
|
+
client,
|
|
325
|
+
config_extra={"recallQueryTimestamp": False},
|
|
326
|
+
)
|
|
327
|
+
row = self._log_row()
|
|
328
|
+
self.assertIsNone(row["query_timestamp"], "gate off must not send an anchor")
|
|
329
|
+
self.assertEqual(client.recall_kwargs, [None])
|
|
330
|
+
|
|
331
|
+
def test_narrow_client_signature_is_safe_on_a_non_temporal_turn(self):
|
|
332
|
+
# Nit #3 — the conditional-passing guarantee. A client whose recall()
|
|
333
|
+
# has the pre-P2 signature (no query_timestamp, no **kwargs) must NOT
|
|
334
|
+
# be handed the kwarg on a non-temporal turn. If it were (unconditional
|
|
335
|
+
# `=None`), binding would TypeError BEFORE the body runs, so `.calls`
|
|
336
|
+
# would stay 0 and the turn would degrade. calls==1 proves recall was
|
|
337
|
+
# entered cleanly with a byte-identical pre-P2 call.
|
|
338
|
+
client = _StrictSignatureClient()
|
|
339
|
+
self._run("how do I restart the hindsight container", client)
|
|
340
|
+
row = self._log_row()
|
|
341
|
+
self.assertEqual(client.calls, 1, "narrow-signature recall must run, not TypeError")
|
|
342
|
+
self.assertIsNone(row["query_timestamp"])
|
|
343
|
+
|
|
344
|
+
def test_cache_hit_row_carries_the_field(self):
|
|
345
|
+
# Nit #4 — the cache-hit log site (no bank runs) must still carry
|
|
346
|
+
# query_timestamp for a uniformly queryable schema and firing-rate
|
|
347
|
+
# measurement. A temporal prompt on a cache hit logs the anchor.
|
|
348
|
+
client = _E2EClient()
|
|
349
|
+
self._run(
|
|
350
|
+
"what did we work on last week",
|
|
351
|
+
client,
|
|
352
|
+
cache_hit_context="<hindsight_memories>cached</hindsight_memories>",
|
|
353
|
+
)
|
|
354
|
+
# No bank ran on the hit — the anchor was computed but never sent.
|
|
355
|
+
self.assertEqual(client.recall_kwargs, [])
|
|
356
|
+
row = self._log_row()
|
|
357
|
+
self.assertIn("query_timestamp", row)
|
|
358
|
+
self.assertIsNotNone(row["query_timestamp"], "cache-hit temporal turn must log the anchor")
|
|
359
|
+
|
|
360
|
+
def test_cache_hit_row_logs_null_on_a_non_temporal_turn(self):
|
|
361
|
+
# Nit #4 companion — cache-hit + no temporal phrase → null field, not
|
|
362
|
+
# a missing key (schema uniformity).
|
|
363
|
+
client = _E2EClient()
|
|
364
|
+
self._run(
|
|
365
|
+
"how do I restart the hindsight container",
|
|
366
|
+
client,
|
|
367
|
+
cache_hit_context="<hindsight_memories>cached</hindsight_memories>",
|
|
368
|
+
)
|
|
369
|
+
self.assertEqual(client.recall_kwargs, [])
|
|
370
|
+
row = self._log_row()
|
|
371
|
+
self.assertIn("query_timestamp", row)
|
|
372
|
+
self.assertIsNone(row["query_timestamp"])
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
if __name__ == "__main__":
|
|
376
|
+
unittest.main()
|