mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-21 01:01:46 +00:00
Fix SDK typecheck errors and update persist drivers for insertEvent signature
- Fix insertEvent call in client.ts to pass sessionId as first argument - Update Daytona provider create options to use Partial type (image has default) - Update StrictUniqueSessionPersistDriver in tests to match new insertEvent signature - Sync persist packages, openapi spec, and docs with upstream changes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6a42f06342
commit
441083ea2a
33 changed files with 1051 additions and 2121 deletions
5
sdks/persist-indexeddb/README.md
Normal file
5
sdks/persist-indexeddb/README.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# @sandbox-agent/persist-indexeddb
|
||||
|
||||
> **Deprecated:** This package has been deprecated and removed.
|
||||
|
||||
Copy the driver source directly into your project. See the [session persistence docs](https://sandboxagent.dev/session-persistence) for guidance.
|
||||
|
|
@ -16,23 +16,16 @@
|
|||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"sandbox-agent": "workspace:*"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"fake-indexeddb": "^6.2.4",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.7.0",
|
||||
"vitest": "^3.0.0"
|
||||
"typescript": "^5.7.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,314 +1,5 @@
|
|||
import type { ListEventsRequest, ListPage, ListPageRequest, SessionEvent, SessionPersistDriver, SessionRecord } from "sandbox-agent";
|
||||
|
||||
const DEFAULT_DB_NAME = "sandbox-agent-session-store";
|
||||
const DEFAULT_DB_VERSION = 2;
|
||||
const SESSIONS_STORE = "sessions";
|
||||
const EVENTS_STORE = "events";
|
||||
const EVENTS_BY_SESSION_INDEX = "by_session_index";
|
||||
const DEFAULT_LIST_LIMIT = 100;
|
||||
|
||||
export interface IndexedDbSessionPersistDriverOptions {
|
||||
databaseName?: string;
|
||||
databaseVersion?: number;
|
||||
indexedDb?: IDBFactory;
|
||||
}
|
||||
|
||||
export class IndexedDbSessionPersistDriver implements SessionPersistDriver {
|
||||
private readonly indexedDb: IDBFactory;
|
||||
private readonly dbName: string;
|
||||
private readonly dbVersion: number;
|
||||
private readonly dbPromise: Promise<IDBDatabase>;
|
||||
|
||||
constructor(options: IndexedDbSessionPersistDriverOptions = {}) {
|
||||
const indexedDb = options.indexedDb ?? globalThis.indexedDB;
|
||||
if (!indexedDb) {
|
||||
throw new Error("IndexedDB is not available in this runtime.");
|
||||
}
|
||||
|
||||
this.indexedDb = indexedDb;
|
||||
this.dbName = options.databaseName ?? DEFAULT_DB_NAME;
|
||||
this.dbVersion = options.databaseVersion ?? DEFAULT_DB_VERSION;
|
||||
this.dbPromise = this.openDatabase();
|
||||
}
|
||||
|
||||
async getSession(id: string): Promise<SessionRecord | undefined> {
|
||||
const db = await this.dbPromise;
|
||||
const row = await requestToPromise<IDBValidKey | SessionRow | undefined>(db.transaction(SESSIONS_STORE, "readonly").objectStore(SESSIONS_STORE).get(id));
|
||||
if (!row || typeof row !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
return decodeSessionRow(row as SessionRow);
|
||||
}
|
||||
|
||||
async listSessions(request: ListPageRequest = {}): Promise<ListPage<SessionRecord>> {
|
||||
const db = await this.dbPromise;
|
||||
const rows = await getAllRows<SessionRow>(db, SESSIONS_STORE);
|
||||
|
||||
rows.sort((a, b) => {
|
||||
if (a.createdAt !== b.createdAt) {
|
||||
return a.createdAt - b.createdAt;
|
||||
}
|
||||
return a.id.localeCompare(b.id);
|
||||
});
|
||||
|
||||
const offset = parseCursor(request.cursor);
|
||||
const limit = normalizeLimit(request.limit);
|
||||
const slice = rows.slice(offset, offset + limit).map(decodeSessionRow);
|
||||
const nextOffset = offset + slice.length;
|
||||
|
||||
return {
|
||||
items: slice,
|
||||
nextCursor: nextOffset < rows.length ? String(nextOffset) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async updateSession(session: SessionRecord): Promise<void> {
|
||||
const db = await this.dbPromise;
|
||||
await transactionPromise(db, [SESSIONS_STORE], "readwrite", (tx) => {
|
||||
tx.objectStore(SESSIONS_STORE).put(encodeSessionRow(session));
|
||||
});
|
||||
}
|
||||
|
||||
async listEvents(request: ListEventsRequest): Promise<ListPage<SessionEvent>> {
|
||||
const db = await this.dbPromise;
|
||||
const rows = (await getAllRows<EventRow>(db, EVENTS_STORE)).filter((row) => row.sessionId === request.sessionId).sort(compareEventRowsByOrder);
|
||||
|
||||
const offset = parseCursor(request.cursor);
|
||||
const limit = normalizeLimit(request.limit);
|
||||
const slice = rows.slice(offset, offset + limit).map(decodeEventRow);
|
||||
const nextOffset = offset + slice.length;
|
||||
|
||||
return {
|
||||
items: slice,
|
||||
nextCursor: nextOffset < rows.length ? String(nextOffset) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async insertEvent(_sessionId: string, event: SessionEvent): Promise<void> {
|
||||
const db = await this.dbPromise;
|
||||
await transactionPromise(db, [EVENTS_STORE], "readwrite", (tx) => {
|
||||
tx.objectStore(EVENTS_STORE).put(encodeEventRow(event));
|
||||
});
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
const db = await this.dbPromise;
|
||||
db.close();
|
||||
}
|
||||
|
||||
private openDatabase(): Promise<IDBDatabase> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = this.indexedDb.open(this.dbName, this.dbVersion);
|
||||
|
||||
request.onupgradeneeded = () => {
|
||||
const db = request.result;
|
||||
|
||||
if (!db.objectStoreNames.contains(SESSIONS_STORE)) {
|
||||
db.createObjectStore(SESSIONS_STORE, { keyPath: "id" });
|
||||
}
|
||||
|
||||
if (!db.objectStoreNames.contains(EVENTS_STORE)) {
|
||||
const events = db.createObjectStore(EVENTS_STORE, { keyPath: "id" });
|
||||
events.createIndex(EVENTS_BY_SESSION_INDEX, ["sessionId", "eventIndex", "id"], {
|
||||
unique: false,
|
||||
});
|
||||
} else {
|
||||
const tx = request.transaction;
|
||||
if (!tx) {
|
||||
return;
|
||||
}
|
||||
const events = tx.objectStore(EVENTS_STORE);
|
||||
if (!events.indexNames.contains(EVENTS_BY_SESSION_INDEX)) {
|
||||
events.createIndex(EVENTS_BY_SESSION_INDEX, ["sessionId", "eventIndex", "id"], {
|
||||
unique: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error ?? new Error("Unable to open IndexedDB"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
type SessionRow = {
|
||||
id: string;
|
||||
agent: string;
|
||||
agentSessionId: string;
|
||||
lastConnectionId: string;
|
||||
createdAt: number;
|
||||
destroyedAt?: number;
|
||||
sandboxId?: string;
|
||||
sessionInit?: SessionRecord["sessionInit"];
|
||||
};
|
||||
|
||||
type EventRow = {
|
||||
id: number | string;
|
||||
eventIndex?: number;
|
||||
sessionId: string;
|
||||
createdAt: number;
|
||||
connectionId: string;
|
||||
sender: "client" | "agent";
|
||||
payload: unknown;
|
||||
};
|
||||
|
||||
function encodeSessionRow(session: SessionRecord): SessionRow {
|
||||
return {
|
||||
id: session.id,
|
||||
agent: session.agent,
|
||||
agentSessionId: session.agentSessionId,
|
||||
lastConnectionId: session.lastConnectionId,
|
||||
createdAt: session.createdAt,
|
||||
destroyedAt: session.destroyedAt,
|
||||
sandboxId: session.sandboxId,
|
||||
sessionInit: session.sessionInit,
|
||||
};
|
||||
}
|
||||
|
||||
function decodeSessionRow(row: SessionRow): SessionRecord {
|
||||
return {
|
||||
id: row.id,
|
||||
agent: row.agent,
|
||||
agentSessionId: row.agentSessionId,
|
||||
lastConnectionId: row.lastConnectionId,
|
||||
createdAt: row.createdAt,
|
||||
destroyedAt: row.destroyedAt,
|
||||
sandboxId: row.sandboxId,
|
||||
sessionInit: row.sessionInit,
|
||||
};
|
||||
}
|
||||
|
||||
function encodeEventRow(event: SessionEvent): EventRow {
|
||||
return {
|
||||
id: event.id,
|
||||
eventIndex: event.eventIndex,
|
||||
sessionId: event.sessionId,
|
||||
createdAt: event.createdAt,
|
||||
connectionId: event.connectionId,
|
||||
sender: event.sender,
|
||||
payload: event.payload,
|
||||
};
|
||||
}
|
||||
|
||||
function decodeEventRow(row: EventRow): SessionEvent {
|
||||
return {
|
||||
id: String(row.id),
|
||||
eventIndex: parseEventIndex(row.eventIndex, row.id),
|
||||
sessionId: row.sessionId,
|
||||
createdAt: row.createdAt,
|
||||
connectionId: row.connectionId,
|
||||
sender: row.sender,
|
||||
payload: row.payload as SessionEvent["payload"],
|
||||
};
|
||||
}
|
||||
|
||||
async function getAllRows<T>(db: IDBDatabase, storeName: string): Promise<T[]> {
|
||||
return await transactionPromise<T[]>(db, [storeName], "readonly", async (tx) => {
|
||||
const request = tx.objectStore(storeName).getAll();
|
||||
return (await requestToPromise(request)) as T[];
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeLimit(limit: number | undefined): number {
|
||||
if (!Number.isFinite(limit) || (limit ?? 0) < 1) {
|
||||
return DEFAULT_LIST_LIMIT;
|
||||
}
|
||||
return Math.floor(limit as number);
|
||||
}
|
||||
|
||||
function parseCursor(cursor: string | undefined): number {
|
||||
if (!cursor) {
|
||||
return 0;
|
||||
}
|
||||
const parsed = Number.parseInt(cursor, 10);
|
||||
if (!Number.isFinite(parsed) || parsed < 0) {
|
||||
return 0;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function compareEventRowsByOrder(a: EventRow, b: EventRow): number {
|
||||
const indexA = parseEventIndex(a.eventIndex, a.id);
|
||||
const indexB = parseEventIndex(b.eventIndex, b.id);
|
||||
if (indexA !== indexB) {
|
||||
return indexA - indexB;
|
||||
}
|
||||
return String(a.id).localeCompare(String(b.id));
|
||||
}
|
||||
|
||||
function parseEventIndex(value: number | undefined, fallback: number | string): number {
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
return Math.max(0, Math.floor(value));
|
||||
}
|
||||
|
||||
const parsed = Number.parseInt(String(fallback), 10);
|
||||
if (!Number.isFinite(parsed) || parsed < 0) {
|
||||
return 0;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function requestToPromise<T>(request: IDBRequest<T>): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error ?? new Error("IndexedDB request failed"));
|
||||
});
|
||||
}
|
||||
|
||||
function transactionPromise<T>(db: IDBDatabase, stores: string[], mode: IDBTransactionMode, run: (tx: IDBTransaction) => T | Promise<T>): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(stores, mode);
|
||||
let settled = false;
|
||||
let resultValue: T | undefined;
|
||||
let runCompleted = false;
|
||||
let txCompleted = false;
|
||||
|
||||
function tryResolve() {
|
||||
if (settled || !runCompleted || !txCompleted) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
resolve(resultValue as T);
|
||||
}
|
||||
|
||||
tx.oncomplete = () => {
|
||||
txCompleted = true;
|
||||
tryResolve();
|
||||
};
|
||||
|
||||
tx.onerror = () => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
reject(tx.error ?? new Error("IndexedDB transaction failed"));
|
||||
};
|
||||
|
||||
tx.onabort = () => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
reject(tx.error ?? new Error("IndexedDB transaction aborted"));
|
||||
};
|
||||
|
||||
Promise.resolve(run(tx))
|
||||
.then((value) => {
|
||||
resultValue = value;
|
||||
runCompleted = true;
|
||||
tryResolve();
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
reject(error);
|
||||
}
|
||||
try {
|
||||
tx.abort();
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
throw new Error(
|
||||
"@sandbox-agent/persist-indexeddb has been deprecated and removed. " +
|
||||
"Copy the reference implementation into your project instead. " +
|
||||
"See https://github.com/nichochar/sandbox-agent/tree/main/examples/persist-indexeddb",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,96 +0,0 @@
|
|||
import "fake-indexeddb/auto";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { IndexedDbSessionPersistDriver } from "../src/index.ts";
|
||||
|
||||
function uniqueDbName(prefix: string): string {
|
||||
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
||||
}
|
||||
|
||||
describe("IndexedDbSessionPersistDriver", () => {
|
||||
it("stores and pages sessions and events", async () => {
|
||||
const dbName = uniqueDbName("indexeddb-driver");
|
||||
const driver = new IndexedDbSessionPersistDriver({ databaseName: dbName });
|
||||
|
||||
await driver.updateSession({
|
||||
id: "s-1",
|
||||
agent: "mock",
|
||||
agentSessionId: "a-1",
|
||||
lastConnectionId: "c-1",
|
||||
createdAt: 100,
|
||||
});
|
||||
|
||||
await driver.updateSession({
|
||||
id: "s-2",
|
||||
agent: "mock",
|
||||
agentSessionId: "a-2",
|
||||
lastConnectionId: "c-2",
|
||||
createdAt: 200,
|
||||
destroyedAt: 300,
|
||||
});
|
||||
|
||||
await driver.insertEvent("s-1", {
|
||||
id: "evt-1",
|
||||
eventIndex: 1,
|
||||
sessionId: "s-1",
|
||||
createdAt: 1,
|
||||
connectionId: "c-1",
|
||||
sender: "client",
|
||||
payload: { jsonrpc: "2.0", method: "session/prompt", params: { sessionId: "a-1" } },
|
||||
});
|
||||
|
||||
await driver.insertEvent("s-1", {
|
||||
id: "evt-2",
|
||||
eventIndex: 2,
|
||||
sessionId: "s-1",
|
||||
createdAt: 2,
|
||||
connectionId: "c-1",
|
||||
sender: "agent",
|
||||
payload: { jsonrpc: "2.0", method: "session/update", params: { sessionId: "a-1" } },
|
||||
});
|
||||
|
||||
const loaded = await driver.getSession("s-2");
|
||||
expect(loaded?.destroyedAt).toBe(300);
|
||||
|
||||
const page1 = await driver.listSessions({ limit: 1 });
|
||||
expect(page1.items).toHaveLength(1);
|
||||
expect(page1.items[0]?.id).toBe("s-1");
|
||||
expect(page1.nextCursor).toBeTruthy();
|
||||
|
||||
const page2 = await driver.listSessions({ cursor: page1.nextCursor, limit: 1 });
|
||||
expect(page2.items).toHaveLength(1);
|
||||
expect(page2.items[0]?.id).toBe("s-2");
|
||||
expect(page2.nextCursor).toBeUndefined();
|
||||
|
||||
const eventsPage = await driver.listEvents({ sessionId: "s-1", limit: 10 });
|
||||
expect(eventsPage.items).toHaveLength(2);
|
||||
expect(eventsPage.items[0]?.id).toBe("evt-1");
|
||||
expect(eventsPage.items[0]?.eventIndex).toBe(1);
|
||||
expect(eventsPage.items[1]?.id).toBe("evt-2");
|
||||
expect(eventsPage.items[1]?.eventIndex).toBe(2);
|
||||
|
||||
await driver.close();
|
||||
});
|
||||
|
||||
it("persists across driver instances for same database", async () => {
|
||||
const dbName = uniqueDbName("indexeddb-reopen");
|
||||
|
||||
{
|
||||
const driver = new IndexedDbSessionPersistDriver({ databaseName: dbName });
|
||||
await driver.updateSession({
|
||||
id: "s-1",
|
||||
agent: "mock",
|
||||
agentSessionId: "a-1",
|
||||
lastConnectionId: "c-1",
|
||||
createdAt: 1,
|
||||
});
|
||||
await driver.close();
|
||||
}
|
||||
|
||||
{
|
||||
const driver = new IndexedDbSessionPersistDriver({ databaseName: dbName });
|
||||
const session = await driver.getSession("s-1");
|
||||
expect(session?.id).toBe("s-1");
|
||||
await driver.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
import "fake-indexeddb/auto";
|
||||
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
||||
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { tmpdir } from "node:os";
|
||||
import { SandboxAgent } from "sandbox-agent";
|
||||
import { spawnSandboxAgent, type SandboxAgentSpawnHandle } from "../../typescript/src/spawn.ts";
|
||||
import { prepareMockAgentDataHome } from "../../typescript/tests/helpers/mock-agent.ts";
|
||||
import { IndexedDbSessionPersistDriver } from "../src/index.ts";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
function findBinary(): string | null {
|
||||
if (process.env.SANDBOX_AGENT_BIN) {
|
||||
return process.env.SANDBOX_AGENT_BIN;
|
||||
}
|
||||
|
||||
const cargoPaths = [resolve(__dirname, "../../../target/debug/sandbox-agent"), resolve(__dirname, "../../../target/release/sandbox-agent")];
|
||||
|
||||
for (const p of cargoPaths) {
|
||||
if (existsSync(p)) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function uniqueDbName(prefix: string): string {
|
||||
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
||||
}
|
||||
|
||||
const BINARY_PATH = findBinary();
|
||||
if (!BINARY_PATH) {
|
||||
throw new Error("sandbox-agent binary not found. Build it (cargo build -p sandbox-agent) or set SANDBOX_AGENT_BIN.");
|
||||
}
|
||||
if (!process.env.SANDBOX_AGENT_BIN) {
|
||||
process.env.SANDBOX_AGENT_BIN = BINARY_PATH;
|
||||
}
|
||||
|
||||
describe("IndexedDB persistence end-to-end", () => {
|
||||
let handle: SandboxAgentSpawnHandle;
|
||||
let baseUrl: string;
|
||||
let token: string;
|
||||
let dataHome: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
dataHome = mkdtempSync(join(tmpdir(), "indexeddb-integration-"));
|
||||
prepareMockAgentDataHome(dataHome);
|
||||
|
||||
handle = await spawnSandboxAgent({
|
||||
enabled: true,
|
||||
log: "silent",
|
||||
timeoutMs: 30000,
|
||||
env: {
|
||||
XDG_DATA_HOME: dataHome,
|
||||
HOME: dataHome,
|
||||
USERPROFILE: dataHome,
|
||||
APPDATA: join(dataHome, "AppData", "Roaming"),
|
||||
LOCALAPPDATA: join(dataHome, "AppData", "Local"),
|
||||
},
|
||||
});
|
||||
baseUrl = handle.baseUrl;
|
||||
token = handle.token;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await handle.dispose();
|
||||
rmSync(dataHome, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("restores sessions/events across sdk instances", async () => {
|
||||
const dbName = uniqueDbName("sandbox-agent-browser-e2e");
|
||||
|
||||
const persist1 = new IndexedDbSessionPersistDriver({ databaseName: dbName });
|
||||
const sdk1 = await SandboxAgent.connect({
|
||||
baseUrl,
|
||||
token,
|
||||
persist: persist1,
|
||||
replayMaxEvents: 40,
|
||||
replayMaxChars: 16000,
|
||||
});
|
||||
|
||||
const created = await sdk1.createSession({ agent: "mock" });
|
||||
await created.prompt([{ type: "text", text: "indexeddb-first" }]);
|
||||
const firstConnectionId = created.lastConnectionId;
|
||||
|
||||
await sdk1.dispose();
|
||||
await persist1.close();
|
||||
|
||||
const persist2 = new IndexedDbSessionPersistDriver({ databaseName: dbName });
|
||||
const sdk2 = await SandboxAgent.connect({
|
||||
baseUrl,
|
||||
token,
|
||||
persist: persist2,
|
||||
replayMaxEvents: 40,
|
||||
replayMaxChars: 16000,
|
||||
});
|
||||
|
||||
const restored = await sdk2.resumeSession(created.id);
|
||||
expect(restored.lastConnectionId).not.toBe(firstConnectionId);
|
||||
|
||||
await restored.prompt([{ type: "text", text: "indexeddb-second" }]);
|
||||
|
||||
const sessions = await sdk2.listSessions({ limit: 20 });
|
||||
expect(sessions.items.some((entry) => entry.id === created.id)).toBe(true);
|
||||
|
||||
const events = await sdk2.getEvents({ sessionId: created.id, limit: 1000 });
|
||||
expect(events.items.length).toBeGreaterThan(0);
|
||||
|
||||
const replayInjected = events.items.find((event) => {
|
||||
if (event.sender !== "client") {
|
||||
return false;
|
||||
}
|
||||
const payload = event.payload as Record<string, unknown>;
|
||||
const method = payload.method;
|
||||
const params = payload.params as Record<string, unknown> | undefined;
|
||||
const prompt = Array.isArray(params?.prompt) ? params?.prompt : [];
|
||||
const firstBlock = prompt[0] as Record<string, unknown> | undefined;
|
||||
return method === "session/prompt" && typeof firstBlock?.text === "string" && firstBlock.text.includes("Previous session history is replayed below");
|
||||
});
|
||||
|
||||
expect(replayInjected).toBeTruthy();
|
||||
|
||||
await sdk2.dispose();
|
||||
await persist2.close();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue