switchroom 0.18.29 → 0.18.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/bin/handoff-briefing.sh +8 -2
  2. package/dist/agent-scheduler/index.js +111 -7
  3. package/dist/auth-broker/index.js +154 -16
  4. package/dist/cli/autoaccept-poll.js +8 -3
  5. package/dist/cli/drive-write-pretool.mjs +8 -3
  6. package/dist/cli/ms-365-write-pretool.mjs +158 -11
  7. package/dist/cli/notion-write-pretool.mjs +103 -4
  8. package/dist/cli/switchroom.js +2089 -1587
  9. package/dist/host-control/main.js +110 -13
  10. package/dist/vault/approvals/kernel-server.js +116 -13
  11. package/dist/vault/broker/server.js +314 -145
  12. package/package.json +3 -3
  13. package/profiles/_base/start.sh.hbs +172 -22
  14. package/telegram-plugin/dist/bridge/bridge.js +71 -47
  15. package/telegram-plugin/dist/gateway/gateway.js +601 -104
  16. package/telegram-plugin/dist/server.js +89 -64
  17. package/telegram-plugin/gateway/gateway.ts +280 -33
  18. package/telegram-plugin/gateway/model-command.ts +104 -0
  19. package/telegram-plugin/gateway/session-model-file.ts +40 -0
  20. package/telegram-plugin/gateway/turn-flush-suppression.ts +82 -0
  21. package/telegram-plugin/gateway/unhandled-message.ts +177 -0
  22. package/telegram-plugin/llm-error-present.ts +24 -0
  23. package/telegram-plugin/model-unavailable.ts +55 -0
  24. package/telegram-plugin/operator-events.ts +113 -0
  25. package/telegram-plugin/pending-user-notice.ts +88 -0
  26. package/telegram-plugin/shared/local-time.ts +43 -0
  27. package/telegram-plugin/tests/catch-all-forwarded-history.test.ts +103 -0
  28. package/telegram-plugin/tests/catch-all-unhandled-message.test.ts +264 -0
  29. package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +26 -2
  30. package/telegram-plugin/tests/litellm-proxy-auth-misconfig.test.ts +278 -0
  31. package/telegram-plugin/tests/local-time.test.ts +68 -1
  32. package/telegram-plugin/tests/model-command.test.ts +133 -0
  33. package/telegram-plugin/tests/session-model-file.test.ts +23 -0
  34. package/telegram-plugin/tests/turn-flush-suppression.test.ts +90 -0
  35. package/vendor/hindsight-memory/scripts/backfill_transcripts.py +399 -2
  36. package/vendor/hindsight-memory/scripts/lib/client.py +47 -0
  37. package/vendor/hindsight-memory/scripts/lib/content.py +53 -1
  38. package/vendor/hindsight-memory/scripts/lib/turnlog.py +450 -0
  39. package/vendor/hindsight-memory/scripts/tests/test_backfill_from_logs.py +467 -0
  40. package/vendor/hindsight-memory/tests/test_content.py +35 -0
@@ -20036,7 +20036,8 @@ var init_protocol2 = __esm(() => {
20036
20036
  v: exports_external.literal(PROTOCOL_VERSION),
20037
20037
  op: exports_external.literal("get-credentials"),
20038
20038
  id: exports_external.string().min(1),
20039
- provider: ProviderNameSchema.optional()
20039
+ provider: ProviderNameSchema.optional(),
20040
+ account: exports_external.string().min(1).optional()
20040
20041
  });
