spb/khaelor Public
KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.
TypeScript 82.9%
HTML 14.9%
CSS 1.1%
JavaScript 0.7%
1/**2 * KHAELOR3 * File: src/cli/replay.ts4 * Description: /replay driver — re-run a session's user turns with another model, optionally sandboxed in a worktree (v2 design §2).5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import type { ResolvedConfig } from "../config/index.js";11import {12 SessionLog,13 buildUsageTotals,14 extractUserTurns,15 summarizeSessionRun,16 writeSessionMeta,17} from "../session/index.js";18import type { SessionRunSummary } from "../session/index.js";19import { createWorktree, removeWorktree } from "../tasks/index.js";20import type { ExecFn } from "../tasks/index.js";21import { assembleEngine, openSessionContext } from "./engine.js";22import type { FileLogger } from "./logger.js";23import { projectHash } from "./sessions.js";2425export interface ReplayOptions {26 config: ResolvedConfig;27 cwd: string;28 sourceSessionId: string;29 sessionsDir: string;30 logger: FileLogger;31 /** Replay inside a throwaway worktree so tool calls have no side effects on the working copy (v2 §2/§6). */32 sandbox?: { exec: ExecFn };33 onProgress?: (message: string) => void;34}3536export interface ReplayResult {37 newSessionId: string;38 turnsReplayed: number;39 summary: SessionRunSummary;40}4142/**43 * Replay = re-run the *user* turns sequentially in a fresh session. Tool44 * calls are re-executed, not replayed from the log — combine with sandbox45 * to avoid side effects. The result is /sdiff-ready (v2 §2).46 */47export async function replaySession(options: ReplayOptions): Promise<ReplayResult> {48 const hash = projectHash(options.cwd);49 const source = await SessionLog.open({50 projectHash: hash,51 sessionId: options.sourceSessionId,52 sessionsDir: options.sessionsDir,53 });54 const turns = extractUserTurns(source.replayedEvents);55 if (turns.length === 0) {56 throw new Error(`Session ${options.sourceSessionId} has no user turns to replay.`);57 }5859 let runCwd = options.cwd;60 let worktree: Awaited<ReturnType<typeof createWorktree>> | null = null;61 if (options.sandbox !== undefined) {62 worktree = await createWorktree(63 options.sandbox.exec,64 options.cwd,65 `replay-${Date.now().toString(36)}`,66 );67 runCwd = worktree.path;68 }6970 try {71 const context = await openSessionContext({72 cwd: runCwd,73 sessionsDir: options.sessionsDir,74 logger: options.logger,75 });76 const engine = await assembleEngine({77 config: options.config,78 cwd: runCwd,79 context,80 logger: options.logger,81 });82 try {83 await writeSessionMeta(options.sessionsDir, context.hash, context.sessionId, {84 parent: null,85 forkPoint: null,86 replayOf: options.sourceSessionId,87 createdAt: Date.now(),88 });89 await engine.captureBaseline();90 let replayed = 0;91 for (const turn of turns) {92 replayed += 1;93 options.onProgress?.(`replaying turn ${replayed}/${turns.length}`);94 engine.session.publishDurable({95 type: "user.message-created",96 payload: { text: turn, mentions: [] },97 });98 const outcome = await engine.runTurn();99 if (outcome.kind === "failed") break; // honest stop — the summary shows it100 }101 const events = engine.session.events();102 // Touch usage so the totals are computed once even if the caller ignores them.103 buildUsageTotals(events);104 return {105 newSessionId: context.sessionId,106 turnsReplayed: replayed,107 summary: summarizeSessionRun(context.sessionId, events),108 };109 } finally {110 await engine.shutdown();111 }112 } finally {113 if (worktree !== null && options.sandbox !== undefined) {114 await removeWorktree(options.sandbox.exec, options.cwd, worktree, { deleteBranch: true });115 }116 }117}118