@nuvin/session 0.3.0-rc.2 → 0.3.0-rc.3

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.
@@ -42,14 +42,114 @@ function removeApproval(state, matches) {
42
42
  return pending.length === state.pending.length ? state : { ...state, pending };
43
43
  }
44
44
 
45
+ // src/state/display-text.ts
46
+ var DISPLAY_TEXT_MAX_BYTES = 512;
47
+ var textEncoder = new TextEncoder();
48
+ function isControlCodePoint(codePoint) {
49
+ return codePoint <= 31 || codePoint >= 127 && codePoint <= 159;
50
+ }
51
+ function sanitizeBoundedDisplayText(text) {
52
+ let output = "";
53
+ let outputBytes = 0;
54
+ let terminalState = "normal";
55
+ for (const codePointText of text) {
56
+ const codePoint = codePointText.codePointAt(0);
57
+ if (codePoint === void 0) continue;
58
+ if (terminalState === "osc") {
59
+ if (codePoint === 7 || codePoint === 156) {
60
+ terminalState = "normal";
61
+ } else if (codePoint === 27) {
62
+ terminalState = "osc-escape";
63
+ }
64
+ continue;
65
+ }
66
+ if (terminalState === "osc-escape") {
67
+ if (codePoint === 92 || codePoint === 7 || codePoint === 156) {
68
+ terminalState = "normal";
69
+ } else if (codePoint !== 27) {
70
+ terminalState = "osc";
71
+ }
72
+ continue;
73
+ }
74
+ if (terminalState === "st-string") {
75
+ if (codePoint === 156) {
76
+ terminalState = "normal";
77
+ } else if (codePoint === 27) {
78
+ terminalState = "st-string-escape";
79
+ }
80
+ continue;
81
+ }
82
+ if (terminalState === "st-string-escape") {
83
+ if (codePoint === 92 || codePoint === 156) {
84
+ terminalState = "normal";
85
+ } else if (codePoint !== 27) {
86
+ terminalState = "st-string";
87
+ }
88
+ continue;
89
+ }
90
+ if (codePoint === 27) {
91
+ terminalState = "escape";
92
+ continue;
93
+ }
94
+ if (codePoint === 155) {
95
+ terminalState = "csi";
96
+ continue;
97
+ }
98
+ if (codePoint === 157) {
99
+ terminalState = "osc";
100
+ continue;
101
+ }
102
+ if (codePoint === 144 || codePoint === 152 || codePoint === 158 || codePoint === 159) {
103
+ terminalState = "st-string";
104
+ continue;
105
+ }
106
+ if (codePoint === 156) {
107
+ terminalState = "normal";
108
+ continue;
109
+ }
110
+ if (terminalState === "csi") {
111
+ if (codePoint >= 64 && codePoint <= 126) {
112
+ terminalState = "normal";
113
+ }
114
+ continue;
115
+ }
116
+ if (terminalState === "escape-intermediate") {
117
+ if (codePoint >= 48 && codePoint <= 126) {
118
+ terminalState = "normal";
119
+ }
120
+ continue;
121
+ }
122
+ if (terminalState === "escape") {
123
+ if (codePoint === 91) {
124
+ terminalState = "csi";
125
+ } else if (codePoint === 93) {
126
+ terminalState = "osc";
127
+ } else if (codePoint === 80 || codePoint === 88 || codePoint === 94 || codePoint === 95) {
128
+ terminalState = "st-string";
129
+ } else if (codePoint >= 32 && codePoint <= 47) {
130
+ terminalState = "escape-intermediate";
131
+ } else {
132
+ terminalState = "normal";
133
+ }
134
+ continue;
135
+ }
136
+ if (isControlCodePoint(codePoint)) continue;
137
+ const codePointBytes = textEncoder.encode(codePointText).byteLength;
138
+ if (outputBytes + codePointBytes > DISPLAY_TEXT_MAX_BYTES) break;
139
+ output += codePointText;
140
+ outputBytes += codePointBytes;
141
+ }
142
+ return output;
143
+ }
144
+
45
145
  // src/state/json.ts
46
146
  function asJsonObject(value) {
47
147
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
48
148
  }
49
149
 
50
150
  // src/state/tool-key.ts
