fix(gateway): retain settled cleanup failures

This commit is contained in:
jesse-merhi
2026-08-13 05:35:39 +10:00
parent 08102533e3
commit fbecf73b4f
3 changed files with 44 additions and 2 deletions
+4
View File
@@ -136,6 +136,10 @@ snapshot, so re-read them on every reconnect.
### Present system-agent QR codes
System-agent setup is administrative. Request `operator.admin` when pairing the
device; every `openclaw.chat` call in this flow, including setup, polling,
cancellation, and reset, requires that scope.
Advertise `GATEWAY_CLIENT_CAPS.SYSTEM_AGENT_QR_CODE` only when the client can
render a QR image. A capable `openclaw.chat` session can then receive a QR
`step` through the same wizard-step contract used for other setup controls:
@@ -216,4 +216,31 @@ describe("system-agent session lifecycle", () => {
await retirementSettlement;
expect(sessions.size).toBe(0);
});
it("retains a settled initialization cleanup failure until retirement reports it once", async () => {
const requestError = new Error("request failed before retirement");
const cleanupError = new Error("cleanup failed before retirement");
const sessions = new Map() as Sessions;
const wizardSessions = new Map() as GatewayRequestContext["wizardSessions"];
const initialization = initializeSystemAgentSession(
sessions,
"failed-session",
async ({ ownEngine }) => {
ownEngine(
session("device:one", async () => {
throw cleanupError;
}).engine,
);
throw requestError;
},
);
await expect(initialization).rejects.toBe(requestError);
await expect(
retireAndDisposeSystemAgentSessions({ sessions, wizardSessions }),
).rejects.toMatchObject({ errors: [cleanupError] });
await expect(
retireAndDisposeSystemAgentSessions({ sessions, wizardSessions }),
).resolves.toBeUndefined();
});
});
@@ -14,6 +14,7 @@ type ApprovalManager = NonNullable<GatewayRequestContext["systemAgentApprovalMan
const retiredStores = new WeakSet<SystemAgentSessions>();
const pendingSessionSettlements = new WeakMap<SystemAgentSessions, Set<Promise<void>>>();
const settledInitializationCleanupFailures = new WeakMap<SystemAgentSessions, Error[]>();
const retiredWizardStores = new WeakSet<WizardSessions>();
const pendingWizardAdmissions = new WeakMap<
WizardSessions,
@@ -77,8 +78,14 @@ export function initializeSystemAgentSession(
await uncommittedEngine?.dispose();
settlement.resolve();
} catch (error) {
// Cleanup belongs to the retiring owner, while the request keeps its original outcome.
settlement.reject(error);
const cleanupError = toErrorObject(error, "Unknown system-agent cleanup failure");
if (!retiredStores.has(sessions)) {
const failures = settledInitializationCleanupFailures.get(sessions) ?? [];
failures.push(cleanupError);
settledInitializationCleanupFailures.set(sessions, failures);
}
// Retirement already snapshots an in-flight lease; only earlier failures need escrow.
settlement.reject(cleanupError);
}
}
})();
@@ -209,8 +216,12 @@ export function retireAndDisposeSystemAgentSessions(params: {
}): Promise<void> {
retiredStores.add(params.sessions);
retiredWizardStores.add(params.wizardSessions);
const settledInitializationFailures =
settledInitializationCleanupFailures.get(params.sessions) ?? [];
settledInitializationCleanupFailures.delete(params.sessions);
const disposals: Promise<unknown>[] = [
...(pendingSessionSettlements.get(params.sessions) ?? []),
...settledInitializationFailures.map((error) => Promise.reject(error)),
...(pendingWizardAdmissions.get(params.wizardSessions) ?? []),
];
for (const [sessionId, session] of params.sessions) {