@openmarket/rooms-client 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/armed-mode.d.ts +0 -8
- package/dist/agent/armed-mode.js +19 -3
- package/dist/agent/clients/common.d.ts +13 -1
- package/dist/agent/clients/common.js +11 -1
- package/dist/agent/topic-inbox.d.ts +6 -0
- package/dist/agent/topic-inbox.js +20 -5
- package/dist/shared/rooms-protocol.d.ts +96 -2
- package/dist/social-client.d.ts +202 -5
- package/dist/social-client.js +372 -5
- package/dist/social-types.d.ts +164 -1
- package/dist/types.d.ts +1 -1
- package/package.json +2 -7
- package/src/agent/armed-mode.ts +20 -3
- package/src/agent/clients/common.ts +18 -2
- package/src/agent/topic-inbox.ts +23 -5
- package/src/shared/rooms-protocol.ts +100 -2
- package/src/social-client.ts +653 -6
- package/src/social-types.ts +175 -1
- package/src/types.ts +3 -0
package/src/agent/armed-mode.ts
CHANGED
|
@@ -164,6 +164,17 @@ export function ownSeqsOf(entries: ReadonlyArray<RoomLedgerEntry>, ownUserId: st
|
|
|
164
164
|
* to the user. Agent posts, system entries, the user's own posts, and
|
|
165
165
|
* non-mentions never trigger. Pure.
|
|
166
166
|
*/
|
|
167
|
+
/** Webhook-ingested entries are machine posts even though isAgent is
|
|
168
|
+
* false (the flag marks agent USERS). They must never count as the human
|
|
169
|
+
* in any autonomy gate: a webhook @mention cannot start an unreviewed
|
|
170
|
+
* autonomous turn, and webhook chatter cannot satisfy the
|
|
171
|
+
* human-activity brake. */
|
|
172
|
+
function isWebhookEntry(entry: RoomLedgerEntry): boolean {
|
|
173
|
+
return Boolean(
|
|
174
|
+
(entry.metadata as { webhook?: unknown } | undefined)?.webhook,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
167
178
|
export function isTriggeringEntry(
|
|
168
179
|
entry: RoomLedgerEntry,
|
|
169
180
|
ownHandle: string,
|
|
@@ -172,7 +183,10 @@ export function isTriggeringEntry(
|
|
|
172
183
|
): boolean {
|
|
173
184
|
if (entry.type !== RoomLedgerEntryType.POST) return false;
|
|
174
185
|
if (entry.isAgent) return false;
|
|
175
|
-
if (
|
|
186
|
+
if (isWebhookEntry(entry)) return false;
|
|
187
|
+
// A poll is substantive content even with no caption, and a captionless
|
|
188
|
+
// poll REPLY is addressed at us like any other reply.
|
|
189
|
+
if (!entry.text?.trim() && entry.poll == null) return false;
|
|
176
190
|
// Never trigger on our own message (we post as this handle/userId).
|
|
177
191
|
const fromHandle = normalizeHandle(entry.handle).toLowerCase();
|
|
178
192
|
const me = normalizeHandle(ownHandle).toLowerCase();
|
|
@@ -185,7 +199,9 @@ export function isTriggeringEntry(
|
|
|
185
199
|
// old mentionsAgent) fired on ordinary words for short handles like "may" /
|
|
186
200
|
// "btc"; armed posting has no human review, so it triggers ONLY on an `@handle`
|
|
187
201
|
// token. Empty handle never matches (mentionsAgentExplicit guards it).
|
|
188
|
-
|
|
202
|
+
// Mentions can live in the poll question too (member-authored content).
|
|
203
|
+
const mentionSource = [entry.text ?? "", entry.poll?.question ?? ""].filter(Boolean).join(" ");
|
|
204
|
+
return mentionsAgentExplicit(mentionSource, ownHandle);
|
|
189
205
|
}
|
|
190
206
|
|
|
191
207
|
/**
|
|
@@ -379,11 +395,12 @@ export function isHumanResetEntry(
|
|
|
379
395
|
): boolean {
|
|
380
396
|
if (entry.type !== RoomLedgerEntryType.POST) return false;
|
|
381
397
|
if (entry.isAgent) return false;
|
|
398
|
+
if (isWebhookEntry(entry)) return false;
|
|
382
399
|
if (entry.userId === ownUserId) return false;
|
|
383
400
|
const fromHandle = normalizeHandle(entry.handle).toLowerCase();
|
|
384
401
|
const me = normalizeHandle(ownHandle).toLowerCase();
|
|
385
402
|
if (me.length > 0 && fromHandle === me) return false;
|
|
386
|
-
return Boolean(entry.text?.trim());
|
|
403
|
+
return Boolean(entry.text?.trim()) || entry.poll != null;
|
|
387
404
|
}
|
|
388
405
|
|
|
389
406
|
/** Note a turn/post error against the kill switch; returns true once the room
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { getProviders, type KnownProvider } from "@earendil-works/pi-ai";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/** Every chat surface, as a runtime list. The `SurfaceId` union derives from
|
|
4
|
+
* this literal so per-surface policy checklists (packages/cli/test/
|
|
5
|
+
* harness-budgets.test.ts) can iterate the surfaces at runtime while a new
|
|
6
|
+
* entry still breaks type-exhaustive tables at compile time. Adding a
|
|
7
|
+
* surface here requires classifying it in EVERY policy set (see AGENTS.md,
|
|
8
|
+
* "Harness invariants"). */
|
|
9
|
+
export const SURFACE_IDS = ["cli", "telegram", "discord", "slack", "webui"] as const;
|
|
10
|
+
|
|
11
|
+
export type SurfaceId = (typeof SURFACE_IDS)[number];
|
|
4
12
|
|
|
5
13
|
export interface AgentToolCall {
|
|
6
14
|
id: string;
|
|
@@ -16,6 +24,11 @@ export type AgentMessage =
|
|
|
16
24
|
export interface ToolDispatchResult {
|
|
17
25
|
isError?: true;
|
|
18
26
|
content: string;
|
|
27
|
+
/** Full-fidelity render payload for in-process SURFACES (e.g. the TUI's
|
|
28
|
+
* backtest card). Present only on the streamed copy channels consume —
|
|
29
|
+
* the agent runtime strips it before the result reaches the model's
|
|
30
|
+
* messages or the conversation store, so it never costs context. */
|
|
31
|
+
artifact?: unknown;
|
|
19
32
|
}
|
|
20
33
|
|
|
21
34
|
export interface LlmTool {
|
|
@@ -79,7 +92,10 @@ export type RunLlmTurn = (input: RunLlmTurnInput) => AsyncIterable<TurnEvent>;
|
|
|
79
92
|
export function stringifyToolContent(value: unknown): string {
|
|
80
93
|
if (value === undefined) return "";
|
|
81
94
|
if (typeof value === "string") return value;
|
|
82
|
-
|
|
95
|
+
// Compact on purpose: this string is LLM input, and 2-space pretty-printing
|
|
96
|
+
// inflated every structured tool result by roughly a fifth in tokens for
|
|
97
|
+
// zero model benefit.
|
|
98
|
+
return JSON.stringify(value);
|
|
83
99
|
}
|
|
84
100
|
|
|
85
101
|
export function parseToolArguments(raw: unknown): unknown {
|
package/src/agent/topic-inbox.ts
CHANGED
|
@@ -26,6 +26,20 @@ import type {
|
|
|
26
26
|
/** The reserved bucket key for untopicked (linear) messages. */
|
|
27
27
|
export const UNTOPICKED_BUCKET = "";
|
|
28
28
|
|
|
29
|
+
/** Effective attention for a bucket: an elapsed timed mute reads as the
|
|
30
|
+
* base attention ('default'). The store expires timed mutes lazily (no
|
|
31
|
+
* deadline-triggered frame), so a hydrated inbox would otherwise stay
|
|
32
|
+
* muted past the deadline until the next hydration. A null or absent
|
|
33
|
+
* deadline keeps the stored attention (permanent mute). */
|
|
34
|
+
export function resolveTopicAttention(
|
|
35
|
+
state: Pick<RoomTopicBucketState, "attention" | "muteUntil">,
|
|
36
|
+
nowMs: number = Date.now(),
|
|
37
|
+
): RoomTopicAttention {
|
|
38
|
+
if (state.attention !== "mute" || state.muteUntil == null) return state.attention;
|
|
39
|
+
const deadline = Date.parse(state.muteUntil);
|
|
40
|
+
return Number.isFinite(deadline) && deadline <= nowMs ? "default" : "mute";
|
|
41
|
+
}
|
|
42
|
+
|
|
29
43
|
export interface TopicInboxItem {
|
|
30
44
|
/** Bucket key: a topicId, or UNTOPICKED_BUCKET for the linear lane. */
|
|
31
45
|
key: string;
|
|
@@ -141,7 +155,7 @@ export class RoomTopicInbox {
|
|
|
141
155
|
}
|
|
142
156
|
|
|
143
157
|
attentionFor(topicId: string): RoomTopicAttention {
|
|
144
|
-
return this.bucketFor(topicId)
|
|
158
|
+
return resolveTopicAttention(this.bucketFor(topicId));
|
|
145
159
|
}
|
|
146
160
|
|
|
147
161
|
/** A registry mutation arrived (TOPIC_UPDATED, or a verb result echo). */
|
|
@@ -196,6 +210,10 @@ export class RoomTopicInbox {
|
|
|
196
210
|
this.buckets.set(topicId, bucket);
|
|
197
211
|
}
|
|
198
212
|
bucket.attention = attention;
|
|
213
|
+
// The mirror knows only the new attention: a stale deadline from a
|
|
214
|
+
// previous timed mute must not expire it. The next hydration restores
|
|
215
|
+
// any server-side deadline.
|
|
216
|
+
delete bucket.muteUntil;
|
|
199
217
|
}
|
|
200
218
|
|
|
201
219
|
/**
|
|
@@ -224,7 +242,7 @@ export class RoomTopicInbox {
|
|
|
224
242
|
channelUnread(): number {
|
|
225
243
|
let total = 0;
|
|
226
244
|
for (const bucket of this.buckets.values()) {
|
|
227
|
-
if (bucket
|
|
245
|
+
if (resolveTopicAttention(bucket) === "mute") continue;
|
|
228
246
|
total += bucket.unreadCount;
|
|
229
247
|
}
|
|
230
248
|
return total;
|
|
@@ -241,7 +259,7 @@ export class RoomTopicInbox {
|
|
|
241
259
|
inboxItems(): TopicInboxItem[] {
|
|
242
260
|
const items: TopicInboxItem[] = [];
|
|
243
261
|
const untopicked = this.bucketFor(UNTOPICKED_BUCKET);
|
|
244
|
-
if (untopicked.unreadCount > 0 && untopicked
|
|
262
|
+
if (untopicked.unreadCount > 0 && resolveTopicAttention(untopicked) !== "mute") {
|
|
245
263
|
items.push({
|
|
246
264
|
key: UNTOPICKED_BUCKET,
|
|
247
265
|
topic: undefined,
|
|
@@ -254,11 +272,11 @@ export class RoomTopicInbox {
|
|
|
254
272
|
for (const topic of this.topics.values()) {
|
|
255
273
|
if (topic.mergedInto) continue;
|
|
256
274
|
const state = this.bucketFor(topic.topicId);
|
|
257
|
-
if (state
|
|
275
|
+
if (resolveTopicAttention(state) === "mute") continue;
|
|
258
276
|
let aliasUnread = 0;
|
|
259
277
|
for (const alias of this.aliasesOf(topic.topicId)) {
|
|
260
278
|
const aliasState = this.bucketFor(alias);
|
|
261
|
-
if (aliasState
|
|
279
|
+
if (resolveTopicAttention(aliasState) === "mute") continue;
|
|
262
280
|
aliasUnread += aliasState.unreadCount;
|
|
263
281
|
}
|
|
264
282
|
if (state.unreadCount + aliasUnread === 0) continue;
|
|
@@ -250,6 +250,19 @@ export interface RoomLedgerEntry {
|
|
|
250
250
|
since?: string | number | null | undefined;
|
|
251
251
|
reactions?: Array<{ emoji: string; count: number }> | undefined;
|
|
252
252
|
editedAt?: string | number | Date | undefined;
|
|
253
|
+
/** Database-serialized edit counter: positive integer, strictly
|
|
254
|
+
* increasing per event row, incremented on every successful text or
|
|
255
|
+
* caption edit; absent means never edited. Orders caption edits where
|
|
256
|
+
* wall-clock editedAt stamps can tie or skew across store replicas.
|
|
257
|
+
* Missing on stores older than edit revisions. */
|
|
258
|
+
editRevision?: number | undefined;
|
|
259
|
+
/** Database-serialized card-mutation counter: positive integer,
|
|
260
|
+
* strictly increasing per event row, bumped on every unfurl card
|
|
261
|
+
* mutation (attach, edit-drop, purge); absent means cards were never
|
|
262
|
+
* mutated. Consumers apply card patches only at or above their known
|
|
263
|
+
* revision, making stale post-removal unfurl frames inert. Missing on
|
|
264
|
+
* stores older than unfurl revisions. */
|
|
265
|
+
unfurlsRevision?: number | undefined;
|
|
253
266
|
deletedAt?: string | number | Date | undefined;
|
|
254
267
|
deletedBy?: string | undefined;
|
|
255
268
|
replyCount?: number | undefined;
|
|
@@ -258,8 +271,14 @@ export interface RoomLedgerEntry {
|
|
|
258
271
|
pinnedBy?: string | null | undefined;
|
|
259
272
|
pinnedAt?: string | number | Date | null | undefined;
|
|
260
273
|
attachments?: RoomAttachment[] | undefined;
|
|
274
|
+
/** Counts-only poll summary when this message carries a poll. Additive:
|
|
275
|
+
* non-poll entries omit it, and live updates ride ENTRY_UPDATED. */
|
|
276
|
+
poll?: RoomPollSummary | null | undefined;
|
|
277
|
+
/** Server-rendered link unfurl cards. Additive: entries without links
|
|
278
|
+
* (and servers older than the unfurl worker) omit it. */
|
|
279
|
+
unfurls?: RoomUnfurl[] | undefined;
|
|
261
280
|
roomRole?: RoomRole | null | undefined;
|
|
262
|
-
metadata?: (Record<string, unknown> & Partial<RoomForwardMetadata>) | undefined;
|
|
281
|
+
metadata?: (Record<string, unknown> & Partial<RoomForwardMetadata> & Partial<RoomWebhookMetadata>) | undefined;
|
|
263
282
|
}
|
|
264
283
|
|
|
265
284
|
export type RoomForwardedFrom =
|
|
@@ -278,6 +297,19 @@ export interface RoomForwardMetadata {
|
|
|
278
297
|
forwardedFrom: RoomForwardedFrom;
|
|
279
298
|
}
|
|
280
299
|
|
|
300
|
+
/** Sender identity for messages posted through a channel webhook (the
|
|
301
|
+
* Discord-compatible ingest). Entries carrying it render the webhook's
|
|
302
|
+
* name and avatar with an APP-style badge; the entry's handle mirrors
|
|
303
|
+
* the display name and its userId is the synthetic `webhook:<id>`, so a
|
|
304
|
+
* webhook can never impersonate a member. */
|
|
305
|
+
export interface RoomWebhookMetadata {
|
|
306
|
+
webhook: {
|
|
307
|
+
id: string;
|
|
308
|
+
name: string;
|
|
309
|
+
avatarUrl?: string | undefined;
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
281
313
|
export interface RoomAttachment {
|
|
282
314
|
/**
|
|
283
315
|
* Object key under room-attachments/; clients sign it at render time.
|
|
@@ -300,6 +332,49 @@ export interface RoomAttachment {
|
|
|
300
332
|
*/
|
|
301
333
|
export type RoomAttachmentInput = RoomAttachment & { key: string };
|
|
302
334
|
|
|
335
|
+
export interface RoomPollOption {
|
|
336
|
+
id: string;
|
|
337
|
+
text: string;
|
|
338
|
+
/** Vote count for this option. Counts only, never voter identities
|
|
339
|
+
* (voters are fetched on demand, exactly like reactors). */
|
|
340
|
+
count: number;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Counts-only poll summary denormalized onto the carrying message (a poll
|
|
345
|
+
* IS a message). Vote/close/edit updates ride the existing ENTRY_UPDATED
|
|
346
|
+
* frame with the refreshed summary; the server's side collections stay the
|
|
347
|
+
* authoritative copy.
|
|
348
|
+
*/
|
|
349
|
+
export interface RoomPollSummary {
|
|
350
|
+
question: string;
|
|
351
|
+
options: RoomPollOption[];
|
|
352
|
+
multi: boolean;
|
|
353
|
+
/** ISO date when the poll auto-closes; null or absent means open-ended. */
|
|
354
|
+
closesAt?: string | null | undefined;
|
|
355
|
+
closed: boolean;
|
|
356
|
+
/** Store-serialized poll ordering revision, strictly increasing per
|
|
357
|
+
* poll: consumers reject frames below their known revision so
|
|
358
|
+
* bus-reordered vote/close updates cannot regress tallies. Present
|
|
359
|
+
* where the relay forwards it; missing on older relays. */
|
|
360
|
+
revision?: number | undefined;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Server-rendered link unfurl card on an entry. `url` is the canonical
|
|
365
|
+
* URL; `thumbnailKey` is the re-hosted copy in the attachments bucket,
|
|
366
|
+
* signed through the member-scoped attachment path. The origin's image URL
|
|
367
|
+
* never crosses the wire. Cards appear via ENTRY_UPDATED when the unfurl
|
|
368
|
+
* worker lands.
|
|
369
|
+
*/
|
|
370
|
+
export interface RoomUnfurl {
|
|
371
|
+
url: string;
|
|
372
|
+
title?: string | undefined;
|
|
373
|
+
description?: string | undefined;
|
|
374
|
+
siteName?: string | undefined;
|
|
375
|
+
thumbnailKey?: string | undefined;
|
|
376
|
+
}
|
|
377
|
+
|
|
303
378
|
/**
|
|
304
379
|
* A registry-backed topic inside a channel. The id is permanent: rename
|
|
305
380
|
* changes `name` only, and links (`om://topic/<topicId>`) keep resolving.
|
|
@@ -370,6 +445,21 @@ export interface RoomReadStatePayload {
|
|
|
370
445
|
* Present when the store surfaces it and on ROOM_SET_ATTENTION acks
|
|
371
446
|
* (which reply READ_STATE scoped to the one room, readState empty). */
|
|
372
447
|
attention?: Record<string, RoomTopicAttention> | undefined;
|
|
448
|
+
/** Per-room timed-mute expiry (ISO date), keyed like attention. Rides
|
|
449
|
+
* beside a 'mute' attention entry when the mute carries an expiry; null
|
|
450
|
+
* marks an explicitly cleared or permanent mute. A lapsed expiry never
|
|
451
|
+
* appears: the server resolves it at read time and the attention value
|
|
452
|
+
* reads as its base ('default'). Missing on servers older than
|
|
453
|
+
* mute-with-duration. */
|
|
454
|
+
muteUntil?: Record<string, string | null> | undefined;
|
|
455
|
+
/** Per-room attention commit revision, keyed like attention: a
|
|
456
|
+
* database-serialized, strictly increasing counter per (user, room),
|
|
457
|
+
* present only for rooms whose attention has ever been committed.
|
|
458
|
+
* ORDERING CONTRACT: a consumer that has synced revision R for a room
|
|
459
|
+
* must ignore snapshot attention entries whose revision is <= R (a
|
|
460
|
+
* snapshot read before a newer commit can arrive after that commit's
|
|
461
|
+
* sync frame). Missing on stores older than attention revisions. */
|
|
462
|
+
attentionRevision?: Record<string, number> | undefined;
|
|
373
463
|
/** Unread mention counts by room, server-computed from the notify
|
|
374
464
|
* worker's ROOM_MENTION rows so badges survive reload and clear through
|
|
375
465
|
* the ack path. Unlike readState/attention the map is SPARSE AND
|
|
@@ -593,7 +683,7 @@ export interface RoomDmMessage {
|
|
|
593
683
|
createdAt?: string | number | undefined;
|
|
594
684
|
/** Sealed DM: content is '' and the opaque envelope rides here. */
|
|
595
685
|
secret?: RoomLedgerSecretPayload | null | undefined;
|
|
596
|
-
metadata?: (Record<string, unknown> & Partial<RoomForwardMetadata>) | undefined;
|
|
686
|
+
metadata?: (Record<string, unknown> & Partial<RoomForwardMetadata> & Partial<RoomWebhookMetadata>) | undefined;
|
|
597
687
|
/** Legacy public chat-image URL (renders as-is, no signing). */
|
|
598
688
|
imageUrl?: string | null | undefined;
|
|
599
689
|
/** Private, signed, scanned DM attachments (dm-attachments/ keyspace).
|
|
@@ -982,6 +1072,14 @@ export interface RoomTopicBucketState {
|
|
|
982
1072
|
unreadCount: number;
|
|
983
1073
|
cursor: number;
|
|
984
1074
|
attention: RoomTopicAttention;
|
|
1075
|
+
/** Standing timed-mute deadline (ISO date; null = permanent mute).
|
|
1076
|
+
* Present only on the room-wide '' bucket and only while its attention
|
|
1077
|
+
* is an active 'mute'; never on topic buckets. Same semantics as the
|
|
1078
|
+
* read-state muteUntil: the store expires timed mutes lazily, so
|
|
1079
|
+
* consumers resolve an elapsed deadline back to the base attention
|
|
1080
|
+
* ('default') at read time (see resolveTopicAttention). Missing on
|
|
1081
|
+
* servers older than mute-with-duration. */
|
|
1082
|
+
muteUntil?: string | null | undefined;
|
|
985
1083
|
}
|
|
986
1084
|
|
|
987
1085
|
export interface RoomTopicResultPayload {
|