switchroom 0.18.29 → 0.18.30

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 (38) 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 +2074 -1585
  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 +73 -20
  14. package/telegram-plugin/dist/bridge/bridge.js +71 -47
  15. package/telegram-plugin/dist/gateway/gateway.js +560 -96
  16. package/telegram-plugin/dist/server.js +89 -64
  17. package/telegram-plugin/gateway/gateway.ts +212 -17
  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/unhandled-message.ts +177 -0
  21. package/telegram-plugin/llm-error-present.ts +24 -0
  22. package/telegram-plugin/model-unavailable.ts +55 -0
  23. package/telegram-plugin/operator-events.ts +113 -0
  24. package/telegram-plugin/pending-user-notice.ts +88 -0
  25. package/telegram-plugin/shared/local-time.ts +43 -0
  26. package/telegram-plugin/tests/catch-all-forwarded-history.test.ts +103 -0
  27. package/telegram-plugin/tests/catch-all-unhandled-message.test.ts +264 -0
  28. package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +26 -2
  29. package/telegram-plugin/tests/litellm-proxy-auth-misconfig.test.ts +278 -0
  30. package/telegram-plugin/tests/local-time.test.ts +68 -1
  31. package/telegram-plugin/tests/model-command.test.ts +133 -0
  32. package/telegram-plugin/tests/session-model-file.test.ts +23 -0
  33. package/vendor/hindsight-memory/scripts/backfill_transcripts.py +399 -2
  34. package/vendor/hindsight-memory/scripts/lib/client.py +47 -0
  35. package/vendor/hindsight-memory/scripts/lib/content.py +53 -1
  36. package/vendor/hindsight-memory/scripts/lib/turnlog.py +450 -0
  37. package/vendor/hindsight-memory/scripts/tests/test_backfill_from_logs.py +467 -0
  38. 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;
@@ -40072,6 +40180,131 @@ function forwardOriginDateIso(o) {
40072
40180
  return new Date(o.date * 1000).toISOString();
40073
40181
  }
40074
40182
 
