@bubblebrain-ai/bubble 0.0.58 → 0.0.60

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.
Files changed (177) hide show
  1. package/README.md +56 -6
  2. package/dist/agent/child-runner.js +5 -1
  3. package/dist/agent/internal-reminder-sanitizer.d.ts +1 -1
  4. package/dist/agent/internal-reminder-sanitizer.js +7 -1
  5. package/dist/agent/profiles.js +4 -4
  6. package/dist/agent/routing-catalog.d.ts +19 -1
  7. package/dist/agent/routing-catalog.js +32 -13
  8. package/dist/agent/subagent/runtime.d.ts +28 -0
  9. package/dist/agent/subagent/runtime.js +34 -30
  10. package/dist/agent/subagent-control.d.ts +7 -0
  11. package/dist/agent/subagent-control.js +2 -0
  12. package/dist/agent/subagent-lifecycle-reminder.js +2 -1
  13. package/dist/agent/subagent-wait-guidance.d.ts +2 -0
  14. package/dist/agent/subagent-wait-guidance.js +8 -0
  15. package/dist/agent/subtask-policy.js +4 -4
  16. package/dist/agent.d.ts +20 -11
  17. package/dist/agent.js +200 -117
  18. package/dist/anthropic-thinking.d.ts +4 -0
  19. package/dist/anthropic-thinking.js +11 -0
  20. package/dist/approval/controller.d.ts +20 -6
  21. package/dist/approval/controller.js +67 -8
  22. package/dist/approval/protected-paths.d.ts +1 -0
  23. package/dist/approval/protected-paths.js +35 -0
  24. package/dist/approval/session-cache.d.ts +12 -26
  25. package/dist/approval/session-cache.js +20 -50
  26. package/dist/approval/types.d.ts +27 -1
  27. package/dist/cli.d.ts +5 -0
  28. package/dist/cli.js +25 -2
  29. package/dist/config.d.ts +3 -0
  30. package/dist/config.js +7 -0
  31. package/dist/context/checkpoint.d.ts +18 -0
  32. package/dist/context/checkpoint.js +117 -0
  33. package/dist/context/compact-llm.js +7 -0
  34. package/dist/context/compact.d.ts +8 -0
  35. package/dist/context/compact.js +40 -7
  36. package/dist/context/llm-compactor.d.ts +36 -2
  37. package/dist/context/llm-compactor.js +227 -44
  38. package/dist/context/session-write-lock.d.ts +20 -0
  39. package/dist/context/session-write-lock.js +162 -0
  40. package/dist/context/tool-images.d.ts +3 -0
  41. package/dist/context/tool-images.js +35 -0
  42. package/dist/context/usage.js +22 -5
  43. package/dist/feishu/agent-host/run-driver.js +56 -9
  44. package/dist/lsp/service.d.ts +13 -1
  45. package/dist/lsp/service.js +163 -43
  46. package/dist/main.js +82 -16
  47. package/dist/mcp/config.d.ts +4 -0
  48. package/dist/mcp/config.js +17 -0
  49. package/dist/mcp/manager.d.ts +13 -1
  50. package/dist/mcp/manager.js +32 -1
  51. package/dist/mcp/transports.js +11 -0
  52. package/dist/memory/db.d.ts +1 -0
  53. package/dist/memory/db.js +20 -4
  54. package/dist/memory/phase1.js +122 -16
  55. package/dist/memory/prompts.js +3 -0
  56. package/dist/model-catalog.js +19 -3
  57. package/dist/model-config.d.ts +1 -0
  58. package/dist/model-config.js +5 -5
  59. package/dist/model-pricing.js +26 -9
  60. package/dist/network/provider-transport-log.d.ts +12 -0
  61. package/dist/network/provider-transport-log.js +32 -0
  62. package/dist/network/provider-transport.js +6 -1
  63. package/dist/network/retry.d.ts +1 -1
  64. package/dist/network/retry.js +1 -1
  65. package/dist/oauth/grok.d.ts +1 -0
  66. package/dist/oauth/grok.js +20 -16
  67. package/dist/oauth/openai-codex.d.ts +1 -0
  68. package/dist/oauth/openai-codex.js +29 -25
  69. package/dist/oauth/refresh-control.d.ts +12 -0
  70. package/dist/oauth/refresh-control.js +42 -0
  71. package/dist/oauth/storage.d.ts +55 -1
  72. package/dist/oauth/storage.js +281 -16
  73. package/dist/permissions/rule.d.ts +6 -3
  74. package/dist/permissions/rule.js +71 -12
  75. package/dist/permissions/settings.d.ts +28 -1
  76. package/dist/permissions/settings.js +93 -3
  77. package/dist/permissions/shell-command.d.ts +36 -0
  78. package/dist/permissions/shell-command.js +226 -0
  79. package/dist/permissions/trust-prompt.d.ts +10 -0
  80. package/dist/permissions/trust-prompt.js +34 -0
  81. package/dist/permissions/trust.d.ts +26 -0
  82. package/dist/permissions/trust.js +113 -0
  83. package/dist/prompt/compose.d.ts +3 -0
  84. package/dist/prompt/compose.js +24 -7
  85. package/dist/prompt/environment.js +2 -2
  86. package/dist/prompt/reminders.js +1 -1
  87. package/dist/prompt/runtime.js +1 -1
  88. package/dist/provider-anthropic.d.ts +6 -2
  89. package/dist/provider-anthropic.js +98 -18
  90. package/dist/provider-error-record.d.ts +9 -0
  91. package/dist/provider-error-record.js +42 -14
  92. package/dist/provider-grok.js +5 -1
  93. package/dist/provider-openai-codex.d.ts +8 -0
  94. package/dist/provider-openai-codex.js +107 -24
  95. package/dist/provider-registry.d.ts +45 -2
  96. package/dist/provider-registry.js +300 -88
  97. package/dist/provider-transform.js +8 -0
  98. package/dist/provider-zen-errors.d.ts +9 -0
  99. package/dist/provider-zen-errors.js +36 -0
  100. package/dist/provider.js +36 -5
  101. package/dist/sdk/index.d.ts +69 -8
  102. package/dist/sdk/index.js +252 -39
  103. package/dist/sdk/replay-event-log.d.ts +7 -0
  104. package/dist/sdk/replay-event-log.js +66 -16
  105. package/dist/session-context-fence.d.ts +16 -0
  106. package/dist/session-context-fence.js +26 -0
  107. package/dist/session-log.d.ts +11 -2
  108. package/dist/session-log.js +187 -39
  109. package/dist/session-types.d.ts +5 -1
  110. package/dist/session.d.ts +39 -7
  111. package/dist/session.js +341 -93
  112. package/dist/skills/format.d.ts +31 -1
  113. package/dist/skills/format.js +45 -24
  114. package/dist/slash-commands/commands.d.ts +3 -0
  115. package/dist/slash-commands/commands.js +29 -14
  116. package/dist/system-prompt.d.ts +3 -1
  117. package/dist/tasks/manager.d.ts +7 -0
  118. package/dist/tasks/manager.js +97 -18
  119. package/dist/tools/agent-lifecycle.js +11 -7
  120. package/dist/tools/child-tools.d.ts +1 -1
  121. package/dist/tools/child-tools.js +4 -4
  122. package/dist/tools/edit-apply.d.ts +0 -3
  123. package/dist/tools/edit-apply.js +45 -249
  124. package/dist/tools/edit.js +3 -3
  125. package/dist/tools/exit-plan-mode.d.ts +5 -0
  126. package/dist/tools/exit-plan-mode.js +3 -2
  127. package/dist/tools/file-state.d.ts +4 -0
  128. package/dist/tools/file-state.js +12 -3
  129. package/dist/tools/grep.js +20 -1
  130. package/dist/tools/index.d.ts +1 -1
  131. package/dist/tools/index.js +3 -3
  132. package/dist/tools/ls.d.ts +3 -0
  133. package/dist/tools/ls.js +85 -0
  134. package/dist/tools/path-utils.d.ts +6 -0
  135. package/dist/tools/path-utils.js +38 -1
  136. package/dist/tools/sensitive-paths.d.ts +8 -0
  137. package/dist/tools/sensitive-paths.js +85 -3
  138. package/dist/tools/skill.d.ts +2 -0
  139. package/dist/tools/skill.js +34 -2
  140. package/dist/tools/task-tools.js +11 -7
  141. package/dist/tui/app.js +22 -12
  142. package/dist/tui/components/approval-dialog.d.ts +5 -5
  143. package/dist/tui/components/approval-dialog.js +16 -51
  144. package/dist/tui/components/skills-panel.d.ts +4 -0
  145. package/dist/tui/components/skills-panel.js +15 -1
  146. package/dist/tui/components/streaming-message.js +7 -2
  147. package/dist/tui/components/tasks-pane.d.ts +38 -0
  148. package/dist/tui/components/tasks-pane.js +182 -32
  149. package/dist/tui/components/transcript.d.ts +11 -4
  150. package/dist/tui/components/transcript.js +158 -71
  151. package/dist/tui/composer-autocomplete.d.ts +17 -2
  152. package/dist/tui/composer-autocomplete.js +79 -2
  153. package/dist/tui/controller/controller.d.ts +9 -4
  154. package/dist/tui/controller/controller.js +87 -95
  155. package/dist/tui/controller/goal-runtime-controller.js +1 -1
  156. package/dist/tui/controller/session-transition.js +1 -1
  157. package/dist/tui/model/subagent-view.d.ts +11 -19
  158. package/dist/tui/model/subagent-view.js +56 -85
  159. package/dist/tui/model/task-lifecycle.d.ts +57 -0
  160. package/dist/tui/model/task-lifecycle.js +124 -0
  161. package/dist/tui/model/trace-groups.js +10 -9
  162. package/dist/tui/trace-groups.js +10 -9
  163. package/dist/types.d.ts +20 -0
  164. package/dist/variant/variant-resolver.js +2 -0
  165. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.d.ts +1 -0
  166. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.d.ts.map +1 -1
  167. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.js +6 -0
  168. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.js.map +1 -1
  169. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.d.ts +2 -0
  170. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.d.ts.map +1 -1
  171. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.js +8 -4
  172. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.js.map +1 -1
  173. package/package.json +11 -3
  174. package/dist/prompt/skills.d.ts +0 -2
  175. package/dist/prompt/skills.js +0 -4
  176. package/dist/tools/glob.d.ts +0 -5
  177. package/dist/tools/glob.js +0 -199
