switchroom 0.21.15 → 0.21.17
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 +6 -0
- package/dist/auth-broker/index.js +6 -0
- package/dist/cli/notion-write-pretool.mjs +6 -0
- package/dist/cli/switchroom.js +2086 -871
- package/dist/host-control/main.js +7 -1
- package/dist/vault/approvals/kernel-server.js +6 -0
- package/dist/vault/broker/server.js +6 -0
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +11 -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 +10 -4
- package/vendor/hindsight-memory/hooks/hooks.json +19 -0
- package/vendor/hindsight-memory/scripts/directive_verify.py +43 -1
- package/vendor/hindsight-memory/scripts/lib/client.py +35 -0
- package/vendor/hindsight-memory/scripts/lib/config.py +47 -0
- package/vendor/hindsight-memory/scripts/lib/orientation.py +248 -0
- package/vendor/hindsight-memory/scripts/lib/recall_buffer.py +265 -0
- package/vendor/hindsight-memory/scripts/orientation.py +195 -0
- package/vendor/hindsight-memory/scripts/prefetch.py +166 -0
- package/vendor/hindsight-memory/scripts/recall.py +323 -5
- package/vendor/hindsight-memory/scripts/retain.py +132 -49
- package/vendor/hindsight-memory/scripts/setup_hooks.py +10 -1
- package/vendor/hindsight-memory/scripts/tests/fixtures/rules-block.golden.md +9 -0
- package/vendor/hindsight-memory/scripts/tests/test_orientation_hook.py +283 -0
- package/vendor/hindsight-memory/scripts/tests/test_orientation_logic.py +176 -0
- package/vendor/hindsight-memory/scripts/tests/test_prefetch_invalidation.py +329 -0
- package/vendor/hindsight-memory/scripts/tests/test_prefetch_pipeline.py +247 -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_directive_suppression.py +328 -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_retain_delta.py +304 -0
- package/vendor/hindsight-memory/scripts/tests/test_retain_stop_hook_prefetch_gate.py +109 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""SessionStart hook: Memory v2 M5 — orientation-at-boot (Surface B).
|
|
3
|
+
|
|
4
|
+
carve-M5.md §3. Injects the agent's cron-refreshed ``orientation`` mental
|
|
5
|
+
model into context at SessionStart AND re-seats it after every compaction —
|
|
6
|
+
"reflect once, serve from cache" (E-79), deterministically, zero tool call.
|
|
7
|
+
|
|
8
|
+
DARK BY DEFAULT (red-team kill switch). Entirely gated by
|
|
9
|
+
``memoryOrientationEnabled`` (default False — carve §4, same discipline as
|
|
10
|
+
M4's ``memoryPrefetchEnabled``). When off this hook is a hard no-op: it reads
|
|
11
|
+
config, sees the flag off, and exits BEFORE any bank resolve or network call.
|
|
12
|
+
The flag is per-agent — a stripped/absent value fails to OFF (fail-safe), so
|
|
13
|
+
an un-flipped agent boots exactly as pre-M5.
|
|
14
|
+
|
|
15
|
+
MATCHER-LESS (load-bearing, carve §1/§3). Registered with NO matcher, so it
|
|
16
|
+
fires on every SessionStart source — startup, resume, clear, fork AND compact.
|
|
17
|
+
The compact firing is the free deterministic post-compaction re-seat that
|
|
18
|
+
answers E-88 ("nothing re-seats orientation today"): the hook re-reads the
|
|
19
|
+
model from the engine each time, so a mid-session refresh is picked up at the
|
|
20
|
+
next compaction with no session restart. Do NOT add a matcher.
|
|
21
|
+
|
|
22
|
+
FAIL-SAFE INVARIANTS (all asserted in tests/test_orientation_hook.py):
|
|
23
|
+
- Boot is NEVER blocked on the orientation read. Every failure path
|
|
24
|
+
(server down, no model, read error, read timeout, degraded/undated
|
|
25
|
+
model) degrades to a VISIBLE one-line cold notice + a refresh request,
|
|
26
|
+
and exits 0. Never a silently memoryless boot, never a stale briefing
|
|
27
|
+
presented as fresh.
|
|
28
|
+
- A stale (1.5×–3× cadence) model is injected WITH a visible prefix; a
|
|
29
|
+
degraded (>3×) or undated model is NOT injected — the cold notice is
|
|
30
|
+
emitted instead. Thresholds are per-tier (carve §5), not a fixed 36h.
|
|
31
|
+
|
|
32
|
+
STDOUT CONTRACT. A SessionStart hook's stdout IS added to the model's context
|
|
33
|
+
(unlike PreCompact); we emit ``{"hookSpecificOutput":{"hookEventName":
|
|
34
|
+
"SessionStart","additionalContext": "…"}}``. Exit code is 0 ALWAYS — a
|
|
35
|
+
non-zero exit BLOCKS the turn in Claude Code, and orientation must degrade to
|
|
36
|
+
a notice, never a hard stop (same doctrine as rules-sentinel-hook.sh).
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
import json
|
|
40
|
+
import os
|
|
41
|
+
import sys
|
|
42
|
+
|
|
43
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
44
|
+
|
|
45
|
+
from lib import orientation as orient
|
|
46
|
+
from lib.bank import derive_bank_id
|
|
47
|
+
from lib.client import HindsightClient
|
|
48
|
+
from lib.config import debug_log, load_config
|
|
49
|
+
from lib.daemon import get_api_url
|
|
50
|
+
|
|
51
|
+
#: Generous read-timeout for the model GET (carve §3). overlord's own-bank read
|
|
52
|
+
#: is ~270ms and klanker's ~4ms (m0/m5-0 probes), but the latency asymmetry is
|
|
53
|
+
#: unexplained and possibly bank-size-correlated, so leave headroom — and on a
|
|
54
|
+
#: timeout degrade to the cold notice, never a hung boot.
|
|
55
|
+
ORIENTATION_READ_TIMEOUT_S = 3
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _emit(additional_context: str) -> None:
|
|
59
|
+
"""Print the SessionStart additionalContext envelope to stdout."""
|
|
60
|
+
print(
|
|
61
|
+
json.dumps(
|
|
62
|
+
{
|
|
63
|
+
"hookSpecificOutput": {
|
|
64
|
+
"hookEventName": "SessionStart",
|
|
65
|
+
"additionalContext": additional_context,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def enqueue_refresh(config: dict, bank_id: str, model_name: str) -> None:
|
|
73
|
+
"""Signal that ``bank_id``'s orientation model needs a background refresh.
|
|
74
|
+
|
|
75
|
+
The cold/degraded path requests a refresh so a boot that found no usable
|
|
76
|
+
briefing does not stay briefing-less indefinitely (carve §3-4). This is the
|
|
77
|
+
SEAM the M5-C out-of-session tiered scheduler consumes; the scheduler is
|
|
78
|
+
cadence-driven regardless, so this is a best-effort nudge, never a blocking
|
|
79
|
+
dependency. Kept as a thin, override-able function so the cold-path tests
|
|
80
|
+
can assert the OUTCOME ("a refresh was requested") without a live scheduler.
|
|
81
|
+
|
|
82
|
+
Best-effort and total: any failure is swallowed — a boot must never fail
|
|
83
|
+
because a refresh request could not be recorded.
|
|
84
|
+
"""
|
|
85
|
+
debug_log(
|
|
86
|
+
config,
|
|
87
|
+
f"orientation cold/degraded on bank '{bank_id}' model "
|
|
88
|
+
f"'{model_name}' — refresh requested",
|
|
89
|
+
)
|
|
90
|
+
try:
|
|
91
|
+
state_dir = os.environ.get("HINDSIGHT_STATE_DIR") or os.path.join(
|
|
92
|
+
os.path.expanduser("~"), ".hindsight"
|
|
93
|
+
)
|
|
94
|
+
os.makedirs(state_dir, exist_ok=True)
|
|
95
|
+
marker = os.path.join(state_dir, "orientation-refresh-pending.jsonl")
|
|
96
|
+
with open(marker, "a") as f:
|
|
97
|
+
f.write(json.dumps({"bank_id": bank_id, "model": model_name}) + "\n")
|
|
98
|
+
except Exception as e: # noqa: BLE001 — best-effort, never breaks boot
|
|
99
|
+
debug_log(config, f"orientation refresh-request write failed (ignored): {e}")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _match_model(models: dict, name: str) -> dict | None:
|
|
103
|
+
"""Find the mental model whose ``name`` matches the configured orientation name."""
|
|
104
|
+
items = models.get("items") if isinstance(models, dict) else None
|
|
105
|
+
if not isinstance(items, list):
|
|
106
|
+
return None
|
|
107
|
+
for item in items:
|
|
108
|
+
if isinstance(item, dict) and item.get("name") == name:
|
|
109
|
+
return item
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def run(hook_input: dict, config: dict) -> None:
|
|
114
|
+
"""Core hook logic. Emits at most one additionalContext block; never raises."""
|
|
115
|
+
if not config.get("memoryOrientationEnabled", False):
|
|
116
|
+
debug_log(config, "memoryOrientationEnabled off — orientation no-op")
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
model_name = config.get("memoryOrientationModel", "orientation")
|
|
120
|
+
cadence_hours = int(config.get("memoryOrientationCadenceHours", 48) or 48)
|
|
121
|
+
bank_id = derive_bank_id(hook_input, config)
|
|
122
|
+
|
|
123
|
+
def _dbg(*a):
|
|
124
|
+
debug_log(config, *a)
|
|
125
|
+
|
|
126
|
+
# Resolve the engine REST URL WITHOUT starting the daemon (too slow for
|
|
127
|
+
# SessionStart). If the server is unreachable, degrade to the cold notice —
|
|
128
|
+
# we cannot read the model, and boot must not block.
|
|
129
|
+
try:
|
|
130
|
+
api_url = get_api_url(config, debug_fn=_dbg, allow_daemon_start=False)
|
|
131
|
+
client = HindsightClient(api_url, config.get("hindsightApiToken"))
|
|
132
|
+
except (RuntimeError, ValueError) as e:
|
|
133
|
+
debug_log(config, f"orientation: Hindsight unreachable ({e}) — cold notice")
|
|
134
|
+
enqueue_refresh(config, bank_id, model_name)
|
|
135
|
+
_emit(orient.cold_notice())
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
# Resolve the orientation model by NAME → id, then read its content +
|
|
139
|
+
# freshness watermark. Any read error/timeout is a cold notice, not a hang.
|
|
140
|
+
try:
|
|
141
|
+
models = client.list_mental_models(bank_id, timeout=ORIENTATION_READ_TIMEOUT_S)
|
|
142
|
+
match = _match_model(models, model_name)
|
|
143
|
+
except Exception as e: # noqa: BLE001 — degrade to cold, never block boot
|
|
144
|
+
debug_log(config, f"orientation: list models failed ({e}) — cold notice")
|
|
145
|
+
enqueue_refresh(config, bank_id, model_name)
|
|
146
|
+
_emit(orient.cold_notice())
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
if not match or not match.get("id"):
|
|
150
|
+
debug_log(config, f"orientation: no model named '{model_name}' — cold notice")
|
|
151
|
+
enqueue_refresh(config, bank_id, model_name)
|
|
152
|
+
_emit(orient.cold_notice())
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
full = client.get_mental_model(
|
|
157
|
+
bank_id, match["id"], detail="full", timeout=ORIENTATION_READ_TIMEOUT_S
|
|
158
|
+
)
|
|
159
|
+
except Exception as e: # noqa: BLE001 — includes urllib timeout
|
|
160
|
+
debug_log(config, f"orientation: model read failed ({e}) — cold notice")
|
|
161
|
+
enqueue_refresh(config, bank_id, model_name)
|
|
162
|
+
_emit(orient.cold_notice())
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
content = (full or {}).get("content") or ""
|
|
166
|
+
last_refreshed_at = (full or {}).get("last_refreshed_at")
|
|
167
|
+
state, hours_ago = orient.classify_staleness(last_refreshed_at, cadence_hours)
|
|
168
|
+
|
|
169
|
+
if not content.strip() or state in ("degraded", "unknown"):
|
|
170
|
+
# Empty payload, or refreshed > 3× cadence ago / undated: never present
|
|
171
|
+
# stale-as-fresh. Cold notice + refresh request (carve §5).
|
|
172
|
+
debug_log(config, f"orientation: state={state}, empty={not content.strip()} — cold notice")
|
|
173
|
+
enqueue_refresh(config, bank_id, model_name)
|
|
174
|
+
_emit(orient.cold_notice())
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
debug_log(config, f"orientation: injecting (state={state}, {hours_ago}h ago)")
|
|
178
|
+
_emit(orient.render_orientation(content, state, hours_ago))
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def main():
|
|
182
|
+
config = load_config()
|
|
183
|
+
try:
|
|
184
|
+
hook_input = json.load(sys.stdin)
|
|
185
|
+
except (json.JSONDecodeError, EOFError):
|
|
186
|
+
hook_input = {}
|
|
187
|
+
run(hook_input, config)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
if __name__ == "__main__":
|
|
191
|
+
try:
|
|
192
|
+
main()
|
|
193
|
+
except Exception as e: # noqa: BLE001 — a hook that exits non-zero blocks the turn
|
|
194
|
+
print(f"[Hindsight] Orientation SessionStart error: {e}", file=sys.stderr)
|
|
195
|
+
sys.exit(0)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""M4 P-PRE — async Stop-hook prefetch producer.
|
|
3
|
+
|
|
4
|
+
carve-M4.md: moves recall injection off the synchronous UserPromptSubmit
|
|
5
|
+
path by speculatively retaining + recalling at the END of turn N (this
|
|
6
|
+
script, registered as an async Stop hook), so turn N+1's UserPromptSubmit
|
|
7
|
+
(`recall.py`) can join an already-warm buffer instead of paying the full
|
|
8
|
+
recall latency synchronously.
|
|
9
|
+
|
|
10
|
+
Entirely gated by `memoryPrefetchEnabled` (default OFF/falsy — Fix C, the
|
|
11
|
+
red-team-mandated per-agent kill switch). When off this script is a no-op:
|
|
12
|
+
it reads config, sees the flag off, and exits silently before touching
|
|
13
|
+
`lib.retain`, `lib.recall_buffer`, or the network.
|
|
14
|
+
|
|
15
|
+
Call order (test a-integration in `tests/test_prefetch_pipeline.py`):
|
|
16
|
+
1. delta retain (`retain.run_retain(hook_input, force=False, delta=True)`)
|
|
17
|
+
— persists this turn's NEW slice using the Fix-A content-derived
|
|
18
|
+
document_id, never truncating the session document.
|
|
19
|
+
2. speculative recall (best-effort query derived from the transcript's
|
|
20
|
+
last human turn — the strongest available proxy for what turn N+1
|
|
21
|
+
will ask about).
|
|
22
|
+
3. `recall_buffer.write_buffer` then `recall_buffer.write_sentinel`
|
|
23
|
+
(STRICTLY in that order — the sentinel-ordering contract `lib/
|
|
24
|
+
recall_buffer.py` and `tests/test_recall_buffer.py` prove).
|
|
25
|
+
|
|
26
|
+
Silent on stdout always (an async Stop hook's stdout is not surfaced to the
|
|
27
|
+
transcript); errors go to stderr and NEVER raise past `main()` — a broken
|
|
28
|
+
prefetch must degrade to "recall.py's synchronous path runs as before",
|
|
29
|
+
never break the turn or leave a torn buffer (a crash between steps 2 and 3a
|
|
30
|
+
just means no sentinel is written this turn, which `read_if_fresh` already
|
|
31
|
+
treats as "nothing fresh" — fail-closed by construction).
|
|
32
|
+
|
|
33
|
+
Skips the same junk turns `recall.py` skips (`<task-notification>` prefix)
|
|
34
|
+
— a synthetic follow-up turn is not a real conversation shift worth
|
|
35
|
+
prefetching for.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import json
|
|
41
|
+
import sys
|
|
42
|
+
|
|
43
|
+
from lib.bank import derive_bank_id
|
|
44
|
+
from lib.client import HindsightClient
|
|
45
|
+
from lib.config import debug_log, load_config
|
|
46
|
+
from lib.content import _extract_text_content, strip_channel_envelope
|
|
47
|
+
from lib.daemon import get_api_url
|
|
48
|
+
from lib import recall_buffer
|
|
49
|
+
import retain as retain_module
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _last_human_prompt(transcript_path: str) -> str:
|
|
53
|
+
"""Best-effort: the most recent human-authored user turn's text, used
|
|
54
|
+
as the speculative query for next turn's prefetch. Never raises —
|
|
55
|
+
returns "" on any read/parse failure (degrades to no query, which the
|
|
56
|
+
caller treats as "nothing to prefetch")."""
|
|
57
|
+
try:
|
|
58
|
+
messages = retain_module.read_transcript(transcript_path)
|
|
59
|
+
except Exception:
|
|
60
|
+
return ""
|
|
61
|
+
for msg in reversed(messages):
|
|
62
|
+
if msg.get("role") != "user":
|
|
63
|
+
continue
|
|
64
|
+
text = _extract_text_content(msg.get("content"), role="user")
|
|
65
|
+
text = strip_channel_envelope(text) if text else text
|
|
66
|
+
if text and text.strip():
|
|
67
|
+
return text.strip()
|
|
68
|
+
return ""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def run_prefetch(hook_input: dict, config: dict) -> bool:
|
|
72
|
+
"""Execute one prefetch cycle. Returns True iff a buffer+sentinel pair
|
|
73
|
+
was written. Never raises — every internal step is wrapped so a bug in
|
|
74
|
+
ANY sub-step degrades to "no buffer written this turn", never a crash
|
|
75
|
+
that could take the Stop hook (and thus the turn) down with it."""
|
|
76
|
+
session_id = hook_input.get("session_id") or "unknown"
|
|
77
|
+
|
|
78
|
+
prompt = (hook_input.get("prompt") or hook_input.get("user_prompt") or "").strip()
|
|
79
|
+
if prompt.startswith("<task-notification"):
|
|
80
|
+
debug_log(config, "Prefetch: task-notification turn, skipping")
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
# Step 0 — MUTATION INVALIDATION (red-team-M3 R2, BLOCKER). This turn is
|
|
84
|
+
# about to retain (step 1) — a memory mutation. Drop any buffer left from a
|
|
85
|
+
# prior turn BEFORE recalling, so that if this turn's fresh recall fails or
|
|
86
|
+
# returns empty (steps 2-3 below bail without overwriting), the consumer
|
|
87
|
+
# cannot resurrect the pre-mutation snapshot: with no sentinel it falls to
|
|
88
|
+
# the explicitly stale-marked LAST_RECALL_STATE / degraded notice instead of
|
|
89
|
+
# serving a stale buffer as fresh. A successful recall repopulates the buffer
|
|
90
|
+
# with the post-mutation snapshot at step 3. Never raises.
|
|
91
|
+
recall_buffer.invalidate(session_id)
|
|
92
|
+
|
|
93
|
+
# Step 1 — delta retain (Fix A: content-derived document_id, never the
|
|
94
|
+
# bare {session_id} document; never truncates).
|
|
95
|
+
try:
|
|
96
|
+
retain_module.run_retain(hook_input, force=False, delta=True)
|
|
97
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
98
|
+
debug_log(config, f"Prefetch: delta retain failed, continuing without it: {exc}")
|
|
99
|
+
|
|
100
|
+
# Step 2 — speculative recall.
|
|
101
|
+
transcript_path = hook_input.get("transcript_path") or ""
|
|
102
|
+
query = _last_human_prompt(transcript_path)
|
|
103
|
+
if not query:
|
|
104
|
+
debug_log(config, "Prefetch: no usable query, nothing to prefetch")
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
bank_id = derive_bank_id(hook_input, config)
|
|
109
|
+
api_url = get_api_url(config)
|
|
110
|
+
client = HindsightClient(api_url)
|
|
111
|
+
response = client.recall(
|
|
112
|
+
bank_id,
|
|
113
|
+
query,
|
|
114
|
+
types=None,
|
|
115
|
+
timeout=config.get("memoryPrefetchTimeoutSeconds", 5),
|
|
116
|
+
)
|
|
117
|
+
results = (response or {}).get("results") or []
|
|
118
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
119
|
+
debug_log(config, f"Prefetch: recall fetch failed: {exc}")
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
if not results:
|
|
123
|
+
debug_log(config, "Prefetch: no candidates, nothing to buffer")
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
from lib.content import format_memories
|
|
127
|
+
|
|
128
|
+
memories_block = format_memories(results)
|
|
129
|
+
if not memories_block:
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
# Step 3 — write payload THEN sentinel, strictly in that order.
|
|
133
|
+
try:
|
|
134
|
+
recall_buffer.write_buffer(session_id, memories_block, {"result_count": len(results)})
|
|
135
|
+
recall_buffer.write_sentinel(session_id)
|
|
136
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
137
|
+
debug_log(config, f"Prefetch: buffer write failed: {exc}")
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
return True
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def main():
|
|
144
|
+
config = load_config()
|
|
145
|
+
if not config.get("memoryPrefetchEnabled", False):
|
|
146
|
+
# Fix C, producer side: the whole mechanism is dark by default.
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
hook_input = json.load(sys.stdin)
|
|
151
|
+
except (json.JSONDecodeError, EOFError):
|
|
152
|
+
print("[Hindsight] Prefetch: failed to read hook input", file=sys.stderr)
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
run_prefetch(hook_input, config)
|
|
157
|
+
except Exception as exc: # pragma: no cover - defensive, must never break the turn
|
|
158
|
+
print(f"[Hindsight] Prefetch: unexpected error: {exc}", file=sys.stderr)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
if __name__ == "__main__":
|
|
162
|
+
try:
|
|
163
|
+
main()
|
|
164
|
+
except Exception as e: # pragma: no cover - defensive
|
|
165
|
+
print(f"[Hindsight] Unexpected error in prefetch: {e}", file=sys.stderr)
|
|
166
|
+
sys.exit(0)
|