import type { SecafsRollbackState } from "./rollback-types.js";
* Persistence codec for the plugin's own session-entry state.
*
* Upstream `SessionEntry` does NOT declare the plugin's custom fields. The
* sanctioned place for plugin-owned session state is
* `entry.pluginExtensions[<pluginId>]` (see upstream `config/sessions/types.ts`).
* So on disk we keep these fields under `pluginExtensions["secafs-chat"]`, and
* in memory we surface them at top level for convenient access (the rest of the
* plugin reads `entry.kind` / `entry.mountState` / `entry.secafsRollback`).
*
* - `hoistSecafsExt` (load): copy the namespaced fields up to top level. Falls
* back to any legacy top-level values so stores written before this migration
* keep working (and get rewritten into the namespace on the next save).
* - `foldSecafsExt` (save): move the fields from top level INTO the namespace
* and strip the top-level copies, keeping the on-disk entry within the
* upstream-typed shape.
*/
export const SECAFS_PLUGIN_ID = "secafs-chat";
export interface SecafsExt {
kind?: "secafs";
mountState?: "mounted" | "unmounted" | "stuck";
secafsRollback?: SecafsRollbackState;
alias?: string;
}
const EXT_KEYS = ["kind", "mountState", "secafsRollback", "alias"] as const;
type AnyEntry = Record<string, unknown> & {
pluginExtensions?: Record<string, Record<string, unknown>>;
};
function readNamespace(entry: AnyEntry): SecafsExt | undefined {
const ns = entry.pluginExtensions?.[SECAFS_PLUGIN_ID];
return ns ? (ns as SecafsExt) : undefined;
}
export function hoistSecafsExt<T>(entry: T): T {
const e = entry as unknown as AnyEntry | null;
if (!e || typeof e !== "object") return entry;
const ns = readNamespace(e);
if (!ns) return entry;
const next: AnyEntry = { ...e };
for (const k of EXT_KEYS) {
if (ns[k] !== undefined) next[k] = ns[k];
}
return next as unknown as T;
}
export function foldSecafsExt<T>(entry: T): T {
const e = entry as unknown as AnyEntry | null;
if (!e || typeof e !== "object") return entry;
const topLevel: SecafsExt = {};
let hasTopLevel = false;
for (const k of EXT_KEYS) {
if (e[k] !== undefined) {
(topLevel as Record<string, unknown>)[k] = e[k];
hasTopLevel = true;
}
}
const existing = readNamespace(e);
if (!hasTopLevel && !existing) return entry;
const merged: SecafsExt = { ...(existing ?? {}), ...topLevel };
const next: AnyEntry = { ...e };
for (const k of EXT_KEYS) delete next[k];
next.pluginExtensions = {
...(e.pluginExtensions ?? {}),
[SECAFS_PLUGIN_ID]: merged as Record<string, unknown>,
};
return next as unknown as T;
}
export function hoistSecafsStore<S extends Record<string, unknown>>(store: S): S {
const out: Record<string, unknown> = {};
for (const [k, v] of Object.entries(store)) out[k] = hoistSecafsExt(v);
return out as S;
}
export function foldSecafsStore<S extends Record<string, unknown>>(store: S): S {
const out: Record<string, unknown> = {};
for (const [k, v] of Object.entries(store)) out[k] = foldSecafsExt(v);
return out as S;
}