switchroom 0.15.18 → 0.15.20

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.15.18",
3
+ "version": "0.15.20",
4
4
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -38,22 +38,31 @@ tools is to let you do the edit yourself.
38
38
  derived from the entry content is assigned.
39
39
 
40
40
  **Mind the cost — pick the cheapest tier that does the job:**
41
- - *Default (no `model`)* — the fire runs as a **full turn in your live
42
- session**: your model, your whole context + memory. Right for work that
43
- genuinely needs *you* (your persona, your conversation history). Costly for
44
- routine checks every fire pays your full context.
45
- - *`model: "sonnet"`* (or `context: "fresh"`) routes the fire to a **cheap,
46
- minimal-context cron session** (Tier 1): a fresh Sonnet with just the
47
- prompt, no heavy context. Use for light, self-contained recurring work
48
- (summarise a feed, format a digest) where you don't need your memory. Much
49
- cheaper per fire. *(Honoured only when the operator has enabled cheap-cron;
50
- otherwise it still runs as a normal turn never silently dropped.)*
41
+ - *Default (no `model`)* — a **frequent** cron (every ≤60min) is auto-routed
42
+ to a **cheap, minimal-context cron session** (Tier 1: a fresh Sonnet that
43
+ still shares your memory + tools but drops your accumulated conversation
44
+ context). A **daily/weekly** cron defaults to a **full turn in your live
45
+ session** (Tier 2: your model, your whole context). So routine frequent
46
+ checks are already cheap without you doing anything.
47
+ - *`model: "sonnet"` / `context: "fresh"`* force the cheap Tier-1 session
48
+ even for a **daily/weekly** self-contained job (summarise a feed, format a
49
+ digest) overrides the cadence default. *`context: "agent"`* does the
50
+ opposite: pins a fire to your full live session when it genuinely needs your
51
+ accumulated conversation context (this always wins).
52
+ - *"Post/send a FIXED thing on a schedule"* (a set reminder message, a webhook
53
+ ping — text fully determined, no thinking) — ask the **operator** for a
54
+ **`kind: action`**: it runs **model-free, zero tokens** (no session at all),
55
+ posting your fixed/templated text or firing a fixed request. The cheapest
56
+ tier there is.
51
57
  - *"Only act when X changes"* — don't poll with a frequent prompt cron (every
52
- fire is a wasted turn when nothing changed). Ask the **operator** to set up
53
- a **poll** (model-free check, e.g. a webpage/API via `kind: poll`) or, for
54
- reaction-triggered work, **`reaction_dispatch`** (an emoji reaction wakes
55
- you instantly — zero polling). These need an operator config commit
56
- (egress/identity gates), so request them rather than authoring them yourself.
58
+ fire is a wasted turn when nothing changed). Ask the **operator** for a
59
+ **`kind: poll`** (model-free check, e.g. a webpage/API only a *change*
60
+ wakes you) or, for reaction-triggered work, **`reaction_dispatch`** (an
61
+ emoji reaction wakes you instantly — zero polling).
62
+ - `kind: action` / `kind: poll` / `reaction_dispatch` need an operator config
63
+ commit (egress / identity gates), so **request** them rather than authoring
64
+ them yourself — you can only self-author plain prompt crons (plus the
65
+ `model`/`context` tier hints).
57
66
 
58
67
  - **`schedule_remove(name | cron_hash)`** — delete by `name` (the slug from
59
68
  add) or by 12-hex `cron_hash` (shown in `cron_list` output). Both
