@bastani/atomic 0.9.5-alpha.7 → 0.9.5-alpha.8

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/CHANGELOG.md +17 -2
  2. package/dist/builtin/cursor/package.json +2 -2
  3. package/dist/builtin/intercom/CHANGELOG.md +17 -0
  4. package/dist/builtin/intercom/README.md +27 -21
  5. package/dist/builtin/intercom/broker/broker.ts +3 -6
  6. package/dist/builtin/intercom/broker/paths.ts +42 -5
  7. package/dist/builtin/intercom/broker/spawn.ts +78 -10
  8. package/dist/builtin/intercom/config.ts +6 -3
  9. package/dist/builtin/intercom/contact-supervisor-tool.ts +6 -4
  10. package/dist/builtin/intercom/index-heavy.ts +1 -1
  11. package/dist/builtin/intercom/index.ts +162 -130
  12. package/dist/builtin/intercom/intercom-tool.ts +9 -3
  13. package/dist/builtin/intercom/package.json +10 -1
  14. package/dist/builtin/intercom/result-renderers.ts +1 -1
  15. package/dist/builtin/intercom/ui/compose.ts +2 -2
  16. package/dist/builtin/intercom/ui/inline-message.ts +2 -2
  17. package/dist/builtin/intercom/ui/session-list.ts +2 -2
  18. package/dist/builtin/mcp/package.json +1 -1
  19. package/dist/builtin/subagents/CHANGELOG.md +6 -0
  20. package/dist/builtin/subagents/package.json +1 -1
  21. package/dist/builtin/subagents/src/tui/render-event-formatting.ts +15 -48
  22. package/dist/builtin/subagents/src/tui/render-layout.ts +9 -0
  23. package/dist/builtin/subagents/src/tui/render-widget-graph.ts +27 -25
  24. package/dist/builtin/subagents/src/tui/render-widget.ts +46 -69
  25. package/dist/builtin/subagents/src/tui/render.ts +1 -1
  26. package/dist/builtin/web-access/package.json +1 -1
  27. package/dist/builtin/workflows/CHANGELOG.md +11 -0
  28. package/dist/builtin/workflows/README.md +3 -1
  29. package/dist/builtin/workflows/builtin/goal-artifacts.ts +3 -1
  30. package/dist/builtin/workflows/builtin/goal-prompts.ts +23 -2
  31. package/dist/builtin/workflows/builtin/goal-reducer.ts +21 -0
  32. package/dist/builtin/workflows/builtin/goal-review.ts +60 -6
  33. package/dist/builtin/workflows/builtin/goal-runner.ts +32 -21
  34. package/dist/builtin/workflows/builtin/goal-types.ts +7 -1
  35. package/dist/builtin/workflows/builtin/ralph-core.ts +52 -4
  36. package/dist/builtin/workflows/builtin/ralph-review-gate.ts +10 -3
  37. package/dist/builtin/workflows/builtin/ralph-reviewer-prompt.ts +23 -2
  38. package/dist/builtin/workflows/builtin/ralph-runner.ts +38 -16
  39. package/dist/builtin/workflows/builtin/review-convergence.ts +111 -0
  40. package/dist/builtin/workflows/package.json +1 -1
  41. package/dist/builtin/workflows/skills/create-spec/SKILL.md +2 -0
  42. package/dist/builtin/workflows/src/durable/child-primitive.ts +12 -8
  43. package/dist/builtin/workflows/src/durable/dbos-backend.ts +47 -2
  44. package/dist/builtin/workflows/src/durable/dbos-envelope.ts +28 -0
  45. package/dist/builtin/workflows/src/durable/stage-primitive.ts +165 -27
  46. package/dist/builtin/workflows/src/durable/types.ts +14 -1
  47. package/dist/builtin/workflows/src/engine/run.ts +12 -13
  48. package/dist/builtin/workflows/src/extension/workflow-prompts.ts +2 -0
  49. package/dist/builtin/workflows/src/runs/foreground/executor-continuation.ts +13 -3
  50. package/dist/builtin/workflows/src/runs/foreground/executor-stage-factory.ts +1 -1
  51. package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +18 -19
  52. package/dist/builtin/workflows/src/runs/foreground/stage-runner-unresolved-overflow.ts +38 -0
  53. package/dist/builtin/workflows/src/shared/persistence-restore-helpers.ts +18 -1
  54. package/dist/builtin/workflows/src/shared/persistence-restore.ts +23 -5
  55. package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +4 -0
  56. package/dist/bun/internal-intercom-broker.d.ts +6 -0
  57. package/dist/bun/internal-intercom-broker.d.ts.map +1 -0
  58. package/dist/bun/internal-intercom-broker.js +39 -0
  59. package/dist/bun/internal-intercom-broker.js.map +1 -0
  60. package/dist/bun/split-loader.d.ts.map +1 -1
  61. package/dist/bun/split-loader.js +21 -8
  62. package/dist/bun/split-loader.js.map +1 -1
  63. package/dist/core/agent-session-auto-compaction.d.ts +4 -5
  64. package/dist/core/agent-session-auto-compaction.d.ts.map +1 -1
  65. package/dist/core/agent-session-auto-compaction.js +49 -9
  66. package/dist/core/agent-session-auto-compaction.js.map +1 -1
  67. package/dist/core/agent-session-events.d.ts.map +1 -1
  68. package/dist/core/agent-session-events.js +3 -1
  69. package/dist/core/agent-session-events.js.map +1 -1
  70. package/dist/core/agent-session-methods.d.ts +9 -3
  71. package/dist/core/agent-session-methods.d.ts.map +1 -1
  72. package/dist/core/agent-session-methods.js.map +1 -1
  73. package/dist/core/agent-session-models.d.ts +2 -2
  74. package/dist/core/agent-session-models.d.ts.map +1 -1
  75. package/dist/core/agent-session-models.js.map +1 -1
  76. package/dist/core/agent-session-prompt.d.ts.map +1 -1
  77. package/dist/core/agent-session-prompt.js +1 -0
  78. package/dist/core/agent-session-prompt.js.map +1 -1
  79. package/dist/core/agent-session-retry.d.ts +2 -0
  80. package/dist/core/agent-session-retry.d.ts.map +1 -1
  81. package/dist/core/agent-session-retry.js +152 -4
  82. package/dist/core/agent-session-retry.js.map +1 -1
  83. package/dist/core/agent-session-services.d.ts +1 -0
  84. package/dist/core/agent-session-services.d.ts.map +1 -1
  85. package/dist/core/agent-session-services.js +1 -0
  86. package/dist/core/agent-session-services.js.map +1 -1
  87. package/dist/core/agent-session-types.d.ts +15 -1
  88. package/dist/core/agent-session-types.d.ts.map +1 -1
  89. package/dist/core/agent-session-types.js.map +1 -1
  90. package/dist/core/agent-session.d.ts +4 -0
  91. package/dist/core/agent-session.d.ts.map +1 -1
  92. package/dist/core/agent-session.js +4 -0
  93. package/dist/core/agent-session.js.map +1 -1
  94. package/dist/core/compaction/context-compaction-runner.d.ts.map +1 -1
  95. package/dist/core/compaction/context-compaction-runner.js +36 -20
  96. package/dist/core/compaction/context-compaction-runner.js.map +1 -1
  97. package/dist/core/copilot-anthropic-sse-repair.d.ts +23 -0
  98. package/dist/core/copilot-anthropic-sse-repair.d.ts.map +1 -0
  99. package/dist/core/copilot-anthropic-sse-repair.js +340 -0
  100. package/dist/core/copilot-anthropic-sse-repair.js.map +1 -0
  101. package/dist/core/copilot-gemini-reasoning.d.ts +16 -8
  102. package/dist/core/copilot-gemini-reasoning.d.ts.map +1 -1
  103. package/dist/core/copilot-gemini-reasoning.js +27 -22
  104. package/dist/core/copilot-gemini-reasoning.js.map +1 -1
  105. package/dist/core/copilot-hosts.d.ts +12 -0
  106. package/dist/core/copilot-hosts.d.ts.map +1 -0
  107. package/dist/core/copilot-hosts.js +33 -0
  108. package/dist/core/copilot-hosts.js.map +1 -0
  109. package/dist/core/extensions/agent-events.d.ts +1 -1
  110. package/dist/core/extensions/agent-events.d.ts.map +1 -1
  111. package/dist/core/extensions/agent-events.js.map +1 -1
  112. package/dist/core/http-dispatcher.d.ts.map +1 -1
  113. package/dist/core/http-dispatcher.js +5 -5
  114. package/dist/core/http-dispatcher.js.map +1 -1
  115. package/dist/core/sdk-types.d.ts +2 -0
  116. package/dist/core/sdk-types.d.ts.map +1 -1
  117. package/dist/core/sdk-types.js.map +1 -1
  118. package/dist/core/sdk.d.ts.map +1 -1
  119. package/dist/core/sdk.js +1 -0
  120. package/dist/core/sdk.js.map +1 -1
  121. package/dist/core/settings-manager-basic-accessors.d.ts +1 -0
  122. package/dist/core/settings-manager-basic-accessors.d.ts.map +1 -1
  123. package/dist/core/settings-manager-basic-accessors.js +6 -0
  124. package/dist/core/settings-manager-basic-accessors.js.map +1 -1
  125. package/dist/core/settings-types.d.ts +1 -0
  126. package/dist/core/settings-types.d.ts.map +1 -1
  127. package/dist/core/settings-types.js.map +1 -1
  128. package/dist/index-extensions.d.ts +1 -1
  129. package/dist/index-extensions.d.ts.map +1 -1
  130. package/dist/index-extensions.js.map +1 -1
  131. package/dist/main-deferred-startup.d.ts +12 -0
  132. package/dist/main-deferred-startup.d.ts.map +1 -1
  133. package/dist/main-deferred-startup.js +21 -0
  134. package/dist/main-deferred-startup.js.map +1 -1
  135. package/dist/main-early-input.d.ts +41 -0
  136. package/dist/main-early-input.d.ts.map +1 -0
  137. package/dist/main-early-input.js +147 -0
  138. package/dist/main-early-input.js.map +1 -0
  139. package/dist/main.d.ts +0 -6
  140. package/dist/main.d.ts.map +1 -1
  141. package/dist/main.js +19 -13
  142. package/dist/main.js.map +1 -1
  143. package/dist/modes/interactive/components/chat-session-host-events.d.ts.map +1 -1
  144. package/dist/modes/interactive/components/chat-session-host-events.js +15 -0
  145. package/dist/modes/interactive/components/chat-session-host-events.js.map +1 -1
  146. package/dist/modes/interactive/interactive-agent-events.d.ts.map +1 -1
  147. package/dist/modes/interactive/interactive-agent-events.js +24 -0
  148. package/dist/modes/interactive/interactive-agent-events.js.map +1 -1
  149. package/dist/modes/interactive/interactive-input-handling.d.ts.map +1 -1
  150. package/dist/modes/interactive/interactive-input-handling.js +279 -181
  151. package/dist/modes/interactive/interactive-input-handling.js.map +1 -1
  152. package/dist/modes/interactive/interactive-mode-base.d.ts +9 -0
  153. package/dist/modes/interactive/interactive-mode-base.d.ts.map +1 -1
  154. package/dist/modes/interactive/interactive-mode-base.js +36 -0
  155. package/dist/modes/interactive/interactive-mode-base.js.map +1 -1
  156. package/dist/modes/interactive/interactive-mode-surface.d.ts +4 -0
  157. package/dist/modes/interactive/interactive-mode-surface.d.ts.map +1 -1
  158. package/dist/modes/interactive/interactive-mode-surface.js.map +1 -1
  159. package/dist/modes/interactive/interactive-mode-types.d.ts +3 -0
  160. package/dist/modes/interactive/interactive-mode-types.d.ts.map +1 -1
  161. package/dist/modes/interactive/interactive-mode-types.js.map +1 -1
  162. package/dist/modes/interactive/interactive-render-chat.d.ts.map +1 -1
  163. package/dist/modes/interactive/interactive-render-chat.js +21 -9
  164. package/dist/modes/interactive/interactive-render-chat.js.map +1 -1
  165. package/dist/modes/interactive/interactive-startup.d.ts.map +1 -1
  166. package/dist/modes/interactive/interactive-startup.js +6 -1
  167. package/dist/modes/interactive/interactive-startup.js.map +1 -1
  168. package/docs/compaction.md +4 -0
  169. package/docs/json.md +3 -1
  170. package/docs/providers.md +1 -0
  171. package/docs/rpc.md +3 -1
  172. package/docs/settings.md +26 -0
  173. package/docs/terminal-setup.md +4 -0
  174. package/docs/usage.md +2 -2
  175. package/docs/workflows.md +13 -3
  176. package/npm-shrinkwrap.json +513 -23
  177. package/package.json +3 -2
@@ -5,7 +5,7 @@ import { formatDuration, formatModelThinking, shortenPath } from "../shared/form
5
5
  import { formatNestedAggregate } from "../runs/shared/nested-render.ts";
6
6
  import { formatAgentRunningLabel } from "../shared/status-format.ts";
7
7
  import { buildLiveStatusLine, formatCurrentToolLine, formatTokenStat, formatToolUseStat, statJoin } from "./render-status-progress.ts";
8
- import { runningGlyph, runningSeed, truncLine, type Theme } from "./render-layout.ts";
8
+ import { runningGlyph, runningPulseGlyph, truncLine, type Theme } from "./render-layout.ts";
9
9
 
10
10
  export function formatWidgetAgents(agents: string[]): string {
11
11
  const distinct = [...new Set(agents)];
@@ -40,47 +40,9 @@ export function widgetActivity(job: AsyncJobState): string {
40
40
  return "Done";
41
41
  }
42
42
 
43
- export function widgetStepRunningSeed(step: NonNullable<AsyncJobState["steps"]>[number], fallbackIndex?: number): number | undefined {
44
- return runningSeed(
45
- fallbackIndex,
46
- step.index,
47
- step.toolCount,
48
- step.turnCount,
49
- step.tokens?.total,
50
- step.lastActivityAt,
51
- step.currentToolStartedAt,
52
- step.durationMs,
53
- );
54
- }
55
-
56
- export function widgetStepsRunningSeed(steps: Array<NonNullable<AsyncJobState["steps"]>[number]> | undefined): number | undefined {
57
- let seed: number | undefined;
58
- for (const [index, step] of (steps ?? []).entries()) seed = runningSeed(seed, widgetStepRunningSeed(step, index));
59
- return seed;
60
- }
61
-
62
- export function widgetJobRunningSeed(job: AsyncJobState): number | undefined {
63
- return runningSeed(
64
- job.updatedAt,
65
- job.lastActivityAt,
66
- job.toolCount,
67
- job.turnCount,
68
- job.totalTokens?.total,
69
- job.currentStep,
70
- job.runningSteps,
71
- job.completedSteps,
72
- widgetStepsRunningSeed(job.steps),
73
- );
74
- }
75
-
76
- export function widgetJobsRunningSeed(jobs: AsyncJobState[]): number | undefined {
77
- let seed: number | undefined;
78
- for (const job of jobs) seed = runningSeed(seed, widgetJobRunningSeed(job));
79
- return seed;
80
- }
81
43
 
82
- export function widgetStatusGlyph(job: AsyncJobState, theme: Theme, now?: number): string {
83
- if (job.status === "running") return theme.fg("accent", runningGlyph(widgetJobRunningSeed(job), now));
44
+ export function widgetStatusGlyph(job: AsyncJobState, theme: Theme, pulseFrame?: number): string {
45
+ if (job.status === "running") return theme.fg("accent", runningPulseGlyph(pulseFrame));
84
46
  if (job.status === "queued") return theme.fg("muted", "◦");
85
47
  if (job.status === "complete") return theme.fg("success", "✓");
86
48
  if (job.status === "paused") return theme.fg("warning", "■");
@@ -95,6 +57,14 @@ export function widgetStepGlyph(status: AsyncJobStep["status"], theme: Theme, se
95
57
  return theme.fg("muted", "◦");
96
58
  }
97
59
 
60
+ export function widgetStepPulseGlyph(status: AsyncJobStep["status"], theme: Theme, pulseFrame?: number): string {
61
+ if (status === "running") return theme.fg("accent", runningPulseGlyph(pulseFrame));
62
+ if (status === "complete" || status === "completed") return theme.fg("success", "✓");
63
+ if (status === "failed") return theme.fg("error", "✗");
64
+ if (status === "paused") return theme.fg("warning", "■");
65
+ return theme.fg("muted", "◦");
66
+ }
67
+
98
68
  export function widgetStepStatus(status: AsyncJobStep["status"], theme: Theme): string {
99
69
  if (status === "running") return theme.fg("accent", "running");
100
70
  if (status === "complete" || status === "completed") return theme.fg("success", "complete");
@@ -186,17 +156,14 @@ export function nestedRunName(run: NestedRunSummary): string {
186
156
  return run.id;
187
157
  }
188
158
 
189
- export function nestedStatusGlyph(state: NestedRunSummary["state"] | NestedStepSummary["status"], theme: Theme, seed?: number, now?: number): string {
190
- if (state === "running") return theme.fg("accent", runningGlyph(seed, now));
159
+ export function nestedStatusGlyph(state: NestedRunSummary["state"] | NestedStepSummary["status"], theme: Theme, pulseFrame?: number): string {
160
+ if (state === "running") return theme.fg("accent", runningPulseGlyph(pulseFrame));
191
161
  if (state === "complete" || state === "completed") return theme.fg("success", "✓");
192
162
  if (state === "failed") return theme.fg("error", "✗");
193
163
  if (state === "paused") return theme.fg("warning", "■");
194
164
  return theme.fg("muted", "◦");
195
165
  }
196
166
 
197
- export function nestedRunSeed(run: NestedRunSummary): number | undefined {
198
- return runningSeed(run.lastUpdate, run.lastActivityAt, run.currentStep, run.toolCount, run.turnCount, run.totalTokens?.total, run.currentToolStartedAt);
199
- }
200
167
 
201
168
  export function nestedActivity(input: Pick<NestedRunSummary | NestedStepSummary, "activityState" | "lastActivityAt" | "currentTool" | "currentToolStartedAt" | "currentPath" | "turnCount" | "toolCount">, state: NestedRunSummary["state"] | NestedStepSummary["status"], snapshotNow?: number): string {
202
169
  const facts: string[] = [];
@@ -240,7 +207,7 @@ export function formatNestedWidgetLines(children: NestedRunSummary[] | undefined
240
207
  }
241
208
  const activity = nestedActivity(child, child.state, snapshotNow ?? child.lastUpdate);
242
209
  const error = child.error ? ` · ${child.error}` : "";
243
- lines.push(theme.fg("dim", `${prefix}↳ ${nestedStatusGlyph(child.state, theme, nestedRunSeed(child), now)} ${nestedRunName(child)} · ${child.state} · ${activity}${error}`));
210
+ lines.push(theme.fg("dim", `${prefix}↳ ${nestedStatusGlyph(child.state, theme, now)} ${nestedRunName(child)} · ${child.state} · ${activity}${error}`));
244
211
  if (depth === maxDepth) {
245
212
  const aggregate = formatNestedAggregate([...(child.steps?.flatMap((step) => step.children ?? []) ?? []), ...(child.children ?? [])]);
246
213
  if (aggregate && lines.length < lineBudget) lines.push(theme.fg("dim", `${prefix} ↳ ${aggregate}`));
@@ -248,7 +215,7 @@ export function formatNestedWidgetLines(children: NestedRunSummary[] | undefined
248
215
  }
249
216
  for (const step of child.steps ?? []) {
250
217
  if (lines.length >= lineBudget) return;
251
- lines.push(theme.fg("dim", `${prefix} ↳ ${nestedStatusGlyph(step.status, theme, undefined, now)} ${step.agent} · ${step.status} · ${nestedActivity(step, step.status, snapshotNow ?? child.lastUpdate)}`));
218
+ lines.push(theme.fg("dim", `${prefix} ↳ ${nestedStatusGlyph(step.status, theme, now)} ${step.agent} · ${step.status} · ${nestedActivity(step, step.status, snapshotNow ?? child.lastUpdate)}`));
252
219
  append(step.children, depth + 1, `${prefix} `);
253
220
  }
254
221
  append(child.children, depth + 1, `${prefix} `);
@@ -129,6 +129,15 @@ export function pulseGlyph(frame?: number): string {
129
129
  return PULSE_FRAMES[index % PULSE_FRAMES.length]!;
130
130
  }
131
131
 
132
+ /**
133
+ * Running async/background subagent pulse. It reuses the exact foreground pulse
134
+ * glyph frames while accepting an explicit frame advanced by the widget only on
135
+ * real async progress/status updates, matching foreground result semantics.
136
+ */
137
+ export function runningPulseGlyph(frame?: number): string {
138
+ return pulseGlyph(frame);
139
+ }
140
+
132
141
  export function progressRunningSeed(progress: ProgressSeedSource | undefined): number | undefined {
133
142
  if (!progress) return undefined;
134
143
  return runningSeed(
@@ -14,15 +14,13 @@ import {
14
14
  widgetStatusGlyph,
15
15
  widgetStepActivity,
16
16
  widgetStepActivityLine,
17
- widgetStepGlyph,
18
- widgetStepRunningSeed,
17
+ widgetStepPulseGlyph,
19
18
  widgetStepStats,
20
19
  widgetStepStatus,
21
- widgetStepsRunningSeed,
22
20
  widgetJobName,
23
21
  } from "./render-event-formatting.ts";
24
22
 
25
- export function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth(), now?: number): string[] {
23
+ export function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth(), pulseFrame?: number): string[] {
26
24
  if (!job.steps?.length) return [];
27
25
  const total = job.chainStepCount ?? job.steps.length;
28
26
  const lines: string[] = [];
@@ -30,7 +28,7 @@ export function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded =
30
28
  const steps = job.steps.slice(span.start, span.start + span.count);
31
29
  if (span.isParallel) {
32
30
  const status = aggregateStepStatus(steps);
33
- lines.push(` ${widgetStepGlyph(status, theme, widgetStepsRunningSeed(steps), now)} Step ${span.stepIndex + 1}/${total}: ${themeBold(theme, "parallel group")} ${theme.fg("dim", "·")} ${theme.fg("dim", formatParallelOutcome(steps, span.count))}`);
31
+ lines.push(` ${widgetStepPulseGlyph(status, theme, pulseFrame)} Step ${span.stepIndex + 1}/${total}: ${themeBold(theme, "parallel group")} ${theme.fg("dim", "·")} ${theme.fg("dim", formatParallelOutcome(steps, span.count))}`);
34
32
  continue;
35
33
  }
36
34
  const step = steps[0];
@@ -38,15 +36,15 @@ export function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded =
38
36
  lines.push(` ${theme.fg("dim", `◦ Step ${span.stepIndex + 1}/${total}: pending`)}`);
39
37
  continue;
40
38
  }
41
- lines.push(...foregroundStyleWidgetStepLines(job, theme, step, "Step", span.stepIndex + 1, total, expanded, width, now));
39
+ lines.push(...foregroundStyleWidgetStepLines(job, theme, step, "Step", span.stepIndex + 1, total, expanded, width, pulseFrame));
42
40
  }
43
41
  return lines;
44
42
  }
45
43
 
46
- export function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth(), now?: number): string[] {
44
+ export function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth(), _now?: number, pulseFrame?: number): string[] {
47
45
  if (!job.steps?.length) return [];
48
46
  if (job.mode !== "parallel" && job.mode !== "chain") return [];
49
- if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width, now);
47
+ if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width, pulseFrame);
50
48
  const total = job.stepsTotal ?? job.steps.length;
51
49
  const lines: string[] = [];
52
50
  for (const [index, step] of job.steps.entries()) {
@@ -54,8 +52,8 @@ export function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme, exp
54
52
  const activity = widgetStepActivity(step, job.updatedAt);
55
53
  const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
56
54
  const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking, step.fastMode);
57
- lines.push(` ${theme.fg("dim", `${marker} ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index), now)} ${itemTitle} ${index + 1}/${total}: ${step.agent} · ${widgetStepStatus(step.status, theme)}${modelDisplay}${activity ? ` · ${activity}` : ""}`)}`);
58
- for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, expanded ? 8 : 1, now)) lines.push(` ${nestedLine}`);
55
+ lines.push(` ${theme.fg("dim", `${marker} ${widgetStepPulseGlyph(step.status, theme, pulseFrame)} ${itemTitle} ${index + 1}/${total}: ${step.agent} · ${widgetStepStatus(step.status, theme)}${modelDisplay}${activity ? ` · ${activity}` : ""}`)}`);
56
+ for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, expanded ? 8 : 1, pulseFrame)) lines.push(` ${nestedLine}`);
59
57
  }
