synomem 0.2.0 → 0.3.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/CHANGELOG.md +9 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +99 -0
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +36 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +160 -4
- package/dist/client.js.map +1 -1
- package/dist/import.d.ts +149 -0
- package/dist/import.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +57 -0
- package/dist/mcp/index.js.map +1 -1
- package/dist/ports/repository.d.ts +4 -1
- package/dist/ports/repository.d.ts.map +1 -1
- package/dist/projections.d.ts +9 -1
- package/dist/projections.d.ts.map +1 -1
- package/dist/projections.js +71 -0
- package/dist/projections.js.map +1 -1
- package/dist/remote.d.ts +26 -1
- package/dist/remote.d.ts.map +1 -1
- package/dist/remote.js +10 -0
- package/dist/remote.js.map +1 -1
- package/dist/schemas.d.ts +174 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +63 -1
- package/dist/schemas.js.map +1 -1
- package/dist/service.d.ts +26 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/storage.d.ts +11 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +116 -4
- package/dist/storage.js.map +1 -1
- package/dist/types.d.ts +99 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +166 -0
- package/src/client.ts +198 -2
- package/src/mcp/index.ts +76 -0
- package/src/ports/repository.ts +5 -0
- package/src/projections.ts +70 -0
- package/src/remote.ts +48 -0
- package/src/schemas.ts +65 -1
- package/src/service.ts +26 -0
- package/src/storage.ts +150 -4
- package/src/types.ts +105 -1
package/src/storage.ts
CHANGED
|
@@ -31,6 +31,8 @@ import type {
|
|
|
31
31
|
AgentProfile,
|
|
32
32
|
AgentRuntimeBinding,
|
|
33
33
|
ChangePage,
|
|
34
|
+
PostAcknowledgment,
|
|
35
|
+
PostRoster,
|
|
34
36
|
JsonValue,
|
|
35
37
|
KudosChange,
|
|
36
38
|
SynomemEvent,
|
|
@@ -237,6 +239,22 @@ CREATE UNIQUE INDEX agent_runtime_bindings_unique
|
|
|
237
239
|
CREATE INDEX agent_runtime_bindings_agent ON agent_runtime_bindings(agent_id);
|
|
238
240
|
`;
|
|
239
241
|
|
|
242
|
+
const migrationV6 = `
|
|
243
|
+
-- Acknowledgements are their own rows rather than a column on the post: many
|
|
244
|
+
-- actors acknowledge one post independently, and the interesting question is
|
|
245
|
+
-- who, not how many.
|
|
246
|
+
CREATE TABLE post_acknowledgments (
|
|
247
|
+
post_id TEXT NOT NULL,
|
|
248
|
+
actor_kind TEXT NOT NULL,
|
|
249
|
+
actor_id TEXT NOT NULL,
|
|
250
|
+
actor_display_name TEXT,
|
|
251
|
+
note TEXT,
|
|
252
|
+
acknowledged_at TEXT NOT NULL,
|
|
253
|
+
PRIMARY KEY (post_id, actor_kind, actor_id)
|
|
254
|
+
) STRICT;
|
|
255
|
+
CREATE INDEX post_acknowledgments_post ON post_acknowledgments(post_id);
|
|
256
|
+
`;
|
|
257
|
+
|
|
240
258
|
const migrationV3 = `
|
|
241
259
|
DROP TRIGGER IF EXISTS events_append_only_update;
|
|
242
260
|
DROP TRIGGER IF EXISTS events_append_only_delete;
|
|
@@ -365,6 +383,7 @@ function eventKind(event: SynomemEvent): RecordKind | undefined {
|
|
|
365
383
|
if (event.type.startsWith('kudos.')) return 'kudos';
|
|
366
384
|
if (event.type.startsWith('memo.')) return 'memo';
|
|
367
385
|
if (event.type.startsWith('note.')) return 'note';
|
|
386
|
+
if (event.type.startsWith('post.')) return 'post';
|
|
368
387
|
if (event.type.startsWith('task.')) return 'task';
|
|
369
388
|
if (event.type.startsWith('todo.')) return 'todo';
|
|
370
389
|
return undefined;
|
|
@@ -499,7 +518,7 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
499
518
|
const version = Number(
|
|
500
519
|
(db.prepare('PRAGMA user_version').get() as { user_version: number }).user_version,
|
|
501
520
|
);
|
|
502
|
-
if (version >
|
|
521
|
+
if (version > 6) {
|
|
503
522
|
throw new SynomemError(
|
|
504
523
|
'UNSUPPORTED_SCHEMA',
|
|
505
524
|
`Database schema version ${version} is newer than this package supports.`,
|
|
@@ -567,14 +586,26 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
567
586
|
db.exec('PRAGMA user_version = 5');
|
|
568
587
|
});
|
|
569
588
|
}
|
|
589
|
+
const afterV5 = Number(
|
|
590
|
+
(db.prepare('PRAGMA user_version').get() as { user_version: number }).user_version,
|
|
591
|
+
);
|
|
592
|
+
if (afterV5 === 5) {
|
|
593
|
+
this.transactionSync(() => {
|
|
594
|
+
db.exec(migrationV6);
|
|
595
|
+
db.prepare(
|
|
596
|
+
'INSERT OR IGNORE INTO schema_migrations(version, applied_at) VALUES (?, ?)',
|
|
597
|
+
).run(6, new Date().toISOString());
|
|
598
|
+
db.exec('PRAGMA user_version = 6');
|
|
599
|
+
});
|
|
600
|
+
}
|
|
570
601
|
}
|
|
571
602
|
|
|
572
603
|
private assertSchemaSupported(): void {
|
|
573
604
|
const version = Number(
|
|
574
605
|
(this.db().prepare('PRAGMA user_version').get() as { user_version: number }).user_version,
|
|
575
606
|
);
|
|
576
|
-
if (version !==
|
|
577
|
-
if (version >= 1 && version <=
|
|
607
|
+
if (version !== 6) {
|
|
608
|
+
if (version >= 1 && version <= 5) {
|
|
578
609
|
throw new SynomemError(
|
|
579
610
|
'UNSUPPORTED_SCHEMA',
|
|
580
611
|
`Database schema version ${version} requires migration. Open this home once with readOnly: false, then retry the read-only client.`,
|
|
@@ -835,6 +866,61 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
835
866
|
ownerAgentId: event.ownerAgentId,
|
|
836
867
|
ownerDisplayName: event.ownerDisplayName,
|
|
837
868
|
});
|
|
869
|
+
} else if (event.type === 'post.created') {
|
|
870
|
+
/*
|
|
871
|
+
* A post is addressed to the workspace, so it is recorded as `workspace`
|
|
872
|
+
* visibility rather than carrying a visibility of its own. There is no
|
|
873
|
+
* owner-scoped or recipient-scoped read filter to apply: everyone who can
|
|
874
|
+
* read the workspace can read it, which is the whole point of the domain.
|
|
875
|
+
*/
|
|
876
|
+
insert({
|
|
877
|
+
kind: 'post',
|
|
878
|
+
title: event.title,
|
|
879
|
+
tags: event.tags,
|
|
880
|
+
visibility: 'workspace',
|
|
881
|
+
status: 'active',
|
|
882
|
+
});
|
|
883
|
+
} else if (event.type === 'post.edited') {
|
|
884
|
+
this.db()
|
|
885
|
+
.prepare(
|
|
886
|
+
`UPDATE items_current SET title = ?, tags_json = ?,
|
|
887
|
+
updated_sequence = ?, updated_at = ? WHERE item_id = ?`,
|
|
888
|
+
)
|
|
889
|
+
.run(
|
|
890
|
+
event.title,
|
|
891
|
+
JSON.stringify(event.tags ?? []),
|
|
892
|
+
sequence,
|
|
893
|
+
event.createdAt,
|
|
894
|
+
event.postId,
|
|
895
|
+
);
|
|
896
|
+
} else if (event.type === 'post.archived') {
|
|
897
|
+
updateStatus('archived');
|
|
898
|
+
} else if (event.type === 'post.acknowledged') {
|
|
899
|
+
// One row per actor per post: acknowledging twice is the same statement,
|
|
900
|
+
// not a second one.
|
|
901
|
+
this.db()
|
|
902
|
+
.prepare(
|
|
903
|
+
`INSERT INTO post_acknowledgments
|
|
904
|
+
(post_id, actor_kind, actor_id, actor_display_name, note, acknowledged_at)
|
|
905
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
906
|
+
ON CONFLICT(post_id, actor_kind, actor_id) DO UPDATE SET
|
|
907
|
+
note = excluded.note,
|
|
908
|
+
acknowledged_at = excluded.acknowledged_at`,
|
|
909
|
+
)
|
|
910
|
+
.run(
|
|
911
|
+
event.postId,
|
|
912
|
+
event.actor.kind,
|
|
913
|
+
event.actor.id,
|
|
914
|
+
event.actor.displayName ?? null,
|
|
915
|
+
event.note ?? null,
|
|
916
|
+
event.createdAt,
|
|
917
|
+
);
|
|
918
|
+
} else if (event.type === 'post.acknowledgment.withdrawn') {
|
|
919
|
+
this.db()
|
|
920
|
+
.prepare(
|
|
921
|
+
'DELETE FROM post_acknowledgments WHERE post_id = ? AND actor_kind = ? AND actor_id = ?',
|
|
922
|
+
)
|
|
923
|
+
.run(event.postId, event.actor.kind, event.actor.id);
|
|
838
924
|
} else if (event.type === 'note.revised') {
|
|
839
925
|
this.db()
|
|
840
926
|
.prepare(
|
|
@@ -1418,7 +1504,7 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
1418
1504
|
this.db()
|
|
1419
1505
|
.prepare(
|
|
1420
1506
|
`SELECT COUNT(*) AS count FROM events WHERE type IN
|
|
1421
|
-
('kudos.given', 'memo.sent', 'note.created', 'task.created')`,
|
|
1507
|
+
('kudos.given', 'memo.sent', 'note.created', 'post.created', 'task.created')`,
|
|
1422
1508
|
)
|
|
1423
1509
|
.get() as { count: number }
|
|
1424
1510
|
).count,
|
|
@@ -1571,6 +1657,11 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
1571
1657
|
'note.created',
|
|
1572
1658
|
'note.revised',
|
|
1573
1659
|
'note.archived',
|
|
1660
|
+
'post.created',
|
|
1661
|
+
'post.edited',
|
|
1662
|
+
'post.archived',
|
|
1663
|
+
'post.acknowledged',
|
|
1664
|
+
'post.acknowledgment.withdrawn',
|
|
1574
1665
|
'task.created',
|
|
1575
1666
|
'task.updated',
|
|
1576
1667
|
'task.completed',
|
|
@@ -1687,6 +1778,61 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
1687
1778
|
return candidates.length === 1 ? { match: candidates[0]!, candidates } : { candidates };
|
|
1688
1779
|
}
|
|
1689
1780
|
|
|
1781
|
+
/* -------------------------------------------------------- post acknowledgment */
|
|
1782
|
+
|
|
1783
|
+
listPostAcknowledgments(postId: string): PostAcknowledgment[] {
|
|
1784
|
+
const rows = this.db()
|
|
1785
|
+
.prepare(
|
|
1786
|
+
`SELECT actor_kind, actor_id, actor_display_name, note, acknowledged_at
|
|
1787
|
+
FROM post_acknowledgments WHERE post_id = ? ORDER BY acknowledged_at ASC`,
|
|
1788
|
+
)
|
|
1789
|
+
.all(postId) as unknown as Array<{
|
|
1790
|
+
actor_kind: string;
|
|
1791
|
+
actor_id: string;
|
|
1792
|
+
actor_display_name: string | null;
|
|
1793
|
+
note: string | null;
|
|
1794
|
+
acknowledged_at: string;
|
|
1795
|
+
}>;
|
|
1796
|
+
return rows.map((row) => ({
|
|
1797
|
+
actor: {
|
|
1798
|
+
kind: row.actor_kind as ActorIdentity['kind'],
|
|
1799
|
+
id: row.actor_id,
|
|
1800
|
+
...(row.actor_display_name ? { displayName: row.actor_display_name } : {}),
|
|
1801
|
+
},
|
|
1802
|
+
acknowledgedAt: row.acknowledged_at,
|
|
1803
|
+
...(row.note ? { note: row.note } : {}),
|
|
1804
|
+
}));
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
/**
|
|
1808
|
+
* Who has acknowledged a post and who has not.
|
|
1809
|
+
*
|
|
1810
|
+
* The denominator is agents that existed when the post was written. An agent
|
|
1811
|
+
* created afterwards is counted separately rather than listed as
|
|
1812
|
+
* outstanding — it was not there, and a roster that says otherwise accuses a
|
|
1813
|
+
* newcomer of ignoring something written before it arrived.
|
|
1814
|
+
*/
|
|
1815
|
+
postRoster(postId: string): PostRoster | undefined {
|
|
1816
|
+
const post = this.db()
|
|
1817
|
+
.prepare("SELECT created_at FROM items_current WHERE item_id = ? AND kind = 'post'")
|
|
1818
|
+
.get(postId) as { created_at: string } | undefined;
|
|
1819
|
+
if (!post) return undefined;
|
|
1820
|
+
|
|
1821
|
+
const acknowledged = this.listPostAcknowledgments(postId);
|
|
1822
|
+
const acknowledgedIds = new Set(acknowledged.map((entry) => entry.actor.id));
|
|
1823
|
+
|
|
1824
|
+
const eligible = this.db()
|
|
1825
|
+
.prepare('SELECT id, display_name, created_at FROM agents ORDER BY id ASC')
|
|
1826
|
+
.all() as unknown as Array<{ id: string; display_name: string; created_at: string }>;
|
|
1827
|
+
|
|
1828
|
+
const outstanding = eligible
|
|
1829
|
+
.filter((agent) => agent.created_at <= post.created_at && !acknowledgedIds.has(agent.id))
|
|
1830
|
+
.map((agent) => ({ id: agent.id, displayName: agent.display_name }));
|
|
1831
|
+
const joinedSince = eligible.filter((agent) => agent.created_at > post.created_at).length;
|
|
1832
|
+
|
|
1833
|
+
return { postId, acknowledged, outstanding, joinedSince };
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1690
1836
|
/* ------------------------------------------------------- runtime bindings */
|
|
1691
1837
|
|
|
1692
1838
|
listRuntimeBindings(agentId: string): AgentRuntimeBinding[] {
|
package/src/types.ts
CHANGED
|
@@ -3,7 +3,7 @@ export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue
|
|
|
3
3
|
|
|
4
4
|
export type ActorKind = 'human' | 'agent' | 'system';
|
|
5
5
|
export type Visibility = 'private' | 'workspace' | 'public';
|
|
6
|
-
export type RecordKind = 'kudos' | 'memo' | 'note' | 'task' | 'todo';
|
|
6
|
+
export type RecordKind = 'kudos' | 'memo' | 'note' | 'post' | 'task' | 'todo';
|
|
7
7
|
|
|
8
8
|
export interface ActorIdentity {
|
|
9
9
|
kind: ActorKind;
|
|
@@ -140,6 +140,54 @@ export interface MemoArchivedEvent extends BaseEvent {
|
|
|
140
140
|
recipientAgentId: string;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* A Post is publication: an author says something to the whole workspace rather
|
|
145
|
+
* than to a named recipient.
|
|
146
|
+
*
|
|
147
|
+
* It has no recipient and no assignee, which is what separates it from a memo
|
|
148
|
+
* and a task. Everyone who can read the workspace can read it, and each of them
|
|
149
|
+
* can acknowledge it independently — a memo is read by one person, a post by
|
|
150
|
+
* many, so "who has responded" is the interesting question rather than "was it
|
|
151
|
+
* read".
|
|
152
|
+
*/
|
|
153
|
+
export interface PostCreatedEvent extends BaseEvent {
|
|
154
|
+
type: 'post.created';
|
|
155
|
+
title: string;
|
|
156
|
+
body: string;
|
|
157
|
+
tags?: string[];
|
|
158
|
+
/** The post this replies to. A reply inherits its parent's workspace. */
|
|
159
|
+
replyTo?: string;
|
|
160
|
+
}
|
|
161
|
+
export interface PostEditedEvent extends BaseEvent {
|
|
162
|
+
type: 'post.edited';
|
|
163
|
+
postId: string;
|
|
164
|
+
title: string;
|
|
165
|
+
body: string;
|
|
166
|
+
tags?: string[];
|
|
167
|
+
}
|
|
168
|
+
export interface PostArchivedEvent extends BaseEvent {
|
|
169
|
+
type: 'post.archived';
|
|
170
|
+
postId: string;
|
|
171
|
+
reason?: string;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* One actor saying "I have seen this", and only ever about themselves.
|
|
175
|
+
*
|
|
176
|
+
* Appended by an explicit call. Reading a post never acknowledges it: a roster
|
|
177
|
+
* built from read receipts would answer "whose client fetched this", which for
|
|
178
|
+
* an agent means "whose runtime happened to poll".
|
|
179
|
+
*/
|
|
180
|
+
export interface PostAcknowledgedEvent extends BaseEvent {
|
|
181
|
+
type: 'post.acknowledged';
|
|
182
|
+
postId: string;
|
|
183
|
+
note?: string;
|
|
184
|
+
}
|
|
185
|
+
export interface PostAcknowledgmentWithdrawnEvent extends BaseEvent {
|
|
186
|
+
type: 'post.acknowledgment.withdrawn';
|
|
187
|
+
postId: string;
|
|
188
|
+
reason?: string;
|
|
189
|
+
}
|
|
190
|
+
|
|
143
191
|
export interface NoteCreatedEvent extends BaseEvent {
|
|
144
192
|
type: 'note.created';
|
|
145
193
|
ownerAgentId: string;
|
|
@@ -322,6 +370,11 @@ export type SynomemEvent =
|
|
|
322
370
|
| MemoSentEvent
|
|
323
371
|
| MemoReadEvent
|
|
324
372
|
| MemoArchivedEvent
|
|
373
|
+
| PostCreatedEvent
|
|
374
|
+
| PostEditedEvent
|
|
375
|
+
| PostArchivedEvent
|
|
376
|
+
| PostAcknowledgedEvent
|
|
377
|
+
| PostAcknowledgmentWithdrawnEvent
|
|
325
378
|
| NoteCreatedEvent
|
|
326
379
|
| NoteRevisedEvent
|
|
327
380
|
| NoteArchivedEvent
|
|
@@ -356,6 +409,40 @@ export interface MemoRecord {
|
|
|
356
409
|
archived?: MemoArchivedEvent;
|
|
357
410
|
status: 'unread' | 'read' | 'archived';
|
|
358
411
|
}
|
|
412
|
+
export interface PostAcknowledgment {
|
|
413
|
+
actor: ActorIdentity;
|
|
414
|
+
acknowledgedAt: string;
|
|
415
|
+
note?: string;
|
|
416
|
+
}
|
|
417
|
+
export interface PostRecord {
|
|
418
|
+
event: PostCreatedEvent;
|
|
419
|
+
edits: PostEditedEvent[];
|
|
420
|
+
archived?: PostArchivedEvent;
|
|
421
|
+
/** Everyone who has said they have seen it, in the order they said so. */
|
|
422
|
+
acknowledgments: PostAcknowledgment[];
|
|
423
|
+
status: 'active' | 'archived';
|
|
424
|
+
title: string;
|
|
425
|
+
body: string;
|
|
426
|
+
tags?: string[];
|
|
427
|
+
version: number;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Who has acknowledged a post, and who has not.
|
|
432
|
+
*
|
|
433
|
+
* `outstanding` is the honest denominator: actors who can read the post now AND
|
|
434
|
+
* could have read it when it was posted. Someone who joined afterwards is
|
|
435
|
+
* neither acknowledged nor outstanding — they were not there — and is reported
|
|
436
|
+
* as a count instead, so a roster never accuses a newcomer of ignoring
|
|
437
|
+
* something written before they arrived.
|
|
438
|
+
*/
|
|
439
|
+
export interface PostRoster {
|
|
440
|
+
postId: string;
|
|
441
|
+
acknowledged: PostAcknowledgment[];
|
|
442
|
+
outstanding: Array<{ id: string; displayName: string }>;
|
|
443
|
+
joinedSince: number;
|
|
444
|
+
}
|
|
445
|
+
|
|
359
446
|
export interface NoteRecord {
|
|
360
447
|
event: NoteCreatedEvent;
|
|
361
448
|
revision?: NoteRevisedEvent;
|
|
@@ -529,6 +616,23 @@ export interface SendMemoInput extends MutationInput {
|
|
|
529
616
|
tags?: string[];
|
|
530
617
|
visibility?: Visibility;
|
|
531
618
|
}
|
|
619
|
+
export interface CreatePostInput extends MutationInput {
|
|
620
|
+
title: string;
|
|
621
|
+
body: string;
|
|
622
|
+
tags?: string[];
|
|
623
|
+
/** The post being replied to; a reply inherits its parent's workspace. */
|
|
624
|
+
replyTo?: string;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
export interface UpdatePostInput {
|
|
628
|
+
postId: string;
|
|
629
|
+
expectedVersion: number;
|
|
630
|
+
title?: string;
|
|
631
|
+
body?: string;
|
|
632
|
+
tags?: string[];
|
|
633
|
+
idempotencyKey?: string;
|
|
634
|
+
}
|
|
635
|
+
|
|
532
636
|
export interface CreateNoteInput extends MutationInput {
|
|
533
637
|
ownerAgentId?: string;
|
|
534
638
|
title: string;
|