amalgm 0.1.246 → 0.1.247

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 (49) hide show
  1. package/README.md +5 -3
  2. package/lib/cli.js +92 -4
  3. package/lib/shared-realtime-tunnel.js +82 -19
  4. package/package.json +1 -1
  5. package/runtime/scripts/amalgm-mcp/automations/deliveries.js +276 -0
  6. package/runtime/scripts/amalgm-mcp/automations/rest.js +51 -0
  7. package/runtime/scripts/amalgm-mcp/automations/scheduler.js +11 -67
  8. package/runtime/scripts/amalgm-mcp/automations/store.js +67 -20
  9. package/runtime/scripts/amalgm-mcp/events/ingress.js +69 -215
  10. package/runtime/scripts/amalgm-mcp/events/processor.js +189 -0
  11. package/runtime/scripts/amalgm-mcp/lib/email-embeds.js +41 -0
  12. package/runtime/scripts/amalgm-mcp/lib/email-md.js +107 -1
  13. package/runtime/scripts/amalgm-mcp/lib/generated/email-render.json +10 -2
  14. package/runtime/scripts/amalgm-mcp/observer/README.md +8 -6
  15. package/runtime/scripts/amalgm-mcp/registration/entity-cloud.js +262 -22
  16. package/runtime/scripts/amalgm-mcp/registration/entity-content.js +86 -0
  17. package/runtime/scripts/amalgm-mcp/registration/index.js +4 -1
  18. package/runtime/scripts/amalgm-mcp/registration/service.js +102 -0
  19. package/runtime/scripts/amalgm-mcp/registration/tree.js +19 -5
  20. package/runtime/scripts/amalgm-mcp/registry/index.js +16 -1
  21. package/runtime/scripts/amalgm-mcp/registry/store.js +58 -7
  22. package/runtime/scripts/amalgm-mcp/server/routes/automations.js +8 -0
  23. package/runtime/scripts/amalgm-mcp/server/routes/entities.js +56 -0
  24. package/runtime/scripts/amalgm-mcp/server/routes/workspace.js +14 -0
  25. package/runtime/scripts/amalgm-mcp/tests/adapters.test.js +1 -1
  26. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.watch.test.js +2 -2
  27. package/runtime/scripts/amalgm-mcp/tests/email-render-goldens/citations.html +5 -5
  28. package/runtime/scripts/amalgm-mcp/tests/email-render-goldens/links.html +3 -3
  29. package/runtime/scripts/amalgm-mcp/tests/email-render-goldens/notification-shell.html +1 -1
  30. package/runtime/scripts/amalgm-mcp/tests/email-render-goldens/share-invite.html +13 -0
  31. package/runtime/scripts/amalgm-mcp/tests/email-render-goldens/text.html +1 -1
  32. package/runtime/scripts/amalgm-mcp/tests/email-render.test.js +17 -1
  33. package/runtime/scripts/amalgm-mcp/tests/entity-cloud.test.js +189 -0
  34. package/runtime/scripts/amalgm-mcp/tests/entity-content.test.js +126 -0
  35. package/runtime/scripts/amalgm-mcp/tests/entity-materialization.test.js +118 -0
  36. package/runtime/scripts/amalgm-mcp/tests/entity.registry.test.js +126 -11
  37. package/runtime/scripts/amalgm-mcp/tests/entity.rig.js +20 -6
  38. package/runtime/scripts/amalgm-mcp/tests/fake-watch.js +8 -0
  39. package/runtime/scripts/amalgm-mcp/tests/observer.rig.js +8 -1
  40. package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +630 -102
  41. package/runtime/scripts/amalgm-mcp/tests/registration.test.js +45 -10
  42. package/runtime/scripts/amalgm-mcp/tests/workspace-materialize.test.js +54 -0
  43. package/runtime/scripts/amalgm-mcp/workspace/materialize.js +292 -0
  44. package/runtime/scripts/chat-core/contract.js +21 -3
  45. package/runtime/scripts/chat-core/engine.js +8 -0
  46. package/runtime/scripts/chat-core/server.js +11 -1
  47. package/runtime/scripts/chat-core/tests/workspace-preflight.test.js +116 -0
  48. package/runtime/scripts/chat-core/workspace-preflight.js +89 -0
  49. package/runtime/scripts/local-gateway.js +0 -1
@@ -90,23 +90,64 @@ CREATE TABLE IF NOT EXISTS entity_cloud_mutations (
90
90
  );
91
91
  CREATE INDEX IF NOT EXISTS entity_cloud_mutations_pending
92
92
  ON entity_cloud_mutations (resource_id, mutation_state, sequence);
