Skip to content
Open
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
97 changes: 97 additions & 0 deletions replicas-matrix-bridge/docs/SINGLE_WORKSPACE_MODE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Single-Workspace Mode for replicas-matrix-bridge

The bridge's default behavior is **one Replicas workspace per Matrix room**: on
first message in a previously-unseen room, `dispatch.ts` calls
`POST /v1/replica` to spawn a workspace named `mx-<roomidprefix>-<ts>` and
writes the binding to KV (`room:<roomId> → <replica_id>`).

This document describes how to opt into **single-workspace mode**: every
KV-miss room shares one configured replica. Useful when you have a single
long-lived workspace and want every new project room to drop into it
without paying for/managing a separate sandbox per chat.

## What the fix changes

PR #25 adds an optional `DEFAULT_REPLICA_ID` binding to `Env`.

- If unset → behavior is unchanged. Per-room spawn continues.
- If set → in `dispatch.ts`, the `else` branch of the existing/KV-miss
check now forwards to `DEFAULT_REPLICA_ID` instead of `createReplica`,
pins the KV mapping on success (so future messages route via the normal
`existing` branch), and falls through to legacy `createReplica` only if
the default replica's `sendFollowUp` fails. The bot never goes silent.

Touched files: `src/index.ts`, `src/dispatch.ts`, `wrangler.toml`.

## How to enable

After this PR is merged and the worker is redeployed:

```sh
cd replicas-matrix-bridge
echo "<your-replica-uuid>" | wrangler secret put DEFAULT_REPLICA_ID
# or set under [vars] in wrangler.toml for non-secret value
```

Find a replica id with:

```sh
curl -s -H "Authorization: Bearer $REPLICAS_API_KEY" \
-H "Replicas-Org-Id: 778b1aa3-4327-45a4-9874-c8a3a72df610" \
"https://api.replicas.dev/v1/replica?limit=100" \
| jq '.replicas[] | {id,name,status}'
```

## Alternative: pre-pin KV without a deploy

If you don't want to (or can't) redeploy the worker, you can achieve the
same effect by **pre-populating the KV mapping for a room before any
message arrives** in it. The bridge's `dispatch.ts` only spawns when
`env.MAP.get("room:" + roomId)` returns null, so pre-writing the key
suppresses the spawn.

The Garza OS workspace ships two helpers for this:

- **`~/.replicas/bin/garza-project-provision`** — creates a Matrix room
with `m.room.name`, topic, invitees, power levels, and a
`dev.garza.agent.config` state event marking it as Garza-managed. It
honours `GARZA_ROUTER_REPLICA_ID` (falling back to `WORKSPACE_ID`) and
writes `room:<id> → <router_id>` into the bridge's KV namespace
(`036b2f9231de4d21bf0cdf120b5b4869`) immediately after `createRoom`.
Result: new rooms never spawn.

- **`~/.replicas/bin/garza-rebind-rooms`** — bulk-rewrites all (or a
filtered subset of) existing `room:*` KV entries to point at one
replica id. Useful for consolidating historical sprawl. Use
`--status` for read-only inspection and `--dry-run` to preview a
rebind without writing. The orphaned per-room workspaces remain
alive until you `DELETE /v1/replica/{id}` them, so cleanup is a
separate explicit step.

## KV schema (for reference)

Namespace: `036b2f9231de4d21bf0cdf120b5b4869` (`MAP` binding).

| Key | Value | Purpose |
|---|---|---|
| `room:<roomId>` | replica UUID | Per-room binding read by dispatch |
| `members:<roomId>` | JSON member list | Cached member set for size gating |
| `session:<roomId>` | Olm session metadata | Encryption state |
| `model:<roomId>` | model id string | Per-room `!model` override |

Direct KV write via the Cloudflare API:

```sh
curl -X PUT \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
-H "Content-Type: text/plain" \
"https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/storage/kv/namespaces/036b2f9231de4d21bf0cdf120b5b4869/values/room:!ROOM:matrix.org" \
--data-binary "<replica-uuid>"
```

## Verification

After enabling either mechanism, create a new test room and post a message.
Expected: no new `mx-*` workspace appears in `GET /v1/replica`. The
existing workspace receives the message via the normal `existing` path.

