switchroom 0.13.26 → 0.13.27

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.
Files changed (30) hide show
  1. package/dist/cli/switchroom.js +2 -2
  2. package/package.json +1 -1
  3. package/telegram-plugin/active-reactions-sweep.ts +4 -4
  4. package/telegram-plugin/dist/gateway/gateway.js +239 -64
  5. package/telegram-plugin/docs/waiting-ux-spec.md +17 -1
  6. package/telegram-plugin/gateway/disconnect-flush.ts +10 -6
  7. package/telegram-plugin/gateway/gateway.ts +166 -51
  8. package/telegram-plugin/gateway/inbound-spool.ts +69 -2
  9. package/telegram-plugin/gateway/subagent-handback-inbound-builder.ts +14 -0
  10. package/telegram-plugin/gateway/subagent-progress-inbound-builder.ts +256 -0
  11. package/telegram-plugin/pending-work-progress.ts +5 -1
  12. package/telegram-plugin/status-reactions.ts +70 -58
  13. package/telegram-plugin/stream-reply-handler.ts +7 -36
  14. package/telegram-plugin/subagent-watcher.ts +64 -3
  15. package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +5 -3
  16. package/telegram-plugin/tests/inbound-spool-progress.test.ts +213 -0
  17. package/telegram-plugin/tests/inbound-spool.test.ts +62 -0
  18. package/telegram-plugin/tests/multi-turn-continuity.test.ts +0 -1
  19. package/telegram-plugin/tests/outbound-ordering.test.ts +0 -1
  20. package/telegram-plugin/tests/parse-mode-rotation.test.ts +0 -1
  21. package/telegram-plugin/tests/reply-terminal-reaction.test.ts +78 -135
  22. package/telegram-plugin/tests/status-accent.test.ts +0 -1
  23. package/telegram-plugin/tests/status-reactions.test.ts +56 -27
  24. package/telegram-plugin/tests/stream-reply-error-paths.test.ts +0 -1
  25. package/telegram-plugin/tests/stream-reply-handler.test.ts +9 -25
  26. package/telegram-plugin/tests/streaming-e2e.test.ts +0 -1
  27. package/telegram-plugin/tests/streaming-orchestration.test.ts +0 -1
  28. package/telegram-plugin/tests/subagent-handback-inbound-builder.test.ts +22 -0
  29. package/telegram-plugin/tests/subagent-progress-inbound-builder.test.ts +269 -0
  30. package/telegram-plugin/uat/scenarios/jtbd-reflective-status-reaction-dm.test.ts +204 -0
@@ -114,6 +114,14 @@ export interface WorkerEntry {
114
114
  * narrative line.
115
115
  */
116
116
  lastResultText: string
117
+ /**
118
+ * Last bucket index for which an `onProgress` callback was fired for
119
+ * this sub-agent (#1720). Null until the first envelope. The gateway
120
+ * owns the actual decision via `decideSubagentProgress`; this field
121
+ * persists the cursor across `sub_agent_text` events on the same
122
+ * entry so the watcher doesn't re-fire within the same bucket window.
123
+ */
124
+ lastProgressBucketIdx: number | null
117
125
  /**
118
126
  * Most recent tool call observed on this sub-agent's JSONL tail —
119
127
  * tool name + sanitised arg for fleet-row display (P0 of #662). Null
@@ -281,6 +289,23 @@ export interface SubagentWatcherConfig {
281
289
  * `subagent_handback` inbound. */
282
290
  resultText: string
283
291
  }) => void
292
+ /**
293
+ * #1720: fires on every `sub_agent_text` event for a running
294
+ * sub-agent. The gateway decides whether to materialise a
295
+ * `subagent_progress` envelope via `decideSubagentProgress` (pure,
296
+ * bucket-deterministic); the watcher just surfaces the cue.
297
+ * `setBucketIdx` writes back the per-entry cursor so a same-bucket
298
+ * re-fire is suppressed. Foreground vs background classification is
299
+ * the gateway's call.
300
+ */
301
+ onProgress?: (args: {
302
+ agentId: string
303
+ description: string
304
+ latestSummary: string
305
+ elapsedMs: number
306
+ prevBucketIdx: number | null
307
+ setBucketIdx: (b: number) => void
308
+ }) => void
284
309
  /** `Date.now` override for tests. */
285
310
  now?: () => number
286
311
  /** `setInterval` override for tests. */
