@hasna/conversations 0.2.14 → 0.2.16
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/bin/hook.js +10 -0
- package/bin/index.js +93 -2
- package/bin/mcp.js +93 -2
- package/dist/index.js +10 -0
- package/dist/lib/messages.d.ts +16 -0
- package/package.json +1 -1
package/bin/hook.js
CHANGED
|
@@ -255,6 +255,16 @@ function getDb() {
|
|
|
255
255
|
if (!presenceColNames.includes("project_id")) {
|
|
256
256
|
db.exec("ALTER TABLE agent_presence ADD COLUMN project_id TEXT");
|
|
257
257
|
}
|
|
258
|
+
db.exec(`
|
|
259
|
+
CREATE TABLE IF NOT EXISTS message_read_receipts (
|
|
260
|
+
message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
261
|
+
agent TEXT NOT NULL,
|
|
262
|
+
read_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
|
|
263
|
+
PRIMARY KEY (message_id, agent)
|
|
264
|
+
)
|
|
265
|
+
`);
|
|
266
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_read_receipts_message ON message_read_receipts(message_id)");
|
|
267
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_read_receipts_agent ON message_read_receipts(agent)");
|
|
258
268
|
db.exec(`
|
|
259
269
|
CREATE TABLE IF NOT EXISTS message_mentions (
|
|
260
270
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
package/bin/index.js
CHANGED
|
@@ -2120,6 +2120,16 @@ function getDb() {
|
|
|
2120
2120
|
if (!presenceColNames.includes("project_id")) {
|
|
2121
2121
|
db.exec("ALTER TABLE agent_presence ADD COLUMN project_id TEXT");
|
|
2122
2122
|
}
|
|
2123
|
+
db.exec(`
|
|
2124
|
+
CREATE TABLE IF NOT EXISTS message_read_receipts (
|
|
2125
|
+
message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
2126
|
+
agent TEXT NOT NULL,
|
|
2127
|
+
read_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
|
|
2128
|
+
PRIMARY KEY (message_id, agent)
|
|
2129
|
+
)
|
|
2130
|
+
`);
|
|
2131
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_read_receipts_message ON message_read_receipts(message_id)");
|
|
2132
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_read_receipts_agent ON message_read_receipts(agent)");
|
|
2123
2133
|
db.exec(`
|
|
2124
2134
|
CREATE TABLE IF NOT EXISTS message_mentions (
|
|
2125
2135
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -2814,6 +2824,33 @@ function markUnreadByIds(ids) {
|
|
|
2814
2824
|
const result = db2.prepare(`UPDATE messages SET read_at = NULL WHERE id IN (${placeholders})`).run(...ids);
|
|
2815
2825
|
return result.changes;
|
|
2816
2826
|
}
|
|
2827
|
+
function recordReadReceipt(messageId, agent) {
|
|
2828
|
+
const db2 = getDb();
|
|
2829
|
+
db2.prepare(`INSERT OR REPLACE INTO message_read_receipts (message_id, agent, read_at)
|
|
2830
|
+
VALUES (?, ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'))`).run(messageId, agent.toLowerCase());
|
|
2831
|
+
}
|
|
2832
|
+
function recordReadReceiptsBatch(messageIds, agent) {
|
|
2833
|
+
if (!messageIds.length || !agent)
|
|
2834
|
+
return;
|
|
2835
|
+
const db2 = getDb();
|
|
2836
|
+
const stmt = db2.prepare(`INSERT OR REPLACE INTO message_read_receipts (message_id, agent, read_at)
|
|
2837
|
+
VALUES (?, ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'))`);
|
|
2838
|
+
for (const id of messageIds) {
|
|
2839
|
+
stmt.run(id, agent.toLowerCase());
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
function getReadReceipts(messageId) {
|
|
2843
|
+
const db2 = getDb();
|
|
2844
|
+
return db2.prepare("SELECT * FROM message_read_receipts WHERE message_id = ? ORDER BY read_at ASC").all(messageId);
|
|
2845
|
+
}
|
|
2846
|
+
function getMessageReadStatus(messageId, space) {
|
|
2847
|
+
const db2 = getDb();
|
|
2848
|
+
const receipts = getReadReceipts(messageId);
|
|
2849
|
+
const readers = new Set(receipts.map((r) => r.agent));
|
|
2850
|
+
const members = db2.prepare("SELECT agent FROM space_members WHERE space = ?").all(space);
|
|
2851
|
+
const unread_by = members.map((m) => m.agent).filter((a) => !readers.has(a));
|
|
2852
|
+
return { receipts, unread_by };
|
|
2853
|
+
}
|
|
2817
2854
|
var init_messages = __esm(() => {
|
|
2818
2855
|
init_db();
|
|
2819
2856
|
init_webhooks();
|
|
@@ -4453,7 +4490,7 @@ var init_poll = __esm(() => {
|
|
|
4453
4490
|
var require_package = __commonJS((exports, module) => {
|
|
4454
4491
|
module.exports = {
|
|
4455
4492
|
name: "@hasna/conversations",
|
|
4456
|
-
version: "0.2.
|
|
4493
|
+
version: "0.2.16",
|
|
4457
4494
|
description: "Real-time CLI messaging for AI agents",
|
|
4458
4495
|
type: "module",
|
|
4459
4496
|
bin: {
|
|
@@ -33914,6 +33951,55 @@ var init_mcp2 = __esm(() => {
|
|
|
33914
33951
|
content: [{ type: "text", text: JSON.stringify(spaces) }]
|
|
33915
33952
|
};
|
|
33916
33953
|
});
|
|
33954
|
+
server.registerTool("read_receipts", {
|
|
33955
|
+
description: "Get per-agent read receipts for a message. Shows who has read it and (for space messages) who hasn't.",
|
|
33956
|
+
inputSchema: {
|
|
33957
|
+
message_id: exports_external.coerce.number(),
|
|
33958
|
+
space: exports_external.string().optional().describe("Space name \u2014 if provided, also returns list of members who haven't read yet")
|
|
33959
|
+
}
|
|
33960
|
+
}, async (args) => {
|
|
33961
|
+
const receipts = getReadReceipts(args.message_id);
|
|
33962
|
+
if (args.space) {
|
|
33963
|
+
const status = getMessageReadStatus(args.message_id, args.space);
|
|
33964
|
+
return { content: [{ type: "text", text: JSON.stringify(status) }] };
|
|
33965
|
+
}
|
|
33966
|
+
return { content: [{ type: "text", text: JSON.stringify({ receipts, count: receipts.length }) }] };
|
|
33967
|
+
});
|
|
33968
|
+
server.registerTool("mark_read_receipt", {
|
|
33969
|
+
description: "Manually record that an agent has read a specific message.",
|
|
33970
|
+
inputSchema: {
|
|
33971
|
+
message_id: exports_external.coerce.number(),
|
|
33972
|
+
agent: exports_external.string()
|
|
33973
|
+
}
|
|
33974
|
+
}, async (args) => {
|
|
33975
|
+
recordReadReceipt(args.message_id, args.agent);
|
|
33976
|
+
return { content: [{ type: "text", text: `\u2713 Marked message #${args.message_id} as read by ${args.agent}` }] };
|
|
33977
|
+
});
|
|
33978
|
+
server.registerTool("broadcast", {
|
|
33979
|
+
description: "Send the same message to multiple spaces at once. Useful for status updates, bug reports, or announcements that need to go to several spaces.",
|
|
33980
|
+
inputSchema: {
|
|
33981
|
+
spaces: exports_external.array(exports_external.string()).describe("List of space names to send to"),
|
|
33982
|
+
content: exports_external.string().describe("Message content"),
|
|
33983
|
+
from: exports_external.string().optional().describe("Sender agent name"),
|
|
33984
|
+
priority: exports_external.enum(["low", "normal", "high", "urgent"]).optional()
|
|
33985
|
+
}
|
|
33986
|
+
}, async (args) => {
|
|
33987
|
+
const { spaces, content, from: fromParam, priority } = args;
|
|
33988
|
+
const from = resolveIdentity(fromParam);
|
|
33989
|
+
const results = [];
|
|
33990
|
+
const errors4 = [];
|
|
33991
|
+
for (const space of spaces) {
|
|
33992
|
+
try {
|
|
33993
|
+
const msg = sendMessage({ from, to: space, content, space, priority });
|
|
33994
|
+
results.push({ space, id: msg.id });
|
|
33995
|
+
} catch (e) {
|
|
33996
|
+
errors4.push(`${space}: ${e instanceof Error ? e.message : String(e)}`);
|
|
33997
|
+
}
|
|
33998
|
+
}
|
|
33999
|
+
return {
|
|
34000
|
+
content: [{ type: "text", text: JSON.stringify({ sent: results, errors: errors4, total: results.length }) }]
|
|
34001
|
+
};
|
|
34002
|
+
});
|
|
33917
34003
|
server.registerTool("list_unread_counts", {
|
|
33918
34004
|
description: "Get unread message counts per space without fetching message content. Use this at session start to triage which spaces need attention before calling read_messages.",
|
|
33919
34005
|
inputSchema: {
|
|
@@ -33990,6 +34076,7 @@ var init_mcp2 = __esm(() => {
|
|
|
33990
34076
|
description: "Read messages from a space.",
|
|
33991
34077
|
inputSchema: {
|
|
33992
34078
|
space: exports_external.string(),
|
|
34079
|
+
from: exports_external.string().optional().describe("Agent reading the space \u2014 used for per-agent read receipts"),
|
|
33993
34080
|
since: exports_external.string().optional(),
|
|
33994
34081
|
limit: exports_external.coerce.number().optional(),
|
|
33995
34082
|
mark_read: exports_external.coerce.boolean().optional(),
|
|
@@ -33999,11 +34086,15 @@ var init_mcp2 = __esm(() => {
|
|
|
33999
34086
|
latest: exports_external.coerce.number().optional().describe("Return the N most recent messages, newest first")
|
|
34000
34087
|
}
|
|
34001
34088
|
}, async (args) => {
|
|
34002
|
-
const { space, since, limit, mark_read, max_content_length, threads_only, include_reply_counts, latest } = args;
|
|
34089
|
+
const { space, from: fromParam, since, limit, mark_read, max_content_length, threads_only, include_reply_counts, latest } = args;
|
|
34003
34090
|
const messages = readMessages({ space, since, limit, max_content_length, threads_only, include_reply_counts, latest });
|
|
34004
34091
|
if (mark_read !== false && messages.length > 0) {
|
|
34005
34092
|
markReadByIds(messages.map((m) => m.id));
|
|
34006
34093
|
}
|
|
34094
|
+
if (fromParam && messages.length > 0) {
|
|
34095
|
+
const agent = resolveIdentity(fromParam);
|
|
34096
|
+
recordReadReceiptsBatch(messages.map((m) => m.id), agent);
|
|
34097
|
+
}
|
|
34007
34098
|
return {
|
|
34008
34099
|
content: [{ type: "text", text: JSON.stringify(messages) }]
|
|
34009
34100
|
};
|
package/bin/mcp.js
CHANGED
|
@@ -6752,6 +6752,16 @@ function getDb() {
|
|
|
6752
6752
|
if (!presenceColNames.includes("project_id")) {
|
|
6753
6753
|
db.exec("ALTER TABLE agent_presence ADD COLUMN project_id TEXT");
|
|
6754
6754
|
}
|
|
6755
|
+
db.exec(`
|
|
6756
|
+
CREATE TABLE IF NOT EXISTS message_read_receipts (
|
|
6757
|
+
message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
6758
|
+
agent TEXT NOT NULL,
|
|
6759
|
+
read_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
|
|
6760
|
+
PRIMARY KEY (message_id, agent)
|
|
6761
|
+
)
|
|
6762
|
+
`);
|
|
6763
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_read_receipts_message ON message_read_receipts(message_id)");
|
|
6764
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_read_receipts_agent ON message_read_receipts(agent)");
|
|
6755
6765
|
db.exec(`
|
|
6756
6766
|
CREATE TABLE IF NOT EXISTS message_mentions (
|
|
6757
6767
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -29317,6 +29327,33 @@ function markUnreadByIds(ids) {
|
|
|
29317
29327
|
const result = db2.prepare(`UPDATE messages SET read_at = NULL WHERE id IN (${placeholders})`).run(...ids);
|
|
29318
29328
|
return result.changes;
|
|
29319
29329
|
}
|
|
29330
|
+
function recordReadReceipt(messageId, agent) {
|
|
29331
|
+
const db2 = getDb();
|
|
29332
|
+
db2.prepare(`INSERT OR REPLACE INTO message_read_receipts (message_id, agent, read_at)
|
|
29333
|
+
VALUES (?, ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'))`).run(messageId, agent.toLowerCase());
|
|
29334
|
+
}
|
|
29335
|
+
function recordReadReceiptsBatch(messageIds, agent) {
|
|
29336
|
+
if (!messageIds.length || !agent)
|
|
29337
|
+
return;
|
|
29338
|
+
const db2 = getDb();
|
|
29339
|
+
const stmt = db2.prepare(`INSERT OR REPLACE INTO message_read_receipts (message_id, agent, read_at)
|
|
29340
|
+
VALUES (?, ?, strftime('%Y-%m-%dT%H:%M:%f', 'now'))`);
|
|
29341
|
+
for (const id of messageIds) {
|
|
29342
|
+
stmt.run(id, agent.toLowerCase());
|
|
29343
|
+
}
|
|
29344
|
+
}
|
|
29345
|
+
function getReadReceipts(messageId) {
|
|
29346
|
+
const db2 = getDb();
|
|
29347
|
+
return db2.prepare("SELECT * FROM message_read_receipts WHERE message_id = ? ORDER BY read_at ASC").all(messageId);
|
|
29348
|
+
}
|
|
29349
|
+
function getMessageReadStatus(messageId, space) {
|
|
29350
|
+
const db2 = getDb();
|
|
29351
|
+
const receipts = getReadReceipts(messageId);
|
|
29352
|
+
const readers = new Set(receipts.map((r) => r.agent));
|
|
29353
|
+
const members = db2.prepare("SELECT agent FROM space_members WHERE space = ?").all(space);
|
|
29354
|
+
const unread_by = members.map((m) => m.agent).filter((a) => !readers.has(a));
|
|
29355
|
+
return { receipts, unread_by };
|
|
29356
|
+
}
|
|
29320
29357
|
|
|
29321
29358
|
// src/lib/sessions.ts
|
|
29322
29359
|
init_db();
|
|
@@ -30943,7 +30980,7 @@ function getGraphStats() {
|
|
|
30943
30980
|
// package.json
|
|
30944
30981
|
var package_default = {
|
|
30945
30982
|
name: "@hasna/conversations",
|
|
30946
|
-
version: "0.2.
|
|
30983
|
+
version: "0.2.16",
|
|
30947
30984
|
description: "Real-time CLI messaging for AI agents",
|
|
30948
30985
|
type: "module",
|
|
30949
30986
|
bin: {
|
|
@@ -31283,6 +31320,55 @@ server.registerTool("list_spaces", {
|
|
|
31283
31320
|
content: [{ type: "text", text: JSON.stringify(spaces) }]
|
|
31284
31321
|
};
|
|
31285
31322
|
});
|
|
31323
|
+
server.registerTool("read_receipts", {
|
|
31324
|
+
description: "Get per-agent read receipts for a message. Shows who has read it and (for space messages) who hasn't.",
|
|
31325
|
+
inputSchema: {
|
|
31326
|
+
message_id: exports_external.coerce.number(),
|
|
31327
|
+
space: exports_external.string().optional().describe("Space name \u2014 if provided, also returns list of members who haven't read yet")
|
|
31328
|
+
}
|
|
31329
|
+
}, async (args) => {
|
|
31330
|
+
const receipts = getReadReceipts(args.message_id);
|
|
31331
|
+
if (args.space) {
|
|
31332
|
+
const status = getMessageReadStatus(args.message_id, args.space);
|
|
31333
|
+
return { content: [{ type: "text", text: JSON.stringify(status) }] };
|
|
31334
|
+
}
|
|
31335
|
+
return { content: [{ type: "text", text: JSON.stringify({ receipts, count: receipts.length }) }] };
|
|
31336
|
+
});
|
|
31337
|
+
server.registerTool("mark_read_receipt", {
|
|
31338
|
+
description: "Manually record that an agent has read a specific message.",
|
|
31339
|
+
inputSchema: {
|
|
31340
|
+
message_id: exports_external.coerce.number(),
|
|
31341
|
+
agent: exports_external.string()
|
|
31342
|
+
}
|
|
31343
|
+
}, async (args) => {
|
|
31344
|
+
recordReadReceipt(args.message_id, args.agent);
|
|
31345
|
+
return { content: [{ type: "text", text: `\u2713 Marked message #${args.message_id} as read by ${args.agent}` }] };
|
|
31346
|
+
});
|
|
31347
|
+
server.registerTool("broadcast", {
|
|
31348
|
+
description: "Send the same message to multiple spaces at once. Useful for status updates, bug reports, or announcements that need to go to several spaces.",
|
|
31349
|
+
inputSchema: {
|
|
31350
|
+
spaces: exports_external.array(exports_external.string()).describe("List of space names to send to"),
|
|
31351
|
+
content: exports_external.string().describe("Message content"),
|
|
31352
|
+
from: exports_external.string().optional().describe("Sender agent name"),
|
|
31353
|
+
priority: exports_external.enum(["low", "normal", "high", "urgent"]).optional()
|
|
31354
|
+
}
|
|
31355
|
+
}, async (args) => {
|
|
31356
|
+
const { spaces, content, from: fromParam, priority } = args;
|
|
31357
|
+
const from = resolveIdentity(fromParam);
|
|
31358
|
+
const results = [];
|
|
31359
|
+
const errors3 = [];
|
|
31360
|
+
for (const space of spaces) {
|
|
31361
|
+
try {
|
|
31362
|
+
const msg = sendMessage({ from, to: space, content, space, priority });
|
|
31363
|
+
results.push({ space, id: msg.id });
|
|
31364
|
+
} catch (e) {
|
|
31365
|
+
errors3.push(`${space}: ${e instanceof Error ? e.message : String(e)}`);
|
|
31366
|
+
}
|
|
31367
|
+
}
|
|
31368
|
+
return {
|
|
31369
|
+
content: [{ type: "text", text: JSON.stringify({ sent: results, errors: errors3, total: results.length }) }]
|
|
31370
|
+
};
|
|
31371
|
+
});
|
|
31286
31372
|
server.registerTool("list_unread_counts", {
|
|
31287
31373
|
description: "Get unread message counts per space without fetching message content. Use this at session start to triage which spaces need attention before calling read_messages.",
|
|
31288
31374
|
inputSchema: {
|
|
@@ -31359,6 +31445,7 @@ server.registerTool("read_space", {
|
|
|
31359
31445
|
description: "Read messages from a space.",
|
|
31360
31446
|
inputSchema: {
|
|
31361
31447
|
space: exports_external.string(),
|
|
31448
|
+
from: exports_external.string().optional().describe("Agent reading the space \u2014 used for per-agent read receipts"),
|
|
31362
31449
|
since: exports_external.string().optional(),
|
|
31363
31450
|
limit: exports_external.coerce.number().optional(),
|
|
31364
31451
|
mark_read: exports_external.coerce.boolean().optional(),
|
|
@@ -31368,11 +31455,15 @@ server.registerTool("read_space", {
|
|
|
31368
31455
|
latest: exports_external.coerce.number().optional().describe("Return the N most recent messages, newest first")
|
|
31369
31456
|
}
|
|
31370
31457
|
}, async (args) => {
|
|
31371
|
-
const { space, since, limit, mark_read, max_content_length, threads_only, include_reply_counts, latest } = args;
|
|
31458
|
+
const { space, from: fromParam, since, limit, mark_read, max_content_length, threads_only, include_reply_counts, latest } = args;
|
|
31372
31459
|
const messages = readMessages({ space, since, limit, max_content_length, threads_only, include_reply_counts, latest });
|
|
31373
31460
|
if (mark_read !== false && messages.length > 0) {
|
|
31374
31461
|
markReadByIds(messages.map((m) => m.id));
|
|
31375
31462
|
}
|
|
31463
|
+
if (fromParam && messages.length > 0) {
|
|
31464
|
+
const agent = resolveIdentity(fromParam);
|
|
31465
|
+
recordReadReceiptsBatch(messages.map((m) => m.id), agent);
|
|
31466
|
+
}
|
|
31376
31467
|
return {
|
|
31377
31468
|
content: [{ type: "text", text: JSON.stringify(messages) }]
|
|
31378
31469
|
};
|
package/dist/index.js
CHANGED
|
@@ -279,6 +279,16 @@ function getDb() {
|
|
|
279
279
|
if (!presenceColNames.includes("project_id")) {
|
|
280
280
|
db.exec("ALTER TABLE agent_presence ADD COLUMN project_id TEXT");
|
|
281
281
|
}
|
|
282
|
+
db.exec(`
|
|
283
|
+
CREATE TABLE IF NOT EXISTS message_read_receipts (
|
|
284
|
+
message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
285
|
+
agent TEXT NOT NULL,
|
|
286
|
+
read_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
|
|
287
|
+
PRIMARY KEY (message_id, agent)
|
|
288
|
+
)
|
|
289
|
+
`);
|
|
290
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_read_receipts_message ON message_read_receipts(message_id)");
|
|
291
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_read_receipts_agent ON message_read_receipts(agent)");
|
|
282
292
|
db.exec(`
|
|
283
293
|
CREATE TABLE IF NOT EXISTS message_mentions (
|
|
284
294
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
package/dist/lib/messages.d.ts
CHANGED
|
@@ -92,3 +92,19 @@ export declare function markMentionsRead(agent: string, space?: string): number;
|
|
|
92
92
|
export declare function markUnread(messageId: number): number;
|
|
93
93
|
/** Mark multiple messages as unread. */
|
|
94
94
|
export declare function markUnreadByIds(ids: number[]): number;
|
|
95
|
+
export interface ReadReceipt {
|
|
96
|
+
message_id: number;
|
|
97
|
+
agent: string;
|
|
98
|
+
read_at: string;
|
|
99
|
+
}
|
|
100
|
+
/** Record that an agent has read a specific message. */
|
|
101
|
+
export declare function recordReadReceipt(messageId: number, agent: string): void;
|
|
102
|
+
/** Record read receipts for all messages in a batch. */
|
|
103
|
+
export declare function recordReadReceiptsBatch(messageIds: number[], agent: string): void;
|
|
104
|
+
/** Get all read receipts for a specific message. */
|
|
105
|
+
export declare function getReadReceipts(messageId: number): ReadReceipt[];
|
|
106
|
+
/** Get read status summary for a space message: who has read it and who hasn't. */
|
|
107
|
+
export declare function getMessageReadStatus(messageId: number, space: string): {
|
|
108
|
+
receipts: ReadReceipt[];
|
|
109
|
+
unread_by: string[];
|
|
110
|
+
};
|