288 lines · 9.0 KB · TypeScript
RawDownload
import {  chmodSync,  existsSync,  mkdirSync,  readFileSync,  renameSync,  writeFileSync,} from "node:fs";import path from "node:path";import lockfile from "proper-lockfile";import { resolveDataDir } from "./data-dir";import { isRecord } from "../../../shared/agent/guards"; const SESSION_METADATA_FILENAME = "agent-session-metadata.json";const LOCK_STALE_MS = 10_000;const LOCK_RETRY_MS = 25;const LOCK_ATTEMPTS = 80; export type SessionArchiveState = {  archived: boolean;  archivedAt: string | null;}; type StoredSessionMetadata = {  archived?: boolean;  archivedAt?: string | null;  updatedAt?: string;  cwd?: string;  title?: string | null;  projectId?: string;  projectName?: string;  sessionUpdatedAt?: string;  parentSessionId?: string;  subagentName?: string;}; type SessionMetadataStore = {  version: 1;  sessions: Record<string, StoredSessionMetadata>;}; export type ArchivedSessionMetadata = SessionArchiveState & {  id: string;  updatedAt: string | null;  cwd: string | null;  title: string | null;  projectId: string | null;  projectName: string | null;  sessionUpdatedAt: string | null;}; type SessionArchiveMetadataInput = {  cwd?: string | null;  title?: string | null;  projectId?: string | null;  projectName?: string | null;  sessionUpdatedAt?: string | null;}; function defaultStore(): SessionMetadataStore {  return { version: 1, sessions: {} };} function storePath(): string {  return path.join(resolveDataDir(), SESSION_METADATA_FILENAME);} function normalizeStore(value: unknown): SessionMetadataStore {  if (!isRecord(value) || !isRecord(value.sessions)) return defaultStore();  const sessions: Record<string, StoredSessionMetadata> = {};  for (const [id, metadata] of Object.entries(value.sessions)) {    if (!id.trim() || !isRecord(metadata)) continue;    sessions[id] = {      archived: metadata.archived === true,      archivedAt: typeof metadata.archivedAt === "string" ? metadata.archivedAt : null,      updatedAt: typeof metadata.updatedAt === "string" ? metadata.updatedAt : undefined,      cwd: typeof metadata.cwd === "string" ? metadata.cwd : undefined,      title: typeof metadata.title === "string" ? metadata.title : null,      projectId: typeof metadata.projectId === "string" ? metadata.projectId : undefined,      projectName: typeof metadata.projectName === "string" ? metadata.projectName : undefined,      sessionUpdatedAt:        typeof metadata.sessionUpdatedAt === "string" ? metadata.sessionUpdatedAt : undefined,      parentSessionId:        typeof metadata.parentSessionId === "string" ? metadata.parentSessionId : undefined,      subagentName: typeof metadata.subagentName === "string" ? metadata.subagentName : undefined,    };  }  return { version: 1, sessions };} function backupUnreadableStore(filepath: string): void {  if (!existsSync(filepath)) return;  const backupPath = `${filepath}.corrupt-${Date.now()}.bak`;  try {    renameSync(filepath, backupPath);    console.warn(`[agent-session-metadata] Moved unreadable metadata store to ${backupPath}`);  } catch (error) {    console.warn("[agent-session-metadata] Failed to preserve unreadable metadata store", error);  }} function readStore(): SessionMetadataStore {  const filepath = storePath();  try {    if (!existsSync(filepath)) return defaultStore();    return normalizeStore(JSON.parse(readFileSync(filepath, "utf-8")) as unknown);  } catch (error) {    backupUnreadableStore(filepath);    console.warn("[agent-session-metadata] Failed to read metadata store", error);    return defaultStore();  }} function writeStore(store: SessionMetadataStore): void {  const filepath = storePath();  mkdirSync(path.dirname(filepath), { recursive: true });  const tempPath = `${filepath}.${process.pid}.tmp`;  writeFileSync(tempPath, `${JSON.stringify(store, null, 2)}\n`, "utf-8");  try {    chmodSync(tempPath, 0o600);  } catch {}  renameSync(tempPath, filepath);} async function withStoreLock<T>(callback: () => T): Promise<T> {  const filepath = storePath();  mkdirSync(path.dirname(filepath), { recursive: true });  const release = await lockfile.lock(filepath, {    realpath: false,    stale: LOCK_STALE_MS,    retries: {      retries: LOCK_ATTEMPTS - 1,      factor: 1,      minTimeout: LOCK_RETRY_MS,      maxTimeout: LOCK_RETRY_MS,      randomize: false,    },  });  try {    return callback();  } finally {    await release();  }} function cleanOptionalString(value: string | null | undefined): string | undefined {  const trimmed = value?.trim();  return trimmed || undefined;} function applyMetadataInput(  current: StoredSessionMetadata,  metadata?: SessionArchiveMetadataInput,): StoredSessionMetadata {  if (!metadata) return current;  const next = { ...current };  const cwd = cleanOptionalString(metadata.cwd);  const title = cleanOptionalString(metadata.title);  const projectId = cleanOptionalString(metadata.projectId);  const projectName = cleanOptionalString(metadata.projectName);  const sessionUpdatedAt = cleanOptionalString(metadata.sessionUpdatedAt);  if (cwd) next.cwd = cwd;  if (title) next.title = title;  if (projectId) next.projectId = projectId;  if (projectName) next.projectName = projectName;  if (sessionUpdatedAt) next.sessionUpdatedAt = sessionUpdatedAt;  return next;} export type SessionSubagentLink = {  parentSessionId: string;  subagentName: string | null;}; export type SessionListMetadata = SessionArchiveState & {  parentSessionId: string | null;  subagentName: string | null;}; export function readSessionListMetadata(): (sessionId: string) => SessionListMetadata {  const sessions = readStore().sessions;  return (sessionId) => {    const metadata = sessions[sessionId];    return {      archived: metadata?.archived === true,      archivedAt: metadata?.archived === true ? (metadata.archivedAt ?? null) : null,      parentSessionId: metadata?.parentSessionId ?? null,      subagentName: metadata?.subagentName ?? null,    };  };} export function sessionSubagentLink(sessionId: string): SessionSubagentLink | null {  const metadata = readStore().sessions[sessionId];  if (!metadata?.parentSessionId) return null;  return {    parentSessionId: metadata.parentSessionId,    subagentName: metadata.subagentName ?? null,  };} export async function setSubagentLink(  childSessionId: string,  parentSessionId: string,  subagentName: string | null,): Promise<void> {  const childId = childSessionId.trim();  const parentId = parentSessionId.trim();  if (!childId || !parentId || childId === parentId) return;  await withStoreLock(() => {    const store = readStore();    const current = store.sessions[childId] ?? {};    store.sessions[childId] = {      ...current,      parentSessionId: parentId,      ...(subagentName?.trim() ? { subagentName: subagentName.trim() } : {}),      updatedAt: new Date().toISOString(),    };    writeStore(store);  });} export function listArchivedSessionMetadata(): ArchivedSessionMetadata[] {  return Object.entries(readStore().sessions)    .filter(([, metadata]) => metadata.archived === true)    .map(([id, metadata]) => ({      id,      archived: true,      archivedAt: metadata.archivedAt ?? null,      updatedAt: metadata.updatedAt ?? null,      cwd: metadata.cwd ?? null,      title: metadata.title ?? null,      projectId: metadata.projectId ?? null,      projectName: metadata.projectName ?? null,      sessionUpdatedAt: metadata.sessionUpdatedAt ?? null,    }))    .sort((a, b) => {      const aTime = Date.parse(a.archivedAt ?? a.updatedAt ?? a.sessionUpdatedAt ?? "");      const bTime = Date.parse(b.archivedAt ?? b.updatedAt ?? b.sessionUpdatedAt ?? "");      return (Number.isFinite(bTime) ? bTime : 0) - (Number.isFinite(aTime) ? aTime : 0);    });} export async function setSessionArchived(  sessionId: string,  archived: boolean,  now = new Date(),  metadata?: SessionArchiveMetadataInput,): Promise<SessionArchiveState> {  const id = sessionId.trim();  if (!id) return { archived: false, archivedAt: null };  return withStoreLock(() => {    const store = readStore();    const current = store.sessions[id] ?? {};    const archivedAt = archived ? (current.archivedAt ?? now.toISOString()) : null;    if (archived) {      store.sessions[id] = applyMetadataInput(        {          ...current,          archived: true,          archivedAt,          updatedAt: now.toISOString(),        },        metadata,      );    } else if (current.parentSessionId) {      store.sessions[id] = {        ...current,        archived: false,        archivedAt: null,        updatedAt: now.toISOString(),      };    } else {      delete store.sessions[id];    }    for (const [childId, child] of Object.entries(store.sessions)) {      if (child.parentSessionId !== id || childId === id) continue;      store.sessions[childId] = {        ...child,        archived,        archivedAt: archived ? (child.archivedAt ?? now.toISOString()) : null,        updatedAt: now.toISOString(),      };    }    writeStore(store);    return { archived, archivedAt };  });}

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t