import * as fsp from 'node:fs/promises'; import type { EventBus } from '../kernel/events.js'; import type { SecretScrubber } from '../types/secret-scrubber.js'; import type { FileSnapshot, SessionEvent, SessionMetadata, SessionSummary, SessionWriter } from '../types/session.js'; import type { SessionCheckpointCas } from './session-checkpoint-cas.js'; /** * Append-mode JSONL session writer with batched writes, write serialization, * and enriched summary tracking. * * Extracted from session-store.ts to keep each module focused: this class * owns the per-session write path (append/flush/close/checkpoint/truncate), * while `DefaultSessionStore` owns the store-level read/list/index/delete path. */ export declare class FileSessionWriter implements SessionWriter { readonly id: string; private handle; private readonly startedAt; private readonly meta; private readonly events?; private closed; private closePromise; private manifestFile; private summary; private tokenIn; private tokenOut; private readonly filePath; get transcriptPath(): string | undefined; /** * Lazy session_start/session_resumed init, shared by all appenders. * A single promise (not a boolean) so a second append racing the first * can't push its event into the buffer BEFORE the first append's event — * every appender awaits the same init and resumes in FIFO call order. */ private initPromise; private ensureInit; private readonly resumed; private appendFailCount; private lastAppendWarnAt; private readonly secretScrubber?; private readonly checkpointCas?; private readonly onCloseCb?; /** Implements SessionWriter.traceId — propagated from ContextInit.traceId. */ traceId: string | undefined; private writeBuffer; private writeBufferBytes; private flushTimer; private static readonly FLUSH_INTERVAL_MS; private static readonly FLUSH_SIZE; private static readonly WRITE_BUFFER_MAX_EVENTS; private static readonly WRITE_BUFFER_MAX_BYTES; private bufferOverflowCount; private lastBufferOverflowWarnAt; private eventBytes; private pushWriteBuffer; private writeChain; private flushPromise; /** Enqueue a write on the FIFO chain. Resolves/rejects with that write. */ private enqueueWrite; private iterationCount; private toolCallCount; private toolErrorCount; private toolBreakdown; private fileChangeCount; private compactionCount; private outcome; /** * Scrub secrets out of conversation-turn events before they are observed * for the summary, written to the JSONL log, or surfaced on resume. Only * Conversation events carry free-form user/model text. Snapshot and exact * message-journal normalization also strips transient token-cache fields * even when no scrubber is set. */ private scrubEvent; private pendingFileSnapshots; private pendingFileSnapshotBytes; private static readonly PENDING_FILE_SNAPSHOT_MAX_ENTRIES; private static readonly PENDING_FILE_SNAPSHOT_MAX_BYTES; /** Prompt whose tool work is currently executing. Set by writeCheckpoint. */ private activePromptIndex; /** Tracks open tool_use IDs during the current run to serialize on close for resume. */ private openToolUses; /** * Buffer an event from a synchronous Context callback. ensureInit() starts * by pushing the lifecycle preamble synchronously, so session_start always * precedes observations even though these callbacks cannot await append(). */ private bufferSynchronousEvent; recordFileChange(input: { path: string; action: 'created' | 'modified' | 'deleted'; before: string | null; after: string | null; }): void; recordFileObservation(input: { path: string; hash: string; mtimeMs: number; source: 'user' | 'write'; }): void; recordSideEffect(input: { toolUseId: string; toolName: string; input: Record; outcome?: string | undefined; risk: 'fs.write' | 'shell' | 'package' | 'network' | 'config'; }): void; constructor(id: string, handle: fsp.FileHandle, startedAt: string, meta: Omit, events?: EventBus | undefined, opts?: { resumed?: boolean | undefined; dir?: string | undefined; filePath?: string | undefined; secretScrubber?: SecretScrubber | undefined; checkpointCas?: SessionCheckpointCas | undefined; /** Called on close() with the finalized summary for index/sidecar writes. */ onClose?: ((summary: SessionSummary) => void | Promise) | undefined; }, traceId?: string | undefined); get pendingToolUses(): string[]; private writeSessionStartLazy; append(event: SessionEvent): Promise; appendBatch(events: SessionEvent[]): Promise; /** * Flush buffered events to disk immediately. Critical events * (user_input, llm_response) call this so they survive SIGKILL/crash * instead of sitting in the in-memory buffer for up to 500ms. * * Idempotent — cancels any pending timer, writes whatever has accumulated, * then asks the OS to synchronize the file data before resolving. Even an * empty-buffer flush synchronizes any earlier timer-driven append. */ flush(): Promise; /** * Last-gasp synchronous drain for hard-exit paths (process.exit after * rapid Ctrl+C). The async write chain cannot be awaited when the process * is about to die, but whatever still sits in the in-memory buffer CAN be * saved with a blocking append. Best-effort: an in-flight async write may * be cut off by the exit regardless; errors here are swallowed. */ flushSync(): void; /** Schedule a deferred flush. No-op if a timer is already pending. */ private scheduleFlush; /** * Flush all buffered events to disk as a single appendFile call. * Concurrent callers join one coalesced drain. On failure the drained batch * is prepended to the live buffer, preserving chronology and allowing the * next timer/boundary flush to retry it. Warnings retain the existing * throttled behavior. */ private flushBuffer; private flushBufferOnce; /** * Rebuild every accumulated summary counter by replaying the JSONL as it now * stands on disk. * * Only truncation needs this: the counters are fed by `observeForSummary` as * events go by and cannot un-count. Stream the JSONL so a very long session * does not need a second file-sized allocation during rewind. * * `title` is deliberately re-derived too: the first surviving user_input may * differ once earlier prompts are gone. */ private recomputeSummaryFromDisk; private observeForSummary; close(): Promise; private doClose; writeCheckpoint(promptIndex: number, promptPreview: string): Promise; writeFileSnapshot(promptIndex: number, files: FileSnapshot[]): Promise; /** * Truncate the session file to the checkpoint with the given promptIndex, * removing all events that follow it. Uses a single-pass byte-offset scan * so post-checkpoint content is never read or parsed — O(1) memory instead * of O(N) JSON.parse calls over the full file. */ truncateToCheckpoint(targetPromptIndex: number, revertedFiles?: readonly string[]): Promise; clearSession(): Promise; /** * Write an in-flight marker. The agent loop should call * this at the start of each long-running operation; a matching * `clearInFlightMarker` follows on clean exit. A stale marker * (no end) is what `SessionRecovery.detectStale` looks for. */ writeInFlightMarker(context: string): Promise; /** * Close the in-flight marker. Idempotent in spirit * (you can call it after a successful iteration even if you * didn't open one this round) — but the session log records * every call so postmortem tooling can see "the agent finished * cleanly X times, then died without finishing Y". */ clearInFlightMarker(reason: 'clean' | 'aborted' | 'recovered'): Promise; } //# sourceMappingURL=file-session-writer.d.ts.map