mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
feat(agent): reject unsupported session search
This commit is contained in:
parent
cbc893b0d4
commit
a4c7cb025d
8 changed files with 33 additions and 25 deletions
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
### Breaking Changes
|
||||
|
||||
- `SessionRepository.search()` now throws a `SessionError` with code `unsupported` when no search implementation is configured instead of returning no hits.
|
||||
- Changed `Session` into the sole opened-session aggregate and replaced `SessionRepo` and concrete per-session persistence classes with a non-owning `SessionRepository`, caller-owned `SessionCollection`, and coherent per-session `SessionStorage`. Create collections with `createInMemorySessionCollection()` or `createJsonlSessionCollection()`, compose them with `createSessionRepository({ collection, search: createScanningSessionSearch(collection) })`, and dispose the collection after draining harness and session work.
|
||||
- `Session` instances are now created by `SessionRepository`; direct construction from an independently supplied collection and snapshot was removed.
|
||||
- Replaced per-session `SessionReader` objects and repository-level append routing with coherent `SessionStorage` implementations that own both reads and writes.
|
||||
|
|
|
|||
|
|
@ -2009,7 +2009,8 @@ const session = await repository.create({});
|
|||
|
||||
Use `createJsonlSessionCollection({ fs, sessionsRoot })` for filesystem persistence;
|
||||
the same scanning search composes with it. Omitting `search` is valid, but
|
||||
`repository.search()` then returns no hits. SQLite support is provided
|
||||
`repository.search()` then throws a `SessionError` with code `unsupported`.
|
||||
SQLite support is provided
|
||||
separately by `createSqliteSessionCollection()` and should use
|
||||
`createSqliteSessionSearch(options)` against the same canonical database. Drain
|
||||
all session or harness operations before the collection leaves
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import {
|
|||
type SessionMetadata,
|
||||
type SessionSearch,
|
||||
type SessionSearchHit,
|
||||
type SessionTreeEntry,
|
||||
} from "../types.ts";
|
||||
import { createSessionForkSelection } from "./fork.ts";
|
||||
import { createSession, type Session, type SessionContextBuildOptions } from "./session.ts";
|
||||
|
|
@ -28,16 +27,16 @@ export class SessionRepository<
|
|||
TListOptions = void,
|
||||
> {
|
||||
private readonly collection: SessionCollection<TMetadata, TCreateOptions, TListOptions>;
|
||||
private readonly sessionSearch: SessionSearch<TMetadata> | null;
|
||||
private readonly sessionSearch: SessionSearch<TMetadata> | undefined;
|
||||
private readonly contextBuildOptions: SessionContextBuildOptions;
|
||||
|
||||
constructor(options: {
|
||||
collection: SessionCollection<TMetadata, TCreateOptions, TListOptions>;
|
||||
search?: SessionSearch<TMetadata> | null;
|
||||
search?: SessionSearch<TMetadata>;
|
||||
contextBuildOptions?: SessionContextBuildOptions;
|
||||
}) {
|
||||
this.collection = options.collection;
|
||||
this.sessionSearch = options.search ?? null;
|
||||
this.sessionSearch = options.search;
|
||||
this.contextBuildOptions = options.contextBuildOptions ?? {};
|
||||
}
|
||||
|
||||
|
|
@ -66,7 +65,8 @@ export class SessionRepository<
|
|||
}
|
||||
|
||||
async search(options: Parameters<SessionSearch<TMetadata>["search"]>[0]): Promise<SessionSearchHit<TMetadata>[]> {
|
||||
return this.sessionSearch ? await this.sessionSearch.search(options) : [];
|
||||
if (!this.sessionSearch) throw new SessionError("unsupported", "Session search is not configured");
|
||||
return await this.sessionSearch.search(options);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -76,26 +76,12 @@ export function createSessionRepository<
|
|||
TListOptions = void,
|
||||
>(options: {
|
||||
collection: SessionCollection<TMetadata, TCreateOptions, TListOptions>;
|
||||
search?: SessionSearch<TMetadata> | null;
|
||||
search?: SessionSearch<TMetadata>;
|
||||
contextBuildOptions?: SessionContextBuildOptions;
|
||||
}): SessionRepository<TMetadata, TCreateOptions, TListOptions> {
|
||||
return new SessionRepository(options);
|
||||
}
|
||||
|
||||
export function findSessionEntryMatches<TMetadata extends SessionMetadata>(
|
||||
metadata: TMetadata,
|
||||
entries: readonly SessionTreeEntry[],
|
||||
text: string,
|
||||
): SessionSearchHit<TMetadata>[] {
|
||||
const normalizedText = text.trim().toLowerCase();
|
||||
if (!normalizedText) return [];
|
||||
return entries.flatMap((entry) => {
|
||||
const payload = JSON.stringify(entry);
|
||||
if (!payload.toLowerCase().includes(normalizedText)) return [];
|
||||
return [{ metadata, entryId: entry.id, timestamp: entry.timestamp, snippet: payload }];
|
||||
});
|
||||
}
|
||||
|
||||
export function getFileSystemResultOrThrow<TValue>(result: Result<TValue, FileError>, message: string): TValue {
|
||||
if (!result.ok) {
|
||||
const code = result.error.code === "not_found" ? "not_found" : "storage";
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import type {
|
|||
SessionSearchOptions,
|
||||
SessionStorage,
|
||||
} from "../types.ts";
|
||||
import { findSessionEntryMatches } from "./repository.ts";
|
||||
|
||||
type SessionSearchSource<TMetadata extends SessionMetadata> = {
|
||||
open(metadata: TMetadata): Promise<SessionStorage<TMetadata>>;
|
||||
|
|
@ -21,12 +20,23 @@ class ScanningSessionSearch<TMetadata extends SessionMetadata = SessionMetadata>
|
|||
}
|
||||
|
||||
async search(options: SessionSearchOptions): Promise<SessionSearchHit<TMetadata>[]> {
|
||||
const normalizedText = options.text.trim().toLowerCase();
|
||||
if (!normalizedText) return [];
|
||||
const hits: SessionSearchHit<TMetadata>[] = [];
|
||||
for (const metadata of await this.source.list()) {
|
||||
const cwd = (metadata as { cwd?: unknown }).cwd;
|
||||
if (options.cwd !== undefined && cwd !== options.cwd) continue;
|
||||
const storage = await this.source.open(metadata);
|
||||
hits.push(...findSessionEntryMatches(storage.metadata, await storage.readEntries(), options.text));
|
||||
for (const entry of await storage.readEntries()) {
|
||||
const payload = JSON.stringify(entry);
|
||||
if (!payload.toLowerCase().includes(normalizedText)) continue;
|
||||
hits.push({
|
||||
metadata: storage.metadata,
|
||||
entryId: entry.id,
|
||||
timestamp: entry.timestamp,
|
||||
snippet: payload,
|
||||
});
|
||||
}
|
||||
}
|
||||
return hits;
|
||||
}
|
||||
|
|
@ -225,6 +225,7 @@ export class BranchSummaryError extends Error {
|
|||
|
||||
export type SessionErrorCode =
|
||||
| "not_found"
|
||||
| "unsupported"
|
||||
| "invalid_session"
|
||||
| "invalid_entry"
|
||||
| "invalid_fork_target"
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export {
|
|||
type InMemorySessionCreateOptions,
|
||||
} from "./harness/session/memory-collection.ts";
|
||||
export * from "./harness/session/repository.ts";
|
||||
export * from "./harness/session/search-backend.ts";
|
||||
export * from "./harness/session/scanning-session-search.ts";
|
||||
export {
|
||||
buildContextEntries,
|
||||
buildSessionContext,
|
||||
|
|
|
|||
|
|
@ -200,6 +200,15 @@ function createCountingInMemorySessionCollection(): {
|
|||
}
|
||||
|
||||
describe("InMemorySessionCollection", () => {
|
||||
it("rejects search when no search implementation is configured", async () => {
|
||||
const repo = createSessionRepository({ collection: createInMemorySessionCollection() });
|
||||
|
||||
await expect(repo.search({ text: "needle" })).rejects.toMatchObject({
|
||||
code: "unsupported",
|
||||
message: "Session search is not configured",
|
||||
});
|
||||
});
|
||||
|
||||
it("opens, deletes, and forks by metadata", async () => {
|
||||
const repo = createSessionRepository({ collection: createInMemorySessionCollection() });
|
||||
const session = await repo.create({ id: "session-1" });
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
import { NodeExecutionEnv } from "../../src/harness/env/nodejs.ts";
|
||||
import { createJsonlSessionCollection } from "../../src/harness/session/jsonl-collection.ts";
|
||||
import { createSessionRepository } from "../../src/harness/session/repository.ts";
|
||||
import { createScanningSessionSearch } from "../../src/harness/session/search-backend.ts";
|
||||
import { createScanningSessionSearch } from "../../src/harness/session/scanning-session-search.ts";
|
||||
import type { SessionSearch, SessionSearchHit, SessionSearchOptions } from "../../src/harness/types.ts";
|
||||
import { createTempDir, createUserMessage } from "./session-test-utils.ts";
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue