switchroom 0.15.45 → 0.16.5
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/agent-scheduler/index.js +56 -15
- package/dist/auth-broker/index.js +383 -97
- package/dist/cli/autoaccept-poll.js +4842 -35
- package/dist/cli/drive-write-pretool.mjs +7 -4
- package/dist/cli/notion-write-pretool.mjs +35 -4
- package/dist/cli/self-improve-apply-guard-pretool.mjs +626 -0
- package/dist/cli/self-improve-stop.mjs +428 -0
- package/dist/cli/switchroom.js +2894 -841
- package/dist/host-control/main.js +2685 -207
- package/dist/vault/approvals/kernel-server.js +7453 -7413
- package/dist/vault/broker/server.js +11428 -11388
- package/examples/minimal.yaml +1 -0
- package/examples/switchroom.yaml +1 -0
- package/package.json +3 -3
- package/profiles/_base/start.sh.hbs +97 -1
- package/profiles/_shared/execution-discipline.md.hbs +18 -0
- package/profiles/default/CLAUDE.md.hbs +0 -19
- package/telegram-plugin/.claude-plugin/plugin.json +2 -2
- package/telegram-plugin/answer-stream-flag.ts +12 -49
- package/telegram-plugin/answer-stream.ts +5 -150
- package/telegram-plugin/auth-snapshot-format.ts +280 -48
- package/telegram-plugin/auto-fallback-fleet.ts +44 -1
- package/telegram-plugin/context-exhaustion.ts +12 -0
- package/telegram-plugin/demo-mask.ts +154 -0
- package/telegram-plugin/dist/bridge/bridge.js +55 -12
- package/telegram-plugin/dist/gateway/gateway.js +2938 -977
- package/telegram-plugin/dist/server.js +55 -12
- package/telegram-plugin/docs/waiting-ux-spec.md +2 -2
- package/telegram-plugin/draft-stream.ts +47 -410
- package/telegram-plugin/final-answer-detect.ts +17 -12
- package/telegram-plugin/fleet-fallback-resume.ts +131 -0
- package/telegram-plugin/format.ts +56 -19
- package/telegram-plugin/gateway/auth-add-flow.ts +332 -127
- package/telegram-plugin/gateway/auth-broker-client.ts +2 -2
- package/telegram-plugin/gateway/auth-command.ts +70 -14
- package/telegram-plugin/gateway/clean-shutdown-marker.ts +44 -0
- package/telegram-plugin/gateway/config-approval-handler.test.ts +91 -4
- package/telegram-plugin/gateway/config-approval-handler.ts +94 -13
- package/telegram-plugin/gateway/current-turn-map.ts +188 -0
- package/telegram-plugin/gateway/disconnect-flush.ts +3 -1
- package/telegram-plugin/gateway/effort-command.ts +8 -3
- package/telegram-plugin/gateway/emission-authority.ts +369 -0
- package/telegram-plugin/gateway/feed-open-gate.ts +292 -0
- package/telegram-plugin/gateway/gateway.ts +1857 -292
- package/telegram-plugin/gateway/inject-handler.test.ts +2 -1
- package/telegram-plugin/gateway/model-command.ts +115 -4
- package/telegram-plugin/gateway/ms365-write-approval.test.ts +4 -4
- package/telegram-plugin/gateway/represent-guard.ts +72 -0
- package/telegram-plugin/gateway/status-surface-log.test.ts +5 -4
- package/telegram-plugin/gateway/status-surface-log.ts +14 -3
- package/telegram-plugin/history.ts +33 -11
- package/telegram-plugin/hooks/repo-context-pretool.mjs +26 -0
- package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +5 -0
- package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +8 -0
- package/telegram-plugin/hooks/tool-label-pretool.mjs +39 -15
- package/telegram-plugin/issues-card.ts +4 -0
- package/telegram-plugin/model-unavailable.ts +124 -0
- package/telegram-plugin/narrative-dedup.ts +69 -0
- package/telegram-plugin/over-ping-safety-net.ts +70 -4
- package/telegram-plugin/package.json +3 -3
- package/telegram-plugin/pending-work-progress.ts +12 -0
- package/telegram-plugin/permission-rule.ts +32 -5
- package/telegram-plugin/permission-title.ts +152 -9
- package/telegram-plugin/quota-check.ts +13 -0
- package/telegram-plugin/quota-watch.ts +135 -7
- package/telegram-plugin/registry/turns-schema.test.ts +24 -0
- package/telegram-plugin/registry/turns-schema.ts +9 -0
- package/telegram-plugin/runtime-metrics.ts +13 -0
- package/telegram-plugin/session-tail.ts +96 -11
- package/telegram-plugin/silence-poke.ts +170 -24
- package/telegram-plugin/slot-banner-driver.ts +3 -0
- package/telegram-plugin/status-no-truncate.ts +44 -0
- package/telegram-plugin/status-reactions.ts +20 -3
- package/telegram-plugin/stream-controller.ts +4 -23
- package/telegram-plugin/stream-reply-handler.ts +6 -24
- package/telegram-plugin/streaming-metrics.ts +91 -0
- package/telegram-plugin/subagent-watcher.ts +212 -66
- package/telegram-plugin/tests/activity-ever-opened-sticky.test.ts +47 -0
- package/telegram-plugin/tests/answer-stream-dedup.test.ts +9 -26
- package/telegram-plugin/tests/answer-stream-flag.test.ts +25 -58
- package/telegram-plugin/tests/answer-stream-silent-markers.test.ts +41 -51
- package/telegram-plugin/tests/answer-stream.test.ts +2 -411
- package/telegram-plugin/tests/auth-add-flow.test.ts +488 -253
- package/telegram-plugin/tests/auth-command-format2.test.ts +71 -1
- package/telegram-plugin/tests/auth-snapshot-format.test.ts +376 -6
- package/telegram-plugin/tests/auto-fallback-fleet.test.ts +120 -0
- package/telegram-plugin/tests/cross-turn-card-gate.test.ts +424 -0
- package/telegram-plugin/tests/demo-mask.test.ts +127 -0
- package/telegram-plugin/tests/draft-stream.test.ts +0 -827
- package/telegram-plugin/tests/emission-authority-card-drain-gate.test.ts +236 -0
- package/telegram-plugin/tests/emission-authority-facade.test.ts +488 -0
- package/telegram-plugin/tests/emission-authority-open-gate.test.ts +179 -0
- package/telegram-plugin/tests/emission-authority-ping-gate.test.ts +395 -0
- package/telegram-plugin/tests/emission-determinism-wiring.test.ts +177 -0
- package/telegram-plugin/tests/feed-heartbeat-liveness-open.test.ts +146 -0
- package/telegram-plugin/tests/feed-open-gate.test.ts +259 -0
- package/telegram-plugin/tests/feed-survival.test.ts +526 -0
- package/telegram-plugin/tests/fleet-fallback-resume.test.ts +197 -0
- package/telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts +117 -0
- package/telegram-plugin/tests/gateway-no-reply-single-emit.test.ts +4 -11
- package/telegram-plugin/tests/history.test.ts +60 -0
- package/telegram-plugin/tests/model-command.test.ts +134 -0
- package/telegram-plugin/tests/model-unavailable.test.ts +118 -0
- package/telegram-plugin/tests/narrative-dedup.test.ts +118 -0
- package/telegram-plugin/tests/orphaned-reply-rearm.test.ts +285 -0
- package/telegram-plugin/tests/over-ping-final-answer-decoupling.test.ts +194 -0
- package/telegram-plugin/tests/over-ping-safety-net.test.ts +2 -2
- package/telegram-plugin/tests/per-topic-current-turn.test.ts +373 -0
- package/telegram-plugin/tests/permission-card-origin-kill-switch.test.ts +42 -0
- package/telegram-plugin/tests/permission-rule.test.ts +17 -0
- package/telegram-plugin/tests/permission-title.test.ts +206 -17
- package/telegram-plugin/tests/quota-watch.test.ts +252 -9
- package/telegram-plugin/tests/reply-terminal-reaction.test.ts +6 -1
- package/telegram-plugin/tests/repo-context-pretool.test.ts +62 -0
- package/telegram-plugin/tests/represent-guard.test.ts +162 -0
- package/telegram-plugin/tests/session-tail.test.ts +147 -3
- package/telegram-plugin/tests/silence-liveness-wiring.test.ts +18 -0
- package/telegram-plugin/tests/status-card-budget-parity.test.ts +72 -0
- package/telegram-plugin/tests/status-surface-log.test.ts +146 -0
- package/telegram-plugin/tests/subagent-watcher-clip-narrative.test.ts +58 -0
- package/telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts +102 -0
- package/telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts +225 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +147 -0
- package/telegram-plugin/tests/telegram-activity-visibility-integration.test.ts +597 -0
- package/telegram-plugin/tests/telegram-format.test.ts +101 -6
- package/telegram-plugin/tests/tool-activity-summary.test.ts +550 -15
- package/telegram-plugin/tests/tool-label-pretool.test.ts +73 -0
- package/telegram-plugin/tests/tool-label-sidecar.test.ts +44 -0
- package/telegram-plugin/tests/tool-labels.test.ts +67 -0
- package/telegram-plugin/tests/turn-liveness-floor.test.ts +196 -0
- package/telegram-plugin/tests/turn-liveness-invariant.test.ts +340 -0
- package/telegram-plugin/tests/welcome-text.test.ts +32 -3
- package/telegram-plugin/tests/worker-activity-feed.test.ts +470 -22
- package/telegram-plugin/tool-activity-summary.ts +375 -58
- package/telegram-plugin/turn-liveness-floor.ts +240 -0
- package/telegram-plugin/uat/assertions.ts +115 -0
- package/telegram-plugin/uat/driver.ts +68 -0
- package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +119 -133
- package/telegram-plugin/uat/scenarios/jtbd-answer-pings.test.ts +94 -0
- package/telegram-plugin/uat/scenarios/jtbd-cross-turn-card-dm.test.ts +109 -0
- package/telegram-plugin/uat/scenarios/jtbd-foreground-feed-thinkgap-dm.test.ts +478 -0
- package/telegram-plugin/uat/scenarios/jtbd-foreground-feed-visibility-dm.test.ts +396 -0
- package/telegram-plugin/uat/scenarios/jtbd-liveness-feed-open-dm.test.ts +202 -0
- package/telegram-plugin/uat/scenarios/jtbd-reply-is-last-dm.test.ts +202 -0
- package/telegram-plugin/uat/scenarios/reactions-dm.test.ts +93 -87
- package/telegram-plugin/welcome-text.ts +13 -1
- package/telegram-plugin/worker-activity-feed.ts +157 -82
- package/telegram-plugin/draft-transport.ts +0 -122
- package/telegram-plugin/tests/draft-retirement-wiring.test.ts +0 -82
- package/telegram-plugin/tests/draft-transport.test.ts +0 -211
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
* opt-in only on a truthy value. Guards against an accidental flip back to
|
|
4
4
|
* default-on (which would reintroduce the unformatted-preliminary flash +
|
|
5
5
|
* delete-on-every-reply — see the gateway gate comment).
|
|
6
|
+
*
|
|
7
|
+
* The draft transport (sendMessageDraft) is permanently retired — the lane is
|
|
8
|
+
* now either VISIBLE (opt-in) or DORMANT (the unconditional default). The
|
|
9
|
+
* resolveAnswerLaneConfig 2-state enumeration is the regression guard.
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
12
|
import { describe, it, expect } from 'vitest'
|
|
9
|
-
import { parseVisibleAnswerStreamEnabled,
|
|
13
|
+
import { parseVisibleAnswerStreamEnabled, resolveAnswerLaneConfig } from '../answer-stream-flag.js'
|
|
10
14
|
|
|
11
15
|
describe('parseVisibleAnswerStreamEnabled — default OFF, opt-in', () => {
|
|
12
16
|
it('defaults OFF when unset', () => {
|
|
@@ -26,47 +30,24 @@ describe('parseVisibleAnswerStreamEnabled — default OFF, opt-in', () => {
|
|
|
26
30
|
})
|
|
27
31
|
})
|
|
28
32
|
|
|
29
|
-
describe('parseDraftLaneRetiredEnabled — default RETIRED (2026-06-05), kill-switch off', () => {
|
|
30
|
-
it('defaults to RETIRED (true) when unset — the draft lane is gone by default', () => {
|
|
31
|
-
expect(parseDraftLaneRetiredEnabled(undefined)).toBe(true)
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it('stays RETIRED for any non-disable value (including unrecognized)', () => {
|
|
35
|
-
for (const v of ['1', 'true', 'on', 'yes', '', ' ', 'whatever', 'retired']) {
|
|
36
|
-
expect(parseDraftLaneRetiredEnabled(v)).toBe(true)
|
|
37
|
-
}
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it('restores the legacy draft (false) ONLY on an explicit disable (case/space-insensitive)', () => {
|
|
41
|
-
for (const v of ['0', 'false', 'off', 'no', ' FALSE ', 'Off', 'NO']) {
|
|
42
|
-
expect(parseDraftLaneRetiredEnabled(v)).toBe(false)
|
|
43
|
-
}
|
|
44
|
-
})
|
|
45
|
-
})
|
|
46
|
-
|
|
47
33
|
// ── resolveAnswerLaneConfig — TOTAL-ENUMERATION REGRESSION PROOF ─────────────
|
|
48
34
|
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
// load-bearing INVARIANT: opensVisiblePreview === visibleEnabled, ALWAYS. That
|
|
54
|
-
// invariant is exactly what v0.14.68 broke (it made the preview depend on the
|
|
55
|
-
// draft flag), so a future change that re-conflates them fails here, not in prod.
|
|
35
|
+
// The draft transport is permanently retired. The input space is now a single
|
|
36
|
+
// boolean (visibleEnabled), yielding exactly 2 states: visible or dormant.
|
|
37
|
+
// We enumerate ALL of it and assert the full decision table plus the load-bearing
|
|
38
|
+
// INVARIANT: opensVisiblePreview === visibleEnabled, ALWAYS.
|
|
56
39
|
describe('resolveAnswerLaneConfig — total enumeration (flash-regression proof)', () => {
|
|
57
40
|
const MAX = Number.MAX_SAFE_INTEGER
|
|
58
41
|
const ALL = [
|
|
59
|
-
{ visibleEnabled: false
|
|
60
|
-
{ visibleEnabled:
|
|
61
|
-
{ visibleEnabled: true, draftFnAvailable: false }, // opt-in visible
|
|
62
|
-
{ visibleEnabled: true, draftFnAvailable: true }, // visible wins over draft
|
|
42
|
+
{ visibleEnabled: false }, // the DEFAULT (visible off, draft permanently retired → dormant)
|
|
43
|
+
{ visibleEnabled: true }, // opt-in visible
|
|
63
44
|
]
|
|
64
45
|
|
|
65
|
-
it('the input space is exactly
|
|
66
|
-
expect(ALL.length).toBe(
|
|
46
|
+
it('the input space is exactly 2 rows', () => {
|
|
47
|
+
expect(ALL.length).toBe(2)
|
|
67
48
|
})
|
|
68
49
|
|
|
69
|
-
it('INVARIANT (the regression guard): opensVisiblePreview === visibleEnabled for EVERY
|
|
50
|
+
it('INVARIANT (the regression guard): opensVisiblePreview === visibleEnabled for EVERY input', () => {
|
|
70
51
|
for (const input of ALL) {
|
|
71
52
|
expect(resolveAnswerLaneConfig(input).opensVisiblePreview).toBe(input.visibleEnabled)
|
|
72
53
|
}
|
|
@@ -79,39 +60,25 @@ describe('resolveAnswerLaneConfig — total enumeration (flash-regression proof)
|
|
|
79
60
|
}
|
|
80
61
|
})
|
|
81
62
|
|
|
82
|
-
it('DEFAULT (visible off
|
|
83
|
-
expect(resolveAnswerLaneConfig({ visibleEnabled: false
|
|
63
|
+
it('DEFAULT (visible off) → DORMANT: no preview, MAX gate (no flash)', () => {
|
|
64
|
+
expect(resolveAnswerLaneConfig({ visibleEnabled: false })).toEqual({
|
|
84
65
|
minInitialChars: MAX,
|
|
85
|
-
usesDraftTransport: false,
|
|
86
66
|
opensVisiblePreview: false,
|
|
87
67
|
state: 'dormant',
|
|
88
68
|
})
|
|
89
69
|
})
|
|
90
70
|
|
|
91
|
-
it('visible
|
|
92
|
-
expect(resolveAnswerLaneConfig({ visibleEnabled:
|
|
93
|
-
minInitialChars:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
state: 'draft',
|
|
71
|
+
it('visible on → VISIBLE: preview opens on the first chunk (minChars 1)', () => {
|
|
72
|
+
expect(resolveAnswerLaneConfig({ visibleEnabled: true })).toEqual({
|
|
73
|
+
minInitialChars: 1,
|
|
74
|
+
opensVisiblePreview: true,
|
|
75
|
+
state: 'visible',
|
|
97
76
|
})
|
|
98
77
|
})
|
|
99
78
|
|
|
100
|
-
it('visible
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
usesDraftTransport: false,
|
|
105
|
-
opensVisiblePreview: true,
|
|
106
|
-
state: 'visible',
|
|
107
|
-
})
|
|
108
|
-
}
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
it('a visible preview NEVER opens unless explicitly enabled (no draftFnAvailable forces it on)', () => {
|
|
112
|
-
// The exact v0.14.68 failure shape: retiring the draft (draftFnAvailable=false)
|
|
113
|
-
// must NOT open a visible preview.
|
|
114
|
-
expect(resolveAnswerLaneConfig({ visibleEnabled: false, draftFnAvailable: false }).opensVisiblePreview).toBe(false)
|
|
115
|
-
expect(resolveAnswerLaneConfig({ visibleEnabled: false, draftFnAvailable: false }).minInitialChars).toBe(MAX)
|
|
79
|
+
it('a visible preview NEVER opens unless explicitly enabled', () => {
|
|
80
|
+
// The exact v0.14.68 failure shape: retiring the draft must NOT open a preview.
|
|
81
|
+
expect(resolveAnswerLaneConfig({ visibleEnabled: false }).opensVisiblePreview).toBe(false)
|
|
82
|
+
expect(resolveAnswerLaneConfig({ visibleEnabled: false }).minInitialChars).toBe(MAX)
|
|
116
83
|
})
|
|
117
84
|
})
|
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
import { describe, expect, it, vi, beforeEach
|
|
1
|
+
import { describe, expect, it, vi, beforeEach } from 'vitest'
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
createAnswerStream,
|
|
5
|
-
__resetDraftIdForTests,
|
|
6
5
|
} from '../answer-stream.js'
|
|
7
6
|
|
|
7
|
+
// Throttle window anchored one hour ahead of "now". With lastSentAt=0 and a
|
|
8
|
+
// real Date.now(), update() computes sinceLast = Date.now() - 0, which is
|
|
9
|
+
// always < this anchor, so update() schedules a (cancelled-before-it-fires)
|
|
10
|
+
// timer and buffers pendingText instead of sending immediately. materialize()
|
|
11
|
+
// then cancels the scheduled timer and applies the silent-marker guard to the
|
|
12
|
+
// buffered text. This replaces the old draft-transport path (which bypassed the
|
|
13
|
+
// length gate). Anchoring to now+1h (rather than MAX_SAFE_INTEGER) keeps the
|
|
14
|
+
// scheduled timer's delay within 32-bit range, so it stays runner-agnostic:
|
|
15
|
+
// no vi.setSystemTime (which bun's vitest shim lacks) and no Node
|
|
16
|
+
// TimeoutOverflowWarning under runners whose fake-timer shim is a no-op.
|
|
17
|
+
const HOUR_MS = 60 * 60 * 1000
|
|
18
|
+
let throttleAnchorMs = 0
|
|
19
|
+
|
|
8
20
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
9
21
|
|
|
10
22
|
type SendMessageFn = (
|
|
@@ -29,13 +41,6 @@ type EditMessageTextFn = (
|
|
|
29
41
|
},
|
|
30
42
|
) => Promise<unknown>
|
|
31
43
|
|
|
32
|
-
type SendMessageDraftFn = (
|
|
33
|
-
chatId: string,
|
|
34
|
-
draftId: number,
|
|
35
|
-
text: string,
|
|
36
|
-
params?: { message_thread_id?: number },
|
|
37
|
-
) => Promise<unknown>
|
|
38
|
-
|
|
39
44
|
let nextMessageId = 9000
|
|
40
45
|
|
|
41
46
|
function makeSendMessage(): ReturnType<typeof vi.fn> & SendMessageFn {
|
|
@@ -49,18 +54,9 @@ function makeEditMessageText(): ReturnType<typeof vi.fn> & EditMessageTextFn {
|
|
|
49
54
|
return vi.fn(async () => {}) as unknown as ReturnType<typeof vi.fn> & EditMessageTextFn
|
|
50
55
|
}
|
|
51
56
|
|
|
52
|
-
function makeSendMessageDraft(): ReturnType<typeof vi.fn> & SendMessageDraftFn {
|
|
53
|
-
return vi.fn(async () => {}) as unknown as ReturnType<typeof vi.fn> & SendMessageDraftFn
|
|
54
|
-
}
|
|
55
|
-
|
|
56
57
|
beforeEach(() => {
|
|
57
|
-
__resetDraftIdForTests()
|
|
58
58
|
nextMessageId = 9000
|
|
59
|
-
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
afterEach(() => {
|
|
63
|
-
vi.useRealTimers()
|
|
59
|
+
throttleAnchorMs = Date.now() + HOUR_MS
|
|
64
60
|
})
|
|
65
61
|
|
|
66
62
|
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
@@ -70,9 +66,16 @@ afterEach(() => {
|
|
|
70
66
|
* NO_REPLY / HEARTBEAT_OK silent markers and suppress outbound Telegram
|
|
71
67
|
* messages when the whole turn body is one of those tokens.
|
|
72
68
|
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
69
|
+
* Previously in DMs the draft transport bypassed the minInitialChars length
|
|
70
|
+
* gate in update(), so short markers like "NO_REPLY" (8 chars) reached
|
|
71
|
+
* pendingText; then materialize() would send them as real messages.
|
|
72
|
+
*
|
|
73
|
+
* The draft transport is permanently retired. These tests now use:
|
|
74
|
+
* - minInitialChars: 0 — bypasses the length gate so update() sets pendingText
|
|
75
|
+
* - throttleMs: throttleAnchorMs (now + 1h) — with lastSentAt=0 and a real
|
|
76
|
+
* Date.now(), sinceLast is always < the throttle window, so update() schedules
|
|
77
|
+
* a timer rather than firing sendMessage immediately. materialize() cancels that
|
|
78
|
+
* timer and applies the silent-marker guard to pendingText before any send.
|
|
76
79
|
*
|
|
77
80
|
* Mirrors the sentinel suppression already present in:
|
|
78
81
|
* - server.ts (reply/stream_reply MCP tool handlers)
|
|
@@ -81,26 +84,23 @@ afterEach(() => {
|
|
|
81
84
|
*/
|
|
82
85
|
describe('answer-stream — silent-marker suppression at materialize()', () => {
|
|
83
86
|
it('NO_REPLY as the sole chunk — no outbound message, suppression log line emitted', async () => {
|
|
84
|
-
// Use isPrivateChat: true + sendMessageDraft to replicate the exact repro
|
|
85
|
-
// conditions from #299: DM chat bypasses the minInitialChars length gate
|
|
86
|
-
// in update(), so "NO_REPLY" (8 chars) reaches pendingText and materialize()
|
|
87
|
-
// would previously send it as a real Telegram message.
|
|
88
87
|
const sendMessage = makeSendMessage()
|
|
89
88
|
const editMessageText = makeEditMessageText()
|
|
90
|
-
const sendMessageDraft = makeSendMessageDraft()
|
|
91
89
|
const logs: string[] = []
|
|
92
90
|
const stream = createAnswerStream({
|
|
93
91
|
chatId: 'chat42',
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
// minInitialChars: 0 bypasses the length gate so short markers like
|
|
93
|
+
// "NO_REPLY" (8 chars) set pendingText. Combined with vi.setSystemTime(0),
|
|
94
|
+
// the throttle keeps update() from firing sendMessage immediately, so
|
|
95
|
+
// materialize() can apply the silent-marker guard.
|
|
96
|
+
minInitialChars: 0,
|
|
97
|
+
throttleMs: throttleAnchorMs,
|
|
96
98
|
sendMessage,
|
|
97
99
|
editMessageText,
|
|
98
|
-
sendMessageDraft,
|
|
99
100
|
log: (msg) => logs.push(msg),
|
|
100
101
|
})
|
|
101
102
|
|
|
102
103
|
// Simulate: model emits exactly NO_REPLY, no reply/stream_reply call.
|
|
103
|
-
// In a DM, update() bypasses the length gate and sets pendingText.
|
|
104
104
|
stream.update('NO_REPLY')
|
|
105
105
|
// materialize() is what gateway.ts calls at turn_end when no tool reply
|
|
106
106
|
// was made — this is the path that was broken in #299 (msg id=8268).
|
|
@@ -115,14 +115,12 @@ describe('answer-stream — silent-marker suppression at materialize()', () => {
|
|
|
115
115
|
it('HEARTBEAT_OK as the sole chunk — no outbound message', async () => {
|
|
116
116
|
const sendMessage = makeSendMessage()
|
|
117
117
|
const editMessageText = makeEditMessageText()
|
|
118
|
-
const sendMessageDraft = makeSendMessageDraft()
|
|
119
118
|
const stream = createAnswerStream({
|
|
120
119
|
chatId: 'chat43',
|
|
121
|
-
|
|
122
|
-
throttleMs:
|
|
120
|
+
minInitialChars: 0,
|
|
121
|
+
throttleMs: throttleAnchorMs,
|
|
123
122
|
sendMessage,
|
|
124
123
|
editMessageText,
|
|
125
|
-
sendMessageDraft,
|
|
126
124
|
})
|
|
127
125
|
|
|
128
126
|
stream.update('HEARTBEAT_OK')
|
|
@@ -135,15 +133,13 @@ describe('answer-stream — silent-marker suppression at materialize()', () => {
|
|
|
135
133
|
it('NO_REPLY. (trailing period) — suppressed by trailing-punctuation tolerance', async () => {
|
|
136
134
|
const sendMessage = makeSendMessage()
|
|
137
135
|
const editMessageText = makeEditMessageText()
|
|
138
|
-
const sendMessageDraft = makeSendMessageDraft()
|
|
139
136
|
const logs: string[] = []
|
|
140
137
|
const stream = createAnswerStream({
|
|
141
138
|
chatId: 'chat44',
|
|
142
|
-
|
|
143
|
-
throttleMs:
|
|
139
|
+
minInitialChars: 0,
|
|
140
|
+
throttleMs: throttleAnchorMs,
|
|
144
141
|
sendMessage,
|
|
145
142
|
editMessageText,
|
|
146
|
-
sendMessageDraft,
|
|
147
143
|
log: (msg) => logs.push(msg),
|
|
148
144
|
})
|
|
149
145
|
|
|
@@ -158,22 +154,19 @@ describe('answer-stream — silent-marker suppression at materialize()', () => {
|
|
|
158
154
|
it('substring match ("the agent suggested NO_REPLY earlier") — NOT suppressed, materialises normally', async () => {
|
|
159
155
|
const sendMessage = makeSendMessage()
|
|
160
156
|
const editMessageText = makeEditMessageText()
|
|
161
|
-
const sendMessageDraft = makeSendMessageDraft()
|
|
162
157
|
const stream = createAnswerStream({
|
|
163
158
|
chatId: 'chat45',
|
|
164
|
-
|
|
165
|
-
throttleMs:
|
|
159
|
+
minInitialChars: 0,
|
|
160
|
+
throttleMs: throttleAnchorMs,
|
|
166
161
|
sendMessage,
|
|
167
162
|
editMessageText,
|
|
168
|
-
sendMessageDraft,
|
|
169
163
|
})
|
|
170
164
|
|
|
171
165
|
const prose = 'the agent suggested NO_REPLY earlier'
|
|
172
166
|
stream.update(prose)
|
|
173
|
-
// materialize() should send a fresh message — only 1 call, from materialize
|
|
174
|
-
// itself (update() in draft mode sends a draft, not a sendMessage call).
|
|
175
167
|
const msgId = await stream.materialize()
|
|
176
168
|
|
|
169
|
+
// materialize() sends a fresh message — prose is not a silent marker
|
|
177
170
|
expect(sendMessage).toHaveBeenCalledTimes(1)
|
|
178
171
|
expect(sendMessage).toHaveBeenCalledWith(
|
|
179
172
|
'chat45',
|
|
@@ -190,15 +183,13 @@ describe('answer-stream — silent-marker suppression at materialize()', () => {
|
|
|
190
183
|
// not on any intermediate chunk.
|
|
191
184
|
const sendMessage = makeSendMessage()
|
|
192
185
|
const editMessageText = makeEditMessageText()
|
|
193
|
-
const sendMessageDraft = makeSendMessageDraft()
|
|
194
186
|
const logs: string[] = []
|
|
195
187
|
const stream = createAnswerStream({
|
|
196
188
|
chatId: 'chat47',
|
|
197
|
-
|
|
198
|
-
throttleMs:
|
|
189
|
+
minInitialChars: 0,
|
|
190
|
+
throttleMs: throttleAnchorMs,
|
|
199
191
|
sendMessage,
|
|
200
192
|
editMessageText,
|
|
201
|
-
sendMessageDraft,
|
|
202
193
|
log: (msg) => logs.push(msg),
|
|
203
194
|
})
|
|
204
195
|
|
|
@@ -218,8 +209,7 @@ describe('answer-stream — silent-marker suppression at materialize()', () => {
|
|
|
218
209
|
const logs: string[] = []
|
|
219
210
|
const stream = createAnswerStream({
|
|
220
211
|
chatId: 'chat46',
|
|
221
|
-
|
|
222
|
-
throttleMs: 250,
|
|
212
|
+
throttleMs: throttleAnchorMs,
|
|
223
213
|
sendMessage,
|
|
224
214
|
editMessageText,
|
|
225
215
|
log: (msg) => logs.push(msg),
|