export interface MediaInbound { path: string; type: 'image' | 'audio' | 'video' | 'document' | 'sticker'; mimetype?: string; } /** Task 1516 — the dispatcher's operator-provenance marker, present only on a * scheduled inject. A local copy of the shape (this package never cross-imports * platform/ui), mirroring the local {@link MediaInbound} pattern. */ export interface ScheduleProvenance { accountId: string; eventId: string; authoredBy: string[]; createdByTool: string | null; dispatchedAt: string; } export interface InboundPayload { /** The WhatsApp sender this payload is for (E.164 digits or JID). */ senderId: string; /** The message body, verbatim — multi-line content is preserved. */ text: string; /** The WhatsApp message id, for dedup/observability. */ waMessageId: string; /** Task 697 — every downloaded inbound media item this turn. [] when none. * Audio is present but skipped by the weaver (its transcript rode in as * `text`); each non-audio item becomes one Read-this-file note. The local * mirror of platform/ui's MediaRef — this package never cross-imports it. */ media: MediaInbound[]; /** Task 1378 — the inbound's origin: 'user' (a real WhatsApp message) or * 'schedule' (a synthetic inbound injected by the scheduler). Distinct from * the channel *door* (`ChannelSource`, whatsapp/webchat); carried only into * the turn's structured log line. Absent on legacy frames = 'user'. */ source?: 'user' | 'schedule'; /** Task 1486 — the account's active standing-rules block, fetched and attached * by the gateway (already formatted as a `## Standing rules` markdown * section). Prepended above `## Context` so the operator's standing rules ride * every turn even after the long-lived session has compacted. Absent/blank on * a turn with no active rules. */ standingRules?: string; /** Task 1516 — the dispatcher's operator-provenance marker on a scheduled * inject. Rendered as a platform-verified preamble ABOVE `## Context` (and * above the standing-rules block), outside the reframed untrusted payload. * Absent on a real inbound. */ scheduleProvenance?: ScheduleProvenance; /** Task 1897 — the message this inbound replies to, when the sender quoted * one. Rendered as a bracketed note at the TOP of the reframed payload, * inside `## Context` and above the caption: the quoted body is * sender-controlled and belongs in the untrusted payload, never in the * platform preamble. Absent when the sender quoted nothing. */ replyContext?: ReplyContext; } /** Task 1897 — the quote as the WhatsApp extractor resolves it: the stanza id * always, the body and the participant when WhatsApp supplied them. A local * copy of the shape (this package never cross-imports platform/ui), mirroring * the local {@link MediaInbound} pattern. */ export interface ReplyContext { id: string; text?: string; sender?: string; } /** Task 1897 — the quoted body is unbounded sender-controlled input. 500 * characters identify what is being pointed at without letting a long quote * dominate the turn. Exported so tests assert against the cap, not a literal. * The twin constant lives in platform/ui's app/lib/whatsapp/reply-quote.ts. */ export declare const QUOTED_TEXT_CAP = 500; /** * Task 1897 — the bracketed note that names what the sender replied to. Two * forms: with the quoted body when there is one, and a bare sentence when there * is not (a quoted image or document with no caption resolves to no text). * * Byte-identical to `composeReplyNote` in platform/ui's * app/lib/whatsapp/reply-quote.ts, which serves the passive one-shot door. The * two packages never cross-import (the arrangement REFRAME_INSTRUCTION and * MediaInbound already use), so each side hardcodes the strings in its own test * and a drift fails there. */ export declare function composeReplyNote(rc: ReplyContext): string; /** * The standing per-turn admin reframe. Every channel inbound is demoted to * payload under `## Context`; this directive rides under `## Instruction` (see * {@link composeAdminContent}), so the turn becomes a routing question. It * carries six guarantees, each stated once: (1) dispatch the owning specialist — * content-producer for any deliverable artefact, including follow-up edits — else * load the owning skill, else just answer; (2) search skills and tools before * telling the sender something cannot be done; (3) reply-tool cadence — ack, * progress, result; (4) channel visibility — the peer sees only reply-tool text, * so a turn ending without a reply-tool call delivers nothing; (5) deliver files * via SendUserFile; (6) plain English per the plainly skill. * * Compressed under Task 1497 (history: 753/761/805/890/919/980/982). Clauses 4–6 * are recipient-neutral and byte-identical to the webchat and telegram twins' * ADMIN_REFRAME_INSTRUCTION, drift-guarded by a test in each service. Exported so * the unit tests assert the exact wrapped shape. */ export declare const REFRAME_INSTRUCTION: string; /** Task 853 — the two doors a unified (multi-target) session can be bound to. * Task 1516 — 'scheduler' is not a physical door; it labels a scheduler-injected * turn on the wrapped tag so a multi-target reader attributes it to the platform * scheduler, not to an inbound message. */ export type ChannelSource = 'whatsapp' | 'webchat' | 'scheduler'; /** * Task 1516 — render the dispatcher's operator-provenance as a platform-verified * preamble. Every value is dispatcher-stamped (an inbound message cannot produce * this block), so the grounding treats a turn opening with it as an authorised * standing automation. Copy stays operator-clean (no task numbers, repo paths, or * em-dashes) so the outbound redactor never trips on it. */ export declare function composeScheduleMarker(pv: ScheduleProvenance): string; /** Task 753 — the reframe decision for one inbound: the wrapped `content` the * agent receives, and `payloadBytes`, the byte length of the inner payload * (message + woven media notes, excluding the wrapper) for the observability * line. */ export interface ComposedInbound { content: string; payloadBytes: number; /** Task 853 — the door this inbound arrived on. Default 'whatsapp' * (single-target back-compat); the multi-target loop passes the door. */ source: ChannelSource; } /** * Shape an inbound WhatsApp payload as a Claude Code channel notification. * * The event carries ONLY the reframed message text — no `meta`. The sender, * session id, and WhatsApp message id are all held server-side (the reply is * pinned to the server's fixed sender), so none of it needs to reach the * model's context. Two reasons: (1) data minimisation — the operator's phone * number is never handed to Claude; (2) a bare event gives the agent nothing to * narrate, so it acts on the message instead of announcing "incoming message * from +44 …". */ export declare function buildInboundNotification(p: InboundPayload): { method: "notifications/claude/channel"; params: { content: string; }; }; /** * Task 753 — frame the composed payload as two markdown sections so the agent * routes the request instead of authoring it inline: `## Context` carries the * inbound payload, then `## Instruction` carries {@link REFRAME_INSTRUCTION}. * Every channel turn is framed — there is no deterministic trivia-skip (operator * decision): a greeting is handled by the cascade's `direct` branch, not by a * pre-classifier here. Returns the framed `content` plus `payloadBytes` (the * inner payload's byte length) for the channel server's `[reframe]` * observability line. */ export declare function composeAdminContent(p: InboundPayload, source?: ChannelSource): ComposedInbound; /** * Task 853 — the inner source tag for multi-target emissions. The harness * wraps every channel notification as ``, * and the unified server registers as `channel`, so the outer wrapper cannot * name the door (a `source` notification param is ignored — probe-verified). * This inner tag is the door marker readers attribute from. Single-target * servers never call this: their registration key IS the door. */ export declare function wrapSourceTag(composed: ComposedInbound): string; /** * Defence in depth: this server serves exactly one sender. Even though the * gateway only streams that sender's messages, refuse anything else before * it reaches the session. */ export declare function acceptsSender(mySenderId: string, p: { senderId: string; }): boolean; /** * Build the body for POST /wa-channel/reply. The reply tool exposes only * `text` to the model; the senderId is the server's fixed sender (from spawn * env), never trusted from the model, so a compromised turn cannot redirect a * reply to a different WhatsApp recipient. */ export declare function buildReplyPostBody(mySenderId: string, text: string): { senderId: string; text: string; }; /** * Build the body for POST /wa-channel/reply-document — the file-reply the * channel server posts when the agent calls the `reply-document` tool. Like the * text reply, the senderId is the server's fixed sender (from spawn env), never * model-supplied, so a compromised turn cannot redirect a document to a * different recipient. The caption is omitted entirely when absent. */ export declare function buildReplyDocumentPostBody(mySenderId: string, files: string[], caption?: string): { senderId: string; files: string[]; caption?: string; }; /** * Build the body for POST /wa-channel/received — the receipt ack the channel * server sends after it has emitted a drained inbound to the agent. It confirms * the dev-channel side received the message so the gateway can mark it delivered * (op=channel-received) instead of timing out as drain-undelivered. The senderId * is the server's fixed sender, never model-supplied. */ export declare function buildReceivedPostBody(mySenderId: string, waMessageId: string): { senderId: string; waMessageId: string; }; /** * Retry an idempotent POST until it is accepted or the attempt cap is reached. * `attempt` returns whether the POST was accepted; the helper returns on the * first acceptance, otherwise sleeps `delayMs` and retries, up to `attempts` * total. `sleep` is injected so tests run without real timers. Pure of the MCP * SDK, like the other helpers here. */ export declare function postWithRetry(attempt: () => Promise, opts: { attempts: number; delayMs: number; sleep: (ms: number) => Promise; }): Promise; /** * Build the body for POST /wa-channel/turn-end — the turn-end decision the * channel server posts once per turn (Task 832). senderId + sessionId are the * server's fixed values (spawn env), never model-supplied. `replied` is whether * a reply/reply-document tool_use occurred in the turn. `interactive` is true * when the triggering inbound was a real person (source !== 'schedule'), false * for a scheduled/autonomous inject. `finalText` is the trimmed, outbound-guarded * end-turn assistant text — the interactive delivery fallback restored in * Task 1539: for an interactive turn the gateway force-sends it when non-empty * (delivered=fallback), covering both a turn that never called reply and one that * only acked. A scheduled turn ships an empty `finalText` so it stays silent. */ export declare function buildTurnEndPostBody(mySenderId: string, sessionId: string, replied: boolean, interactive: boolean, finalText: string): { senderId: string; sessionId: string; replied: boolean; interactive: boolean; finalText: string; }; /** Map a non-OK /wa-channel/reply response into an actionable MCP error string. * Terminal (logged out / device removed) tells the agent re-pairing is needed * and it must NOT promise an automatic recovery; transient keeps the * underlying reason (retryable). Falls back to the bare status when the body * carries no usable reason. */ export declare function buildReplyError(status: number, body: unknown): string; export declare const INTERNAL_LEAK_PATTERNS: { name: string; re: RegExp; }[]; export interface Redaction { pattern: string; match: string; } export declare function redactInternalLeaks(text: string): { clean: string; redactions: Redaction[]; }; //# sourceMappingURL=notification.d.ts.map