import type { ConfigManager } from '../config/manager.js'; import type { PlatformServiceManager } from './service-manager.js'; import { DaemonReceiptStore } from './receipts.js'; import { type LifecycleMarkerIo } from './lifecycle-marker.js'; import { type UpdateFileIo } from '../runtime/self-update.js'; import type { DaemonUpdateStatus } from './update-status.js'; export type { DaemonUpdateStatus } from './update-status.js'; import type { RouteBindingManager } from '../channels/route-manager.js'; import type { DaemonSurfaceDeliveryHelper } from './surface-delivery.js'; /** * The daemon's owner-alert callback: put one line in front of the owner over a * channel that still works. * * It reuses the SAME path a failing channel uses (owner-alert.ts) rather than * adding a second notification mechanism. `null` for the preferred surface * because the subject here is not a channel, the daemon cannot update itself, * or has just rolled itself back to an older build, so no surface earns the * first try and the order is simply the most recently used conversation. */ export declare function createDaemonOwnerAlerter(routeBindings: RouteBindingManager, delivery: Pick): (text: string) => void; export { runDaemonSessionStoreBoot } from './daemon-session-store-boot.js'; /** * The daemon heartbeat watcher: a polling watcher that stamps an ISO * timestamp on the configured heartbeat interval. Registered from start() * only when watchers are enabled; the facade stops it on shutdown. */ export declare function registerDaemonHeartbeatWatcher(watcherRegistry: { registerPollingWatcher(input: { id: string; label: string; source: { id: string; kind: 'watcher'; label: string; enabled: boolean; createdAt: number; updatedAt: number; metadata: Record; }; intervalMs: number; run: () => string; }): void; startWatcher(id: string): void; }, configManager: ConfigManager, onBeat?: () => void): void; /** * Identity of the RUNNING artifact for the auto-update loop. The daemon * facade must never assume the SDK package is the shipped artifact: an * embedding host names its own version (what release tags are compared * against) and, optionally, the executable the swap replaces. Absent, the * embedded default, means the HOST manages updates: the loop stays off, * because comparing the SDK's package version against a host's release tags * is meaningless and the swap would target the wrong binary. */ export interface DaemonUpdateArtifact { /** The running artifact's own version, compared against release tags. */ readonly version: string; /** The executable the verified swap replaces. Defaults to process.execPath. */ readonly execPath?: string | undefined; } export interface DaemonLifecycleRuntimeOptions { readonly configManager: ConfigManager; readonly platformServiceManager: PlatformServiceManager; /** The daemon's real activity signal: true only when NO work is in flight. */ readonly isIdle: () => boolean; /** Absent = host-managed updates (the safe embedded default): no auto-update loop AND no boot promotion. */ readonly updateArtifact?: DaemonUpdateArtifact | undefined; /** Injectable process exit (boot promotion hands over by exiting); tests observe instead of dying. */ readonly exitProcess?: ((code: number) => void) | undefined; /** * The daemon's own orderly stop, run before an update or crash-loop-rollback * restart hands over, so shutdown hooks fire on those restarts instead of * being skipped by a bare exit. Absent = nothing to wind down. */ readonly stopGracefully?: (() => Promise | void) | undefined; /** Injectable marker filesystem; tests drive the crash-loop counter in memory. */ readonly markerIo?: LifecycleMarkerIo | undefined; /** Injectable swap/rename filesystem for the crash-loop rollback. */ readonly rollbackIo?: UpdateFileIo | undefined; /** Injectable clock for receipts and marker stamps. */ readonly now?: (() => number) | undefined; /** Injectable stderr; the crash-loop rollback says what it did before the process hands over. */ readonly stderr?: { write(chunk: string): unknown; } | undefined; /** Boot-promotion idle recheck cadence. Default 60s; floored at 1s. */ readonly promotionRetryMs?: number | undefined; /** * True when this process was told to run out of a home that is NOT the * machine's default, `--daemon-home`, `GOODVIBES_DAEMON_HOME`, a test * harness's temp tree. * * Such a daemon must NEVER adopt the machine's service unit. It happened: * a daemon started from a scratchpad directory found the owner's unit not * running, wrote its own scratchpad `ExecStart` into * `~/.config/systemd/user/goodvibes.service`, and exited. systemd then * supervised the throwaway daemon as the machine's daemon, which is how it * came to be reading the owner's real credentials and long-polling their * real Telegram bot, producing the 409 that killed inbound messages. * * Promotion means "this process should be the machine's daemon forever". * A process running from a directory that may not exist tomorrow cannot * honestly claim that, so the claim is refused rather than merely discouraged. */ readonly hasOverriddenHome?: boolean | undefined; /** * Whether this process is a compiled single-file binary. Only a compiled * binary self-promotes to a supervised service, a source/dev run would write * a unit whose ExecStart is a dev command line that fails on the next boot. * Injectable for tests; defaults to the real process-signal check. */ readonly isCompiledBinary?: (() => boolean) | undefined; /** * Put one line in front of the owner over a channel that still works. The * facade supplies the existing owner-alert path; absent (embedded daemons, * tests) means the ERROR log line is the whole record. */ readonly alertOwner?: ((text: string) => void) | undefined; } export declare class DaemonLifecycleRuntime { private readonly options; private autoUpdater; private store; /** Why the self-update loop is not running. Empty once it is. */ private updateLoopOffReason; private promotionTimer; /** * Whether THIS process has ever reached a fully-started daemon. * * start() is re-entered in-process whenever the control-plane binding * changes, and each re-entry used to record another "start attempt". A * long-running, perfectly healthy daemon could therefore accumulate a * failed-start streak from its own restart cycles and then roll ITSELF back * to the kept previous binary, which is exactly what happened: a daemon up * for ten hours restored an older build over itself, and that older build * could not start at all, leaving the machine with no daemon overnight. * * A failed START means the process never came up. A process that came up * cannot retroactively become one, so once this is set the crash-loop guard * takes no further part in this process's life. */ private reachedFullyStarted; constructor(options: DaemonLifecycleRuntimeOptions); /** Lazily-created persisted store for update/crash receipts. */ receiptStore(): DaemonReceiptStore; private markerPath; /** * Undelivered receipts for a consuming /status read (`?receipts=consume`); * marked delivered once served. The route only calls this when the reader * passed the explicit flag, plain status reads are receipt-neutral. * * Fired announce-once feature lines (web surface URL, first contained run) * ride the same exactly-once feed: they are drained from the announcement * store's pending queue here, so a surface reading receipts at attach * renders them instead of them dead-ending in the daemon log. */ collectReceipts(): readonly { id: string; text: string; at: number; }[]; /** The shared announce-once store (same file the runtime's announcers write). */ private announcementStore; /** * Says it on stderr as well as in the log. * * The activity log buffers and flushes asynchronously, and every branch that * uses this exits the process moments later, so the log line that explains * why is exactly the line that gets discarded. stderr is synchronous and * lands wherever the daemon's output goes (the service journal, a terminal), * which is where an operator looks when a daemon keeps restarting. The same * reasoning already governs the fatal-error path in daemon/cli.ts. */ private announceOnStderr; /** * Put a line in front of the owner AND state it at ERROR. Never throws: an * alert that cannot be delivered must not turn a rollback into a crash. */ private alertOwner; /** Marker call options honoring the injected filesystem/clock seams. */ private markerOptions; /** * The FIRST thing daemon start() does, before anything that can fail: record * this boot as an unconfirmed start attempt, and, when the boots before it * kept failing to reach a fully-started daemon, restore the kept previous * binary instead of repeating the same failure again. * * Returns true when the caller must ABANDON this boot: a rollback restart is * in flight and the process is handing over to the restored binary. * * A daemon with no update-artifact identity (host-managed updates, embedded * daemons, dev runs) does not own the binary on disk: it neither counts its * boots nor restores anything, and always returns false. */ onStarting(): boolean; /** * Restore each installed file from its kept `.previous` copy, leave a * receipt, and hand over to the restored binary. Returns false, this boot * continues on the current build, when there is nothing on disk to restore: * a rollback that did not happen must never be reported as one. */ private rollBackToKeptPrevious; /** The same handover the update swap uses: orderly stop first, then restart onto the restored binary. */ private handOverAfterRollback; /** * After the server is accepting: stamp the lifecycle marker (a previous * marker still saying "running" means the last daemon died without an * orderly stop, one honest crash receipt; reaching here also clears the * failed-start streak and re-arms the automatic rollback), then start the * update loop. */ onStarted(): void; /** * During stop(): halt the update loop; on a real shutdown (not a * config-driven in-process restart cycle) stamp the clean-shutdown marker * so the next start does not record a crash receipt. */ onStopping(restarting: boolean): void; /** * The self-update loop: a first check shortly after boot, then the * configured cadence. The swap only happens at a no-active-work moment: the * busy probe is the session broker's real pending-input count. * * EVERY gate that leaves the loop off logs why. A daemon that quietly never * updates is indistinguishable from one that has nothing to update to, and * the log is the only place an owner can tell those apart. */ private startAutoUpdater; /** * The version an automatic rollback rejected and no successful boot has * cleared, or null. Read from the marker on disk each time it is asked for, * because the rollback that recorded it happened in a PREVIOUS process. */ private rejectedUpdateVersion; /** * What this daemon can say about updating itself, from the state the loop * already keeps. * * EVERY gate that leaves the loop off is named here, not just logged. "The * daemon has not updated" reads identically whether there is nothing to * update to, the loop was never armed, or every check has been failing for a * week, and the difference between those is the whole question. */ updateStatus(): DaemonUpdateStatus; /** * Run one check now rather than waiting for the next interval, and report * what the loop knows afterwards. * * The same tick the schedule runs, not a second code path, so what an * on-demand check does and what the hourly one does cannot diverge. A check * that throws is recorded by the loop exactly as a scheduled failure is, and * the returned status carries it; this never rejects, because "the check * failed" is an answer and the caller asked for the state. */ checkForUpdatesNow(): Promise; /** The service-manager actions shared by the update swap and boot promotion. */ private buildServiceActions; /** * Boot-edge service promotion, independent of updates: a STANDALONE * unsupervised daemon (spawned detached by a surface) installs its service * unit and hands over to the supervised instance at its first idle moment *, a freshly-spawned daemon at the latest version no longer stays * unref()'d forever waiting for an update swap to promote it. Embedded * daemons (no updateArtifact identity) never self-promote: exiting would * kill the host process. service.enabled=false opts out; a platform * without a service manager is left alone. */ private promoteToServiceAtBoot; } //# sourceMappingURL=facade-lifecycle.d.ts.map