@coffer-org/server 3.4.0 → 5.0.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 (101) hide show
  1. package/dist/auth-api.js +4 -5
  2. package/dist/cache-clock.d.ts +8 -0
  3. package/dist/cache-clock.js +103 -0
  4. package/dist/cache-refresh.d.ts +7 -0
  5. package/dist/cache-refresh.js +322 -0
  6. package/dist/collection-io.d.ts +13 -0
  7. package/dist/collection-io.js +40 -29
  8. package/dist/compute-unit.d.ts +66 -0
  9. package/dist/compute-unit.js +369 -0
  10. package/dist/condition-where.js +1 -3
  11. package/dist/data-dir.d.ts +3 -0
  12. package/dist/data-dir.js +39 -0
  13. package/dist/entity-schema.js +5 -2
  14. package/dist/extend-table.d.ts +1 -1
  15. package/dist/extend-table.js +22 -8
  16. package/dist/file-fields.js +50 -23
  17. package/dist/frontend-agent.d.ts +1 -1
  18. package/dist/frontend-agent.js +2 -3
  19. package/dist/global-search.js +1 -1
  20. package/dist/index.js +20 -26
  21. package/dist/mcp-contract/client.d.ts +24 -0
  22. package/dist/mcp-contract/client.js +12 -0
  23. package/dist/mcp-contract/schema.d.ts +46 -0
  24. package/dist/mcp-contract/schema.js +90 -0
  25. package/dist/mcp-contract/tools.d.ts +19 -0
  26. package/dist/mcp-contract/tools.js +160 -0
  27. package/dist/mcp-http.js +4 -2
  28. package/dist/mcp-local.d.ts +1 -1
  29. package/dist/mcp-local.js +1 -1
  30. package/dist/mcp-tools.d.ts +13 -1
  31. package/dist/mcp-tools.js +46 -16
  32. package/dist/migrations.d.ts +5 -0
  33. package/dist/migrations.js +122 -0
  34. package/dist/mutate.js +77 -36
  35. package/dist/orchestrator/agent-capabilities.d.ts +2 -0
  36. package/dist/orchestrator/agent-capabilities.js +201 -0
  37. package/dist/orchestrator/allow.d.ts +16 -0
  38. package/dist/orchestrator/allow.js +65 -0
  39. package/dist/orchestrator/attachments.d.ts +2 -0
  40. package/dist/orchestrator/attachments.js +11 -0
  41. package/dist/orchestrator/config.d.ts +4 -0
  42. package/dist/orchestrator/config.js +19 -0
  43. package/dist/orchestrator/db.d.ts +17 -0
  44. package/dist/orchestrator/db.js +12 -0
  45. package/dist/orchestrator/diagnostics.d.ts +9 -0
  46. package/dist/orchestrator/diagnostics.js +13 -0
  47. package/dist/orchestrator/environment.d.ts +2 -0
  48. package/dist/orchestrator/environment.js +14 -0
  49. package/dist/orchestrator/file-inspection.d.ts +3 -0
  50. package/dist/orchestrator/file-inspection.js +61 -0
  51. package/dist/orchestrator/format.d.ts +1 -0
  52. package/dist/orchestrator/format.js +6 -0
  53. package/dist/orchestrator/index.d.ts +30 -0
  54. package/dist/orchestrator/index.js +56 -0
  55. package/dist/orchestrator/live-message.d.ts +14 -0
  56. package/dist/orchestrator/live-message.js +100 -0
  57. package/dist/orchestrator/pipeline.d.ts +11 -0
  58. package/dist/orchestrator/pipeline.js +198 -0
  59. package/dist/orchestrator/registry.d.ts +13 -0
  60. package/dist/orchestrator/registry.js +74 -0
  61. package/dist/orchestrator/starters.d.ts +19 -0
  62. package/dist/orchestrator/starters.js +81 -0
  63. package/dist/orchestrator/suggestion-capabilities.d.ts +5 -0
  64. package/dist/orchestrator/suggestion-capabilities.js +33 -0
  65. package/dist/orchestrator/system-assembly.d.ts +4 -0
  66. package/dist/orchestrator/system-assembly.js +3 -0
  67. package/dist/orchestrator/system-capabilities.d.ts +2 -0
  68. package/dist/orchestrator/system-capabilities.js +63 -0
  69. package/dist/orchestrator/types.d.ts +141 -0
  70. package/dist/orchestrator/types.js +1 -0
  71. package/dist/part-injection.d.ts +3 -4
  72. package/dist/part-injection.js +25 -368
  73. package/dist/plugin-hooks.d.ts +1 -0
  74. package/dist/plugin-i18n.js +2 -1
  75. package/dist/plugin-runtime.d.ts +2 -2
  76. package/dist/plugin-runtime.js +20 -11
  77. package/dist/plugin-updates.d.ts +2 -2
  78. package/dist/plugin-updates.js +6 -12
  79. package/dist/public-url.js +2 -1
  80. package/dist/recompute-derived.d.ts +2 -1
  81. package/dist/recompute-derived.js +111 -34
  82. package/dist/records-api.js +14 -16
  83. package/dist/registry-context.d.ts +1 -1
  84. package/dist/registry-context.js +3 -0
  85. package/dist/search-index.js +1 -1
  86. package/dist/search-indexer.d.ts +1 -1
  87. package/dist/search-indexer.js +3 -3
  88. package/dist/settings-groups.d.ts +14 -0
  89. package/dist/settings-groups.js +25 -0
  90. package/dist/settings-write.d.ts +4 -4
  91. package/dist/settings-write.js +23 -26
  92. package/dist/system-settings.d.ts +3 -0
  93. package/dist/system-settings.js +27 -0
  94. package/dist/thread-store.d.ts +3 -2
  95. package/dist/thread-store.js +6 -5
  96. package/dist/trigger-queue.d.ts +33 -0
  97. package/dist/trigger-queue.js +432 -0
  98. package/dist/uploads.js +6 -3
  99. package/dist/value-compare.d.ts +1 -0
  100. package/dist/value-compare.js +7 -0
  101. package/package.json +17 -3
