Skip to content

Support multiple bots per instance#24

Merged
dahlia merged 36 commits into
mainfrom
multitenancy
Jul 5, 2026
Merged

Support multiple bots per instance#24
dahlia merged 36 commits into
mainfrom
multitenancy

Conversation

@dahlia

@dahlia dahlia commented Jul 4, 2026

Copy link
Copy Markdown
Member

This pull request implements the multi-bot architecture designed in #16: it separates the instance (the shared infrastructure: KV store, message queue, repository, HTTP handling, and the single Fedify federation) from the bot (an individual ActivityPub actor with its own identity and event handlers), so one BotKit process can host many bots.

It supersedes #17, whose two attempts surfaced the key design blockers this PR resolves: the username → identifier mapping for dynamic bots and the routing of local object URIs that carried no owner information. Credit to @moreal for surfacing both problems and for the identifier-first Repository direction explored in his work-in-progress branches.

What's included

  • createInstance() and Instance.createBot() for hosting multiple static bots, plus dynamic bot groups resolved on demand by a dispatcher function (with an optional mapUsername callback for WebFinger, defaulting to username = identifier).
  • Canonical local object URIs now carry the owning bot identifier (/ap/actor/{identifier}/note/{id}); URIs in the pre-0.5 format are still recognized in incoming activities and permanently redirected when dereferenced.
  • The Repository interface stores data for multiple bots (identifier-first methods, a findFollowedBots() reverse lookup for timeline routing, and a migrate() hook); @fedify/botkit-sqlite and @fedify/botkit-postgres gained bot_id columns with automatic in-place upgrades of legacy databases.
  • Shared-inbox activities are routed to the bots they are relevant to instead of being broadcast.
  • Multi-bot instances expose a Mastodon-style instance actor under the reserved _instance identifier for shared-inbox signing, and serve per-bot web pages under /@{username}.
  • Session.bot is now a ReadonlyBot view, so handlers cannot mutate their bot through a session.
  • Single-bot deployments created with createBot() keep their exact behavior: same pages at the web root, same shared-inbox key, and automatic migration of pre-0.5 repository data on startup.

Breaking changes (0.5.0)

  • Every Repository method takes the owning bot identifier as its first parameter; custom implementations need updating.
  • Session.bot is typed ReadonlyBot instead of Bot.
  • Local object URI templates changed (with compatibility for the old format as described above).
  • KvRepository's per-category KvStoreRepositoryPrefixes option was replaced by a single prefix option.

Each commit is self-contained and green (deno task check, Deno and Node.js test suites); the PostgreSQL migration and concurrency paths were verified against a real PostgreSQL 17 instance.

Closes #16.

https://claude.ai/code/session_0157FUYXeusCEmbWyYnwt3Cn

Summary by CodeRabbit

  • New Features
    • Added instance-based multi-bot hosting on a single server, including per-bot ActivityPub routing, web pages, and feeds.
    • Introduced bot-scoped storage with automatic legacy migration on bot startup.
    • Session.bot is now a read-only bot identity, and follow actions can use a configurable form endpoint.
  • Bug Fixes
    • Improved upgrade compatibility for legacy single-bot data and URLs.
    • Strengthened data isolation so messages, followers, and votes never cross bot boundaries.
  • Documentation
    • Added the new “Instance” and “Bot-scoped storage” documentation, plus updated session and bot guidance.
  • Chores
    • Updated the node test workflow to build workspace packages before running tests.

dahlia added 14 commits July 4, 2026 18:47
To support hosting multiple bot actors on a single instance, a single
repository now stores data for multiple bots: every Repository method
takes the identifier of the owning bot actor as its first parameter,
and data belonging to different identifiers are completely isolated.

This commit includes the following changes:

- Added identifier parameter to all Repository methods
- Added ActorScopedRepository, a view of a Repository bound to a single
  bot actor identifier, created via Repository.forIdentifier(); BotImpl
  now scopes its repository through it, so its internals are unchanged
- Added Repository.findFollowedBots(), a reverse lookup answering which
  bots follow a given actor, needed later for routing shared-inbox
  activities from followed actors without enumerating dynamic bots
- Reserved an optional Repository.migrate() hook for adopting data
  stored by BotKit 0.4 or earlier (implementations follow separately)
- Re-keyed KvRepository under ["_botkit", "bots", identifier, ...] with
  a reverse index under ["_botkit", "index", "followees", ...]; the
  per-category KvStoreRepositoryPrefixes option was replaced with a
  single prefix option (KvRepositoryOptions)
- Re-keyed MemoryRepository and MemoryCachedRepository per identifier
- Added a bot_id column and composite primary keys to all tables in
  @fedify/botkit-sqlite and @fedify/botkit-postgres; migration of
  existing databases follows in a separate commit
- Scoped PostgreSQL advisory lock keys by bot identifier
- Fixed KvRepository.vote() not awaiting writes on KV stores without
  CAS support, and MemoryRepository.getMessages() ignoring the limit
  option
- Made KvRepository.removeFollowee() and MemoryCachedRepository repair
  the reverse index and cache on retried removals
