@zooid/transport-matrix 0.7.0 → 0.7.2
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/dist/index.d.ts +78 -4
- package/dist/index.js +194 -14
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/bot-pool.test.ts +200 -13
- package/src/bot-pool.ts +73 -5
- package/src/event-encoders.test.ts +86 -0
- package/src/event-encoders.ts +29 -0
- package/src/index.ts +2 -2
- package/src/matrix-client.test.ts +160 -0
- package/src/matrix-client.ts +64 -0
- package/src/router.test.ts +3 -3
- package/src/router.ts +11 -2
- package/src/space-provisioner.test.ts +191 -1
- package/src/space-provisioner.ts +77 -7
- package/src/transport.test.ts +58 -2
- package/src/transport.ts +72 -4
- package/src/workforce-publisher.test.ts +12 -2
- package/src/workforce-publisher.ts +5 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TransportContextProvider, HistoryOptions, HistoryPage, ThreadOverviewPage, Member, ChannelInfo, AcpRegistry, ApprovalCorrelator } from '@zooid/core';
|
|
1
|
+
import { TransportContextProvider, HistoryOptions, HistoryPage, ThreadOverviewPage, Member, ChannelInfo, RoomBinding, AcpRegistry, ApprovalCorrelator } from '@zooid/core';
|
|
2
2
|
import * as hono_types from 'hono/types';
|
|
3
3
|
import { Hono } from 'hono';
|
|
4
4
|
|
|
@@ -53,6 +53,20 @@ declare class MatrixClient {
|
|
|
53
53
|
/** Optional `m.room.name`. When set, sent in the createRoom body so the
|
|
54
54
|
* room has a display name from the moment it exists. */
|
|
55
55
|
name?: string;
|
|
56
|
+
/** When set, the room is created with a `restricted` join rule whose allow
|
|
57
|
+
* condition references this space room ID — i.e. joinable by space members
|
|
58
|
+
* only, rather than the whole homeserver. */
|
|
59
|
+
restrictedToSpaceId?: string;
|
|
60
|
+
/** Explicit room version. Restricted join rules require v8+; omit to use the
|
|
61
|
+
* homeserver default (modern Tuwunel defaults to v10/v11). */
|
|
62
|
+
roomVersion?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Seeds `m.room.power_levels.users` at creation via
|
|
65
|
+
* `power_level_content_override.users`. The caller owns the full map —
|
|
66
|
+
* typically the AS bot at 100, plus operator and any agents with
|
|
67
|
+
* declared PLs. Empty/absent → no override (the preset's defaults apply).
|
|
68
|
+
*/
|
|
69
|
+
userPowerLevels?: Record<string, number>;
|
|
56
70
|
}): Promise<string>;
|
|
57
71
|
createRoomRaw(opts: {
|
|
58
72
|
asUserId: string;
|
|
@@ -67,6 +81,17 @@ declare class MatrixClient {
|
|
|
67
81
|
}): Promise<{
|
|
68
82
|
event_id: string;
|
|
69
83
|
}>;
|
|
84
|
+
/**
|
|
85
|
+
* Invite a user to a room. Sent as the inviter (`asUserId`) — that user
|
|
86
|
+
* needs invite power in the room. Tolerates the "already in room /
|
|
87
|
+
* already invited" responses idempotently so bootstrap can run on a
|
|
88
|
+
* fresh AND a populated homeserver without branching.
|
|
89
|
+
*/
|
|
90
|
+
invite(opts: {
|
|
91
|
+
roomId: string;
|
|
92
|
+
asUserId: string;
|
|
93
|
+
targetUserId: string;
|
|
94
|
+
}): Promise<void>;
|
|
70
95
|
joinRoom(roomIdOrAlias: string, asUserId: string): Promise<void>;
|
|
71
96
|
sendMessage(input: SendMessageInput): Promise<{
|
|
72
97
|
event_id: string;
|
|
@@ -188,7 +213,13 @@ interface AgentBinding {
|
|
|
188
213
|
userId: string;
|
|
189
214
|
/** Optional human-readable display name. Falls back to the user_id localpart. */
|
|
190
215
|
displayName?: string;
|
|
191
|
-
|
|
216
|
+
/**
|
|
217
|
+
* Rooms this agent is bound to. Each entry's `alias` starts out as the
|
|
218
|
+
* configured `#alias` (or `!id`) and is rewritten to the canonical room
|
|
219
|
+
* ID by `BotPool.bootstrap`. Optional `powerLevel` is seeded into the
|
|
220
|
+
* room's `m.room.power_levels.users` at room creation only.
|
|
221
|
+
*/
|
|
222
|
+
rooms: RoomBinding[];
|
|
192
223
|
trigger: 'mention' | 'any';
|
|
193
224
|
}
|
|
194
225
|
interface ThreadState {
|
|
@@ -219,11 +250,17 @@ interface BootstrapOpts {
|
|
|
219
250
|
spaceRoomId?: string;
|
|
220
251
|
/** AS bot user ID. Required when spaceRoomId is set; sender of the m.space.child write. */
|
|
221
252
|
asUserId?: string;
|
|
253
|
+
/**
|
|
254
|
+
* Operator MXIDs seeded at PL 100 in every agent room this pool creates.
|
|
255
|
+
* Applied via `power_level_content_override.users` at room creation only —
|
|
256
|
+
* never reconciled. Empty/absent = no operator entries.
|
|
257
|
+
*/
|
|
258
|
+
adminUserIds?: string[];
|
|
222
259
|
}
|
|
223
260
|
declare class BotPool {
|
|
224
261
|
private readonly client;
|
|
225
262
|
private readonly agents;
|
|
226
|
-
constructor(client: Pick<MatrixClient, 'registerBot' | 'joinRoom' | 'resolveAlias' | 'createRoom' | 'sendStateEvent' | 'setDisplayName'>, agents: AgentBinding[]);
|
|
263
|
+
constructor(client: Pick<MatrixClient, 'registerBot' | 'invite' | 'joinRoom' | 'resolveAlias' | 'createRoom' | 'sendStateEvent' | 'setDisplayName'>, agents: AgentBinding[]);
|
|
227
264
|
bootstrap(opts?: BootstrapOpts): Promise<void>;
|
|
228
265
|
findByUserId(userId: string): AgentBinding | undefined;
|
|
229
266
|
findByName(name: string): AgentBinding | undefined;
|
|
@@ -237,12 +274,19 @@ interface CreateMatrixTransportOptions {
|
|
|
237
274
|
hsToken: string;
|
|
238
275
|
/** Admin Matrix user ID. When set, BotPool.bootstrap invites this user into rooms it creates. */
|
|
239
276
|
adminUserId?: string;
|
|
277
|
+
/** Post-turn drain: keep collecting trailing `agent_message_chunk`s until the
|
|
278
|
+
* buffer is quiet for this long before flushing. Defaults to `DRAIN_QUIET_MS`.
|
|
279
|
+
* Set to 0 to disable the drain (e.g. in tests). */
|
|
280
|
+
drainQuietMs?: number;
|
|
281
|
+
/** Hard cap on the post-turn drain. Defaults to `DRAIN_MAX_MS`. */
|
|
282
|
+
drainMaxMs?: number;
|
|
240
283
|
}
|
|
241
284
|
declare function createMatrixTransport(opts: CreateMatrixTransportOptions): {
|
|
242
285
|
app: Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
|
|
243
286
|
bootstrap: (bootstrapOpts?: {
|
|
244
287
|
spaceRoomId?: string;
|
|
245
288
|
asUserId?: string;
|
|
289
|
+
adminUserIds?: string[];
|
|
246
290
|
}) => Promise<void>;
|
|
247
291
|
pool: BotPool;
|
|
248
292
|
};
|
|
@@ -253,8 +297,38 @@ interface EnsureSpaceOpts {
|
|
|
253
297
|
serverName: string;
|
|
254
298
|
spaceLocalpart: string;
|
|
255
299
|
preset: 'public_chat' | 'private_chat';
|
|
300
|
+
/**
|
|
301
|
+
* Operator MXIDs to seed at PL 100 in the space's `m.room.power_levels`
|
|
302
|
+
* at creation. The AS bot is always included. Empty/absent → no override
|
|
303
|
+
* (the preset's PL defaults apply). Only consulted on first creation —
|
|
304
|
+
* if the alias already resolves we return the existing room untouched.
|
|
305
|
+
*/
|
|
306
|
+
admins?: string[];
|
|
256
307
|
}
|
|
257
308
|
declare function ensureWorkforceSpace(opts: EnsureSpaceOpts): Promise<string>;
|
|
309
|
+
interface EnsureDefaultChannelOpts {
|
|
310
|
+
client: MatrixClient;
|
|
311
|
+
asUserId: string;
|
|
312
|
+
serverName: string;
|
|
313
|
+
spaceId: string;
|
|
314
|
+
/** Localpart of the default channel; defaults to `general`. */
|
|
315
|
+
channelLocalpart?: string;
|
|
316
|
+
/**
|
|
317
|
+
* Operator MXIDs to seed at PL 100 in the channel's `m.room.power_levels`
|
|
318
|
+
* at creation. The AS bot is always included. Empty/absent → no override
|
|
319
|
+
* (the preset's PL defaults apply). Only consulted on first creation —
|
|
320
|
+
* if the alias already resolves we return the existing room untouched.
|
|
321
|
+
*/
|
|
322
|
+
admins?: string[];
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Ensure a space has a default channel (`#general` by default), restricted to
|
|
326
|
+
* the space's members and attached as an `m.space.child`. Idempotent: returns
|
|
327
|
+
* the existing room if the alias already resolves. Has no agent — it's the
|
|
328
|
+
* human landing room, so it is created here at provisioning time rather than
|
|
329
|
+
* via the agent-room path.
|
|
330
|
+
*/
|
|
331
|
+
declare function ensureDefaultChannel(opts: EnsureDefaultChannelOpts): Promise<string>;
|
|
258
332
|
declare function serverNameFromMxid(mxid: string): string;
|
|
259
333
|
|
|
260
334
|
interface WorkforceRoster {
|
|
@@ -285,4 +359,4 @@ interface StartOpts {
|
|
|
285
359
|
}
|
|
286
360
|
declare function startWorkforcePublisher(opts: StartOpts): Promise<PublisherHandle>;
|
|
287
361
|
|
|
288
|
-
export { type AgentBinding, BotPool, type CreateMatrixTransportOptions, type EnsureSpaceOpts, MatrixClient, type MatrixClientOptions, MatrixContextProvider, type MatrixContextProviderOpts, type MatrixTransportConfig, type MaybeMessage, type PublishOpts, type PublisherHandle, type RouteMatch, type SendCustomEventInput, type SendMessageInput, type StartOpts as StartWorkforcePublisherOpts, type WorkforceRoster, buildWorkforceRoster, createMatrixTransport, ensureWorkforceSpace, extractMentions, publishWorkforce, renderRegistration, route, serverNameFromMxid, startWorkforcePublisher };
|
|
362
|
+
export { type AgentBinding, BotPool, type CreateMatrixTransportOptions, type EnsureDefaultChannelOpts, type EnsureSpaceOpts, MatrixClient, type MatrixClientOptions, MatrixContextProvider, type MatrixContextProviderOpts, type MatrixTransportConfig, type MaybeMessage, type PublishOpts, type PublisherHandle, type RouteMatch, type SendCustomEventInput, type SendMessageInput, type StartOpts as StartWorkforcePublisherOpts, type WorkforceRoster, buildWorkforceRoster, createMatrixTransport, ensureDefaultChannel, ensureWorkforceSpace, extractMentions, publishWorkforce, renderRegistration, route, serverNameFromMxid, startWorkforcePublisher };
|
package/dist/index.js
CHANGED
|
@@ -39,6 +39,22 @@ var MatrixClient = class {
|
|
|
39
39
|
preset: opts.preset ?? "public_chat"
|
|
40
40
|
};
|
|
41
41
|
if (opts.name !== void 0) body.name = opts.name;
|
|
42
|
+
if (opts.roomVersion !== void 0) body.room_version = opts.roomVersion;
|
|
43
|
+
if (opts.restrictedToSpaceId !== void 0) {
|
|
44
|
+
body.initial_state = [
|
|
45
|
+
{
|
|
46
|
+
type: "m.room.join_rules",
|
|
47
|
+
state_key: "",
|
|
48
|
+
content: {
|
|
49
|
+
join_rule: "restricted",
|
|
50
|
+
allow: [{ type: "m.room_membership", room_id: opts.restrictedToSpaceId }]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
if (opts.userPowerLevels && Object.keys(opts.userPowerLevels).length > 0) {
|
|
56
|
+
body.power_level_content_override = { users: opts.userPowerLevels };
|
|
57
|
+
}
|
|
42
58
|
const r = await this.fetch(
|
|
43
59
|
`${this.homeserver}/_matrix/client/v3/createRoom?user_id=${encodeURIComponent(opts.senderUserId)}`,
|
|
44
60
|
{
|
|
@@ -84,6 +100,30 @@ var MatrixClient = class {
|
|
|
84
100
|
if (!r.ok) throw new Error(`sendStateEvent ${opts.eventType} failed: ${r.status}`);
|
|
85
101
|
return await r.json();
|
|
86
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Invite a user to a room. Sent as the inviter (`asUserId`) — that user
|
|
105
|
+
* needs invite power in the room. Tolerates the "already in room /
|
|
106
|
+
* already invited" responses idempotently so bootstrap can run on a
|
|
107
|
+
* fresh AND a populated homeserver without branching.
|
|
108
|
+
*/
|
|
109
|
+
async invite(opts) {
|
|
110
|
+
const url = `${this.homeserver}/_matrix/client/v3/rooms/${encodeURIComponent(opts.roomId)}/invite?user_id=${encodeURIComponent(opts.asUserId)}`;
|
|
111
|
+
const r = await this.fetch(url, {
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: {
|
|
114
|
+
Authorization: `Bearer ${this.asToken}`,
|
|
115
|
+
"content-type": "application/json"
|
|
116
|
+
},
|
|
117
|
+
body: JSON.stringify({ user_id: opts.targetUserId })
|
|
118
|
+
});
|
|
119
|
+
if (r.ok) return;
|
|
120
|
+
if (r.status === 403) {
|
|
121
|
+
const body = await r.text();
|
|
122
|
+
if (/already (in the room|invited|a member|joined)/i.test(body)) return;
|
|
123
|
+
throw new Error(`invite(${opts.targetUserId}) failed: 403 ${body}`);
|
|
124
|
+
}
|
|
125
|
+
throw new Error(`invite(${opts.targetUserId}) failed: ${r.status}`);
|
|
126
|
+
}
|
|
87
127
|
async joinRoom(roomIdOrAlias, asUserId) {
|
|
88
128
|
const url = `${this.homeserver}/_matrix/client/v3/join/${encodeURIComponent(roomIdOrAlias)}?user_id=${encodeURIComponent(asUserId)}`;
|
|
89
129
|
const r = await this.fetch(url, {
|
|
@@ -412,7 +452,7 @@ function route(event, agents, threadStates) {
|
|
|
412
452
|
const threadState = threadRoot ? threadStates?.get(threadRoot) : void 0;
|
|
413
453
|
for (const a of agents) {
|
|
414
454
|
if (event.sender === a.userId) continue;
|
|
415
|
-
if (!a.rooms.
|
|
455
|
+
if (!a.rooms.some((r) => r.alias === event.room_id)) continue;
|
|
416
456
|
if (a.trigger === "any") {
|
|
417
457
|
matches.push(a);
|
|
418
458
|
continue;
|
|
@@ -439,15 +479,50 @@ async function ensureWorkforceSpace(opts) {
|
|
|
439
479
|
const existing = await opts.client.resolveAlias(alias);
|
|
440
480
|
if (existing) return existing;
|
|
441
481
|
const display = opts.spaceLocalpart.charAt(0).toUpperCase() + opts.spaceLocalpart.slice(1);
|
|
442
|
-
|
|
482
|
+
const body = {
|
|
483
|
+
room_alias_name: opts.spaceLocalpart,
|
|
484
|
+
name: display,
|
|
485
|
+
preset: opts.preset,
|
|
486
|
+
creation_content: { type: "m.space" },
|
|
487
|
+
// A workspace is joined by invitation, not self-service. Pin the space's
|
|
488
|
+
// join rule to invite regardless of preset so it can't be walked into
|
|
489
|
+
// (which would otherwise satisfy every restricted child room's allow).
|
|
490
|
+
initial_state: [{ type: "m.room.join_rules", state_key: "", content: { join_rule: "invite" } }]
|
|
491
|
+
};
|
|
492
|
+
if (opts.admins && opts.admins.length > 0) {
|
|
493
|
+
body.invite = opts.admins;
|
|
494
|
+
const users = { [opts.asUserId]: 100 };
|
|
495
|
+
for (const a of opts.admins) users[a] = 100;
|
|
496
|
+
body.power_level_content_override = { users };
|
|
497
|
+
}
|
|
498
|
+
return opts.client.createRoomRaw({ asUserId: opts.asUserId, body });
|
|
499
|
+
}
|
|
500
|
+
async function ensureDefaultChannel(opts) {
|
|
501
|
+
const localpart2 = opts.channelLocalpart ?? "general";
|
|
502
|
+
const alias = `#${localpart2}:${opts.serverName}`;
|
|
503
|
+
const existing = await opts.client.resolveAlias(alias);
|
|
504
|
+
if (existing) return existing;
|
|
505
|
+
let userPowerLevels;
|
|
506
|
+
if (opts.admins && opts.admins.length > 0) {
|
|
507
|
+
userPowerLevels = { [opts.asUserId]: 100 };
|
|
508
|
+
for (const a of opts.admins) userPowerLevels[a] = 100;
|
|
509
|
+
}
|
|
510
|
+
const roomId = await opts.client.createRoom({
|
|
511
|
+
roomAliasName: localpart2,
|
|
512
|
+
invite: [],
|
|
513
|
+
senderUserId: opts.asUserId,
|
|
514
|
+
name: localpart2.charAt(0).toUpperCase() + localpart2.slice(1),
|
|
515
|
+
restrictedToSpaceId: opts.spaceId,
|
|
516
|
+
...userPowerLevels ? { userPowerLevels } : {}
|
|
517
|
+
});
|
|
518
|
+
await opts.client.sendStateEvent({
|
|
519
|
+
roomId: opts.spaceId,
|
|
443
520
|
asUserId: opts.asUserId,
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
preset: opts.preset,
|
|
448
|
-
creation_content: { type: "m.space" }
|
|
449
|
-
}
|
|
521
|
+
eventType: "m.space.child",
|
|
522
|
+
stateKey: roomId,
|
|
523
|
+
content: { via: [opts.serverName] }
|
|
450
524
|
});
|
|
525
|
+
return roomId;
|
|
451
526
|
}
|
|
452
527
|
function serverNameFromMxid(mxid) {
|
|
453
528
|
const colon = mxid.indexOf(":");
|
|
@@ -478,8 +553,23 @@ var BotPool = class {
|
|
|
478
553
|
} catch (err) {
|
|
479
554
|
console.warn(`[matrix] setDisplayName(${a.userId}) failed: ${err.message}`);
|
|
480
555
|
}
|
|
556
|
+
if (opts.spaceRoomId && opts.asUserId) {
|
|
557
|
+
try {
|
|
558
|
+
await this.client.invite({
|
|
559
|
+
roomId: opts.spaceRoomId,
|
|
560
|
+
asUserId: opts.asUserId,
|
|
561
|
+
targetUserId: a.userId
|
|
562
|
+
});
|
|
563
|
+
await this.client.joinRoom(opts.spaceRoomId, a.userId);
|
|
564
|
+
} catch (err) {
|
|
565
|
+
console.warn(
|
|
566
|
+
`[matrix] space membership for ${a.userId} failed: ${err.message}`
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
481
570
|
for (let i = 0; i < a.rooms.length; i++) {
|
|
482
|
-
const
|
|
571
|
+
const binding = a.rooms[i];
|
|
572
|
+
const room = binding.alias;
|
|
483
573
|
try {
|
|
484
574
|
let resolved = room;
|
|
485
575
|
if (room.startsWith("#")) {
|
|
@@ -494,17 +584,25 @@ var BotPool = class {
|
|
|
494
584
|
const colon = room.indexOf(":");
|
|
495
585
|
const aliasLocalpart = colon > 1 ? room.slice(1, colon) : room.slice(1);
|
|
496
586
|
const sender = opts.adminUserId ?? a.userId;
|
|
587
|
+
const userPowerLevels = buildUserPowerLevels(
|
|
588
|
+
opts.asUserId,
|
|
589
|
+
opts.adminUserIds,
|
|
590
|
+
this.agents,
|
|
591
|
+
room
|
|
592
|
+
);
|
|
497
593
|
resolved = await this.client.createRoom({
|
|
498
594
|
roomAliasName: aliasLocalpart,
|
|
499
595
|
invite: opts.adminUserId ? [opts.adminUserId] : [],
|
|
500
596
|
senderUserId: sender,
|
|
501
|
-
name: aliasLocalpart
|
|
597
|
+
name: aliasLocalpart,
|
|
598
|
+
...opts.spaceRoomId ? { restrictedToSpaceId: opts.spaceRoomId } : {},
|
|
599
|
+
...userPowerLevels ? { userPowerLevels } : {}
|
|
502
600
|
});
|
|
503
601
|
}
|
|
504
602
|
aliasToId.set(room, resolved);
|
|
505
603
|
}
|
|
506
604
|
}
|
|
507
|
-
|
|
605
|
+
binding.alias = resolved;
|
|
508
606
|
await this.client.joinRoom(resolved, a.userId);
|
|
509
607
|
if (opts.spaceRoomId && opts.asUserId && !attachedToSpace.has(resolved)) {
|
|
510
608
|
attachedToSpace.add(resolved);
|
|
@@ -543,6 +641,18 @@ function localpart(userId) {
|
|
|
543
641
|
if (!m) throw new Error(`bad user id: ${userId}`);
|
|
544
642
|
return m[1];
|
|
545
643
|
}
|
|
644
|
+
function buildUserPowerLevels(asUserId, admins, agents, roomAlias) {
|
|
645
|
+
const users = {};
|
|
646
|
+
if (asUserId) users[asUserId] = 100;
|
|
647
|
+
if (admins) for (const a of admins) users[a] = 100;
|
|
648
|
+
for (const a of agents) {
|
|
649
|
+
for (const r of a.rooms) {
|
|
650
|
+
if (r.alias !== roomAlias) continue;
|
|
651
|
+
if (r.powerLevel !== void 0) users[a.userId] = r.powerLevel;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return Object.keys(users).length > 0 ? users : void 0;
|
|
655
|
+
}
|
|
546
656
|
|
|
547
657
|
// src/transport.ts
|
|
548
658
|
import { Hono } from "hono";
|
|
@@ -596,6 +706,33 @@ function toPlanBody(evt) {
|
|
|
596
706
|
entries: evt.entries
|
|
597
707
|
};
|
|
598
708
|
}
|
|
709
|
+
var RECOVERY_URLS = {
|
|
710
|
+
auth_missing: "https://zooid.dev/docs/guides/run-in-container#authentication-that-carries-over",
|
|
711
|
+
auth_invalid: "https://zooid.dev/docs/guides/run-in-container#authentication-that-carries-over",
|
|
712
|
+
mount_failed: "https://zooid.dev/docs/guides/run-in-container#what-you-get-for-free",
|
|
713
|
+
image_pull_failed: "https://zooid.dev/docs/guides/run-in-container#skipping-the-image-prepull"
|
|
714
|
+
};
|
|
715
|
+
function toErrorBody(evt, threadRoot) {
|
|
716
|
+
const msg = evt.message.slice(0, 250);
|
|
717
|
+
const out = {
|
|
718
|
+
msgtype: "m.notice",
|
|
719
|
+
body: `\u26A0 [${evt.code}] ${msg}`,
|
|
720
|
+
code: evt.code,
|
|
721
|
+
message: msg,
|
|
722
|
+
transient: evt.transient,
|
|
723
|
+
"m.relates_to": { rel_type: "m.thread", event_id: threadRoot }
|
|
724
|
+
};
|
|
725
|
+
if (evt.sessionId) out.session_id = evt.sessionId;
|
|
726
|
+
if (evt.turnId) out.turn_id = evt.turnId;
|
|
727
|
+
if (evt.detail) out.detail = evt.detail.slice(0, 2e3);
|
|
728
|
+
if (evt.acp_error) out.acp_error = evt.acp_error;
|
|
729
|
+
const recovery = RECOVERY_URLS[evt.code];
|
|
730
|
+
if (recovery) out.recovery = recovery;
|
|
731
|
+
return out;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// src/transport.ts
|
|
735
|
+
import { classify } from "@zooid/acp-client";
|
|
599
736
|
|
|
600
737
|
// src/markdown-to-matrix-html.ts
|
|
601
738
|
import { marked } from "marked";
|
|
@@ -669,12 +806,17 @@ function toMatrixHtml(markdown) {
|
|
|
669
806
|
// src/transport.ts
|
|
670
807
|
var STARTUP_GRACE_MS = 5e3;
|
|
671
808
|
var SEEN_EVENT_CAP = 5e3;
|
|
809
|
+
var DRAIN_QUIET_MS = 300;
|
|
810
|
+
var DRAIN_MAX_MS = 3e3;
|
|
811
|
+
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
672
812
|
function inboundThreadRoot2(evt) {
|
|
673
813
|
const r = evt.content?.["m.relates_to"];
|
|
674
814
|
return r?.rel_type === "m.thread" && r.event_id ? r.event_id : void 0;
|
|
675
815
|
}
|
|
676
816
|
function createMatrixTransport(opts) {
|
|
677
817
|
const { agents, approvals, client, bindings, hsToken, adminUserId } = opts;
|
|
818
|
+
const drainQuietMs = opts.drainQuietMs ?? DRAIN_QUIET_MS;
|
|
819
|
+
const drainMaxMs = opts.drainMaxMs ?? DRAIN_MAX_MS;
|
|
678
820
|
const pool = new BotPool(client, bindings);
|
|
679
821
|
const sessions = /* @__PURE__ */ new Map();
|
|
680
822
|
const buffers = /* @__PURE__ */ new Map();
|
|
@@ -691,7 +833,9 @@ function createMatrixTransport(opts) {
|
|
|
691
833
|
if (event.type === "agent_message_chunk") {
|
|
692
834
|
const block = event.content;
|
|
693
835
|
if (block.type === "text" && typeof block.text === "string") {
|
|
694
|
-
|
|
836
|
+
const current = buffers.get(event.sessionId) ?? "";
|
|
837
|
+
const prefix = block.text === "" && current.length > 0 ? "\n\n" : "";
|
|
838
|
+
buffers.set(event.sessionId, current + prefix + block.text);
|
|
695
839
|
} else {
|
|
696
840
|
console.warn(`[matrix:${name}] dropped chunk block type=${block.type}`, block);
|
|
697
841
|
}
|
|
@@ -879,6 +1023,29 @@ function createMatrixTransport(opts) {
|
|
|
879
1023
|
if (st.participants.at(-1) !== a.name) st.participants.push(a.name);
|
|
880
1024
|
}).catch((err) => {
|
|
881
1025
|
console.error(`[matrix] runTurn failed for ${a.name}:`, err);
|
|
1026
|
+
const c2 = classify(err);
|
|
1027
|
+
const threadRoot = inboundThreadRoot2(evt) ?? evt.event_id;
|
|
1028
|
+
if (!threadRoot || !evt.room_id) return;
|
|
1029
|
+
const body2 = toErrorBody(
|
|
1030
|
+
{
|
|
1031
|
+
kind: "error",
|
|
1032
|
+
agentId: a.name,
|
|
1033
|
+
sessionId: null,
|
|
1034
|
+
turnId: null,
|
|
1035
|
+
code: c2.code,
|
|
1036
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1037
|
+
detail: err instanceof Error && err.stack ? err.stack.slice(0, 2e3) : void 0,
|
|
1038
|
+
transient: c2.transient,
|
|
1039
|
+
acp_error: c2.acp_error
|
|
1040
|
+
},
|
|
1041
|
+
threadRoot
|
|
1042
|
+
);
|
|
1043
|
+
void client.sendCustomEvent({
|
|
1044
|
+
roomId: evt.room_id,
|
|
1045
|
+
asUserId: a.userId,
|
|
1046
|
+
eventType: "eco.zoon.error",
|
|
1047
|
+
content: body2
|
|
1048
|
+
}).catch((e) => console.warn(`[matrix:${a.name}] eco.zoon.error send failed:`, e));
|
|
882
1049
|
});
|
|
883
1050
|
}
|
|
884
1051
|
}
|
|
@@ -931,6 +1098,14 @@ function createMatrixTransport(opts) {
|
|
|
931
1098
|
channelId: evt.room_id,
|
|
932
1099
|
content: [{ type: "text", text: promptText }]
|
|
933
1100
|
});
|
|
1101
|
+
const drainStart = Date.now();
|
|
1102
|
+
let drained = buffers.get(sessionId) ?? "";
|
|
1103
|
+
while (drainQuietMs > 0 && Date.now() - drainStart < drainMaxMs) {
|
|
1104
|
+
await delay(drainQuietMs);
|
|
1105
|
+
const next = buffers.get(sessionId) ?? "";
|
|
1106
|
+
if (next === drained) break;
|
|
1107
|
+
drained = next;
|
|
1108
|
+
}
|
|
934
1109
|
const text = buffers.get(sessionId) ?? "";
|
|
935
1110
|
if (text.length > 0) {
|
|
936
1111
|
const html = toMatrixHtml(text);
|
|
@@ -982,7 +1157,7 @@ function createMatrixTransport(opts) {
|
|
|
982
1157
|
}
|
|
983
1158
|
async function rebuildThreadState(client, roomId, rootEventId, bindings) {
|
|
984
1159
|
const state = { participants: [], rootMentions: [] };
|
|
985
|
-
const asUser = (bindings.find((b) => b.rooms.
|
|
1160
|
+
const asUser = (bindings.find((b) => b.rooms.some((r) => r.alias === roomId)) ?? bindings[0])?.userId;
|
|
986
1161
|
if (!asUser) return state;
|
|
987
1162
|
const root = await client.fetchEvent(roomId, rootEventId, asUser);
|
|
988
1163
|
if (root) {
|
|
@@ -1037,7 +1212,11 @@ function truncate(s, n) {
|
|
|
1037
1212
|
function buildWorkforceRoster(agents) {
|
|
1038
1213
|
return {
|
|
1039
1214
|
version: 1,
|
|
1040
|
-
agents: agents.map((a) => ({
|
|
1215
|
+
agents: agents.map((a) => ({
|
|
1216
|
+
user_id: a.userId,
|
|
1217
|
+
name: a.name,
|
|
1218
|
+
rooms: a.rooms.map((r) => r.alias)
|
|
1219
|
+
}))
|
|
1041
1220
|
};
|
|
1042
1221
|
}
|
|
1043
1222
|
async function publishWorkforce(opts) {
|
|
@@ -1065,6 +1244,7 @@ export {
|
|
|
1065
1244
|
MatrixContextProvider,
|
|
1066
1245
|
buildWorkforceRoster,
|
|
1067
1246
|
createMatrixTransport,
|
|
1247
|
+
ensureDefaultChannel,
|
|
1068
1248
|
ensureWorkforceSpace,
|
|
1069
1249
|
extractMentions,
|
|
1070
1250
|
publishWorkforce,
|