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:
Nathan Flurry 2026-03-15 13:17:10 -07:00
parent 6a42f06342
commit 441083ea2a
33 changed files with 1051 additions and 2121 deletions

View file

@ -0,0 +1,5 @@
# @sandbox-agent/persist-rivet
> **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.

View file

@ -16,30 +16,16 @@
"import": "./dist/index.js"
}
},
"dependencies": {
"sandbox-agent": "workspace:*"
},
"peerDependencies": {
"rivetkit": ">=0.5.0"
},
"peerDependenciesMeta": {
"rivetkit": {
"optional": true
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest"
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@types/node": "^22.0.0",
"tsup": "^8.0.0",
"typescript": "^5.7.0",
"vitest": "^3.0.0"
"typescript": "^5.7.0"
}
}

View file

@ -1,168 +1,5 @@
import type { ListEventsRequest, ListPage, ListPageRequest, SessionEvent, SessionPersistDriver, SessionRecord } from "sandbox-agent";
/** Structural type compatible with rivetkit's ActorContext without importing it. */
export interface ActorContextLike {
state: Record<string, unknown>;
}
export interface RivetPersistData {
sessions: Record<string, SessionRecord>;
events: Record<string, SessionEvent[]>;
}
export type RivetPersistState = {
_sandboxAgentPersist: RivetPersistData;
};
export interface RivetSessionPersistDriverOptions {
/** Maximum number of sessions to retain. Oldest are evicted first. Default: 1024. */
maxSessions?: number;
/** Maximum events per session. Oldest are trimmed first. Default: 500. */
maxEventsPerSession?: number;
/** Key on `c.state` where persist data is stored. Default: `"_sandboxAgentPersist"`. */
stateKey?: string;
}
const DEFAULT_MAX_SESSIONS = 1024;
const DEFAULT_MAX_EVENTS_PER_SESSION = 500;
const DEFAULT_LIST_LIMIT = 100;
const DEFAULT_STATE_KEY = "_sandboxAgentPersist";
export class RivetSessionPersistDriver implements SessionPersistDriver {
private readonly maxSessions: number;
private readonly maxEventsPerSession: number;
private readonly stateKey: string;
private readonly ctx: ActorContextLike;
constructor(ctx: ActorContextLike, options: RivetSessionPersistDriverOptions = {}) {
this.ctx = ctx;
this.maxSessions = normalizeCap(options.maxSessions, DEFAULT_MAX_SESSIONS);
this.maxEventsPerSession = normalizeCap(options.maxEventsPerSession, DEFAULT_MAX_EVENTS_PER_SESSION);
this.stateKey = options.stateKey ?? DEFAULT_STATE_KEY;
// Auto-initialize if absent; preserve existing data on actor wake.
if (!this.ctx.state[this.stateKey]) {
this.ctx.state[this.stateKey] = { sessions: {}, events: {} } satisfies RivetPersistData;
}
}
private get data(): RivetPersistData {
return this.ctx.state[this.stateKey] as RivetPersistData;
}
async getSession(id: string): Promise<SessionRecord | undefined> {
const session = this.data.sessions[id];
return session ? cloneSessionRecord(session) : undefined;
}
async listSessions(request: ListPageRequest = {}): Promise<ListPage<SessionRecord>> {
const sorted = Object.values(this.data.sessions).sort((a, b) => {
if (a.createdAt !== b.createdAt) {
return a.createdAt - b.createdAt;
}
return a.id.localeCompare(b.id);
});
const page = paginate(sorted, request);
return {
items: page.items.map(cloneSessionRecord),
nextCursor: page.nextCursor,
};
}
async updateSession(session: SessionRecord): Promise<void> {
this.data.sessions[session.id] = { ...session };
if (!this.data.events[session.id]) {
this.data.events[session.id] = [];
}
const ids = Object.keys(this.data.sessions);
if (ids.length <= this.maxSessions) {
return;
}
const overflow = ids.length - this.maxSessions;
const removable = Object.values(this.data.sessions)
.sort((a, b) => {
if (a.createdAt !== b.createdAt) {
return a.createdAt - b.createdAt;
}
return a.id.localeCompare(b.id);
})
.slice(0, overflow)
.map((s) => s.id);
for (const sessionId of removable) {
delete this.data.sessions[sessionId];
delete this.data.events[sessionId];
}
}
async listEvents(request: ListEventsRequest): Promise<ListPage<SessionEvent>> {
const all = [...(this.data.events[request.sessionId] ?? [])].sort((a, b) => {
if (a.eventIndex !== b.eventIndex) {
return a.eventIndex - b.eventIndex;
}
return a.id.localeCompare(b.id);
});
const page = paginate(all, request);
return {
items: page.items.map(cloneSessionEvent),
nextCursor: page.nextCursor,
};
}
async insertEvent(sessionId: string, event: SessionEvent): Promise<void> {
const events = this.data.events[sessionId] ?? [];
events.push(cloneSessionEvent(event));
if (events.length > this.maxEventsPerSession) {
events.splice(0, events.length - this.maxEventsPerSession);
}
this.data.events[sessionId] = events;
}
}
function cloneSessionRecord(session: SessionRecord): SessionRecord {
return {
...session,
sessionInit: session.sessionInit ? (JSON.parse(JSON.stringify(session.sessionInit)) as SessionRecord["sessionInit"]) : undefined,
};
}
function cloneSessionEvent(event: SessionEvent): SessionEvent {
return {
...event,
payload: JSON.parse(JSON.stringify(event.payload)) as SessionEvent["payload"],
};
}
function normalizeCap(value: number | undefined, fallback: number): number {
if (!Number.isFinite(value) || (value ?? 0) < 1) {
return fallback;
}
return Math.floor(value as number);
}
function paginate<T>(items: T[], request: ListPageRequest): ListPage<T> {
const offset = parseCursor(request.cursor);
const limit = normalizeCap(request.limit, DEFAULT_LIST_LIMIT);
const slice = items.slice(offset, offset + limit);
const nextOffset = offset + slice.length;
return {
items: slice,
nextCursor: nextOffset < items.length ? String(nextOffset) : undefined,
};
}
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;
}
throw new Error(
"@sandbox-agent/persist-rivet has been deprecated and removed. " +
"Copy the reference implementation into your project instead. " +
"See https://github.com/nichochar/sandbox-agent/tree/main/examples/persist-rivet",
);

