switchroom 0.14.51 → 0.14.53
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 +5 -3
- package/package.json +1 -1
- package/telegram-plugin/answer-stream-flag.ts +18 -0
- package/telegram-plugin/dist/gateway/gateway.js +14 -14
- package/telegram-plugin/gateway/gateway.ts +39 -18
- package/telegram-plugin/tests/answer-stream-flag.test.ts +27 -0
- package/telegram-plugin/tests/gateway-no-reply-single-emit.test.ts +35 -0
package/dist/cli/switchroom.js
CHANGED
|
@@ -49462,8 +49462,8 @@ var {
|
|
|
49462
49462
|
} = import__.default;
|
|
49463
49463
|
|
|
49464
49464
|
// src/build-info.ts
|
|
49465
|
-
var VERSION = "0.14.
|
|
49466
|
-
var COMMIT_SHA = "
|
|
49465
|
+
var VERSION = "0.14.53";
|
|
49466
|
+
var COMMIT_SHA = "40ba8e59";
|
|
49467
49467
|
|
|
49468
49468
|
// src/cli/agent.ts
|
|
49469
49469
|
init_source();
|
|
@@ -52032,7 +52032,9 @@ function buildSettingsHooksBlock(p) {
|
|
|
52032
52032
|
` + ` - Question with a short answer \u2192 just reply with the answer.
|
|
52033
52033
|
` + " - 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".
|
|
52034
52034
|
|
|
52035
|
-
` + '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 ` +
|
|
52035
|
+
` + '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.
|
|
52036
|
+
|
|
52037
|
+
` + 'CRITICAL: "answer" means a call to the reply tool ' + "(mcp__switchroom-telegram__reply, or stream_reply with done=true). " + "Your terminal/transcript text is NEVER delivered to Telegram \u2014 the " + "user sees only what you send through the reply tool. After a long " + "tool sequence (scheduling, multi-step research, sub-agent handback), " + "do not let your closing narration stand as the answer: end the turn " + "by passing that narration to the reply tool. No reply tool call = the " + "user got nothing, however much text you wrote.</turn-pacing>";
|
|
52036
52038
|
const switchroomUserPromptSubmit = [
|
|
52037
52039
|
...useHotReloadStable ? [
|
|
52038
52040
|
{
|
package/package.json
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse the `SWITCHROOM_VISIBLE_ANSWER_STREAM` env flag.
|
|
3
|
+
*
|
|
4
|
+
* Default **OFF** (flipped 2026-06-03, operator request) — see the rationale
|
|
5
|
+
* on the `ANSWER_STREAM_VISIBLE_ENABLED` gate in `gateway/gateway.ts`. When
|
|
6
|
+
* off, the answer lane streams to the invisible compose-box draft and the
|
|
7
|
+
* reply tool is the single canonical formatted message — no unformatted
|
|
8
|
+
* preliminary that flashes and gets deleted. Opt back IN per agent with
|
|
9
|
+
* `SWITCHROOM_VISIBLE_ANSWER_STREAM=1` (also accepts true/on/yes).
|
|
10
|
+
*
|
|
11
|
+
* Extracted as a pure function so the default + parsing are unit-testable
|
|
12
|
+
* (gateway.ts is not importable in isolation — top-level side effects).
|
|
13
|
+
*/
|
|
14
|
+
export function parseVisibleAnswerStreamEnabled(raw: string | undefined): boolean {
|
|
15
|
+
if (raw == null) return false
|
|
16
|
+
const v = raw.trim().toLowerCase()
|
|
17
|
+
return v === '1' || v === 'true' || v === 'on' || v === 'yes'
|
|
18
|
+
}
|
|
@@ -39700,6 +39700,14 @@ function createAnswerStream(config) {
|
|
|
39700
39700
|
};
|
|
39701
39701
|
}
|
|
39702
39702
|
|
|
39703
|
+
// answer-stream-flag.ts
|
|
39704
|
+
function parseVisibleAnswerStreamEnabled(raw) {
|
|
39705
|
+
if (raw == null)
|
|
39706
|
+
return false;
|
|
39707
|
+
const v = raw.trim().toLowerCase();
|
|
39708
|
+
return v === "1" || v === "true" || v === "on" || v === "yes";
|
|
39709
|
+
}
|
|
39710
|
+
|
|
39703
39711
|
// pty-tail.ts
|
|
39704
39712
|
var import_headless = __toESM(require_xterm_headless(), 1);
|
|
39705
39713
|
var PTY_DEBUG = process.env.SWITCHROOM_PTY_DEBUG === "1";
|
|
@@ -52158,10 +52166,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
|
|
|
52158
52166
|
}
|
|
52159
52167
|
|
|
52160
52168
|
// ../src/build-info.ts
|
|
52161
|
-
var VERSION = "0.14.
|
|
52162
|
-
var COMMIT_SHA = "
|
|
52163
|
-
var COMMIT_DATE = "2026-06-
|
|
52164
|
-
var LATEST_PR =
|
|
52169
|
+
var VERSION = "0.14.53";
|
|
52170
|
+
var COMMIT_SHA = "40ba8e59";
|
|
52171
|
+
var COMMIT_DATE = "2026-06-03T13:56:26Z";
|
|
52172
|
+
var LATEST_PR = 2131;
|
|
52165
52173
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
52166
52174
|
|
|
52167
52175
|
// gateway/boot-version.ts
|
|
@@ -54308,15 +54316,7 @@ var STREAM_THROTTLE_MS_OVERRIDE = (() => {
|
|
|
54308
54316
|
return Number.isFinite(n) && n >= 0 ? n : undefined;
|
|
54309
54317
|
})();
|
|
54310
54318
|
var TURN_FLUSH_SAFETY_ENABLED = isTurnFlushSafetyEnabled();
|
|
54311
|
-
var ANSWER_STREAM_VISIBLE_ENABLED = (
|
|
54312
|
-
const raw = process.env.SWITCHROOM_VISIBLE_ANSWER_STREAM;
|
|
54313
|
-
if (raw == null)
|
|
54314
|
-
return true;
|
|
54315
|
-
const v = raw.trim().toLowerCase();
|
|
54316
|
-
if (v === "0" || v === "false" || v === "off" || v === "no")
|
|
54317
|
-
return false;
|
|
54318
|
-
return true;
|
|
54319
|
-
})();
|
|
54319
|
+
var ANSWER_STREAM_VISIBLE_ENABLED = parseVisibleAnswerStreamEnabled(process.env.SWITCHROOM_VISIBLE_ANSWER_STREAM);
|
|
54320
54320
|
var progressDriver = null;
|
|
54321
54321
|
var unpinProgressCardForChat = null;
|
|
54322
54322
|
var getPinnedProgressCardMessageId = null;
|
|
@@ -57086,7 +57086,7 @@ function handleSessionEvent(ev) {
|
|
|
57086
57086
|
chatId: turn.sessionChatId,
|
|
57087
57087
|
isPrivateChat: turn.isDm,
|
|
57088
57088
|
threadId: turn.sessionThreadId,
|
|
57089
|
-
...ANSWER_STREAM_VISIBLE_ENABLED ? { minInitialChars: 1 } : { sendMessageDraft: sendMessageDraftFn },
|
|
57089
|
+
...ANSWER_STREAM_VISIBLE_ENABLED ? { minInitialChars: 1 } : { sendMessageDraft: sendMessageDraftFn, minInitialChars: Number.MAX_SAFE_INTEGER },
|
|
57090
57090
|
sendMessage: async (chatId, text, params) => {
|
|
57091
57091
|
const tid = params?.message_thread_id;
|
|
57092
57092
|
const silent = params?.purpose !== "materialize";
|
|
@@ -96,6 +96,7 @@ import * as pendingProgress from '../pending-work-progress.js'
|
|
|
96
96
|
import { writeSilentEndState, clearSilentEndState, recordUndeliveredTurnEnd } from '../silent-end.js'
|
|
97
97
|
import { isFinalAnswerReply } from '../final-answer-detect.js'
|
|
98
98
|
import { createAnswerStream, type AnswerStreamHandle } from '../answer-stream.js'
|
|
99
|
+
import { parseVisibleAnswerStreamEnabled } from '../answer-stream-flag.js'
|
|
99
100
|
import { type SessionEvent } from '../session-tail.js'
|
|
100
101
|
import {
|
|
101
102
|
shouldSuppressToolActivity,
|
|
@@ -3738,23 +3739,33 @@ const TURN_FLUSH_SAFETY_ENABLED = isTurnFlushSafetyEnabled()
|
|
|
3738
3739
|
// the cross-turn pending-progress system (#1445/#1669) is the
|
|
3739
3740
|
// canonical surface and DOES ping at the appropriate boundaries.
|
|
3740
3741
|
//
|
|
3741
|
-
// 2026-05-25: default flipped ON after fleet-log audit showed
|
|
3742
|
-
// framework-fallback rate
|
|
3743
|
-
//
|
|
3744
|
-
//
|
|
3745
|
-
//
|
|
3746
|
-
//
|
|
3747
|
-
//
|
|
3748
|
-
//
|
|
3749
|
-
//
|
|
3750
|
-
//
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3742
|
+
// 2026-05-25: default flipped ON after a fleet-log audit showed a ~19%
|
|
3743
|
+
// framework-fallback ("still working…") rate — the visible stream gave an
|
|
3744
|
+
// immediate in-timeline signal that suppressed the silence-poke.
|
|
3745
|
+
//
|
|
3746
|
+
// 2026-06-03: default flipped back OFF (operator request). In practice the
|
|
3747
|
+
// visible stream delivered ~none of its intended benefit while imposing a
|
|
3748
|
+
// jarring cost:
|
|
3749
|
+
// - Telegram rate-limits editMessageText to roughly once/second, so real
|
|
3750
|
+
// "watch it type" streaming is impossible; and the model emits almost no
|
|
3751
|
+
// interstitial assistant.text (it thinks → tool → reply), so the
|
|
3752
|
+
// preliminary was a near-empty bubble (observed: 5–13 byte edits).
|
|
3753
|
+
// - On every turn where the model calls the reply tool (≈always), the reply
|
|
3754
|
+
// posts a SEPARATE canonical message and the stream RETRACTS (deletes) its
|
|
3755
|
+
// preliminary — the user sees a raw bubble appear then vanish, replaced by
|
|
3756
|
+
// the formatted reply. In supergroup topics it also mis-routed (preliminary
|
|
3757
|
+
// → General, reply → topic). Net: an unformatted flash + a delete, no
|
|
3758
|
+
// streaming value.
|
|
3759
|
+
// The anti-silence role the visible stream once filled is now covered by the
|
|
3760
|
+
// live ACTIVITY FEED (tool-use streaming, below), the "…typing" chat-action
|
|
3761
|
+
// loop, and `thinking_effort: low` (fast tool-less turns) — so off-by-default
|
|
3762
|
+
// no longer regresses the framework-fallback rate. With the flag off the lane
|
|
3763
|
+
// uses the invisible compose-box draft (the original default, #1664-compatible)
|
|
3764
|
+
// and the reply tool is the single canonical, formatted message.
|
|
3765
|
+
// Opt back IN per agent with SWITCHROOM_VISIBLE_ANSWER_STREAM=1.
|
|
3766
|
+
const ANSWER_STREAM_VISIBLE_ENABLED = parseVisibleAnswerStreamEnabled(
|
|
3767
|
+
process.env.SWITCHROOM_VISIBLE_ANSWER_STREAM,
|
|
3768
|
+
)
|
|
3758
3769
|
|
|
3759
3770
|
// Activity feed. The gateway streams a live "what it's doing" tool-activity
|
|
3760
3771
|
// feed for every turn. The PreToolUse sidecar emits a `tool_label` per tool
|
|
@@ -8426,9 +8437,19 @@ function handleSessionEvent(ev: SessionEvent): void {
|
|
|
8426
8437
|
// tool_use stream (case 'tool_use' above) where the real
|
|
8427
8438
|
// signal lives. assistant.text keeps its visible-message
|
|
8428
8439
|
// home; the reply tool stays the canonical answer.
|
|
8440
|
+
// Flag OFF (default): use the compose-box draft for DMs, and set
|
|
8441
|
+
// minInitialChars effectively-infinite so the lane NEVER opens a
|
|
8442
|
+
// visible chat message. This matters in supergroup TOPICS, where
|
|
8443
|
+
// draft transport is unsupported (gateway.ts:6422) so the lane
|
|
8444
|
+
// would otherwise fall to message transport and post a visible
|
|
8445
|
+
// preview once interstitial text passed the default 50-char gate
|
|
8446
|
+
// — which retract() then deletes (the unformatted flash, marko
|
|
8447
|
+
// General). With the gate unreachable the only posted message is
|
|
8448
|
+
// the canonical reply. (The gate is bypassed for DM draft
|
|
8449
|
+
// transport, so DM draft streaming is unaffected.)
|
|
8429
8450
|
...(ANSWER_STREAM_VISIBLE_ENABLED
|
|
8430
8451
|
? { minInitialChars: 1 }
|
|
8431
|
-
: { sendMessageDraft: sendMessageDraftFn }),
|
|
8452
|
+
: { sendMessageDraft: sendMessageDraftFn, minInitialChars: Number.MAX_SAFE_INTEGER }),
|
|
8432
8453
|
// #1075: route through robustApiCall so flood-wait,
|
|
8433
8454
|
// benign-400, and THREAD_NOT_FOUND are handled uniformly
|
|
8434
8455
|
// instead of crashing the answer-stream loop on a deleted
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pin the SWITCHROOM_VISIBLE_ANSWER_STREAM contract: default OFF (2026-06-03),
|
|
3
|
+
* opt-in only on a truthy value. Guards against an accidental flip back to
|
|
4
|
+
* default-on (which would reintroduce the unformatted-preliminary flash +
|
|
5
|
+
* delete-on-every-reply — see the gateway gate comment).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect } from 'vitest'
|
|
9
|
+
import { parseVisibleAnswerStreamEnabled } from '../answer-stream-flag.js'
|
|
10
|
+
|
|
11
|
+
describe('parseVisibleAnswerStreamEnabled — default OFF, opt-in', () => {
|
|
12
|
+
it('defaults OFF when unset', () => {
|
|
13
|
+
expect(parseVisibleAnswerStreamEnabled(undefined)).toBe(false)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('stays OFF for empty / falsey / unrecognized values', () => {
|
|
17
|
+
for (const v of ['', ' ', '0', 'false', 'off', 'no', 'nope', 'enabled', 'x']) {
|
|
18
|
+
expect(parseVisibleAnswerStreamEnabled(v)).toBe(false)
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('opts IN only on explicit truthy values (case/space-insensitive)', () => {
|
|
23
|
+
for (const v of ['1', 'true', 'on', 'yes', ' TRUE ', 'On', 'YES']) {
|
|
24
|
+
expect(parseVisibleAnswerStreamEnabled(v)).toBe(true)
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
})
|
|
@@ -68,6 +68,41 @@ describe('#656 — answer-stream retract() at turn_end emits nothing', () => {
|
|
|
68
68
|
expect(deleteMessage).not.toHaveBeenCalled()
|
|
69
69
|
})
|
|
70
70
|
|
|
71
|
+
// Visible-answer-stream OFF (default since 2026-06-03): the gateway passes
|
|
72
|
+
// minInitialChars=MAX_SAFE_INTEGER so a SUPERGROUP turn (message transport —
|
|
73
|
+
// draft is unsupported in forum topics) NEVER opens a visible preview, no
|
|
74
|
+
// matter how much interstitial assistant.text streams. This is the full fix
|
|
75
|
+
// for the marko-General unformatted-flash (the DM path is already covered by
|
|
76
|
+
// draft transport). Without it, message transport opens at the 50-char gate
|
|
77
|
+
// and retract() then deletes it.
|
|
78
|
+
it('flag-off supergroup: huge minInitialChars never opens a message even past the 50-char gate', async () => {
|
|
79
|
+
const sendMessage = vi.fn(async () => ({ message_id: nextMessageId++ }))
|
|
80
|
+
const editMessageText = vi.fn(async () => {})
|
|
81
|
+
const deleteMessage = vi.fn(async () => {})
|
|
82
|
+
|
|
83
|
+
const stream = createAnswerStream({
|
|
84
|
+
chatId: 'supergroup-topic',
|
|
85
|
+
isPrivateChat: false, // supergroup → message transport (no draft)
|
|
86
|
+
threadId: 4,
|
|
87
|
+
minInitialChars: Number.MAX_SAFE_INTEGER,
|
|
88
|
+
throttleMs: 250,
|
|
89
|
+
sendMessage: sendMessage as never,
|
|
90
|
+
editMessageText: editMessageText as never,
|
|
91
|
+
deleteMessage: deleteMessage as never,
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// Feed 200 chars — well past the default 50-char open gate.
|
|
95
|
+
stream.update('x'.repeat(200))
|
|
96
|
+
vi.advanceTimersByTime(1000)
|
|
97
|
+
await flushMicrotasks()
|
|
98
|
+
expect(sendMessage).not.toHaveBeenCalled() // never opened a preview
|
|
99
|
+
expect(editMessageText).not.toHaveBeenCalled()
|
|
100
|
+
|
|
101
|
+
await stream.retract()
|
|
102
|
+
expect(sendMessage).not.toHaveBeenCalled()
|
|
103
|
+
expect(deleteMessage).not.toHaveBeenCalled() // nothing to delete → no flash
|
|
104
|
+
})
|
|
105
|
+
|
|
71
106
|
it('retract after a preliminary send: deletes the prelim, no fresh sendMessage', async () => {
|
|
72
107
|
const THROTTLE = 1000
|
|
73
108
|
const sendMessage = vi.fn(async () => ({ message_id: nextMessageId++ }))
|