UNPKG

framework

Version:

The (AI) Framework: turnkey, zero-config AI orchestration that wraps a coding-agent CLI (Claude Code) as a black box and takes you from an idea to a running app. Vite for AI.

83 lines 4.7 kB
import type { ConversationMessage } from '../conversations.js'; /** * Sending a session's answers back to the Discord channel that asked (#932). * * #680 delivered the inbound half: a message in Discord reaches a run over the control channel. * Nothing carried the answer back, so the bot only ever acknowledged ("Sent to the running * session.") and the reply itself was visible in the dashboard alone. That makes a chat * integration write-only: you can talk to the agent and never hear it. * * The source is the committed conversation (#908), not the event log. `events.jsonl` has no * "agent reply" kind -- `log` carries rendered console lines and `driver` carries raw driver * events, neither of which is the settled answer. A conversation turn is exactly the settled text * the user actually read, which is precisely what belongs in a chat channel. * * Which channel is a binding, not a guess: the bot records `runId -> channelId` when it starts or * messages a run, because that is the only moment the channel is known. A run nobody bound is * simply not mirrored, so a dashboard-started session never posts into a channel that never asked * about it (routing that generally is #606's job, not this). * * Two rules keep it from being a nuisance: * * The baseline is taken when the run is bound, not on the first poll. Adopting the transcript at * bind time is what stops a backlog being replayed into a channel, and taking it at bind rather * than at first poll closes the race where a fast agent answers before the first tick and its * reply is baselined away unseen. * * While the bot is switched off the cursor still advances without posting, the same contract the * notification watchers follow: turning it on starts from now instead of flushing everything said * while it was off. * * Bindings are released when their run stops resolving (#941): `readConversation` answers * `undefined` for a run that no longer exists (archived, or its project removed), and after a few * consecutive misses the poll drops the binding. Without that, every chat-touched run stayed in * the map for the daemon's lifetime and each poll scanned every project's live metas per bound * run — no misposts (an archived run resolves nothing), but per-poll IO that only ever grew. The * misses are counted, not acted on at first sight, so a freshly bound run whose meta is not on * disk yet is not dropped by the race. */ /** Where a run's answers go. */ export interface RunBinding { channelId: string; } /** What a mirror needs from the daemon. */ export interface ReplyMirrorOptions { /** * A bound run's conversation, oldest-first. Anything unreadable should resolve `[]`, not throw. * `undefined` means the run itself no longer resolves (archived / project gone); after * {@link UNBIND_AFTER_MISSES} consecutive misses the mirror drops the binding (#941). */ readConversation: (runId: string) => Promise<ConversationMessage[] | undefined>; /** Post one answer into a channel. Resolves whether it was delivered. */ post: (channelId: string, text: string) => Promise<boolean>; /** Whether the bot should speak, read per poll so the toggle needs no daemon restart. */ enabled?: () => Promise<boolean>; /** Poll cadence, ms. Default 3s: a chat reply that lands a minute late is not a reply. */ intervalMs?: number; onLog?: (message: string) => void; } /** A running mirror. */ export interface DiscordReplyMirror { /** * Start mirroring a run's answers into a channel, adopting whatever it has already said. * Awaitable so the caller can bind *before* handing the run a message, which is what makes the * next reply reliably new. */ bind: (runId: string, channelId: string) => Promise<void>; /** Stop mirroring a run (it ended, or its channel went away). */ unbind: (runId: string) => void; /** Whether a run is currently mirrored. */ isBound: (runId: string) => boolean; /** Run one poll now. Exposed so the daemon and tests can drive it deterministically. */ poll: () => Promise<void>; stop: () => void; } /** * How many consecutive unresolvable polls release a binding (#941). Generous on purpose: a * run bound the instant it started has no live meta on disk until its child process boots and * writes one, and dropping the binding during that window would silently unmirror a live chat. * The cost of waiting longer is only how late a dead binding's poll IO stops. */ export declare const UNBIND_AFTER_MISSES = 10; export declare function startDiscordReplyMirror(opts: ReplyMirrorOptions): DiscordReplyMirror; //# sourceMappingURL=reply-mirror.d.ts.map