@mininglamp-oss/cc-channel-octo 1.0.3-dev.3135d38 → 1.0.3-dev.4a55e52

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.
@@ -0,0 +1,223 @@
1
+ /**
2
+ * GROUP.md write-back (P2-C): the app-layer coordinator that persists an
3
+ * agent-authored GROUP.md back to the server (PUT /v1/bot/groups/{groupNo}/md)
4
+ * via A's `updateGroupMd` client, then refreshes A's in-memory cache so the next
5
+ * resolve does not TTL-refetch a stale copy.
6
+ *
7
+ * Two server-contract facts (XIN-201, measured — NOT inferred) shape this:
8
+ *
9
+ * 1. content has a HARD ≤10240-byte (UTF-8) limit; the server answers an
10
+ * oversized body with 400 err.server.bot_api.content_too_large. We reject
11
+ * locally BEFORE the PUT so a too-large write never reaches the server.
12
+ *
13
+ * 2. `version` is a server-side monotonic counter and the PUT does NOT do
14
+ * compare-and-swap (last-write-wins, no CAS). Concurrent writers therefore
15
+ * silently clobber each other server-side. We cannot fix a foreign writer
16
+ * (operator console, another gateway), but we CAN guarantee that this
17
+ * gateway never races itself: all write-backs for the same groupNo are
18
+ * serialized through a per-groupNo promise-chain lock, so the read-modify-
19
+ * write (PUT + cache update) for one call completes before the next starts.
20
+ * Cross-source last-write-wins remains possible by design — see the caveat
21
+ * on {@link GroupMdWriteback}.
22
+ *
23
+ * Owner-gating lives in the MCP tool layer (group-md-tool.ts), not here: this
24
+ * module is the mechanism, the tool is the policy boundary.
25
+ *
26
+ * Never persists anything to disk — the cache it updates is the same in-memory-
27
+ * only cache A's resolver reads (group-md-cache.ts), so this introduces no new
28
+ * durable-trust surface.
29
+ */
30
+ import { updateGroupMd, updateThreadMd } from './octo/api.js';
31
+ /**
32
+ * Hard UTF-8 byte ceiling for GROUP.md content, per the XIN-201 measured
33
+ * server contract. A body above this is rejected locally (a server PUT would
34
+ * answer 400 err.server.bot_api.content_too_large).
35
+ */
36
+ export const MAX_GROUP_MD_CONTENT_BYTES = 10240;
37
+ /**
38
+ * Hard UTF-8 byte ceiling for THREAD.md content. The thread md endpoint shares
39
+ * the group md limit (P3-2 contract check: the official Octo web client edits
40
+ * both group and thread md through the SAME editor, whose `MAX_BYTES` is 10240),
41
+ * so this is an alias of {@link MAX_GROUP_MD_CONTENT_BYTES} rather than a second
42
+ * magic number — if the server limit ever diverges, the two can split here.
43
+ */
44
+ export const MAX_THREAD_MD_CONTENT_BYTES = MAX_GROUP_MD_CONTENT_BYTES;
45
+ /** Thrown when content exceeds {@link MAX_GROUP_MD_CONTENT_BYTES}. */
46
+ export class GroupMdContentTooLargeError extends Error {
47
+ bytes;
48
+ constructor(bytes) {
49
+ super(`GROUP.md content is ${bytes} bytes, over the ${MAX_GROUP_MD_CONTENT_BYTES}-byte UTF-8 limit`);
50
+ this.bytes = bytes;
51
+ this.name = 'GroupMdContentTooLargeError';
52
+ }
53
+ }
54
+ /**
55
+ * Serializes GROUP.md write-backs per groupNo and keeps A's cache in sync.
56
+ *
57
+ * Constructed ONCE per bot (alongside the GroupMdCache it updates) and shared
58
+ * across turns, so the per-groupNo lock actually spans concurrent agent turns —
59
+ * a per-turn instance would defeat the lock. The MCP tool server (built per
60
+ * turn) borrows this shared instance.
61
+ *
62
+ * CAVEAT (documented per XIN-201 item 4): the lock only covers THIS gateway. A
63
+ * write from another source (operator console, a second gateway process) is not
64
+ * coordinated and, because the server has no CAS, last-write-wins across sources
65
+ * — that is an accepted limitation, not a bug.
66
+ */
67
+ export class GroupMdWriteback {
68
+ cache;
69
+ updateFn;
70
+ /** Tail of the in-flight write chain per groupNo (serializes same-key writes). */
71
+ tails = new Map();
72
+ constructor(cache,
73
+ /** Injectable PUT client (defaults to the real octo client). */
74
+ updateFn = updateGroupMd) {
75
+ this.cache = cache;
76
+ this.updateFn = updateFn;
77
+ }
78
+ /**
79
+ * Run `fn` after every previously-queued op for `key` has settled, so bodies
80
+ * for the same key never overlap. Different keys run independently. `fn`'s
81
+ * rejection is surfaced to its own caller; the chain tail swallows it so a
82
+ * failed write does not wedge later writes.
83
+ */
84
+ withLock(key, fn) {
85
+ const prev = this.tails.get(key) ?? Promise.resolve();
86
+ const result = prev.then(fn, fn); // run regardless of prior success/failure
87
+ const tail = result.then(() => undefined, () => undefined);
88
+ this.tails.set(key, tail);
89
+ // Best-effort cleanup: drop the key once the queue drains to this tail, so
90
+ // the Map does not grow without bound across many groups.
91
+ tail.then(() => {
92
+ if (this.tails.get(key) === tail)
93
+ this.tails.delete(key);
94
+ });
95
+ return result;
96
+ }
97
+ /**
98
+ * Persist `content` as the group's GROUP.md and refresh the cache.
99
+ *
100
+ * Rejects with {@link GroupMdContentTooLargeError} (before any server call)
101
+ * when content exceeds the UTF-8 byte limit; propagates the client error when
102
+ * the PUT itself fails (cache is left untouched in that case).
103
+ */
104
+ async writeBack(params) {
105
+ const { apiUrl, botToken, groupNo, content, signal } = params;
106
+ const bytes = Buffer.byteLength(content, 'utf-8');
107
+ if (bytes > MAX_GROUP_MD_CONTENT_BYTES) {
108
+ // Reject locally — do NOT hit the server (it would answer 400).
109
+ throw new GroupMdContentTooLargeError(bytes);
110
+ }
111
+ return this.withLock(groupNo, async () => {
112
+ const { version } = await this.updateFn({ apiUrl, botToken, groupNo, content, signal });
113
+ // Write the just-persisted content/version back into A's in-memory cache
114
+ // so the next resolveGroupInstructions serves what we wrote instead of
115
+ // either re-fetching (TTL) or, worse, returning a now-stale cached copy.
116
+ // updated_at is null because the PUT response carries only { version }.
117
+ this.cache.set(groupNo, {
118
+ content,
119
+ version: typeof version === 'number' ? version : 0,
120
+ updated_at: null,
121
+ });
122
+ return { groupNo, version, bytes };
123
+ });
124
+ }
125
+ }
126
+ /** Thrown when THREAD.md content exceeds {@link MAX_THREAD_MD_CONTENT_BYTES}. */
127
+ export class ThreadMdContentTooLargeError extends Error {
128
+ bytes;
129
+ constructor(bytes) {
130
+ super(`THREAD.md content is ${bytes} bytes, over the ${MAX_THREAD_MD_CONTENT_BYTES}-byte UTF-8 limit`);
131
+ this.bytes = bytes;
132
+ this.name = 'ThreadMdContentTooLargeError';
133
+ }
134
+ }
135
+ /**
136
+ * Thread analogue of {@link GroupMdWriteback} (P3-2): persists an agent-authored
137
+ * THREAD.md back to the server (PUT /v1/bot/groups/{groupNo}/threads/{shortId}/md
138
+ * via `updateThreadMd`), then refreshes the in-memory {@link ThreadMdCache} so
139
+ * the next resolve does not TTL-refetch a stale copy.
140
+ *
141
+ * Carries over every P2-C safety property, adapted to the thread key:
142
+ *
143
+ * 1. Same HARD ≤10240-byte (UTF-8) limit ({@link MAX_THREAD_MD_CONTENT_BYTES},
144
+ * confirmed to equal the group limit — see that constant). Rejected locally
145
+ * BEFORE the PUT so an oversized body never reaches the server.
146
+ *
147
+ * 2. The thread PUT, like the group PUT, does NOT compare-and-swap (body is
148
+ * just `{ content }`, reply is `{ version }`). All write-backs for the same
149
+ * thread are serialized through a per-`groupNo::shortId` promise-chain lock
150
+ * so this gateway never races itself. Cross-source last-write-wins remains
151
+ * possible by design, identical to the group caveat.
152
+ *
153
+ * Keyed by the COMPOSITE `groupNo::shortId` — the SAME key the thread cache and
154
+ * the P3-1 read side use — so a thread's write lock and cache refresh can never
155
+ * be confused with its parent group's (which the group coordinator locks by the
156
+ * bare groupNo). `::` is outside the safe-id charset, so the two key spaces are
157
+ * disjoint. Owner-gating lives in the MCP tool layer (group-md-tool.ts), not
158
+ * here. Never persists to disk — the cache it updates is the same memory-only
159
+ * cache the resolver reads.
160
+ */
161
+ export class ThreadMdWriteback {
162
+ cache;
163
+ updateFn;
164
+ /** Tail of the in-flight write chain per composite key (serializes same-thread writes). */
165
+ tails = new Map();
166
+ constructor(cache,
167
+ /** Injectable PUT client (defaults to the real octo client). */
168
+ updateFn = updateThreadMd) {
169
+ this.cache = cache;
170
+ this.updateFn = updateFn;
171
+ }
172
+ /** Composite lock key; mirrors ThreadMdCache's `groupNo::shortId`. */
173
+ lockKey(groupNo, shortId) {
174
+ return `${groupNo}::${shortId}`;
175
+ }
176
+ /**
177
+ * Run `fn` after every previously-queued op for `key` has settled, so bodies
178
+ * for the same key never overlap. Different keys run independently. `fn`'s
179
+ * rejection is surfaced to its own caller; the chain tail swallows it so a
180
+ * failed write does not wedge later writes. Identical policy to
181
+ * {@link GroupMdWriteback}.
182
+ */
183
+ withLock(key, fn) {
184
+ const prev = this.tails.get(key) ?? Promise.resolve();
185
+ const result = prev.then(fn, fn); // run regardless of prior success/failure
186
+ const tail = result.then(() => undefined, () => undefined);
187
+ this.tails.set(key, tail);
188
+ tail.then(() => {
189
+ if (this.tails.get(key) === tail)
190
+ this.tails.delete(key);
191
+ });
192
+ return result;
193
+ }
194
+ /**
195
+ * Persist `content` as the thread's THREAD.md and refresh the thread cache.
196
+ *
197
+ * Rejects with {@link ThreadMdContentTooLargeError} (before any server call)
198
+ * when content exceeds the UTF-8 byte limit; propagates the client error when
199
+ * the PUT itself fails (cache is left untouched in that case).
200
+ */
201
+ async writeBack(params) {
202
+ const { apiUrl, botToken, groupNo, shortId, content, signal } = params;
203
+ const bytes = Buffer.byteLength(content, 'utf-8');
204
+ if (bytes > MAX_THREAD_MD_CONTENT_BYTES) {
205
+ // Reject locally — do NOT hit the server (it would answer 400).
206
+ throw new ThreadMdContentTooLargeError(bytes);
207
+ }
208
+ return this.withLock(this.lockKey(groupNo, shortId), async () => {
209
+ const { version } = await this.updateFn({ apiUrl, botToken, groupNo, shortId, content, signal });
210
+ // Refresh the just-persisted content/version into the in-memory thread
211
+ // cache (composite-keyed) so the next resolveGroupInstructions thread
212
+ // branch serves what we wrote instead of re-fetching or serving a stale
213
+ // copy. updated_at is null because the PUT response carries only { version }.
214
+ this.cache.set(groupNo, shortId, {
215
+ content,
216
+ version: typeof version === 'number' ? version : 0,
217
+ updated_at: null,
218
+ });
219
+ return { groupNo, shortId, version, bytes };
220
+ });
221
+ }
222
+ }
223
+ //# sourceMappingURL=group-md-writeback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"group-md-writeback.js","sourceRoot":"","sources":["../src/group-md-writeback.ts"],"names":[],"mappings":"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"}
@@ -1,53 +1,76 @@
1
1
  /**
2
- * GROUP.md resolution (P2-A): server-first fetch with a never-lose local-file
3
- * fallback, behind a feature flag.
2
+ * GROUP.md / THREAD.md resolution: server-first fetch with a never-lose
3
+ * local-file fallback, behind a feature flag.
4
4
  *
5
5
  * `resolveGroupInstructions` is the single entry point the message pipeline
6
- * calls to obtain the trusted per-group instruction block injected into the
7
- * agent's system prompt. Resolution order:
6
+ * calls to obtain the trusted per-conversation instruction block injected into
7
+ * the agent's system prompt.
8
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):
9
24
  * 1. Feature flag OFF (default) or no cache wired → pure local file
10
- * (`loadGroupConfig`). Byte-identical to the pre-P2 behavior, so existing
11
- * local-file deployments are unaffected.
25
+ * (`loadGroupConfig`). Byte-identical to the pre-P2 behavior.
12
26
  * 2. Feature flag ON:
13
27
  * a. serve a cached server GROUP.md if present and not past its TTL (keyed
14
- * by the PARENT group number a thread shares its parent group's
15
- * GROUP.md). The cache is IN-MEMORY ONLY (no disk), so the only way
16
- * content reaches this trusted system-prompt channel is a live,
17
- * authenticated fetch never a forgeable on-disk artifact a chat-
18
- * driven Bash/Write could plant (review #172 🔴; see group-md-cache.ts);
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);
19
32
  * b. otherwise fetch from the server (server-first). On success with
20
33
  * non-empty content, cache it and use it;
21
34
  * c. on ANY failure (404 "no GROUP.md", network, timeout, empty content)
22
35
  * fall back to the local file. The local-file path is never lost.
23
36
  *
24
- * Trust: server content stands on its own trust root an authenticated
25
- * `getGroupMd` over the bot token against the SSRF-validated `apiUrl`, server-
26
- * side bot_admin-gated NOT on the OS-permission trust the local `groupConfigDir`
27
- * file relies on. By caching only in memory we never let server content
28
- * masquerade as (or be confused with) a trusted local file on disk.
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).
29
41
  *
30
- * Thread routing (P1): a thread channelId is the composite `<groupNo>____<shortId>`.
31
- * The server GROUP.md endpoint is keyed by the parent group number, so we resolve
32
- * it with `extractParentGroupNo` for the API call + cache key (identity for a
33
- * plain group). The local-file fallback still receives the FULL channelId so its
34
- * own thread/short-id routing in `loadGroupConfig` is preserved unchanged.
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.
35
47
  *
36
48
  * Never throws — a server error degrades to local, a local miss degrades to
37
49
  * "no custom instructions".
38
50
  */
