switchroom 0.20.8 → 0.20.10
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/handoff-briefing.sh +57 -5
- package/bin/working-state-reload-hook.sh +262 -0
- package/dist/agent-scheduler/index.js +16 -13
- package/dist/auth-broker/index.js +70 -30
- package/dist/cli/autoaccept-poll.js +5 -3
- package/dist/cli/drive-write-pretool.mjs +5 -3
- package/dist/cli/ms-365-write-pretool.mjs +5 -3
- package/dist/cli/notion-write-pretool.mjs +6 -6
- package/dist/cli/switchroom.js +42 -13
- package/dist/host-control/main.js +7 -7
- package/dist/vault/approvals/kernel-server.js +6 -6
- package/dist/vault/broker/server.js +6 -6
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +49 -0
- package/profiles/default/CLAUDE.md.hbs +12 -13
- package/telegram-plugin/ask-user.ts +6 -7
- package/telegram-plugin/dist/gateway/gateway.js +192 -66
- package/telegram-plugin/gateway/auth-broker-client.ts +1 -1
- package/telegram-plugin/gateway/auth-command.ts +4 -2
- package/telegram-plugin/gateway/checklist-fallback.ts +8 -1
- package/telegram-plugin/gateway/gateway.ts +8 -4
- package/telegram-plugin/gateway/outbound-send-path.ts +9 -1
- package/telegram-plugin/gateway/subagent-handback-inbound-builder.ts +21 -1
- package/telegram-plugin/gateway/subagent-handback-marker.ts +94 -0
- package/telegram-plugin/gateway/throttle-tier-wiring.ts +93 -18
- package/telegram-plugin/render/emphasis-guard.ts +92 -12
- package/telegram-plugin/render/line-start-guard.ts +27 -2
- package/telegram-plugin/sticker-aliases.ts +12 -14
- package/telegram-plugin/tests/ask-user.test.ts +15 -0
- package/telegram-plugin/tests/checklist-fallback.test.ts +21 -0
- package/telegram-plugin/tests/handback-tasknotif-dedup.test.ts +248 -0
- package/telegram-plugin/tests/render/emphasis-guard.test.ts +105 -6
- package/telegram-plugin/tests/render/heading-guard-blockquote-glued-hash.test.ts +123 -36
- package/telegram-plugin/tests/reply-quote-wire.test.ts +47 -0
- package/telegram-plugin/tests/sticker-aliases.test.ts +43 -0
- package/telegram-plugin/tests/throttle-tier-probe-only.test.ts +216 -0
- package/telegram-plugin/tests/throttle-tier-route-429-wiring.test.ts +92 -0
- package/telegram-plugin/tests/throttle-tier-route-429.test.ts +71 -0
- package/telegram-plugin/throttle-tier.ts +59 -0
- package/vendor/hindsight-memory/CHANGELOG.md +31 -0
- package/vendor/hindsight-memory/hooks/hooks.json +2 -1
- package/vendor/hindsight-memory/scripts/session_start.py +35 -8
- package/vendor/hindsight-memory/scripts/tests/test_session_start_durability.py +107 -0
package/bin/handoff-briefing.sh
CHANGED
|
@@ -34,9 +34,32 @@
|
|
|
34
34
|
# - AGENT_DIR — output destination (if HANDOFF_BRIEFING_STDOUT!=1)
|
|
35
35
|
#
|
|
36
36
|
# Usage:
|
|
37
|
-
# handoff-briefing.sh [--stdout]
|
|
37
|
+
# handoff-briefing.sh [--stdout] [--lean]
|
|
38
38
|
#
|
|
39
39
|
# The --stdout flag overrides HANDOFF_BRIEFING_STDOUT=1.
|
|
40
|
+
#
|
|
41
|
+
# LEAN MODE (--lean, alias --mode=compaction)
|
|
42
|
+
# -------------------------------------------
|
|
43
|
+
# The compaction re-seat path (bin/working-state-reload-hook.sh, wired as the
|
|
44
|
+
# SessionStart(compact) hook) invokes this script with --lean so BOTH legacy-
|
|
45
|
+
# and gateway-briefing agents share ONE assembler at the compaction boundary —
|
|
46
|
+
# no copy of the sqlite/recall logic lives in the hook. Lean mode differs from
|
|
47
|
+
# the boot briefing in three deliberate ways:
|
|
48
|
+
#
|
|
49
|
+
# 1. It emits ONLY the recent-Telegram-tail + Hindsight-recall sections.
|
|
50
|
+
# Daily-memory (Source 3) and the "You just restarted at …" header are
|
|
51
|
+
# SKIPPED. Rationale is token duplication, not latency: Claude Code's
|
|
52
|
+
# native compaction summary already preserves the recent turns, so
|
|
53
|
+
# re-injecting the full boot briefing on every compaction of a long
|
|
54
|
+
# session partially triples coverage.
|
|
55
|
+
# 2. It IGNORES the SWITCHROOM_PENDING_* env (boot-time pending-turn scope).
|
|
56
|
+
# Those name the surface that was mid-turn at the PREVIOUS boot; at a
|
|
57
|
+
# compaction hours into a live session they are stale and would brief the
|
|
58
|
+
# WRONG chat surface. Lean mode zeroes them so the python scoper derives
|
|
59
|
+
# the single most-recently-active (chat_id, thread_id) straight from the
|
|
60
|
+
# DB (scope_source=db-latest).
|
|
61
|
+
# 3. It forces stdout and never touches AGENT_DIR (no output file), so it is
|
|
62
|
+
# safe to call from the hook regardless of whether AGENT_DIR is exported.
|
|
40
63
|
|
|
41
64
|
set -u
|
|
42
65
|
|
|
@@ -71,12 +94,32 @@ HINDSIGHT_TIMEOUT="${HANDOFF_BRIEFING_HINDSIGHT_TIMEOUT:-3}"
|
|
|
71
94
|
TARGET_CHAT_ID="${SWITCHROOM_PENDING_CHAT_ID:-}"
|
|
72
95
|
TARGET_THREAD_ID="${SWITCHROOM_PENDING_THREAD_ID:-}"
|
|
73
96
|
|
|
74
|
-
# Determine output mode
|
|
97
|
+
# Determine output + lean mode. Parse every arg (order-independent) so
|
|
98
|
+
# `--lean`, `--stdout`, or both, work regardless of position.
|
|
75
99
|
STDOUT_MODE=0
|
|
76
|
-
|
|
100
|
+
LEAN_MODE=0
|
|
101
|
+
for _arg in "$@"; do
|
|
102
|
+
case "$_arg" in
|
|
103
|
+
--stdout) STDOUT_MODE=1 ;;
|
|
104
|
+
--lean|--mode=compaction) LEAN_MODE=1 ;;
|
|
105
|
+
*) : ;;
|
|
106
|
+
esac
|
|
107
|
+
done
|
|
108
|
+
if [ "${HANDOFF_BRIEFING_STDOUT:-}" = "1" ]; then
|
|
77
109
|
STDOUT_MODE=1
|
|
78
110
|
fi
|
|
79
111
|
|
|
112
|
+
# Lean (compaction) mode: force stdout, and ZERO the pending-turn env scope so
|
|
113
|
+
# the python scoper falls through to db-latest (see LEAN MODE note in header).
|
|
114
|
+
# This is the load-bearing correctness fix for a mid-session compaction: the
|
|
115
|
+
# SWITCHROOM_PENDING_* surface is the previous boot's, not the currently-active
|
|
116
|
+
# chat. Clearing them here (not in the hook) keeps the single scoping code path.
|
|
117
|
+
if [ "$LEAN_MODE" = "1" ]; then
|
|
118
|
+
STDOUT_MODE=1
|
|
119
|
+
TARGET_CHAT_ID=""
|
|
120
|
+
TARGET_THREAD_ID=""
|
|
121
|
+
fi
|
|
122
|
+
|
|
80
123
|
# ── Source 1: Recent Telegram messages ─────────────────────────────────────────
|
|
81
124
|
TELEGRAM_SECTION=""
|
|
82
125
|
if [ -n "$TELEGRAM_STATE" ] && [ -d "$TELEGRAM_STATE" ]; then
|
|
@@ -362,7 +405,9 @@ fi
|
|
|
362
405
|
# dropping today's memory.
|
|
363
406
|
DAILY_SECTION=""
|
|
364
407
|
TODAY=$(TZ="$_TZ_VAL" date +%Y-%m-%d 2>/dev/null || date +%Y-%m-%d 2>/dev/null || true)
|
|
365
|
-
|
|
408
|
+
# Lean/compaction mode SKIPS daily memory (token duplication — the native
|
|
409
|
+
# summary already carries recent context; see LEAN MODE note in header).
|
|
410
|
+
if [ "$LEAN_MODE" != "1" ] && [ -n "$TODAY" ] && [ -n "$WORKSPACE_DIR" ]; then
|
|
366
411
|
DAILY_FILE="$WORKSPACE_DIR/memory/${TODAY}.md"
|
|
367
412
|
if [ -f "$DAILY_FILE" ] && [ -s "$DAILY_FILE" ]; then
|
|
368
413
|
DAILY_CONTENT=$(cat "$DAILY_FILE")
|
|
@@ -391,7 +436,14 @@ if [ -n "$OUTPUT_FILE" ]; then
|
|
|
391
436
|
else
|
|
392
437
|
# stdout / no-AGENT_DIR mode — buffered; print the whole briefing once.
|
|
393
438
|
if [ -n "$STDOUT_BUFFER" ]; then
|
|
394
|
-
|
|
439
|
+
if [ "$LEAN_MODE" = "1" ]; then
|
|
440
|
+
# Lean/compaction: no "You just restarted at …" boot header — this is a
|
|
441
|
+
# mid-conversation compaction, not a restart, and the hook emits its own
|
|
442
|
+
# <compact-recovery> framing. Print only the assembled sections.
|
|
443
|
+
printf '%s\n' "$STDOUT_BUFFER"
|
|
444
|
+
else
|
|
445
|
+
printf '%s\n\n---\n\n%s\n' "$BRIEFING_HEADER" "$STDOUT_BUFFER"
|
|
446
|
+
fi
|
|
395
447
|
fi
|
|
396
448
|
fi
|
|
397
449
|
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# working-state-reload-hook.sh — deliver post-compaction continuation into
|
|
3
|
+
# context immediately after context compaction.
|
|
4
|
+
#
|
|
5
|
+
# WHY THIS EXISTS
|
|
6
|
+
# ---------------
|
|
7
|
+
# Claude Code auto-compacts late in the context window. The native
|
|
8
|
+
# auto-summarizer produces a structured summary of intent/changes/pending
|
|
9
|
+
# work, but it is lossy: fast-moving detail an agent is actively juggling
|
|
10
|
+
# mid-task (a checklist, the current plan, in-flight IDs, the exact "where
|
|
11
|
+
# was I" scratch, recent phrasing) is exactly what a summary flattens or
|
|
12
|
+
# drops. Worse, the model resuming from a summary can read it as a FRESH
|
|
13
|
+
# start and re-greet the user, breaking a conversation the user experiences
|
|
14
|
+
# as unbroken.
|
|
15
|
+
#
|
|
16
|
+
# This hook closes that gap deterministically, in three layers:
|
|
17
|
+
# 1. It ALWAYS emits a short, static recovery/orientation block — for
|
|
18
|
+
# EVERY agent, whether or not it maintains a working-state file. This
|
|
19
|
+
# is the load-bearing default: it tells the model its context was just
|
|
20
|
+
# compacted mid-conversation, that the native summary is lossy, and
|
|
21
|
+
# which concrete recovery tools exist in this environment.
|
|
22
|
+
# 2. If the agent maintains a working-state file AND it is non-empty, the
|
|
23
|
+
# hook additionally appends that file verbatim (with its last-modified
|
|
24
|
+
# time, so a stale/forgotten file is visibly stale rather than silently
|
|
25
|
+
# steering).
|
|
26
|
+
# 3. It emits a LEAN briefing (P1) — a scoped recent Telegram tail + a
|
|
27
|
+
# Hindsight recall — so the compacted session gets fresh-boot PARITY:
|
|
28
|
+
# it picks up the actual conversation, not just the fact it was
|
|
29
|
+
# compacted. This is delegated to handoff-briefing.sh --lean (the SINGLE
|
|
30
|
+
# briefing assembler; no copy of the sqlite/recall logic here), so BOTH
|
|
31
|
+
# legacy- and gateway-briefing agents share one compaction re-seat path.
|
|
32
|
+
# Lean by design: it skips daily-memory/workspace re-render (token
|
|
33
|
+
# duplication — the native summary already keeps recent turns) and
|
|
34
|
+
# ignores SWITCHROOM_PENDING_* to brief the db-latest chat surface rather
|
|
35
|
+
# than a stale pending-turn one. Graceful: if history.db or Hindsight is
|
|
36
|
+
# unavailable it emits what it can (or nothing) and never fails the hook.
|
|
37
|
+
#
|
|
38
|
+
# It is wired as a SessionStart hook with matcher "compact" (see
|
|
39
|
+
# src/agents/scaffold.ts buildSettingsHooksBlock). Per Claude Code's hook
|
|
40
|
+
# contract:
|
|
41
|
+
# - SessionStart fires with source="compact" on auto OR manual compaction,
|
|
42
|
+
# mid-turn, right after the compaction boundary.
|
|
43
|
+
# - Text a SessionStart hook prints to stdout IS added to the model's
|
|
44
|
+
# context (unlike PreCompact stdout, which is NOT injected).
|
|
45
|
+
# So printing here re-seats orientation (and any working state) into context
|
|
46
|
+
# the instant the summary replaces the transcript — no marker file, no
|
|
47
|
+
# gateway round-trip, no waiting for the next user message.
|
|
48
|
+
#
|
|
49
|
+
# The matcher "compact" is load-bearing: it scopes this hook to compaction
|
|
50
|
+
# ONLY. A bare (matcher-less) SessionStart also fires on "startup", "resume",
|
|
51
|
+
# "clear", and "fork", which would inject the recovery block on every boot —
|
|
52
|
+
# noise, and prompt-cache churn. We rely on the matcher AND, belt-and-braces,
|
|
53
|
+
# re-check the `source` field from stdin below so a future Claude Code matcher
|
|
54
|
+
# regression can never turn this into an every-boot inject.
|
|
55
|
+
#
|
|
56
|
+
# THE WORKING-STATE FILE CONVENTION
|
|
57
|
+
# ---------------------------------
|
|
58
|
+
# $TELEGRAM_STATE_DIR/.working-state.md
|
|
59
|
+
# i.e. <agentDir>/telegram/.working-state.md (TELEGRAM_STATE_DIR is exported
|
|
60
|
+
# by start.sh as "<agentDir>/telegram"). An agent maintains this file itself
|
|
61
|
+
# as its durable scratch of "what I'm mid-way through". If the file is absent
|
|
62
|
+
# or empty — the common case for agents that don't use it — the hook simply
|
|
63
|
+
# skips the append; the static recovery block is still emitted. No file is
|
|
64
|
+
# ever created here.
|
|
65
|
+
#
|
|
66
|
+
# PERFORMANCE
|
|
67
|
+
# -----------
|
|
68
|
+
# The static recovery block and working-state append are local-only (a heredoc
|
|
69
|
+
# plus at most one `stat`/`cat` of a small file) — sub-second. The lean
|
|
70
|
+
# briefing (layer 3) adds one local SQLite read and ONE network hop to
|
|
71
|
+
# Hindsight, which handoff-briefing.sh caps (HANDOFF_BRIEFING_HINDSIGHT_TIMEOUT,
|
|
72
|
+
# default 3s). Worst-case runtime is therefore a few seconds, dominated by that
|
|
73
|
+
# cap; the SessionStart(compact) hook's Claude Code timeout is set accordingly
|
|
74
|
+
# in src/agents/scaffold.ts. Latency here is a non-issue by design: compaction
|
|
75
|
+
# itself takes far longer, and the prompt cache is already invalidated by the
|
|
76
|
+
# summary replacing the transcript. (Contrast the hindsight session_start.py
|
|
77
|
+
# SessionStart hook, which times out at its 5s budget on every firing — a
|
|
78
|
+
# separate, independent context-loss cause tracked against the hindsight-memory
|
|
79
|
+
# plugin, NOT fixed here.)
|
|
80
|
+
#
|
|
81
|
+
# Failure modes are all silent: a hook that errors would surface on the issues
|
|
82
|
+
# card via run-hook.sh, but a missing/absent working-state file, an unreadable
|
|
83
|
+
# mtime, a missing history.db, or an unreachable Hindsight is never an error —
|
|
84
|
+
# the recovery block still emits and the hook exits 0.
|
|
85
|
+
|
|
86
|
+
set -u
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
# Defensive source guard. The matcher "compact" in settings.json already
|
|
90
|
+
# scopes Claude Code to fire this hook only on compaction, but we re-verify
|
|
91
|
+
# the source from the hook's stdin JSON so a matcher regression (or a manual
|
|
92
|
+
# mis-wire) can never cause this to inject on a normal startup/resume/clear/
|
|
93
|
+
# fork boot. If stdin carries a `source` and it is not "compact", exit
|
|
94
|
+
# silently. If there is no stdin (e.g. a unit test invoking the script
|
|
95
|
+
# directly), fall through and trust the matcher.
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
if ! [ -t 0 ]; then
|
|
98
|
+
STDIN_JSON=$(cat 2>/dev/null || true)
|
|
99
|
+
if [ -n "${STDIN_JSON:-}" ]; then
|
|
100
|
+
SOURCE=""
|
|
101
|
+
if command -v jq >/dev/null 2>&1; then
|
|
102
|
+
SOURCE=$(printf '%s' "$STDIN_JSON" | jq -r '.source // empty' 2>/dev/null || true)
|
|
103
|
+
else
|
|
104
|
+
SOURCE=$(printf '%s' "$STDIN_JSON" \
|
|
105
|
+
| grep -o '"source"[[:space:]]*:[[:space:]]*"[^"]*"' \
|
|
106
|
+
| head -1 \
|
|
107
|
+
| sed 's/.*"source"[[:space:]]*:[[:space:]]*"//;s/"$//' 2>/dev/null || true)
|
|
108
|
+
fi
|
|
109
|
+
if [ -n "$SOURCE" ] && [ "$SOURCE" != "compact" ]; then
|
|
110
|
+
exit 0
|
|
111
|
+
fi
|
|
112
|
+
fi
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
# Static recovery/orientation block. ALWAYS emitted on a compaction start,
|
|
117
|
+
# for EVERY agent — this is the load-bearing default. Plain stdout from a
|
|
118
|
+
# SessionStart hook IS added to the model's context by Claude Code, so this
|
|
119
|
+
# re-orients the model the instant the native summary replaces the transcript.
|
|
120
|
+
# Deterministic heredoc string: no network, no CLI fork.
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
cat <<'EOF'
|
|
123
|
+
<compact-recovery source="switchroom working-state-reload hook">
|
|
124
|
+
Your context was just COMPACTED mid-conversation. This is NOT a fresh start:
|
|
125
|
+
you are CONTINUING a conversation the user experiences as unbroken. The native
|
|
126
|
+
summary above is lossy — it flattens or drops fast-moving detail (in-flight
|
|
127
|
+
IDs, the exact "where was I", recent phrasing). Do not greet the user or act
|
|
128
|
+
as if starting over; pick up where the conversation left off.
|
|
129
|
+
|
|
130
|
+
Re-orient using the recovery tools in THIS environment before continuing:
|
|
131
|
+
- Telegram chat history: the get_recent_messages MCP tool
|
|
132
|
+
(mcp__switchroom-telegram__get_recent_messages) to re-read what was just
|
|
133
|
+
being discussed.
|
|
134
|
+
- Hindsight memory: recall / reflect (mcp__hindsight__recall,
|
|
135
|
+
mcp__hindsight__reflect) for facts and decisions from earlier sessions.
|
|
136
|
+
- Workspace files for durable task state.
|
|
137
|
+
</compact-recovery>
|
|
138
|
+
EOF
|
|
139
|
+
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
# Resolve the working-state file. Primary: $TELEGRAM_STATE_DIR (exported by
|
|
142
|
+
# start.sh for telegram-plugin agents). Fallback: derive the conventional
|
|
143
|
+
# telegram state dir from the agent name, so the hook still works if invoked
|
|
144
|
+
# in a context where TELEGRAM_STATE_DIR is not exported. If neither resolves,
|
|
145
|
+
# skip the working-state append — the recovery block above already emitted,
|
|
146
|
+
# and the lean briefing below still runs.
|
|
147
|
+
#
|
|
148
|
+
# NOTE: this is a GUARDED block (not an early `exit`), because the lean
|
|
149
|
+
# post-compaction briefing further down must run for EVERY compaction,
|
|
150
|
+
# including the common case of an agent that keeps no working-state file.
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
STATE_DIR="${TELEGRAM_STATE_DIR:-}"
|
|
153
|
+
if [ -z "$STATE_DIR" ]; then
|
|
154
|
+
AGENT_NAME="${SWITCHROOM_AGENT_NAME:-}"
|
|
155
|
+
if [ -n "$AGENT_NAME" ] && [ -n "${HOME:-}" ]; then
|
|
156
|
+
STATE_DIR="$HOME/.switchroom/agents/$AGENT_NAME/telegram"
|
|
157
|
+
fi
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
WORKING_STATE_FILE=""
|
|
161
|
+
if [ -n "$STATE_DIR" ]; then
|
|
162
|
+
WORKING_STATE_FILE="$STATE_DIR/.working-state.md"
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
# Append the working state only when the file resolves AND is non-empty.
|
|
166
|
+
if [ -n "$WORKING_STATE_FILE" ] && [ -s "$WORKING_STATE_FILE" ]; then
|
|
167
|
+
# -------------------------------------------------------------------------
|
|
168
|
+
# Resolve the working-state file's last-modified time so a stale, forgotten
|
|
169
|
+
# file is VISIBLY stale to the model rather than silently steering it. Try a
|
|
170
|
+
# portable sequence: GNU/busybox `stat -c %y`, then BSD/macOS `stat -f %Sm`,
|
|
171
|
+
# then GNU `date -r <file>`. If none work, omit the mtime — never fail the
|
|
172
|
+
# hook over it.
|
|
173
|
+
# -------------------------------------------------------------------------
|
|
174
|
+
MTIME=""
|
|
175
|
+
if MTIME=$(stat -c %y "$WORKING_STATE_FILE" 2>/dev/null) && [ -n "$MTIME" ]; then
|
|
176
|
+
:
|
|
177
|
+
elif MTIME=$(stat -f '%Sm' "$WORKING_STATE_FILE" 2>/dev/null) && [ -n "$MTIME" ]; then
|
|
178
|
+
:
|
|
179
|
+
elif MTIME=$(date -r "$WORKING_STATE_FILE" 2>/dev/null) && [ -n "$MTIME" ]; then
|
|
180
|
+
:
|
|
181
|
+
else
|
|
182
|
+
MTIME=""
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
# -------------------------------------------------------------------------
|
|
186
|
+
# Append the working state verbatim, wrapped in its own delimiter block. The
|
|
187
|
+
# header line carries the mtime (when resolvable) so a stale file reads as
|
|
188
|
+
# stale.
|
|
189
|
+
# -------------------------------------------------------------------------
|
|
190
|
+
printf '%s\n' '<working-state source="switchroom working-state-reload hook">'
|
|
191
|
+
if [ -n "$MTIME" ]; then
|
|
192
|
+
printf '%s\n' 'The following is your working-state file ('"$WORKING_STATE_FILE"', last updated '"$MTIME"'),'
|
|
193
|
+
else
|
|
194
|
+
printf '%s\n' 'The following is your working-state file ('"$WORKING_STATE_FILE"'),'
|
|
195
|
+
fi
|
|
196
|
+
printf '%s\n' 'reloaded verbatim so in-flight task state survives the summarizer. It may'
|
|
197
|
+
printf '%s\n' 'be stale — reconcile it against the summary and the recovery tools above'
|
|
198
|
+
printf '%s\n' 'before trusting it, then continue.'
|
|
199
|
+
printf '%s\n' '---'
|
|
200
|
+
cat "$WORKING_STATE_FILE"
|
|
201
|
+
printf '\n%s\n' '</working-state>'
|
|
202
|
+
fi
|
|
203
|
+
|
|
204
|
+
# ---------------------------------------------------------------------------
|
|
205
|
+
# Lean post-compaction briefing (P1). ADDITIVE on source=compact, after the
|
|
206
|
+
# static recovery block and the optional working-state append. It re-seats the
|
|
207
|
+
# compacted session to fresh-boot PARITY: a scoped recent Telegram tail + a
|
|
208
|
+
# Hindsight recall, so the agent picks up the actual conversation rather than
|
|
209
|
+
# only being TOLD it was compacted.
|
|
210
|
+
#
|
|
211
|
+
# DRY: the assembly is delegated to handoff-briefing.sh --lean — the SINGLE
|
|
212
|
+
# briefing assembler. No copy of the sqlite/recall logic lives here. Lean mode
|
|
213
|
+
# emits ONLY the Telegram-tail + recall (daily-memory and the boot header are
|
|
214
|
+
# skipped — token duplication, the native summary already keeps recent turns),
|
|
215
|
+
# and IGNORES SWITCHROOM_PENDING_* to derive the db-latest surface (a stale
|
|
216
|
+
# pending-turn scope would brief the wrong chat at a mid-session compaction).
|
|
217
|
+
#
|
|
218
|
+
# SHARED PATH: this hook fires on SessionStart(compact) for EVERY agent,
|
|
219
|
+
# regardless of session_continuity.briefing mode (legacy vs gateway), so both
|
|
220
|
+
# modes get identical compaction re-seat through this one path.
|
|
221
|
+
#
|
|
222
|
+
# GRACEFUL: if the assembler script is not found, or emits nothing (no
|
|
223
|
+
# history.db, Hindsight unreachable), the block is simply omitted. The lean
|
|
224
|
+
# briefing NEVER fails the hook — the recovery block above already stands on
|
|
225
|
+
# its own. handoff-briefing.sh caps its only network hop (Hindsight) at a few
|
|
226
|
+
# seconds, so runtime is bounded.
|
|
227
|
+
# ---------------------------------------------------------------------------
|
|
228
|
+
BRIEFING_SCRIPT=""
|
|
229
|
+
_HOOK_DIR=$(dirname -- "$0" 2>/dev/null || true)
|
|
230
|
+
if [ -n "$_HOOK_DIR" ] && [ -r "$_HOOK_DIR/handoff-briefing.sh" ]; then
|
|
231
|
+
BRIEFING_SCRIPT="$_HOOK_DIR/handoff-briefing.sh"
|
|
232
|
+
elif command -v handoff-briefing.sh >/dev/null 2>&1; then
|
|
233
|
+
BRIEFING_SCRIPT="handoff-briefing.sh"
|
|
234
|
+
fi
|
|
235
|
+
|
|
236
|
+
if [ -n "$BRIEFING_SCRIPT" ]; then
|
|
237
|
+
# Inner timeout, SHORTER than the 8s Claude Code hook budget, so a slow
|
|
238
|
+
# assembler degrades to "recovery block only" instead of losing everything.
|
|
239
|
+
# Without it, if the assembler runs long (e.g. an operator raises
|
|
240
|
+
# HANDOFF_BRIEFING_HINDSIGHT_TIMEOUT past the hook budget) Claude Code kills
|
|
241
|
+
# the WHOLE hook at 8s and discards ALL stdout — including the near-unkillable
|
|
242
|
+
# <compact-recovery> orientation block already printed above. Capping the
|
|
243
|
+
# assembler at 5s keeps the #4390 recovery floor intact. `timeout` is
|
|
244
|
+
# coreutils (present in the agent image); fall back to an un-timed call if it
|
|
245
|
+
# is somehow unavailable, so the lean briefing still works.
|
|
246
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
247
|
+
LEAN_BRIEFING=$(timeout 5 bash "$BRIEFING_SCRIPT" --lean 2>/dev/null || true)
|
|
248
|
+
else
|
|
249
|
+
LEAN_BRIEFING=$(bash "$BRIEFING_SCRIPT" --lean 2>/dev/null || true)
|
|
250
|
+
fi
|
|
251
|
+
if [ -n "$LEAN_BRIEFING" ]; then
|
|
252
|
+
printf '%s\n' '<compact-briefing source="switchroom working-state-reload hook">'
|
|
253
|
+
printf '%s\n' 'The recent conversation and recalled memory below are re-seated so this'
|
|
254
|
+
printf '%s\n' 'compacted session has the same footing as a fresh boot. Use them to pick up'
|
|
255
|
+
printf '%s\n' 'the thread; reconcile against the native summary above before trusting either.'
|
|
256
|
+
printf '%s\n' '---'
|
|
257
|
+
printf '%s\n' "$LEAN_BRIEFING"
|
|
258
|
+
printf '%s\n' '</compact-briefing>'
|
|
259
|
+
fi
|
|
260
|
+
fi
|
|
261
|
+
|
|
262
|
+
exit 0
|
|
@@ -12019,12 +12019,13 @@ function recordReadFailure(agentCfg, failure) {
|
|
|
12019
12019
|
writable: false
|
|
12020
12020
|
});
|
|
12021
12021
|
}
|
|
12022
|
-
function overlayReadFailures(config, agent) {
|
|
12022
|
+
function overlayReadFailures(config, agent, source) {
|
|
12023
12023
|
const agentCfg = config.agents?.[agent];
|
|
12024
12024
|
if (!agentCfg)
|
|
12025
12025
|
return [];
|
|
12026
12026
|
const list = agentCfg[OVERLAY_READ_FAILURES];
|
|
12027
|
-
|
|
12027
|
+
const all = Array.isArray(list) ? list : [];
|
|
12028
|
+
return source ? all.filter((f) => f.source === source) : all;
|
|
12028
12029
|
}
|
|
12029
12030
|
function deriveOverlayTitle(raw, fileName) {
|
|
12030
12031
|
const titleFromComment = raw.match(/^#[^\S\n]*name:[^\S\n]*(\S.*?)[^\S\n]*$/m)?.[1];
|
|
@@ -12035,7 +12036,7 @@ function deriveOverlayTitle(raw, fileName) {
|
|
|
12035
12036
|
return;
|
|
12036
12037
|
return base.length > 0 ? base : undefined;
|
|
12037
12038
|
}
|
|
12038
|
-
function readOverlayFile(agentName, file, agentCfg, warnings) {
|
|
12039
|
+
function readOverlayFile(agentName, file, agentCfg, warnings, source) {
|
|
12039
12040
|
try {
|
|
12040
12041
|
return readFileSync(file, "utf-8");
|
|
12041
12042
|
} catch (err) {
|
|
@@ -12048,7 +12049,7 @@ function readOverlayFile(agentName, file, agentCfg, warnings) {
|
|
|
12048
12049
|
reason: `read error: ${err.message}`,
|
|
12049
12050
|
code: code ?? "EUNKNOWN"
|
|
12050
12051
|
};
|
|
12051
|
-
recordReadFailure(agentCfg, { file, code: w.code });
|
|
12052
|
+
recordReadFailure(agentCfg, { file, code: w.code, source });
|
|
12052
12053
|
warnings.push(w);
|
|
12053
12054
|
console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${file}': ${w.reason}`);
|
|
12054
12055
|
return;
|
|
@@ -12112,14 +12113,14 @@ function applyAgentOverlays(config) {
|
|
|
12112
12113
|
reason: `read error: cannot list overlay directory (${code})`,
|
|
12113
12114
|
code
|
|
12114
12115
|
};
|
|
12115
|
-
recordReadFailure(agentCfg, { file: scheduleDir, code });
|
|
12116
|
+
recordReadFailure(agentCfg, { file: scheduleDir, code, source: "schedule" });
|
|
12116
12117
|
warnings.push(w);
|
|
12117
12118
|
console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${scheduleDir}': ${w.reason}`);
|
|
12118
12119
|
});
|
|
12119
12120
|
if (files.length > 0) {
|
|
12120
12121
|
const merged = [...agentCfg.schedule ?? []];
|
|
12121
12122
|
for (const file of files) {
|
|
12122
|
-
const raw = readOverlayFile(agentName, file, agentCfg, warnings);
|
|
12123
|
+
const raw = readOverlayFile(agentName, file, agentCfg, warnings, "schedule");
|
|
12123
12124
|
if (raw === undefined)
|
|
12124
12125
|
continue;
|
|
12125
12126
|
try {
|
|
@@ -12164,7 +12165,7 @@ function applyAgentOverlays(config) {
|
|
|
12164
12165
|
reason: `read error: cannot list overlay directory (${code})`,
|
|
12165
12166
|
code
|
|
12166
12167
|
};
|
|
12167
|
-
recordReadFailure(agentCfg, { file: skillsDir, code });
|
|
12168
|
+
recordReadFailure(agentCfg, { file: skillsDir, code, source: "skills" });
|
|
12168
12169
|
warnings.push(w);
|
|
12169
12170
|
console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${skillsDir}': ${w.reason}`);
|
|
12170
12171
|
});
|
|
@@ -12172,7 +12173,7 @@ function applyAgentOverlays(config) {
|
|
|
12172
12173
|
const merged = [...agentCfg.skills ?? []];
|
|
12173
12174
|
const seen = new Set(merged);
|
|
12174
12175
|
for (const file of skillFiles) {
|
|
12175
|
-
const raw = readOverlayFile(agentName, file, agentCfg, warnings);
|
|
12176
|
+
const raw = readOverlayFile(agentName, file, agentCfg, warnings, "skills");
|
|
12176
12177
|
if (raw === undefined)
|
|
12177
12178
|
continue;
|
|
12178
12179
|
try {
|
|
@@ -14043,7 +14044,8 @@ var MarkThrottledRequestSchema = exports_external.object({
|
|
|
14043
14044
|
v: exports_external.literal(PROTOCOL_VERSION),
|
|
14044
14045
|
op: exports_external.literal("mark-throttled"),
|
|
14045
14046
|
id: exports_external.string().min(1),
|
|
14046
|
-
until: exports_external.number().int().positive()
|
|
14047
|
+
until: exports_external.number().int().positive(),
|
|
14048
|
+
probeOnly: exports_external.boolean().optional()
|
|
14047
14049
|
});
|
|
14048
14050
|
var RefreshAccountRequestSchema = exports_external.object({
|
|
14049
14051
|
v: exports_external.literal(PROTOCOL_VERSION),
|
|
@@ -14460,12 +14462,13 @@ class AuthBrokerClient {
|
|
|
14460
14462
|
const data = await this.send(req);
|
|
14461
14463
|
return data;
|
|
14462
14464
|
}
|
|
14463
|
-
async markThrottled(until) {
|
|
14465
|
+
async markThrottled(until, probeOnly = false) {
|
|
14464
14466
|
const data = await this.send({
|
|
14465
14467
|
v: PROTOCOL_VERSION,
|
|
14466
14468
|
id: randomUUID(),
|
|
14467
14469
|
op: "mark-throttled",
|
|
14468
|
-
until
|
|
14470
|
+
until,
|
|
14471
|
+
...probeOnly ? { probeOnly: true } : {}
|
|
14469
14472
|
});
|
|
14470
14473
|
return data;
|
|
14471
14474
|
}
|
|
@@ -15337,7 +15340,7 @@ function formatReadFailures(failures) {
|
|
|
15337
15340
|
}
|
|
15338
15341
|
function loadAgentEntriesStrict(configPath, agentName) {
|
|
15339
15342
|
const config = loadConfig(configPath);
|
|
15340
|
-
const failures = overlayReadFailures(config, agentName);
|
|
15343
|
+
const failures = overlayReadFailures(config, agentName, "schedule");
|
|
15341
15344
|
if (failures.length > 0) {
|
|
15342
15345
|
throw new Error(`schedule.d overlay unreadable — refusing a reload that may be ` + `missing entries: ${formatReadFailures(failures)}`);
|
|
15343
15346
|
}
|
|
@@ -15382,7 +15385,7 @@ async function main() {
|
|
|
15382
15385
|
const config = loadConfig(configPath);
|
|
15383
15386
|
const allEntries = collectScheduleEntries(config);
|
|
15384
15387
|
const entries = allEntries.filter((e) => e.agent === agentName);
|
|
15385
|
-
const bootReadFailures = overlayReadFailures(config, agentName);
|
|
15388
|
+
const bootReadFailures = overlayReadFailures(config, agentName, "schedule");
|
|
15386
15389
|
if (bootReadFailures.length > 0) {
|
|
15387
15390
|
process.stderr.write(`agent-scheduler: ${agentName} WARNING: ${bootReadFailures.length} ` + `schedule.d overlay file(s) unreadable at boot — their cron entries ` + `are NOT registered: ${formatReadFailures(bootReadFailures)}
|
|
15388
15391
|
`);
|