@openmarket/rooms-client 0.4.0 → 0.4.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.
@@ -96,7 +96,7 @@ export function buildMergeProposalBootstrap(opts) {
96
96
  /** The librarian bootstrap for `/brief refresh`: the agent rewrites the
97
97
  * room's brief doc itself, in the canonical structure renderBrief pins. */
98
98
  export function buildBriefRefreshBootstrap(opts) {
99
- return lines(opts.personaLine, `You are the LIBRARIAN for ${opts.label}. Maintain its brief: a concise, current snapshot.`, "1. Read the recent conversation with room_history for this room.", `2. Read the existing brief if any: doc_read spaceId ${opts.spaceId}, path ${opts.path}.`, `3. List the docs in play with doc_list (spaceId ${opts.spaceId}).`, `4. Write the brief with doc_write (spaceId ${opts.spaceId}, path ${opts.path}, a note describing what changed), following EXACTLY this structure:`, "", `# Brief: ${opts.label}`, "", `> Auto-generated by the room agent, read-only. Updated ${opts.nowLabel}.`, "", "## Summary", "<one or two short paragraphs on what this room is about and where it stands>", "", "## Open threads", "- <unresolved questions, or `None.`>", "", "## Decisions", "- <decisions reached, or `None yet.`>", "", "## Docs in play", "- `path` (rN, when) for each referenced doc, or `None referenced.`", "", "Keep it tight and factual. Do NOT output a DRAFT line. After writing, reply with one line naming the revision you wrote.");
99
+ return lines(opts.personaLine, `You are the LIBRARIAN for ${opts.label}. Maintain its brief: a concise, current snapshot.`, "1. Read the recent conversation with room_history for this room.", `2. Read the existing brief if any: doc_read spaceId ${opts.spaceId}, path ${opts.path}.`, `3. List the docs in play with doc_list (spaceId ${opts.spaceId}).`, `4. Write the brief with doc_write (spaceId ${opts.spaceId}, path ${opts.path}, a note describing what changed), following EXACTLY this structure:`, "", `# Brief: ${opts.label}`, "", `> Auto-generated by the room agent, read-only. Updated ${opts.nowLabel}.`, "", "## Summary", "<one or two short paragraphs on what this room is about and where it stands>", "", "## Open questions", "- <unresolved questions, or `None.`>", "", "## Decisions", "- <decisions reached, or `None yet.`>", "", "## Docs in play", "- `path` (rN, when) for each referenced doc, or `None referenced.`", "", "Keep it tight and factual. Do NOT output a DRAFT line. After writing, reply with one line naming the revision you wrote.");
100
100
  }
101
101
  /** The scoped-summarize turn message. It runs on the DEFAULT lane bootstrap
102
102
  * (context spine + citation teaching already present); the message carries
@@ -107,7 +107,14 @@ export interface BriefInput {
107
107
  openThreads: string[];
108
108
  decisions: string[];
109
109
  docs: BriefDocRef[];
110
+ /** Highest message seq this brief has incorporated. Rendered into the
111
+ * banner as the librarian's shared cursor; co-librarians read it before
112
+ * spending anything. */
113
+ coveredSeq?: number;
110
114
  }
115
+ /** The banner's machine-readable coverage claim. Parsers must only consult
116
+ * the banner blockquote line, never body prose. */
117
+ export declare const BRIEF_COVERED_RE: RegExp;
111
118
  /** Render the canonical brief markdown. Deterministic shell; the LLM only
112
119
  * supplies the summary/threads/decisions prose. */
113
120
  export declare function renderBrief(input: BriefInput): string;
@@ -128,19 +128,23 @@ export function briefPath(roomName) {
128
128
  .slice(0, 60) || "room";
129
129
  return `briefs/${slug}.md`;
130
130
  }
131
+ /** The banner's machine-readable coverage claim. Parsers must only consult
132
+ * the banner blockquote line, never body prose. */
133
+ export const BRIEF_COVERED_RE = /\bCovers through seq (\d+)\./;
131
134
  /** Render the canonical brief markdown. Deterministic shell; the LLM only
132
135
  * supplies the summary/threads/decisions prose. */
133
136
  export function renderBrief(input) {
137
+ const covered = input.coveredSeq !== undefined ? ` Covers through seq ${input.coveredSeq}.` : "";
134
138
  const lines = [];
135
139
  lines.push(`# Brief: ${input.room}`);
136
140
  lines.push("");
137
- lines.push(`> Auto-generated by the room agent, read-only. Updated ${input.updatedAtLabel}.`);
141
+ lines.push(`> Auto-generated by the room agent, read-only. Updated ${input.updatedAtLabel}.${covered}`);
138
142
  lines.push("");
139
143
  lines.push("## Summary");
140
144
  lines.push("");
141
145
  lines.push(input.summary.trim() || "_No summary yet._");
142
146
  lines.push("");
143
- lines.push("## Open threads");
147
+ lines.push("## Open questions");
144
148
  lines.push("");
145
149
  if (input.openThreads.length === 0)
146
150
  lines.push("_None._");
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type RoomAccessMode, type RoomAttachmentInput, type RoomAuditLogPayload, type RoomAuthSuccessPayload, type RoomBackfillPayload, type RoomClientPlatform, type RoomDmHistoryResultPayload, type RoomDmMessagePayload, type RoomDmOpenedPayload, type RoomFriendPresence, type RoomInvitedPayload, type RoomLedgerEntry, type RoomMemberRemovedPayload, type RoomMemberRoleChangedPayload, type RoomMeta, type RoomPinsPayload, type RoomPresencePayload, type RoomReactorsPayload, type RoomReadStatePayload, type RoomReportedPayload, type RoomRole, type RoomSearchResultsPayload, type RoomSearchScope, type RoomSpace, type RoomThreadPayload, type RoomTopic, type RoomTopicAttention, type RoomTopicResultPayload, type RoomTopicsResultPayload, type RoomUserStatus, type RoomWhoPayload } from "./types.js";
1
+ import { type RoomAccessMode, type RoomAttachmentInput, type RoomAuditLogPayload, type RoomAuthSuccessPayload, type RoomBackfillPayload, type RoomClientPlatform, type RoomDmHistoryResultPayload, type RoomDmInboxUpdatedPayload, type RoomDmMessageDeletedPayload, type RoomDmMessageEditedPayload, type RoomDmMessagePayload, type RoomDmOpenedPayload, type RoomDmPinsResultPayload, type RoomDmReactionUpdatedPayload, type RoomForwardMetadata, type RoomFriendPresence, type RoomInvitedPayload, type RoomLedgerEntry, type RoomMemberRemovedPayload, type RoomMemberRoleChangedPayload, type RoomMeta, type RoomPinsPayload, type RoomPresencePayload, type RoomReactorsPayload, type RoomReadStatePayload, type RoomReportedPayload, type RoomRole, type RoomSearchResultsPayload, type RoomSearchScope, type RoomSpace, type RoomThreadPayload, type RoomTopic, type RoomTopicAttention, type RoomTopicResultPayload, type RoomTopicsResultPayload, type RoomUserStatus, type RoomWhoPayload } from "./types.js";
2
2
  import { type RoomWebSocketCtor, RoomWsClient } from "./ws-client.js";
