@mininglamp-oss/cc-channel-octo 1.0.2 → 1.0.3-dev.1465991

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 (51) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/config.example.json +5 -0
  3. package/dist/agent-bridge.js +6 -1
  4. package/dist/agent-bridge.js.map +1 -1
  5. package/dist/bot-manager.d.ts +103 -0
  6. package/dist/bot-manager.js +184 -0
  7. package/dist/bot-manager.js.map +1 -0
  8. package/dist/config-watcher.d.ts +48 -0
  9. package/dist/config-watcher.js +143 -0
  10. package/dist/config-watcher.js.map +1 -0
  11. package/dist/config.d.ts +38 -0
  12. package/dist/config.js +8 -0
  13. package/dist/config.js.map +1 -1
  14. package/dist/group-config.d.ts +18 -9
  15. package/dist/group-config.js +47 -10
  16. package/dist/group-config.js.map +1 -1
  17. package/dist/group-context.js +9 -1
  18. package/dist/group-context.js.map +1 -1
  19. package/dist/group-md-cache.d.ts +69 -0
  20. package/dist/group-md-cache.js +93 -0
  21. package/dist/group-md-cache.js.map +1 -0
  22. package/dist/group-md-events.d.ts +43 -0
  23. package/dist/group-md-events.js +43 -0
  24. package/dist/group-md-events.js.map +1 -0
  25. package/dist/group-md-tool.d.ts +49 -0
  26. package/dist/group-md-tool.js +95 -0
  27. package/dist/group-md-tool.js.map +1 -0
  28. package/dist/group-md-writeback.d.ts +97 -0
  29. package/dist/group-md-writeback.js +118 -0
  30. package/dist/group-md-writeback.js.map +1 -0
  31. package/dist/group-md.d.ts +53 -0
  32. package/dist/group-md.js +98 -0
  33. package/dist/group-md.js.map +1 -0
  34. package/dist/index.d.ts +15 -30
  35. package/dist/index.js +133 -140
  36. package/dist/index.js.map +1 -1
  37. package/dist/octo/api.d.ts +98 -1
  38. package/dist/octo/api.js +142 -2
  39. package/dist/octo/api.js.map +1 -1
  40. package/dist/octo/channel-id.d.ts +36 -0
  41. package/dist/octo/channel-id.js +52 -0
  42. package/dist/octo/channel-id.js.map +1 -0
  43. package/dist/octo/types.d.ts +22 -0
  44. package/dist/octo/types.js.map +1 -1
  45. package/dist/session-router.d.ts +43 -1
  46. package/dist/session-router.js +79 -3
  47. package/dist/session-router.js.map +1 -1
  48. package/dist/session-store.d.ts +26 -0
  49. package/dist/session-store.js +64 -1
  50. package/dist/session-store.js.map +1 -1
  51. package/package.json +1 -1