- Declared @fedify/botkit as a workspace peer dependency of
  @fedify/botkit-postgres, and made the test:node task build all
  packages before running tests, since the Node.js tests of dependent
  packages now import runtime code from @fedify/botkit

#16

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5.5
Claude-Session: https://claude.ai/code/session_0157FUYXeusCEmbWyYnwt3Cn
BotKit 0.4 and earlier stored KV data under unscoped keys such as
["_botkit", "messages"].  KvRepository.migrate() adopts that data for
the given bot actor identifier so that existing single-bot deployments
keep their data after upgrading to the bot-scoped key layout.

Enumerable categories (key pairs, messages, poll votes, and followers)
are copied eagerly.  Legacy keys are kept rather than deleted, and the
completion marker is written last, so that an interrupted migration is
simply retried on the next run without data loss; each record is copied
only when the scoped key is missing, so retries do not clobber data
written after a previous run.

Sent follows, followees, and follow requests are keyed by UUIDs or URLs
and have no index list in the legacy layout, so they cannot be
enumerated through the KvStore interface.  These categories are instead
migrated lazily: once migrate() has armed the fallback, a lookup that
misses the scoped key consults the legacy key and moves the record
over.  Lazily moved followees are also inserted into the reverse
lookup index used by findFollowedBots(), and the index is re-asserted
on later lookups so that a crash between the move and the indexing is
repaired by retries.

#16

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5.5
Claude-Session: https://claude.ai/code/session_0157FUYXeusCEmbWyYnwt3Cn
Databases created by @fedify/botkit-sqlite 0.4 or earlier have no
bot_id column, and SQLite cannot add a column to a composite primary
key.  Opening such a database now rebuilds the affected tables (create
new, copy rows with an empty-string bot ID, drop, rename) in a single
transaction before the usual schema initialization, with foreign key
enforcement turned off during the rebuild since follow_requests
references followers.  The key_pairs table keeps its integer primary
key, so a plain ALTER TABLE suffices there.

SqliteRepository.migrate() then assigns the carried-over rows to the
given bot actor identifier.  To distinguish rows carried over from
a legacy database from data legitimately stored under an empty-string
identifier, the rebuild records a marker in a botkit_metadata table;
migrate() acts only while the marker exists and removes it in the same
transaction, which also makes repeated calls no-ops.  The foreign key
between followers and follow_requests is deferred to the commit while
both tables move in tandem.

#16

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5.5
Claude-Session: https://claude.ai/code/session_0157FUYXeusCEmbWyYnwt3Cn
Schemas created by @fedify/botkit-postgres 0.4 have no bot_id column.
Schema initialization now detects such tables through the information
schema and upgrades them in place: the bot_id column is added with an
empty-string backfill, primary keys are recreated as composite keys,
the old follow_requests foreign key is replaced with a composite one,
and indexes whose shape changed are dropped so that the regular
initialization recreates them.

The whole upgrade is sent as a single parameter-less multi-statement
query, which PostgreSQL executes over the simple query protocol in one
implicit transaction on one connection, so the upgrade stays atomic
even when the client is a connection pool.

PostgresRepository.migrate() assigns the carried-over rows to the
given bot actor identifier.  Like the SQLite counterpart, the upgrade
records a marker in a botkit_metadata table and migrate() acts only
while the marker exists, so data legitimately stored under an
empty-string identifier is never adopted and repeated calls are
no-ops.  The composite foreign key is declared deferrable, and
migrate() defers it while followers and follow_requests move in
tandem.

#16

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5.5
Claude-Session: https://claude.ai/code/session_0157FUYXeusCEmbWyYnwt3Cn
Local object URIs like /ap/follow/{id} did not encode which bot actor
owns the object, so routing an object back to its owner required
consulting storage.  Once multiple bots share an instance, ownership
becomes part of the protocol boundary, so the canonical templates now
nest under the actor path:

- /ap/actor/{identifier}/follow/{id}
- /ap/actor/{identifier}/create/{id}
- /ap/actor/{identifier}/article/{id} (likewise chat-message, note,
  and question)
- /ap/actor/{identifier}/announce/{id}

Emoji URIs stay instance-global since they carry no ownership
semantics.  Object dispatchers now verify that the identifier in the
URI matches the dispatching bot, so one bot's URI path can never serve
another bot's objects.

Remote servers may have stored object URIs generated by BotKit 0.4 or
earlier and can send them back in later activities such as Accept,
Undo, Like, or replies, so a plain redirect would not be enough.  The
new parseLocalUri() helper falls back to rewriting legacy-format paths
to the canonical format and re-parsing them, attributed to the single
bot actor that predates the upgrade.  All inbound parsing of local
object URIs goes through this helper now, and each site also checks
that the parsed identifier belongs to the handling bot before touching
its repository.

#16

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5.5
Claude-Session: https://claude.ai/code/session_0157FUYXeusCEmbWyYnwt3Cn
Dereferenceable GET and HEAD requests to local object URIs in the
legacy (pre-0.5) format are now permanently redirected to their
canonical identifier-carrying URIs, so links stored by remote servers
and clients keep working after an upgrade.  Inbox deliveries and other
non-dereferenceable requests are not affected; incoming activities
referring to legacy URIs are already recognized by parseLocalUri().

