SPB Git

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%
3.7 KB · 118 lines typescript
Raw Blame History
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