3
3
  export interface RoomClientConfig {
4
4
  url: string;
@@ -212,6 +212,14 @@ export interface RoomDmSendRequest {
212
212
  * URL. Stored on the message's imageUrl; the client signs keys on render.
213
213
  * Additive — omitted for text-only sends. */
214
214
  imageUrl?: string;
215
+ metadata?: RoomForwardMetadata;
216
+ }
217
+ export interface RoomDmReplyRequest extends RoomDmSendRequest {
218
+ replyTo: string;
219
+ }
220
+ export interface RoomDmReadRequest {
221
+ username?: string;
222
+ conversationId?: string;
215
223
  }
216
224
  export interface RoomThreadRequest {
217
225
  room: string;
@@ -254,21 +262,22 @@ export declare function whoamiRoom(config: RoomClientConfig): Promise<RoomAuthSu
254
262
  export declare function requestRoomFriends(client: RoomWsClient): Promise<RoomFriendPresence[]>;
255
263
  export declare function openRoomDmOnClient(client: RoomWsClient, request: RoomDmOpenRequest, timeoutMs?: number): Promise<RoomDmOpenedPayload>;
256
264
  export declare function requestRoomDmHistoryOnClient(client: RoomWsClient, request: RoomDmHistoryRequest, timeoutMs?: number): Promise<RoomDmHistoryResultPayload>;
265
+ export declare function markRoomDmReadOnClient(client: RoomWsClient, request: RoomDmReadRequest, timeoutMs?: number): Promise<RoomDmInboxUpdatedPayload>;
257
266
  export declare function sendRoomDmOnClient(client: RoomWsClient, request: RoomDmSendRequest, timeoutMs?: number): Promise<RoomDmMessagePayload>;
258
- /** Edit one of my DM messages. Fire-and-forget over the WS; the relay fans out
259
- * DM_MESSAGE_EDITED to both participants. */
267
+ export declare function replyRoomDmOnClient(client: RoomWsClient, request: RoomDmReplyRequest, timeoutMs?: number): Promise<RoomDmMessagePayload>;
268
+ /** Edit one of my DM messages and wait for the correlated relay update. */
260
269
  export declare function editRoomDmOnClient(client: RoomWsClient, request: {
261
270
  conversationId?: string;
262
271
  username?: string;
263
272
  messageId: string;
264
273
  content: string;
265
- }): void;
274
+ }, timeoutMs?: number): Promise<RoomDmMessageEditedPayload>;
266
275
  /** Soft-delete one of my DM messages. The relay fans out DM_MESSAGE_DELETED. */
267
276
  export declare function deleteRoomDmOnClient(client: RoomWsClient, request: {
268
277
  conversationId?: string;
269
278
  username?: string;
270
279
  messageId: string;
271
- }): void;
280
+ }, timeoutMs?: number): Promise<RoomDmMessageDeletedPayload>;
272
281
  /** Toggle a reaction on a DM message. The relay fans out DM_REACTION_UPDATED. */
