amalgm 0.1.245 → 0.1.246

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 (157) hide show
  1. package/README.md +12 -0
  2. package/lib/cli.js +146 -8
  3. package/lib/layout.js +89 -4
  4. package/lib/shared-realtime-tunnel.js +384 -43
  5. package/lib/supervisor.js +0 -3
  6. package/package.json +2 -2
  7. package/runtime/scripts/amalgm-mcp/adapters/contract.js +197 -0
  8. package/runtime/scripts/amalgm-mcp/adapters/filesystem.js +135 -0
  9. package/runtime/scripts/amalgm-mcp/adapters/git.js +84 -0
  10. package/runtime/scripts/amalgm-mcp/adapters/index.js +43 -0
  11. package/runtime/scripts/amalgm-mcp/adapters/reference.js +31 -0
  12. package/runtime/scripts/amalgm-mcp/adapters/truth.js +19 -0
  13. package/runtime/scripts/amalgm-mcp/browser/cookie-jar.js +20 -68
  14. package/runtime/scripts/amalgm-mcp/index.js +4 -0
  15. package/runtime/scripts/amalgm-mcp/lib/layout.js +89 -4
  16. package/runtime/scripts/amalgm-mcp/observer/README.md +259 -0
  17. package/runtime/scripts/amalgm-mcp/observer/apply.js +150 -0
  18. package/runtime/scripts/amalgm-mcp/observer/continuity.js +111 -0
  19. package/runtime/scripts/amalgm-mcp/observer/edges.js +111 -0
  20. package/runtime/scripts/amalgm-mcp/observer/index.js +1146 -0
  21. package/runtime/scripts/amalgm-mcp/observer/scan.js +387 -0
  22. package/runtime/scripts/amalgm-mcp/observer/store.js +262 -0
  23. package/runtime/scripts/amalgm-mcp/observer/verify.js +200 -0
  24. package/runtime/scripts/amalgm-mcp/observer/watch.js +62 -0
  25. package/runtime/scripts/amalgm-mcp/project-context/store.js +19 -87
  26. package/runtime/scripts/amalgm-mcp/registration/classify.js +33 -0
  27. package/runtime/scripts/amalgm-mcp/registration/entity-cloud.js +456 -0
  28. package/runtime/scripts/amalgm-mcp/registration/entity-content.js +345 -0
  29. package/runtime/scripts/amalgm-mcp/registration/index.js +752 -0
  30. package/runtime/scripts/amalgm-mcp/registration/refusal.js +42 -0
  31. package/runtime/scripts/amalgm-mcp/registration/repo-followers.js +127 -0
  32. package/runtime/scripts/amalgm-mcp/registration/repo-states.js +109 -0
  33. package/runtime/scripts/amalgm-mcp/registration/service.js +310 -0
  34. package/runtime/scripts/amalgm-mcp/registration/tree.js +178 -0
  35. package/runtime/scripts/amalgm-mcp/registry/evidence.js +826 -0
  36. package/runtime/scripts/amalgm-mcp/registry/index.js +666 -0
  37. package/runtime/scripts/amalgm-mcp/registry/store.js +290 -0
  38. package/runtime/scripts/amalgm-mcp/repocard/README.md +103 -0
  39. package/runtime/scripts/amalgm-mcp/repocard/apply.js +111 -0
  40. package/runtime/scripts/amalgm-mcp/repocard/capture-worker.js +94 -0
  41. package/runtime/scripts/amalgm-mcp/repocard/capture.js +149 -0
  42. package/runtime/scripts/amalgm-mcp/repocard/follow.js +310 -0
  43. package/runtime/scripts/amalgm-mcp/repocard/git.js +48 -0
  44. package/runtime/scripts/amalgm-mcp/repocard/index.js +48 -0
  45. package/runtime/scripts/amalgm-mcp/server/local-service-router.js +2 -0
  46. package/runtime/scripts/amalgm-mcp/server/routes/entities.js +287 -0
  47. package/runtime/scripts/amalgm-mcp/server/routes/state.js +0 -13
  48. package/runtime/scripts/amalgm-mcp/state/attachments.js +11 -45
  49. package/runtime/scripts/amalgm-mcp/state/db.js +46 -215
  50. package/runtime/scripts/amalgm-mcp/state/docs.js +301 -1101
  51. package/runtime/scripts/amalgm-mcp/state/mutation-contracts.js +0 -99
  52. package/runtime/scripts/amalgm-mcp/state/mutations.js +130 -507
  53. package/runtime/scripts/amalgm-mcp/state/promotions.js +1 -21
  54. package/runtime/scripts/amalgm-mcp/state/replicas.js +13 -134
  55. package/runtime/scripts/amalgm-mcp/state/shared-rest.js +3 -46
  56. package/runtime/scripts/amalgm-mcp/tests/adapters.test.js +379 -0
  57. package/runtime/scripts/amalgm-mcp/tests/browser-cookie-cloud.test.js +0 -193
  58. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.life.test.js +449 -0
  59. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.rig.js +107 -0
  60. package/runtime/scripts/amalgm-mcp/tests/doorbell.matrix.watch.test.js +213 -0
  61. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/adapter.js +72 -0
  62. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/assert.js +497 -0
  63. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/bind.js +91 -0
  64. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/graph.js +1031 -0
  65. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/ledger.js +234 -0
  66. package/runtime/scripts/amalgm-mcp/tests/entity-oracle/referee.js +118 -0
  67. package/runtime/scripts/amalgm-mcp/tests/entity.oracle.test.js +1365 -0
  68. package/runtime/scripts/amalgm-mcp/tests/entity.registry.test.js +967 -0
  69. package/runtime/scripts/amalgm-mcp/tests/entity.rig.js +239 -0
  70. package/runtime/scripts/amalgm-mcp/tests/entity.storm.test.js +486 -0
  71. package/runtime/scripts/amalgm-mcp/tests/fake-watch.js +35 -0
  72. package/runtime/scripts/amalgm-mcp/tests/observer.links.test.js +289 -0
  73. package/runtime/scripts/amalgm-mcp/tests/observer.rig.js +103 -0
  74. package/runtime/scripts/amalgm-mcp/tests/observer.roots.test.js +505 -0
  75. package/runtime/scripts/amalgm-mcp/tests/observer.storm.test.js +389 -0
  76. package/runtime/scripts/amalgm-mcp/tests/observer.territory.test.js +845 -0
  77. package/runtime/scripts/amalgm-mcp/tests/observer.test.js +657 -0
  78. package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +17 -32
  79. package/runtime/scripts/amalgm-mcp/tests/registration.service.test.js +256 -0
  80. package/runtime/scripts/amalgm-mcp/tests/registration.test.js +931 -0
  81. package/runtime/scripts/amalgm-mcp/tests/repocard.storm.test.js +284 -0
  82. package/runtime/scripts/amalgm-mcp/tests/repocard.test.js +371 -0
  83. package/runtime/scripts/amalgm-mcp/tests/repofollow.storm.test.js +435 -0
  84. package/runtime/scripts/amalgm-mcp/tests/repofollow.test.js +675 -0
  85. package/runtime/scripts/amalgm-mcp/tests/shared-tunnel-rehydration.test.js +0 -84
  86. package/runtime/scripts/amalgm-mcp/tests/state-docs.test.js +13 -376
  87. package/runtime/scripts/amalgm-mcp/tests/state-mutations.test.js +0 -309
  88. package/runtime/scripts/amalgm-mcp/tests/state-shared-replica.test.js +30 -758
  89. package/runtime/scripts/amalgm-mcp/tests/workspace-cards-store.test.js +3 -72
  90. package/runtime/scripts/amalgm-mcp/tests/workspace-cards.test.js +1 -89
  91. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint-reducer.test.js +24 -55
  92. package/runtime/scripts/amalgm-mcp/tests/workspace-checkpoint.test.js +141 -424
  93. package/runtime/scripts/amalgm-mcp/tests/workspace-object-tunnel.test.js +3 -171
  94. package/runtime/scripts/amalgm-mcp/tests/workspace-objects.test.js +7 -220
  95. package/runtime/scripts/amalgm-mcp/tests/workspace-publication-invariants.test.js +236 -179
  96. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer.test.js +326 -2228
  97. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-cloud.test.js +3 -5319
  98. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-core.test.js +1 -191
  99. package/runtime/scripts/amalgm-mcp/tests/workspace-tree-store.test.js +17 -3140
  100. package/runtime/scripts/amalgm-mcp/tests/workspace-wall.test.js +740 -137
  101. package/runtime/scripts/amalgm-mcp/workspace/access-store.js +1 -5
  102. package/runtime/scripts/amalgm-mcp/workspace/card.js +11 -35
  103. package/runtime/scripts/amalgm-mcp/workspace/cards.js +14 -39
  104. package/runtime/scripts/amalgm-mcp/workspace/checkpoint.js +147 -587
  105. package/runtime/scripts/amalgm-mcp/workspace/git.js +34 -406
  106. package/runtime/scripts/amalgm-mcp/workspace/merge.js +181 -102
  107. package/runtime/scripts/amalgm-mcp/workspace/objects.js +56 -221
  108. package/runtime/scripts/amalgm-mcp/workspace/reducer.js +538 -1964
  109. package/runtime/scripts/amalgm-mcp/workspace/rest.js +28 -49
  110. package/runtime/scripts/amalgm-mcp/workspace/store.js +4 -77
  111. package/runtime/scripts/amalgm-mcp/workspace/transition.js +192 -1734
  112. package/runtime/scripts/amalgm-mcp/workspace/tree/inclusion.js +1 -7
  113. package/runtime/scripts/amalgm-mcp/workspace/tree/local-state.js +61 -73
  114. package/runtime/scripts/amalgm-mcp/workspace/tree/reducer.js +6 -51
  115. package/runtime/scripts/amalgm-mcp/workspace/tree/snapshot.js +8 -79
  116. package/runtime/scripts/amalgm-mcp/workspace/tree-cloud.js +146 -3461
  117. package/runtime/scripts/amalgm-mcp/workspace/tree-store.js +71 -899
  118. package/runtime/scripts/amalgm-mcp/workspace/wall.js +538 -370
  119. package/runtime/scripts/local-gateway.js +1 -0
  120. package/runtime/scripts/amalgm-mcp/state/doc-disk.js +0 -550
  121. package/runtime/scripts/amalgm-mcp/tests/code-project-stream.test.js +0 -399
  122. package/runtime/scripts/amalgm-mcp/tests/fixtures/code-project-stream-v2.json +0 -1
  123. package/runtime/scripts/amalgm-mcp/tests/workspace-durable.test.js +0 -24
  124. package/runtime/scripts/amalgm-mcp/tests/workspace-local-intent.test.js +0 -1999
  125. package/runtime/scripts/amalgm-mcp/tests/workspace-native-file-transaction.test.js +0 -364
  126. package/runtime/scripts/amalgm-mcp/tests/workspace-provenance.test.js +0 -185
  127. package/runtime/scripts/amalgm-mcp/tests/workspace-reducer-liveness.test.js +0 -26
  128. package/runtime/scripts/amalgm-mcp/tests/workspace-sync-stats.test.js +0 -119
  129. package/runtime/scripts/amalgm-mcp/tests/workspace-transition.test.js +0 -1446
  130. package/runtime/scripts/amalgm-mcp/workspace/content.js +0 -32
  131. package/runtime/scripts/amalgm-mcp/workspace/durable.js +0 -45
  132. package/runtime/scripts/amalgm-mcp/workspace/intent.js +0 -535
  133. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/BUILD.md +0 -31
  134. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/darwin-arm64/amalgm-file-transaction +0 -0
  135. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/bin/linux-x64/amalgm-file-transaction +0 -0
  136. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/build.mjs +0 -72
  137. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/client.js +0 -165
  138. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.mod +0 -5
  139. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/go.sum +0 -2
  140. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/main.go +0 -2201
  141. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/manifest.json +0 -19
  142. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_darwin.go +0 -18
  143. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/move_linux.go +0 -18
  144. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_darwin.go +0 -33
  145. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/process_linux.go +0 -47
  146. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/verify.mjs +0 -43
  147. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_darwin.go +0 -114
  148. package/runtime/scripts/amalgm-mcp/workspace/native-file-transaction/writers_linux.go +0 -84
  149. package/runtime/scripts/amalgm-mcp/workspace/project-stream.js +0 -771
  150. package/runtime/scripts/amalgm-mcp/workspace/provenance.js +0 -240
  151. package/runtime/scripts/amalgm-mcp/workspace/sync-stats.js +0 -240
  152. package/runtime/scripts/amalgm-mcp/workspace/tree/floor.js +0 -100
  153. package/runtime/scripts/amalgm-mcp/workspace/tree/observation-holds.js +0 -127
  154. package/runtime/scripts/amalgm-mcp/workspace/tree/path-state.js +0 -12
  155. package/runtime/scripts/amalgm-mcp/workspace/tree/root-binding.js +0 -125
  156. package/runtime/scripts/amalgm-mcp/workspace/tree/symlinks.js +0 -76
  157. package/runtime/scripts/amalgm-mcp/workspace/tree-materialization.js +0 -3766
