switchroom 0.19.30 → 0.19.31
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 +1121 -584
- package/dist/host-control/main.js +151 -73
- package/package.json +1 -1
- package/profiles/_base/cron-session.sh.hbs +5 -1
- package/profiles/_base/start.sh.hbs +15 -1
- package/telegram-plugin/dist/gateway/gateway.js +1435 -551
- package/telegram-plugin/edit-flood-fuse.ts +70 -20
- package/telegram-plugin/gateway/boot-beacon.ts +364 -0
- package/telegram-plugin/gateway/boot-sweep-gate.ts +20 -15
- package/telegram-plugin/gateway/gateway.ts +87 -88
- package/telegram-plugin/gateway/inbound-spool.ts +39 -0
- package/telegram-plugin/gateway/narrative-lane.ts +12 -0
- package/telegram-plugin/gateway/obligation-store.ts +28 -0
- package/telegram-plugin/gateway/stale-pin-sweep-store.ts +221 -0
- package/telegram-plugin/gateway/stale-pin-sweep-wiring.ts +211 -0
- package/telegram-plugin/gateway/stale-pin-sweep.test.ts +804 -0
- package/telegram-plugin/gateway/stale-pin-sweep.ts +1146 -0
- package/telegram-plugin/gateway/status-pin-retarget.ts +15 -2
- package/telegram-plugin/gateway/status-pin-store.ts +33 -11
- package/telegram-plugin/registry/turns-schema.ts +21 -1
- package/telegram-plugin/retry-api-call.ts +46 -21
- package/telegram-plugin/shared/bot-runtime.ts +61 -17
- package/telegram-plugin/shared/gw-trace-gate.ts +18 -2
- package/telegram-plugin/tests/activity-card-wiring.test.ts +7 -7
- package/telegram-plugin/tests/activity-drain-fuse-drop-not-failure.test.ts +324 -0
- package/telegram-plugin/tests/agent-card-result-footer.test.ts +193 -0
- package/telegram-plugin/tests/boot-beacon.test.ts +462 -0
- package/telegram-plugin/tests/boot-pin-sweep-wiring.test.ts +6 -6
- package/telegram-plugin/tests/boot-sweep-gate.test.ts +42 -31
- package/telegram-plugin/tests/inbound-delivery-machine-dispatch.test.ts +2 -0
- package/telegram-plugin/tests/inbound-spool-progress.test.ts +2 -0
- package/telegram-plugin/tests/inbound-spool.test.ts +134 -5
- package/telegram-plugin/tests/narrative-lane-golden.test.ts +28 -0
- package/telegram-plugin/tests/obligation-determinism.test.ts +2 -0
- package/telegram-plugin/tests/obligation-store.test.ts +67 -1
- package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +3 -3
- package/telegram-plugin/tests/status-pin-store.test.ts +26 -5
- package/telegram-plugin/tests/tg-post-logger-error-shape.test.ts +161 -0
- package/telegram-plugin/tests/worker-feed-pin-persistence.test.ts +30 -0
- package/telegram-plugin/tool-activity-summary.ts +104 -38
- package/telegram-plugin/worker-activity-feed.ts +33 -16
- package/telegram-plugin/gateway/dm-pin-sweep.test.ts +0 -251
- package/telegram-plugin/gateway/dm-pin-sweep.ts +0 -178
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DM pin sweep (#3026) — durable clearing of stale bot-authored pins in
|
|
3
|
-
* user DM chats.
|
|
4
|
-
*
|
|
5
|
-
* Why this exists (two independent gaps the probe-based boot sweep can't
|
|
6
|
-
* close for DMs):
|
|
7
|
-
*
|
|
8
|
-
* 1. The boot pin sweep (`shouldSweepChatAtBoot`) deliberately SKIPS
|
|
9
|
-
* positive (DM) chat IDs — the Bot API returns `400 chat not found`
|
|
10
|
-
* for a user who never messaged the bot. So DM chats were never
|
|
11
|
-
* boot-swept at all, and stale pins from a prior session lingered
|
|
12
|
-
* across restarts (observed fleet-wide 2026-07-11).
|
|
13
|
-
*
|
|
14
|
-
* 2. Even when a DM chat IS reachable, `getChat()` exposes ONLY the
|
|
15
|
-
* NEWEST pinned message. Telegram DMs STACK pins and the Bot API has
|
|
16
|
-
* no list-pins method, so older orphan pins are invisible to a
|
|
17
|
-
* probe-based (unpin-one-by-message-id) sweep forever.
|
|
18
|
-
*
|
|
19
|
-
* The durable fix, DM-only: call `unpinAllChatMessages` ONCE per DM chat
|
|
20
|
-
* per gateway boot (clearing the whole stack — safe in a DM, where
|
|
21
|
-
* virtually every pin is bot-authored), then immediately RE-PIN the live
|
|
22
|
-
* tracked cards from the in-memory pin claims (silent, `disable_notification`)
|
|
23
|
-
* so a genuinely in-flight status/worker pin survives the sweep.
|
|
24
|
-
*
|
|
25
|
-
* Groups/supergroups (negative IDs) are NOT touched here — an unpin-all
|
|
26
|
-
* there would nuke human pins. They keep the existing probe-based path.
|
|
27
|
-
*
|
|
28
|
-
* This module is the PURE half (chat-id classification, store→DM-chat-id
|
|
29
|
-
* collection, and a dependency-injected sweeper with the once-per-boot
|
|
30
|
-
* guard). The gateway binds the live Bot API + in-memory claim map.
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* True iff `chatId` is a user DM chat: a positive integer. Group and
|
|
35
|
-
* supergroup IDs are negative; zero / non-numeric / non-integer are
|
|
36
|
-
* malformed and never DMs.
|
|
37
|
-
*/
|
|
38
|
-
export function isDmChatId(chatId: string): boolean {
|
|
39
|
-
const n = Number(chatId)
|
|
40
|
-
return Number.isFinite(n) && Number.isInteger(n) && n > 0
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Collect the DISTINCT DM chat IDs that appear in ANY of the persisted pin
|
|
45
|
-
* stores — the set of DM chats the gateway has a record of having pinned
|
|
46
|
-
* something in during a prior session. These are the DM chats worth
|
|
47
|
-
* sweeping at boot (an unpin-all is wasted on a DM we never pinned in).
|
|
48
|
-
*
|
|
49
|
-
* Pure over its input records; callers pass the loaded store snapshots
|
|
50
|
-
* (status pins, activity cards, queued/busy-ack cards). Non-DM chat IDs
|
|
51
|
-
* are filtered out.
|
|
52
|
-
*/
|
|
53
|
-
export function collectDmChatIdsFromStores(input: {
|
|
54
|
-
statusPins?: readonly { chatId: string }[]
|
|
55
|
-
activityCards?: readonly { chatId: string }[]
|
|
56
|
-
queuedCards?: readonly { chatId: string }[]
|
|
57
|
-
}): string[] {
|
|
58
|
-
const out = new Set<string>()
|
|
59
|
-
const add = (rows?: readonly { chatId: string }[]): void => {
|
|
60
|
-
if (rows == null) return
|
|
61
|
-
for (const r of rows) if (isDmChatId(r.chatId)) out.add(r.chatId)
|
|
62
|
-
}
|
|
63
|
-
add(input.statusPins)
|
|
64
|
-
add(input.activityCards)
|
|
65
|
-
add(input.queuedCards)
|
|
66
|
-
return [...out]
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* The messageIds in `chatId` that must SURVIVE a DM unpin-all because they
|
|
71
|
-
* belong to deliberately-retained store rows: unexpired time-scoped `tool:`
|
|
72
|
-
* pins (the `pin_message` MCP tool, #3001), which `runStatusPinBootCleanup`
|
|
73
|
-
* intentionally KEEPS across restarts. Work-scoped rows (no `expiresAt`) are
|
|
74
|
-
* stale by definition after a restart and are NOT re-pin candidates; expired
|
|
75
|
-
* time-scoped rows are due for sweeping.
|
|
76
|
-
*
|
|
77
|
-
* Pure over a loaded store snapshot; the gateway binds a LIVE store read into
|
|
78
|
-
* `liveTrackedMessageIds` so BOTH the boot sweep and a first-inbound sweep
|
|
79
|
-
* see the kept rows (the in-memory claim maps are empty at boot).
|
|
80
|
-
*/
|
|
81
|
-
export function unexpiredStoreRepinIds(
|
|
82
|
-
rows: readonly { chatId: string; messageId: number; expiresAt?: number }[],
|
|
83
|
-
chatId: string,
|
|
84
|
-
now: number,
|
|
85
|
-
): number[] {
|
|
86
|
-
const out: number[] = []
|
|
87
|
-
for (const r of rows) {
|
|
88
|
-
if (r.chatId !== chatId) continue
|
|
89
|
-
if (r.expiresAt != null && r.expiresAt > now) out.push(r.messageId)
|
|
90
|
-
}
|
|
91
|
-
return out
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export interface DmPinSweeperDeps {
|
|
95
|
-
/** Clear EVERY pinned message in a chat. Bound to the gateway's
|
|
96
|
-
* robust/retry API wrapper so a 429 flood-wait is retried, then
|
|
97
|
-
* degrades (rejects) rather than crashing — the sweeper absorbs it. */
|
|
98
|
-
unpinAll: (chatId: string) => Promise<unknown>
|
|
99
|
-
/** Silently re-pin an EXISTING message (disable_notification). Bound to
|
|
100
|
-
* the gateway's robust/retry API wrapper. */
|
|
101
|
-
pinSilent: (chatId: string, messageId: number) => Promise<unknown>
|
|
102
|
-
/** Pin message ids in this chat that must survive the unpin-all: the
|
|
103
|
-
* in-memory status-pin claims still owned by an in-flight turn/worker,
|
|
104
|
-
* UNIONED with the deliberately-retained store rows (unexpired `tool:`
|
|
105
|
-
* pins, #3001 — see unexpiredStoreRepinIds). The gateway binds both
|
|
106
|
-
* sources; duplicates are deduped by the sweeper before re-pinning. */
|
|
107
|
-
liveTrackedMessageIds: (chatId: string) => number[]
|
|
108
|
-
/** Gate: sweep ONLY when the gateway won the startup mutex. The stores
|
|
109
|
-
* and the chat's pins are shared per-agent state; a LOSING double-boot
|
|
110
|
-
* must never unpin the live holder's legitimate pins. */
|
|
111
|
-
eligible: () => boolean
|
|
112
|
-
/** Optional structured log sink. */
|
|
113
|
-
log?: (line: string) => void
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export interface DmPinSweeper {
|
|
117
|
-
/**
|
|
118
|
-
* Sweep a single DM chat: unpin-all then re-pin live tracked cards.
|
|
119
|
-
* At most ONCE per chat per sweeper lifetime (one gateway boot). No-op
|
|
120
|
-
* for non-DM chats, when not eligible, or when already swept. Never
|
|
121
|
-
* throws — a failed unpin-all/re-pin degrades and is logged.
|
|
122
|
-
*/
|
|
123
|
-
sweep(chatId: string): Promise<void>
|
|
124
|
-
/** True if this chat has already been swept this boot (test/observability). */
|
|
125
|
-
hasSwept(chatId: string): boolean
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export function createDmPinSweeper(deps: DmPinSweeperDeps): DmPinSweeper {
|
|
129
|
-
const swept = new Set<string>()
|
|
130
|
-
return {
|
|
131
|
-
hasSwept: (chatId) => swept.has(chatId),
|
|
132
|
-
async sweep(chatId: string): Promise<void> {
|
|
133
|
-
if (!isDmChatId(chatId)) return
|
|
134
|
-
if (!deps.eligible()) return
|
|
135
|
-
if (swept.has(chatId)) return
|
|
136
|
-
// Claim the once-guard BEFORE the first await so two concurrent
|
|
137
|
-
// triggers (boot + first-inbound landing in the same tick) can't
|
|
138
|
-
// both fire the unpin-all.
|
|
139
|
-
swept.add(chatId)
|
|
140
|
-
|
|
141
|
-
// Snapshot live claims BEFORE the unpin-all so we know exactly which
|
|
142
|
-
// messages to restore. Dedupe — the in-memory and store-row sources
|
|
143
|
-
// can both report the same messageId.
|
|
144
|
-
const liveIds = [...new Set(deps.liveTrackedMessageIds(chatId))]
|
|
145
|
-
|
|
146
|
-
try {
|
|
147
|
-
await deps.unpinAll(chatId)
|
|
148
|
-
} catch (err) {
|
|
149
|
-
const msg = err instanceof Error ? err.message : String(err)
|
|
150
|
-
deps.log?.(
|
|
151
|
-
`telegram gateway: dm-pin-sweep: unpin-all failed (chat=${chatId}): ${msg}\n`,
|
|
152
|
-
)
|
|
153
|
-
// Degrade: the stack may still hold stale pins, but we don't
|
|
154
|
-
// crash and we don't retry this boot. Fall through to re-pin the
|
|
155
|
-
// live claims (harmless if the unpin didn't land).
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
for (const messageId of liveIds) {
|
|
159
|
-
try {
|
|
160
|
-
await deps.pinSilent(chatId, messageId)
|
|
161
|
-
} catch (err) {
|
|
162
|
-
const msg = err instanceof Error ? err.message : String(err)
|
|
163
|
-
deps.log?.(
|
|
164
|
-
`telegram gateway: dm-pin-sweep: re-pin failed ` +
|
|
165
|
-
`(chat=${chatId} msg=${messageId}): ${msg}\n`,
|
|
166
|
-
)
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (liveIds.length > 0) {
|
|
171
|
-
deps.log?.(
|
|
172
|
-
`telegram gateway: dm-pin-sweep: cleared stacked pins in DM ${chatId}, ` +
|
|
173
|
-
`re-pinned ${liveIds.length} live card(s)\n`,
|
|
174
|
-
)
|
|
175
|
-
}
|
|
176
|
-
},
|
|
177
|
-
}
|
|
178
|
-
}
|