import { type MeshAgent, type HookEvent, type HookHandle } from "@cotal-ai/connector-core"; import type { AguiEmitterHolder } from "@cotal-ai/connector-core"; import type { ClaudeEntry } from "./agui-map.js"; export interface ClaudeHandleDeps { /** The session's AG-UI emitter, read lazily — `mcp.ts` assigns it after the handler exists. */ events?: () => AguiEmitterHolder | undefined; } /** The hook side of the connector: the handler plus the delivery callback that commits what it * injected. Both must be wired into {@link startControlServer} — the handler surfaces, the * callback is the only place a peer message is ever acked. */ export interface ClaudeHooks { handle: HookHandle; /** Pass as {@link ControlServerOpts.onReply}. */ onReply: (ev: HookEvent, delivered: boolean) => void; } /** * Claude Code lifecycle events → presence + (on inject-capable events) queued peer messages. * * Two properties keep an unattended peer answering: * * **Presence never gates delivery.** `setStatus`/`setAttention` are broker round-trips that throw * while the endpoint is mid-reconnect ({@link MeshAgent.setStatus} calls `assertConnected`). They * used to sit inside the same try/catch as the delivery work that followed them, so a single failed * presence write skipped the injection on `UserPromptSubmit` and — worse — the `Stop` wake flush, * leaving held messages with nothing left to deliver them. Presence is observability; the wake path * is the product, so presence failures are swallowed here and nothing is sequenced behind them. * * **Peer messages are surfaced, then committed on handoff.** The injection is built with * {@link MeshAgent.peekInbox} and the ids are remembered, not acked. A hook reply still has to reach * the runtime through the relay (which abandons the exchange after 2s) and the ids are committed * only once {@link ClaudeHooks.onReply} says it did. Acking at format time was silent loss: the * message was already in `handledIds`, so its durable redelivery was acked and discarded on arrival * and no retry could ever surface it — the peer just never replied. * * **This is a deliberate choice of at-least-once over at-most-once.** The delivery verdict is not * perfectly two-sided, so pick which way it errs: * • reply landed, confirmation lost ⇒ the batch is surfaced again and the model reads it twice; * • reply lost, treated as delivered ⇒ the message is buried and the peer never answers. * The first costs a duplicate injection (labelled — see {@link REPEAT_NOTE}); the second costs the * workflow. Do not "fix" the duplicate by committing optimistically: that is the burial this whole * file exists to prevent. * * Deferring the ack to turn completion (the OpenCode connector's `ackSurfaced` on `session.idle`) * does NOT substitute for this. `Stop` fires whether or not the reply survived, so it would commit a * batch the model never saw — the same loss, later. OpenCode can bind its ack to the turn because * `drive()` OWNS delivery; this connector only hands a reply off, so the ack binds to the handoff. */ export declare function createClaudeHandle(deps?: ClaudeHandleDeps): ClaudeHooks; /** One `claude/channel` push. A rejection is surfaced to the caller. */ export type ChannelNotify = (params: { content: string; meta: Record | { kind: string; }; }) => Promise; export interface WakePolicy { /** Flip once the MCP handshake confirms the client speaks `claude/channel`. */ setChannelActive(active: boolean): void; /** Teardown: stop the retry timer. */ stop(): void; } /** * The push side of the wake path: turn mesh events into `claude/channel` notifications. * * A nudge only ever *wakes* a turn — the body is surfaced by the hook handler above (or by an * explicit `cotal_inbox` pull). It stays gated on a *mutable* `channelActive` flag (flipped true * only after the MCP handshake confirms the client speaks claude/channel). If it fires before * then it simply no-ops; the false-to-true activation reconciles the remembered mention first, * otherwise one buffered wake. A focus @mention needs that reconcile because its body was already * ack-dropped at ingest (not buffered), so there is no local copy or durable redelivery to wake the * session later. * * **A rejected push is retried.** The notification can fail (a closed or wedged stdio pipe), and it * is the ONLY thing that wakes an idle session: no later hook fires on its own, so a dropped nudge * used to mean silence until a human typed. A bounded backoff re-nudges while anything is still * pending, mirroring the OpenCode connector's `scheduleErrorRetry`. It is driven off * {@link MeshAgent.pendingWake}, so it stops as soon as the batch is delivered and committed, and * it never wakes for held ambient the agent's attention mode says to hold. */ export declare function createWakePolicy(agent: MeshAgent, notify: ChannelNotify, log?: (msg: string) => void): WakePolicy; //# sourceMappingURL=hooks.d.ts.map