amalgm 0.1.238 → 0.1.240

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 (86) hide show
  1. package/lib/shared-realtime-tunnel.js +22 -14
  2. package/package.json +1 -1
  3. package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +68 -20
  4. package/runtime/scripts/amalgm-mcp/project-context/store.js +87 -19
  5. package/runtime/scripts/amalgm-mcp/state/attachments.js +40 -12
  6. package/runtime/scripts/amalgm-mcp/state/db.js +216 -17
  7. package/runtime/scripts/amalgm-mcp/state/doc-disk.js +550 -0
  8. package/runtime/scripts/amalgm-mcp/state/docs.js +1101 -301
  9. package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +99 -0
  10. package/runtime/scripts/amalgm-mcp/state/mutations.js +507 -130
  11. package/runtime/scripts/amalgm-mcp/state/promotions.js +21 -1
  12. package/runtime/scripts/amalgm-mcp/state/replicas.js +134 -13
  13. package/runtime/scripts/amalgm-mcp/state/shared-rest.js +14 -8
  14. package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +193 -0
  15. package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +313 -94
  16. package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +1 -0
  17. package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +32 -17
  18. package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +84 -0
  19. package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +376 -13
  20. package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +309 -0
  21. package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +758 -30
  22. package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +72 -3
  23. package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +33 -6
  24. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +27 -0
  25. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +334 -191
  26. package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +24 -0
  27. package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +1999 -0
  28. package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +364 -0
  29. package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +81 -3
  30. package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +114 -8
  31. package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +185 -0
  32. package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +174 -269
  33. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +26 -0
  34. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +2060 -407
  35. package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +119 -0
  36. package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +1446 -0
  37. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +5319 -3
  38. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +191 -1
  39. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +3140 -17
  40. package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +135 -637
  41. package/runtime/scripts/amalgm-mcp/workspace/access-store.js +5 -1
  42. package/runtime/scripts/amalgm-mcp/workspace/card.js +7 -7
  43. package/runtime/scripts/amalgm-mcp/workspace/cards.js +36 -12
  44. package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +436 -173
  45. package/runtime/scripts/amalgm-mcp/workspace/content.js +32 -0
  46. package/runtime/scripts/amalgm-mcp/workspace/durable.js +45 -0
  47. package/runtime/scripts/amalgm-mcp/workspace/git.js +391 -37
  48. package/runtime/scripts/amalgm-mcp/workspace/intent.js +535 -0
  49. package/runtime/scripts/amalgm-mcp/workspace/merge.js +89 -6
  50. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +31 -0
  51. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
  52. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
  53. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +72 -0
  54. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +165 -0
  55. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +5 -0
  56. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +2 -0
  57. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +2201 -0
  58. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +19 -0
  59. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +18 -0
  60. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +18 -0
  61. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +33 -0
  62. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +47 -0
  63. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +43 -0
  64. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +114 -0
  65. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +84 -0
  66. package/runtime/scripts/amalgm-mcp/workspace/objects.js +119 -54
  67. package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +616 -168
  68. package/runtime/scripts/amalgm-mcp/workspace/provenance.js +240 -0
  69. package/runtime/scripts/amalgm-mcp/workspace/reducer.js +1979 -570
  70. package/runtime/scripts/amalgm-mcp/workspace/rest.js +49 -28
  71. package/runtime/scripts/amalgm-mcp/workspace/store.js +77 -4
  72. package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +240 -0
  73. package/runtime/scripts/amalgm-mcp/workspace/transition.js +1729 -192
  74. package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +100 -0
  75. package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +7 -1
  76. package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +73 -61
  77. package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +127 -0
  78. package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +12 -0
  79. package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +51 -6
  80. package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +125 -0
  81. package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +79 -8
  82. package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +76 -0
  83. package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +3480 -165
  84. package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +3766 -0
  85. package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +899 -71
  86. package/runtime/scripts/amalgm-mcp/workspace/wall.js +352 -497
@@ -16,6 +16,8 @@ const { cachedStatement, openLocalDb } = require('./db');
16
16
  const { insertStateEvent, publishStateEvent } = require('./events');
17
17
  const {
18
18
  diagnosticOperation,
19
+ mutationNeedsOfficialMaterialization,
20
+ mutationNeedsOrderedEcho,
19
21
  protectOperation,
20
22
  revealOperation,
21
23
  } = require('./mutation-contracts');
