@mininglamp-oss/cc-channel-octo 1.0.3-dev.4ca07a0 → 1.0.3-dev.4edf363
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 +6 -1
- package/dist/agent-bridge.js +18 -6
- package/dist/agent-bridge.js.map +1 -1
- package/dist/config.d.ts +52 -0
- package/dist/config.js +6 -0
- package/dist/config.js.map +1 -1
- package/dist/group-context.d.ts +18 -0
- package/dist/group-context.js +77 -6
- package/dist/group-context.js.map +1 -1
- package/dist/group-md-cache.d.ts +50 -0
- package/dist/group-md-cache.js +79 -0
- package/dist/group-md-cache.js.map +1 -1
- package/dist/group-md-events.d.ts +36 -4
- package/dist/group-md-events.js +36 -5
- package/dist/group-md-events.js.map +1 -1
- 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 +47 -24
- package/dist/group-md.js +112 -23
- package/dist/group-md.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +112 -17
- package/dist/index.js.map +1 -1
- package/dist/octo/api.d.ts +59 -0
- package/dist/octo/api.js +53 -0
- package/dist/octo/api.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 +30 -2
- package/dist/session-router.js +72 -9
- package/dist/session-router.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,183 @@
|
|
|
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
|
+
import type { GroupMdCache, ThreadMdCache } 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
|
+
/**
|
|
39
|
+
* Hard UTF-8 byte ceiling for THREAD.md content. The thread md endpoint shares
|
|
40
|
+
* the group md limit (P3-2 contract check: the official Octo web client edits
|
|
41
|
+
* both group and thread md through the SAME editor, whose `MAX_BYTES` is 10240),
|
|
42
|
+
* so this is an alias of {@link MAX_GROUP_MD_CONTENT_BYTES} rather than a second
|
|
43
|
+
* magic number — if the server limit ever diverges, the two can split here.
|
|
44
|
+
*/
|
|
45
|
+
export declare const MAX_THREAD_MD_CONTENT_BYTES = 10240;
|
|
46
|
+
/** Thrown when content exceeds {@link MAX_GROUP_MD_CONTENT_BYTES}. */
|
|
47
|
+
export declare class GroupMdContentTooLargeError extends Error {
|
|
48
|
+
readonly bytes: number;
|
|
49
|
+
constructor(bytes: number);
|
|
50
|
+
}
|
|
51
|
+
/** The `updateGroupMd` client signature, injectable for testing. */
|
|
52
|
+
export type UpdateGroupMdFn = typeof updateGroupMd;
|
|
53
|
+
/** Outcome of a successful write-back. */
|
|
54
|
+
export interface GroupMdWriteResult {
|
|
55
|
+
groupNo: string;
|
|
56
|
+
/** Server-assigned version after the PUT. */
|
|
57
|
+
version: number;
|
|
58
|
+
/** UTF-8 byte length of the content that was written. */
|
|
59
|
+
bytes: number;
|
|
60
|
+
}
|
|
61
|
+
export interface GroupMdWriteParams {
|
|
62
|
+
apiUrl: string;
|
|
63
|
+
botToken: string;
|
|
64
|
+
groupNo: string;
|
|
65
|
+
content: string;
|
|
66
|
+
signal?: AbortSignal;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Serializes GROUP.md write-backs per groupNo and keeps A's cache in sync.
|
|
70
|
+
*
|
|
71
|
+
* Constructed ONCE per bot (alongside the GroupMdCache it updates) and shared
|
|
72
|
+
* across turns, so the per-groupNo lock actually spans concurrent agent turns —
|
|
73
|
+
* a per-turn instance would defeat the lock. The MCP tool server (built per
|
|
74
|
+
* turn) borrows this shared instance.
|
|
75
|
+
*
|
|
76
|
+
* CAVEAT (documented per XIN-201 item 4): the lock only covers THIS gateway. A
|
|
77
|
+
* write from another source (operator console, a second gateway process) is not
|
|
78
|
+
* coordinated and, because the server has no CAS, last-write-wins across sources
|
|
79
|
+
* — that is an accepted limitation, not a bug.
|
|
80
|
+
*/
|
|
81
|
+
export declare class GroupMdWriteback {
|
|
82
|
+
private readonly cache;
|
|
83
|
+
/** Injectable PUT client (defaults to the real octo client). */
|
|
84
|
+
private readonly updateFn;
|
|
85
|
+
/** Tail of the in-flight write chain per groupNo (serializes same-key writes). */
|
|
86
|
+
private readonly tails;
|
|
87
|
+
constructor(cache: GroupMdCache,
|
|
88
|
+
/** Injectable PUT client (defaults to the real octo client). */
|
|
89
|
+
updateFn?: UpdateGroupMdFn);
|
|
90
|
+
/**
|
|
91
|
+
* Run `fn` after every previously-queued op for `key` has settled, so bodies
|
|
92
|
+
* for the same key never overlap. Different keys run independently. `fn`'s
|
|
93
|
+
* rejection is surfaced to its own caller; the chain tail swallows it so a
|
|
94
|
+
* failed write does not wedge later writes.
|
|
95
|
+
*/
|
|
96
|
+
private withLock;
|
|
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
|
+
writeBack(params: GroupMdWriteParams): Promise<GroupMdWriteResult>;
|
|
105
|
+
}
|
|
106
|
+
/** The `updateThreadMd` client signature, injectable for testing. */
|
|
107
|
+
export type UpdateThreadMdFn = typeof updateThreadMd;
|
|
108
|
+
/** Thrown when THREAD.md content exceeds {@link MAX_THREAD_MD_CONTENT_BYTES}. */
|
|
109
|
+
export declare class ThreadMdContentTooLargeError extends Error {
|
|
110
|
+
readonly bytes: number;
|
|
111
|
+
constructor(bytes: number);
|
|
112
|
+
}
|
|
113
|
+
/** Outcome of a successful THREAD.md write-back. */
|
|
114
|
+
export interface ThreadMdWriteResult {
|
|
115
|
+
groupNo: string;
|
|
116
|
+
shortId: string;
|
|
117
|
+
/** Server-assigned version after the PUT. */
|
|
118
|
+
version: number;
|
|
119
|
+
/** UTF-8 byte length of the content that was written. */
|
|
120
|
+
bytes: number;
|
|
121
|
+
}
|
|
122
|
+
export interface ThreadMdWriteParams {
|
|
123
|
+
apiUrl: string;
|
|
124
|
+
botToken: string;
|
|
125
|
+
groupNo: string;
|
|
126
|
+
shortId: string;
|
|
127
|
+
content: string;
|
|
128
|
+
signal?: AbortSignal;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Thread analogue of {@link GroupMdWriteback} (P3-2): persists an agent-authored
|
|
132
|
+
* THREAD.md back to the server (PUT /v1/bot/groups/{groupNo}/threads/{shortId}/md
|
|
133
|
+
* via `updateThreadMd`), then refreshes the in-memory {@link ThreadMdCache} so
|
|
134
|
+
* the next resolve does not TTL-refetch a stale copy.
|
|
135
|
+
*
|
|
136
|
+
* Carries over every P2-C safety property, adapted to the thread key:
|
|
137
|
+
*
|
|
138
|
+
* 1. Same HARD ≤10240-byte (UTF-8) limit ({@link MAX_THREAD_MD_CONTENT_BYTES},
|
|
139
|
+
* confirmed to equal the group limit — see that constant). Rejected locally
|
|
140
|
+
* BEFORE the PUT so an oversized body never reaches the server.
|
|
141
|
+
*
|
|
142
|
+
* 2. The thread PUT, like the group PUT, does NOT compare-and-swap (body is
|
|
143
|
+
* just `{ content }`, reply is `{ version }`). All write-backs for the same
|
|
144
|
+
* thread are serialized through a per-`groupNo::shortId` promise-chain lock
|
|
145
|
+
* so this gateway never races itself. Cross-source last-write-wins remains
|
|
146
|
+
* possible by design, identical to the group caveat.
|
|
147
|
+
*
|
|
148
|
+
* Keyed by the COMPOSITE `groupNo::shortId` — the SAME key the thread cache and
|
|
149
|
+
* the P3-1 read side use — so a thread's write lock and cache refresh can never
|
|
150
|
+
* be confused with its parent group's (which the group coordinator locks by the
|
|
151
|
+
* bare groupNo). `::` is outside the safe-id charset, so the two key spaces are
|
|
152
|
+
* disjoint. Owner-gating lives in the MCP tool layer (group-md-tool.ts), not
|
|
153
|
+
* here. Never persists to disk — the cache it updates is the same memory-only
|
|
154
|
+
* cache the resolver reads.
|
|
155
|
+
*/
|
|
156
|
+
export declare class ThreadMdWriteback {
|
|
157
|
+
private readonly cache;
|
|
158
|
+
/** Injectable PUT client (defaults to the real octo client). */
|
|
159
|
+
private readonly updateFn;
|
|
160
|
+
/** Tail of the in-flight write chain per composite key (serializes same-thread writes). */
|
|
161
|
+
private readonly tails;
|
|
162
|
+
constructor(cache: ThreadMdCache,
|
|
163
|
+
/** Injectable PUT client (defaults to the real octo client). */
|
|
164
|
+
updateFn?: UpdateThreadMdFn);
|
|
165
|
+
/** Composite lock key; mirrors ThreadMdCache's `groupNo::shortId`. */
|
|
166
|
+
private lockKey;
|
|
167
|
+
/**
|
|
168
|
+
* Run `fn` after every previously-queued op for `key` has settled, so bodies
|
|
169
|
+
* for the same key never overlap. Different keys run independently. `fn`'s
|
|
170
|
+
* rejection is surfaced to its own caller; the chain tail swallows it so a
|
|
171
|
+
* failed write does not wedge later writes. Identical policy to
|
|
172
|
+
* {@link GroupMdWriteback}.
|
|
173
|
+
*/
|
|
174
|
+
private withLock;
|
|
175
|
+
/**
|
|
176
|
+
* Persist `content` as the thread's THREAD.md and refresh the thread cache.
|
|
177
|
+
*
|
|
178
|
+
* Rejects with {@link ThreadMdContentTooLargeError} (before any server call)
|
|
179
|
+
* when content exceeds the UTF-8 byte limit; propagates the client error when
|
|
180
|
+
* the PUT itself fails (cache is left untouched in that case).
|
|
181
|
+
*/
|
|
182
|
+
writeBack(params: ThreadMdWriteParams): Promise<ThreadMdWriteResult>;
|
|
183
|
+
}
|
|
@@ -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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"}
|
package/dist/group-md.d.ts
CHANGED
|
@@ -1,53 +1,76 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* GROUP.md resolution
|
|
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-
|
|
7
|
-
* agent's system prompt.
|
|
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
|
|
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
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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
|
|
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
|
|
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>;
|