@@ -7,8 +7,9 @@ import { AgentRunInputQueue } from "../../agent/input-controller.js";
7
7
  import { OverlayRequestController } from "./overlay-controller.js";
8
8
  import { SessionTransitionController } from "./session-transition.js";
9
9
  import { moveStatusMessageToEnd, nextDisplayMessageKey, setUserInputStatus, snapshotDisplayParts, } from "../model/display-history.js";
10
- import { accumulateLiveSubagentUpdate, collectSubagentGroups, mergeSubagentSnapshotsIntoMessages, pruneSettledLiveSubagentTools, } from "../model/subagent-view.js";
10
+ import { applySubagentUpdateToMessages, collectSubagentGroups, mergeSubagentSnapshotsIntoMessages, } from "../model/subagent-view.js";
11
11
  import { reconstructDisplayMessages } from "../model/display-reconstruct.js";
12
+ import { applyTaskLifecycleToMessages, landTaskLifecycles, mergeTaskLifecycleIntoLiveTools, taskLifecycleDisplayMessage, UNMATCHED_LAUNCH, } from "../model/task-lifecycle.js";
12
13
  import { buildImageContentParts, buildImageContentPartsFromDisplayText } from "../model/image-paste.js";
13
14
  import { displayImagesFromPayload, formatImageUserDisplayText } from "../image-display.js";
