import type { CortexStore } from '../db/store.js'; import { type DigestDeps } from './digest.js'; import { scanTranscriptTail } from './transcript.js'; /** * Ambient-capture spool: hook scripts append one JSON line per tool event * (no Node spawn), and a single flush replays the batch into the store. * * Replayed events get flush-time DB timestamps; the original `ts` orders the * replay and stays available in the spool line. Flushes are expected within * the same turn (Stop hook), at a size threshold, or at the next session * start — close enough that capture-time vs flush-time skew does not matter. */ export interface SpoolEntry { v?: number; ts?: string; seq?: number; tool: 'read' | 'edit' | 'write' | 'cmd' | 'agent' | string; file?: string; lines?: string; cmd?: string; exit?: string; stdout?: string; stderr?: string; desc?: string; /** Subagent identity, written by the hook; absent for primary-session work. */ agent_id?: string; agent_type?: string; /** * The hot path replaced this read's output (Story 4.5, AD-7). * * Typed loosely on purpose: `jq` preserves JSON types, so the hook's * `{subst:1}` arrives as a **number**, not a boolean or a string — the same * hazard this file already documents for `agent_id` and `credit_size`. * Interpreted through `isSubstitutedRead`, never by truthiness. */ subst?: unknown; /** * A zero-result search observed by the hook (FR-12, Story 4.3). * * `tool: 'search'`. `stool` names the searching tool ('grep'); `pattern`, * `sroot`, `sglob`, `stype` carry the matching-relevant parameters exactly * as the tool reported them (`sroot` may be '' — the scope root). `zero`, * `sci` and `sml` are jq-emitted flags and arrive as NUMBERS — interpreted * through `readJsonFlag`, never truthiness, the `subst` hazard again. The * hook emits a search line only when the payload POSITIVELY proved zero * results under a recognized response shape; ambiguity emits nothing. */ stool?: string; pattern?: string; sroot?: string; sglob?: string; stype?: string; sci?: unknown; sml?: unknown; zero?: unknown; /** * This `cmd` was launched into the background (Story 4.3 review round). * * PostToolUse fires at LAUNCH, so a backgrounded build's timestamp orders * *before* a later search while the process keeps writing after it — the * ordered `>=` disqualifier is blind to exactly the writer most likely to * invalidate a search. Any `bg` command in a batch disqualifies every search * in that batch, whatever the order. jq emits it as a number, so it is read * through `readJsonFlag`. */ bg?: unknown; /** * A credit that originated on the hot path (AD-15, FR-8 AC #5). * * The hot path may not open SQLite (AD-2), so a substitution that avoids a * read cannot book its own credit. It emits a spool record carrying its own * evidence instead, and the cold-path flush books it under the same * exactly-once claim as every other spool line. **A lost record is no credit, * never a reconstructed one** — there is no replay from inference, because a * credit Cortex cannot evidence is the thing AC #3 forbids. * * Set `tool: 'credit'`. `credit_size` is bytes for a read, output size for a * command, result count for a search. */ credit_kind?: 'read' | 'command' | 'search'; credit_ref?: string; credit_size?: string; credit_tokens?: string; } export interface SpoolFlushResult { processed: number; skipped: number; /** * Failed commands recorded from the transcript because no hook ever saw them * (FR-14). Reported rather than silent: the synthesis loop is bounded, and a * bound nobody can observe is how a cap becomes a lie about coverage. */ synthesized: number; } export declare function deriveSpoolPath(dir: string): string; /** Node-side append matching what the bash hooks write with `>>`. */ export declare function appendSpoolEntry(dir: string, entry: SpoolEntry): void; export declare function spoolSizeBytes(dir: string): number; /** * Meta key holding what the last transcript scan actually saw (FR-14). * * Read by `cortex doctor`. This feature exists because a capability can be * wired, running and dead with nothing anywhere saying so — `command_failure` * and `test_cycle` had never fired across 4,881 recorded commands and no * surface reported it. Shipping the fix with the same blind spot would be the * same mistake one layer down: a host that renames `transcript_path`, stops * emitting `is_error`, or moves the file would produce silence indistinguishable * from "nothing failed". */ export declare const SCAN_STATUS_KEY = "cmd_outcome_scan"; /** * Claim and replay the spool. Crash-safe: an orphaned `.processing` claim from * an earlier run is consumed first; the live spool is claimed via atomic * rename so concurrent appends land in a fresh spool file. */ export interface SpoolFlushOptions { /** * Treat every read in the batch as refund-ineligible regardless of what the * batch shows. Set by `inject-header`'s leftover flush: those lines are from * an earlier session, the digest is being computed a session boundary later, * and anything at all may have happened in between — including the very * SessionStart activity performing the flush. */ conservativeEligibility?: boolean; /** Test seam only; see `computeFileDigest`. Production omits it. */ deps?: DigestDeps; /** * Host transcript for this session (FR-14, Story 4.4). * * The ONLY place a command's pass/fail is observable. Measured 2026-08-03 by * dumping raw hook stdin: the Bash `PostToolUse` payload carries no exit code * in any form, and a command the host deems FAILED fires no `PostToolUse` at * all. Absent means outcomes are simply not attached — never guessed. */ transcriptPath?: string | null; /** Test seam for the transcript scan. Production omits it. */ scanTranscript?: typeof scanTranscriptTail; } export declare function flushSpool(store: CortexStore, dir: string, sessionId: string, /** * Historical positional seam kept so the four pre-existing callers and tests * compile unchanged; new options travel in the fourth parameter. */ deps?: DigestDeps, options?: SpoolFlushOptions): SpoolFlushResult; //# sourceMappingURL=spool.d.ts.map