import type { Channel } from "./channels.js"; /** * Kinship between channels: which agent launched which, and what a parent is * told about its children. Pure and dependency-free so it can be unit-tested * without a server — the same split `crons.ts` uses, and deliberate: the hub is * already large enough. */ /** * How deep a parent chain may go. A notification can trigger a spawn which * triggers another notification, so without a ceiling this is a cascade that * empties the subscription overnight. The pace guard cannot save us here: it * blocks one prompt at a time, never a chain. */ export declare const MAX_LINK_DEPTH = 4; /** How many children one parent may hold, for the same reason. */ export declare const MAX_FANOUT = 12; export type LinkRefusal = "self" | "cycle" | "unknown-parent" | "too-deep" | "too-many-children"; /** * How many ancestors `sessionId` has (0 for a root, or for an unknown id). * `seen` is not paranoia: a hand-edited store can contain a loop, and this must * terminate on it rather than spin. */ export declare function chainDepth(channels: readonly Channel[], sessionId: string): number; /** The channels whose parent is `sessionId` (direct children only). */ export declare function childrenOf(channels: readonly Channel[], sessionId: string): Channel[]; /** * Why linking `child` under `parent` must be refused, or null if it is allowed. * `parent === null` detaches, which is always allowed. * * Every refusal is EXPLICIT. An unknown parent is refused, not a field we * quietly drop — that is the lesson `resolveCronId` already carries (invariant * 17), where a delete answered `{ok:true}` while deleting nothing. Here the * silent version is worse: a parent would believe it will be notified and wait * for a child it was never linked to. */ export declare function linkRefusal(channels: readonly Channel[], child: string, parent: string | null): LinkRefusal | null; /** * Prefix carried by a notification prompt. It reaches the transcript as an * ordinary user message, so without a marker it reads as something the human * typed — and comes back on every web reload and Telegram backfill, which is * exactly the bug `CRON_PROMPT_MARK` exists to prevent. Twin of that one. */ export declare const AGENT_PROMPT_MARK = "\uD83E\uDD16 [agent]"; export declare function markAgentPrompt(text: string): string; export declare function isAgentPrompt(text: string): boolean; /** What happened to a child, as its parent will be told it. */ export interface ChildReport { /** The child channel's display name, or its short id when unnamed. */ name: string; sessionId: string; kind: "done" | "dialog" | "exited" | "timeout"; /** The child's own last assistant text block — what it wrote to be read. */ summary?: string; /** The child's whole answer was the silence placeholder. Carried explicitly * because the tail drops that block, so `summary` can never hold it. */ silent?: boolean; /** kind === "dialog": the pending question and its options. */ question?: string; options?: string[]; branch?: string | null; } /** * The prompt a parent receives about one child. * * Deliberately small. The parent is almost always the LARGEST session in the * tree, which makes it the worst place to pour volume into: measured on this * repo's transcripts, a call re-reads ~359k tokens of prefix, i.e. ~36k * effective per wake. So this carries the child's own summary plus POINTERS, * never the diff — the parent fetches that if it decides it needs one. */ export declare function notificationText(r: ChildReport, port: number): string;