@@ -43144,6 +43144,7 @@ var TELEGRAM_MENU_COMMANDS = [
43144
43144
  { command: "logs", description: "Show recent agent logs" },
43145
43145
  { command: "inject", description: "Inject a Claude Code slash command (e.g. /cost)" },
43146
43146
  { command: "model", description: "Show or switch the Claude model" },
43147
+ { command: "effort", description: "Show or switch the reasoning effort" },
43147
43148
  { command: "doctor", description: "Health check (deps, services, MCP)" },
43148
43149
  { command: "usage", description: "Pro/Max plan quota (5h + 7d windows)" },
43149
43150
  { command: "vault", description: "Manage vault secrets + capability grants" },
@@ -43176,6 +43177,7 @@ function switchroomHelpText(agentName3) {
43176
43177
  `<code>/update</code> \u2014 dry-run plan; <code>/update apply</code> \u2014 actually pull images, reconcile, restart`,
43177
43178
  `<code>/restart [name|all]</code> \u2014 bounce agent (drains in-flight turn by default)`,
43178
43179
  `<code>/version</code> \u2014 show versions + running agent health summary`,
43180
+ `<code>/whoami</code> \u2014 this agent's sandbox: tools, MCP, vault key-names, powers`,
43179
43181
  ``,
43180
43182
  `<b>Auth &amp; config</b>`,
43181
43183
  `<code>/auth</code> \u2014 auth status or actions`,
@@ -43185,6 +43187,7 @@ function switchroomHelpText(agentName3) {
43185
43187
  `<code>/auth rm [agent] &lt;slot&gt; [--force]</code> \u2014 remove a slot`,
43186
43188
  `<code>/model</code> \u2014 show the configured Claude model`,
43187
43189
  `<code>/model &lt;name&gt;</code> \u2014 switch the live session's model (opus \u00b7 sonnet \u00b7 haiku or a full id; until restart)`,
43190
+ `<code>/effort</code> \u2014 show or switch reasoning effort (low \u00b7 medium \u00b7 high \u00b7 xhigh \u00b7 max; until restart)`,
43188
43191
  `<code>/topics</code> \u2014 topic-to-agent mappings`,
43189
43192
  `<code>/permissions [agent]</code> \u2014 show agent permissions`,
43190
43193
  `<code>/grant &lt;tool&gt;</code> \u2014 grant a tool permission`,
@@ -44527,6 +44530,7 @@ var INJECT_COMMANDS = new Map([
44527
44530
  ["/hooks", { description: "List configured hooks", expectsOutput: true }],
44528
44531
  ["/memory", { description: "Open memory picker", expectsOutput: true }],
44529
44532
  ["/model", { description: "Open model picker", expectsOutput: true }],
44533
+ ["/effort", { description: "Set reasoning effort", expectsOutput: true }],
44530
44534
  [
44531
44535
  "/clear",
44532
44536
  { description: "Clear session screen", expectsOutput: false }
@@ -44784,6 +44788,7 @@ var INJECT_COMMANDS2 = new Map([
44784
44788
  ["/hooks", { description: "List configured hooks", expectsOutput: true }],
44785
44789
  ["/memory", { description: "Open memory picker", expectsOutput: true }],
44786
44790
  ["/model", { description: "Open model picker", expectsOutput: true }],
44791
+ ["/effort", { description: "Set reasoning effort", expectsOutput: true }],
44787
44792
  [
44788
44793
  "/clear",
44789
44794
  { description: "Clear session screen", expectsOutput: false }
@@ -45392,6 +45397,161 @@ function extractConfirmation(pane) {
45392
45397
  return null;
45393
45398
  }
45394
45399
 
45400
+ // gateway/effort-command.ts
45401
+ var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
45402
+ function isValidEffortArg(arg) {
45403
+ return EFFORT_LEVELS.includes(arg.toLowerCase());
45404
+ }
45405
+ function parseEffortCommand(text) {
45406
+ const m = text.match(/^\/effort(?:@[A-Za-z0-9_]+)?(?:\s+([\s\S]*))?$/);
45407
+ if (!m)
45408
+ return null;
45409
+ const rest = (m[1] ?? "").trim();
45410
+ if (rest.length === 0)
45411
+ return { kind: "show" };
45412
+ const parts = rest.split(/\s+/);
45413
+ if (parts.length > 1) {
45414
+ return { kind: "help", reason: "effort takes a single level" };
45415
+ }
45416
+ const arg = parts[0];
45417
+ if (arg.toLowerCase() === "help")
45418
+ return { kind: "help" };
45419
+ if (!isValidEffortArg(arg)) {
45420
+ return { kind: "help", reason: `not a valid effort level: ${arg}` };
45421
+ }
45422
+ return { kind: "set", level: arg.toLowerCase() };
45423
+ }
45424
+ var PERSIST_NOTE2 = "<i>Session-only \u2014 reverts to the configured default on restart. To change the default, set <code>thinking_effort:</code> in switchroom.yaml and restart.</i>";
45425
+ var LEVELS_INLINE = EFFORT_LEVELS.map((l) => `<code>${l}</code>`).join(" \u00b7 ");
45426
+ function helpText3(deps, reason) {
45427
+ const lines = [];
45428
+ if (reason)
45429
+ lines.push(`\u26a0\ufe0f ${deps.escapeHtml(reason)}`);
45430
+ lines.push("<b>/effort</b> \u2014 show or switch the reasoning effort (faster\u2192smarter)", "<code>/effort</code> \u2014 show the configured effort + a tap menu", `<code>/effort &lt;level&gt;</code> \u2014 switch the live session (${LEVELS_INLINE})`, PERSIST_NOTE2);
45431
+ return { text: lines.join(`
45432
+ `), html: true };
45433
+ }
45434
+ async function handleEffortCommand(parsed, deps) {
45435
+ if (parsed.kind === "help")
45436
+ return helpText3(deps, parsed.reason);
45437
+ if (parsed.kind === "show") {
45438
+ const configured = deps.getConfiguredEffort();
45439
+ const shown = configured && configured.length > 0 ? configured : "low";
45440
+ return {
45441
+ text: [
45442
+ `<b>Effort \u2014 ${deps.escapeHtml(deps.getAgentName())}</b>`,
45443
+ `Configured default: <code>${deps.escapeHtml(shown)}</code>`,
45444
+ `Switch the live session: ${EFFORT_LEVELS.map((l) => `<code>/effort ${l}</code>`).join(" \u00b7 ")}`,
45445
+ PERSIST_NOTE2
45446
+ ].join(`
45447
+ `),
45448
+ html: true
45449
+ };
45450
+ }
45451
+ if (!isValidEffortArg(parsed.level)) {
45452
+ return helpText3(deps, `not a valid effort level: ${parsed.level}`);
45453
+ }
45454
+ const verbHtml = `<code>/effort ${deps.escapeHtml(parsed.level)}</code>`;
45455
+ let result;
45456
+ try {
45457
+ result = await deps.inject(deps.getAgentName(), `/effort ${parsed.level}`);
45458
+ } catch (err) {
45459
+ const msg = err instanceof Error ? err.message : String(err);
45460
+ return { text: `\u274c ${verbHtml} \u2014 inject failed: ${deps.escapeHtml(msg)}`, html: true };
45461
+ }
45462
+ if (result.outcome === "ok") {
45463
+ return {
45464
+ text: [
45465
+ `${verbHtml}`,
45466
+ deps.preBlock(result.output),
45467
+ ...result.truncated ? ["<i>truncated</i>"] : [],
45468
+ PERSIST_NOTE2
45469
+ ].join(`
45470
+ `),
45471
+ html: true
45472
+ };
45473
+ }
45474
+ if (result.outcome === "ok_no_output") {
45475
+ return {
45476
+ text: [
45477
+ `${verbHtml} \u2014 sent, but no response captured. The agent may be mid-turn; check <code>/inject /status</code> to confirm the active effort.`,
45478
+ PERSIST_NOTE2
45479
+ ].join(`
45480
+ `),
45481
+ html: true
45482
+ };
45483
+ }
45484
+ if (result.errorCode === "session_missing") {
45485
+ return {
45486
+ text: "\u274c tmux session not found \u2014 the agent must be running under the tmux supervisor (the default). Remove <code>experimental.legacy_pty: true</code> if set.",
45487
+ html: true
45488
+ };
45489
+ }
45490
+ return {
45491
+ text: `\u274c ${verbHtml} \u2014 ${deps.escapeHtml(result.errorMessage ?? "inject failed")}`,
45492
+ html: true
45493
+ };
45494
+ }
45495
+ var EFFORT_CALLBACK_PREFIX = "eff:";
45496
+ var EFFORT_CALLBACK_SELECT = "eff:s:";
45497
+ function effortSelectCallbackData(level) {
45498
+ return `${EFFORT_CALLBACK_SELECT}${level}`;
45499
+ }
45500
+ function menuKeyboard2(highlight) {
45501
+ return [
45502
+ EFFORT_LEVELS.map((l) => ({
45503
+ text: l === highlight ? `\u2705 ${l}` : l,
45504
+ callback_data: effortSelectCallbackData(l)
45505
+ }))
45506
+ ];
45507
+ }
45508
+ function buildEffortMenu(deps, highlight) {
45509
+ const configured = deps.getConfiguredEffort() || "low";
45510
+ const live = highlight ?? configured;
45511
+ return {
45512
+ text: [
45513
+ `<b>Effort \u2014 ${deps.escapeHtml(deps.getAgentName())}</b>`,
45514
+ `Default: <code>${deps.escapeHtml(configured)}</code> \u00b7 faster \u2192 smarter: ${LEVELS_INLINE}`,
45515
+ "Tap to switch the live session:",
45516
+ PERSIST_NOTE2
45517
+ ].join(`
45518
+ `),
45519
+ html: true,
45520
+ keyboard: menuKeyboard2(live)
45521
+ };
45522
+ }
45523
+ async function handleEffortMenuCallback(data, deps) {
45524
+ if (!data.startsWith(EFFORT_CALLBACK_SELECT)) {
45525
+ return { reply: buildEffortMenu(deps) };
45526
+ }
45527
+ const level = data.slice(EFFORT_CALLBACK_SELECT.length);
45528
+ if (!isValidEffortArg(level)) {
45529
+ return { reply: buildEffortMenu(deps) };
45530
+ }
45531
+ let banner;
45532
+ let selected;
45533
+ try {
45534
+ const result = await deps.inject(deps.getAgentName(), `/effort ${level}`);
45535
+ if (result.outcome === "ok" || result.outcome === "ok_no_output") {
45536
+ banner = `\u2705 Effort \u2192 <code>${deps.escapeHtml(level)}</code> for this session`;
45537
+ selected = level;
45538
+ } else if (result.errorCode === "session_missing") {
45539
+ banner = "\u274c tmux session not found \u2014 is the agent running under the supervisor?";
45540
+ } else {
45541
+ banner = `\u274c couldn't set effort: ${deps.escapeHtml(result.errorMessage ?? "inject failed")}`;
45542
+ }
45543
+ } catch (err) {
45544
+ const msg = err instanceof Error ? err.message : String(err);
45545
+ banner = `\u274c inject failed: ${deps.escapeHtml(msg)}`;
45546
+ }
45547
+ const menu = buildEffortMenu(deps, selected);
45548
+ return {
45549
+ reply: { ...menu, text: `${banner}
45550
+ ${menu.text}` },
45551
+ selectedEffort: selected
45552
+ };
45553
+ }
45554
+
45395
45555
  // ../src/config/loader.ts
45396
45556
  init_dist();
45397
45557
  init_zod();
@@ -53487,6 +53647,15 @@ function readBashCommand(inputPreview) {
53487
53647
  }
53488
53648
  }
53489
53649
 
53650
+ // gateway/grant-restart.ts
53651
+ function grantRestartDecision(opts) {
53652
+ if ((opts.killSwitch ?? "") === "0")
53653
+ return "disabled";
53654
+ if (!opts.selfAgent || opts.selfAgent !== opts.agentName)
53655
+ return "disabled";
53656
+ return opts.turnInFlight ? "deferred" : "now";
53657
+ }
53658
+
53490
53659
  // permission-diff.ts
53491
53660
  var TARGET_HEADER_A = "--- a/switchroom.yaml";
53492
53661
  var TARGET_HEADER_B = "+++ b/switchroom.yaml";
@@ -54158,11 +54327,11 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
54158
54327
  }
54159
54328
 
54160
54329
  // ../src/build-info.ts
54161
- var VERSION = "0.15.18";
54162
- var COMMIT_SHA = "d7c044b9";
54163
- var COMMIT_DATE = "2026-06-14T08:55:07+10:00";
54330
+ var VERSION = "0.15.20";
54331
+ var COMMIT_SHA = "0b63ab9e";
54332
+ var COMMIT_DATE = "2026-06-14T10:58:14+10:00";
54164
54333
  var LATEST_PR = null;
54165
- var COMMITS_AHEAD_OF_TAG = 3;
54334
+ var COMMITS_AHEAD_OF_TAG = 5;
54166
54335
 
54167
54336
  // gateway/boot-version.ts
54168
54337
  function formatRelativeAgo(iso) {
@@ -61613,6 +61782,26 @@ async function sweepBeforeSelfRestart() {
61613
61782
  `);
61614
61783
  }
61615
61784
  }
61785
+ function scheduleGrantRestart(agentName3, chatId, threadId, reason) {
61786
+ const decision = grantRestartDecision({
61787
+ killSwitch: process.env.SWITCHROOM_AUTORESTART_ON_GRANT,
61788
+ selfAgent: process.env.SWITCHROOM_AGENT_NAME,
61789
+ agentName: agentName3,
61790
+ turnInFlight: turnInFlightForGate()
61791
+ });
61792
+ if (decision === "disabled")
61793
+ return decision;
61794
+ if (chatId != null) {
61795
+ writeRestartMarker({ chat_id: String(chatId), thread_id: threadId ?? null, ack_message_id: null, ts: Date.now() });
61796
+ }
61797
+ stampUserRestartReason(reason);
61798
+ if (decision === "deferred") {
61799
+ pendingRestarts.set(agentName3, Date.now());
61800
+ } else {
61801
+ sweepBeforeSelfRestart().finally(() => triggerSelfRestart(agentName3, reason, 1500));
61802
+ }
61803
+ return decision;
61804
+ }
61616
61805
  function formatAuthOutputForTelegram(output) {
61617
61806
  const trimmed = stripAnsi2(output).trim();
61618
61807
  const url = trimmed.match(/https:\/\/\S+/)?.[0] ?? null;
@@ -62143,6 +62332,43 @@ bot.command("model", async (ctx) => {
62143
62332
  const reply = await handleModelCommand(parsed, deps);
62144
62333
  await switchroomReply(ctx, reply.text, { html: reply.html });
62145
62334
  });
62335
+ function buildEffortDeps() {
62336
+ return {
62337
+ inject: injectSlashCommand,
62338
+ getAgentName: getMyAgentName,
62339
+ getConfiguredEffort: () => {
62340
+ const data = switchroomExecJson(["agent", "list"]);
62341
+ return data?.agents?.find((a) => a.name === getMyAgentName())?.thinking_effort ?? null;
62342
+ },
62343
+ escapeHtml: escapeHtmlForTg,
62344
+ preBlock
62345
+ };
62346
+ }
62347
+ function effortMenuReplyMarkup(reply) {
62348
+ if (!reply.keyboard)
62349
+ return;
62350
+ const kb = new import_grammy9.InlineKeyboard;
62351
+ for (const row of reply.keyboard) {
62352
+ for (const btn of row)
62353
+ kb.text(btn.text, btn.callback_data);
62354
+ kb.row();
62355
+ }
62356
+ return kb;
62357
+ }
62358
+ bot.command("effort", async (ctx) => {
62359
+ if (!isAuthorizedSender(ctx))
62360
+ return;
62361
+ const text = ctx.message?.text ?? ctx.channelPost?.text ?? "";
62362
+ const parsed = parseEffortCommand(text) ?? { kind: "show" };
62363
+ const deps = buildEffortDeps();
62364
+ if (parsed.kind === "show") {
62365
+ const menu = buildEffortMenu(deps);
62366
+ await switchroomReply(ctx, menu.text, { html: true, reply_markup: effortMenuReplyMarkup(menu) });
62367
+ return;
62368
+ }
62369
+ const reply = await handleEffortCommand(parsed, deps);
62370
+ await switchroomReply(ctx, reply.text, { html: reply.html });
62371
+ });
62146
62372
  bot.command("agentstart", async (ctx) => {
62147
62373
  if (!isAuthorizedSender(ctx))
62148
62374
  return;
@@ -64855,6 +65081,56 @@ bot.command("version", async (ctx) => {
64855
65081
  ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: true });
64856
65082
  }
64857
65083
  });
65084
+ bot.command("whoami", async (ctx) => {
65085
+ if (!isAuthorizedSender(ctx))
65086
+ return;
65087
+ try {
65088
+ let raw;
65089
+ try {
65090
+ raw = switchroomExecCombined(["config", "whoami"], 1e4);
65091
+ } catch (err) {
65092
+ raw = err.stdout ?? err.message ?? "whoami failed";
65093
+ }
65094
+ const trimmed = stripAnsi2(raw).trim();
65095
+ let card;
65096
+ try {
65097
+ card = formatWhoamiCard(JSON.parse(trimmed.split(`
65098
+ `).pop() ?? trimmed));
65099
+ } catch {
65100
+ card = preBlock(formatSwitchroomOutput(trimmed || "whoami: no output"));
65101
+ }
65102
+ await switchroomReply(ctx, card, { html: true });
65103
+ } catch (err) {
65104
+ await switchroomReply(ctx, `<b>whoami failed:</b>
65105
+ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: true });
65106
+ }
65107
+ });
65108
+ function formatWhoamiCard(v) {
65109
+ const esc = escapeHtmlForTg;
65110
+ const yn = (b) => b ? "\u2713" : "\u2717";
65111
+ const lines = [];
65112
+ lines.push(`\uD83D\uDC64 <b>${esc(v.name ?? "?")}</b> \xB7 ${esc(v.tier ?? "standard")}`);
65113
+ if (v.persona)
65114
+ lines.push(esc(v.persona));
65115
+ if (v.model)
65116
+ lines.push(`Model: ${esc(v.model)}`);
65117
+ const allow = v.tools?.allow ?? [];
65118
+ lines.push(`Tools: ${allow.length ? esc(allow.slice(0, 8).join(", ")) + (allow.length > 8 ? ` \u2026(+${allow.length - 8})` : "") : "\u2014"}`);
65119
+ if ((v.tools?.deny ?? []).length)
65120
+ lines.push(`Denied: ${esc(v.tools.deny.join(", "))}`);
65121
+ if ((v.mcpServers ?? []).length)
65122
+ lines.push(`MCP: ${esc(v.mcpServers.join(", "))}`);
65123
+ if ((v.skills ?? []).length)
65124
+ lines.push(`Skills: ${esc(v.skills.join(", "))}`);
65125
+ if ((v.vault ?? []).length) {
65126
+ lines.push(`Vault keys (names only): ${v.vault.map((k) => `${esc(k.key)} ${yn(k.readable)}`).join(", ")}`);
65127
+ }
65128
+ const p = v.powers ?? {};
65129
+ lines.push(`Powers: admin ${yn(p.admin)} \xB7 root ${yn(p.root)} \xB7 config-edit ${yn(p.configEdit)} \xB7 cross-agent verbs ${yn(p.crossAgentHostVerbs)}`);
65130
+ lines.push(`Schedule: ${v.scheduleCount ?? 0} cron \xB7 Memory: ${esc(v.memoryBackend ?? "none")}`);
65131
+ return lines.join(`
65132
+ `);
65133
+ }
64858
65134
  bot.command("commands", async (ctx) => {
64859
65135
  if (!isAuthorizedSender(ctx))
64860
65136
  return;
@@ -64870,6 +65146,26 @@ bot.on("callback_query:data", async (ctx) => {
64870
65146
  await handleAuthDashboardCallback(ctx);
64871
65147
  return;
64872
65148
  }
65149
+ if (data.startsWith(EFFORT_CALLBACK_PREFIX)) {
65150
+ const access2 = loadAccess();
65151
+ const senderId2 = String(ctx.from?.id ?? "");
65152
+ if (!access2.allowFrom.includes(senderId2)) {
65153
+ await ctx.answerCallbackQuery({ text: "Not authorized." });
65154
+ return;
65155
+ }
65156
+ await ctx.answerCallbackQuery({ text: "Setting effort\u2026" }).catch(() => {});
65157
+ try {
65158
+ const outcome = await handleEffortMenuCallback(data, buildEffortDeps());
65159
+ await ctx.editMessageText(outcome.reply.text, {
65160
+ parse_mode: "HTML",
65161
+ reply_markup: effortMenuReplyMarkup(outcome.reply) ?? { inline_keyboard: [] }
65162
+ }).catch(() => {});
65163
+ } catch (err) {
65164
+ process.stderr.write(`telegram gateway: effort-menu callback failed: ${err?.message ?? String(err)}
65165
+ `);
65166
+ }
65167
+ return;
65168
+ }
64873
65169
  if (data.startsWith(MODEL_CALLBACK_PREFIX)) {
64874
65170
  const access2 = loadAccess();
64875
65171
  const senderId2 = String(ctx.from?.id ?? "");
@@ -65299,10 +65595,12 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
65299
65595
  }
65300
65596
  const ok = durable;
65301
65597
  const legacyNote = legacy && durable;
65302
- const ackText2 = ok ? legacyNote ? `\u2705 Saved. ${agentName3} can now ${grantPhrase} without asking (legacy path).` : `\u2705 Saved. ${agentName3} can now ${grantPhrase} without asking.` : editLockHint ? `\u26A0\uFE0F Allowed for now \u2014 config edits are locked. Enable hostd.config_edit_enabled.` : `\u26A0\uFE0F Allowed for now, but "always" did NOT save \u2014 it will ask again after restart. Check gateway log.`;
65598
+ const restartScheduled = ok && !legacy && scheduleGrantRestart(agentName3, ctx.chat?.id, ctx.callbackQuery?.message?.message_thread_id, `always-allow: ${grantPhrase}`) !== "disabled";
65599
+ const liveSuffix = restartScheduled ? " \u2014 applying now (restarting to take effect)" : "";
65600
+ const ackText2 = ok ? legacyNote ? `\u2705 Saved. ${agentName3} can now ${grantPhrase} without asking (legacy path).` : `\u2705 Saved. ${agentName3} can now ${grantPhrase} without asking.${liveSuffix}` : editLockHint ? `\u26A0\uFE0F Allowed for now \u2014 config edits are locked. Enable hostd.config_edit_enabled.` : `\u26A0\uFE0F Allowed for now, but "always" did NOT save \u2014 it will ask again after restart. Check gateway log.`;
65303
65601
  const sourceMsg = ctx.callbackQuery?.message;
65304
65602
  const baseText2 = sourceMsg && "text" in sourceMsg && sourceMsg.text ? escapeHtmlForTg(sourceMsg.text) : "";
65305
- const editLabel = ok ? legacyNote ? `\u2705 <b>${escapeHtmlForTg(agentName3)} can now ${escapeHtmlForTg(grantPhrase)}</b> without asking (legacy path); restart agent for full effect` : `\u2705 <b>${escapeHtmlForTg(agentName3)} can now ${escapeHtmlForTg(grantPhrase)}</b> without asking; restart agent for full effect` : editLockHint ? `\u26A0\uFE0F <b>Allowed for now \u2014 "always" did NOT save.</b> Config edits are locked; enable <code>hostd.config_edit_enabled</code>.` : `\u26A0\uFE0F <b>Allowed for now \u2014 "always" did NOT save.</b> It will ask again after restart. Check gateway log.`;
65603
+ const editLabel = ok ? legacyNote ? `\u2705 <b>${escapeHtmlForTg(agentName3)} can now ${escapeHtmlForTg(grantPhrase)}</b> without asking (legacy path); restart agent for full effect` : restartScheduled ? `\u2705 <b>${escapeHtmlForTg(agentName3)} can now ${escapeHtmlForTg(grantPhrase)}</b> without asking \u2014 applying now (restarting to take effect).` : `\u2705 <b>${escapeHtmlForTg(agentName3)} can now ${escapeHtmlForTg(grantPhrase)}</b> without asking; restart agent for full effect` : editLockHint ? `\u26A0\uFE0F <b>Allowed for now \u2014 "always" did NOT save.</b> Config edits are locked; enable <code>hostd.config_edit_enabled</code>.` : `\u26A0\uFE0F <b>Allowed for now \u2014 "always" did NOT save.</b> It will ask again after restart. Check gateway log.`;
65306
65604
  await finalizeCallback(ctx, {
65307
65605
  ackText: ackText2.slice(0, 200),
65308
65606
  newText: baseText2 ? `${baseText2}