import { OBSERVER_STATUSES, type ObservedThread, type ObserverOutput, type ObserverStatus } from "./types.ts";
const MAX_THREADS = 64;
const MAX_REQUEST_LENGTH = 2_000;
const STATUS_SET = new Set<string>(OBSERVER_STATUSES);
const threadProperties = {
canonicalRequest: { type: "string" },
status: { type: "string", enum: [...OBSERVER_STATUSES] },
} as const;
export const OBSERVER_JSON_SCHEMA = {
type: "object",
additionalProperties: false,
properties: {
existing: {
type: "array",
items: {
type: "object",
additionalProperties: false,
properties: { id: { type: "string" }, ...threadProperties },
required: ["id", "canonicalRequest", "status"],
},
},
additions: {
type: "array",
items: {
type: "object",
additionalProperties: false,
properties: threadProperties,
required: ["canonicalRequest", "status"],
},
},
},
required: ["existing", "additions"],
} as const;
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
function parseThread(value: unknown, requireId: boolean): ObservedThread | undefined {
if (!isRecord(value)) return undefined;
const keys = Object.keys(value);
const allowed = requireId ? new Set(["id", "canonicalRequest", "status"]) : new Set(["canonicalRequest", "status"]);
if (keys.some((key) => !allowed.has(key))) return undefined;
if (
typeof value.canonicalRequest !== "string" ||
value.canonicalRequest.trim().length === 0 ||
value.canonicalRequest.length > MAX_REQUEST_LENGTH ||
typeof value.status !== "string" ||
!STATUS_SET.has(value.status) ||
(requireId && typeof value.id !== "string")
) {
return undefined;
}
return {
...(requireId ? { id: value.id as string } : {}),
canonicalRequest: value.canonicalRequest.trim(),
status: value.status as ObserverStatus,
};
}
export function parseObserverOutput(
raw: string,
knownThreads: ReadonlyMap<string, string>,
allowUserChanges: boolean,
): ObserverOutput | undefined {
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
return undefined;
}
if (!isRecord(parsed) || !Array.isArray(parsed.existing) || !Array.isArray(parsed.additions)) return undefined;
if (parsed.existing.length > MAX_THREADS || parsed.additions.length > MAX_THREADS) return undefined;
if (!allowUserChanges && parsed.additions.length > 0) return undefined;
const existing = parsed.existing.map((value) => parseThread(value, true));
const additions = parsed.additions.map((value) => parseThread(value, false));
if (existing.some((thread) => thread === undefined) || additions.some((thread) => thread === undefined)) {
return undefined;
}
const ids = existing.map((thread) => thread!.id!);
if (new Set(ids).size !== ids.length || ids.some((id) => !knownThreads.has(id))) return undefined;
if (ids.length !== knownThreads.size || [...knownThreads.keys()].some((id) => !ids.includes(id))) return undefined;
if (
!allowUserChanges &&
(existing as ObservedThread[]).some(
(thread) => thread.canonicalRequest !== knownThreads.get(thread.id!),
)
) {
return undefined;
}
return {
existing: existing as ObservedThread[],
additions: additions as ObservedThread[],
};
}