deer-flow/backend/packages/harness/deerflow/config/checkpointer_config.py
Hyeonsang Cho b37f769b67
fix(persistence): reject SQLite file: URIs instead of opening them as filenames (#6069)
* fix(persistence): reject SQLite file: URIs instead of opening them as filenames

resolve_sqlite_conn_str passed a file: connection string through
unchanged, but LangGraph's SQLite checkpointer and Store factories
(SqliteSaver, SqliteStore, AsyncSqliteSaver, AsyncSqliteStore) connect
without uri=True, so SQLite treated the whole URI as a literal filename:
file:checkpoints.db?mode=rwc created a file with that exact name in the
working directory, file::memory:?cache=shared persisted to disk, and a
file:/// URI failed to open. The readiness probe did open URIs with
uri=True, so it checked a different file than the runtime used and
reported in-memory URIs as not_configured.

Reject file: URIs in resolve_sqlite_conn_str, the one helper all four
factories go through, with an error that names
checkpointer.connection_string. The probe drops its URI parsing and
reports a rejected connection string as unreachable.

* docs(changelog): reference #6069

* test(persistence): pin the case-sensitive file: URI check
2026-09-30 09:40:02 +08:00

77 lines
2.8 KiB
Python

"""Configuration for LangGraph checkpointer."""
from typing import Literal
from pydantic import BaseModel, Field, field_validator
from deerflow.config.postgres_schema import POSTGRES_SCHEMA_PATTERN, validate_postgres_schema
CheckpointerType = Literal["memory", "sqlite", "postgres"]
class CheckpointerConfig(BaseModel):
"""Configuration for LangGraph state persistence checkpointer."""
type: CheckpointerType = Field(
description="Checkpointer backend type. "
"'memory' is in-process only (lost on restart). "
"'sqlite' persists to a local file (requires langgraph-checkpoint-sqlite). "
"'postgres' persists to PostgreSQL (install with deerflow-harness[postgres])."
)
connection_string: str | None = Field(
default=None,
description="Connection string for sqlite (file path) or postgres (DSN). "
"Optional for sqlite and defaults to 'store.db' when omitted. "
"Required for postgres. "
"For sqlite, use a file path like '.deer-flow/checkpoints.db' or ':memory:' for in-memory; "
"SQLite 'file:' URIs are rejected. "
"For postgres, use a DSN like 'postgresql://user:pass@localhost:5432/db'.",
)
postgres_schema: str = Field(
default="",
description=(f"PostgreSQL schema for legacy checkpointer/store tables (postgres only). Empty string keeps the server default search_path (usually 'public'). Only plain identifiers are allowed: {POSTGRES_SCHEMA_PATTERN}."),
)
@field_validator("postgres_schema")
@classmethod
def _validate_postgres_schema(cls, value: str) -> str:
return validate_postgres_schema(value)
# Global configuration instance — None means no checkpointer is configured.
_checkpointer_config: CheckpointerConfig | None = None
def get_checkpointer_config() -> CheckpointerConfig | None:
"""Get the current checkpointer configuration, or None if not configured."""
return _checkpointer_config
def set_checkpointer_config(config: CheckpointerConfig | None) -> None:
"""Set the checkpointer configuration."""
global _checkpointer_config
_checkpointer_config = config
def ensure_config_loaded() -> None:
"""Lazily load app config when checkpointer config has not been initialized."""
from deerflow.config.app_config import _app_config, get_app_config
config = get_checkpointer_config()
if config is not None or _app_config is not None:
return
try:
get_app_config()
except FileNotFoundError:
pass
def load_checkpointer_config_from_dict(config_dict: dict | None) -> None:
"""Load checkpointer configuration from a dictionary."""
global _checkpointer_config
if config_dict is None:
_checkpointer_config = None
return
_checkpointer_config = CheckpointerConfig(**config_dict)