amalgm 0.1.199 → 0.1.201

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.199",
3
+ "version": "0.1.201",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -40,6 +40,19 @@ test.after(() => {
40
40
  fs.rmSync(tempRoot, { recursive: true, force: true });
41
41
  });
42
42
 
43
+ test('checkpoint publication waits until the reducer finishes writing both repo layers', async () => {
44
+ const { withMaterialization } = require('../workspace/reducer');
45
+ const repoKey = 'https://example.test/repo.git';
46
+
47
+ assert.equal(_test.checkpointMayPublish(repoKey), true);
48
+ await withMaterialization(repoKey, async () => {
49
+ assert.equal(_test.checkpointMayPublish(repoKey), false);
50
+ await Promise.resolve();
51
+ assert.equal(_test.checkpointMayPublish(repoKey), false);
52
+ });
53
+ assert.equal(_test.checkpointMayPublish(repoKey), true);
54
+ });
55
+
43
56
  async function run(cwd, ...args) {
44
57
  await execFileAsync(
45
58
  'git',
@@ -353,6 +353,7 @@ test('peer records and earlier local history survive later publishes', async ()
353
353
  wall: 'code-project@3',
354
354
  project: repoKey,
355
355
  machine: 'machine-b',
356
+ mutationId: 'mut_project_peer',
356
357
  publishedAt: '2026-07-19T00:00:00.000Z',
357
358
  cards: [{ card: 'repo-card@2', machine: 'machine-b', replica: 'rpl_peer', path: '/work/app', head: { branch: 'preview' } }],
358
359
  });
@@ -407,6 +408,7 @@ test('wall compaction deletes only records superseded for every machine', () =>
407
408
  wall: 'code-project@3',
408
409
  project: repoKey,
409
410
  machine,
411
+ mutationId: `mut_project_${machine}_${value}`,
410
412
  cards: [{ value }],
411
413
  });
412
414
  const oldA = line('machine-a', 'old-a');
@@ -422,12 +424,36 @@ test('wall compaction deletes only records superseded for every machine', () =>
422
424
  assert.equal(compacted.includes(oldB), false);
423
425
  });
424
426
 
427
+ test('an unordered legacy record cannot pin wall compaction', () => {
428
+ const unordered = JSON.stringify({
429
+ wall: 'code-project@3',
430
+ project: repoKey,
431
+ machine: '16.612Z',
432
+ cards: [{ value: 'corrupted-legacy-record' }],
433
+ });
434
+ const ordered = (machine) => JSON.stringify({
435
+ wall: 'code-project@3',
436
+ project: repoKey,
437
+ machine,
438
+ mutationId: `mut_project_${machine}`,
439
+ cards: [{ value: machine }],
440
+ });
441
+ const latestA = ordered('machine-a');
442
+ const latestB = ordered('machine-b');
443
+
444
+ assert.equal(
445
+ compactWallText(`${unordered}\n${latestA}\n${latestB}\n`, repoKey),
446
+ `${latestA}\n${latestB}\n`,
447
+ );
448
+ });
449
+
425
450
  test('a concurrent deletion-only compaction and append commute in Yjs', () => {
426
451
  const Y = require('yjs');
427
452
  const line = (machine, value) => JSON.stringify({
428
453
  wall: 'code-project@3',
429
454
  project: repoKey,
430
455
  machine,
456
+ mutationId: `mut_project_${machine}_${value}`,
431
457
  cards: [{ value }],
432
458
  });
433
459
  const oldA = line('machine-a', 'old-a');
@@ -188,6 +188,10 @@ function checkpointBaseReady(card, reduced) {
188
188
  );
189
189
  }
190
190
 
191
+ function checkpointMayPublish(repoKey) {
192
+ return !require('./reducer').isMaterializing(repoKey);
193
+ }
194
+
191
195
  async function publishPass(replicaId = null) {
192
196
  const machine = machineId();
193
197
  const run = { published: 0, skips: 0, waiting: 0, failures: 0 };
@@ -196,6 +200,10 @@ async function publishPass(replicaId = null) {
196
200
  if (card.health !== 'healthy' || card.kind !== 'primary' || card.operation !== 'none') continue;
197
201
  const repoKey = repoKeyOfCard(card);
198
202
  if (!repoKey || !card.head?.oid) continue;
203
+ if (!checkpointMayPublish(repoKey)) {
204
+ run.waiting += 1;
205
+ continue;
206
+ }
199
207
  try {
200
208
  const reduced = require('./reducer').readRepoState(repoKey);
201
209
  if (!checkpointBaseReady(card, reduced)) {
@@ -376,6 +384,7 @@ module.exports = {
376
384
  _test: {
377
385
  checkpointBaseReady,
378
386
  checkpointForSharedState,
387
+ checkpointMayPublish,
379
388
  },
380
389
  CHECKPOINT_CONTRACT,
381
390
  MAX_INLINE_DIFF_BYTES,
@@ -50,6 +50,20 @@ const service = {
50
50
  repos: new Map(),
51
51
  lastPass: null,
52
52
  };
53
+ const materializing = new Set();
54
+
55
+ function isMaterializing(repoKey) {
56
+ return materializing.has(repoKey);
57
+ }
58
+
59
+ async function withMaterialization(repoKey, work) {
60
+ materializing.add(repoKey);
61
+ try {
62
+ return await work();
63
+ } finally {
64
+ materializing.delete(repoKey);
65
+ }
66
+ }
53
67
 
54
68
  function stateFile(repoKey) {
55
69
  const digest = crypto.createHash('sha256').update(repoKey).digest('hex').slice(0, 24);
@@ -587,7 +601,10 @@ async function runReducer() {
587
601
  if (!plan.card) continue;
588
602
  try {
589
603
  if (!local?.head?.oid) {
590
- const hydrated = await hydrateRepo(wall.projectForRepo(repoKey), plan);
604
+ const hydrated = await withMaterialization(
605
+ repoKey,
606
+ () => hydrateRepo(wall.projectForRepo(repoKey), plan),
607
+ );
591
608
  service.repos.set(repoKey, {
592
609
  repoKey,
593
610
  path: wall.projectForRepo(repoKey)?.path || local?.path || null,
@@ -627,7 +644,10 @@ async function runReducer() {
627
644
  service.counters.waiting += 1;
628
645
  continue;
629
646
  }
630
- const outcome = await materializeRepo(refreshed, currentPlan);
647
+ const outcome = await withMaterialization(
648
+ repoKey,
649
+ () => materializeRepo(refreshed, currentPlan),
650
+ );
631
651
  service.repos.set(repoKey, { repoKey, path: local.path, ...outcome, at: new Date().toISOString() });
632
652
  if (outcome.state === 'waiting') service.counters.waiting += 1;
633
653
  if (outcome.moved) {
@@ -718,6 +738,7 @@ module.exports = {
718
738
  advanceIndex,
719
739
  captureIsOnRail,
720
740
  hydrateRepo,
741
+ isMaterializing,
721
742
  localCardMayMaterialize,
722
743
  materializeRepo,
723
744
  readRepoState,
@@ -727,4 +748,5 @@ module.exports = {
727
748
  reducerStats,
728
749
  startWorkspaceReducer,
729
750
  stopWorkspaceReducer,
751
+ withMaterialization,
730
752
  };
@@ -135,6 +135,7 @@ function parseWallLine(line) {
135
135
  entry?.wall !== WALL_CONTRACT
136
136
  || !entry.project
137
137
  || !entry.machine
138
+ || !entry.mutationId
138
139
  || !Array.isArray(entry.cards)
139
140
  ) return null;
140
141
  return entry;