Skip to content

feat: clean up AI booboos, fix bridgev2 usage#103

Open
batuhan wants to merge 221 commits intomainfrom
batuhan/sins
Open

feat: clean up AI booboos, fix bridgev2 usage#103
batuhan wants to merge 221 commits intomainfrom
batuhan/sins

Conversation

@batuhan
Copy link
Copy Markdown
Member

@batuhan batuhan commented Apr 12, 2026

No description provided.

batuhan added 16 commits April 12, 2026 13:28
Move AI runtime login state out of bridge login metadata into a dedicated DB-backed runtime state (aichats_login_state). Add login_state_db.go with load/save/update/clear helpers and wire usage across AI client code (NextChatIndex, LastHeartbeatEvent, DefaultChatPortalID, LastActiveRoomByAgent, etc.). Refactor scheduler to use an internal runtime context and in-process timers instead of Matrix delayed events (remove pending_delay_* usage), update cron/heartbeat scheduling logic and DB read/write to match the new approach, and stop/cleanup scheduler on disconnect. Remove Matrix-specific coupling and connector event handler registration, and switch message-status sending to the agentremote helper. Includes DB migration/schema updates (pkg/aidb changes) to support the new login state table and scheduler column removals.
Move tool approval rules out of the in-memory login state and into a dedicated DB table (aichats_tool_approval_rules) with lookups/inserts. Remove several login_state fields and related helpers (default_chat_portal_id, tool_approvals_json, last_active_room_by_agent_json, and associated types/functions) and simplify load/save to only manage next_chat_index and last_heartbeat_event. Add DB migration to create the new table and index, and delete tool approval rules on logout. Replace in-memory checks/persistence with DB-backed has/insert functions, update imports, and adjust related call sites (message status and AIRoomInfo now use agentremote). Also remove a now-unused portal reference cleanup call from chat deletion.
Update imports and type/function references to the agentremote SDK package (github.com/beeper/agentremote/sdk) across bridge code. Replace old/ambiguous imports and previous sdk/bridgesdk aliases with agentremote, and update types and calls (e.g. Turn, Writer, ApprovalRequest/ApprovalHandle/ToolApprovalResponse, TurnSnapshot/TurnData builders, SetRoomName/SetRoomTopic, ApplyDefaultCommandPrefix, BuildCompactFinalUIMessage, SnapshotFromTurnData, etc.). Tests and helpers were updated to match the SDK package reorganization and new import alias.
Unify and standardize bridge metadata and display names across connectors and entry points. Updated connector descriptions to reference the AgentRemote SDK, simplified DisplayName values (e.g. "Beeper Cloud" -> "AI", "OpenClaw Bridge" -> "OpenClaw Gateway", etc.), and adjusted bridge entry descriptions to match. Also changed OpenAI login StepIDs from the openai namespace to the ai namespace and updated the corresponding test to expect the new display name. No behavioral changes to network URLs or protocol IDs.
Update branding for the AI bridge: change the SDK config Description and DisplayName to "AI Chats bridge for Beeper" / "Beeper AI". Files updated: bridges/ai/constructors.go (Description, DisplayName), bridges/ai/constructors_test.go (expected DisplayName), and cmd/internal/bridgeentry/bridgeentry.go (Definition.Description). No network URLs or IDs were changed.
Add constants for AI DB table names and replace hardcoded table identifiers across AI database code (sessions, login state, internal messages, system events). Add NetworkCapabilities provisioning hints for AI, Codex, and OpenClaw connectors. Simplify agent ID normalization and system events/session handling (scoped login scope helpers, normalized agent ID filtering). Refactor OpenClaw to scope ghost IDs by login, include login in avatar IDs, persist and load OpenClaw login state (save/load device tokens and session sync state), and adjust session key resolution logic. Simplify DummyBridge by removing synthetic provisioning and unused helpers, and remove Codex host-auth reconciliation and related tests/helpers. Misc: remove unused imports and update tests to match the new APIs/behaviour.
Remove low-level escape hatches and legacy handler plumbing from the SDK.

- Drop RawEvent/RawMsg/RawEdit/RawReaction fields from Message, MessageEdit and Reaction types and remove the event import.
- Stop populating those raw fields in convertMatrixMessage.
- Remove many SDK client handler implementations (edit, redaction, typing, room name/topic, backfilling, delete chat, identifier resolution, contact listing and search) and the related compile-time interface checks, leaving a single NetworkAPI check.
- Update connector to use loginAwareClient when setting user login.
- Delete the client_resolution_test.go unit tests that covered identifier resolution/contact listing/search.

