synomem 0.1.1 → 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.
Files changed (73) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/CHANGELOG.md +37 -0
  3. package/README.md +26 -13
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +379 -51
  6. package/dist/cli.js.map +1 -1
  7. package/dist/client.d.ts +131 -17
  8. package/dist/client.d.ts.map +1 -1
  9. package/dist/client.js +489 -76
  10. package/dist/client.js.map +1 -1
  11. package/dist/config.d.ts +2 -2
  12. package/dist/config.js +8 -8
  13. package/dist/import.d.ts +356 -13
  14. package/dist/import.d.ts.map +1 -1
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +2 -2
  18. package/dist/index.js.map +1 -1
  19. package/dist/mcp/index.d.ts.map +1 -1
  20. package/dist/mcp/index.js +213 -31
  21. package/dist/mcp/index.js.map +1 -1
  22. package/dist/mcp-server.js +54 -8
  23. package/dist/mcp-server.js.map +1 -1
  24. package/dist/ports/repository.d.ts +21 -1
  25. package/dist/ports/repository.d.ts.map +1 -1
  26. package/dist/projections.d.ts +26 -1
  27. package/dist/projections.d.ts.map +1 -1
  28. package/dist/projections.js +208 -44
  29. package/dist/projections.js.map +1 -1
  30. package/dist/remote.d.ts +82 -9
  31. package/dist/remote.d.ts.map +1 -1
  32. package/dist/remote.js +53 -2
  33. package/dist/remote.js.map +1 -1
  34. package/dist/schemas.d.ts +476 -21
  35. package/dist/schemas.d.ts.map +1 -1
  36. package/dist/schemas.js +204 -26
  37. package/dist/schemas.js.map +1 -1
  38. package/dist/service.d.ts +82 -10
  39. package/dist/service.d.ts.map +1 -1
  40. package/dist/skill-install.d.ts +8 -2
  41. package/dist/skill-install.d.ts.map +1 -1
  42. package/dist/skill-install.js +6 -11
  43. package/dist/skill-install.js.map +1 -1
  44. package/dist/storage.d.ts +51 -1
  45. package/dist/storage.d.ts.map +1 -1
  46. package/dist/storage.js +366 -36
  47. package/dist/storage.js.map +1 -1
  48. package/dist/types.d.ts +310 -38
  49. package/dist/types.d.ts.map +1 -1
  50. package/docs/cli.md +53 -20
  51. package/docs/examples.md +4 -4
  52. package/docs/mcp.md +12 -4
  53. package/docs/skill.md +10 -7
  54. package/docs/storage-format.md +4 -4
  55. package/openapi/synomem-v1.yaml +24 -24
  56. package/package.json +1 -1
  57. package/skills/synomem/SKILL.md +24 -8
  58. package/skills/synomem/agents/openai.yaml +1 -1
  59. package/skills/synomem/references/examples.md +1 -1
  60. package/src/cli.ts +610 -95
  61. package/src/client.ts +587 -79
  62. package/src/config.ts +8 -8
  63. package/src/index.ts +11 -1
  64. package/src/mcp/index.ts +265 -30
  65. package/src/mcp-server.ts +58 -8
  66. package/src/ports/repository.ts +21 -0
  67. package/src/projections.ts +209 -44
  68. package/src/remote.ts +166 -8
  69. package/src/schemas.ts +211 -26
  70. package/src/service.ts +85 -7
  71. package/src/skill-install.ts +14 -17
  72. package/src/storage.ts +472 -34
  73. package/src/types.ts +332 -39
@@ -16,9 +16,25 @@ import type {
16
16
  KudosRecord,
17
17
  MemoRecord,
18
18
  NoteRecord,
19
+ PostRecord,
20
+ TaskDue,
21
+ TaskRecord,
19
22
  TodoRecord,
20
23
  } from './types.js';
21
24
 
25
+ /**
26
+ * The instant a deadline has passed.
27
+ *
28
+ * A date-only deadline does not invent a time of day; it lapses at the end of
29
+ * that UTC day, which is the latest reading that cannot call work late while
30
+ * the stated day is still running. Every projection must use this one rule, or
31
+ * two stores would disagree about what is overdue.
32
+ */
33
+ export function dueInstant(due: TaskDue | undefined): string | undefined {
34
+ if (!due) return undefined;
35
+ return due.kind === 'date' ? `${due.date}T23:59:59.999Z` : due.datetime;
36
+ }
37
+
22
38
  const generatedMarker = '<!-- Generated by Synomem. Rebuild with `synomem rebuild`. -->';
23
39
 