Also adds regression tests covering an Accept activity carrying
a canonical-format Follow URI and a Like activity referring to
a stored message by its legacy URI.

#16

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5.5
Claude-Session: https://claude.ai/code/session_0157FUYXeusCEmbWyYnwt3Cn
Session.bot used to expose the full Bot interface, so nothing stopped
an event handler from accidentally reassigning another handler through
it (e.g. session.bot.onMention = ...).  Once multiple bots share an
instance, a session leaking mutable access to its bot becomes even
riskier, so the session now exposes a ReadonlyBot: a read-only view of
the bot's identity and profile (identifier, username, name, class,
icon, image, and follower policy).

The event handler properties moved into a BotEventHandlers interface
that Bot extends, which later allows dynamic bot groups to share the
same handler-registration surface.  Text lookups now pass the bot
identifier to Context.getDocumentLoader() explicitly instead of
passing the bot object, which also makes the signing identity
deterministic.

This is a breaking change for code that reached the full Bot through
a session; such code should hold on to the Bot returned by createBot()
instead.

#16

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5.5
Claude-Session: https://claude.ai/code/session_0157FUYXeusCEmbWyYnwt3Cn
BotKit conflated two concepts in one Bot object: the server
infrastructure (KV store, message queue, repository, HTTP handling,
and the Fedify federation) and an individual ActivityPub actor with
its own identity and behavior.  This is the fundamental reason one
BotKit process could only host one bot.

The new createInstance() function creates an Instance that owns the
shared infrastructure and a single Fedify Federation, on which every
hosted bot is registered.  Federation callbacks are registered once by
the instance and routed to the owning bot by the identifier carried in
the URI.  Instance.createBot(identifier, profile) creates a bot hosted
on the instance; duplicate identifiers and usernames are rejected.
Custom emojis, NodeInfo, the shared inbox key, and web page serving
now belong to the instance.

BotImpl keeps all of its per-bot behavior and its public surface, but
no longer owns a Federation: constructing it without an instance
creates a dedicated one, which preserves the single-bot behavior of
BotKit 0.4 and earlier, including the recognition of legacy object
URIs.  Incoming activities are currently delivered to every hosted
bot, which is identical to the previous behavior for a single bot;
relevance-based routing for multiple bots follows separately.

#16

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5.5
Claude-Session: https://claude.ai/code/session_0157FUYXeusCEmbWyYnwt3Cn
Single-bot deployments upgrading from BotKit 0.4 or earlier carry
repository data that predates bot-scoped storage.  The createBot()
compatibility path now wraps its repository in a gate that kicks off
Repository.migrate() with the bot's identifier at construction time
and makes every repository operation await its completion, so the
legacy data is adopted before anything reads or writes the store.
Since createBot() is synchronous, the gate is what guarantees the
ordering without requiring a top-level await from the user.

Repositories without a migrate() implementation are unaffected, and
bots created through Instance.createBot() are not gated, since
a multi-bot instance is necessarily a new deployment.

#16

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5.5
Claude-Session: https://claude.ai/code/session_0157FUYXeusCEmbWyYnwt3Cn
An instance can now host several bots, each with its own actor URI,
WebFinger handle, collections, and isolated repository data; one bot's
URI path never serves another bot's objects.

Since a multi-bot instance has no single obvious actor whose key
should sign shared-inbox related requests, instances created through
createInstance() expose an instance actor under the reserved
"_instance" identifier: a non-discoverable Application whose key pairs
are generated lazily, similar to Mastodon's instance actor.  Bots
cannot take the reserved identifier.  Instances created through the
single-bot createBot() compatibility path keep the pre-0.5 behavior:
the sole bot's key signs shared-inbox requests and no instance actor
is exposed, so nothing new appears on existing deployments.

NodeInfo usage statistics now report the number of hosted bots
instead of a hardcoded single user.

#16

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5.5
Claude-Session: https://claude.ai/code/session_0157FUYXeusCEmbWyYnwt3Cn
Incoming activities delivered to the shared inbox used to be handed to
every bot hosted on the instance, which meant every bot's onMessage
would fire for every incoming message once multiple bots share an
instance.  The instance now routes each activity to the bots it is
actually relevant to:

- Follow and Undo(Follow) route by the target actor URI.
- Accept, Reject, Like, and Undo(Like) route by the owning bot
  identifier carried in the local object URI (legacy-format URIs are
  still recognized).  Activities on objects the instance does not own
  cannot be attributed to any bot and are dropped with a debug log.
- Create routes to the union of bots following the author, bots
  addressed in to/cc (on the activity or on the embedded object,
  including their followers collections), mentioned bots, and the
  owners of the reply and quote targets.
- Announce routes to followers of the sharer, addressed bots, and
  the owner of the shared message.

Timeline routing relies on the new Repository.findFollowedBots()
reverse lookup, so bots resolved dynamically later are covered without
enumerating them.  Personal-inbox deliveries go to their recipient
only.  Instances created through the single-bot createBot()
compatibility path keep delivering everything to their sole bot, which
preserves the pre-0.5 behavior, including likes of objects with
foreign URIs.