package/dist/mcp-tools.js CHANGED
@@ -1,6 +1,7 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { z } from 'zod';
2
- import { buildTools } from '@coffer-org/mcp';
3
- import { SchemaCache } from '@coffer-org/mcp/schema';
3
+ import { buildTools } from "./mcp-contract/tools.js";
4
+ import { SchemaCache } from "./mcp-contract/schema.js";
4
5
  import { LocalClient } from "./mcp-local.js";
5
6
  import { frontendInstructions } from "./frontend-agent.js";
6
7
  import { mintUploadTicket } from "./upload-ticket.js";
@@ -81,7 +82,7 @@ export async function collectMcpTools(opts = {}) {
81
82
  how_to: `curl -H "Authorization: Bearer <token>" -F "file=@<path>" ${uploadUrl} → {"name":"<name>"}. Then set a file field to {"name":"<name>"}. mime/size are filled in by the server — do not send your own.` +
82
83
  (base
83
84
  ? ''
84
- : " The server has no public URL configured (core settings publicUrl / PUBLIC_URL env), so the path is relative — resolve it against this MCP server's own origin."),
85
+ : " The server has no public URL configured (system settings publicUrl / PUBLIC_URL env), so the path is relative — resolve it against this MCP server's own origin."),
85
86
  });
86
87
  },
87
88
  });
