@openmarket/rooms-client 0.3.0 → 0.4.1

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 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 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";
2
2
  import { type RoomWebSocketCtor, RoomWsClient } from "./ws-client.js";
3
3
  export interface RoomClientConfig {
4
4
  url: string;
@@ -213,6 +213,10 @@ export interface RoomDmSendRequest {
213
213
  * Additive — omitted for text-only sends. */
214
214
  imageUrl?: string;
215
215
  }
216
+ export interface RoomDmReadRequest {
217
+ username?: string;
218
+ conversationId?: string;
219
+ }
216
220
  export interface RoomThreadRequest {
217
221
  room: string;
218
222
  rootSeq: number;
@@ -254,6 +258,7 @@ export declare function whoamiRoom(config: RoomClientConfig): Promise<RoomAuthSu
254
258
  export declare function requestRoomFriends(client: RoomWsClient): Promise<RoomFriendPresence[]>;
255
259
  export declare function openRoomDmOnClient(client: RoomWsClient, request: RoomDmOpenRequest, timeoutMs?: number): Promise<RoomDmOpenedPayload>;
256
260
  export declare function requestRoomDmHistoryOnClient(client: RoomWsClient, request: RoomDmHistoryRequest, timeoutMs?: number): Promise<RoomDmHistoryResultPayload>;
261
+ export declare function markRoomDmReadOnClient(client: RoomWsClient, request: RoomDmReadRequest, timeoutMs?: number): Promise<RoomDmInboxUpdatedPayload>;
257
262
  export declare function sendRoomDmOnClient(client: RoomWsClient, request: RoomDmSendRequest, timeoutMs?: number): Promise<RoomDmMessagePayload>;
258
263
  /** Edit one of my DM messages. Fire-and-forget over the WS; the relay fans out
259
264
  * DM_MESSAGE_EDITED to both participants. */
@@ -283,9 +288,18 @@ export declare function joinRoom(config: RoomClientConfig, options: RoomJoinOpti
283
288
  /** Re-join a room on an already-connected client (used after a reconnect). */
284
289
  export declare function rejoinRoomOnClient(client: RoomWsClient, options: RoomJoinOptions): Promise<RoomBackfillPayload>;
285
290
  export declare function attachRejoinOnReconnect(client: RoomWsClient, options: RoomJoinOptions, latestSeq: () => number | undefined, onRejoined?: (backfill: RoomBackfillPayload) => void, onError?: (err: unknown) => void): () => void;
286
- export declare function postRoomMessage(client: RoomWsClient, room: string, text: string, inReplyTo?: number, attachments?: RoomAttachmentInput[], topicId?: string): string;
291
+ /**
292
+ * Post into a joined room and return the frame's clientMsgId (what MESSAGE
293
+ * echoes carry back for optimistic-send reconciliation). `clientMsgId` is
294
+ * caller-suppliable so an offline send queue can retry one logical message
295
+ * under one stable id (same contract as sayRoom / sendRoomDmOnClient);
296
+ * omitted, it mints a UUID exactly as before. The relay validates the id
297
+ * (1..128 chars after trim); dedupe on (userId, clientMsgId) is server-side
298
+ * work and NOT implied here.
299
+ */
300
+ export declare function postRoomMessage(client: RoomWsClient, room: string, text: string, inReplyTo?: number, attachments?: RoomAttachmentInput[], topicId?: string, clientMsgId?: string): string;
287
301
  export declare function sendRoomTyping(client: RoomWsClient, room: string, state: "start" | "stop"): void;
288
- export declare function sendRoomStatus(client: RoomWsClient, status: RoomUserStatus, statusText?: string | null): void;
302
+ export declare function sendRoomStatus(client: RoomWsClient, status: RoomUserStatus, statusText?: string | null, platform?: RoomClientPlatform): void;
289
303
  /**
290
304
  * Fetch a joined room's meta (title, access, postingRoles, viewerRole) over
291
305
  * the live socket via a scoped SEARCH. Returns null on timeout/no match so
@@ -310,7 +324,18 @@ export declare function requestRoomReactors(client: RoomWsClient, request: {
310
324
  * persists it durably; a one-shot socket has no joined rooms, so live
311
325
  * rosters pick the change up from the store (the TUI/GUI send STATUS on
312
326
  * their live connection instead, which also broadcasts PRESENCE_DELTA). */
313
- export declare function setRoomStatusOnce(config: RoomClientConfig, status: RoomUserStatus, statusText?: string | null): Promise<void>;
327
+ export declare function setRoomStatusOnce(config: RoomClientConfig, status: RoomUserStatus, statusText?: string | null, platform?: RoomClientPlatform): Promise<void>;
328
+ /**
329
+ * Set the caller's room-level attention (notification posture) on a live
330
+ * client. The relay validates room participation, persists through the
331
+ * store, and acks with a READ_STATE frame scoped to this room whose
332
+ * attention map carries the persisted value (readState stays empty: cursors
333
+ * are untouched). Private per-user state; nothing broadcasts.
334
+ */
335
+ export declare function setRoomAttentionOnClient(client: RoomWsClient, request: {
336
+ room: string;
337
+ attention: RoomTopicAttention;
338
+ }): Promise<RoomReadStatePayload>;
314
339
  /** Add/remove one reaction (one-shot). Returns the updated entry. */
315
340
  export declare function reactRoom(config: RoomClientConfig, options: {
316
341
  room: string;
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();
@@ -618,21 +632,30 @@ export function attachRejoinOnReconnect(client, options, latestSeq, onRejoined,
618
632
  .catch((err) => onError?.(err));
619
633
  });
620
634
  }
621
- export function postRoomMessage(client, room, text, inReplyTo, attachments, topicId) {
622
- const clientMsgId = randomUUID();
635
+ /**
636
+ * Post into a joined room and return the frame's clientMsgId (what MESSAGE
637
+ * echoes carry back for optimistic-send reconciliation). `clientMsgId` is
638
+ * caller-suppliable so an offline send queue can retry one logical message
639
+ * under one stable id (same contract as sayRoom / sendRoomDmOnClient);
640
+ * omitted, it mints a UUID exactly as before. The relay validates the id
641
+ * (1..128 chars after trim); dedupe on (userId, clientMsgId) is server-side
642
+ * work and NOT implied here.
643
+ */
644
+ export function postRoomMessage(client, room, text, inReplyTo, attachments, topicId, clientMsgId) {
645
+ const msgId = clientMsgId ?? randomUUID();
623
646
  client.send({
624
647
  type: RoomClientMessageType.POST,
625
648
  payload: {
626
649
  room,
627
650
  ...(text ? { text } : {}),
628
651
  ...(attachments?.length ? { attachments: toRoomAttachmentPayloads(attachments) } : {}),
629
- clientMsgId,
652
+ clientMsgId: msgId,
630
653
  ...(inReplyTo !== undefined ? { inReplyTo } : {}),
631
654
  ...(topicId ? { topicId } : {}),
632
655
  },
633
656
  timestamp: Date.now(),
634
657
  });
635
- return clientMsgId;
658
+ return msgId;
636
659
  }
637
660
  export function sendRoomTyping(client, room, state) {
638
661
  if (client.isGuest())
@@ -643,10 +666,16 @@ export function sendRoomTyping(client, room, state) {
643
666
  timestamp: Date.now(),
644
667
  });
645
668
  }
646
- export function sendRoomStatus(client, status, statusText) {
669
+ export function sendRoomStatus(client, status, statusText, platform) {
647
670
  client.send({
648
671
  type: RoomClientMessageType.STATUS,
649
- payload: { status, ...(statusText !== undefined ? { statusText } : {}) },
672
+ payload: {
673
+ status,
674
+ ...(statusText !== undefined ? { statusText } : {}),
675
+ // Session identity, declared once per connection (or on change): the
676
+ // relay carries it onto presence member entries.
677
+ ...(platform ? { platform } : {}),
678
+ },
650
679
  timestamp: Date.now(),
651
680
  });
652
681
  }
@@ -769,11 +798,11 @@ export async function requestRoomReactors(client, request) {
769
798
  * persists it durably; a one-shot socket has no joined rooms, so live
770
799
  * rosters pick the change up from the store (the TUI/GUI send STATUS on
771
800
  * their live connection instead, which also broadcasts PRESENCE_DELTA). */
772
- export async function setRoomStatusOnce(config, status, statusText) {
801
+ export async function setRoomStatusOnce(config, status, statusText, platform) {
773
802
  const client = new RoomWsClient(config);
774
803
  try {
775
804
  await client.connect();
776
- sendRoomStatus(client, status, statusText);
805
+ sendRoomStatus(client, status, statusText, platform);
777
806
  // STATUS has no ack frame: give the socket a beat to flush before close.
778
807
  await new Promise((resolve) => setTimeout(resolve, 200));
779
808
  }
@@ -781,6 +810,24 @@ export async function setRoomStatusOnce(config, status, statusText) {
781
810
  client.close();
782
811
  }
783
812
  }
813
+ /**
814
+ * Set the caller's room-level attention (notification posture) on a live
815
+ * client. The relay validates room participation, persists through the
816
+ * store, and acks with a READ_STATE frame scoped to this room whose
817
+ * attention map carries the persisted value (readState stays empty: cursors
818
+ * are untouched). Private per-user state; nothing broadcasts.
819
+ */
820
+ export async function setRoomAttentionOnClient(client, request) {
821
+ const requestId = randomUUID();
822
+ const room = normalizeRoomName(request.room);
823
+ client.send({
824
+ type: RoomClientMessageType.ROOM_SET_ATTENTION,
825
+ payload: { requestId, room, attention: request.attention },
826
+ timestamp: Date.now(),
827
+ });
828
+ const result = await client.waitFor(RoomServerMessageType.READ_STATE, (msg) => msg.payload.requestId === requestId, undefined, requestId);
829
+ return result.payload;
830
+ }
784
831
  /** One-shot session helper for the config-based mutation verbs below (the
785
832
  * agent tools' transport: connect, join, act, wait for the echo, close). */
786
833
  async function withRoomEcho(config, room, seq, send) {
@@ -48,6 +48,7 @@ export declare const RoomClientMessageType: {
48
48
  readonly TOPIC_RESOLVE: "TOPIC_RESOLVE";
49
49
  readonly TOPIC_SET_ATTENTION: "TOPIC_SET_ATTENTION";
50
50
  readonly TOPIC_LIST: "TOPIC_LIST";
51
+ readonly ROOM_SET_ATTENTION: "ROOM_SET_ATTENTION";
51
52
  };
52
53
  export type RoomClientMessageType = (typeof RoomClientMessageType)[keyof typeof RoomClientMessageType];
53
54
  export declare const RoomServerMessageType: {
@@ -127,6 +128,9 @@ export type RoomRole = "owner" | "admin" | "mod" | "member";
127
128
  * semantics of an absent value). */
128
129
  export type RoomTopicsPolicy = "off" | "allowed" | "required";
129
130
  export type RoomTopicAttention = "follow" | "default" | "mute";
131
+ /** Self-declared client class on STATUS frames; presence member entries
132
+ * carry it through so rosters can render where someone is connected from. */
133
+ export type RoomClientPlatform = "web" | "desktop" | "mobile" | "tui";
130
134
  export interface RoomMember {
131
135
  userId: string;
132
136
  username: string;
@@ -140,6 +144,8 @@ export interface RoomMember {
140
144
  joinedAt: number;
141
145
  avatarUrl?: string | null;
142
146
  statusText?: string | null;
147
+ /** Present when the member's session declared one via STATUS. */
148
+ platform?: RoomClientPlatform | undefined;
143
149
  }
144
150
  export interface RoomMeta {
145
151
  name: string;
@@ -164,6 +170,10 @@ export interface RoomMeta {
164
170
  * 2026-07-12 dial-default flip (post-flip relays always advertise the
165
171
  * stored value, "off" included); explicit "off" means topics are inert. */
166
172
  topicsPolicy?: RoomTopicsPolicy | undefined;
173
+ /** The viewer's room attention (notification posture), a per-viewer field
174
+ * like viewerRole: surfaced on rooms listings for the caller. Absent
175
+ * reads as "default". */
176
+ attention?: RoomTopicAttention | undefined;
167
177
  tags?: string[] | undefined;
168
178
  entities?: string[] | undefined;
169
179
  sourceRefs?: Array<Record<string, unknown>> | undefined;
@@ -295,7 +305,12 @@ export interface RoomPinsPayload {
295
305
  requestId?: string | undefined;
296
306
  }
297
307
  export interface RoomReadStatePayload {
308
+ /** Partial map: rooms absent from a reply are unchanged. */
298
309
  readState: Record<string, number>;
310
+ /** Per-room attention for the requesting user, keyed like readState.
311
+ * Present when the store surfaces it and on ROOM_SET_ATTENTION acks
312
+ * (which reply READ_STATE scoped to the one room, readState empty). */
313
+ attention?: Record<string, RoomTopicAttention> | undefined;
299
314
  requestId?: string | undefined;
300
315
  }
301
316
  export interface RoomSearchResultsPayload {
@@ -560,6 +575,7 @@ export interface RoomDmMessagePayload {
560
575
  clientMsgId?: string;
561
576
  }
562
577
  export interface RoomDmInboxUpdatedPayload {
578
+ requestId?: string;
563
579
  conversationId: string;
564
580
  participant: RoomDmParticipant;
565
581
  unreadCount: number;
@@ -743,6 +759,15 @@ export interface RoomTopicSetAttentionPayload {
743
759
  topicId: string;
744
760
  attention: RoomTopicAttention;
745
761
  }
762
+ /** Room-level attention (notification posture). Private per-user state on
763
+ * the caller's read row; topic attention wins for topic-scoped messages
764
+ * and room attention wins over space attention. The relay acks with a
765
+ * READ_STATE frame scoped to the room (see RoomReadStatePayload). */
766
+ export interface RoomSetAttentionPayload {
767
+ requestId?: string;
768
+ room: string;
769
+ attention: RoomTopicAttention;
770
+ }
746
771
  export interface RoomTopicListPayload {
747
772
  requestId?: string;
748
773
  room: string;
@@ -1017,6 +1042,7 @@ export type RoomClientMessage = {
1017
1042
  payload: {
1018
1043
  status: RoomUserStatus;
1019
1044
  statusText?: string | null;
1045
+ platform?: RoomClientPlatform;
1020
1046
  };
1021
1047
  timestamp?: number;
1022
1048
  } | {
@@ -1092,6 +1118,10 @@ export type RoomClientMessage = {
1092
1118
  type: typeof RoomClientMessageType.TOPIC_LIST;
1093
1119
  payload: RoomTopicListPayload;
1094
1120
  timestamp?: number;
1121
+ } | {
1122
+ type: typeof RoomClientMessageType.ROOM_SET_ATTENTION;
1123
+ payload: RoomSetAttentionPayload;
1124
+ timestamp?: number;
1095
1125
  };
1096
1126
  export type RoomServerMessage = {
1097
1127
  type: typeof RoomServerMessageType.AUTH_SUCCESS;
@@ -48,6 +48,7 @@ export const RoomClientMessageType = {
48
48
  TOPIC_RESOLVE: "TOPIC_RESOLVE",
49
49
  TOPIC_SET_ATTENTION: "TOPIC_SET_ATTENTION",
50
50
  TOPIC_LIST: "TOPIC_LIST",
51
+ ROOM_SET_ATTENTION: "ROOM_SET_ATTENTION",
51
52
  };
52
53
  export const RoomServerMessageType = {
53
54
  AUTH_SUCCESS: "AUTH_SUCCESS",
@@ -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,9 @@ export interface RoomDmMessage {
130
130
  emoji: string;
131
131
  users: string[];
132
132
  }>;
133
+ /** Client-minted idempotency tag echoed back on DM sends, so a sender's
134
+ * durable queue can settle its optimistic row. Additive (0.4.0). */
135
+ clientMsgId?: string;
133
136
  }
134
137
  export interface RoomDmHistory {
135
138
  conversationId: string;
package/dist/types.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export type { RoomAccessMode, RoomAttachment, RoomAttachmentInput, RoomAuditEntry, RoomAuditLogPayload, RoomAuditLogRequestPayload, RoomAuthSuccessPayload, RoomBackfillPayload, RoomClientMessage, 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, 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, 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";
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.6.0";
2
+ export declare const RUNNER_VERSION = "0.6.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.6.0";
2
+ export const RUNNER_VERSION = "0.6.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmarket/rooms-client",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
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",
@@ -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
@@ -13,7 +13,9 @@ import {
13
13
  type RoomBackfillPayload,
14
14
  type RoomClientMessage,
15
15
  RoomClientMessageType,
16
+ type RoomClientPlatform,
16
17
  type RoomDmHistoryResultPayload,
18
+ type RoomDmInboxUpdatedPayload,
17
19
  type RoomDmMessagePayload,
18
20
  type RoomDmOpenedPayload,
19
21
  type RoomFriendPresence,
@@ -292,6 +294,11 @@ export interface RoomDmSendRequest {
292
294
  imageUrl?: string;
293
295
  }
294
296
 
297
+ export interface RoomDmReadRequest {
298
+ username?: string;
299
+ conversationId?: string;
300
+ }
301
+
295
302
  export interface RoomThreadRequest {
296
303
  room: string;
297
304
  rootSeq: number;
@@ -922,6 +929,30 @@ export async function requestRoomDmHistoryOnClient(
922
929
  return history.payload;
923
930
  }
924
931
 
932
+ export async function markRoomDmReadOnClient(
933
+ client: RoomWsClient,
934
+ request: RoomDmReadRequest,
935
+ timeoutMs?: number,
936
+ ): Promise<RoomDmInboxUpdatedPayload> {
937
+ const requestId = randomUUID();
938
+ client.send({
939
+ type: RoomClientMessageType.DM_READ,
940
+ payload: {
941
+ requestId,
942
+ ...(request.username ? { username: request.username } : {}),
943
+ ...(request.conversationId ? { conversationId: request.conversationId } : {}),
944
+ },
945
+ timestamp: Date.now(),
946
+ });
947
+ const updated = await client.waitFor(
948
+ RoomServerMessageType.DM_INBOX_UPDATED,
949
+ (msg) => msg.payload.requestId === requestId,
950
+ timeoutMs,
951
+ requestId,
952
+ );
953
+ return updated.payload;
954
+ }
955
+
925
956
  export async function sendRoomDmOnClient(
926
957
  client: RoomWsClient,
927
958
  request: RoomDmSendRequest,
@@ -1147,6 +1178,15 @@ export function attachRejoinOnReconnect(
1147
1178
  });
1148
1179
  }
1149
1180
 
1181
+ /**
1182
+ * Post into a joined room and return the frame's clientMsgId (what MESSAGE
1183
+ * echoes carry back for optimistic-send reconciliation). `clientMsgId` is
1184
+ * caller-suppliable so an offline send queue can retry one logical message
1185
+ * under one stable id (same contract as sayRoom / sendRoomDmOnClient);
1186
+ * omitted, it mints a UUID exactly as before. The relay validates the id
1187
+ * (1..128 chars after trim); dedupe on (userId, clientMsgId) is server-side
1188
+ * work and NOT implied here.
1189
+ */
1150
1190
  export function postRoomMessage(
1151
1191
  client: RoomWsClient,
1152
1192
  room: string,
@@ -1154,21 +1194,22 @@ export function postRoomMessage(
1154
1194
  inReplyTo?: number,
1155
1195
  attachments?: RoomAttachmentInput[],
1156
1196
  topicId?: string,
1197
+ clientMsgId?: string,
1157
1198
  ): string {
1158
- const clientMsgId = randomUUID();
1199
+ const msgId = clientMsgId ?? randomUUID();
1159
1200
  client.send({
1160
1201
  type: RoomClientMessageType.POST,
1161
1202
  payload: {
1162
1203
  room,
1163
1204
  ...(text ? { text } : {}),
1164
1205
  ...(attachments?.length ? { attachments: toRoomAttachmentPayloads(attachments) } : {}),
1165
- clientMsgId,
1206
+ clientMsgId: msgId,
1166
1207
  ...(inReplyTo !== undefined ? { inReplyTo } : {}),
1167
1208
  ...(topicId ? { topicId } : {}),
1168
1209
  },
1169
1210
  timestamp: Date.now(),
1170
1211
  });
1171
- return clientMsgId;
1212
+ return msgId;
1172
1213
  }
1173
1214
 
1174
1215
  export function sendRoomTyping(client: RoomWsClient, room: string, state: "start" | "stop"): void {
@@ -1184,10 +1225,17 @@ export function sendRoomStatus(
1184
1225
  client: RoomWsClient,
1185
1226
  status: RoomUserStatus,
1186
1227
  statusText?: string | null,
1228
+ platform?: RoomClientPlatform,
1187
1229
  ): void {
1188
1230
  client.send({
1189
1231
  type: RoomClientMessageType.STATUS,
1190
- payload: { status, ...(statusText !== undefined ? { statusText } : {}) },
1232
+ payload: {
1233
+ status,
1234
+ ...(statusText !== undefined ? { statusText } : {}),
1235
+ // Session identity, declared once per connection (or on change): the
1236
+ // relay carries it onto presence member entries.
1237
+ ...(platform ? { platform } : {}),
1238
+ },
1191
1239
  timestamp: Date.now(),
1192
1240
  });
1193
1241
  }
@@ -1393,11 +1441,12 @@ export async function setRoomStatusOnce(
1393
1441
  config: RoomClientConfig,
1394
1442
  status: RoomUserStatus,
1395
1443
  statusText?: string | null,
1444
+ platform?: RoomClientPlatform,
1396
1445
  ): Promise<void> {
1397
1446
  const client = new RoomWsClient(config);
1398
1447
  try {
1399
1448
  await client.connect();
1400
- sendRoomStatus(client, status, statusText);
1449
+ sendRoomStatus(client, status, statusText, platform);
1401
1450
  // STATUS has no ack frame: give the socket a beat to flush before close.
1402
1451
  await new Promise((resolve) => setTimeout(resolve, 200));
1403
1452
  } finally {
@@ -1405,6 +1454,33 @@ export async function setRoomStatusOnce(
1405
1454
  }
1406
1455
  }
1407
1456
 
1457
+ /**
1458
+ * Set the caller's room-level attention (notification posture) on a live
1459
+ * client. The relay validates room participation, persists through the
1460
+ * store, and acks with a READ_STATE frame scoped to this room whose
1461
+ * attention map carries the persisted value (readState stays empty: cursors
1462
+ * are untouched). Private per-user state; nothing broadcasts.
1463
+ */
1464
+ export async function setRoomAttentionOnClient(
1465
+ client: RoomWsClient,
1466
+ request: { room: string; attention: RoomTopicAttention },
1467
+ ): Promise<RoomReadStatePayload> {
1468
+ const requestId = randomUUID();
1469
+ const room = normalizeRoomName(request.room);
1470
+ client.send({
1471
+ type: RoomClientMessageType.ROOM_SET_ATTENTION,
1472
+ payload: { requestId, room, attention: request.attention },
1473
+ timestamp: Date.now(),
1474
+ });
1475
+ const result = await client.waitFor(
1476
+ RoomServerMessageType.READ_STATE,
1477
+ (msg) => msg.payload.requestId === requestId,
1478
+ undefined,
1479
+ requestId,
1480
+ );
1481
+ return result.payload;
1482
+ }
1483
+
1408
1484
  /** One-shot session helper for the config-based mutation verbs below (the
1409
1485
  * agent tools' transport: connect, join, act, wait for the echo, close). */
1410
1486
  async function withRoomEcho(
@@ -48,6 +48,7 @@ export const RoomClientMessageType = {
48
48
  TOPIC_RESOLVE: "TOPIC_RESOLVE",
49
49
  TOPIC_SET_ATTENTION: "TOPIC_SET_ATTENTION",
50
50
  TOPIC_LIST: "TOPIC_LIST",
51
+ ROOM_SET_ATTENTION: "ROOM_SET_ATTENTION",
51
52
  } as const;
52
53
 
53
54
  export type RoomClientMessageType =
@@ -152,6 +153,9 @@ export type RoomRole = "owner" | "admin" | "mod" | "member";
152
153
  * semantics of an absent value). */
153
154
  export type RoomTopicsPolicy = "off" | "allowed" | "required";
154
155
  export type RoomTopicAttention = "follow" | "default" | "mute";
156
+ /** Self-declared client class on STATUS frames; presence member entries
157
+ * carry it through so rosters can render where someone is connected from. */
158
+ export type RoomClientPlatform = "web" | "desktop" | "mobile" | "tui";
155
159
 
156
160
  export interface RoomMember {
157
161
  userId: string;
@@ -166,6 +170,8 @@ export interface RoomMember {
166
170
  joinedAt: number;
167
171
  avatarUrl?: string | null;
168
172
  statusText?: string | null;
173
+ /** Present when the member's session declared one via STATUS. */
174
+ platform?: RoomClientPlatform | undefined;
169
175
  }
170
176
 
171
177
  export interface RoomMeta {
@@ -191,6 +197,10 @@ export interface RoomMeta {
191
197
  * 2026-07-12 dial-default flip (post-flip relays always advertise the
192
198
  * stored value, "off" included); explicit "off" means topics are inert. */
193
199
  topicsPolicy?: RoomTopicsPolicy | undefined;
200
+ /** The viewer's room attention (notification posture), a per-viewer field
201
+ * like viewerRole: surfaced on rooms listings for the caller. Absent
202
+ * reads as "default". */
203
+ attention?: RoomTopicAttention | undefined;
194
204
  tags?: string[] | undefined;
195
205
  entities?: string[] | undefined;
196
206
  sourceRefs?: Array<Record<string, unknown>> | undefined;
@@ -331,7 +341,12 @@ export interface RoomPinsPayload {
331
341
  }
332
342
 
333
343
  export interface RoomReadStatePayload {
344
+ /** Partial map: rooms absent from a reply are unchanged. */
334
345
  readState: Record<string, number>;
346
+ /** Per-room attention for the requesting user, keyed like readState.
347
+ * Present when the store surfaces it and on ROOM_SET_ATTENTION acks
348
+ * (which reply READ_STATE scoped to the one room, readState empty). */
349
+ attention?: Record<string, RoomTopicAttention> | undefined;
335
350
  requestId?: string | undefined;
336
351
  }
337
352
 
@@ -659,6 +674,7 @@ export interface RoomDmMessagePayload {
659
674
  }
660
675
 
661
676
  export interface RoomDmInboxUpdatedPayload {
677
+ requestId?: string;
662
678
  conversationId: string;
663
679
  participant: RoomDmParticipant;
664
680
  unreadCount: number;
@@ -862,6 +878,16 @@ export interface RoomTopicSetAttentionPayload {
862
878
  attention: RoomTopicAttention;
863
879
  }
864
880
 
881
+ /** Room-level attention (notification posture). Private per-user state on
882
+ * the caller's read row; topic attention wins for topic-scoped messages
883
+ * and room attention wins over space attention. The relay acks with a
884
+ * READ_STATE frame scoped to the room (see RoomReadStatePayload). */
885
+ export interface RoomSetAttentionPayload {
886
+ requestId?: string;
887
+ room: string;
888
+ attention: RoomTopicAttention;
889
+ }
890
+
865
891
  export interface RoomTopicListPayload {
866
892
  requestId?: string;
867
893
  room: string;
@@ -1129,7 +1155,11 @@ export type RoomClientMessage =
1129
1155
  }
1130
1156
  | {
1131
1157
  type: typeof RoomClientMessageType.STATUS;
1132
- payload: { status: RoomUserStatus; statusText?: string | null };
1158
+ payload: {
1159
+ status: RoomUserStatus;
1160
+ statusText?: string | null;
1161
+ platform?: RoomClientPlatform;
1162
+ };
1133
1163
  timestamp?: number;
1134
1164
  }
1135
1165
  | {
@@ -1213,6 +1243,11 @@ export type RoomClientMessage =
1213
1243
  type: typeof RoomClientMessageType.TOPIC_LIST;
1214
1244
  payload: RoomTopicListPayload;
1215
1245
  timestamp?: number;
1246
+ }
1247
+ | {
1248
+ type: typeof RoomClientMessageType.ROOM_SET_ATTENTION;
1249
+ payload: RoomSetAttentionPayload;
1250
+ timestamp?: number;
1216
1251
  };
1217
1252
 
1218
1253
  export type RoomServerMessage =
@@ -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,9 @@ 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
+ /** Client-minted idempotency tag echoed back on DM sends, so a sender's
148
+ * durable queue can settle its optimistic row. Additive (0.4.0). */
149
+ clientMsgId?: string;
147
150
  }
148
151
 
149
152
  export interface RoomDmHistory {
package/src/types.ts CHANGED
@@ -8,6 +8,7 @@ export type {
8
8
  RoomAuthSuccessPayload,
9
9
  RoomBackfillPayload,
10
10
  RoomClientMessage,
11
+ RoomClientPlatform,
11
12
  RoomCreatedPayload,
12
13
  RoomCreatePayload,
13
14
  RoomDmHistoryPayload,
@@ -50,6 +51,7 @@ export type {
50
51
  RoomSearchScope,
51
52
  RoomServerMessage,
52
53
  RoomServerMessageOf,
54
+ RoomSetAttentionPayload,
53
55
  RoomSetReadOnlyPayload,
54
56
  RoomSpace,
55
57
  RoomSpaceJoinedPayload,
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.6.0";
2
+ export const RUNNER_VERSION = "0.6.0";