@@ -0,0 +1,49 @@
1
+ /**
2
+ * P2-C: GROUP.md write-back tool — an in-process MCP server letting the agent
3
+ * persist an updated GROUP.md back to the server. The tool surfaces to the model
4
+ * as `mcp__group_md__update_group_md`.
5
+ *
6
+ * The server is built PER TURN (`createGroupMdToolServer`) with the current
7
+ * message's channel coords + the bot owner uid, so:
8
+ * - the write targets the PARENT group of the channel the agent is in
9
+ * (`extractParentGroupNo` — a thread shares its parent group's GROUP.md);
10
+ * - invocation is GATED to the bot owner (registerBot.owner_uid). The group's
11
+ * octo_tag token has server-side write permission, but the agent is driven by
12
+ * untrusted IM users, so this owner gate — not LLM judgment, and independent
13
+ * of the token's group-role permission — is what stops a prompt-injected
14
+ * agent from rewriting the operator's trusted GROUP.md from any chat.
15
+ *
16
+ * Concurrency, the byte ceiling, and the cache refresh are owned by the shared
17
+ * {@link GroupMdWriteback} coordinator (group-md-writeback.ts); this layer is
18
+ * only the owner-gate policy + the MCP surface.
19
+ */
20
+ import { z } from 'zod';
21
+ import { type GroupMdWriteback } from './group-md-writeback.js';
22
+ /** MCP server name; the tool surfaces as `mcp__group_md__update_group_md`. */
23
+ export declare const GROUP_MD_TOOL_SERVER_NAME = "group_md";
24
+ /** Raw coords of the session invoking the tool — gates the call + targets the group. */
25
+ export interface GroupMdSessionCoords {
26
+ /** Full channelId (may be a `<groupNo>____<shortId>` thread composite). */
27
+ channelId: string;
28
+ fromUid: string;
29
+ fromName?: string;
30
+ }
31
+ /** Shared deps the tool needs to perform a write-back. */
32
+ export interface GroupMdToolDeps {
33
+ writeback: GroupMdWriteback;
34
+ apiUrl: string;
35
+ botToken: string;
36
+ }
37
+ /**
38
+ * Build the GROUP.md tool DEFINITIONS for one agent turn. Exported separately
39
+ * from the server so tests can invoke the handler directly. `coords` targets the
40
+ * group + supplies the caller uid; `ownerUid` is the owner gate.
41
+ */
42
+ export declare function buildGroupMdTools(deps: GroupMdToolDeps, coords: GroupMdSessionCoords, ownerUid: string): import("@anthropic-ai/claude-agent-sdk").SdkMcpToolDefinition<{
43
+ content: z.ZodString;
44
+ }>[];
45
+ /**
46
+ * Build the GROUP.md write-back MCP server for one agent turn. `coords` targets
47
+ * the group + supplies the caller uid; `ownerUid` is the owner gate.
48
+ */
49
+ export declare function createGroupMdToolServer(deps: GroupMdToolDeps, coords: GroupMdSessionCoords, ownerUid: string): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * P2-C: GROUP.md write-back tool — an in-process MCP server letting the agent
3
+ * persist an updated GROUP.md back to the server. The tool surfaces to the model
4
+ * as `mcp__group_md__update_group_md`.
5
+ *
6
+ * The server is built PER TURN (`createGroupMdToolServer`) with the current
7
+ * message's channel coords + the bot owner uid, so:
8
+ * - the write targets the PARENT group of the channel the agent is in
9
+ * (`extractParentGroupNo` — a thread shares its parent group's GROUP.md);
10
+ * - invocation is GATED to the bot owner (registerBot.owner_uid). The group's
11
+ * octo_tag token has server-side write permission, but the agent is driven by
12
+ * untrusted IM users, so this owner gate — not LLM judgment, and independent
13
+ * of the token's group-role permission — is what stops a prompt-injected
14
+ * agent from rewriting the operator's trusted GROUP.md from any chat.
15
+ *
16
+ * Concurrency, the byte ceiling, and the cache refresh are owned by the shared
17
+ * {@link GroupMdWriteback} coordinator (group-md-writeback.ts); this layer is
18
+ * only the owner-gate policy + the MCP surface.
19
+ */
20
+ import { z } from 'zod';
21
+ import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk';
22
+ import { extractParentGroupNo } from './octo/channel-id.js';
23
+ import { GroupMdContentTooLargeError, MAX_GROUP_MD_CONTENT_BYTES, } from './group-md-writeback.js';
24
+ /** MCP server name; the tool surfaces as `mcp__group_md__update_group_md`. */
25
+ export const GROUP_MD_TOOL_SERVER_NAME = 'group_md';
26
+ function jsonResult(value) {
27
+ return { content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] };
28
+ }
29
+ function errResult(msg) {
30
+ return { content: [{ type: 'text', text: `Error: ${msg}` }], isError: true };
31
+ }
32
+ /**
33
+ * Build the GROUP.md tool DEFINITIONS for one agent turn. Exported separately
34
+ * from the server so tests can invoke the handler directly. `coords` targets the
35
+ * group + supplies the caller uid; `ownerUid` is the owner gate.
36
+ */
37
+ export function buildGroupMdTools(deps, coords, ownerUid) {
38
+ const isOwner = coords.fromUid === ownerUid && ownerUid !== '';
39
+ return [
40
+ tool('update_group_md', 'Persist new GROUP.md content for THIS group on the server (it becomes the ' +
41
+ "group's trusted operator instructions on the next turn). `content` is the " +
42
+ 'FULL replacement document, not a diff. Hard limit: 10240 bytes UTF-8. Only ' +
43
+ 'the bot owner may call this; a non-owner request is rejected. Last write ' +
44
+ 'wins server-side — compose the complete updated document, do not assume a ' +
45
+ 'concurrent edit merged.', {
46
+ content: z
47
+ .string()
48
+ .describe('Full replacement GROUP.md document (≤10240 bytes UTF-8).'),
49
+ }, async (args) => {
50
+ try {
51
+ if (!isOwner) {
52
+ return errResult('Only the bot owner can update GROUP.md.');
53
+ }
54
+ const groupNo = extractParentGroupNo(coords.channelId);
55
+ if (!groupNo) {
56
+ return errResult('Could not resolve a group number from this channel.');
57
+ }
58
+ // Surface a friendly over-limit message before the coordinator throws
59
+ // (it re-checks as the authoritative boundary; this is just for UX).
60
+ const bytes = Buffer.byteLength(args.content, 'utf-8');
61
+ if (bytes > MAX_GROUP_MD_CONTENT_BYTES) {
62
+ return errResult(`content is ${bytes} bytes, over the ${MAX_GROUP_MD_CONTENT_BYTES}-byte ` +
63
+ `UTF-8 limit — trim it before writing (the server would reject it).`);
64
+ }
65
+ const res = await deps.writeback.writeBack({
66
+ apiUrl: deps.apiUrl,
67
+ botToken: deps.botToken,
68
+ groupNo,
69
+ content: args.content,
70
+ });
71
+ return jsonResult({
72
+ updated: { groupNo: res.groupNo, version: res.version, bytes: res.bytes },
73
+ });
74
+ }
75
+ catch (err) {
76
+ if (err instanceof GroupMdContentTooLargeError) {
77
+ return errResult(`content is ${err.bytes} bytes, over the ${MAX_GROUP_MD_CONTENT_BYTES}-byte UTF-8 limit.`);
78
+ }
79
+ return errResult(err instanceof Error ? err.message : String(err));
80
+ }
81
+ }),
82
+ ];
83
+ }
84
+ /**
85
+ * Build the GROUP.md write-back MCP server for one agent turn. `coords` targets
86
+ * the group + supplies the caller uid; `ownerUid` is the owner gate.
87
+ */
88
+ export function createGroupMdToolServer(deps, coords, ownerUid) {
89
+ return createSdkMcpServer({
90
+ name: GROUP_MD_TOOL_SERVER_NAME,
91
+ version: '1.0.0',
92
+ tools: buildGroupMdTools(deps, coords, ownerUid),
93
+ });
94
+ }
95
+ //# sourceMappingURL=group-md-tool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"group-md-tool.js","sourceRoot":"","sources":["../src/group-md-tool.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,97 @@
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 } from './octo/api.js';
31
+ import type { GroupMdCache } from './group-md-cache.js';
32
+ /**
33
+ * Hard UTF-8 byte ceiling for GROUP.md content, per the XIN-201 measured
34
+ * server contract. A body above this is rejected locally (a server PUT would
35
+ * answer 400 err.server.bot_api.content_too_large).
36
+ */
37
+ export declare const MAX_GROUP_MD_CONTENT_BYTES = 10240;
38
+ /** Thrown when content exceeds {@link MAX_GROUP_MD_CONTENT_BYTES}. */
39
+ export declare class GroupMdContentTooLargeError extends Error {
40
+ readonly bytes: number;
41
+ constructor(bytes: number);
42
+ }
43
+ /** The `updateGroupMd` client signature, injectable for testing. */
44
+ export type UpdateGroupMdFn = typeof updateGroupMd;
45
+ /** Outcome of a successful write-back. */
46
+ export interface GroupMdWriteResult {
47
+ groupNo: string;
48
+ /** Server-assigned version after the PUT. */
49
+ version: number;
50
+ /** UTF-8 byte length of the content that was written. */
51
+ bytes: number;
52
+ }
53
+ export interface GroupMdWriteParams {
54
+ apiUrl: string;
55
+ botToken: string;
56
+ groupNo: string;
57
+ content: string;
58
+ signal?: AbortSignal;
59
+ }
60
+ /**
61
+ * Serializes GROUP.md write-backs per groupNo and keeps A's cache in sync.
62
+ *
63
+ * Constructed ONCE per bot (alongside the GroupMdCache it updates) and shared
64
+ * across turns, so the per-groupNo lock actually spans concurrent agent turns —
65
+ * a per-turn instance would defeat the lock. The MCP tool server (built per
66
+ * turn) borrows this shared instance.
67
+ *
68
+ * CAVEAT (documented per XIN-201 item 4): the lock only covers THIS gateway. A
69
+ * write from another source (operator console, a second gateway process) is not
70
+ * coordinated and, because the server has no CAS, last-write-wins across sources
71
+ * — that is an accepted limitation, not a bug.
72
+ */
73
+ export declare class GroupMdWriteback {
74
+ private readonly cache;
75
+ /** Injectable PUT client (defaults to the real octo client). */
76
+ private readonly updateFn;
77
+ /** Tail of the in-flight write chain per groupNo (serializes same-key writes). */
78
+ private readonly tails;
79
+ constructor(cache: GroupMdCache,
80
+ /** Injectable PUT client (defaults to the real octo client). */
81
+ updateFn?: UpdateGroupMdFn);
82
+ /**
83
+ * Run `fn` after every previously-queued op for `key` has settled, so bodies
84
+ * for the same key never overlap. Different keys run independently. `fn`'s
85
+ * rejection is surfaced to its own caller; the chain tail swallows it so a
86
+ * failed write does not wedge later writes.
87
+ */
88
+ private withLock;
89
+ /**
90
+ * Persist `content` as the group's GROUP.md and refresh the cache.
91
+ *
92
+ * Rejects with {@link GroupMdContentTooLargeError} (before any server call)
93
+ * when content exceeds the UTF-8 byte limit; propagates the client error when
94
+ * the PUT itself fails (cache is left untouched in that case).
95
+ */
96
+ writeBack(params: GroupMdWriteParams): Promise<GroupMdWriteResult>;
97
+ }
@@ -0,0 +1,118 @@
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 } 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
+ /** Thrown when content exceeds {@link MAX_GROUP_MD_CONTENT_BYTES}. */
38
+ export class GroupMdContentTooLargeError extends Error {
39
+ bytes;
40
+ constructor(bytes) {
41
+ super(`GROUP.md content is ${bytes} bytes, over the ${MAX_GROUP_MD_CONTENT_BYTES}-byte UTF-8 limit`);
42
+ this.bytes = bytes;
43
+ this.name = 'GroupMdContentTooLargeError';
44
+ }
45
+ }
46
+ /**
47
+ * Serializes GROUP.md write-backs per groupNo and keeps A's cache in sync.
48
+ *
49
+ * Constructed ONCE per bot (alongside the GroupMdCache it updates) and shared
50
+ * across turns, so the per-groupNo lock actually spans concurrent agent turns —
51
+ * a per-turn instance would defeat the lock. The MCP tool server (built per
52
+ * turn) borrows this shared instance.
53
+ *
54
+ * CAVEAT (documented per XIN-201 item 4): the lock only covers THIS gateway. A
55
+ * write from another source (operator console, a second gateway process) is not
56
+ * coordinated and, because the server has no CAS, last-write-wins across sources
57
+ * — that is an accepted limitation, not a bug.
58
+ */
59
+ export class GroupMdWriteback {
60
+ cache;
61
+ updateFn;
62
+ /** Tail of the in-flight write chain per groupNo (serializes same-key writes). */
63
+ tails = new Map();
64
+ constructor(cache,
65
+ /** Injectable PUT client (defaults to the real octo client). */
66
+ updateFn = updateGroupMd) {
67
+ this.cache = cache;
68
+ this.updateFn = updateFn;
69
+ }
70
+ /**
71
+ * Run `fn` after every previously-queued op for `key` has settled, so bodies
72
+ * for the same key never overlap. Different keys run independently. `fn`'s
73
+ * rejection is surfaced to its own caller; the chain tail swallows it so a
74
+ * failed write does not wedge later writes.
75
+ */
76
+ withLock(key, fn) {
77
+ const prev = this.tails.get(key) ?? Promise.resolve();
78
+ const result = prev.then(fn, fn); // run regardless of prior success/failure
79
+ const tail = result.then(() => undefined, () => undefined);
80
+ this.tails.set(key, tail);
81
+ // Best-effort cleanup: drop the key once the queue drains to this tail, so
82
+ // the Map does not grow without bound across many groups.
83
+ tail.then(() => {
84
+ if (this.tails.get(key) === tail)
85
+ this.tails.delete(key);
86
+ });
87
+ return result;
88
+ }
89
+ /**
90
+ * Persist `content` as the group's GROUP.md and refresh the cache.
91
+ *
92
+ * Rejects with {@link GroupMdContentTooLargeError} (before any server call)
93
+ * when content exceeds the UTF-8 byte limit; propagates the client error when
94
+ * the PUT itself fails (cache is left untouched in that case).
95
+ */
96
+ async writeBack(params) {
97
+ const { apiUrl, botToken, groupNo, content, signal } = params;
98
+ const bytes = Buffer.byteLength(content, 'utf-8');
99
+ if (bytes > MAX_GROUP_MD_CONTENT_BYTES) {
100
+ // Reject locally — do NOT hit the server (it would answer 400).
101
+ throw new GroupMdContentTooLargeError(bytes);
102
+ }
103
+ return this.withLock(groupNo, async () => {
104
+ const { version } = await this.updateFn({ apiUrl, botToken, groupNo, content, signal });
105
+ // Write the just-persisted content/version back into A's in-memory cache
106
+ // so the next resolveGroupInstructions serves what we wrote instead of
107
+ // either re-fetching (TTL) or, worse, returning a now-stale cached copy.
108
+ // updated_at is null because the PUT response carries only { version }.
109
+ this.cache.set(groupNo, {
110
+ content,
111
+ version: typeof version === 'number' ? version : 0,
112
+ updated_at: null,
113
+ });
114
+ return { groupNo, version, bytes };
115
+ });
116
+ }
117
+ }
118
+ //# 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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAG9C;;;;GAIG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,KAAK,CAAC;AAEhD,sEAAsE;AACtE,MAAM,OAAO,2BAA4B,SAAQ,KAAK;IACxB;IAA5B,YAA4B,KAAa;QACvC,KAAK,CACH,uBAAuB,KAAK,oBAAoB,0BAA0B,mBAAmB,CAC9F,CAAC;QAHwB,UAAK,GAAL,KAAK,CAAQ;QAIvC,IAAI,CAAC,IAAI,GAAG,6BAA6B,CAAC;IAC5C,CAAC;CACF;AAsBD;;;;;;;;;;;;GAYG;AACH,MAAM,OAAO,gBAAgB;IAKR;IAEA;IANnB,kFAAkF;IACjE,KAAK,GAAG,IAAI,GAAG,EAA4B,CAAC;IAE7D,YACmB,KAAmB;IACpC,gEAAgE;IAC/C,WAA4B,aAAa;QAFzC,UAAK,GAAL,KAAK,CAAc;QAEnB,aAAQ,GAAR,QAAQ,CAAiC;IACzD,CAAC;IAEJ;;;;;OAKG;IACK,QAAQ,CAAI,GAAW,EAAE,EAAoB;QACnD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;QACtD,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,0CAA0C;QAC5E,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CACtB,GAAG,EAAE,CAAC,SAAS,EACf,GAAG,EAAE,CAAC,SAAS,CAChB,CAAC;QACF,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAC1B,2EAA2E;QAC3E,0DAA0D;QAC1D,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE;YACb,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,IAAI;gBAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC3D,CAAC,CAAC,CAAC;QACH,OAAO,MAAM,CAAC;IAChB,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,SAAS,CAAC,MAA0B;QACxC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC;QAC9D,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAClD,IAAI,KAAK,GAAG,0BAA0B,EAAE,CAAC;YACvC,gEAAgE;YAChE,MAAM,IAAI,2BAA2B,CAAC,KAAK,CAAC,CAAC;QAC/C,CAAC;QACD,OAAO,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE;YACvC,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;YACxF,yEAAyE;YACzE,uEAAuE;YACvE,yEAAyE;YACzE,wEAAwE;YACxE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,EAAE;gBACtB,OAAO;gBACP,OAAO,EAAE,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;gBAClD,UAAU,EAAE,IAAI;aACjB,CAAC,CAAC;YACH,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QACrC,CAAC,CAAC,CAAC;IACL,CAAC;CACF"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * GROUP.md resolution (P2-A): server-first fetch with a never-lose local-file
3
+ * fallback, behind a feature flag.
4
+ *
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:
8
+ *
9
+ * 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.
12
+ * 2. Feature flag ON:
13
+ * 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);
19
+ * b. otherwise fetch from the server (server-first). On success with
20
+ * non-empty content, cache it and use it;
21
+ * c. on ANY failure (404 "no GROUP.md", network, timeout, empty content)
22
+ * fall back to the local file. The local-file path is never lost.
23
+ *
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.
29
+ *
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.
35
+ *
36
+ * Never throws — a server error degrades to local, a local miss degrades to
37
+ * "no custom instructions".
38
+ */
39
+ import type { GroupMdCache } from './group-md-cache.js';
40
+ export interface ResolveGroupInstructionsParams {
41
+ /** Operator local-instruction directory (the existing fallback source). */
42
+ groupConfigDir?: string;
43
+ /** Feature flag: when false/undefined, server fetch is skipped entirely. */
44
+ serverMd?: boolean;
45
+ apiUrl: string;
46
+ botToken: string;
47
+ /** Full channelId (may be a `<groupNo>____<shortId>` thread composite). */
48
+ channelId: string;
49
+ /** Server GROUP.md cache. Omitted (or flag off) → pure local file. */
50
+ cache?: GroupMdCache;
51
+ signal?: AbortSignal;
52
+ }
53
+ export declare function resolveGroupInstructions(params: ResolveGroupInstructionsParams): Promise<string | undefined>;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * GROUP.md resolution (P2-A): server-first fetch with a never-lose local-file
3
+ * fallback, behind a feature flag.
4
+ *
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:
8
+ *
9
+ * 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.
12
+ * 2. Feature flag ON:
13
+ * 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);
19
+ * b. otherwise fetch from the server (server-first). On success with
20
+ * non-empty content, cache it and use it;
21
+ * c. on ANY failure (404 "no GROUP.md", network, timeout, empty content)
22
+ * fall back to the local file. The local-file path is never lost.
23
+ *
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.
29
+ *
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.
35
+ *
36
+ * Never throws — a server error degrades to local, a local miss degrades to
37
+ * "no custom instructions".
38
+ */
39
+ import { getGroupMd } from './octo/api.js';
40
+ import { extractParentGroupNo } from './octo/channel-id.js';
41
+ import { loadGroupConfig, MAX_GROUP_CONFIG_BYTES } from './group-config.js';
42
+ /**
43
+ * Trim and byte-bound server-provided GROUP.md the same way loadGroupConfig
44
+ * bounds a local file, so an oversized server payload can't blow the prompt
45
+ * budget. Returns undefined for empty/whitespace-only content.
46
+ */
47
+ function boundInstructions(content) {
48
+ let text = content;
49
+ if (Buffer.byteLength(text, 'utf-8') > MAX_GROUP_CONFIG_BYTES) {
50
+ const buf = Buffer.from(text, 'utf-8').subarray(0, MAX_GROUP_CONFIG_BYTES);
51
+ // The slice may end mid-codepoint; trim a trailing replacement char.
52
+ text = buf.toString('utf-8').replace(/�+$/, '') + '\n[… group config truncated]';
53
+ }
54
+ const trimmed = text.trim();
55
+ return trimmed.length > 0 ? trimmed : undefined;
56
+ }
57
+ export async function resolveGroupInstructions(params) {
58
+ const { groupConfigDir, serverMd, apiUrl, botToken, channelId, cache, signal } = params;
59
+ const local = () => loadGroupConfig(groupConfigDir, channelId);
60
+ // Flag off (or no cache to dedupe fetches) → pure local file, unchanged behavior.
61
+ if (!serverMd || !cache)
62
+ return local();
63
+ const groupNo = extractParentGroupNo(channelId);
64
+ // a) fresh cached server GROUP.md wins. The cache is in-memory only and TTL-
65
+ // bounded, so an expired entry reads as a miss and falls through to a
66
+ // re-fetch below (staleness backstop; item B adds event-driven refresh).
67
+ const cached = cache.get(groupNo);
68
+ if (cached) {
69
+ const bounded = boundInstructions(cached.content);
70
+ if (bounded)
71
+ return bounded;
72
+ // Cached-but-empty (shouldn't normally happen) → fall through to local.
73
+ return local();
74
+ }
75
+ // b) server-first fetch.
76
+ try {
77
+ const md = await getGroupMd({ apiUrl, botToken, groupNo, signal });
78
+ const bounded = boundInstructions(md?.content ?? '');
79
+ if (bounded) {
80
+ const entry = {
81
+ content: md.content,
82
+ version: typeof md.version === 'number' ? md.version : 0,
83
+ updated_at: md.updated_at ?? null,
84
+ updated_by: md.updated_by,
85
+ };
86
+ cache.set(groupNo, entry);
87
+ return bounded;
88
+ }
89
+ // Server reachable but no/empty GROUP.md → local fallback.
90
+ return local();
91
+ }
92
+ catch (err) {
93
+ // c) 404 / network / timeout — never-lose local fallback.
94
+ console.error(`[cc-channel-octo] group-md: server fetch for ${groupNo} failed, falling back to local: ${String(err)}`);
95
+ return local();
96
+ }
97
+ }
98
+ //# 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
@@ -6,51 +6,36 @@
6
6
  * Orchestrates: loadConfig → createAdapter → SessionStore.init →
