From b428f1ab2227d1a0ecbdacb7222fba14eb7d665a Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Fri, 15 May 2026 04:32:05 +0200 Subject: [PATCH] fix(headless): send terminal notification when loop exits without stopAuto Headless mode waits for 'Assisted/Autonomous mode stopped' to detect completion. When the loop exits via natural break (e.g. step-wizard in /next), stopAuto() is never called, so headless hangs forever. - Add s.stopAutoCalled flag to AutoSession - Set flag in stopAuto(), clear in cleanupAfterLoopExit() - Send terminal notification from cleanupAfterLoopExit() only when stopAuto() was bypassed - Fixes sf headless next hanging after unit completes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/resources/extensions/sf/auto.js | 11 +++++++++++ src/resources/extensions/sf/auto/session.js | 6 ++++++ 2 files changed, 17 insertions(+) diff --git a/src/resources/extensions/sf/auto.js b/src/resources/extensions/sf/auto.js index e23a40463..5362b4280 100644 --- a/src/resources/extensions/sf/auto.js +++ b/src/resources/extensions/sf/auto.js @@ -741,6 +741,16 @@ function cleanupAfterLoopExit(ctx) { clearUnitTimeout(); restoreProjectRootEnv(); restoreMilestoneLockEnv(); + // ── Headless terminal notification ── + // When the loop exits naturally (e.g. step-wizard break) stopAuto() is + // never called, so the "Autonomous/Assisted mode stopped" notification + // that headless.ts waits for is never emitted. Send it here only when + // stopAuto() was bypassed so headless mode can detect completion. + if (!s.stopAutoCalled && ctx) { + const label = s.stepMode ? "Assisted mode stopped" : "Autonomous mode stopped"; + ctx.ui.notify(label, "info", { kind: "terminal", source: "workflow" }); + } + s.stopAutoCalled = false; // Clear crash lock and release session lock so the next `/next` does // not see a stale lock with the current PID and treat it as a "remote" // session (which would cause it to SIGTERM itself). (#2730) @@ -780,6 +790,7 @@ function cleanupAfterLoopExit(ctx) { } export async function stopAuto(ctx, pi, reason) { if (!s.active && !s.paused) return; + s.stopAutoCalled = true; _unregisterCtrlCInterceptor(); const loadedPreferences = loadEffectiveSFPreferences()?.preferences; const reasonSuffix = reason ? ` — ${reason}` : ""; diff --git a/src/resources/extensions/sf/auto/session.js b/src/resources/extensions/sf/auto/session.js index abb0f88c3..c6c589dbf 100644 --- a/src/resources/extensions/sf/auto/session.js +++ b/src/resources/extensions/sf/auto/session.js @@ -137,6 +137,11 @@ export class AutoSession { activeEngineId = null; activeRunDir = null; cmdCtx = null; + /** + * Set to true by stopAuto() so cleanupAfterLoopExit() can avoid sending + * a duplicate terminal notification in headless mode. + */ + stopAutoCalled = false; /** * Last known ExtensionCommandContext that had newSession(). * @@ -361,6 +366,7 @@ export class AutoSession { this.paused = false; this.stepMode = false; this.canAskUser = true; + this.stopAutoCalled = false; this.verbose = false; this.activeEngineId = null; this.activeRunDir = null;