@@ -483,6 +508,21 @@ export function readSubTail(
483
508
  * the clerk agent logged 540k ENOENT lines in 3 days (30/sec
484
509
  * sustained) AND leaked one fs.watch FD per stranded entry. */
485
510
  onFileVanished?: (agentId: string, code: 'ENOENT' | 'EACCES') => void,
511
+ /** Fires on every `sub_agent_text` event for a running sub-agent
512
+ * (#1720). The gateway decides whether to materialise a progress
513
+ * envelope via `decideSubagentProgress` — pure decision, watcher
514
+ * just surfaces the cue. `latestSummary` is the narrative text;
515
+ * `elapsedMs` is `now - entry.dispatchedAt`; `prevBucketIdx` is
516
+ * `entry.lastProgressBucketIdx` (gateway calls `setBucketIdx` on a
517
+ * successful deliver so a same-bucket re-fire is suppressed). */
518
+ onProgress?: (args: {
519
+ agentId: string
520
+ description: string
521
+ latestSummary: string
522
+ elapsedMs: number
523
+ prevBucketIdx: number | null
524
+ setBucketIdx: (b: number) => void
525
+ }) => void,
486
526
  ): void {
487
527
  try {
488
528
  const stat = fs.statSync(entry.filePath)
@@ -620,6 +660,26 @@ export function readSubTail(
620
660
  // args or file content — consistent with the watcher's
621
661
  // "descriptions only" privacy posture.
622
662
  entry.lastResultText = ev.text.trim().slice(0, SUBAGENT_RESULT_TEXT_MAX)
663
+ // #1720: surface a progress cue for the gateway. Only fire
664
+ // while the entry is still running and not historical — a
665
+ // terminal entry's last narrative line is the handback
666
+ // payload, not a mid-flight progress nudge.
667
+ if (onProgress != null && entry.state === 'running' && !entry.historical) {
668
+ try {
669
+ onProgress({
670
+ agentId: entry.agentId,
671
+ description: entry.description,
672
+ latestSummary: entry.lastResultText,
673
+ elapsedMs: now - entry.dispatchedAt,
674
+ prevBucketIdx: entry.lastProgressBucketIdx,
675
+ setBucketIdx: (b: number) => {
676
+ entry.lastProgressBucketIdx = b
677
+ },
678
+ })
679
+ } catch (cbErr) {
680
+ log?.(`subagent-watcher: onProgress callback error ${entry.agentId}: ${(cbErr as Error).message}`)
681
+ }
682
+ }
623
683
  } else if (ev.kind === 'sub_agent_turn_end') {
624
684
  if (entry.state === 'running') {
625
685
  entry.state = 'done'
@@ -802,6 +862,7 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
802
862
  stallTerminalSynthesised: false,
803
863
  lastSummaryLine: '',
804
864
  lastResultText: '',
865
+ lastProgressBucketIdx: null,
805
866
  lastTool: null,
806
867
  historical: isHistorical,
807
868
  }
@@ -832,7 +893,7 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
832
893
  // Initial read
833
894
  readSubTail(entry, tail, n, (desc) => {
834
895
  log?.(`subagent-watcher: description updated for ${agentId}: ${desc}`)
835
- }, fs, log, db, parentStateDir, config.onUnstall)
896
+ }, fs, log, db, parentStateDir, config.onUnstall, undefined, config.onProgress)
836
897
 
837
898
  // If the JSONL already contained a turn_end at registration time
838
899
  // (file written-then-watched), fire the state-transition + completion
@@ -863,7 +924,7 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
863
924
  if (!entry || !t) return
864
925
  readSubTail(entry, t, nowFn(), (desc) => {
865
926
  log?.(`subagent-watcher: description updated for ${agentId}: ${desc}`)
866
- }, fs, log, db, parentStateDir, config.onUnstall, cleanupTerminalAgent)
927
+ }, fs, log, db, parentStateDir, config.onUnstall, cleanupTerminalAgent, config.onProgress)
867
928
  maybySendStateTransition(agentId)
868
929
  })
869
930
  } catch (err) {
@@ -1201,7 +1262,7 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
1201
1262
  if (!tail) continue
1202
1263
  readSubTail(entry, tail, n, (desc) => {
1203
1264
  log?.(`subagent-watcher: description updated for ${agentId}: ${desc}`)
1204
- }, fs, log, db, parentStateDir, config.onUnstall, cleanupTerminalAgent)
1265
+ }, fs, log, db, parentStateDir, config.onUnstall, cleanupTerminalAgent, config.onProgress)
1205
1266
  maybySendStateTransition(agentId)