7
7
  * OctoGateway.start → setMessageHandler wiring the full pipeline.
8
8
  */
9
- import { loadConfig } from './config.js';
9
+ import { loadConfig, resolveBotConfigs } from './config.js';
10
+ import { type ManagedBot } from './bot-manager.js';
10
11
  import { SessionStore } from './session-store.js';
11
12
  import { SessionRouter } from './session-router.js';
12
13
  import { GroupContext } from './group-context.js';
13
14
  import { StreamRelay } from './stream-relay.js';
14
15
  import type { BotMessage } from './octo/types.js';
16
+ import { GroupMdCache } from './group-md-cache.js';
17
+ import { GroupMdWriteback } from './group-md-writeback.js';
15
18
  import { CronStore } from './cron-store.js';
16
19
  /**
17
- * True when there are no bots to run the gateway should idle (stay alive and
18
- * online, with no sockets) until the first bot is provisioned. Pure for unit
19
- * testing. See the idle branch in main().
20
+ * Resolve a single bot's concrete Config by its configId (config.json
21
+ * `bots[].id`). Re-reads via resolveBotConfigs so a bot added after boot picks
22
+ * up the latest global+per-bot merge. Throws if the id is absent — the manager
23
+ * treats that as a failed add (rolled back), never starts a half-bot.
20
24
  */
21
- export declare function shouldRunIdle(botConfigs: {
22
- botId?: string;
23
- }[]): boolean;
24
- export interface BotStack {
25
- botId: string;
26
- router: SessionRouter;
27
- connect: () => Promise<void>;
28
- shutdown: () => Promise<void>;
29
- }
30
- export interface StartFailure {
31
- /** The failed bot's id (or a positional `#<index>` fallback). */
32
- id: string;
33
- reason: unknown;
34
- }
25
+ export declare function resolveBotConfigById(config: ReturnType<typeof loadConfig>, configId: string): ReturnType<typeof resolveBotConfigs>[number];
35
26
  /**
36
- * Split the settled results of starting every bot into the stacks that came up
37
- * and the ones that failed. Pure (no I/O) so the multi-bot resilience policy
38
- * skip failed bots, keep the rest, only fatal when none start is unit
39
- * testable. A failed bot's own partial resources are released inside startBot's
40
- * failure path (see its try/catch); this function never touches a stack.
27
+ * What startBot returns. The hot-reload manager (BotManager) consumes exactly
28
+ * ManagedBot robotUid (loop-guard key) + router + connect + shutdown so
29
+ * BotStack is just ManagedBot. The configId is the addBot argument, not a field
30
+ * on the stack (see plan B2 for why the two identities are kept separate).
41
31
  */
42
- export declare function partitionStartResults(results: PromiseSettledResult<BotStack>[], configs: {
43
- botId?: string;
44
- }[]): {
45
- stacks: BotStack[];
46
- failures: StartFailure[];
47
- };
32
+ export type BotStack = ManagedBot;
48
33
  /**
49
34
  * Process a single inbound message through the full pipeline: route → context →
50
35
  * agent query → stream → persist. Exported so tests can drive the real pipeline
51
36
  * (not a replica) — `main()` is the only production caller.
52
37
  */
53
- 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): Promise<void>;
54
39
  /** Max length of the rendered tool-params string in a 🔧 progress notice. */
55
40
  export declare const MAX_TOOL_PARAM_CHARS = 120;
56
41
  /**