src/store/storeActionLog.ts

// Extracted Store Methods import type { Database } from "bun:sqlite";

import { clamp, nowIso } from "../utils.ts"; import { ACTION_LOG_RETENTION_DAYS_MIN, ACTION_LOG_RETENTION_DAYS_MAX, ACTION_LOG_MAX_ROWS_RUNTIME_MIN, ACTION_LOG_MAX_ROWS_MAX } from "./store.ts"; import { safeJsonParse } from "../normalization/valueParsers.ts"; import { shouldTrackResponseTriggerKind, normalizeResponseTriggerMessageIds } from "./responseTriggers.ts";

// Insert/query limits to keep action-log payloads and scans bounded. const ACTION_LOG_CONTENT_MAX_CHARS = 2000; const DEFAULT_RECENT_ACTIONS_LIMIT = 200; const MAX_RECENT_ACTIONS_LIMIT = 1000; const DEFAULT_RECENT_REFLECTIONS_LIMIT = 20; const MAX_RECENT_REFLECTIONS_LIMIT = 100;

// Reflection scan multipliers oversample recent actions before filtering by kind. const MIN_REFLECTION_SCAN_LIMIT = 60; const REFLECTION_SCAN_LIMIT_MULTIPLIER = 6;

// Browser-session query limits and oversampling behavior. const DEFAULT_RECENT_BROWSER_SESSIONS_LIMIT = 50; const MAX_RECENT_BROWSER_SESSIONS_LIMIT = 200; const BROWSER_SESSION_SCAN_MULTIPLIER = 20;

// Hour-to-millisecond conversion for retention cutoff arithmetic. const HOURS_TO_MS = 60 * 60 * 1000;

interface ActionLogEntry { kind: string; guildId?: string | null; channelId?: string | null; messageId?: string | null; userId?: string | null; content?: string | null; metadata?: unknown; usdCost?: number | null; }

interface ActionLogStore { db: Database; actionWritesSincePrune: number; actionLogPruneEveryWrites: number; actionLogRetentionDays: number; actionLogMaxRows: number; onActionLogged?: ((action: ActionLogEntry & { createdAt: string }) => void) | null; pruneActionLog(args?: { now?: string; maxAgeDays?: number; maxRows?: number }): { deletedActions: number; deletedResponseTriggers: number; }; maybePruneActionLog(args?: { now?: string }): void; indexResponseTriggersForAction(args: { actionId: number; kind: string; metadata?: unknown; createdAt?: string; }): void; }

interface ActionIdRow { id: number; }

interface ActionCountRow { count: number; }

interface ActionTimeRow { created_at: string; }

interface ActionPresenceRow { found: number; created_at?: string; run_id?: string | null; }

interface ReflectionCheckpointRow { found: number; }

interface ActionLogRow { id: number; created_at: string; guild_id: string | null; channel_id: string | null; message_id: string | null; user_id: string | null; kind: string; content: string | null; metadata: string | null; usd_cost: number; }

export function maybePruneActionLog(store: ActionLogStore, { now = nowIso() } = {}) { store.actionWritesSincePrune += 1; if (store.actionWritesSincePrune < store.actionLogPruneEveryWrites) return; store.actionWritesSincePrune = 0; store.pruneActionLog({ now }); }

