switchroom 0.15.17 → 0.15.19

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.
@@ -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" },
@@ -43185,6 +43186,7 @@ function switchroomHelpText(agentName3) {
43185
43186
  `<code>/auth rm [agent] &lt;slot&gt; [--force]</code> \u2014 remove a slot`,
43186
43187
  `<code>/model</code> \u2014 show the configured Claude model`,
43187
43188
  `<code>/model &lt;name&gt;</code> \u2014 switch the live session's model (opus \u00b7 sonnet \u00b7 haiku or a full id; until restart)`,
43189
+ `<code>/effort</code> \u2014 show or switch reasoning effort (low \u00b7 medium \u00b7 high \u00b7 xhigh \u00b7 max; until restart)`,
43188
43190
  `<code>/topics</code> \u2014 topic-to-agent mappings`,
43189
43191
  `<code>/permissions [agent]</code> \u2014 show agent permissions`,
43190
43192
  `<code>/grant &lt;tool&gt;</code> \u2014 grant a tool permission`,
@@ -44527,6 +44529,7 @@ var INJECT_COMMANDS = new Map([
44527
44529
  ["/hooks", { description: "List configured hooks", expectsOutput: true }],
44528
44530
  ["/memory", { description: "Open memory picker", expectsOutput: true }],
44529
44531
  ["/model", { description: "Open model picker", expectsOutput: true }],
44532
+ ["/effort", { description: "Set reasoning effort", expectsOutput: true }],
44530
44533
  [
44531
44534
  "/clear",
44532
44535
  { description: "Clear session screen", expectsOutput: false }
@@ -44784,6 +44787,7 @@ var INJECT_COMMANDS2 = new Map([
44784
44787
  ["/hooks", { description: "List configured hooks", expectsOutput: true }],
44785
44788
  ["/memory", { description: "Open memory picker", expectsOutput: true }],
44786
44789
  ["/model", { description: "Open model picker", expectsOutput: true }],
44790
+ ["/effort", { description: "Set reasoning effort", expectsOutput: true }],
44787
44791
  [
44788
44792
  "/clear",
44789
44793
  { description: "Clear session screen", expectsOutput: false }
@@ -45392,6 +45396,161 @@ function extractConfirmation(pane) {
45392
45396
  return null;
45393
45397
  }
45394
45398
 
45399
+ // gateway/effort-command.ts
45400
+ var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
45401
+ function isValidEffortArg(arg) {
45402
+ return EFFORT_LEVELS.includes(arg.toLowerCase());
45403
+ }
45404
+ function parseEffortCommand(text) {
45405
+ const m = text.match(/^\/effort(?:@[A-Za-z0-9_]+)?(?:\s+([\s\S]*))?$/);
45406
+ if (!m)
45407
+ return null;
45408
+ const rest = (m[1] ?? "").trim();
45409
+ if (rest.length === 0)
45410
+ return { kind: "show" };
45411
+ const parts = rest.split(/\s+/);
45412
+ if (parts.length > 1) {
45413
+ return { kind: "help", reason: "effort takes a single level" };
45414
+ }
45415
+ const arg = parts[0];
45416
+ if (arg.toLowerCase() === "help")
45417
+ return { kind: "help" };
45418
+ if (!isValidEffortArg(arg)) {
45419
+ return { kind: "help", reason: `not a valid effort level: ${arg}` };
45420
+ }
45421
+ return { kind: "set", level: arg.toLowerCase() };
45422
+ }
45423
+ 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>";
45424
+ var LEVELS_INLINE = EFFORT_LEVELS.map((l) => `<code>${l}</code>`).join(" \u00b7 ");
45425
+ function helpText3(deps, reason) {
45426
+ const lines = [];
45427
+ if (reason)
45428
+ lines.push(`\u26a0\ufe0f ${deps.escapeHtml(reason)}`);
45429
+ 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);
45430
+ return { text: lines.join(`
45431
+ `), html: true };
45432
+ }
45433
+ async function handleEffortCommand(parsed, deps) {
45434
+ if (parsed.kind === "help")
45435
+ return helpText3(deps, parsed.reason);
45436
+ if (parsed.kind === "show") {
45437
+ const configured = deps.getConfiguredEffort();
45438
+ const shown = configured && configured.length > 0 ? configured : "low";
45439
+ return {
45440
+ text: [
45441
+ `<b>Effort \u2014 ${deps.escapeHtml(deps.getAgentName())}</b>`,
45442
+ `Configured default: <code>${deps.escapeHtml(shown)}</code>`,
45443
+ `Switch the live session: ${EFFORT_LEVELS.map((l) => `<code>/effort ${l}</code>`).join(" \u00b7 ")}`,
45444
+ PERSIST_NOTE2
45445
+ ].join(`
45446
+ `),
45447
+ html: true
45448
+ };
45449
+ }
45450
+ if (!isValidEffortArg(parsed.level)) {
45451
+ return helpText3(deps, `not a valid effort level: ${parsed.level}`);
45452
+ }
45453
+ const verbHtml = `<code>/effort ${deps.escapeHtml(parsed.level)}</code>`;
45454
+ let result;
45455
+ try {
45456
+ result = await deps.inject(deps.getAgentName(), `/effort ${parsed.level}`);
45457
+ } catch (err) {
45458
+ const msg = err instanceof Error ? err.message : String(err);
45459
+ return { text: `\u274c ${verbHtml} \u2014 inject failed: ${deps.escapeHtml(msg)}`, html: true };
45460
+ }
45461
+ if (result.outcome === "ok") {
45462
+ return {
45463
+ text: [
45464
+ `${verbHtml}`,
45465
+ deps.preBlock(result.output),
45466
+ ...result.truncated ? ["<i>truncated</i>"] : [],
45467
+ PERSIST_NOTE2
45468
+ ].join(`
45469
+ `),
45470
+ html: true
45471
+ };
45472
+ }
45473
+ if (result.outcome === "ok_no_output") {
45474
+ return {
45475
+ text: [
45476
+ `${verbHtml} \u2014 sent, but no response captured. The agent may be mid-turn; check <code>/inject /status</code> to confirm the active effort.`,
45477
+ PERSIST_NOTE2
45478
+ ].join(`
45479
+ `),
45480
+ html: true
45481
+ };
45482
+ }
45483
+ if (result.errorCode === "session_missing") {
45484
+ return {
45485
+ 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.",
45486
+ html: true
45487
+ };
45488
+ }
45489
+ return {
45490
+ text: `\u274c ${verbHtml} \u2014 ${deps.escapeHtml(result.errorMessage ?? "inject failed")}`,
45491
+ html: true
45492
+ };
45493
+ }
45494
+ var EFFORT_CALLBACK_PREFIX = "eff:";
45495
+ var EFFORT_CALLBACK_SELECT = "eff:s:";
45496
+ function effortSelectCallbackData(level) {
45497
+ return `${EFFORT_CALLBACK_SELECT}${level}`;
45498
+ }
45499
+ function menuKeyboard2(highlight) {
45500
+ return [
45501
+ EFFORT_LEVELS.map((l) => ({
45502
+ text: l === highlight ? `\u2705 ${l}` : l,
45503
+ callback_data: effortSelectCallbackData(l)
45504
+ }))
45505
+ ];
45506
+ }
45507
+ function buildEffortMenu(deps, highlight) {
45508
+ const configured = deps.getConfiguredEffort() || "low";
45509
+ const live = highlight ?? configured;
45510
+ return {
45511
+ text: [
45512
+ `<b>Effort \u2014 ${deps.escapeHtml(deps.getAgentName())}</b>`,
45513
+ `Default: <code>${deps.escapeHtml(configured)}</code> \u00b7 faster \u2192 smarter: ${LEVELS_INLINE}`,
45514
+ "Tap to switch the live session:",
45515
+ PERSIST_NOTE2
45516
+ ].join(`
45517
+ `),
45518
+ html: true,
45519
+ keyboard: menuKeyboard2(live)
45520
+ };
45521
+ }
45522
+ async function handleEffortMenuCallback(data, deps) {
45523
+ if (!data.startsWith(EFFORT_CALLBACK_SELECT)) {
45524
+ return { reply: buildEffortMenu(deps) };
45525
+ }
45526
+ const level = data.slice(EFFORT_CALLBACK_SELECT.length);
45527
+ if (!isValidEffortArg(level)) {
45528
+ return { reply: buildEffortMenu(deps) };
45529
+ }
45530
+ let banner;
45531
+ let selected;
45532
+ try {
45533
+ const result = await deps.inject(deps.getAgentName(), `/effort ${level}`);
45534
+ if (result.outcome === "ok" || result.outcome === "ok_no_output") {
45535
+ banner = `\u2705 Effort \u2192 <code>${deps.escapeHtml(level)}</code> for this session`;
45536
+ selected = level;
45537
+ } else if (result.errorCode === "session_missing") {
45538
+ banner = "\u274c tmux session not found \u2014 is the agent running under the supervisor?";
45539
+ } else {
45540
+ banner = `\u274c couldn't set effort: ${deps.escapeHtml(result.errorMessage ?? "inject failed")}`;
45541
+ }
45542
+ } catch (err) {
45543
+ const msg = err instanceof Error ? err.message : String(err);
45544
+ banner = `\u274c inject failed: ${deps.escapeHtml(msg)}`;
45545
+ }
45546
+ const menu = buildEffortMenu(deps, selected);
45547
+ return {
45548
+ reply: { ...menu, text: `${banner}
45549
+ ${menu.text}` },
45550
+ selectedEffort: selected
45551
+ };
45552
+ }
45553
+
45395
45554
  // ../src/config/loader.ts
45396
45555
  init_dist();
45397
45556
  init_zod();
@@ -54158,10 +54317,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
54158
54317
  }
54159
54318
 
54160
54319
  // ../src/build-info.ts
54161
- var VERSION = "0.15.17";
54162
- var COMMIT_SHA = "a5129bd3";
54163
- var COMMIT_DATE = "2026-06-13T22:07:37Z";
54164
- var LATEST_PR = 2332;
54320
+ var VERSION = "0.15.19";
54321
+ var COMMIT_SHA = "3f40c6f9";
54322
+ var COMMIT_DATE = "2026-06-14T00:06:47Z";
54323
+ var LATEST_PR = 2337;
54165
54324
  var COMMITS_AHEAD_OF_TAG = 0;
54166
54325
 
54167
54326
  // gateway/boot-version.ts
@@ -57206,10 +57365,8 @@ if (inboundSpool != null) {
57206
57365
  }
57207
57366
  }
57208
57367
  var pendingPermissionBuffer = createPendingPermissionBuffer();
57209
- function buildPermissionActionRow(requestId, showAlways, showTimeBox = false) {
57210
- const kb = new import_grammy9.InlineKeyboard().text("\u274C Deny", `perm:deny:${requestId}`).text("\u2705 Allow once", `perm:allow:${requestId}`);
57211
- if (showTimeBox)
57212
- kb.text("\u23F1 30 min", `perm:tmb:${requestId}`);
57368
+ function buildPermissionActionRow(requestId, showAlways) {
57369
+ const kb = new import_grammy9.InlineKeyboard().text("\u274C Deny", `perm:deny:${requestId}`).text("\u2705 Allow", `perm:allow:${requestId}`);
57213
57370
  if (showAlways)
57214
57371
  kb.text("\uD83D\uDD01 Always\u2026", `perm:always:${requestId}`);
57215
57372
  return kb;
@@ -57465,10 +57622,8 @@ var ipcServer = createIpcServer({
57465
57622
  description,
57466
57623
  agentName: _client.agentName
57467
57624
  });
57468
- const scopeChoices = resolveScopedAllowChoices(toolName, inputPreview);
57469
- const showAlways = scopeChoices != null;
57470
- const showTimeBox = scopedApprovalTtlMs() > 0 && resolveTimeBox(toolName, inputPreview, scopeChoices) != null;
57471
- const keyboard = buildPermissionActionRow(requestId, showAlways, showTimeBox);
57625
+ const showAlways = resolveScopedAllowChoices(toolName, inputPreview) != null;
57626
+ const keyboard = buildPermissionActionRow(requestId, showAlways);
57472
57627
  const activeTurn = currentTurn;
57473
57628
  const targets = resolvePermissionCardTargets();
57474
57629
  for (const { chatId, threadId } of targets) {
@@ -62147,6 +62302,43 @@ bot.command("model", async (ctx) => {
62147
62302
  const reply = await handleModelCommand(parsed, deps);
62148
62303
  await switchroomReply(ctx, reply.text, { html: reply.html });
62149
62304
  });
62305
+ function buildEffortDeps() {
62306
+ return {
62307
+ inject: injectSlashCommand,
62308
+ getAgentName: getMyAgentName,
62309
+ getConfiguredEffort: () => {
62310
+ const data = switchroomExecJson(["agent", "list"]);
62311
+ return data?.agents?.find((a) => a.name === getMyAgentName())?.thinking_effort ?? null;
62312
+ },
62313
+ escapeHtml: escapeHtmlForTg,
62314
+ preBlock
62315
+ };
62316
+ }
62317
+ function effortMenuReplyMarkup(reply) {
62318
+ if (!reply.keyboard)
62319
+ return;
62320
+ const kb = new import_grammy9.InlineKeyboard;
62321
+ for (const row of reply.keyboard) {
62322
+ for (const btn of row)
62323
+ kb.text(btn.text, btn.callback_data);
62324
+ kb.row();
62325
+ }
62326
+ return kb;
62327
+ }
62328
+ bot.command("effort", async (ctx) => {
62329
+ if (!isAuthorizedSender(ctx))
62330
+ return;
62331
+ const text = ctx.message?.text ?? ctx.channelPost?.text ?? "";
62332
+ const parsed = parseEffortCommand(text) ?? { kind: "show" };
62333
+ const deps = buildEffortDeps();
62334
+ if (parsed.kind === "show") {
62335
+ const menu = buildEffortMenu(deps);
62336
+ await switchroomReply(ctx, menu.text, { html: true, reply_markup: effortMenuReplyMarkup(menu) });
62337
+ return;
62338
+ }
62339
+ const reply = await handleEffortCommand(parsed, deps);
62340
+ await switchroomReply(ctx, reply.text, { html: reply.html });
62341
+ });
62150
62342
  bot.command("agentstart", async (ctx) => {
62151
62343
  if (!isAuthorizedSender(ctx))
62152
62344
  return;
@@ -64874,6 +65066,26 @@ bot.on("callback_query:data", async (ctx) => {
64874
65066
  await handleAuthDashboardCallback(ctx);
64875
65067
  return;
64876
65068
  }
65069
+ if (data.startsWith(EFFORT_CALLBACK_PREFIX)) {
65070
+ const access2 = loadAccess();
65071
+ const senderId2 = String(ctx.from?.id ?? "");
65072
+ if (!access2.allowFrom.includes(senderId2)) {
65073
+ await ctx.answerCallbackQuery({ text: "Not authorized." });
65074
+ return;
65075
+ }
65076
+ await ctx.answerCallbackQuery({ text: "Setting effort\u2026" }).catch(() => {});
65077
+ try {
65078
+ const outcome = await handleEffortMenuCallback(data, buildEffortDeps());
65079
+ await ctx.editMessageText(outcome.reply.text, {
65080
+ parse_mode: "HTML",
65081
+ reply_markup: effortMenuReplyMarkup(outcome.reply) ?? { inline_keyboard: [] }
65082
+ }).catch(() => {});
65083
+ } catch (err) {
65084
+ process.stderr.write(`telegram gateway: effort-menu callback failed: ${err?.message ?? String(err)}
65085
+ `);
65086
+ }
65087
+ return;
65088
+ }
64877
65089
  if (data.startsWith(MODEL_CALLBACK_PREFIX)) {
64878
65090
  const access2 = loadAccess();
64879
65091
  const senderId2 = String(ctx.from?.id ?? "");
@@ -65080,8 +65292,8 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
65080
65292
  const cbMessageId = ctx.callbackQuery?.message?.message_id;
65081
65293
  const metaForMessage = cbMessageId != null ? agentButtonMeta.get(`${cbChatId}:${cbMessageId}`) : undefined;
65082
65294
  const tapMeta = metaForMessage?.get(agentCb.raw);
65083
- const ackText = typeof tapMeta?.ack_text === "string" && tapMeta.ack_text.length > 0 ? tapMeta.ack_text : "\u2713 received";
65084
- await ctx.answerCallbackQuery({ text: ackText }).catch(() => {});
65295
+ const ackText2 = typeof tapMeta?.ack_text === "string" && tapMeta.ack_text.length > 0 ? tapMeta.ack_text : "\u2713 received";
65296
+ await ctx.answerCallbackQuery({ text: ackText2 }).catch(() => {});
65085
65297
  const buttonText = (() => {
65086
65298
  const msg2 = ctx.callbackQuery?.message;
65087
65299
  const kb = msg2 && "reply_markup" in msg2 ? msg2.reply_markup : undefined;
@@ -65150,7 +65362,7 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
65150
65362
  }
65151
65363
  return;
65152
65364
  }
65153
- const m = /^perm:(allow|deny|always|asn|asb|back|tmb):([a-km-z]{5})$/.exec(data);
65365
+ const m = /^perm:(allow|deny|always|asn|asb|back):([a-km-z]{5})$/.exec(data);
65154
65366
  if (!m) {
65155
65367
  await ctx.answerCallbackQuery().catch(() => {});
65156
65368
  return;
@@ -65170,9 +65382,7 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
65170
65382
  }
65171
65383
  let keyboard;
65172
65384
  if (behavior === "back") {
65173
- const backChoices = resolveScopedAllowChoices(details.tool_name, details.input_preview);
65174
- const backTimeBox = scopedApprovalTtlMs() > 0 && resolveTimeBox(details.tool_name, details.input_preview, backChoices) != null;
65175
- keyboard = buildPermissionActionRow(request_id, true, backTimeBox);
65385
+ keyboard = buildPermissionActionRow(request_id, true);
65176
65386
  } else {
65177
65387
  const choices = resolveScopedAllowChoices(details.tool_name, details.input_preview);
65178
65388
  if (choices == null) {
@@ -65305,12 +65515,12 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
65305
65515
  }
65306
65516
  const ok = durable;
65307
65517
  const legacyNote = legacy && durable;
65308
- const ackText = 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.`;
65518
+ 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.`;
65309
65519
  const sourceMsg = ctx.callbackQuery?.message;
65310
65520
  const baseText2 = sourceMsg && "text" in sourceMsg && sourceMsg.text ? escapeHtmlForTg(sourceMsg.text) : "";
65311
65521
  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.`;
65312
65522
  await finalizeCallback(ctx, {
65313
- ackText: ackText.slice(0, 200),
65523
+ ackText: ackText2.slice(0, 200),
65314
65524
  newText: baseText2 ? `${baseText2}
65315
65525
 
65316
65526
  ${editLabel}` : editLabel,
@@ -65318,64 +65528,28 @@ ${editLabel}` : editLabel,
65318
65528
  });
65319
65529
  return;
65320
65530
  }
65321
- if (behavior === "tmb") {
65322
- const details = pendingPermissions.get(request_id);
65323
- if (!details) {
65324
- await ctx.answerCallbackQuery({ text: "Details no longer available." }).catch(() => {});
65325
- return;
65326
- }
65327
- const ttl = scopedApprovalTtlMs();
65328
- if (ttl <= 0) {
65329
- await ctx.answerCallbackQuery({ text: "Time-boxed approvals are disabled." }).catch(() => {});
65330
- return;
65331
- }
65332
- const choices = resolveScopedAllowChoices(details.tool_name, details.input_preview);
65333
- const tb = resolveTimeBox(details.tool_name, details.input_preview, choices);
65334
- if (!tb) {
65335
- await ctx.answerCallbackQuery({ text: "This action can't be time-boxed." }).catch(() => {});
65336
- return;
65337
- }
65338
- const agentName3 = selfAgentName();
65339
- if (!agentName3) {
65340
- await ctx.answerCallbackQuery({ text: "Time-box needs SWITCHROOM_AGENT_NAME \u2014 gateway is misconfigured." }).catch(() => {});
65341
- return;
65342
- }
65343
- pendingPermissions.delete(request_id);
65344
- dispatchPermissionVerdict({ type: "permission", requestId: request_id, behavior: "allow" });
65345
- recordScopedGrant(scopedGrants, agentName3, tb.rule, Date.now(), ttl);
65346
- resumeReactionAfterVerdict();
65347
- postPermissionResumeMessage({
65348
- behavior: "allow",
65349
- action: naturalAction(details.tool_name, details.input_preview)
65350
- });
65351
- process.stderr.write(`telegram gateway: scoped-approval granted rule="${tb.rule}" agent=${agentName3} ttl_ms=${ttl} (request_id=${request_id})
65531
+ const pd = pendingPermissions.get(request_id);
65532
+ const resumeAction = pd ? naturalAction(pd.tool_name, pd.input_preview) : "";
65533
+ const scopedTtl = scopedApprovalTtlMs();
65534
+ const timeBox = behavior === "allow" && scopedTtl > 0 && pd ? resolveTimeBox(pd.tool_name, pd.input_preview, resolveScopedAllowChoices(pd.tool_name, pd.input_preview)) : null;
65535
+ const grantAgent = selfAgentName();
65536
+ pendingPermissions.delete(request_id);
65537
+ if (timeBox && grantAgent) {
65538
+ recordScopedGrant(scopedGrants, grantAgent, timeBox.rule, Date.now(), scopedTtl);
65539
+ process.stderr.write(`telegram gateway: scoped-approval granted via Allow rule="${timeBox.rule}" agent=${grantAgent} ttl_ms=${scopedTtl} (request_id=${request_id})
65352
65540
  `);
65353
- const mins = Math.max(1, Math.round(ttl / 60000));
65354
- const sourceMsg = ctx.callbackQuery?.message;
65355
- const baseText2 = sourceMsg && "text" in sourceMsg && sourceMsg.text ? escapeHtmlForTg(sourceMsg.text) : "";
65356
- const editLabel = `\u23F1 <b>Allowed for ${mins} min \u2014 ${escapeHtmlForTg(tb.breadth)}</b> \xB7 re-asks after that, and now for anything else`;
65357
- await finalizeCallback(ctx, {
65358
- ackText: `\u23F1 Allowed for ${mins} min`.slice(0, 200),
65359
- newText: baseText2 ? `${baseText2}
65360
-
65361
- ${editLabel}` : editLabel,
65362
- parseMode: "HTML"
65363
- });
65364
- return;
65365
65541
  }
65366
- const resumeAction = (() => {
65367
- const d = pendingPermissions.get(request_id);
65368
- return d ? naturalAction(d.tool_name, d.input_preview) : "";
65369
- })();
65370
- pendingPermissions.delete(request_id);
65371
- const label = behavior === "allow" ? "\u2705 Allowed" : "\u274C Denied";
65542
+ const scopedMins = Math.max(1, Math.round(scopedTtl / 60000));
65543
+ const windowGranted = timeBox != null && grantAgent !== "";
65544
+ const ackText = behavior === "deny" ? "\u274C Denied" : windowGranted ? `\u2705 Allowed (${scopedMins} min)` : "\u2705 Allowed once";
65545
+ const htmlLabel = behavior === "deny" ? "\u274C <b>Denied</b>" : windowGranted ? `\u2705 <b>Allowed \u2014 won't ask again about ${escapeHtmlForTg(timeBox.breadth)} for ${scopedMins} min</b>` : "\u2705 <b>Allowed once</b>";
65372
65546
  const msg = ctx.callbackQuery?.message;
65373
65547
  const baseText = msg && "text" in msg && msg.text ? escapeHtmlForTg(msg.text) : "";
65374
65548
  await finalizeCallback(ctx, {
65375
- ackText: label,
65549
+ ackText: ackText.slice(0, 200),
65376
65550
  newText: baseText ? `${baseText}
65377
65551
 
65378
- ${label}` : label,
65552
+ ${htmlLabel}` : htmlLabel,
65379
65553
  parseMode: "HTML",
65380
65554
  synthInbound: () => {
65381
65555
  dispatchPermissionVerdict({