feat: clean up AI booboos, fix bridgev2 usage#103
Conversation
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.
📝 WalkthroughSummary by CodeRabbitRelease Notes
WalkthroughThis pull request undertakes a comprehensive migration from the Changes
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
🧪 Generate unit tests (beta)
|
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.
There was a problem hiding this comment.
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 | 🟠 MajorRemove dead code
buildCommandDescriptionContentand its test.The
BroadcastCommandDescriptionsmethod no longer uses this function—it now directly constructssdk.Commandobjects instead. The only remaining usage is inevents_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 | 🟠 MajorAvoid caching conversation state under the empty key.
When
portal.Portal == nil,conversationStateKeyreturns"", butgetandsetstill read/writerooms[""]. 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 | 🔴 CriticalAdd a legacy-state fallback before switching reads to DB-only storage.
loadConversationStatenow consults only the cache andsdk_conversation_statetable. 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.Metadataremains 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 | 🟡 MinorAssert the rest of the new v1 tables here too.
pkg/aidb/001-init.sqlnow createsaichats_internal_messages,aichats_login_state, andaichats_tool_approval_rulesas part of the fresh schema, but this test doesn't verify them. If upgrade stops creating any of those tables,TestUpgradeV1Freshwill 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 | 🔴 CriticalRestore the nil receiver guard in
getIntent.Line 51 now dereferences
cunconditionally. A nil*Conversationwill panic here before returning an error, and that propagates intoSendMedia,SendNotice,sendMessageContent, andSetTyping.🐛 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 | 🟠 MajorAvoid returning an error after a successful
NewLoginwithout rollback.
ol.User.NewLogin(...)has already created the login beforesaveAIUserLogin(...)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 | 🟠 MajorDon'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 | 🟠 MajorKeep 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 | 🟠 MajorSave the welcome-room state before creating the Matrix room.
EnsurePortalLifecycle(...)can succeed beforesaveCodexPortalState(...)runs. If the save fails afterward, the room already exists but there is no persistedcodexPortalStateto 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
AIdefinition 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 dynamicbinaryName.The comments on lines 26 and 35 hardcode "sdk" in the path description, but the actual code uses the
binaryNamevariable. IfbinaryNamechanges 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 insessionIDs().Go map iteration order is non-deterministic; sorting
outbefore 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 theemojirequirement in the schema, not just the description.Line 144 now says
emojiis required whenremove:true, but the schema still accepts a reaction-removal payload without it. If anything downstream relies on schema validation, malformedreactrequests will still get through. Consider adding anif/thenoroneOfrule for thereactaction.🤖 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 throughportalMeta()to propagate cancellation for DB-backed portal state loads.Line 171 uses
context.Background()when callingloadPortalStateIntoMetadata(), preventing request cancellation/timeouts from propagating to the database operation. SinceportalMeta()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
aiSessionsTablevariable 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 forpurgeLoginIntegrationsand again here at line 72 forclearLoginState. 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.LoggerThen 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 overpanicin helper.Optional: pass
*testing.TintonewTestConversationand uset.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.contentHashand!force, the code saves the session state (line 64-66) even though nothing has changed. This appears redundant sincestatehasn'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 EXISTSwhich is safe for initial creation. SinceaiLoginConfigTableappears 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.
ensureOpenClawPortalStateTableexecutesCREATE TABLE IF NOT EXISTSon every load/save operation. While SQLite/PostgreSQL handle this idempotently, it adds unnecessary overhead.Consider using a
sync.Onceor 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.excludeMessageIDfilter 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.gobridges/ai/abort_helpers_test.gobridges/ai/bridge_info.gobridges/ai/client_capabilities_test.gobridges/ai/command_registry.gobridges/ai/constructors_test.gocmd/agentremote/run_bridge.gobridges/ai/agentstore.gobridges/dummybridge/runtime_test.gosdk/connector_builder_test.gobridges/ai/bridge_db.gobridges/ai/login_loaders.gobridges/codex/connector.gobridges/codex/constructors.gobridges/ai/commands.gobridges/codex/runtime_helpers.gobridges/ai/connector.gosdk/connector_builder.gobridges/ai/constructors.gobridges/ai/sdk_agent_catalog.gobridges/dummybridge/connector.gobridges/ai/handleai.gobridges/opencode/connector.gobridges/openclaw/connector.gocmd/internal/bridgeentry/bridgeentry.gosdk/connector.gosdk/types.gobridges/dummybridge/runtime.gobridges/ai/client.go
There was a problem hiding this comment.
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 | 🟠 MajorGuard
aidb.EnsureSchemawhen the bridge DB is unavailable.
InitConnectorconditionally initializescc.dbonly whenbridge,bridge.DB, andbridge.DB.Databaseare all non-nil. However,StartConnectorunconditionally passes the result ofcc.bridgeDB()toaidb.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 | 🟠 MajorTreat an unset
Agentsflag as enabled.This helper returns
falsewhencfg.Agentsis nil, butshouldEnsureDefaultChatstill 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 | 🟠 MajorReturn the bridge ghost ID for agent contacts.
agentContactResponsenow exposesnetworkid.UserID(agent.ID), but the rest of the bridge consistently usesoc.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
cloneUserLoginMetadatanow 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 | 🟠 MajorPersist
WelcomeSentbefore 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 | 🟠 MajorPersist
AutoGreetingSentbefore 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 | 🟠 MajorSync the generated title to Matrix after saving it.
Lines 461-463 only update
portal.Name/NameSetand 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 | 🟡 MinorRequire
remove:truebefore routing to reaction removal.Line 396 still treats an empty
emojias 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 | 🟠 MajorCap
SessionTranscripthistory fetches.Line 242 counts the full portal history, and Line 250 immediately asks
getAIHistoryMessagesfor 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 onbtc.Client.UserLogin.Metadata.Line 256 checks
btc.Client.UserLogin.Metadata == nil, but after migrating toeffectiveLoginMetadata, this check is no longer meaningful sinceeffectiveLoginMetadatadoesn't directly useUserLogin.Metadata. The subsequent nil check onloginMeta(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 redundantMetadatanil check pattern inresolveOpenRouterImageGenEndpoint.The
btc.Client.UserLogin.Metadata == nilcheck 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 redundantMetadatanil check pattern.Similar to
supportsOpenAIImageGen, thebtc.Client.UserLogin.Metadata == nilcheck at line 281 is no longer necessary after the migration toeffectiveLoginMetadata.🤖 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 redundantMetadatanil check pattern inbuildOpenAIImagesBaseURL.The
btc.Client.UserLogin.Metadata == nilcheck 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 redundantMetadatanil check pattern inbuildGeminiBaseURL.The
btc.Client.UserLogin.Metadata == nilcheck 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:bestEffortExecis redundant and should be removed.The function at lines 107–109 is a direct pass-through to
execDelete. Update the 6 callers acrossdelete_chat.go,internal_prompt_db.go, andlogin_state_db.goto callexecDeletedirectly 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()andensureLoginCredentials()(which both return nil or do nothing for unsupported types). While all current callers pass*aiLoginConfigfromloginConfigSnapshot(), 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 ofcontext.Background().Using
context.Background()here loses any tracing, logging context, or cancellation signals from the caller. IfeffectiveLoginMetadataperforms any I/O or logging that should respect the request context, consider passing the actualctxparameter (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
📒 Files selected for processing (70)
.github/workflows/docker-agentremote.yml.github/workflows/go.yml.github/workflows/publish-release.ymlREADME.mdbridges/ai/agentstore.gobridges/ai/bridge_db.gobridges/ai/bridge_info.gobridges/ai/bridge_info_test.gobridges/ai/broken_login_client.gobridges/ai/chat.gobridges/ai/client.gobridges/ai/commands.gobridges/ai/constructors.gobridges/ai/custom_agents_db.gobridges/ai/delete_chat.gobridges/ai/desktop_api_sessions.gobridges/ai/gravatar.gobridges/ai/handleai.gobridges/ai/handlematrix.gobridges/ai/heartbeat_events.gobridges/ai/heartbeat_session.gobridges/ai/image_generation_tool.gobridges/ai/image_understanding.gobridges/ai/integration_host.gobridges/ai/integrations_config.gobridges/ai/login.gobridges/ai/login_config_db.gobridges/ai/login_loaders.gobridges/ai/login_state_db.gobridges/ai/logout_cleanup.gobridges/ai/mcp_client.gobridges/ai/mcp_helpers.gobridges/ai/mcp_servers.gobridges/ai/media_understanding_cli.gobridges/ai/media_understanding_runner.gobridges/ai/message_state_db.gobridges/ai/metadata.gobridges/ai/model_catalog.gobridges/ai/prompt_builder.gobridges/ai/provisioning.gobridges/ai/response_finalization.gobridges/ai/scheduler_heartbeat_test.gobridges/ai/session_store.gobridges/ai/sessions_tools.gobridges/ai/status_text.gobridges/ai/subagent_announce.gobridges/ai/timezone.gobridges/ai/token_resolver.gobridges/ai/tool_approvals_policy.gobridges/ai/tool_configured.gobridges/ai/tool_policy_chain.gobridges/ai/tool_policy_chain_test.gobridges/ai/tools.gobridges/ai/tools_beeper_feedback.gobridges/codex/config.gobridges/codex/connector.gobridges/codex/connector_test.gobridges/codex/constructors.gobridges/openclaw/README.mdbridges/openclaw/gateway_client.gobridges/openclaw/gateway_client_test.gocmd/agentremote/commands.godocker/agentremote/README.mddocs/bridge-orchestrator.mdpkg/agents/prompt.gopkg/aidb/001-init.sqlpkg/aidb/db.gopkg/aidb/db_test.gosdk/room_features.gotools/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.gobridges/ai/bridge_info_test.gobridges/ai/media_understanding_runner.gobridges/ai/token_resolver.gobridges/ai/bridge_db.gobridges/ai/login.gobridges/codex/constructors.gobridges/codex/connector.gobridges/ai/handleai.gobridges/ai/chat.gobridges/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
agentremoteSDK 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.
TestApplyAIChatsBridgeInfoand the updatedapplyAIChatsBridgeInfo(...)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
aiBridgeProtocolIDcorrectly 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 fordoctorcommand 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
binaryNamerewrite 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
binaryNamethrough compdef/function naming and top-level command descriptions.
676-736: Fish completion migration tobinaryNameis 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=ailogger scoping change looks good.This aligns child-DB logs with the new AI schema ownership.
77-82:bridgeDBFromPortalnil 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 insessionStoreRefis 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.EnsureSchemamigration 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;loginStateSnapshotis guaranteed non-nil.The implementation always returns a non-nil
*loginRuntimeState:
ensureLoginStateLoadedhandles load errors by allocating&loginRuntimeState{}(line 239) and never returns nilcloneLoginRuntimeStatechecks for nil input and returns an allocated struct in all cases (lines 50–51)- Therefore,
loginState.ModelCacheat 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)tobtc.Client.effectiveLoginMetadata(context.Background())is consistent with the broader refactor. The nil check onloginMetais preserved.bridges/ai/mcp_helpers.go (1)
68-80: LGTM!The refactor to accept
owner anywithensureLoginCredentials(owner)provides flexibility for both*UserLoginMetadataand*aiLoginConfigtypes.bridges/ai/gravatar.go (1)
184-189: LGTM!The migration to
loginStateSnapshotis correct with proper nil checks for bothloginStateand nestedGravatar/Primaryfields.bridges/ai/sessions_tools.go (3)
118-127: LGTM!The migration to
getAIHistoryMessagesmaintains the existing error handling (only using messages on success) and properly limits the result set.
270-286: LGTM!Consistent migration to
getAIHistoryMessageswith proper error handling that returns an error result on failure.
572-581: LGTM!The migration in
lastMessageTimestampcorrectly 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
getAIHistoryMessagesinreadLatestAssistantReplypreserves the error handling and message filtering logic.
105-138: LGTM!The migration in
buildSubagentStatsLineappropriately 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
resolveProviderAPIKeyForConfigmaintains 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.limitis applied after merging internal prompt rows intocandidates. 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:
- Tracking internal rows separately and not counting them toward
hr.limit- Using a counter of non-internal entries for image injection rather than the loop index
bridges/ai/agentstore.go (2)
547-547:CreateRoomreturns success even when portal persistence fails silently.
savePortalQuietlogs 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
LoadAgentsfunction properly:
- Accepts context parameter for DB operations
- Loads custom agents via
listCustomAgentsForLogin- Gates presets based on provider
- 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
UserLoginmetadata 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-*toaichats-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.EventTimingtosdk.EventTimingand usessdk.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
BridgeIDandLoginIDfromloginDBContext(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
heartbeatLoginKeyfunction properly creates a composite key fromBridgeID|LoginIDwith appropriate nil checks. This is more robust than usingUserLoginIDalone.
117-126: LGTM - Proper state update with deduplication.The persistence logic correctly:
- Compares the new event against the previous state
- Only persists if there's a meaningful change
- Returns
falseto skip unnecessary writes when events are identicalbridges/ai/login.go (3)
33-36: LGTM - SDK error helper migration.The login error definitions properly migrate to
sdk.NewLoginRespErrorwith appropriate status codes and error codes.
213-257: LGTM - Robust login config handling with proper persistence.The login flow now:
- Loads existing config for relogin scenarios (line 219)
- Handles credentials via
aiLoginConfig(lines 239-242)- Validates using the new
loginMetadataViewpattern (line 243)- Properly persists config with error handling (lines 255-257)
Good addition of the
SAVE_LOGIN_FAILEDerror case.
322-337: The implementation correctly handles credential resolution for new logins.The
loginMetadataViewfunction properly reads credentials fromcfg.Credentialsand clones them into the temporary metadata object, which is then used in the API key resolution. The null-safety check forcfgensures 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
EnsureSchemafunction 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,
EnsureSchemaonly applies001-init.sqlwithout 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.
There was a problem hiding this comment.
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 | 🟡 MinorFix stale constructor doc comments.
Line 43 and Line 44 still reference
NewOpenAIProviderWithBaseURL, but this file now exposesNewOpenAIProviderWithUserIDhere. 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 | 🟠 MajorPropagate portal-save failures from the runtime host.
SavePortalalways returnsnilbecause it routes throughsavePortalQuiet. That breaks the host contract for integrations and lets callers continue after persistence failed. Returnh.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 | 🟠 MajorRoll back
AutoGreetingSentwhen dispatch fails.If
dispatchInternalMessagefails after Line 279, the persisted flag staystrueand 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 | 🟠 MajorRedaction alone doesn't evict the stale assistant turn from AI history.
regenerateFromEditnow reads context viagetAIHistoryMessages, 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 | 🟡 MinorGuard the write helpers before dereferencing
s.client.UserLogin.
loadCustomAgentsandloadCustomAgenthandle a nil or half-initialized adapter safely, butsaveAgentanddeleteAgentstill dereferences.client.UserLoginunconditionally 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 | 🟡 MinorTrim
room.Namebefore copying it intochatInfo.Name.
applyPortalRoomNamenormalizes whitespace, butchatInfo.Name = &room.Namepersists 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 | 🟠 MajorDon't clear runtime state after a partial purge failure.
purgeLoginDatastill 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 | 🔴 CriticalMissing 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 | 🔴 CriticalNil
metadereference at line 102.When
loginMetadata(login)returns nil at line 90, the code proceeds to line 102 wheremeta.Provideris 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
updateLoginConfigstill holds mutex duringsaveAILoginConfigcall, causing deadlock.The function acquires
loginConfigMuat line 143 withdefer Unlock(), then callssaveAILoginConfigat line 155. However,saveAILoginConfig(lines 109-112) also tries to acquireloginConfigMuwhenlogin.Clientis*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 | 🟠 MajorDon'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 | 🟠 MajorInterrupt mode still depends on a stale room-busy snapshot.
roomBusyis decided beforeacquireRoom. 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.
canonicalLoginBridgeIDalready trims, sostrings.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) stringfor 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.Marshalorjson.Unmarshalfails in thedefaultcase, returningnilsilently 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 onoc.The
oc != nilcheck at line 76 is unnecessary since line 24 already returns early whenoc == 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 afterpublishOrReuseClient.Lines 138-140 re-set
UserLogin,ClientBase.SetUserLogin, andlogin.Clientonchosen, butpublishOrReuseClientalready 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: Variablecopyshadows builtin.The variable name
copyat line 59 shadows Go's builtincopyfunction. While harmless here, consider renaming for clarity.♻️ Suggested rename
func cloneHeartbeatEvent(in *HeartbeatEventPayload) *HeartbeatEventPayload { if in == nil { return nil } - copy := *in - return © + 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:imageGenServiceConfigusescontext.Background()instead of accepting context parameter.The function calls
loginConfigSnapshot(context.Background())at line 474, which drops request cancellation signals. Consider accepting actxparameter 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:supportsGeminiImageGenalways returns false.Both the
ProviderMagicProxycase and thedefaultcase returnfalse. 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:saveCfgclosure shadows outererrvariable.The
saveCfgclosure at line 695-702 assigns toerr(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 overwritesportalsByKeyaftercacheHeartbeatTestPortals.Lines 99-101 overwrite
portalsByKeywith only the default chat portal key, discarding the map set bycacheHeartbeatTestPortalsat line 98. This appears intentional to test the default-chat fallback path, but thecacheHeartbeatTestPortalscall 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
| 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) | ||
| } |
There was a problem hiding this comment.
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.
| 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") | ||
| } |
There was a problem hiding this comment.
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.
| 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") |
There was a problem hiding this comment.
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.
| 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 | ||
| } |
There was a problem hiding this comment.
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).
| 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) |
There was a problem hiding this comment.
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.
| 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.
| 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) |
There was a problem hiding this comment.
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.
| | [`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 | |
There was a problem hiding this comment.
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.
| | [`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.
- 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
No description provided.