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
|
@@ -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+$/)]);
|
|
@@ -21415,7 +21432,7 @@ function allocateAgentUid(name) {
|
|
|
21415
21432
|
}
|
|
21416
21433
|
|
|
21417
21434
|
// src/build-info.ts
|
|
21418
|
-
var VERSION = "0.
|
|
21435
|
+
var VERSION = "0.20.1";
|
|
21419
21436
|
|
|
21420
21437
|
// src/setup/hindsight-recall-tunables.ts
|
|
21421
21438
|
var RECALL_DEADLINE_HEADROOM_SECONDS = 2;
|
|
@@ -28355,6 +28372,20 @@ function resolveOperatorUid(ownershipDeps = {}) {
|
|
|
28355
28372
|
// src/cli/rollout.ts
|
|
28356
28373
|
init_atomic();
|
|
28357
28374
|
|
|
28375
|
+
// src/config/hostd-template-version.ts
|
|
28376
|
+
var HOSTD_TEMPLATE_LAST_CHANGED = "v0.19.43";
|
|
28377
|
+
function hostdTemplateRegenVerdict(target, fromVersion) {
|
|
28378
|
+
const cmpTarget = compareReleaseTags(HOSTD_TEMPLATE_LAST_CHANGED, target);
|
|
28379
|
+
if (cmpTarget === null)
|
|
28380
|
+
return "unknown";
|
|
28381
|
+
const cmpFrom = compareReleaseTags(HOSTD_TEMPLATE_LAST_CHANGED, fromVersion);
|
|
28382
|
+
if (cmpFrom === null)
|
|
28383
|
+
return "unknown";
|
|
28384
|
+
const targetHas = cmpTarget <= 0;
|
|
28385
|
+
const fromHas = cmpFrom <= 0;
|
|
28386
|
+
return targetHas === fromHas ? "not-needed" : "required";
|
|
28387
|
+
}
|
|
28388
|
+
|
|
28358
28389
|
// src/cli/resolve-version.ts
|
|
28359
28390
|
import { existsSync as existsSync14, readFileSync as readFileSync10 } from "node:fs";
|
|
28360
28391
|
import { dirname as dirname7, join as join9 } from "node:path";
|
|
@@ -29716,12 +29747,31 @@ function etaLine(s) {
|
|
|
29716
29747
|
const meanMs = done.reduce((t, a) => t + (a.durationMs ?? 0), 0) / done.length;
|
|
29717
29748
|
return `~${formatDurationMs(meanMs * remaining)} left (rough est.)`;
|
|
29718
29749
|
}
|
|
29719
|
-
function deferredLines(target) {
|
|
29750
|
+
function deferredLines(target, fromVersion) {
|
|
29720
29751
|
const bare = target.trim().replace(/^v/, "");
|
|
29721
|
-
|
|
29752
|
+
const head = [
|
|
29722
29753
|
`**Verified on ${target}** — every component this roll owned passed \`verify-components\`, so this is host convergence, not just the agents. Anything the roll was told to skip is named in its warnings.`,
|
|
29723
|
-
`**Still host-side (nothing in a roll can do these)
|
|
29724
|
-
|
|
29754
|
+
`**Still host-side (nothing in a roll can do these):**`
|
|
29755
|
+
];
|
|
29756
|
+
const verdict = hostdTemplateRegenVerdict(target, fromVersion);
|
|
29757
|
+
if (verdict === "required") {
|
|
29758
|
+
return [
|
|
29759
|
+
...head,
|
|
29760
|
+
`- host operator CLI + hostd template regen — regen is **REQUIRED** for this roll (hostd mounts/env changed in ${HOSTD_TEMPLATE_LAST_CHANGED}). One copy-paste:`,
|
|
29761
|
+
` \`sudo npm i -g switchroom@${bare} && switchroom hostd install --tag ${target}\``
|
|
29762
|
+
];
|
|
29763
|
+
}
|
|
29764
|
+
const cliLine = `- host operator CLI — \`sudo npm i -g switchroom@${bare}\``;
|
|
29765
|
+
if (verdict === "not-needed") {
|
|
29766
|
+
return [
|
|
29767
|
+
...head,
|
|
29768
|
+
cliLine,
|
|
29769
|
+
`- hostd template regen: **not needed** for this release — hostd mounts/env unchanged since ${HOSTD_TEMPLATE_LAST_CHANGED}.`
|
|
29770
|
+
];
|
|
29771
|
+
}
|
|
29772
|
+
return [
|
|
29773
|
+
...head,
|
|
29774
|
+
cliLine,
|
|
29725
29775
|
`- hostd template regen (only if the release changed hostd mounts/env) — \`switchroom hostd install --tag ${target}\``
|
|
29726
29776
|
];
|
|
29727
29777
|
}
|
|
@@ -29765,7 +29815,7 @@ function renderWith(s, compact) {
|
|
|
29765
29815
|
if (warnings.length > 0)
|
|
29766
29816
|
parts2.push("", ...warnings);
|
|
29767
29817
|
if (s.deferred !== false)
|
|
29768
|
-
parts2.push("", ...deferredLines(s.target));
|
|
29818
|
+
parts2.push("", ...deferredLines(s.target, s.fromVersion));
|
|
29769
29819
|
return parts2.join(`
|
|
29770
29820
|
`);
|
|
29771
29821
|
}
|
|
@@ -32834,7 +32884,7 @@ class LogTailRolloutNarrator {
|
|
|
32834
32884
|
case "hindsight-refresh":
|
|
32835
32885
|
case "hindsight-refresh-done":
|
|
32836
32886
|
case "hindsight-skipped":
|
|
32837
|
-
return
|
|
32887
|
+
return 1;
|
|
32838
32888
|
case "hostd-web-deferred":
|
|
32839
32889
|
return Number.MAX_SAFE_INTEGER - 1;
|
|
32840
32890
|
default:
|
|
@@ -4434,7 +4434,7 @@ var init_observation_scopes = __esm(() => {
|
|
|
4434
4434
|
});
|
|
4435
4435
|
|
|
4436
4436
|
// src/config/schema.ts
|
|
4437
|
-
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;
|
|
4437
|
+
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;
|
|
4438
4438
|
var init_schema = __esm(() => {
|
|
4439
4439
|
init_zod();
|
|
4440
4440
|
init_observation_scopes();
|
|
@@ -4681,6 +4681,7 @@ var init_schema = __esm(() => {
|
|
|
4681
4681
|
max_turns_in_briefing: exports_external.number().int().positive().optional().describe("Cap on recent user/assistant turn pairs fed to the summarizer."),
|
|
4682
4682
|
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."),
|
|
4683
4683
|
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."),
|
|
4684
|
+
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."),
|
|
4684
4685
|
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."),
|
|
4685
4686
|
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."),
|
|
4686
4687
|
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.")
|
|
@@ -4813,8 +4814,24 @@ var init_schema = __esm(() => {
|
|
|
4813
4814
|
}
|
|
4814
4815
|
return tg;
|
|
4815
4816
|
});
|
|
4817
|
+
BuzzChannelSchema = exports_external.object({
|
|
4818
|
+
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."),
|
|
4819
|
+
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)."),
|
|
4820
|
+
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."),
|
|
4821
|
+
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."),
|
|
4822
|
+
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."),
|
|
4823
|
+
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."),
|
|
4824
|
+
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)."),
|
|
4825
|
+
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."),
|
|
4826
|
+
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)."),
|
|
4827
|
+
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."),
|
|
4828
|
+
channel_map: exports_external.record(exports_external.string(), exports_external.string()).default({}).describe("Optional map of extra group UUIDs → friendly labels."),
|
|
4829
|
+
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."),
|
|
4830
|
+
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.")
|
|
4831
|
+
}).strict();
|
|
4816
4832
|
ChannelsSchema = exports_external.object({
|
|
4817
|
-
telegram: TelegramChannelSchema
|
|
4833
|
+
telegram: TelegramChannelSchema,
|
|
4834
|
+
buzz: BuzzChannelSchema.optional()
|
|
4818
4835
|
}).optional();
|
|
4819
4836
|
TIMEZONE_REGEX = /^UTC$|^[A-Z][A-Za-z0-9_+-]+(\/[A-Z][A-Za-z0-9_+-]+){1,2}$/;
|
|
4820
4837
|
ApproverIdSchema = exports_external.union([exports_external.number(), exports_external.string().regex(/^\d+$/)]);
|
|
@@ -4030,7 +4030,7 @@ var init_observation_scopes = __esm(() => {
|
|
|
4030
4030
|
});
|
|
4031
4031
|
|
|
4032
4032
|
// src/config/schema.ts
|
|
4033
|
-
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;
|
|
4033
|
+
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;
|
|
4034
4034
|
var init_schema = __esm(() => {
|
|
4035
4035
|
init_zod();
|
|
4036
4036
|
init_observation_scopes();
|
|
@@ -4277,6 +4277,7 @@ var init_schema = __esm(() => {
|
|
|
4277
4277
|
max_turns_in_briefing: exports_external.number().int().positive().optional().describe("Cap on recent user/assistant turn pairs fed to the summarizer."),
|
|
4278
4278
|
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."),
|
|
4279
4279
|
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."),
|
|
4280
|
+
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."),
|
|
4280
4281
|
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."),
|
|
4281
4282
|
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."),
|
|
4282
4283
|
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.")
|
|
@@ -4409,8 +4410,24 @@ var init_schema = __esm(() => {
|
|
|
4409
4410
|
}
|
|
4410
4411
|
return tg;
|
|
4411
4412
|
});
|
|
4413
|
+
BuzzChannelSchema = exports_external.object({
|
|
4414
|
+
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."),
|
|
4415
|
+
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)."),
|
|
4416
|
+
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."),
|
|
4417
|
+
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."),
|
|
4418
|
+
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."),
|
|
4419
|
+
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."),
|
|
4420
|
+
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)."),
|
|
4421
|
+
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."),
|
|
4422
|
+
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)."),
|
|
4423
|
+
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."),
|
|
4424
|
+
channel_map: exports_external.record(exports_external.string(), exports_external.string()).default({}).describe("Optional map of extra group UUIDs → friendly labels."),
|
|
4425
|
+
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."),
|
|
4426
|
+
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.")
|
|
4427
|
+
}).strict();
|
|
4412
4428
|
ChannelsSchema = exports_external.object({
|
|
4413
|
-
telegram: TelegramChannelSchema
|
|
4429
|
+
telegram: TelegramChannelSchema,
|
|
4430
|
+
buzz: BuzzChannelSchema.optional()
|
|
4414
4431
|
}).optional();
|
|
4415
4432
|
TIMEZONE_REGEX = /^UTC$|^[A-Z][A-Za-z0-9_+-]+(\/[A-Z][A-Za-z0-9_+-]+){1,2}$/;
|
|
4416
4433
|
ApproverIdSchema = exports_external.union([exports_external.number(), exports_external.string().regex(/^\d+$/)]);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchroom",
|
|
3
3
|
"//version": "NOT the release version — source of truth is the git tag, resolved by scripts/build.mjs:resolveVersion() (see CLAUDE.md > Standard release process). This field is stale by design and only the Layer-4 dev/non-tag fallback for build.mjs + src/cli/resolve-version.ts; do NOT bump it expecting a release to pick it up. npm-pack tarball naming needs a real version — do that as an UNCOMMITTED pack-time bump (see release step 6), never a committed one.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.20.1",
|
|
5
5
|
"description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
"build": "node scripts/build.mjs",
|
|
25
25
|
"build:cli": "node scripts/build.mjs && bun build --compile --target=bun-linux-x64 --minify bin/switchroom.ts --outfile switchroom-linux-amd64",
|
|
26
26
|
"pretest": "npm run build",
|
|
27
|
-
"test": "vitest run && bun test telegram-plugin/tests/agent-state-dir-preload.test.ts telegram-plugin/tests/catch-all-forwarded-history.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/ipc-server-check-pre-approved.test.ts telegram-plugin/tests/rollout-narration-edit-socket.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/subagent-nested-dispatch.test.ts telegram-plugin/tests/nested-worker-visibility-harness.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/registry/api-registry.test.ts telegram-plugin/registry/turns-schema.test.ts telegram-plugin/tests/idle-footer-wiring.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/status-query-telemetry.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
|
|
27
|
+
"test": "vitest run && bun test telegram-plugin/tests/agent-state-dir-preload.test.ts telegram-plugin/tests/catch-all-forwarded-history.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/boot-briefing-builder.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/ipc-server-buzz-dedup.test.ts telegram-plugin/tests/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/ipc-server-check-pre-approved.test.ts telegram-plugin/tests/rollout-narration-edit-socket.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/subagent-nested-dispatch.test.ts telegram-plugin/tests/nested-worker-visibility-harness.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/registry/api-registry.test.ts telegram-plugin/registry/turns-schema.test.ts telegram-plugin/tests/idle-footer-wiring.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/status-query-telemetry.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
|
|
28
28
|
"test:vitest": "vitest run",
|
|
29
|
-
"test:bun": "bun test telegram-plugin/tests/agent-state-dir-preload.test.ts telegram-plugin/tests/catch-all-forwarded-history.test.ts src/vault/grants.test.ts src/vault/grants-db.test.ts src/vault/write-grants.test.ts src/vault/broker/server-grants.test.ts src/vault/broker/server-write-grants.test.ts src/vault/broker/server-scope-persist.test.ts src/vault/broker/server-tokenless-scope.test.ts src/vault/broker/server-mint-grant-passphrase-attest.test.ts src/vault/broker/server-passphrase-attest.test.ts src/vault/broker/server-mint-grant-posture-attest.test.ts src/vault/broker/server-admin-only-keys.test.ts src/vault/broker/client-token.test.ts src/vault/broker/server-unlock.test.ts src/vault/broker/auto-unlock.test.ts src/vault/broker/drift-detection.test.ts tests/vault-broker-passphrase.test.ts src/cli/vault-get-broker.test.ts src/vault/resolver-via-broker.test.ts src/vault/broker/scope.test.ts src/vault/broker/server.test.ts src/litellm/provision-apply-e2e.test.ts src/drive/disconnect.test.ts src/drive/grants.test.ts src/drive/oauth.test.ts src/drive/onboarding.test.ts src/drive/reconciler.test.ts src/drive/vault-slots.test.ts src/drive/wrapper.test.ts src/vault/approvals/kernel.test.ts src/vault/approvals/approval-origin.test.ts src/vault/approvals/self-approval-bypass.test.ts src/vault/approvals/schema-idempotent.test.ts src/vault/broker/server-approvals.test.ts telegram-plugin/tests/boot-probes.test.ts telegram-plugin/tests/boot-version-string.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/ipc-server-check-pre-approved.test.ts telegram-plugin/tests/rollout-narration-edit-socket.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/subagent-nested-dispatch.test.ts telegram-plugin/tests/nested-worker-visibility-harness.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/status-query-telemetry.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts telegram-plugin/uat/load-env.test.ts telegram-plugin/uat/feed-matcher.test.ts telegram-plugin/uat/uat-driver.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
|
|
29
|
+
"test:bun": "bun test telegram-plugin/tests/agent-state-dir-preload.test.ts telegram-plugin/tests/catch-all-forwarded-history.test.ts src/vault/grants.test.ts src/vault/grants-db.test.ts src/vault/write-grants.test.ts src/vault/broker/server-grants.test.ts src/vault/broker/server-write-grants.test.ts src/vault/broker/server-scope-persist.test.ts src/vault/broker/server-tokenless-scope.test.ts src/vault/broker/server-mint-grant-passphrase-attest.test.ts src/vault/broker/server-passphrase-attest.test.ts src/vault/broker/server-mint-grant-posture-attest.test.ts src/vault/broker/server-admin-only-keys.test.ts src/vault/broker/client-token.test.ts src/vault/broker/server-unlock.test.ts src/vault/broker/auto-unlock.test.ts src/vault/broker/drift-detection.test.ts tests/vault-broker-passphrase.test.ts src/cli/vault-get-broker.test.ts src/vault/resolver-via-broker.test.ts src/vault/broker/scope.test.ts src/vault/broker/server.test.ts src/litellm/provision-apply-e2e.test.ts src/drive/disconnect.test.ts src/drive/grants.test.ts src/drive/oauth.test.ts src/drive/onboarding.test.ts src/drive/reconciler.test.ts src/drive/vault-slots.test.ts src/drive/wrapper.test.ts src/vault/approvals/kernel.test.ts src/vault/approvals/approval-origin.test.ts src/vault/approvals/self-approval-bypass.test.ts src/vault/approvals/schema-idempotent.test.ts src/vault/broker/server-approvals.test.ts telegram-plugin/tests/boot-probes.test.ts telegram-plugin/tests/boot-version-string.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/boot-briefing-builder.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/ipc-server-buzz-dedup.test.ts telegram-plugin/tests/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/ipc-server-check-pre-approved.test.ts telegram-plugin/tests/rollout-narration-edit-socket.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/subagent-nested-dispatch.test.ts telegram-plugin/tests/nested-worker-visibility-harness.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/status-query-telemetry.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts telegram-plugin/uat/load-env.test.ts telegram-plugin/uat/feed-matcher.test.ts telegram-plugin/uat/uat-driver.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
|
|
30
30
|
"test:watch": "vitest",
|
|
31
|
-
"lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-test-runner-coverage.mjs && node scripts/check-bun-module-mock-scope.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-auth-test-hermeticity.mjs && node scripts/check-agent-state-dir-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs && node scripts/check-mcp-instructions-budget.mjs && node scripts/check-web-subscription-honest.mjs && node scripts/check-no-unpinned-npx-playwright.mjs && node scripts/check-gateway-line-ratchet.mjs && node scripts/check-retry-flood-hooks.mjs && node scripts/check-callback-ctx-wrapping.mjs && node scripts/check-status-pin-single-path.mjs && node scripts/check-litellm-config-guard.mjs && node scripts/check-release-asset-names.mjs && node scripts/check-agent-attribution-trailers.mjs && node scripts/check-hindsight-write-redaction.mjs && bun scripts/check-secret-pattern-parity.ts",
|
|
31
|
+
"lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-test-runner-coverage.mjs && node scripts/check-bun-module-mock-scope.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-auth-test-hermeticity.mjs && node scripts/check-agent-state-dir-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs && node scripts/check-mcp-instructions-budget.mjs && node scripts/check-web-subscription-honest.mjs && node scripts/check-no-unpinned-npx-playwright.mjs && node scripts/check-gateway-line-ratchet.mjs && node scripts/check-retry-flood-hooks.mjs && node scripts/check-callback-ctx-wrapping.mjs && node scripts/check-status-pin-single-path.mjs && node scripts/check-litellm-config-guard.mjs && node scripts/check-release-asset-names.mjs && node scripts/check-agent-attribution-trailers.mjs && node scripts/check-hindsight-write-redaction.mjs && bun scripts/check-secret-pattern-parity.ts && bun scripts/check-hostd-template-guard.ts",
|
|
32
32
|
"lint:tsc": "tsc --noEmit",
|
|
33
33
|
"lint:hindsight-write-redaction": "node scripts/check-hindsight-write-redaction.mjs",
|
|
34
34
|
"lint:secret-pattern-parity": "bun scripts/check-secret-pattern-parity.ts",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"lint:litellm-config-guard": "node scripts/check-litellm-config-guard.mjs",
|
|
51
51
|
"lint:release-asset-contract": "node scripts/check-release-asset-names.mjs",
|
|
52
52
|
"lint:agent-attribution-trailers": "node scripts/check-agent-attribution-trailers.mjs",
|
|
53
|
+
"lint:hostd-template-guard": "bun scripts/check-hostd-template-guard.ts",
|
|
53
54
|
"prepublishOnly": "npm run build && npm run lint && npm test"
|
|
54
55
|
},
|
|
55
56
|
"dependencies": {
|
|
@@ -63,6 +64,7 @@
|
|
|
63
64
|
"commander": "^13.1.0",
|
|
64
65
|
"grammy": "^1.44",
|
|
65
66
|
"handlebars": "^4.7.8",
|
|
67
|
+
"nostr-tools": "^2.24.1",
|
|
66
68
|
"posthog-node": "^5.29.2",
|
|
67
69
|
"yaml": "^2.7.0",
|
|
68
70
|
"zod": "^3.24.0"
|
|
@@ -57,6 +57,63 @@ if [ "$SWITCHROOM_RUNTIME" = "docker" ] && [ -z "$SWITCHROOM_DOCKER_TMUX_INNER"
|
|
|
57
57
|
# `claude` --continue path, not the gateway), so this needs its own hoist.
|
|
58
58
|
export SWITCHROOM_BOOT_RESUME="{{#if bootResumeMode}}{{{bootResumeMode}}}{{else}}in-flight{{/if}}"
|
|
59
59
|
|
|
60
|
+
# Boot-briefing transport flag (session_continuity.briefing; default
|
|
61
|
+
# 'legacy'). When 'gateway', the gateway daemon assembles the fresh-session
|
|
62
|
+
# reorientation briefing at boot from the durable history.db and injects it
|
|
63
|
+
# as a spooled synthetic inbound (<channel source="boot_briefing">) instead
|
|
64
|
+
# of the handoff-briefing.sh --append-system-prompt path. MUST be exported
|
|
65
|
+
# here, before the gateway fork below (same env-fork landmine as
|
|
66
|
+
# SWITCHROOM_BOOT_RESUME). The gateway also needs the resume MODE to
|
|
67
|
+
# suppress the briefing when 'continue'/'auto' may replay the transcript —
|
|
68
|
+
# hoist that too (the inner-pass SWITCHROOM_RESUME_MODE assignment below
|
|
69
|
+
# renders the same template value, so the two can never disagree).
|
|
70
|
+
export SWITCHROOM_SESSION_BRIEFING="{{#if sessionBriefingMode}}{{{sessionBriefingMode}}}{{else}}legacy{{/if}}"
|
|
71
|
+
export SWITCHROOM_RESUME_MODE="{{#if resumeMode}}{{{resumeMode}}}{{else}}handoff{{/if}}"
|
|
72
|
+
|
|
73
|
+
{{#if hindsightEnabled}}
|
|
74
|
+
# Hindsight endpoint for the gateway boot briefing (session_continuity.briefing:
|
|
75
|
+
# gateway). When assembling the fresh-session reorientation briefing the gateway
|
|
76
|
+
# daemon does its OWN Hindsight recall — mirroring bin/handoff-briefing.sh — to
|
|
77
|
+
# fold recent memory into the briefing alongside the durable Telegram history.
|
|
78
|
+
# The authoritative exports live in the inner pass (~line 893), AFTER the
|
|
79
|
+
# gateway fork below, so without this hoist the forked daemon never sees the
|
|
80
|
+
# endpoint and the Hindsight section silently no-ops for every boot (same
|
|
81
|
+
# start.sh env-fork landmine as SWITCHROOM_SESSION_BRIEFING above). Hoist both
|
|
82
|
+
# here, before the fork; idempotent with the inner-pass exports (same rendered
|
|
83
|
+
# template values). Pinned by tests/scaffold.gateway-env-order.test.ts.
|
|
84
|
+
export HINDSIGHT_API_URL={{{hindsightApiBaseUrlQ}}}
|
|
85
|
+
export HINDSIGHT_BANK_ID={{{hindsightBankIdQ}}}
|
|
86
|
+
{{/if}}
|
|
87
|
+
|
|
88
|
+
# Force-fresh suppression signal for the gateway boot briefing. The gateway
|
|
89
|
+
# suppresses the briefing when this boot is a /reset·/new fresh start —
|
|
90
|
+
# re-feeding the just-reset conversation into the fresh session would defeat
|
|
91
|
+
# the reset. It MUST NOT rely on existsSync(.force-fresh-session) at its own
|
|
92
|
+
# module-eval time: this OUTER pass forks the gateway with `&` (below) then
|
|
93
|
+
# execs the inner tmux pass, whose CONTINUE_FLAG block `rm`s the marker (see
|
|
94
|
+
# ~line 1262). The fork and the `rm` race with no ordering, and under boot
|
|
95
|
+
# CPU contention the `rm` can win before the gateway bundle evaluates its
|
|
96
|
+
# briefing check — the marker vanishes and the reset session gets re-fed the
|
|
97
|
+
# conversation it just reset. Snapshot the marker into env HERE, before the
|
|
98
|
+
# fork, so the gateway reads a value fixed at fork time and immune to the
|
|
99
|
+
# later `rm`. The inner pass still consumes the marker for the claude
|
|
100
|
+
# --continue / session-mode logic; this only mirrors it for the fork race.
|
|
101
|
+
if [ -f "{{agentDir}}/.force-fresh-session" ]; then
|
|
102
|
+
export SWITCHROOM_FORCE_FRESH=1
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
# Session-generation stamp for the gateway boot briefing (#4242). Set ONCE
|
|
106
|
+
# here, per real boot, before the gateway fork below. `_switchroom_supervise`
|
|
107
|
+
# respawns the gateway `bun` process in a `while` loop inside this same shell
|
|
108
|
+
# (it does NOT re-run start.sh), so every crash-respawn inherits this exact
|
|
109
|
+
# value, while the next real container boot re-derives a fresh one. The
|
|
110
|
+
# gateway persists it the first time it briefs and skips re-minting when the
|
|
111
|
+
# persisted stamp matches — that distinguishes "supervisor respawned me into
|
|
112
|
+
# a still-live Claude session" (skip) from "genuine new boot" (brief). The
|
|
113
|
+
# epoch second keeps it monotonic across boots; $$ (this outer pass's PID)
|
|
114
|
+
# plus ${RANDOM} guarantee uniqueness even for two boots within one second.
|
|
115
|
+
export SWITCHROOM_GATEWAY_BOOT_ID="$(date +%s)-$$-${RANDOM}"
|
|
116
|
+
|
|
60
117
|
# Gateway-consumed env MUST be exported HERE, before the gateway fork
|
|
61
118
|
# below. The gateway daemon reads channels.telegram.* knobs (and any
|
|
62
119
|
# agent env) from process.env at startup — e.g. SWITCHROOM_TG_STREAM_
|
|
@@ -408,6 +465,32 @@ x-litellm-tags: agent:$SWITCHROOM_AGENT_NAME,profile:${SWITCHROOM_AGENT_PROFILE:
|
|
|
408
465
|
|
|
409
466
|
_gateway_bundle=/opt/switchroom/telegram-plugin/dist/gateway/gateway.js
|
|
410
467
|
_telegram_enabled={{#if telegramEnabledFlag}}{{telegramEnabledFlag}}{{else}}true{{/if}}
|
|
468
|
+
|
|
469
|
+
# Version-skew handshake for the gateway boot briefing (#4245). This start.sh
|
|
470
|
+
# is fresh; the deployed /opt/switchroom gateway bundle may NOT be. When it
|
|
471
|
+
# sets briefing=gateway it also skips the legacy shell handoff-briefing
|
|
472
|
+
# assembler (inner pass, below) — so if the bundle PREDATES the boot-briefing
|
|
473
|
+
# builder, BOTH paths are dead and the agent gets a SILENT no-briefing until
|
|
474
|
+
# the image is updated. The stale bundle cannot self-report a feature it lacks,
|
|
475
|
+
# so this fresh start.sh does the handshake: grep the bundle for the capability
|
|
476
|
+
# sentinel the builder exports (GATEWAY_BOOT_BRIEFING_CAPABILITY in
|
|
477
|
+
# telegram-plugin/gateway/boot-briefing-capability.ts; the bundle is built
|
|
478
|
+
# un-minified so the literal survives verbatim). On a miss, warn LOUDLY and
|
|
479
|
+
# drop a marker so the inner pass runs the legacy handoff assembler as a
|
|
480
|
+
# fallback instead of leaving this boot with nothing. Resolve the marker afresh
|
|
481
|
+
# every boot (rm, then re-touch only on a confirmed miss) so a later image
|
|
482
|
+
# update self-heals with no leftover state.
|
|
483
|
+
_briefing_skew_marker="{{agentDir}}/.gateway-briefing-unavailable"
|
|
484
|
+
rm -f "$_briefing_skew_marker"
|
|
485
|
+
if [ "$SWITCHROOM_SESSION_BRIEFING" = "gateway" ]; then
|
|
486
|
+
if [ -f "$_gateway_bundle" ] && grep -q '{{gatewayBriefingCapability}}' "$_gateway_bundle" 2>/dev/null; then
|
|
487
|
+
: # bundle carries the boot-briefing builder — gateway path is live
|
|
488
|
+
else
|
|
489
|
+
echo "[start.sh] WARNING: session_continuity.briefing=gateway is configured, but the deployed gateway bundle ($_gateway_bundle) PREDATES the boot-briefing builder (capability sentinel '{{gatewayBriefingCapability}}' absent from the bundle). The gateway cannot assemble the reorientation briefing, and the legacy shell handoff path is skipped for 'gateway' mode — so this boot would otherwise get NO briefing at all. FALLING BACK to the legacy handoff-briefing assembler for this boot. Update the agent image (/opt/switchroom) to a build that includes the boot-briefing feature to use the gateway path." >&2
|
|
490
|
+
touch "$_briefing_skew_marker" 2>/dev/null || true
|
|
491
|
+
fi
|
|
492
|
+
fi
|
|
493
|
+
|
|
411
494
|
if [ "$_telegram_enabled" = "true" ] && [ -f "$_gateway_bundle" ] && command -v bun >/dev/null 2>&1; then
|
|
412
495
|
_switchroom_supervise gateway /var/log/switchroom/gateway-supervisor.log \
|
|
413
496
|
bun "$_gateway_bundle" &
|
|
@@ -455,6 +538,33 @@ x-litellm-tags: agent:$SWITCHROOM_AGENT_NAME,profile:${SWITCHROOM_AGENT_PROFILE:
|
|
|
455
538
|
bun /opt/switchroom/agent-scheduler/index.js &
|
|
456
539
|
fi
|
|
457
540
|
|
|
541
|
+
# 3b) Buzz co-channel inbound sidecar (Phase 1, channels.buzz).
|
|
542
|
+
# A supervised sibling that opens a WebSocket Nostr subscription to a
|
|
543
|
+
# closed Buzz relay, NIP-42-authenticates, and injects allowlisted
|
|
544
|
+
# messages onto the gateway socket as synthesized turns
|
|
545
|
+
# (meta.source="buzz") — exactly like the cron sidecar above. INBOUND
|
|
546
|
+
# ONLY in Phase 1; replies land on Telegram (the authoritative surface).
|
|
547
|
+
#
|
|
548
|
+
# Default OFF and deterministic: the block is present for every agent
|
|
549
|
+
# (start.sh stays byte-identical fleet-wide) but forks the sidecar ONLY
|
|
550
|
+
# when BUZZ_ENABLED=1 is set in the container env. That var is projected
|
|
551
|
+
# by src/agents/compose.ts from the cascade-resolved channels.buzz block,
|
|
552
|
+
# and BUZZ_ENABLED=1 lands ONLY when that block sets enabled:true. An
|
|
553
|
+
# enabled:false/absent block leaves it unset, so for those agents this
|
|
554
|
+
# block is a no-op and the channel is dark by construction. The sidecar
|
|
555
|
+
# itself also fails closed if its env is incomplete, and fetches the nsec
|
|
556
|
+
# in-process from the vault broker (never an env var, never logged).
|
|
557
|
+
if [ "$BUZZ_ENABLED" = "1" ] \
|
|
558
|
+
&& [ -f /opt/switchroom/buzz-gateway/index.js ] \
|
|
559
|
+
&& command -v bun >/dev/null 2>&1; then
|
|
560
|
+
# --oneshot-ok: the sidecar exits 0 when the channel resolves not-live
|
|
561
|
+
# (e.g. mirror:off, or a not-live env). A clean exit 0 = "nothing to do",
|
|
562
|
+
# so DON'T respawn it every 60s (that is the documented churn pattern
|
|
563
|
+
# above). A non-zero crash while live still respawns.
|
|
564
|
+
_switchroom_supervise buzz-gateway /var/log/switchroom/buzz-gateway.log --oneshot-ok \
|
|
565
|
+
bun /opt/switchroom/buzz-gateway/index.js &
|
|
566
|
+
fi
|
|
567
|
+
|
|
458
568
|
{{#if cronSessionEnabled}}
|
|
459
569
|
# 4) cheap cron SESSION (Tier 1, reference/rfcs/cheap-cron-sessions.md §2.2).
|
|
460
570
|
# A SECOND interactive claude (no -p) dedicated to context:fresh cron
|
|
@@ -1353,9 +1463,45 @@ if [ "$_HANDOFF_STALE" = "1" ]; then
|
|
|
1353
1463
|
# daily memory into a compact context brief. Requires python3 (stdlib
|
|
1354
1464
|
# sqlite3) for the Telegram history section; other sections only need
|
|
1355
1465
|
# curl + jq (both optional — each source degrades gracefully).
|
|
1356
|
-
|
|
1466
|
+
#
|
|
1467
|
+
# SKIPPED when session_continuity.briefing='gateway': the gateway daemon
|
|
1468
|
+
# then owns the reorientation briefing (built from the durable history.db
|
|
1469
|
+
# and injected as a spooled <channel source="boot_briefing"> inbound), so
|
|
1470
|
+
# running the shell assembler here would double-inject the same recent
|
|
1471
|
+
# conversation. The Stop-hook .handoff.md path below is left untouched in
|
|
1472
|
+
# this stage of the migration.
|
|
1473
|
+
#
|
|
1474
|
+
# EXCEPTION (#4245): when the outer pass detected a bundle version skew
|
|
1475
|
+
# (briefing=gateway requested but the deployed gateway bundle predates the
|
|
1476
|
+
# boot-briefing builder), it dropped the .gateway-briefing-unavailable marker.
|
|
1477
|
+
# In that case the gateway path is dead, so run the legacy assembler here as a
|
|
1478
|
+
# fallback even in 'gateway' mode — the double-inject risk does not apply
|
|
1479
|
+
# because the stale gateway never queues a briefing. This is a file-based
|
|
1480
|
+
# handshake on purpose: the marker survives the outer→inner tmux re-exec
|
|
1481
|
+
# deterministically (written before the exec, read after), unlike env.
|
|
1482
|
+
if [ "$SWITCHROOM_RESUME_MODE" = "handoff" ] \
|
|
1483
|
+
&& { [ "{{#if sessionBriefingMode}}{{{sessionBriefingMode}}}{{else}}legacy{{/if}}" != "gateway" ] || [ -f "{{agentDir}}/.gateway-briefing-unavailable" ]; } \
|
|
1484
|
+
&& command -v handoff-briefing.sh >/dev/null 2>&1; then
|
|
1357
1485
|
export AGENT_DIR="{{agentDir}}"
|
|
1358
|
-
|
|
1486
|
+
# Clear any prior-boot briefing BEFORE rebuilding (#continuity). The
|
|
1487
|
+
# sidecar rm below (post-injection) is skipped if a boot crashes between
|
|
1488
|
+
# the rebuild and injection, leaving last boot's .handoff-briefing.md on
|
|
1489
|
+
# disk. If this boot's rebuild is then killed by the outer `timeout`
|
|
1490
|
+
# (SIGTERM — `timeout 10` sends SIGTERM by default, not SIGKILL) before
|
|
1491
|
+
# handoff-briefing.sh's atomic write, the injection would re-serve that
|
|
1492
|
+
# STALE prior-boot briefing. Deleting the target up front makes a killed
|
|
1493
|
+
# rebuild yield a MISSING file (injection skips) rather than a stale one.
|
|
1494
|
+
# handoff-briefing.sh recreates it via tmp+mv on success.
|
|
1495
|
+
rm -f "$HANDOFF_BRIEFING_FILE" 2>/dev/null || true
|
|
1496
|
+
# Outer kill budget (10s) must strictly exceed the inner Hindsight cap
|
|
1497
|
+
# (3s, HANDOFF_BRIEFING_HINDSIGHT_TIMEOUT) with margin, so the SIGTERM
|
|
1498
|
+
# (`timeout 10`'s default signal) lands only on a genuinely wedged run —
|
|
1499
|
+
# never mid-write of a recall that was about to return. The local SQLite
|
|
1500
|
+
# (Telegram) and daily-memory reads are sub-second; Hindsight is the only
|
|
1501
|
+
# network hop and is self-capped, so 10s leaves ~7s of headroom over the
|
|
1502
|
+
# worst-case source.
|
|
1503
|
+
export HANDOFF_BRIEFING_HINDSIGHT_TIMEOUT="${HANDOFF_BRIEFING_HINDSIGHT_TIMEOUT:-3}"
|
|
1504
|
+
timeout 10 handoff-briefing.sh 2>/dev/null || true
|
|
1359
1505
|
fi
|
|
1360
1506
|
fi
|
|
1361
1507
|
APPEND_PROMPT={{#if systemPromptAppendShellQuoted}}{{{systemPromptAppendShellQuoted}}}{{else}}""{{/if}}
|
|
@@ -50,7 +50,7 @@ Creating, updating, or refreshing a mental model is **operator-approved** — do
|
|
|
50
50
|
### Directives (replaces feedback rules)
|
|
51
51
|
Hard rules the agent must follow during reflect — guardrails that are always applied.
|
|
52
52
|
|
|
53
|
-
- `mcp__hindsight__create_directive(text)` — e.g., `create_directive("Always prefer TypeScript over JavaScript for this user's projects")`. When the user gives you a correction or "always do X" rule, create a directive instead of writing a feedback `.md` file.
|
|
53
|
+
- `mcp__hindsight__create_directive(text)` — e.g., `create_directive("Always prefer TypeScript over JavaScript for this user's projects")`. When the user gives you a correction or "always do X" rule, create a directive instead of writing a feedback `.md` file. If the rule can be enforced deterministically — a settings.json hook, a permission rule, a skill/script edit, or a config change — prefer that (instead of, or in addition to, the directive) and say which you did; reserve directives for judgment rules code can't enforce.
|
|
54
54
|
|
|
55
55
|
(Read-only inspection tools like `list_memories`, `list_mental_models`, `get_mental_model`, `list_directives` are available under the `mcp__hindsight__*` namespace if you ever need them, but you rarely should — Hindsight's own auto-recall surfaces what matters and the operator handles bank curation out-of-band. The mental-model WRITE tools — `create`/`update`/`refresh`/`delete_mental_model` — are gated: propose via `mcp__switchroom-telegram__mental_model_propose`, don't call them directly.)
|
|
56
56
|
|
|
@@ -18,6 +18,32 @@ judgement criteria. This skill carries the parts that are *this fleet's
|
|
|
18
18
|
specific opinion* — the ones you would get wrong by defaulting to generic
|
|
19
19
|
good practice, because our answer differs from the obvious one.
|
|
20
20
|
|
|
21
|
+
## 0. The design contract binds first (repos that declare one)
|
|
22
|
+
|
|
23
|
+
Switchroom declares its design contract in `reference/` (`reference/README.md`
|
|
24
|
+
is the map; the repo CLAUDE.md "Design contract" section binds it to every
|
|
25
|
+
PR). When the repo you are changing carries one, the whole protocol below
|
|
26
|
+
runs *inside* that contract:
|
|
27
|
+
|
|
28
|
+
- **Before non-trivial work, place the change in the contract:** which of the
|
|
29
|
+
four outcomes it advances, and which job spec it satisfies —
|
|
30
|
+
`reference/product-spec.md` owns the job index; survey specs cheaply with
|
|
31
|
+
`head -7 reference/jobs/*.md`. A change that maps to no outcome and no job
|
|
32
|
+
is a scoping question, not a coding task.
|
|
33
|
+
- **The ship gate is the verdict rule, not just CI green:** a change ships
|
|
34
|
+
only when it (a) advances one of the four outcomes, (b) satisfies its job
|
|
35
|
+
spec — proven by that job's outcome UAT, (c) passes all three principle
|
|
36
|
+
checks in `reference/principles.md` (docs / defaults / consistency; a "no"
|
|
37
|
+
is a redesign, not a ship-and-patch), and (d) crosses no invariant in
|
|
38
|
+
`reference/invariants.md`. Cite the job spec in the PR body.
|
|
39
|
+
- **Design lives in `reference/`, not `docs/`.** A design decision worth
|
|
40
|
+
recording is an RFC or design record in `reference/rfcs/` (`serves:` a job
|
|
41
|
+
or `backs:` an invariant), never a new doc under `docs/`.
|
|
42
|
+
|
|
43
|
+
This does not add ceremony to small changes: §1's fast path stands, and on a
|
|
44
|
+
single-concern change the verdict check is a one-line sanity pass, not a
|
|
45
|
+
report.
|
|
46
|
+
|
|
21
47
|
## 1. Is this a "larger" task? (decides whether you design-align first)
|
|
22
48
|
|
|
23
49
|
Treat it as larger — design report before implementing — when ANY of these hold:
|
|
@@ -37,7 +63,10 @@ Design-aligning a one-liner is its own failure mode.
|
|
|
37
63
|
|
|
38
64
|
The report states what exists today **with citations**, what will change, the
|
|
39
65
|
chosen approach, the alternatives you rejected and why, and the PR staging
|
|
40
|
-
plan.
|
|
66
|
+
plan. In a contract-carrying repo (§0) it opens with the verdict-rule
|
|
67
|
+
mapping: the outcome advanced, the job spec satisfied, and any principle
|
|
68
|
+
check or invariant the design brushes against. Get alignment before
|
|
69
|
+
implementing.
|
|
41
70
|
|
|
42
71
|
Then red-team your own plan item by item. Each item gets a verdict —
|
|
43
72
|
`SOUND`, `RISK`, or `WRONG` — backed by evidence you can point at (a file you
|
|
@@ -59,3 +59,8 @@ Switchroom is a multi-agent orchestrator built on Claude Code. It manages multip
|
|
|
59
59
|
- [cascade.md](cascade.md) — three-layer config cascade semantics
|
|
60
60
|
- [sub-agents.md](sub-agents.md) — delegation patterns and model routing
|
|
61
61
|
- [telegram.md](telegram.md) — enhanced Telegram plugin features
|
|
62
|
+
|
|
63
|
+
**Why it's built this way** lives in the repo's `reference/` directory — the
|
|
64
|
+
design contract (`reference/README.md` is the map: vision, principles,
|
|
65
|
+
invariants, product spec, job specs, RFCs). For "why does switchroom do X"
|
|
66
|
+
questions, that is the source of truth; `docs/` is usage/operation only.
|
|
@@ -290,7 +290,7 @@ config-plus-recreate operation with three known traps. **Follow the runbook:
|
|
|
290
290
|
|
|
291
291
|
## Telegram plugin reference — "what MCP tools", "how does reply work"
|
|
292
292
|
|
|
293
|
-
The `switchroom-telegram` plugin is an enhanced fork of the official Telegram MCP plugin and is the default for all switchroom agents. It exposes **
|
|
293
|
+
The `switchroom-telegram` plugin is an enhanced fork of the official Telegram MCP plugin and is the default for all switchroom agents. It exposes **20+ MCP tools** (all prefixed `mcp__switchroom-telegram__`; the authoritative list is the tool declarations in `telegram-plugin/bridge/bridge.ts`). The core messaging set:
|
|
294
294
|
|
|
295
295
|
| Tool | Purpose |
|
|
296
296
|
|---|---|
|