import { StatelogClient } from "../../statelogClient.js"; import { MessageThread, MessageThreadJSON } from "./messageThread.js"; export type ThreadStoreJSON = { threads: Record; counter: number; activeStack: string[]; /** session-name → thread-id map. Populated by openSession on first * entry; subsequent entries with the same name resume the recorded * thread via resumeExisting. Round-tripped so sessions survive * interrupt resume / cross-node hops. */ sessions?: Record; }; export type MessageThreadID = string; export declare class ThreadStore { /** * Shared long-lived registry (threads/sessions/counter). The * top-level per-run ThreadStore owns its registry; branch views * built via `forkBranchView()` alias the parent's registry so * `threads`, `sessions`, and `counter` reads/writes from any * branch are visible everywhere. Per-branch divergence lives in * `activeStack` (a plain instance field below). */ private registry; /** Per-branch active-thread stack. Each branch view has its own * copy seeded by `forkBranchView` so a branch's `pushActive` / * `popActive` / `llm()` calls write to a branch-local subthread * instead of the parent's currently-active thread. */ activeStack: MessageThreadID[]; constructor(); get threads(): Record; set threads(value: Record); get sessions(): Record; set sessions(value: Record); get counter(): number; set counter(value: number); private get statelogClient(); setStatelogClient(client: StatelogClient): void; /** * Build a per-branch view of this store. Used by `runInBranchAlsFrame` * so each fork / parallel / race branch gets its own active-thread * pointer without losing access to the shared registry — explicit * cross-branch coordination via `thread(session: ...)` or * `thread(continue: id)` keeps working because both still consult * the same `threads` / `sessions` maps. * * The returned view: * - Aliases this store's `ThreadRegistry` by reference. New * threads created in the branch (`create`, `createSubthread`, * session opens) land in the shared registry. The counter is * also shared so branch-created ids never collide. * - Has a fresh `activeStack` seeded with a new subthread of this * store's currently-active thread (if any). Unguarded `llm()` / * `userMessage()` calls inside the branch write to that * subthread rather than the parent's active thread. * * If this store has no active thread (rare — typically only true * for fresh `new ThreadStore()` outside a run), the view starts * with an empty `activeStack` and any unguarded thread access in * the branch will throw the usual "no active thread" error. */ forkBranchView(): ThreadStore; /** * Build a per-branch view that restores a previously-captured * `activeStack` rather than seeding a fresh subthread. Called by * `runInBranchAlsFrame` on resume after an interrupt inside a fork * branch: the pre-interrupt active-thread pointer was snapshotted * onto `BranchState.activeStack` and serialized; on re-entry we * recreate a branch view aliasing the (now-deserialized) parent's * registry and reinstate the exact same activeStack so the resumed * branch's unguarded `llm()` / `userMessage()` calls land on the * same subthread they were writing to pre-interrupt. * * Does NOT create a new subthread — the subthread that was originally * created at first-fork-time still lives in the shared registry and * its id is in `activeStack`. Statelog is not re-emitted. */ restoreBranchView(activeStack: MessageThreadID[]): ThreadStore; static withDefaultActive(client?: StatelogClient): ThreadStore; /** Optional metadata captured from `thread(...)` opts at creation * time. Both applied to the new MessageThread (so runtime behavior * respects them — `hidden`, `label`) AND forwarded to the * `threadCreated` statelog event (so log consumers can identify * which subagent each thread corresponds to without fingerprinting). */ create(meta?: { label?: string | null; session?: string | null; hidden?: boolean; }): MessageThreadID; createAndReturnThread(): MessageThread; createSubthread(meta?: { label?: string | null; hidden?: boolean; }): MessageThreadID; /** Registry-only subthread creation: build a subthread that * inherits from the thread at `parentRegistryId`, log it, and * return its new registry id. Does NOT touch `activeStack` — * callers decide where (if anywhere) to push the new id. Used by * `createSubthread()` (which pushes onto `this.activeStack`) and * `forkBranchView()` (which pushes onto the view's activeStack). */ createSubthreadOf(parentRegistryId: MessageThreadID, meta?: { label?: string | null; hidden?: boolean; }): MessageThreadID; createAndReturnSubthread(): MessageThread; get(id: MessageThreadID): MessageThread; pushActive(id: MessageThreadID): void; popActive(): MessageThreadID | undefined; activeId(): MessageThreadID | undefined; active(): MessageThread | undefined; getOrCreateActive(): MessageThread; /** Re-activate a previously-closed thread. Pushes `id` onto the * active stack (same path as pushActive) without creating a new * MessageThread. Throws if `id` is unknown — a silent fallback to * create-new would mask a real bug (typo, hallucinated id) at the * call site. * * v1: rejects subthreads. A subthread's identity is tied to its * parent's context at the time it was created; resuming one * outside that context could surface confusing message ordering. * If a user wants to continue inside a subthread, they should * resume the parent thread and open a fresh `subthread {}` block. */ resumeExisting(id: MessageThreadID): void; /** Open a named session. Returns `{id, existed}` — `existed` is * `true` when this name already mapped to a thread. Always leaves * the session's thread on top of `activeStack`. First entry * creates a top-level thread (never a subthread); later entries * resume via `resumeExisting` (which already rejects subthreads — * sessions can only map to top-level threads). */ openSession(name: string, meta?: { label?: string | null; hidden?: boolean; }): { id: MessageThreadID; existed: boolean; }; toJSON(): ThreadStoreJSON; static fromJSON(json: ThreadStoreJSON | ThreadStore): ThreadStore; }