switchroom 0.13.62 → 0.13.64

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.
@@ -49083,8 +49083,8 @@ var {
49083
49083
  } = import__.default;
49084
49084
 
49085
49085
  // src/build-info.ts
49086
- var VERSION = "0.13.62";
49087
- var COMMIT_SHA = "7242df89";
49086
+ var VERSION = "0.13.64";
49087
+ var COMMIT_SHA = "52afe8b0";
49088
49088
 
49089
49089
  // src/cli/agent.ts
49090
49090
  init_source();
@@ -49953,6 +49953,21 @@ var DEFAULT_READ_ONLY_PREAPPROVED_TOOLS = [
49953
49953
  "Skill"
49954
49954
  ];
49955
49955
  var WEBKITE_FLEET_DENY_TOOLS = ["WebFetch", "WebSearch"];
49956
+ var WEBKITE_BINARY_CONTAINER_PATH = "/usr/local/bin/webkite";
49957
+ function webkiteBinaryAvailable() {
49958
+ const override = process.env.SWITCHROOM_WEBKITE_BINARY;
49959
+ if (override !== undefined && override !== "") {
49960
+ return existsSync11(override);
49961
+ }
49962
+ return existsSync11(join8(homedir4(), ".switchroom", "bin", "webkite")) || existsSync11(WEBKITE_BINARY_CONTAINER_PATH);
49963
+ }
49964
+ function webkiteDenyForAgent(agentConfig) {
49965
+ if (agentConfig.mcp_servers?.["webkite"] === false)
49966
+ return [];
49967
+ if (!webkiteBinaryAvailable())
49968
+ return [];
49969
+ return WEBKITE_FLEET_DENY_TOOLS;
49970
+ }
49956
49971
  var SWITCHROOM_DEFAULT_MAIN_MODEL = "claude-sonnet-4-6";
49957
49972
  var SWITCHROOM_DEFAULT_THINKING_EFFORT = "low";
