synomem 0.2.0 → 0.4.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 +49 -0
- package/README.md +4 -4
- package/dist/backend.d.ts.map +1 -1
- package/dist/backend.js +8 -2
- package/dist/backend.js.map +1 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +379 -16
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +68 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +283 -12
- package/dist/client.js.map +1 -1
- package/dist/cloud.d.ts +16 -0
- package/dist/cloud.d.ts.map +1 -0
- package/dist/cloud.js +19 -0
- package/dist/cloud.js.map +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -1
- package/dist/config.js.map +1 -1
- package/dist/configure.d.ts +55 -0
- package/dist/configure.d.ts.map +1 -0
- package/dist/configure.js +177 -0
- package/dist/configure.js.map +1 -0
- package/dist/credentials.d.ts +15 -2
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js.map +1 -1
- package/dist/import.d.ts +202 -38
- package/dist/import.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +72 -6
- package/dist/mcp/index.js.map +1 -1
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +8 -1
- package/dist/oauth.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 +75 -4
- package/dist/projections.js.map +1 -1
- package/dist/prompt.d.ts +28 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +72 -0
- package/dist/prompt.js.map +1 -0
- package/dist/remote.d.ts +30 -1
- package/dist/remote.d.ts.map +1 -1
- package/dist/remote.js +14 -0
- package/dist/remote.js.map +1 -1
- package/dist/schemas.d.ts +270 -55
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +120 -6
- package/dist/schemas.js.map +1 -1
- package/dist/service.d.ts +30 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/storage.d.ts +25 -2
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +168 -15
- package/dist/storage.js.map +1 -1
- package/dist/types.d.ts +123 -4
- package/dist/types.d.ts.map +1 -1
- package/docs/cli.md +1 -1
- package/docs/examples.md +1 -1
- package/docs/mcp.md +1 -1
- package/docs/skill.md +1 -1
- package/docs/storage-format.md +1 -1
- package/package.json +8 -8
- package/src/backend.ts +8 -2
- package/src/cli.ts +543 -19
- package/src/client.ts +331 -11
- package/src/cloud.ts +19 -0
- package/src/config.ts +8 -1
- package/src/configure.ts +233 -0
- package/src/credentials.ts +17 -2
- package/src/index.ts +7 -1
- package/src/mcp/index.ts +95 -5
- package/src/oauth.ts +8 -1
- package/src/ports/repository.ts +5 -0
- package/src/projections.ts +74 -4
- package/src/prompt.ts +88 -0
- package/src/remote.ts +72 -0
- package/src/schemas.ts +125 -6
- package/src/service.ts +30 -0
- package/src/storage.ts +217 -14
- package/src/types.ts +129 -3
package/src/remote.ts
CHANGED
|
@@ -6,6 +6,8 @@ import type {
|
|
|
6
6
|
ChangesInput,
|
|
7
7
|
BindRuntimeInput,
|
|
8
8
|
CreateAgentInput,
|
|
9
|
+
CreatePostInput,
|
|
10
|
+
UpdatePostInput,
|
|
9
11
|
CreateNoteInput,
|
|
10
12
|
CreateTaskInput,
|
|
11
13
|
GiveKudosInput,
|
|
@@ -177,6 +179,30 @@ export class RemoteSynomemService implements SynomemService {
|
|
|
177
179
|
'GET',
|
|
178
180
|
`agents/resolve?query=${encodeURIComponent(query)}`,
|
|
179
181
|
),
|
|
182
|
+
archive: (idOrAlias: string) =>
|
|
183
|
+
this.mutation<Awaited<ReturnType<SynomemService['agents']['archive']>>>(
|
|
184
|
+
'POST',
|
|
185
|
+
`agents/${encodeURIComponent(idOrAlias)}/archive`,
|
|
186
|
+
{},
|
|
187
|
+
),
|
|
188
|
+
restore: (idOrAlias: string) =>
|
|
189
|
+
this.mutation<Awaited<ReturnType<SynomemService['agents']['restore']>>>(
|
|
190
|
+
'POST',
|
|
191
|
+
`agents/${encodeURIComponent(idOrAlias)}/restore`,
|
|
192
|
+
{},
|
|
193
|
+
),
|
|
194
|
+
addAliases: (idOrAlias: string, aliases: string[]) =>
|
|
195
|
+
this.mutation<Awaited<ReturnType<SynomemService['agents']['addAliases']>>>(
|
|
196
|
+
'POST',
|
|
197
|
+
`agents/${encodeURIComponent(idOrAlias)}/aliases`,
|
|
198
|
+
{ aliases },
|
|
199
|
+
),
|
|
200
|
+
removeAliases: (idOrAlias: string, aliases: string[]) =>
|
|
201
|
+
this.mutation<Awaited<ReturnType<SynomemService['agents']['removeAliases']>>>(
|
|
202
|
+
'POST',
|
|
203
|
+
`agents/${encodeURIComponent(idOrAlias)}/aliases/remove`,
|
|
204
|
+
{ aliases },
|
|
205
|
+
),
|
|
180
206
|
directory: () =>
|
|
181
207
|
this.request<Awaited<ReturnType<SynomemService['agents']['directory']>>>(
|
|
182
208
|
'GET',
|
|
@@ -200,6 +226,52 @@ export class RemoteSynomemService implements SynomemService {
|
|
|
200
226
|
),
|
|
201
227
|
};
|
|
202
228
|
|
|
229
|
+
readonly posts = {
|
|
230
|
+
create: (input: CreatePostInput) =>
|
|
231
|
+
this.mutation<Awaited<ReturnType<SynomemService['posts']['create']>>>('POST', 'posts', input),
|
|
232
|
+
list: (input: Omit<ItemListInput, 'kinds'> = {}) =>
|
|
233
|
+
this.request<Awaited<ReturnType<SynomemService['posts']['list']>>>(
|
|
234
|
+
'GET',
|
|
235
|
+
`posts${queryString(input)}`,
|
|
236
|
+
),
|
|
237
|
+
get: (id: string) =>
|
|
238
|
+
this.request<Awaited<ReturnType<SynomemService['posts']['get']>>>(
|
|
239
|
+
'GET',
|
|
240
|
+
`posts/${encodeURIComponent(id)}`,
|
|
241
|
+
),
|
|
242
|
+
update: (input: UpdatePostInput) =>
|
|
243
|
+
this.mutation<Awaited<ReturnType<SynomemService['posts']['update']>>>(
|
|
244
|
+
'POST',
|
|
245
|
+
`posts/${encodeURIComponent(input.postId)}/revisions`,
|
|
246
|
+
input,
|
|
247
|
+
['postId'],
|
|
248
|
+
),
|
|
249
|
+
archive: (input: { postId: string; reason?: string; idempotencyKey?: string }) =>
|
|
250
|
+
this.mutation<Awaited<ReturnType<SynomemService['posts']['archive']>>>(
|
|
251
|
+
'POST',
|
|
252
|
+
`posts/${encodeURIComponent(input.postId)}/archive`,
|
|
253
|
+
input,
|
|
254
|
+
['postId'],
|
|
255
|
+
),
|
|
256
|
+
acknowledge: (input: { postId: string; note?: string; idempotencyKey?: string }) =>
|
|
257
|
+
this.mutation<Awaited<ReturnType<SynomemService['posts']['acknowledge']>>>(
|
|
258
|
+
'POST',
|
|
259
|
+
`posts/${encodeURIComponent(input.postId)}/acknowledgment`,
|
|
260
|
+
input,
|
|
261
|
+
['postId'],
|
|
262
|
+
),
|
|
263
|
+
withdrawAcknowledgment: (input: { postId: string; reason?: string }) =>
|
|
264
|
+
this.request<Awaited<ReturnType<SynomemService['posts']['withdrawAcknowledgment']>>>(
|
|
265
|
+
'DELETE',
|
|
266
|
+
`posts/${encodeURIComponent(input.postId)}/acknowledgment`,
|
|
267
|
+
),
|
|
268
|
+
roster: (postId: string) =>
|
|
269
|
+
this.request<Awaited<ReturnType<SynomemService['posts']['roster']>>>(
|
|
270
|
+
'GET',
|
|
271
|
+
`posts/${encodeURIComponent(postId)}/roster`,
|
|
272
|
+
),
|
|
273
|
+
};
|
|
274
|
+
|
|
203
275
|
readonly kudos = {
|
|
204
276
|
give: (input: GiveKudosInput) =>
|
|
205
277
|
this.mutation<Awaited<ReturnType<SynomemService['kudos']['give']>>>('POST', 'kudos', input),
|
package/src/schemas.ts
CHANGED
|
@@ -17,12 +17,34 @@ const reservedIds = new Set([
|
|
|
17
17
|
'lpt1',
|
|
18
18
|
]);
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* A handle: the human-friendly name for an agent, unique within its workspace.
|
|
22
|
+
*
|
|
23
|
+
* Mutable, unlike the canonical ID. People and agents type this, so it stays
|
|
24
|
+
* lowercase kebab and refuses the reserved words that would collide with
|
|
25
|
+
* filesystem or route segments.
|
|
26
|
+
*/
|
|
27
|
+
export const agentHandleSchema = z
|
|
21
28
|
.string()
|
|
22
29
|
.min(1)
|
|
23
30
|
.max(63)
|
|
24
31
|
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, 'Use lowercase ASCII letters, digits, and hyphens')
|
|
25
|
-
.refine((
|
|
32
|
+
.refine((handle) => !reservedIds.has(handle), 'Reserved agent handle');
|
|
33
|
+
|
|
34
|
+
/** A canonical opaque agent ID: a ULID, uppercase Crockford base32. */
|
|
35
|
+
export const agentUlidSchema = z.string().regex(/^[0-9A-HJKMNP-TV-Z]{26}$/);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* An actor ID as it appears in an event.
|
|
39
|
+
*
|
|
40
|
+
* Accepts a ULID or a handle-shaped name. New agents are created with an opaque
|
|
41
|
+
* ULID so a handle can be renamed without orphaning the events that reference
|
|
42
|
+
* the actor; agents that predate that, and human and system actors, carry a
|
|
43
|
+
* name-shaped ID. Widening rather than replacing keeps append-only history
|
|
44
|
+
* readable — rewriting the actor ID inside stored events to tidy the format
|
|
45
|
+
* would be exactly the rewrite the event log exists to prevent.
|
|
46
|
+
*/
|
|
47
|
+
export const agentIdSchema = z.union([agentUlidSchema, agentHandleSchema]);
|
|
26
48
|
|
|
27
49
|
/**
|
|
28
50
|
* An alias as written, folded to the canonical lowercase form.
|
|
@@ -110,16 +132,42 @@ export const evidenceSchema = z
|
|
|
110
132
|
});
|
|
111
133
|
|
|
112
134
|
export const profileSchema = z.object({
|
|
135
|
+
/** Canonical, opaque and immutable. Events reference this, never the handle. */
|
|
113
136
|
id: agentIdSchema,
|
|
137
|
+
handle: agentHandleSchema,
|
|
114
138
|
displayName: z.string().trim().min(1).max(200),
|
|
115
139
|
aliases: z.array(agentAliasSchema).max(50).optional(),
|
|
116
140
|
description: z.string().trim().max(2000).optional(),
|
|
141
|
+
/** Archived agents keep their history and stop being able to act. */
|
|
142
|
+
status: z.enum(['active', 'archived']).default('active'),
|
|
117
143
|
createdAt: z.string().datetime({ offset: true }),
|
|
118
144
|
metadata: metadataSchema.optional(),
|
|
119
145
|
});
|
|
120
146
|
|
|
121
|
-
|
|
122
|
-
|
|
147
|
+
/**
|
|
148
|
+
* Creating an agent names a handle; the canonical ID is generated, never
|
|
149
|
+
* supplied. A caller that could choose the ID could choose one that collides
|
|
150
|
+
* with an archived agent's history.
|
|
151
|
+
*/
|
|
152
|
+
export const createAgentSchema = z
|
|
153
|
+
.object({
|
|
154
|
+
handle: agentHandleSchema,
|
|
155
|
+
displayName: z.string().trim().min(1).max(200),
|
|
156
|
+
aliases: z.array(agentAliasSchema).max(50).optional(),
|
|
157
|
+
description: z.string().trim().max(2000).optional(),
|
|
158
|
+
metadata: metadataSchema.optional(),
|
|
159
|
+
})
|
|
160
|
+
.strict();
|
|
161
|
+
|
|
162
|
+
export const updateAgentSchema = z
|
|
163
|
+
.object({
|
|
164
|
+
handle: agentHandleSchema.optional(),
|
|
165
|
+
displayName: z.string().trim().min(1).max(200).optional(),
|
|
166
|
+
aliases: z.array(agentAliasSchema).max(50).optional(),
|
|
167
|
+
description: z.string().trim().max(2000).optional(),
|
|
168
|
+
metadata: metadataSchema.optional(),
|
|
169
|
+
})
|
|
170
|
+
.strict();
|
|
123
171
|
|
|
124
172
|
/**
|
|
125
173
|
* A runtime binding is a claim about where an agent runs, so the fields stay
|
|
@@ -196,7 +244,14 @@ const agentCreatedSchema = baseEventSchema.extend({
|
|
|
196
244
|
const agentUpdatedSchema = baseEventSchema.extend({
|
|
197
245
|
type: z.literal('agent.updated'),
|
|
198
246
|
agentId: agentIdSchema,
|
|
199
|
-
|
|
247
|
+
/*
|
|
248
|
+
* Archiving is recorded as an update, so `status` belongs in the event even
|
|
249
|
+
* though callers cannot set it through `agents.update` — it moves through
|
|
250
|
+
* `archive` and `restore`, which keep the transition explicit.
|
|
251
|
+
*/
|
|
252
|
+
changes: updateAgentSchema.extend({
|
|
253
|
+
status: z.enum(['active', 'archived']).optional(),
|
|
254
|
+
}),
|
|
200
255
|
});
|
|
201
256
|
|
|
202
257
|
const memoSentSchema = baseEventSchema.extend({
|
|
@@ -257,6 +312,47 @@ const noteArchivedSchema = baseEventSchema.extend({
|
|
|
257
312
|
noteId: z.string().length(26),
|
|
258
313
|
});
|
|
259
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Post events. A post carries no recipient, assignee or visibility: it is
|
|
317
|
+
* addressed to the workspace, and workspace membership is the audience. Adding
|
|
318
|
+
* a visibility field would create a second, weaker way to hide a record.
|
|
319
|
+
*/
|
|
320
|
+
const postFields = {
|
|
321
|
+
title: z
|
|
322
|
+
.string()
|
|
323
|
+
.trim()
|
|
324
|
+
.min(1)
|
|
325
|
+
.max(200)
|
|
326
|
+
.regex(/^[^\r\n]+$/),
|
|
327
|
+
body: z.string().trim().min(1).max(32_000),
|
|
328
|
+
tags: z.array(kudosTagSchema).max(20).optional(),
|
|
329
|
+
};
|
|
330
|
+
const postCreatedSchema = baseEventSchema.extend({
|
|
331
|
+
type: z.literal('post.created'),
|
|
332
|
+
...postFields,
|
|
333
|
+
replyTo: z.string().length(26).optional(),
|
|
334
|
+
});
|
|
335
|
+
const postEditedSchema = baseEventSchema.extend({
|
|
336
|
+
type: z.literal('post.edited'),
|
|
337
|
+
postId: z.string().length(26),
|
|
338
|
+
...postFields,
|
|
339
|
+
});
|
|
340
|
+
const postArchivedSchema = baseEventSchema.extend({
|
|
341
|
+
type: z.literal('post.archived'),
|
|
342
|
+
postId: z.string().length(26),
|
|
343
|
+
reason: z.string().trim().min(1).max(2000).optional(),
|
|
344
|
+
});
|
|
345
|
+
const postAcknowledgedSchema = baseEventSchema.extend({
|
|
346
|
+
type: z.literal('post.acknowledged'),
|
|
347
|
+
postId: z.string().length(26),
|
|
348
|
+
note: z.string().trim().min(1).max(2000).optional(),
|
|
349
|
+
});
|
|
350
|
+
const postAcknowledgmentWithdrawnSchema = baseEventSchema.extend({
|
|
351
|
+
type: z.literal('post.acknowledgment.withdrawn'),
|
|
352
|
+
postId: z.string().length(26),
|
|
353
|
+
reason: z.string().trim().min(1).max(2000).optional(),
|
|
354
|
+
});
|
|
355
|
+
|
|
260
356
|
const taskDueSchema = z.discriminatedUnion('kind', [
|
|
261
357
|
z.object({
|
|
262
358
|
kind: z.literal('date'),
|
|
@@ -380,6 +476,11 @@ const todoArchivedSchema = baseEventSchema.extend({
|
|
|
380
476
|
});
|
|
381
477
|
|
|
382
478
|
export const eventSchema = z.discriminatedUnion('type', [
|
|
479
|
+
postCreatedSchema,
|
|
480
|
+
postEditedSchema,
|
|
481
|
+
postArchivedSchema,
|
|
482
|
+
postAcknowledgedSchema,
|
|
483
|
+
postAcknowledgmentWithdrawnSchema,
|
|
383
484
|
kudosGivenSchema,
|
|
384
485
|
acknowledgedSchema,
|
|
385
486
|
revokedSchema,
|
|
@@ -477,6 +578,24 @@ export const sendMemoSchema = z
|
|
|
477
578
|
...mutationMetadata,
|
|
478
579
|
})
|
|
479
580
|
.strict();
|
|
581
|
+
export const createPostSchema = z
|
|
582
|
+
.object({
|
|
583
|
+
...postFields,
|
|
584
|
+
replyTo: z.string().length(26).optional(),
|
|
585
|
+
...mutationMetadata,
|
|
586
|
+
})
|
|
587
|
+
.strict();
|
|
588
|
+
export const updatePostSchema = z
|
|
589
|
+
.object({
|
|
590
|
+
postId: z.string().length(26),
|
|
591
|
+
expectedVersion: z.number().int().min(1),
|
|
592
|
+
title: postFields.title.optional(),
|
|
593
|
+
body: postFields.body.optional(),
|
|
594
|
+
tags: postFields.tags,
|
|
595
|
+
idempotencyKey: z.string().trim().min(1).max(200).optional(),
|
|
596
|
+
})
|
|
597
|
+
.strict();
|
|
598
|
+
|
|
480
599
|
export const createNoteSchema = z
|
|
481
600
|
.object({
|
|
482
601
|
ownerAgentId: agentIdSchema.optional(),
|
|
@@ -553,7 +672,7 @@ export const updateTodoSchema = z
|
|
|
553
672
|
export const itemListInputSchema = z
|
|
554
673
|
.object({
|
|
555
674
|
kinds: z
|
|
556
|
-
.array(z.enum(['kudos', 'memo', 'note', 'task', 'todo']))
|
|
675
|
+
.array(z.enum(['kudos', 'memo', 'note', 'post', 'task', 'todo']))
|
|
557
676
|
.max(5)
|
|
558
677
|
.optional(),
|
|
559
678
|
participantAgentId: agentIdSchema.optional(),
|
package/src/service.ts
CHANGED
|
@@ -2,6 +2,10 @@ import type {
|
|
|
2
2
|
ActorIdentity,
|
|
3
3
|
AgentDirectoryEntry,
|
|
4
4
|
AgentProfile,
|
|
5
|
+
CreatePostInput,
|
|
6
|
+
PostRecord,
|
|
7
|
+
PostRoster,
|
|
8
|
+
UpdatePostInput,
|
|
5
9
|
AgentResolution,
|
|
6
10
|
AgentRuntimeBinding,
|
|
7
11
|
BindRuntimeInput,
|
|
@@ -75,11 +79,37 @@ export interface SynomemDomainService {
|
|
|
75
79
|
get(idOrAlias: string): Promise<AgentProfile>;
|
|
76
80
|
list(): Promise<AgentProfile[]>;
|
|
77
81
|
resolve(query: string): Promise<AgentResolution>;
|
|
82
|
+
archive(idOrAlias: string): Promise<AgentProfile>;
|
|
83
|
+
restore(idOrAlias: string): Promise<AgentProfile>;
|
|
84
|
+
addAliases(idOrAlias: string, aliases: string[]): Promise<AgentProfile>;
|
|
85
|
+
removeAliases(idOrAlias: string, aliases: string[]): Promise<AgentProfile>;
|
|
78
86
|
directory(): Promise<AgentDirectoryEntry[]>;
|
|
79
87
|
bindings(idOrAlias: string): Promise<AgentRuntimeBinding[]>;
|
|
80
88
|
bindRuntime(input: BindRuntimeInput): Promise<AgentRuntimeBinding>;
|
|
81
89
|
unbindRuntime(bindingId: string): Promise<boolean>;
|
|
82
90
|
};
|
|
91
|
+
readonly posts: {
|
|
92
|
+
create(input: CreatePostInput): Promise<{
|
|
93
|
+
record: PostRecord;
|
|
94
|
+
created: boolean;
|
|
95
|
+
deduplicated: boolean;
|
|
96
|
+
}>;
|
|
97
|
+
list(input?: Omit<ItemListInput, 'kinds'>): Promise<Page<ItemSummary>>;
|
|
98
|
+
get(id: string): Promise<PostRecord>;
|
|
99
|
+
update(input: UpdatePostInput): Promise<PostRecord>;
|
|
100
|
+
archive(input: {
|
|
101
|
+
postId: string;
|
|
102
|
+
reason?: string;
|
|
103
|
+
idempotencyKey?: string;
|
|
104
|
+
}): Promise<PostRecord>;
|
|
105
|
+
acknowledge(input: {
|
|
106
|
+
postId: string;
|
|
107
|
+
note?: string;
|
|
108
|
+
idempotencyKey?: string;
|
|
109
|
+
}): Promise<PostRecord>;
|
|
110
|
+
withdrawAcknowledgment(input: { postId: string; reason?: string }): Promise<PostRecord>;
|
|
111
|
+
roster(postId: string): Promise<PostRoster>;
|
|
112
|
+
};
|
|
83
113
|
readonly kudos: {
|
|
84
114
|
give(input: GiveKudosInput): Promise<GiveKudosResult>;
|
|
85
115
|
list(input?: KudosListInput): Promise<Page<KudosSummary>>;
|
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,37 @@ 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
|
+
|
|
258
|
+
const migrationV7 = `
|
|
259
|
+
-- Opaque canonical IDs, with the handle as a separate mutable name.
|
|
260
|
+
--
|
|
261
|
+
-- Existing agents keep their name-shaped ID and take it as their handle too.
|
|
262
|
+
-- Rewriting the actor ID inside stored events to tidy the format would be
|
|
263
|
+
-- exactly the rewrite an append-only log exists to prevent, so history stays
|
|
264
|
+
-- as written and only NEW agents get a generated opaque ID.
|
|
265
|
+
ALTER TABLE agents ADD COLUMN handle TEXT;
|
|
266
|
+
UPDATE agents SET handle = id WHERE handle IS NULL;
|
|
267
|
+
CREATE UNIQUE INDEX agents_handle ON agents(handle);
|
|
268
|
+
|
|
269
|
+
-- Archived agents keep their records and stop being able to act.
|
|
270
|
+
ALTER TABLE agents ADD COLUMN status TEXT NOT NULL DEFAULT 'active';
|
|
271
|
+
`;
|
|
272
|
+
|
|
240
273
|
const migrationV3 = `
|
|
241
274
|
DROP TRIGGER IF EXISTS events_append_only_update;
|
|
242
275
|
DROP TRIGGER IF EXISTS events_append_only_delete;
|
|
@@ -365,6 +398,7 @@ function eventKind(event: SynomemEvent): RecordKind | undefined {
|
|
|
365
398
|
if (event.type.startsWith('kudos.')) return 'kudos';
|
|
366
399
|
if (event.type.startsWith('memo.')) return 'memo';
|
|
367
400
|
if (event.type.startsWith('note.')) return 'note';
|
|
401
|
+
if (event.type.startsWith('post.')) return 'post';
|
|
368
402
|
if (event.type.startsWith('task.')) return 'task';
|
|
369
403
|
if (event.type.startsWith('todo.')) return 'todo';
|
|
370
404
|
return undefined;
|
|
@@ -411,7 +445,8 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
411
445
|
|
|
412
446
|
constructor(options: StorageOptions) {
|
|
413
447
|
this.home = resolve(options.home);
|
|
414
|
-
|
|
448
|
+
// The home is the storage directory; see configLocation in backend.ts.
|
|
449
|
+
this.storageDirectory = this.home;
|
|
415
450
|
this.databasePath = join(this.storageDirectory, 'synomem.sqlite3');
|
|
416
451
|
this.configPath = join(this.storageDirectory, 'config.json');
|
|
417
452
|
this.readOnly = options.readOnly;
|
|
@@ -499,7 +534,7 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
499
534
|
const version = Number(
|
|
500
535
|
(db.prepare('PRAGMA user_version').get() as { user_version: number }).user_version,
|
|
501
536
|
);
|
|
502
|
-
if (version >
|
|
537
|
+
if (version > 7) {
|
|
503
538
|
throw new SynomemError(
|
|
504
539
|
'UNSUPPORTED_SCHEMA',
|
|
505
540
|
`Database schema version ${version} is newer than this package supports.`,
|
|
@@ -567,14 +602,38 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
567
602
|
db.exec('PRAGMA user_version = 5');
|
|
568
603
|
});
|
|
569
604
|
}
|
|
605
|
+
const afterV5 = Number(
|
|
606
|
+
(db.prepare('PRAGMA user_version').get() as { user_version: number }).user_version,
|
|
607
|
+
);
|
|
608
|
+
if (afterV5 === 5) {
|
|
609
|
+
this.transactionSync(() => {
|
|
610
|
+
db.exec(migrationV6);
|
|
611
|
+
db.prepare(
|
|
612
|
+
'INSERT OR IGNORE INTO schema_migrations(version, applied_at) VALUES (?, ?)',
|
|
613
|
+
).run(6, new Date().toISOString());
|
|
614
|
+
db.exec('PRAGMA user_version = 6');
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
const afterV6 = Number(
|
|
618
|
+
(db.prepare('PRAGMA user_version').get() as { user_version: number }).user_version,
|
|
619
|
+
);
|
|
620
|
+
if (afterV6 === 6) {
|
|
621
|
+
this.transactionSync(() => {
|
|
622
|
+
db.exec(migrationV7);
|
|
623
|
+
db.prepare(
|
|
624
|
+
'INSERT OR IGNORE INTO schema_migrations(version, applied_at) VALUES (?, ?)',
|
|
625
|
+
).run(7, new Date().toISOString());
|
|
626
|
+
db.exec('PRAGMA user_version = 7');
|
|
627
|
+
});
|
|
628
|
+
}
|
|
570
629
|
}
|
|
571
630
|
|
|
572
631
|
private assertSchemaSupported(): void {
|
|
573
632
|
const version = Number(
|
|
574
633
|
(this.db().prepare('PRAGMA user_version').get() as { user_version: number }).user_version,
|
|
575
634
|
);
|
|
576
|
-
if (version !==
|
|
577
|
-
if (version >= 1 && version <=
|
|
635
|
+
if (version !== 7) {
|
|
636
|
+
if (version >= 1 && version <= 6) {
|
|
578
637
|
throw new SynomemError(
|
|
579
638
|
'UNSUPPORTED_SCHEMA',
|
|
580
639
|
`Database schema version ${version} requires migration. Open this home once with readOnly: false, then retry the read-only client.`,
|
|
@@ -835,6 +894,61 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
835
894
|
ownerAgentId: event.ownerAgentId,
|
|
836
895
|
ownerDisplayName: event.ownerDisplayName,
|
|
837
896
|
});
|
|
897
|
+
} else if (event.type === 'post.created') {
|
|
898
|
+
/*
|
|
899
|
+
* A post is addressed to the workspace, so it is recorded as `workspace`
|
|
900
|
+
* visibility rather than carrying a visibility of its own. There is no
|
|
901
|
+
* owner-scoped or recipient-scoped read filter to apply: everyone who can
|
|
902
|
+
* read the workspace can read it, which is the whole point of the domain.
|
|
903
|
+
*/
|
|
904
|
+
insert({
|
|
905
|
+
kind: 'post',
|
|
906
|
+
title: event.title,
|
|
907
|
+
tags: event.tags,
|
|
908
|
+
visibility: 'workspace',
|
|
909
|
+
status: 'active',
|
|
910
|
+
});
|
|
911
|
+
} else if (event.type === 'post.edited') {
|
|
912
|
+
this.db()
|
|
913
|
+
.prepare(
|
|
914
|
+
`UPDATE items_current SET title = ?, tags_json = ?,
|
|
915
|
+
updated_sequence = ?, updated_at = ? WHERE item_id = ?`,
|
|
916
|
+
)
|
|
917
|
+
.run(
|
|
918
|
+
event.title,
|
|
919
|
+
JSON.stringify(event.tags ?? []),
|
|
920
|
+
sequence,
|
|
921
|
+
event.createdAt,
|
|
922
|
+
event.postId,
|
|
923
|
+
);
|
|
924
|
+
} else if (event.type === 'post.archived') {
|
|
925
|
+
updateStatus('archived');
|
|
926
|
+
} else if (event.type === 'post.acknowledged') {
|
|
927
|
+
// One row per actor per post: acknowledging twice is the same statement,
|
|
928
|
+
// not a second one.
|
|
929
|
+
this.db()
|
|
930
|
+
.prepare(
|
|
931
|
+
`INSERT INTO post_acknowledgments
|
|
932
|
+
(post_id, actor_kind, actor_id, actor_display_name, note, acknowledged_at)
|
|
933
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
934
|
+
ON CONFLICT(post_id, actor_kind, actor_id) DO UPDATE SET
|
|
935
|
+
note = excluded.note,
|
|
936
|
+
acknowledged_at = excluded.acknowledged_at`,
|
|
937
|
+
)
|
|
938
|
+
.run(
|
|
939
|
+
event.postId,
|
|
940
|
+
event.actor.kind,
|
|
941
|
+
event.actor.id,
|
|
942
|
+
event.actor.displayName ?? null,
|
|
943
|
+
event.note ?? null,
|
|
944
|
+
event.createdAt,
|
|
945
|
+
);
|
|
946
|
+
} else if (event.type === 'post.acknowledgment.withdrawn') {
|
|
947
|
+
this.db()
|
|
948
|
+
.prepare(
|
|
949
|
+
'DELETE FROM post_acknowledgments WHERE post_id = ? AND actor_kind = ? AND actor_id = ?',
|
|
950
|
+
)
|
|
951
|
+
.run(event.postId, event.actor.kind, event.actor.id);
|
|
838
952
|
} else if (event.type === 'note.revised') {
|
|
839
953
|
this.db()
|
|
840
954
|
.prepare(
|
|
@@ -1418,7 +1532,7 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
1418
1532
|
this.db()
|
|
1419
1533
|
.prepare(
|
|
1420
1534
|
`SELECT COUNT(*) AS count FROM events WHERE type IN
|
|
1421
|
-
('kudos.given', 'memo.sent', 'note.created', 'task.created')`,
|
|
1535
|
+
('kudos.given', 'memo.sent', 'note.created', 'post.created', 'task.created')`,
|
|
1422
1536
|
)
|
|
1423
1537
|
.get() as { count: number }
|
|
1424
1538
|
).count,
|
|
@@ -1444,12 +1558,23 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
1444
1558
|
return { schemaVersion, appliedVersions };
|
|
1445
1559
|
}
|
|
1446
1560
|
|
|
1561
|
+
/**
|
|
1562
|
+
* Aliases that also name an agent directly.
|
|
1563
|
+
*
|
|
1564
|
+
* Matches against the HANDLE as well as the canonical ID. With opaque IDs an
|
|
1565
|
+
* alias can no longer accidentally equal one, but it can easily equal another
|
|
1566
|
+
* agent's handle — which is the collision that actually makes a lookup
|
|
1567
|
+
* ambiguous now.
|
|
1568
|
+
*/
|
|
1447
1569
|
aliasIdentityConflicts(): Array<{ alias: string; agentId: string }> {
|
|
1448
1570
|
return this.db()
|
|
1449
1571
|
.prepare(
|
|
1450
1572
|
`SELECT x.alias, x.agent_id AS agentId
|
|
1451
|
-
FROM aliases x
|
|
1452
|
-
|
|
1573
|
+
FROM aliases x
|
|
1574
|
+
JOIN agents a ON lower(a.id) = x.normalized_alias
|
|
1575
|
+
OR lower(a.handle) = x.normalized_alias
|
|
1576
|
+
WHERE a.id != x.agent_id
|
|
1577
|
+
ORDER BY x.alias`,
|
|
1453
1578
|
)
|
|
1454
1579
|
.all() as unknown as Array<{ alias: string; agentId: string }>;
|
|
1455
1580
|
}
|
|
@@ -1571,6 +1696,11 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
1571
1696
|
'note.created',
|
|
1572
1697
|
'note.revised',
|
|
1573
1698
|
'note.archived',
|
|
1699
|
+
'post.created',
|
|
1700
|
+
'post.edited',
|
|
1701
|
+
'post.archived',
|
|
1702
|
+
'post.acknowledged',
|
|
1703
|
+
'post.acknowledgment.withdrawn',
|
|
1574
1704
|
'task.created',
|
|
1575
1705
|
'task.updated',
|
|
1576
1706
|
'task.completed',
|
|
@@ -1636,10 +1766,13 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
1636
1766
|
const parsed = profileSchema.parse(profile);
|
|
1637
1767
|
this.db()
|
|
1638
1768
|
.prepare(
|
|
1639
|
-
|
|
1769
|
+
`INSERT INTO agents(id, handle, status, display_name, profile_json, created_at, updated_at)
|
|
1770
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
1640
1771
|
)
|
|
1641
1772
|
.run(
|
|
1642
1773
|
parsed.id,
|
|
1774
|
+
parsed.handle,
|
|
1775
|
+
parsed.status,
|
|
1643
1776
|
parsed.displayName,
|
|
1644
1777
|
JSON.stringify(parsed),
|
|
1645
1778
|
parsed.createdAt,
|
|
@@ -1651,8 +1784,18 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
1651
1784
|
updateAgent(profile: AgentProfile, updatedAt: string): void {
|
|
1652
1785
|
const parsed = profileSchema.parse(profile);
|
|
1653
1786
|
this.db()
|
|
1654
|
-
.prepare(
|
|
1655
|
-
|
|
1787
|
+
.prepare(
|
|
1788
|
+
`UPDATE agents SET handle = ?, status = ?, display_name = ?, profile_json = ?,
|
|
1789
|
+
updated_at = ? WHERE id = ?`,
|
|
1790
|
+
)
|
|
1791
|
+
.run(
|
|
1792
|
+
parsed.handle,
|
|
1793
|
+
parsed.status,
|
|
1794
|
+
parsed.displayName,
|
|
1795
|
+
JSON.stringify(parsed),
|
|
1796
|
+
updatedAt,
|
|
1797
|
+
parsed.id,
|
|
1798
|
+
);
|
|
1656
1799
|
this.db().prepare('DELETE FROM aliases WHERE agent_id = ?').run(parsed.id);
|
|
1657
1800
|
this.insertAliases(parsed.id, parsed.aliases ?? []);
|
|
1658
1801
|
}
|
|
@@ -1679,14 +1822,69 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
1679
1822
|
`SELECT DISTINCT a.profile_json
|
|
1680
1823
|
FROM agents a
|
|
1681
1824
|
LEFT JOIN aliases x ON x.agent_id = a.id
|
|
1682
|
-
WHERE lower(a.id) = ? OR x.normalized_alias = ?
|
|
1825
|
+
WHERE lower(a.id) = ? OR lower(a.handle) = ? OR x.normalized_alias = ?
|
|
1683
1826
|
ORDER BY a.id ASC`,
|
|
1684
1827
|
)
|
|
1685
|
-
.all(normalized, normalized) as unknown as ProfileRow[];
|
|
1828
|
+
.all(normalized, normalized, normalized) as unknown as ProfileRow[];
|
|
1686
1829
|
const candidates = rows.map((row) => profileSchema.parse(JSON.parse(row.profile_json)));
|
|
1687
1830
|
return candidates.length === 1 ? { match: candidates[0]!, candidates } : { candidates };
|
|
1688
1831
|
}
|
|
1689
1832
|
|
|
1833
|
+
/* -------------------------------------------------------- post acknowledgment */
|
|
1834
|
+
|
|
1835
|
+
listPostAcknowledgments(postId: string): PostAcknowledgment[] {
|
|
1836
|
+
const rows = this.db()
|
|
1837
|
+
.prepare(
|
|
1838
|
+
`SELECT actor_kind, actor_id, actor_display_name, note, acknowledged_at
|
|
1839
|
+
FROM post_acknowledgments WHERE post_id = ? ORDER BY acknowledged_at ASC`,
|
|
1840
|
+
)
|
|
1841
|
+
.all(postId) as unknown as Array<{
|
|
1842
|
+
actor_kind: string;
|
|
1843
|
+
actor_id: string;
|
|
1844
|
+
actor_display_name: string | null;
|
|
1845
|
+
note: string | null;
|
|
1846
|
+
acknowledged_at: string;
|
|
1847
|
+
}>;
|
|
1848
|
+
return rows.map((row) => ({
|
|
1849
|
+
actor: {
|
|
1850
|
+
kind: row.actor_kind as ActorIdentity['kind'],
|
|
1851
|
+
id: row.actor_id,
|
|
1852
|
+
...(row.actor_display_name ? { displayName: row.actor_display_name } : {}),
|
|
1853
|
+
},
|
|
1854
|
+
acknowledgedAt: row.acknowledged_at,
|
|
1855
|
+
...(row.note ? { note: row.note } : {}),
|
|
1856
|
+
}));
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
/**
|
|
1860
|
+
* Who has acknowledged a post and who has not.
|
|
1861
|
+
*
|
|
1862
|
+
* The denominator is agents that existed when the post was written. An agent
|
|
1863
|
+
* created afterwards is counted separately rather than listed as
|
|
1864
|
+
* outstanding — it was not there, and a roster that says otherwise accuses a
|
|
1865
|
+
* newcomer of ignoring something written before it arrived.
|
|
1866
|
+
*/
|
|
1867
|
+
postRoster(postId: string): PostRoster | undefined {
|
|
1868
|
+
const post = this.db()
|
|
1869
|
+
.prepare("SELECT created_at FROM items_current WHERE item_id = ? AND kind = 'post'")
|
|
1870
|
+
.get(postId) as { created_at: string } | undefined;
|
|
1871
|
+
if (!post) return undefined;
|
|
1872
|
+
|
|
1873
|
+
const acknowledged = this.listPostAcknowledgments(postId);
|
|
1874
|
+
const acknowledgedIds = new Set(acknowledged.map((entry) => entry.actor.id));
|
|
1875
|
+
|
|
1876
|
+
const eligible = this.db()
|
|
1877
|
+
.prepare('SELECT id, display_name, created_at FROM agents ORDER BY id ASC')
|
|
1878
|
+
.all() as unknown as Array<{ id: string; display_name: string; created_at: string }>;
|
|
1879
|
+
|
|
1880
|
+
const outstanding = eligible
|
|
1881
|
+
.filter((agent) => agent.created_at <= post.created_at && !acknowledgedIds.has(agent.id))
|
|
1882
|
+
.map((agent) => ({ id: agent.id, displayName: agent.display_name }));
|
|
1883
|
+
const joinedSince = eligible.filter((agent) => agent.created_at > post.created_at).length;
|
|
1884
|
+
|
|
1885
|
+
return { postId, acknowledged, outstanding, joinedSince };
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1690
1888
|
/* ------------------------------------------------------- runtime bindings */
|
|
1691
1889
|
|
|
1692
1890
|
listRuntimeBindings(agentId: string): AgentRuntimeBinding[] {
|
|
@@ -1818,11 +2016,16 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
1818
2016
|
});
|
|
1819
2017
|
}
|
|
1820
2018
|
|
|
1821
|
-
|
|
2019
|
+
/**
|
|
2020
|
+
* @param directory the agent's projection directory name, which is its
|
|
2021
|
+
* handle rather than its canonical ID: these rows are keyed by the path on
|
|
2022
|
+
* disk, and projections are named for people to read.
|
|
2023
|
+
*/
|
|
2024
|
+
replaceAgentProjectionManifest(directory: string, paths: string[], generatedAt: string): void {
|
|
1822
2025
|
this.transactionSync(() => {
|
|
1823
2026
|
this.db()
|
|
1824
2027
|
.prepare('DELETE FROM projection_manifest WHERE path LIKE ? OR path LIKE ?')
|
|
1825
|
-
.run(`${
|
|
2028
|
+
.run(`${directory}/%`, `${directory}\\%`);
|
|
1826
2029
|
const insert = this.db().prepare(
|
|
1827
2030
|
'INSERT INTO projection_manifest(path, generated_at) VALUES (?, ?)',
|
|
1828
2031
|
);
|