amalgm 0.1.73 → 0.1.74

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.73",
3
+ "version": "0.1.74",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -15,6 +15,7 @@
15
15
  ],
16
16
  "scripts": {
17
17
  "sync-runtime": "node ../../scripts/sync-npm-package-runtime.mjs",
18
+ "prepack": "node ../../scripts/sync-npm-package-runtime.mjs",
18
19
  "pack:dry": "npm pack --dry-run",
19
20
  "check": "node --check bin/amalgm.js && node --check lib/auth-store.js && node --check lib/cli.js && node --check lib/paths.js && node --check lib/process-cleanup.js && node --check lib/runtime-identity.js && node --check lib/runtime-manifest.js && node --check lib/service.js && node --check lib/state-migration.js && node --check lib/supervisor.js && node --check lib/tunnel-chat.js && node --check lib/tunnel-events.js && node --check runtime/lib/runtime-manifest.js && node --check runtime/scripts/runtime-auth.js && node --check runtime/scripts/proxy-token-store.js && node --check runtime/scripts/local-gateway.js && node --check runtime/scripts/port-monitor.js && node --check runtime/scripts/chat-server.js && node --check runtime/scripts/chat-server/index.js && node --check runtime/scripts/chat-server/config.js && node --check runtime/scripts/chat-core/tooling/native-binaries.js && node --check runtime/scripts/chat-core/tooling/package-import.js && node --check runtime/scripts/chat-core/tooling/runtime-home.js && node --check runtime/scripts/amalgm-mcp/index.js && node --check runtime/scripts/amalgm-mcp/config.js && node --check runtime/scripts/lib/project-paths.js && node --check runtime/scripts/lib/runtime-paths.js"
20
21
  },
@@ -217,9 +217,19 @@ class ChatCore {
217
217
  let stopReason = 'end_turn';
218
218
  let sawDone = false;
219
219
  let finalDoneEvent = null;
220
+ let finalDoneChunk = null;
220
221
 
221
222
  const append = (e, options = {}) => {
222
223
  const emitFrame = options.emitFrame !== false;
224
+ const visible = options.visible !== false;
225
+ const nextProviderSessionId = e.providerSessionId || providerSessionId;
226
+ const nextUsage = e.type === 'usage.final' ? e.usage : usage;
227
+ let chunk = null;
228
+ if (emitFrame) {
229
+ const frame = frameFor(e, { sessionId: contract.sessionId, providerSessionId: nextProviderSessionId, usage: nextUsage });
230
+ chunk = this.turns.addChunk(turnId, (index) => withSseId(frame, index), { visible, seal: e.type === 'done' });
231
+ if (!chunk) throw new Error('Raw stream write failed before event application');
232
+ }
223
233
  if (e.providerSessionId) providerSessionId = e.providerSessionId;
224
234
  if (e.type === 'usage.final') {
225
235
  usage = e.usage;
@@ -229,27 +239,26 @@ class ChatCore {
229
239
  sawDone = true;
230
240
  stopReason = e.stopReason || stopReason;
231
241
  finalDoneEvent = e;
242
+ finalDoneChunk = chunk;
232
243
  }
233
244
  parts.apply(e);
234
- if (!emitFrame) return;
235
- const frame = frameFor(e, { sessionId: contract.sessionId, providerSessionId, usage });
236
- this.turns.addChunk(turnId, (index) => withSseId(frame, index));
245
+ return chunk;
237
246
  };
238
247
 
239
248
  try {
240
249
  try {
241
250
  const stream = await this.runtime.prompt(contract, runtimeInput);
242
- for await (const e of stream) append(e, { emitFrame: e.type !== 'done' });
251
+ for await (const e of stream) append(e, { visible: e.type !== 'done' });
243
252
  const current = this.turns.get(turnId);
244
253
  if (!sawDone) {
245
254
  append(done({
246
255
  providerSessionId,
247
256
  stopReason: current?.status === 'cancelling' ? 'cancelled' : stopReason,
248
- }), { emitFrame: false });
257
+ }), { visible: false });
249
258
  }
250
259
  } catch (err) {
251
260
  append(errorEvent(err.message, { providerSessionId }));
252
- append(done({ providerSessionId, stopReason: 'error' }), { emitFrame: false });
261
+ append(done({ providerSessionId, stopReason: 'error' }), { visible: false });
253
262
  stopReason = 'error';
254
263
  }
255
264
  entry.streamEnded = true;
@@ -279,7 +288,11 @@ class ChatCore {
279
288
  this.turns.mark(turnId, 'save_failed');
280
289
  throw err;
281
290
  }
282
- append(finalDoneEvent || done({ providerSessionId, stopReason }));
291
+ if (finalDoneChunk) {
292
+ this.turns.releaseChunk(turnId, finalDoneChunk.index);
293
+ } else {
294
+ append(finalDoneEvent || done({ providerSessionId, stopReason }));
295
+ }
283
296
  this.turns.mark(turnId, stopReason === 'error' ? 'error' : stopReason === 'cancelled' ? 'cancelled' : 'complete');
284
297
  this.turns.clear(turnId);
285
298
  return { providerSessionId, usage, parts: savedParts, stopReason };