#16

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5.5
Claude-Session: https://claude.ai/code/session_0157FUYXeusCEmbWyYnwt3Cn
The web interface used to serve the single bot at the site root, with
/followers, /message/{id}, /feed.xml, /tags/{tag}, and the remote
follow form all implicitly belonging to that bot.  Multi-bot instances
now serve a bot list at the root and each bot's pages under a path
derived from its username:

- /@{username}: the bot's profile
- /@{username}/{messageId}: a message permalink
- /@{username}/followers, /@{username}/tags/{tag},
  /@{username}/feed.xml, and POST /@{username}/follow

The page handlers were extracted into shared functions parameterized
by a base path, so instances created through the single-bot
createBot() compatibility path keep serving the exact same pages at
the root.  The actor's advertised web URL and the url property of
published messages follow the instance's mode, and usernames are
percent-encoded wherever they appear in paths.

#16

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5.5
Claude-Session: https://claude.ai/code/session_0157FUYXeusCEmbWyYnwt3Cn
Passing a dispatcher function to Instance.createBot() now creates
a bot group: a registry of event handlers shared by every bot the
dispatcher resolves.  This suits scenarios like "one bot per region,"
where thousands of potential bots are backed by a database rather than
declared up front:

    const weatherBots = instance.createBot(async (ctx, identifier) => {
      if (!identifier.startsWith("weather_")) return null;
      const region = await db.getRegion(identifier.slice(8));
      return region && { username: identifier, name: region.name };
    });

Resolution prefers statically registered bots and probes dispatchers
in their registration order.  A resolved bot is a transient view whose
event handlers are read live from the group at dispatch time, so
handlers registered after a bot was first resolved still fire, and no
per-bot instances accumulate; resolutions are memoized per Fedify
context, whose entries die with the context.

For WebFinger, a group can supply a mapUsername callback resolving
usernames to identifiers; the mapping is accepted only when it
resolves to a bot of the very group that mapped it.  Without the
callback, usernames are assumed to equal identifiers, but the fallback
never exposes static bots' internal identifiers as usernames.  Dynamic
bots participate in shared-inbox routing, including timeline routing
through the followee reverse lookup, and get their own /@{username}
web pages.  BotGroup.getSession(origin, identifier) opens sessions for
proactive publishing from dynamic bots.

#16

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5.5
Claude-Session: https://claude.ai/code/session_0157FUYXeusCEmbWyYnwt3Cn
The instance module joins the per-module export entries of
@fedify/botkit, so the multi-bot API can be imported directly from
@fedify/botkit/instance in both Deno and Node.js.

#16

Assisted-by: Claude Code:claude-fable-5
Claude-Session: https://claude.ai/code/session_0157FUYXeusCEmbWyYnwt3Cn
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@dahlia, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 25 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 088e9361-8372-487c-bfd4-5cb053219eee

📥 Commits

Reviewing files that changed from the base of the PR and between 1251d82 and da6b208.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (6)
  • packages/botkit-postgres/package.json
  • packages/botkit-sqlite/package.json
  • packages/botkit/src/instance-impl.test.ts
  • packages/botkit/src/instance-impl.ts
  • packages/botkit/src/text.test.ts
  • packages/botkit/src/text.ts
📝 Walkthrough

Walkthrough

This PR adds multi-bot instance hosting for BotKit, scopes repository storage by bot identifier with migration support, updates bot/session/message URI handling, and refactors web routing and public API surfaces for per-bot serving.

Changes

Multi-bot Instance Support

