/** * Session ledger: durable session↔instance mapping (FR1). * * sessions.ndjson is append-only; the instance.yaml claim field remains * concurrency control only. Attribution intervals are derived here. * * The active-session stash (design §2.3) joins platform session ids to * claims: SessionStart hooks write stash entries; `aidlc claim` resolves * the current session with a strict single-candidate rule. * * Design: design.md §2.2, §2.3, §4.3 */ import type { SessionEvent, ClaimInterval, ActiveSession } from './types.js'; /** * How recently a stash entry must show activity to count as a *candidate* for * "which session am I?". * * Deletion (TTL) and candidacy (liveness) are different questions and the bug * came from answering only the first. `resolveCurrentSession` used to treat every * unexpired entry as a candidate, so each finished session poisoned resolution * for up to 48 hours; this repo carried 18 entries of which 3 were running. * * 30 minutes is chosen against the caller: `resolveCurrentSession` runs from * inside a live session (`aidlc claim`), whose transcript was appended to seconds * ago. Anything that has been silent for half an hour is not the session asking. */ export declare const STASH_LIVENESS_MS: number; export declare function sessionsPath(projectRoot: string, instance: string): string; export declare function stashDir(projectRoot: string): string; /** Append a session event (O_APPEND whole-line write). */ export declare function appendSessionEvent(path: string, event: SessionEvent): void; /** * Read valid session events; missing file → empty; bad lines skipped. * Byte-identical duplicate lines (merge=union artifacts, git-worktree * AC-20) collapse to one; distinct events always differ somewhere in the * line, so whole-line dedup never drops a legitimate event. * * `quiet` suppresses the skipped-lines warning. The pin resolver reads one log * per instance and `aidlc status` resolves every instance — 40 in this repo — so * an unconditional warning would fire 40 times on a status listing, on files * where `merge=union` makes a partial line plausible. `aidlc cost` reports on one * instance at a time and leaves the warning on, where it is actionable * (agent-portability/AC-33). */ export declare function readSessionEvents(path: string, opts?: { quiet?: boolean; }): SessionEvent[]; /** * Resolve attribution intervals from session events (FR1.3). * * claim → release pairs per session; an unreleased claim is bounded by * min(now, lastEventForSession + claimTimeoutMinutes) and marked open. * Renew events extend the liveness bound of an open claim. */ export declare function resolveIntervals(events: SessionEvent[], claimTimeoutMinutes: number, now?: Date): ClaimInterval[]; /** Write a stash entry for a just-started platform session. */ export declare function stashSession(projectRoot: string, session: ActiveSession): void; /** * Remove one session's stash entry — the step that never existed. * * Entries were written on SessionStart and removed only by a 48-hour timer, so a * finished session went on being a candidate for two days and * `resolveCurrentSession` refused to guess between it and the live one. The * SessionEnd hook already receives the ending session's id; this is what it does * with it. * * Returns whether an entry was removed. Idempotent by contract: an end hook can * fire twice, and a second call is a `false`, not an error. * * SECURITY: the id is regex-validated before it reaches `join`, so a hook payload * cannot steer the unlink outside the stash directory — the same rule * `stashSession` applies on the way in. */ export declare function unstashSession(projectRoot: string, sessionId: string): boolean; /** * Whether an entry looks like a session that is still running. * * Three cases, ordered by how much the entry lets us know: * * - declares a readable `transcript_path` → live iff it was appended to within * STASH_LIVENESS_MS. This is the real signal: a running session writes its * transcript every turn. * - declares a `transcript_path` that is not there → fall back to `started_at`. * A session can be seconds old with no transcript on disk yet (SessionStart * fires before the first message is written), so absence alone must not read * as death — but an entry claiming a transcript that never appeared and * starting hours ago is not running. * - declares no `transcript_path` → liveness is unmeasurable, so it stays a * candidate and only the TTL applies. Kiro supplies no transcript, and * excluding those entries would strip resolution from long kiro sessions that * work today. */ export declare function isLiveEntry(entry: ActiveSession, now?: Date): boolean; /** * Stash entries past the TTL (or unparseable — pruneStash removes those * too). Read-only half of pruneStash, shared with `aidlc doctor` (AC-18). * * Deliberately TTL-only: liveness decides candidacy, never deletion, so an idle * session's entry stays on disk and `aidlc doctor` keeps reporting exactly what it * removes. */ export declare function expiredStashEntries(projectRoot: string, now?: Date): string[]; /** * Remove stash entries older than the TTL. Called opportunistically. * Returns the removed filenames. */ export declare function pruneStash(projectRoot: string, now?: Date): string[]; /** * Resolve a directory to its repo identity (git common dir), or null. * * The anchor is canonicalized because two spellings of one directory otherwise * read as two repositories: a hook payload saying `/tmp/x` and a project root * resolved to `/private/tmp/x` produced *zero* candidates, which looks exactly * like "no session here". `cost/agent.ts:canonical` already does this, in its * non-git branch only. * * What is NOT changed: `join(cwd, out)`. `git rev-parse --git-common-dir` answers * relatively from a primary checkout and absolutely from a linked worktree, so * `join` concatenates rather than replaces and a worktree gets its own identity — * load-bearing for `resolveCurrentSession`, whose single-candidate rule wants a * worktree session and a primary-checkout session to look like different * workspaces. See the note at `sync.ts:repoCommonDir`, which needs the opposite * and therefore has its own function. */ export declare function repoIdentity(cwd: string): string | null; /** * Resolve symlinks for comparison; unresolvable paths compare as written. * * Shared with `cost/agent.ts`, which needs the same rule for the same reason — * two spellings of one directory must not compare as two directories. */ export declare function canonical(p: string): string; /** * Find the current platform session with the strict single-candidate rule: * exactly one **live** stash entry whose repo identity matches → that session; * zero or multiple → null (caller degrades to id_source: agent). * * The rule itself is unchanged and correct — two genuinely live sessions are * ambiguous, and guessing between them would attribute one session's cost to the * other's instance. What changed is the candidate set: `isLiveEntry` drops * sessions that have finished or died, which is what the rule always assumed and * nothing enforced. */ export declare function resolveCurrentSession(projectRoot: string, now?: Date): ActiveSession | null; //# sourceMappingURL=sessions.d.ts.map