@bastani/atomic 0.9.4-alpha.10 → 0.9.4-alpha.11

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 (70) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/builtin/cursor/package.json +2 -2
  3. package/dist/builtin/intercom/package.json +1 -1
  4. package/dist/builtin/mcp/package.json +1 -1
  5. package/dist/builtin/subagents/CHANGELOG.md +7 -0
  6. package/dist/builtin/subagents/package.json +1 -1
  7. package/dist/builtin/subagents/src/slash/slash-commands.ts +2 -2
  8. package/dist/builtin/subagents/src/tui/render-result-compact.ts +3 -2
  9. package/dist/builtin/subagents/src/tui/render-result.ts +3 -3
  10. package/dist/builtin/subagents/src/tui/render-widget-graph.ts +4 -3
  11. package/dist/builtin/subagents/src/tui/render-widget.ts +10 -4
  12. package/dist/builtin/web-access/package.json +1 -1
  13. package/dist/builtin/workflows/CHANGELOG.md +6 -0
  14. package/dist/builtin/workflows/package.json +1 -1
  15. package/dist/builtin/workflows/src/tui/keybindings-adapter.ts +4 -0
  16. package/dist/builtin/workflows/src/tui/overlay-adapter.ts +4 -0
  17. package/dist/builtin/workflows/src/tui/stage-chat-view-input.ts +12 -1
  18. package/dist/builtin/workflows/src/tui/stage-chat-view-state.ts +2 -0
  19. package/dist/builtin/workflows/src/tui/stage-chat-view-types.ts +6 -0
  20. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +4 -0
  21. package/dist/builtin/workflows/src/tui/workflow-attach-pane-types.ts +4 -0
  22. package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +8 -9
  23. package/dist/core/agent-session-events.d.ts.map +1 -1
  24. package/dist/core/agent-session-events.js +2 -0
  25. package/dist/core/agent-session-events.js.map +1 -1
  26. package/dist/core/agent-session-methods.d.ts +4 -1
  27. package/dist/core/agent-session-methods.d.ts.map +1 -1
  28. package/dist/core/agent-session-methods.js.map +1 -1
  29. package/dist/core/agent-session-tool-registry.d.ts.map +1 -1
  30. package/dist/core/agent-session-tool-registry.js +4 -0
  31. package/dist/core/agent-session-tool-registry.js.map +1 -1
  32. package/dist/core/agent-session.d.ts +4 -1
  33. package/dist/core/agent-session.d.ts.map +1 -1
  34. package/dist/core/agent-session.js +4 -0
  35. package/dist/core/agent-session.js.map +1 -1
  36. package/dist/core/async/format.d.ts +3 -0
  37. package/dist/core/async/format.d.ts.map +1 -0
  38. package/dist/core/async/format.js +50 -0
  39. package/dist/core/async/format.js.map +1 -0
  40. package/dist/core/async/job-manager.d.ts +39 -0
  41. package/dist/core/async/job-manager.d.ts.map +1 -0
  42. package/dist/core/async/job-manager.js +254 -0
  43. package/dist/core/async/job-manager.js.map +1 -0
  44. package/dist/core/async/session-manager.d.ts +18 -0
  45. package/dist/core/async/session-manager.d.ts.map +1 -0
  46. package/dist/core/async/session-manager.js +61 -0
  47. package/dist/core/async/session-manager.js.map +1 -0
  48. package/dist/core/async/types.d.ts +23 -0
  49. package/dist/core/async/types.d.ts.map +1 -0
  50. package/dist/core/async/types.js +2 -0
  51. package/dist/core/async/types.js.map +1 -0
  52. package/dist/core/tools/bash-async-execution.d.ts +25 -0
  53. package/dist/core/tools/bash-async-execution.d.ts.map +1 -0
  54. package/dist/core/tools/bash-async-execution.js +71 -0
  55. package/dist/core/tools/bash-async-execution.js.map +1 -0
  56. package/dist/core/tools/bash-async-jobs.d.ts +10 -0
  57. package/dist/core/tools/bash-async-jobs.d.ts.map +1 -1
  58. package/dist/core/tools/bash-async-jobs.js +18 -2
  59. package/dist/core/tools/bash-async-jobs.js.map +1 -1
  60. package/dist/core/tools/bash-async-output.d.ts +3 -1
  61. package/dist/core/tools/bash-async-output.d.ts.map +1 -1
  62. package/dist/core/tools/bash-async-output.js +4 -1
  63. package/dist/core/tools/bash-async-output.js.map +1 -1
  64. package/dist/core/tools/bash.d.ts +10 -4
  65. package/dist/core/tools/bash.d.ts.map +1 -1
  66. package/dist/core/tools/bash.js +20 -38
  67. package/dist/core/tools/bash.js.map +1 -1
  68. package/docs/tools.md +4 -2
  69. package/npm-shrinkwrap.json +23 -23
  70. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.4-alpha.11] - 2026-07-03
6
+
7
+ ### Fixed
8
+
9
+ - Fixed async `bash({ async: true })` jobs finishing silently: completed or failed session-managed background bash jobs now enqueue an `async-job-result` follow-up message into the originating chat session automatically, while explicit completed-job polling, explicit cancellation, and parent aborts acknowledge the job and suppress duplicate or unwanted idle turns. Suppression is checked again at the streaming boundary so a completed job polled while its automatic follow-up is staged does not later deliver a duplicate. Async delivery bookkeeping is now bounded by the existing background-job retention defaults, suppressions stay tied to retained jobs so disposed-session running jobs cannot later fall back into the owner session, 12KB–50KB follow-up outputs persist their full output path before inline preview truncation, just-under-threshold raw outputs remain fully inline instead of being preview-truncated without a `fullOutputPath`, shared-manager lifecycle tracking prevents owner-session disposal from dropping later-session jobs while cleaning stale handlers from disposed fork/subagent sessions, and non-blocking delivery attempts keep one live streaming session from delaying unrelated async job completions.
10
+ - Fixed a background bash job leak where a failed async-manager registration (disposed manager/session or a capacity race mid-flight) left a never-executed job permanently reported as `running` to `__atomic_bash_job` polls; the managed job entry is now discarded when registration fails and the tool call error is surfaced unchanged.
11
+
5
12
  ## [0.9.4-alpha.10] - 2026-07-03
6
13
 
7
14
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/cursor",
3
- "version": "0.9.4-alpha.10",
3
+ "version": "0.9.4-alpha.11",
4
4
  "private": true,
5
5
  "description": "Experimental first-party Atomic extension for Cursor OAuth, model discovery, and streaming provider registration.",
6
6
  "contributors": [
@@ -40,7 +40,7 @@
40
40
  }
41
41
  },