40183
+ // gateway/unhandled-message.ts
40184
+ var MESSAGE_ENVELOPE_KEYS = new Set([
40185
+ "message_id",
40186
+ "message_thread_id",
40187
+ "date",
40188
+ "chat",
40189
+ "from",
40190
+ "sender_chat",
40191
+ "forward_origin",
40192
+ "reply_to_message",
40193
+ "external_reply",
40194
+ "quote",
40195
+ "reply_to_story",
40196
+ "edit_date",
40197
+ "media_group_id",
40198
+ "author_signature",
40199
+ "is_topic_message",
40200
+ "is_automatic_forward",
40201
+ "via_bot",
40202
+ "sender_boost_count",
40203
+ "business_connection_id",
40204
+ "effect_id",
40205
+ "has_protected_content",
40206
+ "is_from_offline",
40207
+ "link_preview_options",
40208
+ "show_caption_above_media",
40209
+ "entities",
40210
+ "caption_entities",
40211
+ "paid_star_count"
40212
+ ]);
40213
+ var SERVICE_NOISE_KEYS = new Set([
40214
+ "new_chat_members",
40215
+ "left_chat_member",
40216
+ "new_chat_title",
40217
+ "new_chat_photo",
40218
+ "delete_chat_photo",
40219
+ "group_chat_created",
40220
+ "supergroup_chat_created",
40221
+ "channel_chat_created",
40222
+ "message_auto_delete_timer_changed",
40223
+ "migrate_to_chat_id",
40224
+ "migrate_from_chat_id",
40225
+ "forum_topic_created",
40226
+ "forum_topic_edited",
40227
+ "forum_topic_closed",
40228
+ "forum_topic_reopened",
40229
+ "general_forum_topic_hidden",
40230
+ "general_forum_topic_unhidden",
40231
+ "video_chat_scheduled",
40232
+ "video_chat_started",
40233
+ "video_chat_ended",
40234
+ "video_chat_participants_invited",
40235
+ "giveaway_created",
40236
+ "giveaway",
40237
+ "giveaway_winners",
40238
+ "giveaway_completed",
40239
+ "boost_added",
40240
+ "chat_background_set",
40241
+ "write_access_allowed",
40242
+ "proximity_alert_triggered",
40243
+ "chat_set_theme",
40244
+ "connected_website",
40245
+ "direct_message_price_changed"
40246
+ ]);
40247
+ function messageContentKeys(msg) {
40248
+ return Object.keys(msg).filter((k) => !MESSAGE_ENVELOPE_KEYS.has(k));
40249
+ }
40250
+ function planUnhandledMessage(msg) {
40251
+ const contentKeys = messageContentKeys(msg);
40252
+ if (contentKeys.length > 0 && contentKeys.every((k) => SERVICE_NOISE_KEYS.has(k))) {
40253
+ return { action: "log-only", contentKeys };
40254
+ }
40255
+ const contentType = contentKeys[0] ?? "unknown";
40256
+ const text = (typeof msg.text === "string" ? msg.text : undefined) ?? (typeof msg.caption === "string" ? msg.caption : undefined) ?? `(unhandled message content: ${contentType})`;
40257
+ return { action: "turn", text, contentKeys };
40258
+ }
40259
+ var TAP_MAX_LINES_PER_MINUTE = 300;
40260
+ function installUpdateTap(bot, log, nowMs = Date.now) {
40261
+ let windowStart = 0;
40262
+ let windowCount = 0;
40263
+ let suppressed = 0;
40264
+ bot.use(async (ctx, next) => {
40265
+ try {
40266
+ const now = nowMs();
40267
+ if (now - windowStart >= 60000) {
40268
+ if (suppressed > 0) {
40269
+ log(`telegram gateway: rx tap suppressed ${suppressed} update lines in the last minute (cap ${TAP_MAX_LINES_PER_MINUTE}/min)
40270
+ `);
40271
+ }
40272
+ windowStart = now;
40273
+ windowCount = 0;
40274
+ suppressed = 0;
40275
+ }
40276
+ if (windowCount < TAP_MAX_LINES_PER_MINUTE) {
40277
+ windowCount++;
40278
+ const upd = ctx.update;
40279
+ const updateType = Object.keys(upd).find((k) => k !== "update_id") ?? "unknown";
40280
+ const msg = ctx.message;
40281
+ const detail = msg ? ` content=[${messageContentKeys(msg).join(",")}]` : "";
40282
+ log(`telegram gateway: rx update_id=${ctx.update.update_id} type=${updateType}${detail}
40283
+ `);
40284
+ } else {
40285
+ suppressed++;
40286
+ }
40287
+ } catch {}
40288
+ await next();
40289
+ });
40290
+ }
40291
+ function installUnhandledMessageCatchAll(bot, onInbound, log) {
40292
+ bot.on("message", async (ctx) => {
40293
+ try {
40294
+ const msg = ctx.message;
40295
+ const plan = planUnhandledMessage(msg);
40296
+ 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}
40297
+ `);
40298
+ if (plan.action === "turn") {
40299
+ await onInbound(ctx, plan.text);
40300
+ }
40301
+ } catch (err) {
40302
+ log(`telegram gateway: catch-all handler error: ${err.message}
40303
+ `);
40304
+ }
40305
+ });
40306
+ }
40307
+
40075
40308
  // shared/local-time.ts
40076
40309
  function localDay2(ms, tz) {
40077
40310
  return new Intl.DateTimeFormat("en-CA", {
@@ -40114,6 +40347,22 @@ function fmtLocalStamp2(ms, tz) {
40114
40347
  return new Date(ms).toISOString();
40115
40348
  }
40116
40349
  }
40350
+ var LEADING_ISO_Z = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z)(\s|$)/;
40351
+ function renderLogTimestampsLocal(text, tz) {
40352
+ if (!text)
40353
+ return text;
40354
+ return text.split(`
40355
+ `).map((line) => {
40356
+ const m = LEADING_ISO_Z.exec(line);
40357
+ if (!m)
40358
+ return line;
40359
+ const ms = Date.parse(m[1]);
40360
+ if (Number.isNaN(ms))
40361
+ return line;
40362
+ return `${fmtLocalStamp2(ms, tz)}${m[2] === "" ? "" : " "}${line.slice(m[0].length)}`;
40363
+ }).join(`
40364
+ `);
40365
+ }
40117
40366
 
40118
40367
  // status-reactions.ts
40119
40368
  var TELEGRAM_REACTION_WHITELIST = new Set([
@@ -63791,71 +64040,6 @@ function extractRequestId(raw) {
63791
64040
  return m ? m[1] : undefined;
63792
64041
  }
63793
64042
 
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
64043
  // model-unavailable.ts
63860
64044
  init_quota_check();
63861
64045
  init_card_format();
@@ -63896,6 +64080,18 @@ function isLitellmProxyLocal429(text4) {
63896
64080
  return true;
63897
64081
  return litellmV3LimiterSignalPair.every((s) => lower.includes(s));
63898
64082
  }
64083
+ function isLitellmProxyAuthMisconfig(text4) {
64084
+ if (typeof text4 !== "string" || text4.length === 0)
64085
+ return false;
64086
+ const sample = text4.length > 16384 ? text4.slice(0, 16384) : text4;
64087
+ const lower = sample.toLowerCase();
64088
+ if (lower.includes("x-api-key header is required"))
64089
+ return true;
64090
+ const isAuthErr = lower.includes("authentication_error") || lower.includes("authenticationerror");
64091
+ if (!isAuthErr)
64092
+ return false;
64093
+ return lower.includes("fallback") && lower.includes("x-api-key");
64094
+ }
63899
64095
  function parseLitellmLimitDetail(text4, parseTimeNow = new Date) {
63900
64096
  const empty2 = { limitType: null, limit: null, currentUsage: null, resetAtMs: null };
63901
64097
  if (typeof text4 !== "string" || text4.length === 0)
@@ -64140,6 +64336,83 @@ function parseRelativeDuration(s) {
64140
64336
  return matched && total > 0 ? total : null;
64141
64337
  }
64142
64338
 
64339
+ // operator-events.ts
64340
+ function classifyClaudeError(raw) {
64341
+ try {
64342
+ return classifyInner(raw);
64343
+ } catch {
64344
+ return "unknown-4xx";
64345
+ }
64346
+ }
64347
+ function classifyInner(raw) {
64348
+ if (raw == null)
64349
+ return "unknown-4xx";
64350
+ const obj = typeof raw === "object" ? raw : {};
64351
+ const errorType = extractString(obj, "error_type") ?? extractString(obj, "type") ?? extractString(getNestedObj(obj, "error"), "type") ?? "";
64352
+ const errorCode = extractString(obj, "code") ?? extractString(getNestedObj(obj, "error"), "code") ?? "";
64353
+ const message = extractString(obj, "message") ?? extractString(getNestedObj(obj, "error"), "message") ?? (typeof raw === "string" ? raw : "") ?? "";
64354
+ const status = extractNumber(obj, "status") ?? extractNumber(obj, "statusCode") ?? extractNumber(obj, "status_code") ?? null;
64355
+ const sdkCode = extractString(obj, "error_code") ?? "";
64356
+ if (isLitellmProxyAuthMisconfig(`${errorType}
64357
+ ${errorCode}
64358
+ ${sdkCode}
64359
+ ${message}`)) {
64360
+ return "proxy-misconfig";
64361
+ }
64362
+ if (errorType === "authentication_error" || errorCode === "authentication_error" || sdkCode === "authentication_error" || message.toLowerCase().includes("authentication_error")) {
64363
+ const msg = message.toLowerCase();
64364
+ if (msg.includes("expired") || msg.includes("refresh")) {
64365
+ return "credentials-expired";
64366
+ }
64367
+ return "credentials-invalid";
64368
+ }
64369
+ 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")) {
64370
+ return "credentials-invalid";
64371
+ }
64372
+ 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")) {
64373
+ return "credit-exhausted";
64374
+ }
64375
+ if (errorType === "rate_limit_error" || errorCode === "rate_limit_error" || sdkCode === "rate_limit_error" || message.toLowerCase().includes("rate_limit_error") || message.toLowerCase().includes("rate limit")) {
64376
+ return "rate-limited";
64377
+ }
64378
+ if (errorType === "overloaded_error" || errorCode === "overloaded_error" || sdkCode === "overloaded_error" || message.toLowerCase().includes("overloaded_error") || message.toLowerCase().includes("overloaded")) {
64379
+ return "rate-limited";
64380
+ }
64381
+ if (errorType === "agent-crashed" || errorCode === "agent-crashed") {
64382
+ return "agent-crashed";
64383
+ }
64384
+ if (errorType === "agent-restarted-unexpectedly" || errorCode === "agent-restarted-unexpectedly") {
64385
+ return "agent-restarted-unexpectedly";
64386
+ }
64387
+ if (status != null) {
64388
+ if (status >= 400 && status < 500)
64389
+ return "unknown-4xx";
64390
+ if (status >= 500 && status < 600)
64391
+ return "unknown-5xx";
64392
+ }
64393
+ return "unknown-4xx";
64394
+ }
64395
+ function extractString(obj, key) {
64396
+ const v = obj[key];
64397
+ return typeof v === "string" && v.length > 0 ? v : null;
64398
+ }
64399
+ function extractNumber(obj, key) {
64400
+ const v = obj[key];
64401
+ return typeof v === "number" ? v : null;
64402
+ }
64403
+ function getNestedObj(obj, key) {
64404
+ const v = obj[key];
64405
+ return typeof v === "object" && v != null ? v : {};
64406
+ }
64407
+ var DEFAULT_OPERATOR_EVENT_COOLDOWN_MS = 5 * 60000;
64408
+ var cooldownMap = new Map;
64409
+ var OPERATOR_ACTIONABLE_KINDS = new Set([
64410
+ "credentials-expired",
64411
+ "credentials-invalid",
64412
+ "credit-exhausted",
64413
+ "proxy-misconfig"
64414
+ ]);
64415
+
64143
64416
  // session-tail.ts
64144
64417
  function sanitizeCwdToProjectName(cwd) {
64145
64418
  return cwd.replace(/[^a-zA-Z0-9]/g, "-");
@@ -65541,13 +65814,17 @@ class AuthBrokerClient2 {
65541
65814
  sock.destroy();
65542
65815
  }
65543
65816
  }
65544
- async getCredentials(provider) {
65817
+ async getCredentials(provider, account) {
65545
65818
  const base = {
65546
65819
  v: PROTOCOL_VERSION,
65547
65820
  id: randomUUID4(),
65548
65821
  op: "get-credentials"
65549
65822
  };
65550
- const req = provider !== undefined ? { ...base, provider } : base;
65823
+ let req = base;
65824
+ if (provider !== undefined)
65825
+ req = { ...req, provider };
65826
+ if (account !== undefined)
65827
+ req = { ...req, account };
65551
65828
  const data = await this.send(req);
65552
65829
  return data;
65553
65830
  }
@@ -66757,6 +67034,20 @@ function renderOperatorEvent(ev) {
66757
67034
  ]
66758
67035
  }
66759
67036
  };
67037
+ case "proxy-misconfig":
67038
+ return {
67039
+ text: [
67040
+ `\uD83D\uDEE0\ufe0f **Model-gateway auth misconfig** for **${agent}**.`,
67041
+ detail ? `_${detail}_` : "",
67042
+ `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.`
67043
+ ].filter(Boolean).join(`
67044
+ `),
67045
+ keyboard: {
67046
+ inline_keyboard: [
67047
+ [{ text: "\u274c Dismiss", callback_data: `op:dismiss:${encodeURIComponent(ev.agent)}` }]
67048
+ ]
67049
+ }
67050
+ };
66760
67051
  case "credit-exhausted":
66761
67052
  return {
66762
67053
  text: [
@@ -66917,6 +67208,55 @@ function shouldEmitOperatorEvent(agent, kind, now = Date.now(), cooldownMs = DEF
66917
67208
  cooldownMap2.set(key, now);
66918
67209
  return true;
66919
67210
  }
67211
+ var OPERATOR_ACTIONABLE_KINDS2 = new Set([
67212
+ "credentials-expired",
67213
+ "credentials-invalid",
67214
+ "credit-exhausted",
67215
+ "proxy-misconfig"
67216
+ ]);
67217
+ function isOperatorActionableKind(kind) {
67218
+ return OPERATOR_ACTIONABLE_KINDS2.has(kind);
67219
+ }
67220
+ function decideOperatorEventAudience(kind, allowFrom, operatorChatId) {
67221
+ if (!isOperatorActionableKind(kind)) {
67222
+ return { operatorChats: [...allowFrom], userNoticeChats: [] };
67223
+ }
67224
+ const operator = operatorChatId != null && allowFrom.includes(operatorChatId) ? operatorChatId : allowFrom[0];
67225
+ const operatorChats = operator != null ? [operator] : [];
67226
+ const userNoticeChats = allowFrom.filter((c) => c !== operator);
67227
+ return { operatorChats, userNoticeChats };
67228
+ }
67229
+ function renderUserFacingFailureNotice() {
67230
+ 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.";
67231
+ }
67232
+
67233
+ // pending-user-notice.ts
67234
+ var PENDING_USER_NOTICE_TTL_MS = 10 * 60000;
67235
+
67236
+ class PendingUserNoticeGate {
67237
+ pending = [];
67238
+ schedule(notice) {
67239
+ this.prune(notice.atMs);
67240
+ this.pending = this.pending.filter((p) => p.agent !== notice.agent);
67241
+ this.pending.push(notice);
67242
+ }
67243
+ resolveTurnEnd(turnDeliveredReply, now = Date.now()) {
67244
+ this.prune(now);
67245
+ const out = turnDeliveredReply ? [] : [...this.pending];
67246
+ this.pending = [];
67247
+ return out;
67248
+ }
67249
+ hasPending(now = Date.now()) {
67250
+ return this.pending.some((p) => now - p.atMs < PENDING_USER_NOTICE_TTL_MS);
67251
+ }
67252
+ prune(now) {
67253
+ this.pending = this.pending.filter((p) => now - p.atMs < PENDING_USER_NOTICE_TTL_MS);
67254
+ }
67255
+ reset() {
67256
+ this.pending = [];
67257
+ }
67258
+ }
67259
+ var pendingUserNoticeGate = new PendingUserNoticeGate;
66920
67260
 
66921
67261
  // operator-events-history.ts
66922
67262
  var EVENT_TTL_MS = 60 * 60 * 1000;
@@ -67030,7 +67370,13 @@ function parseLlmError(raw, retryState) {
67030
67370
  }
67031
67371
  function classifyKindAndSource(text4) {
67032
67372
  const lower = text4.toLowerCase();
67373
+ if (isLitellmProxyAuthMisconfig(text4)) {
67374
+ return { kind: "infra_misconfig", source: "litellm-local" };
67375
+ }
67033
67376
  const claudeKind = classifyClaudeError({ message: text4, type: text4 });
67377
+ if (claudeKind === "proxy-misconfig") {
67378
+ return { kind: "infra_misconfig", source: "litellm-local" };
67379
+ }
67034
67380
  if (claudeKind === "credentials-expired" || claudeKind === "credentials-invalid") {
67035
67381
  return { kind: "auth", source: "anthropic" };
67036
67382
  }
@@ -67073,6 +67419,8 @@ function buildCoreText(kind, source) {
67073
67419
  return "Usage limit reached on this Claude subscription.";
67074
67420
  case "auth":
67075
67421
  return "Claude login needs re-authentication.";
67422
+ case "infra_misconfig":
67423
+ return "Local model-gateway auth misconfig (proxy fallback dropped the OAuth header).";
67076
67424
  case "transient":
67077
67425
  return source === "network" ? "Couldn't reach Anthropic (network) \u2014 retrying automatically." : "A temporary upstream hiccup \u2014 retrying automatically.";
67078
67426
  case "unknown":
@@ -67118,6 +67466,8 @@ function buildRecommendation(parsed, tz) {
67118
67466
  switch (parsed.kind) {
67119
67467
  case "auth":
67120
67468
  return "\u2192 Re-authenticate this account to continue.";
67469
+ case "infra_misconfig":
67470
+ return "\u2192 Fix the LiteLLM proxy fallback config (deployment missing OAuth passthrough).";
67121
67471
  case "quota_wall": {
67122
67472
  const reset2 = formatResetClock(parsed.resetAt, tz);
67123
67473
  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 +67509,8 @@ function kindEmoji(kind) {
67159
67509
  return "\u26a0\ufe0f";
67160
67510
  case "auth":
67161
67511
  return "\uD83D\uDD11";
67512
+ case "infra_misconfig":
67513
+ return "\uD83D\uDEE0\ufe0f";
67162
67514
  case "transient":
67163
67515
  return "\uD83C\uDF10";
67164
67516
  case "unknown":
@@ -71318,6 +71670,30 @@ function isValidModelArg(arg) {
71318
71670
  function isSrModel(name) {
71319
71671
  return name.startsWith("sr-");
71320
71672
  }
71673
+ function parseModelSwitchTarget(reason) {
71674
+ const m = reason.match(/\/model\s+(\S+)/);
71675
+ return m ? m[1] : null;
71676
+ }
71677
+ function modelFamilyToken(token) {
71678
+ const t = token.trim().toLowerCase();
71679
+ if (t.startsWith("claude-")) {
71680
+ const family = t.slice("claude-".length).split("-").filter((p) => p.length > 0)[0];
71681
+ return family ?? t;
71682
+ }
71683
+ return t;
71684
+ }
71685
+ function classifyModelSwitchConfirmation(input) {
71686
+ const { reason, launched, configured } = input;
71687
+ const isApplyBoot = launched.length > 0 && launched !== configured;
71688
+ if (isApplyBoot)
71689
+ return { kind: "applied", launched };
71690
+ const target = parseModelSwitchTarget(reason);
71691
+ const revertedTo = launched.length > 0 ? launched : configured;
71692
+ if (target != null && target.toLowerCase() !== "default" && modelFamilyToken(target) !== modelFamilyToken(revertedTo)) {
71693
+ return { kind: "not-applied", target, revertedTo };
71694
+ }
71695
+ return { kind: "default", launched: revertedTo };
71696
+ }
71321
71697
  function parseModelCommand(text4) {
71322
71698
  const m = text4.match(/^\/model(?:@[A-Za-z0-9_]+)?(?:\s+([\s\S]*))?$/);
71323
71699
  if (!m)
@@ -71761,6 +72137,7 @@ var MODEL_CALLBACK_ALIAS2 = "mdl:alias:";
71761
72137
  // gateway/session-model-file.ts
71762
72138
  var SESSION_MODEL_FILE = ".session-model";
71763
72139
  var CONFIGURED_DEFAULT_MODEL_FILE = ".configured-default-model";
72140
+ var SESSION_MODEL_BOOT_ATTEMPTS_FILE = ".session-model-boot-attempts";
71764
72141
  function atomicWrite(path2, content3) {
71765
72142
  const tmp = `${path2}.tmp-${process.pid}-${Date.now()}`;
71766
72143
  writeFileSync21(tmp, content3, "utf8");
@@ -71792,6 +72169,14 @@ function clearSessionModelFile(agentDir) {
71792
72169
  rmSync4(join30(agentDir, SESSION_MODEL_FILE), { force: true });
71793
72170
  } catch {}
71794
72171
  }
72172
+ function consumeSessionModelCarrierOnHealthyBoot(agentDir) {
72173
+ try {
72174
+ rmSync4(join30(agentDir, SESSION_MODEL_FILE), { force: true });
72175
+ } catch {}
72176
+ try {
72177
+ rmSync4(join30(agentDir, SESSION_MODEL_BOOT_ATTEMPTS_FILE), { force: true });
72178
+ } catch {}
72179
+ }
71795
72180
  function restoreSessionModelFileRaw(agentDir, raw) {
71796
72181
  if (raw == null) {
71797
72182
  clearSessionModelFile(agentDir);
@@ -72165,14 +72550,7 @@ init_merge();
72165
72550
 
72166
72551
  // ../src/agents/scaffold.ts
72167
72552
  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
- ]);
72553
+ init_timezone();
72176
72554
 
72177
72555
  // ../src/cli/agent-config.ts
72178
72556
  import { join as join31 } from "node:path";
@@ -72922,6 +73300,7 @@ init_schema();
72922
73300
  init_paths();
72923
73301
  init_overlay_loader();
72924
73302
  init_merge();
73303
+ init_timezone();
72925
73304
  var import_yaml4 = __toESM(require_dist(), 1);
72926
73305
  import { readFileSync as readFileSync27, existsSync as existsSync27 } from "node:fs";
72927
73306
  import { homedir as homedir11 } from "node:os";
@@ -73049,8 +73428,30 @@ function loadConfig2(configPath) {
73049
73428
  if (notionIssues.length > 0) {
73050
73429
  throw new ConfigError2(`Invalid notion_workspace configuration in ${filePath}`, notionIssues);
73051
73430
  }
73431
+ validateAllTimezones2(config, filePath);
73052
73432
  return config;
73053
73433
  }
73434
+ function validateAllTimezones2(config, filePath) {
73435
+ const issues = [];
73436
+ const check = (zone, where) => {
73437
+ if (zone == null)
73438
+ return;
73439
+ if (!isResolvableTimezone(zone)) {
73440
+ 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").`);
73441
+ }
73442
+ };
73443
+ check(config.switchroom?.timezone, "switchroom.timezone");
73444
+ check(config.defaults?.timezone, "defaults.timezone");
73445
+ for (const [profileName, profile] of Object.entries(config.profiles ?? {})) {
73446
+ check(profile?.timezone, `profiles.${profileName}.timezone`);
73447
+ }
73448
+ for (const [agentName3, agentRaw] of Object.entries(config.agents)) {
73449
+ check(agentRaw?.timezone, `agents.${agentName3}.timezone`);
73450
+ }
73451
+ if (issues.length > 0) {
73452
+ throw new ConfigError2(`Invalid timezone configuration in ${filePath}`, issues);
73453
+ }
73454
+ }
73054
73455
  function validateAllCronTopicAliases2(config, filePath) {
73055
73456
  const issues = [];
73056
73457
  for (const [agentName3, agentRaw] of Object.entries(config.agents)) {
@@ -84169,10 +84570,10 @@ function effectiveTurnAgeMs(markerAgeMs, turnStartedAt, now) {
84169
84570
  }
84170
84571
 
84171
84572
  // ../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;
84573
+ var VERSION = "0.18.30";
84574
+ var COMMIT_SHA = "7729c674";
84575
+ var COMMIT_DATE = "2026-07-17T10:52:43+10:00";
84576
+ var LATEST_PR = 3293;
84176
84577
  var COMMITS_AHEAD_OF_TAG = 0;
84177
84578
 
84178
84579
  // gateway/boot-version.ts
@@ -86203,6 +86604,7 @@ var TOPIC_ID = process.env.TELEGRAM_TOPIC_ID ? Number(process.env.TELEGRAM_TOPIC
86203
86604
  var AGENT_ADMIN = process.env.SWITCHROOM_AGENT_ADMIN === "true";
86204
86605
  var bot = new import_grammy13.Bot(TOKEN);
86205
86606
  installTgPostLogger(bot);
86607
+ installUpdateTap(bot, (line) => process.stderr.write(line));
86206
86608
  var lastGetUpdatesHeartbeatMs = Date.now();
86207
86609
  bot.api.config.use(async (prev, method, payload, signal) => {
86208
86610
  try {
@@ -87712,6 +88114,7 @@ function endCurrentTurnAtomic(turn, opts) {
87712
88114
  turn.narrativeGate?.teardown();
87713
88115
  purgeReactionTracking(statusKey(turn.sessionChatId, turn.sessionThreadId), turn);
87714
88116
  armNoReplyDrainTimer(turn);
88117
+ flushPendingUserFailureNotices(turn.finalAnswerDelivered || turn.replyCalled);
87715
88118
  return turnEndedAt;
87716
88119
  }
87717
88120
  function maybeProactiveCompact() {
@@ -89157,7 +89560,12 @@ function emitGatewayOperatorEvent(event) {
89157
89560
  const opEventSupergroup = resolveAgentSupergroupChatId();
89158
89561
  process.stderr.write(`telegram gateway: operator-event posting agent=${agent} kind=${kind} to ${access.allowFrom.length} chat(s)` + (opEventTopic != null ? ` topic=${opEventTopic}` : "") + `
89159
89562
  `);
89160
- for (const chat_id of access.allowFrom) {
89563
+ const { operatorChats, userNoticeChats } = decideOperatorEventAudience(kind, access.allowFrom, access.allowFrom[0]);
89564
+ if (userNoticeChats.length > 0) {
89565
+ process.stderr.write(`telegram gateway: operator-event operator-only routing agent=${agent} kind=${kind} operatorChats=${operatorChats.length} userNoticeChats=${userNoticeChats.length}
89566
+ `);
89567
+ }
89568
+ for (const chat_id of operatorChats) {
89161
89569
  const opEventThread = topicForRecipient({ recipientChatId: chat_id, resolvedTopic: opEventTopic, supergroupChatId: opEventSupergroup });
89162
89570
  const opts = {
89163
89571
  ...renderedKeyboard ? { reply_markup: renderedKeyboard } : {},
@@ -89177,6 +89585,38 @@ function emitGatewayOperatorEvent(event) {
89177
89585
  `);
89178
89586
  });
89179
89587
  }
89588
+ if (userNoticeChats.length > 0) {
89589
+ pendingUserNoticeGate.schedule({
89590
+ chatIds: userNoticeChats,
89591
+ text: renderUserFacingFailureNotice(),
89592
+ agent,
89593
+ kind,
89594
+ atMs: Date.now()
89595
+ });
89596
+ process.stderr.write(`telegram gateway: operator-event user-notice deferred to turn-end agent=${agent} kind=${kind} chats=${userNoticeChats.length}
89597
+ `);
89598
+ }
89599
+ }
89600
+ function flushPendingUserFailureNotices(turnDeliveredReply) {
89601
+ const notices = pendingUserNoticeGate.resolveTurnEnd(turnDeliveredReply);
89602
+ if (notices.length === 0)
89603
+ return;
89604
+ const noticeTopic = resolveAgentOutboundTopic({ kind: "compact-watchdog" });
89605
+ const noticeSupergroup = resolveAgentSupergroupChatId();
89606
+ for (const notice of notices) {
89607
+ process.stderr.write(`telegram gateway: user-notice flush (turn died reply-less) agent=${notice.agent} kind=${notice.kind} chats=${notice.chatIds.length}
89608
+ `);
89609
+ for (const chat_id of notice.chatIds) {
89610
+ const thread = topicForRecipient({ recipientChatId: chat_id, resolvedTopic: noticeTopic, supergroupChatId: noticeSupergroup });
89611
+ const opts = {
89612
+ ...thread != null ? { message_thread_id: thread } : {}
89613
+ };
89614
+ bot.api.sendRichMessage(chat_id, richMessage2(notice.text), opts).catch((e) => {
89615
+ process.stderr.write(`telegram gateway: user-notice send to ${chat_id} failed agent=${notice.agent} kind=${notice.kind}: ${e}
89616
+ `);
89617
+ });
89618
+ }
89619
+ }
89180
89620
  }
89181
89621
  function postLegacyBanner(chatId, threadId, ackMessageId, ageSec, site) {
89182
89622
  const text5 = `\uD83C\uDF9B\uFE0F Switchroom restarted \u2014 ready. (took ~${ageSec}s)`;
@@ -89687,6 +90127,11 @@ function ensureIssuesCard(chatId, threadId) {
89687
90127
  }
89688
90128
  process.stderr.write(`telegram gateway: wrote PID file ${GATEWAY_PID_PATH} pid=${process.pid} startedAt=${GATEWAY_STARTED_AT_MS}
89689
90129
  `);
90130
+ {
90131
+ const carrierAgentDir = resolveAgentDirFromEnv();
90132
+ if (carrierAgentDir != null)
90133
+ consumeSessionModelCarrierOnHealthyBoot(carrierAgentDir);
90134
+ }
89690
90135
  runBootPinCleanupAndDmSweep();
89691
90136
  } catch (err) {
89692
90137
  process.stderr.write(`telegram gateway: boot.lock_acquire_failed err=${err.message} agent=${SWITCHROOM_AGENT_NAME}
@@ -89695,6 +90140,11 @@ function ensureIssuesCard(chatId, threadId) {
89695
90140
  writePidFile(GATEWAY_PID_PATH, { pid: process.pid, startedAtMs: GATEWAY_STARTED_AT_MS });
89696
90141
  process.stderr.write(`telegram gateway: wrote PID file ${GATEWAY_PID_PATH} pid=${process.pid} startedAt=${GATEWAY_STARTED_AT_MS} (mutex-fallback)
89697
90142
  `);
90143
+ {
90144
+ const carrierAgentDir = resolveAgentDirFromEnv();
90145
+ if (carrierAgentDir != null)
90146
+ consumeSessionModelCarrierOnHealthyBoot(carrierAgentDir);
90147
+ }
89698
90148
  runBootPinCleanupAndDmSweep();
89699
90149
  } catch (writeErr) {
89700
90150
  process.stderr.write(`telegram gateway: writePidFile failed: ${writeErr}
@@ -96288,9 +96738,10 @@ async function dispatchShortVerbViaHostd(ctx, req, label, legacyArgs) {
96288
96738
  await switchroomReply(ctx, `\u274C **${escapeHtmlForTg2(label)} failed via hostd** (result=${escapeHtmlForTg2(hostdResp.result)}):
96289
96739
  ` + preBlock(stripAnsi3(errBody)), { html: true });
96290
96740
  }
96291
- async function runSwitchroomCommand(ctx, args, label, classification = "query") {
96741
+ async function runSwitchroomCommand(ctx, args, label, classification = "query", transformOutput) {
96292
96742
  try {
96293
- const output = stripAnsi3(switchroomExec(args));
96743
+ const stripped = stripAnsi3(switchroomExec(args));
96744
+ const output = transformOutput ? transformOutput(stripped) : stripped;
96294
96745
  const formatted = formatSwitchroomOutput(output);
96295
96746
  if (formatted) {
96296
96747
  await switchroomReply(ctx, preBlock(formatted), { html: true, classification });
@@ -98677,7 +99128,8 @@ bot.command("logs", async (ctx) => {
98677
99128
  }
98678
99129
  const lines = linesArg ? parseInt(linesArg, 10) : 20;
98679
99130
  const lineCount = isNaN(lines) || lines < 1 ? 20 : Math.min(lines, 200);
98680
- await runSwitchroomCommand(ctx, ["agent", "logs", name, "--lines", String(lineCount)], `logs ${name}`, "heavy");
99131
+ const tz = resolveEnvTimezone2();
99132
+ await runSwitchroomCommand(ctx, ["agent", "logs", name, "--lines", String(lineCount), "--timestamps"], `logs ${name}`, "heavy", (raw) => renderLogTimestampsLocal(raw, tz));
98681
99133
  });
98682
99134
  bot.command("memory", async (ctx) => {
98683
99135
  if (!isAuthorizedSender(ctx))
@@ -100073,6 +100525,7 @@ bot.on("message:pinned_message", async (ctx) => {
100073
100525
  `);
100074
100526
  }
100075
100527
  });
100528
+ installUnhandledMessageCatchAll(bot, (ctx, text5) => handleInboundCoalesced(ctx, text5, undefined), (line) => process.stderr.write(line));
100076
100529
  var reactionsCfg = null;
100077
100530
  var reactionHourCap = null;
100078
100531
  var reactionDebounce = null;
@@ -100833,12 +101286,23 @@ var didOneTimeSetup = false;
100833
101286
  `);
100834
101287
  if (modelSwitchReason != null && modelSwitchMarkerChat) {
100835
101288
  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)}
101289
+ const confirmation = classifyModelSwitchConfirmation({
101290
+ reason: modelSwitchReason,
101291
+ launched,
101292
+ configured
101293
+ });
101294
+ const hasSessionModelAlert = existsSync50(join55(smAgentDir, ".session-model-alert"));
101295
+ if (confirmation.kind === "not-applied" && hasSessionModelAlert) {
101296
+ 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}
101297
+ `);
101298
+ } else {
101299
+ 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.`;
101300
+ lockedBot.api.sendMessage(chat.chatId, body, {
101301
+ parse_mode: "Markdown",
101302
+ ...chat.threadId != null ? { message_thread_id: chat.threadId } : {}
101303
+ }).catch((err) => process.stderr.write(`telegram gateway: model-switch confirmation send failed: ${err?.message ?? String(err)}
100841
101304
  `));
101305
+ }
100842
101306
  }
100843
101307
  } catch {}
100844
101308
  }