switchroom 0.19.48 → 0.20.1
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/bin/handoff-briefing.sh +213 -74
- package/dist/agent-scheduler/index.js +18 -1
- package/dist/auth-broker/index.js +19 -2
- package/dist/buzz-gateway/index.js +9367 -0
- package/dist/cli/notion-write-pretool.mjs +18 -1
- package/dist/cli/switchroom.js +24734 -16371
- package/dist/host-control/main.js +59 -9
- package/dist/vault/approvals/kernel-server.js +19 -2
- package/dist/vault/broker/server.js +19 -2
- package/package.json +6 -4
- package/profiles/_base/start.sh.hbs +148 -2
- package/profiles/default/CLAUDE.md.hbs +1 -1
- package/skills/dev-protocol/SKILL.md +30 -1
- package/skills/switchroom-architecture/SKILL.md +5 -0
- package/skills/switchroom-cli/SKILL.md +1 -1
- package/telegram-plugin/dist/bridge/bridge.js +7 -4
- package/telegram-plugin/dist/gateway/gateway.js +2376 -1039
- package/telegram-plugin/dist/server.js +7 -4
- package/telegram-plugin/gateway/access-store.test.ts +234 -0
- package/telegram-plugin/gateway/access-store.ts +194 -0
- package/telegram-plugin/gateway/boot-briefing-builder.ts +586 -0
- package/telegram-plugin/gateway/boot-briefing-capability.ts +31 -0
- package/telegram-plugin/gateway/boot-briefing-wiring.ts +332 -0
- package/telegram-plugin/gateway/buzz-mirror-correlation-store.ts +285 -0
- package/telegram-plugin/gateway/buzz-mirror.ts +494 -0
- package/telegram-plugin/gateway/buzz-type-guards.ts +34 -0
- package/telegram-plugin/gateway/channel-route.ts +272 -0
- package/telegram-plugin/gateway/gateway.ts +115 -203
- package/telegram-plugin/gateway/inbound-router.ts +93 -3
- package/telegram-plugin/gateway/inbound-spool.ts +33 -1
- package/telegram-plugin/gateway/ipc-protocol.ts +81 -2
- package/telegram-plugin/gateway/ipc-server.ts +197 -2
- package/telegram-plugin/gateway/outbound-send-path.ts +85 -2
- package/telegram-plugin/gateway/pending-turn-env.ts +70 -0
- package/telegram-plugin/gateway/stream-render.ts +21 -0
- package/telegram-plugin/gateway/subagent-handback-marker.ts +12 -0
- package/telegram-plugin/gateway/user-failure-notices.ts +172 -0
- package/telegram-plugin/history.ts +15 -0
- package/telegram-plugin/llm-error-present.ts +9 -4
- package/telegram-plugin/model-unavailable.ts +4 -0
- package/telegram-plugin/operator-events.fixtures.json +12 -12
- package/telegram-plugin/operator-events.ts +81 -9
- package/telegram-plugin/session-tail.ts +7 -1
- package/telegram-plugin/tests/boot-briefing-builder.test.ts +995 -0
- package/telegram-plugin/tests/buzz-mirror-correlation-store.test.ts +173 -0
- package/telegram-plugin/tests/buzz-mirror.test.ts +538 -0
- package/telegram-plugin/tests/buzz-origin-stamp-gate.test.ts +159 -0
- package/telegram-plugin/tests/channel-route.test.ts +306 -0
- package/telegram-plugin/tests/inbound-spool.test.ts +47 -0
- package/telegram-plugin/tests/ipc-server-buzz-dedup.test.ts +124 -0
- package/telegram-plugin/tests/ipc-server-buzz-peer.test.ts +269 -0
- package/telegram-plugin/tests/operator-events-session-tail.test.ts +63 -0
- package/telegram-plugin/tests/operator-events.test.ts +71 -7
- package/telegram-plugin/tests/outbound-send-path.test.ts +24 -0
- package/telegram-plugin/tests/reply-to-buffer-fallback.test.ts +273 -0
- package/telegram-plugin/tests/reply-to-buffer-history.test.ts +134 -0
- package/telegram-plugin/tests/user-failure-notices.test.ts +165 -0
- package/telegram-plugin/voice-normalize-text.ts +5 -0
- package/vendor/hindsight-memory/scripts/directive_verify.py +4 -0
- package/vendor/hindsight-memory/scripts/recall.py +7 -2
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
createCorrelationStore,
|
|
4
|
+
type CorrelationFsLike,
|
|
5
|
+
} from '../gateway/buzz-mirror-correlation-store.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* In-memory fake filesystem implementing CorrelationFsLike. Shared across store
|
|
9
|
+
* instances to simulate a gateway restart against the SAME journal — the core
|
|
10
|
+
* of the #4222 fix (an edit_message correction must survive a restart).
|
|
11
|
+
*/
|
|
12
|
+
function makeFakeFs(): CorrelationFsLike & { files: Map<string, string>; dirs: Set<string> } {
|
|
13
|
+
const files = new Map<string, string>()
|
|
14
|
+
const dirs = new Set<string>()
|
|
15
|
+
const fdPaths = new Map<number, string>()
|
|
16
|
+
let nextFd = 3
|
|
17
|
+
return {
|
|
18
|
+
files,
|
|
19
|
+
dirs,
|
|
20
|
+
existsSync: (p) => files.has(p) || dirs.has(p),
|
|
21
|
+
mkdirSync: (p) => { dirs.add(p) },
|
|
22
|
+
readFileSync: (p) => {
|
|
23
|
+
if (!files.has(p)) throw new Error(`ENOENT ${p}`)
|
|
24
|
+
return files.get(p)!
|
|
25
|
+
},
|
|
26
|
+
writeFileSync: (p, data) => { files.set(p, data) },
|
|
27
|
+
renameSync: (from, to) => {
|
|
28
|
+
files.set(to, files.get(from) ?? '')
|
|
29
|
+
files.delete(from)
|
|
30
|
+
},
|
|
31
|
+
openSync: (p) => {
|
|
32
|
+
if (!files.has(p)) files.set(p, '')
|
|
33
|
+
const fd = nextFd++
|
|
34
|
+
fdPaths.set(fd, p)
|
|
35
|
+
return fd
|
|
36
|
+
},
|
|
37
|
+
writeSync: (fd, data) => {
|
|
38
|
+
const p = fdPaths.get(fd)!
|
|
39
|
+
files.set(p, (files.get(p) ?? '') + data)
|
|
40
|
+
},
|
|
41
|
+
fsyncSync: () => { /* no-op */ },
|
|
42
|
+
closeSync: (fd) => { fdPaths.delete(fd) },
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const JP = '/state/buzz/mirror-correlation.jsonl'
|
|
47
|
+
|
|
48
|
+
describe('createCorrelationStore — durable msg→Buzz correlation (#4222)', () => {
|
|
49
|
+
it('replays a persisted mapping into a NEW store instance (restart survival)', () => {
|
|
50
|
+
const fs = makeFakeFs()
|
|
51
|
+
const a = createCorrelationStore({ journalPath: JP, fs })
|
|
52
|
+
a.set('555:1001', { eventId: 'evt-A', channelId: 'chan-A' })
|
|
53
|
+
a.close()
|
|
54
|
+
|
|
55
|
+
// Fresh instance over the SAME journal — the mapping must reload.
|
|
56
|
+
const b = createCorrelationStore({ journalPath: JP, fs })
|
|
57
|
+
expect(b.get('555:1001')).toEqual({ eventId: 'evt-A', channelId: 'chan-A' })
|
|
58
|
+
expect(b.size()).toBe(1)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('last-write-wins on a re-set key across a restart', () => {
|
|
62
|
+
const fs = makeFakeFs()
|
|
63
|
+
const a = createCorrelationStore({ journalPath: JP, fs })
|
|
64
|
+
a.set('555:1', { eventId: 'evt-old', channelId: 'chan-A' })
|
|
65
|
+
a.set('555:1', { eventId: 'evt-new', channelId: 'chan-A' })
|
|
66
|
+
a.close()
|
|
67
|
+
|
|
68
|
+
const b = createCorrelationStore({ journalPath: JP, fs })
|
|
69
|
+
expect(b.get('555:1')).toEqual({ eventId: 'evt-new', channelId: 'chan-A' })
|
|
70
|
+
expect(b.size()).toBe(1)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('persists + replays the NIP-10 threadRoot (outbound thread continuity)', () => {
|
|
74
|
+
const fs = makeFakeFs()
|
|
75
|
+
const a = createCorrelationStore({ journalPath: JP, fs })
|
|
76
|
+
// A mirror that threaded under a deeper parent records a root distinct from
|
|
77
|
+
// its own event id — that root must survive a restart so a later reply can
|
|
78
|
+
// emit the correct NIP-10 `root` marker.
|
|
79
|
+
a.set('555:200', { eventId: 'evt-mid', channelId: 'chan-A', threadRoot: 'evt-root' })
|
|
80
|
+
a.close()
|
|
81
|
+
|
|
82
|
+
const b = createCorrelationStore({ journalPath: JP, fs })
|
|
83
|
+
expect(b.get('555:200')).toEqual({ eventId: 'evt-mid', channelId: 'chan-A', threadRoot: 'evt-root' })
|
|
84
|
+
// The record on disk actually carries the field (not just an in-memory echo).
|
|
85
|
+
expect(fs.files.get(JP) ?? '').toContain('"threadRoot":"evt-root"')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('a pre-threadRoot journal record replays with threadRoot undefined (backward compat)', () => {
|
|
89
|
+
const fs = makeFakeFs()
|
|
90
|
+
fs.dirs.add('/state/buzz')
|
|
91
|
+
// A record written before threadRoot existed — no threadRoot key.
|
|
92
|
+
fs.files.set(JP, JSON.stringify({ key: '555:1', eventId: 'evt-A', channelId: 'chan-A' }) + '\n')
|
|
93
|
+
const s = createCorrelationStore({ journalPath: JP, fs })
|
|
94
|
+
const v = s.get('555:1')
|
|
95
|
+
expect(v?.eventId).toBe('evt-A')
|
|
96
|
+
expect(v?.channelId).toBe('chan-A')
|
|
97
|
+
expect(v?.threadRoot).toBeUndefined()
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('in-memory-only mode (no journalPath) never touches the fs but still bounds', () => {
|
|
101
|
+
const fs = makeFakeFs()
|
|
102
|
+
const s = createCorrelationStore({ capacity: 2, fs })
|
|
103
|
+
s.set('a', { eventId: 'e1', channelId: 'c' })
|
|
104
|
+
s.set('b', { eventId: 'e2', channelId: 'c' })
|
|
105
|
+
s.set('c', { eventId: 'e3', channelId: 'c' })
|
|
106
|
+
expect(s.get('a')).toBeUndefined() // evicted (FIFO, capacity 2)
|
|
107
|
+
expect(s.size()).toBe(2)
|
|
108
|
+
expect(fs.files.size).toBe(0) // no journal written
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('enforces the FIFO capacity bound in memory AND on disk after compaction', () => {
|
|
112
|
+
const fs = makeFakeFs()
|
|
113
|
+
const capacity = 4
|
|
114
|
+
const a = createCorrelationStore({ journalPath: JP, fs, capacity })
|
|
115
|
+
// Insert more than capacity — oldest keys evict.
|
|
116
|
+
for (let i = 0; i < 20; i++) a.set(`k:${i}`, { eventId: `e${i}`, channelId: 'c' })
|
|
117
|
+
expect(a.size()).toBe(capacity)
|
|
118
|
+
expect(a.get('k:0')).toBeUndefined()
|
|
119
|
+
expect(a.get('k:19')).toEqual({ eventId: 'e19', channelId: 'c' })
|
|
120
|
+
a.close()
|
|
121
|
+
|
|
122
|
+
// Restart: boot compaction must keep the on-disk journal bounded too — a new
|
|
123
|
+
// instance sees exactly `capacity` keys, and the compacted journal file has
|
|
124
|
+
// no more than `capacity` lines.
|
|
125
|
+
const b = createCorrelationStore({ journalPath: JP, fs, capacity })
|
|
126
|
+
expect(b.size()).toBe(capacity)
|
|
127
|
+
const lines = (fs.files.get(JP) ?? '').split('\n').filter((l) => l.trim())
|
|
128
|
+
expect(lines.length).toBeLessThanOrEqual(capacity)
|
|
129
|
+
expect(b.get('k:19')).toEqual({ eventId: 'e19', channelId: 'c' })
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('re-compacts in-session so the journal never grows unbounded', () => {
|
|
133
|
+
const fs = makeFakeFs()
|
|
134
|
+
const capacity = 4
|
|
135
|
+
const s = createCorrelationStore({ journalPath: JP, fs, capacity })
|
|
136
|
+
// Churn far more writes than capacity; the in-session compaction (at
|
|
137
|
+
// capacity * COMPACTION_FACTOR appends) must keep the file bounded.
|
|
138
|
+
for (let i = 0; i < 200; i++) s.set(`k:${i}`, { eventId: `e${i}`, channelId: 'c' })
|
|
139
|
+
const lines = (fs.files.get(JP) ?? '').split('\n').filter((l) => l.trim())
|
|
140
|
+
// Bounded by roughly capacity * COMPACTION_FACTOR (4*4=16) + the compacted
|
|
141
|
+
// baseline — assert it stays a small multiple of capacity, never 200.
|
|
142
|
+
expect(lines.length).toBeLessThan(capacity * 8)
|
|
143
|
+
expect(s.size()).toBe(capacity)
|
|
144
|
+
s.close()
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('tolerates a torn final journal line (crash mid-write)', () => {
|
|
148
|
+
const fs = makeFakeFs()
|
|
149
|
+
fs.dirs.add('/state/buzz')
|
|
150
|
+
fs.files.set(
|
|
151
|
+
JP,
|
|
152
|
+
JSON.stringify({ key: '1:1', eventId: 'e1', channelId: 'c' }) + '\n' + '{"key":"1:2","eventId', // truncated
|
|
153
|
+
)
|
|
154
|
+
const s = createCorrelationStore({ journalPath: JP, fs })
|
|
155
|
+
expect(s.get('1:1')).toEqual({ eventId: 'e1', channelId: 'c' })
|
|
156
|
+
expect(s.get('1:2')).toBeUndefined()
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('degrades to empty in-memory when the journal cannot be read', () => {
|
|
160
|
+
const fs = makeFakeFs()
|
|
161
|
+
fs.dirs.add('/state/buzz')
|
|
162
|
+
fs.files.set(JP, 'exists-but-unreadable')
|
|
163
|
+
const throwingFs: CorrelationFsLike = {
|
|
164
|
+
...fs,
|
|
165
|
+
readFileSync: () => { throw new Error('EIO') },
|
|
166
|
+
}
|
|
167
|
+
const s = createCorrelationStore({ journalPath: JP, fs: throwingFs })
|
|
168
|
+
// No crash; store is empty and still usable.
|
|
169
|
+
expect(s.size()).toBe(0)
|
|
170
|
+
s.set('1:1', { eventId: 'e1', channelId: 'c' })
|
|
171
|
+
expect(s.get('1:1')).toEqual({ eventId: 'e1', channelId: 'c' })
|
|
172
|
+
})
|
|
173
|
+
})
|