switchroom 0.18.28 → 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.
- package/bin/handoff-briefing.sh +15 -2
- package/dist/agent-scheduler/index.js +111 -7
- package/dist/auth-broker/index.js +154 -73
- package/dist/cli/autoaccept-poll.js +8 -3
- package/dist/cli/drive-write-pretool.mjs +8 -3
- package/dist/cli/ms-365-write-pretool.mjs +158 -11
- package/dist/cli/notion-write-pretool.mjs +103 -4
- package/dist/cli/switchroom.js +2712 -2219
- package/dist/host-control/main.js +110 -70
- package/dist/vault/approvals/kernel-server.js +116 -70
- package/dist/vault/broker/server.js +314 -202
- package/package.json +3 -3
- package/profiles/_base/start.sh.hbs +105 -34
- package/telegram-plugin/dist/bridge/bridge.js +71 -47
- package/telegram-plugin/dist/gateway/gateway.js +1128 -666
- package/telegram-plugin/dist/server.js +89 -64
- package/telegram-plugin/gateway/backstop-delivery.ts +272 -0
- package/telegram-plugin/gateway/forward-origin.ts +9 -1
- package/telegram-plugin/gateway/gateway.ts +656 -388
- package/telegram-plugin/gateway/model-command.ts +331 -602
- package/telegram-plugin/gateway/session-model-file.ts +40 -0
- package/telegram-plugin/gateway/turn-record-status.ts +45 -0
- package/telegram-plugin/gateway/unhandled-message.ts +177 -0
- package/telegram-plugin/history.ts +153 -23
- package/telegram-plugin/llm-error-present.ts +24 -0
- package/telegram-plugin/model-unavailable.ts +55 -0
- package/telegram-plugin/operator-events.ts +113 -0
- package/telegram-plugin/pending-user-notice.ts +88 -0
- package/telegram-plugin/shared/local-time.ts +99 -0
- package/telegram-plugin/tests/backstop-delivery.test.ts +250 -0
- package/telegram-plugin/tests/catch-all-forwarded-history.test.ts +103 -0
- package/telegram-plugin/tests/catch-all-unhandled-message.test.ts +264 -0
- package/telegram-plugin/tests/forward-origin.test.ts +30 -3
- package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +111 -60
- package/telegram-plugin/tests/history.test.ts +88 -0
- package/telegram-plugin/tests/litellm-proxy-auth-misconfig.test.ts +278 -0
- package/telegram-plugin/tests/local-time.test.ts +135 -0
- package/telegram-plugin/tests/model-command.test.ts +427 -1512
- package/telegram-plugin/tests/session-model-file.test.ts +23 -0
- package/telegram-plugin/tests/turn-flush-safety.test.ts +34 -0
- package/telegram-plugin/tier-downgrade.ts +4 -3
- package/telegram-plugin/turn-flush-safety.ts +25 -1
- package/vendor/hindsight-memory/scripts/backfill_transcripts.py +399 -2
- package/vendor/hindsight-memory/scripts/lib/client.py +47 -0
- package/vendor/hindsight-memory/scripts/lib/content.py +93 -7
- package/vendor/hindsight-memory/scripts/lib/turnlog.py +450 -0
- package/vendor/hindsight-memory/scripts/tests/test_backfill_from_logs.py +467 -0
- package/vendor/hindsight-memory/tests/test_content.py +63 -7
|
@@ -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
|
-
|
|
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:
|
|
21035
|
-
|
|
21036
|
-
|
|
21037
|
-
|
|
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;
|
|
@@ -28638,6 +28746,7 @@ var init_redact = __esm(() => {
|
|
|
28638
28746
|
// history.ts
|
|
28639
28747
|
var exports_history = {};
|
|
28640
28748
|
__export(exports_history, {
|
|
28749
|
+
verifyHistoryWritable: () => verifyHistoryWritable,
|
|
28641
28750
|
recordReaction: () => recordReaction,
|
|
28642
28751
|
recordOutbound: () => recordOutbound,
|
|
28643
28752
|
recordInbound: () => recordInbound,
|
|
@@ -28676,6 +28785,15 @@ function loadDatabaseClass() {
|
|
|
28676
28785
|
throw new Error(`history.ts requires Bun runtime (bun:sqlite). Caller: ${err.message}`);
|
|
28677
28786
|
}
|
|
28678
28787
|
}
|
|
28788
|
+
function warnHistory(msg) {
|
|
28789
|
+
try {
|
|
28790
|
+
process.stderr.write(`telegram history: ${msg}
|
|
28791
|
+
`);
|
|
28792
|
+
} catch {}
|
|
28793
|
+
}
|
|
28794
|
+
function isValidMessageId(id) {
|
|
28795
|
+
return typeof id === "number" && Number.isInteger(id) && id > 0;
|
|
28796
|
+
}
|
|
28679
28797
|
function initHistory(stateDir, retentionDays = 30) {
|
|
28680
28798
|
if (db != null)
|
|
28681
28799
|
return;
|
|
@@ -28757,6 +28875,33 @@ function initHistory(stateDir, retentionDays = 30) {
|
|
|
28757
28875
|
const cutoff = Math.floor(Date.now() / 1000) - retentionDays * 86400;
|
|
28758
28876
|
db.prepare("DELETE FROM messages WHERE ts < ?").run(cutoff);
|
|
28759
28877
|
}
|
|
28878
|
+
const check = verifyHistoryWritable();
|
|
28879
|
+
if (!check.ok) {
|
|
28880
|
+
warnHistory(`WRITER SELF-CHECK FAILED at boot (path=${path2}): ${check.error ?? "unknown"} \u2014 ` + `history recording is NOT durable; get_recent_messages recovery and the ` + `reply-backstop already-replied suppression will be blind. Investigate the ` + `DB path/mount/permissions before trusting delivery accounting.`);
|
|
28881
|
+
}
|
|
28882
|
+
}
|
|
28883
|
+
function verifyHistoryWritable() {
|
|
28884
|
+
if (db == null)
|
|
28885
|
+
return { ok: false, error: "initHistory() not called" };
|
|
28886
|
+
const SENTINEL_CHAT = "__history_selfcheck__";
|
|
28887
|
+
const sentinelId = Date.now();
|
|
28888
|
+
try {
|
|
28889
|
+
db.prepare("DELETE FROM messages WHERE chat_id = ?").run(SENTINEL_CHAT);
|
|
28890
|
+
db.prepare(`INSERT OR REPLACE INTO messages
|
|
28891
|
+
(chat_id, thread_id, message_id, role, ts, text)
|
|
28892
|
+
VALUES (?, NULL, ?, 'assistant', ?, ?)`).run(SENTINEL_CHAT, sentinelId, Math.floor(Date.now() / 1000), "selfcheck");
|
|
28893
|
+
const row = db.prepare("SELECT text FROM messages WHERE chat_id = ? AND message_id = ?").get(SENTINEL_CHAT, sentinelId);
|
|
28894
|
+
if (row?.text !== "selfcheck") {
|
|
28895
|
+
return { ok: false, error: "sentinel row not read back after insert" };
|
|
28896
|
+
}
|
|
28897
|
+
return { ok: true };
|
|
28898
|
+
} catch (err) {
|
|
28899
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
28900
|
+
} finally {
|
|
28901
|
+
try {
|
|
28902
|
+
db.prepare("DELETE FROM messages WHERE chat_id = ?").run(SENTINEL_CHAT);
|
|
28903
|
+
} catch {}
|
|
28904
|
+
}
|
|
28760
28905
|
}
|
|
28761
28906
|
function _resetForTests() {
|
|
28762
28907
|
if (db != null) {
|
|
@@ -28815,6 +28960,10 @@ function requireDb() {
|
|
|
28815
28960
|
function recordInbound(args) {
|
|
28816
28961
|
if (args.message_id == null)
|
|
28817
28962
|
return;
|
|
28963
|
+
if (!isValidMessageId(args.message_id)) {
|
|
28964
|
+
warnHistory(`recordInbound: dropping row with invalid message_id=${String(args.message_id)} ` + `(chat=${args.chat_id}) \u2014 a delivered inbound will be absent from history`);
|
|
28965
|
+
return;
|
|
28966
|
+
}
|
|
28818
28967
|
const stmt = requireDb().prepare(`
|
|
28819
28968
|
INSERT OR REPLACE INTO messages
|
|
28820
28969
|
(chat_id, thread_id, message_id, role, user, user_id, ts, text, attachment_kind, group_id, reply_to_message_id, reply_to_text, forwarded_from, forwarded_from_type, forwarded_from_id, forwarded_date, forwarded_message_id)
|
|
@@ -28826,23 +28975,38 @@ function recordOutbound(args) {
|
|
|
28826
28975
|
if (args.message_ids.length === 0)
|
|
28827
28976
|
return;
|
|
28828
28977
|
const ts = args.ts ?? Math.floor(Date.now() / 1000);
|
|
28829
|
-
const
|
|
28978
|
+
const validRows = [];
|
|
28979
|
+
for (let i = 0;i < args.message_ids.length; i++) {
|
|
28980
|
+
const id = args.message_ids[i];
|
|
28981
|
+
if (!isValidMessageId(id)) {
|
|
28982
|
+
warnHistory(`recordOutbound: dropping chunk ${i} with invalid message_id=${String(id)} ` + `(chat=${args.chat_id}) \u2014 a delivered outbound will be absent from history, ` + `blinding the reply-backstop already-replied suppression`);
|
|
28983
|
+
continue;
|
|
28984
|
+
}
|
|
28985
|
+
validRows.push({
|
|
28986
|
+
id,
|
|
28987
|
+
text: redact(args.texts[i] ?? ""),
|
|
28988
|
+
attachKind: args.attachment_kinds?.[i] ?? null
|
|
28989
|
+
});
|
|
28990
|
+
}
|
|
28991
|
+
if (validRows.length === 0)
|
|
28992
|
+
return;
|
|
28993
|
+
const groupId = validRows[0].id;
|
|
28830
28994
|
const stmt = requireDb().prepare(`
|
|
28831
28995
|
INSERT OR REPLACE INTO messages
|
|
28832
28996
|
(chat_id, thread_id, message_id, role, user, user_id, ts, text, attachment_kind, group_id)
|
|
28833
28997
|
VALUES (?, ?, ?, 'assistant', NULL, NULL, ?, ?, ?, ?)
|
|
28834
28998
|
`);
|
|
28835
|
-
const tx = requireDb().transaction((
|
|
28836
|
-
for (const
|
|
28837
|
-
stmt.run(args.chat_id, args.thread_id ?? null,
|
|
28999
|
+
const tx = requireDb().transaction((rows) => {
|
|
29000
|
+
for (const r of rows) {
|
|
29001
|
+
stmt.run(args.chat_id, args.thread_id ?? null, r.id, ts, r.text, r.attachKind, groupId);
|
|
28838
29002
|
}
|
|
28839
29003
|
});
|
|
28840
|
-
|
|
28841
|
-
|
|
28842
|
-
|
|
28843
|
-
args.
|
|
28844
|
-
|
|
28845
|
-
|
|
29004
|
+
try {
|
|
29005
|
+
tx(validRows);
|
|
29006
|
+
} catch (err) {
|
|
29007
|
+
warnHistory(`recordOutbound: INSERT failed (chat=${args.chat_id} ids=[${validRows.map((r) => r.id).join(",")}]): ` + `${err instanceof Error ? err.message : String(err)} \u2014 this outbound will be absent from history`);
|
|
29008
|
+
throw err;
|
|
29009
|
+
}
|
|
28846
29010
|
}
|
|
28847
29011
|
function recordEdit(args) {
|
|
28848
29012
|
requireDb().prepare(`
|
|
@@ -39831,6 +39995,64 @@ function buildExtraAttachmentMeta(resolved) {
|
|
|
39831
39995
|
return out;
|
|
39832
39996
|
}
|
|
39833
39997
|
|
|
39998
|
+
// shared/local-time.ts
|
|
39999
|
+
function fmtLocalClock(ms, tz) {
|
|
40000
|
+
return new Intl.DateTimeFormat("en-US", {
|
|
40001
|
+
timeZone: tz,
|
|
40002
|
+
hour: "numeric",
|
|
40003
|
+
minute: "2-digit",
|
|
40004
|
+
hour12: true
|
|
40005
|
+
}).format(new Date(ms)).replace(/\s([AP])M$/, (_m, p) => `${p.toLowerCase()}m`);
|
|
40006
|
+
}
|
|
40007
|
+
function fmtLocalDate(ms, tz) {
|
|
40008
|
+
return new Intl.DateTimeFormat("en-GB", {
|
|
40009
|
+
timeZone: tz,
|
|
40010
|
+
day: "numeric",
|
|
40011
|
+
month: "short"
|
|
40012
|
+
}).format(new Date(ms));
|
|
40013
|
+
}
|
|
40014
|
+
function localDay(ms, tz) {
|
|
40015
|
+
return new Intl.DateTimeFormat("en-CA", {
|
|
40016
|
+
timeZone: tz,
|
|
40017
|
+
year: "numeric",
|
|
40018
|
+
month: "2-digit",
|
|
40019
|
+
day: "2-digit"
|
|
40020
|
+
}).format(new Date(ms));
|
|
40021
|
+
}
|
|
40022
|
+
function tzAbbrev(ms, tz) {
|
|
40023
|
+
const at = new Date(ms);
|
|
40024
|
+
for (const loc of ["en-US", "en-AU", "en-GB"]) {
|
|
40025
|
+
const v = new Intl.DateTimeFormat(loc, { timeZone: tz, timeZoneName: "short" }).formatToParts(at).find((p) => p.type === "timeZoneName")?.value;
|
|
40026
|
+
if (v && !/^(?:GMT|UTC)/i.test(v))
|
|
40027
|
+
return v;
|
|
40028
|
+
}
|
|
40029
|
+
return new Intl.DateTimeFormat("en-US", { timeZone: tz, timeZoneName: "short" }).formatToParts(at).find((p) => p.type === "timeZoneName")?.value ?? tz;
|
|
40030
|
+
}
|
|
40031
|
+
function resolveEnvTimezone(env = process.env) {
|
|
40032
|
+
return env.SWITCHROOM_TIMEZONE ?? env.TZ ?? "UTC";
|
|
40033
|
+
}
|
|
40034
|
+
function fmtLocalStamp(ms, tz) {
|
|
40035
|
+
try {
|
|
40036
|
+
const at = new Date(ms);
|
|
40037
|
+
const weekday = new Intl.DateTimeFormat("en-US", {
|
|
40038
|
+
timeZone: tz,
|
|
40039
|
+
weekday: "long"
|
|
40040
|
+
}).format(at);
|
|
40041
|
+
const date = localDay(ms, tz);
|
|
40042
|
+
const time = new Intl.DateTimeFormat("en-US", {
|
|
40043
|
+
timeZone: tz,
|
|
40044
|
+
hour: "2-digit",
|
|
40045
|
+
minute: "2-digit",
|
|
40046
|
+
hour12: true
|
|
40047
|
+
}).format(at);
|
|
40048
|
+
return `${weekday} ${date} ${time} ${tzAbbrev(ms, tz)}`;
|
|
40049
|
+
} catch {
|
|
40050
|
+
if (tz !== "UTC")
|
|
40051
|
+
return fmtLocalStamp(ms, "UTC");
|
|
40052
|
+
return new Date(ms).toISOString();
|
|
40053
|
+
}
|
|
40054
|
+
}
|
|
40055
|
+
|
|
39834
40056
|
// steering.ts
|
|
39835
40057
|
function escapeXmlAttribute(s) {
|
|
39836
40058
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -39947,7 +40169,7 @@ function buildForwardOriginMeta(origins) {
|
|
|
39947
40169
|
if (o.id != null)
|
|
39948
40170
|
out[`forwarded_from_id${suffix}`] = String(o.id);
|
|
39949
40171
|
if (o.date != null) {
|
|
39950
|
-
out[`forwarded_date${suffix}`] =
|
|
40172
|
+
out[`forwarded_date${suffix}`] = fmtLocalStamp(o.date * 1000, resolveEnvTimezone());
|
|
39951
40173
|
}
|
|
39952
40174
|
});
|
|
39953
40175
|
return out;
|
|
@@ -39958,6 +40180,190 @@ function forwardOriginDateIso(o) {
|
|
|
39958
40180
|
return new Date(o.date * 1000).toISOString();
|
|
39959
40181
|
}
|
|
39960
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
|
+
|
|
40308
|
+
// shared/local-time.ts
|
|
40309
|
+
function localDay2(ms, tz) {
|
|
40310
|
+
return new Intl.DateTimeFormat("en-CA", {
|
|
40311
|
+
timeZone: tz,
|
|
40312
|
+
year: "numeric",
|
|
40313
|
+
month: "2-digit",
|
|
40314
|
+
day: "2-digit"
|
|
40315
|
+
}).format(new Date(ms));
|
|
40316
|
+
}
|
|
40317
|
+
function tzAbbrev2(ms, tz) {
|
|
40318
|
+
const at = new Date(ms);
|
|
40319
|
+
for (const loc of ["en-US", "en-AU", "en-GB"]) {
|
|
40320
|
+
const v = new Intl.DateTimeFormat(loc, { timeZone: tz, timeZoneName: "short" }).formatToParts(at).find((p) => p.type === "timeZoneName")?.value;
|
|
40321
|
+
if (v && !/^(?:GMT|UTC)/i.test(v))
|
|
40322
|
+
return v;
|
|
40323
|
+
}
|
|
40324
|
+
return new Intl.DateTimeFormat("en-US", { timeZone: tz, timeZoneName: "short" }).formatToParts(at).find((p) => p.type === "timeZoneName")?.value ?? tz;
|
|
40325
|
+
}
|
|
40326
|
+
function resolveEnvTimezone2(env = process.env) {
|
|
40327
|
+
return env.SWITCHROOM_TIMEZONE ?? env.TZ ?? "UTC";
|
|
40328
|
+
}
|
|
40329
|
+
function fmtLocalStamp2(ms, tz) {
|
|
40330
|
+
try {
|
|
40331
|
+
const at = new Date(ms);
|
|
40332
|
+
const weekday = new Intl.DateTimeFormat("en-US", {
|
|
40333
|
+
timeZone: tz,
|
|
40334
|
+
weekday: "long"
|
|
40335
|
+
}).format(at);
|
|
40336
|
+
const date = localDay2(ms, tz);
|
|
40337
|
+
const time = new Intl.DateTimeFormat("en-US", {
|
|
40338
|
+
timeZone: tz,
|
|
40339
|
+
hour: "2-digit",
|
|
40340
|
+
minute: "2-digit",
|
|
40341
|
+
hour12: true
|
|
40342
|
+
}).format(at);
|
|
40343
|
+
return `${weekday} ${date} ${time} ${tzAbbrev2(ms, tz)}`;
|
|
40344
|
+
} catch {
|
|
40345
|
+
if (tz !== "UTC")
|
|
40346
|
+
return fmtLocalStamp2(ms, "UTC");
|
|
40347
|
+
return new Date(ms).toISOString();
|
|
40348
|
+
}
|
|
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
|
+
}
|
|
40366
|
+
|
|
39961
40367
|
// status-reactions.ts
|
|
39962
40368
|
var TELEGRAM_REACTION_WHITELIST = new Set([
|
|
39963
40369
|
"\uD83D\uDC4D",
|
|
@@ -57171,40 +57577,6 @@ function sendGateConfigFromEnv(env = process.env) {
|
|
|
57171
57577
|
return out;
|
|
57172
57578
|
}
|
|
57173
57579
|
|
|
57174
|
-
// shared/local-time.ts
|
|
57175
|
-
function fmtLocalClock(ms, tz) {
|
|
57176
|
-
return new Intl.DateTimeFormat("en-US", {
|
|
57177
|
-
timeZone: tz,
|
|
57178
|
-
hour: "numeric",
|
|
57179
|
-
minute: "2-digit",
|
|
57180
|
-
hour12: true
|
|
57181
|
-
}).format(new Date(ms)).replace(/\s([AP])M$/, (_m, p) => `${p.toLowerCase()}m`);
|
|
57182
|
-
}
|
|
57183
|
-
function fmtLocalDate(ms, tz) {
|
|
57184
|
-
return new Intl.DateTimeFormat("en-GB", {
|
|
57185
|
-
timeZone: tz,
|
|
57186
|
-
day: "numeric",
|
|
57187
|
-
month: "short"
|
|
57188
|
-
}).format(new Date(ms));
|
|
57189
|
-
}
|
|
57190
|
-
function localDay(ms, tz) {
|
|
57191
|
-
return new Intl.DateTimeFormat("en-CA", {
|
|
57192
|
-
timeZone: tz,
|
|
57193
|
-
year: "numeric",
|
|
57194
|
-
month: "2-digit",
|
|
57195
|
-
day: "2-digit"
|
|
57196
|
-
}).format(new Date(ms));
|
|
57197
|
-
}
|
|
57198
|
-
function tzAbbrev(ms, tz) {
|
|
57199
|
-
const at = new Date(ms);
|
|
57200
|
-
for (const loc of ["en-US", "en-AU", "en-GB"]) {
|
|
57201
|
-
const v = new Intl.DateTimeFormat(loc, { timeZone: tz, timeZoneName: "short" }).formatToParts(at).find((p) => p.type === "timeZoneName")?.value;
|
|
57202
|
-
if (v && !/^(?:GMT|UTC)/i.test(v))
|
|
57203
|
-
return v;
|
|
57204
|
-
}
|
|
57205
|
-
return new Intl.DateTimeFormat("en-US", { timeZone: tz, timeZoneName: "short" }).formatToParts(at).find((p) => p.type === "timeZoneName")?.value ?? tz;
|
|
57206
|
-
}
|
|
57207
|
-
|
|
57208
57580
|
// send-gate-observability.ts
|
|
57209
57581
|
function formatStatsLine(stats) {
|
|
57210
57582
|
const c = stats.global;
|
|
@@ -57239,7 +57611,7 @@ function createStatsLogger(config) {
|
|
|
57239
57611
|
function isAlertableScope(scopeKey) {
|
|
57240
57612
|
return scopeKey === "global" || scopeKey.startsWith("chat:") || scopeKey.startsWith("group:");
|
|
57241
57613
|
}
|
|
57242
|
-
function
|
|
57614
|
+
function fmtLocalStamp3(ms, tz, refMs) {
|
|
57243
57615
|
const clock = fmtLocalClock(ms, tz);
|
|
57244
57616
|
const sameDay = refMs != null && localDay(ms, tz) === localDay(refMs, tz);
|
|
57245
57617
|
const body = sameDay ? clock : `${fmtLocalDate(ms, tz)} ${clock}`;
|
|
@@ -57283,7 +57655,7 @@ function createFloodWindowObserver(config) {
|
|
|
57283
57655
|
}
|
|
57284
57656
|
function openAlertText(w, now) {
|
|
57285
57657
|
const openFor = fmtDur(now - w.observedAt);
|
|
57286
|
-
return `\u26a0\ufe0f Telegram flood ban active (scope \`${w.scopeKey}\`). ` + `Open for ${openFor}, expected to clear at ${
|
|
57658
|
+
return `\u26a0\ufe0f Telegram flood ban active (scope \`${w.scopeKey}\`). ` + `Open for ${openFor}, expected to clear at ${fmtLocalStamp3(w.untilTs, tz, now)}. ` + `Outbound to that scope is being suppressed.`;
|
|
57287
57659
|
}
|
|
57288
57660
|
function closeAlertText(recs) {
|
|
57289
57661
|
const observedAt = Math.min(...recs.map((r) => r.observedAt));
|
|
@@ -63314,6 +63686,7 @@ function endsWithSilentMarker(text4) {
|
|
|
63314
63686
|
return false;
|
|
63315
63687
|
return isSilentFlushMarker(lines[lines.length - 1]);
|
|
63316
63688
|
}
|
|
63689
|
+
var FLUSH_SUBSTANTIVE_MIN_CHARS = 200;
|
|
63317
63690
|
function selectFlushDeliveryText(blocks) {
|
|
63318
63691
|
const candidates = blocks.map((b) => b.trim()).filter((b) => b.length > 0);
|
|
63319
63692
|
if (candidates.length === 0)
|
|
@@ -63328,8 +63701,18 @@ function selectFlushDeliveryText(blocks) {
|
|
|
63328
63701
|
`);
|
|
63329
63702
|
}
|
|
63330
63703
|
var NARRATION_OPENER = /^(let me\b|lemme\b|i'?ll\b|i will\b|i am going to\b|i'?m going to\b|i'?m about to\b|going to\b|first,?\s+(?:let me|i'?ll|i will)\b|now,?\s+(?:let me|i'?ll|i will)\b|next,?\s+(?:let me|i'?ll|i will)\b|let'?s\b)/i;
|
|
63704
|
+
var NARRATION_TRAILER = /(?:\.{3}|\u2026|:)\s*$/;
|
|
63705
|
+
function isTrailingNarrationLine(block) {
|
|
63706
|
+
const t = block.trim();
|
|
63707
|
+
if (t.length === 0 || t.length >= FLUSH_SUBSTANTIVE_MIN_CHARS)
|
|
63708
|
+
return false;
|
|
63709
|
+
if (t.includes(`
|
|
63710
|
+
`))
|
|
63711
|
+
return false;
|
|
63712
|
+
return NARRATION_TRAILER.test(t);
|
|
63713
|
+
}
|
|
63331
63714
|
function isNarrationBlock(block) {
|
|
63332
|
-
return NARRATION_OPENER.test(block.trimStart());
|
|
63715
|
+
return NARRATION_OPENER.test(block.trimStart()) || isTrailingNarrationLine(block);
|
|
63333
63716
|
}
|
|
63334
63717
|
function decideTurnFlush(input) {
|
|
63335
63718
|
const flushEnabled = input.flushEnabled !== false;
|
|
@@ -63657,71 +64040,6 @@ function extractRequestId(raw) {
|
|
|
63657
64040
|
return m ? m[1] : undefined;
|
|
63658
64041
|
}
|
|
63659
64042
|
|
|
63660
|
-
// operator-events.ts
|
|
63661
|
-
function classifyClaudeError(raw) {
|
|
63662
|
-
try {
|
|
63663
|
-
return classifyInner(raw);
|
|
63664
|
-
} catch {
|
|
63665
|
-
return "unknown-4xx";
|
|
63666
|
-
}
|
|
63667
|
-
}
|
|
63668
|
-
function classifyInner(raw) {
|
|
63669
|
-
if (raw == null)
|
|
63670
|
-
return "unknown-4xx";
|
|
63671
|
-
const obj = typeof raw === "object" ? raw : {};
|
|
63672
|
-
const errorType = extractString(obj, "error_type") ?? extractString(obj, "type") ?? extractString(getNestedObj(obj, "error"), "type") ?? "";
|
|
63673
|
-
const errorCode = extractString(obj, "code") ?? extractString(getNestedObj(obj, "error"), "code") ?? "";
|
|
63674
|
-
const message = extractString(obj, "message") ?? extractString(getNestedObj(obj, "error"), "message") ?? (typeof raw === "string" ? raw : "") ?? "";
|
|
63675
|
-
const status = extractNumber(obj, "status") ?? extractNumber(obj, "statusCode") ?? extractNumber(obj, "status_code") ?? null;
|
|
63676
|
-
const sdkCode = extractString(obj, "error_code") ?? "";
|
|
63677
|
-
if (errorType === "authentication_error" || errorCode === "authentication_error" || sdkCode === "authentication_error" || message.toLowerCase().includes("authentication_error")) {
|
|
63678
|
-
const msg = message.toLowerCase();
|
|
63679
|
-
if (msg.includes("expired") || msg.includes("refresh")) {
|
|
63680
|
-
return "credentials-expired";
|
|
63681
|
-
}
|
|
63682
|
-
return "credentials-invalid";
|
|
63683
|
-
}
|
|
63684
|
-
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")) {
|
|
63685
|
-
return "credentials-invalid";
|
|
63686
|
-
}
|
|
63687
|
-
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")) {
|
|
63688
|
-
return "credit-exhausted";
|
|
63689
|
-
}
|
|
63690
|
-
if (errorType === "rate_limit_error" || errorCode === "rate_limit_error" || sdkCode === "rate_limit_error" || message.toLowerCase().includes("rate_limit_error") || message.toLowerCase().includes("rate limit")) {
|
|
63691
|
-
return "rate-limited";
|
|
63692
|
-
}
|
|
63693
|
-
if (errorType === "overloaded_error" || errorCode === "overloaded_error" || sdkCode === "overloaded_error" || message.toLowerCase().includes("overloaded_error") || message.toLowerCase().includes("overloaded")) {
|
|
63694
|
-
return "rate-limited";
|
|
63695
|
-
}
|
|
63696
|
-
if (errorType === "agent-crashed" || errorCode === "agent-crashed") {
|
|
63697
|
-
return "agent-crashed";
|
|
63698
|
-
}
|
|
63699
|
-
if (errorType === "agent-restarted-unexpectedly" || errorCode === "agent-restarted-unexpectedly") {
|
|
63700
|
-
return "agent-restarted-unexpectedly";
|
|
63701
|
-
}
|
|
63702
|
-
if (status != null) {
|
|
63703
|
-
if (status >= 400 && status < 500)
|
|
63704
|
-
return "unknown-4xx";
|
|
63705
|
-
if (status >= 500 && status < 600)
|
|
63706
|
-
return "unknown-5xx";
|
|
63707
|
-
}
|
|
63708
|
-
return "unknown-4xx";
|
|
63709
|
-
}
|
|
63710
|
-
function extractString(obj, key) {
|
|
63711
|
-
const v = obj[key];
|
|
63712
|
-
return typeof v === "string" && v.length > 0 ? v : null;
|
|
63713
|
-
}
|
|
63714
|
-
function extractNumber(obj, key) {
|
|
63715
|
-
const v = obj[key];
|
|
63716
|
-
return typeof v === "number" ? v : null;
|
|
63717
|
-
}
|
|
63718
|
-
function getNestedObj(obj, key) {
|
|
63719
|
-
const v = obj[key];
|
|
63720
|
-
return typeof v === "object" && v != null ? v : {};
|
|
63721
|
-
}
|
|
63722
|
-
var DEFAULT_OPERATOR_EVENT_COOLDOWN_MS = 5 * 60000;
|
|
63723
|
-
var cooldownMap = new Map;
|
|
63724
|
-
|
|
63725
64043
|
// model-unavailable.ts
|
|
63726
64044
|
init_quota_check();
|
|
63727
64045
|
init_card_format();
|
|
@@ -63762,6 +64080,18 @@ function isLitellmProxyLocal429(text4) {
|
|
|
63762
64080
|
return true;
|
|
63763
64081
|
return litellmV3LimiterSignalPair.every((s) => lower.includes(s));
|
|
63764
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
|
+
}
|
|
63765
64095
|
function parseLitellmLimitDetail(text4, parseTimeNow = new Date) {
|
|
63766
64096
|
const empty2 = { limitType: null, limit: null, currentUsage: null, resetAtMs: null };
|
|
63767
64097
|
if (typeof text4 !== "string" || text4.length === 0)
|
|
@@ -64006,6 +64336,83 @@ function parseRelativeDuration(s) {
|
|
|
64006
64336
|
return matched && total > 0 ? total : null;
|
|
64007
64337
|
}
|
|
64008
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
|
+
|
|
64009
64416
|
// session-tail.ts
|
|
64010
64417
|
function sanitizeCwdToProjectName(cwd) {
|
|
64011
64418
|
return cwd.replace(/[^a-zA-Z0-9]/g, "-");
|
|
@@ -65407,13 +65814,17 @@ class AuthBrokerClient2 {
|
|
|
65407
65814
|
sock.destroy();
|
|
65408
65815
|
}
|
|
65409
65816
|
}
|
|
65410
|
-
async getCredentials(provider) {
|
|
65817
|
+
async getCredentials(provider, account) {
|
|
65411
65818
|
const base = {
|
|
65412
65819
|
v: PROTOCOL_VERSION,
|
|
65413
65820
|
id: randomUUID4(),
|
|
65414
65821
|
op: "get-credentials"
|
|
65415
65822
|
};
|
|
65416
|
-
|
|
65823
|
+
let req = base;
|
|
65824
|
+
if (provider !== undefined)
|
|
65825
|
+
req = { ...req, provider };
|
|
65826
|
+
if (account !== undefined)
|
|
65827
|
+
req = { ...req, account };
|
|
65417
65828
|
const data = await this.send(req);
|
|
65418
65829
|
return data;
|
|
65419
65830
|
}
|
|
@@ -66623,6 +67034,20 @@ function renderOperatorEvent(ev) {
|
|
|
66623
67034
|
]
|
|
66624
67035
|
}
|
|
66625
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
|
+
};
|
|
66626
67051
|
case "credit-exhausted":
|
|
66627
67052
|
return {
|
|
66628
67053
|
text: [
|
|
@@ -66783,6 +67208,55 @@ function shouldEmitOperatorEvent(agent, kind, now = Date.now(), cooldownMs = DEF
|
|
|
66783
67208
|
cooldownMap2.set(key, now);
|
|
66784
67209
|
return true;
|
|
66785
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;
|
|
66786
67260
|
|
|
66787
67261
|
// operator-events-history.ts
|
|
66788
67262
|
var EVENT_TTL_MS = 60 * 60 * 1000;
|
|
@@ -66896,7 +67370,13 @@ function parseLlmError(raw, retryState) {
|
|
|
66896
67370
|
}
|
|
66897
67371
|
function classifyKindAndSource(text4) {
|
|
66898
67372
|
const lower = text4.toLowerCase();
|
|
67373
|
+
if (isLitellmProxyAuthMisconfig(text4)) {
|
|
67374
|
+
return { kind: "infra_misconfig", source: "litellm-local" };
|
|
67375
|
+
}
|
|
66899
67376
|
const claudeKind = classifyClaudeError({ message: text4, type: text4 });
|
|
67377
|
+
if (claudeKind === "proxy-misconfig") {
|
|
67378
|
+
return { kind: "infra_misconfig", source: "litellm-local" };
|
|
67379
|
+
}
|
|
66900
67380
|
if (claudeKind === "credentials-expired" || claudeKind === "credentials-invalid") {
|
|
66901
67381
|
return { kind: "auth", source: "anthropic" };
|
|
66902
67382
|
}
|
|
@@ -66939,6 +67419,8 @@ function buildCoreText(kind, source) {
|
|
|
66939
67419
|
return "Usage limit reached on this Claude subscription.";
|
|
66940
67420
|
case "auth":
|
|
66941
67421
|
return "Claude login needs re-authentication.";
|
|
67422
|
+
case "infra_misconfig":
|
|
67423
|
+
return "Local model-gateway auth misconfig (proxy fallback dropped the OAuth header).";
|
|
66942
67424
|
case "transient":
|
|
66943
67425
|
return source === "network" ? "Couldn't reach Anthropic (network) \u2014 retrying automatically." : "A temporary upstream hiccup \u2014 retrying automatically.";
|
|
66944
67426
|
case "unknown":
|
|
@@ -66984,6 +67466,8 @@ function buildRecommendation(parsed, tz) {
|
|
|
66984
67466
|
switch (parsed.kind) {
|
|
66985
67467
|
case "auth":
|
|
66986
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).";
|
|
66987
67471
|
case "quota_wall": {
|
|
66988
67472
|
const reset2 = formatResetClock(parsed.resetAt, tz);
|
|
66989
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.";
|
|
@@ -67025,6 +67509,8 @@ function kindEmoji(kind) {
|
|
|
67025
67509
|
return "\u26a0\ufe0f";
|
|
67026
67510
|
case "auth":
|
|
67027
67511
|
return "\uD83D\uDD11";
|
|
67512
|
+
case "infra_misconfig":
|
|
67513
|
+
return "\uD83D\uDEE0\ufe0f";
|
|
67028
67514
|
case "transient":
|
|
67029
67515
|
return "\uD83C\uDF10";
|
|
67030
67516
|
case "unknown":
|
|
@@ -69214,7 +69700,7 @@ function endsWithSilentMarker2(text4) {
|
|
|
69214
69700
|
return false;
|
|
69215
69701
|
return isSilentFlushMarker2(lines[lines.length - 1]);
|
|
69216
69702
|
}
|
|
69217
|
-
var
|
|
69703
|
+
var FLUSH_SUBSTANTIVE_MIN_CHARS2 = 200;
|
|
69218
69704
|
function selectFlushDeliveryText2(blocks) {
|
|
69219
69705
|
const candidates = blocks.map((b) => b.trim()).filter((b) => b.length > 0);
|
|
69220
69706
|
if (candidates.length === 0)
|
|
@@ -69229,8 +69715,18 @@ function selectFlushDeliveryText2(blocks) {
|
|
|
69229
69715
|
`);
|
|
69230
69716
|
}
|
|
69231
69717
|
var NARRATION_OPENER2 = /^(let me\b|lemme\b|i'?ll\b|i will\b|i am going to\b|i'?m going to\b|i'?m about to\b|going to\b|first,?\s+(?:let me|i'?ll|i will)\b|now,?\s+(?:let me|i'?ll|i will)\b|next,?\s+(?:let me|i'?ll|i will)\b|let'?s\b)/i;
|
|
69718
|
+
var NARRATION_TRAILER2 = /(?:\.{3}|\u2026|:)\s*$/;
|
|
69719
|
+
function isTrailingNarrationLine2(block) {
|
|
69720
|
+
const t = block.trim();
|
|
69721
|
+
if (t.length === 0 || t.length >= FLUSH_SUBSTANTIVE_MIN_CHARS2)
|
|
69722
|
+
return false;
|
|
69723
|
+
if (t.includes(`
|
|
69724
|
+
`))
|
|
69725
|
+
return false;
|
|
69726
|
+
return NARRATION_TRAILER2.test(t);
|
|
69727
|
+
}
|
|
69232
69728
|
function isNarrationBlock2(block) {
|
|
69233
|
-
return NARRATION_OPENER2.test(block.trimStart());
|
|
69729
|
+
return NARRATION_OPENER2.test(block.trimStart()) || isTrailingNarrationLine2(block);
|
|
69234
69730
|
}
|
|
69235
69731
|
function decideTurnFlush2(input) {
|
|
69236
69732
|
const flushEnabled = input.flushEnabled !== false;
|
|
@@ -71165,16 +71661,6 @@ Allowed: \`${deps.escapeHtml(allow)}\``, { html: true });
|
|
|
71165
71661
|
await deps.reply(ctx, finalText, { html: true, accent });
|
|
71166
71662
|
}
|
|
71167
71663
|
|
|
71168
|
-
// ../src/agents/model-picker.ts
|
|
71169
|
-
function labelTag(label) {
|
|
71170
|
-
let h = 2166136261;
|
|
71171
|
-
for (const ch of label) {
|
|
71172
|
-
h ^= ch.codePointAt(0);
|
|
71173
|
-
h = Math.imul(h, 16777619) >>> 0;
|
|
71174
|
-
}
|
|
71175
|
-
return h.toString(16).padStart(8, "0");
|
|
71176
|
-
}
|
|
71177
|
-
|
|
71178
71664
|
// gateway/model-command.ts
|
|
71179
71665
|
var MODEL_ALIASES = ["opus", "sonnet", "haiku", "fable", "default"];
|
|
71180
71666
|
var MODEL_ARG_RE = /^[A-Za-z0-9][A-Za-z0-9._/\[\]-]{0,99}$/;
|
|
@@ -71184,11 +71670,29 @@ function isValidModelArg(arg) {
|
|
|
71184
71670
|
function isSrModel(name) {
|
|
71185
71671
|
return name.startsWith("sr-");
|
|
71186
71672
|
}
|
|
71187
|
-
function
|
|
71188
|
-
const
|
|
71189
|
-
|
|
71190
|
-
|
|
71191
|
-
|
|
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 };
|
|
71192
71696
|
}
|
|
71193
71697
|
function parseModelCommand(text4) {
|
|
71194
71698
|
const m = text4.match(/^\/model(?:@[A-Za-z0-9_]+)?(?:\s+([\s\S]*))?$/);
|
|
@@ -71233,13 +71737,13 @@ function modelCommandReceiptLine(agent, parsed, busy) {
|
|
|
71233
71737
|
const arg = parsed.kind === "set" ? parsed.model : parsed.kind === "show" ? "(show)" : "(help)";
|
|
71234
71738
|
return `telegram gateway: gw /model received agent=${agent} kind=${parsed.kind} arg=${arg} busy=${busy}`;
|
|
71235
71739
|
}
|
|
71236
|
-
var PERSIST_NOTE = "
|
|
71740
|
+
var PERSIST_NOTE = "_A `/model` switch relaunches the session (~30s) on the chosen model. Session-only \u2014 reverts to the configured `model:` on the next restart. `/model default` reverts now. Live scrollback is replaced by a fresh session; memory and the handoff briefing carry the context. To change the default permanently, set `model:` in switchroom.yaml._";
|
|
71237
71741
|
function helpText2(deps, reason) {
|
|
71238
71742
|
const srAliasExamples = Object.keys(SR_MODEL_ALIASES).map((a) => `\`${a}\``).join(" \u00b7 ");
|
|
71239
71743
|
const lines = [];
|
|
71240
71744
|
if (reason)
|
|
71241
71745
|
lines.push(`\u26a0\ufe0f ${deps.escapeHtml(reason)}`);
|
|
71242
|
-
lines.push("**/model** \u2014 show or switch the Claude model", "`/model` \u2014 show the configured model", `\`/model <name>\` \u2014 switch the live session (${MODEL_ALIASES.map((a) => `\`${a}\``).join(" \u00b7 ")} or a full model id)`, `_OpenRouter shortcuts:_ ${srAliasExamples}`, "
|
|
71746
|
+
lines.push("**/model** \u2014 show or switch the Claude model", "`/model` \u2014 show the configured model", `\`/model <name>\` \u2014 switch the live session (${MODEL_ALIASES.map((a) => `\`${a}\``).join(" \u00b7 ")} or a full model id)`, `_OpenRouter shortcuts:_ ${srAliasExamples}`, "_Every switch relaunches the session (~30s) on the chosen model \u2014 Claude and OpenRouter (sr-\\*) alike._", PERSIST_NOTE);
|
|
71243
71747
|
return { text: lines.join(`
|
|
71244
71748
|
`), html: true };
|
|
71245
71749
|
}
|
|
@@ -71273,132 +71777,46 @@ async function handleModelCommand(parsed, deps) {
|
|
|
71273
71777
|
html: true
|
|
71274
71778
|
};
|
|
71275
71779
|
}
|
|
71276
|
-
|
|
71277
|
-
|
|
71278
|
-
try {
|
|
71279
|
-
await deps.scheduleModelRelaunch(model, `user: /model ${model} (sr-to-claude restart)`);
|
|
71280
|
-
} catch (err) {
|
|
71281
|
-
if (isRestartInFlight(err)) {
|
|
71282
|
-
return {
|
|
71283
|
-
text: `\u23f3 A restart is already in flight \u2014 your switch to \`${deps.escapeHtml(model)}\` will apply as it completes (~15s).`,
|
|
71284
|
-
html: true
|
|
71285
|
-
};
|
|
71286
|
-
}
|
|
71287
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
71288
|
-
return {
|
|
71289
|
-
text: `\u274c Could not schedule restart: ${deps.escapeHtml(msg)}`,
|
|
71290
|
-
html: true
|
|
71291
|
-
};
|
|
71292
|
-
}
|
|
71293
|
-
return {
|
|
71294
|
-
text: [
|
|
71295
|
-
`Switching from \`${deps.escapeHtml(currentSession)}\` back to Claude \u2014 restarting session cleanly. Claude will be ready in ~30s.`,
|
|
71296
|
-
PERSIST_NOTE
|
|
71297
|
-
].join(`
|
|
71298
|
-
`),
|
|
71299
|
-
html: true
|
|
71300
|
-
};
|
|
71780
|
+
if (model.toLowerCase() === "default") {
|
|
71781
|
+
return scheduleDefaultRelaunchReply(deps, "user: /model default (revert relaunch)");
|
|
71301
71782
|
}
|
|
71302
|
-
|
|
71303
|
-
|
|
71304
|
-
|
|
71305
|
-
|
|
71306
|
-
|
|
71307
|
-
|
|
71308
|
-
|
|
71309
|
-
|
|
71310
|
-
};
|
|
71311
|
-
}
|
|
71312
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
71313
|
-
return {
|
|
71314
|
-
text: `\u274c Could not schedule model switch: ${deps.escapeHtml(msg)}`,
|
|
71315
|
-
html: true
|
|
71316
|
-
};
|
|
71317
|
-
}
|
|
71783
|
+
return scheduleRelaunchReply(deps, model, `user: /model ${model} (session-only relaunch)`);
|
|
71784
|
+
}
|
|
71785
|
+
function switchingLine(deps, model) {
|
|
71786
|
+
const friendly = isSrModel(model) ? srFriendlyLabel(model) : model;
|
|
71787
|
+
return `\uD83D\uDD04 Switching to \`${deps.escapeHtml(friendly)}\` \u2014 relaunching the session (~30s).`;
|
|
71788
|
+
}
|
|
71789
|
+
function relaunchErrorReply(deps, model, err) {
|
|
71790
|
+
if (isRestartInFlight(err)) {
|
|
71318
71791
|
return {
|
|
71319
|
-
text:
|
|
71320
|
-
`Switching to \`${deps.escapeHtml(model)}\` \u2014 restarting session (~30s).`,
|
|
71321
|
-
PERSIST_NOTE
|
|
71322
|
-
].join(`
|
|
71323
|
-
`),
|
|
71792
|
+
text: `\u23f3 A restart is already in flight \u2014 your switch to \`${deps.escapeHtml(model)}\` will apply as it completes (~15s).`,
|
|
71324
71793
|
html: true
|
|
71325
71794
|
};
|
|
71326
71795
|
}
|
|
71327
|
-
const
|
|
71328
|
-
|
|
71796
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
71797
|
+
return { text: `\u274c Could not schedule model switch: ${deps.escapeHtml(msg)}`, html: true };
|
|
71798
|
+
}
|
|
71799
|
+
async function scheduleRelaunchReply(deps, model, reason) {
|
|
71329
71800
|
try {
|
|
71330
|
-
|
|
71331
|
-
successPattern: MODEL_SWITCH_CONFIRMATION_PREFIX,
|
|
71332
|
-
errorPattern: MODEL_SWITCH_ERROR_RE,
|
|
71333
|
-
settleBeforeSendMs: 1500
|
|
71334
|
-
});
|
|
71801
|
+
await deps.scheduleModelRelaunch(model, reason);
|
|
71335
71802
|
} catch (err) {
|
|
71336
|
-
|
|
71337
|
-
return {
|
|
71338
|
-
text: `\u274c ${verbHtml} \u2014 inject failed: ${deps.escapeHtml(msg)}`,
|
|
71339
|
-
html: true
|
|
71340
|
-
};
|
|
71803
|
+
return relaunchErrorReply(deps, model, err);
|
|
71341
71804
|
}
|
|
71342
|
-
|
|
71343
|
-
|
|
71344
|
-
|
|
71345
|
-
|
|
71346
|
-
|
|
71347
|
-
|
|
71348
|
-
|
|
71349
|
-
|
|
71350
|
-
...result.truncated ? ["_truncated_"] : [],
|
|
71351
|
-
PERSIST_NOTE
|
|
71352
|
-
].join(`
|
|
71353
|
-
`),
|
|
71354
|
-
html: true
|
|
71355
|
-
};
|
|
71356
|
-
}
|
|
71357
|
-
const confirmed = sessionModelFromConfirmation(confirmation) ?? model;
|
|
71358
|
-
return {
|
|
71359
|
-
text: [
|
|
71360
|
-
`${verbHtml}`,
|
|
71361
|
-
deps.preBlock(confirmation),
|
|
71362
|
-
...result.truncated ? ["_truncated_"] : [],
|
|
71363
|
-
PERSIST_NOTE
|
|
71364
|
-
].join(`
|
|
71365
|
-
`),
|
|
71366
|
-
html: true,
|
|
71367
|
-
selectedModel: confirmed
|
|
71368
|
-
};
|
|
71369
|
-
}
|
|
71370
|
-
const errLine = result.outcome === "ok" ? modelSwitchErrorLine(result.output) : null;
|
|
71371
|
-
if (errLine) {
|
|
71372
|
-
return {
|
|
71373
|
-
text: [
|
|
71374
|
-
`\u274c ${verbHtml} \u2014 the switch did not take:`,
|
|
71375
|
-
deps.preBlock(errLine),
|
|
71376
|
-
"Check `/model` for a valid, available model."
|
|
71377
|
-
].join(`
|
|
71378
|
-
`),
|
|
71379
|
-
html: true
|
|
71380
|
-
};
|
|
71381
|
-
}
|
|
71382
|
-
const optimisticLabel = optimisticModelRecordLabel(model);
|
|
71383
|
-
return {
|
|
71384
|
-
text: [
|
|
71385
|
-
`${verbHtml} \u2014 sent, but couldn't read a confirmation line. \`/status\` will show the live model once it's confirmed.`,
|
|
71386
|
-
PERSIST_NOTE
|
|
71387
|
-
].join(`
|
|
71388
|
-
`),
|
|
71389
|
-
html: true,
|
|
71390
|
-
selectedModel: optimisticLabel,
|
|
71391
|
-
optimistic: true
|
|
71392
|
-
};
|
|
71393
|
-
}
|
|
71394
|
-
if (result.errorCode === "session_missing") {
|
|
71395
|
-
return {
|
|
71396
|
-
text: "\u274c tmux session not found \u2014 the agent must be running under the tmux supervisor (the default). Remove `experimental.legacy_pty: true` if set.",
|
|
71397
|
-
html: true
|
|
71398
|
-
};
|
|
71805
|
+
return { text: [switchingLine(deps, model), PERSIST_NOTE].join(`
|
|
71806
|
+
`), html: true };
|
|
71807
|
+
}
|
|
71808
|
+
async function scheduleDefaultRelaunchReply(deps, reason) {
|
|
71809
|
+
try {
|
|
71810
|
+
await deps.scheduleModelDefaultRelaunch(reason);
|
|
71811
|
+
} catch (err) {
|
|
71812
|
+
return relaunchErrorReply(deps, "default", err);
|
|
71399
71813
|
}
|
|
71400
71814
|
return {
|
|
71401
|
-
text:
|
|
71815
|
+
text: [
|
|
71816
|
+
"\uD83D\uDD04 Reverting to the configured default model \u2014 relaunching the session (~30s).",
|
|
71817
|
+
PERSIST_NOTE
|
|
71818
|
+
].join(`
|
|
71819
|
+
`),
|
|
71402
71820
|
html: true
|
|
71403
71821
|
};
|
|
71404
71822
|
}
|
|
@@ -71451,15 +71869,6 @@ function expandSrAlias(arg) {
|
|
|
71451
71869
|
function srFriendlyLabel(srName) {
|
|
71452
71870
|
return SR_MODEL_LABELS[srName] ?? srName.replace(/^sr-/, "").replace(/-/g, " ");
|
|
71453
71871
|
}
|
|
71454
|
-
function optimisticModelRecordLabel(token) {
|
|
71455
|
-
if (isSrModel(token))
|
|
71456
|
-
return token;
|
|
71457
|
-
const lower = token.toLowerCase();
|
|
71458
|
-
if (MODEL_ALIASES.includes(lower)) {
|
|
71459
|
-
return lower.charAt(0).toUpperCase() + lower.slice(1);
|
|
71460
|
-
}
|
|
71461
|
-
return token;
|
|
71462
|
-
}
|
|
71463
71872
|
function classifyDiscoveredOptions(options) {
|
|
71464
71873
|
return {
|
|
71465
71874
|
claude: options.filter((o) => !o.label.startsWith("sr-") && !o.label.includes("/") && (/^[A-Z]/.test(o.label) || o.label.startsWith("claude-"))),
|
|
@@ -71467,7 +71876,19 @@ function classifyDiscoveredOptions(options) {
|
|
|
71467
71876
|
};
|
|
71468
71877
|
}
|
|
71469
71878
|
function modelSelectCallbackData(label) {
|
|
71470
|
-
|
|
71879
|
+
const token = canonicalClaudeToken(label);
|
|
71880
|
+
if (token)
|
|
71881
|
+
return `${MODEL_CALLBACK_SELECT}${token}`;
|
|
71882
|
+
if (/^default\b/i.test(label.trim()))
|
|
71883
|
+
return `${MODEL_CALLBACK_SELECT}default`;
|
|
71884
|
+
return MODEL_CALLBACK_SELECT;
|
|
71885
|
+
}
|
|
71886
|
+
function isRecognizedSwitchToken(token) {
|
|
71887
|
+
if (token.toLowerCase() === "default")
|
|
71888
|
+
return true;
|
|
71889
|
+
if (isSrModel(token))
|
|
71890
|
+
return true;
|
|
71891
|
+
return canonicalClaudeToken(token) !== null;
|
|
71471
71892
|
}
|
|
71472
71893
|
var BUSY_REFUSAL_TEXT = "\u23f3 The agent is mid-turn \u2014 the model picker needs an idle prompt. Your tap was not applied.";
|
|
71473
71894
|
function isBusyRefusalText(text4) {
|
|
@@ -71617,75 +72038,28 @@ async function handleModelMenuCallback(data, deps) {
|
|
|
71617
72038
|
if (data === MODEL_CALLBACK_PAGE_MAIN) {
|
|
71618
72039
|
return { answer: "Back", reply: await buildModelMenu(deps, "main") };
|
|
71619
72040
|
}
|
|
71620
|
-
if (data.startsWith(MODEL_CALLBACK_ALIAS)) {
|
|
71621
|
-
const alias = data.slice(MODEL_CALLBACK_ALIAS.length);
|
|
71622
|
-
if (!isValidModelArg(alias)) {
|
|
71623
|
-
return { answer: "Invalid model name", reply: await buildModelMenu(deps) };
|
|
71624
|
-
}
|
|
71625
|
-
if (deps.isBusy()) {
|
|
71626
|
-
return {
|
|
71627
|
-
answer: "\u23f3 Agent is mid-turn \u2014 tap again when it\u2019s idle",
|
|
71628
|
-
reply: { text: BUSY_REFUSAL_TEXT, html: true },
|
|
71629
|
-
toastOnly: true,
|
|
71630
|
-
busyRefusal: true
|
|
71631
|
-
};
|
|
71632
|
-
}
|
|
71633
|
-
let aliasResult;
|
|
71634
|
-
try {
|
|
71635
|
-
aliasResult = await deps.inject(deps.getAgentName(), `/model ${alias}`, {
|
|
71636
|
-
successPattern: MODEL_SWITCH_CONFIRMATION_PREFIX,
|
|
71637
|
-
errorPattern: MODEL_SWITCH_ERROR_RE,
|
|
71638
|
-
settleBeforeSendMs: 1500
|
|
71639
|
-
});
|
|
71640
|
-
} catch (err) {
|
|
71641
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
71642
|
-
return {
|
|
71643
|
-
answer: "Switch failed",
|
|
71644
|
-
reply: await menuWithBanner(deps, `\u274c Switch to **${deps.escapeHtml(alias)}** failed: ${deps.escapeHtml(msg)}`)
|
|
71645
|
-
};
|
|
71646
|
-
}
|
|
71647
|
-
if (aliasResult.outcome === "ok" || aliasResult.outcome === "ok_no_output") {
|
|
71648
|
-
const confirmation = aliasResult.outcome === "ok" ? modelSwitchConfirmationLine(aliasResult.output) : null;
|
|
71649
|
-
if (confirmation) {
|
|
71650
|
-
const kept = isKeptModelConfirmation(confirmation);
|
|
71651
|
-
return {
|
|
71652
|
-
answer: confirmation,
|
|
71653
|
-
reply: await menuWithBannerStatic(deps, `\u2705 ${deps.escapeHtml(confirmation)}`),
|
|
71654
|
-
...kept ? {} : {
|
|
71655
|
-
selectedModel: sessionModelFromConfirmation(confirmation) ?? optimisticModelRecordLabel(alias),
|
|
71656
|
-
selectedModelToken: alias
|
|
71657
|
-
}
|
|
71658
|
-
};
|
|
71659
|
-
}
|
|
71660
|
-
const aliasErr = aliasResult.outcome === "ok" ? modelSwitchErrorLine(aliasResult.output) : null;
|
|
71661
|
-
if (aliasErr) {
|
|
71662
|
-
return {
|
|
71663
|
-
answer: "Switch failed",
|
|
71664
|
-
reply: await menuWithBanner(deps, `\u274c Switch to **${deps.escapeHtml(alias)}** did not take: ${deps.escapeHtml(aliasErr)}`)
|
|
71665
|
-
};
|
|
71666
|
-
}
|
|
71667
|
-
const optimisticLabel = optimisticModelRecordLabel(alias);
|
|
71668
|
-
return {
|
|
71669
|
-
answer: `Sent /model ${alias} \u2014 check /status`,
|
|
71670
|
-
reply: await menuWithBannerStatic(deps, `Sent \`/model ${deps.escapeHtml(alias)}\` \u2014 couldn\u2019t read a confirmation line. \`/status\` will show the live model once it\u2019s confirmed.`),
|
|
71671
|
-
selectedModel: optimisticLabel,
|
|
71672
|
-
selectedModelToken: alias
|
|
71673
|
-
};
|
|
71674
|
-
}
|
|
71675
|
-
return {
|
|
71676
|
-
answer: "Switch failed",
|
|
71677
|
-
reply: await menuWithBanner(deps, `\u274c Switch to **${deps.escapeHtml(alias)}** failed \u2014 agent may be mid-turn`)
|
|
71678
|
-
};
|
|
71679
|
-
}
|
|
71680
72041
|
if (data === MODEL_CALLBACK_HEADER) {
|
|
71681
72042
|
return { answer: "Tap a model in this section to switch", reply: { text: "", html: true }, toastOnly: true };
|
|
71682
72043
|
}
|
|
71683
|
-
if (data.startsWith(MODEL_CALLBACK_SR)) {
|
|
71684
|
-
|
|
71685
|
-
|
|
71686
|
-
if (
|
|
72044
|
+
if (data.startsWith(MODEL_CALLBACK_ALIAS) || data.startsWith(MODEL_CALLBACK_SR) || data.startsWith(MODEL_CALLBACK_SELECT)) {
|
|
72045
|
+
let token;
|
|
72046
|
+
let label;
|
|
72047
|
+
if (data.startsWith(MODEL_CALLBACK_ALIAS)) {
|
|
72048
|
+
token = data.slice(MODEL_CALLBACK_ALIAS.length);
|
|
72049
|
+
label = token;
|
|
72050
|
+
} else if (data.startsWith(MODEL_CALLBACK_SR)) {
|
|
72051
|
+
token = data.slice(MODEL_CALLBACK_SR.length);
|
|
72052
|
+
label = srFriendlyLabel(token);
|
|
72053
|
+
} else {
|
|
72054
|
+
token = data.slice(MODEL_CALLBACK_SELECT.length);
|
|
72055
|
+
label = token;
|
|
72056
|
+
}
|
|
72057
|
+
if (!isValidModelArg(token)) {
|
|
71687
72058
|
return { answer: "Invalid model name", reply: await buildModelMenu(deps) };
|
|
71688
72059
|
}
|
|
72060
|
+
if (!isRecognizedSwitchToken(token)) {
|
|
72061
|
+
return { answer: "Model list changed \u2014 menu refreshed", reply: await buildModelMenu(deps) };
|
|
72062
|
+
}
|
|
71689
72063
|
if (deps.isBusy()) {
|
|
71690
72064
|
return {
|
|
71691
72065
|
answer: "\u23f3 Agent is mid-turn \u2014 tap again when it\u2019s idle",
|
|
@@ -71694,87 +72068,38 @@ async function handleModelMenuCallback(data, deps) {
|
|
|
71694
72068
|
busyRefusal: true
|
|
71695
72069
|
};
|
|
71696
72070
|
}
|
|
71697
|
-
|
|
71698
|
-
|
|
71699
|
-
|
|
71700
|
-
|
|
72071
|
+
return menuRelaunchOutcome(deps, token, label);
|
|
72072
|
+
}
|
|
72073
|
+
return { answer: "Unknown action", reply: await buildModelMenu(deps) };
|
|
72074
|
+
}
|
|
72075
|
+
async function menuRelaunchOutcome(deps, token, label) {
|
|
72076
|
+
const isDefault = token.toLowerCase() === "default";
|
|
72077
|
+
try {
|
|
72078
|
+
if (isDefault) {
|
|
72079
|
+
await deps.scheduleModelDefaultRelaunch("user: /model default (revert relaunch, menu)");
|
|
72080
|
+
} else {
|
|
72081
|
+
await deps.scheduleModelRelaunch(token, `user: /model ${token} (session-only relaunch, menu)`);
|
|
72082
|
+
}
|
|
72083
|
+
} catch (err) {
|
|
72084
|
+
if (isRestartInFlight(err)) {
|
|
71701
72085
|
return {
|
|
71702
|
-
answer: "
|
|
71703
|
-
reply: await menuWithBannerStatic(deps, `\
|
|
72086
|
+
answer: "A restart is already in flight (~15s)",
|
|
72087
|
+
reply: await menuWithBannerStatic(deps, `\u23f3 A restart is already in flight \u2014 your switch to **${deps.escapeHtml(label)}** will apply as it completes (~15s).`)
|
|
71704
72088
|
};
|
|
71705
72089
|
}
|
|
72090
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
71706
72091
|
return {
|
|
71707
|
-
answer:
|
|
71708
|
-
reply: await menuWithBannerStatic(deps, `\
|
|
71709
|
-
${PERSIST_NOTE}`),
|
|
71710
|
-
selectedModel: srName
|
|
71711
|
-
};
|
|
71712
|
-
}
|
|
71713
|
-
if (!data.startsWith(MODEL_CALLBACK_SELECT)) {
|
|
71714
|
-
return { answer: "Unknown action", reply: await buildModelMenu(deps) };
|
|
71715
|
-
}
|
|
71716
|
-
if (deps.isBusy()) {
|
|
71717
|
-
return {
|
|
71718
|
-
answer: "\u23f3 Agent is mid-turn \u2014 tap again when it\u2019s idle",
|
|
71719
|
-
reply: { text: BUSY_REFUSAL_TEXT, html: true },
|
|
71720
|
-
toastOnly: true,
|
|
71721
|
-
busyRefusal: true
|
|
71722
|
-
};
|
|
71723
|
-
}
|
|
71724
|
-
const tag = data.slice(MODEL_CALLBACK_SELECT.length);
|
|
71725
|
-
const discovered = await deps.discover(deps.getAgentName());
|
|
71726
|
-
if (!discovered.ok) {
|
|
71727
|
-
return {
|
|
71728
|
-
answer: "Picker unavailable",
|
|
71729
|
-
reply: await menuWithBanner(deps, `\u274c Could not open the model picker: ${deps.escapeHtml(discovered.reason)}`)
|
|
71730
|
-
};
|
|
71731
|
-
}
|
|
71732
|
-
const target = discovered.options.find((o) => labelTag(o.label) === tag);
|
|
71733
|
-
if (!target) {
|
|
71734
|
-
const fresh = await buildModelMenu(deps);
|
|
71735
|
-
return { answer: "Model list changed \u2014 menu refreshed", reply: fresh };
|
|
71736
|
-
}
|
|
71737
|
-
const result = await deps.select(deps.getAgentName(), target.label);
|
|
71738
|
-
if (!result.ok) {
|
|
71739
|
-
return {
|
|
71740
|
-
answer: "Switch failed \u2014 see the menu",
|
|
71741
|
-
reply: await menuWithBanner(deps, `\u274c Switch to **${deps.escapeHtml(target.label)}** failed: ${deps.escapeHtml(result.reason)}`)
|
|
71742
|
-
};
|
|
71743
|
-
}
|
|
71744
|
-
if (isKeptModelConfirmation(result.confirmation)) {
|
|
71745
|
-
return {
|
|
71746
|
-
answer: deps.escapeHtml(result.confirmation),
|
|
71747
|
-
reply: await menuWithBanner(deps, `\u2705 ${deps.escapeHtml(result.confirmation)}`)
|
|
72092
|
+
answer: "Switch failed",
|
|
72093
|
+
reply: await menuWithBannerStatic(deps, `\u274c Switch to **${deps.escapeHtml(label)}** failed: ${deps.escapeHtml(msg)}`)
|
|
71748
72094
|
};
|
|
71749
72095
|
}
|
|
71750
|
-
const
|
|
71751
|
-
const selectedModel = sessionModelFromConfirmation(result.confirmation) ?? token ?? undefined;
|
|
71752
|
-
const clearedDefault = token == null && /^default\b/i.test(target.label.trim());
|
|
72096
|
+
const friendly = isDefault ? "the configured default" : label;
|
|
71753
72097
|
return {
|
|
71754
|
-
answer:
|
|
71755
|
-
reply: await
|
|
71756
|
-
|
|
71757
|
-
...token ? { selectedModelToken: token } : {},
|
|
71758
|
-
...clearedDefault ? { clearedDefault: true } : {}
|
|
72098
|
+
answer: `Switching to ${isDefault ? "default" : label} \u2014 relaunching (~30s)`,
|
|
72099
|
+
reply: await menuWithBannerStatic(deps, `\uD83D\uDD04 Switching session to **${deps.escapeHtml(friendly)}** \u2014 relaunching (~30s).
|
|
72100
|
+
${PERSIST_NOTE}`)
|
|
71759
72101
|
};
|
|
71760
72102
|
}
|
|
71761
|
-
function isSrToClaudeTransition(prevModel, nextModel) {
|
|
71762
|
-
return !!prevModel?.startsWith("sr-") && !nextModel.startsWith("sr-");
|
|
71763
|
-
}
|
|
71764
|
-
function modelSwitchConfirmationLine(output) {
|
|
71765
|
-
const line = output.split(`
|
|
71766
|
-
`).map((l) => l.trim()).find((l) => MODEL_SWITCH_CONFIRMATION_PREFIX.test(l));
|
|
71767
|
-
return line && line.length > 0 ? line : null;
|
|
71768
|
-
}
|
|
71769
|
-
var MODEL_SWITCH_ERROR_RE = /^\s*[\u23fa\u25cf\u2022>\u23bf-]?\s*(?:Error:\s*)?(?:Model(?:\s+'[^']+')?\s+not found|Invalid model|Unknown model|No such model|(?:[\w'\u2019.\-]+\s+){0,4}(?:(?:is |are )?(?:not available|unavailable|not enabled|not supported)|access denied|requires\b[^\n]{0,40}\b(?:subscription|plan)|no access)\b)/i;
|
|
71770
|
-
function modelSwitchErrorLine(output) {
|
|
71771
|
-
const line = output.split(`
|
|
71772
|
-
`).map((l) => l.trim()).find((l) => MODEL_SWITCH_ERROR_RE.test(l));
|
|
71773
|
-
return line && line.length > 0 ? line : null;
|
|
71774
|
-
}
|
|
71775
|
-
function isKeptModelConfirmation(confirmation) {
|
|
71776
|
-
return /^\s*[\u23fa\u25cf\u2022>\u23bf-]?\s*Kept model as\b/i.test(confirmation.trim());
|
|
71777
|
-
}
|
|
71778
72103
|
function canonicalClaudeToken(label) {
|
|
71779
72104
|
const l = label.trim();
|
|
71780
72105
|
if (l.toLowerCase().startsWith("claude-"))
|
|
@@ -71789,21 +72114,6 @@ function canonicalClaudeToken(label) {
|
|
|
71789
72114
|
function isRestartInFlight(err) {
|
|
71790
72115
|
return !!err && typeof err === "object" && err.code === "restart_in_flight";
|
|
71791
72116
|
}
|
|
71792
|
-
var MODEL_SWITCH_CONFIRMATION_PREFIX = /^\s*[\u23fa\u25cf\u2022>\u23bf-]?\s*(?:Set model to|Switched to|Kept model as)\b/i;
|
|
71793
|
-
function sessionModelFromConfirmation(confirmation) {
|
|
71794
|
-
const m = /(?:Set model to|Switched to)\s+(.+?)(?:\s+for (?:this|the) session|\s+and saved\b|\s*\(|\s*$)/i.exec(confirmation.trim());
|
|
71795
|
-
const name = m?.[1]?.trim();
|
|
71796
|
-
return name && name.length > 0 ? name : null;
|
|
71797
|
-
}
|
|
71798
|
-
async function menuWithBanner(deps, banner) {
|
|
71799
|
-
const fresh = await buildModelMenu(deps);
|
|
71800
|
-
return {
|
|
71801
|
-
text: [banner, "", fresh.text].join(`
|
|
71802
|
-
`),
|
|
71803
|
-
html: true,
|
|
71804
|
-
...fresh.keyboard ? { keyboard: fresh.keyboard } : {}
|
|
71805
|
-
};
|
|
71806
|
-
}
|
|
71807
72117
|
async function menuWithBannerStatic(deps, banner) {
|
|
71808
72118
|
const v1 = await handleModelCommand({ kind: "show" }, deps);
|
|
71809
72119
|
return {
|
|
@@ -71827,6 +72137,7 @@ var MODEL_CALLBACK_ALIAS2 = "mdl:alias:";
|
|
|
71827
72137
|
// gateway/session-model-file.ts
|
|
71828
72138
|
var SESSION_MODEL_FILE = ".session-model";
|
|
71829
72139
|
var CONFIGURED_DEFAULT_MODEL_FILE = ".configured-default-model";
|
|
72140
|
+
var SESSION_MODEL_BOOT_ATTEMPTS_FILE = ".session-model-boot-attempts";
|
|
71830
72141
|
function atomicWrite(path2, content3) {
|
|
71831
72142
|
const tmp = `${path2}.tmp-${process.pid}-${Date.now()}`;
|
|
71832
72143
|
writeFileSync21(tmp, content3, "utf8");
|
|
@@ -71858,6 +72169,14 @@ function clearSessionModelFile(agentDir) {
|
|
|
71858
72169
|
rmSync4(join30(agentDir, SESSION_MODEL_FILE), { force: true });
|
|
71859
72170
|
} catch {}
|
|
71860
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
|
+
}
|
|
71861
72180
|
function restoreSessionModelFileRaw(agentDir, raw) {
|
|
71862
72181
|
if (raw == null) {
|
|
71863
72182
|
clearSessionModelFile(agentDir);
|
|
@@ -72215,70 +72534,6 @@ async function discoverModels(agentName3, opts = {}) {
|
|
|
72215
72534
|
return { ok: true, options: parsed.options, currentLabel: current?.label ?? null, ...tail };
|
|
72216
72535
|
});
|
|
72217
72536
|
}
|
|
72218
|
-
async function selectModel(agentName3, targetLabel, opts = {}) {
|
|
72219
|
-
const io = makeIo(agentName3, opts);
|
|
72220
|
-
if (!io.runner.hasSession(io.socket, io.session)) {
|
|
72221
|
-
return { ok: false, reason: "tmux session not found" };
|
|
72222
|
-
}
|
|
72223
|
-
return withPaneLock(`${io.socket}:${io.session}`, async () => {
|
|
72224
|
-
io.startedAt = Date.now();
|
|
72225
|
-
let selected = false;
|
|
72226
|
-
try {
|
|
72227
|
-
const parsed = await openPickerWithRetry(io);
|
|
72228
|
-
if (!parsed || !parsed.footerSeen) {
|
|
72229
|
-
return { ok: false, reason: "picker did not render \u2014 agent may be mid-turn" };
|
|
72230
|
-
}
|
|
72231
|
-
const target = parsed.options.find((o) => o.label === targetLabel);
|
|
72232
|
-
if (!target) {
|
|
72233
|
-
const have = parsed.options.map((o) => o.label).join(", ");
|
|
72234
|
-
return { ok: false, reason: `option "${targetLabel}" not offered (have: ${have})` };
|
|
72235
|
-
}
|
|
72236
|
-
if (parsed.cursorIndex < 0) {
|
|
72237
|
-
return { ok: false, reason: "cursor marker not visible \u2014 refusing blind navigation" };
|
|
72238
|
-
}
|
|
72239
|
-
const delta = target.index - parsed.cursorIndex;
|
|
72240
|
-
const key = delta > 0 ? "Down" : "Up";
|
|
72241
|
-
for (let i = 0;i < Math.abs(delta); i++) {
|
|
72242
|
-
if (expired(io))
|
|
72243
|
-
return { ok: false, reason: "timed out navigating picker" };
|
|
72244
|
-
sendKey(io, key);
|
|
72245
|
-
await io.sleep(Math.min(io.stepMs, 250));
|
|
72246
|
-
}
|
|
72247
|
-
await io.sleep(io.stepMs);
|
|
72248
|
-
const verifyPane = io.runner.capture(io.socket, io.session) ?? "";
|
|
72249
|
-
const verify = parseModelPicker(verifyPane);
|
|
72250
|
-
const atRow = verify?.options.find((o) => o.index === verify.cursorIndex);
|
|
72251
|
-
if (!verify || !atRow || atRow.label !== targetLabel) {
|
|
72252
|
-
return {
|
|
72253
|
-
ok: false,
|
|
72254
|
-
reason: `cursor verification failed (on "${atRow?.label ?? "?"}", wanted "${targetLabel}")`
|
|
72255
|
-
};
|
|
72256
|
-
}
|
|
72257
|
-
sendLiteral(io, "s");
|
|
72258
|
-
selected = true;
|
|
72259
|
-
await io.sleep(io.stepMs);
|
|
72260
|
-
const after = io.runner.capture(io.socket, io.session) ?? "";
|
|
72261
|
-
const confirmation = extractConfirmation(after) ?? `Switched to ${targetLabel} (session)`;
|
|
72262
|
-
return { ok: true, confirmation };
|
|
72263
|
-
} catch (err) {
|
|
72264
|
-
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
72265
|
-
} finally {
|
|
72266
|
-
if (!selected) {
|
|
72267
|
-
await dismissOrWarn(io, "select");
|
|
72268
|
-
}
|
|
72269
|
-
}
|
|
72270
|
-
});
|
|
72271
|
-
}
|
|
72272
|
-
function extractConfirmation(pane) {
|
|
72273
|
-
const lines = pane.split(`
|
|
72274
|
-
`);
|
|
72275
|
-
for (let i = lines.length - 1;i >= 0; i--) {
|
|
72276
|
-
const t = lines[i].replace(/^\s*\u23bf\s*/, "").trim();
|
|
72277
|
-
if (/^(Set model to|Kept model as|Switched to)/i.test(t))
|
|
72278
|
-
return t;
|
|
72279
|
-
}
|
|
72280
|
-
return null;
|
|
72281
|
-
}
|
|
72282
72537
|
|
|
72283
72538
|
// ../src/agents/scaffold.ts
|
|
72284
72539
|
import { join as join33, resolve as resolve6 } from "node:path";
|
|
@@ -72295,14 +72550,7 @@ init_merge();
|
|
|
72295
72550
|
|
|
72296
72551
|
// ../src/agents/scaffold.ts
|
|
72297
72552
|
init_merge();
|
|
72298
|
-
|
|
72299
|
-
// ../src/config/timezone.ts
|
|
72300
|
-
var CONTAINER_DEFAULT_UTC_ZONES = new Set([
|
|
72301
|
-
"UTC",
|
|
72302
|
-
"Etc/UTC",
|
|
72303
|
-
"Etc/Universal",
|
|
72304
|
-
"Universal"
|
|
72305
|
-
]);
|
|
72553
|
+
init_timezone();
|
|
72306
72554
|
|
|
72307
72555
|
// ../src/cli/agent-config.ts
|
|
72308
72556
|
import { join as join31 } from "node:path";
|
|
@@ -73052,6 +73300,7 @@ init_schema();
|
|
|
73052
73300
|
init_paths();
|
|
73053
73301
|
init_overlay_loader();
|
|
73054
73302
|
init_merge();
|
|
73303
|
+
init_timezone();
|
|
73055
73304
|
var import_yaml4 = __toESM(require_dist(), 1);
|
|
73056
73305
|
import { readFileSync as readFileSync27, existsSync as existsSync27 } from "node:fs";
|
|
73057
73306
|
import { homedir as homedir11 } from "node:os";
|
|
@@ -73179,8 +73428,30 @@ function loadConfig2(configPath) {
|
|
|
73179
73428
|
if (notionIssues.length > 0) {
|
|
73180
73429
|
throw new ConfigError2(`Invalid notion_workspace configuration in ${filePath}`, notionIssues);
|
|
73181
73430
|
}
|
|
73431
|
+
validateAllTimezones2(config, filePath);
|
|
73182
73432
|
return config;
|
|
73183
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
|
+
}
|
|
73184
73455
|
function validateAllCronTopicAliases2(config, filePath) {
|
|
73185
73456
|
const issues = [];
|
|
73186
73457
|
for (const [agentName3, agentRaw] of Object.entries(config.agents)) {
|
|
@@ -77548,17 +77819,20 @@ function computeTurnStatus(turn) {
|
|
|
77548
77819
|
return turn.finalAnswerDelivered ? "complete" : "no_reply";
|
|
77549
77820
|
}
|
|
77550
77821
|
}
|
|
77551
|
-
function
|
|
77822
|
+
function backstopSendOutcomeGated(args) {
|
|
77552
77823
|
if (args.threw)
|
|
77553
77824
|
return "failed";
|
|
77554
77825
|
if (args.chunkCount === 0)
|
|
77555
77826
|
return "failed";
|
|
77556
|
-
|
|
77827
|
+
const freshCount = args.sentIds.filter((id) => args.cardMessageId == null || id !== args.cardMessageId).length;
|
|
77828
|
+
if (freshCount === 0)
|
|
77829
|
+
return "failed";
|
|
77830
|
+
if (freshCount < args.chunkCount)
|
|
77557
77831
|
return "failed";
|
|
77558
77832
|
return "delivered";
|
|
77559
77833
|
}
|
|
77560
|
-
function
|
|
77561
|
-
const outcome =
|
|
77834
|
+
function finalizeBackstopSendGated(turn, send) {
|
|
77835
|
+
const outcome = backstopSendOutcomeGated(send);
|
|
77562
77836
|
turn.deliveryOutcome = outcome;
|
|
77563
77837
|
return outcome;
|
|
77564
77838
|
}
|
|
@@ -77573,6 +77847,120 @@ function buildTurnRecord(turn, endedAt) {
|
|
|
77573
77847
|
};
|
|
77574
77848
|
}
|
|
77575
77849
|
|
|
77850
|
+
// gateway/backstop-delivery.ts
|
|
77851
|
+
class BackstopDeliveryLedger {
|
|
77852
|
+
latched = new Set;
|
|
77853
|
+
chunks = new Map;
|
|
77854
|
+
pending = new Map;
|
|
77855
|
+
claim(turnId) {
|
|
77856
|
+
if (this.latched.has(turnId))
|
|
77857
|
+
return false;
|
|
77858
|
+
this.latched.add(turnId);
|
|
77859
|
+
return true;
|
|
77860
|
+
}
|
|
77861
|
+
release(turnId) {
|
|
77862
|
+
this.latched.delete(turnId);
|
|
77863
|
+
}
|
|
77864
|
+
markPending(turnId, index2) {
|
|
77865
|
+
let set = this.pending.get(turnId);
|
|
77866
|
+
if (set == null) {
|
|
77867
|
+
set = new Set;
|
|
77868
|
+
this.pending.set(turnId, set);
|
|
77869
|
+
}
|
|
77870
|
+
set.add(index2);
|
|
77871
|
+
}
|
|
77872
|
+
recordChunk(turnId, index2, messageIds) {
|
|
77873
|
+
let m = this.chunks.get(turnId);
|
|
77874
|
+
if (m == null) {
|
|
77875
|
+
m = new Map;
|
|
77876
|
+
this.chunks.set(turnId, m);
|
|
77877
|
+
}
|
|
77878
|
+
m.set(index2, messageIds.slice());
|
|
77879
|
+
this.pending.get(turnId)?.delete(index2);
|
|
77880
|
+
}
|
|
77881
|
+
hasChunk(turnId, index2) {
|
|
77882
|
+
return (this.chunks.get(turnId)?.get(index2)?.length ?? 0) > 0;
|
|
77883
|
+
}
|
|
77884
|
+
sentIds(turnId) {
|
|
77885
|
+
const m = this.chunks.get(turnId);
|
|
77886
|
+
if (m == null)
|
|
77887
|
+
return [];
|
|
77888
|
+
const out = [];
|
|
77889
|
+
for (const index2 of Array.from(m.keys()).sort((a, b) => a - b)) {
|
|
77890
|
+
out.push(...m.get(index2) ?? []);
|
|
77891
|
+
}
|
|
77892
|
+
return out;
|
|
77893
|
+
}
|
|
77894
|
+
entries(turnId) {
|
|
77895
|
+
const m = this.chunks.get(turnId);
|
|
77896
|
+
if (m == null)
|
|
77897
|
+
return [];
|
|
77898
|
+
return Array.from(m.keys()).sort((a, b) => a - b).map((index2) => ({ index: index2, messageIds: (m.get(index2) ?? []).slice() }));
|
|
77899
|
+
}
|
|
77900
|
+
unsentIndices(turnId, chunkCount) {
|
|
77901
|
+
const out = [];
|
|
77902
|
+
for (let i = 0;i < chunkCount; i++) {
|
|
77903
|
+
if (!this.hasChunk(turnId, i))
|
|
77904
|
+
out.push(i);
|
|
77905
|
+
}
|
|
77906
|
+
return out;
|
|
77907
|
+
}
|
|
77908
|
+
clear(turnId) {
|
|
77909
|
+
this.latched.delete(turnId);
|
|
77910
|
+
this.chunks.delete(turnId);
|
|
77911
|
+
this.pending.delete(turnId);
|
|
77912
|
+
}
|
|
77913
|
+
}
|
|
77914
|
+
function backstopReceiptIds(sentIds, cardMessageId) {
|
|
77915
|
+
return sentIds.filter((id) => cardMessageId == null || id !== cardMessageId);
|
|
77916
|
+
}
|
|
77917
|
+
async function runBackstopDelivery(ledger, turnId, chunks, cardMessageId, deps, maxAttempts = 3) {
|
|
77918
|
+
const stderr = deps.stderr ?? (() => {});
|
|
77919
|
+
const chunkCount = chunks.length;
|
|
77920
|
+
let attempts = 0;
|
|
77921
|
+
for (let attempt = 1;attempt <= Math.max(1, maxAttempts); attempt++) {
|
|
77922
|
+
attempts = attempt;
|
|
77923
|
+
const resume = ledger.unsentIndices(turnId, chunkCount);
|
|
77924
|
+
if (resume.length === 0)
|
|
77925
|
+
break;
|
|
77926
|
+
if (attempt > 1) {
|
|
77927
|
+
stderr(`telegram gateway: backstop delivery retry ${attempt}/${maxAttempts} \u2014 ` + `resuming at unsent chunk(s) [${resume.join(", ")}] for turn ${turnId}
|
|
77928
|
+
`);
|
|
77929
|
+
}
|
|
77930
|
+
let attemptThrew = false;
|
|
77931
|
+
try {
|
|
77932
|
+
for (const i of resume) {
|
|
77933
|
+
if (ledger.hasChunk(turnId, i))
|
|
77934
|
+
continue;
|
|
77935
|
+
ledger.markPending(turnId, i);
|
|
77936
|
+
const ids = await deps.sendChunk(i, chunks[i]);
|
|
77937
|
+
ledger.recordChunk(turnId, i, ids);
|
|
77938
|
+
}
|
|
77939
|
+
} catch (err) {
|
|
77940
|
+
attemptThrew = true;
|
|
77941
|
+
stderr(`telegram gateway: backstop delivery attempt ${attempt}/${maxAttempts} failed: ` + `${err instanceof Error ? err.message : String(err)}
|
|
77942
|
+
`);
|
|
77943
|
+
}
|
|
77944
|
+
if (!attemptThrew && ledger.unsentIndices(turnId, chunkCount).length === 0)
|
|
77945
|
+
break;
|
|
77946
|
+
}
|
|
77947
|
+
const sentIds = ledger.sentIds(turnId);
|
|
77948
|
+
const delivered = chunkCount > 0 && ledger.unsentIndices(turnId, chunkCount).length === 0 && backstopReceiptIds(sentIds, cardMessageId).length > 0;
|
|
77949
|
+
const exhausted = !delivered;
|
|
77950
|
+
if (deps.recordOutbound && sentIds.length > 0) {
|
|
77951
|
+
const texts = [];
|
|
77952
|
+
const ids = [];
|
|
77953
|
+
for (const { index: index2, messageIds } of ledger.entries(turnId)) {
|
|
77954
|
+
for (const id of messageIds) {
|
|
77955
|
+
ids.push(id);
|
|
77956
|
+
texts.push(chunks[index2] ?? "");
|
|
77957
|
+
}
|
|
77958
|
+
}
|
|
77959
|
+
deps.recordOutbound(ids, texts);
|
|
77960
|
+
}
|
|
77961
|
+
return { sentIds, chunkCount, delivered, attempts, exhausted };
|
|
77962
|
+
}
|
|
77963
|
+
|
|
77576
77964
|
// gateway/inbound-delivery-confirm.ts
|
|
77577
77965
|
function createDeliveryQueue() {
|
|
77578
77966
|
return { pending: new Map };
|
|
@@ -84182,10 +84570,10 @@ function effectiveTurnAgeMs(markerAgeMs, turnStartedAt, now) {
|
|
|
84182
84570
|
}
|
|
84183
84571
|
|
|
84184
84572
|
// ../src/build-info.ts
|
|
84185
|
-
var VERSION = "0.18.
|
|
84186
|
-
var COMMIT_SHA = "
|
|
84187
|
-
var COMMIT_DATE = "2026-07-
|
|
84188
|
-
var LATEST_PR =
|
|
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;
|
|
84189
84577
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
84190
84578
|
|
|
84191
84579
|
// gateway/boot-version.ts
|
|
@@ -86216,6 +86604,7 @@ var TOPIC_ID = process.env.TELEGRAM_TOPIC_ID ? Number(process.env.TELEGRAM_TOPIC
|
|
|
86216
86604
|
var AGENT_ADMIN = process.env.SWITCHROOM_AGENT_ADMIN === "true";
|
|
86217
86605
|
var bot = new import_grammy13.Bot(TOKEN);
|
|
86218
86606
|
installTgPostLogger(bot);
|
|
86607
|
+
installUpdateTap(bot, (line) => process.stderr.write(line));
|
|
86219
86608
|
var lastGetUpdatesHeartbeatMs = Date.now();
|
|
86220
86609
|
bot.api.config.use(async (prev, method, payload, signal) => {
|
|
86221
86610
|
try {
|
|
@@ -86743,6 +87132,73 @@ var deferredDoneReactions = new DeferredDoneReactions({
|
|
|
86743
87132
|
});
|
|
86744
87133
|
var outboundDedup = new OutboundDedupCache;
|
|
86745
87134
|
var flushedTurnSupersede = new FlushedTurnSupersedeRegistry;
|
|
87135
|
+
var backstopDeliveryLedger = new BackstopDeliveryLedger;
|
|
87136
|
+
var BACKSTOP_DELIVERY_MAX_ATTEMPTS = (() => {
|
|
87137
|
+
const raw = Number(process.env.SWITCHROOM_BACKSTOP_DELIVERY_MAX_ATTEMPTS);
|
|
87138
|
+
return Number.isFinite(raw) && raw >= 1 ? Math.floor(raw) : 3;
|
|
87139
|
+
})();
|
|
87140
|
+
async function deliverAnswer(args) {
|
|
87141
|
+
const { chatId, turnId } = args;
|
|
87142
|
+
const rendered = addParagraphSpacers2(args.text);
|
|
87143
|
+
const chunks = splitMarkdownChunks2(rendered, RICH_MESSAGE_MAX_CHARS2);
|
|
87144
|
+
const deps = {
|
|
87145
|
+
sendRich: (opts, body, tid) => robustApiCall(() => bot.api.sendRichMessage(chatId, body, opts), { threadId: tid, chat_id: chatId, priorityClass: "critical" }),
|
|
87146
|
+
sendLiteral: (opts, txt, tid) => robustApiCall(() => bot.api.sendMessage(chatId, txt, opts), { threadId: tid, chat_id: chatId, priorityClass: "critical" }),
|
|
87147
|
+
sendLiteralRaw: (opts, txt) => bot.api.sendMessage(chatId, txt, opts),
|
|
87148
|
+
sendRichRaw: (opts, body) => bot.api.sendRichMessage(chatId, body, opts),
|
|
87149
|
+
editPreview: (mid, body, opts, tid) => robustApiCall(() => bot.api.editMessageText(chatId, mid, body, opts), { threadId: tid, chat_id: chatId, priorityClass: "critical", messageId: mid, editPayload: body }),
|
|
87150
|
+
richMessage: richMessage2,
|
|
87151
|
+
logOutbound,
|
|
87152
|
+
deleteStalePreview: async (id) => {
|
|
87153
|
+
await swallowingApiCall(() => bot.api.deleteMessage(chatId, id), { chat_id: chatId, verb: "deliverAnswer.deleteStalePreview" });
|
|
87154
|
+
},
|
|
87155
|
+
stderr: (s) => {
|
|
87156
|
+
process.stderr.write(s);
|
|
87157
|
+
}
|
|
87158
|
+
};
|
|
87159
|
+
let liveThreadId = args.threadId;
|
|
87160
|
+
const sendChunk = async (_chunkIndex, text5) => {
|
|
87161
|
+
const chunkIds = [];
|
|
87162
|
+
const res = await sendReplyChunks(deps, {
|
|
87163
|
+
chatId,
|
|
87164
|
+
chunks: [text5],
|
|
87165
|
+
literalText: false,
|
|
87166
|
+
suppressText: false,
|
|
87167
|
+
threadId: liveThreadId,
|
|
87168
|
+
previewMessageId: null,
|
|
87169
|
+
sentIds: chunkIds,
|
|
87170
|
+
buildSendOpts: (_i, _isLast, tid) => ({
|
|
87171
|
+
...tid != null ? { message_thread_id: tid } : {},
|
|
87172
|
+
link_preview_options: { is_disabled: true }
|
|
87173
|
+
}),
|
|
87174
|
+
buildPreviewEditOpts: () => ({})
|
|
87175
|
+
});
|
|
87176
|
+
liveThreadId = res.threadId;
|
|
87177
|
+
return chunkIds;
|
|
87178
|
+
};
|
|
87179
|
+
const result = await runBackstopDelivery(backstopDeliveryLedger, turnId, chunks, args.cardMessageId, {
|
|
87180
|
+
sendChunk,
|
|
87181
|
+
recordOutbound: HISTORY_ENABLED ? (messageIds, texts) => {
|
|
87182
|
+
try {
|
|
87183
|
+
recordOutbound({
|
|
87184
|
+
chat_id: chatId,
|
|
87185
|
+
thread_id: args.threadId ?? null,
|
|
87186
|
+
message_ids: messageIds,
|
|
87187
|
+
texts
|
|
87188
|
+
});
|
|
87189
|
+
} catch {}
|
|
87190
|
+
} : undefined,
|
|
87191
|
+
stderr: (s) => {
|
|
87192
|
+
process.stderr.write(s);
|
|
87193
|
+
}
|
|
87194
|
+
}, BACKSTOP_DELIVERY_MAX_ATTEMPTS);
|
|
87195
|
+
return {
|
|
87196
|
+
sentIds: result.sentIds,
|
|
87197
|
+
chunkCount: result.chunkCount,
|
|
87198
|
+
delivered: result.delivered,
|
|
87199
|
+
exhausted: result.exhausted
|
|
87200
|
+
};
|
|
87201
|
+
}
|
|
86746
87202
|
var chatAvailableReactions = new Map;
|
|
86747
87203
|
var chatProbesInFlight = new Set;
|
|
86748
87204
|
var activeTurnStartedAt = new Map;
|
|
@@ -87644,7 +88100,7 @@ function endCurrentTurnAtomic(turn, opts) {
|
|
|
87644
88100
|
process.stderr.write(`telegram gateway: status-surface DEGRADED reason=${degraded.reason} turnId=${turn.turnId} chat=${turn.sessionChatId} thread=${turn.sessionThreadId ?? "-"} ${degraded.detail}
|
|
87645
88101
|
`);
|
|
87646
88102
|
}
|
|
87647
|
-
if (OBLIGATION_LEDGER_ENABLED) {
|
|
88103
|
+
if (OBLIGATION_LEDGER_ENABLED && opts?.deferObligationClose !== true) {
|
|
87648
88104
|
if (decideObligationTurnEnd(turn.finalAnswerDelivered, turn.replyCalled) === "close") {
|
|
87649
88105
|
obligationLedger.close(turn.turnId);
|
|
87650
88106
|
} else {
|
|
@@ -87658,6 +88114,7 @@ function endCurrentTurnAtomic(turn, opts) {
|
|
|
87658
88114
|
turn.narrativeGate?.teardown();
|
|
87659
88115
|
purgeReactionTracking(statusKey(turn.sessionChatId, turn.sessionThreadId), turn);
|
|
87660
88116
|
armNoReplyDrainTimer(turn);
|
|
88117
|
+
flushPendingUserFailureNotices(turn.finalAnswerDelivered || turn.replyCalled);
|
|
87661
88118
|
return turnEndedAt;
|
|
87662
88119
|
}
|
|
87663
88120
|
function maybeProactiveCompact() {
|
|
@@ -89103,7 +89560,12 @@ function emitGatewayOperatorEvent(event) {
|
|
|
89103
89560
|
const opEventSupergroup = resolveAgentSupergroupChatId();
|
|
89104
89561
|
process.stderr.write(`telegram gateway: operator-event posting agent=${agent} kind=${kind} to ${access.allowFrom.length} chat(s)` + (opEventTopic != null ? ` topic=${opEventTopic}` : "") + `
|
|
89105
89562
|
`);
|
|
89106
|
-
|
|
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) {
|
|
89107
89569
|
const opEventThread = topicForRecipient({ recipientChatId: chat_id, resolvedTopic: opEventTopic, supergroupChatId: opEventSupergroup });
|
|
89108
89570
|
const opts = {
|
|
89109
89571
|
...renderedKeyboard ? { reply_markup: renderedKeyboard } : {},
|
|
@@ -89123,6 +89585,38 @@ function emitGatewayOperatorEvent(event) {
|
|
|
89123
89585
|
`);
|
|
89124
89586
|
});
|
|
89125
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
|
+
}
|
|
89126
89620
|
}
|
|
89127
89621
|
function postLegacyBanner(chatId, threadId, ackMessageId, ageSec, site) {
|
|
89128
89622
|
const text5 = `\uD83C\uDF9B\uFE0F Switchroom restarted \u2014 ready. (took ~${ageSec}s)`;
|
|
@@ -89633,6 +90127,11 @@ function ensureIssuesCard(chatId, threadId) {
|
|
|
89633
90127
|
}
|
|
89634
90128
|
process.stderr.write(`telegram gateway: wrote PID file ${GATEWAY_PID_PATH} pid=${process.pid} startedAt=${GATEWAY_STARTED_AT_MS}
|
|
89635
90129
|
`);
|
|
90130
|
+
{
|
|
90131
|
+
const carrierAgentDir = resolveAgentDirFromEnv();
|
|
90132
|
+
if (carrierAgentDir != null)
|
|
90133
|
+
consumeSessionModelCarrierOnHealthyBoot(carrierAgentDir);
|
|
90134
|
+
}
|
|
89636
90135
|
runBootPinCleanupAndDmSweep();
|
|
89637
90136
|
} catch (err) {
|
|
89638
90137
|
process.stderr.write(`telegram gateway: boot.lock_acquire_failed err=${err.message} agent=${SWITCHROOM_AGENT_NAME}
|
|
@@ -89641,6 +90140,11 @@ function ensureIssuesCard(chatId, threadId) {
|
|
|
89641
90140
|
writePidFile(GATEWAY_PID_PATH, { pid: process.pid, startedAtMs: GATEWAY_STARTED_AT_MS });
|
|
89642
90141
|
process.stderr.write(`telegram gateway: wrote PID file ${GATEWAY_PID_PATH} pid=${process.pid} startedAt=${GATEWAY_STARTED_AT_MS} (mutex-fallback)
|
|
89643
90142
|
`);
|
|
90143
|
+
{
|
|
90144
|
+
const carrierAgentDir = resolveAgentDirFromEnv();
|
|
90145
|
+
if (carrierAgentDir != null)
|
|
90146
|
+
consumeSessionModelCarrierOnHealthyBoot(carrierAgentDir);
|
|
90147
|
+
}
|
|
89644
90148
|
runBootPinCleanupAndDmSweep();
|
|
89645
90149
|
} catch (writeErr) {
|
|
89646
90150
|
process.stderr.write(`telegram gateway: writePidFile failed: ${writeErr}
|
|
@@ -93156,7 +93660,7 @@ async function executeGetRecentMessages(args) {
|
|
|
93156
93660
|
const rows = query({ chat_id, thread_id, limit, before_message_id });
|
|
93157
93661
|
const summary = rows.map((r) => {
|
|
93158
93662
|
const who = r.role === "user" ? r.user ?? "user" : "assistant";
|
|
93159
|
-
const time =
|
|
93663
|
+
const time = fmtLocalStamp2(r.ts * 1000, resolveEnvTimezone2());
|
|
93160
93664
|
const attach = r.attachment_kind ? ` [${r.attachment_kind}]` : "";
|
|
93161
93665
|
const replyCtx = r.reply_to_message_id != null ? ` \u21AA\uFE0F#${r.reply_to_message_id}${r.reply_to_text ? `:"${r.reply_to_text.slice(0, 60)}${r.reply_to_text.length > 60 ? "\u2026" : ""}"` : ""}` : "";
|
|
93162
93666
|
const reactionTag = r.user_reaction ? ` [reaction: ${r.user_reaction}]` : "";
|
|
@@ -94288,16 +94792,15 @@ function handleSessionEvent(ev) {
|
|
|
94288
94792
|
}
|
|
94289
94793
|
turn.finalAnswerDelivered = true;
|
|
94290
94794
|
turn.finalAnswerSubstantive = true;
|
|
94291
|
-
|
|
94292
|
-
|
|
94293
|
-
}
|
|
94795
|
+
turn.answerDelivered = true;
|
|
94796
|
+
const backstopLatchClaimed = backstopDeliveryLedger.claim(turn.turnId);
|
|
94294
94797
|
const cardTakeover = progressDriver?.takeOverCard({
|
|
94295
94798
|
chatId: backstopChatId,
|
|
94296
94799
|
threadId: backstopThreadId != null ? String(backstopThreadId) : undefined
|
|
94297
94800
|
}) ?? { wasEmitted: false, turnKey: null };
|
|
94298
94801
|
const backstopCardMessageId = cardTakeover.wasEmitted && cardTakeover.turnKey != null ? getPinnedProgressCardMessageId?.(cardTakeover.turnKey) ?? null : null;
|
|
94299
94802
|
const backstopCardTurnKey = cardTakeover.turnKey;
|
|
94300
|
-
const backstopTurnEndedAt = endCurrentTurnAtomic(turn, { deferRecord: true });
|
|
94803
|
+
const backstopTurnEndedAt = endCurrentTurnAtomic(turn, { deferRecord: true, deferObligationClose: true });
|
|
94301
94804
|
preambleSuppressor.dropNow();
|
|
94302
94805
|
{
|
|
94303
94806
|
const tKey = statusKey(chatId, threadId);
|
|
@@ -94316,80 +94819,47 @@ function handleSessionEvent(ev) {
|
|
|
94316
94819
|
`);
|
|
94317
94820
|
if (backstopTurnEndedAt != null) {
|
|
94318
94821
|
turn.deliveryOutcome = "suppressed";
|
|
94822
|
+
if (OBLIGATION_LEDGER_ENABLED)
|
|
94823
|
+
obligationLedger.close(turn.turnId);
|
|
94319
94824
|
emitTurnRecord(turn, backstopTurnEndedAt);
|
|
94320
94825
|
}
|
|
94321
94826
|
return;
|
|
94322
94827
|
}
|
|
94323
94828
|
} catch {}
|
|
94324
94829
|
}
|
|
94830
|
+
if (!backstopLatchClaimed) {
|
|
94831
|
+
process.stderr.write(`telegram gateway: turn-flush skipped \u2014 turn ${turn.turnId} already claimed the delivery latch
|
|
94832
|
+
`);
|
|
94833
|
+
return;
|
|
94834
|
+
}
|
|
94325
94835
|
process.stderr.write(`telegram gateway: turn-flush firing \u2014 ${capturedText.length} chars without reply tool (chat=${backstopChatId} cardMsgId=${backstopCardMessageId ?? "none"})
|
|
94326
94836
|
`);
|
|
94327
|
-
|
|
94328
|
-
|
|
94329
|
-
|
|
94330
|
-
};
|
|
94331
|
-
const limit = RICH_MESSAGE_MAX_CHARS2;
|
|
94332
|
-
let htmlChunks = [];
|
|
94333
|
-
const sentIds = [];
|
|
94334
|
-
let sendThrew = false;
|
|
94837
|
+
let sentIds = [];
|
|
94838
|
+
let chunkCount = 0;
|
|
94839
|
+
let delivered = false;
|
|
94335
94840
|
try {
|
|
94336
|
-
const
|
|
94337
|
-
|
|
94338
|
-
|
|
94339
|
-
|
|
94340
|
-
|
|
94341
|
-
|
|
94342
|
-
|
|
94343
|
-
|
|
94344
|
-
|
|
94345
|
-
|
|
94346
|
-
});
|
|
94347
|
-
sentIds.push(backstopCardMessageId);
|
|
94348
|
-
firstSendUsedEdit = true;
|
|
94349
|
-
} catch (err) {
|
|
94350
|
-
process.stderr.write(`telegram gateway: turn-flush card-takeover edit failed: ${err.message} \u2014 falling back to sendMessage
|
|
94351
|
-
`);
|
|
94352
|
-
if (err instanceof Error && err.message === "THREAD_NOT_FOUND") {
|
|
94353
|
-
liveThreadId = undefined;
|
|
94354
|
-
}
|
|
94355
|
-
}
|
|
94356
|
-
}
|
|
94357
|
-
const remainingChunks = firstSendUsedEdit ? htmlChunks.slice(1) : htmlChunks;
|
|
94358
|
-
for (const c of remainingChunks) {
|
|
94359
|
-
const sent = await retryWithThreadFallback2(robustApiCall, (tid) => {
|
|
94360
|
-
const opts = {
|
|
94361
|
-
link_preview_options: { is_disabled: true },
|
|
94362
|
-
...tid != null ? { message_thread_id: tid } : {}
|
|
94363
|
-
};
|
|
94364
|
-
return bot.api.sendRichMessage(backstopChatId, richMessage2(c), opts);
|
|
94365
|
-
}, {
|
|
94366
|
-
threadId: liveThreadId,
|
|
94367
|
-
chat_id: backstopChatId,
|
|
94368
|
-
verb: "turn-flush.sendMessage"
|
|
94369
|
-
});
|
|
94370
|
-
if (liveThreadId != null) {
|
|
94371
|
-
const sentMsg = sent;
|
|
94372
|
-
if (sentMsg.message_thread_id == null)
|
|
94373
|
-
liveThreadId = undefined;
|
|
94374
|
-
}
|
|
94375
|
-
sentIds.push(sent.message_id);
|
|
94376
|
-
}
|
|
94377
|
-
if (HISTORY_ENABLED && sentIds.length > 0) {
|
|
94378
|
-
try {
|
|
94379
|
-
recordOutbound({
|
|
94380
|
-
chat_id: backstopChatId,
|
|
94381
|
-
thread_id: backstopThreadId ?? null,
|
|
94382
|
-
message_ids: sentIds,
|
|
94383
|
-
texts: htmlChunks
|
|
94384
|
-
});
|
|
94385
|
-
} catch {}
|
|
94386
|
-
}
|
|
94841
|
+
const delivery = await deliverAnswer({
|
|
94842
|
+
chatId: backstopChatId,
|
|
94843
|
+
threadId: backstopThreadId,
|
|
94844
|
+
text: capturedText,
|
|
94845
|
+
turnId: turn.turnId,
|
|
94846
|
+
cardMessageId: backstopCardMessageId
|
|
94847
|
+
});
|
|
94848
|
+
sentIds = delivery.sentIds;
|
|
94849
|
+
chunkCount = delivery.chunkCount;
|
|
94850
|
+
delivered = delivery.delivered;
|
|
94387
94851
|
outboundDedup.record(backstopChatId, backstopThreadId, capturedText, Date.now(), currentTurn?.registryKey ?? null);
|
|
94388
94852
|
if (sentIds.length > 0) {
|
|
94389
94853
|
flushedTurnSupersede.record(backstopChatId, backstopThreadId, { turnId: turn.turnId, messageIds: sentIds, text: capturedText }, Date.now());
|
|
94390
94854
|
}
|
|
94391
|
-
if (
|
|
94855
|
+
if (!delivered) {
|
|
94856
|
+
if (backstopCtrl)
|
|
94857
|
+
backstopCtrl.finalize("error");
|
|
94858
|
+
backstopDeliveryLedger.release(turn.turnId);
|
|
94859
|
+
turn.answerDelivered = false;
|
|
94860
|
+
} else if (backstopCtrl) {
|
|
94392
94861
|
backstopCtrl.finalize("done");
|
|
94862
|
+
}
|
|
94393
94863
|
if (backstopCardTurnKey != null) {
|
|
94394
94864
|
completeProgressCardTurn?.({
|
|
94395
94865
|
chatId: backstopChatId,
|
|
@@ -94400,21 +94870,33 @@ function handleSessionEvent(ev) {
|
|
|
94400
94870
|
unpinProgressCardForChat?.(backstopChatId, backstopThreadId);
|
|
94401
94871
|
}
|
|
94402
94872
|
} catch (err) {
|
|
94403
|
-
|
|
94404
|
-
process.stderr.write(`telegram gateway: turn-flush send failed: ${err.message}
|
|
94873
|
+
process.stderr.write(`telegram gateway: turn-flush post-delivery bookkeeping failed: ${err.message}
|
|
94405
94874
|
`);
|
|
94406
|
-
|
|
94407
|
-
|
|
94408
|
-
|
|
94875
|
+
if (!delivered) {
|
|
94876
|
+
turn.answerDelivered = false;
|
|
94877
|
+
backstopDeliveryLedger.release(turn.turnId);
|
|
94878
|
+
if (backstopCtrl)
|
|
94879
|
+
backstopCtrl.finalize("error");
|
|
94880
|
+
}
|
|
94409
94881
|
} finally {
|
|
94410
94882
|
if (backstopTurnEndedAt != null) {
|
|
94411
|
-
|
|
94412
|
-
threw:
|
|
94413
|
-
|
|
94414
|
-
chunkCount
|
|
94883
|
+
finalizeBackstopSendGated(turn, {
|
|
94884
|
+
threw: !delivered,
|
|
94885
|
+
sentIds,
|
|
94886
|
+
chunkCount,
|
|
94887
|
+
cardMessageId: backstopCardMessageId
|
|
94415
94888
|
});
|
|
94889
|
+
if (OBLIGATION_LEDGER_ENABLED) {
|
|
94890
|
+
if (delivered) {
|
|
94891
|
+
obligationLedger.close(turn.turnId);
|
|
94892
|
+
} else {
|
|
94893
|
+
turn.finalAnswerDelivered = false;
|
|
94894
|
+
obligationLedger.noteTurnEnded(turn.turnId, Date.now());
|
|
94895
|
+
}
|
|
94896
|
+
}
|
|
94416
94897
|
emitTurnRecord(turn, backstopTurnEndedAt);
|
|
94417
94898
|
}
|
|
94899
|
+
backstopDeliveryLedger.clear(turn.turnId);
|
|
94418
94900
|
}
|
|
94419
94901
|
})();
|
|
94420
94902
|
return;
|
|
@@ -95469,7 +95951,7 @@ ${preBlock(write.output)}`;
|
|
|
95469
95951
|
...msgId != null ? { message_id: String(msgId) } : {},
|
|
95470
95952
|
user: displayUser,
|
|
95471
95953
|
user_id: String(from.id),
|
|
95472
|
-
ts:
|
|
95954
|
+
ts: fmtLocalStamp2((ctx.message?.date ?? 0) * 1000, resolveEnvTimezone2()),
|
|
95473
95955
|
...messageThreadId != null ? { message_thread_id: String(messageThreadId) } : {},
|
|
95474
95956
|
...originTurnId != null ? { origin_turn_id: originTurnId } : {},
|
|
95475
95957
|
...topicScope != null ? { topic_scope: topicScope } : {},
|
|
@@ -96256,9 +96738,10 @@ async function dispatchShortVerbViaHostd(ctx, req, label, legacyArgs) {
|
|
|
96256
96738
|
await switchroomReply(ctx, `\u274C **${escapeHtmlForTg2(label)} failed via hostd** (result=${escapeHtmlForTg2(hostdResp.result)}):
|
|
96257
96739
|
` + preBlock(stripAnsi3(errBody)), { html: true });
|
|
96258
96740
|
}
|
|
96259
|
-
async function runSwitchroomCommand(ctx, args, label, classification = "query") {
|
|
96741
|
+
async function runSwitchroomCommand(ctx, args, label, classification = "query", transformOutput) {
|
|
96260
96742
|
try {
|
|
96261
|
-
const
|
|
96743
|
+
const stripped = stripAnsi3(switchroomExec(args));
|
|
96744
|
+
const output = transformOutput ? transformOutput(stripped) : stripped;
|
|
96262
96745
|
const formatted = formatSwitchroomOutput(output);
|
|
96263
96746
|
if (formatted) {
|
|
96264
96747
|
await switchroomReply(ctx, preBlock(formatted), { html: true, classification });
|
|
@@ -96611,7 +97094,6 @@ function buildModelDeps(restartCtx) {
|
|
|
96611
97094
|
return [];
|
|
96612
97095
|
}
|
|
96613
97096
|
},
|
|
96614
|
-
select: (a, label) => selectModel(a, label),
|
|
96615
97097
|
isBusy: () => currentTurn !== null,
|
|
96616
97098
|
getAgentName: getMyAgentName,
|
|
96617
97099
|
getQuotaBrief: async () => {
|
|
@@ -96630,14 +97112,12 @@ function buildModelDeps(restartCtx) {
|
|
|
96630
97112
|
} catch {}
|
|
96631
97113
|
return null;
|
|
96632
97114
|
},
|
|
96633
|
-
inject: injectSlashCommand,
|
|
96634
97115
|
getConfiguredModel: () => {
|
|
96635
97116
|
const data = switchroomExecJson(["agent", "list"]);
|
|
96636
97117
|
return data?.agents?.find((a) => a.name === getMyAgentName())?.model ?? null;
|
|
96637
97118
|
},
|
|
96638
97119
|
escapeHtml: escapeHtmlForTg2,
|
|
96639
97120
|
preBlock,
|
|
96640
|
-
getActiveSessionModel: () => sessionModelSource.getOverride(),
|
|
96641
97121
|
scheduleRestart: async (reason) => {
|
|
96642
97122
|
const name = getMyAgentName();
|
|
96643
97123
|
const existing = readRestartMarker();
|
|
@@ -96680,6 +97160,30 @@ function buildModelDeps(restartCtx) {
|
|
|
96680
97160
|
const prevFileRaw = readSessionModelFileRaw(agentDir);
|
|
96681
97161
|
writeSessionModelFile(agentDir, model, readConfiguredDefaultModel(agentDir) ?? resolveMainModel(deps.getConfiguredModel() ?? undefined));
|
|
96682
97162
|
sessionModelSource.setOverride(model);
|
|
97163
|
+
process.stderr.write(`telegram gateway: gw /model relaunch scheduled agent=${getMyAgentName()} token=${model} reason=${JSON.stringify(reason)}
|
|
97164
|
+
`);
|
|
97165
|
+
clearPremiumRecoveryOnManualSwitch(model);
|
|
97166
|
+
try {
|
|
97167
|
+
await deps.scheduleRestart(reason);
|
|
97168
|
+
} catch (err) {
|
|
97169
|
+
if (err?.code !== "restart_in_flight") {
|
|
97170
|
+
restoreSessionModelFileRaw(agentDir, prevFileRaw);
|
|
97171
|
+
sessionModelSource.setOverride(prevOverride);
|
|
97172
|
+
}
|
|
97173
|
+
throw err;
|
|
97174
|
+
}
|
|
97175
|
+
},
|
|
97176
|
+
scheduleModelDefaultRelaunch: async (reason) => {
|
|
97177
|
+
const agentDir = resolveAgentDirFromEnv();
|
|
97178
|
+
if (!agentDir)
|
|
97179
|
+
throw new Error("agent dir unresolvable \u2014 cannot clear session-model file");
|
|
97180
|
+
const prevOverride = sessionModelSource.getOverride();
|
|
97181
|
+
const prevFileRaw = readSessionModelFileRaw(agentDir);
|
|
97182
|
+
clearSessionModelFile(agentDir);
|
|
97183
|
+
sessionModelSource.setOverride(null);
|
|
97184
|
+
process.stderr.write(`telegram gateway: gw /model relaunch scheduled agent=${getMyAgentName()} token=(default) reason=${JSON.stringify(reason)}
|
|
97185
|
+
`);
|
|
97186
|
+
clearPremiumRecoveryOnManualSwitch(null);
|
|
96683
97187
|
try {
|
|
96684
97188
|
await deps.scheduleRestart(reason);
|
|
96685
97189
|
} catch (err) {
|
|
@@ -96704,60 +97208,6 @@ function modelMenuReplyMarkup(reply) {
|
|
|
96704
97208
|
}
|
|
96705
97209
|
return kb;
|
|
96706
97210
|
}
|
|
96707
|
-
function recordTypedModelSwitch(reply, requestedModelArg, _deps) {
|
|
96708
|
-
const requested = requestedModelArg != null ? expandSrAlias(requestedModelArg) : null;
|
|
96709
|
-
if (requested?.toLowerCase() === "default") {
|
|
96710
|
-
const smDir = resolveAgentDirFromEnv();
|
|
96711
|
-
if (smDir)
|
|
96712
|
-
clearSessionModelFile(smDir);
|
|
96713
|
-
if (reply.selectedModel)
|
|
96714
|
-
sessionModelSource.setOverride(null);
|
|
96715
|
-
return "";
|
|
96716
|
-
}
|
|
96717
|
-
if (!reply.selectedModel)
|
|
96718
|
-
return "";
|
|
96719
|
-
sessionModelSource.setOverride(reply.selectedModel);
|
|
96720
|
-
clearPremiumRecoveryOnManualSwitch(reply.selectedModel);
|
|
96721
|
-
return "";
|
|
96722
|
-
}
|
|
96723
|
-
function recordModelMenuSideEffects(outcome, modelDeps, cbChatId, cbThreadId, prevSessionModel) {
|
|
96724
|
-
if (outcome.selectedModel) {
|
|
96725
|
-
sessionModelSource.setOverride(outcome.selectedModel);
|
|
96726
|
-
clearPremiumRecoveryOnManualSwitch(outcome.selectedModel);
|
|
96727
|
-
}
|
|
96728
|
-
if (outcome.clearedDefault) {
|
|
96729
|
-
const smDir = resolveAgentDirFromEnv();
|
|
96730
|
-
if (smDir)
|
|
96731
|
-
clearSessionModelFile(smDir);
|
|
96732
|
-
}
|
|
96733
|
-
if (outcome.selectedModel && isSrToClaudeTransition(prevSessionModel, outcome.selectedModel)) {
|
|
96734
|
-
const agentName3 = getMyAgentName();
|
|
96735
|
-
const agentDir = resolveAgentDirFromEnv();
|
|
96736
|
-
const token = outcome.selectedModelToken;
|
|
96737
|
-
if (agentDir && token) {
|
|
96738
|
-
try {
|
|
96739
|
-
writeSessionModelFile(agentDir, token, readConfiguredDefaultModel(agentDir) ?? resolveMainModel(modelDeps.getConfiguredModel() ?? undefined));
|
|
96740
|
-
sessionModelSource.setOverride(token);
|
|
96741
|
-
} catch (e) {
|
|
96742
|
-
process.stderr.write(`telegram gateway: sr-to-claude session-model write failed: ${e?.message ?? String(e)}
|
|
96743
|
-
`);
|
|
96744
|
-
}
|
|
96745
|
-
} else if (agentDir) {
|
|
96746
|
-
clearSessionModelFile(agentDir);
|
|
96747
|
-
}
|
|
96748
|
-
writeRestartMarker({ chat_id: cbChatId, thread_id: cbThreadId ?? null, ack_message_id: null, ts: Date.now() });
|
|
96749
|
-
stampUserRestartReason("user: sr-to-claude model switch (menu)");
|
|
96750
|
-
if (turnInFlightForGate()) {
|
|
96751
|
-
pendingRestarts.set(agentName3, Date.now());
|
|
96752
|
-
} else {
|
|
96753
|
-
sweepBeforeSelfRestart().finally(() => triggerSelfRestart(agentName3, "sr-to-claude-model-switch", 1500));
|
|
96754
|
-
}
|
|
96755
|
-
return {
|
|
96756
|
-
restartNotice: `\uD83D\uDD04 Switching from **${escapeHtmlForTg2(prevSessionModel)}** back to Claude \u2014 restarting session cleanly. Claude will be ready in ~30s.`
|
|
96757
|
-
};
|
|
96758
|
-
}
|
|
96759
|
-
return {};
|
|
96760
|
-
}
|
|
96761
97211
|
async function editPendingCommandCard(chatId, messageId, text5) {
|
|
96762
97212
|
try {
|
|
96763
97213
|
await robustApiCall(() => lockedBot.api.editMessageText(chatId, messageId, richMessage2(hardenCardBreaks2(text5)), {
|
|
@@ -96779,14 +97229,11 @@ function enqueueSessionCommand(cmd) {
|
|
|
96779
97229
|
async function applyQueuedModelCommand(cmd) {
|
|
96780
97230
|
const deps = buildModelDeps({ chatId: cmd.chatId, threadId: cmd.threadId });
|
|
96781
97231
|
if (cmd.origin === "menu") {
|
|
96782
|
-
const prevSessionModel = sessionModelSource.getOverride();
|
|
96783
97232
|
const outcome = await handleModelMenuCallback(cmd.arg, deps);
|
|
96784
|
-
|
|
96785
|
-
return restartNotice ?? outcome.reply.text;
|
|
97233
|
+
return outcome.reply.text;
|
|
96786
97234
|
}
|
|
96787
97235
|
const reply = await handleModelCommand({ kind: "set", model: cmd.arg }, deps);
|
|
96788
|
-
|
|
96789
|
-
return reply.text + warning;
|
|
97236
|
+
return reply.text;
|
|
96790
97237
|
}
|
|
96791
97238
|
async function applyQueuedEffortCommand(cmd) {
|
|
96792
97239
|
const deps = buildEffortDeps();
|
|
@@ -96924,8 +97371,7 @@ bot.command("model", async (ctx) => {
|
|
|
96924
97371
|
return;
|
|
96925
97372
|
}
|
|
96926
97373
|
const reply = await handleModelCommand(parsed, deps);
|
|
96927
|
-
|
|
96928
|
-
await switchroomReply(ctx, reply.text + persistWarning, { html: reply.html });
|
|
97374
|
+
await switchroomReply(ctx, reply.text, { html: reply.html });
|
|
96929
97375
|
});
|
|
96930
97376
|
var sessionEffortOverride = null;
|
|
96931
97377
|
function buildEffortDeps() {
|
|
@@ -98682,7 +99128,8 @@ bot.command("logs", async (ctx) => {
|
|
|
98682
99128
|
}
|
|
98683
99129
|
const lines = linesArg ? parseInt(linesArg, 10) : 20;
|
|
98684
99130
|
const lineCount = isNaN(lines) || lines < 1 ? 20 : Math.min(lines, 200);
|
|
98685
|
-
|
|
99131
|
+
const tz = resolveEnvTimezone2();
|
|
99132
|
+
await runSwitchroomCommand(ctx, ["agent", "logs", name, "--lines", String(lineCount), "--timestamps"], `logs ${name}`, "heavy", (raw) => renderLogTimestampsLocal(raw, tz));
|
|
98686
99133
|
});
|
|
98687
99134
|
bot.command("memory", async (ctx) => {
|
|
98688
99135
|
if (!isAuthorizedSender(ctx))
|
|
@@ -98979,31 +99426,10 @@ bot.on("callback_query:data", async (ctx) => {
|
|
|
98979
99426
|
}
|
|
98980
99427
|
const isPageNav = data === MODEL_CALLBACK_PAGE_EXTERNAL || data === MODEL_CALLBACK_PAGE_MAIN;
|
|
98981
99428
|
await ctx.answerCallbackQuery({ text: isPageNav ? "Loading\u2026" : "Switching\u2026" }).catch(() => {});
|
|
98982
|
-
if (data.startsWith(MODEL_CALLBACK_SR)) {
|
|
98983
|
-
const srName = data.slice(MODEL_CALLBACK_SR.length);
|
|
98984
|
-
const srLabel = escapeHtmlForTg2(srFriendlyLabel(srName));
|
|
98985
|
-
if (!isValidModelArg(srName)) {
|
|
98986
|
-
await ctx.editMessageText(richMessage2("\u274C Invalid model name"), { reply_markup: { inline_keyboard: [] } }).catch(() => {});
|
|
98987
|
-
return;
|
|
98988
|
-
}
|
|
98989
|
-
await ctx.editMessageText(richMessage2(`\uD83D\uDD04 Switching session to **${srLabel}** \u2014 restarting (~30s). _Session-only; reverts to the configured default on the next restart._`), { reply_markup: { inline_keyboard: [] } }).catch(() => {});
|
|
98990
|
-
try {
|
|
98991
|
-
await modelDeps.scheduleModelRelaunch(srName, `user: /model ${srName} (session-only relaunch, menu)`);
|
|
98992
|
-
} catch (err) {
|
|
98993
|
-
await ctx.editMessageText(richMessage2(`\u274C Could not switch to **${srLabel}**: ${escapeHtmlForTg2(err?.message ?? String(err))}`), { reply_markup: { inline_keyboard: [] } }).catch(() => {});
|
|
98994
|
-
}
|
|
98995
|
-
return;
|
|
98996
|
-
}
|
|
98997
99429
|
try {
|
|
98998
|
-
const prevSessionModel = sessionModelSource.getOverride();
|
|
98999
99430
|
const outcome = await handleModelMenuCallback(data, modelDeps);
|
|
99000
99431
|
if (outcome.toastOnly)
|
|
99001
99432
|
return;
|
|
99002
|
-
const { restartNotice } = recordModelMenuSideEffects(outcome, modelDeps, cbChatId, cbThreadId, prevSessionModel);
|
|
99003
|
-
if (restartNotice) {
|
|
99004
|
-
await ctx.editMessageText(richMessage2(restartNotice), { reply_markup: { inline_keyboard: [] } }).catch(() => {});
|
|
99005
|
-
return;
|
|
99006
|
-
}
|
|
99007
99433
|
await ctx.editMessageText(richMessage2(outcome.reply.text), {
|
|
99008
99434
|
reply_markup: modelMenuReplyMarkup(outcome.reply) ?? { inline_keyboard: [] }
|
|
99009
99435
|
}).catch(() => {});
|
|
@@ -100099,6 +100525,7 @@ bot.on("message:pinned_message", async (ctx) => {
|
|
|
100099
100525
|
`);
|
|
100100
100526
|
}
|
|
100101
100527
|
});
|
|
100528
|
+
installUnhandledMessageCatchAll(bot, (ctx, text5) => handleInboundCoalesced(ctx, text5, undefined), (line) => process.stderr.write(line));
|
|
100102
100529
|
var reactionsCfg = null;
|
|
100103
100530
|
var reactionHourCap = null;
|
|
100104
100531
|
var reactionDebounce = null;
|
|
@@ -100708,6 +101135,8 @@ var didOneTimeSetup = false;
|
|
|
100708
101135
|
`);
|
|
100709
101136
|
}
|
|
100710
101137
|
} catch {}
|
|
101138
|
+
let modelSwitchMarkerChat = null;
|
|
101139
|
+
let modelSwitchReason = null;
|
|
100711
101140
|
try {
|
|
100712
101141
|
const nowMs2 = Date.now();
|
|
100713
101142
|
const marker = readRestartMarker();
|
|
@@ -100737,6 +101166,12 @@ var didOneTimeSetup = false;
|
|
|
100737
101166
|
const ageSec = Math.max(1, Math.round(ageMs / 1000));
|
|
100738
101167
|
process.stderr.write(`telegram gateway: boot: restart-marker present, chat_id=${marker.chat_id} age=${ageSec}s within5min=${ageMs < 300000}
|
|
100739
101168
|
`);
|
|
101169
|
+
if (ageMs < 300000) {
|
|
101170
|
+
modelSwitchMarkerChat = { chatId: marker.chat_id, threadId: marker.thread_id };
|
|
101171
|
+
if (typeof cleanMarker?.reason === "string" && cleanMarker.reason.startsWith("user: /model")) {
|
|
101172
|
+
modelSwitchReason = cleanMarker.reason;
|
|
101173
|
+
}
|
|
101174
|
+
}
|
|
100740
101175
|
clearRestartMarker();
|
|
100741
101176
|
}
|
|
100742
101177
|
if (!isRealRestart) {
|
|
@@ -100762,7 +101197,11 @@ var didOneTimeSetup = false;
|
|
|
100762
101197
|
firstSeenAt: new Date
|
|
100763
101198
|
});
|
|
100764
101199
|
}
|
|
100765
|
-
|
|
101200
|
+
const suppressBootCardForModelSwitch = modelSwitchReason != null && modelSwitchMarkerChat != null;
|
|
101201
|
+
if (target && suppressBootCardForModelSwitch) {
|
|
101202
|
+
process.stderr.write(`telegram gateway: boot: suppressing generic boot card \u2014 /model switch apply-boot (reason=${JSON.stringify(modelSwitchReason)}); the confirmation card replaces it
|
|
101203
|
+
`);
|
|
101204
|
+
} else if (target) {
|
|
100766
101205
|
const { chatId, threadId, ackMsgId } = target;
|
|
100767
101206
|
const bootDedupe = shouldSkipDuplicateBootCard({ activeBootCard, bootCardPending }, "boot");
|
|
100768
101207
|
if (bootDedupe.skip) {
|
|
@@ -100841,7 +101280,30 @@ var didOneTimeSetup = false;
|
|
|
100841
101280
|
const raw = d?.agents?.find((a) => a.name === getMyAgentName())?.model ?? null;
|
|
100842
101281
|
return resolveMainModel(raw ?? undefined);
|
|
100843
101282
|
})();
|
|
100844
|
-
|
|
101283
|
+
const isApplyBoot = launched.length > 0 && launched !== configured;
|
|
101284
|
+
sessionModelSource.setOverride(isApplyBoot ? launched : null);
|
|
101285
|
+
process.stderr.write(`telegram gateway: gw /model relaunch applied agent=${getMyAgentName()} launched=${launched || "(none)"} configured=${configured} override=${isApplyBoot ? "set" : "cleared"}
|
|
101286
|
+
`);
|
|
101287
|
+
if (modelSwitchReason != null && modelSwitchMarkerChat) {
|
|
101288
|
+
const chat = modelSwitchMarkerChat;
|
|
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)}
|
|
101304
|
+
`));
|
|
101305
|
+
}
|
|
101306
|
+
}
|
|
100845
101307
|
} catch {}
|
|
100846
101308
|
}
|
|
100847
101309
|
const activeEffortPath = join55(smAgentDir, ".active-session-effort");
|