This simplifies the SDK surface by removing escape hatches and legacy handler boilerplate; callers should use higher-level APIs or implement needed handlers via the new configuration surface.
Add persistent storage for AI login configuration and SDK conversation state. Introduce aichats_login_config table and new load/save helpers (loadAIUserLoginConfig, saveAIUserLogin) in bridges/ai/login_config_db.go; update callers to use saveAIUserLogin so AI metadata is stored in DB and compacted on the runtime login row. Add SDK conversation state DB storage with sdk_conversation_state table and DB-backed load/save logic in sdk/conversation_state.go. Update SQL migration (pkg/aidb/001-init.sql) and tests to include the new table. Also tidy misc related changes: add aiLoginConfigTable const, propagate context to login loaders, normalize agent ID usages, simplify OpenCode login instance builders (remove unused instanceID), switch OpenClaw base64 output to base64.RawURLEncoding, and minor imports/formatting adjustments.
Introduce a DB-backed openClawPortalState for OpenClaw: add openClawPortalDBScope helper, ensureOpenClawPortalStateTable, load/save/clear functions, and replace catalog/metadata callers to use the portal state rather than in-meta fields. Refactor OpenClaw PortalMetadata into a minimal struct and move many runtime fields into openClawPortalState; update catalog logic to operate on state. Remove SDKPortalMetadata carriers and their getters/setters from dummybridge and opencode PortalMetadata (and drop the related test). Tighten SDK conversation state checks to handle nil portal.Portal, and update conversation state tests to use an in-memory sqlite Bridge DB and the DB-backed conversation state store; adjust test helpers and expectations accordingly.
Clarify and reformat portal metadata fields and persistable state: updated PortalMetadata comment, aligned field formatting, and adjusted persisted aiPersistedPortalState struct formatting. OpenClaw capability handling refactored: GetCapabilities now builds capabilities from a profile, added openClawCapabilitiesFromProfile, capabilityIDForPortalState and maybeRefreshPortalCapabilities to centralize capability ID generation and conditional refresh. Tests updated to use openClawPortalState (instead of PortalMetadata) and to reflect API changes in helpers that build OpenClaw message/history metadata. Also added IsOpenClawRoom field to openClawPortalState. Mostly mechanical changes to improve clarity and enable capability refresh logic.
Large refactor of the AI bridge: removed Matrix-specific helper files and pin/reaction state APIs, and simplified portal/room handling. Default chat creation and selection logic was streamlined (deterministic key handling removed), listAllChatPortals now queries the AI portal state table, and room name/topic setters were removed in favor of updating portal metadata directly. Reaction removal was consolidated to a new removeReaction flow that uses the bridge DB, and message delete/pin/list-pin handlers were removed. Added session transcript support and adjusted integration host DB access/renames and Codex client to use portal state records.
Cleanup: remove several unused helper functions and perform minor import/formatting tidy-ups. Removed clonePortalState (bridges/ai/portal_state_db.go), hostAuthLoginID, hasManagedCodexLogin, and resolveCodexCommand (bridges/codex/connector.go), and clearOpenClawPortalState (bridges/openclaw/metadata.go). Also adjusted imports and spacing in multiple sdk and bridge files (bridges/ai/session_store.go, bridges/ai/system_events_db.go, sdk/*) to improve code clarity. No functional behavior changes intended.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 12, 2026

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced AI bridge state persistence and recovery with improved database-backed configuration management
    • Better session routing and heartbeat delivery with dedicated portal state tracking
    • Improved message history persistence and context reconstruction
  • Improvements

    • Refactored SDK integration for more reliable provider handling and authentication
    • Streamlined approval flow implementation with better turn-based decision tracking
    • Optimized portal metadata handling with dedicated state stores per provider
    • Renamed service branding from "Beeper Cloud" to "Beeper AI" / "AI Chats"
  • Bug Fixes

    • Fixed session routing fallback logic for agent activity tracking
    • Improved desktop API credential resolution and instance management
    • Enhanced error handling in message persistence and portal cleanup
  • Internal Changes

    • Consolidated SDK usage and removed deprecated import aliases
    • Refactored state management from login metadata to dedicated configuration/runtime stores
    • Improved test infrastructure with database-backed fixtures

Walkthrough

This pull request undertakes a comprehensive migration from the github.com/beeper/agentremote package structure to github.com/beeper/agentremote/sdk as the canonical SDK import across all bridges. The refactoring simultaneously restructures how login configuration, portal metadata, and message persistence are managed, introducing new database-backed state models (loginRuntimeState, openClawPortalState, codexPortalState), implementing scope-aware portal/session helpers, and removing Matrix-specific integrations while expanding AI-owned turn/transcript persistence.

Changes

Cohort / File(s) Summary
SDK Migration (Import Consolidation)
bridges/ai/abort_helpers_test.go, bridges/ai/bridge_info.go, bridges/ai/broken_login_client.go, bridges/codex/compat_helpers.go, bridges/openclaw/sdk_agent.go, and 30+ additional files
Replaced github.com/beeper/agentremote and github.com/beeper/agentremote/sdk (aliased as bridgesdk) imports with direct github.com/beeper/agentremote/sdk usage. Updated all call sites for approval flows, message IDs, event timing, capability building, login validation, and metadata helpers to use sdk.* equivalents.
Login Configuration Refactoring
bridges/ai/login_config_db.go (new), bridges/ai/login_loaders.go, bridges/ai/login.go, bridges/ai/token_resolver.go, bridges/ai/provisioning.go
Split login state from persisted UserLoginMetadata (credentials, timezone, profile, gravatar) into database-backed aiLoginConfig model with lazy-loaded snapshots and mutation helpers. Updated login flow to use completeLogin with loginCompletionInput and new saveAILoginConfig persistence.
Runtime State Separation
bridges/ai/login_state_db.go (new), bridges/ai/heartbeat_execute.go, bridges/ai/heartbeat_state.go, bridges/ai/responder_metadata_test.go, and 15+ test files
Introduced loginRuntimeState for transient per-login data (ModelCache, FileAnnotationCache, error tracking, heartbeat state); replaced in-memory metadata mutation with context-scoped snapshots and conditional persistence via ensureLoginStateLoaded, loginStateSnapshot, updateLoginState.
Portal Metadata Simplification
bridges/ai/metadata.go, bridges/codex/metadata.go, bridges/dummybridge/metadata.go, bridges/openclaw/identifiers.go
Removed persistent fields from PortalMetadata (Title, Slug, CodexThreadID, CodexCwd, ElevatedLevel, AwaitingCwdSetup, ManagedImport, ModuleMeta, ToolApprovalsConfig, HeartbeatState, SessionBootstrapByAgent, custom agents cache). Introduced bridge-specific state types (openClawPortalState, codexPortalState) persisted via separate helpers; replaced module-meta internal-room detection with InternalRoomKind field.
Bridge-Specific Portal State
bridges/openclaw/manager.go, bridges/openclaw/events.go, bridges/openclaw/catalog.go, bridges/codex/directory_manager.go, bridges/codex/client.go
Added persistent state models for OpenClaw (CodexThreadID, ManagedImport, CodexCwd, background/backfill cursors, token usage, session keys) and Codex (Title, Slug, internal room kind) loaded/saved via new helper functions; updated portal enrichment and state mutation to target bridge-specific state instead of generic metadata.
Database Schema Expansion
bridges/ai/bridge_db.go, bridges/ai/login_state_db.go, bridges/ai/custom_agents_db.go, bridges/ai/session_store.go, bridges/ai/turn_store.go (new)
Added table constants for AI-scoped storage (sessions, login_state, custom_agents, portal_state, tool_approval_rules, turns/refs, cron/heartbeat jobs and run keys, system events). Introduced loginScope, portalScope database-accessor abstractions and helper queries for custom agents, portal state, turn persistence, and run-key tracking.
Message Persistence Refactor
bridges/ai/turn_store.go (new), bridges/ai/message_helpers.go (new), bridges/ai/streaming_persistence.go, bridges/ai/internal_dispatch.go
Introduced AI-owned turn storage separate from bridge messages via upsertAITurn, persistAIConversationMessage, persistAIInternalPromptTurn, with per-portal context epochs and sequence tracking. Updated message creation to populate CanonicalTurnData from sdk.TurnData instead of constructing from prompt blocks; added upsertTransportPortalMessage for bridge-layer message upserting.
Portal/Message Lookup Helpers
bridges/ai/message_parts.go (new), bridges/ai/identifiers.go, bridges/ai/reactions.go
Added loadPortalMessagePartByMXID, loadPortalMessagePartByID methods for portal-scoped message lookup; updated reaction/edit/reply logic to use portal-scoped lookups instead of unscoped DB queries. Changed identifier helpers to use sdk.* equivalents and added setPortalResolvedTarget for ghost/metadata synchronization.
Queue/Dispatch Refactoring
bridges/ai/queue_runtime.go (new), bridges/ai/queue_status.go, bridges/ai/queue_settings.go (changed), bridges/ai/pending_queue.go
Introduced dispatchOrQueueCore, processPendingQueue for unified queue orchestration with room locking, debounce timing, backlog management, and async draining. Replaced session-based queue-settings lookup with explicit configuration-derived parameters; removed resolveQueueSettingsForPortal and applyQueueDropPolicy in favor of inline airuntime.ResolveQueueOverflow handling.
Heartbeat State Restructuring
bridges/ai/heartbeat_events.go, bridges/ai/heartbeat_state.go, bridges/ai/heartbeat_execute.go, bridges/ai/heartbeat_delivery.go (deleted), bridges/ai/heartbeat_session.go (deleted)
Replaced per-session heartbeat dedup with agent-scoped managedHeartbeatState; consolidated heartbeat routing into single resolveHeartbeatRoute helper returning heartbeatRoute struct; removed delayed-event persistence and session-based route recording in favor of touchStoredSession and heartbeat-specific state updates.
Removal of Matrix-Specific Features
bridges/ai/matrix_coupling.go (deleted), bridges/ai/scheduler_events.go (modified), bridges/ai/command_registry.go (modified)
Deleted Matrix delayed-event coupling helpers and ScheduleTickEventType registration; removed BroadcastCommandDescriptions and command-description state content construction; replaced schedule-tick Matrix event handling with in-process timer mechanism in scheduler.
Tool Approval Refactoring
bridges/ai/tool_approvals.go, bridges/ai/tool_approvals_rules.go (deleted), bridges/codex/approval_rpc.go (new), bridges/codex/approval_runtime.go (new)
Migrated tool approval storage from login metadata to sdk.ApprovalRequest-based runtime flow; removed metadata-based "always allow" rule checking and persistence (replaced by DB-backed rule lookups in new code). Added Codex-specific RPC approval handlers with elevated-level elevation and session-scope grants.
Agent/Custom Agent Management
bridges/ai/agentstore.go, bridges/ai/custom_agents_db.go (new), bridges/ai/approval_prompt_presentation.go (deleted)
Updated AgentStoreAdapter to load custom agents from DB via new helpers rather than login metadata; removed exported constructors NewAgentStoreAdapter/NewBossStoreAdapter. Deleted approval presentation builders (no longer needed with SDK-based approval flow).
Prompt Building Refactor
bridges/ai/prompt_builder.go, bridges/ai/canonical_prompt_messages.go, bridges/ai/canonical_history.go (deleted), bridges/ai/prompt_projection_local.go (deleted), bridges/ai/prompt_context_local.go
Added buildUserPromptTurn, promptMessagesFromTurnData to synchronize between blocks and SDK turn data; refactored history replay to use loaded AITurn records with optional image injection. Simplified prompt-context conversion by removing message-buffer and per-type conversion functions; deleted obsolete history bundling and downloaded-image buffering helpers.
Integration Module Wiring
bridges/ai/integrations.go, bridges/ai/integration_host.go, bridges/ai/module_config.go (new), bridges/ai/module_config_test.go (new)
Replaced integrationmodules.BuiltinModules with explicit cron/memory module registration; removed portal-creation/heartbeat-scheduling helpers from integration host. Added per-module enablement gating and module-config normalization helpers for agent-specific settings (e.g., memory search).
Test Infrastructure
bridges/ai/test_login_helpers_test.go (new), bridges/ai/persistence_boundaries_test.go (new), bridges/ai/agent_activity_test.go (new), and 40+ updated test files
Added centralized AIClient test constructors (newTestAIClientWithProvider, newDBBackedTestAIClient) and state setters (setTestLoginConfig, setTestLoginState, seedTestCustomAgent); updated all affected tests to use new helpers instead of manual database.UserLogin/bridgev2.UserLogin construction.
Miscellaneous Removals and Simplifications
bridges/ai/approval_prompt_presentation.go, bridges/ai/tools_matrix_api.go, bridges/ai/client_runtime_helpers.go, bridges/ai/queue_helpers.go, and others
Deleted Matrix API wrappers (pinned events, reactions, user profiles), approval presentation builders, no-op handler methods (read receipt, mute), generic queue overflow handlers, and miscellaneous unused helpers; removed unused method AIClient.BroadcastCommandDescriptions.
OpenClaw/Openclaw Gateway Refactoring
bridges/openclaw/identifiers.go, bridges/openclaw/manager.go, bridges/openclaw/gateway_client.go, bridges/openclaw/login.go
Changed ghost user ID format from openclaw-agent:<url-escaped> to v1:openclaw-agent:<base64url> with scoped variant; updated device token/sync state/backfill cursors to use bridge-specific persisted state. Replaced SHA-256 hashing with stringutil.ShortHash for consistent hash derivation.
Documentation and Config Updates
README.md, .github/workflows/*, bridges.manifest.yml, .gitignore, bridges/ai/integrations_config.go
Updated bridge descriptions from "Beeper Cloud" to "Beeper AI" / "Beeper AI Chats"; removed opencode/openclaw instances from manifest; updated workflow names for AgentRemote Manager; added .codex-tmp to gitignore.

Sequence Diagram(s)

The changes do not introduce new features or substantially alter control flow in a way that would benefit from sequence diagram visualization—rather, they refactor existing functionality by migrating packages, restructuring state storage, and consolidating similar patterns. The complexity is primarily in state-model and database-layer changes rather than new cross-component interactions.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120+ minutes

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch batuhan/sins

batuhan added 2 commits April 12, 2026 15:31
Rename user-facing references to "AgentRemote CLI" across workflows, README, docker docs, help text, and Homebrew cask generation. Introduce defaultCodexClientInfoName/title constants, apply them in the Codex connector and add a unit test to assert defaults. Add a user-agent base constant for OpenClaw Gateway and use it when building client identity and tests. Minor prompt and help string tweaks to clarify wording and usage.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 17

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (9)
bridges/ai/command_registry.go (1)

197-210: ⚠️ Potential issue | 🟠 Major

Remove dead code buildCommandDescriptionContent and its test.

The BroadcastCommandDescriptions method no longer uses this function—it now directly constructs sdk.Command objects instead. The only remaining usage is in events_test.go:51, which tests the unused function directly. Remove both the function definition and the test.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/command_registry.go` around lines 197 - 210, Remove the dead
helper buildCommandDescriptionContent and its associated unit test that directly
invokes it (the test in events_test that targets
buildCommandDescriptionContent); update references so
BroadcastCommandDescriptions continues to construct sdk.Command objects directly
and delete both the function buildCommandDescriptionContent and the test file's
test case that calls it. Ensure no other code references
buildCommandDescriptionContent remain and run the test suite to confirm removal
is safe.
sdk/conversation_state.go (2)

75-106: ⚠️ Potential issue | 🟠 Major

Avoid caching conversation state under the empty key.

When portal.Portal == nil, conversationStateKey returns "", but get and set still read/write rooms[""]. That lets unrelated partially initialized portals share cached state.

Suggested fix
 func (s *conversationStateStore) get(portal *bridgev2.Portal) *sdkConversationState {
 	if s == nil || portal == nil {
 		return &sdkConversationState{}
 	}
 	key := conversationStateKey(portal)
+	if key == "" {
+		return &sdkConversationState{}
+	}
 	s.mu.RLock()
 	state := s.rooms[key]
 	s.mu.RUnlock()
 	if state != nil {
 		return state.clone()
@@
 func (s *conversationStateStore) set(portal *bridgev2.Portal, state *sdkConversationState) {
 	if s == nil || portal == nil {
 		return
 	}
 	key := conversationStateKey(portal)
+	if key == "" {
+		return
+	}
 	s.mu.Lock()
 	s.rooms[key] = state.clone()
 	s.mu.Unlock()
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/conversation_state.go` around lines 75 - 106, The code currently allows
storing/reading under an empty key when conversationStateKey(portal) returns "",
causing unrelated portals to share state; update conversationStateStore.get and
conversationStateStore.set to treat an empty key as invalid: after computing key
:= conversationStateKey(portal) return a new empty sdkConversationState (without
accessing s.rooms) if key == "" in get, and return immediately without writing
to s.rooms if key == "" in set; reference conversationStateKey,
conversationStateStore.get, conversationStateStore.set, s.rooms and ensure the
same nil checks remain.

134-176: ⚠️ Potential issue | 🔴 Critical

Add a legacy-state fallback before switching reads to DB-only storage.

loadConversationState now consults only the cache and sdk_conversation_state table. Existing deployments that still have conversation state in portal metadata will silently load defaults until something rewrites the room—a regression unless a migration backfills the data or a fallback read from legacy storage is implemented.

The test explicitly validates that portal.Metadata remains untouched after saving (line 65-66), confirming the migration path doesn't write to the old storage location. No backfill or legacy read fallback is present in the codebase.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/conversation_state.go` around lines 134 - 176, loadConversationState
currently only checks the cache and DB, so add a legacy fallback: when state is
still nil/default after consulting the store and DB (inside
loadConversationState), inspect portal.Metadata for the legacy
conversation-state entry (the key used previously for storing state in portal
metadata), unmarshal that JSON into an sdkConversationState, and use it as the
loaded state (without mutating portal.Metadata). After loading from legacy
metadata, call state.ensureDefaults() and store.set(portal, state) as currently
done so the rest of the code treats it like a normal load; keep
loadConversationStateFromDB unchanged.
pkg/aidb/db_test.go (1)

53-68: ⚠️ Potential issue | 🟡 Minor

Assert the rest of the new v1 tables here too.

pkg/aidb/001-init.sql now creates aichats_internal_messages, aichats_login_state, and aichats_tool_approval_rules as part of the fresh schema, but this test doesn't verify them. If upgrade stops creating any of those tables, TestUpgradeV1Fresh will still pass.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/aidb/db_test.go` around lines 53 - 68, TestUpgradeV1Fresh is missing
assertions for newly added v1 tables; update the test (TestUpgradeV1Fresh in
pkg/aidb/db_test.go) to include checks for the three tables created by
pkg/aidb/001-init.sql: aichats_internal_messages, aichats_login_state, and
aichats_tool_approval_rules so the fresh-schema path fails if any of those
tables are not created during upgrade.
sdk/conversation.go (1)

47-52: ⚠️ Potential issue | 🔴 Critical

Restore the nil receiver guard in getIntent.

Line 51 now dereferences c unconditionally. A nil *Conversation will panic here before returning an error, and that propagates into SendMedia, SendNotice, sendMessageContent, and SetTyping.

🐛 Proposed fix
 func (c *Conversation) getIntent(ctx context.Context) (bridgev2.MatrixAPI, error) {
+	if c == nil {
+		return nil, fmt.Errorf("conversation unavailable")
+	}
 	if c != nil && c.intentOverride != nil {
 		return c.intentOverride(ctx)
 	}
 	return resolveMatrixIntent(ctx, c.login, c.portal, c.sender, bridgev2.RemoteEventMessage)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/conversation.go` around lines 47 - 52, The getIntent method on
Conversation now dereferences c unconditionally which will panic for a nil
*Conversation and cascade into SendMedia, SendNotice, sendMessageContent, and
SetTyping; restore the nil receiver guard by checking if c == nil at the top of
Conversation.getIntent and return a sensible error (e.g., fmt.Errorf or a
package-level err like ErrNoConversation) instead of calling
resolveMatrixIntent, and keep the existing intentOverride branch (i.e., if c !=
nil && c.intentOverride != nil return c.intentOverride(ctx)); ensure callers
that expect an error continue to propagate it.
bridges/ai/login.go (1)

239-248: ⚠️ Potential issue | 🟠 Major

Avoid returning an error after a successful NewLogin without rollback.

ol.User.NewLogin(...) has already created the login before saveAIUserLogin(...) runs. If the second write fails, the user sees a failed login while the new login still exists, which can strand state and make retries/relogins produce duplicate or confusing accounts. Please make these writes atomic, or explicitly clean up the just-created login before returning.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/login.go` around lines 239 - 248, The code calls
ol.User.NewLogin(...) which creates a login and then calls saveAIUserLogin(...);
if saveAIUserLogin fails you must roll back the created login to avoid stranded
state. Modify the error path after saveAIUserLogin to call the appropriate
deletion method (e.g., ol.User.DeleteLogin(ctx, loginID) or equivalent) to
remove the created login (and handle/log any deletion error), then return the
original wrapped save error; ensure you reference ol.User.NewLogin,
saveAIUserLogin, and loginID when implementing the rollback so creation is
undone on failure.
bridges/ai/tool_approvals_rules.go (1)

59-80: ⚠️ Potential issue | 🟠 Major

Don't use context.Background() for approval-rule lookups.

These checks are on the tool/approval path now that they hit the DB. Using context.Background() makes them uncancellable, so a slow or wedged DB can stall message handling indefinitely. Please thread the caller context through these methods, or wrap the queries in a short timeout.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/tool_approvals_rules.go` around lines 59 - 80, The approval checks
isMcpAlwaysAllowed and isBuiltinAlwaysAllowed currently call
hasToolApprovalRule/hasBuiltinToolApprovalRule with context.Background(), making
DB lookups uncancellable; change these functions to accept a context.Context
parameter (e.g., ctx) and pass that ctx into oc.hasToolApprovalRule and
oc.hasBuiltinToolApprovalRule, or alternatively create a derived context with a
short timeout inside each function and use that for the DB call; update all
callers of isMcpAlwaysAllowed and isBuiltinAlwaysAllowed to supply the caller
context so the DB queries are cancellable.
bridges/ai/chat.go (1)

1095-1132: ⚠️ Potential issue | 🟠 Major

Keep a legacy-room fallback before creating a new default chat.

This path now only checks defaultChatPortalKey(...). For existing logins whose default AI room was created under an older key/selection path, bootstrap will create a second default room instead of reusing the current one.

Falling back to listAllChatPortals(...) / chooseDefaultChatPortal(...) before creating a new portal would preserve existing rooms during upgrade.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/chat.go` around lines 1095 - 1132, Before creating a new default
portal, add a legacy-room fallback: after the GetExistingPortalByKey check fails
(portal == nil) call oc.listAllChatPortals(ctx, oc.UserLogin) and pass the
result into oc.chooseDefaultChatPortal(...) to see if an older portal should be
reused; if chooseDefaultChatPortal returns a portal, reuse it (materialize if
MXID missing via oc.materializePortalRoom) and return nil instead of proceeding
to create a new portal with oc.initPortalForChat; keep existing logging behavior
and error handling around materializePortalRoom and initPortalForChat, and
reference the existing symbols GetExistingPortalByKey, listAllChatPortals,
chooseDefaultChatPortal, materializePortalRoom, and initPortalForChat when
making the change.
bridges/codex/directory_manager.go (1)

133-164: ⚠️ Potential issue | 🟠 Major

Save the welcome-room state before creating the Matrix room.

EnsurePortalLifecycle(...) can succeed before saveCodexPortalState(...) runs. If the save fails afterward, the room already exists but there is no persisted codexPortalState to find or repair it on restart.

Suggested ordering
  state := &codexPortalState{
  	Title:            "New Codex Chat",
  	Slug:             "codex-welcome",
  	AwaitingCwdSetup: true,
  }
  portalMeta(portal).IsCodexRoom = true
+ if err := saveCodexPortalState(ctx, portal, state); err != nil {
+ 	return nil, err
+ }
  if err := sdk.ConfigureDMPortal(ctx, sdk.ConfigureDMPortalParams{
  	Portal:      portal,
  	Title:       state.Title,
  	OtherUserID: codexGhostID,
  	Save:        false,
  }); err != nil {
  	return nil, err
  }
  info := cc.composeCodexChatInfo(portal, state, false)
  created, err := sdk.EnsurePortalLifecycle(ctx, sdk.PortalLifecycleOptions{
  	Login:             cc.UserLogin,
  	Portal:            portal,
  	ChatInfo:          info,
  	SaveBeforeCreate:  true,
  	AIRoomKind:        sdk.AIRoomKindAgent,
  	ForceCapabilities: true,
  })
  if err != nil {
  	return nil, err
  }
- if err := saveCodexPortalState(ctx, portal, state); err != nil {
- 	return nil, err
- }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/codex/directory_manager.go` around lines 133 - 164, The current flow
calls sdk.EnsurePortalLifecycle (in bridges/codex/directory_manager.go) before
persisting codexPortalState with saveCodexPortalState, which can create the
Matrix room without saved state; move the saveCodexPortalState call to occur
immediately after initializing the codexPortalState and before calling
sdk.EnsurePortalLifecycle, so the state is persisted (and errors returned) prior
to creating the room; keep the calls to portalMeta(portal).IsCodexRoom = true
and sdk.ConfigureDMPortal/cc.composeCodexChatInfo/sendSystemNotice intact, but
ensure saveCodexPortalState is executed and its error handled before invoking
EnsurePortalLifecycle to avoid orphaned rooms without persisted state.
🧹 Nitpick comments (13)
cmd/internal/bridgeentry/bridgeentry.go (1)

21-50: Inconsistent description format for AI bridge.

The AI definition uses a different description format ("AI Chats bridge for Beeper") compared to all other bridges which follow the pattern "X bridge built with the AgentRemote SDK." If this is intentional, consider adding a comment explaining why. Otherwise, align the format for consistency.

 	AI = Definition{
 		Name:        "ai",
-		Description: "AI Chats bridge for Beeper",
+		Description: "AI Chats bridge built with the AgentRemote SDK.",
 		Port:        29345,
 		DBName:      "ai.db",
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/internal/bridgeentry/bridgeentry.go` around lines 21 - 50, The AI
Definition's Description ("AI Chats bridge for Beeper") is inconsistent with the
other bridge Definitions (Codex, OpenCode, OpenClaw, DummyBridge) which use the
"X bridge built with the AgentRemote SDK." pattern; update the AI variable's
Description in the Definition for AI to match that pattern (e.g., "AI bridge
built with the AgentRemote SDK.") or, if the different wording is intentional,
add a brief inline comment above the AI Definition explaining the special case
so reviewers understand the deviation.
cmd/agentremote/profile.go (1)

26-42: Comments reference hardcoded "sdk" path but code uses dynamic binaryName.

The comments on lines 26 and 35 hardcode "sdk" in the path description, but the actual code uses the binaryName variable. If binaryName changes in the future, these comments will be misleading.

📝 Suggested fix to make comments more accurate
-// configRoot returns ~/.config/sdk
+// configRoot returns ~/.config/<binaryName>
 func configRoot() (string, error) {
 	home, err := os.UserHomeDir()
 	if err != nil {
 		return "", err
 	}
 	return filepath.Join(home, ".config", binaryName), nil
 }

-// profileRoot returns ~/.config/sdk/profiles/<profile>
+// profileRoot returns ~/.config/<binaryName>/profiles/<profile>
 func profileRoot(profile string) (string, error) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/agentremote/profile.go` around lines 26 - 42, Update the doc comments for
configRoot and profileRoot to reflect that the path uses the dynamic binaryName
variable rather than the hardcoded "sdk" string: change the comment above
configRoot to indicate it returns ~/.config/<binaryName> and the comment above
profileRoot to indicate it returns ~/.config/<binaryName>/profiles/<profile>,
referencing the functions configRoot and profileRoot so future readers know the
path is derived from binaryName.
bridges/opencode/opencode_instance_state.go (1)

100-110: Consider deterministic ordering in sessionIDs().

Go map iteration order is non-deterministic; sorting out before return can reduce flaky ordering in callers/tests/logs.

Suggested refactor
 func (inst *openCodeInstance) sessionIDs() []string {
 	if inst == nil {
 		return nil
 	}
 	inst.seenMu.Lock()
 	defer inst.seenMu.Unlock()
 	out := make([]string, 0, len(inst.knownSessions))
 	for sessionID := range inst.knownSessions {
 		out = append(out, sessionID)
 	}
+	sort.Strings(out)
 	return out
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/opencode/opencode_instance_state.go` around lines 100 - 110, The
sessionIDs() method returns session IDs in non-deterministic map order; lock
seenMu, collect keys from inst.knownSessions as done, then sort the slice (e.g.,
using sort.Strings) before returning to ensure deterministic ordering for
callers, tests, and logs; update the function (sessionIDs) to import the sort
package and call sort.Strings(out) right before the return.
pkg/shared/toolspec/toolspec.go (1)

143-145: Encode the emoji requirement in the schema, not just the description.

Line 144 now says emoji is required when remove:true, but the schema still accepts a reaction-removal payload without it. If anything downstream relies on schema validation, malformed react requests will still get through. Consider adding an if/then or oneOf rule for the react action.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/shared/toolspec/toolspec.go` around lines 143 - 145, The schema currently
documents that "emoji" is required when "remove": true for the "react" action
but doesn't enforce it; update the toolspec schema construction in toolspec.go
(the code building properties like StringProperty("emoji") and
BooleanProperty("remove") for the "react" action) to add a conditional JSON
Schema rule that enforces emoji when remove is true — for example add an if/then
clause scoped to action === "react" (if: {properties:{action:{const:"react"},
remove:{const:true}}} then: {required:["emoji"]}) or express the same via oneOf
alternatives; ensure the new rule is attached to the same schema object used by
the validator so malformed react/remove payloads are rejected.
bridges/ai/identifiers.go (1)

171-171: Thread caller context through portalMeta() to propagate cancellation for DB-backed portal state loads.

Line 171 uses context.Background() when calling loadPortalStateIntoMetadata(), preventing request cancellation/timeouts from propagating to the database operation. Since portalMeta() is an accessor-style helper with 50+ call sites across the codebase, refactoring it to accept a context parameter would require updating all callers, though many already have context available in their call path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/identifiers.go` at line 171, The call to
loadPortalStateIntoMetadata is using context.Background() inside portalMeta(),
preventing cancellation/timeouts from propagating; change portalMeta to accept a
context.Context parameter and propagate that ctx into
loadPortalStateIntoMetadata (i.e., call loadPortalStateIntoMetadata(ctx, portal,
meta) instead of using Background()), then update portalMeta call sites to pass
the available request/context where present (and only use context.Background()
as an explicit fallback in rare callers that truly lack a context), ensuring all
DB-backed portal state loads honor cancellation.
bridges/ai/logout_cleanup.go (2)

60-71: Inconsistent table name usage.

Line 45 uses the aiSessionsTable variable for the sessions table, but these new deletions use hardcoded table names (aichats_internal_messages, aichats_tool_approval_rules, aichats_login_state). Consider using variables consistently for all AI-related tables to centralize table name definitions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/logout_cleanup.go` around lines 60 - 71, The three DELETEs use
hardcoded table names; replace them with the centralized table-name variables
(like the existing aiSessionsTable pattern) so table names are consistent and
configurable; update the bestEffortExec calls to use the corresponding variables
(e.g., internalMessagesTable, toolApprovalRulesTable, loginStateTable or
whatever names are defined in the same package), and if those variables don't
exist add them alongside aiSessionsTable where other AI table constants are
declared so all DELETEs reference the shared table-name variables rather than
literal strings.

72-74: Consider consolidating duplicate type assertion.

The (*AIClient) type assertion is performed twice: once at line 36 for purgeLoginIntegrations and again here at line 72 for clearLoginState. Consider calling both methods within a single type-assertion block to reduce redundancy.

♻️ Proposed consolidation
 	if client, ok := login.Client.(*AIClient); ok && client != nil {
 		client.purgeLoginIntegrations(ctx, login, bridgeID, loginID)
+		defer client.clearLoginState(ctx)
 	}
 	var logger *zerolog.Logger

Then remove the second type assertion block at lines 72-74.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/logout_cleanup.go` around lines 72 - 74, The code performs the
same type assertion on login.Client to *AIClient twice; consolidate by doing a
single assertion (e.g., if client, ok := login.Client.(*AIClient); ok && client
!= nil { ... }) and call both client.purgeLoginIntegrations(ctx) and
client.clearLoginState(ctx) inside that block, then remove the duplicate
assertion and the separate clearLoginState call so both methods run under one
nil-checked *AIClient branch.
sdk/conversation_test.go (1)

47-49: Prefer test-failure signaling over panic in helper.

Optional: pass *testing.T into newTestConversation and use t.Fatalf(...) so failures are attributed cleanly to the calling test.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/conversation_test.go` around lines 47 - 49, The helper currently panics
on save errors which obscures test attribution; modify the test helper (e.g.,
newTestConversation) to accept a *testing.T (or change it to return an error)
and replace the panic(err) after conv.saveState(...) with t.Fatalf("saveState
failed: %v", err) so failures are reported as test failures and attributed to
the calling test; update all callers of newTestConversation to pass the
*testing.T (or handle the returned error) accordingly.
pkg/integrations/memory/sessions.go (1)

63-67: Unnecessary state save when hash matches.

When the computed hash matches state.contentHash and !force, the code saves the session state (line 64-66) even though nothing has changed. This appears redundant since state hasn't been modified at this point.

💡 Suggested simplification
 		if !force && hash == state.contentHash {
-			if err := m.saveSessionState(ctx, key, state); err != nil {
-				m.log.Warn().Err(err).Str("session", key).Msg("memory session state save failed")
-			}
 			continue
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/integrations/memory/sessions.go` around lines 63 - 67, The code
redundantly calls m.saveSessionState(ctx, key, state) when !force and hash ==
state.contentHash even though state wasn't modified; remove the save call from
that branch (the if-block in sessions.go that checks !force && hash ==
state.contentHash) so it simply continues, and ensure any necessary saves remain
where state is actually mutated (keep m.saveSessionState only in places like
saveSessionState invocations after state changes).
bridges/ai/login_config_db.go (1)

69-84: Table creation uses safe constant, but consider schema migrations.

The DDL uses CREATE TABLE IF NOT EXISTS which is safe for initial creation. Since aiLoginConfigTable appears to be a constant, there's no SQL injection risk. However, for production systems, consider using formal schema migrations for better version control and rollback capabilities.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/login_config_db.go` around lines 69 - 84, The current
ensureAILoginConfigTable function contains inline DDL (CREATE TABLE IF NOT
EXISTS) for aiLoginConfigTable; replace this ad-hoc creation with a proper
schema migration: remove the inline CREATE TABLE from ensureAILoginConfigTable
and instead register a migration (e.g., a new migration entry or function like
migrateCreateAILoginConfigTable) that performs the DDL via your project's
migration framework or migration runner, ensure the migration is idempotent and
versioned, and update any bootstrap code that called ensureAILoginConfigTable to
run/verify migrations (refer to symbols ensureAILoginConfigTable and
aiLoginConfigTable to locate the code to change).
bridges/openclaw/metadata.go (2)

127-143: Consider caching table creation status.

ensureOpenClawPortalStateTable executes CREATE TABLE IF NOT EXISTS on every load/save operation. While SQLite/PostgreSQL handle this idempotently, it adds unnecessary overhead.

Consider using a sync.Once or a package-level flag to track whether the table has been created during this process lifetime.

♻️ Optional: Cache table creation
+var openClawPortalStateTableCreated sync.Once
+
 func ensureOpenClawPortalStateTable(ctx context.Context, portal *bridgev2.Portal, login *bridgev2.UserLogin) error {
 	scope := openClawPortalDBScopeFor(portal, login)
 	if scope == nil {
 		return nil
 	}
-	_, err := scope.db.Exec(ctx, `
+	var err error
+	openClawPortalStateTableCreated.Do(func() {
+		_, err = scope.db.Exec(ctx, `
 		CREATE TABLE IF NOT EXISTS openclaw_portal_state (
 			...
 		)
-	`)
+		`)
+	})
 	return err
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/openclaw/metadata.go` around lines 127 - 143, The
ensureOpenClawPortalStateTable function runs CREATE TABLE IF NOT EXISTS on every
call; add a cheap in-process cache to avoid repeated DDL: introduce a
package-level sync.Once or a map[<dbIdentifier>]bool guarded by a mutex to
record that the openclaw_portal_state table has been created, check that cache
at the top of ensureOpenClawPortalStateTable (using the same scope derived via
openClawPortalDBScopeFor/ scope.db to derive a stable key), and only call
scope.db.Exec when the cache says the table hasn’t been created yet, updating
the cache after a successful Exec (or using Once.Do to run the Exec exactly
once).

115-116: Portal key uses null byte separator.

The composite key string(portal.PortalKey.ID) + "\x00" + string(portal.PortalKey.Receiver) works but is unconventional. Null bytes in strings can cause issues with some tools/loggers. Consider using a printable delimiter like | or storing ID and Receiver as separate columns.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/openclaw/metadata.go` around lines 115 - 116, The code builds a
composite portalKey using a null byte separator which can cause tooling/logging
issues; replace the null ("\x00") separator in the expression that assigns
portalKey (currently string(portal.PortalKey.ID) + "\x00" +
string(portal.PortalKey.Receiver)) with a printable delimiter (e.g., "|" or
another safe character) or, better, persist ID and Receiver as separate fields
instead of a single concatenated string; update any consumers of portalKey to
parse the new delimiter or to use the separate fields accordingly (look for
usages of portalKey and portal.PortalKey.ID / portal.PortalKey.Receiver).
bridges/ai/internal_prompt_db.go (1)

102-134: Consider filtering in SQL instead of post-fetch.

Lines 125-127 and 132-134 filter rows after fetching. Moving these conditions to the WHERE clause would reduce data transfer and processing:

♻️ Proposed optimization
 	rows, err := scope.db.Query(ctx, `
 		SELECT event_id, canonical_turn_data, exclude_from_history, created_at_ms
 		FROM `+aiInternalMessagesTable+`
-		WHERE bridge_id=$1 AND login_id=$2 AND room_id=$3
+		WHERE bridge_id=$1 AND login_id=$2 AND room_id=$3 AND exclude_from_history=0
+		  AND ($5 = 0 OR created_at_ms >= $5)
 		ORDER BY created_at_ms DESC, event_id DESC
 		LIMIT $4
-	`, scope.bridgeID, scope.loginID, portal.MXID.String(), limit)
+	`, scope.bridgeID, scope.loginID, portal.MXID.String(), limit, resetAt)

Note: The opts.excludeMessageID filter may still need post-fetch handling unless added as another WHERE clause.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/internal_prompt_db.go` around lines 102 - 134, Move the post-fetch
filters into the SQL query to reduce rows returned: update the scope.db.Query
call that selects from aiInternalMessagesTable to include "exclude_from_history
= false" and, when resetAt > 0, add a "created_at_ms >= $N" predicate (and bind
resetAt) so rows with excludeFromHistory and older createdAtMs are filtered out
in SQL; keep the opts.excludeMessageID check in Go unless you also want to add
it as another WHERE clause (in which case bind opts.excludeMessageID and compare
event_id != $M). Target the Query call and the variables excludeFromHistory,
createdAtMs, resetAt, and opts.excludeMessageID when applying these changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@bridges/ai/agentstore.go`:
- Line 563: CreateRoom and ModifyRoom call b.client.savePortalQuiet which
swallows errors, causing those APIs to report success even when portal
persistence fails; change the call to use a persistence function that returns an
error (or modify savePortalQuiet to return an error instead of only logging) and
handle that error in CreateRoom and ModifyRoom: call
savePortal/savePortalWithError (or update savePortalQuiet to return error),
check the returned error, and if non-nil return an appropriate failure to the
caller instead of a success response.

In `@bridges/ai/command_registry.go`:
- Around line 176-194: The log call at the end of the command broadcasting block
uses len(handlers) which is incorrect after filtering; change the debug log to
report len(cmds) instead so it reflects the actual number broadcast. Locate the
block that builds cmds from handlers (variables: handlers, cmds) and the call to
sdk.BroadcastCommandDescriptions(ctx, portal, bot, cmds), then update the
log.Debug().Int("count", ...) argument to use len(cmds) and keep the same "room"
portal.MXID and message.

In `@bridges/ai/delete_chat.go`:
- Around line 73-82: The delete cleanup must also clear login-level portal
metadata so stale default-chat/last-active-room IDs don't point at the deleted
portal; add a bestEffortExec call (same pattern as the existing bestEffortExec
calls that delete from aiSessionsTable and aichats_system_events) to UPDATE the
login record(s) for the same bridgeID/loginID and set the default-chat and
last-active-room fields to NULL (or their zero values) before/after
deleteInternalPromptsForRoom; locate the cleanup block referencing
bestEffortExec, aiSessionsTable, deleteInternalPromptsForRoom, and
id.RoomID(sessionKey) and add the SQL UPDATE against the login table clearing
the relevant columns.

In `@bridges/ai/handleai.go`:
- Around line 315-317: The code currently sets currentMeta.AutoGreetingSent (and
similarly WelcomeSent) and calls oc.savePortalQuiet but ignores save failures
before calling oc.dispatchInternalMessage, which can cause sent messages without
persisted flags; change the flow so oc.savePortalQuiet returns an error (or use
an existing error-returning save variant) and check its result: after setting
currentMeta.AutoGreetingSent (and for WelcomeSent in the other block) call
oc.savePortalQuiet (or a new savePortal that returns error) and if it returns an
error immediately return/bail instead of proceeding to
oc.dispatchInternalMessage; ensure you update both the AutoGreetingSent branch
(where currentMeta.AutoGreetingSent is set) and the WelcomeSent branch (around
the 383-387 area) to persist first and only dispatch the one-shot message on
successful save.
- Around line 452-459: The code updates portal.Name and portal.NameSet then
calls savePortalQuiet which only persists to DB; to sync the generated title to
the Matrix room you must call the Matrix-sync path after saving — either invoke
materializePortalRoom(bgCtx, portal, ...) (as used elsewhere when returning
PortalInfo) or call oc.sdk.EnsurePortalLifecycle(bgCtx, portal) (as in
scheduler_rooms.go) so the room state is updated; add the appropriate call
immediately after oc.savePortalQuiet(bgCtx, portal, "room title") so the DB
change is also propagated to the Matrix room.

In `@bridges/ai/integration_host.go`:
- Around line 238-272: SessionTranscript currently calls CountMessagesInPortal
then fetches that many messages with GetLastNInPortal which can load thousands
into memory; cap the number fetched (e.g., limit := min(count, 500) or a
configurable constant) and call GetLastNInPortal with that limit instead of
count to prevent unbounded memory use. Update the logic around
CountMessagesInPortal and the call to GetLastNInPortal in
runtimeIntegrationHost.SessionTranscript (and ensure
h.client.backgroundContext(ctx) is still passed) so only the capped number of
recent messages are retrieved.

In `@bridges/ai/login_config_db.go`:
- Around line 139-143: The swap-and-save pattern around
compactAIUserLoginMetadata and login.Save can leave the system inconsistent
because the separate custom DB upsert (the AI login config table) may succeed
while login.Save fails; to fix, make both updates atomic: if both tables share
the same DB, execute the custom upsert and the login.Save inside a single
transaction (use the DB transaction API surrounding the upsert and the call to
login.Save), otherwise implement a compensating rollback for the custom upsert
(e.g., delete or restore the previous row) when login.Save returns an error;
locate and change code around compactAIUserLoginMetadata, login.Save(ctx) and
the custom upsert logic so both succeed or the earlier change is reverted.

In `@bridges/ai/portal_state_db.go`:
- Around line 165-170: The current error handling block incorrectly hides
database errors by checking strings.TrimSpace(raw) in the same OR with err ==
sql.ErrNoRows; move and split checks so real Scan() errors are returned: first
check if err != nil and return err, then handle the no-row case (err ==
sql.ErrNoRows) and separately check if strings.TrimSpace(raw) == "" to return
nil,nil; reference the variables err and raw and the sql.ErrNoRows comparison in
the error-handling block around the Scan()/query result processing.

In `@bridges/ai/prompt_builder.go`:
- Around line 152-171: The current code merges internalRows into candidates and
then applies hr.limit to the combined slice, allowing internal prompts to evict
recent chat messages and corrupt image-injection indexing; fix by treating
internal rows separately: when building candidates, create a separate
internalCandidates slice from loadInternalPromptHistory and keep chatCandidates
for user/assistant messages, then merge for ordering but when enforcing hr.limit
only count/trim chatCandidates (preserve internalCandidates regardless of
hr.limit), and update any image-injection logic that uses the loop index `i` to
instead use a counter of non-internal chat entries; reference
loadInternalPromptHistory, internalRows, candidates, replayCandidate
(id/role/ts/messages), hr.limit and the sort/trimming block when making this
change.

In `@bridges/ai/tools.go`:
- Around line 390-398: The comment and logic in executeMessageReact disagree:
empty emoji is being treated as a removal but executeMessageReactRemove requires
an explicit emoji. Update executeMessageReact to only call
executeMessageReactRemove when the remove flag is true (i.e., change the
condition from `remove || emoji == ""` to `remove`) and instead validate/return
an error when emoji is empty and remove is false; also update the top comment to
reflect that removals require remove:true (or explicit emoji in the args) and
reference executeMessageReactRemove in the comment so callers know which path
needs an explicit emoji.

In `@bridges/codex/directory_manager.go`:
- Around line 354-369: The code flips state.AwaitingCwdSetup to false and saves
the portal state before calling cc.ensureRPC and cc.ensureCodexThread, which can
leave the room out of welcome mode if those calls fail; either move the state
mutations (CodexCwd, CodexThreadID, AwaitingCwdSetup, ManagedImport, Title,
Slug) and the saveCodexPortalState call to after cc.ensureCodexThread succeeds,
or capture the prior state before mutating and, if cc.ensureRPC or
cc.ensureCodexThread returns an error, call saveCodexPortalState to restore the
previous state so the room remains in welcome mode; references:
state.AwaitingCwdSetup, saveCodexPortalState, cc.ensureRPC,
cc.ensureCodexThread, and cc.syncCodexRoomTopic.

In `@bridges/openclaw/catalog.go`:
- Around line 94-104: The persisted state fields OpenClawDefaultAgentID,
OpenClawKnownModelCount, OpenClawToolCount and OpenClawToolProfile must be reset
before re-enriching so stale values aren’t left when lookups are empty or the
default/selected agent changes; update the code around oc.agentDefaultID(),
state.OpenClawAgentID/ state.OpenClawDMTargetAgentID (used with
stringutil.TrimDefault), loadModelCatalog(ctx, false) and loadToolsCatalog(ctx,
agentID, false) to clear state.OpenClawDefaultAgentID (set to ""),
state.OpenClawKnownModelCount (set to 0), and
state.OpenClawToolCount/state.OpenClawToolProfile (set to 0 and nil/empty)
before attempting the loads, then repopulate them only when the corresponding
loads return non-empty results and use summarizeToolsCatalog only when catalog
is non-nil.

In `@bridges/openclaw/identifiers.go`:
- Around line 33-39: The current ID format built by openClawScopedGhostUserID
uses url.PathEscape but still allows colons to survive, causing ambiguous
parsing; change the encoding to an unambiguous one (e.g., hex or base64 URL-safe
encoding like base64.RawURLEncoding or hex.EncodeToString) for both the loginID
and agentID when constructing the string and add a versioned prefix (e.g.,
"v1:openclaw-agent:") to the ID; update the corresponding parser (the function
that parses/scans scoped ghost IDs) to decode the chosen encoding and honor the
version prefix so splitting on ":" is safe and unambiguous.

In `@bridges/openclaw/login.go`:
- Around line 262-269: If persisting credentials via saveOpenClawLoginState
fails after sdk.CreateAndCompleteLogin succeeded, implement a rollback or
coordination: either wrap the create+persist in a transaction/atomic operation
or, on saveOpenClawLoginState error, call the SDK cleanup to remove the newly
created login (e.g., invoke a delete/revert API for the created login using the
login ID) so you don't leave a half-created login without credentials; reference
the saveOpenClawLoginState call, the sdk.CreateAndCompleteLogin invocation, and
the openClawPersistedLoginState struct when adding the rollback/transaction
logic.

In `@bridges/openclaw/manager.go`:
- Around line 596-619: The code updates only the local sessionKey after
resolving a synthetic DM but doesn't persist it, so racey events can miss the
real session; before calling gateway.SendMessage(...) set
state.OpenClawSessionKey = strings.TrimSpace(resolvedKey) (or the trimmed
sessionKey if no resolve) and persist the portal/state using the existing
persistence method in this manager (so the saved state reflects the real session
key), ensuring the saved key is in place before SendMessage; alternatively, if
you prefer sync refresh, call m.syncSessions synchronously (using
m.client.BackgroundContext(ctx)) immediately after setting/persisting the key
and before SendMessage so the portal state is consistent.

In `@bridges/openclaw/provisioning.go`:
- Around line 282-322: The code may create the Matrix room via
sdk.EnsurePortalLifecycle before the updated OpenClaw state is durably persisted
and before portalMeta(portal).IsOpenClawRoom is set; to fix, persist the updated
state and set the in-memory marker before calling sdk.EnsurePortalLifecycle:
after oc.enrichPortalState(ctx, state) call save the state with
saveOpenClawPortalState(ctx, portal, oc.UserLogin, state) and set
portalMeta(portal).IsOpenClawRoom = true, handling and returning any error
immediately, then proceed to call sdk.EnsurePortalLifecycle; this ensures
failure to persist aborts before the room is created and the in-memory marker is
set consistently.

In `@pkg/integrations/memory/integration.go`:
- Around line 168-174: The PurgeForLogin implementation currently returns before
shutting down in-process managers when resolveStateDB() is nil; move the call to
StopManagersForLogin(scope.BridgeID, scope.LoginID) so it runs unconditionally
before the nil check, then keep the existing nil return and the subsequent
PurgeTables(ctx, db, scope.BridgeID, scope.LoginID) call when a DB exists;
update the function in Integration (PurgeForLogin) that calls resolveStateDB(),
StopManagersForLogin and PurgeTables to ensure managers are always stopped even
if MemoryStateDB() is unavailable.

---

Outside diff comments:
In `@bridges/ai/chat.go`:
- Around line 1095-1132: Before creating a new default portal, add a legacy-room
fallback: after the GetExistingPortalByKey check fails (portal == nil) call
oc.listAllChatPortals(ctx, oc.UserLogin) and pass the result into
oc.chooseDefaultChatPortal(...) to see if an older portal should be reused; if
chooseDefaultChatPortal returns a portal, reuse it (materialize if MXID missing
via oc.materializePortalRoom) and return nil instead of proceeding to create a
new portal with oc.initPortalForChat; keep existing logging behavior and error
handling around materializePortalRoom and initPortalForChat, and reference the
existing symbols GetExistingPortalByKey, listAllChatPortals,
chooseDefaultChatPortal, materializePortalRoom, and initPortalForChat when
making the change.

In `@bridges/ai/command_registry.go`:
- Around line 197-210: Remove the dead helper buildCommandDescriptionContent and
its associated unit test that directly invokes it (the test in events_test that
targets buildCommandDescriptionContent); update references so
BroadcastCommandDescriptions continues to construct sdk.Command objects directly
and delete both the function buildCommandDescriptionContent and the test file's
test case that calls it. Ensure no other code references
buildCommandDescriptionContent remain and run the test suite to confirm removal
is safe.

In `@bridges/ai/login.go`:
- Around line 239-248: The code calls ol.User.NewLogin(...) which creates a
login and then calls saveAIUserLogin(...); if saveAIUserLogin fails you must
roll back the created login to avoid stranded state. Modify the error path after
saveAIUserLogin to call the appropriate deletion method (e.g.,
ol.User.DeleteLogin(ctx, loginID) or equivalent) to remove the created login
(and handle/log any deletion error), then return the original wrapped save
error; ensure you reference ol.User.NewLogin, saveAIUserLogin, and loginID when
implementing the rollback so creation is undone on failure.

In `@bridges/ai/tool_approvals_rules.go`:
- Around line 59-80: The approval checks isMcpAlwaysAllowed and
isBuiltinAlwaysAllowed currently call
hasToolApprovalRule/hasBuiltinToolApprovalRule with context.Background(), making
DB lookups uncancellable; change these functions to accept a context.Context
parameter (e.g., ctx) and pass that ctx into oc.hasToolApprovalRule and
oc.hasBuiltinToolApprovalRule, or alternatively create a derived context with a
short timeout inside each function and use that for the DB call; update all
callers of isMcpAlwaysAllowed and isBuiltinAlwaysAllowed to supply the caller
context so the DB queries are cancellable.

In `@bridges/codex/directory_manager.go`:
- Around line 133-164: The current flow calls sdk.EnsurePortalLifecycle (in
bridges/codex/directory_manager.go) before persisting codexPortalState with
saveCodexPortalState, which can create the Matrix room without saved state; move
the saveCodexPortalState call to occur immediately after initializing the
codexPortalState and before calling sdk.EnsurePortalLifecycle, so the state is
persisted (and errors returned) prior to creating the room; keep the calls to
portalMeta(portal).IsCodexRoom = true and
sdk.ConfigureDMPortal/cc.composeCodexChatInfo/sendSystemNotice intact, but
ensure saveCodexPortalState is executed and its error handled before invoking
EnsurePortalLifecycle to avoid orphaned rooms without persisted state.

In `@pkg/aidb/db_test.go`:
- Around line 53-68: TestUpgradeV1Fresh is missing assertions for newly added v1
tables; update the test (TestUpgradeV1Fresh in pkg/aidb/db_test.go) to include
checks for the three tables created by pkg/aidb/001-init.sql:
aichats_internal_messages, aichats_login_state, and aichats_tool_approval_rules
so the fresh-schema path fails if any of those tables are not created during
upgrade.

In `@sdk/conversation_state.go`:
- Around line 75-106: The code currently allows storing/reading under an empty
key when conversationStateKey(portal) returns "", causing unrelated portals to
share state; update conversationStateStore.get and conversationStateStore.set to
treat an empty key as invalid: after computing key :=
conversationStateKey(portal) return a new empty sdkConversationState (without
accessing s.rooms) if key == "" in get, and return immediately without writing
to s.rooms if key == "" in set; reference conversationStateKey,
conversationStateStore.get, conversationStateStore.set, s.rooms and ensure the
same nil checks remain.
- Around line 134-176: loadConversationState currently only checks the cache and
DB, so add a legacy fallback: when state is still nil/default after consulting
the store and DB (inside loadConversationState), inspect portal.Metadata for the
legacy conversation-state entry (the key used previously for storing state in
portal metadata), unmarshal that JSON into an sdkConversationState, and use it
as the loaded state (without mutating portal.Metadata). After loading from
legacy metadata, call state.ensureDefaults() and store.set(portal, state) as
currently done so the rest of the code treats it like a normal load; keep
loadConversationStateFromDB unchanged.

In `@sdk/conversation.go`:
- Around line 47-52: The getIntent method on Conversation now dereferences c
unconditionally which will panic for a nil *Conversation and cascade into
SendMedia, SendNotice, sendMessageContent, and SetTyping; restore the nil
receiver guard by checking if c == nil at the top of Conversation.getIntent and
return a sensible error (e.g., fmt.Errorf or a package-level err like
ErrNoConversation) instead of calling resolveMatrixIntent, and keep the existing
intentOverride branch (i.e., if c != nil && c.intentOverride != nil return
c.intentOverride(ctx)); ensure callers that expect an error continue to
propagate it.

---

Nitpick comments:
In `@bridges/ai/identifiers.go`:
- Line 171: The call to loadPortalStateIntoMetadata is using
context.Background() inside portalMeta(), preventing cancellation/timeouts from
propagating; change portalMeta to accept a context.Context parameter and
propagate that ctx into loadPortalStateIntoMetadata (i.e., call
loadPortalStateIntoMetadata(ctx, portal, meta) instead of using Background()),
then update portalMeta call sites to pass the available request/context where
present (and only use context.Background() as an explicit fallback in rare
callers that truly lack a context), ensuring all DB-backed portal state loads
honor cancellation.

In `@bridges/ai/internal_prompt_db.go`:
- Around line 102-134: Move the post-fetch filters into the SQL query to reduce
rows returned: update the scope.db.Query call that selects from
aiInternalMessagesTable to include "exclude_from_history = false" and, when
resetAt > 0, add a "created_at_ms >= $N" predicate (and bind resetAt) so rows
with excludeFromHistory and older createdAtMs are filtered out in SQL; keep the
opts.excludeMessageID check in Go unless you also want to add it as another
WHERE clause (in which case bind opts.excludeMessageID and compare event_id !=
$M). Target the Query call and the variables excludeFromHistory, createdAtMs,
resetAt, and opts.excludeMessageID when applying these changes.

In `@bridges/ai/login_config_db.go`:
- Around line 69-84: The current ensureAILoginConfigTable function contains
inline DDL (CREATE TABLE IF NOT EXISTS) for aiLoginConfigTable; replace this
ad-hoc creation with a proper schema migration: remove the inline CREATE TABLE
from ensureAILoginConfigTable and instead register a migration (e.g., a new
migration entry or function like migrateCreateAILoginConfigTable) that performs
the DDL via your project's migration framework or migration runner, ensure the
migration is idempotent and versioned, and update any bootstrap code that called
ensureAILoginConfigTable to run/verify migrations (refer to symbols
ensureAILoginConfigTable and aiLoginConfigTable to locate the code to change).

In `@bridges/ai/logout_cleanup.go`:
- Around line 60-71: The three DELETEs use hardcoded table names; replace them
with the centralized table-name variables (like the existing aiSessionsTable
pattern) so table names are consistent and configurable; update the
bestEffortExec calls to use the corresponding variables (e.g.,
internalMessagesTable, toolApprovalRulesTable, loginStateTable or whatever names
are defined in the same package), and if those variables don't exist add them
alongside aiSessionsTable where other AI table constants are declared so all
DELETEs reference the shared table-name variables rather than literal strings.
- Around line 72-74: The code performs the same type assertion on login.Client
to *AIClient twice; consolidate by doing a single assertion (e.g., if client, ok
:= login.Client.(*AIClient); ok && client != nil { ... }) and call both
client.purgeLoginIntegrations(ctx) and client.clearLoginState(ctx) inside that
block, then remove the duplicate assertion and the separate clearLoginState call
so both methods run under one nil-checked *AIClient branch.

In `@bridges/openclaw/metadata.go`:
- Around line 127-143: The ensureOpenClawPortalStateTable function runs CREATE
TABLE IF NOT EXISTS on every call; add a cheap in-process cache to avoid
repeated DDL: introduce a package-level sync.Once or a map[<dbIdentifier>]bool
guarded by a mutex to record that the openclaw_portal_state table has been
created, check that cache at the top of ensureOpenClawPortalStateTable (using
the same scope derived via openClawPortalDBScopeFor/ scope.db to derive a stable
key), and only call scope.db.Exec when the cache says the table hasn’t been
created yet, updating the cache after a successful Exec (or using Once.Do to run
the Exec exactly once).
- Around line 115-116: The code builds a composite portalKey using a null byte
separator which can cause tooling/logging issues; replace the null ("\x00")
separator in the expression that assigns portalKey (currently
string(portal.PortalKey.ID) + "\x00" + string(portal.PortalKey.Receiver)) with a
printable delimiter (e.g., "|" or another safe character) or, better, persist ID
and Receiver as separate fields instead of a single concatenated string; update
any consumers of portalKey to parse the new delimiter or to use the separate
fields accordingly (look for usages of portalKey and portal.PortalKey.ID /
portal.PortalKey.Receiver).

In `@bridges/opencode/opencode_instance_state.go`:
- Around line 100-110: The sessionIDs() method returns session IDs in
non-deterministic map order; lock seenMu, collect keys from inst.knownSessions
as done, then sort the slice (e.g., using sort.Strings) before returning to
ensure deterministic ordering for callers, tests, and logs; update the function
(sessionIDs) to import the sort package and call sort.Strings(out) right before
the return.

In `@cmd/agentremote/profile.go`:
- Around line 26-42: Update the doc comments for configRoot and profileRoot to
reflect that the path uses the dynamic binaryName variable rather than the
hardcoded "sdk" string: change the comment above configRoot to indicate it
returns ~/.config/<binaryName> and the comment above profileRoot to indicate it
returns ~/.config/<binaryName>/profiles/<profile>, referencing the functions
configRoot and profileRoot so future readers know the path is derived from
binaryName.

In `@cmd/internal/bridgeentry/bridgeentry.go`:
- Around line 21-50: The AI Definition's Description ("AI Chats bridge for
Beeper") is inconsistent with the other bridge Definitions (Codex, OpenCode,
OpenClaw, DummyBridge) which use the "X bridge built with the AgentRemote SDK."
pattern; update the AI variable's Description in the Definition for AI to match
that pattern (e.g., "AI bridge built with the AgentRemote SDK.") or, if the
different wording is intentional, add a brief inline comment above the AI
Definition explaining the special case so reviewers understand the deviation.

In `@pkg/integrations/memory/sessions.go`:
- Around line 63-67: The code redundantly calls m.saveSessionState(ctx, key,
state) when !force and hash == state.contentHash even though state wasn't
modified; remove the save call from that branch (the if-block in sessions.go
that checks !force && hash == state.contentHash) so it simply continues, and
ensure any necessary saves remain where state is actually mutated (keep
m.saveSessionState only in places like saveSessionState invocations after state
changes).

In `@pkg/shared/toolspec/toolspec.go`:
- Around line 143-145: The schema currently documents that "emoji" is required
when "remove": true for the "react" action but doesn't enforce it; update the
toolspec schema construction in toolspec.go (the code building properties like
StringProperty("emoji") and BooleanProperty("remove") for the "react" action) to
add a conditional JSON Schema rule that enforces emoji when remove is true — for
example add an if/then clause scoped to action === "react" (if:
{properties:{action:{const:"react"}, remove:{const:true}}} then:
{required:["emoji"]}) or express the same via oneOf alternatives; ensure the new
rule is attached to the same schema object used by the validator so malformed
react/remove payloads are rejected.

In `@sdk/conversation_test.go`:
- Around line 47-49: The helper currently panics on save errors which obscures
test attribution; modify the test helper (e.g., newTestConversation) to accept a
*testing.T (or change it to return an error) and replace the panic(err) after
conv.saveState(...) with t.Fatalf("saveState failed: %v", err) so failures are
reported as test failures and attributed to the calling test; update all callers
of newTestConversation to pass the *testing.T (or handle the returned error)
accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: build-agentremote-docker (amd64)
  • GitHub Check: build-docker
  • GitHub Check: Lint
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-03-16T09:01:24.464Z
Learnt from: batuhan
Repo: beeper/agentremote PR: 71
File: bridges/ai/connector.go:53-63
Timestamp: 2026-03-16T09:01:24.464Z
Learning: In package ai, the AI connector’s configuration type (Config) defines Bridge as a value field of type BridgeConfig (not a pointer). Therefore, accessing oc.Config.Bridge.CommandPrefix in OpenAIConnector.applyRuntimeDefaults (bridges/ai/connector.go) is safe and does not require a nil-check.

Applied to files:

  • bridges/ai/integrations.go
  • bridges/ai/abort_helpers_test.go
  • bridges/ai/bridge_info.go
  • bridges/ai/client_capabilities_test.go
  • bridges/ai/command_registry.go
  • bridges/ai/constructors_test.go
  • cmd/agentremote/run_bridge.go
  • bridges/ai/agentstore.go
  • bridges/dummybridge/runtime_test.go
  • sdk/connector_builder_test.go
  • bridges/ai/bridge_db.go
  • bridges/ai/login_loaders.go
  • bridges/codex/connector.go
  • bridges/codex/constructors.go
  • bridges/ai/commands.go
  • bridges/codex/runtime_helpers.go
  • bridges/ai/connector.go
  • sdk/connector_builder.go
  • bridges/ai/constructors.go
  • bridges/ai/sdk_agent_catalog.go
  • bridges/dummybridge/connector.go
  • bridges/ai/handleai.go
  • bridges/opencode/connector.go
  • bridges/openclaw/connector.go
  • cmd/internal/bridgeentry/bridgeentry.go
  • sdk/connector.go
  • sdk/types.go
  • bridges/dummybridge/runtime.go
  • bridges/ai/client.go

Comment thread bridges/ai/agentstore.go Outdated
Comment thread bridges/ai/command_registry.go Outdated
Comment thread bridges/ai/delete_chat.go Outdated
Comment thread bridges/ai/handleai.go
Comment thread bridges/ai/handleai.go Outdated
Comment thread bridges/openclaw/identifiers.go Outdated
Comment thread bridges/openclaw/login.go Outdated
Comment thread bridges/openclaw/manager.go Outdated
Comment thread bridges/openclaw/provisioning.go Outdated
Comment thread pkg/integrations/memory/integration.go
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
bridges/codex/constructors.go (1)

42-55: ⚠️ Potential issue | 🟠 Major

Guard aidb.EnsureSchema when the bridge DB is unavailable.

InitConnector conditionally initializes cc.db only when bridge, bridge.DB, and bridge.DB.Database are all non-nil. However, StartConnector unconditionally passes the result of cc.bridgeDB() to aidb.EnsureSchema, which explicitly errors on nil input rather than short-circuiting. This causes startup failures in no-DB scenarios.

Suggested fix
 		StartConnector: func(ctx context.Context, _ *bridgev2.Bridge) error {
 			db := cc.bridgeDB()
-			if err := aidb.EnsureSchema(ctx, db); err != nil {
-				return err
+			if db != nil {
+				if err := aidb.EnsureSchema(ctx, db); err != nil {
+					return err
+				}
 			}
 			cc.applyRuntimeDefaults()
 			sdk.PrimeUserLoginCache(ctx, cc.br)
 			return nil
 		},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/codex/constructors.go` around lines 42 - 55, StartConnector calls
aidb.EnsureSchema with cc.bridgeDB() even when InitConnector left cc.db nil;
wrap the EnsureSchema call in a nil-check so it only runs when cc.bridgeDB() !=
nil (e.g. assign db := cc.bridgeDB(); if db == nil { return nil } else if err :=
aidb.EnsureSchema(ctx, db); err != nil { return err }), ensuring StartConnector
short-circuits cleanly in no-DB scenarios and avoids passing nil to
aidb.EnsureSchema.
bridges/ai/chat.go (2)

46-52: ⚠️ Potential issue | 🟠 Major

Treat an unset Agents flag as enabled.

This helper returns false when cfg.Agents is nil, but shouldEnsureDefaultChat still treats nil as “enabled”. That leaves default-config logins in a split state where bootstrap creates the agent chat while search/create-chat flows reject agents.

Suggested fix
 func (oc *AIClient) agentsEnabledForLogin() bool {
 	if oc == nil || oc.UserLogin == nil {
 		return false
 	}
 	cfg := oc.loginConfigSnapshot(context.Background())
-	return cfg.Agents != nil && *cfg.Agents
+	return cfg.Agents == nil || *cfg.Agents
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/chat.go` around lines 46 - 52, agentsEnabledForLogin currently
returns false when cfg.Agents is nil, causing a mismatch with
shouldEnsureDefaultChat which treats nil as enabled; update
agentsEnabledForLogin (in AIClient) to treat a nil Agents pointer as enabled by
returning true when cfg.Agents == nil, otherwise return the boolean value
(*cfg.Agents) from the loginConfigSnapshot result so default-config logins
consistently enable agents.

239-268: ⚠️ Potential issue | 🟠 Major

Return the bridge ghost ID for agent contacts.

agentContactResponse now exposes networkid.UserID(agent.ID), but the rest of the bridge consistently uses oc.agentUserID(agentID) for agent ghosts. Search/contact results can therefore hand back an ID that doesn't hydrate to the actual ghost or open the right DM.

Suggested fix
 func (oc *AIClient) agentContactResponse(ctx context.Context, agent *sdk.Agent) *bridgev2.ResolveIdentifierResponse {
 	if agent == nil || !oc.agentsEnabledForLogin() {
 		return nil
 	}
+	userID := networkid.UserID(agent.ID)
+	if agentID := catalogAgentID(agent); agentID != "" {
+		userID = oc.agentUserID(agentID)
+	}
 	resp := &bridgev2.ResolveIdentifierResponse{
-		UserID: networkid.UserID(agent.ID),
+		UserID: userID,
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/chat.go` around lines 239 - 268, The response currently sets
resp.UserID to networkid.UserID(agent.ID) which doesn't match how agent ghosts
are stored; update agentContactResponse so the UserID returned for agent
contacts uses the bridge ghost ID via oc.agentUserID(agentID) (using the
catalogAgentID result) instead of networkid.UserID(agent.ID); if
catalogAgentID(agent) is empty, keep the existing fallback behavior, and ensure
this change happens before calling UserLogin.Bridge.GetGhostByID so the ghost
hydrate uses the correct ID (refer to functions agentContactResponse,
catalogAgentID, oc.ResolveResponderForAgent, and oc.agentUserID).
bridges/ai/metadata.go (1)

313-325: ⚠️ Potential issue | 🟠 Major

cloneUserLoginMetadata now strips all transient login state.

After the tag changes above, this JSON round-trip preserves essentially only Provider. Any caller that clones before mutating or persisting login state will silently lose credentials, caches, profile data, custom agents, and error counters. This needs an explicit clone path for the non-serialized fields.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/metadata.go` around lines 313 - 325, cloneUserLoginMetadata
performs a JSON round-trip which strips non-serialized/transient fields
(credentials, caches, profile data, custom agents, error counters), so update
cloneUserLoginMetadata to perform the JSON marshal/unmarshal for the
serializable fields and then explicitly copy the transient fields from src into
the resulting clone (e.g., copy Credentials, Cache, Profile, CustomAgents,
ErrorCounters or their pointer/deep copies as appropriate) so callers that clone
before mutating/persisting don't lose runtime state; locate and modify
cloneUserLoginMetadata to perform the manual transfer after unmarshal and ensure
pointer safety (deep-copy if needed) before returning the clone.
♻️ Duplicate comments (5)
bridges/ai/handleai.go (3)

387-390: ⚠️ Potential issue | 🟠 Major

Persist WelcomeSent before sending the notice.

This has the same one-shot persistence gap as the auto-greeting path: if the save doesn't stick, the welcome notice can be emitted again later.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/handleai.go` around lines 387 - 390, Set meta.WelcomeSent = true
and persist that change with oc.savePortalQuiet using a cancellable context
(bgCtx from context.WithTimeout(oc.backgroundContext(ctx), 10*time.Second))
before any code that emits the welcome notice; ensure you call
oc.savePortalQuiet immediately after setting meta.WelcomeSent and check/handle
its error (log/stop the notice emission) so the notice is only sent if the
persistence succeeds.

319-321: ⚠️ Potential issue | 🟠 Major

Persist AutoGreetingSent before dispatching the greeting.

This still sends the one-shot greeting even if the flag wasn't durably saved. A restart or retry can resend it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/handleai.go` around lines 319 - 321, The AutoGreetingSent flag is
being set in-memory but not guaranteed persisted before sending the greeting;
change the order and add error handling so persistence happens first: set
currentMeta.AutoGreetingSent = true, call oc.savePortalQuiet(bgCtx, current,
"auto greeting state") and check its error (do not call
oc.dispatchInternalMessage if save failed), logging or returning the save error;
only after a successful save call oc.dispatchInternalMessage(...,
"auto-greeting", true). This uses the existing symbols
currentMeta.AutoGreetingSent, oc.savePortalQuiet, and
oc.dispatchInternalMessage.

456-463: ⚠️ Potential issue | 🟠 Major

Sync the generated title to Matrix after saving it.

Lines 461-463 only update portal.Name/NameSet and persist the portal record. Without a lifecycle/materialization sync, clients won't see the generated room title update.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/handleai.go` around lines 456 - 463, The portal title is only
persisted locally (portal.Name/portal.NameSet and oc.savePortalQuiet) but not
pushed to Matrix; after oc.savePortalQuiet(bgCtx, portal, "room title") call the
existing lifecycle/materialization sync that pushes portal metadata to Matrix
(i.e., the oc method responsible for materializing/syncing portals) so the
updated portal.Title/portal.Name is sent to clients; if no such helper exists,
add and call a method like oc.MaterializePortal(ctx, portal) or
oc.SyncPortalTitleToMatrix(ctx, portal) immediately after saving to trigger the
external update.
bridges/ai/tools.go (1)

390-397: ⚠️ Potential issue | 🟡 Minor

Require remove:true before routing to reaction removal.

Line 396 still treats an empty emoji as a removal request. That sends malformed add-reaction calls into the remove handler instead of rejecting them up front, and it still doesn't match the comment about “explicit emoji” removal.

Suggested fix
-// Supports adding reactions (with emoji) and removing reactions (with remove:true or explicit emoji).
+// Supports adding reactions with `emoji` and removing reactions with `remove:true`.
 func executeMessageReact(ctx context.Context, args map[string]any, btc *BridgeToolContext) (string, error) {
 	emoji, _ := args["emoji"].(string)
 	remove, _ := args["remove"].(bool)

 	// Check if this is a removal request.
-	if remove || emoji == "" {
+	if remove {
 		return executeMessageReactRemove(ctx, args, btc)
 	}
+	if strings.TrimSpace(emoji) == "" {
+		return "", errors.New("action=react requires 'emoji' unless remove=true")
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/tools.go` around lines 390 - 397, The routing in
executeMessageReact incorrectly treats an empty emoji as a removal request;
change the conditional so only remove == true forwards to
executeMessageReactRemove (i.e., if remove { return
executeMessageReactRemove(...) }), and add explicit validation before attempting
an add reaction that rejects empty or missing emoji (return an error) so
malformed add-reaction calls are rejected instead of sent to
executeMessageReactRemove; reference executeMessageReact and
executeMessageReactRemove when making the change.
bridges/ai/integration_host.go (1)

238-250: ⚠️ Potential issue | 🟠 Major

Cap SessionTranscript history fetches.

Line 242 counts the full portal history, and Line 250 immediately asks getAIHistoryMessages for that full count. Long-lived chats can turn this into an unbounded in-memory load.

Suggested fix
 func (h *runtimeIntegrationHost) SessionTranscript(ctx context.Context, portalKey networkid.PortalKey) ([]integrationruntime.MessageSummary, error) {
 	if h == nil || h.client == nil || h.client.UserLogin == nil || h.client.UserLogin.Bridge == nil || h.client.UserLogin.Bridge.DB == nil {
 		return nil, nil
 	}
 	count, err := h.client.UserLogin.Bridge.DB.Message.CountMessagesInPortal(ctx, portalKey)
 	if err != nil || count <= 0 {
 		return nil, err
 	}
+	const maxTranscriptMessages = 500
+	if count > maxTranscriptMessages {
+		count = maxTranscriptMessages
+	}
 	portal, err := h.client.UserLogin.Bridge.GetPortalByKey(h.client.backgroundContext(ctx), portalKey)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/integration_host.go` around lines 238 - 250, SessionTranscript
currently requests the entire portal message count (CountMessagesInPortal) and
passes it unbounded into getAIHistoryMessages which can blow up memory for
long-lived chats; cap the fetch size by defining a sensible max (e.g.
maxHistoryMessages) and clamp the count before calling getAIHistoryMessages (use
min(count, maxHistoryMessages)), update SessionTranscript to pass the capped
value to getAIHistoryMessages and consider logging or annotating when the
returned history was truncated; reference functions/values: SessionTranscript,
CountMessagesInPortal, getAIHistoryMessages.
🧹 Nitpick comments (8)
bridges/ai/image_generation_tool.go (5)

255-272: Redundant nil check on btc.Client.UserLogin.Metadata.

Line 256 checks btc.Client.UserLogin.Metadata == nil, but after migrating to effectiveLoginMetadata, this check is no longer meaningful since effectiveLoginMetadata doesn't directly use UserLogin.Metadata. The subsequent nil check on loginMeta (line 260) is sufficient.

♻️ Suggested simplification
 func supportsOpenAIImageGen(btc *BridgeToolContext) bool {
-	if btc == nil || btc.Client == nil || btc.Client.UserLogin == nil || btc.Client.UserLogin.Metadata == nil {
+	if btc == nil || btc.Client == nil || btc.Client.UserLogin == nil {
 		return false
 	}
 	loginMeta := btc.Client.effectiveLoginMetadata(context.Background())
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/image_generation_tool.go` around lines 255 - 272, The nil check
for btc.Client.UserLogin.Metadata in supportsOpenAIImageGen is redundant because
effectiveLoginMetadata() is the authoritative source; remove the
btc.Client.UserLogin.Metadata == nil check from the initial guard and rely on
the existing nil check for loginMeta returned by
btc.Client.effectiveLoginMetadata(context.Background()), keeping the rest of the
logic (including Provider switch, magic proxy credential checks via
loginCredentialAPIKey/loginCredentialBaseURL, and resolveOpenAIAPIKey)
unchanged.

623-660: Same redundant Metadata nil check pattern in resolveOpenRouterImageGenEndpoint.

The btc.Client.UserLogin.Metadata == nil check at line 624 can be removed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/image_generation_tool.go` around lines 623 - 660, In
resolveOpenRouterImageGenEndpoint, remove the redundant nil check for
btc.Client.UserLogin.Metadata from the initial guard (the check that includes
btc.Client.UserLogin.Metadata == nil) because you call
btc.Client.effectiveLoginMetadata(...) later which already handles missing
Metadata; keep the other nil checks (btc, btc.Client, btc.Client.UserLogin)
intact and ensure the function still returns the same false/empty values when
required.

280-295: Same redundant Metadata nil check pattern.

Similar to supportsOpenAIImageGen, the btc.Client.UserLogin.Metadata == nil check at line 281 is no longer necessary after the migration to effectiveLoginMetadata.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/image_generation_tool.go` around lines 280 - 295, Remove the
redundant btc.Client.UserLogin.Metadata nil check in supportsGeminiImageGen:
rely on effectiveLoginMetadata(context.Background()) for login metadata
validation (as in supportsOpenAIImageGen). Update the initial guard to only
check btc, btc.Client and btc.Client.UserLogin as needed, call
btc.Client.effectiveLoginMetadata(...) and return false if it is nil, then keep
the existing provider switch (ProviderMagicProxy -> false, default -> false);
reference supportsGeminiImageGen, BridgeToolContext,
btc.Client.UserLogin.Metadata, and effectiveLoginMetadata to locate and edit the
code.

480-507: Same redundant Metadata nil check pattern in buildOpenAIImagesBaseURL.

The btc.Client.UserLogin.Metadata == nil check at line 481 can be removed for consistency with the new pattern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/image_generation_tool.go` around lines 480 - 507, Remove the
redundant btc.Client.UserLogin.Metadata == nil check from the initial guard in
buildOpenAIImagesBaseURL; keep the nil checks for btc, btc.Client, and
btc.Client.UserLogin only, and rely on loginMeta :=
btc.Client.effectiveLoginMetadata(...) to handle metadata presence and return
the appropriate error if nil—update the initial conditional accordingly so the
function compiles and behavior is consistent with effectiveLoginMetadata.

509-533: Same redundant Metadata nil check pattern in buildGeminiBaseURL.

The btc.Client.UserLogin.Metadata == nil check at line 510 can be removed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/image_generation_tool.go` around lines 509 - 533, Remove the
redundant nil check for UserLogin.Metadata in buildGeminiBaseURL: keep the
existing nil guards for btc, btc.Client, and btc.Client.UserLogin but delete the
btc.Client.UserLogin.Metadata == nil condition since effectiveLoginMetadata
(btc.Client.effectiveLoginMetadata) is already called and validated; ensure
buildGeminiBaseURL still returns the same errors when loginMeta is nil and that
Provider-specific logic (ProviderMagicProxy branch,
connector.resolveServiceConfig, normalizeProxyBaseURL, joinProxyPath) remains
unchanged.
bridges/ai/logout_cleanup.go (1)

94-109: bestEffortExec is redundant and should be removed.

The function at lines 107–109 is a direct pass-through to execDelete. Update the 6 callers across delete_chat.go, internal_prompt_db.go, and login_state_db.go to call execDelete directly instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/logout_cleanup.go` around lines 94 - 109, Remove the redundant
wrapper function bestEffortExec and replace its uses with direct calls to
execDelete; specifically delete the bestEffortExec declaration and update the
six call sites that currently call bestEffortExec (across delete_chat.go,
internal_prompt_db.go, and login_state_db.go) to call execDelete(ctx, db,
logger, query, args...) directly so argument expansion and behavior remain
unchanged.
bridges/ai/mcp_helpers.go (1)

82-101: Type switch should include a defensive default case for consistency.

The type switch at lines 95–100 lacks a default case, unlike the similar switches in loginCredentials() and ensureLoginCredentials() (which both return nil or do nothing for unsupported types). While all current callers pass *aiLoginConfig from loginConfigSnapshot(), adding a default case maintains defensive consistency and guards against future refactoring where unexpected types might be passed.

♻️ Suggested defensive handling
 	if loginCredentialsEmpty(creds) {
 		switch v := owner.(type) {
 		case *UserLoginMetadata:
 			v.Credentials = nil
 		case *aiLoginConfig:
 			v.Credentials = nil
+		default:
+			// Unsupported owner type; credentials not cleared
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/mcp_helpers.go` around lines 82 - 101, The type switch in
clearLoginMCPServer does not include a default branch, making it inconsistent
with loginCredentials() and ensureLoginCredentials(); update the switch in
clearLoginMCPServer (the one switching on owner in function clearLoginMCPServer)
to add a defensive default case that does nothing (no-op) so unsupported owner
types are ignored safely; keep the existing cases for *UserLoginMetadata and
*aiLoginConfig and ensure the default mirrors the harmless behavior used in
loginCredentials()/ensureLoginCredentials().
bridges/ai/media_understanding_runner.go (1)

926-926: Consider passing the actual context instead of context.Background().

Using context.Background() here loses any tracing, logging context, or cancellation signals from the caller. If effectiveLoginMetadata performs any I/O or logging that should respect the request context, consider passing the actual ctx parameter (or the surrounding function's context) instead.

This pattern appears at multiple call sites in this file (lines 926, 942, 1032, 1038, 1054).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/media_understanding_runner.go` at line 926, The call to
oc.effectiveLoginMetadata currently uses context.Background() which discards
caller tracing/cancellation; update calls like
oc.connector.resolveServiceConfig(oc.effectiveLoginMetadata(context.Background()))
to pass the upstream context (e.g., the local ctx variable) instead so
effectiveLoginMetadata and resolveServiceConfig receive the real request
context; apply the same change at all similar call sites in this file
(references: effectiveLoginMetadata, resolveServiceConfig).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@bridges/ai/custom_agents_db.go`:
- Around line 107-135: In saveCustomAgentForLogin ensure the trimmed agent.ID is
non-empty before using it: call strings.TrimSpace(agent.ID) into a local id
variable, and if id=="" return an error (or non-nil) so we don't write an
empty-key into loginMetadata (meta.CustomAgents) or insert a DB row; update the
branches that use strings.TrimSpace(agent.ID) (the fallback map insertion and
the scope.db.Exec call) to use that validated id; this prevents saving blank IDs
while keeping references to customAgentScopeForLogin, loginMetadata,
cloneAgentDefinitionContentMap, and aiCustomAgentsTable intact.

In `@bridges/ai/desktop_api_sessions.go`:
- Line 161: The call to effectiveLoginMetadata is using context.Background(),
dropping request cancellation/deadlines; update desktopAPIInstances to accept a
context.Context parameter (rename to desktopAPIInstances(ctx)) and thread that
ctx into the call that builds creds (use
loginCredentials(effectiveLoginMetadata(ctx))). Also update all internal calls
and call sites (e.g., places in sessions_tools.go and other callers) to pass the
incoming request context; this ensures the chain through loginConfigSnapshot →
ensureLoginConfigLoaded → loadAILoginConfig(ctx) receives the proper context and
cancels I/O when requests are cancelled.

In `@bridges/ai/gravatar.go`:
- Around line 38-43: The function ensureGravatarState dereferences
state.Gravatar without checking that the pointer state is non-nil; update
ensureGravatarState to first check if state == nil and either return a new
GravatarState (or allocate and return a new loginRuntimeState with Gravatar set)
or explicitly document/require non-nil callers — modify ensureGravatarState (and
any callers if you choose to allocate) to avoid panics by handling a nil
*loginRuntimeState before accessing state.Gravatar or state.Gravatar =
&GravatarState{}.

In `@bridges/ai/handleai.go`:
- Around line 129-156: The health-transition logic currently reads state via
loginStateSnapshot() then mutates via updateLoginState(), causing racey
decisions; fix by making the decision and mutation atomic inside
updateLoginState() for both recordProviderFailure (the failure path) and
recordProviderSuccess: inside the updateLoginState callback read the current
state.ConsecutiveErrors, compute nextErrors (or wasUnhealthy→nextHealthy) based
on the change, update state.ConsecutiveErrors and state.LastErrorAt there, and
set a boolean/enum captured by the outer scope indicating whether a
health-transition event should be emitted; after updateLoginState returns,
consult that captured result and call oc.UserLogin.BridgeState.Send(...) if and
only if the atomic transition crossed the healthWarningThreshold (use the same
threshold constant in both places). Ensure you reference and change the logic in
the functions that call loginStateSnapshot(), updateLoginState(), and
oc.UserLogin.BridgeState.Send (recordProviderFailure/recordProviderSuccess
flows) so all decisioning happens inside updateLoginState.

In `@bridges/ai/login_state_db.go`:
- Around line 64-75: The fallback conversion in loginRuntimeStateFromMetadata
currently omits the legacy NextChatIndex and LastHeartbeatEvent fields so chat
numbering and heartbeat seeding are lost; update loginRuntimeStateFromMetadata
to copy meta.NextChatIndex and meta.LastHeartbeatEvent into the returned
*loginRuntimeState (and likewise update the inverse mapper(s) that convert
runtime state back to UserLoginMetadata in the same file — any functions
handling mapping around loginRuntimeState/UserLoginMetadata between lines
~103-139) so both NextChatIndex and LastHeartbeatEvent are preserved across
migrations.
- Around line 249-265: The updateLoginState function currently lets fn mutate
oc.loginState in-place before persistence, so a failed save leaves the in-memory
cache out of sync; fix this by making a deep copy of oc.loginState (type
loginRuntimeState) inside updateLoginState, call fn on that copy, and only if fn
returns true attempt saveLoginRuntimeState with the copied state; on successful
save replace oc.loginState with the saved copy, otherwise leave oc.loginState
unchanged and return the error. Keep locking via oc.loginStateMu around the
load/copy/replace steps and use loadLoginRuntimeState when oc.loginState is nil
as currently done.

In `@bridges/ai/message_state_db.go`:
- Around line 127-136: The placeholder numbering uses the loop index i which
doesn't account for skipped blank IDs, causing gaps like $4,$6; fix the loop in
the code that builds args/placeholders (the block that iterates messageIDs and
populates args and placeholders) so the placeholder is derived from the actual
arg position rather than i—either keep an explicit counter starting at 4 and
increment only when you append a non-blank ID, or append the ID to args first
and then generate the placeholder with fmt.Sprintf("$%d", len(args)) so the
placeholder number always matches the argument index.

In `@bridges/ai/metadata.go`:
- Around line 95-114: The portal-state gap: add a metadata fallback and
documentation for portal reads—update loadAIPortalState to fallback to bridgev2
metadata (implement an aiPortalStateFromMetadata helper mirroring
aiLoginConfigFromMetadata/loginRuntimeStateFromMetadata) so aichats_portal_state
missing rows will populate Portal state from UserLoginMetadata, and ensure
CustomAgents persistence by retaining fallback in aichats_custom_agents reads to
UserLoginMetadata.CustomAgents; also add a brief comment near loadAILoginConfig,
loadLoginRuntimeState, loadAIPortalState explaining the runtime fallback chain
and call sites must load portal state before use.

In `@bridges/ai/status_text.go`:
- Line 203: The loop in lastAssistantUsage that iterates over history (populated
via oc.getAIHistoryMessages/GetLastNInPortal) is iterating backwards and thus
returns the oldest assistant message with tokens; change the loop to iterate
forward (for i := 0; i < len(history); i++) so it checks newest-first order as
returned by GetLastNInPortal and returns the most recent assistant message with
token usage; update any comments accordingly to reflect newest-first semantics.

In `@bridges/ai/tool_configured.go`:
- Line 57: The call to effectiveLoginMetadata currently uses
context.Background() (via meta =
oc.effectiveLoginMetadata(context.Background())), which detaches DB/metadata
lookups from the caller's cancellation/deadlines; update function signatures and
call-sites so the request context is threaded through: change
effectiveToolConfig to accept a ctx context.Context and use that when calling
effectiveLoginMetadata(ctx), and update effectiveSearchConfig and
effectiveFetchConfig to accept and forward the same ctx into
effectiveToolConfig; finally replace the context.Background() call with the
propagated ctx so all loginConfigSnapshot/database operations use the caller's
context.

---

Outside diff comments:
In `@bridges/ai/chat.go`:
- Around line 46-52: agentsEnabledForLogin currently returns false when
cfg.Agents is nil, causing a mismatch with shouldEnsureDefaultChat which treats
nil as enabled; update agentsEnabledForLogin (in AIClient) to treat a nil Agents
pointer as enabled by returning true when cfg.Agents == nil, otherwise return
the boolean value (*cfg.Agents) from the loginConfigSnapshot result so
default-config logins consistently enable agents.
- Around line 239-268: The response currently sets resp.UserID to
networkid.UserID(agent.ID) which doesn't match how agent ghosts are stored;
update agentContactResponse so the UserID returned for agent contacts uses the
bridge ghost ID via oc.agentUserID(agentID) (using the catalogAgentID result)
instead of networkid.UserID(agent.ID); if catalogAgentID(agent) is empty, keep
the existing fallback behavior, and ensure this change happens before calling
UserLogin.Bridge.GetGhostByID so the ghost hydrate uses the correct ID (refer to
functions agentContactResponse, catalogAgentID, oc.ResolveResponderForAgent, and
oc.agentUserID).

In `@bridges/ai/metadata.go`:
- Around line 313-325: cloneUserLoginMetadata performs a JSON round-trip which
strips non-serialized/transient fields (credentials, caches, profile data,
custom agents, error counters), so update cloneUserLoginMetadata to perform the
JSON marshal/unmarshal for the serializable fields and then explicitly copy the
transient fields from src into the resulting clone (e.g., copy Credentials,
Cache, Profile, CustomAgents, ErrorCounters or their pointer/deep copies as
appropriate) so callers that clone before mutating/persisting don't lose runtime
state; locate and modify cloneUserLoginMetadata to perform the manual transfer
after unmarshal and ensure pointer safety (deep-copy if needed) before returning
the clone.

In `@bridges/codex/constructors.go`:
- Around line 42-55: StartConnector calls aidb.EnsureSchema with cc.bridgeDB()
even when InitConnector left cc.db nil; wrap the EnsureSchema call in a
nil-check so it only runs when cc.bridgeDB() != nil (e.g. assign db :=
cc.bridgeDB(); if db == nil { return nil } else if err := aidb.EnsureSchema(ctx,
db); err != nil { return err }), ensuring StartConnector short-circuits cleanly
in no-DB scenarios and avoids passing nil to aidb.EnsureSchema.

---

Duplicate comments:
In `@bridges/ai/handleai.go`:
- Around line 387-390: Set meta.WelcomeSent = true and persist that change with
oc.savePortalQuiet using a cancellable context (bgCtx from
context.WithTimeout(oc.backgroundContext(ctx), 10*time.Second)) before any code
that emits the welcome notice; ensure you call oc.savePortalQuiet immediately
after setting meta.WelcomeSent and check/handle its error (log/stop the notice
emission) so the notice is only sent if the persistence succeeds.
- Around line 319-321: The AutoGreetingSent flag is being set in-memory but not
guaranteed persisted before sending the greeting; change the order and add error
handling so persistence happens first: set currentMeta.AutoGreetingSent = true,
call oc.savePortalQuiet(bgCtx, current, "auto greeting state") and check its
error (do not call oc.dispatchInternalMessage if save failed), logging or
returning the save error; only after a successful save call
oc.dispatchInternalMessage(..., "auto-greeting", true). This uses the existing
symbols currentMeta.AutoGreetingSent, oc.savePortalQuiet, and
oc.dispatchInternalMessage.
- Around line 456-463: The portal title is only persisted locally
(portal.Name/portal.NameSet and oc.savePortalQuiet) but not pushed to Matrix;
after oc.savePortalQuiet(bgCtx, portal, "room title") call the existing
lifecycle/materialization sync that pushes portal metadata to Matrix (i.e., the
oc method responsible for materializing/syncing portals) so the updated
portal.Title/portal.Name is sent to clients; if no such helper exists, add and
call a method like oc.MaterializePortal(ctx, portal) or
oc.SyncPortalTitleToMatrix(ctx, portal) immediately after saving to trigger the
external update.

In `@bridges/ai/integration_host.go`:
- Around line 238-250: SessionTranscript currently requests the entire portal
message count (CountMessagesInPortal) and passes it unbounded into
getAIHistoryMessages which can blow up memory for long-lived chats; cap the
fetch size by defining a sensible max (e.g. maxHistoryMessages) and clamp the
count before calling getAIHistoryMessages (use min(count, maxHistoryMessages)),
update SessionTranscript to pass the capped value to getAIHistoryMessages and
consider logging or annotating when the returned history was truncated;
reference functions/values: SessionTranscript, CountMessagesInPortal,
getAIHistoryMessages.

In `@bridges/ai/tools.go`:
- Around line 390-397: The routing in executeMessageReact incorrectly treats an
empty emoji as a removal request; change the conditional so only remove == true
forwards to executeMessageReactRemove (i.e., if remove { return
executeMessageReactRemove(...) }), and add explicit validation before attempting
an add reaction that rejects empty or missing emoji (return an error) so
malformed add-reaction calls are rejected instead of sent to
executeMessageReactRemove; reference executeMessageReact and
executeMessageReactRemove when making the change.

---

Nitpick comments:
In `@bridges/ai/image_generation_tool.go`:
- Around line 255-272: The nil check for btc.Client.UserLogin.Metadata in
supportsOpenAIImageGen is redundant because effectiveLoginMetadata() is the
authoritative source; remove the btc.Client.UserLogin.Metadata == nil check from
the initial guard and rely on the existing nil check for loginMeta returned by
btc.Client.effectiveLoginMetadata(context.Background()), keeping the rest of the
logic (including Provider switch, magic proxy credential checks via
loginCredentialAPIKey/loginCredentialBaseURL, and resolveOpenAIAPIKey)
unchanged.
- Around line 623-660: In resolveOpenRouterImageGenEndpoint, remove the
redundant nil check for btc.Client.UserLogin.Metadata from the initial guard
(the check that includes btc.Client.UserLogin.Metadata == nil) because you call
btc.Client.effectiveLoginMetadata(...) later which already handles missing
Metadata; keep the other nil checks (btc, btc.Client, btc.Client.UserLogin)
intact and ensure the function still returns the same false/empty values when
required.
- Around line 280-295: Remove the redundant btc.Client.UserLogin.Metadata nil
check in supportsGeminiImageGen: rely on
effectiveLoginMetadata(context.Background()) for login metadata validation (as
in supportsOpenAIImageGen). Update the initial guard to only check btc,
btc.Client and btc.Client.UserLogin as needed, call
btc.Client.effectiveLoginMetadata(...) and return false if it is nil, then keep
the existing provider switch (ProviderMagicProxy -> false, default -> false);
reference supportsGeminiImageGen, BridgeToolContext,
btc.Client.UserLogin.Metadata, and effectiveLoginMetadata to locate and edit the
code.
- Around line 480-507: Remove the redundant btc.Client.UserLogin.Metadata == nil
check from the initial guard in buildOpenAIImagesBaseURL; keep the nil checks
for btc, btc.Client, and btc.Client.UserLogin only, and rely on loginMeta :=
btc.Client.effectiveLoginMetadata(...) to handle metadata presence and return
the appropriate error if nil—update the initial conditional accordingly so the
function compiles and behavior is consistent with effectiveLoginMetadata.
- Around line 509-533: Remove the redundant nil check for UserLogin.Metadata in
buildGeminiBaseURL: keep the existing nil guards for btc, btc.Client, and
btc.Client.UserLogin but delete the btc.Client.UserLogin.Metadata == nil
condition since effectiveLoginMetadata (btc.Client.effectiveLoginMetadata) is
already called and validated; ensure buildGeminiBaseURL still returns the same
errors when loginMeta is nil and that Provider-specific logic
(ProviderMagicProxy branch, connector.resolveServiceConfig,
normalizeProxyBaseURL, joinProxyPath) remains unchanged.

In `@bridges/ai/logout_cleanup.go`:
- Around line 94-109: Remove the redundant wrapper function bestEffortExec and
replace its uses with direct calls to execDelete; specifically delete the
bestEffortExec declaration and update the six call sites that currently call
bestEffortExec (across delete_chat.go, internal_prompt_db.go, and
login_state_db.go) to call execDelete(ctx, db, logger, query, args...) directly
so argument expansion and behavior remain unchanged.

In `@bridges/ai/mcp_helpers.go`:
- Around line 82-101: The type switch in clearLoginMCPServer does not include a
default branch, making it inconsistent with loginCredentials() and
ensureLoginCredentials(); update the switch in clearLoginMCPServer (the one
switching on owner in function clearLoginMCPServer) to add a defensive default
case that does nothing (no-op) so unsupported owner types are ignored safely;
keep the existing cases for *UserLoginMetadata and *aiLoginConfig and ensure the
default mirrors the harmless behavior used in
loginCredentials()/ensureLoginCredentials().

In `@bridges/ai/media_understanding_runner.go`:
- Line 926: The call to oc.effectiveLoginMetadata currently uses
context.Background() which discards caller tracing/cancellation; update calls
like
oc.connector.resolveServiceConfig(oc.effectiveLoginMetadata(context.Background()))
to pass the upstream context (e.g., the local ctx variable) instead so
effectiveLoginMetadata and resolveServiceConfig receive the real request
context; apply the same change at all similar call sites in this file
(references: effectiveLoginMetadata, resolveServiceConfig).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cd0b8b37-f583-4ab0-a29a-c4740330be73

📥 Commits

Reviewing files that changed from the base of the PR and between 07e680c and 3d3f7a7.

📒 Files selected for processing (70)
  • .github/workflows/docker-agentremote.yml
  • .github/workflows/go.yml
  • .github/workflows/publish-release.yml
  • README.md
  • bridges/ai/agentstore.go
  • bridges/ai/bridge_db.go
  • bridges/ai/bridge_info.go
  • bridges/ai/bridge_info_test.go
  • bridges/ai/broken_login_client.go
  • bridges/ai/chat.go
  • bridges/ai/client.go
  • bridges/ai/commands.go
  • bridges/ai/constructors.go
  • bridges/ai/custom_agents_db.go
  • bridges/ai/delete_chat.go
  • bridges/ai/desktop_api_sessions.go
  • bridges/ai/gravatar.go
  • bridges/ai/handleai.go
  • bridges/ai/handlematrix.go
  • bridges/ai/heartbeat_events.go
  • bridges/ai/heartbeat_session.go
  • bridges/ai/image_generation_tool.go
  • bridges/ai/image_understanding.go
  • bridges/ai/integration_host.go
  • bridges/ai/integrations_config.go
  • bridges/ai/login.go
  • bridges/ai/login_config_db.go
  • bridges/ai/login_loaders.go
  • bridges/ai/login_state_db.go
  • bridges/ai/logout_cleanup.go
  • bridges/ai/mcp_client.go
  • bridges/ai/mcp_helpers.go
  • bridges/ai/mcp_servers.go
  • bridges/ai/media_understanding_cli.go
  • bridges/ai/media_understanding_runner.go
  • bridges/ai/message_state_db.go
  • bridges/ai/metadata.go
  • bridges/ai/model_catalog.go
  • bridges/ai/prompt_builder.go
  • bridges/ai/provisioning.go
  • bridges/ai/response_finalization.go
  • bridges/ai/scheduler_heartbeat_test.go
  • bridges/ai/session_store.go
  • bridges/ai/sessions_tools.go
  • bridges/ai/status_text.go
  • bridges/ai/subagent_announce.go
  • bridges/ai/timezone.go
  • bridges/ai/token_resolver.go
  • bridges/ai/tool_approvals_policy.go
  • bridges/ai/tool_configured.go
  • bridges/ai/tool_policy_chain.go
  • bridges/ai/tool_policy_chain_test.go
  • bridges/ai/tools.go
  • bridges/ai/tools_beeper_feedback.go
  • bridges/codex/config.go
  • bridges/codex/connector.go
  • bridges/codex/connector_test.go
  • bridges/codex/constructors.go
  • bridges/openclaw/README.md
  • bridges/openclaw/gateway_client.go
  • bridges/openclaw/gateway_client_test.go
  • cmd/agentremote/commands.go
  • docker/agentremote/README.md
  • docs/bridge-orchestrator.md
  • pkg/agents/prompt.go
  • pkg/aidb/001-init.sql
  • pkg/aidb/db.go
  • pkg/aidb/db_test.go
  • sdk/room_features.go
  • tools/generate-homebrew-cask.sh
✅ Files skipped from review due to trivial changes (18)
  • docs/bridge-orchestrator.md
  • bridges/ai/media_understanding_cli.go
  • .github/workflows/go.yml
  • pkg/agents/prompt.go
  • .github/workflows/docker-agentremote.yml
  • bridges/ai/tool_policy_chain_test.go
  • docker/agentremote/README.md
  • bridges/ai/tools_beeper_feedback.go
  • bridges/ai/integrations_config.go
  • bridges/ai/mcp_client.go
  • bridges/ai/tool_policy_chain.go
  • tools/generate-homebrew-cask.sh
  • README.md
  • bridges/openclaw/gateway_client.go
  • bridges/codex/config.go
  • bridges/openclaw/README.md
  • bridges/ai/provisioning.go
  • .github/workflows/publish-release.yml
🚧 Files skipped from review as they are similar to previous changes (10)
  • bridges/ai/broken_login_client.go
  • bridges/ai/bridge_info.go
  • bridges/ai/commands.go
  • pkg/aidb/db_test.go
  • bridges/ai/handlematrix.go
  • bridges/ai/login_loaders.go
  • bridges/codex/connector_test.go
  • bridges/ai/login_config_db.go
  • bridges/ai/session_store.go
  • bridges/ai/constructors.go
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: build-agentremote-cli-docker (amd64)
  • GitHub Check: build-docker
  • GitHub Check: Lint
  • GitHub Check: build-docker
  • GitHub Check: Lint
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-03-16T09:01:24.464Z
Learnt from: batuhan
Repo: beeper/agentremote PR: 71
File: bridges/ai/connector.go:53-63
Timestamp: 2026-03-16T09:01:24.464Z
Learning: In package ai, the AI connector’s configuration type (Config) defines Bridge as a value field of type BridgeConfig (not a pointer). Therefore, accessing oc.Config.Bridge.CommandPrefix in OpenAIConnector.applyRuntimeDefaults (bridges/ai/connector.go) is safe and does not require a nil-check.

Applied to files:

  • bridges/ai/tool_configured.go
  • bridges/ai/bridge_info_test.go
  • bridges/ai/media_understanding_runner.go
  • bridges/ai/token_resolver.go
  • bridges/ai/bridge_db.go
  • bridges/ai/login.go
  • bridges/codex/constructors.go
  • bridges/codex/connector.go
  • bridges/ai/handleai.go
  • bridges/ai/chat.go
  • bridges/ai/client.go
🔇 Additional comments (46)
bridges/openclaw/gateway_client_test.go (1)

83-84: Nice alignment with production user-agent composition.

Using openClawGatewayUserAgentBase + resolveGatewayClientVersion() makes this test less brittle than a hardcoded literal and keeps it consistent with the gateway client implementation.

sdk/room_features.go (1)

74-74: Namespace fallback update looks correct.

Switching the default capability ID to the agentremote SDK namespace is consistent with the refactor and keeps the fallback aligned with current package identity.

bridges/ai/bridge_info_test.go (3)

38-52: Rename and call-site migration are consistent.

TestApplyAIChatsBridgeInfo and the updated applyAIChatsBridgeInfo(...) invocation are aligned, and the visible-DM expectations still validate the intended behavior.


58-71: Beeper-room protocol assertion now matches new contract.

Switching this assertion to aiBridgeProtocolID correctly reflects the fixed AI protocol behavior for beeper-addressed rooms.


88-88: Background-room path was correctly migrated too.

The applyAIChatsBridgeInfo(...) call update in the internal/background test keeps normalization coverage intact.

cmd/agentremote/commands.go (6)

302-303: Branding update for doctor command looks consistent.

Nice cleanup on the user-facing wording here; it aligns with the new AgentRemote Manager naming.


347-358: Centralized command-spec normalization is a solid refactor.

Applying the binaryName rewrite in one place is clean and helps keep help text/completions in sync.


476-477: Usage banner changes are clear and user-friendly.

Good move using branded header text while keeping invocation dynamic via binaryName.


503-577: Bash completion parameterization is implemented cleanly.

All key wiring points now respect binaryName, which keeps completion generation aligned with renamed binaries.


585-635: Zsh completion updates are consistent and complete.

Nice job propagating binaryName through compdef/function naming and top-level command descriptions.


676-736: Fish completion migration to binaryName is thorough.

The command target replacement is complete across top-level, positional, and flag completion entries.

bridges/codex/connector.go (1)

53-76: Runtime defaulting migration looks consistent.

The switch to the SDK helpers keeps the defaults centralized, and the new client-info fallbacks avoid hardcoded duplicates in this path.

bridges/ai/timezone.go (1)

44-46: Snapshot-based timezone resolution looks correct.

The new config-snapshot lookup keeps the existing normalize/fallback behavior intact and safely falls back to env/default when needed.

bridges/ai/bridge_db.go (3)

11-21: AI table-name constants centralization is a good cleanup.

Consolidating these names reduces hard-coded drift across DB call sites.


29-29: db_section=ai logger scoping change looks good.

This aligns child-DB logs with the new AI schema ownership.


77-82: bridgeDBFromPortal nil guards are solid.

The helper is defensive and consistent with the existing login-based DB resolver flow.

bridges/ai/heartbeat_session.go (1)

41-42: Including bridge/login in sessionStoreRef is the right direction.

This improves cross-login/session isolation compared to agent-only scoping.

bridges/ai/mcp_servers.go (1)

148-148: Snapshot-based MCP token sourcing looks correct.

This is consistent with the ongoing move away from direct login metadata reads.

bridges/ai/model_catalog.go (1)

220-220: Using effective login metadata here is a good fix.

The call site now matches the newer metadata resolution flow while keeping the existing guard path.

bridges/ai/scheduler_heartbeat_test.go (1)

123-124: aidb.EnsureSchema migration in test setup looks good.

This keeps test initialization aligned with the current AI schema bootstrap API.

bridges/ai/image_understanding.go (1)

81-82: No nil-contract issue; loginStateSnapshot is guaranteed non-nil.

The implementation always returns a non-nil *loginRuntimeState:

  • ensureLoginStateLoaded handles load errors by allocating &loginRuntimeState{} (line 239) and never returns nil
  • cloneLoginRuntimeState checks for nil input and returns an allocated struct in all cases (lines 50–51)
  • Therefore, loginState.ModelCache at line 85 is accessed on a guaranteed non-nil pointer without panic risk.
bridges/ai/image_generation_tool.go (1)

182-186: LGTM!

The migration from loginMetadata(btc.Client.UserLogin) to btc.Client.effectiveLoginMetadata(context.Background()) is consistent with the broader refactor. The nil check on loginMeta is preserved.

bridges/ai/mcp_helpers.go (1)

68-80: LGTM!

The refactor to accept owner any with ensureLoginCredentials(owner) provides flexibility for both *UserLoginMetadata and *aiLoginConfig types.

bridges/ai/gravatar.go (1)

184-189: LGTM!

The migration to loginStateSnapshot is correct with proper nil checks for both loginState and nested Gravatar/Primary fields.

bridges/ai/sessions_tools.go (3)

118-127: LGTM!

The migration to getAIHistoryMessages maintains the existing error handling (only using messages on success) and properly limits the result set.


270-286: LGTM!

Consistent migration to getAIHistoryMessages with proper error handling that returns an error result on failure.


572-581: LGTM!

The migration in lastMessageTimestamp correctly handles errors and empty results by returning 0.

bridges/ai/tool_approvals_policy.go (1)

17-28: LGTM!

The narrowed approval bypass list correctly aligns with the removal of corresponding message tool actions (reactions, read, member-info, channel-info, list-pins). The remaining exemptions are appropriately limited to read-only operations.

bridges/ai/subagent_announce.go (2)

45-74: LGTM!

The migration to getAIHistoryMessages in readLatestAssistantReply preserves the error handling and message filtering logic.


105-138: LGTM!

The migration in buildSubagentStatsLine appropriately ignores errors (using _) since stats are optional and the function already handles empty message slices gracefully.

bridges/ai/token_resolver.go (1)

185-225: LGTM!

The refactor cleanly separates metadata-based resolution from config-based resolution. The extracted resolveProviderAPIKeyForConfig maintains the same provider-specific logic while enabling config-driven API key resolution.

bridges/ai/logout_cleanup.go (1)

12-91: LGTM on expanded cleanup coverage.

The comprehensive cleanup now covers all AI-specific tables, and the in-memory state reset ensures the client is properly cleared on logout.

bridges/ai/prompt_builder.go (1)

164-172: Internal prompt rows can evict chat history from replay.

The hr.limit is applied after merging internal prompt rows into candidates. This means a burst of internal prompt rows could push out actual user/assistant messages entirely.

Additionally, the image-injection logic at line 201 (i < maxHistoryImageMessages) now counts internal rows too, potentially corrupting the vision injection window.

Consider:

  1. Tracking internal rows separately and not counting them toward hr.limit
  2. Using a counter of non-internal entries for image injection rather than the loop index
bridges/ai/agentstore.go (2)

547-547: CreateRoom returns success even when portal persistence fails silently.

savePortalQuiet logs errors but does not return them. The caller receives a successful room creation response even if metadata persistence failed. This could lead to inconsistent state where the Matrix room exists but portal metadata is missing.

Consider using a persistence function that returns errors, or accepting this as a trade-off for user experience (room works but some metadata may be missing).


36-66: LGTM - Clean migration to login-scoped table helpers.

The LoadAgents function properly:

  1. Accepts context parameter for DB operations
  2. Loads custom agents via listCustomAgentsForLogin
  3. Gates presets based on provider
  4. Handles nil checks appropriately
bridges/ai/delete_chat.go (1)

62-93: LGTM - Comprehensive cleanup of persisted session artifacts.

The delete function now properly cleans up multiple tables:

  • AI sessions
  • System events
  • Portal state
  • Message state

This aligns with the migration to DB-backed tables for portal/session state.

Regarding the past review concern about login-level portal references: given that this PR migrates away from in-memory UserLogin metadata to DB-backed tables, stale references in login metadata should no longer be a concern as long as the new tables (aiPortalStateTable, aichats_message_state) are the source of truth.

bridges/ai/media_understanding_runner.go (1)

601-601: Temp directory prefix updated to match new branding.

The change from agentremote-media-* to aichats-media-* aligns with the broader rename in this PR.

bridges/ai/response_finalization.go (2)

32-47: LGTM - SDK migration for continuation messages.

The function correctly migrates from agentremote.EventTiming to sdk.EventTiming and uses sdk.BuildContinuationMessage. The timing parameters are properly passed through.


135-137: LGTM - Proper session store keying with bridge and login IDs.

The heartbeat session store reference now correctly derives BridgeID and LoginID from loginDBContext(oc), aligning with the broader session-store changes. This ensures proper scoping of heartbeat state per login.

bridges/ai/heartbeat_events.go (2)

61-66: LGTM - Robust composite key for heartbeat login keying.

The new heartbeatLoginKey function properly creates a composite key from BridgeID|LoginID with appropriate nil checks. This is more robust than using UserLoginID alone.


117-126: LGTM - Proper state update with deduplication.

The persistence logic correctly:

  1. Compares the new event against the previous state
  2. Only persists if there's a meaningful change
  3. Returns false to skip unnecessary writes when events are identical
bridges/ai/login.go (3)

33-36: LGTM - SDK error helper migration.

The login error definitions properly migrate to sdk.NewLoginRespError with appropriate status codes and error codes.


213-257: LGTM - Robust login config handling with proper persistence.

The login flow now:

  1. Loads existing config for relogin scenarios (line 219)
  2. Handles credentials via aiLoginConfig (lines 239-242)
  3. Validates using the new loginMetadataView pattern (line 243)
  4. Properly persists config with error handling (lines 255-257)

Good addition of the SAVE_LOGIN_FAILED error case.


322-337: The implementation correctly handles credential resolution for new logins.

The loginMetadataView function properly reads credentials from cfg.Credentials and clones them into the temporary metadata object, which is then used in the API key resolution. The null-safety check for cfg ensures this works correctly even when the configuration is not yet fully initialized.

pkg/aidb/db.go (1)

27-39: LGTM — Simplified schema initialization for unreleased bridge.

The EnsureSchema function is appropriate for a bridge that hasn't been released yet. The comment clearly documents the rationale, and the schema file correctly uses idempotent statements (CREATE TABLE IF NOT EXISTS, CREATE INDEX IF NOT EXISTS) that allow safe re-application on every startup.

Once this bridge is released, implement proper migration logic if schema changes are required.

pkg/aidb/001-init.sql (1)

1-2: Plan migration strategy before first release.

The bridge has not been released yet (as documented in the code), so the concern about existing installs breaking is not currently applicable. However, EnsureSchema only applies 001-init.sql without any incremental migration support. Once the first release ships, any subsequent schema changes will have no path to upgrade existing databases. Add a migration or version-tracking mechanism now rather than waiting for the first schema update request.

			> Likely an incorrect or invalid review comment.

Comment thread bridges/ai/custom_agents_db.go
Comment thread bridges/ai/desktop_api_sessions.go Outdated
Comment thread bridges/ai/gravatar.go Outdated
Comment thread bridges/ai/handleai.go Outdated
Comment thread bridges/ai/login_state_db.go Outdated
Comment thread bridges/ai/login_state_db.go Outdated
Comment thread bridges/ai/message_state_db.go Outdated
Comment thread bridges/ai/metadata.go Outdated
Comment thread bridges/ai/status_text.go
Comment thread bridges/ai/tool_configured.go Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
bridges/ai/provider_openai.go (1)

43-45: ⚠️ Potential issue | 🟡 Minor

Fix stale constructor doc comments.

Line 43 and Line 44 still reference NewOpenAIProviderWithBaseURL, but this file now exposes NewOpenAIProviderWithUserID here. Please update the docblock to match the current API.

✏️ Proposed fix
-// NewOpenAIProviderWithBaseURL creates an OpenAI provider with custom base URL
-// Used for OpenRouter, Beeper proxy, or custom endpoints
 // NewOpenAIProviderWithUserID creates an OpenAI provider that passes user_id with each request.
-// Used for Beeper proxy to ensure correct rate limiting and feature flags per user.
+// Supports custom base URLs (OpenRouter, Beeper proxy, or other endpoints) and
+// passes user_id for per-user rate limiting and feature flags.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/provider_openai.go` around lines 43 - 45, The docblock is stale:
it references NewOpenAIProviderWithBaseURL but the exported constructor is
NewOpenAIProviderWithUserID; update the top comments to accurately name and
describe the current constructors (e.g., mention NewOpenAIProviderWithUserID
and, if applicable, NewOpenAIProviderWithBaseURL only when that function exists)
so the comment matches the API; edit the comment lines near the OpenAI provider
constructors (references to NewOpenAIProviderWithBaseURL and
NewOpenAIProviderWithUserID) to use the correct function names and concise
descriptions.
bridges/ai/integration_host.go (1)

56-64: ⚠️ Potential issue | 🟠 Major

Propagate portal-save failures from the runtime host.

SavePortal always returns nil because it routes through savePortalQuiet. That breaks the host contract for integrations and lets callers continue after persistence failed. Return h.client.savePortal(...) directly.

Suggested fix
 func (h *runtimeIntegrationHost) SavePortal(ctx context.Context, portal *bridgev2.Portal, reason string) error {
 	if h == nil || h.client == nil {
 		return nil
 	}
 	if portal == nil {
 		return nil
 	}
-	h.client.savePortalQuiet(ctx, portal, reason)
-	return nil
+	return h.client.savePortal(ctx, portal, reason)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/integration_host.go` around lines 56 - 64, The SavePortal method
currently swallows errors by calling h.client.savePortalQuiet and always
returning nil; update runtimeIntegrationHost.SavePortal to preserve the existing
nil checks but call and return the result of h.client.savePortal(ctx, portal,
reason) (instead of savePortalQuiet) so persistence failures propagate to
callers; keep the early returns when h or h.client or portal are nil.
bridges/ai/handleai.go (1)

278-285: ⚠️ Potential issue | 🟠 Major

Roll back AutoGreetingSent when dispatch fails.

If dispatchInternalMessage fails after Line 279, the persisted flag stays true and this room never retries the auto-greeting. Mirror the welcome-message path here: clear the flag and persist the rollback before returning.

Suggested fix
 			currentMeta.AutoGreetingSent = true
 			if err := oc.savePortal(bgCtx, current, "auto greeting state"); err != nil {
 				oc.loggerForContext(ctx).Warn().Err(err).Msg("Failed to persist auto greeting state")
 				return
 			}
 			if _, _, err := oc.dispatchInternalMessage(bgCtx, current, currentMeta, autoGreetingPrompt, "auto-greeting", true); err != nil {
+				currentMeta.AutoGreetingSent = false
+				if saveErr := oc.savePortal(bgCtx, current, "auto greeting rollback"); saveErr != nil {
+					oc.loggerForContext(ctx).Warn().Err(saveErr).Msg("Failed to roll back auto greeting state")
+				}
 				oc.loggerForContext(ctx).Warn().Err(err).Msg("Failed to dispatch auto greeting")
 			}
 			return
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/handleai.go` around lines 278 - 285, The code sets
currentMeta.AutoGreetingSent = true and persists it via oc.savePortal, but if
oc.dispatchInternalMessage fails the flag remains true; change the error path
after oc.dispatchInternalMessage (the call in the block using bgCtx, current,
currentMeta, autoGreetingPrompt, "auto-greeting") to reset
currentMeta.AutoGreetingSent = false and call oc.savePortal(bgCtx, current,
"rollback auto greeting state") (log any save error) before returning so the
persisted state is rolled back and the room can retry the auto-greeting.
bridges/ai/handlematrix.go (1)

504-511: ⚠️ Potential issue | 🟠 Major

Redaction alone doesn't evict the stale assistant turn from AI history.

regenerateFromEdit now reads context via getAIHistoryMessages, but this branch only redacts the Matrix event. Unless the old assistant turn is also deleted or excluded from the AI store, transcripts and later context reads can still include the stale response alongside the regenerated one.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/handlematrix.go` around lines 504 - 511, The code only redacts the
Matrix event (via redactEventViaPortal) but does not remove the stale assistant
turn from the AI history store, so regenerateFromEdit which uses
getAIHistoryMessages can still return the old response; after successful
redaction (when assistantResponse != nil and assistantResponse.MXID != ""), also
remove or mark the corresponding AI history entry as deleted/evicted from the AI
store (e.g., call the module method that removes AI transcript entries or mark
assistantResponse.ID as excluded) and then call notifySessionMutation so
subsequent getAIHistoryMessages calls will not include the stale response;
ensure the chosen removal API (e.g., oc.evictAIMessageFromStore /
oc.deleteAIHistoryEntry) is used with the same unique identifier used by
getAIHistoryMessages.
♻️ Duplicate comments (8)
bridges/ai/agentstore.go (2)

82-95: ⚠️ Potential issue | 🟡 Minor

Guard the write helpers before dereferencing s.client.UserLogin.

loadCustomAgents and loadCustomAgent handle a nil or half-initialized adapter safely, but saveAgent and deleteAgent still dereference s.client.UserLogin unconditionally on Line 88 and Line 94. That turns missing login state into a panic instead of a normal error.

Suggested fix
 func (s *AgentStoreAdapter) saveAgent(ctx context.Context, agent *AgentDefinitionContent) error {
 	if agent == nil {
 		return nil
 	}
+	if s == nil || s.client == nil || s.client.UserLogin == nil {
+		return errors.New("login state is not available")
+	}
 	s.mu.Lock()
 	defer s.mu.Unlock()
 	return saveCustomAgentForLogin(ctx, s.client.UserLogin, agent)
 }
 
 func (s *AgentStoreAdapter) deleteAgent(ctx context.Context, agentID string) error {
+	if s == nil || s.client == nil || s.client.UserLogin == nil {
+		return errors.New("login state is not available")
+	}
 	s.mu.Lock()
 	defer s.mu.Unlock()
 	return deleteCustomAgentForLogin(ctx, s.client.UserLogin, agentID)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/agentstore.go` around lines 82 - 95, saveAgent and deleteAgent
currently dereference s.client.UserLogin without guarding, which can panic when
the adapter is nil/half-initialized; update AgentStoreAdapter.saveAgent and
AgentStoreAdapter.deleteAgent to check that s.client != nil and that
s.client.UserLogin is non-empty (or otherwise valid) before calling
saveCustomAgentForLogin or deleteCustomAgentForLogin, returning a descriptive
error if the login is missing; perform the check while holding s.mu (or acquire
mu before checking) to preserve concurrency guarantees and keep calls to
saveCustomAgentForLogin/deleteCustomAgentForLogin only when the login is
present.

505-512: ⚠️ Potential issue | 🟡 Minor

Trim room.Name before copying it into chatInfo.Name.

applyPortalRoomName normalizes whitespace, but chatInfo.Name = &room.Name persists the raw input. " " can still save a blank portal name, and " foo " leaves the room state and stored metadata out of sync.

Suggested fix
 		Mutate: func(portal *bridgev2.Portal, chatInfo *bridgev2.ChatInfo) {
-			if room.Name == "" {
-				return
-			}
-			b.client.applyPortalRoomName(ctx, portal, room.Name)
-			if chatInfo != nil {
-				chatInfo.Name = &room.Name
+			if trimmedName := strings.TrimSpace(room.Name); trimmedName != "" {
+				b.client.applyPortalRoomName(ctx, portal, trimmedName)
+				if chatInfo != nil {
+					chatInfo.Name = &trimmedName
+				}
 			}
 		},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/agentstore.go` around lines 505 - 512, The Mutate closure assigns
the raw room.Name into chatInfo.Name without trimming, causing whitespace-only
or mismatched stored names; before calling b.client.applyPortalRoomName(ctx,
portal, room.Name) and before setting chatInfo.Name, trim whitespace (e.g.
strings.TrimSpace) into a local variable (or overwrite room.Name with the
trimmed value), check for empty after trimming, then use that trimmed value when
calling applyPortalRoomName and when assigning chatInfo.Name to ensure stored
metadata and room state remain consistent.
bridges/ai/logout_cleanup.go (1)

19-19: ⚠️ Potential issue | 🟠 Major

Don't clear runtime state after a partial purge failure.

purgeLoginData still only logs joined delete errors, then immediately drops the in-memory login state and config anyway. That leaves logout looking successful while some login-owned rows may still be in the database, and it makes retry/inspection harder because the process has already forgotten the runtime state. Please return the aggregated error to the logout caller before resetting local state.

Also applies to: 93-100

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/logout_cleanup.go` at line 19, purgeLoginData currently logs
aggregated delete errors but immediately clears the in-memory login state and
config, which can hide failures; change purgeLoginData to return an error
(aggregate any delete errors you already join) and only clear the runtime login
state/config after the returned error is nil. Locate the similar cleanup block
referenced at the other cleanup section (the one that also logs joined delete
errors and then resets in-memory state) and apply the same pattern: return the
aggregated error to the caller instead of swallowing it, and move any code that
drops in-memory login state/config to run only when the error is nil.
bridges/ai/login_loaders.go (2)

24-26: ⚠️ Potential issue | 🔴 Critical

Missing nil check on metadata can cause panic.

loginMetadata(existing.UserLogin) can return nil, causing a nil-pointer dereference when accessing .Provider.

🐛 Proposed fix
 	existingProvider := ""
 	existingBaseURL := ""
 	if existing.UserLogin != nil {
-		existingProvider = strings.TrimSpace(loginMetadata(existing.UserLogin).Provider)
+		if meta := loginMetadata(existing.UserLogin); meta != nil {
+			existingProvider = strings.TrimSpace(meta.Provider)
+		}
 	}

,

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/login_loaders.go` around lines 24 - 26, The code calls
loginMetadata(existing.UserLogin) and dereferences .Provider without checking
the returned metadata; to fix, call loginMetadata(existing.UserLogin), store its
result in a variable (e.g., meta), verify meta != nil (and optionally
meta.Provider != "") before accessing meta.Provider, and only then assign
existingProvider = strings.TrimSpace(meta.Provider); update the block that
references existing.UserLogin and existingProvider to use this nil-checked meta
variable to avoid a nil-pointer panic.

89-102: ⚠️ Potential issue | 🔴 Critical

Nil meta dereference at line 102.

When loginMetadata(login) returns nil at line 90, the code proceeds to line 102 where meta.Provider is accessed, causing a panic.

🐛 Proposed fix
 	if meta == nil {
 		meta = loginMetadata(login)
 	}
+	if meta == nil {
+		oc.evictCachedClient(login.ID, nil)
+		login.Client = newBrokenLoginClient(login, initLoginClientError)
+		return nil
+	}
 	if cfg == nil {

,

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/login_loaders.go` around lines 89 - 102, The code may dereference
nil meta when calling oc.resolveProviderAPIKeyForConfig(meta.Provider, cfg);
ensure meta is non-nil before accessing meta.Provider by initializing a safe
default: after calling meta = loginMetadata(login) check if meta is still nil
and if so create a default login metadata (e.g., &loginMetadataType{Provider:
""} or equivalent) or use a local provider variable derived via a nil-safe
helper; update the block around loginMetadata(login) and the key assignment so
resolveProviderAPIKeyForConfig receives a non-nil provider value, referencing
the symbols meta, loginMetadata, cfg, loadAILoginConfig and
resolveProviderAPIKeyForConfig to locate and modify the logic.
bridges/ai/login_config_db.go (1)

139-156: ⚠️ Potential issue | 🔴 Critical

updateLoginConfig still holds mutex during saveAILoginConfig call, causing deadlock.

The function acquires loginConfigMu at line 143 with defer Unlock(), then calls saveAILoginConfig at line 155. However, saveAILoginConfig (lines 109-112) also tries to acquire loginConfigMu when login.Client is *AIClient, causing a deadlock on any successful mutation.

The past review comment indicated this was addressed, but the current code still exhibits the same pattern.

🔒 Suggested fix: release mutex before save, or use a DB-only save path
 func (oc *AIClient) updateLoginConfig(ctx context.Context, fn func(*aiLoginConfig) bool) error {
 	if oc == nil || oc.UserLogin == nil {
 		return nil
 	}
 	oc.loginConfigMu.Lock()
-	defer oc.loginConfigMu.Unlock()
 	if oc.loginConfig == nil {
 		cfg, err := loadAILoginConfig(ctx, oc.UserLogin)
 		if err != nil {
+			oc.loginConfigMu.Unlock()
 			return err
 		}
 		oc.loginConfig = cfg
 	}
 	if !fn(oc.loginConfig) {
+		oc.loginConfigMu.Unlock()
 		return nil
 	}
-	return saveAILoginConfig(ctx, oc.UserLogin, oc.loginConfig)
+	cfg := cloneAILoginConfig(oc.loginConfig)
+	oc.loginConfigMu.Unlock()
+	return saveAILoginConfig(ctx, oc.UserLogin, cfg)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/login_config_db.go` around lines 139 - 156, The updateLoginConfig
function currently holds oc.loginConfigMu (locked in updateLoginConfig) while
calling saveAILoginConfig, which can re-enter the same mutex via
saveAILoginConfig when login.Client is *AIClient; to fix, compute and apply the
mutation under the lock (call loadAILoginConfig and invoke fn(oc.loginConfig)
while locked) but do NOT call saveAILoginConfig while holding oc.loginConfigMu:
record whether fn returned true and copy or retain the pointer to the updated
config, then unlock before calling saveAILoginConfig(ctx, oc.UserLogin,
oc.loginConfig). Ensure the lock/unlock uses oc.loginConfigMu and that no other
code path assumes saveAILoginConfig runs under the same mutex.
bridges/ai/chat.go (1)

970-983: ⚠️ Potential issue | 🟠 Major

Don't ignore agent-portal persistence failures.

This still mutates resolved-target / model-override / avatar fields and then calls savePortalQuiet. If that save fails, chat creation continues with only in-memory identity changes, so the room can reopen against the wrong target after reload. Make this path return an error and abort the create flow on failure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/chat.go` around lines 970 - 983, The code mutates portal fields
(setPortalResolvedTarget, pm.RuntimeModelOverride via ResolveAlias,
portal.AvatarID/AvatarMXC) and then calls oc.savePortalQuiet but ignores its
error; change the flow so oc.savePortalQuiet(ctx, portal, saveReason) returns an
error which you check immediately, and if non-nil you return that error to abort
the create flow (do not proceed to oc.ensureAgentGhostDisplayName). Ensure the
caller receives the propagated error instead of continuing with only in-memory
changes so the room creation fails on persistence failure.
bridges/ai/queue_runtime.go (1)

165-183: ⚠️ Potential issue | 🟠 Major

Interrupt mode still depends on a stale room-busy snapshot.

roomBusy is decided before acquireRoom. If another run grabs the room between Lines 165 and 183, interrupt mode falls through to queue/steer instead of canceling the active run. Re-check the interrupt decision after a failed acquire, or make the busy-check + acquire decision atomic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/queue_runtime.go` around lines 165 - 183, The interrupt decision
uses a stale roomBusy snapshot; after attempting to acquire the room with
oc.acquireRoom (i.e., when directRun is false), re-check the current busy state
instead of relying on the original roomBusy: call oc.roomHasActiveRun(roomID) ||
oc.roomHasPendingQueueWork(roomID) again and if queueSettings.Mode ==
airuntime.QueueModeInterrupt and the re-check shows the room is busy, invoke
oc.cancelRoomRun(roomID) and oc.clearPendingQueue(ctx, roomID) and then attempt
to acquire the room again (retry oc.acquireRoom) so the interrupt path reliably
cancels an active run grabbed between the original check and the acquire.
🧹 Nitpick comments (13)
bridges/ai/bootstrap_context.go (1)

17-19: Consider logging store-initialization failures before returning.

This path currently fails closed with no signal, which can make missing bootstrap context hard to diagnose.

Suggested tweak
 	store, err := oc.textFSStoreForAgent(agentID)
 	if err != nil {
+		oc.loggerForContext(ctx).Warn().
+			Err(err).
+			Str("agent_id", agentID).
+			Msg("failed to initialize agent textfs store for bootstrap context")
 		return nil
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/bootstrap_context.go` around lines 17 - 19, The code returns nil
on failure from oc.textFSStoreForAgent(agentID) without any logging; update the
failure path in the function containing oc.textFSStoreForAgent(agentID) to log
the error (including agentID and err) before returning so initialization
failures are visible—use the package logger available in that scope (e.g.,
oc.logger.Errorf or a standard log.Printf if no logger exists) and include a
clear message like "failed to initialize textFS store for agent" plus the error
details.
bridges/ai/bridge_db.go (1)

270-281: Minor: Redundant trim on line 277.

canonicalLoginBridgeID already trims, so strings.TrimSpace(bridgeID) on line 277 is redundant. Not harmful, but could be simplified.

♻️ Optional simplification
 func loginScopeForLogin(login *bridgev2.UserLogin) *loginScope {
 	db := bridgeDBFromLogin(login)
 	if db == nil {
 		return nil
 	}
 	bridgeID := canonicalLoginBridgeID(login)
 	loginID := canonicalLoginID(login)
-	if strings.TrimSpace(bridgeID) == "" || loginID == "" {
+	if bridgeID == "" || loginID == "" {
 		return nil
 	}
 	return &loginScope{db: db, bridgeID: bridgeID, loginID: loginID}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/bridge_db.go` around lines 270 - 281, The check uses
strings.TrimSpace(bridgeID) redundantly because canonicalLoginBridgeID already
trims; in function loginScopeForLogin replace the TrimSpace call with a simple
empty-string check (bridgeID == "") so the conditional becomes if bridgeID == ""
|| loginID == "" { return nil }, keeping the rest of the logic and symbols
(loginScope, bridgeDBFromLogin, canonicalLoginBridgeID, canonicalLoginID)
unchanged.
bridges/ai/canonical_prompt_messages.go (1)

89-119: Tool arguments normalization is thorough but complex.

The nested JSON parse-then-re-marshal logic handles edge cases (string JSON, raw objects, fallback to formatted value). Consider extracting this into a helper like normalizeToolArguments(input any) string for clarity and testability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/canonical_prompt_messages.go` around lines 89 - 119, The
tool-argument normalization block in the "tool" case is complex and should be
extracted into a helper function named normalizeToolArguments(input any) string;
implement normalizeToolArguments to preserve the current behavior: return "{}"
by default, handle nil, handle string inputs by trimming and attempting
json.Unmarshal then re-marshal (falling back to json.Marshal of the raw string),
handle non-string inputs by json.Marshal, and if still "{}", use
strings.TrimSpace(formatPromptCanonicalValue(input)) and try to json.Marshal
that value (or return the raw value) — then replace the inline logic in the
switch (case "tool") to call normalizeToolArguments(part.Input) and assign its
return to toolArguments.
bridges/ai/module_config.go (1)

43-63: Consider logging normalization failures for observability.

When json.Marshal or json.Unmarshal fails in the default case, returning nil silently may mask configuration issues. A debug-level log could help diagnose malformed agent definitions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/module_config.go` around lines 43 - 63, The function
normalizeMemorySearchConfig silently returns nil on json.Marshal/json.Unmarshal
errors which hides malformed configs; update normalizeMemorySearchConfig to log
debug/error messages when json.Marshal or json.Unmarshal fail (include the error
and the raw input) before returning nil, using the package's logger (or standard
logger) so failures in agents.MemorySearchConfig normalization are observable
and easier to debug.
bridges/ai/prompt_builder.go (2)

208-214: Error messages for audio/video could be more actionable.

The errors "audio/video attachments must be preprocessed into text before prompt assembly" indicate a programming error (caller didn't preprocess). Consider including guidance on what preprocessing is expected.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/prompt_builder.go` around lines 208 - 214, The error messages for
pendingTypeAudio and pendingTypeVideo are unhelpful; update the fmt.Errorf
messages in the switch handling (the case for pendingTypeAudio, pendingTypeVideo
in prompt_builder.go that returns PromptContext{}) to include actionable
guidance on what preprocessing is required (e.g., run a transcription step or
convert the attachment to text and populate the attachment text field or change
the pending type to a text variant) and include the attachment.mediaType or
attachment identifier for context (use opts.attachment.mediaType). Ensure the
new messages clearly tell callers to transcribe the media and attach the
resulting text before calling Assemble/PromptContext creation.

139-167: Downloading history images on every replay could be slow.

For each history candidate with generated images, this downloads and re-encodes images (line 148). With many conversations or large images, this could add significant latency. Consider caching the base64 data in the message metadata or turn data.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/prompt_builder.go` around lines 139 - 167, The current loop always
calls oc.downloadMediaBase64 for each candidate.meta.GeneratedFiles which
re-downloads and re-encodes images on every replay; change the logic in the
injectImages / candidate.meta.GeneratedFiles handling to first check for cached
base64 or cached MIME (e.g., a new GeneratedFiles[].ImageB64 or
GeneratedFiles[].CachedMime field) and only call oc.downloadMediaBase64 when
that cache is missing or expired, then store the returned b64 and actualMimeType
back into candidate.meta.GeneratedFiles for future replays; update the
PromptBlock construction to use the cached ImageB64/MimeType when present and
only fall back to oc.downloadMediaBase64 otherwise (consider adding a TTL or
size check to the cache entry to avoid stale/huge images).
bridges/ai/internal_dispatch.go (1)

75-78: Redundant nil check on oc.

The oc != nil check at line 76 is unnecessary since line 24 already returns early when oc == nil.

♻️ Suggested simplification
-	var cfg *Config
-	if oc != nil && oc.connector != nil {
-		cfg = &oc.connector.Config
+	var cfg *Config
+	if oc.connector != nil {
+		cfg = &oc.connector.Config
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/internal_dispatch.go` around lines 75 - 78, Redundant nil check:
remove the unnecessary "oc != nil" condition when setting cfg (currently: if oc
!= nil && oc.connector != nil { cfg = &oc.connector.Config }) because the
function already returns early when oc == nil; change the guard to check only
"oc.connector != nil" (or simply use oc.connector directly) so that cfg is set
using oc.connector.Config without the extra oc nil check, referencing the
variables oc, oc.connector, and cfg in internal_dispatch.go.
bridges/ai/login_loaders.go (1)

136-142: Redundant re-binding after publishOrReuseClient.

Lines 138-140 re-set UserLogin, ClientBase.SetUserLogin, and login.Client on chosen, but publishOrReuseClient already performs these assignments at lines 63-65 or 75-77. This is harmless but adds noise.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/login_loaders.go` around lines 136 - 142, The three assignments
after publishOrReuseClient are redundant because publishOrReuseClient already
sets chosen.UserLogin, calls chosen.ClientBase.SetUserLogin(login), and assigns
login.Client when it returns an existing or new client; remove the duplicate
lines that re-set chosen.UserLogin, chosen.ClientBase.SetUserLogin, and
login.Client in the block that checks chosen != nil, leaving the
chosen.scheduleBootstrap() call intact.
bridges/ai/login_state_db.go (1)

55-61: Variable copy shadows builtin.

The variable name copy at line 59 shadows Go's builtin copy function. While harmless here, consider renaming for clarity.

♻️ Suggested rename
 func cloneHeartbeatEvent(in *HeartbeatEventPayload) *HeartbeatEventPayload {
 	if in == nil {
 		return nil
 	}
-	copy := *in
-	return &copy
+	clone := *in
+	return &clone
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/login_state_db.go` around lines 55 - 61, The local variable named
"copy" in function cloneHeartbeatEvent shadows Go's builtin copy; rename it
(e.g., to "cloned" or "out") so it doesn't shadow the builtin: inside
cloneHeartbeatEvent (working with *HeartbeatEventPayload) change the variable
declaration "copy := *in" to a non-conflicting name and return its address
(e.g., "cloned := *in" then "return &cloned").
bridges/ai/image_generation_tool.go (2)

469-478: imageGenServiceConfig uses context.Background() instead of accepting context parameter.

The function calls loginConfigSnapshot(context.Background()) at line 474, which drops request cancellation signals. Consider accepting a ctx parameter and passing it through.

♻️ Suggested change
-func imageGenServiceConfig(btc *BridgeToolContext, service string) (string, ServiceConfig, bool) {
+func imageGenServiceConfig(ctx context.Context, btc *BridgeToolContext, service string) (string, ServiceConfig, bool) {
 	if btc == nil || btc.Client == nil || btc.Client.connector == nil || btc.Client.UserLogin == nil || btc.Client.UserLogin.Metadata == nil {
 		return "", ServiceConfig{}, false
 	}
 	provider := loginMetadata(btc.Client.UserLogin).Provider
-	loginCfg := btc.Client.loginConfigSnapshot(context.Background())
+	loginCfg := btc.Client.loginConfigSnapshot(ctx)
 	services := btc.Client.connector.resolveServiceConfig(provider, loginCfg)
 	cfg, ok := services[service]
 	return provider, cfg, ok
 }

Update call sites to pass context through.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/image_generation_tool.go` around lines 469 - 478,
imageGenServiceConfig currently calls loginConfigSnapshot with
context.Background(), losing cancellation; update its signature to accept a
context.Context (e.g., func imageGenServiceConfig(ctx context.Context, btc
*BridgeToolContext, service string) ...) and replace
loginConfigSnapshot(context.Background()) with loginConfigSnapshot(ctx), then
update all callers to pass through their context values so request cancellation
and deadlines propagate; reference the imageGenServiceConfig function,
loginConfigSnapshot method, and any call sites that invoke imageGenServiceConfig
to update them accordingly.

273-284: supportsGeminiImageGen always returns false.

Both the ProviderMagicProxy case and the default case return false. This appears intentional (Gemini image gen not yet supported), but the function could be simplified or documented.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/image_generation_tool.go` around lines 273 - 284, The
supportsGeminiImageGen function currently always returns false (both the
ProviderMagicProxy case and default return false); either simplify it to a
single explicit return false or implement the intended provider logic: locate
supportsGeminiImageGen and replace the switch with a provider check that returns
true for providers that support Gemini image gen (e.g., add a case that returns
true for the appropriate Provider constant) and false otherwise, ensuring you
still guard for nil btc/Client/UserLogin/Metadata as currently done.
bridges/ai/provisioning.go (1)

694-702: saveCfg closure shadows outer err variable.

The saveCfg closure at line 695-702 assigns to err (line 697), which is the function parameter from line 679. This works but could be clearer by declaring a local error variable in the closure.

♻️ Minor clarity improvement
 	saveCfg := func() error {
 		setLoginMCPServer(loginCfg, target.Name, cfg)
-		if err = client.replaceLoginConfig(ctx, loginCfg); err != nil {
-			return err
+		if saveErr := client.replaceLoginConfig(ctx, loginCfg); saveErr != nil {
+			return saveErr
 		}
 		client.invalidateMCPToolCache()
 		return nil
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/provisioning.go` around lines 694 - 702, The closure saveCfg
currently assigns to the outer function's err parameter, which can be confusing;
change the closure to use a local error variable instead (e.g., localErr or
cerr) so it doesn't shadow the outer err: call setLoginMCPServer(loginCfg,
target.Name, cfg) as before, then use a locally scoped error when invoking
client.replaceLoginConfig(ctx, loginCfg) and return that local error, then call
client.invalidateMCPToolCache() and return nil on success; this keeps the outer
err untouched and improves clarity around saveCfg's error handling.
bridges/ai/heartbeat_delivery_test.go (1)

90-113: Test overwrites portalsByKey after cacheHeartbeatTestPortals.

Lines 99-101 overwrite portalsByKey with only the default chat portal key, discarding the map set by cacheHeartbeatTestPortals at line 98. This appears intentional to test the default-chat fallback path, but the cacheHeartbeatTestPortals call becomes partially redundant.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/heartbeat_delivery_test.go` around lines 90 - 113, The test
TestResolveHeartbeatDeliveryTargetFallsBackToDefaultChat calls
cacheHeartbeatTestPortals and then overwrites the internal portalsByKey map with
setUnexportedField, making the cacheHeartbeatTestPortals call redundant; fix by
removing the cacheHeartbeatTestPortals(t, client, defaultPortal) call (or
instead merge the existing map with the single-entry map if you need both
entries) so that portalsByKey is only set once; update the test to either rely
solely on cacheHeartbeatTestPortals to populate portalsByKey or to set the
precise map via setUnexportedField (referencing
TestResolveHeartbeatDeliveryTargetFallsBackToDefaultChat,
cacheHeartbeatTestPortals, setUnexportedField, portalsByKey, and
defaultChatPortalKey).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@bridges/ai/chat.go`:
- Around line 559-576: The code currently calls oc.createChat(...) before
oc.resolveResponder(...), which can leave a created chat while returning an
error; fix by resolving the responder first (move the oc.resolveResponder(...)
call to before the createChat branch) so you only create the chat after
responder resolution, or if you prefer to keep creation first, catch errors from
oc.resolveResponder(...) and do not return an error—log a warning and populate a
fallback PortalMetadata/Responder with a default UserInfo so subsequent logic
can proceed using that default; reference the functions createChat and
resolveResponder and the types PortalMetadata, ResolvedTarget
(ResolvedTargetModel) when making the change.

In `@bridges/ai/handleai.go`:
- Around line 439-450: The code sets meta.TitleGenerated = true and persists it
via savePortal before calling materializePortalRoom, so if materializePortalRoom
fails the persisted flag prevents future regenerations; update the flow in the
portal title generation path (portalMeta, applyPortalRoomName, savePortal,
materializePortalRoom) so that TitleGenerated is only set and saved after
materializePortalRoom returns successfully, or if you prefer keep the current
save order then catch materializePortalRoom errors and roll back TitleGenerated
and persist that rollback via savePortal; ensure logging remains for failures.

In `@bridges/ai/handlematrix.go`:
- Around line 412-419: The call to oc.persistAIConversationMessage currently
only logs failures and continues, which allows downstream updates
(oc.UserLogin.Bridge.DB.Message.Update) and acking the edit even though
AI-history persistence failed; change the flow in the block that calls
oc.persistAIConversationMessage (and the analogous block at lines 423-438) so
that if persistAIConversationMessage returns an error you stop processing and
return that error immediately instead of only logging—i.e., propagate the error
back to the caller after logging (or replace the log-with-continue with a return
err) so edit.EditTarget is not updated or acknowledged when persistence fails.

In `@bridges/ai/login_config_db.go`:
- Around line 117-133: ensureLoginConfigLoaded currently returns the internal
pointer oc.loginConfig after releasing loginConfigMu, allowing callers to mutate
shared state without synchronization; modify ensureLoginConfigLoaded to return a
deep copy/clone of the aiLoginConfig (or explicitly document immutability) so
callers cannot mutate internal state—implement a clone method on aiLoginConfig
(e.g., aiLoginConfig.Clone()) and return oc.loginConfig.Clone() from
ensureLoginConfigLoaded (or update docs/comments near ensureLoginConfigLoaded
and the aiLoginConfig type to state callers must treat the returned value as
read-only).

In `@bridges/ai/message_helpers.go`:
- Around line 107-134: The code currently only updates when existing != nil &&
existing.Room == portal.PortalKey, which lets a room mismatch fall through to
db.Insert and create duplicate MXID rows; change the logic in the MXID handling
(around GetPartByMXID, existing, transport, portal.PortalKey) to: if existing !=
nil { copy the same fields from transport into existing (Room = transport.Room,
ID, PartID, SenderID, Timestamp, SendTxnID), reset existing.Metadata =
&MessageMetadata{}, and call db.Update(ctx, existing); } else { return
db.Insert(ctx, &transport) }. In short, remove the Room == portal.PortalKey
guard so any found existing row is updated rather than inserting a duplicate.

In `@bridges/ai/queue_runtime.go`:
- Around line 297-303: The buildPromptContextForPendingMessage error path only
notifies item.pending.Event and leaves item.pending.StatusEvents without
terminal status; update the error handling in the block after
buildPromptContextForPendingMessage to propagate the failure to all tracked
events by either iterating over item.pending.StatusEvents and calling
notifyMatrixSendFailure(ctx, item.pending.Portal, eachEvent, err) for each, or
by invoking queueStatusEvents/queueStatusEventsFailure helper that fans out the
terminal failure to all status events; preserve the existing calls to
removePendingAckReactions, releaseRoom, and processPendingQueue and ensure the
same error is passed through to each notification.

In `@README.md`:
- Around line 29-30: The Codex table row in README.md reads like a comma splice;
update the Codex row (the entry containing "`Codex`" and "`codex app-server`")
to use clearer grammar—e.g., change the clause after the pipe to "Provides a
local `codex app-server` runtime. Requires Codex to be installed" or "Provides a
local `codex app-server` runtime; requires Codex to be installed" so the
sentence is not comma-spliced and reads cleanly.

---

Outside diff comments:
In `@bridges/ai/handleai.go`:
- Around line 278-285: The code sets currentMeta.AutoGreetingSent = true and
persists it via oc.savePortal, but if oc.dispatchInternalMessage fails the flag
remains true; change the error path after oc.dispatchInternalMessage (the call
in the block using bgCtx, current, currentMeta, autoGreetingPrompt,
"auto-greeting") to reset currentMeta.AutoGreetingSent = false and call
oc.savePortal(bgCtx, current, "rollback auto greeting state") (log any save
error) before returning so the persisted state is rolled back and the room can
retry the auto-greeting.

In `@bridges/ai/handlematrix.go`:
- Around line 504-511: The code only redacts the Matrix event (via
redactEventViaPortal) but does not remove the stale assistant turn from the AI
history store, so regenerateFromEdit which uses getAIHistoryMessages can still
return the old response; after successful redaction (when assistantResponse !=
nil and assistantResponse.MXID != ""), also remove or mark the corresponding AI
history entry as deleted/evicted from the AI store (e.g., call the module method
that removes AI transcript entries or mark assistantResponse.ID as excluded) and
then call notifySessionMutation so subsequent getAIHistoryMessages calls will
not include the stale response; ensure the chosen removal API (e.g.,
oc.evictAIMessageFromStore / oc.deleteAIHistoryEntry) is used with the same
unique identifier used by getAIHistoryMessages.

In `@bridges/ai/integration_host.go`:
- Around line 56-64: The SavePortal method currently swallows errors by calling
h.client.savePortalQuiet and always returning nil; update
runtimeIntegrationHost.SavePortal to preserve the existing nil checks but call
and return the result of h.client.savePortal(ctx, portal, reason) (instead of
savePortalQuiet) so persistence failures propagate to callers; keep the early
returns when h or h.client or portal are nil.

In `@bridges/ai/provider_openai.go`:
- Around line 43-45: The docblock is stale: it references
NewOpenAIProviderWithBaseURL but the exported constructor is
NewOpenAIProviderWithUserID; update the top comments to accurately name and
describe the current constructors (e.g., mention NewOpenAIProviderWithUserID
and, if applicable, NewOpenAIProviderWithBaseURL only when that function exists)
so the comment matches the API; edit the comment lines near the OpenAI provider
constructors (references to NewOpenAIProviderWithBaseURL and
NewOpenAIProviderWithUserID) to use the correct function names and concise
descriptions.

---

Duplicate comments:
In `@bridges/ai/agentstore.go`:
- Around line 82-95: saveAgent and deleteAgent currently dereference
s.client.UserLogin without guarding, which can panic when the adapter is
nil/half-initialized; update AgentStoreAdapter.saveAgent and
AgentStoreAdapter.deleteAgent to check that s.client != nil and that
s.client.UserLogin is non-empty (or otherwise valid) before calling
saveCustomAgentForLogin or deleteCustomAgentForLogin, returning a descriptive
error if the login is missing; perform the check while holding s.mu (or acquire
mu before checking) to preserve concurrency guarantees and keep calls to
saveCustomAgentForLogin/deleteCustomAgentForLogin only when the login is
present.
- Around line 505-512: The Mutate closure assigns the raw room.Name into
chatInfo.Name without trimming, causing whitespace-only or mismatched stored
names; before calling b.client.applyPortalRoomName(ctx, portal, room.Name) and
before setting chatInfo.Name, trim whitespace (e.g. strings.TrimSpace) into a
local variable (or overwrite room.Name with the trimmed value), check for empty
after trimming, then use that trimmed value when calling applyPortalRoomName and
when assigning chatInfo.Name to ensure stored metadata and room state remain
consistent.

In `@bridges/ai/chat.go`:
- Around line 970-983: The code mutates portal fields (setPortalResolvedTarget,
pm.RuntimeModelOverride via ResolveAlias, portal.AvatarID/AvatarMXC) and then
calls oc.savePortalQuiet but ignores its error; change the flow so
oc.savePortalQuiet(ctx, portal, saveReason) returns an error which you check
immediately, and if non-nil you return that error to abort the create flow (do
not proceed to oc.ensureAgentGhostDisplayName). Ensure the caller receives the
propagated error instead of continuing with only in-memory changes so the room
creation fails on persistence failure.

In `@bridges/ai/login_config_db.go`:
- Around line 139-156: The updateLoginConfig function currently holds
oc.loginConfigMu (locked in updateLoginConfig) while calling saveAILoginConfig,
which can re-enter the same mutex via saveAILoginConfig when login.Client is
*AIClient; to fix, compute and apply the mutation under the lock (call
loadAILoginConfig and invoke fn(oc.loginConfig) while locked) but do NOT call
saveAILoginConfig while holding oc.loginConfigMu: record whether fn returned
true and copy or retain the pointer to the updated config, then unlock before
calling saveAILoginConfig(ctx, oc.UserLogin, oc.loginConfig). Ensure the
lock/unlock uses oc.loginConfigMu and that no other code path assumes
saveAILoginConfig runs under the same mutex.

In `@bridges/ai/login_loaders.go`:
- Around line 24-26: The code calls loginMetadata(existing.UserLogin) and
dereferences .Provider without checking the returned metadata; to fix, call
loginMetadata(existing.UserLogin), store its result in a variable (e.g., meta),
verify meta != nil (and optionally meta.Provider != "") before accessing
meta.Provider, and only then assign existingProvider =
strings.TrimSpace(meta.Provider); update the block that references
existing.UserLogin and existingProvider to use this nil-checked meta variable to
avoid a nil-pointer panic.
- Around line 89-102: The code may dereference nil meta when calling
oc.resolveProviderAPIKeyForConfig(meta.Provider, cfg); ensure meta is non-nil
before accessing meta.Provider by initializing a safe default: after calling
meta = loginMetadata(login) check if meta is still nil and if so create a
default login metadata (e.g., &loginMetadataType{Provider: ""} or equivalent) or
use a local provider variable derived via a nil-safe helper; update the block
around loginMetadata(login) and the key assignment so
resolveProviderAPIKeyForConfig receives a non-nil provider value, referencing
the symbols meta, loginMetadata, cfg, loadAILoginConfig and
resolveProviderAPIKeyForConfig to locate and modify the logic.

In `@bridges/ai/logout_cleanup.go`:
- Line 19: purgeLoginData currently logs aggregated delete errors but
immediately clears the in-memory login state and config, which can hide
failures; change purgeLoginData to return an error (aggregate any delete errors
you already join) and only clear the runtime login state/config after the
returned error is nil. Locate the similar cleanup block referenced at the other
cleanup section (the one that also logs joined delete errors and then resets
in-memory state) and apply the same pattern: return the aggregated error to the
caller instead of swallowing it, and move any code that drops in-memory login
state/config to run only when the error is nil.

In `@bridges/ai/queue_runtime.go`:
- Around line 165-183: The interrupt decision uses a stale roomBusy snapshot;
after attempting to acquire the room with oc.acquireRoom (i.e., when directRun
is false), re-check the current busy state instead of relying on the original
roomBusy: call oc.roomHasActiveRun(roomID) || oc.roomHasPendingQueueWork(roomID)
again and if queueSettings.Mode == airuntime.QueueModeInterrupt and the re-check
shows the room is busy, invoke oc.cancelRoomRun(roomID) and
oc.clearPendingQueue(ctx, roomID) and then attempt to acquire the room again
(retry oc.acquireRoom) so the interrupt path reliably cancels an active run
grabbed between the original check and the acquire.

---

Nitpick comments:
In `@bridges/ai/bootstrap_context.go`:
- Around line 17-19: The code returns nil on failure from
oc.textFSStoreForAgent(agentID) without any logging; update the failure path in
the function containing oc.textFSStoreForAgent(agentID) to log the error
(including agentID and err) before returning so initialization failures are
visible—use the package logger available in that scope (e.g., oc.logger.Errorf
or a standard log.Printf if no logger exists) and include a clear message like
"failed to initialize textFS store for agent" plus the error details.

In `@bridges/ai/bridge_db.go`:
- Around line 270-281: The check uses strings.TrimSpace(bridgeID) redundantly
because canonicalLoginBridgeID already trims; in function loginScopeForLogin
replace the TrimSpace call with a simple empty-string check (bridgeID == "") so
the conditional becomes if bridgeID == "" || loginID == "" { return nil },
keeping the rest of the logic and symbols (loginScope, bridgeDBFromLogin,
canonicalLoginBridgeID, canonicalLoginID) unchanged.

In `@bridges/ai/canonical_prompt_messages.go`:
- Around line 89-119: The tool-argument normalization block in the "tool" case
is complex and should be extracted into a helper function named
normalizeToolArguments(input any) string; implement normalizeToolArguments to
preserve the current behavior: return "{}" by default, handle nil, handle string
inputs by trimming and attempting json.Unmarshal then re-marshal (falling back
to json.Marshal of the raw string), handle non-string inputs by json.Marshal,
and if still "{}", use strings.TrimSpace(formatPromptCanonicalValue(input)) and
try to json.Marshal that value (or return the raw value) — then replace the
inline logic in the switch (case "tool") to call
normalizeToolArguments(part.Input) and assign its return to toolArguments.

In `@bridges/ai/heartbeat_delivery_test.go`:
- Around line 90-113: The test
TestResolveHeartbeatDeliveryTargetFallsBackToDefaultChat calls
cacheHeartbeatTestPortals and then overwrites the internal portalsByKey map with
setUnexportedField, making the cacheHeartbeatTestPortals call redundant; fix by
removing the cacheHeartbeatTestPortals(t, client, defaultPortal) call (or
instead merge the existing map with the single-entry map if you need both
entries) so that portalsByKey is only set once; update the test to either rely
solely on cacheHeartbeatTestPortals to populate portalsByKey or to set the
precise map via setUnexportedField (referencing
TestResolveHeartbeatDeliveryTargetFallsBackToDefaultChat,
cacheHeartbeatTestPortals, setUnexportedField, portalsByKey, and
defaultChatPortalKey).

In `@bridges/ai/image_generation_tool.go`:
- Around line 469-478: imageGenServiceConfig currently calls loginConfigSnapshot
with context.Background(), losing cancellation; update its signature to accept a
context.Context (e.g., func imageGenServiceConfig(ctx context.Context, btc
*BridgeToolContext, service string) ...) and replace
loginConfigSnapshot(context.Background()) with loginConfigSnapshot(ctx), then
update all callers to pass through their context values so request cancellation
and deadlines propagate; reference the imageGenServiceConfig function,
loginConfigSnapshot method, and any call sites that invoke imageGenServiceConfig
to update them accordingly.
- Around line 273-284: The supportsGeminiImageGen function currently always
returns false (both the ProviderMagicProxy case and default return false);
either simplify it to a single explicit return false or implement the intended
provider logic: locate supportsGeminiImageGen and replace the switch with a
provider check that returns true for providers that support Gemini image gen
(e.g., add a case that returns true for the appropriate Provider constant) and
false otherwise, ensuring you still guard for nil btc/Client/UserLogin/Metadata
as currently done.

In `@bridges/ai/internal_dispatch.go`:
- Around line 75-78: Redundant nil check: remove the unnecessary "oc != nil"
condition when setting cfg (currently: if oc != nil && oc.connector != nil { cfg
= &oc.connector.Config }) because the function already returns early when oc ==
nil; change the guard to check only "oc.connector != nil" (or simply use
oc.connector directly) so that cfg is set using oc.connector.Config without the
extra oc nil check, referencing the variables oc, oc.connector, and cfg in
internal_dispatch.go.

In `@bridges/ai/login_loaders.go`:
- Around line 136-142: The three assignments after publishOrReuseClient are
redundant because publishOrReuseClient already sets chosen.UserLogin, calls
chosen.ClientBase.SetUserLogin(login), and assigns login.Client when it returns
an existing or new client; remove the duplicate lines that re-set
chosen.UserLogin, chosen.ClientBase.SetUserLogin, and login.Client in the block
that checks chosen != nil, leaving the chosen.scheduleBootstrap() call intact.

In `@bridges/ai/login_state_db.go`:
- Around line 55-61: The local variable named "copy" in function
cloneHeartbeatEvent shadows Go's builtin copy; rename it (e.g., to "cloned" or
"out") so it doesn't shadow the builtin: inside cloneHeartbeatEvent (working
with *HeartbeatEventPayload) change the variable declaration "copy := *in" to a
non-conflicting name and return its address (e.g., "cloned := *in" then "return
&cloned").

In `@bridges/ai/module_config.go`:
- Around line 43-63: The function normalizeMemorySearchConfig silently returns
nil on json.Marshal/json.Unmarshal errors which hides malformed configs; update
normalizeMemorySearchConfig to log debug/error messages when json.Marshal or
json.Unmarshal fail (include the error and the raw input) before returning nil,
using the package's logger (or standard logger) so failures in
agents.MemorySearchConfig normalization are observable and easier to debug.

In `@bridges/ai/prompt_builder.go`:
- Around line 208-214: The error messages for pendingTypeAudio and
pendingTypeVideo are unhelpful; update the fmt.Errorf messages in the switch
handling (the case for pendingTypeAudio, pendingTypeVideo in prompt_builder.go
that returns PromptContext{}) to include actionable guidance on what
preprocessing is required (e.g., run a transcription step or convert the
attachment to text and populate the attachment text field or change the pending
type to a text variant) and include the attachment.mediaType or attachment
identifier for context (use opts.attachment.mediaType). Ensure the new messages
clearly tell callers to transcribe the media and attach the resulting text
before calling Assemble/PromptContext creation.
- Around line 139-167: The current loop always calls oc.downloadMediaBase64 for
each candidate.meta.GeneratedFiles which re-downloads and re-encodes images on
every replay; change the logic in the injectImages /
candidate.meta.GeneratedFiles handling to first check for cached base64 or
cached MIME (e.g., a new GeneratedFiles[].ImageB64 or
GeneratedFiles[].CachedMime field) and only call oc.downloadMediaBase64 when
that cache is missing or expired, then store the returned b64 and actualMimeType
back into candidate.meta.GeneratedFiles for future replays; update the
PromptBlock construction to use the cached ImageB64/MimeType when present and
only fall back to oc.downloadMediaBase64 otherwise (consider adding a TTL or
size check to the cache entry to avoid stale/huge images).

In `@bridges/ai/provisioning.go`:
- Around line 694-702: The closure saveCfg currently assigns to the outer
function's err parameter, which can be confusing; change the closure to use a
local error variable instead (e.g., localErr or cerr) so it doesn't shadow the
outer err: call setLoginMCPServer(loginCfg, target.Name, cfg) as before, then
use a locally scoped error when invoking client.replaceLoginConfig(ctx,
loginCfg) and return that local error, then call client.invalidateMCPToolCache()
and return nil on success; this keeps the outer err untouched and improves
clarity around saveCfg's error handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

Comment thread bridges/ai/chat.go
Comment on lines +559 to +576
var chatResp *bridgev2.CreateChatResponse
if createChat {
oc.loggerForContext(ctx).Info().Str("model", modelID).Msg("Creating new chat")
chatResp, err = oc.createChat(ctx, chatCreateParams{ModelID: modelID})
if err != nil {
return nil, fmt.Errorf("failed to create chat: %w", err)
}
}

responder, err := oc.resolveResponder(ctx, &PortalMetadata{
ResolvedTarget: &ResolvedTarget{
Kind: ResolvedTargetModel,
ModelID: modelID,
},
}, ResponderResolveOptions{})
if err != nil {
return nil, fmt.Errorf("failed to get ghost: %w", err)
return nil, fmt.Errorf("failed to resolve model responder: %w", err)
}
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 | 🟠 Major

Avoid failing after the model chat is already created.

In the model path, createChat runs before resolveResponder. If responder resolution fails at Lines 574-576, the caller gets an error even though the room may already exist. Resolve first, or degrade to default UserInfo instead of returning an error after the side effect.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/chat.go` around lines 559 - 576, The code currently calls
oc.createChat(...) before oc.resolveResponder(...), which can leave a created
chat while returning an error; fix by resolving the responder first (move the
oc.resolveResponder(...) call to before the createChat branch) so you only
create the chat after responder resolution, or if you prefer to keep creation
first, catch errors from oc.resolveResponder(...) and do not return an error—log
a warning and populate a fallback PortalMetadata/Responder with a default
UserInfo so subsequent logic can proceed using that default; reference the
functions createChat and resolveResponder and the types PortalMetadata,
ResolvedTarget (ResolvedTargetModel) when making the change.

Comment thread bridges/ai/handleai.go
Comment on lines +439 to 450
meta := portalMeta(portal)
if meta != nil {
meta.TitleGenerated = true
}
oc.applyPortalRoomName(bgCtx, portal, title)
if err := oc.savePortal(bgCtx, portal, "room title"); err != nil {
oc.loggerForContext(ctx).Warn().Err(err).Msg("Failed to persist generated room title")
return
}
if err := oc.materializePortalRoom(bgCtx, portal, oc.chatInfoFromPortal(bgCtx, portal), portalRoomMaterializeOptions{}); err != nil {
oc.loggerForContext(ctx).Warn().Err(err).Msg("Failed to sync generated room title to Matrix")
}
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 | 🟠 Major

Don't finalize the generated title before Matrix sync succeeds.

TitleGenerated is persisted before materializePortalRoom runs. If the sync fails at Line 448, later replies will skip regeneration even though the Matrix room still has the old title. Persist the flag only after a successful sync, or roll it back on failure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/handleai.go` around lines 439 - 450, The code sets
meta.TitleGenerated = true and persists it via savePortal before calling
materializePortalRoom, so if materializePortalRoom fails the persisted flag
prevents future regenerations; update the flow in the portal title generation
path (portalMeta, applyPortalRoomName, savePortal, materializePortalRoom) so
that TitleGenerated is only set and saved after materializePortalRoom returns
successfully, or if you prefer keep the current save order then catch
materializePortalRoom errors and roll back TitleGenerated and persist that
rollback via savePortal; ensure logging remains for failures.

Comment on lines +412 to +419
if err := oc.persistAIConversationMessage(ctx, portal, transcriptMsg); err != nil {
oc.loggerForContext(ctx).Warn().Err(err).Msg("Failed to persist edited conversation turn")
}
if edit.EditTarget != nil {
edit.EditTarget.Metadata = cloneMessageMetadata(transcriptMeta)
if oc.UserLogin != nil && oc.UserLogin.Bridge != nil && oc.UserLogin.Bridge.DB != nil && oc.UserLogin.Bridge.DB.Message != nil {
if err := oc.UserLogin.Bridge.DB.Message.Update(ctx, edit.EditTarget); err != nil {
oc.loggerForContext(ctx).Warn().Err(err).Msg("Failed to update bridge message metadata after edit")
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 | 🟠 Major

Stop the edit flow if AI-history persistence fails.

After Line 412 logs the error, the code still updates the bridge row and may regenerate from the old AI history. That can acknowledge the edit in Matrix while later context generation still sees the pre-edit turn. Return the persistence error here instead of continuing.

Suggested fix
-	if err := oc.persistAIConversationMessage(ctx, portal, transcriptMsg); err != nil {
-		oc.loggerForContext(ctx).Warn().Err(err).Msg("Failed to persist edited conversation turn")
-	}
+	if err := oc.persistAIConversationMessage(ctx, portal, transcriptMsg); err != nil {
+		oc.loggerForContext(ctx).Warn().Err(err).Msg("Failed to persist edited conversation turn")
+		return err
+	}

Also applies to: 423-438

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/handlematrix.go` around lines 412 - 419, The call to
oc.persistAIConversationMessage currently only logs failures and continues,
which allows downstream updates (oc.UserLogin.Bridge.DB.Message.Update) and
acking the edit even though AI-history persistence failed; change the flow in
the block that calls oc.persistAIConversationMessage (and the analogous block at
lines 423-438) so that if persistAIConversationMessage returns an error you stop
processing and return that error immediately instead of only logging—i.e.,
propagate the error back to the caller after logging (or replace the
log-with-continue with a return err) so edit.EditTarget is not updated or
acknowledged when persistence fails.

Comment on lines +117 to +133
func (oc *AIClient) ensureLoginConfigLoaded(ctx context.Context) *aiLoginConfig {
if oc == nil {
return &aiLoginConfig{}
}
oc.loginConfigMu.Lock()
defer oc.loginConfigMu.Unlock()
if oc.loginConfig != nil {
return oc.loginConfig
}
cfg, err := loadAILoginConfig(ctx, oc.UserLogin)
if err != nil {
oc.loggerForContext(ctx).Warn().Err(err).Msg("Failed to load AI login config")
cfg = &aiLoginConfig{}
}
oc.loginConfig = cfg
return oc.loginConfig
}
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

ensureLoginConfigLoaded returns internal pointer while holding lock, allowing mutation without synchronization.

The function returns oc.loginConfig directly (line 132) after releasing the mutex via defer. Callers can mutate the returned config without synchronization. Consider returning a clone or documenting that callers must not mutate the result.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/login_config_db.go` around lines 117 - 133,
ensureLoginConfigLoaded currently returns the internal pointer oc.loginConfig
after releasing loginConfigMu, allowing callers to mutate shared state without
synchronization; modify ensureLoginConfigLoaded to return a deep copy/clone of
the aiLoginConfig (or explicitly document immutability) so callers cannot mutate
internal state—implement a clone method on aiLoginConfig (e.g.,
aiLoginConfig.Clone()) and return oc.loginConfig.Clone() from
ensureLoginConfigLoaded (or update docs/comments near ensureLoginConfigLoaded
and the aiLoginConfig type to state callers must treat the returned value as
read-only).

Comment on lines +107 to +134
if transport.MXID != "" {
existing, err := db.GetPartByMXID(ctx, transport.MXID)
if err != nil {
return err
}
if existing != nil && existing.Room == portal.PortalKey {
existing.Room = transport.Room
if transport.ID != "" {
existing.ID = transport.ID
}
if transport.PartID != "" {
existing.PartID = transport.PartID
}
if transport.SenderID != "" {
existing.SenderID = transport.SenderID
}
if !transport.Timestamp.IsZero() {
existing.Timestamp = transport.Timestamp
}
if transport.SendTxnID != "" {
existing.SendTxnID = transport.SendTxnID
}
existing.Metadata = &MessageMetadata{}
return db.Update(ctx, existing)
}
}

return db.Insert(ctx, &transport)
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 | 🟠 Major

Update the found MXID row instead of inserting a second one.

Once GetPartByMXID returns existing, this helper has already found the transport row to upsert. The existing.Room == portal.PortalKey guard makes a room mismatch fall through to Insert, which can either violate a unique-MXID constraint or leave two rows for the same Matrix event.

Suggested fix
 	if transport.MXID != "" {
 		existing, err := db.GetPartByMXID(ctx, transport.MXID)
 		if err != nil {
 			return err
 		}
-		if existing != nil && existing.Room == portal.PortalKey {
+		if existing != nil {
 			existing.Room = transport.Room
 			if transport.ID != "" {
 				existing.ID = transport.ID
 			}
 			if transport.PartID != "" {
 				existing.PartID = transport.PartID
 			}
 			if transport.SenderID != "" {
 				existing.SenderID = transport.SenderID
 			}
 			if !transport.Timestamp.IsZero() {
 				existing.Timestamp = transport.Timestamp
 			}
 			if transport.SendTxnID != "" {
 				existing.SendTxnID = transport.SendTxnID
 			}
 			existing.Metadata = &MessageMetadata{}
 			return db.Update(ctx, existing)
 		}
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if transport.MXID != "" {
existing, err := db.GetPartByMXID(ctx, transport.MXID)
if err != nil {
return err
}
if existing != nil && existing.Room == portal.PortalKey {
existing.Room = transport.Room
if transport.ID != "" {
existing.ID = transport.ID
}
if transport.PartID != "" {
existing.PartID = transport.PartID
}
if transport.SenderID != "" {
existing.SenderID = transport.SenderID
}
if !transport.Timestamp.IsZero() {
existing.Timestamp = transport.Timestamp
}
if transport.SendTxnID != "" {
existing.SendTxnID = transport.SendTxnID
}
existing.Metadata = &MessageMetadata{}
return db.Update(ctx, existing)
}
}
return db.Insert(ctx, &transport)
if transport.MXID != "" {
existing, err := db.GetPartByMXID(ctx, transport.MXID)
if err != nil {
return err
}
if existing != nil {
existing.Room = transport.Room
if transport.ID != "" {
existing.ID = transport.ID
}
if transport.PartID != "" {
existing.PartID = transport.PartID
}
if transport.SenderID != "" {
existing.SenderID = transport.SenderID
}
if !transport.Timestamp.IsZero() {
existing.Timestamp = transport.Timestamp
}
if transport.SendTxnID != "" {
existing.SendTxnID = transport.SendTxnID
}
existing.Metadata = &MessageMetadata{}
return db.Update(ctx, existing)
}
}
return db.Insert(ctx, &transport)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/message_helpers.go` around lines 107 - 134, The code currently
only updates when existing != nil && existing.Room == portal.PortalKey, which
lets a room mismatch fall through to db.Insert and create duplicate MXID rows;
change the logic in the MXID handling (around GetPartByMXID, existing,
transport, portal.PortalKey) to: if existing != nil { copy the same fields from
transport into existing (Room = transport.Room, ID, PartID, SenderID, Timestamp,
SendTxnID), reset existing.Metadata = &MessageMetadata{}, and call
db.Update(ctx, existing); } else { return db.Insert(ctx, &transport) }. In
short, remove the Room == portal.PortalKey guard so any found existing row is
updated rather than inserting a duplicate.

Comment on lines +297 to +303
promptContext, err := oc.buildPromptContextForPendingMessage(ctx, item.pending, prompt)
if err != nil {
oc.loggerForContext(ctx).Err(err).Msg("Failed to build prompt for pending queue item")
oc.notifyMatrixSendFailure(ctx, item.pending.Portal, item.pending.Event, err)
oc.removePendingAckReactions(oc.backgroundContext(ctx), item.pending.Portal, item.pending)
oc.releaseRoom(roomID)
oc.processPendingQueue(oc.backgroundContext(ctx), roomID)
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

Propagate queue-build failures to every tracked status event.

This path only calls notifyMatrixSendFailure for item.pending.Event. Any extra events in item.pending.StatusEvents never get a terminal status here, even though the other queue paths fan out via queueStatusEvents(...).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/queue_runtime.go` around lines 297 - 303, The
buildPromptContextForPendingMessage error path only notifies item.pending.Event
and leaves item.pending.StatusEvents without terminal status; update the error
handling in the block after buildPromptContextForPendingMessage to propagate the
failure to all tracked events by either iterating over item.pending.StatusEvents
and calling notifyMatrixSendFailure(ctx, item.pending.Portal, eachEvent, err)
for each, or by invoking queueStatusEvents/queueStatusEventsFailure helper that
fans out the terminal failure to all status events; preserve the existing calls
to removePendingAckReactions, releaseRoom, and processPendingQueue and ensure
the same error is passed through to each notification.

Comment thread README.md
Comment on lines +29 to +30
| [`AI Chats`](./bridges/ai/README.md) | Talk to any model on Beeper AI |
| [`Codex`](./bridges/codex/README.md) | A local `codex app-server` runtime, requires Codex to be installed |
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

Fix the Codex row sentence grammar for readability.

Line 30 reads like a comma splice and is slightly awkward in user-facing docs.

✏️ Suggested doc wording
-| [`Codex`](./bridges/codex/README.md) | A local `codex app-server` runtime, requires Codex to be installed |
+| [`Codex`](./bridges/codex/README.md) | A local `codex app-server` runtime; requires Codex to be installed |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| [`AI Chats`](./bridges/ai/README.md) | Talk to any model on Beeper AI |
| [`Codex`](./bridges/codex/README.md) | A local `codex app-server` runtime, requires Codex to be installed |
| [`AI Chats`](./bridges/ai/README.md) | Talk to any model on Beeper AI |
| [`Codex`](./bridges/codex/README.md) | A local `codex app-server` runtime; requires Codex to be installed |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 29 - 30, The Codex table row in README.md reads like
a comma splice; update the Codex row (the entry containing "`Codex`" and "`codex
app-server`") to use clearer grammar—e.g., change the clause after the pipe to
"Provides a local `codex app-server` runtime. Requires Codex to be installed" or
"Provides a local `codex app-server` runtime; requires Codex to be installed" so
the sentence is not comma-spliced and reads cleanly.

batuhan pushed a commit that referenced this pull request Apr 15, 2026
- 20+ tools the agent has access to (image gen, TTS, beeper_feedback,
  desktop control, cross-agent messaging, cron scheduling...)
- 16 hidden subsystems (link previews, citations, gravatar, compaction,
  heartbeats, workspace templates, boss agent, typing indicators...)
- The graveyard: pkg/connector monolith, direct Anthropic/Gemini/Beeper
  providers, approval_flow.go (1,619 lines), heartbeat delivery, ~20
  abandoned PRs. PR #103 branch name: batuhan/sins.

https://claude.ai/code/session_013UeU7h7ae2RNjfRgGfS2Wg
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant