/** * conversation-rewind-host.ts, a surface answering the daemon's questions * about a conversation only it is holding. * * ── What was broken ─────────────────────────────────────────────────────── * * Files rewind works from anywhere, because the workspace checkpoint store is * the daemon's. The conversation half is answerable only by the process running * the loop, and once the surfaces became pure clients that process is not the * daemon. The daemon's in-process conversation registry, which nothing outside * the daemon could populate, answered "0 messages to drop" for every session * hosted elsewhere: a confident answer to a question it could not reach. * * This module is the other end of the fix. The surface OFFERS the conversation * it is running; the daemon then asks that surface when a rewind touches it. * * ── Why a poll, and why it is not a poll ────────────────────────────────── * * This is a reverse call: the daemon asking a connected client and awaiting an * answer while a `rewind.plan` call waits on it. The delivery is a `take` that * the surface holds open, a long poll, not a tight loop. With nothing waiting * the call parks for up to the daemon's own ceiling and returns empty; with * work waiting it returns immediately. So the steady state is one open request, * not repeated requests, and an answer starts moving the instant the question * is raised. * * The same call renews the lease. A surface that is polling is a surface that * is alive, so there is no separate keepalive to get out of step with it, and * a crashed surface simply stops being consulted when its lease lapses, with * nobody cleaning up after it. * * ── Answering honestly ──────────────────────────────────────────────────── * * A question about a session this process is NOT holding is answered * `unavailable` with the reason, never zero. That distinction is the whole * point: a real zero and an unreachable conversation look identical as numbers, * and reporting the second as the first is what made the old behaviour a lie * rather than a gap. * * Every failure path resolves. A request that throws while being answered is * answered `unavailable` with the error text rather than left to time out, so * the waiting `rewind.plan` gets a reason instead of a stall. */ import { logger } from '../../utils/index.js'; import type { RewindConversationPort } from '../../rewind/types.js'; import type { DaemonVerbCaller } from './daemon-verbs.js'; export interface ConversationRewindHostOptions { readonly verbs: DaemonVerbCaller; /** The port that answers, this process's own conversations, by session id. */ readonly port: RewindConversationPort; /** Whether this process is actually holding that session's conversation. */ readonly hosts: (sessionId: string) => boolean; /** How this surface names itself in the refusals a person reads. */ readonly label?: string; readonly waitMs?: number; readonly retryBackoffMs?: number; readonly log?: Pick; /** Injectable sleep (tests). */ readonly sleep?: (ms: number) => Promise; } export interface ConversationRewindHostClient { /** * Name the session whose conversation this surface is holding, without * starting the polling loop. The registration itself happens on the first * `pump`, so a caller that wants to drive the cycles deterministically, a * test, or a one-shot check, uses `offer` + `pump` and never has two takes * racing on one host id. */ offer(sessionId: string): void; /** `offer`, plus the loop that keeps taking questions. Idempotent per session. */ start(sessionId: string): void; /** Withdraw the offer and stop. Idempotent; safe to call with nothing running. */ stop(): Promise; /** The host id the daemon issued, once registered. Null before that. */ hostId(): string | null; /** One take-and-answer cycle: register if needed, take, answer each question. */ pump(): Promise; } export declare function createConversationRewindHost(options: ConversationRewindHostOptions): ConversationRewindHostClient; //# sourceMappingURL=conversation-rewind-host.d.ts.map