amalgm 0.1.205 → 0.1.207

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.205",
3
+ "version": "0.1.207",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -120,9 +120,11 @@ function hashText(text) {
120
120
  function isInternalStateDoc(filePath) {
121
121
  const resolved = path.resolve(filePath);
122
122
  const parent = path.dirname(resolved);
123
+ const collection = path.basename(parent);
124
+ const isStateCollection = path.basename(path.dirname(parent)) === 'state';
123
125
  return (
124
- path.basename(parent) === 'repo-checkpoints'
125
- && path.basename(path.dirname(parent)) === 'state'
126
+ isStateCollection
127
+ && (collection === 'repo-checkpoints' || collection === 'code-projects')
126
128
  ) || /^machine-cards-v\d+\.jsonl$/.test(path.basename(resolved));
127
129
  }
128
130
 
@@ -221,7 +223,7 @@ function resolveDocPath(pathInput) {
221
223
  }
222
224
  const stats = fs.statSync(resolved);
223
225
  if (!stats.isFile()) throw invalid('path is not a file');
224
- if (stats.size > MAX_DOC_BYTES) {
226
+ if (stats.size > MAX_DOC_BYTES && !isInternalStateDoc(resolved)) {
225
227
  throw invalid(`file exceeds the ${MAX_DOC_BYTES}-byte live document limit`, 413);
226
228
  }
227
229
  return resolved;
@@ -135,19 +135,29 @@ test('external disk edits reconcile into the live doc via the watcher', async ()
135
135
  });
136
136
 
137
137
  test('internal protocol documents never treat their own disk flush as another writer', async () => {
138
- const stateDir = path.join(process.env.AMALGM_DIR, 'state', 'repo-checkpoints');
139
- fs.mkdirSync(stateDir, { recursive: true });
140
- const target = path.join(stateDir, 'protocol.json');
138
+ const checkpointDir = path.join(process.env.AMALGM_DIR, 'state', 'repo-checkpoints');
139
+ const projectDir = path.join(process.env.AMALGM_DIR, 'state', 'code-projects');
140
+ fs.mkdirSync(checkpointDir, { recursive: true });
141
+ fs.mkdirSync(projectDir, { recursive: true });
142
+ const target = path.join(checkpointDir, 'protocol.json');
143
+ const project = path.join(projectDir, 'project.jsonl');
141
144
  fs.writeFileSync(target, '{"value":1}\n', 'utf8');
145
+ fs.writeFileSync(project, '{"value":1}\n', 'utf8');
142
146
  const opened = docs.openDoc(target);
147
+ const openedProject = docs.openDoc(project);
143
148
  const before = currentSeq();
144
149
 
145
150
  assert.equal(docs._private.isInternalStateDoc(target), true);
151
+ assert.equal(docs._private.isInternalStateDoc(project), true);
146
152
  fs.writeFileSync(target, '{"value":2}\n', 'utf8');
153
+ fs.writeFileSync(project, '{"value":2}\n', 'utf8');
147
154
  await sleep(700);
148
155
 
149
156
  assert.equal(docs.getDocText(target), '{"value":1}\n');
150
- const events = listEventsAfter(before).filter((event) => event.resource === opened.resource);
157
+ assert.equal(docs.getDocText(project), '{"value":1}\n');
158
+ const events = listEventsAfter(before).filter((event) => (
159
+ event.resource === opened.resource || event.resource === openedProject.resource
160
+ ));
151
161
  assert.equal(events.length, 0);
152
162
  });
153
163
 
@@ -203,6 +213,11 @@ test('guards: binary, missing, oversized, and malformed inputs are rejected', ()
203
213
 
204
214
  const big = writeDoc('big.md', 'x'.repeat(2 * 1024 * 1024 + 1));
205
215
  assert.throws(() => docs.openDoc(big), /live document limit/);
216
+ const projectDir = path.join(process.env.AMALGM_DIR, 'state', 'code-projects');
217
+ fs.mkdirSync(projectDir, { recursive: true });
218
+ const largeProtocol = path.join(projectDir, 'large-project.jsonl');
219
+ fs.writeFileSync(largeProtocol, 'x'.repeat(2 * 1024 * 1024 + 1));
220
+ assert.doesNotThrow(() => docs.openDoc(largeProtocol));
206
221
 
207
222
  const target = writeDoc('guarded.md', 'ok\n');
208
223
  assert.throws(() => docs.applyDocUpdates(target, [123]), /base64-encoded/);
@@ -364,6 +364,18 @@ test('side branches keep one rail operation while the checked-out card advances'
364
364
  card = readWall(repoKey)[0].cards[0];
365
365
  assert.equal(card.positionMutationId, advanced.mutationId);
366
366
  assert.equal(card.branches.find((entry) => entry.name === branch).mutationId, created.mutationId);
367
+
368
+ acknowledgeCloudMutation(project.resourceId, advanced.mutationId, {
369
+ version: 4,
370
+ committedAt: '2026-07-23T00:00:05.000Z',
371
+ });
372
+ await run(repo, 'branch', '-D', branch);
373
+ await refreshRepoCards();
374
+ const deleted = publishWall(repoKey);
375
+ assert.equal(deleted.published, true);
376
+ card = readWall(repoKey)[0].cards[0];
377
+ assert.equal(card.branches.find((entry) => entry.name === branch).oid, null);
378
+ assert.equal(card.branches.find((entry) => entry.name === branch).mutationId, deleted.mutationId);
367
379
  });
368
380
 
369
381
  test('peer records and earlier local history survive later publishes', async () => {
@@ -466,6 +478,30 @@ test('an unordered legacy record cannot pin wall compaction', () => {
466
478
  );
467
479
  });
468
480
 
481
+ test('retired machine values stay represented without pinning obsolete active history', () => {
482
+ const line = (machine, value) => JSON.stringify({
483
+ wall: 'code-project@3',
484
+ project: repoKey,
485
+ machine,
486
+ mutationId: `mut_project_${machine}_${value}`,
487
+ cards: [{ value }],
488
+ });
489
+ const retiredA = line('retired-a', 'latest-retired-a');
490
+ const retiredB = line('retired-b', 'latest-retired-b');
491
+ const oldA = line('machine-a', 'old-a');
492
+ const oldB = line('machine-b', 'old-b');
493
+ const latestA = line('machine-a', 'latest-a');
494
+ const latestB = line('machine-b', 'latest-b');
495
+
496
+ assert.equal(
497
+ compactWallText(
498
+ `${retiredA}\n${retiredB}\n${oldA}\n${oldB}\n${latestA}\n${latestB}\n`,
499
+ repoKey,
500
+ ),
501
+ `${retiredA}\n${retiredB}\n${latestA}\n${latestB}\n`,
502
+ );
503
+ });
504
+
469
505
  test('a concurrent deletion-only compaction and append commute in Yjs', () => {
470
506
  const Y = require('yjs');
471
507
  const line = (machine, value) => JSON.stringify({
@@ -225,7 +225,7 @@ function oidMap(refs) {
225
225
  * materialized. A local value equal to either that baseline or the current
226
226
  * rail is not new work. Anything else is a local contribution.
227
227
  */
228
- function hasLocalContribution(card, plan, baseline, checkpoint = null) {
228
+ function hasLocalContribution(card, plan, baseline, checkpoint = null, previousCard = null) {
229
229
  if (!plan?.card) return true;
230
230
  const onRail = samePosition(card, plan.card);
231
231
  const atBaseline = baseline
@@ -236,11 +236,21 @@ function hasLocalContribution(card, plan, baseline, checkpoint = null) {
236
236
  const currentRefs = oidMap(card.refs);
237
237
  const railRefs = oidMap(plan.branches);
238
238
  const baselineRefs = oidMap(baseline?.branches);
239
- for (const name of new Set([...currentRefs.keys(), ...baselineRefs.keys()])) {
239
+ const previousRefs = oidMap(previousCard?.branches || previousCard?.refs);
240
+ for (const name of new Set([
241
+ ...currentRefs.keys(),
242
+ ...baselineRefs.keys(),
243
+ ...previousRefs.keys(),
244
+ ])) {
240
245
  const current = currentRefs.get(name) ?? null;
241
246
  const shared = railRefs.get(name) ?? null;
242
247
  const previous = baselineRefs.get(name) ?? null;
243
- if (current !== shared && (!baselineRefs.has(name) || current !== previous)) return true;
248
+ const changedFromPublished = previousRefs.has(name)
249
+ && current !== (previousRefs.get(name) ?? null);
250
+ if (
251
+ current !== shared
252
+ && (!baselineRefs.has(name) || current !== previous || changedFromPublished)
253
+ ) return true;
244
254
  }
245
255
 
246
256
  if (
@@ -446,12 +456,10 @@ function withoutPublishedAt(line) {
446
456
  }
447
457
 
448
458
  /**
449
- * Keep the newest complete record for every machine, plus anything after
450
- * the earliest of those records. Everything before that point is obsolete:
451
- * every machine represented there has a newer complete value later.
452
- *
453
- * Compaction is deletion-only. Concurrent deletions and appends commute in
454
- * Yjs, so this bounds the wall without bringing back whole-document races.
459
+ * Keep every machine's newest complete record and delete the largest span
460
+ * containing none of them. Repeating this as the wall grows removes all
461
+ * obsolete history, even when a retired machine's last record is near the
462
+ * beginning. Each pass is one deletion, so concurrent appends commute in Yjs.
455
463
  */
456
464
  function compactWallText(current, repoKey) {
457
465
  const latest = new Map();
@@ -460,12 +468,21 @@ function compactWallText(current, repoKey) {
460
468
  for (let index = 0; index < lines.length; index += 1) {
461
469
  const line = lines[index];
462
470
  const entry = line ? parseWallLine(line) : null;
463
- if (entry?.project === repoKey) latest.set(entry.machine, offset);
464
- offset += line.length + (index < lines.length - 1 ? 1 : 0);
471
+ const end = offset + line.length + (index < lines.length - 1 ? 1 : 0);
472
+ if (entry?.project === repoKey) latest.set(entry.machine, { start: offset, end });
473
+ offset = end;
465
474
  }
466
475
  if (latest.size === 0) return current;
467
- const cutoff = Math.min(...latest.values());
468
- return cutoff > 0 ? current.slice(cutoff) : current;
476
+ const records = [...latest.values()].sort((left, right) => left.start - right.start);
477
+ const gaps = [{ start: 0, end: records[0].start }];
478
+ for (let index = 0; index < records.length - 1; index += 1) {
479
+ gaps.push({ start: records[index].end, end: records[index + 1].start });
480
+ }
481
+ const gap = gaps.reduce((largest, candidate) => (
482
+ candidate.end - candidate.start > largest.end - largest.start ? candidate : largest
483
+ ));
484
+ if (gap.end <= gap.start) return current;
485
+ return `${current.slice(0, gap.start)}${current.slice(gap.end)}`;
469
486
  }
470
487
 
471
488
  function publishProject(project, rawCards) {
@@ -499,7 +516,8 @@ function publishProject(project, rawCards) {
499
516
  const removed = [...previousReplicas].some((replica) => !currentReplicas.has(replica));
500
517
  const contributes = rawCards.some((card) => {
501
518
  const checkpoint = readCheckpoint(machineId(), project.repoKey);
502
- return hasLocalContribution(card, sharedPlan, baseline, checkpoint)
519
+ const previousCard = previousEntry?.cards?.find((entry) => entry.replica === card.replica);
520
+ return hasLocalContribution(card, sharedPlan, baseline, checkpoint, previousCard)
503
521
  || hasObjectUpgrade(card, sharedPlan);
504
522
  });
505
523
  if (!removed && !contributes) {