export function pruneActionLog(store: ActionLogStore, { now = nowIso(), maxAgeDays = store.actionLogRetentionDays, maxRows = store.actionLogMaxRows } = {}) { const nowText = String(now || nowIso()); const nowMs = Date.parse(nowText); const referenceMs = Number.isFinite(nowMs) ? nowMs : Date.now(); const boundedMaxAgeDays = clamp( Math.floor(Number(maxAgeDays) || store.actionLogRetentionDays), ACTION_LOG_RETENTION_DAYS_MIN, ACTION_LOG_RETENTION_DAYS_MAX ); const boundedMaxRows = clamp( Math.floor(Number(maxRows) || store.actionLogMaxRows), ACTION_LOG_MAX_ROWS_RUNTIME_MIN, ACTION_LOG_MAX_ROWS_MAX ); const cutoffIso = new Date(referenceMs - boundedMaxAgeDays * 24 * HOURS_TO_MS).toISOString();

let deletedActions = Number( store.db .prepare( DELETE FROM actions WHERE created_at < ? ) .run(cutoffIso)?.changes || 0 );

const oldestKeptRow = store.db .prepare<ActionIdRow, [number]>( SELECT id FROM actions ORDER BY id DESC LIMIT 1 OFFSET ? ) .get(Math.max(0, boundedMaxRows - 1)); const oldestKeptId = Number(oldestKeptRow?.id || 0); if (Number.isInteger(oldestKeptId) && oldestKeptId > 0) { deletedActions += Number( store.db .prepare( DELETE FROM actions WHERE id < ? ) .run(oldestKeptId)?.changes || 0 ); }

const deletedResponseTriggers = Number( store.db .prepare( DELETE FROM response_triggers WHERE created_at < ? OR NOT EXISTS ( SELECT 1 FROM actions WHERE actions.id = response_triggers.action_id ) ) .run(cutoffIso)?.changes || 0 );

return { deletedActions, deletedResponseTriggers }; }