60
58
  return lines;
61
59
  }
@@ -69,15 +67,15 @@ export function foregroundStyleWidgetStepLines(
69
67
  total: number,
70
68
  expanded: boolean,
71
69
  width: number,
72
- now?: number,
70
+ pulseFrame?: number,
73
71
  ): string[] {
74
72
  const status = widgetStepStatus(step.status, theme);
75
73
  const stats = widgetStepStats(theme, step);
76
74
  const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking, step.fastMode);
77
- const lines = [` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index - 1), now)} ${itemTitle} ${index}/${total}: ${themeBold(theme, step.agent)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`];
75
+ const lines = [` ${widgetStepPulseGlyph(step.status, theme, pulseFrame)} ${itemTitle} ${index}/${total}: ${themeBold(theme, step.agent)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`];
78
76
  const activity = widgetStepActivityLine(step, width, expanded, job.updatedAt);
79
77
  if (activity) lines.push(` ${theme.fg("dim", `⎿ ${activity}`)}`);
80
- for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, undefined, now)) {
78
+ for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, undefined, pulseFrame)) {
81
79
  lines.push(` ${nestedLine}`);
82
80
  }
83
81
  if (step.status === "running") {
@@ -100,40 +98,40 @@ export function foregroundStyleWidgetStepLines(
100
98
  return lines;
101
99
  }
102
100
 
103
- export function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded: boolean, width: number, now?: number): string[] {
101
+ export function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded: boolean, width: number, pulseFrame?: number): string[] {
104
102
  if (!job.steps?.length) return [
105
103
  ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
106
- ...formatNestedWidgetLines(job.nestedChildren, theme, width, expanded, job.updatedAt, undefined, now).map((line) => ` ${line}`),
104
+ ...formatNestedWidgetLines(job.nestedChildren, theme, width, expanded, job.updatedAt, undefined, pulseFrame).map((line) => ` ${line}`),
107
105
  ];
108
- if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width, now);
106
+ if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width, pulseFrame);
109
107
  const total = job.stepsTotal ?? job.steps.length;
