switchroom 0.18.9 → 0.18.11
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/dist/agent-scheduler/index.js +1 -0
- package/dist/auth-broker/index.js +198 -13
- package/dist/cli/notion-write-pretool.mjs +1 -0
- package/dist/cli/switchroom.js +214 -83
- package/dist/cli/ui/apple-touch-icon.png +0 -0
- package/dist/cli/ui/favicon-32.png +0 -0
- package/dist/cli/ui/favicon.ico +0 -0
- package/dist/host-control/main.js +760 -257
- package/dist/vault/approvals/kernel-server.js +3 -1
- package/dist/vault/broker/server.js +3 -1
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +119 -37
- package/profiles/_shared/dev-protocol.md.hbs +42 -0
- package/skills/dev-protocol/SKILL.md +131 -0
- package/telegram-plugin/README.md +2 -1
- package/telegram-plugin/admin-commands/dispatch.test.ts +40 -2
- package/telegram-plugin/admin-commands/index.ts +6 -1
- package/telegram-plugin/bridge/bridge.ts +23 -1
- package/telegram-plugin/bridge/crash-breadcrumb.ts +42 -0
- package/telegram-plugin/chat-lock.ts +13 -0
- package/telegram-plugin/dist/bridge/bridge.js +24 -1
- package/telegram-plugin/dist/gateway/gateway.js +1832 -263
- package/telegram-plugin/dist/server.js +29 -2
- package/telegram-plugin/fallback-card-collapse.ts +131 -0
- package/telegram-plugin/gateway/bridge-dead-watchdog.ts +546 -0
- package/telegram-plugin/gateway/effort-command.ts +47 -3
- package/telegram-plugin/gateway/gateway.ts +1435 -211
- package/telegram-plugin/gateway/model-command.ts +94 -8
- package/telegram-plugin/gateway/pending-session-command.ts +365 -0
- package/telegram-plugin/gateway/permission-timeout.ts +25 -0
- package/telegram-plugin/gateway/resume-inbound-builder.ts +23 -3
- package/telegram-plugin/gateway/session-model-file.ts +166 -23
- package/telegram-plugin/gateway/stop-command.ts +56 -0
- package/telegram-plugin/photo-precheck.ts +201 -0
- package/telegram-plugin/quota-watch.ts +141 -2
- package/telegram-plugin/registry/subagents-schema.ts +26 -3
- package/telegram-plugin/registry/subagents.test.ts +67 -0
- package/telegram-plugin/retry-api-call.ts +31 -0
- package/telegram-plugin/subagent-watcher.ts +392 -1
- package/telegram-plugin/tests/bridge-dead-watchdog.test.ts +576 -0
- package/telegram-plugin/tests/buffer-gate-broadened.test.ts +11 -5
- package/telegram-plugin/tests/chat-lock-unhandled-rejection.test.ts +101 -0
- package/telegram-plugin/tests/crash-breadcrumb.test.ts +57 -0
- package/telegram-plugin/tests/effort-command.test.ts +59 -2
- package/telegram-plugin/tests/fallback-card-collapse.test.ts +104 -0
- package/telegram-plugin/tests/gateway-pending-command-wiring.test.ts +124 -0
- package/telegram-plugin/tests/gateway-secret-detect.test.ts +7 -1
- package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +19 -11
- package/telegram-plugin/tests/model-command.test.ts +46 -3
- package/telegram-plugin/tests/pending-session-command.test.ts +322 -0
- package/telegram-plugin/tests/permission-timeout.test.ts +26 -0
- package/telegram-plugin/tests/permission-verdict-resume-guard.test.ts +16 -0
- package/telegram-plugin/tests/photo-dimension-fallback.test.ts +129 -0
- package/telegram-plugin/tests/photo-precheck.test.ts +240 -0
- package/telegram-plugin/tests/photo-reroute-wiring.test.ts +85 -0
- package/telegram-plugin/tests/quota-watch.test.ts +225 -0
- package/telegram-plugin/tests/session-model-file.test.ts +101 -2
- package/telegram-plugin/tests/stop-command.test.ts +234 -0
- package/telegram-plugin/tests/subagent-watcher-env-thresholds.test.ts +27 -9
- package/telegram-plugin/tests/subagent-watcher-resurrection.test.ts +398 -0
- package/telegram-plugin/tests/subagent-watcher-stall-terminal.test.ts +172 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +37 -0
- package/telegram-plugin/tests/worker-visibility-prose-silent-harness.test.ts +18 -4
- package/telegram-plugin/welcome-text.ts +4 -3
- package/telegram-plugin/worker-activity-feed.ts +27 -0
|
@@ -20887,6 +20887,7 @@ var init_schema = __esm(() => {
|
|
|
20887
20887
|
uid: exports_external.number().int().nonnegative().optional().describe("Optional UID to chown the consumer socket to (defaults to 0 = root, " + "suitable for sibling containers running as root)."),
|
|
20888
20888
|
mirror_dir: exports_external.string().optional().describe("Optional host-side directory path. When set, the broker actively " + "writes the consumer's effective-account `.credentials.json` mirror " + "here \u2014 in addition to serving creds on demand via `get-credentials`. " + "Use this to eliminate the pull-latency gap: without a mirror the " + "consumer only gets failover creds at its next scheduled re-fetch " + "(up to 30 min). With a mirror the broker pushes failover creds " + "immediately when it detects exhaustion (consumer-quota-sensor tick, " + "or a mark-exhausted RPC on the pinned account). The directory must " + "be accessible to the broker container (bind-mounted from the host) " + "and to the consumer container; the broker writes " + "`<mirror_dir>/.credentials.json` atomically. Chown is attempted to " + "`uid` (default 0) \u2014 swallowed when CAP_CHOWN is absent.")
|
|
20889
20889
|
})).optional().describe("Non-agent peers that hold a broker socket (RFC H \u00a74.8). Each gets " + "its own `/run/switchroom/auth-broker/<name>/sock` chowned to its UID. " + "Consumers cannot be admins; a consumer name that collides with an " + "agent (whether that agent has `admin: true` or not) is a config " + "error caught at schema validation."),
|
|
20890
|
+
proactive_failover_pct: exports_external.number().min(1).max(99).optional().describe("Soft-avoid threshold (percent) for proactive auth failover (#3031). " + "When set (e.g. 95), an account whose fresh 7-day utilization is at/" + "above this value \u2014 or whose 5h utilization is at/above min(pct+3, " + "98) \u2014 enters the SOFT-AVOID preference tier: the broker prefers a " + "fully-eligible fallback account on the serving path, without ever " + "blocking the account (the hard wall stays 99.5). Enter/exit uses " + "hysteresis (exit below pct-5 or on window reset) so the preference " + "does not flap between probe ticks. Accounts serving past the wall " + "via `allow_overage_accounts` are never soft-avoided. UNSET (the " + "default) disables the tier entirely \u2014 behavior is identical to a " + "fleet without this field."),
|
|
20890
20891
|
allow_overage_accounts: exports_external.array(exports_external.string().min(1)).optional().describe("Opt-in list of account labels (bare strings matching `auth.active` / " + "`auth.fallback_order` entries) that may be served PAST the weekly " + "utilization wall when Anthropic overage billing is available for the " + "account (`overageStatus === 'allowed'`). Overage is REAL MONEY \u2014 " + "default is empty (no account gets this). An account in this list is " + "only kept eligible when its fresh quota snapshot reports " + "`overageStatus: 'allowed'` AND `overageDisabledReason` is NOT " + "'out_of_credits' (i.e. the overage credit has not been exhausted). " + "As soon as `overageDisabledReason` becomes 'out_of_credits', the " + "account is blocked immediately regardless of this flag. Overage lifts " + "ONLY the utilization wall \u2014 it cannot lift an active exhaustion mark " + "written by a real 429 (`mark-exhausted`).")
|
|
20891
20892
|
}).optional().describe("Switchroom-auth-broker configuration (RFC H). Fleet-wide active account, " + "fallback order, admin-agent ACL, and ephemeral-consumer surface. " + "Required from the v0.8+ schema onwards; pre-v0.8 fleets are migrated " + "in-place by `switchroom apply` (see src/auth/migrate-schema.ts)."),
|
|
20892
20893
|
drive: GoogleWorkspaceConfigSchema.describe("RFC D legacy key \u2014 use `google_workspace:` instead. Optional Google " + "Workspace onboarding configuration. When set, supplies Google OAuth " + "client credentials, the approver allowlist for `switchroom drive " + "connect`, and the optional tier knob. Env vars " + "(SWITCHROOM_GOOGLE_CLIENT_ID, SWITCHROOM_GOOGLE_CLIENT_SECRET, " + "SWITCHROOM_APPROVER_USER_ID) take precedence over this block when " + "set, preserving back-compat with the env-only flow shipped in #766."),
|
|
@@ -27284,8 +27285,8 @@ import {
|
|
|
27284
27285
|
mkdirSync as mkdirSync15,
|
|
27285
27286
|
unlinkSync as unlinkSync7,
|
|
27286
27287
|
fsyncSync,
|
|
27287
|
-
openSync as
|
|
27288
|
-
closeSync as
|
|
27288
|
+
openSync as openSync3,
|
|
27289
|
+
closeSync as closeSync3,
|
|
27289
27290
|
lstatSync,
|
|
27290
27291
|
realpathSync
|
|
27291
27292
|
} from "node:fs";
|
|
@@ -27371,10 +27372,10 @@ var init_vault = __esm(() => {
|
|
|
27371
27372
|
// ../src/vault/resolver.ts
|
|
27372
27373
|
import {
|
|
27373
27374
|
chmodSync as chmodSync2,
|
|
27374
|
-
closeSync as
|
|
27375
|
+
closeSync as closeSync4,
|
|
27375
27376
|
mkdirSync as mkdirSync16,
|
|
27376
27377
|
mkdtempSync,
|
|
27377
|
-
openSync as
|
|
27378
|
+
openSync as openSync4,
|
|
27378
27379
|
rmSync,
|
|
27379
27380
|
statSync as statSync5,
|
|
27380
27381
|
writeSync as writeSync3
|
|
@@ -27442,11 +27443,11 @@ function materializationRoot() {
|
|
|
27442
27443
|
}
|
|
27443
27444
|
function writeFileExclusive(filePath, content3) {
|
|
27444
27445
|
const buf = typeof content3 === "string" ? Buffer.from(content3, "utf8") : content3;
|
|
27445
|
-
const fd =
|
|
27446
|
+
const fd = openSync4(filePath, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL, 384);
|
|
27446
27447
|
try {
|
|
27447
27448
|
writeSync3(fd, buf);
|
|
27448
27449
|
} finally {
|
|
27449
|
-
|
|
27450
|
+
closeSync4(fd);
|
|
27450
27451
|
}
|
|
27451
27452
|
}
|
|
27452
27453
|
function materializeFilesEntry(key, files) {
|
|
@@ -37415,7 +37416,7 @@ async function startBootCard(chatId, threadId, bot, opts, ackMessageId, log) {
|
|
|
37415
37416
|
return { messageId: -1, complete: () => {} };
|
|
37416
37417
|
}
|
|
37417
37418
|
}
|
|
37418
|
-
const
|
|
37419
|
+
const ackText2 = renderBootCard({
|
|
37419
37420
|
agentName: opts.agentName,
|
|
37420
37421
|
agentSlug: opts.agentSlug,
|
|
37421
37422
|
version: opts.version,
|
|
@@ -37429,7 +37430,7 @@ async function startBootCard(chatId, threadId, bot, opts, ackMessageId, log) {
|
|
|
37429
37430
|
let messageId = -1;
|
|
37430
37431
|
if (reuseId != null && bot.editMessageTextStrict != null) {
|
|
37431
37432
|
try {
|
|
37432
|
-
const outcome = await bot.editMessageTextStrict(chatId, reuseId,
|
|
37433
|
+
const outcome = await bot.editMessageTextStrict(chatId, reuseId, ackText2, {
|
|
37433
37434
|
link_preview_options: { is_disabled: true },
|
|
37434
37435
|
...threadId != null ? { message_thread_id: threadId } : {}
|
|
37435
37436
|
});
|
|
@@ -37445,7 +37446,7 @@ async function startBootCard(chatId, threadId, bot, opts, ackMessageId, log) {
|
|
|
37445
37446
|
}
|
|
37446
37447
|
if (messageId < 0) {
|
|
37447
37448
|
try {
|
|
37448
|
-
const sent = await bot.sendMessage(chatId,
|
|
37449
|
+
const sent = await bot.sendMessage(chatId, ackText2, {
|
|
37449
37450
|
link_preview_options: { is_disabled: true },
|
|
37450
37451
|
...threadId != null ? { message_thread_id: threadId } : {},
|
|
37451
37452
|
...ackMessageId != null ? { reply_parameters: { message_id: ackMessageId } } : {},
|
|
@@ -37544,7 +37545,7 @@ async function startBootCard(chatId, threadId, bot, opts, ackMessageId, log) {
|
|
|
37544
37545
|
...opts.updateOutcomeLine ? { updateOutcomeLine: opts.updateOutcomeLine } : {},
|
|
37545
37546
|
...configChanges.length > 0 ? { configChanges } : {}
|
|
37546
37547
|
});
|
|
37547
|
-
if (currentText !==
|
|
37548
|
+
if (currentText !== ackText2) {
|
|
37548
37549
|
try {
|
|
37549
37550
|
await bot.editMessageText(chatId, messageId, currentText, {
|
|
37550
37551
|
link_preview_options: { is_disabled: true },
|
|
@@ -38164,7 +38165,7 @@ __export(exports_materialize_bot_token, {
|
|
|
38164
38165
|
materializeBotToken: () => materializeBotToken,
|
|
38165
38166
|
BotTokenMaterializeError: () => BotTokenMaterializeError
|
|
38166
38167
|
});
|
|
38167
|
-
import { existsSync as
|
|
38168
|
+
import { existsSync as existsSync49 } from "node:fs";
|
|
38168
38169
|
function pickConfiguredToken(config, agentName3) {
|
|
38169
38170
|
if (agentName3) {
|
|
38170
38171
|
const agent = config.agents?.[agentName3];
|
|
@@ -38178,7 +38179,7 @@ function tryDirectVaultRead3(ref, config, passphrase) {
|
|
|
38178
38179
|
if (!passphrase)
|
|
38179
38180
|
return null;
|
|
38180
38181
|
const vaultPath = resolvePath(config.vault?.path ?? "~/.switchroom/vault.enc");
|
|
38181
|
-
if (!
|
|
38182
|
+
if (!existsSync49(vaultPath))
|
|
38182
38183
|
return null;
|
|
38183
38184
|
try {
|
|
38184
38185
|
const secrets = openVault(passphrase, vaultPath);
|
|
@@ -38270,7 +38271,7 @@ __export(exports_tmux, {
|
|
|
38270
38271
|
captureAgentPane: () => captureAgentPane
|
|
38271
38272
|
});
|
|
38272
38273
|
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
38273
|
-
import { mkdirSync as mkdirSync38, readdirSync as
|
|
38274
|
+
import { mkdirSync as mkdirSync38, readdirSync as readdirSync11, statSync as statSync15, unlinkSync as unlinkSync20, writeFileSync as writeFileSync41 } from "node:fs";
|
|
38274
38275
|
import { resolve as resolve10 } from "node:path";
|
|
38275
38276
|
function captureAgentPane(opts) {
|
|
38276
38277
|
const { agentName: agentName3, agentDir, reason } = opts;
|
|
@@ -38316,7 +38317,7 @@ function captureAgentPane(opts) {
|
|
|
38316
38317
|
` + `
|
|
38317
38318
|
`;
|
|
38318
38319
|
try {
|
|
38319
|
-
|
|
38320
|
+
writeFileSync41(outPath, Buffer.concat([Buffer.from(header, "utf8"), body]), {
|
|
38320
38321
|
mode: 420
|
|
38321
38322
|
});
|
|
38322
38323
|
} catch (err) {
|
|
@@ -38386,7 +38387,7 @@ function sleepSync2(ms) {
|
|
|
38386
38387
|
function pruneOldReports(dir, retain) {
|
|
38387
38388
|
let entries;
|
|
38388
38389
|
try {
|
|
38389
|
-
entries =
|
|
38390
|
+
entries = readdirSync11(dir);
|
|
38390
38391
|
} catch {
|
|
38391
38392
|
return;
|
|
38392
38393
|
}
|
|
@@ -38400,7 +38401,7 @@ function pruneOldReports(dir, retain) {
|
|
|
38400
38401
|
}).sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
38401
38402
|
for (const stale of files.slice(retain)) {
|
|
38402
38403
|
try {
|
|
38403
|
-
|
|
38404
|
+
unlinkSync20(stale.full);
|
|
38404
38405
|
} catch {}
|
|
38405
38406
|
}
|
|
38406
38407
|
}
|
|
@@ -39020,19 +39021,19 @@ var import_runner3 = __toESM(require_mod3(), 1);
|
|
|
39020
39021
|
import { randomBytes as randomBytes9, createHash as createHash3 } from "crypto";
|
|
39021
39022
|
import { execFileSync as execFileSync8, execSync as execSync2, spawn } from "child_process";
|
|
39022
39023
|
import {
|
|
39023
|
-
readFileSync as
|
|
39024
|
-
writeFileSync as
|
|
39024
|
+
readFileSync as readFileSync53,
|
|
39025
|
+
writeFileSync as writeFileSync42,
|
|
39025
39026
|
mkdirSync as mkdirSync39,
|
|
39026
|
-
readdirSync as
|
|
39027
|
+
readdirSync as readdirSync12,
|
|
39027
39028
|
rmSync as rmSync5,
|
|
39028
39029
|
statSync as statSync16,
|
|
39029
|
-
renameSync as
|
|
39030
|
+
renameSync as renameSync17,
|
|
39030
39031
|
realpathSync as realpathSync4,
|
|
39031
39032
|
chmodSync as chmodSync8,
|
|
39032
|
-
openSync as
|
|
39033
|
-
closeSync as
|
|
39034
|
-
existsSync as
|
|
39035
|
-
unlinkSync as
|
|
39033
|
+
openSync as openSync11,
|
|
39034
|
+
closeSync as closeSync11,
|
|
39035
|
+
existsSync as existsSync50,
|
|
39036
|
+
unlinkSync as unlinkSync21,
|
|
39036
39037
|
appendFileSync as appendFileSync6
|
|
39037
39038
|
} from "fs";
|
|
39038
39039
|
import { homedir as homedir17 } from "os";
|
|
@@ -39326,6 +39327,22 @@ function resolveSafeBoundaryEnabled(configured) {
|
|
|
39326
39327
|
return configured !== false;
|
|
39327
39328
|
}
|
|
39328
39329
|
|
|
39330
|
+
// gateway/stop-command.ts
|
|
39331
|
+
function parseStopKeyword(text) {
|
|
39332
|
+
return /^stop[.!]?$/i.test(text.trim());
|
|
39333
|
+
}
|
|
39334
|
+
function buildStopReply(turnInFlight, queuedSessionCmds) {
|
|
39335
|
+
if (!turnInFlight) {
|
|
39336
|
+
return { text: "Nothing running \u2014 no in-flight turn to cancel." };
|
|
39337
|
+
}
|
|
39338
|
+
const lines = ["\u23f9 Turn cancelled."];
|
|
39339
|
+
if (queuedSessionCmds.length > 0) {
|
|
39340
|
+
lines.push(`Queued command${queuedSessionCmds.length > 1 ? "s" : ""} (${queuedSessionCmds.join(", ")}) survive${queuedSessionCmds.length > 1 ? "" : "s"} and will apply once the session is idle.`);
|
|
39341
|
+
}
|
|
39342
|
+
return { text: lines.join(`
|
|
39343
|
+
`) };
|
|
39344
|
+
}
|
|
39345
|
+
|
|
39329
39346
|
// gateway/busy-ack.ts
|
|
39330
39347
|
var BUSY_ACK_STEP_AGE_THRESHOLD_MS = 12000;
|
|
39331
39348
|
function shouldPostBusyAck(input) {
|
|
@@ -42199,6 +42216,17 @@ function createWorkerActivityFeed(opts) {
|
|
|
42199
42216
|
markFinalized(agentId);
|
|
42200
42217
|
handles.delete(agentId);
|
|
42201
42218
|
},
|
|
42219
|
+
resurrect(agentId) {
|
|
42220
|
+
const wasFinalized = finalized.delete(agentId);
|
|
42221
|
+
const h = handles.get(agentId);
|
|
42222
|
+
if (h != null) {
|
|
42223
|
+
h.finished = false;
|
|
42224
|
+
h.pendingFinish = null;
|
|
42225
|
+
}
|
|
42226
|
+
if (wasFinalized || h != null) {
|
|
42227
|
+
log(`worker-feed: resurrect agent=${agentId} \u2014 cleared finalized gate; card will repaint on next running cue`);
|
|
42228
|
+
}
|
|
42229
|
+
},
|
|
42202
42230
|
heartbeatTick,
|
|
42203
42231
|
stop() {
|
|
42204
42232
|
if (heartbeatTimer != null) {
|
|
@@ -42568,6 +42596,17 @@ function buildTimedOutCardEdits(cardText, cards) {
|
|
|
42568
42596
|
stripKeyboard: true
|
|
42569
42597
|
}));
|
|
42570
42598
|
}
|
|
42599
|
+
var CANCELLED_FOOTER = `
|
|
42600
|
+
|
|
42601
|
+
\u23f9 Cancelled \u2014 the turn was stopped`;
|
|
42602
|
+
function buildCancelledCardEdits(cardText, cards) {
|
|
42603
|
+
return cards.map(({ chatId, messageId }) => ({
|
|
42604
|
+
chatId,
|
|
42605
|
+
messageId,
|
|
42606
|
+
text: `${cardText}${CANCELLED_FOOTER}`,
|
|
42607
|
+
stripKeyboard: true
|
|
42608
|
+
}));
|
|
42609
|
+
}
|
|
42571
42610
|
var STALE_TAP_NOTICE = "This request already resolved (timed out) \u2014 ask again to act.";
|
|
42572
42611
|
|
|
42573
42612
|
// shared/bot-runtime.ts
|
|
@@ -56238,6 +56277,7 @@ function createChatLock() {
|
|
|
56238
56277
|
if (chains.get(key) === tracked)
|
|
56239
56278
|
chains.delete(key);
|
|
56240
56279
|
});
|
|
56280
|
+
tracked.catch(() => {});
|
|
56241
56281
|
chains.set(key, tracked);
|
|
56242
56282
|
return next;
|
|
56243
56283
|
}
|
|
@@ -56378,6 +56418,146 @@ async function retryWithThreadFallback2(retry, send, opts) {
|
|
|
56378
56418
|
throw err;
|
|
56379
56419
|
}
|
|
56380
56420
|
}
|
|
56421
|
+
function isPhotoDimensionRejectError(err) {
|
|
56422
|
+
if (!(err instanceof import_grammy6.GrammyError) || err.error_code !== 400)
|
|
56423
|
+
return false;
|
|
56424
|
+
const d = (err.description || "").toLowerCase();
|
|
56425
|
+
return d.includes("photo_invalid_dimensions") || d.includes("photo_save_file_invalid") || d.includes("photo dimensions") || d.includes("image_process_failed") || d.includes("photo is too big") || d.includes("too big for a photo") || d.includes("image is too big") || d.includes("file is too big");
|
|
56426
|
+
}
|
|
56427
|
+
|
|
56428
|
+
// photo-precheck.ts
|
|
56429
|
+
import { closeSync as closeSync2, fstatSync, openSync as openSync2, readSync } from "node:fs";
|
|
56430
|
+
var PHOTO_MAX_DIMENSION_SUM = 1e4;
|
|
56431
|
+
var PHOTO_MAX_ASPECT_RATIO = 10;
|
|
56432
|
+
var PHOTO_MAX_BYTES = 10 * 1024 * 1024;
|
|
56433
|
+
function pngDimensions(buf) {
|
|
56434
|
+
if (buf.length < 24)
|
|
56435
|
+
return null;
|
|
56436
|
+
if (buf.readUInt32BE(0) !== 2303741511 || buf.readUInt32BE(4) !== 218765834)
|
|
56437
|
+
return null;
|
|
56438
|
+
if (buf.toString("ascii", 12, 16) !== "IHDR")
|
|
56439
|
+
return null;
|
|
56440
|
+
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
|
|
56441
|
+
}
|
|
56442
|
+
function jpegDimensions(buf) {
|
|
56443
|
+
if (buf.length < 4 || buf[0] !== 255 || buf[1] !== 216)
|
|
56444
|
+
return null;
|
|
56445
|
+
let off = 2;
|
|
56446
|
+
while (off + 9 < buf.length) {
|
|
56447
|
+
if (buf[off] !== 255) {
|
|
56448
|
+
off++;
|
|
56449
|
+
continue;
|
|
56450
|
+
}
|
|
56451
|
+
const marker = buf[off + 1];
|
|
56452
|
+
if (marker === 216 || marker >= 208 && marker <= 215 || marker === 1 || marker === 255) {
|
|
56453
|
+
off += 2;
|
|
56454
|
+
continue;
|
|
56455
|
+
}
|
|
56456
|
+
const len = buf.readUInt16BE(off + 2);
|
|
56457
|
+
if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
|
|
56458
|
+
return { height: buf.readUInt16BE(off + 5), width: buf.readUInt16BE(off + 7) };
|
|
56459
|
+
}
|
|
56460
|
+
if (len < 2)
|
|
56461
|
+
return null;
|
|
56462
|
+
off += 2 + len;
|
|
56463
|
+
}
|
|
56464
|
+
return null;
|
|
56465
|
+
}
|
|
56466
|
+
function gifDimensions(buf) {
|
|
56467
|
+
if (buf.length < 10)
|
|
56468
|
+
return null;
|
|
56469
|
+
const sig = buf.toString("ascii", 0, 6);
|
|
56470
|
+
if (sig !== "GIF87a" && sig !== "GIF89a")
|
|
56471
|
+
return null;
|
|
56472
|
+
return { width: buf.readUInt16LE(6), height: buf.readUInt16LE(8) };
|
|
56473
|
+
}
|
|
56474
|
+
function webpDimensions(buf) {
|
|
56475
|
+
if (buf.length < 30)
|
|
56476
|
+
return null;
|
|
56477
|
+
if (buf.toString("ascii", 0, 4) !== "RIFF" || buf.toString("ascii", 8, 12) !== "WEBP")
|
|
56478
|
+
return null;
|
|
56479
|
+
const chunk = buf.toString("ascii", 12, 16);
|
|
56480
|
+
if (chunk === "VP8X") {
|
|
56481
|
+
const width = 1 + (buf[24] | buf[25] << 8 | buf[26] << 16);
|
|
56482
|
+
const height = 1 + (buf[27] | buf[28] << 8 | buf[29] << 16);
|
|
56483
|
+
return { width, height };
|
|
56484
|
+
}
|
|
56485
|
+
if (chunk === "VP8L") {
|
|
56486
|
+
if (buf[20] !== 47)
|
|
56487
|
+
return null;
|
|
56488
|
+
const b0 = buf[21], b1 = buf[22], b2 = buf[23], b3 = buf[24];
|
|
56489
|
+
const width = 1 + ((b1 & 63) << 8 | b0);
|
|
56490
|
+
const height = 1 + ((b3 & 15) << 10 | b2 << 2 | (b1 & 192) >> 6);
|
|
56491
|
+
return { width, height };
|
|
56492
|
+
}
|
|
56493
|
+
if (chunk === "VP8 ") {
|
|
56494
|
+
if (buf[23] !== 157 || buf[24] !== 1 || buf[25] !== 42)
|
|
56495
|
+
return null;
|
|
56496
|
+
const width = buf.readUInt16LE(26) & 16383;
|
|
56497
|
+
const height = buf.readUInt16LE(28) & 16383;
|
|
56498
|
+
return { width, height };
|
|
56499
|
+
}
|
|
56500
|
+
return null;
|
|
56501
|
+
}
|
|
56502
|
+
function probeImageDimensions(buf) {
|
|
56503
|
+
return pngDimensions(buf) ?? jpegDimensions(buf) ?? gifDimensions(buf) ?? webpDimensions(buf);
|
|
56504
|
+
}
|
|
56505
|
+
var HEADER_PROBE_BYTES = 32;
|
|
56506
|
+
var JPEG_PROBE_BYTES = 64 * 1024;
|
|
56507
|
+
function probeImageFile(path2) {
|
|
56508
|
+
let fd;
|
|
56509
|
+
try {
|
|
56510
|
+
fd = openSync2(path2, "r");
|
|
56511
|
+
const bytes = fstatSync(fd).size;
|
|
56512
|
+
const headBuf = Buffer.alloc(HEADER_PROBE_BYTES);
|
|
56513
|
+
const headRead = readSync(fd, headBuf, 0, HEADER_PROBE_BYTES, 0);
|
|
56514
|
+
let probe = headBuf.subarray(0, headRead);
|
|
56515
|
+
if (headRead >= 2 && probe[0] === 255 && probe[1] === 216) {
|
|
56516
|
+
const jpegBuf = Buffer.alloc(Math.min(JPEG_PROBE_BYTES, bytes));
|
|
56517
|
+
const jpegRead = readSync(fd, jpegBuf, 0, jpegBuf.length, 0);
|
|
56518
|
+
probe = jpegBuf.subarray(0, jpegRead);
|
|
56519
|
+
}
|
|
56520
|
+
const dims = probeImageDimensions(probe);
|
|
56521
|
+
if (!dims || dims.width <= 0 || dims.height <= 0)
|
|
56522
|
+
return null;
|
|
56523
|
+
return { ...dims, bytes };
|
|
56524
|
+
} catch {
|
|
56525
|
+
return null;
|
|
56526
|
+
} finally {
|
|
56527
|
+
if (fd !== undefined) {
|
|
56528
|
+
try {
|
|
56529
|
+
closeSync2(fd);
|
|
56530
|
+
} catch {}
|
|
56531
|
+
}
|
|
56532
|
+
}
|
|
56533
|
+
}
|
|
56534
|
+
function photoSendViolation(probe) {
|
|
56535
|
+
const { width, height, bytes } = probe;
|
|
56536
|
+
if (width + height > PHOTO_MAX_DIMENSION_SUM) {
|
|
56537
|
+
return `dimensions ${width}x${height} exceed width+height<=${PHOTO_MAX_DIMENSION_SUM}`;
|
|
56538
|
+
}
|
|
56539
|
+
const ratio = Math.max(width, height) / Math.min(width, height);
|
|
56540
|
+
if (ratio > PHOTO_MAX_ASPECT_RATIO) {
|
|
56541
|
+
return `aspect ratio ${ratio.toFixed(1)}:1 (${width}x${height}) exceeds ${PHOTO_MAX_ASPECT_RATIO}:1 photo bound`;
|
|
56542
|
+
}
|
|
56543
|
+
if (bytes > PHOTO_MAX_BYTES) {
|
|
56544
|
+
return `file size ${(bytes / (1024 * 1024)).toFixed(1)}MB exceeds ${PHOTO_MAX_BYTES / (1024 * 1024)}MB photo ceiling`;
|
|
56545
|
+
}
|
|
56546
|
+
return null;
|
|
56547
|
+
}
|
|
56548
|
+
function rerouteResultSuffix(reroutes) {
|
|
56549
|
+
if (reroutes.length === 0)
|
|
56550
|
+
return "";
|
|
56551
|
+
const details = reroutes.map((r) => `${r.path.split("/").pop() ?? r.path}: ${r.reason}`).join("; ");
|
|
56552
|
+
return ` (${reroutes.length} file(s) sent as document, not inline photo: ${details})`;
|
|
56553
|
+
}
|
|
56554
|
+
function classifyPhotoFile(path2) {
|
|
56555
|
+
const probe = probeImageFile(path2);
|
|
56556
|
+
if (!probe)
|
|
56557
|
+
return { route: "photo" };
|
|
56558
|
+
const violation = photoSendViolation(probe);
|
|
56559
|
+
return violation ? { route: "document", reason: violation } : { route: "photo" };
|
|
56560
|
+
}
|
|
56381
56561
|
|
|
56382
56562
|
// shared/bot-runtime.ts
|
|
56383
56563
|
var import_grammy7 = __toESM(require_mod2(), 1);
|
|
@@ -67772,7 +67952,7 @@ function parseModelCommand(text4) {
|
|
|
67772
67952
|
}
|
|
67773
67953
|
return { kind: "set", model: arg };
|
|
67774
67954
|
}
|
|
67775
|
-
var PERSIST_NOTE = "_Sticky
|
|
67955
|
+
var PERSIST_NOTE = "_Sticky \u2014 persists across restarts, deploys, and crashes until `/model default` clears it (or the configured `model:` in switchroom.yaml changes, which resets it and notifies you). To change the default, set `model:` in switchroom.yaml._";
|
|
67776
67956
|
function helpText2(deps, reason) {
|
|
67777
67957
|
const srAliasExamples = Object.keys(SR_MODEL_ALIASES).map((a) => `\`${a}\``).join(" \u00b7 ");
|
|
67778
67958
|
const lines = [];
|
|
@@ -67808,7 +67988,7 @@ async function handleModelCommand(parsed, deps) {
|
|
|
67808
67988
|
const model = expandSrAlias(parsed.model);
|
|
67809
67989
|
if (deps.isBusy()) {
|
|
67810
67990
|
return {
|
|
67811
|
-
text: "\u23f3 The agent is mid-turn \u2014 a model switch needs an idle session.
|
|
67991
|
+
text: "\u23f3 The agent is mid-turn \u2014 a model switch needs an idle session. The switch was not applied.",
|
|
67812
67992
|
html: true
|
|
67813
67993
|
};
|
|
67814
67994
|
}
|
|
@@ -67967,6 +68147,13 @@ var SR_MODEL_ALIASES = {
|
|
|
67967
68147
|
glm: "sr-glm-5",
|
|
67968
68148
|
codex: "sr-codex-5.5"
|
|
67969
68149
|
};
|
|
68150
|
+
function isOfflineTrustedModelToken(token) {
|
|
68151
|
+
if (MODEL_ALIASES.includes(token))
|
|
68152
|
+
return true;
|
|
68153
|
+
if (token in SR_MODEL_ALIASES)
|
|
68154
|
+
return true;
|
|
68155
|
+
return Object.values(SR_MODEL_ALIASES).includes(token);
|
|
68156
|
+
}
|
|
67970
68157
|
function expandSrAlias(arg) {
|
|
67971
68158
|
return SR_MODEL_ALIASES[arg.toLowerCase()] ?? arg;
|
|
67972
68159
|
}
|
|
@@ -67982,10 +68169,49 @@ function classifyDiscoveredOptions(options) {
|
|
|
67982
68169
|
function modelSelectCallbackData(label) {
|
|
67983
68170
|
return `${MODEL_CALLBACK_SELECT}${labelTag(label)}`;
|
|
67984
68171
|
}
|
|
67985
|
-
|
|
68172
|
+
var BUSY_REFUSAL_TEXT = "\u23f3 The agent is mid-turn \u2014 the model picker needs an idle prompt. Your tap was not applied.";
|
|
68173
|
+
function isBusyRefusalText(text4) {
|
|
68174
|
+
return text4.includes("The agent is mid-turn");
|
|
68175
|
+
}
|
|
68176
|
+
function busyStaticMenu(deps, page) {
|
|
68177
|
+
const externalNames = externalModelNames([]);
|
|
68178
|
+
if (page === "external") {
|
|
68179
|
+
return {
|
|
68180
|
+
text: [
|
|
68181
|
+
`**Model \u2014 ${deps.escapeHtml(deps.getAgentName())}** \u00b7 \uD83C\uDF10 External`,
|
|
68182
|
+
"_Agent is mid-turn \u2014 taps are queued and apply the moment the turn ends._",
|
|
68183
|
+
"These models are **billed separately** via OpenRouter. Tap one to switch the **live session**:",
|
|
68184
|
+
PERSIST_NOTE
|
|
68185
|
+
].join(`
|
|
68186
|
+
`),
|
|
68187
|
+
html: true,
|
|
68188
|
+
keyboard: externalPageKeyboard(externalNames)
|
|
68189
|
+
};
|
|
68190
|
+
}
|
|
68191
|
+
const rows = [];
|
|
68192
|
+
for (const alias of MODEL_ALIASES) {
|
|
68193
|
+
if (alias === "default")
|
|
68194
|
+
continue;
|
|
68195
|
+
rows.push([{
|
|
68196
|
+
text: alias.charAt(0).toUpperCase() + alias.slice(1),
|
|
68197
|
+
callback_data: `${MODEL_CALLBACK_ALIAS}${alias}`
|
|
68198
|
+
}]);
|
|
68199
|
+
}
|
|
68200
|
+
rows.push([{ text: "Default (configured)", callback_data: `${MODEL_CALLBACK_ALIAS}default` }]);
|
|
68201
|
+
if (externalNames.length > 0) {
|
|
68202
|
+
rows.push([{ text: "\uD83C\uDF10 External models \u25b8", callback_data: MODEL_CALLBACK_PAGE_EXTERNAL }]);
|
|
68203
|
+
}
|
|
68204
|
+
rows.push([{ text: "\uD83D\uDD04 Refresh", callback_data: MODEL_CALLBACK_REFRESH }]);
|
|
67986
68205
|
return {
|
|
67987
|
-
text:
|
|
67988
|
-
|
|
68206
|
+
text: [
|
|
68207
|
+
`**Model \u2014 ${deps.escapeHtml(deps.getAgentName())}**`,
|
|
68208
|
+
"_Agent is mid-turn, so the live picker can\u2019t be read right now \u2014 this is the quick list. A tap is queued and applies the moment the turn ends._",
|
|
68209
|
+
"Tap a model to switch the **live session**:",
|
|
68210
|
+
PERSIST_NOTE
|
|
68211
|
+
].join(`
|
|
68212
|
+
`),
|
|
68213
|
+
html: true,
|
|
68214
|
+
keyboard: rows
|
|
67989
68215
|
};
|
|
67990
68216
|
}
|
|
67991
68217
|
function headerRow(label) {
|
|
@@ -68034,7 +68260,7 @@ function externalPageKeyboard(externalNames) {
|
|
|
68034
68260
|
}
|
|
68035
68261
|
async function buildModelMenu(deps, page = "main") {
|
|
68036
68262
|
if (deps.isBusy())
|
|
68037
|
-
return
|
|
68263
|
+
return busyStaticMenu(deps, page);
|
|
68038
68264
|
const [discovered, quota, srNames] = await Promise.all([
|
|
68039
68265
|
deps.discover(deps.getAgentName()),
|
|
68040
68266
|
deps.getQuotaBrief().catch(() => null),
|
|
@@ -68099,8 +68325,9 @@ async function handleModelMenuCallback(data, deps) {
|
|
|
68099
68325
|
if (deps.isBusy()) {
|
|
68100
68326
|
return {
|
|
68101
68327
|
answer: "\u23f3 Agent is mid-turn \u2014 tap again when it\u2019s idle",
|
|
68102
|
-
reply:
|
|
68103
|
-
toastOnly: true
|
|
68328
|
+
reply: { text: BUSY_REFUSAL_TEXT, html: true },
|
|
68329
|
+
toastOnly: true,
|
|
68330
|
+
busyRefusal: true
|
|
68104
68331
|
};
|
|
68105
68332
|
}
|
|
68106
68333
|
let aliasResult;
|
|
@@ -68142,8 +68369,9 @@ async function handleModelMenuCallback(data, deps) {
|
|
|
68142
68369
|
if (deps.isBusy()) {
|
|
68143
68370
|
return {
|
|
68144
68371
|
answer: "\u23f3 Agent is mid-turn \u2014 tap again when it\u2019s idle",
|
|
68145
|
-
reply:
|
|
68146
|
-
toastOnly: true
|
|
68372
|
+
reply: { text: BUSY_REFUSAL_TEXT, html: true },
|
|
68373
|
+
toastOnly: true,
|
|
68374
|
+
busyRefusal: true
|
|
68147
68375
|
};
|
|
68148
68376
|
}
|
|
68149
68377
|
try {
|
|
@@ -68168,8 +68396,9 @@ ${PERSIST_NOTE}`),
|
|
|
68168
68396
|
if (deps.isBusy()) {
|
|
68169
68397
|
return {
|
|
68170
68398
|
answer: "\u23f3 Agent is mid-turn \u2014 tap again when it\u2019s idle",
|
|
68171
|
-
reply:
|
|
68172
|
-
toastOnly: true
|
|
68399
|
+
reply: { text: BUSY_REFUSAL_TEXT, html: true },
|
|
68400
|
+
toastOnly: true,
|
|
68401
|
+
busyRefusal: true
|
|
68173
68402
|
};
|
|
68174
68403
|
}
|
|
68175
68404
|
const tag = data.slice(MODEL_CALLBACK_SELECT.length);
|
|
@@ -68278,9 +68507,8 @@ function isValidModelArg2(arg) {
|
|
|
68278
68507
|
var SESSION_MODEL_FILE = ".session-model";
|
|
68279
68508
|
var RELAUNCH_MODEL_INTENT_FILE = ".relaunch-model-intent";
|
|
68280
68509
|
var CONFIGURED_DEFAULT_MODEL_FILE = ".configured-default-model";
|
|
68281
|
-
|
|
68282
|
-
|
|
68283
|
-
return REVERT_RESTART_REASONS.has(reason) ? "revert" : "keep";
|
|
68510
|
+
function intentForRestartReason(_reason) {
|
|
68511
|
+
return "keep";
|
|
68284
68512
|
}
|
|
68285
68513
|
function atomicWrite(path2, content3) {
|
|
68286
68514
|
const tmp = `${path2}.tmp-${process.pid}-${Date.now()}`;
|
|
@@ -68295,6 +68523,17 @@ function serializeSessionModel(rec) {
|
|
|
68295
68523
|
})}
|
|
68296
68524
|
`;
|
|
68297
68525
|
}
|
|
68526
|
+
function parseSessionModel(text4) {
|
|
68527
|
+
try {
|
|
68528
|
+
const raw = JSON.parse(text4);
|
|
68529
|
+
if (typeof raw.model !== "string" || typeof raw.configuredDefaultAtWrite !== "string" || typeof raw.ts !== "number" || !isValidModelArg2(raw.model)) {
|
|
68530
|
+
return null;
|
|
68531
|
+
}
|
|
68532
|
+
return { model: raw.model, configuredDefaultAtWrite: raw.configuredDefaultAtWrite, ts: raw.ts };
|
|
68533
|
+
} catch {
|
|
68534
|
+
return null;
|
|
68535
|
+
}
|
|
68536
|
+
}
|
|
68298
68537
|
function writeSessionModelFile(agentDir, model, configuredDefaultAtWrite) {
|
|
68299
68538
|
if (!isValidModelArg2(model)) {
|
|
68300
68539
|
throw new Error(`refusing to persist non-canonical session model token: ${JSON.stringify(model)}`);
|
|
@@ -68308,9 +68547,15 @@ function readSessionModelFileRaw(agentDir) {
|
|
|
68308
68547
|
return null;
|
|
68309
68548
|
}
|
|
68310
68549
|
}
|
|
68550
|
+
function readSessionModelFile(agentDir) {
|
|
68551
|
+
const raw = readSessionModelFileRaw(agentDir);
|
|
68552
|
+
return raw == null ? null : parseSessionModel(raw);
|
|
68553
|
+
}
|
|
68311
68554
|
function clearSessionModelFile(agentDir) {
|
|
68312
68555
|
try {
|
|
68313
68556
|
rmSync4(join28(agentDir, SESSION_MODEL_FILE), { force: true });
|
|
68557
|
+
rmSync4(join28(agentDir, ".session-model-kept-notified"), { force: true });
|
|
68558
|
+
rmSync4(join28(agentDir, ".session-model-boot-attempts"), { force: true });
|
|
68314
68559
|
} catch {}
|
|
68315
68560
|
}
|
|
68316
68561
|
function restoreSessionModelFileRaw(agentDir, raw) {
|
|
@@ -68331,6 +68576,26 @@ function writeRelaunchModelIntent(agentDir, intent, reason) {
|
|
|
68331
68576
|
`);
|
|
68332
68577
|
}
|
|
68333
68578
|
}
|
|
68579
|
+
var GATEWAY_SHUTDOWN_INTENT_REASON_PREFIX = "gateway-shutdown:";
|
|
68580
|
+
function readRelaunchModelIntent(agentDir) {
|
|
68581
|
+
try {
|
|
68582
|
+
const raw = readFileSync24(join28(agentDir, RELAUNCH_MODEL_INTENT_FILE), "utf8");
|
|
68583
|
+
const parsed = JSON.parse(raw);
|
|
68584
|
+
if (parsed.intent !== "keep" && parsed.intent !== "revert" || typeof parsed.reason !== "string" || typeof parsed.ts !== "number") {
|
|
68585
|
+
return null;
|
|
68586
|
+
}
|
|
68587
|
+
return { intent: parsed.intent, reason: parsed.reason, ts: parsed.ts };
|
|
68588
|
+
} catch {
|
|
68589
|
+
return null;
|
|
68590
|
+
}
|
|
68591
|
+
}
|
|
68592
|
+
function clearStaleGatewayShutdownIntent(agentDir) {
|
|
68593
|
+
const rec = readRelaunchModelIntent(agentDir);
|
|
68594
|
+
if (rec == null || !rec.reason.startsWith(GATEWAY_SHUTDOWN_INTENT_REASON_PREFIX))
|
|
68595
|
+
return false;
|
|
68596
|
+
clearRelaunchModelIntent(agentDir);
|
|
68597
|
+
return true;
|
|
68598
|
+
}
|
|
68334
68599
|
function clearRelaunchModelIntent(agentDir) {
|
|
68335
68600
|
try {
|
|
68336
68601
|
rmSync4(join28(agentDir, RELAUNCH_MODEL_INTENT_FILE), { force: true });
|
|
@@ -68344,6 +68609,38 @@ function readConfiguredDefaultModel(agentDir) {
|
|
|
68344
68609
|
return null;
|
|
68345
68610
|
}
|
|
68346
68611
|
}
|
|
68612
|
+
var SESSION_EFFORT_FILE = ".session-effort";
|
|
68613
|
+
var EFFORT_LEVEL_RE = /^(low|medium|high|xhigh|max)$/;
|
|
68614
|
+
function parseSessionEffort(text4) {
|
|
68615
|
+
try {
|
|
68616
|
+
const raw = JSON.parse(text4);
|
|
68617
|
+
if (typeof raw.level !== "string" || typeof raw.configuredDefaultAtWrite !== "string" || typeof raw.ts !== "number" || !EFFORT_LEVEL_RE.test(raw.level)) {
|
|
68618
|
+
return null;
|
|
68619
|
+
}
|
|
68620
|
+
return { level: raw.level, configuredDefaultAtWrite: raw.configuredDefaultAtWrite, ts: raw.ts };
|
|
68621
|
+
} catch {
|
|
68622
|
+
return null;
|
|
68623
|
+
}
|
|
68624
|
+
}
|
|
68625
|
+
function writeSessionEffortFile(agentDir, level, configuredDefaultAtWrite) {
|
|
68626
|
+
if (!EFFORT_LEVEL_RE.test(level)) {
|
|
68627
|
+
throw new Error(`refusing to persist non-allowlisted effort level: ${JSON.stringify(level)}`);
|
|
68628
|
+
}
|
|
68629
|
+
atomicWrite(join28(agentDir, SESSION_EFFORT_FILE), `${JSON.stringify({ level, configuredDefaultAtWrite: configuredDefaultAtWrite ?? "", ts: Date.now() })}
|
|
68630
|
+
`);
|
|
68631
|
+
}
|
|
68632
|
+
function readSessionEffortFile(agentDir) {
|
|
68633
|
+
try {
|
|
68634
|
+
return parseSessionEffort(readFileSync24(join28(agentDir, SESSION_EFFORT_FILE), "utf8"));
|
|
68635
|
+
} catch {
|
|
68636
|
+
return null;
|
|
68637
|
+
}
|
|
68638
|
+
}
|
|
68639
|
+
function clearSessionEffortFile(agentDir) {
|
|
68640
|
+
try {
|
|
68641
|
+
rmSync4(join28(agentDir, SESSION_EFFORT_FILE), { force: true });
|
|
68642
|
+
} catch {}
|
|
68643
|
+
}
|
|
68347
68644
|
|
|
68348
68645
|
// ../src/agents/model-picker.ts
|
|
68349
68646
|
var HEADER_RE = /Select model/;
|
|
@@ -68581,6 +68878,7 @@ import { homedir as homedir9 } from "node:os";
|
|
|
68581
68878
|
init_loader();
|
|
68582
68879
|
|
|
68583
68880
|
// ../src/cli/agent-config.ts
|
|
68881
|
+
init_loader();
|
|
68584
68882
|
init_merge();
|
|
68585
68883
|
|
|
68586
68884
|
// ../src/vault/broker/acl.ts
|
|
@@ -68608,7 +68906,7 @@ import_handlebars.default.registerHelper("isNumber", (value) => {
|
|
|
68608
68906
|
return typeof value === "number" && Number.isFinite(value);
|
|
68609
68907
|
});
|
|
68610
68908
|
var SHARED_FRAGMENTS_DIR = resolve5(PROFILES_ROOT, "_shared");
|
|
68611
|
-
var SHARED_FRAGMENTS = ["vault-protocol", "agent-self-service", "execution-discipline", "reply-discipline"];
|
|
68909
|
+
var SHARED_FRAGMENTS = ["vault-protocol", "agent-self-service", "execution-discipline", "reply-discipline", "dev-protocol"];
|
|
68612
68910
|
for (const name of SHARED_FRAGMENTS) {
|
|
68613
68911
|
const fragPath = join30(SHARED_FRAGMENTS_DIR, `${name}.md.hbs`);
|
|
68614
68912
|
if (existsSync26(fragPath)) {
|
|
@@ -68662,6 +68960,7 @@ init_paths();
|
|
|
68662
68960
|
// ../src/agents/scaffold.ts
|
|
68663
68961
|
var REPO_ROOT = resolve6(import.meta.dirname, "../..");
|
|
68664
68962
|
var SWITCHROOM_DEFAULT_MAIN_MODEL = "claude-sonnet-5";
|
|
68963
|
+
var SWITCHROOM_DEFAULT_THINKING_EFFORT = "low";
|
|
68665
68964
|
function resolveMainModel(model) {
|
|
68666
68965
|
if (model === undefined || model === "default")
|
|
68667
68966
|
return SWITCHROOM_DEFAULT_MAIN_MODEL;
|
|
@@ -68696,31 +68995,56 @@ function parseEffortCommand(text4) {
|
|
|
68696
68995
|
const arg = parts[0];
|
|
68697
68996
|
if (arg.toLowerCase() === "help")
|
|
68698
68997
|
return { kind: "help" };
|
|
68998
|
+
if (arg.toLowerCase() === "default")
|
|
68999
|
+
return { kind: "default" };
|
|
68699
69000
|
if (!isValidEffortArg(arg)) {
|
|
68700
69001
|
return { kind: "help", reason: `not a valid effort level: ${arg}` };
|
|
68701
69002
|
}
|
|
68702
69003
|
return { kind: "set", level: arg.toLowerCase() };
|
|
68703
69004
|
}
|
|
68704
|
-
var PERSIST_NOTE2 = "
|
|
69005
|
+
var PERSIST_NOTE2 = "_Sticky \u2014 persists across restarts and deploys until `/effort default` clears it. To change the configured default, set `thinking_effort:` in switchroom.yaml._";
|
|
68705
69006
|
var LEVELS_INLINE = EFFORT_LEVELS.map((l) => `\`${l}\``).join(" \u00b7 ");
|
|
68706
69007
|
function helpText3(deps, reason) {
|
|
68707
69008
|
const lines = [];
|
|
68708
69009
|
if (reason)
|
|
68709
69010
|
lines.push(`\u26a0\ufe0f ${deps.escapeHtml(reason)}`);
|
|
68710
|
-
lines.push("**/effort** \u2014 show or switch the reasoning effort (faster\u2192smarter)", "`/effort` \u2014 show the configured effort + a tap menu", `\`/effort <level>\` \u2014 switch the live session (${LEVELS_INLINE})`, PERSIST_NOTE2);
|
|
69011
|
+
lines.push("**/effort** \u2014 show or switch the reasoning effort (faster\u2192smarter)", "`/effort` \u2014 show the configured effort + a tap menu", `\`/effort <level>\` \u2014 switch the live session (${LEVELS_INLINE})`, "`/effort default` \u2014 clear the override, back to the configured default", PERSIST_NOTE2);
|
|
68711
69012
|
return { text: lines.join(`
|
|
68712
69013
|
`), html: true };
|
|
68713
69014
|
}
|
|
68714
69015
|
async function handleEffortCommand(parsed, deps) {
|
|
68715
69016
|
if (parsed.kind === "help")
|
|
68716
69017
|
return helpText3(deps, parsed.reason);
|
|
69018
|
+
if (parsed.kind === "default") {
|
|
69019
|
+
const configured = deps.getConfiguredEffort();
|
|
69020
|
+
const restoreLevel = configured && isValidEffortArg(configured) ? configured.toLowerCase() : "low";
|
|
69021
|
+
let restored = null;
|
|
69022
|
+
try {
|
|
69023
|
+
restored = await deps.applyEffort(deps.getAgentName(), restoreLevel);
|
|
69024
|
+
} catch {
|
|
69025
|
+
restored = null;
|
|
69026
|
+
}
|
|
69027
|
+
deps.clearSessionEffort?.();
|
|
69028
|
+
const cfgHtml = `\`${deps.escapeHtml(restoreLevel)}\``;
|
|
69029
|
+
if (restored?.ok) {
|
|
69030
|
+
return {
|
|
69031
|
+
text: `\u2705 Effort override cleared \u2014 back on the configured default ${cfgHtml}. It will also boot on ${cfgHtml} from now on.`,
|
|
69032
|
+
html: true
|
|
69033
|
+
};
|
|
69034
|
+
}
|
|
69035
|
+
return {
|
|
69036
|
+
text: `\u2705 Effort override cleared \u2014 the agent boots on the configured default ${cfgHtml} from now on. (Couldn't switch the live session right now; it may already be on ${cfgHtml}, or will be after the next restart.)`,
|
|
69037
|
+
html: true
|
|
69038
|
+
};
|
|
69039
|
+
}
|
|
68717
69040
|
if (parsed.kind === "show") {
|
|
68718
69041
|
const configured = deps.getConfiguredEffort();
|
|
68719
69042
|
const shown = configured && configured.length > 0 ? configured : "low";
|
|
69043
|
+
const override = deps.getSessionEffort?.() ?? null;
|
|
68720
69044
|
return {
|
|
68721
69045
|
text: [
|
|
68722
69046
|
`**Effort \u2014 ${deps.escapeHtml(deps.getAgentName())}**`,
|
|
68723
|
-
`Configured default: \`${deps.escapeHtml(shown)}
|
|
69047
|
+
`Configured default: \`${deps.escapeHtml(shown)}\`${override ? ` \u00b7 session override: \`${deps.escapeHtml(override)}\`` : ""}`,
|
|
68724
69048
|
`Switch the live session: ${EFFORT_LEVELS.map((l) => `\`/effort ${l}\``).join(" \u00b7 ")}`,
|
|
68725
69049
|
PERSIST_NOTE2
|
|
68726
69050
|
].join(`
|
|
@@ -68776,7 +69100,7 @@ function menuKeyboard(highlight) {
|
|
|
68776
69100
|
}
|
|
68777
69101
|
function buildEffortMenu(deps, highlight) {
|
|
68778
69102
|
const configured = deps.getConfiguredEffort() || "low";
|
|
68779
|
-
const live = highlight ?? configured;
|
|
69103
|
+
const live = highlight ?? deps.getSessionEffort?.() ?? configured;
|
|
68780
69104
|
return {
|
|
68781
69105
|
text: [
|
|
68782
69106
|
`**Effort \u2014 ${deps.escapeHtml(deps.getAgentName())}**`,
|
|
@@ -68823,6 +69147,122 @@ ${menu.text}` },
|
|
|
68823
69147
|
};
|
|
68824
69148
|
}
|
|
68825
69149
|
|
|
69150
|
+
// gateway/pending-session-command.ts
|
|
69151
|
+
function createPendingSessionCommandSlots() {
|
|
69152
|
+
const slots = new Map;
|
|
69153
|
+
return {
|
|
69154
|
+
get: (kind) => slots.get(kind) ?? null,
|
|
69155
|
+
set(cmd) {
|
|
69156
|
+
const displaced = slots.get(cmd.kind) ?? null;
|
|
69157
|
+
slots.delete(cmd.kind);
|
|
69158
|
+
slots.set(cmd.kind, cmd);
|
|
69159
|
+
return displaced;
|
|
69160
|
+
},
|
|
69161
|
+
take(kind) {
|
|
69162
|
+
const cur = slots.get(kind) ?? null;
|
|
69163
|
+
slots.delete(kind);
|
|
69164
|
+
return cur;
|
|
69165
|
+
},
|
|
69166
|
+
takeAll() {
|
|
69167
|
+
const all2 = [...slots.values()];
|
|
69168
|
+
slots.clear();
|
|
69169
|
+
return all2;
|
|
69170
|
+
},
|
|
69171
|
+
list: () => [...slots.values()],
|
|
69172
|
+
clear() {
|
|
69173
|
+
slots.clear();
|
|
69174
|
+
},
|
|
69175
|
+
get size() {
|
|
69176
|
+
return slots.size;
|
|
69177
|
+
}
|
|
69178
|
+
};
|
|
69179
|
+
}
|
|
69180
|
+
async function drainTakenCommands(taken, io) {
|
|
69181
|
+
for (let i = 0;i < taken.length; i++) {
|
|
69182
|
+
const cmd = taken[i];
|
|
69183
|
+
if (io.restartPending()) {
|
|
69184
|
+
await io.editCard(cmd, io.resolveForRestartText(cmd));
|
|
69185
|
+
continue;
|
|
69186
|
+
}
|
|
69187
|
+
if (io.turnInFlight()) {
|
|
69188
|
+
for (const rest of taken.slice(i))
|
|
69189
|
+
io.reEnqueue(rest);
|
|
69190
|
+
return;
|
|
69191
|
+
}
|
|
69192
|
+
let body;
|
|
69193
|
+
try {
|
|
69194
|
+
body = await io.apply(cmd);
|
|
69195
|
+
} catch (err) {
|
|
69196
|
+
await io.editCard(cmd, io.failureText(cmd, err));
|
|
69197
|
+
continue;
|
|
69198
|
+
}
|
|
69199
|
+
if (io.isBusyRefusal(body)) {
|
|
69200
|
+
for (const rest of taken.slice(i))
|
|
69201
|
+
io.reEnqueue(rest);
|
|
69202
|
+
return;
|
|
69203
|
+
}
|
|
69204
|
+
await io.editCard(cmd, body);
|
|
69205
|
+
}
|
|
69206
|
+
}
|
|
69207
|
+
function drainCapDecision(queued, now, capMs, turnInFlight) {
|
|
69208
|
+
const overdue = queued.some((c) => now - c.requestedAt > capMs);
|
|
69209
|
+
if (!overdue)
|
|
69210
|
+
return "wait";
|
|
69211
|
+
return turnInFlight ? "defer-turn-in-flight" : "force";
|
|
69212
|
+
}
|
|
69213
|
+
var EFFORT_MENU_SELECT_PREFIX = "eff:s:";
|
|
69214
|
+
function classifyShutdownPersist(cmd) {
|
|
69215
|
+
if (cmd.kind === "effort") {
|
|
69216
|
+
const level = cmd.origin === "menu" && cmd.arg.startsWith(EFFORT_MENU_SELECT_PREFIX) ? cmd.arg.slice(EFFORT_MENU_SELECT_PREFIX.length) : cmd.arg;
|
|
69217
|
+
if (level === "default")
|
|
69218
|
+
return { persist: "clear-effort", arg: level };
|
|
69219
|
+
return { persist: "effort", arg: level };
|
|
69220
|
+
}
|
|
69221
|
+
if (cmd.origin === "menu")
|
|
69222
|
+
return { persist: null, arg: cmd.arg };
|
|
69223
|
+
if (cmd.arg === "default")
|
|
69224
|
+
return { persist: "clear-model", arg: cmd.arg };
|
|
69225
|
+
return { persist: "model", arg: cmd.arg };
|
|
69226
|
+
}
|
|
69227
|
+
function persistedText(cmd, escapeHtml5) {
|
|
69228
|
+
const noun = KIND_NOUN[cmd.kind];
|
|
69229
|
+
const isClear = cmd.arg === "default" || cmd.targetLabel === "default";
|
|
69230
|
+
if (isClear) {
|
|
69231
|
+
return `\uD83D\uDCBE The session is restarting \u2014 your ${noun} override was cleared as requested; the agent boots on the configured default.`;
|
|
69232
|
+
}
|
|
69233
|
+
return `\uD83D\uDCBE The session is restarting \u2014 your \`${escapeHtml5(cmd.targetLabel)}\` ${noun} choice is saved and applies as the agent boots.`;
|
|
69234
|
+
}
|
|
69235
|
+
function shutdownResolutionActions(slots, escapeHtml5) {
|
|
69236
|
+
return slots.takeAll().map((cmd) => resolveForRestart(cmd, escapeHtml5));
|
|
69237
|
+
}
|
|
69238
|
+
function resolveForRestart(cmd, escapeHtml5) {
|
|
69239
|
+
const { persist, arg } = classifyShutdownPersist(cmd);
|
|
69240
|
+
return {
|
|
69241
|
+
cmd,
|
|
69242
|
+
persist,
|
|
69243
|
+
arg,
|
|
69244
|
+
persistedText: persistedText(cmd, escapeHtml5),
|
|
69245
|
+
reissueText: restartSupersededText(cmd, escapeHtml5)
|
|
69246
|
+
};
|
|
69247
|
+
}
|
|
69248
|
+
var KIND_NOUN = {
|
|
69249
|
+
model: "model",
|
|
69250
|
+
effort: "effort"
|
|
69251
|
+
};
|
|
69252
|
+
var KIND_VERB = {
|
|
69253
|
+
model: "switch to",
|
|
69254
|
+
effort: "set effort to"
|
|
69255
|
+
};
|
|
69256
|
+
function ackText(kind, targetLabel, escapeHtml5) {
|
|
69257
|
+
return `\uD83D\uDCE5 Agent is mid-turn \u2014 I'll ${KIND_VERB[kind]} \`${escapeHtml5(targetLabel)}\` the moment this turn finishes.`;
|
|
69258
|
+
}
|
|
69259
|
+
function supersededText(displaced, next, escapeHtml5) {
|
|
69260
|
+
return `\u21a9\ufe0f Superseded \u2014 queued \`${escapeHtml5(next.targetLabel)}\` instead (your earlier \`${escapeHtml5(displaced.targetLabel)}\` ${KIND_NOUN[displaced.kind]} request was replaced before it applied).`;
|
|
69261
|
+
}
|
|
69262
|
+
function restartSupersededText(cmd, escapeHtml5) {
|
|
69263
|
+
return `\u21a9\ufe0f The session is restarting before your \`${escapeHtml5(cmd.targetLabel)}\` ${KIND_NOUN[cmd.kind]} change could apply \u2014 re-issue \`/${cmd.kind} ${escapeHtml5(cmd.targetLabel)}\` once the agent is back.`;
|
|
69264
|
+
}
|
|
69265
|
+
|
|
68826
69266
|
// welcome-text.ts
|
|
68827
69267
|
init_demo_mask();
|
|
68828
69268
|
init_card_format();
|
|
@@ -68866,11 +69306,12 @@ function switchroomHelpText(agentName3) {
|
|
|
68866
69306
|
`\`/deny [id]\` \u2014 deny pending tool permission`,
|
|
68867
69307
|
`\`/pending\` \u2014 list pending permission prompts`,
|
|
68868
69308
|
`\`/interrupt [name]\` \u2014 interrupt an agent turn`,
|
|
69309
|
+
`\`/stop\` \u2014 cancel my in-flight turn (bare "stop" works too)`,
|
|
68869
69310
|
``,
|
|
68870
69311
|
`**Agents**`,
|
|
68871
69312
|
`\`/agents\` \u2014 list all agents`,
|
|
68872
69313
|
`\`/agentstart [name]\` \u2014 start an agent`,
|
|
68873
|
-
`\`/
|
|
69314
|
+
`\`/agentstop [name]\` \u2014 stop an agent's container`,
|
|
68874
69315
|
`\`/logs [name] [lines]\` \u2014 show agent logs`,
|
|
68875
69316
|
`\`/memory <query>\` \u2014 search agent memory`,
|
|
68876
69317
|
``,
|
|
@@ -74127,10 +74568,10 @@ function skillProposalKeyboard(id) {
|
|
|
74127
74568
|
|
|
74128
74569
|
// ../src/self-improve/skill-proposals.ts
|
|
74129
74570
|
import {
|
|
74130
|
-
closeSync as
|
|
74571
|
+
closeSync as closeSync5,
|
|
74131
74572
|
existsSync as existsSync33,
|
|
74132
74573
|
mkdirSync as mkdirSync26,
|
|
74133
|
-
openSync as
|
|
74574
|
+
openSync as openSync5,
|
|
74134
74575
|
readFileSync as readFileSync30,
|
|
74135
74576
|
writeSync as writeSync4
|
|
74136
74577
|
} from "node:fs";
|
|
@@ -74152,12 +74593,12 @@ function ensureDir3(stateDir) {
|
|
|
74152
74593
|
}
|
|
74153
74594
|
}
|
|
74154
74595
|
function appendLine2(path2, obj) {
|
|
74155
|
-
const fd =
|
|
74596
|
+
const fd = openSync5(path2, "a");
|
|
74156
74597
|
try {
|
|
74157
74598
|
writeSync4(fd, JSON.stringify(obj) + `
|
|
74158
74599
|
`);
|
|
74159
74600
|
} finally {
|
|
74160
|
-
|
|
74601
|
+
closeSync5(fd);
|
|
74161
74602
|
}
|
|
74162
74603
|
}
|
|
74163
74604
|
function readLines2(path2, isValid2) {
|
|
@@ -75582,7 +76023,7 @@ var ADMIN_COMMAND_NAMES = new Set([
|
|
|
75582
76023
|
"agents",
|
|
75583
76024
|
"logs",
|
|
75584
76025
|
"restart",
|
|
75585
|
-
"
|
|
76026
|
+
"agentstop",
|
|
75586
76027
|
"agentstart",
|
|
75587
76028
|
"update",
|
|
75588
76029
|
"reconcile",
|
|
@@ -75629,10 +76070,10 @@ function classifyAdminGate(text4, myAgentName) {
|
|
|
75629
76070
|
// subagent-watcher.ts
|
|
75630
76071
|
import {
|
|
75631
76072
|
existsSync as existsSync35,
|
|
75632
|
-
openSync as
|
|
75633
|
-
readSync,
|
|
76073
|
+
openSync as openSync7,
|
|
76074
|
+
readSync as readSync2,
|
|
75634
76075
|
statSync as statSync10,
|
|
75635
|
-
closeSync as
|
|
76076
|
+
closeSync as closeSync7,
|
|
75636
76077
|
watch,
|
|
75637
76078
|
readdirSync as readdirSync6,
|
|
75638
76079
|
readFileSync as readFileSync36
|
|
@@ -76056,12 +76497,13 @@ function recordSubagentStall(db2, args) {
|
|
|
76056
76497
|
}
|
|
76057
76498
|
function reapStuckRunningRows(db2, args) {
|
|
76058
76499
|
const cutoff = args.now - args.ttlMs;
|
|
76059
|
-
const
|
|
76060
|
-
SELECT id FROM subagents
|
|
76500
|
+
const rows = db2.prepare(`
|
|
76501
|
+
SELECT id, jsonl_agent_id FROM subagents
|
|
76061
76502
|
WHERE status = 'running'
|
|
76062
76503
|
AND background = 1
|
|
76063
76504
|
AND COALESCE(last_activity_at, started_at) < ?
|
|
76064
76505
|
`).all(cutoff);
|
|
76506
|
+
const candidates = args.isLive ? rows.filter((r) => !args.isLive(r.jsonl_agent_id)) : rows;
|
|
76065
76507
|
for (const row of candidates) {
|
|
76066
76508
|
recordSubagentStall(db2, {
|
|
76067
76509
|
id: row.id,
|
|
@@ -76116,10 +76558,10 @@ function recordNestedSubagentDispatch(db2, args) {
|
|
|
76116
76558
|
|
|
76117
76559
|
// gateway/turn-active-marker.ts
|
|
76118
76560
|
import {
|
|
76119
|
-
closeSync as
|
|
76561
|
+
closeSync as closeSync6,
|
|
76120
76562
|
existsSync as existsSync34,
|
|
76121
76563
|
mkdirSync as mkdirSync27,
|
|
76122
|
-
openSync as
|
|
76564
|
+
openSync as openSync6,
|
|
76123
76565
|
readFileSync as readFileSync35,
|
|
76124
76566
|
statSync as statSync9,
|
|
76125
76567
|
unlinkSync as unlinkSync15,
|
|
@@ -76137,8 +76579,8 @@ function touchTurnActiveMarker(stateDir) {
|
|
|
76137
76579
|
utimesSync(path2, now, now);
|
|
76138
76580
|
} catch {
|
|
76139
76581
|
try {
|
|
76140
|
-
const fd =
|
|
76141
|
-
|
|
76582
|
+
const fd = openSync6(path2, "r+");
|
|
76583
|
+
closeSync6(fd);
|
|
76142
76584
|
} catch {}
|
|
76143
76585
|
}
|
|
76144
76586
|
}
|
|
@@ -76148,7 +76590,10 @@ var DEFAULT_RESCAN_MS = 1000;
|
|
|
76148
76590
|
var DEFAULT_STALL_THRESHOLD_MS = 60000;
|
|
76149
76591
|
var DEFAULT_SILENT_SYNTHESIS_STALL_THRESHOLD_MS = 300000;
|
|
76150
76592
|
var DEFAULT_SILENT_STALL_TERMINAL_MS = 300000;
|
|
76593
|
+
var DEFAULT_INFLIGHT_TERMINAL_CAP_MS = 45 * 60000;
|
|
76151
76594
|
var LONG_RUNNING_TOOLS = new Set(["Bash"]);
|
|
76595
|
+
var MAX_RESURRECTIONS = 1;
|
|
76596
|
+
var FALSE_FINISH_TRACKER_CAP = 512;
|
|
76152
76597
|
function isLongRunningTool(name) {
|
|
76153
76598
|
return name != null && LONG_RUNNING_TOOLS.has(name);
|
|
76154
76599
|
}
|
|
@@ -76403,6 +76848,9 @@ function readSubTail(entry, tail, now, onDescriptionUpdate, fs2, log, db2, paren
|
|
|
76403
76848
|
entry.lastReplyText = ev.input.text;
|
|
76404
76849
|
}
|
|
76405
76850
|
entry.toolCount++;
|
|
76851
|
+
if (ev.toolUseId != null && ev.toolUseId !== "") {
|
|
76852
|
+
entry.inflightToolUseIds.add(ev.toolUseId);
|
|
76853
|
+
}
|
|
76406
76854
|
entry.lastTool = {
|
|
76407
76855
|
name: ev.toolName,
|
|
76408
76856
|
sanitisedArg: sanitiseToolArg(ev.toolName, ev.input ?? {})
|
|
@@ -76431,6 +76879,9 @@ function readSubTail(entry, tail, now, onDescriptionUpdate, fs2, log, db2, paren
|
|
|
76431
76879
|
}
|
|
76432
76880
|
}
|
|
76433
76881
|
} else if (ev.kind === "sub_agent_nested_spawn") {
|
|
76882
|
+
if (ev.toolUseId != null && ev.toolUseId.length > 0) {
|
|
76883
|
+
entry.inflightToolUseIds.add(ev.toolUseId);
|
|
76884
|
+
}
|
|
76434
76885
|
if (db2 != null && ev.toolUseId != null && ev.toolUseId.length > 0) {
|
|
76435
76886
|
try {
|
|
76436
76887
|
const nestedInput = ev.input ?? {};
|
|
@@ -76454,7 +76905,12 @@ function readSubTail(entry, tail, now, onDescriptionUpdate, fs2, log, db2, paren
|
|
|
76454
76905
|
fireNarrativeProgress();
|
|
76455
76906
|
}
|
|
76456
76907
|
entry.pendingNarrative = { text: ev.text };
|
|
76908
|
+
} else if (ev.kind === "sub_agent_tool_result") {
|
|
76909
|
+
if (ev.toolUseId != null && ev.toolUseId !== "") {
|
|
76910
|
+
entry.inflightToolUseIds.delete(ev.toolUseId);
|
|
76911
|
+
}
|
|
76457
76912
|
} else if (ev.kind === "sub_agent_turn_end") {
|
|
76913
|
+
entry.inflightToolUseIds.clear();
|
|
76458
76914
|
resolvePendingSubNarrative(null, undefined);
|
|
76459
76915
|
if (entry.state === "running") {
|
|
76460
76916
|
entry.state = "done";
|
|
@@ -76495,6 +76951,7 @@ function startSubagentWatcher(config) {
|
|
|
76495
76951
|
const stallThresholdMs = config.stallThresholdMs ?? parseEnvMs("SWITCHROOM_SUBAGENT_STALL_MS") ?? DEFAULT_STALL_THRESHOLD_MS;
|
|
76496
76952
|
const silentSynthesisStallThresholdMs = config.silentSynthesisStallThresholdMs ?? parseEnvMs("SWITCHROOM_SUBAGENT_SILENT_SYNTH_STALL_MS") ?? DEFAULT_SILENT_SYNTHESIS_STALL_THRESHOLD_MS;
|
|
76497
76953
|
const silentStallTerminalMs = config.silentStallTerminalMs ?? parseEnvMs("SWITCHROOM_SUBAGENT_STALL_TERMINAL_MS") ?? DEFAULT_SILENT_STALL_TERMINAL_MS;
|
|
76954
|
+
const inflightTerminalCapMs = config.inflightTerminalCapMs ?? parseEnvMs("SWITCHROOM_SUBAGENT_INFLIGHT_TERMINAL_CAP_MS") ?? DEFAULT_INFLIGHT_TERMINAL_CAP_MS;
|
|
76498
76955
|
const inflightPromoteMaxAgeMs = config.inflightPromoteMaxAgeMs ?? parseEnvMs("SWITCHROOM_SUBAGENT_INFLIGHT_MAX_AGE_MS") ?? DEFAULT_INFLIGHT_PROMOTE_MAX_AGE_MS;
|
|
76499
76956
|
const terminatedAgentIdsCap = config.terminatedAgentIdsCap ?? TERMINATED_AGENT_IDS_CAP;
|
|
76500
76957
|
const bootPromoteEnabled = config.bootPromoteEnabled ?? process.env.SWITCHROOM_SUBAGENT_BOOT_PROMOTE !== "0";
|
|
@@ -76523,9 +76980,9 @@ function startSubagentWatcher(config) {
|
|
|
76523
76980
|
existsSync: existsSync35,
|
|
76524
76981
|
readdirSync: readdirSync6,
|
|
76525
76982
|
statSync: statSync10,
|
|
76526
|
-
openSync:
|
|
76527
|
-
closeSync:
|
|
76528
|
-
readSync,
|
|
76983
|
+
openSync: openSync7,
|
|
76984
|
+
closeSync: closeSync7,
|
|
76985
|
+
readSync: readSync2,
|
|
76529
76986
|
watch
|
|
76530
76987
|
};
|
|
76531
76988
|
const registry = new Map;
|
|
@@ -76535,6 +76992,7 @@ function startSubagentWatcher(config) {
|
|
|
76535
76992
|
const pendingCloses = new Map;
|
|
76536
76993
|
const historicalFiles = new Set;
|
|
76537
76994
|
const terminatedAgentIds = new Set;
|
|
76995
|
+
const falseFinishTracker = new Map;
|
|
76538
76996
|
let bootScanInProgress = true;
|
|
76539
76997
|
let stopped = false;
|
|
76540
76998
|
function registerAgent(filePath, agentId) {
|
|
@@ -76559,6 +77017,7 @@ function startSubagentWatcher(config) {
|
|
|
76559
77017
|
lastResultText: "",
|
|
76560
77018
|
lastProgressBucketIdx: null,
|
|
76561
77019
|
lastTool: null,
|
|
77020
|
+
inflightToolUseIds: new Set,
|
|
76562
77021
|
historical: isHistorical
|
|
76563
77022
|
};
|
|
76564
77023
|
registry.set(agentId, entry);
|
|
@@ -76739,6 +77198,93 @@ function startSubagentWatcher(config) {
|
|
|
76739
77198
|
terminatedAgentIds.add(agentId);
|
|
76740
77199
|
log?.(`subagent-watcher: cleaned up terminal agent ${agentId}`);
|
|
76741
77200
|
}
|
|
77201
|
+
function recordFalseFinish(agentId, filePath, n) {
|
|
77202
|
+
let size = 0;
|
|
77203
|
+
try {
|
|
77204
|
+
size = fs2.statSync(filePath).size;
|
|
77205
|
+
} catch {}
|
|
77206
|
+
const prior = falseFinishTracker.get(agentId);
|
|
77207
|
+
if (prior == null && falseFinishTracker.size >= FALSE_FINISH_TRACKER_CAP) {
|
|
77208
|
+
const oldest = falseFinishTracker.keys().next().value;
|
|
77209
|
+
if (oldest != null)
|
|
77210
|
+
falseFinishTracker.delete(oldest);
|
|
77211
|
+
}
|
|
77212
|
+
falseFinishTracker.set(agentId, {
|
|
77213
|
+
filePath,
|
|
77214
|
+
synthesisedAt: n,
|
|
77215
|
+
sizeAtSynthesis: size,
|
|
77216
|
+
resurrectionCount: prior?.resurrectionCount ?? 0,
|
|
77217
|
+
resurrectedForThisFinish: false,
|
|
77218
|
+
lost: prior?.lost ?? false
|
|
77219
|
+
});
|
|
77220
|
+
}
|
|
77221
|
+
function resurrectAgent(agentId, filePath) {
|
|
77222
|
+
const pc = pendingCloses.get(agentId);
|
|
77223
|
+
if (pc != null) {
|
|
77224
|
+
clearT(pc);
|
|
77225
|
+
pendingCloses.delete(agentId);
|
|
77226
|
+
}
|
|
77227
|
+
terminatedAgentIds.delete(agentId);
|
|
77228
|
+
historicalFiles.delete(filePath);
|
|
77229
|
+
const existing = registry.get(agentId);
|
|
77230
|
+
if (existing != null) {
|
|
77231
|
+
existing.state = "running";
|
|
77232
|
+
existing.historical = false;
|
|
77233
|
+
existing.stallNotified = false;
|
|
77234
|
+
existing.stalledAt = null;
|
|
77235
|
+
existing.stallTerminalSynthesised = false;
|
|
77236
|
+
existing.completionNotified = false;
|
|
77237
|
+
existing.errored = false;
|
|
77238
|
+
existing.errorDetail = undefined;
|
|
77239
|
+
existing.lastActivityAt = nowFn();
|
|
77240
|
+
knownFiles.add(filePath);
|
|
77241
|
+
return;
|
|
77242
|
+
}
|
|
77243
|
+
knownFiles.add(filePath);
|
|
77244
|
+
registerAgent(filePath, agentId);
|
|
77245
|
+
}
|
|
77246
|
+
function checkResurrections() {
|
|
77247
|
+
const n = nowFn();
|
|
77248
|
+
for (const [agentId, rec] of falseFinishTracker) {
|
|
77249
|
+
if (rec.lost)
|
|
77250
|
+
continue;
|
|
77251
|
+
if (rec.resurrectedForThisFinish)
|
|
77252
|
+
continue;
|
|
77253
|
+
let size;
|
|
77254
|
+
try {
|
|
77255
|
+
size = fs2.statSync(rec.filePath).size;
|
|
77256
|
+
} catch {
|
|
77257
|
+
continue;
|
|
77258
|
+
}
|
|
77259
|
+
if (size <= rec.sizeAtSynthesis)
|
|
77260
|
+
continue;
|
|
77261
|
+
const entry = registry.get(agentId);
|
|
77262
|
+
const description = entry?.description ?? "sub-agent";
|
|
77263
|
+
if (rec.resurrectionCount >= MAX_RESURRECTIONS) {
|
|
77264
|
+
rec.lost = true;
|
|
77265
|
+
log?.(`subagent-watcher: worker ${agentId} FALSELY finalised again after a prior resurrection (resurrectionCount=${rec.resurrectionCount} >= ${MAX_RESURRECTIONS}) \u2014 NAMED AS LOST, not resurrecting again (bounded resurrection chain, issue #3023)`);
|
|
77266
|
+
if (config.onWorkerLost != null) {
|
|
77267
|
+
try {
|
|
77268
|
+
config.onWorkerLost(agentId, description);
|
|
77269
|
+
} catch (cbErr) {
|
|
77270
|
+
log?.(`subagent-watcher: onWorkerLost callback error ${agentId}: ${cbErr.message}`);
|
|
77271
|
+
}
|
|
77272
|
+
}
|
|
77273
|
+
continue;
|
|
77274
|
+
}
|
|
77275
|
+
rec.resurrectionCount++;
|
|
77276
|
+
rec.resurrectedForThisFinish = true;
|
|
77277
|
+
log?.(`subagent-watcher: RESURRECTING worker ${agentId} \u2014 JSONL resumed growing (${rec.sizeAtSynthesis} \u2192 ${size} bytes) after a false terminal synthesis; the worker is still alive, reviving its card (resurrection #${rec.resurrectionCount}, issue #3023)`);
|
|
77278
|
+
if (config.onResurrect != null) {
|
|
77279
|
+
try {
|
|
77280
|
+
config.onResurrect(agentId, description);
|
|
77281
|
+
} catch (cbErr) {
|
|
77282
|
+
log?.(`subagent-watcher: onResurrect callback error ${agentId}: ${cbErr.message}`);
|
|
77283
|
+
}
|
|
77284
|
+
}
|
|
77285
|
+
resurrectAgent(agentId, rec.filePath);
|
|
77286
|
+
}
|
|
77287
|
+
}
|
|
76742
77288
|
function checkStalls() {
|
|
76743
77289
|
const n = nowFn();
|
|
76744
77290
|
for (const entry of registry.values()) {
|
|
@@ -76786,11 +77332,20 @@ function startSubagentWatcher(config) {
|
|
|
76786
77332
|
continue;
|
|
76787
77333
|
if (n - entry.stalledAt < silentStallTerminalMs)
|
|
76788
77334
|
continue;
|
|
77335
|
+
if (entry.inflightToolUseIds.size > 0) {
|
|
77336
|
+
const totalIdleMs = n - entry.lastActivityAt;
|
|
77337
|
+
if (totalIdleMs < inflightTerminalCapMs) {
|
|
77338
|
+
log?.(`subagent-watcher: silent-stall terminal synthesis deferred for ${entry.agentId} \u2014 ${entry.inflightToolUseIds.size} tool call(s) still in flight, ${Math.floor(totalIdleMs / 1000)}s idle < ${Math.floor(inflightTerminalCapMs / 1000)}s cap (long-running tool, not a dead worker)`);
|
|
77339
|
+
continue;
|
|
77340
|
+
}
|
|
77341
|
+
log?.(`subagent-watcher: in-flight deferral cap reached for ${entry.agentId} (${Math.floor(totalIdleMs / 1000)}s idle >= ${Math.floor(inflightTerminalCapMs / 1000)}s cap with ${entry.inflightToolUseIds.size} tool call(s) still unresolved) \u2014 treating as died-mid-tool, proceeding with terminal synthesis`);
|
|
77342
|
+
}
|
|
76789
77343
|
entry.stallTerminalSynthesised = true;
|
|
76790
77344
|
entry.state = "done";
|
|
76791
77345
|
const postStallSec = Math.floor((n - entry.stalledAt) / 1000);
|
|
76792
77346
|
const totalIdleSec = Math.floor((n - entry.lastActivityAt) / 1000);
|
|
76793
77347
|
log?.(`subagent-watcher: silent-stall terminal synthesis for ${entry.agentId} (stalled ${postStallSec}s post-notify, ${totalIdleSec}s total idle) \u2014 bg worker JSONL lacks turn_end; synthesising sub_agent_turn_end so deferred-completion gate releases`);
|
|
77348
|
+
recordFalseFinish(entry.agentId, entry.filePath, n);
|
|
76794
77349
|
if (db2 != null) {
|
|
76795
77350
|
try {
|
|
76796
77351
|
const rowRef = db2.prepare("SELECT id FROM subagents WHERE jsonl_agent_id = ?").get(entry.agentId);
|
|
@@ -76947,6 +77502,7 @@ function startSubagentWatcher(config) {
|
|
|
76947
77502
|
maybySendStateTransition(agentId);
|
|
76948
77503
|
}
|
|
76949
77504
|
checkStalls();
|
|
77505
|
+
checkResurrections();
|
|
76950
77506
|
}
|
|
76951
77507
|
rescanSubagentDirs();
|
|
76952
77508
|
bootScanInProgress = false;
|
|
@@ -76954,7 +77510,16 @@ function startSubagentWatcher(config) {
|
|
|
76954
77510
|
if (db2 == null)
|
|
76955
77511
|
return;
|
|
76956
77512
|
try {
|
|
76957
|
-
const result = reapStuckRunningRows(db2, {
|
|
77513
|
+
const result = reapStuckRunningRows(db2, {
|
|
77514
|
+
ttlMs: reaperTtlMs,
|
|
77515
|
+
now: nowFn(),
|
|
77516
|
+
isLive: (jsonlAgentId) => {
|
|
77517
|
+
if (jsonlAgentId == null)
|
|
77518
|
+
return false;
|
|
77519
|
+
const entry = registry.get(jsonlAgentId);
|
|
77520
|
+
return entry != null && entry.state === "running" && !entry.historical;
|
|
77521
|
+
}
|
|
77522
|
+
});
|
|
76958
77523
|
if (result.reaped > 0) {
|
|
76959
77524
|
log?.(`subagent-watcher: reaper transitioned ${result.reaped} stuck-running row(s) to stalled (ttl=${Math.round(reaperTtlMs / 60000)}min)`);
|
|
76960
77525
|
}
|
|
@@ -77125,7 +77690,7 @@ function determineRestartReason(opts) {
|
|
|
77125
77690
|
init_boot_card();
|
|
77126
77691
|
|
|
77127
77692
|
// gateway/update-announce.ts
|
|
77128
|
-
import { existsSync as existsSync41, mkdirSync as mkdirSync32, openSync as
|
|
77693
|
+
import { existsSync as existsSync41, mkdirSync as mkdirSync32, openSync as openSync8, closeSync as closeSync8, readFileSync as readFileSync43 } from "node:fs";
|
|
77129
77694
|
import { join as join44 } from "node:path";
|
|
77130
77695
|
import { homedir as homedir15 } from "node:os";
|
|
77131
77696
|
|
|
@@ -77326,8 +77891,8 @@ function claimUpdateAnnouncement(requestId, opts = {}) {
|
|
|
77326
77891
|
const safeId = requestId.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 200);
|
|
77327
77892
|
const path2 = join44(dir, safeId);
|
|
77328
77893
|
try {
|
|
77329
|
-
const fd =
|
|
77330
|
-
|
|
77894
|
+
const fd = openSync8(path2, "wx");
|
|
77895
|
+
closeSync8(fd);
|
|
77331
77896
|
return true;
|
|
77332
77897
|
} catch {
|
|
77333
77898
|
return false;
|
|
@@ -77550,10 +78115,10 @@ import { join as join46 } from "node:path";
|
|
|
77550
78115
|
|
|
77551
78116
|
// ../src/issues/store.ts
|
|
77552
78117
|
import {
|
|
77553
|
-
closeSync as
|
|
78118
|
+
closeSync as closeSync9,
|
|
77554
78119
|
existsSync as existsSync42,
|
|
77555
78120
|
mkdirSync as mkdirSync33,
|
|
77556
|
-
openSync as
|
|
78121
|
+
openSync as openSync9,
|
|
77557
78122
|
readdirSync as readdirSync9,
|
|
77558
78123
|
readFileSync as readFileSync45,
|
|
77559
78124
|
renameSync as renameSync15,
|
|
@@ -77675,7 +78240,7 @@ function withLock(stateDir, fn) {
|
|
|
77675
78240
|
let fd = null;
|
|
77676
78241
|
while (fd === null) {
|
|
77677
78242
|
try {
|
|
77678
|
-
fd =
|
|
78243
|
+
fd = openSync9(lockPath, "wx");
|
|
77679
78244
|
try {
|
|
77680
78245
|
writeSync5(fd, String(process.pid));
|
|
77681
78246
|
} catch {}
|
|
@@ -77695,7 +78260,7 @@ function withLock(stateDir, fn) {
|
|
|
77695
78260
|
return fn();
|
|
77696
78261
|
} finally {
|
|
77697
78262
|
try {
|
|
77698
|
-
|
|
78263
|
+
closeSync9(fd);
|
|
77699
78264
|
} catch {}
|
|
77700
78265
|
try {
|
|
77701
78266
|
unlinkSync17(lockPath);
|
|
@@ -79117,6 +79682,40 @@ function resolveQuotaWatchTuning(env) {
|
|
|
79117
79682
|
function buildQuotaClaimKey(accountLabel, transition2, chatId) {
|
|
79118
79683
|
return `quota-watch:${accountLabel}:${transition2}:${chatId}`;
|
|
79119
79684
|
}
|
|
79685
|
+
var FLEET_ROLL_ANNOUNCE_KEY = "__fleet_roll_announce__";
|
|
79686
|
+
var FLEET_ROLL_MAX_AGE_MS = QUOTA_WATCH_CLAIM_WINDOW_MS;
|
|
79687
|
+
var QUOTA_WATCH_ROLL_DEDUP_MS = QUOTA_WATCH_CLAIM_WINDOW_MS;
|
|
79688
|
+
function evaluateFleetRollAnnounce(args) {
|
|
79689
|
+
const { roll, prev, now } = args;
|
|
79690
|
+
const maxAgeMs = args.maxAgeMs ?? FLEET_ROLL_MAX_AGE_MS;
|
|
79691
|
+
if (!roll)
|
|
79692
|
+
return { kind: "skip", reason: "no-roll" };
|
|
79693
|
+
if (prev.lastNotifiedAt >= roll.at)
|
|
79694
|
+
return { kind: "skip", reason: "already-announced" };
|
|
79695
|
+
if (now - roll.at > maxAgeMs)
|
|
79696
|
+
return { kind: "skip", reason: "stale-roll" };
|
|
79697
|
+
return {
|
|
79698
|
+
kind: "notify",
|
|
79699
|
+
message: buildFleetRollMessage(roll, now),
|
|
79700
|
+
newState: { lastNotifiedHealth: "healthy", lastNotifiedAt: roll.at }
|
|
79701
|
+
};
|
|
79702
|
+
}
|
|
79703
|
+
function buildFleetRollMessage(roll, now) {
|
|
79704
|
+
const winLabel = roll.window === "5h" ? "5-hour window" : roll.window === "7d" ? "7-day window" : "quota window";
|
|
79705
|
+
const pctPart = typeof roll.pct === "number" ? ` at ${fmtPct(roll.pct)}` : "";
|
|
79706
|
+
const resetPart = typeof roll.exhausted_until === "number" && roll.exhausted_until > now ? ` (resets ${formatRelative(new Date(roll.exhausted_until), new Date(now))})` : "";
|
|
79707
|
+
const softAvoid = roll.reason === "soft-avoid";
|
|
79708
|
+
const causeLine = softAvoid ? `Proactive switch \u2014 \`${codeSpanSafe(roll.from)}\` is approaching its limits (${winLabel}${pctPart})${resetPart}, so the fleet moved early instead of hitting the wall.` : `${winLabel}${pctPart} on \`${codeSpanSafe(roll.from)}\`${resetPart}.`;
|
|
79709
|
+
return [
|
|
79710
|
+
`\uD83D\uDD01 **Switched fleet to \`${codeSpanSafe(roll.to)}\`**`,
|
|
79711
|
+
``,
|
|
79712
|
+
causeLine,
|
|
79713
|
+
`Work continues uninterrupted \u2014 agents and scheduled jobs now serve from \`${codeSpanSafe(roll.to)}\`.`,
|
|
79714
|
+
``,
|
|
79715
|
+
`_Automatic broker failover${softAvoid ? " (proactive, before exhaustion)" : ""}. Run /auth for fleet status; \`/auth use ${codeSpanSafe(roll.from)}\` to switch back once it ${softAvoid ? "has headroom again" : "refills"}._`
|
|
79716
|
+
].join(`
|
|
79717
|
+
`);
|
|
79718
|
+
}
|
|
79120
79719
|
function isLiveCorroboration(entry) {
|
|
79121
79720
|
return entry?.result.ok === true && entry.served === "live";
|
|
79122
79721
|
}
|
|
@@ -79142,6 +79741,16 @@ function evaluateQuotaWatchAccount(args) {
|
|
|
79142
79741
|
lastNotifiedHealth: "throttling",
|
|
79143
79742
|
lastNotifiedAt: now
|
|
79144
79743
|
};
|
|
79744
|
+
const lastRoll = args.lastRoll;
|
|
79745
|
+
if (lastRoll && lastRoll.from === label && now - lastRoll.at <= QUOTA_WATCH_ROLL_DEDUP_MS && prev.lastNotifiedAt < lastRoll.at) {
|
|
79746
|
+
return {
|
|
79747
|
+
kind: "reconcile",
|
|
79748
|
+
accountLabel: label,
|
|
79749
|
+
newAccountState: newState,
|
|
79750
|
+
transition: "entered-throttling",
|
|
79751
|
+
reason: "roll-announced"
|
|
79752
|
+
};
|
|
79753
|
+
}
|
|
79145
79754
|
return {
|
|
79146
79755
|
kind: "notify",
|
|
79147
79756
|
accountLabel: label,
|
|
@@ -79255,7 +79864,7 @@ function buildThrottlingMessage(agentName3, snap) {
|
|
|
79255
79864
|
const activeNote = snap.isActive ? "" : `
|
|
79256
79865
|
This is a non-active account. Consider \`/auth use ${codeSpanSafe(snap.label)}\` to switch, or keep it as a fallback reserve.`;
|
|
79257
79866
|
const altNote = snap.isActive ? `
|
|
79258
|
-
|
|
79867
|
+
No action needed: if usage keeps climbing, the fleet will prefer another account once this reaches the failover threshold (you'll get a switch announcement). Or switch early with \`/auth use <other-account>\`, or wait for the ${winLabel} window to refill${resetStr}.` : "";
|
|
79259
79868
|
return [
|
|
79260
79869
|
`\uD83D\uDFE1 **Quota approaching limit** \u2014 \`${codeSpanSafe(snap.label)}\``,
|
|
79261
79870
|
``,
|
|
@@ -79313,15 +79922,50 @@ function patchQuotaWatchState(current, accountLabel, accountState) {
|
|
|
79313
79922
|
return { ...current, [accountLabel]: accountState };
|
|
79314
79923
|
}
|
|
79315
79924
|
|
|
79925
|
+
// fallback-card-collapse.ts
|
|
79926
|
+
var CARD_COLLAPSE_MAX_AGE_MS = 5 * 60000;
|
|
79927
|
+
function createModelUnavailableCardRegistry(maxAgeMs = CARD_COLLAPSE_MAX_AGE_MS) {
|
|
79928
|
+
const cards = new Map;
|
|
79929
|
+
return {
|
|
79930
|
+
record(chatId, rec) {
|
|
79931
|
+
cards.set(chatId, rec);
|
|
79932
|
+
},
|
|
79933
|
+
take(chatId, now) {
|
|
79934
|
+
const rec = cards.get(chatId);
|
|
79935
|
+
if (!rec)
|
|
79936
|
+
return null;
|
|
79937
|
+
cards.delete(chatId);
|
|
79938
|
+
if (!rec.promisedFallback)
|
|
79939
|
+
return null;
|
|
79940
|
+
if (now - rec.atMs > maxAgeMs)
|
|
79941
|
+
return null;
|
|
79942
|
+
return rec;
|
|
79943
|
+
},
|
|
79944
|
+
size() {
|
|
79945
|
+
return cards.size;
|
|
79946
|
+
}
|
|
79947
|
+
};
|
|
79948
|
+
}
|
|
79949
|
+
function decideAnnouncementDelivery(outcomeKind, card) {
|
|
79950
|
+
if (outcomeKind !== "switched")
|
|
79951
|
+
return "send";
|
|
79952
|
+
return card !== null ? "edit" : "send";
|
|
79953
|
+
}
|
|
79954
|
+
function foldAnnouncementIntoCard(cardText, announcement) {
|
|
79955
|
+
return `${cardText}
|
|
79956
|
+
|
|
79957
|
+
${announcement}`;
|
|
79958
|
+
}
|
|
79959
|
+
|
|
79316
79960
|
// gateway/gateway.ts
|
|
79317
79961
|
init_auth_snapshot_format2();
|
|
79318
79962
|
|
|
79319
79963
|
// gateway/turn-active-marker.ts
|
|
79320
79964
|
import {
|
|
79321
|
-
closeSync as
|
|
79965
|
+
closeSync as closeSync10,
|
|
79322
79966
|
existsSync as existsSync46,
|
|
79323
79967
|
mkdirSync as mkdirSync36,
|
|
79324
|
-
openSync as
|
|
79968
|
+
openSync as openSync10,
|
|
79325
79969
|
readFileSync as readFileSync49,
|
|
79326
79970
|
statSync as statSync13,
|
|
79327
79971
|
unlinkSync as unlinkSync18,
|
|
@@ -79346,8 +79990,8 @@ function touchTurnActiveMarker2(stateDir) {
|
|
|
79346
79990
|
utimesSync2(path2, now, now);
|
|
79347
79991
|
} catch {
|
|
79348
79992
|
try {
|
|
79349
|
-
const fd =
|
|
79350
|
-
|
|
79993
|
+
const fd = openSync10(path2, "r+");
|
|
79994
|
+
closeSync10(fd);
|
|
79351
79995
|
} catch {}
|
|
79352
79996
|
}
|
|
79353
79997
|
}
|
|
@@ -79398,10 +80042,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
|
|
|
79398
80042
|
}
|
|
79399
80043
|
|
|
79400
80044
|
// ../src/build-info.ts
|
|
79401
|
-
var VERSION = "0.18.
|
|
79402
|
-
var COMMIT_SHA = "
|
|
79403
|
-
var COMMIT_DATE = "2026-07-
|
|
79404
|
-
var LATEST_PR =
|
|
80045
|
+
var VERSION = "0.18.11";
|
|
80046
|
+
var COMMIT_SHA = "8fbd9574";
|
|
80047
|
+
var COMMIT_DATE = "2026-07-11T05:08:14Z";
|
|
80048
|
+
var LATEST_PR = 3053;
|
|
79405
80049
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
79406
80050
|
|
|
79407
80051
|
// gateway/boot-version.ts
|
|
@@ -80616,6 +81260,13 @@ When the restart hit, ${count} sub-agent${count === 1 ? " was" : "s were"} still
|
|
|
80616
81260
|
` + lines.join(`
|
|
80617
81261
|
`) + overflow + closing;
|
|
80618
81262
|
}
|
|
81263
|
+
function renderRestartCauseBlock(cause) {
|
|
81264
|
+
if (!cause || cause.note.trim().length === 0)
|
|
81265
|
+
return "";
|
|
81266
|
+
return `
|
|
81267
|
+
|
|
81268
|
+
Why this restart happened: ${cause.note.trim()}`;
|
|
81269
|
+
}
|
|
80619
81270
|
var RESUME_SYNTHETIC_PROMPT_PREFIX = "You just restarted.";
|
|
80620
81271
|
function isResumeSyntheticTurn(turn) {
|
|
80621
81272
|
const p = turn.user_prompt_preview;
|
|
@@ -80648,6 +81299,8 @@ function buildResumeInterruptedInbound(ctx) {
|
|
|
80648
81299
|
};
|
|
80649
81300
|
if (ctx.turn.user_prompt_preview)
|
|
80650
81301
|
meta.original_prompt = ctx.turn.user_prompt_preview;
|
|
81302
|
+
if (ctx.restartCause)
|
|
81303
|
+
meta.restart_cause = ctx.restartCause.reason;
|
|
80651
81304
|
return {
|
|
80652
81305
|
type: "inbound",
|
|
80653
81306
|
chatId: ctx.turn.chat_id,
|
|
@@ -80656,7 +81309,7 @@ function buildResumeInterruptedInbound(ctx) {
|
|
|
80656
81309
|
user: "switchroom",
|
|
80657
81310
|
userId: 0,
|
|
80658
81311
|
ts,
|
|
80659
|
-
text: `You just restarted. Your previous turn was interrupted ${elapsed} ago, ` + `before it finished \u2014 it was cut off by a restart, not completed.` + promptClause(ctx.turn) + ` That quoted text is only the first ~200 characters of the original ` + `request, and you've lost your in-memory context across the restart \u2014 so ` + `BEFORE you continue, call get_recent_messages for this chat to read your ` + `full original message and the surrounding conversation, so you resume the ` + `COMPLETE task (including any instructions in the tail of a long request), ` + `not just the truncated preview. Then pick that work back up and carry it ` + `through to completion. In your first message, briefly let the user know ` + `you're resuming what was interrupted (mention roughly how long ago in ` + `plain language) so they're not left wondering \u2014 then carry on with the ` + `actual task. Do not ask whether to resume; just resume. If even after ` + `reading the recent messages you genuinely can't tell what the work was, ` + `say so and ask.` + renderInterruptedSubagentsBlock(ctx.subagents),
|
|
81312
|
+
text: `You just restarted. Your previous turn was interrupted ${elapsed} ago, ` + `before it finished \u2014 it was cut off by a restart, not completed.` + promptClause(ctx.turn) + ` That quoted text is only the first ~200 characters of the original ` + `request, and you've lost your in-memory context across the restart \u2014 so ` + `BEFORE you continue, call get_recent_messages for this chat to read your ` + `full original message and the surrounding conversation, so you resume the ` + `COMPLETE task (including any instructions in the tail of a long request), ` + `not just the truncated preview. Then pick that work back up and carry it ` + `through to completion. In your first message, briefly let the user know ` + `you're resuming what was interrupted (mention roughly how long ago in ` + `plain language) so they're not left wondering \u2014 then carry on with the ` + `actual task. Do not ask whether to resume; just resume. If even after ` + `reading the recent messages you genuinely can't tell what the work was, ` + `say so and ask.` + renderInterruptedSubagentsBlock(ctx.subagents) + renderRestartCauseBlock(ctx.restartCause),
|
|
80660
81313
|
meta
|
|
80661
81314
|
};
|
|
80662
81315
|
}
|
|
@@ -80679,6 +81332,8 @@ function buildResumeWatchdogReportInbound(ctx) {
|
|
|
80679
81332
|
meta.tool_call_count = String(ctx.turn.tool_call_count);
|
|
80680
81333
|
if (ctx.turn.user_prompt_preview)
|
|
80681
81334
|
meta.original_prompt = ctx.turn.user_prompt_preview;
|
|
81335
|
+
if (ctx.restartCause)
|
|
81336
|
+
meta.restart_cause = ctx.restartCause.reason;
|
|
80682
81337
|
return {
|
|
80683
81338
|
type: "inbound",
|
|
80684
81339
|
chatId: ctx.turn.chat_id,
|
|
@@ -80687,7 +81342,7 @@ function buildResumeWatchdogReportInbound(ctx) {
|
|
|
80687
81342
|
user: "switchroom",
|
|
80688
81343
|
userId: 0,
|
|
80689
81344
|
ts,
|
|
80690
|
-
text: `You just restarted. Your previous turn (started ${since} ago) was ` + `killed by the hang-watchdog: it made no observable progress for ${idle} ` + `and the watchdog restarts a turn that goes that long without activity.` + toolClause + promptClause(ctx.turn) + ` Do NOT silently resume it \u2014 it may hang again the same way. Instead, ` + `tell the user plainly what happened: that your last turn was killed ` + `after ${idle} of no progress, and roughly what it was doing. Then ask ` + `whether they want you to retry it or take a different angle. Report ` + `only the honest cause \u2014 no observable progress for that long \u2014 don't ` + `speculate about a deeper root cause you can't see.` + renderInterruptedSubagentsBlock(ctx.subagents, { assertive: false }),
|
|
81345
|
+
text: `You just restarted. Your previous turn (started ${since} ago) was ` + `killed by the hang-watchdog: it made no observable progress for ${idle} ` + `and the watchdog restarts a turn that goes that long without activity.` + toolClause + promptClause(ctx.turn) + ` Do NOT silently resume it \u2014 it may hang again the same way. Instead, ` + `tell the user plainly what happened: that your last turn was killed ` + `after ${idle} of no progress, and roughly what it was doing. Then ask ` + `whether they want you to retry it or take a different angle. Report ` + `only the honest cause \u2014 no observable progress for that long \u2014 don't ` + `speculate about a deeper root cause you can't see.` + renderInterruptedSubagentsBlock(ctx.subagents, { assertive: false }) + renderRestartCauseBlock(ctx.restartCause),
|
|
80691
81346
|
meta
|
|
80692
81347
|
};
|
|
80693
81348
|
}
|
|
@@ -80716,6 +81371,8 @@ function buildResumeDeferredReportInbound(ctx) {
|
|
|
80716
81371
|
};
|
|
80717
81372
|
if (ctx.turn.user_prompt_preview)
|
|
80718
81373
|
meta.original_prompt = ctx.turn.user_prompt_preview;
|
|
81374
|
+
if (ctx.restartCause)
|
|
81375
|
+
meta.restart_cause = ctx.restartCause.reason;
|
|
80719
81376
|
const cause = ctx.reason === "loop-guard" ? `Your previous turn was ALREADY a resume of earlier interrupted work, ` + `and it was interrupted again by another restart ${elapsed} ago. To ` + `avoid an endless restart\u2192resume loop (and re-running work that may ` + `have already partly executed), the framework has stopped ` + `auto-resuming this chain.` : `Your previous turn was interrupted ${elapsed} ago by a deliberate ` + `restart, before it finished. This agent is configured NOT to ` + `auto-resume work across deliberate restarts (boot_resume: never), so ` + `it was not replayed automatically.`;
|
|
80720
81377
|
return {
|
|
80721
81378
|
type: "inbound",
|
|
@@ -80725,7 +81382,7 @@ function buildResumeDeferredReportInbound(ctx) {
|
|
|
80725
81382
|
user: "switchroom",
|
|
80726
81383
|
userId: 0,
|
|
80727
81384
|
ts,
|
|
80728
|
-
text: `${RESUME_SYNTHETIC_PROMPT_PREFIX} ${cause}` + promptClause(ctx.turn) + ` Do NOT silently resume it. Instead, briefly tell the user what was in ` + `flight (call get_recent_messages for this chat if you need the full ` + `original request \u2014 the quoted preview is only the first ~200 ` + `characters), then ask whether they want you to pick it back up or drop ` + `it. If you genuinely can't tell what the work was, say so and ask.` + renderInterruptedSubagentsBlock(ctx.subagents, { assertive: false }),
|
|
81385
|
+
text: `${RESUME_SYNTHETIC_PROMPT_PREFIX} ${cause}` + promptClause(ctx.turn) + ` Do NOT silently resume it. Instead, briefly tell the user what was in ` + `flight (call get_recent_messages for this chat if you need the full ` + `original request \u2014 the quoted preview is only the first ~200 ` + `characters), then ask whether they want you to pick it back up or drop ` + `it. If you genuinely can't tell what the work was, say so and ask.` + renderInterruptedSubagentsBlock(ctx.subagents, { assertive: false }) + renderRestartCauseBlock(ctx.restartCause),
|
|
80729
81386
|
meta
|
|
80730
81387
|
};
|
|
80731
81388
|
}
|
|
@@ -80745,6 +81402,282 @@ function selectResumeBuilder(endedVia, opts) {
|
|
|
80745
81402
|
return kind;
|
|
80746
81403
|
}
|
|
80747
81404
|
|
|
81405
|
+
// gateway/bridge-dead-watchdog.ts
|
|
81406
|
+
import { readFileSync as readFileSync51, writeFileSync as writeFileSync40, renameSync as renameSync16, unlinkSync as unlinkSync19 } from "node:fs";
|
|
81407
|
+
|
|
81408
|
+
// gateway/cron-session.ts
|
|
81409
|
+
var CRON_IDENTITY_SUFFIX2 = "-cron";
|
|
81410
|
+
function isCronIdentity2(name) {
|
|
81411
|
+
return typeof name === "string" && name.endsWith(CRON_IDENTITY_SUFFIX2);
|
|
81412
|
+
}
|
|
81413
|
+
|
|
81414
|
+
// gateway/bridge-dead-watchdog.ts
|
|
81415
|
+
var BRIDGE_DEAD_RESTART_REASON = "bridge-dead-resume";
|
|
81416
|
+
var DEFAULT_BRIDGE_DEAD_GRACE_MS = 90000;
|
|
81417
|
+
var ESCALATION_MARKER_MAX_AGE_MS = 10 * 60000;
|
|
81418
|
+
var MAX_CONSECUTIVE_ESCALATIONS = 2;
|
|
81419
|
+
var CRASH_LOG_FRESH_WINDOW_MS = 15 * 60000;
|
|
81420
|
+
var CRASH_LOG_TAIL_LINES = 3;
|
|
81421
|
+
function decideBridgeDeadEscalation(s) {
|
|
81422
|
+
if (s.bridgeRegistered)
|
|
81423
|
+
return { action: "skip", why: "bridge-registered" };
|
|
81424
|
+
if (s.shuttingDown)
|
|
81425
|
+
return { action: "skip", why: "shutting-down" };
|
|
81426
|
+
if (s.alreadyEscalated)
|
|
81427
|
+
return { action: "skip", why: "already-escalated" };
|
|
81428
|
+
if (!s.bridgeEverRegistered && s.priorStreak >= s.maxConsecutive) {
|
|
81429
|
+
return { action: "skip", why: "streak-capped" };
|
|
81430
|
+
}
|
|
81431
|
+
if (!s.sessionAlive)
|
|
81432
|
+
return { action: "retry", why: "session-not-alive" };
|
|
81433
|
+
return { action: "escalate" };
|
|
81434
|
+
}
|
|
81435
|
+
function readFreshCrashLogTail(path2, opts = {}) {
|
|
81436
|
+
const nowMs2 = opts.nowMs ?? Date.now();
|
|
81437
|
+
const freshWindowMs = opts.freshWindowMs ?? CRASH_LOG_FRESH_WINDOW_MS;
|
|
81438
|
+
const maxLines = opts.maxLines ?? CRASH_LOG_TAIL_LINES;
|
|
81439
|
+
const readFile = opts.readFile ?? ((p) => readFileSync51(p, "utf8"));
|
|
81440
|
+
let raw;
|
|
81441
|
+
try {
|
|
81442
|
+
raw = readFile(path2);
|
|
81443
|
+
} catch {
|
|
81444
|
+
return null;
|
|
81445
|
+
}
|
|
81446
|
+
const fresh = raw.split(`
|
|
81447
|
+
`).filter((line) => {
|
|
81448
|
+
const m = line.match(/^(\d{4}-\d{2}-\d{2}T[0-9:.]+Z)\s/);
|
|
81449
|
+
if (!m)
|
|
81450
|
+
return false;
|
|
81451
|
+
const t = Date.parse(m[1]);
|
|
81452
|
+
return Number.isFinite(t) && nowMs2 - t >= 0 && nowMs2 - t <= freshWindowMs;
|
|
81453
|
+
}).slice(-maxLines);
|
|
81454
|
+
if (fresh.length === 0)
|
|
81455
|
+
return null;
|
|
81456
|
+
return fresh.map((l) => l.slice(0, 500)).join(" || ");
|
|
81457
|
+
}
|
|
81458
|
+
function writeBridgeDeadEscalationMarker(path2, marker) {
|
|
81459
|
+
const tmp = `${path2}.tmp-${process.pid}-${Date.now()}`;
|
|
81460
|
+
writeFileSync40(tmp, JSON.stringify(marker), "utf8");
|
|
81461
|
+
renameSync16(tmp, path2);
|
|
81462
|
+
}
|
|
81463
|
+
function consumeBridgeDeadEscalationMarker(path2, nowMs2 = Date.now(), maxAgeMs = ESCALATION_MARKER_MAX_AGE_MS) {
|
|
81464
|
+
let marker = null;
|
|
81465
|
+
try {
|
|
81466
|
+
const parsed = JSON.parse(readFileSync51(path2, "utf8"));
|
|
81467
|
+
if (typeof parsed.ts === "number" && Number.isFinite(parsed.ts) && typeof parsed.reason === "string") {
|
|
81468
|
+
const age = nowMs2 - parsed.ts;
|
|
81469
|
+
if (age >= 0 && age < maxAgeMs) {
|
|
81470
|
+
marker = { ts: parsed.ts, reason: parsed.reason };
|
|
81471
|
+
marker.count = typeof parsed.count === "number" && Number.isFinite(parsed.count) && parsed.count >= 1 ? Math.floor(parsed.count) : 1;
|
|
81472
|
+
if (typeof parsed.crashTail === "string")
|
|
81473
|
+
marker.crashTail = parsed.crashTail;
|
|
81474
|
+
}
|
|
81475
|
+
}
|
|
81476
|
+
} catch {}
|
|
81477
|
+
try {
|
|
81478
|
+
unlinkSync19(path2);
|
|
81479
|
+
} catch {}
|
|
81480
|
+
return marker;
|
|
81481
|
+
}
|
|
81482
|
+
function buildBridgeDeadIdleNoticeInbound(args) {
|
|
81483
|
+
const ts = args.nowMs ?? Date.now();
|
|
81484
|
+
return {
|
|
81485
|
+
type: "inbound",
|
|
81486
|
+
chatId: args.chatId,
|
|
81487
|
+
messageId: ts,
|
|
81488
|
+
user: "switchroom",
|
|
81489
|
+
userId: 0,
|
|
81490
|
+
ts,
|
|
81491
|
+
text: `You just restarted. The framework itself triggered this restart: your Telegram ` + `MCP bridge process had died (chat tools were unavailable \u2014 you could not send ` + `replies), so the container was bounced to restore the chat surface. No work was ` + `in flight when it happened. Briefly let the user know the messaging bridge died ` + `and the framework restarted you to fix it \u2014 one short message, no drama. This was ` + `NOT an operator-initiated restart and NOT a hang-watchdog kill; report only that ` + `honest cause.`,
|
|
81492
|
+
meta: {
|
|
81493
|
+
source: "bridge_dead_restart",
|
|
81494
|
+
chat_id: args.chatId,
|
|
81495
|
+
message_id: String(ts),
|
|
81496
|
+
restart_cause: args.marker.reason
|
|
81497
|
+
}
|
|
81498
|
+
};
|
|
81499
|
+
}
|
|
81500
|
+
function isRealBridgeIdentity(agentName3) {
|
|
81501
|
+
return agentName3 != null && agentName3.length > 0 && !isCronIdentity2(agentName3);
|
|
81502
|
+
}
|
|
81503
|
+
function createBridgeDeadWatchdog(opts) {
|
|
81504
|
+
const setTimer = opts.setTimer ?? ((fn, ms) => {
|
|
81505
|
+
const t = setTimeout(fn, ms);
|
|
81506
|
+
t.unref?.();
|
|
81507
|
+
return t;
|
|
81508
|
+
});
|
|
81509
|
+
const clearTimer = opts.clearTimer ?? ((h) => clearTimeout(h));
|
|
81510
|
+
const nowMs2 = opts.nowMs ?? (() => Date.now());
|
|
81511
|
+
const writeMarker = opts.writeMarker ?? writeBridgeDeadEscalationMarker;
|
|
81512
|
+
const readCrashTail = opts.readCrashTail ?? ((p, t) => readFreshCrashLogTail(p, { nowMs: t }));
|
|
81513
|
+
const priorStreak = opts.priorStreak ?? 0;
|
|
81514
|
+
const maxConsecutive = opts.maxConsecutive ?? MAX_CONSECUTIVE_ESCALATIONS;
|
|
81515
|
+
let timer3 = null;
|
|
81516
|
+
let bridgeRegistered = false;
|
|
81517
|
+
let bridgeEverRegistered = false;
|
|
81518
|
+
let escalated = false;
|
|
81519
|
+
const cancel = () => {
|
|
81520
|
+
if (timer3 != null) {
|
|
81521
|
+
clearTimer(timer3);
|
|
81522
|
+
timer3 = null;
|
|
81523
|
+
}
|
|
81524
|
+
};
|
|
81525
|
+
const effectiveGraceMs = () => !bridgeEverRegistered && priorStreak >= 1 ? opts.graceMs * 2 : opts.graceMs;
|
|
81526
|
+
const armInternal = () => {
|
|
81527
|
+
if (escalated)
|
|
81528
|
+
return;
|
|
81529
|
+
if (!bridgeEverRegistered && priorStreak >= maxConsecutive) {
|
|
81530
|
+
opts.log(`telegram gateway: [bridge-dead-watchdog] STANDING DOWN \u2014 ${priorStreak} consecutive ` + `bridge-dead escalations already restarted this container without the bridge coming ` + `back (cap=${maxConsecutive}). The bridge is failing deterministically; a further ` + `restart will not fix it. Investigate the bridge (plugin bundle, .mcp.json, ` + `STATE_DIR/bridge-crash.log). Manual recovery: fix the cause, then restart the ` + `container. SWITCHROOM_BRIDGE_DEAD_ESCALATION=0 disables this watchdog entirely.`);
|
|
81531
|
+
return;
|
|
81532
|
+
}
|
|
81533
|
+
cancel();
|
|
81534
|
+
timer3 = setTimer(() => {
|
|
81535
|
+
timer3 = null;
|
|
81536
|
+
check();
|
|
81537
|
+
}, effectiveGraceMs());
|
|
81538
|
+
};
|
|
81539
|
+
const check = () => {
|
|
81540
|
+
const decision = decideBridgeDeadEscalation({
|
|
81541
|
+
bridgeRegistered,
|
|
81542
|
+
bridgeEverRegistered,
|
|
81543
|
+
sessionAlive: opts.isSessionAlive(),
|
|
81544
|
+
shuttingDown: opts.isShuttingDown(),
|
|
81545
|
+
alreadyEscalated: escalated,
|
|
81546
|
+
priorStreak,
|
|
81547
|
+
maxConsecutive
|
|
81548
|
+
});
|
|
81549
|
+
if (decision.action === "retry") {
|
|
81550
|
+
opts.log(`telegram gateway: [bridge-dead-watchdog] no bridge registered after ${effectiveGraceMs()}ms ` + `but claude session not found \u2014 re-arming (no escalation)`);
|
|
81551
|
+
armInternal();
|
|
81552
|
+
return decision;
|
|
81553
|
+
}
|
|
81554
|
+
if (decision.action === "skip") {
|
|
81555
|
+
if (decision.why !== "bridge-registered") {
|
|
81556
|
+
opts.log(`telegram gateway: [bridge-dead-watchdog] bridge missing but skipping escalation (${decision.why})`);
|
|
81557
|
+
}
|
|
81558
|
+
return decision;
|
|
81559
|
+
}
|
|
81560
|
+
escalated = true;
|
|
81561
|
+
const t = nowMs2();
|
|
81562
|
+
const crashTail = (() => {
|
|
81563
|
+
try {
|
|
81564
|
+
return readCrashTail(opts.crashLogPath, t);
|
|
81565
|
+
} catch {
|
|
81566
|
+
return null;
|
|
81567
|
+
}
|
|
81568
|
+
})();
|
|
81569
|
+
const streakCount = bridgeEverRegistered ? 1 : priorStreak + 1;
|
|
81570
|
+
try {
|
|
81571
|
+
const marker = {
|
|
81572
|
+
ts: t,
|
|
81573
|
+
reason: BRIDGE_DEAD_RESTART_REASON,
|
|
81574
|
+
count: streakCount
|
|
81575
|
+
};
|
|
81576
|
+
if (crashTail != null)
|
|
81577
|
+
marker.crashTail = crashTail;
|
|
81578
|
+
writeMarker(opts.markerPath, marker);
|
|
81579
|
+
} catch (err) {
|
|
81580
|
+
opts.log(`telegram gateway: [bridge-dead-watchdog] escalation marker write failed: ${err.message}`);
|
|
81581
|
+
}
|
|
81582
|
+
opts.log(`telegram gateway: [bridge-dead-watchdog] ESCALATING reason=${BRIDGE_DEAD_RESTART_REASON} ` + `consecutive=${streakCount}/${maxConsecutive} \u2014 ` + `no MCP bridge registered within ${effectiveGraceMs()}ms grace window while the claude session is alive ` + `(Claude Code never respawns a dead MCP server; bouncing the container to restore the chat surface). ` + (crashTail != null ? `bridge-crash.log tail: ${crashTail}` : `no fresh bridge-crash.log entries`));
|
|
81583
|
+
const fired = opts.escalate(BRIDGE_DEAD_RESTART_REASON);
|
|
81584
|
+
if (!fired) {
|
|
81585
|
+
opts.log(`telegram gateway: [bridge-dead-watchdog] escalate() reported failure \u2014 ` + `restart not dispatched (fuse stays blown; no retry this boot)`);
|
|
81586
|
+
}
|
|
81587
|
+
return decision;
|
|
81588
|
+
};
|
|
81589
|
+
return {
|
|
81590
|
+
arm: armInternal,
|
|
81591
|
+
noteBridgeRegistered: (agentName3) => {
|
|
81592
|
+
if (!isRealBridgeIdentity(agentName3))
|
|
81593
|
+
return;
|
|
81594
|
+
bridgeRegistered = true;
|
|
81595
|
+
bridgeEverRegistered = true;
|
|
81596
|
+
cancel();
|
|
81597
|
+
},
|
|
81598
|
+
noteBridgeDisconnected: (agentName3) => {
|
|
81599
|
+
if (!isRealBridgeIdentity(agentName3))
|
|
81600
|
+
return;
|
|
81601
|
+
bridgeRegistered = false;
|
|
81602
|
+
armInternal();
|
|
81603
|
+
},
|
|
81604
|
+
check,
|
|
81605
|
+
stop: cancel,
|
|
81606
|
+
hasEscalated: () => escalated
|
|
81607
|
+
};
|
|
81608
|
+
}
|
|
81609
|
+
|
|
81610
|
+
// gateway/boot-probes.ts
|
|
81611
|
+
import { readFileSync as readFileSync52, readdirSync as readdirSync10, existsSync as existsSync48 } from "fs";
|
|
81612
|
+
init_quota_cache();
|
|
81613
|
+
init_quota_check();
|
|
81614
|
+
import { execFile as execFileCb2 } from "child_process";
|
|
81615
|
+
import { promisify as promisify2 } from "util";
|
|
81616
|
+
var execFile2 = promisify2(execFileCb2);
|
|
81617
|
+
var realProcFs2 = {
|
|
81618
|
+
readdir: (p) => readdirSync10(p),
|
|
81619
|
+
readFile: (p) => readFileSync52(p, "utf-8")
|
|
81620
|
+
};
|
|
81621
|
+
function findAgentProcessInContainer2(fs3 = realProcFs2) {
|
|
81622
|
+
let entries;
|
|
81623
|
+
try {
|
|
81624
|
+
entries = fs3.readdir("/proc");
|
|
81625
|
+
} catch {
|
|
81626
|
+
return null;
|
|
81627
|
+
}
|
|
81628
|
+
const candidates = [];
|
|
81629
|
+
for (const entry of entries) {
|
|
81630
|
+
if (!/^\d+$/.test(entry))
|
|
81631
|
+
continue;
|
|
81632
|
+
const pid = Number(entry);
|
|
81633
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
81634
|
+
continue;
|
|
81635
|
+
if (pid === process.pid)
|
|
81636
|
+
continue;
|
|
81637
|
+
let comm = "";
|
|
81638
|
+
try {
|
|
81639
|
+
comm = fs3.readFile(`/proc/${pid}/comm`).trim();
|
|
81640
|
+
} catch {
|
|
81641
|
+
continue;
|
|
81642
|
+
}
|
|
81643
|
+
let rssKb = 0;
|
|
81644
|
+
try {
|
|
81645
|
+
const status = fs3.readFile(`/proc/${pid}/status`);
|
|
81646
|
+
const m = status.match(/^VmRSS:\s+(\d+)/m);
|
|
81647
|
+
if (m)
|
|
81648
|
+
rssKb = parseInt(m[1], 10) || 0;
|
|
81649
|
+
} catch {
|
|
81650
|
+
continue;
|
|
81651
|
+
}
|
|
81652
|
+
let starttime = 0;
|
|
81653
|
+
try {
|
|
81654
|
+
const stat = fs3.readFile(`/proc/${pid}/stat`);
|
|
81655
|
+
const close = stat.lastIndexOf(")");
|
|
81656
|
+
const tail = close >= 0 ? stat.slice(close + 2) : stat;
|
|
81657
|
+
const fields = tail.trim().split(/\s+/);
|
|
81658
|
+
const st = Number(fields[19]);
|
|
81659
|
+
if (Number.isFinite(st) && st > 0)
|
|
81660
|
+
starttime = st;
|
|
81661
|
+
} catch {
|
|
81662
|
+
continue;
|
|
81663
|
+
}
|
|
81664
|
+
candidates.push({ pid, rssKb, comm, starttime });
|
|
81665
|
+
}
|
|
81666
|
+
if (candidates.length === 0)
|
|
81667
|
+
return null;
|
|
81668
|
+
const isAgent = (c) => c.comm === "claude";
|
|
81669
|
+
const isWrapper = (c) => c.comm === "tmux" || c.comm.startsWith("tmux:") || c.comm === "expect" || c.comm === "script" || c.comm === "bash" || c.comm === "sh" || c.comm === "tini" || c.comm === "sleep";
|
|
81670
|
+
const claudeMatches = candidates.filter(isAgent);
|
|
81671
|
+
if (claudeMatches.length > 0) {
|
|
81672
|
+
claudeMatches.sort((a, b) => b.rssKb - a.rssKb);
|
|
81673
|
+
return claudeMatches[0];
|
|
81674
|
+
}
|
|
81675
|
+
const nodeMatches = candidates.filter((c) => c.comm === "node" && !isWrapper(c)).sort((a, b) => b.rssKb - a.rssKb);
|
|
81676
|
+
if (nodeMatches.length > 0)
|
|
81677
|
+
return nodeMatches[0];
|
|
81678
|
+
return null;
|
|
81679
|
+
}
|
|
81680
|
+
|
|
80748
81681
|
// registry/subagents-schema.ts
|
|
80749
81682
|
var SUBAGENTS_SCHEMA_SQL = `
|
|
80750
81683
|
CREATE TABLE IF NOT EXISTS subagents (
|
|
@@ -80900,7 +81833,7 @@ function alwaysAllowDrainDeps() {
|
|
|
80900
81833
|
return {
|
|
80901
81834
|
readConfigText: () => {
|
|
80902
81835
|
const cfgPath = process.env.SWITCHROOM_CONFIG ?? SWITCHROOM_CONFIG ?? findConfigFile2();
|
|
80903
|
-
return
|
|
81836
|
+
return readFileSync53(cfgPath, "utf8");
|
|
80904
81837
|
},
|
|
80905
81838
|
resolveAllowList: (_configText, agentName3) => {
|
|
80906
81839
|
const cfg = loadConfig2();
|
|
@@ -81011,6 +81944,13 @@ function triggerSelfRestart(targetAgent, reason, delayMs = 300) {
|
|
|
81011
81944
|
return false;
|
|
81012
81945
|
}
|
|
81013
81946
|
}
|
|
81947
|
+
{
|
|
81948
|
+
const bootSmDir = resolveAgentDirFromEnv();
|
|
81949
|
+
if (bootSmDir != null && clearStaleGatewayShutdownIntent(bootSmDir)) {
|
|
81950
|
+
process.stderr.write(`telegram gateway: cleared stale gateway-shutdown relaunch-model intent (previous bounce was gateway-only \u2014 container never restarted)
|
|
81951
|
+
`);
|
|
81952
|
+
}
|
|
81953
|
+
}
|
|
81014
81954
|
var cachedClaudeCliVersion = undefined;
|
|
81015
81955
|
function getClaudeCliVersion() {
|
|
81016
81956
|
if (cachedClaudeCliVersion !== undefined)
|
|
@@ -81040,7 +81980,7 @@ function formatBootVersion() {
|
|
|
81040
81980
|
}
|
|
81041
81981
|
try {
|
|
81042
81982
|
chmodSync8(ENV_FILE, 384);
|
|
81043
|
-
for (const line of
|
|
81983
|
+
for (const line of readFileSync53(ENV_FILE, "utf8").split(`
|
|
81044
81984
|
`)) {
|
|
81045
81985
|
const m = line.match(/^(\w+)=(.*)$/);
|
|
81046
81986
|
if (m && process.env[m[1]] === undefined)
|
|
@@ -81105,7 +82045,7 @@ bot.api.config.use(async (prev, method, payload, signal) => {
|
|
|
81105
82045
|
});
|
|
81106
82046
|
var GRAMMY_VERSION = (() => {
|
|
81107
82047
|
try {
|
|
81108
|
-
const raw =
|
|
82048
|
+
const raw = readFileSync53(new URL("../../node_modules/grammy/package.json", import.meta.url), "utf8");
|
|
81109
82049
|
return JSON.parse(raw).version ?? "unknown";
|
|
81110
82050
|
} catch {
|
|
81111
82051
|
return "unknown";
|
|
@@ -81191,7 +82131,7 @@ function assertSendable(f) {
|
|
|
81191
82131
|
}
|
|
81192
82132
|
function readAccessFile() {
|
|
81193
82133
|
try {
|
|
81194
|
-
const raw =
|
|
82134
|
+
const raw = readFileSync53(ACCESS_FILE, "utf8");
|
|
81195
82135
|
const parsed = JSON.parse(raw);
|
|
81196
82136
|
const allowFrom = validateStringArray("allowFrom", parsed.allowFrom ?? []);
|
|
81197
82137
|
const groups = {};
|
|
@@ -81229,7 +82169,7 @@ function readAccessFile() {
|
|
|
81229
82169
|
if (err.code === "ENOENT")
|
|
81230
82170
|
return defaultAccess();
|
|
81231
82171
|
try {
|
|
81232
|
-
|
|
82172
|
+
renameSync17(ACCESS_FILE, `${ACCESS_FILE}.corrupt-${Date.now()}`);
|
|
81233
82173
|
} catch {}
|
|
81234
82174
|
process.stderr.write(`telegram gateway: access.json is corrupt, moved aside. Starting fresh.
|
|
81235
82175
|
`);
|
|
@@ -81251,7 +82191,7 @@ function loadAccess() {
|
|
|
81251
82191
|
}
|
|
81252
82192
|
function readPeopleFile() {
|
|
81253
82193
|
try {
|
|
81254
|
-
const raw =
|
|
82194
|
+
const raw = readFileSync53(PEOPLE_FILE, "utf8");
|
|
81255
82195
|
const parsed = JSON.parse(raw);
|
|
81256
82196
|
if (!Array.isArray(parsed.entries))
|
|
81257
82197
|
return [];
|
|
@@ -81275,9 +82215,9 @@ function saveAccess(a) {
|
|
|
81275
82215
|
return;
|
|
81276
82216
|
mkdirSync39(STATE_DIR, { recursive: true, mode: 448 });
|
|
81277
82217
|
const tmp = ACCESS_FILE + ".tmp";
|
|
81278
|
-
|
|
82218
|
+
writeFileSync42(tmp, JSON.stringify(a, null, 2) + `
|
|
81279
82219
|
`, { mode: 384 });
|
|
81280
|
-
|
|
82220
|
+
renameSync17(tmp, ACCESS_FILE);
|
|
81281
82221
|
}
|
|
81282
82222
|
function pruneExpired(a) {
|
|
81283
82223
|
const now = Date.now();
|
|
@@ -81304,6 +82244,7 @@ if (HISTORY_ENABLED) {
|
|
|
81304
82244
|
}
|
|
81305
82245
|
var turnsDb = null;
|
|
81306
82246
|
var bootResumeInbound = null;
|
|
82247
|
+
var bridgeDeadPriorStreak = 0;
|
|
81307
82248
|
try {
|
|
81308
82249
|
const agentDir = STATE_DIR.endsWith("/telegram") ? STATE_DIR.slice(0, -"/telegram".length) : STATE_DIR;
|
|
81309
82250
|
turnsDb = openTurnsDb(agentDir);
|
|
@@ -81312,11 +82253,11 @@ try {
|
|
|
81312
82253
|
let markerAgeMs = null;
|
|
81313
82254
|
try {
|
|
81314
82255
|
const markerPath = join53(STATE_DIR, TURN_ACTIVE_MARKER_FILE2);
|
|
81315
|
-
if (
|
|
82256
|
+
if (existsSync50(markerPath)) {
|
|
81316
82257
|
const st = statSync16(markerPath);
|
|
81317
82258
|
markerAgeMs = Date.now() - st.mtimeMs;
|
|
81318
82259
|
try {
|
|
81319
|
-
const payload = JSON.parse(
|
|
82260
|
+
const payload = JSON.parse(readFileSync53(markerPath, "utf8"));
|
|
81320
82261
|
if (typeof payload.turnKey === "string" && payload.turnKey.length > 0) {
|
|
81321
82262
|
markerTurnKey = payload.turnKey;
|
|
81322
82263
|
}
|
|
@@ -81339,6 +82280,16 @@ try {
|
|
|
81339
82280
|
process.stderr.write(`telegram gateway: turn-registry initialized at ${join53(agentDir, "telegram", "registry.db")}
|
|
81340
82281
|
`);
|
|
81341
82282
|
}
|
|
82283
|
+
const bridgeDeadMarker = consumeBridgeDeadEscalationMarker(join53(STATE_DIR, "bridge-dead-escalation.json"));
|
|
82284
|
+
if (bridgeDeadMarker != null) {
|
|
82285
|
+
bridgeDeadPriorStreak = bridgeDeadMarker.count ?? 1;
|
|
82286
|
+
process.stderr.write(`telegram gateway: boot: prior restart was a bridge-dead escalation (reason=${bridgeDeadMarker.reason}, consecutive=${bridgeDeadPriorStreak}${bridgeDeadMarker.crashTail ? `, crashTail=${bridgeDeadMarker.crashTail}` : ""})
|
|
82287
|
+
`);
|
|
82288
|
+
}
|
|
82289
|
+
const bridgeDeadRestartCause = bridgeDeadMarker != null ? {
|
|
82290
|
+
reason: bridgeDeadMarker.reason,
|
|
82291
|
+
note: "The framework itself triggered this restart: your Telegram MCP bridge process had died " + "(chat tools were unavailable \u2014 you could not send replies), so the container was bounced " + "to restore the chat surface. This was NOT an operator-initiated restart and NOT a hang-watchdog kill."
|
|
82292
|
+
} : undefined;
|
|
81342
82293
|
const pending2 = findLatestTurnIfInterrupted(turnsDb);
|
|
81343
82294
|
const selfAgent = process.env.SWITCHROOM_AGENT_NAME ?? "";
|
|
81344
82295
|
if (pending2 != null && selfAgent) {
|
|
@@ -81370,7 +82321,11 @@ try {
|
|
|
81370
82321
|
if (bootResumeKind === "resume") {
|
|
81371
82322
|
bootResumeInbound = {
|
|
81372
82323
|
agent: selfAgent,
|
|
81373
|
-
msg: buildResumeInterruptedInbound({
|
|
82324
|
+
msg: buildResumeInterruptedInbound({
|
|
82325
|
+
turn: pending2,
|
|
82326
|
+
subagents: interruptedSubagents,
|
|
82327
|
+
restartCause: bridgeDeadRestartCause
|
|
82328
|
+
})
|
|
81374
82329
|
};
|
|
81375
82330
|
} else if (bootResumeKind === "report") {
|
|
81376
82331
|
let idleMs = pending2.turn_key === timeoutTurnKey && markerAgeMs != null ? markerAgeMs : null;
|
|
@@ -81385,7 +82340,12 @@ try {
|
|
|
81385
82340
|
idleMs = Math.max(0, Date.now() - pending2.started_at);
|
|
81386
82341
|
bootResumeInbound = {
|
|
81387
82342
|
agent: selfAgent,
|
|
81388
|
-
msg: buildResumeWatchdogReportInbound({
|
|
82343
|
+
msg: buildResumeWatchdogReportInbound({
|
|
82344
|
+
turn: pending2,
|
|
82345
|
+
idleMs,
|
|
82346
|
+
subagents: interruptedSubagents,
|
|
82347
|
+
restartCause: bridgeDeadRestartCause
|
|
82348
|
+
})
|
|
81389
82349
|
};
|
|
81390
82350
|
} else if (bootResumeKind === "defer-loop" || bootResumeKind === "defer-suppressed") {
|
|
81391
82351
|
bootResumeInbound = {
|
|
@@ -81393,7 +82353,8 @@ try {
|
|
|
81393
82353
|
msg: buildResumeDeferredReportInbound({
|
|
81394
82354
|
turn: pending2,
|
|
81395
82355
|
reason: bootResumeKind === "defer-loop" ? "loop-guard" : "clean-restart-suppressed",
|
|
81396
|
-
subagents: interruptedSubagents
|
|
82356
|
+
subagents: interruptedSubagents,
|
|
82357
|
+
restartCause: bridgeDeadRestartCause
|
|
81397
82358
|
})
|
|
81398
82359
|
};
|
|
81399
82360
|
}
|
|
@@ -81406,6 +82367,29 @@ try {
|
|
|
81406
82367
|
}
|
|
81407
82368
|
if (bootResumeInbound != null) {
|
|
81408
82369
|
process.stderr.write(`telegram gateway: boot-resume queued kind=${bootResumeKind} mode=${bootResumeMode} turnKey=${pending2.turn_key} endedVia=${pending2.ended_via ?? "open"} chat=${pending2.chat_id}
|
|
82370
|
+
`);
|
|
82371
|
+
}
|
|
82372
|
+
}
|
|
82373
|
+
if (bridgeDeadMarker != null && bootResumeInbound == null && selfAgent) {
|
|
82374
|
+
const idleNoticeChat = (() => {
|
|
82375
|
+
try {
|
|
82376
|
+
return loadAccess().allowFrom[0] ?? null;
|
|
82377
|
+
} catch {
|
|
82378
|
+
return null;
|
|
82379
|
+
}
|
|
82380
|
+
})();
|
|
82381
|
+
if (idleNoticeChat != null) {
|
|
82382
|
+
bootResumeInbound = {
|
|
82383
|
+
agent: selfAgent,
|
|
82384
|
+
msg: buildBridgeDeadIdleNoticeInbound({
|
|
82385
|
+
chatId: String(idleNoticeChat),
|
|
82386
|
+
marker: bridgeDeadMarker
|
|
82387
|
+
})
|
|
82388
|
+
};
|
|
82389
|
+
process.stderr.write(`telegram gateway: boot: bridge-dead idle notice queued chat=${idleNoticeChat} (no turn was in flight)
|
|
82390
|
+
`);
|
|
82391
|
+
} else {
|
|
82392
|
+
process.stderr.write(`telegram gateway: boot: bridge-dead idle notice skipped \u2014 no allowFrom chat to surface it in
|
|
81409
82393
|
`);
|
|
81410
82394
|
}
|
|
81411
82395
|
}
|
|
@@ -81423,13 +82407,13 @@ try {
|
|
|
81423
82407
|
pending2.interrupt_reason != null ? `SWITCHROOM_PENDING_INTERRUPT_REASON=${pending2.interrupt_reason}` : `SWITCHROOM_PENDING_INTERRUPT_REASON=`
|
|
81424
82408
|
];
|
|
81425
82409
|
const pendingEnvTmp = `${pendingEnvPath}.tmp-${process.pid}`;
|
|
81426
|
-
|
|
82410
|
+
writeFileSync42(pendingEnvTmp, lines.join(`
|
|
81427
82411
|
`) + `
|
|
81428
82412
|
`, { mode: 384 });
|
|
81429
|
-
|
|
82413
|
+
renameSync17(pendingEnvTmp, pendingEnvPath);
|
|
81430
82414
|
process.stderr.write(`telegram gateway: pending-turn env written to ${pendingEnvPath} turnKey=${pending2.turn_key} endedVia=${pending2.ended_via ?? "open"}
|
|
81431
82415
|
`);
|
|
81432
|
-
} else if (
|
|
82416
|
+
} else if (existsSync50(pendingEnvPath)) {
|
|
81433
82417
|
rmSync5(pendingEnvPath, { force: true });
|
|
81434
82418
|
process.stderr.write(`telegram gateway: pending-turn env cleared (clean previous shutdown)
|
|
81435
82419
|
`);
|
|
@@ -81520,7 +82504,7 @@ if (!STATIC) {
|
|
|
81520
82504
|
function checkApprovals() {
|
|
81521
82505
|
let files;
|
|
81522
82506
|
try {
|
|
81523
|
-
files =
|
|
82507
|
+
files = readdirSync12(APPROVED_DIR);
|
|
81524
82508
|
} catch {
|
|
81525
82509
|
return;
|
|
81526
82510
|
}
|
|
@@ -81631,10 +82615,10 @@ function noteAgentOutputAt(key, ts) {
|
|
|
81631
82615
|
}
|
|
81632
82616
|
var OBLIGATION_STORE_PATH = join53(STATE_DIR, "obligations.json");
|
|
81633
82617
|
var obligationStoreFs = {
|
|
81634
|
-
readFileSync: (p) =>
|
|
81635
|
-
writeFileSync: (p, d) =>
|
|
81636
|
-
renameSync: (a, b) =>
|
|
81637
|
-
existsSync: (p) =>
|
|
82618
|
+
readFileSync: (p) => readFileSync53(p, "utf8"),
|
|
82619
|
+
writeFileSync: (p, d) => writeFileSync42(p, d),
|
|
82620
|
+
renameSync: (a, b) => renameSync17(a, b),
|
|
82621
|
+
existsSync: (p) => existsSync50(p)
|
|
81638
82622
|
};
|
|
81639
82623
|
var obligationLedger = new ObligationLedger(OBLIGATION_REPRESENT_MAX, {
|
|
81640
82624
|
onChange: STATIC || !OBLIGATION_LEDGER_ENABLED ? undefined : (snapshot) => persistObligations(OBLIGATION_STORE_PATH, obligationStoreFs, snapshot)
|
|
@@ -81741,6 +82725,8 @@ async function deliverButtonTapInbound(agent, inbound) {
|
|
|
81741
82725
|
return "buffered-bridge-offline";
|
|
81742
82726
|
}
|
|
81743
82727
|
var pendingRestarts = new Map;
|
|
82728
|
+
var pendingSessionCommand = createPendingSessionCommandSlots();
|
|
82729
|
+
var PENDING_CMD_DRAIN_CAP_MS = 60000;
|
|
81744
82730
|
var lastSessionActiveFile = null;
|
|
81745
82731
|
var compactState = initialCompactState();
|
|
81746
82732
|
var compactDispatching = false;
|
|
@@ -82072,6 +83058,119 @@ async function fireDeferredInterrupt(reason) {
|
|
|
82072
83058
|
`);
|
|
82073
83059
|
}
|
|
82074
83060
|
}
|
|
83061
|
+
var haltBoundaryWaiters = new Set;
|
|
83062
|
+
function notifyHaltBoundaryWaiters() {
|
|
83063
|
+
if (haltBoundaryWaiters.size === 0)
|
|
83064
|
+
return;
|
|
83065
|
+
for (const check of [...haltBoundaryWaiters])
|
|
83066
|
+
check();
|
|
83067
|
+
}
|
|
83068
|
+
function waitForSafeBoundary(maxWaitMs) {
|
|
83069
|
+
if (!toolFlightTracker.isMidToolCall())
|
|
83070
|
+
return Promise.resolve("boundary");
|
|
83071
|
+
return new Promise((resolve11) => {
|
|
83072
|
+
let done = false;
|
|
83073
|
+
const finish = (reason) => {
|
|
83074
|
+
if (done)
|
|
83075
|
+
return;
|
|
83076
|
+
done = true;
|
|
83077
|
+
clearTimeout(timer3);
|
|
83078
|
+
haltBoundaryWaiters.delete(check);
|
|
83079
|
+
resolve11(reason);
|
|
83080
|
+
};
|
|
83081
|
+
const timer3 = setTimeout(() => finish("timeout"), maxWaitMs);
|
|
83082
|
+
const check = () => {
|
|
83083
|
+
if (!toolFlightTracker.isMidToolCall())
|
|
83084
|
+
finish("boundary");
|
|
83085
|
+
};
|
|
83086
|
+
haltBoundaryWaiters.add(check);
|
|
83087
|
+
});
|
|
83088
|
+
}
|
|
83089
|
+
function cancelPendingPermissionsForHalt(origin) {
|
|
83090
|
+
if (pendingPermissions.size === 0)
|
|
83091
|
+
return;
|
|
83092
|
+
for (const [requestId, details] of pendingPermissions) {
|
|
83093
|
+
dispatchPermissionVerdict({
|
|
83094
|
+
type: "permission",
|
|
83095
|
+
requestId,
|
|
83096
|
+
behavior: "deny",
|
|
83097
|
+
message: "Cancelled \u2014 the operator stopped this turn. Do not retry."
|
|
83098
|
+
});
|
|
83099
|
+
stripCancelledPermissionCards(details.card_text, details.cards);
|
|
83100
|
+
pendingPermissions.delete(requestId);
|
|
83101
|
+
permCardStore.remove(requestId);
|
|
83102
|
+
process.stderr.write(`telegram gateway: halt-now cancelled pending permission origin=${origin} request=${requestId} tool=${details.tool_name}
|
|
83103
|
+
`);
|
|
83104
|
+
}
|
|
83105
|
+
}
|
|
83106
|
+
async function stripCancelledPermissionCards(cardText, cards) {
|
|
83107
|
+
for (const edit of buildCancelledCardEdits(cardText, cards)) {
|
|
83108
|
+
await swallowingApiCall(() => bot.api.editMessageText(edit.chatId, edit.messageId, richMessage2(edit.text), {}), { chat_id: edit.chatId, verb: "permission_halt.strip" });
|
|
83109
|
+
}
|
|
83110
|
+
}
|
|
83111
|
+
async function executeHaltNow(origin) {
|
|
83112
|
+
const agentName3 = process.env.SWITCHROOM_AGENT_NAME;
|
|
83113
|
+
if (!agentName3)
|
|
83114
|
+
return;
|
|
83115
|
+
const haltTarget = currentTurn;
|
|
83116
|
+
const haltTargetKey = haltTarget?.registryKey ?? null;
|
|
83117
|
+
const access = loadAccess();
|
|
83118
|
+
const timing = decideInterruptTiming({
|
|
83119
|
+
safeBoundaryEnabled: resolveSafeBoundaryEnabled(access.interruptSafeBoundary),
|
|
83120
|
+
midToolCall: toolFlightTracker.isMidToolCall()
|
|
83121
|
+
});
|
|
83122
|
+
const startedAt = Date.now();
|
|
83123
|
+
if (timing === "defer") {
|
|
83124
|
+
const maxWaitMs = resolveInterruptMaxWaitMs(access.interruptMaxWaitMs);
|
|
83125
|
+
const reason = await waitForSafeBoundary(maxWaitMs);
|
|
83126
|
+
process.stderr.write(`telegram gateway: halt-now boundary-wait origin=${origin} reason=${reason} waited_ms=${Date.now() - startedAt} in_flight=${toolFlightTracker.inFlightCount()}
|
|
83127
|
+
`);
|
|
83128
|
+
}
|
|
83129
|
+
if (currentTurn !== haltTarget) {
|
|
83130
|
+
process.stderr.write(`telegram gateway: halt-now skipped \u2014 turn changed during boundary wait ` + `origin=${origin} target_key=${haltTargetKey ?? "none"} current_key=${currentTurn?.registryKey ?? "none"}
|
|
83131
|
+
`);
|
|
83132
|
+
return;
|
|
83133
|
+
}
|
|
83134
|
+
try {
|
|
83135
|
+
const { sendAgentInterrupt: sendAgentInterrupt2 } = await Promise.resolve().then(() => (init_tmux(), exports_tmux));
|
|
83136
|
+
const r = sendAgentInterrupt2({ agentName: agentName3 });
|
|
83137
|
+
if ("ok" in r) {
|
|
83138
|
+
process.stderr.write(`telegram gateway: halt-now SIGINT delivered via tmux send-keys origin=${origin} agent=${agentName3}
|
|
83139
|
+
`);
|
|
83140
|
+
} else {
|
|
83141
|
+
process.stderr.write(`telegram gateway: halt-now SIGINT via tmux failed origin=${origin} agent=${agentName3}: ${r.error}
|
|
83142
|
+
`);
|
|
83143
|
+
}
|
|
83144
|
+
} catch (err) {
|
|
83145
|
+
process.stderr.write(`telegram gateway: halt-now SIGINT failed origin=${origin}: ${err.message}
|
|
83146
|
+
`);
|
|
83147
|
+
}
|
|
83148
|
+
cancelInterruptedObligation();
|
|
83149
|
+
cancelPendingPermissionsForHalt(origin);
|
|
83150
|
+
const turn = currentTurn;
|
|
83151
|
+
if (turn != null) {
|
|
83152
|
+
if (turnsDb != null && turn.registryKey != null) {
|
|
83153
|
+
const _turnKey = turn.registryKey;
|
|
83154
|
+
try {
|
|
83155
|
+
recordTurnEnd(turnsDb, {
|
|
83156
|
+
turnKey: _turnKey,
|
|
83157
|
+
endedVia: "stop",
|
|
83158
|
+
lastAssistantMsgId: turn.lastAssistantMsgId,
|
|
83159
|
+
lastAssistantDone: turn.lastAssistantDone,
|
|
83160
|
+
toolCallCount: turn.toolCallCount
|
|
83161
|
+
});
|
|
83162
|
+
} catch (err) {
|
|
83163
|
+
process.stderr.write(`telegram gateway: recordTurnEnd(halt) failed turnKey=${_turnKey}: ${err.message}
|
|
83164
|
+
`);
|
|
83165
|
+
}
|
|
83166
|
+
}
|
|
83167
|
+
const key = statusKey(turn.sessionChatId, turn.sessionThreadId);
|
|
83168
|
+
endCurrentTurnAtomic(turn);
|
|
83169
|
+
releaseTurnBufferGate(key, turn);
|
|
83170
|
+
}
|
|
83171
|
+
if (!turnInFlightForGate())
|
|
83172
|
+
drainPendingSessionCommand();
|
|
83173
|
+
}
|
|
82075
83174
|
var preambleSuppressor = new PreambleSuppressor({
|
|
82076
83175
|
emitAnswer: (cumulative) => {
|
|
82077
83176
|
const stream = currentTurn?.answerStream ?? null;
|
|
@@ -82194,6 +83293,7 @@ function purgeReactionTracking(key, endingTurn) {
|
|
|
82194
83293
|
}
|
|
82195
83294
|
drainBufferedIfAllowed(endingTurn, "turn-complete");
|
|
82196
83295
|
if (!turnInFlightForGate()) {
|
|
83296
|
+
drainPendingSessionCommand();
|
|
82197
83297
|
if (pendingRestarts.size > 0) {
|
|
82198
83298
|
for (const [agentName3, _timestamp] of pendingRestarts.entries()) {
|
|
82199
83299
|
triggerSelfRestart(agentName3, "turn-complete-pending-restart");
|
|
@@ -82258,7 +83358,7 @@ function emitTurnRecord(turn, endedAt) {
|
|
|
82258
83358
|
return;
|
|
82259
83359
|
}
|
|
82260
83360
|
},
|
|
82261
|
-
rename: (from, to) =>
|
|
83361
|
+
rename: (from, to) => renameSync17(from, to)
|
|
82262
83362
|
});
|
|
82263
83363
|
appendFileSync6(turnsPath, rec);
|
|
82264
83364
|
} catch {}
|
|
@@ -83357,6 +84457,17 @@ var pendingStateReaper = setInterval(() => {
|
|
|
83357
84457
|
triggerSelfRestart(agentName3, "restart-drain-cap-forced", 100);
|
|
83358
84458
|
}
|
|
83359
84459
|
}
|
|
84460
|
+
const queuedCmds = pendingSessionCommand.list();
|
|
84461
|
+
const cmdDecision = drainCapDecision(queuedCmds, now, PENDING_CMD_DRAIN_CAP_MS, turnInFlightForGate());
|
|
84462
|
+
if (cmdDecision === "defer-turn-in-flight") {
|
|
84463
|
+
process.stderr.write(`telegram gateway: [pending-cmd-drain] deferred reason=turn-in-flight queued=${queuedCmds.map((c) => c.kind).join(",")}
|
|
84464
|
+
`);
|
|
84465
|
+
} else if (cmdDecision === "force") {
|
|
84466
|
+
const oldest = Math.min(...queuedCmds.map((c) => c.requestedAt));
|
|
84467
|
+
process.stderr.write(`telegram gateway: [pending-cmd-drain] forcing queued=${queuedCmds.map((c) => `${c.kind}:${c.targetLabel}`).join(",")} waited=${Math.round((now - oldest) / 1000)}s threshold=${Math.round(PENDING_CMD_DRAIN_CAP_MS / 1000)}s
|
|
84468
|
+
`);
|
|
84469
|
+
drainPendingSessionCommand();
|
|
84470
|
+
}
|
|
83360
84471
|
}, 60000);
|
|
83361
84472
|
pendingStateReaper.unref();
|
|
83362
84473
|
function looksLikeAuthCode(text5) {
|
|
@@ -83410,6 +84521,7 @@ function emitGatewayOperatorEvent(event) {
|
|
|
83410
84521
|
const modelUnavailable = resolveModelUnavailableFromOperatorEvent(event);
|
|
83411
84522
|
let renderedText;
|
|
83412
84523
|
let renderedKeyboard;
|
|
84524
|
+
let cardPromisedFallback = false;
|
|
83413
84525
|
if (modelUnavailable) {
|
|
83414
84526
|
const isAutoKind = modelUnavailable.kind === "quota_exhausted";
|
|
83415
84527
|
const willActuallyFire = isAutoKind && wouldFireFleetAutoFallback();
|
|
@@ -83419,6 +84531,7 @@ function emitGatewayOperatorEvent(event) {
|
|
|
83419
84531
|
autoFallbackInFlight: willActuallyFire
|
|
83420
84532
|
});
|
|
83421
84533
|
renderedKeyboard = undefined;
|
|
84534
|
+
cardPromisedFallback = willActuallyFire;
|
|
83422
84535
|
if (willActuallyFire) {
|
|
83423
84536
|
const untilMs = resolveExhaustUntil(modelUnavailable.resetAt?.getTime());
|
|
83424
84537
|
fireFleetAutoFallback(agent, untilMs);
|
|
@@ -83450,7 +84563,16 @@ function emitGatewayOperatorEvent(event) {
|
|
|
83450
84563
|
...renderedKeyboard ? { reply_markup: renderedKeyboard } : {},
|
|
83451
84564
|
...opEventThread != null ? { message_thread_id: opEventThread } : {}
|
|
83452
84565
|
};
|
|
83453
|
-
bot.api.sendRichMessage(chat_id, richMessage2(renderedText), opts).
|
|
84566
|
+
bot.api.sendRichMessage(chat_id, richMessage2(renderedText), opts).then((sent) => {
|
|
84567
|
+
if (cardPromisedFallback) {
|
|
84568
|
+
modelUnavailableCardRegistry.record(String(chat_id), {
|
|
84569
|
+
messageId: sent.message_id,
|
|
84570
|
+
text: renderedText,
|
|
84571
|
+
atMs: Date.now(),
|
|
84572
|
+
promisedFallback: true
|
|
84573
|
+
});
|
|
84574
|
+
}
|
|
84575
|
+
}).catch((e) => {
|
|
83454
84576
|
process.stderr.write(`telegram gateway: operator-event send to ${chat_id} failed agent=${agent} kind=${kind}: ${e}
|
|
83455
84577
|
`);
|
|
83456
84578
|
});
|
|
@@ -83512,18 +84634,18 @@ var statusPinChatIds = new Map;
|
|
|
83512
84634
|
var statusPinPinnedAt = new Map;
|
|
83513
84635
|
var STATUS_PIN_STORE_PATH = join53(STATE_DIR, "status-pins.json");
|
|
83514
84636
|
var statusPinStoreFs = {
|
|
83515
|
-
readFileSync: (p) =>
|
|
83516
|
-
writeFileSync: (p, d) =>
|
|
83517
|
-
renameSync: (a, b) =>
|
|
83518
|
-
existsSync: (p) =>
|
|
84637
|
+
readFileSync: (p) => readFileSync53(p, "utf8"),
|
|
84638
|
+
writeFileSync: (p, d) => writeFileSync42(p, d),
|
|
84639
|
+
renameSync: (a, b) => renameSync17(a, b),
|
|
84640
|
+
existsSync: (p) => existsSync50(p)
|
|
83519
84641
|
};
|
|
83520
84642
|
var statusPinPersistEnabled = !STATIC && PIN_STATUS_WHILE_WORKING;
|
|
83521
84643
|
var ACTIVITY_CARD_STORE_PATH = join53(STATE_DIR, "activity-cards-pending.json");
|
|
83522
84644
|
var activityCardStoreFs = {
|
|
83523
|
-
readFileSync: (p) =>
|
|
83524
|
-
writeFileSync: (p, d) =>
|
|
83525
|
-
renameSync: (a, b) =>
|
|
83526
|
-
existsSync: (p) =>
|
|
84645
|
+
readFileSync: (p) => readFileSync53(p, "utf8"),
|
|
84646
|
+
writeFileSync: (p, d) => writeFileSync42(p, d),
|
|
84647
|
+
renameSync: (a, b) => renameSync17(a, b),
|
|
84648
|
+
existsSync: (p) => existsSync50(p)
|
|
83527
84649
|
};
|
|
83528
84650
|
var activityCardPersistEnabled = !STATIC;
|
|
83529
84651
|
var bannerPinPersistEnabled = !STATIC;
|
|
@@ -84167,10 +85289,10 @@ var inboundSpool = STATIC ? undefined : createInboundSpool({
|
|
|
84167
85289
|
path: join53(STATE_DIR, "inbound-spool.jsonl"),
|
|
84168
85290
|
fs: {
|
|
84169
85291
|
appendFileSync: (p, d) => appendFileSync6(p, d),
|
|
84170
|
-
readFileSync: (p) =>
|
|
84171
|
-
writeFileSync: (p, d) =>
|
|
84172
|
-
renameSync: (a, b) =>
|
|
84173
|
-
existsSync: (p) =>
|
|
85292
|
+
readFileSync: (p) => readFileSync53(p, "utf8"),
|
|
85293
|
+
writeFileSync: (p, d) => writeFileSync42(p, d),
|
|
85294
|
+
renameSync: (a, b) => renameSync17(a, b),
|
|
85295
|
+
existsSync: (p) => existsSync50(p),
|
|
84174
85296
|
statSizeSync: (p) => statSync16(p).size
|
|
84175
85297
|
},
|
|
84176
85298
|
onDegraded: (info) => {
|
|
@@ -84351,6 +85473,27 @@ async function stripStalePermissionCard(card) {
|
|
|
84351
85473
|
`);
|
|
84352
85474
|
}
|
|
84353
85475
|
}
|
|
85476
|
+
var BRIDGE_DEAD_ESCALATION_ENABLED = process.env.SWITCHROOM_BRIDGE_DEAD_ESCALATION !== "0";
|
|
85477
|
+
var BRIDGE_DEAD_GRACE_MS = (() => {
|
|
85478
|
+
const v = Number(process.env.SWITCHROOM_BRIDGE_DEAD_GRACE_MS);
|
|
85479
|
+
return Number.isFinite(v) && v > 0 ? v : DEFAULT_BRIDGE_DEAD_GRACE_MS;
|
|
85480
|
+
})();
|
|
85481
|
+
var bridgeDeadWatchdog = createBridgeDeadWatchdog({
|
|
85482
|
+
graceMs: BRIDGE_DEAD_GRACE_MS,
|
|
85483
|
+
isSessionAlive: () => findAgentProcessInContainer2()?.comm === "claude",
|
|
85484
|
+
isShuttingDown: () => shuttingDown,
|
|
85485
|
+
escalate: (reason) => triggerSelfRestart(process.env.SWITCHROOM_AGENT_NAME ?? "", reason, 1500),
|
|
85486
|
+
crashLogPath: join53(STATE_DIR, "bridge-crash.log"),
|
|
85487
|
+
markerPath: join53(STATE_DIR, "bridge-dead-escalation.json"),
|
|
85488
|
+
log: (line) => process.stderr.write(`${line}
|
|
85489
|
+
`),
|
|
85490
|
+
priorStreak: bridgeDeadPriorStreak
|
|
85491
|
+
});
|
|
85492
|
+
if (BRIDGE_DEAD_ESCALATION_ENABLED) {
|
|
85493
|
+
bridgeDeadWatchdog.arm();
|
|
85494
|
+
process.stderr.write(`telegram gateway: [bridge-dead-watchdog] armed (grace=${BRIDGE_DEAD_GRACE_MS}ms)
|
|
85495
|
+
`);
|
|
85496
|
+
}
|
|
84354
85497
|
var ipcServer = createIpcServer({
|
|
84355
85498
|
socketPath: SOCKET_PATH,
|
|
84356
85499
|
onClientRegistered(client3) {
|
|
@@ -84367,6 +85510,7 @@ var ipcServer = createIpcServer({
|
|
|
84367
85510
|
return;
|
|
84368
85511
|
}
|
|
84369
85512
|
const bridgeUpEffects = client3.agentName != null ? shadowEmit({ kind: "bridgeUp", at: Date.now() }) : [];
|
|
85513
|
+
bridgeDeadWatchdog.noteBridgeRegistered(client3.agentName);
|
|
84370
85514
|
client3.send({ type: "status", status: "agent_connected" });
|
|
84371
85515
|
if (client3.agentName != null) {
|
|
84372
85516
|
if (isDispatchEnabled()) {
|
|
@@ -84498,6 +85642,8 @@ var ipcServer = createIpcServer({
|
|
|
84498
85642
|
process.stderr.write(`telegram gateway: bridge disconnected \u2014 agent=${client3.agentName}
|
|
84499
85643
|
`);
|
|
84500
85644
|
shadowEmit({ kind: "bridgeDown", at: Date.now() });
|
|
85645
|
+
if (BRIDGE_DEAD_ESCALATION_ENABLED)
|
|
85646
|
+
bridgeDeadWatchdog.noteBridgeDisconnected(client3.agentName);
|
|
84501
85647
|
}
|
|
84502
85648
|
flushOnAgentDisconnect({
|
|
84503
85649
|
agentName: client3.agentName,
|
|
@@ -84552,6 +85698,7 @@ var ipcServer = createIpcServer({
|
|
|
84552
85698
|
if (pendingDeferredInterrupt != null && !toolFlightTracker.isMidToolCall()) {
|
|
84553
85699
|
fireDeferredInterrupt("boundary");
|
|
84554
85700
|
}
|
|
85701
|
+
notifyHaltBoundaryWaiters();
|
|
84555
85702
|
if (currentTurn != null) {
|
|
84556
85703
|
const key = statusKey(currentTurn.sessionChatId, currentTurn.sessionThreadId);
|
|
84557
85704
|
if (ev.kind === "thinking") {
|
|
@@ -85948,20 +87095,58 @@ ${url}`;
|
|
|
85948
87095
|
turn2.silentAnchorText = effectiveText;
|
|
85949
87096
|
}
|
|
85950
87097
|
}
|
|
85951
|
-
const
|
|
87098
|
+
const photoPrecheck = new Map;
|
|
87099
|
+
const documentReroutes = [];
|
|
87100
|
+
const sentAsDocument = new Set;
|
|
87101
|
+
for (const f of files) {
|
|
87102
|
+
if (!PHOTO_EXTS.has(extname(f).toLowerCase()))
|
|
87103
|
+
continue;
|
|
87104
|
+
const cls = classifyPhotoFile(f);
|
|
87105
|
+
photoPrecheck.set(f, cls);
|
|
87106
|
+
if (cls.route === "document") {
|
|
87107
|
+
documentReroutes.push({ path: f, reason: cls.reason });
|
|
87108
|
+
sentAsDocument.add(f);
|
|
87109
|
+
process.stderr.write(`telegram gateway: photo-precheck rerouting ${f} as document (${cls.reason})
|
|
87110
|
+
`);
|
|
87111
|
+
}
|
|
87112
|
+
}
|
|
87113
|
+
const sendableAsPhoto = (f) => PHOTO_EXTS.has(extname(f).toLowerCase()) && photoPrecheck.get(f)?.route !== "document";
|
|
87114
|
+
const allPhotos = files.length >= 2 && files.length <= 10 && files.every(sendableAsPhoto);
|
|
85952
87115
|
const replyParams = reply_to != null && replyMode !== "off" ? { reply_parameters: { message_id: reply_to } } : {};
|
|
87116
|
+
const sendAsDocument = (f) => retryWithThreadFallback2(robustApiCall, (tid) => {
|
|
87117
|
+
const baseOpts = {
|
|
87118
|
+
...replyParams,
|
|
87119
|
+
...tid != null ? { message_thread_id: tid } : {}
|
|
87120
|
+
};
|
|
87121
|
+
return lockedBot.api.sendDocument(chat_id, new import_grammy13.InputFile(f), baseOpts);
|
|
87122
|
+
}, { threadId, chat_id, verb: "sendDocument" });
|
|
85953
87123
|
if (allPhotos) {
|
|
85954
87124
|
const media = files.map((f) => ({
|
|
85955
87125
|
type: "photo",
|
|
85956
87126
|
media: new import_grammy13.InputFile(f)
|
|
85957
87127
|
}));
|
|
85958
|
-
|
|
85959
|
-
|
|
85960
|
-
|
|
85961
|
-
|
|
85962
|
-
|
|
85963
|
-
|
|
85964
|
-
|
|
87128
|
+
let sent;
|
|
87129
|
+
try {
|
|
87130
|
+
sent = await retryWithThreadFallback2(robustApiCall, (tid) => {
|
|
87131
|
+
const baseOpts = {
|
|
87132
|
+
...replyParams,
|
|
87133
|
+
...tid != null ? { message_thread_id: tid } : {}
|
|
87134
|
+
};
|
|
87135
|
+
return lockedBot.api.sendMediaGroup(chat_id, media, baseOpts);
|
|
87136
|
+
}, { threadId, chat_id, verb: "sendMediaGroup" });
|
|
87137
|
+
} catch (err) {
|
|
87138
|
+
if (!isPhotoDimensionRejectError(err))
|
|
87139
|
+
throw err;
|
|
87140
|
+
process.stderr.write(`telegram gateway: sendMediaGroup rejected the photo album (${err instanceof Error ? err.message : String(err)}); falling back to per-file sendDocument
|
|
87141
|
+
`);
|
|
87142
|
+
sent = [];
|
|
87143
|
+
const albumReason = "Telegram rejected the photo album; whole album re-sent as documents";
|
|
87144
|
+
for (const f of files) {
|
|
87145
|
+
sent.push(await sendAsDocument(f));
|
|
87146
|
+
sentAsDocument.add(f);
|
|
87147
|
+
documentReroutes.push({ path: f, reason: albumReason });
|
|
87148
|
+
}
|
|
87149
|
+
}
|
|
85965
87150
|
if (threadId != null) {
|
|
85966
87151
|
const first = sent[0];
|
|
85967
87152
|
if (first && first.message_thread_id == null)
|
|
@@ -85971,23 +87156,33 @@ ${url}`;
|
|
|
85971
87156
|
sentIds.push(m.message_id);
|
|
85972
87157
|
} else {
|
|
85973
87158
|
for (const f of files) {
|
|
85974
|
-
const ext = extname(f).toLowerCase();
|
|
85975
87159
|
const input = new import_grammy13.InputFile(f);
|
|
85976
|
-
const isPhoto =
|
|
85977
|
-
|
|
85978
|
-
|
|
85979
|
-
|
|
85980
|
-
|
|
85981
|
-
|
|
85982
|
-
|
|
85983
|
-
|
|
87160
|
+
const isPhoto = sendableAsPhoto(f);
|
|
87161
|
+
let sent;
|
|
87162
|
+
try {
|
|
87163
|
+
sent = await retryWithThreadFallback2(robustApiCall, (tid) => {
|
|
87164
|
+
const baseOpts = {
|
|
87165
|
+
...replyParams,
|
|
87166
|
+
...tid != null ? { message_thread_id: tid } : {}
|
|
87167
|
+
};
|
|
87168
|
+
return isPhoto ? lockedBot.api.sendPhoto(chat_id, input, baseOpts) : lockedBot.api.sendDocument(chat_id, input, baseOpts);
|
|
87169
|
+
}, { threadId, chat_id, verb: isPhoto ? "sendPhoto" : "sendDocument" });
|
|
87170
|
+
} catch (err) {
|
|
87171
|
+
if (!(isPhoto && isPhotoDimensionRejectError(err)))
|
|
87172
|
+
throw err;
|
|
87173
|
+
process.stderr.write(`telegram gateway: sendPhoto rejected ${f} (${err instanceof Error ? err.message : String(err)}); falling back to sendDocument
|
|
87174
|
+
`);
|
|
87175
|
+
sent = await sendAsDocument(f);
|
|
87176
|
+
sentAsDocument.add(f);
|
|
87177
|
+
documentReroutes.push({ path: f, reason: "Telegram rejected it as a photo; re-sent as document" });
|
|
87178
|
+
}
|
|
85984
87179
|
if (threadId != null && sent.message_thread_id == null) {
|
|
85985
87180
|
threadId = undefined;
|
|
85986
87181
|
}
|
|
85987
87182
|
sentIds.push(sent.message_id);
|
|
85988
87183
|
}
|
|
85989
87184
|
}
|
|
85990
|
-
const result = sentIds.length === 1 ? `sent (id: ${sentIds[0]})` : `sent ${sentIds.length} parts (ids: ${sentIds.join(", ")})
|
|
87185
|
+
const result = (sentIds.length === 1 ? `sent (id: ${sentIds[0]})` : `sent ${sentIds.length} parts (ids: ${sentIds.join(", ")})`) + rerouteResultSuffix(documentReroutes);
|
|
85991
87186
|
if (HISTORY_ENABLED && sentIds.length > 0) {
|
|
85992
87187
|
try {
|
|
85993
87188
|
const fileCount = files.length;
|
|
@@ -85999,9 +87194,11 @@ ${url}`;
|
|
|
85999
87194
|
attachKinds.push(null);
|
|
86000
87195
|
}
|
|
86001
87196
|
for (let i = 0;i < fileCount; i++) {
|
|
86002
|
-
const
|
|
86003
|
-
|
|
86004
|
-
|
|
87197
|
+
const f = files[i] ?? "";
|
|
87198
|
+
const ext = extname(f).toLowerCase();
|
|
87199
|
+
const kind = PHOTO_EXTS.has(ext) && !sentAsDocument.has(f) ? "photo" : "document";
|
|
87200
|
+
texts.push(`(${kind}: ${f})`);
|
|
87201
|
+
attachKinds.push(kind);
|
|
86005
87202
|
}
|
|
86006
87203
|
recordOutbound({ chat_id, thread_id: threadId ?? null, message_ids: sentIds, texts, attachment_kinds: attachKinds });
|
|
86007
87204
|
} catch (err) {
|
|
@@ -86262,8 +87459,8 @@ async function publishToTelegraph(text5, shortName, authorName) {
|
|
|
86262
87459
|
const accountPath = join53(STATE_DIR, "telegraph-account.json");
|
|
86263
87460
|
let account = null;
|
|
86264
87461
|
try {
|
|
86265
|
-
if (
|
|
86266
|
-
const raw =
|
|
87462
|
+
if (existsSync50(accountPath)) {
|
|
87463
|
+
const raw = readFileSync53(accountPath, "utf-8");
|
|
86267
87464
|
const parsed = JSON.parse(raw);
|
|
86268
87465
|
if (parsed.shortName && parsed.accessToken) {
|
|
86269
87466
|
account = parsed;
|
|
@@ -86283,7 +87480,7 @@ async function publishToTelegraph(text5, shortName, authorName) {
|
|
|
86283
87480
|
account = created.value;
|
|
86284
87481
|
try {
|
|
86285
87482
|
mkdirSync39(STATE_DIR, { recursive: true, mode: 448 });
|
|
86286
|
-
|
|
87483
|
+
writeFileSync42(accountPath, JSON.stringify(account, null, 2), { mode: 384 });
|
|
86287
87484
|
} catch (err) {
|
|
86288
87485
|
process.stderr.write(`telegram gateway: telegraph cache write failed: ${err.message}
|
|
86289
87486
|
`);
|
|
@@ -86723,7 +87920,7 @@ async function executeVaultRequestAccess(args) {
|
|
|
86723
87920
|
}
|
|
86724
87921
|
function readLiveSwitchroomConfigText() {
|
|
86725
87922
|
const cfgPath = process.env.SWITCHROOM_CONFIG ?? findConfigFile2();
|
|
86726
|
-
return
|
|
87923
|
+
return readFileSync53(cfgPath, "utf8");
|
|
86727
87924
|
}
|
|
86728
87925
|
var MENTAL_MODEL_NAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
|
|
86729
87926
|
function buildMentalModelProposeKeyboard(stageId) {
|
|
@@ -86894,7 +88091,7 @@ async function executeDownloadAttachment(args) {
|
|
|
86894
88091
|
});
|
|
86895
88092
|
mkdirSync39(INBOX_DIR, { recursive: true, mode: 448 });
|
|
86896
88093
|
assertInsideInbox(INBOX_DIR, dlPath);
|
|
86897
|
-
|
|
88094
|
+
writeFileSync42(dlPath, buf, { mode: 384 });
|
|
86898
88095
|
return { content: [{ type: "text", text: dlPath }] };
|
|
86899
88096
|
}
|
|
86900
88097
|
async function executeEditMessage(args) {
|
|
@@ -88401,6 +89598,9 @@ async function handleInboundCoalesced(ctx, text5, downloadImage, attachment) {
|
|
|
88401
89598
|
if (parseInterruptMarker(text5).isInterrupt) {
|
|
88402
89599
|
return handleInbound(ctx, text5, downloadImage, attachment);
|
|
88403
89600
|
}
|
|
89601
|
+
if (parseStopKeyword(text5) && downloadImage == null && attachment == null) {
|
|
89602
|
+
return handleInbound(ctx, text5, downloadImage, attachment);
|
|
89603
|
+
}
|
|
88404
89604
|
const hasAttachment = downloadImage != null || attachment != null;
|
|
88405
89605
|
const maxAttachments = coalesceMaxAttachments();
|
|
88406
89606
|
if (hasAttachment && ctx.message?.media_group_id != null && maxAttachments <= 1) {
|
|
@@ -88509,6 +89709,27 @@ async function handleInbound(ctx, text5, downloadImage, attachment, extraAttachm
|
|
|
88509
89709
|
process.stderr.write(`telegram gateway: inbound classifier error: ${err.message}
|
|
88510
89710
|
`);
|
|
88511
89711
|
}
|
|
89712
|
+
if (parseStopKeyword(text5) && downloadImage == null && attachment == null && (extraAttachments == null || extraAttachments.length === 0)) {
|
|
89713
|
+
const queuedLabels = pendingSessionCommand.list().map((c) => `/${c.kind} ${c.targetLabel}`);
|
|
89714
|
+
const inFlight = turnInFlightForGate();
|
|
89715
|
+
process.stderr.write(`telegram gateway: stop-keyword received chat_id=${chat_id} in_flight_turn=${inFlight} queued_cmds=${queuedLabels.length}
|
|
89716
|
+
`);
|
|
89717
|
+
if (inFlight) {
|
|
89718
|
+
if (msgId != null) {
|
|
89719
|
+
bot.api.setMessageReaction(chat_id, msgId, [
|
|
89720
|
+
{ type: "emoji", emoji: "\u26A1" }
|
|
89721
|
+
]).catch(() => {});
|
|
89722
|
+
}
|
|
89723
|
+
await executeHaltNow("stop-keyword");
|
|
89724
|
+
}
|
|
89725
|
+
const stopReply = buildStopReply(inFlight, queuedLabels);
|
|
89726
|
+
await swallowingApiCall(() => bot.api.sendMessage(chat_id, stopReply.text, messageThreadId != null ? { message_thread_id: messageThreadId } : {}), {
|
|
89727
|
+
chat_id,
|
|
89728
|
+
verb: "stop-keyword-reply",
|
|
89729
|
+
...messageThreadId != null ? { threadId: messageThreadId } : {}
|
|
89730
|
+
});
|
|
89731
|
+
return;
|
|
89732
|
+
}
|
|
88512
89733
|
const interrupt = parseInterruptMarker(text5);
|
|
88513
89734
|
let deferInterrupt = false;
|
|
88514
89735
|
if (interrupt.isInterrupt) {
|
|
@@ -88525,6 +89746,15 @@ async function handleInbound(ctx, text5, downloadImage, attachment, extraAttachm
|
|
|
88525
89746
|
{ type: "emoji", emoji: "\u26A1" }
|
|
88526
89747
|
]).catch(() => {});
|
|
88527
89748
|
}
|
|
89749
|
+
if (interrupt.emptyBody) {
|
|
89750
|
+
await executeHaltNow("bang-empty");
|
|
89751
|
+
await swallowingApiCall(() => bot.api.sendMessage(chat_id, "\u26A1 Interrupted. Send your replacement instruction now.", messageThreadId != null ? { message_thread_id: messageThreadId } : {}), {
|
|
89752
|
+
chat_id,
|
|
89753
|
+
verb: "interrupt-empty-body",
|
|
89754
|
+
...messageThreadId != null ? { threadId: messageThreadId } : {}
|
|
89755
|
+
});
|
|
89756
|
+
return;
|
|
89757
|
+
}
|
|
88528
89758
|
if (agentName3 && !deferInterrupt) {
|
|
88529
89759
|
try {
|
|
88530
89760
|
const { sendAgentInterrupt: sendAgentInterrupt2 } = await Promise.resolve().then(() => (init_tmux(), exports_tmux));
|
|
@@ -88542,14 +89772,6 @@ async function handleInbound(ctx, text5, downloadImage, attachment, extraAttachm
|
|
|
88542
89772
|
}
|
|
88543
89773
|
cancelInterruptedObligation();
|
|
88544
89774
|
}
|
|
88545
|
-
if (interrupt.emptyBody) {
|
|
88546
|
-
await swallowingApiCall(() => bot.api.sendMessage(chat_id, "\u26A1 Interrupted. Send your replacement instruction now.", messageThreadId != null ? { message_thread_id: messageThreadId } : {}), {
|
|
88547
|
-
chat_id,
|
|
88548
|
-
verb: "interrupt-empty-body",
|
|
88549
|
-
...messageThreadId != null ? { threadId: messageThreadId } : {}
|
|
88550
|
-
});
|
|
88551
|
-
return;
|
|
88552
|
-
}
|
|
88553
89775
|
text5 = interrupt.body;
|
|
88554
89776
|
}
|
|
88555
89777
|
if (STATUS_QUERY_RE.test(text5)) {
|
|
@@ -89420,7 +90642,7 @@ function writeRestartMarker(marker) {
|
|
|
89420
90642
|
if (!p)
|
|
89421
90643
|
return;
|
|
89422
90644
|
try {
|
|
89423
|
-
|
|
90645
|
+
writeFileSync42(p, JSON.stringify(marker));
|
|
89424
90646
|
lastPlannedRestartAt = Date.now();
|
|
89425
90647
|
process.stderr.write(`telegram gateway: restart-marker: write chat_id=${marker.chat_id} thread_id=${marker.thread_id ?? "-"} ack=${marker.ack_message_id ?? "-"} path=${p}
|
|
89426
90648
|
`);
|
|
@@ -89439,7 +90661,7 @@ function readRestartMarker() {
|
|
|
89439
90661
|
if (!p)
|
|
89440
90662
|
return null;
|
|
89441
90663
|
try {
|
|
89442
|
-
return JSON.parse(
|
|
90664
|
+
return JSON.parse(readFileSync53(p, "utf8"));
|
|
89443
90665
|
} catch {
|
|
89444
90666
|
return null;
|
|
89445
90667
|
}
|
|
@@ -89588,7 +90810,7 @@ var _dockerReachable;
|
|
|
89588
90810
|
function isDockerReachable() {
|
|
89589
90811
|
if (_dockerReachable !== undefined)
|
|
89590
90812
|
return _dockerReachable;
|
|
89591
|
-
if (!
|
|
90813
|
+
if (!existsSync50("/var/run/docker.sock")) {
|
|
89592
90814
|
_dockerReachable = false;
|
|
89593
90815
|
return _dockerReachable;
|
|
89594
90816
|
}
|
|
@@ -89609,8 +90831,8 @@ function spawnSwitchroomDetached(args, onFailure) {
|
|
|
89609
90831
|
let outFd = null;
|
|
89610
90832
|
try {
|
|
89611
90833
|
mkdirSync39(STATE_DIR, { recursive: true });
|
|
89612
|
-
outFd =
|
|
89613
|
-
|
|
90834
|
+
outFd = openSync11(logPath, "a");
|
|
90835
|
+
writeFileSync42(logPath, `
|
|
89614
90836
|
[${new Date().toISOString()}] spawn ${SWITCHROOM_CLI} ${fullArgs.join(" ")}
|
|
89615
90837
|
`, { flag: "a" });
|
|
89616
90838
|
} catch {}
|
|
@@ -89624,7 +90846,7 @@ function spawnSwitchroomDetached(args, onFailure) {
|
|
|
89624
90846
|
});
|
|
89625
90847
|
if (outFd != null) {
|
|
89626
90848
|
try {
|
|
89627
|
-
|
|
90849
|
+
closeSync11(outFd);
|
|
89628
90850
|
} catch {}
|
|
89629
90851
|
}
|
|
89630
90852
|
if (onFailure) {
|
|
@@ -89636,7 +90858,7 @@ function spawnSwitchroomDetached(args, onFailure) {
|
|
|
89636
90858
|
return;
|
|
89637
90859
|
let tail = "";
|
|
89638
90860
|
try {
|
|
89639
|
-
const full =
|
|
90861
|
+
const full = readFileSync53(logPath, "utf8");
|
|
89640
90862
|
tail = full.split(`
|
|
89641
90863
|
`).slice(-30).join(`
|
|
89642
90864
|
`).trim();
|
|
@@ -90004,9 +91226,9 @@ bot.use(async (ctx, next) => {
|
|
|
90004
91226
|
function readRecentDenialsForAgent(agentName3, windowMs, limit) {
|
|
90005
91227
|
try {
|
|
90006
91228
|
const auditPath = join53(homedir17(), ".switchroom", "vault-audit.log");
|
|
90007
|
-
if (!
|
|
91229
|
+
if (!existsSync50(auditPath))
|
|
90008
91230
|
return [];
|
|
90009
|
-
const raw =
|
|
91231
|
+
const raw = readFileSync53(auditPath, "utf8");
|
|
90010
91232
|
return recentDenialsFromAuditLog(raw, { agentName: agentName3, windowMs, limit });
|
|
90011
91233
|
} catch {
|
|
90012
91234
|
return [];
|
|
@@ -90057,7 +91279,7 @@ async function buildAgentMetadata(agentName3) {
|
|
|
90057
91279
|
try {
|
|
90058
91280
|
const agentDir = resolveAgentDirFromEnv();
|
|
90059
91281
|
if (agentDir) {
|
|
90060
|
-
const raw =
|
|
91282
|
+
const raw = readFileSync53(join53(agentDir, ".claude", ".claude.json"), "utf8");
|
|
90061
91283
|
claudeJson = JSON.parse(raw);
|
|
90062
91284
|
}
|
|
90063
91285
|
} catch {}
|
|
@@ -90327,6 +91549,192 @@ function modelMenuReplyMarkup(reply) {
|
|
|
90327
91549
|
}
|
|
90328
91550
|
return kb;
|
|
90329
91551
|
}
|
|
91552
|
+
function recordTypedModelSwitch(reply, requestedModelArg, deps) {
|
|
91553
|
+
const requested = requestedModelArg != null ? expandSrAlias(requestedModelArg) : null;
|
|
91554
|
+
if (requested?.toLowerCase() === "default") {
|
|
91555
|
+
const smDir2 = resolveAgentDirFromEnv();
|
|
91556
|
+
if (smDir2)
|
|
91557
|
+
clearSessionModelFile(smDir2);
|
|
91558
|
+
if (reply.selectedModel)
|
|
91559
|
+
sessionModelSource.setOverride(null);
|
|
91560
|
+
return "";
|
|
91561
|
+
}
|
|
91562
|
+
if (!reply.selectedModel)
|
|
91563
|
+
return "";
|
|
91564
|
+
sessionModelSource.setOverride(reply.selectedModel);
|
|
91565
|
+
const smDir = resolveAgentDirFromEnv();
|
|
91566
|
+
if (smDir && requested && isValidModelArg(requested) && !isSrModel(requested)) {
|
|
91567
|
+
try {
|
|
91568
|
+
writeSessionModelFile(smDir, requested, readConfiguredDefaultModel(smDir) ?? resolveMainModel(deps.getConfiguredModel() ?? undefined));
|
|
91569
|
+
} catch (err) {
|
|
91570
|
+
process.stderr.write(`telegram gateway: session-model persist failed (typed /model): ${err?.message ?? String(err)}
|
|
91571
|
+
`);
|
|
91572
|
+
return `
|
|
91573
|
+
\u26A0\uFE0F Couldn\u2019t persist the sticky override \u2014 the switch is live now but won\u2019t survive a relaunch.`;
|
|
91574
|
+
}
|
|
91575
|
+
}
|
|
91576
|
+
return "";
|
|
91577
|
+
}
|
|
91578
|
+
function recordModelMenuSideEffects(outcome, modelDeps, cbChatId, cbThreadId, prevSessionModel) {
|
|
91579
|
+
if (outcome.selectedModel) {
|
|
91580
|
+
sessionModelSource.setOverride(outcome.selectedModel);
|
|
91581
|
+
const smDir = resolveAgentDirFromEnv();
|
|
91582
|
+
if (smDir && outcome.selectedModelToken) {
|
|
91583
|
+
try {
|
|
91584
|
+
writeSessionModelFile(smDir, outcome.selectedModelToken, readConfiguredDefaultModel(smDir) ?? resolveMainModel(modelDeps.getConfiguredModel() ?? undefined));
|
|
91585
|
+
} catch (err) {
|
|
91586
|
+
outcome.reply.text += `
|
|
91587
|
+
\u26A0\uFE0F Couldn\u2019t persist the sticky override \u2014 the switch is live now but won\u2019t survive a relaunch.`;
|
|
91588
|
+
process.stderr.write(`telegram gateway: session-model persist failed (menu): ${err?.message ?? String(err)}
|
|
91589
|
+
`);
|
|
91590
|
+
}
|
|
91591
|
+
}
|
|
91592
|
+
}
|
|
91593
|
+
if (outcome.clearedDefault) {
|
|
91594
|
+
const smDir = resolveAgentDirFromEnv();
|
|
91595
|
+
if (smDir)
|
|
91596
|
+
clearSessionModelFile(smDir);
|
|
91597
|
+
}
|
|
91598
|
+
if (outcome.selectedModel && isSrToClaudeTransition(prevSessionModel, outcome.selectedModel)) {
|
|
91599
|
+
const agentName3 = getMyAgentName();
|
|
91600
|
+
const agentDir = resolveAgentDirFromEnv();
|
|
91601
|
+
const token = outcome.selectedModelToken;
|
|
91602
|
+
if (agentDir && token) {
|
|
91603
|
+
try {
|
|
91604
|
+
writeSessionModelFile(agentDir, token, readConfiguredDefaultModel(agentDir) ?? resolveMainModel(modelDeps.getConfiguredModel() ?? undefined));
|
|
91605
|
+
sessionModelSource.setOverride(token);
|
|
91606
|
+
} catch (e) {
|
|
91607
|
+
process.stderr.write(`telegram gateway: sr-to-claude session-model write failed: ${e?.message ?? String(e)}
|
|
91608
|
+
`);
|
|
91609
|
+
}
|
|
91610
|
+
} else if (agentDir) {
|
|
91611
|
+
clearSessionModelFile(agentDir);
|
|
91612
|
+
}
|
|
91613
|
+
writeRestartMarker({ chat_id: cbChatId, thread_id: cbThreadId ?? null, ack_message_id: null, ts: Date.now() });
|
|
91614
|
+
stampUserRestartReason("user: sr-to-claude model switch (menu)");
|
|
91615
|
+
if (turnInFlightForGate()) {
|
|
91616
|
+
pendingRestarts.set(agentName3, Date.now());
|
|
91617
|
+
} else {
|
|
91618
|
+
sweepBeforeSelfRestart().finally(() => triggerSelfRestart(agentName3, "sr-to-claude-model-switch", 1500));
|
|
91619
|
+
}
|
|
91620
|
+
return {
|
|
91621
|
+
restartNotice: `\uD83D\uDD04 Switching from **${escapeHtmlForTg2(prevSessionModel)}** back to Claude \u2014 restarting session cleanly. Claude will be ready in ~30s.`
|
|
91622
|
+
};
|
|
91623
|
+
}
|
|
91624
|
+
return {};
|
|
91625
|
+
}
|
|
91626
|
+
async function editPendingCommandCard(chatId, messageId, text5) {
|
|
91627
|
+
try {
|
|
91628
|
+
await robustApiCall(() => lockedBot.api.editMessageText(chatId, messageId, richMessage2(hardenCardBreaks2(text5)), {
|
|
91629
|
+
reply_markup: { inline_keyboard: [] }
|
|
91630
|
+
}), { chat_id: chatId, verb: "pending-cmd-card" });
|
|
91631
|
+
} catch (err) {
|
|
91632
|
+
process.stderr.write(`telegram gateway: pending-command card edit failed chat=${chatId} msg=${messageId}: ${err?.message ?? String(err)}
|
|
91633
|
+
`);
|
|
91634
|
+
}
|
|
91635
|
+
}
|
|
91636
|
+
function enqueueSessionCommand(cmd) {
|
|
91637
|
+
const displaced = pendingSessionCommand.set(cmd);
|
|
91638
|
+
if (displaced) {
|
|
91639
|
+
editPendingCommandCard(displaced.ackChatId, displaced.ackMessageId, supersededText(displaced, cmd, escapeHtmlForTg2));
|
|
91640
|
+
}
|
|
91641
|
+
if (!turnInFlightForGate())
|
|
91642
|
+
drainPendingSessionCommand();
|
|
91643
|
+
}
|
|
91644
|
+
async function applyQueuedModelCommand(cmd) {
|
|
91645
|
+
const deps = buildModelDeps({ chatId: cmd.chatId, threadId: cmd.threadId });
|
|
91646
|
+
if (cmd.origin === "menu") {
|
|
91647
|
+
const prevSessionModel = sessionModelSource.getOverride();
|
|
91648
|
+
const outcome = await handleModelMenuCallback(cmd.arg, deps);
|
|
91649
|
+
const { restartNotice } = recordModelMenuSideEffects(outcome, deps, cmd.chatId, cmd.threadId, prevSessionModel);
|
|
91650
|
+
return restartNotice ?? outcome.reply.text;
|
|
91651
|
+
}
|
|
91652
|
+
const reply = await handleModelCommand({ kind: "set", model: cmd.arg }, deps);
|
|
91653
|
+
const warning = recordTypedModelSwitch(reply, cmd.arg, deps);
|
|
91654
|
+
return reply.text + warning;
|
|
91655
|
+
}
|
|
91656
|
+
async function applyQueuedEffortCommand(cmd) {
|
|
91657
|
+
const deps = buildEffortDeps();
|
|
91658
|
+
if (cmd.origin === "menu") {
|
|
91659
|
+
const outcome = await handleEffortMenuCallback(cmd.arg, deps);
|
|
91660
|
+
return outcome.reply.text;
|
|
91661
|
+
}
|
|
91662
|
+
const parsed = cmd.arg === "default" ? { kind: "default" } : { kind: "set", level: cmd.arg };
|
|
91663
|
+
const reply = await handleEffortCommand(parsed, deps);
|
|
91664
|
+
return reply.text;
|
|
91665
|
+
}
|
|
91666
|
+
function reEnqueueUnlessSuperseded(cmd) {
|
|
91667
|
+
const newer = pendingSessionCommand.get(cmd.kind);
|
|
91668
|
+
if (newer != null) {
|
|
91669
|
+
editPendingCommandCard(cmd.ackChatId, cmd.ackMessageId, supersededText(cmd, newer, escapeHtmlForTg2));
|
|
91670
|
+
return;
|
|
91671
|
+
}
|
|
91672
|
+
pendingSessionCommand.set(cmd);
|
|
91673
|
+
}
|
|
91674
|
+
function persistQueuedCommandForRestart(action) {
|
|
91675
|
+
if (action.persist == null)
|
|
91676
|
+
return action.reissueText;
|
|
91677
|
+
const agentDir = resolveAgentDirFromEnv();
|
|
91678
|
+
if (agentDir == null)
|
|
91679
|
+
return action.reissueText;
|
|
91680
|
+
try {
|
|
91681
|
+
switch (action.persist) {
|
|
91682
|
+
case "model": {
|
|
91683
|
+
if (!isOfflineTrustedModelToken(action.arg)) {
|
|
91684
|
+
return `\u21A9\uFE0F Couldn\u2019t verify \`${escapeHtmlForTg2(action.cmd.targetLabel || action.arg)}\` as a known model without the live session \u2014 it was NOT saved. Re-issue \`/model ${escapeHtmlForTg2(action.arg)}\` once the agent is back.`;
|
|
91685
|
+
}
|
|
91686
|
+
const configured = readConfiguredDefaultModel(agentDir) ?? resolveMainModel(undefined);
|
|
91687
|
+
writeSessionModelFile(agentDir, expandSrAlias(action.arg), configured);
|
|
91688
|
+
writeRelaunchModelIntent(agentDir, "keep", "queued /model carried across restart");
|
|
91689
|
+
break;
|
|
91690
|
+
}
|
|
91691
|
+
case "clear-model":
|
|
91692
|
+
clearSessionModelFile(agentDir);
|
|
91693
|
+
break;
|
|
91694
|
+
case "effort":
|
|
91695
|
+
writeSessionEffortFile(agentDir, action.arg, getConfiguredEffortForPersist());
|
|
91696
|
+
break;
|
|
91697
|
+
case "clear-effort":
|
|
91698
|
+
clearSessionEffortFile(agentDir);
|
|
91699
|
+
break;
|
|
91700
|
+
}
|
|
91701
|
+
return action.persistedText;
|
|
91702
|
+
} catch (err) {
|
|
91703
|
+
process.stderr.write(`telegram gateway: queued-command persist-for-restart failed kind=${action.cmd.kind} arg=${action.arg}: ${err?.message ?? String(err)}
|
|
91704
|
+
`);
|
|
91705
|
+
return action.reissueText;
|
|
91706
|
+
}
|
|
91707
|
+
}
|
|
91708
|
+
function getConfiguredEffortForPersist() {
|
|
91709
|
+
try {
|
|
91710
|
+
const data = switchroomExecJson(["agent", "list"]);
|
|
91711
|
+
return data?.agents?.find((a) => a.name === getMyAgentName())?.thinking_effort ?? SWITCHROOM_DEFAULT_THINKING_EFFORT;
|
|
91712
|
+
} catch {
|
|
91713
|
+
return SWITCHROOM_DEFAULT_THINKING_EFFORT;
|
|
91714
|
+
}
|
|
91715
|
+
}
|
|
91716
|
+
var pendingCmdDraining = false;
|
|
91717
|
+
async function drainPendingSessionCommand() {
|
|
91718
|
+
if (pendingCmdDraining)
|
|
91719
|
+
return;
|
|
91720
|
+
if (pendingSessionCommand.size === 0)
|
|
91721
|
+
return;
|
|
91722
|
+
pendingCmdDraining = true;
|
|
91723
|
+
try {
|
|
91724
|
+
await drainTakenCommands(pendingSessionCommand.takeAll(), {
|
|
91725
|
+
restartPending: () => pendingRestarts.size > 0,
|
|
91726
|
+
turnInFlight: () => turnInFlightForGate(),
|
|
91727
|
+
apply: (cmd) => cmd.kind === "model" ? applyQueuedModelCommand(cmd) : applyQueuedEffortCommand(cmd),
|
|
91728
|
+
isBusyRefusal: isBusyRefusalText,
|
|
91729
|
+
resolveForRestartText: (cmd) => persistQueuedCommandForRestart(resolveForRestart(cmd, escapeHtmlForTg2)),
|
|
91730
|
+
editCard: (cmd, text5) => editPendingCommandCard(cmd.ackChatId, cmd.ackMessageId, text5),
|
|
91731
|
+
reEnqueue: reEnqueueUnlessSuperseded,
|
|
91732
|
+
failureText: (cmd, err) => `\u274C Couldn\u2019t apply the queued ${cmd.kind} switch to \`${escapeHtmlForTg2(cmd.targetLabel)}\`: ${escapeHtmlForTg2(err?.message ?? String(err))}`
|
|
91733
|
+
});
|
|
91734
|
+
} finally {
|
|
91735
|
+
pendingCmdDraining = false;
|
|
91736
|
+
}
|
|
91737
|
+
}
|
|
90330
91738
|
bot.command("model", async (ctx) => {
|
|
90331
91739
|
if (!isAuthorizedSender(ctx))
|
|
90332
91740
|
return;
|
|
@@ -90340,38 +91748,53 @@ bot.command("model", async (ctx) => {
|
|
|
90340
91748
|
await switchroomReply(ctx, menu.text, { html: true, reply_markup: modelMenuReplyMarkup(menu) });
|
|
90341
91749
|
return;
|
|
90342
91750
|
}
|
|
90343
|
-
|
|
90344
|
-
|
|
90345
|
-
|
|
90346
|
-
|
|
90347
|
-
|
|
90348
|
-
|
|
90349
|
-
|
|
90350
|
-
|
|
90351
|
-
|
|
90352
|
-
|
|
90353
|
-
|
|
90354
|
-
|
|
90355
|
-
|
|
90356
|
-
|
|
90357
|
-
|
|
90358
|
-
} catch (err) {
|
|
90359
|
-
persistWarning = `
|
|
90360
|
-
\u26A0\uFE0F Couldn\u2019t persist the sticky override \u2014 the switch is live now but won\u2019t survive a relaunch.`;
|
|
90361
|
-
process.stderr.write(`telegram gateway: session-model persist failed (typed /model): ${err?.message ?? String(err)}
|
|
90362
|
-
`);
|
|
90363
|
-
}
|
|
90364
|
-
}
|
|
91751
|
+
if (parsed.kind === "set" && deps.isBusy()) {
|
|
91752
|
+
const target = expandSrAlias(parsed.model);
|
|
91753
|
+
const sent = await ctx.replyWithRichMessage(richMessage2(hardenCardBreaks2(ackText("model", target, escapeHtmlForTg2))), threadId != null ? { message_thread_id: threadId } : {});
|
|
91754
|
+
enqueueSessionCommand({
|
|
91755
|
+
kind: "model",
|
|
91756
|
+
origin: "typed",
|
|
91757
|
+
arg: target,
|
|
91758
|
+
targetLabel: target,
|
|
91759
|
+
chatId,
|
|
91760
|
+
threadId,
|
|
91761
|
+
ackChatId: chatId,
|
|
91762
|
+
ackMessageId: sent.message_id,
|
|
91763
|
+
requestedAt: Date.now()
|
|
91764
|
+
});
|
|
91765
|
+
return;
|
|
90365
91766
|
}
|
|
91767
|
+
const reply = await handleModelCommand(parsed, deps);
|
|
91768
|
+
const persistWarning = recordTypedModelSwitch(reply, parsed.kind === "set" ? parsed.model : null, deps);
|
|
90366
91769
|
await switchroomReply(ctx, reply.text + persistWarning, { html: reply.html });
|
|
90367
91770
|
});
|
|
90368
91771
|
function buildEffortDeps() {
|
|
90369
91772
|
return {
|
|
90370
|
-
applyEffort: (agent, level) =>
|
|
91773
|
+
applyEffort: async (agent, level) => {
|
|
91774
|
+
const result = await applyEffort(agent, level);
|
|
91775
|
+
if (result.ok) {
|
|
91776
|
+
const agentDir = resolveAgentDirFromEnv();
|
|
91777
|
+
if (agentDir) {
|
|
91778
|
+
try {
|
|
91779
|
+
writeSessionEffortFile(agentDir, level, getConfiguredEffortForPersist());
|
|
91780
|
+
} catch (err) {
|
|
91781
|
+
process.stderr.write(`telegram gateway: session-effort persist failed level=${level}: ${err?.message ?? String(err)}
|
|
91782
|
+
`);
|
|
91783
|
+
}
|
|
91784
|
+
}
|
|
91785
|
+
}
|
|
91786
|
+
return result;
|
|
91787
|
+
},
|
|
90371
91788
|
getAgentName: getMyAgentName,
|
|
90372
|
-
getConfiguredEffort: () =>
|
|
90373
|
-
|
|
90374
|
-
|
|
91789
|
+
getConfiguredEffort: () => getConfiguredEffortForPersist(),
|
|
91790
|
+
clearSessionEffort: () => {
|
|
91791
|
+
const agentDir = resolveAgentDirFromEnv();
|
|
91792
|
+
if (agentDir)
|
|
91793
|
+
clearSessionEffortFile(agentDir);
|
|
91794
|
+
},
|
|
91795
|
+
getSessionEffort: () => {
|
|
91796
|
+
const agentDir = resolveAgentDirFromEnv();
|
|
91797
|
+
return agentDir ? readSessionEffortFile(agentDir)?.level ?? null : null;
|
|
90375
91798
|
},
|
|
90376
91799
|
escapeHtml: escapeHtmlForTg2
|
|
90377
91800
|
};
|
|
@@ -90398,6 +91821,24 @@ bot.command("effort", async (ctx) => {
|
|
|
90398
91821
|
await switchroomReply(ctx, menu.text, { html: true, reply_markup: effortMenuReplyMarkup(menu) });
|
|
90399
91822
|
return;
|
|
90400
91823
|
}
|
|
91824
|
+
if ((parsed.kind === "set" || parsed.kind === "default") && currentTurn !== null) {
|
|
91825
|
+
const requestedLevel = parsed.kind === "set" ? parsed.level : "default";
|
|
91826
|
+
const chatId = String(ctx.chat.id);
|
|
91827
|
+
const threadId = resolveThreadId(chatId, ctx.message?.message_thread_id);
|
|
91828
|
+
const sent = await ctx.replyWithRichMessage(richMessage2(hardenCardBreaks2(ackText("effort", requestedLevel, escapeHtmlForTg2))), threadId != null ? { message_thread_id: threadId } : {});
|
|
91829
|
+
enqueueSessionCommand({
|
|
91830
|
+
kind: "effort",
|
|
91831
|
+
origin: "typed",
|
|
91832
|
+
arg: requestedLevel,
|
|
91833
|
+
targetLabel: requestedLevel,
|
|
91834
|
+
chatId,
|
|
91835
|
+
threadId,
|
|
91836
|
+
ackChatId: chatId,
|
|
91837
|
+
ackMessageId: sent.message_id,
|
|
91838
|
+
requestedAt: Date.now()
|
|
91839
|
+
});
|
|
91840
|
+
return;
|
|
91841
|
+
}
|
|
90401
91842
|
const reply = await handleEffortCommand(parsed, deps);
|
|
90402
91843
|
await switchroomReply(ctx, reply.text, { html: reply.html });
|
|
90403
91844
|
});
|
|
@@ -90413,7 +91854,7 @@ bot.command("agentstart", async (ctx) => {
|
|
|
90413
91854
|
}
|
|
90414
91855
|
await dispatchShortVerbViaHostd(ctx, { v: 1, op: "agent_start", request_id: hostdRequestId2("gw-start"), args: { name } }, `start ${name}`, ["agent", "start", name]);
|
|
90415
91856
|
});
|
|
90416
|
-
bot.command("
|
|
91857
|
+
bot.command("agentstop", async (ctx) => {
|
|
90417
91858
|
if (!isAuthorizedSender(ctx))
|
|
90418
91859
|
return;
|
|
90419
91860
|
const name = ctx.match?.trim() || getMyAgentName();
|
|
@@ -90425,6 +91866,23 @@ bot.command("stop", async (ctx) => {
|
|
|
90425
91866
|
}
|
|
90426
91867
|
await dispatchShortVerbViaHostd(ctx, { v: 1, op: "agent_stop", request_id: hostdRequestId2("gw-stop"), args: { name } }, `stop ${name}`, ["agent", "stop", name]);
|
|
90427
91868
|
});
|
|
91869
|
+
bot.command("stop", async (ctx) => {
|
|
91870
|
+
if (!isAuthorizedSender(ctx))
|
|
91871
|
+
return;
|
|
91872
|
+
const arg = ctx.match?.trim();
|
|
91873
|
+
if (arg) {
|
|
91874
|
+
await switchroomReply(ctx, `/stop takes no argument \u2014 it cancels MY in-flight turn. ` + `To stop a container, use /agentstop ${arg}. Nothing was stopped.`);
|
|
91875
|
+
return;
|
|
91876
|
+
}
|
|
91877
|
+
const queuedLabels = pendingSessionCommand.list().map((c) => `/${c.kind} ${c.targetLabel}`);
|
|
91878
|
+
const inFlight = turnInFlightForGate();
|
|
91879
|
+
if (!inFlight) {
|
|
91880
|
+
await switchroomReply(ctx, buildStopReply(false, queuedLabels).text);
|
|
91881
|
+
return;
|
|
91882
|
+
}
|
|
91883
|
+
await executeHaltNow("stop-command");
|
|
91884
|
+
await switchroomReply(ctx, buildStopReply(true, queuedLabels).text);
|
|
91885
|
+
});
|
|
90428
91886
|
bot.command("restart", async (ctx) => {
|
|
90429
91887
|
if (!isAuthorizedSender(ctx))
|
|
90430
91888
|
return;
|
|
@@ -90443,10 +91901,10 @@ bot.command("restart", async (ctx) => {
|
|
|
90443
91901
|
}
|
|
90444
91902
|
const chatId = String(ctx.chat.id);
|
|
90445
91903
|
const threadId = resolveThreadId(chatId, ctx.message?.message_thread_id);
|
|
90446
|
-
const
|
|
91904
|
+
const ackText2 = restartAckText(name);
|
|
90447
91905
|
let ackId = null;
|
|
90448
91906
|
try {
|
|
90449
|
-
const sent = await retryWithThreadFallback2(robustApiCall, (tid) => lockedBot.api.sendRichMessage(chatId, richMessage2(
|
|
91907
|
+
const sent = await retryWithThreadFallback2(robustApiCall, (tid) => lockedBot.api.sendRichMessage(chatId, richMessage2(ackText2), {
|
|
90450
91908
|
disable_notification: true,
|
|
90451
91909
|
...tid != null ? { message_thread_id: tid } : {}
|
|
90452
91910
|
}), { threadId, chat_id: chatId, verb: "restart.ack" });
|
|
@@ -90462,7 +91920,7 @@ bot.command("restart", async (ctx) => {
|
|
|
90462
91920
|
{
|
|
90463
91921
|
const smDir = resolveAgentDirFromEnv();
|
|
90464
91922
|
if (smDir)
|
|
90465
|
-
writeRelaunchModelIntent(smDir, "
|
|
91923
|
+
writeRelaunchModelIntent(smDir, "keep", "user: /restart from chat");
|
|
90466
91924
|
}
|
|
90467
91925
|
await sweepBeforeSelfRestart();
|
|
90468
91926
|
const hostdResp = await tryHostdDispatch2(getMyAgentName(), {
|
|
@@ -90496,8 +91954,8 @@ function flushAgentHandoff(agentDir) {
|
|
|
90496
91954
|
for (const fname of [".handoff.md", ".handoff-topic"]) {
|
|
90497
91955
|
const p = join53(agentDir, fname);
|
|
90498
91956
|
try {
|
|
90499
|
-
if (
|
|
90500
|
-
|
|
91957
|
+
if (existsSync50(p)) {
|
|
91958
|
+
unlinkSync21(p);
|
|
90501
91959
|
removed++;
|
|
90502
91960
|
}
|
|
90503
91961
|
} catch (err) {
|
|
@@ -90535,24 +91993,24 @@ async function handleNewCommand(ctx) {
|
|
|
90535
91993
|
const flushed = agentDir != null ? flushAgentHandoff(agentDir) : 0;
|
|
90536
91994
|
const chatId = String(ctx.chat.id);
|
|
90537
91995
|
const threadId = resolveThreadId(chatId, ctx.message?.message_thread_id);
|
|
90538
|
-
const
|
|
91996
|
+
const ackText2 = newSessionAckText(name, flushed > 0);
|
|
90539
91997
|
let ackId = null;
|
|
90540
91998
|
try {
|
|
90541
|
-
const sent = await retryWithThreadFallback2(robustApiCall, (tid) => lockedBot.api.sendRichMessage(chatId, richMessage2(
|
|
91999
|
+
const sent = await retryWithThreadFallback2(robustApiCall, (tid) => lockedBot.api.sendRichMessage(chatId, richMessage2(ackText2), {
|
|
90542
92000
|
disable_notification: true,
|
|
90543
92001
|
...tid != null ? { message_thread_id: tid } : {}
|
|
90544
92002
|
}), { threadId, chat_id: chatId, verb: "new-or-reset.ack" });
|
|
90545
92003
|
ackId = sent.message_id;
|
|
90546
92004
|
if (HISTORY_ENABLED) {
|
|
90547
92005
|
try {
|
|
90548
|
-
recordOutbound({ chat_id: chatId, thread_id: threadId ?? null, message_ids: [sent.message_id], texts: [
|
|
92006
|
+
recordOutbound({ chat_id: chatId, thread_id: threadId ?? null, message_ids: [sent.message_id], texts: [ackText2], attachment_kinds: [] });
|
|
90549
92007
|
} catch {}
|
|
90550
92008
|
}
|
|
90551
92009
|
} catch {}
|
|
90552
92010
|
writeRestartMarker({ chat_id: chatId, thread_id: threadId ?? null, ack_message_id: ackId, ts: Date.now() });
|
|
90553
92011
|
if (agentDir != null) {
|
|
90554
92012
|
try {
|
|
90555
|
-
|
|
92013
|
+
writeFileSync42(join53(agentDir, ".force-fresh-session"), `${kind} at ${new Date().toISOString()}
|
|
90556
92014
|
`, "utf8");
|
|
90557
92015
|
} catch (err) {
|
|
90558
92016
|
process.stderr.write(`telegram gateway: failed to write force-fresh marker: ${err}
|
|
@@ -90628,7 +92086,7 @@ Either run \`switchroom update\` from the host shell, or set \`host_control.enab
|
|
|
90628
92086
|
}
|
|
90629
92087
|
const chatId = String(ctx.chat.id);
|
|
90630
92088
|
const threadId = resolveThreadId(chatId, ctx.message?.message_thread_id);
|
|
90631
|
-
const
|
|
92089
|
+
const ackText2 = `\uD83D\uDE80 **update started** \u2014 running ${[
|
|
90632
92090
|
"`switchroom update`",
|
|
90633
92091
|
...passthrough.map((t) => `\`${t}\``)
|
|
90634
92092
|
].join(" ")}
|
|
@@ -90636,7 +92094,7 @@ Either run \`switchroom update\` from the host shell, or set \`host_control.enab
|
|
|
90636
92094
|
The gateway will restart as part of the recreate step; watch for the post-restart greeting card to confirm completion.`;
|
|
90637
92095
|
let ackId = null;
|
|
90638
92096
|
try {
|
|
90639
|
-
const sent = await retryWithThreadFallback2(robustApiCall, (tid) => lockedBot.api.sendRichMessage(chatId, richMessage2(
|
|
92097
|
+
const sent = await retryWithThreadFallback2(robustApiCall, (tid) => lockedBot.api.sendRichMessage(chatId, richMessage2(ackText2), {
|
|
90640
92098
|
disable_notification: true,
|
|
90641
92099
|
...tid != null ? { message_thread_id: tid } : {}
|
|
90642
92100
|
}), { threadId, chat_id: chatId, verb: "update.ack" });
|
|
@@ -90919,16 +92377,16 @@ bot.command("interrupt", async (ctx) => {
|
|
|
90919
92377
|
await runSwitchroomCommand(ctx, ["agent", "interrupt", name], `interrupt ${name}`);
|
|
90920
92378
|
});
|
|
90921
92379
|
var lockoutOps = {
|
|
90922
|
-
readFileSync: (p, enc) =>
|
|
90923
|
-
writeFileSync: (p, data, opts) =>
|
|
90924
|
-
existsSync: (p) =>
|
|
92380
|
+
readFileSync: (p, enc) => readFileSync53(p, enc),
|
|
92381
|
+
writeFileSync: (p, data, opts) => writeFileSync42(p, data, opts),
|
|
92382
|
+
existsSync: (p) => existsSync50(p),
|
|
90925
92383
|
mkdirSync: (p, opts) => mkdirSync39(p, opts),
|
|
90926
92384
|
joinPath: (...parts) => join53(...parts)
|
|
90927
92385
|
};
|
|
90928
92386
|
var FLEET_FALLBACK_DEDUP_MS = 30000;
|
|
90929
92387
|
function isAuthBrokerSocketReachable() {
|
|
90930
92388
|
try {
|
|
90931
|
-
return
|
|
92389
|
+
return existsSync50(resolveAuthBrokerSocketPath2());
|
|
90932
92390
|
} catch {
|
|
90933
92391
|
return false;
|
|
90934
92392
|
}
|
|
@@ -90937,6 +92395,7 @@ var fleetFallbackGate = createFleetFallbackGate({
|
|
|
90937
92395
|
dedupMs: FLEET_FALLBACK_DEDUP_MS,
|
|
90938
92396
|
brokerReachable: isAuthBrokerSocketReachable
|
|
90939
92397
|
});
|
|
92398
|
+
var modelUnavailableCardRegistry = createModelUnavailableCardRegistry();
|
|
90940
92399
|
var fleetFallbackResumeGate = createFleetFallbackResumeGate({
|
|
90941
92400
|
maxAgeMs: (() => {
|
|
90942
92401
|
const v = Number(process.env.SWITCHROOM_RESUME_MAX_AGE_MS);
|
|
@@ -91024,6 +92483,16 @@ async function doFireFleetAutoFallback(triggerAgent, untilMs) {
|
|
|
91024
92483
|
return outcome.kind === "switched";
|
|
91025
92484
|
const opts = { disable_notification: true };
|
|
91026
92485
|
for (const chat_id of access.allowFrom) {
|
|
92486
|
+
const card = modelUnavailableCardRegistry.take(String(chat_id), Date.now());
|
|
92487
|
+
if (decideAnnouncementDelivery(outcome.kind, card) === "edit" && card) {
|
|
92488
|
+
try {
|
|
92489
|
+
await bot.api.editMessageText(chat_id, card.messageId, richMessage2(foldAnnouncementIntoCard(card.text, outcome.announcement)), {});
|
|
92490
|
+
continue;
|
|
92491
|
+
} catch (err) {
|
|
92492
|
+
process.stderr.write(`telegram gateway: [fleet-fallback] card edit failed chat=${chat_id} \u2014 sending announcement separately: ${err?.message ?? err}
|
|
92493
|
+
`);
|
|
92494
|
+
}
|
|
92495
|
+
}
|
|
91027
92496
|
swallowingApiCall(() => bot.api.sendRichMessage(chat_id, richMessage2(outcome.announcement), opts), { chat_id, verb: "fleet-fallback:notify" });
|
|
91028
92497
|
}
|
|
91029
92498
|
if (outcome.kind === "switched") {
|
|
@@ -91144,6 +92613,38 @@ async function runQuotaWatch(opts = {}) {
|
|
|
91144
92613
|
`);
|
|
91145
92614
|
}
|
|
91146
92615
|
process.stderr.write(`telegram gateway: quota-watch: fleet all-exhausted ${fleetDecision.transition}
|
|
92616
|
+
`);
|
|
92617
|
+
}
|
|
92618
|
+
}
|
|
92619
|
+
{
|
|
92620
|
+
const rollPrev = watchState[FLEET_ROLL_ANNOUNCE_KEY] ?? emptyAccountState();
|
|
92621
|
+
const rollDecision = evaluateFleetRollAnnounce({
|
|
92622
|
+
roll: listStateData.last_fleet_roll ?? null,
|
|
92623
|
+
prev: rollPrev,
|
|
92624
|
+
now
|
|
92625
|
+
});
|
|
92626
|
+
if (rollDecision.kind === "notify") {
|
|
92627
|
+
for (const chat_id of access.allowFrom) {
|
|
92628
|
+
if (tuning.fleetDedup) {
|
|
92629
|
+
const granted = await claimQuotaNotification(brokerClient, buildQuotaClaimKey(FLEET_ROLL_ANNOUNCE_KEY, String(listStateData.last_fleet_roll.at), chat_id));
|
|
92630
|
+
if (!granted) {
|
|
92631
|
+
process.stderr.write(`telegram gateway: quota-watch: fleet-roll claim denied chat=${chat_id} \u2014 another agent notified
|
|
92632
|
+
`);
|
|
92633
|
+
continue;
|
|
92634
|
+
}
|
|
92635
|
+
}
|
|
92636
|
+
await swallowingApiCall(() => bot.api.sendRichMessage(chat_id, richMessage2(rollDecision.message), {
|
|
92637
|
+
disable_notification: true
|
|
92638
|
+
}), { chat_id, verb: "quota-watch.fleet-roll" });
|
|
92639
|
+
}
|
|
92640
|
+
watchState = patchQuotaWatchState(watchState, FLEET_ROLL_ANNOUNCE_KEY, rollDecision.newState);
|
|
92641
|
+
try {
|
|
92642
|
+
saveQuotaWatchState(stateDir, watchState);
|
|
92643
|
+
} catch (err) {
|
|
92644
|
+
process.stderr.write(`telegram gateway: quota-watch: fleet-roll state save failed: ${err}
|
|
92645
|
+
`);
|
|
92646
|
+
}
|
|
92647
|
+
process.stderr.write(`telegram gateway: quota-watch: announced broker fleet roll ${listStateData.last_fleet_roll.from} \u2192 ${listStateData.last_fleet_roll.to}
|
|
91147
92648
|
`);
|
|
91148
92649
|
}
|
|
91149
92650
|
}
|
|
@@ -91151,9 +92652,10 @@ async function runQuotaWatch(opts = {}) {
|
|
|
91151
92652
|
const labelToSnapIndex = new Map(snapshots.map((s, i) => [s.label, i]));
|
|
91152
92653
|
let reconciledCount = 0;
|
|
91153
92654
|
let mutatedState = watchState;
|
|
92655
|
+
const lastRoll = listStateData.last_fleet_roll ?? null;
|
|
91154
92656
|
for (const snap of snapshots) {
|
|
91155
92657
|
const prev = watchState[snap.label] ?? emptyAccountState();
|
|
91156
|
-
const decision = evaluateQuotaWatchAccount({ agentName: agentName3, snap, prev, now, bootTick, tuning });
|
|
92658
|
+
const decision = evaluateQuotaWatchAccount({ agentName: agentName3, snap, prev, now, bootTick, tuning, lastRoll });
|
|
91157
92659
|
if (decision.kind === "reconcile") {
|
|
91158
92660
|
mutatedState = patchQuotaWatchState(mutatedState, decision.accountLabel, decision.newAccountState);
|
|
91159
92661
|
reconciledCount++;
|
|
@@ -91209,7 +92711,7 @@ async function runQuotaWatch(opts = {}) {
|
|
|
91209
92711
|
if (isLiveCorroboration(freshEntry) && freshEntry.result.ok && snapIndex >= 0) {
|
|
91210
92712
|
const enrichedSnap = { ...snapshots[snapIndex], quota: freshEntry.result.data, capturedAtMs: undefined };
|
|
91211
92713
|
const prev = watchState[accountLabel] ?? emptyAccountState();
|
|
91212
|
-
const re = evaluateQuotaWatchAccount({ agentName: agentName3, snap: enrichedSnap, prev, now, bootTick, tuning });
|
|
92714
|
+
const re = evaluateQuotaWatchAccount({ agentName: agentName3, snap: enrichedSnap, prev, now, bootTick, tuning, lastRoll });
|
|
91213
92715
|
if (re.kind === "notify" && re.transition === decision.transition) {
|
|
91214
92716
|
enrichedDecision = re;
|
|
91215
92717
|
} else if (re.kind === "reconcile") {
|
|
@@ -92045,6 +93547,28 @@ bot.on("callback_query:data", async (ctx) => {
|
|
|
92045
93547
|
await ctx.answerCallbackQuery({ text: "Not authorized." });
|
|
92046
93548
|
return;
|
|
92047
93549
|
}
|
|
93550
|
+
const effLevel = data.startsWith("eff:s:") ? data.slice("eff:s:".length) : null;
|
|
93551
|
+
const effCardMsgId = ctx.callbackQuery?.message?.message_id;
|
|
93552
|
+
if (effLevel != null && currentTurn !== null && effCardMsgId != null) {
|
|
93553
|
+
await ctx.answerCallbackQuery({ text: "\uD83D\uDCE5 Queued \u2014 applies when the turn ends" }).catch(() => {});
|
|
93554
|
+
const effChatId = String(ctx.chat?.id ?? "");
|
|
93555
|
+
const effThreadId = resolveThreadId(effChatId, ctx.callbackQuery?.message?.message_thread_id);
|
|
93556
|
+
await ctx.editMessageText(richMessage2(hardenCardBreaks2(ackText("effort", effLevel, escapeHtmlForTg2))), {
|
|
93557
|
+
reply_markup: { inline_keyboard: [] }
|
|
93558
|
+
}).catch(() => {});
|
|
93559
|
+
enqueueSessionCommand({
|
|
93560
|
+
kind: "effort",
|
|
93561
|
+
origin: "menu",
|
|
93562
|
+
arg: data,
|
|
93563
|
+
targetLabel: effLevel,
|
|
93564
|
+
chatId: effChatId,
|
|
93565
|
+
threadId: effThreadId,
|
|
93566
|
+
ackChatId: effChatId,
|
|
93567
|
+
ackMessageId: effCardMsgId,
|
|
93568
|
+
requestedAt: Date.now()
|
|
93569
|
+
});
|
|
93570
|
+
return;
|
|
93571
|
+
}
|
|
92048
93572
|
await ctx.answerCallbackQuery({ text: "Setting effort\u2026" }).catch(() => {});
|
|
92049
93573
|
try {
|
|
92050
93574
|
const outcome = await handleEffortMenuCallback(data, buildEffortDeps());
|
|
@@ -92075,6 +93599,46 @@ bot.on("callback_query:data", async (ctx) => {
|
|
|
92075
93599
|
const cbThreadId = resolveThreadId(cbChatId, ctx.callbackQuery?.message?.message_thread_id);
|
|
92076
93600
|
const modelDeps = buildModelDeps({ chatId: cbChatId, threadId: cbThreadId });
|
|
92077
93601
|
if (modelDeps.isBusy()) {
|
|
93602
|
+
const cardMsgId = ctx.callbackQuery?.message?.message_id;
|
|
93603
|
+
let queueArg = null;
|
|
93604
|
+
let queueOrigin = "menu";
|
|
93605
|
+
let queueLabel = "the selected model";
|
|
93606
|
+
if (data.startsWith(MODEL_CALLBACK_SR)) {
|
|
93607
|
+
const srName = data.slice(MODEL_CALLBACK_SR.length);
|
|
93608
|
+
if (isValidModelArg(srName)) {
|
|
93609
|
+
queueArg = srName;
|
|
93610
|
+
queueOrigin = "typed";
|
|
93611
|
+
queueLabel = srFriendlyLabel(srName);
|
|
93612
|
+
}
|
|
93613
|
+
} else if (data.startsWith(MODEL_CALLBACK_ALIAS)) {
|
|
93614
|
+
const alias = data.slice(MODEL_CALLBACK_ALIAS.length);
|
|
93615
|
+
if (isValidModelArg(alias)) {
|
|
93616
|
+
queueArg = alias;
|
|
93617
|
+
queueOrigin = "typed";
|
|
93618
|
+
queueLabel = alias;
|
|
93619
|
+
}
|
|
93620
|
+
} else if (data.startsWith("mdl:s:")) {
|
|
93621
|
+
queueArg = data;
|
|
93622
|
+
queueOrigin = "menu";
|
|
93623
|
+
}
|
|
93624
|
+
if (queueArg != null && cardMsgId != null) {
|
|
93625
|
+
await ctx.answerCallbackQuery({ text: "\uD83D\uDCE5 Queued \u2014 applies when the turn ends" }).catch(() => {});
|
|
93626
|
+
await ctx.editMessageText(richMessage2(hardenCardBreaks2(ackText("model", queueLabel, escapeHtmlForTg2))), {
|
|
93627
|
+
reply_markup: { inline_keyboard: [] }
|
|
93628
|
+
}).catch(() => {});
|
|
93629
|
+
enqueueSessionCommand({
|
|
93630
|
+
kind: "model",
|
|
93631
|
+
origin: queueOrigin,
|
|
93632
|
+
arg: queueArg,
|
|
93633
|
+
targetLabel: queueLabel,
|
|
93634
|
+
chatId: cbChatId,
|
|
93635
|
+
threadId: cbThreadId,
|
|
93636
|
+
ackChatId: cbChatId,
|
|
93637
|
+
ackMessageId: cardMsgId,
|
|
93638
|
+
requestedAt: Date.now()
|
|
93639
|
+
});
|
|
93640
|
+
return;
|
|
93641
|
+
}
|
|
92078
93642
|
await ctx.answerCallbackQuery({ text: "\u23F3 Agent is mid-turn \u2014 tap again when it\u2019s idle", show_alert: false }).catch(() => {});
|
|
92079
93643
|
return;
|
|
92080
93644
|
}
|
|
@@ -92099,56 +93663,14 @@ bot.on("callback_query:data", async (ctx) => {
|
|
|
92099
93663
|
}
|
|
92100
93664
|
return;
|
|
92101
93665
|
}
|
|
92102
|
-
const didInterimSrEdit = false;
|
|
92103
93666
|
try {
|
|
92104
93667
|
const prevSessionModel = sessionModelSource.getOverride();
|
|
92105
93668
|
const outcome = await handleModelMenuCallback(data, modelDeps);
|
|
92106
|
-
if (outcome.
|
|
92107
|
-
sessionModelSource.setOverride(outcome.selectedModel);
|
|
92108
|
-
const smDir = resolveAgentDirFromEnv();
|
|
92109
|
-
if (smDir && outcome.selectedModelToken) {
|
|
92110
|
-
try {
|
|
92111
|
-
writeSessionModelFile(smDir, outcome.selectedModelToken, readConfiguredDefaultModel(smDir) ?? resolveMainModel(modelDeps.getConfiguredModel() ?? undefined));
|
|
92112
|
-
} catch (err) {
|
|
92113
|
-
outcome.reply.text += `
|
|
92114
|
-
\u26A0\uFE0F Couldn\u2019t persist the sticky override \u2014 the switch is live now but won\u2019t survive a relaunch.`;
|
|
92115
|
-
process.stderr.write(`telegram gateway: session-model persist failed (menu): ${err?.message ?? String(err)}
|
|
92116
|
-
`);
|
|
92117
|
-
}
|
|
92118
|
-
}
|
|
92119
|
-
}
|
|
92120
|
-
if (outcome.clearedDefault) {
|
|
92121
|
-
const smDir = resolveAgentDirFromEnv();
|
|
92122
|
-
if (smDir)
|
|
92123
|
-
clearSessionModelFile(smDir);
|
|
92124
|
-
}
|
|
92125
|
-
if (outcome.toastOnly && !didInterimSrEdit)
|
|
93669
|
+
if (outcome.toastOnly)
|
|
92126
93670
|
return;
|
|
92127
|
-
|
|
92128
|
-
|
|
92129
|
-
await ctx.editMessageText(richMessage2(
|
|
92130
|
-
{
|
|
92131
|
-
const agentDir = resolveAgentDirFromEnv();
|
|
92132
|
-
const token = outcome.selectedModelToken;
|
|
92133
|
-
if (agentDir && token) {
|
|
92134
|
-
try {
|
|
92135
|
-
writeSessionModelFile(agentDir, token, readConfiguredDefaultModel(agentDir) ?? resolveMainModel(modelDeps.getConfiguredModel() ?? undefined));
|
|
92136
|
-
sessionModelSource.setOverride(token);
|
|
92137
|
-
} catch (e) {
|
|
92138
|
-
process.stderr.write(`telegram gateway: sr-to-claude session-model write failed: ${e?.message ?? String(e)}
|
|
92139
|
-
`);
|
|
92140
|
-
}
|
|
92141
|
-
} else if (agentDir) {
|
|
92142
|
-
clearSessionModelFile(agentDir);
|
|
92143
|
-
}
|
|
92144
|
-
}
|
|
92145
|
-
writeRestartMarker({ chat_id: cbChatId, thread_id: cbThreadId ?? null, ack_message_id: null, ts: Date.now() });
|
|
92146
|
-
stampUserRestartReason("user: sr-to-claude model switch (menu)");
|
|
92147
|
-
if (turnInFlightForGate()) {
|
|
92148
|
-
pendingRestarts.set(agentName3, Date.now());
|
|
92149
|
-
} else {
|
|
92150
|
-
sweepBeforeSelfRestart().finally(() => triggerSelfRestart(agentName3, "sr-to-claude-model-switch", 1500));
|
|
92151
|
-
}
|
|
93671
|
+
const { restartNotice } = recordModelMenuSideEffects(outcome, modelDeps, cbChatId, cbThreadId, prevSessionModel);
|
|
93672
|
+
if (restartNotice) {
|
|
93673
|
+
await ctx.editMessageText(richMessage2(restartNotice), { reply_markup: { inline_keyboard: [] } }).catch(() => {});
|
|
92152
93674
|
return;
|
|
92153
93675
|
}
|
|
92154
93676
|
await ctx.editMessageText(richMessage2(outcome.reply.text), {
|
|
@@ -92365,7 +93887,7 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
|
|
|
92365
93887
|
let audio = null;
|
|
92366
93888
|
if (entry.filePath != null) {
|
|
92367
93889
|
try {
|
|
92368
|
-
audio =
|
|
93890
|
+
audio = readFileSync53(entry.filePath);
|
|
92369
93891
|
} catch {
|
|
92370
93892
|
audio = null;
|
|
92371
93893
|
}
|
|
@@ -92443,8 +93965,8 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
|
|
|
92443
93965
|
const cbMessageId = ctx.callbackQuery?.message?.message_id;
|
|
92444
93966
|
const metaForMessage = cbMessageId != null ? agentButtonMeta.get(`${cbChatId}:${cbMessageId}`) : undefined;
|
|
92445
93967
|
const tapMeta = metaForMessage?.get(agentCb.raw);
|
|
92446
|
-
const
|
|
92447
|
-
await ctx.answerCallbackQuery({ text:
|
|
93968
|
+
const ackText3 = typeof tapMeta?.ack_text === "string" && tapMeta.ack_text.length > 0 ? tapMeta.ack_text : "\u2713 received";
|
|
93969
|
+
await ctx.answerCallbackQuery({ text: ackText3 }).catch(() => {});
|
|
92448
93970
|
const buttonText = (() => {
|
|
92449
93971
|
const msg2 = ctx.callbackQuery?.message;
|
|
92450
93972
|
const kb = msg2 && "reply_markup" in msg2 ? msg2.reply_markup : undefined;
|
|
@@ -92596,7 +94118,7 @@ ${interimLabel}` : interimLabel
|
|
|
92596
94118
|
const unifiedDiff = (() => {
|
|
92597
94119
|
try {
|
|
92598
94120
|
const cfgPath = process.env.SWITCHROOM_CONFIG ?? SWITCHROOM_CONFIG ?? findConfigFile2();
|
|
92599
|
-
const raw =
|
|
94121
|
+
const raw = readFileSync53(cfgPath, "utf8");
|
|
92600
94122
|
return synthesizeAllowRuleDiff({ agentName: agentName3, rule: chosen.rule, configText: raw });
|
|
92601
94123
|
} catch (err) {
|
|
92602
94124
|
process.stderr.write(`telegram gateway: always-allow diff synth failed: ${err.message}
|
|
@@ -92732,7 +94254,7 @@ ${editLabel}` : editLabel), {
|
|
|
92732
94254
|
}
|
|
92733
94255
|
const scopedMins = Math.max(1, Math.round(scopedTtl / 60000));
|
|
92734
94256
|
const windowGranted = timeBox != null && grantAgent !== "";
|
|
92735
|
-
const
|
|
94257
|
+
const ackText2 = behavior === "deny" ? "\u274C Denied" : windowGranted ? `\u2705 Allowed (${scopedMins} min)` : "\u2705 Allowed once";
|
|
92736
94258
|
const htmlLabel = behavior === "deny" ? "\u274C **Denied**" : windowGranted ? `\u2705 **Allowed \u2014 won't ask again about ${escapeHtmlForTg2(timeBox.breadth)} for ${scopedMins} min**` : "\u2705 **Allowed once**";
|
|
92737
94259
|
const msg = ctx.callbackQuery?.message;
|
|
92738
94260
|
const baseText = msg && "text" in msg && msg.text ? escapeHtmlForTg2(msg.text) : "";
|
|
@@ -92745,7 +94267,7 @@ ${editLabel}` : editLabel), {
|
|
|
92745
94267
|
|
|
92746
94268
|
${resumeLine}` : htmlLabel;
|
|
92747
94269
|
await finalizeCallback2(ctx, {
|
|
92748
|
-
ackText:
|
|
94270
|
+
ackText: ackText2.slice(0, 200),
|
|
92749
94271
|
newText: baseText ? `${baseText}
|
|
92750
94272
|
|
|
92751
94273
|
${labelWithResume}` : labelWithResume,
|
|
@@ -92787,7 +94309,7 @@ bot.on("message:photo", async (ctx) => {
|
|
|
92787
94309
|
});
|
|
92788
94310
|
mkdirSync39(INBOX_DIR, { recursive: true, mode: 448 });
|
|
92789
94311
|
assertInsideInbox(INBOX_DIR, dlPath);
|
|
92790
|
-
|
|
94312
|
+
writeFileSync42(dlPath, buf, { mode: 384 });
|
|
92791
94313
|
return dlPath;
|
|
92792
94314
|
} catch (err) {
|
|
92793
94315
|
const msg = err instanceof Error ? err.message : "unknown error";
|
|
@@ -93487,6 +95009,7 @@ bot.catch((err) => {
|
|
|
93487
95009
|
`);
|
|
93488
95010
|
});
|
|
93489
95011
|
var SHUTDOWN_DRAIN_BUDGET_MS = 35000;
|
|
95012
|
+
var SHUTDOWN_PROGRESS_FLUSH_BUDGET_MS = 1500;
|
|
93490
95013
|
function countInFlight() {
|
|
93491
95014
|
return pendingPermissions.size + pendingVaultOps.size + inboundCoalescer.size() + pendingReauthFlows.size;
|
|
93492
95015
|
}
|
|
@@ -93498,6 +95021,7 @@ async function shutdown(signal) {
|
|
|
93498
95021
|
const agentName3 = process.env.SWITCHROOM_AGENT_NAME ?? "-";
|
|
93499
95022
|
process.stderr.write(`telegram gateway: shutting down
|
|
93500
95023
|
`);
|
|
95024
|
+
bridgeDeadWatchdog.stop();
|
|
93501
95025
|
const isOsSignal = signal === "SIGTERM" || signal === "SIGINT";
|
|
93502
95026
|
if (isOsSignal) {
|
|
93503
95027
|
try {
|
|
@@ -93508,12 +95032,43 @@ async function shutdown(signal) {
|
|
|
93508
95032
|
`);
|
|
93509
95033
|
} catch (err) {
|
|
93510
95034
|
process.stderr.write(`telegram gateway: shutdown.clean_marker_write_failed err=${err.message}
|
|
95035
|
+
`);
|
|
95036
|
+
}
|
|
95037
|
+
try {
|
|
95038
|
+
const smDir = resolveAgentDirFromEnv();
|
|
95039
|
+
if (smDir != null) {
|
|
95040
|
+
const hasOverride = readSessionModelFile(smDir) != null;
|
|
95041
|
+
const intentAlreadyStamped = existsSync50(join53(smDir, RELAUNCH_MODEL_INTENT_FILE));
|
|
95042
|
+
if (hasOverride && !intentAlreadyStamped) {
|
|
95043
|
+
writeRelaunchModelIntent(smDir, "keep", `${GATEWAY_SHUTDOWN_INTENT_REASON_PREFIX} graceful ${signal} shutdown (deploy/rolling restart) \u2014 preserving user-chosen session model`);
|
|
95044
|
+
process.stderr.write(`telegram gateway: shutdown.session_model_keep_stamped signal=${signal}
|
|
95045
|
+
`);
|
|
95046
|
+
}
|
|
95047
|
+
}
|
|
95048
|
+
} catch (err) {
|
|
95049
|
+
process.stderr.write(`telegram gateway: shutdown.session_model_keep_stamp_failed err=${err.message}
|
|
93511
95050
|
`);
|
|
93512
95051
|
}
|
|
93513
95052
|
} else {
|
|
93514
95053
|
process.stderr.write(`telegram gateway: shutdown.clean_marker_skipped signal=${signal} (crash path \u2014 banner will fire on next boot)
|
|
93515
95054
|
`);
|
|
93516
95055
|
}
|
|
95056
|
+
const orphanedCmdActions = shutdownResolutionActions(pendingSessionCommand, escapeHtmlForTg2);
|
|
95057
|
+
const orphanedCmdEdits = orphanedCmdActions.map((a) => ({
|
|
95058
|
+
chatId: a.cmd.ackChatId,
|
|
95059
|
+
messageId: a.cmd.ackMessageId,
|
|
95060
|
+
text: persistQueuedCommandForRestart(a)
|
|
95061
|
+
}));
|
|
95062
|
+
if (orphanedCmdEdits.length > 0) {
|
|
95063
|
+
process.stderr.write(`telegram gateway: shutdown.pending_cmd_resolved count=${orphanedCmdEdits.length}
|
|
95064
|
+
`);
|
|
95065
|
+
await Promise.race([
|
|
95066
|
+
Promise.allSettled(orphanedCmdEdits.map((e) => editPendingCommandCard(e.chatId, e.messageId, e.text))),
|
|
95067
|
+
new Promise((resolve11) => {
|
|
95068
|
+
setTimeout(resolve11, SHUTDOWN_PROGRESS_FLUSH_BUDGET_MS).unref();
|
|
95069
|
+
})
|
|
95070
|
+
]);
|
|
95071
|
+
}
|
|
93517
95072
|
const shutdownTurn = currentTurn;
|
|
93518
95073
|
if (turnsDb != null && shutdownTurn?.registryKey != null) {
|
|
93519
95074
|
const wasScheduledRestart = pendingRestarts.size > 0;
|
|
@@ -93927,9 +95482,9 @@ var didOneTimeSetup = false;
|
|
|
93927
95482
|
const smAgentDir = resolveAgentDirFromEnv();
|
|
93928
95483
|
if (smAgentDir) {
|
|
93929
95484
|
const activePath = join53(smAgentDir, ".active-session-model");
|
|
93930
|
-
if (
|
|
95485
|
+
if (existsSync50(activePath)) {
|
|
93931
95486
|
try {
|
|
93932
|
-
const launched =
|
|
95487
|
+
const launched = readFileSync53(activePath, "utf8").trim();
|
|
93933
95488
|
const configured = (() => {
|
|
93934
95489
|
const d = switchroomExecJson(["agent", "list"]);
|
|
93935
95490
|
const raw = d?.agents?.find((a) => a.name === getMyAgentName())?.model ?? null;
|
|
@@ -93939,15 +95494,15 @@ var didOneTimeSetup = false;
|
|
|
93939
95494
|
} catch {}
|
|
93940
95495
|
}
|
|
93941
95496
|
const alertPath = join53(smAgentDir, ".session-model-alert");
|
|
93942
|
-
if (
|
|
95497
|
+
if (existsSync50(alertPath)) {
|
|
93943
95498
|
let alertText = null;
|
|
93944
95499
|
try {
|
|
93945
|
-
alertText =
|
|
95500
|
+
alertText = readFileSync53(alertPath, "utf8").trim();
|
|
93946
95501
|
} catch {
|
|
93947
95502
|
alertText = null;
|
|
93948
95503
|
}
|
|
93949
95504
|
try {
|
|
93950
|
-
|
|
95505
|
+
unlinkSync21(alertPath);
|
|
93951
95506
|
} catch {}
|
|
93952
95507
|
if (alertText && alertText.length > 0) {
|
|
93953
95508
|
const operators = loadAccess().allowFrom;
|
|
@@ -94067,6 +95622,20 @@ var didOneTimeSetup = false;
|
|
|
94067
95622
|
parentStateDir: STATE_DIR,
|
|
94068
95623
|
log: (msg) => process.stderr.write(`telegram gateway: ${msg}
|
|
94069
95624
|
`),
|
|
95625
|
+
onResurrect: (agentId, _description) => {
|
|
95626
|
+
try {
|
|
95627
|
+
workerActivityFeed?.resurrect(agentId);
|
|
95628
|
+
} catch (err) {
|
|
95629
|
+
process.stderr.write(`telegram gateway: worker resurrect error agent=${agentId}: ${err.message}
|
|
95630
|
+
`);
|
|
95631
|
+
}
|
|
95632
|
+
process.stderr.write(`telegram gateway: worker ${agentId} card RESURRECTED \u2014 false terminal finish reversed, worker resumed (issue #3023)
|
|
95633
|
+
`);
|
|
95634
|
+
},
|
|
95635
|
+
onWorkerLost: (agentId, _description) => {
|
|
95636
|
+
process.stderr.write(`telegram gateway: worker ${agentId} NAMED AS LOST \u2014 falsely finalised twice, resurrection chain bound reached (issue #3023)
|
|
95637
|
+
`);
|
|
95638
|
+
},
|
|
94070
95639
|
onFinish: ({ agentId, outcome, description, resultText, toolCount, durationMs, background: entryBackground }) => {
|
|
94071
95640
|
deferredDoneReactions.promote();
|
|
94072
95641
|
let fleetChatId = "";
|