switchroom 0.15.19 → 0.15.21

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.19",
3
+ "version": "0.15.21",
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
@@ -43177,6 +43177,7 @@ function switchroomHelpText(agentName3) {
43177
43177
  `<code>/update</code> \u2014 dry-run plan; <code>/update apply</code> \u2014 actually pull images, reconcile, restart`,
43178
43178
  `<code>/restart [name|all]</code> \u2014 bounce agent (drains in-flight turn by default)`,
43179
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`,
43180
43181
  ``,
43181
43182
  `<b>Auth &amp; config</b>`,
43182
43183
  `<code>/auth</code> \u2014 auth status or actions`,
@@ -45453,43 +45454,32 @@ async function handleEffortCommand(parsed, deps) {
45453
45454
  const verbHtml = `<code>/effort ${deps.escapeHtml(parsed.level)}</code>`;
45454
45455
  let result;
45455
45456
  try {
45456
- result = await deps.inject(deps.getAgentName(), `/effort ${parsed.level}`);
45457
+ result = await deps.applyEffort(deps.getAgentName(), parsed.level);
45457
45458
  } catch (err) {
45458
45459
  const msg = err instanceof Error ? err.message : String(err);
45459
- return { text: `\u274c ${verbHtml} \u2014 inject failed: ${deps.escapeHtml(msg)}`, html: true };
45460
+ return { text: `\u274c ${verbHtml} \u2014 failed: ${deps.escapeHtml(msg)}`, html: true };
45460
45461
  }
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
- };
45462
+ return { text: applyResultText(parsed.level, result, deps), html: true };
45463
+ }
45464
+ function applyResultText(level, result, deps) {
45465
+ const verbHtml = `<code>/effort ${deps.escapeHtml(level)}</code>`;
45466
+ if (result.ok) {
45467
+ const lines = [`\u2705 ${verbHtml} \u2014 ${deps.escapeHtml(result.output)}`];
45468
+ if (result.confirmed) {
45469
+ lines.push("<i>Switched mid-conversation \u2014 your next turn re-reads the cached history (slower, one time).</i>");
45470
+ }
45471
+ lines.push(PERSIST_NOTE2);
45472
+ return lines.join(`
45473
+ `);
45472
45474
  }
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
- };
45475
+ if (result.reason === "session_missing") {
45476
+ return "\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.";
45482
45477
  }
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
- };
45478
+ if (result.reason === "confirm_failed") {
45479
+ const wedged = result.wedged ? " The confirmation prompt may still be open on the pane \u2014 check it." : " The change was cancelled and the pane left as it was.";
45480
+ return `\u274c ${verbHtml} \u2014 couldn't confirm the switch.${wedged}`;
45488
45481
  }
45489
- return {
45490
- text: `\u274c ${verbHtml} \u2014 ${deps.escapeHtml(result.errorMessage ?? "inject failed")}`,
45491
- html: true
45492
- };
45482
+ return `\u274c ${verbHtml} \u2014 sent, but couldn't confirm it applied. The agent may be mid-turn; check <code>/inject /status</code>.`;
45493
45483
  }
45494
45484
  var EFFORT_CALLBACK_PREFIX = "eff:";
45495
45485
  var EFFORT_CALLBACK_SELECT = "eff:s:";
@@ -45530,18 +45520,20 @@ async function handleEffortMenuCallback(data, deps) {
45530
45520
  let banner;
45531
45521
  let selected;
45532
45522
  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`;
45523
+ const result = await deps.applyEffort(deps.getAgentName(), level);
45524
+ if (result.ok) {
45525
+ banner = result.confirmed ? `\u2705 Effort \u2192 <code>${deps.escapeHtml(level)}</code> (mid-conversation: next turn re-reads history)` : `\u2705 Effort \u2192 <code>${deps.escapeHtml(level)}</code> for this session`;
45536
45526
  selected = level;
45537
- } else if (result.errorCode === "session_missing") {
45527
+ } else if (result.reason === "session_missing") {
45538
45528
  banner = "\u274c tmux session not found \u2014 is the agent running under the supervisor?";
45529
+ } else if (result.reason === "confirm_failed") {
45530
+ banner = result.wedged ? "\u26a0\ufe0f Couldn\u2019t confirm the switch \u2014 the prompt may still be open on the pane." : "\u274c Couldn\u2019t confirm the switch \u2014 cancelled, effort unchanged.";
45539
45531
  } else {
45540
- banner = `\u274c couldn't set effort: ${deps.escapeHtml(result.errorMessage ?? "inject failed")}`;
45532
+ banner = "\u274c Sent, but couldn\u2019t confirm it applied (agent may be mid-turn).";
45541
45533
  }
45542
45534
  } catch (err) {
45543
45535
  const msg = err instanceof Error ? err.message : String(err);
45544
- banner = `\u274c inject failed: ${deps.escapeHtml(msg)}`;
45536
+ banner = `\u274c failed: ${deps.escapeHtml(msg)}`;
45545
45537
  }
45546
45538
  const menu = buildEffortMenu(deps, selected);
45547
45539
  return {
@@ -45551,6 +45543,80 @@ ${menu.text}` },
45551
45543
  };
45552
45544
  }
45553
45545
 
45546
+ // ../src/agents/effort-picker.ts
45547
+ var CONFIRM_RE = /Change effort level\?/i;
45548
+ function appliedRe(level) {
45549
+ return new RegExp(`${level}\\s*\\u00b7\\s*/effort`);
45550
+ }
45551
+ function applyLine(pane, level) {
45552
+ const re = new RegExp(`Set effort level to ${level}\\b.*`, "i");
45553
+ for (const line of pane.split(`
45554
+ `)) {
45555
+ const m = line.match(re);
45556
+ if (m)
45557
+ return m[0].trim();
45558
+ }
45559
+ return `Set effort level to ${level}`;
45560
+ }
45561
+ var realSleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
45562
+ async function applyEffort(agentName3, level, opts = {}) {
45563
+ const runner = opts._runner ?? makeTmuxRunner2(opts.tmuxBin ?? "tmux");
45564
+ const socket = opts.socketName ?? `switchroom-${agentName3}`;
45565
+ const session = opts.sessionName ?? agentName3;
45566
+ const stepMs = opts.stepMs ?? 600;
45567
+ const timeoutMs = opts.timeoutMs ?? 1e4;
45568
+ const sleep2 = opts._sleep ?? realSleep2;
45569
+ const log = opts._log ?? ((line) => process.stderr.write(`${line}
45570
+ `));
45571
+ if (!runner.hasSession(socket, session)) {
45572
+ return { ok: false, reason: "session_missing" };
45573
+ }
45574
+ return withPaneLock(`${socket}:${session}`, async () => {
45575
+ const startedAt = Date.now();
45576
+ const expired2 = () => Date.now() - startedAt >= timeoutMs;
45577
+ try {
45578
+ runner.send(socket, session, ["send-keys", "-l", `/effort ${level}`]);
45579
+ runner.send(socket, session, ["send-keys", "Enter"]);
45580
+ let confirmed = false;
45581
+ let confirmKeys = 0;
45582
+ while (!expired2()) {
45583
+ await sleep2(stepMs);
45584
+ const pane = runner.capture(socket, session) ?? "";
45585
+ if (CONFIRM_RE.test(pane)) {
45586
+ if (confirmKeys >= 2) {
45587
+ runner.send(socket, session, ["send-keys", "Escape"]);
45588
+ await sleep2(stepMs);
45589
+ const after = runner.capture(socket, session) ?? "";
45590
+ log(`effort-picker: confirm modal would not dismiss for ${agentName3} ` + `(socket=${socket}) \u2014 cancelled`);
45591
+ return { ok: false, reason: "confirm_failed", wedged: CONFIRM_RE.test(after) };
45592
+ }
45593
+ runner.send(socket, session, ["send-keys", "Enter"]);
45594
+ confirmed = true;
45595
+ confirmKeys += 1;
45596
+ continue;
45597
+ }
45598
+ if (appliedRe(level).test(pane)) {
45599
+ return { ok: true, level, confirmed, output: applyLine(pane, level) };
45600
+ }
45601
+ }
45602
+ const final = runner.capture(socket, session) ?? "";
45603
+ if (CONFIRM_RE.test(final)) {
45604
+ runner.send(socket, session, ["send-keys", "Escape"]);
45605
+ log(`effort-picker: timeout with modal open for ${agentName3} \u2014 cancelled`);
45606
+ return { ok: false, reason: "confirm_failed", wedged: true };
45607
+ }
45608
+ return { ok: false, reason: "apply_unverified" };
45609
+ } finally {
45610
+ try {
45611
+ const pane = runner.capture(socket, session) ?? "";
45612
+ if (CONFIRM_RE.test(pane)) {
45613
+ runner.send(socket, session, ["send-keys", "Escape"]);
45614
+ }
45615
+ } catch {}
45616
+ }
45617
+ });
45618
+ }
45619
+
45554
45620
  // ../src/config/loader.ts
45555
45621
  init_dist();
45556
45622
  init_zod();
@@ -53646,6 +53712,15 @@ function readBashCommand(inputPreview) {
53646
53712
  }
53647
53713
  }
53648
53714
 
53715
+ // gateway/grant-restart.ts
53716
+ function grantRestartDecision(opts) {
53717
+ if ((opts.killSwitch ?? "") === "0")
53718
+ return "disabled";
53719
+ if (!opts.selfAgent || opts.selfAgent !== opts.agentName)
53720
+ return "disabled";
53721
+ return opts.turnInFlight ? "deferred" : "now";
53722
+ }
53723
+
53649
53724
  // permission-diff.ts
53650
53725
  var TARGET_HEADER_A = "--- a/switchroom.yaml";
53651
53726
  var TARGET_HEADER_B = "+++ b/switchroom.yaml";
@@ -54317,10 +54392,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
54317
54392
  }
54318
54393
 
54319
54394
  // ../src/build-info.ts
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;
54395
+ var VERSION = "0.15.21";
54396
+ var COMMIT_SHA = "36706e85";
54397
+ var COMMIT_DATE = "2026-06-14T01:34:05Z";
54398
+ var LATEST_PR = 2345;
54324
54399
  var COMMITS_AHEAD_OF_TAG = 0;
54325
54400
 
54326
54401
  // gateway/boot-version.ts
@@ -55819,6 +55894,23 @@ function formatFeedElapsed(ms) {
55819
55894
  function turnInFlightForGate() {
55820
55895
  return isDeliveryCutoverEnabled() ? isMachineInTurn() : claudeBusyKeys.size > 0;
55821
55896
  }
55897
+ function deliverResumeSyntheticOrBuffer(agent, inbound) {
55898
+ const decision = decideInboundDelivery({
55899
+ turnInFlight: turnInFlightForGate(),
55900
+ isSteering: false,
55901
+ isInterrupt: false
55902
+ });
55903
+ if (decision === "buffer-until-idle") {
55904
+ pendingInboundBuffer.push(agent, inbound);
55905
+ return false;
55906
+ }
55907
+ const delivered = ipcServer.sendToAgent(agent, inbound);
55908
+ if (delivered)
55909
+ markClaudeBusyForInbound(inbound);
55910
+ else
55911
+ pendingInboundBuffer.push(agent, inbound);
55912
+ return delivered;
55913
+ }
55822
55914
  var pendingRestarts = new Map;
55823
55915
  var lastSessionActiveFile = null;
55824
55916
  var compactState = initialCompactState();
@@ -59301,11 +59393,7 @@ async function captureProvidedSecret(ctx, chat_id, msgId, value) {
59301
59393
  stage_id: armed.stageId
59302
59394
  }
59303
59395
  };
59304
- const fdelivered = ipcServer.sendToAgent(armed.agent, failMsg);
59305
- if (fdelivered)
59306
- markClaudeBusyForInbound(failMsg);
59307
- else
59308
- pendingInboundBuffer.push(armed.agent, failMsg);
59396
+ deliverResumeSyntheticOrBuffer(armed.agent, failMsg);
59309
59397
  return true;
59310
59398
  }
59311
59399
  await switchroomReply(ctx, `\u2705 saved as <code>vault:${escapeHtmlForTg(armed.key)}</code> (masked: <code>${escapeHtmlForTg(maskToken2(value))}</code>). The agent can now reference it.`, { html: true });
@@ -59327,11 +59415,7 @@ async function captureProvidedSecret(ctx, chat_id, msgId, value) {
59327
59415
  stage_id: armed.stageId
59328
59416
  }
59329
59417
  };
59330
- const delivered = ipcServer.sendToAgent(armed.agent, synthetic);
59331
- if (delivered)
59332
- markClaudeBusyForInbound(synthetic);
59333
- else
59334
- pendingInboundBuffer.push(armed.agent, synthetic);
59418
+ const delivered = deliverResumeSyntheticOrBuffer(armed.agent, synthetic);
59335
59419
  process.stderr.write(`telegram gateway: secret_provided injection agent=${armed.agent} key=${armed.key} stage=${armed.stageId} delivered=${delivered}
59336
59420
  `);
59337
59421
  return true;
@@ -59393,11 +59477,7 @@ async function handleSecretRequestCallback(ctx, data) {
59393
59477
  stage_id: stageId
59394
59478
  }
59395
59479
  };
59396
- const delivered = ipcServer.sendToAgent(pending2.agent, synthetic);
59397
- if (delivered)
59398
- markClaudeBusyForInbound(synthetic);
59399
- else
59400
- pendingInboundBuffer.push(pending2.agent, synthetic);
59480
+ deliverResumeSyntheticOrBuffer(pending2.agent, synthetic);
59401
59481
  return;
59402
59482
  }
59403
59483
  await ctx.answerCallbackQuery().catch(() => {});
@@ -61772,6 +61852,26 @@ async function sweepBeforeSelfRestart() {
61772
61852
  `);
61773
61853
  }
61774
61854
  }
61855
+ function scheduleGrantRestart(agentName3, chatId, threadId, reason) {
61856
+ const decision = grantRestartDecision({
61857
+ killSwitch: process.env.SWITCHROOM_AUTORESTART_ON_GRANT,
61858
+ selfAgent: process.env.SWITCHROOM_AGENT_NAME,
61859
+ agentName: agentName3,
61860
+ turnInFlight: turnInFlightForGate()
61861
+ });
61862
+ if (decision === "disabled")
61863
+ return decision;
61864
+ if (chatId != null) {
61865
+ writeRestartMarker({ chat_id: String(chatId), thread_id: threadId ?? null, ack_message_id: null, ts: Date.now() });
61866
+ }
61867
+ stampUserRestartReason(reason);
61868
+ if (decision === "deferred") {
61869
+ pendingRestarts.set(agentName3, Date.now());
61870
+ } else {
61871
+ sweepBeforeSelfRestart().finally(() => triggerSelfRestart(agentName3, reason, 1500));
61872
+ }
61873
+ return decision;
61874
+ }
61775
61875
  function formatAuthOutputForTelegram(output) {
61776
61876
  const trimmed = stripAnsi2(output).trim();
61777
61877
  const url = trimmed.match(/https:\/\/\S+/)?.[0] ?? null;
@@ -62304,14 +62404,13 @@ bot.command("model", async (ctx) => {
62304
62404
  });
62305
62405
  function buildEffortDeps() {
62306
62406
  return {
62307
- inject: injectSlashCommand,
62407
+ applyEffort: (agent, level) => applyEffort(agent, level),
62308
62408
  getAgentName: getMyAgentName,
62309
62409
  getConfiguredEffort: () => {
62310
62410
  const data = switchroomExecJson(["agent", "list"]);
62311
62411
  return data?.agents?.find((a) => a.name === getMyAgentName())?.thinking_effort ?? null;
62312
62412
  },
62313
- escapeHtml: escapeHtmlForTg,
62314
- preBlock
62413
+ escapeHtml: escapeHtmlForTg
62315
62414
  };
62316
62415
  }
62317
62416
  function effortMenuReplyMarkup(reply) {
@@ -63578,14 +63677,9 @@ async function performVaultAccessApproval(ctx, pending2, stageId, senderId, atte
63578
63677
  stageId,
63579
63678
  operatorId: senderId
63580
63679
  });
63581
- const delivered = ipcServer.sendToAgent(pending2.agent, synthetic);
63582
- if (delivered)
63583
- markClaudeBusyForInbound(synthetic);
63680
+ const delivered = deliverResumeSyntheticOrBuffer(pending2.agent, synthetic);
63584
63681
  process.stderr.write(`telegram gateway: vault_grant_approved injection agent=${pending2.agent} key=${pending2.key} stage=${stageId} delivered=${delivered}
63585
63682
  `);
63586
- if (!delivered) {
63587
- pendingInboundBuffer.push(pending2.agent, synthetic);
63588
- }
63589
63683
  }
63590
63684
  async function handleVaultRequestAccessCallback(ctx, data) {
63591
63685
  const senderId = String(ctx.from?.id ?? "");
@@ -63627,14 +63721,9 @@ async function handleVaultRequestAccessCallback(ctx, data) {
63627
63721
  stageId,
63628
63722
  operatorId: senderId
63629
63723
  });
63630
- const denyDelivered = ipcServer.sendToAgent(pending2.agent, denyInbound);
63631
- if (denyDelivered)
63632
- markClaudeBusyForInbound(denyInbound);
63724
+ const denyDelivered = deliverResumeSyntheticOrBuffer(pending2.agent, denyInbound);
63633
63725
  process.stderr.write(`telegram gateway: vault_grant_denied injection agent=${pending2.agent} key=${pending2.key} stage=${stageId} delivered=${denyDelivered}
63634
63726
  `);
63635
- if (!denyDelivered) {
63636
- pendingInboundBuffer.push(pending2.agent, denyInbound);
63637
- }
63638
63727
  return;
63639
63728
  }
63640
63729
  if (action === "approve") {
@@ -63726,13 +63815,9 @@ async function handleVaultRequestSaveCallback(ctx, data) {
63726
63815
  stageId,
63727
63816
  operatorId: senderId
63728
63817
  });
