amalgm 0.1.76 → 0.1.77

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.76",
3
+ "version": "0.1.77",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -129,6 +129,49 @@ function ensureClaudeToolState(state) {
129
129
  return state;
130
130
  }
131
131
 
132
+ function ensureClaudeTextState(state) {
133
+ if (!state) return null;
134
+ if (!state.claudeTextByIndex) state.claudeTextByIndex = new Map();
135
+ if (!state.claudeReasoningByIndex) state.claudeReasoningByIndex = new Map();
136
+ return state;
137
+ }
138
+
139
+ function blockIndex(value) {
140
+ const index = Number(value);
141
+ return Number.isInteger(index) && index >= 0 ? index : null;
142
+ }
143
+
144
+ function emitIndexedTextDelta(state, index, kind, delta, providerSessionId, raw) {
145
+ if (!delta) return [];
146
+ const textState = ensureClaudeTextState(state);
147
+ const key = blockIndex(index);
148
+ if (textState && key !== null) {
149
+ const map = kind === 'reasoning' ? textState.claudeReasoningByIndex : textState.claudeTextByIndex;
150
+ map.set(key, `${map.get(key) || ''}${delta}`);
151
+ }
152
+ return kind === 'reasoning'
153
+ ? [reasoningDelta(delta, { providerSessionId, ...raw })]
154
+ : [textDelta(delta, { providerSessionId, ...raw })];
155
+ }
156
+
157
+ function emitIndexedTextSnapshot(block, field, kind, providerSessionId, state, raw) {
158
+ const text = block?.[field];
159
+ if (!text) return [];
160
+ let delta = text;
161
+ const textState = ensureClaudeTextState(state);
162
+ const key = blockIndex(block.index);
163
+ if (textState && key !== null) {
164
+ const map = kind === 'reasoning' ? textState.claudeReasoningByIndex : textState.claudeTextByIndex;
165
+ const streamed = map.get(key) || '';
166
+ delta = streamed ? (text.startsWith(streamed) ? text.slice(streamed.length) : '') : text;
167
+ map.set(key, text);
168
+ }
169
+ if (!delta) return [];
170
+ return kind === 'reasoning'
171
+ ? [reasoningDelta(delta, { providerSessionId, ...raw })]
172
+ : [textDelta(delta, { providerSessionId, ...raw })];
173
+ }
174
+
132
175
  function stableStringify(value) {
133
176
  if (value == null || typeof value !== 'object') return JSON.stringify(value);
134
177
  if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
@@ -259,9 +302,11 @@ function startOrUpdateClaudeTool(block, providerSessionId, state, raw) {
259
302
 
260
303
  function normalizeClaudeContentBlock(block, providerSessionId, state, raw = undefined) {
261
304
  if (!block || typeof block !== 'object') return [];
262
- if (block.type === 'text' && block.text) return [textDelta(block.text, { providerSessionId, ...raw })];
305
+ if (block.type === 'text' && block.text) {
306
+ return emitIndexedTextSnapshot(block, 'text', 'text', providerSessionId, state, raw);
307
+ }
263
308
  if ((block.type === 'thinking' || block.type === 'redacted_thinking') && block.thinking) {
264
- return [reasoningDelta(block.thinking, { providerSessionId, ...raw })];
309
+ return emitIndexedTextSnapshot(block, 'thinking', 'reasoning', providerSessionId, state, raw);
265
310
  }
266
311
  if (TOOL_USE_BLOCK_TYPES.has(block.type)) {
267
312
  return startOrUpdateClaudeTool(block, providerSessionId, state, raw);
@@ -315,6 +360,14 @@ function normalizeClaudeStreamEvent(message, providerSessionId, state) {
315
360
  return startOrUpdateClaudeTool({ ...block, index: event.index }, providerSessionId, state, raw);
316
361
  }
317
362
 
363
+ if (event.type === 'content_block_delta' && event.delta?.type === 'text_delta') {
364
+ return emitIndexedTextDelta(state, event.index, 'text', event.delta.text, providerSessionId, raw);
365
+ }
366
+
367
+ if (event.type === 'content_block_delta' && event.delta?.type === 'thinking_delta') {
368
+ return emitIndexedTextDelta(state, event.index, 'reasoning', event.delta.thinking, providerSessionId, raw);
369
+ }
370
+
318
371
  if (event.type === 'content_block_delta' && event.delta?.type === 'input_json_delta') {
319
372
  const toolState = ensureClaudeToolState(state);
320
373
  const tool = toolState?.claudeToolsByIndex.get(Number(event.index));
@@ -376,9 +429,9 @@ function normalizeClaudeMessage(message, state) {
376
429
 
377
430
  if (message.type === 'assistant') {
378
431
  const events = [];
379
- for (const block of message.message?.content || []) {
432
+ for (const [index, block] of (message.message?.content || []).entries()) {
380
433
  events.push(...normalizeClaudeContentBlock(
381
- block,
434
+ { ...block, index },
382
435
  providerSessionId,
383
436
  state,
384
437
  eventRaw('claude.sdk.message', 'claude/assistant/content_block', message),
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+
3
+ const test = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const { normalizeClaudeMessage } = require('../normalizers/claude');
6
+
7
+ test('claude text stream deltas emit before final assistant snapshot without duplication', () => {
8
+ const state = { providerSessionId: 'claude-session' };
9
+
10
+ const first = normalizeClaudeMessage({
11
+ type: 'content_block_delta',
12
+ index: 0,
13
+ delta: { type: 'text_delta', text: 'hel' },
14
+ }, state);
15
+ assert.equal(first.length, 1);
16
+ assert.equal(first[0].type, 'text.delta');
17
+ assert.equal(first[0].text, 'hel');
18
+
19
+ const second = normalizeClaudeMessage({
20
+ type: 'content_block_delta',
21
+ index: 0,
22
+ delta: { type: 'text_delta', text: 'lo' },
23
+ }, state);
24
+ assert.equal(second.length, 1);
25
+ assert.equal(second[0].type, 'text.delta');
26
+ assert.equal(second[0].text, 'lo');
27
+
28
+ const finalSnapshot = normalizeClaudeMessage({
29
+ type: 'assistant',
30
+ message: {
31
+ content: [{ type: 'text', text: 'hello' }],
32
+ },
33
+ }, state);
34
+ assert.deepEqual(finalSnapshot, []);
35
+ });
36
+
37
+ test('claude thinking stream deltas emit reasoning before final snapshot without duplication', () => {
38
+ const state = { providerSessionId: 'claude-session' };
39
+
40
+ const streamed = normalizeClaudeMessage({
41
+ type: 'content_block_delta',
42
+ index: 0,
43
+ delta: { type: 'thinking_delta', thinking: 'inspect first' },
44
+ }, state);
45
+ assert.equal(streamed.length, 1);
46
+ assert.equal(streamed[0].type, 'reasoning.delta');
47
+ assert.equal(streamed[0].text, 'inspect first');
48
+
49
+ const finalSnapshot = normalizeClaudeMessage({
50
+ type: 'assistant',
51
+ message: {
52
+ content: [{ type: 'thinking', thinking: 'inspect first' }],
53
+ },
54
+ }, state);
55
+ assert.deepEqual(finalSnapshot, []);
56
+ });