/** * Per-session focus_state keying (T11345 · Epic T11284 · SG-COGNITIVE-SUBSTRATE). * * `focus_state` is the per-agent work-state blob (currentTask, sessionNotes, * nextAction, …). Historically it lived under ONE global meta key * (`focus_state`) shared by every agent — so two concurrent agents writing * their current task clobbered each other. This module keys it per resolved * session id (`focus_state:`). Nothing writes the legacy global * key any more (T12501 AC2): an unbound caller may still READ it, and a bound * session migrates a live pointer out of it once. * * Single source of truth: every read/write callsite across engine-ops, * briefing, drift-watchdog, session-drift, session-switch, and orchestrate/pivot * routes through {@link readFocusState} / {@link writeFocusState}, and every * caller picks its session through {@link resolveFocusSessionId}, so the * keying is never duplicated (T12501). * * @task T11345 * @epic T11284 */ import type { TaskWorkState } from '@cleocode/contracts'; /** * The legacy global focus_state meta key, shared by all agents before T11345. * * READ-ONLY LEGACY DATA (T12501): only a caller bound to no session reads it * ({@link resolveFocusSessionId} returned `null`), and nothing writes a focus * into it — {@link writeFocusState} takes a bound session id only, so unbound * terminals can no longer overwrite each other here. A bound session's focus * lives only under `focus_state:`. The exceptions only ever CLEAR its pointer: a * bound session with no key yet adopts a live legacy pointer once on upgrade * ({@link readFocusState}), a bound `cleo stop` releases it * ({@link releaseLegacyPointer}), and {@link clearFocusForFinishedTask} clears * a pointer to a finished task wherever it is. * * @task T11345 * @task T12501 */ export declare const LEGACY_FOCUS_STATE_KEY: "focus_state"; /** * Minimal structural view of the metadata accessor used by the focus-state * helpers. Declared locally (not importing the full `DataAccessor`) so this * module stays decoupled from the store implementation — any object exposing * the two meta methods satisfies it. * * @task T11345 */ export interface FocusStateMetaAccessor { getMetaValue(key: string): Promise; setMetaValue(key: string, value: unknown): Promise; /** * Run a read-modify-write atomically (the store's write transaction). When * present, {@link clearFocusForFinishedTask} compares and clears each key * inside it, so a concurrent focus write is never overwritten (T12689). */ transaction?(fn: (tx: { setMetaValue(key: string, value: unknown): Promise; }) => Promise): Promise; } /** * THE focus-key rule (T12501): the session whose focus key the CALLER reads * and writes. Every focus reader and writer — start, stop, pivot, complete's * clear, `cleo current`, session status, briefing, inject, bootstrap, stats, * validation, attention and orchestrator startup — resolves through this one * function, so a write and the read that follows it can never land on * different keys. * * It is the mutation resolver (`resolveBoundSessionId` in `store/session-store.ts`): the daemon * connection handle, then an env-named session whose row exists, then the * session this terminal started or resumed. It never guesses the newest * active row — from an unbound terminal that is another agent's session, and * its focus is not this caller's. * * `null` means the caller is bound to no session, or there is no session store * at `cwd` (absent — never created by this read — or predating the session / * binding tables). A reader then reads the legacy global key * ({@link focusStateKey}); a writer refuses ({@link requireFocusSessionId}). * Any other store failure is rethrown, so a fault is never reported as * "unbound". * * @param cwd - Project root for session resolution. * @returns The bound session id, or `null` when the caller is unbound. * @throws The store error, unless the store lacks the session / binding tables. * @example * ```ts * const focus = await readLiveFocus(acc, await resolveFocusSessionId(projectRoot)); * ``` * @task T12501 */ export declare function resolveFocusSessionId(cwd?: string): Promise; /** * Compute the focus_state meta key for a session id from * {@link resolveFocusSessionId} (or an explicitly named session). * * - A non-empty session id → the per-session key `focus_state:`. * - `null` / `undefined` (the caller is bound to no session) → the legacy * global key, {@link LEGACY_FOCUS_STATE_KEY}. Only READS land here: an * unbound caller may read pre-upgrade legacy focus, but no writer can reach * this branch — {@link writeFocusState} takes a bound id only (T12501 AC2). * * @param sessionId - Resolved session id, or `null` when none. * @returns The meta key to read/write. * @task T11345 */ export declare function focusStateKey(sessionId: string | null | undefined): string; /** * The focus session id for a caller already resolved by the read resolver * (`resolveSessionForRead`): its own session when bound, `null` when the row * is only the newest active one. Equal to {@link resolveFocusSessionId} — the * read resolver tries the same bound tiers first — so a surface that needs * both the session row and the focus key resolves once (T12501). * * @param read - The read resolution: the session and its `unbound` label. * @returns The bound session id, or `null` for an unbound caller. * @example * ```ts * const read = await resolveSessionForRead(root); * const focus = await readLiveFocus(acc, focusSessionIdFromRead(read)); * ``` * @task T12501 */ export declare function focusSessionIdFromRead(read: { readonly session: { readonly id: string; } | null; readonly unbound: boolean; }): string | null; /** * Read the RAW focus_state blob for a session id — for read-modify-write * only. Anything that reports or acts on the current task reads * {@link readLiveFocus}, which never returns a finished task (T12684). * * Reads {@link focusStateKey}. For an unbound caller (`null`) that is the * legacy global key: pre-upgrade data that nothing writes any more (T12501 * AC2). It is never read as a bound session's focus — except once, on upgrade: when the * session's key is ABSENT (not merely `currentTask: null`) and the legacy key * holds a live pointer, the session adopts it and the legacy pointer is * cleared, so no second session can adopt it too (T12501). * * @param accessor - Metadata accessor. * @param sessionId - Session id from {@link resolveFocusSessionId}, or `null` * for an unbound caller (reads the read-only legacy key). * @returns The focus_state blob, or `null` when there is none. * @task T11345 * @task T12501 */ export declare function readFocusState(accessor: FocusStateMetaAccessor, sessionId: string | null | undefined): Promise; /** * Clear the legacy global pointer when it names `taskId` — for a BOUND * caller's `cleo stop`, so a pre-upgrade pointer to the task it stopped is not * left behind for unbound callers (T12501). Compare-and-set on the pointer. * * @param accessor - Metadata accessor. * @param taskId - The task being stopped. * @returns `true` when the legacy pointer was cleared. * @task T12501 */ export declare function releaseLegacyPointer(accessor: FocusStateMetaAccessor, taskId: string): Promise; /** * Write the focus_state blob for a BOUND session (T11345 · T12501). * * Only a session's own key `focus_state:` is ever written, so * concurrent sessions never clobber each other. There is no unbound form: * `sessionId` is a `string`, and an empty one throws `E_SESSION_UNBOUND`. * Before T12501 AC2 a `null` id wrote the legacy global key, which every * unbound terminal shared, so two terminals' `cleo start` overwrote each * other. Writers resolve the id with {@link requireFocusSessionId}. (The legacy * key is touched only to CLEAR a pointer during migration: adoption on * upgrade, {@link releaseLegacyPointer} and {@link clearFocusForFinishedTask}.) * * @param accessor - Metadata accessor. * @param sessionId - The caller's bound session id (never null or empty). * @param value - The focus_state blob to persist. * @throws CleoError `SESSION_UNBOUND` when `sessionId` is empty. * @task T11345 * @task T12501 */ export declare function writeFocusState(accessor: FocusStateMetaAccessor, sessionId: string, value: TaskWorkState): Promise; /** * Message for refusing a focus write from an unbound caller (T12501). * * @param operation - What the caller tried to do (e.g. `start work on T12`). * @param stableTerminal - `false` when the caller has no stable terminal * identity (no key at all, or only a legacy ppid key): a binding written from * such a shell does not survive to its next command, so the remedy is the id * its own `session start` printed — never an id read from `session status`, * which may show ANOTHER agent's newest session (labelled `unbound: true`). * @returns The refusal text. * @task T12501 */ export declare function focusUnboundMessage(operation: string, stableTerminal?: boolean): string; /** * Resolve the caller's focus session for a focus WRITE, refusing when unbound * (T12501 AC2 · T12500). * * Same resolver as every reader ({@link resolveFocusSessionId}); where a reader * treats `null` as "unbound", a writer refuses with `E_SESSION_UNBOUND` and the * standard bind remedies instead of writing the shared legacy key. It refuses * even when no session is active at all: there is still no key it may write. * * @param operation - What the caller is attempting, for the message. * @param cwd - Project root for session resolution. * @returns The bound session id. * @throws CleoError `SESSION_UNBOUND` when the caller is bound to no session. * @example * ```ts * const sessionId = await requireFocusSessionId(`start work on ${taskId}`, projectRoot); * await writeFocusState(acc, sessionId, focus); * ``` * @task T12501 */ export declare function requireFocusSessionId(operation: string, cwd?: string): Promise; /** * Clear the focus pointer to a task that just finished, in every scope that * holds it: each given session key and the legacy global key (T12660). * * Only a blob whose `currentTask` IS `taskId` is touched, and only that field * is cleared — notes and phase stay. Nothing else ever cleared the legacy key, * so `cleo current` kept reporting a task done weeks earlier. * * @param accessor - Metadata accessor. * @param sessionIds - Session ids whose scoped key may hold the pointer * (the caller's {@link resolveFocusSessionId}); duplicates and nulls are * ignored. * @param taskId - The completed task. * @returns The meta keys that were cleared (empty when none pointed at it). * @task T12660 */ export declare function clearFocusForFinishedTask(accessor: FocusStateMetaAccessor, sessionIds: ReadonlyArray, taskId: string): Promise; /** A focus pointer that names a task no longer workable (T12660). */ export interface StaleFocusPointer { /** Task the pointer names. */ taskId: string; /** Its live status, or `missing` when the task no longer exists. */ status: string; } /** * Whether a focus pointer is stale: its task is done, cancelled, archived or * missing. A stale pointer is never reported as the current task (T12660). * * @param taskId - Pointer value. * @param liveStatus - The task's live status, or `undefined` when it is missing. * @returns The stale pointer, or `null` when the task is still workable. * @task T12660 */ export declare function staleFocusPointer(taskId: string, liveStatus: string | undefined): StaleFocusPointer | null; /** * One-line warning for a stale focus pointer, naming the task, its status and * what to do next. * * @param stale - The stale pointer. * @param next - The next ready task, when one exists. * @returns The warning text. * @task T12660 */ export declare function staleFocusWarning(stale: StaleFocusPointer, next: { id: string; title: string; } | null): string; /** The metadata accessor plus a live task lookup, for {@link readLiveFocus}. */ export interface LiveFocusAccessor extends FocusStateMetaAccessor { /** Load one task by id (any status, archived included); null when missing. */ loadSingleTask(taskId: string): Promise<{ status: string; } | null>; } /** A focus read validated against the pointed task's live status (T12684). */ export interface LiveFocus { /** The stored blob (notes, phase, …), or null when there is none. */ state: TaskWorkState | null; /** The current task — only while it is still workable, else null. */ currentTask: string | null; /** The pointer, when it names a done, cancelled, archived or missing task. */ staleFocus: StaleFocusPointer | null; } /** * THE focus reader for anything that reports or acts on the current task * (T12684): `cleo current`, briefing, inject, bootstrap, orchestrator startup, * stats, validation, attention and the drift watchdog. A pointer to a done, * cancelled, archived or missing task comes back as `currentTask: null` plus * `staleFocus` — never as the current task. The stored key is not touched: * another session completing a task leaves this session's key alone, and this * read reports it stale. * * @param accessor - Metadata accessor with a live task lookup. * @param sessionId - Session id from {@link resolveFocusSessionId}, or `null` * for an unbound caller (the legacy key). * @param known - Tasks the caller already loaded (e.g. the briefing's task * map): a pointer found there skips the extra lookup (T12698). A pointer * absent from it (archived tasks are not listed) is still looked up. * @returns The blob, the live current task, and the stale pointer if any. * @task T12684 */ export declare function readLiveFocus(accessor: LiveFocusAccessor, sessionId: string | null | undefined, known?: ReadonlyMap): Promise; //# sourceMappingURL=focus-state-store.d.ts.map