switchroom 0.19.30 → 0.19.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/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/bridge/bridge.js +3 -0
- package/telegram-plugin/dist/gateway/gateway.js +1660 -677
- package/telegram-plugin/dist/server.js +3 -0
- 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/gateway/stream-render.ts +578 -321
- package/telegram-plugin/registry/turns-schema.ts +21 -1
- package/telegram-plugin/retry-api-call.ts +46 -21
- package/telegram-plugin/session-tail.ts +13 -0
- 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/stream-render-golden.test.ts +20 -5
- package/telegram-plugin/tests/tg-post-logger-error-shape.test.ts +161 -0
- package/telegram-plugin/tests/turn-mint-defers-until-dequeue.test.ts +196 -0
- package/telegram-plugin/tests/turn-mint-harness.ts +155 -0
- package/telegram-plugin/tests/turn-supersede-finalizes-prior-card.test.ts +124 -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
|
@@ -79,6 +79,15 @@ export interface StatusPinClaim {
|
|
|
79
79
|
messageId: number
|
|
80
80
|
/** Chat the pin lives in. Never empty — the reconcile refuses an empty chat. */
|
|
81
81
|
chatId: string
|
|
82
|
+
/**
|
|
83
|
+
* Forum topic the pinned message lives in, when known.
|
|
84
|
+
*
|
|
85
|
+
* The claim is keyed by `(chat, thread)`. It is NOT needed to unpin this one
|
|
86
|
+
* message (`unpinChatMessage` takes no `message_thread_id`), but it is the
|
|
87
|
+
* only thing that lets a sweep aim `unpinAllForumTopicMessages` at the right
|
|
88
|
+
* topic when a crash leaves a chat-wide stack of orphans behind.
|
|
89
|
+
*/
|
|
90
|
+
threadId?: number
|
|
82
91
|
/** Wall-clock ms the claim was FIRST taken (survives a same-id re-pin). */
|
|
83
92
|
pinnedAt: number
|
|
84
93
|
}
|
|
@@ -86,6 +95,9 @@ export interface StatusPinClaim {
|
|
|
86
95
|
export interface StatusPinReconcileArgs {
|
|
87
96
|
pinKey: string
|
|
88
97
|
chatId: string
|
|
98
|
+
/** Forum topic the pinned message lives in, when the caller knows it. Carried
|
|
99
|
+
* into both the in-memory claim and the durable row (see StatusPinClaim). */
|
|
100
|
+
threadId?: number
|
|
89
101
|
/** The claim held for this key when the reconcile started. */
|
|
90
102
|
prev: PinState | null
|
|
91
103
|
/** What the caller wants pinned for this key now. */
|
|
@@ -116,7 +128,7 @@ export interface StatusPinReconcileArgs {
|
|
|
116
128
|
* is read once by the caller and both legs run inside that critical section.
|
|
117
129
|
*/
|
|
118
130
|
export async function runStatusPinReconcile(args: StatusPinReconcileArgs): Promise<void> {
|
|
119
|
-
const { pinKey, chatId, prev, desired, persist, runPin, claims } = args
|
|
131
|
+
const { pinKey, chatId, threadId, prev, desired, persist, runPin, claims } = args
|
|
120
132
|
const now = args.now ?? Date.now
|
|
121
133
|
|
|
122
134
|
// Publish a leg's outcome into the single claim registry. `pinnedAt` is
|
|
@@ -128,7 +140,7 @@ export async function runStatusPinReconcile(args: StatusPinReconcileArgs): Promi
|
|
|
128
140
|
return
|
|
129
141
|
}
|
|
130
142
|
const pinnedAt = claims.get(pinKey)?.pinnedAt ?? now()
|
|
131
|
-
claims.set(pinKey, { messageId: next.messageId, chatId, pinnedAt })
|
|
143
|
+
claims.set(pinKey, { messageId: next.messageId, chatId, threadId, pinnedAt })
|
|
132
144
|
}
|
|
133
145
|
|
|
134
146
|
// One leg, with the persist ordering its action requires. Only a fresh `pin`
|
|
@@ -152,6 +164,7 @@ export async function runStatusPinReconcile(args: StatusPinReconcileArgs): Promi
|
|
|
152
164
|
fs: persist.fs,
|
|
153
165
|
pinKey,
|
|
154
166
|
chatId,
|
|
167
|
+
threadId,
|
|
155
168
|
op,
|
|
156
169
|
applyPin: () => runPin(legAction, from),
|
|
157
170
|
now: now(),
|
|
@@ -55,6 +55,23 @@ export interface StatusPinStoreFsSeam {
|
|
|
55
55
|
export interface PersistedStatusPin {
|
|
56
56
|
pinKey: string
|
|
57
57
|
chatId: string
|
|
58
|
+
/**
|
|
59
|
+
* Forum topic the pinned message lives in, when the caller knows it.
|
|
60
|
+
*
|
|
61
|
+
* The durable claim is keyed by `(chat, thread)`, not by chat alone. Telegram's
|
|
62
|
+
* pin stack is CHAT-WIDE — a "topic pin" is a chat-level pin whose message
|
|
63
|
+
* happens to sit in that thread, and `pinChatMessage`/`unpinChatMessage` take
|
|
64
|
+
* no `message_thread_id` at all. So the thread is NOT needed to unpin one
|
|
65
|
+
* known message; it is needed to DRAIN a topic, because the only topic-scoped
|
|
66
|
+
* pin verb a bot has is `unpinAllForumTopicMessages(chat_id, message_thread_id)`.
|
|
67
|
+
*
|
|
68
|
+
* Without this field a row records "there were orphan pins in forum chat X"
|
|
69
|
+
* and nothing more, so a boot after a crash cannot aim the one verb that would
|
|
70
|
+
* clear them — which is how a chat-wide stack grows on every restart. Optional
|
|
71
|
+
* so a v1–v3 snapshot still loads; a row without it degrades to the
|
|
72
|
+
* chat-level path.
|
|
73
|
+
*/
|
|
74
|
+
threadId?: number
|
|
58
75
|
messageId: number
|
|
59
76
|
/** True while the pin API call is in-flight / unconfirmed (see above). */
|
|
60
77
|
pending?: boolean
|
|
@@ -102,16 +119,17 @@ export const BOOT_UNPIN_MAX_ATTEMPTS = 5
|
|
|
102
119
|
|
|
103
120
|
/** Envelope version. v1 had no `pending` field; a v1 row loads as a confirmed
|
|
104
121
|
* pin (pending undefined). v2 adds the optional `pending` flag. v3 adds the
|
|
105
|
-
* optional `pinnedAt` claim timestamp (#3810).
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
122
|
+
* optional `pinnedAt` claim timestamp (#3810). v4 adds the optional `threadId`
|
|
123
|
+
* so the claim is keyed by (chat, thread) and a forum topic can be drained
|
|
124
|
+
* after a crash. All load fail-open — an unknown/newer version yields [].
|
|
125
|
+
* Every field added since v1 is optional, so the versions are mutually
|
|
126
|
+
* readable and a downgrade degrades rather than breaks. */
|
|
109
127
|
interface SnapshotEnvelope {
|
|
110
|
-
v: 1 | 2 | 3
|
|
128
|
+
v: 1 | 2 | 3 | 4
|
|
111
129
|
pins: PersistedStatusPin[]
|
|
112
130
|
}
|
|
113
131
|
|
|
114
|
-
const SNAPSHOT_VERSIONS = new Set([1, 2, 3])
|
|
132
|
+
const SNAPSHOT_VERSIONS = new Set([1, 2, 3, 4])
|
|
115
133
|
|
|
116
134
|
function isPinRow(x: unknown): x is PersistedStatusPin {
|
|
117
135
|
if (x == null || typeof x !== 'object') return false
|
|
@@ -121,6 +139,7 @@ function isPinRow(x: unknown): x is PersistedStatusPin {
|
|
|
121
139
|
o.pinKey.length > 0 &&
|
|
122
140
|
typeof o.chatId === 'string' &&
|
|
123
141
|
o.chatId.length > 0 &&
|
|
142
|
+
(o.threadId === undefined || typeof o.threadId === 'number') &&
|
|
124
143
|
typeof o.messageId === 'number' &&
|
|
125
144
|
(o.pending === undefined || typeof o.pending === 'boolean') &&
|
|
126
145
|
(o.expiresAt === undefined || typeof o.expiresAt === 'number') &&
|
|
@@ -170,7 +189,7 @@ export function persistStatusPins(
|
|
|
170
189
|
snapshot: readonly PersistedStatusPin[],
|
|
171
190
|
log: (line: string) => void = (l) => process.stderr.write(l),
|
|
172
191
|
): void {
|
|
173
|
-
const env: SnapshotEnvelope = { v:
|
|
192
|
+
const env: SnapshotEnvelope = { v: 4, pins: [...snapshot] }
|
|
174
193
|
const tmp = path + '.tmp'
|
|
175
194
|
try {
|
|
176
195
|
fs.writeFileSync(tmp, JSON.stringify(env))
|
|
@@ -450,6 +469,9 @@ export function reconcileAndPersistStatusPin(args: {
|
|
|
450
469
|
fs: StatusPinStoreFsSeam
|
|
451
470
|
pinKey: string
|
|
452
471
|
chatId: string
|
|
472
|
+
/** Forum topic the pinned message lives in, when known — persisted so a boot
|
|
473
|
+
* after a crash can aim `unpinAllForumTopicMessages` at the right topic. */
|
|
474
|
+
threadId?: number
|
|
453
475
|
op: StatusPinPersistOp
|
|
454
476
|
/** Execute the real pin/unpin; returns the confirmed message id (pin) or
|
|
455
477
|
* null (cleared). Must never throw — API errors are swallowed inside. */
|
|
@@ -458,7 +480,7 @@ export function reconcileAndPersistStatusPin(args: {
|
|
|
458
480
|
now?: number
|
|
459
481
|
log?: (line: string) => void
|
|
460
482
|
}): Promise<{ messageId: number } | null> {
|
|
461
|
-
const { path, fs, pinKey, chatId, op } = args
|
|
483
|
+
const { path, fs, pinKey, chatId, threadId, op } = args
|
|
462
484
|
const log = args.log ?? ((l: string) => process.stderr.write(l))
|
|
463
485
|
const now = args.now ?? Date.now()
|
|
464
486
|
|
|
@@ -489,7 +511,7 @@ export function reconcileAndPersistStatusPin(args: {
|
|
|
489
511
|
path,
|
|
490
512
|
fs,
|
|
491
513
|
pinKey,
|
|
492
|
-
{ pinKey, chatId, messageId: op.messageId, pending: true, pinnedAt },
|
|
514
|
+
{ pinKey, chatId, threadId, messageId: op.messageId, pending: true, pinnedAt },
|
|
493
515
|
log,
|
|
494
516
|
)
|
|
495
517
|
const next = await args.applyPin()
|
|
@@ -504,7 +526,7 @@ export function reconcileAndPersistStatusPin(args: {
|
|
|
504
526
|
path,
|
|
505
527
|
fs,
|
|
506
528
|
pinKey,
|
|
507
|
-
{ pinKey, chatId, messageId: next.messageId, pinnedAt: claimAge(next.messageId) },
|
|
529
|
+
{ pinKey, chatId, threadId, messageId: next.messageId, pinnedAt: claimAge(next.messageId) },
|
|
508
530
|
log,
|
|
509
531
|
)
|
|
510
532
|
return next
|
|
@@ -541,7 +563,7 @@ export function reconcileAndPersistStatusPin(args: {
|
|
|
541
563
|
path,
|
|
542
564
|
fs,
|
|
543
565
|
pinKey,
|
|
544
|
-
{ pinKey, chatId, messageId: next.messageId, pinnedAt: claimAge(next.messageId) },
|
|
566
|
+
{ pinKey, chatId, threadId, messageId: next.messageId, pinnedAt: claimAge(next.messageId) },
|
|
545
567
|
log,
|
|
546
568
|
)
|
|
547
569
|
}
|