feat(sessions): persist shared unread state

This commit is contained in:
Federico Jaramillo Martinez
2026-07-20 19:36:16 +02:00
parent a20a8c8c09
commit 115d74e79a
37 changed files with 4272 additions and 66 deletions
+36 -1
View File
@@ -1,5 +1,5 @@
import type { FastifyInstance } from "fastify";
import { SESSION_TREE_CUSTOM_INSTRUCTIONS_MAX_LENGTH, type SessionBulkMutationRequest, type SessionBulkMutationRef, type SessionCleanupRequest, type SessionTreeNavigateRequest, type SessionTreeSummaryChoice } from "../../shared/apiTypes.js";
import { SESSION_TREE_CUSTOM_INSTRUCTIONS_MAX_LENGTH, SESSION_UNREAD_CATALOG_ID_MAX_LENGTH, SESSION_UNREAD_CWD_MAX_LENGTH, SESSION_UNREAD_SESSION_ID_MAX_LENGTH, type SessionBulkMutationRequest, type SessionBulkMutationRef, type SessionCleanupRequest, type SessionTreeNavigateRequest, type SessionTreeSummaryChoice, type SessionUnreadAcknowledgeRequest } from "../../shared/apiTypes.js";
import { projectBrowserMessageResponse } from "../browserMessageProjection.js";
import { normalizeRequestCwd } from "../workingDirectory.js";
import type { SessionEventHub } from "../realtime/sessionEventHub.js";
@@ -62,6 +62,35 @@ export function registerSessionRoutes(app: FastifyInstance, sessions: SessionRou
}
});
app.get(`${prefix}/sessions/unread`, async (_request, reply) => {
try {
return await sessions.unreadCatalog();
} catch (error) {
return reply.code(503).send({ error: errorMessage(error) });
}
});
app.post<{ Params: { sessionId: string }; Body: Record<string, unknown> | undefined }>(`${prefix}/sessions/:sessionId/unread/acknowledge`, async (request, reply) => {
let sessionId: string;
let acknowledgement: SessionUnreadAcknowledgeRequest;
try {
const body = requireRecord(request.body);
sessionId = requireNonEmptyBoundedString(request.params.sessionId, "sessionId", SESSION_UNREAD_SESSION_ID_MAX_LENGTH);
acknowledgement = {
cwd: normalizeRequestCwd(requireNonEmptyBoundedString(body["cwd"], "cwd", SESSION_UNREAD_CWD_MAX_LENGTH)),
catalogId: requireNonEmptyBoundedString(body["catalogId"], "catalogId", SESSION_UNREAD_CATALOG_ID_MAX_LENGTH),
throughCompletionOrder: requirePositiveSafeInteger(body["throughCompletionOrder"], "throughCompletionOrder"),
};
} catch (error) {
return reply.code(400).send({ error: errorMessage(error) });
}
try {
return await sessions.acknowledgeUnread(sessionId, acknowledgement);
} catch (error) {
return reply.code(503).send({ error: errorMessage(error) });
}
});
app.post<{ Body: SessionCleanupRequest | undefined }>(`${prefix}/sessions/cleanup/preview`, async (request, reply) => {
try {
return await sessions.cleanupPreview(normalizeSessionCleanupRequest(optionalRecord(request.body)));
@@ -509,6 +538,12 @@ function requireNonNegativeSafeInteger(value: unknown, field: string): number {
return value;
}
function requirePositiveSafeInteger(value: unknown, field: string): number {
const parsed = requireNonNegativeSafeInteger(value, field);
if (parsed === 0) throw new Error(`${field} field must be positive`);
return parsed;
}
function requireThinkingLevel(value: unknown): string {
if (typeof value !== "string" || value === "") throw new Error("level field is invalid");
return value;