fix: abort embedded prompts on cancel
This commit is contained in:
@@ -15,6 +15,9 @@ Docs: https://docs.clawd.bot
|
||||
|
||||
## 2026.1.18-2
|
||||
|
||||
### Fixes
|
||||
- Tests: stabilize plugin SDK resolution and embedded agent timeouts.
|
||||
|
||||
## 2026.1.17-6
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -146,7 +146,7 @@ const readSessionMessages = async (sessionFile: string) => {
|
||||
};
|
||||
|
||||
describe("runEmbeddedPiAgent", () => {
|
||||
it("appends new user + assistant after existing transcript entries", async () => {
|
||||
it("appends new user + assistant after existing transcript entries", { timeout: 90_000 }, async () => {
|
||||
const { SessionManager } = await import("@mariozechner/pi-coding-agent");
|
||||
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-agent-"));
|
||||
@@ -271,7 +271,7 @@ describe("runEmbeddedPiAgent", () => {
|
||||
expect(firstAssistantIndex).toBeGreaterThan(firstUserIndex);
|
||||
expect(secondUserIndex).toBeGreaterThan(firstAssistantIndex);
|
||||
expect(secondAssistantIndex).toBeGreaterThan(secondUserIndex);
|
||||
}, 20_000);
|
||||
}, 90_000);
|
||||
it("repairs orphaned user messages and continues", async () => {
|
||||
const { SessionManager } = await import("@mariozechner/pi-coding-agent");
|
||||
|
||||
|
||||
@@ -192,7 +192,7 @@ describe("runEmbeddedPiAgent", () => {
|
||||
|
||||
await expect(fs.stat(path.join(agentDir, "models.json"))).resolves.toBeTruthy();
|
||||
});
|
||||
it("persists the first user message before assistant output", { timeout: 45_000 }, async () => {
|
||||
it("persists the first user message before assistant output", { timeout: 60_000 }, async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-agent-"));
|
||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-workspace-"));
|
||||
const sessionFile = path.join(workspaceDir, "session.jsonl");
|
||||
|
||||
@@ -359,6 +359,33 @@ export async function runEmbeddedAttempt(
|
||||
runAbortController.abort();
|
||||
void activeSession.abort();
|
||||
};
|
||||
const abortable = <T>(promise: Promise<T>): Promise<T> => {
|
||||
const signal = runAbortController.signal;
|
||||
if (signal.aborted) {
|
||||
const err = new Error("aborted");
|
||||
(err as { name?: string }).name = "AbortError";
|
||||
return Promise.reject(err);
|
||||
}
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const onAbort = () => {
|
||||
const err = new Error("aborted");
|
||||
(err as { name?: string }).name = "AbortError";
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
reject(err);
|
||||
};
|
||||
signal.addEventListener("abort", onAbort, { once: true });
|
||||
promise.then(
|
||||
(value) => {
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
resolve(value);
|
||||
},
|
||||
(err) => {
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
reject(err);
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const subscription = subscribeEmbeddedPiSession({
|
||||
session: activeSession,
|
||||
@@ -454,7 +481,7 @@ export async function runEmbeddedAttempt(
|
||||
}
|
||||
|
||||
try {
|
||||
await activeSession.prompt(params.prompt, { images: params.images });
|
||||
await abortable(activeSession.prompt(params.prompt, { images: params.images }));
|
||||
} catch (err) {
|
||||
promptError = err;
|
||||
} finally {
|
||||
|
||||
@@ -172,6 +172,10 @@ vi.mock("../agents/skills-status.js", () => ({
|
||||
buildWorkspaceSkillStatus: () => ({ skills: [] }),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/loader.js", () => ({
|
||||
loadClawdbotPlugins: () => ({ plugins: [], diagnostics: [] }),
|
||||
}));
|
||||
|
||||
vi.mock("../config/config.js", async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
@@ -248,7 +252,7 @@ vi.mock("../telegram/pairing-store.js", () => ({
|
||||
|
||||
vi.mock("../pairing/pairing-store.js", () => ({
|
||||
readChannelAllowFromStore: vi.fn().mockResolvedValue([]),
|
||||
upsertChannelPairingRequest: vi.fn().mockResolvedValue({ code: "", created: false }),
|
||||
upsertChannelPairingRequest: vi.fn().mockResolvedValue({ code: "000000", created: false }),
|
||||
}));
|
||||
|
||||
vi.mock("../telegram/token.js", () => ({
|
||||
@@ -374,5 +378,5 @@ describe("doctor command", () => {
|
||||
|
||||
expect(runLegacyStateMigrations).toHaveBeenCalledTimes(1);
|
||||
expect(confirm).not.toHaveBeenCalled();
|
||||
}, 20_000);
|
||||
}, 30_000);
|
||||
});
|
||||
|
||||
@@ -172,6 +172,10 @@ vi.mock("../agents/skills-status.js", () => ({
|
||||
buildWorkspaceSkillStatus: () => ({ skills: [] }),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/loader.js", () => ({
|
||||
loadClawdbotPlugins: () => ({ plugins: [], diagnostics: [] }),
|
||||
}));
|
||||
|
||||
vi.mock("../config/config.js", async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
@@ -248,7 +252,7 @@ vi.mock("../telegram/pairing-store.js", () => ({
|
||||
|
||||
vi.mock("../pairing/pairing-store.js", () => ({
|
||||
readChannelAllowFromStore: vi.fn().mockResolvedValue([]),
|
||||
upsertChannelPairingRequest: vi.fn().mockResolvedValue({ code: "", created: false }),
|
||||
upsertChannelPairingRequest: vi.fn().mockResolvedValue({ code: "000000", created: false }),
|
||||
}));
|
||||
|
||||
vi.mock("../telegram/token.js", () => ({
|
||||
@@ -362,7 +366,7 @@ describe("doctor command", () => {
|
||||
expect(written.routing).toBeUndefined();
|
||||
});
|
||||
|
||||
it("migrates legacy gateway services", async () => {
|
||||
it("migrates legacy gateway services", { timeout: 30_000 }, async () => {
|
||||
readConfigFileSnapshot.mockResolvedValue({
|
||||
path: "/tmp/clawdbot.json",
|
||||
exists: true,
|
||||
|
||||
@@ -172,6 +172,10 @@ vi.mock("../agents/skills-status.js", () => ({
|
||||
buildWorkspaceSkillStatus: () => ({ skills: [] }),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/loader.js", () => ({
|
||||
loadClawdbotPlugins: () => ({ plugins: [], diagnostics: [] }),
|
||||
}));
|
||||
|
||||
vi.mock("../config/config.js", async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
@@ -248,7 +252,7 @@ vi.mock("../telegram/pairing-store.js", () => ({
|
||||
|
||||
vi.mock("../pairing/pairing-store.js", () => ({
|
||||
readChannelAllowFromStore: vi.fn().mockResolvedValue([]),
|
||||
upsertChannelPairingRequest: vi.fn().mockResolvedValue({ code: "", created: false }),
|
||||
upsertChannelPairingRequest: vi.fn().mockResolvedValue({ code: "000000", created: false }),
|
||||
}));
|
||||
|
||||
vi.mock("../telegram/token.js", () => ({
|
||||
@@ -375,7 +379,7 @@ describe("doctor command", () => {
|
||||
|
||||
expect(runLegacyStateMigrations).toHaveBeenCalledTimes(1);
|
||||
expect(confirm).not.toHaveBeenCalled();
|
||||
}, 20_000);
|
||||
}, 30_000);
|
||||
|
||||
it("skips gateway restarts in non-interactive mode", async () => {
|
||||
readConfigFileSnapshot.mockResolvedValue({
|
||||
@@ -448,5 +452,5 @@ describe("doctor command", () => {
|
||||
const profiles = (written.auth as { profiles: Record<string, unknown> }).profiles;
|
||||
expect(profiles["anthropic:me@example.com"]).toBeTruthy();
|
||||
expect(profiles["anthropic:default"]).toBeUndefined();
|
||||
}, 20_000);
|
||||
}, 30_000);
|
||||
});
|
||||
|
||||
@@ -172,6 +172,10 @@ vi.mock("../agents/skills-status.js", () => ({
|
||||
buildWorkspaceSkillStatus: () => ({ skills: [] }),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/loader.js", () => ({
|
||||
loadClawdbotPlugins: () => ({ plugins: [], diagnostics: [] }),
|
||||
}));
|
||||
|
||||
vi.mock("../config/config.js", async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
@@ -248,7 +252,7 @@ vi.mock("../telegram/pairing-store.js", () => ({
|
||||
|
||||
vi.mock("../pairing/pairing-store.js", () => ({
|
||||
readChannelAllowFromStore: vi.fn().mockResolvedValue([]),
|
||||
upsertChannelPairingRequest: vi.fn().mockResolvedValue({ code: "", created: false }),
|
||||
upsertChannelPairingRequest: vi.fn().mockResolvedValue({ code: "000000", created: false }),
|
||||
}));
|
||||
|
||||
vi.mock("../telegram/token.js", () => ({
|
||||
@@ -373,7 +377,7 @@ describe("doctor command", () => {
|
||||
);
|
||||
}),
|
||||
).toBe(true);
|
||||
}, 20_000);
|
||||
}, 30_000);
|
||||
|
||||
it("warns when extra workspace directories exist", async () => {
|
||||
readConfigFileSnapshot.mockResolvedValue({
|
||||
|
||||
@@ -172,6 +172,10 @@ vi.mock("../agents/skills-status.js", () => ({
|
||||
buildWorkspaceSkillStatus: () => ({ skills: [] }),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/loader.js", () => ({
|
||||
loadClawdbotPlugins: () => ({ plugins: [], diagnostics: [] }),
|
||||
}));
|
||||
|
||||
vi.mock("../config/config.js", async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
@@ -248,7 +252,7 @@ vi.mock("../telegram/pairing-store.js", () => ({
|
||||
|
||||
vi.mock("../pairing/pairing-store.js", () => ({
|
||||
readChannelAllowFromStore: vi.fn().mockResolvedValue([]),
|
||||
upsertChannelPairingRequest: vi.fn().mockResolvedValue({ code: "", created: false }),
|
||||
upsertChannelPairingRequest: vi.fn().mockResolvedValue({ code: "000000", created: false }),
|
||||
}));
|
||||
|
||||
vi.mock("../telegram/token.js", () => ({
|
||||
@@ -348,7 +352,7 @@ describe("doctor command", () => {
|
||||
const stateNote = note.mock.calls.find((call) => call[1] === "State integrity");
|
||||
expect(stateNote).toBeTruthy();
|
||||
expect(String(stateNote?.[0])).toContain("CRITICAL");
|
||||
}, 20_000);
|
||||
}, 30_000);
|
||||
|
||||
it("warns about opencode provider overrides", async () => {
|
||||
readConfigFileSnapshot.mockResolvedValue({
|
||||
|
||||
@@ -252,7 +252,7 @@ async function connectClient(params: { url: string; token: string }) {
|
||||
}
|
||||
|
||||
describe("gateway (mock openai): tool calling", () => {
|
||||
it("runs a Read tool call end-to-end via gateway agent loop", async () => {
|
||||
it("runs a Read tool call end-to-end via gateway agent loop", { timeout: 90_000 }, async () => {
|
||||
const prev = {
|
||||
home: process.env.HOME,
|
||||
configPath: process.env.CLAWDBOT_CONFIG_PATH,
|
||||
|
||||
@@ -268,5 +268,5 @@ describe("gateway wizard (e2e)", () => {
|
||||
process.env.CLAWDBOT_SKIP_CRON = prev.skipCron;
|
||||
process.env.CLAWDBOT_SKIP_CANVAS_HOST = prev.skipCanvas;
|
||||
}
|
||||
}, 60_000);
|
||||
}, 90_000);
|
||||
});
|
||||
|
||||
@@ -14,10 +14,10 @@ import {
|
||||
installGatewayTestHooks();
|
||||
|
||||
describe("gateway server auth/connect", () => {
|
||||
test("closes silent handshakes after timeout", { timeout: 15_000 }, async () => {
|
||||
test("closes silent handshakes after timeout", { timeout: 30_000 }, async () => {
|
||||
const { server, ws } = await startServerWithClient();
|
||||
const closed = await new Promise<boolean>((resolve) => {
|
||||
const timer = setTimeout(() => resolve(false), 12_000);
|
||||
const timer = setTimeout(() => resolve(false), 25_000);
|
||||
ws.once("close", () => {
|
||||
clearTimeout(timer);
|
||||
resolve(true);
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
installGatewayTestHooks();
|
||||
|
||||
describe("gateway server health/presence", () => {
|
||||
test("connect + health + presence + status succeed", { timeout: 8000 }, async () => {
|
||||
test("connect + health + presence + status succeed", { timeout: 20_000 }, async () => {
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ describe("gateway server misc", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("send dedupes by idempotencyKey", { timeout: 8000 }, async () => {
|
||||
test("send dedupes by idempotencyKey", { timeout: 20_000 }, async () => {
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ describe("gateway server models + voicewake", () => {
|
||||
|
||||
test(
|
||||
"voicewake.get returns defaults and voicewake.set broadcasts",
|
||||
{ timeout: 15_000 },
|
||||
{ timeout: 30_000 },
|
||||
async () => {
|
||||
const homeDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-home-"));
|
||||
const restoreHome = setTempHome(homeDir);
|
||||
|
||||
@@ -1,5 +1,39 @@
|
||||
import type { LogLevel } from "../../logging/levels.js";
|
||||
|
||||
type ShouldLogVerbose = typeof import("../../globals.js").shouldLogVerbose;
|
||||
type DispatchReplyWithBufferedBlockDispatcher =
|
||||
typeof import("../../auto-reply/reply/provider-dispatcher.js").dispatchReplyWithBufferedBlockDispatcher;
|
||||
type CreateReplyDispatcherWithTyping =
|
||||
typeof import("../../auto-reply/reply/reply-dispatcher.js").createReplyDispatcherWithTyping;
|
||||
type ResolveEffectiveMessagesConfig =
|
||||
typeof import("../../agents/identity.js").resolveEffectiveMessagesConfig;
|
||||
type ResolveHumanDelayConfig = typeof import("../../agents/identity.js").resolveHumanDelayConfig;
|
||||
type ResolveAgentRoute = typeof import("../../routing/resolve-route.js").resolveAgentRoute;
|
||||
type BuildPairingReply = typeof import("../../pairing/pairing-messages.js").buildPairingReply;
|
||||
type ReadChannelAllowFromStore =
|
||||
typeof import("../../pairing/pairing-store.js").readChannelAllowFromStore;
|
||||
type UpsertChannelPairingRequest =
|
||||
typeof import("../../pairing/pairing-store.js").upsertChannelPairingRequest;
|
||||
type FetchRemoteMedia = typeof import("../../media/fetch.js").fetchRemoteMedia;
|
||||
type SaveMediaBuffer = typeof import("../../media/store.js").saveMediaBuffer;
|
||||
type BuildMentionRegexes = typeof import("../../auto-reply/reply/mentions.js").buildMentionRegexes;
|
||||
type MatchesMentionPatterns =
|
||||
typeof import("../../auto-reply/reply/mentions.js").matchesMentionPatterns;
|
||||
type ResolveChannelGroupPolicy =
|
||||
typeof import("../../config/group-policy.js").resolveChannelGroupPolicy;
|
||||
type ResolveChannelGroupRequireMention =
|
||||
typeof import("../../config/group-policy.js").resolveChannelGroupRequireMention;
|
||||
type CreateInboundDebouncer =
|
||||
typeof import("../../auto-reply/inbound-debounce.js").createInboundDebouncer;
|
||||
type ResolveInboundDebounceMs =
|
||||
typeof import("../../auto-reply/inbound-debounce.js").resolveInboundDebounceMs;
|
||||
type ResolveCommandAuthorizedFromAuthorizers =
|
||||
typeof import("../../channels/command-gating.js").resolveCommandAuthorizedFromAuthorizers;
|
||||
type ResolveTextChunkLimit = typeof import("../../auto-reply/chunk.js").resolveTextChunkLimit;
|
||||
type ChunkMarkdownText = typeof import("../../auto-reply/chunk.js").chunkMarkdownText;
|
||||
type HasControlCommand = typeof import("../../auto-reply/command-detection.js").hasControlCommand;
|
||||
type ResolveStateDir = typeof import("../../config/paths.js").resolveStateDir;
|
||||
|
||||
export type RuntimeLogger = {
|
||||
debug?: (message: string) => void;
|
||||
info: (message: string) => void;
|
||||
@@ -11,52 +45,52 @@ export type PluginRuntime = {
|
||||
version: string;
|
||||
channel: {
|
||||
text: {
|
||||
chunkMarkdownText: typeof import("../../auto-reply/chunk.js").chunkMarkdownText;
|
||||
resolveTextChunkLimit: typeof import("../../auto-reply/chunk.js").resolveTextChunkLimit;
|
||||
hasControlCommand: typeof import("../../auto-reply/command-detection.js").hasControlCommand;
|
||||
chunkMarkdownText: ChunkMarkdownText;
|
||||
resolveTextChunkLimit: ResolveTextChunkLimit;
|
||||
hasControlCommand: HasControlCommand;
|
||||
};
|
||||
reply: {
|
||||
dispatchReplyWithBufferedBlockDispatcher: typeof import("../../auto-reply/reply/provider-dispatcher.js").dispatchReplyWithBufferedBlockDispatcher;
|
||||
createReplyDispatcherWithTyping: typeof import("../../auto-reply/reply/reply-dispatcher.js").createReplyDispatcherWithTyping;
|
||||
resolveEffectiveMessagesConfig: typeof import("../../agents/identity.js").resolveEffectiveMessagesConfig;
|
||||
resolveHumanDelayConfig: typeof import("../../agents/identity.js").resolveHumanDelayConfig;
|
||||
dispatchReplyWithBufferedBlockDispatcher: DispatchReplyWithBufferedBlockDispatcher;
|
||||
createReplyDispatcherWithTyping: CreateReplyDispatcherWithTyping;
|
||||
resolveEffectiveMessagesConfig: ResolveEffectiveMessagesConfig;
|
||||
resolveHumanDelayConfig: ResolveHumanDelayConfig;
|
||||
};
|
||||
routing: {
|
||||
resolveAgentRoute: typeof import("../../routing/resolve-route.js").resolveAgentRoute;
|
||||
resolveAgentRoute: ResolveAgentRoute;
|
||||
};
|
||||
pairing: {
|
||||
buildPairingReply: typeof import("../../pairing/pairing-messages.js").buildPairingReply;
|
||||
readAllowFromStore: typeof import("../../pairing/pairing-store.js").readChannelAllowFromStore;
|
||||
upsertPairingRequest: typeof import("../../pairing/pairing-store.js").upsertChannelPairingRequest;
|
||||
buildPairingReply: BuildPairingReply;
|
||||
readAllowFromStore: ReadChannelAllowFromStore;
|
||||
upsertPairingRequest: UpsertChannelPairingRequest;
|
||||
};
|
||||
media: {
|
||||
fetchRemoteMedia: typeof import("../../media/fetch.js").fetchRemoteMedia;
|
||||
saveMediaBuffer: typeof import("../../media/store.js").saveMediaBuffer;
|
||||
fetchRemoteMedia: FetchRemoteMedia;
|
||||
saveMediaBuffer: SaveMediaBuffer;
|
||||
};
|
||||
mentions: {
|
||||
buildMentionRegexes: typeof import("../../auto-reply/reply/mentions.js").buildMentionRegexes;
|
||||
matchesMentionPatterns: typeof import("../../auto-reply/reply/mentions.js").matchesMentionPatterns;
|
||||
buildMentionRegexes: BuildMentionRegexes;
|
||||
matchesMentionPatterns: MatchesMentionPatterns;
|
||||
};
|
||||
groups: {
|
||||
resolveGroupPolicy: typeof import("../../config/group-policy.js").resolveChannelGroupPolicy;
|
||||
resolveRequireMention: typeof import("../../config/group-policy.js").resolveChannelGroupRequireMention;
|
||||
resolveGroupPolicy: ResolveChannelGroupPolicy;
|
||||
resolveRequireMention: ResolveChannelGroupRequireMention;
|
||||
};
|
||||
debounce: {
|
||||
createInboundDebouncer: typeof import("../../auto-reply/inbound-debounce.js").createInboundDebouncer;
|
||||
resolveInboundDebounceMs: typeof import("../../auto-reply/inbound-debounce.js").resolveInboundDebounceMs;
|
||||
createInboundDebouncer: CreateInboundDebouncer;
|
||||
resolveInboundDebounceMs: ResolveInboundDebounceMs;
|
||||
};
|
||||
commands: {
|
||||
resolveCommandAuthorizedFromAuthorizers: typeof import("../../channels/command-gating.js").resolveCommandAuthorizedFromAuthorizers;
|
||||
resolveCommandAuthorizedFromAuthorizers: ResolveCommandAuthorizedFromAuthorizers;
|
||||
};
|
||||
};
|
||||
logging: {
|
||||
shouldLogVerbose: typeof import("../../globals.js").shouldLogVerbose;
|
||||
shouldLogVerbose: ShouldLogVerbose;
|
||||
getChildLogger: (
|
||||
bindings?: Record<string, unknown>,
|
||||
opts?: { level?: LogLevel },
|
||||
) => RuntimeLogger;
|
||||
};
|
||||
state: {
|
||||
resolveStateDir: typeof import("../../config/paths.js").resolveStateDir;
|
||||
resolveStateDir: ResolveStateDir;
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user