49958
49973
  function dedupe2(items) {
@@ -50522,7 +50537,7 @@ function buildWorkspaceContext(args) {
50522
50537
  tools,
50523
50538
  toolsDeny: dedupe2([
50524
50539
  ...tools.deny ?? [],
50525
- ...agentConfig.mcp_servers?.["webkite"] === false ? [] : WEBKITE_FLEET_DENY_TOOLS
50540
+ ...webkiteDenyForAgent(agentConfig)
50526
50541
  ]),
50527
50542
  permissionAllow,
50528
50543
  defaultModeAcceptEdits: hasAllWildcard,
@@ -51358,16 +51373,14 @@ function buildSettingsHooksBlock(p) {
51358
51373
  const useHotReloadStable = agentConfig.channels?.telegram?.hotReloadStable === true;
51359
51374
  const turnPacingDirective = "<turn-pacing>You are messaging a human via Telegram. The framework " + "automatically shows the user a live preview in their compose area as " + 'you work \u2014 they see "Read a file", "Ran 2 commands", etc. as your ' + `tool_use events stream. You do NOT need to ack manually.
51360
51375
 
51361
- ` + 'Do NOT call the reply tool with placeholder acks like "on it", ' + '"good question \u2014 one sec", "let me dig in", "checking now", etc. ' + "Those add chat clutter on top of the activity preview the user is " + "already seeing. The activity preview clears the moment you send a " + `real reply.
51362
-
51363
- ` + `Call reply only when you have something substantive to deliver:
51364
- ` + ` - The actual answer (any length \u2014 short or long)
51365
- ` + ` - A genuine question back to the user
51366
- ` + " - A real mid-work milestone or pivot that changes what the user " + 'should expect (e.g. "halfway through \u2014 found an unexpected issue, ' + `want me to continue?"). Not "still working".
51376
+ ` + "ALWAYS reply to a message the user sends you. A direct message " + 'expects a response: a greeting ("hi", "hey", "you there?") gets a ' + "greeting back; a thanks gets a brief acknowledgement; a question " + "gets an answer. NEVER end a turn with NO_REPLY when the user has " + "just sent you something \u2014 NO_REPLY is only for genuine non-prompts " + `(a system-synthesized event you have already fully handled).
51367
51377
 
51368
- ` + "For trivial one-sentence answers: just reply with the answer. The " + `reply IS the answer, not an ack.
51378
+ ` + "What you should NOT do is send a placeholder ack BEFORE doing the " + 'work \u2014 no "on it", "good question \u2014 one sec", "let me dig in", ' + '"checking now". Those add chat clutter on top of the activity ' + "preview the user already sees, and the preview clears the moment " + `your real reply lands. Do not ack-then-answer; just answer.
51369
51379
 
51370
- ` + "For complex tool-driven work: go straight to the tools. The compose-" + "area preview is the ambient liveness signal. Reply once you have " + "the answer or a real reason to break in.</turn-pacing>";
51380
+ ` + `So:
51381
+ ` + " - Trivial / social message \u2192 reply once, briefly, in your voice. " + `The reply IS the response.
51382
+ ` + ` - Question with a short answer \u2192 just reply with the answer.
51383
+ ` + " - 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>';
51371
51384
  const switchroomUserPromptSubmit = [
51372
51385
  ...useHotReloadStable ? [
51373
51386
  {
@@ -51530,7 +51543,7 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
51530
51543
  ]);
51531
51544
  const desiredDeny = dedupe2([
51532
51545
  ...tools.deny ?? [],
51533
- ...agentConfig.mcp_servers?.["webkite"] === false ? [] : WEBKITE_FLEET_DENY_TOOLS
51546
+ ...webkiteDenyForAgent(agentConfig)
51534
51547
  ]);
51535
51548
  let topicId = agentConfig.topic_id;
51536
51549
  if (topicId === undefined) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.13.62",
3
+ "version": "0.13.64",
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": {
@@ -49716,10 +49716,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
49716
49716
  }
49717
49717
 
49718
49718
  // ../src/build-info.ts
49719
- var VERSION = "0.13.62";
49720
- var COMMIT_SHA = "7242df89";
49721
- var COMMIT_DATE = "2026-05-28T02:42:06Z";
49722
- var LATEST_PR = 1943;
49719
+ var VERSION = "0.13.64";
49720
+ var COMMIT_SHA = "52afe8b0";
49721
+ var COMMIT_DATE = "2026-05-28T04:44:59Z";
49722
+ var LATEST_PR = 1948;
49723
49723
  var COMMITS_AHEAD_OF_TAG = 0;
49724
49724
 
49725
49725
  // gateway/boot-version.ts
@@ -51489,6 +51489,13 @@ var ANSWER_STREAM_VISIBLE_ENABLED = (() => {
51489
51489
  return false;
51490
51490
  return true;
51491
51491
  })();
51492
+ var DRAFT_MIRROR_ENABLED = (() => {
51493
+ const raw = process.env.SWITCHROOM_DRAFT_MIRROR;
51494
+ if (raw == null)
51495
+ return false;
51496
+ const v = raw.trim().toLowerCase();
51497
+ return !(v === "0" || v === "false" || v === "off" || v === "no");
51498
+ })();
51492
51499
  var progressDriver = null;
51493
51500
  var unpinProgressCardForChat = null;
51494
51501
  var getPinnedProgressCardMessageId = null;
@@ -53856,7 +53863,7 @@ function handleSessionEvent(ev) {
53856
53863
  clearActivitySummary(turn);
53857
53864
  }
53858
53865
  }
53859
- if (!turn.replyCalled && !isTelegramSurfaceTool(name)) {
53866
+ if (!DRAFT_MIRROR_ENABLED && !turn.replyCalled && !isTelegramSurfaceTool(name)) {
53860
53867
  const rendered = registerAndRender(turn.toolActivity, name);
53861
53868
  if (rendered != null) {
53862
53869
  turn.activityPendingRender = rendered;
@@ -53884,7 +53891,7 @@ function handleSessionEvent(ev) {
53884
53891
  chatId: turn.sessionChatId,
53885
53892
  isPrivateChat: turn.isDm,
53886
53893
  threadId: turn.sessionThreadId,
53887
- ...ANSWER_STREAM_VISIBLE_ENABLED ? { minInitialChars: 1 } : { sendMessageDraft: sendMessageDraftFn },
53894
+ ...DRAFT_MIRROR_ENABLED ? { sendMessageDraft: sendMessageDraftFn } : ANSWER_STREAM_VISIBLE_ENABLED ? { minInitialChars: 1 } : { sendMessageDraft: sendMessageDraftFn },
53888
53895
  sendMessage: async (chatId, text, params) => {
53889
53896
  const tid = params?.message_thread_id;
53890
53897
  const silent = params?.purpose !== "materialize";
@@ -3192,6 +3192,26 @@ const ANSWER_STREAM_VISIBLE_ENABLED = (() => {
3192
3192
  if (v === '0' || v === 'false' || v === 'off' || v === 'no') return false
3193
3193
  return true
3194
3194
  })()
3195
+
3196
+ // Draft-mirror preview (RFC docs/rfcs/draft-mirror-preview.md), Phase 1.
3197
+ // When enabled, the model's prose narration streams into the ephemeral
3198
+ // compose-area draft (sendMessageDraft) instead of a visible real
3199
+ // message — a live "what's it doing" preview that clears when the
3200
+ // reply lands. Default OFF (canary flag). When on it (a) forces the
3201
+ // answer-stream onto draft transport regardless of
3202
+ // ANSWER_STREAM_VISIBLE_ENABLED, and (b) suppresses the activity-summary
3203
+ // tool-count draft so the two don't collide on the single per-chat
3204
+ // draft slot. Delivery on a no-reply turn is owned by turn-flush
3205
+ // (decideTurnFlush → capturedText fresh send), NOT answer-stream
3206
+ // materialize() — which is dead on the draft-only path (streamMsgId
3207
+ // stays null, so its turn-end gate is false). Kill switch:
3208
+ // SWITCHROOM_DRAFT_MIRROR unset/0/false/off/no.
3209
+ const DRAFT_MIRROR_ENABLED = (() => {
3210
+ const raw = process.env.SWITCHROOM_DRAFT_MIRROR
3211
+ if (raw == null) return false
3212
+ const v = raw.trim().toLowerCase()
3213
+ return !(v === '0' || v === 'false' || v === 'off' || v === 'no')
3214
+ })()
3195
3215
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
3196
3216
  const progressDriver: any = null
3197
3217
  const unpinProgressCardForChat: ((chatId: string, threadId: number | undefined) => void) | null = null
@@ -7110,7 +7130,13 @@ function handleSessionEvent(ev: SessionEvent): void {
7110
7130
  // exactly once at a time and re-running until pending matches
7111
7131
  // the last-sent. Captures `turn` so a late drain after turn-swap
7112
7132
  // can't corrupt the next turn's atom.
7113
- if (!turn.replyCalled && !isTelegramSurfaceTool(name)) {
7133
+ // DRAFT_MIRROR (RFC draft-mirror-preview, Phase 1): the model's
7134
+ // prose narration owns the single per-chat draft slot. Suppress
7135
+ // the activity-summary tool-count draft so the two don't collide
7136
+ // (Telegram shows one draft per chat — the later write clobbers
7137
+ // the earlier). The activity-summary code stays intact for the
7138
+ // kill-switch path; it's retired for good only in Phase 4.
7139
+ if (!DRAFT_MIRROR_ENABLED && !turn.replyCalled && !isTelegramSurfaceTool(name)) {
7114
7140
  const rendered = registerAndRender(turn.toolActivity, name)
7115
7141
  if (rendered != null) {
7116
7142
  turn.activityPendingRender = rendered
@@ -7158,13 +7184,20 @@ function handleSessionEvent(ev: SessionEvent): void {
7158
7184
  chatId: turn.sessionChatId,
7159
7185
  isPrivateChat: turn.isDm,
7160
7186
  threadId: turn.sessionThreadId,
7161
- // #869-Phase1 visible-answer-stream: omit the draft API so
7162
- // the lane uses the real sendMessage / editMessageText path
7163
- // and edits a user-visible chat-timeline message instead
7164
- // of the invisible compose-box draft.
7165
- ...(ANSWER_STREAM_VISIBLE_ENABLED
7166
- ? { minInitialChars: 1 }
7167
- : { sendMessageDraft: sendMessageDraftFn }),
7187
+ // Transport selection:
7188
+ // - DRAFT_MIRROR (RFC draft-mirror-preview, Phase 1): force
7189
+ // the ephemeral compose-area draft so narration is a
7190
+ // clears-on-reply preview. Wins over visible-answer-stream.
7191
+ // No-reply delivery is owned by turn-flush, not materialize.
7192
+ // - else #869-Phase1 visible-answer-stream: omit the draft
7193
+ // API so the lane edits a user-visible chat-timeline
7194
+ // message (minInitialChars:1 opens it on the first chunk).
7195
+ // - else legacy: draft transport.
7196
+ ...(DRAFT_MIRROR_ENABLED
7197
+ ? { sendMessageDraft: sendMessageDraftFn }
7198
+ : ANSWER_STREAM_VISIBLE_ENABLED
7199
+ ? { minInitialChars: 1 }
7200
+ : { sendMessageDraft: sendMessageDraftFn }),
7168
7201
  // #1075: route through robustApiCall so flood-wait,
7169
7202
  // benign-400, and THREAD_NOT_FOUND are handled uniformly
7170
7203
  // instead of crashing the answer-stream loop on a deleted
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Greeting reply scenario — driver DMs the test bot a bare "hi", bot
3
+ * MUST reply (not NO_REPLY).
4
+ *
5
+ * Regression gate for the v0.13.61 turn-pacing v3 over-correction: the
6
+ * "don't ack" directive made the model classify a bare greeting as
7
+ * "not substantive" and end the turn with NO_REPLY, leaving the user
8
+ * staring at silence. v4 (this fix) re-asserts "always reply to a
9
+ * direct message; a greeting gets a greeting."
10
+ *
11
+ * Runs against real Telegram. Same env requirements as
12
+ * smoke-dm-reply.test.ts. Invoke via `bun run test:uat greeting`.
13
+ */
14
+
15
+ import { describe, it, expect } from "vitest";
16
+ import { spinUp } from "../harness.js";
17
+
18
+ describe("uat: greeting gets a reply (v4 turn-pacing regression gate)", () => {
19
+ it(
20
+ "driver DMs a bare 'hi' and the bot replies within 60s (not NO_REPLY)",
21
+ async () => {
22
+ const sc = await spinUp({ agent: "test-harness" });
23
+
24
+ try {
25
+ const started = Date.now();
26
+ await sc.sendDM("hi");
27
+
28
+ // A greeting should come back fast — well under the silence-poke
29
+ // soft window. 60s budget tolerates a cold turn but a NO_REPLY
30
+ // (the bug) would blow past it via the 300s framework fallback.
31
+ const reply = await sc.expectMessage(/.+/, {
32
+ from: "bot",
33
+ timeout: 60_000,
34
+ });
35
+ const elapsed = Date.now() - started;
36
+
37
+ expect(reply.text.length).toBeGreaterThan(0);
38
+ expect(reply.senderUserId).toBe(sc.botUserId);
39
+ // The bug path replies only after the 300s framework fallback.
40
+ // A real greeting reply lands fast; assert it beat the fallback.
41
+ expect(elapsed).toBeLessThan(60_000);
42
+ } finally {
43
+ await sc.tearDown();
44
+ }
45
+ },
46
+ 90_000,
47
+ );
48
+ });