51
- function scopedToolKey(parentToolCallId, toolCallId) {
52
- return JSON.stringify([parentToolCallId ?? null, toolCallId]);
151
+ function scopedToolKey(toolCallPath, toolCallId) {
152
+ return JSON.stringify([...toolCallPath ?? [], toolCallId]);
53
153
  }
54
154
 
55
155
  // src/state/messages.ts
@@ -73,8 +173,11 @@ function addMessage(state, message) {
73
173
  nextId: state.nextId + 1
74
174
  };
75
175
  }
76
- function scopedRegistryKey(scope, id) {
77
- return scope ? `${scope}::${id}` : id;
176
+ function cloneToolCallPath(toolCallPath) {
177
+ return toolCallPath && toolCallPath.length > 0 ? [...toolCallPath] : void 0;
178
+ }
179
+ function scopedRegistryKey(toolCallPath, id) {
180
+ return scopedToolKey(toolCallPath, id);
78
181
  }
79
182
  function compactError(error) {
80
183
  const normalized = error.trim().replace(/\s+/g, " ");
@@ -91,9 +194,9 @@ function retryReasonText(event) {
91
194
  function formatProviderRetryText(event) {
92
195
  return `${retryReasonText(event)}: ${compactError(event.error)} \xB7 ${retryDelayText(event.delayMs)} \xB7 attempt ${event.attempt + 1}/${event.maxAttempts}`;
93
196
  }
94
- function removeProviderStreamMessages(state, assistantMessageId, reasoningMessageId, parentToolCallId) {
95
- const assistantKey = scopedRegistryKey(parentToolCallId, assistantMessageId);
96
- const reasoningKey = scopedRegistryKey(parentToolCallId, reasoningMessageId);
197
+ function removeProviderStreamMessages(state, assistantMessageId, reasoningMessageId, toolCallPath) {
198
+ const assistantKey = scopedRegistryKey(toolCallPath, assistantMessageId);
199
+ const reasoningKey = scopedRegistryKey(toolCallPath, reasoningMessageId);
97
200
  const assistantRowId = state.assistantMessageIds[assistantKey];
98
201
  const reasoningRowId = state.reasoningMessageIds[reasoningKey];
99
202
  if (!assistantRowId && !reasoningRowId) {
@@ -121,8 +224,8 @@ function removeProviderStreamMessages(state, assistantMessageId, reasoningMessag
121
224
  messages: state.messages.filter((message) => !removedIds.has(message.id))
122
225
  };
123
226
  }
124
- function upsertProviderRetryMessage(state, event, parentToolCallId, now) {
125
- const trackingKey = scopedRegistryKey(parentToolCallId, event.assistantMessageId);
227
+ function upsertProviderRetryMessage(state, event, toolCallPath, now) {
228
+ const trackingKey = scopedRegistryKey(toolCallPath, event.assistantMessageId);
126
229
  const existingId = state.providerRetryMessageIds[trackingKey];
127
230
  const attempt = {
128
231
  failedAttempt: event.attempt,
@@ -150,11 +253,12 @@ function upsertProviderRetryMessage(state, event, parentToolCallId, now) {
150
253
  });
151
254
  }
152
255
  const id = createMessageId(state);
256
+ const storedToolCallPath = cloneToolCallPath(toolCallPath);
153
257
  const nextState = addMessage(state, {
154
258
  id,
155
259
  role: "warning",
156
260
  text,
157
- ...parentToolCallId ? { parentToolCallId } : {},
261
+ ...storedToolCallPath ? { toolCallPath: storedToolCallPath } : {},
158
262
  providerRetry: {
159
263
  assistantMessageId: event.assistantMessageId,
160
264
  attempts: [attempt]
@@ -168,8 +272,8 @@ function upsertProviderRetryMessage(state, event, parentToolCallId, now) {
168
272
  }
169
273
  };
170
274
  }
171
- function upsertTrackedTextMessage(state, registryKey, messageId, role, text, mode, live, parentToolCallId, timing) {
172
- const trackingKey = scopedRegistryKey(parentToolCallId, messageId);
275
+ function upsertTrackedTextMessage(state, registryKey, messageId, role, text, mode, live, toolCallPath, timing) {
276
+ const trackingKey = scopedRegistryKey(toolCallPath, messageId);
173
277
  const existingId = state[registryKey][trackingKey];
174
278
  if (existingId) {
175
279
  return withUpdatedMessage(state, existingId, (message) => {
@@ -188,12 +292,13 @@ function upsertTrackedTextMessage(state, registryKey, messageId, role, text, mod
188
292
  });
189
293
  }
190
294
  const id = createMessageId(state);
295
+ const storedToolCallPath = cloneToolCallPath(toolCallPath);
191
296
  const nextState = addMessage(state, {
192
297
  id,
193
298
  role,
194
299
  text,
195
300
  live,
196
- ...parentToolCallId ? { parentToolCallId } : {},
301
+ ...storedToolCallPath ? { toolCallPath: storedToolCallPath } : {},
197
302
  ...timing !== void 0 ? { startedAt: timing } : {},
198
303
  // A reasoning message that arrives already-settled stamps completedAt too.
199
304
  ...timing !== void 0 && !live ? { completedAt: timing } : {}
@@ -206,10 +311,11 @@ function upsertTrackedTextMessage(state, registryKey, messageId, role, text, mod
206
311
  }
207
312
  };
208
313
  }
209
- function settleLiveReasoning(state, parentToolCallId, now) {
314
+ function settleLiveReasoning(state, toolCallPath, now) {
315
+ const scopeKey = scopedRegistryKey(toolCallPath, "");
210
316
  let changed = false;
211
317
  const messages = state.messages.map((message) => {
212
- if (message.role === "reasoning" && message.live && message.parentToolCallId === parentToolCallId) {
318
+ if (message.role === "reasoning" && message.live && scopedRegistryKey(message.toolCallPath, "") === scopeKey) {
213
319
  changed = true;
214
320
  return { ...message, live: false, completedAt: message.completedAt ?? now };
215
321
  }
@@ -228,20 +334,22 @@ function settleAllLiveReasoning(state, now) {
228
334
  });
229
335
  return changed ? { ...state, messages } : state;
230
336
  }
231
- function summarizeToolInput(input) {
337
+ function summarizeToolCallInput(toolName, input) {
232
338
  const objectInput = asJsonObject(input);
233
- if (!objectInput) {
234
- return JSON.stringify(input);
235
- }
236
- const command = objectInput.command;
237
- if (typeof command === "string") {
238
- return command;
339
+ if (toolName === "Program") {
340
+ const description = objectInput && typeof objectInput.description === "string" ? objectInput.description : "";
341
+ return sanitizeBoundedDisplayText(description);
239
342
  }
240
- const entries = Object.entries(objectInput);
241
- if (entries.length === 1 && typeof entries[0]?.[1] === "string") {
242
- return String(entries[0][1]);
343
+ let summary;
344
+ if (!objectInput) {
345
+ summary = JSON.stringify(input);
346
+ } else if (typeof objectInput.command === "string") {
347
+ summary = objectInput.command;
348
+ } else {
349
+ const entries = Object.entries(objectInput);
350
+ summary = entries.length === 1 && typeof entries[0]?.[1] === "string" ? String(entries[0][1]) : JSON.stringify(objectInput);
243
351
  }
244
- return JSON.stringify(objectInput);
352
+ return sanitizeBoundedDisplayText(summary);
245
353
  }
246
354
  var BASH_STREAMING_PREVIEW_LINES = 5;
247
355
  var STREAM_TAIL_BUDGET = 64 * 1024;
@@ -320,8 +428,8 @@ function mergeToolResultText(currentText, result) {
320
428
  }
321
429
  return currentText;
322
430
  }
323
- function ensureToolMessage(state, toolCall, parentToolCallId) {
324
- const toolKey = scopedToolKey(parentToolCallId, toolCall.id);
431
+ function ensureToolMessage(state, toolCall, toolCallPath) {
432
+ const toolKey = scopedToolKey(toolCallPath, toolCall.id);
325
433
  const existingId = state.toolMessageIds[toolKey];
326
434
  if (existingId) {
327
435
  return withUpdatedMessage(state, existingId, (message) => {
@@ -330,21 +438,23 @@ function ensureToolMessage(state, toolCall, parentToolCallId) {
330
438
  }
331
439
  return {
332
440
  ...message,
333
- input: asJsonObject(toolCall.input)
441
+ input: asJsonObject(toolCall.input),
442
+ summary: summarizeToolCallInput(toolCall.name, toolCall.input)
334
443
  };
335
444
  });
336
445
  }
337
446
  const id = createMessageId(state);
447
+ const storedToolCallPath = cloneToolCallPath(toolCallPath);
338
448
  const nextState = addMessage(state, {
339
449
  id,
340
450
  role: "tool",
341
451
  toolName: toolCall.name,
342
452
  toolCallId: toolCall.id,
343
- summary: summarizeToolInput(toolCall.input),
453
+ summary: summarizeToolCallInput(toolCall.name, toolCall.input),
344
454
  text: "",
345
455
  status: "pending",
346
456
  input: asJsonObject(toolCall.input),
347
- ...parentToolCallId ? { parentToolCallId } : {}
457
+ ...storedToolCallPath ? { toolCallPath: storedToolCallPath } : {}
348
458
  });
349
459
  return {
350
460
  ...nextState,
@@ -354,8 +464,8 @@ function ensureToolMessage(state, toolCall, parentToolCallId) {
354
464
  }
355
465
  };
356
466
  }
357
- function setToolMessageStatus(state, toolCallId, status, parentToolCallId) {
358
- const toolMessageId = state.toolMessageIds[scopedToolKey(parentToolCallId, toolCallId)];
467
+ function setToolMessageStatus(state, toolCallId, status, toolCallPath) {
468
+ const toolMessageId = state.toolMessageIds[scopedToolKey(toolCallPath, toolCallId)];
359
469
  if (!toolMessageId) {
360
470
  return state;
361
471
  }
@@ -369,8 +479,8 @@ function setToolMessageStatus(state, toolCallId, status, parentToolCallId) {
369
479
  };
370
480
  });
371
481
  }
372
- function setToolMessageApprovalKind(state, toolCallId, approvalKind, parentToolCallId) {
373
- const toolMessageId = state.toolMessageIds[scopedToolKey(parentToolCallId, toolCallId)];
482
+ function setToolMessageApprovalKind(state, toolCallId, approvalKind, toolCallPath) {
483
+ const toolMessageId = state.toolMessageIds[scopedToolKey(toolCallPath, toolCallId)];
374
484
  if (!toolMessageId) {
375
485
  return state;
376
486
  }
@@ -384,8 +494,8 @@ function setToolMessageApprovalKind(state, toolCallId, approvalKind, parentToolC
384
494
  };
385
495
  });
386
496
  }
387
- function setToolMessageHidden(state, toolCallId, hidden, parentToolCallId) {
388
- const toolMessageId = state.toolMessageIds[scopedToolKey(parentToolCallId, toolCallId)];
497
+ function setToolMessageHidden(state, toolCallId, hidden, toolCallPath) {
498
+ const toolMessageId = state.toolMessageIds[scopedToolKey(toolCallPath, toolCallId)];
389
499
  if (!toolMessageId) {
390
500
  return state;
391
501
  }
@@ -399,12 +509,13 @@ function setToolMessageHidden(state, toolCallId, hidden, parentToolCallId) {
399
509
  };
400
510
  });
401
511
  }
402
- function applyToolResult(state, result, now, parentToolCallId) {
512
+ function applyToolResult(state, result, now, toolCallPath) {
403
513
  const status = asJsonObject(result.structured)?.error === "tool_rejected" ? "rejected" : result.status;
404
- const toolKey = scopedToolKey(parentToolCallId, result.callId);
514
+ const toolKey = scopedToolKey(toolCallPath, result.callId);
405
515
  const toolMessageId = state.toolMessageIds[toolKey];
406
516
  if (!toolMessageId) {
407
517
  const id = createMessageId(state);
518
+ const storedToolCallPath = cloneToolCallPath(toolCallPath);
408
519
  const next = addMessage(state, {
409
520
  id,
410
521
  completedAt: now,
@@ -415,7 +526,7 @@ function applyToolResult(state, result, now, parentToolCallId) {
415
526
  summary: "",
416
527
  text: result.output,
417
528
  structured: result.structured,
418
- ...parentToolCallId ? { parentToolCallId } : {}
529
+ ...storedToolCallPath ? { toolCallPath: storedToolCallPath } : {}
419
530
  });
420
531
  return {
421
532
  ...next,
@@ -451,24 +562,33 @@ function createMessageState() {
451
562
  };
452
563
  }
453
564
  function createMessageStateFromMessages(messages) {
454
- const nextId = messages.reduce((max, message) => {
565
+ const normalizedMessages = messages.map((message) => {
566
+ const toolCallPath = cloneToolCallPath(message.toolCallPath);
567
+ if (toolCallPath) return { ...message, toolCallPath };
568
+ if (message.toolCallPath !== void 0) {
569
+ const { toolCallPath: _toolCallPath, ...rootMessage } = message;
570
+ return rootMessage;
571
+ }
572
+ return message;
573
+ });
574
+ const nextId = normalizedMessages.reduce((max, message) => {
455
575
  const match = /^message-(\d+)$/.exec(message.id);
456
576
  if (!match) return max;
457
577
  return Math.max(max, Number(match[1]) + 1);
458
- }, 1) + messages.length;
578
+ }, 1) + normalizedMessages.length;
459
579
  const toolMessageIds = {};
460
580
  const providerRetryMessageIds = {};
461
- for (const message of messages) {
581
+ for (const message of normalizedMessages) {
462
582
  if (message.role === "tool") {
463
- toolMessageIds[scopedToolKey(message.parentToolCallId, message.toolCallId)] = message.id;
583
+ toolMessageIds[scopedToolKey(message.toolCallPath, message.toolCallId)] = message.id;
464
584
  }
465
585
  if (isProviderRetryMessage(message)) {
466
- providerRetryMessageIds[scopedRegistryKey(message.parentToolCallId, message.providerRetry.assistantMessageId)] = message.id;
586
+ providerRetryMessageIds[scopedRegistryKey(message.toolCallPath, message.providerRetry.assistantMessageId)] = message.id;
467
587
  }
468
588
  }
469
589
  return {
470
590
  assistantMessageIds: {},
471
- messages,
591
+ messages: normalizedMessages,
472
592
  nextId,
473
593
  providerRetryMessageIds,
474
594
  reasoningMessageIds: {},
@@ -505,21 +625,21 @@ function appendWarningMessage(state, text) {
505
625
  text
506
626
  });
507
627
  }
508
- function applyAgentEvent(state, event, parentToolCallId, now = Date.now()) {
628
+ function applyAgentEvent(state, event, toolCallPath, now = Date.now()) {
509
629
  switch (event.type) {
510
630
  case "assistant_chunk":
511
631
  if (!event.chunk.text) {
512
632
  return state;
513
633
  }
514
634
  return upsertTrackedTextMessage(
515
- settleLiveReasoning(state, parentToolCallId, now),
635
+ settleLiveReasoning(state, toolCallPath, now),
516
636
  "assistantMessageIds",
517
637
  event.messageId,
518
638
  "assistant",
519
639
  event.chunk.text,
520
640
  "append",
521
641
  true,
522
- parentToolCallId
642
+ toolCallPath
523
643
  );
524
644
  case "assistant_message": {
525
645
  const text = getTextFromMessage(event.message).trim();
@@ -527,14 +647,14 @@ function applyAgentEvent(state, event, parentToolCallId, now = Date.now()) {
527
647
  return state;
528
648
  }
529
649
  return upsertTrackedTextMessage(
530
- settleLiveReasoning(state, parentToolCallId, now),
650
+ settleLiveReasoning(state, toolCallPath, now),
531
651
  "assistantMessageIds",
532
652
  event.message.id,
533
653
  "assistant",
534
654
  text,
535
655
  "replace",
536
656
  false,
537
- parentToolCallId
657
+ toolCallPath
538
658
  );
539
659
  }
540
660
  case "reasoning_chunk":
@@ -549,7 +669,7 @@ function applyAgentEvent(state, event, parentToolCallId, now = Date.now()) {
549
669
  event.text,
550
670
  "append",
551
671
  true,
552
- parentToolCallId,
672
+ toolCallPath,
553
673
  now
554
674
  );
555
675
  case "reasoning_message": {
@@ -565,7 +685,7 @@ function applyAgentEvent(state, event, parentToolCallId, now = Date.now()) {
565
685
  text,
566
686
  "replace",
567
687
  false,
568
- parentToolCallId,
688
+ toolCallPath,
569
689
  now
570
690
  );
571
691
  }
@@ -575,21 +695,21 @@ function applyAgentEvent(state, event, parentToolCallId, now = Date.now()) {
575
695
  return state;
576
696
  }
577
697
  return upsertTrackedTextMessage(
578
- settleLiveReasoning(state, parentToolCallId, now),
698
+ settleLiveReasoning(state, toolCallPath, now),
579
699
  "assistantMessageIds",
580
700
  event.message.id,
581
701
  "assistant",
582
702
  text,
583
703
  "replace",
584
704
  false,
585
- parentToolCallId
705
+ toolCallPath
586
706
  );
587
707
  }
588
708
  case "tool_call":
589
- return ensureToolMessage(state, event.toolCall, parentToolCallId);
709
+ return ensureToolMessage(state, event.toolCall, toolCallPath);
590
710
  case "tool_started": {
591
- const toolState = ensureToolMessage(state, event.toolCall, parentToolCallId);
592
- const toolMessageId = toolState.toolMessageIds[scopedToolKey(parentToolCallId, event.toolCall.id)];
711
+ const toolState = ensureToolMessage(state, event.toolCall, toolCallPath);
712
+ const toolMessageId = toolState.toolMessageIds[scopedToolKey(toolCallPath, event.toolCall.id)];
593
713
  return withUpdatedMessage(toolState, toolMessageId, (message) => {
594
714
  if (message.role !== "tool") {
595
715
  return message;
@@ -602,8 +722,8 @@ function applyAgentEvent(state, event, parentToolCallId, now = Date.now()) {
602
722
  });
603
723
  }
604
724
  case "tool_output_chunk": {
605
- const toolState = ensureToolMessage(state, event.toolCall, parentToolCallId);
606
- const toolMessageId = toolState.toolMessageIds[scopedToolKey(parentToolCallId, event.toolCall.id)];
725
+ const toolState = ensureToolMessage(state, event.toolCall, toolCallPath);
726
+ const toolMessageId = toolState.toolMessageIds[scopedToolKey(toolCallPath, event.toolCall.id)];
607
727
  return withUpdatedMessage(toolState, toolMessageId, (message) => {
608
728
  if (message.role !== "tool") {
609
729
  return message;
@@ -622,14 +742,14 @@ function applyAgentEvent(state, event, parentToolCallId, now = Date.now()) {
622
742
  });
623
743
  }
624
744
  case "tool_completed": {
625
- const toolState = ensureToolMessage(state, event.toolCall, parentToolCallId);
626
- return applyToolResult(toolState, event.result, now, parentToolCallId);
745
+ const toolState = ensureToolMessage(state, event.toolCall, toolCallPath);
746
+ return applyToolResult(toolState, event.result, now, toolCallPath);
627
747
  }
628
748
  case "tool_result":
629
- return applyToolResult(state, event.result, now, parentToolCallId);
749
+ return applyToolResult(state, event.result, now, toolCallPath);
630
750
  case "tool_rejected": {
631
- const toolState = ensureToolMessage(state, event.toolCall, parentToolCallId);
632
- const toolMessageId = toolState.toolMessageIds[scopedToolKey(parentToolCallId, event.toolCall.id)];
751
+ const toolState = ensureToolMessage(state, event.toolCall, toolCallPath);
752
+ const toolMessageId = toolState.toolMessageIds[scopedToolKey(toolCallPath, event.toolCall.id)];
633
753
  return withUpdatedMessage(toolState, toolMessageId, (message) => {
634
754
  if (message.role !== "tool") {
635
755
  return message;
@@ -648,31 +768,48 @@ function applyAgentEvent(state, event, parentToolCallId, now = Date.now()) {
648
768
  state,
649
769
  event.assistantMessageId,
650
770
  event.reasoningMessageId,
651
- parentToolCallId
771
+ toolCallPath
652
772
  );
653
773
  case "provider_retry":
654
- return upsertProviderRetryMessage(state, event, parentToolCallId, now);
774
+ return upsertProviderRetryMessage(state, event, toolCallPath, now);
655
775
  default:
656
776
  return state;
657
777
  }
658
778
  }
659
779
 
660
780
  // src/state/questions.ts
781
+ function cloneQuestion(question) {
782
+ if (question.toolCallPath && question.toolCallPath.length > 0) {
783
+ return { ...question, toolCallPath: [...question.toolCallPath] };
784
+ }
785
+ if (question.toolCallPath !== void 0) {
786
+ const { toolCallPath: _toolCallPath, ...rootQuestion } = question;
787
+ return rootQuestion;
788
+ }
789
+ return question;
790
+ }
791
+ function questionKey(questionId, toolCallPath) {
792
+ return scopedToolKey(toolCallPath, questionId);
793
+ }
661
794
  function createQuestionQueueState() {
662
795
  return { active: null, pending: [] };
663
796
  }
664
797
  function enqueueQuestion(state, question) {
798
+ const queuedQuestion = cloneQuestion(question);
665
799
  if (state.active === null) {
666
- return { ...state, active: question };
800
+ return { ...state, active: queuedQuestion };
667
801
  }
668
- return { ...state, pending: [...state.pending, question] };
802
+ return { ...state, pending: [...state.pending, queuedQuestion] };
669
803
  }
670
- function removeQuestion(state, questionId) {
671
- if (state.active?.questionId === questionId) {
804
+ function removeQuestion(state, questionId, toolCallPath) {
805
+ const settledKey = questionKey(questionId, toolCallPath);
806
+ if (state.active && questionKey(state.active.questionId, state.active.toolCallPath) === settledKey) {
672
807
  const [active, ...pending2] = state.pending;
673
808
  return { active: active ?? null, pending: pending2 };
674
809
  }
675
- const pending = state.pending.filter((question) => question.questionId !== questionId);
810
+ const pending = state.pending.filter(
811
+ (question) => questionKey(question.questionId, question.toolCallPath) !== settledKey
812
+ );
676
813
  return pending.length === state.pending.length ? state : { ...state, pending };
677
814
  }
678
815
 
@@ -732,6 +869,41 @@ function foldWorkflowDelta(state, delta) {
732
869
  }
733
870
 
734
871
  // src/state/session.ts
872
+ function cloneApproval(approval) {
873
+ const { toolCallPath, ...rootApproval } = approval;
874
+ return toolCallPath && toolCallPath.length > 0 ? { ...rootApproval, toolCallPath: [...toolCallPath] } : rootApproval;
875
+ }
876
+ function cloneQuestion2(question) {
877
+ const { toolCallPath, ...rootQuestion } = question;
878
+ return toolCallPath && toolCallPath.length > 0 ? { ...rootQuestion, toolCallPath: [...toolCallPath] } : rootQuestion;
879
+ }
880
+ function cloneMessage(message) {
881
+ const { toolCallPath, ...rootMessage } = message;
882
+ return toolCallPath && toolCallPath.length > 0 ? { ...rootMessage, toolCallPath: [...toolCallPath] } : rootMessage;
883
+ }
884
+ function normalizeResetState(state) {
885
+ return {
886
+ ...state,
887
+ approval: {
888
+ ...state.approval,
889
+ active: state.approval.active ? cloneApproval(state.approval.active) : null,
890
+ pending: state.approval.pending.map(cloneApproval)
891
+ },
892
+ messages: {
893
+ ...state.messages,
894
+ assistantMessageIds: { ...state.messages.assistantMessageIds },
895
+ messages: state.messages.messages.map(cloneMessage),
896
+ providerRetryMessageIds: { ...state.messages.providerRetryMessageIds },
897
+ reasoningMessageIds: { ...state.messages.reasoningMessageIds },
898
+ toolMessageIds: { ...state.messages.toolMessageIds }
899
+ },
900
+ question: {
901
+ ...state.question,
902
+ active: state.question.active ? cloneQuestion2(state.question.active) : null,
903
+ pending: state.question.pending.map(cloneQuestion2)
904
+ }
905
+ };
906
+ }
735
907
  function createSessionViewState() {
736
908
  return {
737
909
  approval: createApprovalQueueState(),
@@ -753,7 +925,7 @@ function reduceSessionEvent(state, event) {
753
925
  let messages = applyAgentEvent(
754
926
  state.messages,
755
927
  event.event,
756
- event.scope?.parentToolCallId,
928
+ event.scope?.toolCallPath,
757
929
  event.at
758
930
  );
759
931
  if (event.toolStatus && event.event.type === "tool_call") {
@@ -761,14 +933,14 @@ function reduceSessionEvent(state, event) {
761
933
  messages,
762
934
  event.event.toolCall.id,
763
935
  event.toolStatus,
764
- event.scope?.parentToolCallId
936
+ event.scope?.toolCallPath
765
937
  );
766
938
  if (event.toolStatus === "approved") {
767
939
  messages = setToolMessageApprovalKind(
768
940
  messages,
769
941
  event.event.toolCall.id,
770
942
  "auto",
771
- event.scope?.parentToolCallId
943
+ event.scope?.toolCallPath
772
944
  );
773
945
  }
774
946
  }
@@ -782,12 +954,12 @@ function reduceSessionEvent(state, event) {
782
954
  case "approval-requested":
783
955
  return {
784
956
  ...state,
785
- approval: enqueueApproval(state.approval, event.approval),
957
+ approval: enqueueApproval(state.approval, cloneApproval(event.approval)),
786
958
  messages: setToolMessageHidden(
787
959
  state.messages,
788
960
  event.approval.toolCallId,
789
961
  true,
790
- event.approval.parentToolCallId
962
+ event.approval.toolCallPath
791
963
  )
792
964
  };
793
965
  case "approval-settled": {
@@ -795,31 +967,30 @@ function reduceSessionEvent(state, event) {
795
967
  state.messages,
796
968
  event.toolCallId,
797
969
  event.status,
798
- event.parentToolCallId
970
+ event.toolCallPath
799
971
  );
800
972
  if (event.status === "approved" && event.by === "client") {
801
- stamped = setToolMessageApprovalKind(
802
- stamped,
803
- event.toolCallId,
804
- "user",
805
- event.parentToolCallId
806
- );
973
+ stamped = setToolMessageApprovalKind(stamped, event.toolCallId, "user", event.toolCallPath);
807
974
  }
975
+ const settledKey = scopedToolKey(event.toolCallPath, event.toolCallId);
808
976
  return {
809
977
  ...state,
810
978
  approval: removeApproval(
811
979
  state.approval,
812
- (item) => item.toolCallId === event.toolCallId && item.parentToolCallId === event.parentToolCallId
980
+ (item) => scopedToolKey(item.toolCallPath, item.toolCallId) === settledKey
813
981
  ),
814
982
  // Approved tools materialize as a transcript row; rejected ones never
815
983
  // ran and stay hidden.
816
- messages: event.status === "approved" ? setToolMessageHidden(stamped, event.toolCallId, false, event.parentToolCallId) : stamped
984
+ messages: event.status === "approved" ? setToolMessageHidden(stamped, event.toolCallId, false, event.toolCallPath) : stamped
817
985
  };
818
986
  }
819
987
  case "question-asked":
820
988
  return { ...state, question: enqueueQuestion(state.question, event.question) };
821
989
  case "question-settled":
822
- return { ...state, question: removeQuestion(state.question, event.questionId) };
990
+ return {
991
+ ...state,
992
+ question: removeQuestion(state.question, event.questionId, event.toolCallPath)
993
+ };
823
994
  case "turn-status": {
824
995
  const messages = event.busy ? state.messages : settleAllLiveReasoning(state.messages, event.at);
825
996
  return {
@@ -856,7 +1027,7 @@ function reduceSessionEvent(state, event) {
856
1027
  };
857
1028
  }
858
1029
  case "state-reset":
859
- return event.state;
1030
+ return normalizeResetState(event.state);
860
1031
  default:
861
1032
  return state;
862
1033
  }
@@ -869,10 +1040,13 @@ export {
869
1040
  enqueueApproval,
870
1041
  dequeueActiveApproval,
871
1042
  removeApproval,
1043
+ DISPLAY_TEXT_MAX_BYTES,
1044
+ sanitizeBoundedDisplayText,
872
1045
  asJsonObject,
873
1046
  scopedToolKey,
874
1047
  isProviderRetryMessage,
875
1048
  settleAllLiveReasoning,
1049
+ summarizeToolCallInput,
876
1050
  setToolMessageStatus,
877
1051
  setToolMessageApprovalKind,
878
1052
  setToolMessageHidden,