@@ -213,87 +213,3 @@ test('a pruned-tail join answer triggers rehydration through the local state ser
213
213
  server.close();
214
214
  }
215
215
  });
216
-
217
- test('an authorized attachment with a local materialization failure stays retryable', async () => {
218
- const calls = [];
219
- let attachmentClaimed = false;
220
- const server = http.createServer((req, res) => {
221
- let body = '';
222
- req.on('data', (chunk) => { body += chunk; });
223
- req.on('end', () => {
224
- const parsed = body ? JSON.parse(body) : null;
225
- calls.push({ path: req.url, body: parsed });
226
- res.setHeader('content-type', 'application/json');
227
- if (req.url === '/state/shared/replicas') {
228
- res.end('{"replicas":[]}');
229
- return;
230
- }
231
- if (req.url.startsWith('/state/stream')) {
232
- res.writeHead(200, { 'content-type': 'text/event-stream' });
233
- res.write('\n');
234
- return;
235
- }
236
- if (req.url === '/state/shared/attachments/claim') {
237
- const attachments = attachmentClaimed
238
- ? []
239
- : [{
240
- attachmentId: 'att_local_materialize_retry_01',
241
- resourceId: 'shr_localretrytest01',
242
- }];
243
- attachmentClaimed = true;
244
- res.end(JSON.stringify({ attachments }));
245
- return;
246
- }
247
- if (req.url === '/state/shared/attachments/ack') {
248
- res.statusCode = 409;
249
- res.end('{"error":"local disk changed during snapshot install"}');
250
- return;
251
- }
252
- res.end('{"mutations":[],"promotions":[],"attachments":[]}');
253
- });
254
- });
255
- await new Promise((resolve) => { server.listen(0, '127.0.0.1', resolve); });
256
- const sent = [];
257
- const tunnel = createSharedRealtimeTunnel({
258
- send: (frame) => { sent.push(frame); return true; },
259
- sendLarge: (frame) => { sent.push(frame); return true; },
260
- localPort: () => server.address().port,
261
- localHeaders: (headers) => ({ ...headers }),
262
- warn: () => {},
263
- });
264
- try {
265
- tunnel.enable(['shared.mutation.v1']);
266
- await until(
267
- () => sent.some((frame) => frame.type === 'shared.resource.attach'),
268
- 'the attachment request',
269
- );
270
- const request = sent.find((frame) => frame.type === 'shared.resource.attach');
271
- tunnel.handleFrame({
272
- type: 'shared.resource.attached',
273
- request_id: request.request_id,
274
- resource_id: request.resource_id,
275
- authority_epoch: 1,
276
- head_version: 0,
277
- snapshot_version: 0,
278
- snapshot_checksum: 'a'.repeat(64),
279
- snapshot_b64: 'AA==',
280
- contract: 'text-yjs@1',
281
- schema_version: 1,
282
- });
283
- await until(
284
- () => calls.some((call) => call.path === '/state/shared/attachments/fail'),
285
- 'the retryable local attachment failure',
286
- );
287
- assert.deepEqual(
288
- calls.find((call) => call.path === '/state/shared/attachments/fail').body,
289
- {
290
- attachmentId: 'att_local_materialize_retry_01',
291
- message: 'local disk changed during snapshot install',
292
- retryable: true,
293
- },
294
- );
295
- } finally {
296
- tunnel.stop();
297
- server.close();
298
- }
299
- });
@@ -2,11 +2,9 @@
2
2
 