@@ -41,13 +41,28 @@ class TurnStore {
41
41
  return this.turns.get(turnId) || null;
42
42
  }
43
43
 
44
- addChunk(turnId, frame) {
44
+ addChunk(turnId, frame, options = {}) {
45
45
  const entry = this.turns.get(turnId);
46
46
  if (!entry || !['streaming', 'cancelling'].includes(entry.status)) return null;
47
+ if (Number.isInteger(entry.sealedIndex)) return null;
47
48
  const index = entry.chunks.length;
48
49
  const data = typeof frame === 'function' ? frame(index) : frame;
49
- const chunk = { index, data, timestamp: Date.now() };
50
+ const visible = options.visible !== false;
51
+ const chunk = { index, data, timestamp: Date.now(), visible };
50
52
  entry.chunks.push(chunk);
53
+ if (options.seal === true) entry.sealedIndex = index;
54
+ entry.updatedAt = Date.now();
55
+ if (visible) this.emit(turnId, chunk, entry);
56
+ return chunk;
57
+ }
58
+
59
+ releaseChunk(turnId, index) {
60
+ const entry = this.turns.get(turnId);
61
+ if (!entry || !['streaming', 'cancelling'].includes(entry.status)) return null;
62
+ const chunk = entry.chunks.find((candidate) => candidate.index === index);
63
+ if (!chunk) return null;
64
+ if (chunk.visible !== false) return chunk;
65
+ chunk.visible = true;
51
66
  entry.updatedAt = Date.now();
52
67
  this.emit(turnId, chunk, entry);
53
68
  return chunk;
@@ -75,9 +90,12 @@ class TurnStore {
75
90
  this.listeners.delete(turnId);
76
91
  }
77
92
 
78
- chunksAfter(turnId, afterIndex = -1) {
93
+ chunksAfter(turnId, afterIndex = -1, options = {}) {
79
94
  const entry = this.turns.get(turnId);
80
- return entry ? entry.chunks.filter((chunk) => chunk.index > afterIndex) : [];
95
+ const visibleOnly = options.visibleOnly === true;
96
+ return entry
97
+ ? entry.chunks.filter((chunk) => chunk.index > afterIndex && (!visibleOnly || chunk.visible !== false))
98
+ : [];
81
99
  }
82
100
 
83
101
  subscribe(turnId, callback) {
@@ -59,6 +59,10 @@ function normalizeAfterIndex(afterIndex) {
59
59
  return Number.isFinite(afterIndex) ? afterIndex : -1;
60
60
  }
61
61
 
62
+ function visibleChunks(entry) {
63
+ return entry.chunks.filter((chunk) => chunk.visible !== false);
64
+ }
65
+
62
66
  function writeTurnTail(core, entry, sse, options = {}) {
63
67
  const format = options.format || 'frame';
64
68
  let lastSent = normalizeAfterIndex(options.afterIndex);
@@ -69,7 +73,7 @@ function writeTurnTail(core, entry, sse, options = {}) {
69
73
  sse.write(namedSse('chunk', {
70
74
  codeSessionId: current.sessionId,
71
75
  chunk,
72
- total: current.chunks.length,
76
+ total: visibleChunks(current).length,
73
77
  }));
74
78
  } else {
75
79
  sse.write(chunk.data);
@@ -81,7 +85,7 @@ function writeTurnTail(core, entry, sse, options = {}) {
81
85
  if (format === 'raw-temp') {
82
86
  sse.write(namedSse('complete', {
83
87
  codeSessionId: current.sessionId,
84
- total: current.chunks.length,
88
+ total: visibleChunks(current).length,
85
89
  }));
86
90
  sse.end();
87
91
  return;
@@ -101,7 +105,7 @@ function writeTurnTail(core, entry, sse, options = {}) {
101
105
  });
102
106
  sse.addCleanup(off);
103
107
 
104
- const existing = entry.chunks.filter((chunk) => chunk.index > lastSent);
108
+ const existing = visibleChunks(entry).filter((chunk) => chunk.index > lastSent);
105
109
  if (format === 'raw-temp') {
106
110
  sse.write(namedSse('existing', {
107
111
  codeSessionId: entry.sessionId,
@@ -129,6 +129,11 @@ test('complete frame waits for assistant message save', async () => {
129
129
  assert.equal(frames.some((frame) => frame.includes('agent_message_chunk')), true, 'content should still stream immediately');
130
130
  assert.equal(frames.some((frame) => frame.includes('"complete"')), false, 'complete must not outrun assistant save');
131
131
  assert.equal(core.active('session-test').active, true, 'turn remains active while persistence is pending');
132
+ const pendingEntry = core.turns.latest('session-test');
133
+ const pendingComplete = pendingEntry.chunks.find((chunk) => chunk.data.includes('"_type":"complete"'));
134
+ assert.ok(pendingComplete, 'raw-temp stores final complete before assistant save');
135
+ assert.equal(pendingComplete.visible, false, 'final complete is hidden from UI until assistant save succeeds');
136
+ assert.equal(pendingEntry.sealedIndex, pendingComplete.index, 'final complete seals the raw-temp log');
132
137
 
133
138
  resolveAssistantSave();
134
139
  await run;