Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions apps/web/app/api/chat/chat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ vi.mock("@/app/api/sessions/shared", () => ({
getAgentSession: vi.fn(() => undefined),
}));

vi.mock("@/lib/dench-cloud-settings", () => ({
readConfiguredSelectedDenchModel: vi.fn(() => null),
}));

describe("Chat API routes", () => {
beforeEach(() => {
vi.resetModules();
Expand Down Expand Up @@ -85,6 +89,9 @@ describe("Chat API routes", () => {
vi.mock("@/app/api/sessions/shared", () => ({
getAgentSession: vi.fn(() => undefined),
}));
vi.mock("@/lib/dench-cloud-settings", () => ({
readConfiguredSelectedDenchModel: vi.fn(() => null),
}));
});

afterEach(() => {
Expand Down Expand Up @@ -179,6 +186,36 @@ describe("Chat API routes", () => {
);
});

it("passes the configured selected model into startRun when no override is provided", async () => {
const { startRun, hasActiveRun, subscribeToRun } = await import("@/lib/active-runs");
const { readConfiguredSelectedDenchModel } = await import("@/lib/dench-cloud-settings");
vi.mocked(hasActiveRun).mockReturnValue(false);
vi.mocked(subscribeToRun).mockReturnValue(() => {});
vi.mocked(readConfiguredSelectedDenchModel).mockReturnValue("anthropic.claude-sonnet-4-6-v1");

const { POST } = await import("./route.js");
const req = new Request("http://localhost/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
messages: [
{ id: "m1", role: "user", parts: [{ type: "text", text: "hello" }] },
],
sessionId: "s1",
}),
});

await POST(req);

expect(startRun).toHaveBeenCalledWith(
expect.objectContaining({
sessionId: "s1",
sessionModel: "anthropic.claude-sonnet-4-6-v1",
modelOverride: undefined,
}),
);
});

it("returns JSON when an unsafe OpenAI switch needs acknowledgement", async () => {
const { getAgentSession } = await import("@/app/api/sessions/shared");
const { startRun } = await import("@/lib/active-runs");
Expand Down
4 changes: 4 additions & 0 deletions apps/web/app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
isLikelyOpenAiModelId,
needsOpenAiSwitchAcknowledgement,
} from "@/lib/chat-models";
import { readConfiguredSelectedDenchModel } from "@/lib/dench-cloud-settings";

export const runtime = "nodejs";

Expand Down Expand Up @@ -259,6 +260,8 @@ export async function POST(req: Request) {
const gatewayThreadId = sessionMeta?.gatewaySessionId ?? sessionId;

const imageAttachments = extractImageAttachmentsFromMessage(agentMessage);
const sessionModel =
normalizedModelOverride ?? readConfiguredSelectedDenchModel() ?? undefined;

try {
startRun({
Expand All @@ -267,6 +270,7 @@ export async function POST(req: Request) {
agentSessionId: gatewayThreadId,
overrideAgentId: effectiveAgentId,
modelOverride: normalizedModelOverride,
sessionModel,
imageAttachments: imageAttachments.length > 0
? imageAttachments
: undefined,
Expand Down
29 changes: 29 additions & 0 deletions apps/web/app/api/chat/stop/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,33 @@ describe("POST /api/chat/stop", () => {
expect(listSubagentsForRequesterSession).not.toHaveBeenCalled();
expect(json).toEqual({ aborted: true, abortedChildren: 0 });
});

it("stops a gateway-backed session when a non-subagent sessionKey is provided", async () => {
const { abortRun, getActiveRun } = await import("@/lib/active-runs");
const { listSubagentsForRequesterSession } = await import("@/lib/subagent-registry");

vi.mocked(getActiveRun).mockImplementation(((runKey: string) => {
if (runKey === "agent:main:telegram:channel-1") {
return { status: "running" };
}
return undefined;
}) as never);
vi.mocked(abortRun).mockReturnValue(true);

const { POST } = await import("./route.js");
const req = new Request("http://localhost/api/chat/stop", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
sessionKey: "agent:main:telegram:channel-1",
}),
});

const res = await POST(req);
const json = await res.json();

expect(abortRun).toHaveBeenCalledWith("agent:main:telegram:channel-1");
expect(listSubagentsForRequesterSession).not.toHaveBeenCalled();
expect(json).toEqual({ aborted: true, abortedChildren: 0 });
});
});
14 changes: 9 additions & 5 deletions apps/web/app/api/chat/stop/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* POST /api/chat/stop
*
* Abort an active agent run. Called by the Stop button.
* Works for both parent sessions (by sessionId) and subagent sessions (by sessionKey).
* Works for parent sessions (by sessionId) and any session-key backed run.
*/
import { abortRun, getActiveRun } from "@/lib/active-runs";
import { listSubagentsForRequesterSession } from "@/lib/subagent-registry";
Expand All @@ -17,11 +17,15 @@ export async function POST(req: Request) {
.json()
.catch(() => ({}));

const isSubagentSession = typeof body.sessionKey === "string" && body.sessionKey.includes(":subagent:");
const runKey = isSubagentSession && body.sessionKey ? body.sessionKey : body.sessionId;
const sessionKey =
typeof body.sessionKey === "string" && body.sessionKey.trim()
? body.sessionKey.trim()
: undefined;
const isSubagentSession = Boolean(sessionKey?.includes(":subagent:"));
const runKey = sessionKey ?? body.sessionId;

if (!runKey) {
return new Response("sessionId or subagent sessionKey required", { status: 400 });
return new Response("sessionId or sessionKey required", { status: 400 });
}

const run = getActiveRun(runKey);
Expand All @@ -30,7 +34,7 @@ export async function POST(req: Request) {
const aborted = canAbort ? abortRun(runKey) : false;
let abortedChildren = 0;

if (!isSubagentSession && body.sessionId && body.cascadeChildren) {
if (!sessionKey && body.sessionId && body.cascadeChildren) {
const fallbackAgentId = resolveActiveAgentId();
const requesterSessionKey = resolveSessionKey(body.sessionId, fallbackAgentId);
for (const subagent of listSubagentsForRequesterSession(requesterSessionKey)) {
Expand Down
Loading
Loading