@mininglamp-oss/cc-channel-octo 1.0.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.
Files changed (87) hide show
  1. package/CHANGELOG.md +349 -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 +79 -0
  7. package/dist/agent-bridge.js +392 -0
  8. package/dist/agent-bridge.js.map +1 -0
  9. package/dist/commands.d.ts +57 -0
  10. package/dist/commands.js +121 -0
  11. package/dist/commands.js.map +1 -0
  12. package/dist/config.d.ts +278 -0
  13. package/dist/config.js +330 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/cron-evaluator.d.ts +53 -0
  16. package/dist/cron-evaluator.js +191 -0
  17. package/dist/cron-evaluator.js.map +1 -0
  18. package/dist/cron-fire-marker.d.ts +24 -0
  19. package/dist/cron-fire-marker.js +25 -0
  20. package/dist/cron-fire-marker.js.map +1 -0
  21. package/dist/cron-scheduler.d.ts +46 -0
  22. package/dist/cron-scheduler.js +114 -0
  23. package/dist/cron-scheduler.js.map +1 -0
  24. package/dist/cron-store.d.ts +62 -0
  25. package/dist/cron-store.js +63 -0
  26. package/dist/cron-store.js.map +1 -0
  27. package/dist/cron-tool.d.ts +44 -0
  28. package/dist/cron-tool.js +151 -0
  29. package/dist/cron-tool.js.map +1 -0
  30. package/dist/cwd-resolver.d.ts +72 -0
  31. package/dist/cwd-resolver.js +166 -0
  32. package/dist/cwd-resolver.js.map +1 -0
  33. package/dist/db-adapter.d.ts +21 -0
  34. package/dist/db-adapter.js +64 -0
  35. package/dist/db-adapter.js.map +1 -0
  36. package/dist/file-inline-wrap.d.ts +94 -0
  37. package/dist/file-inline-wrap.js +243 -0
  38. package/dist/file-inline-wrap.js.map +1 -0
  39. package/dist/gateway.d.ts +100 -0
  40. package/dist/gateway.js +420 -0
  41. package/dist/gateway.js.map +1 -0
  42. package/dist/group-config.d.ts +41 -0
  43. package/dist/group-config.js +104 -0
  44. package/dist/group-config.js.map +1 -0
  45. package/dist/group-context.d.ts +64 -0
  46. package/dist/group-context.js +396 -0
  47. package/dist/group-context.js.map +1 -0
  48. package/dist/inbound.d.ts +136 -0
  49. package/dist/inbound.js +667 -0
  50. package/dist/inbound.js.map +1 -0
  51. package/dist/index.d.ts +33 -0
  52. package/dist/index.js +922 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/media-inbound.d.ts +38 -0
  55. package/dist/media-inbound.js +131 -0
  56. package/dist/media-inbound.js.map +1 -0
  57. package/dist/mention-utils.d.ts +99 -0
  58. package/dist/mention-utils.js +185 -0
  59. package/dist/mention-utils.js.map +1 -0
  60. package/dist/octo/api.d.ts +148 -0
  61. package/dist/octo/api.js +320 -0
  62. package/dist/octo/api.js.map +1 -0
  63. package/dist/octo/socket.d.ts +102 -0
  64. package/dist/octo/socket.js +793 -0
  65. package/dist/octo/socket.js.map +1 -0
  66. package/dist/octo/types.d.ts +126 -0
  67. package/dist/octo/types.js +35 -0
  68. package/dist/octo/types.js.map +1 -0
  69. package/dist/prompt-safety.d.ts +78 -0
  70. package/dist/prompt-safety.js +148 -0
  71. package/dist/prompt-safety.js.map +1 -0
  72. package/dist/session-router.d.ts +127 -0
  73. package/dist/session-router.js +432 -0
  74. package/dist/session-router.js.map +1 -0
  75. package/dist/session-store.d.ts +89 -0
  76. package/dist/session-store.js +297 -0
  77. package/dist/session-store.js.map +1 -0
  78. package/dist/skill-linker.d.ts +31 -0
  79. package/dist/skill-linker.js +160 -0
  80. package/dist/skill-linker.js.map +1 -0
  81. package/dist/stream-relay.d.ts +42 -0
  82. package/dist/stream-relay.js +243 -0
  83. package/dist/stream-relay.js.map +1 -0
  84. package/dist/url-policy.d.ts +103 -0
  85. package/dist/url-policy.js +290 -0
  86. package/dist/url-policy.js.map +1 -0
  87. package/package.json +79 -0
@@ -0,0 +1,104 @@
1
+ /**
2
+ * v1.0: GROUP.md / THREAD.md per-conversation instruction files.
3
+ *
4
+ * Operators can drop a markdown file with custom instructions for a specific
5
+ * group (or, later, thread) into a configured directory. Its contents are
6
+ * injected into the agent's system prompt as a trusted instruction block, so a
7
+ * group can have its own persona / rules without code changes.
8
+ *
9
+ * SECURITY — read carefully. The `[Group instructions]` block is injected into
10
+ * the system prompt UNSANITIZED, so its contents are trusted. That trust holds
11
+ * ONLY if the file is writable solely by the operator (the gateway process user).
12
+ *
13
+ * Placing `groupConfigDir` outside `cwdBase` is necessary but NOT sufficient:
14
+ * under the shipped defaults (`allowedTools: '*'`, `bypassPermissions`) the agent
15
+ * has `Bash`/`Write` and can write ABSOLUTE paths anywhere the gateway user can
16
+ * write — `cwdBase` is a starting dir, not a chroot. So a malicious user in one
17
+ * group could drive the agent to write `<groupConfigDir>/<otherGroup>.md` and
18
+ * inject persistent, trusted instructions into a different group.
19
+ *
20
+ * The real protection is OS-level: `groupConfigDir` and its files MUST be made
21
+ * non-writable by the gateway process user (e.g. root-owned, mode 0755/0644),
22
+ * and/or the deployment hardened (drop `Bash`, sandboxed FS, unprivileged user).
23
+ * As cheap defense-in-depth, loadGroupConfig() refuses to inject a file that is
24
+ * group- or world-writable. The group id is filename-pinned to a safe slug so a
25
+ * crafted id can't traverse out of the config dir.
26
+ */
27
+ import { closeSync, existsSync, openSync, readSync, statSync } from 'node:fs';
28
+ import { join } from 'node:path';
29
+ /** Max bytes of an instruction file we will inject (keeps the prompt bounded). */
30
+ export const MAX_GROUP_CONFIG_BYTES = 16_384; // 16 KiB
31
+ /**
32
+ * Only allow ids that are safe as a single path segment — letters, digits, and
33
+ * a few separators. Anything else (slashes, dots-only, `..`) is rejected so a
34
+ * channel/thread id cannot escape `groupConfigDir`.
35
+ */
36
+ function isSafeId(id) {
37
+ return /^[a-zA-Z0-9._-]+$/.test(id) && id !== '.' && id !== '..';
38
+ }
39
+ /**
40
+ * Load the instruction file for a group, or undefined when none applies.
41
+ *
42
+ * Looks for `<groupConfigDir>/<groupId>.md`. Returns the trimmed contents,
43
+ * truncated to MAX_GROUP_CONFIG_BYTES. Returns undefined when:
44
+ * - groupConfigDir is not configured,
45
+ * - groupId is empty or unsafe as a path segment,
46
+ * - the file does not exist or is unreadable.
47
+ *
48
+ * Never throws — a misconfigured dir or unreadable file degrades to "no custom
49
+ * instructions" rather than failing the turn.
50
+ */
51
+ export function loadGroupConfig(groupConfigDir, groupId) {
52
+ if (!groupConfigDir)
53
+ return undefined;
54
+ if (!groupId || !isSafeId(groupId))
55
+ return undefined;
56
+ const path = join(groupConfigDir, `${groupId}.md`);
57
+ try {
58
+ if (!existsSync(path))
59
+ return undefined;
60
+ const st = statSync(path);
61
+ if (!st.isFile())
62
+ return undefined;
63
+ // Defense-in-depth: refuse a group/world-writable file. Its contents are
64
+ // injected UNSANITIZED into the system prompt, so a file anyone-but-the-
65
+ // operator can write is an untrusted injection sink. This catches the most
66
+ // common misconfiguration; it is NOT a substitute for proper OS perms +
67
+ // a hardened deployment (the agent can still write operator-owned paths
68
+ // under default Bash/bypassPermissions — see the module header).
69
+ if ((st.mode & 0o022) !== 0) {
70
+ console.error(`[cc-channel-octo] refusing group config ${path}: file is group/world-writable ` +
71
+ `(mode ${(st.mode & 0o777).toString(8)}). Make it writable only by the gateway user.`);
72
+ return undefined;
73
+ }
74
+ // Read at most MAX+1 bytes so a mistakenly huge file can't block the event
75
+ // loop or allocate unbounded memory on every group turn. Reading one extra
76
+ // byte lets us detect (and mark) truncation.
77
+ const fd = openSync(path, 'r');
78
+ let content;
79
+ let wasTruncated = false;
80
+ try {
81
+ const buf = Buffer.allocUnsafe(MAX_GROUP_CONFIG_BYTES + 1);
82
+ const bytesRead = readSync(fd, buf, 0, MAX_GROUP_CONFIG_BYTES + 1, 0);
83
+ wasTruncated = bytesRead > MAX_GROUP_CONFIG_BYTES;
84
+ const slice = buf.subarray(0, Math.min(bytesRead, MAX_GROUP_CONFIG_BYTES));
85
+ content = slice.toString('utf-8');
86
+ }
87
+ finally {
88
+ closeSync(fd);
89
+ }
90
+ if (wasTruncated) {
91
+ // The slice may end mid-codepoint; trim back to a valid UTF-8 boundary by
92
+ // dropping any trailing replacement char produced by a split sequence.
93
+ content = content.replace(/�+$/, '');
94
+ content += '\n[… group config truncated]';
95
+ }
96
+ const trimmed = content.trim();
97
+ return trimmed.length > 0 ? trimmed : undefined;
98
+ }
99
+ catch (err) {
100
+ console.error(`[cc-channel-octo] group config read failed for ${path}: ${String(err)}`);
101
+ return undefined;
102
+ }
103
+ }
104
+ //# sourceMappingURL=group-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"group-config.js","sourceRoot":"","sources":["../src/group-config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Group Context — group chat message cache + maxContextChars budget + mention mapping.
3
+ */
4
+ import type { DbAdapter } from './db-adapter.js';
5
+ export declare class GroupContext {
6
+ private readonly messageCache;
7
+ private readonly memberMapByChannel;
8
+ private readonly nameToUidByChannel;
9
+ private readonly robotFlags;
10
+ private readonly lastRefresh;
11
+ private readonly adapter;
12
+ private readonly maxContextChars;
13
+ private readonly maxWindowSize;
14
+ private upsertMember;
15
+ private selectMembers;
16
+ private insertMessage;
17
+ private selectRecentMessages;
18
+ private deleteOldMessages;
19
+ private selectMessagesSince;
20
+ private selectMaxId;
21
+ private upsertCursor;
22
+ private selectCursor;
23
+ constructor(adapter: DbAdapter, maxContextChars: number);
24
+ private initStatements;
25
+ private getMemberMap;
26
+ private getNameToUid;
27
+ pushMessage(channelId: string, fromUid: string, fromName: string, content: string, timestamp: number): void;
28
+ learnMember(channelId: string, uid: string, name: string): void;
29
+ refreshMembers(channelId: string, apiUrl: string, botToken: string): Promise<void>;
30
+ fetchAndLearnUser(uid: string, channelId: string, apiUrl: string, botToken: string): Promise<string | undefined>;
31
+ buildContext(channelId: string): string;
32
+ /** Read the per-channel consumption cursor (highest already-injected id), or 0. */
33
+ getContextCursor(channelId: string): number;
34
+ /** Advance the per-channel cursor to `lastId` (monotonic — never moves backward). */
35
+ setContextCursor(channelId: string, lastId: number): void;
36
+ /** The highest group_messages.id for a channel (for cursor priming), or 0. */
37
+ getMaxMessageId(channelId: string): number;
38
+ /**
39
+ * Build a group-context block from messages NEWER than `sinceId` (the delta the
40
+ * model hasn't seen yet), within `maxContextChars`. Returns the block text (same
41
+ * `[Recent group messages]` format as buildContext) and the highest id that
42
+ * EXISTS in the channel above `sinceId` (so the caller advances the cursor past
43
+ * the whole delta, including any oldest lines the char budget dropped — those
44
+ * are the least-relevant and are intentionally not re-shown). Empty text + the
45
+ * unchanged cursor when there's nothing new.
46
+ *
47
+ * Rows are fetched newest-first (so a backlog larger than the budget keeps the
48
+ * most-recent messages); the selected slice is reversed back to chronological
49
+ * order for display. Unlike buildContext (in-memory rolling window), this reads
50
+ * from the DB so the cursor delta is exact even across restarts / window eviction.
51
+ */
52
+ buildContextSince(channelId: string, sinceId: number): {
53
+ text: string;
54
+ lastId: number;
55
+ };
56
+ resolveMentions(text: string, channelId: string): string[];
57
+ getName(uid: string, channelId: string): string | undefined;
58
+ /** G23: Check the server-authoritative robot flag for a group member. */
59
+ isRobot(channelId: string, uid: string): boolean | undefined;
60
+ loadMembersFromDb(channelId: string): void;
61
+ loadMessagesFromDb(channelId: string): void;
62
+ /** Load all persisted members and messages from DB (call once at startup). */
63
+ loadAllFromDb(): void;
64
+ }
@@ -0,0 +1,396 @@
1
+ /**
2
+ * Group Context — group chat message cache + maxContextChars budget + mention mapping.
3
+ */
4
+ import { getGroupMembers, fetchUserInfo } from './octo/api.js';
5
+ import { sanitizeDisplayName } from './prompt-safety.js';
6
+ const REFRESH_INTERVAL_MS = 60 * 60 * 1000;
7
+ export class GroupContext {
8
+ messageCache = new Map();
9
+ // Per-channel member maps to avoid cross-group name collisions
10
+ memberMapByChannel = new Map(); // channelId → uid → name
11
+ nameToUidByChannel = new Map(); // channelId → name → uid
12
+ // G23: per-channel robot flag map (channelId → uid → isRobot).
13
+ // Populated from refreshMembers; stored for future routing integrations
14
+ // (e.g. 免@ gate that should treat bot members differently).
15
+ robotFlags = new Map();
16
+ lastRefresh = new Map();
17
+ adapter;
18
+ maxContextChars;
19
+ maxWindowSize;
20
+ upsertMember;
21
+ selectMembers;
22
+ insertMessage;
23
+ selectRecentMessages;
24
+ deleteOldMessages;
25
+ selectMessagesSince;
26
+ selectMaxId;
27
+ upsertCursor;
28
+ selectCursor;
29
+ constructor(adapter, maxContextChars) {
30
+ this.adapter = adapter;
31
+ this.maxContextChars = maxContextChars;
32
+ this.maxWindowSize = 100;
33
+ this.initStatements();
34
+ }
35
+ initStatements() {
36
+ this.adapter.exec(`
37
+ CREATE TABLE IF NOT EXISTS group_messages (
38
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
39
+ channel_id TEXT NOT NULL,
40
+ from_uid TEXT NOT NULL,
41
+ from_name TEXT NOT NULL,
42
+ content TEXT NOT NULL,
43
+ timestamp INTEGER NOT NULL
44
+ )
45
+ `);
46
+ this.adapter.exec(`
47
+ CREATE INDEX IF NOT EXISTS idx_group_messages_channel
48
+ ON group_messages (channel_id, id DESC)
49
+ `);
50
+ // Per-channel consumption cursor: the highest group_messages.id that has
51
+ // already been injected into a turn for this channel. Only group messages
52
+ // NEWER than this are injected next turn (the bot's standing context lives in
53
+ // the SDK session, so re-injecting the whole window every turn would be both
54
+ // redundant and a frozen-prompt violation). Mirrors the reset_barriers /
55
+ // sdk_sessions single-row-per-key pattern.
56
+ this.adapter.exec(`
57
+ CREATE TABLE IF NOT EXISTS group_context_cursors (
58
+ channel_id TEXT PRIMARY KEY,
59
+ last_id INTEGER NOT NULL,
60
+ updated_at INTEGER NOT NULL
61
+ )
62
+ `);
63
+ this.upsertMember = this.adapter.prepare('INSERT INTO group_members (group_id, uid, name, updated_at) VALUES (?, ?, ?, ?) ON CONFLICT(group_id, uid) DO UPDATE SET name = excluded.name, updated_at = excluded.updated_at');
64
+ this.selectMembers = this.adapter.prepare('SELECT uid, name FROM group_members WHERE group_id = ?');
65
+ this.insertMessage = this.adapter.prepare('INSERT INTO group_messages (channel_id, from_uid, from_name, content, timestamp) VALUES (?, ?, ?, ?, ?)');
66
+ this.selectRecentMessages = this.adapter.prepare('SELECT from_uid, from_name, content, timestamp FROM group_messages WHERE channel_id = ? ORDER BY id DESC LIMIT ?');
67
+ this.deleteOldMessages = this.adapter.prepare('DELETE FROM group_messages WHERE channel_id = ? AND id NOT IN (SELECT id FROM group_messages WHERE channel_id = ? ORDER BY id DESC LIMIT ?)');
68
+ // Cursor delta: messages strictly newer than the cursor, NEWEST-first so a
69
+ // backlog larger than the fetch limit keeps the most-recent messages (the
70
+ // relevant ones) rather than ancient ones. LIMIT (maxWindowSize=100) is <=
71
+ // deleteOldMessages' retained 200, so every fetchable row survives trimming.
72
+ // buildContextSince re-sorts the budget-selected slice into chronological
73
+ // order for display. Mirrors the in-memory buildContext rolling-window.
74
+ this.selectMessagesSince = this.adapter.prepare('SELECT id, from_name, content FROM group_messages WHERE channel_id = ? AND id > ? ORDER BY id DESC LIMIT ?');
75
+ this.selectMaxId = this.adapter.prepare('SELECT MAX(id) AS maxId FROM group_messages WHERE channel_id = ?');
76
+ this.upsertCursor = this.adapter.prepare('INSERT INTO group_context_cursors (channel_id, last_id, updated_at) VALUES (?, ?, ?) ' +
77
+ 'ON CONFLICT(channel_id) DO UPDATE SET last_id = excluded.last_id, updated_at = excluded.updated_at ' +
78
+ 'WHERE excluded.last_id > group_context_cursors.last_id');
79
+ this.selectCursor = this.adapter.prepare('SELECT last_id FROM group_context_cursors WHERE channel_id = ?');
80
+ }
81
+ getMemberMap(channelId) {
82
+ let m = this.memberMapByChannel.get(channelId);
83
+ if (!m) {
84
+ m = new Map();
85
+ this.memberMapByChannel.set(channelId, m);
86
+ }
87
+ return m;
88
+ }
89
+ getNameToUid(channelId) {
90
+ let m = this.nameToUidByChannel.get(channelId);
91
+ if (!m) {
92
+ m = new Map();
93
+ this.nameToUidByChannel.set(channelId, m);
94
+ }
95
+ return m;
96
+ }
97
+ pushMessage(channelId, fromUid, fromName, content, timestamp) {
98
+ // SECURITY: fromName is the user-controlled IM display name and is rendered
99
+ // into the [Group context] block as `<name>:<content>`. Bound + strip it at
100
+ // the boundary (shared choke point) so it can't forge a label/line. fromUid
101
+ // is ALSO user-controlled, and sanitizeDisplayName returns its fallback
102
+ // verbatim — so sanitize the uid fallback too rather than passing it raw
103
+ // (PR #128 review: raw-uid-as-fallback re-introduces the injection).
104
+ const safeName = sanitizeDisplayName(fromName) || sanitizeDisplayName(fromUid) || 'unknown';
105
+ let window = this.messageCache.get(channelId);
106
+ if (!window) {
107
+ window = [];
108
+ this.messageCache.set(channelId, window);
109
+ }
110
+ window.push({ fromUid, fromName: safeName, content, timestamp });
111
+ while (window.length > this.maxWindowSize) {
112
+ window.shift();
113
+ }
114
+ this.learnMember(channelId, fromUid, safeName);
115
+ try {
116
+ this.insertMessage.run(channelId, fromUid, safeName, content, timestamp);
117
+ // Trim old messages to keep DB bounded (keep 2x window for safety)
118
+ this.deleteOldMessages.run(channelId, channelId, this.maxWindowSize * 2);
119
+ }
120
+ catch (err) {
121
+ console.error(`group-context: insert message failed: ${String(err)}`);
122
+ }
123
+ }
124
+ learnMember(channelId, uid, name) {
125
+ if (!uid || !name)
126
+ return;
127
+ const memberMap = this.getMemberMap(channelId);
128
+ const nameMap = this.getNameToUid(channelId);
129
+ const existing = memberMap.get(uid);
130
+ if (existing !== name) {
131
+ // Remove old reverse mapping only if it still points to THIS uid.
132
+ // If another user already claimed the same display name, don't clobber.
133
+ if (existing && nameMap.get(existing) === uid) {
134
+ nameMap.delete(existing);
135
+ }
136
+ memberMap.set(uid, name);
137
+ nameMap.set(name, uid);
138
+ try {
139
+ this.upsertMember.run(channelId, uid, name, Date.now());
140
+ }
141
+ catch (err) {
142
+ console.error(`group-context: upsert member failed: ${String(err)}`);
143
+ }
144
+ }
145
+ }
146
+ async refreshMembers(channelId, apiUrl, botToken) {
147
+ const now = Date.now();
148
+ const last = this.lastRefresh.get(channelId) ?? 0;
149
+ if (now - last < REFRESH_INTERVAL_MS)
150
+ return;
151
+ // Don't set lastRefresh here — only on success
152
+ try {
153
+ const members = await getGroupMembers({ apiUrl, botToken, groupNo: channelId });
154
+ this.lastRefresh.set(channelId, now); // Record only on success
155
+ const memberMap = this.getMemberMap(channelId);
156
+ const nameMap = this.getNameToUid(channelId);
157
+ for (const m of members) {
158
+ if (!m.uid || !m.name)
159
+ continue;
160
+ const oldName = memberMap.get(m.uid);
161
+ if (oldName && oldName !== m.name && nameMap.get(oldName) === m.uid) {
162
+ nameMap.delete(oldName);
163
+ }
164
+ memberMap.set(m.uid, m.name);
165
+ nameMap.set(m.name, m.uid);
166
+ // G23: Track server-authoritative robot flag for future 免@ gate.
167
+ if (m.robot !== undefined) {
168
+ let rfMap = this.robotFlags.get(channelId);
169
+ if (!rfMap) {
170
+ rfMap = new Map();
171
+ this.robotFlags.set(channelId, rfMap);
172
+ }
173
+ rfMap.set(m.uid, m.robot === 1);
174
+ }
175
+ try {
176
+ this.upsertMember.run(channelId, m.uid, m.name, now);
177
+ }
178
+ catch (err) {
179
+ console.error(`group-context: upsert member failed: ${String(err)}`);
180
+ }
181
+ }
182
+ }
183
+ catch (err) {
184
+ console.error(`group-context: refreshMembers(${channelId}) failed: ${String(err)}`);
185
+ // Don't update lastRefresh on failure — allow retry
186
+ }
187
+ }
188
+ async fetchAndLearnUser(uid, channelId, apiUrl, botToken) {
189
+ const memberMap = this.getMemberMap(channelId);
190
+ const cached = memberMap.get(uid);
191
+ if (cached)
192
+ return cached;
193
+ try {
194
+ const info = await fetchUserInfo({ apiUrl, botToken, uid });
195
+ if (info?.name) {
196
+ this.learnMember(channelId, uid, info.name);
197
+ return info.name;
198
+ }
199
+ }
200
+ catch (err) {
201
+ console.error(`group-context: fetchUserInfo(${uid}) failed: ${String(err)}`);
202
+ }
203
+ return undefined;
204
+ }
205
+ buildContext(channelId) {
206
+ const window = this.messageCache.get(channelId);
207
+ if (!window || window.length === 0)
208
+ return '';
209
+ const header = '[Recent group messages]\n';
210
+ const trailer = '\n';
211
+ const budget = this.maxContextChars - header.length - trailer.length;
212
+ if (budget <= 0)
213
+ return '';
214
+ const selected = [];
215
+ let used = 0;
216
+ for (let i = window.length - 1; i >= 0; i--) {
217
+ const m = window[i];
218
+ const line = `${m.fromName}:${m.content}`;
219
+ const cost = line.length + (selected.length > 0 ? 1 : 0);
220
+ if (used + cost > budget)
221
+ break;
222
+ selected.push(line);
223
+ used += cost;
224
+ }
225
+ if (selected.length === 0)
226
+ return '';
227
+ selected.reverse();
228
+ return `${header}${selected.join('\n')}${trailer}`;
229
+ }
230
+ /** Read the per-channel consumption cursor (highest already-injected id), or 0. */
231
+ getContextCursor(channelId) {
232
+ try {
233
+ const row = this.selectCursor.get(channelId);
234
+ return row?.last_id ?? 0;
235
+ }
236
+ catch (err) {
237
+ console.error(`group-context: getContextCursor(${channelId}) failed: ${String(err)}`);
238
+ return 0;
239
+ }
240
+ }
241
+ /** Advance the per-channel cursor to `lastId` (monotonic — never moves backward). */
242
+ setContextCursor(channelId, lastId) {
243
+ try {
244
+ this.upsertCursor.run(channelId, lastId, Date.now());
245
+ }
246
+ catch (err) {
247
+ console.error(`group-context: setContextCursor(${channelId}) failed: ${String(err)}`);
248
+ }
249
+ }
250
+ /** The highest group_messages.id for a channel (for cursor priming), or 0. */
251
+ getMaxMessageId(channelId) {
252
+ try {
253
+ const row = this.selectMaxId.get(channelId);
254
+ return row?.maxId ?? 0;
255
+ }
256
+ catch (err) {
257
+ console.error(`group-context: getMaxMessageId(${channelId}) failed: ${String(err)}`);
258
+ return 0;
259
+ }
260
+ }
261
+ /**
262
+ * Build a group-context block from messages NEWER than `sinceId` (the delta the
263
+ * model hasn't seen yet), within `maxContextChars`. Returns the block text (same
264
+ * `[Recent group messages]` format as buildContext) and the highest id that
265
+ * EXISTS in the channel above `sinceId` (so the caller advances the cursor past
266
+ * the whole delta, including any oldest lines the char budget dropped — those
267
+ * are the least-relevant and are intentionally not re-shown). Empty text + the
268
+ * unchanged cursor when there's nothing new.
269
+ *
270
+ * Rows are fetched newest-first (so a backlog larger than the budget keeps the
271
+ * most-recent messages); the selected slice is reversed back to chronological
272
+ * order for display. Unlike buildContext (in-memory rolling window), this reads
273
+ * from the DB so the cursor delta is exact even across restarts / window eviction.
274
+ */
275
+ buildContextSince(channelId, sinceId) {
276
+ let rows;
277
+ try {
278
+ rows = this.selectMessagesSince.all(channelId, sinceId, this.maxWindowSize);
279
+ }
280
+ catch (err) {
281
+ console.error(`group-context: buildContextSince(${channelId}) failed: ${String(err)}`);
282
+ return { text: '', lastId: sinceId };
283
+ }
284
+ if (rows.length === 0)
285
+ return { text: '', lastId: sinceId };
286
+ // rows are newest-first; the highest id is the first row. Advance the cursor to
287
+ // it regardless of what the budget keeps, so we never re-show a line.
288
+ const lastId = rows[0].id;
289
+ const header = '[Recent group messages]\n';
290
+ const trailer = '\n';
291
+ const budget = this.maxContextChars - header.length - trailer.length;
292
+ if (budget <= 0)
293
+ return { text: '', lastId };
294
+ // Walk newest→oldest (rows[0] is newest) keeping lines within budget.
295
+ const selected = [];
296
+ let used = 0;
297
+ for (let i = 0; i < rows.length; i++) {
298
+ const line = `${rows[i].from_name}:${rows[i].content}`;
299
+ const cost = line.length + (selected.length > 0 ? 1 : 0);
300
+ if (used + cost > budget)
301
+ break;
302
+ selected.push(line);
303
+ used += cost;
304
+ }
305
+ if (selected.length === 0)
306
+ return { text: '', lastId };
307
+ selected.reverse(); // chronological order for display
308
+ return { text: `${header}${selected.join('\n')}${trailer}`, lastId };
309
+ }
310
+ resolveMentions(text, channelId) {
311
+ const uids = [];
312
+ const seen = new Set();
313
+ const nameMap = this.nameToUidByChannel.get(channelId);
314
+ if (!nameMap)
315
+ return uids;
316
+ // Match @name where name is a run of word-like characters (letters, digits, underscores,
317
+ // CJK ideographs, Hangul, Kana, etc.) — stops at punctuation and whitespace.
318
+ const regex = /@([\w\u4e00-\u9fff\u3400-\u4dbf\uac00-\ud7af\u3040-\u309f\u30a0-\u30ff]+)/g;
319
+ let match;
320
+ while ((match = regex.exec(text)) !== null) {
321
+ // Strip known trailing punctuation that might stick to names
322
+ let name = match[1];
323
+ name = name.replace(/[,.!?;:,。!?;:、)\]]+$/, '');
324
+ if (!name)
325
+ continue;
326
+ const uid = nameMap.get(name);
327
+ if (uid && !seen.has(uid)) {
328
+ seen.add(uid);
329
+ uids.push(uid);
330
+ }
331
+ }
332
+ return uids;
333
+ }
334
+ getName(uid, channelId) {
335
+ return this.getMemberMap(channelId).get(uid);
336
+ }
337
+ /** G23: Check the server-authoritative robot flag for a group member. */
338
+ isRobot(channelId, uid) {
339
+ return this.robotFlags.get(channelId)?.get(uid);
340
+ }
341
+ loadMembersFromDb(channelId) {
342
+ try {
343
+ const rows = this.selectMembers.all(channelId);
344
+ const memberMap = this.getMemberMap(channelId);
345
+ const nameMap = this.getNameToUid(channelId);
346
+ for (const r of rows) {
347
+ if (!r.uid || !r.name)
348
+ continue;
349
+ memberMap.set(r.uid, r.name);
350
+ nameMap.set(r.name, r.uid);
351
+ }
352
+ }
353
+ catch (err) {
354
+ console.error(`group-context: loadMembersFromDb(${channelId}) failed: ${String(err)}`);
355
+ }
356
+ }
357
+ loadMessagesFromDb(channelId) {
358
+ try {
359
+ const rows = this.selectRecentMessages.all(channelId, this.maxWindowSize);
360
+ if (rows.length === 0)
361
+ return;
362
+ // Rows come in DESC order, reverse to chronological
363
+ rows.reverse();
364
+ const existing = this.messageCache.get(channelId);
365
+ if (existing && existing.length > 0)
366
+ return; // Don't overwrite live data
367
+ // Map snake_case DB columns to camelCase GroupMessage
368
+ this.messageCache.set(channelId, rows.map(r => ({
369
+ fromUid: r.from_uid,
370
+ fromName: r.from_name,
371
+ content: r.content,
372
+ timestamp: r.timestamp,
373
+ })));
374
+ }
375
+ catch (err) {
376
+ console.error(`group-context: loadMessagesFromDb(${channelId}) failed: ${String(err)}`);
377
+ }
378
+ }
379
+ /** Load all persisted members and messages from DB (call once at startup). */
380
+ loadAllFromDb() {
381
+ try {
382
+ const rows = this.adapter.prepare('SELECT DISTINCT group_id FROM group_members').all();
383
+ for (const row of rows) {
384
+ this.loadMembersFromDb(row.group_id);
385
+ this.loadMessagesFromDb(row.group_id);
386
+ }
387
+ if (rows.length > 0) {
388
+ console.log(`[group-context] Loaded members + messages for ${rows.length} group(s) from DB`);
389
+ }
390
+ }
391
+ catch (err) {
392
+ console.error(`group-context: loadAllFromDb failed: ${String(err)}`);
393
+ }
394
+ }
395
+ }
396
+ //# sourceMappingURL=group-context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"group-context.js","sourceRoot":"","sources":["../src/group-context.ts"],"names":[],"mappings":"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"}