diff --git a/sdk/api-reference/openhands.sdk.conversation.mdx b/sdk/api-reference/openhands.sdk.conversation.mdx index b5e99911f..518429b39 100644 --- a/sdk/api-reference/openhands.sdk.conversation.mdx +++ b/sdk/api-reference/openhands.sdk.conversation.mdx @@ -566,6 +566,11 @@ Initialize the conversation. override by key (last wins), hooks concatenate (all run). * `persistence_dir` – Directory for persisting conversation state and events. Can be a string path or Path object. + * `file_store` – Optional custom FileStore for persisting conversation state + and events. Use this to supply a PostgreSQLFileStore (or any FileStore + implementation) instead of the default local-filesystem store. + Mutually exclusive with `persistence_dir`; when `file_store` is provided, + `persistence_dir` is ignored. * `conversation_id` – Optional ID for the conversation. If provided, will be used to identify the conversation. The user might want to suffix their persistent filestore with this ID. diff --git a/sdk/guides/convo-persistence.mdx b/sdk/guides/convo-persistence.mdx index d2f520f0c..4567e41fb 100644 --- a/sdk/guides/convo-persistence.mdx +++ b/sdk/guides/convo-persistence.mdx @@ -377,6 +377,72 @@ Events are appended incrementally (one file per event), while base state is over +## PostgreSQL-backed Persistence + +In environments where the local filesystem is not persistent across requests (Cloud Run, +containers without mounted volumes, serverless), `LocalFileStore` is unavailable. +`PostgreSQLFileStore` stores the full EventLog — including `tool_call` and `tool_result` +events — in a PostgreSQL table, enabling proper multi-turn resume without a shared filesystem. + +### Installation + +```bash +pip install "openhands-sdk[postgresql]" +``` + +### Usage + +```python focus={1,7-10,14} icon="python" +from openhands.sdk.io.postgresql import PostgreSQLFileStore + +dsn = "postgresql://user:pass@host:5432/db" + +# Each conversation gets its own namespace to isolate event logs. +for request in incoming_requests: + store = PostgreSQLFileStore( + dsn=dsn, + namespace=str(request.conversation_id), + ) + conv = LocalConversation( + agent=agent, + workspace=workspace, + file_store=store, + conversation_id=request.conversation_id, + ) + conv.send_message(request.message) + conv.run() +``` + +On the first call, `PostgreSQLFileStore` creates the `sdk_filestore` table if it doesn't exist +and persists all events. On subsequent calls with the same `namespace`, the SDK reads the +existing EventLog from PostgreSQL and resumes from the exact point where the conversation left off. + +### Schema + +`PostgreSQLFileStore` manages a single table: + +```sql +CREATE TABLE sdk_filestore ( + namespace TEXT NOT NULL, -- conversation_id + path TEXT NOT NULL, -- event file path (e.g. .events/event-0000000000-.json) + content TEXT NOT NULL, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (namespace, path) +); +``` + +### Locking + +`PostgreSQLFileStore` uses a per-path `threading.Lock` to serialize EventLog index assignment. +This is sufficient for single-process deployments. For multi-process deployments, subclass +`PostgreSQLFileStore` and override `lock()` with PostgreSQL advisory locks +(`pg_try_advisory_lock`). + + + `file_store` and `persistence_dir` are mutually exclusive. When `file_store` is provided, + `persistence_dir` is ignored. + + ## Next Steps - **[Pause and Resume](/sdk/guides/convo-pause-and-resume)** - Control execution flow