/** * Research state file — resume mechanism for interrupted research * (tech-plan §3, T07). * * Research costs 4-250 credits per request. A research task runs * asynchronously server-side: POST /research creates it and returns a * `request_id`, then GET /research/{id} polls until completion. If the * CLI exits (Ctrl-C, crash) mid-poll, the task keeps running and * consuming credits. Without a persistence mechanism, the next identical * request would POST a SECOND task — a double charge. * * This module persists `{ requestId, identityHash, createdAt, status }` * to `~/.scoutline/research/.json` so the next invocation of * the same request detects the in-flight task and polls it instead of * creating a new one. The state-hash is deterministic for a given * `{provider, capability, credentialFingerprint, request}` tuple (see * `computeResearchStateHash`). * * Boundary rules (ARCHITECTURE.md §2): * - May import cache-root resolution and normalized errors. * - Must NOT import transport, command presentation, or a Provider * Adapter. * * Resilience contract (tech-plan §3 / G1-G3): * - `write()` uses `{ flag: "wx" }` for atomic creation. A concurrent * invocation that finds the file already exists gets EEXIST and * polls the existing task instead of creating a new one. * - `read()` catches JSON parse errors, deletes the corrupt file, and * returns `null` (treated as absent → new task created). * - `remove()` deletes the file and ignores ENOENT (already gone). */ /** * A single in-flight research task's persisted state. `status` tracks the * last-seen poll status ("pending" or "in_progress"); the poll loop * updates it so a resume after Ctrl-C skips statuses already observed. */ export interface ResearchState { readonly requestId: string; readonly identityHash: string; readonly createdAt: string; readonly status: "pending" | "in_progress"; } /** * Port the Adapter uses to read/write/remove research state. Production * wires {@link createProductionResearchStateFile}; tests inject in-memory * doubles to exercise the lifecycle deterministically without touching * the filesystem. */ export interface ResearchStateFile { read(identityHash: string): Promise; write(identityHash: string, state: ResearchState): Promise; remove(identityHash: string): Promise; } /** * Inputs to the research state hash. `credentialFingerprint` is the full * lowercase SHA-256 hex digest of the active credential (same value used * for the response-cache fingerprint). `request` is the normalized * ResearchRequest whose recursively key-sorted JSON becomes part of the * hash. */ export interface ResearchStateHashInput { readonly provider: string; readonly capability: string; readonly credentialFingerprint: string; readonly request: unknown; } /** * Compute the deterministic state-file identity hash for a research * request (tech-plan §3 / CR3). * * state-hash = SHA-256(recursively-key-sorted-JSON({ * provider, capability, credentialFingerprint, request * })) * * Same canonical approach as `buildProviderCacheKey`'s request hashing, * extended to include provider + capability + credential. Rotating the * API key orphans old state files (correct — the old task belongs to the * old key's billing). The hash never contains a raw credential. */ export declare function computeResearchStateHash(input: ResearchStateHashInput): string; /** * Build a production {@link ResearchStateFile} backed by files under * `~/.scoutline/research/` (via {@link researchStateDir}). One JSON file * per in-flight task, named `.json`. * * - `write()` atomically creates the file with `{ flag: "wx" }`. A * concurrent invocation that finds it exists throws EEXIST; the caller * catches that and polls the existing task. * - `read()` catches JSON parse errors, deletes the corrupt file, and * returns `null`. * - `remove()` deletes the file and ignores ENOENT. */ export declare function createProductionResearchStateFile(): ResearchStateFile; /** * Convenience helper exported for the Adapter and tests: builds an * in-memory {@link ResearchStateFile} that throws EEXIST on a second * write to the same hash, mirroring the production `{ flag: "wx" }` * contract exactly. The adapter's lifecycle must not depend on whether * the state file is disk-backed or memory-backed. */ export declare function createInMemoryResearchStateFile(): ResearchStateFile & { readonly store: Map; }; //# sourceMappingURL=research-state.d.ts.map