fix(session): surface empty handoff generation as failure not cancel
The #7904 fix stopped masking provider errors as "Handoff cancelled", but an empty or whitespace-only generation still fell through: whitespace-only text passed the `!handoffText` guard and produced a bogus handoff, while empty text returned undefined which the interactive /handoff caller mapped to "Handoff cancelled" with no detail and no log entry. Treat empty/whitespace-only output as a real failure: a user-initiated handoff throws "Handoff generation produced no content" (surfaced as "Handoff failed: ...") and logs it; auto-handoff keeps returning undefined so maintenance falls back to context-full compaction. Also log genuine handoff failures in the command controller so they persist for debugging. Fixes #7993
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `/handoff` reporting "Handoff cancelled" for an empty or whitespace-only generation; a user-initiated handoff now surfaces "Handoff failed: Handoff generation produced no content" and logs the failure, while auto-handoff still falls back to context-full compaction ([#7993](https://github.com/can1357/oh-my-pi/issues/7993)).
|
||||
|
||||
## [17.2.11] - 2026-08-07
|
||||
|
||||
### Added
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
type UsageReport,
|
||||
} from "@oh-my-pi/pi-ai";
|
||||
import { Loader, Markdown, padding, Spacer, Text, visibleWidth } from "@oh-my-pi/pi-tui";
|
||||
import { formatDuration, Snowflake, sanitizeText } from "@oh-my-pi/pi-utils";
|
||||
import { formatDuration, logger, Snowflake, sanitizeText } from "@oh-my-pi/pi-utils";
|
||||
import { shouldEnableAppendOnlyContext } from "../../config/append-only-context-mode";
|
||||
import { type BashResult, isPersistentShellCdCommand } from "../../exec/bash-executor";
|
||||
import { type LoadedCustomShare, loadCustomShare } from "../../export/custom-share";
|
||||
@@ -1442,6 +1442,9 @@ export class CommandController {
|
||||
if (message === "Handoff cancelled") {
|
||||
this.ctx.showError("Handoff cancelled");
|
||||
} else {
|
||||
// Persist the real failure so it is debuggable after the transient
|
||||
// TUI error clears (#7993).
|
||||
logger.error("Handoff failed", { error: message });
|
||||
this.ctx.showError(`Handoff failed: ${message}`);
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -211,8 +211,22 @@ export class SessionHandoff {
|
||||
if (handoffSignal.aborted) {
|
||||
throw new Error("Handoff cancelled");
|
||||
}
|
||||
if (!handoffText) {
|
||||
return undefined;
|
||||
if (!handoffText || handoffText.trim().length === 0) {
|
||||
// Empty/whitespace-only generation is a real failure, not a user
|
||||
// cancellation. #7904 stopped masking provider errors as "Handoff
|
||||
// cancelled"; an empty document is the remaining path that produced the
|
||||
// same misleading, undebuggable message (#7993).
|
||||
logger.warn("Handoff generation produced no content", {
|
||||
sessionId: this.#host.sessionId(),
|
||||
autoTriggered: options?.autoTriggered ?? false,
|
||||
});
|
||||
// Auto-handoff is best-effort: returning undefined lets maintenance fall
|
||||
// back to context-full compaction. A user-initiated handoff must surface
|
||||
// the failure instead of a silent, misleading "cancelled".
|
||||
if (options?.autoTriggered) {
|
||||
return undefined;
|
||||
}
|
||||
throw new Error("Handoff generation produced no content");
|
||||
}
|
||||
|
||||
// Start a new session
|
||||
|
||||
@@ -2025,4 +2025,25 @@ describe("AgentSession handoff", () => {
|
||||
expect(generateHandoffSpy).toHaveBeenCalledTimes(1);
|
||||
expect(session.isGeneratingHandoff).toBe(false);
|
||||
});
|
||||
|
||||
it("surfaces empty handoff generation as a failure, not a false cancel", async () => {
|
||||
// Regression for #7993: the #7904 fix stopped masking provider errors as
|
||||
// "Handoff cancelled", but an empty/whitespace-only generation still returned
|
||||
// undefined, which the /handoff caller reported as "Handoff cancelled" with no
|
||||
// detail. Empty output is a real failure and must surface as one.
|
||||
const generateHandoffSpy = vi.spyOn(compactionModule, "generateHandoffFromContext").mockResolvedValue(" \n ");
|
||||
|
||||
await expect(session.handoff()).rejects.toThrow("Handoff generation produced no content");
|
||||
expect(generateHandoffSpy).toHaveBeenCalledTimes(1);
|
||||
expect(session.isGeneratingHandoff).toBe(false);
|
||||
});
|
||||
|
||||
it("auto-triggered handoff returns undefined on empty generation for context-full fallback", async () => {
|
||||
// Auto-handoff is best-effort: an empty document must NOT throw so maintenance
|
||||
// can fall back to context-full compaction (see runAutoCompaction).
|
||||
vi.spyOn(compactionModule, "generateHandoffFromContext").mockResolvedValue("");
|
||||
|
||||
const result = await session.handoff(undefined, { autoTriggered: true });
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user