Skip to content

chore: scaffold five new backend packages (postgres, zulip, discord, telegram, slack)#2

Merged
sharpTrick merged 8 commits into
mainfrom
claude/parley-five-backends-5zrx9d
Jul 3, 2026
Merged

chore: scaffold five new backend packages (postgres, zulip, discord, telegram, slack)#2
sharpTrick merged 8 commits into
mainfrom
claude/parley-five-backends-5zrx9d

Conversation

@sharpTrick

Copy link
Copy Markdown
Owner

Package skeletons only (package.json + tsconfig.json), lead-owned so the
shared package-lock.json is written once. Implementations follow per-package.

Co-Authored-By: Claude Fable 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01QTSH1UJmMGg7VG6diAfcG8

claude added 8 commits July 3, 2026 00:53
…telegram, slack)

Package skeletons only (package.json + tsconfig.json), lead-owned so the
shared package-lock.json is written once. Implementations follow per-package.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QTSH1UJmMGg7VG6diAfcG8
…, LISTEN/NOTIFY push)

BIGSERIAL seq serves as both cursor and backendMsgId; post() serializes
per-topic via pg_advisory_xact_lock so seq order matches visibility order.
subscribe is true event-driven push: a dedicated LISTEN client on
parley_<md5(topic)> channels, draining seq > lastSeen on each notify
(payload treated as a hint only) with reconnect re-LISTEN + re-drain.
Senders table backs resolveIdentity. Conformance green (6/6, including
concurrent multi-instance writers) against a live local Postgres 16.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QTSH1UJmMGg7VG6diAfcG8
…gateway push)

Message snowflakes serve as both cursor and backendMsgId (per-channel
strictly increasing; exclusive-since delegated to the REST ?after= param,
BigInt compare where local ordering is unavoidable). subscribe speaks a
minimal gateway subset (HELLO/IDENTIFY/heartbeat/MESSAGE_CREATE) over one
shared websocket; reconnect re-IDENTIFYs and leans on cursor catch-up
instead of RESUME. Conformance green (6/6) against an in-process fake
gateway+REST server; hosted-SaaS positioning noted in-code per the brief.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QTSH1UJmMGg7VG6diAfcG8
…ursor, event-queue push)

Zulip's globally monotonic message ids serve as both cursor and
backendMsgId; exclusive-since is server-side via anchor+include_anchor=false
narrowed to stream+topic. Parley topics map near-1:1 onto Zulip topics
within one configured stream (topic mutability caveat documented).
subscribe registers a narrowed event queue and long-polls /events; queue
GC (BAD_EVENT_QUEUE_ID) recovers by re-registering plus a cursor gap-fill.
Conformance green (8 passed) against an in-process urlencoded-only fake;
a real-server suite is env-gated behind PARLEY_ZULIP_URL/EMAIL/API_KEY.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QTSH1UJmMGg7VG6diAfcG8
… Socket Mode push)

Per-channel message ts doubles as cursor and backendMsgId, compared
integer-wise (seconds then suffix) — never lexically or as floats.
fetchRecent assembles the full (since, now] range across newest-first
next_cursor pages, sorts ascending, and takes the earliest limit so a
long backlog's middle is never skipped. subscribe rides one Socket Mode
websocket (single-use URLs, fresh apps.connections.open per reconnect)
and acks every envelope before processing. Conformance green (7 passed,
incl. concurrent writers and an ack-discipline test) against an
in-process fake; hosted-SaaS positioning and free-plan retention caveat
noted in-code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QTSH1UJmMGg7VG6diAfcG8
…_id cursor, local observed store)

backendMsgId is the composite <chat_id>:<message_id> (message_id is only
per-chat unique); the cursor is the per-chat message_id, monotonic within
a topic since a topic maps to exactly one chat. The Bot API exposes no
history endpoint, so fetchRecent replays an append-only JSONL store of
observed messages (own sendMessage responses + getUpdates) — pre-join
history cannot be backfilled, the one structural strain on the seam's
durable-history line, documented in code and README. One getUpdates
long-poll per token (a second poller 409s), so the multi-writer
conformance case is skipped by design. Conformance + inbound-flow tests
green (6 passed, 1 skipped).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QTSH1UJmMGg7VG6diAfcG8
…ADME tables, dev-compose)