63729
- const dDelivered = ipcServer.sendToAgent(pending2.agent, discardInbound);
63730
- if (dDelivered)
63731
- markClaudeBusyForInbound(discardInbound);
63818
+ const dDelivered = deliverResumeSyntheticOrBuffer(pending2.agent, discardInbound);
63732
63819
  process.stderr.write(`telegram gateway: vault_save_discarded injection agent=${pending2.agent} key=${pending2.key} stage=${stageId} delivered=${dDelivered}
63733
63820
  `);
63734
- if (!dDelivered)
63735
- pendingInboundBuffer.push(pending2.agent, discardInbound);
63736
63821
  return;
63737
63822
  }
63738
63823
  if (action === "rename") {
@@ -63793,13 +63878,9 @@ async function handleVaultRequestSaveCallback(ctx, data) {
63793
63878
  operatorId: senderId,
63794
63879
  reason: failReason
63795
63880
  });
63796
- const fDelivered = ipcServer.sendToAgent(pending2.agent, failInbound);
63797
- if (fDelivered)
63798
- markClaudeBusyForInbound(failInbound);
63881
+ const fDelivered = deliverResumeSyntheticOrBuffer(pending2.agent, failInbound);
63799
63882
  process.stderr.write(`telegram gateway: vault_save_failed injection agent=${pending2.agent} key=${pending2.key} stage=${stageId} delivered=${fDelivered}
63800
63883
  `);
63801
- if (!fDelivered)
63802
- pendingInboundBuffer.push(pending2.agent, failInbound);
63803
63884
  return;
63804
63885
  }
63805
63886
  pendingVaultRequestSaves.delete(stageId);
@@ -63817,13 +63898,9 @@ async function handleVaultRequestSaveCallback(ctx, data) {
63817
63898
  stageId,
63818
63899
  operatorId: senderId
63819
63900
  });
63820
- const okDelivered = ipcServer.sendToAgent(pending2.agent, okInbound);
63821
- if (okDelivered)
63822
- markClaudeBusyForInbound(okInbound);
63901
+ const okDelivered = deliverResumeSyntheticOrBuffer(pending2.agent, okInbound);
63823
63902
  process.stderr.write(`telegram gateway: vault_save_completed injection agent=${pending2.agent} key=${pending2.key} stage=${stageId} delivered=${okDelivered}
63824
63903
  `);
63825
- if (!okDelivered)
63826
- pendingInboundBuffer.push(pending2.agent, okInbound);
63827
63904
  return;
63828
63905
  }
63829
63906
  await ctx.answerCallbackQuery({ text: "Unknown action" }).catch(() => {});
