Skip to content

Commit 97d7e4b

Browse files
committed
fix(daemon): make schtasks fallback baseline-aware in launch observation (#91144)
The previous baseline filter lived only inside hasLaunchEvidence, so a pre-existing verified gateway listener caused readLaunchObservation to report state 'running' (via resolveListenerBackedScheduledTaskRuntime) and short-circuit the fallback poll before the baseline filter was ever consulted — exactly the foreground-listener suppression that #91144 reported. Move task command/port resolution and baseline capture above readLaunchObservation, and treat a listener-backed 'running' whose pid is in the baseline as 'not-yet-run' so the bounded poll continues and hasLaunchEvidence re-checks with baseline filtering. Resolve the baseline from the installed task command port (same as hasLaunchEvidence) so the include check is consistent when argv/env override the configured gateway port. Restore the async resolveScheduledTaskGatewayListenerPids path so inspectPortUsage diagnostics are still used. Add two regression tests: - pre-existing verified gateway listener on the task port still fires the fallback (covers the P1 short-circuit the prior diff missed) - a fresh gateway listener appearing after /Run on a port that had a baseline listener does not relaunch (suppresses false-positive)
1 parent 074f9df commit 97d7e4b

2 files changed

Lines changed: 73 additions & 18 deletions

File tree

src/daemon/schtasks.startup-fallback.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,37 @@ describe("Windows startup fallback", () => {
405405
});
406406
});
407407

408+
it("fires the fallback when a pre-existing verified gateway listener holds the task port (#91144)", async () => {
409+
await withWindowsEnv("openclaw-win-startup-", async ({ env }) => {
410+
fastForwardTaskStartWait();
411+
findVerifiedGatewayListenerPidsOnPortSync.mockReturnValue([19444]);
412+
addAcceptedRunNeverStartsResponses();
413+
414+
await installGatewayScheduledTask(env);
415+
416+
expectStartupFallbackSpawn();
417+
});
418+
});
419+
420+
it("does not relaunch the task script when a fresh gateway listener appears after /Run on a port with a baseline listener (#91144)", async () => {
421+
await withWindowsEnv("openclaw-win-startup-", async ({ env }) => {
422+
fastForwardTaskStartWait();
423+
const sequence = [[19444], [19444], [19444, 28112]];
424+
let sequenceIndex = 0;
425+
findVerifiedGatewayListenerPidsOnPortSync.mockImplementation(() => {
426+
const value = sequence[sequenceIndex] ?? [];
427+
sequenceIndex += 1;
428+
return value;
429+
});
430+
addAcceptedRunNeverStartsResponses();
431+
432+
await installGatewayScheduledTask(env);
433+
434+
expect(spawn).not.toHaveBeenCalled();
435+
expect(findVerifiedGatewayListenerPidsOnPortSync).toHaveBeenCalled();
436+
});
437+
});
438+
408439
it("does not relaunch when the node Scheduled Task process is already running", async () => {
409440
await withWindowsEnv("openclaw-win-startup-", async ({ env }) => {
410441
vi.spyOn(process, "platform", "get").mockReturnValue("win32");

src/daemon/schtasks.ts

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1098,12 +1098,48 @@ async function shouldFallbackScheduledTaskLaunch(params: {
10981098
env: GatewayServiceEnv;
10991099
scriptPath: string;
11001100
}): Promise<boolean> {
1101+
const manageGatewayPort = shouldManageGatewayListenerPort(params.env);
1102+
const taskCommand = await readScheduledTaskCommand(params.env).catch(() => null);
1103+
const installedArguments = taskCommand?.programArguments;
1104+
const taskPort =
1105+
parsePortFromProgramArguments(installedArguments) ??
1106+
parsePositivePort(taskCommand?.environment?.OPENCLAW_GATEWAY_PORT) ??
1107+
resolveConfiguredGatewayPort(params.env);
1108+
// Capture pre-existing verified gateway listeners on the task port before
1109+
// polling for launch evidence. A listener that was already bound before the
1110+
// scheduled-task /Run attempt is not evidence that the task started; it must
1111+
// be filtered out of later observations so the fallback is not falsely
1112+
// suppressed (#91144). Resolve the port from the installed task command so
1113+
// the baseline and the launch-evidence check observe the same port.
1114+
const baselineGatewayPids =
1115+
manageGatewayPort && taskPort
1116+
? findVerifiedGatewayListenerPidsOnPortSync(taskPort)
1117+
: [];
1118+
11011119
const readLaunchObservation = async (): Promise<{
11021120
state: "running" | "not-yet-run" | "other";
11031121
signature: string;
11041122
}> => {
11051123
const runtime = await readScheduledTaskRuntime(params.env).catch(() => null);
11061124
if (runtime?.status === "running") {
1125+
// readScheduledTaskRuntime reports a listener-backed "running" status
1126+
// when schtasks itself is not running but a verified gateway listener
1127+
// holds the port. If that listener was present in the baseline (i.e. it
1128+
// pre-existed the /Run attempt), it does not prove the scheduled task
1129+
// launched; treat it as not-yet-run so the fallback poll continues and
1130+
// hasLaunchEvidence can re-check with baseline filtering (#91144).
1131+
if (
1132+
baselineGatewayPids.length > 0 &&
1133+
typeof runtime.pid === "number" &&
1134+
baselineGatewayPids.includes(runtime.pid)
1135+
) {
1136+
return {
1137+
state: "not-yet-run",
1138+
signature: [runtime.state, runtime.lastRunTime, runtime.lastRunResult, runtime.detail]
1139+
.filter(Boolean)
1140+
.join("|"),
1141+
};
1142+
}
11071143
return {
11081144
state: "running",
11091145
signature: [runtime.state, runtime.lastRunTime, runtime.lastRunResult, runtime.detail]
@@ -1127,27 +1163,15 @@ async function shouldFallbackScheduledTaskLaunch(params: {
11271163
.join("|"),
11281164
};
11291165
};
1130-
const manageGatewayPort = shouldManageGatewayListenerPort(params.env);
1131-
const configuredPort = resolveConfiguredGatewayPort(params.env);
1132-
const baselineGatewayPids =
1133-
manageGatewayPort && configuredPort
1134-
? findVerifiedGatewayListenerPidsOnPortSync(configuredPort)
1135-
: [];
11361166

11371167
const hasLaunchEvidence = async (): Promise<boolean> => {
1138-
const command = await readScheduledTaskCommand(params.env).catch(() => null);
1139-
const installedArguments = command?.programArguments;
1140-
const taskPort =
1141-
parsePortFromProgramArguments(installedArguments) ??
1142-
parsePositivePort(command?.environment?.OPENCLAW_GATEWAY_PORT) ??
1143-
resolveConfiguredGatewayPort(params.env);
11441168
if (manageGatewayPort && taskPort) {
1145-
const listenerPids = findVerifiedGatewayListenerPidsOnPortSync(taskPort);
1146-
if (baselineGatewayPids.length > 0) {
1147-
if (listenerPids.some((pid) => !baselineGatewayPids.includes(pid))) {
1148-
return true;
1149-
}
1150-
} else if (listenerPids.length > 0) {
1169+
const listenerPids = await resolveScheduledTaskGatewayListenerPids(taskPort);
1170+
const freshListenerPids =
1171+
baselineGatewayPids.length > 0
1172+
? listenerPids.filter((pid) => !baselineGatewayPids.includes(pid))
1173+
: listenerPids;
1174+
if (freshListenerPids.length > 0) {
11511175
return true;
11521176
}
11531177
}

0 commit comments

Comments
 (0)