From 7eef2fdf0bb1d126c03d2c8c7d24fada6b0db7b6 Mon Sep 17 00:00:00 2001 From: thesujai Date: Fri, 28 Aug 2026 15:59:32 +0530 Subject: [PATCH 1/5] feat: add agent sesison migrate --- .changeset/session-import-api.md | 5 + packages/trueforge/scripts/write-openapi.ts | 1 + packages/trueforge/src/apis/sessionImport.ts | 30 ++++ packages/trueforge/src/apis/settings.ts | 11 +- packages/trueforge/src/app.ts | 3 + .../session-store/importSessionSnapshot.ts | 157 ++++++++++++++++++ .../trueforge/src/db/sessionSnapshotImport.ts | 72 ++++++++ packages/trueforge/src/main.ts | 8 + .../src/routes/sessionImportRoutes.ts | 43 +++++ .../trueforge/src/schemas/sessionImport.ts | 77 +++++++++ 10 files changed, 406 insertions(+), 1 deletion(-) create mode 100644 .changeset/session-import-api.md create mode 100644 packages/trueforge/src/apis/sessionImport.ts create mode 100644 packages/trueforge/src/db/postgres/session-store/importSessionSnapshot.ts create mode 100644 packages/trueforge/src/db/sessionSnapshotImport.ts create mode 100644 packages/trueforge/src/routes/sessionImportRoutes.ts create mode 100644 packages/trueforge/src/schemas/sessionImport.ts diff --git a/.changeset/session-import-api.md b/.changeset/session-import-api.md new file mode 100644 index 000000000..5e087e333 --- /dev/null +++ b/.changeset/session-import-api.md @@ -0,0 +1,5 @@ +--- +"@truefoundry/trueforge": patch +--- + +Add ops-only POST /api/v1/settings/sessions/import for historical one-session backfill (skip-if-exists). diff --git a/packages/trueforge/scripts/write-openapi.ts b/packages/trueforge/scripts/write-openapi.ts index afb058658..b322b84b3 100644 --- a/packages/trueforge/scripts/write-openapi.ts +++ b/packages/trueforge/scripts/write-openapi.ts @@ -68,6 +68,7 @@ const app = createServerApp({ sandboxProviderStore: new SqliteSandboxProviderStore(db), agentStore: new SqliteAgentStore(db), scheduleStore: new SqliteScheduleStore(db), + sessionSnapshotImporter: undefined, sessionStore, sessions: new Sessions({ sessionStore }), activeTurns: new ActiveTurnRegistry(), diff --git a/packages/trueforge/src/apis/sessionImport.ts b/packages/trueforge/src/apis/sessionImport.ts new file mode 100644 index 000000000..65d986261 --- /dev/null +++ b/packages/trueforge/src/apis/sessionImport.ts @@ -0,0 +1,30 @@ +/** + * Admin session snapshot import under /api/v1/settings/sessions. + * Postgres-only; returns 501 when no importer is wired (standalone). + */ +import { OpenAPIHono, type RouteHandler } from '@hono/zod-openapi'; +import { HTTPException } from 'hono/http-exception'; +import type { ISessionSnapshotImporter } from '../db/sessionSnapshotImport'; +import { importSessionSnapshotRoute } from '../routes/sessionImportRoutes'; + +export interface SessionImportRouterDeps { + sessionSnapshotImporter: ISessionSnapshotImporter | undefined; +} + +export function createSessionImportRouter(deps: SessionImportRouterDeps) { + const router = new OpenAPIHono(); + + const importHandler: RouteHandler = async c => { + if (deps.sessionSnapshotImporter === undefined) { + throw new HTTPException(501, { + message: 'Session import requires Postgres (STANDALONE=false)', + }); + } + const body = c.req.valid('json'); + const result = await deps.sessionSnapshotImporter.importSessionSnapshot(body); + return c.json({ data: result }, result.imported ? 201 : 200); + }; + + router.openapi(importSessionSnapshotRoute, importHandler); + return router; +} diff --git a/packages/trueforge/src/apis/settings.ts b/packages/trueforge/src/apis/settings.ts index 8738d2d2d..8a31de81d 100644 --- a/packages/trueforge/src/apis/settings.ts +++ b/packages/trueforge/src/apis/settings.ts @@ -1,6 +1,6 @@ /** * Admin/settings API surface under /api/v1/settings. - * Sub-routers (model-providers, mcp-servers, skills, sandbox-providers) mount here. + * Sub-routers (model-providers, mcp-servers, skills, sandbox-providers, sessions) mount here. * Auth is applied at the /api/v1/settings mount boundary in app.ts (admin when auth is enabled). */ import { OpenAPIHono } from '@hono/zod-openapi'; @@ -9,12 +9,14 @@ import type { ResolveUserContext } from '../auth/identity'; import type { IMcpServerStore } from '../db/mcpServerStore'; import type { IModelProviderStore } from '../db/modelProviderStore'; import type { ISandboxProviderStore } from '../db/sandboxProviderStore'; +import type { ISessionSnapshotImporter } from '../db/sessionSnapshotImport'; import type { ISkillStore } from '../db/skillStore'; import type { WithTransaction } from '../db/transaction'; import type { IOAuthTokenStore } from '../mcp/auth/types'; import { createSettingsMcpServersRouter } from './mcpServers'; import { createModelProvidersRouter } from './modelProviders'; import { createSandboxProvidersRouter } from './sandboxProviders'; +import { createSessionImportRouter } from './sessionImport'; import { createSkillsRouter } from './skills'; export interface SettingsRouterDeps { @@ -23,6 +25,7 @@ export interface SettingsRouterDeps { tokenStore: IOAuthTokenStore; skillStore: ISkillStore; sandboxProviderStore: ISandboxProviderStore; + sessionSnapshotImporter: ISessionSnapshotImporter | undefined; withTransaction: WithTransaction; logger: Logger; resolveUserContext: ResolveUserContext; @@ -62,5 +65,11 @@ export function createSettingsRouter(deps: SettingsRouterDeps { sandboxProviderStore: ISandboxProviderStore; agentStore: IAgentStore; scheduleStore: IScheduleStore; + sessionSnapshotImporter: ISessionSnapshotImporter | undefined; sessionStore: ISessionStore; sessions: Sessions; activeTurns: ActiveTurnRegistry; @@ -271,6 +273,7 @@ export function createServerApp(deps: ServerDeps) { tokenStore: deps.tokenStore, skillStore: deps.skillStore, sandboxProviderStore: deps.sandboxProviderStore, + sessionSnapshotImporter: deps.sessionSnapshotImporter, withTransaction: deps.withTransaction, logger: deps.logger, resolveUserContext, diff --git a/packages/trueforge/src/db/postgres/session-store/importSessionSnapshot.ts b/packages/trueforge/src/db/postgres/session-store/importSessionSnapshot.ts new file mode 100644 index 000000000..580b3e273 --- /dev/null +++ b/packages/trueforge/src/db/postgres/session-store/importSessionSnapshot.ts @@ -0,0 +1,157 @@ +/** + * Postgres historical session snapshot insert (skip-if-exists). + */ +import type { Kysely, RawBuilder } from 'kysely'; +import { sql } from 'kysely'; +import { + isContextPrefix, + type ImportSessionSnapshotInput, + type ImportSessionSnapshotResult, + type ISessionSnapshotImporter, +} from '../../sessionSnapshotImport'; +import type { Database } from '../types'; + +function jsonbColumn(value: unknown): RawBuilder { + return sql`${JSON.stringify(value)}::jsonb`; +} + +export class PostgresSessionSnapshotImporter implements ISessionSnapshotImporter { + constructor(private readonly db: Kysely) {} + + async importSessionSnapshot(input: ImportSessionSnapshotInput): Promise { + const sessionId = input.session.session_id; + return this.db.transaction().execute(async trx => { + const existing = await trx + .selectFrom('session') + .select('session_id') + .where('session_id', '=', sessionId) + .executeTakeFirst(); + if (existing !== undefined) { + return { imported: false, session_id: sessionId }; + } + + const { session, turns } = input; + await trx + .insertInto('session') + .values({ + tenant_id: session.tenant_id, + session_id: sessionId, + created_by: session.created_by, + agent_id: null, + agent_name: null, + agent_spec: jsonbColumn(session.agent_spec), + title: session.title, + last_turn_id: session.last_turn_id, + custom: session.custom !== null ? jsonbColumn(session.custom) : null, + last_activity_timestamp_ms: session.last_activity_timestamp_ms, + created_at: new Date(session.created_at), + updated_at: new Date(session.updated_at), + }) + .execute(); + + const prevContextByThread = new Map(); + const prevContextIdsByThread = new Map(); + + for (const turn of turns) { + await trx + .insertInto('turn') + .values({ + session_id: sessionId, + turn_id: turn.turn_id, + first_turn_id: turn.first_turn_id, + previous_turn_id: turn.previous_turn_id, + ancestor_ids: turn.ancestor_ids, + input: jsonbColumn(turn.input), + state: jsonbColumn(turn.state), + checkpoint: jsonbColumn(turn.checkpoint), + custom: turn.custom !== null ? jsonbColumn(turn.custom) : null, + created_at: new Date(turn.created_at), + updated_at: new Date(turn.updated_at), + }) + .execute(); + + for (const thread of turn.threads) { + const prevCtx = prevContextByThread.get(thread.thread_id) ?? []; + const prevIds = prevContextIdsByThread.get(thread.thread_id) ?? []; + const appendOnly = isContextPrefix(prevCtx, thread.context); + const newMessages = appendOnly ? thread.context.slice(prevCtx.length) : thread.context; + const reusedIds = appendOnly ? prevIds : []; + + const newIds: number[] = []; + if (newMessages.length > 0) { + const inserted = await trx + .insertInto('thread_context_log') + .values( + newMessages.map(msg => ({ + session_id: sessionId, + thread_id: thread.thread_id, + turn_id: turn.turn_id, + body: jsonbColumn(msg), + created_at: new Date(turn.updated_at), + })), + ) + .returning(['append_id']) + .execute(); + for (const row of inserted) { + newIds.push(row.append_id); + } + } + + const contextIds = [...reusedIds, ...newIds]; + await trx + .insertInto('turn_thread') + .values({ + session_id: sessionId, + turn_id: turn.turn_id, + thread_id: thread.thread_id, + checkpoint: jsonbColumn({ parent: thread.parent, completion: thread.completion }), + agent_info: thread.agent_info !== null ? jsonbColumn(thread.agent_info) : null, + current_context_usage: jsonbColumn(thread.current_context_usage), + context_ids: contextIds, + updated_at: new Date(turn.updated_at), + }) + .execute(); + + prevContextByThread.set(thread.thread_id, thread.context); + prevContextIdsByThread.set(thread.thread_id, contextIds); + + if (thread.capability_state !== null) { + const capEntries = Object.entries(thread.capability_state); + if (capEntries.length > 0) { + await trx + .insertInto('thread_capability_state') + .values( + capEntries.map(([key, state]) => ({ + session_id: sessionId, + turn_id: turn.turn_id, + thread_id: thread.thread_id, + key, + state: jsonbColumn(state), + updated_at: new Date(turn.updated_at), + })), + ) + .execute(); + } + } + } + + if (turn.events.length > 0) { + await trx + .insertInto('session_event') + .values( + turn.events.map(event => ({ + session_id: sessionId, + turn_id: turn.turn_id, + event_id: event.id, + event: jsonbColumn(event), + created_at: new Date(event.created_at), + })), + ) + .execute(); + } + } + + return { imported: true, session_id: sessionId }; + }); + } +} diff --git a/packages/trueforge/src/db/sessionSnapshotImport.ts b/packages/trueforge/src/db/sessionSnapshotImport.ts new file mode 100644 index 000000000..9f4788066 --- /dev/null +++ b/packages/trueforge/src/db/sessionSnapshotImport.ts @@ -0,0 +1,72 @@ +/** + * Ops-only session snapshot import. Postgres historical backfill — not ISessionStore. + */ +export interface ImportSessionTurnThread { + thread_id: string; + context: unknown[]; + current_context_usage: unknown; + parent: unknown | null; + completion: unknown | null; + agent_info: unknown | null; + capability_state: Record | null; +} + +export interface ImportSessionTurnEvent { + id: string; + created_at: string; + [key: string]: unknown; +} + +export interface ImportSessionTurn { + turn_id: string; + first_turn_id: string; + previous_turn_id: string | null; + ancestor_ids: string[]; + input: unknown[]; + state: unknown; + checkpoint: { mcp_servers: unknown | null; sandbox_info: unknown | null }; + custom: Record | null; + created_at: string; + updated_at: string; + threads: ImportSessionTurnThread[]; + events: ImportSessionTurnEvent[]; +} + +export interface ImportSessionSnapshotSession { + session_id: string; + tenant_id: string; + created_by: string; + agent_spec: Record; + title: string | null; + last_turn_id: string | null; + custom: Record | null; + last_activity_timestamp_ms: number; + created_at: string; + updated_at: string; +} + +export interface ImportSessionSnapshotInput { + session: ImportSessionSnapshotSession; + turns: ImportSessionTurn[]; +} + +export interface ImportSessionSnapshotResult { + imported: boolean; + session_id: string; +} + +export interface ISessionSnapshotImporter { + importSessionSnapshot(input: ImportSessionSnapshotInput): Promise; +} + +export function isContextPrefix(prefix: unknown[], full: unknown[]): boolean { + if (prefix.length > full.length) { + return false; + } + for (let i = 0; i < prefix.length; i++) { + if (JSON.stringify(prefix[i]) !== JSON.stringify(full[i])) { + return false; + } + } + return true; +} diff --git a/packages/trueforge/src/main.ts b/packages/trueforge/src/main.ts index 047d0f7a1..a3a8f9c2b 100644 --- a/packages/trueforge/src/main.ts +++ b/packages/trueforge/src/main.ts @@ -59,6 +59,7 @@ import type { IModelProviderStore } from './db/modelProviderStore'; import type { Database as PostgresDatabase } from './db/postgres/types'; import type { ISandboxProviderStore } from './db/sandboxProviderStore'; import type { IScheduleStore } from './db/scheduleStore'; +import type { ISessionSnapshotImporter } from './db/sessionSnapshotImport'; import type { ISkillStore } from './db/skillStore'; import type { Database as SqliteDatabase } from './db/sqlite/types'; import type { WithTransaction } from './db/transaction'; @@ -81,6 +82,7 @@ interface ServerPersistence { sandboxProviderStore: ISandboxProviderStore; agentStore: IAgentStore; scheduleStore: IScheduleStore; + sessionSnapshotImporter: ISessionSnapshotImporter | undefined; destroyDb: () => Promise; redis: RedisClientType | undefined; } @@ -132,6 +134,7 @@ async function createStandalonePersistence(options: { sandboxProviderStore: new SqliteSandboxProviderStore(db), agentStore: new SqliteAgentStore(db), scheduleStore: new SqliteScheduleStore(db), + sessionSnapshotImporter: undefined, destroyDb: () => db.destroy(), redis: undefined, }; @@ -165,6 +168,7 @@ async function createDistributedPersistence(options: { import('./db/postgres/sandbox-provider-store/PostgresSandboxProviderStore'), import('./db/postgres/agent-store/PostgresAgentStore'), import('./db/postgres/schedule-store/PostgresScheduleStore'), + import('./db/postgres/session-store/importSessionSnapshot'), ]), ]); const [ @@ -176,6 +180,7 @@ async function createDistributedPersistence(options: { { PostgresSandboxProviderStore }, { PostgresAgentStore }, { PostgresScheduleStore }, + { PostgresSessionSnapshotImporter }, ] = postgresStores; const db = createDb({ @@ -198,6 +203,7 @@ async function createDistributedPersistence(options: { sandboxProviderStore: new PostgresSandboxProviderStore(db), agentStore: new PostgresAgentStore(db), scheduleStore: new PostgresScheduleStore(db), + sessionSnapshotImporter: new PostgresSessionSnapshotImporter(db), destroyDb: () => db.destroy(), redis: await connectRedis({ url: redisUrl, logger }), }; @@ -215,6 +221,7 @@ async function createServerRuntime(persistence: ServerPersistence< sandboxProviderStore, agentStore, scheduleStore, + sessionSnapshotImporter, destroyDb, redis, } = persistence; @@ -259,6 +266,7 @@ async function createServerRuntime(persistence: ServerPersistence< sandboxProviderStore, agentStore, scheduleStore, + sessionSnapshotImporter, sessionStore, sessions: new Sessions({ sessionStore }), activeTurns, diff --git a/packages/trueforge/src/routes/sessionImportRoutes.ts b/packages/trueforge/src/routes/sessionImportRoutes.ts new file mode 100644 index 000000000..9c53d4f61 --- /dev/null +++ b/packages/trueforge/src/routes/sessionImportRoutes.ts @@ -0,0 +1,43 @@ +/** + * Ops session import route (mounted at /api/v1/settings/sessions/import). + */ +import { createRoute } from '@hono/zod-openapi'; +import { RequestErrorResponseSchema } from '../schemas/errors'; +import { + ImportSessionSnapshotRequestSchema, + ImportSessionSnapshotResponseSchema, +} from '../schemas/sessionImport'; +import { OpenApiTag } from './openapiTags'; + +export const importSessionSnapshotRoute = createRoute({ + method: 'post', + path: '/import', + tags: [OpenApiTag.AGENT_SESSIONS], + summary: 'Import one historical session snapshot', + description: 'Ops/backfill only. Skip if session_id exists; else insert in one transaction.', + 'x-fern-ignore': true, + request: { + body: { + content: { 'application/json': { schema: ImportSessionSnapshotRequestSchema } }, + required: true, + }, + }, + responses: { + 200: { + content: { 'application/json': { schema: ImportSessionSnapshotResponseSchema } }, + description: 'Skipped — already exists.', + }, + 201: { + content: { 'application/json': { schema: ImportSessionSnapshotResponseSchema } }, + description: 'Imported.', + }, + 400: { + content: { 'application/json': { schema: RequestErrorResponseSchema } }, + description: 'Invalid body.', + }, + 501: { + content: { 'application/json': { schema: RequestErrorResponseSchema } }, + description: 'Import requires Postgres (not available in standalone).', + }, + }, +}); diff --git a/packages/trueforge/src/schemas/sessionImport.ts b/packages/trueforge/src/schemas/sessionImport.ts new file mode 100644 index 000000000..ea04885d6 --- /dev/null +++ b/packages/trueforge/src/schemas/sessionImport.ts @@ -0,0 +1,77 @@ +/** + * Wire schema for POST /api/v1/settings/sessions/import (one-shot ops backfill). + * Loose validation — caller (k8s-controller) owns massaging. + */ +import { z } from '@hono/zod-openapi'; + +export const ImportSessionSnapshotRequestSchema = z + .object({ + session: z + .object({ + session_id: z.string().min(1), + tenant_id: z.string().min(1), + created_by: z.string().min(1), + agent_spec: z.record(z.string(), z.unknown()), + title: z.string().nullable(), + last_turn_id: z.string().nullable(), + custom: z.record(z.string(), z.unknown()).nullable(), + last_activity_timestamp_ms: z.number(), + created_at: z.string().min(1), + updated_at: z.string().min(1), + }) + .passthrough(), + turns: z + .array( + z + .object({ + turn_id: z.string().min(1), + first_turn_id: z.string().min(1), + previous_turn_id: z.string().nullable(), + ancestor_ids: z.array(z.string()), + input: z.array(z.unknown()), + state: z.unknown(), + checkpoint: z.object({ + mcp_servers: z.unknown().nullable(), + sandbox_info: z.unknown().nullable(), + }), + custom: z.record(z.string(), z.unknown()).nullable(), + created_at: z.string().min(1), + updated_at: z.string().min(1), + threads: z.array( + z + .object({ + thread_id: z.string().min(1), + context: z.array(z.unknown()), + current_context_usage: z.unknown(), + parent: z.unknown().nullable(), + completion: z.unknown().nullable(), + agent_info: z.unknown().nullable(), + capability_state: z.record(z.string(), z.unknown()).nullable(), + }) + .passthrough(), + ), + events: z.array( + z + .object({ + id: z.string().min(1), + created_at: z.string().min(1), + }) + .passthrough(), + ), + }) + .passthrough(), + ) + .min(1), + }) + .openapi('ImportSessionSnapshotRequest'); + +export const ImportSessionSnapshotResponseSchema = z + .object({ + data: z.object({ + imported: z.boolean(), + session_id: z.string(), + }), + }) + .openapi('ImportSessionSnapshotResponse'); + +export type ImportSessionSnapshotRequest = z.infer; From 0817072dafae442291b0fee4f5583b1e93df9afd Mon Sep 17 00:00:00 2001 From: "trueforge-dev-bot[bot]" Date: Fri, 28 Aug 2026 10:36:57 +0000 Subject: [PATCH 2/5] Regenerate OpenAPI document and TypeScript SDK --- .github/fern/openapi/openapi.json | 282 ++++++++++++++++++++++++++++++ docs/openapi.json | 282 ++++++++++++++++++++++++++++++ 2 files changed, 564 insertions(+) diff --git a/.github/fern/openapi/openapi.json b/.github/fern/openapi/openapi.json index aeed990c5..14412dd3d 100644 --- a/.github/fern/openapi/openapi.json +++ b/.github/fern/openapi/openapi.json @@ -1395,6 +1395,226 @@ ], "type": "object" }, + "ImportSessionSnapshotRequest": { + "properties": { + "session": { + "additionalProperties": {}, + "properties": { + "agent_spec": { + "additionalProperties": {}, + "type": "object" + }, + "created_at": { + "minLength": 1, + "type": "string" + }, + "created_by": { + "minLength": 1, + "type": "string" + }, + "custom": { + "additionalProperties": {}, + "type": [ + "object", + "null" + ] + }, + "last_activity_timestamp_ms": { + "type": "number" + }, + "last_turn_id": { + "type": [ + "string", + "null" + ] + }, + "session_id": { + "minLength": 1, + "type": "string" + }, + "tenant_id": { + "minLength": 1, + "type": "string" + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "updated_at": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "session_id", + "tenant_id", + "created_by", + "agent_spec", + "title", + "last_turn_id", + "custom", + "last_activity_timestamp_ms", + "created_at", + "updated_at" + ], + "type": "object" + }, + "turns": { + "items": { + "additionalProperties": {}, + "properties": { + "ancestor_ids": { + "items": { + "type": "string" + }, + "type": "array" + }, + "checkpoint": { + "properties": { + "mcp_servers": {}, + "sandbox_info": {} + }, + "type": "object" + }, + "created_at": { + "minLength": 1, + "type": "string" + }, + "custom": { + "additionalProperties": {}, + "type": [ + "object", + "null" + ] + }, + "events": { + "items": { + "additionalProperties": {}, + "properties": { + "created_at": { + "minLength": 1, + "type": "string" + }, + "id": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "id", + "created_at" + ], + "type": "object" + }, + "type": "array" + }, + "first_turn_id": { + "minLength": 1, + "type": "string" + }, + "input": { + "items": {}, + "type": "array" + }, + "previous_turn_id": { + "type": [ + "string", + "null" + ] + }, + "state": {}, + "threads": { + "items": { + "additionalProperties": {}, + "properties": { + "agent_info": {}, + "capability_state": { + "additionalProperties": {}, + "type": [ + "object", + "null" + ] + }, + "completion": {}, + "context": { + "items": {}, + "type": "array" + }, + "current_context_usage": {}, + "parent": {}, + "thread_id": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "thread_id", + "context", + "capability_state" + ], + "type": "object" + }, + "type": "array" + }, + "turn_id": { + "minLength": 1, + "type": "string" + }, + "updated_at": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "turn_id", + "first_turn_id", + "previous_turn_id", + "ancestor_ids", + "input", + "checkpoint", + "custom", + "created_at", + "updated_at", + "threads", + "events" + ], + "type": "object" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "session", + "turns" + ], + "type": "object" + }, + "ImportSessionSnapshotResponse": { + "properties": { + "data": { + "properties": { + "imported": { + "type": "boolean" + }, + "session_id": { + "type": "string" + } + }, + "required": [ + "imported", + "session_id" + ], + "type": "object" + } + }, + "required": [ + "data" + ], + "type": "object" + }, "InitialUserMessage": { "properties": { "content": { @@ -7304,6 +7524,68 @@ "x-fern-sdk-method-name": "create_or_update" } }, + "/api/v1/settings/sessions/import": { + "post": { + "description": "Ops/backfill only. Skip if session_id exists; else insert in one transaction.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportSessionSnapshotRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportSessionSnapshotResponse" + } + } + }, + "description": "Skipped — already exists." + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportSessionSnapshotResponse" + } + } + }, + "description": "Imported." + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Invalid body." + }, + "501": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Import requires Postgres (not available in standalone)." + } + }, + "summary": "Import one historical session snapshot", + "tags": [ + "Agent Sessions" + ], + "x-fern-ignore": true + } + }, "/api/v1/settings/skills": { "get": { "description": "All configured skills with nested manifests (settings / admin projection).", diff --git a/docs/openapi.json b/docs/openapi.json index aeed990c5..14412dd3d 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -1395,6 +1395,226 @@ ], "type": "object" }, + "ImportSessionSnapshotRequest": { + "properties": { + "session": { + "additionalProperties": {}, + "properties": { + "agent_spec": { + "additionalProperties": {}, + "type": "object" + }, + "created_at": { + "minLength": 1, + "type": "string" + }, + "created_by": { + "minLength": 1, + "type": "string" + }, + "custom": { + "additionalProperties": {}, + "type": [ + "object", + "null" + ] + }, + "last_activity_timestamp_ms": { + "type": "number" + }, + "last_turn_id": { + "type": [ + "string", + "null" + ] + }, + "session_id": { + "minLength": 1, + "type": "string" + }, + "tenant_id": { + "minLength": 1, + "type": "string" + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "updated_at": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "session_id", + "tenant_id", + "created_by", + "agent_spec", + "title", + "last_turn_id", + "custom", + "last_activity_timestamp_ms", + "created_at", + "updated_at" + ], + "type": "object" + }, + "turns": { + "items": { + "additionalProperties": {}, + "properties": { + "ancestor_ids": { + "items": { + "type": "string" + }, + "type": "array" + }, + "checkpoint": { + "properties": { + "mcp_servers": {}, + "sandbox_info": {} + }, + "type": "object" + }, + "created_at": { + "minLength": 1, + "type": "string" + }, + "custom": { + "additionalProperties": {}, + "type": [ + "object", + "null" + ] + }, + "events": { + "items": { + "additionalProperties": {}, + "properties": { + "created_at": { + "minLength": 1, + "type": "string" + }, + "id": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "id", + "created_at" + ], + "type": "object" + }, + "type": "array" + }, + "first_turn_id": { + "minLength": 1, + "type": "string" + }, + "input": { + "items": {}, + "type": "array" + }, + "previous_turn_id": { + "type": [ + "string", + "null" + ] + }, + "state": {}, + "threads": { + "items": { + "additionalProperties": {}, + "properties": { + "agent_info": {}, + "capability_state": { + "additionalProperties": {}, + "type": [ + "object", + "null" + ] + }, + "completion": {}, + "context": { + "items": {}, + "type": "array" + }, + "current_context_usage": {}, + "parent": {}, + "thread_id": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "thread_id", + "context", + "capability_state" + ], + "type": "object" + }, + "type": "array" + }, + "turn_id": { + "minLength": 1, + "type": "string" + }, + "updated_at": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "turn_id", + "first_turn_id", + "previous_turn_id", + "ancestor_ids", + "input", + "checkpoint", + "custom", + "created_at", + "updated_at", + "threads", + "events" + ], + "type": "object" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "session", + "turns" + ], + "type": "object" + }, + "ImportSessionSnapshotResponse": { + "properties": { + "data": { + "properties": { + "imported": { + "type": "boolean" + }, + "session_id": { + "type": "string" + } + }, + "required": [ + "imported", + "session_id" + ], + "type": "object" + } + }, + "required": [ + "data" + ], + "type": "object" + }, "InitialUserMessage": { "properties": { "content": { @@ -7304,6 +7524,68 @@ "x-fern-sdk-method-name": "create_or_update" } }, + "/api/v1/settings/sessions/import": { + "post": { + "description": "Ops/backfill only. Skip if session_id exists; else insert in one transaction.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportSessionSnapshotRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportSessionSnapshotResponse" + } + } + }, + "description": "Skipped — already exists." + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImportSessionSnapshotResponse" + } + } + }, + "description": "Imported." + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Invalid body." + }, + "501": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Import requires Postgres (not available in standalone)." + } + }, + "summary": "Import one historical session snapshot", + "tags": [ + "Agent Sessions" + ], + "x-fern-ignore": true + } + }, "/api/v1/settings/skills": { "get": { "description": "All configured skills with nested manifests (settings / admin projection).", From f7dc19aa500d95926bf3572c7a25f9d8133df1f3 Mon Sep 17 00:00:00 2001 From: thesujai Date: Fri, 28 Aug 2026 16:23:54 +0530 Subject: [PATCH 3/5] refactor: streamline session snapshot import interfaces and improve JSON handling --- .../session-store/importSessionSnapshot.ts | 52 +++++++---------- .../src/db/postgres/sqlExpressions.ts | 7 ++- .../trueforge/src/db/sessionSnapshotImport.ts | 57 ++----------------- .../src/routes/sessionImportRoutes.ts | 5 +- .../trueforge/src/schemas/sessionImport.ts | 2 + 5 files changed, 34 insertions(+), 89 deletions(-) diff --git a/packages/trueforge/src/db/postgres/session-store/importSessionSnapshot.ts b/packages/trueforge/src/db/postgres/session-store/importSessionSnapshot.ts index 580b3e273..db2502189 100644 --- a/packages/trueforge/src/db/postgres/session-store/importSessionSnapshot.ts +++ b/packages/trueforge/src/db/postgres/session-store/importSessionSnapshot.ts @@ -1,37 +1,24 @@ /** * Postgres historical session snapshot insert (skip-if-exists). */ -import type { Kysely, RawBuilder } from 'kysely'; -import { sql } from 'kysely'; +import type { Kysely } from 'kysely'; import { isContextPrefix, type ImportSessionSnapshotInput, type ImportSessionSnapshotResult, type ISessionSnapshotImporter, } from '../../sessionSnapshotImport'; +import { json } from '../sqlExpressions'; import type { Database } from '../types'; -function jsonbColumn(value: unknown): RawBuilder { - return sql`${JSON.stringify(value)}::jsonb`; -} - export class PostgresSessionSnapshotImporter implements ISessionSnapshotImporter { constructor(private readonly db: Kysely) {} async importSessionSnapshot(input: ImportSessionSnapshotInput): Promise { const sessionId = input.session.session_id; return this.db.transaction().execute(async trx => { - const existing = await trx - .selectFrom('session') - .select('session_id') - .where('session_id', '=', sessionId) - .executeTakeFirst(); - if (existing !== undefined) { - return { imported: false, session_id: sessionId }; - } - const { session, turns } = input; - await trx + const insertedSession = await trx .insertInto('session') .values({ tenant_id: session.tenant_id, @@ -39,15 +26,20 @@ export class PostgresSessionSnapshotImporter implements ISessionSnapshotImporter created_by: session.created_by, agent_id: null, agent_name: null, - agent_spec: jsonbColumn(session.agent_spec), + agent_spec: json(session.agent_spec), title: session.title, last_turn_id: session.last_turn_id, - custom: session.custom !== null ? jsonbColumn(session.custom) : null, + custom: session.custom !== null ? json(session.custom) : null, last_activity_timestamp_ms: session.last_activity_timestamp_ms, created_at: new Date(session.created_at), updated_at: new Date(session.updated_at), }) - .execute(); + .onConflict(oc => oc.column('session_id').doNothing()) + .returning('session_id') + .executeTakeFirst(); + if (insertedSession === undefined) { + return { imported: false, session_id: sessionId }; + } const prevContextByThread = new Map(); const prevContextIdsByThread = new Map(); @@ -61,10 +53,10 @@ export class PostgresSessionSnapshotImporter implements ISessionSnapshotImporter first_turn_id: turn.first_turn_id, previous_turn_id: turn.previous_turn_id, ancestor_ids: turn.ancestor_ids, - input: jsonbColumn(turn.input), - state: jsonbColumn(turn.state), - checkpoint: jsonbColumn(turn.checkpoint), - custom: turn.custom !== null ? jsonbColumn(turn.custom) : null, + input: json(turn.input), + state: json(turn.state), + checkpoint: json(turn.checkpoint), + custom: turn.custom !== null ? json(turn.custom) : null, created_at: new Date(turn.created_at), updated_at: new Date(turn.updated_at), }) @@ -73,7 +65,7 @@ export class PostgresSessionSnapshotImporter implements ISessionSnapshotImporter for (const thread of turn.threads) { const prevCtx = prevContextByThread.get(thread.thread_id) ?? []; const prevIds = prevContextIdsByThread.get(thread.thread_id) ?? []; - const appendOnly = isContextPrefix(prevCtx, thread.context); + const appendOnly = isContextPrefix({ prefix: prevCtx, full: thread.context }); const newMessages = appendOnly ? thread.context.slice(prevCtx.length) : thread.context; const reusedIds = appendOnly ? prevIds : []; @@ -86,7 +78,7 @@ export class PostgresSessionSnapshotImporter implements ISessionSnapshotImporter session_id: sessionId, thread_id: thread.thread_id, turn_id: turn.turn_id, - body: jsonbColumn(msg), + body: json(msg), created_at: new Date(turn.updated_at), })), ) @@ -104,9 +96,9 @@ export class PostgresSessionSnapshotImporter implements ISessionSnapshotImporter session_id: sessionId, turn_id: turn.turn_id, thread_id: thread.thread_id, - checkpoint: jsonbColumn({ parent: thread.parent, completion: thread.completion }), - agent_info: thread.agent_info !== null ? jsonbColumn(thread.agent_info) : null, - current_context_usage: jsonbColumn(thread.current_context_usage), + checkpoint: json({ parent: thread.parent, completion: thread.completion }), + agent_info: thread.agent_info !== null ? json(thread.agent_info) : null, + current_context_usage: json(thread.current_context_usage), context_ids: contextIds, updated_at: new Date(turn.updated_at), }) @@ -126,7 +118,7 @@ export class PostgresSessionSnapshotImporter implements ISessionSnapshotImporter turn_id: turn.turn_id, thread_id: thread.thread_id, key, - state: jsonbColumn(state), + state: json(state), updated_at: new Date(turn.updated_at), })), ) @@ -143,7 +135,7 @@ export class PostgresSessionSnapshotImporter implements ISessionSnapshotImporter session_id: sessionId, turn_id: turn.turn_id, event_id: event.id, - event: jsonbColumn(event), + event: json(event), created_at: new Date(event.created_at), })), ) diff --git a/packages/trueforge/src/db/postgres/sqlExpressions.ts b/packages/trueforge/src/db/postgres/sqlExpressions.ts index 90e44bc8d..7d2b7730f 100644 --- a/packages/trueforge/src/db/postgres/sqlExpressions.ts +++ b/packages/trueforge/src/db/postgres/sqlExpressions.ts @@ -3,8 +3,11 @@ */ import { sql, type Expression, type RawBuilder } from 'kysely'; -/** Bind a JS value as jsonb (stringified + cast). Required for arrays and for `||` / jsonb_set operands. */ -export function json(value: T): RawBuilder { +/** + * Bind a JS value as jsonb (stringified + cast). Required for arrays and for `||` / jsonb_set operands. + * `T` is taken from the call-site column type (loose ops payloads are `unknown` at the wire). + */ +export function json(value: unknown): RawBuilder { return sql`${JSON.stringify(value)}::jsonb`; } diff --git a/packages/trueforge/src/db/sessionSnapshotImport.ts b/packages/trueforge/src/db/sessionSnapshotImport.ts index 9f4788066..2cc791499 100644 --- a/packages/trueforge/src/db/sessionSnapshotImport.ts +++ b/packages/trueforge/src/db/sessionSnapshotImport.ts @@ -1,65 +1,16 @@ /** * Ops-only session snapshot import. Postgres historical backfill — not ISessionStore. */ -export interface ImportSessionTurnThread { - thread_id: string; - context: unknown[]; - current_context_usage: unknown; - parent: unknown | null; - completion: unknown | null; - agent_info: unknown | null; - capability_state: Record | null; -} - -export interface ImportSessionTurnEvent { - id: string; - created_at: string; - [key: string]: unknown; -} - -export interface ImportSessionTurn { - turn_id: string; - first_turn_id: string; - previous_turn_id: string | null; - ancestor_ids: string[]; - input: unknown[]; - state: unknown; - checkpoint: { mcp_servers: unknown | null; sandbox_info: unknown | null }; - custom: Record | null; - created_at: string; - updated_at: string; - threads: ImportSessionTurnThread[]; - events: ImportSessionTurnEvent[]; -} +import type { ImportSessionSnapshotRequest, ImportSessionSnapshotResult } from '../schemas/sessionImport'; -export interface ImportSessionSnapshotSession { - session_id: string; - tenant_id: string; - created_by: string; - agent_spec: Record; - title: string | null; - last_turn_id: string | null; - custom: Record | null; - last_activity_timestamp_ms: number; - created_at: string; - updated_at: string; -} - -export interface ImportSessionSnapshotInput { - session: ImportSessionSnapshotSession; - turns: ImportSessionTurn[]; -} - -export interface ImportSessionSnapshotResult { - imported: boolean; - session_id: string; -} +export type ImportSessionSnapshotInput = ImportSessionSnapshotRequest; +export type { ImportSessionSnapshotResult }; export interface ISessionSnapshotImporter { importSessionSnapshot(input: ImportSessionSnapshotInput): Promise; } -export function isContextPrefix(prefix: unknown[], full: unknown[]): boolean { +export function isContextPrefix({ prefix, full }: { prefix: unknown[]; full: unknown[] }): boolean { if (prefix.length > full.length) { return false; } diff --git a/packages/trueforge/src/routes/sessionImportRoutes.ts b/packages/trueforge/src/routes/sessionImportRoutes.ts index 9c53d4f61..bf737ec1d 100644 --- a/packages/trueforge/src/routes/sessionImportRoutes.ts +++ b/packages/trueforge/src/routes/sessionImportRoutes.ts @@ -3,10 +3,7 @@ */ import { createRoute } from '@hono/zod-openapi'; import { RequestErrorResponseSchema } from '../schemas/errors'; -import { - ImportSessionSnapshotRequestSchema, - ImportSessionSnapshotResponseSchema, -} from '../schemas/sessionImport'; +import { ImportSessionSnapshotRequestSchema, ImportSessionSnapshotResponseSchema } from '../schemas/sessionImport'; import { OpenApiTag } from './openapiTags'; export const importSessionSnapshotRoute = createRoute({ diff --git a/packages/trueforge/src/schemas/sessionImport.ts b/packages/trueforge/src/schemas/sessionImport.ts index ea04885d6..154717e20 100644 --- a/packages/trueforge/src/schemas/sessionImport.ts +++ b/packages/trueforge/src/schemas/sessionImport.ts @@ -75,3 +75,5 @@ export const ImportSessionSnapshotResponseSchema = z .openapi('ImportSessionSnapshotResponse'); export type ImportSessionSnapshotRequest = z.infer; +export type ImportSessionSnapshotResponse = z.infer; +export type ImportSessionSnapshotResult = ImportSessionSnapshotResponse['data']; From 246433d71521e6ee3be1d8a4894764d7dc9ebf9f Mon Sep 17 00:00:00 2001 From: thesujai Date: Fri, 28 Aug 2026 16:39:07 +0530 Subject: [PATCH 4/5] refactor: update session snapshot import types and enhance JSON handling in Postgres integration --- .../session-store/importSessionSnapshot.ts | 32 ++++++++----------- .../src/db/postgres/sqlExpressions.ts | 15 ++++++--- .../trueforge/src/db/sessionSnapshotImport.ts | 5 +-- .../trueforge/src/schemas/sessionImport.ts | 22 +++++++------ .../tests/unit/apis/modelProviders.test.ts | 1 + 5 files changed, 39 insertions(+), 36 deletions(-) diff --git a/packages/trueforge/src/db/postgres/session-store/importSessionSnapshot.ts b/packages/trueforge/src/db/postgres/session-store/importSessionSnapshot.ts index db2502189..4bf12c17e 100644 --- a/packages/trueforge/src/db/postgres/session-store/importSessionSnapshot.ts +++ b/packages/trueforge/src/db/postgres/session-store/importSessionSnapshot.ts @@ -2,19 +2,15 @@ * Postgres historical session snapshot insert (skip-if-exists). */ import type { Kysely } from 'kysely'; -import { - isContextPrefix, - type ImportSessionSnapshotInput, - type ImportSessionSnapshotResult, - type ISessionSnapshotImporter, -} from '../../sessionSnapshotImport'; -import { json } from '../sqlExpressions'; +import type { ImportSessionSnapshotRequest, ImportSessionSnapshotResult } from '../../../schemas/sessionImport'; +import { isContextPrefix, type ISessionSnapshotImporter } from '../../sessionSnapshotImport'; +import { json, jsonUnknown } from '../sqlExpressions'; import type { Database } from '../types'; export class PostgresSessionSnapshotImporter implements ISessionSnapshotImporter { constructor(private readonly db: Kysely) {} - async importSessionSnapshot(input: ImportSessionSnapshotInput): Promise { + async importSessionSnapshot(input: ImportSessionSnapshotRequest): Promise { const sessionId = input.session.session_id; return this.db.transaction().execute(async trx => { const { session, turns } = input; @@ -26,7 +22,7 @@ export class PostgresSessionSnapshotImporter implements ISessionSnapshotImporter created_by: session.created_by, agent_id: null, agent_name: null, - agent_spec: json(session.agent_spec), + agent_spec: jsonUnknown(session.agent_spec), title: session.title, last_turn_id: session.last_turn_id, custom: session.custom !== null ? json(session.custom) : null, @@ -53,9 +49,9 @@ export class PostgresSessionSnapshotImporter implements ISessionSnapshotImporter first_turn_id: turn.first_turn_id, previous_turn_id: turn.previous_turn_id, ancestor_ids: turn.ancestor_ids, - input: json(turn.input), - state: json(turn.state), - checkpoint: json(turn.checkpoint), + input: jsonUnknown(turn.input), + state: jsonUnknown(turn.state), + checkpoint: jsonUnknown(turn.checkpoint), custom: turn.custom !== null ? json(turn.custom) : null, created_at: new Date(turn.created_at), updated_at: new Date(turn.updated_at), @@ -78,7 +74,7 @@ export class PostgresSessionSnapshotImporter implements ISessionSnapshotImporter session_id: sessionId, thread_id: thread.thread_id, turn_id: turn.turn_id, - body: json(msg), + body: jsonUnknown(msg), created_at: new Date(turn.updated_at), })), ) @@ -96,9 +92,9 @@ export class PostgresSessionSnapshotImporter implements ISessionSnapshotImporter session_id: sessionId, turn_id: turn.turn_id, thread_id: thread.thread_id, - checkpoint: json({ parent: thread.parent, completion: thread.completion }), - agent_info: thread.agent_info !== null ? json(thread.agent_info) : null, - current_context_usage: json(thread.current_context_usage), + checkpoint: jsonUnknown({ parent: thread.parent, completion: thread.completion }), + agent_info: thread.agent_info !== null ? jsonUnknown(thread.agent_info) : null, + current_context_usage: jsonUnknown(thread.current_context_usage), context_ids: contextIds, updated_at: new Date(turn.updated_at), }) @@ -118,7 +114,7 @@ export class PostgresSessionSnapshotImporter implements ISessionSnapshotImporter turn_id: turn.turn_id, thread_id: thread.thread_id, key, - state: json(state), + state: jsonUnknown(state), updated_at: new Date(turn.updated_at), })), ) @@ -135,7 +131,7 @@ export class PostgresSessionSnapshotImporter implements ISessionSnapshotImporter session_id: sessionId, turn_id: turn.turn_id, event_id: event.id, - event: json(event), + event: jsonUnknown(event), created_at: new Date(event.created_at), })), ) diff --git a/packages/trueforge/src/db/postgres/sqlExpressions.ts b/packages/trueforge/src/db/postgres/sqlExpressions.ts index 7d2b7730f..c1e956ebc 100644 --- a/packages/trueforge/src/db/postgres/sqlExpressions.ts +++ b/packages/trueforge/src/db/postgres/sqlExpressions.ts @@ -3,14 +3,19 @@ */ import { sql, type Expression, type RawBuilder } from 'kysely'; -/** - * Bind a JS value as jsonb (stringified + cast). Required for arrays and for `||` / jsonb_set operands. - * `T` is taken from the call-site column type (loose ops payloads are `unknown` at the wire). - */ -export function json(value: unknown): RawBuilder { +function asJsonb(value: unknown): RawBuilder { return sql`${JSON.stringify(value)}::jsonb`; } +/** Bind a JS value as jsonb (stringified + cast). Required for arrays and for `||` / jsonb_set operands. */ +export function json(value: T): RawBuilder { + return asJsonb(value); +} + +export function jsonUnknown(value: unknown): RawBuilder { + return asJsonb(value); +} + /** * `jsonb_set(target, path, new_value)`. * `path` may be a text[] expression (`sql\`ARRAY['threads', ${id}]\``) or a literal path diff --git a/packages/trueforge/src/db/sessionSnapshotImport.ts b/packages/trueforge/src/db/sessionSnapshotImport.ts index 2cc791499..68a83b47a 100644 --- a/packages/trueforge/src/db/sessionSnapshotImport.ts +++ b/packages/trueforge/src/db/sessionSnapshotImport.ts @@ -3,11 +3,8 @@ */ import type { ImportSessionSnapshotRequest, ImportSessionSnapshotResult } from '../schemas/sessionImport'; -export type ImportSessionSnapshotInput = ImportSessionSnapshotRequest; -export type { ImportSessionSnapshotResult }; - export interface ISessionSnapshotImporter { - importSessionSnapshot(input: ImportSessionSnapshotInput): Promise; + importSessionSnapshot(input: ImportSessionSnapshotRequest): Promise; } export function isContextPrefix({ prefix, full }: { prefix: unknown[]; full: unknown[] }): boolean { diff --git a/packages/trueforge/src/schemas/sessionImport.ts b/packages/trueforge/src/schemas/sessionImport.ts index 154717e20..187c75982 100644 --- a/packages/trueforge/src/schemas/sessionImport.ts +++ b/packages/trueforge/src/schemas/sessionImport.ts @@ -19,7 +19,7 @@ export const ImportSessionSnapshotRequestSchema = z created_at: z.string().min(1), updated_at: z.string().min(1), }) - .passthrough(), + .loose(), turns: z .array( z @@ -48,7 +48,7 @@ export const ImportSessionSnapshotRequestSchema = z agent_info: z.unknown().nullable(), capability_state: z.record(z.string(), z.unknown()).nullable(), }) - .passthrough(), + .loose(), ), events: z.array( z @@ -56,24 +56,28 @@ export const ImportSessionSnapshotRequestSchema = z id: z.string().min(1), created_at: z.string().min(1), }) - .passthrough(), + .loose(), ), }) - .passthrough(), + .loose(), ) .min(1), }) .openapi('ImportSessionSnapshotRequest'); +export const ImportSessionSnapshotResultSchema = z + .object({ + imported: z.boolean(), + session_id: z.string(), + }) + .openapi('ImportSessionSnapshotResult'); + export const ImportSessionSnapshotResponseSchema = z .object({ - data: z.object({ - imported: z.boolean(), - session_id: z.string(), - }), + data: ImportSessionSnapshotResultSchema, }) .openapi('ImportSessionSnapshotResponse'); export type ImportSessionSnapshotRequest = z.infer; +export type ImportSessionSnapshotResult = z.infer; export type ImportSessionSnapshotResponse = z.infer; -export type ImportSessionSnapshotResult = ImportSessionSnapshotResponse['data']; diff --git a/packages/trueforge/tests/unit/apis/modelProviders.test.ts b/packages/trueforge/tests/unit/apis/modelProviders.test.ts index 29247b273..b206c3b38 100644 --- a/packages/trueforge/tests/unit/apis/modelProviders.test.ts +++ b/packages/trueforge/tests/unit/apis/modelProviders.test.ts @@ -99,6 +99,7 @@ async function createRouters(): Promise<{ tokenStore: new SqliteOAuthTokenStore(db), skillStore: new SqliteSkillStore(db), sandboxProviderStore: new SqliteSandboxProviderStore(db), + sessionSnapshotImporter: undefined, withTransaction: callback => db.transaction().execute(callback), logger: winston.createLogger({ silent: true }), resolveUserContext: () => LOCAL_USER_CONTEXT, From 11510b8eccb02458f17c120ece116004ab4d2d1b Mon Sep 17 00:00:00 2001 From: "trueforge-dev-bot[bot]" Date: Fri, 28 Aug 2026 11:11:18 +0000 Subject: [PATCH 5/5] Regenerate OpenAPI document and TypeScript SDK --- .github/fern/openapi/openapi.json | 29 ++++++++++++++++------------- docs/openapi.json | 29 ++++++++++++++++------------- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/.github/fern/openapi/openapi.json b/.github/fern/openapi/openapi.json index 14412dd3d..1aaf1c514 100644 --- a/.github/fern/openapi/openapi.json +++ b/.github/fern/openapi/openapi.json @@ -1595,19 +1595,7 @@ "ImportSessionSnapshotResponse": { "properties": { "data": { - "properties": { - "imported": { - "type": "boolean" - }, - "session_id": { - "type": "string" - } - }, - "required": [ - "imported", - "session_id" - ], - "type": "object" + "$ref": "#/components/schemas/ImportSessionSnapshotResult" } }, "required": [ @@ -1615,6 +1603,21 @@ ], "type": "object" }, + "ImportSessionSnapshotResult": { + "properties": { + "imported": { + "type": "boolean" + }, + "session_id": { + "type": "string" + } + }, + "required": [ + "imported", + "session_id" + ], + "type": "object" + }, "InitialUserMessage": { "properties": { "content": { diff --git a/docs/openapi.json b/docs/openapi.json index 14412dd3d..1aaf1c514 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -1595,19 +1595,7 @@ "ImportSessionSnapshotResponse": { "properties": { "data": { - "properties": { - "imported": { - "type": "boolean" - }, - "session_id": { - "type": "string" - } - }, - "required": [ - "imported", - "session_id" - ], - "type": "object" + "$ref": "#/components/schemas/ImportSessionSnapshotResult" } }, "required": [ @@ -1615,6 +1603,21 @@ ], "type": "object" }, + "ImportSessionSnapshotResult": { + "properties": { + "imported": { + "type": "boolean" + }, + "session_id": { + "type": "string" + } + }, + "required": [ + "imported", + "session_id" + ], + "type": "object" + }, "InitialUserMessage": { "properties": { "content": {