42
42
  "dependencies": {
43
- "@bastani/atomic-natives": "0.9.4-alpha.10",
43
+ "@bastani/atomic-natives": "0.9.4-alpha.11",
44
44
  "@bufbuild/protobuf": "^2.12.1",
45
45
  "@earendil-works/pi-ai": "^0.80.3"
46
46
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/intercom",
3
- "version": "0.9.4-alpha.10",
3
+ "version": "0.9.4-alpha.11",
4
4
  "private": true,
5
5
  "description": "Atomic extension providing a private coordination channel between parent and child agent sessions. Fork of: https://github.com/nicobailon/pi-intercom",
6
6
  "contributors": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/mcp",
3
- "version": "0.9.4-alpha.10",
3
+ "version": "0.9.4-alpha.11",
4
4
  "private": true,
5
5
  "description": "Atomic extension that adapts MCP (Model Context Protocol) servers into the coding agent. Fork of: https://github.com/nicobailon/pi-mcp-adapter",
6
6
  "contributors": [
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.4-alpha.11] - 2026-07-03
6
+
7
+ ### Fixed
8
+
9
+ - Fixed subagent live-detail hints to render the configured `app.tools.expand` keybinding instead of hardcoding Ctrl+O, so remapped users see the same binding honored by attached workflow stage viewers ([#1607](https://github.com/bastani-inc/atomic/issues/1607)).
10
+ - Fixed the running subagent widget so it preserves the last known expanded/open state across workflow stage-node exit re-renders instead of transiently collapsing when the live widget context is stale until the next subagent status update ([#1619](https://github.com/bastani-inc/atomic/issues/1619)).
11
+
5
12
  ## [0.9.4-alpha.9] - 2026-07-02
6
13
 
7
14
  ### Changed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/subagents",
3
- "version": "0.9.4-alpha.10",
3
+ "version": "0.9.4-alpha.11",
4
4
  "private": true,
5
5
  "description": "Atomic extension for delegating tasks to subagents with chains, parallel execution, and TUI clarification. Fork of: https://github.com/nicobailon/pi-subagents",
6
6
  "contributors": [
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
- import type { ExtensionAPI, ExtensionContext } from "@bastani/atomic";
4
+ import { keyHint, type ExtensionAPI, type ExtensionContext } from "@bastani/atomic";
5
5
  import { Key, matchesKey } from "@earendil-works/pi-tui";
6
6
  import { discoverAgents, discoverAgentsAll, type ChainConfig } from "../agents/agents.ts";
7
7
  import type { SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
@@ -164,7 +164,7 @@ async function requestSlashRun(
164
164
  if (!ctx.hasUI) return;
165
165
  const tool = update.currentTool ? ` ${update.currentTool}` : "";
166
166
  const count = update.toolCount ?? 0;
167
- ctx.ui.setStatus("subagent-slash", `${count} tools${tool} | ctrl+o live detail`);
167
+ ctx.ui.setStatus("subagent-slash", `${count} tools${tool} | ${keyHint("app.tools.expand", "live detail")}`);
168
168
  };
169
169
 
170
170
  const onTerminalInput = ctx.hasUI
@@ -1,3 +1,4 @@
1
+ import { keyHint } from "@bastani/atomic";
1
2
  import { Container, Text, type Component } from "@earendil-works/pi-tui";
2
3
  import type { AgentProgress, AsyncJobStep, Details } from "../shared/types.ts";
3
4
  import { shortenPath } from "../shared/formatters.ts";
@@ -45,7 +46,7 @@ export function renderSingleCompact(d: Details, r: Details["results"][number], t
45
46
  c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${activity}`), width), 0, 0));
46
47
  const liveStatus = buildLiveStatusLine(r.progress, progressSnapshotNow);
47
48
  if (liveStatus && liveStatus !== activity) c.addChild(new Text(truncLine(theme.fg("dim", ` ${liveStatus}`), width), 0, 0));
48
- c.addChild(new Text(truncLine(theme.fg("accent", " Press ctrl+o for live detail"), width), 0, 0));
49
+ c.addChild(new Text(truncLine(theme.fg("accent", ` Press ${keyHint("app.tools.expand", "for live detail")}`), width), 0, 0));
49
50
  if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${shortenPath(r.artifactPaths.outputPath)}`), width), 0, 0));
50
51
  return c;
51
52
  }
@@ -141,7 +142,7 @@ export function renderMultiCompact(d: Details, theme: Theme, now?: number, pulse
141
142
  if (rRunning && rProg && "status" in rProg) {
142
143
  const activity = compactCurrentActivity(rProg, now);
143
144
  c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${activity}`), width), 0, 0));
144
- c.addChild(new Text(truncLine(theme.fg("accent", " Press ctrl+o for live detail"), width), 0, 0));
145
+ c.addChild(new Text(truncLine(theme.fg("accent", ` Press ${keyHint("app.tools.expand", "for live detail")}`), width), 0, 0));
145
146
  } else if (!rPending && (r.exitCode !== 0 || r.interrupted || r.detached || hasEmptyTextOutputWithoutOutputTarget(r.task, output))) {
146
147
  c.addChild(new Text(truncLine(theme.fg(r.exitCode !== 0 ? "error" : "dim", ` ⎿ ${resultStatusLine(r, output)}`), width), 0, 0));
147
148
  }
@@ -1,5 +1,5 @@
1
1
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
- import { getMarkdownTheme } from "@bastani/atomic";
2
+ import { getMarkdownTheme, keyHint } from "@bastani/atomic";
3
3
  import { Container, Markdown, Spacer, Text, type Component } from "@earendil-works/pi-tui";
4
4
  import type { AgentProgress, AsyncJobStep, Details } from "../shared/types.ts";
5
5
  import { formatDuration, formatTokens, formatUsage, shortenPath } from "../shared/formatters.ts";
@@ -116,7 +116,7 @@ export function renderSubagentResult(
116
116
  if (liveStatusLine) {
117
117
  c.addChild(new Text(fit(theme.fg("accent", liveStatusLine)), 0, 0));
118
118
  }
119
- c.addChild(new Text(fit(theme.fg("accent", "Press ctrl+o for live detail")), 0, 0));
119
+ c.addChild(new Text(fit(theme.fg("accent", `Press ${keyHint("app.tools.expand", "for live detail")}`)), 0, 0));
120
120
  if (r.artifactPaths) {
121
121
  c.addChild(new Text(fit(theme.fg("dim", `Artifacts: ${shortenPath(r.artifactPaths.outputPath)}`)), 0, 0));
122
122
  }
@@ -350,7 +350,7 @@ export function renderSubagentResult(
350
350
  if (liveStatusLine) {
351
351
  c.addChild(new Text(fit(theme.fg("accent", ` ${liveStatusLine}`)), 0, 0));
352
352
  }
353
- c.addChild(new Text(fit(theme.fg("accent", " Press ctrl+o for live detail")), 0, 0));
353
+ c.addChild(new Text(fit(theme.fg("accent", ` Press ${keyHint("app.tools.expand", "for live detail")}`)), 0, 0));
354
354
  if (r.artifactPaths) {
355
355
  c.addChild(new Text(fit(theme.fg("dim", ` artifacts: ${shortenPath(r.artifactPaths.outputPath)}`)), 0, 0));
356
356
  }
@@ -1,3 +1,4 @@
1
+ import { keyHint } from "@bastani/atomic";
1
2
  import type { AsyncJobState } from "../shared/types.ts";
2
3
  import { shortenPath } from "../shared/formatters.ts";
3
4
  import { aggregateStepStatus, formatParallelOutcome } from "../shared/status-format.ts";
@@ -80,7 +81,7 @@ export function foregroundStyleWidgetStepLines(
80
81
  lines.push(` ${nestedLine}`);
81
82
  }
82
83
  if (step.status === "running") {
83
- if (!expanded) lines.push(` ${theme.fg("accent", "Press ctrl+o for live detail")}`);
84
+ if (!expanded) lines.push(` ${theme.fg("accent", `Press ${keyHint("app.tools.expand", "for live detail")}`)}`);
84
85
  const output = widgetOutputPath(job, step);
85
86
  if (output) lines.push(` ${theme.fg("dim", `output: ${shortenPath(output)}`)}`);
86
87
  if (expanded) {
@@ -147,7 +148,7 @@ export function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width
147
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}` : ""}`);
148
149
  for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, false, job.updatedAt, undefined, now)) lines.push(` ${nestedLine}`);
149
150
  }
150
- if (job.steps.some((step) => step.status === "running")) lines.push(theme.fg("accent", " Press ctrl+o for live detail"));
151
+ if (job.steps.some((step) => step.status === "running")) lines.push(theme.fg("accent", ` Press ${keyHint("app.tools.expand", "for live detail")}`));
151
152
  return lines.map((line) => truncLine(line, width));
152
153
  }
153
154
 
@@ -161,6 +162,6 @@ export function fitWidgetLineBudget(lines: string[], theme: Theme, width: number
161
162
  const hiddenCount = lines.length - visibleLines;
162
163
  const hint = expanded
163
164
  ? `… ${hiddenCount} live-detail lines hidden`
164
- : `… ${hiddenCount} lines hidden · ctrl+o expands`;
165
+ : `… ${hiddenCount} lines hidden · ${keyHint("app.tools.expand", "expands")}`;
165
166
  return [...lines.slice(0, visibleLines), truncLine(theme.fg("dim", hint), width)];
166
167
  }
@@ -63,6 +63,7 @@ interface RenderRequestingContext {
63
63
  let latestWidgetCtx: ExtensionContext | undefined;
64
64
  let latestWidgetJobs: AsyncJobState[] = [];
65
65
  let latestWidgetFrameNow = 0;
66
+ let latestWidgetExpanded = false;
66
67
  let widgetTimer: ReturnType<typeof setInterval> | undefined;
67
68
  let mountedWidgetCtx: ExtensionContext | undefined;
68
69
  let mountedWidgetOwnerKey: string | undefined;
@@ -78,16 +79,21 @@ function getLatestWidgetFrameNow(): number {
78
79
 
79
80
  function getLatestWidgetExpanded(): boolean {
80
81
  // LiveWidgetComponent re-renders outside a specific renderWidget() call, so
81
- // read expansion from the latest live singleton context. If that context was
82
- // cleared or went stale, collapse safely instead of consulting a stale caller.
83
- if (!latestWidgetCtx?.hasUI) return false;
84
- return latestWidgetCtx.ui.getToolsExpanded?.() ?? false;
82
+ // remember the last expansion state observed from a live host UI. Workflow
83
+ // stage-node detach can briefly repaint the mounted singleton with a stale or
84
+ // no-UI context before the next subagent status update refreshes it; falling
85
+ // back to the cached value avoids a transient collapse while jobs keep running.
86
+ if (!latestWidgetCtx?.hasUI) return latestWidgetExpanded;
87
+ const expanded = latestWidgetCtx.ui.getToolsExpanded?.();
88
+ if (typeof expanded === "boolean") latestWidgetExpanded = expanded;
89
+ return latestWidgetExpanded;
85
90
  }
86
91
 
87
92
  function clearLatestWidgetState(): void {
88
93
  latestWidgetCtx = undefined;
89
94
  latestWidgetJobs = [];
90
95
  latestWidgetFrameNow = 0;
96
+ latestWidgetExpanded = false;
91
97
  mountedWidgetCtx = undefined;
92
98
  mountedWidgetOwnerKey = undefined;
93
99
  widgetMounted = false;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/web-access",
3
- "version": "0.9.4-alpha.10",
3
+ "version": "0.9.4-alpha.11",
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,12 @@ 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.4-alpha.11] - 2026-07-03
10
+
11
+ ### Fixed
12
+
13
+ - Fixed attached workflow stage viewers so the host `app.tools.expand` keybinding (Ctrl+O by default) toggles live-detail/tool-output expansion just like the main chat, including custom keybinding remaps ([#1607](https://github.com/bastani-inc/atomic/issues/1607)).
14
+
9
15
  ## [0.9.4-alpha.9] - 2026-07-02
10
16
 
11
17
  ### Changed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/workflows",
3
- "version": "0.9.4-alpha.10",
3
+ "version": "0.9.4-alpha.11",
4
4
  "private": true,
5
5
  "description": "Atomic extension for multi-stage workflow authoring and execution.",
6
6
  "contributors": [
@@ -38,6 +38,10 @@ export interface KeybindingsLike {
38
38
  matches(data: string, action: string): boolean;
39
39
  }
40
40
 
41
+ export const APP_ACTION = {
42
+ toolsExpand: "app.tools.expand",
43
+ } as const;
44
+
41
45
  export const TUI_ACTION = {
42
46
  editorCursorUp: "tui.editor.cursorUp",
43
47
  editorCursorDown: "tui.editor.cursorDown",
@@ -48,6 +48,8 @@ export interface OverlayUISurface {
48
48
  focusHostInlineCustomUi?: () => boolean;
49
49
  getEditorComponent?: () => PiEditorFactory | undefined;
50
50
  getChatRenderSettings?: () => OverlayChatRenderSettings | undefined;
51
+ getToolsExpanded?: () => boolean;
52
+ setToolsExpanded?: (expanded: boolean) => void;
51
53
  getFooterDataProvider?: () => ReadonlyFooterDataProvider;
52
54
  setStatus?: (key: string, value: string | undefined) => void;
53
55
  }
@@ -334,6 +336,8 @@ export function buildGraphOverlayAdapter(
334
336
  piKeybindings: keybindings,
335
337
  piEditorFactory: ui?.getEditorComponent?.(),
336
338
  getChatRenderSettings: ui?.getChatRenderSettings,
339
+ getToolsExpanded: ui?.getToolsExpanded,
340
+ setToolsExpanded: ui?.setToolsExpanded,
337
341
  footerData: ui?.getFooterDataProvider?.(),
338
342
  // Pi-tui owns terminal dimensions; thread its row count down
339
343
  // so the overlay frame fills the actual viewport rather than
@@ -3,7 +3,7 @@ import {
3
3
  handlePromptCardInput,
4
4
  isPromptEscapeInput,
5
5
  } from "./prompt-card.js";
6
- import { isKeybindingsLike } from "./keybindings-adapter.js";
6
+ import { APP_ACTION, isKeybindingsLike, matchesAction } from "./keybindings-adapter.js";
7
7
  import {
8
8
  setComponentFocused,
9
9
  setEditorFocused,
@@ -35,6 +35,7 @@ export function handleStageChatInput(
35
35
  ctx.requestRender?.();
36
36
  return true;
37
37
  }
38
+ if (handleToolsExpandInput(ctx, data)) return true;
38
39
  if (ctx.mountedCustomUi) {
39
40
  return handleMountedCustomUiInput(ctx, data);
40
41
  }
@@ -87,6 +88,16 @@ export function handleStageChatInput(
87
88
  return ctx.chatHost.handleInput(data);
88
89
  }
89
90
 
91
+ function handleToolsExpandInput(ctx: StageChatViewContext, data: string): boolean {
92
+ const keybindings = isKeybindingsLike(ctx.piKeybindings) ? ctx.piKeybindings : undefined;
93
+ if (!matchesAction(keybindings, data, APP_ACTION.toolsExpand)) return false;
94
+ const expanded = ctx.getToolsExpanded?.() === true;
95
+ ctx.setToolsExpanded?.(!expanded);
96
+ ctx.chatHost.invalidate();
97
+ ctx.requestRender?.();
98
+ return true;
99
+ }
100
+
90
101
  function handleMountedCustomUiInput(
91
102
  ctx: StageChatViewContext,
92
103
  data: string,
@@ -57,6 +57,8 @@ export function initializeStageChatView(
57
57
  ctx.piTheme = opts.piTheme;
58
58
  ctx.piKeybindings = opts.piKeybindings;
59
59
  ctx.piEditorFactory = opts.piEditorFactory;
60
+ ctx.getToolsExpanded = opts.getToolsExpanded;
61
+ ctx.setToolsExpanded = opts.setToolsExpanded;
60
62
  ctx.stageUiBroker = opts.stageUiBroker ?? stageUiBroker;
61
63
  ctx.canSubmitPrompt = opts.canSubmitPrompt;
62
64
  ctx.mountedCustomUi = null;
@@ -63,6 +63,10 @@ export interface StageChatViewOpts {
63
63
  getChatRenderSettings?: () =>
64
64
  | Partial<Omit<ChatMessageRenderOptions, "ui" | "cwd">>
65
65
  | undefined;
66
+ /** Parent host expansion state for tool-output/live-detail widgets. */
67
+ getToolsExpanded?: () => boolean;
68
+ /** Toggle parent host expansion state for tool-output/live-detail widgets. */
69
+ setToolsExpanded?: (expanded: boolean) => void;
66
70
  /** Parent footer data provider inherited from the host UI for core footer/usage rendering. */
67
71
  footerData?: ReadonlyFooterDataProvider;
68
72
  /**
@@ -133,6 +137,8 @@ export interface StageChatViewContext {
133
137
  piTheme: unknown;
134
138
  piKeybindings: unknown;
135
139
  piEditorFactory: StageChatViewOpts["piEditorFactory"] | undefined;
140
+ getToolsExpanded: (() => boolean) | undefined;
141
+ setToolsExpanded: ((expanded: boolean) => void) | undefined;
136
142
  chatHost: ChatSessionHost<NoticeEntry>;
137
143
  stageUiBroker: StageUiBroker;
138
144
  canSubmitPrompt: ((runId: string, stageId: string, promptId: string) => boolean) | undefined;
@@ -83,6 +83,8 @@ export class StageChatView implements Component, Focusable {
83
83
  private piTheme!: StageChatViewContext["piTheme"];
84
84
  private piKeybindings!: StageChatViewContext["piKeybindings"];
85
85
  private piEditorFactory!: StageChatViewContext["piEditorFactory"];
86
+ private getToolsExpanded!: StageChatViewContext["getToolsExpanded"];
87
+ private setToolsExpanded!: StageChatViewContext["setToolsExpanded"];
86
88
  private chatHost!: StageChatViewContext["chatHost"];
87
89
  private stageUiBroker!: StageChatViewContext["stageUiBroker"];
88
90
  private canSubmitPrompt!: StageChatViewContext["canSubmitPrompt"];
@@ -213,6 +215,8 @@ export class StageChatView implements Component, Focusable {
213
215
  void this.piTheme;
214
216
  void this.piKeybindings;
215
217
  void this.piEditorFactory;
218
+ void this.getToolsExpanded;
219
+ void this.setToolsExpanded;
216
220
  void this.stageUiBroker;
217
221
  void this.canSubmitPrompt;
218
222
  void this.mountingRequestId;
@@ -41,6 +41,10 @@ export interface WorkflowAttachPaneOpts {
41
41
  piEditorFactory?: (tui: TUI, theme: EditorTheme, keybindings: unknown) => EditorComponent;
42
42
  /** Parent chat rendering settings and extension renderers, inherited from the host UI. */
43
43
  getChatRenderSettings?: () => Partial<Omit<ChatMessageRenderOptions, "ui" | "cwd">> | undefined;
44
+ /** Parent host tool-output/live-detail expansion state, inherited from the host UI. */
45
+ getToolsExpanded?: () => boolean;
46
+ /** Parent host tool-output/live-detail expansion setter, inherited from the host UI. */
47
+ setToolsExpanded?: (expanded: boolean) => void;
44
48
  /** Parent footer data provider, inherited so attached chats can render the core coding-agent footer. */
45
49
  footerData?: ReadonlyFooterDataProvider;
46
50
  /**
@@ -24,16 +24,9 @@ import type { ChatMessageRenderOptions, ReadonlyFooterDataProvider } from "@bast
24
24
  import type { Store } from "../shared/store.js";
25
25
  import type { GraphTheme } from "./graph-theme.js";
26
26
  import { GraphView } from "./graph-view.js";
27
- import {
28
- StageChatView,
29
- type StageChatDetachMetadata,
30
- type StageChatDetachReason,
31
- } from "./stage-chat-view.js";
27
+ import { StageChatView, type StageChatDetachMetadata, type StageChatDetachReason } from "./stage-chat-view.js";
32
28
  import { Key, matchesKey } from "./text-helpers.js";
33
- import type {
34
- StageControlHandle,
35
- StageControlRegistry,
36
- } from "../runs/foreground/stage-control-registry.js";
29
+ import type { StageControlHandle, StageControlRegistry } from "../runs/foreground/stage-control-registry.js";
37
30
  import type { StageUiBroker } from "../shared/stage-ui-broker.js";
38
31
  import type { StageSnapshot, StoreSnapshot } from "../shared/store-types.js";
39
32
  import { expandWorkflowGraph } from "../shared/expanded-workflow-graph.js";
@@ -68,6 +61,8 @@ export class WorkflowAttachPane implements Component {
68
61
  private piKeybindings?: unknown;
69
62
  private piEditorFactory?: (tui: TUI, theme: EditorTheme, keybindings: unknown) => EditorComponent;
70
63
  private getChatRenderSettings?: () => Partial<Omit<ChatMessageRenderOptions, "ui" | "cwd">> | undefined;
64
+ private getToolsExpanded?: () => boolean;
65
+ private setToolsExpanded?: (expanded: boolean) => void;
71
66
  private footerData?: ReadonlyFooterDataProvider;
72
67
  private now: () => number;
73
68
  private mode: WorkflowAttachPaneMode = "graph";
@@ -104,6 +99,8 @@ export class WorkflowAttachPane implements Component {
104
99
  this.piKeybindings = opts.piKeybindings;
105
100
  this.piEditorFactory = opts.piEditorFactory;
106
101
  this.getChatRenderSettings = opts.getChatRenderSettings;
102
+ this.getToolsExpanded = opts.getToolsExpanded;
103
+ this.setToolsExpanded = opts.setToolsExpanded;
107
104
  this.footerData = opts.footerData;
108
105
  this.now = opts.now ?? Date.now;
109
106
  this.unsubscribeStore = this.store.subscribe((snapshot) => this._handleStoreUpdate(snapshot));
@@ -197,6 +194,8 @@ export class WorkflowAttachPane implements Component {
197
194
  piKeybindings: this.piKeybindings,
198
195
  piEditorFactory: this.piEditorFactory,
199
196
  getChatRenderSettings: this.getChatRenderSettings,
197
+ getToolsExpanded: this.getToolsExpanded,
198
+ setToolsExpanded: this.setToolsExpanded,
200
199
  footerData: this.footerData,
201
200
  getViewportRows: this.getViewportRows,
202
201
  stageUiBroker: this.stageUiBroker,
@@ -1 +1 @@
1
- {"version":3,"file":"agent-session-events.d.ts","sourceRoot":"","sources":["../../src/core/agent-session-events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAC9E,OAAO,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAe,MAAM,8BAA8B,CAAC;AAG3F,OAAO,KAAK,EAAE,2BAA2B,IAAI,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC9F,OAAO,EAA0F,KAAK,iBAAiB,EAAE,KAAK,yBAAyB,EAAE,MAAM,0BAA0B,CAAC;AAG1L,wBAAgB,KAAK,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAIxE;AAGD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAMzD;AAED,4EAA4E;AAE5E,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAe7E;AAGD,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAuB1F;AAGD,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,GAAG,gBAAgB,GAAG,SAAS,CAQvH;AAGD,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA+G7F;AAGD,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAsBvF;AAGD,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAWvF;AAED,wCAAwC;AAExC,wBAAgB,aAAa,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAMtD;AAED,0CAA0C;AAE1C,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAMhF;AAED,8EAA8E;AAE9E,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,YAAY,GAAG,gBAAgB,GAAG,SAAS,CAS1F;AAGD,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,GAAG,IAAI,CAchH;AAED,kDAAkD;AAElD,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAuE9F;AAED;;;;GAIG;AAEH,wBAAgB,SAAS,CAAC,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,yBAAyB,GAAG,MAAM,IAAI,CAU7F;AAED;;;;GAIG;AAEH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAK7D;AAED;;;GAGG;AAEH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAG1D;AAED;;;GAGG;AAEH,wBAAgB,OAAO,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAOhD;AAMD,uBAAuB;AAEvB,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;CAkBrC,CAAC","sourcesContent":["import type { AgentEvent, AgentMessage } from \"@earendil-works/pi-agent-core\";\nimport type { AssistantMessage, Message, TextContent } from \"@earendil-works/pi-ai/compat\";\nimport { cleanupSessionResources } from \"@earendil-works/pi-ai/compat\";\nimport { formatCopilotProviderError } from \"./copilot-errors.ts\";\nimport type { AgentSessionInternalSurface as AgentSession } from \"./agent-session-methods.ts\";\nimport { customMessageExcludesContext, isSingleGenericAbortTextContent, replacementAbortContent, type AgentSessionEvent, type AgentSessionEventListener } from \"./agent-session-types.ts\";\nimport type { MessageEndEvent, MessageStartEvent, MessageUpdateEvent, ToolExecutionEndEvent, ToolExecutionStartEvent, ToolExecutionUpdateEvent, TurnEndEvent, TurnStartEvent } from \"./extensions/index.ts\";\n\nexport function _emit(this: AgentSession, event: AgentSessionEvent): void {\n\tfor (const l of this._eventListeners) {\n\t\tl(event);\n\t}\n}\n\n\nexport function _emitQueueUpdate(this: AgentSession): void {\n\tthis._emit({\n\t\ttype: \"queue_update\",\n\t\tsteering: [...this._steeringMessages],\n\t\tfollowUp: [...this._followUpMessages],\n\t});\n}\n\n/** Internal handler for agent events - shared by subscribe and reconnect */\n\nexport function _handleAgentEvent(this: AgentSession, event: AgentEvent): void {\n\t// Create retry promise synchronously before queueing async processing.\n\t// Agent.emit() calls this handler synchronously, and prompt() calls waitForRetry()\n\t// as soon as agent.prompt() resolves. If _retryPromise is created only inside\n\t// _processAgentEvent, slow earlier queued events can delay agent_end processing\n\t// and waitForRetry() can miss the in-flight retry.\n\tthis._createRetryPromiseForAgentEnd(event);\n\n\tthis._agentEventQueue = this._agentEventQueue.then(\n\t\t() => this._processAgentEvent(event),\n\t\t() => this._processAgentEvent(event),\n\t);\n\n\t// Keep queue alive if an event handler fails\n\tthis._agentEventQueue.catch(() => {});\n}\n\n\nexport function _createRetryPromiseForAgentEnd(this: AgentSession, event: AgentEvent): void {\n\tif (event.type !== \"agent_end\" || this._retryPromise) {\n\t\treturn;\n\t}\n\n\tconst settings = this.settingsManager.getRetrySettings();\n\tif (!settings.enabled) {\n\t\treturn;\n\t}\n\n\tconst lastAssistant = this._findLastAssistantInMessages(event.messages);\n\tif (!lastAssistant) {\n\t\treturn;\n\t}\n\n\tconst shouldRetry = this._isRetryableError(lastAssistant) || this._isEmptyCompletion(lastAssistant) || this._isSafetyRefusal(lastAssistant);\n\tif (!shouldRetry) {\n\t\treturn;\n\t}\n\n\tthis._retryPromise = new Promise((resolve) => {\n\t\tthis._retryResolve = resolve;\n\t});\n}\n\n\nexport function _findLastAssistantInMessages(this: AgentSession, messages: AgentMessage[]): AssistantMessage | undefined {\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst message = messages[i];\n\t\tif (message.role === \"assistant\") {\n\t\t\treturn message as AssistantMessage;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n\nexport async function _processAgentEvent(this: AgentSession, event: AgentEvent): Promise<void> {\n\t// When a user message starts, check if it's from either queue and remove it BEFORE emitting\n\t// This ensures the UI sees the updated queue state\n\tif (event.type === \"message_start\" && event.message.role === \"user\") {\n\t\tthis._overflowRecoveryAttempted = false;\n\t\tconst messageText = this._getUserMessageText(event.message);\n\t\tif (messageText) {\n\t\t\t// Check steering queue first\n\t\t\tconst steeringIndex = this._steeringMessages.indexOf(messageText);\n\t\t\tif (steeringIndex !== -1) {\n\t\t\t\tthis._steeringMessages.splice(steeringIndex, 1);\n\t\t\t\tthis._emitQueueUpdate();\n\t\t\t} else {\n\t\t\t\t// Check follow-up queue\n\t\t\t\tconst followUpIndex = this._followUpMessages.indexOf(messageText);\n\t\t\t\tif (followUpIndex !== -1) {\n\t\t\t\t\tthis._followUpMessages.splice(followUpIndex, 1);\n\t\t\t\t\tthis._emitQueueUpdate();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tthis._applyInterruptAbortMessage(event);\n\tthis._applyProviderErrorGuidance(event);\n\n\t// Emit to extensions first\n\tawait this._emitExtensionEvent(event);\n\n\t// Notify all listeners\n\tthis._emit(event);\n\n\t// Handle session persistence\n\tif (event.type === \"message_end\") {\n\t\t// Check if this is a custom message from extensions\n\t\tif (event.message.role === \"custom\") {\n\t\t\t// Persist as CustomMessageEntry\n\t\t\tthis.sessionManager.appendCustomMessageEntry(\n\t\t\t\tevent.message.customType,\n\t\t\t\tevent.message.content,\n\t\t\t\tevent.message.display,\n\t\t\t\tevent.message.details,\n\t\t\t\tcustomMessageExcludesContext(event.message),\n\t\t\t);\n\t\t} else if (\n\t\t\tevent.message.role === \"user\" ||\n\t\t\tevent.message.role === \"assistant\" ||\n\t\t\tevent.message.role === \"toolResult\"\n\t\t) {\n\t\t\tif (event.message.role === \"assistant\") {\n\t\t\t\tthis._normalizePersistedGeminiToolArgs(event.message);\n\t\t\t}\n\t\t\t// Regular LLM message - persist as SessionMessageEntry\n\t\t\tthis.sessionManager.appendMessage(event.message);\n\t\t}\n\t\t// Other message types (bashExecution, branchSummary) are persisted elsewhere\n\n\t\t// Track assistant message for auto-compaction (checked on agent_end)\n\t\tif (event.message.role === \"assistant\") {\n\t\t\tthis._lastAssistantMessage = event.message;\n\n\t\t\tconst assistantMsg = event.message as AssistantMessage;\n\t\t\t// Treat degenerate empty completions (no content, zero output tokens) and\n\t\t\t// intercepted canned safety refusals as failures alongside stopReason ===\n\t\t\t// \"error\". Otherwise such a turn that stops with reason \"stop\"/\"length\"\n\t\t\t// would reset the retry counter on every attempt, causing unbounded\n\t\t\t// retries instead of honoring maxRetries.\n\t\t\tconst assistantFailed = assistantMsg.stopReason === \"error\" || this._isEmptyCompletion(assistantMsg) || this._isSafetyRefusal(assistantMsg);\n\t\t\tif (!assistantFailed) {\n\t\t\t\tthis._overflowRecoveryAttempted = false;\n\t\t\t}\n\n\t\t\t// Reset retry counter immediately on successful assistant response\n\t\t\t// This prevents accumulation across multiple LLM calls within a turn\n\t\t\tif (!assistantFailed && this._retryAttempt > 0) {\n\t\t\t\tthis._emit({\n\t\t\t\t\ttype: \"auto_retry_end\",\n\t\t\t\t\tsuccess: true,\n\t\t\t\t\tattempt: this._retryAttempt,\n\t\t\t\t});\n\t\t\t\tthis._retryAttempt = 0;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check auto-retry and auto-compaction after agent completes\n\tif (event.type === \"agent_end\" && this._lastAssistantMessage) {\n\t\tconst msg = this._lastAssistantMessage;\n\t\tthis._lastAssistantMessage = undefined;\n\n\t\t// Check for retryable errors first (overloaded, rate limit, server errors,\n\t\t// transient provider finish_reason errors, degenerate empty completions,\n\t\t// or intercepted canned safety refusals)\n\t\tconst retryableError = this._isRetryableError(msg);\n\t\tconst emptyCompletion = !retryableError && this._isEmptyCompletion(msg);\n\t\tconst safetyRefusal = !retryableError && !emptyCompletion && this._isSafetyRefusal(msg);\n\t\tif (retryableError || emptyCompletion || safetyRefusal) {\n\t\t\tif (emptyCompletion && !msg.errorMessage) {\n\t\t\t\t// Surface a clear reason in the retry banner; empty completions carry no\n\t\t\t\t// provider error message of their own.\n\t\t\t\tmsg.errorMessage = \"Provider returned an empty completion\";\n\t\t\t} else if (safetyRefusal && !msg.errorMessage) {\n\t\t\t\tmsg.errorMessage = \"Provider returned a canned safety refusal\";\n\t\t\t}\n\t\t\tconst didRetry = await this._handleRetryableError(msg);\n\t\t\tif (didRetry) return; // Retry was initiated, don't proceed to compaction\n\t\t}\n\n\t\tthis._resolveRetry();\n\t\tawait this._checkCompaction(msg);\n\t}\n}\n\n\nexport function _applyInterruptAbortMessage(this: AgentSession, event: AgentEvent): void {\n\tconst abortMessage = this._activeInterruptAbortMessage;\n\tif (!abortMessage) return;\n\n\tif (event.type === \"tool_execution_end\" && event.isError && isSingleGenericAbortTextContent(event.result.content)) {\n\t\tevent.result.content = replacementAbortContent(abortMessage);\n\t\treturn;\n\t}\n\n\tif (event.type !== \"message_start\" && event.type !== \"message_end\") return;\n\n\tif (event.message.role === \"toolResult\" && event.message.isError && isSingleGenericAbortTextContent(event.message.content)) {\n\t\tevent.message.content = replacementAbortContent(abortMessage);\n\t\treturn;\n\t}\n\n\tif (event.message.role === \"assistant\") {\n\t\tconst assistantMessage = event.message as AssistantMessage;\n\t\tif (assistantMessage.stopReason === \"aborted\") {\n\t\t\tassistantMessage.errorMessage = abortMessage;\n\t\t}\n\t}\n}\n\n\nexport function _applyProviderErrorGuidance(this: AgentSession, event: AgentEvent): void {\n\tif (event.type !== \"message_start\" && event.type !== \"message_update\" && event.type !== \"message_end\") return;\n\tif (event.message.role !== \"assistant\") return;\n\n\tconst assistantMessage = event.message as AssistantMessage;\n\tif (assistantMessage.stopReason !== \"error\" || !assistantMessage.errorMessage) return;\n\n\tassistantMessage.errorMessage = formatCopilotProviderError(\n\t\tassistantMessage.provider,\n\t\tassistantMessage.errorMessage,\n\t);\n}\n\n/** Resolve the pending retry promise */\n\nexport function _resolveRetry(this: AgentSession): void {\n\tif (this._retryResolve) {\n\t\tthis._retryResolve();\n\t\tthis._retryResolve = undefined;\n\t\tthis._retryPromise = undefined;\n\t}\n}\n\n/** Extract text content from a message */\n\nexport function _getUserMessageText(this: AgentSession, message: Message): string {\n\tif (message.role !== \"user\") return \"\";\n\tconst content = message.content;\n\tif (typeof content === \"string\") return content;\n\tconst textBlocks = content.filter((c) => c.type === \"text\");\n\treturn textBlocks.map((c) => (c as TextContent).text).join(\"\");\n}\n\n/** Find the last assistant message in agent state (including aborted ones) */\n\nexport function _findLastAssistantMessage(this: AgentSession): AssistantMessage | undefined {\n\tconst messages = this.agent.state.messages;\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst msg = messages[i];\n\t\tif (msg.role === \"assistant\") {\n\t\t\treturn msg as AssistantMessage;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n\nexport function _replaceMessageInPlace(this: AgentSession, target: AgentMessage, replacement: AgentMessage): void {\n\t// Agent-core stores the finalized message object in its state before emitting message_end.\n\t// SessionManager persistence happens later in _processAgentEvent() with event.message.\n\t// Mutating this object in place keeps agent state, later turn/agent events, listeners,\n\t// and the eventual SessionManager.appendMessage(event.message) persistence in sync.\n\tif (target === replacement) {\n\t\treturn;\n\t}\n\n\tconst targetRecord = target as unknown as Record<string, unknown>;\n\tfor (const key of Object.keys(targetRecord)) {\n\t\tdelete targetRecord[key];\n\t}\n\tObject.assign(targetRecord, replacement);\n}\n\n/** Emit extension events based on agent events */\n\nexport async function _emitExtensionEvent(this: AgentSession, event: AgentEvent): Promise<void> {\n\tif (event.type === \"agent_start\") {\n\t\tthis._turnIndex = 0;\n\t\tawait this._extensionRunner.emit({ type: \"agent_start\" });\n\t} else if (event.type === \"agent_end\") {\n\t\tawait this._extensionRunner.emit({ type: \"agent_end\", messages: event.messages });\n\t} else if (event.type === \"turn_start\") {\n\t\tconst extensionEvent: TurnStartEvent = {\n\t\t\ttype: \"turn_start\",\n\t\t\tturnIndex: this._turnIndex,\n\t\t\ttimestamp: Date.now(),\n\t\t};\n\t\tawait this._extensionRunner.emit(extensionEvent);\n\t} else if (event.type === \"turn_end\") {\n\t\tconst extensionEvent: TurnEndEvent = {\n\t\t\ttype: \"turn_end\",\n\t\t\tturnIndex: this._turnIndex,\n\t\t\tmessage: event.message,\n\t\t\ttoolResults: event.toolResults,\n\t\t};\n\t\tawait this._extensionRunner.emit(extensionEvent);\n\t\tthis._turnIndex++;\n\t} else if (event.type === \"message_start\") {\n\t\tconst extensionEvent: MessageStartEvent = {\n\t\t\ttype: \"message_start\",\n\t\t\tmessage: event.message,\n\t\t};\n\t\tawait this._extensionRunner.emit(extensionEvent);\n\t} else if (event.type === \"message_update\") {\n\t\tconst extensionEvent: MessageUpdateEvent = {\n\t\t\ttype: \"message_update\",\n\t\t\tmessage: event.message,\n\t\t\tassistantMessageEvent: event.assistantMessageEvent,\n\t\t};\n\t\tawait this._extensionRunner.emit(extensionEvent);\n\t} else if (event.type === \"message_end\") {\n\t\tconst extensionEvent: MessageEndEvent = {\n\t\t\ttype: \"message_end\",\n\t\t\tmessage: event.message,\n\t\t};\n\t\tconst replacement = await this._extensionRunner.emitMessageEnd(extensionEvent);\n\t\tif (replacement) {\n\t\t\tthis._replaceMessageInPlace(event.message, replacement);\n\t\t}\n\t} else if (event.type === \"tool_execution_start\") {\n\t\tconst extensionEvent: ToolExecutionStartEvent = {\n\t\t\ttype: \"tool_execution_start\",\n\t\t\ttoolCallId: event.toolCallId,\n\t\t\ttoolName: event.toolName,\n\t\t\targs: event.args,\n\t\t};\n\t\tawait this._extensionRunner.emit(extensionEvent);\n\t} else if (event.type === \"tool_execution_update\") {\n\t\tconst extensionEvent: ToolExecutionUpdateEvent = {\n\t\t\ttype: \"tool_execution_update\",\n\t\t\ttoolCallId: event.toolCallId,\n\t\t\ttoolName: event.toolName,\n\t\t\targs: event.args,\n\t\t\tpartialResult: event.partialResult,\n\t\t};\n\t\tawait this._extensionRunner.emit(extensionEvent);\n\t} else if (event.type === \"tool_execution_end\") {\n\t\tconst extensionEvent: ToolExecutionEndEvent = {\n\t\t\ttype: \"tool_execution_end\",\n\t\t\ttoolCallId: event.toolCallId,\n\t\t\ttoolName: event.toolName,\n\t\t\tresult: event.result,\n\t\t\tisError: event.isError,\n\t\t};\n\t\tawait this._extensionRunner.emit(extensionEvent);\n\t}\n}\n\n/**\n * Subscribe to agent events.\n * Session persistence is handled internally (saves messages on message_end).\n * Multiple listeners can be added. Returns unsubscribe function for this listener.\n */\n\nexport function subscribe(this: AgentSession, listener: AgentSessionEventListener): () => void {\n\tthis._eventListeners.push(listener);\n\n\t// Return unsubscribe function for this specific listener\n\treturn () => {\n\t\tconst index = this._eventListeners.indexOf(listener);\n\t\tif (index !== -1) {\n\t\t\tthis._eventListeners.splice(index, 1);\n\t\t}\n\t};\n}\n\n/**\n * Temporarily disconnect from agent events.\n * User listeners are preserved and will receive events again after resubscribe().\n * Used internally during operations that need to pause event processing.\n */\n\nexport function _disconnectFromAgent(this: AgentSession): void {\n\tif (this._unsubscribeAgent) {\n\t\tthis._unsubscribeAgent();\n\t\tthis._unsubscribeAgent = undefined;\n\t}\n}\n\n/**\n * Reconnect to agent events after _disconnectFromAgent().\n * Preserves all existing listeners.\n */\n\nexport function _reconnectToAgent(this: AgentSession): void {\n\tif (this._unsubscribeAgent) return; // Already connected\n\tthis._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);\n}\n\n/**\n * Remove all listeners and disconnect from agent.\n * Call this when completely done with the session.\n */\n\nexport function dispose(this: AgentSession): void {\n\tthis._extensionRunner.invalidate(\n\t\t\"This extension ctx is stale after session replacement or reload. Do not use a captured pi or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().\",\n\t);\n\tthis._disconnectFromAgent();\n\tthis._eventListeners = [];\n\tcleanupSessionResources(this.sessionId);\n}\n\n// =========================================================================\n// Read-only State Access\n// =========================================================================\n\n/** Full agent state */\n\nexport const agentSessionEventsMethods = {\n\t_emit,\n\t_emitQueueUpdate,\n\t_handleAgentEvent,\n\t_createRetryPromiseForAgentEnd,\n\t_findLastAssistantInMessages,\n\t_processAgentEvent,\n\t_applyInterruptAbortMessage,\n\t_applyProviderErrorGuidance,\n\t_resolveRetry,\n\t_getUserMessageText,\n\t_findLastAssistantMessage,\n\t_replaceMessageInPlace,\n\t_emitExtensionEvent,\n\tsubscribe,\n\t_disconnectFromAgent,\n\t_reconnectToAgent,\n\tdispose,\n};\n"]}
1
+ {"version":3,"file":"agent-session-events.d.ts","sourceRoot":"","sources":["../../src/core/agent-session-events.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAC9E,OAAO,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAe,MAAM,8BAA8B,CAAC;AAG3F,OAAO,KAAK,EAAE,2BAA2B,IAAI,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC9F,OAAO,EAA0F,KAAK,iBAAiB,EAAE,KAAK,yBAAyB,EAAE,MAAM,0BAA0B,CAAC;AAG1L,wBAAgB,KAAK,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAIxE;AAGD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAMzD;AAED,4EAA4E;AAE5E,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAe7E;AAGD,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAuB1F;AAGD,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,GAAG,gBAAgB,GAAG,SAAS,CAQvH;AAGD,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA+G7F;AAGD,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAsBvF;AAGD,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAWvF;AAED,wCAAwC;AAExC,wBAAgB,aAAa,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAMtD;AAED,0CAA0C;AAE1C,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAMhF;AAED,8EAA8E;AAE9E,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,YAAY,GAAG,gBAAgB,GAAG,SAAS,CAS1F;AAGD,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,GAAG,IAAI,CAchH;AAED,kDAAkD;AAElD,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAuE9F;AAED;;;;GAIG;AAEH,wBAAgB,SAAS,CAAC,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,yBAAyB,GAAG,MAAM,IAAI,CAU7F;AAED;;;;GAIG;AAEH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAK7D;AAED;;;GAGG;AAEH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAG1D;AAED;;;GAGG;AAEH,wBAAgB,OAAO,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAQhD;AAMD,uBAAuB;AAEvB,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;CAkBrC,CAAC","sourcesContent":["import { disposeSessionAsyncJobManager } from \"./async/session-manager.js\";\nimport type { AgentEvent, AgentMessage } from \"@earendil-works/pi-agent-core\";\nimport type { AssistantMessage, Message, TextContent } from \"@earendil-works/pi-ai/compat\";\nimport { cleanupSessionResources } from \"@earendil-works/pi-ai/compat\";\nimport { formatCopilotProviderError } from \"./copilot-errors.ts\";\nimport type { AgentSessionInternalSurface as AgentSession } from \"./agent-session-methods.ts\";\nimport { customMessageExcludesContext, isSingleGenericAbortTextContent, replacementAbortContent, type AgentSessionEvent, type AgentSessionEventListener } from \"./agent-session-types.ts\";\nimport type { MessageEndEvent, MessageStartEvent, MessageUpdateEvent, ToolExecutionEndEvent, ToolExecutionStartEvent, ToolExecutionUpdateEvent, TurnEndEvent, TurnStartEvent } from \"./extensions/index.ts\";\n\nexport function _emit(this: AgentSession, event: AgentSessionEvent): void {\n\tfor (const l of this._eventListeners) {\n\t\tl(event);\n\t}\n}\n\n\nexport function _emitQueueUpdate(this: AgentSession): void {\n\tthis._emit({\n\t\ttype: \"queue_update\",\n\t\tsteering: [...this._steeringMessages],\n\t\tfollowUp: [...this._followUpMessages],\n\t});\n}\n\n/** Internal handler for agent events - shared by subscribe and reconnect */\n\nexport function _handleAgentEvent(this: AgentSession, event: AgentEvent): void {\n\t// Create retry promise synchronously before queueing async processing.\n\t// Agent.emit() calls this handler synchronously, and prompt() calls waitForRetry()\n\t// as soon as agent.prompt() resolves. If _retryPromise is created only inside\n\t// _processAgentEvent, slow earlier queued events can delay agent_end processing\n\t// and waitForRetry() can miss the in-flight retry.\n\tthis._createRetryPromiseForAgentEnd(event);\n\n\tthis._agentEventQueue = this._agentEventQueue.then(\n\t\t() => this._processAgentEvent(event),\n\t\t() => this._processAgentEvent(event),\n\t);\n\n\t// Keep queue alive if an event handler fails\n\tthis._agentEventQueue.catch(() => {});\n}\n\n\nexport function _createRetryPromiseForAgentEnd(this: AgentSession, event: AgentEvent): void {\n\tif (event.type !== \"agent_end\" || this._retryPromise) {\n\t\treturn;\n\t}\n\n\tconst settings = this.settingsManager.getRetrySettings();\n\tif (!settings.enabled) {\n\t\treturn;\n\t}\n\n\tconst lastAssistant = this._findLastAssistantInMessages(event.messages);\n\tif (!lastAssistant) {\n\t\treturn;\n\t}\n\n\tconst shouldRetry = this._isRetryableError(lastAssistant) || this._isEmptyCompletion(lastAssistant) || this._isSafetyRefusal(lastAssistant);\n\tif (!shouldRetry) {\n\t\treturn;\n\t}\n\n\tthis._retryPromise = new Promise((resolve) => {\n\t\tthis._retryResolve = resolve;\n\t});\n}\n\n\nexport function _findLastAssistantInMessages(this: AgentSession, messages: AgentMessage[]): AssistantMessage | undefined {\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst message = messages[i];\n\t\tif (message.role === \"assistant\") {\n\t\t\treturn message as AssistantMessage;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n\nexport async function _processAgentEvent(this: AgentSession, event: AgentEvent): Promise<void> {\n\t// When a user message starts, check if it's from either queue and remove it BEFORE emitting\n\t// This ensures the UI sees the updated queue state\n\tif (event.type === \"message_start\" && event.message.role === \"user\") {\n\t\tthis._overflowRecoveryAttempted = false;\n\t\tconst messageText = this._getUserMessageText(event.message);\n\t\tif (messageText) {\n\t\t\t// Check steering queue first\n\t\t\tconst steeringIndex = this._steeringMessages.indexOf(messageText);\n\t\t\tif (steeringIndex !== -1) {\n\t\t\t\tthis._steeringMessages.splice(steeringIndex, 1);\n\t\t\t\tthis._emitQueueUpdate();\n\t\t\t} else {\n\t\t\t\t// Check follow-up queue\n\t\t\t\tconst followUpIndex = this._followUpMessages.indexOf(messageText);\n\t\t\t\tif (followUpIndex !== -1) {\n\t\t\t\t\tthis._followUpMessages.splice(followUpIndex, 1);\n\t\t\t\t\tthis._emitQueueUpdate();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tthis._applyInterruptAbortMessage(event);\n\tthis._applyProviderErrorGuidance(event);\n\n\t// Emit to extensions first\n\tawait this._emitExtensionEvent(event);\n\n\t// Notify all listeners\n\tthis._emit(event);\n\n\t// Handle session persistence\n\tif (event.type === \"message_end\") {\n\t\t// Check if this is a custom message from extensions\n\t\tif (event.message.role === \"custom\") {\n\t\t\t// Persist as CustomMessageEntry\n\t\t\tthis.sessionManager.appendCustomMessageEntry(\n\t\t\t\tevent.message.customType,\n\t\t\t\tevent.message.content,\n\t\t\t\tevent.message.display,\n\t\t\t\tevent.message.details,\n\t\t\t\tcustomMessageExcludesContext(event.message),\n\t\t\t);\n\t\t} else if (\n\t\t\tevent.message.role === \"user\" ||\n\t\t\tevent.message.role === \"assistant\" ||\n\t\t\tevent.message.role === \"toolResult\"\n\t\t) {\n\t\t\tif (event.message.role === \"assistant\") {\n\t\t\t\tthis._normalizePersistedGeminiToolArgs(event.message);\n\t\t\t}\n\t\t\t// Regular LLM message - persist as SessionMessageEntry\n\t\t\tthis.sessionManager.appendMessage(event.message);\n\t\t}\n\t\t// Other message types (bashExecution, branchSummary) are persisted elsewhere\n\n\t\t// Track assistant message for auto-compaction (checked on agent_end)\n\t\tif (event.message.role === \"assistant\") {\n\t\t\tthis._lastAssistantMessage = event.message;\n\n\t\t\tconst assistantMsg = event.message as AssistantMessage;\n\t\t\t// Treat degenerate empty completions (no content, zero output tokens) and\n\t\t\t// intercepted canned safety refusals as failures alongside stopReason ===\n\t\t\t// \"error\". Otherwise such a turn that stops with reason \"stop\"/\"length\"\n\t\t\t// would reset the retry counter on every attempt, causing unbounded\n\t\t\t// retries instead of honoring maxRetries.\n\t\t\tconst assistantFailed = assistantMsg.stopReason === \"error\" || this._isEmptyCompletion(assistantMsg) || this._isSafetyRefusal(assistantMsg);\n\t\t\tif (!assistantFailed) {\n\t\t\t\tthis._overflowRecoveryAttempted = false;\n\t\t\t}\n\n\t\t\t// Reset retry counter immediately on successful assistant response\n\t\t\t// This prevents accumulation across multiple LLM calls within a turn\n\t\t\tif (!assistantFailed && this._retryAttempt > 0) {\n\t\t\t\tthis._emit({\n\t\t\t\t\ttype: \"auto_retry_end\",\n\t\t\t\t\tsuccess: true,\n\t\t\t\t\tattempt: this._retryAttempt,\n\t\t\t\t});\n\t\t\t\tthis._retryAttempt = 0;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check auto-retry and auto-compaction after agent completes\n\tif (event.type === \"agent_end\" && this._lastAssistantMessage) {\n\t\tconst msg = this._lastAssistantMessage;\n\t\tthis._lastAssistantMessage = undefined;\n\n\t\t// Check for retryable errors first (overloaded, rate limit, server errors,\n\t\t// transient provider finish_reason errors, degenerate empty completions,\n\t\t// or intercepted canned safety refusals)\n\t\tconst retryableError = this._isRetryableError(msg);\n\t\tconst emptyCompletion = !retryableError && this._isEmptyCompletion(msg);\n\t\tconst safetyRefusal = !retryableError && !emptyCompletion && this._isSafetyRefusal(msg);\n\t\tif (retryableError || emptyCompletion || safetyRefusal) {\n\t\t\tif (emptyCompletion && !msg.errorMessage) {\n\t\t\t\t// Surface a clear reason in the retry banner; empty completions carry no\n\t\t\t\t// provider error message of their own.\n\t\t\t\tmsg.errorMessage = \"Provider returned an empty completion\";\n\t\t\t} else if (safetyRefusal && !msg.errorMessage) {\n\t\t\t\tmsg.errorMessage = \"Provider returned a canned safety refusal\";\n\t\t\t}\n\t\t\tconst didRetry = await this._handleRetryableError(msg);\n\t\t\tif (didRetry) return; // Retry was initiated, don't proceed to compaction\n\t\t}\n\n\t\tthis._resolveRetry();\n\t\tawait this._checkCompaction(msg);\n\t}\n}\n\n\nexport function _applyInterruptAbortMessage(this: AgentSession, event: AgentEvent): void {\n\tconst abortMessage = this._activeInterruptAbortMessage;\n\tif (!abortMessage) return;\n\n\tif (event.type === \"tool_execution_end\" && event.isError && isSingleGenericAbortTextContent(event.result.content)) {\n\t\tevent.result.content = replacementAbortContent(abortMessage);\n\t\treturn;\n\t}\n\n\tif (event.type !== \"message_start\" && event.type !== \"message_end\") return;\n\n\tif (event.message.role === \"toolResult\" && event.message.isError && isSingleGenericAbortTextContent(event.message.content)) {\n\t\tevent.message.content = replacementAbortContent(abortMessage);\n\t\treturn;\n\t}\n\n\tif (event.message.role === \"assistant\") {\n\t\tconst assistantMessage = event.message as AssistantMessage;\n\t\tif (assistantMessage.stopReason === \"aborted\") {\n\t\t\tassistantMessage.errorMessage = abortMessage;\n\t\t}\n\t}\n}\n\n\nexport function _applyProviderErrorGuidance(this: AgentSession, event: AgentEvent): void {\n\tif (event.type !== \"message_start\" && event.type !== \"message_update\" && event.type !== \"message_end\") return;\n\tif (event.message.role !== \"assistant\") return;\n\n\tconst assistantMessage = event.message as AssistantMessage;\n\tif (assistantMessage.stopReason !== \"error\" || !assistantMessage.errorMessage) return;\n\n\tassistantMessage.errorMessage = formatCopilotProviderError(\n\t\tassistantMessage.provider,\n\t\tassistantMessage.errorMessage,\n\t);\n}\n\n/** Resolve the pending retry promise */\n\nexport function _resolveRetry(this: AgentSession): void {\n\tif (this._retryResolve) {\n\t\tthis._retryResolve();\n\t\tthis._retryResolve = undefined;\n\t\tthis._retryPromise = undefined;\n\t}\n}\n\n/** Extract text content from a message */\n\nexport function _getUserMessageText(this: AgentSession, message: Message): string {\n\tif (message.role !== \"user\") return \"\";\n\tconst content = message.content;\n\tif (typeof content === \"string\") return content;\n\tconst textBlocks = content.filter((c) => c.type === \"text\");\n\treturn textBlocks.map((c) => (c as TextContent).text).join(\"\");\n}\n\n/** Find the last assistant message in agent state (including aborted ones) */\n\nexport function _findLastAssistantMessage(this: AgentSession): AssistantMessage | undefined {\n\tconst messages = this.agent.state.messages;\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst msg = messages[i];\n\t\tif (msg.role === \"assistant\") {\n\t\t\treturn msg as AssistantMessage;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n\nexport function _replaceMessageInPlace(this: AgentSession, target: AgentMessage, replacement: AgentMessage): void {\n\t// Agent-core stores the finalized message object in its state before emitting message_end.\n\t// SessionManager persistence happens later in _processAgentEvent() with event.message.\n\t// Mutating this object in place keeps agent state, later turn/agent events, listeners,\n\t// and the eventual SessionManager.appendMessage(event.message) persistence in sync.\n\tif (target === replacement) {\n\t\treturn;\n\t}\n\n\tconst targetRecord = target as unknown as Record<string, unknown>;\n\tfor (const key of Object.keys(targetRecord)) {\n\t\tdelete targetRecord[key];\n\t}\n\tObject.assign(targetRecord, replacement);\n}\n\n/** Emit extension events based on agent events */\n\nexport async function _emitExtensionEvent(this: AgentSession, event: AgentEvent): Promise<void> {\n\tif (event.type === \"agent_start\") {\n\t\tthis._turnIndex = 0;\n\t\tawait this._extensionRunner.emit({ type: \"agent_start\" });\n\t} else if (event.type === \"agent_end\") {\n\t\tawait this._extensionRunner.emit({ type: \"agent_end\", messages: event.messages });\n\t} else if (event.type === \"turn_start\") {\n\t\tconst extensionEvent: TurnStartEvent = {\n\t\t\ttype: \"turn_start\",\n\t\t\tturnIndex: this._turnIndex,\n\t\t\ttimestamp: Date.now(),\n\t\t};\n\t\tawait this._extensionRunner.emit(extensionEvent);\n\t} else if (event.type === \"turn_end\") {\n\t\tconst extensionEvent: TurnEndEvent = {\n\t\t\ttype: \"turn_end\",\n\t\t\tturnIndex: this._turnIndex,\n\t\t\tmessage: event.message,\n\t\t\ttoolResults: event.toolResults,\n\t\t};\n\t\tawait this._extensionRunner.emit(extensionEvent);\n\t\tthis._turnIndex++;\n\t} else if (event.type === \"message_start\") {\n\t\tconst extensionEvent: MessageStartEvent = {\n\t\t\ttype: \"message_start\",\n\t\t\tmessage: event.message,\n\t\t};\n\t\tawait this._extensionRunner.emit(extensionEvent);\n\t} else if (event.type === \"message_update\") {\n\t\tconst extensionEvent: MessageUpdateEvent = {\n\t\t\ttype: \"message_update\",\n\t\t\tmessage: event.message,\n\t\t\tassistantMessageEvent: event.assistantMessageEvent,\n\t\t};\n\t\tawait this._extensionRunner.emit(extensionEvent);\n\t} else if (event.type === \"message_end\") {\n\t\tconst extensionEvent: MessageEndEvent = {\n\t\t\ttype: \"message_end\",\n\t\t\tmessage: event.message,\n\t\t};\n\t\tconst replacement = await this._extensionRunner.emitMessageEnd(extensionEvent);\n\t\tif (replacement) {\n\t\t\tthis._replaceMessageInPlace(event.message, replacement);\n\t\t}\n\t} else if (event.type === \"tool_execution_start\") {\n\t\tconst extensionEvent: ToolExecutionStartEvent = {\n\t\t\ttype: \"tool_execution_start\",\n\t\t\ttoolCallId: event.toolCallId,\n\t\t\ttoolName: event.toolName,\n\t\t\targs: event.args,\n\t\t};\n\t\tawait this._extensionRunner.emit(extensionEvent);\n\t} else if (event.type === \"tool_execution_update\") {\n\t\tconst extensionEvent: ToolExecutionUpdateEvent = {\n\t\t\ttype: \"tool_execution_update\",\n\t\t\ttoolCallId: event.toolCallId,\n\t\t\ttoolName: event.toolName,\n\t\t\targs: event.args,\n\t\t\tpartialResult: event.partialResult,\n\t\t};\n\t\tawait this._extensionRunner.emit(extensionEvent);\n\t} else if (event.type === \"tool_execution_end\") {\n\t\tconst extensionEvent: ToolExecutionEndEvent = {\n\t\t\ttype: \"tool_execution_end\",\n\t\t\ttoolCallId: event.toolCallId,\n\t\t\ttoolName: event.toolName,\n\t\t\tresult: event.result,\n\t\t\tisError: event.isError,\n\t\t};\n\t\tawait this._extensionRunner.emit(extensionEvent);\n\t}\n}\n\n/**\n * Subscribe to agent events.\n * Session persistence is handled internally (saves messages on message_end).\n * Multiple listeners can be added. Returns unsubscribe function for this listener.\n */\n\nexport function subscribe(this: AgentSession, listener: AgentSessionEventListener): () => void {\n\tthis._eventListeners.push(listener);\n\n\t// Return unsubscribe function for this specific listener\n\treturn () => {\n\t\tconst index = this._eventListeners.indexOf(listener);\n\t\tif (index !== -1) {\n\t\t\tthis._eventListeners.splice(index, 1);\n\t\t}\n\t};\n}\n\n/**\n * Temporarily disconnect from agent events.\n * User listeners are preserved and will receive events again after resubscribe().\n * Used internally during operations that need to pause event processing.\n */\n\nexport function _disconnectFromAgent(this: AgentSession): void {\n\tif (this._unsubscribeAgent) {\n\t\tthis._unsubscribeAgent();\n\t\tthis._unsubscribeAgent = undefined;\n\t}\n}\n\n/**\n * Reconnect to agent events after _disconnectFromAgent().\n * Preserves all existing listeners.\n */\n\nexport function _reconnectToAgent(this: AgentSession): void {\n\tif (this._unsubscribeAgent) return; // Already connected\n\tthis._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);\n}\n\n/**\n * Remove all listeners and disconnect from agent.\n * Call this when completely done with the session.\n */\n\nexport function dispose(this: AgentSession): void {\n\tthis._extensionRunner.invalidate(\n\t\t\"This extension ctx is stale after session replacement or reload. Do not use a captured pi or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().\",\n\t);\n\tdisposeSessionAsyncJobManager(this._asyncJobManager, this._asyncJobManagerSessionId);\n\tthis._disconnectFromAgent();\n\tthis._eventListeners = [];\n\tcleanupSessionResources(this.sessionId);\n}\n\n// =========================================================================\n// Read-only State Access\n// =========================================================================\n\n/** Full agent state */\n\nexport const agentSessionEventsMethods = {\n\t_emit,\n\t_emitQueueUpdate,\n\t_handleAgentEvent,\n\t_createRetryPromiseForAgentEnd,\n\t_findLastAssistantInMessages,\n\t_processAgentEvent,\n\t_applyInterruptAbortMessage,\n\t_applyProviderErrorGuidance,\n\t_resolveRetry,\n\t_getUserMessageText,\n\t_findLastAssistantMessage,\n\t_replaceMessageInPlace,\n\t_emitExtensionEvent,\n\tsubscribe,\n\t_disconnectFromAgent,\n\t_reconnectToAgent,\n\tdispose,\n};\n"]}
@@ -1,3 +1,4 @@
1
+ import { disposeSessionAsyncJobManager } from "./async/session-manager.js";
1
2
  import { cleanupSessionResources } from "@earendil-works/pi-ai/compat";
2
3
  import { formatCopilotProviderError } from "./copilot-errors.js";
3
4
  import { customMessageExcludesContext, isSingleGenericAbortTextContent, replacementAbortContent } from "./agent-session-types.js";
@@ -349,6 +350,7 @@ export function _reconnectToAgent() {
349
350
  */
350
351
  export function dispose() {
351
352
  this._extensionRunner.invalidate("This extension ctx is stale after session replacement or reload. Do not use a captured pi or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().");
353
+ disposeSessionAsyncJobManager(this._asyncJobManager, this._asyncJobManagerSessionId);
352
354
  this._disconnectFromAgent();
353
355
  this._eventListeners = [];
354
356
  cleanupSessionResources(this.sessionId);