/**
 * 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
 *       messages.jsonl
 *       subsessions/{subSessionId}/
 *         subsession.json
 *
 * 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`; migration to the canonical `.namzu/projects/`
 * path lives in Phase 7 of the overall roadmap.
 */
import { appendFile, mkdir, readFile, readdir, rename, rm, unlink, writeFile, } from 'node:fs/promises';
import { join } from 'node:path';
import { TenantIsolationError } from '../../session/errors.js';
import { SessionAlreadySummarizedError } from '../../session/summary/errors.js';
import { generateMessageId, generateProjectId, generateSessionId, generateSubSessionId, } from '../../utils/id.js';
import { getAncestry, getChildren, orderChildren } from './linkage.js';
/**
 * Non-terminal statuses from which {@link DiskSessionStore.recordSummary}
 * flips the owning session to `'idle'` as part of the atomic materialize +
 * transition contract (session-hierarchy.md §8.1).
 */
const SUMMARY_TERMINAL_FLIP_STATUSES = new Set([
    'active',
    'locked',
    'awaiting_merge',
]);
export class DiskSessionStore {
    rootDir;
    projectIndex = new Map();
    sessionIndex = new Map();
    subSessionIndex = new Map();
    constructor(config) {
        this.rootDir = config.rootDir;
    }
    // Project CRUD ------------------------------------------------------------
    async createProject(params, tenantId) {
        if (params.tenantId !== tenantId) {
            throw new TenantIsolationError({
                requested: tenantId,
                resource: `project(name=${params.name})`,
            });
        }
        const now = new Date();
        const project = {
            id: generateProjectId(),
            tenantId,
            name: params.name,
            config: {
                maxDelegationDepth: 4,
                maxDelegationWidth: 8,
                maxInterventionDepth: 10,
            },
            createdAt: now,
            updatedAt: now,
        };
        const dir = join(this.rootDir, 'projects', project.id);
        await mkdir(dir, { recursive: true });
        await atomicWriteJson(join(dir, 'project.json'), serializeProject(project));
        this.projectIndex.set(project.id, { projectId: project.id, path: dir });
        return project;
    }
    async getProject(projectId, tenantId) {
        const dir = this.projectDir(projectId);
        const raw = await readJson(join(dir, 'project.json'));
        if (!raw)
            return null;
        this.assertTenant(raw.tenantId, tenantId, `project(${projectId})`);
        return deserializeProject(raw);
    }
    // Session CRUD ------------------------------------------------------------
    async createSession(params, tenantId) {
        const project = await this.getProject(params.projectId, tenantId);
        if (!project) {
            throw new Error(`Project ${params.projectId} not found`);
        }
        const now = new Date();
        const session = {
            id: generateSessionId(),
            threadId: params.threadId,
            projectId: params.projectId,
            tenantId,
            status: 'idle',
            currentActor: params.currentActor,
            previousActors: [],
            workspaceId: null,
            ownerVersion: 0,
            createdAt: now,
            updatedAt: now,
        };
        const dir = join(this.projectDir(params.projectId), 'sessions', session.id);
        await mkdir(dir, { recursive: true });
        await atomicWriteJson(join(dir, 'session.json'), serializeSession(session));
        this.sessionIndex.set(session.id, {
            sessionId: session.id,
            projectId: params.projectId,
            path: dir,
        });
        return session;
    }
    async getSession(sessionId, tenantId) {
        const located = await this.locateSession(sessionId);
        if (!located)
            return null;
        const raw = await readJson(join(located.path, 'session.json'));
        if (!raw)
            return null;
        this.assertTenant(raw.tenantId, tenantId, `session(${sessionId})`);
        return deserializeSession(raw);
    }
    async listSessions(threadId, tenantId) {
        // Walk projects/*/sessions/* and filter on the persisted record. Sessions
        // don't live under a thread-scoped path in the current layout — the
        // denormalized `threadId` on every session.json is the authority. Matches
        // DiskThreadStore.listThreads in scan semantics.
        //
        // Cost: O(all sessions across all projects in the root) per call. The
        // MVP disk store prioritizes simplicity over index freshness, matching
        // `buildLinkageView` / `locateSession` which use the same pattern. A
        // production driver would maintain a threadId → sessionIds secondary
        // index populated on createSession / deleteSession. Acceptable for
        // ThreadManager archive/delete today because those operations are
        // admin-initiated and infrequent.
        const projectsDir = join(this.rootDir, 'projects');
        let projectDirs;
        try {
            projectDirs = await readdir(projectsDir);
        }
        catch (err) {
            const code = err.code;
            if (code === 'ENOENT')
                return [];
            throw err;
        }
        const results = [];
        for (const rawProject of projectDirs) {
            if (!rawProject.startsWith('prj_'))
                continue;
            const sessionsRoot = join(projectsDir, rawProject, 'sessions');
            let sessionDirs;
            try {
                sessionDirs = await readdir(sessionsRoot);
            }
            catch {
                continue;
            }
            for (const rawSessionId of sessionDirs) {
                if (!rawSessionId.startsWith('ses_'))
                    continue;
                const path = join(sessionsRoot, rawSessionId);
                const raw = await readJson(join(path, 'session.json'));
                if (!raw)
                    continue;
                if (raw.tenantId !== tenantId)
                    continue;
                if (raw.threadId !== threadId)
                    continue;
                results.push(deserializeSession(raw));
                this.sessionIndex.set(raw.id, {
                    sessionId: raw.id,
                    projectId: rawProject,
                    path,
                });
            }
        }
        results.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
        return results;
    }
    async updateSession(session, tenantId) {
        const located = await this.locateSession(session.id);
        if (!located) {
            throw new Error(`Session ${session.id} not found`);
        }
        if (session.tenantId !== tenantId) {
            throw new TenantIsolationError({
                requested: tenantId,
                resource: `session(${session.id}) payload`,
            });
        }
        const existing = await readJson(join(located.path, 'session.json'));
        if (existing) {
            this.assertTenant(existing.tenantId, tenantId, `session(${session.id})`);
        }
        const updated = { ...session, updatedAt: new Date() };
        await atomicWriteJson(join(located.path, 'session.json'), serializeSession(updated));
    }
    async deleteSession(sessionId, tenantId) {
        const located = await this.locateSession(sessionId);
        if (!located)
            return; // Idempotent: missing = no-op.
        const existing = await readJson(join(located.path, 'session.json'));
        if (!existing)
            return;
        this.assertTenant(existing.tenantId, tenantId, `session(${sessionId})`);
        // Policy: reject if sub-sessions are attached. Callers must delete
        // children first — Convention #5 deny-by-default; no implicit cascade.
        // We check BOTH directions (this session as parent, or as child) to
        // match the in-memory semantics.
        const subsDir = join(located.path, 'subsessions');
        let subEntries = [];
        try {
            subEntries = await readdir(subsDir);
        }
        catch (err) {
            const code = err.code;
            if (code !== 'ENOENT')
                throw err;
        }
        if (subEntries.some((e) => e.startsWith('sub_'))) {
            throw new Error(`Session ${sessionId} has attached sub-sessions; delete them before deleting the session`);
        }
        // Also scan tree for sub-session records that reference this session as
        // `childSessionId`. We need to walk siblings; acceptable cost for the
        // MVP disk store since the broadcast rollback path always pairs a
        // deleteSubSession + deleteSession call on the child (no orphans at
        // steady state).
        const projectsDir = join(this.rootDir, 'projects');
        let projectDirs;
        try {
            projectDirs = await readdir(projectsDir);
        }
        catch (err) {
            const code = err.code;
            if (code === 'ENOENT')
                projectDirs = [];
            else
                throw err;
        }
        for (const rawProject of projectDirs) {
            if (!rawProject.startsWith('prj_'))
                continue;
            const sessionsRoot = join(projectsDir, rawProject, 'sessions');
            let siblingSessions = [];
            try {
                siblingSessions = await readdir(sessionsRoot);
            }
            catch {
                continue;
            }
            for (const rawSib of siblingSessions) {
                if (!rawSib.startsWith('ses_'))
                    continue;
                const sibSubsDir = join(sessionsRoot, rawSib, 'subsessions');
                let sibSubs = [];
                try {
                    sibSubs = await readdir(sibSubsDir);
                }
                catch {
                    continue;
                }
                for (const rawSub of sibSubs) {
                    if (!rawSub.startsWith('sub_'))
                        continue;
                    const subRaw = await readJson(join(sibSubsDir, rawSub, 'subsession.json'));
                    if (!subRaw)
                        continue;
                    if (subRaw.childSessionId === sessionId || subRaw.parentSessionId === sessionId) {
                        throw new Error(`Session ${sessionId} has attached sub-sessions; delete them before deleting the session`);
                    }
                }
            }
        }
        // Recursive removal — `fs.rm` with `recursive: true` is the atomic
        // primitive for bulk delete. No write-tmp-rename applies here (we're
        // destroying state, not creating it).
        await rm(located.path, { recursive: true, force: true });
        this.sessionIndex.delete(sessionId);
    }
    // SubSession CRUD ---------------------------------------------------------
    async createSubSession(params, tenantId) {
        const parent = await this.getSession(params.parentSessionId, tenantId);
        if (!parent)
            throw new Error(`Parent session ${params.parentSessionId} not found`);
        const child = await this.getSession(params.childSessionId, tenantId);
        if (!child)
            throw new Error(`Child session ${params.childSessionId} not found`);
        const parentLoc = this.sessionIndex.get(params.parentSessionId);
        if (!parentLoc)
            throw new Error(`Parent session ${params.parentSessionId} missing from index`);
        const now = new Date();
        const subSession = {
            id: generateSubSessionId(),
            parentSessionId: params.parentSessionId,
            childSessionId: params.childSessionId,
            kind: params.kind,
            status: 'pending',
            spawnedBy: params.spawnedBy,
            spawnedAt: now,
            failureMode: params.failureMode ?? 'delegate',
            completionMode: params.completionMode ?? 'summary_ref',
            workspaceId: null,
            updatedAt: now,
        };
        const dir = join(parentLoc.path, 'subsessions', subSession.id);
        await mkdir(dir, { recursive: true });
        await atomicWriteJson(join(dir, 'subsession.json'), serializeSubSession(subSession, tenantId));
        this.subSessionIndex.set(subSession.id, {
            subSessionId: subSession.id,
            sessionId: params.parentSessionId,
            projectId: parentLoc.projectId,
            path: dir,
        });
        return subSession;
    }
    async getSubSession(subSessionId, tenantId) {
        const located = await this.locateSubSession(subSessionId);
        if (!located)
            return null;
        const raw = await readJson(join(located.path, 'subsession.json'));
        if (!raw)
            return null;
        this.assertTenant(raw.tenantId, tenantId, `sub-session(${subSessionId})`);
        return deserializeSubSession(raw);
    }
    async updateSubSession(subSession, tenantId) {
        const located = await this.locateSubSession(subSession.id);
        if (!located) {
            throw new Error(`SubSession ${subSession.id} not found`);
        }
        const existing = await readJson(join(located.path, 'subsession.json'));
        if (existing) {
            this.assertTenant(existing.tenantId, tenantId, `sub-session(${subSession.id})`);
        }
        const updated = { ...subSession, updatedAt: new Date() };
        await atomicWriteJson(join(located.path, 'subsession.json'), serializeSubSession(updated, tenantId));
    }
    async deleteSubSession(subSessionId, tenantId) {
        const located = await this.locateSubSession(subSessionId);
        if (!located)
            return; // Idempotent: missing = no-op.
        const existing = await readJson(join(located.path, 'subsession.json'));
        if (!existing) {
            // Record vanished between locate + read — treat as already deleted.
            this.subSessionIndex.delete(subSessionId);
            return;
        }
        this.assertTenant(existing.tenantId, tenantId, `sub-session(${subSessionId})`);
        await rm(located.path, { recursive: true, force: true });
        this.subSessionIndex.delete(subSessionId);
    }
    // Messages ----------------------------------------------------------------
    async appendMessage(sessionId, message, tenantId) {
        const located = await this.locateSession(sessionId);
        if (!located)
            throw new Error(`Session ${sessionId} not found`);
        const session = await readJson(join(located.path, 'session.json'));
        if (!session)
            throw new Error(`Session ${sessionId} not found on disk`);
        this.assertTenant(session.tenantId, tenantId, `session(${sessionId})`);
        const id = generateMessageId();
        const entry = {
            id,
            sessionId,
            tenantId,
            message,
            at: new Date().toISOString(),
        };
        await appendFile(join(located.path, 'messages.jsonl'), `${JSON.stringify(entry)}\n`, 'utf-8');
        return id;
    }
    async loadMessages(sessionId, tenantId) {
        const rows = await this.loadSessionMessages(sessionId, tenantId);
        return rows.map((r) => r.message);
    }
    async loadSessionMessages(sessionId, tenantId) {
        const located = await this.locateSession(sessionId);
        if (!located)
            return [];
        const session = await readJson(join(located.path, 'session.json'));
        if (!session)
            return [];
        this.assertTenant(session.tenantId, tenantId, `session(${sessionId})`);
        const path = join(located.path, 'messages.jsonl');
        let raw;
        try {
            raw = await readFile(path, 'utf-8');
        }
        catch (err) {
            const code = err.code;
            if (code === 'ENOENT')
                return [];
            throw err;
        }
        const lines = raw.split('\n').filter((l) => l.length > 0);
        return lines.map((line) => {
            const persisted = JSON.parse(line);
            return {
                id: persisted.id,
                sessionId: persisted.sessionId,
                tenantId: persisted.tenantId,
                message: persisted.message,
                at: new Date(persisted.at),
            };
        });
    }
    // Linkage -----------------------------------------------------------------
    async getChildren(sessionId, tenantId) {
        const session = await this.getSession(sessionId, tenantId);
        if (!session)
            return [];
        const view = await this.buildLinkageView(tenantId);
        return orderChildren(getChildren(view, sessionId));
    }
    async getAncestry(sessionId, tenantId) {
        const session = await this.getSession(sessionId, tenantId);
        if (!session)
            return [];
        const view = await this.buildLinkageView(tenantId);
        return getAncestry(view, sessionId);
    }
    async drill(sessionId, tenantId) {
        const session = await this.getSession(sessionId, tenantId);
        if (!session)
            return null;
        const view = await this.buildLinkageView(tenantId);
        return {
            session,
            children: orderChildren(getChildren(view, sessionId)),
            ancestry: getAncestry(view, sessionId),
        };
    }
    // Summary (§4.7 / §8.1) ---------------------------------------------------
    /**
     * 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}.
     */
    async recordSummary(summary, tenantId) {
        if (summary.tenantId !== tenantId) {
            throw new TenantIsolationError({
                requested: tenantId,
                resource: `summary(${summary.id}) payload`,
            });
        }
        const located = await this.locateSession(summary.sessionRef);
        if (!located) {
            throw new Error(`Session ${summary.sessionRef} not found`);
        }
        const sessionRaw = await readJson(join(located.path, 'session.json'));
        if (!sessionRaw) {
            throw new Error(`Session ${summary.sessionRef} not found on disk`);
        }
        this.assertTenant(sessionRaw.tenantId, tenantId, `session(${summary.sessionRef})`);
        const summaryPath = join(located.path, 'summary.json');
        const existingRaw = await readJson(summaryPath);
        if (existingRaw) {
            this.assertTenant(existingRaw.tenantId, tenantId, `summary(${existingRaw.id})`);
            if (existingRaw.id !== summary.id) {
                throw new SessionAlreadySummarizedError({
                    sessionId: summary.sessionRef,
                    existingSummaryId: existingRaw.id,
                });
            }
            // Same summary id — recovery replay. No duplicate write; fall through
            // to the status flip so crash-between-writes is recovered.
        }
        else {
            // Step 1: persist summary.
            await atomicWriteJson(summaryPath, serializeSummary(summary));
        }
        // Step 2: flip session status atomically if still non-terminal.
        if (SUMMARY_TERMINAL_FLIP_STATUSES.has(sessionRaw.status)) {
            const flipped = {
                ...sessionRaw,
                status: 'idle',
                updatedAt: new Date().toISOString(),
            };
            await atomicWriteJson(join(located.path, 'session.json'), flipped);
        }
    }
    async getSummary(sessionId, tenantId) {
        const located = await this.locateSession(sessionId);
        if (!located)
            return null;
        const raw = await readJson(join(located.path, 'summary.json'));
        if (!raw)
            return null;
        this.assertTenant(raw.tenantId, tenantId, `summary(${raw.id})`);
        return deserializeSummary(raw);
    }
    // Helpers -----------------------------------------------------------------
    assertTenant(actual, requested, resource) {
        if (actual !== requested) {
            throw new TenantIsolationError({ requested, resource });
        }
    }
    projectDir(projectId) {
        const cached = this.projectIndex.get(projectId);
        if (cached)
            return cached.path;
        const path = join(this.rootDir, 'projects', projectId);
        this.projectIndex.set(projectId, { projectId, path });
        return path;
    }
    async locateSession(sessionId) {
        const cached = this.sessionIndex.get(sessionId);
        if (cached)
            return cached;
        const projectsDir = join(this.rootDir, 'projects');
        let projectDirs;
        try {
            projectDirs = await readdir(projectsDir);
        }
        catch (err) {
            const code = err.code;
            if (code === 'ENOENT')
                return null;
            throw err;
        }
        for (const rawId of projectDirs) {
            if (!rawId.startsWith('prj_'))
                continue;
            const projectId = rawId;
            const sessionsRoot = join(projectsDir, projectId, 'sessions');
            let sessionDirs;
            try {
                sessionDirs = await readdir(sessionsRoot);
            }
            catch {
                continue;
            }
            for (const rawSessionId of sessionDirs) {
                if (!rawSessionId.startsWith('ses_'))
                    continue;
                if (rawSessionId === sessionId) {
                    const entry = {
                        sessionId,
                        projectId,
                        path: join(sessionsRoot, rawSessionId),
                    };
                    this.sessionIndex.set(sessionId, entry);
                    return entry;
                }
            }
        }
        return null;
    }
    async locateSubSession(subSessionId) {
        const cached = this.subSessionIndex.get(subSessionId);
        if (cached)
            return cached;
        const projectsDir = join(this.rootDir, 'projects');
        let projectDirs;
        try {
            projectDirs = await readdir(projectsDir);
        }
        catch (err) {
            const code = err.code;
            if (code === 'ENOENT')
                return null;
            throw err;
        }
        for (const rawProject of projectDirs) {
            if (!rawProject.startsWith('prj_'))
                continue;
            const projectId = rawProject;
            const sessionsRoot = join(projectsDir, projectId, 'sessions');
            let sessionDirs;
            try {
                sessionDirs = await readdir(sessionsRoot);
            }
            catch {
                continue;
            }
            for (const rawSession of sessionDirs) {
                if (!rawSession.startsWith('ses_'))
                    continue;
                const sessionId = rawSession;
                const subsDir = join(sessionsRoot, sessionId, 'subsessions');
                let subDirs;
                try {
                    subDirs = await readdir(subsDir);
                }
                catch {
                    continue;
                }
                for (const rawSub of subDirs) {
                    if (rawSub === subSessionId) {
                        const entry = {
                            subSessionId,
                            sessionId,
                            projectId,
                            path: join(subsDir, rawSub),
                        };
                        this.subSessionIndex.set(subSessionId, entry);
                        return entry;
                    }
                }
            }
        }
        return null;
    }
    async buildLinkageView(tenantId) {
        // Walk the full projects → sessions → subsessions tree once per call.
        // Acceptable for an MVP disk store; a production impl would cache.
        const allSubs = [];
        const projectsDir = join(this.rootDir, 'projects');
        let projectDirs;
        try {
            projectDirs = await readdir(projectsDir);
        }
        catch (err) {
            const code = err.code;
            if (code === 'ENOENT')
                return emptyLinkageView();
            throw err;
        }
        for (const rawProject of projectDirs) {
            if (!rawProject.startsWith('prj_'))
                continue;
            const sessionsRoot = join(projectsDir, rawProject, 'sessions');
            let sessionDirs;
            try {
                sessionDirs = await readdir(sessionsRoot);
            }
            catch {
                continue;
            }
            for (const rawSession of sessionDirs) {
                if (!rawSession.startsWith('ses_'))
                    continue;
                const subsRoot = join(sessionsRoot, rawSession, 'subsessions');
                let subDirs;
                try {
                    subDirs = await readdir(subsRoot);
                }
                catch {
                    continue;
                }
                for (const rawSub of subDirs) {
                    const raw = await readJson(join(subsRoot, rawSub, 'subsession.json'));
                    if (!raw)
                        continue;
                    if (raw.tenantId !== tenantId)
                        continue;
                    allSubs.push(deserializeSubSession(raw));
                }
            }
        }
        return {
            findChildSubSessions: (parentSessionId) => allSubs.filter((s) => s.parentSessionId === parentSessionId),
            findParentSubSession: (childSessionId) => allSubs.find((s) => s.childSessionId === childSessionId) ?? null,
        };
    }
}
function emptyLinkageView() {
    return {
        findChildSubSessions: () => [],
        findParentSubSession: () => null,
    };
}
// Serialization helpers -----------------------------------------------------
function serializeProject(p) {
    return {
        id: p.id,
        tenantId: p.tenantId,
        name: p.name,
        config: p.config,
        createdAt: p.createdAt.toISOString(),
        updatedAt: p.updatedAt.toISOString(),
    };
}
function deserializeProject(p) {
    return {
        id: p.id,
        tenantId: p.tenantId,
        name: p.name,
        config: p.config,
        createdAt: new Date(p.createdAt),
        updatedAt: new Date(p.updatedAt),
    };
}
function serializeSession(s) {
    return {
        id: s.id,
        threadId: s.threadId,
        projectId: s.projectId,
        tenantId: s.tenantId,
        status: s.status,
        currentActor: s.currentActor,
        previousActors: s.previousActors,
        workspaceId: s.workspaceId,
        ownerVersion: s.ownerVersion,
        createdAt: s.createdAt.toISOString(),
        updatedAt: s.updatedAt.toISOString(),
    };
}
function deserializeSession(s) {
    return {
        id: s.id,
        threadId: s.threadId,
        projectId: s.projectId,
        tenantId: s.tenantId,
        status: s.status,
        currentActor: s.currentActor,
        previousActors: s.previousActors,
        workspaceId: s.workspaceId,
        ownerVersion: s.ownerVersion,
        createdAt: new Date(s.createdAt),
        updatedAt: new Date(s.updatedAt),
    };
}
function serializeSubSession(s, tenantId) {
    return {
        id: s.id,
        parentSessionId: s.parentSessionId,
        childSessionId: s.childSessionId,
        tenantId,
        kind: s.kind,
        status: s.status,
        spawnedBy: s.spawnedBy,
        spawnedAt: s.spawnedAt.toISOString(),
        failureMode: s.failureMode,
        completionMode: s.completionMode,
        workspaceId: s.workspaceId,
        ...(s.broadcastGroupId !== undefined && { broadcastGroupId: s.broadcastGroupId }),
        ...(s.summaryRef !== undefined && { summaryRef: s.summaryRef }),
        ...(s.archiveRef !== undefined && { archiveRef: s.archiveRef }),
        ...(s.archivedAt !== undefined && { archivedAt: s.archivedAt.toISOString() }),
        updatedAt: s.updatedAt.toISOString(),
    };
}
function deserializeSubSession(s) {
    return {
        id: s.id,
        parentSessionId: s.parentSessionId,
        childSessionId: s.childSessionId,
        kind: s.kind,
        status: s.status,
        spawnedBy: s.spawnedBy,
        spawnedAt: new Date(s.spawnedAt),
        failureMode: s.failureMode,
        completionMode: s.completionMode,
        workspaceId: s.workspaceId,
        ...(s.broadcastGroupId !== undefined && { broadcastGroupId: s.broadcastGroupId }),
        ...(s.summaryRef !== undefined && { summaryRef: s.summaryRef }),
        ...(s.archiveRef !== undefined && { archiveRef: s.archiveRef }),
        ...(s.archivedAt !== undefined && { archivedAt: new Date(s.archivedAt) }),
        updatedAt: new Date(s.updatedAt),
    };
}
function serializeSummary(s) {
    return {
        id: s.id,
        sessionRef: s.sessionRef,
        tenantId: s.tenantId,
        outcome: s.outcome,
        deliverables: s.deliverables,
        agentSummary: s.agentSummary,
        keyDecisions: s.keyDecisions.map((k) => ({
            at: k.at.toISOString(),
            summary: k.summary,
        })),
        at: s.at.toISOString(),
        materializedBy: 'kernel',
    };
}
function deserializeSummary(s) {
    const decisions = s.keyDecisions.map((k) => ({
        at: new Date(k.at),
        summary: k.summary,
    }));
    return {
        id: s.id,
        sessionRef: s.sessionRef,
        tenantId: s.tenantId,
        outcome: s.outcome,
        deliverables: s.deliverables,
        agentSummary: s.agentSummary,
        keyDecisions: decisions,
        at: new Date(s.at),
        materializedBy: 'kernel',
    };
}
// FS helpers -----------------------------------------------------------------
async function readJson(path) {
    try {
        const raw = await readFile(path, 'utf-8');
        return JSON.parse(raw);
    }
    catch (err) {
        const code = err.code;
        if (code === 'ENOENT')
            return null;
        throw err;
    }
}
async function atomicWriteJson(filePath, value) {
    const tempPath = `${filePath}.tmp`;
    try {
        await writeFile(tempPath, JSON.stringify(value, null, 2), 'utf-8');
        await rename(tempPath, filePath);
    }
    catch (err) {
        await unlink(tempPath).catch(() => undefined);
        throw err;
    }
}
//# sourceMappingURL=disk.js.map