/** * Adoption of an operator-supplied Slack thread as a live session's root. * * Three separate authorities have to agree before a mapping exists: * 1. *Configuration*: a complete Slack target (both tokens plus workspace and * channel) must already be configured; binding never accepts a target. * 2. *Daemon*: only the running owner may mutate mappings, and only while it is * still the exact owner (`ownerId`/`pid`/`incarnation`/daemon generation) * captured before the request was published. * 3. *Session*: the running daemon supplies a redacted Router authority proving * the exact session and endpoint generation; this module never resolves an * endpoint or retains its credentials. * * Provider verification happens before any lock is taken, and the mapping commit * re-proves daemon and session authority inside the store lock, so an authority * that changes mid-flight leaves no mapping behind. */ import type { Settings } from "../../config/settings"; import { type EnsureChatDaemonResult } from "./chat-daemon-control"; import { ConversationStore } from "./conversation-store"; import { type SlackConversation } from "./slack-conversation"; export type SlackThreadBindingErrorCode = "invalid_root" | "target_not_configured" | "daemon_unavailable" | "daemon_owner_changed" | "session_not_live" | "root_not_found" | "provider_unavailable" | "root_conflict" | "session_conflict" | "binding_outcome_unknown" | "binding_failed"; /** A fail-closed rejection while adopting an operator-supplied Slack thread. */ export declare class SlackThreadBindingError extends Error { readonly code: SlackThreadBindingErrorCode; constructor(code: SlackThreadBindingErrorCode, message: string); } export declare function isBoundedSlackRootTs(value: string): boolean; /** Reject a non-addressable root before any authority read or persistence. */ export declare function assertBoundedSlackRootTs(rootTs: string): void; export interface SlackThreadClaimInput { store: ConversationStore; /** The session root-claim key shared with stock root publication. */ key: string; teamId: string; channelId: string; sessionId: string; rootTs: string; endpointGeneration: number; attachmentAuthorityId?: string; /** * Re-proves session authority inside the store lock, immediately before * commit. It may be invoked more than once for one claim: authority here is * monotone — a rolled endpoint generation, a replaced daemon owner tuple and * a retired single-winner request claim never come back — so repeating the * proof can only ever narrow the window, never widen it. It must therefore be * idempotent and must never perform a remote call. */ revalidate: () => Promise; now?: () => number; } /** * Claim an existing Slack root for a session without publishing a replacement. * * The claim transacts the same `intent:` key that stock root * publication uses, so a bind and a concurrent first notification serialize on * one invariant: whichever commits first owns the session's single root, and * the loser observes it instead of creating a second one. */ export declare function claimSlackThreadBinding(input: SlackThreadClaimInput): Promise; export interface ConfiguredSlackThreadBindingInput { settings: Settings; sessionId: string; threadTs: string; } export interface ConfiguredSlackThreadBindingDeps { ensureDaemon?: (settings: Settings) => Promise; timeoutMs?: number; pollIntervalMs?: number; /** Bounded wait for a daemon that took terminal authority before the caller gave up. */ settleGraceMs?: number; now?: () => number; sleep?: (ms: number) => Promise; } /** Safe confirmation of an applied binding. It carries identifiers only. */ export interface BoundSlackThread { sessionId: string; endpointGeneration: number; teamId: string; channelId: string; rootTs: string; ownerId: string; daemonGeneration: number; } /** * Adopt an existing Slack root for a live session through the running daemon. * * The CLI never writes the mapping store: it proves the configured target and * the exact current owner, then asks that owner to perform the mutation. A * daemon that is no longer the captured owner answers `owner_changed`, so a * replacement or restart between capture and execution can never apply the * request. * * A submission that is not answered is cancelled through the channel's * single-winner arbitration before it is reported as a failure. If that * cancellation loses to the daemon's own commit authority, the outcome is * reported as unknown instead of as a failure, because a mapping may already * exist; re-running the same command observes the settled state idempotently. * * The command channel proves *correlation*, never authorship: every field a * response echoes is copied verbatim out of the plaintext request published * beside it. A definitive answer is therefore reported only after this caller * itself observes the exact mutation in the conversation store — see * `corroborateBoundMapping`. */ export declare function bindConfiguredSlackThread(input: ConfiguredSlackThreadBindingInput, deps?: ConfiguredSlackThreadBindingDeps): Promise;