Merge remote-tracking branch 'origin/farm/699d8722/fix-resume-subagent-compaction-crash'
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
- Improved error reporting for `omp tiny-models download` by displaying the actual worker-side download error.
|
||||
- Resolved status inconsistencies between `/extensions`, `/mcp list`, and the dashboard, ensuring MCP server states, allowlists/denylists, and configuration files (like `mcp.json`) stay fully synchronized.
|
||||
- Improved branch-mode task merges to preserve the agent's original commit history (messages and authors) and fixed a bug where merges were rejected due to unrelated dirty changes in the parent checkout.
|
||||
- Fixed the `Working…` loader staying gone for the rest of a parent turn when a long-running tool (e.g. a `task` subagent) finished inside a transient overlay window (auto-snapcompact, auto-context-full, auto-retry). Those overlays null the working loader on start and the overlay-end handler is the only restorer keyed off the missing loader; if the subagent's `tool_execution_end` lands while the overlay is still active (or its end handler errored before re-arming), the spinner stayed gone until the next turn even though the parent kept streaming. `tool_execution_end` now mirrors the `tool_execution_update` self-heal so the working loader survives a subagent completing inside the overlay window ([#3858](https://github.com/can1357/oh-my-pi/issues/3858)).
|
||||
|
||||
## [16.2.6] - 2026-06-29
|
||||
|
||||
|
||||
@@ -890,6 +890,17 @@ export class EventController {
|
||||
}
|
||||
|
||||
async #handleToolExecutionEnd(event: Extract<AgentSessionEvent, { type: "tool_execution_end" }>): Promise<void> {
|
||||
// A long-running tool (`task` subagent, async bash poll, …) is the next
|
||||
// streaming event on the parent session when an inner transient overlay
|
||||
// (auto-compaction / auto-retry) clears the status container between this
|
||||
// tool's start and end. `auto_*_end` is the overlay-side restorer; for any
|
||||
// path that nulls the loader without a follow-up overlay-end (e.g. an
|
||||
// errored overlay teardown, or the subagent finishing inside the overlay
|
||||
// window), the next tool event is the only restore point before the turn
|
||||
// settles. `tool_execution_update` already self-heals — keep
|
||||
// `tool_execution_end` symmetric so the spinner survives a subagent
|
||||
// completing inside the overlay window (#3858).
|
||||
this.#ensureWorkingLoaderWhileStreaming();
|
||||
if (event.toolName === "read") {
|
||||
if (this.#inlineReadToolImages(event.toolCallId, event.result)) {
|
||||
const component = this.ctx.pendingTools.get(event.toolCallId);
|
||||
|
||||
@@ -299,6 +299,47 @@ describe("EventController working loader reconciliation", () => {
|
||||
expect(ctx.ensureLoadingAnimation).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("self-heals missing working loader when a task subagent finishes mid-turn (#3858)", async () => {
|
||||
// `task` subagents run inside the parent's streaming turn. While the task is
|
||||
// running a transient overlay (auto-compaction / auto-retry) can drop the
|
||||
// working loader by clearing the status container, and the overlay's end
|
||||
// handler is the only restorer keyed off the missing loader. If the task
|
||||
// finishes between the overlay's start and end (or any other branch where
|
||||
// the loader was nulled without a follow-up overlay-end), `tool_execution_end`
|
||||
// is the next streaming event that lands and must heal the loader, mirroring
|
||||
// the `tool_execution_update` reconciler. Without this the spinner stays
|
||||
// gone for the remainder of the parent turn even though the agent keeps
|
||||
// streaming (the user-visible regression in #3858).
|
||||
const { controller, ctx } = createFixture();
|
||||
(ctx.viewSession as unknown as { isStreaming: boolean }).isStreaming = true;
|
||||
|
||||
await controller.handleEvent({
|
||||
type: "tool_execution_end",
|
||||
toolCallId: "task-1",
|
||||
toolName: "task",
|
||||
isError: false,
|
||||
result: { content: [{ type: "text", text: "ok" }], details: {} },
|
||||
} as Extract<AgentSessionEvent, { type: "tool_execution_end" }>);
|
||||
|
||||
expect(ctx.ensureLoadingAnimation).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not restore the working loader while an overlay loader (auto-retry) owns the status container at tool_execution_end", async () => {
|
||||
const { controller, ctx } = createFixture();
|
||||
ctx.retryLoader = { stop: vi.fn() } as unknown as InteractiveModeContext["retryLoader"];
|
||||
(ctx.viewSession as unknown as { isStreaming: boolean }).isStreaming = true;
|
||||
|
||||
await controller.handleEvent({
|
||||
type: "tool_execution_end",
|
||||
toolCallId: "task-2",
|
||||
toolName: "task",
|
||||
isError: false,
|
||||
result: { content: [{ type: "text", text: "ok" }], details: {} },
|
||||
} as Extract<AgentSessionEvent, { type: "tool_execution_end" }>);
|
||||
|
||||
expect(ctx.ensureLoadingAnimation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps transient retry status exclusive while a retry loader is visible", async () => {
|
||||
const { controller, ctx } = createFixture();
|
||||
ctx.retryLoader = { stop: vi.fn() } as unknown as InteractiveModeContext["retryLoader"];
|
||||
|
||||
@@ -21,6 +21,11 @@ function createContext() {
|
||||
updateEditorTopBorder: vi.fn(),
|
||||
clearPinnedError: vi.fn(),
|
||||
ensureLoadingAnimation: vi.fn(),
|
||||
// `viewSession.isStreaming` is read by `#ensureWorkingLoaderWhileStreaming`,
|
||||
// which runs at the top of `tool_execution_end` (and other streaming-event
|
||||
// handlers). Leaving it false matches the implicit assumption in this
|
||||
// fixture: the todo HUD lifecycle is independent of the working loader.
|
||||
viewSession: { isStreaming: false },
|
||||
todoReminderContainer,
|
||||
setTodos: vi.fn(),
|
||||
present,
|
||||
|
||||
Reference in New Issue
Block a user