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
TypeScript
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