/** * Artifact store abstraction (Phase 2): stable refs for raw large payloads, kept out of * band from prompt context. This module defines the `ArtifactStore` interface plus two * implementations: an in-memory one for tests, and `createFileArtifactStore` (session- * scoped, filesystem-backed). A SQLite-backed implementation waits until the Phase M0 * storage-authority/location/concurrency decisions are accepted (see * docs/context-management-rework/memory-architecture.md). * * `createFileArtifactStore` is wired into live grep/find tool construction in * agent-session.ts (session-scoped under `/work/context/sessions//artifacts/`). * References are registered at pack time and released when context-gc evicts the * corresponding grep/find tool result (opportunistic, conservative cleanup), with a * best-effort dispose-time sweep for zero-reference artifacts. Payloads are retrievable * out of band via the artifact_retrieve tool (context/artifact-retrieval.ts). */ import { type ContextArtifactRef } from "./context-item.ts"; export interface ArtifactWriteRequest { kind: ContextArtifactRef["kind"]; content: string; toolName?: string; command?: string; path?: string; sessionEntryId?: string; createdAtTurn: number; reproducible: boolean; } export interface ArtifactRecord { ref: ContextArtifactRef; content: string; } export type MissingArtifactReason = "not_found" | "cleaned_up"; export interface MissingArtifactMarker { id: string; missing: true; reason: MissingArtifactReason; } export declare function isMissingArtifactMarker(value: ArtifactRecord | MissingArtifactMarker): value is MissingArtifactMarker; /** * Artifact id for a capture event, not merely a payload: it hashes every ref-defining * field (kind, tool/command/path, content, sessionEntryId, createdAtTurn, reproducible). * A repeat write with identical content but a different turn or session entry is a * distinct capture and must get a distinct id -- otherwise the later capture's metadata * would be silently discarded in favor of the first write. Only a truly identical * request (same capture, re-submitted) is idempotent under this id. */ export declare function generateArtifactId(request: Pick): string; export interface ArtifactStore { write(request: ArtifactWriteRequest): ArtifactRecord; read(id: string): ArtifactRecord | MissingArtifactMarker; /** * Metadata-only lookup: the ref if `id` resolves to a live artifact, `undefined` * otherwise. Never loads the payload -- for the file store this must not touch the * payload file at all beyond an existence check, so a caller that only needs to know * "does this still exist, and what are its ref fields" (e.g. a per-turn audit pass) * never pays the cost of reading potentially large content off disk. */ readRef(id: string): ContextArtifactRef | undefined; has(id: string): boolean; /** * Register that `holderId` (a context item id, session entry id, etc.) depends on this * artifact. Returns false if `id` does not exist (never written, or already cleaned * up) so a caller cannot believe it protected an artifact that was never registered. * Callers must fail closed (treat the artifact as unprotected) on a false return. */ addReference(id: string, holderId: string): boolean; /** Release a previously registered dependency. Returns true only if a reference was actually removed. */ removeReference(id: string, holderId: string): boolean; referenceCount(id: string): number; /** Delete only artifacts with zero active references. Returns the ids actually deleted. */ cleanup(): string[]; } export declare function createInMemoryArtifactStore(): ArtifactStore; export interface FileArtifactStoreOptions { /** Leased session work directory where payloads and metadata live. Created on first use. */ baseDir: string; /** Migrate an existing store before probing it; must remain zero-write when no legacy data exists. */ prepareBaseDir?: () => void; /** Acquire the owning lease immediately before the first real store access. */ acquireBaseDir?: () => string; } /** * Filesystem-backed `ArtifactStore`: payload and metadata (including reference holder ids) * are written to `baseDir` so content, ref fields, and cleanup-protecting references all survive * recreating the store (e.g. across a process restart against the same directory) -- unlike the * in-memory store, which loses everything when the instance is dropped. Each artifact has a small * metadata sidecar plus one hashed holder marker per active reference. Marker updates are O(1) and * every lifecycle mutation shares the artifact lock, so foreground/background processes cannot * lose ownership through a shared-array read-modify-write race. * * The one thing that does NOT survive recreation: the missing-artifact reason * distinction. A fresh instance has no in-memory record of which ids it personally * cleaned up, so a previously-cleaned-up id reads back as "not_found" rather than * "cleaned_up" after a restart. This still always returns an explicit missing marker, * never fabricated or empty content -- it only affects which of the two reason codes is * reported. */ export declare function createFileArtifactStore(options: FileArtifactStoreOptions): ArtifactStore; //# sourceMappingURL=context-artifacts.d.ts.map