switchroom 0.14.2 → 0.14.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.
@@ -30840,8 +30840,11 @@ function defaultStatBroker(p) {
30840
30840
  return { kind: "ok-with-stat", ino: inoStr, size };
30841
30841
  }
30842
30842
  function spawnDockerStat(p) {
30843
+ return spawnDockerStatForContainer("switchroom-vault-broker", p);
30844
+ }
30845
+ function spawnDockerStatForContainer(containerName2, p) {
30843
30846
  try {
30844
- const stdout = execFileSync16("docker", ["exec", "switchroom-vault-broker", "stat", "-c", "%i %s", p], { stdio: ["ignore", "pipe", "pipe"], timeout: 3000, encoding: "utf8" });
30847
+ const stdout = execFileSync16("docker", ["exec", containerName2, "stat", "-c", "%i %s", p], { stdio: ["ignore", "pipe", "pipe"], timeout: 3000, encoding: "utf8" });
30845
30848
  return { status: 0, stdout, stderr: "", error: null };
30846
30849
  } catch (err) {
30847
30850
  const e = err;
@@ -30933,9 +30936,80 @@ function runVaultBrokerDurabilityChecks(_config, opts) {
30933
30936
  probeMachineIdMount(),
30934
30937
  formatBindMountResult("vault-broker: vault.enc bind mount", join52(home2, ".switchroom", "vault", "vault.enc"), "/state/vault/vault.enc", probe2(join52(home2, ".switchroom", "vault", "vault.enc"), "/state/vault/vault.enc")),
30935
30938
  formatBindMountResult("vault-broker: vault-grants.db bind mount (#1737)", join52(home2, ".switchroom", "vault-grants.db"), "/root/.switchroom/vault-grants.db", probe2(join52(home2, ".switchroom", "vault-grants.db"), "/root/.switchroom/vault-grants.db")),
30936
- formatBindMountResult("vault-broker: vault-audit.log bind mount (#1025)", join52(home2, ".switchroom", "vault-audit.log"), "/root/.switchroom/vault-audit.log", probe2(join52(home2, ".switchroom", "vault-audit.log"), "/root/.switchroom/vault-audit.log"))
30939
+ formatBindMountResult("vault-broker: vault-audit.log bind mount (#1025)", join52(home2, ".switchroom", "vault-audit.log"), "/root/.switchroom/vault-audit.log", probe2(join52(home2, ".switchroom", "vault-audit.log"), "/root/.switchroom/vault-audit.log")),
30940
+ probeKernelDbDurability(home2, {
30941
+ statBroker: opts?.kernelStatBroker
30942
+ })
30937
30943
  ];
30938
30944
  }
30945
+ function probeKernelDbDurability(home2, opts) {
30946
+ const hostDir = join52(home2, ".switchroom", "approvals");
30947
+ const containerDir = "/state/approvals";
30948
+ const name = "approval-kernel: approvals bind mount (allow_always durability)";
30949
+ const kernelStat = opts?.statBroker ?? defaultKernelStatBroker;
30950
+ const result = probeBindMountInode(hostDir, containerDir, {
30951
+ statBroker: kernelStat,
30952
+ statHost: opts?.statHost
30953
+ });
30954
+ if (result.kind === "ok") {
30955
+ return {
30956
+ name,
30957
+ status: "ok",
30958
+ detail: `${hostDir} == ${containerDir} (same inode) \u2014 allow_always decisions persist across kernel recreate`
30959
+ };
30960
+ }
30961
+ if (result.kind === "host-missing") {
30962
+ return {
30963
+ name,
30964
+ status: "warn",
30965
+ detail: `host directory ${hostDir} missing \u2014 \`switchroom apply\` pre-creates it on greenfield`,
30966
+ fix: "Run `switchroom apply` to pre-create the host approvals directory"
30967
+ };
30968
+ }
30969
+ if (result.kind === "broker-unreachable") {
30970
+ return {
30971
+ name,
30972
+ status: "skip",
30973
+ detail: "approval-kernel container unreachable \u2014 bind mount unverified"
30974
+ };
30975
+ }
30976
+ if (result.kind === "broker-stat-failed") {
30977
+ return {
30978
+ name,
30979
+ status: "warn",
30980
+ detail: `approval-kernel stat failed: ${result.msg}`
30981
+ };
30982
+ }
30983
+ return {
30984
+ name,
30985
+ status: "fail",
30986
+ detail: `inode mismatch \u2014 approval-kernel \`/state/approvals\` is NOT backed by the host bind mount. ` + `host inode=${result.hostInode} size=${result.hostSize}; ` + `kernel inode=${result.brokerInode} size=${result.brokerSize}. ` + `The kernel is writing kernel.db to an ephemeral container-local directory; ` + `all allow_always decisions are lost on every container recreate (e.g. after \`switchroom update\`).`,
30987
+ fix: "Run `switchroom apply` to regenerate compose with the " + "`~/.switchroom/approvals:/state/approvals` bind mount, then " + "`docker compose -p switchroom up -d approval-kernel` to recreate the kernel container."
30988
+ };
30989
+ }
30990
+ function defaultKernelStatBroker(p) {
30991
+ const r = spawnDockerStatForContainer("switchroom-approval-kernel", p);
30992
+ if (r.error || r.status === null)
30993
+ return { kind: "broker-unreachable" };
30994
+ if (r.status !== 0) {
30995
+ if (r.status >= 125)
30996
+ return { kind: "broker-unreachable" };
30997
+ return {
30998
+ kind: "broker-stat-failed",
30999
+ msg: r.stderr?.trim() || `exit ${r.status}`
31000
+ };
31001
+ }
31002
+ const out = r.stdout.trim();
31003
+ const [inoStr, sizeStr] = out.split(/\s+/);
31004
+ const size = Number(sizeStr);
31005
+ if (!inoStr || !Number.isFinite(size)) {
31006
+ return {
31007
+ kind: "broker-stat-failed",
31008
+ msg: `unparseable stat output: ${out}`
31009
+ };
31010
+ }
31011
+ return { kind: "ok-with-stat", ino: inoStr, size };
31012
+ }
30939
31013
  function probeAutoUnlockBlob(home2) {
30940
31014
  const blobPath = join52(home2, ".switchroom", "vault-auto-unlock");
30941
31015
  if (!existsSync52(blobPath)) {
@@ -49278,8 +49352,8 @@ var {
49278
49352
  } = import__.default;
49279
49353
 
49280
49354
  // src/build-info.ts
49281
- var VERSION = "0.14.2";
49282
- var COMMIT_SHA = "3c7d0238";
49355
+ var VERSION = "0.14.4";
49356
+ var COMMIT_SHA = "a9f2d29a";
49283
49357
 
49284
49358
  // src/cli/agent.ts
49285
49359
  init_source();
@@ -51763,7 +51837,9 @@ function buildSettingsHooksBlock(p) {
51763
51837
  ` + `So:
51764
51838
  ` + " - Trivial / social message \u2192 reply once, briefly, in your voice. " + `The reply IS the response.
51765
51839
  ` + ` - Question with a short answer \u2192 just reply with the answer.
51766
- ` + " - Complex tool-driven work \u2192 go straight to the tools (the " + "compose-area preview is the ambient liveness signal), then reply " + 'once with the answer or a genuine mid-work pivot ("halfway ' + 'through \u2014 found an unexpected issue, want me to continue?"). Not ' + '"still working".</turn-pacing>';
51840
+ ` + " - Complex tool-driven work \u2192 go straight to the tools (the " + "compose-area preview is the ambient liveness signal), then reply " + 'once with the answer or a genuine mid-work pivot ("halfway ' + 'through \u2014 found an unexpected issue, want me to continue?"). Not ' + `"still working".
51841
+
51842
+ ` + 'Do NOT send a trailing confirmation after your answer \u2014 no "Done.", ' + '"Sent.", "Hope that helps." as a separate message once you have ' + "already replied. Your answer is the last thing the user should " + `see; a follow-up "Done." is dead-air clutter (and the user's ` + "device already pinged on the answer). Stop after the answer.</turn-pacing>";
51767
51843
  const switchroomUserPromptSubmit = [
51768
51844
  ...useHotReloadStable ? [
51769
51845
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.14.2",
3
+ "version": "0.14.4",
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": {
@@ -23041,6 +23041,7 @@ import { join as join2 } from "node:path";
23041
23041
  function createToolLabelSidecar(opts) {
23042
23042
  const path = join2(opts.stateDir, `tool-labels-${opts.sessionId}.jsonl`);
23043
23043
  const labels = new Map;
23044
+ const seen = [];
23044
23045
  const subscribers = new Set;
23045
23046
  let offset = 0;
23046
23047
  let stopped = false;
@@ -23063,14 +23064,15 @@ function createToolLabelSidecar(opts) {
23063
23064
  } catch {
23064
23065
  continue;
23065
23066
  }
23066
- if (!row || typeof row.tool_use_id !== "string" || typeof row.label !== "string")
23067
+ if (!row || typeof row.tool_use_id !== "string" || typeof row.label !== "string" || typeof row.tool_name !== "string")
23067
23068
  continue;
23068
23069
  if (labels.has(row.tool_use_id))
23069
23070
  continue;
23070
23071
  labels.set(row.tool_use_id, row.label);
23072
+ seen.push({ toolUseId: row.tool_use_id, label: row.label, toolName: row.tool_name });
23071
23073
  for (const cb of subscribers) {
23072
23074
  try {
23073
- cb(row.tool_use_id, row.label);
23075
+ cb(row.tool_use_id, row.label, row.tool_name);
23074
23076
  } catch {}
23075
23077
  }
23076
23078
  }
@@ -23109,6 +23111,11 @@ function createToolLabelSidecar(opts) {
23109
23111
  return labels.get(toolUseId);
23110
23112
  },
23111
23113
  onLabel(cb) {
23114
+ for (const r of seen) {
23115
+ try {
23116
+ cb(r.toolUseId, r.label, r.toolName);
23117
+ } catch {}
23118
+ }
23112
23119
  subscribers.add(cb);
23113
23120
  return () => subscribers.delete(cb);
23114
23121
  },
@@ -23441,6 +23448,9 @@ function startSessionTail(config2) {
23441
23448
  try {
23442
23449
  const s = createToolLabelSidecar({ stateDir: stateDirForSidecar, sessionId });
23443
23450
  sidecars.set(sessionId, s);
23451
+ s.onLabel((toolUseId, label, toolName) => {
23452
+ rawOnEvent({ kind: "tool_label", toolUseId, label, toolName });
23453
+ });
23444
23454
  return s;
23445
23455
  } catch (err) {
23446
23456
  log?.(`session-tail: sidecar create failed: ${err.message}`);
@@ -23554,6 +23564,9 @@ function startSessionTail(config2) {
23554
23564
  }
23555
23565
  log?.(`session-tail: attached to ${file} (cursor=${cursor})`);
23556
23566
  }
23567
+ const attachSid = sessionIdForFile(file);
23568
+ if (attachSid)
23569
+ ensureSidecar(attachSid);
23557
23570
  try {
23558
23571
  watcher = watch(file, () => readNew());
23559
23572
  } catch (err) {
@@ -31866,121 +31866,7 @@ function registerAndRender(state, toolName) {
31866
31866
  return null;
31867
31867
  return formatSummary(state);
31868
31868
  }
31869
- function baseName(p) {
31870
- if (typeof p !== "string" || p.length === 0)
31871
- return null;
31872
- const parts = p.split("/").filter(Boolean);
31873
- return parts.length > 0 ? parts[parts.length - 1] : p;
31874
- }
31875
- function hostName(u) {
31876
- if (typeof u !== "string" || u.length === 0)
31877
- return null;
31878
- try {
31879
- return new URL(u).hostname.replace(/^www\./, "");
31880
- } catch {
31881
- return u.replace(/^https?:\/\//, "").split("/")[0] || null;
31882
- }
31883
- }
31884
- function clip(s, n) {
31885
- if (typeof s !== "string")
31886
- return null;
31887
- const t = s.trim();
31888
- if (t.length === 0)
31889
- return null;
31890
- return t.length > n ? t.slice(0, n - 1) + "\u2026" : t;
31891
- }
31892
- function describeToolUse(toolName, input) {
31893
- if (!toolName)
31894
- return null;
31895
- const inp = input ?? {};
31896
- const mcpMatch = /^mcp__(.+?)__(.+)$/.exec(toolName);
31897
- if (mcpMatch) {
31898
- const server = mcpMatch[1].toLowerCase();
31899
- const tool = mcpMatch[2].toLowerCase();
31900
- if (server === "switchroom-telegram")
31901
- return null;
31902
- if (server === "hindsight") {
31903
- if (tool === "recall" || tool === "reflect")
31904
- return "Searching memory";
31905
- if (tool === "retain" || tool === "update_memory" || tool === "sync_retain")
31906
- return "Saving to memory";
31907
- return "Working with memory";
31908
- }
31909
- if (server === "google-workspace" || server === "claude_ai_google_calendar") {
31910
- return "Checking your calendar";
31911
- }
31912
- if (server === "claude_ai_gmail")
31913
- return "Checking your email";
31914
- if (server === "claude_ai_google_drive")
31915
- return "Looking through your files";
31916
- if (server === "notion" || server === "claude_ai_notion") {
31917
- return "Checking your notes";
31918
- }
31919
- const desc = clip(inp.description, 60) ?? clip(inp.query, 50) ?? clip(inp.title, 50);
31920
- if (desc)
31921
- return desc;
31922
- return "Using " + tool.replace(/[-_]+/g, " ");
31923
- }
31924
- switch (toolName) {
31925
- case "Bash": {
31926
- return clip(inp.description, 70) ?? "Running a command";
31927
- }
31928
- case "BashOutput":
31929
- case "KillShell":
31930
- return "Managing a background command";
31931
- case "Read": {
31932
- const f = baseName(inp.file_path);
31933
- return f ? `Reading ${f}` : "Reading a file";
31934
- }
31935
- case "Edit":
31936
- case "MultiEdit":
31937
- case "NotebookEdit": {
31938
- const f = baseName(inp.file_path) ?? baseName(inp.notebook_path);
31939
- return f ? `Editing ${f}` : "Editing a file";
31940
- }
31941
- case "Write": {
31942
- const f = baseName(inp.file_path);
31943
- return f ? `Writing ${f}` : "Writing a file";
31944
- }
31945
- case "Grep":
31946
- case "Glob": {
31947
- const p = clip(inp.pattern, 40);
31948
- return p ? `Searching for ${p}` : "Searching files";
31949
- }
31950
- case "WebFetch": {
31951
- const h = hostName(inp.url);
31952
- return h ? `Reading ${h}` : "Reading a web page";
31953
- }
31954
- case "WebSearch": {
31955
- const q = clip(inp.query, 50);
31956
- return q ? `Searching the web for ${q}` : "Searching the web";
31957
- }
31958
- case "Task":
31959
- case "Agent": {
31960
- const d = clip(inp.description, 60);
31961
- return d ? `Delegating: ${d}` : "Delegating to a sub-agent";
31962
- }
31963
- case "TodoWrite":
31964
- case "TaskCreate":
31965
- case "TaskUpdate":
31966
- case "TaskList":
31967
- return "Updating the plan";
31968
- case "ToolSearch":
31969
- return "Finding the right tool";
31970
- default:
31971
- return "Working\u2026";
31972
- }
31973
- }
31974
31869
  var MIRROR_MAX_LINES = 6;
31975
- function appendActivityLine(lines, toolName, input) {
31976
- const line = describeToolUse(toolName, input);
31977
- if (line == null)
31978
- return null;
31979
- if (lines.length === 0 || lines[lines.length - 1] !== line) {
31980
- lines.push(line);
31981
- }
31982
- return renderActivityFeed(lines);
31983
- }
31984
31870
  function renderActivityFeed(lines) {
31985
31871
  if (lines.length === 0)
31986
31872
  return null;
@@ -31991,6 +31877,15 @@ function renderActivityFeed(lines) {
31991
31877
  return hidden > 0 ? `\u00b7 +${hidden} earlier\u2026
31992
31878
  ${body}` : body;
31993
31879
  }
31880
+ function appendActivityLabel(lines, label) {
31881
+ const l = (label ?? "").trim();
31882
+ if (l.length === 0)
31883
+ return null;
31884
+ if (lines.length === 0 || lines[lines.length - 1] !== l) {
31885
+ lines.push(l);
31886
+ }
31887
+ return renderActivityFeed(lines);
31888
+ }
31994
31889
 
31995
31890
  // tool-labels.ts
31996
31891
  var MAX_LABEL_CHARS = 60;
@@ -46282,6 +46177,13 @@ function transition(state3, event) {
46282
46177
  // gateway/inbound-delivery-machine-shadow.ts
46283
46178
  var state3 = initialState();
46284
46179
  var enabled5 = process.env.SWITCHROOM_DELIVERY_MACHINE_SHADOW !== "0";
46180
+ var cutoverEnabled = enabled5 && process.env.SWITCHROOM_DELIVERY_MACHINE_CUTOVER !== "0";
46181
+ function isDeliveryCutoverEnabled() {
46182
+ return cutoverEnabled;
46183
+ }
46184
+ function isMachineInTurn() {
46185
+ return state3.global.kind === "bridge_alive_in_turn";
46186
+ }
46285
46187
  function shadowEmit(event) {
46286
46188
  if (!enabled5)
46287
46189
  return [];
@@ -49843,6 +49745,9 @@ function skillBasenameFromPath2(input) {
49843
49745
  const trimmed = path.replace(/\/SKILL\.md$/i, "").replace(/\/$/, "");
49844
49746
  return basename6(trimmed) || null;
49845
49747
  }
49748
+ function isRulePersisted(resolvedAllow, ruleRule) {
49749
+ return resolvedAllow.includes(ruleRule);
49750
+ }
49846
49751
 
49847
49752
  // credits-watch.ts
49848
49753
  import { readFileSync as readFileSync29, writeFileSync as writeFileSync18, existsSync as existsSync30, mkdirSync as mkdirSync16 } from "fs";
@@ -50163,11 +50068,11 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
50163
50068
  }
50164
50069
 
50165
50070
  // ../src/build-info.ts
50166
- var VERSION = "0.14.2";
50167
- var COMMIT_SHA = "3c7d0238";
50168
- var COMMIT_DATE = "2026-05-28T07:47:55Z";
50169
- var LATEST_PR = 1958;
50170
- var COMMITS_AHEAD_OF_TAG = 0;
50071
+ var VERSION = "0.14.4";
50072
+ var COMMIT_SHA = "a9f2d29a";
50073
+ var COMMIT_DATE = "2026-05-28T20:55:22+10:00";
50074
+ var LATEST_PR = null;
50075
+ var COMMITS_AHEAD_OF_TAG = 5;
50171
50076
 
50172
50077
  // gateway/boot-version.ts
50173
50078
  function formatRelativeAgo(iso) {
@@ -51091,6 +50996,9 @@ function markClaudeBusyForInbound(m) {
51091
50996
  }
51092
50997
  claudeBusyKeys.add(chatKey2(m.chatId, tid));
51093
50998
  }
50999
+ function turnInFlightForGate() {
51000
+ return isDeliveryCutoverEnabled() ? isMachineInTurn() : claudeBusyKeys.size > 0;
51001
+ }
51094
51002
  var pendingRestarts = new Map;
51095
51003
  var lastSessionActiveFile = null;
51096
51004
  var compactState = initialCompactState();
@@ -51156,7 +51064,7 @@ function purgeReactionTracking(key, endingTurn) {
51156
51064
  if (agentDir != null)
51157
51065
  removeActiveReaction(agentDir, msgInfo.chatId, msgInfo.messageId);
51158
51066
  }
51159
- if (claudeBusyKeys.size === 0) {
51067
+ if (!turnInFlightForGate()) {
51160
51068
  const selfAgentForFlush = process.env.SWITCHROOM_AGENT_NAME ?? "";
51161
51069
  if (pendingInboundBuffer.depth(selfAgentForFlush) > 0) {
51162
51070
  const fr = redeliverBufferedInbound(pendingInboundBuffer, selfAgentForFlush, (m) => {
@@ -51186,7 +51094,7 @@ function releaseTurnBufferGate(key) {
51186
51094
  activeTurnStartedAt.delete(key);
51187
51095
  claudeBusyKeys.delete(key);
51188
51096
  shadowEmit({ kind: "turnEnd", key, at: Date.now(), outboundEmitted: true });
51189
- if (claudeBusyKeys.size === 0) {
51097
+ if (!turnInFlightForGate()) {
51190
51098
  const selfAgentForFlush = process.env.SWITCHROOM_AGENT_NAME ?? "";
51191
51099
  if (pendingInboundBuffer.depth(selfAgentForFlush) > 0) {
51192
51100
  const fr = redeliverBufferedInbound(pendingInboundBuffer, selfAgentForFlush, (m) => {
@@ -52134,6 +52042,11 @@ startTimer({
52134
52042
  `);
52135
52043
  }
52136
52044
  });
52045
+ var DELIVERY_MACHINE_TICK_MS = 30000;
52046
+ var _deliveryMachineTick = setInterval(() => {
52047
+ shadowEmit({ kind: "tick", now: Date.now() });
52048
+ }, DELIVERY_MACHINE_TICK_MS);
52049
+ _deliveryMachineTick.unref?.();
52137
52050
  startTimer2({
52138
52051
  editMessage: async (ctx) => {
52139
52052
  const editOpts = ctx.parseMode != null ? { parse_mode: ctx.parseMode } : undefined;
@@ -52435,7 +52348,7 @@ ${reminder}
52435
52348
  onHeartbeat(_client, _msg) {},
52436
52349
  onScheduleRestart(client3, msg) {
52437
52350
  const { agentName: agentName3 } = msg;
52438
- const turnInFlight = claudeBusyKeys.size > 0;
52351
+ const turnInFlight = turnInFlightForGate();
52439
52352
  if (!turnInFlight) {
52440
52353
  try {
52441
52354
  client3.send({
@@ -52690,7 +52603,7 @@ if (!STATIC) {
52690
52603
  setInterval(() => {
52691
52604
  const selfAgent = process.env.SWITCHROOM_AGENT_NAME ?? "";
52692
52605
  const r = idleDrainTick(pendingInboundBuffer, selfAgent, () => {
52693
- if (claudeBusyKeys.size > 0)
52606
+ if (turnInFlightForGate())
52694
52607
  return false;
52695
52608
  const c = ipcServer.getClient(selfAgent);
52696
52609
  return c != null && c.isAlive();
@@ -52971,6 +52884,7 @@ ${url}`;
52971
52884
  });
52972
52885
  noteOutbound(statusKey(chat_id, threadId), Date.now());
52973
52886
  noteOutbound2(statusKey(chat_id, threadId), Date.now());
52887
+ shadowEmit({ kind: "modelOutbound", key: statusKey(chat_id, threadId), at: Date.now() });
52974
52888
  if (isFinalAnswerReply({ text: rawText, disableNotification })) {
52975
52889
  clearSilentEndState(statusKey(chat_id, threadId));
52976
52890
  }
@@ -53298,6 +53212,7 @@ async function executeStreamReply(args) {
53298
53212
  const sKey = statusKey(streamChatId, streamThreadId);
53299
53213
  noteOutbound(sKey, Date.now());
53300
53214
  noteOutbound2(sKey, Date.now());
53215
+ shadowEmit({ kind: "modelOutbound", key: sKey, at: Date.now() });
53301
53216
  if (isFinalAnswerReply({
53302
53217
  text: args.text ?? "",
53303
53218
  disableNotification: args.disable_notification === true,
@@ -54248,6 +54163,11 @@ function handleSessionEvent(ev) {
54248
54163
  isDm: isDmChatId(ev.chatId)
54249
54164
  };
54250
54165
  currentTurn = next;
54166
+ shadowEmit({
54167
+ kind: "turnStart",
54168
+ key: statusKey(ev.chatId, ev.threadId != null ? Number(ev.threadId) : undefined),
54169
+ at: startedAt
54170
+ });
54251
54171
  preambleSuppressor.reset();
54252
54172
  clearSilentEndState(statusKey(ev.chatId, ev.threadId != null ? Number(ev.threadId) : null));
54253
54173
  if (turnsDb != null) {
@@ -54309,12 +54229,12 @@ function handleSessionEvent(ev) {
54309
54229
  clearTimeout(turn.orphanedReplyTimeoutId);
54310
54230
  turn.orphanedReplyTimeoutId = null;
54311
54231
  }
54312
- if (wasFirstReply) {
54232
+ if (wasFirstReply && !DRAFT_MIRROR_ENABLED) {
54313
54233
  clearActivitySummary(turn);
54314
54234
  }
54315
54235
  }
54316
- if (!turn.replyCalled && !isTelegramSurfaceTool(name)) {
54317
- const rendered = DRAFT_MIRROR_ENABLED ? appendActivityLine(turn.mirrorLines, name, ev.input) : registerAndRender(turn.toolActivity, name);
54236
+ if (!DRAFT_MIRROR_ENABLED && !turn.replyCalled && !isTelegramSurfaceTool(name)) {
54237
+ const rendered = registerAndRender(turn.toolActivity, name);
54318
54238
  if (rendered != null) {
54319
54239
  turn.activityPendingRender = rendered;
54320
54240
  if (turn.activityInFlight == null) {
@@ -54332,6 +54252,23 @@ function handleSessionEvent(ev) {
54332
54252
  }
54333
54253
  return;
54334
54254
  }
54255
+ case "tool_label": {
54256
+ if (!DRAFT_MIRROR_ENABLED)
54257
+ return;
54258
+ const turn = currentTurn;
54259
+ if (turn == null)
54260
+ return;
54261
+ if (isTelegramSurfaceTool(ev.toolName))
54262
+ return;
54263
+ const rendered = appendActivityLabel(turn.mirrorLines, ev.label);
54264
+ if (rendered != null) {
54265
+ turn.activityPendingRender = rendered;
54266
+ if (turn.activityInFlight == null) {
54267
+ turn.activityInFlight = drainActivitySummary(turn);
54268
+ }
54269
+ }
54270
+ return;
54271
+ }
54335
54272
  case "text": {
54336
54273
  const turn = currentTurn;
54337
54274
  if (turn != null) {
@@ -54461,6 +54398,9 @@ function handleSessionEvent(ev) {
54461
54398
  clearTimeout(turn.orphanedReplyTimeoutId);
54462
54399
  turn.orphanedReplyTimeoutId = null;
54463
54400
  }
54401
+ if (DRAFT_MIRROR_ENABLED && turn != null) {
54402
+ clearActivitySummary(turn);
54403
+ }
54464
54404
  preambleSuppressor.flushNow();
54465
54405
  let streamFinalizedAsAnswer = false;
54466
54406
  if (turn?.answerStream != null) {
@@ -55001,6 +54941,7 @@ async function handleInbound(ctx, text, downloadImage, attachment) {
55001
54941
  }
55002
54942
  const inboundReceivedAt = Date.now();
55003
54943
  const _shadowKey = statusKey(ctx.chat?.id != null ? String(ctx.chat.id) : "0", ctx.message?.message_thread_id);
54944
+ const machineInTurnAtReceipt = isDeliveryCutoverEnabled() ? isMachineInTurn() : null;
55004
54945
  shadowEmit({
55005
54946
  kind: "inbound",
55006
54947
  key: _shadowKey,
@@ -55011,7 +54952,7 @@ async function handleInbound(ctx, text, downloadImage, attachment) {
55011
54952
  },
55012
54953
  at: Date.now()
55013
54954
  });
55014
- const turnInFlightAtReceipt = claudeBusyKeys.size > 0;
54955
+ const turnInFlightAtReceipt = machineInTurnAtReceipt ?? claudeBusyKeys.size > 0;
55015
54956
  const access = result.access;
55016
54957
  const from = ctx.from;
55017
54958
  const chat_id = String(ctx.chat.id);
@@ -59096,20 +59037,44 @@ ${prettyInput}`;
59096
59037
  return;
59097
59038
  }
59098
59039
  let grantOk = false;
59040
+ let grantFailReason = "";
59099
59041
  try {
59100
59042
  switchroomExec(["agent", "grant", agentName3, rule.rule, "--no-restart"]);
59101
- grantOk = true;
59102
- process.stderr.write(`telegram gateway: always-allow added rule="${rule.rule}" agent=${agentName3} (request_id=${request_id})
59043
+ try {
59044
+ const cfg = loadConfig2();
59045
+ const rawAgent = cfg.agents?.[agentName3];
59046
+ if (rawAgent) {
59047
+ const resolved = resolveAgentConfig2(cfg.defaults, cfg.profiles, rawAgent);
59048
+ const allowList = resolved.tools?.allow ?? [];
59049
+ if (isRulePersisted(allowList, rule.rule)) {
59050
+ grantOk = true;
59051
+ process.stderr.write(`telegram gateway: always-allow added rule="${rule.rule}" agent=${agentName3} (request_id=${request_id})
59052
+ `);
59053
+ } else {
59054
+ grantFailReason = `rule "${rule.rule}" not found in resolved tools.allow after write \u2014 config location may have drifted`;
59055
+ process.stderr.write(`telegram gateway: always-allow VERIFY FAILED: ${grantFailReason} (request_id=${request_id})
59056
+ `);
59057
+ }
59058
+ } else {
59059
+ grantFailReason = `agent "${agentName3}" not found in config after write`;
59060
+ process.stderr.write(`telegram gateway: always-allow VERIFY FAILED: ${grantFailReason} (request_id=${request_id})
59103
59061
  `);
59062
+ }
59063
+ } catch (verifyErr) {
59064
+ grantFailReason = `config re-read failed: ${verifyErr.message}`;
59065
+ process.stderr.write(`telegram gateway: always-allow VERIFY FAILED: ${grantFailReason} (request_id=${request_id})
59066
+ `);
59067
+ }
59104
59068
  } catch (err) {
59105
- process.stderr.write(`telegram gateway: always-allow grant failed: ${err.message}
59069
+ grantFailReason = err.message;
59070
+ process.stderr.write(`telegram gateway: always-allow grant failed: ${grantFailReason}
59106
59071
  `);
59107
59072
  }
59108
59073
  pendingPermissions.delete(request_id);
59109
- const ackText = grantOk ? `\uD83D\uDD01 Always allow ${rule.label} for ${agentName3}` : `\u2705 Allowed (always-allow yaml edit failed; check gateway log)`;
59074
+ const ackText = grantOk ? `\uD83D\uDD01 Always allow ${rule.label} for ${agentName3}` : `\u26A0\uFE0F Allowed for now, but "always" did NOT save \u2014 it will ask again after restart. Check gateway log.`;
59110
59075
  const sourceMsg = ctx.callbackQuery?.message;
59111
59076
  const baseText2 = sourceMsg && "text" in sourceMsg && sourceMsg.text ? escapeHtmlForTg(sourceMsg.text) : "";
59112
- const editLabel = grantOk ? `\uD83D\uDD01 <b>Always allow ${escapeHtmlForTg(rule.label)}</b> for ${escapeHtmlForTg(agentName3)} \u2014 restart agent for full effect` : `\u2705 <b>Allowed</b> (always-allow rule edit failed; see logs)`;
59077
+ const editLabel = grantOk ? `\uD83D\uDD01 <b>Always allow ${escapeHtmlForTg(rule.label)}</b> for ${escapeHtmlForTg(agentName3)} \u2014 restart agent for full effect` : `\u26A0\uFE0F <b>Allowed for now \u2014 "always" did NOT save.</b> It will ask again after restart. Check gateway log.`;
59113
59078
  await finalizeCallback(ctx, {
59114
59079
  ackText: ackText.slice(0, 200),
59115
59080
  newText: baseText2 ? `${baseText2}
@@ -17069,6 +17069,7 @@ import { join as join3 } from "node:path";
17069
17069
  function createToolLabelSidecar(opts) {
17070
17070
  const path = join3(opts.stateDir, `tool-labels-${opts.sessionId}.jsonl`);
17071
17071
  const labels = new Map;
17072
+ const seen = [];
17072
17073
  const subscribers = new Set;
17073
17074
  let offset = 0;
17074
17075
  let stopped = false;
@@ -17091,14 +17092,15 @@ function createToolLabelSidecar(opts) {
17091
17092
  } catch {
17092
17093
  continue;
17093
17094
  }
17094
- if (!row || typeof row.tool_use_id !== "string" || typeof row.label !== "string")
17095
+ if (!row || typeof row.tool_use_id !== "string" || typeof row.label !== "string" || typeof row.tool_name !== "string")
17095
17096
  continue;
17096
17097
  if (labels.has(row.tool_use_id))
17097
17098
  continue;
17098
17099
  labels.set(row.tool_use_id, row.label);
17100
+ seen.push({ toolUseId: row.tool_use_id, label: row.label, toolName: row.tool_name });
17099
17101
  for (const cb of subscribers) {
17100
17102
  try {
17101
- cb(row.tool_use_id, row.label);
17103
+ cb(row.tool_use_id, row.label, row.tool_name);
17102
17104
  } catch {}
17103
17105
  }
17104
17106
  }
@@ -17137,6 +17139,11 @@ function createToolLabelSidecar(opts) {
17137
17139
  return labels.get(toolUseId);
17138
17140
  },
17139
17141
  onLabel(cb) {
17142
+ for (const r of seen) {
17143
+ try {
17144
+ cb(r.toolUseId, r.label, r.toolName);
17145
+ } catch {}
17146
+ }
17140
17147
  subscribers.add(cb);
17141
17148
  return () => subscribers.delete(cb);
17142
17149
  },
@@ -17479,6 +17486,9 @@ function startSessionTail(config2) {
17479
17486
  try {
17480
17487
  const s = createToolLabelSidecar({ stateDir: stateDirForSidecar, sessionId });
17481
17488
  sidecars.set(sessionId, s);
17489
+ s.onLabel((toolUseId, label, toolName) => {
17490
+ rawOnEvent({ kind: "tool_label", toolUseId, label, toolName });
17491
+ });
17482
17492
  return s;
17483
17493
  } catch (err) {
17484
17494
  log?.(`session-tail: sidecar create failed: ${err.message}`);
@@ -17592,6 +17602,9 @@ function startSessionTail(config2) {
17592
17602
  }
17593
17603
  log?.(`session-tail: attached to ${file} (cursor=${cursor})`);
17594
17604
  }
17605
+ const attachSid = sessionIdForFile(file);
17606
+ if (attachSid)
17607
+ ensureSidecar(attachSid);
17595
17608
  try {
17596
17609
  watcher = watch(file, () => readNew());
17597
17610
  } catch (err) {