/** * Durable, scope-aware conversation sessions. One conversation facet — the * whole chat, one topic thread, or one sender inside a chat — owns exactly one * agent session whose id is derived from that facet alone, so a restarted * process reaches the conversation's stored session instead of starting it over * and a topic group no longer funnels every thread into one agent. * @module dsh-lark-channel/session */ import type { NormalizedMessage } from '@larksuite/channel'; import type { HostAgentHandle } from './host.ts'; /** Which conversation facet owns one agent session. */ export type SessionScope = 'chat' | 'chat-thread' | 'chat-sender'; /** * Marks a session id as this channel's, in the host agent registry and in the * on-disk session log. Stable: changing it orphans every stored conversation. */ export declare const SESSION_PREFIX = "lark-"; /** * Which conversation a control card governs, and where it was published. * * A card that changes a conversation's settings has to say which conversation * it means, because a click arrives with only a chat and an operator — not the * thread or the sender facet the key was derived from. The chat is carried so * a FORWARDED card cannot act: the same payload pressed in another room no * longer matches the room the card was published to. */ export interface ConversationSubject { /** The conversation key, from {@link conversationKey}. */ readonly key: string; /** The chat the card was published to. */ readonly chatId: string; /** That chat's kind, so a click is authorized exactly as a message is. */ readonly chatType: string; /** The only operator the click may come from, when the scope is per-sender. */ readonly owner?: string | undefined; } /** * Derive the stable conversation key one session owns. Pure: the same * conversation facet yields the same key in every process. * @param scope - the facet a session is bound to. * @param msg - normalized inbound chat message. * @returns the conversation key. * @throws {Error} when `scope` is outside {@link SessionScope}. */ export declare function conversationKey(scope: SessionScope, msg: NormalizedMessage): string; /** * Brand a conversation key as the session id that owns it. Concatenation only, * so the mapping is injective by construction: two conversations can never * share one session, and one conversation resolves to the same durable session * on every boot. * @param key - a conversation key from {@link conversationKey}. * @returns the session id to look up, resume, or create. */ export declare function sessionIdFor(key: string, prefix?: string): string; /** One agent this channel drives, and whether disposing it is this channel's job. */ export interface OpenedSession { readonly handle: HostAgentHandle; /** False when the agent was already live under another owner. */ readonly owned: boolean; } /** * Host operations the ladder needs, as plain functions: the host `agents` * registry satisfies them through the bridge, while tests substitute an * in-memory object and need no Cordis mount. */ export interface SessionLadder { /** * Return the live agent for this id, if the host already has one. * @param sessionId - the branded session id. * @returns the live handle, or undefined when nothing runs on that id. */ lookup(sessionId: string): HostAgentHandle | undefined; /** * Load a persisted session as a live agent. * @param sessionId - the branded session id. * @returns the resumed handle. * @throws when no session is stored under the id, or its log cannot be read. */ resume(sessionId: string): Promise; /** * Create a fresh agent on this id. * @param sessionId - the branded session id. * @returns the created handle. * @throws when the agent cannot be composed. */ create(sessionId: string): Promise; /** * Report a failure that is handled rather than propagated. The line is * operator-facing and self-describing. * @param line - the complete report line. */ report(line: string): void; } /** * Get, resume, or create the agent bound to one conversation key, deduplicated * per key so a burst of messages cannot race two sessions into existence. * Bindings live until {@link ConversationSessions.close}, which disposes every * agent this store owns. */ export declare class ConversationSessions { private readonly scope; private readonly ladder; private readonly idFor; /** Resolved sessions by conversation key. */ private readonly opened; /** Conversation key per live session id, in binding order. */ private readonly keys; /** Acquisitions still walking the ladder, joined by concurrent messages. */ private readonly opening; /** * Release epoch per key. A release advances it SYNCHRONOUSLY before any * await, so every walk that started earlier can tell its result is stale — * a waiter that merely joined a shared opening promise would otherwise be * handed the very agent the release is about to dispose. */ private readonly generations; /** * Teardown still running, per key. The epoch above protects a walk that * STARTED before the release; this protects the one that starts during it. * * A release detaches its maps and then awaits `dispose()`, and for that * moment the host registry can still answer with the agent being torn down. * A fresh walk arriving there finds it through `lookup`, adopts it as * someone ELSE's live agent — so it will never dispose it — publishes it as * the conversation's binding, and the release then destroys the object the * new binding points at. Nothing about that is visible until the next * message drives a disposed agent. */ private readonly retiring; private closed; /** * @param scope - the conversation facet every session is keyed by. * @param ladder - the host operations to walk. * @param idFor - session id per conversation key; the default is the plain * branding, and a workspace-aware channel injects a deriver that * discriminates by directory too. */ constructor(scope: SessionScope, ladder: SessionLadder, idFor?: (key: string) => string); /** Session ids currently bound, in insertion order. */ get sessionIds(): string[]; /** * The live agent this store opened for one session id. * * The store's own record rather than the registry's: this channel created * or resumed the agent, so the authoritative answer to "is there something * to drive for this conversation" is here, not in a lookup whose publication * rules belong to the host. * @param sessionId - the session id to look up. * @returns the agent, or undefined when this store drives no such session. */ agentFor(sessionId: string): HostAgentHandle['agent'] | undefined; /** * The conversation key a live session id serves. * @param sessionId - a session id, as carried by a host session event. * @returns the key, or undefined when this store does not drive the session. */ keyOf(sessionId: string): string | undefined; /** * Resolve the agent for one inbound message. * @param msg - normalized inbound chat message. * @returns the bound session, the same object for every later message of its key. * @throws {Error} when this store is closed, or when no ladder rung yielded an agent. */ acquire(msg: NormalizedMessage): Promise; /** * Resolve the agent for one conversation key. A loop rather than a single * walk: every await is a window for a release or a switch to move the * conversation on, and a result that no longer matches the key's current * state is retried under the new state instead of being handed out stale. * @param key - the conversation key. * @returns the bound session, the same object for every later call of its key. * @throws {Error} when this store is closed, or when creation genuinely fails. */ acquireKey(key: string): Promise; /** The release epoch one key is currently in. */ private generation; /** * Unbind one conversation and dispose the agent it held, so the next message * walks the ladder afresh — which is what makes a workspace switch take * effect. An adopted agent (another owner's) is unbound but left running: * whoever created it still owns taking it down. * @param key - the conversation key to release. * @returns whether a binding existed. */ release(key: string): Promise; /** * Stop accepting new work and dispose every owned agent. The bindings are * dropped before the first await, so a second call disposes nothing twice. * @returns once every owned disposal has settled. * @throws {AggregateError} carrying every disposal rejection. */ close(): Promise; /** * Walk the ladder for one key and publish the result under it. * @param key - the conversation key being bound. * @returns the bound session. * @throws {Error} when the ladder yielded nothing, or when the store closed * mid-walk — the disposal sweep has already run, so the agent it produced is * taken down here instead of outliving its owner. */ /** * Walk the ladder for one key and publish the result — unless the world * moved while walking. A close or a release during the walk makes the * product stale; the walk owns its own product, so it disposes what it made * and rejects, rather than leaving that to whoever noticed later. * @param key - the conversation key being bound. * @param generation - the release epoch this walk belongs to. */ private bind; /** * Reach the agent for one key: an already live one, else the stored session, * else a fresh one. * @param key - the conversation key. * @returns the first rung that yielded an agent, with its ownership. * @throws when creation — the last rung — also fails. */ private reach; } //# sourceMappingURL=session.d.ts.map