/** * Continuing a session this conversation did not start. * * A conversation's session id is DERIVED — chat, workspace, epoch — and that is * what makes a restarted process find the same conversation again. The one * thing derivation cannot express is "carry on with that other session": the * one left behind by `/new`, or the one opened on a laptop in the web UI and * now wanted on a phone. * * So a pick is an override on the derivation, and the whole design here follows * from what a chat can safely be shown and safely be given: * * - **Nothing is typed.** A session id is a machine identifier; asking someone * to transcribe `lark-oc_…--e3` on a phone is not an interface. Every switch * is a press on a row this channel produced, which is also what makes the * list the only place authorization has to happen. * - **The list is the boundary.** A conversation may continue its own past * sessions and sessions no conversation owns; never another chat's, whose * title alone is a summary of what was said there. * - **The workspace decides what is on offer.** A session carries the directory * it runs in, so continuing one from elsewhere would move the sandbox without * anyone saying so. To reach those, `/cd` there first. * - **A pick is undone by picking.** This conversation's own derived session is * always a row, so going back is the same gesture as going away — no second * verb, nothing to remember. * @module dsh-lark-channel/sessions */ import type { HostSessionQuery, HostSessionRecord } from './host.ts'; import type { ConversationSubject } from './session.ts'; /** List the sessions this conversation may continue. Channel-owned: needs no agent. */ export declare const SESSIONS_COMMAND = "sessions"; /** Marks this plugin's session rows apart from other card actions. */ export declare const SESSIONS_ACTION = "dsh-lark-channel/sessions"; /** How many rows the picker offers before it asks for a keyword instead. */ export declare const PICKER_ROWS = 8; /** * How many candidates beyond the visible rows are described anyway. * * Describing costs a log read per session, so the window is bounded — but a * window exactly as wide as the card runs the card short whenever a candidate * turns out to be a session nothing ever happened in. */ export declare const PICKER_SPARE = 4; /** * How many candidates a keyword is matched against by title. * * A title is a log read per session, so a keyword search over a corpus of * hundreds cannot read them all. Ordered newest-first, so what it does read is * the half of the corpus a person is plausibly looking for. */ export declare const SEARCH_MAX = 60; /** One session a conversation may continue, as the picker shows it. */ export interface SessionChoice { readonly id: string; /** The host's folded title, absent when the log carries none. */ readonly title?: string | undefined; /** When the session was created, for the row's relative time. */ readonly createdAt?: number | undefined; /** Whether an agent is driving it right now — on another surface, usually. */ readonly live: boolean; /** Whether this is the conversation's own derived session. */ readonly own: boolean; /** Whether the conversation is on it now. */ readonly current: boolean; /** * The last thing a person said in it — what a reader actually recognizes a * conversation by, and the one label that stays true as it goes on. */ readonly lastSaid?: string | undefined; /** Turns taken, so a long thread reads as one. */ readonly turns?: number | undefined; /** When it last moved, which is what "recent" should mean in the list. */ readonly lastActive?: number | undefined; } /** What one session's own log says about it, beyond its header. */ export interface SessionFacts { /** Turns taken, which is the honest measure of how much is in there. */ readonly turns: number; /** When it last moved, which is what "recent" means in a list. */ readonly lastActive?: number | undefined; /** The last thing a PERSON said in it. */ readonly lastSaid?: string | undefined; } /** * Read what makes one session recognizable: how much has happened, when it * last moved, and the last thing a person said in it. * * The last human line, not the first and not the title. A title here is folded * from the session's FIRST prompt, so a chat that opened with "hello" is * called Hello forever; and the opening line of a long conversation says as * little. What a person recognizes a conversation by is what it has been about * lately. * * Telling a person's message from an injected one needs the event's `source`, * which only the raw read carries: the same `user/message` stream holds system * prompt snapshots, skill catalogs and job notices, and any of them can be the * newest entry. So the seqs come from the cheap listing and exactly one * bounded window is read around the newest one. * @param query - the host session-query engine. * @param id - the session to describe. * @returns the facts, as far as the engine offers them. */ export declare function sessionFacts(query: HostSessionQuery, id: string): Promise; /** Card payload carried by one session row. */ export interface SessionActionValue extends ConversationSubject { readonly kind: typeof SESSIONS_ACTION; /** The session to continue; the row for the derived one carries it too. */ readonly session: string; } /** * Narrow an arbitrary card-action value to this module's payload. * @param value - raw button value from a card action event. * @returns the typed payload, or undefined for foreign card actions. */ export declare function sessionActionValue(value: unknown): SessionActionValue | undefined; /** Construction options for {@link ChatSessionPicks}. */ export interface ChatSessionPicksOptions { /** Persisted conversation-key → session id; an empty entry means derived. */ readonly entries?: Record | undefined; /** Deep-merge one patch into the plugin's settings section; false = not composed. */ readonly persist?: ((patch: { chatSessions: Record; }) => Promise) | undefined; /** Operator console line. */ readonly report?: ((line: string) => void) | undefined; } /** * Which session each conversation was told to continue, against "the one it * derives" meaning no entry. Pure state plus injected persistence, mirroring * the workspace and model stores. */ export declare class ChatSessionPicks { private readonly entries; private readonly persist; private readonly report; private warnedNotDurable; constructor(options?: ChatSessionPicksOptions); /** * The session one conversation was told to continue. * @param key - the conversation key. * @returns the picked id, or undefined when it runs on its derived one. */ pickFor(key: string): string | undefined; /** * The conversations that were told to continue one session. * * Asked before anything would CREATE that id: a pick names a session that * already exists, so reaching the create rung under a picked id means the * resume failed — and starting an empty session in its place would answer * "continue that conversation" by writing over the one that was asked for. * @param sessionId - the session id about to be created. * @returns the conversation keys picking it, empty when none. */ keysPicking(sessionId: string): string[]; /** * Record a pick, or clear it. * * Clearing is what `/cd` and `/new` do: both change which session this * conversation derives, and a pick that survived them would quietly win over * the very thing the person just asked for. * @param key - the conversation key. * @param sessionId - the session to continue; undefined returns to derivation. * @returns whether it changed, and whether it survives a restart. */ set(key: string, sessionId: string | undefined): Promise<{ changed: boolean; durable: boolean; }>; } /** * Whether one session is work an agent delegated to itself rather than a * conversation someone had. * * The distinction is the host's, not this channel's invention, and the host * draws it firmly: a delegated session opens with an instruction its parent * wrote, its approval policy is pinned to `never` so nothing in it can ever * ask a human anything, and its own prompt tells it to report limitations back * to the parent instead. Nobody was ever in one, which is why offering one as * something to "continue" reads as noise — three rows of "你是代码仓库分析专家…" * burying the conversation someone is actually looking for. * * Three header facts say it, and any of them is enough: a deployment that * stamps only one still gets the right answer. * @param record - the corpus record to judge. * @returns true when the session belongs to delegated work. */ export declare function isDelegated(record: HostSessionRecord): boolean; /** What the picker needs to know about the conversation it is built for. */ export interface SessionPickerInput { /** This conversation's own base session id, before workspace and epoch suffixes. */ readonly base: string; /** The session this conversation resolves to right now. */ readonly current: string; /** The canonical workspace whose sessions are on offer. */ readonly workspace: string; /** The channel's session-id marker, so another surface's sessions can be told apart. */ readonly marker: string; /** Optional keyword, matched against titles and ids. */ readonly keyword?: string | undefined; /** Sessions the operator archived; the host hides these from every surface. */ readonly archived?: ReadonlySet | undefined; } /** What the picker offers a conversation, and what it is leaving out. */ export interface OfferedSessions { /** Exactly the rows the card draws — and the only ids a press may name. */ readonly rows: readonly SessionChoice[]; /** Older ones the card does not draw, so it can say how many. */ readonly hidden: number; } /** * The sessions one conversation may continue, newest first. * * Filtering happens here rather than in the card, because what is offered IS * the authorization: a row that never appears cannot be pressed, and no other * check stands between a press and a resumed session. * @param records - the host's corpus listing. * @param titles - folded titles by session id. * @param input - the conversation the picker is for. * @param canonical - resolves one path to its canonical form for comparison. * @returns the choices to offer, in card order. */ export declare function sessionChoices(records: readonly HostSessionRecord[], titles: ReadonlyMap, input: SessionPickerInput, canonical: (path: string) => string): SessionChoice[]; /** * Read the titles of several sessions, tolerating the ones that cannot be read. * * The host isolates failures per session, so a corpus with one unreadable log * still yields every other title — and a session whose title cannot be folded * simply shows without one. * @param query - the host session-query service. * @param ids - the sessions to fold titles for. * @param signal - cancellation for the batch. * @returns titles by session id; absent for anything unreadable or untitled. */ export declare function readTitles(query: HostSessionQuery, ids: readonly string[], signal?: AbortSignal): Promise>; /** Everything one derivation of the picker needs, and nothing about a chat. */ export interface SessionOffer { /** The host's session-query engine. */ readonly query: HostSessionQuery; /** The conversation the list is built for. */ readonly scope: SessionPickerInput; /** Resolves a path to its canonical form, so a symlinked workspace matches. */ readonly canonical: (path: string) => string; /** Cancellation for the reads this makes. */ readonly signal?: AbortSignal | undefined; /** Where a listing failure is reported; the picker itself degrades to empty. */ readonly report?: ((line: string) => void) | undefined; } /** * The sessions one conversation may continue right now, and how many more it * has that the card will not show. * * Derived on every call rather than remembered: sessions appear while a chat * is idle — the web UI opens one, `/new` leaves one behind — and a list built * once would offer yesterday's answer to today's press. * * What comes back IS what the card draws, and a press is authorized against * exactly this. One list, one boundary. * @param offer - the query, the conversation's scope, and how to canonicalize. * @returns the rows to offer, newest activity first, and the hidden count. */ export declare function offerSessions(offer: SessionOffer): Promise; //# sourceMappingURL=sessions.d.ts.map