switchroom 0.20.2 → 0.20.4
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/bin/handoff-briefing.sh +41 -1
- package/dist/cli/switchroom.js +10259 -1232
- package/dist/host-control/main.js +1 -1
- package/package.json +3 -3
- package/profiles/default/CLAUDE.md.hbs +13 -11
- package/telegram-plugin/dist/gateway/gateway.js +429 -98
- package/telegram-plugin/edit-flood-fuse.ts +332 -14
- package/telegram-plugin/gateway/feed-open-gate.ts +28 -8
- package/telegram-plugin/gateway/feed-reopen-gate.ts +88 -6
- package/telegram-plugin/gateway/gateway.ts +128 -104
- package/telegram-plugin/gateway/narrative-lane.ts +4 -0
- package/telegram-plugin/gateway/progress-fallback-cap.ts +195 -0
- package/telegram-plugin/gateway/stream-render.ts +67 -12
- package/telegram-plugin/gateway/subagent-handback-inbound-builder.ts +13 -0
- package/telegram-plugin/gateway/subagent-origin-surface.ts +181 -0
- package/telegram-plugin/gateway/subagent-progress-inbound-builder.ts +7 -0
- package/telegram-plugin/registry/subagents-schema.ts +61 -0
- package/telegram-plugin/registry/turns-schema.ts +26 -0
- package/telegram-plugin/tests/edit-flood-fuse-cosmetic-fairness.test.ts +229 -0
- package/telegram-plugin/tests/feed-open-gate.test.ts +42 -0
- package/telegram-plugin/tests/feed-reopen-gate.test.ts +114 -0
- package/telegram-plugin/tests/progress-cap.test.ts +182 -0
- package/telegram-plugin/tests/progress-fallback-cap.test.ts +91 -0
- package/telegram-plugin/tests/progress-update.test.ts +108 -12
- package/telegram-plugin/tests/subagent-handback-inbound-builder.test.ts +46 -0
- package/telegram-plugin/tests/subagent-progress-inbound-builder.test.ts +26 -0
- package/telegram-plugin/tests/worker-origin-gap-dispatch.test.ts +304 -0
|
@@ -353,3 +353,29 @@ describe('decideSubagentProgress', () => {
|
|
|
353
353
|
}
|
|
354
354
|
})
|
|
355
355
|
})
|
|
356
|
+
|
|
357
|
+
// ─── msg-6897 misroute regression (2026-08-04): meta.chat_id is LOAD-BEARING ──
|
|
358
|
+
// Same turn-registration contract as the handback builder (see
|
|
359
|
+
// subagent-handback-inbound-builder.test.ts): without meta.chat_id the
|
|
360
|
+
// progress turn's channel XML has no chat_id, the gateway mints no turn atom
|
|
361
|
+
// (no `turns` row, no `turn-active.json`), and a worker dispatched from
|
|
362
|
+
// inside the progress turn can never be attributed to its chat/topic.
|
|
363
|
+
describe('buildSubagentProgressInbound — meta.chat_id turn registration (msg-6897)', () => {
|
|
364
|
+
it('carries the origin chat as meta.chat_id', () => {
|
|
365
|
+
const inbound = buildSubagentProgressInbound({
|
|
366
|
+
ctx: {
|
|
367
|
+
chatId: '-1004223464247',
|
|
368
|
+
threadId: 77,
|
|
369
|
+
subagentJsonlId: 'stem1',
|
|
370
|
+
taskDescription: 'Topic work',
|
|
371
|
+
latestSummary: 'still going',
|
|
372
|
+
elapsedMs: 6 * 60_000,
|
|
373
|
+
bucketIdx: 1,
|
|
374
|
+
progressIntervalMs: 5 * 60_000,
|
|
375
|
+
},
|
|
376
|
+
nowMs: 1_700_000_000_000,
|
|
377
|
+
})
|
|
378
|
+
expect(inbound.meta.chat_id).toBe('-1004223464247')
|
|
379
|
+
expect(inbound.meta.message_thread_id).toBe('77')
|
|
380
|
+
})
|
|
381
|
+
})
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression: the gap-dispatch worker misroute (Telegram msg 6897,
|
|
3
|
+
* 2026-08-04).
|
|
4
|
+
*
|
|
5
|
+
* A depth-1 background worker was dispatched ~6.6 min AFTER the last topic
|
|
6
|
+
* turn's row had ended, with NO `turns` row open at its `started_at` (the
|
|
7
|
+
* dispatching turn was a synthesized handback turn that never registered a
|
|
8
|
+
* turn surface). Consequences on the pre-fix code:
|
|
9
|
+
*
|
|
10
|
+
* - the pretool hook's #2085 dispatch-time stamp had no `turn-active.json`
|
|
11
|
+
* to read → `parent_turn_key` NULL at insert;
|
|
12
|
+
* - the watcher's started_at window backfill
|
|
13
|
+
* (`SELECT turn_key FROM turns WHERE started_at <= ? AND (ended_at IS
|
|
14
|
+
* NULL OR ended_at >= ?)`) matched nothing → NULL stays;
|
|
15
|
+
* - `resolveSubagentOriginChat` → null → BOTH the worker card
|
|
16
|
+
* (`resolveWorkerFeedChat`) and the handback decision
|
|
17
|
+
* (`decideSubagentHandback`) fell through `fleetChatId || allowFrom[0]`
|
|
18
|
+
* to the OWNER DM with the forum thread stripped.
|
|
19
|
+
*
|
|
20
|
+
* These tests pin the two closures:
|
|
21
|
+
* 1. `stampSubagentDispatchTurn` — the gateway-side dispatch-time stamp
|
|
22
|
+
* (marker-free, window-free) that attributes the row from the live
|
|
23
|
+
* turn key the gateway holds when it observes the Agent/Task tool_use.
|
|
24
|
+
* 2. `resolveWorkerSurfaceChat` — the safe-degradation floor: when the
|
|
25
|
+
* stamp still missed, resolution falls to the MOST-RECENT turn's
|
|
26
|
+
* chat+topic (the #3458 mechanism, DB-backed) BEFORE the owner DM —
|
|
27
|
+
* asserted end-to-end into `decideSubagentHandback`'s built inbound.
|
|
28
|
+
*
|
|
29
|
+
* bun:sqlite — run under Bun:
|
|
30
|
+
* bun test telegram-plugin/tests/worker-origin-gap-dispatch.test.ts
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { describe, it, expect, beforeEach, afterEach } from 'bun:test'
|
|
34
|
+
import { openTurnsDbInMemory } from '../registry/turns-schema.js'
|
|
35
|
+
import {
|
|
36
|
+
applySubagentsSchema,
|
|
37
|
+
stampSubagentDispatchTurn,
|
|
38
|
+
resolveSubagentOriginTurnKey,
|
|
39
|
+
getSubagent,
|
|
40
|
+
} from '../registry/subagents-schema.js'
|
|
41
|
+
import {
|
|
42
|
+
resolveWorkerSurfaceChat,
|
|
43
|
+
resolveSubagentOriginChatDb,
|
|
44
|
+
resolveRecentTurnFallbackChat,
|
|
45
|
+
} from '../gateway/subagent-origin-surface.js'
|
|
46
|
+
import { decideSubagentHandback } from '../gateway/subagent-handback-inbound-builder.js'
|
|
47
|
+
|
|
48
|
+
type Db = ReturnType<typeof openTurnsDbInMemory>
|
|
49
|
+
|
|
50
|
+
const TOPIC_CHAT = '-1004223464247'
|
|
51
|
+
const TOPIC_THREAD = '77'
|
|
52
|
+
const OWNER_DM = '555'
|
|
53
|
+
|
|
54
|
+
let db: Db
|
|
55
|
+
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
db = openTurnsDbInMemory()
|
|
58
|
+
applySubagentsSchema(db)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
try { db.close() } catch { /* ignore */ }
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
function insertTurn(args: {
|
|
66
|
+
turnKey: string
|
|
67
|
+
chatId: string
|
|
68
|
+
threadId?: string | null
|
|
69
|
+
startedAt: number
|
|
70
|
+
endedAt?: number | null
|
|
71
|
+
}) {
|
|
72
|
+
db.prepare(`
|
|
73
|
+
INSERT INTO turns (turn_key, chat_id, thread_id, started_at, ended_at, created_at, updated_at)
|
|
74
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
75
|
+
`).run(
|
|
76
|
+
args.turnKey,
|
|
77
|
+
args.chatId,
|
|
78
|
+
args.threadId ?? null,
|
|
79
|
+
args.startedAt,
|
|
80
|
+
args.endedAt ?? null,
|
|
81
|
+
args.startedAt,
|
|
82
|
+
args.startedAt,
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** A hook-inserted depth-1 row: parent_turn_key NULL (no marker at dispatch),
|
|
87
|
+
* jsonl already linked (the backfill linked the stem but had no containing
|
|
88
|
+
* turn window to attribute from — the exact msg-6897 row shape). */
|
|
89
|
+
function insertGapDispatchedWorker(args: { id: string; jsonlAgentId: string; startedAt: number }) {
|
|
90
|
+
db.prepare(`
|
|
91
|
+
INSERT INTO subagents
|
|
92
|
+
(id, parent_session_id, parent_turn_key, agent_type, description,
|
|
93
|
+
background, started_at, last_activity_at, status, jsonl_agent_id)
|
|
94
|
+
VALUES (?, 'sess-1', NULL, 'worker', 'Gap-dispatched task', 1, ?, ?, 'running', ?)
|
|
95
|
+
`).run(args.id, args.startedAt, args.startedAt, args.jsonlAgentId)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
describe('gap dispatch — safe-degradation floor (msg-6897)', () => {
|
|
99
|
+
/** The incident fixture: topic turn [1_000_000, 1_600_000] (ended), worker
|
|
100
|
+
* dispatched ~6.6 min after end — no turns row open at started_at. */
|
|
101
|
+
function seedIncident() {
|
|
102
|
+
insertTurn({
|
|
103
|
+
turnKey: `${TOPIC_CHAT}:${TOPIC_THREAD}:1000000`,
|
|
104
|
+
chatId: TOPIC_CHAT,
|
|
105
|
+
threadId: TOPIC_THREAD,
|
|
106
|
+
startedAt: 1_000_000,
|
|
107
|
+
endedAt: 1_600_000,
|
|
108
|
+
})
|
|
109
|
+
const dispatchAt = 1_600_000 + Math.round(6.6 * 60_000)
|
|
110
|
+
insertGapDispatchedWorker({ id: 'toolu_gap', jsonlAgentId: 'wkr_gap', startedAt: dispatchAt })
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
it('pre-condition: origin resolution genuinely misses for the gap-dispatched row', () => {
|
|
114
|
+
seedIncident()
|
|
115
|
+
// The row is linked but unattributed — the DB-confirmed incident state.
|
|
116
|
+
expect(getSubagent(db, 'toolu_gap')?.parent_turn_key).toBeNull()
|
|
117
|
+
expect(resolveSubagentOriginTurnKey(db, 'wkr_gap')).toBeNull()
|
|
118
|
+
expect(resolveSubagentOriginChatDb(db, 'wkr_gap')).toBeNull()
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('worker card surface resolves to the origin topic, NOT the owner DM with thread stripped', () => {
|
|
122
|
+
seedIncident()
|
|
123
|
+
const dest = resolveWorkerSurfaceChat(db, 'wkr_gap', { fleetChatId: '', ownerDm: OWNER_DM })
|
|
124
|
+
// THE outcome assertion: {chat_id, thread_id} — the topic, not the DM.
|
|
125
|
+
expect(dest.chatId).toBe(TOPIC_CHAT)
|
|
126
|
+
expect(dest.threadId).toBe(Number(TOPIC_THREAD))
|
|
127
|
+
expect(dest.via).toBe('recent-turn')
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('handback inbound routes to the origin topic, NOT the owner DM with thread stripped', () => {
|
|
131
|
+
seedIncident()
|
|
132
|
+
// The gateway's onFinish ladder post-fix: origin (null here) → recent-turn
|
|
133
|
+
// floor, fed into the SAME pure decision the gateway runs.
|
|
134
|
+
const surface = resolveWorkerSurfaceChat(db, 'wkr_gap', { fleetChatId: '', ownerDm: OWNER_DM })
|
|
135
|
+
const decision = decideSubagentHandback({
|
|
136
|
+
handbackEnvValue: undefined,
|
|
137
|
+
outcome: 'completed',
|
|
138
|
+
isBackground: true,
|
|
139
|
+
fleetChatId: surface.via === 'owner-dm' ? '' : surface.chatId,
|
|
140
|
+
...(surface.threadId != null ? { originThreadId: surface.threadId } : {}),
|
|
141
|
+
ownerChatId: OWNER_DM,
|
|
142
|
+
taskDescription: 'Gap-dispatched task',
|
|
143
|
+
resultText: 'done',
|
|
144
|
+
jsonlAgentId: 'wkr_gap',
|
|
145
|
+
nowMs: 1_700_000_000_000,
|
|
146
|
+
})
|
|
147
|
+
expect(decision.deliver).toBe(true)
|
|
148
|
+
if (decision.deliver) {
|
|
149
|
+
expect(decision.chatId).toBe(TOPIC_CHAT)
|
|
150
|
+
expect(decision.inbound.threadId).toBe(Number(TOPIC_THREAD))
|
|
151
|
+
expect(decision.inbound.meta.message_thread_id).toBe(TOPIC_THREAD)
|
|
152
|
+
// And the minted handback turn registers its own surface (fix 2).
|
|
153
|
+
expect(decision.inbound.meta.chat_id).toBe(TOPIC_CHAT)
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('a properly-attributed worker still resolves via origin — the floor never hijacks it', () => {
|
|
158
|
+
// Origin topic A; a NEWER turn ran in topic B. The attributed worker must
|
|
159
|
+
// keep routing to A (origin), not follow the recent-turn floor to B.
|
|
160
|
+
insertTurn({ turnKey: `${TOPIC_CHAT}:4:1000`, chatId: TOPIC_CHAT, threadId: '4', startedAt: 1000, endedAt: 2000 })
|
|
161
|
+
insertTurn({ turnKey: `${TOPIC_CHAT}:9:5000`, chatId: TOPIC_CHAT, threadId: '9', startedAt: 5000, endedAt: 6000 })
|
|
162
|
+
db.prepare(`
|
|
163
|
+
INSERT INTO subagents
|
|
164
|
+
(id, parent_session_id, parent_turn_key, agent_type, description,
|
|
165
|
+
background, started_at, last_activity_at, status, jsonl_agent_id)
|
|
166
|
+
VALUES ('toolu_ok', 'sess-1', ?, 'worker', 'Attributed', 1, 1500, 1500, 'running', 'wkr_ok')
|
|
167
|
+
`).run(`${TOPIC_CHAT}:4:1000`)
|
|
168
|
+
|
|
169
|
+
const dest = resolveWorkerSurfaceChat(db, 'wkr_ok', { fleetChatId: '', ownerDm: OWNER_DM })
|
|
170
|
+
expect(dest.via).toBe('origin')
|
|
171
|
+
expect(dest.chatId).toBe(TOPIC_CHAT)
|
|
172
|
+
expect(dest.threadId).toBe(4)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('floor is bounded by the dispatch time — a NEWER unrelated chat turn never captures the surface', () => {
|
|
176
|
+
seedIncident()
|
|
177
|
+
// The operator later drives a turn in a DIFFERENT (possibly shared) group
|
|
178
|
+
// B, AFTER the worker's dispatch. The floor must resolve to the last turn
|
|
179
|
+
// BEFORE the dispatch (topic A) — never leak the worker's surface into B.
|
|
180
|
+
insertTurn({
|
|
181
|
+
turnKey: '-1009999999999:_:9000000',
|
|
182
|
+
chatId: '-1009999999999',
|
|
183
|
+
threadId: null,
|
|
184
|
+
startedAt: 9_000_000,
|
|
185
|
+
endedAt: 9_100_000,
|
|
186
|
+
})
|
|
187
|
+
const dest = resolveWorkerSurfaceChat(db, 'wkr_gap', { fleetChatId: '', ownerDm: OWNER_DM })
|
|
188
|
+
expect(dest.chatId).toBe(TOPIC_CHAT)
|
|
189
|
+
expect(dest.threadId).toBe(Number(TOPIC_THREAD))
|
|
190
|
+
expect(dest.via).toBe('recent-turn')
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('only post-dispatch turns exist — floor declines and falls to the owner DM, not the newer chat', () => {
|
|
194
|
+
insertGapDispatchedWorker({ id: 'toolu_late', jsonlAgentId: 'wkr_late', startedAt: 1000 })
|
|
195
|
+
insertTurn({ turnKey: '-1008888888888:_:5000', chatId: '-1008888888888', threadId: null, startedAt: 5000, endedAt: 6000 })
|
|
196
|
+
const dest = resolveWorkerSurfaceChat(db, 'wkr_late', { fleetChatId: '', ownerDm: OWNER_DM })
|
|
197
|
+
expect(dest.chatId).toBe(OWNER_DM)
|
|
198
|
+
expect(dest.via).toBe('owner-dm')
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('empty turns table still floors to the owner DM (durable last resort)', () => {
|
|
202
|
+
insertGapDispatchedWorker({ id: 'toolu_bare', jsonlAgentId: 'wkr_bare', startedAt: 1000 })
|
|
203
|
+
const dest = resolveWorkerSurfaceChat(db, 'wkr_bare', { fleetChatId: '', ownerDm: OWNER_DM })
|
|
204
|
+
expect(dest.chatId).toBe(OWNER_DM)
|
|
205
|
+
expect(dest.via).toBe('owner-dm')
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('null db (turn registry disabled) degrades to fleet → owner DM as before', () => {
|
|
209
|
+
const dest = resolveWorkerSurfaceChat(null, 'wkr_any', { fleetChatId: '', ownerDm: OWNER_DM })
|
|
210
|
+
expect(dest).toEqual({ chatId: OWNER_DM, via: 'owner-dm' })
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
describe('gateway dispatch-time stamp — stampSubagentDispatchTurn (msg-6897)', () => {
|
|
215
|
+
const TURN_KEY = `${TOPIC_CHAT}:${TOPIC_THREAD}:2000000`
|
|
216
|
+
|
|
217
|
+
function seedOpenTurn() {
|
|
218
|
+
insertTurn({
|
|
219
|
+
turnKey: TURN_KEY,
|
|
220
|
+
chatId: TOPIC_CHAT,
|
|
221
|
+
threadId: TOPIC_THREAD,
|
|
222
|
+
startedAt: 2_000_000,
|
|
223
|
+
endedAt: null,
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
it('UPDATE path: stamps a hook-inserted row whose parent_turn_key is NULL', () => {
|
|
228
|
+
seedOpenTurn()
|
|
229
|
+
insertGapDispatchedWorker({ id: 'toolu_stamp', jsonlAgentId: 'wkr_stamp', startedAt: 2_000_500 })
|
|
230
|
+
|
|
231
|
+
stampSubagentDispatchTurn(db, {
|
|
232
|
+
toolUseId: 'toolu_stamp',
|
|
233
|
+
parentTurnKey: TURN_KEY,
|
|
234
|
+
agentType: 'worker',
|
|
235
|
+
description: 'Gap-dispatched task',
|
|
236
|
+
background: true,
|
|
237
|
+
now: 2_000_500,
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
expect(getSubagent(db, 'toolu_stamp')?.parent_turn_key).toBe(TURN_KEY)
|
|
241
|
+
// End-to-end: the stamped row now resolves to the origin topic.
|
|
242
|
+
const origin = resolveSubagentOriginChatDb(db, 'wkr_stamp')
|
|
243
|
+
expect(origin?.chatId).toBe(TOPIC_CHAT)
|
|
244
|
+
expect(origin?.threadId).toBe(Number(TOPIC_THREAD))
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('INSERT path: the gateway stamp can land BEFORE the hook row (race) and the hook insert then no-ops', () => {
|
|
248
|
+
seedOpenTurn()
|
|
249
|
+
stampSubagentDispatchTurn(db, {
|
|
250
|
+
toolUseId: 'toolu_first',
|
|
251
|
+
parentTurnKey: TURN_KEY,
|
|
252
|
+
agentType: 'worker',
|
|
253
|
+
description: 'Raced dispatch',
|
|
254
|
+
background: true,
|
|
255
|
+
now: 2_000_600,
|
|
256
|
+
})
|
|
257
|
+
const row = getSubagent(db, 'toolu_first')
|
|
258
|
+
expect(row?.parent_turn_key).toBe(TURN_KEY)
|
|
259
|
+
expect(row?.background).toBe(true)
|
|
260
|
+
expect(row?.status).toBe('running')
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('never overwrites a hook-stamped parent_turn_key (normal in-turn dispatch unchanged)', () => {
|
|
264
|
+
seedOpenTurn()
|
|
265
|
+
db.prepare(`
|
|
266
|
+
INSERT INTO subagents
|
|
267
|
+
(id, parent_session_id, parent_turn_key, agent_type, description,
|
|
268
|
+
background, started_at, last_activity_at, status, jsonl_agent_id)
|
|
269
|
+
VALUES ('toolu_pre', 'sess-1', 'other:4:999', 'worker', 'Pre-stamped', 1, 2000700, 2000700, 'running', 'wkr_pre')
|
|
270
|
+
`).run()
|
|
271
|
+
|
|
272
|
+
stampSubagentDispatchTurn(db, {
|
|
273
|
+
toolUseId: 'toolu_pre',
|
|
274
|
+
parentTurnKey: TURN_KEY,
|
|
275
|
+
background: true,
|
|
276
|
+
now: 2_000_700,
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
expect(getSubagent(db, 'toolu_pre')?.parent_turn_key).toBe('other:4:999')
|
|
280
|
+
})
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
describe('recent-turn floor internals', () => {
|
|
284
|
+
it('resolveRecentTurnFallbackChat picks the newest PRE-DISPATCH turn and parses its thread', () => {
|
|
285
|
+
insertTurn({ turnKey: 'a:_:100', chatId: '111', threadId: null, startedAt: 100, endedAt: 200 })
|
|
286
|
+
insertTurn({ turnKey: `b:${TOPIC_THREAD}:300`, chatId: TOPIC_CHAT, threadId: TOPIC_THREAD, startedAt: 300, endedAt: null })
|
|
287
|
+
// A turn STARTED AFTER the worker's dispatch must never win the floor.
|
|
288
|
+
insertTurn({ turnKey: 'c:_:900', chatId: '-1007777777777', threadId: null, startedAt: 900, endedAt: 950 })
|
|
289
|
+
insertGapDispatchedWorker({ id: 'toolu_f', jsonlAgentId: 'wkr_f', startedAt: 500 })
|
|
290
|
+
const recent = resolveRecentTurnFallbackChat(db, 'wkr_f')
|
|
291
|
+
expect(recent?.chatId).toBe(TOPIC_CHAT)
|
|
292
|
+
expect(recent?.threadId).toBe(Number(TOPIC_THREAD))
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it('returns null on an empty turns table', () => {
|
|
296
|
+
insertGapDispatchedWorker({ id: 'toolu_g', jsonlAgentId: 'wkr_g', startedAt: 500 })
|
|
297
|
+
expect(resolveRecentTurnFallbackChat(db, 'wkr_g')).toBeNull()
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('returns null when the worker has no subagents row (no dispatch time to bound by)', () => {
|
|
301
|
+
insertTurn({ turnKey: 'a:_:100', chatId: '111', threadId: null, startedAt: 100, endedAt: 200 })
|
|
302
|
+
expect(resolveRecentTurnFallbackChat(db, 'wkr_unknown')).toBeNull()
|
|
303
|
+
})
|
|
304
|
+
})
|