From 5e48d41ecd8c750c954e6c4152fd343f7705e182 Mon Sep 17 00:00:00 2001 From: Cristina Poncela Cubeiro <140309543+cristinaponcela@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:07:58 +0200 Subject: [PATCH] refactor: repo facade --- .../agent/src/harness/session/repo-utils.ts | 145 +++++++----------- .../src/harness/session/search-backend.ts | 44 ------ .../agent/src/harness/session/search-index.ts | 95 ++++++++++++ packages/agent/src/harness/session/session.ts | 42 ++--- packages/agent/src/harness/types.ts | 17 +- packages/agent/src/index.ts | 1 + packages/agent/test/harness/repo.test.ts | 52 +++---- .../test/harness/sqlite-migrations.test.ts | 72 +++++---- .../agent/test/harness/sqlite-node.test.ts | 106 ++++++++----- .../storage/sqlite-node/src/sqlite/repo.ts | 14 +- .../sqlite-node/src/sqlite/search-backend.ts | 22 ++- scripts/browser-smoke-entry.ts | 6 +- 12 files changed, 318 insertions(+), 298 deletions(-) create mode 100644 packages/agent/src/harness/session/search-index.ts diff --git a/packages/agent/src/harness/session/repo-utils.ts b/packages/agent/src/harness/session/repo-utils.ts index b2b17085d..4e104a2d3 100644 --- a/packages/agent/src/harness/session/repo-utils.ts +++ b/packages/agent/src/harness/session/repo-utils.ts @@ -8,11 +8,9 @@ import { type SessionMetadata, type SessionSearch, type SessionSearchHit, - type SessionSearchIndex, type SessionStorage, type SessionStore, type SessionTreeEntry, - type SessionWriter, } from "../types.ts"; import { Session } from "./session.ts"; @@ -24,110 +22,75 @@ export function createTimestamp(): string { return new Date().toISOString(); } -export function toSession( - storage: SessionStorage, - writer?: SessionWriter, -): Session { - return new Session(storage, {}, writer); +export function toSession(storage: SessionStorage): Session { + return new Session(storage); } -/** Repository-owned coordination between canonical storage writes and independent search indexing. */ -class SessionRepoWriter implements SessionWriter { - private readonly storage: SessionStorage; - private readonly metadata: TMetadata; - private readonly index: SessionSearchIndex; - - constructor(storage: SessionStorage, metadata: TMetadata, index: SessionSearchIndex) { - this.storage = storage; - this.metadata = metadata; - this.index = index; - } - - async appendEntry(entry: SessionTreeEntry): Promise { - await this.storage.appendEntry(entry); - await this.index.upsert({ metadata: this.metadata, entry }); - } - - async setLeafId(leafId: string | null) { - const entry = await this.storage.setLeafId(leafId); - await this.index.upsert({ metadata: this.metadata, entry }); - return entry; - } +export function after( + operation: (...args: TArgs) => Promise, + callback: (...args: TArgs) => Promise, +): (...args: TArgs) => Promise { + return async (...args: TArgs) => { + const result = await operation(...args); + await callback(...args); + return result; + }; } -export async function toRepoSession( - storage: SessionStorage, - search: SessionSearch, -): Promise> { - if (!search.index) return toSession(storage); - const metadata = await storage.getMetadata(); - 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< +export class SessionRepository< TMetadata extends SessionMetadata = SessionMetadata, TCreateOptions extends SessionCreateOptions = SessionCreateOptions, TListOptions = void, > { - readonly storage: BoundSessionStore; - readonly search: SessionSearch | null; + private readonly store: SessionStore; + private readonly searchBackend: SessionSearch | null; constructor(options: { - storage: SessionStore; + store: SessionStore; search?: SessionSearch | null; }) { - this.search = options.search === undefined ? (options.storage.defaultSearch ?? null) : options.search; - this.storage = bindSessionStore(options.storage, this.search); + this.store = options.store; + this.searchBackend = options.search === undefined ? (options.store.defaultSearch ?? null) : options.search; + } + + async create(options: TCreateOptions): Promise> { + return toSession(await this.store.create(options)); + } + + async open(metadata: TMetadata): Promise> { + return toSession(await this.store.open(metadata)); + } + + list(options?: TListOptions): Promise { + return this.store.list(options); + } + + async delete(metadata: TMetadata): Promise { + await this.store.delete(metadata); + } + + async fork(source: TMetadata, options: SessionForkOptions & TCreateOptions): Promise> { + return toSession(await this.store.fork(source, options)); + } + + async search(options: Parameters["search"]>[0]): Promise[]> { + return this.searchBackend ? await this.searchBackend.search(options) : []; } } +export function createSessionRepository< + TMetadata extends SessionMetadata = SessionMetadata, + TCreateOptions extends SessionCreateOptions = SessionCreateOptions, + TListOptions = void, +>(options: { + store: SessionStore; + search?: SessionSearch | null; +}): SessionRepository { + return new SessionRepository(options); +} + +export { SessionRepository as SessionRepo }; + 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 7288fe7df..f203bae2a 100644 --- a/packages/agent/src/harness/session/search-backend.ts +++ b/packages/agent/src/harness/session/search-backend.ts @@ -2,11 +2,8 @@ import type { SessionMetadata, SessionSearch, SessionSearchHit, - SessionSearchIndex, SessionSearchOptions, - SessionSearchRecord, SessionStorage, - SessionTreeEntry, } from "../types.ts"; import { findSessionEntryMatches } from "./repo-utils.ts"; @@ -41,44 +38,3 @@ export class ScanningSessionSearch implements SessionSearchIndex { - private readonly indexes: readonly SessionSearchIndex[]; - - constructor(indexes: readonly SessionSearchIndex[]) { - this.indexes = indexes; - } - - async upsert(record: SessionSearchRecord): Promise { - for (const index of this.indexes) await index.upsert(record); - } - - async replaceSession(metadata: TMetadata, entries: readonly SessionTreeEntry[]): Promise { - for (const index of this.indexes) await index.replaceSession(metadata, entries); - } - - async removeSession(metadata: TMetadata): Promise { - for (const index of this.indexes) await index.removeSession(metadata); - } -} - -/** Delegates queries to one search and fans index mutations out sequentially, like io.MultiWriter. */ -export class FanoutSessionSearch - implements SessionSearch -{ - private readonly reader: SessionSearch; - readonly index?: SessionSearchIndex; - - constructor(options: { - reader: SessionSearch; - writers: readonly SessionSearch[]; - }) { - this.reader = options.reader; - const indexes = options.writers.flatMap((search) => (search.index ? [search.index] : [])); - this.index = indexes.length > 0 ? new FanoutSessionSearchIndex(indexes) : undefined; - } - - async search(options: SessionSearchOptions): Promise[]> { - return await this.reader.search(options); - } -} diff --git a/packages/agent/src/harness/session/search-index.ts b/packages/agent/src/harness/session/search-index.ts new file mode 100644 index 000000000..a2ef84976 --- /dev/null +++ b/packages/agent/src/harness/session/search-index.ts @@ -0,0 +1,95 @@ +import type { + SessionCreateOptions, + SessionForkOptions, + SessionMetadata, + SessionSearch, + SessionSearchIndex, + SessionStorage, + SessionStore, + SessionTreeEntry, +} from "../types.ts"; + +export function indexSessionStorage( + storage: SessionStorage, + index: SessionSearchIndex, +): SessionStorage { + return new Proxy(storage, { + get(target, property) { + if (property === "appendEntry") { + return async (entry: SessionTreeEntry) => { + await target.appendEntry(entry); + await index.upsertEntry(await target.getMetadata(), entry); + }; + } + if (property === "setLeafId") { + return async (leafId: string | null) => { + const entry = await target.setLeafId(leafId); + await index.upsertEntry(await target.getMetadata(), entry); + return entry; + }; + } + const value = Reflect.get(target, property, target) as unknown; + return typeof value === "function" ? value.bind(target) : value; + }, + }); +} + +export class IndexedSessionStore< + TMetadata extends SessionMetadata = SessionMetadata, + TCreateOptions extends SessionCreateOptions = SessionCreateOptions, + TListOptions = void, +> implements SessionStore +{ + readonly defaultSearch?: SessionSearch; + private readonly store: SessionStore; + private readonly index: SessionSearchIndex; + + constructor(options: { + store: SessionStore; + index: SessionSearchIndex; + defaultSearch?: SessionSearch; + }) { + this.store = options.store; + this.index = options.index; + this.defaultSearch = options.defaultSearch ?? options.store.defaultSearch; + } + + async create(options: TCreateOptions): Promise> { + return indexSessionStorage(await this.store.create(options), this.index); + } + + async open(metadata: TMetadata): Promise> { + return indexSessionStorage(await this.store.open(metadata), this.index); + } + + async list(options?: TListOptions): Promise { + return this.store.list(options); + } + + async delete(metadata: TMetadata): Promise { + await this.store.delete(metadata); + await this.index.deleteSession(metadata); + } + + async fork(source: TMetadata, options: SessionForkOptions & TCreateOptions): Promise> { + const storage = await this.store.fork(source, options); + await this.index.replaceSession(await storage.getMetadata(), await storage.getEntries()); + return indexSessionStorage(storage, this.index); + } +} + +export async function rebuildSessionSearchIndex( + store: SessionStore, + index: SessionSearchIndex, + options?: TListOptions, +): Promise { + for (const metadata of await store.list(options)) { + const storage = await store.open(metadata); + try { + await index.replaceSession(metadata, await storage.getEntries()); + } finally { + const maybeClosable = storage as SessionStorage & { cleanup?: () => Promise }; + if (typeof maybeClosable.cleanup === "function") await maybeClosable.cleanup(); + } + } +} diff --git a/packages/agent/src/harness/session/session.ts b/packages/agent/src/harness/session/session.ts index 98da840bd..5fd6c3b27 100644 --- a/packages/agent/src/harness/session/session.ts +++ b/packages/agent/src/harness/session/session.ts @@ -8,6 +8,7 @@ import type { CustomEntry, CustomMessageEntry, LabelEntry, + LeafEntry, MessageEntry, ModelChangeEntry, SessionContext, @@ -17,7 +18,6 @@ import type { SessionStats, SessionStorage, SessionTreeEntry, - SessionWriter, ThinkingLevelChangeEntry, } from "../types.ts"; import { SessionError } from "../types.ts"; @@ -148,35 +148,12 @@ export function buildSessionContext( return { ...state, messages }; } -function bindSessionWriter( - storage: SessionStorage, - writer: SessionWriter, -): SessionStorage { - if (writer === storage) return storage; - return new Proxy(storage, { - get(target, property) { - if (property === "appendEntry") return writer.appendEntry.bind(writer); - if (property === "setLeafId") return writer.setLeafId.bind(writer); - const value = Reflect.get(target, property, target) as unknown; - return typeof value === "function" ? value.bind(target) : value; - }, - }); -} - export class Session { private readonly storage: SessionStorage; - private readonly exposedStorage: SessionStorage; - private readonly writer: SessionWriter; private readonly contextBuildOptions: SessionContextBuildOptions; - constructor( - storage: SessionStorage, - contextBuildOptions: SessionContextBuildOptions = {}, - writer: SessionWriter = storage, - ) { + constructor(storage: SessionStorage, contextBuildOptions: SessionContextBuildOptions = {}) { this.storage = storage; - this.exposedStorage = bindSessionWriter(storage, writer); - this.writer = writer; this.contextBuildOptions = contextBuildOptions; } @@ -184,8 +161,9 @@ export class Session { return this.storage.getMetadata(); } + /** Prefer the Session facade methods. Exposed for low-level adapter compatibility. */ getStorage(): SessionStorage { - return this.exposedStorage; + return this.storage; } getLeafId(): Promise { @@ -235,8 +213,16 @@ export class Session { return this.storage.getSessionName(); } + async appendEntry(entry: SessionTreeEntry): Promise { + await this.storage.appendEntry(entry); + } + + setLeafId(leafId: string | null): Promise { + return this.storage.setLeafId(leafId); + } + private async appendTypedEntry(entry: TEntry): Promise { - await this.writer.appendEntry(entry); + await this.appendEntry(entry); return entry.id; } @@ -366,7 +352,7 @@ export class Session { if (entryId !== null && !(await this.storage.getEntry(entryId))) { throw new SessionError("not_found", `Entry ${entryId} not found`); } - await this.writer.setLeafId(entryId); + await this.setLeafId(entryId); if (!summary) return undefined; return this.appendTypedEntry({ type: "branch_summary", diff --git a/packages/agent/src/harness/types.ts b/packages/agent/src/harness/types.ts index 4297cc744..0f59d74bd 100644 --- a/packages/agent/src/harness/types.ts +++ b/packages/agent/src/harness/types.ts @@ -513,9 +513,6 @@ export interface SessionStorage; } -/** The canonical mutation subset that a Session writes through. */ -export type SessionWriter = Pick; - export type { Session } from "./session/session.ts"; export interface SessionCreateOptions { @@ -535,22 +532,16 @@ export interface SessionSearchHit { - metadata: TMetadata; - entry: SessionTreeEntry; -} - +/** Maintains a derived search index. This is intentionally separate from query-only SessionSearch. */ export interface SessionSearchIndex { - upsert(record: SessionSearchRecord): Promise; + upsertEntry(metadata: TMetadata, entry: SessionTreeEntry): Promise; replaceSession(metadata: TMetadata, entries: readonly SessionTreeEntry[]): Promise; - removeSession(metadata: TMetadata): Promise; + deleteSession(metadata: TMetadata): Promise; } -/** Owns session search queries and optionally exposes index maintenance. */ +/** Owns session search queries. Index maintenance is composed at the store/adapter boundary. */ export interface SessionSearch { search(options: SessionSearchOptions): Promise[]>; - index?: SessionSearchIndex; } export interface SessionForkOptions { diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index f7f3c23ac..9811e84d2 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -35,6 +35,7 @@ export * from "./harness/session/memory-repo.ts"; export * from "./harness/session/memory-storage.ts"; export * from "./harness/session/repo-utils.ts"; export * from "./harness/session/search-backend.ts"; +export * from "./harness/session/search-index.ts"; export * from "./harness/session/session.ts"; export * from "./harness/skills.ts"; export * from "./harness/system-prompt.ts"; diff --git a/packages/agent/test/harness/repo.test.ts b/packages/agent/test/harness/repo.test.ts index f7cebbdd0..22ac198e7 100644 --- a/packages/agent/test/harness/repo.test.ts +++ b/packages/agent/test/harness/repo.test.ts @@ -3,25 +3,25 @@ import { describe, expect, it } from "vitest"; import { NodeExecutionEnv } from "../../src/harness/env/nodejs.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 { SessionRepository } from "../../src/harness/session/repo-utils.ts"; import { createAssistantMessage, createTempDir, createUserMessage } from "./session-test-utils.ts"; describe("MemorySessionStore", () => { it("opens, deletes, and forks by metadata", async () => { - const repo = new SessionRepo({ storage: new MemorySessionStore() }); - const session = await repo.storage.create({ id: "session-1" }); + const repo = new SessionRepository({ store: new MemorySessionStore() }); + const session = await repo.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")); - 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" }); + await expect((await repo.open(metadata)).getMetadata()).resolves.toEqual(metadata); + expect((await repo.list()).map((info) => info.id)).toEqual(["session-1"]); + const fork = await repo.fork(metadata, { entryId: user2, id: "session-2" }); expect((await fork.getEntries()).map((entry) => entry.id)).toEqual([user1, assistant1]); - const fullFork = await repo.storage.fork(metadata, { id: "session-3" }); + const fullFork = await repo.fork(metadata, { id: "session-3" }); expect((await fullFork.getEntries()).map((entry) => entry.id)).toEqual([user1, assistant1, user2]); - await repo.storage.delete(metadata); - await expect(repo.storage.open(metadata)).rejects.toThrow("Session not found: session-1"); + await repo.delete(metadata); + await expect(repo.open(metadata)).rejects.toThrow("Session not found: session-1"); }); }); @@ -31,16 +31,16 @@ describe("JsonlSessionStore", () => { const env = new NodeExecutionEnv({ cwd: root }); const cwd = "/tmp/my-project"; const otherCwd = "/tmp/other-project"; - 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 repo = new SessionRepository({ store: new JsonlSessionStore({ 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 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.storage.list({ cwd })).map((sessionMetadata) => sessionMetadata.id)).toEqual([metadata.id]); - expect((await repo.storage.list()).map((sessionMetadata) => sessionMetadata.id).sort()).toEqual( + expect((await repo.list({ cwd })).map((sessionMetadata) => sessionMetadata.id)).toEqual([metadata.id]); + expect((await repo.list()).map((sessionMetadata) => sessionMetadata.id).sort()).toEqual( [metadata.id, otherMetadata.id].sort(), ); }); @@ -48,42 +48,42 @@ describe("JsonlSessionStore", () => { it("opens, deletes, and forks by metadata", async () => { const root = createTempDir(); const env = new NodeExecutionEnv({ cwd: root }); - const repo = new SessionRepo({ storage: new JsonlSessionStore({ fs: env, sessionsRoot: root }) }); - const source = await repo.storage.create({ cwd: "/tmp/source", id: "source-session" }); + const repo = new SessionRepository({ store: new JsonlSessionStore({ fs: env, sessionsRoot: root }) }); + const source = await repo.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.storage.open(sourceMetadata)).getMetadata()).resolves.toEqual(sourceMetadata); - const fork = await repo.storage.fork(sourceMetadata, { cwd: "/tmp/target", id: "fork-session", entryId: user2 }); + await expect((await repo.open(sourceMetadata)).getMetadata()).resolves.toEqual(sourceMetadata); + const fork = await repo.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.storage.fork(sourceMetadata, { cwd: "/tmp/target", id: "full-fork-session" }); + const fullFork = await repo.fork(sourceMetadata, { cwd: "/tmp/target", id: "full-fork-session" }); expect((await fullFork.getEntries()).map((entry) => entry.id)).toEqual([user1, assistant1, user2]); - await repo.storage.delete(sourceMetadata); + await repo.delete(sourceMetadata); expect(existsSync(sourceMetadata.path)).toBe(false); - await expect(repo.storage.open(sourceMetadata)).rejects.toThrow("Session not found"); + await expect(repo.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 SessionRepo({ storage: new JsonlSessionStore({ fs: env, sessionsRoot: root }) }); - const source = await repo.storage.create({ + const repo = new SessionRepository({ store: new JsonlSessionStore({ fs: env, sessionsRoot: root }) }); + const source = await repo.create({ cwd: "/tmp/source", id: "source-session", metadata: { profile: "reviewer" }, }); const sourceMetadata = await source.getMetadata(); expect(sourceMetadata.metadata).toEqual({ profile: "reviewer" }); - expect((await repo.storage.list({ cwd: "/tmp/source" })).map((listed) => listed.metadata)).toEqual([ + expect((await repo.list({ cwd: "/tmp/source" })).map((listed) => listed.metadata)).toEqual([ { profile: "reviewer" }, ]); - const fork = await repo.storage.fork(sourceMetadata, { cwd: "/tmp/target", id: "fork-session" }); + const fork = await repo.fork(sourceMetadata, { cwd: "/tmp/target", id: "fork-session" }); expect((await fork.getMetadata()).metadata).toEqual({ profile: "reviewer" }); - const overridden = await repo.storage.fork(sourceMetadata, { + const overridden = await repo.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 c3bd0ebf9..5cf612eae 100644 --- a/packages/agent/test/harness/sqlite-migrations.test.ts +++ b/packages/agent/test/harness/sqlite-migrations.test.ts @@ -14,7 +14,7 @@ import { 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 { SessionRepository } from "../../src/harness/session/repo-utils.ts"; import { createAssistantMessage, createUserMessage } from "./session-test-utils.ts"; function createTempDir(): string { @@ -70,8 +70,8 @@ describe("SQLite migrations", () => { const databasePath = join(root, "sessions.sqlite"); const env = new NodeExecutionEnv({ cwd: root }); const sqlite = createNodeSqliteFactory(); - const repo = new SessionRepo({ storage: new SqliteSessionStore({ env, sqlite, databasePath }) }); - await repo.storage.create({ cwd: root, id: "session-1" }); + const repo = new SessionRepository({ store: new SqliteSessionStore({ env, sqlite, databasePath }) }); + await repo.create({ cwd: root, id: "session-1" }); const db = await sqlite.open(databasePath); try { @@ -112,23 +112,21 @@ describe("SQLite migrations", () => { const root = createTempDir(); const databasePath = join(root, "sessions.sqlite"); const env = new NodeExecutionEnv({ cwd: root }); - const repo = new SessionRepo({ - storage: new SqliteSessionStore({ env, sqlite: createNodeSqliteFactory(), databasePath }), + const repo = new SessionRepository({ + store: new SqliteSessionStore({ env, sqlite: createNodeSqliteFactory(), databasePath }), }); - const source = await repo.storage.create({ + const source = await repo.create({ cwd: root, id: "session-1", metadata: { profile: "reviewer" }, }); const sourceMetadata = await source.getMetadata(); expect(sourceMetadata.metadata).toEqual({ profile: "reviewer" }); - 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 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 fork.getMetadata()).metadata).toEqual({ profile: "reviewer" }); - const overridden = await repo.storage.fork(sourceMetadata, { + const overridden = await repo.fork(sourceMetadata, { cwd: root, id: "session-3", metadata: { profile: "writer" }, @@ -141,11 +139,11 @@ describe("SQLite migrations", () => { const databasePath = join(root, "sessions.sqlite"); const env = new NodeExecutionEnv({ cwd: root }); const sqlite = createNodeSqliteFactory(); - const repo = new SessionRepo({ storage: new SqliteSessionStore({ env, sqlite, databasePath }) }); - const session = await repo.storage.create({ cwd: root, id: "session-1" }); + const repo = new SessionRepository({ store: new SqliteSessionStore({ env, sqlite, databasePath }) }); + const session = await repo.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); + await session.setLeafId(rootId); const db = await sqlite.open(databasePath); try { @@ -167,7 +165,7 @@ describe("SQLite migrations", () => { await db.close(); } - const reopened = await repo.storage.open(await session.getMetadata()); + const reopened = await repo.open(await session.getMetadata()); expect(await reopened.getLeafId()).toBe(rootId); expect(childId).not.toBe(rootId); }); @@ -177,11 +175,11 @@ describe("SQLite migrations", () => { const databasePath = join(root, "sessions.sqlite"); const env = new NodeExecutionEnv({ cwd: root }); const sqlite = createNodeSqliteFactory(); - const repo = new SessionRepo({ storage: new SqliteSessionStore({ env, sqlite, databasePath }) }); - const session = await repo.storage.create({ cwd: root, id: "session-1" }); + const repo = new SessionRepository({ store: new SqliteSessionStore({ env, sqlite, databasePath }) }); + const session = await repo.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); + await session.setLeafId(rootId); const secondChildId = await session.appendMessage(createAssistantMessage("second child")); const db = await sqlite.open(databasePath); @@ -205,17 +203,17 @@ describe("SQLite migrations", () => { const root = createTempDir(); const databasePath = join(root, "sessions.sqlite"); const env = new NodeExecutionEnv({ cwd: root }); - const repo = new SessionRepo({ - storage: new SqliteSessionStore({ env, sqlite: createNodeSqliteFactory(), databasePath }), + const repo = new SessionRepository({ + store: new SqliteSessionStore({ env, sqlite: createNodeSqliteFactory(), databasePath }), }); - const session = await repo.storage.create({ cwd: root, id: "session-1" }); + const session = await repo.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.setLeafId(rootId); await session.appendMessage(createAssistantMessage("branched child")); - const reopened = await repo.storage.open(await session.getMetadata()); + const reopened = await repo.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({ @@ -227,10 +225,10 @@ describe("SQLite migrations", () => { const root = createTempDir(); const databasePath = join(root, "sessions.sqlite"); const env = new NodeExecutionEnv({ cwd: root }); - const repo = new SessionRepo({ - storage: new SqliteSessionStore({ env, sqlite: createNodeSqliteFactory(), databasePath }), + const repo = new SessionRepository({ + store: new SqliteSessionStore({ env, sqlite: createNodeSqliteFactory(), databasePath }), }); - const session = await repo.storage.create({ cwd: root, id: "session-1" }); + const session = await repo.create({ cwd: root, id: "session-1" }); await session.appendMessage(createUserMessage("one")); await session.appendMessage(createAssistantMessage("two")); await session.appendMessage(createUserMessage("three")); @@ -256,11 +254,11 @@ describe("SQLite migrations", () => { open: async () => db, }; const env = new NodeExecutionEnv({ cwd: root }); - const repo = new SessionRepo({ - storage: new SqliteSessionStore({ env, sqlite, databasePath: join(root, "sessions.sqlite") }), + const repo = new SessionRepository({ + store: new SqliteSessionStore({ env, sqlite, databasePath: join(root, "sessions.sqlite") }), }); - await expect(repo.storage.create({ cwd: root, id: "session-1" })).rejects.toThrow("insert failed"); + await expect(repo.create({ cwd: root, id: "session-1" })).rejects.toThrow("insert failed"); expect(db.closeCount).toBe(1); }); @@ -276,8 +274,8 @@ describe("SQLite migrations", () => { open: async () => db, }; const env = new NodeExecutionEnv({ cwd: root }); - const repo = new SessionRepo({ - storage: new SqliteSessionStore({ env, sqlite, databasePath: join(root, "sessions.sqlite") }), + const repo = new SessionRepository({ + store: new SqliteSessionStore({ env, sqlite, databasePath: join(root, "sessions.sqlite") }), }); const metadata: SqliteSessionMetadata = { id: "missing", @@ -287,7 +285,7 @@ describe("SQLite migrations", () => { }; writeFileSync(metadata.path, ""); - await expect(repo.storage.open(metadata)).rejects.toThrow("Session not found: missing"); + await expect(repo.open(metadata)).rejects.toThrow("Session not found: missing"); expect(db.closeCount).toBe(1); }); @@ -296,7 +294,7 @@ describe("SQLite migrations", () => { const databasePath = join(root, "sessions.sqlite"); const env = new NodeExecutionEnv({ cwd: root }); const storage = new SqliteSessionStore({ env, sqlite: createNodeSqliteFactory(), databasePath }); - const repo = new SessionRepo({ storage }); + const repo = new SessionRepository({ store: storage }); let cleanupCount = 0; const sourceStorage = { async getEntries() { @@ -313,7 +311,7 @@ describe("SQLite migrations", () => { storage.open = async () => sourceStorage as never; try { - await repo.storage.fork( + await repo.fork( { id: "session-1", createdAt: new Date().toISOString(), @@ -369,8 +367,8 @@ describe("SQLite migrations", () => { const databasePath = join(root, "sessions.sqlite"); const env = new NodeExecutionEnv({ cwd: root }); const sqlite = createNodeSqliteFactory(); - const repo = new SessionRepo({ storage: new SqliteSessionStore({ env, sqlite, databasePath }) }); - const session = await repo.storage.create({ cwd: root, id: "session-1" }); + const repo = new SessionRepository({ store: new SqliteSessionStore({ env, sqlite, databasePath }) }); + const session = await repo.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 5e6fd06f2..d46435948 100644 --- a/packages/agent/test/harness/sqlite-node.test.ts +++ b/packages/agent/test/harness/sqlite-node.test.ts @@ -8,15 +8,14 @@ import { } from "../../../storage/sqlite-node/src/index.ts"; import { NodeExecutionEnv } from "../../src/harness/env/nodejs.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 { SessionRepository } from "../../src/harness/session/repo-utils.ts"; +import { IndexedSessionStore } from "../../src/harness/session/search-index.ts"; import type { JsonlSessionMetadata, SessionSearch, SessionSearchHit, SessionSearchIndex, SessionSearchOptions, - SessionSearchRecord, SessionTreeEntry, } from "../../src/harness/types.ts"; import { createTempDir, createUserMessage } from "./session-test-utils.ts"; @@ -25,15 +24,15 @@ 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 SessionRepo({ - storage: new JsonlSessionStore({ fs: env, sessionsRoot: join(root, "sessions") }), + const repo = new SessionRepository({ + store: 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 included = await repo.create({ cwd: root, id: "included" }); + const excluded = await repo.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?.search({ text: "AUTH", cwd: root })).resolves.toEqual([ + await expect(repo.search({ text: "AUTH", cwd: root })).resolves.toEqual([ expect.objectContaining({ entryId, metadata: expect.objectContaining({ id: "included" }) }), ]); }); @@ -45,17 +44,17 @@ describe("SqliteSessionStore with default SQLite FTS5 search", () => { const env = new NodeExecutionEnv({ cwd: root }); const sqlite = createNodeSqliteFactory(); const databasePath = join(root, "sessions.sqlite"); - 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 repo = new SessionRepository({ store: new SqliteSessionStore({ env, sqlite, databasePath }) }); + const included = await repo.create({ cwd: root, id: "included" }); + const excluded = await repo.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?.search({ text: "auth", cwd: root })).resolves.toEqual([ + await expect(repo.search({ text: "auth", cwd: root })).resolves.toEqual([ expect.objectContaining({ entryId, metadata: expect.objectContaining({ id: "included" }) }), ]); - await expect(repo.search?.search({ text: "uth", cwd: root })).resolves.toEqual([ + await expect(repo.search({ text: "uth", cwd: root })).resolves.toEqual([ expect.objectContaining({ entryId, metadata: expect.objectContaining({ id: "included" }) }), ]); @@ -70,8 +69,8 @@ describe("SqliteSessionStore with default SQLite FTS5 search", () => { await db.close(); } - await repo.storage.delete(metadata); - await expect(repo.search?.search({ text: "auth", cwd: root })).resolves.toEqual([]); + await repo.delete(metadata); + await expect(repo.search({ text: "auth", cwd: root })).resolves.toEqual([]); }); }); @@ -82,21 +81,29 @@ describe("JsonlSessionStore with SQLite search index", () => { const root = createTempDir(); const env = new NodeExecutionEnv({ cwd: root }); const sqlite = createNodeSqliteFactory(); - const repo = new SessionRepo({ - storage: new JsonlSessionStore({ fs: env, sessionsRoot: join(root, "sessions") }), - search: new SqliteSessionSearch({ env, sqlite, databasePath: join(root, "search.sqlite") }), + const search = new SqliteSessionSearch({ + env, + sqlite, + databasePath: join(root, "search.sqlite"), }); - const session = await repo.storage.create({ cwd: root, id: "jsonl-session" }); + const repo = new SessionRepository({ + store: new IndexedSessionStore({ + store: new JsonlSessionStore({ fs: env, sessionsRoot: join(root, "sessions") }), + index: search, + defaultSearch: search, + }), + }); + const session = await repo.create({ cwd: root, id: "jsonl-session" }); const entryId = await session.appendMessage(createUserMessage("Find the auth defect")); - await expect(repo.search?.search({ text: "auth", cwd: root })).resolves.toEqual([ + await expect(repo.search({ text: "auth", cwd: root })).resolves.toEqual([ expect.objectContaining({ entryId, metadata: expect.objectContaining({ id: "jsonl-session" }) }), ]); }); }); describe("JsonlSessionStore with multiple search indexes", () => { - it("queries one search implementation and fans index writes out to both", async () => { + it("queries one search implementation and composes index writes explicitly", async () => { const root = createTempDir(); const env = new NodeExecutionEnv({ cwd: root }); const sqlite = createNodeSqliteFactory(); @@ -110,14 +117,31 @@ describe("JsonlSessionStore with multiple search indexes", () => { sqlite, databasePath: join(root, "secondary-search.sqlite"), }); - const repo = new SessionRepo({ - storage: new JsonlSessionStore({ fs: env, sessionsRoot: join(root, "sessions") }), - search: new FanoutSessionSearch({ reader: primary, writers: [primary, secondary] }), + const index: SessionSearchIndex = { + async upsertEntry(metadata, entry) { + await primary.upsertEntry(metadata, entry); + await secondary.upsertEntry(metadata, entry); + }, + async replaceSession(metadata, entries) { + await primary.replaceSession(metadata, entries); + await secondary.replaceSession(metadata, entries); + }, + async deleteSession(metadata) { + await primary.deleteSession(metadata); + await secondary.deleteSession(metadata); + }, + }; + const repo = new SessionRepository({ + store: new IndexedSessionStore({ + store: new JsonlSessionStore({ fs: env, sessionsRoot: join(root, "sessions") }), + index, + defaultSearch: primary, + }), }); - const session = await repo.storage.create({ cwd: root, id: "jsonl-session" }); + const session = await repo.create({ cwd: root, id: "jsonl-session" }); const entryId = await session.appendMessage(createUserMessage("indexed in both places")); - await expect(repo.search?.search({ text: "both" })).resolves.toEqual([ + await expect(repo.search({ text: "both" })).resolves.toEqual([ expect.objectContaining({ entryId, metadata: expect.objectContaining({ id: "jsonl-session" }) }), ]); await expect(secondary.search({ text: "both" })).resolves.toEqual([ @@ -129,39 +153,41 @@ describe("JsonlSessionStore with multiple search indexes", () => { describe("SqliteSessionStore with custom search", () => { it("can swap out the default search implementation", async () => { const root = createTempDir(); - const upserts: SessionSearchRecord[] = []; + const upserts: Array<{ metadata: SqliteSessionMetadata; entry: SessionTreeEntry }> = []; const removals: SqliteSessionMetadata[] = []; const searches: SessionSearchOptions[] = []; const index: SessionSearchIndex = { - async upsert(record) { - upserts.push(record); + async upsertEntry(metadata, entry) { + upserts.push({ metadata, entry }); }, async replaceSession(metadata, entries: readonly SessionTreeEntry[]) { for (const entry of entries) upserts.push({ metadata, entry }); }, - async removeSession(metadata) { + async deleteSession(metadata) { removals.push(metadata); }, }; const search: SessionSearch = { - index, async search(options): Promise[]> { searches.push(options); return []; }, }; - const repo = new SessionRepo({ - storage: new SqliteSessionStore({ - env: new NodeExecutionEnv({ cwd: root }), - sqlite: createNodeSqliteFactory(), - databasePath: join(root, "sessions.sqlite"), + const repo = new SessionRepository({ + store: new IndexedSessionStore({ + store: new SqliteSessionStore({ + env: new NodeExecutionEnv({ cwd: root }), + sqlite: createNodeSqliteFactory(), + databasePath: join(root, "sessions.sqlite"), + }), + index, }), search, }); - const session = await repo.storage.create({ cwd: root, id: "session-1" }); + const session = await repo.create({ cwd: root, id: "session-1" }); const metadata = await session.getMetadata(); const entryId = await session.appendMessage(createUserMessage("indexed remotely")); - await session.getStorage().setLeafId(null); + await session.setLeafId(null); expect(upserts).toContainEqual( expect.objectContaining({ metadata, entry: expect.objectContaining({ id: entryId }) }), @@ -169,9 +195,9 @@ describe("SqliteSessionStore with custom search", () => { expect(upserts).toContainEqual( expect.objectContaining({ metadata, entry: expect.objectContaining({ type: "leaf", targetId: null }) }), ); - await expect(repo.search?.search({ text: "indexed remotely" })).resolves.toEqual([]); + await expect(repo.search({ text: "indexed remotely" })).resolves.toEqual([]); expect(searches).toEqual([{ text: "indexed remotely" }]); - await repo.storage.delete(metadata); + await repo.delete(metadata); expect(removals).toEqual([metadata]); }); }); diff --git a/packages/storage/sqlite-node/src/sqlite/repo.ts b/packages/storage/sqlite-node/src/sqlite/repo.ts index f22f3b68e..da0c2c95b 100644 --- a/packages/storage/sqlite-node/src/sqlite/repo.ts +++ b/packages/storage/sqlite-node/src/sqlite/repo.ts @@ -3,6 +3,7 @@ import { createSessionId, getEntriesToFork, getFileSystemResultOrThrow, + indexSessionStorage, SessionError, } from "@earendil-works/pi-agent-core"; import { applyMigrations } from "./migrations.ts"; @@ -43,6 +44,7 @@ export class SqliteSessionStore implements SqliteSessionStoreApi { private readonly sqlite: SqliteDatabaseFactory; private readonly databasePathInput: string; readonly defaultSearch: SessionSearch; + private readonly searchIndex: SqliteSessionSearch; private databasePath: string | undefined; constructor(options: { @@ -53,11 +55,12 @@ export class SqliteSessionStore implements SqliteSessionStoreApi { this.env = options.env; this.sqlite = options.sqlite; this.databasePathInput = options.databasePath; - this.defaultSearch = new SqliteSessionSearch({ + this.searchIndex = new SqliteSessionSearch({ env: this.env, sqlite: this.sqlite, databasePath: this.databasePathInput, }); + this.defaultSearch = this.searchIndex; } private async getDatabasePath(): Promise { @@ -96,12 +99,13 @@ export class SqliteSessionStore implements SqliteSessionStoreApi { const db = await this.openDatabase(); try { const id = options.id ?? createSessionId(); - return await SqliteSessionStorage.create(db, await this.getDatabasePath(), { + const storage = await SqliteSessionStorage.create(db, await this.getDatabasePath(), { cwd: options.cwd, sessionId: id, parentSessionId: options.parentSessionId, metadata: options.metadata, }); + return indexSessionStorage(storage, this.searchIndex); } catch (error) { await db.close(); throw error; @@ -116,7 +120,7 @@ export class SqliteSessionStore implements SqliteSessionStoreApi { } const db = await this.openDatabase(); try { - return await SqliteSessionStorage.open(db, metadata); + return indexSessionStorage(await SqliteSessionStorage.open(db, metadata), this.searchIndex); } catch (error) { await db.close(); throw error; @@ -157,6 +161,7 @@ export class SqliteSessionStore implements SqliteSessionStoreApi { const result = await db.prepare("DELETE FROM sessions WHERE id = ?").run(metadata.id); if (result.changes === 0) throw new SessionError("not_found", `Session not found: ${metadata.id}`); }); + await this.searchIndex.deleteSession(metadata); } finally { await db.close(); } @@ -183,7 +188,8 @@ export class SqliteSessionStore implements SqliteSessionStoreApi { metadata: options.metadata ?? sourceMetadata.metadata, }); for (const entry of forkedEntries) await storage.appendEntry(entry); - return storage; + await this.searchIndex.replaceSession(await storage.getMetadata(), await storage.getEntries()); + return indexSessionStorage(storage, this.searchIndex); } 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 e0d0bc756..4f82516d8 100644 --- a/packages/storage/sqlite-node/src/sqlite/search-backend.ts +++ b/packages/storage/sqlite-node/src/sqlite/search-backend.ts @@ -4,7 +4,6 @@ import type { SessionSearchHit, SessionSearchIndex, SessionSearchOptions, - SessionSearchRecord, SessionTreeEntry, } from "@earendil-works/pi-agent-core"; import { getFileSystemResultOrThrow } from "@earendil-works/pi-agent-core"; @@ -45,7 +44,6 @@ CREATE VIRTUAL TABLE IF NOT EXISTS session_search_fts USING fts5( export class SqliteSessionSearch implements SessionSearch, SessionSearchIndex { - readonly index: SessionSearchIndex = this; private readonly options: { env: Pick; sqlite: SqliteDatabaseFactory; @@ -89,15 +87,15 @@ export class SqliteSessionSearch): Promise { + async upsertEntry(metadata: TMetadata, entry: SessionTreeEntry): Promise { const db = await this.openDatabase(); try { - const cwd = (record.metadata as { cwd?: unknown }).cwd; - const sessionId = record.metadata.id; - const entryId = record.entry.id; - const timestamp = record.entry.timestamp; - const metadataJson = JSON.stringify(record.metadata); - const searchText = JSON.stringify(record.entry); + const cwd = (metadata as { cwd?: unknown }).cwd; + const sessionId = metadata.id; + const entryId = entry.id; + const timestamp = entry.timestamp; + const metadataJson = JSON.stringify(metadata); + const searchText = JSON.stringify(entry); await db.transaction(async () => { await db .prepare("DELETE FROM session_search_fts WHERE session_id = ? AND entry_id = ?") @@ -114,13 +112,13 @@ export class SqliteSessionSearch { - await this.removeSession(metadata); + await this.deleteSession(metadata); for (const entry of entries) { - await this.upsert({ metadata, entry }); + await this.upsertEntry(metadata, entry); } } - async removeSession(metadata: TMetadata): Promise { + async deleteSession(metadata: TMetadata): Promise { const db = await this.openDatabase(); try { await db.prepare("DELETE FROM session_search_fts WHERE session_id = ?").run(metadata.id); diff --git a/scripts/browser-smoke-entry.ts b/scripts/browser-smoke-entry.ts index c5c7ee247..32176ce75 100644 --- a/scripts/browser-smoke-entry.ts +++ b/scripts/browser-smoke-entry.ts @@ -13,7 +13,7 @@ import { MemorySessionStore, ok, parseCommandArgs, - SessionRepo, + SessionRepository, streamProxy, toError, truncateHead, @@ -27,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 SessionRepo({ storage: new MemorySessionStore() }); +const repo = new SessionRepository({ store: 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]); @@ -40,7 +40,7 @@ console.log( schema.type, typeof stream.push, agent.hasQueuedMessages(), - typeof repo.storage.create, + typeof repo.create, result.value, llmMessages.length, bashExecutionToText({