/** * Async-job state file — resume mechanism for interrupted async jobs * (tech-plan §3, T07 / FC-01). * * Some Provider operations run asynchronously server-side: a create call * starts the job and returns a `requestId`, then a poll call checks it * until completion. Tavily Research is the first (POST /research → * GET /research/{id}); Firecrawl async Crawl (FC-04) is the second. If * the CLI exits (Ctrl-C, crash) mid-poll, the job keeps running and * consuming credits/quota. Without a persistence mechanism, the next * identical request would start a SECOND job — a double charge. * * This module persists `{ requestId, identityHash, createdAt, status }` * to `/.json` so the next invocation of the same * request detects the in-flight job and polls it instead of creating a * new one. The state-hash is deterministic for a given * `{provider, capability, credentialFingerprint, request}` tuple (see * {@link computeAsyncJobStateHash}). The state directory is injected by * the caller (the Provider Descriptor resolves it via * `asyncJobStateDir(capability)`); this module never touches the cache * root, keeping it pure and unit-testable. * * Boundary rules (ARCHITECTURE.md §2): * - May import normalized errors (none today) and node builtins only. * - Must NOT import cache-root resolution, 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 job instead of creating a new one. * - `read()` catches JSON parse errors, deletes the corrupt file, and * returns `null` (treated as absent → new job created). * - `remove()` deletes the file and ignores ENOENT (already gone). * * Wire-compat: the persisted `requestId` field name and the `status` * enum are fixed by the on-disk format and the SIGINT reader in * `commands/research.ts`. They are NOT renamed even though the module * is now async-job-generic (critique C1). */ /** * A single in-flight async job'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. * * The `requestId` field name is part of the on-disk wire format and is * deliberately kept even though the module is generic — the SIGINT path * reads `parsed.requestId` off disk synchronously. */ export interface AsyncJobState { readonly requestId: string; readonly identityHash: string; readonly createdAt: string; readonly status: "pending" | "in_progress"; } /** * Port the Adapter uses to read/write/remove async-job state. Production * wires {@link createProductionAsyncJobStateFile}; tests inject in-memory * doubles to exercise the lifecycle deterministically without touching * the filesystem. */ export interface AsyncJobStateFile { read(identityHash: string): Promise; write(identityHash: string, state: AsyncJobState): Promise; remove(identityHash: string): Promise; } /** * Inputs to the async-job 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 * Capability request whose recursively key-sorted JSON becomes part of * the hash. */ export interface AsyncJobStateHashInput { readonly provider: string; readonly capability: string; readonly credentialFingerprint: string; readonly request: unknown; } /** * Compute the deterministic state-file identity hash for an async job * (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 job belongs to the * old key's billing). The hash never contains a raw credential. */ export declare function computeAsyncJobStateHash(input: AsyncJobStateHashInput): string; /** * Fail loud when a Provider Descriptor is built with exactly ONE of the * async-job state knobs injected (#158). This is a DI-programmer error, * not a CLI input error, so it throws a plain `Error` — there is no * exit-code contract and no user remedy beyond fixing the injection * site. Deriving the dir from the file is impossible (in-memory doubles * have no path), and deriving the file from the dir silently re-wires * the state file the injector thought they had replaced, so BOTH * half-pairings reject. * * @param provider provider id for the error message (e.g. `"tavily"`) * @param fileKnob dependency key of the state-file port * @param dirKnob dependency key of the create-lock dir * @param fileInjected whether the file knob was supplied * @param dirInjected whether the dir knob was supplied */ export declare function assertAsyncJobStateKnobPair(provider: string, fileKnob: string, dirKnob: string, fileInjected: boolean, dirInjected: boolean): void; /** * Build a production {@link AsyncJobStateFile} backed by files under the * caller-supplied `dir` (one JSON file per in-flight job, named * `.json`). The caller — typically the Provider Descriptor — * resolves `dir` via `asyncJobStateDir(capability)`; this module performs * no cache-root resolution itself. * * - `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 job. * - `read()` catches JSON parse errors, deletes the corrupt file, and * returns `null`. * - `remove()` deletes the file and ignores ENOENT. */ export declare function createProductionAsyncJobStateFile(dir: string): AsyncJobStateFile; /** * Convenience helper exported for the Adapter and tests: builds an * in-memory {@link AsyncJobStateFile} 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 createInMemoryAsyncJobStateFile(): AsyncJobStateFile & { readonly store: Map; }; //# sourceMappingURL=async-job-state.d.ts.map