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

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