36 changes: 36 additions & 0 deletions replicas-matrix-bridge/src/dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,42 @@ export async function handleMatrixMessage(
replicaId = await createReplica(env, roomId, eventId, text);
spawnedFresh = true;
}
} else if (env.DEFAULT_REPLICA_ID) {
// Single-workspace mode: forward to the configured default replica
// instead of POST /v1/replica. Pin the mapping on success so this
// path only fires once per room. On any send failure (gone, 5xx,
// network blip) fall through to fresh-spawn — same safety stance
// as the existing-replica error path above.
const defaultId = env.DEFAULT_REPLICA_ID;
console.log(`[dispatch] using DEFAULT_REPLICA_ID=${defaultId} for fresh room=${roomId}`);
const followUpP = sendFollowUp(defaultId, text, env, roomId, eventId);
const watcherP = startWatcher(env, defaultId, roomId, eventId, text, undefined, undefined);
const followUp = await followUpP;
await watcherP;
if (followUp.ok) {
replicaId = defaultId;
// Pin the mapping so subsequent messages route via the normal
// `existing` branch without re-checking DEFAULT_REPLICA_ID.
// Honour REPLICA_TTL_SECONDS the same way the fresh-spawn
// path does below.
const ttlEnv = parseInt(env.REPLICA_TTL_SECONDS, 10);
const opts: KVNamespacePutOptions = ttlEnv > 0
? { expirationTtl: Math.max(60, ttlEnv) }
: {};
await env.MAP.put(key, defaultId, opts);
// spawnedFresh stays false — we already started the watcher
// above so we want the follow-up path below, not the
// fresh-spawn startWatcher call.
} else {
console.log(
`[dispatch] DEFAULT_REPLICA_ID=${defaultId} sendFollowUp failed gone=${followUp.gone} — falling back to fresh spawn`,
);
env.WATCHER.get(env.WATCHER.idFromName(defaultId))
.fetch("https://watcher/cancel", { method: "POST" })
.catch(() => {});
replicaId = await createReplica(env, roomId, eventId, text);
spawnedFresh = true;
}
} else {
replicaId = await createReplica(env, roomId, eventId, text);
spawnedFresh = true;
Expand Down
8 changes: 8 additions & 0 deletions replicas-matrix-bridge/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ export interface Env {
REPLICAS_AGENT_OVERRIDE?: string;
REPLICAS_MODEL_OVERRIDE?: string;
REPLICAS_THINKING_OVERRIDE?: string;
// Single-workspace mode. When set, dispatch reuses this replica id for
// any room whose KV mapping is missing instead of POST /v1/replica.
// On first message in a new room we pin the mapping (room:<id> ->
// DEFAULT_REPLICA_ID) so subsequent traffic flows the normal
// `existing` path and never falls back. Unset to keep per-room
// auto-spawn behavior. Set via `wrangler secret put DEFAULT_REPLICA_ID`
// or under [vars] in wrangler.toml.
DEFAULT_REPLICA_ID?: string;
// JSON array of Megolm session keys (Element key-export format, decrypted
// out of band and stashed here). Used by the listener to decrypt
// m.room.encrypted events whose session_id we hold.
Expand Down
7 changes: 7 additions & 0 deletions replicas-matrix-bridge/wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ REPLICA_TTL_SECONDS = "0"
REPLICAS_AGENT_OVERRIDE = "claude"
REPLICAS_MODEL_OVERRIDE = "claude-sonnet-4-6"
REPLICAS_THINKING_OVERRIDE = "medium"
# Single-workspace mode. When set, dispatch reuses this replica id for
# rooms whose KV mapping is missing instead of POST /v1/replica. Pinned
# on first message so subsequent messages route via the normal existing
# path. Leave commented (or unset) to keep the per-room auto-spawn
# behavior. Can also be set as a secret with:
# wrangler secret put DEFAULT_REPLICA_ID
# DEFAULT_REPLICA_ID = "00000000-0000-0000-0000-000000000000"
# Bot device id on matrix.org (the device the MATRIX_ACCESS_TOKEN was minted
# for). Used as the id in the Olm device-key upload + signature subject.
MATRIX_DEVICE_ID = "Ww3fWv0z7s"
Expand Down
Loading