switchroom 0.15.2 → 0.15.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/turn-pacing-hook.sh +112 -0
- package/bin/workspace-dynamic-hook.sh +105 -15
- package/bin/workspace-stable-hook.sh +2 -2
- package/dist/agent-scheduler/index.js +2 -1
- package/dist/auth-broker/index.js +75 -12
- package/dist/cli/notion-write-pretool.mjs +2 -1
- package/dist/cli/switchroom.js +1596 -1515
- package/dist/host-control/main.js +2 -1
- 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 +35 -2
- package/profiles/default/CLAUDE.md.hbs +13 -4
- package/telegram-plugin/dist/gateway/gateway.js +533 -33
- package/telegram-plugin/gateway/gateway.ts +152 -14
- package/telegram-plugin/gateway/inbound-spool.ts +107 -16
- package/telegram-plugin/gateway/model-command.ts +261 -7
- package/telegram-plugin/tests/inbound-spool.test.ts +101 -0
- package/telegram-plugin/tests/model-command.test.ts +179 -0
- package/telegram-plugin/tests/welcome-text.test.ts +11 -0
- package/telegram-plugin/uat/scenarios/jtbd-model-command-dm.test.ts +93 -0
- package/telegram-plugin/welcome-text.ts +16 -1
- package/profiles/default/workspace/HEARTBEAT.md.hbs +0 -40
|
@@ -23936,7 +23936,8 @@ var init_schema = __esm(() => {
|
|
|
23936
23936
|
stream_mode: exports_external.enum(["pty", "checklist"]).optional().describe("How live progress is streamed to Telegram during a turn. " + "'pty' (default) surfaces text snapshots of Claude Code's TUI \u2014 " + "compatible but can flicker as Ink re-renders. 'checklist' drives " + "a structured progress card from session-tail events \u2014 stable " + "order, per-tool status emojis, fires only on semantic transitions."),
|
|
23937
23937
|
stream_throttle_ms: exports_external.number().int().nonnegative().optional().describe("Throttle window in ms between successive stream edits (or " + "sendMessageDraft tics) during a turn. Lower = more responsive " + "stream, higher = fewer API calls. Floored at 250 by draft-stream " + "itself. Default 300 for draft transport (DMs) and 1000 for " + "message transport (groups/forums). Override per-agent if a " + "particular agent needs snappier or quieter streaming."),
|
|
23938
23938
|
clear_status_on_completion: exports_external.boolean().optional().describe("When true, the live activity/status feed (the in-place 'what it's " + "doing' message \u2014 Reading X, Searching the web for Y, \u2026) is DELETED " + "when the turn's final answer lands, so only the reply remains. " + "Default false: the status message is left in the chat as a record " + "(its last step marked done) \u2014 no post-then-delete. Per-agent " + "override; cascades defaults \u2192 profile \u2192 agent (per-key)."),
|
|
23939
|
-
hotReloadStable: exports_external.boolean().optional().describe("If true, the stable workspace prefix (AGENTS.md, SOUL.md, USER.md, " + "IDENTITY.md, TOOLS.md
|
|
23939
|
+
hotReloadStable: exports_external.boolean().optional().describe("If true, the stable workspace prefix (AGENTS.md, SOUL.md, USER.md, " + "IDENTITY.md, TOOLS.md) is re-injected on every turn via " + "the UserPromptSubmit hook instead of baked into --append-system-prompt " + "at session start. Lets workspace edits propagate without a restart. " + "Costs ~5-10% per-turn latency/spend since the stable prefix is no " + "longer prompt-cached."),
|
|
23940
|
+
inject_on_change: exports_external.boolean().optional().describe("Context-efficiency gate for per-turn hook injection (default true). " + "When true (the default), the turn-pacing directive and dynamic " + "workspace content are only re-emitted when their content changes or " + "the session_id changes \u2014 suppressing redundant injection that " + "otherwise triples compaction frequency. Set to false to revert to " + "the legacy always-emit behaviour (every turn injects the full " + "content regardless of whether it changed)."),
|
|
23940
23941
|
orphan_promotion_ms: exports_external.number().int().nonnegative().optional().describe("How long (ms) a parent turn waits for a sub-agent JSONL watcher " + "to deliver sub_agent_started before the heartbeat promotes the spawn " + "to a synthesised 'running' row. Default 5000. Set to 0 to disable " + "orphan promotion entirely."),
|
|
23941
23942
|
cold_sub_agent_threshold_ms: exports_external.number().int().nonnegative().optional().describe("JSONL-cold threshold (ms). When a running sub-agent emits no events " + "for this long, the heartbeat synthesises a turn_end for it so the " + "deferred-completion path can proceed. Default 30000. Set to 0 to " + "disable the synthetic close."),
|
|
23942
23943
|
deferred_completion_timeout_ms: exports_external.number().int().nonnegative().optional().describe("Force-close timeout (ms) for deferred sub-agent completion. After " + "the parent turn_end arrives while sub-agents are still running, the " + "card is force-closed after this many ms even if sub-agents never " + "finish. Watcher-disconnect safety net. Default 180000 (3 min)."),
|
|
@@ -42942,7 +42943,8 @@ function formatAuthLine(auth) {
|
|
|
42942
42943
|
function formatAgentLine(meta) {
|
|
42943
42944
|
const m = meta.model && meta.model.length > 0 ? meta.model : "default";
|
|
42944
42945
|
const topic = meta.topicName ? ` \u00b7 topic: ${escapeHtml6([meta.topicEmoji, meta.topicName].filter(Boolean).join(" "))}` : "";
|
|
42945
|
-
|
|
42946
|
+
const session = meta.sessionModel && meta.sessionModel.length > 0 ? ` \u00b7 live session: <code>${escapeHtml6(meta.sessionModel)}</code>` : "";
|
|
42947
|
+
return `<b>${escapeHtml6(meta.agentName)}</b> \u00b7 model: <code>${escapeHtml6(m)}</code>${session}${topic}`;
|
|
42946
42948
|
}
|
|
42947
42949
|
function startText(agentName3, dmDisabled) {
|
|
42948
42950
|
if (dmDisabled)
|
|
@@ -44328,6 +44330,10 @@ async function fetchQuota2(opts) {
|
|
|
44328
44330
|
}
|
|
44329
44331
|
return parsed;
|
|
44330
44332
|
}
|
|
44333
|
+
function formatQuotaLine2(q) {
|
|
44334
|
+
const fmt = (n) => `${Math.round(n)}%`;
|
|
44335
|
+
return `${fmt(q.fiveHourUtilizationPct)} / 5h \u00b7 ${fmt(q.sevenDayUtilizationPct)} / 7d`;
|
|
44336
|
+
}
|
|
44331
44337
|
function formatResetRelative2(target, now = new Date) {
|
|
44332
44338
|
if (!target)
|
|
44333
44339
|
return "\u2014";
|
|
@@ -44399,6 +44405,22 @@ var TOKEN_VALIDITY_MS2 = 365 * 24 * 60 * 60000;
|
|
|
44399
44405
|
// ../src/agents/inject.ts
|
|
44400
44406
|
import { execFile, execFileSync } from "node:child_process";
|
|
44401
44407
|
import { promisify } from "node:util";
|
|
44408
|
+
|
|
44409
|
+
// ../src/agents/pane-lock.ts
|
|
44410
|
+
var tails = new Map;
|
|
44411
|
+
function withPaneLock(key, fn) {
|
|
44412
|
+
const tail = tails.get(key) ?? Promise.resolve();
|
|
44413
|
+
const run2 = tail.then(() => fn());
|
|
44414
|
+
const next = run2.then(() => {}, () => {});
|
|
44415
|
+
tails.set(key, next);
|
|
44416
|
+
next.then(() => {
|
|
44417
|
+
if (tails.get(key) === next)
|
|
44418
|
+
tails.delete(key);
|
|
44419
|
+
});
|
|
44420
|
+
return run2;
|
|
44421
|
+
}
|
|
44422
|
+
|
|
44423
|
+
// ../src/agents/inject.ts
|
|
44402
44424
|
var execFileAsync = promisify(execFile);
|
|
44403
44425
|
var INJECT_COMMANDS = new Map([
|
|
44404
44426
|
["/cost", { description: "Show session cost", expectsOutput: true }],
|
|
@@ -44548,13 +44570,13 @@ async function injectSlashCommand(agentName3, command, opts = {}) {
|
|
|
44548
44570
|
const session = opts.sessionName ?? agentName3;
|
|
44549
44571
|
const settleMs = opts.settleMs ?? 2000;
|
|
44550
44572
|
const timeoutMs = opts.timeoutMs ?? 5000;
|
|
44551
|
-
return injectSlashCommandWith(makeTmuxRunner(tmuxBin), {
|
|
44573
|
+
return withPaneLock(`${socket}:${session}`, () => injectSlashCommandWith(makeTmuxRunner(tmuxBin), {
|
|
44552
44574
|
socket,
|
|
44553
44575
|
session,
|
|
44554
44576
|
command: command.trim(),
|
|
44555
44577
|
settleMs,
|
|
44556
44578
|
timeoutMs
|
|
44557
|
-
});
|
|
44579
|
+
}));
|
|
44558
44580
|
}
|
|
44559
44581
|
async function injectSlashCommandWith(runner, args) {
|
|
44560
44582
|
const { socket, session, command, settleMs, timeoutMs } = args;
|
|
@@ -44685,6 +44707,34 @@ var INJECT_BLOCKED2 = new Map([
|
|
|
44685
44707
|
["/quit", { reason: "would kill the agent process" }]
|
|
44686
44708
|
]);
|
|
44687
44709
|
var INJECT_BLOCKLIST2 = new Set(INJECT_BLOCKED2.keys());
|
|
44710
|
+
function makeTmuxRunner2(tmuxBin) {
|
|
44711
|
+
return {
|
|
44712
|
+
capture(socket, session) {
|
|
44713
|
+
try {
|
|
44714
|
+
return execFileSync2(tmuxBin, ["-L", socket, "capture-pane", "-p", "-t", session, "-S", "-200"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
44715
|
+
} catch {
|
|
44716
|
+
return null;
|
|
44717
|
+
}
|
|
44718
|
+
},
|
|
44719
|
+
send(socket, session, args) {
|
|
44720
|
+
const [subcmd, ...rest] = args;
|
|
44721
|
+
const flagEnd = rest.findIndex((a) => !a.startsWith("-"));
|
|
44722
|
+
const flagsBeforeKeys = flagEnd === -1 ? rest : rest.slice(0, flagEnd);
|
|
44723
|
+
const keys = flagEnd === -1 ? [] : rest.slice(flagEnd);
|
|
44724
|
+
execFileSync2(tmuxBin, ["-L", socket, subcmd, ...flagsBeforeKeys, "-t", session, ...keys], { stdio: ["pipe", "pipe", "pipe"] });
|
|
44725
|
+
},
|
|
44726
|
+
hasSession(socket, session) {
|
|
44727
|
+
try {
|
|
44728
|
+
execFileSync2(tmuxBin, ["-L", socket, "has-session", "-t", session], {
|
|
44729
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
44730
|
+
});
|
|
44731
|
+
return true;
|
|
44732
|
+
} catch {
|
|
44733
|
+
return false;
|
|
44734
|
+
}
|
|
44735
|
+
}
|
|
44736
|
+
};
|
|
44737
|
+
}
|
|
44688
44738
|
|
|
44689
44739
|
// stream-reply-handler.ts
|
|
44690
44740
|
function buildAccentHeader2(accent) {
|
|
@@ -44815,6 +44865,16 @@ Allowed: <code>${deps.escapeHtml(allow)}</code>`, { html: true });
|
|
|
44815
44865
|
await deps.reply(ctx, finalText, { html: true, accent });
|
|
44816
44866
|
}
|
|
44817
44867
|
|
|
44868
|
+
// ../src/agents/model-picker.ts
|
|
44869
|
+
function labelTag(label) {
|
|
44870
|
+
let h = 2166136261;
|
|
44871
|
+
for (const ch of label) {
|
|
44872
|
+
h ^= ch.codePointAt(0);
|
|
44873
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
44874
|
+
}
|
|
44875
|
+
return h.toString(16).padStart(8, "0");
|
|
44876
|
+
}
|
|
44877
|
+
|
|
44818
44878
|
// gateway/model-command.ts
|
|
44819
44879
|
var MODEL_ALIASES = ["opus", "sonnet", "haiku", "default"];
|
|
44820
44880
|
var MODEL_ARG_RE = /^[A-Za-z0-9][A-Za-z0-9._\[\]-]{0,99}$/;
|
|
@@ -44914,6 +44974,325 @@ async function handleModelCommand(parsed, deps) {
|
|
|
44914
44974
|
html: true
|
|
44915
44975
|
};
|
|
44916
44976
|
}
|
|
44977
|
+
var MODEL_CALLBACK_PREFIX = "mdl:";
|
|
44978
|
+
var MODEL_CALLBACK_SELECT = "mdl:s:";
|
|
44979
|
+
var MODEL_CALLBACK_REFRESH = "mdl:r";
|
|
44980
|
+
function modelSelectCallbackData(label) {
|
|
44981
|
+
return `${MODEL_CALLBACK_SELECT}${labelTag(label)}`;
|
|
44982
|
+
}
|
|
44983
|
+
function busyReply(deps) {
|
|
44984
|
+
return {
|
|
44985
|
+
text: "\u23f3 The agent is mid-turn \u2014 the model picker needs an idle prompt. Try again in a moment.",
|
|
44986
|
+
html: true
|
|
44987
|
+
};
|
|
44988
|
+
}
|
|
44989
|
+
function menuKeyboard(options) {
|
|
44990
|
+
const rows = options.map((o) => [
|
|
44991
|
+
{
|
|
44992
|
+
text: o.current ? `\u2705 ${o.label}` : o.label,
|
|
44993
|
+
callback_data: modelSelectCallbackData(o.label)
|
|
44994
|
+
}
|
|
44995
|
+
]);
|
|
44996
|
+
rows.push([{ text: "\uD83D\uDD04 Refresh", callback_data: MODEL_CALLBACK_REFRESH }]);
|
|
44997
|
+
return rows;
|
|
44998
|
+
}
|
|
44999
|
+
async function buildModelMenu(deps) {
|
|
45000
|
+
if (deps.isBusy())
|
|
45001
|
+
return busyReply(deps);
|
|
45002
|
+
const [discovered, quota] = await Promise.all([
|
|
45003
|
+
deps.discover(deps.getAgentName()),
|
|
45004
|
+
deps.getQuotaBrief().catch(() => null)
|
|
45005
|
+
]);
|
|
45006
|
+
if (!discovered.ok) {
|
|
45007
|
+
const v1 = await handleModelCommand({ kind: "show" }, deps);
|
|
45008
|
+
return {
|
|
45009
|
+
text: [`<i>(picker unavailable: ${deps.escapeHtml(discovered.reason)})</i>`, v1.text].join(`
|
|
45010
|
+
`),
|
|
45011
|
+
html: true
|
|
45012
|
+
};
|
|
45013
|
+
}
|
|
45014
|
+
const current = discovered.options.find((o) => o.current);
|
|
45015
|
+
const lines = [`<b>Model \u2014 ${deps.escapeHtml(deps.getAgentName())}</b>`];
|
|
45016
|
+
if (discovered.dismissFailed) {
|
|
45017
|
+
lines.push("\u26a0\ufe0f <i>The picker may still be open on the agent pane \u2014 check it before switching.</i>");
|
|
45018
|
+
}
|
|
45019
|
+
if (current) {
|
|
45020
|
+
const detail = current.detail ? ` \u00b7 ${deps.escapeHtml(current.detail)}` : "";
|
|
45021
|
+
lines.push(`Default (new sessions): <b>${deps.escapeHtml(current.label)}</b>${detail}`);
|
|
45022
|
+
} else {
|
|
45023
|
+
lines.push("Default (new sessions): <i>unknown (no \u2714 row in picker)</i>");
|
|
45024
|
+
}
|
|
45025
|
+
if (quota)
|
|
45026
|
+
lines.push(`Quota: ${deps.escapeHtml(quota)}`);
|
|
45027
|
+
lines.push("", "Tap a model to switch the <b>live session</b>:");
|
|
45028
|
+
lines.push(PERSIST_NOTE);
|
|
45029
|
+
return { text: lines.join(`
|
|
45030
|
+
`), html: true, keyboard: menuKeyboard(discovered.options) };
|
|
45031
|
+
}
|
|
45032
|
+
async function handleModelMenuCallback(data, deps) {
|
|
45033
|
+
if (data === MODEL_CALLBACK_REFRESH) {
|
|
45034
|
+
return { answer: "Refreshed", reply: await buildModelMenu(deps) };
|
|
45035
|
+
}
|
|
45036
|
+
if (!data.startsWith(MODEL_CALLBACK_SELECT)) {
|
|
45037
|
+
return { answer: "Unknown action", reply: await buildModelMenu(deps) };
|
|
45038
|
+
}
|
|
45039
|
+
if (deps.isBusy()) {
|
|
45040
|
+
return {
|
|
45041
|
+
answer: "\u23f3 Agent is mid-turn \u2014 tap again when it\u2019s idle",
|
|
45042
|
+
reply: busyReply(deps),
|
|
45043
|
+
toastOnly: true
|
|
45044
|
+
};
|
|
45045
|
+
}
|
|
45046
|
+
const tag = data.slice(MODEL_CALLBACK_SELECT.length);
|
|
45047
|
+
const discovered = await deps.discover(deps.getAgentName());
|
|
45048
|
+
if (!discovered.ok) {
|
|
45049
|
+
return {
|
|
45050
|
+
answer: "Picker unavailable",
|
|
45051
|
+
reply: await menuWithBanner(deps, `\u274c Could not open the model picker: ${deps.escapeHtml(discovered.reason)}`)
|
|
45052
|
+
};
|
|
45053
|
+
}
|
|
45054
|
+
const target = discovered.options.find((o) => labelTag(o.label) === tag);
|
|
45055
|
+
if (!target) {
|
|
45056
|
+
const fresh = await buildModelMenu(deps);
|
|
45057
|
+
return { answer: "Model list changed \u2014 menu refreshed", reply: fresh };
|
|
45058
|
+
}
|
|
45059
|
+
const result = await deps.select(deps.getAgentName(), target.label);
|
|
45060
|
+
if (!result.ok) {
|
|
45061
|
+
return {
|
|
45062
|
+
answer: "Switch failed \u2014 see the menu",
|
|
45063
|
+
reply: await menuWithBanner(deps, `\u274c Switch to <b>${deps.escapeHtml(target.label)}</b> failed: ${deps.escapeHtml(result.reason)}`)
|
|
45064
|
+
};
|
|
45065
|
+
}
|
|
45066
|
+
return {
|
|
45067
|
+
answer: deps.escapeHtml(result.confirmation),
|
|
45068
|
+
reply: await menuWithBanner(deps, `\u2705 ${deps.escapeHtml(result.confirmation)}`),
|
|
45069
|
+
selectedModel: sessionModelFromConfirmation(result.confirmation) ?? target.label
|
|
45070
|
+
};
|
|
45071
|
+
}
|
|
45072
|
+
function sessionModelFromConfirmation(confirmation) {
|
|
45073
|
+
const m = /(?:Set model to|Switched to)\s+(.+?)(?:\s+for (?:this|the) session|\s*\(|\s*$)/i.exec(confirmation.trim());
|
|
45074
|
+
const name = m?.[1]?.trim();
|
|
45075
|
+
return name && name.length > 0 ? name : null;
|
|
45076
|
+
}
|
|
45077
|
+
async function menuWithBanner(deps, banner) {
|
|
45078
|
+
const fresh = await buildModelMenu(deps);
|
|
45079
|
+
return {
|
|
45080
|
+
text: [banner, "", fresh.text].join(`
|
|
45081
|
+
`),
|
|
45082
|
+
html: true,
|
|
45083
|
+
...fresh.keyboard ? { keyboard: fresh.keyboard } : {}
|
|
45084
|
+
};
|
|
45085
|
+
}
|
|
45086
|
+
|
|
45087
|
+
// ../src/agents/model-picker.ts
|
|
45088
|
+
var HEADER_RE = /Select model/;
|
|
45089
|
+
var OPTION_RE = /^\s*(\u276f)?\s*(\d+)\.\s+(.*)$/;
|
|
45090
|
+
var FOOTER_RE = /Esc to cancel/i;
|
|
45091
|
+
var EFFORT_RE = /^\s*\u25cb\s/;
|
|
45092
|
+
function parseModelPicker(pane) {
|
|
45093
|
+
const lines = pane.split(`
|
|
45094
|
+
`);
|
|
45095
|
+
let headerIdx = -1;
|
|
45096
|
+
for (let i = lines.length - 1;i >= 0; i--) {
|
|
45097
|
+
if (HEADER_RE.test(lines[i])) {
|
|
45098
|
+
headerIdx = i;
|
|
45099
|
+
break;
|
|
45100
|
+
}
|
|
45101
|
+
}
|
|
45102
|
+
if (headerIdx < 0)
|
|
45103
|
+
return null;
|
|
45104
|
+
const options = [];
|
|
45105
|
+
let cursorIndex = -1;
|
|
45106
|
+
let footerSeen = false;
|
|
45107
|
+
for (let i = headerIdx + 1;i < lines.length; i++) {
|
|
45108
|
+
const raw = lines[i];
|
|
45109
|
+
if (FOOTER_RE.test(raw)) {
|
|
45110
|
+
footerSeen = true;
|
|
45111
|
+
break;
|
|
45112
|
+
}
|
|
45113
|
+
if (EFFORT_RE.test(raw))
|
|
45114
|
+
continue;
|
|
45115
|
+
const m = OPTION_RE.exec(raw);
|
|
45116
|
+
if (m) {
|
|
45117
|
+
const index = Number(m[2]);
|
|
45118
|
+
const rest = m[3].trimEnd();
|
|
45119
|
+
const gap = rest.search(/\s{2,}/);
|
|
45120
|
+
let label = (gap >= 0 ? rest.slice(0, gap) : rest).trim();
|
|
45121
|
+
const detail = gap >= 0 ? rest.slice(gap).trim() : "";
|
|
45122
|
+
let current = false;
|
|
45123
|
+
if (/[\u2714\u2713]$/.test(label)) {
|
|
45124
|
+
current = true;
|
|
45125
|
+
label = label.replace(/\s*[\u2714\u2713]$/, "").trim();
|
|
45126
|
+
}
|
|
45127
|
+
if (label.length === 0)
|
|
45128
|
+
continue;
|
|
45129
|
+
options.push({ index, label, detail, current });
|
|
45130
|
+
if (m[1])
|
|
45131
|
+
cursorIndex = index;
|
|
45132
|
+
continue;
|
|
45133
|
+
}
|
|
45134
|
+
if (options.length > 0 && /^\s{4,}\S/.test(raw)) {
|
|
45135
|
+
const last = options[options.length - 1];
|
|
45136
|
+
last.detail = `${last.detail} ${raw.trim()}`.trim();
|
|
45137
|
+
continue;
|
|
45138
|
+
}
|
|
45139
|
+
}
|
|
45140
|
+
if (options.length === 0)
|
|
45141
|
+
return null;
|
|
45142
|
+
return { options, cursorIndex, footerSeen };
|
|
45143
|
+
}
|
|
45144
|
+
var realSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
45145
|
+
function makeIo(agentName3, opts) {
|
|
45146
|
+
return {
|
|
45147
|
+
runner: opts._runner ?? makeTmuxRunner2(opts.tmuxBin ?? "tmux"),
|
|
45148
|
+
socket: opts.socketName ?? `switchroom-${agentName3}`,
|
|
45149
|
+
session: opts.sessionName ?? agentName3,
|
|
45150
|
+
stepMs: opts.stepMs ?? 600,
|
|
45151
|
+
timeoutMs: opts.timeoutMs ?? 1e4,
|
|
45152
|
+
sleep: opts._sleep ?? realSleep,
|
|
45153
|
+
log: opts._log ?? ((line) => process.stderr.write(`${line}
|
|
45154
|
+
`)),
|
|
45155
|
+
startedAt: Date.now()
|
|
45156
|
+
};
|
|
45157
|
+
}
|
|
45158
|
+
async function dismissOrWarn(io, verb) {
|
|
45159
|
+
const dismissed = await dismissPicker(io);
|
|
45160
|
+
if (!dismissed) {
|
|
45161
|
+
io.log(`model-picker: ${verb}: picker may still be open after Esc retries ` + `(socket=${io.socket} session=${io.session}) \u2014 pane needs eyes`);
|
|
45162
|
+
}
|
|
45163
|
+
return dismissed;
|
|
45164
|
+
}
|
|
45165
|
+
function expired(io) {
|
|
45166
|
+
return Date.now() - io.startedAt >= io.timeoutMs;
|
|
45167
|
+
}
|
|
45168
|
+
function sendLiteral(io, text) {
|
|
45169
|
+
io.runner.send(io.socket, io.session, ["send-keys", "-l", text]);
|
|
45170
|
+
}
|
|
45171
|
+
function sendKey(io, key) {
|
|
45172
|
+
io.runner.send(io.socket, io.session, ["send-keys", key]);
|
|
45173
|
+
}
|
|
45174
|
+
async function openPicker(io) {
|
|
45175
|
+
sendLiteral(io, "/model");
|
|
45176
|
+
sendKey(io, "Enter");
|
|
45177
|
+
for (;; ) {
|
|
45178
|
+
await io.sleep(io.stepMs);
|
|
45179
|
+
const pane = io.runner.capture(io.socket, io.session) ?? "";
|
|
45180
|
+
const parsed = parseModelPicker(pane);
|
|
45181
|
+
if (parsed?.footerSeen)
|
|
45182
|
+
return parsed;
|
|
45183
|
+
if (expired(io))
|
|
45184
|
+
return parsed;
|
|
45185
|
+
}
|
|
45186
|
+
}
|
|
45187
|
+
async function dismissPicker(io) {
|
|
45188
|
+
for (let attempt = 0;attempt < 2; attempt++) {
|
|
45189
|
+
try {
|
|
45190
|
+
sendKey(io, "Escape");
|
|
45191
|
+
} catch {
|
|
45192
|
+
return false;
|
|
45193
|
+
}
|
|
45194
|
+
await io.sleep(io.stepMs);
|
|
45195
|
+
const pane = io.runner.capture(io.socket, io.session) ?? "";
|
|
45196
|
+
const parsed = parseModelPicker(pane);
|
|
45197
|
+
if (!parsed || !parsed.footerSeen)
|
|
45198
|
+
return true;
|
|
45199
|
+
const tail = pane.slice(pane.lastIndexOf("Esc to cancel"));
|
|
45200
|
+
if (tail.split(`
|
|
45201
|
+
`).slice(1).some((l) => l.trim().length > 0))
|
|
45202
|
+
return true;
|
|
45203
|
+
}
|
|
45204
|
+
return false;
|
|
45205
|
+
}
|
|
45206
|
+
async function discoverModels(agentName3, opts = {}) {
|
|
45207
|
+
const io = makeIo(agentName3, opts);
|
|
45208
|
+
if (!io.runner.hasSession(io.socket, io.session)) {
|
|
45209
|
+
return { ok: false, reason: "tmux session not found" };
|
|
45210
|
+
}
|
|
45211
|
+
return withPaneLock(`${io.socket}:${io.session}`, async () => {
|
|
45212
|
+
io.startedAt = Date.now();
|
|
45213
|
+
let parsed = null;
|
|
45214
|
+
let dismissed = true;
|
|
45215
|
+
try {
|
|
45216
|
+
parsed = await openPicker(io);
|
|
45217
|
+
} finally {
|
|
45218
|
+
dismissed = await dismissOrWarn(io, "discover");
|
|
45219
|
+
}
|
|
45220
|
+
const tail = dismissed ? {} : { dismissFailed: true };
|
|
45221
|
+
if (!parsed || !parsed.footerSeen) {
|
|
45222
|
+
return {
|
|
45223
|
+
ok: false,
|
|
45224
|
+
reason: "picker did not render \u2014 agent may be mid-turn or the CLI changed its /model UI",
|
|
45225
|
+
...tail
|
|
45226
|
+
};
|
|
45227
|
+
}
|
|
45228
|
+
const current = parsed.options.find((o) => o.current) ?? null;
|
|
45229
|
+
return { ok: true, options: parsed.options, currentLabel: current?.label ?? null, ...tail };
|
|
45230
|
+
});
|
|
45231
|
+
}
|
|
45232
|
+
async function selectModel(agentName3, targetLabel, opts = {}) {
|
|
45233
|
+
const io = makeIo(agentName3, opts);
|
|
45234
|
+
if (!io.runner.hasSession(io.socket, io.session)) {
|
|
45235
|
+
return { ok: false, reason: "tmux session not found" };
|
|
45236
|
+
}
|
|
45237
|
+
return withPaneLock(`${io.socket}:${io.session}`, async () => {
|
|
45238
|
+
io.startedAt = Date.now();
|
|
45239
|
+
let selected = false;
|
|
45240
|
+
try {
|
|
45241
|
+
const parsed = await openPicker(io);
|
|
45242
|
+
if (!parsed || !parsed.footerSeen) {
|
|
45243
|
+
return { ok: false, reason: "picker did not render \u2014 agent may be mid-turn" };
|
|
45244
|
+
}
|
|
45245
|
+
const target = parsed.options.find((o) => o.label === targetLabel);
|
|
45246
|
+
if (!target) {
|
|
45247
|
+
const have = parsed.options.map((o) => o.label).join(", ");
|
|
45248
|
+
return { ok: false, reason: `option "${targetLabel}" not offered (have: ${have})` };
|
|
45249
|
+
}
|
|
45250
|
+
if (parsed.cursorIndex < 0) {
|
|
45251
|
+
return { ok: false, reason: "cursor marker not visible \u2014 refusing blind navigation" };
|
|
45252
|
+
}
|
|
45253
|
+
const delta = target.index - parsed.cursorIndex;
|
|
45254
|
+
const key = delta > 0 ? "Down" : "Up";
|
|
45255
|
+
for (let i = 0;i < Math.abs(delta); i++) {
|
|
45256
|
+
if (expired(io))
|
|
45257
|
+
return { ok: false, reason: "timed out navigating picker" };
|
|
45258
|
+
sendKey(io, key);
|
|
45259
|
+
await io.sleep(Math.min(io.stepMs, 250));
|
|
45260
|
+
}
|
|
45261
|
+
await io.sleep(io.stepMs);
|
|
45262
|
+
const verifyPane = io.runner.capture(io.socket, io.session) ?? "";
|
|
45263
|
+
const verify = parseModelPicker(verifyPane);
|
|
45264
|
+
const atRow = verify?.options.find((o) => o.index === verify.cursorIndex);
|
|
45265
|
+
if (!verify || !atRow || atRow.label !== targetLabel) {
|
|
45266
|
+
return {
|
|
45267
|
+
ok: false,
|
|
45268
|
+
reason: `cursor verification failed (on "${atRow?.label ?? "?"}", wanted "${targetLabel}")`
|
|
45269
|
+
};
|
|
45270
|
+
}
|
|
45271
|
+
sendLiteral(io, "s");
|
|
45272
|
+
selected = true;
|
|
45273
|
+
await io.sleep(io.stepMs);
|
|
45274
|
+
const after = io.runner.capture(io.socket, io.session) ?? "";
|
|
45275
|
+
const confirmation = extractConfirmation(after) ?? `Switched to ${targetLabel} (session)`;
|
|
45276
|
+
return { ok: true, confirmation };
|
|
45277
|
+
} catch (err) {
|
|
45278
|
+
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
45279
|
+
} finally {
|
|
45280
|
+
if (!selected) {
|
|
45281
|
+
await dismissOrWarn(io, "select");
|
|
45282
|
+
}
|
|
45283
|
+
}
|
|
45284
|
+
});
|
|
45285
|
+
}
|
|
45286
|
+
function extractConfirmation(pane) {
|
|
45287
|
+
const lines = pane.split(`
|
|
45288
|
+
`);
|
|
45289
|
+
for (let i = lines.length - 1;i >= 0; i--) {
|
|
45290
|
+
const t = lines[i].replace(/^\s*\u23bf\s*/, "").trim();
|
|
45291
|
+
if (/^(Set model to|Kept model as|Switched to)/i.test(t))
|
|
45292
|
+
return t;
|
|
45293
|
+
}
|
|
45294
|
+
return null;
|
|
45295
|
+
}
|
|
44917
45296
|
|
|
44918
45297
|
// ../src/config/loader.ts
|
|
44919
45298
|
init_dist();
|
|
@@ -47981,13 +48360,20 @@ function spoolId(msg) {
|
|
|
47981
48360
|
const src = msg.meta?.source ?? "-";
|
|
47982
48361
|
return `s:${msg.chatId}:${src}:${msg.ts}`;
|
|
47983
48362
|
}
|
|
48363
|
+
function escChatKey(msg) {
|
|
48364
|
+
const threadRaw = msg.meta?.threadId;
|
|
48365
|
+
const thread = typeof threadRaw === "string" && threadRaw.length > 0 ? threadRaw : "-";
|
|
48366
|
+
return `${msg.chatId}:${thread}`;
|
|
48367
|
+
}
|
|
47984
48368
|
function createInboundSpool(opts) {
|
|
47985
48369
|
const { path, fs: fs2 } = opts;
|
|
47986
48370
|
const now = opts.now ?? Date.now;
|
|
47987
48371
|
const log = opts.log ?? ((l) => process.stderr.write(l));
|
|
47988
48372
|
const escalateAfterMs = opts.escalateAfterMs ?? 15 * 60 * 1000;
|
|
47989
48373
|
const compactAtBytes = opts.compactAtBytes ?? 256 * 1024;
|
|
48374
|
+
const escalateNoticeCooldownMs = opts.escalateNoticeCooldownMs ?? 30 * 60 * 1000;
|
|
47990
48375
|
const live = new Map;
|
|
48376
|
+
const escAttemptByChat = new Map;
|
|
47991
48377
|
function parseLine(line) {
|
|
47992
48378
|
const s = line.trim();
|
|
47993
48379
|
if (!s)
|
|
@@ -48001,8 +48387,15 @@ function createInboundSpool(opts) {
|
|
|
48001
48387
|
if (rec == null || typeof rec !== "object")
|
|
48002
48388
|
return null;
|
|
48003
48389
|
const r = rec;
|
|
48004
|
-
if (r.t !== "put" && r.t !== "ack")
|
|
48390
|
+
if (r.t !== "put" && r.t !== "ack" && r.t !== "esc")
|
|
48005
48391
|
return null;
|
|
48392
|
+
if (r.t === "esc") {
|
|
48393
|
+
if (typeof r.chat !== "string" && typeof r.chat !== "number")
|
|
48394
|
+
return null;
|
|
48395
|
+
if (typeof r.at !== "number")
|
|
48396
|
+
return null;
|
|
48397
|
+
return r;
|
|
48398
|
+
}
|
|
48006
48399
|
if (typeof r.id !== "string" || r.id.length === 0)
|
|
48007
48400
|
return null;
|
|
48008
48401
|
if (r.t === "put") {
|
|
@@ -48017,6 +48410,7 @@ function createInboundSpool(opts) {
|
|
|
48017
48410
|
}
|
|
48018
48411
|
function hydrate() {
|
|
48019
48412
|
live.clear();
|
|
48413
|
+
escAttemptByChat.clear();
|
|
48020
48414
|
if (!fs2.existsSync(path))
|
|
48021
48415
|
return;
|
|
48022
48416
|
let raw = "";
|
|
@@ -48036,6 +48430,8 @@ function createInboundSpool(opts) {
|
|
|
48036
48430
|
msg: rec.msg,
|
|
48037
48431
|
firstAt: rec.firstAt
|
|
48038
48432
|
});
|
|
48433
|
+
} else if (rec.t === "esc") {
|
|
48434
|
+
escAttemptByChat.set(`${rec.chat}:${rec.thread ?? "-"}`, rec.at);
|
|
48039
48435
|
} else {
|
|
48040
48436
|
live.delete(rec.id);
|
|
48041
48437
|
}
|
|
@@ -48063,6 +48459,17 @@ function createInboundSpool(opts) {
|
|
|
48063
48459
|
for (const [id, e] of live) {
|
|
48064
48460
|
lines.push(JSON.stringify({ t: "put", id, agent: e.agent, msg: e.msg, firstAt: e.firstAt }));
|
|
48065
48461
|
}
|
|
48462
|
+
for (const [key, at] of escAttemptByChat) {
|
|
48463
|
+
const sep3 = key.lastIndexOf(":");
|
|
48464
|
+
const chat = key.slice(0, sep3);
|
|
48465
|
+
const thread = key.slice(sep3 + 1);
|
|
48466
|
+
lines.push(JSON.stringify({
|
|
48467
|
+
t: "esc",
|
|
48468
|
+
chat,
|
|
48469
|
+
...thread !== "-" ? { thread } : {},
|
|
48470
|
+
at
|
|
48471
|
+
}));
|
|
48472
|
+
}
|
|
48066
48473
|
const tmp = path + ".compact.tmp";
|
|
48067
48474
|
try {
|
|
48068
48475
|
fs2.writeFileSync(tmp, lines.length ? lines.join(`
|
|
@@ -48124,27 +48531,39 @@ function createInboundSpool(opts) {
|
|
|
48124
48531
|
return n;
|
|
48125
48532
|
},
|
|
48126
48533
|
sweepEscalations(onEscalate) {
|
|
48127
|
-
const
|
|
48128
|
-
|
|
48534
|
+
const tNow = now();
|
|
48535
|
+
const cutoff = tNow - escalateAfterMs;
|
|
48536
|
+
let dropped = 0;
|
|
48537
|
+
let posted = 0;
|
|
48129
48538
|
for (const [id, e] of [...live.entries()]) {
|
|
48130
48539
|
if (e.firstAt > cutoff)
|
|
48131
48540
|
continue;
|
|
48132
48541
|
live.delete(id);
|
|
48133
48542
|
appendRecord({ t: "ack", id });
|
|
48543
|
+
const key = escChatKey(e.msg);
|
|
48544
|
+
const lastAttempt = escAttemptByChat.get(key);
|
|
48545
|
+
const postNotice = lastAttempt === undefined || tNow - lastAttempt >= escalateNoticeCooldownMs;
|
|
48546
|
+
escAttemptByChat.set(key, tNow);
|
|
48547
|
+
const threadRaw = e.msg.meta?.threadId;
|
|
48548
|
+
const thread = typeof threadRaw === "string" && threadRaw.length > 0 ? threadRaw : undefined;
|
|
48549
|
+
appendRecord({ t: "esc", chat: e.msg.chatId, thread, at: tNow });
|
|
48134
48550
|
try {
|
|
48135
|
-
onEscalate({ agent: e.agent, msg: e.msg });
|
|
48551
|
+
onEscalate({ agent: e.agent, msg: e.msg }, { postNotice });
|
|
48136
48552
|
} catch (err) {
|
|
48137
48553
|
log(`inbound-spool: onEscalate threw id=${id}: ${err.message}
|
|
48138
48554
|
`);
|
|
48139
48555
|
}
|
|
48140
|
-
|
|
48556
|
+
if (postNotice)
|
|
48557
|
+
posted++;
|
|
48558
|
+
dropped++;
|
|
48141
48559
|
}
|
|
48142
|
-
if (
|
|
48143
|
-
|
|
48560
|
+
if (dropped > 0) {
|
|
48561
|
+
const suppressed = dropped - posted;
|
|
48562
|
+
log(`inbound-spool: escalated+dropped ${dropped} undelivered entr${dropped === 1 ? "y" : "ies"} ` + `(older than ${escalateAfterMs}ms; ${posted} notice${posted === 1 ? "" : "s"} posted` + `${suppressed > 0 ? `, ${suppressed} coalesced` : ""})
|
|
48144
48563
|
`);
|
|
48145
48564
|
maybeCompact();
|
|
48146
48565
|
}
|
|
48147
|
-
return
|
|
48566
|
+
return dropped;
|
|
48148
48567
|
},
|
|
48149
48568
|
liveCount() {
|
|
48150
48569
|
return live.size;
|
|
@@ -50976,7 +51395,7 @@ function startSubagentWatcher(config) {
|
|
|
50976
51395
|
watch
|
|
50977
51396
|
};
|
|
50978
51397
|
const registry = new Map;
|
|
50979
|
-
const
|
|
51398
|
+
const tails2 = new Map;
|
|
50980
51399
|
const dirWatchers = new Map;
|
|
50981
51400
|
const knownFiles = new Set;
|
|
50982
51401
|
const pendingCloses = new Map;
|
|
@@ -51022,7 +51441,7 @@ function startSubagentWatcher(config) {
|
|
|
51022
51441
|
hasEmittedStart: false,
|
|
51023
51442
|
watcher: null
|
|
51024
51443
|
};
|
|
51025
|
-
|
|
51444
|
+
tails2.set(agentId, tail);
|
|
51026
51445
|
readSubTail(entry, tail, n, (desc) => {
|
|
51027
51446
|
log?.(`subagent-watcher: description updated for ${agentId}: ${desc}`);
|
|
51028
51447
|
}, fs2, log, db2, parentStateDir, config.onUnstall, undefined, config.onProgress);
|
|
@@ -51060,7 +51479,7 @@ function startSubagentWatcher(config) {
|
|
|
51060
51479
|
if (stopped)
|
|
51061
51480
|
return;
|
|
51062
51481
|
const entry2 = registry.get(agentId);
|
|
51063
|
-
const t =
|
|
51482
|
+
const t = tails2.get(agentId);
|
|
51064
51483
|
if (!entry2 || !t)
|
|
51065
51484
|
return;
|
|
51066
51485
|
readSubTail(entry2, t, nowFn(), (desc) => {
|
|
@@ -51129,14 +51548,14 @@ function startSubagentWatcher(config) {
|
|
|
51129
51548
|
pendingCloses.set(agentId, handle);
|
|
51130
51549
|
}
|
|
51131
51550
|
function cleanupTerminalAgent(agentId) {
|
|
51132
|
-
const tail =
|
|
51551
|
+
const tail = tails2.get(agentId);
|
|
51133
51552
|
if (tail?.watcher) {
|
|
51134
51553
|
try {
|
|
51135
51554
|
tail.watcher.close();
|
|
51136
51555
|
} catch {}
|
|
51137
51556
|
tail.watcher = null;
|
|
51138
51557
|
}
|
|
51139
|
-
|
|
51558
|
+
tails2.delete(agentId);
|
|
51140
51559
|
const entry = registry.get(agentId);
|
|
51141
51560
|
if (entry?.filePath) {
|
|
51142
51561
|
knownFiles.delete(entry.filePath);
|
|
@@ -51306,7 +51725,7 @@ function startSubagentWatcher(config) {
|
|
|
51306
51725
|
for (const [agentId, entry] of registry) {
|
|
51307
51726
|
if (entry.state !== "running")
|
|
51308
51727
|
continue;
|
|
51309
|
-
const tail =
|
|
51728
|
+
const tail = tails2.get(agentId);
|
|
51310
51729
|
if (!tail)
|
|
51311
51730
|
continue;
|
|
51312
51731
|
readSubTail(entry, tail, n, (desc) => {
|
|
@@ -51348,7 +51767,7 @@ function startSubagentWatcher(config) {
|
|
|
51348
51767
|
} catch {}
|
|
51349
51768
|
}
|
|
51350
51769
|
dirWatchers.clear();
|
|
51351
|
-
for (const tail of
|
|
51770
|
+
for (const tail of tails2.values()) {
|
|
51352
51771
|
if (tail.watcher) {
|
|
51353
51772
|
try {
|
|
51354
51773
|
tail.watcher.close();
|
|
@@ -51356,7 +51775,7 @@ function startSubagentWatcher(config) {
|
|
|
51356
51775
|
tail.watcher = null;
|
|
51357
51776
|
}
|
|
51358
51777
|
}
|
|
51359
|
-
|
|
51778
|
+
tails2.clear();
|
|
51360
51779
|
registry.clear();
|
|
51361
51780
|
knownFiles.clear();
|
|
51362
51781
|
terminatedAgentIds.clear();
|
|
@@ -53182,10 +53601,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
|
|
|
53182
53601
|
}
|
|
53183
53602
|
|
|
53184
53603
|
// ../src/build-info.ts
|
|
53185
|
-
var VERSION = "0.15.
|
|
53186
|
-
var COMMIT_SHA = "
|
|
53187
|
-
var COMMIT_DATE = "2026-06-
|
|
53188
|
-
var LATEST_PR =
|
|
53604
|
+
var VERSION = "0.15.4";
|
|
53605
|
+
var COMMIT_SHA = "dd68b93e";
|
|
53606
|
+
var COMMIT_DATE = "2026-06-11T14:24:12Z";
|
|
53607
|
+
var LATEST_PR = 2280;
|
|
53189
53608
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
53190
53609
|
|
|
53191
53610
|
// gateway/boot-version.ts
|
|
@@ -56636,11 +57055,13 @@ if (!STATIC) {
|
|
|
56636
57055
|
process.stderr.write(`telegram gateway: idle-drain flushed ${r.redelivered}/${r.drained} buffered inbound for ${selfAgent}${r.rebuffered > 0 ? ` (${r.rebuffered} re-buffered)` : ""}
|
|
56637
57056
|
`);
|
|
56638
57057
|
}
|
|
56639
|
-
inboundSpool?.sweepEscalations((e) => {
|
|
57058
|
+
inboundSpool?.sweepEscalations((e, { postNotice }) => {
|
|
56640
57059
|
const chat = e.msg.chatId;
|
|
56641
57060
|
const escThread = typeof e.msg.meta?.threadId === "string" && e.msg.meta.threadId ? Number(e.msg.meta.threadId) : undefined;
|
|
56642
57061
|
const threadOpts = escThread != null ? { message_thread_id: escThread } : {};
|
|
56643
57062
|
reapQueuedStatus(chat, escThread);
|
|
57063
|
+
if (!postNotice)
|
|
57064
|
+
return;
|
|
56644
57065
|
swallowingApiCall(() => bot.api.sendMessage(chat, "\u26A0\uFE0F I couldn't deliver an earlier message to the agent after repeated retries (it survived restarts but the agent never picked it up). Please resend it.", { ...threadOpts }), { chat_id: chat, verb: "inbound-spool-escalation" });
|
|
56645
57066
|
});
|
|
56646
57067
|
}, IDLE_DRAIN_INTERVAL_MS).unref();
|
|
@@ -60696,6 +61117,7 @@ function buildAgentAudit(agentName3) {
|
|
|
60696
61117
|
return;
|
|
60697
61118
|
}
|
|
60698
61119
|
}
|
|
61120
|
+
var activeSessionModelOverride = null;
|
|
60699
61121
|
async function buildAgentMetadata(agentName3) {
|
|
60700
61122
|
const list2 = switchroomExecJson(["agent", "list"]);
|
|
60701
61123
|
const brokerState = switchroomExecJson(["auth", "show"]);
|
|
@@ -60712,6 +61134,7 @@ async function buildAgentMetadata(agentName3) {
|
|
|
60712
61134
|
return {
|
|
60713
61135
|
agentName: agentName3,
|
|
60714
61136
|
model: a?.model ?? null,
|
|
61137
|
+
sessionModel: activeSessionModelOverride,
|
|
60715
61138
|
extendsProfile: a?.extends ?? a?.template ?? null,
|
|
60716
61139
|
topicName: a?.topic_name ?? null,
|
|
60717
61140
|
topicEmoji: a?.topic_emoji ?? null,
|
|
@@ -60829,21 +61252,60 @@ bot.command("inject", async (ctx) => {
|
|
|
60829
61252
|
formatOutput: formatSwitchroomOutput
|
|
60830
61253
|
});
|
|
60831
61254
|
});
|
|
60832
|
-
|
|
60833
|
-
|
|
60834
|
-
|
|
60835
|
-
|
|
60836
|
-
|
|
60837
|
-
const reply = await handleModelCommand(parsed, {
|
|
60838
|
-
inject: injectSlashCommand,
|
|
61255
|
+
function buildModelDeps() {
|
|
61256
|
+
return {
|
|
61257
|
+
discover: (a) => discoverModels(a),
|
|
61258
|
+
select: (a, label) => selectModel(a, label),
|
|
61259
|
+
isBusy: () => currentTurn !== null,
|
|
60839
61260
|
getAgentName: getMyAgentName,
|
|
61261
|
+
getQuotaBrief: async () => {
|
|
61262
|
+
try {
|
|
61263
|
+
const probed = await probeQuotaForBootCard(getMyAgentName(), 4000);
|
|
61264
|
+
if (probed?.ok)
|
|
61265
|
+
return formatQuotaLine2(probed.data);
|
|
61266
|
+
} catch {}
|
|
61267
|
+
try {
|
|
61268
|
+
const agentDir = resolveAgentDirFromEnv();
|
|
61269
|
+
if (agentDir) {
|
|
61270
|
+
const local = await fetchQuota2({ claudeConfigDir: join35(agentDir, ".claude") });
|
|
61271
|
+
if (local.ok)
|
|
61272
|
+
return formatQuotaLine2(local.data);
|
|
61273
|
+
}
|
|
61274
|
+
} catch {}
|
|
61275
|
+
return null;
|
|
61276
|
+
},
|
|
61277
|
+
inject: injectSlashCommand,
|
|
60840
61278
|
getConfiguredModel: () => {
|
|
60841
61279
|
const data = switchroomExecJson(["agent", "list"]);
|
|
60842
61280
|
return data?.agents?.find((a) => a.name === getMyAgentName())?.model ?? null;
|
|
60843
61281
|
},
|
|
60844
61282
|
escapeHtml: escapeHtmlForTg,
|
|
60845
61283
|
preBlock
|
|
60846
|
-
}
|
|
61284
|
+
};
|
|
61285
|
+
}
|
|
61286
|
+
function modelMenuReplyMarkup(reply) {
|
|
61287
|
+
if (!reply.keyboard)
|
|
61288
|
+
return;
|
|
61289
|
+
const kb = new import_grammy9.InlineKeyboard;
|
|
61290
|
+
for (const row of reply.keyboard) {
|
|
61291
|
+
for (const btn of row)
|
|
61292
|
+
kb.text(btn.text, btn.callback_data);
|
|
61293
|
+
kb.row();
|
|
61294
|
+
}
|
|
61295
|
+
return kb;
|
|
61296
|
+
}
|
|
61297
|
+
bot.command("model", async (ctx) => {
|
|
61298
|
+
if (!isAuthorizedSender(ctx))
|
|
61299
|
+
return;
|
|
61300
|
+
const text = ctx.message?.text ?? ctx.channelPost?.text ?? "";
|
|
61301
|
+
const parsed = parseModelCommand(text) ?? { kind: "show" };
|
|
61302
|
+
const deps = buildModelDeps();
|
|
61303
|
+
if (parsed.kind === "show" && process.env.SWITCHROOM_MODEL_MENU !== "0") {
|
|
61304
|
+
const menu = await buildModelMenu(deps);
|
|
61305
|
+
await switchroomReply(ctx, menu.text, { html: true, reply_markup: modelMenuReplyMarkup(menu) });
|
|
61306
|
+
return;
|
|
61307
|
+
}
|
|
61308
|
+
const reply = await handleModelCommand(parsed, deps);
|
|
60847
61309
|
await switchroomReply(ctx, reply.text, { html: reply.html });
|
|
60848
61310
|
});
|
|
60849
61311
|
bot.command("agentstart", async (ctx) => {
|
|
@@ -63573,6 +64035,44 @@ bot.on("callback_query:data", async (ctx) => {
|
|
|
63573
64035
|
await handleAuthDashboardCallback(ctx);
|
|
63574
64036
|
return;
|
|
63575
64037
|
}
|
|
64038
|
+
if (data.startsWith(MODEL_CALLBACK_PREFIX)) {
|
|
64039
|
+
const access2 = loadAccess();
|
|
64040
|
+
const senderId2 = String(ctx.from?.id ?? "");
|
|
64041
|
+
if (!access2.allowFrom.includes(senderId2)) {
|
|
64042
|
+
await ctx.answerCallbackQuery({ text: "Not authorized." });
|
|
64043
|
+
return;
|
|
64044
|
+
}
|
|
64045
|
+
if (process.env.SWITCHROOM_MODEL_MENU === "0") {
|
|
64046
|
+
await ctx.answerCallbackQuery({ text: "Model menu is disabled (SWITCHROOM_MODEL_MENU=0)." }).catch(() => {});
|
|
64047
|
+
await ctx.editMessageText("Model menu is disabled on this agent. Use <code>/model <name></code>.", {
|
|
64048
|
+
parse_mode: "HTML",
|
|
64049
|
+
reply_markup: { inline_keyboard: [] }
|
|
64050
|
+
}).catch(() => {});
|
|
64051
|
+
return;
|
|
64052
|
+
}
|
|
64053
|
+
const modelDeps = buildModelDeps();
|
|
64054
|
+
if (modelDeps.isBusy()) {
|
|
64055
|
+
await ctx.answerCallbackQuery({ text: "\u23F3 Agent is mid-turn \u2014 tap again when it\u2019s idle", show_alert: false }).catch(() => {});
|
|
64056
|
+
return;
|
|
64057
|
+
}
|
|
64058
|
+
await ctx.answerCallbackQuery({ text: "Switching\u2026" }).catch(() => {});
|
|
64059
|
+
try {
|
|
64060
|
+
const outcome = await handleModelMenuCallback(data, modelDeps);
|
|
64061
|
+
if (outcome.selectedModel) {
|
|
64062
|
+
activeSessionModelOverride = outcome.selectedModel;
|
|
64063
|
+
}
|
|
64064
|
+
if (outcome.toastOnly)
|
|
64065
|
+
return;
|
|
64066
|
+
await ctx.editMessageText(outcome.reply.text, {
|
|
64067
|
+
parse_mode: "HTML",
|
|
64068
|
+
reply_markup: modelMenuReplyMarkup(outcome.reply) ?? { inline_keyboard: [] }
|
|
64069
|
+
}).catch(() => {});
|
|
64070
|
+
} catch (err) {
|
|
64071
|
+
process.stderr.write(`telegram gateway: model-menu callback failed: ${err?.message ?? String(err)}
|
|
64072
|
+
`);
|
|
64073
|
+
}
|
|
64074
|
+
return;
|
|
64075
|
+
}
|
|
63576
64076
|
if (data.startsWith("cn:")) {
|
|
63577
64077
|
const access2 = loadAccess();
|
|
63578
64078
|
const senderId2 = String(ctx.from?.id ?? "");
|