/** * Inter-session messaging for concurrently running KONECK instances. * * Each live session registers itself and listens on a unix socket. Another session can hand work * over (`/send`) or ask a question and wait for the answer (`/ask`). Two decisions shape the * design: * * Addressing is by *place*, not by id. Nobody wants to type `/send session_9f82a`, so a target * resolves against the working directory and repository name first — `/send backend` finds the * session running in `~/dev/backend`. Ids remain available as `#a1b2` for the ambiguous case. * * Delivery never interrupts. A session in the middle of a tool call cannot safely take a new * instruction, so messages land in a queue and are drained when the current turn finishes. * * A unix socket is used rather than a TCP port because filesystem permissions are the * authorisation model: 0600 on the socket keeps other users out, and a per-session token keeps * unrelated local processes from injecting instructions into an agent that can write files. */ export interface SessionRecord { id: string; pid: number; cwd: string; repoName: string; socketPath: string; authToken: string; startedAt: string; /** Refreshed while the session lives; a stale one is how a crashed session is spotted. */ heartbeat: string; status: 'idle' | 'busy'; model: string; provider: string; } export type Envelope = { kind: 'send'; token: string; from: SessionSummary; instruction: string; payload: HandoffPayload; } | { kind: 'ask'; token: string; from: SessionSummary; question: string; } | { kind: 'ping'; token: string; }; export interface SessionSummary { id: string; repoName: string; cwd: string; } /** * What travels between sessions. Deliberately a summary, never a transcript: a raw history costs * thousands of tokens on the receiving side and mostly restates what the sender already resolved. */ export interface HandoffPayload { summary: string; changedFiles: string[]; instruction: string; /** Explicit outcome the sender was working toward, when one was set. */ goal?: string; /** Informational only: the receiver never inherits another session's safety controls. */ senderPosture?: { mode: 'auto' | 'edits' | 'careful' | 'plan'; effort: 'low' | 'medium' | 'high' | 'max'; requireApproval: boolean; }; } /** * Handoffs cross a process boundary. Redact at that boundary, not only in one UI, so every caller * gets the same guarantee and a future sender cannot accidentally turn a local socket into a * secret-forwarding channel. */ export declare function redactHandoffPayload(payload: HandoffPayload): HandoffPayload; export type Reply = { ok: true; answer?: string; } | { ok: false; error: string; }; /** * Sockets live in the temp directory, not beside the registry. * * A unix socket path is copied into sockaddr_un.sun_path, which is 108 bytes on Linux and 104 on * macOS — a limit the kernel enforces with a bare EINVAL that names nothing. Under a home * directory of any depth, `~/.koneck/sessions/.sock` passes that quietly: one observed path * was 138 bytes and every connection failed. The temp directory keeps the whole path near thirty * characters regardless of where the session runs. */ declare function socketPathFor(id: string): string; /** Longest path the kernel will accept in sockaddr_un, with a byte spare. */ declare const SUN_PATH_MAX: number; export { socketPathFor, SUN_PATH_MAX }; /** * The name a session answers to: its repository if it is in one, otherwise its own directory. * * The search stops at the home directory rather than continuing to the filesystem root. Plenty * of people keep their dotfiles in a repo at $HOME, and walking past it made every session * anywhere under home resolve to the same name — observed live, where three sessions in * different projects all reported "kona" and no /send target could be told apart. A repo at * $HOME only counts when the session is actually sitting in $HOME. */ export declare function repoNameFor(cwd: string, home?: string): string; /** A process that no longer exists, or has stopped beating, is not a session. */ export declare function isAlive(record: SessionRecord): boolean; /** Every live session, with dead ones pruned from the registry as a side effect. */ export declare function listSessions(): SessionRecord[]; /** * Finds the session a target refers to. Accepted, in order of specificity: `#id`, an exact id, an * exact repo or directory name, then a unique prefix match. Ambiguity is reported rather than * guessed — sending work to the wrong repository is worse than being asked to be specific. */ export declare function resolveTarget(target: string, selfId?: string): { ok: true; session: SessionRecord; } | { ok: false; error: string; }; export interface BusHandlers { /** A handoff arrived. Returns once queued; it does not wait for the work to be done. */ onSend: (from: SessionSummary, payload: HandoffPayload) => void; /** A question arrived. The answer is sent back to the caller, which is blocked meanwhile. */ onAsk: (from: SessionSummary, question: string) => Promise; } export interface Bus { record: SessionRecord; setStatus(status: 'idle' | 'busy'): void; close(): void; } /** * Registers this session and starts listening. Failure is non-fatal: messaging is a convenience, * and a session that cannot open a socket should still be a working agent. */ export declare function startBus(opts: { id: string; cwd: string; model: string; provider: string; }, handlers: BusHandlers): Bus | null; /** Sends one message to a session and waits for its reply. */ export declare function deliver(to: SessionRecord, message: Envelope, timeoutMs?: number): Promise; /** Removes sockets and registry entries for sessions that are no longer running. */ export declare function pruneDead(): Promise; //# sourceMappingURL=session-bus.d.ts.map