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.
Files changed (43) hide show
  1. package/bin/handoff-briefing.sh +57 -5
  2. package/bin/working-state-reload-hook.sh +262 -0
  3. package/dist/agent-scheduler/index.js +16 -13
  4. package/dist/auth-broker/index.js +70 -30
  5. package/dist/cli/autoaccept-poll.js +5 -3
  6. package/dist/cli/drive-write-pretool.mjs +5 -3
  7. package/dist/cli/ms-365-write-pretool.mjs +5 -3
  8. package/dist/cli/notion-write-pretool.mjs +6 -6
  9. package/dist/cli/switchroom.js +42 -13
  10. package/dist/host-control/main.js +7 -7
  11. package/dist/vault/approvals/kernel-server.js +6 -6
  12. package/dist/vault/broker/server.js +6 -6
  13. package/package.json +1 -1
  14. package/profiles/_base/start.sh.hbs +49 -0
  15. package/profiles/default/CLAUDE.md.hbs +12 -13
  16. package/telegram-plugin/ask-user.ts +6 -7
  17. package/telegram-plugin/dist/gateway/gateway.js +192 -66
  18. package/telegram-plugin/gateway/auth-broker-client.ts +1 -1
  19. package/telegram-plugin/gateway/auth-command.ts +4 -2
  20. package/telegram-plugin/gateway/checklist-fallback.ts +8 -1
  21. package/telegram-plugin/gateway/gateway.ts +8 -4
  22. package/telegram-plugin/gateway/outbound-send-path.ts +9 -1
  23. package/telegram-plugin/gateway/subagent-handback-inbound-builder.ts +21 -1
  24. package/telegram-plugin/gateway/subagent-handback-marker.ts +94 -0
  25. package/telegram-plugin/gateway/throttle-tier-wiring.ts +93 -18
  26. package/telegram-plugin/render/emphasis-guard.ts +92 -12
  27. package/telegram-plugin/render/line-start-guard.ts +27 -2
  28. package/telegram-plugin/sticker-aliases.ts +12 -14
  29. package/telegram-plugin/tests/ask-user.test.ts +15 -0
  30. package/telegram-plugin/tests/checklist-fallback.test.ts +21 -0
  31. package/telegram-plugin/tests/handback-tasknotif-dedup.test.ts +248 -0
  32. package/telegram-plugin/tests/render/emphasis-guard.test.ts +105 -6
  33. package/telegram-plugin/tests/render/heading-guard-blockquote-glued-hash.test.ts +123 -36
  34. package/telegram-plugin/tests/reply-quote-wire.test.ts +47 -0
  35. package/telegram-plugin/tests/sticker-aliases.test.ts +43 -0
  36. package/telegram-plugin/tests/throttle-tier-probe-only.test.ts +216 -0
  37. package/telegram-plugin/tests/throttle-tier-route-429-wiring.test.ts +92 -0
  38. package/telegram-plugin/tests/throttle-tier-route-429.test.ts +71 -0
  39. package/telegram-plugin/throttle-tier.ts +59 -0
  40. package/vendor/hindsight-memory/CHANGELOG.md +31 -0
  41. package/vendor/hindsight-memory/hooks/hooks.json +2 -1
  42. package/vendor/hindsight-memory/scripts/session_start.py +35 -8
  43. package/vendor/hindsight-memory/scripts/tests/test_session_start_durability.py +107 -0
@@ -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
- if [ "${HANDOFF_BRIEFING_STDOUT:-}" = "1" ] || [ "${1:-}" = "--stdout" ]; then
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
- if [ -n "$TODAY" ] && [ -n "$WORKSPACE_DIR" ]; then
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
- printf '%s\n\n---\n\n%s\n' "$BRIEFING_HEADER" "$STDOUT_BUFFER"
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
- return Array.isArray(list) ? list : [];
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
  `);