{"version":3,"file":"session-keepalive-BuG3wvik.mjs","names":["fetchLiveSessionsDefault","sendToSessionDefault"],"sources":["../src/audit/session-usage.ts","../src/daemon/session-keepalive.ts"],"sourcesContent":["/**\n * session-usage.ts — parse the usage numbers out of a Claude Code session\n * (or subagent, or pai-worker event-mirror) JSONL transcript.\n *\n * All three log shapes share the same assistant-message envelope\n * ({ type: \"assistant\", message: { id, model, usage } }), so one parser\n * covers `pai audit tokens session` and the per-log readings feeding\n * `pai audit tokens spawn`.\n *\n * Streaming writes one JSONL line per content block of the same logical\n * turn, repeating message.id and usage each time — summing every line would\n * multiply usage by the block count. Keeping only the first line seen per\n * message.id is what the reference script (and this parser) count instead.\n */\n\nimport { createReadStream, existsSync } from \"node:fs\";\nimport { createInterface } from \"node:readline\";\nimport { readFileSync } from \"node:fs\";\n\nexport interface UsageTotals {\n  cache_read_input_tokens: number;\n  cache_creation_input_tokens: number;\n  input_tokens: number;\n  output_tokens: number;\n}\n\nexport interface CacheCreationSplit {\n  ephemeral5m: number;\n  ephemeral1h: number;\n}\n\nexport interface CompactionEvent {\n  trigger: string;\n  preTokens: number;\n  turnIndex: number;\n}\n\nexport interface ModelSwitch {\n  turnIndex: number;\n  from: string;\n  to: string;\n  cacheRead: number;\n  cacheCreation: number;\n}\n\nexport interface FallbackEvent {\n  turnIndex: number;\n  from: string;\n  to: string;\n  category: string;\n  scope: string;\n}\n\nexport interface SessionUsageReport {\n  path: string;\n  sizeBytes: number;\n  turns: number;\n  totals: UsageTotals;\n  /** Per-model assistant-turn counts. */\n  models: Record<string, number>;\n  /** cache_read + cache_creation + input on the first / last assistant turn seen. */\n  firstTurnContext: number | null;\n  lastTurnContext: number | null;\n  cacheCreationSplit: CacheCreationSplit;\n  /** Average / max of the per-turn context value across all turns; null if no turns. */\n  avgContext: number | null;\n  maxContext: number | null;\n  /** Turns whose per-turn context value exceeds the report's threshold. */\n  turnsAboveThreshold: number;\n  /** Turns whose cache_creation_input_tokens exceeds 20000. */\n  cacheRebuildTurns: number;\n  /** Real human-authored user prompts (excludes tool-result-only \"user\" lines). */\n  userPrompts: number;\n  /**\n   * Sum over assistant turns of the real user prompts seen before that turn;\n   * multiplied by per-prompt hook tokens it gives the tokens UserPromptSubmit\n   * output occupied across the whole session.\n   */\n  promptExposure: number;\n  compactions: CompactionEvent[];\n  /** ISO timestamp of the first assistant turn. */\n  firstTurnAt: string | null;\n  /** Model of the last folded turn; drives switch detection. */\n  lastModel: string | null;\n  modelSwitches: ModelSwitch[];\n  fallbacks: FallbackEvent[];\n  /** Timestamp (ms) of the last folded assistant turn; drives idle-gap detection. */\n  lastTurnAtMs: number | null;\n  /** Gaps between consecutive assistant turns exceeding 60 minutes — evidence\n   *  for whether the session ever went idle long enough to risk its cache TTL\n   *  (see sessions.cacheKeepalive, src/daemon/config.ts). */\n  idleGapsOver60min: number;\n  /** Real user prompts whose text is exactly the configured keepalive word\n   *  (set only when parseSessionUsage is called with one); null otherwise. */\n  keepaliveBeats: number | null;\n}\n\nconst USAGE_KEYS: (keyof UsageTotals)[] = [\n  \"cache_read_input_tokens\",\n  \"cache_creation_input_tokens\",\n  \"input_tokens\",\n  \"output_tokens\",\n];\n\nfunction emptyTotals(): UsageTotals {\n  return { cache_read_input_tokens: 0, cache_creation_input_tokens: 0, input_tokens: 0, output_tokens: 0 };\n}\n\nexport interface AssistantLine {\n  type?: string;\n  uuid?: string;\n  subtype?: string;\n  /** Claude Code marks skill expansions and injected system reminders isMeta; they fire no UserPromptSubmit hook. */\n  isMeta?: boolean;\n  /** Present on newer logs: { kind: \"human\" } for a typed prompt. */\n  origin?: { kind?: string };\n  compactMetadata?: { trigger?: string; preTokens?: number };\n  timestamp?: string;\n  originalModel?: string;\n  fallbackModel?: string;\n  apiRefusalCategory?: string;\n  scope?: string;\n  message?: {\n    id?: string;\n    model?: string;\n    usage?: Record<string, unknown> & {\n      cache_creation?: { ephemeral_5m_input_tokens?: number; ephemeral_1h_input_tokens?: number };\n    };\n    content?: string | Array<{ type?: string }>;\n    stop_reason?: string | null;\n  };\n}\n\n/**\n * `type:\"system\"` `subtype:\"compact_boundary\"` lines mark a compaction.\n * turnIndex is the count of assistant turns already folded when it fired,\n * so it lines up with the turn numbering the text/JSON report prints.\n */\nexport function isCompactBoundary(line: AssistantLine): boolean {\n  return line.type === \"system\" && line.subtype === \"compact_boundary\";\n}\n\nexport function parseCompactionEvent(line: AssistantLine, turnIndex: number): CompactionEvent | null {\n  const meta = line.compactMetadata;\n  if (!meta || typeof meta.preTokens !== \"number\") return null;\n  return { trigger: meta.trigger ?? \"unknown\", preTokens: meta.preTokens, turnIndex };\n}\n\n/**\n * `type:\"system\"` `subtype:\"model_refusal_fallback\"` lines mark an\n * automatic model switch fired by an API safety refusal (e.g. \"cyber\"\n * category), not a user or config choice — worth flagging separately since\n * it can rebuild the whole prompt cache mid-session.\n */\nexport function isModelFallback(line: AssistantLine): boolean {\n  return line.type === \"system\" && line.subtype === \"model_refusal_fallback\";\n}\n\nexport function parseFallbackEvent(line: AssistantLine, turnIndex: number): FallbackEvent {\n  return {\n    turnIndex,\n    from: line.originalModel ?? \"unknown\",\n    to: line.fallbackModel ?? \"unknown\",\n    category: line.apiRefusalCategory ?? \"unknown\",\n    scope: line.scope ?? \"unknown\",\n  };\n}\n\n/**\n * Fold one already-parsed JSONL line into a report being accumulated.\n * Exported separately so both the streaming file reader below and tests\n * (which build fixtures as arrays of objects, not files) share one path.\n * Returns the turn's context value when a new turn was counted, else null\n * (non-assistant line, no usage, or a duplicate message.id already seen).\n */\nexport function foldAssistantLine(\n  report: Pick<\n    SessionUsageReport,\n    | \"turns\"\n    | \"totals\"\n    | \"models\"\n    | \"firstTurnContext\"\n    | \"lastTurnContext\"\n    | \"cacheCreationSplit\"\n    | \"maxContext\"\n    | \"turnsAboveThreshold\"\n    | \"cacheRebuildTurns\"\n    | \"firstTurnAt\"\n    | \"lastModel\"\n    | \"modelSwitches\"\n    | \"lastTurnAtMs\"\n    | \"idleGapsOver60min\"\n  >,\n  seenIds: Set<string>,\n  line: AssistantLine,\n  threshold: number\n): number | null {\n  if (line.type !== \"assistant\") return null;\n  const message = line.message;\n  const usage = message?.usage;\n  if (!usage) return null;\n  const id = message?.id ?? line.uuid;\n  if (id) {\n    if (seenIds.has(id)) return null;\n    seenIds.add(id);\n  }\n  report.turns++;\n  for (const key of USAGE_KEYS) {\n    const v = usage[key];\n    if (typeof v === \"number\") report.totals[key] += v;\n  }\n  const context =\n    (Number(usage.cache_read_input_tokens) || 0) +\n    (Number(usage.cache_creation_input_tokens) || 0) +\n    (Number(usage.input_tokens) || 0);\n  if (report.firstTurnContext === null) {\n    report.firstTurnContext = context;\n    report.firstTurnAt = line.timestamp ?? null;\n  }\n  const atMs = line.timestamp ? Date.parse(line.timestamp) : NaN;\n  if (!Number.isNaN(atMs)) {\n    if (report.lastTurnAtMs !== null && atMs - report.lastTurnAtMs > 60 * 60 * 1000) {\n      report.idleGapsOver60min++;\n    }\n    report.lastTurnAtMs = atMs;\n  }\n  report.lastTurnContext = context;\n  report.maxContext = report.maxContext === null ? context : Math.max(report.maxContext, context);\n  if (context > threshold) report.turnsAboveThreshold++;\n  if ((Number(usage.cache_creation_input_tokens) || 0) > 20000) report.cacheRebuildTurns++;\n  const model = message?.model ?? \"unknown\";\n  report.models[model] = (report.models[model] ?? 0) + 1;\n  if (report.lastModel !== null && report.lastModel !== model) {\n    report.modelSwitches.push({\n      turnIndex: report.turns,\n      from: report.lastModel,\n      to: model,\n      cacheRead: Number(usage.cache_read_input_tokens) || 0,\n      cacheCreation: Number(usage.cache_creation_input_tokens) || 0,\n    });\n  }\n  report.lastModel = model;\n  const split = usage.cache_creation;\n  if (split) {\n    report.cacheCreationSplit.ephemeral5m += Number(split.ephemeral_5m_input_tokens) || 0;\n    report.cacheCreationSplit.ephemeral1h += Number(split.ephemeral_1h_input_tokens) || 0;\n  }\n  return context;\n}\n\n/**\n * True for a real human-authored `type:\"user\"` prompt line: content is a\n * plain string, or a content array with no `tool_result` block. Claude Code\n * encodes tool results as `type:\"user\"` messages whose content array is\n * entirely (or partly) tool_result blocks — those must not count as prompts.\n */\nexport function isRealUserPrompt(line: AssistantLine): boolean {\n  if (line.type !== \"user\") return false;\n  if (line.isMeta) return false;\n  if (line.origin?.kind && line.origin.kind !== \"human\") return false;\n  const content = line.message?.content;\n  if (typeof content === \"string\") return true;\n  if (Array.isArray(content)) return !content.some((block) => block?.type === \"tool_result\");\n  return false;\n}\n\n/** Plain text of a real user prompt: the string content, or its first text block. */\nexport function extractUserPromptText(line: AssistantLine): string {\n  const content = line.message?.content;\n  if (typeof content === \"string\") return content;\n  if (Array.isArray(content)) {\n    const block = content.find((b) => b?.type === \"text\") as { text?: string } | undefined;\n    return block?.text ?? \"\";\n  }\n  return \"\";\n}\n\n/**\n * Parse a session/subagent/worker-event JSONL file into a usage report.\n * `keepaliveWord`, when given, counts real user prompts whose text exactly\n * matches it (trimmed) — the sessions.cacheKeepalive beat prompt — into\n * `keepaliveBeats`; omitted, `keepaliveBeats` stays null.\n */\nexport async function parseSessionUsage(\n  path: string,\n  threshold = 200_000,\n  keepaliveWord?: string\n): Promise<SessionUsageReport> {\n  const report: SessionUsageReport = {\n    path,\n    sizeBytes: existsSync(path) ? readFileSync(path).byteLength : 0,\n    turns: 0,\n    totals: emptyTotals(),\n    models: {},\n    firstTurnContext: null,\n    lastTurnContext: null,\n    cacheCreationSplit: { ephemeral5m: 0, ephemeral1h: 0 },\n    avgContext: null,\n    maxContext: null,\n    turnsAboveThreshold: 0,\n    cacheRebuildTurns: 0,\n    userPrompts: 0,\n    promptExposure: 0,\n    compactions: [],\n    firstTurnAt: null,\n    lastModel: null,\n    modelSwitches: [],\n    fallbacks: [],\n    lastTurnAtMs: null,\n    idleGapsOver60min: 0,\n    keepaliveBeats: keepaliveWord ? 0 : null,\n  };\n  const seenIds = new Set<string>();\n  let contextSum = 0;\n\n  const rl = createInterface({ input: createReadStream(path, \"utf8\"), crlfDelay: Infinity });\n  for await (const raw of rl) {\n    if (!raw.trim()) continue;\n    let obj: AssistantLine;\n    try {\n      obj = JSON.parse(raw) as AssistantLine;\n    } catch {\n      continue;\n    }\n    if (obj.type === \"user\") {\n      if (isRealUserPrompt(obj)) {\n        report.userPrompts++;\n        if (keepaliveWord && extractUserPromptText(obj).trim() === keepaliveWord) {\n          report.keepaliveBeats = (report.keepaliveBeats ?? 0) + 1;\n        }\n      }\n      continue;\n    }\n    if (isCompactBoundary(obj)) {\n      const event = parseCompactionEvent(obj, report.turns);\n      if (event) report.compactions.push(event);\n      continue;\n    }\n    if (isModelFallback(obj)) {\n      report.fallbacks.push(parseFallbackEvent(obj, report.turns));\n      continue;\n    }\n    const context = foldAssistantLine(report, seenIds, obj, threshold);\n    if (context !== null) {\n      contextSum += context;\n      report.promptExposure += report.userPrompts;\n    }\n  }\n  report.avgContext = report.turns > 0 ? Math.round(contextSum / report.turns) : null;\n  return report;\n}\n\nexport function totalUsageTokens(totals: UsageTotals): number {\n  return totals.cache_read_input_tokens + totals.cache_creation_input_tokens + totals.input_tokens + totals.output_tokens;\n}\n","/**\n * session-keepalive.ts — idle-triggered prompt-cache keepalive beat for live\n * interactive Claude Code sessions (`sessions.cacheKeepalive`, see config.ts\n * and docs/cache-keepalive.md, \"Interactive sessions\").\n *\n * One beat = one trivial prompt typed into a session through AIBroker's\n * send_to_session (src/cli/lib/aibroker-client.ts). A cache READ refreshes\n * the provider's ephemeral prompt-cache TTL at roughly 0.1x the cost of the\n * 2x rewrite a cold cache forces on the next real prompt.\n *\n * Distinct from workers/keepalive.ts, which beats a *worker provider's*\n * cache on a fixed timer regardless of activity: this only beats a session\n * that has actually been idle long enough to be at risk, and only inside a\n * configured working-hours window, so it never fires while the user is\n * present at the keyboard or overnight when nobody will read the reply\n * before the cache would have expired anyway.\n */\n\nimport { existsSync, readFileSync, readdirSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport {\n  fetchLiveSessions as fetchLiveSessionsDefault,\n  sendToSession as sendToSessionDefault,\n  type AiBrokerSessionMeta,\n} from \"../cli/lib/aibroker-client.js\";\nimport { writeJsonAtomic } from \"../config/json-store.js\";\nimport { paiHomePath } from \"../config/pai-home.js\";\nimport { appendLedger } from \"../workers/ledger.js\";\nimport { readWorkersSection } from \"../workers/config.js\";\nimport { workersLogDir } from \"../workers/paths.js\";\nimport { worktreesDir } from \"../workers/worktree.js\";\nimport { itermUuid, sessionMapPath, type SessionMapEntry } from \"../workers/scope.js\";\nimport { isRealUserPrompt, extractUserPromptText, type AssistantLine } from \"../audit/session-usage.js\";\nimport type { SessionsCacheKeepaliveConfig } from \"./config.js\";\n\n// ---------------------------------------------------------------------------\n// State file — per-session beat counters, rebuildable (see json-store.ts on\n// when NOT to use readJsonStrict: a damaged file here just resets counters,\n// never blocks the feature).\n// ---------------------------------------------------------------------------\n\nexport interface SessionKeepaliveEntry {\n  /** Beats sent since the last real (non-keepalive) user prompt. */\n  beats: number;\n  /** Identity of the last real user prompt observed, so a fresh one can be\n   *  told apart from the keepalive's own echo landing back in the transcript. */\n  lastRealPromptKey: string | null;\n  /** ISO stamp of the last beat sent. */\n  lastBeatAt: string | null;\n}\n\nexport type SessionKeepaliveState = Record<string, SessionKeepaliveEntry>;\n\nexport function sessionKeepaliveStatePath(): string {\n  return paiHomePath(\"session-keepalive.json\");\n}\n\nfunction emptyEntry(): SessionKeepaliveEntry {\n  return { beats: 0, lastRealPromptKey: null, lastBeatAt: null };\n}\n\nexport function loadSessionKeepaliveState(path: string = sessionKeepaliveStatePath()): SessionKeepaliveState {\n  if (!existsSync(path)) return {};\n  try {\n    return JSON.parse(readFileSync(path, \"utf8\")) as SessionKeepaliveState;\n  } catch {\n    return {};\n  }\n}\n\nexport function saveSessionKeepaliveState(\n  state: SessionKeepaliveState,\n  path: string = sessionKeepaliveStatePath()\n): void {\n  writeJsonAtomic(path, state, { backup: false, label: path });\n}\n\n// ---------------------------------------------------------------------------\n// Active-hours window\n// ---------------------------------------------------------------------------\n\n/** Parse \"HH:MM-HH:MM\" into minutes-since-midnight. Throws on a malformed\n *  spec — an explicit config error beats a window that is silently always\n *  on or always off. */\nexport function parseActiveHours(spec: string): { startMin: number; endMin: number } {\n  const m = spec.match(/^(\\d{1,2}):(\\d{2})-(\\d{1,2}):(\\d{2})$/);\n  if (!m) {\n    throw new Error(`sessions.cacheKeepalive.activeHours: invalid \"${spec}\" (want \"HH:MM-HH:MM\")`);\n  }\n  return {\n    startMin: Number(m[1]) * 60 + Number(m[2]),\n    endMin: Number(m[3]) * 60 + Number(m[4]),\n  };\n}\n\n/**\n * Whether `now` (local time) sits inside the window. A window that wraps\n * midnight (e.g. \"22:00-06:00\") is honoured by inverting the test instead of\n * requiring startMin < endMin.\n */\nexport function isWithinActiveHours(now: Date, spec: string): boolean {\n  const { startMin, endMin } = parseActiveHours(spec);\n  const nowMin = now.getHours() * 60 + now.getMinutes();\n  if (startMin <= endMin) return nowMin >= startMin && nowMin < endMin;\n  return nowMin >= startMin || nowMin < endMin;\n}\n\n// ---------------------------------------------------------------------------\n// Transcript lookup\n// ---------------------------------------------------------------------------\n\n/**\n * Full path to a live session's transcript under ~/.claude/projects, or null\n * when none is found — the case for a session too new to have written a file\n * yet. A worker running in a worktree DOES write a transcript here (Claude\n * Code encodes its worktree cwd as the project dir name), so workers are not\n * excluded \"for free\" — see isWorkerSession.\n */\nexport function findSessionTranscript(\n  sessionId: string,\n  projectsDir: string = join(homedir(), \".claude\", \"projects\")\n): string | null {\n  let projectDirs: string[];\n  try {\n    projectDirs = readdirSync(projectsDir);\n  } catch {\n    return null;\n  }\n  for (const projectDir of projectDirs) {\n    const candidate = join(projectsDir, projectDir, `${sessionId}.jsonl`);\n    if (existsSync(candidate)) return candidate;\n  }\n  return null;\n}\n\n/**\n * AIBroker's `fetchLiveSessions()` identifies a session by its iTerm2 pane id\n * (e.g. an AIBroker/iTerm UUID), not the Claude session id `<uuid>.jsonl`\n * transcripts are named after — the two are unrelated identifiers. The\n * status line bridges them on every refresh via `claude-session-map.json`\n * (see `recordSessionMapEntry` in ../workers/scope.ts): this picks, among the\n * map entries whose `term` pane UUID matches, the most recently written one.\n * Returns null when the pane has no (fresh enough) mapped Claude session.\n */\nexport function resolveClaudeSessionIdFromMap(paneId: string, logDir: string): string | null {\n  const path = sessionMapPath(logDir);\n  if (!paneId || !existsSync(path)) return null;\n  let map: Record<string, SessionMapEntry>;\n  try {\n    map = JSON.parse(readFileSync(path, \"utf8\")) as Record<string, SessionMapEntry>;\n  } catch {\n    return null;\n  }\n  let best: SessionMapEntry | null = null;\n  for (const entry of Object.values(map)) {\n    if (!entry.term || itermUuid(entry.term) !== paneId) continue;\n    if (!best || entry.ts > best.ts) best = entry;\n  }\n  return best?.session ?? null;\n}\n\n/** Claude Code's project-dir encoding of a cwd: every \"/\" becomes \"-\". */\nfunction encodeProjectDirName(cwd: string): string {\n  return cwd.replace(/\\//g, \"-\");\n}\n\n/**\n * Is this live \"claude\"-kind session actually a worker pane rather than an\n * interactive one? `claude -p` workers running in a worktree write their\n * transcript under ~/.claude/projects too (encoded cwd = the worktree dir),\n * so a missing transcript is NOT how workers get excluded — this predicate\n * is. Either signal is enough:\n *   (a) the transcript's project-dir name is the encoded form of a path\n *       under <logDir>/worktrees (every worker worktree lives there), or\n *   (b) the broker's session name/paiName names a path under the workers\n *       log dir or one of its worktrees (best-effort: AIBroker does not\n *       guarantee this, but honors it when present).\n */\nexport function isWorkerSession(\n  meta: AiBrokerSessionMeta,\n  transcriptPath: string | null,\n  logDir: string\n): boolean {\n  const worktreesPrefix = encodeProjectDirName(worktreesDir(logDir));\n  if (transcriptPath) {\n    const projectDirName = transcriptPath.split(\"/\").slice(0, -1).pop() ?? \"\";\n    if (projectDirName.startsWith(worktreesPrefix)) return true;\n  }\n  const rawPrefix = worktreesDir(logDir);\n  for (const field of [meta.name, meta.paiName]) {\n    if (field && (field.includes(rawPrefix) || field.includes(logDir))) return true;\n  }\n  return false;\n}\n\n// ---------------------------------------------------------------------------\n// Tail snapshot: last turn's context, mid-turn flag, last real prompt\n// ---------------------------------------------------------------------------\n\nexport interface TranscriptSnapshot {\n  /** cache_read + cache_creation + input on the most recent assistant turn seen. */\n  context: number | null;\n  /** True when the last line in the file shows the session still working: an\n   *  assistant turn with no usage yet (streaming) or a stop_reason other than\n   *  end_turn/stop_sequence (e.g. mid tool-call), or a real user prompt with\n   *  no reply behind it yet. A beat sent now would land mid-generation. */\n  midTurn: boolean;\n  /** The last real (human-authored) user prompt seen, if any. */\n  lastRealPrompt: { key: string; text: string } | null;\n}\n\nfunction isMidTurn(line: AssistantLine | null): boolean {\n  if (!line) return false;\n  if (line.type === \"user\" && isRealUserPrompt(line)) return true;\n  if (line.type === \"assistant\") {\n    const stopReason = line.message?.stop_reason;\n    if (stopReason === undefined || stopReason === null) return true;\n    return stopReason !== \"end_turn\" && stopReason !== \"stop_sequence\";\n  }\n  return false;\n}\n\nexport function readTranscriptSnapshot(path: string): TranscriptSnapshot {\n  let lines: string[];\n  try {\n    lines = readFileSync(path, \"utf8\").split(\"\\n\");\n  } catch {\n    return { context: null, midTurn: false, lastRealPrompt: null };\n  }\n\n  let lastLine: AssistantLine | null = null;\n  let context: number | null = null;\n  let lastRealPrompt: { key: string; text: string } | null = null;\n\n  for (let i = lines.length - 1; i >= 0; i--) {\n    const raw = lines[i].trim();\n    if (!raw) continue;\n    let obj: AssistantLine;\n    try {\n      obj = JSON.parse(raw) as AssistantLine;\n    } catch {\n      continue;\n    }\n    if (lastLine === null) lastLine = obj;\n\n    if (context === null && obj.type === \"assistant\" && obj.message?.usage) {\n      const usage = obj.message.usage;\n      context =\n        (Number(usage.cache_read_input_tokens) || 0) +\n        (Number(usage.cache_creation_input_tokens) || 0) +\n        (Number(usage.input_tokens) || 0);\n    }\n\n    if (lastRealPrompt === null && isRealUserPrompt(obj)) {\n      const key = obj.uuid ?? obj.timestamp ?? String(i);\n      lastRealPrompt = { key, text: extractUserPromptText(obj).trim() };\n    }\n\n    if (context !== null && lastRealPrompt !== null) break;\n  }\n\n  return { context, midTurn: isMidTurn(lastLine), lastRealPrompt };\n}\n\n// ---------------------------------------------------------------------------\n// Ledger — same file WORKER-KEEPALIVE lines go to (workers.logDir/ledger.log)\n// ---------------------------------------------------------------------------\n\n/** Ledger event tag; `pai daemon keepalive` and any log-tailer key off this. */\nexport const SESSION_KEEPALIVE_EVENT = \"SESSION-KEEPALIVE\";\n\nexport function sessionKeepaliveLedgerPath(): string {\n  const { workers } = readWorkersSection();\n  return join(workersLogDir(workers), \"ledger.log\");\n}\n\n/** The counts/last lines `pai daemon keepalive` prints, scoped to this event. */\nexport function sessionKeepaliveLedgerSummary(\n  path: string = sessionKeepaliveLedgerPath(),\n  lastN = 10\n): { sent: number; skipped: number; lastLines: string[] } {\n  if (!existsSync(path)) return { sent: 0, skipped: 0, lastLines: [] };\n  const lines = readFileSync(path, \"utf8\")\n    .split(\"\\n\")\n    .filter((l) => l.includes(SESSION_KEEPALIVE_EVENT));\n  const sent = lines.filter((l) => / result=sent(\\s|$)/.test(l)).length;\n  return { sent, skipped: lines.length - sent, lastLines: lines.slice(-lastN) };\n}\n\n// ---------------------------------------------------------------------------\n// The tick\n// ---------------------------------------------------------------------------\n\nexport interface SessionKeepaliveDeps {\n  now: () => Date;\n  fetchLiveSessions: () => Promise<AiBrokerSessionMeta[]>;\n  /** AIBroker pane id (fetchLiveSessions' `sessionId`) -> Claude transcript\n   *  session id, via the status line's claude-session-map.json bridge. */\n  resolveClaudeSessionId: (paneId: string) => string | null;\n  findTranscript: (sessionId: string) => string | null;\n  mtimeMs: (path: string) => number | null;\n  readSnapshot: (path: string) => TranscriptSnapshot;\n  sendBeat: (sessionId: string, text: string) => Promise<{ ok: boolean; error?: string }>;\n  loadState: () => SessionKeepaliveState;\n  saveState: (s: SessionKeepaliveState) => void;\n  ledger: (kv: Record<string, string | number | null | undefined>) => void;\n}\n\nexport interface SessionKeepaliveResult {\n  sessionId: string;\n  result: \"sent\" | string; // \"skipped:<reason>\"\n}\n\nfunction defaultDeps(): SessionKeepaliveDeps {\n  const { workers } = readWorkersSection();\n  const logDir = workersLogDir(workers);\n  return {\n    now: () => new Date(),\n    fetchLiveSessions: fetchLiveSessionsDefault,\n    resolveClaudeSessionId: (paneId) => resolveClaudeSessionIdFromMap(paneId, logDir),\n    findTranscript: (id) => findSessionTranscript(id),\n    mtimeMs: (p) => {\n      try {\n        return statSync(p).mtimeMs;\n      } catch {\n        return null;\n      }\n    },\n    readSnapshot: readTranscriptSnapshot,\n    sendBeat: (id, text) => sendToSessionDefault(id, text),\n    loadState: () => loadSessionKeepaliveState(),\n    saveState: (s) => saveSessionKeepaliveState(s),\n    ledger: (kv) => appendLedger(sessionKeepaliveLedgerPath(), SESSION_KEEPALIVE_EVENT, kv),\n  };\n}\n\n/**\n * One tick over every live interactive session: beat the ones that qualify,\n * skip (with a reason) the ones that don't. Returns one result per live\n * \"claude\"-kind session for tests to assert against; writes the ledger line\n * and (when any counter changed) the state file as side effects.\n */\nexport async function runSessionKeepaliveTick(\n  config: SessionsCacheKeepaliveConfig,\n  overrides: Partial<SessionKeepaliveDeps> = {}\n): Promise<SessionKeepaliveResult[]> {\n  if (!config.enabled) return [];\n\n  const d: SessionKeepaliveDeps = { ...defaultDeps(), ...overrides };\n  const sessions = await d.fetchLiveSessions();\n  const state = d.loadState();\n  const now = d.now();\n  const { workers } = readWorkersSection();\n  const logDir = workersLogDir(workers);\n  const results: SessionKeepaliveResult[] = [];\n  let anyChanged = false;\n\n  for (const s of sessions) {\n    if (s.kind !== \"claude\") continue;\n    const paneId = s.sessionId;\n\n    const claudeId = d.resolveClaudeSessionId(paneId);\n    if (!claudeId) {\n      d.ledger({ session: null, pane: paneId, result: \"skipped:unmapped\" });\n      results.push({ sessionId: paneId, result: \"skipped:unmapped\" });\n      continue;\n    }\n    const sessionId = claudeId;\n    const entry = { ...(state[sessionId] ?? emptyEntry()) };\n    let changed = false;\n\n    const transcript = d.findTranscript(sessionId);\n\n    if (isWorkerSession(s, transcript, logDir)) {\n      d.ledger({ session: sessionId, pane: paneId, result: \"skipped:worker\" });\n      results.push({ sessionId, result: \"skipped:worker\" });\n      continue;\n    }\n\n    if (!transcript) {\n      d.ledger({ session: sessionId, pane: paneId, result: \"skipped:no-transcript\" });\n      results.push({ sessionId, result: \"skipped:no-transcript\" });\n      continue;\n    }\n\n    const snapshot = d.readSnapshot(transcript);\n\n    // A fresh real prompt (not the keepalive's own echo) resets the beat\n    // count — the idle stretch it capped is over.\n    if (snapshot.lastRealPrompt && snapshot.lastRealPrompt.key !== entry.lastRealPromptKey) {\n      entry.lastRealPromptKey = snapshot.lastRealPrompt.key;\n      if (snapshot.lastRealPrompt.text !== config.prompt) entry.beats = 0;\n      changed = true;\n    }\n\n    const mtime = d.mtimeMs(transcript);\n    const idleMin = mtime === null ? null : (now.getTime() - mtime) / 60_000;\n\n    const skip: string | null =\n      mtime === null || idleMin === null\n        ? \"no-mtime\"\n        : idleMin < config.idleMinutes\n          ? `idle:${idleMin.toFixed(1)}min`\n          : !isWithinActiveHours(now, config.activeHours)\n            ? \"hours\"\n            : snapshot.context === null || snapshot.context < config.minContextTokens\n              ? \"context\"\n              : snapshot.midTurn\n                ? \"mid-turn\"\n                : entry.beats >= config.maxBeats\n                  ? \"max-beats\"\n                  : null;\n\n    const ledgerBase = {\n      session: sessionId,\n      pane: paneId,\n      idle_min: idleMin === null ? null : idleMin.toFixed(1),\n      context: snapshot.context,\n    };\n\n    if (skip) {\n      d.ledger({ ...ledgerBase, beat: `${entry.beats}/${config.maxBeats}`, result: `skipped:${skip}` });\n      results.push({ sessionId, result: `skipped:${skip}` });\n    } else {\n      const sent = await d.sendBeat(paneId, config.prompt);\n      if (sent.ok) {\n        entry.beats += 1;\n        entry.lastBeatAt = now.toISOString();\n        changed = true;\n        d.ledger({ ...ledgerBase, beat: `${entry.beats}/${config.maxBeats}`, result: \"sent\" });\n        results.push({ sessionId, result: \"sent\" });\n      } else {\n        d.ledger({ ...ledgerBase, beat: `${entry.beats}/${config.maxBeats}`, result: \"skipped:send-failed\" });\n        results.push({ sessionId, result: \"skipped:send-failed\" });\n      }\n    }\n\n    if (changed) {\n      state[sessionId] = entry;\n      anyChanged = true;\n    }\n  }\n\n  if (anyChanged) d.saveState(state);\n  return results;\n}\n\n"],"mappings":"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"}