switchroom 0.19.48 → 0.20.1
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 +213 -74
- package/dist/agent-scheduler/index.js +18 -1
- package/dist/auth-broker/index.js +19 -2
- package/dist/buzz-gateway/index.js +9367 -0
- package/dist/cli/notion-write-pretool.mjs +18 -1
- package/dist/cli/switchroom.js +24734 -16371
- package/dist/host-control/main.js +59 -9
- package/dist/vault/approvals/kernel-server.js +19 -2
- package/dist/vault/broker/server.js +19 -2
- package/package.json +6 -4
- package/profiles/_base/start.sh.hbs +148 -2
- package/profiles/default/CLAUDE.md.hbs +1 -1
- package/skills/dev-protocol/SKILL.md +30 -1
- package/skills/switchroom-architecture/SKILL.md +5 -0
- package/skills/switchroom-cli/SKILL.md +1 -1
- package/telegram-plugin/dist/bridge/bridge.js +7 -4
- package/telegram-plugin/dist/gateway/gateway.js +2376 -1039
- package/telegram-plugin/dist/server.js +7 -4
- package/telegram-plugin/gateway/access-store.test.ts +234 -0
- package/telegram-plugin/gateway/access-store.ts +194 -0
- package/telegram-plugin/gateway/boot-briefing-builder.ts +586 -0
- package/telegram-plugin/gateway/boot-briefing-capability.ts +31 -0
- package/telegram-plugin/gateway/boot-briefing-wiring.ts +332 -0
- package/telegram-plugin/gateway/buzz-mirror-correlation-store.ts +285 -0
- package/telegram-plugin/gateway/buzz-mirror.ts +494 -0
- package/telegram-plugin/gateway/buzz-type-guards.ts +34 -0
- package/telegram-plugin/gateway/channel-route.ts +272 -0
- package/telegram-plugin/gateway/gateway.ts +115 -203
- package/telegram-plugin/gateway/inbound-router.ts +93 -3
- package/telegram-plugin/gateway/inbound-spool.ts +33 -1
- package/telegram-plugin/gateway/ipc-protocol.ts +81 -2
- package/telegram-plugin/gateway/ipc-server.ts +197 -2
- package/telegram-plugin/gateway/outbound-send-path.ts +85 -2
- package/telegram-plugin/gateway/pending-turn-env.ts +70 -0
- package/telegram-plugin/gateway/stream-render.ts +21 -0
- package/telegram-plugin/gateway/subagent-handback-marker.ts +12 -0
- package/telegram-plugin/gateway/user-failure-notices.ts +172 -0
- package/telegram-plugin/history.ts +15 -0
- package/telegram-plugin/llm-error-present.ts +9 -4
- package/telegram-plugin/model-unavailable.ts +4 -0
- package/telegram-plugin/operator-events.fixtures.json +12 -12
- package/telegram-plugin/operator-events.ts +81 -9
- package/telegram-plugin/session-tail.ts +7 -1
- package/telegram-plugin/tests/boot-briefing-builder.test.ts +995 -0
- package/telegram-plugin/tests/buzz-mirror-correlation-store.test.ts +173 -0
- package/telegram-plugin/tests/buzz-mirror.test.ts +538 -0
- package/telegram-plugin/tests/buzz-origin-stamp-gate.test.ts +159 -0
- package/telegram-plugin/tests/channel-route.test.ts +306 -0
- package/telegram-plugin/tests/inbound-spool.test.ts +47 -0
- package/telegram-plugin/tests/ipc-server-buzz-dedup.test.ts +124 -0
- package/telegram-plugin/tests/ipc-server-buzz-peer.test.ts +269 -0
- package/telegram-plugin/tests/operator-events-session-tail.test.ts +63 -0
- package/telegram-plugin/tests/operator-events.test.ts +71 -7
- package/telegram-plugin/tests/outbound-send-path.test.ts +24 -0
- package/telegram-plugin/tests/reply-to-buffer-fallback.test.ts +273 -0
- package/telegram-plugin/tests/reply-to-buffer-history.test.ts +134 -0
- package/telegram-plugin/tests/user-failure-notices.test.ts +165 -0
- package/telegram-plugin/voice-normalize-text.ts +5 -0
- package/vendor/hindsight-memory/scripts/directive_verify.py +4 -0
- package/vendor/hindsight-memory/scripts/recall.py +7 -2
package/bin/handoff-briefing.sh
CHANGED
|
@@ -47,7 +47,29 @@ HINDSIGHT_BANK="${HINDSIGHT_BANK_ID:-}"
|
|
|
47
47
|
AGENT_DIR="${AGENT_DIR:-}"
|
|
48
48
|
WORKSPACE_DIR="${WORKSPACE_DIR:-$AGENT_DIR}"
|
|
49
49
|
MAX_MESSAGES="${HANDOFF_BRIEFING_MAX_MESSAGES:-20}"
|
|
50
|
-
|
|
50
|
+
# Hindsight is the only network hop; cap it at 3s so it finishes well inside
|
|
51
|
+
# start.sh's outer `timeout 10` kill budget (was 4s under a 5s outer, which
|
|
52
|
+
# left almost no margin — a slow recall could be killed mid-write). Local
|
|
53
|
+
# SQLite + daily-memory reads are sub-second, so 3s is the whole network cost.
|
|
54
|
+
HINDSIGHT_TIMEOUT="${HANDOFF_BRIEFING_HINDSIGHT_TIMEOUT:-3}"
|
|
55
|
+
|
|
56
|
+
# Chat/thread scope for the recent-conversation section (#continuity). A
|
|
57
|
+
# forum/group agent's history.db holds messages from many topics; an unscoped
|
|
58
|
+
# "last 20" briefing pollutes the reorientation with unrelated threads. Prefer
|
|
59
|
+
# the surface that was mid-turn when the prior session ended (exported by
|
|
60
|
+
# start.sh from the pending-turn env on an interrupted boot); otherwise the
|
|
61
|
+
# python fallback derives the single most-recently-active (chat_id, thread_id)
|
|
62
|
+
# surface straight from the DB. Empty chat ⇒ let python derive it.
|
|
63
|
+
#
|
|
64
|
+
# TARGET_THREAD_ID is tri-state: a numbered thread scopes to that topic; the
|
|
65
|
+
# literal sentinel `NULL` means the surface's thread is genuinely NULL (a DM /
|
|
66
|
+
# forum General topic) and scopes with `thread_id IS NULL`; empty means the
|
|
67
|
+
# thread is UNKNOWN and falls back to chat-only scope (all threads). The
|
|
68
|
+
# pending-turn env writer emits `NULL` when it knows the interrupted turn's
|
|
69
|
+
# thread was null, so a General-topic interrupt reboots correctly scoped
|
|
70
|
+
# instead of pulling in every other topic's messages.
|
|
71
|
+
TARGET_CHAT_ID="${SWITCHROOM_PENDING_CHAT_ID:-}"
|
|
72
|
+
TARGET_THREAD_ID="${SWITCHROOM_PENDING_THREAD_ID:-}"
|
|
51
73
|
|
|
52
74
|
# Determine output mode
|
|
53
75
|
STDOUT_MODE=0
|
|
@@ -61,28 +83,119 @@ if [ -n "$TELEGRAM_STATE" ] && [ -d "$TELEGRAM_STATE" ]; then
|
|
|
61
83
|
HISTORY_DB="$TELEGRAM_STATE/history.db"
|
|
62
84
|
if [ -f "$HISTORY_DB" ] && command -v python3 >/dev/null 2>&1; then
|
|
63
85
|
# Use python3's stdlib sqlite3 — no bun:sqlite, no extra deps.
|
|
64
|
-
# Query the most recent $MAX_MESSAGES rows
|
|
65
|
-
#
|
|
66
|
-
|
|
86
|
+
# Query the most recent $MAX_MESSAGES rows for the ACTIVE surface only
|
|
87
|
+
# (chat/thread-scoped, see above), ordered by ts DESC, then reverse for
|
|
88
|
+
# chronological display. We skip system messages (role NULL). The scope
|
|
89
|
+
# source is logged to stderr for boot diagnostics.
|
|
90
|
+
# python stderr is NOT suppressed: the script's only stderr output is the
|
|
91
|
+
# intentional scope breadcrumb + a single graceful line on any caught error
|
|
92
|
+
# (every DB path is wrapped in try/except → sys.exit(0)). start.sh runs this
|
|
93
|
+
# under `2>/dev/null`, so the breadcrumb is a debug/manual-run diagnostic.
|
|
94
|
+
TELEGRAM_ROWS=$(python3 - "$HISTORY_DB" "$MAX_MESSAGES" "$TARGET_CHAT_ID" "$TARGET_THREAD_ID" <<'PYEOF'
|
|
67
95
|
import sys, sqlite3, datetime
|
|
68
96
|
|
|
69
97
|
db_path = sys.argv[1]
|
|
70
98
|
limit = int(sys.argv[2])
|
|
99
|
+
target_chat = sys.argv[3] if len(sys.argv) > 3 else ""
|
|
100
|
+
target_thread = sys.argv[4] if len(sys.argv) > 4 else ""
|
|
71
101
|
|
|
72
102
|
try:
|
|
73
103
|
conn = sqlite3.connect(db_path)
|
|
74
104
|
conn.row_factory = sqlite3.Row
|
|
75
105
|
cur = conn.cursor()
|
|
76
|
-
|
|
106
|
+
|
|
107
|
+
# Resolve the surface to scope to.
|
|
108
|
+
# - explicit env target (pending-turn chat/thread) wins — UNLESS that chat
|
|
109
|
+
# has zero rows (rotated/fresh DB), in which case we fall through to
|
|
110
|
+
# db-latest so a stale env target doesn't yield an empty section;
|
|
111
|
+
# - else derive the single most-recently-active (chat_id, thread_id).
|
|
112
|
+
# thread is tri-state: a real value, NULL (DM / general), or unknown.
|
|
113
|
+
# - target_thread == "NULL" (sentinel from start.sh / pending-turn-env)
|
|
114
|
+
# means the surface's thread is GENUINELY NULL (DM / general topic) —
|
|
115
|
+
# scope with `thread_id IS NULL`, do NOT pull in numbered-thread rows.
|
|
116
|
+
# - target_thread == "" (empty) means UNKNOWN — chat-only scope (all
|
|
117
|
+
# threads), the safe backward-compatible fallback.
|
|
118
|
+
scope_chat = None
|
|
119
|
+
scope_thread = None # int thread id
|
|
120
|
+
scope_thread_known = False # True once we know the exact thread (incl. NULL)
|
|
121
|
+
scope_thread_is_null = False # True when the surface's thread is NULL
|
|
122
|
+
scope_source = "unscoped"
|
|
123
|
+
|
|
124
|
+
def _chat_has_rows(chat_id):
|
|
125
|
+
cur.execute(
|
|
126
|
+
"SELECT 1 FROM messages WHERE role IN ('user', 'assistant') "
|
|
127
|
+
"AND chat_id = ? LIMIT 1",
|
|
128
|
+
[chat_id],
|
|
129
|
+
)
|
|
130
|
+
return cur.fetchone() is not None
|
|
131
|
+
|
|
132
|
+
def _derive_db_latest():
|
|
133
|
+
# The single most-recently-active (chat_id, thread_id) surface.
|
|
134
|
+
cur.execute(
|
|
135
|
+
"""
|
|
136
|
+
SELECT chat_id, thread_id
|
|
137
|
+
FROM messages
|
|
138
|
+
WHERE role IN ('user', 'assistant')
|
|
139
|
+
ORDER BY ts DESC
|
|
140
|
+
LIMIT 1
|
|
141
|
+
"""
|
|
142
|
+
)
|
|
143
|
+
return cur.fetchone()
|
|
144
|
+
|
|
145
|
+
# Env target — accept it only when its chat actually has rows (2b). A
|
|
146
|
+
# rotated/fresh DB can leave a pending-turn chat with no persisted
|
|
147
|
+
# messages; scoping to it would silently emit an empty section, so we
|
|
148
|
+
# fall through to db-latest instead.
|
|
149
|
+
if target_chat and _chat_has_rows(target_chat):
|
|
150
|
+
scope_chat = target_chat
|
|
151
|
+
scope_source = "env"
|
|
152
|
+
if target_thread == "NULL":
|
|
153
|
+
scope_thread_is_null = True
|
|
154
|
+
scope_thread_known = True
|
|
155
|
+
elif target_thread != "":
|
|
156
|
+
try:
|
|
157
|
+
scope_thread = int(target_thread)
|
|
158
|
+
scope_thread_known = True
|
|
159
|
+
except ValueError:
|
|
160
|
+
scope_thread = None # unparseable → chat-only scope
|
|
161
|
+
# else: unknown thread → chat-only scope (all threads of the chat)
|
|
162
|
+
|
|
163
|
+
if scope_chat is None:
|
|
164
|
+
latest = _derive_db_latest()
|
|
165
|
+
if latest is not None:
|
|
166
|
+
scope_chat = latest["chat_id"]
|
|
167
|
+
scope_source = "db-latest"
|
|
168
|
+
scope_thread_known = True
|
|
169
|
+
if latest["thread_id"] is None:
|
|
170
|
+
scope_thread_is_null = True
|
|
171
|
+
else:
|
|
172
|
+
scope_thread = latest["thread_id"]
|
|
173
|
+
|
|
174
|
+
if scope_chat is None:
|
|
175
|
+
# Empty DB (nothing to scope) — nothing to show.
|
|
176
|
+
sys.stderr.write("handoff-briefing: no messages to scope; empty section\n")
|
|
177
|
+
sys.exit(0)
|
|
178
|
+
|
|
179
|
+
where = ["role IN ('user', 'assistant')", "chat_id = ?"]
|
|
180
|
+
params = [scope_chat]
|
|
181
|
+
if scope_thread_known:
|
|
182
|
+
if scope_thread_is_null:
|
|
183
|
+
where.append("thread_id IS NULL")
|
|
184
|
+
else:
|
|
185
|
+
where.append("thread_id = ?")
|
|
186
|
+
params.append(scope_thread)
|
|
187
|
+
params.append(limit)
|
|
188
|
+
|
|
189
|
+
sys.stderr.write(
|
|
190
|
+
"handoff-briefing: scoping recent conversation to chat=%s thread=%s (source=%s)\n"
|
|
191
|
+
% (scope_chat, "NULL" if scope_thread_is_null else (scope_thread if scope_thread_known else "any"), scope_source)
|
|
192
|
+
)
|
|
193
|
+
|
|
77
194
|
cur.execute(
|
|
78
|
-
""
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
ORDER BY ts DESC
|
|
83
|
-
LIMIT ?
|
|
84
|
-
""",
|
|
85
|
-
(limit,),
|
|
195
|
+
"SELECT role, user, ts, text FROM messages WHERE "
|
|
196
|
+
+ " AND ".join(where)
|
|
197
|
+
+ " ORDER BY ts DESC LIMIT ?",
|
|
198
|
+
params,
|
|
86
199
|
)
|
|
87
200
|
rows = list(reversed(cur.fetchall()))
|
|
88
201
|
conn.close()
|
|
@@ -110,6 +223,70 @@ $TELEGRAM_ROWS"
|
|
|
110
223
|
fi
|
|
111
224
|
fi
|
|
112
225
|
|
|
226
|
+
# ── Header (cheap, no network) — computed early so the recent-conversation
|
|
227
|
+
# section can be flushed to disk BEFORE the slow Hindsight hop. ──────────────
|
|
228
|
+
# Restart timestamp — model-facing: it lands in the resume-turn system prompt
|
|
229
|
+
# via --append-system-prompt ("You just restarted at …"). Render the agent's
|
|
230
|
+
# LOCAL am/pm wall clock, NOT UTC, so the restart turn never sees a competing
|
|
231
|
+
# UTC "now". Same SWITCHROOM_TIMEZONE → TZ → UTC cascade the daily section uses.
|
|
232
|
+
_TZ_VAL="${SWITCHROOM_TIMEZONE:-${TZ:-UTC}}"
|
|
233
|
+
TIMESTAMP=$(TZ="$_TZ_VAL" date '+%A %Y-%m-%d %I:%M %p %Z' 2>/dev/null || date '+%A %Y-%m-%d %I:%M %p %Z')
|
|
234
|
+
RESTART_REASON="unknown"
|
|
235
|
+
if [ -n "$AGENT_DIR" ] && [ -f "$AGENT_DIR/.restart-reason" ]; then
|
|
236
|
+
RESTART_REASON=$(cat "$AGENT_DIR/.restart-reason" 2>/dev/null | head -1 | tr -d '\r\n')
|
|
237
|
+
fi
|
|
238
|
+
if [ -n "${SWITCHROOM_PENDING_ENDED_VIA:-}" ]; then
|
|
239
|
+
RESTART_REASON="$SWITCHROOM_PENDING_ENDED_VIA"
|
|
240
|
+
fi
|
|
241
|
+
BRIEFING_HEADER="You just restarted at ${TIMESTAMP}. Previous session ended via: ${RESTART_REASON}. Consult this briefing before responding."
|
|
242
|
+
|
|
243
|
+
# Resolve the output destination up front (file mode only).
|
|
244
|
+
OUTPUT_FILE=""
|
|
245
|
+
OUTPUT_TMP=""
|
|
246
|
+
if [ "$STDOUT_MODE" != "1" ] && [ -n "$AGENT_DIR" ]; then
|
|
247
|
+
OUTPUT_FILE="$AGENT_DIR/.handoff-briefing.md"
|
|
248
|
+
OUTPUT_TMP="${OUTPUT_FILE}.tmp.$$"
|
|
249
|
+
fi
|
|
250
|
+
|
|
251
|
+
# Incremental emit (#continuity). Sections are flushed to the output file as
|
|
252
|
+
# each source resolves — the recent-conversation section (the highest-value,
|
|
253
|
+
# always-local part) is written BEFORE the network-bound Hindsight hop. So a
|
|
254
|
+
# late-stage kill (e.g. start.sh's outer `timeout`, SIGTERM by default) mid-Hindsight still
|
|
255
|
+
# leaves the recent conversation on disk instead of nothing. The first flush
|
|
256
|
+
# writes header+section atomically via tmp+mv; later sections append. In
|
|
257
|
+
# stdout/no-AGENT_DIR mode we buffer and print once at the end instead.
|
|
258
|
+
FILE_STARTED=0
|
|
259
|
+
STDOUT_BUFFER=""
|
|
260
|
+
emit_section() {
|
|
261
|
+
# $1 = section content (assumed non-empty)
|
|
262
|
+
section="$1"
|
|
263
|
+
if [ -n "$OUTPUT_FILE" ]; then
|
|
264
|
+
if [ "$FILE_STARTED" = "0" ]; then
|
|
265
|
+
# First section: header + divider + section, written atomically.
|
|
266
|
+
printf '%s\n\n---\n\n%s' "$BRIEFING_HEADER" "$section" > "$OUTPUT_TMP" \
|
|
267
|
+
&& mv -f "$OUTPUT_TMP" "$OUTPUT_FILE"
|
|
268
|
+
FILE_STARTED=1
|
|
269
|
+
else
|
|
270
|
+
printf '\n\n---\n\n%s' "$section" >> "$OUTPUT_FILE"
|
|
271
|
+
fi
|
|
272
|
+
else
|
|
273
|
+
if [ -z "$STDOUT_BUFFER" ]; then
|
|
274
|
+
STDOUT_BUFFER="$section"
|
|
275
|
+
else
|
|
276
|
+
STDOUT_BUFFER="$STDOUT_BUFFER
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
$section"
|
|
281
|
+
fi
|
|
282
|
+
fi
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
# Flush the recent-conversation section NOW, before Hindsight.
|
|
286
|
+
if [ -n "$TELEGRAM_SECTION" ]; then
|
|
287
|
+
emit_section "$TELEGRAM_SECTION"
|
|
288
|
+
fi
|
|
289
|
+
|
|
113
290
|
# ── Source 2: Hindsight recall ──────────────────────────────────────────────────
|
|
114
291
|
HINDSIGHT_SECTION=""
|
|
115
292
|
if [ -n "$HINDSIGHT_URL" ] && [ -n "$HINDSIGHT_BANK" ] && command -v curl >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then
|
|
@@ -133,14 +310,16 @@ $RECALL_TEXT"
|
|
|
133
310
|
fi
|
|
134
311
|
fi
|
|
135
312
|
fi
|
|
313
|
+
if [ -n "$HINDSIGHT_SECTION" ]; then
|
|
314
|
+
emit_section "$HINDSIGHT_SECTION"
|
|
315
|
+
fi
|
|
136
316
|
|
|
137
317
|
# ── Source 3: Today's daily memory ─────────────────────────────────────────────
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
# UTC here would look up the wrong day's file
|
|
141
|
-
# date is ahead of/behind UTC, silently
|
|
142
|
-
#
|
|
143
|
-
_TZ_VAL="${SWITCHROOM_TIMEZONE:-${TZ:-UTC}}"
|
|
318
|
+
# TODAY keys the daily-memory lookup (memory/${TODAY}.md) in the agent's LOCAL
|
|
319
|
+
# time (_TZ_VAL, resolved in the header block above) — NOT the process default
|
|
320
|
+
# (UTC on most hosts/CI). Using UTC here would look up the wrong day's file
|
|
321
|
+
# during the window where the local date is ahead of/behind UTC, silently
|
|
322
|
+
# dropping today's memory.
|
|
144
323
|
DAILY_SECTION=""
|
|
145
324
|
TODAY=$(TZ="$_TZ_VAL" date +%Y-%m-%d 2>/dev/null || date +%Y-%m-%d 2>/dev/null || true)
|
|
146
325
|
if [ -n "$TODAY" ] && [ -n "$WORKSPACE_DIR" ]; then
|
|
@@ -154,66 +333,26 @@ $DAILY_CONTENT"
|
|
|
154
333
|
fi
|
|
155
334
|
fi
|
|
156
335
|
fi
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
# Restart timestamp — model-facing: it lands in the resume-turn system prompt
|
|
160
|
-
# via --append-system-prompt ("You just restarted at …"). Render the agent's
|
|
161
|
-
# LOCAL am/pm wall clock, NOT UTC, so the restart turn never sees a competing
|
|
162
|
-
# UTC "now" (the whole point of the deterministic-local-time work). Same
|
|
163
|
-
# SWITCHROOM_TIMEZONE → TZ → UTC cascade and `%A %Y-%m-%d %I:%M %p %Z` am/pm
|
|
164
|
-
# format the UserPromptSubmit local-time hook (bin/timezone-hook.sh) uses.
|
|
165
|
-
# (_TZ_VAL is computed once above, in the daily-memory section.)
|
|
166
|
-
TIMESTAMP=$(TZ="$_TZ_VAL" date '+%A %Y-%m-%d %I:%M %p %Z' 2>/dev/null || date '+%A %Y-%m-%d %I:%M %p %Z')
|
|
167
|
-
|
|
168
|
-
# Determine restart reason if available
|
|
169
|
-
RESTART_REASON="unknown"
|
|
170
|
-
if [ -n "$AGENT_DIR" ] && [ -f "$AGENT_DIR/.restart-reason" ]; then
|
|
171
|
-
RESTART_REASON=$(cat "$AGENT_DIR/.restart-reason" 2>/dev/null | head -1 | tr -d '\r\n')
|
|
336
|
+
if [ -n "$DAILY_SECTION" ]; then
|
|
337
|
+
emit_section "$DAILY_SECTION"
|
|
172
338
|
fi
|
|
173
|
-
# Also check SWITCHROOM_PENDING_ENDED_VIA if set by start.sh
|
|
174
|
-
if [ -n "${SWITCHROOM_PENDING_ENDED_VIA:-}" ]; then
|
|
175
|
-
RESTART_REASON="$SWITCHROOM_PENDING_ENDED_VIA"
|
|
176
|
-
fi
|
|
177
|
-
|
|
178
|
-
# Build the briefing body
|
|
179
|
-
SECTIONS=""
|
|
180
|
-
for section in "$TELEGRAM_SECTION" "$HINDSIGHT_SECTION" "$DAILY_SECTION"; do
|
|
181
|
-
if [ -n "$section" ]; then
|
|
182
|
-
if [ -n "$SECTIONS" ]; then
|
|
183
|
-
SECTIONS="$SECTIONS
|
|
184
|
-
|
|
185
|
-
---
|
|
186
339
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
340
|
+
# ── Finalize ────────────────────────────────────────────────────────────────────
|
|
341
|
+
# Every section was flushed as it resolved (emit_section). Here we only close
|
|
342
|
+
# out the chosen sink. An all-empty briefing wrote nothing — leave it that way
|
|
343
|
+
# so start.sh skips the --append-system-prompt arg.
|
|
344
|
+
if [ -n "$OUTPUT_FILE" ]; then
|
|
345
|
+
# File mode. FILE_STARTED=1 means at least one section was written atomically
|
|
346
|
+
# (header+first) with the rest appended; add the trailing newline the
|
|
347
|
+
# single-shot writer used to emit. If nothing was written, no file exists.
|
|
348
|
+
if [ "$FILE_STARTED" = "1" ]; then
|
|
349
|
+
printf '\n' >> "$OUTPUT_FILE"
|
|
191
350
|
fi
|
|
192
|
-
done
|
|
193
|
-
|
|
194
|
-
# Empty briefing — nothing to inject
|
|
195
|
-
if [ -z "$SECTIONS" ]; then
|
|
196
|
-
exit 0
|
|
197
|
-
fi
|
|
198
|
-
|
|
199
|
-
BRIEFING="You just restarted at ${TIMESTAMP}. Previous session ended via: ${RESTART_REASON}. Consult this briefing before responding.
|
|
200
|
-
|
|
201
|
-
---
|
|
202
|
-
|
|
203
|
-
${SECTIONS}"
|
|
204
|
-
|
|
205
|
-
# ── Output ──────────────────────────────────────────────────────────────────────
|
|
206
|
-
if [ "$STDOUT_MODE" = "1" ]; then
|
|
207
|
-
printf '%s\n' "$BRIEFING"
|
|
208
351
|
else
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
printf '%s\n' "$
|
|
212
|
-
exit 0
|
|
352
|
+
# stdout / no-AGENT_DIR mode — buffered; print the whole briefing once.
|
|
353
|
+
if [ -n "$STDOUT_BUFFER" ]; then
|
|
354
|
+
printf '%s\n\n---\n\n%s\n' "$BRIEFING_HEADER" "$STDOUT_BUFFER"
|
|
213
355
|
fi
|
|
214
|
-
OUTPUT_FILE="$AGENT_DIR/.handoff-briefing.md"
|
|
215
|
-
OUTPUT_TMP="${OUTPUT_FILE}.tmp.$$"
|
|
216
|
-
printf '%s\n' "$BRIEFING" > "$OUTPUT_TMP" && mv -f "$OUTPUT_TMP" "$OUTPUT_FILE"
|
|
217
356
|
fi
|
|
218
357
|
|
|
219
358
|
exit 0
|
|
@@ -11216,6 +11216,7 @@ var SessionContinuitySchema = exports_external.object({
|
|
|
11216
11216
|
max_turns_in_briefing: exports_external.number().int().positive().optional().describe("Cap on recent user/assistant turn pairs fed to the summarizer."),
|
|
11217
11217
|
resume_mode: exports_external.enum(["auto", "continue", "handoff", "none"]).optional().describe("How to resume the next session. 'handoff' (default as of #362) " + "never passes --continue; a fresh Claude starts each restart and " + "reads a briefing assembled from recent Telegram messages, Hindsight " + "recall, and today's daily memory file. 'auto' uses --continue when " + "the latest JSONL is smaller than resume_max_bytes, else falls back " + "to the handoff briefing. 'continue' always passes --continue. " + "'none' starts completely fresh every time."),
|
|
11218
11218
|
resume_max_bytes: exports_external.number().int().positive().optional().describe("Byte threshold above which 'auto' mode falls back to handoff " + "instead of --continue. Default 2_000_000 (~2MB). Large transcripts " + "can blow out the context window even with prefix caching, and " + "--continue replay is known-fragile at scale."),
|
|
11219
|
+
briefing: exports_external.enum(["gateway", "legacy"]).optional().describe("Which mechanism assembles the fresh-session reorientation briefing " + "(default 'legacy'). 'legacy' keeps today's behaviour: the Stop-hook " + ".handoff.md and/or bin/handoff-briefing.sh, injected via " + "--append-system-prompt. 'gateway' moves it to a gateway boot-time " + "builder sourced from the durable history.db (crash-independent, " + "surface-scoped, token-budgeted) and injects it as a synthetic " + '<channel source="boot_briefing"> inbound over the durable spool — ' + "keeping the system-prompt prefix stable for cross-session prompt " + "caching. Suppressed automatically when resume_mode is " + "'continue'/'auto' (the transcript may be replayed) and on a /reset " + "force-fresh boot. Threaded to the gateway as " + "SWITCHROOM_SESSION_BRIEFING."),
|
|
11219
11220
|
boot_resume: exports_external.enum(["always", "in-flight", "never"]).optional().describe("How the gateway auto-resumes a turn that was IN FLIGHT when the " + "agent restarted. 'in-flight' (default) resumes genuinely " + "interrupted work even after a deliberate/operator restart — a " + "sanctioned restart landing mid-turn no longer silently drops the " + "work. 'always' forces resume unconditionally (same as the " + "SWITCHROOM_BOOT_RESUME_ALWAYS=1 escape hatch). 'never' is the " + "quota-saving posture: don't auto-replay work across a clean " + "restart — but the user is STILL sent a passive notice of what was " + "in flight (silence is never used). Independent of the at-most-once " + "resume ledger and the bounded resume-chain loop-guard, which always " + "apply. Threaded to the gateway as SWITCHROOM_BOOT_RESUME."),
|
|
11220
11221
|
session_retention_max_count: exports_external.number().int().nonnegative().optional().describe("Session-JSONL retention (issue #2792): keep at most this many " + "newest session transcripts under .claude/projects; older ones " + "past both this count and the age bound are pruned by the Stop " + "hook. The newest sessions (and the handoff source) are always " + "kept. Default 20; set 0 to disable the count bound."),
|
|
11221
11222
|
session_retention_max_age_days: exports_external.number().int().nonnegative().optional().describe("Session-JSONL retention (issue #2792): prune session transcripts " + "older than this many days (a file is deleted only when it is BOTH " + "over the count bound and older than this). Default 30; set 0 to " + "disable the age bound.")
|
|
@@ -11348,8 +11349,24 @@ var TelegramChannelSchema = exports_external.object({
|
|
|
11348
11349
|
}
|
|
11349
11350
|
return tg;
|
|
11350
11351
|
});
|
|
11352
|
+
var BuzzChannelSchema = exports_external.object({
|
|
11353
|
+
enabled: exports_external.boolean().default(false).describe("Master switch for the per-agent Buzz sidecar. Default false — the " + "channel ships dark; start.sh forks the sidecar only when true."),
|
|
11354
|
+
relay_url: exports_external.string().regex(/^wss?:\/\//, "relay_url must be a ws:// or wss:// URL").describe("CANONICAL WebSocket URL of the closed Buzz relay — the exact string " + "the relay expects in the NIP-42 `relay` auth tag (e.g. " + "'ws://127.0.0.1:3000'). A live probe proved the relay validates this " + "tag as an exact string match against its own URL BEFORE the " + "membership check, so it is the relay's advertised identity, NOT " + "necessarily the address the sidecar dials. Set relay_dial_url when " + "the reachable address differs (a docker-network IP)."),
|
|
11355
|
+
relay_dial_url: exports_external.string().regex(/^wss?:\/\//, "relay_dial_url must be a ws:// or wss:// URL").optional().describe("Reachable ws:// / wss:// address the sidecar DIALS when it differs " + "from the canonical relay_url (e.g. a docker-network IP the relay's " + "own 127.0.0.1 can't stand in for). The NIP-42 auth tag still uses " + "relay_url. Defaults to relay_url when unset."),
|
|
11356
|
+
relay_host: exports_external.string().regex(/^(\[[0-9a-fA-F:]+\]|[^\s/?#:@]+)(:\d+)?$/, "relay_host must be a bare host[:port] authority — no scheme, path, or userinfo (e.g. '127.0.0.1:3000')").describe("REQUIRED HTTP Host header authority sent verbatim on the WS upgrade " + "(e.g. '127.0.0.1:3000', port included). The relay resolves its " + "community from this header before the upgrade and returns HTTP 404 if " + "it is missing/wrong, so it must match the relay's configured " + "authority and is deployment config, never derived from the dial URL."),
|
|
11357
|
+
nsec_vault_key: exports_external.string().default("buzz/{agent}-nsec").describe("Vault KEY NAME for the agent's Nostr secret key. Broker-fetched " + "in-process at sidecar boot; NEVER resolved into env or logged. " + "'{agent}' is substituted with the agent name."),
|
|
11358
|
+
operator_pubkey: exports_external.string().regex(/^(npub1[02-9ac-hj-np-z]{58}|[0-9a-f]{64})$/, "operator_pubkey must be a bech32 npub or 64-char hex pubkey").describe("The operator's Nostr pubkey (npub or hex). Always in the effective " + "inbound allowlist — the fail-closed default is operator-only."),
|
|
11359
|
+
authorized_pubkeys: exports_external.array(exports_external.string()).default([]).describe("Additional pubkeys (npub or hex) whose signed events may become " + "turns. Effective allowlist = this ∪ {operator_pubkey}. Empty by " + "default (operator-only)."),
|
|
11360
|
+
mirror: exports_external.enum(["both", "origin", "off"]).default("both").describe("Cross-surface mirror mode. 'both' answers on the origin channel AND " + "mirrors a copy to the other; 'off' is a true kill-switch that disables " + "the channel in BOTH directions (the inbound sidecar exits idle). " + "Phase 2b (S2): 'origin' is DEFERRED — the hub's mirror hook lives only " + "in sendReply, so 'origin' cannot be honored soundly; a configured " + "'origin' is degraded to 'off' (dark) at runtime by both the sidecar " + "config loader and the hub (channel-route.ts parseConfiguredMirrorMode). " + "Only 'both' and 'off' ship live in 2b."),
|
|
11361
|
+
chat_id: exports_external.string().min(1, "chat_id must be a non-empty Telegram chat id").describe("Telegram chat id an injected Buzz turn is routed to. Phase 1 is " + "inbound-only, so the agent's reply lands here on Telegram (the " + "authoritative surface); in later phases this is the chat the Buzz " + "turn's Telegram copy maps to. Required — the sidecar refuses to run " + "live without it (BUZZ_CHAT_ID)."),
|
|
11362
|
+
default_channel_id: exports_external.string().describe("Relay-minted group UUID (the NIP-29 `h` tag) the sidecar subscribes " + "to and stamps on injected turns."),
|
|
11363
|
+
channel_map: exports_external.record(exports_external.string(), exports_external.string()).default({}).describe("Optional map of extra group UUIDs → friendly labels."),
|
|
11364
|
+
pubkey_names: exports_external.record(exports_external.string(), exports_external.string()).default({}).describe("Optional petnames: hex/npub pubkey → display name, used to label " + "the sender on injected turns."),
|
|
11365
|
+
pinned_relay_digest: exports_external.string().optional().describe("Pinned relay image digest (M4). RESERVED — no consumer of this field " + "exists; the existing compat-check (compat-check.ts) validates only " + "the wire contract (AUTH kind, message kind, tag names) and does not " + "read this field. Kept in the schema so the intended digest-pin can " + "be wired without a config shape change.")
|
|
11366
|
+
}).strict();
|
|
11351
11367
|
var ChannelsSchema = exports_external.object({
|
|
11352
|
-
telegram: TelegramChannelSchema
|
|
11368
|
+
telegram: TelegramChannelSchema,
|
|
11369
|
+
buzz: BuzzChannelSchema.optional()
|
|
11353
11370
|
}).optional();
|
|
11354
11371
|
var TIMEZONE_REGEX = /^UTC$|^[A-Z][A-Za-z0-9_+-]+(\/[A-Z][A-Za-z0-9_+-]+){1,2}$/;
|
|
11355
11372
|
var ApproverIdSchema = exports_external.union([exports_external.number(), exports_external.string().regex(/^\d+$/)]);
|
|
@@ -10994,7 +10994,7 @@ var init_observation_scopes = __esm(() => {
|
|
|
10994
10994
|
});
|
|
10995
10995
|
|
|
10996
10996
|
// src/config/schema.ts
|
|
10997
|
-
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, ObservationScopeStrategySchema, AntiConfabulationDirectiveSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleServiceTokenSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, releaseBlockFields, ReleaseBlock, RootReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
|
|
10997
|
+
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, ObservationScopeStrategySchema, AntiConfabulationDirectiveSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, BuzzChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleServiceTokenSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, releaseBlockFields, ReleaseBlock, RootReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
|
|
10998
10998
|
var init_schema = __esm(() => {
|
|
10999
10999
|
init_zod();
|
|
11000
11000
|
init_observation_scopes();
|
|
@@ -11241,6 +11241,7 @@ var init_schema = __esm(() => {
|
|
|
11241
11241
|
max_turns_in_briefing: exports_external.number().int().positive().optional().describe("Cap on recent user/assistant turn pairs fed to the summarizer."),
|
|
11242
11242
|
resume_mode: exports_external.enum(["auto", "continue", "handoff", "none"]).optional().describe("How to resume the next session. 'handoff' (default as of #362) " + "never passes --continue; a fresh Claude starts each restart and " + "reads a briefing assembled from recent Telegram messages, Hindsight " + "recall, and today's daily memory file. 'auto' uses --continue when " + "the latest JSONL is smaller than resume_max_bytes, else falls back " + "to the handoff briefing. 'continue' always passes --continue. " + "'none' starts completely fresh every time."),
|
|
11243
11243
|
resume_max_bytes: exports_external.number().int().positive().optional().describe("Byte threshold above which 'auto' mode falls back to handoff " + "instead of --continue. Default 2_000_000 (~2MB). Large transcripts " + "can blow out the context window even with prefix caching, and " + "--continue replay is known-fragile at scale."),
|
|
11244
|
+
briefing: exports_external.enum(["gateway", "legacy"]).optional().describe("Which mechanism assembles the fresh-session reorientation briefing " + "(default 'legacy'). 'legacy' keeps today's behaviour: the Stop-hook " + ".handoff.md and/or bin/handoff-briefing.sh, injected via " + "--append-system-prompt. 'gateway' moves it to a gateway boot-time " + "builder sourced from the durable history.db (crash-independent, " + "surface-scoped, token-budgeted) and injects it as a synthetic " + '<channel source="boot_briefing"> inbound over the durable spool — ' + "keeping the system-prompt prefix stable for cross-session prompt " + "caching. Suppressed automatically when resume_mode is " + "'continue'/'auto' (the transcript may be replayed) and on a /reset " + "force-fresh boot. Threaded to the gateway as " + "SWITCHROOM_SESSION_BRIEFING."),
|
|
11244
11245
|
boot_resume: exports_external.enum(["always", "in-flight", "never"]).optional().describe("How the gateway auto-resumes a turn that was IN FLIGHT when the " + "agent restarted. 'in-flight' (default) resumes genuinely " + "interrupted work even after a deliberate/operator restart — a " + "sanctioned restart landing mid-turn no longer silently drops the " + "work. 'always' forces resume unconditionally (same as the " + "SWITCHROOM_BOOT_RESUME_ALWAYS=1 escape hatch). 'never' is the " + "quota-saving posture: don't auto-replay work across a clean " + "restart — but the user is STILL sent a passive notice of what was " + "in flight (silence is never used). Independent of the at-most-once " + "resume ledger and the bounded resume-chain loop-guard, which always " + "apply. Threaded to the gateway as SWITCHROOM_BOOT_RESUME."),
|
|
11245
11246
|
session_retention_max_count: exports_external.number().int().nonnegative().optional().describe("Session-JSONL retention (issue #2792): keep at most this many " + "newest session transcripts under .claude/projects; older ones " + "past both this count and the age bound are pruned by the Stop " + "hook. The newest sessions (and the handoff source) are always " + "kept. Default 20; set 0 to disable the count bound."),
|
|
11246
11247
|
session_retention_max_age_days: exports_external.number().int().nonnegative().optional().describe("Session-JSONL retention (issue #2792): prune session transcripts " + "older than this many days (a file is deleted only when it is BOTH " + "over the count bound and older than this). Default 30; set 0 to " + "disable the age bound.")
|
|
@@ -11373,8 +11374,24 @@ var init_schema = __esm(() => {
|
|
|
11373
11374
|
}
|
|
11374
11375
|
return tg;
|
|
11375
11376
|
});
|
|
11377
|
+
BuzzChannelSchema = exports_external.object({
|
|
11378
|
+
enabled: exports_external.boolean().default(false).describe("Master switch for the per-agent Buzz sidecar. Default false — the " + "channel ships dark; start.sh forks the sidecar only when true."),
|
|
11379
|
+
relay_url: exports_external.string().regex(/^wss?:\/\//, "relay_url must be a ws:// or wss:// URL").describe("CANONICAL WebSocket URL of the closed Buzz relay — the exact string " + "the relay expects in the NIP-42 `relay` auth tag (e.g. " + "'ws://127.0.0.1:3000'). A live probe proved the relay validates this " + "tag as an exact string match against its own URL BEFORE the " + "membership check, so it is the relay's advertised identity, NOT " + "necessarily the address the sidecar dials. Set relay_dial_url when " + "the reachable address differs (a docker-network IP)."),
|
|
11380
|
+
relay_dial_url: exports_external.string().regex(/^wss?:\/\//, "relay_dial_url must be a ws:// or wss:// URL").optional().describe("Reachable ws:// / wss:// address the sidecar DIALS when it differs " + "from the canonical relay_url (e.g. a docker-network IP the relay's " + "own 127.0.0.1 can't stand in for). The NIP-42 auth tag still uses " + "relay_url. Defaults to relay_url when unset."),
|
|
11381
|
+
relay_host: exports_external.string().regex(/^(\[[0-9a-fA-F:]+\]|[^\s/?#:@]+)(:\d+)?$/, "relay_host must be a bare host[:port] authority — no scheme, path, or userinfo (e.g. '127.0.0.1:3000')").describe("REQUIRED HTTP Host header authority sent verbatim on the WS upgrade " + "(e.g. '127.0.0.1:3000', port included). The relay resolves its " + "community from this header before the upgrade and returns HTTP 404 if " + "it is missing/wrong, so it must match the relay's configured " + "authority and is deployment config, never derived from the dial URL."),
|
|
11382
|
+
nsec_vault_key: exports_external.string().default("buzz/{agent}-nsec").describe("Vault KEY NAME for the agent's Nostr secret key. Broker-fetched " + "in-process at sidecar boot; NEVER resolved into env or logged. " + "'{agent}' is substituted with the agent name."),
|
|
11383
|
+
operator_pubkey: exports_external.string().regex(/^(npub1[02-9ac-hj-np-z]{58}|[0-9a-f]{64})$/, "operator_pubkey must be a bech32 npub or 64-char hex pubkey").describe("The operator's Nostr pubkey (npub or hex). Always in the effective " + "inbound allowlist — the fail-closed default is operator-only."),
|
|
11384
|
+
authorized_pubkeys: exports_external.array(exports_external.string()).default([]).describe("Additional pubkeys (npub or hex) whose signed events may become " + "turns. Effective allowlist = this ∪ {operator_pubkey}. Empty by " + "default (operator-only)."),
|
|
11385
|
+
mirror: exports_external.enum(["both", "origin", "off"]).default("both").describe("Cross-surface mirror mode. 'both' answers on the origin channel AND " + "mirrors a copy to the other; 'off' is a true kill-switch that disables " + "the channel in BOTH directions (the inbound sidecar exits idle). " + "Phase 2b (S2): 'origin' is DEFERRED — the hub's mirror hook lives only " + "in sendReply, so 'origin' cannot be honored soundly; a configured " + "'origin' is degraded to 'off' (dark) at runtime by both the sidecar " + "config loader and the hub (channel-route.ts parseConfiguredMirrorMode). " + "Only 'both' and 'off' ship live in 2b."),
|
|
11386
|
+
chat_id: exports_external.string().min(1, "chat_id must be a non-empty Telegram chat id").describe("Telegram chat id an injected Buzz turn is routed to. Phase 1 is " + "inbound-only, so the agent's reply lands here on Telegram (the " + "authoritative surface); in later phases this is the chat the Buzz " + "turn's Telegram copy maps to. Required — the sidecar refuses to run " + "live without it (BUZZ_CHAT_ID)."),
|
|
11387
|
+
default_channel_id: exports_external.string().describe("Relay-minted group UUID (the NIP-29 `h` tag) the sidecar subscribes " + "to and stamps on injected turns."),
|
|
11388
|
+
channel_map: exports_external.record(exports_external.string(), exports_external.string()).default({}).describe("Optional map of extra group UUIDs → friendly labels."),
|
|
11389
|
+
pubkey_names: exports_external.record(exports_external.string(), exports_external.string()).default({}).describe("Optional petnames: hex/npub pubkey → display name, used to label " + "the sender on injected turns."),
|
|
11390
|
+
pinned_relay_digest: exports_external.string().optional().describe("Pinned relay image digest (M4). RESERVED — no consumer of this field " + "exists; the existing compat-check (compat-check.ts) validates only " + "the wire contract (AUTH kind, message kind, tag names) and does not " + "read this field. Kept in the schema so the intended digest-pin can " + "be wired without a config shape change.")
|
|
11391
|
+
}).strict();
|
|
11376
11392
|
ChannelsSchema = exports_external.object({
|
|
11377
|
-
telegram: TelegramChannelSchema
|
|
11393
|
+
telegram: TelegramChannelSchema,
|
|
11394
|
+
buzz: BuzzChannelSchema.optional()
|
|
11378
11395
|
}).optional();
|
|
11379
11396
|
TIMEZONE_REGEX = /^UTC$|^[A-Z][A-Za-z0-9_+-]+(\/[A-Z][A-Za-z0-9_+-]+){1,2}$/;
|
|
11380
11397
|
ApproverIdSchema = exports_external.union([exports_external.number(), exports_external.string().regex(/^\d+$/)]);
|