evOo-local/packages/adapters/src/executor-computer.ts
Ingo Wagner a570e9e71b feat: complete onboarding redesign + major file splits
Onboarding: emoji-first flow with shape/color selection, user preferences,
language setup (9 locales), and bot creation with avatarValue persistence.

executor.ts: split 4928→4159 lines into 8 modules (completion, computer,
effects, model, utils, tools, attachments, types).

pi-runtime.ts: split 1726→561 lines into 2 modules (model-config, tool-utils).

E2E: new onboarding-emoji-first tests + updated completeOnboarding helper.
2026-09-21 22:52:04 +00:00

232 lines
6.6 KiB
TypeScript

import type {
NotificationMessage,
NotificationProvider,
} from "@rakazo/adapter-kit";
import { runContinueJob } from "@rakazo/adapter-kit";
import type { MessageBlock } from "@rakazo/contracts";
import type { PrismaClient, Prisma } from "@rakazo/db";
import {
appendEventInTransaction,
createThreadMessageInTransaction,
} from "@rakazo/db";
import { redactSecrets } from "@rakazo/core";
import { getLogger } from "@rakazo/logging";
import type { ExecutorDeps } from "./executor.js";
import { withComputerScreenAvailability } from "./computer-screens.js";
import {
TAKEOVER_RESUME_CHECKPOINTS,
type TakeoverResumeCheckpoint,
} from "./takeover-resume.js";
import { sanitizeTextBlocks } from "./prose-handoff-recovery.js";
// Helper used by publishMessage — also exported so executor.ts can call it directly
export async function persistMessageInTransaction(
tx: Prisma.TransactionClient,
run: { id: string; spaceId: string; threadId: string; botId: string },
role: "user" | "bot" | "system",
blocks: MessageBlock[],
markUnread?: boolean,
clientNonce?: string,
) {
const message = await createThreadMessageInTransaction(tx, {
threadId: run.threadId,
role,
blocks,
botId: run.botId,
runId: run.id,
markUnread,
clientNonce,
});
const event = await appendEventInTransaction(tx, {
spaceId: run.spaceId,
threadId: run.threadId,
botId: run.botId,
type: "thread.message.created",
runId: run.id,
payload: { messageId: message.id, role, blocks },
});
return { message, eventSeq: event.seq };
}
export async function computerScreenToolResult(
work: () => Promise<unknown>,
finish?: (result: unknown) => Promise<unknown>,
) {
const result = await withComputerScreenAvailability(work);
return finish ? finish(result) : result;
}
export async function runNotificationsEnabled(
prisma: PrismaClient,
run: { spaceId: string; userId: string; botId: string; threadId: string },
): Promise<boolean> {
const source = await prisma.run.findFirst({
where: {
botId: run.botId,
threadId: run.threadId,
spaceId: run.spaceId,
userId: run.userId,
},
select: {
bot: { select: { notifyOnFinish: true } },
thread: { select: { groupId: true } },
},
});
return Boolean(source && (source.thread.groupId || source.bot.notifyOnFinish));
}
export async function notifyRun(
deps: ExecutorDeps,
run: { spaceId: string; userId: string; botId: string; threadId: string },
message: NotificationMessage,
) {
if (!deps.notifications) return;
const enabled = await runNotificationsEnabled(deps.prisma, run).catch((error) => {
getLogger().error("notification preference lookup", error);
return false;
});
if (!enabled) return;
await deps.notifications
.send(message, {
operationId: "notify",
traceId: run.botId,
spaceId: run.spaceId,
userId: run.userId,
botId: run.botId,
signal: new AbortController().signal,
})
.catch((error) => {
getLogger().error("run notification", error);
});
}
export async function renewRunLease(
deps: ExecutorDeps,
runId: string,
workerId: string,
fence: number,
): Promise<boolean> {
const renewed = await deps.prisma.run.updateMany({
where: { id: runId, status: "running", leaseOwner: workerId, leaseFence: fence },
data: { leaseExpiresAt: new Date(Date.now() + 5 * 60_000) },
});
return renewed.count === 1;
}
export function computerRetryDelay(fence: number): number {
return Math.min(10_000, 250 * 2 ** Math.min(Math.max(fence - 1, 0), 5));
}
export function computerRunRequeueData(
resumeCheckpoint: TakeoverResumeCheckpoint | null,
error: string | null = null,
heldForTakeover = false,
) {
return {
status:
heldForTakeover && !resumeCheckpoint ? ("waiting_takeover" as const) : ("queued" as const),
error,
leaseOwner: null,
leaseExpiresAt: null,
checkpoint: resumeCheckpoint,
};
}
export async function writeComputerRunRequeue(
deps: ExecutorDeps,
runId: string,
workerId: string,
fence: number,
resumeCheckpoint: TakeoverResumeCheckpoint | null,
heldForTakeover = false,
error: string | null = null,
): Promise<boolean> {
const whereLease = {
id: runId,
status: "running" as const,
leaseOwner: workerId,
leaseFence: fence,
};
const releasedHold = {
status: "queued" as const,
error,
leaseOwner: null,
leaseExpiresAt: null,
};
const preserve = await deps.prisma.run.updateMany({
where: {
...whereLease,
checkpoint: { in: [...TAKEOVER_RESUME_CHECKPOINTS] },
},
data: releasedHold,
});
if (preserve.count === 1) return true;
const planned = await deps.prisma.run.updateMany({
where: { ...whereLease, checkpoint: null },
data: computerRunRequeueData(resumeCheckpoint, error, heldForTakeover),
});
if (planned.count === 1) return true;
const retried = await deps.prisma.run.updateMany({
where: {
...whereLease,
checkpoint: { in: [...TAKEOVER_RESUME_CHECKPOINTS] },
},
data: releasedHold,
});
return retried.count === 1;
}
export async function requeueComputerRun(
deps: ExecutorDeps,
runId: string,
workerId: string,
fence: number,
resumeCheckpoint: TakeoverResumeCheckpoint | null,
heldForTakeover = false,
): Promise<void> {
const released = await writeComputerRunRequeue(
deps,
runId,
workerId,
fence,
resumeCheckpoint,
heldForTakeover,
);
if (!released) return;
await deps.jobs.enqueue({
...runContinueJob(runId),
availableAt: new Date(Date.now() + computerRetryDelay(fence)),
});
}
export function redactBlocks(blocks: MessageBlock[], secrets: string[]): MessageBlock[] {
return sanitizeTextBlocks(
blocks.map((block) => {
if (block.kind === "text") {
return { kind: "text" as const, text: redactSecrets(block.text, secrets) };
}
if (block.kind === "bot_message_sent" || block.kind === "bot_message_received") {
return { ...block, text: redactSecrets(block.text, secrets) };
}
return block;
}),
);
}
export async function publishMessage(
deps: ExecutorDeps,
run: { id: string; spaceId: string; threadId: string; botId: string },
role: "user" | "bot" | "system",
blocks: MessageBlock[],
markUnread?: boolean,
clientNonce?: string,
) {
const committed = await deps.prisma.$transaction((tx) =>
persistMessageInTransaction(tx, run, role, blocks, markUnread, clientNonce),
);
await deps.events.notify(run.threadId, committed.eventSeq).catch((error) => {
getLogger().error("thread message realtime notification", error);
});
return committed.message;
}