@@ -204,7 +205,7 @@ export async function collectMcpTools(opts = {}) {
204
205
  server: 'coffer',
205
206
  bareName: 'list_settings',
206
207
  httpName: 'list_settings',
207
- description: 'List every plugin and system settings group (including "core"): each field\'s key/kind/required and the current values (secrets masked). Call before update_settings.',
208
+ description: "List every settings group (the instance's own `system` group and each plugin's): each field's key/kind/required and the current values (secrets masked). Call before update_settings.",
208
209
  inputSchema: {},
209
210
  scope: 'settings',
210
211
  role: 'admin',
@@ -221,13 +222,13 @@ export async function collectMcpTools(opts = {}) {
221
222
  server: 'coffer',
222
223
  bareName: 'update_settings',
223
224
  httpName: 'update_settings',
224
- description: 'Update a plugin or system settings group. plugin — the plugin id (e.g. "claude-agent", "core"); fields — only the settings to change (see list_settings). Send real secret values; the masked placeholder (********) is treated as unchanged.',
225
- inputSchema: { plugin: z.string(), fields: z.record(z.string(), z.unknown()) },
225
+ description: 'Update a plugin or system settings group. group — the settings group id (e.g. "system", "claude-agent"); fields — only the settings to change (see list_settings). Send real secret values; the masked placeholder (********) is treated as unchanged.',
226
+ inputSchema: { group: z.string(), fields: z.record(z.string(), z.unknown()) },
226
227
  scope: 'settings',
227
228
  role: 'admin',
228
229
  handler: async (args) => {
229
230
  try {
230
- const row = await writePluginSettings(getEm().fork(), args.plugin, args.fields, actor);
231
+ const row = await writePluginSettings(getEm().fork(), args.group, args.fields, actor);
231
232
  return ok(row);
232
233
  }
233
234
  catch (e) {
@@ -239,7 +240,7 @@ export async function collectMcpTools(opts = {}) {
239
240
  return fail(`Validation error:\n${lines.join('\n')}`);
240
241
  }
241
242
  if (e instanceof NotFoundError)
242
- return fail(`Plugin '${String(args.plugin)}' has no settings.`);
243
+ return fail(`No settings group '${String(args.group)}'.`);
243
244
  return fail(`Error: ${e.message}`);
244
245
  }
245
246
  },
@@ -304,6 +305,10 @@ export function collectLibraryPurposes(reg) {
304
305
  : []),
305
306
  }));
306
307
  }
