@mininglamp-oss/cc-channel-octo 1.0.1-dev.0ac574a

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.
Files changed (93) hide show
  1. package/CHANGELOG.md +361 -0
  2. package/LICENSE +191 -0
  3. package/README.md +577 -0
  4. package/config.bot.example.json +15 -0
  5. package/config.example.json +33 -0
  6. package/dist/agent-bridge.d.ts +91 -0
  7. package/dist/agent-bridge.js +397 -0
  8. package/dist/agent-bridge.js.map +1 -0
  9. package/dist/cli.d.ts +109 -0
  10. package/dist/cli.js +467 -0
  11. package/dist/cli.js.map +1 -0
  12. package/dist/commands.d.ts +57 -0
  13. package/dist/commands.js +121 -0
  14. package/dist/commands.js.map +1 -0
  15. package/dist/config.d.ts +294 -0
  16. package/dist/config.js +344 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/configure.d.ts +11 -0
  19. package/dist/configure.js +106 -0
  20. package/dist/configure.js.map +1 -0
  21. package/dist/cron-evaluator.d.ts +53 -0
  22. package/dist/cron-evaluator.js +191 -0
  23. package/dist/cron-evaluator.js.map +1 -0
  24. package/dist/cron-fire-marker.d.ts +24 -0
  25. package/dist/cron-fire-marker.js +25 -0
  26. package/dist/cron-fire-marker.js.map +1 -0
  27. package/dist/cron-scheduler.d.ts +46 -0
  28. package/dist/cron-scheduler.js +114 -0
  29. package/dist/cron-scheduler.js.map +1 -0
  30. package/dist/cron-store.d.ts +62 -0
  31. package/dist/cron-store.js +63 -0
  32. package/dist/cron-store.js.map +1 -0
  33. package/dist/cron-tool.d.ts +44 -0
  34. package/dist/cron-tool.js +151 -0
  35. package/dist/cron-tool.js.map +1 -0
  36. package/dist/cwd-resolver.d.ts +72 -0
  37. package/dist/cwd-resolver.js +166 -0
  38. package/dist/cwd-resolver.js.map +1 -0
  39. package/dist/db-adapter.d.ts +21 -0
  40. package/dist/db-adapter.js +64 -0
  41. package/dist/db-adapter.js.map +1 -0
  42. package/dist/file-inline-wrap.d.ts +94 -0
  43. package/dist/file-inline-wrap.js +243 -0
  44. package/dist/file-inline-wrap.js.map +1 -0
  45. package/dist/gateway.d.ts +105 -0
  46. package/dist/gateway.js +425 -0
  47. package/dist/gateway.js.map +1 -0
  48. package/dist/group-config.d.ts +41 -0
  49. package/dist/group-config.js +104 -0
  50. package/dist/group-config.js.map +1 -0
  51. package/dist/group-context.d.ts +81 -0
  52. package/dist/group-context.js +466 -0
  53. package/dist/group-context.js.map +1 -0
  54. package/dist/inbound.d.ts +136 -0
  55. package/dist/inbound.js +667 -0
  56. package/dist/inbound.js.map +1 -0
  57. package/dist/index.d.ts +65 -0
  58. package/dist/index.js +1026 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/media-inbound.d.ts +38 -0
  61. package/dist/media-inbound.js +131 -0
  62. package/dist/media-inbound.js.map +1 -0
  63. package/dist/mention-utils.d.ts +108 -0
  64. package/dist/mention-utils.js +199 -0
  65. package/dist/mention-utils.js.map +1 -0
  66. package/dist/octo/api.d.ts +148 -0
  67. package/dist/octo/api.js +320 -0
  68. package/dist/octo/api.js.map +1 -0
  69. package/dist/octo/socket.d.ts +102 -0
  70. package/dist/octo/socket.js +793 -0
  71. package/dist/octo/socket.js.map +1 -0
  72. package/dist/octo/types.d.ts +126 -0
  73. package/dist/octo/types.js +35 -0
  74. package/dist/octo/types.js.map +1 -0
  75. package/dist/prompt-safety.d.ts +78 -0
  76. package/dist/prompt-safety.js +148 -0
  77. package/dist/prompt-safety.js.map +1 -0
  78. package/dist/session-router.d.ts +144 -0
  79. package/dist/session-router.js +490 -0
  80. package/dist/session-router.js.map +1 -0
  81. package/dist/session-store.d.ts +89 -0
  82. package/dist/session-store.js +297 -0
  83. package/dist/session-store.js.map +1 -0
  84. package/dist/skill-linker.d.ts +31 -0
  85. package/dist/skill-linker.js +160 -0
  86. package/dist/skill-linker.js.map +1 -0
  87. package/dist/stream-relay.d.ts +42 -0
  88. package/dist/stream-relay.js +243 -0
  89. package/dist/stream-relay.js.map +1 -0
  90. package/dist/url-policy.d.ts +103 -0
  91. package/dist/url-policy.js +290 -0
  92. package/dist/url-policy.js.map +1 -0
  93. package/package.json +79 -0
@@ -0,0 +1,490 @@
1
+ /**
2
+ * Session Router — routing + concurrency control + mention gate + rate limiting.
3
+ */
4
+ import { ChannelType, MessageType } from './octo/types.js';
5
+ import { sendMessage } from './octo/api.js';
6
+ import { isAuthenticCronFire } from './cron-fire-marker.js';
7
+ const BUCKET_STALE_MS = 5 * 60 * 1000; // 5 minutes
8
+ /** Global rate limit: 10x per-session limit, shared across all sessions. */
9
+ const GLOBAL_RATE_MULTIPLIER = 10;
10
+ /** Maximum allowed content length in bytes (Q10). Messages exceeding this are rejected. */
11
+ const MAX_CONTENT_BYTES = 32_768; // 32 KB
12
+ export class SessionRouter {
13
+ config;
14
+ robotId;
15
+ /** G18: owner_uid from registerBot. Stored for future permission model. */
16
+ ownerUid;
17
+ inboundQueues = new Map();
18
+ tokenBuckets = new Map();
19
+ /** G20: per-user buckets keyed by from_uid alone (cross-channel rate limit). */
20
+ userBuckets = new Map();
21
+ globalBucket = null;
22
+ /**
23
+ * G14: UIDs known to be bots. Initialized with this bot's robotId; can be
24
+ * extended via registerKnownBot() for future multi-bot deployments.
25
+ */
26
+ knownBotUids = new Set();
27
+ constructor(config, robotId, ownerUid = '') {
28
+ this.config = config;
29
+ this.robotId = robotId;
30
+ this.ownerUid = ownerUid;
31
+ this.knownBotUids.add(robotId);
32
+ }
33
+ /** G14: register another known bot uid (future multi-bot support). */
34
+ registerKnownBot(uid) {
35
+ if (uid)
36
+ this.knownBotUids.add(uid);
37
+ }
38
+ /** G18: owner_uid stored from registerBot. Used by future permission model. */
39
+ getOwnerUid() {
40
+ return this.ownerUid;
41
+ }
42
+ /**
43
+ * Acquire a per-session lock for the full message handling pipeline.
44
+ * Callers (e.g. index.ts) use this to ensure the entire handleMessage
45
+ * chain — not just routing — runs serially per session key.
46
+ */
47
+ async withSessionLock(key, fn) {
48
+ const prev = this.inboundQueues.get(key) ?? Promise.resolve();
49
+ let resolveGate;
50
+ const gate = new Promise((r) => {
51
+ resolveGate = r;
52
+ });
53
+ this.inboundQueues.set(key, gate);
54
+ try {
55
+ await prev;
56
+ return await fn();
57
+ }
58
+ finally {
59
+ resolveGate();
60
+ if (this.inboundQueues.get(key) === gate) {
61
+ this.inboundQueues.delete(key);
62
+ }
63
+ }
64
+ }
65
+ /**
66
+ * Route a message and, if it should be processed, run the handler callback
67
+ * under the same per-session lock. This ensures no gap between route decision
68
+ * and pipeline execution — concurrent same-session messages cannot interleave.
69
+ */
70
+ /**
71
+ * Route a message and, if it should be processed, run the handler callback
72
+ * under the same per-session lock. This ensures no gap between route decision
73
+ * and pipeline execution — concurrent same-session messages cannot interleave.
74
+ *
75
+ * Returns the RouteResult (or `null` for silent-drop cases) so the caller
76
+ * can inspect `rejectionReason` to decide whether the message should still
77
+ * influence downstream side-effects like group context caching
78
+ * (C1 / P2.5 — stage 6).
79
+ */
80
+ async routeAndHandle(msg, handler) {
81
+ const key = this.sessionKey(msg);
82
+ return this.withSessionLock(key, async () => {
83
+ const result = await this.processMessage(msg, key);
84
+ if (result && result.shouldProcess) {
85
+ await this.runHandlerWithTimeout(result, handler);
86
+ }
87
+ return result;
88
+ });
89
+ }
90
+ /**
91
+ * #141: Run the handler under a dispatch timeout so a hung turn (a stuck SDK
92
+ * query, a wedged tool subprocess, a stalled stream) cannot block the session
93
+ * forever. The handler runs inside withSessionLock — if it never returns, the
94
+ * lock's gate never resolves and EVERY subsequent message for this session is
95
+ * stuck permanently (silent). Racing the handler against a timeout guarantees
96
+ * the lock releases.
97
+ *
98
+ * Scope (mirrors openclaw #75): we do NOT cancel the in-flight turn — the SDK
99
+ * query keeps running to completion in the background; we only unblock the
100
+ * queue. Worst case is a delayed real reply arriving after the apology.
101
+ *
102
+ * `timeoutError` is a per-invocation Error so the catch identifies OUR timeout
103
+ * by reference equality, never by string comparison (a same-text upstream
104
+ * error must not be misclassified).
105
+ */
106
+ async runHandlerWithTimeout(result, handler) {
107
+ const timeoutMs = this.config.dispatchTimeoutMs;
108
+ if (!timeoutMs || timeoutMs <= 0) {
109
+ // Timeout disabled — run unguarded.
110
+ await handler(result);
111
+ return;
112
+ }
113
+ let timeoutHandle;
114
+ const timeoutError = new Error(`dispatch timed out after ${timeoutMs}ms`);
115
+ const timeoutPromise = new Promise((_, reject) => {
116
+ timeoutHandle = setTimeout(() => reject(timeoutError), timeoutMs);
117
+ });
118
+ // The handler keeps running after a timeout (we don't cancel the in-flight
119
+ // turn — see scope note above). Once the race settles on timeoutError, that
120
+ // orphaned promise is no longer awaited; attach a no-op catch so a late
121
+ // rejection from a handler that doesn't self-contain its errors can't surface
122
+ // as an unhandledRejection. Today's caller (index.ts) is fully try/caught, so
123
+ // this is defense-in-depth for future callers.
124
+ const handlerPromise = handler(result);
125
+ handlerPromise.catch(() => { });
126
+ try {
127
+ await Promise.race([handlerPromise, timeoutPromise]);
128
+ }
129
+ catch (err) {
130
+ if (err === timeoutError) {
131
+ console.warn(`session-router: dispatch hung past ${timeoutMs}ms, releasing session lock (session=${result.sessionKey})`);
132
+ // Bounded apology — replySafe swallows its own errors, and the
133
+ // underlying sendMessage in octo/api.ts is itself time-bounded, so a
134
+ // sick Octo API can't re-hang us here.
135
+ await this.replySafe(result.message, '⚠️ 处理超时,请稍后重试。');
136
+ return; // swallow: the lock releases, the queue advances
137
+ }
138
+ // A real handler error — index.ts's handler already catches and replies
139
+ // internally, so reaching here is unexpected. Swallow to keep the lock
140
+ // release path identical (never let an error wedge the queue).
141
+ console.error(`session-router: handler error (session=${result.sessionKey}): ${String(err)}`);
142
+ }
143
+ finally {
144
+ if (timeoutHandle)
145
+ clearTimeout(timeoutHandle);
146
+ }
147
+ }
148
+ async route(msg) {
149
+ const key = this.sessionKey(msg);
150
+ return this.withSessionLock(key, () => this.processMessage(msg, key));
151
+ }
152
+ sessionKey(msg) {
153
+ const spaceId = this.extractSpaceId(msg);
154
+ if (msg.channel_type === ChannelType.DM) {
155
+ // DM is per-user (private): same peer always resumes the same session.
156
+ // from_uid IS the peer's identity here; a missing one is unroutable —
157
+ // never fall back to '' (that would collapse every uid-less DM into ONE
158
+ // shared session across unrelated peers, leaking history/memory). Mirrors
159
+ // the group channel_id guard below. Caught upstream → message dropped.
160
+ if (!msg.from_uid) {
161
+ throw new Error('DM message has no from_uid — cannot derive a session key');
162
+ }
163
+ return spaceId ? `${spaceId}:${msg.from_uid}` : msg.from_uid;
164
+ }
165
+ // Group is per-CHANNEL (shared): every member of a group shares one session,
166
+ // history, working dir, and memory — a group is a collective workspace, not
167
+ // N private chats. (Reverses the per-(channel×user) split from PR #64; see
168
+ // src/cwd-resolver.ts header. Space isolation is implicit: one bot = one
169
+ // space = one process with its own dataDir/cwdBase/memoryBase.)
170
+ //
171
+ // channel_id IS the group's identity here, so a missing one is unroutable —
172
+ // never fall back to '' (that would collapse every channel-less group message
173
+ // into ONE shared session across unrelated channels, leaking history/memory
174
+ // between them). Fail loud instead; route() treats the throw as a drop.
175
+ if (!msg.channel_id) {
176
+ throw new Error('Group message has no channel_id — cannot derive a session key');
177
+ }
178
+ return msg.channel_id;
179
+ }
180
+ /**
181
+ * Extract spaceId from channel_id.
182
+ * DM format: s{spaceId}_{uid1}@s{spaceId}_{uid2}
183
+ * Group format: s{spaceId}_{groupNo} (but groups already use channel_id in key)
184
+ */
185
+ extractSpaceId(msg) {
186
+ // For groups, channel_id already provides isolation
187
+ if (this.isGroupLike(msg.channel_type))
188
+ return "";
189
+ // DM: try from_uid first (format: s{spaceId}_{peerId})
190
+ const uid = msg.from_uid;
191
+ if (uid.startsWith("s")) {
192
+ const lastUnderscore = uid.lastIndexOf("_");
193
+ if (lastUnderscore > 0) {
194
+ return uid.substring(1, lastUnderscore);
195
+ }
196
+ }
197
+ // DM compound: s{spaceId}_{uid1}@s{spaceId}_{uid2}
198
+ const channelId = msg.channel_id;
199
+ if (channelId && channelId.startsWith("s")) {
200
+ const atIdx = channelId.indexOf("@");
201
+ const firstPart = atIdx > 0 ? channelId.substring(0, atIdx) : channelId;
202
+ if (firstPart.startsWith("s")) {
203
+ const lastUnderscore = firstPart.lastIndexOf("_");
204
+ if (lastUnderscore > 0) {
205
+ return firstPart.substring(1, lastUnderscore);
206
+ }
207
+ }
208
+ }
209
+ return "";
210
+ }
211
+ isBlockedBot(uid) {
212
+ return this.config.botBlocklist?.includes(uid) ?? false;
213
+ }
214
+ /**
215
+ * G14: Heuristic bot detection. Octo bot uids conventionally end in `_bot`.
216
+ * This is NOT a perfect check — a human could pick that suffix — but it
217
+ * catches the common case where a bot DMs another bot and triggers an
218
+ * uncontrolled response loop. Bots whitelisted in `allowedBotUids` bypass
219
+ * this gate.
220
+ */
221
+ looksLikeBot(uid) {
222
+ if (this.knownBotUids.has(uid))
223
+ return true;
224
+ if (uid.endsWith('_bot'))
225
+ return true;
226
+ return false;
227
+ }
228
+ isAllowedBot(uid) {
229
+ return this.config.allowedBotUids?.includes(uid) ?? false;
230
+ }
231
+ isGroupLike(channelType) {
232
+ return channelType === ChannelType.Group || channelType === ChannelType.CommunityTopic;
233
+ }
234
+ /**
235
+ * Allowlist of channel types we actually converse on: DM, Group, and
236
+ * CommunityTopic. Octo also emits system/command channels (e.g. channel_type
237
+ * 8 "systemcmdonline" on connect) which are NOT user conversations — those
238
+ * must be dropped, otherwise they fall through the DM/group gates and get
239
+ * answered with an unsolicited LLM reply. Found in live deployment (#68).
240
+ */
241
+ isSupportedChannel(channelType) {
242
+ return channelType === ChannelType.DM || this.isGroupLike(channelType);
243
+ }
244
+ isMentioned(msg) {
245
+ const mention = msg.payload.mention;
246
+ if (!mention)
247
+ return false;
248
+ if (mention.uids?.includes(this.robotId))
249
+ return true;
250
+ // Note: mention.all is a humans-only signal (@所有人), bots do NOT respond.
251
+ if (mention.ais)
252
+ return true;
253
+ return false;
254
+ }
255
+ /**
256
+ * #115: True for a GENUINE in-process cron fire — `payload._cronFire` AND a
257
+ * matching per-process nonce. Such messages bypass the group @mention gate
258
+ * (owner-gated at creation, bound to this session). A forged inbound payload
259
+ * can set `_cronFire` but not the secret nonce, so it does not pass. Real
260
+ * inbound messages never carry the marker.
261
+ */
262
+ isCronFire(msg) {
263
+ return isAuthenticCronFire(msg.payload);
264
+ }
265
+ async processMessage(msg, key) {
266
+ // Skip messages from self.
267
+ if (msg.from_uid === this.robotId)
268
+ return null;
269
+ // Drop anything that isn't a real conversation channel (DM / group /
270
+ // community topic). Octo emits system/command channels (e.g. channel_type 8
271
+ // "systemcmdonline" on connect) that otherwise slip past the DM/group gates
272
+ // and get an unsolicited reply (#68).
273
+ if (!this.isSupportedChannel(msg.channel_type))
274
+ return null;
275
+ // Skip stream update messages (G21) — only process final (non-stream) messages.
276
+ // When streamOn is true, this is a partial update of an ongoing stream; the final
277
+ // message arrives with streamOn=false and contains the complete content.
278
+ if (msg.streamOn)
279
+ return null;
280
+ // DM blocklist filter.
281
+ if (msg.channel_type === ChannelType.DM && this.isBlockedBot(msg.from_uid)) {
282
+ return null;
283
+ }
284
+ // G14: DM from anything that looks like a bot — silently drop unless
285
+ // explicitly whitelisted. Prevents bot↔bot reply loops.
286
+ if (msg.channel_type === ChannelType.DM &&
287
+ this.looksLikeBot(msg.from_uid) &&
288
+ !this.isAllowedBot(msg.from_uid)) {
289
+ return null;
290
+ }
291
+ // Group: drop messages from other bots (blocklisted or self) entirely.
292
+ if (this.isGroupLike(msg.channel_type) && this.isBlockedBot(msg.from_uid)) {
293
+ return null;
294
+ }
295
+ // G14: Group messages from bot-looking uids — only respond if explicitly
296
+ // @-mentioned. The mention gate below already enforces this, but bots in
297
+ // the blocklist (above) get hard-dropped without even checking mentions.
298
+ // Group mention gate — skip unless mentioned OR in mention-free group (G12).
299
+ // #115: cron-fired synthetic messages bypass the @mention requirement — they
300
+ // were created (owner-gated) and bound to this session; there's no human to
301
+ // @-mention the bot at fire time. Rate limiting below still applies.
302
+ if (this.isGroupLike(msg.channel_type) && !this.isMentioned(msg) && !this.isCronFire(msg)) {
303
+ // G12: Check if this group is in the mention-free list
304
+ const isMentionFree = this.config.mentionFreeGroups?.includes(msg.channel_id ?? '') ?? false;
305
+ if (!isMentionFree) {
306
+ return null;
307
+ }
308
+ // Multi-bot loop guard: in a mention-free group there is no @-mention gate
309
+ // to stop one bot from replying to another bot's plain-text message. Drop
310
+ // messages from known/bot-looking uids (unless explicitly whitelisted) so
311
+ // two bots in the same mention-free room cannot enter an unbounded reply
312
+ // loop. An @-mention still goes through (handled by the branch above).
313
+ if (this.looksLikeBot(msg.from_uid) && !this.isAllowedBot(msg.from_uid)) {
314
+ return null;
315
+ }
316
+ }
317
+ // Skip system events (group join/leave, etc.) — no user-facing reply needed.
318
+ if (msg.payload.event)
319
+ return null;
320
+ // Rate limit check BEFORE non-text check — prevents DM spam of non-text
321
+ // messages from bypassing rate limiting entirely.
322
+ // G20 fix: peek all three buckets without consuming; only consume on full
323
+ // pass. On block, attach notified state to the actual blocking bucket so
324
+ // the debounce reply doesn't spam when a different bucket has tokens.
325
+ // #115: cron fires skip the rate limit — a scheduler-fired task is an
326
+ // operator-scheduled action (already bounded by the cron interval), not
327
+ // user spam. Without this a fire that lands while the owner's bucket is
328
+ // exhausted would be silently dropped while its nextRun has already advanced.
329
+ if (!this.isCronFire(msg)) {
330
+ const blocker = this.checkAllRateLimits(key, msg.from_uid);
331
+ if (blocker) {
332
+ if (!blocker.notified) {
333
+ blocker.notified = true;
334
+ await this.replySafe(msg, '请稍后再试');
335
+ }
336
+ return {
337
+ sessionKey: key,
338
+ shouldProcess: false,
339
+ message: msg,
340
+ rejectionReason: 'rate_limited',
341
+ };
342
+ }
343
+ }
344
+ // G1: All payload types are now resolved by inbound.resolveContent in
345
+ // handleMessage. The router only filters rate limits, size, and bot
346
+ // loops — type-specific handling lives in the pipeline.
347
+ // Q10: Reject messages exceeding content length limit (text only — media
348
+ // URLs are bounded by their own size and rendered via resolveContent).
349
+ const content = msg.payload.content ?? '';
350
+ if (msg.payload.type === MessageType.Text &&
351
+ Buffer.byteLength(content, 'utf-8') > MAX_CONTENT_BYTES) {
352
+ await this.replySafe(msg, '消息过长,请缩短后重试');
353
+ return {
354
+ sessionKey: key,
355
+ shouldProcess: false,
356
+ message: msg,
357
+ rejectionReason: 'oversized',
358
+ };
359
+ }
360
+ // G13: Strip leading @botname from group TEXT messages for cleaner LLM input.
361
+ // For non-text payloads (images, files, etc.) cleanContent stays undefined
362
+ // so the pipeline falls back to the resolveContent rendering instead of an
363
+ // empty string.
364
+ let cleanContent;
365
+ if (msg.payload.type === MessageType.Text) {
366
+ cleanContent = content;
367
+ if (this.isGroupLike(msg.channel_type)) {
368
+ const mention = msg.payload.mention;
369
+ // Path 1: entities-based removal (precise offset/length).
370
+ if (mention?.entities && Array.isArray(mention.entities)) {
371
+ const botEntity = mention.entities.find((e) => e.uid === this.robotId && e.offset === 0);
372
+ if (botEntity && typeof botEntity.length === 'number') {
373
+ cleanContent = content.substring(botEntity.length).trimStart();
374
+ }
375
+ }
376
+ // Path 2: regex fallback — only when the bot was explicitly @mentioned.
377
+ // In mention-free groups (G12) where the bot wasn't @'d, do NOT touch
378
+ // the message — a leading @ is addressed to someone else.
379
+ if (cleanContent === content && this.isMentioned(msg)) {
380
+ cleanContent = content.replace(/^@\S+\s*/, '').trimStart();
381
+ }
382
+ // If stripping emptied the content, keep original.
383
+ if (!cleanContent)
384
+ cleanContent = content;
385
+ }
386
+ }
387
+ return { sessionKey: key, shouldProcess: true, message: msg, cleanContent };
388
+ }
389
+ /**
390
+ * G20 fix: Check all three rate limits (global, per-session, per-user) in
391
+ * one pass. Refills all three buckets, then either consumes 1 token from
392
+ * each (when all pass) or returns the blocking bucket (when any fails).
393
+ *
394
+ * Returns null on success (tokens consumed), or the blocking bucket on
395
+ * failure (no tokens consumed). The caller uses the blocking bucket's
396
+ * `notified` flag to debounce the "请稍后再试" reply per-bucket, so a user
397
+ * blocked by per-user limit doesn't get spammed when their per-session
398
+ * bucket still has tokens.
399
+ */
400
+ checkAllRateLimits(key, uid) {
401
+ this.cleanStaleBuckets();
402
+ const now = Date.now();
403
+ const maxPerMinute = this.config.rateLimit.maxPerMinute;
404
+ const globalMax = maxPerMinute * GLOBAL_RATE_MULTIPLIER;
405
+ const globalBucket = this.getOrCreateGlobalBucket(now, globalMax);
406
+ // Per-participant session bucket: key by session AND uid. For a group the
407
+ // sessionKey is the channel_id (shared), so keying the rate bucket by
408
+ // sessionKey alone would collapse the WHOLE room into one maxPerMinute quota
409
+ // (the 6th message/min from ANY member blocked). Including uid restores a
410
+ // per-member per-channel quota — matching the pre-shared-session behavior —
411
+ // while the global + per-user buckets still bound abuse. For a DM the
412
+ // sessionKey already embeds the peer, so this is just per-peer. Joined with a
413
+ // newline (never present in a uid/key) so distinct pairs can't alias.
414
+ const sessionBucketKey = `${key}\n${uid}`;
415
+ const sessionBucket = this.getOrCreateBucket(this.tokenBuckets, sessionBucketKey, now, maxPerMinute);
416
+ const userBucket = this.getOrCreateBucket(this.userBuckets, uid, now, maxPerMinute);
417
+ this.refillBucket(globalBucket, now, globalMax);
418
+ this.refillBucket(sessionBucket, now, maxPerMinute);
419
+ this.refillBucket(userBucket, now, maxPerMinute);
420
+ // Check in priority order: global → per-user → per-session.
421
+ // Per-user before per-session so a user blocked across groups gets a
422
+ // consistent debounce target instead of one per session bucket.
423
+ if (globalBucket.tokens < 1)
424
+ return globalBucket;
425
+ if (userBucket.tokens < 1)
426
+ return userBucket;
427
+ if (sessionBucket.tokens < 1)
428
+ return sessionBucket;
429
+ // All pass — consume one token from each, and clear notified flags so
430
+ // future blocks get a fresh debounce window.
431
+ globalBucket.tokens -= 1;
432
+ sessionBucket.tokens -= 1;
433
+ userBucket.tokens -= 1;
434
+ globalBucket.notified = false;
435
+ sessionBucket.notified = false;
436
+ userBucket.notified = false;
437
+ return null;
438
+ }
439
+ getOrCreateBucket(map, key, now, capacity) {
440
+ let bucket = map.get(key);
441
+ if (!bucket) {
442
+ bucket = { tokens: capacity, lastRefill: now, notified: false };
443
+ map.set(key, bucket);
444
+ }
445
+ return bucket;
446
+ }
447
+ getOrCreateGlobalBucket(now, capacity) {
448
+ if (!this.globalBucket) {
449
+ this.globalBucket = { tokens: capacity, lastRefill: now, notified: false };
450
+ }
451
+ return this.globalBucket;
452
+ }
453
+ refillBucket(bucket, now, capacity) {
454
+ const elapsed = now - bucket.lastRefill;
455
+ const refill = (elapsed / 60_000) * capacity;
456
+ bucket.tokens = Math.min(capacity, bucket.tokens + refill);
457
+ bucket.lastRefill = now;
458
+ }
459
+ /** Remove token buckets that haven't been used in 5 minutes. */
460
+ cleanStaleBuckets() {
461
+ const now = Date.now();
462
+ for (const [key, bucket] of this.tokenBuckets) {
463
+ if (now - bucket.lastRefill > BUCKET_STALE_MS) {
464
+ this.tokenBuckets.delete(key);
465
+ }
466
+ }
467
+ for (const [uid, bucket] of this.userBuckets) {
468
+ if (now - bucket.lastRefill > BUCKET_STALE_MS) {
469
+ this.userBuckets.delete(uid);
470
+ }
471
+ }
472
+ }
473
+ async replySafe(msg, content) {
474
+ if (!msg.channel_id || msg.channel_type === undefined)
475
+ return;
476
+ try {
477
+ await sendMessage({
478
+ apiUrl: this.config.apiUrl,
479
+ botToken: this.config.botToken,
480
+ channelId: msg.channel_id,
481
+ channelType: msg.channel_type,
482
+ content,
483
+ });
484
+ }
485
+ catch (err) {
486
+ console.error(`session-router: reply failed: ${String(err)}`);
487
+ }
488
+ }
489
+ }
490
+ //# sourceMappingURL=session-router.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-router.js","sourceRoot":"","sources":["../src/session-router.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Session Store — SQLite persistence via better-sqlite3 + thin adapter.
3
+ */
4
+ import type { DbAdapter } from './db-adapter.js';
5
+ export interface Session {
6
+ id: string;
7
+ channelId: string;
8
+ channelType: number;
9
+ createdAt: number;
10
+ updatedAt: number;
11
+ }
12
+ export declare class SessionStore {
13
+ private readonly adapter;
14
+ private selectSession;
15
+ private insertSession;
16
+ private touchSession;
17
+ private insertMessage;
18
+ private selectRecentMessages;
19
+ private deleteExpired;
20
+ private deleteSessionStmt;
21
+ private upsertResetBarrier;
22
+ private selectResetBarrier;
23
+ private upsertSdkSession;
24
+ private selectSdkSession;
25
+ private deleteSdkSession;
26
+ private deleteExpiredSdkSessions;
27
+ /** Tracks the last message_seq at which the bot replied, per group session key. */
28
+ private lastBotReplySeq;
29
+ constructor(adapter: DbAdapter);
30
+ init(): void;
31
+ getOrCreate(id: string, channelId: string, channelType: number): Session;
32
+ appendUser(sessionId: string, content: string, messageSeq?: number, fromName?: string): void;
33
+ appendAssistant(sessionId: string, content: string, messageSeq?: number, botName?: string): void;
34
+ private append;
35
+ /**
36
+ * Render one history turn with speaker attribution. Group sessions are shared
37
+ * across members, so every turn names its sender — `[user <name>]:` and
38
+ * `[assistant <botName>]:`. The name is sanitized at write time (see append())
39
+ * so it cannot forge turn labels; the `?? role` coalesce only guards rows from
40
+ * before this column existed.
41
+ *
42
+ * SECURITY: the message CONTENT is also user-controlled and travels into the
43
+ * shared `[Conversation history]` block. A body whose line starts with
44
+ * `[assistant ...]:` / `[user ...]:` would forge an extra turn that, in shared
45
+ * group mode, every member then reads as real conversation (cross-user context
46
+ * poisoning — the same threat the from_name strip closes, but via content and
47
+ * easier to exploit since no display name is needed). So we neutralize any
48
+ * line-leading role label in the content here, at render time. This is the one
49
+ * coherent policy: turn labels can ONLY originate from this renderer, never
50
+ * from a user-controlled name or body.
51
+ */
52
+ private renderTurn;
53
+ buildHistoryPrefix(sessionId: string, limit: number): string;
54
+ cleanExpired(): number;
55
+ deleteSession(sessionId: string): void;
56
+ /**
57
+ * v0.3 /reset: record a barrier so cold-start backfill never resurrects
58
+ * history at or before `resetSeq`. Persisted independently of the session row
59
+ * (survives deleteSession + restart). Monotonic — a later reset raises the
60
+ * barrier, an out-of-order/older seq is ignored.
61
+ *
62
+ * `resetSeq` is the message_seq of the /reset command itself; everything up to
63
+ * and including it is considered intentionally discarded.
64
+ */
65
+ setResetBarrier(sessionId: string, resetSeq: number): void;
66
+ /** Return the reset barrier seq for a session, or undefined if never reset. */
67
+ getResetBarrier(sessionId: string): number | undefined;
68
+ /**
69
+ * v0.3 persistent sessions: record the SDK session UUID for a sessionKey so a
70
+ * later turn can resume it. Upserts (latest wins).
71
+ */
72
+ setSdkSessionId(sessionId: string, sdkSessionId: string): void;
73
+ /** Return the stored SDK session UUID for a sessionKey, or undefined. */
74
+ getSdkSessionId(sessionId: string): string | undefined;
75
+ /** Forget the SDK session mapping (e.g. on /reset or a resume failure). */
76
+ clearSdkSessionId(sessionId: string): void;
77
+ close(): void;
78
+ /** Record the message_seq at which the bot last replied for a session. */
79
+ setLastBotReplySeq(sessionId: string, seq: number): void;
80
+ /** Get the message_seq at which the bot last replied for a session. */
81
+ getLastBotReplySeq(sessionId: string): number | undefined;
82
+ /**
83
+ * Build history prefix with answered/new segmentation (G10).
84
+ * Messages with message_seq <= lastBotReplySeq are labeled [answered history],
85
+ * messages after are labeled [new messages]. Falls back to flat history if
86
+ * no lastBotReplySeq tracked or no seq data available.
87
+ */
88
+ buildSegmentedHistoryPrefix(sessionId: string, limit: number): string;
89
+ }