24
40
  function manifestPath(home: string, path: string): string {
@@ -111,7 +127,7 @@ function memoInboxMarkdown(record: MemoRecord): string {
111
127
  ].join('\n')}\n`;
112
128
  }
113
129
 
114
- function todoInboxMarkdown(record: TodoRecord): string {
130
+ function taskInboxMarkdown(record: TaskRecord): string {
115
131
  const due =
116
132
  record.current.due?.kind === 'date'
117
133
  ? record.current.due.date
@@ -126,7 +142,7 @@ function todoInboxMarkdown(record: TodoRecord): string {
126
142
  `**Status:** ${record.status} `,
127
143
  `**Priority:** ${record.current.priority} `,
128
144
  `**Due:** ${escapeMarkdown(due)} `,
129
- `**Todo ID:** \`${record.event.id}\``,
145
+ `**Task ID:** \`${record.event.id}\``,
130
146
  '',
131
147
  ...(record.current.description ? [escapeMarkdown(record.current.description), ''] : []),
132
148
  ].join('\n')}\n`;
@@ -157,16 +173,16 @@ function memoryMarkdown(profile: AgentProfile, records: NoteRecord[], rebuiltAt:
157
173
  return `${lines.join('\n')}\n`;
158
174
  }
159
175
 
160
- function todosMarkdown(profile: AgentProfile, records: TodoRecord[], rebuiltAt: string): string {
176
+ function tasksMarkdown(profile: AgentProfile, records: TaskRecord[], rebuiltAt: string): string {
161
177
  const lines = [
162
178
  generatedMarker,
163
179
  '',
164
- `# Todos — ${escapeMarkdown(profile.displayName)}`,
180
+ `# Tasks — ${escapeMarkdown(profile.displayName)}`,
165
181
  '',
166
182
  `_Last rebuilt: ${rebuiltAt}_`,
167
183
  '',
168
184
  ];
169
- if (!records.length) lines.push('_No todos yet._', '');
185
+ if (!records.length) lines.push('_No tasks yet._', '');
170
186
  for (const record of records) {
171
187
  const due =
172
188
  record.current.due?.kind === 'date'
@@ -180,7 +196,7 @@ function todosMarkdown(profile: AgentProfile, records: TodoRecord[], rebuiltAt:
180
196
  `**Status:** ${record.status} `,
181
197
  `**Priority:** ${record.current.priority} `,
182
198
  `**Due:** ${escapeMarkdown(due)} `,
183
- `**Todo ID:** \`${record.event.id}\``,
199
+ `**Task ID:** \`${record.event.id}\``,
184
200
  '',
185
201
  );
186
202
  }
@@ -239,6 +255,75 @@ export function memoRecordsFromEvents(events: SynomemEvent[]): MemoRecord[] {
239
255
  return [...records.values()].sort((a, b) => b.event.createdAt.localeCompare(a.event.createdAt));
240
256
  }
241
257
 
