evOo-local/packages/adapters/src/job-reconciler.ts
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

413 lines
14 KiB
TypeScript

import {
type JobPublisher,
messagingDeliverJob,
routineWakeupJob,
runContinueJob,
} from "@rakazo/adapter-kit";
import type { MessageBlock } from "@rakazo/contracts";
import type { Pool, PrismaClient, ThreadEvents } from "@rakazo/db";
import { getLogger } from "@rakazo/logging";
import type { PoolClient } from "pg";
import { returnBotMessageOutcome } from "./bot-messages.js";
import { scheduleComputerControlExpiry } from "./computer-control.js";
import { isUserProgressClientNonce } from "./user-progress.js";
const DEFAULT_INTERVAL_MS = 30_000;
const DEFAULT_BATCH_SIZE = 100;
const ROUTINE_LOOKAHEAD_MS = 60_000;
const CONTROL_LOOKAHEAD_MS = 60_000;
// Two keys give Rakazo's lock a namespace without relying on a hash that might collide
// with an application using the one-key advisory-lock API.
const RECONCILIATION_LOCK_NAMESPACE = 1_380_019_075;
const RECONCILIATION_LOCK_ID = 1;
type Cursor = { at: Date; id: string };
type ControlCursor = { at: Date | null; id: string };
export interface ReconciliationLeadership {
tryAcquire(): Promise<boolean>;
release(): Promise<void>;
}
/**
* Holds a session advisory lock for the lifetime of the elected reconciler. Followers
* retry on every reconciliation interval, so a disconnected or stopped leader is
* replaced without coordinating through application state.
*/
export function createPostgresReconciliationLeadership(
pool: Pick<Pool, "connect">,
options: { lockId?: number } = {},
): ReconciliationLeadership {
const lockId = options.lockId ?? RECONCILIATION_LOCK_ID;
let leaderClient: PoolClient | undefined;
let leaderErrorListener: (() => void) | undefined;
const loseClient = (client: PoolClient) => {
if (leaderClient !== client) return;
leaderClient = undefined;
leaderErrorListener = undefined;
client.release(true);
};
return {
async tryAcquire() {
if (leaderClient) return true;
const candidate = await pool.connect();
try {
const result = await candidate.query<{ acquired: boolean }>(
"SELECT pg_try_advisory_lock($1::integer, $2::integer) AS acquired",
[RECONCILIATION_LOCK_NAMESPACE, lockId],
);
if (!result.rows[0]?.acquired) {
candidate.release();
return false;
}
leaderClient = candidate;
leaderErrorListener = () => loseClient(candidate);
candidate.once("error", leaderErrorListener);
return true;
} catch (error) {
candidate.release(true);
throw error;
}
},
async release() {
const client = leaderClient;
if (!client) return;
leaderClient = undefined;
if (leaderErrorListener) client.removeListener("error", leaderErrorListener);
leaderErrorListener = undefined;
let destroy = false;
try {
const result = await client.query<{ released: boolean }>(
"SELECT pg_advisory_unlock($1::integer, $2::integer) AS released",
[RECONCILIATION_LOCK_NAMESPACE, lockId],
);
destroy = result.rows[0]?.released !== true;
} catch {
// Never return a connection with an uncertain session lock to the pool.
destroy = true;
} finally {
client.release(destroy);
}
},
};
}
export function createJobReconciler(
deps: {
prisma: PrismaClient;
jobs: JobPublisher;
events?: ThreadEvents;
leadership?: ReconciliationLeadership;
reconcileComputerUpdates?: () => Promise<void>;
reconcileCloudAgents?: () => Promise<void>;
},
options: { intervalMs?: number; batchSize?: number } = {},
) {
const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
let timer: ReturnType<typeof setInterval> | undefined;
let reconciling: Promise<void> | undefined;
let runCursor: Cursor | undefined;
let routineCursor: Cursor | undefined;
let controlCursor: ControlCursor | undefined;
let controlScanDeadline: Date | undefined;
const reconcileOnce = async () => {
if (reconciling) return reconciling;
reconciling = (async () => {
if (deps.leadership && !(await deps.leadership.tryAcquire())) return;
const auxiliary = await Promise.allSettled(
[deps.reconcileCloudAgents, deps.reconcileComputerUpdates].map(async (reconcile) =>
reconcile?.(),
),
);
for (const result of auxiliary) {
if (result.status === "rejected")
getLogger().error("auxiliary reconciliation", result.reason);
}
const now = new Date();
controlScanDeadline ??= new Date(now.getTime() + CONTROL_LOOKAHEAD_MS);
const runCursorFilter = runCursor
? {
OR: [
{ updatedAt: { gt: runCursor.at } },
{ updatedAt: runCursor.at, id: { gt: runCursor.id } },
],
}
: undefined;
const routineCursorFilter = routineCursor
? {
OR: [
{ nextRunAt: { gt: routineCursor.at } },
{ nextRunAt: routineCursor.at, id: { gt: routineCursor.id } },
],
}
: undefined;
const controlCursorFilter = controlCursor
? controlCursor.at
? {
OR: [
{ controlLeaseExpiresAt: { gt: controlCursor.at } },
{
controlLeaseExpiresAt: controlCursor.at,
id: { gt: controlCursor.id },
},
{ controlLeaseExpiresAt: null },
],
}
: { controlLeaseExpiresAt: null, id: { gt: controlCursor.id } }
: undefined;
const [runs, routines, controls, dueOutbound, unmirroredMessagingRuns] = await Promise.all([
deps.prisma.run.findMany({
where: {
AND: [
{
OR: [
{ status: "queued" },
{
status: { in: ["leased", "running"] },
leaseExpiresAt: { lte: now },
},
],
},
...(runCursorFilter ? [runCursorFilter] : []),
],
},
orderBy: [{ updatedAt: "asc" }, { id: "asc" }],
take: batchSize,
select: { id: true, updatedAt: true },
}),
deps.prisma.routine.findMany({
where: {
AND: [
{
active: true,
nextRunAt: { lte: new Date(now.getTime() + ROUTINE_LOOKAHEAD_MS) },
},
...(routineCursorFilter ? [routineCursorFilter] : []),
],
},
orderBy: [{ nextRunAt: "asc" }, { id: "asc" }],
take: batchSize,
select: { id: true, nextRunAt: true },
}),
deps.prisma.computer.findMany({
where: {
AND: [
{ controlLeaseId: { not: null } },
{
OR: [
{ controlLeaseExpiresAt: null },
{
controlLeaseExpiresAt: {
lte: controlScanDeadline,
},
},
],
},
...(controlCursorFilter ? [controlCursorFilter] : []),
],
},
orderBy: [{ controlLeaseExpiresAt: "asc" }, { id: "asc" }],
take: batchSize,
select: {
id: true,
controlBotId: true,
controlLeaseId: true,
controlLeaseExpiresAt: true,
},
}),
deps.prisma.messagingOutbound.findFirst({
where: {
status: "pending",
OR: [{ nextAttemptAt: null }, { nextAttemptAt: { lte: now } }],
},
select: { id: true },
}),
deps.prisma.run.findMany({
where: { trigger: "messaging", status: "completed", messagingMirroredAt: null },
orderBy: [{ updatedAt: "asc" }, { id: "asc" }],
take: batchSize,
select: { id: true },
}),
]);
const events = deps.events;
if (events) {
const outcomes = await deps.prisma.run.findMany({
where: {
trigger: "bot_message",
status: { in: ["completed", "failed"] },
botOutcomeReturnedAt: null,
},
orderBy: [{ updatedAt: "asc" }, { id: "asc" }],
take: batchSize,
select: {
id: true,
spaceId: true,
threadId: true,
botId: true,
userId: true,
sourceMessageId: true,
status: true,
error: true,
bot: { select: { name: true } },
},
});
await Promise.all(
outcomes.map(async (run) => {
const transcript =
run.status === "failed"
? { text: "", progressOnly: false }
: await botRunOutcomeText(deps.prisma, run.id);
const text =
run.status === "failed"
? `Could not complete the delegated request: ${run.error ?? "unknown error"}`
: transcript.text ||
"The delegated bot completed its turn without a written summary.";
// Same stable delivery key as the executor path (auto-outcome:<runId>), so a
// concurrent or earlier return is replayed instead of double-posted. Progress-only
// transcripts (all mid-turn user-progress messages) return as status.
const intent =
run.status === "failed" || !transcript.text.trim() || transcript.progressOnly
? "status"
: ("result" as const);
const returned = await returnBotMessageOutcome(
{ prisma: deps.prisma, jobs: deps.jobs, events },
run,
{ id: run.botId, name: run.bot.name },
text,
intent,
).catch((error) => {
getLogger().error("bot message outcome reconciliation", error);
return false;
});
if (!returned) {
await deps.prisma.run.updateMany({
where: { id: run.id, botOutcomeReturnedAt: null },
data: { updatedAt: new Date() },
});
}
}),
);
}
await Promise.all([
...runs.map((run) => deps.jobs.enqueue(runContinueJob(run.id))),
...routines.flatMap((routine) =>
routine.nextRunAt
? [deps.jobs.enqueue(routineWakeupJob(routine.id, routine.nextRunAt))]
: [],
),
...controls.flatMap((computer) =>
computer.controlLeaseId
? [
scheduleComputerControlExpiry(
deps.jobs,
computer.id,
computer.controlLeaseId,
computer.controlLeaseExpiresAt ?? now,
),
]
: [],
),
...(dueOutbound ? [deps.jobs.enqueue(messagingDeliverJob())] : []),
...unmirroredMessagingRuns.map((run) => deps.jobs.enqueue(messagingDeliverJob(run.id))),
]);
const lastRun = runs.at(-1);
runCursor =
runs.length === batchSize && lastRun
? { at: lastRun.updatedAt, id: lastRun.id }
: undefined;
const lastRoutine = routines.at(-1);
routineCursor =
routines.length === batchSize && lastRoutine?.nextRunAt
? { at: lastRoutine.nextRunAt, id: lastRoutine.id }
: undefined;
const lastControl = controls.at(-1);
controlCursor =
controls.length === batchSize && lastControl
? { at: lastControl.controlLeaseExpiresAt, id: lastControl.id }
: undefined;
if (!controlCursor) controlScanDeadline = undefined;
})().finally(() => {
reconciling = undefined;
});
return reconciling;
};
const reconcileSafely = () => {
void reconcileOnce().catch((error) =>
getLogger().error("background job reconciliation", error),
);
};
return {
reconcileOnce,
start() {
if (timer) return;
reconcileSafely();
timer = setInterval(reconcileSafely, intervalMs);
timer.unref?.();
},
async stop() {
if (timer) clearInterval(timer);
timer = undefined;
await reconciling?.catch(() => undefined);
await deps.leadership?.release();
},
};
}
/** Prefer the full bot transcript for a run so interim progress is not mistaken for the sole result. */
async function botRunOutcomeText(
prisma: {
message: {
findMany: (args: {
where: { runId: string; role: "bot" };
orderBy: { seq: "asc" };
select: { blocks: true; clientNonce: true };
}) => Promise<Array<{ blocks: unknown; clientNonce: string | null }>>;
};
},
runId: string,
): Promise<{ text: string; progressOnly: boolean }> {
const messages = await prisma.message.findMany({
where: { runId, role: "bot" },
orderBy: { seq: "asc" },
select: { blocks: true, clientNonce: true },
});
const progressParts: string[] = [];
const finalParts: string[] = [];
for (const message of messages) {
const text = messageText(message.blocks);
if (!text) continue;
if (isUserProgressClientNonce(message.clientNonce)) progressParts.push(text);
else finalParts.push(text);
}
// Prefer the latest non-progress reply when present so earlier untagged mid-run
// publishes (for example pre-takeover narration) do not contaminate the result.
// Progress-only turns still join progress beats as status.
if (finalParts.length > 0) {
return { text: finalParts[finalParts.length - 1]!, progressOnly: false };
}
return { text: progressParts.join("\n\n"), progressOnly: progressParts.length > 0 };
}
function messageText(blocks: unknown): string {
if (!Array.isArray(blocks)) return "";
return (blocks as MessageBlock[])
.filter((block): block is Extract<MessageBlock, { kind: "text" }> => block.kind === "text")
.map((block) => block.text)
.join("")
.trim();
}