/** * * Each caller has its own on-disk binding file at * `.peaks/_runtime/callers/.json`. This service is the * single read/write surface for that file. Legacy single-file bindings * (`.peaks/_runtime/session.json` and `.peaks/.session.json`) remain * readable for one minor release (M1 / M4); the read path falls back * to them with a `legacy-fallback-used: true` flag. * * M2: legacy bindings resolve into a synthetic callerId of the form * `legacy-<8hex-of-sha256(outerSessionId)>`, with `claudeSessionId` * and `projectRoot` as fallback hash inputs. The synthetic id is * permanent and recognisable by the `legacy-` prefix. * * See `.peaks/_runtime/2026-06-09-session-8bfe7d/prd/source/caller-id-contract.md` * for the freeze-in contract. */ import { type CallerBinding } from './caller-id-types.js'; /** * On-disk location of the per-caller binding file. P4 invariant: * the `callers/.json` lives at `.peaks/_runtime/callers/`, * the same root as the per-peak session dirs. */ export declare function getCallerBindingFile(projectRoot: string, callerId: string): string; /** * On-disk location of the per-(peak, caller) active-skill file. D6: * one file per (peakSessionId, callerId) pair; two callers bound to * the same peak session never clobber each other. */ export declare function getActiveSkillFileForCaller(projectRoot: string, peakSessionId: string, callerId: string): string; /** * Resolve a stable, deterministic callerId for a legacy single-file * binding (M2). The hash input priority is: * * session.json for sessions created after the slice shipped). * 2. `claudeSessionId` (legacy field name on pre-018 presence * files; honour the read side so v1.2.x data does not lose its * binding). * 3. `projectRoot` (truly anonymous case: pre-018 sessions that * never recorded an outer / claude id). * * The synthetic id is `legacy-`. * 32 bits of entropy is enough for typical on-disk state * (R1: <100 legacy peak sessions per project; the test asserts * uniqueness across 1000 synthetic ids). */ export declare function synthesiseLegacyCallerId(input: string): string; /** * Read a per-caller binding file. Returns `null` if the file does * not exist, is malformed, or is for a different project (M1 back-compat * read returns the legacy file but only after the per-caller file is * absent). */ export declare function getCallerBinding(projectRoot: string, callerId: string): CallerBinding | null; /** * Resolution-time read of a per-caller binding. * * `getCallerBinding` answers a SHAPE question: "is there a well-formed * binding file for this caller?". Session resolution needs a second * (rid=caller-binding-staleness) found it is not when the bound session's * directory is gone — the binding was still trusted, so a command * resolved the dead session id and `mkdir`'d a fresh tree under * * Returns a tagged result rather than `null` so the fall-through is * observable to the caller instead of silent: * - `bound` — usable; resolve to `binding.peakSessionId`. * - `stale` — the file is well-formed but its session directory is * gone; callers fall through the chain and may report it. * - `absent` — no binding file (or a malformed one). */ export type CallerBindingResolution = { readonly status: 'bound'; readonly binding: CallerBinding; } | { readonly status: 'stale'; readonly binding: CallerBinding; } | { readonly status: 'absent'; }; export declare function resolveCallerBinding(projectRoot: string, callerId: string): CallerBindingResolution; /** * Write or update a per-caller binding file. The caller is responsible * for the binding object (callerId must match the file stem, peakSessionId * must be a valid session id, projectRoot is canonicalized). Idempotent: * re-writing the same callerId overwrites the file. * * (temp-file-then-rename) so a power-loss mid-write cannot leave the * caller-binding file in a half-truncated state. `atomicWriteJson` * owns its own `mkdirSync(dir, { recursive: true })`, so the inline * mkdir block is removed. The previous `writeFileSync` path was a * documented 4.0.14 carry-forward micro-fix from QA's issue #1. */ export declare function setCallerBinding(projectRoot: string, callerId: string, binding: CallerBinding): void; /** * Repoint an EXISTING per-caller binding at a new peak session id. * * `peaks workspace init --session-id --allow-session-rebind` rewrites * the project-global `.peaks/_runtime/session.json`. Without this call the * per-caller file keeps shadowing it for `getSessionIdCanonical`, so the * rebind silently did not take for every command that resolves through * that variant (`peaks session checkpoint`, `peaks session 24h-mode`, ...) * while `getCurrentSessionId` reported the new session. * * Only the binding of the caller that performed the rebind is repointed — * a second caller keeps its own session, which is the multi-caller * isolation the per-caller design exists for. * * Every other field is preserved: `createdAt` is the session creation * stamp the legacy `session.json` dual-write reuses, and `skill` / `mode` * / `gate` are live presence state. * * @returns `true` when a binding existed and was repointed, `false` when * the caller had no binding file (nothing was shadowing the rebind, and * we do not create one speculatively). */ export declare function updateCallerBindingSessionId(projectRoot: string, callerId: string, peakSessionId: string): boolean; /** * Enumerate the per-caller binding files under * `.peaks/_runtime/callers/`. Returns the parsed bindings plus the * raw filenames (so callers can list orphan / legacy files without * re-reading). */ export declare function listCallerBindings(projectRoot: string): CallerBinding[]; /** * Slice 4.0.8 (RD §6 migration): on a `presence:set` sweep, the * legacy per-caller `active-skill-.json` files are * migrated into the canonical `presence-index/.json` + * `leases/presence--.json` layout. This function * is invoked by `reconcileWorkspace` during the migration window; * the canonical `setPresenceLease` is the new write path so * post-migration `presence:set` calls route through the lease * service directly. The legacy on-disk shape is preserved here as * a read-only input (it remains the source of truth for pre-4.0.8 * callers that haven't migrated yet). */ export declare function reconcileLegacyCallerPresence(input: { projectRoot: string; callerId: string; peakSessionId: string; }): { migrated: boolean; reason: 'already-canonical' | 'missing-legacy' | 'success' | 'io-error'; error?: string; };