import { EventEmitter } from "node:events"; import { CotalEndpoint, type EpAttributedReply, type RegistryEndpointsInput, type RegistryEndpointsOutput, type ControlReply, type Presence, type PresenceCondition, type PresenceStatus, type AttentionMode, type ChannelMode, type CotalMessage, type InlinePersona } from "@cotal-ai/core"; import { type AgentConfig } from "./config.js"; export type { AttentionMode, ChannelMode }; /** Client-side floor for a spawn action's submit + follow. The manager acceptance carries the exact * connector-selected readiness budget, and core extends the follow through that budget plus * delivery margin. This floor still outlives the manager's generic 30s default and protects * older responders whose acceptance predates that field. */ export declare const SPAWN_TIMEOUT_MS = 40000; /** Grace for a mesh op issued while the link is still coming up. `start()` deliberately returns * immediately so the connector's MCP surface boots while the broker is absent — which means a * host that auto-submits a prompt (`cotal spawn --prompt`) can call a mesh tool a second or two * after launch, several seconds before the first bind lands. Failing that call on the spot * reports "the mesh is down" about a mesh that is merely still connecting, and the agent then * repeats it to the user as fact. Waiting the window out is the honest answer; the bound is what * keeps a genuinely dead broker from becoming a hang instead of an error. */ export declare const CONNECT_GRACE_MS = 10000; /** Whether the configured broker is one the caller could actually start themselves. Matches the * host of a `nats://`/`ws://`/`wss://` server string; anything else (a hosted mesh, a LAN box) is * someone else's process. */ export declare function isLocalServer(servers: string): boolean; /** The card key that says whether this session arms its event plane. Namespaced, because SPEC §11 * reserves bare meta keys to the spec. */ export declare const EVENTS_META_KEY = "ai.cotal.events"; /** Connector-owned construction options, never read from an agent file. */ export interface MeshAgentOptions { /** The connector process directory is not this session's (one process hosts several sessions), * so `meta.cwd` stays unknown until the harness reports it through {@link MeshAgent.setCwd}. */ withholdProcessCwd?: boolean; /** A once task this seat pulled and did not claim within {@link ONCE_HANDOFF_MS} is sealed and * reported. For a connector whose wake for the task is a push it cannot confirm, so a dead push * cannot hold a served task. */ onceClaimBound?: boolean; } /** Host-authorized participation for one native personal connection. The connector validates the * identity fields against the live connection before arming its existing self-only turn relay. */ export interface NativeTurnParticipationLease { owner: string; actor: string; lifecycleUid: string; name: string; managerInstanceId: string; expiresAt: number; } /** A message that has arrived for us, normalized for the agent to read. */ export interface InboxItem { id: string; /** Opaque per-delivery RECEIVE key (#624): the address a host uses to drain/ack THIS buffered * delivery. It is the wire `id` for every message that carries one; a message whose `id` is the * empty string gets a minted key, because an empty id is never a dedup key and never a * selectable one. It is NOT wire identity and NOT dedup authority: nothing coalesces on it, so * a redelivered copy of an id-less message mints its own key and surfaces again (at-least-once, * the disclosed cost). It exists so the exact-id drains and in-flight protection can select the * DELIVERY rather than an id value that two distinct messages share. */ recvKey: string; ts: number; fromId: string; fromName: string; fromRole?: string; kind: "channel" | "dm" | "anycast"; /** Set when kind === "channel". */ channel?: string; /** Set when kind === "anycast" (the role addressed). */ service?: string; /** Lowercased names called out on a channel message (priority hint). */ mentions?: string[]; /** True iff this message mentions us by name — computed once, here. Drives high-priority wake. */ mentionsMe: boolean; /** True iff this is backfilled history (a "catching up" block on join), not a live message. */ historical: boolean; text: string; replyTo?: string; contextId?: string; /** How many times the broker has delivered this copy, 1 on the first; absent on a live copy. * Broker-derived, never read from the payload: above 1, an earlier delivery of this message went * unacked past its ack wait and may have run, here or on another reader. */ deliveryCount?: number; } /** A bounded, non-consuming view of the currently buffered inbox for presentation-only surfaces. */ export interface InboxSnapshot { /** Total buffered deliveries, including rows beyond {@link items}. */ unread: number; /** Oldest buffered rows, copied without receive/ack handles. */ items: InboxItem[]; } /** Reply correlation for outgoing messages (SPEC §5): `contextId` is the asker's conversation id, * `replyTo` the id of the message being answered. */ export interface Correlation { contextId?: string; replyTo?: string; /** The peer whose conversation the call runs in, for a caller with its own `contextId`: a DM to * that peer answers it, and a DM to any other peer is a new question. */ peerId?: string; } /** Where a session's focus-mode recall has been read to: a timestamp plus the id that breaks its ties. */ export interface RecallMark { ts: number; id: string; } /** Total order on {@link RecallMark}: by time, then by id, so items sharing a millisecond still queue. */ export declare function afterRecallMark(a: RecallMark, b: RecallMark): boolean; /** How many pending messages one session buffers before the overflow valve in * {@link MeshAgent.buffer} evicts. Exported so the overflow smokes read the bound they exercise. */ export declare const MAX_INBOX = 200; export type InboxScope = "all" | "automatic" | "pull-only"; export interface ExactDrainResult { items: InboxItem[]; missingKeys: string[]; } /** * The five states a caller has to tell apart, derived in one place so every consumer agrees. * * `degraded` is the one that matters and the one a single boolean gets wrong: the endpoint is bound * but the socket underneath it is down, so sends queue or fail while the client reconnects. It is * NOT the same as `disconnected`, and it is not a stopped session either. `stopped` is terminal and * is not a fault at all. */ export type ConnectionState = "ready" | "stalled" | "degraded" | "connecting" | "disconnected" | "stopped"; /** * How long a non-empty automatic queue may make no progress before this session stops calling * itself `ready` (#1233). * * Generous on purpose. A healthy seat commits automatic deliveries within seconds of the turn that * carries them, and the incident this bound exists for ran for HOURS: 27 deliveries held 13.8h * behind timed-out soft interrupts, reported `ready` throughout. Ten minutes is far outside normal * turn length and far inside the window in which an operator still has a healthy seat to save. */ export declare const AUTOMATIC_QUEUE_STALL_MS = 600000; /** * A thin, mesh-native agent: a {@link CotalEndpoint} plus a buffered inbox and * name-based peer resolution. This is the shared core behind the MCP server * (and, later, the lifecycle hooks) — it owns the NATS connection and presence. * * Connecting is resilient: {@link start} kicks off a background retry loop so the * MCP server is responsive immediately even if the mesh isn't up yet. * * Emits `"incoming"` (InboxItem) when a message is buffered or an unacked durable copy * redelivers, so a push layer can apply its normal delivery policy again; `"mention-wake"` * (InboxItem) when a `focus`-mode agent is @-mentioned on a channel — the body was * acked-and-dropped (not buffered), so this * only asks the push layer to *wake* the agent to pull it; `"wake"` (no payload) to ask that * layer to wake the session now (the Stop→idle flush of held messages); `"stopping"` (no payload) * once, when {@link MeshAgent.stop} begins, whatever the transport's state; `"error"` (Error) for * endpoint faults. */ export declare class MeshAgent extends EventEmitter { readonly ep: CotalEndpoint; readonly config: AgentConfig; private readonly launchEnv?; private readonly options; private inbox; /** Ids already SURFACED to the model (handled) — bounded, commit-aware dedup ACROSS a drain. The * live↔durable transition window can deliver the two copies of one message far enough apart that the * first is already drained (removed from {@link inbox}) when the second arrives; the pending-inbox * check alone would then re-buffer and double-surface it. Recorded at HANDLE time ({@link drainInbox}), * never at receive time — so a later durable duplicate of an already-handled id is safe to ack (the * logical message was delivered), which is exactly what the removed endpoint-level `firstSeenChat` * got wrong (it acked at receive time, before handling). Two rotating windows bound memory. */ private handledIds; private handledIdsPrev; /** Receive-time classes for overflow-evicted, unhandled channel ambient. Capacity exhaustion * permanently degrades unknown ambient to pull-only for this session rather than risk a late * live/durable copy changing from quiet to automatic. */ private evictedClassifications; /** Surfaced to the host but not yet committed or abandoned, counted per holding frame because * frames overlap. See {@link holdInFlight}. */ private inFlightIds; /** Ack-wait renewal timers of held durable deliveries, by receive key. See {@link keepOwned}. */ private renewals; private classificationUnsafe; /** Terminal receive-time decisions that must survive the live→durable transition: successfully * surfaced pull-only messages and hard drops under muted/focus. Capacity loss degrades the * corresponding whole class fail-closed for the rest of the session. */ private protectedPullOnlyIds; private protectedDropIds; private dropUnsafe; /** How many times the endpoint saw this session's stored copies at their limit (#5404) since a * cotal_inbox reply last carried the notice. */ private _storedCopiesFull; /** The count those stored copies held, from the latest copy that found them full. */ private _storedCopiesLimit; private _connected; /** Raw NATS transport liveness, separate from `_connected` (the full Cotal bind/readiness). */ private _transportConnected; /** Wall-clock time of the latest inbox drain that actually committed at least one delivery. * This is measured only after the backing acknowledgements succeed, never inferred from a read * attempt or from an empty inbox. */ private _lastInboxDrainedAt?; /** Wall-clock time of the latest drain that committed at least one AUTOMATIC delivery. * * Deliberately separate from {@link _lastInboxDrainedAt}, and #1233 is why: on a host-mode * connector the tool inbox and the connector-managed automatic queue are different queues, and * the measured seat was draining the first every few minutes while the second had not moved in * 13.8 hours. A progress mark that counts a pull-only drain as progress would have reported that * seat as making progress, which is the exact lie this field exists to refuse. */ private _lastAutomaticDrainedAt?; /** The latest commit that took the OLDEST automatic delivery then queued (#1526). * * Distinct from {@link _lastAutomaticDrainedAt}, and the distinction is the defect: any automatic * commit used to count, so a seat that kept committing fresh arrivals over a head it could not * deliver reset its own stall clock on every turn and reported `ready` indefinitely, while the * entries at the front of its queue aged past two hours. A queue drains when its head moves; a * queue that serves only its newest arrivals is not draining, it is skipping. */ private _lastAutomaticHeadDrainedAt?; /** Latest connection failure, retained until the endpoint binds so a bounded readiness gate can * explain why an otherwise healthy host never joined the mesh. */ private lastConnectionError?; private endpointNoticeLog; private _status; private _attention; private _recallCursor; /** Recall items stamped ahead of this session's clock that it has already handed over. They are * tracked by id rather than by {@link _recallCursor}, because a sender's clock must not be able to * move this session's mark. Bounded by {@link MAX_AHEAD}. */ private aheadDelivered; /** Per-channel attention overrides — the AUTHORITATIVE runtime state (read by {@link ingest} on * every message). Seeded from the agent-file default; mutated by {@link setChannelMode}; mirrored * to presence for peers. An absent key ⇒ that channel follows the global {@link _attention}. Reset * on restart (rebuilt from config; presence sweep clears the mirror). */ private channelModes; /** The turn relay's seat-side intake: every pulled-and-unyielded run turn, by goal id. Fed by * {@link pollTurns}; surfaced into host context by {@link surfacePendingTurns}; drained by * {@link yieldTurn} (explicit) and the working→idle boundary in {@link setStatus} (automatic * `done`). The pull is also the reconciler: a turn settled elsewhere (deadline, another yield * path) vanishes from `turn-pending`, and the manager's notice for it brings the pull that drops it. */ private activeTurns; /** Deadlines of turns the run settled before this seat yielded them, by goal id, so a late yield * is refused with {@link settledTurnError}. Bounded by {@link MAX_SETTLED_TURNS}. */ private settledTurns; /** Goals whose yield the manager answered since the current pull began. That pull may have * snapshotted a goal before its answer; taking it back would surface the settled turn again, and * the next pull would record it as the run's settlement. */ private yieldedSincePull; /** Set once the launching manager's turn notices are subscribed; the endpoint re-binds the * subscription on every rebuilt connection. */ private turnNoticeArmed; /** The single retry after a pull that failed for a reason that can pass. Unset while pulls succeed. */ private turnRetryTimer?; /** The timed pull for a launching manager whose answers say it sends no turn notices: a manager * from before them. Unset while the manager says it sends them. */ private legacyTurnPoll?; /** The last pull failure this seat reported, so one that keeps failing is said once. */ private pullTrouble?; private turnPollBusy; /** A notice or reconnect arrived while a pull was in flight, so one more pull follows it. */ private turnPullAgain; private _contextId; /** The per-call correlation a {@link withCorrelation} scope binds for the sends inside it. */ private readonly correlation; /** By message id, oldest first: the live DMs and anycasts peers sent us that no DM back has * answered yet (bounded). `pending` marks one a DM in flight is answering; `text` is the start of * its body, to name it when a DM must say which one it answers. */ private readonly unanswered; /** By sender id, least recently heard first: the display name of each peer that sent us a DM or * anycast (bounded). The id is the authenticated sender (the broker forge-locks it on the * subject), so a DM back to it is addressed to a real identity even when that sender has no * roster row, as a one-shot `cotal send` never has. See {@link heldSender}. */ private readonly senders; /** By `contextId`, the questions this seat asked inside a {@link withCorrelation} scope, and whom * they went to (bounded). See {@link answersQuestion}. */ private readonly asked; /** Chat-stream frontier captured when this agent entered `focus` — recall surfaces ambient * published after it ("since you entered focus"). Undefined unless in focus. */ private focusSince?; private enteringFocus; /** The receive-key namespace secret (#624): a per-session random value minted at construction, * never written to any wire or log. Minted receive keys are `${secret}.${seq}`, so they are * DISJOINT from wire ids by construction: an attacker-chosen wire id cannot equal one, so one * verdict can never select two entries through a forged collision. Recognition is FUNCTIONAL * (the key starts with the secret), so it cannot saturate the way a bounded set would: every * minted key stays recognizable for the session's life, with nothing to expire or overflow. */ private readonly recvKeySecret; private recvKeySeq; private focusExcludedIds; /** #662: id-less channel deliveries settled this focus episode, per content digest. Live ingest has * no stream sequence, so content is all an id-less delivery shares with its stream copy, and * identical copies may each have had a different disposition. So recall binds each settled copy * to a stream sequence and hands back only the copies no settled copy is bound to. Replaced, never * cleared, per episode, so a hold from an earlier episode cannot un-count a later one. */ private focusIdless; /** Unbound copies plus bound sequences in {@link focusIdless}, bounded by * {@link FOCUS_EXCLUSION_CAP}. */ private focusIdlessEntries; /** Bumped as each channel's recall read starts: a copy settled before a read that the read could * not bind is behind the focus frontier or out of retention, and no later read will see it. */ private idlessEpoch; /** Recall runs one at a time, so an older read can never settle over a newer one (#662). */ private recallLock; /** Bumped each time the transport goes down, so a settled copy's frontier read can tell whether * the connection stayed up from the copy's arrival to the answer (#662). */ private transportDrops; private focusRecallUnsafeChannels; private _stopping; /** Presence writes go out IN CALL ORDER (#636, #2055). setStatus is two awaited puts and every * connector fires it without awaiting, so two overlapping calls interleave their puts and the * record that lands last is whichever call's second put finished last, not the call made last; * at a driven turn's end the measured order was working, idle, working, and the seat then read * working forever. Every write below runs its whole body through this chain, so the later * call's puts start only after the earlier call's have settled. A rejected write must not * poison the chain: the tail is swallowed and the next write still runs. */ private presenceChain; /** Host lifecycle status is independent of presence-only updates such as cotal_status. */ private turnStatus; /** An explicit env scopes launch metadata and bearer subprocesses. Omitted preserves process inheritance. */ constructor(config: AgentConfig, env?: NodeJS.ProcessEnv, options?: MeshAgentOptions); get id(): string; get connected(): boolean; /** Whether this session's current NATS transport is live, independent of full endpoint readiness. */ get transportConnected(): boolean; /** Current connection failure, including repeated presence writes refused on a live transport. * A successful write clears that diagnosis. Pre-bind failures survive stop for post-mortem use. */ get connectionIssue(): string | undefined; /** #1356: the space's presence bucket has been refusing writes since this time, or `undefined`. * * The endpoint can remain bound with a live transport while JetStream refuses its writes. * Two consecutive failures make the session degraded; a successful write clears the record. * * Presence writes can fail alongside other publishes. See CotalEndpoint.presenceWriteFailure. */ get presenceWriteFailure(): { since: number; forMs: number; error?: string; bucket: string; consecutiveFailures: number; stuck: boolean; } | undefined; /** The latest successful, non-empty inbox drain in this session. */ get lastInboxDrainedAt(): number | undefined; /** The latest drain that committed at least one AUTOMATIC (connector-managed) delivery. */ get lastAutomaticDrainedAt(): number | undefined; /** The latest drain that committed the automatic delivery that was then at the FRONT of the queue * (#1526). This is the fact {@link automaticQueueStalledForMs} measures progress by; the looser * {@link lastAutomaticDrainedAt} is still reported beside it, because the two disagreeing is * precisely the shape of a wedged head being served around. */ get lastAutomaticHeadDrainedAt(): number | undefined; /** The receive key of the oldest still-queued automatic delivery: the queue's head, and the entry * whose commit counts as progress. */ private oldestAutomaticKey; /** Record head progress when this batch takes the current head of the automatic queue. Call * BEFORE the batch leaves {@link inbox}, since the head is read from the live queue. */ private noteAutomaticHeadProgress; /** * How long a non-empty automatic queue has been making no progress, or `undefined`. * * PROGRESS AT THE HEAD, not depth and not throughput. The queue is not stalled because it is deep; * it is stalled because the entries at the front of it are not coming off. The clock therefore * starts at the later of the arrival of the oldest still-queued delivery and the last commit that * took the queue's head, so a seat that is steadily draining keeps resetting it however busy it * is, and a seat that has not moved its head since that queue formed accrues from the moment it * formed. * * Keyed on the HEAD rather than on any automatic commit (#1526). The reported session held 96 * deliveries with the oldest over two hours old and reported `ready` throughout: it was committing * fresh arrivals, and under a looser measure each of those reset the clock for the backlog behind * them. A seat that serves only its newest traffic is skipping its queue, not draining it, and the * messages that are actually undelivered are the ones the measure must speak for. * * Measured from the oldest ARRIVAL rather than from session start so a session that never drained * anything is still measurable — that is precisely the shape of a seat whose first soft interrupt * timed out. */ automaticQueueStalledForMs(now?: number): number | undefined; /** Whether {@link stop} has been called. Terminal, and never cleared: a stopped session does not * serve again. This is the ONLY way to tell a deliberate shutdown from a lost connection, because * `stop()` clears readiness and transport together, so those two read identically in both cases. */ get stopping(): boolean; /** How many times this session's stored copies reached their limit since a cotal_inbox reply last * said so, so older channel posts may have been dropped before they reached it (#5404). Zero when * there is nothing to say. */ get storedCopiesFull(): number; /** The count the stored copies held when they last reached it, from the copy that said so. */ get storedCopiesLimit(): number; /** A reply carrying the stored-copy notice went out, read when the count was `seen`. Clear what * it covered; a limit seen while that reply was being built stays for the next one. */ clearStoredCopiesFull(seen: number): void; /** Session-local channel degradation, independent of transport liveness. */ get channelDeliveryIssue(): string | undefined; /** The connection, write-health and channel-delivery facts combined. Repeated presence write * failures or exhausted drop tracking degrade a bound session even with a live transport. * Every combination maps, so a caller never has * to guess what an unlisted pair means, and a caller that disagrees with this reading can still * read {@link connected}, {@link transportConnected} and {@link stopping} directly. * * `stalled` is the fourth fact and it is not about the connection at all — see * {@link automaticQueueStalledForMs}. It is reported HERE, ahead of `ready`, because #1233 was * not a caller misreading the facts: a seat holding 27 undeliverable messages for 13.8 hours * answered this question with `ready`, and every operator who asked it was told the seat was * fine. A status that reads healthy while the thing it describes is not happening is the defect, * so the one word a caller acts on has to move. It is ordered below `degraded` deliberately: a * dead socket is the more specific and more actionable fault, and reporting the queue symptom * over its own cause would hide it. */ get connectionState(): ConnectionState; /** Wait for the endpoint's real post-bind connection signal. `start()` deliberately stays * background for connectors whose MCP surface must boot while the broker is absent; a host that * advertises mesh readiness uses this bounded gate before making that claim. */ waitUntilConnected(timeoutMs?: number): Promise; /** Correlates outgoing messages to the host agent's current context/window. */ setContextId(contextId: string | undefined): void; /** Run `fn` with a per-call correlation for the sends it makes. A `replyTo` names the message a * DM answers unless the DM names one itself, and `contextId` is then that message's * conversation, copied from the message when the scope omits it. Without a `replyTo`, * `contextId` is the caller's own conversation: a send, a question DM or an anycast carries it * ahead of the agent-wide one from {@link setContextId}, and {@link answersQuestion} recognizes * a DM back that copies it. A connector that serves several host sessions from one seat uses it, * because one agent-wide context id cannot tell two concurrent sessions apart. */ withCorrelation(correlation: Correlation, fn: () => T): T; /** Whether `item` is a live DM answering a question this seat asked inside a * {@link withCorrelation} scope: it copies that question's `contextId`, and its authenticated * sender is the peer the question went to, or has the role an anycast question went to. A * `contextId` is a string any peer can set, so the copy alone proves nothing. */ answersQuestion(item: InboxItem): boolean; /** The correlation a send carries, and the question it asks (a `contextId` the caller's scope * owns, which {@link recordQuestion} remembers). `answered` is the message a DM answers: a reply * copies its `contextId`, which belongs to the asker, and names it in `replyTo` (SPEC §5). */ private stamp; /** Remember a question this seat asked, so {@link answersQuestion} can recognize its answer. */ private recordQuestion; /** The unanswered message a DM to `peerId` answers. `replyTo` names it, and must be a message * from that peer that is still waiting for an answer. Otherwise the scope's `replyTo` names it if * it is that peer's, else it is the oldest one from that peer no other DM in flight is answering. * The oldest is taken only while all of that peer's waiting messages, including those a DM in * flight is answering, share one conversation (`contextId`): across several, a guess could put * the answer in the wrong conversation, so the DM is refused with the list to choose from. A * caller with its own `contextId` answers only in the conversation its scope names. */ private answering; /** Begin connecting with background retry. Resolves after the first completed mesh join. */ start(retryMs?: number): Promise; private consumerClaim?; private connectFlight?; private connectLoop; private stopFlight?; stop(): Promise; private stopOnce; /** Manual reconnect: tear down the mesh connection and rebuild it in-process, WITHOUT * stopping the agent (the recovery path, so it does NOT assert connected). Delegates to * {@link CotalEndpoint.reconnect}, which is serialized with the self-heal supervisor and * interruptible. Returns a one-line status for the caller to surface (e.g. the * cotal_reconnect tool → TUI); on failure the endpoint keeps retrying in the background. */ reconnect(): Promise<{ ok: boolean; message: string; }>; private ingest; private buffer; private rememberEvicted; private excludeFromFocus; /** #662: the tally for one more settled or handed-back copy of an id-less message; the caller adds * the copy. At the bound, the channel's recall is reported incomplete rather than risk handing * back a copy it no longer counts. */ private reserveIdless; /** #662: add one settled copy, bounded by the chat frontier read after it arrived. If that read * fails, or answers only after the transport dropped, nothing bounds the copy, so the channel's * recall is reported incomplete instead. */ private settleIdless; /** #662: forget a tally with nothing left in it, so the map is bounded by the entries it counts. */ private dropEmptyIdless; /** #662: one buffered copy of an id-less message left the inbox unhandled. Only that copy is * un-counted, or the sequence it was bound to freed: an identical copy may have been muted. A copy * no read has bound yet stays in arrival order, marked, so the read binds it to its own stream * copy rather than letting an earlier identical copy take that one. */ private unsettleIdless; /** #662: after a complete read of `channel` that started at `epoch`, a copy settled before it that * it could not bind will not be in a later read either, so it is dropped. The read is the * channel's whole retained window, so a bound sequence it lacks aged out. */ private settleIdlessRead; /** #662: the frontier reads of every copy of `channel` settled so far have answered. */ private idlessReady; /** #662: bind the copies settled before the read that started at `epoch` to its unbound stream * copies `free` (ascending). Identical copies share a sender subject, so they arrive in stream * order, and a frontier, read after its copy arrived, bounds that copy and every copy before it. * Frontier reads can answer out of order, so their sizes say nothing about arrival order. So the * copies bind in arrival order, latest first, each to the latest free stream copy at or below the * lowest frontier from it on, and a stream copy none of them takes (a reconnect gap) stays free. * An evicted copy takes its own stream copy and leaves it free, so recall hands that one back. */ private bindIdless; /** #662: a copy settled after the read that started at `epoch` began may or may not be in it, so it * binds nothing there, and an unbound stream copy at `seq` it could be is neither free nor bound. */ private idlessUnsure; private protectDisposition; /** Normalize a wire message into an {@link InboxItem}. `kind` is the **authenticated** class * from {@link MessageMeta} (subject-derived), never the forgeable payload `to`/`toService`; * core has already normalized `channel` and `toService` from the authenticated chat and svc * subjects. Shared by live ingest and focus recall ({@link recallAmbient}). */ private toInboxItem; /** Return pending messages in stable receive order. Automatic delivery excludes quiet ambient; * pull-only is the explicit cotal_inbox lane. */ peekInbox(scope?: InboxScope): InboxItem[]; /** Mark a surfaced batch as mid-delivery, so the overflow valve will not evict it out from under * the host. Pair with {@link releaseInFlight} on the delivery verdict, whichever way it goes. * * **Counted, not a set.** Hook frames overlap, so the same id is routinely in two open batches at * once; if a hold were a boolean, the first frame's verdict would unprotect ids the second is * still delivering, and an arrival between the two verdicts would ack one — the very loss this * guard exists to stop, reached through the concurrency the per-frame keying deliberately allows. * * **All or nothing, and it says which.** At the {@link MAX_IN_FLIGHT} ceiling this refuses the * whole batch and returns `false`; the caller must then NOT surface it. Protecting only part of a * batch, or protecting none while the caller surfaces anyway, silently reopens exactly the loss * this guard exists to close — the unprotected ids sit in the inbox for the whole handoff window * with the overflow valve free to ack them. Declining to surface costs a deferral: the messages * stay buffered and go out on a later frame, once a verdict releases capacity. */ holdInFlight(ids: readonly string[]): boolean; /** Whether this id is currently protected from the overflow valve — i.e. some frame is mid-delivery * holding it. Read-only observability for the property {@link holdInFlight} establishes. A caller * that needs a batch to actually BE in flight before it acts must wait on this, never on a sleep: * the handoff runs through a real relay process whose latency is not the caller's to predict. */ isInFlight(id: string): boolean; /** One frame's verdict is in (either way). The id is ordinary backlog again only once EVERY frame * holding it has reported — a release from one must not speak for another still in flight. */ releaseInFlight(ids: readonly string[]): void; private own; /** Keep a held durable delivery with this reader for as long as the hold lasts, by restarting its * ack wait at half the wait. Only a HELD delivery is renewed: a queued one keeps the broker's * timer, so a reader busy with other work does not sit on a role request another reader could * take, and an abandoned one (released) runs out and redelivers. */ private keepOwned; private stopRenewal; /** Restart one copy's ack wait, unless this reader can no longer vouch that it still holds that * copy. JetStream acks by stream sequence rather than by delivery, so a renewal sent after the * wait may have run out (a stalled loop, a link down for longer than the wait) would extend the * copy the broker has since handed to another reader. Renewal stops instead and the copy is the * broker's to redeliver. Nothing is sent while the link is down, because the client buffers a * publish and would replay it after the wait had run out; a handle whose connection was torn * down throws, which is the same lost ownership. */ private renewOwned; /** Return scoped pending messages and ack them — call only when they're actually surfaced. */ drainInbox(limit?: number, scope?: InboxScope): InboxItem[]; /** Ack exact surfaced deliveries without assuming they still form the physical inbox prefix. * Takes RECEIVE keys ({@link InboxItem.recvKey}): the wire id for real messages, a minted key * for id-less ones, and selects by them, so a host that surfaced one empty-id item drains THAT * delivery, never its neighbors: the sweep the raw id produced (every pending empty-id item * acked and marked handled in one call) is closed by construction, not by filtering. Every * requested key whose item is present is acked and (for a real id) marked handled, including an * item overflow-evicted during the turn. */ drainInboxDeliveries(keys: readonly string[]): ExactDrainResult; private commitPending; private inScope; /** Record an id as surfaced/handled, for {@link ingest}'s commit-aware cross-path dedup. Bounded via * two rotating windows: when the live set fills, it becomes the previous window and a fresh one * starts — so memory stays ~2× the cap while the lookup horizon never shrinks below it. */ private markHandled; inboxCount(scope?: InboxScope): number; /** * Read a bounded presentation snapshot without draining, acknowledging, committing, holding or * changing attention. Each item is a shallow copy: reassigning its fields leaves the live inbox * unchanged, but nested values such as `mentions` are shared, so callers must treat items as * read-only. */ inboxSnapshot(limit?: number): InboxSnapshot; /** Local receive time of the oldest still-buffered automatic delivery, if any. */ oldestAutomaticReceivedAt(): number | undefined; /** * How far this session has read the focus-mode channel recall. * * {@link recallAmbient} re-derives the same items from an unchanged frontier on every call, so a * reader that shows only what fits in one response would show the same prefix forever. This mark * moves when a response actually delivered recall items, and it belongs to ONE walk over ONE * frontier: it says how far this focus episode has read, not what the stream still holds, and * {@link setAttention} forgets it whenever the frontier under it changes. * * WHAT THIS MARK DOES NOT OWN, said here so nothing above it claims otherwise. It orders what * {@link recallAmbient} hands over; it does not decide what becomes recallable. A message that only * becomes readable after this mark has passed its timestamp, through late persistence, is below the * watermark and will not be walked to. That is governed by the frontier `recallAmbient` derives from * ({@link chatFrontier} and the focus watermark), not here, and it is the same for any * timestamp-ordered reader of that stream. * * What the mark DOES own is where it can be moved to, and only this session's own traffic may move * it: an item stamped ahead of the local clock is walked by {@link recallAhead}, not by this mark, * so no sender can push it past the messages it has not read yet. */ get recallCursor(): RecallMark; /** * Record the last recall item actually handed to the caller. * * The mark is a PAIR, not a timestamp, because two recall items can share a millisecond: a * timestamp alone either filters the twin out for good, if it advances past both, or re-serves the * one already delivered, if it stops below them. Ordering by `(ts, id)` gives every item a place of * its own, so the next call resumes strictly after the last one delivered. */ noteRecalled(mark: RecallMark): void; /** * Does this recall item claim a time this session has not reached? * * `ts` is stamped by the SENDING endpoint, so it is neither trustworthy nor bounded, and a mark * that walks it is a mark a peer can move. One message stamped far ahead otherwise parks * {@link recallCursor} in the future: every ordinary message after it sorts below the mark, is * filtered out of recall for the rest of the session, and the reply says there is no chatter. That * is one peer suppressing OTHER peers' recall, which makes it a security property and not only a * clock one, and the defence has to hold when the field is chosen rather than merely wrong. * * So the walk splits. An item at or behind this session's clock is ordered by its timestamp and * moves the mark. An item ahead of it is not ordered at all and NEVER moves the mark: it is handed * over once and remembered by id ({@link noteRecalledAhead}), so it neither leads the walk nor * comes back on the next call. * * THE LANES SWAP MEMBERSHIP, and the record is what keeps them honest with each other. An item * stamped just ahead of the clock crosses into the ordered lane the moment the clock passes it, * arriving above a mark that never moved for it, so the reader has to consult * {@link recallAheadSeen} there too or hand it over a second time. That crossing is the one part of * this mechanism that cannot be staged by choosing timestamps, so its cell waits for real time * rather than reasoning about it. */ recallAhead(item: { ts: number; }): boolean; /** Has this future-stamped recall item already been handed over in this session? */ recallAheadSeen(id: string): boolean; /** * How many more future-stamped recall items this session will take responsibility for. * * Exactness costs memory, and that memory is what a flood of forged stamps would grow, so it is * bounded. A caller must not hand over what it cannot record, or it will hand it over again on * every call forever; past this bound it has to say so instead. Note where the cost lands: an item * at or behind the clock never enters this set, so a peer that spends the whole bound spends it on * ITS OWN messages and cannot use it to silence anyone else's. */ recallAheadRoom(): number; /** Record that a future-stamped recall item was actually handed to the caller. */ noteRecalledAhead(id: string): void; /** * Forget how far a recall walk had read, because the thing it was walking is gone. * * The mark is session-local, which is a longer life than it can honestly carry: it describes a * position in ONE walk over ONE frontier, and {@link setAttention} both drops the frontier on the * way out of focus and captures a new one on the way in. Measured before this reset, a mark left * over from an earlier focus episode filtered a new episode's messages out of recall whenever they * were stamped behind it, which a lagging or a chosen clock produces. The focus watermark was * already cleared here; the mark that walks it was not. */ private resetRecallWalk; /** Buffered receive-time lane for one delivery, addressed by its receive key. Undefined means it * is no longer pending. */ inboxScope(key: string): Exclude | undefined; /** Count of automatic messages that count as directed for a wake decision: real dm/anycast * (authenticated kind) or a live channel @mention. Pull-only history never counts, including * mentions retained while focused. The Stop→idle flush uses this in `dnd`/`focus`. */ directedPendingCount(): number; /** Buffered items that should WAKE a Stop→idle flush — the mode-and-channel-aware predicate the * connectors use instead of branching on attention themselves: * - directed (dm/anycast) or a live @mention → always (a quiet live @mention wakes; muted never buffers); * - NORMAL automatic ambient → only under global `open` (today's behavior); * - receive-time pull-only ambient → never. * Subsumes {@link directedPendingCount}: in `dnd`/`focus` (no override) the open term is false, so it * equals the directed count; in `open` it adds normal ambient but excludes quiet-channel ambient. * `skip` leaves out items the caller has already woken the session for. */ pendingWake(skip?: (item: InboxItem) => boolean): number; /** The inbox half of {@link pendingWake}: buffered items that should wake the agent, without run * turns. A surface that cannot surface a run turn, such as the remote gateway, waits on this, so * a turn it can never clear does not end every wait at once. */ inboxWake(skip?: (item: InboxItem) => boolean): number; /** Ask any push layer (the channel) to wake the session now — used by the Stop→idle flush * to deliver a batch of held messages. Emits `"wake"`; a no-op if nothing listens. Never acks * or drains. Ack sites are now two: {@link drainInbox} (surfaced items) and the focus ingest * ack-drop (ambient/@mentions a focus agent chose not to receive into context). */ requestWake(): void; /** This agent's global attention mode. Authoritative here; mirrored to presence (advisory) so peers * can see it. Delivery never reads it back from presence — local state wins. */ get attention(): AttentionMode; /** This agent's per-channel override for `channel` (undefined ⇒ follow the global mode). */ channelMode(channel?: string): ChannelMode | undefined; /** A snapshot of every per-channel override (for the at-a-glance views). */ channelModeEntries(): Record; /** Set (or clear, with `"normal"`) one channel's attention override. Validates the channel is * concrete and within this agent's read ACL (`allowSubscribe` — so a mode can be pre-set for a * channel it may read but hasn't joined yet), updates the AUTHORITATIVE in-memory map, then mirrors * the whole map to presence (best-effort; advisory). Per-instance + runtime: it NEVER writes the * agent file (a shared template) and resets on restart. * * **Prospective only:** it does NOT purge messages already buffered from that channel — those were * already received and still drain/wake per their original handling. Muting changes what arrives * next, not what's already in the inbox. */ setChannelMode(channel: string, mode: ChannelMode | "normal"): Promise; /** Set the attention mode. Entering `focus` captures the chat frontier as the focus-watermark * (recall surfaces ambient published after it); leaving focus clears it. Requires a live * connection only for `focus` (it reads the stream frontier). Ambient already *buffered* when * focus is entered is not retroactively ack-dropped. Traffic retained during the asynchronous * frontier read is tagged out of recall, so it surfaces once from its receive-time lane whether * it landed just before or after the captured frontier. Only ambient arriving after the local * switch is ack-dropped. */ setAttention(mode: AttentionMode): Promise; /** Focus recall: the channel ambient + @mentions ack-dropped since this agent entered focus, * read back from the chat stream on demand and **replay-gated per channel** (a `replay=off` * channel yields nothing, and is named in `droppedChannels` — recall must not become a history * bypass, and it must not claim a suppressed channel's window was empty and complete either). * Items are marked `historical` (catch-up framing). `droppedChannels` also names channels whose * earliest retained message postdates the focus-watermark — older ambient may have aged out of * the per-channel window — and wildcard subscriptions (`team.>`), which recall cannot read back * per concrete sub-channel (#977: a wildcard join is not itself a channel ingest can consult a * replay policy for) and so cannot vouch for either (never-silent throughout). Empty unless in * focus. Calls run one at a time (#662). `underway` names recalled items that already went out in * part: an exclusion that lands after that does not hide one, since recall would then move past * the rest of it (#613). */ recallAmbient(underway?: ReadonlySet): Promise<{ items: InboxItem[]; droppedChannels: string[]; }>; private recallAmbientOnce; send(text: string, channel?: string, mentions?: string[]): Promise; /** * What a caller can TELL about a send target BEFORE the publish: whether the name * already existed (joined, registry, or prior traffic) and close matches when it * did not. Does not refuse create. Graded before multicast so the new message cannot * make the name look pre-existing. */ describeSendChannel(channel: string): Promise<{ known: boolean; hints: string[]; }>; /** Throw if any name isn't a peer we've observed. Validates against the FULL roster * (incl. self — your own name is a valid participant; resolvePeer's self-filter would * wrongly reject it), case-insensitively. Send is all-or-nothing: one unknown @name aborts * the whole broadcast (fail-loud on typos). Caveat: only catches peers THIS client has read * — an offline peer lingers in the roster, but one no read has seen throws. See * docs/architecture.mdx. * * #1229: only a read that succeeded supports an ABSENCE verdict. A name already in the roster * is accepted whatever the read did; an absent name under a failed read refuses with the read's * own error (never "no such peer") and the send does not go out. */ private assertKnownMentions; /** The mention guard and `dm` refuse an unverifiable name through this one sentence, so the two * refusals cannot explain one failed read differently. */ private cannotVerify; anycast(role: string, text: string): Promise<{ msg: CotalMessage; ack: { seq: number; duplicate: boolean; }; /** Live roster rows holding `role`, ourselves included; absent when the roster read failed, * since only a read that succeeded can say that no holder exists (#1229). */ holdersAtSend?: number; }>; /** Resolve a peer by instance id (exact) or display name against the roster of the last * {@link readRoster}. Deterministic and fail-loud: returns * one peer, `undefined` if none match, or throws `AmbiguousPeerError` on a same-name collision — * it never silently picks. See `resolvePeer` in @cotal-ai/core. */ resolvePeer(target: string): Presence | undefined; /** #384: a peer that sent us a DM or anycast and has no roster row, by its exact id or, when the * roster read succeeded, by a display name only one such sender carried. A one-shot * `cotal send` never registers presence, so without this a reply to it could not be addressed. */ private heldSender; /** Why a DM target resolved to no one, under a roster read that succeeded. A target that names this * session says so, since the roster leaves it out of DM resolution. Present names that start with * the target are listed for the caller to choose from; none of them is ever picked. */ private noPeer; dm(target: string, text: string, opts?: { replyTo?: string; }): Promise<{ msg: CotalMessage; /** The roster row the target resolved to; absent when it resolved to an unrostered sender. */ peer?: Presence; recipient: { id: string; name: string; }; ack: { seq: number; duplicate: boolean; }; /** `unrostered`: the recipient is a peer that messaged us and has no roster row (#384). */ recipientStatusAtSend: Presence["status"] | "unrostered"; }>; /** Ask the manager to spawn a new teammate into this space (its `spawn` action). * The request uses {@link SPAWN_TIMEOUT_MS} as its floor; after acceptance, core follows through * the exact connector-selected readiness budget carried by the acceptance. * How it lands — a detached PTY, a tmux window, a cmux tab — is the manager's * runtime; from here it just joins the mesh as a lateral peer. `opts.agent` picks * the harness (absent it, the persona's `agent:` pin, else the manager's default), `opts.model` / * `opts.variant` override the persona file's model selectors, `opts.prompt` submits the new * peer's first turn (or, with `opts.once`, is its one task, after which the manager stops it), `opts.runtime` chooses the terminal runtime of this one seat, and `opts.cwd` roots it at a different folder/repo * than the manager's workspace — the same knobs the operator's `cotal spawn --detach` carries, so * the agent and operator spawn doors share one control-op contract. (Session `resume` is * intentionally NOT forwarded here: forking a host-local `~/.claude` transcript is an * operator-local intent, kept off the peer-facing spawn door — see #159.) * * Channels: `opts.join` names concrete channels the new peer joins on top of its persona's read * set, each checked here against THIS agent's read ACL before anything is sent (a spawn never * widens). The request also carries this agent's own read/post ACL as `callerAcl`; the manager * consults it only for a persona that names no channel field, so such a helper is scoped to its * spawner. The joined set is never copied: without `join`, a channel-less persona starts on none. * * `opts.persona` defines the helper inline, with `name` as its name: the manager validates and * bounds it as it does a catalog file. Its `model` is audited like the `opts.model` override. */ spawn(name: string, role?: string, opts?: { agent?: string; model?: string; variant?: string; launchOptions?: Record; cwd?: string; prompt?: string; events?: boolean; runtime?: string; instance?: string; join?: string[]; persona?: InlinePersona; once?: { deadlineMs?: number; }; }): Promise; /** The manager's recorded model pin for a managed seat (`inspect.model`). Absence is a real * state: a launch may pin none. Distinct from a failed inspect, which cannot attest. * `unreported` marks a failure the manager side settled at once: it answered without the seat's * row, its surface has no `inspect` for this caller, or no responder took the request. */ inspectModel(name: string, instance?: string): Promise<{ ok: true; model?: string; } | { ok: false; error: string; unreported?: true; }>; /** The registry listing as this session's manager-call credential sees it (SPEC 13.20), asked * over a connection opened on that credential and described on this session's own. On a static * mesh manager calls ride this session's credential, and a host-issued bearer has no manager-call * credential: `none` says which. `unbound`: that credential carries no listing binding. */ listManagerEndpoints(input: RegistryEndpointsInput, instance?: string): Promise<{ none: string; } | { unbound: true; } | { unbound?: false; reply: EpAttributedReply; page?: RegistryEndpointsOutput; }>; /** Manager-call bearers this session holds, by the exact bearer argv (so by selected manager; * the space, owner, actor and lifecycle are this session's own). One exchange serves every call * until {@link MANAGER_BEARER_MARGIN_MS} before the bearer expires, and concurrent calls share it. * Each call still presents the bearer to the broker and checks its binding, so this keeps no * authorization decision; any failed call drops the entry, so the next one exchanges afresh. */ private managerBearers; private managerCallBearer; /** Run the spawner's bearer command with this session's environment on every credential path. */ private execBearerCmd; /** One v0.4 manager-endpoint invoke (P2 item 1, 1c.2b): the generic {@link CotalEndpoint.invokeService} * path (describe → §13.7 store fetch → digest-verified recompile → typed command), adapted back to * the {@link ControlReply} shape every tool above consumes, with the instance that answered it. `undefined` args are stripped BEFORE the * compiled input contract validates (closed schemas reject present-but-undefined keys; the ctl path * shed them in JSON serialization). A broker-denied publish (a capability the caller's credential * does not hold, e.g. a capability-less purge) has NO responder and surfaces as the deadline — * named here so the refusal reads as the tier boundary it is. */ private managerInvoke; /** Resolve a managed agent's CURRENT principal triple (owner-mode targets are (owner, actor, * lifecycleUid), never an alias — §13.2) through the manager's `inspect` read. A STATIC row's * `id` is the bare actor (nkey) under the caller's own owner; a USER-mode row's `id` is the * composite `owner.actor` principal key — split it, or the embedded dot breaks the target * block's subject arity. The owner-mode standing mint pins the caller's OWN owner, so a * foreign-owner target is broker-denied at publish (the same own-domain boundary as ctl). * `instanceId` reads the row from that manager alone. */ private managerTargetFor; /** Ask the manager to tear a teammate down (its `stop` op). Graceful by default — * the session is told to exit cleanly (so it leaves the mesh) before the * process/tab is closed; `graceful:false` is a hard, immediate kill. * * No `name` ⇒ self-despawn: rides the self-service control subject and the manager * resolves the target as the managed agent whose id == this caller — so it can only * ever stop itself, never a peer. A `name` ⇒ rides the privileged control subject * (transport-gated to spawn-capable/admin); the manager refines own-child vs admin. * `instance` addresses the manager that runs the named peer, under `spawn`'s pin rules. */ despawn(name?: string, opts?: { graceful?: boolean; instance?: string; }): Promise; /** One `run-*` command to the hosting manager. The five verbs are untargeted and ride the `run` * capability's rows (open mode: anyone). `start` and `resume` return the run id as soon as the * manager has the drive; the run continues there. Their deadline outlives the manager's own * activation wait, so a slow launch reads as the manager's "still launching" refusal and never * as a manager that did not answer. */ run(verb: "start" | "resume" | "answer" | "status" | "ps", args: Record, instance?: string): Promise; /** Open managed seats use their launch address without changing credentialed routing. */ private openManagerInstance; /** An explicit manager instance for a manager call, under `spawn`'s rules: a lifecycle token, * and in user-auth mode no other instance than the one this session is bound to. */ private managerPin; /** Whether a manager launched this seat. A manager accepts a run turn only against a seat it * launched (`serveTurnGoal`), and every manager launch path (local spawn, restart, resume, and * `delegatedSeatCommand` → `cotal spawn` handoff) sets `COTAL_MANAGER_INSTANCE` * (`config.managerInstanceId`), so a directly connected session has no turn relay to arm. */ private hasTurnRelay; private turnRelayAuthorized; private nativeTurnLease?; /** Arm or renew native personal participation. This grants only the existing self-addressed turn * pull/yield path. It does not make the connection a managed seat or grant stop/attach custody. */ authorizeNativeTurnParticipation(lease: NativeTurnParticipationLease): void; /** Ask this identity's selected manager for a finite self-only participation lease, validate the * returned binding locally, then arm the existing turn relay. */ participateInTurns(): Promise; nativeTurnParticipationExpiresAt(): number | undefined; /** Number of currently held workflow turns, surfaced or not. Read-only diagnostics for native * ingress and acceptance tests; the manager's next pull remains the settlement authority. */ activeTurnCount(): number; private nativeTurnLeaseValid; /** Subscribe to the launching manager's turn notices (SPEC 14.3) and pull once. The manager * publishes a notice whenever this seat's `turn-pending` answer changes, and repeats it until * the seat pulls a new turn, so an idle seat sends nothing, across reconnects too. The pull on a * notice, here at the first bind, and on a reconnect while the seat holds a turn is the only * intake; the subscription exists before the first pull, so a turn accepted in between is not missed. */ private armTurnRelay; /** Pull now, or once more after the pull in flight, so notices that arrive together cost one pull. */ private requestTurnPull; /** Pull this seat's pending run turns from its launching manager (`turn-pending`, self-mode). * New turns request a wake so the payload surfaces on the next injectable frame; turns gone from * the reply were settled elsewhere (deadline, agent-down, a competing yield path) and are * dropped. Only a manager-launched seat (`hasTurnRelay`) pulls; any failure to reach the * launching manager is passed to {@link turnPullFailed}. */ private pollTurns; /** Follow what the launching manager says about turn notices. A manager from before them never * sends one, so this seat pulls on a timer while its answers say so, and says that once. */ private noteTurnNotices; /** A pull failed. A manager that is down, still reconciling or unanswered can recover, so ONE * retry is armed for {@link TURN_PULL_RETRY_MS} later and re-armed while that keeps failing. A * refusal (a denied capability, a request the manager rejects) is reported and not retried: * asking again on a timer would be the idle poll this relay removed, against a door that is shut. * The next notice or reconnect pulls again either way. */ private turnPullFailed; /** * A pull that did not come back, said ONCE. * * Only a manager-launched seat pulls (`hasTurnRelay`), so an unanswered pull means its own * launching manager did not answer. Each distinct reason is reported once and saying it again * waits for the reason to change or for a pull to succeed. Only the manager's startup * reconciliation window (`still reconciling`) stays silent because it resolves itself. */ private notePullTrouble; /** Format every not-yet-surfaced turn still inside its deadline as an injectable context block, * WITHOUT marking anything (undefined when none wait). Two-phase on purpose, like the inbox's * format-then-verdict rail: marking at format time would let a lost frame auto-yield `done` for * work the model never saw. A caller commits with {@link commitSurfacedTurns} only once the * injection verifiably reached the host. The payload is opaque relay bytes; when it parses as * JSON with a string `context`, that rendered context is what the model reads, else the raw * payload is shown. */ peekPendingTurns(): { text: string; goalIds: string[]; once?: true; } | undefined; /** Mark peeked turns as surfaced — the second phase, called once their injection verifiably * reached the host. Surfacing is what arms the automatic `done` at the next turn boundary; a * turn never committed re-surfaces on a later frame instead of yielding a lie. */ commitSurfacedTurns(goalIds: readonly string[]): void; /** Take the permit to hand these turns to the harness, at the dispatch site, after every gate * that can return without sending and right before the native send, with no await between. All * or nothing for the once task among them: false means another caller holds it or its handoff * already ended, and the caller sends no once task. Run turns need no permit and are offered * again as before. */ claimTurns(goalIds: readonly string[]): boolean; /** The adapter gave up on a dispatch that carried these turns. A claimed once task is sealed and * reported to the manager; it is never offered again, because the harness may have it. */ releaseTurns(goalIds: readonly string[]): void; /** The dispatch carrying these turns left the seat, and its acceptance arrives with the model's * response: stop the once task's handoff timer, which measures the dispatch only. */ noteTurnsDispatched(goalIds: readonly string[]): void; /** Seal and report a once task still unclaimed {@link ONCE_HANDOFF_MS} after this seat pulled it. */ private boundOnceClaim; /** A turn the harness may be handed: any run turn, and a once task whose handoff has not begun. */ private offerable; /** The key a once task's handoff state is kept under in this process. */ private onceUid; /** Tell the manager a once task's handoff was not confirmed (`turn-unconfirmed`), so it fails the * task and stops this seat. Retried with backoff for one handoff bound while the manager cannot * be reached; any answer that is not transient ends the retry. A report that has not landed by * then ends this seat instead, and the manager's proof of that exit settles the task. A seat that * cannot end itself keeps reporting, at most a minute apart, until a manager answers or this * process ends. */ private reportUnconfirmed; /** Yield one active turn back to the run (`turn-yield`, self-mode). Without `turn` it targets * the OLDEST surfaced turn — the one the current session turn is working on. The entry is * dropped locally only when the manager's answer settles it, by this yield or by a terminal * the run reached first; a refused one stays for the poll to reconcile (the manager's answer, * not a local guess, decides whether it is settled). */ yieldTurn(status: "done" | "blocked" | "handoff", opts?: { to?: string; note?: string; turn?: string; }): Promise; /** Drop an active turn the run settled without this seat's yield, remembering its deadline. */ private settleTurn; /** Ask the manager to purge the space's retained chat backlog (its `purge` op). Cleanup only — * it doesn't touch live agents or the anycast work queue. `includeDms` also clears DM history. */ purgeHistory(opts?: { includeDms?: boolean; }): Promise; /** Define a persona and persist it as config (the manager's `definePersona` op writes * .cotal/agents/.md). `spawn(name)` then launches an agent wearing it. * * Writing is the whole job: announcing is OPT-IN via `announce`, and silence is the default. * This used to end in a bare `this.send(...)`, which has no channel argument, so * {@link Endpoint.multicast} resolved the destination as the caller's FIRST CONCRETE CHANNEL — * `general` for most personas, purely by list order. Nobody ever chose that: defining a review * panel sprayed one broadcast per seat into every peer's inbox on the mesh. `reply.ok` was the * send's ONLY gate, so a failed manager reply announced nothing — but a host-level tool retry * hit an idempotent overwrite, succeeded again, and announced the same persona twice. The text * was worse than the volume: "spawn it to bring it online" is an imperative addressed to * strangers, indistinguishable from an attempt to get unrelated agents to run someone else's * code, and it tripped a real provenance investigation. * * So: no `announce` ⇒ no mesh traffic at all, and the definer already knows what it defined (the * reply says so, and `spawn` on a missing persona fails loud). With `announce` ⇒ that channel and * only that channel, never one inferred from ordering; post rights stay broker-enforced, so a * definer without them fails there loudly rather than falling back to `general`. The wording is a * statement of what the sender did, not an instruction to the reader — the sender's identity is * already on the envelope, so an attributed fact is what a peer can actually evaluate. */ definePersona(def: { name: string; prompt: string; instance?: string; model?: string; role?: string; agent?: string; subscribe?: string[]; allowSubscribe?: string[]; allowPublish?: string[]; capabilities?: string[]; announce?: string; }): Promise; /** Mesh-side persona catalog list: name, role, model, description, scoped by the same * ownership rule as `definePersona`. */ listPersonas(instance?: string): Promise; /** Mesh-side persona catalog show of one card. Unauthorized / missing names are not-found. */ showPersona(name: string, instance?: string): Promise; /** The full roster, including ourselves, read from the broker now: a seat holds no presence watch * (SPEC §6). Every roster surface reads through here (cotal_roster, the orientation card, the * @mention guard, DM resolution and the anycast holder count), and calls made while a read is * in flight share it. A failed read returns the roster of the last read that succeeded with * the failure beside it: a name it holds is still a peer, but an absence is no verdict (#1229). */ readRoster(): Promise; /** Our last self-reported presence status. */ get status(): PresenceStatus; /** Run one presence write through the chain (#636): the write starts only after every earlier * admitted write has settled, and a rejection is swallowed at the TAIL only, so it propagates * to this write's own caller while the next admitted write still runs. * * The straggler rule: departure is the last write, later writes are refused, not reordered. * Once `stop()` has begun, any write admitted here rejects AT ONCE (no put queued, no wait on * the chain) with a fixed error, instead of running after — or, for a write that calls * `requireConnected` (e.g. `setStatus`), sitting out the ten-second connect grace before * rejecting anyway. `stop()` itself submits departure's own put through this same method (the * `forDeparture` escape) so departure keeps landing after every write already admitted, even * though `_stopping` is already true by the time it does. */ private inOrder; /** Detach the chain from writes the CALLER has abandoned, so the next write is ordered behind * what has actually settled rather than behind work nobody is waiting for any more. * * The chain above orders departure after every admitted write, which is right while those writes * are still progressing and wrong once a teardown has given up on them. A host bounds its wait on * admitted intake and then attempts departure; without this, that attempt queues behind the very * write the bound just abandoned, so the bound buys nothing, offline never publishes, and the seat * stays at its last status until its presence TTL expires. The host says in its own log that * abandoned work is uncancelled and may land after departure, and the chain quietly contradicted * it. * * ABANDONED IS NOT CANCELLED here either. The detached writes keep running and may still publish, * which is the same residual the caller already announces; what changes is only that departure * stops waiting on them. Call it exactly where the give-up is decided, never as a general reset: * a caller that invokes this while its writes are still progressing has reintroduced the race the * chain exists to close. */ abandonPresenceWrites(): void; setStatus(status: PresenceStatus, activity?: string): Promise; /** * Publish presence for something that is NOT a turn ending. * * The boundary above reads working→idle as "the seat finished its turn", and that reading holds * only at a real turn terminal. A session (re)start writes idle too: Claude Code fires * `SessionStart` on compact, clear and resume, and an auto-compaction lands in the MIDDLE of a * long turn. Routed through `setStatus` it yielded `done` for work the model had not finished, * and the run moved on. An adapter uses this for every idle that is a lifecycle event rather * than an ending. */ resetStatus(status: PresenceStatus, activity?: string): Promise; private publishStatus; /** Change only the activity line. Routing it through `setStatus` with the current status read the * status before queued writes settled, then wrote that stale value back over them. */ setActivity(activity: string): Promise; /** Relay a harness-reported condition into presence, or clear it. */ setCondition(condition: PresenceCondition | null): Promise; /** Relay harness-reported work progress (a turn event) as presence `activeAt`. The next heartbeat * carries it, so an observer can tell a turn that stopped advancing from one that is progressing. */ noteActivity(at?: number): void; /** The working→idle boundary: yield `done` for every SURFACED turn (its payload was in the * context of the turn that just ended; ending without an explicit yield IS the done signal). * The manager's notice for each settled yield brings the next queued turn. Fire-and-forget * from the presence path — a yield must never block a status write. */ private onTurnBoundary; /** Record the host's actual model and optional variant learned after launch, so peers see the * selection in `cotal_roster` and the web roster even when the operator never pinned one. Explicit * `model:` / `variant:` config wins; this only fills the gap. `provider` is best-effort and * reported independently of the model pin: a pinned seat can still report which provider is * serving it, so the early return only short-circuits when no provider was given either. Best- * effort presence mirror (no `requireConnected` — safe pre-connect; it rides the first publish). */ setModel(model: string, variant?: string, provider?: string): Promise; /** Report the session's working directory as its harness supplied it. Advisory display data. */ setCwd(cwd: string): Promise; /** The boot-time "push" half of channel onboarding: a fenced, one-line description per * subscribed channel that has one (the full `instructions` stay pull-only via * cotal_channel_info — N paragraphs of least-attended text don't belong at boot). Attributed, * advisory framing — the same injection fence as the pull. Best-effort: empty until the * registry cache has loaded (returns undefined when there's nothing to say). */ channelBriefing(): string | undefined; /** Read the channel registry from the broker now: a seat holds no registry watch, so a surface * that shows the registry reads it first (SPEC §7). Calls made while a read is in flight share it. */ readChannels(): Promise; /** A channel's registry config + effective replay policy, from the endpoint's cache as of its last * registry read ({@link readChannels}). Config only — never membership (that view is kept off * agents on purpose). */ channelInfo(channel: string): { description?: string; instructions?: string; replay: boolean; registered: boolean; event?: "own" | "agent"; }; /** Whether `channel` is an agent's event channel (`events..`) and whose: this * session's own, or another agent's. Undefined for a chat channel. */ private eventChannelOf; /** Channels we're currently subscribed to (live — reflects join/leave). */ joinedChannels(): string[]; /** Discoverable channel list: every channel with traffic or a registry entry, tagged with * its one-line description, replay policy, and whether WE are subscribed (self only — never * other peers' membership). The companion to cotal_join. Each pattern WE follow is a row too, * marked `follow`: a family (`team.>`) with the health of its own stored copies, any other pattern * as live only. */ listChannels(): Promise<{ channel: string; description?: string; replay: boolean; joined: boolean; durableUnclosed: boolean; deliveryHealth?: "active" | "degraded" | "unknown"; messages: number; mode: ChannelMode | "normal"; event?: "own" | "agent"; follow?: "family" | "live"; }[]>; /** Join a channel mid-session (backfills history if replay is on; idempotent). `durable` reports * whether a durable backstop is active (Plane-3, SPEC §8, for a `durable`-class channel when a * manager is present) — `false` means joined LIVE only, so messages sent while this session is * offline won't be replayed. `reason` explains a `durable:false` on a channel that EXPECTED a * backstop (e.g. no privileged provisioner); absent on a `live`-class channel (joined live is the * contract there). */ joinChannel(channel: string): Promise<{ joined: boolean; backfilled: number; durable: boolean; reason?: string; }>; /** Leave a channel mid-session (refuses to leave the last one). */ leaveChannel(channel: string): Promise<{ left: boolean; }>; /** The connectedness gate every mesh op goes through. Waits out the initial connect window * rather than failing into it (see CONNECT_GRACE_MS), so the common startup race resolves as * a slightly slow first call instead of a false "mesh is down". */ private requireConnected; /** Public bounded wait for the mesh link, for callers OUTSIDE the op methods that need to gate * their own startup on it. The AG-UI emitter is the proven case: it lazy-starts from the first * lifecycle hook, `--prompt` fires that hook within a second of launch, and its start reached * the endpoint before the first bind — "endpoint not started", terminal by the holder's design, * so one race at spawn silenced the event plane for the whole session. */ whenConnected(timeoutMs?: number): Promise; /** Resolves true on the endpoint's real post-bind connection signal, false if the window closes * first. Listens on the ENDPOINT because its (re)binds are the single source of truth (see the * constructor), and re-checks after binding the listener so a connection that lands between the * guard above and the subscription cannot be missed. */ private awaitConnection; /** `cotal up` starts a broker on THIS machine, so it is only advice when the configured one is * local. Someone whose agent joins a hosted mesh cannot fix it by starting their own, and being * told to try sends them to repair the wrong thing entirely. */ private notConnectedMessage; /** Keep an ordered-consumer reset storm from painting hundreds of status lines through an * attached Codex TUI. Consumer names are generated per reset, so normalize them before * deduplicating; otherwise every `_71`, `_72`, ... would look like a new fault. */ private handleEndpointError; private handleEndpointWarning; private logEndpointNotice; private log; } /** One {@link MeshAgent.readRoster}: the roster, and why it is last-known when the read failed. */ export interface RosterRead { roster: Presence[]; failure?: Error; } /** Why a roster whose read failed cannot support an absence verdict (#1229). The roster, the * orientation card and the mention and DM refusals all print this clause, so none of them can word * one failure differently. */ export declare function presenceReadCondition(failure: Error): string; /** Names already known that differ from `channel` by one insertion, deletion, or substitution. */ export declare function closeChannelNames(channel: string, known: readonly string[]): string[]; //# sourceMappingURL=agent.d.ts.map