@@ -65051,6 +65128,56 @@ bot.command("version", async (ctx) => {
65051
65128
  ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: true });
65052
65129
  }
65053
65130
  });
65131
+ bot.command("whoami", async (ctx) => {
65132
+ if (!isAuthorizedSender(ctx))
65133
+ return;
65134
+ try {
65135
+ let raw;
65136
+ try {
65137
+ raw = switchroomExecCombined(["config", "whoami"], 1e4);
65138
+ } catch (err) {
65139
+ raw = err.stdout ?? err.message ?? "whoami failed";
65140
+ }
65141
+ const trimmed = stripAnsi2(raw).trim();
65142
+ let card;
65143
+ try {
65144
+ card = formatWhoamiCard(JSON.parse(trimmed.split(`
65145
+ `).pop() ?? trimmed));
65146
+ } catch {
65147
+ card = preBlock(formatSwitchroomOutput(trimmed || "whoami: no output"));
65148
+ }
65149
+ await switchroomReply(ctx, card, { html: true });
65150
+ } catch (err) {
65151
+ await switchroomReply(ctx, `<b>whoami failed:</b>
65152
+ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: true });
65153
+ }
65154
+ });
65155
+ function formatWhoamiCard(v) {
65156
+ const esc = escapeHtmlForTg;
65157
+ const yn = (b) => b ? "\u2713" : "\u2717";
65158
+ const lines = [];
65159
+ lines.push(`\uD83D\uDC64 <b>${esc(v.name ?? "?")}</b> \xB7 ${esc(v.tier ?? "standard")}`);
65160
+ if (v.persona)
65161
+ lines.push(esc(v.persona));
65162
+ if (v.model)
65163
+ lines.push(`Model: ${esc(v.model)}`);
65164
+ const allow = v.tools?.allow ?? [];
65165
+ lines.push(`Tools: ${allow.length ? esc(allow.slice(0, 8).join(", ")) + (allow.length > 8 ? ` \u2026(+${allow.length - 8})` : "") : "\u2014"}`);
65166
+ if ((v.tools?.deny ?? []).length)
65167
+ lines.push(`Denied: ${esc(v.tools.deny.join(", "))}`);
65168
+ if ((v.mcpServers ?? []).length)
65169
+ lines.push(`MCP: ${esc(v.mcpServers.join(", "))}`);
65170
+ if ((v.skills ?? []).length)
65171
+ lines.push(`Skills: ${esc(v.skills.join(", "))}`);
65172
+ if ((v.vault ?? []).length) {
65173
+ lines.push(`Vault keys (names only): ${v.vault.map((k) => `${esc(k.key)} ${yn(k.readable)}`).join(", ")}`);
65174
+ }
65175
+ const p = v.powers ?? {};
65176
+ 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)}`);
65177
+ lines.push(`Schedule: ${v.scheduleCount ?? 0} cron \xB7 Memory: ${esc(v.memoryBackend ?? "none")}`);
65178
+ return lines.join(`
65179
+ `);
65180
+ }
65054
65181
  bot.command("commands", async (ctx) => {
65055
65182
  if (!isAuthorizedSender(ctx))
65056
65183
  return;
@@ -65515,10 +65642,12 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
65515
65642
  }
65516
65643
  const ok = durable;
65517
65644
  const legacyNote = legacy && durable;
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.`;
65645
+ const restartScheduled = ok && !legacy && scheduleGrantRestart(agentName3, ctx.chat?.id, ctx.callbackQuery?.message?.message_thread_id, `always-allow: ${grantPhrase}`) !== "disabled";
65646
+ const liveSuffix = restartScheduled ? " \u2014 applying now (restarting to take effect)" : "";
65647
+ 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.`;
65519
65648
  const sourceMsg = ctx.callbackQuery?.message;
65520
65649
  const baseText2 = sourceMsg && "text" in sourceMsg && sourceMsg.text ? escapeHtmlForTg(sourceMsg.text) : "";
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.`;
65650
+ 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.`;
65522
65651
  await finalizeCallback(ctx, {
65523
65652
  ackText: ackText2.slice(0, 200),
65524
65653
  newText: baseText2 ? `${baseText2}