View file

@ -1,236 +0,0 @@
import { describe, it, expect } from "vitest";
import { RivetSessionPersistDriver } from "../src/index.ts";
import type { RivetPersistData } from "../src/index.ts";
function makeCtx() {
return { state: {} as Record<string, unknown> };
}
describe("RivetSessionPersistDriver", () => {
it("auto-initializes state on construction", () => {
const ctx = makeCtx();
new RivetSessionPersistDriver(ctx);
const data = ctx.state._sandboxAgentPersist as RivetPersistData;
expect(data).toBeDefined();
expect(data.sessions).toEqual({});
expect(data.events).toEqual({});
});
it("preserves existing state on construction (actor wake)", async () => {
const ctx = makeCtx();
const driver1 = new RivetSessionPersistDriver(ctx);
await driver1.updateSession({
id: "s-1",
agent: "mock",
agentSessionId: "a-1",
lastConnectionId: "c-1",
createdAt: 100,
});
// Simulate actor wake: new driver instance, same state object
const driver2 = new RivetSessionPersistDriver(ctx);
const session = await driver2.getSession("s-1");
expect(session?.id).toBe("s-1");
expect(session?.createdAt).toBe(100);
});
it("stores and retrieves sessions", async () => {
const driver = new RivetSessionPersistDriver(makeCtx());
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,
});
const loaded = await driver.getSession("s-2");
expect(loaded?.destroyedAt).toBe(300);
const missing = await driver.getSession("s-nonexistent");
expect(missing).toBeUndefined();
});
it("pages sessions sorted by createdAt", async () => {
const driver = new RivetSessionPersistDriver(makeCtx());
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,
});
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();
});
it("stores and pages events", async () => {
const driver = new RivetSessionPersistDriver(makeCtx());
await driver.updateSession({
id: "s-1",
agent: "mock",
agentSessionId: "a-1",
lastConnectionId: "c-1",
createdAt: 1,
});
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 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);
});
it("evicts oldest sessions when maxSessions exceeded", async () => {
const driver = new RivetSessionPersistDriver(makeCtx(), { maxSessions: 2 });
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,
});
// Adding a third session should evict the oldest (s-1)
await driver.updateSession({
id: "s-3",
agent: "mock",
agentSessionId: "a-3",
lastConnectionId: "c-3",
createdAt: 300,
});
expect(await driver.getSession("s-1")).toBeUndefined();
expect(await driver.getSession("s-2")).toBeDefined();
expect(await driver.getSession("s-3")).toBeDefined();
});
it("trims oldest events when maxEventsPerSession exceeded", async () => {
const driver = new RivetSessionPersistDriver(makeCtx(), { maxEventsPerSession: 2 });
await driver.updateSession({
id: "s-1",
agent: "mock",
agentSessionId: "a-1",
lastConnectionId: "c-1",
createdAt: 1,
});
for (let i = 1; i <= 3; i++) {
await driver.insertEvent("s-1", {
id: `evt-${i}`,
eventIndex: i,
sessionId: "s-1",
createdAt: i,
connectionId: "c-1",
sender: "client",
payload: { jsonrpc: "2.0", method: "session/prompt", params: { sessionId: "a-1" } },
});
}
const page = await driver.listEvents({ sessionId: "s-1" });
expect(page.items).toHaveLength(2);
// Oldest event (evt-1) should be trimmed
expect(page.items[0]?.id).toBe("evt-2");
expect(page.items[1]?.id).toBe("evt-3");
});
it("clones data to prevent external mutation", async () => {
const driver = new RivetSessionPersistDriver(makeCtx());
await driver.updateSession({
id: "s-1",
agent: "mock",
agentSessionId: "a-1",
lastConnectionId: "c-1",
createdAt: 1,
});
const s1 = await driver.getSession("s-1");
const s2 = await driver.getSession("s-1");
expect(s1).toEqual(s2);
expect(s1).not.toBe(s2); // Different object references
});
it("supports custom stateKey", async () => {
const ctx = makeCtx();
const driver = new RivetSessionPersistDriver(ctx, { stateKey: "myPersist" });
await driver.updateSession({
id: "s-1",
agent: "mock",
agentSessionId: "a-1",
lastConnectionId: "c-1",
createdAt: 1,
});
expect((ctx.state.myPersist as RivetPersistData).sessions["s-1"]).toBeDefined();
expect(ctx.state._sandboxAgentPersist).toBeUndefined();
});
it("returns empty results for unknown session events", async () => {
const driver = new RivetSessionPersistDriver(makeCtx());
const page = await driver.listEvents({ sessionId: "nonexistent" });
expect(page.items).toHaveLength(0);
expect(page.nextCursor).toBeUndefined();
});
});