14
15
  import { GoalRuntimeController } from "./goal-runtime-controller.js";
@@ -29,6 +30,7 @@ export class BubbleTuiController {
29
30
  queue = createInputQueueState();
30
31
  sessionTransition;
31
32
  startedAtMs;
33
+ turnStartedAt;
32
34
  goalRuntime;
33
35
  taskRuntime;
34
36
  pendingTaskAnnouncements = new Map();
@@ -36,9 +38,6 @@ export class BubbleTuiController {
36
38
  transcript = [];
37
39
  runActive = false;
38
40
  runState = null;
39
- /** Background subagent updates can outlive the provider turn that launched
40
- * them. Keep their latest synthetic tool snapshot in the live trace. */
41
- liveSubagentTools = new Map();
42
41
  /** Provider-turn accumulators for true child-session inspection. */
43
42
  childRuns = new Map();
44
43
  activeInputController = null;
@@ -51,10 +50,7 @@ export class BubbleTuiController {
51
50
  constructor(deps) {
52
51
  this.deps = deps;
53
52
  this.sessionManager = deps.sessionManager;
54
- this.transcript = [
55
- ...reconstructDisplayMessages([...deps.agent.messages]),
56
- ...restoredTaskLifecycleMessages(this.sessionManager),
57
- ];
53
+ this.transcript = landTaskLifecycles(reconstructDisplayMessages([...deps.agent.messages]), restoredTaskLifecycles(this.sessionManager));
58
54
  this.overlays = new OverlayRequestController();
59
55
  this.startedAtMs = Date.now();
60
56
  this.sessionTransition = new SessionTransitionController({
@@ -70,17 +66,18 @@ export class BubbleTuiController {
70
66
  this.externalGeneration += 1;
71
67
  },
72
68
  clearLiveSubagentTools: () => {
73
- this.liveSubagentTools.clear();
74
69
  this.childRuns.clear();
75
- this.liveSubagentVersion += 1;
76
70
  },
77
71
  commit: (manager, transcript) => {
78
72
  this.sessionManager = manager;
79
73
  this.deps.ports.flush.cancelFlush();
80
- this.transcript = [...transcript, ...restoredTaskLifecycleMessages(manager)];
74
+ this.transcript = landTaskLifecycles(transcript, restoredTaskLifecycles(manager));
81
75
  this.queue.queued.length = 0;
82
76
  this.liveStreamVisible = false;
83
77
  this.runState = null;
78
+ // Session-scoped: the restored session has not started a run, so none
79
+ // of its subagents or tasks belong to a "current turn".
80
+ this.turnStartedAt = undefined;
84
81
  this.activeInputController = null;
85
82
  this.activeAbortController = null;
86
83
  this.commandActivity = null;
@@ -143,7 +140,6 @@ export class BubbleTuiController {
143
140
  }));
144
141
  }
145
142
  externalGeneration = 0;
146
- liveSubagentVersion = 0;
147
143
  subscribe(listener) {
148
144
  this.listeners.add(listener);
149
145
  return () => this.listeners.delete(listener);
@@ -172,7 +168,11 @@ export class BubbleTuiController {
172
168
  this.goalRuntime.handleCommand(input, cwd);
173
169
  }
174
170
  getSubagentGroups() {
175
- const fromTrace = collectSubagentGroups(this.transcript, [...this.liveSubagentTools.values()]);
171
+ // Launches from earlier rounds live in the transcript (kept current by
172
+ // applySubagentUpdateToMessages); this round's launches are still in the
173
+ // streaming accumulator until turn_end commits them.
174
+ const streamingTools = this.runActive && this.runState ? this.runState.accumulator.toolCalls : [];
175
+ const fromTrace = collectSubagentGroups(this.transcript, streamingTools);
176
176
  const claimed = new Set(fromTrace.flatMap((group) => group.members.map((member) => member.subAgentId).filter(Boolean)));
177
177
  const direct = this.deps.agent.listSubAgents()
178
178
  .filter((snapshot) => !claimed.has(snapshot.agentId))
@@ -201,6 +201,14 @@ export class BubbleTuiController {
201
201
  }));
202
202
  return [...fromTrace, ...direct];
203
203
  }
204
+ /**
205
+ * Start of the current (or, when idle, the latest) agent run. The tasks pane
206
+ * keeps finished work that was launched since then; wall clock on purpose,
207
+ * it is compared with subagent/task/workflow creation stamps.
208
+ */
209
+ getTurnStartedAt() {
210
+ return this.turnStartedAt;
211
+ }
204
212
  getWorkflows() {
205
213
  return this.deps.agent.listWorkflows();
206
214
  }
@@ -262,16 +270,48 @@ export class BubbleTuiController {
262
270
  // Switching back to the owner session reconstructs terminal task rows
263
271
  // from its persisted marker before held completions are released. Treat
264
272
  // that row as the same lifecycle event instead of appending a duplicate.
265
- if (this.transcript.some((message) => message.toolCalls?.some((tool) => tool.metadata?.taskId === task.id && tool.metadata?.taskLifecycle !== undefined))) {
273
+ // Task ids repeat across processes, so the row must be this very run.
274
+ if (this.transcript.some((message) => message.toolCalls?.some((tool) => tool.metadata?.taskId === task.id
275
+ && tool.metadata?.taskLifecycle !== undefined
276
+ && tool.metadata?.endedAt === task.endedAt))) {
277
+ return;
278
+ }
279
+ const output = this.deps.processManager?.taskOutputTail(task.id, 12_000);
280
+ // Land the outcome on the launch row: this round's launches are still in
281
+ // the streaming accumulator, earlier rounds' have committed.
282
+ if (this.runActive && this.runState
283
+ && mergeTaskLifecycleIntoLiveTools(this.runState.accumulator.toolCalls, task, output)) {
284
+ this.state.touch();
285
+ this.notify();
286
+ return;
287
+ }
288
+ const landed = applyTaskLifecycleToMessages(this.transcript, task, output);
289
+ if (landed.merged) {
290
+ this.transcript = landed.messages;
291
+ this.state.touch();
292
+ this.notify();
266
293
  return;
267
294
  }
268
295
  if (this.runActive) {
296
+ // Launch row not settled yet (or absent): retry on the next tool_end,
297
+ // and fall back to a detached row when the round commits.
269
298
  this.pendingTaskAnnouncements.set(task.id, task);
270
299
  this.state.touch();
271
300
  this.notify();
272
301
  return;
273
302
  }
274
- this.appendDisplayMessage(taskLifecycleDisplayMessage(task, this.deps.processManager?.taskOutputTail(task.id, 12_000)));
303
+ this.appendDisplayMessage(taskLifecycleDisplayMessage(task, output));
304
+ }
305
+ /** Pending completions whose launch row has settled since they arrived. */
306
+ landPendingTaskAnnouncementsInLiveRound() {
307
+ if (!this.runState || this.pendingTaskAnnouncements.size === 0)
308
+ return;
309
+ for (const [id, task] of this.pendingTaskAnnouncements) {
310
+ const output = this.deps.processManager?.taskOutputTail(task.id, 12_000);
311
+ if (mergeTaskLifecycleIntoLiveTools(this.runState.accumulator.toolCalls, task, output)) {
312
+ this.pendingTaskAnnouncements.delete(id);
313
+ }
314
+ }
275
315
  }
276
316
  /** Move the newest live Bash Execute into the unified task manager. */
277
317
  promoteActiveBash() {
@@ -288,7 +328,6 @@ export class BubbleTuiController {
288
328
  /** Drop every transcript row (/clear). */
289
329
  clearTranscript() {
290
330
  this.transcript = [];
291
- this.liveSubagentTools.clear();
292
331
  this.childRuns.clear();
293
332
  this.state.touch();
294
333
  this.notify();
@@ -296,13 +335,8 @@ export class BubbleTuiController {
296
335
  /** Re-project the visible transcript after a non-controller command rewrites Agent history. */
297
336
  rebuildTranscriptFromAgent() {
298
337
  this.state.withTransaction(() => {
299
- this.transcript = [
300
- ...reconstructDisplayMessages([...this.deps.agent.messages]),
301
- ...restoredTaskLifecycleMessages(this.sessionManager),
302
- ];
303
- this.liveSubagentTools.clear();
338
+ this.transcript = landTaskLifecycles(reconstructDisplayMessages([...this.deps.agent.messages]), restoredTaskLifecycles(this.sessionManager));
304
339
  this.childRuns.clear();
305
- this.liveSubagentVersion += 1;
306
340
  this.state.touch();
307
341
  });
308
342
  this.notify();
@@ -320,13 +354,8 @@ export class BubbleTuiController {
320
354
  this.state.withTransaction(() => {
321
355
  purgeForSessionSwitch(this.queue);
322
356
  this.queue.queued.length = 0;
323
- this.transcript = [
324
- ...reconstructDisplayMessages([...this.deps.agent.messages]),
325
- ...restoredTaskLifecycleMessages(this.sessionManager),
326
- ];
327
- this.liveSubagentTools.clear();
357
+ this.transcript = landTaskLifecycles(reconstructDisplayMessages([...this.deps.agent.messages]), restoredTaskLifecycles(this.sessionManager));
328
358
  this.childRuns.clear();
329
- this.liveSubagentVersion += 1;
330
359
  this.liveStreamVisible = false;
331
360
  this.runState = null;
332
361
  this.state.touch();
@@ -493,20 +522,7 @@ export class BubbleTuiController {
493
522
  if (!this.runActive || !this.runState || !this.liveStreamVisible)
494
523
  return null;
495
524
  const acc = this.runState.accumulator;
496
- const currentIds = new Set(acc.toolCalls.map((tool) => tool.id));
497
- const carriedTools = [...this.liveSubagentTools.values()].filter((tool) => !currentIds.has(tool.id));
498
- const allTools = [...acc.toolCalls, ...carriedTools];
499
525
  const parts = snapshotDisplayParts(acc.parts);
500
- if (carriedTools.length > 0) {
501
- parts.push({
502
- type: "tools",
503
- toolCalls: carriedTools.map((tool) => ({
504
- ...tool,
505
- args: { ...tool.args },
506
- metadata: tool.metadata ? { ...tool.metadata } : undefined,
507
- })),
508
- });
509
- }
510
526
  return {
511
527
  content: acc.content,
512
528
  // Ink kept the current provider turn's reasoning visible even after
@@ -514,7 +530,7 @@ export class BubbleTuiController {
514
530
  reasoning: acc.reasoning,
515
531
  // Return a render-safe snapshot: reducer events continue mutating the
516
532
  // accumulator in place while the TUI may still hold the previous tail.
517
- tools: allTools.map((tool) => ({
533
+ tools: acc.toolCalls.map((tool) => ({
518
534
  ...tool,
519
535
  args: { ...tool.args },
520
536
  metadata: tool.metadata ? { ...tool.metadata } : undefined,
@@ -524,7 +540,7 @@ export class BubbleTuiController {
524
540
  parts,
525
541
  // Phase is provider-turn local, matching Ink's clearAssistantStream().
526
542
  // A tool in an earlier committed turn must not suppress fresh Thinking.
527
- phase: allTools.length > 0 ? "working" : "thinking",
543
+ phase: acc.toolCalls.length > 0 ? "working" : "thinking",
528
544
  };
529
545
  }
530
546
  pendingOverlayCount() {
@@ -558,6 +574,7 @@ export class BubbleTuiController {
558
574
  const agentInput = isSubmitPayload(input) ? this.submitAgentInput(input) : input;
559
575
  this.runActive = true;
560
576
  this.liveStreamVisible = true;
577
+ this.turnStartedAt = Date.now();
561
578
  this.runState = createRunState(Date.now());
562
579
  const inputController = new AgentRunInputQueue(`run-${this.runState.accumulator.runId}`);
563
580
  const abortController = new AbortController();
@@ -583,7 +600,6 @@ export class BubbleTuiController {
583
600
  upstreamAbort();
584
601
  else
585
602
  options?.abortSignal?.addEventListener("abort", upstreamAbort, { once: true });
586
- pruneSettledLiveSubagentTools(this.liveSubagentTools);
587
603
  this.state.touch();
588
604
  this.notify();
589
605
  const ctx = {
@@ -611,22 +627,28 @@ export class BubbleTuiController {
611
627
  }
612
628
  const { state, effects } = reduceAgentEvent(this.runState, event, ctx);
613
629
  this.runState = state;
630
+ if (event.type === "tool_end")
631
+ this.landPendingTaskAnnouncementsInLiveRound();
614
632
  const subagentMetadata = event.type === "tool_update"
615
633
  ? event.update.metadata
616
634
  : event.type === "tool_end"
617
635
  ? event.result.metadata
618
636
  : undefined;
619
- if (subagentMetadata?.kind === "subagent") {
620
- this.transcript = mergeSubagentSnapshotsIntoMessages(this.transcript, subagentMetadata);
621
- }
622
637
  if (event.type === "tool_update"
638
+ && subagentMetadata?.kind === "subagent"
623
639
  && effects.some((effect) => effect.kind === "live-subagent-changed")) {
624
- accumulateLiveSubagentUpdate(this.liveSubagentTools, {
640
+ // The launching call already committed: merge the update into its
641
+ // transcript entry so the settled row stays the single source of
642
+ // truth for live and settled rendering alike.
643
+ this.transcript = applySubagentUpdateToMessages(this.transcript, {
625
644
  id: event.id,
626
645
  name: event.name,
627
- metadata: event.update.metadata,
646
+ metadata: subagentMetadata,
628
647
  });
629
648
  }
649
+ else if (subagentMetadata?.kind === "subagent") {
650
+ this.transcript = mergeSubagentSnapshotsIntoMessages(this.transcript, subagentMetadata);
651
+ }
630
652
  if (event.type === "tool_update" && event.update.childEvent) {
631
653
  this.reduceChildEvent(event.update.subAgentId, event.update.childEvent);
632
654
  }
@@ -763,10 +785,10 @@ export class BubbleTuiController {
763
785
  this.activeInputController = null;
764
786
  this.activeAbortController = null;
765
787
  if (this.pendingTaskAnnouncements.size > 0) {
766
- this.transcript = [
767
- ...this.transcript,
768
- ...[...this.pendingTaskAnnouncements.values()].map((task) => taskLifecycleDisplayMessage(task, this.deps.processManager?.taskOutputTail(task.id, 12_000))),
769
- ];
788
+ this.transcript = landTaskLifecycles(this.transcript, [...this.pendingTaskAnnouncements.values()].map((task) => ({
789
+ task,
790
+ output: this.deps.processManager?.taskOutputTail(task.id, 12_000),
791
+ })));
770
792
  this.pendingTaskAnnouncements.clear();
771
793
  }
772
794
  this.state.touch();
@@ -929,42 +951,7 @@ export class BubbleTuiController {
929
951
  return { reason, wallMs: Date.now() - this.startedAtMs };
930
952
  }
931
953
  }
932
- function taskLifecycleDisplayMessage(task, output) {
933
- const status = task.status === "killed"
934
- ? "cancelled"
935
- : task.status === "failed"
936
- ? "failed"
937
- : "completed";
938
- const tool = {
939
- id: `task-lifecycle:${task.id}:${task.endedAt ?? Date.now()}`,
940
- name: "bash",
941
- args: {
942
- command: task.command,
943
- ...(task.description ? { description: task.description } : {}),
944
- },
945
- status,
946
- isError: task.status === "failed",
947
- result: output?.trim() ? output : undefined,
948
- startedAt: task.startedAt,
949
- metadata: {
950
- kind: "shell",
951
- background: true,
952
- taskId: task.id,
953
- taskLifecycle: task.status,
954
- exitCode: task.exitCode ?? null,
955
- endedAt: task.endedAt,
956
- outputLines: task.outputLines,
957
- },
958
- };
959
- return {
960
- key: nextDisplayMessageKey("task"),
961
- role: "assistant",
962
- content: "",
963
- toolCalls: [tool],
964
- parts: [{ type: "tools", toolCalls: [tool] }],
965
- };
966
- }
967
- function restoredTaskLifecycleMessages(manager) {
954
+ function restoredTaskLifecycles(manager) {
968
955
  let entries;
969
956
  try {
970
957
  entries = manager.getEntries();
@@ -981,7 +968,10 @@ function restoredTaskLifecycleMessages(manager) {
981
968
  }
982
969
  }
983
970
  const starts = new Map();
984
- const completed = new Map();
971
+ // Ids restart at task_0001 per process: count launches per id so each
972
+ // terminal marker lands on the launch row it belongs to.
973
+ const launches = new Map();
974
+ const terminals = [];
985
975
  for (let index = startIndex; index < entries.length; index += 1) {
986
976
  const entry = entries[index];
987
977
  if (entry?.type !== "marker" || !entry.value)
@@ -998,6 +988,7 @@ function restoredTaskLifecycleMessages(manager) {
998
988
  continue;
999
989
  if (entry.kind === "task_started") {
1000
990
  starts.set(id, payload);
991
+ launches.set(id, (launches.get(id) ?? 0) + 1);
1001
992
  continue;
1002
993
  }
1003
994
  if (entry.kind !== "task_finished" && entry.kind !== "task_killed")
@@ -1010,8 +1001,11 @@ function restoredTaskLifecycleMessages(manager) {
1010
1001
  : "completed";
1011
1002
  const startedAt = numeric(payload.startedAt) ?? numeric(start.startedAt) ?? entry.timestamp;
1012
1003
  const endedAt = numeric(payload.endedAt) ?? entry.timestamp;
1013
- completed.set(id, {
1014
- order: index,
1004
+ const seen = launches.get(id) ?? 0;
1005
+ terminals.push({
1006
+ // A start persisted before conversation_clear is out of scope: keep its
1007
+ // terminal state detached rather than landing it on a later reuse.
1008
+ occurrence: seen > 0 ? seen - 1 : UNMATCHED_LAUNCH,
1015
1009
  task: {
1016
1010
  kind: "task",
1017
1011
  id,
@@ -1030,9 +1024,7 @@ function restoredTaskLifecycleMessages(manager) {
1030
1024
  output: stringValue(payload.output),
1031
1025
  });
1032
1026
  }
1033
- return [...completed.values()]
1034
- .sort((left, right) => left.order - right.order)
1035
- .map(({ task, output }) => taskLifecycleDisplayMessage(task, output));
1027
+ return terminals;
1036
1028
  }
1037
1029
  function numeric(value) {
1038
1030
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
@@ -179,7 +179,7 @@ export class GoalRuntimeController {
179
179
  persistGoal(goal) {
180
180
  try {
181
181
  const manager = this.deps.getSessionManager();
182
- manager.setMetadata({ ...manager.getMetadata(), goal: goal ?? undefined });
182
+ manager.mutateMetadata((current) => ({ ...current, goal: goal ?? undefined }));
183
183
  }
184
184
  catch {
185
185
  // Persistence is best-effort; a filesystem failure must not break a run.
@@ -42,7 +42,7 @@ export class SessionTransitionController {
42
42
  this.deps.agent.setSessionID(manager.getSessionFile());
43
43
  // 7. External binding refresh comes from the new manager's metadata.
44
44
  void manager.getMetadata().externalRuntime;
45
- // 8. Drop live subagent accumulators (ghost-group defense).
45
+ // 8. Drop child-run accumulators (ghost-group defense).
46
46
  this.deps.clearLiveSubagentTools();
47
47
  // 11. Settle every blocking request (behavior delta vs legacy).
48
48
  this.deps.overlays.settleAll("session-switch");
@@ -41,6 +41,8 @@ export interface SubagentGroup {
41
41
  label: string;
42
42
  members: SubagentDisplay[];
43
43
  }
44
+ /** Maps every tool call in the transcript; returns the same array when nothing changed. */
45
+ export declare function mapTranscriptTools(messages: DisplayMessage[], updateTool: (tool: DisplayToolCall) => DisplayToolCall): DisplayMessage[];
44
46
  /**
45
47
  * Projects a newer lifecycle snapshot back onto every existing trace that
46
48
  * already references the same child. spawn_agent settles as soon as the child
@@ -58,29 +60,19 @@ export declare function synchronizeSubagentSnapshots(messages: DisplayMessage[])
58
60
  */
59
61
  export declare function mergeToolMetadata(current: ToolResultMetadata | undefined, incoming: ToolResultMetadata | undefined): ToolResultMetadata | undefined;
60
62
  /**
61
- * Drops accumulator entries whose every member reached a final status: by then
62
- * the authoritative snapshot lives in the settled transcript (wait_workflow /
63
- * wait_agent result), so the entry is dead weight that would otherwise pile up
64
- * for the life of the process. Entries with running/queued members stay — for
65
- * a workflow spanning turns they are the only live view. Returns whether the
66
- * map changed (callers bump their version counter on true).
63
+ * Applies a subagent tool_update whose launching call has already settled out
64
+ * of the streaming round. Children keep reporting against the launching
65
+ * spawn_agent / run_workflow call id across later rounds (e.g. while a
66
+ * wait_workflow blocks), so the update is merged into that entry by id — which
67
+ * absorbs members a workflow spawned after commit — and then projected onto
68
+ * every echo referencing the same members. The settled transcript is the only
69
+ * data source; the live tail never carries synthetic tool rows.
67
70
  */
68
- export declare function pruneSettledLiveSubagentTools(map: Map<string, DisplayToolCall>): boolean;
69
- /**
70
- * Accumulates a subagent tool_update whose originating tool call has already
71
- * settled out of the current streaming round. The TUI clears its streaming
72
- * toolCalls on every turn_start, but background children keep reporting
73
- * against the launching run_workflow/spawn_agent call id across later rounds
74
- * (e.g. while a wait_workflow blocks) — without this side-channel those
75
- * updates are dropped and traces only appear after the whole team finishes.
76
- * Entries act as synthetic tool calls feeding collectSubagentGroups.
77
- * Returns true when the update was absorbed.
78
- */
79
- export declare function accumulateLiveSubagentUpdate(map: Map<string, DisplayToolCall>, event: {
71
+ export declare function applySubagentUpdateToMessages(messages: DisplayMessage[], update: {
80
72
  id: string;
81
73
  name: string;
82
74
  metadata?: ToolResultMetadata;
83
- }): boolean;
75
+ }): DisplayMessage[];
84
76
  /**
85
77
  * Collects every spawned subagent from the live transcript + streaming tools,
86
78
  * grouped by their originating tool call, for the inspector. Pure.
@@ -103,6 +103,39 @@ function freshestSubagentSnapshots(messages) {
103
103
  }
104
104
  return [...freshest.values()];
105
105
  }
106
+ /** Maps every tool call in the transcript; returns the same array when nothing changed. */
107
+ export function mapTranscriptTools(messages, updateTool) {
108
+ let anyChanged = false;
109
+ const updated = messages.map((message) => {
110
+ let messageChanged = false;
111
+ const toolCalls = message.toolCalls?.map((tool) => {
112
+ const next = updateTool(tool);
113
+ if (next !== tool)
114
+ messageChanged = true;
115
+ return next;
116
+ });
117
+ const parts = message.parts?.map((part) => {
118
+ if (part.type !== "tools")
119
+ return part;
120
+ let partChanged = false;
121
+ const nextTools = part.toolCalls.map((tool) => {
122
+ const next = updateTool(tool);
123
+ if (next !== tool)
124
+ partChanged = true;
125
+ return next;
126
+ });
127
+ if (!partChanged)
128
+ return part;
129
+ messageChanged = true;
130
+ return { ...part, toolCalls: nextTools };
131
+ });
132
+ if (!messageChanged)
133
+ return message;
134
+ anyChanged = true;
135
+ return { ...message, toolCalls, parts };
136
+ });
137
+ return anyChanged ? updated : messages;
138
+ }
106
139
  /**
107
140
  * Projects a newer lifecycle snapshot back onto every existing trace that
108
141
  * already references the same child. spawn_agent settles as soon as the child
@@ -140,36 +173,7 @@ export function mergeSubagentSnapshotsIntoMessages(messages, incoming) {
140
173
  });
141
174
  return changed ? { ...tool, metadata: { ...tool.metadata, subagents } } : tool;
142
175
  };
143
- let anyChanged = false;
144
- const updated = messages.map((message) => {
145
- let messageChanged = false;
146
- const toolCalls = message.toolCalls?.map((tool) => {
147
- const next = updateTool(tool);
148
- if (next !== tool)
149
- messageChanged = true;
150
- return next;
151
- });
152
- const parts = message.parts?.map((part) => {
153
- if (part.type !== "tools")
154
- return part;
155
- let partChanged = false;
156
- const nextTools = part.toolCalls.map((tool) => {
157
- const next = updateTool(tool);
158
- if (next !== tool)
159
- partChanged = true;
160
- return next;
161
- });
162
- if (!partChanged)
163
- return part;
164
- messageChanged = true;
165
- return { ...part, toolCalls: nextTools };
166
- });
167
- if (!messageChanged)
168
- return message;
169
- anyChanged = true;
170
- return { ...message, toolCalls, parts };
171
- });
172
- return anyChanged ? updated : messages;
176
+ return mapTranscriptTools(messages, updateTool);
173
177
  }
174
178
  /** Reconciles persisted launch/wait/list echoes when rebuilding a session. */
175
179
  export function synchronizeSubagentSnapshots(messages) {
@@ -210,64 +214,31 @@ export function mergeToolMetadata(current, incoming) {
210
214
  subagents: [...byId.values()],
211
215
  };
212
216
  }
213
- const FINAL_MEMBER_STATUSES = new Set(["completed", "failed", "blocked", "cancelled", "closed"]);
214
217
  /**
215
- * Drops accumulator entries whose every member reached a final status: by then
216
- * the authoritative snapshot lives in the settled transcript (wait_workflow /
217
- * wait_agent result), so the entry is dead weight that would otherwise pile up
218
- * for the life of the process. Entries with running/queued members stay — for
219
- * a workflow spanning turns they are the only live view. Returns whether the
220
- * map changed (callers bump their version counter on true).
218
+ * Applies a subagent tool_update whose launching call has already settled out
219
+ * of the streaming round. Children keep reporting against the launching
220
+ * spawn_agent / run_workflow call id across later rounds (e.g. while a
221
+ * wait_workflow blocks), so the update is merged into that entry by id — which
222
+ * absorbs members a workflow spawned after commit — and then projected onto
223
+ * every echo referencing the same members. The settled transcript is the only
224
+ * data source; the live tail never carries synthetic tool rows.
221
225
  */
222
- export function pruneSettledLiveSubagentTools(map) {
223
- let changed = false;
224
- for (const [id, tc] of map) {
225
- const members = Array.isArray(tc.metadata?.subagents) ? tc.metadata.subagents : [];
226
- const allFinal = members.every((member) => typeof member === "object" && member !== null
227
- && FINAL_MEMBER_STATUSES.has(String(member.status)));
228
- if (allFinal) {
229
- map.delete(id);
230
- changed = true;
231
- }
232
- }
233
- return changed;
234
- }
235
- /**
236
- * Accumulates a subagent tool_update whose originating tool call has already
237
- * settled out of the current streaming round. The TUI clears its streaming
238
- * toolCalls on every turn_start, but background children keep reporting
239
- * against the launching run_workflow/spawn_agent call id across later rounds
240
- * (e.g. while a wait_workflow blocks) — without this side-channel those
241
- * updates are dropped and traces only appear after the whole team finishes.
242
- * Entries act as synthetic tool calls feeding collectSubagentGroups.
243
- * Returns true when the update was absorbed.
244
- */
245
- export function accumulateLiveSubagentUpdate(map, event) {
246
- if (event.metadata?.kind !== "subagent")
247
- return false;
248
- const prev = map.get(event.id);
249
- const metadata = mergeToolMetadata(prev?.metadata, event.metadata);
250
- if (!metadata)
251
- return false;
252
- // Per-child updates carry no mode; group them under the launching workflow
253
- // call rather than falling back to one single-agent group per member.
254
- if (event.name === "run_workflow" && metadata.mode === undefined)
255
- metadata.mode = "workflow";
256
- const members = Array.isArray(metadata.subagents)
257
- ? metadata.subagents.filter((member) => (typeof member === "object" && member !== null))
258
- : [];
259
- const statuses = members.map((member) => String(member.status ?? "running"));
260
- const stillRunning = statuses.some((status) => status === "queued" || status === "running");
261
- const failed = statuses.some((status) => status === "failed" || status === "blocked" || status === "cancelled");
262
- map.set(event.id, {
263
- id: event.id,
264
- name: event.name,
265
- args: prev?.args ?? {},
266
- metadata,
267
- status: stillRunning ? "running" : failed ? "failed" : "completed",
268
- isError: failed,
226
+ export function applySubagentUpdateToMessages(messages, update) {
227
+ if (update.metadata?.kind !== "subagent")
228
+ return messages;
229
+ const byId = mapTranscriptTools(messages, (tool) => {
230
+ if (tool.id !== update.id)
231
+ return tool;
232
+ const metadata = mergeToolMetadata(tool.metadata, update.metadata);
233
+ if (!metadata || metadata === tool.metadata)
234
+ return tool;
235
+ // Per-child updates carry no mode; keep them grouped under the launching
236
+ // workflow call rather than falling back to one single-agent group each.
237
+ if (update.name === "run_workflow" && metadata.mode === undefined)
238
+ metadata.mode = "workflow";
239
+ return { ...tool, metadata };
269
240
  });
270
- return true;
241
+ return mergeSubagentSnapshotsIntoMessages(byId, update.metadata);
271
242
  }
272
243
  /**
273
244
  * Collects every spawned subagent from the live transcript + streaming tools,