switchroom 0.18.14 → 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 +473 -49
- package/dist/cli/notion-write-pretool.mjs +3 -0
- package/dist/cli/switchroom.js +1200 -1067
- package/dist/host-control/main.js +56 -51
- package/dist/vault/approvals/kernel-server.js +19 -12
- package/dist/vault/broker/server.js +675 -668
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +81 -139
- package/telegram-plugin/dist/bridge/bridge.js +21 -0
- package/telegram-plugin/dist/gateway/gateway.js +531 -259
- package/telegram-plugin/dist/server.js +22 -1
- 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 +310 -219
- 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/model-unavailable.ts +214 -0
- package/telegram-plugin/quota-watch.ts +16 -4
- package/telegram-plugin/runtime-metrics.ts +47 -0
- package/telegram-plugin/send-gate-degraded.test.ts +9 -7
- package/telegram-plugin/send-gate.ts +34 -4
- package/telegram-plugin/session-tail.ts +14 -2
- 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/model-unavailable.test.ts +187 -0
- package/telegram-plugin/tests/operator-events-session-tail.test.ts +55 -0
- 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/runtime-metrics.test.ts +24 -0
- 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/throttle-tier.test.ts +176 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +207 -0
- package/telegram-plugin/throttle-tier.ts +98 -1
- package/telegram-plugin/worker-activity-feed.ts +83 -8
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Structural pins for the session-model
|
|
3
|
-
* (reference/rfcs/session-model-stickiness.md).
|
|
2
|
+
* Structural pins for the session-scoped /model wiring in gateway.ts
|
|
3
|
+
* (reference/rfcs/session-model-stickiness.md §0.1, rev 4 — consume-once).
|
|
4
4
|
*
|
|
5
5
|
* The behaviour lives in un-exported inline closures (buildModelDeps's
|
|
6
|
-
* scheduleModelRelaunch/scheduleRestart,
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* the handler contract in model-command.test.ts.
|
|
6
|
+
* scheduleModelRelaunch/scheduleRestart, the typed/menu recorders, and the
|
|
7
|
+
* boot re-hydration block inside the startup IIFE), so — mirroring the other
|
|
8
|
+
* gateway-*.test.ts source-pins — we assert on the source structure. The
|
|
9
|
+
* end-to-end boot behaviour is exercised in tests/scaffold.session-model.test.ts
|
|
10
|
+
* (rendered start.sh), the file helpers in session-model-file.test.ts, and the
|
|
11
|
+
* handler contract in model-command.test.ts.
|
|
13
12
|
*/
|
|
14
13
|
|
|
15
14
|
import { describe, it, expect } from 'vitest'
|
|
@@ -20,35 +19,27 @@ import { dirname, resolve } from 'node:path'
|
|
|
20
19
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
21
20
|
const GATEWAY_SRC = readFileSync(resolve(__dirname, '..', 'gateway', 'gateway.ts'), 'utf8')
|
|
22
21
|
|
|
23
|
-
describe('gateway:
|
|
24
|
-
it('
|
|
25
|
-
|
|
26
|
-
expect(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// be on disk synchronously before the SIGTERM is even scheduled.
|
|
32
|
-
expect(writeIdx).toBeGreaterThan(0)
|
|
33
|
-
expect(killIdx).toBeGreaterThan(writeIdx)
|
|
34
|
-
// And before the setTimeout that schedules it.
|
|
35
|
-
const timeoutIdx = win.indexOf('setTimeout(')
|
|
36
|
-
expect(timeoutIdx).toBeGreaterThan(writeIdx)
|
|
22
|
+
describe('gateway: the .relaunch-model-intent subsystem is retired (rev 4)', () => {
|
|
23
|
+
it('gateway.ts no longer writes/clears/imports any relaunch-model intent', () => {
|
|
24
|
+
expect(GATEWAY_SRC).not.toContain('writeRelaunchModelIntent')
|
|
25
|
+
expect(GATEWAY_SRC).not.toContain('clearRelaunchModelIntent')
|
|
26
|
+
expect(GATEWAY_SRC).not.toContain('intentForRestartReason')
|
|
27
|
+
expect(GATEWAY_SRC).not.toContain('clearStaleGatewayShutdownIntent')
|
|
28
|
+
expect(GATEWAY_SRC).not.toContain('GATEWAY_SHUTDOWN_INTENT_REASON_PREFIX')
|
|
29
|
+
expect(GATEWAY_SRC).not.toContain('.relaunch-model-intent')
|
|
37
30
|
})
|
|
38
31
|
|
|
39
|
-
it('
|
|
32
|
+
it('triggerSelfRestart just signals — no intent stamp before the kill', () => {
|
|
40
33
|
const fnIdx = GATEWAY_SRC.indexOf('function triggerSelfRestart(')
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
expect(
|
|
44
|
-
|
|
45
|
-
expect(legacyWin).toContain('writeRelaunchModelIntent(smDir, intentForRestartReason(reason), reason)')
|
|
46
|
-
expect(legacyWin.indexOf('writeRelaunchModelIntent')).toBeLessThan(legacyWin.indexOf('spawn('))
|
|
34
|
+
expect(fnIdx).toBeGreaterThan(0)
|
|
35
|
+
const win = GATEWAY_SRC.slice(fnIdx, fnIdx + 3000)
|
|
36
|
+
expect(win).toContain("process.kill(1, 'SIGTERM')")
|
|
37
|
+
expect(win).not.toContain('writeRelaunchModelIntent')
|
|
47
38
|
})
|
|
48
39
|
})
|
|
49
40
|
|
|
50
|
-
describe('gateway: scheduleModelRelaunch dep (
|
|
51
|
-
it('writes the
|
|
41
|
+
describe('gateway: scheduleModelRelaunch dep (consume-once .session-model carrier)', () => {
|
|
42
|
+
it('writes the carrier via writeSessionModelFile before dispatching the restart', () => {
|
|
52
43
|
const idx = GATEWAY_SRC.indexOf('scheduleModelRelaunch: async')
|
|
53
44
|
expect(idx).toBeGreaterThan(0)
|
|
54
45
|
const win = GATEWAY_SRC.slice(idx, idx + 1800)
|
|
@@ -67,7 +58,7 @@ describe('gateway: scheduleModelRelaunch dep (durable .session-model)', () => {
|
|
|
67
58
|
expect(restartIdx).toBeGreaterThan(setIdx)
|
|
68
59
|
})
|
|
69
60
|
|
|
70
|
-
it('rolls back the prior
|
|
61
|
+
it('rolls back the prior carrier content (not just deletion) on a non-in-flight dispatch failure', () => {
|
|
71
62
|
const idx = GATEWAY_SRC.indexOf('scheduleModelRelaunch: async')
|
|
72
63
|
const win = GATEWAY_SRC.slice(idx, idx + 1800)
|
|
73
64
|
expect(win).toContain('const prevFileRaw = readSessionModelFileRaw(agentDir)')
|
|
@@ -75,15 +66,6 @@ describe('gateway: scheduleModelRelaunch dep (durable .session-model)', () => {
|
|
|
75
66
|
expect(win).toContain("!== 'restart_in_flight'")
|
|
76
67
|
})
|
|
77
68
|
|
|
78
|
-
it('the non-in-flight rollback ALSO clears the keep-intent (a live intent with no restart coming would wrongly KEEP across a crash)', () => {
|
|
79
|
-
const idx = GATEWAY_SRC.indexOf('scheduleModelRelaunch: async')
|
|
80
|
-
const win = GATEWAY_SRC.slice(idx, idx + 2400)
|
|
81
|
-
const inFlightIdx = win.indexOf("!== 'restart_in_flight'")
|
|
82
|
-
const clearIdx = win.indexOf('clearRelaunchModelIntent(agentDir)')
|
|
83
|
-
expect(inFlightIdx).toBeGreaterThan(0)
|
|
84
|
-
expect(clearIdx).toBeGreaterThan(inFlightIdx)
|
|
85
|
-
})
|
|
86
|
-
|
|
87
69
|
it('reuses the same scheduleRestart dispatch (not a bespoke restart path)', () => {
|
|
88
70
|
const idx = GATEWAY_SRC.indexOf('scheduleModelRelaunch: async')
|
|
89
71
|
const win = GATEWAY_SRC.slice(idx, idx + 1800)
|
|
@@ -91,63 +73,20 @@ describe('gateway: scheduleModelRelaunch dep (durable .session-model)', () => {
|
|
|
91
73
|
})
|
|
92
74
|
})
|
|
93
75
|
|
|
94
|
-
describe('gateway:
|
|
95
|
-
it('
|
|
96
|
-
const idx = GATEWAY_SRC.indexOf('scheduleRestart: async (reason: string)')
|
|
97
|
-
expect(idx).toBeGreaterThan(0)
|
|
98
|
-
const win = GATEWAY_SRC.slice(idx, idx + 3200)
|
|
99
|
-
const keepIdx = win.indexOf("writeRelaunchModelIntent(smDir, 'keep', reason)")
|
|
100
|
-
const dispatchIdx = win.indexOf("op: 'agent_restart'")
|
|
101
|
-
expect(keepIdx).toBeGreaterThan(0)
|
|
102
|
-
expect(dispatchIdx).toBeGreaterThan(keepIdx)
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
it('scheduleRestart clears the keep-intent when hostd refuses (failed dispatch → no live intent on disk)', () => {
|
|
106
|
-
const idx = GATEWAY_SRC.indexOf('scheduleRestart: async (reason: string)')
|
|
107
|
-
const win = GATEWAY_SRC.slice(idx, idx + 3200)
|
|
108
|
-
const failIdx = win.indexOf('hostd restart failed')
|
|
109
|
-
const clearIdx = win.indexOf('clearRelaunchModelIntent(smDir)')
|
|
110
|
-
expect(clearIdx).toBeGreaterThan(0)
|
|
111
|
-
expect(failIdx).toBeGreaterThan(clearIdx) // cleared before the throw's message
|
|
112
|
-
// And it sits in the same error branch as clearRestartMarker.
|
|
113
|
-
const markerIdx = win.indexOf('clearRestartMarker()')
|
|
114
|
-
expect(clearIdx).toBeGreaterThan(markerIdx)
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
it('/restart stamps a KEEP intent before dispatch (#3039: a restart is not "clear my model")', () => {
|
|
118
|
-
const idx = GATEWAY_SRC.indexOf("stampUserRestartReason('user: /restart from chat')")
|
|
119
|
-
expect(idx).toBeGreaterThan(0)
|
|
120
|
-
const win = GATEWAY_SRC.slice(idx, idx + 900)
|
|
121
|
-
const keepIdx = win.indexOf("writeRelaunchModelIntent(smDir, 'keep', 'user: /restart from chat')")
|
|
122
|
-
const dispatchIdx = win.indexOf("hostdRequestId('gw-restart')")
|
|
123
|
-
expect(keepIdx).toBeGreaterThan(0)
|
|
124
|
-
expect(dispatchIdx).toBeGreaterThan(keepIdx)
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
it('/new and /reset stamp keep-intent (fresh conversation, same model — contract row 7)', () => {
|
|
128
|
-
const idx = GATEWAY_SRC.indexOf('stampUserRestartReason(`user: /${kind} from chat`)')
|
|
129
|
-
expect(idx).toBeGreaterThan(0)
|
|
130
|
-
const win = GATEWAY_SRC.slice(idx, idx + 700)
|
|
131
|
-
const keepIdx = win.indexOf("writeRelaunchModelIntent(agentDir, 'keep', `user: /${kind} from chat`)")
|
|
132
|
-
const dispatchIdx = win.indexOf('tryHostdDispatch')
|
|
133
|
-
expect(keepIdx).toBeGreaterThan(0)
|
|
134
|
-
expect(dispatchIdx).toBeGreaterThan(keepIdx)
|
|
135
|
-
})
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
describe('gateway: model-menu callback persists the sticky override', () => {
|
|
139
|
-
it('a confirmed selection persists the canonical token (selectedModelToken), never the display label', () => {
|
|
140
|
-
// Recording extracted into recordModelMenuSideEffects (#3017) — shared by the
|
|
141
|
-
// live dispatcher and the deferred (queued mid-turn) apply so both record
|
|
142
|
-
// identically.
|
|
76
|
+
describe('gateway: menu callback carrier handling (session-scoped)', () => {
|
|
77
|
+
it('a live Claude selection records the in-memory override but writes NO carrier', () => {
|
|
143
78
|
const idx = GATEWAY_SRC.indexOf('function recordModelMenuSideEffects')
|
|
144
79
|
expect(idx).toBeGreaterThan(0)
|
|
145
|
-
const win = GATEWAY_SRC.slice(idx, idx +
|
|
146
|
-
|
|
147
|
-
expect(win).
|
|
80
|
+
const win = GATEWAY_SRC.slice(idx, idx + 900)
|
|
81
|
+
// The first (live-switch) block sets the override but no longer persists.
|
|
82
|
+
expect(win).toContain('sessionModelSource.setOverride(outcome.selectedModel)')
|
|
83
|
+
const overrideIdx = win.indexOf('sessionModelSource.setOverride(outcome.selectedModel)')
|
|
84
|
+
const nextClearIdx = win.indexOf('outcome.clearedDefault')
|
|
85
|
+
const writeBetween = win.slice(overrideIdx, nextClearIdx)
|
|
86
|
+
expect(writeBetween).not.toContain('writeSessionModelFile(')
|
|
148
87
|
})
|
|
149
88
|
|
|
150
|
-
it('a confirmed "Default" selection CLEARS the
|
|
89
|
+
it('a confirmed "Default" selection CLEARS the carrier', () => {
|
|
151
90
|
const idx = GATEWAY_SRC.indexOf('function recordModelMenuSideEffects')
|
|
152
91
|
const win = GATEWAY_SRC.slice(idx, idx + 2400)
|
|
153
92
|
expect(win).toContain('outcome.clearedDefault')
|
|
@@ -155,9 +94,6 @@ describe('gateway: model-menu callback persists the sticky override', () => {
|
|
|
155
94
|
})
|
|
156
95
|
|
|
157
96
|
it('the sr-* callback branch calls scheduleModelRelaunch, not inject', () => {
|
|
158
|
-
// Anchor on the sr-* TARGET dispatcher branch specifically (a mid-turn
|
|
159
|
-
// busy-gate #3017 also matches `if (data.startsWith(MODEL_CALLBACK_SR))`, so
|
|
160
|
-
// anchor on the relaunch call that is unique to the idle apply branch).
|
|
161
97
|
const idx = GATEWAY_SRC.indexOf('const srLabel = escapeHtmlForTg(srFriendlyLabel(srName))')
|
|
162
98
|
expect(idx).toBeGreaterThan(0)
|
|
163
99
|
const win = GATEWAY_SRC.slice(idx, idx + 1400)
|
|
@@ -165,53 +101,37 @@ describe('gateway: model-menu callback persists the sticky override', () => {
|
|
|
165
101
|
expect(win).toMatch(/scheduleModelRelaunch[\s\S]*?\n\s*return\n/)
|
|
166
102
|
})
|
|
167
103
|
|
|
168
|
-
it('the sr-to-claude transition writes the
|
|
104
|
+
it('the sr-to-claude transition (a relaunch) writes the carrier, and clears it on a Default tap', () => {
|
|
169
105
|
const idx = GATEWAY_SRC.indexOf('isSrToClaudeTransition(prevSessionModel, outcome.selectedModel)')
|
|
170
106
|
expect(idx).toBeGreaterThan(0)
|
|
171
107
|
const win = GATEWAY_SRC.slice(idx, idx + 3600)
|
|
172
108
|
expect(win).toMatch(/writeSessionModelFile\(\s*agentDir,\s*token/)
|
|
173
109
|
expect(win).toContain('clearSessionModelFile(agentDir)')
|
|
174
|
-
// Restart rides triggerSelfRestart with a keep-classified reason.
|
|
175
110
|
expect(win).toContain("triggerSelfRestart(agentName, 'sr-to-claude-model-switch'")
|
|
176
111
|
})
|
|
177
112
|
})
|
|
178
113
|
|
|
179
|
-
describe('gateway: typed /model
|
|
180
|
-
it('
|
|
181
|
-
// Recording extracted into recordTypedModelSwitch (#3017) — shared by the
|
|
182
|
-
// live `bot.command('model')` handler and the deferred (queued mid-turn) apply.
|
|
114
|
+
describe('gateway: typed /model is session-scoped (live Claude switch writes no carrier)', () => {
|
|
115
|
+
it('the Claude path sets the in-memory override but never persists a carrier', () => {
|
|
183
116
|
const idx = GATEWAY_SRC.indexOf('function recordTypedModelSwitch')
|
|
184
117
|
expect(idx).toBeGreaterThan(0)
|
|
185
|
-
const win = GATEWAY_SRC.slice(idx, idx +
|
|
186
|
-
expect(win).toContain(
|
|
187
|
-
|
|
188
|
-
expect(win).toContain('
|
|
189
|
-
// Non-default: the requested token (shape-gated, non-sr) is what persists.
|
|
190
|
-
expect(win).toContain('isValidModelArg(requested) && !isSrModel(requested)')
|
|
191
|
-
expect(win).toMatch(/writeSessionModelFile\(\s*smDir,\s*requested/)
|
|
118
|
+
const win = GATEWAY_SRC.slice(idx, idx + 1400)
|
|
119
|
+
expect(win).toContain('sessionModelSource.setOverride(reply.selectedModel)')
|
|
120
|
+
// No durable carrier write on the live-switch path.
|
|
121
|
+
expect(win).not.toContain('writeSessionModelFile(')
|
|
192
122
|
})
|
|
193
123
|
|
|
194
|
-
it('`/model default`
|
|
124
|
+
it('`/model default` clears the carrier + in-memory override (silent-switch path must not resurrect)', () => {
|
|
195
125
|
const idx = GATEWAY_SRC.indexOf('function recordTypedModelSwitch')
|
|
196
|
-
const win = GATEWAY_SRC.slice(idx, idx +
|
|
197
|
-
|
|
198
|
-
|
|
126
|
+
const win = GATEWAY_SRC.slice(idx, idx + 1400)
|
|
127
|
+
expect(win).toContain("requested?.toLowerCase() === 'default'")
|
|
128
|
+
expect(win).toContain('clearSessionModelFile(smDir)')
|
|
129
|
+
expect(win).toContain('sessionModelSource.setOverride(null)')
|
|
130
|
+
// The file-clear is not gated on a positive confirmation.
|
|
199
131
|
const clearIdx = win.indexOf('if (smDir) clearSessionModelFile(smDir)')
|
|
200
132
|
const gatedOverrideIdx = win.indexOf('if (reply.selectedModel) sessionModelSource.setOverride(null)')
|
|
201
133
|
expect(clearIdx).toBeGreaterThan(0)
|
|
202
134
|
expect(gatedOverrideIdx).toBeGreaterThan(clearIdx)
|
|
203
|
-
// And the whole default branch is not nested in an `if (reply.selectedModel)` block:
|
|
204
|
-
const between = win.slice(0, clearIdx)
|
|
205
|
-
expect(between).not.toContain('if (reply.selectedModel) {')
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
it('a persist failure is surfaced ON THE REPLY, not just stderr (typed + menu paths)', () => {
|
|
209
|
-
expect(GATEWAY_SRC.match(/won’t survive a relaunch/g)?.length ?? 0).toBeGreaterThanOrEqual(2)
|
|
210
|
-
const typedIdx = GATEWAY_SRC.indexOf('persistWarning =')
|
|
211
|
-
expect(typedIdx).toBeGreaterThan(0)
|
|
212
|
-
expect(GATEWAY_SRC).toContain('reply.text + persistWarning')
|
|
213
|
-
// Menu path appends onto the outgoing card text.
|
|
214
|
-
expect(GATEWAY_SRC).toContain('outcome.reply.text +=')
|
|
215
135
|
})
|
|
216
136
|
})
|
|
217
137
|
|