agent-coord-mcp 0.14.0 → 0.15.0
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/README.md +48 -4
- package/dist/server.js +45 -5
- package/dist/server.js.map +1 -1
- package/dist/store.js +58 -0
- package/dist/store.js.map +1 -1
- package/dist/tools/admin.js +296 -0
- package/dist/tools/admin.js.map +1 -0
- package/dist/tools/index.js +7 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/messaging.js +317 -0
- package/dist/tools/messaging.js.map +1 -0
- package/dist/tools/registry.js +237 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/rooms.js +136 -0
- package/dist/tools/rooms.js.map +1 -0
- package/dist/tools/shared.js +61 -0
- package/dist/tools/shared.js.map +1 -0
- package/dist/tools/transport.js +1003 -0
- package/dist/tools/transport.js.map +1 -0
- package/dist/tools.js +11 -79
- package/dist/tools.js.map +1 -1
- package/hooks/tmux-pusher.mjs +37 -3
- package/package.json +1 -1
- package/scripts/coord-chat.mjs +1 -1
- package/scripts/coord-node.sh +98 -0
- package/scripts/coord-pusher.mjs +37 -4
- package/scripts/coord-token.mjs +126 -0
- package/src/server.ts +54 -5
- package/src/store.ts +62 -0
- package/src/tools/admin.ts +347 -0
- package/src/tools/index.ts +6 -0
- package/src/tools/messaging.ts +425 -0
- package/src/tools/registry.ts +329 -0
- package/src/tools/rooms.ts +215 -0
- package/src/tools/shared.ts +167 -0
- package/src/{tools.ts → tools/transport.ts} +23 -1008
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { AGENTS_FILE, ARCHIVE_STATUS_FILE, CURSOR_DIR, DEFAULT_ROOM, INBOX_DIR, ROOMS_FILE, STATUS_FILE, archiveInboxFile, archiveJsonl, archiveRoomFile, deleteFile, getRooms, listCursorFiles, listInboxFiles, listReceiptFiles, normalizeRoom, pruneHistory, readJson, readJsonl, rewriteJsonl, roomFile, updateJson, } from "../store.js";
|
|
4
|
+
// ---------- prune ----------
|
|
5
|
+
export const PRUNE_TARGETS = ["rooms", "status", "inbox", "receipts", "members"];
|
|
6
|
+
export const pruneSchema = {
|
|
7
|
+
olderThanDays: z.number().positive().max(365).optional(),
|
|
8
|
+
decisionDays: z.number().positive().max(365).optional(),
|
|
9
|
+
room: z.string().optional(),
|
|
10
|
+
targets: z.array(z.enum(PRUNE_TARGETS)).optional(),
|
|
11
|
+
removeOrphanInboxes: z.boolean().optional(),
|
|
12
|
+
archiveEmptyRooms: z.boolean().optional(),
|
|
13
|
+
dryRun: z.boolean().optional(),
|
|
14
|
+
};
|
|
15
|
+
// Retention predicate: kind="decision" messages live by the (longer)
|
|
16
|
+
// decision cutoff; everything else by the standard cutoff.
|
|
17
|
+
function keepEntry(e, cutoff, decisionCutoff) {
|
|
18
|
+
return e.kind === "decision" ? e.ts > decisionCutoff : e.ts > cutoff;
|
|
19
|
+
}
|
|
20
|
+
// Shift every agent's cursor offsets down by the number of entries removed
|
|
21
|
+
// ahead of them (append-only + oldest-first removal ⇒ plain subtraction,
|
|
22
|
+
// clamped at 0). Shared by prune and live compaction — without this, an
|
|
23
|
+
// offset past the new file length makes read_messages return [] forever.
|
|
24
|
+
export async function adjustCursors(removals) {
|
|
25
|
+
const roomRemovedByChan = removals.roomRemovedByChan ?? {};
|
|
26
|
+
const statusRemoved = removals.statusRemoved ?? 0;
|
|
27
|
+
const perAgentInboxRemoved = removals.perAgentInboxRemoved ?? {};
|
|
28
|
+
const cursorsAdjusted = [];
|
|
29
|
+
for (const cname of await listCursorFiles()) {
|
|
30
|
+
const id = cname.replace(/\.json$/, "");
|
|
31
|
+
const cursorPath = path.join(CURSOR_DIR, cname);
|
|
32
|
+
let touched = false;
|
|
33
|
+
await updateJson(cursorPath, {}, (current) => {
|
|
34
|
+
for (const [chan, removed] of Object.entries(roomRemovedByChan)) {
|
|
35
|
+
if (chan === DEFAULT_ROOM) {
|
|
36
|
+
if (current.roomOffset !== undefined) {
|
|
37
|
+
current.roomOffset = Math.max(0, current.roomOffset - removed);
|
|
38
|
+
touched = true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else if (current.roomOffsets?.[chan] !== undefined) {
|
|
42
|
+
current.roomOffsets[chan] = Math.max(0, current.roomOffsets[chan] - removed);
|
|
43
|
+
touched = true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (current.statusOffset !== undefined && statusRemoved > 0) {
|
|
47
|
+
current.statusOffset = Math.max(0, current.statusOffset - statusRemoved);
|
|
48
|
+
touched = true;
|
|
49
|
+
}
|
|
50
|
+
const myInboxRemoved = perAgentInboxRemoved[id] ?? 0;
|
|
51
|
+
if (current.inboxOffset !== undefined && myInboxRemoved > 0) {
|
|
52
|
+
current.inboxOffset = Math.max(0, current.inboxOffset - myInboxRemoved);
|
|
53
|
+
touched = true;
|
|
54
|
+
}
|
|
55
|
+
return current;
|
|
56
|
+
});
|
|
57
|
+
if (touched)
|
|
58
|
+
cursorsAdjusted.push(id);
|
|
59
|
+
}
|
|
60
|
+
return cursorsAdjusted;
|
|
61
|
+
}
|
|
62
|
+
export async function pruneTool(args) {
|
|
63
|
+
const days = args.olderThanDays ?? 7;
|
|
64
|
+
const decisionDays = args.decisionDays ?? 30;
|
|
65
|
+
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
66
|
+
const decisionCutoff = Date.now() - decisionDays * 24 * 60 * 60 * 1000;
|
|
67
|
+
const dryRun = args.dryRun ?? false;
|
|
68
|
+
// room-scoped prune touches only that channel's messages; targets narrows
|
|
69
|
+
// the sweep otherwise. Default remains "everything".
|
|
70
|
+
const scopedRoom = args.room ? normalizeRoom(args.room) : undefined;
|
|
71
|
+
const targets = new Set(scopedRoom ? ["rooms"] : args.targets ?? [...PRUNE_TARGETS]);
|
|
72
|
+
const keep = (e) => keepEntry(e, cutoff, decisionCutoff);
|
|
73
|
+
const reg = await readJson(AGENTS_FILE, {});
|
|
74
|
+
const knownAgents = new Set(Object.keys(reg));
|
|
75
|
+
// A member is stale when its agent is gone from the registry (orphan) or
|
|
76
|
+
// hasn't heartbeated since the cutoff (dead session).
|
|
77
|
+
const staleAgent = (m) => !knownAgents.has(m) || (reg[m]?.lastHeartbeat ?? 0) <= cutoff;
|
|
78
|
+
const channels = scopedRoom ? [scopedRoom] : Object.keys(await getRooms());
|
|
79
|
+
if (dryRun) {
|
|
80
|
+
let roomMessages = 0;
|
|
81
|
+
if (targets.has("rooms")) {
|
|
82
|
+
for (const chan of channels) {
|
|
83
|
+
const msgs = await readJsonl(roomFile(chan));
|
|
84
|
+
roomMessages += msgs.filter((e) => !keep(e)).length;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const status = targets.has("status") ? await readJsonl(STATUS_FILE) : [];
|
|
88
|
+
let inboxRemoved = 0;
|
|
89
|
+
const orphans = [];
|
|
90
|
+
if (targets.has("inbox")) {
|
|
91
|
+
for (const fname of await listInboxFiles()) {
|
|
92
|
+
const id = fname.replace(/\.jsonl$/, "");
|
|
93
|
+
if (!knownAgents.has(id) && (args.removeOrphanInboxes ?? true))
|
|
94
|
+
orphans.push(fname);
|
|
95
|
+
const entries = await readJsonl(path.join(INBOX_DIR, fname));
|
|
96
|
+
inboxRemoved += entries.filter((e) => e.ts <= cutoff).length;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const orphanMembers = new Set();
|
|
100
|
+
const staleMembers = new Set();
|
|
101
|
+
const emptyRooms = [];
|
|
102
|
+
if (targets.has("members")) {
|
|
103
|
+
const rooms = await getRooms();
|
|
104
|
+
for (const [chan, e] of Object.entries(rooms)) {
|
|
105
|
+
const remaining = [];
|
|
106
|
+
for (const m of e.members ?? []) {
|
|
107
|
+
if (!knownAgents.has(m))
|
|
108
|
+
orphanMembers.add(m);
|
|
109
|
+
else if (staleAgent(m))
|
|
110
|
+
staleMembers.add(m);
|
|
111
|
+
else
|
|
112
|
+
remaining.push(m);
|
|
113
|
+
}
|
|
114
|
+
if ((args.archiveEmptyRooms ?? true) &&
|
|
115
|
+
chan !== DEFAULT_ROOM &&
|
|
116
|
+
remaining.length === 0) {
|
|
117
|
+
const msgs = await readJsonl(roomFile(chan));
|
|
118
|
+
const lastTs = msgs[msgs.length - 1]?.ts ?? 0;
|
|
119
|
+
if (lastTs <= cutoff)
|
|
120
|
+
emptyRooms.push(chan);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
let receiptsRemoved = 0;
|
|
125
|
+
const orphanReceipts = [];
|
|
126
|
+
if (targets.has("receipts")) {
|
|
127
|
+
for (const filePath of await listReceiptFiles()) {
|
|
128
|
+
const id = path.basename(filePath).replace(/\.jsonl$/, "");
|
|
129
|
+
const entries = await readJsonl(filePath);
|
|
130
|
+
if (!knownAgents.has(id) && (args.removeOrphanInboxes ?? true))
|
|
131
|
+
orphanReceipts.push(id);
|
|
132
|
+
receiptsRemoved += entries.filter((e) => e.ts <= cutoff).length;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
dryRun: true,
|
|
137
|
+
cutoff,
|
|
138
|
+
olderThanDays: days,
|
|
139
|
+
decisionDays,
|
|
140
|
+
...(scopedRoom ? { room: scopedRoom } : {}),
|
|
141
|
+
wouldRemove: {
|
|
142
|
+
roomMessages,
|
|
143
|
+
statusEntries: status.filter((e) => e.ts <= cutoff).length,
|
|
144
|
+
inboxMessages: inboxRemoved,
|
|
145
|
+
orphanInboxes: orphans,
|
|
146
|
+
orphanMembers: [...orphanMembers],
|
|
147
|
+
staleMembers: [...staleMembers],
|
|
148
|
+
archivedRooms: emptyRooms,
|
|
149
|
+
receipts: receiptsRemoved,
|
|
150
|
+
orphanReceipts,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
// Per-channel removed counts so we can shift the matching offset in each
|
|
155
|
+
// cursor (default channel → roomOffset, others → roomOffsets[chan]).
|
|
156
|
+
// Aged-out entries are archived to archive/rooms/<chan>.jsonl, never deleted.
|
|
157
|
+
const roomRemovedByChan = {};
|
|
158
|
+
let roomRemovedTotal = 0;
|
|
159
|
+
if (targets.has("rooms")) {
|
|
160
|
+
for (const chan of channels) {
|
|
161
|
+
const r = await archiveJsonl(roomFile(chan), archiveRoomFile(chan), keep);
|
|
162
|
+
if (r.removed > 0) {
|
|
163
|
+
roomRemovedByChan[chan] = r.removed;
|
|
164
|
+
roomRemovedTotal += r.removed;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const statusResult = targets.has("status")
|
|
169
|
+
? await archiveJsonl(STATUS_FILE, ARCHIVE_STATUS_FILE, (e) => e.ts > cutoff)
|
|
170
|
+
: { kept: 0, removed: 0, archived: 0 };
|
|
171
|
+
let inboxRemoved = 0;
|
|
172
|
+
const deletedOrphans = [];
|
|
173
|
+
// perAgentInboxRemoved: how many entries were stripped from each *kept* agent's
|
|
174
|
+
// inbox, so we can shift only that agent's inboxOffset cursor below.
|
|
175
|
+
const perAgentInboxRemoved = {};
|
|
176
|
+
if (targets.has("inbox")) {
|
|
177
|
+
for (const fname of await listInboxFiles()) {
|
|
178
|
+
const id = fname.replace(/\.jsonl$/, "");
|
|
179
|
+
const filePath = path.join(INBOX_DIR, fname);
|
|
180
|
+
if (!knownAgents.has(id) && (args.removeOrphanInboxes ?? true)) {
|
|
181
|
+
// Orphan inbox: archive everything it held, then drop the live file.
|
|
182
|
+
const r = await archiveJsonl(filePath, archiveInboxFile(id), () => false);
|
|
183
|
+
inboxRemoved += r.removed;
|
|
184
|
+
await deleteFile(filePath);
|
|
185
|
+
deletedOrphans.push(id);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const r = await archiveJsonl(filePath, archiveInboxFile(id), (e) => e.ts > cutoff);
|
|
189
|
+
inboxRemoved += r.removed;
|
|
190
|
+
perAgentInboxRemoved[id] = r.removed;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const cursorsAdjusted = await adjustCursors({
|
|
194
|
+
roomRemovedByChan,
|
|
195
|
+
statusRemoved: statusResult.removed,
|
|
196
|
+
perAgentInboxRemoved,
|
|
197
|
+
});
|
|
198
|
+
// Membership sweep: drop orphans (not in registry) and stale members (no
|
|
199
|
+
// heartbeat since cutoff) so list_rooms / joinedRooms() stop surfacing
|
|
200
|
+
// ghosts. Then archive non-default rooms left with no members and no
|
|
201
|
+
// activity since the cutoff: remaining messages move to the archive, the
|
|
202
|
+
// room leaves the registry, and every cursor forgets its offset — the same
|
|
203
|
+
// cleanup delete_room performs, minus the data loss.
|
|
204
|
+
const orphanMembers = new Set();
|
|
205
|
+
const staleMembers = new Set();
|
|
206
|
+
const archivedRooms = [];
|
|
207
|
+
if (targets.has("members")) {
|
|
208
|
+
await updateJson(ROOMS_FILE, {}, (current) => {
|
|
209
|
+
for (const e of Object.values(current)) {
|
|
210
|
+
if ((e.members?.length ?? 0) === 0)
|
|
211
|
+
continue;
|
|
212
|
+
e.members = (e.members ?? []).filter((m) => {
|
|
213
|
+
if (!knownAgents.has(m)) {
|
|
214
|
+
orphanMembers.add(m);
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
if (staleAgent(m)) {
|
|
218
|
+
staleMembers.add(m);
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
return true;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
return current;
|
|
225
|
+
});
|
|
226
|
+
if (args.archiveEmptyRooms ?? true) {
|
|
227
|
+
const rooms = await getRooms();
|
|
228
|
+
for (const [chan, e] of Object.entries(rooms)) {
|
|
229
|
+
if (chan === DEFAULT_ROOM || (e.members?.length ?? 0) > 0)
|
|
230
|
+
continue;
|
|
231
|
+
const file = roomFile(chan);
|
|
232
|
+
const msgs = await readJsonl(file);
|
|
233
|
+
const lastTs = msgs[msgs.length - 1]?.ts ?? 0;
|
|
234
|
+
if (lastTs > cutoff)
|
|
235
|
+
continue;
|
|
236
|
+
await archiveJsonl(file, archiveRoomFile(chan), () => false);
|
|
237
|
+
await deleteFile(file);
|
|
238
|
+
await updateJson(ROOMS_FILE, {}, (current) => {
|
|
239
|
+
delete current[chan];
|
|
240
|
+
return current;
|
|
241
|
+
});
|
|
242
|
+
for (const cname of await listCursorFiles()) {
|
|
243
|
+
await updateJson(path.join(CURSOR_DIR, cname), {}, (current) => {
|
|
244
|
+
if (current.roomOffsets?.[chan] !== undefined)
|
|
245
|
+
delete current.roomOffsets[chan];
|
|
246
|
+
return current;
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
archivedRooms.push(chan);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// Receipts are out-of-band proof logs with no cursor and no analysis value —
|
|
254
|
+
// trim old entries by ts and delete logs for agents no longer registered.
|
|
255
|
+
// The one stream that is genuinely deleted, not archived.
|
|
256
|
+
let receiptsRemoved = 0;
|
|
257
|
+
const deletedOrphanReceipts = [];
|
|
258
|
+
if (targets.has("receipts")) {
|
|
259
|
+
for (const filePath of await listReceiptFiles()) {
|
|
260
|
+
const id = path.basename(filePath).replace(/\.jsonl$/, "");
|
|
261
|
+
if (!knownAgents.has(id) && (args.removeOrphanInboxes ?? true)) {
|
|
262
|
+
const entries = await readJsonl(filePath);
|
|
263
|
+
receiptsRemoved += entries.length;
|
|
264
|
+
await deleteFile(filePath);
|
|
265
|
+
deletedOrphanReceipts.push(id);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const r = await rewriteJsonl(filePath, (e) => e.ts > cutoff);
|
|
269
|
+
receiptsRemoved += r.removed;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// Sweep expired reversible-history entries (TTL'd cache; also self-prunes on
|
|
273
|
+
// every read, so this just catches entries on a server with few reads).
|
|
274
|
+
await pruneHistory();
|
|
275
|
+
return {
|
|
276
|
+
dryRun: false,
|
|
277
|
+
cutoff,
|
|
278
|
+
olderThanDays: days,
|
|
279
|
+
decisionDays,
|
|
280
|
+
...(scopedRoom ? { room: scopedRoom } : {}),
|
|
281
|
+
removed: {
|
|
282
|
+
roomMessages: roomRemovedTotal,
|
|
283
|
+
statusEntries: statusResult.removed,
|
|
284
|
+
inboxMessages: inboxRemoved,
|
|
285
|
+
orphanInboxes: deletedOrphans,
|
|
286
|
+
orphanMembers: [...orphanMembers],
|
|
287
|
+
staleMembers: [...staleMembers],
|
|
288
|
+
archivedRooms,
|
|
289
|
+
receipts: receiptsRemoved,
|
|
290
|
+
orphanReceipts: deletedOrphanReceipts,
|
|
291
|
+
},
|
|
292
|
+
archivedTo: "archive/ (rooms/<chan>.jsonl, status.jsonl, inbox/<agent>.jsonl; receipts deleted)",
|
|
293
|
+
cursorsAdjusted,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
//# sourceMappingURL=admin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin.js","sourceRoot":"","sources":["../../src/tools/admin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,UAAU,EACV,YAAY,EACZ,SAAS,EACT,UAAU,EACV,WAAW,EACX,gBAAgB,EAChB,YAAY,EACZ,eAAe,EACf,UAAU,EACV,QAAQ,EACR,eAAe,EACf,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,QAAQ,EACR,UAAU,GAEX,MAAM,aAAa,CAAC;AAQrB,8BAA8B;AAE9B,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,CAAU,CAAC;AAG1F,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE;IACxD,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE;IACvD,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC3B,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,QAAQ,EAAE;IAClD,mBAAmB,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE;IAC3C,iBAAiB,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE;IACzC,MAAM,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE;CAC/B,CAAC;AAEF,qEAAqE;AACrE,2DAA2D;AAC3D,SAAS,SAAS,CAAC,CAAgC,EAAE,MAAc,EAAE,cAAsB;IACzF,OAAO,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,MAAM,CAAC;AACvE,CAAC;AAED,2EAA2E;AAC3E,yEAAyE;AACzE,wEAAwE;AACxE,yEAAyE;AACzE,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,QAInC;IACC,MAAM,iBAAiB,GAAG,QAAQ,CAAC,iBAAiB,IAAI,EAAE,CAAC;IAC3D,MAAM,aAAa,GAAG,QAAQ,CAAC,aAAa,IAAI,CAAC,CAAC;IAClD,MAAM,oBAAoB,GAAG,QAAQ,CAAC,oBAAoB,IAAI,EAAE,CAAC;IACjE,MAAM,eAAe,GAAa,EAAE,CAAC;IACrC,KAAK,MAAM,KAAK,IAAI,MAAM,eAAe,EAAE,EAAE,CAAC;QAC5C,MAAM,EAAE,GAAG,KAAK,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;QACxC,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QAChD,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,MAAM,UAAU,CAAS,UAAU,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,EAAE;YACnD,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,iBAAiB,CAAC,EAAE,CAAC;gBAChE,IAAI,IAAI,KAAK,YAAY,EAAE,CAAC;oBAC1B,IAAI,OAAO,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;wBACrC,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,UAAU,GAAG,OAAO,CAAC,CAAC;wBAC/D,OAAO,GAAG,IAAI,CAAC;oBACjB,CAAC;gBACH,CAAC;qBAAM,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC,IAAI,CAAC,KAAK,SAAS,EAAE,CAAC;oBACrD,OAAO,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC;oBAC7E,OAAO,GAAG,IAAI,CAAC;gBACjB,CAAC;YACH,CAAC;YACD,IAAI,OAAO,CAAC,YAAY,KAAK,SAAS,IAAI,aAAa,GAAG,CAAC,EAAE,CAAC;gBAC5D,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,YAAY,GAAG,aAAa,CAAC,CAAC;gBACzE,OAAO,GAAG,IAAI,CAAC;YACjB,CAAC;YACD,MAAM,cAAc,GAAG,oBAAoB,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;YACrD,IAAI,OAAO,CAAC,WAAW,KAAK,SAAS,IAAI,cAAc,GAAG,CAAC,EAAE,CAAC;gBAC5D,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,WAAW,GAAG,cAAc,CAAC,CAAC;gBACxE,OAAO,GAAG,IAAI,CAAC;YACjB,CAAC;YACD,OAAO,OAAO,CAAC;QACjB,CAAC,CAAC,CAAC;QACH,IAAI,OAAO;YAAE,eAAe,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACxC,CAAC;IACD,OAAO,eAAe,CAAC;AACzB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,IAQ/B;IACC,MAAM,IAAI,GAAG,IAAI,CAAC,aAAa,IAAI,CAAC,CAAC;IACrC,MAAM,YAAY,GAAG,IAAI,CAAC,YAAY,IAAI,EAAE,CAAC;IAC7C,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IACvD,MAAM,cAAc,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,YAAY,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IACvE,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC;IACpC,0EAA0E;IAC1E,qDAAqD;IACrD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACpE,MAAM,OAAO,GAAG,IAAI,GAAG,CACrB,UAAU,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,GAAG,aAAa,CAAC,CAC5D,CAAC;IACF,MAAM,IAAI,GAAG,CAAC,CAAgC,EAAE,EAAE,CAAC,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,cAAc,CAAC,CAAC;IAExF,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAgB,WAAW,EAAE,EAAE,CAAC,CAAC;IAC3D,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9C,yEAAyE;IACzE,sDAAsD;IACtD,MAAM,UAAU,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,aAAa,IAAI,CAAC,CAAC,IAAI,MAAM,CAAC;IAEhG,MAAM,QAAQ,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,QAAQ,EAAE,CAAC,CAAC;IAE3E,IAAI,MAAM,EAAE,CAAC;QACX,IAAI,YAAY,GAAG,CAAC,CAAC;QACrB,IAAI,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YACzB,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;gBAC5B,MAAM,IAAI,GAAG,MAAM,SAAS,CAAU,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;gBACtD,YAAY,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;YACtD,CAAC;QACH,CAAC;QACD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,SAAS,CAAc,WAAW,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACtF,IAAI,YAAY,GAAG,CAAC,CAAC;QACrB,MAAM,OAAO,GAAa,EAAE,CAAC;QAC7B,IAAI,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YACzB,KAAK,MAAM,KAAK,IAAI,MAAM,cAAc,EAAE,EAAE,CAAC;gBAC3C,MAAM,EAAE,GAAG,KAAK,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;gBACzC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,mBAAmB,IAAI,IAAI,CAAC;oBAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBACpF,MAAM,OAAO,GAAG,MAAM,SAAS,CAAU,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC;gBACtE,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,MAAM,CAAC,CAAC,MAAM,CAAC;YAC/D,CAAC;QACH,CAAC;QACD,MAAM,aAAa,GAAG,IAAI,GAAG,EAAU,CAAC;QACxC,MAAM,YAAY,GAAG,IAAI,GAAG,EAAU,CAAC;QACvC,MAAM,UAAU,GAAa,EAAE,CAAC;QAChC,IAAI,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;YAC3B,MAAM,KAAK,GAAG,MAAM,QAAQ,EAAE,CAAC;YAC/B,KAAK,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC9C,MAAM,SAAS,GAAa,EAAE,CAAC;gBAC/B,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,IAAI,EAAE,EAAE,CAAC;oBAChC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC;wBAAE,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;yBACzC,IAAI,UAAU,CAAC,CAAC,CAAC;wBAAE,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;;wBACvC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBACzB,CAAC;gBACD,IACE,CAAC,IAAI,CAAC,iBAAiB,IAAI,IAAI,CAAC;oBAChC,IAAI,KAAK,YAAY;oBACrB,SAAS,CAAC,MAAM,KAAK,CAAC,EACtB,CAAC;oBACD,MAAM,IAAI,GAAG,MAAM,SAAS,CAAU,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;oBACtD,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;oBAC9C,IAAI,MAAM,IAAI,MAAM;wBAAE,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAC9C,CAAC;YACH,CAAC;QACH,CAAC;QACD,IAAI,eAAe,GAAG,CAAC,CAAC;QACxB,MAAM,cAAc,GAAa,EAAE,CAAC;QACpC,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;YAC5B,KAAK,MAAM,QAAQ,IAAI,MAAM,gBAAgB,EAAE,EAAE,CAAC;gBAChD,MAAM,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;gBAC3D,MAAM,OAAO,GAAG,MAAM,SAAS,CAAiB,QAAQ,CAAC,CAAC;gBAC1D,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,mBAAmB,IAAI,IAAI,CAAC;oBAAE,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;gBACxF,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,MAAM,CAAC,CAAC,MAAM,CAAC;YAClE,CAAC;QACH,CAAC;QACD,OAAO;YACL,MAAM,EAAE,IAAI;YACZ,MAAM;YACN,aAAa,EAAE,IAAI;YACnB,YAAY;YACZ,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC3C,WAAW,EAAE;gBACX,YAAY;gBACZ,aAAa,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,MAAM,CAAC,CAAC,MAAM;gBAC1D,aAAa,EAAE,YAAY;gBAC3B,aAAa,EAAE,OAAO;gBACtB,aAAa,EAAE,CAAC,GAAG,aAAa,CAAC;gBACjC,YAAY,EAAE,CAAC,GAAG,YAAY,CAAC;gBAC/B,aAAa,EAAE,UAAU;gBACzB,QAAQ,EAAE,eAAe;gBACzB,cAAc;aACf;SACF,CAAC;IACJ,CAAC;IAED,yEAAyE;IACzE,qEAAqE;IACrE,8EAA8E;IAC9E,MAAM,iBAAiB,GAA2B,EAAE,CAAC;IACrD,IAAI,gBAAgB,GAAG,CAAC,CAAC;IACzB,IAAI,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;QACzB,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;YAC5B,MAAM,CAAC,GAAG,MAAM,YAAY,CAAU,QAAQ,CAAC,IAAI,CAAC,EAAE,eAAe,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC;YACnF,IAAI,CAAC,CAAC,OAAO,GAAG,CAAC,EAAE,CAAC;gBAClB,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC;gBACpC,gBAAgB,IAAI,CAAC,CAAC,OAAO,CAAC;YAChC,CAAC;QACH,CAAC;IACH,CAAC;IACD,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC;QACxC,CAAC,CAAC,MAAM,YAAY,CAAc,WAAW,EAAE,mBAAmB,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,MAAM,CAAC;QACzF,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;IAEzC,IAAI,YAAY,GAAG,CAAC,CAAC;IACrB,MAAM,cAAc,GAAa,EAAE,CAAC;IACpC,gFAAgF;IAChF,qEAAqE;IACrE,MAAM,oBAAoB,GAA2B,EAAE,CAAC;IACxD,IAAI,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;QACzB,KAAK,MAAM,KAAK,IAAI,MAAM,cAAc,EAAE,EAAE,CAAC;YAC3C,MAAM,EAAE,GAAG,KAAK,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;YACzC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;YAC7C,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,mBAAmB,IAAI,IAAI,CAAC,EAAE,CAAC;gBAC/D,qEAAqE;gBACrE,MAAM,CAAC,GAAG,MAAM,YAAY,CAAU,QAAQ,EAAE,gBAAgB,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC;gBACnF,YAAY,IAAI,CAAC,CAAC,OAAO,CAAC;gBAC1B,MAAM,UAAU,CAAC,QAAQ,CAAC,CAAC;gBAC3B,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;gBACxB,SAAS;YACX,CAAC;YACD,MAAM,CAAC,GAAG,MAAM,YAAY,CAAU,QAAQ,EAAE,gBAAgB,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,MAAM,CAAC,CAAC;YAC5F,YAAY,IAAI,CAAC,CAAC,OAAO,CAAC;YAC1B,oBAAoB,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC;QACvC,CAAC;IACH,CAAC;IAED,MAAM,eAAe,GAAG,MAAM,aAAa,CAAC;QAC1C,iBAAiB;QACjB,aAAa,EAAE,YAAY,CAAC,OAAO;QACnC,oBAAoB;KACrB,CAAC,CAAC;IAEH,yEAAyE;IACzE,uEAAuE;IACvE,qEAAqE;IACrE,yEAAyE;IACzE,2EAA2E;IAC3E,qDAAqD;IACrD,MAAM,aAAa,GAAG,IAAI,GAAG,EAAU,CAAC;IACxC,MAAM,YAAY,GAAG,IAAI,GAAG,EAAU,CAAC;IACvC,MAAM,aAAa,GAAa,EAAE,CAAC;IACnC,IAAI,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;QAC3B,MAAM,UAAU,CAAe,UAAU,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,EAAE;YACzD,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;gBACvC,IAAI,CAAC,CAAC,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC,CAAC,KAAK,CAAC;oBAAE,SAAS;gBAC7C,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE;oBACzC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;wBACxB,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;wBACrB,OAAO,KAAK,CAAC;oBACf,CAAC;oBACD,IAAI,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;wBAClB,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;wBACpB,OAAO,KAAK,CAAC;oBACf,CAAC;oBACD,OAAO,IAAI,CAAC;gBACd,CAAC,CAAC,CAAC;YACL,CAAC;YACD,OAAO,OAAO,CAAC;QACjB,CAAC,CAAC,CAAC;QAEH,IAAI,IAAI,CAAC,iBAAiB,IAAI,IAAI,EAAE,CAAC;YACnC,MAAM,KAAK,GAAG,MAAM,QAAQ,EAAE,CAAC;YAC/B,KAAK,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC9C,IAAI,IAAI,KAAK,YAAY,IAAI,CAAC,CAAC,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC,CAAC,GAAG,CAAC;oBAAE,SAAS;gBACpE,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;gBAC5B,MAAM,IAAI,GAAG,MAAM,SAAS,CAAU,IAAI,CAAC,CAAC;gBAC5C,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;gBAC9C,IAAI,MAAM,GAAG,MAAM;oBAAE,SAAS;gBAC9B,MAAM,YAAY,CAAU,IAAI,EAAE,eAAe,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC;gBACtE,MAAM,UAAU,CAAC,IAAI,CAAC,CAAC;gBACvB,MAAM,UAAU,CAAe,UAAU,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,EAAE;oBACzD,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC;oBACrB,OAAO,OAAO,CAAC;gBACjB,CAAC,CAAC,CAAC;gBACH,KAAK,MAAM,KAAK,IAAI,MAAM,eAAe,EAAE,EAAE,CAAC;oBAC5C,MAAM,UAAU,CAAS,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,EAAE;wBACrE,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC,IAAI,CAAC,KAAK,SAAS;4BAAE,OAAO,OAAO,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;wBAChF,OAAO,OAAO,CAAC;oBACjB,CAAC,CAAC,CAAC;gBACL,CAAC;gBACD,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC3B,CAAC;QACH,CAAC;IACH,CAAC;IAED,6EAA6E;IAC7E,0EAA0E;IAC1E,0DAA0D;IAC1D,IAAI,eAAe,GAAG,CAAC,CAAC;IACxB,MAAM,qBAAqB,GAAa,EAAE,CAAC;IAC3C,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5B,KAAK,MAAM,QAAQ,IAAI,MAAM,gBAAgB,EAAE,EAAE,CAAC;YAChD,MAAM,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;YAC3D,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,mBAAmB,IAAI,IAAI,CAAC,EAAE,CAAC;gBAC/D,MAAM,OAAO,GAAG,MAAM,SAAS,CAAiB,QAAQ,CAAC,CAAC;gBAC1D,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC;gBAClC,MAAM,UAAU,CAAC,QAAQ,CAAC,CAAC;gBAC3B,qBAAqB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;gBAC/B,SAAS;YACX,CAAC;YACD,MAAM,CAAC,GAAG,MAAM,YAAY,CAAiB,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,MAAM,CAAC,CAAC;YAC7E,eAAe,IAAI,CAAC,CAAC,OAAO,CAAC;QAC/B,CAAC;IACH,CAAC;IAED,6EAA6E;IAC7E,wEAAwE;IACxE,MAAM,YAAY,EAAE,CAAC;IAErB,OAAO;QACL,MAAM,EAAE,KAAK;QACb,MAAM;QACN,aAAa,EAAE,IAAI;QACnB,YAAY;QACZ,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3C,OAAO,EAAE;YACP,YAAY,EAAE,gBAAgB;YAC9B,aAAa,EAAE,YAAY,CAAC,OAAO;YACnC,aAAa,EAAE,YAAY;YAC3B,aAAa,EAAE,cAAc;YAC7B,aAAa,EAAE,CAAC,GAAG,aAAa,CAAC;YACjC,YAAY,EAAE,CAAC,GAAG,YAAY,CAAC;YAC/B,aAAa;YACb,QAAQ,EAAE,eAAe;YACzB,cAAc,EAAE,qBAAqB;SACtC;QACD,UAAU,EAAE,oFAAoF;QAChG,eAAe;KAChB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC"}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { adjustCursors } from "./admin.js";
|
|
2
|
+
import { ARCHIVE_STATUS_FILE, archiveJsonl, archiveRoomFile } from "../store.js";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { watch } from "node:fs";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { AGENTS_FILE, DEFAULT_ROOM, STATUS_FILE, appendJsonl, cursorFile, ensureRoom, fileSize, inboxFile, normalizeRoom, readJson, readJsonl, roomFile, stashHistory, retrieveHistory, updateJson, } from "../store.js";
|
|
7
|
+
import { sourceFile, getOffset, setOffset, MAX_WAIT_MS, } from "./shared.js";
|
|
8
|
+
// ---------- send_message ----------
|
|
9
|
+
export const sendMessageSchema = {
|
|
10
|
+
from: z.string().min(1),
|
|
11
|
+
to: z.string().optional(),
|
|
12
|
+
room: z.string().optional(),
|
|
13
|
+
text: z.string().min(1),
|
|
14
|
+
kind: z.enum(["decision", "status", "chatter"]).optional(),
|
|
15
|
+
};
|
|
16
|
+
export async function sendMessageTool(args) {
|
|
17
|
+
// DM → inbox. Otherwise resolve the channel (default `general`), make sure it
|
|
18
|
+
// exists in the registry, and tag the message with its channel.
|
|
19
|
+
if (args.to) {
|
|
20
|
+
const msg = {
|
|
21
|
+
id: randomUUID(),
|
|
22
|
+
ts: Date.now(),
|
|
23
|
+
from: args.from,
|
|
24
|
+
to: args.to,
|
|
25
|
+
text: args.text,
|
|
26
|
+
};
|
|
27
|
+
const target = inboxFile(args.to);
|
|
28
|
+
await appendJsonl(target, msg);
|
|
29
|
+
// Offline delivery is intentional (the inbox is created on demand), but a
|
|
30
|
+
// typo'd recipient shouldn't vanish silently — surface a warning when the
|
|
31
|
+
// target isn't a known agent so the caller can catch the mistake.
|
|
32
|
+
const reg = await readJson(AGENTS_FILE, {});
|
|
33
|
+
const warning = reg[args.to]
|
|
34
|
+
? undefined
|
|
35
|
+
: `recipient '${args.to}' is not a registered agent — message stored in their inbox but no one may be listening`;
|
|
36
|
+
return { ok: true, id: msg.id, target, room: undefined, warning };
|
|
37
|
+
}
|
|
38
|
+
const chan = normalizeRoom(args.room);
|
|
39
|
+
if (chan !== DEFAULT_ROOM)
|
|
40
|
+
await ensureRoom(chan, args.from);
|
|
41
|
+
const msg = {
|
|
42
|
+
id: randomUUID(),
|
|
43
|
+
ts: Date.now(),
|
|
44
|
+
from: args.from,
|
|
45
|
+
room: chan,
|
|
46
|
+
text: args.text,
|
|
47
|
+
...(args.kind ? { kind: args.kind } : {}),
|
|
48
|
+
};
|
|
49
|
+
const target = roomFile(chan);
|
|
50
|
+
await appendJsonl(target, msg);
|
|
51
|
+
await maybeCompactRoom(chan);
|
|
52
|
+
return { ok: true, id: msg.id, target, room: chan };
|
|
53
|
+
}
|
|
54
|
+
// ---------- live compaction (self-limiting streams) ----------
|
|
55
|
+
// Rooms and the status stream compact themselves on write: once a live file
|
|
56
|
+
// grows past its threshold, the oldest entries move to the archive (never
|
|
57
|
+
// deleted) and every cursor shifts down. Fresh decisions (< decisionDays old)
|
|
58
|
+
// are exempt — they stay in the live file. Cursor adjustment subtracts the
|
|
59
|
+
// full removed count, so an agent parked behind a kept decision may re-read
|
|
60
|
+
// it once; at-least-once beats silently skipping.
|
|
61
|
+
function envInt(name, fallback) {
|
|
62
|
+
const n = Number(process.env[name]);
|
|
63
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
|
|
64
|
+
}
|
|
65
|
+
const ROOM_COMPACT_THRESHOLD = envInt("AGENT_COORD_ROOM_MAX", 1000);
|
|
66
|
+
const ROOM_COMPACT_KEEP = envInt("AGENT_COORD_ROOM_KEEP", 500);
|
|
67
|
+
const STATUS_COMPACT_THRESHOLD = envInt("AGENT_COORD_STATUS_MAX", 2000);
|
|
68
|
+
const STATUS_COMPACT_KEEP = envInt("AGENT_COORD_STATUS_KEEP", 1000);
|
|
69
|
+
// Skip the entry count entirely while the file is small — the common case on
|
|
70
|
+
// every send. ~150 bytes/entry means the threshold can't be hit below this.
|
|
71
|
+
const COMPACT_SIZE_GATE = 100 * 1024;
|
|
72
|
+
const DECISION_FRESH_MS = 30 * 24 * 60 * 60 * 1000;
|
|
73
|
+
async function maybeCompactRoom(chan) {
|
|
74
|
+
const file = roomFile(chan);
|
|
75
|
+
if ((await fileSize(file)) < COMPACT_SIZE_GATE)
|
|
76
|
+
return;
|
|
77
|
+
const entries = await readJsonl(file);
|
|
78
|
+
if (entries.length <= ROOM_COMPACT_THRESHOLD)
|
|
79
|
+
return;
|
|
80
|
+
const boundaryTs = entries[entries.length - ROOM_COMPACT_KEEP].ts;
|
|
81
|
+
const decisionCutoff = Date.now() - DECISION_FRESH_MS;
|
|
82
|
+
const r = await archiveJsonl(file, archiveRoomFile(chan), (e) => e.ts >= boundaryTs || (e.kind === "decision" && e.ts > decisionCutoff));
|
|
83
|
+
if (r.removed > 0)
|
|
84
|
+
await adjustCursors({ roomRemovedByChan: { [chan]: r.removed } });
|
|
85
|
+
}
|
|
86
|
+
async function maybeCompactStatus() {
|
|
87
|
+
if ((await fileSize(STATUS_FILE)) < COMPACT_SIZE_GATE)
|
|
88
|
+
return;
|
|
89
|
+
const entries = await readJsonl(STATUS_FILE);
|
|
90
|
+
if (entries.length <= STATUS_COMPACT_THRESHOLD)
|
|
91
|
+
return;
|
|
92
|
+
const boundaryTs = entries[entries.length - STATUS_COMPACT_KEEP].ts;
|
|
93
|
+
const r = await archiveJsonl(STATUS_FILE, ARCHIVE_STATUS_FILE, (e) => e.ts >= boundaryTs);
|
|
94
|
+
if (r.removed > 0)
|
|
95
|
+
await adjustCursors({ statusRemoved: r.removed });
|
|
96
|
+
}
|
|
97
|
+
// ---------- read_messages ----------
|
|
98
|
+
export const readMessagesSchema = {
|
|
99
|
+
agentId: z.string().min(1),
|
|
100
|
+
source: z.enum(["inbox", "room", "status"]),
|
|
101
|
+
room: z.string().optional(),
|
|
102
|
+
limit: z.number().int().positive().max(500).optional(),
|
|
103
|
+
peek: z.boolean().optional(),
|
|
104
|
+
sinceTs: z.number().optional(),
|
|
105
|
+
};
|
|
106
|
+
export async function readMessagesTool(args) {
|
|
107
|
+
const file = sourceFile(args.source, args.agentId, args.room);
|
|
108
|
+
const all = await readJsonl(file);
|
|
109
|
+
let entries = [];
|
|
110
|
+
let totalNew = 0;
|
|
111
|
+
// Room and status reads default to 50 entries to prevent agents flooding
|
|
112
|
+
// themselves with full history on join — the status stream grows unbounded
|
|
113
|
+
// across the fleet. Inbox drains fully since it is targeted by nature.
|
|
114
|
+
const effectiveLimit = args.limit ?? (args.source === "inbox" ? undefined : 50);
|
|
115
|
+
if (args.peek) {
|
|
116
|
+
const cursor = await readJson(cursorFile(args.agentId), {});
|
|
117
|
+
const startOffset = getOffset(cursor, args.source, args.room);
|
|
118
|
+
entries = all.slice(startOffset);
|
|
119
|
+
if (args.sinceTs !== undefined)
|
|
120
|
+
entries = entries.filter((e) => e.ts > args.sinceTs);
|
|
121
|
+
totalNew = entries.length;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
await updateJson(cursorFile(args.agentId), {}, (current) => {
|
|
125
|
+
const startOffset = getOffset(current, args.source, args.room);
|
|
126
|
+
let e = all.slice(startOffset);
|
|
127
|
+
if (args.sinceTs !== undefined)
|
|
128
|
+
e = e.filter((x) => x.ts > args.sinceTs);
|
|
129
|
+
totalNew = e.length;
|
|
130
|
+
entries = e;
|
|
131
|
+
// Advance past EVERYTHING we account for here (recent window + any
|
|
132
|
+
// overflow we stash below). The overflow is recoverable via the history
|
|
133
|
+
// hash, so it must not requeue for the next read — that would re-flood.
|
|
134
|
+
if (e.length > 0)
|
|
135
|
+
setOffset(current, args.source, args.room, startOffset + e.length);
|
|
136
|
+
return current;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
// CCR overflow handling (room and status sources). When the backlog exceeds
|
|
140
|
+
// the window, return the RECENT slice raw and replace the older overflow with
|
|
141
|
+
// a compact digest carrying a retrieval hash. The agent expands it on demand
|
|
142
|
+
// via retrieve_room_history. Peek is side-effect-free, so it never stashes —
|
|
143
|
+
// it reports the count and tells the agent to do a real read to get a hash.
|
|
144
|
+
let recent = entries;
|
|
145
|
+
let history;
|
|
146
|
+
if (args.source !== "inbox" && effectiveLimit && entries.length > effectiveLimit) {
|
|
147
|
+
const overflow = entries.slice(0, entries.length - effectiveLimit);
|
|
148
|
+
recent = entries.slice(entries.length - effectiveLimit);
|
|
149
|
+
const stashKey = args.source === "room" ? normalizeRoom(args.room) : "status";
|
|
150
|
+
if (args.peek) {
|
|
151
|
+
history = { digest: digestOverflow(overflow, undefined), older: overflow.length };
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
const hash = await stashHistory(stashKey, args.agentId, overflow);
|
|
155
|
+
history = { digest: digestOverflow(overflow, hash), hash, older: overflow.length };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else if (effectiveLimit && entries.length > effectiveLimit) {
|
|
159
|
+
// Inbox keeps the legacy oldest-first chunking (no stash) — targeted
|
|
160
|
+
// messages must never be skipped over.
|
|
161
|
+
recent = entries.slice(0, effectiveLimit);
|
|
162
|
+
}
|
|
163
|
+
// Drop the agent's own posts on shared channels — reading your own broadcast
|
|
164
|
+
// back is never useful and confuses turn-based agents into self-replies.
|
|
165
|
+
// Cursor has already advanced past them, so they won't reappear.
|
|
166
|
+
const visible = args.source === "room" || args.source === "status"
|
|
167
|
+
? recent.filter((e) => entryAuthor(e) !== args.agentId)
|
|
168
|
+
: recent;
|
|
169
|
+
return {
|
|
170
|
+
ok: true,
|
|
171
|
+
messages: visible,
|
|
172
|
+
totalNew,
|
|
173
|
+
returned: visible.length,
|
|
174
|
+
room: args.source === "room" ? normalizeRoom(args.room) : undefined,
|
|
175
|
+
...(history ? { history } : {}),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
// Lossless summary of a stashed backlog slice: surfaces error/failure posts
|
|
179
|
+
// verbatim (the lines that usually matter most in a flood) and collapses the
|
|
180
|
+
// rest to counts. Mirrors headroom's content-aware digest, kept deliberately
|
|
181
|
+
// simple — the full originals are one retrieve_room_history call away.
|
|
182
|
+
function digestOverflow(over, hash) {
|
|
183
|
+
const authors = new Set(over.map(entryAuthor).filter(Boolean));
|
|
184
|
+
const errorRe = /\b(error|fatal|fail(ed|ure)?|panic|exception)\b/i;
|
|
185
|
+
const errors = over.filter((m) => errorRe.test(JSON.stringify(m)));
|
|
186
|
+
const first = over[0]?.ts;
|
|
187
|
+
const last = over[over.length - 1]?.ts;
|
|
188
|
+
const span = first && last && last > first ? ` over ${Math.round((last - first) / 60000)}m` : "";
|
|
189
|
+
const parts = [
|
|
190
|
+
`[${over.length} earlier message${over.length === 1 ? "" : "s"} compressed`,
|
|
191
|
+
`${authors.size} agent${authors.size === 1 ? "" : "s"}${span}`,
|
|
192
|
+
];
|
|
193
|
+
if (errors.length)
|
|
194
|
+
parts.push(`${errors.length} error post${errors.length === 1 ? "" : "s"}`);
|
|
195
|
+
const head = parts.join(", ");
|
|
196
|
+
const tail = hash
|
|
197
|
+
? ` hash=${hash}] — call retrieve_room_history(hash="${hash}") to expand`
|
|
198
|
+
: `] — read without peek to get an expandable hash`;
|
|
199
|
+
// Decisions are the one thing a digest must not bury — quote them verbatim
|
|
200
|
+
// (capped) below the summary line.
|
|
201
|
+
const decisions = over.filter((m) => m.kind === "decision");
|
|
202
|
+
const quoted = decisions
|
|
203
|
+
.slice(-5)
|
|
204
|
+
.map((d) => ` [decision] ${d.from}: ${d.text.length > 200 ? d.text.slice(0, 200) + "…" : d.text}`);
|
|
205
|
+
const decisionBlock = decisions.length
|
|
206
|
+
? `\n${quoted.join("\n")}${decisions.length > 5 ? `\n (+${decisions.length - 5} earlier decisions in hash)` : ""}`
|
|
207
|
+
: "";
|
|
208
|
+
return head + tail + decisionBlock;
|
|
209
|
+
}
|
|
210
|
+
// ---------- retrieve_room_history ----------
|
|
211
|
+
export const retrieveRoomHistorySchema = {
|
|
212
|
+
agentId: z.string().min(1),
|
|
213
|
+
hash: z.string().min(1),
|
|
214
|
+
query: z.string().optional(),
|
|
215
|
+
};
|
|
216
|
+
export async function retrieveRoomHistoryTool(args) {
|
|
217
|
+
const res = await retrieveHistory(args.hash, args.agentId, args.query);
|
|
218
|
+
if (!res.ok) {
|
|
219
|
+
const reason = res.reason === "expired"
|
|
220
|
+
? "That history entry has expired (30m TTL). Re-read the channel with a higher limit to fetch it again."
|
|
221
|
+
: res.reason === "forbidden"
|
|
222
|
+
? "That history hash was produced for a different agent and cannot be retrieved by you."
|
|
223
|
+
: "No history entry for that hash. It may have expired or never existed.";
|
|
224
|
+
return { ok: false, reason: res.reason, message: reason };
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
ok: true,
|
|
228
|
+
room: res.room,
|
|
229
|
+
hash: args.hash,
|
|
230
|
+
total: res.total,
|
|
231
|
+
returned: res.messages.length,
|
|
232
|
+
messages: res.messages,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function entryAuthor(e) {
|
|
236
|
+
return "from" in e ? e.from : e.agentId;
|
|
237
|
+
}
|
|
238
|
+
// ---------- post_status ----------
|
|
239
|
+
export const postStatusSchema = {
|
|
240
|
+
agentId: z.string().min(1),
|
|
241
|
+
status: z.string().min(1),
|
|
242
|
+
detail: z.string().optional(),
|
|
243
|
+
};
|
|
244
|
+
export async function postStatusTool(args) {
|
|
245
|
+
const entry = {
|
|
246
|
+
id: randomUUID(),
|
|
247
|
+
ts: Date.now(),
|
|
248
|
+
agentId: args.agentId,
|
|
249
|
+
status: args.status,
|
|
250
|
+
detail: args.detail,
|
|
251
|
+
};
|
|
252
|
+
await appendJsonl(STATUS_FILE, entry);
|
|
253
|
+
await maybeCompactStatus();
|
|
254
|
+
return { ok: true, id: entry.id };
|
|
255
|
+
}
|
|
256
|
+
// ---------- wait_for_message ----------
|
|
257
|
+
export const waitForMessageSchema = {
|
|
258
|
+
agentId: z.string().min(1),
|
|
259
|
+
source: z.enum(["inbox", "room", "status"]),
|
|
260
|
+
room: z.string().optional(),
|
|
261
|
+
timeoutMs: z.number().int().positive().max(MAX_WAIT_MS).optional(),
|
|
262
|
+
};
|
|
263
|
+
export async function waitForMessageTool(args) {
|
|
264
|
+
const totalTimeout = Math.min(args.timeoutMs ?? 30_000, MAX_WAIT_MS);
|
|
265
|
+
const file = sourceFile(args.source, args.agentId, args.room);
|
|
266
|
+
const deadline = Date.now() + totalTimeout;
|
|
267
|
+
// Loop so that file growth caused only by the agent's own self-posts (which
|
|
268
|
+
// readMessagesTool now filters out for room/status) doesn't return an empty
|
|
269
|
+
// result — keep waiting until we have something to deliver or time out.
|
|
270
|
+
while (Date.now() < deadline) {
|
|
271
|
+
const startSize = await fileSize(file);
|
|
272
|
+
const remaining = deadline - Date.now();
|
|
273
|
+
if (remaining <= 0)
|
|
274
|
+
break;
|
|
275
|
+
const changed = await new Promise((resolve) => {
|
|
276
|
+
let settled = false;
|
|
277
|
+
const finish = (v) => {
|
|
278
|
+
if (settled)
|
|
279
|
+
return;
|
|
280
|
+
settled = true;
|
|
281
|
+
clearInterval(poll);
|
|
282
|
+
try {
|
|
283
|
+
watcher?.close();
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
// ignore
|
|
287
|
+
}
|
|
288
|
+
clearTimeout(t);
|
|
289
|
+
resolve(v);
|
|
290
|
+
};
|
|
291
|
+
const check = async () => {
|
|
292
|
+
const sz = await fileSize(file);
|
|
293
|
+
if (sz > startSize)
|
|
294
|
+
finish(true);
|
|
295
|
+
};
|
|
296
|
+
let watcher;
|
|
297
|
+
try {
|
|
298
|
+
watcher = watch(file, () => {
|
|
299
|
+
void check();
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
// file may not exist; polling will handle
|
|
304
|
+
}
|
|
305
|
+
const poll = setInterval(() => void check(), 500);
|
|
306
|
+
const t = setTimeout(() => finish(false), remaining);
|
|
307
|
+
});
|
|
308
|
+
if (!changed)
|
|
309
|
+
break;
|
|
310
|
+
const result = await readMessagesTool({ agentId: args.agentId, source: args.source, room: args.room });
|
|
311
|
+
if (result.returned > 0)
|
|
312
|
+
return result;
|
|
313
|
+
// otherwise, only self-posts arrived; keep waiting on the remaining budget
|
|
314
|
+
}
|
|
315
|
+
return { ok: false, timedOut: true };
|
|
316
|
+
}
|
|
317
|
+
//# sourceMappingURL=messaging.js.map
|