import type { BlackboardEngine } from "./blackboard.js"; import type { Archiver } from "./archiver.js"; export declare class PendingProcessor { private readonly twiningDir; private readonly blackboardEngine; private readonly archiver; private readonly archiveRetain; constructor(twiningDir: string, blackboardEngine: BlackboardEngine, archiver: Archiver | null, /** Newest-K retention for queued archive actions (config archive.retain_recent). * Hook-fired sweeps must be bounded like every other automatic sweep (D4) — * this path used to full-board sweep with no retention (review finding). */ archiveRetain?: number); /** * Process all pending posts and actions currently queued. * Returns counts of processed items. Never throws — failures are logged. * * Safe to call repeatedly (startup, and again on a periodic timer): drain * uses a rename-based swap rather than read-then-truncate, so a line * appended by a hook while a drain is in flight is never lost (see * `drainJsonlFile`). */ processPending(): Promise<{ posts_processed: number; actions_processed: number; }>; /** * Backward-compatible name for the startup call site. Identical behavior * to `processPending` — kept so "process on startup" reads clearly at the * call site in server.ts, alongside the periodic-drain call. */ processOnStartup(): Promise<{ posts_processed: number; actions_processed: number; }>; /** * Drain a pending-queue JSONL file, applying `handler` to each parsed * line, and return the number of lines successfully processed. * * Concurrency design: the old implementation read the file, processed it, * then truncated it (`writeFileSync(path, "")`). That truncate wipes * whatever is on disk *at truncate time* — including a line the * subagent-stop hook appended (via unlocked `>>`) between the read and * the truncate. That window would silently LOSE a post. * * Fix: rename the live file to a swap name unique to THIS drain call * (`.processing..`), then read/process/delete the * renamed copy. Appenders use create-if-missing writes (bash `>>`, * `fs.appendFileSync`), so anything appended after the rename lands in a * brand-new file at the original path and is simply picked up by the * *next* drain — never lost. The unique name matters: with a SHARED swap * name, drainer A's cleanup `rmSync` could land between drainer B's * rename and B's read, unlinking B's batch unread. With per-drain names * no drainer ever touches another drainer's swap file, so under * concurrent drains a post can be processed twice (both drainers claim * different batches, or a leftover is recovered while its owner is mid- * crash-recovery) but never lost. At-least-once semantics: duplicates * possible, loss is not. * * Crash recovery: a drainer that renamed but died before deleting leaves * `.processing..` behind. Every drain first scans the * directory for such leftovers and CLAIMS each by renaming it to its own * unique name before reading (claim-by-rename — a loser's rename throws * ENOENT, which is benign: another drainer owns it). */ private drainJsonlFile; /** Swap-file name unique to this drain call — never shared across drainers. */ private makeSwapPath; /** * Read, process, and delete a swap file this drain call owns. * * A line whose handler throws (invalid entry_type from a foreign/older * hook, empty summary — anything post() rejects) or that fails to parse * is DEAD-LETTERED to `.dead.jsonl` (#45), never silently * dropped: the old skip-then-delete-swap made the loss permanent, which * is exactly the "a real finding that fails to flush can be silently * lost" field failure. Dead-letter entries carry the raw line plus the * error text so they are inspectable and re-queueable by hand (append * the `line` values back to the live pending file after fixing them). */ private drainSwappedFile; } //# sourceMappingURL=pending-processor.d.ts.map