20041
20042
  ListStateRequestSchema = exports_external.object({
20042
20043
  v: exports_external.literal(PROTOCOL_VERSION),
@@ -20351,13 +20352,17 @@ class AuthBrokerClient {
20351
20352
  sock.destroy();
20352
20353
  }
20353
20354
  }
20354
- async getCredentials(provider) {
20355
+ async getCredentials(provider, account) {
20355
20356
  const base = {
20356
20357
  v: PROTOCOL_VERSION,
20357
20358
  id: randomUUID(),
20358
20359
  op: "get-credentials"
20359
20360
  };
20360
- const req = provider !== undefined ? { ...base, provider } : base;
20361
+ let req = base;
20362
+ if (provider !== undefined)
20363
+ req = { ...req, provider };
20364
+ if (account !== undefined)
20365
+ req = { ...req, account };
20361
20366
  const data = await this.send(req);
20362
20367
  return data;
20363
20368
  }
@@ -20632,7 +20637,7 @@ var init_client2 = __esm(() => {
20632
20637
  });
20633
20638
 
20634
20639
  // ../src/config/schema.ts
20635
- var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
20640
+ var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
20636
20641
  var init_schema = __esm(() => {
20637
20642
  init_zod();
20638
20643
  CodeRepoEntrySchema = exports_external.object({
@@ -21030,11 +21035,52 @@ var init_schema = __esm(() => {
21030
21035
  approvers: exports_external.array(ApproverIdSchema).min(1).optional().describe("Per-agent approver override. When set, replaces (does not extend) " + "the top-level drive.approvers list for this agent's onboarding card."),
21031
21036
  tier: GoogleWorkspaceTierSchema.optional().describe("Per-agent tier override (RFC G Phase 1). When set, replaces the " + "top-level google_workspace.tier for this agent. Common case: most " + "agents on `core`, one specialist on `extended` for Slides access.")
21032
21037
  }).optional();
21038
+ MicrosoftAccountEmailSchema = exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
21039
+ message: "microsoft_workspace.account must be a Microsoft account email like " + "'alice@outlook.com' or 'alice@contoso.com' (colons not allowed)"
21040
+ }).transform((v) => v.trim().toLowerCase());
21041
+ MicrosoftToolTokenSchema = exports_external.string().min(1).regex(/^[a-z0-9-]+$/, {
21042
+ message: "microsoft_workspace tools[] tokens are forwarded to softeria's " + "--enabled-tools regex; each must be lowercase kebab-case " + "([a-z0-9-]+, e.g. 'mail', 'calendar', 'send-mail') so an unescaped " + "regex metacharacter can't crash the launcher"
21043
+ });
21044
+ MicrosoftAccountBindingSchema = exports_external.object({
21045
+ account: MicrosoftAccountEmailSchema.describe("The Microsoft account this binding uses. Must be a key in top-level " + "`microsoft_accounts:` with this agent in its `enabled_for[]`."),
21046
+ tools: exports_external.array(MicrosoftToolTokenSchema).min(1).optional().describe("Per-account tool allowlist \u2192 softeria `--enabled-tools <regex>` " + "(tokens joined with `|`). Omitted = all tools exposed for this account."),
21047
+ org_mode: exports_external.boolean().optional().describe("Per-binding org_mode override (RFC #1873 \u00a76.4).")
21048
+ });
21033
21049
  AgentMicrosoftWorkspaceConfigSchema = exports_external.object({
21034
- account: exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
21035
- message: "microsoft_workspace.account must be a Microsoft account email like " + "'alice@outlook.com' or 'alice@contoso.com' (colons not allowed)"
21036
- }).transform((v) => v.trim().toLowerCase()).optional().describe("RFC #1873: the Microsoft account this agent uses for the M365 MCP. " + "Must be a key in top-level `microsoft_accounts:` with this agent " + "listed in its `enabled_for[]`. Read by the auth-broker " + "(get-credentials, provider=microsoft) and by the scaffold to " + "decide whether to emit the `ms-365` MCP entry. Normalized to " + "lowercase so it matches the microsoft_accounts key (which is " + "also normalized)."),
21037
- org_mode: exports_external.boolean().optional().describe("Per-agent org_mode override (RFC #1873 \u00a76.4). When set, replaces " + "the top-level microsoft_workspace.org_mode for this agent. " + "Defaults to top-level value (which defaults to false).")
21050
+ account: MicrosoftAccountEmailSchema.optional().describe("RFC #1873: the Microsoft account this agent uses for the M365 MCP. " + "Must be a key in top-level `microsoft_accounts:` with this agent " + "listed in its `enabled_for[]`. Read by the auth-broker " + "(get-credentials, provider=microsoft) and by the scaffold to " + "decide whether to emit the `ms-365` MCP entry. Normalized to " + "lowercase so it matches the microsoft_accounts key (which is " + "also normalized). Mutually exclusive with `accounts` (plural)."),
21051
+ tools: exports_external.array(MicrosoftToolTokenSchema).min(1).optional().describe("Per-account tool allowlist for the SINGULAR `account` form \u2192 " + "softeria `--enabled-tools <regex>`. Omitted = all tools. Only " + "valid with the singular `account`; using it together with the " + "plural `accounts` (which carries per-binding `tools`) is an error."),
21052
+ org_mode: exports_external.boolean().optional().describe("Per-agent org_mode override (RFC #1873 \u00a76.4). When set, replaces " + "the top-level microsoft_workspace.org_mode for this agent. " + "Defaults to top-level value (which defaults to false)."),
21053
+ accounts: exports_external.array(MicrosoftAccountBindingSchema).min(1).optional().describe("Plural multi-account form: bind MULTIPLE Microsoft accounts to " + "this agent, each with its own tool scope. Mutually exclusive with " + "the singular `account`. Each account gets its own `ms-365-<slug>` " + "MCP server.")
21054
+ }).superRefine((v, ctx) => {
21055
+ const hasSingular = v.account !== undefined;
21056
+ const hasPlural = v.accounts !== undefined;
21057
+ if (hasSingular && hasPlural) {
21058
+ ctx.addIssue({
21059
+ code: exports_external.ZodIssueCode.custom,
21060
+ message: "microsoft_workspace: use EITHER `account` (singular) OR " + "`accounts` (plural array), not both",
21061
+ path: ["accounts"]
21062
+ });
21063
+ }
21064
+ if (hasPlural && v.tools !== undefined) {
21065
+ ctx.addIssue({
21066
+ code: exports_external.ZodIssueCode.custom,
21067
+ message: "microsoft_workspace: block-level `tools` applies to the singular " + "`account` only; with `accounts` put `tools` inside each binding",
21068
+ path: ["tools"]
21069
+ });
21070
+ }
21071
+ if (hasPlural) {
21072
+ const seen = new Set;
21073
+ for (const b of v.accounts) {
21074
+ if (seen.has(b.account)) {
21075
+ ctx.addIssue({
21076
+ code: exports_external.ZodIssueCode.custom,
21077
+ message: `microsoft_workspace: duplicate account '${b.account}' in accounts[]`,
21078
+ path: ["accounts"]
21079
+ });
21080
+ }
21081
+ seen.add(b.account);
21082
+ }
21083
+ }
21038
21084
  }).optional();
21039
21085
  AgentNotionWorkspaceConfigSchema = exports_external.object({
21040
21086
  databases: exports_external.array(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,62}$/, {
@@ -21409,6 +21455,26 @@ var init_schema = __esm(() => {
21409
21455
  for (const [name, a] of Object.entries(cfg.agents ?? {})) {
21410
21456
  checkServes(a.serves, ["agents", name, "serves"]);
21411
21457
  }
21458
+ const microsoftAccounts = cfg.microsoft_accounts;
21459
+ for (const [name, a] of Object.entries(cfg.agents ?? {})) {
21460
+ const mw = a.microsoft_workspace;
21461
+ const accounts = mw?.accounts;
21462
+ if (!accounts)
21463
+ continue;
21464
+ accounts.forEach((b, i) => {
21465
+ const acct = b?.account?.trim().toLowerCase();
21466
+ if (!acct)
21467
+ return;
21468
+ const enabledFor = microsoftAccounts?.[acct]?.enabled_for ?? [];
21469
+ if (!enabledFor.includes(name)) {
21470
+ ctx.addIssue({
21471
+ code: exports_external.ZodIssueCode.custom,
21472
+ message: `agent '${name}' binds Microsoft account '${acct}' but is not in ` + `microsoft_accounts['${acct}'].enabled_for[] \u2014 operator must run ` + `\`switchroom auth microsoft enable ${acct} ${name}\``,
21473
+ path: ["agents", name, "microsoft_workspace", "accounts", i, "account"]
21474
+ });
21475
+ }
21476
+ });
21477
+ }
21412
21478
  });
21413
21479
  });
21414
21480
 
@@ -22024,6 +22090,25 @@ function validateNotionWorkspaceConfig(config) {
22024
22090
  return issues;
22025
22091
  }
22026
22092
 
22093
+ // ../src/config/timezone.ts
22094
+ function isResolvableTimezone(zone) {
22095
+ try {
22096
+ new Intl.DateTimeFormat("en-US", { timeZone: zone });
22097
+ return true;
22098
+ } catch {
22099
+ return false;
22100
+ }
22101
+ }
22102
+ var CONTAINER_DEFAULT_UTC_ZONES;
22103
+ var init_timezone = __esm(() => {
22104
+ CONTAINER_DEFAULT_UTC_ZONES = new Set([
22105
+ "UTC",
22106
+ "Etc/UTC",
22107
+ "Etc/Universal",
22108
+ "Universal"
22109
+ ]);
22110
+ });
22111
+
22027
22112
  // ../src/config/loader.ts
22028
22113
  import { readFileSync as readFileSync9, existsSync as existsSync8 } from "node:fs";
22029
22114
  import { homedir as homedir4 } from "node:os";
@@ -22142,8 +22227,30 @@ function loadConfig(configPath) {
22142
22227
  if (notionIssues.length > 0) {
22143
22228
  throw new ConfigError(`Invalid notion_workspace configuration in ${filePath}`, notionIssues);
22144
22229
  }
22230
+ validateAllTimezones(config, filePath);
22145
22231
  return config;
22146
22232
  }
22233
+ function validateAllTimezones(config, filePath) {
22234
+ const issues = [];
22235
+ const check = (zone, where) => {
22236
+ if (zone == null)
22237
+ return;
22238
+ if (!isResolvableTimezone(zone)) {
22239
+ issues.push(` ${where}: "${zone}" is not a resolvable IANA timezone ` + `(shape is valid but no such zone exists \u2014 check for a typo, ` + `e.g. "Australia/Melbourne", "America/New_York", "UTC").`);
22240
+ }
22241
+ };
22242
+ check(config.switchroom?.timezone, "switchroom.timezone");
22243
+ check(config.defaults?.timezone, "defaults.timezone");
22244
+ for (const [profileName, profile] of Object.entries(config.profiles ?? {})) {
22245
+ check(profile?.timezone, `profiles.${profileName}.timezone`);
22246
+ }
22247
+ for (const [agentName, agentRaw] of Object.entries(config.agents)) {
22248
+ check(agentRaw?.timezone, `agents.${agentName}.timezone`);
22249
+ }
22250
+ if (issues.length > 0) {
22251
+ throw new ConfigError(`Invalid timezone configuration in ${filePath}`, issues);
22252
+ }
22253
+ }
22147
22254
  function validateAllCronTopicAliases(config, filePath) {
22148
22255
  const issues = [];
22149
22256
  for (const [agentName, agentRaw] of Object.entries(config.agents)) {
@@ -22179,6 +22286,7 @@ var init_loader = __esm(() => {
22179
22286
  init_paths();
22180
22287
  init_overlay_loader();
22181
22288
  init_merge();
22289
+ init_timezone();
22182
22290
  import_yaml3 = __toESM(require_dist(), 1);
22183
22291
  ConfigError = class ConfigError extends Error {
22184
22292
  details;
@@ -37359,6 +37467,26 @@ var init_config_approval_handler = __esm(() => {
37359
37467
  pending = new Map;
37360
37468
  });
37361
37469
 
37470
+ // final-answer-detect.ts
37471
+ var FINAL_ANSWER_MIN_CHARS2 = 200;
37472
+
37473
+ // gateway/turn-flush-suppression.ts
37474
+ var exports_turn_flush_suppression = {};
37475
+ __export(exports_turn_flush_suppression, {
37476
+ shouldSuppressTurnFlush: () => shouldSuppressTurnFlush,
37477
+ FLUSH_SUPPRESSION_WINDOW_MS: () => FLUSH_SUPPRESSION_WINDOW_MS
37478
+ });
37479
+ function shouldSuppressTurnFlush(deps, args) {
37480
+ const minChars = Math.max(1, Math.min(FINAL_ANSWER_MIN_CHARS2, args.answerLength));
37481
+ try {
37482
+ return deps.hasSubstantiveOutbound(args.chatId, args.nowMs - FLUSH_SUPPRESSION_WINDOW_MS, args.threadId, minChars);
37483
+ } catch {
37484
+ return false;
37485
+ }
37486
+ }
37487
+ var FLUSH_SUPPRESSION_WINDOW_MS = 2000;
37488
+ var init_turn_flush_suppression = () => {};
37489
+
37362
37490
  // ../src/vault/approvals/client.ts
37363
37491
  function resolveKernelSocketPath2(opts) {
37364
37492
  if (opts?.socket)
@@ -40072,6 +40200,131 @@ function forwardOriginDateIso(o) {
40072
40200
  return new Date(o.date * 1000).toISOString();
40073
40201
  }
40074
40202
 
40203
+ // gateway/unhandled-message.ts
40204
+ var MESSAGE_ENVELOPE_KEYS = new Set([
40205
+ "message_id",
40206
+ "message_thread_id",
40207
+ "date",
40208
+ "chat",
40209
+ "from",
40210
+ "sender_chat",
40211
+ "forward_origin",
40212
+ "reply_to_message",
40213
+ "external_reply",
40214
+ "quote",
40215
+ "reply_to_story",
40216
+ "edit_date",
40217
+ "media_group_id",
40218
+ "author_signature",
40219
+ "is_topic_message",
40220
+ "is_automatic_forward",
40221
+ "via_bot",
40222
+ "sender_boost_count",
40223
+ "business_connection_id",
40224
+ "effect_id",
40225
+ "has_protected_content",
40226
+ "is_from_offline",
40227
+ "link_preview_options",
40228
+ "show_caption_above_media",
40229
+ "entities",
40230
+ "caption_entities",
40231
+ "paid_star_count"
40232
+ ]);
40233
+ var SERVICE_NOISE_KEYS = new Set([
40234
+ "new_chat_members",
40235
+ "left_chat_member",
40236
+ "new_chat_title",
40237
+ "new_chat_photo",
40238
+ "delete_chat_photo",
40239
+ "group_chat_created",
40240
+ "supergroup_chat_created",
40241
+ "channel_chat_created",
40242
+ "message_auto_delete_timer_changed",
40243
+ "migrate_to_chat_id",
40244
+ "migrate_from_chat_id",
40245
+ "forum_topic_created",
40246
+ "forum_topic_edited",
40247
+ "forum_topic_closed",
40248
+ "forum_topic_reopened",
40249
+ "general_forum_topic_hidden",
40250
+ "general_forum_topic_unhidden",
40251
+ "video_chat_scheduled",
40252
+ "video_chat_started",
40253
+ "video_chat_ended",
40254
+ "video_chat_participants_invited",
40255
+ "giveaway_created",
40256
+ "giveaway",
40257
+ "giveaway_winners",
40258
+ "giveaway_completed",
40259
+ "boost_added",
40260
+ "chat_background_set",
40261
+ "write_access_allowed",
40262
+ "proximity_alert_triggered",
40263
+ "chat_set_theme",
40264
+ "connected_website",
40265
+ "direct_message_price_changed"
40266
+ ]);
40267
+ function messageContentKeys(msg) {
40268
+ return Object.keys(msg).filter((k) => !MESSAGE_ENVELOPE_KEYS.has(k));
40269
+ }
40270
+ function planUnhandledMessage(msg) {
40271
+ const contentKeys = messageContentKeys(msg);
40272
+ if (contentKeys.length > 0 && contentKeys.every((k) => SERVICE_NOISE_KEYS.has(k))) {
40273
+ return { action: "log-only", contentKeys };
40274
+ }
40275
+ const contentType = contentKeys[0] ?? "unknown";
40276
+ const text = (typeof msg.text === "string" ? msg.text : undefined) ?? (typeof msg.caption === "string" ? msg.caption : undefined) ?? `(unhandled message content: ${contentType})`;
40277
+ return { action: "turn", text, contentKeys };
40278
+ }
40279
+ var TAP_MAX_LINES_PER_MINUTE = 300;
40280
+ function installUpdateTap(bot, log, nowMs = Date.now) {
40281
+ let windowStart = 0;
40282
+ let windowCount = 0;
40283
+ let suppressed = 0;
40284
+ bot.use(async (ctx, next) => {
40285
+ try {
40286
+ const now = nowMs();
40287
+ if (now - windowStart >= 60000) {
40288
+ if (suppressed > 0) {
40289
+ log(`telegram gateway: rx tap suppressed ${suppressed} update lines in the last minute (cap ${TAP_MAX_LINES_PER_MINUTE}/min)
40290
+ `);
40291
+ }
40292
+ windowStart = now;
40293
+ windowCount = 0;
40294
+ suppressed = 0;
40295
+ }
40296
+ if (windowCount < TAP_MAX_LINES_PER_MINUTE) {
40297
+ windowCount++;
40298
+ const upd = ctx.update;
40299
+ const updateType = Object.keys(upd).find((k) => k !== "update_id") ?? "unknown";
40300
+ const msg = ctx.message;
40301
+ const detail = msg ? ` content=[${messageContentKeys(msg).join(",")}]` : "";
40302
+ log(`telegram gateway: rx update_id=${ctx.update.update_id} type=${updateType}${detail}
40303
+ `);
40304
+ } else {
40305
+ suppressed++;
40306
+ }
40307
+ } catch {}
40308
+ await next();
40309
+ });
40310
+ }
40311
+ function installUnhandledMessageCatchAll(bot, onInbound, log) {
40312
+ bot.on("message", async (ctx) => {
40313
+ try {
40314
+ const msg = ctx.message;
40315
+ const plan = planUnhandledMessage(msg);
40316
+ log(`telegram gateway: catch-all inbound (no specific handler) ` + `update_id=${ctx.update.update_id} chat_id=${ctx.chat?.id ?? "?"} ` + `message_id=${ctx.message?.message_id ?? "?"} ` + `content_keys=[${plan.contentKeys.join(",")}] action=${plan.action}
40317
+ `);
40318
+ if (plan.action === "turn") {
40319
+ await onInbound(ctx, plan.text);
40320
+ }
40321
+ } catch (err) {
40322
+ log(`telegram gateway: catch-all handler error: ${err.message}
40323
+ `);
40324
+ }
40325
+ });
40326
+ }
40327
+
40075
40328
  // shared/local-time.ts
40076
40329
  function localDay2(ms, tz) {
40077
40330
  return new Intl.DateTimeFormat("en-CA", {
@@ -40114,6 +40367,22 @@ function fmtLocalStamp2(ms, tz) {
40114
40367
  return new Date(ms).toISOString();
40115
40368
  }
40116
40369
  }
40370
+ var LEADING_ISO_Z = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z)(\s|$)/;
40371
+ function renderLogTimestampsLocal(text, tz) {
40372
+ if (!text)
40373
+ return text;
40374
+ return text.split(`
40375
+ `).map((line) => {
40376
+ const m = LEADING_ISO_Z.exec(line);
40377
+ if (!m)
40378
+ return line;
40379
+ const ms = Date.parse(m[1]);
40380
+ if (Number.isNaN(ms))
40381
+ return line;
40382
+ return `${fmtLocalStamp2(ms, tz)}${m[2] === "" ? "" : " "}${line.slice(m[0].length)}`;
40383
+ }).join(`
40384
+ `);
40385
+ }
40117
40386
 
40118
40387
  // status-reactions.ts
40119
40388
  var TELEGRAM_REACTION_WHITELIST = new Set([
@@ -63791,71 +64060,6 @@ function extractRequestId(raw) {
63791
64060
  return m ? m[1] : undefined;
63792
64061
  }
63793
64062
 
63794
- // operator-events.ts
63795
- function classifyClaudeError(raw) {
63796
- try {
63797
- return classifyInner(raw);
63798
- } catch {
63799
- return "unknown-4xx";
63800
- }
63801
- }
63802
- function classifyInner(raw) {
63803
- if (raw == null)
63804
- return "unknown-4xx";
63805
- const obj = typeof raw === "object" ? raw : {};
63806
- const errorType = extractString(obj, "error_type") ?? extractString(obj, "type") ?? extractString(getNestedObj(obj, "error"), "type") ?? "";
63807
- const errorCode = extractString(obj, "code") ?? extractString(getNestedObj(obj, "error"), "code") ?? "";
63808
- const message = extractString(obj, "message") ?? extractString(getNestedObj(obj, "error"), "message") ?? (typeof raw === "string" ? raw : "") ?? "";
63809
- const status = extractNumber(obj, "status") ?? extractNumber(obj, "statusCode") ?? extractNumber(obj, "status_code") ?? null;
63810
- const sdkCode = extractString(obj, "error_code") ?? "";
63811
- if (errorType === "authentication_error" || errorCode === "authentication_error" || sdkCode === "authentication_error" || message.toLowerCase().includes("authentication_error")) {
63812
- const msg = message.toLowerCase();
63813
- if (msg.includes("expired") || msg.includes("refresh")) {
63814
- return "credentials-expired";
63815
- }
63816
- return "credentials-invalid";
63817
- }
63818
- if (errorType === "invalid_api_key" || errorCode === "invalid_api_key" || sdkCode === "invalid_api_key" || message.toLowerCase().includes("invalid_api_key") || message.toLowerCase().includes("invalid api key")) {
63819
- return "credentials-invalid";
63820
- }
63821
- if (errorType === "credit_balance_too_low" || errorCode === "credit_balance_too_low" || sdkCode === "credit_balance_too_low" || message.toLowerCase().includes("credit_balance_too_low") || message.toLowerCase().includes("credit balance")) {
63822
- return "credit-exhausted";
63823
- }
63824
- if (errorType === "rate_limit_error" || errorCode === "rate_limit_error" || sdkCode === "rate_limit_error" || message.toLowerCase().includes("rate_limit_error") || message.toLowerCase().includes("rate limit")) {
63825
- return "rate-limited";
63826
- }
63827
- if (errorType === "overloaded_error" || errorCode === "overloaded_error" || sdkCode === "overloaded_error" || message.toLowerCase().includes("overloaded_error") || message.toLowerCase().includes("overloaded")) {
63828
- return "rate-limited";
63829
- }
63830
- if (errorType === "agent-crashed" || errorCode === "agent-crashed") {
63831
- return "agent-crashed";
63832
- }
63833
- if (errorType === "agent-restarted-unexpectedly" || errorCode === "agent-restarted-unexpectedly") {
63834
- return "agent-restarted-unexpectedly";
63835
- }
63836
- if (status != null) {
63837
- if (status >= 400 && status < 500)
63838
- return "unknown-4xx";
63839
- if (status >= 500 && status < 600)
63840
- return "unknown-5xx";
63841
- }
63842
- return "unknown-4xx";
63843
- }
63844
- function extractString(obj, key) {
63845
- const v = obj[key];
63846
- return typeof v === "string" && v.length > 0 ? v : null;
63847
- }
63848
- function extractNumber(obj, key) {
63849
- const v = obj[key];
63850
- return typeof v === "number" ? v : null;
63851
- }
63852
- function getNestedObj(obj, key) {
63853
- const v = obj[key];
63854
- return typeof v === "object" && v != null ? v : {};
63855
- }
63856
- var DEFAULT_OPERATOR_EVENT_COOLDOWN_MS = 5 * 60000;
63857
- var cooldownMap = new Map;
63858
-
63859
64063
  // model-unavailable.ts
63860
64064
  init_quota_check();
63861
64065
  init_card_format();
@@ -63896,6 +64100,18 @@ function isLitellmProxyLocal429(text4) {
63896
64100
  return true;
63897
64101
  return litellmV3LimiterSignalPair.every((s) => lower.includes(s));
63898
64102
  }
64103
+ function isLitellmProxyAuthMisconfig(text4) {
64104
+ if (typeof text4 !== "string" || text4.length === 0)
64105
+ return false;
64106
+ const sample = text4.length > 16384 ? text4.slice(0, 16384) : text4;
64107
+ const lower = sample.toLowerCase();
64108
+ if (lower.includes("x-api-key header is required"))
64109
+ return true;
64110
+ const isAuthErr = lower.includes("authentication_error") || lower.includes("authenticationerror");
64111
+ if (!isAuthErr)
64112
+ return false;
64113
+ return lower.includes("fallback") && lower.includes("x-api-key");
64114
+ }
63899
64115
  function parseLitellmLimitDetail(text4, parseTimeNow = new Date) {
63900
64116
  const empty2 = { limitType: null, limit: null, currentUsage: null, resetAtMs: null };
63901
64117
  if (typeof text4 !== "string" || text4.length === 0)
@@ -64140,6 +64356,83 @@ function parseRelativeDuration(s) {
64140
64356
  return matched && total > 0 ? total : null;
64141
64357
  }
64142
64358
 
64359
+ // operator-events.ts
64360
+ function classifyClaudeError(raw) {
64361
+ try {
64362
+ return classifyInner(raw);
64363
+ } catch {
64364
+ return "unknown-4xx";
64365
+ }
64366
+ }
64367
+ function classifyInner(raw) {
64368
+ if (raw == null)
64369
+ return "unknown-4xx";
64370
+ const obj = typeof raw === "object" ? raw : {};
64371
+ const errorType = extractString(obj, "error_type") ?? extractString(obj, "type") ?? extractString(getNestedObj(obj, "error"), "type") ?? "";
64372
+ const errorCode = extractString(obj, "code") ?? extractString(getNestedObj(obj, "error"), "code") ?? "";
64373
+ const message = extractString(obj, "message") ?? extractString(getNestedObj(obj, "error"), "message") ?? (typeof raw === "string" ? raw : "") ?? "";
64374
+ const status = extractNumber(obj, "status") ?? extractNumber(obj, "statusCode") ?? extractNumber(obj, "status_code") ?? null;
64375
+ const sdkCode = extractString(obj, "error_code") ?? "";
64376
+ if (isLitellmProxyAuthMisconfig(`${errorType}
64377
+ ${errorCode}
64378
+ ${sdkCode}
64379
+ ${message}`)) {
64380
+ return "proxy-misconfig";
64381
+ }
64382
+ if (errorType === "authentication_error" || errorCode === "authentication_error" || sdkCode === "authentication_error" || message.toLowerCase().includes("authentication_error")) {
64383
+ const msg = message.toLowerCase();
64384
+ if (msg.includes("expired") || msg.includes("refresh")) {
64385
+ return "credentials-expired";
64386
+ }
64387
+ return "credentials-invalid";
64388
+ }
64389
+ if (errorType === "invalid_api_key" || errorCode === "invalid_api_key" || sdkCode === "invalid_api_key" || message.toLowerCase().includes("invalid_api_key") || message.toLowerCase().includes("invalid api key")) {
64390
+ return "credentials-invalid";
64391
+ }
64392
+ if (errorType === "credit_balance_too_low" || errorCode === "credit_balance_too_low" || sdkCode === "credit_balance_too_low" || message.toLowerCase().includes("credit_balance_too_low") || message.toLowerCase().includes("credit balance")) {
64393
+ return "credit-exhausted";
64394
+ }
64395
+ if (errorType === "rate_limit_error" || errorCode === "rate_limit_error" || sdkCode === "rate_limit_error" || message.toLowerCase().includes("rate_limit_error") || message.toLowerCase().includes("rate limit")) {
64396
+ return "rate-limited";
64397
+ }
64398
+ if (errorType === "overloaded_error" || errorCode === "overloaded_error" || sdkCode === "overloaded_error" || message.toLowerCase().includes("overloaded_error") || message.toLowerCase().includes("overloaded")) {
64399
+ return "rate-limited";
64400
+ }
64401
+ if (errorType === "agent-crashed" || errorCode === "agent-crashed") {
64402
+ return "agent-crashed";
64403
+ }
64404
+ if (errorType === "agent-restarted-unexpectedly" || errorCode === "agent-restarted-unexpectedly") {
64405
+ return "agent-restarted-unexpectedly";
64406
+ }
64407
+ if (status != null) {
64408
+ if (status >= 400 && status < 500)
64409
+ return "unknown-4xx";
64410
+ if (status >= 500 && status < 600)
64411
+ return "unknown-5xx";
64412
+ }
64413
+ return "unknown-4xx";
64414
+ }
64415
+ function extractString(obj, key) {
64416
+ const v = obj[key];
64417
+ return typeof v === "string" && v.length > 0 ? v : null;
64418
+ }
64419
+ function extractNumber(obj, key) {
64420
+ const v = obj[key];
64421
+ return typeof v === "number" ? v : null;
64422
+ }
64423
+ function getNestedObj(obj, key) {
64424
+ const v = obj[key];
64425
+ return typeof v === "object" && v != null ? v : {};
64426
+ }
64427
+ var DEFAULT_OPERATOR_EVENT_COOLDOWN_MS = 5 * 60000;
64428
+ var cooldownMap = new Map;
64429
+ var OPERATOR_ACTIONABLE_KINDS = new Set([
64430
+ "credentials-expired",
64431
+ "credentials-invalid",
64432
+ "credit-exhausted",
64433
+ "proxy-misconfig"
64434
+ ]);
64435
+
64143
64436
  // session-tail.ts
64144
64437
  function sanitizeCwdToProjectName(cwd) {
64145
64438
  return cwd.replace(/[^a-zA-Z0-9]/g, "-");
@@ -65541,13 +65834,17 @@ class AuthBrokerClient2 {
65541
65834
  sock.destroy();
65542
65835
  }
65543
65836
  }
65544
- async getCredentials(provider) {
65837
+ async getCredentials(provider, account) {
65545
65838
  const base = {
65546
65839
  v: PROTOCOL_VERSION,
65547
65840
  id: randomUUID4(),
65548
65841
  op: "get-credentials"
65549
65842
  };
65550
- const req = provider !== undefined ? { ...base, provider } : base;
65843
+ let req = base;
65844
+ if (provider !== undefined)
65845
+ req = { ...req, provider };
65846
+ if (account !== undefined)
65847
+ req = { ...req, account };
65551
65848
  const data = await this.send(req);
65552
65849
  return data;
65553
65850
  }
@@ -66757,6 +67054,20 @@ function renderOperatorEvent(ev) {
66757
67054
  ]
66758
67055
  }
66759
67056
  };
67057
+ case "proxy-misconfig":
67058
+ return {
67059
+ text: [
67060
+ `\uD83D\uDEE0\ufe0f **Model-gateway auth misconfig** for **${agent}**.`,
67061
+ detail ? `_${detail}_` : "",
67062
+ `The local LiteLLM proxy re-dispatched a fallback without forwarding the OAuth header (keyless deployment \u2192 Anthropic 401). Fix the proxy fallback config \u2014 this is NOT a login problem.`
67063
+ ].filter(Boolean).join(`
67064
+ `),
67065
+ keyboard: {
67066
+ inline_keyboard: [
67067
+ [{ text: "\u274c Dismiss", callback_data: `op:dismiss:${encodeURIComponent(ev.agent)}` }]
67068
+ ]
67069
+ }
67070
+ };
66760
67071
  case "credit-exhausted":
66761
67072
  return {
66762
67073
  text: [
@@ -66917,6 +67228,55 @@ function shouldEmitOperatorEvent(agent, kind, now = Date.now(), cooldownMs = DEF
66917
67228
  cooldownMap2.set(key, now);
66918
67229
  return true;
66919
67230
  }
67231
+ var OPERATOR_ACTIONABLE_KINDS2 = new Set([
67232
+ "credentials-expired",
67233
+ "credentials-invalid",
67234
+ "credit-exhausted",
67235
+ "proxy-misconfig"
67236
+ ]);
67237
+ function isOperatorActionableKind(kind) {
67238
+ return OPERATOR_ACTIONABLE_KINDS2.has(kind);
67239
+ }
67240
+ function decideOperatorEventAudience(kind, allowFrom, operatorChatId) {
67241
+ if (!isOperatorActionableKind(kind)) {
67242
+ return { operatorChats: [...allowFrom], userNoticeChats: [] };
67243
+ }
67244
+ const operator = operatorChatId != null && allowFrom.includes(operatorChatId) ? operatorChatId : allowFrom[0];
67245
+ const operatorChats = operator != null ? [operator] : [];
67246
+ const userNoticeChats = allowFrom.filter((c) => c !== operator);
67247
+ return { operatorChats, userNoticeChats };
67248
+ }
67249
+ function renderUserFacingFailureNotice() {
67250
+ return "\u26a0\ufe0f Sorry \u2014 I couldn't complete that just now. It's a problem on our side, not anything you did. Please try again shortly.";
67251
+ }
67252
+
67253
+ // pending-user-notice.ts
67254
+ var PENDING_USER_NOTICE_TTL_MS = 10 * 60000;
67255
+
67256
+ class PendingUserNoticeGate {
67257
+ pending = [];
67258
+ schedule(notice) {
67259
+ this.prune(notice.atMs);
67260
+ this.pending = this.pending.filter((p) => p.agent !== notice.agent);
67261
+ this.pending.push(notice);
67262
+ }
67263
+ resolveTurnEnd(turnDeliveredReply, now = Date.now()) {
67264
+ this.prune(now);
67265
+ const out = turnDeliveredReply ? [] : [...this.pending];
67266
+ this.pending = [];
67267
+ return out;
67268
+ }
67269
+ hasPending(now = Date.now()) {
67270
+ return this.pending.some((p) => now - p.atMs < PENDING_USER_NOTICE_TTL_MS);
67271
+ }
67272
+ prune(now) {
67273
+ this.pending = this.pending.filter((p) => now - p.atMs < PENDING_USER_NOTICE_TTL_MS);
67274
+ }
67275
+ reset() {
67276
+ this.pending = [];
67277
+ }
67278
+ }
67279
+ var pendingUserNoticeGate = new PendingUserNoticeGate;
66920
67280
 
66921
67281
  // operator-events-history.ts
66922
67282
  var EVENT_TTL_MS = 60 * 60 * 1000;
@@ -67030,7 +67390,13 @@ function parseLlmError(raw, retryState) {
67030
67390
  }
67031
67391
  function classifyKindAndSource(text4) {
67032
67392
  const lower = text4.toLowerCase();
67393
+ if (isLitellmProxyAuthMisconfig(text4)) {
67394
+ return { kind: "infra_misconfig", source: "litellm-local" };
67395
+ }
67033
67396
  const claudeKind = classifyClaudeError({ message: text4, type: text4 });
67397
+ if (claudeKind === "proxy-misconfig") {
67398
+ return { kind: "infra_misconfig", source: "litellm-local" };
67399
+ }
67034
67400
  if (claudeKind === "credentials-expired" || claudeKind === "credentials-invalid") {
67035
67401
  return { kind: "auth", source: "anthropic" };
67036
67402
  }
@@ -67073,6 +67439,8 @@ function buildCoreText(kind, source) {
67073
67439
  return "Usage limit reached on this Claude subscription.";
67074
67440
  case "auth":
67075
67441
  return "Claude login needs re-authentication.";
67442
+ case "infra_misconfig":
67443
+ return "Local model-gateway auth misconfig (proxy fallback dropped the OAuth header).";
67076
67444
  case "transient":
67077
67445
  return source === "network" ? "Couldn't reach Anthropic (network) \u2014 retrying automatically." : "A temporary upstream hiccup \u2014 retrying automatically.";
67078
67446
  case "unknown":
@@ -67118,6 +67486,8 @@ function buildRecommendation(parsed, tz) {
67118
67486
  switch (parsed.kind) {
67119
67487
  case "auth":
67120
67488
  return "\u2192 Re-authenticate this account to continue.";
67489
+ case "infra_misconfig":
67490
+ return "\u2192 Fix the LiteLLM proxy fallback config (deployment missing OAuth passthrough).";
67121
67491
  case "quota_wall": {
67122
67492
  const reset2 = formatResetClock(parsed.resetAt, tz);
67123
67493
  return reset2 ? `\u2192 Switch to another account, or wait for the quota to reset at ${reset2}.` : "\u2192 Switch to another account, or wait for the quota to reset.";
@@ -67159,6 +67529,8 @@ function kindEmoji(kind) {
67159
67529
  return "\u26a0\ufe0f";
67160
67530
  case "auth":
67161
67531
  return "\uD83D\uDD11";
67532
+ case "infra_misconfig":
67533
+ return "\uD83D\uDEE0\ufe0f";
67162
67534
  case "transient":
67163
67535
  return "\uD83C\uDF10";
67164
67536
  case "unknown":
@@ -71318,6 +71690,30 @@ function isValidModelArg(arg) {
71318
71690
  function isSrModel(name) {
71319
71691
  return name.startsWith("sr-");
71320
71692
  }
71693
+ function parseModelSwitchTarget(reason) {
71694
+ const m = reason.match(/\/model\s+(\S+)/);
71695
+ return m ? m[1] : null;
71696
+ }
71697
+ function modelFamilyToken(token) {
71698
+ const t = token.trim().toLowerCase();
71699
+ if (t.startsWith("claude-")) {
71700
+ const family = t.slice("claude-".length).split("-").filter((p) => p.length > 0)[0];
71701
+ return family ?? t;
71702
+ }
71703
+ return t;
71704
+ }
71705
+ function classifyModelSwitchConfirmation(input) {
71706
+ const { reason, launched, configured } = input;
71707
+ const isApplyBoot = launched.length > 0 && launched !== configured;
71708
+ if (isApplyBoot)
71709
+ return { kind: "applied", launched };
71710
+ const target = parseModelSwitchTarget(reason);
71711
+ const revertedTo = launched.length > 0 ? launched : configured;
71712
+ if (target != null && target.toLowerCase() !== "default" && modelFamilyToken(target) !== modelFamilyToken(revertedTo)) {
71713
+ return { kind: "not-applied", target, revertedTo };
71714
+ }
71715
+ return { kind: "default", launched: revertedTo };
71716
+ }
71321
71717
  function parseModelCommand(text4) {
71322
71718
  const m = text4.match(/^\/model(?:@[A-Za-z0-9_]+)?(?:\s+([\s\S]*))?$/);
71323
71719
  if (!m)
@@ -71761,6 +72157,7 @@ var MODEL_CALLBACK_ALIAS2 = "mdl:alias:";
71761
72157
  // gateway/session-model-file.ts
71762
72158
  var SESSION_MODEL_FILE = ".session-model";
71763
72159
  var CONFIGURED_DEFAULT_MODEL_FILE = ".configured-default-model";
72160
+ var SESSION_MODEL_BOOT_ATTEMPTS_FILE = ".session-model-boot-attempts";
71764
72161
  function atomicWrite(path2, content3) {
71765
72162
  const tmp = `${path2}.tmp-${process.pid}-${Date.now()}`;
71766
72163
  writeFileSync21(tmp, content3, "utf8");
@@ -71792,6 +72189,14 @@ function clearSessionModelFile(agentDir) {
71792
72189
  rmSync4(join30(agentDir, SESSION_MODEL_FILE), { force: true });
71793
72190
  } catch {}
71794
72191
  }
72192
+ function consumeSessionModelCarrierOnHealthyBoot(agentDir) {
72193
+ try {
72194
+ rmSync4(join30(agentDir, SESSION_MODEL_FILE), { force: true });
72195
+ } catch {}
72196
+ try {
72197
+ rmSync4(join30(agentDir, SESSION_MODEL_BOOT_ATTEMPTS_FILE), { force: true });
72198
+ } catch {}
72199
+ }
71795
72200
  function restoreSessionModelFileRaw(agentDir, raw) {
71796
72201
  if (raw == null) {
71797
72202
  clearSessionModelFile(agentDir);
@@ -72165,14 +72570,7 @@ init_merge();
72165
72570
 
72166
72571
  // ../src/agents/scaffold.ts
72167
72572
  init_merge();
72168
-
72169
- // ../src/config/timezone.ts
72170
- var CONTAINER_DEFAULT_UTC_ZONES = new Set([
72171
- "UTC",
72172
- "Etc/UTC",
72173
- "Etc/Universal",
72174
- "Universal"
72175
- ]);
72573
+ init_timezone();
72176
72574
 
72177
72575
  // ../src/cli/agent-config.ts
72178
72576
  import { join as join31 } from "node:path";
@@ -72269,7 +72667,10 @@ var SWITCHROOM_DEFAULT_THINKING_EFFORT = "low";
72269
72667
  function resolveMainModel(model) {
72270
72668
  if (model === undefined || model === "default")
72271
72669
  return SWITCHROOM_DEFAULT_MAIN_MODEL;
72272
- return model;
72670
+ return normalizeModelAlias(model);
72671
+ }
72672
+ function normalizeModelAlias(model) {
72673
+ return model === "claude-fable-5" ? "fable" : model;
72273
72674
  }
72274
72675
  var CLAUDE_MD_YOURS_PLACEHOLDER = "This space is yours. Add per-agent rules, exceptions, or context the " + "Switchroom template doesn't capture. Everything above the marker line is " + "regenerated on every apply; this section is preserved.";
72275
72676
  var SWITCHROOM_OWNED_SETTINGS_KEYS = new Set([
@@ -72922,6 +73323,7 @@ init_schema();
72922
73323
  init_paths();
72923
73324
  init_overlay_loader();
72924
73325
  init_merge();
73326
+ init_timezone();
72925
73327
  var import_yaml4 = __toESM(require_dist(), 1);
72926
73328
  import { readFileSync as readFileSync27, existsSync as existsSync27 } from "node:fs";
72927
73329
  import { homedir as homedir11 } from "node:os";
@@ -73049,8 +73451,30 @@ function loadConfig2(configPath) {
73049
73451
  if (notionIssues.length > 0) {
73050
73452
  throw new ConfigError2(`Invalid notion_workspace configuration in ${filePath}`, notionIssues);
73051
73453
  }
73454
+ validateAllTimezones2(config, filePath);
73052
73455
  return config;
73053
73456
  }
73457
+ function validateAllTimezones2(config, filePath) {
73458
+ const issues = [];
73459
+ const check = (zone, where) => {
73460
+ if (zone == null)
73461
+ return;
73462
+ if (!isResolvableTimezone(zone)) {
73463
+ issues.push(` ${where}: "${zone}" is not a resolvable IANA timezone ` + `(shape is valid but no such zone exists \u2014 check for a typo, ` + `e.g. "Australia/Melbourne", "America/New_York", "UTC").`);
73464
+ }
73465
+ };
73466
+ check(config.switchroom?.timezone, "switchroom.timezone");
73467
+ check(config.defaults?.timezone, "defaults.timezone");
73468
+ for (const [profileName, profile] of Object.entries(config.profiles ?? {})) {
73469
+ check(profile?.timezone, `profiles.${profileName}.timezone`);
73470
+ }
73471
+ for (const [agentName3, agentRaw] of Object.entries(config.agents)) {
73472
+ check(agentRaw?.timezone, `agents.${agentName3}.timezone`);
73473
+ }
73474
+ if (issues.length > 0) {
73475
+ throw new ConfigError2(`Invalid timezone configuration in ${filePath}`, issues);
73476
+ }
73477
+ }
73054
73478
  function validateAllCronTopicAliases2(config, filePath) {
73055
73479
  const issues = [];
73056
73480
  for (const [agentName3, agentRaw] of Object.entries(config.agents)) {
@@ -84169,10 +84593,10 @@ function effectiveTurnAgeMs(markerAgeMs, turnStartedAt, now) {
84169
84593
  }
84170
84594
 
84171
84595
  // ../src/build-info.ts
84172
- var VERSION = "0.18.29";
84173
- var COMMIT_SHA = "8f9b38b3";
84174
- var COMMIT_DATE = "2026-07-16T21:47:27+10:00";
84175
- var LATEST_PR = 3281;
84596
+ var VERSION = "0.18.31";
84597
+ var COMMIT_SHA = "486b55f9";
84598
+ var COMMIT_DATE = "2026-07-17T07:36:42Z";
84599
+ var LATEST_PR = 3302;
84176
84600
  var COMMITS_AHEAD_OF_TAG = 0;
84177
84601
 
84178
84602
  // gateway/boot-version.ts
@@ -86203,6 +86627,7 @@ var TOPIC_ID = process.env.TELEGRAM_TOPIC_ID ? Number(process.env.TELEGRAM_TOPIC
86203
86627
  var AGENT_ADMIN = process.env.SWITCHROOM_AGENT_ADMIN === "true";
86204
86628
  var bot = new import_grammy13.Bot(TOKEN);
86205
86629
  installTgPostLogger(bot);
86630
+ installUpdateTap(bot, (line) => process.stderr.write(line));
86206
86631
  var lastGetUpdatesHeartbeatMs = Date.now();
86207
86632
  bot.api.config.use(async (prev, method, payload, signal) => {
86208
86633
  try {
@@ -86755,8 +87180,9 @@ async function deliverAnswer(args) {
86755
87180
  }
86756
87181
  };
86757
87182
  let liveThreadId = args.threadId;
86758
- const sendChunk = async (_chunkIndex, text5) => {
87183
+ const sendChunk = async (chunkIndex, text5) => {
86759
87184
  const chunkIds = [];
87185
+ const anchor = chunkIndex === 0 && args.replyToMessageId != null ? { reply_parameters: { message_id: args.replyToMessageId, allow_sending_without_reply: true } } : {};
86760
87186
  const res = await sendReplyChunks(deps, {
86761
87187
  chatId,
86762
87188
  chunks: [text5],
@@ -86766,6 +87192,7 @@ async function deliverAnswer(args) {
86766
87192
  previewMessageId: null,
86767
87193
  sentIds: chunkIds,
86768
87194
  buildSendOpts: (_i, _isLast, tid) => ({
87195
+ ...anchor,
86769
87196
  ...tid != null ? { message_thread_id: tid } : {},
86770
87197
  link_preview_options: { is_disabled: true }
86771
87198
  }),
@@ -87712,6 +88139,7 @@ function endCurrentTurnAtomic(turn, opts) {
87712
88139
  turn.narrativeGate?.teardown();
87713
88140
  purgeReactionTracking(statusKey(turn.sessionChatId, turn.sessionThreadId), turn);
87714
88141
  armNoReplyDrainTimer(turn);
88142
+ flushPendingUserFailureNotices(turn.finalAnswerDelivered || turn.replyCalled);
87715
88143
  return turnEndedAt;
87716
88144
  }
87717
88145
  function maybeProactiveCompact() {
@@ -89157,7 +89585,12 @@ function emitGatewayOperatorEvent(event) {
89157
89585
  const opEventSupergroup = resolveAgentSupergroupChatId();
89158
89586
  process.stderr.write(`telegram gateway: operator-event posting agent=${agent} kind=${kind} to ${access.allowFrom.length} chat(s)` + (opEventTopic != null ? ` topic=${opEventTopic}` : "") + `
89159
89587
  `);
89160
- for (const chat_id of access.allowFrom) {
89588
+ const { operatorChats, userNoticeChats } = decideOperatorEventAudience(kind, access.allowFrom, access.allowFrom[0]);
89589
+ if (userNoticeChats.length > 0) {
89590
+ process.stderr.write(`telegram gateway: operator-event operator-only routing agent=${agent} kind=${kind} operatorChats=${operatorChats.length} userNoticeChats=${userNoticeChats.length}
89591
+ `);
89592
+ }
89593
+ for (const chat_id of operatorChats) {
89161
89594
  const opEventThread = topicForRecipient({ recipientChatId: chat_id, resolvedTopic: opEventTopic, supergroupChatId: opEventSupergroup });
89162
89595
  const opts = {
89163
89596
  ...renderedKeyboard ? { reply_markup: renderedKeyboard } : {},
@@ -89177,6 +89610,38 @@ function emitGatewayOperatorEvent(event) {
89177
89610
  `);
89178
89611
  });
89179
89612
  }
89613
+ if (userNoticeChats.length > 0) {
89614
+ pendingUserNoticeGate.schedule({
89615
+ chatIds: userNoticeChats,
89616
+ text: renderUserFacingFailureNotice(),
89617
+ agent,
89618
+ kind,
89619
+ atMs: Date.now()
89620
+ });
89621
+ process.stderr.write(`telegram gateway: operator-event user-notice deferred to turn-end agent=${agent} kind=${kind} chats=${userNoticeChats.length}
89622
+ `);
89623
+ }
89624
+ }
89625
+ function flushPendingUserFailureNotices(turnDeliveredReply) {
89626
+ const notices = pendingUserNoticeGate.resolveTurnEnd(turnDeliveredReply);
89627
+ if (notices.length === 0)
89628
+ return;
89629
+ const noticeTopic = resolveAgentOutboundTopic({ kind: "compact-watchdog" });
89630
+ const noticeSupergroup = resolveAgentSupergroupChatId();
89631
+ for (const notice of notices) {
89632
+ process.stderr.write(`telegram gateway: user-notice flush (turn died reply-less) agent=${notice.agent} kind=${notice.kind} chats=${notice.chatIds.length}
89633
+ `);
89634
+ for (const chat_id of notice.chatIds) {
89635
+ const thread = topicForRecipient({ recipientChatId: chat_id, resolvedTopic: noticeTopic, supergroupChatId: noticeSupergroup });
89636
+ const opts = {
89637
+ ...thread != null ? { message_thread_id: thread } : {}
89638
+ };
89639
+ bot.api.sendRichMessage(chat_id, richMessage2(notice.text), opts).catch((e) => {
89640
+ process.stderr.write(`telegram gateway: user-notice send to ${chat_id} failed agent=${notice.agent} kind=${notice.kind}: ${e}
89641
+ `);
89642
+ });
89643
+ }
89644
+ }
89180
89645
  }
89181
89646
  function postLegacyBanner(chatId, threadId, ackMessageId, ageSec, site) {
89182
89647
  const text5 = `\uD83C\uDF9B\uFE0F Switchroom restarted \u2014 ready. (took ~${ageSec}s)`;
@@ -89687,6 +90152,11 @@ function ensureIssuesCard(chatId, threadId) {
89687
90152
  }
89688
90153
  process.stderr.write(`telegram gateway: wrote PID file ${GATEWAY_PID_PATH} pid=${process.pid} startedAt=${GATEWAY_STARTED_AT_MS}
89689
90154
  `);
90155
+ {
90156
+ const carrierAgentDir = resolveAgentDirFromEnv();
90157
+ if (carrierAgentDir != null)
90158
+ consumeSessionModelCarrierOnHealthyBoot(carrierAgentDir);
90159
+ }
89690
90160
  runBootPinCleanupAndDmSweep();
89691
90161
  } catch (err) {
89692
90162
  process.stderr.write(`telegram gateway: boot.lock_acquire_failed err=${err.message} agent=${SWITCHROOM_AGENT_NAME}
@@ -89695,6 +90165,11 @@ function ensureIssuesCard(chatId, threadId) {
89695
90165
  writePidFile(GATEWAY_PID_PATH, { pid: process.pid, startedAtMs: GATEWAY_STARTED_AT_MS });
89696
90166
  process.stderr.write(`telegram gateway: wrote PID file ${GATEWAY_PID_PATH} pid=${process.pid} startedAt=${GATEWAY_STARTED_AT_MS} (mutex-fallback)
89697
90167
  `);
90168
+ {
90169
+ const carrierAgentDir = resolveAgentDirFromEnv();
90170
+ if (carrierAgentDir != null)
90171
+ consumeSessionModelCarrierOnHealthyBoot(carrierAgentDir);
90172
+ }
89698
90173
  runBootPinCleanupAndDmSweep();
89699
90174
  } catch (writeErr) {
89700
90175
  process.stderr.write(`telegram gateway: writePidFile failed: ${writeErr}
@@ -93958,6 +94433,7 @@ function handleSessionEvent(ev) {
93958
94433
  const turn = currentTurn;
93959
94434
  if (turn == null)
93960
94435
  return;
94436
+ resetAnswerReadyFlushTimeout();
93961
94437
  const ctrl = activeStatusReactions.get(statusKey(turn.sessionChatId, turn.sessionThreadId));
93962
94438
  if (ctrl)
93963
94439
  ctrl.setThinking();
@@ -94362,15 +94838,21 @@ function handleSessionEvent(ev) {
94362
94838
  await new Promise((resolve11) => setTimeout(resolve11, 500));
94363
94839
  if (HISTORY_ENABLED) {
94364
94840
  try {
94365
- const { getRecentOutboundCount: getRecentOutboundCount2 } = await Promise.resolve().then(() => (init_history(), exports_history));
94366
- const recentCount = getRecentOutboundCount2(backstopChatId, 2);
94367
- if (recentCount > 0) {
94368
- process.stderr.write(`telegram gateway: turn-flush suppressed \u2014 reply tool sent ${recentCount} message(s) within 2s
94841
+ const { hasOutboundDeliveredSince: hasOutboundDeliveredSince2 } = await Promise.resolve().then(() => (init_history(), exports_history));
94842
+ const { shouldSuppressTurnFlush: shouldSuppressTurnFlush2 } = await Promise.resolve().then(() => (init_turn_flush_suppression(), exports_turn_flush_suppression));
94843
+ const suppress = shouldSuppressTurnFlush2({ hasSubstantiveOutbound: hasOutboundDeliveredSince2 }, {
94844
+ chatId: backstopChatId,
94845
+ threadId: backstopThreadId ?? null,
94846
+ answerLength: capturedText.length,
94847
+ nowMs: Date.now()
94848
+ });
94849
+ if (suppress) {
94850
+ process.stderr.write(`telegram gateway: turn-flush suppressed \u2014 a substantive same-thread outbound landed within 2s
94369
94851
  `);
94370
94852
  if (backstopTurnEndedAt != null) {
94371
94853
  turn.deliveryOutcome = "suppressed";
94372
94854
  if (OBLIGATION_LEDGER_ENABLED)
94373
- obligationLedger.close(turn.turnId);
94855
+ obligationLedger.noteTurnEnded(turn.turnId, Date.now());
94374
94856
  emitTurnRecord(turn, backstopTurnEndedAt);
94375
94857
  }
94376
94858
  return;
@@ -94393,7 +94875,8 @@ function handleSessionEvent(ev) {
94393
94875
  threadId: backstopThreadId,
94394
94876
  text: capturedText,
94395
94877
  turnId: turn.turnId,
94396
- cardMessageId: backstopCardMessageId
94878
+ cardMessageId: backstopCardMessageId,
94879
+ replyToMessageId: turn.sourceMessageId
94397
94880
  });
94398
94881
  sentIds = delivery.sentIds;
94399
94882
  chunkCount = delivery.chunkCount;
@@ -96288,9 +96771,10 @@ async function dispatchShortVerbViaHostd(ctx, req, label, legacyArgs) {
96288
96771
  await switchroomReply(ctx, `\u274C **${escapeHtmlForTg2(label)} failed via hostd** (result=${escapeHtmlForTg2(hostdResp.result)}):
96289
96772
  ` + preBlock(stripAnsi3(errBody)), { html: true });
96290
96773
  }
96291
- async function runSwitchroomCommand(ctx, args, label, classification = "query") {
96774
+ async function runSwitchroomCommand(ctx, args, label, classification = "query", transformOutput) {
96292
96775
  try {
96293
- const output = stripAnsi3(switchroomExec(args));
96776
+ const stripped = stripAnsi3(switchroomExec(args));
96777
+ const output = transformOutput ? transformOutput(stripped) : stripped;
96294
96778
  const formatted = formatSwitchroomOutput(output);
96295
96779
  if (formatted) {
96296
96780
  await switchroomReply(ctx, preBlock(formatted), { html: true, classification });
@@ -98677,7 +99161,8 @@ bot.command("logs", async (ctx) => {
98677
99161
  }
98678
99162
  const lines = linesArg ? parseInt(linesArg, 10) : 20;
98679
99163
  const lineCount = isNaN(lines) || lines < 1 ? 20 : Math.min(lines, 200);
98680
- await runSwitchroomCommand(ctx, ["agent", "logs", name, "--lines", String(lineCount)], `logs ${name}`, "heavy");
99164
+ const tz = resolveEnvTimezone2();
99165
+ await runSwitchroomCommand(ctx, ["agent", "logs", name, "--lines", String(lineCount), "--timestamps"], `logs ${name}`, "heavy", (raw) => renderLogTimestampsLocal(raw, tz));
98681
99166
  });
98682
99167
  bot.command("memory", async (ctx) => {
98683
99168
  if (!isAuthorizedSender(ctx))
@@ -100073,6 +100558,7 @@ bot.on("message:pinned_message", async (ctx) => {
100073
100558
  `);
100074
100559
  }
100075
100560
  });
100561
+ installUnhandledMessageCatchAll(bot, (ctx, text5) => handleInboundCoalesced(ctx, text5, undefined), (line) => process.stderr.write(line));
100076
100562
  var reactionsCfg = null;
100077
100563
  var reactionHourCap = null;
100078
100564
  var reactionDebounce = null;
@@ -100833,12 +101319,23 @@ var didOneTimeSetup = false;
100833
101319
  `);
100834
101320
  if (modelSwitchReason != null && modelSwitchMarkerChat) {
100835
101321
  const chat = modelSwitchMarkerChat;
100836
- const body = isApplyBoot ? `\u2705 Now running \`${launched}\` \u2014 session-only, reverts to the configured model on the next restart. Fresh session; memory and the handoff briefing carry the context.` : `\u2705 Now running \`${launched || configured}\` (the configured default) \u2014 fresh session; memory and the handoff briefing carry the context.`;
100837
- lockedBot.api.sendMessage(chat.chatId, body, {
100838
- parse_mode: "Markdown",
100839
- ...chat.threadId != null ? { message_thread_id: chat.threadId } : {}
100840
- }).catch((err) => process.stderr.write(`telegram gateway: model-switch confirmation send failed: ${err?.message ?? String(err)}
101322
+ const confirmation = classifyModelSwitchConfirmation({
101323
+ reason: modelSwitchReason,
101324
+ launched,
101325
+ configured
101326
+ });
101327
+ const hasSessionModelAlert = existsSync50(join55(smAgentDir, ".session-model-alert"));
101328
+ if (confirmation.kind === "not-applied" && hasSessionModelAlert) {
101329
+ process.stderr.write(`telegram gateway: gw /model relaunch applied \u2014 suppressing not-applied confirmation (a .session-model-alert is present and will be relayed) agent=${getMyAgentName()} target=${confirmation.target}
101330
+ `);
101331
+ } else {
101332
+ const body = confirmation.kind === "applied" ? `\u2705 Now running \`${confirmation.launched}\` \u2014 session-only, reverts to the configured model on the next restart. Fresh session; memory and the handoff briefing carry the context.` : confirmation.kind === "not-applied" ? `\u26A0\uFE0F Your switch to \`${confirmation.target}\` didn't apply \u2014 the agent reverted to \`${confirmation.revertedTo}\` (the apply-boot didn't complete). Re-issue \`/model ${confirmation.target}\` to try again.` : `\u2705 Now running \`${confirmation.launched}\` (the configured default) \u2014 fresh session; memory and the handoff briefing carry the context.`;
101333
+ lockedBot.api.sendMessage(chat.chatId, body, {
101334
+ parse_mode: "Markdown",
101335
+ ...chat.threadId != null ? { message_thread_id: chat.threadId } : {}
101336
+ }).catch((err) => process.stderr.write(`telegram gateway: model-switch confirmation send failed: ${err?.message ?? String(err)}
100841
101337
  `));
101338
+ }
100842
101339
  }
100843
101340
  } catch {}
100844
101341
  }