258
+ /**
259
+ * Rebuilds posts from their events, acknowledgements included.
260
+ *
261
+ * Edits are kept as a list rather than collapsed into the current text. A reader
262
+ * has to be able to see that a post changed after somebody acknowledged it —
263
+ * silently rewriting what was acknowledged is how a record becomes a lie.
264
+ */
265
+ export function postRecordsFromEvents(events: SynomemEvent[]): PostRecord[] {
266
+ const records = new Map<string, PostRecord>();
267
+ for (const event of events) {
268
+ if (event.type === 'post.created') {
269
+ records.set(event.id, {
270
+ event,
271
+ edits: [],
272
+ acknowledgments: [],
273
+ status: 'active',
274
+ title: event.title,
275
+ body: event.body,
276
+ ...(event.tags ? { tags: event.tags } : {}),
277
+ // The text version, counting only changes to the text.
278
+ //
279
+ // Deliberately not the aggregate version, which also counts every
280
+ // acknowledgement. If they were the same number, somebody
281
+ // acknowledging a post would invalidate an edit the author was in the
282
+ // middle of making — a conflict with nothing to reconcile.
283
+ version: 1,
284
+ });
285
+ } else if (event.type === 'post.edited') {
286
+ const record = records.get(event.postId);
287
+ if (record) {
288
+ record.edits.push(event);
289
+ record.title = event.title;
290
+ record.body = event.body;
291
+ if (event.tags) record.tags = event.tags;
292
+ record.version += 1;
293
+ }
294
+ } else if (event.type === 'post.archived') {
295
+ const record = records.get(event.postId);
296
+ if (record) {
297
+ record.archived = event;
298
+ record.status = 'archived';
299
+ }
300
+ } else if (event.type === 'post.acknowledged') {
301
+ const record = records.get(event.postId);
302
+ if (record) {
303
+ // One statement per actor: a repeat replaces rather than accumulates.
304
+ const existing = record.acknowledgments.findIndex(
305
+ (entry) => entry.actor.id === event.actor.id && entry.actor.kind === event.actor.kind,
306
+ );
307
+ const entry = {
308
+ actor: event.actor,
309
+ acknowledgedAt: event.createdAt,
310
+ ...(event.note ? { note: event.note } : {}),
311
+ };
312
+ if (existing >= 0) record.acknowledgments[existing] = entry;
313
+ else record.acknowledgments.push(entry);
314
+ }
315
+ } else if (event.type === 'post.acknowledgment.withdrawn') {
316
+ const record = records.get(event.postId);
317
+ if (record) {
318
+ record.acknowledgments = record.acknowledgments.filter(
319
+ (entry) => !(entry.actor.id === event.actor.id && entry.actor.kind === event.actor.kind),
320
+ );
321
+ }
322
+ }
323
+ }
324
+ return [...records.values()];
325
+ }
326
+
242
327
  export function noteRecordsFromEvents(events: SynomemEvent[]): NoteRecord[] {
243
328
  const records = new Map<string, NoteRecord>();
244
329
  for (const event of events) {
@@ -278,6 +363,13 @@ export function noteRecordsFromEvents(events: SynomemEvent[]): NoteRecord[] {
278
363
  return [...records.values()].sort((a, b) => b.event.createdAt.localeCompare(a.event.createdAt));
279
364
  }
280
365
 
366
+ /**
367
+ * Builds private Todo records.
368
+ *
369
+ * Deliberately simpler than the Task reducer: there is no acceptance state and
370
+ * no response history, because there is nobody to negotiate with. A Todo goes
371
+ * open → completed / canceled / archived, and back to open on reopen.
372
+ */
281
373
  export function todoRecordsFromEvents(events: SynomemEvent[]): TodoRecord[] {
282
374
  const records = new Map<string, TodoRecord>();
283
375
  for (const event of events) {
@@ -286,14 +378,13 @@ export function todoRecordsFromEvents(events: SynomemEvent[]): TodoRecord[] {
286
378
  event,
287
379
  current: {
288
380
  title: event.title,
289
- ...(event.description !== undefined ? { description: event.description } : {}),
381
+ ...(event.details !== undefined ? { details: event.details } : {}),
290
382
  priority: event.priority,
291
383
  ...(event.due ? { due: event.due } : {}),
292
384
  tags: event.tags ?? [],
293
- visibility: event.visibility,
294
385
  version: event.aggregateVersion,
295
386
  },
296
- status: event.requiresAcceptance ? 'assigned' : 'open',
387
+ status: 'open',
297
388
  });
298
389
  } else if (event.type === 'todo.updated') {
299
390
  const record = records.get(event.todoId);
@@ -301,24 +392,25 @@ export function todoRecordsFromEvents(events: SynomemEvent[]): TodoRecord[] {
301
392
  record.update = event;
302
393
  record.current = {
303
394
  title: event.title,
304
- ...(event.description !== undefined ? { description: event.description } : {}),
395
+ ...(event.details !== undefined ? { details: event.details } : {}),
305
396
  priority: event.priority,
306
397
  ...(event.due ? { due: event.due } : {}),
307
398
  tags: event.tags ?? [],
308
- visibility: event.visibility,
309
399
  version: event.aggregateVersion,
310
400
  };
311
401
  }
312
- } else if (event.type === 'todo.accepted') {
402
+ } else if (event.type === 'todo.reopened') {
313
403
  const record = records.get(event.todoId);
314
404
  if (record) {
405
+ record.reopened = event;
315
406
  record.current.version = event.aggregateVersion;
316
407
  record.status = 'open';
408
+ delete record.terminal;
317
409
  }
318
410
  } else if (
319
411
  event.type === 'todo.completed' ||
320
- event.type === 'todo.rejected' ||
321
- event.type === 'todo.canceled'
412
+ event.type === 'todo.canceled' ||
413
+ event.type === 'todo.archived'
322
414
  ) {
323
415
  const record = records.get(event.todoId);
324
416
  if (record) {
@@ -327,12 +419,85 @@ export function todoRecordsFromEvents(events: SynomemEvent[]): TodoRecord[] {
327
419
  record.status =
328
420
  event.type === 'todo.completed'
329
421
  ? 'completed'
330
- : event.type === 'todo.rejected'
422
+ : event.type === 'todo.canceled'
423
+ ? 'canceled'
424
+ : 'archived';
425
+ }
426
+ }
427
+ }
428
+ return [...records.values()];
429
+ }
430
+
431
+ export function taskRecordsFromEvents(events: SynomemEvent[]): TaskRecord[] {
432
+ const records = new Map<string, TaskRecord>();
433
+ for (const event of events) {
434
+ if (event.type === 'task.created') {
435
+ records.set(event.id, {
436
+ event,
437
+ current: {
438
+ title: event.title,
439
+ ...(event.description !== undefined ? { description: event.description } : {}),
440
+ priority: event.priority,
441
+ ...(event.due ? { due: event.due } : {}),
442
+ tags: event.tags ?? [],
443
+ visibility: event.visibility,
444
+ version: event.aggregateVersion,
445
+ },
446
+ status: event.requiresAcceptance ? 'assigned' : 'open',
447
+ responses: [],
448
+ });
449
+ } else if (event.type === 'task.updated') {
450
+ const record = records.get(event.taskId);
451
+ if (record) {
452
+ record.update = event;
453
+ record.current = {
454
+ title: event.title,
455
+ ...(event.description !== undefined ? { description: event.description } : {}),
456
+ priority: event.priority,
457
+ ...(event.due ? { due: event.due } : {}),
458
+ tags: event.tags ?? [],
459
+ visibility: event.visibility,
460
+ version: event.aggregateVersion,
461
+ };
462
+ }
463
+ } else if (event.type === 'task.accepted') {
464
+ const record = records.get(event.taskId);
465
+ if (record) {
466
+ record.current.version = event.aggregateVersion;
467
+ record.status = 'open';
468
+ record.responses.push({
469
+ kind: 'accepted',
470
+ ...(event.response !== undefined ? { response: event.response } : {}),
471
+ actor: event.actor,
472
+ at: event.createdAt,
473
+ });
474
+ }
475
+ } else if (
476
+ event.type === 'task.completed' ||
477
+ event.type === 'task.rejected' ||
478
+ event.type === 'task.canceled'
479
+ ) {
480
+ const record = records.get(event.taskId);
481
+ if (record) {
482
+ record.terminal = event;
483
+ record.current.version = event.aggregateVersion;
484
+ if (event.type === 'task.rejected') {
485
+ record.responses.push({
486
+ kind: 'rejected',
487
+ response: event.response,
488
+ actor: event.actor,
489
+ at: event.createdAt,
490
+ });
491
+ }
492
+ record.status =
493
+ event.type === 'task.completed'
494
+ ? 'completed'
495
+ : event.type === 'task.rejected'
331
496
  ? 'rejected'
332
497
  : 'canceled';
333
498
  }
334
- } else if (event.type === 'todo.reopened') {
335
- const record = records.get(event.todoId);
499
+ } else if (event.type === 'task.reopened') {
500
+ const record = records.get(event.taskId);
336
501
  if (record) {
337
502
  record.reopened = event;
338
503
  record.current.version = event.aggregateVersion;
@@ -358,9 +523,9 @@ export function renderMarkdownExport(events: SynomemEvent[]): string {
358
523
  (record) =>
359
524
  `## Note: ${escapeMarkdown(record.current.title)}\n\n${escapeMarkdown(record.current.body)}\n\nStatus: ${record.status}; version ${record.current.version}\n\nID: \`${record.event.id}\``,
360
525
  ),
361
- ...todoRecordsFromEvents(events).map(
526
+ ...taskRecordsFromEvents(events).map(
362
527
  (record) =>
363
- `## 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}\``,
528
+ `## Task: ${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}\``,
364
529
  ),
365
530
  ];
366
531
  return `${sections.join('\n\n')}\n`;
@@ -380,7 +545,7 @@ export class ProjectionManager implements ProjectionWriter {
380
545
  const records = recordsFromEvents(events);
381
546
  const memos = memoRecordsFromEvents(events);
382
547
  const notes = noteRecordsFromEvents(events);
383
- const todos = todoRecordsFromEvents(events);
548
+ const tasks = taskRecordsFromEvents(events);
384
549
  const rebuiltAt = events.at(-1)?.createdAt ?? new Date(0).toISOString();
385
550
  const generated: string[] = [];
386
551
 
@@ -394,12 +559,12 @@ export class ProjectionManager implements ProjectionWriter {
394
559
  const inboxDirectory = join(agentDirectory, 'inbox');
395
560
  const kudosInboxDirectory = join(inboxDirectory, 'kudos');
396
561
  const memoInboxDirectory = join(inboxDirectory, 'memos');
397
- const todoInboxDirectory = join(inboxDirectory, 'todos');
562
+ const taskInboxDirectory = join(inboxDirectory, 'tasks');
398
563
  assertNoSymlinkEscape(this.storage.home, inboxDirectory);
399
564
  ensureDirectory(inboxDirectory);
400
565
  ensureDirectory(kudosInboxDirectory);
401
566
  ensureDirectory(memoInboxDirectory);
402
- ensureDirectory(todoInboxDirectory);
567
+ ensureDirectory(taskInboxDirectory);
403
568
 
404
569
  atomicWriteDerivedFile(
405
570
  profilePath,
@@ -424,11 +589,11 @@ export class ProjectionManager implements ProjectionWriter {
424
589
  atomicWriteDerivedFile(memoryPath, memoryMarkdown(profile, agentNotes, rebuiltAt));
425
590
  generated.push(manifestPath(this.storage.home, memoryPath));
426
591
  }
427
- const agentTodos = todos.filter((record) => record.event.assigneeAgentId === profile.id);
428
- if (this.storage.config.projection.writeTodosMarkdown) {
429
- const todosPath = join(agentDirectory, 'TODOS.md');
430
- atomicWriteDerivedFile(todosPath, todosMarkdown(profile, agentTodos, rebuiltAt));
431
- generated.push(manifestPath(this.storage.home, todosPath));
592
+ const agentTasks = tasks.filter((record) => record.event.assigneeAgentId === profile.id);
593
+ if (this.storage.config.projection.writeTasksMarkdown) {
594
+ const tasksPath = join(agentDirectory, 'TASKS.md');
595
+ atomicWriteDerivedFile(tasksPath, tasksMarkdown(profile, agentTasks, rebuiltAt));
596
+ generated.push(manifestPath(this.storage.home, tasksPath));
432
597
  }
433
598
  if (this.storage.config.projection.writeInboxEntries) {
434
599
  for (const record of agentRecords.filter(
@@ -445,11 +610,11 @@ export class ProjectionManager implements ProjectionWriter {
445
610
  atomicWriteDerivedFile(inboxPath, memoInboxMarkdown(record));
446
611
  generated.push(manifestPath(this.storage.home, inboxPath));
447
612
  }
448
- for (const record of agentTodos.filter(
613
+ for (const record of agentTasks.filter(
449
614
  (item) => item.status === 'open' || item.status === 'assigned',
450
615
  )) {
451
- const inboxPath = join(todoInboxDirectory, `${record.event.id}.md`);
452
- atomicWriteDerivedFile(inboxPath, todoInboxMarkdown(record));
616
+ const inboxPath = join(taskInboxDirectory, `${record.event.id}.md`);
617
+ atomicWriteDerivedFile(inboxPath, taskInboxMarkdown(record));
453
618
  generated.push(manifestPath(this.storage.home, inboxPath));
454
619
  }
455
620
  }
@@ -487,7 +652,7 @@ export class ProjectionManager implements ProjectionWriter {
487
652
  const notes = noteRecordsFromEvents(events).filter(
488
653
  (record) => record.event.ownerAgentId === profile.id,
489
654
  );
490
- const todos = todoRecordsFromEvents(events).filter(
655
+ const tasks = taskRecordsFromEvents(events).filter(
491
656
  (record) => record.event.assigneeAgentId === profile.id,
492
657
  );
493
658
  const rebuiltAt = events.at(-1)?.createdAt ?? new Date(0).toISOString();
@@ -495,14 +660,14 @@ export class ProjectionManager implements ProjectionWriter {
495
660
  const inboxDirectory = join(agentDirectory, 'inbox');
496
661
  const kudosInboxDirectory = join(inboxDirectory, 'kudos');
497
662
  const memoInboxDirectory = join(inboxDirectory, 'memos');
498
- const todoInboxDirectory = join(inboxDirectory, 'todos');
663
+ const taskInboxDirectory = join(inboxDirectory, 'tasks');
499
664
  assertNoSymlinkEscape(this.storage.home, agentDirectory);
500
665
  assertNoSymlinkEscape(this.storage.home, inboxDirectory);
501
666
  ensureDirectory(agentDirectory);
502
667
  ensureDirectory(inboxDirectory);
503
668
  ensureDirectory(kudosInboxDirectory);
504
669
  ensureDirectory(memoInboxDirectory);
505
- ensureDirectory(todoInboxDirectory);
670
+ ensureDirectory(taskInboxDirectory);
506
671
 
507
672
  const generated: string[] = [];
508
673
  const profilePath = join(agentDirectory, 'profile.json');
@@ -532,10 +697,10 @@ export class ProjectionManager implements ProjectionWriter {
532
697
  generated.push(manifestPath(this.storage.home, memoryPath));
533
698
  }
534
699
 
535
- if (this.storage.config.projection.writeTodosMarkdown) {
536
- const todosPath = join(agentDirectory, 'TODOS.md');
537
- atomicWriteDerivedFile(todosPath, todosMarkdown(profile, todos, rebuiltAt));
538
- generated.push(manifestPath(this.storage.home, todosPath));
700
+ if (this.storage.config.projection.writeTasksMarkdown) {
701
+ const tasksPath = join(agentDirectory, 'TASKS.md');
702
+ atomicWriteDerivedFile(tasksPath, tasksMarkdown(profile, tasks, rebuiltAt));
703
+ generated.push(manifestPath(this.storage.home, tasksPath));
539
704
  }
540
705
 
541
706
  if (this.storage.config.projection.writeInboxEntries) {
@@ -553,12 +718,12 @@ export class ProjectionManager implements ProjectionWriter {
553
718
  if (!existsSync(inboxPath)) atomicWriteDerivedFile(inboxPath, memoInboxMarkdown(record));
554
719
  generated.push(relativePath);
555
720
  }
556
- for (const record of todos.filter(
721
+ for (const record of tasks.filter(
557
722
  (item) => item.status === 'open' || item.status === 'assigned',
558
723
  )) {
559
- const inboxPath = join(todoInboxDirectory, `${record.event.id}.md`);
724
+ const inboxPath = join(taskInboxDirectory, `${record.event.id}.md`);
560
725
  const relativePath = manifestPath(this.storage.home, inboxPath);
561
- if (!existsSync(inboxPath)) atomicWriteDerivedFile(inboxPath, todoInboxMarkdown(record));
726
+ if (!existsSync(inboxPath)) atomicWriteDerivedFile(inboxPath, taskInboxMarkdown(record));
562
727
  generated.push(relativePath);
563
728
  }
564
729
  }
@@ -589,13 +754,13 @@ export class ProjectionManager implements ProjectionWriter {
589
754
  const events = this.storage.getReadableEvents();
590
755
  const records = recordsFromEvents(events);
591
756
  const memos = memoRecordsFromEvents(events);
592
- const todos = todoRecordsFromEvents(events);
757
+ const tasks = taskRecordsFromEvents(events);
593
758
  const paths: string[] = [];
594
759
  for (const profile of profiles) {
595
760
  paths.push(`${profile.id}/profile.json`);
596
761
  if (this.storage.config.projection.writeWinsMarkdown) paths.push(`${profile.id}/WINS.md`);
597
762
  if (this.storage.config.projection.writeMemoryMarkdown) paths.push(`${profile.id}/MEMORY.md`);
598
- if (this.storage.config.projection.writeTodosMarkdown) paths.push(`${profile.id}/TODOS.md`);
763
+ if (this.storage.config.projection.writeTasksMarkdown) paths.push(`${profile.id}/TASKS.md`);
599
764
  if (this.storage.config.projection.writeInboxEntries) {
600
765
  for (const record of records.filter(
601
766
  (item) =>
@@ -610,12 +775,12 @@ export class ProjectionManager implements ProjectionWriter {
610
775
  )) {
611
776
  paths.push(`${profile.id}/inbox/memos/${record.event.id}.md`);
612
777
  }
613
- for (const record of todos.filter(
778
+ for (const record of tasks.filter(
614
779
  (item) =>
615
780
  item.event.assigneeAgentId === profile.id &&
616
781
  (item.status === 'open' || item.status === 'assigned'),
617
782
  )) {
618
- paths.push(`${profile.id}/inbox/todos/${record.event.id}.md`);
783
+ paths.push(`${profile.id}/inbox/tasks/${record.event.id}.md`);
619
784
  }
620
785
  }
621
786
  }
package/src/remote.ts CHANGED
@@ -4,16 +4,21 @@ import type { SynomemService, SynomemServiceCapabilities, SynomemServiceInfo } f
4
4
  import type {
5
5
  ActorIdentity,
6
6
  ChangesInput,
7
+ BindRuntimeInput,
7
8
  CreateAgentInput,
9
+ CreatePostInput,
10
+ UpdatePostInput,
8
11
  CreateNoteInput,
9
- CreateTodoInput,
12
+ CreateTaskInput,
10
13
  GiveKudosInput,
11
14
  ItemListInput,
12
15
  KudosListInput,
13
16
  ReviseNoteInput,
14
17
  SendMemoInput,
15
18
  UpdateAgentInput,
19
+ UpdateTaskInput,
16
20
  UpdateTodoInput,
21
+ CreateTodoInput,
17
22
  } from './types.js';
18
23
 
19
24
  const defaultMaximumResponseBytes = 1024 * 1024;
@@ -169,6 +174,78 @@ export class RemoteSynomemService implements SynomemService {
169
174
  ),
170
175
  list: () =>
171
176
  this.request<Awaited<ReturnType<SynomemService['agents']['list']>>>('GET', 'agents'),
177
+ resolve: (query: string) =>
178
+ this.request<Awaited<ReturnType<SynomemService['agents']['resolve']>>>(
179
+ 'GET',
180
+ `agents/resolve?query=${encodeURIComponent(query)}`,
181
+ ),
182
+ directory: () =>
183
+ this.request<Awaited<ReturnType<SynomemService['agents']['directory']>>>(
184
+ 'GET',
185
+ 'agents/directory',
186
+ ),
187
+ bindings: (idOrAlias: string) =>
188
+ this.request<Awaited<ReturnType<SynomemService['agents']['bindings']>>>(
189
+ 'GET',
190
+ `agents/${encodeURIComponent(idOrAlias)}/runtimes`,
191
+ ),
192
+ bindRuntime: (input: BindRuntimeInput) =>
193
+ this.mutation<Awaited<ReturnType<SynomemService['agents']['bindRuntime']>>>(
194
+ 'POST',
195
+ `agents/${encodeURIComponent(input.agentId)}/runtimes`,
196
+ input,
197
+ ),
198
+ unbindRuntime: (bindingId: string) =>
199
+ this.request<Awaited<ReturnType<SynomemService['agents']['unbindRuntime']>>>(
200
+ 'DELETE',
201
+ `agents/runtimes/${encodeURIComponent(bindingId)}`,
202
+ ),
203
+ };
204
+
205
+ readonly posts = {
206
+ create: (input: CreatePostInput) =>
207
+ this.mutation<Awaited<ReturnType<SynomemService['posts']['create']>>>('POST', 'posts', input),
208
+ list: (input: Omit<ItemListInput, 'kinds'> = {}) =>
209
+ this.request<Awaited<ReturnType<SynomemService['posts']['list']>>>(
210
+ 'GET',
211
+ `posts${queryString(input)}`,
212
+ ),
213
+ get: (id: string) =>
214
+ this.request<Awaited<ReturnType<SynomemService['posts']['get']>>>(
215
+ 'GET',
216
+ `posts/${encodeURIComponent(id)}`,
217
+ ),
218
+ update: (input: UpdatePostInput) =>
219
+ this.mutation<Awaited<ReturnType<SynomemService['posts']['update']>>>(
220
+ 'POST',
221
+ `posts/${encodeURIComponent(input.postId)}/revisions`,
222
+ input,
223
+ ['postId'],
224
+ ),
225
+ archive: (input: { postId: string; reason?: string; idempotencyKey?: string }) =>
226
+ this.mutation<Awaited<ReturnType<SynomemService['posts']['archive']>>>(
227
+ 'POST',
228
+ `posts/${encodeURIComponent(input.postId)}/archive`,
229
+ input,
230
+ ['postId'],
231
+ ),
232
+ acknowledge: (input: { postId: string; note?: string; idempotencyKey?: string }) =>
233
+ this.mutation<Awaited<ReturnType<SynomemService['posts']['acknowledge']>>>(
234
+ 'POST',
235
+ `posts/${encodeURIComponent(input.postId)}/acknowledgment`,
236
+ input,
237
+ ['postId'],
238
+ ),
239
+ withdrawAcknowledgment: (input: { postId: string; reason?: string }) =>
240
+ this.request<Awaited<ReturnType<SynomemService['posts']['withdrawAcknowledgment']>>>(
241
+ 'DELETE',
242
+ `posts/${encodeURIComponent(input.postId)}/acknowledgment`,
243
+ ),
244
+ roster: (postId: string) =>
245
+ this.request<Awaited<ReturnType<SynomemService['posts']['roster']>>>(
246
+ 'GET',
247
+ `posts/${encodeURIComponent(postId)}/roster`,
248
+ ),
172
249
  };
173
250
 
174
251
  readonly kudos = {
@@ -261,6 +338,42 @@ export class RemoteSynomemService implements SynomemService {
261
338
  ),
262
339
  };
263
340
 
341
+ readonly tasks = {
342
+ create: (input: CreateTaskInput) =>
343
+ this.mutation<Awaited<ReturnType<SynomemService['tasks']['create']>>>('POST', 'tasks', input),
344
+ list: (input: Omit<ItemListInput, 'kinds'> = {}) =>
345
+ this.request<Awaited<ReturnType<SynomemService['tasks']['list']>>>(
346
+ 'GET',
347
+ `tasks${queryString(input)}`,
348
+ ),
349
+ get: (id: string) =>
350
+ this.request<Awaited<ReturnType<SynomemService['tasks']['get']>>>(
351
+ 'GET',
352
+ `tasks/${encodeURIComponent(id)}`,
353
+ ),
354
+ update: (input: UpdateTaskInput) =>
355
+ this.mutation<Awaited<ReturnType<SynomemService['tasks']['update']>>>(
356
+ 'POST',
357
+ `tasks/${encodeURIComponent(input.taskId)}/revisions`,
358
+ input,
359
+ ['taskId'],
360
+ ),
361
+ accept: (input: { taskId: string; response?: string; idempotencyKey?: string }) =>
362
+ this.taskTransition('accept', input),
363
+ reject: (input: { taskId: string; response: string; idempotencyKey?: string }) =>
364
+ this.taskTransition('reject', input),
365
+ complete: (input: { taskId: string; note?: string; idempotencyKey?: string }) =>
366
+ this.taskTransition('complete', input),
367
+ reopen: (input: { taskId: string; idempotencyKey?: string }) =>
368
+ this.taskTransition('reopen', input),
369
+ cancel: (input: { taskId: string; reason?: string; idempotencyKey?: string }) =>
370
+ this.taskTransition('cancel', input),
371
+ };
372
+
373
+ /**
374
+ * Private self-reminders. The remote surface mirrors the local one exactly so
375
+ * a caller does not have to know which backend it is talking to.
376
+ */
264
377
  readonly todos = {
265
378
  create: (input: CreateTodoInput) =>
266
379
  this.mutation<Awaited<ReturnType<SynomemService['todos']['create']>>>('POST', 'todos', input),
@@ -281,16 +394,43 @@ export class RemoteSynomemService implements SynomemService {
281
394
  input,
282
395
  ['todoId'],
283
396
  ),
284
- accept: (input: { todoId: string; idempotencyKey?: string }) =>
285
- this.todoTransition('accept', input),
286
- reject: (input: { todoId: string; reason?: string; idempotencyKey?: string }) =>
287
- this.todoTransition('reject', input),
288
397
  complete: (input: { todoId: string; note?: string; idempotencyKey?: string }) =>
289
398
  this.todoTransition('complete', input),
290
399
  reopen: (input: { todoId: string; idempotencyKey?: string }) =>
291
400
  this.todoTransition('reopen', input),
292
401
  cancel: (input: { todoId: string; reason?: string; idempotencyKey?: string }) =>
293
402
  this.todoTransition('cancel', input),
403
+ archive: (input: { todoId: string; idempotencyKey?: string }) =>
404
+ this.todoTransition('archive', input),
405
+ };
406
+
407
+ /**
408
+ * Discovery mirrors the local semantics: the age/deadline is resolved here so
409
+ * both backends answer the same question, rather than each server deciding
410
+ * what "older than 24 hours" means.
411
+ */
412
+ readonly discovery = {
413
+ unanswered: (
414
+ input: Omit<ItemListInput, 'awaitingResponse' | 'pending'> & { olderThanHours?: number } = {},
415
+ ) => {
416
+ const { olderThanHours, awaitingSince, ...rest } = input;
417
+ const since =
418
+ awaitingSince ??
419
+ (olderThanHours !== undefined
420
+ ? new Date(Date.now() - olderThanHours * 3_600_000).toISOString()
421
+ : undefined);
422
+ return this.request<Awaited<ReturnType<SynomemService['items']['list']>>>(
423
+ 'GET',
424
+ `items${queryString({ ...rest, awaitingResponse: true, ...(since ? { awaitingSince: since } : {}) })}`,
425
+ );
426
+ },
427
+ overdue: (input: Omit<ItemListInput, 'overdueAsOf'> & { asOf?: string } = {}) => {
428
+ const { asOf, ...rest } = input;
429
+ return this.request<Awaited<ReturnType<SynomemService['items']['list']>>>(
430
+ 'GET',
431
+ `items${queryString({ ...rest, overdueAsOf: asOf ?? new Date().toISOString() })}`,
432
+ );
433
+ },
294
434
  };
295
435
 
296
436
  readonly items = {
@@ -410,11 +550,29 @@ export class RemoteSynomemService implements SynomemService {
410
550
  );
411
551
  }
412
552
 
413
- private todoTransition(
553
+ private taskTransition(
414
554
  transition: 'accept' | 'reject' | 'complete' | 'reopen' | 'cancel',
555
+ input: {
556
+ taskId: string;
557
+ idempotencyKey?: string;
558
+ reason?: string;
559
+ note?: string;
560
+ response?: string;
561
+ },
562
+ ) {
563
+ return this.mutation<Awaited<ReturnType<SynomemService['tasks']['accept']>>>(
564
+ 'POST',
565
+ `tasks/${encodeURIComponent(input.taskId)}/${transition}`,
566
+ input,
567
+ ['taskId'],
568
+ );
569
+ }
570
+
571
+ private todoTransition(
572
+ transition: 'complete' | 'reopen' | 'cancel' | 'archive',
415
573
  input: { todoId: string; idempotencyKey?: string; reason?: string; note?: string },
416
574
  ) {
417
- return this.mutation<Awaited<ReturnType<SynomemService['todos']['accept']>>>(
575
+ return this.mutation<Awaited<ReturnType<SynomemService['todos']['complete']>>>(
418
576
  'POST',
419
577
  `todos/${encodeURIComponent(input.todoId)}/${transition}`,
420
578
  input,
@@ -437,7 +595,7 @@ export class RemoteSynomemService implements SynomemService {
437
595
  }
438
596
 
439
597
  private async request<T>(
440
- method: 'GET' | 'POST' | 'PATCH',
598
+ method: 'GET' | 'POST' | 'PATCH' | 'DELETE',
441
599
  path: string,
442
600
  body?: object,
443
601
  idempotencyKey?: string,