Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions db/migrations/00000000000000_baseline.sql
Original file line number Diff line number Diff line change
Expand Up @@ -223,9 +223,8 @@
-- DROP TABLE public.organization_lobu_links_d20260519;
-- -- ... etc.
--
-- Fresh DBs (local dev, PGlite, CI): no surgery, no backups; dbmate
-- applies this file from scratch. Wipe local PGlite dirs
-- (`rm -rf <workspace>/data`) to take advantage of the squash.
-- Fresh DBs (local dev, CI): no surgery, no backups; dbmate
-- applies this file from scratch.
-- =============================================================================

--
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/scripts/build.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ if (fs.existsSync(catalogManifestSrc) && fs.existsSync("dist/connectors")) {
fs.cpSync(catalogManifestSrc, "dist/connectors/.catalog-manifest.json");
}

// Copy database migrations for the bundled PGlite local server.
// Copy database migrations for the bundled embedded-Postgres local server.
copyDirIfExists("../../db/migrations", "dist/db/migrations");

// Copy the built owletto web UI (admin/console SPA) next to the server bundle
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/__tests__/dev.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ describe("lobu run backend bundle resolution", () => {
).toBe(false);
});

test("no effective URL means no refusal (PGlite path)", () => {
test("no effective URL means no refusal (embedded-Postgres path)", () => {
expect(
shouldRefuseSharedDatabaseUrl({
effectiveDatabaseUrl: undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ This page compares Lobu against other ways to run agents for multiple users.
| **Multi-tenant** | Per-user/channel isolation | Single user | Per-thread sandbox | Per-conversation |
| **Platforms** | Slack, Telegram, WhatsApp, Discord, Teams, Google Chat, REST API | CLI and API | API endpoints (MCP, A2A, Agent Protocol) | API |
| **Embeddable** | Mount inside Next.js, Express, Hono, Fastify | No | No | No |
| **Self-hosted** | Single Node process (bundled PGlite, or BYO Postgres) | Single process | LangSmith hosted (self-host option) | Cloud only |
| **Self-hosted** | Single Node process (embedded Postgres, or BYO Postgres) | Single process | LangSmith hosted (self-host option) | Cloud only |
| **Model support** | Any provider via config | Any provider | Any LangChain-compatible provider | Anthropic only |
| **Runtime** | OpenClaw | OpenClaw | LangGraph | Claude |
| **Network isolation** | Gateway-mediated egress, domain filtering | Host network | Sandbox-level | Platform-managed |
Expand Down Expand Up @@ -129,7 +129,7 @@ OpenClaw (~800k LOC) was designed as a **single-tenant, single-user system**. Pr
| Secret handling | Gateway proxy injects credentials | Direct env vars |
| Egress control | Domain allowlists via HTTP proxy | Host network |
| Worker lifecycle | Persistent subprocess per channel | Always running |
| Deployment | Single Node process (bundled PGlite or BYO Postgres) | Single process |
| Deployment | Single Node process (embedded Postgres or BYO Postgres) | Single process |

Inside each Lobu worker, the full OpenClaw runtime runs untouched. Lobu rewrites only the gateway layer (~40k LOC) to be multi-tenant.

Expand Down
4 changes: 2 additions & 2 deletions packages/landing/src/content/docs/getting-started/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ Lobu is the open-source backend for multi-user AI agents. It gives every user or

## Quick start

Lobu boots as a single Node process. By default it uses local PGlite with pgvector enabled, so you can start without Docker or a separate Postgres. Set `DATABASE_URL` only when you want to use an external Postgres.
Lobu boots as a single Node process. By default it uses an embedded Postgres (PG18 + pgvector), so you can start without Docker or a separate Postgres. Set `DATABASE_URL` only when you want to use an external Postgres.

```bash
# 1. Scaffold
npx @lobu/cli@latest init my-agent
cd my-agent

# 2. Boot locally (uses PGlite by default)
# 2. Boot locally (uses embedded Postgres by default)
npx @lobu/cli@latest run

# 3. Chat with your agent (in another terminal)
Expand Down
2 changes: 1 addition & 1 deletion packages/landing/src/content/docs/guides/architecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Lobu is embedded-only: the gateway, agent workers, the embeddings model, and the

- **Gateway**: orchestration, OAuth, secrets, domain policy, routing — all in the host Node process.
- **Worker**: model execution, tools, workspace state — a sandboxed subprocess that never sees real credentials.
- **Postgres (with pgvector)**: the only external dependency Lobu ever needs. Holds the run queue, agent settings, grants, secrets, chat history, and MCP proxy sessions. Scaffolded projects (`lobu init` / `lobu run`) default to an **in-process PGlite** database, so `DATABASE_URL` is optional there; only the monorepo `make dev` requires an external Postgres. There is no Redis anywhere.
- **Postgres (with pgvector)**: the only external dependency Lobu ever needs. Holds the run queue, agent settings, grants, secrets, chat history, and MCP proxy sessions. Scaffolded projects (`lobu init` / `lobu run`) default to an **embedded Postgres** (PG18 + pgvector, runs in-process), so `DATABASE_URL` is optional there; only the monorepo `make dev` requires an external Postgres. There is no Redis anywhere.

## Persistent Memory

Expand Down
10 changes: 5 additions & 5 deletions packages/landing/src/content/docs/guides/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: Troubleshooting
description: Common issues and how to fix them.
---

Lobu boots as a single Node process: `lobu run`. A scaffolded project defaults to an **in-process PGlite** database (entry `start-local.bundle.mjs`), so `DATABASE_URL` is optional — set it only when you want an external Postgres (with pgvector), in which case the entry is `server.bundle.mjs`. The monorepo `make dev` always requires `DATABASE_URL`. Worker subprocesses are spawned by the gateway's `EmbeddedDeploymentManager`. There is no Redis.
Lobu boots as a single Node process: `lobu run`. A scaffolded project defaults to an **embedded Postgres** (PG18 + pgvector, entry `start-local.bundle.mjs`), so `DATABASE_URL` is optional — set it only when you want an external Postgres, in which case the entry is `server.bundle.mjs`. The monorepo `make dev` always requires `DATABASE_URL`. Worker subprocesses are spawned by the gateway's `EmbeddedDeploymentManager`. There is no Redis.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace em dashes in landing copy on these updated lines.

Please replace with alternative punctuation (e.g., comma/period/parentheses) in the changed user-facing text at Line 6 and Line 143.

As per coding guidelines, "Remove em dashes from user-facing text in landing copy".

Also applies to: 143-143

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/landing/src/content/docs/guides/troubleshooting.md` at line 6, The
landing copy contains em dashes that must be removed in user-facing text; update
the sentences in the described markdown (the line containing "so `DATABASE_URL`
is optional — set it only when you want an external Postgres..." and the other
affected line at 143) by replacing the em dash (—) with appropriate punctuation
(e.g., a comma, period, or parentheses) so the text reads naturally without em
dashes; edit the markdown lines in
packages/landing/src/content/docs/guides/troubleshooting.md accordingly (locate
the sentence with "start-local.bundle.mjs" / "server.bundle.mjs" and the
sentence at line 143) and ensure grammar/spacing remains correct after the
replacement.


## Worker won't start

Expand All @@ -22,7 +22,7 @@ make clean-workers # in the monorepo
# Common causes:
# - Port 8787 already in use → Change GATEWAY_PORT or PORT in .env
# - DATABASE_URL set but not reachable → see "Agent not responding" below
# (with the default PGlite backend there's no DATABASE_URL to misconfigure)
# (with the default embedded-Postgres backend there's no DATABASE_URL to misconfigure)
# - Invalid lobu.config.ts → npx @lobu/cli@latest validate
```

Expand All @@ -33,7 +33,7 @@ make clean-workers # in the monorepo
curl http://localhost:8787/health

# If you've configured an external Postgres, check the connection
# (skip this with the default in-process PGlite backend)
# (skip this with the default embedded-Postgres backend)
psql "$DATABASE_URL" -c 'select 1'

# Clear stale chat history (for stuck conversations).
Expand Down Expand Up @@ -104,7 +104,7 @@ curl -v http://localhost:8118

```bash
# Check Node process memory (the entry is server.bundle.mjs with an external
# Postgres, or start-local.bundle.mjs with the default PGlite backend)
# Postgres, or start-local.bundle.mjs with the default embedded-Postgres backend)
ps -o pid,rss,command -p "$(pgrep -f '(server|start-local)\.bundle\.mjs')"

# Workspaces accumulate per agent under ./workspaces/
Expand Down Expand Up @@ -140,7 +140,7 @@ npx @lobu/cli@latest memory health

## Postgres not reachable

Only relevant if you've configured an external Postgres via `DATABASE_URL` — the default scaffolded backend is in-process PGlite and has nothing to connect to.
Only relevant if you've configured an external Postgres via `DATABASE_URL` — the default scaffolded backend is an embedded Postgres and has nothing to connect to.

```bash
# Verify DATABASE_URL in .env
Expand Down
6 changes: 3 additions & 3 deletions packages/landing/src/content/docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@ Generates:
- `*.reaction.ts` — watcher reaction scripts, referenced via `defineWatcher({ reaction })`
- `AGENTS.md`, `TESTING.md`, `README.md`, `.gitignore`

Interactive prompts guide you through provider, platform, network access policy, gateway port, public URL, and memory configuration. Local runs use bundled PGlite by default; set `DATABASE_URL` when you want to use external Postgres with pgvector.
Interactive prompts guide you through provider, platform, network access policy, gateway port, public URL, and memory configuration. Local runs use an embedded Postgres (PG18 + pgvector) by default; set `DATABASE_URL` when you want to use an external Postgres.

---

### `run` (aliases: `dev`, `start`)

Run the embedded Lobu stack. `lobu.config.ts` is not required. With no `DATABASE_URL`, the command starts bundled local PGlite and stores data under `~/.lobu/data` (override with `LOBU_DATA_DIR`). If `DATABASE_URL` is set in the environment or `.env`, Lobu uses that external Postgres instead.
Run the embedded Lobu stack. `lobu.config.ts` is not required. With no `DATABASE_URL`, the command starts an embedded Postgres (PG18 + pgvector) and stores data under `~/.lobu/data` (override with `LOBU_DATA_DIR`). If `DATABASE_URL` is set in the environment or `.env`, Lobu uses that external Postgres instead.

```bash
npx @lobu/cli@latest run
Expand Down Expand Up @@ -324,6 +324,6 @@ cd my-agent
npx @lobu/cli@latest validate
npx @lobu/cli@latest apply --org my-org

# 4. Run locally (PGlite by default; external Postgres if DATABASE_URL is set)
# 4. Run locally (embedded Postgres by default; external Postgres if DATABASE_URL is set)
npx @lobu/cli@latest run
```
2 changes: 1 addition & 1 deletion packages/owletto
2 changes: 1 addition & 1 deletion packages/server/scripts/build-server-bundle.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* External: bare specifiers stay external (loaded from node_modules at
* runtime). Published `@lobu/cli` declares those runtime dependencies
* directly so Node's resolver finds them from the CLI install. Native addons
* (isolated-vm), PGlite native/WASM assets, and packages with
* (isolated-vm), embedded-postgres + pgvector binaries, and packages with
* require-in-the-middle hooks (Sentry, OpenTelemetry, pino) MUST stay external
* to keep their runtime hooks working.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* needs no external Postgres, exactly like `lobu run`. Same binary + pgvector
* injection as the production embedded path (src/embedded-runtime.ts), so tests
* exercise the real engine (prepared statements, multi-conn pool, LISTEN/NOTIFY,
* cube/earthdistance, pgvector) with no PGlite-specific quirks.
* cube/earthdistance, pgvector).
*/

import { mkdtempSync, rmSync } from 'node:fs';
Expand Down
4 changes: 2 additions & 2 deletions packages/server/src/__tests__/setup/test-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export async function setupTestDatabase(): Promise<void> {
//
// Postgres 15+ removed the implicit CREATE privilege on schema `public` from
// the `public` role: only the schema OWNER can run DDL there (including
// DROP/CREATE SCHEMA). PGlite and superuser/owner connections can recreate
// DROP/CREATE SCHEMA). Embedded Postgres and superuser/owner connections can recreate
// the whole schema; a plain (non-owner) connection user against a real PG15+
// server cannot, and `DROP SCHEMA IF EXISTS public CASCADE` fails with
// `must be owner of schema public`. Reset gracefully across all three.
Expand Down Expand Up @@ -196,7 +196,7 @@ export async function setupTestDatabase(): Promise<void> {
/**
* Reset schema `public` to a clean slate, working whether the connection user
* owns the schema or not. Returns whether the connection user ends up owning
* `public` (true on PGlite / superuser / schema-owner connections).
* `public` (true on embedded Postgres / superuser / schema-owner connections).
*
* Preferred path (owner / superuser): take ownership of `public` if we can,
* then DROP/CREATE the whole schema — the historical behaviour, which also
Expand Down
27 changes: 4 additions & 23 deletions packages/server/src/auth/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -635,22 +635,10 @@ export async function createAuth(env: Env, request?: Request) {
user: {
create: {
before: async (user, ctx) => {
// Single-user-mode chokepoint. The /api/auth/* URL filter
// in index.ts blocks /api/auth/sign-up/*, but Better Auth
// also creates users on magic-link verify and OAuth
// callbacks — paths the URL guard never sees. This hook
// fires before every user INSERT, so it's the one place
// that closes the fork-via-magic-link / fork-via-OAuth
// backdoor.
//
// The count goes through ctx.internalAdapter so it joins
// the in-flight transaction connection. Calling getDb()
// here would request a second pool connection while
// sign-up's runWithTransaction holds the only one —
// deadlock in PGlite mode (pool max=1). See #947.
// Missing ctx (called outside the BA endpoint pipeline)
// throws via `ctx!` → BA returns FAILED_TO_CREATE_USER,
// which is the fail-closed posture we want.
// Single-user-mode chokepoint. The URL filter in index.ts
// blocks /api/auth/sign-up/*, but Better Auth also creates
// users on magic-link verify and OAuth callbacks; this hook
// fires before every user INSERT.
if (env.LOBU_SINGLE_USER === "1") {
// Exclude the synthetic install_operator row
// (auto-provisioned by ensureInstallOperator) so the
Expand Down Expand Up @@ -757,13 +745,6 @@ export async function createAuth(env: Env, request?: Request) {
// password-hash row at boot. See
// docs/install-operator-bootstrap.md.
if (account.providerId !== "credential") {
// Route through ctx.internalAdapter so the lookup
// shares the in-flight transaction connection on the
// one path that wraps in runWithTransaction —
// createOAuthUser, called from OAuth callback for new
// users. Avoids the PGlite pool-max=1 deadlock; see
// #947. `/link-social` and existing-user callback
// links aren't transactional today but stay safe.
const linkedUser =
await ctx!.context.internalAdapter.findUserById(account.userId);
const principalKind = (
Expand Down
4 changes: 2 additions & 2 deletions packages/server/src/gateway/__tests__/turn-liveness.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* Integration tests for turn-liveness (#946) against a real Postgres (PGlite in
* CI). Exercises the durable election marker end to end: arm, discharge,
* Integration tests for turn-liveness (#946) against a real Postgres (embedded
* PG18 in CI). Exercises the durable election marker end to end: arm, discharge,
* fast-path failure, the first-writer-wins election (failTurnIfPending),
* atomic terminal-reply commit, the deadline sweep + exactly-once, and the
* globally-unique (deploymentName:messageId) marker key.
Expand Down
7 changes: 1 addition & 6 deletions packages/server/src/gateway/orchestration/turn-liveness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,16 +229,11 @@ export async function sweepExpiredTurns(
try {
const sql = getDb();
const failed = await sql.begin(async (tx: DbClient) => {
// Literal queue_name (no bound params) on the claim to avoid a PGlite
// quirk where parameterized RETURNING statements intermittently report a
// parameter-count mismatch (see runs-queue stale sweep). The emit insert
// below DOES use params but has no RETURNING, so it's unaffected.
const rows = await tx.unsafe<{ action_input: unknown }>(
// status + run_type match the partial predicate and leading column of
// `runs_lobu_claim_idx`, so the inner SELECT is an index range scan
// (run_type, queue_name, …, run_at) — not a full scan of `runs` (which
// retains 30 days of completed rows). Literals only (no bound params)
// to dodge the PGlite parameterized-RETURNING quirk.
// retains 30 days of completed rows).
`DELETE FROM public.runs
WHERE id IN (
SELECT id FROM public.runs
Expand Down
15 changes: 7 additions & 8 deletions packages/server/src/lobu/agent-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1070,10 +1070,10 @@ const STABLE_PLATFORM_LOCK_NAMESPACE = 0x73746263; // "stbc"
/**
* FNV-1a 32-bit hash of the stable ID. Computed in JS (rather than calling
* Postgres's `hashtext()`) so the parameter passed to pg_advisory_xact_lock
* is a plain int — postgres-js's parameter type inference and PGlite's
* extended-query plan cache get tangled when nesting `hashtext(text)::int`
* inside the lock's `(int, int)` signature. The deterministic JS hash gives
* us the same contract: same stable ID → same lock key.
* is a plain int — postgres-js's parameter type inference gets tangled
* when nesting `hashtext(text)::int` inside the lock's `(int, int)`
* signature. The deterministic JS hash gives us the same contract: same
* stable ID → same lock key.
*/
function hashStableId(stableId: string): number {
let hash = 0x811c9dc5;
Expand Down Expand Up @@ -1112,17 +1112,16 @@ const stablePlatformLockChains: Map<string, Promise<unknown>> = new Map();
*
* Wrapping the whole flow in a single `sql.begin(...)` is not viable: the
* tx connection plus parent-pool writes via `connectionStore` /
* `chatManager.addConnection` would self-deadlock both on the pglite-mode
* serialized-client queue and on row-level locks against an uncommitted
* placeholder row from a different connection.
* `chatManager.addConnection` would self-deadlock on row-level locks
* against an uncommitted placeholder row from a different connection.
*/
async function withStablePlatformLock<T>(stableId: string, fn: () => Promise<T>): Promise<T> {
const lockKey = hashStableId(stableId);
const previous = stablePlatformLockChains.get(stableId) ?? Promise.resolve();
const work = previous.then(async () => {
// Touch the DB-side advisory lock so multi-host writers serialize too.
// Inlined via unsafe() because the `(int, int)` overload confuses
// PGlite's extended-query plan cache when other queries on the same
// postgres-js's parameter type inference when other queries on the same
// backend cycle through different parameter type oids; both inputs are
// validated int32s, no SQL injection surface. Failure here is non-fatal —
// the in-process chain still serializes for the embedded case.
Expand Down
2 changes: 1 addition & 1 deletion packages/server/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default defineConfig({
],
testTimeout: 30_000,
hookTimeout: 60_000,
// Integration tests share one Postgres/PGlite. Running multiple files in
// Integration tests share one Postgres. Running multiple files in
// parallel means one file's `cleanupTestDatabase()` can wipe another file's
// fixtures mid-run. Serialize files so fixtures stay stable.
pool: "forks",
Expand Down
Loading