switchroom 0.13.18 → 0.13.19
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
|
@@ -47331,8 +47331,8 @@ var {
|
|
|
47331
47331
|
} = import__.default;
|
|
47332
47332
|
|
|
47333
47333
|
// src/build-info.ts
|
|
47334
|
-
var VERSION = "0.13.
|
|
47335
|
-
var COMMIT_SHA = "
|
|
47334
|
+
var VERSION = "0.13.19";
|
|
47335
|
+
var COMMIT_SHA = "de154395";
|
|
47336
47336
|
|
|
47337
47337
|
// src/cli/agent.ts
|
|
47338
47338
|
init_source();
|
package/package.json
CHANGED
|
@@ -37062,6 +37062,9 @@ function decideSilentReplyAnchor(input) {
|
|
|
37062
37062
|
if (!input.effectivelySilent) {
|
|
37063
37063
|
return { kind: "fresh", becomesAnchor: false };
|
|
37064
37064
|
}
|
|
37065
|
+
if (input.wasOverPingSuppressed === true) {
|
|
37066
|
+
return { kind: "fresh", becomesAnchor: false };
|
|
37067
|
+
}
|
|
37065
37068
|
if (input.hasFiles || input.hasButtons) {
|
|
37066
37069
|
return { kind: "fresh", becomesAnchor: false };
|
|
37067
37070
|
}
|
|
@@ -48204,10 +48207,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
|
|
|
48204
48207
|
}
|
|
48205
48208
|
|
|
48206
48209
|
// ../src/build-info.ts
|
|
48207
|
-
var VERSION = "0.13.
|
|
48208
|
-
var COMMIT_SHA = "
|
|
48209
|
-
var COMMIT_DATE = "2026-05-
|
|
48210
|
-
var LATEST_PR =
|
|
48210
|
+
var VERSION = "0.13.19";
|
|
48211
|
+
var COMMIT_SHA = "de154395";
|
|
48212
|
+
var COMMIT_DATE = "2026-05-23T07:08:03Z";
|
|
48213
|
+
var LATEST_PR = 1682;
|
|
48211
48214
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
48212
48215
|
|
|
48213
48216
|
// gateway/boot-version.ts
|
|
@@ -50668,6 +50671,7 @@ async function executeReply(args) {
|
|
|
50668
50671
|
const format = args.format ?? configParseMode;
|
|
50669
50672
|
const disableLinkPreview = args.disable_web_page_preview != null ? Boolean(args.disable_web_page_preview) : access.disableLinkPreview ?? true;
|
|
50670
50673
|
let disableNotification = args.disable_notification === true;
|
|
50674
|
+
let wasOverPingSuppressed = false;
|
|
50671
50675
|
{
|
|
50672
50676
|
const turn2 = currentTurn;
|
|
50673
50677
|
if (turn2 != null) {
|
|
@@ -50686,6 +50690,7 @@ async function executeReply(args) {
|
|
|
50686
50690
|
sinceFirstPingMs: decision.sinceFirstPingMs ?? 0
|
|
50687
50691
|
});
|
|
50688
50692
|
disableNotification = true;
|
|
50693
|
+
wasOverPingSuppressed = true;
|
|
50689
50694
|
} else if (decision.claimSlot) {
|
|
50690
50695
|
turn2.firstPingAt = now;
|
|
50691
50696
|
}
|
|
@@ -50794,7 +50799,8 @@ ${url}`;
|
|
|
50794
50799
|
anchorText: turn2.silentAnchorText,
|
|
50795
50800
|
newReplyText: effectiveText,
|
|
50796
50801
|
hasFiles: files.length > 0,
|
|
50797
|
-
hasButtons: replyMarkup != null
|
|
50802
|
+
hasButtons: replyMarkup != null,
|
|
50803
|
+
wasOverPingSuppressed
|
|
50798
50804
|
});
|
|
50799
50805
|
if (decision.kind === "edit-anchor") {
|
|
50800
50806
|
const editParams = {
|
|
@@ -4252,6 +4252,12 @@ async function executeReply(args: Record<string, unknown>): Promise<{ content: A
|
|
|
4252
4252
|
// and subsequent pings would be silenced. Acceptable trade-off (a
|
|
4253
4253
|
// failed first ping is an edge case; the alternative — claim after
|
|
4254
4254
|
// send — races concurrent reply calls).
|
|
4255
|
+
// Tracks whether the over-ping safety net coerced this reply
|
|
4256
|
+
// from ping→silent. Threaded into the silent-anchor predicate
|
|
4257
|
+
// below: a demoted final-answer reply must NOT merge into the
|
|
4258
|
+
// silent preamble bubble; it lands as a fresh silent bubble so
|
|
4259
|
+
// the user can still find it (see #1674 / silent-anchor follow-up).
|
|
4260
|
+
let wasOverPingSuppressed = false
|
|
4255
4261
|
{
|
|
4256
4262
|
const turn = currentTurn
|
|
4257
4263
|
if (turn != null) {
|
|
@@ -4278,6 +4284,7 @@ async function executeReply(args: Record<string, unknown>): Promise<{ content: A
|
|
|
4278
4284
|
sinceFirstPingMs: decision.sinceFirstPingMs ?? 0,
|
|
4279
4285
|
})
|
|
4280
4286
|
disableNotification = true
|
|
4287
|
+
wasOverPingSuppressed = true
|
|
4281
4288
|
} else if (decision.claimSlot) {
|
|
4282
4289
|
turn.firstPingAt = now
|
|
4283
4290
|
}
|
|
@@ -4445,6 +4452,7 @@ async function executeReply(args: Record<string, unknown>): Promise<{ content: A
|
|
|
4445
4452
|
newReplyText: effectiveText,
|
|
4446
4453
|
hasFiles: files.length > 0,
|
|
4447
4454
|
hasButtons: replyMarkup != null,
|
|
4455
|
+
wasOverPingSuppressed,
|
|
4448
4456
|
})
|
|
4449
4457
|
if (decision.kind === 'edit-anchor') {
|
|
4450
4458
|
const editParams: {
|
|
@@ -65,6 +65,15 @@ export interface SilentReplyAnchorDecisionInput {
|
|
|
65
65
|
* are too easy to get wrong, and the markup is rare enough
|
|
66
66
|
* that fresh-send is the safer default. */
|
|
67
67
|
hasButtons: boolean
|
|
68
|
+
/** True iff this reply was an intended-ping (model requested
|
|
69
|
+
* `disable_notification:false`) that the over-ping safety net
|
|
70
|
+
* (#1674) coerced to silent. Anchor merge MUST bypass when true:
|
|
71
|
+
* semantically the model intended this as a distinct/final
|
|
72
|
+
* delivery, and merging it into the existing silent preamble
|
|
73
|
+
* would bury the content (the user already stopped looking at
|
|
74
|
+
* the anchor bubble because earlier ticks edited it silently).
|
|
75
|
+
* Optional — defaults to false for non-gateway callers. */
|
|
76
|
+
wasOverPingSuppressed?: boolean
|
|
68
77
|
}
|
|
69
78
|
|
|
70
79
|
/**
|
|
@@ -108,6 +117,19 @@ export function decideSilentReplyAnchor(
|
|
|
108
117
|
return { kind: 'fresh', becomesAnchor: false }
|
|
109
118
|
}
|
|
110
119
|
|
|
120
|
+
// Over-ping-suppressed replies bypass the anchor. The model
|
|
121
|
+
// intended a ping (almost always: a final/distinct reply); the
|
|
122
|
+
// safety net demoted to silent so the user isn't double-beeped.
|
|
123
|
+
// Merging the demoted reply into the existing silent anchor
|
|
124
|
+
// hides it — the user has already disengaged from the bubble
|
|
125
|
+
// that's been edited silently for the rest of the turn. Land
|
|
126
|
+
// as a fresh silent bubble instead, preserving discoverability.
|
|
127
|
+
// Don't capture as next anchor either: this reply is the
|
|
128
|
+
// *answer*, not more preamble.
|
|
129
|
+
if (input.wasOverPingSuppressed === true) {
|
|
130
|
+
return { kind: 'fresh', becomesAnchor: false }
|
|
131
|
+
}
|
|
132
|
+
|
|
111
133
|
// Files / buttons bypass the anchor — edit-text can't merge
|
|
112
134
|
// media, and keyboards across edits are a foot-gun.
|
|
113
135
|
if (input.hasFiles || input.hasButtons) {
|
|
@@ -175,4 +175,73 @@ describe('decideSilentReplyAnchor — silent replies edit a single growing ancho
|
|
|
175
175
|
expect(d.mergedText.length).toBe(TELEGRAM_MSG_CAP)
|
|
176
176
|
}
|
|
177
177
|
})
|
|
178
|
+
|
|
179
|
+
// Follow-up to #1679 — when the over-ping safety net coerces a
|
|
180
|
+
// model-intended ping to silent, the demoted reply must NOT be
|
|
181
|
+
// merged into the existing silent anchor. The anchor has been
|
|
182
|
+
// edited silently for the whole turn; the user has long since
|
|
183
|
+
// disengaged. Merging the (semantically final) demoted reply
|
|
184
|
+
// there would hide the answer entirely.
|
|
185
|
+
describe('over-ping-suppressed messages bypass anchor merge', () => {
|
|
186
|
+
it('demoted reply with an active anchor lands as a fresh silent (not edit, not next-anchor)', () => {
|
|
187
|
+
const d = decideSilentReplyAnchor({
|
|
188
|
+
effectivelySilent: true,
|
|
189
|
+
anchorMessageId: 12345,
|
|
190
|
+
anchorText: 'on it — gathering facts',
|
|
191
|
+
newReplyText: 'Delivered all three steps with a wrap-up summary.',
|
|
192
|
+
hasFiles: false,
|
|
193
|
+
hasButtons: false,
|
|
194
|
+
wasOverPingSuppressed: true,
|
|
195
|
+
})
|
|
196
|
+
expect(d).toEqual({ kind: 'fresh', becomesAnchor: false })
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('demoted reply with no anchor yet also fresh-sends without capturing the anchor', () => {
|
|
200
|
+
// The model fired a stray ping before any silent ack; the
|
|
201
|
+
// safety-net demoted that ping. A demoted message is never
|
|
202
|
+
// anchor material — it's an answer, not preamble.
|
|
203
|
+
const d = decideSilentReplyAnchor({
|
|
204
|
+
effectivelySilent: true,
|
|
205
|
+
anchorMessageId: null,
|
|
206
|
+
anchorText: '',
|
|
207
|
+
newReplyText: 'Delivered all three steps with a wrap-up summary.',
|
|
208
|
+
hasFiles: false,
|
|
209
|
+
hasButtons: false,
|
|
210
|
+
wasOverPingSuppressed: true,
|
|
211
|
+
})
|
|
212
|
+
expect(d).toEqual({ kind: 'fresh', becomesAnchor: false })
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('genuinely silent reply (not over-ping-suppressed) still merges normally', () => {
|
|
216
|
+
// Regression guard: the new bypass must not over-fire on
|
|
217
|
+
// legitimate beat-3 silent ticks.
|
|
218
|
+
const d = decideSilentReplyAnchor({
|
|
219
|
+
effectivelySilent: true,
|
|
220
|
+
anchorMessageId: 12345,
|
|
221
|
+
anchorText: 'on it — gathering facts',
|
|
222
|
+
newReplyText: 'Step 1: hostname is example-host',
|
|
223
|
+
hasFiles: false,
|
|
224
|
+
hasButtons: false,
|
|
225
|
+
wasOverPingSuppressed: false,
|
|
226
|
+
})
|
|
227
|
+
expect(d).toEqual({
|
|
228
|
+
kind: 'edit-anchor',
|
|
229
|
+
messageId: 12345,
|
|
230
|
+
mergedText:
|
|
231
|
+
'on it — gathering facts\n\nStep 1: hostname is example-host',
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('omitting wasOverPingSuppressed defaults to false (backward compat)', () => {
|
|
236
|
+
const d = decideSilentReplyAnchor({
|
|
237
|
+
effectivelySilent: true,
|
|
238
|
+
anchorMessageId: 12345,
|
|
239
|
+
anchorText: 'on it',
|
|
240
|
+
newReplyText: 'next thought',
|
|
241
|
+
hasFiles: false,
|
|
242
|
+
hasButtons: false,
|
|
243
|
+
})
|
|
244
|
+
expect(d.kind).toBe('edit-anchor')
|
|
245
|
+
})
|
|
246
|
+
})
|
|
178
247
|
})
|