Layer / File(s) Summary
Repository scoping and migration
packages/botkit/src/repository.ts, packages/botkit/src/repository.test.ts
Repository, ActorScopedRepository, KvRepository, MemoryRepository, and MemoryCachedRepository require bot identifiers on storage operations, add forIdentifier() views and migrate() hooks, and expand isolation and migration coverage.
SQLite repository schema and migration
packages/botkit-sqlite/src/mod.ts, packages/botkit-sqlite/src/mod.test.ts
SqliteRepository adds bot_id-scoped tables and legacy rebuild handling, and the tests cover multitenancy plus legacy adoption.
Postgres repository schema and migration
packages/botkit-postgres/src/mod.ts, packages/botkit-postgres/src/mod.test.ts, packages/botkit-postgres/package.json
PostgresRepository adds bot-scoped tables, metadata, and upgrade paths, with identifier-scoped CRUD and updated concurrency/migration tests; the peer dependency is switched to workspace:.
Legacy object URI helpers
packages/botkit/src/uri.ts, packages/botkit/src/uri.test.ts
Adds rewriteLegacyObjectPath() and parseLocalUri() for canonical and legacy local URI parsing, with unit tests.
Bot and Session type surface
packages/botkit/src/bot.ts, packages/botkit/src/session.ts, packages/botkit/src/bot.test.ts
Splits event handlers into BotEventHandlers, adds ReadonlyBot, changes createBot() wrapping, narrows Session.bot, and updates tests for the readonly view and migration behavior.
BotImpl instance integration
packages/botkit/src/bot-impl.ts, packages/botkit/src/bot-impl.test.ts, packages/botkit/src/follow-impl.test.ts
BotImpl is hosted through InstanceImpl, delegates infrastructure behavior, validates identifier-scoped URIs, and updates bot/follow/message tests to the new identifier-aware contracts.
Message, session, and text URI updates
packages/botkit/src/message-impl.ts, packages/botkit/src/session-impl.ts, packages/botkit/src/text.ts, packages/botkit/src/message-impl.test.ts, packages/botkit/src/session-impl.test.ts, packages/botkit/src/text.test.ts
Object URI generation, session URLs, and mention loaders now include bot identifiers, and the tests are adjusted for the new scoped behavior.
Instance API and routing engine
packages/botkit/src/instance.ts, packages/botkit/src/instance-impl.ts, packages/botkit/src/mod.ts, packages/botkit/deno.json, packages/botkit/package.json, packages/botkit/src/instance-impl.test.ts, packages/botkit/src/instance-multi.test.ts, packages/botkit/src/instance-routing.test.ts
Adds the public instance API, InstanceImpl routing and dispatch logic, instance actor signing and emoji handling, export wiring, and test coverage for multi-bot hosting and routing.
Multi-bot page routing
packages/botkit/src/pages.tsx, packages/botkit/src/components/FollowButton.tsx, packages/botkit/src/pages.test.ts
Page handlers become base-path aware, multi-bot routes are added, and the follow button can override its form action.
Documentation and changelog
CHANGES.md, docs/concepts/instance.md, docs/concepts/*.md, docs/start.md, AGENTS.md, docs/.vitepress/config.mts, deno.json
Adds 0.5.0 changelog entries, new and updated concept docs, navigation, architecture notes, and the updated test task.

Estimated code review effort: 5 (Critical) | ~120 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding multi-bot support per instance.
Linked Issues check ✅ Passed The PR implements the requested instance/bot split, multi-bot creation APIs, bot-scoped storage, routing, compatibility, and read-only session bot.
Out of Scope Changes check ✅ Passed The changes appear aligned with the multi-bot redesign, with docs, tests, exports, and supporting build adjustments all serving that scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch multitenancy

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Adds the Instance concept document covering createInstance(), static
bots, dynamic bot groups, the instance actor, per-bot web pages, and
how to migrate an existing single-bot deployment.  The Bot and
Repository documents point to it where the 0.5.0 changes affect them,
and the changelog records the new APIs and the breaking changes to
the Repository interface, Session.bot, and local object URIs.

#16

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5.5
Claude-Session: https://claude.ai/code/session_0157FUYXeusCEmbWyYnwt3Cn
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown

Test Results

  1 files  ±  0   18 suites  +6   12m 0s ⏱️ + 6m 11s
347 tests +154  346 ✅ +154  1 💤 ±0  0 ❌ ±0 
408 runs  +154  406 ✅ +154  2 💤 ±0  0 ❌ ±0 

Results for commit da6b208. ± Comparison against base commit b2371c7.

♻️ This comment has been updated with latest results.

@codecov

codecov Bot commented Jul 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 77.87334% with 566 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
packages/botkit-postgres/src/mod.ts 4.44% 258 Missing ⚠️
packages/botkit/src/instance-impl.ts 77.03% 161 Missing and 25 partials ⚠️
packages/botkit/src/bot-impl.ts 89.41% 45 Missing and 6 partials ⚠️
packages/botkit/src/repository.ts 93.61% 30 Missing and 8 partials ⚠️
packages/botkit-sqlite/src/mod.ts 88.50% 28 Missing and 2 partials ⚠️
packages/botkit/src/instance.ts 91.42% 3 Missing ⚠️
Files with missing lines Coverage Δ
packages/botkit/src/bot.ts 100.00% <100.00%> (+25.22%) ⬆️
packages/botkit/src/message-impl.ts 85.77% <100.00%> (+3.14%) ⬆️
packages/botkit/src/session-impl.ts 84.12% <100.00%> (+0.54%) ⬆️
packages/botkit/src/text.ts 95.69% <100.00%> (+0.12%) ⬆️
packages/botkit/src/uri.ts 100.00% <100.00%> (ø)
packages/botkit/src/instance.ts 91.42% <91.42%> (ø)
packages/botkit-sqlite/src/mod.ts 73.15% <88.50%> (+18.88%) ⬆️
packages/botkit/src/repository.ts 88.54% <93.61%> (+2.95%) ⬆️
packages/botkit/src/bot-impl.ts 90.52% <89.41%> (+5.55%) ⬆️
packages/botkit/src/instance-impl.ts 77.03% <77.03%> (ø)
... and 1 more
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for hosting multiple bots on a single server instance in BotKit, allowing them to share infrastructure while maintaining isolated actor identities, event handlers, and data. Key changes include the addition of createInstance(), dynamic bot groups via BotDispatcher, bot-scoped repositories (KvRepository, SqliteRepository, and PostgresRepository) with migration utilities, and updated routing and web pages. Feedback on these changes highlights a security concern where dynamic bots could potentially hijack the reserved _instance identifier, and recommends replacing an unsafe any type cast with unknown in instance-impl.ts to improve type safety.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread packages/botkit/src/instance-impl.ts
Comment thread packages/botkit/src/instance-impl.ts
@dahlia

dahlia commented Jul 4, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7434d1ccf4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/botkit/src/repository.ts
dahlia added 5 commits July 5, 2026 04:28
The Session document did not mention the Session.bot property at all,
which matters more now that dynamic bot group handlers rely on it to
tell which bot they are running as; it also notes the 0.5.0 type
change to ReadonlyBot and shows BotGroup.getSession() taking a bot
identifier.  The quick start now points readers who want several bots
on one server to the Instance chapter.

#16

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5.5
Claude-Session: https://claude.ai/code/session_0157FUYXeusCEmbWyYnwt3Cn
A dispatcher returning null passes the identifier on to the next bot
group, in the order the groups were created.  The behavior existed but
had no explicit test for the fallthrough itself, only for first-match
precedence, and the documentation left it implicit.

#16

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5.5
Claude-Session: https://claude.ai/code/session_0157FUYXeusCEmbWyYnwt3Cn
Shows an instance combining a static bot with two prefix-namespaced
dynamic groups, walks through how an identifier resolves across them,
and explains why overlapping groups are resolved by creation order
rather than rejected, including the separate order WebFinger username
resolution follows.

#16

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5.5
Claude-Session: https://claude.ai/code/session_0157FUYXeusCEmbWyYnwt3Cn
The reserved identifier was hardcoded as "_instance", which a
developer could plausibly want for a bot of their own.  The default is
now the more clearly BotKit-owned "__botkit_instance__", and the new
CreateInstanceOptions.instanceActorIdentifier option overrides it when
even that collides.  The INSTANCE_ACTOR_IDENTIFIER constant was
renamed to DEFAULT_INSTANCE_ACTOR_IDENTIFIER accordingly.

Dynamic dispatchers could previously resolve a bot under the reserved
identifier, which the actor dispatcher would shadow with the instance
actor while other routes served the dynamic bot.  Bot resolution now
rejects the reserved identifier outright, before static and dynamic
lookups alike, so no bot can take it through any path.

#16

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5.5
Claude-Session: https://claude.ai/code/session_0157FUYXeusCEmbWyYnwt3Cn
The option was only mentioned in passing.  The instance document now
names the two multi-bot-specific options in its option overview and
shows how to override the instance actor identifier, along with why
it must not change once the instance is federated.

#16

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5.5
Claude-Session: https://claude.ai/code/session_0157FUYXeusCEmbWyYnwt3Cn
dahlia added 4 commits July 5, 2026 09:09
Serving a custom emoji opened the file and then called stat() and
built the streaming Response outside any error handling, so a failure
after a successful open (for example a permission error on stat)
leaked the file descriptor.  The handle is now closed before the error
is rethrown; on success it continues to be consumed and closed by the
response stream as before.

#24 (comment)

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5.5
Claude-Session: https://claude.ai/code/session_0157FUYXeusCEmbWyYnwt3Cn
Two processes initializing the schema at the same time could both see
the pre-0.5 layout and race the same sequence of ALTER TABLE
statements, failing on duplicate columns or conflicting constraint
changes.  The whole upgrade now runs inside a single PL/pgSQL DO block
that first takes a transaction-scoped advisory lock keyed on the
schema name, re-checks each table's shape under the lock, and applies
the DDL only where the bot_id column is still missing, so concurrent
initializers queue up and the losers find nothing left to do.  The
outer information_schema probe remains as a fast path that skips the
block entirely on already-upgraded databases.  A regression test runs
two initializations concurrently against a seeded legacy schema and
verifies the data still migrates.

#24 (comment)

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5.5
Claude-Session: https://claude.ai/code/session_0157FUYXeusCEmbWyYnwt3Cn
The AuthorizedMessage guard that turns update() and delete() into
no-ops when the session does not own the message existed but had no
test on a multi-bot instance.  A new test publishes a message as one
bot, wraps it in another bot's session, and asserts that update() and
delete() send no activities and leave the repository untouched.

#24 (comment)

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5.5
Claude-Session: https://claude.ai/code/session_0157FUYXeusCEmbWyYnwt3Cn
The KV migration used to copy only the categories reachable through
index lists and left sent follows, followees, and follow requests to a
lazy read-through fallback, on the assumption that a KvStore cannot be
enumerated.  That assumption is outdated: KvStore.list() is required
since Fedify 2.0.  The migration now enumerates every legacy category
with list() and copies it up front, entering followees into the
reverse lookup index as it goes, so findFollowedBots() routes
timelines correctly right after an upgrade instead of only after each
followee happens to be touched.  The entire lazy fallback machinery is
removed.

The migration was also racy: two bots migrating concurrently could
both pass the marker check and adopt the same legacy rows, breaking
cross-identifier isolation.  The marker now records its adopter and is
claimed with a compare-and-set before anything is copied; only the
claiming identifier copies, retrying on the next call if a previous
run was interrupted before the completion flag was written.  Indexing
errors propagate so an incomplete reverse index is never marked done,
and only the transient lock keys of the message and follower lists are
skipped, so a poll option literally named "lock" survives the
migration.

#24 (comment)
#24 (comment)

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5.5
Claude-Session: https://claude.ai/code/session_0157FUYXeusCEmbWyYnwt3Cn
@dahlia

dahlia commented Jul 5, 2026

Copy link
Copy Markdown
Member Author

@codex review

@dahlia

dahlia commented Jul 5, 2026

Copy link
Copy Markdown
Member Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for hosting multiple bots on a single server instance using the new createInstance() API in @fedify/botkit. It updates the repository interfaces (KvRepository, SqliteRepository, and PostgresRepository) to scope and isolate data by bot actor identifiers, and adds support for redirecting legacy object URIs. A critical resource leak was identified in instance-impl.ts where serving custom emojis via FileHandle.readableWebStream() can leak file descriptors in Deno; using fs.readFile instead was suggested to prevent server crashes.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread packages/botkit/src/instance-impl.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f609c6a8b1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/botkit/src/bot-impl.ts Outdated
dahlia added 2 commits July 5, 2026 10:38
When a Like or emoji reaction reached one bot's personal inbox but its
object was a local URI owned by another bot, the identifier check
failed and the else branch dereferenced the object as if it were
remote, so the recipient could fire onLike or onReact for a message it
does not own.  A parsed local message URI whose identifier does not
match the recipient is now ignored; the owning bot still receives the
activity through its own routing.  Announces are intentionally left
as they were, since a boost of another local bot's message is
legitimate timeline content for the recipient.

#24 (comment)

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5.5
Claude-Session: https://claude.ai/code/session_0157FUYXeusCEmbWyYnwt3Cn
FileHandle.readableWebStream() does not close the underlying handle
when the stream completes, in Node.js or in Deno, so every custom
emoji request leaked a file descriptor even on the success path and
would eventually exhaust the process's open-file limit.  Custom emojis
are small images, so the handler now reads the whole file with
fs.readFile() and serves the buffer directly, removing the manual
file handle management (and its error-path close) entirely.

#24 (comment)

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5.5
Claude-Session: https://claude.ai/code/session_0157FUYXeusCEmbWyYnwt3Cn
@dahlia

dahlia commented Jul 5, 2026

Copy link
Copy Markdown
Member Author

@codex review

@dahlia

dahlia commented Jul 5, 2026

Copy link
Copy Markdown
Member Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for hosting multiple bots on a single server instance in @fedify/botkit. It adds the createInstance() function and Instance interface to manage shared infrastructure, scopes the Repository interface and database schemas (SQLite and Postgres) by bot actor identifiers, and updates local object URIs to include the bot identifier. Additionally, Session.bot is now read-only to prevent event handler mutation. The review feedback recommends making username duplicate checks and username resolution case-insensitive to align with fediverse standards and ensure reliable routing.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread packages/botkit/src/instance-impl.ts
Comment thread packages/botkit/src/instance-impl.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/botkit/src/bot-impl.ts (1)

890-906: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract shared local/remote object resolution logic.

Three near-identical blocks resolve "is this a local message owned by me, or should I dereference remotely" (onAnnounced, #parseLike, #parseReaction). Consolidating into one private helper (e.g. #resolveMessageObject(ctx, objectId, remoteFallback)) would remove the duplication and prevent exactly the kind of inconsistency flagged above from recurring.

Also applies to: 924-946, 989-1029

🤖 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/botkit/src/bot-impl.ts` around lines 890 - 906, Extract the repeated
local-vs-remote object resolution into a single private helper used by
onAnnounced, `#parseLike`, and `#parseReaction`. Move the shared logic around
parseLocalUri, repository.getMessage, and the remote fallback getObject call
into something like `#resolveMessageObject`(ctx, objectId, remoteFallback). Update
each caller to pass its own objectId and remote dereference path so the
ownership check and Create handling stay consistent in one place.
🤖 Prompt for all review comments with 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.

Nitpick comments:
In `@packages/botkit/src/bot-impl.ts`:
- Around line 890-906: Extract the repeated local-vs-remote object resolution
into a single private helper used by onAnnounced, `#parseLike`, and
`#parseReaction`. Move the shared logic around parseLocalUri,
repository.getMessage, and the remote fallback getObject call into something
like `#resolveMessageObject`(ctx, objectId, remoteFallback). Update each caller to
pass its own objectId and remote dereference path so the ownership check and
Create handling stay consistent in one place.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 45febb31-7841-46e5-9798-1da6c523faa4

📥 Commits

Reviewing files that changed from the base of the PR and between ae50d4c and 1251d82.

📒 Files selected for processing (13)
  • CHANGES.md
  • packages/botkit-postgres/src/mod.test.ts
  • packages/botkit-postgres/src/mod.ts
  • packages/botkit/src/bot-impl.ts
  • packages/botkit/src/components/Layout.tsx
  • packages/botkit/src/instance-impl.ts
  • packages/botkit/src/instance-multi.test.ts
  • packages/botkit/src/instance-routing.test.ts
  • packages/botkit/src/message-impl.test.ts
  • packages/botkit/src/pages.test.ts
  • packages/botkit/src/pages.tsx
  • packages/botkit/src/repository.test.ts
  • packages/botkit/src/repository.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • packages/botkit/src/instance-multi.test.ts
  • CHANGES.md
  • packages/botkit/src/instance-impl.ts
  • packages/botkit-postgres/src/mod.ts
  • packages/botkit-postgres/src/mod.test.ts
  • packages/botkit/src/pages.tsx
  • packages/botkit/src/repository.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1251d82c9a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/botkit-postgres/package.json Outdated
dahlia added 2 commits July 5, 2026 10:55
Fediverse usernames are looked up with varying casing: WebFinger acct:
resources, mentions, and profile page visits do not preserve the case
the bot was registered with.  Static username matching in mapHandle()
now lowercases both sides, and registering a bot whose username
differs from an existing one only in case is rejected, since the two
would be indistinguishable to such lookups.  Group mapUsername
callbacks and the username-as-identifier fallback for dynamic groups
are unchanged: the former is user code and identifiers remain
case-sensitive.

#24 (comment)
#24 (comment)

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5.5
Claude-Session: https://claude.ai/code/session_0157FUYXeusCEmbWyYnwt3Cn
The bare workspace: protocol in the repository packages' peer
dependency on @fedify/botkit is replaced by pnpm publish with the
exact version (the published botkit-sqlite 0.4.3 pins peer 0.4.3), so
installing them alongside any later 0.5.x of the core package would
fail peer resolution.  workspace:^ keeps the local workspace linking
and publishes as ^0.5.0 instead; verified by packing the tarball and
inspecting its package.json.

#24 (comment)

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5.5
Claude-Session: https://claude.ai/code/session_0157FUYXeusCEmbWyYnwt3Cn
@dahlia

dahlia commented Jul 5, 2026

Copy link
Copy Markdown
Member Author

@codex review

@dahlia

dahlia commented Jul 5, 2026

Copy link
Copy Markdown
Member Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for hosting multiple bots on a single server instance in @fedify/botkit (v0.5.0), adding the createInstance() function and Instance interface to manage shared infrastructure. It updates the Repository interface and database adapters (botkit-sqlite and botkit-postgres) to support bot-scoped storage with legacy schema migrations, scopes local object URIs with bot identifiers, and types Session.bot as ReadonlyBot. Feedback on the implementation highlights a potential runtime TypeError when generating ETag headers if fileInfo.mtime is null, as well as an invalid generic type cast on Uint8Array that could cause compilation errors.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread packages/botkit/src/instance-impl.ts
Comment thread packages/botkit/src/instance-impl.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6fe1533320

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/botkit/src/pages.tsx
dahlia added 2 commits July 5, 2026 11:13
On a multi-bot instance the web app serves hashtag pages only under
/@{username}/tags/{tag}, but the hashtag renderers kept emitting
origin-rooted /tags/{tag} URLs, so every hashtag link in a published
post led to a 404.  HashtagText and the Markdown hashtag plugin now
derive their base from the bot's own web path; on a single-bot
instance that base is the origin itself, so pre-0.5 URLs are
unchanged.  Sessions constructed outside BotKit (as in tests) fall
back to the origin as before.

#24 (comment)

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5.5
Claude-Session: https://claude.ai/code/session_0157FUYXeusCEmbWyYnwt3Cn
When a custom emoji file has no mtime, the ETag interpolated the
string "undefined" through the short-circuited optional chain, while
Last-Modified already fell back to the current time.  Both headers
now derive from the same fallback value.

#24 (comment)

Assisted-by: Claude Code:claude-fable-5
Assisted-by: Codex:gpt-5.5
Claude-Session: https://claude.ai/code/session_0157FUYXeusCEmbWyYnwt3Cn
@dahlia

dahlia commented Jul 5, 2026

Copy link
Copy Markdown
Member Author

@codex review

@dahlia

dahlia commented Jul 5, 2026

Copy link
Copy Markdown
Member Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for hosting multiple bots on a single server instance in BotKit (version 0.5.0). It adds the createInstance() function and Instance interface to manage shared infrastructure (key-value store, message queue, repository, and HTTP handling) across multiple bots, each with its own actor identity and event handlers. To support this multi-tenancy, the Repository interface and its implementations (KvRepository, SqliteRepository, and PostgresRepository) have been updated to scope data by bot actor identifiers, including automatic schema migration from legacy layouts. Additionally, local object URIs now carry the bot identifier, with backward compatibility for legacy URIs, and Session.bot has been made read-only (ReadonlyBot) to prevent session-based mutations. Comprehensive documentation, tests, and web page routing updates have also been added to support these new multi-bot capabilities. I have no feedback to provide as there are no review comments to assess.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

Reviewed commit: da6b208da4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@dahlia dahlia merged commit cfc4181 into main Jul 5, 2026
6 checks passed
@dahlia dahlia deleted the multitenancy branch July 5, 2026 02:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support multiple bots per instance

1 participant