/** * Session logging for DroidClaw. * Writes incremental .partial.json after each step (crash-safe), * and a final .json summary at session end. */ import { mkdirSync, writeFileSync } from "fs"; import { join } from "path"; import type { ActionDecision } from "./actions.js"; export interface StepLog { step: number; timestamp: string; foregroundApp: string | null; elementCount: number; screenChanged: boolean; llmDecision: { action: string; reason?: string; coordinates?: [number, number]; text?: string; think?: string; plan?: string[]; planProgress?: string; }; actionResult: { success: boolean; message: string; }; llmLatencyMs: number; actionLatencyMs: number; } export interface SessionSummary { sessionId: string; goal: string; provider: string; model: string; startTime: string; endTime: string; totalSteps: number; successCount: number; failCount: number; completed: boolean; steps: StepLog[]; } export class SessionLogger { private sessionId: string; private logDir: string; private steps: StepLog[] = []; private goal: string; private provider: string; private model: string; private startTime: string; constructor(logDir: string, goal: string, provider: string, model: string) { this.sessionId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; this.logDir = logDir; this.goal = goal; this.provider = provider; this.model = model; this.startTime = new Date().toISOString(); mkdirSync(this.logDir, { recursive: true }); } logStep( step: number, foregroundApp: string | null, elementCount: number, screenChanged: boolean, decision: ActionDecision, result: { success: boolean; message: string }, llmLatencyMs: number, actionLatencyMs: number ): void { const entry: StepLog = { step, timestamp: new Date().toISOString(), foregroundApp, elementCount, screenChanged, llmDecision: { action: decision.action, reason: decision.reason, coordinates: decision.coordinates, text: decision.text, think: decision.think, plan: decision.plan, planProgress: decision.planProgress, }, actionResult: { success: result.success, message: result.message, }, llmLatencyMs, actionLatencyMs, }; this.steps.push(entry); // Write partial file after each step (crash-safe) const partialPath = join(this.logDir, `${this.sessionId}.partial.json`); writeFileSync(partialPath, JSON.stringify(this.buildSummary(false), null, 2)); } finalize(completed: boolean): void { const summary = this.buildSummary(completed); const finalPath = join(this.logDir, `${this.sessionId}.json`); writeFileSync(finalPath, JSON.stringify(summary, null, 2)); console.log(`Session log saved: ${finalPath}`); } private buildSummary(completed: boolean): SessionSummary { return { sessionId: this.sessionId, goal: this.goal, provider: this.provider, model: this.model, startTime: this.startTime, endTime: new Date().toISOString(), totalSteps: this.steps.length, successCount: this.steps.filter((s) => s.actionResult.success).length, failCount: this.steps.filter((s) => !s.actionResult.success).length, completed, steps: this.steps, }; } }