amalgm 0.1.179 → 0.1.181

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.179",
3
+ "version": "0.1.181",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -26,7 +26,12 @@ const { closeLocalDb } = require('../state/db');
26
26
  const { captureCheckpoint, captureWorktreeDiff } = require('../workspace/git');
27
27
  const merge = require('../workspace/merge');
28
28
  const { applyPatch } = require('../workspace/transition');
29
- const { checkpointFile, revisionOf } = require('../workspace/checkpoint');
29
+ const {
30
+ checkpointFile,
31
+ checkpointValueReady,
32
+ readCheckpointValue,
33
+ revisionOf,
34
+ } = require('../workspace/checkpoint');
30
35
 
31
36
  test.after(() => {
32
37
  closeLocalDb();
@@ -161,3 +166,17 @@ test('checkpoint captures are local cache; their operations travel on the one wa
161
166
  assert.notEqual(first, checkpointFile('m1', 'https://github.com/a/c'));
162
167
  assert.match(first, /repo-checkpoints-v5/);
163
168
  });
169
+
170
+ test('an uploading checkpoint payload is not published as a transition', () => {
171
+ const pending = {
172
+ revision: 'a'.repeat(64),
173
+ payload: {
174
+ kind: 'pending',
175
+ contract: 'opaque-chunks@1',
176
+ checksum: 'b'.repeat(64),
177
+ },
178
+ };
179
+ assert.equal(checkpointValueReady(pending), false);
180
+ assert.equal(readCheckpointValue('c'.repeat(40), pending).state, 'payload-pending');
181
+ assert.equal(checkpointValueReady({ revision: null, indexDiff: '', worktreeDiff: '' }), true);
182
+ });
@@ -22,7 +22,14 @@ fs.writeFileSync(
22
22
 
23
23
  const { closeLocalDb } = require('../state/db');
24
24
  const { revisionOf } = require('../workspace/checkpoint');
25
- const { hydrateRepo, materializeRepo, readRepoState, reduceWall } = require('../workspace/reducer');
25
+ const {
26
+ captureIsOnRail,
27
+ hydrateRepo,
28
+ materializeRepo,
29
+ readRepoState,
30
+ reduceCheckpoint,
31
+ reduceWall,
32
+ } = require('../workspace/reducer');
26
33
 
27
34
  test.after(() => {
28
35
  closeLocalDb();
@@ -123,6 +130,24 @@ test('a stale lower rail card cannot roll back a newer branch and commit', () =>
123
130
  assert.equal(plan.card.positionVersion, 12);
124
131
  });
125
132
 
133
+ test('an old pending payload does not block the later complete checkpoint transition', async () => {
134
+ const { repo, oid } = await makeRepo('pending-payload');
135
+ const diff = await patchFor(repo, { 'a.txt': 'a1\n' });
136
+ const pending = transition('machine-a', 2, oid, '', '');
137
+ pending.from = {
138
+ revision: 'a'.repeat(64),
139
+ payload: { kind: 'pending', contract: 'opaque-chunks@1', checksum: 'b'.repeat(64) },
140
+ };
141
+ pending.to = pending.from;
142
+ const complete = transition('machine-a', 3, oid, '', diff);
143
+ const reduced = await reduceCheckpoint(
144
+ { path: repo },
145
+ { card: card(repo, oid, 1), transitions: [pending, complete] },
146
+ );
147
+ assert.equal(reduced.state, 'ready');
148
+ assert.match(reduced.indexDiff, /a1/);
149
+ });
150
+
126
151
  test('an empty project folder hydrates from the winning card bundle without cloning', async () => {
127
152
  const sourceRepo = await makeRepo('hydrate-source');
128
153
  const source = sourceRepo.repo;
@@ -272,6 +297,39 @@ test('one materializer converges to the reduced checkpoint and later removes a r
272
297
  assert.equal(fs.readFileSync(path.join(repo, 'b.txt'), 'utf8'), 'b1\n');
273
298
  });
274
299
 
300
+ test('once local work is on the rail, a checkpoint conflict materializes exactly without echoing', async () => {
301
+ const { repo, oid } = await makeRepo('materialize-conflict');
302
+ const localPatch = await patchFor(repo, { 'a.txt': 'local answer\n' });
303
+ const remotePatch = await patchFor(repo, { 'a.txt': 'remote answer\n' });
304
+ const localTransition = transition('machine-local', 2, oid, '', localPatch);
305
+ const plan = {
306
+ repoKey: 'root:test',
307
+ card: card(repo, oid, 1),
308
+ transitions: [
309
+ localTransition,
310
+ transition('machine-remote', 3, oid, '', remotePatch),
311
+ ],
312
+ };
313
+
314
+ fs.writeFileSync(path.join(repo, 'a.txt'), 'local answer\n');
315
+ await git(repo, 'add', 'a.txt');
316
+ assert.equal(captureIsOnRail(plan, {
317
+ baseOid: oid,
318
+ indexDiff: localPatch,
319
+ worktreeDiff: '',
320
+ }), true);
321
+
322
+ const outcome = await materializeRepo(plan.card, plan);
323
+ assert.equal(outcome.state, 'conflict');
324
+ const first = fs.readFileSync(path.join(repo, 'a.txt'), 'utf8');
325
+ assert.match(first, /local answer/);
326
+ assert.match(first, /remote answer/);
327
+ assert.match(first, /<<<<<<< current/);
328
+
329
+ await materializeRepo(plan.card, plan);
330
+ assert.equal(fs.readFileSync(path.join(repo, 'a.txt'), 'utf8'), first);
331
+ });
332
+
275
333
  test('a writer publishing again cannot erase its earlier uncommitted work', async () => {
276
334
  const { repo, oid } = await makeRepo('checkpoint-history');
277
335
  const patchA = await patchFor(repo, { 'a.txt': 'a1\n' });
@@ -119,10 +119,17 @@ function checkpointValue(machine, repoKey, baseOid, indexDiff, worktreeDiff) {
119
119
  };
120
120
  }
121
121
 
122
+ function checkpointValueReady(value) {
123
+ return !value?.payload || value.payload.kind === 'chunks';
124
+ }
125
+
122
126
  function readCheckpointValue(baseOid, value) {
123
127
  let indexDiff = typeof value?.indexDiff === 'string' ? value.indexDiff : '';
124
128
  let worktreeDiff = typeof value?.worktreeDiff === 'string' ? value.worktreeDiff : '';
125
129
  if (value?.payload) {
130
+ if (value.payload.kind === 'pending') {
131
+ return { state: 'payload-pending', indexDiff: null, worktreeDiff: null };
132
+ }
126
133
  const payload = require('./objects').readPayload(value.payload);
127
134
  if (payload.state !== 'ready') {
128
135
  return { state: payload.state, indexDiff: null, worktreeDiff: null };
@@ -203,26 +210,32 @@ async function publishPass(replicaId = null) {
203
210
  continue;
204
211
  }
205
212
 
213
+ const from = checkpointValue(
214
+ machine,
215
+ repoKey,
216
+ card.head.oid,
217
+ reduced.indexDiff,
218
+ reduced.worktreeDiff,
219
+ );
220
+ const to = checkpointValue(
221
+ machine,
222
+ repoKey,
223
+ card.head.oid,
224
+ captured.indexDiff,
225
+ captured.worktreeDiff,
226
+ );
227
+ if (!checkpointValueReady(from) || !checkpointValueReady(to)) {
228
+ run.waiting += 1;
229
+ continue;
230
+ }
206
231
  const envelope = {
207
232
  checkpoint: CHECKPOINT_CONTRACT,
208
233
  machine,
209
234
  repoKey,
210
235
  branch: card.head.branch,
211
236
  baseOid: card.head.oid,
212
- from: checkpointValue(
213
- machine,
214
- repoKey,
215
- card.head.oid,
216
- reduced.indexDiff,
217
- reduced.worktreeDiff,
218
- ),
219
- to: checkpointValue(
220
- machine,
221
- repoKey,
222
- card.head.oid,
223
- captured.indexDiff,
224
- captured.worktreeDiff,
225
- ),
237
+ from,
238
+ to,
226
239
  parked,
227
240
  capturedAt: new Date().toISOString(),
228
241
  };
@@ -322,6 +335,7 @@ module.exports = {
322
335
  MAX_INLINE_DIFF_BYTES,
323
336
  checkpointFile,
324
337
  checkpointStats,
338
+ checkpointValueReady,
325
339
  parseCheckpoint,
326
340
  publishCheckpoints,
327
341
  readCheckpoint,
@@ -32,6 +32,7 @@ const {
32
32
  repoKeyOfCard,
33
33
  revisionOf,
34
34
  } = require('./checkpoint');
35
+ const { machineId } = require('./identity');
35
36
  const merge = require('./merge');
36
37
  const transition = require('./transition');
37
38
 
@@ -378,6 +379,7 @@ async function reduceCheckpoint(localCard, plan) {
378
379
  for (const operation of plan.transitions) {
379
380
  const from = readCheckpointValue(baseOid, operation.from);
380
381
  const to = readCheckpointValue(baseOid, operation.to);
382
+ if (from.state === 'payload-pending' || to.state === 'payload-pending') continue;
381
383
  if (from.state !== 'ready' || to.state !== 'ready') {
382
384
  return { state: 'waiting', reason: `checkpoint-${from.state !== 'ready' ? from.state : to.state}` };
383
385
  }
@@ -415,6 +417,25 @@ async function reduceCheckpoint(localCard, plan) {
415
417
  };
416
418
  }
417
419
 
420
+ function captureIsOnRail(plan, captured, localMachine = machineId()) {
421
+ if (
422
+ !captured
423
+ || captured.indexDiff === null
424
+ || captured.baseOid !== plan.card.head.oid
425
+ ) return false;
426
+ const latest = [...plan.transitions]
427
+ .reverse()
428
+ .find((operation) => operation.machine === localMachine);
429
+ return Boolean(
430
+ latest
431
+ && latest.to?.revision === revisionOf(
432
+ captured.baseOid,
433
+ captured.indexDiff,
434
+ captured.worktreeDiff,
435
+ ),
436
+ );
437
+ }
438
+
418
439
  async function materializeRepo(localCardInput, plan) {
419
440
  const refreshed = await refreshRepoCard(localCardInput.replica) || localCardInput;
420
441
  const cardResult = await moveToWinningCard(refreshed, plan.card);
@@ -435,16 +456,28 @@ async function materializeRepo(localCardInput, plan) {
435
456
  if (reduced.state !== 'ready') return reduced;
436
457
 
437
458
  const baseTree = await merge.treeOf(localCard.path, plan.card.head.oid);
438
- const heldWorktree = previous?.baseOid === plan.card.head.oid
439
- && previous.heldTree
440
- && await merge.hasTree(localCard.path, previous.heldTree)
441
- ? previous.heldTree
442
- : baseTree;
443
- const heldIndex = previous?.baseOid === plan.card.head.oid
444
- && previous.heldIndexTree
445
- && await merge.hasTree(localCard.path, previous.heldIndexTree)
446
- ? previous.heldIndexTree
447
- : baseTree;
459
+ const captured = await captureCheckpoint(localCard.path);
460
+ const represented = captureIsOnRail(plan, captured);
461
+ const capturedIndex = represented
462
+ ? await merge.treeOfPatch(localCard.path, plan.card.head.oid, captured.indexDiff)
463
+ : null;
464
+ const capturedWorktree = represented
465
+ ? await merge.treeOfPatch(localCard.path, capturedIndex, captured.worktreeDiff)
466
+ : null;
467
+ const heldWorktree = capturedWorktree || (
468
+ previous?.baseOid === plan.card.head.oid
469
+ && previous.heldTree
470
+ && await merge.hasTree(localCard.path, previous.heldTree)
471
+ ? previous.heldTree
472
+ : baseTree
473
+ );
474
+ const heldIndex = capturedIndex || (
475
+ previous?.baseOid === plan.card.head.oid
476
+ && previous.heldIndexTree
477
+ && await merge.hasTree(localCard.path, previous.heldIndexTree)
478
+ ? previous.heldIndexTree
479
+ : baseTree
480
+ );
448
481
  const currentIndex = await merge.indexTree(localCard.path);
449
482
  const reducedLocalIndex = await merge.reduceTree(
450
483
  localCard.path,
@@ -641,9 +674,11 @@ function reducerStats() {
641
674
  }
642
675
 
643
676
  module.exports = {
677
+ captureIsOnRail,
644
678
  hydrateRepo,
645
679
  materializeRepo,
646
680
  readRepoState,
681
+ reduceCheckpoint,
647
682
  reduceNow,
648
683
  reduceWall,
649
684
  reducerStats,