switchroom 0.14.71 → 0.14.72

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.
@@ -49601,8 +49601,8 @@ var {
49601
49601
  } = import__.default;
49602
49602
 
49603
49603
  // src/build-info.ts
49604
- var VERSION = "0.14.71";
49605
- var COMMIT_SHA = "314a0e0e";
49604
+ var VERSION = "0.14.72";
49605
+ var COMMIT_SHA = "0e840d59";
49606
49606
 
49607
49607
  // src/cli/agent.ts
49608
49608
  init_source();
@@ -52178,7 +52178,7 @@ function buildSettingsHooksBlock(p) {
52178
52178
 
52179
52179
  ` + "GROUND BEFORE YOU ASSERT. Any fact in your reply that can change " + "(a number, a status, a price, a date, who-uses-what, anything " + '"current" or "latest") must come from a source you actually checked ' + "THIS turn: your data tool, a file, the web. Memory and what you " + '"already know" are leads to verify, not sources. If you have not ' + "checked it this turn, do not state it as fact: go get it now, or tell " + "the user you will confirm and then do it. A confident wrong number is " + `worse than "let me check".
52180
52180
 
52181
- ` + "VOICE: write like a sharp colleague, not a chatbot. Do not open with " + `affirmation ("You're absolutely right", "Great question", "Great ` + 'catch", "Exactly!"); just answer. Skip AI-tell filler ("smoking ' + `gun", "delve", "it's worth noting", "a testament to", "in today's ` + 'fast-paced..."). Lead with the answer, plain words, kept short. When ' + `the user is wrong, say so directly; flattery is not help.
52181
+ ` + "VOICE: write like a sharp colleague, not a chatbot. Lead with the " + "answer, plain words, plain punctuation (commas and periods, not " + `em-dashes). Skip the hollow openers ("You're absolutely right", ` + '"Great question", "Great catch", "Exactly!") and AI-tell filler ' + `("smoking gun", "delve", "it's worth noting", "a testament to", "in ` + `today's fast-paced..."). Genuine acknowledgement is fine when it is ` + 'real and adds something ("good catch, that was my bug"); what to ' + "avoid is the reflexive praise that opens every reply and means " + "nothing. When the user is wrong, say so directly; flattery is not " + `help.
52182
52182
 
52183
52183
  ` + '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. Call the reply tool as " + "your FIRST action when you have the answer \u2014 do not write it out as " + "transcript text first and call reply afterward: a framework backstop " + "flushes unsent text after a delay and then your real reply lands late " + "and out of order.</turn-pacing>";
52184
52184
  const switchroomUserPromptSubmit = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.14.71",
3
+ "version": "0.14.72",
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": {
@@ -42543,6 +42543,10 @@ function enabled4() {
42543
42543
  const v = process.env.SWITCHROOM_DISABLE_VOICE_SCRUB;
42544
42544
  return !(v === "1" || v === "true");
42545
42545
  }
42546
+ function openerStripEnabled() {
42547
+ const v = process.env.SWITCHROOM_VOICE_STRIP_OPENERS;
42548
+ return v === "1" || v === "true";
42549
+ }
42546
42550
  var LEADING_AFFIRMATION_RE = /^(\s*)(you(?:['\u2019]| a)re absolutely right|you(?:['\u2019]| a)re so right|you(?:['\u2019]| a)re absolutely correct|absolutely right|exactly right|great catch|good catch|nice catch|spot on)\b(?:\s*$|\s*[!.,;:\u2014\u2013-][\s!.,;:\u2014\u2013-]*)/i;
42547
42551
  function stripLeadingAffirmation(text) {
42548
42552
  const m = LEADING_AFFIRMATION_RE.exec(text);
@@ -42620,7 +42624,7 @@ function scrubVoice(text) {
42620
42624
  return { scrubbed: text, replaced: 0, openersStripped: 0 };
42621
42625
  }
42622
42626
  const { parked, parts } = park(text);
42623
- const opener = stripLeadingAffirmation(parked);
42627
+ const opener = openerStripEnabled() ? stripLeadingAffirmation(parked) : { out: parked, count: 0 };
42624
42628
  const { out, replaced } = replaceDashes(opener.out);
42625
42629
  const total = replaced + opener.count;
42626
42630
  if (total === 0) {
@@ -52815,10 +52819,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
52815
52819
  }
52816
52820
 
52817
52821
  // ../src/build-info.ts
52818
- var VERSION = "0.14.71";
52819
- var COMMIT_SHA = "314a0e0e";
52820
- var COMMIT_DATE = "2026-06-05T14:23:58Z";
52821
- var LATEST_PR = 2181;
52822
+ var VERSION = "0.14.72";
52823
+ var COMMIT_SHA = "0e840d59";
52824
+ var COMMIT_DATE = "2026-06-06T00:39:32Z";
52825
+ var LATEST_PR = 2183;
52822
52826
  var COMMITS_AHEAD_OF_TAG = 0;
52823
52827
 
52824
52828
  // gateway/boot-version.ts
@@ -173,12 +173,17 @@ describe('scrubVoice — em / en dash replacement', () => {
173
173
  })
174
174
  })
175
175
 
176
- describe('scrubVoice — leading sycophancy openers', () => {
176
+ describe('scrubVoice — leading sycophancy openers (opt-in backstop)', () => {
177
+ // The opener strip is OFF by default (tone is the prompt's job); these
178
+ // tests opt it in to exercise the mechanism that remains available via
179
+ // SWITCHROOM_VOICE_STRIP_OPENERS=1.
177
180
  beforeEach(() => {
178
181
  delete process.env.SWITCHROOM_DISABLE_VOICE_SCRUB
182
+ process.env.SWITCHROOM_VOICE_STRIP_OPENERS = '1'
179
183
  })
180
184
  afterEach(() => {
181
185
  delete process.env.SWITCHROOM_DISABLE_VOICE_SCRUB
186
+ delete process.env.SWITCHROOM_VOICE_STRIP_OPENERS
182
187
  })
183
188
 
184
189
  it('strips a leading "You\'re absolutely right" and recapitalizes', () => {
@@ -261,3 +266,43 @@ describe('scrubVoice — leading sycophancy openers', () => {
261
266
  expect(r.openersStripped).toBe(0)
262
267
  })
263
268
  })
269
+
270
+ describe('scrubVoice — opener strip is OFF by default (prompt carries tone)', () => {
271
+ // No SWITCHROOM_VOICE_STRIP_OPENERS set: the deterministic layer must
272
+ // NOT delete words. Em-dash normalization (punctuation, no content
273
+ // removed) still runs. Tone is the prompt VOICE directive's job.
274
+ beforeEach(() => {
275
+ delete process.env.SWITCHROOM_DISABLE_VOICE_SCRUB
276
+ delete process.env.SWITCHROOM_VOICE_STRIP_OPENERS
277
+ })
278
+ afterEach(() => {
279
+ delete process.env.SWITCHROOM_VOICE_STRIP_OPENERS
280
+ })
281
+
282
+ it('does NOT strip a leading affirmation by default', () => {
283
+ const r = scrubVoice("You're absolutely right, the build is broken.")
284
+ expect(r.scrubbed).toBe("You're absolutely right, the build is broken.")
285
+ expect(r.openersStripped).toBe(0)
286
+ })
287
+
288
+ it('does NOT strip "Great catch" by default', () => {
289
+ const r = scrubVoice('Great catch! Fixed it.')
290
+ expect(r.scrubbed).toBe('Great catch! Fixed it.')
291
+ expect(r.openersStripped).toBe(0)
292
+ })
293
+
294
+ it('STILL normalizes em-dashes by default (punctuation, no content removed)', () => {
295
+ const r = scrubVoice('on it — checking the calendar')
296
+ expect(r.scrubbed).toBe('on it, checking the calendar')
297
+ expect(r.replaced).toBe(1)
298
+ expect(r.openersStripped).toBe(0)
299
+ })
300
+
301
+ it('an affirmation opener with an em-dash keeps the words, fixes only the dash', () => {
302
+ const r = scrubVoice('Exactly right — the token had expired.')
303
+ // Opener preserved; the em-dash after it becomes a comma.
304
+ expect(r.scrubbed).toBe('Exactly right, the token had expired.')
305
+ expect(r.openersStripped).toBe(0)
306
+ expect(r.replaced).toBe(1)
307
+ })
308
+ })
@@ -12,22 +12,27 @@
12
12
  * owns enforcement, soft instructions fail under load. Make the
13
13
  * framework do it.
14
14
  *
15
- * Scope. Two mechanical transforms, both semantically safe:
16
- * 1. Em / en dashes -> comma/period/hyphen. Pure transform with no
17
- * semantic loss on whitespace-separated prose; a no-op inside code
18
- * or a URL.
15
+ * Scope. By default ONE mechanical transform, the only one that removes
16
+ * no content:
17
+ * 1. Em / en dashes -> comma/period/hyphen. Pure punctuation
18
+ * substitution, no semantic loss on whitespace-separated prose; a
19
+ * no-op inside code or a URL. Kept deterministic because prompt-only
20
+ * guidance was measured to fail at it (em-dashes in 73% of replies
21
+ * despite the SOUL rule).
22
+ *
23
+ * OPT-IN, off by default:
19
24
  * 2. Leading sycophancy openers ("You're absolutely right", "Great
20
- * catch", "Exactly right") -> deleted, next word recapitalized. A
21
- * leading pure-affirmation clause carries near-zero meaning, so
22
- * removing it strips the AI-tell without touching the substance.
23
- * Conservative by construction: only at the very start, only the
24
- * known affirmation set, only when real content follows (a
25
- * standalone "You're absolutely right!" ack is left intact).
25
+ * catch") -> deleted + recapitalized. This one removes WORDS, and a
26
+ * context-free hook can strip a sincere "good catch, that was my
27
+ * bug" along with the hollow kind. Per operator steer (2026-06),
28
+ * tone is carried by the prompt VOICE directive (where the model has
29
+ * context to keep genuine acknowledgement and drop only the empty
30
+ * reflexive praise), not by blind deletion here. Re-enable the
31
+ * backstop with `SWITCHROOM_VOICE_STRIP_OPENERS=1`.
26
32
  *
27
- * Still scoped OUT: the wider mid-sentence "AI-tell phrase denylist"
33
+ * Always scoped OUT: the wider mid-sentence "AI-tell phrase denylist"
28
34
  * (smoking gun, delve, etc.). Substituting those mid-clause risks
29
- * semantic loss, so they stay with the prompt-side voice guidance
30
- * (the turn-pacing VOICE directive), not this mechanical gate.
35
+ * semantic loss, so they stay with the prompt-side voice guidance.
31
36
  *
32
37
  * Pipeline integration. Apply BEFORE markdownToHtml so the scrub
33
38
  * runs on the original model text, not on rendered HTML where
@@ -82,6 +87,25 @@ function enabled(): boolean {
82
87
  return !(v === '1' || v === 'true')
83
88
  }
84
89
 
90
+ /**
91
+ * Leading-affirmation stripping is OPT-IN and OFF by default.
92
+ *
93
+ * Rationale (operator steer, 2026-06): tone should be carried by the
94
+ * prompt (the VOICE directive), where the model has context to keep a
95
+ * genuine acknowledgement and drop only the hollow reflexive kind.
96
+ * Deleting an opener in a context-free hook is bad UX: it can strip a
97
+ * sincere "good catch, that was my bug" along with the empty praise.
98
+ * So the deterministic layer no longer removes WORDS by default; it
99
+ * only normalizes em/en dashes (a punctuation substitution that removes
100
+ * no content, and that prompt-only guidance was measured to fail at).
101
+ * Set `SWITCHROOM_VOICE_STRIP_OPENERS=1` to re-enable the deterministic
102
+ * opener strip as a backstop.
103
+ */
104
+ function openerStripEnabled(): boolean {
105
+ const v = process.env.SWITCHROOM_VOICE_STRIP_OPENERS
106
+ return v === '1' || v === 'true'
107
+ }
108
+
85
109
  /**
86
110
  * Leading sycophancy/affirmation openers. Matched ONLY at the very start
87
111
  * of the message, ONLY this known pure-filler set, and the trailing
@@ -250,7 +274,13 @@ export function scrubVoice(text: string): VoiceScrubResult {
250
274
  return { scrubbed: text, replaced: 0, openersStripped: 0 }
251
275
  }
252
276
  const { parked, parts } = park(text)
253
- const opener = stripLeadingAffirmation(parked)
277
+ // Opener strip is opt-in (default off) — see openerStripEnabled(). By
278
+ // default the deterministic layer removes no words; only em/en dashes
279
+ // are normalized below, and tone is left to the prompt's VOICE
280
+ // directive where the model can judge genuine vs hollow.
281
+ const opener = openerStripEnabled()
282
+ ? stripLeadingAffirmation(parked)
283
+ : { out: parked, count: 0 }
254
284
  const { out, replaced } = replaceDashes(opener.out)
255
285
  const total = replaced + opener.count
256
286
  if (total === 0) {
@@ -1,18 +1,20 @@
1
1
  /**
2
- * Voice-scrub fuzz — end-to-end proof of the deterministic voice gate.
2
+ * Voice-scrub fuzz — end-to-end check of the voice layers.
3
3
  *
4
- * The gateway's `scrubVoice` strips em/en dashes and leading sycophancy
5
- * openers ("You're absolutely right", "Great catch", ...) from every
6
- * outbound reply. This fuzz file drives REAL Telegram inbounds engineered
7
- * to bait those exact AI-tells (statements the agent will want to affirm;
8
- * prose asks where models reach for em-dashes) and asserts the observed
9
- * reply carries neither.
4
+ * Two layers with different strengths:
5
+ * - Em/en dashes are DETERMINISTIC: the gateway's `scrubVoice`
6
+ * normalizes every dash on every outbound reply, so "no em-dash
7
+ * reaches the user" is a hard, observable guarantee (asserted).
8
+ * - Sycophancy openers are PROBABILISTIC: the deterministic opener
9
+ * strip is off by default (operator steer 2026-06: context-free word
10
+ * deletion is bad UX), and tone is carried by the prompt VOICE
11
+ * directive where the model can keep genuine acknowledgement. So an
12
+ * opener is a soft signal (warn), not a gate failure.
10
13
  *
11
- * Why this is a good UAT target: unlike the grounding/voice PROMPT
12
- * guidance (soft, semantic, not cleanly observable), the scrub is a
13
- * deterministic transform on the wire, so mtcute's view of the sent
14
- * message is ground truth. If an em-dash or a leading affirmation reaches
15
- * the user, the gate failed.
14
+ * This fuzz file drives REAL Telegram inbounds engineered to bait both
15
+ * (statements the agent will want to affirm; prose asks where models
16
+ * reach for em-dashes). mtcute's view of the sent message is ground
17
+ * truth for the deterministic dash check.
16
18
  *
17
19
  * Self-skips green when the harness can't spin up (env unwired) — same as
18
20
  * the sibling fuzz files; uat/** is excluded from gating CI.
@@ -61,7 +63,7 @@ const LEADING_AFFIRMATION_RE =
61
63
  describe("uat: voice-scrub fuzz — no em-dashes, no sycophancy openers reach the user", () => {
62
64
  for (const vc of VOICE_CASES) {
63
65
  it(
64
- `[voice] ${vc.name} — reply is dash-free and affirmation-free`,
66
+ `[voice] ${vc.name} — reply is dash-free (affirmation now prompt-tier)`,
65
67
  async () => {
66
68
  const sc = await spinUp({ agent: "test-harness" });
67
69
  try {
@@ -87,10 +89,15 @@ describe("uat: voice-scrub fuzz — no em-dashes, no sycophancy openers reach th
87
89
  );
88
90
  }
89
91
 
90
- // Invariant 3: reply does not OPEN with a sycophancy affirmation.
92
+ // Invariant 3 (SOFT): reply ideally does not OPEN with a hollow
93
+ // affirmation. This is now PROBABILISTIC, not deterministic: the
94
+ // opener strip is off by default and tone is the prompt VOICE
95
+ // directive's job, so an occasional opener is a soft signal (the
96
+ // model judged it genuine), not a hard gate failure. Warn, don't
97
+ // fail — a hard assert here would flake on a prompt-driven lever.
91
98
  if (LEADING_AFFIRMATION_RE.test(text.trim())) {
92
- throw new Error(
93
- `[voice] ${vc.name}: reply opened with a stripped-class affirmation. `
99
+ console.warn(
100
+ `[voice] ${vc.name}: reply opened with an affirmation (prompt-tier, not enforced). `
94
101
  + `Reply: ${JSON.stringify(text.slice(0, 200))}`,
95
102
  );
96
103
  }