1206
1267
  }
1207
1268
 
@@ -20,7 +20,9 @@ import { describe, it, expect, vi } from 'vitest'
20
20
  import { flushOnAgentDisconnect } from '../gateway/disconnect-flush.js'
21
21
 
22
22
  interface FakeCtrl {
23
- setDone: () => void
23
+ // #1713: disconnect-flush now routes through finalize() single
24
+ // terminal path for the status-reaction controller.
25
+ finalize: (reason?: 'done' | 'error') => void
24
26
  }
25
27
  interface FakeStream {
26
28
  isFinal: () => boolean
@@ -37,8 +39,8 @@ function makeDeps(agentName: string | null) {
37
39
  const log = vi.fn()
38
40
 
39
41
  const activeStatusReactions = new Map<string, FakeCtrl>([
40
- ['chat1:thr1:msg1', { setDone: setDoneA }],
41
- ['chat2:thr2:msg2', { setDone: setDoneB }],
42
+ ['chat1:thr1:msg1', { finalize: setDoneA }],
43
+ ['chat2:thr2:msg2', { finalize: setDoneB }],
42
44
  ])
43
45
  const activeReactionMsgIds = new Map<string, { chatId: string; messageId: number }>([
44
46
  ['chat1:thr1:msg1', { chatId: 'chat1', messageId: 1 }],
@@ -0,0 +1,213 @@
1
+ /**
2
+ * #1720 — extensions to inbound-spool for the progress envelope.
3
+ *
4
+ * Three behaviours pinned here that the existing spool tests don't
5
+ * cover:
6
+ *
7
+ * 1. spoolId for `subagent_progress` is bucket-deterministic — two
8
+ * envelopes for the same (jsonl id, bucket idx) collapse to ONE
9
+ * live entry regardless of ts.
10
+ * 2. `liveEntries()` skips entries whose `meta.expiresAt` has
11
+ * passed (stale progress is worse than no progress).
12
+ * 3. `dropMatching(predicate)` tombstones live entries by spool id
13
+ * prefix — the handback path uses this to sweep stale progress
14
+ * envelopes for a sub-agent at the moment its handback is queued.
15
+ */
16
+
17
+ import { describe, it, expect } from 'vitest'
18
+ import {
19
+ createInboundSpool,
20
+ spoolId,
21
+ type InboundSpoolFsSeam,
22
+ } from '../gateway/inbound-spool.js'
23
+ import type { InboundMessage } from '../gateway/ipc-protocol.js'
24
+
25
+ function progressMsg(over: {
26
+ jsonlId: string
27
+ bucketIdx: number
28
+ ts?: number
29
+ chatId?: string
30
+ expiresAt?: number
31
+ }): InboundMessage {
32
+ const ts = over.ts ?? 1_000_000
33
+ const meta: Record<string, string> = {
34
+ source: 'subagent_progress',
35
+ subagent_jsonl_id: over.jsonlId,
36
+ bucket_idx: String(over.bucketIdx),
37
+ }
38
+ if (over.expiresAt != null) meta.expiresAt = String(over.expiresAt)
39
+ return {
40
+ type: 'inbound',
41
+ chatId: over.chatId ?? 'c1',
42
+ messageId: ts,
43
+ user: 'subagent-watcher',
44
+ userId: 0,
45
+ ts,
46
+ text: 'progress envelope',
47
+ meta,
48
+ }
49
+ }
50
+
51
+ function fakeFs(): InboundSpoolFsSeam {
52
+ const files = new Map<string, string>()
53
+ return {
54
+ appendFileSync: (p, d) => files.set(p, (files.get(p) ?? '') + d),
55
+ readFileSync: (p) => files.get(p) ?? '',
56
+ writeFileSync: (p, d) => files.set(p, d),
57
+ renameSync: (from, to) => {
58
+ files.set(to, files.get(from) ?? '')
59
+ files.delete(from)
60
+ },
61
+ existsSync: (p) => files.has(p),
62
+ statSizeSync: (p) => Buffer.byteLength(files.get(p) ?? ''),
63
+ }
64
+ }
65
+
66
+ const PATH = '/state/agent/telegram/inbound-spool.jsonl'
67
+
68
+ describe('spoolId — subagent_progress branch (#1720)', () => {
69
+ it('two progress envelopes for the same (jsonl id, bucket idx) have IDENTICAL ids', () => {
70
+ const a = spoolId(progressMsg({ jsonlId: 'j1', bucketIdx: 2, ts: 1000 }))
71
+ const b = spoolId(progressMsg({ jsonlId: 'j1', bucketIdx: 2, ts: 9999 }))
72
+ expect(a).toBe('s:progress:j1:2')
73
+ expect(a).toBe(b)
74
+ })
75
+
76
+ it('different bucket idx → different id', () => {
77
+ expect(
78
+ spoolId(progressMsg({ jsonlId: 'j1', bucketIdx: 1 })),
79
+ ).not.toBe(spoolId(progressMsg({ jsonlId: 'j1', bucketIdx: 2 })))
80
+ })
81
+
82
+ it('different jsonl id → different id', () => {
83
+ expect(
84
+ spoolId(progressMsg({ jsonlId: 'j1', bucketIdx: 1 })),
85
+ ).not.toBe(spoolId(progressMsg({ jsonlId: 'j2', bucketIdx: 1 })))
86
+ })
87
+ })
88
+
89
+ describe('inbound-spool — progress envelopes coalesce by bucket', () => {
90
+ it('three puts for the same (jsonl id, bucket idx) yield ONE live entry', () => {
91
+ const fs = fakeFs()
92
+ const s = createInboundSpool({ path: PATH, fs })
93
+ expect(s.put('worker', progressMsg({ jsonlId: 'j1', bucketIdx: 1, ts: 1000 }))).toBe(true)
94
+ expect(s.put('worker', progressMsg({ jsonlId: 'j1', bucketIdx: 1, ts: 2000 }))).toBe(false)
95
+ expect(s.put('worker', progressMsg({ jsonlId: 'j1', bucketIdx: 1, ts: 3000 }))).toBe(false)
96
+ expect(s.liveCount()).toBe(1)
97
+ })
98
+
99
+ it('a fresh spool over the same file still sees ONE live entry (restart-safe coalesce)', () => {
100
+ const fs = fakeFs()
101
+ const s1 = createInboundSpool({ path: PATH, fs })
102
+ s1.put('worker', progressMsg({ jsonlId: 'j1', bucketIdx: 1, ts: 1000 }))
103
+ s1.put('worker', progressMsg({ jsonlId: 'j1', bucketIdx: 1, ts: 2000 }))
104
+ const s2 = createInboundSpool({ path: PATH, fs })
105
+ expect(s2.liveCount()).toBe(1)
106
+ expect(s2.liveEntries()[0].msg.meta.subagent_jsonl_id).toBe('j1')
107
+ })
108
+
109
+ it('successive buckets DO NOT collapse', () => {
110
+ const fs = fakeFs()
111
+ const s = createInboundSpool({ path: PATH, fs })
112
+ s.put('worker', progressMsg({ jsonlId: 'j1', bucketIdx: 1 }))
113
+ s.put('worker', progressMsg({ jsonlId: 'j1', bucketIdx: 2 }))
114
+ s.put('worker', progressMsg({ jsonlId: 'j1', bucketIdx: 3 }))
115
+ expect(s.liveCount()).toBe(3)
116
+ })
117
+ })
118
+
119
+ describe('inbound-spool — TTL filter on liveEntries (#1720)', () => {
120
+ it('an entry whose expiresAt is in the past is OMITTED from liveEntries', () => {
121
+ const fs = fakeFs()
122
+ let t = 1_000_000
123
+ const s = createInboundSpool({ path: PATH, fs, now: () => t })
124
+ // expiresAt = 1_002_000 — within window at put-time
125
+ s.put('worker', progressMsg({ jsonlId: 'j1', bucketIdx: 1, ts: t, expiresAt: 1_002_000 }))
126
+ expect(s.liveEntries()).toHaveLength(1)
127
+ expect(s.liveCount()).toBe(1) // raw count includes it
128
+ // Advance clock past expiry
129
+ t = 1_010_000
130
+ expect(s.liveEntries()).toHaveLength(0)
131
+ // liveCount is the raw map size — TTL is a read-time filter.
132
+ expect(s.liveCount()).toBe(1)
133
+ })
134
+
135
+ it('entries without expiresAt are not affected (handback envelopes stay live)', () => {
136
+ const fs = fakeFs()
137
+ let t = 1_000_000
138
+ const s = createInboundSpool({ path: PATH, fs, now: () => t })
139
+ s.put('worker', {
140
+ type: 'inbound',
141
+ chatId: 'c1',
142
+ messageId: 999,
143
+ user: 'x',
144
+ userId: 0,
145
+ ts: t,
146
+ text: 'handback',
147
+ meta: { source: 'subagent_handback' },
148
+ })
149
+ t = 9_000_000_000
150
+ expect(s.liveEntries()).toHaveLength(1)
151
+ })
152
+
153
+ it('a non-numeric or empty expiresAt is treated as "no expiry"', () => {
154
+ const fs = fakeFs()
155
+ let t = 1_000_000
156
+ const s = createInboundSpool({ path: PATH, fs, now: () => t })
157
+ s.put('worker', {
158
+ type: 'inbound',
159
+ chatId: 'c1',
160
+ messageId: 1,
161
+ user: 'x',
162
+ userId: 0,
163
+ ts: t,
164
+ text: 'x',
165
+ meta: { source: 'subagent_progress', subagent_jsonl_id: 'j1', bucket_idx: '1', expiresAt: '' },
166
+ })
167
+ s.put('worker', {
168
+ type: 'inbound',
169
+ chatId: 'c1',
170
+ messageId: 2,
171
+ user: 'x',
172
+ userId: 0,
173
+ ts: t,
174
+ text: 'x',
175
+ meta: { source: 'subagent_progress', subagent_jsonl_id: 'j2', bucket_idx: '1', expiresAt: 'not-a-number' },
176
+ })
177
+ t = 9_000_000_000
178
+ expect(s.liveEntries()).toHaveLength(2)
179
+ })
180
+ })
181
+
182
+ describe('inbound-spool — dropMatching (#1720 handback sweep)', () => {
183
+ it('drops every live entry whose id matches the predicate', () => {
184
+ const fs = fakeFs()
185
+ const s = createInboundSpool({ path: PATH, fs })
186
+ s.put('worker', progressMsg({ jsonlId: 'j1', bucketIdx: 1 }))
187
+ s.put('worker', progressMsg({ jsonlId: 'j1', bucketIdx: 2 }))
188
+ s.put('worker', progressMsg({ jsonlId: 'j2', bucketIdx: 1 }))
189
+ expect(s.liveCount()).toBe(3)
190
+ const dropped = s.dropMatching((id) => id.startsWith('s:progress:j1:'))
191
+ expect(dropped).toBe(2)
192
+ expect(s.liveCount()).toBe(1)
193
+ expect(s.liveEntries()[0].msg.meta.subagent_jsonl_id).toBe('j2')
194
+ })
195
+
196
+ it('a drop survives across a restart (tombstoned in the log)', () => {
197
+ const fs = fakeFs()
198
+ const s1 = createInboundSpool({ path: PATH, fs })
199
+ s1.put('worker', progressMsg({ jsonlId: 'j1', bucketIdx: 1 }))
200
+ s1.put('worker', progressMsg({ jsonlId: 'j1', bucketIdx: 2 }))
201
+ expect(s1.dropMatching((id) => id.startsWith('s:progress:j1:'))).toBe(2)
202
+ const s2 = createInboundSpool({ path: PATH, fs })
203
+ expect(s2.liveCount()).toBe(0)
204
+ })
205
+
206
+ it('an empty match set is a no-op (returns 0)', () => {
207
+ const fs = fakeFs()
208
+ const s = createInboundSpool({ path: PATH, fs })
209
+ s.put('worker', progressMsg({ jsonlId: 'j1', bucketIdx: 1 }))
210
+ expect(s.dropMatching((id) => id.startsWith('s:progress:nope:'))).toBe(0)
211
+ expect(s.liveCount()).toBe(1)
212
+ })
213
+ })
@@ -67,6 +67,68 @@ describe('spoolId — stable dedup key', () => {
67
67
  const b = spoolId(msg({ messageId: 0, meta: { source: 'cron' }, ts: 100 }))
68
68
  expect(a).toBe(b)
69
69
  })
70
+ // #1719: a subagent_handback envelope carries the JSONL agent id, and
71
+ // spoolId() keys on it — so a re-built envelope for the same finished
72
+ // sub-agent (different Date.now()-derived ts/messageId across a
73
+ // restart or the onFinish race) collapses to one spool entry instead
74
+ // of re-firing the handback turn.
75
+ it('subagent_handback → s:handback:<jsonl_agent_id>, stable across ts', () => {
76
+ const a = spoolId(
77
+ msg({
78
+ messageId: 1700_000_000_000,
79
+ ts: 1700_000_000_000,
80
+ meta: { source: 'subagent_handback', subagent_jsonl_id: 'abc-123' },
81
+ }),
82
+ )
83
+ const b = spoolId(
84
+ msg({
85
+ messageId: 1700_000_999_999,
86
+ ts: 1700_000_999_999,
87
+ meta: { source: 'subagent_handback', subagent_jsonl_id: 'abc-123' },
88
+ }),
89
+ )
90
+ expect(a).toBe('s:handback:abc-123')
91
+ expect(b).toBe(a) // stable across Date.now() drift / restart re-build
92
+ })
93
+ it('subagent_handback for distinct sub-agents stays distinct', () => {
94
+ const a = spoolId(
95
+ msg({ messageId: 0, meta: { source: 'subagent_handback', subagent_jsonl_id: 'a' } }),
96
+ )
97
+ const b = spoolId(
98
+ msg({ messageId: 0, meta: { source: 'subagent_handback', subagent_jsonl_id: 'b' } }),
99
+ )
100
+ expect(a).not.toBe(b)
101
+ })
102
+ it('subagent_handback without jsonl id falls back to legacy id (back-compat)', () => {
103
+ const a = spoolId(
104
+ msg({ messageId: 555, meta: { source: 'subagent_handback' }, ts: 100 }),
105
+ )
106
+ // messageId > 0 → legacy m:<chat>:<msgId> still wins.
107
+ expect(a).toBe('m:c1:555')
108
+ })
109
+ })
110
+
111
+ describe('inbound-spool — subagent_handback dedup across restart re-build (#1719)', () => {
112
+ it('two handback envelopes for the same jsonl id collapse to one live entry', () => {
113
+ const fs = fakeFs()
114
+ const s = createInboundSpool({ path: PATH, fs })
115
+ const first = msg({
116
+ messageId: 1700_000_000_000,
117
+ ts: 1700_000_000_000,
118
+ meta: { source: 'subagent_handback', subagent_jsonl_id: 'jsonl-xyz' },
119
+ })
120
+ // Simulates a second onFinish (or boot-replay re-build) for the
121
+ // same sub-agent. Different ts / messageId — same jsonl id.
122
+ const second = msg({
123
+ messageId: 1700_000_999_999,
124
+ ts: 1700_000_999_999,
125
+ meta: { source: 'subagent_handback', subagent_jsonl_id: 'jsonl-xyz' },
126
+ })
127
+ expect(s.put('worker', first)).toBe(true)
128
+ expect(s.put('worker', second)).toBe(false) // deduped, no re-fire
129
+ expect(s.liveCount()).toBe(1)
130
+ expect(s.liveEntries()).toHaveLength(1)
131
+ })
70
132
  })
71
133
 
72
134
  describe('inbound-spool — put / ack / dedup', () => {
@@ -37,7 +37,6 @@ function makeDeps(bot: FakeBot, overrides?: Partial<StreamReplyDeps>): StreamRep
37
37
  disableLinkPreview: true,
38
38
  defaultFormat: 'html',
39
39
  logStreamingEvent: () => {},
40
- endStatusReaction: () => {},
41
40
  historyEnabled: false,
42
41
  recordOutbound: () => {},
43
42
  writeError: () => {},
@@ -215,7 +215,6 @@ describe('wrapBot + handleStreamReply + reply ordering', () => {
215
215
  disableLinkPreview: true,
216
216
  defaultFormat: 'text',
217
217
  logStreamingEvent: () => {},
218
- endStatusReaction: () => {},
219
218
  historyEnabled: false,
220
219
  recordOutbound: () => {},
221
220
  writeError: () => {},
@@ -37,7 +37,6 @@ function makeDeps(bot: FakeBot, overrides?: Partial<StreamReplyDeps>): StreamRep
37
37
  disableLinkPreview: true,
38
38
  defaultFormat: 'html',
39
39
  logStreamingEvent: () => {},
40
- endStatusReaction: () => {},
41
40
  historyEnabled: false,
42
41
  recordOutbound: () => {},
43
42
  writeError: () => {},