110
108
  const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
111
109
  const lines: string[] = [];
112
110
  for (const [index, step] of job.steps.entries()) {
113
- lines.push(...foregroundStyleWidgetStepLines(job, theme, step, itemTitle, index + 1, total, expanded, width, now));
111
+ lines.push(...foregroundStyleWidgetStepLines(job, theme, step, itemTitle, index + 1, total, expanded, width, pulseFrame));
114
112
  }
115
113
  const attached = new Set(job.steps.flatMap((step) => step.children?.map((child) => child.id) ?? []));
116
114
  const unattached = job.nestedChildren?.filter((child) => !attached.has(child.id)) ?? [];
117
- for (const nestedLine of formatNestedWidgetLines(unattached, theme, width, expanded, job.updatedAt, undefined, now)) {
115
+ for (const nestedLine of formatNestedWidgetLines(unattached, theme, width, expanded, job.updatedAt, undefined, pulseFrame)) {
118
116
  lines.push(` ${nestedLine}`);
119
117
  }
120
118
  return lines;
121
119
  }
122
120
 
123
- export function buildSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, expanded: boolean, now?: number): string[] {
121
+ export function buildSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, expanded: boolean, _now?: number, pulseFrame?: number): string[] {
124
122
  const stats = widgetStats(job, theme);
125
123
  const count = job.mode === "chain" ? job.chainStepCount : job.stepsTotal ?? job.agents?.length ?? job.steps?.length;
126
124
  const mode = widgetJobName(job);
127
125
  const title = `async subagent ${mode}${count && count > 1 ? ` (${count})` : ""}`;
128
126
  return [
129
127
  `${theme.fg("toolTitle", themeBold(theme, title))} ${theme.fg("dim", "· background")}`,
130
- `${widgetStatusGlyph(job, theme, now)} ${themeBold(theme, mode)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
131
- ...foregroundStyleWidgetDetails(job, theme, expanded, width, now),
128
+ `${widgetStatusGlyph(job, theme, pulseFrame)} ${themeBold(theme, mode)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
129
+ ...foregroundStyleWidgetDetails(job, theme, expanded, width, pulseFrame),
132
130
  ].map((line) => truncLine(line, width));
133
131
  }
134
132
 
135
- export function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, now?: number): string[] {
136
- const fullLines = buildSingleWidgetLines(job, theme, width, false, now);
133
+ export function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, _now?: number, pulseFrame?: number): string[] {
134
+ const fullLines = buildSingleWidgetLines(job, theme, width, false, _now, pulseFrame);
137
135
  if (fullLines.length <= 10 || !job.steps?.length || (job.mode !== "parallel" && !job.activeParallelGroup)) return fullLines;
138
136
 
139
137
  const total = job.stepsTotal ?? job.steps.length;
@@ -145,8 +143,12 @@ export function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width
145
143
  const stepStats = widgetStepStats(theme, step);
146
144
  const activitySuffix = activity ? ` ${theme.fg("dim", "·")} ${theme.fg("dim", activity)}` : "";
147
145
  const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking, step.fastMode);
