mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 20:35:39 -06:00
fix(system-agent): retain QR cleanup on inference loss
This commit is contained in:
+11
-14
@@ -560,9 +560,14 @@ const setupWizard: ChannelSetupWizard = {
|
||||
Setup code that already owns a QR-backed operation can pass its raw QR text and completion promise through `WizardPrompter.qrCode`. OpenClaw renders and transports a bounded image; the plugin remains the authority for success, failure, and cancellation.
|
||||
|
||||
```typescript
|
||||
const link = startDeviceLink();
|
||||
if (!prompter.qrCode) {
|
||||
throw new Error(
|
||||
"This setup host cannot present QR credentials. Use the plugin's native setup flow.",
|
||||
);
|
||||
}
|
||||
|
||||
if (prompter.qrCode) {
|
||||
const link = startDeviceLink();
|
||||
try {
|
||||
await prompter.qrCode({
|
||||
title: "Link a device",
|
||||
message: "Scan the code and approve the device.",
|
||||
@@ -570,21 +575,13 @@ const setupWizard: ChannelSetupWizard = {
|
||||
expiresAtMs: link.expiresAtMs,
|
||||
dismissed: link.finished,
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
await prompter.note(
|
||||
`Open this device-link URI with your non-QR setup flow:\n${link.uri}`,
|
||||
"Link a device",
|
||||
);
|
||||
await link.finished;
|
||||
} catch (error) {
|
||||
link.cancel();
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
link.cancel();
|
||||
throw error;
|
||||
}
|
||||
```
|
||||
|
||||
`dismissed` is required and must settle with the producer operation. `qrCode(...)` returns `Promise<void>` only after that promise settles; there is no separate user Continue acknowledgement. Provide a non-QR fallback when `prompter.qrCode` is unavailable.
|
||||
`dismissed` is required and must settle with the producer operation. `qrCode(...)` returns `Promise<void>` only after that promise settles; there is no separate user Continue acknowledgement. Check capability before starting a credential-bearing operation. When `prompter.qrCode` is unavailable, route the operator to a plugin-native setup flow instead of putting the raw link URI in prompt text.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Shared allowFrom prompts">
|
||||
|
||||
@@ -331,7 +331,9 @@ export class SystemAgentChatEngine {
|
||||
this.router.clearForInferenceLoss();
|
||||
delete this.agentSession.cliSession;
|
||||
if (cancelWizard) {
|
||||
void this.wizard.dispose();
|
||||
// Inference loss terminates the conversation. Start the aggregate owner
|
||||
// disposal now so later Gateway/TUI cleanup joins the producer settlement.
|
||||
void this.dispose().catch(() => undefined);
|
||||
}
|
||||
this.history.splice(0);
|
||||
throw new SystemAgentInferenceUnavailableError("conversation", failures);
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
CANCEL_HINT,
|
||||
countCancelHints,
|
||||
expectDefined,
|
||||
SystemAgentInferenceUnavailableError,
|
||||
SystemAgentWizardAnswerError,
|
||||
type OpenClawConfig,
|
||||
type WizardPrompter,
|
||||
@@ -319,6 +320,81 @@ describe("SystemAgentChatEngine wizard", () => {
|
||||
expect(disposed).toBe(true);
|
||||
});
|
||||
|
||||
it("retains QR cleanup after inference loss clears the active bridge", async () => {
|
||||
const baseConfig = {
|
||||
agents: { defaults: { model: "openai/gpt-5.5" } },
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
apiKey: "test-key",
|
||||
auth: "api-key",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
const changedConfig = {
|
||||
agents: { defaults: { model: "anthropic/claude-opus-4-8" } },
|
||||
} satisfies OpenClawConfig;
|
||||
const verifiedInference = await createAmbientVerifiedBinding(baseConfig);
|
||||
let currentConfig: OpenClawConfig = baseConfig;
|
||||
let cleanupStarted = false;
|
||||
let releaseCleanup!: () => void;
|
||||
const cleanup = new Promise<void>((resolve) => {
|
||||
releaseCleanup = resolve;
|
||||
});
|
||||
const engine = new SystemAgentChatEngine({
|
||||
surface: "gateway",
|
||||
supportsQrCode: true,
|
||||
verifiedInference,
|
||||
runAgentTurn: async () => null,
|
||||
planWithAssistant: async () => null,
|
||||
deps: {
|
||||
readConfigFileSnapshot: vi.fn(async () => configSnapshot(currentConfig)) as never,
|
||||
loadOverview: fakeOverviewLoader(),
|
||||
},
|
||||
runChannelSetupWizard: async (_channel, prompter, _beforePersistentApply, signal) => {
|
||||
const owner = new Promise<void>((_resolve, reject) => {
|
||||
signal.addEventListener(
|
||||
"abort",
|
||||
() => reject(new Error("QR owner aborted", { cause: signal.reason })),
|
||||
{ once: true },
|
||||
);
|
||||
});
|
||||
try {
|
||||
await prompter.qrCode?.({
|
||||
title: "Link a device",
|
||||
message: "Scan this QR code and approve the device.",
|
||||
text: QR_TEXT,
|
||||
dismissed: owner,
|
||||
});
|
||||
} finally {
|
||||
cleanupStarted = true;
|
||||
await cleanup;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await engine.handle("connect telegram");
|
||||
currentConfig = changedConfig;
|
||||
await expect(engine.handle("status")).rejects.toBeInstanceOf(
|
||||
SystemAgentInferenceUnavailableError,
|
||||
);
|
||||
await vi.waitFor(() => expect(cleanupStarted).toBe(true));
|
||||
|
||||
let disposed = false;
|
||||
const disposal = engine.dispose().then(() => {
|
||||
disposed = true;
|
||||
});
|
||||
await Promise.resolve();
|
||||
expect(disposed).toBe(false);
|
||||
|
||||
releaseCleanup();
|
||||
await disposal;
|
||||
expect(disposed).toBe(true);
|
||||
});
|
||||
|
||||
it("scrubs an expired QR while its owner remains cancellable", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(1_800_000_000_000);
|
||||
|
||||
Reference in New Issue
Block a user