feat: add temporal run engine integration

This commit is contained in:
chungyeong
2026-05-13 08:39:19 +09:00
parent 78ebd5ef78
commit aa3033771a
37 changed files with 7338 additions and 224 deletions

View File

@@ -11,6 +11,7 @@ export interface SessionAdapter {
}
export interface StartInput {
sessionId?: string;
runId: string;
roleId: string;
backend: Backend;

View File

@@ -54,6 +54,10 @@ export class FakeSessionAdapter implements SessionAdapter {
this.now = options.now ?? (() => new Date());
}
reserveSessionId(): string {
return this.sessionIdFactory();
}
async start(input: StartInput): Promise<SessionHandle> {
if (input.backend !== "fake") {
throw new DevflowError("FakeSessionAdapter only supports the fake backend", {
@@ -63,7 +67,7 @@ export class FakeSessionAdapter implements SessionAdapter {
});
}
const handle: SessionHandle = { sessionId: this.sessionIdFactory() };
const handle: SessionHandle = { sessionId: input.sessionId ?? this.sessionIdFactory() };
const record: FakeSessionRecord = {
handle,
runId: input.runId,

View File

@@ -6,7 +6,7 @@ import {
runs,
tuiSessions,
} from "@devflow/db";
import { and, eq, inArray, ne, notInArray, sql } from "drizzle-orm";
import { and, eq, inArray, notInArray, sql } from "drizzle-orm";
import type {
ProbeResult,
@@ -196,11 +196,11 @@ export class SessionManager implements SessionRuntime {
.where(
this.recoveryRunIds === undefined
? and(
ne(tuiSessions.state, "FAILED_NEEDS_HUMAN"),
notInArray(tuiSessions.state, [...nonRecoverableSessionStates]),
notInArray(runs.state, [...terminalRunStates]),
)
: and(
ne(tuiSessions.state, "FAILED_NEEDS_HUMAN"),
notInArray(tuiSessions.state, [...nonRecoverableSessionStates]),
notInArray(runs.state, [...terminalRunStates]),
inArray(tuiSessions.runId, [...this.recoveryRunIds]),
),
@@ -218,6 +218,7 @@ export class SessionManager implements SessionRuntime {
try {
const resumed = await this.resumeWithRetry(handle);
this.handles.set(resumed.sessionId, resumed);
await this.markStartupRecoverySucceeded(session, resumed);
recoveredSessionIds.push(resumed.sessionId);
} catch (error) {
await this.markRecoveryFailed(session, error);
@@ -228,6 +229,59 @@ export class SessionManager implements SessionRuntime {
return { recoveredSessionIds, failedSessionIds };
}
private async markStartupRecoverySucceeded(
session: {
id: string;
runId: string;
roleId: string;
backend: string;
recoveryAttempts: number;
state: string;
},
handle: SessionHandle,
): Promise<void> {
if (this.db === undefined || !["CREATED", "BOOTSTRAPPING"].includes(session.state)) {
return;
}
const eventRepository = new RunEventRepository(this.db);
const sessionUpdate: {
state: "READY";
lastKnownPanePid?: number;
tmuxSession?: string;
tmuxWindow?: string;
} = { state: "READY" };
if (handle.pid !== undefined) {
sessionUpdate.lastKnownPanePid = handle.pid;
}
if (handle.tmuxSession !== undefined) {
sessionUpdate.tmuxSession = handle.tmuxSession;
}
if (handle.tmuxWindow !== undefined) {
sessionUpdate.tmuxWindow = handle.tmuxWindow;
}
await this.db.transaction(async (tx) => {
await tx.update(tuiSessions).set(sessionUpdate).where(eq(tuiSessions.id, session.id));
await eventRepository.appendInTransaction(tx, {
runId: session.runId,
type: "session.created",
payload: { sessionId: session.id, roleId: session.roleId, backend: session.backend },
idempotencyKey: `session.created:${session.id}`,
});
await eventRepository.appendInTransaction(tx, {
runId: session.runId,
type: "session.ready",
payload: {
sessionId: session.id,
roleId: session.roleId,
recoveryAttempts: session.recoveryAttempts,
},
idempotencyKey: `session.ready:${session.id}:${session.recoveryAttempts}`,
});
});
}
private async markRecoveryFailed(
session: {
id: string;
@@ -380,6 +434,7 @@ export class SessionManager implements SessionRuntime {
}
const terminalRunStates = ["completed", "failed", "aborted"] as const;
const nonRecoverableSessionStates = ["FAILED_NEEDS_HUMAN"] as const;
function isTerminalRunState(state: string): state is (typeof terminalRunStates)[number] {
return terminalRunStates.includes(state as (typeof terminalRunStates)[number]);