148
- lines.push(` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index), now)} ${itemTitle} ${index + 1}/${total}: ${themeBold(theme, step.agent)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${activitySuffix}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}`);
149
- for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, false, job.updatedAt, undefined, now)) lines.push(` ${nestedLine}`);
146
+ lines.push(` ${widgetStepPulseGlyph(step.status, theme, pulseFrame)} ${itemTitle} ${index + 1}/${total}: ${themeBold(theme, step.agent)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${activitySuffix}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}`);
147
+ for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, false, job.updatedAt, undefined, pulseFrame)) lines.push(` ${nestedLine}`);
148
+ if (step.status === "running") {
149
+ const output = widgetOutputPath(job, step);
150
+ if (output) lines.push(` ${theme.fg("dim", `output: ${shortenPath(output)}`)}`);
151
+ }
150
152
  }
151
153
  if (job.steps.some((step) => step.status === "running")) lines.push(theme.fg("accent", ` Press ${keyHint("app.tools.expand", "for live detail")}`));
152
154
  return lines.map((line) => truncLine(line, width));
@@ -2,8 +2,10 @@ import type { ExtensionContext } from "@bastani/atomic";
2
2
  import { Container, Text, type Component } from "@earendil-works/pi-tui";
3
3
  import * as path from "node:path";
4
4
  import { MAX_WIDGET_JOBS, WIDGET_KEY, type AsyncJobState } from "../shared/types.ts";
5
- import { getTermWidth, RUNNING_ANIMATION_MS, runningGlyph, truncLine, type Theme } from "./render-layout.ts";
5
+ import { getTermWidth, runningPulseGlyph, truncLine, type Theme } from "./render-layout.ts";
6
6
  import { themeBold } from "./render-status-progress.ts";