Serialized integration after the parallel per-package builds: root tsconfig
references + vitest aliases for the five new packages; DESIGN §6 cursor
list, §12 v0.6 build-order block, §13 tree, §18 tags; README backend table
(with hosted-SaaS markers and the Telegram no-pre-join-history caveat),
status blurb, keywords; dev-compose postgres service + upstream docker-zulip
pointer; CONTRIBUTING note on in-process fakes for SaaS backends; PROGRESS
v0.6 entry. Full suite: 135 passed / 7 skipped; bridge-core diff vs main is
empty — the seam held.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QTSH1UJmMGg7VG6diAfcG8
@sharpTrick sharpTrick merged commit 5d6e145 into main Jul 3, 2026
2 checks passed
sharpTrick added a commit that referenced this pull request Jul 3, 2026
## What & why

Adds **`parley_list_users`** — an MCP tool that reports who is **live**
on the bus, with an optional glob filter (e.g. `claude-*`), for picking
a hand-off target.

This started as "should *list users* be a core seam method every backend
implements?" The answer was **no**: the seam is six methods on purpose,
several backends have no enumerable user set
(Redis/NATS/Discord/Telegram), and a mandatory `listUsers` would mean
four different things wearing one name — the failure the conformance
suite exists to prevent.

The better design (Patrick's insight): **make presence a message.** Each
bridge announces `hello` / `heartbeat` / `goodbye`, and liveness is
derived **entirely above the seam** from `post`/`fetchRecent` + a TTL
window.

## Result
- **Works identically on every backend** — no new seam method.
- **Lists an idle instance that has never posted** (the case a
sender-scan misses).
- **Zero changes to any backend plugin; the conformance suite is
unchanged** — the repo's own proof that prime directive #2 held.
- Presence streams are **isolated**: derived presence topics are never
subscribed and never enter catch-up/dedup, so heartbeats never surface
as `<channel>` events or pollute durable history.
- Reports **Parley-participant liveness, not a human directory** — a
human in a native client appears once they send a real message.

## Design decisions (locked with the user)
- **Dedicated presence stream** (not heartbeats in real topics) — keeps
durable history and push clean, no `Message`-model change.
- **Presence-only, uniform** (not presence + per-backend directory) —
keeps the "same behavior everywhere" property.

## Changes (all in `bridge-core`)
- `engine/presence.ts` — `presenceTopicFor`, encode/decode,
`computeLive` (TTL liveness)
- `transport/presence-loop.ts` — best-effort hello/heartbeat/goodbye
emitter
- `identity-filter.ts` — `matchGlob` / `filterHandles`
- `transport/tools.ts` — `parley_list_users` tool (+ `ToolDeps` gains
`presenceTtlMs`/`now`)
- lifecycle wiring: `stdio-bridge` (`attach`/`shutdown`) + `http` (app
scope)
- `config.ts` — `presence` block (`enabled`/`heartbeat_ms`/`ttl_ms`)
- docs: DESIGN §7/§11, README; fixed the stale §4 "real account lookup
for Matrix/XMPP" note (both echo the handle today)

## Testing
- **154 tests pass**, typecheck clean. 25 new unit tests (presence,
glob, emitter loop, 6 tool cases).
- **End-to-end on a real SQLite backend through the full bridge
lifecycle** — all 7 checks passed: idle agent discoverable via presence
alone; filter narrows; real messages never pollute the presence stream
or invent a user; clean-shutdown `goodbye` drops fast; a stale/crashed
heartbeat is reclaimed by TTL.

## Known follow-up
- Presence streams grow append-only (heartbeat every 30s). `list_users`
reads a bounded recent window; retention/pruning is a future
optimization (the seam has no delete) — noted, not blocking.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants