fix: preserve interaction replies across chat recovery
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
import type { Request, Response, Router } from "express";
|
||||
import { z } from "zod";
|
||||
import { getLocalAgentContext } from "../context/localContext.js";
|
||||
import { InteractionRequestError, type InteractionRequestStore } from "../sessions/interaction-request-store.js";
|
||||
import { SessionContextMismatchError, type SessionMetadataStore } from "../sessions/metadataStore.js";
|
||||
import type { OpencodeRuntimeAdapter } from "../runtime/opencode.js";
|
||||
import { reconcileInteractionDelivery } from "../sessions/interaction-delivery-reconciliation.js";
|
||||
|
||||
const identifier = z.string().min(1).max(128);
|
||||
const listQuerySchema = z.object({
|
||||
limit: z.string().regex(/^[1-9]\d?$/).transform(Number).pipe(z.number().max(50)).optional(),
|
||||
cursor: identifier.optional(),
|
||||
}).strict();
|
||||
|
||||
type Options = { sessions: SessionMetadataStore; interactions: InteractionRequestStore;
|
||||
runtime?: Pick<OpencodeRuntimeAdapter, "agentMessageState" | "readInteractionResult">;
|
||||
isRunning?: (sessionId: string) => boolean };
|
||||
|
||||
/** These routes only persist user intent. Authorized /responses owns model continuation. */
|
||||
export function registerInteractionRequestRoutes(router: Router, options: Options) {
|
||||
const handle = (operation: "list" | "get" | "draft" | "reply" | "skip" | "cancel" | "resume") => async (req: Request, res: Response) => {
|
||||
res.setHeader("Cache-Control", "private, no-store");
|
||||
try {
|
||||
const sessionId = identifier.parse(req.params.session_id);
|
||||
const session = await options.sessions.get(getLocalAgentContext(req), sessionId);
|
||||
if (!session) { res.status(404).json({ code: "SESSION_NOT_FOUND", detail: "session not found" }); return; }
|
||||
const context = { sessionId: session.sessionId, actorKey: session.actorKey, projectKey: session.projectKey };
|
||||
if (operation === "list") {
|
||||
const page = await options.interactions.list(context, listQuerySchema.parse(req.query));
|
||||
const items = await Promise.all(page.items.map((record) => reconcileInteractionDelivery(context, record, options.interactions, options.runtime, options.isRunning?.(sessionId))));
|
||||
res.json({ ...page, items });
|
||||
return;
|
||||
}
|
||||
const interactionId = identifier.parse(req.params.interaction_id);
|
||||
if (operation === "get") {
|
||||
if (Object.keys(req.query).length) throw new InteractionRequestError("invalid_query", 400);
|
||||
const record = await options.interactions.get(context, interactionId);
|
||||
if (!record) { res.status(404).json({ code: "interaction_not_found", detail: "interaction not found" }); return; }
|
||||
res.json(await reconcileInteractionDelivery(context, record, options.interactions, options.runtime, options.isRunning?.(sessionId)));
|
||||
return;
|
||||
}
|
||||
if (Object.keys(req.query).length) throw new InteractionRequestError("invalid_query", 400);
|
||||
const record = operation === "draft"
|
||||
? await options.interactions.updateDraft(context, interactionId, req.body)
|
||||
: await options.interactions[operation](context, interactionId, req.body);
|
||||
// A queued or blocked delivery is a successful saved answer, never an inference error.
|
||||
res.json(record);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) { res.status(400).json({ code: "INTERACTION_INVALID", detail: "invalid interaction request" }); return; }
|
||||
if (error instanceof InteractionRequestError) { res.status(error.status).json({ code: error.code, detail: error.message, ...(error.current ? { current: error.current } : {}) }); return; }
|
||||
if (error instanceof SessionContextMismatchError) { res.status(404).json({ code: "SESSION_NOT_FOUND", detail: "session not found" }); return; }
|
||||
res.status(503).json({ code: "INTERACTION_UNAVAILABLE", detail: "interaction unavailable" });
|
||||
}
|
||||
};
|
||||
const base = "/session/:session_id/interactions";
|
||||
router.get(base, handle("list"));
|
||||
router.get(`${base}/:interaction_id`, handle("get"));
|
||||
router.put(`${base}/:interaction_id/draft`, handle("draft"));
|
||||
for (const operation of ["reply", "skip", "cancel", "resume"] as const) router.post(`${base}/:interaction_id/${operation}`, handle(operation));
|
||||
}
|
||||
Reference in New Issue
Block a user