switchroom 0.14.31 → 0.14.32
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 +80 -80
- package/dist/auth-broker/index.js +80 -80
- package/dist/cli/drive-write-pretool.mjs +10 -10
- package/dist/cli/notion-write-pretool.mjs +82 -82
- package/dist/cli/skill-validate-pretool.mjs +72 -72
- package/dist/cli/switchroom.js +357 -357
- package/dist/host-control/main.js +148 -148
- package/dist/vault/approvals/kernel-server.js +82 -82
- package/dist/vault/broker/server.js +83 -83
- package/package.json +1 -1
- package/telegram-plugin/dist/bridge/bridge.js +112 -112
- package/telegram-plugin/dist/gateway/gateway.js +219 -195
- package/telegram-plugin/dist/server.js +160 -160
- package/telegram-plugin/gateway/gateway.ts +56 -9
- package/telegram-plugin/registry/turns-schema.test.ts +34 -0
- package/telegram-plugin/registry/turns-schema.ts +18 -0
|
@@ -427,6 +427,7 @@ import {
|
|
|
427
427
|
recordTurnEnd,
|
|
428
428
|
findLatestTurnIfInterrupted,
|
|
429
429
|
findRecentTurnsForChat,
|
|
430
|
+
getTurnByKey,
|
|
430
431
|
} from '../registry/turns-schema.js'
|
|
431
432
|
import {
|
|
432
433
|
buildResumeInterruptedInbound,
|
|
@@ -1117,6 +1118,41 @@ try {
|
|
|
1117
1118
|
turnsDb = null
|
|
1118
1119
|
}
|
|
1119
1120
|
|
|
1121
|
+
/**
|
|
1122
|
+
* Resolve the chat/thread a background sub-agent was dispatched from, so
|
|
1123
|
+
* its live worker card + handback route back to the originating
|
|
1124
|
+
* conversation (group / forum topic) instead of the operator DM.
|
|
1125
|
+
*
|
|
1126
|
+
* Walks jsonl_agent_id → `subagents.parent_turn_key` →
|
|
1127
|
+
* `turns.chat_id`/`thread_id`. Returns null on any miss so the caller
|
|
1128
|
+
* keeps its existing `allowFrom[0]` DM fallback — best-effort, never
|
|
1129
|
+
* throws out of the worker-card hot path. This restores the chat context
|
|
1130
|
+
* the pinned-card fleet used to carry before it was removed in #1122
|
|
1131
|
+
* (progressDriver is permanently null, so the old fleet lookup always
|
|
1132
|
+
* yielded the DM for a Task dispatched from a group/topic).
|
|
1133
|
+
*/
|
|
1134
|
+
function resolveSubagentOriginChat(
|
|
1135
|
+
agentId: string,
|
|
1136
|
+
): { chatId: string; threadId?: number } | null {
|
|
1137
|
+
if (turnsDb == null) return null
|
|
1138
|
+
try {
|
|
1139
|
+
const sub = getSubagentByJsonlId(turnsDb, agentId)
|
|
1140
|
+
if (sub?.parent_turn_key == null) return null
|
|
1141
|
+
const turn = getTurnByKey(turnsDb, sub.parent_turn_key)
|
|
1142
|
+
if (turn == null || turn.chat_id.length === 0) return null
|
|
1143
|
+
const threadNum =
|
|
1144
|
+
turn.thread_id != null && turn.thread_id.length > 0
|
|
1145
|
+
? Number(turn.thread_id)
|
|
1146
|
+
: NaN
|
|
1147
|
+
return {
|
|
1148
|
+
chatId: turn.chat_id,
|
|
1149
|
+
threadId: Number.isFinite(threadNum) ? threadNum : undefined,
|
|
1150
|
+
}
|
|
1151
|
+
} catch {
|
|
1152
|
+
return null
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1120
1156
|
// ─── Periodic history reaper (#1073) ──────────────────────────────────────
|
|
1121
1157
|
// The init-time prune in history.ts only touched the `messages` table.
|
|
1122
1158
|
// `subagents` and `turns` in registry.db grew unbounded — every Agent()
|
|
@@ -18371,11 +18407,15 @@ void (async () => {
|
|
|
18371
18407
|
handbackEnvValue: process.env.SWITCHROOM_SUBAGENT_HANDBACK,
|
|
18372
18408
|
outcome,
|
|
18373
18409
|
isBackground,
|
|
18374
|
-
|
|
18375
|
-
//
|
|
18376
|
-
//
|
|
18377
|
-
//
|
|
18378
|
-
|
|
18410
|
+
// Route the handback (the worker's result → a synthesized
|
|
18411
|
+
// turn) back to the conversation the Task was dispatched
|
|
18412
|
+
// from, so the result lands where the user asked — not the
|
|
18413
|
+
// agent's DM. Falls back to fleetChatId/ownerChatId.
|
|
18414
|
+
fleetChatId: resolveSubagentOriginChat(agentId)?.chatId || fleetChatId,
|
|
18415
|
+
// Owner-chat fallback: if the parent-turn chat can't be
|
|
18416
|
+
// resolved, route to the owner chat. Every switchroom fleet
|
|
18417
|
+
// agent is DM-shaped, so allowFrom[0] is the conversation
|
|
18418
|
+
// that dispatched.
|
|
18379
18419
|
ownerChatId: loadAccess().allowFrom[0] ?? '',
|
|
18380
18420
|
taskDescription: description,
|
|
18381
18421
|
resultText,
|
|
@@ -18505,12 +18545,16 @@ void (async () => {
|
|
|
18505
18545
|
// message owns the progress beat. Push a running cue and
|
|
18506
18546
|
// return BEFORE the legacy bucket relay so the same activity
|
|
18507
18547
|
// isn't double-surfaced (in-message edit + injected
|
|
18508
|
-
// "still working" inbound turn).
|
|
18509
|
-
//
|
|
18548
|
+
// "still working" inbound turn). Route to the conversation
|
|
18549
|
+
// the Task was dispatched from (group / forum topic) via the
|
|
18550
|
+
// parent turn; fall back to the owner DM when that can't be
|
|
18551
|
+
// resolved (the pinned-card fleet that used to carry the chat
|
|
18552
|
+
// is gone — see resolveSubagentOriginChat).
|
|
18510
18553
|
if (workerFeedEnabled) {
|
|
18554
|
+
const origin = resolveSubagentOriginChat(agentId)
|
|
18511
18555
|
void workerActivityFeed.update(
|
|
18512
18556
|
agentId,
|
|
18513
|
-
fleetChatId || (loadAccess().allowFrom[0] ?? ''),
|
|
18557
|
+
origin?.chatId || fleetChatId || (loadAccess().allowFrom[0] ?? ''),
|
|
18514
18558
|
{
|
|
18515
18559
|
description: dispatch.feedDescription,
|
|
18516
18560
|
lastTool,
|
|
@@ -18519,6 +18563,7 @@ void (async () => {
|
|
|
18519
18563
|
elapsedMs,
|
|
18520
18564
|
state: 'running',
|
|
18521
18565
|
},
|
|
18566
|
+
origin?.threadId,
|
|
18522
18567
|
)
|
|
18523
18568
|
return
|
|
18524
18569
|
}
|
|
@@ -18526,7 +18571,9 @@ void (async () => {
|
|
|
18526
18571
|
const decision = decideSubagentProgress({
|
|
18527
18572
|
disableEnvValue: process.env.SWITCHROOM_DISABLE_SUBAGENT_PROGRESS,
|
|
18528
18573
|
isBackground,
|
|
18529
|
-
|
|
18574
|
+
// Prefer the conversation the Task was dispatched from over
|
|
18575
|
+
// the owner DM (see resolveSubagentOriginChat).
|
|
18576
|
+
fleetChatId: resolveSubagentOriginChat(agentId)?.chatId || fleetChatId,
|
|
18530
18577
|
ownerChatId: loadAccess().allowFrom[0] ?? '',
|
|
18531
18578
|
subagentJsonlId: agentId,
|
|
18532
18579
|
taskDescription: description,
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
recordTurnStart,
|
|
21
21
|
recordTurnEnd,
|
|
22
22
|
findRecentTurnsForChat,
|
|
23
|
+
getTurnByKey,
|
|
23
24
|
} from './turns-schema.js'
|
|
24
25
|
|
|
25
26
|
// ---------------------------------------------------------------------------
|
|
@@ -99,3 +100,36 @@ describe('findRecentTurnsForChat', () => {
|
|
|
99
100
|
db.close()
|
|
100
101
|
})
|
|
101
102
|
})
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// getTurnByKey — recover the dispatch chat/thread for a sub-agent's parent
|
|
106
|
+
// turn (subagents.parent_turn_key -> turns.turn_key). Without this the
|
|
107
|
+
// worker card / handback fall back to the operator DM (#worker-card-routing).
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
describe('getTurnByKey', () => {
|
|
111
|
+
it('returns null when the turn key does not exist', () => {
|
|
112
|
+
const db = openTurnsDbInMemory()
|
|
113
|
+
expect(getTurnByKey(db, 'nope')).toBeNull()
|
|
114
|
+
db.close()
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('recovers chat_id + thread_id for a group/topic turn', () => {
|
|
118
|
+
const db = openTurnsDbInMemory()
|
|
119
|
+
recordTurnStart(db, { turnKey: 'g:11', chatId: '-1001234567890', threadId: '42' })
|
|
120
|
+
const turn = getTurnByKey(db, 'g:11')
|
|
121
|
+
expect(turn?.turn_key).toBe('g:11')
|
|
122
|
+
expect(turn?.chat_id).toBe('-1001234567890')
|
|
123
|
+
expect(turn?.thread_id).toBe('42')
|
|
124
|
+
db.close()
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('recovers chat_id with null thread_id for a plain group/DM turn', () => {
|
|
128
|
+
const db = openTurnsDbInMemory()
|
|
129
|
+
recordTurnStart(db, { turnKey: 'dm:7', chatId: '12345' })
|
|
130
|
+
const turn = getTurnByKey(db, 'dm:7')
|
|
131
|
+
expect(turn?.chat_id).toBe('12345')
|
|
132
|
+
expect(turn?.thread_id).toBeNull()
|
|
133
|
+
db.close()
|
|
134
|
+
})
|
|
135
|
+
})
|
|
@@ -348,6 +348,24 @@ export function findOrphanedTurns(db: SqliteDatabase, chatId: string): Turn[] {
|
|
|
348
348
|
return rows.map(mapRow)
|
|
349
349
|
}
|
|
350
350
|
|
|
351
|
+
/**
|
|
352
|
+
* Fetch a single turn by its primary key, or null if absent.
|
|
353
|
+
*
|
|
354
|
+
* Used to recover the chat/thread a background sub-agent was dispatched
|
|
355
|
+
* from: `subagents.parent_turn_key` is an FK-by-convention to
|
|
356
|
+
* `turns.turn_key`, so this resolves the originating conversation
|
|
357
|
+
* (chat_id + thread_id) for a worker card / handback. Without it the
|
|
358
|
+
* worker feed falls back to the operator DM (the pinned-card fleet that
|
|
359
|
+
* used to carry the chat was removed in #1122), so a Task dispatched from
|
|
360
|
+
* a group/topic posted its progress to the agent's DM instead.
|
|
361
|
+
*/
|
|
362
|
+
export function getTurnByKey(db: SqliteDatabase, turnKey: string): Turn | null {
|
|
363
|
+
const row = db
|
|
364
|
+
.prepare(`SELECT * FROM turns WHERE turn_key = ?`)
|
|
365
|
+
.get(turnKey) as RawTurnRow | undefined
|
|
366
|
+
return row ? mapRow(row) : null
|
|
367
|
+
}
|
|
368
|
+
|
|
351
369
|
export interface OrphanClassifyOpts {
|
|
352
370
|
/**
|
|
353
371
|
* `turnKey` from the on-disk `turn-active.json` marker — the single
|