/** SDK-owned platform module. This implementation is maintained in goodvibes-sdk. */ /** Options for configuring the activity logger. */ /** Options for {@link ActivityLogger.flushSync}. */ export interface FlushOptions { /** * Rebuild the log directory if it has vanished, then retry the append. * Defaults to true, which is the behaviour a live logger needs. `dispose()` * passes false so teardown cannot resurrect a directory its owner removed. */ readonly recreateDestination?: boolean | undefined; } export interface ActivityLoggerOptions { /** Rotation threshold in bytes; the live file rotates to `.1` once it reaches this size. */ readonly maxBytes?: number | undefined; /** * Where the logger reports its own failure, exactly once, when it gives up on * a destination. Defaults to `process.stderr`. Injected by tests, which * cannot assert "reported once" against a global stream. */ readonly report?: ((line: string) => void) | undefined; } /** * ActivityLogger, Persistent debug logger for GoodVibes. * Writes to .goodvibes/logs/activity.md * * Entries are batched, flushed when the buffer reaches LOG_BUFFER_MAX or after * LOG_FLUSH_INTERVAL_MS, whichever comes first, so a busy process does not * touch the disk per line. The batched write itself is SYNCHRONOUS. It used to * be `appendFile` with a callback, which meant a write could still be in flight * when the process ended: a daemon handing over or shutting down took its last * lines with it, and the moments a log is dropped are exactly the moments it is * needed. A batch is at most LOG_BUFFER_MAX entries, so the blocking cost is * bounded and paid at most ten times a second. * * Rotation: the live file is size-capped at `maxBytes` (default * LOG_ROTATION_MAX_BYTES). When a flush would carry the file past the cap it * is renamed to `activity.md.1` (a single backup, overwritten each rotation) * and a fresh file is started. The size is tracked with an in-memory byte * counter, seeded once from the existing file at configure() and incremented * per flush, so the hot write path never stats the file per entry. * * Destination loss: a log directory that disappears under a running process * (a temp dir reclaimed at teardown, a workspace moved) is recreated and the * write retried. A destination that cannot be written at all is reported ONCE * and then abandoned, the logger stops accepting entries rather than emitting * `flush error: ENOENT` on a loop into a stream nobody is reading. */ export declare class ActivityLogger { private logPath; private buffer; private flushTimer; /** Bytes in the live file since the last rotation; drives the cheap size check. */ private liveBytes; private maxBytes; private report; /** True once the destination has been declared unwritable and reported. */ private degraded; private disposed; private consecutiveFailures; /** Entries lost to the buffer cap, reported in the log once one lands. */ private droppedEntries; /** * True once a destination has been named. Until then every info/warn/error * in the whole platform accumulates in memory and reaches no file, which is * how a host that forgets `configure()` turns the entire platform mute, a * shipped daemon did exactly that, and an inbound channel surface that * refused to start produced no record anywhere. A host that cannot be sure * it configured the logger asks this and supplies a destination. */ get isConfigured(): boolean; /** * True once the logger has given up on its destination. A host that wants to * know whether its log is real can ask; nothing in the platform is required * to. */ get isDegraded(): boolean; /** * True once `dispose()` has run and before any later `configure()`. * * A disposed logger accepts nothing and writes nothing. Observable because * the alternative, finding out by watching a temp directory come back, is * how this was found in the first place. */ get isDisposed(): boolean; configure(logDir: string, options?: ActivityLoggerOptions): void; /** * Stop tracking this logger for the process-exit flush. For hosts and tests * that create their own instances; the shared singleton lives for the life of * the process. */ dispose(): void; private scheduleFlush; /** * Rotate the live file to `.1` (one backup, overwritten) when it has reached * the size cap. Cheap: acts only on the in-memory byte counter, never stats * per entry. * * A rotation that cannot happen is never reported here. A missing file or * directory is the destination-loss case the append path already handles, and * any other rotation failure simply leaves the current file in place, an * append that cannot rotate is never dropped, so there is nothing a reader * could do with a message about it beyond the one the append path will send * if the write itself then fails. */ private rotateIfNeeded; /** * Write everything buffered, now, on this thread. * * Public because a host with a controlled exit path (a clean stop, a signal * handler, an update handover that hands the port to a new process) must be * able to make its final lines land at a point it chooses rather than trust * that the process happens to stay alive long enough. */ flushSync(options?: FlushOptions): void; /** * One append attempt, with the destination recreated once if it vanished. * * Recreating is the honest response to the case actually observed: the * directory existed when the host named it and was removed underneath a * running process. Recreating restores exactly what the host asked for. It is * attempted once per flush, never in a loop, if the path cannot be a * directory at all, the retry fails and the caller degrades. */ private append; /** * Hold a failed batch for the next attempt, and give up once a destination * has failed MAX_CONSECUTIVE_FLUSH_FAILURES times in a row. * * Giving up is the point. The prior behaviour wrote `[ActivityLogger] flush * error: ENOENT` to stderr on every single flush, once per 100ms for as long * as the process lived, while continuing to accept entries as though they * were being recorded. One report, then stop accepting, is what a reader can * actually act on. */ private recordFailedFlush; /** Drop oldest entries past the cap, remembering how many were lost. */ private enforceBufferCap; private takeDropNotice; private write; info(message: string, data?: Record): void; warn(message: string, data?: Record): void; error(message: string, data?: Record): void; debug(message: string, data?: Record): void; } export declare const logger: ActivityLogger; export declare function configureActivityLogger(logDir: string, options?: ActivityLoggerOptions): void; /** * Make everything the shared logger holds land on disk, now. * * Called by hosts at the exit paths they control. The process-exit hook covers * the ones they do not, but a host that is deliberately handing over should not * depend on a backstop to record why. */ export declare function flushActivityLogSync(): void; /** * Name a destination only if the host has not already named one, and report * whether this call was the one that did it. * * The platform's runtimes are embedded by several hosts, and a host that * forgets `configureActivityLogger` does not get a degraded log, it gets no * log at all, for every component in the process. That is not a defect the * component can detect from the inside, so the long-lived runtimes call this * at start with their own working directory as the fallback. A host that DID * configure a destination keeps it; this never relocates an existing log. */ export declare function ensureActivityLoggerConfigured(logDir: string, options?: ActivityLoggerOptions): boolean; //# sourceMappingURL=logger.d.ts.map