synomem 0.1.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/AGENTS.md +42 -0
- package/ARCHITECTURE.md +126 -0
- package/CHANGELOG.md +35 -0
- package/CODE_OF_CONDUCT.md +29 -0
- package/CONTRIBUTING.md +48 -0
- package/LICENSE +21 -0
- package/README.md +247 -0
- package/SECURITY.md +117 -0
- package/dist/backend.d.ts +9 -0
- package/dist/backend.d.ts.map +1 -0
- package/dist/backend.js +60 -0
- package/dist/backend.js.map +1 -0
- package/dist/cli.d.ts +34 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1040 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +168 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +1031 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +33 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +153 -0
- package/dist/config.js.map +1 -0
- package/dist/credentials.d.ts +36 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +127 -0
- package/dist/credentials.js.map +1 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +60 -0
- package/dist/errors.js.map +1 -0
- package/dist/fs-utils.d.ts +7 -0
- package/dist/fs-utils.d.ts.map +1 -0
- package/dist/fs-utils.js +68 -0
- package/dist/fs-utils.js.map +1 -0
- package/dist/import.d.ts +655 -0
- package/dist/import.d.ts.map +1 -0
- package/dist/import.js +226 -0
- package/dist/import.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +14 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +698 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp-server.d.ts +3 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +45 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/oauth.d.ts +27 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +201 -0
- package/dist/oauth.js.map +1 -0
- package/dist/ports/projections.d.ts +6 -0
- package/dist/ports/projections.d.ts.map +1 -0
- package/dist/ports/projections.js +2 -0
- package/dist/ports/projections.js.map +1 -0
- package/dist/ports/repository.d.ts +31 -0
- package/dist/ports/repository.d.ts.map +1 -0
- package/dist/ports/repository.js +2 -0
- package/dist/ports/repository.js.map +1 -0
- package/dist/projections.d.ts +24 -0
- package/dist/projections.d.ts.map +1 -0
- package/dist/projections.js +531 -0
- package/dist/projections.js.map +1 -0
- package/dist/remote.d.ts +120 -0
- package/dist/remote.d.ts.map +1 -0
- package/dist/remote.js +286 -0
- package/dist/remote.js.map +1 -0
- package/dist/schemas.d.ts +901 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +424 -0
- package/dist/schemas.js.map +1 -0
- package/dist/service.d.ts +126 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +2 -0
- package/dist/service.js.map +1 -0
- package/dist/skill-install.d.ts +34 -0
- package/dist/skill-install.d.ts.map +1 -0
- package/dist/skill-install.js +256 -0
- package/dist/skill-install.js.map +1 -0
- package/dist/storage.d.ts +94 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +1229 -0
- package/dist/storage.js.map +1 -0
- package/dist/types.d.ts +442 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +14 -0
- package/dist/version.js.map +1 -0
- package/docs/cli.md +180 -0
- package/docs/examples.md +70 -0
- package/docs/mcp.md +96 -0
- package/docs/recovery.md +45 -0
- package/docs/releasing.md +61 -0
- package/docs/skill.md +93 -0
- package/docs/storage-format.md +83 -0
- package/openapi/synomem-v1.yaml +674 -0
- package/package.json +100 -0
- package/skills/synomem/SKILL.md +71 -0
- package/skills/synomem/agents/openai.yaml +8 -0
- package/skills/synomem/references/examples.md +28 -0
- package/src/backend.ts +91 -0
- package/src/cli.ts +1623 -0
- package/src/client.ts +1261 -0
- package/src/config.ts +170 -0
- package/src/credentials.ts +194 -0
- package/src/errors.ts +64 -0
- package/src/fs-utils.ts +85 -0
- package/src/import.ts +284 -0
- package/src/index.ts +61 -0
- package/src/mcp/index.ts +973 -0
- package/src/mcp-server.ts +46 -0
- package/src/oauth.ts +252 -0
- package/src/ports/projections.ts +6 -0
- package/src/ports/repository.ts +67 -0
- package/src/projections.ts +624 -0
- package/src/remote.ts +516 -0
- package/src/schemas.ts +457 -0
- package/src/service.ts +140 -0
- package/src/skill-install.ts +332 -0
- package/src/storage.ts +1603 -0
- package/src/types.ts +458 -0
- package/src/version.ts +16 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,1031 @@
|
|
|
1
|
+
import { accessSync, constants as fsConstants, existsSync, lstatSync } from 'node:fs';
|
|
2
|
+
import { join, relative, resolve } from 'node:path';
|
|
3
|
+
import { ulid } from 'ulid';
|
|
4
|
+
import { resolveHome } from './config.js';
|
|
5
|
+
import { asSynomemError, SynomemError } from './errors.js';
|
|
6
|
+
import { assertNoSymlinkEscape } from './fs-utils.js';
|
|
7
|
+
import { escapeMarkdown, memoRecordsFromEvents, noteRecordsFromEvents, ProjectionManager, recordsFromEvents, todoRecordsFromEvents, } from './projections.js';
|
|
8
|
+
import { actorSchema, agentIdSchema, createAgentSchema, createNoteSchema, createTodoSchema, changesInputSchema, giveKudosSchema, itemListInputSchema, listInputSchema, reviseNoteSchema, sendMemoSchema, updateTodoSchema, updateAgentSchema, } from './schemas.js';
|
|
9
|
+
import { SynomemStorage } from './storage.js';
|
|
10
|
+
export class SynomemCore {
|
|
11
|
+
actor;
|
|
12
|
+
repository;
|
|
13
|
+
projectionWriter;
|
|
14
|
+
clock;
|
|
15
|
+
idGenerator;
|
|
16
|
+
signal;
|
|
17
|
+
administrative;
|
|
18
|
+
initialized = false;
|
|
19
|
+
agents = {
|
|
20
|
+
create: (input) => this.createAgent(input),
|
|
21
|
+
update: (id, changes) => this.updateAgent(id, changes),
|
|
22
|
+
get: (idOrAlias) => this.getAgent(idOrAlias),
|
|
23
|
+
list: () => this.listAgents(),
|
|
24
|
+
};
|
|
25
|
+
kudos = {
|
|
26
|
+
give: (input) => this.giveKudos(input),
|
|
27
|
+
list: (input = {}) => this.listKudos(input),
|
|
28
|
+
changes: (input = {}) => this.listKudosChanges(input),
|
|
29
|
+
get: (id) => this.getKudos(id),
|
|
30
|
+
acknowledge: (input) => this.acknowledgeKudos(input),
|
|
31
|
+
revoke: (input) => this.revokeKudos(input),
|
|
32
|
+
};
|
|
33
|
+
memos = {
|
|
34
|
+
send: (input) => this.sendMemo(input),
|
|
35
|
+
list: (input = {}) => this.listItems({ ...input, kinds: ['memo'] }),
|
|
36
|
+
get: (id) => this.getMemo(id),
|
|
37
|
+
read: (input) => this.readMemo(input),
|
|
38
|
+
archive: (input) => this.archiveMemo(input),
|
|
39
|
+
};
|
|
40
|
+
notes = {
|
|
41
|
+
create: (input) => this.createNote(input),
|
|
42
|
+
list: (input = {}) => this.listItems({ ...input, kinds: ['note'] }),
|
|
43
|
+
get: (id) => this.getNote(id),
|
|
44
|
+
revise: (input) => this.reviseNote(input),
|
|
45
|
+
archive: (input) => this.archiveNote(input),
|
|
46
|
+
};
|
|
47
|
+
todos = {
|
|
48
|
+
create: (input) => this.createTodo(input),
|
|
49
|
+
list: (input = {}) => this.listItems({ ...input, kinds: ['todo'] }),
|
|
50
|
+
get: (id) => this.getTodo(id),
|
|
51
|
+
update: (input) => this.updateTodo(input),
|
|
52
|
+
accept: (input) => this.acceptTodo(input),
|
|
53
|
+
reject: (input) => this.rejectTodo(input),
|
|
54
|
+
complete: (input) => this.completeTodo(input),
|
|
55
|
+
reopen: (input) => this.reopenTodo(input),
|
|
56
|
+
cancel: (input) => this.cancelTodo(input),
|
|
57
|
+
};
|
|
58
|
+
items = {
|
|
59
|
+
list: (input = {}) => this.listItems(input),
|
|
60
|
+
get: (id) => this.getItem(id),
|
|
61
|
+
changes: (input = {}) => this.listItemChanges(input),
|
|
62
|
+
};
|
|
63
|
+
constructor(options) {
|
|
64
|
+
try {
|
|
65
|
+
this.actor = actorSchema.parse(options.actor ?? { kind: 'system', id: 'workspace' });
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
throw asSynomemError(error);
|
|
69
|
+
}
|
|
70
|
+
this.clock = options.clock ?? (() => new Date());
|
|
71
|
+
this.idGenerator = options.idGenerator ?? (() => ulid(this.clock().getTime()));
|
|
72
|
+
this.signal = options.signal;
|
|
73
|
+
this.administrative = options.administrative ?? this.actor.kind === 'human';
|
|
74
|
+
this.repository = options.repository;
|
|
75
|
+
this.projectionWriter = options.projectionWriter;
|
|
76
|
+
}
|
|
77
|
+
async init() {
|
|
78
|
+
this.checkAbort();
|
|
79
|
+
if (this.initialized)
|
|
80
|
+
return;
|
|
81
|
+
await this.repository.init();
|
|
82
|
+
this.initialized = true;
|
|
83
|
+
}
|
|
84
|
+
async close() {
|
|
85
|
+
await this.repository.close();
|
|
86
|
+
this.initialized = false;
|
|
87
|
+
}
|
|
88
|
+
now() {
|
|
89
|
+
return this.clock().toISOString();
|
|
90
|
+
}
|
|
91
|
+
nextId() {
|
|
92
|
+
return this.idGenerator();
|
|
93
|
+
}
|
|
94
|
+
eventBase(aggregateId, aggregateVersion, id = this.nextId()) {
|
|
95
|
+
return {
|
|
96
|
+
schemaVersion: 1,
|
|
97
|
+
id,
|
|
98
|
+
workspaceId: this.repository.config.workspaceId,
|
|
99
|
+
aggregateId,
|
|
100
|
+
aggregateVersion,
|
|
101
|
+
createdAt: this.now(),
|
|
102
|
+
actor: this.actor,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
checkAbort() {
|
|
106
|
+
this.signal?.throwIfAborted();
|
|
107
|
+
}
|
|
108
|
+
validate(operation) {
|
|
109
|
+
try {
|
|
110
|
+
return operation();
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
throw asSynomemError(error);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async createAgent(input) {
|
|
117
|
+
this.checkAbort();
|
|
118
|
+
await this.repository.assertEventCompatibility();
|
|
119
|
+
const parsed = this.validate(() => createAgentSchema.parse(input));
|
|
120
|
+
if (await this.repository.getAgent(parsed.id)) {
|
|
121
|
+
throw new SynomemError('AGENT_EXISTS', `Agent or alias already exists: ${parsed.id}`);
|
|
122
|
+
}
|
|
123
|
+
const aliases = [...new Set(parsed.aliases ?? [])].sort();
|
|
124
|
+
if (aliases.includes(parsed.id)) {
|
|
125
|
+
throw new SynomemError('ALIAS_CONFLICT', 'An agent cannot use its own ID as an alias.');
|
|
126
|
+
}
|
|
127
|
+
for (const alias of aliases) {
|
|
128
|
+
if (await this.repository.getAgent(alias)) {
|
|
129
|
+
throw new SynomemError('ALIAS_CONFLICT', `Alias already belongs to an agent: ${alias}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const profile = {
|
|
133
|
+
id: parsed.id,
|
|
134
|
+
displayName: parsed.displayName,
|
|
135
|
+
...(aliases.length ? { aliases } : {}),
|
|
136
|
+
...(parsed.description !== undefined ? { description: parsed.description } : {}),
|
|
137
|
+
createdAt: this.now(),
|
|
138
|
+
...(parsed.metadata !== undefined ? { metadata: parsed.metadata } : {}),
|
|
139
|
+
};
|
|
140
|
+
const event = {
|
|
141
|
+
...this.eventBase(profile.id, 1),
|
|
142
|
+
type: 'agent.created',
|
|
143
|
+
agent: profile,
|
|
144
|
+
};
|
|
145
|
+
await this.repository.transaction(async () => {
|
|
146
|
+
await this.repository.insertAgent(profile);
|
|
147
|
+
await this.repository.insertEvent(event);
|
|
148
|
+
});
|
|
149
|
+
await this.projectionWriter.syncAgent(profile.id);
|
|
150
|
+
return profile;
|
|
151
|
+
}
|
|
152
|
+
async updateAgent(idOrAlias, changes) {
|
|
153
|
+
this.checkAbort();
|
|
154
|
+
await this.repository.assertEventCompatibility();
|
|
155
|
+
this.validate(() => agentIdSchema.parse(idOrAlias));
|
|
156
|
+
const parsed = this.validate(() => updateAgentSchema.parse(changes));
|
|
157
|
+
const existing = await this.repository.getAgent(idOrAlias);
|
|
158
|
+
if (!existing)
|
|
159
|
+
throw new SynomemError('AGENT_NOT_FOUND', `Unknown agent: ${idOrAlias}`);
|
|
160
|
+
const aliases = parsed.aliases ? [...new Set(parsed.aliases)].sort() : existing.aliases;
|
|
161
|
+
if (aliases?.includes(existing.id)) {
|
|
162
|
+
throw new SynomemError('ALIAS_CONFLICT', 'An agent cannot use its own ID as an alias.');
|
|
163
|
+
}
|
|
164
|
+
for (const alias of aliases ?? []) {
|
|
165
|
+
const owner = await this.repository.getAgent(alias);
|
|
166
|
+
if (owner && owner.id !== existing.id) {
|
|
167
|
+
throw new SynomemError('ALIAS_CONFLICT', `Alias already belongs to ${owner.id}: ${alias}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const updated = {
|
|
171
|
+
...existing,
|
|
172
|
+
...parsed,
|
|
173
|
+
...(aliases?.length ? { aliases } : { aliases: undefined }),
|
|
174
|
+
};
|
|
175
|
+
const changesForEvent = { ...parsed, ...(parsed.aliases ? { aliases } : {}) };
|
|
176
|
+
await this.repository.transaction(async () => {
|
|
177
|
+
const event = {
|
|
178
|
+
...this.eventBase(existing.id, await this.repository.nextAggregateVersion(existing.id)),
|
|
179
|
+
type: 'agent.updated',
|
|
180
|
+
agentId: existing.id,
|
|
181
|
+
changes: changesForEvent,
|
|
182
|
+
};
|
|
183
|
+
await this.repository.updateAgent(updated, event.createdAt);
|
|
184
|
+
await this.repository.insertEvent(event);
|
|
185
|
+
});
|
|
186
|
+
await this.projectionWriter.syncAgent(updated.id);
|
|
187
|
+
return updated;
|
|
188
|
+
}
|
|
189
|
+
async getAgent(idOrAlias) {
|
|
190
|
+
this.checkAbort();
|
|
191
|
+
this.validate(() => agentIdSchema.parse(idOrAlias));
|
|
192
|
+
const profile = await this.repository.getAgent(idOrAlias);
|
|
193
|
+
if (!profile)
|
|
194
|
+
throw new SynomemError('AGENT_NOT_FOUND', `Unknown agent: ${idOrAlias}`);
|
|
195
|
+
return profile;
|
|
196
|
+
}
|
|
197
|
+
async listAgents() {
|
|
198
|
+
this.checkAbort();
|
|
199
|
+
return await this.repository.listAgents();
|
|
200
|
+
}
|
|
201
|
+
async giveKudos(input) {
|
|
202
|
+
this.checkAbort();
|
|
203
|
+
await this.repository.assertEventCompatibility();
|
|
204
|
+
const parsed = this.validate(() => giveKudosSchema.parse({
|
|
205
|
+
...input,
|
|
206
|
+
visibility: input.visibility ?? this.repository.config.defaultVisibility,
|
|
207
|
+
}));
|
|
208
|
+
const recipient = await this.repository.getAgent(parsed.recipientAgentId);
|
|
209
|
+
if (!recipient) {
|
|
210
|
+
throw new SynomemError('AGENT_NOT_FOUND', `Unknown recipient: ${parsed.recipientAgentId}`);
|
|
211
|
+
}
|
|
212
|
+
if (!this.repository.config.allowSelfAwards &&
|
|
213
|
+
this.actor.kind !== 'human' &&
|
|
214
|
+
this.actor.id === recipient.id) {
|
|
215
|
+
throw new SynomemError('SELF_AWARD_FORBIDDEN', 'Non-human actors cannot award kudos to a matching agent identity.');
|
|
216
|
+
}
|
|
217
|
+
const outcome = await this.repository.transaction(async () => {
|
|
218
|
+
const prior = await this.priorMutation(parsed.idempotencyKey, 'kudos.given');
|
|
219
|
+
if (prior?.type === 'kudos.given')
|
|
220
|
+
return { event: prior, created: false };
|
|
221
|
+
const id = this.nextId();
|
|
222
|
+
const event = {
|
|
223
|
+
...this.eventBase(id, 1, id),
|
|
224
|
+
type: 'kudos.given',
|
|
225
|
+
recipientAgentId: recipient.id,
|
|
226
|
+
recipientDisplayName: recipient.displayName,
|
|
227
|
+
title: parsed.title,
|
|
228
|
+
reason: parsed.reason,
|
|
229
|
+
visibility: parsed.visibility,
|
|
230
|
+
...(parsed.evidence ? { evidence: parsed.evidence } : {}),
|
|
231
|
+
...(parsed.tags ? { tags: [...new Set(parsed.tags)].sort() } : {}),
|
|
232
|
+
...(parsed.idempotencyKey ? { idempotencyKey: parsed.idempotencyKey } : {}),
|
|
233
|
+
...(parsed.source ? { source: parsed.source } : {}),
|
|
234
|
+
...(parsed.metadata ? { metadata: parsed.metadata } : {}),
|
|
235
|
+
};
|
|
236
|
+
await this.repository.insertEvent(event);
|
|
237
|
+
return { event, created: true };
|
|
238
|
+
});
|
|
239
|
+
if (outcome.created)
|
|
240
|
+
await this.projectionWriter.syncAgent(recipient.id);
|
|
241
|
+
const record = await this.getKudosRecord(outcome.event.id);
|
|
242
|
+
return { record, created: outcome.created, deduplicated: !outcome.created };
|
|
243
|
+
}
|
|
244
|
+
async getKudosRecord(id) {
|
|
245
|
+
await this.requireVisibleItem(id, 'kudos');
|
|
246
|
+
const record = recordsFromEvents(await this.repository.getReadableSynomemEvents(id))[0];
|
|
247
|
+
if (!record)
|
|
248
|
+
throw new SynomemError('KUDOS_NOT_FOUND', `Unknown kudos: ${id}`);
|
|
249
|
+
return record;
|
|
250
|
+
}
|
|
251
|
+
async getKudos(id) {
|
|
252
|
+
this.checkAbort();
|
|
253
|
+
return await this.getKudosRecord(id);
|
|
254
|
+
}
|
|
255
|
+
async listKudos(input) {
|
|
256
|
+
this.checkAbort();
|
|
257
|
+
const filters = this.validate(() => listInputSchema.parse(input));
|
|
258
|
+
const recipient = filters.recipientAgentId
|
|
259
|
+
? await this.repository.getAgent(filters.recipientAgentId)
|
|
260
|
+
: undefined;
|
|
261
|
+
if (filters.recipientAgentId && !recipient) {
|
|
262
|
+
throw new SynomemError('AGENT_NOT_FOUND', `Unknown agent: ${filters.recipientAgentId}`);
|
|
263
|
+
}
|
|
264
|
+
return await this.repository.listKudosSummaries({
|
|
265
|
+
...filters,
|
|
266
|
+
...(recipient ? { recipientAgentId: recipient.id } : {}),
|
|
267
|
+
}, this.actor);
|
|
268
|
+
}
|
|
269
|
+
async listKudosChanges(input) {
|
|
270
|
+
this.checkAbort();
|
|
271
|
+
const parsed = this.validate(() => changesInputSchema.parse(input));
|
|
272
|
+
return await this.repository.listKudosChanges(parsed.after, parsed.limit, this.actor);
|
|
273
|
+
}
|
|
274
|
+
async acknowledgeKudos(input) {
|
|
275
|
+
this.checkAbort();
|
|
276
|
+
await this.repository.assertEventCompatibility();
|
|
277
|
+
if (input.note !== undefined && (input.note.trim().length < 1 || input.note.length > 2000)) {
|
|
278
|
+
throw new SynomemError('INVALID_INPUT', 'Acknowledgment notes must be 1–2000 characters.');
|
|
279
|
+
}
|
|
280
|
+
const record = await this.getKudosRecord(input.kudosId);
|
|
281
|
+
if (record.acknowledgment)
|
|
282
|
+
return record;
|
|
283
|
+
if (record.revocation)
|
|
284
|
+
throw new SynomemError('INVALID_INPUT', 'Revoked kudos cannot be acknowledged.');
|
|
285
|
+
const isRecipient = this.actor.kind === 'agent' && this.actor.id === record.event.recipientAgentId;
|
|
286
|
+
if (!this.administrative && !isRecipient) {
|
|
287
|
+
throw new SynomemError('ACKNOWLEDGMENT_FORBIDDEN', 'Only the recipient agent or a human administrator may acknowledge kudos.');
|
|
288
|
+
}
|
|
289
|
+
const event = {
|
|
290
|
+
...this.eventBase(record.event.id, 2),
|
|
291
|
+
type: 'kudos.acknowledged',
|
|
292
|
+
kudosId: record.event.id,
|
|
293
|
+
recipientAgentId: record.event.recipientAgentId,
|
|
294
|
+
...(input.note ? { note: input.note.trim() } : {}),
|
|
295
|
+
};
|
|
296
|
+
await this.repository.transaction(() => this.repository.insertEvent(event));
|
|
297
|
+
await this.projectionWriter.syncAgent(record.event.recipientAgentId);
|
|
298
|
+
return await this.getKudosRecord(input.kudosId);
|
|
299
|
+
}
|
|
300
|
+
async revokeKudos(input) {
|
|
301
|
+
this.checkAbort();
|
|
302
|
+
await this.repository.assertEventCompatibility();
|
|
303
|
+
const reason = input.reason.trim();
|
|
304
|
+
if (!reason || reason.length > 2000) {
|
|
305
|
+
throw new SynomemError('INVALID_INPUT', 'Revocation reasons must be 1–2000 characters.');
|
|
306
|
+
}
|
|
307
|
+
const record = await this.getKudosRecord(input.kudosId);
|
|
308
|
+
if (record.revocation)
|
|
309
|
+
return record;
|
|
310
|
+
const isOriginalActor = record.event.actor.kind === this.actor.kind && record.event.actor.id === this.actor.id;
|
|
311
|
+
if (input.administrative === true && !this.administrative) {
|
|
312
|
+
throw new SynomemError('REVOCATION_FORBIDDEN', 'Only an administrator may request an administrative revocation.');
|
|
313
|
+
}
|
|
314
|
+
const administrative = this.administrative && !isOriginalActor;
|
|
315
|
+
if (!isOriginalActor && !this.administrative) {
|
|
316
|
+
throw new SynomemError('REVOCATION_FORBIDDEN', 'Only the original actor or an administrator may revoke kudos.');
|
|
317
|
+
}
|
|
318
|
+
const event = {
|
|
319
|
+
...this.eventBase(record.event.id, record.acknowledgment ? 3 : 2),
|
|
320
|
+
type: 'kudos.revoked',
|
|
321
|
+
kudosId: record.event.id,
|
|
322
|
+
reason,
|
|
323
|
+
mode: administrative && !isOriginalActor ? 'administrative' : 'actor-requested',
|
|
324
|
+
};
|
|
325
|
+
await this.repository.transaction(() => this.repository.insertEvent(event));
|
|
326
|
+
await this.projectionWriter.syncAgent(record.event.recipientAgentId);
|
|
327
|
+
return await this.getKudosRecord(input.kudosId);
|
|
328
|
+
}
|
|
329
|
+
async priorMutation(idempotencyKey, expectedType) {
|
|
330
|
+
if (!idempotencyKey)
|
|
331
|
+
return undefined;
|
|
332
|
+
const prior = await this.repository.getEventByIdempotency(this.actor.kind, this.actor.id, idempotencyKey);
|
|
333
|
+
if (prior && prior.type !== expectedType) {
|
|
334
|
+
throw new SynomemError('IDEMPOTENCY_CONFLICT', `Idempotency key was already used for ${prior.type}.`);
|
|
335
|
+
}
|
|
336
|
+
return prior;
|
|
337
|
+
}
|
|
338
|
+
canViewItem(summary) {
|
|
339
|
+
return (this.administrative ||
|
|
340
|
+
summary.visibility !== 'private' ||
|
|
341
|
+
(summary.actor.kind === this.actor.kind && summary.actor.id === this.actor.id) ||
|
|
342
|
+
(this.actor.kind === 'agent' &&
|
|
343
|
+
(summary.recipientAgentId === this.actor.id ||
|
|
344
|
+
summary.ownerAgentId === this.actor.id ||
|
|
345
|
+
summary.assigneeAgentId === this.actor.id)));
|
|
346
|
+
}
|
|
347
|
+
async requireVisibleItem(id, kind) {
|
|
348
|
+
const summary = await this.repository.getItemSummary(id);
|
|
349
|
+
if (!summary || summary.kind !== kind) {
|
|
350
|
+
const code = kind === 'memo'
|
|
351
|
+
? 'MEMO_NOT_FOUND'
|
|
352
|
+
: kind === 'note'
|
|
353
|
+
? 'NOTE_NOT_FOUND'
|
|
354
|
+
: kind === 'todo'
|
|
355
|
+
? 'TODO_NOT_FOUND'
|
|
356
|
+
: 'KUDOS_NOT_FOUND';
|
|
357
|
+
throw new SynomemError(code, `Unknown ${kind}: ${id}`);
|
|
358
|
+
}
|
|
359
|
+
if (!this.canViewItem(summary)) {
|
|
360
|
+
throw new SynomemError('POLICY_FORBIDDEN', `This ${kind} is not visible to the configured actor.`);
|
|
361
|
+
}
|
|
362
|
+
return summary;
|
|
363
|
+
}
|
|
364
|
+
async getMemoRecord(id) {
|
|
365
|
+
await this.requireVisibleItem(id, 'memo');
|
|
366
|
+
const record = memoRecordsFromEvents(await this.repository.getReadableItemEvents(id))[0];
|
|
367
|
+
if (!record)
|
|
368
|
+
throw new SynomemError('MEMO_NOT_FOUND', `Unknown memo: ${id}`);
|
|
369
|
+
return record;
|
|
370
|
+
}
|
|
371
|
+
async getMemo(id) {
|
|
372
|
+
this.checkAbort();
|
|
373
|
+
return await this.getMemoRecord(id);
|
|
374
|
+
}
|
|
375
|
+
async sendMemo(input) {
|
|
376
|
+
this.checkAbort();
|
|
377
|
+
await this.repository.assertEventCompatibility();
|
|
378
|
+
const parsed = this.validate(() => sendMemoSchema.parse(input));
|
|
379
|
+
const recipient = await this.repository.getAgent(parsed.recipientAgentId);
|
|
380
|
+
if (!recipient)
|
|
381
|
+
throw new SynomemError('AGENT_NOT_FOUND', `Unknown recipient: ${parsed.recipientAgentId}`);
|
|
382
|
+
const outcome = await this.repository.transaction(async () => {
|
|
383
|
+
const prior = await this.priorMutation(parsed.idempotencyKey, 'memo.sent');
|
|
384
|
+
if (prior?.type === 'memo.sent')
|
|
385
|
+
return { id: prior.id, created: false };
|
|
386
|
+
const id = this.nextId();
|
|
387
|
+
const event = {
|
|
388
|
+
...this.eventBase(id, 1, id),
|
|
389
|
+
type: 'memo.sent',
|
|
390
|
+
recipientAgentId: recipient.id,
|
|
391
|
+
recipientDisplayName: recipient.displayName,
|
|
392
|
+
subject: parsed.subject,
|
|
393
|
+
body: parsed.body,
|
|
394
|
+
tags: [...new Set(parsed.tags ?? [])].sort(),
|
|
395
|
+
visibility: parsed.visibility ?? this.repository.config.defaultVisibility,
|
|
396
|
+
...(parsed.idempotencyKey ? { idempotencyKey: parsed.idempotencyKey } : {}),
|
|
397
|
+
...(parsed.source ? { source: parsed.source } : {}),
|
|
398
|
+
...(parsed.metadata ? { metadata: parsed.metadata } : {}),
|
|
399
|
+
};
|
|
400
|
+
await this.repository.insertEvent(event);
|
|
401
|
+
return { id, created: true };
|
|
402
|
+
});
|
|
403
|
+
if (outcome.created)
|
|
404
|
+
await this.projectionWriter.syncAgent(recipient.id);
|
|
405
|
+
return {
|
|
406
|
+
record: await this.getMemoRecord(outcome.id),
|
|
407
|
+
created: outcome.created,
|
|
408
|
+
deduplicated: !outcome.created,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
assertRecipient(recipientAgentId, operation) {
|
|
412
|
+
if (!this.administrative &&
|
|
413
|
+
!(this.actor.kind === 'agent' && this.actor.id === recipientAgentId)) {
|
|
414
|
+
throw new SynomemError('MUTATION_FORBIDDEN', `Only the recipient agent or a human administrator may ${operation}.`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
async readMemo(input) {
|
|
418
|
+
const record = await this.getMemoRecord(input.memoId);
|
|
419
|
+
this.assertRecipient(record.event.recipientAgentId, 'mark this memo read');
|
|
420
|
+
if (record.read)
|
|
421
|
+
return record;
|
|
422
|
+
await this.repository.transaction(async () => {
|
|
423
|
+
const prior = await this.priorMutation(input.idempotencyKey, 'memo.read');
|
|
424
|
+
if (prior)
|
|
425
|
+
return;
|
|
426
|
+
const event = {
|
|
427
|
+
...this.eventBase(record.event.id, await this.repository.nextAggregateVersion(record.event.id)),
|
|
428
|
+
type: 'memo.read',
|
|
429
|
+
memoId: record.event.id,
|
|
430
|
+
recipientAgentId: record.event.recipientAgentId,
|
|
431
|
+
...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
|
|
432
|
+
};
|
|
433
|
+
await this.repository.insertEvent(event);
|
|
434
|
+
});
|
|
435
|
+
await this.projectionWriter.syncAgent(record.event.recipientAgentId);
|
|
436
|
+
return await this.getMemoRecord(input.memoId);
|
|
437
|
+
}
|
|
438
|
+
async archiveMemo(input) {
|
|
439
|
+
const record = await this.getMemoRecord(input.memoId);
|
|
440
|
+
this.assertRecipient(record.event.recipientAgentId, 'archive this memo');
|
|
441
|
+
if (record.archived)
|
|
442
|
+
return record;
|
|
443
|
+
await this.repository.transaction(async () => {
|
|
444
|
+
const prior = await this.priorMutation(input.idempotencyKey, 'memo.archived');
|
|
445
|
+
if (prior)
|
|
446
|
+
return;
|
|
447
|
+
const event = {
|
|
448
|
+
...this.eventBase(record.event.id, await this.repository.nextAggregateVersion(record.event.id)),
|
|
449
|
+
type: 'memo.archived',
|
|
450
|
+
memoId: record.event.id,
|
|
451
|
+
recipientAgentId: record.event.recipientAgentId,
|
|
452
|
+
...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
|
|
453
|
+
};
|
|
454
|
+
await this.repository.insertEvent(event);
|
|
455
|
+
});
|
|
456
|
+
await this.projectionWriter.syncAgent(record.event.recipientAgentId);
|
|
457
|
+
return await this.getMemoRecord(input.memoId);
|
|
458
|
+
}
|
|
459
|
+
async getNoteRecord(id) {
|
|
460
|
+
await this.requireVisibleItem(id, 'note');
|
|
461
|
+
const record = noteRecordsFromEvents(await this.repository.getReadableItemEvents(id))[0];
|
|
462
|
+
if (!record)
|
|
463
|
+
throw new SynomemError('NOTE_NOT_FOUND', `Unknown note: ${id}`);
|
|
464
|
+
return record;
|
|
465
|
+
}
|
|
466
|
+
async getNote(id) {
|
|
467
|
+
this.checkAbort();
|
|
468
|
+
return await this.getNoteRecord(id);
|
|
469
|
+
}
|
|
470
|
+
async createNote(input) {
|
|
471
|
+
this.checkAbort();
|
|
472
|
+
await this.repository.assertEventCompatibility();
|
|
473
|
+
const parsed = this.validate(() => createNoteSchema.parse(input));
|
|
474
|
+
const ownerId = parsed.ownerAgentId ?? (this.actor.kind === 'agent' ? this.actor.id : undefined);
|
|
475
|
+
if (!ownerId)
|
|
476
|
+
throw new SynomemError('INVALID_INPUT', 'A human or system actor must specify ownerAgentId.');
|
|
477
|
+
const owner = await this.repository.getAgent(ownerId);
|
|
478
|
+
if (!owner)
|
|
479
|
+
throw new SynomemError('AGENT_NOT_FOUND', `Unknown note owner: ${ownerId}`);
|
|
480
|
+
if (!this.administrative && (this.actor.kind !== 'agent' || this.actor.id !== owner.id)) {
|
|
481
|
+
throw new SynomemError('MUTATION_FORBIDDEN', 'Agents may create notes only for themselves.');
|
|
482
|
+
}
|
|
483
|
+
const outcome = await this.repository.transaction(async () => {
|
|
484
|
+
const prior = await this.priorMutation(parsed.idempotencyKey, 'note.created');
|
|
485
|
+
if (prior?.type === 'note.created')
|
|
486
|
+
return { id: prior.id, created: false };
|
|
487
|
+
const id = this.nextId();
|
|
488
|
+
const event = {
|
|
489
|
+
...this.eventBase(id, 1, id),
|
|
490
|
+
type: 'note.created',
|
|
491
|
+
ownerAgentId: owner.id,
|
|
492
|
+
ownerDisplayName: owner.displayName,
|
|
493
|
+
title: parsed.title,
|
|
494
|
+
body: parsed.body,
|
|
495
|
+
tags: [...new Set(parsed.tags ?? [])].sort(),
|
|
496
|
+
visibility: 'private',
|
|
497
|
+
...(parsed.idempotencyKey ? { idempotencyKey: parsed.idempotencyKey } : {}),
|
|
498
|
+
...(parsed.source ? { source: parsed.source } : {}),
|
|
499
|
+
...(parsed.metadata ? { metadata: parsed.metadata } : {}),
|
|
500
|
+
};
|
|
501
|
+
await this.repository.insertEvent(event);
|
|
502
|
+
return { id, created: true };
|
|
503
|
+
});
|
|
504
|
+
if (outcome.created)
|
|
505
|
+
await this.projectionWriter.syncAgent(owner.id);
|
|
506
|
+
return {
|
|
507
|
+
record: await this.getNoteRecord(outcome.id),
|
|
508
|
+
created: outcome.created,
|
|
509
|
+
deduplicated: !outcome.created,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
assertNoteOwner(record) {
|
|
513
|
+
if (!this.administrative &&
|
|
514
|
+
!(this.actor.kind === 'agent' && this.actor.id === record.event.ownerAgentId)) {
|
|
515
|
+
throw new SynomemError('MUTATION_FORBIDDEN', 'Only the note owner or a human administrator may change it.');
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
async reviseNote(input) {
|
|
519
|
+
const parsed = this.validate(() => reviseNoteSchema.parse(input));
|
|
520
|
+
const record = await this.getNoteRecord(parsed.noteId);
|
|
521
|
+
this.assertNoteOwner(record);
|
|
522
|
+
if (record.status === 'archived')
|
|
523
|
+
throw new SynomemError('INVALID_INPUT', 'Archived notes cannot be revised.');
|
|
524
|
+
if (parsed.expectedVersion !== record.current.version)
|
|
525
|
+
throw new SynomemError('REVISION_CONFLICT', `Expected note version ${parsed.expectedVersion}; current version is ${record.current.version}.`);
|
|
526
|
+
await this.repository.transaction(async () => {
|
|
527
|
+
const prior = await this.priorMutation(parsed.idempotencyKey, 'note.revised');
|
|
528
|
+
if (prior)
|
|
529
|
+
return;
|
|
530
|
+
if ((await this.repository.nextAggregateVersion(record.event.id)) !==
|
|
531
|
+
parsed.expectedVersion + 1)
|
|
532
|
+
throw new SynomemError('REVISION_CONFLICT', 'The note changed before this revision was stored.');
|
|
533
|
+
const event = {
|
|
534
|
+
...this.eventBase(record.event.id, parsed.expectedVersion + 1),
|
|
535
|
+
type: 'note.revised',
|
|
536
|
+
noteId: record.event.id,
|
|
537
|
+
title: parsed.title ?? record.current.title,
|
|
538
|
+
body: parsed.body ?? record.current.body,
|
|
539
|
+
tags: parsed.tags ?? record.current.tags,
|
|
540
|
+
visibility: 'private',
|
|
541
|
+
...(parsed.idempotencyKey ? { idempotencyKey: parsed.idempotencyKey } : {}),
|
|
542
|
+
...(parsed.source ? { source: parsed.source } : {}),
|
|
543
|
+
...(parsed.metadata ? { metadata: parsed.metadata } : {}),
|
|
544
|
+
};
|
|
545
|
+
await this.repository.insertEvent(event);
|
|
546
|
+
});
|
|
547
|
+
await this.projectionWriter.syncAgent(record.event.ownerAgentId);
|
|
548
|
+
return await this.getNoteRecord(parsed.noteId);
|
|
549
|
+
}
|
|
550
|
+
async archiveNote(input) {
|
|
551
|
+
const record = await this.getNoteRecord(input.noteId);
|
|
552
|
+
this.assertNoteOwner(record);
|
|
553
|
+
if (record.archived)
|
|
554
|
+
return record;
|
|
555
|
+
await this.repository.transaction(async () => {
|
|
556
|
+
const prior = await this.priorMutation(input.idempotencyKey, 'note.archived');
|
|
557
|
+
if (prior)
|
|
558
|
+
return;
|
|
559
|
+
const event = {
|
|
560
|
+
...this.eventBase(record.event.id, await this.repository.nextAggregateVersion(record.event.id)),
|
|
561
|
+
type: 'note.archived',
|
|
562
|
+
noteId: record.event.id,
|
|
563
|
+
...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
|
|
564
|
+
};
|
|
565
|
+
await this.repository.insertEvent(event);
|
|
566
|
+
});
|
|
567
|
+
await this.projectionWriter.syncAgent(record.event.ownerAgentId);
|
|
568
|
+
return await this.getNoteRecord(input.noteId);
|
|
569
|
+
}
|
|
570
|
+
async getTodoRecord(id) {
|
|
571
|
+
await this.requireVisibleItem(id, 'todo');
|
|
572
|
+
const record = todoRecordsFromEvents(await this.repository.getReadableItemEvents(id))[0];
|
|
573
|
+
if (!record)
|
|
574
|
+
throw new SynomemError('TODO_NOT_FOUND', `Unknown todo: ${id}`);
|
|
575
|
+
return record;
|
|
576
|
+
}
|
|
577
|
+
async getTodo(id) {
|
|
578
|
+
this.checkAbort();
|
|
579
|
+
return await this.getTodoRecord(id);
|
|
580
|
+
}
|
|
581
|
+
async createTodo(input) {
|
|
582
|
+
this.checkAbort();
|
|
583
|
+
await this.repository.assertEventCompatibility();
|
|
584
|
+
const parsed = this.validate(() => createTodoSchema.parse(input));
|
|
585
|
+
const assigneeId = parsed.assigneeAgentId ?? (this.actor.kind === 'agent' ? this.actor.id : undefined);
|
|
586
|
+
if (!assigneeId)
|
|
587
|
+
throw new SynomemError('INVALID_INPUT', 'A human or system actor must specify assigneeAgentId.');
|
|
588
|
+
const assignee = await this.repository.getAgent(assigneeId);
|
|
589
|
+
if (!assignee)
|
|
590
|
+
throw new SynomemError('AGENT_NOT_FOUND', `Unknown todo assignee: ${assigneeId}`);
|
|
591
|
+
if (this.actor.kind === 'agent' &&
|
|
592
|
+
this.actor.id !== assignee.id &&
|
|
593
|
+
!this.repository.config.allowCrossAgentTodos)
|
|
594
|
+
throw new SynomemError('POLICY_FORBIDDEN', 'Cross-agent todo assignment is disabled.');
|
|
595
|
+
const outcome = await this.repository.transaction(async () => {
|
|
596
|
+
const prior = await this.priorMutation(parsed.idempotencyKey, 'todo.created');
|
|
597
|
+
if (prior?.type === 'todo.created')
|
|
598
|
+
return { id: prior.id, created: false };
|
|
599
|
+
const id = this.nextId();
|
|
600
|
+
const requiresAcceptance = this.actor.kind !== 'agent' || this.actor.id !== assignee.id;
|
|
601
|
+
const event = {
|
|
602
|
+
...this.eventBase(id, 1, id),
|
|
603
|
+
type: 'todo.created',
|
|
604
|
+
assigneeAgentId: assignee.id,
|
|
605
|
+
assigneeDisplayName: assignee.displayName,
|
|
606
|
+
title: parsed.title,
|
|
607
|
+
...(parsed.description !== undefined ? { description: parsed.description } : {}),
|
|
608
|
+
priority: parsed.priority ?? 3,
|
|
609
|
+
...(parsed.due ? { due: parsed.due } : {}),
|
|
610
|
+
tags: [...new Set(parsed.tags ?? [])].sort(),
|
|
611
|
+
visibility: parsed.visibility ?? this.repository.config.defaultVisibility,
|
|
612
|
+
requiresAcceptance,
|
|
613
|
+
...(parsed.idempotencyKey ? { idempotencyKey: parsed.idempotencyKey } : {}),
|
|
614
|
+
...(parsed.source ? { source: parsed.source } : {}),
|
|
615
|
+
...(parsed.metadata ? { metadata: parsed.metadata } : {}),
|
|
616
|
+
};
|
|
617
|
+
await this.repository.insertEvent(event);
|
|
618
|
+
return { id, created: true };
|
|
619
|
+
});
|
|
620
|
+
if (outcome.created)
|
|
621
|
+
await this.projectionWriter.syncAgent(assignee.id);
|
|
622
|
+
return {
|
|
623
|
+
record: await this.getTodoRecord(outcome.id),
|
|
624
|
+
created: outcome.created,
|
|
625
|
+
deduplicated: !outcome.created,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
assertTodoParticipant(record) {
|
|
629
|
+
const isCreator = record.event.actor.kind === this.actor.kind && record.event.actor.id === this.actor.id;
|
|
630
|
+
const isAssignee = this.actor.kind === 'agent' && this.actor.id === record.event.assigneeAgentId;
|
|
631
|
+
if (!this.administrative && !isCreator && !isAssignee)
|
|
632
|
+
throw new SynomemError('MUTATION_FORBIDDEN', 'Only the todo creator, assignee, or a human administrator may change it.');
|
|
633
|
+
}
|
|
634
|
+
assertTodoAssignee(record) {
|
|
635
|
+
if (!this.administrative &&
|
|
636
|
+
!(this.actor.kind === 'agent' && this.actor.id === record.event.assigneeAgentId)) {
|
|
637
|
+
throw new SynomemError('MUTATION_FORBIDDEN', 'Only the assigned agent or a human administrator may accept or reject this todo.');
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
async updateTodo(input) {
|
|
641
|
+
const parsed = this.validate(() => updateTodoSchema.parse(input));
|
|
642
|
+
const record = await this.getTodoRecord(parsed.todoId);
|
|
643
|
+
this.assertTodoParticipant(record);
|
|
644
|
+
if (record.status !== 'open')
|
|
645
|
+
throw new SynomemError('INVALID_INPUT', 'Only open todos can be updated.');
|
|
646
|
+
if (parsed.expectedVersion !== record.current.version)
|
|
647
|
+
throw new SynomemError('REVISION_CONFLICT', `Expected todo version ${parsed.expectedVersion}; current version is ${record.current.version}.`);
|
|
648
|
+
await this.repository.transaction(async () => {
|
|
649
|
+
const prior = await this.priorMutation(parsed.idempotencyKey, 'todo.updated');
|
|
650
|
+
if (prior)
|
|
651
|
+
return;
|
|
652
|
+
if ((await this.repository.nextAggregateVersion(record.event.id)) !==
|
|
653
|
+
parsed.expectedVersion + 1)
|
|
654
|
+
throw new SynomemError('REVISION_CONFLICT', 'The todo changed before this update was stored.');
|
|
655
|
+
const due = parsed.due === null ? undefined : (parsed.due ?? record.current.due);
|
|
656
|
+
const event = {
|
|
657
|
+
...this.eventBase(record.event.id, parsed.expectedVersion + 1),
|
|
658
|
+
type: 'todo.updated',
|
|
659
|
+
todoId: record.event.id,
|
|
660
|
+
title: parsed.title ?? record.current.title,
|
|
661
|
+
...(parsed.description !== undefined
|
|
662
|
+
? { description: parsed.description }
|
|
663
|
+
: record.current.description !== undefined
|
|
664
|
+
? { description: record.current.description }
|
|
665
|
+
: {}),
|
|
666
|
+
priority: parsed.priority ?? record.current.priority,
|
|
667
|
+
...(due ? { due } : {}),
|
|
668
|
+
tags: parsed.tags ?? record.current.tags,
|
|
669
|
+
visibility: parsed.visibility ?? record.current.visibility,
|
|
670
|
+
...(parsed.idempotencyKey ? { idempotencyKey: parsed.idempotencyKey } : {}),
|
|
671
|
+
...(parsed.source ? { source: parsed.source } : {}),
|
|
672
|
+
...(parsed.metadata ? { metadata: parsed.metadata } : {}),
|
|
673
|
+
};
|
|
674
|
+
await this.repository.insertEvent(event);
|
|
675
|
+
});
|
|
676
|
+
await this.projectionWriter.syncAgent(record.event.assigneeAgentId);
|
|
677
|
+
return await this.getTodoRecord(parsed.todoId);
|
|
678
|
+
}
|
|
679
|
+
async todoTransition(input, type) {
|
|
680
|
+
const record = await this.getTodoRecord(input.todoId);
|
|
681
|
+
if (type === 'todo.accepted' || type === 'todo.rejected')
|
|
682
|
+
this.assertTodoAssignee(record);
|
|
683
|
+
else
|
|
684
|
+
this.assertTodoParticipant(record);
|
|
685
|
+
if ((type === 'todo.accepted' || type === 'todo.rejected') && record.status !== 'assigned') {
|
|
686
|
+
if (type === 'todo.accepted' && record.status === 'open')
|
|
687
|
+
return record;
|
|
688
|
+
if (type === 'todo.rejected' && record.status === 'rejected')
|
|
689
|
+
return record;
|
|
690
|
+
throw new SynomemError('INVALID_INPUT', 'Only assigned todos may be accepted or rejected.');
|
|
691
|
+
}
|
|
692
|
+
if (type === 'todo.completed' && record.status !== 'open') {
|
|
693
|
+
if (record.status === 'completed')
|
|
694
|
+
return record;
|
|
695
|
+
throw new SynomemError('INVALID_INPUT', 'Only accepted or self-created open todos may be completed.');
|
|
696
|
+
}
|
|
697
|
+
if (type === 'todo.reopened' && record.status !== 'completed' && record.status !== 'canceled') {
|
|
698
|
+
if (record.status === 'open')
|
|
699
|
+
return record;
|
|
700
|
+
throw new SynomemError('INVALID_INPUT', 'Only completed or canceled todos may be reopened.');
|
|
701
|
+
}
|
|
702
|
+
if (type === 'todo.canceled' && record.status !== 'assigned' && record.status !== 'open') {
|
|
703
|
+
if (record.status === 'canceled')
|
|
704
|
+
return record;
|
|
705
|
+
throw new SynomemError('INVALID_INPUT', 'Only assigned or open todos may be canceled.');
|
|
706
|
+
}
|
|
707
|
+
await this.repository.transaction(async () => {
|
|
708
|
+
const prior = await this.priorMutation(input.idempotencyKey, type);
|
|
709
|
+
if (prior)
|
|
710
|
+
return;
|
|
711
|
+
const base = {
|
|
712
|
+
...this.eventBase(record.event.id, await this.repository.nextAggregateVersion(record.event.id)),
|
|
713
|
+
todoId: record.event.id,
|
|
714
|
+
...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
|
|
715
|
+
};
|
|
716
|
+
const event = type === 'todo.completed'
|
|
717
|
+
? { ...base, type, ...(input.note ? { note: input.note.trim() } : {}) }
|
|
718
|
+
: type === 'todo.canceled' || type === 'todo.rejected'
|
|
719
|
+
? { ...base, type, ...(input.reason ? { reason: input.reason.trim() } : {}) }
|
|
720
|
+
: { ...base, type };
|
|
721
|
+
await this.repository.insertEvent(event);
|
|
722
|
+
});
|
|
723
|
+
await this.projectionWriter.syncAgent(record.event.assigneeAgentId);
|
|
724
|
+
return await this.getTodoRecord(input.todoId);
|
|
725
|
+
}
|
|
726
|
+
completeTodo(input) {
|
|
727
|
+
return this.todoTransition(input, 'todo.completed');
|
|
728
|
+
}
|
|
729
|
+
acceptTodo(input) {
|
|
730
|
+
return this.todoTransition(input, 'todo.accepted');
|
|
731
|
+
}
|
|
732
|
+
rejectTodo(input) {
|
|
733
|
+
return this.todoTransition(input, 'todo.rejected');
|
|
734
|
+
}
|
|
735
|
+
reopenTodo(input) {
|
|
736
|
+
return this.todoTransition(input, 'todo.reopened');
|
|
737
|
+
}
|
|
738
|
+
cancelTodo(input) {
|
|
739
|
+
return this.todoTransition(input, 'todo.canceled');
|
|
740
|
+
}
|
|
741
|
+
async listItems(input) {
|
|
742
|
+
this.checkAbort();
|
|
743
|
+
const parsed = this.validate(() => itemListInputSchema.parse(input));
|
|
744
|
+
return await this.repository.listItemSummaries(parsed, this.actor);
|
|
745
|
+
}
|
|
746
|
+
async listItemChanges(input) {
|
|
747
|
+
this.checkAbort();
|
|
748
|
+
const parsed = this.validate(() => changesInputSchema
|
|
749
|
+
.extend({
|
|
750
|
+
kinds: itemListInputSchema.shape.kinds,
|
|
751
|
+
})
|
|
752
|
+
.parse(input));
|
|
753
|
+
return await this.repository.listItemChanges(parsed.after, parsed.limit, this.actor, parsed.kinds);
|
|
754
|
+
}
|
|
755
|
+
async getItem(id) {
|
|
756
|
+
const summary = await this.repository.getItemSummary(id);
|
|
757
|
+
if (!summary)
|
|
758
|
+
throw new SynomemError('ITEM_NOT_FOUND', `Unknown item: ${id}`);
|
|
759
|
+
if (!this.canViewItem(summary)) {
|
|
760
|
+
throw new SynomemError('POLICY_FORBIDDEN', 'This item is not visible to the configured actor.');
|
|
761
|
+
}
|
|
762
|
+
if (summary.kind === 'kudos')
|
|
763
|
+
return this.getKudos(id);
|
|
764
|
+
if (summary.kind === 'memo')
|
|
765
|
+
return this.getMemo(id);
|
|
766
|
+
if (summary.kind === 'note')
|
|
767
|
+
return this.getNote(id);
|
|
768
|
+
return this.getTodo(id);
|
|
769
|
+
}
|
|
770
|
+
async stats(input = {}) {
|
|
771
|
+
this.checkAbort();
|
|
772
|
+
const parsed = this.validate(() => listInputSchema.parse(input));
|
|
773
|
+
const { cursor: _cursor, limit: _limit, offset: _offset, ...filters } = parsed;
|
|
774
|
+
void _cursor;
|
|
775
|
+
void _limit;
|
|
776
|
+
void _offset;
|
|
777
|
+
const recipient = filters.recipientAgentId
|
|
778
|
+
? await this.repository.getAgent(filters.recipientAgentId)
|
|
779
|
+
: undefined;
|
|
780
|
+
if (filters.recipientAgentId && !recipient) {
|
|
781
|
+
throw new SynomemError('AGENT_NOT_FOUND', `Unknown agent: ${filters.recipientAgentId}`);
|
|
782
|
+
}
|
|
783
|
+
const records = [];
|
|
784
|
+
let cursor;
|
|
785
|
+
const viewer = this.actor;
|
|
786
|
+
let hasMore = true;
|
|
787
|
+
while (hasMore) {
|
|
788
|
+
const page = await this.repository.listKudosSummaries({
|
|
789
|
+
...filters,
|
|
790
|
+
...(recipient ? { recipientAgentId: recipient.id } : {}),
|
|
791
|
+
limit: 50,
|
|
792
|
+
offset: 0,
|
|
793
|
+
...(cursor ? { cursor } : {}),
|
|
794
|
+
}, viewer);
|
|
795
|
+
records.push(...page.items);
|
|
796
|
+
cursor = page.nextCursor;
|
|
797
|
+
hasMore = page.hasMore && Boolean(cursor) && page.items.length > 0;
|
|
798
|
+
}
|
|
799
|
+
const includedRecords = this.repository.config.includePrivateInStats
|
|
800
|
+
? records
|
|
801
|
+
: records.filter((record) => record.visibility !== 'private');
|
|
802
|
+
const stats = {
|
|
803
|
+
total: includedRecords.length,
|
|
804
|
+
active: includedRecords.filter((record) => record.revocationStatus === 'active').length,
|
|
805
|
+
acknowledged: includedRecords.filter((record) => record.status === 'acknowledged' && record.revocationStatus === 'active').length,
|
|
806
|
+
revoked: includedRecords.filter((record) => record.revocationStatus === 'revoked').length,
|
|
807
|
+
byAgent: {},
|
|
808
|
+
byActor: {},
|
|
809
|
+
byTag: {},
|
|
810
|
+
};
|
|
811
|
+
for (const record of includedRecords) {
|
|
812
|
+
stats.byAgent[record.recipientAgentId] = (stats.byAgent[record.recipientAgentId] ?? 0) + 1;
|
|
813
|
+
const actor = `${record.actor.kind}:${record.actor.id}`;
|
|
814
|
+
stats.byActor[actor] = (stats.byActor[actor] ?? 0) + 1;
|
|
815
|
+
for (const tag of record.tags)
|
|
816
|
+
stats.byTag[tag] = (stats.byTag[tag] ?? 0) + 1;
|
|
817
|
+
}
|
|
818
|
+
return stats;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
export class SynomemClient extends SynomemCore {
|
|
822
|
+
home;
|
|
823
|
+
storage;
|
|
824
|
+
projections;
|
|
825
|
+
constructor(options = {}) {
|
|
826
|
+
const home = resolveHome(options.home);
|
|
827
|
+
const storage = new SynomemStorage({
|
|
828
|
+
home,
|
|
829
|
+
readOnly: options.readOnly ?? false,
|
|
830
|
+
...(options.config ? { config: options.config } : {}),
|
|
831
|
+
});
|
|
832
|
+
const projections = new ProjectionManager(storage);
|
|
833
|
+
super({
|
|
834
|
+
repository: storage,
|
|
835
|
+
projectionWriter: projections,
|
|
836
|
+
...(options.actor ? { actor: options.actor } : {}),
|
|
837
|
+
...(options.clock ? { clock: options.clock } : {}),
|
|
838
|
+
...(options.idGenerator ? { idGenerator: options.idGenerator } : {}),
|
|
839
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
840
|
+
});
|
|
841
|
+
this.home = home;
|
|
842
|
+
this.storage = storage;
|
|
843
|
+
this.projections = projections;
|
|
844
|
+
}
|
|
845
|
+
async doctor() {
|
|
846
|
+
this.checkAbort();
|
|
847
|
+
const diagnostics = [];
|
|
848
|
+
try {
|
|
849
|
+
try {
|
|
850
|
+
accessSync(this.home, fsConstants.R_OK | (this.storage.readOnly ? 0 : fsConstants.W_OK));
|
|
851
|
+
diagnostics.push({
|
|
852
|
+
level: 'ok',
|
|
853
|
+
code: 'HOME_PERMISSIONS_OK',
|
|
854
|
+
message: `Storage home is ${this.storage.readOnly ? 'readable' : 'readable and writable'}.`,
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
catch {
|
|
858
|
+
diagnostics.push({
|
|
859
|
+
level: 'error',
|
|
860
|
+
code: 'HOME_PERMISSIONS_FAILED',
|
|
861
|
+
message: 'Storage home permissions do not permit the configured access mode.',
|
|
862
|
+
path: this.home,
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
const integrity = this.storage.integrityCheck();
|
|
866
|
+
if (integrity.length === 1 && integrity[0] === 'ok') {
|
|
867
|
+
diagnostics.push({
|
|
868
|
+
level: 'ok',
|
|
869
|
+
code: 'SQLITE_INTEGRITY_OK',
|
|
870
|
+
message: 'SQLite integrity check passed.',
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
else {
|
|
874
|
+
diagnostics.push({
|
|
875
|
+
level: 'error',
|
|
876
|
+
code: 'SQLITE_INTEGRITY_FAILED',
|
|
877
|
+
message: integrity.join('; '),
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
const journal = this.storage.journalMode();
|
|
881
|
+
diagnostics.push({
|
|
882
|
+
level: journal === 'wal' || this.storage.readOnly ? 'ok' : 'warning',
|
|
883
|
+
code: 'SQLITE_JOURNAL_MODE',
|
|
884
|
+
message: `SQLite journal mode is ${journal}.`,
|
|
885
|
+
});
|
|
886
|
+
const eventScan = this.storage.scanEvents();
|
|
887
|
+
if (eventScan.invalid.length) {
|
|
888
|
+
for (const invalid of eventScan.invalid) {
|
|
889
|
+
diagnostics.push({
|
|
890
|
+
level: 'error',
|
|
891
|
+
code: invalid.error.code,
|
|
892
|
+
message: invalid.error.message,
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
else {
|
|
897
|
+
diagnostics.push({
|
|
898
|
+
level: 'ok',
|
|
899
|
+
code: 'EVENTS_VALID',
|
|
900
|
+
message: `${eventScan.events.length} canonical event${eventScan.events.length === 1 ? '' : 's'} validated.`,
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
const indexHealth = this.storage.currentIndexHealth();
|
|
904
|
+
const indexValid = indexHealth.given === indexHealth.indexed && indexHealth.stateMismatches === 0;
|
|
905
|
+
diagnostics.push({
|
|
906
|
+
level: indexValid ? 'ok' : 'error',
|
|
907
|
+
code: indexValid ? 'CURRENT_INDEX_VALID' : 'CURRENT_INDEX_INCONSISTENT',
|
|
908
|
+
message: `${indexHealth.indexed} of ${indexHealth.given} kudos are present in the current-state index; ${indexHealth.stateMismatches} state mismatch${indexHealth.stateMismatches === 1 ? '' : 'es'} detected.`,
|
|
909
|
+
});
|
|
910
|
+
const itemHealth = this.storage.itemIndexHealth();
|
|
911
|
+
const itemsValid = itemHealth.created === itemHealth.indexed;
|
|
912
|
+
diagnostics.push({
|
|
913
|
+
level: itemsValid ? 'ok' : 'error',
|
|
914
|
+
code: itemsValid ? 'ITEM_INDEX_VALID' : 'ITEM_INDEX_INCONSISTENT',
|
|
915
|
+
message: `${itemHealth.indexed} of ${itemHealth.created} item aggregates are present in the shared current-state index.`,
|
|
916
|
+
});
|
|
917
|
+
const migrationState = this.storage.migrationState();
|
|
918
|
+
const migrationsValid = migrationState.schemaVersion === 3 &&
|
|
919
|
+
JSON.stringify(migrationState.appliedVersions) === JSON.stringify([1, 2, 3]);
|
|
920
|
+
diagnostics.push({
|
|
921
|
+
level: migrationsValid ? 'ok' : 'error',
|
|
922
|
+
code: migrationsValid ? 'MIGRATIONS_VALID' : 'MIGRATIONS_INCONSISTENT',
|
|
923
|
+
message: `Database schema version is ${migrationState.schemaVersion}; recorded migrations: ${migrationState.appliedVersions.join(', ') || 'none'}.`,
|
|
924
|
+
});
|
|
925
|
+
const aliasConflicts = this.storage.aliasIdentityConflicts();
|
|
926
|
+
diagnostics.push({
|
|
927
|
+
level: aliasConflicts.length ? 'error' : 'ok',
|
|
928
|
+
code: aliasConflicts.length ? 'ALIAS_CONFLICTS_FOUND' : 'ALIASES_VALID',
|
|
929
|
+
message: aliasConflicts.length
|
|
930
|
+
? `Aliases collide with direct agent identities: ${aliasConflicts.map((item) => `${item.alias}→${item.agentId}`).join(', ')}.`
|
|
931
|
+
: 'No aliases collide with direct agent identities.',
|
|
932
|
+
});
|
|
933
|
+
const expected = this.projections.expectedPaths();
|
|
934
|
+
const manifest = this.storage.projectionManifest().sort();
|
|
935
|
+
const stale = JSON.stringify(expected) === JSON.stringify(manifest) ? [] : expected;
|
|
936
|
+
diagnostics.push({
|
|
937
|
+
level: stale.length ? 'warning' : 'ok',
|
|
938
|
+
code: stale.length ? 'PROJECTIONS_STALE' : 'PROJECTIONS_CURRENT',
|
|
939
|
+
message: stale.length
|
|
940
|
+
? 'Generated projections need rebuilding.'
|
|
941
|
+
: 'Projection manifest is current.',
|
|
942
|
+
});
|
|
943
|
+
for (const profile of this.storage.listAgents()) {
|
|
944
|
+
const directory = join(this.home, profile.id);
|
|
945
|
+
try {
|
|
946
|
+
assertNoSymlinkEscape(this.home, directory);
|
|
947
|
+
if (existsSync(directory) && lstatSync(directory).isSymbolicLink()) {
|
|
948
|
+
throw new SynomemError('UNSAFE_PATH', 'Agent directory is a symbolic link.');
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
catch (error) {
|
|
952
|
+
diagnostics.push({
|
|
953
|
+
level: 'error',
|
|
954
|
+
code: 'UNSAFE_SYMLINK',
|
|
955
|
+
message: error instanceof Error ? error.message : String(error),
|
|
956
|
+
path: relative(this.home, directory),
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
catch (error) {
|
|
962
|
+
diagnostics.push({
|
|
963
|
+
level: 'error',
|
|
964
|
+
code: error instanceof SynomemError ? error.code : 'DOCTOR_FAILED',
|
|
965
|
+
message: error instanceof Error ? error.message : String(error),
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
return { healthy: !diagnostics.some((item) => item.level === 'error'), diagnostics };
|
|
969
|
+
}
|
|
970
|
+
async export(format) {
|
|
971
|
+
this.checkAbort();
|
|
972
|
+
const rows = this.storage.rawEventRows();
|
|
973
|
+
if (format === 'jsonl')
|
|
974
|
+
return `${rows.map((row) => row.payload).join('\n')}\n`;
|
|
975
|
+
const exported = rows.map((row) => {
|
|
976
|
+
try {
|
|
977
|
+
return JSON.parse(row.payload);
|
|
978
|
+
}
|
|
979
|
+
catch {
|
|
980
|
+
return { _synomemUnreadableEvent: { id: row.id, rawPayload: row.payload } };
|
|
981
|
+
}
|
|
982
|
+
});
|
|
983
|
+
if (format === 'json')
|
|
984
|
+
return `${JSON.stringify(exported, null, 2)}\n`;
|
|
985
|
+
const scan = this.storage.scanEvents();
|
|
986
|
+
const events = scan.events;
|
|
987
|
+
const records = recordsFromEvents(events);
|
|
988
|
+
const memos = memoRecordsFromEvents(events);
|
|
989
|
+
const notes = noteRecordsFromEvents(events);
|
|
990
|
+
const todos = todoRecordsFromEvents(events);
|
|
991
|
+
const warning = scan.invalid.length
|
|
992
|
+
? `> Warning: ${scan.invalid.length} unsupported or malformed event(s) omitted from this Markdown view: ${scan.invalid.map((item) => item.id).join(', ')}\n\n`
|
|
993
|
+
: '';
|
|
994
|
+
const sections = [
|
|
995
|
+
...records.map((record) => `## Kudos: ${escapeMarkdown(record.event.title)}\n\n${escapeMarkdown(record.event.reason)}\n\nStatus: ${record.revocationStatus === 'revoked' ? 'Revoked' : record.status}\n\nID: \`${record.event.id}\``),
|
|
996
|
+
...memos.map((record) => `## Memo: ${escapeMarkdown(record.event.subject)}\n\n${escapeMarkdown(record.event.body)}\n\nStatus: ${record.status}\n\nID: \`${record.event.id}\``),
|
|
997
|
+
...notes.map((record) => `## Note: ${escapeMarkdown(record.current.title)}\n\n${escapeMarkdown(record.current.body)}\n\nStatus: ${record.status}; version ${record.current.version}\n\nID: \`${record.event.id}\``),
|
|
998
|
+
...todos.map((record) => `## Todo: ${escapeMarkdown(record.current.title)}\n\n${record.current.description ? `${escapeMarkdown(record.current.description)}\n\n` : ''}Status: ${record.status}; priority ${record.current.priority}\n\nID: \`${record.event.id}\``),
|
|
999
|
+
];
|
|
1000
|
+
return `${warning}${sections.join('\n\n')}\n`;
|
|
1001
|
+
}
|
|
1002
|
+
async backup(destination) {
|
|
1003
|
+
this.checkAbort();
|
|
1004
|
+
return this.storage.backup(resolve(destination));
|
|
1005
|
+
}
|
|
1006
|
+
async rebuild() {
|
|
1007
|
+
this.checkAbort();
|
|
1008
|
+
return this.projections.rebuild();
|
|
1009
|
+
}
|
|
1010
|
+
async capabilities() {
|
|
1011
|
+
this.checkAbort();
|
|
1012
|
+
return {
|
|
1013
|
+
backend: 'local',
|
|
1014
|
+
binding: { workspaceId: this.storage.config.workspaceId, actor: this.actor },
|
|
1015
|
+
administration: {
|
|
1016
|
+
agentCreationViaMcp: this.storage.config.allowAgentCreationViaMcp,
|
|
1017
|
+
rebuildViaMcp: this.storage.config.allowRebuildViaMcp,
|
|
1018
|
+
},
|
|
1019
|
+
projections: { ...this.storage.config.projection },
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
async info() {
|
|
1023
|
+
this.checkAbort();
|
|
1024
|
+
return { backend: 'local', home: this.home, databasePath: this.storage.databasePath };
|
|
1025
|
+
}
|
|
1026
|
+
async getCanonicalEvent(id) {
|
|
1027
|
+
this.checkAbort();
|
|
1028
|
+
return this.storage.getEvent(id);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
//# sourceMappingURL=client.js.map
|