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.
package/dist/cli/switchroom.js
CHANGED
|
@@ -49083,8 +49083,8 @@ var {
|
|
|
49083
49083
|
} = import__.default;
|
|
49084
49084
|
|
|
49085
49085
|
// src/build-info.ts
|
|
49086
|
-
var VERSION = "0.13.
|
|
49087
|
-
var COMMIT_SHA = "
|
|
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
|
|
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
|
-
` +
|
|
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
|
-
` + "
|
|
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
|
-
` +
|
|
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
|
|
51546
|
+
...webkiteDenyForAgent(agentConfig)
|
|
51534
51547
|
]);
|
|
51535
51548
|
let topicId = agentConfig.topic_id;
|
|
51536
51549
|
if (topicId === undefined) {
|
package/package.json
CHANGED
|
@@ -49716,10 +49716,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
|
|
|
49716
49716
|
}
|
|
49717
49717
|
|
|
49718
49718
|
// ../src/build-info.ts
|
|
49719
|
-
var VERSION = "0.13.
|
|
49720
|
-
var COMMIT_SHA = "
|
|
49721
|
-
var COMMIT_DATE = "2026-05-
|
|
49722
|
-
var LATEST_PR =
|
|
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
|
-
|
|
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
|
-
//
|
|
7162
|
-
//
|
|
7163
|
-
//
|
|
7164
|
-
//
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
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
|
+
});
|