273
282
  export declare function reactRoomDmOnClient(client: RoomWsClient, request: {
274
283
  conversationId?: string;
@@ -276,7 +285,23 @@ export declare function reactRoomDmOnClient(client: RoomWsClient, request: {
276
285
  messageId: string;
277
286
  emoji: string;
278
287
  op: "add" | "remove";
288
+ }, timeoutMs?: number): Promise<RoomDmReactionUpdatedPayload>;
289
+ /** Set shared pin state for a DM message. The relay fans out DM_PIN_UPDATED. */
290
+ export declare function pinRoomDmOnClient(client: RoomWsClient, request: {
291
+ conversationId?: string;
292
+ username?: string;
293
+ messageId: string;
294
+ }): void;
295
+ export declare function unpinRoomDmOnClient(client: RoomWsClient, request: {
296
+ conversationId?: string;
297
+ username?: string;
298
+ messageId: string;
279
299
  }): void;
300
+ export declare function requestRoomDmPinsOnClient(client: RoomWsClient, request: {
301
+ conversationId?: string;
302
+ username?: string;
303
+ limit?: number;
304
+ }, timeoutMs?: number): Promise<RoomDmPinsResultPayload>;
280
305
  export declare function peekRoom(config: RoomClientConfig, options: RoomPeekOptions): Promise<RoomBackfillPayload>;
281
306
  export declare function sayRoom(config: RoomClientConfig, options: RoomSayOptions): Promise<RoomSayResult>;
282
307
  export declare function joinRoom(config: RoomClientConfig, options: RoomJoinOptions): Promise<RoomJoinedSession>;
@@ -292,7 +317,7 @@ export declare function attachRejoinOnReconnect(client: RoomWsClient, options: R
292
317
  * (1..128 chars after trim); dedupe on (userId, clientMsgId) is server-side
293
318
  * work and NOT implied here.
294
319
  */
295
- export declare function postRoomMessage(client: RoomWsClient, room: string, text: string, inReplyTo?: number, attachments?: RoomAttachmentInput[], topicId?: string, clientMsgId?: string): string;
320
+ export declare function postRoomMessage(client: RoomWsClient, room: string, text: string, inReplyTo?: number, attachments?: RoomAttachmentInput[], topicId?: string, clientMsgId?: string, metadata?: RoomForwardMetadata): string;
296
321
  export declare function sendRoomTyping(client: RoomWsClient, room: string, state: "start" | "stop"): void;
297
322
  export declare function sendRoomStatus(client: RoomWsClient, status: RoomUserStatus, statusText?: string | null, platform?: RoomClientPlatform): void;
298
323
  /**
package/dist/client.js CHANGED
@@ -454,6 +454,20 @@ export async function requestRoomDmHistoryOnClient(client, request, timeoutMs) {
454
454
  const history = await client.waitFor(RoomServerMessageType.DM_HISTORY, (msg) => msg.payload.requestId === requestId, timeoutMs, requestId);
455
455
  return history.payload;
456
456
  }
457
+ export async function markRoomDmReadOnClient(client, request, timeoutMs) {
458
+ const requestId = randomUUID();
459
+ client.send({
460
+ type: RoomClientMessageType.DM_READ,
461
+ payload: {
462
+ requestId,
463
+ ...(request.username ? { username: request.username } : {}),
464
+ ...(request.conversationId ? { conversationId: request.conversationId } : {}),
465
+ },
466
+ timestamp: Date.now(),
467
+ });
468
+ const updated = await client.waitFor(RoomServerMessageType.DM_INBOX_UPDATED, (msg) => msg.payload.requestId === requestId, timeoutMs, requestId);
469
+ return updated.payload;
470
+ }
457
471
  export async function sendRoomDmOnClient(client, request, timeoutMs) {
458
472
  const requestId = randomUUID();
459
473
  const clientMsgId = request.clientMsgId ?? randomUUID();
@@ -466,6 +480,29 @@ export async function sendRoomDmOnClient(client, request, timeoutMs) {
466
480
  ...(request.username ? { username: request.username } : {}),
467
481
  ...(request.conversationId ? { conversationId: request.conversationId } : {}),
468
482
  ...(request.imageUrl ? { imageUrl: request.imageUrl } : {}),
483
+ ...(request.metadata ? { metadata: request.metadata } : {}),
484
+ },
485
+ timestamp: Date.now(),
486
+ });
487
+ const sent = await client.waitFor(RoomServerMessageType.DM_MESSAGE, (msg) => msg.payload.requestId === requestId ||
488
+ msg.payload.clientMsgId === clientMsgId ||
489
+ msg.payload.message.clientMsgId === clientMsgId, timeoutMs, requestId);
490
+ return sent.payload;
491
+ }
492
+ export async function replyRoomDmOnClient(client, request, timeoutMs) {
493
+ const requestId = randomUUID();
494
+ const clientMsgId = request.clientMsgId ?? randomUUID();
495
+ client.send({
496
+ type: RoomClientMessageType.DM_REPLY,
497
+ payload: {
498
+ requestId,
499
+ content: request.content,
500
+ replyTo: request.replyTo,
501
+ clientMsgId,
502
+ ...(request.username ? { username: request.username } : {}),
503
+ ...(request.conversationId ? { conversationId: request.conversationId } : {}),
504
+ ...(request.imageUrl ? { imageUrl: request.imageUrl } : {}),
505
+ ...(request.metadata ? { metadata: request.metadata } : {}),
469
506
  },
470
507
  timestamp: Date.now(),
471
508
  });
@@ -474,12 +511,13 @@ export async function sendRoomDmOnClient(client, request, timeoutMs) {
474
511
  msg.payload.message.clientMsgId === clientMsgId, timeoutMs, requestId);
475
512
  return sent.payload;
476
513
  }
477
- /** Edit one of my DM messages. Fire-and-forget over the WS; the relay fans out
478
- * DM_MESSAGE_EDITED to both participants. */
479
- export function editRoomDmOnClient(client, request) {
514
+ /** Edit one of my DM messages and wait for the correlated relay update. */
515
+ export async function editRoomDmOnClient(client, request, timeoutMs) {
516
+ const requestId = randomUUID();
480
517
  client.send({
481
518
  type: RoomClientMessageType.DM_EDIT,
482
519
  payload: {
520
+ requestId,
483
521
  messageId: request.messageId,
484
522
  content: request.content,
485
523
  ...(request.conversationId ? { conversationId: request.conversationId } : {}),
@@ -487,24 +525,32 @@ export function editRoomDmOnClient(client, request) {
487
525
  },
488
526
  timestamp: Date.now(),
489
527
  });
528
+ const updated = await client.waitFor(RoomServerMessageType.DM_MESSAGE_EDITED, (msg) => msg.payload.requestId === requestId, timeoutMs, requestId);
529
+ return updated.payload;
490
530
  }
491
531
  /** Soft-delete one of my DM messages. The relay fans out DM_MESSAGE_DELETED. */
492
- export function deleteRoomDmOnClient(client, request) {
532
+ export async function deleteRoomDmOnClient(client, request, timeoutMs) {
533
+ const requestId = randomUUID();
493
534
  client.send({
494
535
  type: RoomClientMessageType.DM_DELETE,
495
536
  payload: {
537
+ requestId,
496
538
  messageId: request.messageId,
497
539
  ...(request.conversationId ? { conversationId: request.conversationId } : {}),
498
540
  ...(request.username ? { username: request.username } : {}),
499
541
  },
500
542
  timestamp: Date.now(),
501
543
  });
544
+ const updated = await client.waitFor(RoomServerMessageType.DM_MESSAGE_DELETED, (msg) => msg.payload.requestId === requestId, timeoutMs, requestId);
545
+ return updated.payload;
502
546
  }
503
547
  /** Toggle a reaction on a DM message. The relay fans out DM_REACTION_UPDATED. */
504
- export function reactRoomDmOnClient(client, request) {
548
+ export async function reactRoomDmOnClient(client, request, timeoutMs) {
549
+ const requestId = randomUUID();
505
550
  client.send({
506
551
  type: RoomClientMessageType.DM_REACT,
507
552
  payload: {
553
+ requestId,
508
554
  messageId: request.messageId,
509
555
  emoji: request.emoji,
510
556
  op: request.op,
@@ -513,6 +559,46 @@ export function reactRoomDmOnClient(client, request) {
513
559
  },
514
560
  timestamp: Date.now(),
515
561
  });
562
+ const updated = await client.waitFor(RoomServerMessageType.DM_REACTION_UPDATED, (msg) => msg.payload.requestId === requestId, timeoutMs, requestId);
563
+ return updated.payload;
564
+ }
565
+ /** Set shared pin state for a DM message. The relay fans out DM_PIN_UPDATED. */
566
+ export function pinRoomDmOnClient(client, request) {
567
+ client.send({
568
+ type: RoomClientMessageType.DM_PIN,
569
+ payload: {
570
+ messageId: request.messageId,
571
+ ...(request.conversationId ? { conversationId: request.conversationId } : {}),
572
+ ...(request.username ? { username: request.username } : {}),
573
+ },
574
+ timestamp: Date.now(),
575
+ });
576
+ }
577
+ export function unpinRoomDmOnClient(client, request) {
578
+ client.send({
579
+ type: RoomClientMessageType.DM_UNPIN,
580
+ payload: {
581
+ messageId: request.messageId,
582
+ ...(request.conversationId ? { conversationId: request.conversationId } : {}),
583
+ ...(request.username ? { username: request.username } : {}),
584
+ },
585
+ timestamp: Date.now(),
586
+ });
587
+ }
588
+ export async function requestRoomDmPinsOnClient(client, request, timeoutMs) {
589
+ const requestId = randomUUID();
590
+ client.send({
591
+ type: RoomClientMessageType.DM_PINS,
592
+ payload: {
593
+ requestId,
594
+ ...(request.conversationId ? { conversationId: request.conversationId } : {}),
595
+ ...(request.username ? { username: request.username } : {}),
596
+ ...(request.limit ? { limit: request.limit } : {}),
597
+ },
598
+ timestamp: Date.now(),
599
+ });
600
+ const result = await client.waitFor(RoomServerMessageType.DM_PINS, (message) => message.payload.requestId === requestId, timeoutMs, requestId);
601
+ return result.payload;
516
602
  }
517
603
  export async function peekRoom(config, options) {
518
604
  if (!config.webSocketCtor) {
@@ -627,7 +713,7 @@ export function attachRejoinOnReconnect(client, options, latestSeq, onRejoined,
627
713
  * (1..128 chars after trim); dedupe on (userId, clientMsgId) is server-side
628
714
  * work and NOT implied here.
629
715
  */
630
- export function postRoomMessage(client, room, text, inReplyTo, attachments, topicId, clientMsgId) {
716
+ export function postRoomMessage(client, room, text, inReplyTo, attachments, topicId, clientMsgId, metadata) {
631
717
  const msgId = clientMsgId ?? randomUUID();
632
718
  client.send({
633
719
  type: RoomClientMessageType.POST,
@@ -638,6 +724,7 @@ export function postRoomMessage(client, room, text, inReplyTo, attachments, topi
638
724
  clientMsgId: msgId,
639
725
  ...(inReplyTo !== undefined ? { inReplyTo } : {}),
640
726
  ...(topicId ? { topicId } : {}),
727
+ ...(metadata ? { metadata } : {}),
641
728
  },
642
729
  timestamp: Date.now(),
643
730
  });
@@ -37,10 +37,14 @@ export declare const RoomClientMessageType: {
37
37
  readonly DM_OPEN: "DM_OPEN";
38
38
  readonly DM_HISTORY: "DM_HISTORY";
39
39
  readonly DM_SEND: "DM_SEND";
40
+ readonly DM_REPLY: "DM_REPLY";
40
41
  readonly DM_READ: "DM_READ";
41
42
  readonly DM_EDIT: "DM_EDIT";
42
43
  readonly DM_DELETE: "DM_DELETE";
43
44
  readonly DM_REACT: "DM_REACT";
45
+ readonly DM_PIN: "DM_PIN";
46
+ readonly DM_UNPIN: "DM_UNPIN";
47
+ readonly DM_PINS: "DM_PINS";
44
48
  readonly TOPIC_CREATE: "TOPIC_CREATE";
45
49
  readonly TOPIC_RENAME: "TOPIC_RENAME";
46
50
  readonly TOPIC_MOVE: "TOPIC_MOVE";
@@ -94,6 +98,8 @@ export declare const RoomServerMessageType: {
94
98
  readonly DM_MESSAGE_EDITED: "DM_MESSAGE_EDITED";
95
99
  readonly DM_MESSAGE_DELETED: "DM_MESSAGE_DELETED";
96
100
  readonly DM_REACTION_UPDATED: "DM_REACTION_UPDATED";
101
+ readonly DM_PIN_UPDATED: "DM_PIN_UPDATED";
102
+ readonly DM_PINS: "DM_PINS";
97
103
  readonly TOPIC: "TOPIC";
98
104
  readonly TOPICS: "TOPICS";
99
105
  readonly TOPIC_UPDATED: "TOPIC_UPDATED";
@@ -225,6 +231,19 @@ export interface RoomLedgerEntry {
225
231
  pinnedAt?: string | number | Date | null | undefined;
226
232
  attachments?: RoomAttachment[] | undefined;
227
233
  roomRole?: RoomRole | null | undefined;
234
+ metadata?: (Record<string, unknown> & Partial<RoomForwardMetadata>) | undefined;
235
+ }
236
+ export type RoomForwardedFrom = {
237
+ kind: "room";
238
+ room: string;
239
+ seq: number;
240
+ handle: string;
241
+ } | {
242
+ kind: "dm";
243
+ handle: string;
244
+ };
245
+ export interface RoomForwardMetadata {
246
+ forwardedFrom: RoomForwardedFrom;
228
247
  }
229
248
  export interface RoomAttachment {
230
249
  /**
@@ -472,12 +491,14 @@ export interface RoomDmMessage {
472
491
  createdAt?: string | number | undefined;
473
492
  /** Sealed DM: content is '' and the opaque envelope rides here. */
474
493
  secret?: RoomLedgerSecretPayload | null | undefined;
475
- metadata?: Record<string, unknown> | undefined;
494
+ metadata?: (Record<string, unknown> & Partial<RoomForwardMetadata>) | undefined;
476
495
  /** Legacy public chat-image URL (renders as-is, no signing). */
477
496
  imageUrl?: string | null | undefined;
478
497
  /** Private, signed, scanned DM attachments (dm-attachments/ keyspace).
479
498
  * Additive: old messages carry none and render unchanged. */
480
499
  attachments?: RoomAttachment[] | undefined;
500
+ /** Shared pin state. Legacy messages omit it and read as unpinned. */
501
+ pinned?: boolean | undefined;
481
502
  }
482
503
  export interface RoomDmOpenPayload {
483
504
  requestId?: string;
@@ -502,6 +523,11 @@ export interface RoomDmSendPayload {
502
523
  /** DM-attachment key or legacy public image URL; stored on the message's
503
524
  * imageUrl. Additive — text-only sends omit it. */
504
525
  imageUrl?: string;
526
+ metadata?: RoomForwardMetadata | undefined;
527
+ }
528
+ /** Client → server: send a DM whose parent is another message in the same conversation. */
529
+ export interface RoomDmReplyPayload extends RoomDmSendPayload {
530
+ replyTo: string;
505
531
  }
506
532
  /** Client → server: edit one of my DM messages (sealed messages are rejected). */
507
533
  export interface RoomDmEditPayload {
@@ -520,6 +546,7 @@ export interface RoomDmDeletePayload {
520
546
  }
521
547
  /** Server → both participants: a DM message was edited. */
522
548
  export interface RoomDmMessageEditedPayload {
549
+ requestId?: string;
523
550
  conversationId: string;
524
551
  messageId: string;
525
552
  content: string;
@@ -528,6 +555,7 @@ export interface RoomDmMessageEditedPayload {
528
555
  }
529
556
  /** Server → both participants: a DM message was soft-deleted (tombstone). */
530
557
  export interface RoomDmMessageDeletedPayload {
558
+ requestId?: string;
531
559
  conversationId: string;
532
560
  messageId: string;
533
561
  deletedAt: string | number;
@@ -544,6 +572,7 @@ export interface RoomDmReactPayload {
544
572
  }
545
573
  /** Server → both participants: a DM message's aggregated reactions changed. */
546
574
  export interface RoomDmReactionUpdatedPayload {
575
+ requestId?: string;
547
576
  conversationId: string;
548
577
  messageId: string;
549
578
  reactions: Array<{
@@ -552,6 +581,31 @@ export interface RoomDmReactionUpdatedPayload {
552
581
  }>;
553
582
  participant: RoomDmParticipant;
554
583
  }
584
+ export interface RoomDmPinPayload {
585
+ requestId?: string;
586
+ conversationId?: string;
587
+ username?: string;
588
+ messageId: string;
589
+ }
590
+ export interface RoomDmPinsPayload {
591
+ requestId?: string;
592
+ conversationId?: string;
593
+ username?: string;
594
+ limit?: number;
595
+ }
596
+ export interface RoomDmPinUpdatedPayload {
597
+ requestId?: string;
598
+ conversationId: string;
599
+ participant: RoomDmParticipant;
600
+ messageId: string;
601
+ pinned: boolean;
602
+ }
603
+ export interface RoomDmPinsResultPayload {
604
+ requestId?: string;
605
+ conversationId: string;
606
+ participant: RoomDmParticipant;
607
+ pins: RoomDmMessage[];
608
+ }
555
609
  export interface RoomDmReadPayload {
556
610
  requestId?: string;
557
611
  username?: string;
@@ -575,6 +629,7 @@ export interface RoomDmMessagePayload {
575
629
  clientMsgId?: string;
576
630
  }
577
631
  export interface RoomDmInboxUpdatedPayload {
632
+ requestId?: string;
578
633
  conversationId: string;
579
634
  participant: RoomDmParticipant;
580
635
  unreadCount: number;
@@ -888,6 +943,7 @@ export type RoomClientMessage = {
888
943
  clientMsgId?: string;
889
944
  reason?: string;
890
945
  requestId?: string;
946
+ metadata?: RoomForwardMetadata;
891
947
  };
892
948
  timestamp?: number;
893
949
  } | {
@@ -1073,6 +1129,10 @@ export type RoomClientMessage = {
1073
1129
  type: typeof RoomClientMessageType.DM_SEND;
1074
1130
  payload: RoomDmSendPayload;
1075
1131
  timestamp?: number;
1132
+ } | {
1133
+ type: typeof RoomClientMessageType.DM_REPLY;
1134
+ payload: RoomDmReplyPayload;
1135
+ timestamp?: number;
1076
1136
  } | {
1077
1137
  type: typeof RoomClientMessageType.DM_READ;
1078
1138
  payload: RoomDmReadPayload;
@@ -1089,6 +1149,18 @@ export type RoomClientMessage = {
1089
1149
  type: typeof RoomClientMessageType.DM_REACT;
1090
1150
  payload: RoomDmReactPayload;
1091
1151
  timestamp?: number;
1152
+ } | {
1153
+ type: typeof RoomClientMessageType.DM_PIN;
1154
+ payload: RoomDmPinPayload;
1155
+ timestamp?: number;
1156
+ } | {
1157
+ type: typeof RoomClientMessageType.DM_UNPIN;
1158
+ payload: RoomDmPinPayload;
1159
+ timestamp?: number;
1160
+ } | {
1161
+ type: typeof RoomClientMessageType.DM_PINS;
1162
+ payload: RoomDmPinsPayload;
1163
+ timestamp?: number;
1092
1164
  } | {
1093
1165
  type: typeof RoomClientMessageType.TOPIC_CREATE;
1094
1166
  payload: RoomTopicCreatePayload;
@@ -1295,6 +1367,14 @@ export type RoomServerMessage = {
1295
1367
  type: typeof RoomServerMessageType.DM_REACTION_UPDATED;
1296
1368
  payload: RoomDmReactionUpdatedPayload;
1297
1369
  timestamp: number;
1370
+ } | {
1371
+ type: typeof RoomServerMessageType.DM_PIN_UPDATED;
1372
+ payload: RoomDmPinUpdatedPayload;
1373
+ timestamp: number;
1374
+ } | {
1375
+ type: typeof RoomServerMessageType.DM_PINS;
1376
+ payload: RoomDmPinsResultPayload;
1377
+ timestamp: number;
1298
1378
  } | {
1299
1379
  type: typeof RoomServerMessageType.TOPIC;
1300
1380
  payload: RoomTopicResultPayload;
@@ -37,10 +37,14 @@ export const RoomClientMessageType = {
37
37
  DM_OPEN: "DM_OPEN",
38
38
  DM_HISTORY: "DM_HISTORY",
39
39
  DM_SEND: "DM_SEND",
40
+ DM_REPLY: "DM_REPLY",
40
41
  DM_READ: "DM_READ",
41
42
  DM_EDIT: "DM_EDIT",
42
43
  DM_DELETE: "DM_DELETE",
43
44
  DM_REACT: "DM_REACT",
45
+ DM_PIN: "DM_PIN",
46
+ DM_UNPIN: "DM_UNPIN",
47
+ DM_PINS: "DM_PINS",
44
48
  TOPIC_CREATE: "TOPIC_CREATE",
45
49
  TOPIC_RENAME: "TOPIC_RENAME",
46
50
  TOPIC_MOVE: "TOPIC_MOVE",
@@ -93,6 +97,8 @@ export const RoomServerMessageType = {
93
97
  DM_MESSAGE_EDITED: "DM_MESSAGE_EDITED",
94
98
  DM_MESSAGE_DELETED: "DM_MESSAGE_DELETED",
95
99
  DM_REACTION_UPDATED: "DM_REACTION_UPDATED",
100
+ DM_PIN_UPDATED: "DM_PIN_UPDATED",
101
+ DM_PINS: "DM_PINS",
96
102
  TOPIC: "TOPIC",
97
103
  TOPICS: "TOPICS",
98
104
  TOPIC_UPDATED: "TOPIC_UPDATED",
@@ -174,7 +174,8 @@ export interface RoomSpaceSummary {
174
174
  export declare function listRoomSpacesViaChat(config: RoomSocialConfig): Promise<RoomSpaceSummary[]>;
175
175
  export interface RoomSpaceMember {
176
176
  userId: string;
177
- role: "owner" | "admin" | "member";
177
+ /** The ladder role. 'librarian' maintains the generated briefs/ docs. */
178
+ role: "owner" | "admin" | "librarian" | "member";
178
179
  /** Named identity roles (RoomSpaceRole.roleId). */
179
180
  roleIds?: string[];
180
181
  joinedAt?: string;
@@ -329,7 +330,7 @@ export declare function listRoomChannelMembers(config: RoomSocialConfig, roomId:
329
330
  export declare function removeSpaceMember(config: RoomSocialConfig, spaceId: string, targetUserId: string): Promise<{
330
331
  removed?: boolean;
331
332
  }>;
332
- export declare function changeSpaceMemberRole(config: RoomSocialConfig, spaceId: string, targetUserId: string, role: "admin" | "member"): Promise<RoomSpaceMember>;
333
+ export declare function changeSpaceMemberRole(config: RoomSocialConfig, spaceId: string, targetUserId: string, role: "admin" | "librarian" | "member"): Promise<RoomSpaceMember>;
333
334
  /** Set (string) or clear (null) a member's per-space nickname; the store validates and enforces uniqueness. */
334
335
  export declare function setSpaceMemberNickname(config: RoomSocialConfig, spaceId: string, targetUserId: string, nickname: string | null): Promise<RoomSpaceMember>;
335
336
  export declare function createRoomSpace(config: RoomSocialConfig, input: {
@@ -130,6 +130,8 @@ export interface RoomDmMessage {
130
130
  emoji: string;
131
131
  users: string[];
132
132
  }>;
133
+ /** Shared pin state. Additive; absent means unpinned. */
134
+ pinned?: boolean;
133
135
  /** Client-minted idempotency tag echoed back on DM sends, so a sender's
134
136
  * durable queue can settle its optimistic row. Additive (0.4.0). */
135
137
  clientMsgId?: string;
package/dist/types.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export type { RoomAccessMode, RoomAttachment, RoomAttachmentInput, RoomAuditEntry, RoomAuditLogPayload, RoomAuditLogRequestPayload, RoomAuthSuccessPayload, RoomBackfillPayload, RoomClientMessage, RoomClientPlatform, RoomCreatedPayload, RoomCreatePayload, RoomDmHistoryPayload, RoomDmHistoryResultPayload, RoomDmInboxUpdatedPayload, RoomDmMessage, RoomDmMessagePayload, RoomDmOpenedPayload, RoomDmOpenPayload, RoomDmParticipant, RoomDmReadPayload, RoomDmSendPayload, RoomEntryUpdatedPayload, RoomErrorPayload, RoomFriendPresence, RoomInvitedPayload, RoomInvitePayload, RoomLedgerEntry, RoomMember, RoomMemberRemovedPayload, RoomMemberRoleChangedPayload, RoomMemberTier, RoomMessagePayload, RoomMeta, RoomModerateMemberPayload, RoomMutedPayload, RoomPinsPayload, RoomPresenceDeltaPayload, RoomPresencePayload, RoomReactorsPayload, RoomReadStatePayload, RoomRemoveMemberPayload, RoomReportedPayload, RoomReportPayload, RoomResyncPayload, RoomRole, RoomRoleChangePayload, RoomSearchPayload, RoomSearchResultsPayload, RoomSearchScope, RoomServerMessage, RoomServerMessageOf, RoomSetAttentionPayload, RoomSetReadOnlyPayload, RoomSpace, RoomSpaceJoinedPayload, RoomSpacesPayload, RoomThreadPayload, RoomTopic, RoomTopicAttention, RoomTopicBucketState, RoomTopicCreatePayload, RoomTopicListPayload, RoomTopicMergePayload, RoomTopicMovePayload, RoomTopicRenamePayload, RoomTopicResolvePayload, RoomTopicResultPayload, RoomTopicSetAttentionPayload, RoomTopicsPolicy, RoomTopicsResultPayload, RoomTopicUpdatedPayload, RoomTypingPayload, RoomUserStatus, RoomWhoPayload, } from "./shared/rooms-protocol.js";
1
+ export type { RoomAccessMode, RoomAttachment, RoomAttachmentInput, RoomAuditEntry, RoomAuditLogPayload, RoomAuditLogRequestPayload, RoomAuthSuccessPayload, RoomBackfillPayload, RoomClientMessage, RoomClientPlatform, RoomCreatedPayload, RoomCreatePayload, RoomDmHistoryPayload, RoomDmHistoryResultPayload, RoomDmInboxUpdatedPayload, RoomDmMessage, RoomDmMessageDeletedPayload, RoomDmMessageEditedPayload, RoomDmMessagePayload, RoomDmOpenedPayload, RoomDmOpenPayload, RoomDmParticipant, RoomDmPinsResultPayload, RoomDmPinUpdatedPayload, RoomDmReactionUpdatedPayload, RoomDmReadPayload, RoomDmReplyPayload, RoomDmSendPayload, RoomEntryUpdatedPayload, RoomErrorPayload, RoomForwardedFrom, RoomForwardMetadata, RoomFriendPresence, RoomInvitedPayload, RoomInvitePayload, RoomLedgerEntry, RoomMember, RoomMemberRemovedPayload, RoomMemberRoleChangedPayload, RoomMemberTier, RoomMessagePayload, RoomMeta, RoomModerateMemberPayload, RoomMutedPayload, RoomPinsPayload, RoomPresenceDeltaPayload, RoomPresencePayload, RoomReactorsPayload, RoomReadStatePayload, RoomRemoveMemberPayload, RoomReportedPayload, RoomReportPayload, RoomResyncPayload, RoomRole, RoomRoleChangePayload, RoomSearchPayload, RoomSearchResultsPayload, RoomSearchScope, RoomServerMessage, RoomServerMessageOf, RoomSetAttentionPayload, RoomSetReadOnlyPayload, RoomSpace, RoomSpaceJoinedPayload, RoomSpacesPayload, RoomThreadPayload, RoomTopic, RoomTopicAttention, RoomTopicBucketState, RoomTopicCreatePayload, RoomTopicListPayload, RoomTopicMergePayload, RoomTopicMovePayload, RoomTopicRenamePayload, RoomTopicResolvePayload, RoomTopicResultPayload, RoomTopicSetAttentionPayload, RoomTopicsPolicy, RoomTopicsResultPayload, RoomTopicUpdatedPayload, RoomTypingPayload, RoomUserStatus, RoomWhoPayload, } from "./shared/rooms-protocol.js";
2
2
  export { applyRoomPresenceDelta, isSystemRoomLedgerEntryType, ROOM_PRESENCE_SAMPLE_LIMIT, RoomClientMessageType, RoomLedgerEntryType, RoomServerMessageType, SUPPORTED_ROOM_PROTOCOL_VERSION, } from "./shared/rooms-protocol.js";
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.5.1";
2
- export declare const RUNNER_VERSION = "0.5.1";
1
+ export declare const VERSION = "0.7.0";
2
+ export declare const RUNNER_VERSION = "0.7.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
- export const VERSION = "0.5.1";
2
- export const RUNNER_VERSION = "0.5.1";
1
+ export const VERSION = "0.7.0";
2
+ export const RUNNER_VERSION = "0.7.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmarket/rooms-client",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "OM Rooms protocol client: wire types, WebSocket + REST clients, and chat view-models. Browser-safe (no node:/bun: imports).",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -17,7 +17,12 @@
17
17
  "bun": ">=1.3.0",
18
18
  "node": ">=22"
19
19
  },
20
- "files": ["src", "dist", "README.md", "LICENSE"],
20
+ "files": [
21
+ "src",
22
+ "dist",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
21
26
  "publishConfig": {
22
27
  "access": "public"
23
28
  },
@@ -202,7 +202,7 @@ export function buildBriefRefreshBootstrap(opts: {
202
202
  "## Summary",
203
203
  "<one or two short paragraphs on what this room is about and where it stands>",
204
204
  "",
205
- "## Open threads",
205
+ "## Open questions",
206
206
  "- <unresolved questions, or `None.`>",
207
207
  "",
208
208
  "## Decisions",
@@ -202,21 +202,32 @@ export interface BriefInput {
202
202
  openThreads: string[];
203
203
  decisions: string[];
204
204
  docs: BriefDocRef[];
205
+ /** Highest message seq this brief has incorporated. Rendered into the
206
+ * banner as the librarian's shared cursor; co-librarians read it before
207
+ * spending anything. */
208
+ coveredSeq?: number;
205
209
  }
206
210
 
211
+ /** The banner's machine-readable coverage claim. Parsers must only consult
212
+ * the banner blockquote line, never body prose. */
213
+ export const BRIEF_COVERED_RE = /\bCovers through seq (\d+)\./;
214
+
207
215
  /** Render the canonical brief markdown. Deterministic shell; the LLM only
208
216
  * supplies the summary/threads/decisions prose. */
209
217
  export function renderBrief(input: BriefInput): string {
218
+ const covered = input.coveredSeq !== undefined ? ` Covers through seq ${input.coveredSeq}.` : "";
210
219
  const lines: string[] = [];
211
220
  lines.push(`# Brief: ${input.room}`);
212
221
  lines.push("");
213
- lines.push(`> Auto-generated by the room agent, read-only. Updated ${input.updatedAtLabel}.`);
222
+ lines.push(
223
+ `> Auto-generated by the room agent, read-only. Updated ${input.updatedAtLabel}.${covered}`,
224
+ );
214
225
  lines.push("");
215
226
  lines.push("## Summary");
216
227
  lines.push("");
217
228
  lines.push(input.summary.trim() || "_No summary yet._");
218
229
  lines.push("");
219
- lines.push("## Open threads");
230
+ lines.push("## Open questions");
220
231
  lines.push("");
221
232
  if (input.openThreads.length === 0) lines.push("_None._");
222
233
  else for (const t of input.openThreads) lines.push(`- ${t}`);
package/src/client.ts CHANGED
@@ -15,8 +15,14 @@ import {
15
15
  RoomClientMessageType,
16
16
  type RoomClientPlatform,
17
17
  type RoomDmHistoryResultPayload,
18
+ type RoomDmInboxUpdatedPayload,
19
+ type RoomDmMessageDeletedPayload,
20
+ type RoomDmMessageEditedPayload,
18
21
  type RoomDmMessagePayload,
19
22
  type RoomDmOpenedPayload,
23
+ type RoomDmPinsResultPayload,
24
+ type RoomDmReactionUpdatedPayload,
25
+ type RoomForwardMetadata,
20
26
  type RoomFriendPresence,
21
27
  type RoomInvitedPayload,
22
28
  type RoomLedgerEntry,
@@ -291,6 +297,16 @@ export interface RoomDmSendRequest {
291
297
  * URL. Stored on the message's imageUrl; the client signs keys on render.
292
298
  * Additive — omitted for text-only sends. */
293
299
  imageUrl?: string;
300
+ metadata?: RoomForwardMetadata;
301
+ }
302
+
303
+ export interface RoomDmReplyRequest extends RoomDmSendRequest {
304
+ replyTo: string;
305
+ }
306
+
307
+ export interface RoomDmReadRequest {
308
+ username?: string;
309
+ conversationId?: string;
294
310
  }
295
311
 
296
312
  export interface RoomThreadRequest {
@@ -923,6 +939,30 @@ export async function requestRoomDmHistoryOnClient(
923
939
  return history.payload;
924
940
  }
925
941
 
942
+ export async function markRoomDmReadOnClient(
943
+ client: RoomWsClient,
944
+ request: RoomDmReadRequest,
945
+ timeoutMs?: number,
946
+ ): Promise<RoomDmInboxUpdatedPayload> {
947
+ const requestId = randomUUID();
948
+ client.send({
949
+ type: RoomClientMessageType.DM_READ,
950
+ payload: {
951
+ requestId,
952
+ ...(request.username ? { username: request.username } : {}),
953
+ ...(request.conversationId ? { conversationId: request.conversationId } : {}),
954
+ },
955
+ timestamp: Date.now(),
956
+ });
957
+ const updated = await client.waitFor(
958
+ RoomServerMessageType.DM_INBOX_UPDATED,
959
+ (msg) => msg.payload.requestId === requestId,
960
+ timeoutMs,
961
+ requestId,
962
+ );
963
+ return updated.payload;
964
+ }
965
+
926
966
  export async function sendRoomDmOnClient(
927
967
  client: RoomWsClient,
928
968
  request: RoomDmSendRequest,
@@ -939,6 +979,40 @@ export async function sendRoomDmOnClient(
939
979
  ...(request.username ? { username: request.username } : {}),
940
980
  ...(request.conversationId ? { conversationId: request.conversationId } : {}),
941
981
  ...(request.imageUrl ? { imageUrl: request.imageUrl } : {}),
982
+ ...(request.metadata ? { metadata: request.metadata } : {}),
983
+ },
984
+ timestamp: Date.now(),
985
+ });
986
+ const sent = await client.waitFor(
987
+ RoomServerMessageType.DM_MESSAGE,
988
+ (msg) =>
989
+ msg.payload.requestId === requestId ||
990
+ msg.payload.clientMsgId === clientMsgId ||
991
+ msg.payload.message.clientMsgId === clientMsgId,
992
+ timeoutMs,
993
+ requestId,
994
+ );
995
+ return sent.payload;
996
+ }
997
+
998
+ export async function replyRoomDmOnClient(
999
+ client: RoomWsClient,
1000
+ request: RoomDmReplyRequest,
1001
+ timeoutMs?: number,
1002
+ ): Promise<RoomDmMessagePayload> {
1003
+ const requestId = randomUUID();
1004
+ const clientMsgId = request.clientMsgId ?? randomUUID();
1005
+ client.send({
1006
+ type: RoomClientMessageType.DM_REPLY,
1007
+ payload: {
1008
+ requestId,
1009
+ content: request.content,
1010
+ replyTo: request.replyTo,
1011
+ clientMsgId,
1012
+ ...(request.username ? { username: request.username } : {}),
1013
+ ...(request.conversationId ? { conversationId: request.conversationId } : {}),
1014
+ ...(request.imageUrl ? { imageUrl: request.imageUrl } : {}),
1015
+ ...(request.metadata ? { metadata: request.metadata } : {}),
942
1016
  },
943
1017
  timestamp: Date.now(),
944
1018
  });
@@ -954,15 +1028,17 @@ export async function sendRoomDmOnClient(
954
1028
  return sent.payload;
955
1029
  }
956
1030
 
957
- /** Edit one of my DM messages. Fire-and-forget over the WS; the relay fans out
958
- * DM_MESSAGE_EDITED to both participants. */
959
- export function editRoomDmOnClient(
1031
+ /** Edit one of my DM messages and wait for the correlated relay update. */
1032
+ export async function editRoomDmOnClient(
960
1033
  client: RoomWsClient,
961
1034
  request: { conversationId?: string; username?: string; messageId: string; content: string },
962
- ): void {
1035
+ timeoutMs?: number,
1036
+ ): Promise<RoomDmMessageEditedPayload> {
1037
+ const requestId = randomUUID();
963
1038
  client.send({
964
1039
  type: RoomClientMessageType.DM_EDIT,
965
1040
  payload: {
1041
+ requestId,
966
1042
  messageId: request.messageId,
967
1043
  content: request.content,
968
1044
  ...(request.conversationId ? { conversationId: request.conversationId } : {}),
@@ -970,26 +1046,43 @@ export function editRoomDmOnClient(
970
1046
  },
971
1047
  timestamp: Date.now(),
972
1048
  });
1049
+ const updated = await client.waitFor(
1050
+ RoomServerMessageType.DM_MESSAGE_EDITED,
1051
+ (msg) => msg.payload.requestId === requestId,
1052
+ timeoutMs,
1053
+ requestId,
1054
+ );
1055
+ return updated.payload;
973
1056
  }
974
1057
 
975
1058
  /** Soft-delete one of my DM messages. The relay fans out DM_MESSAGE_DELETED. */
976
- export function deleteRoomDmOnClient(
1059
+ export async function deleteRoomDmOnClient(
977
1060
  client: RoomWsClient,
978
1061
  request: { conversationId?: string; username?: string; messageId: string },
979
- ): void {
1062
+ timeoutMs?: number,
1063
+ ): Promise<RoomDmMessageDeletedPayload> {
1064
+ const requestId = randomUUID();
980
1065
  client.send({
981
1066
  type: RoomClientMessageType.DM_DELETE,
982
1067
  payload: {
1068
+ requestId,
983
1069
  messageId: request.messageId,
984
1070
  ...(request.conversationId ? { conversationId: request.conversationId } : {}),
985
1071
  ...(request.username ? { username: request.username } : {}),
986
1072
  },
987
1073
  timestamp: Date.now(),
988
1074
  });
1075
+ const updated = await client.waitFor(
1076
+ RoomServerMessageType.DM_MESSAGE_DELETED,
1077
+ (msg) => msg.payload.requestId === requestId,
1078
+ timeoutMs,
1079
+ requestId,
1080
+ );
1081
+ return updated.payload;
989
1082
  }
990
1083
 
991
1084
  /** Toggle a reaction on a DM message. The relay fans out DM_REACTION_UPDATED. */
992
- export function reactRoomDmOnClient(
1085
+ export async function reactRoomDmOnClient(
993
1086
  client: RoomWsClient,
994
1087
  request: {
995
1088
  conversationId?: string;
@@ -998,10 +1091,13 @@ export function reactRoomDmOnClient(
998
1091
  emoji: string;
999
1092
  op: "add" | "remove";
1000
1093
  },
1001
- ): void {
1094
+ timeoutMs?: number,
1095
+ ): Promise<RoomDmReactionUpdatedPayload> {
1096
+ const requestId = randomUUID();
1002
1097
  client.send({
1003
1098
  type: RoomClientMessageType.DM_REACT,
1004
1099
  payload: {
1100
+ requestId,
1005
1101
  messageId: request.messageId,
1006
1102
  emoji: request.emoji,
1007
1103
  op: request.op,
@@ -1010,6 +1106,69 @@ export function reactRoomDmOnClient(
1010
1106
  },
1011
1107
  timestamp: Date.now(),
1012
1108
  });
1109
+ const updated = await client.waitFor(
1110
+ RoomServerMessageType.DM_REACTION_UPDATED,
1111
+ (msg) => msg.payload.requestId === requestId,
1112
+ timeoutMs,
1113
+ requestId,
1114
+ );
1115
+ return updated.payload;
1116
+ }
1117
+
1118
+ /** Set shared pin state for a DM message. The relay fans out DM_PIN_UPDATED. */
1119
+ export function pinRoomDmOnClient(
1120
+ client: RoomWsClient,
1121
+ request: { conversationId?: string; username?: string; messageId: string },
1122
+ ): void {
1123
+ client.send({
1124
+ type: RoomClientMessageType.DM_PIN,
1125
+ payload: {
1126
+ messageId: request.messageId,
1127
+ ...(request.conversationId ? { conversationId: request.conversationId } : {}),
1128
+ ...(request.username ? { username: request.username } : {}),
1129
+ },
1130
+ timestamp: Date.now(),
1131
+ });
1132
+ }
1133
+
1134
+ export function unpinRoomDmOnClient(
1135
+ client: RoomWsClient,
1136
+ request: { conversationId?: string; username?: string; messageId: string },
1137
+ ): void {
1138
+ client.send({
1139
+ type: RoomClientMessageType.DM_UNPIN,
1140
+ payload: {
1141
+ messageId: request.messageId,
1142
+ ...(request.conversationId ? { conversationId: request.conversationId } : {}),
1143
+ ...(request.username ? { username: request.username } : {}),
1144
+ },
1145
+ timestamp: Date.now(),
1146
+ });
1147
+ }
1148
+
1149
+ export async function requestRoomDmPinsOnClient(
1150
+ client: RoomWsClient,
1151
+ request: { conversationId?: string; username?: string; limit?: number },
1152
+ timeoutMs?: number,
1153
+ ): Promise<RoomDmPinsResultPayload> {
1154
+ const requestId = randomUUID();
1155
+ client.send({
1156
+ type: RoomClientMessageType.DM_PINS,
1157
+ payload: {
1158
+ requestId,
1159
+ ...(request.conversationId ? { conversationId: request.conversationId } : {}),
1160
+ ...(request.username ? { username: request.username } : {}),
1161
+ ...(request.limit ? { limit: request.limit } : {}),
1162
+ },
1163
+ timestamp: Date.now(),
1164
+ });
1165
+ const result = await client.waitFor(
1166
+ RoomServerMessageType.DM_PINS,
1167
+ (message) => message.payload.requestId === requestId,
1168
+ timeoutMs,
1169
+ requestId,
1170
+ );
1171
+ return result.payload;
1013
1172
  }
1014
1173
 
1015
1174
  export async function peekRoom(
@@ -1165,6 +1324,7 @@ export function postRoomMessage(
1165
1324
  attachments?: RoomAttachmentInput[],
1166
1325
  topicId?: string,
1167
1326
  clientMsgId?: string,
1327
+ metadata?: RoomForwardMetadata,
1168
1328
  ): string {
1169
1329
  const msgId = clientMsgId ?? randomUUID();
1170
1330
  client.send({
@@ -1176,6 +1336,7 @@ export function postRoomMessage(
1176
1336
  clientMsgId: msgId,
1177
1337
  ...(inReplyTo !== undefined ? { inReplyTo } : {}),
1178
1338
  ...(topicId ? { topicId } : {}),
1339
+ ...(metadata ? { metadata } : {}),
1179
1340
  },
1180
1341
  timestamp: Date.now(),
1181
1342
  });
@@ -37,10 +37,14 @@ export const RoomClientMessageType = {
37
37
  DM_OPEN: "DM_OPEN",
38
38
  DM_HISTORY: "DM_HISTORY",
39
39
  DM_SEND: "DM_SEND",
40
+ DM_REPLY: "DM_REPLY",
40
41
  DM_READ: "DM_READ",
41
42
  DM_EDIT: "DM_EDIT",
42
43
  DM_DELETE: "DM_DELETE",
43
44
  DM_REACT: "DM_REACT",
45
+ DM_PIN: "DM_PIN",
46
+ DM_UNPIN: "DM_UNPIN",
47
+ DM_PINS: "DM_PINS",
44
48
  TOPIC_CREATE: "TOPIC_CREATE",
45
49
  TOPIC_RENAME: "TOPIC_RENAME",
46
50
  TOPIC_MOVE: "TOPIC_MOVE",
@@ -97,6 +101,8 @@ export const RoomServerMessageType = {
97
101
  DM_MESSAGE_EDITED: "DM_MESSAGE_EDITED",
98
102
  DM_MESSAGE_DELETED: "DM_MESSAGE_DELETED",
99
103
  DM_REACTION_UPDATED: "DM_REACTION_UPDATED",
104
+ DM_PIN_UPDATED: "DM_PIN_UPDATED",
105
+ DM_PINS: "DM_PINS",
100
106
  TOPIC: "TOPIC",
101
107
  TOPICS: "TOPICS",
102
108
  TOPIC_UPDATED: "TOPIC_UPDATED",
@@ -253,6 +259,23 @@ export interface RoomLedgerEntry {
253
259
  pinnedAt?: string | number | Date | null | undefined;
254
260
  attachments?: RoomAttachment[] | undefined;
255
261
  roomRole?: RoomRole | null | undefined;
262
+ metadata?: (Record<string, unknown> & Partial<RoomForwardMetadata>) | undefined;
263
+ }
264
+
265
+ export type RoomForwardedFrom =
266
+ | {
267
+ kind: "room";
268
+ room: string;
269
+ seq: number;
270
+ handle: string;
271
+ }
272
+ | {
273
+ kind: "dm";
274
+ handle: string;
275
+ };
276
+
277
+ export interface RoomForwardMetadata {
278
+ forwardedFrom: RoomForwardedFrom;
256
279
  }
257
280
 
258
281
  export interface RoomAttachment {
@@ -560,12 +583,14 @@ export interface RoomDmMessage {
560
583
  createdAt?: string | number | undefined;
561
584
  /** Sealed DM: content is '' and the opaque envelope rides here. */
562
585
  secret?: RoomLedgerSecretPayload | null | undefined;
563
- metadata?: Record<string, unknown> | undefined;
586
+ metadata?: (Record<string, unknown> & Partial<RoomForwardMetadata>) | undefined;
564
587
  /** Legacy public chat-image URL (renders as-is, no signing). */
565
588
  imageUrl?: string | null | undefined;
566
589
  /** Private, signed, scanned DM attachments (dm-attachments/ keyspace).
567
590
  * Additive: old messages carry none and render unchanged. */
568
591
  attachments?: RoomAttachment[] | undefined;
592
+ /** Shared pin state. Legacy messages omit it and read as unpinned. */
593
+ pinned?: boolean | undefined;
569
594
  }
570
595
 
571
596
  export interface RoomDmOpenPayload {
@@ -593,6 +618,12 @@ export interface RoomDmSendPayload {
593
618
  /** DM-attachment key or legacy public image URL; stored on the message's
594
619
  * imageUrl. Additive — text-only sends omit it. */
595
620
  imageUrl?: string;
621
+ metadata?: RoomForwardMetadata | undefined;
622
+ }
623
+
624
+ /** Client → server: send a DM whose parent is another message in the same conversation. */
625
+ export interface RoomDmReplyPayload extends RoomDmSendPayload {
626
+ replyTo: string;
596
627
  }
597
628
 
598
629
  /** Client → server: edit one of my DM messages (sealed messages are rejected). */
@@ -614,6 +645,7 @@ export interface RoomDmDeletePayload {
614
645
 
615
646
  /** Server → both participants: a DM message was edited. */
616
647
  export interface RoomDmMessageEditedPayload {
648
+ requestId?: string;
617
649
  conversationId: string;
618
650
  messageId: string;
619
651
  content: string;
@@ -623,6 +655,7 @@ export interface RoomDmMessageEditedPayload {
623
655
 
624
656
  /** Server → both participants: a DM message was soft-deleted (tombstone). */
625
657
  export interface RoomDmMessageDeletedPayload {
658
+ requestId?: string;
626
659
  conversationId: string;
627
660
  messageId: string;
628
661
  deletedAt: string | number;
@@ -641,12 +674,42 @@ export interface RoomDmReactPayload {
641
674
 
642
675
  /** Server → both participants: a DM message's aggregated reactions changed. */
643
676
  export interface RoomDmReactionUpdatedPayload {
677
+ requestId?: string;
644
678
  conversationId: string;
645
679
  messageId: string;
646
680
  reactions: Array<{ emoji: string; users: string[] }>;
647
681
  participant: RoomDmParticipant;
648
682
  }
649
683
 
684
+ export interface RoomDmPinPayload {
685
+ requestId?: string;
686
+ conversationId?: string;
687
+ username?: string;
688
+ messageId: string;
689
+ }
690
+
691
+ export interface RoomDmPinsPayload {
692
+ requestId?: string;
693
+ conversationId?: string;
694
+ username?: string;
695
+ limit?: number;
696
+ }
697
+
698
+ export interface RoomDmPinUpdatedPayload {
699
+ requestId?: string;
700
+ conversationId: string;
701
+ participant: RoomDmParticipant;
702
+ messageId: string;
703
+ pinned: boolean;
704
+ }
705
+
706
+ export interface RoomDmPinsResultPayload {
707
+ requestId?: string;
708
+ conversationId: string;
709
+ participant: RoomDmParticipant;
710
+ pins: RoomDmMessage[];
711
+ }
712
+
650
713
  export interface RoomDmReadPayload {
651
714
  requestId?: string;
652
715
  username?: string;
@@ -674,6 +737,7 @@ export interface RoomDmMessagePayload {
674
737
  }
675
738
 
676
739
  export interface RoomDmInboxUpdatedPayload {
740
+ requestId?: string;
677
741
  conversationId: string;
678
742
  participant: RoomDmParticipant;
679
743
  unreadCount: number;
@@ -1013,6 +1077,7 @@ export type RoomClientMessage =
1013
1077
  clientMsgId?: string;
1014
1078
  reason?: string;
1015
1079
  requestId?: string;
1080
+ metadata?: RoomForwardMetadata;
1016
1081
  };
1017
1082
  timestamp?: number;
1018
1083
  }
@@ -1188,6 +1253,11 @@ export type RoomClientMessage =
1188
1253
  payload: RoomDmSendPayload;
1189
1254
  timestamp?: number;
1190
1255
  }
1256
+ | {
1257
+ type: typeof RoomClientMessageType.DM_REPLY;
1258
+ payload: RoomDmReplyPayload;
1259
+ timestamp?: number;
1260
+ }
1191
1261
  | {
1192
1262
  type: typeof RoomClientMessageType.DM_READ;
1193
1263
  payload: RoomDmReadPayload;
@@ -1208,6 +1278,21 @@ export type RoomClientMessage =
1208
1278
  payload: RoomDmReactPayload;
1209
1279
  timestamp?: number;
1210
1280
  }
1281
+ | {
1282
+ type: typeof RoomClientMessageType.DM_PIN;
1283
+ payload: RoomDmPinPayload;
1284
+ timestamp?: number;
1285
+ }
1286
+ | {
1287
+ type: typeof RoomClientMessageType.DM_UNPIN;
1288
+ payload: RoomDmPinPayload;
1289
+ timestamp?: number;
1290
+ }
1291
+ | {
1292
+ type: typeof RoomClientMessageType.DM_PINS;
1293
+ payload: RoomDmPinsPayload;
1294
+ timestamp?: number;
1295
+ }
1211
1296
  | {
1212
1297
  type: typeof RoomClientMessageType.TOPIC_CREATE;
1213
1298
  payload: RoomTopicCreatePayload;
@@ -1460,6 +1545,16 @@ export type RoomServerMessage =
1460
1545
  payload: RoomDmReactionUpdatedPayload;
1461
1546
  timestamp: number;
1462
1547
  }
1548
+ | {
1549
+ type: typeof RoomServerMessageType.DM_PIN_UPDATED;
1550
+ payload: RoomDmPinUpdatedPayload;
1551
+ timestamp: number;
1552
+ }
1553
+ | {
1554
+ type: typeof RoomServerMessageType.DM_PINS;
1555
+ payload: RoomDmPinsResultPayload;
1556
+ timestamp: number;
1557
+ }
1463
1558
  | {
1464
1559
  type: typeof RoomServerMessageType.TOPIC;
1465
1560
  payload: RoomTopicResultPayload;
@@ -657,7 +657,8 @@ export async function listRoomSpacesViaChat(config: RoomSocialConfig): Promise<R
657
657
 
658
658
  export interface RoomSpaceMember {
659
659
  userId: string;
660
- role: "owner" | "admin" | "member";
660
+ /** The ladder role. 'librarian' maintains the generated briefs/ docs. */
661
+ role: "owner" | "admin" | "librarian" | "member";
661
662
  /** Named identity roles (RoomSpaceRole.roleId). */
662
663
  roleIds?: string[];
663
664
  joinedAt?: string;
@@ -1047,7 +1048,7 @@ export async function changeSpaceMemberRole(
1047
1048
  config: RoomSocialConfig,
1048
1049
  spaceId: string,
1049
1050
  targetUserId: string,
1050
- role: "admin" | "member",
1051
+ role: "admin" | "librarian" | "member",
1051
1052
  ): Promise<RoomSpaceMember> {
1052
1053
  const result = await socialRequest<{ member: RoomSpaceMember }>(
1053
1054
  config,
@@ -144,6 +144,8 @@ export interface RoomDmMessage {
144
144
  imageUrl?: string | null;
145
145
  /** Aggregated reactions (per-emoji userId lists). Additive. */
146
146
  reactions?: Array<{ emoji: string; users: string[] }>;
147
+ /** Shared pin state. Additive; absent means unpinned. */
148
+ pinned?: boolean;
147
149
  /** Client-minted idempotency tag echoed back on DM sends, so a sender's
148
150
  * durable queue can settle its optimistic row. Additive (0.4.0). */
149
151
  clientMsgId?: string;
package/src/types.ts CHANGED
@@ -15,14 +15,22 @@ export type {
15
15
  RoomDmHistoryResultPayload,
16
16
  RoomDmInboxUpdatedPayload,
17
17
  RoomDmMessage,
18
+ RoomDmMessageDeletedPayload,
19
+ RoomDmMessageEditedPayload,
18
20
  RoomDmMessagePayload,
19
21
  RoomDmOpenedPayload,
20
22
  RoomDmOpenPayload,
21
23
  RoomDmParticipant,
24
+ RoomDmPinsResultPayload,
25
+ RoomDmPinUpdatedPayload,
26
+ RoomDmReactionUpdatedPayload,
22
27
  RoomDmReadPayload,
28
+ RoomDmReplyPayload,
23
29
  RoomDmSendPayload,
24
30
  RoomEntryUpdatedPayload,
25
31
  RoomErrorPayload,
32
+ RoomForwardedFrom,
33
+ RoomForwardMetadata,
26
34
  RoomFriendPresence,
27
35
  RoomInvitedPayload,
28
36
  RoomInvitePayload,
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
- export const VERSION = "0.5.1";
2
- export const RUNNER_VERSION = "0.5.1";
1
+ export const VERSION = "0.7.0";
2
+ export const RUNNER_VERSION = "0.7.0";