amalgm 0.1.242 → 0.1.244

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/lib/supervisor.js CHANGED
@@ -174,6 +174,8 @@ const SAFE_PROCESS_ENV_KEYS = [
174
174
  'LANG',
175
175
  'LC_ALL',
176
176
  'LOGNAME',
177
+ 'AMALGM_AUTO_UPDATE',
178
+ 'AMALGM_DISABLE_AUTO_UPDATE',
177
179
  'AMALGM_NATIVE_NODE_MODULES',
178
180
  'AMALGM_SKIP_NATIVE_INSTALL',
179
181
  'ELECTRON_RUN_AS_NODE',
@@ -1343,5 +1345,6 @@ module.exports = {
1343
1345
  liveDeclaredRuntimePorts,
1344
1346
  runtimeStateFilesUnder,
1345
1347
  runtimeStateSearchRoots,
1348
+ safeBaseProcessEnv,
1346
1349
  },
1347
1350
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.242",
3
+ "version": "0.1.244",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -269,8 +269,8 @@ test('failed uploads yield the bulk lane to later durable objects', () => {
269
269
  assert.notEqual(second.sha256, first.sha256);
270
270
  const terminal = failUpload(second.resourceId, second.sha256, {
271
271
  retryable: false,
272
- code: 'shared_resource_read_forbidden',
273
- message: 'the stale rail is no longer readable',
272
+ code: 'invalid_repo_object',
273
+ message: 'this exact object is invalid',
274
274
  });
275
275
  assert.equal(terminal.changed, true);
276
276
  assert.equal(terminal.terminal, true);
@@ -288,21 +288,35 @@ test('failed uploads yield the bulk lane to later durable objects', () => {
288
288
 
289
289
  test('legacy shared-resource uploads can terminate without starving current projects', () => {
290
290
  const legacyResourceId = 'shr_legacyobjectqueue0001';
291
- const legacyBytes = Buffer.from('stale object from a legacy rail');
292
- const legacyDigest = crypto.createHash('sha256').update(legacyBytes).digest('hex');
291
+ const legacyChunks = [
292
+ Buffer.from('first stale object chunk from a legacy rail'),
293
+ Buffer.from('second stale object chunk from a legacy rail'),
294
+ ].map((bytes) => ({
295
+ bytes,
296
+ sha256: crypto.createHash('sha256').update(bytes).digest('hex'),
297
+ }));
293
298
  const objectRoot = path.join(process.env.AMALGM_DIR, 'state', 'repo-objects');
294
299
  const manifests = path.join(objectRoot, 'manifests');
295
300
  const chunks = path.join(objectRoot, 'chunks');
296
- fs.writeFileSync(path.join(chunks, `${legacyDigest}.bin`), legacyBytes);
301
+ for (const chunk of legacyChunks) {
302
+ fs.writeFileSync(path.join(chunks, `${chunk.sha256}.bin`), chunk.bytes);
303
+ }
297
304
  fs.writeFileSync(path.join(manifests, '000-legacy-shared-resource.json'), JSON.stringify({
298
305
  contract: 'git-bundle-chunks@2',
299
306
  resourceId: legacyResourceId,
300
307
  repoKey: 'legacy-repo',
301
308
  oid: '1'.repeat(40),
302
309
  ref: `refs/amalgm/exports/${'1'.repeat(40)}`,
303
- checksum: locatorChecksum([{ sha256: legacyDigest, bytes: legacyBytes.length }]),
304
- bytes: legacyBytes.length,
305
- chunks: [{ sha256: legacyDigest, bytes: legacyBytes.length, uploaded: false }],
310
+ checksum: locatorChecksum(legacyChunks.map((chunk) => ({
311
+ sha256: chunk.sha256,
312
+ bytes: chunk.bytes.length,
313
+ }))),
314
+ bytes: legacyChunks.reduce((total, chunk) => total + chunk.bytes.length, 0),
315
+ chunks: legacyChunks.map((chunk) => ({
316
+ sha256: chunk.sha256,
317
+ bytes: chunk.bytes.length,
318
+ uploaded: false,
319
+ })),
306
320
  createdAt: new Date().toISOString(),
307
321
  }));
308
322
 
@@ -312,7 +326,7 @@ test('legacy shared-resource uploads can terminate without starving current proj
312
326
  );
313
327
  const legacy = claimUpload();
314
328
  assert.equal(legacy.resourceId, legacyResourceId);
315
- assert.equal(legacy.sha256, legacyDigest);
329
+ assert.equal(legacy.sha256, legacyChunks[0].sha256);
316
330
  const terminal = failUpload(legacy.resourceId, legacy.sha256, {
317
331
  retryable: false,
318
332
  code: 'shared_resource_read_forbidden',
@@ -323,6 +337,9 @@ test('legacy shared-resource uploads can terminate without starving current proj
323
337
 
324
338
  const current = claimUpload();
325
339
  assert.equal(current.resourceId, PROJECT);
326
- assert.notEqual(current.sha256, legacy.sha256);
340
+ assert.ok(
341
+ legacyChunks.every((chunk) => current.sha256 !== chunk.sha256),
342
+ 'a resource-wide rejection skips every remaining chunk from the stale rail',
343
+ );
327
344
  acknowledgeUpload(current.resourceId, current.sha256);
328
345
  });
@@ -34,6 +34,12 @@ const REQUESTS = path.join(ROOT, 'requests');
34
34
  const preparing = new Map();
35
35
  const UPLOAD_RETRY_BASE_MS = 1_000;
36
36
  const UPLOAD_RETRY_MAX_MS = 60_000;
37
+ const RESOURCE_WIDE_UPLOAD_FAILURES = new Set([
38
+ 'invalid_repo_project',
39
+ 'private_resource_forbidden',
40
+ 'shared_resource_read_forbidden',
41
+ 'shared_resource_write_forbidden',
42
+ ]);
37
43
 
38
44
  function isProjectId(value) {
39
45
  return require('./tree/identity').isUuid(String(value || ''));
@@ -299,6 +305,10 @@ function failUpload(resourceId, digest, failure = {}) {
299
305
  ensureDirs();
300
306
  const failedAt = new Date().toISOString();
301
307
  const retryable = failure?.retryable !== false;
308
+ const failureCode = typeof failure?.code === 'string'
309
+ ? failure.code
310
+ : 'repo_object_upload_failed';
311
+ const resourceWide = RESOURCE_WIDE_UPLOAD_FAILURES.has(failureCode);
302
312
  let changed = false;
303
313
  let terminal = false;
304
314
  let retryAfter = null;
@@ -308,30 +318,38 @@ function failUpload(resourceId, digest, failure = {}) {
308
318
  const file = path.join(directory, name);
309
319
  const manifest = readJson(file);
310
320
  if (manifest?.resourceId !== resourceId || !manifest?.chunks) continue;
311
- const chunk = manifest.chunks.find((entry) => (
312
- entry.sha256 === digest && !entry.uploaded
321
+ const failedChunks = manifest.chunks.filter((entry) => (
322
+ !entry.uploaded && (resourceWide || entry.sha256 === digest)
313
323
  ));
314
- if (!chunk) continue;
315
- attempts = Math.max(attempts, Number(chunk.uploadAttempts || 0) + 1);
316
- chunk.uploadAttempts = attempts;
317
- chunk.uploadLastError = {
318
- code: typeof failure?.code === 'string' ? failure.code : 'repo_object_upload_failed',
319
- message: typeof failure?.message === 'string' ? failure.message : 'repo object upload failed',
320
- retryable,
321
- at: failedAt,
322
- };
323
- if (retryable) {
324
- const delayMs = Math.min(
325
- UPLOAD_RETRY_MAX_MS,
326
- UPLOAD_RETRY_BASE_MS * (2 ** Math.min(attempts - 1, 16)),
327
- );
328
- retryAfter = new Date(Date.now() + delayMs).toISOString();
329
- chunk.uploadRetryAfter = retryAfter;
330
- delete chunk.uploadTerminal;
331
- } else {
332
- chunk.uploadTerminal = { ...chunk.uploadLastError };
333
- delete chunk.uploadRetryAfter;
334
- terminal = true;
324
+ if (failedChunks.length < 1) continue;
325
+ for (const chunk of failedChunks) {
326
+ const chunkAttempts = Number(chunk.uploadAttempts || 0) + 1;
327
+ attempts = Math.max(attempts, chunkAttempts);
328
+ chunk.uploadAttempts = chunkAttempts;
329
+ chunk.uploadLastError = {
330
+ code: failureCode,
331
+ message: typeof failure?.message === 'string'
332
+ ? failure.message
333
+ : 'repo object upload failed',
334
+ retryable,
335
+ at: failedAt,
336
+ };
337
+ if (retryable) {
338
+ const delayMs = Math.min(
339
+ UPLOAD_RETRY_MAX_MS,
340
+ UPLOAD_RETRY_BASE_MS * (2 ** Math.min(chunkAttempts - 1, 16)),
341
+ );
342
+ const chunkRetryAfter = new Date(Date.now() + delayMs).toISOString();
343
+ retryAfter = retryAfter && Date.parse(retryAfter) > Date.parse(chunkRetryAfter)
344
+ ? retryAfter
345
+ : chunkRetryAfter;
346
+ chunk.uploadRetryAfter = chunkRetryAfter;
347
+ delete chunk.uploadTerminal;
348
+ } else {
349
+ chunk.uploadTerminal = { ...chunk.uploadLastError };
350
+ delete chunk.uploadRetryAfter;
351
+ terminal = true;
352
+ }
335
353
  }
336
354
  writeJson(file, manifest);
337
355
  changed = true;