amalgm 0.1.198 → 0.1.200

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.198",
3
+ "version": "0.1.200",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -31,6 +31,7 @@ const { refreshRepoCards } = require('../workspace/cards');
31
31
  const { checkpointFile, repoKeyOfCard } = require('../workspace/checkpoint');
32
32
  const {
33
33
  bindProject,
34
+ compactWallText,
34
35
  hasLocalContribution,
35
36
  isCommittedCard,
36
37
  projectForRepo,
@@ -352,6 +353,7 @@ test('peer records and earlier local history survive later publishes', async ()
352
353
  wall: 'code-project@3',
353
354
  project: repoKey,
354
355
  machine: 'machine-b',
356
+ mutationId: 'mut_project_peer',
355
357
  publishedAt: '2026-07-19T00:00:00.000Z',
356
358
  cards: [{ card: 'repo-card@2', machine: 'machine-b', replica: 'rpl_peer', path: '/work/app', head: { branch: 'preview' } }],
357
359
  });
@@ -401,6 +403,103 @@ test('three concurrent append records stay whole under Yjs ordering', () => {
401
403
  );
402
404
  });
403
405
 
406
+ test('wall compaction deletes only records superseded for every machine', () => {
407
+ const line = (machine, value) => JSON.stringify({
408
+ wall: 'code-project@3',
409
+ project: repoKey,
410
+ machine,
411
+ mutationId: `mut_project_${machine}_${value}`,
412
+ cards: [{ value }],
413
+ });
414
+ const oldA = line('machine-a', 'old-a');
415
+ const oldB = line('machine-b', 'old-b');
416
+ const latestA = line('machine-a', 'latest-a');
417
+ const torn = '{"wall":"code-project@3","machine":"torn"';
418
+ const latestB = line('machine-b', 'latest-b');
419
+ const current = `${oldA}\n${oldB}\n${latestA}\n${torn}\n${latestB}\n`;
420
+ const compacted = compactWallText(current, repoKey);
421
+
422
+ assert.equal(compacted, `${latestA}\n${torn}\n${latestB}\n`);
423
+ assert.equal(compacted.includes(oldA), false);
424
+ assert.equal(compacted.includes(oldB), false);
425
+ });
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
+
450
+ test('a concurrent deletion-only compaction and append commute in Yjs', () => {
451
+ const Y = require('yjs');
452
+ const line = (machine, value) => JSON.stringify({
453
+ wall: 'code-project@3',
454
+ project: repoKey,
455
+ machine,
456
+ mutationId: `mut_project_${machine}_${value}`,
457
+ cards: [{ value }],
458
+ });
459
+ const oldA = line('machine-a', 'old-a');
460
+ const oldB = line('machine-b', 'old-b');
461
+ const latestA = line('machine-a', 'latest-a');
462
+ const latestB = line('machine-b', 'latest-b');
463
+ const baseText = `${oldA}\n${oldB}\n${latestA}\n${latestB}\n`;
464
+ const compacted = compactWallText(baseText, repoKey);
465
+ const base = new Y.Doc();
466
+ base.getText('content').insert(0, baseText);
467
+ const baseUpdate = Y.encodeStateAsUpdate(base);
468
+
469
+ const writerUpdate = (change) => {
470
+ const writer = new Y.Doc();
471
+ Y.applyUpdate(writer, baseUpdate);
472
+ let update = null;
473
+ writer.on('update', (value) => {
474
+ update = value;
475
+ });
476
+ change(writer.getText('content'));
477
+ return update;
478
+ };
479
+ const compactUpdate = writerUpdate((text) => {
480
+ text.delete(0, baseText.length - compacted.length);
481
+ });
482
+ const appendLine = `${line('machine-c', 'latest-c')}\n`;
483
+ const appendUpdate = writerUpdate((text) => {
484
+ text.insert(text.length, appendLine);
485
+ });
486
+
487
+ const replicas = [
488
+ [compactUpdate, appendUpdate],
489
+ [appendUpdate, compactUpdate],
490
+ ].map((order) => {
491
+ const replica = new Y.Doc();
492
+ Y.applyUpdate(replica, baseUpdate);
493
+ for (const update of order) Y.applyUpdate(replica, update);
494
+ return replica.getText('content').toString();
495
+ });
496
+ assert.equal(replicas[0], replicas[1]);
497
+ assert.deepEqual(
498
+ replicas[0].trim().split('\n').map((entry) => JSON.parse(entry).cards[0].value),
499
+ ['latest-a', 'latest-b', 'latest-c'],
500
+ );
501
+ });
502
+
404
503
  test('a torn line is ignored by readers, never fatal', () => {
405
504
  writeDocText(
406
505
  project.file,
@@ -409,6 +508,23 @@ test('a torn line is ignored by readers, never fatal', () => {
409
508
  assert.deepEqual(readWall(repoKey).map((entry) => entry.machine).sort(), ['machine-a', 'machine-b']);
410
509
  });
411
510
 
511
+ test('publishing compacts an oversized history before appending the new record', async () => {
512
+ const before = getDocText(project.file);
513
+ writeDocText(project.file, `${'obsolete\n'.repeat(132_000)}${before}`);
514
+ assert.ok(Buffer.byteLength(getDocText(project.file), 'utf8') > 1024 * 1024);
515
+
516
+ const repo = path.join(tempRoot, 'projects/app');
517
+ fs.writeFileSync(path.join(repo, 'after-compaction.txt'), 'bounded\n');
518
+ await run(repo, 'add', '.');
519
+ await run(repo, 'commit', '-m', 'publish after compaction');
520
+ await refreshRepoCards();
521
+ const result = publishWall(repoKey);
522
+
523
+ assert.equal(result.published, true);
524
+ assert.ok(Buffer.byteLength(getDocText(project.file), 'utf8') < 1024 * 1024);
525
+ assert.deepEqual(readWall(repoKey).map((entry) => entry.machine).sort(), ['machine-a', 'machine-b']);
526
+ });
527
+
412
528
  test('project stats name every project resource, binding, and publish counter', () => {
413
529
  const stats = wallStats();
414
530
  const row = stats.projects.find((entry) => entry.repoKey === repoKey);
@@ -23,6 +23,7 @@ const WALL_CONTRACT = 'code-project@3';
23
23
  const PROJECT_DIR = path.join(AMALGM_DIR, 'state', 'code-projects');
24
24
  const REGISTRY_FILE = path.join(PROJECT_DIR, 'projects.json');
25
25
  const PUBLISH_QUIET_MS = 25;
26
+ const COMPACT_AT_BYTES = 1024 * 1024;
26
27
  const ENSURE_STEP_TIMEOUT_MS = 20_000;
27
28
  const ENSURE_INTERVAL_MS = 15_000;
28
29
  const ATTACH_REJECTED = /shared_resource_read_forbidden/;
@@ -134,6 +135,7 @@ function parseWallLine(line) {
134
135
  entry?.wall !== WALL_CONTRACT
135
136
  || !entry.project
136
137
  || !entry.machine
138
+ || !entry.mutationId
137
139
  || !Array.isArray(entry.cards)
138
140
  ) return null;
139
141
  return entry;
@@ -442,6 +444,29 @@ function withoutPublishedAt(line) {
442
444
  return JSON.stringify(rest);
443
445
  }
444
446
 
447
+ /**
448
+ * Keep the newest complete record for every machine, plus anything after
449
+ * the earliest of those records. Everything before that point is obsolete:
450
+ * every machine represented there has a newer complete value later.
451
+ *
452
+ * Compaction is deletion-only. Concurrent deletions and appends commute in
453
+ * Yjs, so this bounds the wall without bringing back whole-document races.
454
+ */
455
+ function compactWallText(current, repoKey) {
456
+ const latest = new Map();
457
+ const lines = current.split('\n');
458
+ let offset = 0;
459
+ for (let index = 0; index < lines.length; index += 1) {
460
+ const line = lines[index];
461
+ const entry = line ? parseWallLine(line) : null;
462
+ if (entry?.project === repoKey) latest.set(entry.machine, offset);
463
+ offset += line.length + (index < lines.length - 1 ? 1 : 0);
464
+ }
465
+ if (latest.size === 0) return current;
466
+ const cutoff = Math.min(...latest.values());
467
+ return cutoff > 0 ? current.slice(cutoff) : current;
468
+ }
469
+
445
470
  function publishProject(project, rawCards) {
446
471
  const replica = wallReplica(project);
447
472
  if (!replica) {
@@ -449,7 +474,7 @@ function publishProject(project, rawCards) {
449
474
  return { published: false, reason: 'project-not-bound', repoKey: project.repoKey };
450
475
  }
451
476
  const docs = require('../state/docs');
452
- const current = docs.getDocText(project.file);
477
+ let current = docs.getDocText(project.file);
453
478
  let previous = null;
454
479
  let previousEntry = null;
455
480
  for (const line of current.split('\n')) {
@@ -501,9 +526,20 @@ function publishProject(project, rawCards) {
501
526
  }
502
527
 
503
528
  const separator = current && !current.endsWith('\n') ? '\n' : '';
529
+ if (Buffer.byteLength(`${current}${separator}${mine}\n`, 'utf8') > COMPACT_AT_BYTES) {
530
+ const compacted = compactWallText(current, project.repoKey);
531
+ if (compacted !== current) {
532
+ docs.writeDocText(project.file, compacted, {
533
+ baseHash: sha256(current),
534
+ mutationId: `mut_project_compact_${crypto.randomBytes(16).toString('base64url')}`,
535
+ });
536
+ current = docs.getDocText(project.file);
537
+ }
538
+ }
539
+ const nextSeparator = current && !current.endsWith('\n') ? '\n' : '';
504
540
  const result = docs.writeDocText(
505
541
  project.file,
506
- `${current}${separator}${mine}\n`,
542
+ `${current}${nextSeparator}${mine}\n`,
507
543
  { baseHash: sha256(current), mutationId },
508
544
  );
509
545
  service.counters.publishes += 1;
@@ -686,6 +722,7 @@ function wallStats() {
686
722
  module.exports = {
687
723
  WALL_CONTRACT,
688
724
  bindProject,
725
+ compactWallText,
689
726
  hasLocalContribution,
690
727
  isCommittedCard,
691
728
  projectFile,