308
+ export async function siteSection(siteUrl) {
309
+ const site = await frontendInstructions(siteUrl);
310
+ return site ? `## web\n${site}` : null;
311
+ }
307
312
  export async function buildDomainSections() {
308
313
  const libraries = collectLibraryPurposes();
309
314
  let overview = null;
@@ -340,14 +345,39 @@ export async function buildDomainSections() {
340
345
  'For duplicates, read both records completely, compare fields/collections/extends/attachments, recommend the less complete record for trash, and offer a field-by-field merge first. ' +
341
346
  'Use list_trash and restore_record for recovery; purge_record is irreversible and requires explicit confirmation.';
342
347
  const rules = (await collectPluginInstructions()).map(({ id, instructions }) => `## ${id}\n${instructions}`);
343
- const site = await frontendInstructions();
344
- return [
345
- dataModel,
346
- ...(overview ? [overview] : []),
347
- ...(singleSection ? [singleSection] : []),
348
- ...(site ? [`## web\n${site}`] : []),
349
- ...rules,
350
- ];
348
+ return [dataModel, ...(overview ? [overview] : []), ...(singleSection ? [singleSection] : []), ...rules];
349
+ }
350
+ export async function collectStarterHints(hooks = pluginHooks, emFactory = () => getEm().fork(), reg) {
351
+ const parts = [];
352
+ for (const [id, h] of Object.entries(hooks)) {
353
+ const hint = h.agent?.starterHint;
354
+ if (hint == null)
355
+ continue;
356
+ try {
357
+ const text = typeof hint === 'function' ? await hint(pluginCtx(id, emFactory())) : hint;
358
+ if (text)
359
+ parts.push(`- ${id}: ${text}`);
360
+ }
361
+ catch (e) {
362
+ log.warn(`plugin ${id}: starterHint skipped — ${e.message}`);
363
+ }
364
+ }
365
+ let registry = reg;
366
+ if (!registry) {
367
+ try {
368
+ registry = getActiveRegistry();
369
+ }
370
+ catch {
371
+ registry = undefined;
372
+ }
373
+ }
374
+ for (const v of registry?.libraries ?? []) {
375
+ if (v.meta.starterHint)
376
+ parts.push(`- ${v.meta.id}: ${v.meta.starterHint}`);
377
+ }
378
+ const combined = parts.join('\n');
379
+ const hash = createHash('sha1').update(combined).digest('hex');
380
+ return { combined, hash };
351
381
  }
352
382
  export function buildMcpInstructions(sections) {
353
383
  const base = [
@@ -29,6 +29,11 @@ export type OpRecorder = (op: MigrationOp) => void;
29
29
  export declare function describeMigrationConcerns(plugin: string, version: number, ops: MigrationOp[]): string | null;
30
30
  export declare function makeTable(em: EntityManager, table: string, record?: OpRecorder): TableOps;
31
31
  export declare function renameSystemShelfKey(em: EntityManager): Promise<void>;
32
+ export declare const SYSTEM_MIGRATIONS: {
33
+ name: string;
34
+ up(em: EntityManager): Promise<void>;
35
+ }[];
36
+ export declare function runSystemMigrations(em: EntityManager): Promise<void>;
32
37
  export declare function validateMigrations(pluginId: string, list: Migration[]): void;
33
38
  interface RunArgs {
34
39
  em: EntityManager;
@@ -1,6 +1,8 @@
1
1
  import { DatabaseSchema } from '@mikro-orm/sql';
2
+ import { fieldEntries } from '@coffer-org/sdk/shelf';
2
3
  import { dialectOf } from "./dialect.js";
3
4
  import { getLogger } from '@coffer-org/sdk/logger';
5
+ import { SYSTEM_SETTINGS } from "./system-settings.js";
4
6
  const log = getLogger('migrations');
5
7
  function sqlType(col) {
6
8
  if (col === 'integer' || col === 'boolean')
@@ -392,6 +394,126 @@ export function makeTable(em, table, record) {
392
394
  export async function renameSystemShelfKey(em) {
393
395
  await makeTable(em, '_embeddings').renameColumn('type', 'shelf_key');
394
396
  }
397
+ const LEGACY_ORCHESTRATOR_FIELDS = ['agent_id', 'access_password', 'trigger_prefix', 'reply_window'];
398
+ export const SYSTEM_MIGRATIONS = [
399
+ {
400
+ name: 'embeddings-shelf-key',
401
+ up: renameSystemShelfKey,
402
+ },
403
+ {
404
+ name: 'core-settings-to-system',
405
+ up: async (em) => {
406
+ const conn = em.getConnection();
407
+ const src = await introspectTable(em, '_settings__core');
408
+ const dst = await introspectTable(em, '_settings__system');
409
+ if (!src.exists || !dst.exists)
410
+ return;
411
+ const already = (await conn.execute(`SELECT 1 FROM "_settings__system" WHERE "plugin_id" = 'system' LIMIT 1`));
412
+ if (already.length > 0)
413
+ return;
414
+ const rows = (await conn.execute(`SELECT * FROM "_settings__core" WHERE "plugin_id" = 'core' LIMIT 1`));
415
+ const row = rows[0];
416
+ if (!row)
417
+ return;
418
+ const carried = ['publicUrl', 'language'].filter((c) => src.columns.has(c) && dst.columns.has(c));
419
+ if (carried.length === 0)
420
+ return;
421
+ const cols = ['plugin_id', ...carried];
422
+ const values = ['system', ...carried.map((c) => row[c] ?? null)];
423
+ await conn.execute(`INSERT INTO "_settings__system" (${cols.map((c) => `"${c}"`).join(', ')}) ` +
424
+ `VALUES (${cols.map(() => '?').join(', ')})`, values);
425
+ log.info(`[system-migration] settings moved from _settings__core to _settings__system`);
426
+ },
427
+ },
428
+ {
429
+ name: 'core-state-to-system',
430
+ up: async (em) => {
431
+ const conn = em.getConnection();
432
+ if (!(await introspectTable(em, '_plugin_state')).exists)
433
+ return;
434
+ await conn.execute(`UPDATE "_plugin_state" SET "plugin" = 'system' ` +
435
+ `WHERE "plugin" = 'core' ` +
436
+ `AND "key" NOT IN (SELECT "key" FROM "_plugin_state" WHERE "plugin" = 'system')`);
437
+ await conn.execute(`DELETE FROM "_plugin_state" WHERE "plugin" = 'core'`);
438
+ },
439
+ },
440
+ {
441
+ name: 'drop-legacy-core-settings',
442
+ up: async (em) => {
443
+ const conn = em.getConnection();
444
+ if (!(await introspectTable(em, '_settings__core')).exists)
445
+ return;
446
+ if (!(await introspectTable(em, '_settings__system')).exists)
447
+ return;
448
+ const landed = (await conn.execute(`SELECT 1 FROM "_settings__system" WHERE "plugin_id" = 'system' LIMIT 1`));
449
+ if (landed.length === 0)
450
+ return;
451
+ await conn.execute(`DROP TABLE IF EXISTS "_settings__core"`);
452
+ log.info('[system-migration] dropped the legacy _settings__core table');
453
+ },
454
+ },
455
+ {
456
+ name: 'orchestrator-settings-to-system',
457
+ up: async (em) => {
458
+ const DEFAULTS = Object.fromEntries(fieldEntries(SYSTEM_SETTINGS.fields)
459
+ .filter(([k]) => LEGACY_ORCHESTRATOR_FIELDS.includes(k))
460
+ .map(([k, f]) => [k, f.default]));
461
+ const conn = em.getConnection();
462
+ const src = await introspectTable(em, '_settings__orchestrator');
463
+ const dst = await introspectTable(em, '_settings__system');
464
+ if (!src.exists || !dst.exists)
465
+ return;
466
+ const carried = LEGACY_ORCHESTRATOR_FIELDS.filter((c) => src.columns.has(c) && dst.columns.has(c));
467
+ if (carried.length === 0)
468
+ return;
469
+ const rows = (await conn.execute(`SELECT * FROM "_settings__orchestrator" WHERE "plugin_id" = 'orchestrator' LIMIT 1`));
470
+ const row = rows[0];
471
+ if (!row)
472
+ return;
473
+ let wrote = false;
474
+ for (const c of carried) {
475
+ const value = row[c];
476
+ if (value === undefined || value === null)
477
+ continue;
478
+ const def = DEFAULTS[c];
479
+ const whereExtra = def === undefined ? `${q(c)} IS NULL` : `(${q(c)} IS NULL OR ${q(c)} = ?)`;
480
+ const params = def === undefined ? [value] : [value, def];
481
+ await conn.execute(`UPDATE "_settings__system" SET ${q(c)} = ? WHERE "plugin_id" = 'system' AND ${whereExtra}`, params);
482
+ wrote = true;
483
+ }
484
+ if (wrote)
485
+ log.info('[system-migration] orchestrator settings carried into _settings__system');
486
+ },
487
+ },
488
+ {
489
+ name: 'drop-legacy-orchestrator-settings',
490
+ up: async (em) => {
491
+ const conn = em.getConnection();
492
+ const src = await introspectTable(em, '_settings__orchestrator');
493
+ const dst = await introspectTable(em, '_settings__system');
494
+ if (!src.exists || !dst.exists)
495
+ return;
496
+ const fields = LEGACY_ORCHESTRATOR_FIELDS;
497
+ const srcRows = (await conn.execute(`SELECT * FROM "_settings__orchestrator" WHERE "plugin_id" = 'orchestrator' LIMIT 1`));
498
+ const srcRow = srcRows[0];
499
+ const needsLanding = fields.filter((c) => src.columns.has(c) && srcRow?.[c] !== undefined && srcRow?.[c] !== null);
500
+ if (needsLanding.length > 0) {
501
+ const dstRows = (await conn.execute(`SELECT * FROM "_settings__system" WHERE "plugin_id" = 'system' LIMIT 1`));
502
+ const dstRow = dstRows[0];
503
+ const landed = needsLanding.every((c) => dst.columns.has(c) && dstRow?.[c] !== undefined && dstRow?.[c] !== null);
504
+ if (!landed)
505
+ return;
506
+ }
507
+ await conn.execute(`DROP TABLE IF EXISTS "_settings__orchestrator"`);
508
+ log.info('[system-migration] dropped the legacy _settings__orchestrator table');
509
+ },
510
+ },
511
+ ];
512
+ export async function runSystemMigrations(em) {
513
+ for (const m of SYSTEM_MIGRATIONS) {
514
+ await m.up(em);
515
+ }
516
+ }
395
517
  export function validateMigrations(pluginId, list) {
396
518
  const versions = list.map((m) => m.version).sort((a, b) => a - b);
397
519
  versions.forEach((v, i) => {
package/dist/mutate.js CHANGED
@@ -1,35 +1,70 @@
1
1
  import { serialize } from '@mikro-orm/core';
2
- import { buildZodObject, buildZodObjectPartial, fieldEntries } from '@coffer-org/sdk/shelf';
2
+ import { buildZodObject, buildZodObjectPartial, fieldEntries, collectionGroups } from '@coffer-org/sdk/shelf';
3
3
  import { isJsonStored, decodeVmsg } from '@coffer-org/sdk/fields';
4
4
  import { resolveFlag } from '@coffer-org/sdk/condition';
5
- import { applyDerived } from '@coffer-org/sdk/derive';
6
5
  import { getEm } from "./db.js";
7
6
  import { selectRows } from "./read-rows.js";
8
7
  import { normalizeFileFields, dropUnchangedFileFields, touchesFileFields } from "./file-fields.js";
9
8
  import { notifyRecordsChanged } from "./index-signal.js";
10
9
  import { encodeTemporal, decodeTemporal } from "./temporal.js";
11
- import { splitCollections, writeCollections, deleteCollections, flattenEmbedded, nestEmbedded, readCollections, } from "./collection-io.js";
12
- import { injectParts, applyStoredParts, hasStoredPartWork, newPartMemo, } from "./part-injection.js";
10
+ import { splitCollections, writeCollections, deleteCollections, flattenEmbedded, flattenEmbeddedAt, nestEmbedded, readCollections, readCollectionsWithMeta, } from "./collection-io.js";
11
+ import { stripClockKeysDeep, clockValuesOf } from "./cache-clock.js";
12
+ import { refreshCachedUnits } from "./cache-refresh.js";
13
+ import { hasStoredPartWork, newPartMemo } from "./part-injection.js";
14
+ import { changedFields, runTriggers } from "./trigger-queue.js";
15
+ import { getActiveRegistry } from "./registry-context.js";
16
+ import { sameValue } from "./value-compare.js";
13
17
  const EMPTY_SETTINGS = {};
14
18
  const EMPTY_GLOBAL = {};
15
19
  function nowIso() {
16
20
  return new Date().toISOString();
17
21
  }
18
- function partCtx(m, memo, em) {
22
+ function partCtx(m, memo, now, em) {
19
23
  return {
20
24
  settings: EMPTY_SETTINGS,
21
25
  global: EMPTY_GLOBAL,
22
26
  meta: { library: m.library, shelf: m.shelf },
23
27
  ...(memo ? { memo } : {}),
24
28
  ...(em ? { em } : {}),
29
+ ...(now !== undefined ? { now } : {}),
25
30
  };
26
31
  }
27
- async function mergeStoredParts(m, record, memo, em) {
28
- const computed = await applyStoredParts(m.fields, record, partCtx(m, memo, em));
29
- if (computed === record)
30
- return false;
31
- Object.assign(record, computed);
32
- return true;
32
+ async function driveTriggers(m, before, after, memo, now, em, opts = {}) {
33
+ const changed = changedFields(before, after, m.fields);
34
+ const graph = getActiveRegistry().mutateGraph;
35
+ const shelfKey = `${m.library}/${m.shelf}`;
36
+ const preTrigger = { ...after };
37
+ const touched = [];
38
+ const triggered = await runTriggers(m.fields, after, changed, graph, shelfKey, partCtx(m, memo, now, em), {
39
+ ...opts,
40
+ touched,
41
+ });
42
+ Object.assign(after, triggered);
43
+ return { changed: changedFields(preTrigger, after, m.fields).map((c) => c.path.join('.')), touched };
44
+ }
45
+ async function persistUntouchedCollectionDiffs(tx, m, id, patchedKeys, before, beforeMeta, after) {
46
+ for (const c of collectionGroups(m.fields)) {
47
+ if (patchedKeys.has(c.key))
48
+ continue;
49
+ const beforeRows = before[c.key] ?? [];
50
+ const afterRows = after[c.key] ?? [];
51
+ const metas = beforeMeta[c.key] ?? [];
52
+ const childFields = c.group.fields;
53
+ const table = `${m.library}__${m.shelf}__${c.key}`;
54
+ const n = Math.min(beforeRows.length, afterRows.length, metas.length);
55
+ for (let i = 0; i < n; i++) {
56
+ const rowId = metas[i].id;
57
+ const flatBefore = encodeJsonAt(childFields, flattenEmbeddedAt(childFields, beforeRows[i]));
58
+ const flatAfter = encodeJsonAt(childFields, flattenEmbeddedAt(childFields, afterRows[i]));
59
+ const rowPatch = {};
60
+ for (const [k, v] of Object.entries(flatAfter)) {
61
+ if (!sameValue(flatBefore[k] ?? null, v ?? null))
62
+ rowPatch[k] = v ?? null;
63
+ }
64
+ if (Object.keys(rowPatch).length > 0)
65
+ await tx.nativeUpdate(table, { id: rowId }, rowPatch);
66
+ }
67
+ }
33
68
  }
34
69
  export function encodeJsonAt(fields, data) {
35
70
  const out = { ...data };
@@ -84,10 +119,11 @@ export async function getRecord(m, entityName, id, opts = {}) {
84
119
  if (!row)
85
120
  return undefined;
86
121
  const flat = decodeTemporal(m, row);
122
+ const clocks = clockValuesOf(flat);
87
123
  const nested = nestEmbedded(m, flat);
88
- const collections = await readCollections(fork, m, id);
124
+ const { collections, meta } = await readCollectionsWithMeta(fork, m, id);
89
125
  const record = { ...nested, ...collections };
90
- return injectParts(m.fields, record, partCtx(m));
126
+ return refreshCachedUnits(m, entityName, record, clocks, meta, partCtx(m));
91
127
  }
92
128
  export async function createRecord(m, entityName, input, ctx, afterBase) {
93
129
  const parsed = buildZodObject(m).safeParse(input);
@@ -99,7 +135,9 @@ export async function createRecord(m, entityName, input, ctx, afterBase) {
99
135
  if (fileIssues.length)
100
136
  throw new ValidationError(fileIssues);
101
137
  const memo = newPartMemo();
102
- await mergeStoredParts(m, parsedData, memo);
138
+ const triggerResult = hasStoredPartWork(m.fields)
139
+ ? await driveTriggers(m, {}, parsedData, memo, ts, undefined, { seedAll: true })
140
+ : { changed: [], touched: [] };
103
141
  const { base, collections } = splitCollections(m, { ...parsedData });
104
142
  const data = encodeJson(m, encodeTemporal(m, flattenEmbedded(m, { ...base, created_at: ts, updated_at: ts })));
105
143
  let id;
@@ -110,21 +148,16 @@ export async function createRecord(m, entityName, input, ctx, afterBase) {
110
148
  await tx.flush();
111
149
  id = entity.id;
112
150
  await writeCollections(tx, m, id, collections);
113
- const derived = await applyDerived(m.fields, { ...parsedData, id });
114
- if (Object.keys(derived).length) {
115
- await tx.nativeUpdate(entityName, { id }, encodeJson(m, encodeTemporal(m, flattenEmbedded(m, derived))));
116
- Object.assign(parsedData, derived);
117
- }
118
151
  const _extends = afterBase ? await afterBase(tx, id) : undefined;
119
- writeEvent(tx, ctx.actor, 'create', `${m.library}/${m.shelf}`, id, null, {
120
- ...parsedData,
121
- id,
122
- ...(_extends ? { _extends } : {}),
123
- });
152
+ writeEvent(tx, ctx.actor, 'create', `${m.library}/${m.shelf}`, id, null, stripClockKeysDeep(m.fields, { ...parsedData, id, ...(_extends ? { _extends } : {}) }));
124
153
  });
125
154
  notifyRecordsChanged();
126
155
  const record = { ...parsedData, id, created_at: ts, updated_at: ts };
127
- return injectParts(m.fields, record, partCtx(m, memo));
156
+ return {
157
+ ...stripClockKeysDeep(m.fields, record),
158
+ changed: triggerResult.changed,
159
+ ...(triggerResult.touched.length ? { changedRecords: triggerResult.touched } : {}),
160
+ };
128
161
  }
129
162
  export async function updateRecord(m, entityName, id, input, ctx, afterBase) {
130
163
  let patch = input;
@@ -141,6 +174,7 @@ export async function updateRecord(m, entityName, id, input, ctx, afterBase) {
141
174
  throw new ValidationError(parsed.error.issues.map(toIssue));
142
175
  const memo = newPartMemo();
143
176
  let result;
177
+ let triggerResult = { changed: [], touched: [] };
144
178
  await getEm()
145
179
  .fork()
146
180
  .transactional(async (tx) => {
@@ -158,7 +192,7 @@ export async function updateRecord(m, entityName, id, input, ctx, afterBase) {
158
192
  const merged = { ...existing, ...base };
159
193
  const reqIssues = [];
160
194
  for (const [key, f] of fieldEntries(m.fields)) {
161
- if (f.virtual || f.required === true)
195
+ if (f.required === true)
162
196
  continue;
163
197
  if (key.includes('__'))
164
198
  continue;
@@ -170,30 +204,37 @@ export async function updateRecord(m, entityName, id, input, ctx, afterBase) {
170
204
  }
171
205
  if (reqIssues.length)
172
206
  throw new ValidationError(reqIssues);
173
- const storedCollections = hasStoredPartWork(m.fields) ? await readCollections(tx, m, id) : {};
207
+ const storedWithMeta = hasStoredPartWork(m.fields)
208
+ ? await readCollectionsWithMeta(tx, m, id)
209
+ : { collections: {}, meta: {} };
210
+ const storedCollections = storedWithMeta.collections;
174
211
  const target = { ...merged, ...storedCollections, ...collections };
175
- if (await mergeStoredParts(m, target, memo, tx)) {
212
+ if (hasStoredPartWork(m.fields)) {
213
+ const before = { ...existing, ...storedCollections };
214
+ triggerResult = await driveTriggers(m, before, target, memo, ts, tx);
176
215
  const resplit = splitCollections(m, target);
177
216
  Object.assign(merged, resplit.base);
178
- for (const k of Object.keys(collections)) {
217
+ const patchedKeys = new Set(Object.keys(collections));
218
+ for (const k of patchedKeys) {
179
219
  const rows = resplit.collections[k];
180
220
  if (rows)
181
221
  collections[k] = rows;
182
222
  }
223
+ await persistUntouchedCollectionDiffs(tx, m, id, patchedKeys, storedCollections, storedWithMeta.meta, resplit.collections);
183
224
  }
184
225
  const dbRow = encodeJson(m, encodeTemporal(m, flattenEmbedded(m, { ...merged, updated_at: ts })));
185
226
  await tx.upsert(entityName, dbRow);
186
227
  await writeCollections(tx, m, id, collections);
187
228
  const allCollections = await readCollections(tx, m, id);
188
- const derived = await applyDerived(m.fields, { ...merged, ...allCollections });
189
- if (Object.keys(derived).length) {
190
- await tx.nativeUpdate(entityName, { id }, encodeJson(m, encodeTemporal(m, flattenEmbedded(m, derived))));
191
- Object.assign(merged, derived);
192
- }
193
229
  const _extends = afterBase ? await afterBase(tx, id) : undefined;
194
230
  const after = { ...merged, ...allCollections, updated_at: ts, ...(_extends ? { _extends } : {}) };
231
+ stripClockKeysDeep(m.fields, after);
195
232
  writeEvent(tx, ctx.actor, 'update', `${m.library}/${m.shelf}`, id, existing, after);
196
- result = await injectParts(m.fields, after, partCtx(m, memo, tx));
233
+ result = {
234
+ ...after,
235
+ changed: triggerResult.changed,
236
+ ...(triggerResult.touched.length ? { changedRecords: triggerResult.touched } : {}),
237
+ };
197
238
  });
198
239
  notifyRecordsChanged();
199
240
  return result;
@@ -228,7 +269,7 @@ export async function restoreRecord(m, entityName, id, ctx, afterBase) {
228
269
  const _extends = afterBase ? await afterBase(tx, id) : undefined;
229
270
  const restored = { ...nested, ...collections, ...(_extends ? { _extends } : {}) };
230
271
  writeEvent(tx, ctx.actor, 'restore', `${m.library}/${m.shelf}`, id, null, restored);
231
- result = await injectParts(m.fields, restored, partCtx(m, undefined, tx));
272
+ result = restored;
232
273
  });
233
274
  notifyRecordsChanged();
234
275
  return result;
@@ -0,0 +1,2 @@
1
+ import type { AgentToolProvider, AttachmentRef } from './types.ts';
2
+ export declare function makeAttachmentCapabilities(attachments: AttachmentRef[]): Promise<AgentToolProvider>;