refactor: consolidate chat state and task bridges
Centralize session scope, chat projections and typed task bridges. Keep historical interaction replies authoritative and publish terminal state to subscribers. Validate Server contracts before replacing generated artifacts.
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { publishRunEvent } from "./chat-run-accumulator.js";
|
||||
import { toSessionScope } from "../sessions/session-scope.js";
|
||||
import { refreshSessionCredential } from "../product/credential-refresh.js";
|
||||
import { type Router } from "express";
|
||||
import { z } from "zod";
|
||||
@@ -8,9 +10,7 @@ import { type OpencodeRuntimeAdapter } from "../runtime/opencode.js";
|
||||
import { SessionContextMismatchError, type SessionMetadataStore } from "../sessions/metadataStore.js";
|
||||
import { type SessionUiStateStore } from "../sessions/uiStateStore.js";
|
||||
import {
|
||||
type ActiveRun,
|
||||
toPermissionStatus,
|
||||
updateLastAssistantPermission,
|
||||
type ActiveRun, applyInteractionResponse,
|
||||
} from "./chatUiState.js";
|
||||
|
||||
const permissionReplyPayloadSchema = z.object({
|
||||
@@ -35,7 +35,6 @@ type RegisterInteractionRoutesOptions = {
|
||||
sessionUiStateStore: SessionUiStateStore;
|
||||
};
|
||||
|
||||
const toSessionUiStateContext = (sessionId: string) => ({ sessionId });
|
||||
|
||||
export const registerChatInteractionRoutes = (
|
||||
chatRouter: Router,
|
||||
@@ -104,12 +103,13 @@ export const registerChatInteractionRoutes = (
|
||||
}
|
||||
const persistPermissionState = async () => {
|
||||
const currentState = await sessionUiStateStore.read(
|
||||
toSessionUiStateContext(sessionRecord.sessionId),
|
||||
toSessionScope(sessionRecord),
|
||||
);
|
||||
await sessionUiStateStore.write(toSessionUiStateContext(sessionRecord.sessionId), {
|
||||
await sessionUiStateStore.write(toSessionScope(sessionRecord), {
|
||||
sessionId: sessionRecord.sessionId,
|
||||
isTitleManuallyEdited: currentState?.isTitleManuallyEdited ?? false,
|
||||
messages: run.messages,
|
||||
...currentState,
|
||||
messages: run.uiMessages?.() ?? run.messages,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -121,18 +121,10 @@ export const registerChatInteractionRoutes = (
|
||||
message: parsed.data.message,
|
||||
});
|
||||
} catch (error) {
|
||||
run.messages = updateLastAssistantPermission(
|
||||
run.messages,
|
||||
requestId,
|
||||
(permission) => ({
|
||||
...permission,
|
||||
status: "error",
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "failed to reply permission",
|
||||
}),
|
||||
);
|
||||
publishRunEvent(run, "permission_response", {
|
||||
session_id: sessionRecord.sessionId, request_id: requestId, status: "error",
|
||||
error: error instanceof Error ? error.message : "failed to reply permission",
|
||||
});
|
||||
await persistPermissionState().catch((persistError) => {
|
||||
logger.warn(
|
||||
{ err: persistError, sessionId: sessionRecord.sessionId },
|
||||
@@ -146,30 +138,15 @@ export const registerChatInteractionRoutes = (
|
||||
return;
|
||||
}
|
||||
|
||||
run.pendingPermissions.delete(requestId);
|
||||
const status = toPermissionStatus(parsed.data.reply);
|
||||
run.messages = updateLastAssistantPermission(
|
||||
run.messages,
|
||||
requestId,
|
||||
(permission) => ({
|
||||
...permission,
|
||||
status,
|
||||
repliedAt: Date.now(),
|
||||
}),
|
||||
);
|
||||
publishRunEvent(run, "permission_response", {
|
||||
session_id: sessionRecord.sessionId, request_id: requestId, reply: parsed.data.reply,
|
||||
});
|
||||
await persistPermissionState().catch((persistError) => {
|
||||
logger.warn(
|
||||
{ err: persistError, sessionId: sessionRecord.sessionId },
|
||||
"failed to persist permission reply state",
|
||||
);
|
||||
});
|
||||
for (const subscriber of run.subscribers) {
|
||||
subscriber.write("permission_response", {
|
||||
session_id: sessionRecord.sessionId,
|
||||
request_id: requestId,
|
||||
reply: parsed.data.reply,
|
||||
});
|
||||
}
|
||||
|
||||
res.status(202).json({
|
||||
session_id: sessionRecord.sessionId,
|
||||
@@ -222,20 +199,15 @@ export const registerChatInteractionRoutes = (
|
||||
const nativeStatus = confirmedStatus ? undefined : await runtime.getQuestionStatus({ sessionId: session.sessionId, requestId: canonicalId });
|
||||
const status = confirmedStatus ?? (nativeStatus === "cancelled" ? "rejected" : nativeStatus === "missing" ? "invalidated" : nativeStatus!);
|
||||
const persist = async (nextStatus: string, answers?: string[][]) => {
|
||||
const update = (messages: unknown[]) => messages.map((message) => {
|
||||
if (!isRecord(message)) return message;
|
||||
const questions = Array.isArray(message.questions) ? message.questions.map((q: unknown) => isRecord(q) && q.requestId === canonicalId ? { ...q, status: nextStatus, ...(answers ? { answers } : {}), error: undefined } : q) : undefined;
|
||||
const parts = Array.isArray(message.parts) ? [...message.parts] : undefined;
|
||||
if (parts && nextStatus !== "pending" && parts.some((part: unknown) => isRecord(part) && part.type === "data-question_request" && isRecord(part.data) && part.data.request_id === canonicalId)) {
|
||||
const response = { type: "data-question_response", data: { session_id: session.sessionId, request_id: canonicalId, status: nextStatus, rejected: nextStatus === "rejected" || nextStatus === "invalidated", ...(answers ? { answers } : {}) } };
|
||||
const index = parts.findIndex((part: unknown) => isRecord(part) && part.type === "data-question_response" && isRecord(part.data) && part.data.request_id === canonicalId);
|
||||
if (index >= 0) parts[index] = { ...parts[index], data: { ...(isRecord(parts[index]) && isRecord(parts[index].data) ? parts[index].data : {}), ...response.data } }; else parts.push(response);
|
||||
}
|
||||
return { ...message, ...(questions ? { questions } : {}), ...(parts ? { parts } : {}) };
|
||||
});
|
||||
const response = {
|
||||
session_id: session.sessionId, request_id: canonicalId, status: nextStatus,
|
||||
rejected: nextStatus === "rejected" || nextStatus === "invalidated", ...(answers ? { answers } : {}),
|
||||
};
|
||||
// Update the live projection before entering the same atomic persistence queue.
|
||||
if (run) { run.messages = update(run.messages); if (nextStatus !== "pending") run.pendingQuestions.delete(canonicalId); }
|
||||
await sessionUiStateStore.update(context, (latest) => ({ ...latest, messages: update(latest.messages) }));
|
||||
if (run && nextStatus !== "pending") publishRunEvent(run, "question_response", response);
|
||||
await sessionUiStateStore.update(context, (latest) => ({
|
||||
...latest, messages: applyInteractionResponse(latest.messages, "question_response", response),
|
||||
}));
|
||||
};
|
||||
if (action === "status" || status !== "pending") {
|
||||
await persist(status);
|
||||
@@ -246,7 +218,6 @@ export const registerChatInteractionRoutes = (
|
||||
if (action === "reply") await runtime.replyQuestion({ sessionId: session.sessionId, requestId: canonicalId, answers: answers ?? [] });
|
||||
else await runtime.rejectQuestion({ sessionId: session.sessionId, requestId: canonicalId });
|
||||
await persist(action === "reply" ? "answered" : "rejected", answers);
|
||||
for (const subscriber of run?.subscribers ?? []) subscriber.write("question_response", { session_id: session.sessionId, request_id: canonicalId, ...(action === "reply" ? { answers } : { rejected: true }) });
|
||||
if (run && run.status !== "running" && !run.pendingPermissions.size && !run.pendingQuestions.size) activeRuns.delete(session.sessionId);
|
||||
res.status(202).json({ session_id: session.sessionId, request_id: canonicalId, ...(action === "reply" ? { answers } : { rejected: true }) });
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user