@mininglamp-oss/cc-channel-octo 1.0.3-dev.203dc5f β 1.0.3-dev.3332be0
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/config.bot.example.json +2 -1
- package/config.example.json +8 -1
- package/dist/agent-bridge.js +6 -1
- package/dist/agent-bridge.js.map +1 -1
- package/dist/config.d.ts +80 -0
- package/dist/config.js +12 -0
- package/dist/config.js.map +1 -1
- package/dist/group-config.d.ts +18 -9
- package/dist/group-config.js +47 -10
- package/dist/group-config.js.map +1 -1
- package/dist/group-context.d.ts +18 -0
- package/dist/group-context.js +86 -7
- package/dist/group-context.js.map +1 -1
- package/dist/group-md-cache.d.ts +119 -0
- package/dist/group-md-cache.js +172 -0
- package/dist/group-md-cache.js.map +1 -0
- package/dist/group-md-events.d.ts +75 -0
- package/dist/group-md-events.js +74 -0
- package/dist/group-md-events.js.map +1 -0
- package/dist/group-md-tool.d.ts +80 -0
- package/dist/group-md-tool.js +181 -0
- package/dist/group-md-tool.js.map +1 -0
- package/dist/group-md-writeback.d.ts +183 -0
- package/dist/group-md-writeback.js +223 -0
- package/dist/group-md-writeback.js.map +1 -0
- package/dist/group-md.d.ts +76 -0
- package/dist/group-md.js +187 -0
- package/dist/group-md.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +130 -15
- package/dist/index.js.map +1 -1
- package/dist/octo/api.d.ts +157 -1
- package/dist/octo/api.js +189 -1
- package/dist/octo/api.js.map +1 -1
- package/dist/octo/channel-id.d.ts +36 -0
- package/dist/octo/channel-id.js +52 -0
- package/dist/octo/channel-id.js.map +1 -0
- package/dist/octo/types.d.ts +22 -0
- package/dist/octo/types.js.map +1 -1
- package/dist/prompt-safety.d.ts +20 -0
- package/dist/prompt-safety.js +24 -0
- package/dist/prompt-safety.js.map +1 -1
- package/dist/session-router.d.ts +63 -1
- package/dist/session-router.js +129 -3
- package/dist/session-router.js.map +1 -1
- package/dist/session-store.d.ts +26 -0
- package/dist/session-store.js +64 -1
- package/dist/session-store.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GROUP.md / THREAD.md resolution: server-first fetch with a never-lose
|
|
3
|
+
* local-file fallback, behind a feature flag.
|
|
4
|
+
*
|
|
5
|
+
* `resolveGroupInstructions` is the single entry point the message pipeline
|
|
6
|
+
* calls to obtain the trusted per-conversation instruction block injected into
|
|
7
|
+
* the agent's system prompt.
|
|
8
|
+
*
|
|
9
|
+
* π΄ Thread vs group are MUTUALLY EXCLUSIVE (θζΏζζΏε£εΎ, #88 P3). The entry
|
|
10
|
+
* point routes on channelId shape:
|
|
11
|
+
* - A thread (`<groupNo>____<shortId>`, CommunityTopic) resolves its OWN
|
|
12
|
+
* THREAD.md only. It NEVER falls back to β nor stacks β the parent group's
|
|
13
|
+
* GROUP.md, and it NEVER reads or writes the group's `groupMdCache` (which
|
|
14
|
+
* is keyed by the parent groupNo). See `resolveThreadInstructions`.
|
|
15
|
+
* - A plain group (`<groupNo>`, no separator) resolves its GROUP.md exactly as
|
|
16
|
+
* before β the group branch below is byte-for-byte the pre-P3 behavior.
|
|
17
|
+
*
|
|
18
|
+
* The bug this fixes (XIN-224): the old resolver collapsed EVERY channelId to
|
|
19
|
+
* its parent groupNo via `extractParentGroupNo`, so with `serverMd` on a thread
|
|
20
|
+
* message was injected the parent group's GROUP.md β violating the redline that
|
|
21
|
+
* a thread injects only its own THREAD.md.
|
|
22
|
+
*
|
|
23
|
+
* Group-branch resolution order (unchanged):
|
|
24
|
+
* 1. Feature flag OFF (default) or no cache wired β pure local file
|
|
25
|
+
* (`loadGroupConfig`). Byte-identical to the pre-P2 behavior.
|
|
26
|
+
* 2. Feature flag ON:
|
|
27
|
+
* a. serve a cached server GROUP.md if present and not past its TTL (keyed
|
|
28
|
+
* by the group number). The cache is IN-MEMORY ONLY (no disk), so the
|
|
29
|
+
* only way content reaches this trusted system-prompt channel is a
|
|
30
|
+
* live, authenticated fetch β never a forgeable on-disk artifact
|
|
31
|
+
* (review #172 π΄; see group-md-cache.ts);
|
|
32
|
+
* b. otherwise fetch from the server (server-first). On success with
|
|
33
|
+
* non-empty content, cache it and use it;
|
|
34
|
+
* c. on ANY failure (404 "no GROUP.md", network, timeout, empty content)
|
|
35
|
+
* fall back to the local file. The local-file path is never lost.
|
|
36
|
+
*
|
|
37
|
+
* Thread-branch resolution order (P3-1) mirrors the group branch, keyed by the
|
|
38
|
+
* COMPOSITE `<groupNo>::<shortId>` (see `resolveThreadInstructions`), gated on
|
|
39
|
+
* the independent `threadMd` flag; when the flag is off a thread still resolves
|
|
40
|
+
* its local `<shortId>.md` (already the correct non-stacking behavior).
|
|
41
|
+
*
|
|
42
|
+
* Trust: server content stands on its own trust root β an authenticated
|
|
43
|
+
* `getGroupMd` / `getThreadMd` over the bot token against the SSRF-validated
|
|
44
|
+
* `apiUrl` β NOT on the OS-permission trust the local `groupConfigDir` file
|
|
45
|
+
* relies on. By caching only in memory we never let server content masquerade
|
|
46
|
+
* as (or be confused with) a trusted local file on disk.
|
|
47
|
+
*
|
|
48
|
+
* Never throws β a server error degrades to local, a local miss degrades to
|
|
49
|
+
* "no custom instructions".
|
|
50
|
+
*/
|
|
51
|
+
import type { GroupMdCache, ThreadMdCache } from './group-md-cache.js';
|
|
52
|
+
export interface ResolveGroupInstructionsParams {
|
|
53
|
+
/** Operator local-instruction directory (the existing fallback source). */
|
|
54
|
+
groupConfigDir?: string;
|
|
55
|
+
/** Feature flag: when false/undefined, server GROUP.md fetch is skipped. */
|
|
56
|
+
serverMd?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* P3-1 feature flag: when true, a THREAD channel resolves its THREAD.md
|
|
59
|
+
* server-first (GET /v1/bot/groups/{groupNo}/threads/{shortId}/md) before its
|
|
60
|
+
* local `<shortId>.md`. When false/undefined (default) a thread resolves ONLY
|
|
61
|
+
* its local file β which is already the correct non-stacking behavior, so the
|
|
62
|
+
* flag gates the NEW server-read capability, not the bug fix. Independent of
|
|
63
|
+
* `serverMd` (which governs the group server-read path).
|
|
64
|
+
*/
|
|
65
|
+
threadMd?: boolean;
|
|
66
|
+
apiUrl: string;
|
|
67
|
+
botToken: string;
|
|
68
|
+
/** Full channelId (may be a `<groupNo>____<shortId>` thread composite). */
|
|
69
|
+
channelId: string;
|
|
70
|
+
/** Server GROUP.md cache (group branch). Omitted (or flag off) β pure local. */
|
|
71
|
+
cache?: GroupMdCache;
|
|
72
|
+
/** Server THREAD.md cache (thread branch). Omitted (or flag off) β pure local. */
|
|
73
|
+
threadCache?: ThreadMdCache;
|
|
74
|
+
signal?: AbortSignal;
|
|
75
|
+
}
|
|
76
|
+
export declare function resolveGroupInstructions(params: ResolveGroupInstructionsParams): Promise<string | undefined>;
|
package/dist/group-md.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GROUP.md / THREAD.md resolution: server-first fetch with a never-lose
|
|
3
|
+
* local-file fallback, behind a feature flag.
|
|
4
|
+
*
|
|
5
|
+
* `resolveGroupInstructions` is the single entry point the message pipeline
|
|
6
|
+
* calls to obtain the trusted per-conversation instruction block injected into
|
|
7
|
+
* the agent's system prompt.
|
|
8
|
+
*
|
|
9
|
+
* π΄ Thread vs group are MUTUALLY EXCLUSIVE (θζΏζζΏε£εΎ, #88 P3). The entry
|
|
10
|
+
* point routes on channelId shape:
|
|
11
|
+
* - A thread (`<groupNo>____<shortId>`, CommunityTopic) resolves its OWN
|
|
12
|
+
* THREAD.md only. It NEVER falls back to β nor stacks β the parent group's
|
|
13
|
+
* GROUP.md, and it NEVER reads or writes the group's `groupMdCache` (which
|
|
14
|
+
* is keyed by the parent groupNo). See `resolveThreadInstructions`.
|
|
15
|
+
* - A plain group (`<groupNo>`, no separator) resolves its GROUP.md exactly as
|
|
16
|
+
* before β the group branch below is byte-for-byte the pre-P3 behavior.
|
|
17
|
+
*
|
|
18
|
+
* The bug this fixes (XIN-224): the old resolver collapsed EVERY channelId to
|
|
19
|
+
* its parent groupNo via `extractParentGroupNo`, so with `serverMd` on a thread
|
|
20
|
+
* message was injected the parent group's GROUP.md β violating the redline that
|
|
21
|
+
* a thread injects only its own THREAD.md.
|
|
22
|
+
*
|
|
23
|
+
* Group-branch resolution order (unchanged):
|
|
24
|
+
* 1. Feature flag OFF (default) or no cache wired β pure local file
|
|
25
|
+
* (`loadGroupConfig`). Byte-identical to the pre-P2 behavior.
|
|
26
|
+
* 2. Feature flag ON:
|
|
27
|
+
* a. serve a cached server GROUP.md if present and not past its TTL (keyed
|
|
28
|
+
* by the group number). The cache is IN-MEMORY ONLY (no disk), so the
|
|
29
|
+
* only way content reaches this trusted system-prompt channel is a
|
|
30
|
+
* live, authenticated fetch β never a forgeable on-disk artifact
|
|
31
|
+
* (review #172 π΄; see group-md-cache.ts);
|
|
32
|
+
* b. otherwise fetch from the server (server-first). On success with
|
|
33
|
+
* non-empty content, cache it and use it;
|
|
34
|
+
* c. on ANY failure (404 "no GROUP.md", network, timeout, empty content)
|
|
35
|
+
* fall back to the local file. The local-file path is never lost.
|
|
36
|
+
*
|
|
37
|
+
* Thread-branch resolution order (P3-1) mirrors the group branch, keyed by the
|
|
38
|
+
* COMPOSITE `<groupNo>::<shortId>` (see `resolveThreadInstructions`), gated on
|
|
39
|
+
* the independent `threadMd` flag; when the flag is off a thread still resolves
|
|
40
|
+
* its local `<shortId>.md` (already the correct non-stacking behavior).
|
|
41
|
+
*
|
|
42
|
+
* Trust: server content stands on its own trust root β an authenticated
|
|
43
|
+
* `getGroupMd` / `getThreadMd` over the bot token against the SSRF-validated
|
|
44
|
+
* `apiUrl` β NOT on the OS-permission trust the local `groupConfigDir` file
|
|
45
|
+
* relies on. By caching only in memory we never let server content masquerade
|
|
46
|
+
* as (or be confused with) a trusted local file on disk.
|
|
47
|
+
*
|
|
48
|
+
* Never throws β a server error degrades to local, a local miss degrades to
|
|
49
|
+
* "no custom instructions".
|
|
50
|
+
*/
|
|
51
|
+
import { getGroupMd, getThreadMd } from './octo/api.js';
|
|
52
|
+
import { extractParentGroupNo, extractThreadShortId, isThreadChannelId } from './octo/channel-id.js';
|
|
53
|
+
import { loadGroupConfig, MAX_GROUP_CONFIG_BYTES } from './group-config.js';
|
|
54
|
+
/**
|
|
55
|
+
* Trim and byte-bound server-provided GROUP.md the same way loadGroupConfig
|
|
56
|
+
* bounds a local file, so an oversized server payload can't blow the prompt
|
|
57
|
+
* budget. Returns undefined for empty/whitespace-only content.
|
|
58
|
+
*/
|
|
59
|
+
function boundInstructions(content) {
|
|
60
|
+
let text = content;
|
|
61
|
+
if (Buffer.byteLength(text, 'utf-8') > MAX_GROUP_CONFIG_BYTES) {
|
|
62
|
+
const buf = Buffer.from(text, 'utf-8').subarray(0, MAX_GROUP_CONFIG_BYTES);
|
|
63
|
+
// The slice may end mid-codepoint; trim a trailing replacement char.
|
|
64
|
+
text = buf.toString('utf-8').replace(/οΏ½+$/, '') + '\n[β¦ group config truncated]';
|
|
65
|
+
}
|
|
66
|
+
const trimmed = text.trim();
|
|
67
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
68
|
+
}
|
|
69
|
+
export async function resolveGroupInstructions(params) {
|
|
70
|
+
// π΄ Thread and group are mutually exclusive. A thread resolves ONLY its own
|
|
71
|
+
// THREAD.md and must never touch the parent group's GROUP.md nor its cache.
|
|
72
|
+
if (isThreadChannelId(params.channelId)) {
|
|
73
|
+
return resolveThreadInstructions(params);
|
|
74
|
+
}
|
|
75
|
+
return resolveGroupBranch(params);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Plain-group branch β byte-for-byte the pre-P3 `resolveGroupInstructions`
|
|
79
|
+
* behavior (server-first GROUP.md + in-memory cache keyed by groupNo + never-
|
|
80
|
+
* lose local `<groupNo>.md` fallback). Reached only for non-thread channelIds.
|
|
81
|
+
*/
|
|
82
|
+
async function resolveGroupBranch(params) {
|
|
83
|
+
const { groupConfigDir, serverMd, apiUrl, botToken, channelId, cache, signal } = params;
|
|
84
|
+
const local = () => loadGroupConfig(groupConfigDir, channelId);
|
|
85
|
+
// Flag off (or no cache to dedupe fetches) β pure local file, unchanged behavior.
|
|
86
|
+
if (!serverMd || !cache)
|
|
87
|
+
return local();
|
|
88
|
+
const groupNo = extractParentGroupNo(channelId);
|
|
89
|
+
// a) fresh cached server GROUP.md wins. The cache is in-memory only and TTL-
|
|
90
|
+
// bounded, so an expired entry reads as a miss and falls through to a
|
|
91
|
+
// re-fetch below (staleness backstop; item B adds event-driven refresh).
|
|
92
|
+
const cached = cache.get(groupNo);
|
|
93
|
+
if (cached) {
|
|
94
|
+
const bounded = boundInstructions(cached.content);
|
|
95
|
+
if (bounded)
|
|
96
|
+
return bounded;
|
|
97
|
+
// Cached-but-empty (shouldn't normally happen) β fall through to local.
|
|
98
|
+
return local();
|
|
99
|
+
}
|
|
100
|
+
// b) server-first fetch.
|
|
101
|
+
try {
|
|
102
|
+
const md = await getGroupMd({ apiUrl, botToken, groupNo, signal });
|
|
103
|
+
const bounded = boundInstructions(md?.content ?? '');
|
|
104
|
+
if (bounded) {
|
|
105
|
+
const entry = {
|
|
106
|
+
content: md.content,
|
|
107
|
+
version: typeof md.version === 'number' ? md.version : 0,
|
|
108
|
+
updated_at: md.updated_at ?? null,
|
|
109
|
+
updated_by: md.updated_by,
|
|
110
|
+
};
|
|
111
|
+
cache.set(groupNo, entry);
|
|
112
|
+
return bounded;
|
|
113
|
+
}
|
|
114
|
+
// Server reachable but no/empty GROUP.md β local fallback.
|
|
115
|
+
return local();
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
// c) 404 / network / timeout β never-lose local fallback.
|
|
119
|
+
console.error(`[cc-channel-octo] group-md: server fetch for ${groupNo} failed, falling back to local: ${String(err)}`);
|
|
120
|
+
return local();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Thread branch (P3-1) β resolves a CommunityTopic thread's OWN THREAD.md.
|
|
125
|
+
*
|
|
126
|
+
* π΄ By construction this branch NEVER reads or writes the group `cache`
|
|
127
|
+
* (keyed by parent groupNo) and NEVER calls `getGroupMd`, so a thread can never
|
|
128
|
+
* be injected β or have cached β its parent group's GROUP.md.
|
|
129
|
+
*
|
|
130
|
+
* Resolution mirrors the group branch, keyed by the COMPOSITE `groupNo::shortId`:
|
|
131
|
+
* - `threadMd` off (or no thread cache) β local `<shortId>.md` only
|
|
132
|
+
* (`loadGroupConfig` is already thread-aware). This is the correct
|
|
133
|
+
* non-stacking behavior even with the new server capability disabled.
|
|
134
|
+
* - `threadMd` on:
|
|
135
|
+
* a. serve a fresh cached THREAD.md (in-memory only, TTL-bounded);
|
|
136
|
+
* b. else server-first `getThreadMd`, cache non-empty content and use it;
|
|
137
|
+
* c. on ANY failure / empty β never-lose local `<shortId>.md` fallback.
|
|
138
|
+
*/
|
|
139
|
+
async function resolveThreadInstructions(params) {
|
|
140
|
+
const { groupConfigDir, threadMd, apiUrl, botToken, channelId, threadCache, signal } = params;
|
|
141
|
+
// Local fallback keeps the FULL channelId β loadGroupConfig routes a thread to
|
|
142
|
+
// its own `<shortId>.md`, never the parent group's `<groupNo>.md`.
|
|
143
|
+
const local = () => loadGroupConfig(groupConfigDir, channelId);
|
|
144
|
+
// Flag off (or no thread cache) β pure local `<shortId>.md`. Already correct
|
|
145
|
+
// non-stacking behavior β the flag only gates the server-read capability.
|
|
146
|
+
if (!threadMd || !threadCache)
|
|
147
|
+
return local();
|
|
148
|
+
const groupNo = extractParentGroupNo(channelId);
|
|
149
|
+
const shortId = extractThreadShortId(channelId);
|
|
150
|
+
// A malformed thread channelId (`<groupNo>____` with no shortId) can't be
|
|
151
|
+
// server-keyed β degrade to local rather than fetch a group-scoped path.
|
|
152
|
+
if (!shortId)
|
|
153
|
+
return local();
|
|
154
|
+
// a) fresh cached server THREAD.md wins. Composite-keyed, in-memory only,
|
|
155
|
+
// TTL-bounded (expired β miss β re-fetch below).
|
|
156
|
+
const cached = threadCache.get(groupNo, shortId);
|
|
157
|
+
if (cached) {
|
|
158
|
+
const bounded = boundInstructions(cached.content);
|
|
159
|
+
if (bounded)
|
|
160
|
+
return bounded;
|
|
161
|
+
// Cached-but-empty (shouldn't normally happen) β fall through to local.
|
|
162
|
+
return local();
|
|
163
|
+
}
|
|
164
|
+
// b) server-first fetch of this thread's own THREAD.md.
|
|
165
|
+
try {
|
|
166
|
+
const md = await getThreadMd({ apiUrl, botToken, groupNo, shortId, signal });
|
|
167
|
+
const bounded = boundInstructions(md?.content ?? '');
|
|
168
|
+
if (bounded) {
|
|
169
|
+
const entry = {
|
|
170
|
+
content: md.content,
|
|
171
|
+
version: typeof md.version === 'number' ? md.version : 0,
|
|
172
|
+
updated_at: md.updated_at ?? null,
|
|
173
|
+
updated_by: md.updated_by,
|
|
174
|
+
};
|
|
175
|
+
threadCache.set(groupNo, shortId, entry);
|
|
176
|
+
return bounded;
|
|
177
|
+
}
|
|
178
|
+
// Server reachable but no/empty THREAD.md β local fallback.
|
|
179
|
+
return local();
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
// c) 404 / network / timeout β never-lose local fallback.
|
|
183
|
+
console.error(`[cc-channel-octo] thread-md: server fetch for ${groupNo}::${shortId} failed, falling back to local: ${String(err)}`);
|
|
184
|
+
return local();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=group-md.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"group-md.js","sourceRoot":"","sources":["../src/group-md.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ import { SessionRouter } from './session-router.js';
|
|
|
13
13
|
import { GroupContext } from './group-context.js';
|
|
14
14
|
import { StreamRelay } from './stream-relay.js';
|
|
15
15
|
import type { BotMessage } from './octo/types.js';
|
|
16
|
+
import { GroupMdCache, ThreadMdCache } from './group-md-cache.js';
|
|
17
|
+
import { GroupMdWriteback, ThreadMdWriteback } from './group-md-writeback.js';
|
|
16
18
|
import { CronStore } from './cron-store.js';
|
|
17
19
|
/**
|
|
18
20
|
* Resolve a single bot's concrete Config by its configId (config.json
|
|
@@ -33,7 +35,7 @@ export type BotStack = ManagedBot;
|
|
|
33
35
|
* agent query β stream β persist. Exported so tests can drive the real pipeline
|
|
34
36
|
* (not a replica) β `main()` is the only production caller.
|
|
35
37
|
*/
|
|
36
|
-
export declare function handleMessage(msg: BotMessage, config: ReturnType<typeof loadConfig>, store: SessionStore, router: SessionRouter, groupContext: GroupContext, streamRelay: StreamRelay, botId: string, cronStore?: CronStore): Promise<void>;
|
|
38
|
+
export declare function handleMessage(msg: BotMessage, config: ReturnType<typeof loadConfig>, store: SessionStore, router: SessionRouter, groupContext: GroupContext, streamRelay: StreamRelay, botId: string, cronStore?: CronStore, groupMdCache?: GroupMdCache, groupMdWriteback?: GroupMdWriteback, threadMdCache?: ThreadMdCache, threadMdWriteback?: ThreadMdWriteback): Promise<void>;
|
|
37
39
|
/** Max length of the rendered tool-params string in a π§ progress notice. */
|
|
38
40
|
export declare const MAX_TOOL_PARAM_CHARS = 120;
|
|
39
41
|
/**
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import { OctoGateway } from './gateway.js';
|
|
|
15
15
|
import { SessionRouter } from './session-router.js';
|
|
16
16
|
import { GroupContext } from './group-context.js';
|
|
17
17
|
import { queryAgent } from './agent-bridge.js';
|
|
18
|
-
import { sanitizeDisplayName, escapeSectionMarkers, sanitizePromptBody } from './prompt-safety.js';
|
|
18
|
+
import { sanitizeDisplayName, escapeSectionMarkers, sanitizePromptBody, formatSenderLabel } from './prompt-safety.js';
|
|
19
19
|
import { cleanupExpiredCwds, resolveMemoryDir, resolveSessionCwd } from './cwd-resolver.js';
|
|
20
20
|
import { StreamRelay } from './stream-relay.js';
|
|
21
21
|
import { sendMessage, sendReadReceipt, getChannelMessages, getUploadCredentials } from './octo/api.js';
|
|
@@ -23,10 +23,14 @@ import { ChannelType, MessageType } from './octo/types.js';
|
|
|
23
23
|
import { resolveContent, tryResolveFile, resolveHistoricalMessagePlaceholder } from './inbound.js';
|
|
24
24
|
import { downloadInboundImage, MAX_IMAGES_PER_MESSAGE } from './media-inbound.js';
|
|
25
25
|
import { handleCommand } from './commands.js';
|
|
26
|
-
import {
|
|
26
|
+
import { resolveGroupInstructions } from './group-md.js';
|
|
27
|
+
import { GroupMdCache, ThreadMdCache, DEFAULT_GROUP_MD_TTL_MS } from './group-md-cache.js';
|
|
28
|
+
import { GroupMdWriteback, ThreadMdWriteback } from './group-md-writeback.js';
|
|
27
29
|
import { CronStore } from './cron-store.js';
|
|
28
30
|
import { CronScheduler } from './cron-scheduler.js';
|
|
29
31
|
import { createCronToolServer, CRON_TOOL_SERVER_NAME } from './cron-tool.js';
|
|
32
|
+
import { createGroupMdToolServer, GROUP_MD_TOOL_SERVER_NAME, createThreadMdToolServer, THREAD_MD_TOOL_SERVER_NAME, } from './group-md-tool.js';
|
|
33
|
+
import { isThreadChannelId } from './octo/channel-id.js';
|
|
30
34
|
import { buildInlinedFileBody, truncateUtf8ByBytes, assembleUserMessage, MAX_USER_LLM_BYTES } from './file-inline-wrap.js';
|
|
31
35
|
import { join } from 'node:path';
|
|
32
36
|
import { mkdirSync, realpathSync } from 'node:fs';
|
|
@@ -177,6 +181,34 @@ async function startBot(config, multi) {
|
|
|
177
181
|
// --- Group context ---
|
|
178
182
|
const groupContext = new GroupContext(adapter, config.context.maxContextChars);
|
|
179
183
|
groupContext.loadAllFromDb();
|
|
184
|
+
// --- P2-A: server GROUP.md cache. IN-MEMORY ONLY (no disk) β the resolved
|
|
185
|
+
// GROUP.md is injected as a TRUSTED system-prompt block, and a disk cache the
|
|
186
|
+
// gateway user can write would be a chat-injection β trusted-prompt poisoning
|
|
187
|
+
// vector that survives restart (review #172 π΄; see group-md-cache.ts). TTL
|
|
188
|
+
// bounds staleness (review #172 π‘). Decoupled from the P1 GroupContext
|
|
189
|
+
// caches; created unconditionally (cheap, only populated when serverMd is on). ---
|
|
190
|
+
const groupMdCache = new GroupMdCache(config.serverMdTtlMs ?? DEFAULT_GROUP_MD_TTL_MS);
|
|
191
|
+
// --- P3-1: server THREAD.md cache. Same IN-MEMORY-ONLY security model as the
|
|
192
|
+
// group cache above, but keyed by the COMPOSITE `groupNo::shortId` so one
|
|
193
|
+
// group's threads never collide (defensive even though shortId is a globally
|
|
194
|
+
// unique snowflake today). Created unconditionally (cheap; only populated when
|
|
195
|
+
// config.threadMd is on). Kept SEPARATE from groupMdCache so the thread branch
|
|
196
|
+
// can never read or write a parent-group GROUP.md entry (θζΏζζΏδΊζ₯ε£εΎ). ---
|
|
197
|
+
const threadMdCache = new ThreadMdCache(config.serverMdTtlMs ?? DEFAULT_GROUP_MD_TTL_MS);
|
|
198
|
+
// --- P2-C: GROUP.md write-back coordinator. Shared across turns so its
|
|
199
|
+
// per-groupNo write lock actually serializes concurrent agent turns writing
|
|
200
|
+
// the same group (a per-turn instance would defeat the lock). Updates the
|
|
201
|
+
// SAME in-memory groupMdCache above on a successful PUT β no new disk surface.
|
|
202
|
+
// Cheap to construct unconditionally; only exercised when the write-back tool
|
|
203
|
+
// is registered (config.mdWriteback). ---
|
|
204
|
+
const groupMdWriteback = new GroupMdWriteback(groupMdCache);
|
|
205
|
+
// --- P3-2: THREAD.md write-back coordinator. Thread analogue of
|
|
206
|
+
// groupMdWriteback, shared across turns so its per-`groupNo::shortId` write
|
|
207
|
+
// lock serializes concurrent turns writing the same thread. Updates the SAME
|
|
208
|
+
// in-memory threadMdCache above on a successful PUT (no disk surface). Cheap
|
|
209
|
+
// to construct unconditionally; only exercised when the thread write-back
|
|
210
|
+
// tool is registered (config.mdWriteback on a thread channel). ---
|
|
211
|
+
const threadMdWriteback = new ThreadMdWriteback(threadMdCache);
|
|
180
212
|
// --- #115: cron (opt-in). ---
|
|
181
213
|
if (config.sdk.cron && config.botId) {
|
|
182
214
|
cronStore = new CronStore(join(config.baseDir, config.botId, 'cron.json'));
|
|
@@ -223,7 +255,11 @@ async function startBot(config, multi) {
|
|
|
223
255
|
console.warn(`[cc-channel-octo] ${label}Could not prefetch media CDN host (inbound media limited to apiUrl host): ${err instanceof Error ? err.message : String(err)}`);
|
|
224
256
|
}
|
|
225
257
|
// --- Session router ---
|
|
226
|
-
|
|
258
|
+
// P2-B / P3-2: the router holds both md caches so a server GROUP.md or
|
|
259
|
+
// THREAD.md change event invalidates the affected entry (re-fetched
|
|
260
|
+
// authoritatively next turn β never trusting the event payload). No-op when
|
|
261
|
+
// the corresponding flag (serverMd / threadMd) is off.
|
|
262
|
+
const router = new SessionRouter(config, gateway.botId, gateway.ownerUid, groupMdCache, threadMdCache);
|
|
227
263
|
// --- Active handler tracking (Q6: in-flight drain on shutdown) ---
|
|
228
264
|
const activeHandlers = new Set();
|
|
229
265
|
// Install the message handler NOW (before the socket opens). The socket is
|
|
@@ -238,7 +274,7 @@ async function startBot(config, multi) {
|
|
|
238
274
|
// group message could be cached into group context as un-processed chatter).
|
|
239
275
|
if (msg.from_uid === gateway.botId)
|
|
240
276
|
return;
|
|
241
|
-
const p = handleMessage(msg, config, store, router, groupContext, streamRelay, gateway.botId, cronStore)
|
|
277
|
+
const p = handleMessage(msg, config, store, router, groupContext, streamRelay, gateway.botId, cronStore, groupMdCache, groupMdWriteback, threadMdCache, threadMdWriteback)
|
|
242
278
|
.catch((err) => {
|
|
243
279
|
console.error(`[cc-channel-octo] ${label}Unhandled message handler error:`, err instanceof Error ? err.message : err);
|
|
244
280
|
})
|
|
@@ -259,7 +295,7 @@ async function startBot(config, multi) {
|
|
|
259
295
|
onFire: (msg) => {
|
|
260
296
|
if (gateway.draining)
|
|
261
297
|
return Promise.resolve();
|
|
262
|
-
const p = handleMessage(msg, config, store, router, groupContext, streamRelay, gateway.botId, cronStore)
|
|
298
|
+
const p = handleMessage(msg, config, store, router, groupContext, streamRelay, gateway.botId, cronStore, groupMdCache, groupMdWriteback, threadMdCache, threadMdWriteback)
|
|
263
299
|
.finally(() => { activeHandlers.delete(p); });
|
|
264
300
|
activeHandlers.add(p);
|
|
265
301
|
return p;
|
|
@@ -307,7 +343,7 @@ async function startBot(config, multi) {
|
|
|
307
343
|
* agent query β stream β persist. Exported so tests can drive the real pipeline
|
|
308
344
|
* (not a replica) β `main()` is the only production caller.
|
|
309
345
|
*/
|
|
310
|
-
export async function handleMessage(msg, config, store, router, groupContext, streamRelay, botId, cronStore) {
|
|
346
|
+
export async function handleMessage(msg, config, store, router, groupContext, streamRelay, botId, cronStore, groupMdCache, groupMdWriteback, threadMdCache, threadMdWriteback) {
|
|
311
347
|
const channelId = msg.channel_id ?? '';
|
|
312
348
|
const channelType = msg.channel_type ?? ChannelType.DM;
|
|
313
349
|
const isGroup = channelType === ChannelType.Group || channelType === ChannelType.CommunityTopic;
|
|
@@ -546,10 +582,30 @@ export async function handleMessage(msg, config, store, router, groupContext, st
|
|
|
546
582
|
// persists the raw user content without the quote prefix to avoid prefix
|
|
547
583
|
// duplication on conversation replay.
|
|
548
584
|
//
|
|
549
|
-
//
|
|
550
|
-
//
|
|
551
|
-
//
|
|
552
|
-
|
|
585
|
+
// Sender-identity prefix (groups only): the `[Current message β respond to
|
|
586
|
+
// this ONLY]` anchor previously carried an anonymous body, so a shared-group
|
|
587
|
+
// Claude Code / Claw agent could not tell WHICH member sent the request
|
|
588
|
+
// being replied to (the anchor block itself has no sender field, unlike
|
|
589
|
+
// `[Recent group messages]` where each line already prefixes `name(uid)οΌ`).
|
|
590
|
+
// We prepend `name(uid)οΌ` β sanitized via formatSenderLabel β to align the
|
|
591
|
+
// current-message identity semantics with the historical block, so the
|
|
592
|
+
// agent has a single, uniform convention for identifying speakers across
|
|
593
|
+
// the whole prompt. DM channels stay anonymous: there's exactly one human
|
|
594
|
+
// party, adding the prefix would be pure noise. The prefix is persisted
|
|
595
|
+
// via appendUser below too β otherwise a session-replay would drop identity
|
|
596
|
+
// from history while keeping it on current, defeating the whole point.
|
|
597
|
+
//
|
|
598
|
+
// Resolve the displayName via GroupContext so the wire's from_name (which
|
|
599
|
+
// is optional and, when present, sometimes just echoes the uid) is
|
|
600
|
+
// upgraded to the roster displayName learned via refreshMembers. Without
|
|
601
|
+
// this, human users whose wire payload lacks from_name render as
|
|
602
|
+
// `uidοΌbody` instead of `name(uid)οΌbody`, breaking the promised uniform
|
|
603
|
+
// shape.
|
|
604
|
+
const resolvedName = isGroup && msg.channel_id
|
|
605
|
+
? groupContext.resolveDisplayName(msg.channel_id, msg.from_uid, msg.from_name)
|
|
606
|
+
: undefined;
|
|
607
|
+
const senderPrefix = isGroup ? `${formatSenderLabel(msg.from_uid, resolvedName)}οΌ` : '';
|
|
608
|
+
const userBody = quotePrefix + senderPrefix + bodyText;
|
|
553
609
|
// G4: Backfill history from API when local cache is empty for groups.
|
|
554
610
|
// Only triggered on first interaction with a group (cold start) to avoid
|
|
555
611
|
// duplicate API calls; checked via a sentinel marker stored in-memory.
|
|
@@ -710,11 +766,35 @@ export async function handleMessage(msg, config, store, router, groupContext, st
|
|
|
710
766
|
const memoryDir = resolveMemoryDir(memBase, sessionCtx);
|
|
711
767
|
sessionOpts = { ...(sessionOpts ?? {}), memoryDir };
|
|
712
768
|
}
|
|
713
|
-
//
|
|
714
|
-
//
|
|
715
|
-
//
|
|
769
|
+
// External MCP servers declared in config (sdk.mcpServers): seed the map
|
|
770
|
+
// FIRST so the per-turn in-process servers below (cron, GROUP.md write-back)
|
|
771
|
+
// merge on top and win any name clash β those are bound to this turn's
|
|
772
|
+
// session coords and must not be shadowed by a same-named external server.
|
|
773
|
+
if (config.sdk.mcpServers && Object.keys(config.sdk.mcpServers).length > 0) {
|
|
774
|
+
sessionOpts = {
|
|
775
|
+
...(sessionOpts ?? {}),
|
|
776
|
+
mcpServers: { ...(sessionOpts?.mcpServers ?? {}), ...config.sdk.mcpServers },
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
// GROUP.md: inject operator-provided per-group instructions into the
|
|
780
|
+
// system prompt. Only for groups β DMs key on the peer uid, not a shared
|
|
781
|
+
// channel. P2-A: a plain group resolves GROUP.md server-first (GET
|
|
782
|
+
// /v1/bot/groups/{groupNo}/md, cached) with a never-lose fallback to the
|
|
783
|
+
// local `groupConfigDir/<channelId>.md` file (gated behind `config.serverMd`).
|
|
784
|
+
// P3-1: a thread (CommunityTopic) resolves its OWN THREAD.md ONLY β the
|
|
785
|
+
// resolver routes on channelId shape and NEVER stacks the parent group's
|
|
786
|
+
// GROUP.md for a thread (server thread read gated behind `config.threadMd`).
|
|
716
787
|
if (isGroup) {
|
|
717
|
-
const groupInstructions =
|
|
788
|
+
const groupInstructions = await resolveGroupInstructions({
|
|
789
|
+
groupConfigDir: config.groupConfigDir,
|
|
790
|
+
serverMd: config.serverMd,
|
|
791
|
+
threadMd: config.threadMd,
|
|
792
|
+
apiUrl: config.apiUrl,
|
|
793
|
+
botToken: config.botToken,
|
|
794
|
+
channelId,
|
|
795
|
+
cache: groupMdCache,
|
|
796
|
+
threadCache: threadMdCache,
|
|
797
|
+
});
|
|
718
798
|
if (groupInstructions) {
|
|
719
799
|
sessionOpts = { ...(sessionOpts ?? {}), groupInstructions };
|
|
720
800
|
}
|
|
@@ -732,9 +812,44 @@ export async function handleMessage(msg, config, store, router, groupContext, st
|
|
|
732
812
|
const cronServer = createCronToolServer(cronStore, coords, router.getOwnerUid());
|
|
733
813
|
sessionOpts = {
|
|
734
814
|
...(sessionOpts ?? {}),
|
|
735
|
-
mcpServers: { [CRON_TOOL_SERVER_NAME]: cronServer },
|
|
815
|
+
mcpServers: { ...(sessionOpts?.mcpServers ?? {}), [CRON_TOOL_SERVER_NAME]: cronServer },
|
|
736
816
|
};
|
|
737
817
|
}
|
|
818
|
+
// P2-C / P3-2: when md write-back is on, inject the write-back MCP tool
|
|
819
|
+
// bound to THIS message's channel coords and gated to the bot owner uid.
|
|
820
|
+
// Only for group-like channels β a DM has no shared md. The tool is chosen
|
|
821
|
+
// by channel SHAPE and the two are MUTUALLY EXCLUSIVE (#88 P3 redline):
|
|
822
|
+
// - a THREAD (CommunityTopic composite `<groupNo>____<shortId>`) gets the
|
|
823
|
+
// thread tool, which writes the thread's OWN THREAD.md
|
|
824
|
+
// (PUT /threads/{shortId}/md) β never the parent group's GROUP.md. This
|
|
825
|
+
// is the XIN-230 follow-up: the write side now matches the P3-1 read
|
|
826
|
+
// side instead of collapsing a thread to its parent groupNo.
|
|
827
|
+
// - a plain GROUP gets the GROUP.md tool, unchanged (P2-C).
|
|
828
|
+
// Each borrows its shared writeback coordinator so the per-key write lock
|
|
829
|
+
// and the cache it refreshes are process-wide, not per-turn.
|
|
830
|
+
if (config.mdWriteback && isGroup) {
|
|
831
|
+
const coords = {
|
|
832
|
+
channelId,
|
|
833
|
+
fromUid: msg.from_uid,
|
|
834
|
+
fromName: msg.from_name,
|
|
835
|
+
};
|
|
836
|
+
if (isThreadChannelId(channelId)) {
|
|
837
|
+
if (threadMdWriteback) {
|
|
838
|
+
const threadMdServer = createThreadMdToolServer({ writeback: threadMdWriteback, apiUrl: config.apiUrl, botToken: config.botToken }, coords, router.getOwnerUid());
|
|
839
|
+
sessionOpts = {
|
|
840
|
+
...(sessionOpts ?? {}),
|
|
841
|
+
mcpServers: { ...(sessionOpts?.mcpServers ?? {}), [THREAD_MD_TOOL_SERVER_NAME]: threadMdServer },
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
else if (groupMdWriteback) {
|
|
846
|
+
const groupMdServer = createGroupMdToolServer({ writeback: groupMdWriteback, apiUrl: config.apiUrl, botToken: config.botToken }, coords, router.getOwnerUid());
|
|
847
|
+
sessionOpts = {
|
|
848
|
+
...(sessionOpts ?? {}),
|
|
849
|
+
mcpServers: { ...(sessionOpts?.mcpServers ?? {}), [GROUP_MD_TOOL_SERVER_NAME]: groupMdServer },
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
}
|
|
738
853
|
const rawChunks = queryAgent(userContentForLLM, config, sessionCtx, onToolUse, sessionOpts);
|
|
739
854
|
// Tee the generator: collect full text while streaming to Octo
|
|
740
855
|
const collected = [];
|