export function logAction(store: ActionLogStore, action: ActionLogEntry) { const metadata = action.metadata ? JSON.stringify(action.metadata) : null; const createdAt = nowIso(); const actionKind = String(action.kind);

const result = store.db .prepare( INSERT INTO actions( created_at, guild_id, channel_id, message_id, user_id, kind, content, metadata, usd_cost ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ) .run( createdAt, action.guildId ? String(action.guildId) : null, action.channelId ? String(action.channelId) : null, action.messageId ? String(action.messageId) : null, action.userId ? String(action.userId) : null, actionKind, action.content ? String(action.content).slice(0, ACTION_LOG_CONTENT_MAX_CHARS) : null, metadata, Number(action.usdCost) || 0 );

store.indexResponseTriggersForAction({ actionId: Number(result?.lastInsertRowid || 0), kind: actionKind, metadata: action.metadata, createdAt }); try { store.maybePruneActionLog({ now: createdAt }); } catch { // maintenance must never break action writes }

if (store.onActionLogged) { const listener = store.onActionLogged; const loggedAction = { ...action, kind: actionKind, createdAt }; queueMicrotask(() => { try { listener(loggedAction); } catch { // listener must never break store writes } }); } }

export function countActionsSince(store: ActionLogStore, kind, sinceIso) { const row = store.db .prepare<ActionCountRow, [string, string]>("SELECT COUNT(*) AS count FROM actions WHERE kind = ? AND created_at >= ?") .get(String(kind), String(sinceIso)); return Number(row?.count ?? 0); }

export function getLastActionTime(store: ActionLogStore, kind) { const row = store.db .prepare<ActionTimeRow, [string]>( SELECT created_at FROM actions WHERE kind = ? ORDER BY created_at DESC LIMIT 1 ) .get(String(kind));

return row?.created_at ?? null; }

export function getRecentActions( store: ActionLogStore, limit = DEFAULT_RECENT_ACTIONS_LIMIT, opts: { kinds?: string[]; sinceIso?: string | null; guildId?: string | null } = {} ) { const parsedLimit = Number(limit); const boundedLimit = clamp( Number.isFinite(parsedLimit) ? Math.floor(parsedLimit) : DEFAULT_RECENT_ACTIONS_LIMIT, 1, MAX_RECENT_ACTIONS_LIMIT ); const normalizedKinds = [...new Set( (Array.isArray(opts?.kinds) ? opts.kinds : []) .map((value) => String(value || "").trim()) .filter(Boolean) )]; const normalizedSinceIso = String(opts?.sinceIso || "").trim(); const normalizedGuildId = String(opts?.guildId || "").trim(); const conditions: string[] = []; const params: Array<string | number> = [];

if (normalizedKinds.length) { conditions.push(kind IN (${normalizedKinds.map(() => "?").join(", ")})); params.push(...normalizedKinds); } if (normalizedSinceIso) { conditions.push("created_at >= ?"); params.push(normalizedSinceIso); } if (normalizedGuildId) { conditions.push("guild_id = ?"); params.push(normalizedGuildId); }

const whereClause = conditions.length ? WHERE ${conditions.join(" AND ")} : ""; const rows = store.db .prepare<ActionLogRow, Array<string | number>>( SELECT id, created_at, guild_id, channel_id, message_id, user_id, kind, content, metadata, usd_cost FROM actions ${whereClause} ORDER BY created_at DESC LIMIT ? ) .all(...params, boundedLimit);

return rows.map((row) => ({ ...row, metadata: safeJsonParse(row.metadata, null) })); }

export function getRecentMemoryReflections( store: ActionLogStore, limit = DEFAULT_RECENT_REFLECTIONS_LIMIT, opts: { guildId?: string | null } = {} ) { const parsedLimit = Number(limit); const boundedLimit = clamp( Number.isFinite(parsedLimit) ? Math.floor(parsedLimit) : DEFAULT_RECENT_REFLECTIONS_LIMIT, 1, MAX_RECENT_REFLECTIONS_LIMIT ); const normalizedGuildId = String(opts?.guildId || "").trim(); const params: Array<string | number> = []; const conditions = ["kind IN ('memory_reflection_start', 'memory_reflection_complete', 'memory_reflection_error')"]; if (normalizedGuildId) { conditions.push("guild_id = ?"); params.push(normalizedGuildId); } params.push(Math.max(MIN_REFLECTION_SCAN_LIMIT, boundedLimit * REFLECTION_SCAN_LIMIT_MULTIPLIER)); const rows = store.db .prepare<ActionLogRow, Array<string | number>>( SELECT id, created_at, guild_id, channel_id, message_id, user_id, kind, content, metadata, usd_cost FROM actions WHERE ${conditions.join(" AND ")} ORDER BY created_at DESC LIMIT ? ) .all(...params);

const runs = new Map(); for (const row of rows) { const metadata = safeJsonParse(row.metadata, null) || {}; const dateKey = String(metadata?.dateKey || "").trim(); const guildId = String(metadata?.guildId || row.guild_id || "").trim(); const runId = String(metadata?.runId || "").trim() || ${dateKey}:${guildId}; if (!dateKey || !guildId) continue;

const existing = runs.get(runId) || {
  runId: runId.includes(":") ? null : runId,
  dateKey,
  guildId,
  channelId: row.channel_id ? String(row.channel_id) : null,
  status: "running",
  startedAt: null,
  completedAt: null,
  erroredAt: null,
  durationMs: null,
  strategy: null,
  provider: null,
  model: null,
  extractorProvider: null,
  extractorModel: null,
  adjudicatorProvider: null,
  adjudicatorModel: null,
  usdCost: 0,
  maxFacts: null,
  journalEntryCount: null,
  authorCount: null,
  factsExtracted: 0,
  factsSelected: 0,
  factsAdded: 0,
  factsSaved: 0,
  factsSkipped: 0,
  extractedFacts: [],
  selectedFacts: [],
  savedFacts: [],
  skippedFacts: [],
  rawResponseText: null,
  usage: null,
  reflectionPasses: [],
  startContent: null,
  completionContent: null,
  errorContent: null
};

existing.strategy = existing.strategy || (metadata?.strategy ? String(metadata.strategy) : null);
existing.provider = existing.provider || (metadata?.provider ? String(metadata.provider) : null);
existing.model = existing.model || (metadata?.model ? String(metadata.model) : null);
existing.extractorProvider =
  existing.extractorProvider || (metadata?.extractorProvider ? String(metadata.extractorProvider) : null);
existing.extractorModel =
  existing.extractorModel || (metadata?.extractorModel ? String(metadata.extractorModel) : null);
existing.adjudicatorProvider =
  existing.adjudicatorProvider || (metadata?.adjudicatorProvider ? String(metadata.adjudicatorProvider) : null);
existing.adjudicatorModel =
  existing.adjudicatorModel || (metadata?.adjudicatorModel ? String(metadata.adjudicatorModel) : null);
existing.maxFacts =
  existing.maxFacts ?? (Number.isFinite(Number(metadata?.maxFacts)) ? Math.round(Number(metadata.maxFacts)) : null);
existing.journalEntryCount =
  existing.journalEntryCount ??
  (Number.isFinite(Number(metadata?.journalEntryCount)) ? Math.round(Number(metadata.journalEntryCount)) : null);
existing.authorCount =
  existing.authorCount ?? (Number.isFinite(Number(metadata?.authorCount)) ? Math.round(Number(metadata.authorCount)) : null);

if (row.kind === "memory_reflection_start") {
  existing.startedAt = existing.startedAt || String(row.created_at || "");
  existing.startContent = existing.startContent || (row.content ? String(row.content) : null);
} else if (row.kind === "memory_reflection_complete") {
  existing.status = "completed";
  existing.completedAt = existing.completedAt || String(row.created_at || "");
  existing.completionContent = existing.completionContent || (row.content ? String(row.content) : null);
  existing.usdCost = Number.isFinite(Number(row.usd_cost)) ? Number(row.usd_cost) : 0;
  existing.factsExtracted = Math.max(0, Number(metadata?.factsExtracted) || 0);
  existing.factsSelected = Math.max(0, Number(metadata?.factsSelected) || 0);
  existing.factsAdded = Math.max(0, Number(metadata?.factsAdded) || 0);
  existing.factsSaved = Math.max(0, Number(metadata?.factsSaved) || 0);
  existing.factsSkipped = Math.max(0, Number(metadata?.factsSkipped) || 0);
  existing.extractedFacts = Array.isArray(metadata?.extractedFacts) ? metadata.extractedFacts : [];
  existing.selectedFacts = Array.isArray(metadata?.selectedFacts) ? metadata.selectedFacts : [];
  existing.savedFacts = Array.isArray(metadata?.savedFacts) ? metadata.savedFacts : [];
  existing.skippedFacts = Array.isArray(metadata?.skippedFacts) ? metadata.skippedFacts : [];
  existing.rawResponseText =
    existing.rawResponseText || (metadata?.rawResponseText ? String(metadata.rawResponseText) : null);
  existing.usage = metadata?.usage && typeof metadata.usage === "object" ? metadata.usage : null;
  existing.reflectionPasses = Array.isArray(metadata?.reflectionPasses) ? metadata.reflectionPasses : [];
} else if (row.kind === "memory_reflection_error") {
  if (existing.status !== "completed") {
    existing.status = "error";
  }
  existing.erroredAt = existing.erroredAt || String(row.created_at || "");
  existing.errorContent = existing.errorContent || (row.content ? String(row.content) : null);
}

runs.set(runId, existing);

}

const sorted = [...runs.values()] .map((run) => { const startedAtMs = Date.parse(String(run.startedAt || "")); const completedAtMs = Date.parse(String(run.completedAt || "")); const erroredAtMs = Date.parse(String(run.erroredAt || "")); const finishedAtMs = Number.isFinite(completedAtMs) ? completedAtMs : erroredAtMs; return { ...run, durationMs: Number.isFinite(startedAtMs) && Number.isFinite(finishedAtMs) ? Math.max(0, finishedAtMs - startedAtMs) : null }; }) .sort((a, b) => { const aTime = Date.parse(String(a.completedAt || a.erroredAt || a.startedAt || "")) || 0; const bTime = Date.parse(String(b.completedAt || b.erroredAt || b.startedAt || "")) || 0; return bTime - aTime; });

return sorted.slice(0, boundedLimit); }

export function indexResponseTriggersForAction(store: ActionLogStore, { actionId, kind, metadata, createdAt = nowIso() }) { const normalizedActionId = Number(actionId); if (!Number.isInteger(normalizedActionId) || normalizedActionId <= 0) return; if (!shouldTrackResponseTriggerKind(kind)) return;

const triggerMessageIds = normalizeResponseTriggerMessageIds(metadata); if (!triggerMessageIds.length) return;

const insertTrigger = store.db.prepare<never, [string, number, string]>( INSERT OR IGNORE INTO response_triggers(trigger_message_id, action_id, created_at) VALUES (?, ?, ?) ); const insertTx = store.db.transaction((ids, responseActionId, responseCreatedAt) => { for (const triggerMessageId of ids) { insertTrigger.run(triggerMessageId, responseActionId, responseCreatedAt); } }); insertTx(triggerMessageIds, normalizedActionId, String(createdAt || nowIso())); }

export function hasReflectionBeenCompleted(store: ActionLogStore, dateKey: string, guildId: string): boolean { const checkpointRow = store.db .prepare<ReflectionCheckpointRow, [string, string]>( SELECT 1 AS found FROM reflection_checkpoints WHERE date_key = ? AND guild_id = ? LIMIT 1 ) .get(String(dateKey), String(guildId)); if (checkpointRow) return true;

const row = store.db .prepare<ActionPresenceRow, [string, string]>( SELECT 1 AS found, created_at, json_extract(metadata, '$.runId') AS run_id FROM actions WHERE kind = 'memory_reflection_complete' AND guild_id = ? AND json_extract(metadata, '$.dateKey') = ? LIMIT 1 ) .get(String(guildId), String(dateKey)); if (row) { markReflectionCompleted(store, dateKey, guildId, { runId: row.run_id || null, completedAt: row.created_at || nowIso() }); } return Boolean(row); }

export function markReflectionCompleted( store: ActionLogStore, dateKey: string, guildId: string, { runId = null, completedAt = nowIso() }: { runId?: string | null; completedAt?: string | null } = {} ) { const normalizedDateKey = String(dateKey || "").trim(); const normalizedGuildId = String(guildId || "").trim(); if (!normalizedDateKey || !normalizedGuildId) return { ok: false, reason: "checkpoint_key_required" } as const;

store.db .prepare<never, [string, string, string, string | null]>( INSERT INTO reflection_checkpoints(date_key, guild_id, completed_at, run_id) VALUES (?, ?, ?, ?) ON CONFLICT(date_key, guild_id) DO UPDATE SET completed_at = excluded.completed_at, run_id = excluded.run_id ) .run(normalizedDateKey, normalizedGuildId, String(completedAt || nowIso()), runId ? String(runId) : null);

return { ok: true, reason: "marked" } as const; }

export function deleteReflectionRun(store: ActionLogStore, runId: string): { deleted: number } { const normalizedRunId = String(runId || "").trim(); if (!normalizedRunId) return { deleted: 0 };

const result = store.db .prepare<never, [string]>( DELETE FROM actions WHERE kind IN ('memory_reflection_start', 'memory_reflection_complete', 'memory_reflection_error') AND json_extract(metadata, '$.runId') = ? ) .run(normalizedRunId); const deletedCheckpoints = store.db .prepare<never, [string]>( DELETE FROM reflection_checkpoints WHERE run_id = ? ) .run(normalizedRunId); return { deleted: Number(result.changes || 0) + Number(deletedCheckpoints.changes || 0) }; }

export function deleteMemoryReflectionRunsForGuild(store: ActionLogStore, guildId: string) { const normalizedGuildId = String(guildId || "").trim(); if (!normalizedGuildId) { return { ok: false, reason: "guild_required", deleted: 0 } as const; }

const result = store.db .prepare<never, [string]>( DELETE FROM actions WHERE guild_id = ? AND kind IN ('memory_reflection_start', 'memory_reflection_complete', 'memory_reflection_error') ) .run(normalizedGuildId);

const deletedCheckpoints = store.db .prepare<never, [string]>( DELETE FROM reflection_checkpoints WHERE guild_id = ? ) .run(normalizedGuildId);

return { ok: true, reason: "deleted", deleted: Number(result?.changes || 0) + Number(deletedCheckpoints?.changes || 0) } as const; }

export function getRecentBrowserSessions( store: ActionLogStore, limit = DEFAULT_RECENT_BROWSER_SESSIONS_LIMIT, opts: { sinceIso?: string | null; guildId?: string | null } = {} ) { const parsedLimit = clamp( Math.floor(Number(limit) || DEFAULT_RECENT_BROWSER_SESSIONS_LIMIT), 1, MAX_RECENT_BROWSER_SESSIONS_LIMIT ); const sinceIso = String(opts?.sinceIso || "").trim(); const guildId = String(opts?.guildId || "").trim();

const conditions: string[] = [ "kind IN ('browser_browse_call', 'browser_browse_failed', 'browser_tool_step', 'browser_agent_session_turn')" ]; const params: Array<string | number> = [];

if (sinceIso) { conditions.push("created_at >= ?"); params.push(sinceIso); } if (guildId) { conditions.push("guild_id = ?"); params.push(guildId); }

const whereClause = conditions.join(" AND "); const rows = store.db .prepare<ActionLogRow, Array<string | number>>( SELECT id, created_at, guild_id, channel_id, message_id, user_id, kind, content, metadata, usd_cost FROM actions WHERE ${whereClause} ORDER BY created_at DESC LIMIT ? ) .all(...params, parsedLimit * BROWSER_SESSION_SCAN_MULTIPLIER);

interface ToolStepEntry { tool: string; step: number; timestamp: string; }

interface SessionAccum { sessionId: string; startedAt: string; lastActiveAt: string; source: string | null; guildId: string | null; channelId: string | null; userId: string | null; instruction: string | null; totalCostUsd: number; totalSteps: number; hitStepLimit: boolean; durationMs: number | null; runtime: string | null; provider: string | null; model: string | null; currentUrl: string | null; failed: boolean; errorName: string | null; errorMessage: string | null; toolSteps: ToolStepEntry[]; }

const sessions = new Map<string, SessionAccum>();

for (const row of rows) { const metadata = safeJsonParse(row.metadata, null) as Record<string, unknown> | null; const meta = metadata || {};

const sessionId =
  String(meta.sessionId || meta.sessionKey || "").trim() ||
  `browse-${String(row.id || row.created_at || "")}`;

const existing = sessions.get(sessionId) || {
  sessionId,
  startedAt: String(row.created_at || ""),
  lastActiveAt: String(row.created_at || ""),
  source: null,
  guildId: null,
  channelId: null,
  userId: null,
  instruction: null,
  totalCostUsd: 0,
  totalSteps: 0,
  hitStepLimit: false,
  durationMs: null,
  runtime: null,
  provider: null,
  model: null,
  currentUrl: null,
  failed: false,
  errorName: null,
  errorMessage: null,
  toolSteps: []
};

const rowTime = String(row.created_at || "");
if (rowTime && (!existing.startedAt || rowTime < existing.startedAt)) {
  existing.startedAt = rowTime;
}
if (rowTime && (!existing.lastActiveAt || rowTime > existing.lastActiveAt)) {
  existing.lastActiveAt = rowTime;
}

existing.guildId = existing.guildId || (row.guild_id ? String(row.guild_id) : null);
existing.channelId = existing.channelId || (row.channel_id ? String(row.channel_id) : null);
existing.userId = existing.userId || (row.user_id ? String(row.user_id) : null);
existing.source = existing.source || (meta.source ? String(meta.source) : null);
existing.runtime = existing.runtime || (meta.runtime ? String(meta.runtime) : null);
existing.provider = existing.provider || (meta.provider ? String(meta.provider) : null);
existing.model = existing.model || (meta.model ? String(meta.model) : null);
existing.currentUrl = existing.currentUrl || (meta.currentUrl ? String(meta.currentUrl) : null);

if (row.kind === "browser_browse_call") {
  existing.instruction = existing.instruction || (row.content ? String(row.content) : null);
  const steps = Number(meta.steps);
  if (Number.isFinite(steps) && steps > existing.totalSteps) existing.totalSteps = steps;
  if (meta.hitStepLimit) existing.hitStepLimit = true;
  const cost = Number(meta.totalCostUsd || row.usd_cost);
  if (Number.isFinite(cost)) existing.totalCostUsd += cost;
  const durationMs = Number(meta.durationMs);
  if (Number.isFinite(durationMs) && durationMs >= 0) {
    existing.durationMs = Math.max(existing.durationMs || 0, durationMs);
  }
} else if (row.kind === "browser_browse_failed") {
  existing.instruction = existing.instruction || (row.content ? String(row.content) : null);
  existing.failed = true;
  existing.errorName = existing.errorName || (meta.errorName ? String(meta.errorName) : null);
  existing.errorMessage = existing.errorMessage || (meta.errorMessage ? String(meta.errorMessage) : null);
  const steps = Number(meta.steps);
  if (Number.isFinite(steps) && steps > existing.totalSteps) existing.totalSteps = steps;
  const cost = Number(meta.totalCostUsd || row.usd_cost);
  if (Number.isFinite(cost)) existing.totalCostUsd += cost;
  const durationMs = Number(meta.durationMs);
  if (Number.isFinite(durationMs) && durationMs >= 0) {
    existing.durationMs = Math.max(existing.durationMs || 0, durationMs);
  }
} else if (row.kind === "browser_tool_step") {
  const step = Math.max(0, Math.floor(Number(meta.step) || 0));
  if (step > existing.totalSteps) existing.totalSteps = step;
  existing.toolSteps.push({
    tool: String(meta.tool || row.content || "unknown"),
    step,
    timestamp: rowTime
  });
} else if (row.kind === "browser_agent_session_turn") {
  if (!existing.instruction) {
    existing.instruction = row.content ? String(row.content) : null;
  }
  const steps = Number(meta.steps);
  if (Number.isFinite(steps)) existing.totalSteps += steps;
  const cost = Number(meta.turnCostUsd || row.usd_cost);
  if (Number.isFinite(cost)) existing.totalCostUsd += cost;
  const dur = Number(meta.durationMs);
  if (Number.isFinite(dur)) {
    existing.durationMs = (existing.durationMs || 0) + dur;
  }
}

sessions.set(sessionId, existing);

}

const sorted = [...sessions.values()] .map((session) => { session.toolSteps.sort((a, b) => a.step - b.step); if (session.durationMs === null) { const startMs = Date.parse(session.startedAt); const endMs = Date.parse(session.lastActiveAt); if (Number.isFinite(startMs) && Number.isFinite(endMs) && endMs > startMs) { session.durationMs = endMs - startMs; } } return session; }) .sort((a, b) => { const aTime = Date.parse(a.lastActiveAt) || 0; const bTime = Date.parse(b.lastActiveAt) || 0; return bTime - aTime; });

return sorted.slice(0, parsedLimit); }

export function hasTriggeredResponse(store: ActionLogStore, triggerMessageId) { const id = String(triggerMessageId).trim(); if (!id) return false;

const row = store.db .prepare<ActionPresenceRow, [string]>( SELECT 1 AS found FROM response_triggers WHERE trigger_message_id = ? LIMIT 1 ) .get(id);

return Boolean(row); }