diff --git a/packages/agent/src/harness/session/jsonl-repo.ts b/packages/agent/src/harness/session/jsonl-repo.ts index 684b04c3b..85eb4fb9f 100644 --- a/packages/agent/src/harness/session/jsonl-repo.ts +++ b/packages/agent/src/harness/session/jsonl-repo.ts @@ -3,23 +3,16 @@ import type { JsonlSessionCreateOptions, JsonlSessionListOptions, JsonlSessionMetadata, - JsonlSessionRepoApi, - Session, + JsonlSessionStoreApi, SessionSearch, - SessionSearchOptions, + SessionStorage, } from "../types.ts"; import { SessionError, toError } from "../types.ts"; import { JsonlSessionStorage, loadJsonlSessionMetadata } from "./jsonl-storage.ts"; -import { - createSessionId, - createTimestamp, - getEntriesToFork, - getFileSystemResultOrThrow, - toRepoSession, -} from "./repo-utils.ts"; +import { createSessionId, createTimestamp, getEntriesToFork, getFileSystemResultOrThrow } from "./repo-utils.ts"; import { ScanningSessionSearch } from "./search-backend.ts"; -type JsonlSessionRepoFileSystem = Pick< +type JsonlSessionStoreFileSystem = Pick< FileSystem, | "cwd" | "absolutePath" @@ -38,25 +31,19 @@ function encodeCwd(cwd: string): string { return `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`; } -export class JsonlSessionRepo implements JsonlSessionRepoApi { - private readonly fs: JsonlSessionRepoFileSystem; +export class JsonlSessionStore implements JsonlSessionStoreApi { + private readonly fs: JsonlSessionStoreFileSystem; private readonly sessionsRootInput: string; - private readonly sessionSearch: SessionSearch; + readonly defaultSearch: SessionSearch; private sessionsRoot: string | undefined; - constructor(options: { - fs: JsonlSessionRepoFileSystem; - sessionsRoot: string; - search?: SessionSearch; - }) { + constructor(options: { fs: JsonlSessionStoreFileSystem; sessionsRoot: string }) { this.fs = options.fs; this.sessionsRootInput = options.sessionsRoot; - this.sessionSearch = - options.search ?? - new ScanningSessionSearch({ - open: (metadata) => this.open(metadata), - list: () => this.list(), - }); + this.defaultSearch = new ScanningSessionSearch({ + open: (metadata) => this.open(metadata), + list: () => this.list(), + }); } private async getSessionsRoot(): Promise { @@ -86,7 +73,7 @@ export class JsonlSessionRepo implements JsonlSessionRepoApi { ); } - async create(options: JsonlSessionCreateOptions): Promise> { + async create(options: JsonlSessionCreateOptions): Promise> { const id = options.id ?? createSessionId(); const createdAt = createTimestamp(); const sessionDir = await this.getSessionDir(options.cwd); @@ -95,23 +82,21 @@ export class JsonlSessionRepo implements JsonlSessionRepoApi { `Failed to create session directory ${sessionDir}`, ); const filePath = await this.createSessionFilePath(options.cwd, id, createdAt); - const storage = await JsonlSessionStorage.create(this.fs, filePath, { + return await JsonlSessionStorage.create(this.fs, filePath, { cwd: options.cwd, sessionId: id, parentSessionPath: options.parentSessionPath, metadata: options.metadata, }); - return await toRepoSession(storage, this.sessionSearch); } - async open(metadata: JsonlSessionMetadata): Promise> { + async open(metadata: JsonlSessionMetadata): Promise> { if ( !getFileSystemResultOrThrow(await this.fs.exists(metadata.path), `Failed to check session ${metadata.path}`) ) { throw new SessionError("not_found", `Session not found: ${metadata.path}`); } - const storage = await JsonlSessionStorage.open(this.fs, metadata.path); - return await toRepoSession(storage, this.sessionSearch); + return await JsonlSessionStorage.open(this.fs, metadata.path); } async list(options: JsonlSessionListOptions = {}): Promise { @@ -138,12 +123,7 @@ export class JsonlSessionRepo implements JsonlSessionRepoApi { return sessions; } - async search(options: SessionSearchOptions) { - return await this.sessionSearch.search(options); - } - async delete(metadata: JsonlSessionMetadata): Promise { - await this.sessionSearch.index?.removeSession(metadata); getFileSystemResultOrThrow( await this.fs.remove(metadata.path, { force: true }), `Failed to delete session ${metadata.path}`, @@ -153,9 +133,9 @@ export class JsonlSessionRepo implements JsonlSessionRepoApi { async fork( sourceMetadata: JsonlSessionMetadata, options: JsonlSessionCreateOptions & { entryId?: string; position?: "before" | "at"; id?: string }, - ): Promise> { + ): Promise> { const source = await this.open(sourceMetadata); - const forkedEntries = await getEntriesToFork(source.getStorage(), options); + const forkedEntries = await getEntriesToFork(source, options); const id = options.id ?? createSessionId(); const createdAt = createTimestamp(); const sessionDir = await this.getSessionDir(options.cwd); @@ -176,8 +156,7 @@ export class JsonlSessionRepo implements JsonlSessionRepoApi { for (const entry of forkedEntries) { await storage.appendEntry(entry); } - await this.sessionSearch.index?.replaceSession(await storage.getMetadata(), forkedEntries); - return await toRepoSession(storage, this.sessionSearch); + return storage; } private async listSessionDirs(): Promise { diff --git a/packages/agent/src/harness/session/memory-repo.ts b/packages/agent/src/harness/session/memory-repo.ts index 6695c4990..042875d06 100644 --- a/packages/agent/src/harness/session/memory-repo.ts +++ b/packages/agent/src/harness/session/memory-repo.ts @@ -1,72 +1,61 @@ import { - type Session, SessionError, type SessionMetadata, - type SessionRepo, type SessionSearch, - type SessionSearchOptions, + type SessionStorage, + type SessionStore, } from "../types.ts"; import { InMemorySessionStorage } from "./memory-storage.ts"; -import { createSessionId, createTimestamp, getEntriesToFork, toRepoSession } from "./repo-utils.ts"; +import { createSessionId, createTimestamp, getEntriesToFork } from "./repo-utils.ts"; import { ScanningSessionSearch } from "./search-backend.ts"; -export class InMemorySessionRepo implements SessionRepo { - private sessions = new Map>(); - private readonly sessionSearch: SessionSearch; +export class MemorySessionStore implements SessionStore { + private sessions = new Map>(); + readonly defaultSearch: SessionSearch; - constructor(options: { search?: SessionSearch } = {}) { - this.sessionSearch = - options.search ?? - new ScanningSessionSearch({ - open: (metadata) => this.open(metadata), - list: () => this.list(), - }); + constructor() { + this.defaultSearch = new ScanningSessionSearch({ + open: (metadata) => this.open(metadata), + list: () => this.list(), + }); } - async create(options: { id?: string } = {}): Promise> { + async create(options: { id?: string } = {}): Promise> { const metadata: SessionMetadata = { id: options.id ?? createSessionId(), createdAt: createTimestamp(), }; const storage = new InMemorySessionStorage({ metadata }); - const session = await toRepoSession(storage, this.sessionSearch); - this.sessions.set(metadata.id, session); - return session; + this.sessions.set(metadata.id, storage); + return storage; } - async open(metadata: SessionMetadata): Promise> { - const session = this.sessions.get(metadata.id); - if (!session) throw new SessionError("not_found", `Session not found: ${metadata.id}`); - return session; + async open(metadata: SessionMetadata): Promise> { + const storage = this.sessions.get(metadata.id); + if (!storage) throw new SessionError("not_found", `Session not found: ${metadata.id}`); + return storage; } async list(): Promise { - return Promise.all([...this.sessions.values()].map((session) => session.getMetadata())); - } - - async search(options: SessionSearchOptions) { - return await this.sessionSearch.search(options); + return Promise.all([...this.sessions.values()].map((storage) => storage.getMetadata())); } async delete(metadata: SessionMetadata): Promise { - await this.sessionSearch.index?.removeSession(metadata); this.sessions.delete(metadata.id); } async fork( sourceMetadata: SessionMetadata, options: { entryId?: string; position?: "before" | "at"; id?: string }, - ): Promise> { + ): Promise> { const source = await this.open(sourceMetadata); - const forkedEntries = await getEntriesToFork(source.getStorage(), options); + const forkedEntries = await getEntriesToFork(source, options); const metadata: SessionMetadata = { id: options.id ?? createSessionId(), createdAt: createTimestamp(), }; const storage = new InMemorySessionStorage({ metadata, entries: forkedEntries }); - await this.sessionSearch.index?.replaceSession(metadata, forkedEntries); - const session = await toRepoSession(storage, this.sessionSearch); - this.sessions.set(metadata.id, session); - return session; + this.sessions.set(metadata.id, storage); + return storage; } } diff --git a/packages/agent/src/harness/session/repo-utils.ts b/packages/agent/src/harness/session/repo-utils.ts index b8ebec8d2..b2b17085d 100644 --- a/packages/agent/src/harness/session/repo-utils.ts +++ b/packages/agent/src/harness/session/repo-utils.ts @@ -2,12 +2,15 @@ import { uuidv7 } from "@earendil-works/pi-ai"; import { type FileError, type Result, + type SessionCreateOptions, SessionError, + type SessionForkOptions, type SessionMetadata, type SessionSearch, type SessionSearchHit, type SessionSearchIndex, type SessionStorage, + type SessionStore, type SessionTreeEntry, type SessionWriter, } from "../types.ts"; @@ -61,6 +64,70 @@ export async function toRepoSession( return toSession(storage, new SessionRepoWriter(storage, metadata, search.index)); } +type BoundSessionStore< + TMetadata extends SessionMetadata, + TCreateOptions extends SessionCreateOptions, + TListOptions, +> = Omit, "defaultSearch" | "create" | "open" | "fork"> & { + create(options: TCreateOptions): Promise>; + open(metadata: TMetadata): Promise>; + fork(source: TMetadata, options: SessionForkOptions & TCreateOptions): Promise>; +}; + +function bindSessionStore( + storage: SessionStore, + search: SessionSearch | null, +): BoundSessionStore { + const wrap = async (sessionStorage: SessionStorage): Promise> => + search ? await toRepoSession(sessionStorage, search) : toSession(sessionStorage); + return new Proxy(storage, { + get(target, property) { + if (property === "create") { + return async (options: TCreateOptions) => await wrap(await target.create(options)); + } + if (property === "open") { + return async (metadata: TMetadata) => await wrap(await target.open(metadata)); + } + if (property === "fork") { + return async (source: TMetadata, options: SessionForkOptions & TCreateOptions) => { + const sessionStorage = await target.fork(source, options); + await search?.index?.replaceSession( + await sessionStorage.getMetadata(), + await sessionStorage.getEntries(), + ); + return await wrap(sessionStorage); + }; + } + if (property === "delete") { + return async (metadata: TMetadata) => { + await search?.index?.removeSession(metadata); + await target.delete(metadata); + }; + } + const value = Reflect.get(target, property, target) as unknown; + return typeof value === "function" ? value.bind(target) : value; + }, + }) as unknown as BoundSessionStore; +} + +/** Composes canonical storage and optional search as independently accessible capabilities. */ +export class SessionRepo< + TMetadata extends SessionMetadata = SessionMetadata, + TCreateOptions extends SessionCreateOptions = SessionCreateOptions, + TListOptions = void, +> { + readonly storage: BoundSessionStore; + readonly search: SessionSearch | null; + + constructor(options: { + storage: SessionStore; + search?: SessionSearch | null; + }) { + this.search = options.search === undefined ? (options.storage.defaultSearch ?? null) : options.search; + this.storage = bindSessionStore(options.storage, this.search); + } +} + export function findSessionEntryMatches( metadata: TMetadata, entries: SessionTreeEntry[], diff --git a/packages/agent/src/harness/session/search-backend.ts b/packages/agent/src/harness/session/search-backend.ts index 9c226965b..7288fe7df 100644 --- a/packages/agent/src/harness/session/search-backend.ts +++ b/packages/agent/src/harness/session/search-backend.ts @@ -1,17 +1,17 @@ import type { - Session, SessionMetadata, SessionSearch, SessionSearchHit, SessionSearchIndex, SessionSearchOptions, SessionSearchRecord, + SessionStorage, SessionTreeEntry, } from "../types.ts"; import { findSessionEntryMatches } from "./repo-utils.ts"; type SessionSearchSource = { - open(metadata: TMetadata): Promise>; + open(metadata: TMetadata): Promise>; list(): Promise; }; @@ -30,12 +30,12 @@ export class ScanningSessionSearch Promise }; - if (typeof storage.cleanup === "function") await storage.cleanup(); + const maybeClosable = storage as { cleanup?: () => Promise }; + if (typeof maybeClosable.cleanup === "function") await maybeClosable.cleanup(); } } return hits; diff --git a/packages/agent/src/harness/types.ts b/packages/agent/src/harness/types.ts index 9a2010e32..4297cc744 100644 --- a/packages/agent/src/harness/types.ts +++ b/packages/agent/src/harness/types.ts @@ -559,17 +559,17 @@ export interface SessionForkOptions { id?: string; } -export interface SessionRepo< +export interface SessionStore< TMetadata extends SessionMetadata = SessionMetadata, TCreateOptions extends SessionCreateOptions = SessionCreateOptions, TListOptions = void, > { - create(options: TCreateOptions): Promise>; - open(metadata: TMetadata): Promise>; + readonly defaultSearch?: SessionSearch; + create(options: TCreateOptions): Promise>; + open(metadata: TMetadata): Promise>; list(options?: TListOptions): Promise; - search(options: SessionSearchOptions): Promise[]>; delete(metadata: TMetadata): Promise; - fork(source: TMetadata, options: SessionForkOptions & TCreateOptions): Promise>; + fork(source: TMetadata, options: SessionForkOptions & TCreateOptions): Promise>; } export interface JsonlSessionCreateOptions extends SessionCreateOptions { @@ -582,8 +582,8 @@ export interface JsonlSessionListOptions { cwd?: string; } -export interface JsonlSessionRepoApi - extends SessionRepo {} +export interface JsonlSessionStoreApi + extends SessionStore {} export type AgentHarnessPhase = "idle" | "turn" | "compaction" | "branch_summary" | "retry"; diff --git a/packages/agent/test/harness/repo.test.ts b/packages/agent/test/harness/repo.test.ts index 009fb0ce7..f7cebbdd0 100644 --- a/packages/agent/test/harness/repo.test.ts +++ b/packages/agent/test/harness/repo.test.ts @@ -1,45 +1,46 @@ import { existsSync } from "node:fs"; import { describe, expect, it } from "vitest"; import { NodeExecutionEnv } from "../../src/harness/env/nodejs.ts"; -import { JsonlSessionRepo } from "../../src/harness/session/jsonl-repo.ts"; -import { InMemorySessionRepo } from "../../src/harness/session/memory-repo.ts"; +import { JsonlSessionStore } from "../../src/harness/session/jsonl-repo.ts"; +import { MemorySessionStore } from "../../src/harness/session/memory-repo.ts"; +import { SessionRepo } from "../../src/harness/session/repo-utils.ts"; import { createAssistantMessage, createTempDir, createUserMessage } from "./session-test-utils.ts"; -describe("InMemorySessionRepo", () => { +describe("MemorySessionStore", () => { it("opens, deletes, and forks by metadata", async () => { - const repo = new InMemorySessionRepo(); - const session = await repo.create({ id: "session-1" }); + const repo = new SessionRepo({ storage: new MemorySessionStore() }); + const session = await repo.storage.create({ id: "session-1" }); const metadata = await session.getMetadata(); const user1 = await session.appendMessage(createUserMessage("one")); const assistant1 = await session.appendMessage(createAssistantMessage("two")); const user2 = await session.appendMessage(createUserMessage("three")); - expect(await repo.open(metadata)).toBe(session); - expect((await repo.list()).map((info) => info.id)).toEqual(["session-1"]); - const fork = await repo.fork(metadata, { entryId: user2, id: "session-2" }); + await expect((await repo.storage.open(metadata)).getMetadata()).resolves.toEqual(metadata); + expect((await repo.storage.list()).map((info) => info.id)).toEqual(["session-1"]); + const fork = await repo.storage.fork(metadata, { entryId: user2, id: "session-2" }); expect((await fork.getEntries()).map((entry) => entry.id)).toEqual([user1, assistant1]); - const fullFork = await repo.fork(metadata, { id: "session-3" }); + const fullFork = await repo.storage.fork(metadata, { id: "session-3" }); expect((await fullFork.getEntries()).map((entry) => entry.id)).toEqual([user1, assistant1, user2]); - await repo.delete(metadata); - await expect(repo.open(metadata)).rejects.toThrow("Session not found: session-1"); + await repo.storage.delete(metadata); + await expect(repo.storage.open(metadata)).rejects.toThrow("Session not found: session-1"); }); }); -describe("JsonlSessionRepo", () => { +describe("JsonlSessionStore", () => { it("stores sessions below encoded cwd directories and lists by cwd", async () => { const root = createTempDir(); const env = new NodeExecutionEnv({ cwd: root }); const cwd = "/tmp/my-project"; const otherCwd = "/tmp/other-project"; - const repo = new JsonlSessionRepo({ fs: env, sessionsRoot: root }); - const session = await repo.create({ cwd, id: "019de8c2-de29-73e9-ae0c-e134db34c447" }); - const otherSession = await repo.create({ cwd: otherCwd, id: "other-session" }); + const repo = new SessionRepo({ storage: new JsonlSessionStore({ fs: env, sessionsRoot: root }) }); + const session = await repo.storage.create({ cwd, id: "019de8c2-de29-73e9-ae0c-e134db34c447" }); + const otherSession = await repo.storage.create({ cwd: otherCwd, id: "other-session" }); const metadata = await session.getMetadata(); const otherMetadata = await otherSession.getMetadata(); expect(metadata.path).toContain("--tmp-my-project--"); expect(otherMetadata.path).toContain("--tmp-other-project--"); expect(existsSync(metadata.path)).toBe(true); - expect((await repo.list({ cwd })).map((sessionMetadata) => sessionMetadata.id)).toEqual([metadata.id]); - expect((await repo.list()).map((sessionMetadata) => sessionMetadata.id).sort()).toEqual( + expect((await repo.storage.list({ cwd })).map((sessionMetadata) => sessionMetadata.id)).toEqual([metadata.id]); + expect((await repo.storage.list()).map((sessionMetadata) => sessionMetadata.id).sort()).toEqual( [metadata.id, otherMetadata.id].sort(), ); }); @@ -47,42 +48,42 @@ describe("JsonlSessionRepo", () => { it("opens, deletes, and forks by metadata", async () => { const root = createTempDir(); const env = new NodeExecutionEnv({ cwd: root }); - const repo = new JsonlSessionRepo({ fs: env, sessionsRoot: root }); - const source = await repo.create({ cwd: "/tmp/source", id: "source-session" }); + const repo = new SessionRepo({ storage: new JsonlSessionStore({ fs: env, sessionsRoot: root }) }); + const source = await repo.storage.create({ cwd: "/tmp/source", id: "source-session" }); const sourceMetadata = await source.getMetadata(); const user1 = await source.appendMessage(createUserMessage("one")); const assistant1 = await source.appendMessage(createAssistantMessage("two")); const user2 = await source.appendMessage(createUserMessage("three")); - await expect((await repo.open(sourceMetadata)).getMetadata()).resolves.toEqual(sourceMetadata); - const fork = await repo.fork(sourceMetadata, { cwd: "/tmp/target", id: "fork-session", entryId: user2 }); + await expect((await repo.storage.open(sourceMetadata)).getMetadata()).resolves.toEqual(sourceMetadata); + const fork = await repo.storage.fork(sourceMetadata, { cwd: "/tmp/target", id: "fork-session", entryId: user2 }); const forkMetadata = await fork.getMetadata(); expect(forkMetadata.cwd).toBe("/tmp/target"); expect(forkMetadata.parentSessionPath).toBe(sourceMetadata.path); expect((await fork.getEntries()).map((entry) => entry.id)).toEqual([user1, assistant1]); - const fullFork = await repo.fork(sourceMetadata, { cwd: "/tmp/target", id: "full-fork-session" }); + const fullFork = await repo.storage.fork(sourceMetadata, { cwd: "/tmp/target", id: "full-fork-session" }); expect((await fullFork.getEntries()).map((entry) => entry.id)).toEqual([user1, assistant1, user2]); - await repo.delete(sourceMetadata); + await repo.storage.delete(sourceMetadata); expect(existsSync(sourceMetadata.path)).toBe(false); - await expect(repo.open(sourceMetadata)).rejects.toThrow("Session not found"); + await expect(repo.storage.open(sourceMetadata)).rejects.toThrow("Session not found"); }); it("persists header metadata through create, list, and fork", async () => { const root = createTempDir(); const env = new NodeExecutionEnv({ cwd: root }); - const repo = new JsonlSessionRepo({ fs: env, sessionsRoot: root }); - const source = await repo.create({ + const repo = new SessionRepo({ storage: new JsonlSessionStore({ fs: env, sessionsRoot: root }) }); + const source = await repo.storage.create({ cwd: "/tmp/source", id: "source-session", metadata: { profile: "reviewer" }, }); const sourceMetadata = await source.getMetadata(); expect(sourceMetadata.metadata).toEqual({ profile: "reviewer" }); - expect((await repo.list({ cwd: "/tmp/source" })).map((listed) => listed.metadata)).toEqual([ + expect((await repo.storage.list({ cwd: "/tmp/source" })).map((listed) => listed.metadata)).toEqual([ { profile: "reviewer" }, ]); - const fork = await repo.fork(sourceMetadata, { cwd: "/tmp/target", id: "fork-session" }); + const fork = await repo.storage.fork(sourceMetadata, { cwd: "/tmp/target", id: "fork-session" }); expect((await fork.getMetadata()).metadata).toEqual({ profile: "reviewer" }); - const overridden = await repo.fork(sourceMetadata, { + const overridden = await repo.storage.fork(sourceMetadata, { cwd: "/tmp/target", id: "overridden-session", metadata: { profile: "writer" }, diff --git a/packages/agent/test/harness/sqlite-migrations.test.ts b/packages/agent/test/harness/sqlite-migrations.test.ts index 013d4c93b..c3bd0ebf9 100644 --- a/packages/agent/test/harness/sqlite-migrations.test.ts +++ b/packages/agent/test/harness/sqlite-migrations.test.ts @@ -9,11 +9,12 @@ import { type SqliteDatabaseFactory, type SqliteRunResult, type SqliteSessionMetadata, - SqliteSessionRepo, SqliteSessionStorage, + SqliteSessionStore, type SqliteStatement, } from "../../../storage/sqlite-node/src/index.ts"; import { NodeExecutionEnv } from "../../src/harness/env/nodejs.ts"; +import { SessionRepo } from "../../src/harness/session/repo-utils.ts"; import { createAssistantMessage, createUserMessage } from "./session-test-utils.ts"; function createTempDir(): string { @@ -69,8 +70,8 @@ describe("SQLite migrations", () => { const databasePath = join(root, "sessions.sqlite"); const env = new NodeExecutionEnv({ cwd: root }); const sqlite = createNodeSqliteFactory(); - const repo = new SqliteSessionRepo({ env, sqlite, databasePath }); - await repo.create({ cwd: root, id: "session-1" }); + const repo = new SessionRepo({ storage: new SqliteSessionStore({ env, sqlite, databasePath }) }); + await repo.storage.create({ cwd: root, id: "session-1" }); const db = await sqlite.open(databasePath); try { @@ -111,19 +112,23 @@ describe("SQLite migrations", () => { const root = createTempDir(); const databasePath = join(root, "sessions.sqlite"); const env = new NodeExecutionEnv({ cwd: root }); - const repo = new SqliteSessionRepo({ env, sqlite: createNodeSqliteFactory(), databasePath }); - const source = await repo.create({ + const repo = new SessionRepo({ + storage: new SqliteSessionStore({ env, sqlite: createNodeSqliteFactory(), databasePath }), + }); + const source = await repo.storage.create({ cwd: root, id: "session-1", metadata: { profile: "reviewer" }, }); const sourceMetadata = await source.getMetadata(); expect(sourceMetadata.metadata).toEqual({ profile: "reviewer" }); - expect((await repo.list({ cwd: root })).map((listed) => listed.metadata)).toEqual([{ profile: "reviewer" }]); - expect((await (await repo.open(sourceMetadata)).getMetadata()).metadata).toEqual({ profile: "reviewer" }); - const fork = await repo.fork(sourceMetadata, { cwd: root, id: "session-2" }); + expect((await repo.storage.list({ cwd: root })).map((listed) => listed.metadata)).toEqual([ + { profile: "reviewer" }, + ]); + expect((await (await repo.storage.open(sourceMetadata)).getMetadata()).metadata).toEqual({ profile: "reviewer" }); + const fork = await repo.storage.fork(sourceMetadata, { cwd: root, id: "session-2" }); expect((await fork.getMetadata()).metadata).toEqual({ profile: "reviewer" }); - const overridden = await repo.fork(sourceMetadata, { + const overridden = await repo.storage.fork(sourceMetadata, { cwd: root, id: "session-3", metadata: { profile: "writer" }, @@ -136,8 +141,8 @@ describe("SQLite migrations", () => { const databasePath = join(root, "sessions.sqlite"); const env = new NodeExecutionEnv({ cwd: root }); const sqlite = createNodeSqliteFactory(); - const repo = new SqliteSessionRepo({ env, sqlite, databasePath }); - const session = await repo.create({ cwd: root, id: "session-1" }); + const repo = new SessionRepo({ storage: new SqliteSessionStore({ env, sqlite, databasePath }) }); + const session = await repo.storage.create({ cwd: root, id: "session-1" }); const rootId = await session.appendMessage(createUserMessage("root")); const childId = await session.appendMessage(createAssistantMessage("child")); await session.getStorage().setLeafId(rootId); @@ -162,7 +167,7 @@ describe("SQLite migrations", () => { await db.close(); } - const reopened = await repo.open(await session.getMetadata()); + const reopened = await repo.storage.open(await session.getMetadata()); expect(await reopened.getLeafId()).toBe(rootId); expect(childId).not.toBe(rootId); }); @@ -172,8 +177,8 @@ describe("SQLite migrations", () => { const databasePath = join(root, "sessions.sqlite"); const env = new NodeExecutionEnv({ cwd: root }); const sqlite = createNodeSqliteFactory(); - const repo = new SqliteSessionRepo({ env, sqlite, databasePath }); - const session = await repo.create({ cwd: root, id: "session-1" }); + const repo = new SessionRepo({ storage: new SqliteSessionStore({ env, sqlite, databasePath }) }); + const session = await repo.storage.create({ cwd: root, id: "session-1" }); const rootId = await session.appendMessage(createUserMessage("root")); const firstChildId = await session.appendMessage(createAssistantMessage("first child")); await session.getStorage().setLeafId(rootId); @@ -200,15 +205,17 @@ describe("SQLite migrations", () => { const root = createTempDir(); const databasePath = join(root, "sessions.sqlite"); const env = new NodeExecutionEnv({ cwd: root }); - const repo = new SqliteSessionRepo({ env, sqlite: createNodeSqliteFactory(), databasePath }); - const session = await repo.create({ cwd: root, id: "session-1" }); + const repo = new SessionRepo({ + storage: new SqliteSessionStore({ env, sqlite: createNodeSqliteFactory(), databasePath }), + }); + const session = await repo.storage.create({ cwd: root, id: "session-1" }); const rootId = await session.appendMessage(createUserMessage("root")); await session.appendMessage(createAssistantMessage("first child")); await session.appendSessionName(" Reopened Session "); await session.getStorage().setLeafId(rootId); await session.appendMessage(createAssistantMessage("branched child")); - const reopened = await repo.open(await session.getMetadata()); + const reopened = await repo.storage.open(await session.getMetadata()); expect(await reopened.getSessionName()).toBe("Reopened Session"); expect((await reopened.buildContext()).messages.map((message) => message.role)).toEqual(["user", "assistant"]); expect((await reopened.buildContext()).messages.at(-1)).toMatchObject({ @@ -220,8 +227,10 @@ describe("SQLite migrations", () => { const root = createTempDir(); const databasePath = join(root, "sessions.sqlite"); const env = new NodeExecutionEnv({ cwd: root }); - const repo = new SqliteSessionRepo({ env, sqlite: createNodeSqliteFactory(), databasePath }); - const session = await repo.create({ cwd: root, id: "session-1" }); + const repo = new SessionRepo({ + storage: new SqliteSessionStore({ env, sqlite: createNodeSqliteFactory(), databasePath }), + }); + const session = await repo.storage.create({ cwd: root, id: "session-1" }); await session.appendMessage(createUserMessage("one")); await session.appendMessage(createAssistantMessage("two")); await session.appendMessage(createUserMessage("three")); @@ -247,9 +256,11 @@ describe("SQLite migrations", () => { open: async () => db, }; const env = new NodeExecutionEnv({ cwd: root }); - const repo = new SqliteSessionRepo({ env, sqlite, databasePath: join(root, "sessions.sqlite") }); + const repo = new SessionRepo({ + storage: new SqliteSessionStore({ env, sqlite, databasePath: join(root, "sessions.sqlite") }), + }); - await expect(repo.create({ cwd: root, id: "session-1" })).rejects.toThrow("insert failed"); + await expect(repo.storage.create({ cwd: root, id: "session-1" })).rejects.toThrow("insert failed"); expect(db.closeCount).toBe(1); }); @@ -265,7 +276,9 @@ describe("SQLite migrations", () => { open: async () => db, }; const env = new NodeExecutionEnv({ cwd: root }); - const repo = new SqliteSessionRepo({ env, sqlite, databasePath: join(root, "sessions.sqlite") }); + const repo = new SessionRepo({ + storage: new SqliteSessionStore({ env, sqlite, databasePath: join(root, "sessions.sqlite") }), + }); const metadata: SqliteSessionMetadata = { id: "missing", createdAt: new Date().toISOString(), @@ -274,7 +287,7 @@ describe("SQLite migrations", () => { }; writeFileSync(metadata.path, ""); - await expect(repo.open(metadata)).rejects.toThrow("Session not found: missing"); + await expect(repo.storage.open(metadata)).rejects.toThrow("Session not found: missing"); expect(db.closeCount).toBe(1); }); @@ -282,7 +295,8 @@ describe("SQLite migrations", () => { const root = createTempDir(); const databasePath = join(root, "sessions.sqlite"); const env = new NodeExecutionEnv({ cwd: root }); - const repo = new SqliteSessionRepo({ env, sqlite: createNodeSqliteFactory(), databasePath }); + const storage = new SqliteSessionStore({ env, sqlite: createNodeSqliteFactory(), databasePath }); + const repo = new SessionRepo({ storage }); let cleanupCount = 0; const sourceStorage = { async getEntries() { @@ -295,19 +309,11 @@ describe("SQLite migrations", () => { cleanupCount += 1; }, } as const; - const originalOpen = repo.open.bind(repo); - repo.open = async () => - ({ - getStorage() { - return sourceStorage; - }, - async getEntries() { - return []; - }, - }) as never; + const originalOpen = storage.open.bind(storage); + storage.open = async () => sourceStorage as never; try { - await repo.fork( + await repo.storage.fork( { id: "session-1", createdAt: new Date().toISOString(), @@ -317,7 +323,7 @@ describe("SQLite migrations", () => { { cwd: root, id: "session-2" }, ); } finally { - repo.open = originalOpen; + storage.open = originalOpen; } expect(cleanupCount).toBe(1); @@ -363,8 +369,8 @@ describe("SQLite migrations", () => { const databasePath = join(root, "sessions.sqlite"); const env = new NodeExecutionEnv({ cwd: root }); const sqlite = createNodeSqliteFactory(); - const repo = new SqliteSessionRepo({ env, sqlite, databasePath }); - const session = await repo.create({ cwd: root, id: "session-1" }); + const repo = new SessionRepo({ storage: new SqliteSessionStore({ env, sqlite, databasePath }) }); + const session = await repo.storage.create({ cwd: root, id: "session-1" }); const userId = await session.appendMessage(createUserMessage("one")); await session.appendThinkingLevelChange("high"); await session.appendModelChange("anthropic", "claude-sonnet-4-5"); diff --git a/packages/agent/test/harness/sqlite-node.test.ts b/packages/agent/test/harness/sqlite-node.test.ts index 3e40562d4..5e6fd06f2 100644 --- a/packages/agent/test/harness/sqlite-node.test.ts +++ b/packages/agent/test/harness/sqlite-node.test.ts @@ -3,11 +3,12 @@ import { describe, expect, it } from "vitest"; import { createNodeSqliteFactory, type SqliteSessionMetadata, - SqliteSessionRepo, SqliteSessionSearch, + SqliteSessionStore, } from "../../../storage/sqlite-node/src/index.ts"; import { NodeExecutionEnv } from "../../src/harness/env/nodejs.ts"; -import { JsonlSessionRepo } from "../../src/harness/session/jsonl-repo.ts"; +import { JsonlSessionStore } from "../../src/harness/session/jsonl-repo.ts"; +import { SessionRepo } from "../../src/harness/session/repo-utils.ts"; import { FanoutSessionSearch } from "../../src/harness/session/search-backend.ts"; import type { JsonlSessionMetadata, @@ -20,39 +21,41 @@ import type { } from "../../src/harness/types.ts"; import { createTempDir, createUserMessage } from "./session-test-utils.ts"; -describe("JsonlSessionRepo with scanning search", () => { +describe("JsonlSessionStore with scanning search", () => { it("searches canonical session entries by scanning", async () => { const root = createTempDir(); const env = new NodeExecutionEnv({ cwd: root }); - const repo = new JsonlSessionRepo({ fs: env, sessionsRoot: join(root, "sessions") }); - const included = await repo.create({ cwd: root, id: "included" }); - const excluded = await repo.create({ cwd: `${root}/other`, id: "excluded" }); + const repo = new SessionRepo({ + storage: new JsonlSessionStore({ fs: env, sessionsRoot: join(root, "sessions") }), + }); + const included = await repo.storage.create({ cwd: root, id: "included" }); + const excluded = await repo.storage.create({ cwd: `${root}/other`, id: "excluded" }); const entryId = await included.appendMessage(createUserMessage("Find the auth defect")); await excluded.appendMessage(createUserMessage("Find the auth defect")); - await expect(repo.search({ text: "AUTH", cwd: root })).resolves.toEqual([ + await expect(repo.search?.search({ text: "AUTH", cwd: root })).resolves.toEqual([ expect.objectContaining({ entryId, metadata: expect.objectContaining({ id: "included" }) }), ]); }); }); -describe("SqliteSessionRepo with default SQLite FTS5 search", () => { +describe("SqliteSessionStore with default SQLite FTS5 search", () => { it("uses SQLite FTS5 by default", async () => { const root = createTempDir(); const env = new NodeExecutionEnv({ cwd: root }); const sqlite = createNodeSqliteFactory(); const databasePath = join(root, "sessions.sqlite"); - const repo = new SqliteSessionRepo({ env, sqlite, databasePath }); - const included = await repo.create({ cwd: root, id: "included" }); - const excluded = await repo.create({ cwd: `${root}/other`, id: "excluded" }); + const repo = new SessionRepo({ storage: new SqliteSessionStore({ env, sqlite, databasePath }) }); + const included = await repo.storage.create({ cwd: root, id: "included" }); + const excluded = await repo.storage.create({ cwd: `${root}/other`, id: "excluded" }); const metadata = await included.getMetadata(); const entryId = await included.appendMessage(createUserMessage("Find the auth defect")); await excluded.appendMessage(createUserMessage("Find the auth defect")); - await expect(repo.search({ text: "auth", cwd: root })).resolves.toEqual([ + await expect(repo.search?.search({ text: "auth", cwd: root })).resolves.toEqual([ expect.objectContaining({ entryId, metadata: expect.objectContaining({ id: "included" }) }), ]); - await expect(repo.search({ text: "uth", cwd: root })).resolves.toEqual([ + await expect(repo.search?.search({ text: "uth", cwd: root })).resolves.toEqual([ expect.objectContaining({ entryId, metadata: expect.objectContaining({ id: "included" }) }), ]); @@ -67,33 +70,32 @@ describe("SqliteSessionRepo with default SQLite FTS5 search", () => { await db.close(); } - await repo.delete(metadata); - await expect(repo.search({ text: "auth", cwd: root })).resolves.toEqual([]); + await repo.storage.delete(metadata); + await expect(repo.search?.search({ text: "auth", cwd: root })).resolves.toEqual([]); }); }); -describe("JsonlSessionRepo with SQLite search index", () => { +describe("JsonlSessionStore with SQLite search index", () => { // This is not the intended production pairing; it exists to demonstrate that // Canonical session storage and search are independently swappable/composable. it("writes JSONL session entries into the configured SQLite search", async () => { const root = createTempDir(); const env = new NodeExecutionEnv({ cwd: root }); const sqlite = createNodeSqliteFactory(); - const repo = new JsonlSessionRepo({ - fs: env, - sessionsRoot: join(root, "sessions"), + const repo = new SessionRepo({ + storage: new JsonlSessionStore({ fs: env, sessionsRoot: join(root, "sessions") }), search: new SqliteSessionSearch({ env, sqlite, databasePath: join(root, "search.sqlite") }), }); - const session = await repo.create({ cwd: root, id: "jsonl-session" }); + const session = await repo.storage.create({ cwd: root, id: "jsonl-session" }); const entryId = await session.appendMessage(createUserMessage("Find the auth defect")); - await expect(repo.search({ text: "auth", cwd: root })).resolves.toEqual([ + await expect(repo.search?.search({ text: "auth", cwd: root })).resolves.toEqual([ expect.objectContaining({ entryId, metadata: expect.objectContaining({ id: "jsonl-session" }) }), ]); }); }); -describe("JsonlSessionRepo with multiple search indexes", () => { +describe("JsonlSessionStore with multiple search indexes", () => { it("queries one search implementation and fans index writes out to both", async () => { const root = createTempDir(); const env = new NodeExecutionEnv({ cwd: root }); @@ -108,15 +110,14 @@ describe("JsonlSessionRepo with multiple search indexes", () => { sqlite, databasePath: join(root, "secondary-search.sqlite"), }); - const repo = new JsonlSessionRepo({ - fs: env, - sessionsRoot: join(root, "sessions"), + const repo = new SessionRepo({ + storage: new JsonlSessionStore({ fs: env, sessionsRoot: join(root, "sessions") }), search: new FanoutSessionSearch({ reader: primary, writers: [primary, secondary] }), }); - const session = await repo.create({ cwd: root, id: "jsonl-session" }); + const session = await repo.storage.create({ cwd: root, id: "jsonl-session" }); const entryId = await session.appendMessage(createUserMessage("indexed in both places")); - await expect(repo.search({ text: "both" })).resolves.toEqual([ + await expect(repo.search?.search({ text: "both" })).resolves.toEqual([ expect.objectContaining({ entryId, metadata: expect.objectContaining({ id: "jsonl-session" }) }), ]); await expect(secondary.search({ text: "both" })).resolves.toEqual([ @@ -125,7 +126,7 @@ describe("JsonlSessionRepo with multiple search indexes", () => { }); }); -describe("SqliteSessionRepo with custom search", () => { +describe("SqliteSessionStore with custom search", () => { it("can swap out the default search implementation", async () => { const root = createTempDir(); const upserts: SessionSearchRecord[] = []; @@ -149,13 +150,15 @@ describe("SqliteSessionRepo with custom search", () => { return []; }, }; - const repo = new SqliteSessionRepo({ - env: new NodeExecutionEnv({ cwd: root }), - sqlite: createNodeSqliteFactory(), - databasePath: join(root, "sessions.sqlite"), + const repo = new SessionRepo({ + storage: new SqliteSessionStore({ + env: new NodeExecutionEnv({ cwd: root }), + sqlite: createNodeSqliteFactory(), + databasePath: join(root, "sessions.sqlite"), + }), search, }); - const session = await repo.create({ cwd: root, id: "session-1" }); + const session = await repo.storage.create({ cwd: root, id: "session-1" }); const metadata = await session.getMetadata(); const entryId = await session.appendMessage(createUserMessage("indexed remotely")); await session.getStorage().setLeafId(null); @@ -166,9 +169,9 @@ describe("SqliteSessionRepo with custom search", () => { expect(upserts).toContainEqual( expect.objectContaining({ metadata, entry: expect.objectContaining({ type: "leaf", targetId: null }) }), ); - await expect(repo.search({ text: "indexed remotely" })).resolves.toEqual([]); + await expect(repo.search?.search({ text: "indexed remotely" })).resolves.toEqual([]); expect(searches).toEqual([{ text: "indexed remotely" }]); - await repo.delete(metadata); + await repo.storage.delete(metadata); expect(removals).toEqual([metadata]); }); }); diff --git a/packages/storage/sqlite-node/README.md b/packages/storage/sqlite-node/README.md index 3b423ff67..9c6a809d9 100644 --- a/packages/storage/sqlite-node/README.md +++ b/packages/storage/sqlite-node/README.md @@ -2,4 +2,4 @@ Node sqlite storage backend for `@earendil-works/pi-agent-core` sessions. Provides the `node:sqlite` adapter (`SqliteDatabase` implementation) and the SQLite session -repo/storage implementation (`SqliteSessionRepo`, migrations, materialized views). +store/storage implementation (`SqliteSessionStore`, migrations, materialized views). diff --git a/packages/storage/sqlite-node/src/sqlite/repo.ts b/packages/storage/sqlite-node/src/sqlite/repo.ts index 2b2a31fd8..f22f3b68e 100644 --- a/packages/storage/sqlite-node/src/sqlite/repo.ts +++ b/packages/storage/sqlite-node/src/sqlite/repo.ts @@ -1,16 +1,9 @@ -import type { - Session, - SessionSearch, - SessionSearchOptions, - SessionStorage, - SessionTreeEntry, -} from "@earendil-works/pi-agent-core"; +import type { SessionSearch, SessionStorage, SessionTreeEntry } from "@earendil-works/pi-agent-core"; import { createSessionId, getEntriesToFork, getFileSystemResultOrThrow, SessionError, - toRepoSession, } from "@earendil-works/pi-agent-core"; import { applyMigrations } from "./migrations.ts"; import { SqliteSessionSearch } from "./search-backend.ts"; @@ -22,8 +15,8 @@ import type { SqliteSessionCreateOptions, SqliteSessionListOptions, SqliteSessionMetadata, - SqliteSessionRepoApi, - SqliteSessionRepoEnv, + SqliteSessionStoreApi, + SqliteSessionStoreEnv, } from "./types.ts"; function getParentPath(path: string): string { @@ -45,29 +38,26 @@ async function cleanupSessionStorage(storage: SessionStorage): Promise { if (typeof maybeClosable.cleanup === "function") await maybeClosable.cleanup(); } -export class SqliteSessionRepo implements SqliteSessionRepoApi { - private readonly env: SqliteSessionRepoEnv; +export class SqliteSessionStore implements SqliteSessionStoreApi { + private readonly env: SqliteSessionStoreEnv; private readonly sqlite: SqliteDatabaseFactory; private readonly databasePathInput: string; - private readonly sessionSearch: SessionSearch; + readonly defaultSearch: SessionSearch; private databasePath: string | undefined; constructor(options: { - env: SqliteSessionRepoEnv; + env: SqliteSessionStoreEnv; sqlite: SqliteDatabaseFactory; databasePath: string; - search?: SessionSearch; }) { this.env = options.env; this.sqlite = options.sqlite; this.databasePathInput = options.databasePath; - this.sessionSearch = - options.search ?? - new SqliteSessionSearch({ - env: this.env, - sqlite: this.sqlite, - databasePath: this.databasePathInput, - }); + this.defaultSearch = new SqliteSessionSearch({ + env: this.env, + sqlite: this.sqlite, + databasePath: this.databasePathInput, + }); } private async getDatabasePath(): Promise { @@ -102,24 +92,23 @@ export class SqliteSessionRepo implements SqliteSessionRepoApi { } } - async create(options: SqliteSessionCreateOptions): Promise> { + async create(options: SqliteSessionCreateOptions): Promise> { const db = await this.openDatabase(); try { const id = options.id ?? createSessionId(); - const storage = await SqliteSessionStorage.create(db, await this.getDatabasePath(), { + return await SqliteSessionStorage.create(db, await this.getDatabasePath(), { cwd: options.cwd, sessionId: id, parentSessionId: options.parentSessionId, metadata: options.metadata, }); - return await toRepoSession(storage, this.sessionSearch); } catch (error) { await db.close(); throw error; } } - async open(metadata: SqliteSessionMetadata): Promise> { + async open(metadata: SqliteSessionMetadata): Promise> { if ( !getFileSystemResultOrThrow(await this.env.exists(metadata.path), `Failed to check database ${metadata.path}`) ) { @@ -127,7 +116,7 @@ export class SqliteSessionRepo implements SqliteSessionRepoApi { } const db = await this.openDatabase(); try { - return await toRepoSession(await SqliteSessionStorage.open(db, metadata), this.sessionSearch); + return await SqliteSessionStorage.open(db, metadata); } catch (error) { await db.close(); throw error; @@ -156,12 +145,7 @@ export class SqliteSessionRepo implements SqliteSessionRepoApi { } } - async search(options: SessionSearchOptions) { - return await this.sessionSearch.search(options); - } - async delete(metadata: SqliteSessionMetadata): Promise { - await this.sessionSearch.index?.removeSession(metadata); const db = await this.openDatabase(); try { await db.transaction(async () => { @@ -181,13 +165,13 @@ export class SqliteSessionRepo implements SqliteSessionRepoApi { async fork( sourceMetadata: SqliteSessionMetadata, options: SqliteSessionCreateOptions & { entryId?: string; position?: "before" | "at"; id?: string }, - ): Promise> { + ): Promise> { const source = await this.open(sourceMetadata); let forkedEntries: SessionTreeEntry[]; try { - forkedEntries = await getEntriesToFork(source.getStorage(), options); + forkedEntries = await getEntriesToFork(source, options); } finally { - await cleanupSessionStorage(source.getStorage()); + await cleanupSessionStorage(source); } const db = await this.openDatabase(); try { @@ -199,8 +183,7 @@ export class SqliteSessionRepo implements SqliteSessionRepoApi { metadata: options.metadata ?? sourceMetadata.metadata, }); for (const entry of forkedEntries) await storage.appendEntry(entry); - await this.sessionSearch.index?.replaceSession(await storage.getMetadata(), forkedEntries); - return await toRepoSession(storage, this.sessionSearch); + return storage; } catch (error) { await db.close(); throw error; diff --git a/packages/storage/sqlite-node/src/sqlite/search-backend.ts b/packages/storage/sqlite-node/src/sqlite/search-backend.ts index b90781d09..e0d0bc756 100644 --- a/packages/storage/sqlite-node/src/sqlite/search-backend.ts +++ b/packages/storage/sqlite-node/src/sqlite/search-backend.ts @@ -8,7 +8,7 @@ import type { SessionTreeEntry, } from "@earendil-works/pi-agent-core"; import { getFileSystemResultOrThrow } from "@earendil-works/pi-agent-core"; -import type { SqliteDatabase, SqliteDatabaseFactory, SqliteSessionRepoEnv } from "./types.ts"; +import type { SqliteDatabase, SqliteDatabaseFactory, SqliteSessionStoreEnv } from "./types.ts"; function getParentPath(path: string): string { const normalized = path.replace(/[\\/]+$/, ""); @@ -47,14 +47,14 @@ export class SqliteSessionSearch = this; private readonly options: { - env: Pick; + env: Pick; sqlite: SqliteDatabaseFactory; databasePath: string; }; private databasePath: string | undefined; constructor(options: { - env: Pick; + env: Pick; sqlite: SqliteDatabaseFactory; databasePath: string; }) { diff --git a/packages/storage/sqlite-node/src/sqlite/types.ts b/packages/storage/sqlite-node/src/sqlite/types.ts index 564b65466..3fb3ea5e8 100644 --- a/packages/storage/sqlite-node/src/sqlite/types.ts +++ b/packages/storage/sqlite-node/src/sqlite/types.ts @@ -1,4 +1,4 @@ -import type { FileSystem, SessionCreateOptions, SessionMetadata, SessionRepo } from "@earendil-works/pi-agent-core"; +import type { FileSystem, SessionCreateOptions, SessionMetadata, SessionStore } from "@earendil-works/pi-agent-core"; /** Result of a prepared SQLite statement execution. */ export interface SqliteRunResult { @@ -44,7 +44,7 @@ export interface SqliteSessionListOptions { cwd?: string; } -export interface SqliteSessionRepoApi - extends SessionRepo {} +export interface SqliteSessionStoreApi + extends SessionStore {} -export type SqliteSessionRepoEnv = Pick; +export type SqliteSessionStoreEnv = Pick; diff --git a/scripts/browser-smoke-entry.ts b/scripts/browser-smoke-entry.ts index 31dada999..c5c7ee247 100644 --- a/scripts/browser-smoke-entry.ts +++ b/scripts/browser-smoke-entry.ts @@ -10,9 +10,10 @@ import { formatSkillInvocation, formatSkillsForSystemPrompt, getOrThrow, - InMemorySessionRepo, + MemorySessionStore, ok, parseCommandArgs, + SessionRepo, streamProxy, toError, truncateHead, @@ -26,7 +27,7 @@ const stream = createAssistantMessageEventStream(); const agent = new Agent({ initialState: { model }, streamFn: streamSimple }); agent.steer({ role: "user", content: [{ type: "text", text: "queued" }], timestamp: 0 }); -const repo = new InMemorySessionRepo(); +const repo = new SessionRepo({ storage: new MemorySessionStore() }); const result = getOrThrow(ok({ value: 1 })); const customMessage = createCustomMessage("note", "hello", true, undefined, "2026-01-01T00:00:00.000Z"); const llmMessages = convertToLlm([customMessage]); @@ -39,7 +40,7 @@ console.log( schema.type, typeof stream.push, agent.hasQueuedMessages(), - typeof repo.create, + typeof repo.storage.create, result.value, llmMessages.length, bashExecutionToText({