docs: add Postgres + D1 session persistence guides#87
docs: add Postgres + D1 session persistence guides#87ketankhairnar wants to merge 4 commits intowithastro:mainfrom
Conversation
4547547 to
6359cf2
Compare
|
Update on direction: @FredKSchott replied on the architectural question over in discussion #89 and gave the green light for "session persistence as a That changes this PR's natural home. The current diff puts the recipes under Two ways to land it:
(1) feels right — the connectors and the guides are coherent enough that splitting them adds friction without saving review time. Happy to do (2) if you'd rather review the category-introduction work in isolation. Will hold off on more changes here until you weigh in. The current diff is still useful as docs-only if (1) is the wrong call. |
|
definitely #1, thanks! |
6359cf2 to
2851f68
Compare
|
Restructured per @FredKSchott's "definitely #1" — the recipes now ship as What's new on the branch (two commits on top of the original):
Connector / doc / example consistency. The TS code block embedded in each connector is byte-identical to the example file at No CLI/runtime changes needed. Smoke-tested locally against a static file server (registry stand-in):
Aliases were dropped ( Branch is rebased onto current |
End-to-end proofBuilt Build outputCLI ↔ registry round-tripListing |
2851f68 to
1b1235b
Compare
Adds `SessionDelta` interface and optional `saveDelta?` method on
`SessionStore`. Adapters that implement it persist O(delta) per turn
instead of O(history). Existing implementations (in-memory, Cloudflare
DO) work unchanged because the method is optional.
The hook is non-breaking: if `saveDelta` is absent, the SDK falls
through to the existing `save(full)` path. If present, the SDK passes
only entries appended since the last save.
`SessionDelta` carries `{ newEntries, leafId, metadata }`. No
`supersedes` field in v1 — `appendCompaction` is append-only (it pushes
a new `CompactionEntry` without mutating prior history) so the
recipe-side adapter recovers prior entries from its own delta records on
load(). A `supersedes` field can be added later non-breakingly if a
future entry kind needs it.
Closes the SDK-side ask in withastro#89 (the recipe-side restructure landed as
part of withastro#87).
Two parallel session-persistence guides for Flue's two main deploy targets, both implementing the existing SessionStore contract: - docs/persist-postgres.md - Node deploys (Render, Fly, Railway, EC2) where the in-memory default loses state on restart. - docs/persist-d1.md - Cloudflare deploys where you want sessions queryable from outside the agent process (admin dashboards, separate Workers, BI exports). Complements the default DO-SQLite store rather than replacing it. Each guide ships: - A copy-pasteable store function (~50 lines) that adapts a user-owned client/binding to SessionStore. - The minimal CREATE TABLE schema, with `data` treated as opaque so Flue can evolve SessionData without breaking deployed schemas. - A "Schema choices" section calling out the append-log + index pattern Claude Code, Codex, and OpenCode 1.2+ converged on for long sessions, plus a hot/cold split, with use-when guidance. - Concurrency notes (last-writer-wins; sticky-routing or app-level locks for multi-instance workloads). - Worked example agents under examples/hello-world. D1 schema and the Cloudflare DO-SQLite store flue ships by default now share the same column types (id TEXT, data TEXT, updated_at INTEGER) so a future migration tool could move rows between them without translation. D1 store accepts `db: unknown` and casts internally, matching the convention `getVirtualSandbox(bucket: unknown)` already uses. Cross-linked from docs/deploy-node.md and docs/deploy-cloudflare.md.
Introduce a new connector category for session-store backends, alongside
the existing sandbox category. Two named connectors ship initially:
postgres and d1, both single-blob shape (one row per session, the entire
SessionData blob in one column, rewritten on every save).
Files:
connectors/persist.md — generic category root, addressable as
`flue add <url> --category persist`
connectors/persist--postgres.md — `flue add postgres`
connectors/persist--d1.md — `flue add d1`
Each named connector follows the same section structure as the sandbox
siblings: What this connector does → Where to write the file → File
contents → Required dependencies → Credentials → Schema → Wiring it into
an agent → Verify. The TS code block in each is byte-identical to the
example file that lives at examples/hello-world/.flue/persist/<name>.ts,
so the recipe and the example don't drift.
The connectors/README.md table is updated to declare persist as a
supported category and to document body conventions for both categories.
The CLI prebuild script (generate-connector-index.ts) requires no
changes — it's already category-agnostic. Smoke-tested against a local
file server: `flue add postgres`, `flue add d1`, and
`flue add <url> --category persist` all resolve and pipe correctly.
Restructure the Postgres and D1 persistence guides to match the shape of the sibling sandbox how-tos (e.g. docs/connect-daytona.md): prerequisites → install the connector → use the store → verify, with deeper reference material (schema choices, concurrency, troubleshooting) following the verify section instead of interleaving with setup. The recipe content that was inlined into these guides previously now lives in the connector files (connectors/persist--postgres.md, connectors/persist--d1.md) and is fetched via `flue add postgres` / `flue add d1`. The docs link to the connectors and stay focused on the how-to: when you'd want this, what to install, how to verify it, what to revisit when scaling. Schema-choice analysis (single-blob vs append-log + index vs hot/cold split) is preserved as reference material — it explains the trade-off the connectors are making, not part of the install path.
1b1235b to
1a9ed4b
Compare
|
Rebased onto current
Verification re-ran clean:
Sibling-PR note: #95 (the |
|
Dry-run update for this branch: Used a real Postgres 16 Docker container on a unique local port: docker run -d --rm --name flue-pg-smoke-55432 \
-e POSTGRES_PASSWORD=flue \
-p 127.0.0.1:55432:5432 \
postgres:16Readiness check passed: What passed:
Live Postgres smoke result: {"id":"smoke-1778389552223","savedEntries":2,"deleted":true}D1-like smoke result: {"id":"d1-smoke-1778389576157","savedEntries":1,"deleted":true}Live LLM resume smoke: Turn 1: {
"threadId": "llm-pg-smoke-2",
"response": "OK"
}Turn 2: {
"threadId": "llm-pg-smoke-2",
"response": "FLUE-CERULEAN-55432"
}Postgres inspection included: Notes / caveats:
Remaining review concern from the dry run: the single-blob recipes round-trip correctly. This does not prove the append-log guidance is safe against the current SDK history-deletion path during overflow recovery. |
|
Follow-up fix pushed to What changed after the review pass:
Verification was not dry-run-only:
Additional local verification:
|
Adds `SessionDelta` interface and optional `saveDelta?` method on
`SessionStore`. Adapters that implement it persist O(delta) per turn
instead of O(history). Existing implementations (in-memory, Cloudflare
DO) work unchanged because the method is optional.
The hook is non-breaking: if `saveDelta` is absent, the SDK falls
through to the existing `save(full)` path. If present, the SDK passes
only entries appended since the last save.
`SessionDelta` carries `{ newEntries, leafId, metadata }`. No
`supersedes` field in v1 — `appendCompaction` is append-only (it pushes
a new `CompactionEntry` without mutating prior history) so the
recipe-side adapter recovers prior entries from its own delta records on
load(). A `supersedes` field can be added later non-breakingly if a
future entry kind needs it.
Closes the SDK-side ask in withastro#89 (the recipe-side restructure landed as
part of withastro#87).
Adds `SessionDelta` interface and optional `saveDelta?` method on
`SessionStore`. Adapters that implement it persist O(delta) per turn
instead of O(history). Existing implementations (in-memory, Cloudflare
DO) work unchanged because the method is optional.
The hook is non-breaking: if `saveDelta` is absent, the SDK falls
through to the existing `save(full)` path. If present, the SDK passes
only entries appended since the last save.
`SessionDelta` carries `{ newEntries, leafId, metadata }`. No
`supersedes` field in v1 — `appendCompaction` is append-only (it pushes
a new `CompactionEntry` without mutating prior history) so the
recipe-side adapter recovers prior entries from its own delta records on
load(). A `supersedes` field can be added later non-breakingly if a
future entry kind needs it.
Closes the SDK-side ask in withastro#89 (the recipe-side restructure landed as
part of withastro#87).
Two parallel session-persistence guides for Flue's two main deploy targets, both implementing the existing
SessionStorecontract — no SDK changes.What this adds
docs/persist-postgres.md— Node deploys (Render, Fly, Railway, EC2) where the in-memory default loses state on restart. The existingdocs/deploy-node.md"Session persistence" section already gestures at this ("You can back this with any database: SQLite, Postgres, Redis, etc.") but had no worked example; this fills that gap.docs/persist-d1.md— Cloudflare deploys where you want sessions queryable from outside the agent process (admin dashboards, separate UI Workers, BI exports). Complements the default DO-SQLite store rather than replacing it.Each guide ships:
SessionStore.CREATE TABLEschema, withdatatreated as opaque so Flue can evolveSessionDatawithout breaking deployed schemas.examples/hello-world.Conventions matched
db: unknownand casts internally — matchesgetVirtualSandbox(bucket: unknown)(packages/sdk/src/cloudflare/virtual-sandbox.ts). No@cloudflare/workers-typesrequired to typecheck the recipe.connectors/category. The README explicitly asks contributors not to add categories without prior discussion, so the persistence work lives indocs/andexamples/instead — same shape asdocs/connect-daytona.md.@flue/sdk(bare) — matches Fred'swith-request.tsand the README support-agent example, not@flue/sdk/client.What I'm not changing
SessionStoreinterface itself — these recipes work with today's blob-overwrite shape. Filing a separate issue surfacing the append-log pattern that Claude Code, Codex, and OpenCode 1.2+ converged on, with a small opt-insaveDeltahook proposal — that's an architectural conversation, separate from these recipes.id TEXT, data TEXT, updated_at INTEGER) intentionally matches what the existingcreateDOStoreinbuild-plugin-cloudflare.ts:205creates — same column names, same types — so future tooling can move rows between the two without translation.The example workspace gains
pgand@types/pgas deps (matching the precedent of@daytona/sdkforwith-sandbox.ts).Verification
pnpm --filter @flue/sdk build✓pnpm --filter @flue/cli build✓flue build --target nodeon the example workspace registers both new agents ✓npx tsc --noEmitclean across the example workspace ✓Live smoke against a real Postgres / D1 — Docker daemon wasn't running locally; the SQL is canonical
INSERT ... ON CONFLICT DO UPDATEmirroringInMemorySessionStore's shape. Happy to verify with a docker-compose run if maintainers want it as part of review.Files
docs/persist-postgres.mddocs/persist-d1.mdexamples/hello-world/.flue/persist/postgres.tspostgresStore(client, options?)examples/hello-world/.flue/persist/d1.tsd1Store(db, options?)examples/hello-world/.flue/agents/with-postgres-persist.tsexamples/hello-world/.flue/agents/with-d1-persist.tsexamples/hello-world/package.jsonpgand@types/pgdocs/deploy-node.mddocs/deploy-cloudflare.mdCross-context: this builds on the conversation in #52 — when
flue.config.tslands with apersistfield, both stores work zero-migration viaflue.config.ts { persist: postgresStore(client) }because the API surface doesn't change.