39
- import type { GroupMdCache } from './group-md-cache.js';
51
+ import type { GroupMdCache, ThreadMdCache } from './group-md-cache.js';
40
52
  export interface ResolveGroupInstructionsParams {
41
53
  /** Operator local-instruction directory (the existing fallback source). */
42
54
  groupConfigDir?: string;
43
- /** Feature flag: when false/undefined, server fetch is skipped entirely. */
55
+ /** Feature flag: when false/undefined, server GROUP.md fetch is skipped. */
44
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;
45
66
  apiUrl: string;
46
67
  botToken: string;
47
68
  /** Full channelId (may be a `<groupNo>____<shortId>` thread composite). */
48
69
  channelId: string;
49
- /** Server GROUP.md cache. Omitted (or flag off) → pure local file. */
70
+ /** Server GROUP.md cache (group branch). Omitted (or flag off) → pure local. */
50
71
  cache?: GroupMdCache;
72
+ /** Server THREAD.md cache (thread branch). Omitted (or flag off) → pure local. */
73
+ threadCache?: ThreadMdCache;
51
74
  signal?: AbortSignal;
52
75
  }
53
76
  export declare function resolveGroupInstructions(params: ResolveGroupInstructionsParams): Promise<string | undefined>;
package/dist/group-md.js CHANGED
@@ -1,43 +1,55 @@
1
1
  /**
2
- * GROUP.md resolution (P2-A): server-first fetch with a never-lose local-file
3
- * fallback, behind a feature flag.
2
+ * GROUP.md / THREAD.md resolution: server-first fetch with a never-lose
3
+ * local-file fallback, behind a feature flag.
4
4
  *
5
5
  * `resolveGroupInstructions` is the single entry point the message pipeline
6
- * calls to obtain the trusted per-group instruction block injected into the
7
- * agent's system prompt. Resolution order:
6
+ * calls to obtain the trusted per-conversation instruction block injected into
7
+ * the agent's system prompt.
8
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):
9
24
  * 1. Feature flag OFF (default) or no cache wired → pure local file
10
- * (`loadGroupConfig`). Byte-identical to the pre-P2 behavior, so existing
11
- * local-file deployments are unaffected.
25
+ * (`loadGroupConfig`). Byte-identical to the pre-P2 behavior.
12
26
  * 2. Feature flag ON:
13
27
  * a. serve a cached server GROUP.md if present and not past its TTL (keyed
14
- * by the PARENT group number a thread shares its parent group's
15
- * GROUP.md). The cache is IN-MEMORY ONLY (no disk), so the only way
16
- * content reaches this trusted system-prompt channel is a live,
17
- * authenticated fetch never a forgeable on-disk artifact a chat-
18
- * driven Bash/Write could plant (review #172 🔴; see group-md-cache.ts);
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);
19
32
  * b. otherwise fetch from the server (server-first). On success with
20
33
  * non-empty content, cache it and use it;
21
34
  * c. on ANY failure (404 "no GROUP.md", network, timeout, empty content)
22
35
  * fall back to the local file. The local-file path is never lost.
23
36
  *
24
- * Trust: server content stands on its own trust root an authenticated
25
- * `getGroupMd` over the bot token against the SSRF-validated `apiUrl`, server-
26
- * side bot_admin-gated NOT on the OS-permission trust the local `groupConfigDir`
27
- * file relies on. By caching only in memory we never let server content
28
- * masquerade as (or be confused with) a trusted local file on disk.
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).
29
41
  *
30
- * Thread routing (P1): a thread channelId is the composite `<groupNo>____<shortId>`.
31
- * The server GROUP.md endpoint is keyed by the parent group number, so we resolve
32
- * it with `extractParentGroupNo` for the API call + cache key (identity for a
33
- * plain group). The local-file fallback still receives the FULL channelId so its
34
- * own thread/short-id routing in `loadGroupConfig` is preserved unchanged.
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.
35
47
  *
36
48
  * Never throws — a server error degrades to local, a local miss degrades to
37
49
  * "no custom instructions".
38
50
  */
