/** * DiskSessionStore — filesystem-backed implementation of * {@link SessionStore}. * * Every mutation is write-tmp-rename (Convention #8). Directory layout * matches session-hierarchy.md §7 / §13.4: * * {rootDir}/projects/{projectId}/ * project.json * sessions/{sessionId}/ * session.json * summary.json * subsessions/{subSessionId}/ * subsession.json * * It holds the session ENTITIES (status, actor, ownership version, * sub-session edges, summaries) and nothing of the conversation. A session's * messages are records in its session log (`SessionLog`, * `~/.namzu/projects//.jsonl`), read through * `foldSessionMessages` and written only by the turn recorder under the * session lease; listing turns and children across sessions is the * `SessionIndex`. This store no longer writes `messages.jsonl`, and one left * behind by an older build is never read. * * Tenant scoping is enforced through the JSON payload (`tenantId` field on * every record) rather than the path layout; cross-tenant reads reject with * {@link TenantIsolationError} (Convention #17, session-hierarchy.md §12.2). * * Constructor takes `rootDir`. This entity tree is NOT the session layout: * it is keyed by project id, so a `rootDir` equal to `NAMZU_HOME` puts * UUID-named directories under `projects/`, which `namzu state` reports as * legacy. Give it a directory of its own. Moving the entities onto the * session log and index needs record types for session status, ownership * and sub-session edges that the log does not have yet. */ import type { SessionId, TenantId } from '../../types/ids/index.js'; import type { Project, ProjectStatus } from '../../types/project/entity.js'; import type { Session } from '../../types/session/entity.js'; import type { ProjectId, SubSessionId, TopicId } from '../../types/session/ids.js'; import type { CreateProjectParams, CreateSessionParams, CreateSubSessionParams, ProjectConfigInput, SessionStore, SessionView } from '../../types/session/store.js'; import type { SubSession } from '../../types/session/sub-session.js'; import type { SessionSummaryRef } from '../../types/summary/ref.js'; /** * Validate topic UUIDs before accepting a stored record. Prefixed IDs are * rejected without rewriting stored references. * This migrator runs over every session-store record kind; records without * a topicId are intentionally untouched. */ export declare function migrateSessionStoreTopicIdPrefix(record: Record): Record; /** * v1 → v2: the FK field `session.json` carries to its owning Topic was * spelled `threadId`. NZ-TOPIC-01 renamed the layer, not the field * (comment on `types/topic/store.ts`); NZ-TOPIC-03 is that rename landing, * with this as its data migration. * * One migration function runs over every kind this schema stamps — * project.json, session.json, subsession.json and summary.json — via the * single shared `readJson` / `migrate` call. Only `PersistedSession` ever * carried `threadId`; an unconditional rewrite here would stamp * `topicId: undefined` onto the other kinds. Exported, not module-private, * so that guarantee is unit-testable directly against the function rather * than only observable through whichever deserializer happens to forward * an extra field today (most of them don't — they map named fields, which * is exactly why a stray key here would otherwise go unnoticed). NOT part * of the package's public surface: `store/session/index.ts` re-exports * `DiskSessionStore` by explicit name only, no wildcard. */ export declare function migrateSessionStoreThreadIdToTopicId(record: Record): Record; /** * Config for {@link DiskSessionStore}. `rootDir` is absolute; all files live * under it per the layout documented in the module header. */ export interface DiskSessionStoreConfig { rootDir: string; } export declare class DiskSessionStore implements SessionStore { private readonly rootDir; private readonly projectIndex; private readonly sessionIndex; private readonly subSessionIndex; constructor(config: DiskSessionStoreConfig); createProject(params: CreateProjectParams, tenantId: TenantId): Promise; /** * Reads ONE index file. Not a scan of `projects/*` — that opens every * `project.json` on the machine to answer a question about one * directory, and gets slower with every project a host has ever made. */ findProjectByRootPath(rootPath: string, tenantId: TenantId): Promise; private boundProjectId; private rootPathIndexPath; private rootPathBindingLocation; private publishRootPathBinding; getProject(projectId: ProjectId, tenantId: TenantId): Promise; updateProject(projectId: ProjectId, config: ProjectConfigInput, tenantId: TenantId): Promise; setProjectStatus(projectId: ProjectId, status: ProjectStatus, tenantId: TenantId, expectedOwnerVersion: number): Promise; listProjects(tenantId: TenantId): Promise; createSession(params: CreateSessionParams, tenantId: TenantId): Promise; getSession(sessionId: SessionId, tenantId: TenantId): Promise; listSessionsByTopic(topicId: TopicId, tenantId: TenantId): Promise; listSessionsByProject(projectId: ProjectId, tenantId: TenantId): Promise; updateSession(session: Session, tenantId: TenantId, expectedOwnerVersion?: number): Promise; deleteSession(sessionId: SessionId, tenantId: TenantId): Promise; createSubSession(params: CreateSubSessionParams, tenantId: TenantId): Promise; getSubSession(subSessionId: SubSessionId, tenantId: TenantId): Promise; updateSubSession(subSession: SubSession, tenantId: TenantId): Promise; deleteSubSession(subSessionId: SubSessionId, tenantId: TenantId): Promise; getChildren(sessionId: SessionId, tenantId: TenantId): Promise; getAncestry(sessionId: SessionId, tenantId: TenantId): Promise; drill(sessionId: SessionId, tenantId: TenantId): Promise; /** * Atomic materialize-with-terminal-transition (§8.1). Two write-tmp-renames: * * 1. Persist `summary.json` under the session directory. * 2. Flip `session.json#status` to `'idle'` if it's in a non-terminal * state (`'active' | 'locked' | 'awaiting_merge'`). * * Each rename is atomic individually. A crash between step 1 and step 2 * leaves summary present + session still non-terminal — recovery replays * the flip via {@link SessionSummaryMaterializer.recover}. Idempotent when * the same summary is re-presented (recovery path); rejects a *different* * summary for the same session as {@link SessionAlreadySummarizedError}. */ recordSummary(summary: SessionSummaryRef & { materializedBy: 'kernel'; }, tenantId: TenantId): Promise; getSummary(sessionId: SessionId, tenantId: TenantId): Promise; private assertTenant; private projectDir; private locateSession; private locateSubSession; private buildLinkageView; } //# sourceMappingURL=disk.d.ts.map