93
+ CREATE TABLE IF NOT EXISTS entity_cloud_catalog_state (
94
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
95
+ resource_id TEXT NOT NULL,
96
+ authority_epoch INTEGER NOT NULL CHECK (authority_epoch >= 1),
97
+ snapshot_version INTEGER NOT NULL CHECK (snapshot_version >= 0),
98
+ updated_at TEXT NOT NULL
99
+ );
100
+ CREATE TABLE IF NOT EXISTS entity_cloud_catalog (
101
+ resource_id TEXT NOT NULL,
102
+ entity_uuid TEXT NOT NULL,
103
+ record_json TEXT NOT NULL,
104
+ snapshot_epoch INTEGER NOT NULL CHECK (snapshot_epoch >= 1),
105
+ snapshot_version INTEGER NOT NULL CHECK (snapshot_version >= 0),
106
+ updated_at TEXT NOT NULL,
107
+ PRIMARY KEY (resource_id, entity_uuid)
108
+ );
109
+ CREATE INDEX IF NOT EXISTS entity_cloud_catalog_snapshot
110
+ ON entity_cloud_catalog (resource_id, snapshot_epoch, snapshot_version);
111
+ CREATE TABLE IF NOT EXISTS entity_materializations (
112
+ job_id TEXT PRIMARY KEY,
113
+ resource_id TEXT NOT NULL,
114
+ entity_uuid TEXT NOT NULL,
115
+ target_path TEXT NOT NULL,
116
+ authority_epoch INTEGER NOT NULL CHECK (authority_epoch >= 1),
117
+ head_version INTEGER NOT NULL CHECK (head_version >= 0),
118
+ state TEXT NOT NULL CHECK (state IN ('waiting_for_content', 'materializing', 'complete', 'failed')),
119
+ error TEXT,
120
+ created_at TEXT NOT NULL,
121
+ updated_at TEXT NOT NULL
122
+ );
123
+ CREATE INDEX IF NOT EXISTS entity_materializations_pending
124
+ ON entity_materializations (resource_id, state, created_at);
93
125
  `;
94
126
 
95
127
  const CORE_TABLES = ['registry_meta', 'entities', 'channel_bindings', 'contested_ground'];
96
128
  const REPO_STATE_TABLES = [...CORE_TABLES, 'repo_states'];
97
- const TABLES = [...REPO_STATE_TABLES, 'entity_cloud_replica', 'entity_cloud_mutations'];
129
+ const CLOUD_TABLES = ['entity_cloud_replica', 'entity_cloud_mutations'];
130
+ const MATERIALIZATION_TABLES = ['entity_cloud_catalog_state', 'entity_cloud_catalog', 'entity_materializations'];
131
+ const TABLES = [...REPO_STATE_TABLES, ...CLOUD_TABLES, ...MATERIALIZATION_TABLES];
98
132
 
99
133
  // The schema generation this code speaks, stamped as PRAGMA user_version at
100
134
  // creation — completeness is judged against a declared shape, never
101
135
  // guessed. Generation 3 adds the current Card + Checkpoint document for
102
136
  // each repo.git entity. Generation 4 adds the entity-cloud replica and its
103
137
  // durable outbox to that SAME identity database: a local entity mutation and
104
- // the cloud delivery intent must commit together. Generations 2 and 3 are
138
+ // the cloud delivery intent must commit together. Generation 5 adds the
139
+ // cloud-only catalog and durable materialization jobs: remote graph facts may
140
+ // be known locally before any observer is bound, so they cannot masquerade
141
+ // as local registered ground. Generation 6 records the one snapshot version
142
+ // that populated that catalog; a later mutation tail cannot pose as a fresh
143
+ // active head for `amalgm add`. Generations 2 and 3 are
105
144
  // additive predecessors: migrations only add tables beside existing identity
106
145
  // records, never rewrite UUIDs or evidence bindings. The per-tree era never
107
146
  // stamped a version and reads 0 — an older generation, refused distinctly
108
147
  // from loss until an explicit migration exists.
109
- const GENERATION = 4;
148
+ const GENERATION = 6;
149
+ const MATERIALIZATION_GENERATION = 5;
150
+ const CLOUD_GENERATION = 4;
110
151
  const REPO_STATE_GENERATION = 3;
111
152
  const PREVIOUS_GENERATION = 2;
112
153
 
@@ -132,15 +173,25 @@ function inspect(db) {
132
173
 
133
174
  function isMigratableGenerationShape(db, inspection) {
134
175
  const generation = db.pragma('user_version', { simple: true });
176
+ if (generation === MATERIALIZATION_GENERATION) {
177
+ return inspection.missing.length === 1
178
+ && inspection.missing[0] === 'entity_cloud_catalog_state'
179
+ && TABLES.filter((name) => name !== 'entity_cloud_catalog_state').every((name) => !inspection.missing.includes(name));
180
+ }
181
+ if (generation === CLOUD_GENERATION) {
182
+ return inspection.missing.length === MATERIALIZATION_TABLES.length
183
+ && MATERIALIZATION_TABLES.every((name) => inspection.missing.includes(name))
184
+ && [...REPO_STATE_TABLES, ...CLOUD_TABLES].every((name) => !inspection.missing.includes(name));
185
+ }
135
186
  if (generation === PREVIOUS_GENERATION) {
136
- return inspection.missing.length === 3
137
- && ['repo_states', 'entity_cloud_replica', 'entity_cloud_mutations']
187
+ return inspection.missing.length === ['repo_states', ...CLOUD_TABLES, ...MATERIALIZATION_TABLES].length
188
+ && ['repo_states', ...CLOUD_TABLES, ...MATERIALIZATION_TABLES]
138
189
  .every((name) => inspection.missing.includes(name))
139
190
  && CORE_TABLES.every((name) => !inspection.missing.includes(name));
140
191
  }
141
192
  return generation === REPO_STATE_GENERATION
142
- && inspection.missing.length === 2
143
- && ['entity_cloud_replica', 'entity_cloud_mutations']
193
+ && inspection.missing.length === [...CLOUD_TABLES, ...MATERIALIZATION_TABLES].length
194
+ && [...CLOUD_TABLES, ...MATERIALIZATION_TABLES]
144
195
  .every((name) => inspection.missing.includes(name))
145
196
  && REPO_STATE_TABLES.every((name) => !inspection.missing.includes(name));
146
197
  }
@@ -31,6 +31,14 @@ async function handleAutomationRoutes(ctx) {
31
31
  await automationsRest.handleRuns(await ctx.readJsonBody(), ctx.sendJson);
32
32
  return true;
33
33
  }
34
+ if (ctx.pathname === '/automations/deliveries' && ctx.method === 'POST') {
35
+ await automationsRest.handleDeliveries(await ctx.readJsonBody(), ctx.sendJson);
36
+ return true;
37
+ }
38
+ if (ctx.pathname === '/automations/deliveries/retry' && ctx.method === 'POST') {
39
+ await automationsRest.handleDeliveryRetry(await ctx.readJsonBody(), ctx.sendJson);
40
+ return true;
41
+ }
34
42
  if (ctx.pathname === '/automations/validate' && ctx.method === 'POST') {
35
43
  await automationsRest.handleValidate(await ctx.readJsonBody(), ctx.sendJson);
36
44
  return true;
@@ -211,6 +211,37 @@ async function handleEntityRoutes(ctx) {
211
211
  return true;
212
212
  }
213
213
 
214
+ // Cloud-to-local content is claimed by the existing realtime tunnel. A
215
+ // materialization job never opens R2 itself; it asks the authenticated
216
+ // gateway for the exact immutable manifest/chunk named by its pinned
217
+ // snapshot, then retries safely until its local content cache verifies it.
218
+ if (ctx.pathname === '/entities/cloud/content/downloads/claim' && ctx.method === 'POST') {
219
+ const service = registrationService();
220
+ service.advanceMaterializations();
221
+ ctx.sendJson(200, { download: service.entityCloud.claimDownload() });
222
+ return true;
223
+ }
224
+
225
+ if (ctx.pathname === '/entities/cloud/content/downloads/receive' && ctx.method === 'POST') {
226
+ const body = await ctx.readJsonBody();
227
+ const service = registrationService();
228
+ try {
229
+ const received = service.entityCloud.receiveDownload({
230
+ resourceId: body?.resourceId,
231
+ contentHash: body?.contentHash,
232
+ kind: body?.kind,
233
+ partIndex: body?.partIndex,
234
+ sha256: body?.sha256,
235
+ dataBase64: body?.dataBase64,
236
+ });
237
+ service.advanceMaterializations();
238
+ ctx.sendJson(200, { ...received });
239
+ } catch (error) {
240
+ ctx.sendJson(400, { error: error.message });
241
+ }
242
+ return true;
243
+ }
244
+
214
245
  if (ctx.pathname === '/entities/cloud/mutations/ack' && ctx.method === 'POST') {
215
246
  const body = await ctx.readJsonBody();
216
247
  try {
@@ -257,6 +288,31 @@ async function handleEntityRoutes(ctx) {
257
288
  return true;
258
289
  }
259
290
 
291
+ if (ctx.pathname === '/entities/add' && ctx.method === 'POST') {
292
+ const body = await ctx.readJsonBody();
293
+ const service = registrationService();
294
+ try {
295
+ const job = await service.add({ uuid: body?.uuid, targetPath: body?.path });
296
+ ctx.sendJson(job.state === 'complete' ? 200 : 202, job);
297
+ } catch (error) {
298
+ ctx.sendJson(409, { error: error.message });
299
+ }
300
+ return true;
301
+ }
302
+
303
+ const materializationMatch = /^\/entities\/add\/(mat_[A-Za-z0-9_-]+)$/.exec(ctx.pathname);
304
+ if (materializationMatch && ctx.method === 'GET') {
305
+ const service = registrationService();
306
+ service.advanceMaterializations();
307
+ const job = service.entityCloud.materializationStatus(materializationMatch[1]);
308
+ if (!job) {
309
+ ctx.sendJson(404, { error: 'cloud materialization does not exist' });
310
+ } else {
311
+ ctx.sendJson(200, job);
312
+ }
313
+ return true;
314
+ }
315
+
260
316
  if (ctx.pathname !== '/entities/register' || ctx.method !== 'POST') return false;
261
317
 
262
318
  const body = await ctx.readJsonBody();
@@ -87,6 +87,20 @@ async function handleWorkspaceRoutes(ctx) {
87
87
  await workspaceRest.handleCloudWorkspaceAttachment(await ctx.readJsonBody(), ctx.sendJson);
88
88
  return true;
89
89
  }
90
+ if (ctx.pathname === '/workspace/materialize' && ctx.method === 'POST') {
91
+ try {
92
+ const result = await require('../../workspace/materialize').materializeWorkspace(
93
+ await ctx.readJsonBody(),
94
+ );
95
+ ctx.sendJson(200, result);
96
+ } catch (error) {
97
+ ctx.sendJson(Number(error?.statusCode) || 500, {
98
+ error: error instanceof Error ? error.message : 'Workspace materialization failed',
99
+ code: error?.code || 'workspace_materialization_failed',
100
+ });
101
+ }
102
+ return true;
103
+ }
90
104
  if (ctx.pathname === '/workspace/access' && ctx.method === 'GET') {
91
105
  workspaceRest.handleAccessRead(ctx.getQuery(), ctx.sendJson);
92
106
  return true;
@@ -231,7 +231,7 @@ test('a membership that cannot be read completely is unsettled — never quietly
231
231
  });
232
232
 
233
233
  test('STITCH: production detection and the adapter speak one truth — no parallel detector', () => {
234
- // The observer's content decision (safeHash) now reads through the
234
+ // The observer's content decision (safeContent) now reads through the
235
235
  // adapter socket. Prove agreement on real ground: the hash the
236
236
  // production observer records for a file is byte-identical to the
237
237
  // truth the adapter answers for the same ground.
@@ -42,7 +42,7 @@ async function live(lay, run) {
42
42
  try {
43
43
  const dir = makeDir('live');
44
44
  lay(dir);
45
- const { observer, events } = makeObserver({ settleMs: 25 });
45
+ const { observer, events } = makeObserver({ settleMs: 25, prove: fake.prove });
46
46
  const root = observer.enrollRoot(dir);
47
47
  observer.start();
48
48
  try {
@@ -174,7 +174,7 @@ test('entrance — the single-file root\'s parent doorbell: the parent hears wha
174
174
  const holder = makeDir('single');
175
175
  const file = path.join(holder, 'solo.md');
176
176
  fs.writeFileSync(file, 'v1');
177
- const { observer, events } = makeObserver({ settleMs: 25 });
177
+ const { observer, events } = makeObserver({ settleMs: 25, prove: fake.prove });
178
178
  const root = observer.enrollRoot(file);
179
179
  assert.equal(root.kind, 'file', 'the smallest root: one channel');
180
180
  observer.start();
@@ -1,5 +1,5 @@
1
- <p style="margin:0;line-height:1.75;color:#ffffff">Inline file citation: <span title="Project notes" class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:middle"><img src="cid:amalgm-icon-path-markdown-a8a29e" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>notes.md</span> mid-sentence.</p>
2
- <div style="margin:15px 0 0"><p style="margin:0;line-height:1.75;color:#ffffff">Every icon kind: <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:middle"><img src="cid:amalgm-icon-react" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>app.tsx</span> letter-react, <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:middle"><img src="cid:amalgm-icon-letter-ts-3178c6-ffffff" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>main.ts</span> letter, <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:middle"><img src="cid:amalgm-icon-path-markdown-a8a29e" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>readme.md</span> path, <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:middle"><img src="cid:amalgm-icon-lucide-braces-facc15" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>data.json</span> lucide, <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:middle"><img src="cid:amalgm-icon-lucide-filetext-f87171" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>report.pdf</span> lucide-color.</p></div>
3
- <div style="margin:15px 0 0"><p style="margin:0;line-height:1.75;color:#ffffff">Special filenames: <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:middle"><img src="cid:amalgm-icon-lucide-package-facc15" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>package.json</span> and <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:middle"><img src="cid:amalgm-icon-lucide-lock-facc15" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>.env.local</span> and <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:middle"><img src="cid:amalgm-icon-lucide-package-38bdf8" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>Dockerfile</span>.</p></div>
4
- <div style="margin:15px 0 0"><p style="margin:0;line-height:1.75;color:#ffffff">Folder: <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:middle"><img src="cid:amalgm-icon-lucide-folder-d6d3d1" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>src</span>, skill: <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:middle"><img src="cid:amalgm-icon-lucide-box-d6d3d1" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>pdf-tools</span>, agent: <span title="Research agent" class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:middle"><img src="cid:amalgm-icon-lucide-atsign-ffffff" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>@researcher</span>.</p></div>
5
- <div style="margin:15px 0 0"><p style="margin:0;line-height:1.75;color:#ffffff">Unknown extension falls back: <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:middle"><img src="cid:amalgm-icon-lucide-file-a8a29e" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>blob.xyz</span>.</p></div>
1
+ <p style="margin:0;line-height:1.75;color:#ffffff">Inline file citation: <span title="Project notes" class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="cid:amalgm-icon-path-markdown-a8a29e" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>notes.md</span> mid-sentence.</p>
2
+ <div style="margin:15px 0 0"><p style="margin:0;line-height:1.75;color:#ffffff">Every icon kind: <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="cid:amalgm-icon-react" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>app.tsx</span> letter-react, <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="cid:amalgm-icon-letter-ts-3178c6-ffffff" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>main.ts</span> letter, <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="cid:amalgm-icon-path-markdown-a8a29e" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>readme.md</span> path, <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="cid:amalgm-icon-lucide-braces-facc15" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>data.json</span> lucide, <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="cid:amalgm-icon-lucide-filetext-f87171" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>report.pdf</span> lucide-color.</p></div>
3
+ <div style="margin:15px 0 0"><p style="margin:0;line-height:1.75;color:#ffffff">Special filenames: <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="cid:amalgm-icon-lucide-package-facc15" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>package.json</span> and <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="cid:amalgm-icon-lucide-lock-facc15" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>.env.local</span> and <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="cid:amalgm-icon-lucide-package-38bdf8" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>Dockerfile</span>.</p></div>
4
+ <div style="margin:15px 0 0"><p style="margin:0;line-height:1.75;color:#ffffff">Folder: <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="cid:amalgm-icon-lucide-folder-d6d3d1" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>src</span>, skill: <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="cid:amalgm-icon-lucide-box-d6d3d1" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>pdf-tools</span>, agent: <span title="Research agent" class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="cid:amalgm-icon-lucide-atsign-ffffff" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>@researcher</span>.</p></div>
5
+ <div style="margin:15px 0 0"><p style="margin:0;line-height:1.75;color:#ffffff">Unknown extension falls back: <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="cid:amalgm-icon-lucide-file-a8a29e" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>blob.xyz</span>.</p></div>
@@ -1,3 +1,3 @@
1
- <p style="margin:0;line-height:1.75;color:#ffffff">Web citation: read <a href="https://www.anthropic.com/news" target="_blank" rel="noreferrer" class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:middle"><img src="https://www.google.com/s2/favicons?domain=www.anthropic.com&amp;sz=32" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;border-radius:3px;vertical-align:-1px"></span>Anthropic</a> for details.</p>
2
- <div style="margin:15px 0 0"><p style="margin:0;line-height:1.75;color:#ffffff">Bare-ish label: <a href="https://example.com/" target="_blank" rel="noreferrer" class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:middle"><img src="https://www.google.com/s2/favicons?domain=example.com&amp;sz=32" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;border-radius:3px;vertical-align:-1px"></span>example.com</a> and a deep link <a href="https://amalgm.ai/tab?kind=tasks&amp;id=run-1" target="_blank" rel="noreferrer" class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:middle"><img src="https://www.google.com/s2/favicons?domain=amalgm.ai&amp;sz=32" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;border-radius:3px;vertical-align:-1px"></span>View run</a>.</p></div>
3
- <div style="margin:15px 0 0"><p style="margin:0;line-height:1.75;color:#ffffff">A link with <strong style="color:#ffffff;font-weight:600">bold</strong> around it: <strong style="color:#ffffff;font-weight:600">see <a href="https://docs.example.dev/guide" target="_blank" rel="noreferrer" class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:middle"><img src="https://www.google.com/s2/favicons?domain=docs.example.dev&amp;sz=32" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;border-radius:3px;vertical-align:-1px"></span>docs</a></strong> now.</p></div>
1
+ <p style="margin:0;line-height:1.75;color:#ffffff">Web citation: read <a href="https://www.anthropic.com/news" target="_blank" rel="noreferrer" class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="https://www.google.com/s2/favicons?domain=www.anthropic.com&amp;sz=32" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;border-radius:3px;vertical-align:-1px"></span>Anthropic</a> for details.</p>
2
+ <div style="margin:15px 0 0"><p style="margin:0;line-height:1.75;color:#ffffff">Bare-ish label: <a href="https://example.com/" target="_blank" rel="noreferrer" class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="https://www.google.com/s2/favicons?domain=example.com&amp;sz=32" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;border-radius:3px;vertical-align:-1px"></span>example.com</a> and a deep link <a href="https://amalgm.ai/tab?kind=tasks&amp;id=run-1" target="_blank" rel="noreferrer" class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="https://www.google.com/s2/favicons?domain=amalgm.ai&amp;sz=32" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;border-radius:3px;vertical-align:-1px"></span>View run</a>.</p></div>
3
+ <div style="margin:15px 0 0"><p style="margin:0;line-height:1.75;color:#ffffff">A link with <strong style="color:#ffffff;font-weight:600">bold</strong> around it: <strong style="color:#ffffff;font-weight:600">see <a href="https://docs.example.dev/guide" target="_blank" rel="noreferrer" class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="https://www.google.com/s2/favicons?domain=docs.example.dev&amp;sz=32" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;border-radius:3px;vertical-align:-1px"></span>docs</a></strong> now.</p></div>
@@ -3,7 +3,7 @@
3
3
  <body style="margin:0;padding:0;background:#000000;font-family:"SF Pro Text", "SF Pro Display", -apple-system, BlinkMacSystemFont, system-ui, sans-serif">
4
4
  <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#000000"><tr><td align="center" style="padding:0">
5
5
  <table role="presentation" width="520" cellpadding="0" cellspacing="0" style="max-width:520px;width:100%"><tr><td style="padding:40px 28px 48px">
6
- <div style="font-size:14px;color:#ffffff"><p style="margin:0;line-height:1.75;color:#ffffff">Run finished. See <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:middle"><img src="cid:amalgm-icon-path-markdown-a8a29e" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>notes.md</span>.</p></div>
6
+ <div style="font-size:14px;color:#ffffff"><p style="margin:0;line-height:1.75;color:#ffffff">Run finished. See <span class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="cid:amalgm-icon-path-markdown-a8a29e" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;vertical-align:-1px"></span>notes.md</span>.</p></div>
7
7
  <p style="margin:24px 0 0"><a href="https://amalgm.ai/tab?kind=tasks&amp;id=run-1" style="color:#60a5fa;text-decoration:none;font-size:14px">View details &rarr;</a></p>
8
8
  <p style="margin:48px 0 0;font-size:12px;color:#ffffff">amalgm.ai</p>
9
9
  </td></tr></table>
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
3
+ <body style="margin:0;padding:0;background:#000000;font-family:'SF Pro Text', 'SF Pro Display', -apple-system, BlinkMacSystemFont, system-ui, sans-serif">
4
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#000000"><tr><td align="center" style="padding:0">
5
+ <table role="presentation" width="520" cellpadding="0" cellspacing="0" style="max-width:520px;width:100%;background:#000000;border:1px solid #292524;border-radius:8px;border-collapse:separate;overflow:hidden"><tr><td style="padding:40px 28px 48px">
6
+ <div style="font-size:14px;color:#ffffff"><h2 style="margin:0;font-size:16px;font-weight:400;color:#ffffff;line-height:1.35"><strong style="font-weight:650">Aayush Trivedi</strong> shared a file with you</h2>
7
+ <div style="margin:15px 0 0"><a href="https://amalgm.ai/s/s1i_abcdefghijklmnopqrstuvwxyzABCDEFGH123456789" target="_blank" style="text-decoration:none;display:block;background:#000000;border:1px solid #1c1917;border-radius:12px;padding:0;margin:2px 0"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse"><tr><td style="padding:8px 0 8px 12px;vertical-align:middle;line-height:0;width:14px"><img src="cid:amalgm-icon-lucide-filetext-f87171" alt="" width="14" height="14" style="display:inline-block;width:14px;height:14px;border:0"></td><td style="padding:8px 12px 8px 10px;vertical-align:middle"><span style="font-size:14px;line-height:18px;color:#ffffff;font-weight:650;white-space:nowrap">Q3 launch brief.pdf</span></td><td align="right" style="padding:8px 12px 8px 0;vertical-align:middle;line-height:0;width:14px"><img src="cid:amalgm-icon-lucide-externallink-ffffff" alt="" width="14" height="14" style="display:inline-block;width:14px;height:14px;border:0"></td></tr></table></a></div>
8
+ <div style="margin:15px 0 0"><p style="margin:0;line-height:1.75;color:#ffffff">You can <strong style="color:#ffffff;font-weight:600">edit</strong> this file.</p></div>
9
+ <div style="margin:15px 0 0"><blockquote style="margin:0;line-height:1.75;font-style:italic;padding:0 0 0 16px;border-left:3px solid #292524;color:#d6d3d1">sam@acme.com &mdash; sign in with this address to accept.</blockquote></div></div>
10
+ <p style="margin:48px 0 0;font-size:12px;color:#ffffff">amalgm.ai</p>
11
+ </td></tr></table>
12
+ </td></tr></table>
13
+ </body></html>
@@ -2,7 +2,7 @@
2
2
  <div style="margin:15px 0 0"><h2 style="margin:0;font-size:16px;font-weight:650;color:#ffffff;line-height:1.35">Heading two</h2></div>
3
3
  <div style="margin:15px 0 0"><h3 style="margin:0;font-size:15px;font-weight:650;color:#ffffff;line-height:1.35">Heading three</h3></div>
4
4
  <div style="margin:15px 0 0"><p style="margin:0;line-height:1.75;color:#ffffff">Body text with <strong style="color:#ffffff;font-weight:600">bold</strong>, <em>italic</em>, <strong style="color:#ffffff;font-weight:600"><em>bold italic</em></strong>, and <code style="background:#0a0a0a;border:1px solid #1c1917;padding:1.875px 7.5px;border-radius:5.625px;font-size:12px;line-height:1.2;font-family:'JetBrains Mono', 'SFMono-Regular', Menlo, monospace;color:#f5f5f4">inline code</code>.</p></div>
5
- <div style="margin:15px 0 0"><ul style="margin:0;padding-left:24px;color:#ffffff"><li style="margin:4px 0;padding:3.75px 0;line-height:1.5;color:#ffffff">bullet one</li><li style="margin:4px 0;padding:3.75px 0;line-height:1.5;color:#ffffff">bullet two with <a href="https://example.com/a" target="_blank" rel="noreferrer" class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:middle"><img src="https://www.google.com/s2/favicons?domain=example.com&amp;sz=32" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;border-radius:3px;vertical-align:-1px"></span>a link</a></li></ul></div>
5
+ <div style="margin:15px 0 0"><ul style="margin:0;padding-left:24px;color:#ffffff"><li style="margin:4px 0;padding:3.75px 0;line-height:1.5;color:#ffffff">bullet one</li><li style="margin:4px 0;padding:3.75px 0;line-height:1.5;color:#ffffff">bullet two with <a href="https://example.com/a" target="_blank" rel="noreferrer" class="render-citation" style="display:inline;vertical-align:baseline;font:inherit;font-size:inherit;line-height:inherit;color:#60a5fa;text-decoration:none;white-space:nowrap;padding:0;background:transparent;border:none;"><span style="display:inline-block;width:12px;height:12px;font-size:12px;line-height:12px;margin-right:0.3em;vertical-align:-0.1em"><img src="https://www.google.com/s2/favicons?domain=example.com&amp;sz=32" alt="" width="12" height="12" style="display:inline-block;width:12px;height:12px;border:0;border-radius:3px;vertical-align:-1px"></span>a link</a></li></ul></div>
6
6
  <div style="margin:15px 0 0"><ol style="margin:0;padding-left:24px;color:#ffffff"><li style="margin:4px 0;padding:3.75px 0;line-height:1.5;color:#ffffff">ordered one</li><li style="margin:4px 0;padding:3.75px 0;line-height:1.5;color:#ffffff">ordered two</li></ol></div>
7
7
  <div style="margin:15px 0 0"><blockquote style="margin:0;line-height:1.75;font-style:italic;padding:0 0 0 16px;border-left:3px solid #292524;color:#d6d3d1">A quote line continues here.</blockquote></div>
8
8
  <div style="margin:15px 0 0"><div style="border:1px solid #292524;border-radius:12px;overflow:hidden;margin:0"><table style="width:100%;border-collapse:collapse"><thead><tr><th style="padding:5.625px 11.25px;text-align:left;vertical-align:top;font-size:14px;line-height:1.5;border-right:1px solid #292524;border-bottom:1px solid #292524;background:#0c0b0a;font-weight:500;color:#ffffff">Metric</th><th style="padding:5.625px 11.25px;text-align:left;vertical-align:top;font-size:14px;line-height:1.5;border-bottom:1px solid #292524;background:#0c0b0a;font-weight:500;color:#ffffff">Value</th></tr></thead><tbody><tr><td style="padding:5.625px 11.25px;text-align:left;vertical-align:top;font-size:14px;line-height:1.5;border-right:1px solid #292524;border-bottom:1px solid #292524;color:#ffffff">Sessions</td><td style="padding:5.625px 11.25px;text-align:left;vertical-align:top;font-size:14px;line-height:1.5;border-bottom:1px solid #292524;color:#ffffff">42</td></tr><tr><td style="padding:5.625px 11.25px;text-align:left;vertical-align:top;font-size:14px;line-height:1.5;border-right:1px solid #292524;color:#ffffff">Errors</td><td style="padding:5.625px 11.25px;text-align:left;vertical-align:top;font-size:14px;line-height:1.5;color:#ffffff">0</td></tr></tbody></table></div></div>
@@ -17,7 +17,11 @@ const fs = require('node:fs');
17
17
  const path = require('node:path');
18
18
  const test = require('node:test');
19
19
 
20
- const { markdownToEmailHtml, formatNotificationEmail } = require('../lib/email-md');
20
+ const {
21
+ markdownToEmailHtml,
22
+ formatNotificationEmail,
23
+ formatShareInviteEmail,
24
+ } = require('../lib/email-md');
21
25
  const { prepareEmailIconAttachments } = require('../lib/email-icons');
22
26
 
23
27
  const FIXTURE_DIR = path.join(__dirname, 'email-render-fixtures');
@@ -56,6 +60,18 @@ test('email render golden: notification shell', () => {
56
60
  assertGolden('notification-shell.html', `${html}\n`);
57
61
  });
58
62
 
63
+ test('email render golden: share invite shell', () => {
64
+ const html = formatShareInviteEmail({
65
+ sharerName: 'Aayush Trivedi',
66
+ recipientEmail: 'sam@acme.com',
67
+ entityName: 'Q3 launch brief.pdf',
68
+ entityType: 'file',
69
+ inviteUrl: 'https://amalgm.ai/s/s1i_abcdefghijklmnopqrstuvwxyzABCDEFGH123456789',
70
+ access: 'edit',
71
+ });
72
+ assertGolden('share-invite.html', `${html}\n`);
73
+ });
74
+
59
75
  test('every cid icon referenced by goldens has a PNG asset', () => {
60
76
  const htmls = fs
61
77
  .readdirSync(GOLDEN_DIR)
@@ -0,0 +1,189 @@
1
+ 'use strict';
2
+
3
+ const test = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+
6
+ const Database = require('better-sqlite3');
7
+ const { createRegistry } = require('../registry');
8
+ const { createEntityCloud, parseSnapshot, snapshotFromRecords } = require('../registration/entity-cloud');
9
+ const { createEntityContent } = require('../registration/entity-content');
10
+ const fs = require('fs');
11
+ const os = require('os');
12
+ const path = require('path');
13
+
14
+ test('the entity cloud snapshot establishes identity, then each later entity commit has one durable ordered outbox row', () => {
15
+ const db = new Database(':memory:');
16
+ const cloud = createEntityCloud({ db, userId: 'user-1', deviceId: 'device-1' });
17
+ const registry = createRegistry({
18
+ db,
19
+ ephemeral: true,
20
+ emit: () => {},
21
+ journal: cloud.journal,
22
+ });
23
+
24
+ const root = registry.enrollRoot({ type: 'workspace', name: 'ground' });
25
+ assert.equal(db.prepare('SELECT COUNT(*) AS count FROM entity_cloud_mutations').get().count, 0,
26
+ 'initial bootstrap identity is carried by the first immutable snapshot, not an unscoped tail');
27
+
28
+ const bootstrap = cloud.bootstrap(registry);
29
+ const snapshot = parseSnapshot(Buffer.from(bootstrap.snapshotBase64, 'base64'));
30
+ assert.deepEqual(snapshot.records.map((record) => record.uuid), [root.uuid]);
31
+ cloud.install({
32
+ registry,
33
+ resourceId: bootstrap.resourceId,
34
+ authorityEpoch: 1,
35
+ headVersion: 0,
36
+ snapshotBase64: bootstrap.snapshotBase64,
37
+ snapshotChecksum: bootstrap.snapshotChecksum,
38
+ });
39
+
40
+ const first = registry.create({ type: 'file.text', parentUUID: root.uuid, name: 'one.md', payloadVersion: 'head-one' });
41
+ const second = registry.create({ type: 'file.text', parentUUID: root.uuid, name: 'two.md', payloadVersion: 'head-two' });
42
+ assert.equal(db.prepare('SELECT COUNT(*) AS count FROM entity_cloud_mutations').get().count, 2);
43
+
44
+ const firstClaim = cloud.claim();
45
+ assert.equal(firstClaim.length, 1, 'one resource emits one operation at a time so base versions cannot race');
46
+ assert.equal(firstClaim[0].baseVersion, 0);
47
+ assert.equal(firstClaim[0].operation.record.uuid, first.uuid);
48
+ cloud.acknowledge({
49
+ resourceId: firstClaim[0].resourceId,
50
+ mutationId: firstClaim[0].mutationId,
51
+ version: 1,
52
+ committedAt: '2026-08-04T00:00:00.000Z',
53
+ });
54
+
55
+ const secondClaim = cloud.claim();
56
+ assert.equal(secondClaim.length, 1);
57
+ assert.equal(secondClaim[0].baseVersion, 1, 'the next head starts from the acknowledged cloud version');
58
+ assert.equal(secondClaim[0].operation.record.uuid, second.uuid);
59
+ });
60
+
61
+ test('a signed-in registration is refused until the cloud entity resource is active', () => {
62
+ const db = new Database(':memory:');
63
+ const cloud = createEntityCloud({ db, userId: 'user-1', deviceId: 'device-1' });
64
+ const registry = createRegistry({ db, ephemeral: true, emit: () => {}, journal: cloud.journal });
65
+ assert.throws(() => cloud.assertRegistrationReady(), /entity cloud registry is not ready/);
66
+ registry.enrollRoot({ type: 'workspace', name: 'bootstrap-only' });
67
+ const bootstrap = cloud.bootstrap(registry);
68
+ cloud.install({
69
+ registry,
70
+ resourceId: bootstrap.resourceId,
71
+ authorityEpoch: 1,
72
+ headVersion: 0,
73
+ snapshotBase64: bootstrap.snapshotBase64,
74
+ snapshotChecksum: bootstrap.snapshotChecksum,
75
+ });
76
+ assert.doesNotThrow(() => cloud.assertRegistrationReady());
77
+ });
78
+
79
+ test('a compacted remote snapshot enters the cloud catalog, not locally watched identity, and add claims its immutable content', () => {
80
+ const sourceDb = new Database(':memory:');
81
+ const sourceCloud = createEntityCloud({ db: sourceDb, userId: 'user-1', deviceId: 'source-device' });
82
+ const sourceRegistry = createRegistry({ db: sourceDb, ephemeral: true, emit: () => {}, journal: sourceCloud.journal });
83
+ const root = sourceRegistry.enrollRoot({ type: 'workspace', name: 'project' });
84
+ const contentHash = 'b'.repeat(64);
85
+ const file = sourceRegistry.create({
86
+ type: 'file.text', parentUUID: root.uuid, name: 'readme.md', payloadVersion: contentHash,
87
+ });
88
+ const snapshot = snapshotFromRecords(sourceRegistry.syncRecords());
89
+ const bytes = Buffer.from(JSON.stringify(snapshot), 'utf8');
90
+
91
+ const targetDir = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-entity-catalog-'));
92
+ const targetDb = new Database(':memory:');
93
+ const targetContent = createEntityContent({ dir: targetDir });
94
+ const targetCloud = createEntityCloud({
95
+ db: targetDb, userId: 'user-1', deviceId: 'target-device', content: targetContent,
96
+ });
97
+ const targetRegistry = createRegistry({ db: targetDb, ephemeral: true, emit: () => {}, journal: targetCloud.journal });
98
+ const resourceId = sourceCloud.identity().resourceId;
99
+ targetCloud.install({
100
+ registry: targetRegistry,
101
+ resourceId,
102
+ authorityEpoch: 3,
103
+ headVersion: 12,
104
+ snapshotBase64: bytes.toString('base64'),
105
+ snapshotChecksum: require('crypto').createHash('sha256').update(bytes).digest('hex'),
106
+ });
107
+
108
+ assert.deepEqual(targetRegistry.syncRecords(), [], 'a remote graph does not pretend to be watched local ground');
109
+ const job = targetCloud.startMaterialization({ uuid: root.uuid, targetPath: targetDir });
110
+ assert.equal(job.state, 'waiting_for_content');
111
+ assert.deepEqual(targetCloud.catalogTree({ resourceId, uuid: root.uuid }).map((record) => record.uuid), [root.uuid, file.uuid]);
112
+ assert.deepEqual(targetCloud.claimDownload(), {
113
+ resourceId,
114
+ contentHash,
115
+ kind: 'manifest',
116
+ partIndex: 0,
117
+ sha256: null,
118
+ });
119
+ fs.rmSync(targetDir, { recursive: true, force: true });
120
+ });
121
+
122
+ test('add accepts only a catalog installed by the active compacted snapshot, never a snapshot plus a mutation tail', () => {
123
+ const sourceDb = new Database(':memory:');
124
+ const sourceCloud = createEntityCloud({ db: sourceDb, userId: 'user-1', deviceId: 'source-device' });
125
+ const sourceRegistry = createRegistry({ db: sourceDb, ephemeral: true, emit: () => {}, journal: sourceCloud.journal });
126
+ const root = sourceRegistry.enrollRoot({ type: 'workspace', name: 'project' });
127
+ const file = sourceRegistry.create({
128
+ type: 'file.text', parentUUID: root.uuid, name: 'readme.md', payloadVersion: 'b'.repeat(64),
129
+ });
130
+ const initialSnapshot = snapshotFromRecords(sourceRegistry.syncRecords());
131
+ const initialBytes = Buffer.from(JSON.stringify(initialSnapshot), 'utf8');
132
+
133
+ const targetDir = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-active-head-'));
134
+ const targetDb = new Database(':memory:');
135
+ const targetCloud = createEntityCloud({ db: targetDb, userId: 'user-1', deviceId: 'target-device' });
136
+ const targetRegistry = createRegistry({ db: targetDb, ephemeral: true, emit: () => {}, journal: targetCloud.journal });
137
+ const resourceId = sourceCloud.identity().resourceId;
138
+ try {
139
+ targetCloud.install({
140
+ registry: targetRegistry,
141
+ resourceId,
142
+ authorityEpoch: 3,
143
+ headVersion: 12,
144
+ snapshotBase64: initialBytes.toString('base64'),
145
+ snapshotChecksum: require('crypto').createHash('sha256').update(initialBytes).digest('hex'),
146
+ });
147
+
148
+ const cloudFile = initialSnapshot.records.find((record) => record.uuid === file.uuid);
149
+ const changed = { ...cloudFile, name: 'renamed.md', version: 'c'.repeat(64) };
150
+ targetCloud.ingest({
151
+ registry: targetRegistry,
152
+ envelope: {
153
+ resourceId,
154
+ mutationId: 'ent_remote_change',
155
+ contract: 'amalgm-entity-registry@1',
156
+ schemaVersion: 1,
157
+ operationKind: 'entity.upsert',
158
+ operation: { kind: 'entity.upsert', record: changed },
159
+ },
160
+ version: 13,
161
+ committedAt: '2026-08-04T00:00:00.000Z',
162
+ });
163
+
164
+ assert.throws(
165
+ () => targetCloud.startMaterialization({ uuid: root.uuid, targetPath: targetDir }),
166
+ /catalog is not pinned to the active cloud head/,
167
+ 'the mutation updated live catalog facts, but not the snapshot barrier',
168
+ );
169
+
170
+ const cloudRoot = initialSnapshot.records.find((record) => record.uuid === root.uuid);
171
+ const compactedBytes = Buffer.from(JSON.stringify(snapshotFromRecords([cloudRoot, changed])), 'utf8');
172
+ targetCloud.install({
173
+ registry: targetRegistry,
174
+ resourceId,
175
+ authorityEpoch: 3,
176
+ headVersion: 13,
177
+ snapshotBase64: compactedBytes.toString('base64'),
178
+ snapshotChecksum: require('crypto').createHash('sha256').update(compactedBytes).digest('hex'),
179
+ });
180
+ assert.equal(
181
+ targetCloud.startMaterialization({ uuid: root.uuid, targetPath: targetDir }).headVersion,
182
+ 13,
183
+ 'the fresh compacted snapshot—not a history replay—makes add available again',
184
+ );
185
+ } finally {
186
+ sourceDb.close();
187
+ fs.rmSync(targetDir, { recursive: true, force: true });
188
+ }
189
+ });