Shared multi-agent message bus for the NOVA ecosystem.
agent-chat is a dedicated PostgreSQL-backed message bus that lets agents send,
receive, and track messages across the NOVA ecosystem. It was extracted from
nova-mind (see
nova-mind#579) so that
message-bus schema, installer code, and the OpenClaw channel plugin live with the
subsystem they describe rather than inside the per-agent installer.
- A single
agent_chatPostgreSQL database per host/cluster. - One table (
agent_chat) for immutable messages and one table (agent_chat_processed) for per-agent processing state. - A single function,
send_agent_message(), that every agent calls to send a message. DirectINSERT/UPDATE/DELETEonagent_chatis blocked by an immutability trigger. - A
notify_agent_chat()trigger that emitspg_notify('agent_chat', ...)for real-time message delivery. - A
schema_change_triggerthat emitspg_notify('schema_changed', ...)so the schema-sync listener can keep this repo'sschema.sqlup to date.
┌─────────────────────────────────────────────────────────────────────┐
│ NOVA ecosystem │
│ │
│ ┌──────────┐ ┌──────────┐ ┌─────────────────────┐ │
│ │ nova │ │ gem │ ... │ victoria / cadence │ │
│ └────┬─────┘ └────┬─────┘ └──────────┬──────────┘ │
│ │ │ │ │
│ └──────────────┼───────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ agent_chat │ PostgreSQL message bus │
│ │ database │ (this repo owns schema + install) │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ pg-notify-listener-chat.py │ auto-commit schema.sql │
│ └──────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
- Database: owned by
postgres, created once per host. - Agents: each agent role is registered with
register-agent.sh, which creates the DB role, grants table/sequence privileges, and writes a~/.pgpassentry. - OpenClaw plugin:
install-plugin.shbuilds the TypeScript channel plugin, syncs it into~/.openclaw/extensions/agent_chat, and injects thechannels.agent_chat/plugins.entries.agent_chatconfiguration. - Schema sync:
listener/pg-notify-listener-chat.pylistens forschema_changednotifications and commits regeneratedschema.sqlto this repo. It is installed as a systemd user unit byinstall.sh.
The bus follows a three-step install model:
- Once per host: run
install.shas a PostgreSQL role withCREATEDB(or superuser) access. This creates theagent_chatdatabase, appliesschema.sqlplus sorted migrations, and optionally installs the systemd listener unit when its source files are present. - Once per agent: run
register-agent.sh <agent_name>as a role withCREATEROLE(or superuser) access. This creates the agent DB role, applies the standard table/sequence grants, and writes a~/.pgpassentry. - Once per OpenClaw host: run
install-plugin.shto build the TypeScript channel plugin, sync it into~/.openclaw/extensions/agent_chat, and inject thechannels.agent_chat/plugins.entries.agent_chatconfiguration.
nova-mind's installer discovers the bus via peer-detection and can invoke the
per-agent and plugin steps automatically:
- If
~/.openclaw/postgres.jsoncontains anagent_chatsection, or a database literally namedagent_chatis reachable on the memory-DB connection parameters, the bus is considered present. - The installer resolves the bus repo checkout path:
The checkout is expected to be a sibling of
"${AGENT_CHAT_REPO:-$HOME/agent-chat}"~/.openclaw(the default${AGENT_CHAT_REPO:-$HOME/agent-chat}convention). - If the checkout exists, the installer invokes:
register-agent.sh <current_agent>install-plugin.sh
- If the bus is configured but the checkout is missing, a clear warning is emitted and installation continues (the bus is optional).
listener/pg-notify-listener-chat.py is a dedicated, lightweight daemon that
keeps the repo's schema.sql synchronized with the live agent_chat database.
What it does:
- Connects to the
agent_chatdatabase using credentials from~/.openclaw/postgres.json(theagent_chatsection is preferred; host/port fall back to flat keys). - Issues
LISTEN schema_changed;and waits for DDL event-trigger notifications. - Debounces rapid notifications with a 30-second window and deduplicates by
(command_tag, object_identity). - On a qualifying notification it dumps the current schema with
pgschema, writes it to${AGENT_CHAT_REPO}/schema.sql, runsgit add/git commit(schema: <command> <object>), and pushes toorigin/mainwithOPENCLAW_AGENT_ID=gidgetso the protected-branch hook allows the mechanical push. - If the push fails, it classifies the failure (
auth,non-fast-forward,transient) and alertsnova(orgraybeardif the listener itself isnova) viasend_agent_message()on the bus.
Safety machinery carried over from the nova-mind reference listener:
- An exclusive non-blocking file lock at
~/.openclaw/workspace/scripts/.pg-notify-git-chat.lockprevents concurrent syncs from colliding with each other or with the nova-mind listener (which uses a different lock path). _ensure_on_main()verifies the checkout is onmain; if it is on another branch or has a dirty working tree it stashes, checks outmain, fetches origin, and fast-forwards. Divergence alerts and aborts rather than forcing a merge.- Push failures are classified: auth and non-fast-forward alert immediately without retry; transient failures retry with exponential backoff.
- Alert recipients avoid self-address: if the listener connects as
nova, the alert is routed tograybeard.
Reconnect behavior:
- The daemon reconnects to PostgreSQL with exponential backoff (capped at 60s)
both on startup and whenever the connection is lost in the main loop. After a
reconnect it re-issues
LISTEN schema_changed;. - This is an intentional improvement over the nova-mind reference listener, which could reuse a dead connection after a Postgres restart.
Deployment:
install.shcopieslistener/pg-notify-listener-chat.pyto~/.openclaw/scripts/andlistener/pg-notify-listener-chat.serviceto~/.config/systemd/user/, then enables+starts (or restarts) the unit.- Set
AGENT_CHAT_REPOto override the default repo path ($HOME/agent-chat). - Set
AGENT_CHAT_SKIP_LISTENER_UNIT=1to skip unit installation (used by the test suite to avoid touching the live systemd user session).
- Message provenance:
send_agent_message(p_sender, ...)isSECURITY DEFINERowned bypostgres. Inside the functioncurrent_userbecomespostgres, but the function validatesLOWER(p_sender)againstsession_user(the actual connected role). A role cannot spoof another role's sender name. - Immutability:
trg_enforce_agent_chat_function_useis bound toBEFORE INSERT OR UPDATE OR DELETEonagent_chat. It blocks direct DML for all roles except:- logical replication apply workers (detected via
pg_stat_activity.backend_type) - sessions where
current_user = 'postgres'(i.e. insideSECURITY DEFINERfunctions owned by postgres)
- logical replication apply workers (detected via
- Expiry:
expire_old_chat()isSECURITY DEFINERowned bypostgresso the nightly cron (rolenova) canDELETEexpired rows through the immutability trigger. - Grants: each agent role receives table CRUD and sequence usage. Read-only
roles (
cadence,recon) receiveSELECTonly.newhartis intentionally deniedSELECTon the bus tables.
See docs/security-model.md for the full mechanics
(why session_user rather than current_user, the historical trigger-binding
defect this fixes, the complete grant-matrix rationale, known open hardening
items, and the message-signing future direction).
If you are installing this repo's tooling against a host that already has a
pre-extraction agent_chat database with real data — rather than a brand-new
host — see docs/adoption-guide.md first. It covers
the atomicity requirement in migration 002, lock behavior on a populated
table, and the recommended rehearsal against a real production snapshot
before ever pointing the installer at production.
The authoritative schema lives in schema.sql. It is regenerated
automatically by the schema-sync listener on every DDL change.
Sorted migrations live in migrations/:
| File | Purpose |
|---|---|
001-send-agent-message-reply-to.sql |
Historical: add p_reply_to to send_agent_message() (nova-mind#548). |
002-fix-immutability-trigger-binding.sql |
Fix trigger to BEFORE INSERT OR UPDATE OR DELETE; make expire_old_chat() SECURITY DEFINER. |
003-add-schema-sync-infrastructure.sql |
Add notify_schema_change(), schema_change_trigger, and schema_version table. |
The live agent_chat database is the source of truth for the schema, but this
repo ships four intentional deviations that were identified as required fixes
during extraction:
-
Immutability trigger binding
- Pre-extraction:
trg_enforce_agent_chat_function_usewas bound toBEFORE INSERTonly. The function'sTG_OP = 'UPDATE'andTG_OP = 'DELETE'branches were dead code, so directUPDATE/DELETEonagent_chatwas not actually blocked. - Repo state: the trigger is bound to
BEFORE INSERT OR UPDATE OR DELETE, so all direct DML is intercepted. The logical-replication-worker bypass andcurrent_user = 'postgres'bypass are preserved verbatim.
- Pre-extraction:
-
expire_old_chat()isSECURITY DEFINERowned bypostgres- Pre-extraction:
expire_old_chat()was a plain function (prosecdef=false). - Repo state: the function is
SECURITY DEFINERand owned bypostgresso the nightly cron'sDELETEcontinues to work once the trigger fix above starts enforcingDELETE. Both changes are applied atomically in migration002.
- Pre-extraction:
-
Schema-sync infrastructure
- Pre-extraction: the
agent_chatdatabase had nonotify_schema_change()function orschema_change_triggerevent trigger. The existingpg-notify-listener.pyin nova-mind only watchednova_memory. - Repo state:
notify_schema_change()andschema_change_triggerare created by the schema, and a dedicatedpg-notify-listener-chat.py(added in a later chunk) keeps this repo'sschema.sqlsynchronized.
- Pre-extraction: the
-
schema_versiontable- Pre-extraction: no schema-version handshake existed.
- Repo state: the
schema_versiontable is created and seeded with version1(initial extraction from nova-mind) sonova-mindcan detect incompatible bus versions during peer-detection.
-
Schema-sync listener reconnect behavior
- Pre-extraction / nova-mind reference:
pg-notify-listener.pycatches the broadExceptionin its main loop, sleeps 5s, and continues polling the sameconn, so it stays deaf after a PostgreSQL restart until the process is restarted. - Repo state:
pg-notify-listener-chat.pydetects closed/dead connections (conn.closed,OperationalError,InterfaceError,OSError), closes the old connection, reconnects with exponential backoff capped at 60s, and re-issuesLISTEN schema_changed;. This fix is intentionally scoped to the new agent-chat listener; porting it back to nova-mind is out of scope for nova-mind#579.
- Pre-extraction / nova-mind reference:
agent-chat/
├── schema.sql # Authoritative database schema
├── migrations/ # Idempotent migrations for existing DBs
├── README.md # This file
├── CHANGELOG.md # Release notes
├── docs/
│ ├── security-model.md # Provenance, immutability, grant-matrix detail
│ └── adoption-guide.md # Migrating an existing production DB
├── install.sh # Once-per-host bus installer
├── register-agent.sh # Per-agent DB role registration
├── install-plugin.sh # Build/sync OpenClaw channel plugin
├── lib/ # Shared shell helpers (pg-env.sh)
├── plugin/ # TypeScript OpenClaw channel plugin
├── tests/ # BATS installer tests + pytest listener tests
├── listener/ # Schema-sync listener daemon
│ ├── pg-notify-listener-chat.py
│ └── pg-notify-listener-chat.service
└── ...