switchroom 0.18.9 → 0.18.10
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 +28 -4
- package/dist/host-control/main.js +3 -2
- package/dist/vault/approvals/kernel-server.js +2 -1
- package/dist/vault/broker/server.js +2 -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 +1831 -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/;
|
|
@@ -68608,7 +68905,7 @@ import_handlebars.default.registerHelper("isNumber", (value) => {
|
|
|
68608
68905
|
return typeof value === "number" && Number.isFinite(value);
|
|
68609
68906
|
});
|
|
68610
68907
|
var SHARED_FRAGMENTS_DIR = resolve5(PROFILES_ROOT, "_shared");
|
|
68611
|
-
var SHARED_FRAGMENTS = ["vault-protocol", "agent-self-service", "execution-discipline", "reply-discipline"];
|
|
68908
|
+
var SHARED_FRAGMENTS = ["vault-protocol", "agent-self-service", "execution-discipline", "reply-discipline", "dev-protocol"];
|
|
68612
68909
|
for (const name of SHARED_FRAGMENTS) {
|
|
68613
68910
|
const fragPath = join30(SHARED_FRAGMENTS_DIR, `${name}.md.hbs`);
|
|
68614
68911
|
if (existsSync26(fragPath)) {
|
|
@@ -68662,6 +68959,7 @@ init_paths();
|
|
|
68662
68959
|
// ../src/agents/scaffold.ts
|
|
68663
68960
|
var REPO_ROOT = resolve6(import.meta.dirname, "../..");
|
|
68664
68961
|
var SWITCHROOM_DEFAULT_MAIN_MODEL = "claude-sonnet-5";
|
|
68962
|
+
var SWITCHROOM_DEFAULT_THINKING_EFFORT = "low";
|
|
68665
68963
|
function resolveMainModel(model) {
|
|
68666
68964
|
if (model === undefined || model === "default")
|
|
68667
68965
|
return SWITCHROOM_DEFAULT_MAIN_MODEL;
|
|
@@ -68696,31 +68994,56 @@ function parseEffortCommand(text4) {
|
|
|
68696
68994
|
const arg = parts[0];
|
|
68697
68995
|
if (arg.toLowerCase() === "help")
|
|
68698
68996
|
return { kind: "help" };
|
|
68997
|
+
if (arg.toLowerCase() === "default")
|
|
68998
|
+
return { kind: "default" };
|
|
68699
68999
|
if (!isValidEffortArg(arg)) {
|
|
68700
69000
|
return { kind: "help", reason: `not a valid effort level: ${arg}` };
|
|
68701
69001
|
}
|
|
68702
69002
|
return { kind: "set", level: arg.toLowerCase() };
|
|
68703
69003
|
}
|
|
68704
|
-
var PERSIST_NOTE2 = "
|
|
69004
|
+
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
69005
|
var LEVELS_INLINE = EFFORT_LEVELS.map((l) => `\`${l}\``).join(" \u00b7 ");
|
|
68706
69006
|
function helpText3(deps, reason) {
|
|
68707
69007
|
const lines = [];
|
|
68708
69008
|
if (reason)
|
|
68709
69009
|
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);
|
|
69010
|
+
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
69011
|
return { text: lines.join(`
|
|
68712
69012
|
`), html: true };
|
|
68713
69013
|
}
|
|
68714
69014
|
async function handleEffortCommand(parsed, deps) {
|
|
68715
69015
|
if (parsed.kind === "help")
|
|
68716
69016
|
return helpText3(deps, parsed.reason);
|
|
69017
|
+
if (parsed.kind === "default") {
|
|
69018
|
+
const configured = deps.getConfiguredEffort();
|
|
69019
|
+
const restoreLevel = configured && isValidEffortArg(configured) ? configured.toLowerCase() : "low";
|
|
69020
|
+
let restored = null;
|
|
69021
|
+
try {
|
|
69022
|
+
restored = await deps.applyEffort(deps.getAgentName(), restoreLevel);
|
|
69023
|
+
} catch {
|
|
69024
|
+
restored = null;
|
|
69025
|
+
}
|
|
69026
|
+
deps.clearSessionEffort?.();
|
|
69027
|
+
const cfgHtml = `\`${deps.escapeHtml(restoreLevel)}\``;
|
|
69028
|
+
if (restored?.ok) {
|
|
69029
|
+
return {
|
|
69030
|
+
text: `\u2705 Effort override cleared \u2014 back on the configured default ${cfgHtml}. It will also boot on ${cfgHtml} from now on.`,
|
|
69031
|
+
html: true
|
|
69032
|
+
};
|
|
69033
|
+
}
|
|
69034
|
+
return {
|
|
69035
|
+
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.)`,
|
|
69036
|
+
html: true
|
|
69037
|
+
};
|
|
69038
|
+
}
|
|
68717
69039
|
if (parsed.kind === "show") {
|
|
68718
69040
|
const configured = deps.getConfiguredEffort();
|
|
68719
69041
|
const shown = configured && configured.length > 0 ? configured : "low";
|
|
69042
|
+
const override = deps.getSessionEffort?.() ?? null;
|
|
68720
69043
|
return {
|
|
68721
69044
|
text: [
|
|
68722
69045
|
`**Effort \u2014 ${deps.escapeHtml(deps.getAgentName())}**`,
|
|
68723
|
-
`Configured default: \`${deps.escapeHtml(shown)}
|
|
69046
|
+
`Configured default: \`${deps.escapeHtml(shown)}\`${override ? ` \u00b7 session override: \`${deps.escapeHtml(override)}\`` : ""}`,
|
|
68724
69047
|
`Switch the live session: ${EFFORT_LEVELS.map((l) => `\`/effort ${l}\``).join(" \u00b7 ")}`,
|
|
68725
69048
|
PERSIST_NOTE2
|
|
68726
69049
|
].join(`
|
|
@@ -68776,7 +69099,7 @@ function menuKeyboard(highlight) {
|
|
|
68776
69099
|
}
|
|
68777
69100
|
function buildEffortMenu(deps, highlight) {
|
|
68778
69101
|
const configured = deps.getConfiguredEffort() || "low";
|
|
68779
|
-
const live = highlight ?? configured;
|
|
69102
|
+
const live = highlight ?? deps.getSessionEffort?.() ?? configured;
|
|
68780
69103
|
return {
|
|
68781
69104
|
text: [
|
|
68782
69105
|
`**Effort \u2014 ${deps.escapeHtml(deps.getAgentName())}**`,
|
|
@@ -68823,6 +69146,122 @@ ${menu.text}` },
|
|
|
68823
69146
|
};
|
|
68824
69147
|
}
|
|
68825
69148
|
|
|
69149
|
+
// gateway/pending-session-command.ts
|
|
69150
|
+
function createPendingSessionCommandSlots() {
|
|
69151
|
+
const slots = new Map;
|
|
69152
|
+
return {
|
|
69153
|
+
get: (kind) => slots.get(kind) ?? null,
|
|
69154
|
+
set(cmd) {
|
|
69155
|
+
const displaced = slots.get(cmd.kind) ?? null;
|
|
69156
|
+
slots.delete(cmd.kind);
|
|
69157
|
+
slots.set(cmd.kind, cmd);
|
|
69158
|
+
return displaced;
|
|
69159
|
+
},
|
|
69160
|
+
take(kind) {
|
|
69161
|
+
const cur = slots.get(kind) ?? null;
|
|
69162
|
+
slots.delete(kind);
|
|
69163
|
+
return cur;
|
|
69164
|
+
},
|
|
69165
|
+
takeAll() {
|
|
69166
|
+
const all2 = [...slots.values()];
|
|
69167
|
+
slots.clear();
|
|
69168
|
+
return all2;
|
|
69169
|
+
},
|
|
69170
|
+
list: () => [...slots.values()],
|
|
69171
|
+
clear() {
|
|
69172
|
+
slots.clear();
|
|
69173
|
+
},
|
|
69174
|
+
get size() {
|
|
69175
|
+
return slots.size;
|
|
69176
|
+
}
|
|
69177
|
+
};
|
|
69178
|
+
}
|
|
69179
|
+
async function drainTakenCommands(taken, io) {
|
|
69180
|
+
for (let i = 0;i < taken.length; i++) {
|
|
69181
|
+
const cmd = taken[i];
|
|
69182
|
+
if (io.restartPending()) {
|
|
69183
|
+
await io.editCard(cmd, io.resolveForRestartText(cmd));
|
|
69184
|
+
continue;
|
|
69185
|
+
}
|
|
69186
|
+
if (io.turnInFlight()) {
|
|
69187
|
+
for (const rest of taken.slice(i))
|
|
69188
|
+
io.reEnqueue(rest);
|
|
69189
|
+
return;
|
|
69190
|
+
}
|
|
69191
|
+
let body;
|
|
69192
|
+
try {
|
|
69193
|
+
body = await io.apply(cmd);
|
|
69194
|
+
} catch (err) {
|
|
69195
|
+
await io.editCard(cmd, io.failureText(cmd, err));
|
|
69196
|
+
continue;
|
|
69197
|
+
}
|
|
69198
|
+
if (io.isBusyRefusal(body)) {
|
|
69199
|
+
for (const rest of taken.slice(i))
|
|
69200
|
+
io.reEnqueue(rest);
|
|
69201
|
+
return;
|
|
69202
|
+
}
|
|
69203
|
+
await io.editCard(cmd, body);
|
|
69204
|
+
}
|
|
69205
|
+
}
|
|
69206
|
+
function drainCapDecision(queued, now, capMs, turnInFlight) {
|
|
69207
|
+
const overdue = queued.some((c) => now - c.requestedAt > capMs);
|
|
69208
|
+
if (!overdue)
|
|
69209
|
+
return "wait";
|
|
69210
|
+
return turnInFlight ? "defer-turn-in-flight" : "force";
|
|
69211
|
+
}
|
|
69212
|
+
var EFFORT_MENU_SELECT_PREFIX = "eff:s:";
|
|
69213
|
+
function classifyShutdownPersist(cmd) {
|
|
69214
|
+
if (cmd.kind === "effort") {
|
|
69215
|
+
const level = cmd.origin === "menu" && cmd.arg.startsWith(EFFORT_MENU_SELECT_PREFIX) ? cmd.arg.slice(EFFORT_MENU_SELECT_PREFIX.length) : cmd.arg;
|
|
69216
|
+
if (level === "default")
|
|
69217
|
+
return { persist: "clear-effort", arg: level };
|
|
69218
|
+
return { persist: "effort", arg: level };
|
|
69219
|
+
}
|
|
69220
|
+
if (cmd.origin === "menu")
|
|
69221
|
+
return { persist: null, arg: cmd.arg };
|
|
69222
|
+
if (cmd.arg === "default")
|
|
69223
|
+
return { persist: "clear-model", arg: cmd.arg };
|
|
69224
|
+
return { persist: "model", arg: cmd.arg };
|
|
69225
|
+
}
|
|
69226
|
+
function persistedText(cmd, escapeHtml5) {
|
|
69227
|
+
const noun = KIND_NOUN[cmd.kind];
|
|
69228
|
+
const isClear = cmd.arg === "default" || cmd.targetLabel === "default";
|
|
69229
|
+
if (isClear) {
|
|
69230
|
+
return `\uD83D\uDCBE The session is restarting \u2014 your ${noun} override was cleared as requested; the agent boots on the configured default.`;
|
|
69231
|
+
}
|
|
69232
|
+
return `\uD83D\uDCBE The session is restarting \u2014 your \`${escapeHtml5(cmd.targetLabel)}\` ${noun} choice is saved and applies as the agent boots.`;
|
|
69233
|
+
}
|
|
69234
|
+
function shutdownResolutionActions(slots, escapeHtml5) {
|
|
69235
|
+
return slots.takeAll().map((cmd) => resolveForRestart(cmd, escapeHtml5));
|
|
69236
|
+
}
|
|
69237
|
+
function resolveForRestart(cmd, escapeHtml5) {
|
|
69238
|
+
const { persist, arg } = classifyShutdownPersist(cmd);
|
|
69239
|
+
return {
|
|
69240
|
+
cmd,
|
|
69241
|
+
persist,
|
|
69242
|
+
arg,
|
|
69243
|
+
persistedText: persistedText(cmd, escapeHtml5),
|
|
69244
|
+
reissueText: restartSupersededText(cmd, escapeHtml5)
|
|
69245
|
+
};
|
|
69246
|
+
}
|
|
69247
|
+
var KIND_NOUN = {
|
|
69248
|
+
model: "model",
|
|
69249
|
+
effort: "effort"
|
|
69250
|
+
};
|
|
69251
|
+
var KIND_VERB = {
|
|
69252
|
+
model: "switch to",
|
|
69253
|
+
effort: "set effort to"
|
|
69254
|
+
};
|
|
69255
|
+
function ackText(kind, targetLabel, escapeHtml5) {
|
|
69256
|
+
return `\uD83D\uDCE5 Agent is mid-turn \u2014 I'll ${KIND_VERB[kind]} \`${escapeHtml5(targetLabel)}\` the moment this turn finishes.`;
|
|
69257
|
+
}
|
|
69258
|
+
function supersededText(displaced, next, escapeHtml5) {
|
|
69259
|
+
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).`;
|
|
69260
|
+
}
|
|
69261
|
+
function restartSupersededText(cmd, escapeHtml5) {
|
|
69262
|
+
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.`;
|
|
69263
|
+
}
|
|
69264
|
+
|
|
68826
69265
|
// welcome-text.ts
|
|
68827
69266
|
init_demo_mask();
|
|
68828
69267
|
init_card_format();
|
|
@@ -68866,11 +69305,12 @@ function switchroomHelpText(agentName3) {
|
|
|
68866
69305
|
`\`/deny [id]\` \u2014 deny pending tool permission`,
|
|
68867
69306
|
`\`/pending\` \u2014 list pending permission prompts`,
|
|
68868
69307
|
`\`/interrupt [name]\` \u2014 interrupt an agent turn`,
|
|
69308
|
+
`\`/stop\` \u2014 cancel my in-flight turn (bare "stop" works too)`,
|
|
68869
69309
|
``,
|
|
68870
69310
|
`**Agents**`,
|
|
68871
69311
|
`\`/agents\` \u2014 list all agents`,
|
|
68872
69312
|
`\`/agentstart [name]\` \u2014 start an agent`,
|
|
68873
|
-
`\`/
|
|
69313
|
+
`\`/agentstop [name]\` \u2014 stop an agent's container`,
|
|
68874
69314
|
`\`/logs [name] [lines]\` \u2014 show agent logs`,
|
|
68875
69315
|
`\`/memory <query>\` \u2014 search agent memory`,
|
|
68876
69316
|
``,
|
|
@@ -74127,10 +74567,10 @@ function skillProposalKeyboard(id) {
|
|
|
74127
74567
|
|
|
74128
74568
|
// ../src/self-improve/skill-proposals.ts
|
|
74129
74569
|
import {
|
|
74130
|
-
closeSync as
|
|
74570
|
+
closeSync as closeSync5,
|
|
74131
74571
|
existsSync as existsSync33,
|
|
74132
74572
|
mkdirSync as mkdirSync26,
|
|
74133
|
-
openSync as
|
|
74573
|
+
openSync as openSync5,
|
|
74134
74574
|
readFileSync as readFileSync30,
|
|
74135
74575
|
writeSync as writeSync4
|
|
74136
74576
|
} from "node:fs";
|
|
@@ -74152,12 +74592,12 @@ function ensureDir3(stateDir) {
|
|
|
74152
74592
|
}
|
|
74153
74593
|
}
|
|
74154
74594
|
function appendLine2(path2, obj) {
|
|
74155
|
-
const fd =
|
|
74595
|
+
const fd = openSync5(path2, "a");
|
|
74156
74596
|
try {
|
|
74157
74597
|
writeSync4(fd, JSON.stringify(obj) + `
|
|
74158
74598
|
`);
|
|
74159
74599
|
} finally {
|
|
74160
|
-
|
|
74600
|
+
closeSync5(fd);
|
|
74161
74601
|
}
|
|
74162
74602
|
}
|
|
74163
74603
|
function readLines2(path2, isValid2) {
|
|
@@ -75582,7 +76022,7 @@ var ADMIN_COMMAND_NAMES = new Set([
|
|
|
75582
76022
|
"agents",
|
|
75583
76023
|
"logs",
|
|
75584
76024
|
"restart",
|
|
75585
|
-
"
|
|
76025
|
+
"agentstop",
|
|
75586
76026
|
"agentstart",
|
|
75587
76027
|
"update",
|
|
75588
76028
|
"reconcile",
|
|
@@ -75629,10 +76069,10 @@ function classifyAdminGate(text4, myAgentName) {
|
|
|
75629
76069
|
// subagent-watcher.ts
|
|
75630
76070
|
import {
|
|
75631
76071
|
existsSync as existsSync35,
|
|
75632
|
-
openSync as
|
|
75633
|
-
readSync,
|
|
76072
|
+
openSync as openSync7,
|
|
76073
|
+
readSync as readSync2,
|
|
75634
76074
|
statSync as statSync10,
|
|
75635
|
-
closeSync as
|
|
76075
|
+
closeSync as closeSync7,
|
|
75636
76076
|
watch,
|
|
75637
76077
|
readdirSync as readdirSync6,
|
|
75638
76078
|
readFileSync as readFileSync36
|
|
@@ -76056,12 +76496,13 @@ function recordSubagentStall(db2, args) {
|
|
|
76056
76496
|
}
|
|
76057
76497
|
function reapStuckRunningRows(db2, args) {
|
|
76058
76498
|
const cutoff = args.now - args.ttlMs;
|
|
76059
|
-
const
|
|
76060
|
-
SELECT id FROM subagents
|
|
76499
|
+
const rows = db2.prepare(`
|
|
76500
|
+
SELECT id, jsonl_agent_id FROM subagents
|
|
76061
76501
|
WHERE status = 'running'
|
|
76062
76502
|
AND background = 1
|
|
76063
76503
|
AND COALESCE(last_activity_at, started_at) < ?
|
|
76064
76504
|
`).all(cutoff);
|
|
76505
|
+
const candidates = args.isLive ? rows.filter((r) => !args.isLive(r.jsonl_agent_id)) : rows;
|
|
76065
76506
|
for (const row of candidates) {
|
|
76066
76507
|
recordSubagentStall(db2, {
|
|
76067
76508
|
id: row.id,
|
|
@@ -76116,10 +76557,10 @@ function recordNestedSubagentDispatch(db2, args) {
|
|
|
76116
76557
|
|
|
76117
76558
|
// gateway/turn-active-marker.ts
|
|
76118
76559
|
import {
|
|
76119
|
-
closeSync as
|
|
76560
|
+
closeSync as closeSync6,
|
|
76120
76561
|
existsSync as existsSync34,
|
|
76121
76562
|
mkdirSync as mkdirSync27,
|
|
76122
|
-
openSync as
|
|
76563
|
+
openSync as openSync6,
|
|
76123
76564
|
readFileSync as readFileSync35,
|
|
76124
76565
|
statSync as statSync9,
|
|
76125
76566
|
unlinkSync as unlinkSync15,
|
|
@@ -76137,8 +76578,8 @@ function touchTurnActiveMarker(stateDir) {
|
|
|
76137
76578
|
utimesSync(path2, now, now);
|
|
76138
76579
|
} catch {
|
|
76139
76580
|
try {
|
|
76140
|
-
const fd =
|
|
76141
|
-
|
|
76581
|
+
const fd = openSync6(path2, "r+");
|
|
76582
|
+
closeSync6(fd);
|
|
76142
76583
|
} catch {}
|
|
76143
76584
|
}
|
|
76144
76585
|
}
|
|
@@ -76148,7 +76589,10 @@ var DEFAULT_RESCAN_MS = 1000;
|
|
|
76148
76589
|
var DEFAULT_STALL_THRESHOLD_MS = 60000;
|
|
76149
76590
|
var DEFAULT_SILENT_SYNTHESIS_STALL_THRESHOLD_MS = 300000;
|
|
76150
76591
|
var DEFAULT_SILENT_STALL_TERMINAL_MS = 300000;
|
|
76592
|
+
var DEFAULT_INFLIGHT_TERMINAL_CAP_MS = 45 * 60000;
|
|
76151
76593
|
var LONG_RUNNING_TOOLS = new Set(["Bash"]);
|
|
76594
|
+
var MAX_RESURRECTIONS = 1;
|
|
76595
|
+
var FALSE_FINISH_TRACKER_CAP = 512;
|
|
76152
76596
|
function isLongRunningTool(name) {
|
|
76153
76597
|
return name != null && LONG_RUNNING_TOOLS.has(name);
|
|
76154
76598
|
}
|
|
@@ -76403,6 +76847,9 @@ function readSubTail(entry, tail, now, onDescriptionUpdate, fs2, log, db2, paren
|
|
|
76403
76847
|
entry.lastReplyText = ev.input.text;
|
|
76404
76848
|
}
|
|
76405
76849
|
entry.toolCount++;
|
|
76850
|
+
if (ev.toolUseId != null && ev.toolUseId !== "") {
|
|
76851
|
+
entry.inflightToolUseIds.add(ev.toolUseId);
|
|
76852
|
+
}
|
|
76406
76853
|
entry.lastTool = {
|
|
76407
76854
|
name: ev.toolName,
|
|
76408
76855
|
sanitisedArg: sanitiseToolArg(ev.toolName, ev.input ?? {})
|
|
@@ -76431,6 +76878,9 @@ function readSubTail(entry, tail, now, onDescriptionUpdate, fs2, log, db2, paren
|
|
|
76431
76878
|
}
|
|
76432
76879
|
}
|
|
76433
76880
|
} else if (ev.kind === "sub_agent_nested_spawn") {
|
|
76881
|
+
if (ev.toolUseId != null && ev.toolUseId.length > 0) {
|
|
76882
|
+
entry.inflightToolUseIds.add(ev.toolUseId);
|
|
76883
|
+
}
|
|
76434
76884
|
if (db2 != null && ev.toolUseId != null && ev.toolUseId.length > 0) {
|
|
76435
76885
|
try {
|
|
76436
76886
|
const nestedInput = ev.input ?? {};
|
|
@@ -76454,7 +76904,12 @@ function readSubTail(entry, tail, now, onDescriptionUpdate, fs2, log, db2, paren
|
|
|
76454
76904
|
fireNarrativeProgress();
|
|
76455
76905
|
}
|
|
76456
76906
|
entry.pendingNarrative = { text: ev.text };
|
|
76907
|
+
} else if (ev.kind === "sub_agent_tool_result") {
|
|
76908
|
+
if (ev.toolUseId != null && ev.toolUseId !== "") {
|
|
76909
|
+
entry.inflightToolUseIds.delete(ev.toolUseId);
|
|
76910
|
+
}
|
|
76457
76911
|
} else if (ev.kind === "sub_agent_turn_end") {
|
|
76912
|
+
entry.inflightToolUseIds.clear();
|
|
76458
76913
|
resolvePendingSubNarrative(null, undefined);
|
|
76459
76914
|
if (entry.state === "running") {
|
|
76460
76915
|
entry.state = "done";
|
|
@@ -76495,6 +76950,7 @@ function startSubagentWatcher(config) {
|
|
|
76495
76950
|
const stallThresholdMs = config.stallThresholdMs ?? parseEnvMs("SWITCHROOM_SUBAGENT_STALL_MS") ?? DEFAULT_STALL_THRESHOLD_MS;
|
|
76496
76951
|
const silentSynthesisStallThresholdMs = config.silentSynthesisStallThresholdMs ?? parseEnvMs("SWITCHROOM_SUBAGENT_SILENT_SYNTH_STALL_MS") ?? DEFAULT_SILENT_SYNTHESIS_STALL_THRESHOLD_MS;
|
|
76497
76952
|
const silentStallTerminalMs = config.silentStallTerminalMs ?? parseEnvMs("SWITCHROOM_SUBAGENT_STALL_TERMINAL_MS") ?? DEFAULT_SILENT_STALL_TERMINAL_MS;
|
|
76953
|
+
const inflightTerminalCapMs = config.inflightTerminalCapMs ?? parseEnvMs("SWITCHROOM_SUBAGENT_INFLIGHT_TERMINAL_CAP_MS") ?? DEFAULT_INFLIGHT_TERMINAL_CAP_MS;
|
|
76498
76954
|
const inflightPromoteMaxAgeMs = config.inflightPromoteMaxAgeMs ?? parseEnvMs("SWITCHROOM_SUBAGENT_INFLIGHT_MAX_AGE_MS") ?? DEFAULT_INFLIGHT_PROMOTE_MAX_AGE_MS;
|
|
76499
76955
|
const terminatedAgentIdsCap = config.terminatedAgentIdsCap ?? TERMINATED_AGENT_IDS_CAP;
|
|
76500
76956
|
const bootPromoteEnabled = config.bootPromoteEnabled ?? process.env.SWITCHROOM_SUBAGENT_BOOT_PROMOTE !== "0";
|
|
@@ -76523,9 +76979,9 @@ function startSubagentWatcher(config) {
|
|
|
76523
76979
|
existsSync: existsSync35,
|
|
76524
76980
|
readdirSync: readdirSync6,
|
|
76525
76981
|
statSync: statSync10,
|
|
76526
|
-
openSync:
|
|
76527
|
-
closeSync:
|
|
76528
|
-
readSync,
|
|
76982
|
+
openSync: openSync7,
|
|
76983
|
+
closeSync: closeSync7,
|
|
76984
|
+
readSync: readSync2,
|
|
76529
76985
|
watch
|
|
76530
76986
|
};
|
|
76531
76987
|
const registry = new Map;
|
|
@@ -76535,6 +76991,7 @@ function startSubagentWatcher(config) {
|
|
|
76535
76991
|
const pendingCloses = new Map;
|
|
76536
76992
|
const historicalFiles = new Set;
|
|
76537
76993
|
const terminatedAgentIds = new Set;
|
|
76994
|
+
const falseFinishTracker = new Map;
|
|
76538
76995
|
let bootScanInProgress = true;
|
|
76539
76996
|
let stopped = false;
|
|
76540
76997
|
function registerAgent(filePath, agentId) {
|
|
@@ -76559,6 +77016,7 @@ function startSubagentWatcher(config) {
|
|
|
76559
77016
|
lastResultText: "",
|
|
76560
77017
|
lastProgressBucketIdx: null,
|
|
76561
77018
|
lastTool: null,
|
|
77019
|
+
inflightToolUseIds: new Set,
|
|
76562
77020
|
historical: isHistorical
|
|
76563
77021
|
};
|
|
76564
77022
|
registry.set(agentId, entry);
|
|
@@ -76739,6 +77197,93 @@ function startSubagentWatcher(config) {
|
|
|
76739
77197
|
terminatedAgentIds.add(agentId);
|
|
76740
77198
|
log?.(`subagent-watcher: cleaned up terminal agent ${agentId}`);
|
|
76741
77199
|
}
|
|
77200
|
+
function recordFalseFinish(agentId, filePath, n) {
|
|
77201
|
+
let size = 0;
|
|
77202
|
+
try {
|
|
77203
|
+
size = fs2.statSync(filePath).size;
|
|
77204
|
+
} catch {}
|
|
77205
|
+
const prior = falseFinishTracker.get(agentId);
|
|
77206
|
+
if (prior == null && falseFinishTracker.size >= FALSE_FINISH_TRACKER_CAP) {
|
|
77207
|
+
const oldest = falseFinishTracker.keys().next().value;
|
|
77208
|
+
if (oldest != null)
|
|
77209
|
+
falseFinishTracker.delete(oldest);
|
|
77210
|
+
}
|
|
77211
|
+
falseFinishTracker.set(agentId, {
|
|
77212
|
+
filePath,
|
|
77213
|
+
synthesisedAt: n,
|
|
77214
|
+
sizeAtSynthesis: size,
|
|
77215
|
+
resurrectionCount: prior?.resurrectionCount ?? 0,
|
|
77216
|
+
resurrectedForThisFinish: false,
|
|
77217
|
+
lost: prior?.lost ?? false
|
|
77218
|
+
});
|
|
77219
|
+
}
|
|
77220
|
+
function resurrectAgent(agentId, filePath) {
|
|
77221
|
+
const pc = pendingCloses.get(agentId);
|
|
77222
|
+
if (pc != null) {
|
|
77223
|
+
clearT(pc);
|
|
77224
|
+
pendingCloses.delete(agentId);
|
|
77225
|
+
}
|
|
77226
|
+
terminatedAgentIds.delete(agentId);
|
|
77227
|
+
historicalFiles.delete(filePath);
|
|
77228
|
+
const existing = registry.get(agentId);
|
|
77229
|
+
if (existing != null) {
|
|
77230
|
+
existing.state = "running";
|
|
77231
|
+
existing.historical = false;
|
|
77232
|
+
existing.stallNotified = false;
|
|
77233
|
+
existing.stalledAt = null;
|
|
77234
|
+
existing.stallTerminalSynthesised = false;
|
|
77235
|
+
existing.completionNotified = false;
|
|
77236
|
+
existing.errored = false;
|
|
77237
|
+
existing.errorDetail = undefined;
|
|
77238
|
+
existing.lastActivityAt = nowFn();
|
|
77239
|
+
knownFiles.add(filePath);
|
|
77240
|
+
return;
|
|
77241
|
+
}
|
|
77242
|
+
knownFiles.add(filePath);
|
|
77243
|
+
registerAgent(filePath, agentId);
|
|
77244
|
+
}
|
|
77245
|
+
function checkResurrections() {
|
|
77246
|
+
const n = nowFn();
|
|
77247
|
+
for (const [agentId, rec] of falseFinishTracker) {
|
|
77248
|
+
if (rec.lost)
|
|
77249
|
+
continue;
|
|
77250
|
+
if (rec.resurrectedForThisFinish)
|
|
77251
|
+
continue;
|
|
77252
|
+
let size;
|
|
77253
|
+
try {
|
|
77254
|
+
size = fs2.statSync(rec.filePath).size;
|
|
77255
|
+
} catch {
|
|
77256
|
+
continue;
|
|
77257
|
+
}
|
|
77258
|
+
if (size <= rec.sizeAtSynthesis)
|
|
77259
|
+
continue;
|
|
77260
|
+
const entry = registry.get(agentId);
|
|
77261
|
+
const description = entry?.description ?? "sub-agent";
|
|
77262
|
+
if (rec.resurrectionCount >= MAX_RESURRECTIONS) {
|
|
77263
|
+
rec.lost = true;
|
|
77264
|
+
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)`);
|
|
77265
|
+
if (config.onWorkerLost != null) {
|
|
77266
|
+
try {
|
|
77267
|
+
config.onWorkerLost(agentId, description);
|
|
77268
|
+
} catch (cbErr) {
|
|
77269
|
+
log?.(`subagent-watcher: onWorkerLost callback error ${agentId}: ${cbErr.message}`);
|
|
77270
|
+
}
|
|
77271
|
+
}
|
|
77272
|
+
continue;
|
|
77273
|
+
}
|
|
77274
|
+
rec.resurrectionCount++;
|
|
77275
|
+
rec.resurrectedForThisFinish = true;
|
|
77276
|
+
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)`);
|
|
77277
|
+
if (config.onResurrect != null) {
|
|
77278
|
+
try {
|
|
77279
|
+
config.onResurrect(agentId, description);
|
|
77280
|
+
} catch (cbErr) {
|
|
77281
|
+
log?.(`subagent-watcher: onResurrect callback error ${agentId}: ${cbErr.message}`);
|
|
77282
|
+
}
|
|
77283
|
+
}
|
|
77284
|
+
resurrectAgent(agentId, rec.filePath);
|
|
77285
|
+
}
|
|
77286
|
+
}
|
|
76742
77287
|
function checkStalls() {
|
|
76743
77288
|
const n = nowFn();
|
|
76744
77289
|
for (const entry of registry.values()) {
|
|
@@ -76786,11 +77331,20 @@ function startSubagentWatcher(config) {
|
|
|
76786
77331
|
continue;
|
|
76787
77332
|
if (n - entry.stalledAt < silentStallTerminalMs)
|
|
76788
77333
|
continue;
|
|
77334
|
+
if (entry.inflightToolUseIds.size > 0) {
|
|
77335
|
+
const totalIdleMs = n - entry.lastActivityAt;
|
|
77336
|
+
if (totalIdleMs < inflightTerminalCapMs) {
|
|
77337
|
+
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)`);
|
|
77338
|
+
continue;
|
|
77339
|
+
}
|
|
77340
|
+
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`);
|
|
77341
|
+
}
|
|
76789
77342
|
entry.stallTerminalSynthesised = true;
|
|
76790
77343
|
entry.state = "done";
|
|
76791
77344
|
const postStallSec = Math.floor((n - entry.stalledAt) / 1000);
|
|
76792
77345
|
const totalIdleSec = Math.floor((n - entry.lastActivityAt) / 1000);
|
|
76793
77346
|
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`);
|
|
77347
|
+
recordFalseFinish(entry.agentId, entry.filePath, n);
|
|
76794
77348
|
if (db2 != null) {
|
|
76795
77349
|
try {
|
|
76796
77350
|
const rowRef = db2.prepare("SELECT id FROM subagents WHERE jsonl_agent_id = ?").get(entry.agentId);
|
|
@@ -76947,6 +77501,7 @@ function startSubagentWatcher(config) {
|
|
|
76947
77501
|
maybySendStateTransition(agentId);
|
|
76948
77502
|
}
|
|
76949
77503
|
checkStalls();
|
|
77504
|
+
checkResurrections();
|
|
76950
77505
|
}
|
|
76951
77506
|
rescanSubagentDirs();
|
|
76952
77507
|
bootScanInProgress = false;
|
|
@@ -76954,7 +77509,16 @@ function startSubagentWatcher(config) {
|
|
|
76954
77509
|
if (db2 == null)
|
|
76955
77510
|
return;
|
|
76956
77511
|
try {
|
|
76957
|
-
const result = reapStuckRunningRows(db2, {
|
|
77512
|
+
const result = reapStuckRunningRows(db2, {
|
|
77513
|
+
ttlMs: reaperTtlMs,
|
|
77514
|
+
now: nowFn(),
|
|
77515
|
+
isLive: (jsonlAgentId) => {
|
|
77516
|
+
if (jsonlAgentId == null)
|
|
77517
|
+
return false;
|
|
77518
|
+
const entry = registry.get(jsonlAgentId);
|
|
77519
|
+
return entry != null && entry.state === "running" && !entry.historical;
|
|
77520
|
+
}
|
|
77521
|
+
});
|
|
76958
77522
|
if (result.reaped > 0) {
|
|
76959
77523
|
log?.(`subagent-watcher: reaper transitioned ${result.reaped} stuck-running row(s) to stalled (ttl=${Math.round(reaperTtlMs / 60000)}min)`);
|
|
76960
77524
|
}
|
|
@@ -77125,7 +77689,7 @@ function determineRestartReason(opts) {
|
|
|
77125
77689
|
init_boot_card();
|
|
77126
77690
|
|
|
77127
77691
|
// gateway/update-announce.ts
|
|
77128
|
-
import { existsSync as existsSync41, mkdirSync as mkdirSync32, openSync as
|
|
77692
|
+
import { existsSync as existsSync41, mkdirSync as mkdirSync32, openSync as openSync8, closeSync as closeSync8, readFileSync as readFileSync43 } from "node:fs";
|
|
77129
77693
|
import { join as join44 } from "node:path";
|
|
77130
77694
|
import { homedir as homedir15 } from "node:os";
|
|
77131
77695
|
|
|
@@ -77326,8 +77890,8 @@ function claimUpdateAnnouncement(requestId, opts = {}) {
|
|
|
77326
77890
|
const safeId = requestId.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 200);
|
|
77327
77891
|
const path2 = join44(dir, safeId);
|
|
77328
77892
|
try {
|
|
77329
|
-
const fd =
|
|
77330
|
-
|
|
77893
|
+
const fd = openSync8(path2, "wx");
|
|
77894
|
+
closeSync8(fd);
|
|
77331
77895
|
return true;
|
|
77332
77896
|
} catch {
|
|
77333
77897
|
return false;
|
|
@@ -77550,10 +78114,10 @@ import { join as join46 } from "node:path";
|
|
|
77550
78114
|
|
|
77551
78115
|
// ../src/issues/store.ts
|
|
77552
78116
|
import {
|
|
77553
|
-
closeSync as
|
|
78117
|
+
closeSync as closeSync9,
|
|
77554
78118
|
existsSync as existsSync42,
|
|
77555
78119
|
mkdirSync as mkdirSync33,
|
|
77556
|
-
openSync as
|
|
78120
|
+
openSync as openSync9,
|
|
77557
78121
|
readdirSync as readdirSync9,
|
|
77558
78122
|
readFileSync as readFileSync45,
|
|
77559
78123
|
renameSync as renameSync15,
|
|
@@ -77675,7 +78239,7 @@ function withLock(stateDir, fn) {
|
|
|
77675
78239
|
let fd = null;
|
|
77676
78240
|
while (fd === null) {
|
|
77677
78241
|
try {
|
|
77678
|
-
fd =
|
|
78242
|
+
fd = openSync9(lockPath, "wx");
|
|
77679
78243
|
try {
|
|
77680
78244
|
writeSync5(fd, String(process.pid));
|
|
77681
78245
|
} catch {}
|
|
@@ -77695,7 +78259,7 @@ function withLock(stateDir, fn) {
|
|
|
77695
78259
|
return fn();
|
|
77696
78260
|
} finally {
|
|
77697
78261
|
try {
|
|
77698
|
-
|
|
78262
|
+
closeSync9(fd);
|
|
77699
78263
|
} catch {}
|
|
77700
78264
|
try {
|
|
77701
78265
|
unlinkSync17(lockPath);
|
|
@@ -79117,6 +79681,40 @@ function resolveQuotaWatchTuning(env) {
|
|
|
79117
79681
|
function buildQuotaClaimKey(accountLabel, transition2, chatId) {
|
|
79118
79682
|
return `quota-watch:${accountLabel}:${transition2}:${chatId}`;
|
|
79119
79683
|
}
|
|
79684
|
+
var FLEET_ROLL_ANNOUNCE_KEY = "__fleet_roll_announce__";
|
|
79685
|
+
var FLEET_ROLL_MAX_AGE_MS = QUOTA_WATCH_CLAIM_WINDOW_MS;
|
|
79686
|
+
var QUOTA_WATCH_ROLL_DEDUP_MS = QUOTA_WATCH_CLAIM_WINDOW_MS;
|
|
79687
|
+
function evaluateFleetRollAnnounce(args) {
|
|
79688
|
+
const { roll, prev, now } = args;
|
|
79689
|
+
const maxAgeMs = args.maxAgeMs ?? FLEET_ROLL_MAX_AGE_MS;
|
|
79690
|
+
if (!roll)
|
|
79691
|
+
return { kind: "skip", reason: "no-roll" };
|
|
79692
|
+
if (prev.lastNotifiedAt >= roll.at)
|
|
79693
|
+
return { kind: "skip", reason: "already-announced" };
|
|
79694
|
+
if (now - roll.at > maxAgeMs)
|
|
79695
|
+
return { kind: "skip", reason: "stale-roll" };
|
|
79696
|
+
return {
|
|
79697
|
+
kind: "notify",
|
|
79698
|
+
message: buildFleetRollMessage(roll, now),
|
|
79699
|
+
newState: { lastNotifiedHealth: "healthy", lastNotifiedAt: roll.at }
|
|
79700
|
+
};
|
|
79701
|
+
}
|
|
79702
|
+
function buildFleetRollMessage(roll, now) {
|
|
79703
|
+
const winLabel = roll.window === "5h" ? "5-hour window" : roll.window === "7d" ? "7-day window" : "quota window";
|
|
79704
|
+
const pctPart = typeof roll.pct === "number" ? ` at ${fmtPct(roll.pct)}` : "";
|
|
79705
|
+
const resetPart = typeof roll.exhausted_until === "number" && roll.exhausted_until > now ? ` (resets ${formatRelative(new Date(roll.exhausted_until), new Date(now))})` : "";
|
|
79706
|
+
const softAvoid = roll.reason === "soft-avoid";
|
|
79707
|
+
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}.`;
|
|
79708
|
+
return [
|
|
79709
|
+
`\uD83D\uDD01 **Switched fleet to \`${codeSpanSafe(roll.to)}\`**`,
|
|
79710
|
+
``,
|
|
79711
|
+
causeLine,
|
|
79712
|
+
`Work continues uninterrupted \u2014 agents and scheduled jobs now serve from \`${codeSpanSafe(roll.to)}\`.`,
|
|
79713
|
+
``,
|
|
79714
|
+
`_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"}._`
|
|
79715
|
+
].join(`
|
|
79716
|
+
`);
|
|
79717
|
+
}
|
|
79120
79718
|
function isLiveCorroboration(entry) {
|
|
79121
79719
|
return entry?.result.ok === true && entry.served === "live";
|
|
79122
79720
|
}
|
|
@@ -79142,6 +79740,16 @@ function evaluateQuotaWatchAccount(args) {
|
|
|
79142
79740
|
lastNotifiedHealth: "throttling",
|
|
79143
79741
|
lastNotifiedAt: now
|
|
79144
79742
|
};
|
|
79743
|
+
const lastRoll = args.lastRoll;
|
|
79744
|
+
if (lastRoll && lastRoll.from === label && now - lastRoll.at <= QUOTA_WATCH_ROLL_DEDUP_MS && prev.lastNotifiedAt < lastRoll.at) {
|
|
79745
|
+
return {
|
|
79746
|
+
kind: "reconcile",
|
|
79747
|
+
accountLabel: label,
|
|
79748
|
+
newAccountState: newState,
|
|
79749
|
+
transition: "entered-throttling",
|
|
79750
|
+
reason: "roll-announced"
|
|
79751
|
+
};
|
|
79752
|
+
}
|
|
79145
79753
|
return {
|
|
79146
79754
|
kind: "notify",
|
|
79147
79755
|
accountLabel: label,
|
|
@@ -79255,7 +79863,7 @@ function buildThrottlingMessage(agentName3, snap) {
|
|
|
79255
79863
|
const activeNote = snap.isActive ? "" : `
|
|
79256
79864
|
This is a non-active account. Consider \`/auth use ${codeSpanSafe(snap.label)}\` to switch, or keep it as a fallback reserve.`;
|
|
79257
79865
|
const altNote = snap.isActive ? `
|
|
79258
|
-
|
|
79866
|
+
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
79867
|
return [
|
|
79260
79868
|
`\uD83D\uDFE1 **Quota approaching limit** \u2014 \`${codeSpanSafe(snap.label)}\``,
|
|
79261
79869
|
``,
|
|
@@ -79313,15 +79921,50 @@ function patchQuotaWatchState(current, accountLabel, accountState) {
|
|
|
79313
79921
|
return { ...current, [accountLabel]: accountState };
|
|
79314
79922
|
}
|
|
79315
79923
|
|
|
79924
|
+
// fallback-card-collapse.ts
|
|
79925
|
+
var CARD_COLLAPSE_MAX_AGE_MS = 5 * 60000;
|
|
79926
|
+
function createModelUnavailableCardRegistry(maxAgeMs = CARD_COLLAPSE_MAX_AGE_MS) {
|
|
79927
|
+
const cards = new Map;
|
|
79928
|
+
return {
|
|
79929
|
+
record(chatId, rec) {
|
|
79930
|
+
cards.set(chatId, rec);
|
|
79931
|
+
},
|
|
79932
|
+
take(chatId, now) {
|
|
79933
|
+
const rec = cards.get(chatId);
|
|
79934
|
+
if (!rec)
|
|
79935
|
+
return null;
|
|
79936
|
+
cards.delete(chatId);
|
|
79937
|
+
if (!rec.promisedFallback)
|
|
79938
|
+
return null;
|
|
79939
|
+
if (now - rec.atMs > maxAgeMs)
|
|
79940
|
+
return null;
|
|
79941
|
+
return rec;
|
|
79942
|
+
},
|
|
79943
|
+
size() {
|
|
79944
|
+
return cards.size;
|
|
79945
|
+
}
|
|
79946
|
+
};
|
|
79947
|
+
}
|
|
79948
|
+
function decideAnnouncementDelivery(outcomeKind, card) {
|
|
79949
|
+
if (outcomeKind !== "switched")
|
|
79950
|
+
return "send";
|
|
79951
|
+
return card !== null ? "edit" : "send";
|
|
79952
|
+
}
|
|
79953
|
+
function foldAnnouncementIntoCard(cardText, announcement) {
|
|
79954
|
+
return `${cardText}
|
|
79955
|
+
|
|
79956
|
+
${announcement}`;
|
|
79957
|
+
}
|
|
79958
|
+
|
|
79316
79959
|
// gateway/gateway.ts
|
|
79317
79960
|
init_auth_snapshot_format2();
|
|
79318
79961
|
|
|
79319
79962
|
// gateway/turn-active-marker.ts
|
|
79320
79963
|
import {
|
|
79321
|
-
closeSync as
|
|
79964
|
+
closeSync as closeSync10,
|
|
79322
79965
|
existsSync as existsSync46,
|
|
79323
79966
|
mkdirSync as mkdirSync36,
|
|
79324
|
-
openSync as
|
|
79967
|
+
openSync as openSync10,
|
|
79325
79968
|
readFileSync as readFileSync49,
|
|
79326
79969
|
statSync as statSync13,
|
|
79327
79970
|
unlinkSync as unlinkSync18,
|
|
@@ -79346,8 +79989,8 @@ function touchTurnActiveMarker2(stateDir) {
|
|
|
79346
79989
|
utimesSync2(path2, now, now);
|
|
79347
79990
|
} catch {
|
|
79348
79991
|
try {
|
|
79349
|
-
const fd =
|
|
79350
|
-
|
|
79992
|
+
const fd = openSync10(path2, "r+");
|
|
79993
|
+
closeSync10(fd);
|
|
79351
79994
|
} catch {}
|
|
79352
79995
|
}
|
|
79353
79996
|
}
|
|
@@ -79398,10 +80041,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
|
|
|
79398
80041
|
}
|
|
79399
80042
|
|
|
79400
80043
|
// ../src/build-info.ts
|
|
79401
|
-
var VERSION = "0.18.
|
|
79402
|
-
var COMMIT_SHA = "
|
|
79403
|
-
var COMMIT_DATE = "2026-07-
|
|
79404
|
-
var LATEST_PR =
|
|
80044
|
+
var VERSION = "0.18.10";
|
|
80045
|
+
var COMMIT_SHA = "1f778844";
|
|
80046
|
+
var COMMIT_DATE = "2026-07-11T13:26:53+10:00";
|
|
80047
|
+
var LATEST_PR = 3044;
|
|
79405
80048
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
79406
80049
|
|
|
79407
80050
|
// gateway/boot-version.ts
|
|
@@ -80616,6 +81259,13 @@ When the restart hit, ${count} sub-agent${count === 1 ? " was" : "s were"} still
|
|
|
80616
81259
|
` + lines.join(`
|
|
80617
81260
|
`) + overflow + closing;
|
|
80618
81261
|
}
|
|
81262
|
+
function renderRestartCauseBlock(cause) {
|
|
81263
|
+
if (!cause || cause.note.trim().length === 0)
|
|
81264
|
+
return "";
|
|
81265
|
+
return `
|
|
81266
|
+
|
|
81267
|
+
Why this restart happened: ${cause.note.trim()}`;
|
|
81268
|
+
}
|
|
80619
81269
|
var RESUME_SYNTHETIC_PROMPT_PREFIX = "You just restarted.";
|
|
80620
81270
|
function isResumeSyntheticTurn(turn) {
|
|
80621
81271
|
const p = turn.user_prompt_preview;
|
|
@@ -80648,6 +81298,8 @@ function buildResumeInterruptedInbound(ctx) {
|
|
|
80648
81298
|
};
|
|
80649
81299
|
if (ctx.turn.user_prompt_preview)
|
|
80650
81300
|
meta.original_prompt = ctx.turn.user_prompt_preview;
|
|
81301
|
+
if (ctx.restartCause)
|
|
81302
|
+
meta.restart_cause = ctx.restartCause.reason;
|
|
80651
81303
|
return {
|
|
80652
81304
|
type: "inbound",
|
|
80653
81305
|
chatId: ctx.turn.chat_id,
|
|
@@ -80656,7 +81308,7 @@ function buildResumeInterruptedInbound(ctx) {
|
|
|
80656
81308
|
user: "switchroom",
|
|
80657
81309
|
userId: 0,
|
|
80658
81310
|
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),
|
|
81311
|
+
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
81312
|
meta
|
|
80661
81313
|
};
|
|
80662
81314
|
}
|
|
@@ -80679,6 +81331,8 @@ function buildResumeWatchdogReportInbound(ctx) {
|
|
|
80679
81331
|
meta.tool_call_count = String(ctx.turn.tool_call_count);
|
|
80680
81332
|
if (ctx.turn.user_prompt_preview)
|
|
80681
81333
|
meta.original_prompt = ctx.turn.user_prompt_preview;
|
|
81334
|
+
if (ctx.restartCause)
|
|
81335
|
+
meta.restart_cause = ctx.restartCause.reason;
|
|
80682
81336
|
return {
|
|
80683
81337
|
type: "inbound",
|
|
80684
81338
|
chatId: ctx.turn.chat_id,
|
|
@@ -80687,7 +81341,7 @@ function buildResumeWatchdogReportInbound(ctx) {
|
|
|
80687
81341
|
user: "switchroom",
|
|
80688
81342
|
userId: 0,
|
|
80689
81343
|
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 }),
|
|
81344
|
+
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
81345
|
meta
|
|
80692
81346
|
};
|
|
80693
81347
|
}
|
|
@@ -80716,6 +81370,8 @@ function buildResumeDeferredReportInbound(ctx) {
|
|
|
80716
81370
|
};
|
|
80717
81371
|
if (ctx.turn.user_prompt_preview)
|
|
80718
81372
|
meta.original_prompt = ctx.turn.user_prompt_preview;
|
|
81373
|
+
if (ctx.restartCause)
|
|
81374
|
+
meta.restart_cause = ctx.restartCause.reason;
|
|
80719
81375
|
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
81376
|
return {
|
|
80721
81377
|
type: "inbound",
|
|
@@ -80725,7 +81381,7 @@ function buildResumeDeferredReportInbound(ctx) {
|
|
|
80725
81381
|
user: "switchroom",
|
|
80726
81382
|
userId: 0,
|
|
80727
81383
|
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 }),
|
|
81384
|
+
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
81385
|
meta
|
|
80730
81386
|
};
|
|
80731
81387
|
}
|
|
@@ -80745,6 +81401,282 @@ function selectResumeBuilder(endedVia, opts) {
|
|
|
80745
81401
|
return kind;
|
|
80746
81402
|
}
|
|
80747
81403
|
|
|
81404
|
+
// gateway/bridge-dead-watchdog.ts
|
|
81405
|
+
import { readFileSync as readFileSync51, writeFileSync as writeFileSync40, renameSync as renameSync16, unlinkSync as unlinkSync19 } from "node:fs";
|
|
81406
|
+
|
|
81407
|
+
// gateway/cron-session.ts
|
|
81408
|
+
var CRON_IDENTITY_SUFFIX2 = "-cron";
|
|
81409
|
+
function isCronIdentity2(name) {
|
|
81410
|
+
return typeof name === "string" && name.endsWith(CRON_IDENTITY_SUFFIX2);
|
|
81411
|
+
}
|
|
81412
|
+
|
|
81413
|
+
// gateway/bridge-dead-watchdog.ts
|
|
81414
|
+
var BRIDGE_DEAD_RESTART_REASON = "bridge-dead-resume";
|
|
81415
|
+
var DEFAULT_BRIDGE_DEAD_GRACE_MS = 90000;
|
|
81416
|
+
var ESCALATION_MARKER_MAX_AGE_MS = 10 * 60000;
|
|
81417
|
+
var MAX_CONSECUTIVE_ESCALATIONS = 2;
|
|
81418
|
+
var CRASH_LOG_FRESH_WINDOW_MS = 15 * 60000;
|
|
81419
|
+
var CRASH_LOG_TAIL_LINES = 3;
|
|
81420
|
+
function decideBridgeDeadEscalation(s) {
|
|
81421
|
+
if (s.bridgeRegistered)
|
|
81422
|
+
return { action: "skip", why: "bridge-registered" };
|
|
81423
|
+
if (s.shuttingDown)
|
|
81424
|
+
return { action: "skip", why: "shutting-down" };
|
|
81425
|
+
if (s.alreadyEscalated)
|
|
81426
|
+
return { action: "skip", why: "already-escalated" };
|
|
81427
|
+
if (!s.bridgeEverRegistered && s.priorStreak >= s.maxConsecutive) {
|
|
81428
|
+
return { action: "skip", why: "streak-capped" };
|
|
81429
|
+
}
|
|
81430
|
+
if (!s.sessionAlive)
|
|
81431
|
+
return { action: "retry", why: "session-not-alive" };
|
|
81432
|
+
return { action: "escalate" };
|
|
81433
|
+
}
|
|
81434
|
+
function readFreshCrashLogTail(path2, opts = {}) {
|
|
81435
|
+
const nowMs2 = opts.nowMs ?? Date.now();
|
|
81436
|
+
const freshWindowMs = opts.freshWindowMs ?? CRASH_LOG_FRESH_WINDOW_MS;
|
|
81437
|
+
const maxLines = opts.maxLines ?? CRASH_LOG_TAIL_LINES;
|
|
81438
|
+
const readFile = opts.readFile ?? ((p) => readFileSync51(p, "utf8"));
|
|
81439
|
+
let raw;
|
|
81440
|
+
try {
|
|
81441
|
+
raw = readFile(path2);
|
|
81442
|
+
} catch {
|
|
81443
|
+
return null;
|
|
81444
|
+
}
|
|
81445
|
+
const fresh = raw.split(`
|
|
81446
|
+
`).filter((line) => {
|
|
81447
|
+
const m = line.match(/^(\d{4}-\d{2}-\d{2}T[0-9:.]+Z)\s/);
|
|
81448
|
+
if (!m)
|
|
81449
|
+
return false;
|
|
81450
|
+
const t = Date.parse(m[1]);
|
|
81451
|
+
return Number.isFinite(t) && nowMs2 - t >= 0 && nowMs2 - t <= freshWindowMs;
|
|
81452
|
+
}).slice(-maxLines);
|
|
81453
|
+
if (fresh.length === 0)
|
|
81454
|
+
return null;
|
|
81455
|
+
return fresh.map((l) => l.slice(0, 500)).join(" || ");
|
|
81456
|
+
}
|
|
81457
|
+
function writeBridgeDeadEscalationMarker(path2, marker) {
|
|
81458
|
+
const tmp = `${path2}.tmp-${process.pid}-${Date.now()}`;
|
|
81459
|
+
writeFileSync40(tmp, JSON.stringify(marker), "utf8");
|
|
81460
|
+
renameSync16(tmp, path2);
|
|
81461
|
+
}
|
|
81462
|
+
function consumeBridgeDeadEscalationMarker(path2, nowMs2 = Date.now(), maxAgeMs = ESCALATION_MARKER_MAX_AGE_MS) {
|
|
81463
|
+
let marker = null;
|
|
81464
|
+
try {
|
|
81465
|
+
const parsed = JSON.parse(readFileSync51(path2, "utf8"));
|
|
81466
|
+
if (typeof parsed.ts === "number" && Number.isFinite(parsed.ts) && typeof parsed.reason === "string") {
|
|
81467
|
+
const age = nowMs2 - parsed.ts;
|
|
81468
|
+
if (age >= 0 && age < maxAgeMs) {
|
|
81469
|
+
marker = { ts: parsed.ts, reason: parsed.reason };
|
|
81470
|
+
marker.count = typeof parsed.count === "number" && Number.isFinite(parsed.count) && parsed.count >= 1 ? Math.floor(parsed.count) : 1;
|
|
81471
|
+
if (typeof parsed.crashTail === "string")
|
|
81472
|
+
marker.crashTail = parsed.crashTail;
|
|
81473
|
+
}
|
|
81474
|
+
}
|
|
81475
|
+
} catch {}
|
|
81476
|
+
try {
|
|
81477
|
+
unlinkSync19(path2);
|
|
81478
|
+
} catch {}
|
|
81479
|
+
return marker;
|
|
81480
|
+
}
|
|
81481
|
+
function buildBridgeDeadIdleNoticeInbound(args) {
|
|
81482
|
+
const ts = args.nowMs ?? Date.now();
|
|
81483
|
+
return {
|
|
81484
|
+
type: "inbound",
|
|
81485
|
+
chatId: args.chatId,
|
|
81486
|
+
messageId: ts,
|
|
81487
|
+
user: "switchroom",
|
|
81488
|
+
userId: 0,
|
|
81489
|
+
ts,
|
|
81490
|
+
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.`,
|
|
81491
|
+
meta: {
|
|
81492
|
+
source: "bridge_dead_restart",
|
|
81493
|
+
chat_id: args.chatId,
|
|
81494
|
+
message_id: String(ts),
|
|
81495
|
+
restart_cause: args.marker.reason
|
|
81496
|
+
}
|
|
81497
|
+
};
|
|
81498
|
+
}
|
|
81499
|
+
function isRealBridgeIdentity(agentName3) {
|
|
81500
|
+
return agentName3 != null && agentName3.length > 0 && !isCronIdentity2(agentName3);
|
|
81501
|
+
}
|
|
81502
|
+
function createBridgeDeadWatchdog(opts) {
|
|
81503
|
+
const setTimer = opts.setTimer ?? ((fn, ms) => {
|
|
81504
|
+
const t = setTimeout(fn, ms);
|
|
81505
|
+
t.unref?.();
|
|
81506
|
+
return t;
|
|
81507
|
+
});
|
|
81508
|
+
const clearTimer = opts.clearTimer ?? ((h) => clearTimeout(h));
|
|
81509
|
+
const nowMs2 = opts.nowMs ?? (() => Date.now());
|
|
81510
|
+
const writeMarker = opts.writeMarker ?? writeBridgeDeadEscalationMarker;
|
|
81511
|
+
const readCrashTail = opts.readCrashTail ?? ((p, t) => readFreshCrashLogTail(p, { nowMs: t }));
|
|
81512
|
+
const priorStreak = opts.priorStreak ?? 0;
|
|
81513
|
+
const maxConsecutive = opts.maxConsecutive ?? MAX_CONSECUTIVE_ESCALATIONS;
|
|
81514
|
+
let timer3 = null;
|
|
81515
|
+
let bridgeRegistered = false;
|
|
81516
|
+
let bridgeEverRegistered = false;
|
|
81517
|
+
let escalated = false;
|
|
81518
|
+
const cancel = () => {
|
|
81519
|
+
if (timer3 != null) {
|
|
81520
|
+
clearTimer(timer3);
|
|
81521
|
+
timer3 = null;
|
|
81522
|
+
}
|
|
81523
|
+
};
|
|
81524
|
+
const effectiveGraceMs = () => !bridgeEverRegistered && priorStreak >= 1 ? opts.graceMs * 2 : opts.graceMs;
|
|
81525
|
+
const armInternal = () => {
|
|
81526
|
+
if (escalated)
|
|
81527
|
+
return;
|
|
81528
|
+
if (!bridgeEverRegistered && priorStreak >= maxConsecutive) {
|
|
81529
|
+
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.`);
|
|
81530
|
+
return;
|
|
81531
|
+
}
|
|
81532
|
+
cancel();
|
|
81533
|
+
timer3 = setTimer(() => {
|
|
81534
|
+
timer3 = null;
|
|
81535
|
+
check();
|
|
81536
|
+
}, effectiveGraceMs());
|
|
81537
|
+
};
|
|
81538
|
+
const check = () => {
|
|
81539
|
+
const decision = decideBridgeDeadEscalation({
|
|
81540
|
+
bridgeRegistered,
|
|
81541
|
+
bridgeEverRegistered,
|
|
81542
|
+
sessionAlive: opts.isSessionAlive(),
|
|
81543
|
+
shuttingDown: opts.isShuttingDown(),
|
|
81544
|
+
alreadyEscalated: escalated,
|
|
81545
|
+
priorStreak,
|
|
81546
|
+
maxConsecutive
|
|
81547
|
+
});
|
|
81548
|
+
if (decision.action === "retry") {
|
|
81549
|
+
opts.log(`telegram gateway: [bridge-dead-watchdog] no bridge registered after ${effectiveGraceMs()}ms ` + `but claude session not found \u2014 re-arming (no escalation)`);
|
|
81550
|
+
armInternal();
|
|
81551
|
+
return decision;
|
|
81552
|
+
}
|
|
81553
|
+
if (decision.action === "skip") {
|
|
81554
|
+
if (decision.why !== "bridge-registered") {
|
|
81555
|
+
opts.log(`telegram gateway: [bridge-dead-watchdog] bridge missing but skipping escalation (${decision.why})`);
|
|
81556
|
+
}
|
|
81557
|
+
return decision;
|
|
81558
|
+
}
|
|
81559
|
+
escalated = true;
|
|
81560
|
+
const t = nowMs2();
|
|
81561
|
+
const crashTail = (() => {
|
|
81562
|
+
try {
|
|
81563
|
+
return readCrashTail(opts.crashLogPath, t);
|
|
81564
|
+
} catch {
|
|
81565
|
+
return null;
|
|
81566
|
+
}
|
|
81567
|
+
})();
|
|
81568
|
+
const streakCount = bridgeEverRegistered ? 1 : priorStreak + 1;
|
|
81569
|
+
try {
|
|
81570
|
+
const marker = {
|
|
81571
|
+
ts: t,
|
|
81572
|
+
reason: BRIDGE_DEAD_RESTART_REASON,
|
|
81573
|
+
count: streakCount
|
|
81574
|
+
};
|
|
81575
|
+
if (crashTail != null)
|
|
81576
|
+
marker.crashTail = crashTail;
|
|
81577
|
+
writeMarker(opts.markerPath, marker);
|
|
81578
|
+
} catch (err) {
|
|
81579
|
+
opts.log(`telegram gateway: [bridge-dead-watchdog] escalation marker write failed: ${err.message}`);
|
|
81580
|
+
}
|
|
81581
|
+
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`));
|
|
81582
|
+
const fired = opts.escalate(BRIDGE_DEAD_RESTART_REASON);
|
|
81583
|
+
if (!fired) {
|
|
81584
|
+
opts.log(`telegram gateway: [bridge-dead-watchdog] escalate() reported failure \u2014 ` + `restart not dispatched (fuse stays blown; no retry this boot)`);
|
|
81585
|
+
}
|
|
81586
|
+
return decision;
|
|
81587
|
+
};
|
|
81588
|
+
return {
|
|
81589
|
+
arm: armInternal,
|
|
81590
|
+
noteBridgeRegistered: (agentName3) => {
|
|
81591
|
+
if (!isRealBridgeIdentity(agentName3))
|
|
81592
|
+
return;
|
|
81593
|
+
bridgeRegistered = true;
|
|
81594
|
+
bridgeEverRegistered = true;
|
|
81595
|
+
cancel();
|
|
81596
|
+
},
|
|
81597
|
+
noteBridgeDisconnected: (agentName3) => {
|
|
81598
|
+
if (!isRealBridgeIdentity(agentName3))
|
|
81599
|
+
return;
|
|
81600
|
+
bridgeRegistered = false;
|
|
81601
|
+
armInternal();
|
|
81602
|
+
},
|
|
81603
|
+
check,
|
|
81604
|
+
stop: cancel,
|
|
81605
|
+
hasEscalated: () => escalated
|
|
81606
|
+
};
|
|
81607
|
+
}
|
|
81608
|
+
|
|
81609
|
+
// gateway/boot-probes.ts
|
|
81610
|
+
import { readFileSync as readFileSync52, readdirSync as readdirSync10, existsSync as existsSync48 } from "fs";
|
|
81611
|
+
init_quota_cache();
|
|
81612
|
+
init_quota_check();
|
|
81613
|
+
import { execFile as execFileCb2 } from "child_process";
|
|
81614
|
+
import { promisify as promisify2 } from "util";
|
|
81615
|
+
var execFile2 = promisify2(execFileCb2);
|
|
81616
|
+
var realProcFs2 = {
|
|
81617
|
+
readdir: (p) => readdirSync10(p),
|
|
81618
|
+
readFile: (p) => readFileSync52(p, "utf-8")
|
|
81619
|
+
};
|
|
81620
|
+
function findAgentProcessInContainer2(fs3 = realProcFs2) {
|
|
81621
|
+
let entries;
|
|
81622
|
+
try {
|
|
81623
|
+
entries = fs3.readdir("/proc");
|
|
81624
|
+
} catch {
|
|
81625
|
+
return null;
|
|
81626
|
+
}
|
|
81627
|
+
const candidates = [];
|
|
81628
|
+
for (const entry of entries) {
|
|
81629
|
+
if (!/^\d+$/.test(entry))
|
|
81630
|
+
continue;
|
|
81631
|
+
const pid = Number(entry);
|
|
81632
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
81633
|
+
continue;
|
|
81634
|
+
if (pid === process.pid)
|
|
81635
|
+
continue;
|
|
81636
|
+
let comm = "";
|
|
81637
|
+
try {
|
|
81638
|
+
comm = fs3.readFile(`/proc/${pid}/comm`).trim();
|
|
81639
|
+
} catch {
|
|
81640
|
+
continue;
|
|
81641
|
+
}
|
|
81642
|
+
let rssKb = 0;
|
|
81643
|
+
try {
|
|
81644
|
+
const status = fs3.readFile(`/proc/${pid}/status`);
|
|
81645
|
+
const m = status.match(/^VmRSS:\s+(\d+)/m);
|
|
81646
|
+
if (m)
|
|
81647
|
+
rssKb = parseInt(m[1], 10) || 0;
|
|
81648
|
+
} catch {
|
|
81649
|
+
continue;
|
|
81650
|
+
}
|
|
81651
|
+
let starttime = 0;
|
|
81652
|
+
try {
|
|
81653
|
+
const stat = fs3.readFile(`/proc/${pid}/stat`);
|
|
81654
|
+
const close = stat.lastIndexOf(")");
|
|
81655
|
+
const tail = close >= 0 ? stat.slice(close + 2) : stat;
|
|
81656
|
+
const fields = tail.trim().split(/\s+/);
|
|
81657
|
+
const st = Number(fields[19]);
|
|
81658
|
+
if (Number.isFinite(st) && st > 0)
|
|
81659
|
+
starttime = st;
|
|
81660
|
+
} catch {
|
|
81661
|
+
continue;
|
|
81662
|
+
}
|
|
81663
|
+
candidates.push({ pid, rssKb, comm, starttime });
|
|
81664
|
+
}
|
|
81665
|
+
if (candidates.length === 0)
|
|
81666
|
+
return null;
|
|
81667
|
+
const isAgent = (c) => c.comm === "claude";
|
|
81668
|
+
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";
|
|
81669
|
+
const claudeMatches = candidates.filter(isAgent);
|
|
81670
|
+
if (claudeMatches.length > 0) {
|
|
81671
|
+
claudeMatches.sort((a, b) => b.rssKb - a.rssKb);
|
|
81672
|
+
return claudeMatches[0];
|
|
81673
|
+
}
|
|
81674
|
+
const nodeMatches = candidates.filter((c) => c.comm === "node" && !isWrapper(c)).sort((a, b) => b.rssKb - a.rssKb);
|
|
81675
|
+
if (nodeMatches.length > 0)
|
|
81676
|
+
return nodeMatches[0];
|
|
81677
|
+
return null;
|
|
81678
|
+
}
|
|
81679
|
+
|
|
80748
81680
|
// registry/subagents-schema.ts
|
|
80749
81681
|
var SUBAGENTS_SCHEMA_SQL = `
|
|
80750
81682
|
CREATE TABLE IF NOT EXISTS subagents (
|
|
@@ -80900,7 +81832,7 @@ function alwaysAllowDrainDeps() {
|
|
|
80900
81832
|
return {
|
|
80901
81833
|
readConfigText: () => {
|
|
80902
81834
|
const cfgPath = process.env.SWITCHROOM_CONFIG ?? SWITCHROOM_CONFIG ?? findConfigFile2();
|
|
80903
|
-
return
|
|
81835
|
+
return readFileSync53(cfgPath, "utf8");
|
|
80904
81836
|
},
|
|
80905
81837
|
resolveAllowList: (_configText, agentName3) => {
|
|
80906
81838
|
const cfg = loadConfig2();
|
|
@@ -81011,6 +81943,13 @@ function triggerSelfRestart(targetAgent, reason, delayMs = 300) {
|
|
|
81011
81943
|
return false;
|
|
81012
81944
|
}
|
|
81013
81945
|
}
|
|
81946
|
+
{
|
|
81947
|
+
const bootSmDir = resolveAgentDirFromEnv();
|
|
81948
|
+
if (bootSmDir != null && clearStaleGatewayShutdownIntent(bootSmDir)) {
|
|
81949
|
+
process.stderr.write(`telegram gateway: cleared stale gateway-shutdown relaunch-model intent (previous bounce was gateway-only \u2014 container never restarted)
|
|
81950
|
+
`);
|
|
81951
|
+
}
|
|
81952
|
+
}
|
|
81014
81953
|
var cachedClaudeCliVersion = undefined;
|
|
81015
81954
|
function getClaudeCliVersion() {
|
|
81016
81955
|
if (cachedClaudeCliVersion !== undefined)
|
|
@@ -81040,7 +81979,7 @@ function formatBootVersion() {
|
|
|
81040
81979
|
}
|
|
81041
81980
|
try {
|
|
81042
81981
|
chmodSync8(ENV_FILE, 384);
|
|
81043
|
-
for (const line of
|
|
81982
|
+
for (const line of readFileSync53(ENV_FILE, "utf8").split(`
|
|
81044
81983
|
`)) {
|
|
81045
81984
|
const m = line.match(/^(\w+)=(.*)$/);
|
|
81046
81985
|
if (m && process.env[m[1]] === undefined)
|
|
@@ -81105,7 +82044,7 @@ bot.api.config.use(async (prev, method, payload, signal) => {
|
|
|
81105
82044
|
});
|
|
81106
82045
|
var GRAMMY_VERSION = (() => {
|
|
81107
82046
|
try {
|
|
81108
|
-
const raw =
|
|
82047
|
+
const raw = readFileSync53(new URL("../../node_modules/grammy/package.json", import.meta.url), "utf8");
|
|
81109
82048
|
return JSON.parse(raw).version ?? "unknown";
|
|
81110
82049
|
} catch {
|
|
81111
82050
|
return "unknown";
|
|
@@ -81191,7 +82130,7 @@ function assertSendable(f) {
|
|
|
81191
82130
|
}
|
|
81192
82131
|
function readAccessFile() {
|
|
81193
82132
|
try {
|
|
81194
|
-
const raw =
|
|
82133
|
+
const raw = readFileSync53(ACCESS_FILE, "utf8");
|
|
81195
82134
|
const parsed = JSON.parse(raw);
|
|
81196
82135
|
const allowFrom = validateStringArray("allowFrom", parsed.allowFrom ?? []);
|
|
81197
82136
|
const groups = {};
|
|
@@ -81229,7 +82168,7 @@ function readAccessFile() {
|
|
|
81229
82168
|
if (err.code === "ENOENT")
|
|
81230
82169
|
return defaultAccess();
|
|
81231
82170
|
try {
|
|
81232
|
-
|
|
82171
|
+
renameSync17(ACCESS_FILE, `${ACCESS_FILE}.corrupt-${Date.now()}`);
|
|
81233
82172
|
} catch {}
|
|
81234
82173
|
process.stderr.write(`telegram gateway: access.json is corrupt, moved aside. Starting fresh.
|
|
81235
82174
|
`);
|
|
@@ -81251,7 +82190,7 @@ function loadAccess() {
|
|
|
81251
82190
|
}
|
|
81252
82191
|
function readPeopleFile() {
|
|
81253
82192
|
try {
|
|
81254
|
-
const raw =
|
|
82193
|
+
const raw = readFileSync53(PEOPLE_FILE, "utf8");
|
|
81255
82194
|
const parsed = JSON.parse(raw);
|
|
81256
82195
|
if (!Array.isArray(parsed.entries))
|
|
81257
82196
|
return [];
|
|
@@ -81275,9 +82214,9 @@ function saveAccess(a) {
|
|
|
81275
82214
|
return;
|
|
81276
82215
|
mkdirSync39(STATE_DIR, { recursive: true, mode: 448 });
|
|
81277
82216
|
const tmp = ACCESS_FILE + ".tmp";
|
|
81278
|
-
|
|
82217
|
+
writeFileSync42(tmp, JSON.stringify(a, null, 2) + `
|
|
81279
82218
|
`, { mode: 384 });
|
|
81280
|
-
|
|
82219
|
+
renameSync17(tmp, ACCESS_FILE);
|
|
81281
82220
|
}
|
|
81282
82221
|
function pruneExpired(a) {
|
|
81283
82222
|
const now = Date.now();
|
|
@@ -81304,6 +82243,7 @@ if (HISTORY_ENABLED) {
|
|
|
81304
82243
|
}
|
|
81305
82244
|
var turnsDb = null;
|
|
81306
82245
|
var bootResumeInbound = null;
|
|
82246
|
+
var bridgeDeadPriorStreak = 0;
|
|
81307
82247
|
try {
|
|
81308
82248
|
const agentDir = STATE_DIR.endsWith("/telegram") ? STATE_DIR.slice(0, -"/telegram".length) : STATE_DIR;
|
|
81309
82249
|
turnsDb = openTurnsDb(agentDir);
|
|
@@ -81312,11 +82252,11 @@ try {
|
|
|
81312
82252
|
let markerAgeMs = null;
|
|
81313
82253
|
try {
|
|
81314
82254
|
const markerPath = join53(STATE_DIR, TURN_ACTIVE_MARKER_FILE2);
|
|
81315
|
-
if (
|
|
82255
|
+
if (existsSync50(markerPath)) {
|
|
81316
82256
|
const st = statSync16(markerPath);
|
|
81317
82257
|
markerAgeMs = Date.now() - st.mtimeMs;
|
|
81318
82258
|
try {
|
|
81319
|
-
const payload = JSON.parse(
|
|
82259
|
+
const payload = JSON.parse(readFileSync53(markerPath, "utf8"));
|
|
81320
82260
|
if (typeof payload.turnKey === "string" && payload.turnKey.length > 0) {
|
|
81321
82261
|
markerTurnKey = payload.turnKey;
|
|
81322
82262
|
}
|
|
@@ -81339,6 +82279,16 @@ try {
|
|
|
81339
82279
|
process.stderr.write(`telegram gateway: turn-registry initialized at ${join53(agentDir, "telegram", "registry.db")}
|
|
81340
82280
|
`);
|
|
81341
82281
|
}
|
|
82282
|
+
const bridgeDeadMarker = consumeBridgeDeadEscalationMarker(join53(STATE_DIR, "bridge-dead-escalation.json"));
|
|
82283
|
+
if (bridgeDeadMarker != null) {
|
|
82284
|
+
bridgeDeadPriorStreak = bridgeDeadMarker.count ?? 1;
|
|
82285
|
+
process.stderr.write(`telegram gateway: boot: prior restart was a bridge-dead escalation (reason=${bridgeDeadMarker.reason}, consecutive=${bridgeDeadPriorStreak}${bridgeDeadMarker.crashTail ? `, crashTail=${bridgeDeadMarker.crashTail}` : ""})
|
|
82286
|
+
`);
|
|
82287
|
+
}
|
|
82288
|
+
const bridgeDeadRestartCause = bridgeDeadMarker != null ? {
|
|
82289
|
+
reason: bridgeDeadMarker.reason,
|
|
82290
|
+
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."
|
|
82291
|
+
} : undefined;
|
|
81342
82292
|
const pending2 = findLatestTurnIfInterrupted(turnsDb);
|
|
81343
82293
|
const selfAgent = process.env.SWITCHROOM_AGENT_NAME ?? "";
|
|
81344
82294
|
if (pending2 != null && selfAgent) {
|
|
@@ -81370,7 +82320,11 @@ try {
|
|
|
81370
82320
|
if (bootResumeKind === "resume") {
|
|
81371
82321
|
bootResumeInbound = {
|
|
81372
82322
|
agent: selfAgent,
|
|
81373
|
-
msg: buildResumeInterruptedInbound({
|
|
82323
|
+
msg: buildResumeInterruptedInbound({
|
|
82324
|
+
turn: pending2,
|
|
82325
|
+
subagents: interruptedSubagents,
|
|
82326
|
+
restartCause: bridgeDeadRestartCause
|
|
82327
|
+
})
|
|
81374
82328
|
};
|
|
81375
82329
|
} else if (bootResumeKind === "report") {
|
|
81376
82330
|
let idleMs = pending2.turn_key === timeoutTurnKey && markerAgeMs != null ? markerAgeMs : null;
|
|
@@ -81385,7 +82339,12 @@ try {
|
|
|
81385
82339
|
idleMs = Math.max(0, Date.now() - pending2.started_at);
|
|
81386
82340
|
bootResumeInbound = {
|
|
81387
82341
|
agent: selfAgent,
|
|
81388
|
-
msg: buildResumeWatchdogReportInbound({
|
|
82342
|
+
msg: buildResumeWatchdogReportInbound({
|
|
82343
|
+
turn: pending2,
|
|
82344
|
+
idleMs,
|
|
82345
|
+
subagents: interruptedSubagents,
|
|
82346
|
+
restartCause: bridgeDeadRestartCause
|
|
82347
|
+
})
|
|
81389
82348
|
};
|
|
81390
82349
|
} else if (bootResumeKind === "defer-loop" || bootResumeKind === "defer-suppressed") {
|
|
81391
82350
|
bootResumeInbound = {
|
|
@@ -81393,7 +82352,8 @@ try {
|
|
|
81393
82352
|
msg: buildResumeDeferredReportInbound({
|
|
81394
82353
|
turn: pending2,
|
|
81395
82354
|
reason: bootResumeKind === "defer-loop" ? "loop-guard" : "clean-restart-suppressed",
|
|
81396
|
-
subagents: interruptedSubagents
|
|
82355
|
+
subagents: interruptedSubagents,
|
|
82356
|
+
restartCause: bridgeDeadRestartCause
|
|
81397
82357
|
})
|
|
81398
82358
|
};
|
|
81399
82359
|
}
|
|
@@ -81406,6 +82366,29 @@ try {
|
|
|
81406
82366
|
}
|
|
81407
82367
|
if (bootResumeInbound != null) {
|
|
81408
82368
|
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}
|
|
82369
|
+
`);
|
|
82370
|
+
}
|
|
82371
|
+
}
|
|
82372
|
+
if (bridgeDeadMarker != null && bootResumeInbound == null && selfAgent) {
|
|
82373
|
+
const idleNoticeChat = (() => {
|
|
82374
|
+
try {
|
|
82375
|
+
return loadAccess().allowFrom[0] ?? null;
|
|
82376
|
+
} catch {
|
|
82377
|
+
return null;
|
|
82378
|
+
}
|
|
82379
|
+
})();
|
|
82380
|
+
if (idleNoticeChat != null) {
|
|
82381
|
+
bootResumeInbound = {
|
|
82382
|
+
agent: selfAgent,
|
|
82383
|
+
msg: buildBridgeDeadIdleNoticeInbound({
|
|
82384
|
+
chatId: String(idleNoticeChat),
|
|
82385
|
+
marker: bridgeDeadMarker
|
|
82386
|
+
})
|
|
82387
|
+
};
|
|
82388
|
+
process.stderr.write(`telegram gateway: boot: bridge-dead idle notice queued chat=${idleNoticeChat} (no turn was in flight)
|
|
82389
|
+
`);
|
|
82390
|
+
} else {
|
|
82391
|
+
process.stderr.write(`telegram gateway: boot: bridge-dead idle notice skipped \u2014 no allowFrom chat to surface it in
|
|
81409
82392
|
`);
|
|
81410
82393
|
}
|
|
81411
82394
|
}
|
|
@@ -81423,13 +82406,13 @@ try {
|
|
|
81423
82406
|
pending2.interrupt_reason != null ? `SWITCHROOM_PENDING_INTERRUPT_REASON=${pending2.interrupt_reason}` : `SWITCHROOM_PENDING_INTERRUPT_REASON=`
|
|
81424
82407
|
];
|
|
81425
82408
|
const pendingEnvTmp = `${pendingEnvPath}.tmp-${process.pid}`;
|
|
81426
|
-
|
|
82409
|
+
writeFileSync42(pendingEnvTmp, lines.join(`
|
|
81427
82410
|
`) + `
|
|
81428
82411
|
`, { mode: 384 });
|
|
81429
|
-
|
|
82412
|
+
renameSync17(pendingEnvTmp, pendingEnvPath);
|
|
81430
82413
|
process.stderr.write(`telegram gateway: pending-turn env written to ${pendingEnvPath} turnKey=${pending2.turn_key} endedVia=${pending2.ended_via ?? "open"}
|
|
81431
82414
|
`);
|
|
81432
|
-
} else if (
|
|
82415
|
+
} else if (existsSync50(pendingEnvPath)) {
|
|
81433
82416
|
rmSync5(pendingEnvPath, { force: true });
|
|
81434
82417
|
process.stderr.write(`telegram gateway: pending-turn env cleared (clean previous shutdown)
|
|
81435
82418
|
`);
|
|
@@ -81520,7 +82503,7 @@ if (!STATIC) {
|
|
|
81520
82503
|
function checkApprovals() {
|
|
81521
82504
|
let files;
|
|
81522
82505
|
try {
|
|
81523
|
-
files =
|
|
82506
|
+
files = readdirSync12(APPROVED_DIR);
|
|
81524
82507
|
} catch {
|
|
81525
82508
|
return;
|
|
81526
82509
|
}
|
|
@@ -81631,10 +82614,10 @@ function noteAgentOutputAt(key, ts) {
|
|
|
81631
82614
|
}
|
|
81632
82615
|
var OBLIGATION_STORE_PATH = join53(STATE_DIR, "obligations.json");
|
|
81633
82616
|
var obligationStoreFs = {
|
|
81634
|
-
readFileSync: (p) =>
|
|
81635
|
-
writeFileSync: (p, d) =>
|
|
81636
|
-
renameSync: (a, b) =>
|
|
81637
|
-
existsSync: (p) =>
|
|
82617
|
+
readFileSync: (p) => readFileSync53(p, "utf8"),
|
|
82618
|
+
writeFileSync: (p, d) => writeFileSync42(p, d),
|
|
82619
|
+
renameSync: (a, b) => renameSync17(a, b),
|
|
82620
|
+
existsSync: (p) => existsSync50(p)
|
|
81638
82621
|
};
|
|
81639
82622
|
var obligationLedger = new ObligationLedger(OBLIGATION_REPRESENT_MAX, {
|
|
81640
82623
|
onChange: STATIC || !OBLIGATION_LEDGER_ENABLED ? undefined : (snapshot) => persistObligations(OBLIGATION_STORE_PATH, obligationStoreFs, snapshot)
|
|
@@ -81741,6 +82724,8 @@ async function deliverButtonTapInbound(agent, inbound) {
|
|
|
81741
82724
|
return "buffered-bridge-offline";
|
|
81742
82725
|
}
|
|
81743
82726
|
var pendingRestarts = new Map;
|
|
82727
|
+
var pendingSessionCommand = createPendingSessionCommandSlots();
|
|
82728
|
+
var PENDING_CMD_DRAIN_CAP_MS = 60000;
|
|
81744
82729
|
var lastSessionActiveFile = null;
|
|
81745
82730
|
var compactState = initialCompactState();
|
|
81746
82731
|
var compactDispatching = false;
|
|
@@ -82072,6 +83057,119 @@ async function fireDeferredInterrupt(reason) {
|
|
|
82072
83057
|
`);
|
|
82073
83058
|
}
|
|
82074
83059
|
}
|
|
83060
|
+
var haltBoundaryWaiters = new Set;
|
|
83061
|
+
function notifyHaltBoundaryWaiters() {
|
|
83062
|
+
if (haltBoundaryWaiters.size === 0)
|
|
83063
|
+
return;
|
|
83064
|
+
for (const check of [...haltBoundaryWaiters])
|
|
83065
|
+
check();
|
|
83066
|
+
}
|
|
83067
|
+
function waitForSafeBoundary(maxWaitMs) {
|
|
83068
|
+
if (!toolFlightTracker.isMidToolCall())
|
|
83069
|
+
return Promise.resolve("boundary");
|
|
83070
|
+
return new Promise((resolve11) => {
|
|
83071
|
+
let done = false;
|
|
83072
|
+
const finish = (reason) => {
|
|
83073
|
+
if (done)
|
|
83074
|
+
return;
|
|
83075
|
+
done = true;
|
|
83076
|
+
clearTimeout(timer3);
|
|
83077
|
+
haltBoundaryWaiters.delete(check);
|
|
83078
|
+
resolve11(reason);
|
|
83079
|
+
};
|
|
83080
|
+
const timer3 = setTimeout(() => finish("timeout"), maxWaitMs);
|
|
83081
|
+
const check = () => {
|
|
83082
|
+
if (!toolFlightTracker.isMidToolCall())
|
|
83083
|
+
finish("boundary");
|
|
83084
|
+
};
|
|
83085
|
+
haltBoundaryWaiters.add(check);
|
|
83086
|
+
});
|
|
83087
|
+
}
|
|
83088
|
+
function cancelPendingPermissionsForHalt(origin) {
|
|
83089
|
+
if (pendingPermissions.size === 0)
|
|
83090
|
+
return;
|
|
83091
|
+
for (const [requestId, details] of pendingPermissions) {
|
|
83092
|
+
dispatchPermissionVerdict({
|
|
83093
|
+
type: "permission",
|
|
83094
|
+
requestId,
|
|
83095
|
+
behavior: "deny",
|
|
83096
|
+
message: "Cancelled \u2014 the operator stopped this turn. Do not retry."
|
|
83097
|
+
});
|
|
83098
|
+
stripCancelledPermissionCards(details.card_text, details.cards);
|
|
83099
|
+
pendingPermissions.delete(requestId);
|
|
83100
|
+
permCardStore.remove(requestId);
|
|
83101
|
+
process.stderr.write(`telegram gateway: halt-now cancelled pending permission origin=${origin} request=${requestId} tool=${details.tool_name}
|
|
83102
|
+
`);
|
|
83103
|
+
}
|
|
83104
|
+
}
|
|
83105
|
+
async function stripCancelledPermissionCards(cardText, cards) {
|
|
83106
|
+
for (const edit of buildCancelledCardEdits(cardText, cards)) {
|
|
83107
|
+
await swallowingApiCall(() => bot.api.editMessageText(edit.chatId, edit.messageId, richMessage2(edit.text), {}), { chat_id: edit.chatId, verb: "permission_halt.strip" });
|
|
83108
|
+
}
|
|
83109
|
+
}
|
|
83110
|
+
async function executeHaltNow(origin) {
|
|
83111
|
+
const agentName3 = process.env.SWITCHROOM_AGENT_NAME;
|
|
83112
|
+
if (!agentName3)
|
|
83113
|
+
return;
|
|
83114
|
+
const haltTarget = currentTurn;
|
|
83115
|
+
const haltTargetKey = haltTarget?.registryKey ?? null;
|
|
83116
|
+
const access = loadAccess();
|
|
83117
|
+
const timing = decideInterruptTiming({
|
|
83118
|
+
safeBoundaryEnabled: resolveSafeBoundaryEnabled(access.interruptSafeBoundary),
|
|
83119
|
+
midToolCall: toolFlightTracker.isMidToolCall()
|
|
83120
|
+
});
|
|
83121
|
+
const startedAt = Date.now();
|
|
83122
|
+
if (timing === "defer") {
|
|
83123
|
+
const maxWaitMs = resolveInterruptMaxWaitMs(access.interruptMaxWaitMs);
|
|
83124
|
+
const reason = await waitForSafeBoundary(maxWaitMs);
|
|
83125
|
+
process.stderr.write(`telegram gateway: halt-now boundary-wait origin=${origin} reason=${reason} waited_ms=${Date.now() - startedAt} in_flight=${toolFlightTracker.inFlightCount()}
|
|
83126
|
+
`);
|
|
83127
|
+
}
|
|
83128
|
+
if (currentTurn !== haltTarget) {
|
|
83129
|
+
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"}
|
|
83130
|
+
`);
|
|
83131
|
+
return;
|
|
83132
|
+
}
|
|
83133
|
+
try {
|
|
83134
|
+
const { sendAgentInterrupt: sendAgentInterrupt2 } = await Promise.resolve().then(() => (init_tmux(), exports_tmux));
|
|
83135
|
+
const r = sendAgentInterrupt2({ agentName: agentName3 });
|
|
83136
|
+
if ("ok" in r) {
|
|
83137
|
+
process.stderr.write(`telegram gateway: halt-now SIGINT delivered via tmux send-keys origin=${origin} agent=${agentName3}
|
|
83138
|
+
`);
|
|
83139
|
+
} else {
|
|
83140
|
+
process.stderr.write(`telegram gateway: halt-now SIGINT via tmux failed origin=${origin} agent=${agentName3}: ${r.error}
|
|
83141
|
+
`);
|
|
83142
|
+
}
|
|
83143
|
+
} catch (err) {
|
|
83144
|
+
process.stderr.write(`telegram gateway: halt-now SIGINT failed origin=${origin}: ${err.message}
|
|
83145
|
+
`);
|
|
83146
|
+
}
|
|
83147
|
+
cancelInterruptedObligation();
|
|
83148
|
+
cancelPendingPermissionsForHalt(origin);
|
|
83149
|
+
const turn = currentTurn;
|
|
83150
|
+
if (turn != null) {
|
|
83151
|
+
if (turnsDb != null && turn.registryKey != null) {
|
|
83152
|
+
const _turnKey = turn.registryKey;
|
|
83153
|
+
try {
|
|
83154
|
+
recordTurnEnd(turnsDb, {
|
|
83155
|
+
turnKey: _turnKey,
|
|
83156
|
+
endedVia: "stop",
|
|
83157
|
+
lastAssistantMsgId: turn.lastAssistantMsgId,
|
|
83158
|
+
lastAssistantDone: turn.lastAssistantDone,
|
|
83159
|
+
toolCallCount: turn.toolCallCount
|
|
83160
|
+
});
|
|
83161
|
+
} catch (err) {
|
|
83162
|
+
process.stderr.write(`telegram gateway: recordTurnEnd(halt) failed turnKey=${_turnKey}: ${err.message}
|
|
83163
|
+
`);
|
|
83164
|
+
}
|
|
83165
|
+
}
|
|
83166
|
+
const key = statusKey(turn.sessionChatId, turn.sessionThreadId);
|
|
83167
|
+
endCurrentTurnAtomic(turn);
|
|
83168
|
+
releaseTurnBufferGate(key, turn);
|
|
83169
|
+
}
|
|
83170
|
+
if (!turnInFlightForGate())
|
|
83171
|
+
drainPendingSessionCommand();
|
|
83172
|
+
}
|
|
82075
83173
|
var preambleSuppressor = new PreambleSuppressor({
|
|
82076
83174
|
emitAnswer: (cumulative) => {
|
|
82077
83175
|
const stream = currentTurn?.answerStream ?? null;
|
|
@@ -82194,6 +83292,7 @@ function purgeReactionTracking(key, endingTurn) {
|
|
|
82194
83292
|
}
|
|
82195
83293
|
drainBufferedIfAllowed(endingTurn, "turn-complete");
|
|
82196
83294
|
if (!turnInFlightForGate()) {
|
|
83295
|
+
drainPendingSessionCommand();
|
|
82197
83296
|
if (pendingRestarts.size > 0) {
|
|
82198
83297
|
for (const [agentName3, _timestamp] of pendingRestarts.entries()) {
|
|
82199
83298
|
triggerSelfRestart(agentName3, "turn-complete-pending-restart");
|
|
@@ -82258,7 +83357,7 @@ function emitTurnRecord(turn, endedAt) {
|
|
|
82258
83357
|
return;
|
|
82259
83358
|
}
|
|
82260
83359
|
},
|
|
82261
|
-
rename: (from, to) =>
|
|
83360
|
+
rename: (from, to) => renameSync17(from, to)
|
|
82262
83361
|
});
|
|
82263
83362
|
appendFileSync6(turnsPath, rec);
|
|
82264
83363
|
} catch {}
|
|
@@ -83357,6 +84456,17 @@ var pendingStateReaper = setInterval(() => {
|
|
|
83357
84456
|
triggerSelfRestart(agentName3, "restart-drain-cap-forced", 100);
|
|
83358
84457
|
}
|
|
83359
84458
|
}
|
|
84459
|
+
const queuedCmds = pendingSessionCommand.list();
|
|
84460
|
+
const cmdDecision = drainCapDecision(queuedCmds, now, PENDING_CMD_DRAIN_CAP_MS, turnInFlightForGate());
|
|
84461
|
+
if (cmdDecision === "defer-turn-in-flight") {
|
|
84462
|
+
process.stderr.write(`telegram gateway: [pending-cmd-drain] deferred reason=turn-in-flight queued=${queuedCmds.map((c) => c.kind).join(",")}
|
|
84463
|
+
`);
|
|
84464
|
+
} else if (cmdDecision === "force") {
|
|
84465
|
+
const oldest = Math.min(...queuedCmds.map((c) => c.requestedAt));
|
|
84466
|
+
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
|
|
84467
|
+
`);
|
|
84468
|
+
drainPendingSessionCommand();
|
|
84469
|
+
}
|
|
83360
84470
|
}, 60000);
|
|
83361
84471
|
pendingStateReaper.unref();
|
|
83362
84472
|
function looksLikeAuthCode(text5) {
|
|
@@ -83410,6 +84520,7 @@ function emitGatewayOperatorEvent(event) {
|
|
|
83410
84520
|
const modelUnavailable = resolveModelUnavailableFromOperatorEvent(event);
|
|
83411
84521
|
let renderedText;
|
|
83412
84522
|
let renderedKeyboard;
|
|
84523
|
+
let cardPromisedFallback = false;
|
|
83413
84524
|
if (modelUnavailable) {
|
|
83414
84525
|
const isAutoKind = modelUnavailable.kind === "quota_exhausted";
|
|
83415
84526
|
const willActuallyFire = isAutoKind && wouldFireFleetAutoFallback();
|
|
@@ -83419,6 +84530,7 @@ function emitGatewayOperatorEvent(event) {
|
|
|
83419
84530
|
autoFallbackInFlight: willActuallyFire
|
|
83420
84531
|
});
|
|
83421
84532
|
renderedKeyboard = undefined;
|
|
84533
|
+
cardPromisedFallback = willActuallyFire;
|
|
83422
84534
|
if (willActuallyFire) {
|
|
83423
84535
|
const untilMs = resolveExhaustUntil(modelUnavailable.resetAt?.getTime());
|
|
83424
84536
|
fireFleetAutoFallback(agent, untilMs);
|
|
@@ -83450,7 +84562,16 @@ function emitGatewayOperatorEvent(event) {
|
|
|
83450
84562
|
...renderedKeyboard ? { reply_markup: renderedKeyboard } : {},
|
|
83451
84563
|
...opEventThread != null ? { message_thread_id: opEventThread } : {}
|
|
83452
84564
|
};
|
|
83453
|
-
bot.api.sendRichMessage(chat_id, richMessage2(renderedText), opts).
|
|
84565
|
+
bot.api.sendRichMessage(chat_id, richMessage2(renderedText), opts).then((sent) => {
|
|
84566
|
+
if (cardPromisedFallback) {
|
|
84567
|
+
modelUnavailableCardRegistry.record(String(chat_id), {
|
|
84568
|
+
messageId: sent.message_id,
|
|
84569
|
+
text: renderedText,
|
|
84570
|
+
atMs: Date.now(),
|
|
84571
|
+
promisedFallback: true
|
|
84572
|
+
});
|
|
84573
|
+
}
|
|
84574
|
+
}).catch((e) => {
|
|
83454
84575
|
process.stderr.write(`telegram gateway: operator-event send to ${chat_id} failed agent=${agent} kind=${kind}: ${e}
|
|
83455
84576
|
`);
|
|
83456
84577
|
});
|
|
@@ -83512,18 +84633,18 @@ var statusPinChatIds = new Map;
|
|
|
83512
84633
|
var statusPinPinnedAt = new Map;
|
|
83513
84634
|
var STATUS_PIN_STORE_PATH = join53(STATE_DIR, "status-pins.json");
|
|
83514
84635
|
var statusPinStoreFs = {
|
|
83515
|
-
readFileSync: (p) =>
|
|
83516
|
-
writeFileSync: (p, d) =>
|
|
83517
|
-
renameSync: (a, b) =>
|
|
83518
|
-
existsSync: (p) =>
|
|
84636
|
+
readFileSync: (p) => readFileSync53(p, "utf8"),
|
|
84637
|
+
writeFileSync: (p, d) => writeFileSync42(p, d),
|
|
84638
|
+
renameSync: (a, b) => renameSync17(a, b),
|
|
84639
|
+
existsSync: (p) => existsSync50(p)
|
|
83519
84640
|
};
|
|
83520
84641
|
var statusPinPersistEnabled = !STATIC && PIN_STATUS_WHILE_WORKING;
|
|
83521
84642
|
var ACTIVITY_CARD_STORE_PATH = join53(STATE_DIR, "activity-cards-pending.json");
|
|
83522
84643
|
var activityCardStoreFs = {
|
|
83523
|
-
readFileSync: (p) =>
|
|
83524
|
-
writeFileSync: (p, d) =>
|
|
83525
|
-
renameSync: (a, b) =>
|
|
83526
|
-
existsSync: (p) =>
|
|
84644
|
+
readFileSync: (p) => readFileSync53(p, "utf8"),
|
|
84645
|
+
writeFileSync: (p, d) => writeFileSync42(p, d),
|
|
84646
|
+
renameSync: (a, b) => renameSync17(a, b),
|
|
84647
|
+
existsSync: (p) => existsSync50(p)
|
|
83527
84648
|
};
|
|
83528
84649
|
var activityCardPersistEnabled = !STATIC;
|
|
83529
84650
|
var bannerPinPersistEnabled = !STATIC;
|
|
@@ -84167,10 +85288,10 @@ var inboundSpool = STATIC ? undefined : createInboundSpool({
|
|
|
84167
85288
|
path: join53(STATE_DIR, "inbound-spool.jsonl"),
|
|
84168
85289
|
fs: {
|
|
84169
85290
|
appendFileSync: (p, d) => appendFileSync6(p, d),
|
|
84170
|
-
readFileSync: (p) =>
|
|
84171
|
-
writeFileSync: (p, d) =>
|
|
84172
|
-
renameSync: (a, b) =>
|
|
84173
|
-
existsSync: (p) =>
|
|
85291
|
+
readFileSync: (p) => readFileSync53(p, "utf8"),
|
|
85292
|
+
writeFileSync: (p, d) => writeFileSync42(p, d),
|
|
85293
|
+
renameSync: (a, b) => renameSync17(a, b),
|
|
85294
|
+
existsSync: (p) => existsSync50(p),
|
|
84174
85295
|
statSizeSync: (p) => statSync16(p).size
|
|
84175
85296
|
},
|
|
84176
85297
|
onDegraded: (info) => {
|
|
@@ -84351,6 +85472,27 @@ async function stripStalePermissionCard(card) {
|
|
|
84351
85472
|
`);
|
|
84352
85473
|
}
|
|
84353
85474
|
}
|
|
85475
|
+
var BRIDGE_DEAD_ESCALATION_ENABLED = process.env.SWITCHROOM_BRIDGE_DEAD_ESCALATION !== "0";
|
|
85476
|
+
var BRIDGE_DEAD_GRACE_MS = (() => {
|
|
85477
|
+
const v = Number(process.env.SWITCHROOM_BRIDGE_DEAD_GRACE_MS);
|
|
85478
|
+
return Number.isFinite(v) && v > 0 ? v : DEFAULT_BRIDGE_DEAD_GRACE_MS;
|
|
85479
|
+
})();
|
|
85480
|
+
var bridgeDeadWatchdog = createBridgeDeadWatchdog({
|
|
85481
|
+
graceMs: BRIDGE_DEAD_GRACE_MS,
|
|
85482
|
+
isSessionAlive: () => findAgentProcessInContainer2()?.comm === "claude",
|
|
85483
|
+
isShuttingDown: () => shuttingDown,
|
|
85484
|
+
escalate: (reason) => triggerSelfRestart(process.env.SWITCHROOM_AGENT_NAME ?? "", reason, 1500),
|
|
85485
|
+
crashLogPath: join53(STATE_DIR, "bridge-crash.log"),
|
|
85486
|
+
markerPath: join53(STATE_DIR, "bridge-dead-escalation.json"),
|
|
85487
|
+
log: (line) => process.stderr.write(`${line}
|
|
85488
|
+
`),
|
|
85489
|
+
priorStreak: bridgeDeadPriorStreak
|
|
85490
|
+
});
|
|
85491
|
+
if (BRIDGE_DEAD_ESCALATION_ENABLED) {
|
|
85492
|
+
bridgeDeadWatchdog.arm();
|
|
85493
|
+
process.stderr.write(`telegram gateway: [bridge-dead-watchdog] armed (grace=${BRIDGE_DEAD_GRACE_MS}ms)
|
|
85494
|
+
`);
|
|
85495
|
+
}
|
|
84354
85496
|
var ipcServer = createIpcServer({
|
|
84355
85497
|
socketPath: SOCKET_PATH,
|
|
84356
85498
|
onClientRegistered(client3) {
|
|
@@ -84367,6 +85509,7 @@ var ipcServer = createIpcServer({
|
|
|
84367
85509
|
return;
|
|
84368
85510
|
}
|
|
84369
85511
|
const bridgeUpEffects = client3.agentName != null ? shadowEmit({ kind: "bridgeUp", at: Date.now() }) : [];
|
|
85512
|
+
bridgeDeadWatchdog.noteBridgeRegistered(client3.agentName);
|
|
84370
85513
|
client3.send({ type: "status", status: "agent_connected" });
|
|
84371
85514
|
if (client3.agentName != null) {
|
|
84372
85515
|
if (isDispatchEnabled()) {
|
|
@@ -84498,6 +85641,8 @@ var ipcServer = createIpcServer({
|
|
|
84498
85641
|
process.stderr.write(`telegram gateway: bridge disconnected \u2014 agent=${client3.agentName}
|
|
84499
85642
|
`);
|
|
84500
85643
|
shadowEmit({ kind: "bridgeDown", at: Date.now() });
|
|
85644
|
+
if (BRIDGE_DEAD_ESCALATION_ENABLED)
|
|
85645
|
+
bridgeDeadWatchdog.noteBridgeDisconnected(client3.agentName);
|
|
84501
85646
|
}
|
|
84502
85647
|
flushOnAgentDisconnect({
|
|
84503
85648
|
agentName: client3.agentName,
|
|
@@ -84552,6 +85697,7 @@ var ipcServer = createIpcServer({
|
|
|
84552
85697
|
if (pendingDeferredInterrupt != null && !toolFlightTracker.isMidToolCall()) {
|
|
84553
85698
|
fireDeferredInterrupt("boundary");
|
|
84554
85699
|
}
|
|
85700
|
+
notifyHaltBoundaryWaiters();
|
|
84555
85701
|
if (currentTurn != null) {
|
|
84556
85702
|
const key = statusKey(currentTurn.sessionChatId, currentTurn.sessionThreadId);
|
|
84557
85703
|
if (ev.kind === "thinking") {
|
|
@@ -85948,20 +87094,58 @@ ${url}`;
|
|
|
85948
87094
|
turn2.silentAnchorText = effectiveText;
|
|
85949
87095
|
}
|
|
85950
87096
|
}
|
|
85951
|
-
const
|
|
87097
|
+
const photoPrecheck = new Map;
|
|
87098
|
+
const documentReroutes = [];
|
|
87099
|
+
const sentAsDocument = new Set;
|
|
87100
|
+
for (const f of files) {
|
|
87101
|
+
if (!PHOTO_EXTS.has(extname(f).toLowerCase()))
|
|
87102
|
+
continue;
|
|
87103
|
+
const cls = classifyPhotoFile(f);
|
|
87104
|
+
photoPrecheck.set(f, cls);
|
|
87105
|
+
if (cls.route === "document") {
|
|
87106
|
+
documentReroutes.push({ path: f, reason: cls.reason });
|
|
87107
|
+
sentAsDocument.add(f);
|
|
87108
|
+
process.stderr.write(`telegram gateway: photo-precheck rerouting ${f} as document (${cls.reason})
|
|
87109
|
+
`);
|
|
87110
|
+
}
|
|
87111
|
+
}
|
|
87112
|
+
const sendableAsPhoto = (f) => PHOTO_EXTS.has(extname(f).toLowerCase()) && photoPrecheck.get(f)?.route !== "document";
|
|
87113
|
+
const allPhotos = files.length >= 2 && files.length <= 10 && files.every(sendableAsPhoto);
|
|
85952
87114
|
const replyParams = reply_to != null && replyMode !== "off" ? { reply_parameters: { message_id: reply_to } } : {};
|
|
87115
|
+
const sendAsDocument = (f) => retryWithThreadFallback2(robustApiCall, (tid) => {
|
|
87116
|
+
const baseOpts = {
|
|
87117
|
+
...replyParams,
|
|
87118
|
+
...tid != null ? { message_thread_id: tid } : {}
|
|
87119
|
+
};
|
|
87120
|
+
return lockedBot.api.sendDocument(chat_id, new import_grammy13.InputFile(f), baseOpts);
|
|
87121
|
+
}, { threadId, chat_id, verb: "sendDocument" });
|
|
85953
87122
|
if (allPhotos) {
|
|
85954
87123
|
const media = files.map((f) => ({
|
|
85955
87124
|
type: "photo",
|
|
85956
87125
|
media: new import_grammy13.InputFile(f)
|
|
85957
87126
|
}));
|
|
85958
|
-
|
|
85959
|
-
|
|
85960
|
-
|
|
85961
|
-
|
|
85962
|
-
|
|
85963
|
-
|
|
85964
|
-
|
|
87127
|
+
let sent;
|
|
87128
|
+
try {
|
|
87129
|
+
sent = await retryWithThreadFallback2(robustApiCall, (tid) => {
|
|
87130
|
+
const baseOpts = {
|
|
87131
|
+
...replyParams,
|
|
87132
|
+
...tid != null ? { message_thread_id: tid } : {}
|
|
87133
|
+
};
|
|
87134
|
+
return lockedBot.api.sendMediaGroup(chat_id, media, baseOpts);
|
|
87135
|
+
}, { threadId, chat_id, verb: "sendMediaGroup" });
|
|
87136
|
+
} catch (err) {
|
|
87137
|
+
if (!isPhotoDimensionRejectError(err))
|
|
87138
|
+
throw err;
|
|
87139
|
+
process.stderr.write(`telegram gateway: sendMediaGroup rejected the photo album (${err instanceof Error ? err.message : String(err)}); falling back to per-file sendDocument
|
|
87140
|
+
`);
|
|
87141
|
+
sent = [];
|
|
87142
|
+
const albumReason = "Telegram rejected the photo album; whole album re-sent as documents";
|
|
87143
|
+
for (const f of files) {
|
|
87144
|
+
sent.push(await sendAsDocument(f));
|
|
87145
|
+
sentAsDocument.add(f);
|
|
87146
|
+
documentReroutes.push({ path: f, reason: albumReason });
|
|
87147
|
+
}
|
|
87148
|
+
}
|
|
85965
87149
|
if (threadId != null) {
|
|
85966
87150
|
const first = sent[0];
|
|
85967
87151
|
if (first && first.message_thread_id == null)
|
|
@@ -85971,23 +87155,33 @@ ${url}`;
|
|
|
85971
87155
|
sentIds.push(m.message_id);
|
|
85972
87156
|
} else {
|
|
85973
87157
|
for (const f of files) {
|
|
85974
|
-
const ext = extname(f).toLowerCase();
|
|
85975
87158
|
const input = new import_grammy13.InputFile(f);
|
|
85976
|
-
const isPhoto =
|
|
85977
|
-
|
|
85978
|
-
|
|
85979
|
-
|
|
85980
|
-
|
|
85981
|
-
|
|
85982
|
-
|
|
85983
|
-
|
|
87159
|
+
const isPhoto = sendableAsPhoto(f);
|
|
87160
|
+
let sent;
|
|
87161
|
+
try {
|
|
87162
|
+
sent = await retryWithThreadFallback2(robustApiCall, (tid) => {
|
|
87163
|
+
const baseOpts = {
|
|
87164
|
+
...replyParams,
|
|
87165
|
+
...tid != null ? { message_thread_id: tid } : {}
|
|
87166
|
+
};
|
|
87167
|
+
return isPhoto ? lockedBot.api.sendPhoto(chat_id, input, baseOpts) : lockedBot.api.sendDocument(chat_id, input, baseOpts);
|
|
87168
|
+
}, { threadId, chat_id, verb: isPhoto ? "sendPhoto" : "sendDocument" });
|
|
87169
|
+
} catch (err) {
|
|
87170
|
+
if (!(isPhoto && isPhotoDimensionRejectError(err)))
|
|
87171
|
+
throw err;
|
|
87172
|
+
process.stderr.write(`telegram gateway: sendPhoto rejected ${f} (${err instanceof Error ? err.message : String(err)}); falling back to sendDocument
|
|
87173
|
+
`);
|
|
87174
|
+
sent = await sendAsDocument(f);
|
|
87175
|
+
sentAsDocument.add(f);
|
|
87176
|
+
documentReroutes.push({ path: f, reason: "Telegram rejected it as a photo; re-sent as document" });
|
|
87177
|
+
}
|
|
85984
87178
|
if (threadId != null && sent.message_thread_id == null) {
|
|
85985
87179
|
threadId = undefined;
|
|
85986
87180
|
}
|
|
85987
87181
|
sentIds.push(sent.message_id);
|
|
85988
87182
|
}
|
|
85989
87183
|
}
|
|
85990
|
-
const result = sentIds.length === 1 ? `sent (id: ${sentIds[0]})` : `sent ${sentIds.length} parts (ids: ${sentIds.join(", ")})
|
|
87184
|
+
const result = (sentIds.length === 1 ? `sent (id: ${sentIds[0]})` : `sent ${sentIds.length} parts (ids: ${sentIds.join(", ")})`) + rerouteResultSuffix(documentReroutes);
|
|
85991
87185
|
if (HISTORY_ENABLED && sentIds.length > 0) {
|
|
85992
87186
|
try {
|
|
85993
87187
|
const fileCount = files.length;
|
|
@@ -85999,9 +87193,11 @@ ${url}`;
|
|
|
85999
87193
|
attachKinds.push(null);
|
|
86000
87194
|
}
|
|
86001
87195
|
for (let i = 0;i < fileCount; i++) {
|
|
86002
|
-
const
|
|
86003
|
-
|
|
86004
|
-
|
|
87196
|
+
const f = files[i] ?? "";
|
|
87197
|
+
const ext = extname(f).toLowerCase();
|
|
87198
|
+
const kind = PHOTO_EXTS.has(ext) && !sentAsDocument.has(f) ? "photo" : "document";
|
|
87199
|
+
texts.push(`(${kind}: ${f})`);
|
|
87200
|
+
attachKinds.push(kind);
|
|
86005
87201
|
}
|
|
86006
87202
|
recordOutbound({ chat_id, thread_id: threadId ?? null, message_ids: sentIds, texts, attachment_kinds: attachKinds });
|
|
86007
87203
|
} catch (err) {
|
|
@@ -86262,8 +87458,8 @@ async function publishToTelegraph(text5, shortName, authorName) {
|
|
|
86262
87458
|
const accountPath = join53(STATE_DIR, "telegraph-account.json");
|
|
86263
87459
|
let account = null;
|
|
86264
87460
|
try {
|
|
86265
|
-
if (
|
|
86266
|
-
const raw =
|
|
87461
|
+
if (existsSync50(accountPath)) {
|
|
87462
|
+
const raw = readFileSync53(accountPath, "utf-8");
|
|
86267
87463
|
const parsed = JSON.parse(raw);
|
|
86268
87464
|
if (parsed.shortName && parsed.accessToken) {
|
|
86269
87465
|
account = parsed;
|
|
@@ -86283,7 +87479,7 @@ async function publishToTelegraph(text5, shortName, authorName) {
|
|
|
86283
87479
|
account = created.value;
|
|
86284
87480
|
try {
|
|
86285
87481
|
mkdirSync39(STATE_DIR, { recursive: true, mode: 448 });
|
|
86286
|
-
|
|
87482
|
+
writeFileSync42(accountPath, JSON.stringify(account, null, 2), { mode: 384 });
|
|
86287
87483
|
} catch (err) {
|
|
86288
87484
|
process.stderr.write(`telegram gateway: telegraph cache write failed: ${err.message}
|
|
86289
87485
|
`);
|
|
@@ -86723,7 +87919,7 @@ async function executeVaultRequestAccess(args) {
|
|
|
86723
87919
|
}
|
|
86724
87920
|
function readLiveSwitchroomConfigText() {
|
|
86725
87921
|
const cfgPath = process.env.SWITCHROOM_CONFIG ?? findConfigFile2();
|
|
86726
|
-
return
|
|
87922
|
+
return readFileSync53(cfgPath, "utf8");
|
|
86727
87923
|
}
|
|
86728
87924
|
var MENTAL_MODEL_NAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
|
|
86729
87925
|
function buildMentalModelProposeKeyboard(stageId) {
|
|
@@ -86894,7 +88090,7 @@ async function executeDownloadAttachment(args) {
|
|
|
86894
88090
|
});
|
|
86895
88091
|
mkdirSync39(INBOX_DIR, { recursive: true, mode: 448 });
|
|
86896
88092
|
assertInsideInbox(INBOX_DIR, dlPath);
|
|
86897
|
-
|
|
88093
|
+
writeFileSync42(dlPath, buf, { mode: 384 });
|
|
86898
88094
|
return { content: [{ type: "text", text: dlPath }] };
|
|
86899
88095
|
}
|
|
86900
88096
|
async function executeEditMessage(args) {
|
|
@@ -88401,6 +89597,9 @@ async function handleInboundCoalesced(ctx, text5, downloadImage, attachment) {
|
|
|
88401
89597
|
if (parseInterruptMarker(text5).isInterrupt) {
|
|
88402
89598
|
return handleInbound(ctx, text5, downloadImage, attachment);
|
|
88403
89599
|
}
|
|
89600
|
+
if (parseStopKeyword(text5) && downloadImage == null && attachment == null) {
|
|
89601
|
+
return handleInbound(ctx, text5, downloadImage, attachment);
|
|
89602
|
+
}
|
|
88404
89603
|
const hasAttachment = downloadImage != null || attachment != null;
|
|
88405
89604
|
const maxAttachments = coalesceMaxAttachments();
|
|
88406
89605
|
if (hasAttachment && ctx.message?.media_group_id != null && maxAttachments <= 1) {
|
|
@@ -88509,6 +89708,27 @@ async function handleInbound(ctx, text5, downloadImage, attachment, extraAttachm
|
|
|
88509
89708
|
process.stderr.write(`telegram gateway: inbound classifier error: ${err.message}
|
|
88510
89709
|
`);
|
|
88511
89710
|
}
|
|
89711
|
+
if (parseStopKeyword(text5) && downloadImage == null && attachment == null && (extraAttachments == null || extraAttachments.length === 0)) {
|
|
89712
|
+
const queuedLabels = pendingSessionCommand.list().map((c) => `/${c.kind} ${c.targetLabel}`);
|
|
89713
|
+
const inFlight = turnInFlightForGate();
|
|
89714
|
+
process.stderr.write(`telegram gateway: stop-keyword received chat_id=${chat_id} in_flight_turn=${inFlight} queued_cmds=${queuedLabels.length}
|
|
89715
|
+
`);
|
|
89716
|
+
if (inFlight) {
|
|
89717
|
+
if (msgId != null) {
|
|
89718
|
+
bot.api.setMessageReaction(chat_id, msgId, [
|
|
89719
|
+
{ type: "emoji", emoji: "\u26A1" }
|
|
89720
|
+
]).catch(() => {});
|
|
89721
|
+
}
|
|
89722
|
+
await executeHaltNow("stop-keyword");
|
|
89723
|
+
}
|
|
89724
|
+
const stopReply = buildStopReply(inFlight, queuedLabels);
|
|
89725
|
+
await swallowingApiCall(() => bot.api.sendMessage(chat_id, stopReply.text, messageThreadId != null ? { message_thread_id: messageThreadId } : {}), {
|
|
89726
|
+
chat_id,
|
|
89727
|
+
verb: "stop-keyword-reply",
|
|
89728
|
+
...messageThreadId != null ? { threadId: messageThreadId } : {}
|
|
89729
|
+
});
|
|
89730
|
+
return;
|
|
89731
|
+
}
|
|
88512
89732
|
const interrupt = parseInterruptMarker(text5);
|
|
88513
89733
|
let deferInterrupt = false;
|
|
88514
89734
|
if (interrupt.isInterrupt) {
|
|
@@ -88525,6 +89745,15 @@ async function handleInbound(ctx, text5, downloadImage, attachment, extraAttachm
|
|
|
88525
89745
|
{ type: "emoji", emoji: "\u26A1" }
|
|
88526
89746
|
]).catch(() => {});
|
|
88527
89747
|
}
|
|
89748
|
+
if (interrupt.emptyBody) {
|
|
89749
|
+
await executeHaltNow("bang-empty");
|
|
89750
|
+
await swallowingApiCall(() => bot.api.sendMessage(chat_id, "\u26A1 Interrupted. Send your replacement instruction now.", messageThreadId != null ? { message_thread_id: messageThreadId } : {}), {
|
|
89751
|
+
chat_id,
|
|
89752
|
+
verb: "interrupt-empty-body",
|
|
89753
|
+
...messageThreadId != null ? { threadId: messageThreadId } : {}
|
|
89754
|
+
});
|
|
89755
|
+
return;
|
|
89756
|
+
}
|
|
88528
89757
|
if (agentName3 && !deferInterrupt) {
|
|
88529
89758
|
try {
|
|
88530
89759
|
const { sendAgentInterrupt: sendAgentInterrupt2 } = await Promise.resolve().then(() => (init_tmux(), exports_tmux));
|
|
@@ -88542,14 +89771,6 @@ async function handleInbound(ctx, text5, downloadImage, attachment, extraAttachm
|
|
|
88542
89771
|
}
|
|
88543
89772
|
cancelInterruptedObligation();
|
|
88544
89773
|
}
|
|
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
89774
|
text5 = interrupt.body;
|
|
88554
89775
|
}
|
|
88555
89776
|
if (STATUS_QUERY_RE.test(text5)) {
|
|
@@ -89420,7 +90641,7 @@ function writeRestartMarker(marker) {
|
|
|
89420
90641
|
if (!p)
|
|
89421
90642
|
return;
|
|
89422
90643
|
try {
|
|
89423
|
-
|
|
90644
|
+
writeFileSync42(p, JSON.stringify(marker));
|
|
89424
90645
|
lastPlannedRestartAt = Date.now();
|
|
89425
90646
|
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
90647
|
`);
|
|
@@ -89439,7 +90660,7 @@ function readRestartMarker() {
|
|
|
89439
90660
|
if (!p)
|
|
89440
90661
|
return null;
|
|
89441
90662
|
try {
|
|
89442
|
-
return JSON.parse(
|
|
90663
|
+
return JSON.parse(readFileSync53(p, "utf8"));
|
|
89443
90664
|
} catch {
|
|
89444
90665
|
return null;
|
|
89445
90666
|
}
|
|
@@ -89588,7 +90809,7 @@ var _dockerReachable;
|
|
|
89588
90809
|
function isDockerReachable() {
|
|
89589
90810
|
if (_dockerReachable !== undefined)
|
|
89590
90811
|
return _dockerReachable;
|
|
89591
|
-
if (!
|
|
90812
|
+
if (!existsSync50("/var/run/docker.sock")) {
|
|
89592
90813
|
_dockerReachable = false;
|
|
89593
90814
|
return _dockerReachable;
|
|
89594
90815
|
}
|
|
@@ -89609,8 +90830,8 @@ function spawnSwitchroomDetached(args, onFailure) {
|
|
|
89609
90830
|
let outFd = null;
|
|
89610
90831
|
try {
|
|
89611
90832
|
mkdirSync39(STATE_DIR, { recursive: true });
|
|
89612
|
-
outFd =
|
|
89613
|
-
|
|
90833
|
+
outFd = openSync11(logPath, "a");
|
|
90834
|
+
writeFileSync42(logPath, `
|
|
89614
90835
|
[${new Date().toISOString()}] spawn ${SWITCHROOM_CLI} ${fullArgs.join(" ")}
|
|
89615
90836
|
`, { flag: "a" });
|
|
89616
90837
|
} catch {}
|
|
@@ -89624,7 +90845,7 @@ function spawnSwitchroomDetached(args, onFailure) {
|
|
|
89624
90845
|
});
|
|
89625
90846
|
if (outFd != null) {
|
|
89626
90847
|
try {
|
|
89627
|
-
|
|
90848
|
+
closeSync11(outFd);
|
|
89628
90849
|
} catch {}
|
|
89629
90850
|
}
|
|
89630
90851
|
if (onFailure) {
|
|
@@ -89636,7 +90857,7 @@ function spawnSwitchroomDetached(args, onFailure) {
|
|
|
89636
90857
|
return;
|
|
89637
90858
|
let tail = "";
|
|
89638
90859
|
try {
|
|
89639
|
-
const full =
|
|
90860
|
+
const full = readFileSync53(logPath, "utf8");
|
|
89640
90861
|
tail = full.split(`
|
|
89641
90862
|
`).slice(-30).join(`
|
|
89642
90863
|
`).trim();
|
|
@@ -90004,9 +91225,9 @@ bot.use(async (ctx, next) => {
|
|
|
90004
91225
|
function readRecentDenialsForAgent(agentName3, windowMs, limit) {
|
|
90005
91226
|
try {
|
|
90006
91227
|
const auditPath = join53(homedir17(), ".switchroom", "vault-audit.log");
|
|
90007
|
-
if (!
|
|
91228
|
+
if (!existsSync50(auditPath))
|
|
90008
91229
|
return [];
|
|
90009
|
-
const raw =
|
|
91230
|
+
const raw = readFileSync53(auditPath, "utf8");
|
|
90010
91231
|
return recentDenialsFromAuditLog(raw, { agentName: agentName3, windowMs, limit });
|
|
90011
91232
|
} catch {
|
|
90012
91233
|
return [];
|
|
@@ -90057,7 +91278,7 @@ async function buildAgentMetadata(agentName3) {
|
|
|
90057
91278
|
try {
|
|
90058
91279
|
const agentDir = resolveAgentDirFromEnv();
|
|
90059
91280
|
if (agentDir) {
|
|
90060
|
-
const raw =
|
|
91281
|
+
const raw = readFileSync53(join53(agentDir, ".claude", ".claude.json"), "utf8");
|
|
90061
91282
|
claudeJson = JSON.parse(raw);
|
|
90062
91283
|
}
|
|
90063
91284
|
} catch {}
|
|
@@ -90327,6 +91548,192 @@ function modelMenuReplyMarkup(reply) {
|
|
|
90327
91548
|
}
|
|
90328
91549
|
return kb;
|
|
90329
91550
|
}
|
|
91551
|
+
function recordTypedModelSwitch(reply, requestedModelArg, deps) {
|
|
91552
|
+
const requested = requestedModelArg != null ? expandSrAlias(requestedModelArg) : null;
|
|
91553
|
+
if (requested?.toLowerCase() === "default") {
|
|
91554
|
+
const smDir2 = resolveAgentDirFromEnv();
|
|
91555
|
+
if (smDir2)
|
|
91556
|
+
clearSessionModelFile(smDir2);
|
|
91557
|
+
if (reply.selectedModel)
|
|
91558
|
+
sessionModelSource.setOverride(null);
|
|
91559
|
+
return "";
|
|
91560
|
+
}
|
|
91561
|
+
if (!reply.selectedModel)
|
|
91562
|
+
return "";
|
|
91563
|
+
sessionModelSource.setOverride(reply.selectedModel);
|
|
91564
|
+
const smDir = resolveAgentDirFromEnv();
|
|
91565
|
+
if (smDir && requested && isValidModelArg(requested) && !isSrModel(requested)) {
|
|
91566
|
+
try {
|
|
91567
|
+
writeSessionModelFile(smDir, requested, readConfiguredDefaultModel(smDir) ?? resolveMainModel(deps.getConfiguredModel() ?? undefined));
|
|
91568
|
+
} catch (err) {
|
|
91569
|
+
process.stderr.write(`telegram gateway: session-model persist failed (typed /model): ${err?.message ?? String(err)}
|
|
91570
|
+
`);
|
|
91571
|
+
return `
|
|
91572
|
+
\u26A0\uFE0F Couldn\u2019t persist the sticky override \u2014 the switch is live now but won\u2019t survive a relaunch.`;
|
|
91573
|
+
}
|
|
91574
|
+
}
|
|
91575
|
+
return "";
|
|
91576
|
+
}
|
|
91577
|
+
function recordModelMenuSideEffects(outcome, modelDeps, cbChatId, cbThreadId, prevSessionModel) {
|
|
91578
|
+
if (outcome.selectedModel) {
|
|
91579
|
+
sessionModelSource.setOverride(outcome.selectedModel);
|
|
91580
|
+
const smDir = resolveAgentDirFromEnv();
|
|
91581
|
+
if (smDir && outcome.selectedModelToken) {
|
|
91582
|
+
try {
|
|
91583
|
+
writeSessionModelFile(smDir, outcome.selectedModelToken, readConfiguredDefaultModel(smDir) ?? resolveMainModel(modelDeps.getConfiguredModel() ?? undefined));
|
|
91584
|
+
} catch (err) {
|
|
91585
|
+
outcome.reply.text += `
|
|
91586
|
+
\u26A0\uFE0F Couldn\u2019t persist the sticky override \u2014 the switch is live now but won\u2019t survive a relaunch.`;
|
|
91587
|
+
process.stderr.write(`telegram gateway: session-model persist failed (menu): ${err?.message ?? String(err)}
|
|
91588
|
+
`);
|
|
91589
|
+
}
|
|
91590
|
+
}
|
|
91591
|
+
}
|
|
91592
|
+
if (outcome.clearedDefault) {
|
|
91593
|
+
const smDir = resolveAgentDirFromEnv();
|
|
91594
|
+
if (smDir)
|
|
91595
|
+
clearSessionModelFile(smDir);
|
|
91596
|
+
}
|
|
91597
|
+
if (outcome.selectedModel && isSrToClaudeTransition(prevSessionModel, outcome.selectedModel)) {
|
|
91598
|
+
const agentName3 = getMyAgentName();
|
|
91599
|
+
const agentDir = resolveAgentDirFromEnv();
|
|
91600
|
+
const token = outcome.selectedModelToken;
|
|
91601
|
+
if (agentDir && token) {
|
|
91602
|
+
try {
|
|
91603
|
+
writeSessionModelFile(agentDir, token, readConfiguredDefaultModel(agentDir) ?? resolveMainModel(modelDeps.getConfiguredModel() ?? undefined));
|
|
91604
|
+
sessionModelSource.setOverride(token);
|
|
91605
|
+
} catch (e) {
|
|
91606
|
+
process.stderr.write(`telegram gateway: sr-to-claude session-model write failed: ${e?.message ?? String(e)}
|
|
91607
|
+
`);
|
|
91608
|
+
}
|
|
91609
|
+
} else if (agentDir) {
|
|
91610
|
+
clearSessionModelFile(agentDir);
|
|
91611
|
+
}
|
|
91612
|
+
writeRestartMarker({ chat_id: cbChatId, thread_id: cbThreadId ?? null, ack_message_id: null, ts: Date.now() });
|
|
91613
|
+
stampUserRestartReason("user: sr-to-claude model switch (menu)");
|
|
91614
|
+
if (turnInFlightForGate()) {
|
|
91615
|
+
pendingRestarts.set(agentName3, Date.now());
|
|
91616
|
+
} else {
|
|
91617
|
+
sweepBeforeSelfRestart().finally(() => triggerSelfRestart(agentName3, "sr-to-claude-model-switch", 1500));
|
|
91618
|
+
}
|
|
91619
|
+
return {
|
|
91620
|
+
restartNotice: `\uD83D\uDD04 Switching from **${escapeHtmlForTg2(prevSessionModel)}** back to Claude \u2014 restarting session cleanly. Claude will be ready in ~30s.`
|
|
91621
|
+
};
|
|
91622
|
+
}
|
|
91623
|
+
return {};
|
|
91624
|
+
}
|
|
91625
|
+
async function editPendingCommandCard(chatId, messageId, text5) {
|
|
91626
|
+
try {
|
|
91627
|
+
await robustApiCall(() => lockedBot.api.editMessageText(chatId, messageId, richMessage2(hardenCardBreaks2(text5)), {
|
|
91628
|
+
reply_markup: { inline_keyboard: [] }
|
|
91629
|
+
}), { chat_id: chatId, verb: "pending-cmd-card" });
|
|
91630
|
+
} catch (err) {
|
|
91631
|
+
process.stderr.write(`telegram gateway: pending-command card edit failed chat=${chatId} msg=${messageId}: ${err?.message ?? String(err)}
|
|
91632
|
+
`);
|
|
91633
|
+
}
|
|
91634
|
+
}
|
|
91635
|
+
function enqueueSessionCommand(cmd) {
|
|
91636
|
+
const displaced = pendingSessionCommand.set(cmd);
|
|
91637
|
+
if (displaced) {
|
|
91638
|
+
editPendingCommandCard(displaced.ackChatId, displaced.ackMessageId, supersededText(displaced, cmd, escapeHtmlForTg2));
|
|
91639
|
+
}
|
|
91640
|
+
if (!turnInFlightForGate())
|
|
91641
|
+
drainPendingSessionCommand();
|
|
91642
|
+
}
|
|
91643
|
+
async function applyQueuedModelCommand(cmd) {
|
|
91644
|
+
const deps = buildModelDeps({ chatId: cmd.chatId, threadId: cmd.threadId });
|
|
91645
|
+
if (cmd.origin === "menu") {
|
|
91646
|
+
const prevSessionModel = sessionModelSource.getOverride();
|
|
91647
|
+
const outcome = await handleModelMenuCallback(cmd.arg, deps);
|
|
91648
|
+
const { restartNotice } = recordModelMenuSideEffects(outcome, deps, cmd.chatId, cmd.threadId, prevSessionModel);
|
|
91649
|
+
return restartNotice ?? outcome.reply.text;
|
|
91650
|
+
}
|
|
91651
|
+
const reply = await handleModelCommand({ kind: "set", model: cmd.arg }, deps);
|
|
91652
|
+
const warning = recordTypedModelSwitch(reply, cmd.arg, deps);
|
|
91653
|
+
return reply.text + warning;
|
|
91654
|
+
}
|
|
91655
|
+
async function applyQueuedEffortCommand(cmd) {
|
|
91656
|
+
const deps = buildEffortDeps();
|
|
91657
|
+
if (cmd.origin === "menu") {
|
|
91658
|
+
const outcome = await handleEffortMenuCallback(cmd.arg, deps);
|
|
91659
|
+
return outcome.reply.text;
|
|
91660
|
+
}
|
|
91661
|
+
const parsed = cmd.arg === "default" ? { kind: "default" } : { kind: "set", level: cmd.arg };
|
|
91662
|
+
const reply = await handleEffortCommand(parsed, deps);
|
|
91663
|
+
return reply.text;
|
|
91664
|
+
}
|
|
91665
|
+
function reEnqueueUnlessSuperseded(cmd) {
|
|
91666
|
+
const newer = pendingSessionCommand.get(cmd.kind);
|
|
91667
|
+
if (newer != null) {
|
|
91668
|
+
editPendingCommandCard(cmd.ackChatId, cmd.ackMessageId, supersededText(cmd, newer, escapeHtmlForTg2));
|
|
91669
|
+
return;
|
|
91670
|
+
}
|
|
91671
|
+
pendingSessionCommand.set(cmd);
|
|
91672
|
+
}
|
|
91673
|
+
function persistQueuedCommandForRestart(action) {
|
|
91674
|
+
if (action.persist == null)
|
|
91675
|
+
return action.reissueText;
|
|
91676
|
+
const agentDir = resolveAgentDirFromEnv();
|
|
91677
|
+
if (agentDir == null)
|
|
91678
|
+
return action.reissueText;
|
|
91679
|
+
try {
|
|
91680
|
+
switch (action.persist) {
|
|
91681
|
+
case "model": {
|
|
91682
|
+
if (!isOfflineTrustedModelToken(action.arg)) {
|
|
91683
|
+
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.`;
|
|
91684
|
+
}
|
|
91685
|
+
const configured = readConfiguredDefaultModel(agentDir) ?? resolveMainModel(undefined);
|
|
91686
|
+
writeSessionModelFile(agentDir, expandSrAlias(action.arg), configured);
|
|
91687
|
+
writeRelaunchModelIntent(agentDir, "keep", "queued /model carried across restart");
|
|
91688
|
+
break;
|
|
91689
|
+
}
|
|
91690
|
+
case "clear-model":
|
|
91691
|
+
clearSessionModelFile(agentDir);
|
|
91692
|
+
break;
|
|
91693
|
+
case "effort":
|
|
91694
|
+
writeSessionEffortFile(agentDir, action.arg, getConfiguredEffortForPersist());
|
|
91695
|
+
break;
|
|
91696
|
+
case "clear-effort":
|
|
91697
|
+
clearSessionEffortFile(agentDir);
|
|
91698
|
+
break;
|
|
91699
|
+
}
|
|
91700
|
+
return action.persistedText;
|
|
91701
|
+
} catch (err) {
|
|
91702
|
+
process.stderr.write(`telegram gateway: queued-command persist-for-restart failed kind=${action.cmd.kind} arg=${action.arg}: ${err?.message ?? String(err)}
|
|
91703
|
+
`);
|
|
91704
|
+
return action.reissueText;
|
|
91705
|
+
}
|
|
91706
|
+
}
|
|
91707
|
+
function getConfiguredEffortForPersist() {
|
|
91708
|
+
try {
|
|
91709
|
+
const data = switchroomExecJson(["agent", "list"]);
|
|
91710
|
+
return data?.agents?.find((a) => a.name === getMyAgentName())?.thinking_effort ?? SWITCHROOM_DEFAULT_THINKING_EFFORT;
|
|
91711
|
+
} catch {
|
|
91712
|
+
return SWITCHROOM_DEFAULT_THINKING_EFFORT;
|
|
91713
|
+
}
|
|
91714
|
+
}
|
|
91715
|
+
var pendingCmdDraining = false;
|
|
91716
|
+
async function drainPendingSessionCommand() {
|
|
91717
|
+
if (pendingCmdDraining)
|
|
91718
|
+
return;
|
|
91719
|
+
if (pendingSessionCommand.size === 0)
|
|
91720
|
+
return;
|
|
91721
|
+
pendingCmdDraining = true;
|
|
91722
|
+
try {
|
|
91723
|
+
await drainTakenCommands(pendingSessionCommand.takeAll(), {
|
|
91724
|
+
restartPending: () => pendingRestarts.size > 0,
|
|
91725
|
+
turnInFlight: () => turnInFlightForGate(),
|
|
91726
|
+
apply: (cmd) => cmd.kind === "model" ? applyQueuedModelCommand(cmd) : applyQueuedEffortCommand(cmd),
|
|
91727
|
+
isBusyRefusal: isBusyRefusalText,
|
|
91728
|
+
resolveForRestartText: (cmd) => persistQueuedCommandForRestart(resolveForRestart(cmd, escapeHtmlForTg2)),
|
|
91729
|
+
editCard: (cmd, text5) => editPendingCommandCard(cmd.ackChatId, cmd.ackMessageId, text5),
|
|
91730
|
+
reEnqueue: reEnqueueUnlessSuperseded,
|
|
91731
|
+
failureText: (cmd, err) => `\u274C Couldn\u2019t apply the queued ${cmd.kind} switch to \`${escapeHtmlForTg2(cmd.targetLabel)}\`: ${escapeHtmlForTg2(err?.message ?? String(err))}`
|
|
91732
|
+
});
|
|
91733
|
+
} finally {
|
|
91734
|
+
pendingCmdDraining = false;
|
|
91735
|
+
}
|
|
91736
|
+
}
|
|
90330
91737
|
bot.command("model", async (ctx) => {
|
|
90331
91738
|
if (!isAuthorizedSender(ctx))
|
|
90332
91739
|
return;
|
|
@@ -90340,38 +91747,53 @@ bot.command("model", async (ctx) => {
|
|
|
90340
91747
|
await switchroomReply(ctx, menu.text, { html: true, reply_markup: modelMenuReplyMarkup(menu) });
|
|
90341
91748
|
return;
|
|
90342
91749
|
}
|
|
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
|
-
}
|
|
91750
|
+
if (parsed.kind === "set" && deps.isBusy()) {
|
|
91751
|
+
const target = expandSrAlias(parsed.model);
|
|
91752
|
+
const sent = await ctx.replyWithRichMessage(richMessage2(hardenCardBreaks2(ackText("model", target, escapeHtmlForTg2))), threadId != null ? { message_thread_id: threadId } : {});
|
|
91753
|
+
enqueueSessionCommand({
|
|
91754
|
+
kind: "model",
|
|
91755
|
+
origin: "typed",
|
|
91756
|
+
arg: target,
|
|
91757
|
+
targetLabel: target,
|
|
91758
|
+
chatId,
|
|
91759
|
+
threadId,
|
|
91760
|
+
ackChatId: chatId,
|
|
91761
|
+
ackMessageId: sent.message_id,
|
|
91762
|
+
requestedAt: Date.now()
|
|
91763
|
+
});
|
|
91764
|
+
return;
|
|
90365
91765
|
}
|
|
91766
|
+
const reply = await handleModelCommand(parsed, deps);
|
|
91767
|
+
const persistWarning = recordTypedModelSwitch(reply, parsed.kind === "set" ? parsed.model : null, deps);
|
|
90366
91768
|
await switchroomReply(ctx, reply.text + persistWarning, { html: reply.html });
|
|
90367
91769
|
});
|
|
90368
91770
|
function buildEffortDeps() {
|
|
90369
91771
|
return {
|
|
90370
|
-
applyEffort: (agent, level) =>
|
|
91772
|
+
applyEffort: async (agent, level) => {
|
|
91773
|
+
const result = await applyEffort(agent, level);
|
|
91774
|
+
if (result.ok) {
|
|
91775
|
+
const agentDir = resolveAgentDirFromEnv();
|
|
91776
|
+
if (agentDir) {
|
|
91777
|
+
try {
|
|
91778
|
+
writeSessionEffortFile(agentDir, level, getConfiguredEffortForPersist());
|
|
91779
|
+
} catch (err) {
|
|
91780
|
+
process.stderr.write(`telegram gateway: session-effort persist failed level=${level}: ${err?.message ?? String(err)}
|
|
91781
|
+
`);
|
|
91782
|
+
}
|
|
91783
|
+
}
|
|
91784
|
+
}
|
|
91785
|
+
return result;
|
|
91786
|
+
},
|
|
90371
91787
|
getAgentName: getMyAgentName,
|
|
90372
|
-
getConfiguredEffort: () =>
|
|
90373
|
-
|
|
90374
|
-
|
|
91788
|
+
getConfiguredEffort: () => getConfiguredEffortForPersist(),
|
|
91789
|
+
clearSessionEffort: () => {
|
|
91790
|
+
const agentDir = resolveAgentDirFromEnv();
|
|
91791
|
+
if (agentDir)
|
|
91792
|
+
clearSessionEffortFile(agentDir);
|
|
91793
|
+
},
|
|
91794
|
+
getSessionEffort: () => {
|
|
91795
|
+
const agentDir = resolveAgentDirFromEnv();
|
|
91796
|
+
return agentDir ? readSessionEffortFile(agentDir)?.level ?? null : null;
|
|
90375
91797
|
},
|
|
90376
91798
|
escapeHtml: escapeHtmlForTg2
|
|
90377
91799
|
};
|
|
@@ -90398,6 +91820,24 @@ bot.command("effort", async (ctx) => {
|
|
|
90398
91820
|
await switchroomReply(ctx, menu.text, { html: true, reply_markup: effortMenuReplyMarkup(menu) });
|
|
90399
91821
|
return;
|
|
90400
91822
|
}
|
|
91823
|
+
if ((parsed.kind === "set" || parsed.kind === "default") && currentTurn !== null) {
|
|
91824
|
+
const requestedLevel = parsed.kind === "set" ? parsed.level : "default";
|
|
91825
|
+
const chatId = String(ctx.chat.id);
|
|
91826
|
+
const threadId = resolveThreadId(chatId, ctx.message?.message_thread_id);
|
|
91827
|
+
const sent = await ctx.replyWithRichMessage(richMessage2(hardenCardBreaks2(ackText("effort", requestedLevel, escapeHtmlForTg2))), threadId != null ? { message_thread_id: threadId } : {});
|
|
91828
|
+
enqueueSessionCommand({
|
|
91829
|
+
kind: "effort",
|
|
91830
|
+
origin: "typed",
|
|
91831
|
+
arg: requestedLevel,
|
|
91832
|
+
targetLabel: requestedLevel,
|
|
91833
|
+
chatId,
|
|
91834
|
+
threadId,
|
|
91835
|
+
ackChatId: chatId,
|
|
91836
|
+
ackMessageId: sent.message_id,
|
|
91837
|
+
requestedAt: Date.now()
|
|
91838
|
+
});
|
|
91839
|
+
return;
|
|
91840
|
+
}
|
|
90401
91841
|
const reply = await handleEffortCommand(parsed, deps);
|
|
90402
91842
|
await switchroomReply(ctx, reply.text, { html: reply.html });
|
|
90403
91843
|
});
|
|
@@ -90413,7 +91853,7 @@ bot.command("agentstart", async (ctx) => {
|
|
|
90413
91853
|
}
|
|
90414
91854
|
await dispatchShortVerbViaHostd(ctx, { v: 1, op: "agent_start", request_id: hostdRequestId2("gw-start"), args: { name } }, `start ${name}`, ["agent", "start", name]);
|
|
90415
91855
|
});
|
|
90416
|
-
bot.command("
|
|
91856
|
+
bot.command("agentstop", async (ctx) => {
|
|
90417
91857
|
if (!isAuthorizedSender(ctx))
|
|
90418
91858
|
return;
|
|
90419
91859
|
const name = ctx.match?.trim() || getMyAgentName();
|
|
@@ -90425,6 +91865,23 @@ bot.command("stop", async (ctx) => {
|
|
|
90425
91865
|
}
|
|
90426
91866
|
await dispatchShortVerbViaHostd(ctx, { v: 1, op: "agent_stop", request_id: hostdRequestId2("gw-stop"), args: { name } }, `stop ${name}`, ["agent", "stop", name]);
|
|
90427
91867
|
});
|
|
91868
|
+
bot.command("stop", async (ctx) => {
|
|
91869
|
+
if (!isAuthorizedSender(ctx))
|
|
91870
|
+
return;
|
|
91871
|
+
const arg = ctx.match?.trim();
|
|
91872
|
+
if (arg) {
|
|
91873
|
+
await switchroomReply(ctx, `/stop takes no argument \u2014 it cancels MY in-flight turn. ` + `To stop a container, use /agentstop ${arg}. Nothing was stopped.`);
|
|
91874
|
+
return;
|
|
91875
|
+
}
|
|
91876
|
+
const queuedLabels = pendingSessionCommand.list().map((c) => `/${c.kind} ${c.targetLabel}`);
|
|
91877
|
+
const inFlight = turnInFlightForGate();
|
|
91878
|
+
if (!inFlight) {
|
|
91879
|
+
await switchroomReply(ctx, buildStopReply(false, queuedLabels).text);
|
|
91880
|
+
return;
|
|
91881
|
+
}
|
|
91882
|
+
await executeHaltNow("stop-command");
|
|
91883
|
+
await switchroomReply(ctx, buildStopReply(true, queuedLabels).text);
|
|
91884
|
+
});
|
|
90428
91885
|
bot.command("restart", async (ctx) => {
|
|
90429
91886
|
if (!isAuthorizedSender(ctx))
|
|
90430
91887
|
return;
|
|
@@ -90443,10 +91900,10 @@ bot.command("restart", async (ctx) => {
|
|
|
90443
91900
|
}
|
|
90444
91901
|
const chatId = String(ctx.chat.id);
|
|
90445
91902
|
const threadId = resolveThreadId(chatId, ctx.message?.message_thread_id);
|
|
90446
|
-
const
|
|
91903
|
+
const ackText2 = restartAckText(name);
|
|
90447
91904
|
let ackId = null;
|
|
90448
91905
|
try {
|
|
90449
|
-
const sent = await retryWithThreadFallback2(robustApiCall, (tid) => lockedBot.api.sendRichMessage(chatId, richMessage2(
|
|
91906
|
+
const sent = await retryWithThreadFallback2(robustApiCall, (tid) => lockedBot.api.sendRichMessage(chatId, richMessage2(ackText2), {
|
|
90450
91907
|
disable_notification: true,
|
|
90451
91908
|
...tid != null ? { message_thread_id: tid } : {}
|
|
90452
91909
|
}), { threadId, chat_id: chatId, verb: "restart.ack" });
|
|
@@ -90462,7 +91919,7 @@ bot.command("restart", async (ctx) => {
|
|
|
90462
91919
|
{
|
|
90463
91920
|
const smDir = resolveAgentDirFromEnv();
|
|
90464
91921
|
if (smDir)
|
|
90465
|
-
writeRelaunchModelIntent(smDir, "
|
|
91922
|
+
writeRelaunchModelIntent(smDir, "keep", "user: /restart from chat");
|
|
90466
91923
|
}
|
|
90467
91924
|
await sweepBeforeSelfRestart();
|
|
90468
91925
|
const hostdResp = await tryHostdDispatch2(getMyAgentName(), {
|
|
@@ -90496,8 +91953,8 @@ function flushAgentHandoff(agentDir) {
|
|
|
90496
91953
|
for (const fname of [".handoff.md", ".handoff-topic"]) {
|
|
90497
91954
|
const p = join53(agentDir, fname);
|
|
90498
91955
|
try {
|
|
90499
|
-
if (
|
|
90500
|
-
|
|
91956
|
+
if (existsSync50(p)) {
|
|
91957
|
+
unlinkSync21(p);
|
|
90501
91958
|
removed++;
|
|
90502
91959
|
}
|
|
90503
91960
|
} catch (err) {
|
|
@@ -90535,24 +91992,24 @@ async function handleNewCommand(ctx) {
|
|
|
90535
91992
|
const flushed = agentDir != null ? flushAgentHandoff(agentDir) : 0;
|
|
90536
91993
|
const chatId = String(ctx.chat.id);
|
|
90537
91994
|
const threadId = resolveThreadId(chatId, ctx.message?.message_thread_id);
|
|
90538
|
-
const
|
|
91995
|
+
const ackText2 = newSessionAckText(name, flushed > 0);
|
|
90539
91996
|
let ackId = null;
|
|
90540
91997
|
try {
|
|
90541
|
-
const sent = await retryWithThreadFallback2(robustApiCall, (tid) => lockedBot.api.sendRichMessage(chatId, richMessage2(
|
|
91998
|
+
const sent = await retryWithThreadFallback2(robustApiCall, (tid) => lockedBot.api.sendRichMessage(chatId, richMessage2(ackText2), {
|
|
90542
91999
|
disable_notification: true,
|
|
90543
92000
|
...tid != null ? { message_thread_id: tid } : {}
|
|
90544
92001
|
}), { threadId, chat_id: chatId, verb: "new-or-reset.ack" });
|
|
90545
92002
|
ackId = sent.message_id;
|
|
90546
92003
|
if (HISTORY_ENABLED) {
|
|
90547
92004
|
try {
|
|
90548
|
-
recordOutbound({ chat_id: chatId, thread_id: threadId ?? null, message_ids: [sent.message_id], texts: [
|
|
92005
|
+
recordOutbound({ chat_id: chatId, thread_id: threadId ?? null, message_ids: [sent.message_id], texts: [ackText2], attachment_kinds: [] });
|
|
90549
92006
|
} catch {}
|
|
90550
92007
|
}
|
|
90551
92008
|
} catch {}
|
|
90552
92009
|
writeRestartMarker({ chat_id: chatId, thread_id: threadId ?? null, ack_message_id: ackId, ts: Date.now() });
|
|
90553
92010
|
if (agentDir != null) {
|
|
90554
92011
|
try {
|
|
90555
|
-
|
|
92012
|
+
writeFileSync42(join53(agentDir, ".force-fresh-session"), `${kind} at ${new Date().toISOString()}
|
|
90556
92013
|
`, "utf8");
|
|
90557
92014
|
} catch (err) {
|
|
90558
92015
|
process.stderr.write(`telegram gateway: failed to write force-fresh marker: ${err}
|
|
@@ -90628,7 +92085,7 @@ Either run \`switchroom update\` from the host shell, or set \`host_control.enab
|
|
|
90628
92085
|
}
|
|
90629
92086
|
const chatId = String(ctx.chat.id);
|
|
90630
92087
|
const threadId = resolveThreadId(chatId, ctx.message?.message_thread_id);
|
|
90631
|
-
const
|
|
92088
|
+
const ackText2 = `\uD83D\uDE80 **update started** \u2014 running ${[
|
|
90632
92089
|
"`switchroom update`",
|
|
90633
92090
|
...passthrough.map((t) => `\`${t}\``)
|
|
90634
92091
|
].join(" ")}
|
|
@@ -90636,7 +92093,7 @@ Either run \`switchroom update\` from the host shell, or set \`host_control.enab
|
|
|
90636
92093
|
The gateway will restart as part of the recreate step; watch for the post-restart greeting card to confirm completion.`;
|
|
90637
92094
|
let ackId = null;
|
|
90638
92095
|
try {
|
|
90639
|
-
const sent = await retryWithThreadFallback2(robustApiCall, (tid) => lockedBot.api.sendRichMessage(chatId, richMessage2(
|
|
92096
|
+
const sent = await retryWithThreadFallback2(robustApiCall, (tid) => lockedBot.api.sendRichMessage(chatId, richMessage2(ackText2), {
|
|
90640
92097
|
disable_notification: true,
|
|
90641
92098
|
...tid != null ? { message_thread_id: tid } : {}
|
|
90642
92099
|
}), { threadId, chat_id: chatId, verb: "update.ack" });
|
|
@@ -90919,16 +92376,16 @@ bot.command("interrupt", async (ctx) => {
|
|
|
90919
92376
|
await runSwitchroomCommand(ctx, ["agent", "interrupt", name], `interrupt ${name}`);
|
|
90920
92377
|
});
|
|
90921
92378
|
var lockoutOps = {
|
|
90922
|
-
readFileSync: (p, enc) =>
|
|
90923
|
-
writeFileSync: (p, data, opts) =>
|
|
90924
|
-
existsSync: (p) =>
|
|
92379
|
+
readFileSync: (p, enc) => readFileSync53(p, enc),
|
|
92380
|
+
writeFileSync: (p, data, opts) => writeFileSync42(p, data, opts),
|
|
92381
|
+
existsSync: (p) => existsSync50(p),
|
|
90925
92382
|
mkdirSync: (p, opts) => mkdirSync39(p, opts),
|
|
90926
92383
|
joinPath: (...parts) => join53(...parts)
|
|
90927
92384
|
};
|
|
90928
92385
|
var FLEET_FALLBACK_DEDUP_MS = 30000;
|
|
90929
92386
|
function isAuthBrokerSocketReachable() {
|
|
90930
92387
|
try {
|
|
90931
|
-
return
|
|
92388
|
+
return existsSync50(resolveAuthBrokerSocketPath2());
|
|
90932
92389
|
} catch {
|
|
90933
92390
|
return false;
|
|
90934
92391
|
}
|
|
@@ -90937,6 +92394,7 @@ var fleetFallbackGate = createFleetFallbackGate({
|
|
|
90937
92394
|
dedupMs: FLEET_FALLBACK_DEDUP_MS,
|
|
90938
92395
|
brokerReachable: isAuthBrokerSocketReachable
|
|
90939
92396
|
});
|
|
92397
|
+
var modelUnavailableCardRegistry = createModelUnavailableCardRegistry();
|
|
90940
92398
|
var fleetFallbackResumeGate = createFleetFallbackResumeGate({
|
|
90941
92399
|
maxAgeMs: (() => {
|
|
90942
92400
|
const v = Number(process.env.SWITCHROOM_RESUME_MAX_AGE_MS);
|
|
@@ -91024,6 +92482,16 @@ async function doFireFleetAutoFallback(triggerAgent, untilMs) {
|
|
|
91024
92482
|
return outcome.kind === "switched";
|
|
91025
92483
|
const opts = { disable_notification: true };
|
|
91026
92484
|
for (const chat_id of access.allowFrom) {
|
|
92485
|
+
const card = modelUnavailableCardRegistry.take(String(chat_id), Date.now());
|
|
92486
|
+
if (decideAnnouncementDelivery(outcome.kind, card) === "edit" && card) {
|
|
92487
|
+
try {
|
|
92488
|
+
await bot.api.editMessageText(chat_id, card.messageId, richMessage2(foldAnnouncementIntoCard(card.text, outcome.announcement)), {});
|
|
92489
|
+
continue;
|
|
92490
|
+
} catch (err) {
|
|
92491
|
+
process.stderr.write(`telegram gateway: [fleet-fallback] card edit failed chat=${chat_id} \u2014 sending announcement separately: ${err?.message ?? err}
|
|
92492
|
+
`);
|
|
92493
|
+
}
|
|
92494
|
+
}
|
|
91027
92495
|
swallowingApiCall(() => bot.api.sendRichMessage(chat_id, richMessage2(outcome.announcement), opts), { chat_id, verb: "fleet-fallback:notify" });
|
|
91028
92496
|
}
|
|
91029
92497
|
if (outcome.kind === "switched") {
|
|
@@ -91144,6 +92612,38 @@ async function runQuotaWatch(opts = {}) {
|
|
|
91144
92612
|
`);
|
|
91145
92613
|
}
|
|
91146
92614
|
process.stderr.write(`telegram gateway: quota-watch: fleet all-exhausted ${fleetDecision.transition}
|
|
92615
|
+
`);
|
|
92616
|
+
}
|
|
92617
|
+
}
|
|
92618
|
+
{
|
|
92619
|
+
const rollPrev = watchState[FLEET_ROLL_ANNOUNCE_KEY] ?? emptyAccountState();
|
|
92620
|
+
const rollDecision = evaluateFleetRollAnnounce({
|
|
92621
|
+
roll: listStateData.last_fleet_roll ?? null,
|
|
92622
|
+
prev: rollPrev,
|
|
92623
|
+
now
|
|
92624
|
+
});
|
|
92625
|
+
if (rollDecision.kind === "notify") {
|
|
92626
|
+
for (const chat_id of access.allowFrom) {
|
|
92627
|
+
if (tuning.fleetDedup) {
|
|
92628
|
+
const granted = await claimQuotaNotification(brokerClient, buildQuotaClaimKey(FLEET_ROLL_ANNOUNCE_KEY, String(listStateData.last_fleet_roll.at), chat_id));
|
|
92629
|
+
if (!granted) {
|
|
92630
|
+
process.stderr.write(`telegram gateway: quota-watch: fleet-roll claim denied chat=${chat_id} \u2014 another agent notified
|
|
92631
|
+
`);
|
|
92632
|
+
continue;
|
|
92633
|
+
}
|
|
92634
|
+
}
|
|
92635
|
+
await swallowingApiCall(() => bot.api.sendRichMessage(chat_id, richMessage2(rollDecision.message), {
|
|
92636
|
+
disable_notification: true
|
|
92637
|
+
}), { chat_id, verb: "quota-watch.fleet-roll" });
|
|
92638
|
+
}
|
|
92639
|
+
watchState = patchQuotaWatchState(watchState, FLEET_ROLL_ANNOUNCE_KEY, rollDecision.newState);
|
|
92640
|
+
try {
|
|
92641
|
+
saveQuotaWatchState(stateDir, watchState);
|
|
92642
|
+
} catch (err) {
|
|
92643
|
+
process.stderr.write(`telegram gateway: quota-watch: fleet-roll state save failed: ${err}
|
|
92644
|
+
`);
|
|
92645
|
+
}
|
|
92646
|
+
process.stderr.write(`telegram gateway: quota-watch: announced broker fleet roll ${listStateData.last_fleet_roll.from} \u2192 ${listStateData.last_fleet_roll.to}
|
|
91147
92647
|
`);
|
|
91148
92648
|
}
|
|
91149
92649
|
}
|
|
@@ -91151,9 +92651,10 @@ async function runQuotaWatch(opts = {}) {
|
|
|
91151
92651
|
const labelToSnapIndex = new Map(snapshots.map((s, i) => [s.label, i]));
|
|
91152
92652
|
let reconciledCount = 0;
|
|
91153
92653
|
let mutatedState = watchState;
|
|
92654
|
+
const lastRoll = listStateData.last_fleet_roll ?? null;
|
|
91154
92655
|
for (const snap of snapshots) {
|
|
91155
92656
|
const prev = watchState[snap.label] ?? emptyAccountState();
|
|
91156
|
-
const decision = evaluateQuotaWatchAccount({ agentName: agentName3, snap, prev, now, bootTick, tuning });
|
|
92657
|
+
const decision = evaluateQuotaWatchAccount({ agentName: agentName3, snap, prev, now, bootTick, tuning, lastRoll });
|
|
91157
92658
|
if (decision.kind === "reconcile") {
|
|
91158
92659
|
mutatedState = patchQuotaWatchState(mutatedState, decision.accountLabel, decision.newAccountState);
|
|
91159
92660
|
reconciledCount++;
|
|
@@ -91209,7 +92710,7 @@ async function runQuotaWatch(opts = {}) {
|
|
|
91209
92710
|
if (isLiveCorroboration(freshEntry) && freshEntry.result.ok && snapIndex >= 0) {
|
|
91210
92711
|
const enrichedSnap = { ...snapshots[snapIndex], quota: freshEntry.result.data, capturedAtMs: undefined };
|
|
91211
92712
|
const prev = watchState[accountLabel] ?? emptyAccountState();
|
|
91212
|
-
const re = evaluateQuotaWatchAccount({ agentName: agentName3, snap: enrichedSnap, prev, now, bootTick, tuning });
|
|
92713
|
+
const re = evaluateQuotaWatchAccount({ agentName: agentName3, snap: enrichedSnap, prev, now, bootTick, tuning, lastRoll });
|
|
91213
92714
|
if (re.kind === "notify" && re.transition === decision.transition) {
|
|
91214
92715
|
enrichedDecision = re;
|
|
91215
92716
|
} else if (re.kind === "reconcile") {
|
|
@@ -92045,6 +93546,28 @@ bot.on("callback_query:data", async (ctx) => {
|
|
|
92045
93546
|
await ctx.answerCallbackQuery({ text: "Not authorized." });
|
|
92046
93547
|
return;
|
|
92047
93548
|
}
|
|
93549
|
+
const effLevel = data.startsWith("eff:s:") ? data.slice("eff:s:".length) : null;
|
|
93550
|
+
const effCardMsgId = ctx.callbackQuery?.message?.message_id;
|
|
93551
|
+
if (effLevel != null && currentTurn !== null && effCardMsgId != null) {
|
|
93552
|
+
await ctx.answerCallbackQuery({ text: "\uD83D\uDCE5 Queued \u2014 applies when the turn ends" }).catch(() => {});
|
|
93553
|
+
const effChatId = String(ctx.chat?.id ?? "");
|
|
93554
|
+
const effThreadId = resolveThreadId(effChatId, ctx.callbackQuery?.message?.message_thread_id);
|
|
93555
|
+
await ctx.editMessageText(richMessage2(hardenCardBreaks2(ackText("effort", effLevel, escapeHtmlForTg2))), {
|
|
93556
|
+
reply_markup: { inline_keyboard: [] }
|
|
93557
|
+
}).catch(() => {});
|
|
93558
|
+
enqueueSessionCommand({
|
|
93559
|
+
kind: "effort",
|
|
93560
|
+
origin: "menu",
|
|
93561
|
+
arg: data,
|
|
93562
|
+
targetLabel: effLevel,
|
|
93563
|
+
chatId: effChatId,
|
|
93564
|
+
threadId: effThreadId,
|
|
93565
|
+
ackChatId: effChatId,
|
|
93566
|
+
ackMessageId: effCardMsgId,
|
|
93567
|
+
requestedAt: Date.now()
|
|
93568
|
+
});
|
|
93569
|
+
return;
|
|
93570
|
+
}
|
|
92048
93571
|
await ctx.answerCallbackQuery({ text: "Setting effort\u2026" }).catch(() => {});
|
|
92049
93572
|
try {
|
|
92050
93573
|
const outcome = await handleEffortMenuCallback(data, buildEffortDeps());
|
|
@@ -92075,6 +93598,46 @@ bot.on("callback_query:data", async (ctx) => {
|
|
|
92075
93598
|
const cbThreadId = resolveThreadId(cbChatId, ctx.callbackQuery?.message?.message_thread_id);
|
|
92076
93599
|
const modelDeps = buildModelDeps({ chatId: cbChatId, threadId: cbThreadId });
|
|
92077
93600
|
if (modelDeps.isBusy()) {
|
|
93601
|
+
const cardMsgId = ctx.callbackQuery?.message?.message_id;
|
|
93602
|
+
let queueArg = null;
|
|
93603
|
+
let queueOrigin = "menu";
|
|
93604
|
+
let queueLabel = "the selected model";
|
|
93605
|
+
if (data.startsWith(MODEL_CALLBACK_SR)) {
|
|
93606
|
+
const srName = data.slice(MODEL_CALLBACK_SR.length);
|
|
93607
|
+
if (isValidModelArg(srName)) {
|
|
93608
|
+
queueArg = srName;
|
|
93609
|
+
queueOrigin = "typed";
|
|
93610
|
+
queueLabel = srFriendlyLabel(srName);
|
|
93611
|
+
}
|
|
93612
|
+
} else if (data.startsWith(MODEL_CALLBACK_ALIAS)) {
|
|
93613
|
+
const alias = data.slice(MODEL_CALLBACK_ALIAS.length);
|
|
93614
|
+
if (isValidModelArg(alias)) {
|
|
93615
|
+
queueArg = alias;
|
|
93616
|
+
queueOrigin = "typed";
|
|
93617
|
+
queueLabel = alias;
|
|
93618
|
+
}
|
|
93619
|
+
} else if (data.startsWith("mdl:s:")) {
|
|
93620
|
+
queueArg = data;
|
|
93621
|
+
queueOrigin = "menu";
|
|
93622
|
+
}
|
|
93623
|
+
if (queueArg != null && cardMsgId != null) {
|
|
93624
|
+
await ctx.answerCallbackQuery({ text: "\uD83D\uDCE5 Queued \u2014 applies when the turn ends" }).catch(() => {});
|
|
93625
|
+
await ctx.editMessageText(richMessage2(hardenCardBreaks2(ackText("model", queueLabel, escapeHtmlForTg2))), {
|
|
93626
|
+
reply_markup: { inline_keyboard: [] }
|
|
93627
|
+
}).catch(() => {});
|
|
93628
|
+
enqueueSessionCommand({
|
|
93629
|
+
kind: "model",
|
|
93630
|
+
origin: queueOrigin,
|
|
93631
|
+
arg: queueArg,
|
|
93632
|
+
targetLabel: queueLabel,
|
|
93633
|
+
chatId: cbChatId,
|
|
93634
|
+
threadId: cbThreadId,
|
|
93635
|
+
ackChatId: cbChatId,
|
|
93636
|
+
ackMessageId: cardMsgId,
|
|
93637
|
+
requestedAt: Date.now()
|
|
93638
|
+
});
|
|
93639
|
+
return;
|
|
93640
|
+
}
|
|
92078
93641
|
await ctx.answerCallbackQuery({ text: "\u23F3 Agent is mid-turn \u2014 tap again when it\u2019s idle", show_alert: false }).catch(() => {});
|
|
92079
93642
|
return;
|
|
92080
93643
|
}
|
|
@@ -92099,56 +93662,14 @@ bot.on("callback_query:data", async (ctx) => {
|
|
|
92099
93662
|
}
|
|
92100
93663
|
return;
|
|
92101
93664
|
}
|
|
92102
|
-
const didInterimSrEdit = false;
|
|
92103
93665
|
try {
|
|
92104
93666
|
const prevSessionModel = sessionModelSource.getOverride();
|
|
92105
93667
|
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)
|
|
93668
|
+
if (outcome.toastOnly)
|
|
92126
93669
|
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
|
-
}
|
|
93670
|
+
const { restartNotice } = recordModelMenuSideEffects(outcome, modelDeps, cbChatId, cbThreadId, prevSessionModel);
|
|
93671
|
+
if (restartNotice) {
|
|
93672
|
+
await ctx.editMessageText(richMessage2(restartNotice), { reply_markup: { inline_keyboard: [] } }).catch(() => {});
|
|
92152
93673
|
return;
|
|
92153
93674
|
}
|
|
92154
93675
|
await ctx.editMessageText(richMessage2(outcome.reply.text), {
|
|
@@ -92365,7 +93886,7 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
|
|
|
92365
93886
|
let audio = null;
|
|
92366
93887
|
if (entry.filePath != null) {
|
|
92367
93888
|
try {
|
|
92368
|
-
audio =
|
|
93889
|
+
audio = readFileSync53(entry.filePath);
|
|
92369
93890
|
} catch {
|
|
92370
93891
|
audio = null;
|
|
92371
93892
|
}
|
|
@@ -92443,8 +93964,8 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
|
|
|
92443
93964
|
const cbMessageId = ctx.callbackQuery?.message?.message_id;
|
|
92444
93965
|
const metaForMessage = cbMessageId != null ? agentButtonMeta.get(`${cbChatId}:${cbMessageId}`) : undefined;
|
|
92445
93966
|
const tapMeta = metaForMessage?.get(agentCb.raw);
|
|
92446
|
-
const
|
|
92447
|
-
await ctx.answerCallbackQuery({ text:
|
|
93967
|
+
const ackText3 = typeof tapMeta?.ack_text === "string" && tapMeta.ack_text.length > 0 ? tapMeta.ack_text : "\u2713 received";
|
|
93968
|
+
await ctx.answerCallbackQuery({ text: ackText3 }).catch(() => {});
|
|
92448
93969
|
const buttonText = (() => {
|
|
92449
93970
|
const msg2 = ctx.callbackQuery?.message;
|
|
92450
93971
|
const kb = msg2 && "reply_markup" in msg2 ? msg2.reply_markup : undefined;
|
|
@@ -92596,7 +94117,7 @@ ${interimLabel}` : interimLabel
|
|
|
92596
94117
|
const unifiedDiff = (() => {
|
|
92597
94118
|
try {
|
|
92598
94119
|
const cfgPath = process.env.SWITCHROOM_CONFIG ?? SWITCHROOM_CONFIG ?? findConfigFile2();
|
|
92599
|
-
const raw =
|
|
94120
|
+
const raw = readFileSync53(cfgPath, "utf8");
|
|
92600
94121
|
return synthesizeAllowRuleDiff({ agentName: agentName3, rule: chosen.rule, configText: raw });
|
|
92601
94122
|
} catch (err) {
|
|
92602
94123
|
process.stderr.write(`telegram gateway: always-allow diff synth failed: ${err.message}
|
|
@@ -92732,7 +94253,7 @@ ${editLabel}` : editLabel), {
|
|
|
92732
94253
|
}
|
|
92733
94254
|
const scopedMins = Math.max(1, Math.round(scopedTtl / 60000));
|
|
92734
94255
|
const windowGranted = timeBox != null && grantAgent !== "";
|
|
92735
|
-
const
|
|
94256
|
+
const ackText2 = behavior === "deny" ? "\u274C Denied" : windowGranted ? `\u2705 Allowed (${scopedMins} min)` : "\u2705 Allowed once";
|
|
92736
94257
|
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
94258
|
const msg = ctx.callbackQuery?.message;
|
|
92738
94259
|
const baseText = msg && "text" in msg && msg.text ? escapeHtmlForTg2(msg.text) : "";
|
|
@@ -92745,7 +94266,7 @@ ${editLabel}` : editLabel), {
|
|
|
92745
94266
|
|
|
92746
94267
|
${resumeLine}` : htmlLabel;
|
|
92747
94268
|
await finalizeCallback2(ctx, {
|
|
92748
|
-
ackText:
|
|
94269
|
+
ackText: ackText2.slice(0, 200),
|
|
92749
94270
|
newText: baseText ? `${baseText}
|
|
92750
94271
|
|
|
92751
94272
|
${labelWithResume}` : labelWithResume,
|
|
@@ -92787,7 +94308,7 @@ bot.on("message:photo", async (ctx) => {
|
|
|
92787
94308
|
});
|
|
92788
94309
|
mkdirSync39(INBOX_DIR, { recursive: true, mode: 448 });
|
|
92789
94310
|
assertInsideInbox(INBOX_DIR, dlPath);
|
|
92790
|
-
|
|
94311
|
+
writeFileSync42(dlPath, buf, { mode: 384 });
|
|
92791
94312
|
return dlPath;
|
|
92792
94313
|
} catch (err) {
|
|
92793
94314
|
const msg = err instanceof Error ? err.message : "unknown error";
|
|
@@ -93487,6 +95008,7 @@ bot.catch((err) => {
|
|
|
93487
95008
|
`);
|
|
93488
95009
|
});
|
|
93489
95010
|
var SHUTDOWN_DRAIN_BUDGET_MS = 35000;
|
|
95011
|
+
var SHUTDOWN_PROGRESS_FLUSH_BUDGET_MS = 1500;
|
|
93490
95012
|
function countInFlight() {
|
|
93491
95013
|
return pendingPermissions.size + pendingVaultOps.size + inboundCoalescer.size() + pendingReauthFlows.size;
|
|
93492
95014
|
}
|
|
@@ -93498,6 +95020,7 @@ async function shutdown(signal) {
|
|
|
93498
95020
|
const agentName3 = process.env.SWITCHROOM_AGENT_NAME ?? "-";
|
|
93499
95021
|
process.stderr.write(`telegram gateway: shutting down
|
|
93500
95022
|
`);
|
|
95023
|
+
bridgeDeadWatchdog.stop();
|
|
93501
95024
|
const isOsSignal = signal === "SIGTERM" || signal === "SIGINT";
|
|
93502
95025
|
if (isOsSignal) {
|
|
93503
95026
|
try {
|
|
@@ -93508,12 +95031,43 @@ async function shutdown(signal) {
|
|
|
93508
95031
|
`);
|
|
93509
95032
|
} catch (err) {
|
|
93510
95033
|
process.stderr.write(`telegram gateway: shutdown.clean_marker_write_failed err=${err.message}
|
|
95034
|
+
`);
|
|
95035
|
+
}
|
|
95036
|
+
try {
|
|
95037
|
+
const smDir = resolveAgentDirFromEnv();
|
|
95038
|
+
if (smDir != null) {
|
|
95039
|
+
const hasOverride = readSessionModelFile(smDir) != null;
|
|
95040
|
+
const intentAlreadyStamped = existsSync50(join53(smDir, RELAUNCH_MODEL_INTENT_FILE));
|
|
95041
|
+
if (hasOverride && !intentAlreadyStamped) {
|
|
95042
|
+
writeRelaunchModelIntent(smDir, "keep", `${GATEWAY_SHUTDOWN_INTENT_REASON_PREFIX} graceful ${signal} shutdown (deploy/rolling restart) \u2014 preserving user-chosen session model`);
|
|
95043
|
+
process.stderr.write(`telegram gateway: shutdown.session_model_keep_stamped signal=${signal}
|
|
95044
|
+
`);
|
|
95045
|
+
}
|
|
95046
|
+
}
|
|
95047
|
+
} catch (err) {
|
|
95048
|
+
process.stderr.write(`telegram gateway: shutdown.session_model_keep_stamp_failed err=${err.message}
|
|
93511
95049
|
`);
|
|
93512
95050
|
}
|
|
93513
95051
|
} else {
|
|
93514
95052
|
process.stderr.write(`telegram gateway: shutdown.clean_marker_skipped signal=${signal} (crash path \u2014 banner will fire on next boot)
|
|
93515
95053
|
`);
|
|
93516
95054
|
}
|
|
95055
|
+
const orphanedCmdActions = shutdownResolutionActions(pendingSessionCommand, escapeHtmlForTg2);
|
|
95056
|
+
const orphanedCmdEdits = orphanedCmdActions.map((a) => ({
|
|
95057
|
+
chatId: a.cmd.ackChatId,
|
|
95058
|
+
messageId: a.cmd.ackMessageId,
|
|
95059
|
+
text: persistQueuedCommandForRestart(a)
|
|
95060
|
+
}));
|
|
95061
|
+
if (orphanedCmdEdits.length > 0) {
|
|
95062
|
+
process.stderr.write(`telegram gateway: shutdown.pending_cmd_resolved count=${orphanedCmdEdits.length}
|
|
95063
|
+
`);
|
|
95064
|
+
await Promise.race([
|
|
95065
|
+
Promise.allSettled(orphanedCmdEdits.map((e) => editPendingCommandCard(e.chatId, e.messageId, e.text))),
|
|
95066
|
+
new Promise((resolve11) => {
|
|
95067
|
+
setTimeout(resolve11, SHUTDOWN_PROGRESS_FLUSH_BUDGET_MS).unref();
|
|
95068
|
+
})
|
|
95069
|
+
]);
|
|
95070
|
+
}
|
|
93517
95071
|
const shutdownTurn = currentTurn;
|
|
93518
95072
|
if (turnsDb != null && shutdownTurn?.registryKey != null) {
|
|
93519
95073
|
const wasScheduledRestart = pendingRestarts.size > 0;
|
|
@@ -93927,9 +95481,9 @@ var didOneTimeSetup = false;
|
|
|
93927
95481
|
const smAgentDir = resolveAgentDirFromEnv();
|
|
93928
95482
|
if (smAgentDir) {
|
|
93929
95483
|
const activePath = join53(smAgentDir, ".active-session-model");
|
|
93930
|
-
if (
|
|
95484
|
+
if (existsSync50(activePath)) {
|
|
93931
95485
|
try {
|
|
93932
|
-
const launched =
|
|
95486
|
+
const launched = readFileSync53(activePath, "utf8").trim();
|
|
93933
95487
|
const configured = (() => {
|
|
93934
95488
|
const d = switchroomExecJson(["agent", "list"]);
|
|
93935
95489
|
const raw = d?.agents?.find((a) => a.name === getMyAgentName())?.model ?? null;
|
|
@@ -93939,15 +95493,15 @@ var didOneTimeSetup = false;
|
|
|
93939
95493
|
} catch {}
|
|
93940
95494
|
}
|
|
93941
95495
|
const alertPath = join53(smAgentDir, ".session-model-alert");
|
|
93942
|
-
if (
|
|
95496
|
+
if (existsSync50(alertPath)) {
|
|
93943
95497
|
let alertText = null;
|
|
93944
95498
|
try {
|
|
93945
|
-
alertText =
|
|
95499
|
+
alertText = readFileSync53(alertPath, "utf8").trim();
|
|
93946
95500
|
} catch {
|
|
93947
95501
|
alertText = null;
|
|
93948
95502
|
}
|
|
93949
95503
|
try {
|
|
93950
|
-
|
|
95504
|
+
unlinkSync21(alertPath);
|
|
93951
95505
|
} catch {}
|
|
93952
95506
|
if (alertText && alertText.length > 0) {
|
|
93953
95507
|
const operators = loadAccess().allowFrom;
|
|
@@ -94067,6 +95621,20 @@ var didOneTimeSetup = false;
|
|
|
94067
95621
|
parentStateDir: STATE_DIR,
|
|
94068
95622
|
log: (msg) => process.stderr.write(`telegram gateway: ${msg}
|
|
94069
95623
|
`),
|
|
95624
|
+
onResurrect: (agentId, _description) => {
|
|
95625
|
+
try {
|
|
95626
|
+
workerActivityFeed?.resurrect(agentId);
|
|
95627
|
+
} catch (err) {
|
|
95628
|
+
process.stderr.write(`telegram gateway: worker resurrect error agent=${agentId}: ${err.message}
|
|
95629
|
+
`);
|
|
95630
|
+
}
|
|
95631
|
+
process.stderr.write(`telegram gateway: worker ${agentId} card RESURRECTED \u2014 false terminal finish reversed, worker resumed (issue #3023)
|
|
95632
|
+
`);
|
|
95633
|
+
},
|
|
95634
|
+
onWorkerLost: (agentId, _description) => {
|
|
95635
|
+
process.stderr.write(`telegram gateway: worker ${agentId} NAMED AS LOST \u2014 falsely finalised twice, resurrection chain bound reached (issue #3023)
|
|
95636
|
+
`);
|
|
95637
|
+
},
|
|
94070
95638
|
onFinish: ({ agentId, outcome, description, resultText, toolCount, durationMs, background: entryBackground }) => {
|
|
94071
95639
|
deferredDoneReactions.promote();
|
|
94072
95640
|
let fleetChatId = "";
|