7
+ import { advanceResultPulseFrame } from "./render-result-animation.ts";
8
+ import { widgetRenderKey } from "./render-stable-output.ts";
7
9
  import {
8
10
  buildSingleWidgetLines,
9
11
  compactSingleWidgetLines,
@@ -13,7 +15,6 @@ import {
13
15
  import {
14
16
  widgetActivity,
15
17
  widgetJobName,
16
- widgetJobsRunningSeed,
17
18
  widgetStats,
18
19
  widgetStatusGlyph,
19
20
  } from "./render-event-formatting.ts";
@@ -26,22 +27,24 @@ class LiveWidgetComponent implements Component {
26
27
  private readonly theme: Theme,
27
28
  private readonly getExpanded: () => boolean,
28
29
  private readonly getNow: () => number,
30
+ private readonly getPulseFrame: () => number | undefined,
29
31
  ) {}
30
32
 
31
33
  render(width: number): string[] {
32
34
  const jobs = this.getJobs();
33
35
  const expanded = this.getExpanded();
34
36
  const now = this.getNow();
35
- const lines = this.buildLines(jobs, width, expanded, now);
37
+ const pulseFrame = this.getPulseFrame();
38
+ const lines = this.buildLines(jobs, width, expanded, now, pulseFrame);
36
39
  this.container.clear();
37
40
  for (const line of fitWidgetLineBudget(lines, this.theme, width, expanded)) this.container.addChild(new Text(line, 1, 0));
38
41
  return this.container.render(width);
39
42
  }
40
43
 
41
- private buildLines(jobs: AsyncJobState[], width: number, expanded: boolean, now: number): string[] {
42
- if (expanded) return buildWidgetLines(jobs, this.theme, width, true, now);
43
- if (jobs.length === 1) return compactSingleWidgetLines(jobs[0]!, this.theme, width, now);
44
- return buildWidgetLines(jobs, this.theme, width, false, now);
44
+ private buildLines(jobs: AsyncJobState[], width: number, expanded: boolean, now: number, pulseFrame: number | undefined): string[] {
45
+ if (expanded) return buildWidgetLines(jobs, this.theme, width, true, now, pulseFrame);
46
+ if (jobs.length === 1) return compactSingleWidgetLines(jobs[0]!, this.theme, width, now, pulseFrame);
47
+ return buildWidgetLines(jobs, this.theme, width, false, now, pulseFrame);
45
48
  }
46
49
 
47
50
  invalidate(): void {
@@ -49,22 +52,23 @@ class LiveWidgetComponent implements Component {
49
52
  }
50
53
  }
51
54
 
52
- function buildWidgetComponent(getJobs: () => AsyncJobState[], getExpanded: () => boolean, getNow: () => number): (_tui: unknown, theme: Theme) => Component {
53
- return (_tui, theme) => new LiveWidgetComponent(getJobs, theme, getExpanded, getNow);
55
+ function buildWidgetComponent(getJobs: () => AsyncJobState[], getExpanded: () => boolean, getNow: () => number, getPulseFrame: () => number | undefined): (_tui: unknown, theme: Theme) => Component {
56
+ return (_tui, theme) => new LiveWidgetComponent(getJobs, theme, getExpanded, getNow, getPulseFrame);
54
57
  }
55
58
 
56
59
  interface RenderRequestingContext {
57
60
  ui: ExtensionContext["ui"] & { requestRender?: () => void };
58
61
  }
59
62
 
60
- // There is only ever one async-agents widget per host process, so the widget
61
- // ticker and mounted component read their driving context/jobs from module-level
63
+ // There is only ever one async-agents widget per host process, so the mounted
64
+ // component reads its driving context/jobs/pulse frame from module-level
62
65
  // singletons instead of remounting the widget for every visible update.
63
66
  let latestWidgetCtx: ExtensionContext | undefined;
64
67
  let latestWidgetJobs: AsyncJobState[] = [];
65
68
  let latestWidgetFrameNow = 0;
66
69
  let latestWidgetExpanded = false;
67
- let widgetTimer: ReturnType<typeof setInterval> | undefined;
70
+ let latestWidgetSnapshotKey: string | undefined;
71
+ let latestWidgetPulseFrame: number | undefined;
68
72
  let mountedWidgetCtx: ExtensionContext | undefined;
69
73
  let mountedWidgetOwnerKey: string | undefined;
70
74
  let widgetMounted = false;
@@ -77,6 +81,10 @@ function getLatestWidgetFrameNow(): number {
77
81
  return latestWidgetFrameNow;
78
82
  }
79
83
 
84
+ function getLatestWidgetPulseFrame(): number | undefined {
85
+ return latestWidgetPulseFrame;
86
+ }
87
+
80
88
  function getLatestWidgetExpanded(): boolean {
81
89
  // LiveWidgetComponent re-renders outside a specific renderWidget() call, so
82
90
  // remember the last expansion state observed from a live host UI. Workflow
@@ -94,6 +102,8 @@ function clearLatestWidgetState(): void {
94
102
  latestWidgetJobs = [];
95
103
  latestWidgetFrameNow = 0;
96
104
  latestWidgetExpanded = false;
105
+ latestWidgetSnapshotKey = undefined;
106
+ latestWidgetPulseFrame = undefined;
97
107
  mountedWidgetCtx = undefined;
98
108
  mountedWidgetOwnerKey = undefined;
99
109
  widgetMounted = false;
@@ -142,63 +152,35 @@ function unmountWidgetBestEffort(ctx: ExtensionContext | undefined): void {
142
152
  }
143
153
  }
144
154
 
145
- function hasAnimatedWidgetJobs(jobs: AsyncJobState[]): boolean {
146
- // Animate while any job — or any of its nested steps — is still running so the
147
- // header/step spinners never freeze before the work actually settles.
148
- return jobs.some((job) => job.status === "running" || job.steps?.some((step) => step.status === "running"));
149
- }
150
-
151
- function refreshAnimatedWidget(): void {
152
- if (!latestWidgetCtx?.hasUI) return;
153
- try {
154
- latestWidgetFrameNow = Date.now();
155
- requestWidgetRender(latestWidgetCtx);
156
- } catch {
157
- // A stale render context means the cosmetic ticker can no longer update the
158
- // mounted widget safely; tear it down best-effort and let the next status
159
- // update remount on the active host context.
160
- stopWidgetAnimation();
161
- }
155
+ function widgetSnapshotKey(jobs: AsyncJobState[]): string {
156
+ return jobs.map(widgetRenderKey).join("\n");
162
157
  }
163
158
 
164
- function ensureWidgetAnimation(): void {
165
- if (widgetTimer) return;
166
- widgetTimer = setInterval(() => {
167
- if (!hasAnimatedWidgetJobs(latestWidgetJobs)) {
168
- stopWidgetAnimation();
169
- return;
170
- }
171
- refreshAnimatedWidget();
172
- }, RUNNING_ANIMATION_MS);
173
- widgetTimer.unref?.();
174
- }
175
-
176
- // Stop only the ticker, keeping the last-rendered widget context/jobs intact.
177
- function stopWidgetTicker(): void {
178
- if (widgetTimer) {
179
- clearInterval(widgetTimer);
180
- widgetTimer = undefined;
181
- }
159
+ function refreshWidgetSnapshot(jobs: AsyncJobState[]): void {
160
+ const snapshotKey = widgetSnapshotKey(jobs);
161
+ if (snapshotKey === latestWidgetSnapshotKey) return;
162
+ latestWidgetSnapshotKey = snapshotKey;
163
+ latestWidgetFrameNow = Date.now();
164
+ latestWidgetPulseFrame = advanceResultPulseFrame(latestWidgetPulseFrame);
182
165
  }
183
166
 
184
- // Full teardown: stop the ticker, clear the mounted widget if possible, and
185
- // forget the driving context/jobs entirely.
167
+ // Full teardown: clear the mounted widget if possible, and forget the driving
168
+ // context/jobs entirely.
186
169
  export function stopWidgetAnimation(): void {
187
- stopWidgetTicker();
188
170
  if (widgetMounted) unmountWidgetBestEffort(mountedWidgetCtx);
189
171
  clearLatestWidgetState();
190
172
  }
191
173
 
192
- export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = getTermWidth(), expanded = false, now: number = Date.now()): string[] {
174
+ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = getTermWidth(), expanded = false, now: number = Date.now(), pulseFrame?: number): string[] {
193
175
  if (jobs.length === 0) return [];
194
- if (jobs.length === 1) return buildSingleWidgetLines(jobs[0]!, theme, width, expanded, now);
176
+ if (jobs.length === 1) return buildSingleWidgetLines(jobs[0]!, theme, width, expanded, now, pulseFrame);
195
177
  const running = jobs.filter((job) => job.status === "running");
196
178
  const queued = jobs.filter((job) => job.status === "queued");
197
179
  const finished = jobs.filter((job) => job.status !== "running" && job.status !== "queued");
198
180
 
199
181
  const lines: string[] = [];
200
182
  const hasActive = running.length > 0 || queued.length > 0;
201
- const headerGlyph = running.length > 0 ? runningGlyph(widgetJobsRunningSeed(running), now) : hasActive ? "●" : "○";
183
+ const headerGlyph = running.length > 0 ? runningPulseGlyph(pulseFrame) : hasActive ? "●" : "○";
202
184
  lines.push(truncLine(`${theme.fg(hasActive ? "accent" : "dim", headerGlyph)} ${theme.fg(hasActive ? "accent" : "dim", "Async agents")} ${theme.fg("dim", "· background")}`, width));
203
185
 
204
186
  const items: string[][] = [];
@@ -211,9 +193,9 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
211
193
  if (slots <= 0) { hiddenRunning++; continue; }
212
194
  const stats = widgetStats(job, theme);
213
195
  items.push([
214
- `${widgetStatusGlyph(job, theme, now)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
196
+ `${widgetStatusGlyph(job, theme, pulseFrame)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
215
197
  ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
216
- ...widgetParallelAgentDetails(job, theme, expanded, width, now),
198
+ ...widgetParallelAgentDetails(job, theme, expanded, width, now, pulseFrame),
217
199
  ]);
218
200
  slots--;
219
201
  }
@@ -228,9 +210,9 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
228
210
  if (slots <= 0) { hiddenFinished++; continue; }
229
211
  const stats = widgetStats(job, theme);
230
212
  items.push([
231
- `${widgetStatusGlyph(job, theme, now)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
213
+ `${widgetStatusGlyph(job, theme, pulseFrame)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
232
214
  ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
233
- ...widgetParallelAgentDetails(job, theme, expanded, width, now),
215
+ ...widgetParallelAgentDetails(job, theme, expanded, width, now, pulseFrame),
234
216
  ]);
235
217
  slots--;
236
218
  }
@@ -280,7 +262,7 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
280
262
  }
281
263
  latestWidgetCtx = ctx;
282
264
  latestWidgetJobs = [...jobs];
283
- latestWidgetFrameNow = Date.now();
265
+ refreshWidgetSnapshot(jobs);
284
266
  if (widgetMounted && mountedWidgetOwnerKey !== ownerKey) {
285
267
  // Session rebinding can leave the previous host UI alive briefly; clear the
286
268
  // old mount before installing the singleton widget on the new owner/context.
@@ -290,13 +272,10 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
290
272
  widgetMounted = false;
291
273
  }
292
274
  if (!widgetMounted) {
293
- // belowEditor: the widget animates a running glyph / elapsed labels on a
294
- // timer. pi-tui full-clears the screen+scrollback whenever a changed line
295
- // sits above the viewport fold, so an aboveEditor widget flickers once the
296
- // bottom region grows tall and pushes it above the fold. Rendering below the
297
- // editor keeps the live line within the bottom viewport (flicker-free), and
298
- // matches the workflow companion widget's placement (#1109).
299
- ctx.ui.setWidget(WIDGET_KEY, buildWidgetComponent(getLatestWidgetJobs, getLatestWidgetExpanded, getLatestWidgetFrameNow), {
275
+ // belowEditor keeps the live async widget pinned to the bottom viewport,
276
+ // matching the workflow companion widget's placement (#1109). The pulse frame
277
+ // is advanced only by semantic async status updates, not by a cosmetic timer.
278
+ ctx.ui.setWidget(WIDGET_KEY, buildWidgetComponent(getLatestWidgetJobs, getLatestWidgetExpanded, getLatestWidgetFrameNow, getLatestWidgetPulseFrame), {
300
279
  placement: "belowEditor",
301
280
  });
302
281
  mountedWidgetCtx = ctx;
@@ -311,8 +290,6 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
311
290
  mountedWidgetOwnerKey = ownerKey;
312
291
  requestWidgetRender(ctx);
313
292
  }
314
- // Keep the just-rendered ctx/jobs as the last-rendered state; only the ticker
315
- // is conditional on whether anything is still animating.
316
- if (hasAnimatedWidgetJobs(jobs)) ensureWidgetAnimation();
317
- else stopWidgetTicker();
293
+ // The async pulse is update-driven like foreground subagent results, so no
294
+ // periodic cosmetic ticker is needed.
318
295
  }
@@ -5,7 +5,7 @@
5
5
  * rendering responsibility across sibling modules.
6
6
  */
7
7
 
8
- export { PULSE_FRAMES, RUNNING_ANIMATION_MS, RUNNING_FRAMES, currentRunningFrame, pulseGlyph } from "./render-layout.ts";
8
+ export { PULSE_FRAMES, RUNNING_ANIMATION_MS, RUNNING_FRAMES, currentRunningFrame, pulseGlyph, runningPulseGlyph } from "./render-layout.ts";
9
9
  export {
10
10
  advanceResultPulseFrame,
11
11
  clearLegacyResultAnimationTimer,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/web-access",
3
- "version": "0.9.5-alpha.7",
3
+ "version": "0.9.5-alpha.8",
4
4
  "private": true,
5
5
  "description": "Atomic extension for web search, URL fetching, GitHub repo cloning, PDF/video extraction. Fork of: https://github.com/nicobailon/pi-web-access",
6
6
  "contributors": [
@@ -6,6 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.9.5-alpha.8] - 2026-07-08
10
+
11
+ ### Fixed
12
+
13
+ - Fixed builtin `goal` and `ralph` reviewer convergence reporting so review artifacts and reducer/controller decisions now persist explicit parse/convergence summaries (`parsed`, `approved`, `stopReviewLoop`, `nextAction`, `finalActionRemaining`, and diagnostics). Malformed or missing structured reviewer output is surfaced as a parse failure distinct from parsed reviewer rejection/findings; PR/MR/review creation is represented as a post-approval final action when it is the only remaining task; successful reviewer convergence records the final action (`pull-request` or `finish`) without consuming another implementation loop; and the workflow authoring docs, default workflow guidance, and create-spec prompt now tell custom reviewer-gated workflows to model authorized PR, release, deployment, or publication work as final actions rather than implementation blockers while writing local, action-oriented prompts that do not rely on workflow/stage names as hidden context.
14
+
15
+ - Fixed workflow stage model fallback for unrecoverable context-window overflow: when Atomic's same-model auto-compaction exhausts its recovery attempt, the stage now advances to the next configured `fallbackModels` tier and, once all tiers are exhausted, stops with the terminal context-overflow error instead of compacting and retrying the same model indefinitely.
16
+ - Fixed workflow controlled-pause ordering around unresolved context overflow: paused stages now honor the pause/resume handshake before surfacing the terminal overflow, and a resume message is not sent over an already-recorded unresolved-overflow signal.
17
+ - Fixed reattached workflow stage fallback after unresolved context overflow: if the restored/resumed model tier exhausts context recovery, fallback now continues with the next tier after that resumed model (or stops terminally when it was the final tier) instead of replaying the primary model.
18
+ - Fixed durable workflow resume hydration so replayed `ctx.parallel`/`ctx.task` fanout keeps its original branch parentage, completed stages restore persisted summaries, timing, session/model metadata, and DBOS/file checkpoints round-trip the richer stage metadata used by status and graph views.
19
+
9
20
  ## [0.9.5-alpha.6] - 2026-07-06
10
21
 
11
22
  ### Fixed
@@ -326,7 +326,7 @@ Atomic registers the canonical `structured_output` tool only for schema-enabled
326
326
 
327
327
  ### Model fallbacks
328
328
 
329
- Stages and high-level task helpers can retry transient provider/model failures with an ordered `fallbackModels` list. The primary `model` is tried first, then each fallback, and finally the current Atomic-selected model when available. Fallbacks are only used for retryable model/provider failures such as rate limits, quota/auth/provider outages, unavailable models, network timeouts, and 5xx errors — ordinary tool, shell, validation, cancellation, and workflow-code failures are not retried.
329
+ Stages and high-level task helpers can retry transient provider/model failures with an ordered `fallbackModels` list. The primary `model` is tried first, then each fallback, and finally the current Atomic-selected model when available. Fallbacks are only used for retryable model/provider failures such as rate limits, quota/auth/provider outages, unavailable models, network timeouts, context-window overflows that Atomic's auto-compaction cannot resolve on the current model, and 5xx errors — ordinary tool, shell, validation, cancellation, and workflow-code failures are not retried.
330
330
 
331
331
  ```typescript
332
332
  import { workflow } from "@bastani/workflows";
@@ -544,6 +544,8 @@ Input overrides are bare `key=value` tokens (no leading `--`). Values are JSON-p
544
544
 
545
545
  Workflows always run as **background tasks** in interactive sessions — the chat editor stays free while a run executes. Press **F2** (or `/workflow connect <run-id>`) to attach to the live graph viewer; HIL prompts (`ctx.ui.input/confirm/select/editor/custom`) appear as awaiting-input graph nodes. Press Enter on a focused node, or click a visible graph node directly, to open that stage and answer locally, never as a modal dialog over the chat. Attached stage chats capture mouse/trackpad wheel events by default so scrolling stays inside the active stage transcript or prompt instead of falling through to terminal/main-chat scrollback. Press `ctrl+t` to toggle **copy mode**: copy mode disables workflow-chat mouse reporting so normal terminal/tmux text selection can work; press `ctrl+t` again to leave copy mode and restore workflow-chat scrolling. While copy mode is on, wheel/trackpad gestures are handled by the terminal/tmux and may scroll terminal scrollback, so leave copy mode before using the wheel again. Human input is detected when those runtime `ctx.ui.*` calls execute; workflows no longer have a declaration-time HIL flag.
546
546
 
547
+ Durable resume preserves the original completed-stage shape: replayed stage/task/parallel/child checkpoints retain summaries, durations, session/model metadata, and parallel branch parentage in the graph instead of flattening fanout branches into a sequential replay chain.
548
+
547
549
  Nested `ctx.workflow(...)` calls are displayed as an expanded graph within the top-level run. `/workflow status` and run pickers list only top-level user-launched workflows, not implementation-owned child runs. The `workflow` tool's `stages`, `stage`, `transcript`, `send`, `pause`, `interrupt`, and `resume` actions can still target visible child stage ids, prefixes, or names from the expanded graph; Atomic routes the control action to the owning nested run internally. (`stages`, `stage`, `transcript`, and `send` are `workflow` tool actions, not `/workflow` slash subcommands; the slash command exposes `connect`, `attach`, `pause`, `list`, `status`, `interrupt`, `kill`, `resume`, `reload`, and `inputs`.)
548
550
 
549
551
  Prompt answer replay is live-memory only. `StageSnapshot.promptAnswerState` reports whether continuation can replay a prompt answer (`available`), must ask again because the private ledger entry is gone (`unavailable`), or must ask again because multiple matching prompt nodes are ambiguous (`ambiguous`). Raw answers stay in a private `PromptAnswerRecord` ledger, are never serialized to snapshots or persistence, and remain resident in memory until the answer is cleared, the run is removed, or the store is cleared. Replay keys include prompt kind, message text, select choices, input/editor initial value, custom prompt identity hash, and hashed author callsite, so changing any of those inputs may intentionally re-ask on continuation. Empty `ctx.ui.select(..., [])` calls throw before creating a prompt node. Arbitrary custom-widget answers cannot be supplied with `workflow send`; focus the `custom` awaiting-input node in the interactive graph instead.
@@ -1,6 +1,7 @@
1
1
  import { writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import type { ReviewDecision, ReviewRecord } from "./goal-types.js";
4
+ import type { ReviewConvergenceSummary } from "./review-convergence.js";
4
5
 
5
6
  export function artifactSafeName(value: string): string {
6
7
  const safe = value
@@ -21,6 +22,7 @@ export async function writeReviewArtifact(
21
22
  reviewer: string,
22
23
  decision: ReviewDecision,
23
24
  rawText: string,
25
+ convergenceDecision: ReviewConvergenceSummary,
24
26
  ): Promise<string> {
25
27
  const artifactPath = join(
26
28
  artifactDir,
@@ -28,7 +30,7 @@ export async function writeReviewArtifact(
28
30
  );
29
31
  await writeFile(
30
32
  artifactPath,
31
- `${JSON.stringify({ reviewer, decision, raw_text: rawText }, null, 2)}\n`,
33
+ `${JSON.stringify({ reviewer, decision, convergence_decision: convergenceDecision, raw_text: rawText }, null, 2)}\n`,
32
34
  { encoding: "utf8" },
33
35
  );
34
36
  return artifactPath;