3
3
  const test = require('node:test');
4
4
  const assert = require('node:assert/strict');
5
- const crypto = require('node:crypto');
6
5
  const fs = require('fs');
7
6
  const os = require('os');
8
7
  const path = require('path');
9
- const { spawn } = require('node:child_process');
10
8
 
11
9
  const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-live-docs-test-'));
12
10
  process.env.AMALGM_DIR = path.join(tempRoot, '.amalgm');
@@ -30,19 +28,6 @@ function sleep(ms) {
30
28
  return new Promise((resolve) => setTimeout(resolve, ms));
31
29
  }
32
30
 
33
- function sha256(text) {
34
- return crypto.createHash('sha256').update(text, 'utf8').digest('hex');
35
- }
36
-
37
- function writeFromAnotherProcessAfter(file, text, delayMs) {
38
- return spawn(process.execPath, [
39
- '-e',
40
- 'const fs=require("fs");'
41
- + `setTimeout(()=>fs.writeFileSync(${JSON.stringify(file)},`
42
- + `${JSON.stringify(text)},"utf8"),${delayMs});`,
43
- ], { stdio: 'ignore' });
44
- }
45
-
46
31
  function clientFromState(stateBase64) {
47
32
  const doc = new Y.Doc();
48
33
  Y.applyUpdate(doc, new Uint8Array(Buffer.from(stateBase64, 'base64')));
@@ -85,7 +70,7 @@ test('open seeds the document from disk and returns usable state', () => {
85
70
  assert.equal(client.getText('content').toString(), '# hello\n');
86
71
  });
87
72
 
88
- test('client updates apply, log as patch events, and materialize to disk', async () => {
73
+ test('client updates apply, log as patch events, and flush back to disk', async () => {
89
74
  const target = writeDoc('edit.md', 'hello world\n');
90
75
  const opened = docs.openDoc(target);
91
76
  const client = clientFromState(opened.state);
@@ -104,7 +89,7 @@ test('client updates apply, log as patch events, and materialize to disk', async
104
89
  assert.ok(events[0].patch?.yjs);
105
90
  assert.equal(events[0].value, undefined);
106
91
 
107
- // The saved-local response already stands on a crossed file transaction.
92
+ // Debounced write-back lands on disk.
108
93
  await sleep(500);
109
94
  assert.equal(fs.readFileSync(target, 'utf8'), 'hello, world\n');
110
95
  });
@@ -134,14 +119,14 @@ test('two clients editing concurrently converge to the same text', () => {
134
119
  assert.equal(clientB.getText('content').toString(), engineText);
135
120
  });
136
121
 
137
- test('bounded verification reconciles a dropped external disk notification', () => {
122
+ test('external disk edits reconcile into the live doc via the watcher', async () => {
138
123
  const target = writeDoc('agent.md', 'agent line 1\n');
139
124
  const opened = docs.openDoc(target);
140
125
  const before = currentSeq();
141
126
 
142
127
  // An "agent" writes the file directly, bypassing the doc API.
143
128
  fs.writeFileSync(target, 'agent line 1\nagent line 2\n', 'utf8');
144
- docs._private.verifyOpenDocs();
129
+ await sleep(700);
145
130
 
146
131
  assert.equal(docs.getDocText(target), 'agent line 1\nagent line 2\n');
147
132
  const events = listEventsAfter(before).filter((e) => e.resource === opened.resource);
@@ -149,243 +134,6 @@ test('bounded verification reconciles a dropped external disk notification', ()
149
134
  assert.equal(events[events.length - 1].source, 'doc:disk');
150
135
  });
151
136
 
152
- test('a dropped watcher cannot let a later versioned API write erase the disk save', () => {
153
- const base = 'left\nright\n';
154
- const target = writeDoc('dropped-watcher-api.md', base);
155
- docs.openDoc(target);
156
-
157
- fs.writeFileSync(target, 'left from disk\nright\n', 'utf8');
158
- const written = docs.writeDocText(target, 'left\nright from api\n', {
159
- baseHash: sha256(base),
160
- mutationId: 'dropped_watcher_api_001',
161
- });
162
-
163
- assert.equal(written.merge, 'merged');
164
- assert.equal(docs.getDocText(target), 'left from disk\nright from api\n');
165
- assert.equal(fs.readFileSync(target, 'utf8'), 'left from disk\nright from api\n');
166
- });
167
-
168
- test('the final native CAS retains a save that lands during an API materialization', () => {
169
- const base = 'first\nsecond\n';
170
- const target = writeDoc('api-final-cas.md', base);
171
- docs.openDoc(target);
172
- const child = writeFromAnotherProcessAfter(
173
- target,
174
- 'first from disk\nsecond\n',
175
- 75,
176
- );
177
- docs._private.setDiskTransactionTestHooks({ pauseBeforeInstall: 350 });
178
- try {
179
- docs.writeDocText(target, 'first\nsecond from api\n', {
180
- baseHash: sha256(base),
181
- mutationId: 'api_final_cas_race_001',
182
- });
183
- } finally {
184
- docs._private.setDiskTransactionTestHooks();
185
- }
186
- child.unref();
187
- assert.equal(docs.getDocText(target), 'first from disk\nsecond from api\n');
188
- assert.equal(fs.readFileSync(target, 'utf8'), 'first from disk\nsecond from api\n');
189
- });
190
-
191
- test('the API plan stays pinned when disk changes after merge derivation', () => {
192
- const base = 'first\nsecond\n';
193
- const target = writeDoc('api-derived-base-cas.md', base);
194
- docs.openDoc(target);
195
- const child = writeFromAnotherProcessAfter(
196
- target,
197
- 'first from disk\nsecond\n',
198
- 75,
199
- );
200
- docs._private.setDiskTransactionTestHooks({ pauseAfterEditBeforeJournal: 350 });
201
- let written;
202
- try {
203
- written = docs.writeDocText(target, 'first\nsecond from api\n', {
204
- baseHash: sha256(base),
205
- mutationId: 'api_derived_base_cas_001',
206
- });
207
- } finally {
208
- docs._private.setDiskTransactionTestHooks();
209
- }
210
- child.unref();
211
- assert.equal(docs.getDocText(target), 'first from disk\nsecond from api\n');
212
- assert.equal(fs.readFileSync(target, 'utf8'), 'first from disk\nsecond from api\n');
213
- assert.equal(written.sha256, sha256('first from disk\nsecond from api\n'));
214
- });
215
-
216
- test('a save after native install becomes the successor during receipt recovery', () => {
217
- const base = 'first\nsecond\n';
218
- const target = writeDoc('api-post-install-successor.md', base);
219
- docs.openDoc(target);
220
-
221
- docs._private.setDiskTransactionTestHooks({ crashAfterInstall: true });
222
- try {
223
- assert.throws(
224
- () => docs.writeDocText(target, 'first\nsecond from api\n', {
225
- baseHash: sha256(base),
226
- mutationId: 'api_post_install_successor_001',
227
- }),
228
- /native file transaction failed/,
229
- );
230
- } finally {
231
- docs._private.setDiskTransactionTestHooks();
232
- }
233
- assert.equal(fs.readFileSync(target, 'utf8'), 'first\nsecond from api\n');
234
-
235
- // The helper's receipt proves the API target crossed. This later save is B,
236
- // not evidence that A failed, and must become a durable successor.
237
- fs.writeFileSync(target, 'first from disk\nsecond\n', 'utf8');
238
- docs._private.verifyOpenDocs();
239
-
240
- assert.equal(docs.getDocText(target), 'first from disk\nsecond from api\n');
241
- assert.equal(fs.readFileSync(target, 'utf8'), 'first from disk\nsecond from api\n');
242
- });
243
-
244
- test('UUID ownership recovers a crossed edit after its parent folder is renamed', () => {
245
- const beforeDir = path.join(docDir, 'rename-before');
246
- const afterDir = path.join(docDir, 'rename-after');
247
- fs.mkdirSync(beforeDir);
248
- const beforePath = path.join(beforeDir, 'owned.md');
249
- const afterPath = path.join(afterDir, 'owned.md');
250
- fs.writeFileSync(beforePath, 'before\n', 'utf8');
251
- docs.openDoc(beforePath);
252
-
253
- docs._private.setDiskTransactionTestHooks({ crashAfterInstall: true });
254
- try {
255
- assert.throws(
256
- () => docs.writeDocText(beforePath, 'after API\n', {
257
- baseHash: sha256('before\n'),
258
- mutationId: 'uuid_parent_rename_recovery_001',
259
- }),
260
- /native file transaction failed/,
261
- );
262
- } finally {
263
- docs._private.setDiskTransactionTestHooks();
264
- }
265
-
266
- fs.renameSync(beforeDir, afterDir);
267
- docs.rebindNodePaths([{
268
- nodeId: '11111111-1111-4111-8111-111111111111',
269
- oldPath: beforePath,
270
- newPath: afterPath,
271
- }]);
272
- docs._private.verifyOpenDocs();
273
-
274
- assert.equal(docs.getDocText(afterPath), 'after API\n');
275
- assert.equal(fs.readFileSync(afterPath, 'utf8'), 'after API\n');
276
- });
277
-
278
- test('the bounded verifier schedule reconciles a file whose watcher failed', async () => {
279
- const isolatedDir = path.join(docDir, 'scheduled-verifier');
280
- fs.mkdirSync(isolatedDir, { recursive: true });
281
- const target = path.join(isolatedDir, 'agent.md');
282
- fs.writeFileSync(target, 'before\n', 'utf8');
283
- const originalWatch = fs.watch;
284
- let watchAttempts = 0;
285
- fs.watch = (...args) => {
286
- watchAttempts += 1;
287
- if (watchAttempts === 1) {
288
- const error = new Error('simulated dropped watcher');
289
- error.code = 'EMFILE';
290
- throw error;
291
- }
292
- return originalWatch.apply(fs, args);
293
- };
294
-
295
- try {
296
- const scheduledBefore = docs._private.watchVerificationStats();
297
- const opened = docs.openDoc(target);
298
- const before = currentSeq();
299
- fs.writeFileSync(target, 'after\n', 'utf8');
300
-
301
- const deadline = Date.now() + 7_000;
302
- while (docs.getDocText(target) !== 'after\n' && Date.now() < deadline) {
303
- await sleep(50);
304
- }
305
- assert.equal(
306
- docs.getDocText(target),
307
- 'after\n',
308
- JSON.stringify({
309
- scheduledBefore,
310
- scheduledAfter: docs._private.watchVerificationStats(),
311
- watchAttempts,
312
- disk: fs.readFileSync(target, 'utf8'),
313
- }),
314
- );
315
- const events = listEventsAfter(before).filter((event) => event.resource === opened.resource);
316
- assert.equal(events.length, 1);
317
- assert.equal(events[0].source, 'doc:disk');
318
- assert.ok(watchAttempts >= 2, 'the scheduled pass retried the failed watcher');
319
- const scheduledAfter = docs._private.watchVerificationStats();
320
- assert.equal(scheduledAfter.active, true);
321
- assert.ok(scheduledAfter.passes > scheduledBefore.passes);
322
- } finally {
323
- fs.watch = originalWatch;
324
- }
325
- });
326
-
327
- test('a transient journal failure leaves the exact disk observation retryable', () => {
328
- const target = writeDoc('agent-retry.md', 'before\n');
329
- const opened = docs.openDoc(target);
330
- const before = currentSeq();
331
- const mutations = require('../state/mutations');
332
- const commitLocalMutation = mutations.commitLocalMutation;
333
- fs.writeFileSync(target, 'after\n', 'utf8');
334
- mutations.commitLocalMutation = () => {
335
- throw new Error('simulated journal outage');
336
- };
337
- try {
338
- docs._private.verifyOpenDocs();
339
- } finally {
340
- mutations.commitLocalMutation = commitLocalMutation;
341
- }
342
-
343
- assert.equal(docs.getDocText(target), 'before\n');
344
- assert.equal(
345
- listEventsAfter(before).filter((event) => event.resource === opened.resource).length,
346
- 0,
347
- );
348
-
349
- docs._private.verifyOpenDocs();
350
- assert.equal(docs.getDocText(target), 'after\n');
351
- assert.equal(fs.readFileSync(target, 'utf8'), 'after\n');
352
- const events = listEventsAfter(before).filter((event) => event.resource === opened.resource);
353
- assert.equal(events.length, 1);
354
- assert.ok(events.every((event) => event.source === 'doc:disk'));
355
- });
356
-
357
- test('a transient journal failure cannot consume a whole-text API write', () => {
358
- const target = writeDoc('api-retry.md', 'before\n');
359
- const opened = docs.openDoc(target);
360
- const before = currentSeq();
361
- const mutations = require('../state/mutations');
362
- const commitLocalMutation = mutations.commitLocalMutation;
363
- mutations.commitLocalMutation = () => {
364
- throw new Error('simulated journal outage');
365
- };
366
- try {
367
- assert.throws(
368
- () => docs.writeDocText(target, 'after\n', { mutationId: 'api_retry_write_0001' }),
369
- /simulated journal outage/,
370
- );
371
- } finally {
372
- mutations.commitLocalMutation = commitLocalMutation;
373
- }
374
-
375
- assert.equal(docs.getDocText(target), 'before\n');
376
- assert.equal(fs.readFileSync(target, 'utf8'), 'before\n');
377
- assert.equal(
378
- listEventsAfter(before).filter((event) => event.resource === opened.resource).length,
379
- 0,
380
- );
381
-
382
- docs.writeDocText(target, 'after\n', { mutationId: 'api_retry_write_0001' });
383
- assert.equal(docs.getDocText(target), 'after\n');
384
- const events = listEventsAfter(before).filter((event) => event.resource === opened.resource);
385
- assert.equal(events.length, 1);
386
- assert.equal(events[0].source, 'doc:write');
387
- });
388
-
389
137
  test('internal protocol documents never treat their own disk flush as another writer', async () => {
390
138
  const checkpointDir = path.join(process.env.AMALGM_DIR, 'state', 'repo-checkpoints');
391
139
  const projectDir = path.join(process.env.AMALGM_DIR, 'state', 'code-projects');
@@ -437,7 +185,7 @@ test('documents survive close: persisted state + disk agree on reopen', () => {
437
185
  const update = encodeLocalEdit(client, (ytext) => ytext.insert(0, 'v2 over '));
438
186
  docs.applyDocUpdates(target, [update]);
439
187
 
440
- docs.closeAllDocs(); // captures final disk observations and releases memory
188
+ docs.closeAllDocs(); // flushes disk + state
441
189
 
442
190
  const reopened = docs.openDoc(target);
443
191
  const fresh = clientFromState(reopened.state);
@@ -476,33 +224,8 @@ test('guards: binary, missing, oversized, and malformed inputs are rejected', ()
476
224
  assert.throws(() => docs.applyDocUpdates(target, ['aGVsbG8=']), /invalid Yjs update/);
477
225
  });
478
226
 
479
- test('all managed document writes cross the one native materialization boundary', () => {
480
- const source = fs.readFileSync(require.resolve('../state/docs'), 'utf8');
481
- const rawWriteTargets = Array.from(
482
- source.matchAll(/fs\.writeFileSync\(\s*([^,\n]+)/g),
483
- (match) => match[1].trim(),
484
- );
485
-
486
- assert.deepEqual(
487
- rawWriteTargets,
488
- ['sidecarPath'],
489
- 'only the independent metadata sidecar may bypass document materialization',
490
- );
491
- assert.doesNotMatch(source, /\bflushDocToDisk\b|\bwriteFileAtomic\b|fs\.renameSync\(/);
492
- assert.match(source, /docDisk\.cross\(/);
493
- assert.equal(
494
- (source.match(/docDisk\.insertSnapshotBase\(/g) || []).length,
495
- 1,
496
- 'every snapshot attempt must acquire its physical base through one capture-and-pin helper',
497
- );
498
- assert.match(
499
- source,
500
- /function planCloudDocSnapshot[\s\S]*?captureDiskBeforeMutation[\s\S]*?docDisk\.insertSnapshotBase/,
501
- );
502
- });
503
-
504
227
  // ---------------------------------------------------------------------------
505
- // Durability: a crash (SIGKILL — no graceful close) must never lose
228
+ // Durability: a crash (SIGKILL — no flush, no graceful close) must never lose
506
229
  // an acknowledged edit. The contract is per acknowledgment, not per update:
507
230
  // state is persisted before applyDocUpdates returns (before the POST response
508
231
  // acknowledges); the recorded last-disk hash lets reopen tell "disk is merely
@@ -511,20 +234,20 @@ test('all managed document writes cross the one native materialization boundary'
511
234
 
512
235
  function crashAndReloadDocs() {
513
236
  // Simulate SIGKILL: drop the module (and its open docs, timers untouched)
514
- // without graceful cleanup. The reloaded module sees only durable state.
237
+ // without any flush. The reloaded module sees only what was persisted.
515
238
  delete require.cache[require.resolve('../state/docs')];
516
239
  return require('../state/docs');
517
240
  }
518
241
 
519
- test('an acknowledged edit is on disk before a kill and remains current on reopen', () => {
242
+ test('a kill inside the disk-write debounce loses nothing: reopen restores the edit', async () => {
520
243
  const target = writeDoc('crash.md', 'before the crash\n');
521
244
  const opened = docs.openDoc(target);
522
245
  const client = clientFromState(opened.state);
523
246
  const update = encodeLocalEdit(client, (ytext) => ytext.insert(0, 'acknowledged edit / '));
524
247
  docs.applyDocUpdates(target, [update]);
525
248
 
526
- // Acknowledgment is earned only after the native file transaction crossed.
527
- assert.equal(fs.readFileSync(target, 'utf8'), 'acknowledged edit / before the crash\n');
249
+ // Crash immediately — the 300ms disk write has NOT run; disk is stale.
250
+ assert.equal(fs.readFileSync(target, 'utf8'), 'before the crash\n');
528
251
  const reloaded = crashAndReloadDocs();
529
252
 
530
253
  const reopened = reloaded.openDoc(target);
@@ -535,7 +258,8 @@ test('an acknowledged edit is on disk before a kill and remains current on reope
535
258
  'persisted state must win over a disk that is merely behind',
536
259
  );
537
260
 
538
- // No timer or later process lifetime is needed to make the save real.
261
+ // The stale disk artifact catches up via the rescheduled write-back.
262
+ await sleep(500);
539
263
  assert.equal(fs.readFileSync(target, 'utf8'), 'acknowledged edit / before the crash\n');
540
264
  reloaded.closeAllDocs();
541
265
  });
@@ -553,7 +277,7 @@ test('a multi-update batch is fully persisted by the time it returns', () => {
553
277
  const result = docs.applyDocUpdates(target, updates);
554
278
  assert.equal(result.applied, 3);
555
279
 
556
- // Crash with no graceful close: the journal and crossed files carry all three
280
+ // Crash with no flush: the batch persist alone must carry all three
557
281
  // updates into doc_states.
558
282
  const reloaded = crashAndReloadDocs();
559
283
  const reopened = reloaded.openDoc(target);
@@ -582,90 +306,3 @@ test('an external edit made while closed still folds in on reopen', () => {
582
306
  'a genuinely changed disk is folded in, not overwritten',
583
307
  );
584
308
  });
585
-
586
- test('restart reuses a crossed successor instead of inventing another mutation', () => {
587
- const base = 'first\nsecond\n';
588
- const target = writeDoc('crossed-successor-restart.md', base);
589
- const opened = docs.openDoc(target);
590
- const before = currentSeq();
591
- const mutations = require('../state/mutations');
592
- const baselineMutationIds = new Set(
593
- mutations.listMutations({ localResource: opened.resource })
594
- .map((mutation) => mutation.mutationId),
595
- );
596
-
597
- docs._private.setDiskTransactionTestHooks({ crashAfterInstall: true });
598
- try {
599
- assert.throws(
600
- () => docs.writeDocText(target, 'first\nsecond from api\n', {
601
- baseHash: sha256(base),
602
- mutationId: 'restart_crossed_original_001',
603
- }),
604
- /native file transaction failed/,
605
- );
606
- } finally {
607
- docs._private.setDiskTransactionTestHooks();
608
- }
609
-
610
- fs.writeFileSync(target, 'first from disk\nsecond\n', 'utf8');
611
- const completeMaterializedMutation = mutations.completeMaterializedMutation;
612
- let interrupted = false;
613
- mutations.completeMaterializedMutation = (...args) => {
614
- if (!interrupted) {
615
- interrupted = true;
616
- throw new Error('simulated process death before predecessor completion');
617
- }
618
- return completeMaterializedMutation(...args);
619
- };
620
- try {
621
- assert.throws(
622
- () => docs.writeDocText(target, 'unreached later API\n'),
623
- /simulated process death before predecessor completion/,
624
- );
625
- } finally {
626
- mutations.completeMaterializedMutation = completeMaterializedMutation;
627
- }
628
-
629
- const beforeRestart = mutations.listMutations({ localResource: opened.resource });
630
- assert.equal(
631
- beforeRestart.length,
632
- baselineMutationIds.size + 2,
633
- `one API edit and one disk successor are durable: ${JSON.stringify(
634
- beforeRestart.map((mutation) => ({
635
- mutationId: mutation.mutationId,
636
- state: mutation.state,
637
- operation: mutation.operation,
638
- })),
639
- )}`,
640
- );
641
- assert.ok(
642
- beforeRestart
643
- .filter((mutation) => !baselineMutationIds.has(mutation.mutationId))
644
- .every((mutation) => mutation.state === 'saving-local'),
645
- );
646
-
647
- const reloaded = crashAndReloadDocs();
648
- const reopened = reloaded.openDoc(target);
649
- const finalText = fs.readFileSync(target, 'utf8');
650
- assert.match(finalText, /^first from disk\n/);
651
- assert.match(finalText, /second from api\n/);
652
- assert.equal(clientFromState(reopened.state).getText('content').toString(), finalText);
653
-
654
- const afterRestart = mutations.listMutations({ localResource: opened.resource });
655
- assert.equal(
656
- afterRestart.length,
657
- baselineMutationIds.size + 2,
658
- 'restart must not assign the successor a second ID',
659
- );
660
- assert.ok(
661
- afterRestart
662
- .filter((mutation) => !baselineMutationIds.has(mutation.mutationId))
663
- .every((mutation) => mutation.state === 'saved-local'),
664
- );
665
- assert.equal(
666
- listEventsAfter(before).filter((event) => event.resource === opened.resource).length,
667
- 2,
668
- 'one real API edit plus one real disk save produce exactly two events',
669
- );
670
- reloaded.closeAllDocs();
671
- });