@@ -174,10 +176,12 @@ function normalizeJournalRow(row, options = {}) {
174
176
  state: row.mutation_state,
175
177
  localRevision: row.local_revision == null ? null : Number(row.local_revision),
176
178
  materializedAt: row.materialized_at || null,
179
+ officialProjectedAt: row.official_projected_at || null,
177
180
  eventSeq: row.event_seq == null ? null : Number(row.event_seq),
178
181
  attemptCount: Number(row.attempt_count || 0),
179
182
  nextAttemptAt: row.next_attempt_at || null,
180
183
  lastError: row.last_error || null,
184
+ failureCode: row.failure_code || null,
181
185
  cloudCommittedAt: row.cloud_committed_at || null,
182
186
  createdAt: row.created_at,
183
187
  updatedAt: row.updated_at,
@@ -224,66 +228,67 @@ function sameCloudEnvelope(existing, mutation) {
224
228
  && JSON.stringify(existingOperation) === mutation.operationJson;
225
229
  }
226
230
 
227
- function journalMutation(input, options = {}) {
228
- const mutation = normalizeMutationInput(input);
229
- const database = options.database || openLocalDb();
230
- const persist = typeof options.persist === 'function' ? options.persist : null;
231
-
232
- return database.transaction(() => {
233
- const existing = getMutationRow(database, mutation.channelId, mutation.mutationId);
234
- if (existing) {
235
- if (!sameMutation(existing, mutation)) {
236
- throw invalid(
237
- `mutationId ${mutation.mutationId} was reused with different content`,
238
- 409,
239
- );
240
- }
241
- return { mutation: normalizeJournalRow(existing), duplicate: true };
231
+ function journalNormalizedMutation(database, mutation, persist = null) {
232
+ const existing = getMutationRow(database, mutation.channelId, mutation.mutationId);
233
+ if (existing) {
234
+ if (!sameMutation(existing, mutation)) {
235
+ throw invalid(
236
+ `mutationId ${mutation.mutationId} was reused with different content`,
237
+ 409,
238
+ );
242
239
  }
240
+ return { mutation: normalizeJournalRow(existing), duplicate: true };
241
+ }
243
242
 
244
- if (persist) persist(database, mutation);
243
+ if (persist) persist(database, mutation);
244
+
245
+ const info = cachedStatement(database, `
246
+ INSERT INTO mutation_journal (
247
+ channel_id,
248
+ local_resource,
249
+ shared_resource_id,
250
+ mutation_id,
251
+ device_id,
252
+ authority_epoch,
253
+ base_cloud_version,
254
+ contract,
255
+ schema_version,
256
+ operation_kind,
257
+ operation_json,
258
+ mutation_origin,
259
+ mutation_state,
260
+ local_revision
261
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'local', 'saving-local', ?)
262
+ `).run(
263
+ mutation.channelId,
264
+ mutation.localResource,
265
+ mutation.sharedResourceId,
266
+ mutation.mutationId,
267
+ mutation.deviceId,
268
+ mutation.authorityEpoch,
269
+ mutation.baseCloudVersion,
270
+ mutation.contract,
271
+ mutation.schemaVersion,
272
+ mutation.operationKind,
273
+ JSON.stringify(protectOperation(mutation.contract, mutation.operation)),
274
+ mutation.localRevision,
275
+ );
245
276
 
246
- const info = cachedStatement(database, `
247
- INSERT INTO mutation_journal (
248
- channel_id,
249
- local_resource,
250
- shared_resource_id,
251
- mutation_id,
252
- device_id,
253
- authority_epoch,
254
- base_cloud_version,
255
- contract,
256
- schema_version,
257
- operation_kind,
258
- operation_json,
259
- mutation_origin,
260
- mutation_state,
261
- local_revision
262
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'local', 'saving-local', ?)
263
- `).run(
264
- mutation.channelId,
265
- mutation.localResource,
266
- mutation.sharedResourceId,
267
- mutation.mutationId,
268
- mutation.deviceId,
269
- mutation.authorityEpoch,
270
- mutation.baseCloudVersion,
271
- mutation.contract,
272
- mutation.schemaVersion,
273
- mutation.operationKind,
274
- JSON.stringify(protectOperation(mutation.contract, mutation.operation)),
275
- mutation.localRevision,
276
- );
277
+ const row = cachedStatement(database, 'SELECT * FROM mutation_journal WHERE journal_id = ?')
278
+ .get(Number(info.lastInsertRowid));
279
+ trace('journal', {
280
+ mutationId: mutation.mutationId,
281
+ channel: mutation.channelId,
282
+ shared: mutation.sharedResourceId,
283
+ });
284
+ return { mutation: normalizeJournalRow(row), duplicate: false };
285
+ }
277
286
 
278
- const row = cachedStatement(database, 'SELECT * FROM mutation_journal WHERE journal_id = ?')
279
- .get(Number(info.lastInsertRowid));
280
- trace('journal', {
281
- mutationId: mutation.mutationId,
282
- channel: mutation.channelId,
283
- shared: mutation.sharedResourceId,
284
- });
285
- return { mutation: normalizeJournalRow(row), duplicate: false };
286
- })();
287
+ function journalMutation(input, options = {}) {
288
+ const mutation = normalizeMutationInput(input);
289
+ const database = options.database || openLocalDb();
290
+ const persist = typeof options.persist === 'function' ? options.persist : null;
291
+ return database.transaction(() => journalNormalizedMutation(database, mutation, persist))();
287
292
  }
288
293
 
289
294
  function recordMaterializationError(database, channelId, mutationId, error) {
@@ -307,7 +312,7 @@ function advanceReplicaCursor(database, sharedResourceId) {
307
312
  WHERE shared_resource_id = ?
308
313
  AND official_cloud_version = ?
309
314
  AND mutation_state = 'synced'
310
- AND materialized_at IS NOT NULL
315
+ AND official_projected_at IS NOT NULL
311
316
  LIMIT 1
312
317
  `);
313
318
  const update = cachedStatement(database, `
@@ -332,61 +337,168 @@ function advanceReplicaCursor(database, sharedResourceId) {
332
337
  return replica ? Number(replica.applied_version) : null;
333
338
  }
334
339
 
340
+ function completeMaterializedInTransaction(database, channelId, mutationId, eventInput) {
341
+ const row = getMutationRow(database, channelId, mutationId);
342
+ if (!row) throw invalid(`unknown mutation ${mutationId}`, 404);
343
+ if (row.event_seq != null) {
344
+ return { mutation: normalizeJournalRow(row), duplicate: true, event: null };
345
+ }
346
+ if (row.mutation_state !== 'saving-local') {
347
+ throw invalid(`mutation ${mutationId} cannot materialize from ${row.mutation_state}`, 409);
348
+ }
349
+ if (!eventInput || eventInput.resource !== row.local_resource) {
350
+ throw invalid('materialized event resource must match the journal localResource');
351
+ }
352
+
353
+ const event = insertStateEvent(database, {
354
+ ...eventInput,
355
+ mutationId: row.mutation_id,
356
+ sharedResourceId: row.shared_resource_id,
357
+ });
358
+ const now = new Date().toISOString();
359
+ const nextState = row.mutation_origin === 'cloud' ? 'synced' : 'saved-local';
360
+ const officialProjectedAt = row.mutation_origin === 'cloud' ? now : null;
361
+ cachedStatement(database, `
362
+ UPDATE mutation_journal
363
+ SET mutation_state = ?,
364
+ materialized_at = ?,
365
+ official_projected_at = ?,
366
+ event_seq = ?,
367
+ last_error = NULL,
368
+ failure_code = NULL,
369
+ updated_at = ?
370
+ WHERE journal_id = ?
371
+ `).run(nextState, now, officialProjectedAt, event.seq, now, row.journal_id);
372
+
373
+ if (row.mutation_origin === 'cloud') {
374
+ advanceReplicaCursor(database, row.shared_resource_id);
375
+ }
376
+
377
+ trace('materialized', {
378
+ mutationId: row.mutation_id,
379
+ origin: row.mutation_origin,
380
+ state: nextState,
381
+ eventSeq: event.seq,
382
+ });
383
+ return {
384
+ mutation: normalizeJournalRow(
385
+ cachedStatement(database, 'SELECT * FROM mutation_journal WHERE journal_id = ?')
386
+ .get(row.journal_id),
387
+ ),
388
+ duplicate: false,
389
+ event,
390
+ };
391
+ }
392
+
335
393
  function completeMaterializedMutation(channelIdInput, mutationIdInput, eventInput, options = {}) {
336
394
  const channelId = cleanText(channelIdInput, 'channelId');
337
395
  const mutationId = cleanText(mutationIdInput, 'mutationId', 160);
338
396
  const database = options.database || openLocalDb();
339
- let event = null;
397
+ const result = database.transaction(() => {
398
+ const completed = completeMaterializedInTransaction(
399
+ database,
400
+ channelId,
401
+ mutationId,
402
+ eventInput,
403
+ );
404
+ if (typeof options.finalize === 'function') {
405
+ options.finalize(database, completed);
406
+ }
407
+ return completed;
408
+ })();
409
+ if (result.event) publishStateEvent(result.event);
410
+ return result;
411
+ }
340
412
 
413
+ /**
414
+ * A code-project outbox event is not the official materialization. When that
415
+ * same mutation returns in the numbered cloud tail, project it once more,
416
+ * then move the replica cursor. Repeating this after a crash is safe because
417
+ * the contract materializer folds by official snapshot version.
418
+ */
419
+ function completeOfficialMaterialization(
420
+ channelIdInput,
421
+ mutationIdInput,
422
+ eventInput,
423
+ options = {},
424
+ ) {
425
+ const channelId = cleanText(channelIdInput, 'channelId');
426
+ const mutationId = cleanText(mutationIdInput, 'mutationId', 160);
427
+ const database = options.database || openLocalDb();
428
+ let event = null;
341
429
  const result = database.transaction(() => {
430
+ const finalize = (completed) => {
431
+ if (typeof options.finalize === 'function') {
432
+ options.finalize(database, completed);
433
+ }
434
+ return completed;
435
+ };
342
436
  const row = getMutationRow(database, channelId, mutationId);
343
437
  if (!row) throw invalid(`unknown mutation ${mutationId}`, 404);
344
- if (row.event_seq != null) {
345
- return { mutation: normalizeJournalRow(row), duplicate: true };
438
+ if (
439
+ row.mutation_origin !== 'local'
440
+ || row.mutation_state !== 'synced'
441
+ || row.official_cloud_version == null
442
+ ) throw invalid(`mutation ${mutationId} has no official projection`, 409);
443
+ const replica = cachedStatement(
444
+ database,
445
+ 'SELECT applied_version FROM shared_resource_replicas WHERE resource_id = ?',
446
+ ).get(row.shared_resource_id);
447
+ const appliedVersion = Number(replica?.applied_version);
448
+ const officialVersion = Number(row.official_cloud_version);
449
+ if (row.official_projected_at != null || appliedVersion >= officialVersion) {
450
+ if (row.official_projected_at == null) {
451
+ const now = new Date().toISOString();
452
+ cachedStatement(database, `
453
+ UPDATE mutation_journal
454
+ SET official_projected_at = ?,
455
+ updated_at = ?
456
+ WHERE journal_id = ?
457
+ `).run(now, now, row.journal_id);
458
+ }
459
+ advanceReplicaCursor(database, row.shared_resource_id);
460
+ return finalize({
461
+ mutation: normalizeJournalRow(
462
+ cachedStatement(database, 'SELECT * FROM mutation_journal WHERE journal_id = ?')
463
+ .get(row.journal_id),
464
+ ),
465
+ duplicate: true,
466
+ });
346
467
  }
347
- if (row.mutation_state !== 'saving-local') {
348
- throw invalid(`mutation ${mutationId} cannot materialize from ${row.mutation_state}`, 409);
468
+ if (officialVersion !== appliedVersion + 1) {
469
+ const error = invalid(
470
+ `shared mutation gap for ${row.shared_resource_id}: expected ${appliedVersion + 1}, received ${officialVersion}`,
471
+ 409,
472
+ );
473
+ error.code = 'shared_mutation_gap';
474
+ error.expectedVersion = appliedVersion + 1;
475
+ error.receivedVersion = officialVersion;
476
+ throw error;
349
477
  }
350
478
  if (!eventInput || eventInput.resource !== row.local_resource) {
351
479
  throw invalid('materialized event resource must match the journal localResource');
352
480
  }
353
-
354
481
  event = insertStateEvent(database, {
355
482
  ...eventInput,
356
483
  mutationId: row.mutation_id,
357
484
  sharedResourceId: row.shared_resource_id,
358
485
  });
359
486
  const now = new Date().toISOString();
360
- const nextState = row.mutation_origin === 'cloud' ? 'synced' : 'saved-local';
361
487
  cachedStatement(database, `
362
488
  UPDATE mutation_journal
363
- SET mutation_state = ?,
364
- materialized_at = ?,
365
- event_seq = ?,
366
- last_error = NULL,
489
+ SET official_projected_at = ?,
367
490
  updated_at = ?
368
491
  WHERE journal_id = ?
369
- `).run(nextState, now, event.seq, now, row.journal_id);
370
-
371
- if (row.mutation_origin === 'cloud') {
372
- advanceReplicaCursor(database, row.shared_resource_id);
373
- }
374
-
375
- trace('materialized', {
376
- mutationId: row.mutation_id,
377
- origin: row.mutation_origin,
378
- state: nextState,
379
- eventSeq: event.seq,
380
- });
381
- return {
492
+ `).run(now, now, row.journal_id);
493
+ advanceReplicaCursor(database, row.shared_resource_id);
494
+ return finalize({
382
495
  mutation: normalizeJournalRow(
383
496
  cachedStatement(database, 'SELECT * FROM mutation_journal WHERE journal_id = ?')
384
497
  .get(row.journal_id),
385
498
  ),
386
499
  duplicate: false,
387
- };
500
+ });
388
501
  })();
389
-
390
502
  if (event) publishStateEvent(event);
391
503
  return { ...result, event };
392
504
  }
@@ -411,6 +523,7 @@ function journalCloudMutation(input, options = {}) {
411
523
  const version = positiveInteger(input?.version, 'version');
412
524
  const committedAt = cleanText(input?.committedAt, 'committedAt', 64);
413
525
  const database = options.database || openLocalDb();
526
+ const persist = typeof options.persist === 'function' ? options.persist : null;
414
527
 
415
528
  return database.transaction(() => {
416
529
  const replica = cachedStatement(
@@ -440,22 +553,60 @@ function journalCloudMutation(input, options = {}) {
440
553
  throw invalid(`mutationId ${mutation.mutationId} was reused with different content`, 409);
441
554
  }
442
555
 
443
- // The cloud tail is also the lost-ack recovery path for this runtime's
444
- // own optimistic mutation. Reconcile the official version onto the
445
- // existing row; never create or materialize a second local edit.
446
- if (existing.mutation_origin === 'local' && existing.official_cloud_version == null) {
447
- const now = new Date().toISOString();
448
- cachedStatement(database, `
449
- UPDATE mutation_journal
450
- SET mutation_state = 'synced',
451
- official_cloud_version = ?,
452
- cloud_committed_at = ?,
453
- next_attempt_at = NULL,
454
- last_error = NULL,
455
- updated_at = ?
456
- WHERE journal_id = ?
457
- `).run(version, committedAt, now, existing.journal_id);
458
- advanceReplicaCursor(database, sharedResourceId);
556
+ if (existing.mutation_origin === 'local') {
557
+ const appliedVersion = Number(replica.applied_version);
558
+ if (version > appliedVersion && version !== appliedVersion + 1) {
559
+ const error = invalid(
560
+ `shared mutation gap for ${sharedResourceId}: expected ${appliedVersion + 1}, received ${version}`,
561
+ 409,
562
+ );
563
+ error.code = 'shared_mutation_gap';
564
+ error.expectedVersion = appliedVersion + 1;
565
+ error.receivedVersion = version;
566
+ throw error;
567
+ }
568
+ if (existing.official_cloud_version == null) {
569
+ const now = new Date().toISOString();
570
+ cachedStatement(database, `
571
+ UPDATE mutation_journal
572
+ SET mutation_state = 'synced',
573
+ official_cloud_version = ?,
574
+ cloud_committed_at = ?,
575
+ next_attempt_at = NULL,
576
+ last_error = NULL,
577
+ failure_code = NULL,
578
+ updated_at = ?
579
+ WHERE journal_id = ?
580
+ `).run(version, committedAt, now, existing.journal_id);
581
+ }
582
+ if (!mutationNeedsOrderedEcho(existing.contract)) {
583
+ const now = new Date().toISOString();
584
+ cachedStatement(database, `
585
+ UPDATE mutation_journal
586
+ SET official_projected_at = COALESCE(official_projected_at, ?),
587
+ updated_at = ?
588
+ WHERE journal_id = ?
589
+ `).run(now, now, existing.journal_id);
590
+ advanceReplicaCursor(database, sharedResourceId);
591
+ const reconciled = cachedStatement(
592
+ database,
593
+ 'SELECT * FROM mutation_journal WHERE journal_id = ?',
594
+ ).get(existing.journal_id);
595
+ return {
596
+ mutation: normalizeJournalRow(reconciled),
597
+ duplicate: true,
598
+ alreadyApplied: true,
599
+ };
600
+ }
601
+ if (appliedVersion >= version && existing.official_projected_at == null) {
602
+ const now = new Date().toISOString();
603
+ cachedStatement(database, `
604
+ UPDATE mutation_journal
605
+ SET official_projected_at = ?,
606
+ updated_at = ?
607
+ WHERE journal_id = ?
608
+ `).run(now, now, existing.journal_id);
609
+ }
459
610
  const reconciled = cachedStatement(
460
611
  database,
461
612
  'SELECT * FROM mutation_journal WHERE journal_id = ?',
@@ -463,14 +614,15 @@ function journalCloudMutation(input, options = {}) {
463
614
  return {
464
615
  mutation: normalizeJournalRow(reconciled),
465
616
  duplicate: true,
466
- alreadyApplied: reconciled.event_seq != null,
617
+ alreadyApplied: appliedVersion >= version,
618
+ officialMaterialization: appliedVersion < version,
467
619
  };
468
620
  }
469
621
 
470
622
  return {
471
623
  mutation: normalizeJournalRow(existing),
472
624
  duplicate: true,
473
- alreadyApplied: existing.event_seq != null,
625
+ alreadyApplied: existing.official_projected_at != null,
474
626
  };
475
627
  }
476
628
 
@@ -490,6 +642,7 @@ function journalCloudMutation(input, options = {}) {
490
642
  }
491
643
 
492
644
  const now = new Date().toISOString();
645
+ if (persist) persist(database, mutation);
493
646
  const info = cachedStatement(database, `
494
647
  INSERT INTO mutation_journal (
495
648
  channel_id,
@@ -541,7 +694,10 @@ function journalCloudMutation(input, options = {}) {
541
694
 
542
695
  function ingestCloudMutation(input, options = {}) {
543
696
  const database = options.database || openLocalDb();
544
- const started = journalCloudMutation(input, { database });
697
+ const started = journalCloudMutation(input, {
698
+ database,
699
+ persist: options.persist,
700
+ });
545
701
  if (started.alreadyApplied) return { ...started, event: null };
546
702
 
547
703
  try {
@@ -558,17 +714,56 @@ function ingestCloudMutation(input, options = {}) {
558
714
  const eventInput = typeof options.event === 'function'
559
715
  ? options.event(started.mutation)
560
716
  : options.event;
561
- const completed = completeMaterializedMutation(
562
- started.mutation.channelId,
563
- started.mutation.mutationId,
564
- eventInput,
565
- { database },
566
- );
717
+ const completed = started.officialMaterialization
718
+ ? completeOfficialMaterialization(
719
+ started.mutation.channelId,
720
+ started.mutation.mutationId,
721
+ eventInput,
722
+ { database, finalize: options.finalize },
723
+ )
724
+ : completeMaterializedMutation(
725
+ started.mutation.channelId,
726
+ started.mutation.mutationId,
727
+ eventInput,
728
+ { database, finalize: options.finalize },
729
+ );
567
730
  return { ...completed, duplicate: started.duplicate || completed.duplicate };
568
731
  }
569
732
 
570
733
  function commitLocalMutation(input, options = {}) {
571
734
  const database = options.database || openLocalDb();
735
+ if (mutationNeedsOfficialMaterialization(input?.contract)) {
736
+ if (typeof options.materialize === 'function') {
737
+ throw invalid('code publication cannot materialize official state before cloud order', 409);
738
+ }
739
+ const mutation = normalizeMutationInput(input);
740
+ const persist = typeof options.persist === 'function' ? options.persist : null;
741
+ const result = database.transaction(() => {
742
+ const started = journalNormalizedMutation(database, mutation, persist);
743
+ if (started.duplicate && started.mutation.state !== 'saving-local') {
744
+ return { ...started, event: null };
745
+ }
746
+ const eventInput = typeof options.event === 'function'
747
+ ? options.event(started.mutation)
748
+ : options.event;
749
+ const completed = completeMaterializedInTransaction(
750
+ database,
751
+ started.mutation.channelId,
752
+ started.mutation.mutationId,
753
+ eventInput,
754
+ );
755
+ if (typeof options.finalize === 'function') {
756
+ options.finalize(database, completed);
757
+ }
758
+ return {
759
+ ...completed,
760
+ duplicate: started.duplicate || completed.duplicate,
761
+ };
762
+ })();
763
+ if (result.event) publishStateEvent(result.event);
764
+ return result;
765
+ }
766
+
572
767
  const started = journalMutation(input, { database, persist: options.persist });
573
768
  if (started.duplicate && started.mutation.state !== 'saving-local') {
574
769
  return { ...started, event: null };
@@ -595,7 +790,7 @@ function commitLocalMutation(input, options = {}) {
595
790
  started.mutation.channelId,
596
791
  started.mutation.mutationId,
597
792
  eventInput,
598
- { database },
793
+ { database, finalize: options.finalize },
599
794
  );
600
795
  return {
601
796
  ...completed,
@@ -723,10 +918,10 @@ function acknowledgeCloudMutation(sharedResourceIdInput, mutationIdInput, acknow
723
918
  cloud_committed_at = ?,
724
919
  next_attempt_at = NULL,
725
920
  last_error = NULL,
921
+ failure_code = NULL,
726
922
  updated_at = ?
727
923
  WHERE journal_id = ?
728
924
  `).run(version, committedAt, now, row.journal_id);
729
- advanceReplicaCursor(database, sharedResourceId);
730
925
  event = insertStateEvent(database, {
731
926
  resource: 'shared-mutations',
732
927
  op: 'ack',
@@ -746,6 +941,17 @@ function acknowledgeCloudMutation(sharedResourceIdInput, mutationIdInput, acknow
746
941
  };
747
942
  })();
748
943
  if (event) publishStateEvent(event);
944
+ if (result.mutation?.contract === require('./mutation-contracts').CODE_PROJECT_CONTRACT) {
945
+ const project = require('../workspace/wall').projectForResource(sharedResourceId);
946
+ if (project) {
947
+ require('../workspace/intent').markPublicationNumbered(
948
+ project,
949
+ mutationId,
950
+ version,
951
+ committedAt,
952
+ );
953
+ }
954
+ }
749
955
  return { ...result, event };
750
956
  }
751
957
 
@@ -754,6 +960,10 @@ function failCloudMutation(sharedResourceIdInput, mutationIdInput, failure, opti
754
960
  const mutationId = cleanText(mutationIdInput, 'mutationId', 160);
755
961
  const message = cleanText(failure?.message || failure || 'cloud mutation failed', 'message', 4000);
756
962
  const retryable = failure?.retryable !== false;
963
+ const failureCode = typeof failure?.code === 'string'
964
+ && /^[a-z][a-z0-9_:-]{0,127}$/.test(failure.code)
965
+ ? failure.code
966
+ : null;
757
967
  const retryAt = retryable
758
968
  ? (failure?.retryAt || new Date(Date.now() + 1000).toISOString())
759
969
  : null;
@@ -768,9 +978,17 @@ function failCloudMutation(sharedResourceIdInput, mutationIdInput, failure, opti
768
978
  SET mutation_state = ?,
769
979
  next_attempt_at = ?,
770
980
  last_error = ?,
981
+ failure_code = ?,
771
982
  updated_at = ?
772
983
  WHERE journal_id = ?
773
- `).run(retryable ? 'syncing' : 'rejected', retryAt, message, now, row.journal_id);
984
+ `).run(
985
+ retryable ? 'syncing' : 'rejected',
986
+ retryAt,
987
+ message,
988
+ failureCode,
989
+ now,
990
+ row.journal_id,
991
+ );
774
992
  trace('cloud-fail', { mutationId, shared: sharedResourceId, retryable, message });
775
993
  return normalizeJournalRow(
776
994
  cachedStatement(database, 'SELECT * FROM mutation_journal WHERE journal_id = ?')
@@ -799,18 +1017,38 @@ function markMutationConflicted(sharedResourceIdInput, mutationIdInput, messageI
799
1017
  function listMutations(options = {}) {
800
1018
  const database = options.database || openLocalDb();
801
1019
  const limit = Math.max(1, Math.min(Number(options.limit) || 100, 1000));
802
- const rows = options.state
803
- ? cachedStatement(database, `
1020
+ const localResource = options.localResource == null
1021
+ ? null
1022
+ : cleanText(options.localResource, 'localResource', 8192);
1023
+ let rows;
1024
+ if (options.state && localResource) {
1025
+ rows = cachedStatement(database, `
1026
+ SELECT * FROM mutation_journal
1027
+ WHERE mutation_state = ? AND local_resource = ?
1028
+ ORDER BY journal_id ASC
1029
+ LIMIT ?
1030
+ `).all(options.state, localResource, limit);
1031
+ } else if (options.state) {
1032
+ rows = cachedStatement(database, `
804
1033
  SELECT * FROM mutation_journal
805
1034
  WHERE mutation_state = ?
806
1035
  ORDER BY journal_id ASC
807
1036
  LIMIT ?
808
- `).all(options.state, limit)
809
- : cachedStatement(database, `
1037
+ `).all(options.state, limit);
1038
+ } else if (localResource) {
1039
+ rows = cachedStatement(database, `
1040
+ SELECT * FROM mutation_journal
1041
+ WHERE local_resource = ?
1042
+ ORDER BY journal_id ASC
1043
+ LIMIT ?
1044
+ `).all(localResource, limit);
1045
+ } else {
1046
+ rows = cachedStatement(database, `
810
1047
  SELECT * FROM mutation_journal
811
1048
  ORDER BY journal_id ASC
812
1049
  LIMIT ?
813
1050
  `).all(limit);
1051
+ }
814
1052
  return rows.map((row) => normalizeJournalRow(row, { diagnostic: true }));
815
1053
  }
816
1054
 
@@ -834,6 +1072,120 @@ function listPendingCloudMutations(resourceIdInput, options = {}) {
834
1072
  `).all(resourceId).map((row) => normalizeJournalRow(row));
835
1073
  }
836
1074
 
1075
+ /**
1076
+ * Local overlay rows are every unnumbered mutation plus numbered local work
1077
+ * whose official event has not yet folded into the materialized resource.
1078
+ * Snapshot projection must retain the complete set, so this view is
1079
+ * deliberately unpaged.
1080
+ */
1081
+ function listLocalOverlayMutations(resourceIdInput, options = {}) {
1082
+ const resourceId = cleanText(resourceIdInput, 'resourceId', 132);
1083
+ if (!isCloudResourceId(resourceId)) {
1084
+ throw invalid('resourceId must be an opaque cloud resource id');
1085
+ }
1086
+ const database = options.database || openLocalDb();
1087
+ const throughVersion = options.throughVersion == null
1088
+ ? null
1089
+ : nonNegativeInteger(options.throughVersion, 'throughVersion');
1090
+ if (throughVersion !== null) {
1091
+ return cachedStatement(database, `
1092
+ SELECT * FROM mutation_journal
1093
+ WHERE shared_resource_id = ?
1094
+ AND mutation_origin = 'local'
1095
+ AND (
1096
+ mutation_state != 'synced'
1097
+ OR official_cloud_version IS NULL
1098
+ OR official_cloud_version > ?
1099
+ )
1100
+ ORDER BY journal_id ASC
1101
+ `).all(resourceId, throughVersion).map((row) => normalizeJournalRow(row));
1102
+ }
1103
+ return cachedStatement(database, `
1104
+ SELECT * FROM mutation_journal
1105
+ WHERE shared_resource_id = ?
1106
+ AND mutation_origin = 'local'
1107
+ AND (mutation_state != 'synced' OR official_projected_at IS NULL)
1108
+ ORDER BY journal_id ASC
1109
+ `).all(resourceId).map((row) => normalizeJournalRow(row));
1110
+ }
1111
+
1112
+ function listUnappliedOfficialMutations(resourceIdInput, options = {}) {
1113
+ const resourceId = cleanText(resourceIdInput, 'resourceId', 132);
1114
+ if (!isCloudResourceId(resourceId)) {
1115
+ throw invalid('resourceId must be an opaque cloud resource id');
1116
+ }
1117
+ const database = options.database || openLocalDb();
1118
+ const replica = cachedStatement(
1119
+ database,
1120
+ 'SELECT applied_version FROM shared_resource_replicas WHERE resource_id = ?',
1121
+ ).get(resourceId);
1122
+ const appliedVersion = Number(replica?.applied_version ?? -1);
1123
+ return cachedStatement(database, `
1124
+ SELECT *
1125
+ FROM mutation_journal
1126
+ WHERE shared_resource_id = ?
1127
+ AND mutation_origin = 'local'
1128
+ AND mutation_state = 'synced'
1129
+ AND official_cloud_version > ?
1130
+ ORDER BY official_cloud_version ASC
1131
+ `).all(resourceId, appliedVersion)
1132
+ .filter((row) => mutationNeedsOfficialMaterialization(row.contract))
1133
+ .map((row) => normalizeJournalRow(row));
1134
+ }
1135
+
1136
+ /**
1137
+ * A verified code snapshot through N is the ordinary ordered fold for every
1138
+ * already-numbered local publication at or before N. This repairs the same
1139
+ * durable fact on an equal snapshot retry after a crash.
1140
+ */
1141
+ function completeOfficialSnapshotFold(resourceIdInput, throughInput, options = {}) {
1142
+ const resourceId = cleanText(resourceIdInput, 'resourceId', 132);
1143
+ const through = nonNegativeInteger(throughInput, 'through');
1144
+ const database = options.database || openLocalDb();
1145
+ const contract = cleanText(
1146
+ options.contract || require('./mutation-contracts').CODE_PROJECT_CONTRACT,
1147
+ 'contract',
1148
+ 128,
1149
+ );
1150
+ const now = new Date().toISOString();
1151
+ return cachedStatement(database, `
1152
+ UPDATE mutation_journal
1153
+ SET official_projected_at = COALESCE(official_projected_at, ?),
1154
+ updated_at = ?
1155
+ WHERE shared_resource_id = ?
1156
+ AND contract = ?
1157
+ AND mutation_origin = 'local'
1158
+ AND mutation_state = 'synced'
1159
+ AND official_cloud_version <= ?
1160
+ `).run(
1161
+ now,
1162
+ now,
1163
+ resourceId,
1164
+ contract,
1165
+ through,
1166
+ ).changes;
1167
+ }
1168
+
1169
+ /**
1170
+ * A stale proposal was never an official event. Keep it as a publication
1171
+ * barrier until this replica has actually advanced past the base it used,
1172
+ * then retire it so the unchanged local intent can be proposed anew.
1173
+ */
1174
+ function retireStaleCodeMutations(resourceIdInput, appliedVersionInput, options = {}) {
1175
+ const resourceId = cleanText(resourceIdInput, 'resourceId', 132);
1176
+ const appliedVersion = nonNegativeInteger(appliedVersionInput, 'appliedVersion');
1177
+ const database = options.database || openLocalDb();
1178
+ return cachedStatement(database, `
1179
+ DELETE FROM mutation_journal
1180
+ WHERE shared_resource_id = ?
1181
+ AND mutation_origin = 'local'
1182
+ AND contract = ?
1183
+ AND mutation_state = 'rejected'
1184
+ AND failure_code = 'code_project_stale_base'
1185
+ AND base_cloud_version < ?
1186
+ `).run(resourceId, require('./mutation-contracts').CODE_PROJECT_CONTRACT, appliedVersion).changes;
1187
+ }
1188
+
837
1189
  function pruneMutationJournal(options = {}) {
838
1190
  const database = options.database || openLocalDb();
839
1191
  const hours = Number(options.retentionHours ?? process.env.AMALGM_EVENT_LOG_RETENTION_HOURS ?? 0.25);
@@ -843,23 +1195,44 @@ function pruneMutationJournal(options = {}) {
843
1195
  const cutoffIso = options.cutoffIso
844
1196
  || new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
845
1197
  const batchSize = Math.max(1, Math.floor(Number(options.batchSize) || 1000));
846
- const result = cachedStatement(database, `
847
- DELETE FROM mutation_journal
848
- WHERE journal_id IN (
849
- SELECT journal_id
850
- FROM mutation_journal
851
- WHERE updated_at < ?
852
- AND event_seq IS NOT NULL
853
- AND (
854
- (shared_resource_id IS NULL AND mutation_state = 'saved-local')
855
- OR mutation_state = 'synced'
1198
+ const readPage = cachedStatement(database, `
1199
+ SELECT *
1200
+ FROM mutation_journal
1201
+ WHERE journal_id > ?
1202
+ AND updated_at < ?
1203
+ AND event_seq IS NOT NULL
1204
+ AND (
1205
+ (shared_resource_id IS NULL AND mutation_state = 'saved-local')
1206
+ OR (
1207
+ shared_resource_id IS NOT NULL
1208
+ AND mutation_state = 'synced'
1209
+ AND official_projected_at IS NOT NULL
856
1210
  )
857
- ORDER BY journal_id ASC
858
- LIMIT ?
859
- )
860
- `).run(cutoffIso, batchSize);
1211
+ )
1212
+ ORDER BY journal_id ASC
1213
+ LIMIT ?
1214
+ `);
1215
+ const { mutationMayPrune } = require('./mutation-contracts');
1216
+ const snapshotCache = new Map();
1217
+ const deletable = [];
1218
+ let afterJournalId = 0;
1219
+ while (deletable.length < batchSize) {
1220
+ const page = readPage.all(afterJournalId, cutoffIso, batchSize);
1221
+ if (page.length === 0) break;
1222
+ for (const row of page) {
1223
+ afterJournalId = Number(row.journal_id);
1224
+ if (mutationMayPrune(row, { database, snapshotCache })) {
1225
+ deletable.push(row.journal_id);
1226
+ if (deletable.length === batchSize) break;
1227
+ }
1228
+ }
1229
+ }
1230
+ const remove = cachedStatement(database, 'DELETE FROM mutation_journal WHERE journal_id = ?');
1231
+ const deleted = database.transaction((journalIds) => (
1232
+ journalIds.reduce((count, journalId) => count + remove.run(journalId).changes, 0)
1233
+ ))(deletable);
861
1234
  return {
862
- deleted: result.changes || 0,
1235
+ deleted,
863
1236
  cutoffIso,
864
1237
  retentionHours: hours,
865
1238
  batchSize,
@@ -872,13 +1245,17 @@ module.exports = {
872
1245
  claimCloudMutations,
873
1246
  commitLocalMutation,
874
1247
  completeMaterializedMutation,
1248
+ completeOfficialSnapshotFold,
875
1249
  failCloudMutation,
876
1250
  getMutation,
877
1251
  ingestCloudMutation,
878
1252
  journalCloudMutation,
879
1253
  journalMutation,
1254
+ listLocalOverlayMutations,
1255
+ listUnappliedOfficialMutations,
880
1256
  listPendingCloudMutations,
881
1257
  listMutations,
882
1258
  markMutationConflicted,
883
1259
  pruneMutationJournal,
1260
+ retireStaleCodeMutations,
884
1261
  };