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>
This commit is contained in:
Mikael Hugo 2026-05-15 04:32:05 +02:00
parent 78d52d7967
commit b428f1ab22
2 changed files with 17 additions and 0 deletions

View file

@ -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}` : "";

View file

@ -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;