switchroom 0.18.15 → 0.18.17
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 +3 -0
- package/dist/auth-broker/index.js +432 -10
- package/dist/cli/notion-write-pretool.mjs +3 -0
- package/dist/cli/switchroom.js +50 -1
- package/dist/host-control/main.js +4 -1
- package/dist/vault/approvals/kernel-server.js +3 -0
- package/dist/vault/broker/server.js +3 -0
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +81 -139
- package/telegram-plugin/dist/gateway/gateway.js +386 -259
- package/telegram-plugin/draft-stream.ts +78 -3
- package/telegram-plugin/gateway/bridge-dead-watchdog.ts +3 -4
- package/telegram-plugin/gateway/effort-command.ts +9 -7
- package/telegram-plugin/gateway/gateway.ts +265 -220
- package/telegram-plugin/gateway/litellm-local-notice-wiring.ts +200 -0
- package/telegram-plugin/gateway/model-command.ts +96 -18
- package/telegram-plugin/gateway/pending-session-command.ts +10 -8
- package/telegram-plugin/gateway/session-model-file.ts +38 -172
- package/telegram-plugin/litellm-local-notice.ts +189 -0
- package/telegram-plugin/quota-watch.ts +16 -4
- package/telegram-plugin/runtime-metrics.ts +16 -0
- package/telegram-plugin/send-gate-degraded.test.ts +9 -7
- package/telegram-plugin/send-gate.ts +34 -4
- package/telegram-plugin/stream-controller.ts +143 -20
- package/telegram-plugin/stream-reply-handler.ts +12 -2
- package/telegram-plugin/tests/bot-api.harness.ts +7 -2
- package/telegram-plugin/tests/draft-stream.test.ts +110 -1
- package/telegram-plugin/tests/effort-command.test.ts +4 -4
- package/telegram-plugin/tests/flood-windows-persistence.test.ts +2 -2
- package/telegram-plugin/tests/gateway-pending-command-wiring.test.ts +33 -19
- package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +47 -127
- package/telegram-plugin/tests/litellm-local-notice.test.ts +417 -0
- package/telegram-plugin/tests/model-command.test.ts +84 -1
- package/telegram-plugin/tests/quota-watch.test.ts +21 -0
- package/telegram-plugin/tests/reaction-gate-routing.test.ts +2 -2
- package/telegram-plugin/tests/session-model-file.test.ts +7 -155
- package/telegram-plugin/tests/stream-controller-send-gate.test.ts +521 -0
- package/telegram-plugin/tests/stream-reply-handler.test.ts +44 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +207 -0
- package/telegram-plugin/worker-activity-feed.ts +83 -8
|
@@ -26,6 +26,40 @@
|
|
|
26
26
|
|
|
27
27
|
const TELEGRAM_MAX_CHARS = 32768
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Error a transport layer (stream-controller.ts) throws from its edit
|
|
31
|
+
* callback when the send gate SHED the edit — it did NOT land, and the
|
|
32
|
+
* stream must not record the snapshot as on-screen (#3110). Marked with a
|
|
33
|
+
* property (not a message pattern) so the classification is exact.
|
|
34
|
+
*
|
|
35
|
+
* `flush()` recognizes this and PRESERVES the snapshot in `shedText` so a
|
|
36
|
+
* later `finalize()` — including the argument-less finalize the gateway's
|
|
37
|
+
* cleanup paths use — re-flushes it as the stream's final state instead of
|
|
38
|
+
* silently losing the content (review F2). It deliberately does NOT restore
|
|
39
|
+
* `pendingText`: `flushLoop` drains while `pendingText != null`, and a
|
|
40
|
+
* restore there would busy-spin the loop against an open flood window.
|
|
41
|
+
*/
|
|
42
|
+
export interface DraftEditShedError extends Error {
|
|
43
|
+
draftEditShed: true
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Build the marker error a transport throws for a gate-shed edit. */
|
|
47
|
+
export function makeDraftEditShedError(messageId: number | null): DraftEditShedError {
|
|
48
|
+
return Object.assign(
|
|
49
|
+
new Error(
|
|
50
|
+
`draft edit shed by send gate (id=${messageId ?? 'unknown'}); snapshot preserved for re-flush`,
|
|
51
|
+
),
|
|
52
|
+
{ draftEditShed: true as const },
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** True when `err` is the shed marker thrown by a stream transport. */
|
|
57
|
+
export function isDraftEditShedError(err: unknown): err is DraftEditShedError {
|
|
58
|
+
return (
|
|
59
|
+
err instanceof Error && (err as Partial<DraftEditShedError>).draftEditShed === true
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
29
63
|
// Throttle defaults for the in-place engine.
|
|
30
64
|
// DM chats: 400 ms — slightly more responsive than groups while staying
|
|
31
65
|
// well under Telegram's practical ~1 edit/sec/message ceiling. This
|
|
@@ -116,8 +150,17 @@ export interface DraftStreamHandle {
|
|
|
116
150
|
* Mark the stream as final. Flushes any pending text and rejects all
|
|
117
151
|
* future update() calls. Returns a promise that resolves once the final
|
|
118
152
|
* edit has landed (or the initial send if no edits ever fired).
|
|
153
|
+
*
|
|
154
|
+
* When `finalText` is provided, it becomes the pending snapshot for the
|
|
155
|
+
* final flush (superseding any older pending draft — last-write-wins).
|
|
156
|
+
* Callers that know a text is the LAST one (e.g. `stream_reply`
|
|
157
|
+
* `done=true`) MUST pass it here instead of `update(text)` +
|
|
158
|
+
* `finalize()`: the flush then runs with the stream already final, so
|
|
159
|
+
* the transport layer (stream-controller) classifies the edit that
|
|
160
|
+
* renders the completed answer as `critical` for the send gate — never
|
|
161
|
+
* shed as a cosmetic draft under flood pressure (#3110).
|
|
119
162
|
*/
|
|
120
|
-
finalize(): Promise<void>
|
|
163
|
+
finalize(finalText?: string): Promise<void>
|
|
121
164
|
|
|
122
165
|
/** Returns the captured Telegram message_id, or null if nothing has sent yet. */
|
|
123
166
|
getMessageId(): number | null
|
|
@@ -163,6 +206,14 @@ export function createDraftStream(
|
|
|
163
206
|
let messageId: number | null = config.initialMessageId ?? null
|
|
164
207
|
let pendingText: string | null = null
|
|
165
208
|
let lastSentText: string | null = null
|
|
209
|
+
/**
|
|
210
|
+
* Snapshot of the newest text the transport reported as SHED (thrown
|
|
211
|
+
* `DraftEditShedError`) — content that never landed. Cleared on any
|
|
212
|
+
* successful flush (a newer snapshot superseded it) and consumed by
|
|
213
|
+
* `finalize()` so the stream's last state is re-delivered once the
|
|
214
|
+
* pressure clears instead of being lost (review F2).
|
|
215
|
+
*/
|
|
216
|
+
let shedText: string | null = null
|
|
166
217
|
let lastSentAt = 0
|
|
167
218
|
let inFlight: Promise<void> | null = null
|
|
168
219
|
// Observability — per-stream fire counters for the stream-end trace.
|
|
@@ -216,11 +267,21 @@ export function createDraftStream(
|
|
|
216
267
|
await sendViaMessage(textToSend)
|
|
217
268
|
lastSentText = textToSend
|
|
218
269
|
lastSentAt = Date.now()
|
|
270
|
+
shedText = null // a newer snapshot landed — drop any older shed one
|
|
219
271
|
} catch (err) {
|
|
220
272
|
const msg = (err as Error).message ?? String(err)
|
|
221
|
-
if (
|
|
273
|
+
if (isDraftEditShedError(err)) {
|
|
274
|
+
// #3110 review F2: the send gate shed this edit — it did NOT land.
|
|
275
|
+
// Preserve the snapshot for finalize()'s re-flush (as the stream's
|
|
276
|
+
// final state, sent critical) instead of silently losing it. Do NOT
|
|
277
|
+
// restore pendingText: flushLoop drains while pendingText != null
|
|
278
|
+
// and would busy-spin against an open flood window.
|
|
279
|
+
shedText = textToSend
|
|
280
|
+
log?.(`stream → shed by send gate (id: ${messageId}); snapshot preserved for re-flush`)
|
|
281
|
+
} else if (/\bmessage is not modified\b/i.test(msg)) {
|
|
222
282
|
lastSentText = textToSend
|
|
223
283
|
lastSentAt = Date.now()
|
|
284
|
+
shedText = null // on-screen text == this snapshot; nothing to recover
|
|
224
285
|
log?.(`stream → not modified (id: ${messageId})`)
|
|
225
286
|
} else if (
|
|
226
287
|
/\bmessage to edit not found\b/i.test(msg)
|
|
@@ -327,9 +388,14 @@ export function createDraftStream(
|
|
|
327
388
|
return waitPromise
|
|
328
389
|
},
|
|
329
390
|
|
|
330
|
-
async finalize(): Promise<void> {
|
|
391
|
+
async finalize(finalText?: string): Promise<void> {
|
|
331
392
|
if (final) return
|
|
332
393
|
final = true
|
|
394
|
+
// A caller-supplied final snapshot supersedes any pending draft
|
|
395
|
+
// (last-write-wins) and is flushed below with `final` already set,
|
|
396
|
+
// so the transport classifies this edit as the answer's final
|
|
397
|
+
// render, not a sheddable draft (#3110).
|
|
398
|
+
if (finalText != null && !stopped) pendingText = finalText
|
|
333
399
|
// Drain any pending updates
|
|
334
400
|
if (scheduledTimer != null) {
|
|
335
401
|
clearTimeout(scheduledTimer)
|
|
@@ -338,6 +404,15 @@ export function createDraftStream(
|
|
|
338
404
|
if (inFlight) {
|
|
339
405
|
await inFlight
|
|
340
406
|
}
|
|
407
|
+
// #3110 review F2: if the newest snapshot was SHED by the send gate
|
|
408
|
+
// (never landed) and nothing newer is pending, re-flush it as the
|
|
409
|
+
// stream's final state. Checked AFTER awaiting inFlight so a flush
|
|
410
|
+
// that sheds mid-finalize is recovered too. A provided finalText and
|
|
411
|
+
// any pending draft both outrank the shed snapshot (they are newer).
|
|
412
|
+
if (pendingText == null && shedText != null && !stopped) {
|
|
413
|
+
pendingText = shedText
|
|
414
|
+
}
|
|
415
|
+
shedText = null
|
|
341
416
|
if (pendingText != null && !stopped) {
|
|
342
417
|
await flush()
|
|
343
418
|
}
|
|
@@ -60,10 +60,9 @@ import { readFileSync, writeFileSync, renameSync, unlinkSync } from 'node:fs'
|
|
|
60
60
|
import { isCronIdentity } from './cron-session.js'
|
|
61
61
|
import type { InboundMessage } from './ipc-protocol.js'
|
|
62
62
|
|
|
63
|
-
/** The distinct triggerSelfRestart reason for this escalation.
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* relaunch. */
|
|
63
|
+
/** The distinct triggerSelfRestart reason for this escalation. Like every
|
|
64
|
+
* other switchroom-managed relaunch it reverts any session `/model` override
|
|
65
|
+
* to the configured default (session-scoped, rev 4). */
|
|
67
66
|
export const BRIDGE_DEAD_RESTART_REASON = 'bridge-dead-resume'
|
|
68
67
|
|
|
69
68
|
/** Default grace window before a missing bridge is treated as dead.
|
|
@@ -65,8 +65,9 @@ export function parseEffortCommand(text: string): ParsedEffortCommand | null {
|
|
|
65
65
|
}
|
|
66
66
|
const arg = parts[0]
|
|
67
67
|
if (arg.toLowerCase() === 'help') return { kind: 'help' }
|
|
68
|
-
// `/effort default` — explicit user action that clears the
|
|
69
|
-
//
|
|
68
|
+
// `/effort default` — explicit user action that clears the session
|
|
69
|
+
// override (in-memory + any leftover queued-command carrier) and restores
|
|
70
|
+
// the configured default (#3186, session-scoped).
|
|
70
71
|
if (arg.toLowerCase() === 'default') return { kind: 'default' }
|
|
71
72
|
if (!isValidEffortArg(arg)) {
|
|
72
73
|
return { kind: 'help', reason: `not a valid effort level: ${arg}` }
|
|
@@ -91,14 +92,15 @@ export interface EffortCommandDeps {
|
|
|
91
92
|
*/
|
|
92
93
|
getConfiguredEffort: () => string | null
|
|
93
94
|
/**
|
|
94
|
-
*
|
|
95
|
+
* Clear the session effort override (#3186: the in-memory live level plus
|
|
96
|
+
* any leftover queued-command `.session-effort` carrier). Optional so
|
|
95
97
|
* gateway-agnostic tests can omit it; the gateway always wires it.
|
|
96
98
|
*/
|
|
97
99
|
clearSessionEffort?: () => void
|
|
98
100
|
/**
|
|
99
|
-
* The active
|
|
100
|
-
*
|
|
101
|
-
*
|
|
101
|
+
* The active session effort override level, or null when none (#3186:
|
|
102
|
+
* in-memory, session-scoped — reverts on restart). Optional; used to mark
|
|
103
|
+
* the LIVE level in the menu and the show text honestly.
|
|
102
104
|
*/
|
|
103
105
|
getSessionEffort?: () => string | null
|
|
104
106
|
escapeHtml: (s: string) => string
|
|
@@ -110,7 +112,7 @@ export interface EffortCommandReply {
|
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
const PERSIST_NOTE =
|
|
113
|
-
'
|
|
115
|
+
'_Session-only — this override lasts until the agent’s next restart, then reverts to the configured \`thinking_effort:\`. \`/effort default\` clears it now. To change the default permanently, set \`thinking_effort:\` in switchroom.yaml._'
|
|
114
116
|
|
|
115
117
|
const LEVELS_INLINE = EFFORT_LEVELS.map(l => `\`${l}\``).join(' · ')
|
|
116
118
|
|