39
- import { getGroupMd } from './octo/api.js';
40
- import { extractParentGroupNo } from './octo/channel-id.js';
51
+ import { getGroupMd, getThreadMd } from './octo/api.js';
52
+ import { extractParentGroupNo, extractThreadShortId, isThreadChannelId } from './octo/channel-id.js';
41
53
  import { loadGroupConfig, MAX_GROUP_CONFIG_BYTES } from './group-config.js';
42
54
  /**
43
55
  * Trim and byte-bound server-provided GROUP.md the same way loadGroupConfig
@@ -55,6 +67,19 @@ function boundInstructions(content) {
55
67
  return trimmed.length > 0 ? trimmed : undefined;
56
68
  }
57
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) {
58
83
  const { groupConfigDir, serverMd, apiUrl, botToken, channelId, cache, signal } = params;
59
84
  const local = () => loadGroupConfig(groupConfigDir, channelId);
60
85
  // Flag off (or no cache to dedupe fetches) → pure local file, unchanged behavior.
@@ -95,4 +120,68 @@ export async function resolveGroupInstructions(params) {
95
120
  return local();
96
121
  }
97
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
+ }
98
187
  //# sourceMappingURL=group-md.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"group-md.js","sourceRoot":"","sources":["../src/group-md.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAG5E;;;;GAIG;AACH,SAAS,iBAAiB,CAAC,OAAe;IACxC,IAAI,IAAI,GAAG,OAAO,CAAC;IACnB,IAAI,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,sBAAsB,EAAE,CAAC;QAC9D,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,sBAAsB,CAAC,CAAC;QAC3E,qEAAqE;QACrE,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,8BAA8B,CAAC;IACnF,CAAC;IACD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAC5B,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;AAClD,CAAC;AAgBD,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,MAAsC;IAEtC,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC;IAExF,MAAM,KAAK,GAAG,GAAuB,EAAE,CAAC,eAAe,CAAC,cAAc,EAAE,SAAS,CAAC,CAAC;IAEnF,kFAAkF;IAClF,IAAI,CAAC,QAAQ,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,EAAE,CAAC;IAExC,MAAM,OAAO,GAAG,oBAAoB,CAAC,SAAS,CAAC,CAAC;IAEhD,6EAA6E;IAC7E,yEAAyE;IACzE,4EAA4E;IAC5E,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAClC,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,OAAO,GAAG,iBAAiB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAClD,IAAI,OAAO;YAAE,OAAO,OAAO,CAAC;QAC5B,wEAAwE;QACxE,OAAO,KAAK,EAAE,CAAC;IACjB,CAAC;IAED,yBAAyB;IACzB,IAAI,CAAC;QACH,MAAM,EAAE,GAAG,MAAM,UAAU,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;QACnE,MAAM,OAAO,GAAG,iBAAiB,CAAC,EAAE,EAAE,OAAO,IAAI,EAAE,CAAC,CAAC;QACrD,IAAI,OAAO,EAAE,CAAC;YACZ,MAAM,KAAK,GAAiB;gBAC1B,OAAO,EAAE,EAAE,CAAC,OAAO;gBACnB,OAAO,EAAE,OAAO,EAAE,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;gBACxD,UAAU,EAAE,EAAE,CAAC,UAAU,IAAI,IAAI;gBACjC,UAAU,EAAE,EAAE,CAAC,UAAU;aAC1B,CAAC;YACF,KAAK,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;YAC1B,OAAO,OAAO,CAAC;QACjB,CAAC;QACD,2DAA2D;QAC3D,OAAO,KAAK,EAAE,CAAC;IACjB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,0DAA0D;QAC1D,OAAO,CAAC,KAAK,CACX,gDAAgD,OAAO,mCAAmC,MAAM,CAAC,GAAG,CAAC,EAAE,CACxG,CAAC;QACF,OAAO,KAAK,EAAE,CAAC;IACjB,CAAC;AACH,CAAC"}
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,7 +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 } from './group-md-cache.js';
16
+ import { GroupMdCache, ThreadMdCache } from './group-md-cache.js';
17
+ import { GroupMdWriteback, ThreadMdWriteback } from './group-md-writeback.js';
17
18
  import { CronStore } from './cron-store.js';
18
19
  /**
19
20
  * Resolve a single bot's concrete Config by its configId (config.json
@@ -34,7 +35,7 @@ export type BotStack = ManagedBot;
34
35
  * agent query → stream → persist. Exported so tests can drive the real pipeline
35
36
  * (not a replica) — `main()` is the only production caller.
36
37
  */
37
- export declare function handleMessage(msg: BotMessage, config: ReturnType<typeof loadConfig>, store: SessionStore, router: SessionRouter, groupContext: GroupContext, streamRelay: StreamRelay, botId: string, cronStore?: CronStore, groupMdCache?: GroupMdCache): 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>;
38
39
  /** Max length of the rendered tool-params string in a 🔧 progress notice. */
39
40
  export declare const MAX_TOOL_PARAM_CHARS = 120;
40
41
  /**