Skip to content

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

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

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

Conversation

@batuhan
Copy link
Copy Markdown
Member

@batuhan batuhan commented Apr 12, 2026

No description provided.

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

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

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

coderabbitai bot commented Apr 12, 2026

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • DB-backed AI chat transcripts, login config (profiles/timezone), portal state, and custom agents.
    • Session transcripts exposed to integrations; in-process scheduler with start/stop.
  • Changes

    • Migrated to the AgentRemote SDK; CLI renamed to “AgentRemote Manager”.
    • Tool approvals now persisted; OpenClaw/OpenCode use persisted portal/login state.
    • Default room naming prefers bridge-owned name; desktop API is context-aware.
    • Removed several message actions (pins/member-info/channel info/edit/read lists); reaction removal requires explicit emoji.
  • Bug Fixes

    • More reliable logout cleanup and heartbeat/session tracking; safer portal persistence.
  • Documentation

    • Updated README, workflows, and descriptions to reflect new naming and capabilities.

Walkthrough

This PR migrates the codebase from the top-level agentremote APIs to the sdk package, introduces AI-specific persistent state (login, portal, transcript, internal prompts, tool rules) in aidb, replaces various metadata-based flows with DB-backed ones, refactors schedulers to in-process timers, streamlines portal/room updates, and updates bridges (AI, Codex, OpenClaw, OpenCode, DummyBridge), CLI texts, and tests accordingly.

Changes

Cohort / File(s) Summary
SDK package migration
sdk/*, bridges/*/*.{go,test.go} (widespread), network_caps.go (removed)
Renamed package namespace from agentremote to sdk, updated types/functions/constants, removed some Matrix-handling hooks, added Matrix action helpers, adjusted defaults (capabilities/bridge-info), and removed auto-login.
AI DB/state and persistence
pkg/aidb/db.go, pkg/aidb/001-init.sql, pkg/aidb/json_blob_table.go, bridges/ai/*_db.go, bridges/ai/transcript_db.go, bridges/ai/internal_prompt_db.go
Introduces EnsureSchema and JSONBlobTable, adds AI tables (login_state/config, portal_state, transcripts, internal_messages, sessions, tool_approval_rules), and replaces metadata persistence with DB-backed reads/writes for login/portal/transcripts/internal prompts.
AI client and flows
bridges/ai/client.go, bridges/ai/login*.go, bridges/ai/connector*.go, bridges/ai/metadata.go, bridges/ai/chat*.go, bridges/ai/identifiers.go, bridges/ai/handle*.go, bridges/ai/session_*, bridges/ai/response_*, bridges/ai/message_parts.go
Moves runtime/login config/state to aidb; updates construction, connect/disconnect/logout; rewires message persistence via AI transcripts; replaces multiple DB lookups with helpers; updates portal naming and lifecycle via sdk helpers.
Scheduler refactor
bridges/ai/scheduler*.go, bridges/ai/matrix_coupling.go (removed), bridges/ai/scheduler_events.go (trimmed)
Replaces Matrix delayed events with in-process timers; removes pending-delay fields; updates DB schema access to loginScope; adds Stop(); removes event-type registrations.
Tool approvals pipeline
bridges/ai/tool_*.go, bridges/ai/approval_prompt_presentation.go, sdk/approval_*.go
Moves to sdk types/constants, replaces metadata-based “always allow” with DB-backed rules, adjusts approval flow sender behavior (no synthetic ghost), and tightens allowed message-tool actions.
History and streaming
bridges/ai/streaming_*.go, bridges/ai/prompt_builder.go, bridges/ai/turn_data.go, bridges/ai/ui_message_metadata.go
Threads sdk types for turns/parts/timing; integrates internal prompt history; sorts/merges DB and internal histories; updates metadata construction and persistence.
OpenClaw state migration
bridges/openclaw/*.go, bridges/openclaw/metadata.go
Shifts portal/login state to DB blobs; scopes ghosts to login; updates capability computation and room info from state; adjusts identifiers hashing/format.
OpenCode updates
bridges/opencode/*.go
Migrates to sdk APIs, removes SetRoomName from Host, adds per-instance session tracking, adjusts login flow to always create new login, and narrows capability wiring.
DummyBridge cleanup
bridges/dummybridge/*
Migrates to sdk, removes contact/search/resolve flows and NextChatIndex from metadata; simplifies runtime/session handling.
CLI/workflows/docs
cmd/agentremote/*, .github/workflows/*, README.md, docs/*, docker/agentremote/README.md
Renames user-facing text to “AgentRemote Manager,” parameterizes binaryName, updates completion/help text, and workflow step names.
Integrations memory
pkg/integrations/memory/*, pkg/integrations/runtime/*
Replaces BridgeDB exposure with MemoryStateDB + SessionTranscript API; changes purge to strict (returns errors); updates session content hashing and message summary fields.
Beeper AI naming and misc.
bridges/ai/integrations_config.go, bridges/ai/mcp_client.go, pkg/agents/prompt.go, sdk/room_features.go
Text and identifier defaults updated (Beeper AI labels, user agent prefixes, default capability ID).
Removed features/files
bridges/ai/tools_matrix_api.go, bridges/ai/command_registry.go (broadcast part), sdk/client_resolution_test.go, network_caps.go, various tests
Deletes Matrix API helpers, command-description broadcasting, and some tests aligning with new sdk or removed features.
Tests refactor
bridges/ai/*_test.go, bridges/codex/*_test.go, bridges/openclaw/*_test.go, bridges/opencode/*_test.go, pkg/aidb/db_test.go, sdk/*_test.go
Switches to new helpers, DB-backed clients, state/config setters; updates assertions/types to sdk; adds persistence boundary tests and cleanup coverage.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Client as AIClient
  participant Portal as Portal
  participant AIDB as AI DB (aidb)
  participant Matrix as Matrix API

  User->>Client: Send message
  Client->>Portal: Ensure portal lifecycle/name
  Client->>AIDB: Insert transport-safe bridge message (mapping)
  Client->>AIDB: Upsert AI transcript message (full metadata)
  Client-->>Matrix: SendMessageStatus(room, eventID, Pending)
  Client->>AIDB: Update login/portal state as needed
  Client-->>Matrix: SendMessageStatus(room, eventID, Success)

  Note over Client,AIDB: Later history loads read from aiTranscriptTable
Loading

Estimated code review effort

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

Possibly related PRs

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

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

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 17

Caution

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

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

197-210: ⚠️ Potential issue | 🟠 Major

Remove dead code buildCommandDescriptionContent and its test.

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

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

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

75-106: ⚠️ Potential issue | 🟠 Major

Avoid caching conversation state under the empty key.

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

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

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

134-176: ⚠️ Potential issue | 🔴 Critical

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

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

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

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

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

53-68: ⚠️ Potential issue | 🟡 Minor

Assert the rest of the new v1 tables here too.

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

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

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

47-52: ⚠️ Potential issue | 🔴 Critical

Restore the nil receiver guard in getIntent.

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

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

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

239-248: ⚠️ Potential issue | 🟠 Major

Avoid returning an error after a successful NewLogin without rollback.

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

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

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

59-80: ⚠️ Potential issue | 🟠 Major

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

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

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

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

1095-1132: ⚠️ Potential issue | 🟠 Major

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

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

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

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

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

133-164: ⚠️ Potential issue | 🟠 Major

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

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

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

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

21-50: Inconsistent description format for AI bridge.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

60-71: Inconsistent table name usage.

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

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

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

72-74: Consider consolidating duplicate type assertion.

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

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

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

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

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

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

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

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

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

63-67: Unnecessary state save when hash matches.

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

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

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

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

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

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

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

127-143: Consider caching table creation status.

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

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

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

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

115-116: Portal key uses null byte separator.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---

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

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

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

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

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

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

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

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

---

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

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

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

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

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

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

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

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

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

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

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

Fix all unresolved CodeRabbit comments on this PR:

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

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

Applied to files:

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

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

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

42-55: ⚠️ Potential issue | 🟠 Major

Guard aidb.EnsureSchema when the bridge DB is unavailable.

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

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

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

46-52: ⚠️ Potential issue | 🟠 Major

Treat an unset Agents flag as enabled.

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

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

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

239-268: ⚠️ Potential issue | 🟠 Major

Return the bridge ghost ID for agent contacts.

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

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

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

313-325: ⚠️ Potential issue | 🟠 Major

cloneUserLoginMetadata now strips all transient login state.

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

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

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

387-390: ⚠️ Potential issue | 🟠 Major

Persist WelcomeSent before sending the notice.

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

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

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

319-321: ⚠️ Potential issue | 🟠 Major

Persist AutoGreetingSent before dispatching the greeting.

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

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

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

456-463: ⚠️ Potential issue | 🟠 Major

Sync the generated title to Matrix after saving it.

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

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

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

390-397: ⚠️ Potential issue | 🟡 Minor

Require remove:true before routing to reaction removal.

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

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

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

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

238-250: ⚠️ Potential issue | 🟠 Major

Cap SessionTranscript history fetches.

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

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

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

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

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

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

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

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

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

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

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

280-295: Same redundant Metadata nil check pattern.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---

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

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

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

---

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

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

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

---

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

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

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

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

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

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

📥 Commits

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

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

Applied to files:

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

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

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

sdk/room_features.go (1)

74-74: Namespace fallback update looks correct.

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

bridges/ai/bridge_info_test.go (3)

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

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


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

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


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

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

cmd/agentremote/commands.go (6)

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

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


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

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


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

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


503-577: Bash completion parameterization is implemented cleanly.

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


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

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


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

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

bridges/codex/connector.go (1)

53-76: Runtime defaulting migration looks consistent.

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

bridges/ai/timezone.go (1)

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

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

bridges/ai/bridge_db.go (3)

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

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


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

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


77-82: bridgeDBFromPortal nil guards are solid.

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

bridges/ai/heartbeat_session.go (1)

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

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

bridges/ai/mcp_servers.go (1)

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

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

bridges/ai/model_catalog.go (1)

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

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

bridges/ai/scheduler_heartbeat_test.go (1)

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

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

bridges/ai/image_understanding.go (1)

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

The implementation always returns a non-nil *loginRuntimeState:

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

182-186: LGTM!

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

bridges/ai/mcp_helpers.go (1)

68-80: LGTM!

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

bridges/ai/gravatar.go (1)

184-189: LGTM!

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

bridges/ai/sessions_tools.go (3)

118-127: LGTM!

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


270-286: LGTM!

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


572-581: LGTM!

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

bridges/ai/tool_approvals_policy.go (1)

17-28: LGTM!

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

bridges/ai/subagent_announce.go (2)

45-74: LGTM!

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


105-138: LGTM!

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

bridges/ai/token_resolver.go (1)

185-225: LGTM!

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

bridges/ai/logout_cleanup.go (1)

12-91: LGTM on expanded cleanup coverage.

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

bridges/ai/prompt_builder.go (1)

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

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

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

Consider:

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

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

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

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


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

The LoadAgents function properly:

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

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

The delete function now properly cleans up multiple tables:

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

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

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

bridges/ai/media_understanding_runner.go (1)

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

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

bridges/ai/response_finalization.go (2)

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

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


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

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

bridges/ai/heartbeat_events.go (2)

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

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


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

The persistence logic correctly:

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

33-36: LGTM - SDK error helper migration.

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


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

The login flow now:

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

Good addition of the SAVE_LOGIN_FAILED error case.


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

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

pkg/aidb/db.go (1)

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

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

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

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

1-2: Plan migration strategy before first release.

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

			> Likely an incorrect or invalid review comment.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Caution

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

⚠️ Outside diff range comments (3)
bridges/ai/session_store.go (1)

76-109: ⚠️ Potential issue | 🟠 Major

Use the IDs captured in sessionStoreRef for the SQL path too.

sessionStoreRef now carries BridgeID/LoginID, and the lock key uses them, but the actual SELECT/INSERT still re-read scope from oc. If the client context changes after resolveSessionStoreRef, you can lock one login's session and read/write another login's row. The DB predicates should use ref.BridgeID and ref.LoginID so the ref fully defines the storage target.

Suggested direction
 func (oc *AIClient) getSessionEntry(ctx context.Context, ref sessionStoreRef, sessionKey string) (sessionEntry, bool) {
 	if oc == nil || strings.TrimSpace(sessionKey) == "" {
 		return sessionEntry{}, false
 	}
 	scope := oc.sessionDBScope()
-	if scope == nil {
+	if scope == nil || strings.TrimSpace(ref.BridgeID) == "" || strings.TrimSpace(ref.LoginID) == "" {
 		return sessionEntry{}, false
 	}
 	...
 	err := scope.db.QueryRow(ctx, `
 		SELECT
 			...
 		FROM `+aiSessionsTable+`
 		WHERE bridge_id=$1 AND login_id=$2 AND store_agent_id=$3 AND session_key=$4
 	`,
-		scope.bridgeID, scope.loginID, normalizeAgentID(ref.AgentID), strings.TrimSpace(sessionKey),
+		ref.BridgeID, ref.LoginID, normalizeAgentID(ref.AgentID), strings.TrimSpace(sessionKey),
 	).Scan(...)
 }

 func (oc *AIClient) upsertSessionEntry(ctx context.Context, ref sessionStoreRef, sessionKey string, entry sessionEntry) error {
 	scope := oc.sessionDBScope()
-	if scope == nil {
+	if scope == nil || strings.TrimSpace(ref.BridgeID) == "" || strings.TrimSpace(ref.LoginID) == "" {
 		return nil
 	}
 	...
 	_, err := scope.db.Exec(ctx, `
 		INSERT INTO `+aiSessionsTable+` (
 			...
 		) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
 		...
 	`,
-		scope.bridgeID,
-		scope.loginID,
+		ref.BridgeID,
+		ref.LoginID,
 		normalizeAgentID(ref.AgentID),
 		strings.TrimSpace(sessionKey),
 		...
 	)
 	return err
 }

Also applies to: 136-179, 263-277

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

In `@bridges/ai/session_store.go` around lines 76 - 109, The SQL queries in
getSessionEntry (and the other affected functions around the 136-179 and 263-277
ranges) still use scope.bridgeID/scope.loginID but must use the IDs carried in
sessionStoreRef so the ref fully defines the target; update the
SELECT/INSERT/UPDATE/DELETE parameter lists and WHERE clauses to use
ref.BridgeID and ref.LoginID (e.g., replace scope.bridgeID, scope.loginID with
ref.BridgeID, ref.LoginID) while keeping normalizeAgentID(ref.AgentID) for agent
ID and preserving sessionKey handling; do the same substitution wherever those
other functions (the counterparts to getSessionEntry) currently rely on scope
instead of the ref.
bridges/ai/chat.go (1)

46-52: ⚠️ Potential issue | 🟠 Major

Keep the default agent-enable behavior consistent.

When cfg.Agents is unset, this returns false, but shouldEnsureDefaultChat() still treats the same unset value as enabled. That means a fresh login can bootstrap a default Beeper chat while agent search/resolve/create paths reject agent usage. Pick one default and use it consistently on both sides.

🤖 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
treats a nil cfg.Agents as false while shouldEnsureDefaultChat treats nil as
enabled; make the default behavior consistent by changing agentsEnabledForLogin
to treat a nil cfg.Agents as enabled (i.e., return true when cfg.Agents == nil)
so it returns true by default and otherwise returns *cfg.Agents; update the
implementation in the agentsEnabledForLogin function (which calls
loginConfigSnapshot) to reflect this consistent default with
shouldEnsureDefaultChat.
bridges/ai/handlematrix.go (1)

364-383: ⚠️ Potential issue | 🟠 Major

Use the repaired transcript role for edit handling.

This block loads and normalizes transcriptMeta, but the canonical-turn update and the regenerate gate still key off msgMeta.Role. If edit.EditTarget.Metadata is missing/stale while the transcript row still says "user", the edit is persisted and regeneration is skipped incorrectly. Derive an effective role from transcriptMeta first, then fall back to msgMeta.

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

In `@bridges/ai/handlematrix.go` around lines 364 - 383, The code currently uses
msgMeta.Role to decide canonical-turn updates and whether to regenerate, but
transcriptMeta may contain the authoritative role; change the logic to derive an
effectiveRole from transcriptMeta (e.g., use transcriptMeta.Role when present,
otherwise fall back to msgMeta.Role) and use that effectiveRole in both the
canonical-turn update block (where setCanonicalTurnDataFromPromptMessages and
cloneCanonicalTurnData are called) and the regenerate gate at the end (the if
that currently checks msgMeta.Role != "user"); ensure you still persist via
persistAITranscriptMessage and call oc.notifySessionMutation as before.
♻️ Duplicate comments (9)
bridges/ai/tool_configured.go (1)

16-18: ⚠️ Potential issue | 🟠 Major

Pass the caller context into loginConfigSnapshot.

Line 59 swaps the request context for context.Background(), so the DB-backed config snapshot can outlive caller cancellation and deadlines. Thread the incoming ctx through effectiveSearchConfig, effectiveFetchConfig, and effectiveToolConfig instead.

Suggested fix
-func (oc *AIClient) effectiveSearchConfig(_ context.Context) *search.Config {
+func (oc *AIClient) effectiveSearchConfig(ctx context.Context) *search.Config {
 	return effectiveToolConfig(
+		ctx,
 		oc,
 		func(connector *OpenAIConnector) *search.Config {
 			if connector == nil || connector.Config.Tools.Web == nil {
 				return nil
@@
-func (oc *AIClient) effectiveFetchConfig(_ context.Context) *fetch.Config {
+func (oc *AIClient) effectiveFetchConfig(ctx context.Context) *fetch.Config {
 	return effectiveToolConfig(
+		ctx,
 		oc,
 		func(connector *OpenAIConnector) *fetch.Config {
 			if connector == nil || connector.Config.Tools.Web == nil {
 				return nil
@@
 func effectiveToolConfig[T any](
+	ctx context.Context,
 	oc *AIClient,
 	load func(*OpenAIConnector) *T,
 	applyTokens func(*T, string, *aiLoginConfig, *OpenAIConnector) *T,
 	withDefaults func(*T) *T,
 ) *T {
@@
 		cfg = load(connector)
 		if oc.UserLogin != nil {
 			provider = loginMetadata(oc.UserLogin).Provider
-			loginCfg = oc.loginConfigSnapshot(context.Background())
+			loginCfg = oc.loginConfigSnapshot(ctx)
 		}
 	}

Also applies to: 30-32, 44-63

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

In `@bridges/ai/tool_configured.go` around lines 16 - 18, The code is using
context.Background() when calling loginConfigSnapshot which allows DB-backed
config snapshots to outlive caller cancellation; thread the incoming ctx through
by changing effectiveSearchConfig and effectiveFetchConfig to accept a
context.Context and pass that ctx into effectiveToolConfig, and update
effectiveToolConfig to pass the received ctx into loginConfigSnapshot (use the
same ctx instead of context.Background()); update all call sites to forward the
original caller ctx to these functions (refer to effectiveSearchConfig,
effectiveFetchConfig, effectiveToolConfig, and loginConfigSnapshot).
bridges/ai/tools.go (1)

390-397: ⚠️ Potential issue | 🟡 Minor

Don't treat a missing emoji as an implicit remove.

executeMessageReactRemove still expects an explicit emoji, but emoji == "" is routed there even when remove is false. That turns an invalid add-reaction request into the remove path and gives the wrong validation flow.

🐛 Proposed fix
-// Supports adding reactions (with emoji) and removing reactions (with remove:true or explicit emoji).
+// Supports adding reactions with an 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 current branching in
executeMessageReact treats emoji == "" as an implicit remove and calls
executeMessageReactRemove even when remove is false; change the logic in
executeMessageReact so only remove triggers the removal path: call
executeMessageReactRemove(ctx, args, btc) only when remove is true, and if
remove is false but emoji is empty return a validation error indicating an emoji
is required for adding a reaction (or otherwise handle the missing-emoji case
explicitly) so the add-reaction flow validates correctly; update checks around
the emoji and remove variables and any callers of executeMessageReact
accordingly.
bridges/ai/prompt_builder.go (1)

152-171: ⚠️ Potential issue | 🟠 Major

Keep internal prompt rows outside the history cap.

hr.limit is still applied after internalRows are merged into candidates, so a burst of internal prompt history can push out the newest user/assistant turns entirely. It also shrinks the vision replay window because the later i < maxHistoryImageMessages check now counts internal rows too.

🤖 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 152 - 171, The current code
applies hr.limit and the image-message cap after merging internalRows into
candidates so internal rows can evict recent user/assistant turns; change the
flow to keep internalRows out of the cap by treating them separately:
build/collect the regular candidates (the ones not from
loadInternalPromptHistory), apply slices.SortStableFunc + the hr.limit
truncation and the i < maxHistoryImageMessages logic only to that non-internal
list, and only after truncation append the internalRows converted to
replayCandidate (from loadInternalPromptHistory) into the final candidates slice
so they are not counted against hr.limit or the image-message window; refer to
loadInternalPromptHistory, internalRows, candidates, replayCandidate, hr.limit
and the i < maxHistoryImageMessages check when making this change.
bridges/ai/handlematrix.go (1)

942-959: ⚠️ Potential issue | 🟠 Major

Return persistence errors from savePortalQuiet.

This helper logs and continues on saveAIPortalState / portal.Save failures, so callers cannot stop one-shot flows after the state write failed. That leaves flags like WelcomeSent, AutoGreetingSent, or generated title state vulnerable to duplicate side effects after retries/restarts. Make this return an error and let callers bail when persistence fails.

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

In `@bridges/ai/handlematrix.go` around lines 942 - 959, savePortalQuiet currently
swallows persistence errors from saveAIPortalState and portal.Save so callers
can't abort flows on failed writes; change savePortalQuiet (and its callers) to
return an error instead of void, propagate any non-context.Canceled error from
saveAIPortalState or portal.Save up to the caller, and update all call sites to
handle the returned error (bail out / abort the one-shot flow) so flags like
WelcomeSent / AutoGreetingSent / title-generation are not left in an
inconsistent state; reference the function savePortalQuiet, the helper
saveAIPortalState, and the portal.Save call when making the signature and
call-site changes.
bridges/ai/handleai.go (1)

454-461: ⚠️ Potential issue | 🔴 Critical

Persisting portal.Name alone won't update the Matrix room title.

savePortalQuiet only saves local portal state, so the generated title won't reach the Matrix room UI unless you follow this with the normal room-sync path (materializePortalRoom(...) or EnsurePortalLifecycle(...)). As written, the DB will say the room is titled, but the room name shown to users can stay unchanged.

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

In `@bridges/ai/handleai.go` around lines 454 - 461, The code only updates local
state (meta, portal.Name, portal.NameSet) and calls oc.savePortalQuiet(...)
which does not push changes to Matrix; after saving, invoke the room-sync path
(e.g. call materializePortalRoom(bgCtx, portal) or EnsurePortalLifecycle(bgCtx,
portal) as used elsewhere) so the generated title is propagated to the Matrix
room UI; keep the existing oc.savePortalQuiet(...) call but add the appropriate
materialize/EnsurePortalLifecycle call using the same bgCtx and portal objects.
bridges/ai/login_state_db.go (2)

213-229: ⚠️ Potential issue | 🟠 Major

Don't mutate the cached state before the write succeeds.

fn currently mutates oc.loginState in place, then saveLoginRuntimeState runs afterward. If the save fails, memory keeps the new values even though the DB does not, so later reads observe state that was never persisted. Mutate a cloned copy, persist that copy, and only swap oc.loginState on success.

🤖 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 213 - 229, The updateLoginState
function currently lets fn mutate oc.loginState in-place before persisting;
change it to clone the cached state first (e.g., create a new loginRuntimeState
copy of oc.loginState), call fn on that cloned copy, and if fn returns true then
call saveLoginRuntimeState(ctx, oc, clonedState); only after save succeeds swap
oc.loginState = clonedState while holding oc.loginStateMu. Keep the existing
loadLoginRuntimeState path and locking (oc.loginStateMu) but ensure all
mutations and the save operate on the clone so the in-memory cache is only
updated on successful persistence.

70-103: ⚠️ Potential issue | 🟠 Major

Keep a fallback path for pre-migration login state.

On sql.ErrNoRows this now returns a brand-new state immediately, so upgraded logins lose legacy runtime fields until something repopulates aichats_login_state. That will reset things like chat numbering / heartbeat seeding for existing users during rollout. Preserve the legacy metadata fallback and backfill the new row on first load instead of treating “missing row” as “empty state.”

🤖 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 70 - 103, When QueryRow against
aiLoginStateTable in loadLoginRuntimeState returns sql.ErrNoRows, don't return a
brand-new state immediately; instead attempt to load legacy runtime metadata
(from the pre-migration aichats_login_state or via a helper like
loadLegacyLoginState) using scope.bridgeID and scope.loginID, populate the
loginRuntimeState fields (NextChatIndex, LastHeartbeatEvent, model/file/gravatar
caches, ConsecutiveErrors, LastErrorAt) from that legacy data if present, and
then upsert/backfill a row into aiLoginStateTable so future loads hit the
migrated schema; handle and return any unexpected DB errors as before. Ensure
this logic is implemented inside loadLoginRuntimeState and references
aiLoginStateTable, scope.bridgeID, scope.loginID, and the new-row upsert path.
bridges/openclaw/identifiers.go (1)

31-36: ⚠️ Potential issue | 🟠 Major

PathEscape still leaves this scoped ID format ambiguous.

openClawScopedGhostUserID still builds openclaw-agent:<escaped-login>:<escaped-agent>, but parseOpenClawGhostID still splits on :. A loginID like user:name survives url.PathEscape and will be parsed as login=user, agent=name:.... Please switch to an encoding that cannot emit : (for example base64url or hex) and version the format before rollout.

Also applies to: 47-70

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

In `@bridges/openclaw/identifiers.go` around lines 31 - 36, Change the scoped-ID
encoding from url.PathEscape to a non-colon-producing binary-to-text scheme and
add a version token: in openClawScopedGhostUserID encode loginID and agentID
with base64 URL-safe (no padding) or hex, prefix the ID with a version (e.g.
"openclaw-agent:v1:") and join with ":"; update parseOpenClawGhostID to
recognize the "v1" form and decode the base64url/hex segments back to original
values, and keep backward-compatible handling for the old PathEscape format
during rollout; also apply the same change to the other factory/parser pair
referenced around the 47-70 block so both creation and parsing use the same
versioned, colon-safe encoding.
bridges/openclaw/manager.go (1)

594-617: ⚠️ Potential issue | 🟠 Major

Persist the resolved DM session key before the first send.

This still only updates the local sessionKey. Until syncSessions catches up, the saved portal state keeps pointing at the synthetic key, so a fast reply or approval on the real session can miss resolvePortal(...) and drop the first upstream event.

Suggested fix
 	sessionKey := strings.TrimSpace(state.OpenClawSessionKey)
 	if state.OpenClawDMCreatedFromContact && state.OpenClawSessionID == "" && isOpenClawSyntheticDMSessionKey(state.OpenClawSessionKey) {
 		if resolvedKey, err := gateway.ResolveSessionKey(ctx, state.OpenClawSessionKey); err == nil && strings.TrimSpace(resolvedKey) != "" {
 			sessionKey = strings.TrimSpace(resolvedKey)
+			state.OpenClawSessionKey = sessionKey
+			if err := saveOpenClawPortalState(ctx, msg.Portal, m.client.UserLogin, 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/openclaw/manager.go` around lines 594 - 617, The code only updates
the local sessionKey variable after ResolveSessionKey; persist the resolved
session key into the portal state so upstream events use the real key before the
first SendMessage. Specifically, when gateway.ResolveSessionKey returns a
non-empty resolvedKey (the block that currently sets sessionKey), assign
strings.TrimSpace(resolvedKey) back to state.OpenClawSessionKey and persist that
state using the existing portal-state persistence API your manager exposes (so
that the saved portal points to the real session key) before calling
gateway.SendMessage; keep the isOpenClawSyntheticDMSessionKey and m.syncSessions
logic intact.
🧹 Nitpick comments (3)
bridges/codex/portal_state_db.go (1)

136-149: Prefer per-row tolerance for malformed state_json values.

At Line 147, a single bad JSON blob aborts the entire listing. Since invalid keys are already skipped at Line 142, consider skipping malformed JSON rows too, so one corrupted row doesn’t hide all other portals.

💡 Suggested change
 		state := &codexPortalState{}
 		if strings.TrimSpace(stateRaw) != "" {
 			if err := json.Unmarshal([]byte(stateRaw), state); err != nil {
-				return nil, err
+				continue
 			}
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/codex/portal_state_db.go` around lines 136 - 149, In the rows.Next()
loop inside the portal listing code, don't abort the entire List when
json.Unmarshal into codexPortalState fails; instead, handle malformed state_json
on a per-row basis by skipping that row (continue) and optionally logging the
parse error/context. Update the block where parseCodexPortalKey and
json.Unmarshal are used (referencing parseCodexPortalKey, codexPortalState, and
the rows.Next loop) so that a JSON unmarshal error does not return from the
function but is ignored for that portal entry.
bridges/ai/portal_send.go (1)

139-139: Add minimal context when sdk.SendEditViaPortal fails.

Line 139 currently returns the SDK error as-is; wrapping it with portal/target context makes failures much easier to triage.

Suggested patch
-	return sdk.SendEditViaPortal(oc.UserLogin, portal, sender, targetMsgID, timestamp, streamOrder, "ai_edit_target", converted)
+	if err := sdk.SendEditViaPortal(oc.UserLogin, portal, sender, targetMsgID, timestamp, streamOrder, "ai_edit_target", converted); err != nil {
+		return fmt.Errorf("send edit via portal failed (portal=%s target=%s): %w", portal.MXID, targetMsgID, err)
+	}
+	return nil
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/portal_send.go` at line 139, Return a wrapped error that includes
minimal context when sdk.SendEditViaPortal fails: catch the error returned from
the call to sdk.SendEditViaPortal (the invocation using oc.UserLogin, portal,
sender, targetMsgID, timestamp, streamOrder, "ai_edit_target", converted) and
return a new error that includes the portal identifier and targetMsgID (e.g.,
via fmt.Errorf or errors.Wrapf) so the failure message contains portal and
target context for easier triage.
bridges/ai/media_understanding_runner_openai_test.go (1)

5-9: Use the parameterized connector directly instead of via helper replacement.

newTestAIClientWithProvider currently creates a bare &OpenAIConnector{} that gets immediately replaced. While this doesn't drop any state today, this pattern masks intent and could hide issues if the helper is extended with connector initialization in the future. Consider making tests that need a configured connector pass it directly rather than wrapping and replacing.

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

In `@bridges/ai/media_understanding_runner_openai_test.go` around lines 5 - 9, The
test helper newMediaTestClient should not create a bare client via
newTestAIClientWithProvider and then replace its connector; instead construct or
initialize the AIClient with the provided OpenAIConnector directly (referencing
newMediaTestClient, newTestAIClientWithProvider, AIClient, OpenAIConnector, and
setTestLoginConfig) so the connector passed in (oc) is used as-is; update
newMediaTestClient to create an AIClient with the intended provider and
connector and then call setTestLoginConfig(client, cfg) before returning.
🤖 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/bridge_db.go`:
- Around line 112-117: loginScopeForClient currently uses raw values from
loginDBContext so IDs with surrounding whitespace will be treated as non-empty
but won't match scopes created by other constructors; update loginScopeForClient
to trim bridgeID and loginID (e.g. strings.TrimSpace) immediately after calling
loginDBContext, check emptiness on the trimmed values, and return
&loginScope{db: db, bridgeID: trimmedBridgeID, loginID: trimmedLoginID}; this
mirrors the normalization performed in loginScopeForLogin and
portalScopeForPortal.

In `@bridges/ai/client.go`:
- Line 1703: The call to loadAITranscriptMessage uses the wrong args/types;
remove the extra oc and pass the Portal pointer and a networkid.MessageID for
messageID. Replace the call `loadAITranscriptMessage(ctx, oc, portal.MXID,
msg.ID)` with `loadAITranscriptMessage(ctx, portal, msg.ID)` (or
`loadAITranscriptMessage(ctx, portal, networkid.MessageID(msg.ID))` if an
explicit cast is needed) so the call matches the signature `(ctx
context.Context, portal *bridgev2.Portal, messageID networkid.MessageID)`.

In `@bridges/ai/delete_chat.go`:
- Around line 85-87: The DELETE uses room_id=$3 but transcripts are keyed by
portal_id; update the execDelete call that references aiTranscriptTable (in
delete_chat.go) to use `WHERE bridge_id=$1 AND login_id=$2 AND portal_id=$3` and
pass the portal identifier (e.g., portalID) instead of sessionKey so it matches
how persistAITranscriptMessage and transcript_db.go store/query rows.

In `@bridges/ai/handlematrix.go`:
- Line 357: The call to loadAITranscriptMessage is passing too many arguments
(transcriptMsg, err := loadAITranscriptMessage(ctx, oc, portal.MXID,
edit.EditTarget.ID); the helper's signature changed), so update the invocation
to match the helper's current signature or change the helper if you intended a
room-scoped lookup: either remove the extra parameters (e.g. drop portal.MXID or
edit.EditTarget.ID) and pass only the expected args from ctx and oc, or modify
loadAITranscriptMessage's function definition to accept (ctx, oc, mxid, roomID)
and update its implementation accordingly; locate loadAITranscriptMessage and
handlematrix.go references to keep the call and the helper signatures
consistent.

In `@bridges/ai/transcript_db.go`:
- Around line 211-214: The call to execDelete is passing portal.Bridge.Log by
value but execDelete expects a *zerolog.Logger; update the call site in the
transcript deletion to pass the logger pointer by using the address of
portal.Bridge.Log (i.e., change portal.Bridge.Log to &portal.Bridge.Log) so
execDelete receives *zerolog.Logger for the DELETE of aiTranscriptTable in the
function that uses scope.bridgeID, scope.loginID, scope.portalID.

In `@bridges/openclaw/manager.go`:
- Around line 1967-1971: The loop currently skips any message missing
turn/run/idempotency markers (using
openClawMessageTurnMarker/openClawMessageRunMarker/openClawMessageIdempotencyKey)
which prevents shouldMirrorLatestUserMessageFromHistory from ever hitting its
15-minute timestamp fallback; change the filter so messages without those
markers are still considered by shouldMirrorLatestUserMessageFromHistory (i.e.,
call shouldMirrorLatestUserMessageFromHistory(normalizeOpenClawLiveMessage(...))
before continuing or only continue when markers are missing AND
shouldMirrorLatestUserMessageFromHistory returns false) so gateways that omit
markers can still trigger the timestamp fallback.

In `@bridges/openclaw/metadata.go`:
- Around line 133-149: loadOpenClawPortalState currently returns an empty
openClawPortalState on a missing DB row which causes pre-upgrade portals to lack
session bindings; change the nil-state path to read legacy portal metadata from
the provided portal (e.g., portal.Metadata or existing legacy keys), construct
an openClawPortalState populated with OpenClawSessionKey/room state, persist it
back into the blob store (use openClawPortalStateBlob and
aidb.Save[openClawPortalState] or the project’s save helper) under the same
scope obtained from openClawPortalDBScopeFor, and then return the hydrated state
instead of an empty struct.
- Around line 258-285: The loadOpenClawLoginState function currently treats
sql.ErrNoRows as empty tokens, causing existing provisioned logins to lose
credentials after upgrade; instead, when QueryRow returns sql.ErrNoRows in
loadOpenClawLoginState, read the legacy credentials from the UserLogin's legacy
metadata (the fields on the bridgev2.UserLogin that previously stored
gateway_token/gateway_password/device_token), populate an
openClawPersistedLoginState with those values and return it (and persist it via
ensureOpenClawLoginStateTable if needed) so Start will see the seeded
credentials; update loadOpenClawLoginState to use bridgev2.UserLogin's legacy
metadata as the first-read fallback before returning an empty state.
- Around line 295-315: The code is trimming opaque credential fields before
saving which can corrupt valid credentials; in the DB write for
openclaw_login_state (the scope.db.Exec call that inserts/updates gateway_token,
gateway_password, device_token) stop calling strings.TrimSpace on
state.GatewayToken, state.GatewayPassword, and state.DeviceToken and pass the
raw values instead so the exact credential strings (from state) are persisted;
leave other fields (SessionsSynced, LastSyncAt, updated_at_ms) unchanged.

In `@pkg/aidb/json_blob_table.go`:
- Around line 47-56: The current QueryRow(...).Scan(&raw) result is checked for
empty raw before verifying err, which can hide real DB errors; change the logic
in the function that calls db.QueryRow(...).Scan(&raw) so you first inspect err
returned by Scan (using db.QueryRow and Scan on raw), handle sql.ErrNoRows by
returning (nil, nil) and return any other err immediately, and only after err is
nil check strings.TrimSpace(raw) == "" to return (nil, nil); refer to
t.TableName and t.KeyColumn in the same block to locate the query.
- Around line 17-20: JSONBlobTable's public TableName and KeyColumn need
defensive validation before being concatenated into SQL in Ensure, Load, Save,
and Delete; add a helper (e.g. validateIdentifier or
JSONBlobTable.validateIdentifiers) that enforces an SQL identifier allowlist
(e.g. regex ^[A-Za-z_][A-Za-z0-9_]*$) and call it at the start of Ensure, Load,
Save, and Delete, returning an error if either TableName or KeyColumn fails
validation to avoid unsafe SQL assembly.

---

Outside diff comments:
In `@bridges/ai/chat.go`:
- Around line 46-52: agentsEnabledForLogin currently treats a nil cfg.Agents as
false while shouldEnsureDefaultChat treats nil as enabled; make the default
behavior consistent by changing agentsEnabledForLogin to treat a nil cfg.Agents
as enabled (i.e., return true when cfg.Agents == nil) so it returns true by
default and otherwise returns *cfg.Agents; update the implementation in the
agentsEnabledForLogin function (which calls loginConfigSnapshot) to reflect this
consistent default with shouldEnsureDefaultChat.

In `@bridges/ai/handlematrix.go`:
- Around line 364-383: The code currently uses msgMeta.Role to decide
canonical-turn updates and whether to regenerate, but transcriptMeta may contain
the authoritative role; change the logic to derive an effectiveRole from
transcriptMeta (e.g., use transcriptMeta.Role when present, otherwise fall back
to msgMeta.Role) and use that effectiveRole in both the canonical-turn update
block (where setCanonicalTurnDataFromPromptMessages and cloneCanonicalTurnData
are called) and the regenerate gate at the end (the if that currently checks
msgMeta.Role != "user"); ensure you still persist via persistAITranscriptMessage
and call oc.notifySessionMutation as before.

In `@bridges/ai/session_store.go`:
- Around line 76-109: The SQL queries in getSessionEntry (and the other affected
functions around the 136-179 and 263-277 ranges) still use
scope.bridgeID/scope.loginID but must use the IDs carried in sessionStoreRef so
the ref fully defines the target; update the SELECT/INSERT/UPDATE/DELETE
parameter lists and WHERE clauses to use ref.BridgeID and ref.LoginID (e.g.,
replace scope.bridgeID, scope.loginID with ref.BridgeID, ref.LoginID) while
keeping normalizeAgentID(ref.AgentID) for agent ID and preserving sessionKey
handling; do the same substitution wherever those other functions (the
counterparts to getSessionEntry) currently rely on scope instead of the ref.

---

Duplicate comments:
In `@bridges/ai/handleai.go`:
- Around line 454-461: The code only updates local state (meta, portal.Name,
portal.NameSet) and calls oc.savePortalQuiet(...) which does not push changes to
Matrix; after saving, invoke the room-sync path (e.g. call
materializePortalRoom(bgCtx, portal) or EnsurePortalLifecycle(bgCtx, portal) as
used elsewhere) so the generated title is propagated to the Matrix room UI; keep
the existing oc.savePortalQuiet(...) call but add the appropriate
materialize/EnsurePortalLifecycle call using the same bgCtx and portal objects.

In `@bridges/ai/handlematrix.go`:
- Around line 942-959: savePortalQuiet currently swallows persistence errors
from saveAIPortalState and portal.Save so callers can't abort flows on failed
writes; change savePortalQuiet (and its callers) to return an error instead of
void, propagate any non-context.Canceled error from saveAIPortalState or
portal.Save up to the caller, and update all call sites to handle the returned
error (bail out / abort the one-shot flow) so flags like WelcomeSent /
AutoGreetingSent / title-generation are not left in an inconsistent state;
reference the function savePortalQuiet, the helper saveAIPortalState, and the
portal.Save call when making the signature and call-site changes.

In `@bridges/ai/login_state_db.go`:
- Around line 213-229: The updateLoginState function currently lets fn mutate
oc.loginState in-place before persisting; change it to clone the cached state
first (e.g., create a new loginRuntimeState copy of oc.loginState), call fn on
that cloned copy, and if fn returns true then call saveLoginRuntimeState(ctx,
oc, clonedState); only after save succeeds swap oc.loginState = clonedState
while holding oc.loginStateMu. Keep the existing loadLoginRuntimeState path and
locking (oc.loginStateMu) but ensure all mutations and the save operate on the
clone so the in-memory cache is only updated on successful persistence.
- Around line 70-103: When QueryRow against aiLoginStateTable in
loadLoginRuntimeState returns sql.ErrNoRows, don't return a brand-new state
immediately; instead attempt to load legacy runtime metadata (from the
pre-migration aichats_login_state or via a helper like loadLegacyLoginState)
using scope.bridgeID and scope.loginID, populate the loginRuntimeState fields
(NextChatIndex, LastHeartbeatEvent, model/file/gravatar caches,
ConsecutiveErrors, LastErrorAt) from that legacy data if present, and then
upsert/backfill a row into aiLoginStateTable so future loads hit the migrated
schema; handle and return any unexpected DB errors as before. Ensure this logic
is implemented inside loadLoginRuntimeState and references aiLoginStateTable,
scope.bridgeID, scope.loginID, and the new-row upsert path.

In `@bridges/ai/prompt_builder.go`:
- Around line 152-171: The current code applies hr.limit and the image-message
cap after merging internalRows into candidates so internal rows can evict recent
user/assistant turns; change the flow to keep internalRows out of the cap by
treating them separately: build/collect the regular candidates (the ones not
from loadInternalPromptHistory), apply slices.SortStableFunc + the hr.limit
truncation and the i < maxHistoryImageMessages logic only to that non-internal
list, and only after truncation append the internalRows converted to
replayCandidate (from loadInternalPromptHistory) into the final candidates slice
so they are not counted against hr.limit or the image-message window; refer to
loadInternalPromptHistory, internalRows, candidates, replayCandidate, hr.limit
and the i < maxHistoryImageMessages check when making this change.

In `@bridges/ai/tool_configured.go`:
- Around line 16-18: The code is using context.Background() when calling
loginConfigSnapshot which allows DB-backed config snapshots to outlive caller
cancellation; thread the incoming ctx through by changing effectiveSearchConfig
and effectiveFetchConfig to accept a context.Context and pass that ctx into
effectiveToolConfig, and update effectiveToolConfig to pass the received ctx
into loginConfigSnapshot (use the same ctx instead of context.Background());
update all call sites to forward the original caller ctx to these functions
(refer to effectiveSearchConfig, effectiveFetchConfig, effectiveToolConfig, and
loginConfigSnapshot).

In `@bridges/ai/tools.go`:
- Around line 390-397: The current branching in executeMessageReact treats emoji
== "" as an implicit remove and calls executeMessageReactRemove even when remove
is false; change the logic in executeMessageReact so only remove triggers the
removal path: call executeMessageReactRemove(ctx, args, btc) only when remove is
true, and if remove is false but emoji is empty return a validation error
indicating an emoji is required for adding a reaction (or otherwise handle the
missing-emoji case explicitly) so the add-reaction flow validates correctly;
update checks around the emoji and remove variables and any callers of
executeMessageReact accordingly.

In `@bridges/openclaw/identifiers.go`:
- Around line 31-36: Change the scoped-ID encoding from url.PathEscape to a
non-colon-producing binary-to-text scheme and add a version token: in
openClawScopedGhostUserID encode loginID and agentID with base64 URL-safe (no
padding) or hex, prefix the ID with a version (e.g. "openclaw-agent:v1:") and
join with ":"; update parseOpenClawGhostID to recognize the "v1" form and decode
the base64url/hex segments back to original values, and keep backward-compatible
handling for the old PathEscape format during rollout; also apply the same
change to the other factory/parser pair referenced around the 47-70 block so
both creation and parsing use the same versioned, colon-safe encoding.

In `@bridges/openclaw/manager.go`:
- Around line 594-617: The code only updates the local sessionKey variable after
ResolveSessionKey; persist the resolved session key into the portal state so
upstream events use the real key before the first SendMessage. Specifically,
when gateway.ResolveSessionKey returns a non-empty resolvedKey (the block that
currently sets sessionKey), assign strings.TrimSpace(resolvedKey) back to
state.OpenClawSessionKey and persist that state using the existing portal-state
persistence API your manager exposes (so that the saved portal points to the
real session key) before calling gateway.SendMessage; keep the
isOpenClawSyntheticDMSessionKey and m.syncSessions logic intact.

---

Nitpick comments:
In `@bridges/ai/media_understanding_runner_openai_test.go`:
- Around line 5-9: The test helper newMediaTestClient should not create a bare
client via newTestAIClientWithProvider and then replace its connector; instead
construct or initialize the AIClient with the provided OpenAIConnector directly
(referencing newMediaTestClient, newTestAIClientWithProvider, AIClient,
OpenAIConnector, and setTestLoginConfig) so the connector passed in (oc) is used
as-is; update newMediaTestClient to create an AIClient with the intended
provider and connector and then call setTestLoginConfig(client, cfg) before
returning.

In `@bridges/ai/portal_send.go`:
- Line 139: Return a wrapped error that includes minimal context when
sdk.SendEditViaPortal fails: catch the error returned from the call to
sdk.SendEditViaPortal (the invocation using oc.UserLogin, portal, sender,
targetMsgID, timestamp, streamOrder, "ai_edit_target", converted) and return a
new error that includes the portal identifier and targetMsgID (e.g., via
fmt.Errorf or errors.Wrapf) so the failure message contains portal and target
context for easier triage.

In `@bridges/codex/portal_state_db.go`:
- Around line 136-149: In the rows.Next() loop inside the portal listing code,
don't abort the entire List when json.Unmarshal into codexPortalState fails;
instead, handle malformed state_json on a per-row basis by skipping that row
(continue) and optionally logging the parse error/context. Update the block
where parseCodexPortalKey and json.Unmarshal are used (referencing
parseCodexPortalKey, codexPortalState, and the rows.Next loop) so that a JSON
unmarshal error does not return from the function but is ignored for that portal
entry.
🪄 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: ebf6521c-20e6-4475-88ec-5ff0c18d4ae4

📥 Commits

Reviewing files that changed from the base of the PR and between 3d3f7a7 and 7fbc737.

📒 Files selected for processing (78)
  • bridges/ai/agent_loop_request_builders_test.go
  • bridges/ai/agent_loop_routing_test.go
  • bridges/ai/approval_prompt_presentation.go
  • bridges/ai/bridge_db.go
  • bridges/ai/chat.go
  • bridges/ai/chat_bootstrap_test.go
  • bridges/ai/chat_login_redirect_test.go
  • bridges/ai/chat_resolve_agent_identifier_test.go
  • bridges/ai/client.go
  • bridges/ai/client_capabilities_test.go
  • bridges/ai/custom_agents_db.go
  • bridges/ai/defaults_alignment_test.go
  • bridges/ai/delete_chat.go
  • bridges/ai/desktop_api_sessions.go
  • bridges/ai/desktop_api_sessions_test.go
  • bridges/ai/handleai.go
  • bridges/ai/handlematrix.go
  • bridges/ai/image_generation_tool.go
  • bridges/ai/image_generation_tool_magic_proxy_test.go
  • bridges/ai/integration_host.go
  • bridges/ai/internal_prompt_db.go
  • bridges/ai/login.go
  • bridges/ai/login_config_db.go
  • bridges/ai/login_loaders.go
  • bridges/ai/login_loaders_test.go
  • bridges/ai/login_state_db.go
  • bridges/ai/logout_cleanup.go
  • bridges/ai/magic_proxy_test.go
  • bridges/ai/mcp_helpers.go
  • bridges/ai/mcp_servers_test.go
  • bridges/ai/media_understanding_runner.go
  • bridges/ai/media_understanding_runner_openai_test.go
  • bridges/ai/message_parts.go
  • bridges/ai/metadata.go
  • bridges/ai/model_catalog.go
  • bridges/ai/model_catalog_test.go
  • bridges/ai/portal_send.go
  • bridges/ai/portal_state_db.go
  • bridges/ai/prompt_builder.go
  • bridges/ai/provisioning.go
  • bridges/ai/provisioning_test.go
  • bridges/ai/reply_mentions.go
  • bridges/ai/responder_metadata_test.go
  • bridges/ai/responder_resolution_test.go
  • bridges/ai/scheduler_db.go
  • bridges/ai/scheduler_heartbeat_test.go
  • bridges/ai/sdk_agent_catalog_test.go
  • bridges/ai/session_store.go
  • bridges/ai/streaming_init_test.go
  • bridges/ai/streaming_output_handlers.go
  • bridges/ai/streaming_persistence.go
  • bridges/ai/streaming_tool_selection_test.go
  • bridges/ai/system_events_db.go
  • bridges/ai/test_login_helpers_test.go
  • bridges/ai/token_resolver.go
  • bridges/ai/tool_approvals_rules.go
  • bridges/ai/tool_availability_configured_test.go
  • bridges/ai/tool_configured.go
  • bridges/ai/tool_policy_apply_patch_test.go
  • bridges/ai/tools.go
  • bridges/ai/tools_search_fetch.go
  • bridges/ai/tools_search_fetch_test.go
  • bridges/ai/tools_tts_test.go
  • bridges/ai/transcript_db.go
  • bridges/codex/backfill.go
  • bridges/codex/portal_state_db.go
  • bridges/openclaw/identifiers.go
  • bridges/openclaw/manager.go
  • bridges/openclaw/metadata.go
  • bridges/opencode/opencode_identifiers.go
  • bridges/opencode/opencode_manager.go
  • pkg/aidb/001-init.sql
  • pkg/aidb/db_test.go
  • pkg/aidb/json_blob_table.go
  • pkg/shared/stringutil/hash.go
  • sdk/approval_prompt.go
  • sdk/message_metadata.go
  • turns/session_target_test.go
✅ Files skipped from review due to trivial changes (5)
  • bridges/ai/desktop_api_sessions.go
  • bridges/ai/login.go
  • bridges/ai/login_config_db.go
  • bridges/ai/media_understanding_runner.go
  • bridges/ai/internal_prompt_db.go
🚧 Files skipped from review as they are similar to previous changes (14)
  • bridges/ai/streaming_output_handlers.go
  • bridges/ai/mcp_helpers.go
  • pkg/aidb/db_test.go
  • bridges/ai/sdk_agent_catalog_test.go
  • bridges/ai/model_catalog.go
  • bridges/ai/image_generation_tool.go
  • bridges/ai/login_loaders.go
  • bridges/ai/system_events_db.go
  • bridges/opencode/opencode_identifiers.go
  • bridges/ai/custom_agents_db.go
  • bridges/ai/portal_state_db.go
  • bridges/codex/backfill.go
  • bridges/ai/token_resolver.go
  • bridges/ai/scheduler_db.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: Lint
  • GitHub Check: build-docker
🧰 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/chat_bootstrap_test.go
  • bridges/ai/client_capabilities_test.go
  • bridges/ai/magic_proxy_test.go
  • bridges/ai/tool_configured.go
  • bridges/ai/tool_availability_configured_test.go
  • bridges/ai/streaming_tool_selection_test.go
  • bridges/ai/tool_policy_apply_patch_test.go
  • bridges/ai/defaults_alignment_test.go
  • bridges/ai/tools_tts_test.go
  • bridges/ai/test_login_helpers_test.go
  • bridges/ai/provisioning.go
  • bridges/ai/client.go
  • bridges/ai/handleai.go
  • bridges/ai/tools_search_fetch.go
🪛 GitHub Check: Package Smoke
bridges/ai/transcript_db.go

[failure] 211-211:
cannot use portal.Bridge.Log (variable of struct type zerolog.Logger) as *zerolog.Logger value in argument to execDelete

bridges/ai/handlematrix.go

[failure] 357-357:
too many arguments in call to loadAITranscriptMessage

bridges/ai/client.go

[failure] 1703-1703:
too many arguments in call to loadAITranscriptMessage

🔇 Additional comments (44)
bridges/ai/approval_prompt_presentation.go (3)

6-6: SDK import direction change is clean.

The import swap is straightforward and consistent with the rest of this file’s updated types/helpers.


9-20: Builtin approval presentation flow looks solid.

Trimming at ingress, conditional detail emission, and bounded arg expansion are implemented cleanly, and the final builder call is coherent.


22-37: MCP approval presentation path is well-structured.

The server/tool normalization and input fallback behavior (map details vs summarized value) are clear and robust.

bridges/ai/login_loaders_test.go (3)

4-4: SDK migration changes are consistent and correct.

The updated import and broken-client type assertion align with the direct agentremote/sdk usage and keep this test in sync with the new client type wiring.

Also applies to: 13-13, 70-70


30-33: Rebuild predicate tests are well-updated for config-backed state.

This test now correctly exercises aiClientNeedsRebuildConfig for equivalent config values and all key rebuild triggers (API key, provider, base URL, and nil cached client).

Also applies to: 35-47


61-61: loadAIUserLogin call-site update is correct.

Passing context.Background() matches the new method signature and keeps the behavior-focused assertions intact.

bridges/opencode/opencode_manager.go (11)

17-18: LGTM!

The new imports for stringutil and sdk are appropriately added and used throughout the file for the approval flow migration and hash generation.


27-38: LGTM!

The struct field type updates to sdk.ApprovalFlow[*permissionApprovalRef] and sdk.ApprovalPromptPresentation are consistent with the SDK migration pattern.


40-61: LGTM!

The function correctly builds the approval presentation using SDK types. The pre-allocation with capacity 8 is reasonable for the expected number of detail entries.


94-111: LGTM!

The DeliverDecision callback correctly handles the nil data edge case by returning sdk.ErrApprovalUnknown, and uses sdk.DecisionToString to map decisions to the expected response strings.


265-277: LGTM!

The knownSessions map is properly initialized as a set-like structure to track session IDs. This enables the new session lifecycle tracking via rememberSession and forgetSession calls.


811-822: LGTM!

The SendPrompt call correctly uses the SDK's nested parameter structure. The 10-minute expiration aligns with the approval registration timeout.


849-855: LGTM!

The three-tier fallback logic for resolution origin (ResolvedBy → Source → User) is defensive and handles cases where the origin isn't specified in the external reply event.


874-880: LGTM!

The ResolveExternal call correctly constructs the sdk.ApprovalDecisionPayload to handle permission replies that originated outside the Matrix bridge.


1227-1244: Good scoping improvement.

Changing from listAllChatPortals to listInstanceChatPortals efficiently limits portal enumeration to the affected instance. The InstanceID check at line 1236 is now somewhat redundant but provides a reasonable safety guard.


504-504: Session lifecycle tracking is correctly implemented.

The rememberSession and forgetSession methods are properly defined on openCodeInstance and handle the trimmed session IDs appropriately.


1255-1261: No algorithm change—ShortHash already uses SHA-256 with hex encoding.

stringutil.ShortHash(trimmed, 8) implements SHA-256 + hex encoding of the first n bytes, the same approach the review assumes was the original. This is not a breaking change; if the prior code used the same method, the output is identical.

			> Likely an incorrect or invalid review comment.
bridges/codex/portal_state_db.go (2)

44-116: Good defensive scope handling and persistence guards.

The nil-guarded scope construction and Ensure-before-CRUD flow are clean and reduce accidental panics in this path.


159-172: The suggested fix references a non-existent API and the current implementation is correct.

The networkid.ParsePortalKey() function does not exist in the maunium.net/go/mautrix/bridgev2/networkid package. The current code's approach of using strings.Cut(raw, "/") to split the serialized string is the canonical method for parsing PortalKey.String() output, which produces either "{ID}/{Receiver}" or "{ID}" format.

Additionally, portal IDs in this codebase are designed with formats like "codex:loginid:type:identifier" and "service:loginid:sessionid", which do not contain "/" characters, making the "/" delimiter safe and appropriate for round-trip serialization. The parsing logic is correct and does not suffer from the data loss concern described.

			> Likely an incorrect or invalid review comment.
bridges/ai/metadata.go (1)

310-313: Nice deep-copy for TypingIntervalSeconds.

This removes a remaining pointer alias from clonePortalMetadata, so later runtime edits won't mutate previously captured snapshots.

bridges/ai/reply_mentions.go (1)

71-76: Nice switch to the portal-aware part loader.

This keeps reply detection aligned with the scoped message-part path instead of reaching around it with a direct message-table lookup.

bridges/ai/portal_send.go (1)

188-188: Portal-scoped MXID lookup is a solid fix.

Using loadPortalMessagePartByMXID on Line 188 is a good correctness improvement over global lookup.

bridges/ai/provisioning_test.go (1)

14-56: Nice update to config-backed test coverage.

The rewritten assertions validate both profile lifecycle and timezone persistence/clearing against aiLoginConfig cleanly.

bridges/ai/chat_bootstrap_test.go (1)

11-38: Type migration in table tests looks consistent.

shouldEnsureDefaultChat inputs and cases now correctly track *aiLoginConfig.

bridges/ai/magic_proxy_test.go (2)

40-47: Good signature migration for service config resolution.

Passing ProviderMagicProxy + login config directly keeps these tests aligned with the new resolver contract.


99-104: Exa proxy resolution test is correctly adapted.

Line 104 now validates the login-config-driven base URL path as intended.

bridges/ai/tool_policy_apply_patch_test.go (1)

6-13: Helper refactor improves test fixture consistency.

Using shared client/login-state setup here is clean and matches the rest of the suite.

bridges/ai/streaming_tool_selection_test.go (1)

26-49: Refactored test client bootstrap looks solid.

The explicit runtime-state setup keeps tool-selection tests deterministic and readable.

bridges/ai/client_capabilities_test.go (2)

15-19: Good move to shared AIClient/login-state helpers.

This keeps capability tests aligned with the new runtime-state model.


112-112: Using NewAIConnector() here is the right call.

Line 112 ensures the test validates capabilities against constructor defaults, not a partial literal.

bridges/ai/agent_loop_routing_test.go (1)

6-10: Helper rewrite is clean and consistent.

The shared setup pattern makes model-routing tests easier to maintain.

bridges/ai/image_generation_tool_magic_proxy_test.go (1)

8-13: Shared Magic Proxy fixture migration looks good.

These cases still cover the same provider/base-URL behavior while moving setup onto aiLoginConfig, which keeps the tests aligned with the new login config/state flow.

Also applies to: 28-33, 48-53, 69-74, 90-95, 129-134, 146-151

bridges/ai/tool_availability_configured_test.go (1)

17-27: Helper-based state setup is a solid cleanup.

Using the shared client/login-state helpers keeps these availability tests focused on tool gating instead of low-level fixture wiring.

Also applies to: 43-55, 65-77, 90-94

bridges/ai/desktop_api_sessions_test.go (1)

6-16: Nice simplification of the desktop API fixture.

This keeps the test scoped to the token merge behavior and removes a lot of incidental login wiring.

bridges/ai/mcp_servers_test.go (1)

6-10: The MCP test helper cleanup looks good.

Injecting MCP server config through the shared login helper makes the fixture easier to reuse and keeps it consistent with the rest of the suite.

bridges/ai/defaults_alignment_test.go (1)

17-23: These defaults-alignment fixtures are in better shape now.

Using the DB-backed/custom-agent helpers here makes the tests closer to the paths the production code actually reads from.

Also applies to: 38-44, 59-65

bridges/ai/agent_loop_request_builders_test.go (1)

13-25: The request-builder tests are cleaner with the shared helpers.

The new setup keeps the assertions on the builder outputs while avoiding duplicated login metadata scaffolding.

Also applies to: 62-80

bridges/ai/tools_search_fetch_test.go (1)

11-22: The updated search-config call sites look right.

Passing the provider explicitly makes these tests clearer about which branch they are exercising, and the important token-handling assertions are still intact.

Also applies to: 49-49, 61-74

bridges/ai/responder_resolution_test.go (1)

9-19: Nice test-helper migration.

These setups are much less brittle now, and the assertions still exercise the responder-resolution behavior rather than bridge fixture wiring.

Also applies to: 43-55, 95-102

bridges/ai/tools_tts_test.go (1)

5-8: Helper cleanup looks good.

This keeps the provider/login-config setup centralized while preserving the important fallback and no-connector cases.

Also applies to: 13-15, 28-30, 50-50, 62-62

bridges/ai/message_parts.go (1)

12-30: Good portal-scoped guard.

Verifying the fetched part against portal.PortalKey is the right safety check for the new shared lookup helper.

bridges/ai/chat_login_redirect_test.go (1)

38-54: This test setup is much cleaner now.

Using the shared login-state/config helpers keeps these cases focused on search and redirect behavior instead of metadata plumbing.

Also applies to: 84-85, 169-177

bridges/ai/scheduler_heartbeat_test.go (1)

58-62: Nice alignment with the new runtime/schema wiring.

Using EnsureSchema, the shared login-config helper, and the table constant should make these tests less fragile against future schema churn.

Also applies to: 84-84, 118-137

bridges/ai/streaming_persistence.go (1)

120-158: Good persistence backstop.

Deriving a fallback message ID and persisting the transcript separately should make completed assistant turns more resilient across the different send paths.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

♻️ Duplicate comments (5)
bridges/openclaw/manager.go (2)

1959-1964: ⚠️ Potential issue | 🟠 Major

This filter still makes the timestamp fallback unreachable.

Skipping every candidate that lacks turn/run/idempotency markers means shouldMirrorLatestUserMessageFromHistory never reaches its 15-minute timestamp fallback for gateways that omit those markers. Let the helper inspect marker-less messages instead of continuing here.

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

In `@bridges/openclaw/manager.go` around lines 1959 - 1964, The loop currently
skips any normalized message that lacks turn/run/idempotency markers, which
prevents shouldMirrorLatestUserMessageFromHistory from applying its 15-minute
timestamp fallback; remove the early continue so that
normalizeOpenClawLiveMessage(payload.TS, history.Messages[idx]) results are
always passed into shouldMirrorLatestUserMessageFromHistory and let that helper
decide whether to mirror marker-less messages (i.e., stop filtering by
openClawMessageTurnMarker/openClawMessageRunMarker/openClawMessageIdempotencyKey
in the loop and call shouldMirrorLatestUserMessageFromHistory for every
message).

594-617: ⚠️ Potential issue | 🟠 Major

Persist or synchronously resync the real session before the first send.

When a contact DM resolves from a synthetic key to a real gateway session, only the local sessionKey is updated here. The portal still points at the synthetic session until the async syncSessions finishes, so the first chat/approval event on the real key can arrive before the room is remapped and get dropped. Update/save the resolved key before SendMessage, or make this first resync synchronous.

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

In `@bridges/openclaw/manager.go` around lines 594 - 617, When ResolveSessionKey
returns a real session for a synthetic contact key, don’t only update the local
sessionKey variable — persist or synchronously remap the portal before sending
so the first message isn’t dropped. Modify the block that calls
gateway.ResolveSessionKey (checking state.OpenClawDMCreatedFromContact,
state.OpenClawSessionID and isOpenClawSyntheticDMSessionKey) to either (a)
assign the resolvedKey back into state.OpenClawSessionKey and persist that state
using the existing contact/state persistence routine before calling
gateway.SendMessage, or (b) call m.syncSessions synchronously (wait for it to
complete) before gateway.SendMessage so the portal is remapped; keep the
ResolveSessionKey and gateway.SendMessage calls but ensure state persistence or
synchronous m.syncSessions happens first.
bridges/ai/bridge_db.go (1)

112-117: ⚠️ Potential issue | 🟡 Minor

Trim bridgeID and loginID here too.

This path still builds a scope from raw loginDBContext values, so whitespace-padded IDs won't match scopes created by loginScopeForLogin / portalScopeForPortal.

Suggested fix
 func loginScopeForClient(client *AIClient) *loginScope {
 	db, bridgeID, loginID := loginDBContext(client)
+	bridgeID = strings.TrimSpace(bridgeID)
+	loginID = strings.TrimSpace(loginID)
 	if db == nil || 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 112 - 117, loginScopeForClient
currently builds a scope from raw loginDBContext values so whitespace-padded IDs
can mismatch scopes created by loginScopeForLogin or portalScopeForPortal; call
strings.TrimSpace (or equivalent trimming) on the bridgeID and loginID returned
from loginDBContext, validate the trimmed values (db != nil && bridgeID != "" &&
loginID != ""), and construct the returned &loginScope using the trimmed
bridgeID and loginID so IDs are normalized consistent with
loginScopeForLogin/portalScopeForPortal.
bridges/ai/handleai.go (2)

317-319: ⚠️ Potential issue | 🟠 Major

Persist the one-shot flags before sending the message.

These paths still keep going if savePortalQuiet fails. That can leave AutoGreetingSent / WelcomeSent false in storage even though the greeting/notice was already sent, so retries or a restart can emit duplicates.

Also applies to: 385-389

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

In `@bridges/ai/handleai.go` around lines 317 - 319, Persist the one-shot flags
before sending the message: when setting currentMeta.AutoGreetingSent (and
similarly WelcomeSent) call oc.savePortalQuiet(bgCtx, current, "...") and if
that save returns an error, stop and return the error instead of proceeding to
oc.dispatchInternalMessage; ensure the code checks the error from
savePortalQuiet and only calls oc.dispatchInternalMessage when persistence
succeeded so the flags in storage always reflect sent status (apply same change
in the AutoGreetingSent block around dispatchInternalMessage and the WelcomeSent
block at the 385-389 range).

454-461: ⚠️ Potential issue | 🟠 Major

Sync the generated title to Matrix after saving it.

This only persists portal.Name / portal.NameSet; it doesn't invoke the room-lifecycle sync that actually propagates the rename to the Matrix room, so the DB and room UI can drift.

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

In `@bridges/ai/handleai.go` around lines 454 - 461, The code sets portal.Name and
portal.NameSet and calls oc.savePortalQuiet(portal, "room title") but never
triggers the room-lifecycle sync that propagates the rename to Matrix; after the
save, call the existing room-lifecycle sync mechanism (the same routine used
elsewhere to push portal changes to Matrix) so the generated title is sent to
the Matrix room UI — locate the block around portalMeta(portal) and
oc.savePortalQuiet and add the post-save call to the room sync routine to ensure
DB and Matrix stay in sync.
🧹 Nitpick comments (2)
pkg/agents/tools/params.go (2)

44-48: Silent truncation when converting float to int.

int(n) truncates decimals silently (e.g., 3.93). If tool parameters could contain decimal values that should be rejected or rounded, consider validating. If whole numbers are always expected, this is fine as-is.

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

In `@pkg/agents/tools/params.go` around lines 44 - 48, ReadInt currently casts the
float returned by ReadNumber to int which silently truncates decimals; update
ReadInt(params map[string]any, key string, required bool) to validate that the
returned float64 from ReadNumber is an integer (e.g., compare n to math.Trunc(n)
or use math.Modf) and return a clear error if it has a fractional part, only
then convert to int and return; keep using ReadNumber as the source of the
numeric value and include the param key in the error message for clarity.

60-70: Inconsistent error handling compared to ReadString and ReadMap.

When required=true, this function always returns "parameter %q is required" even when the key exists but has the wrong type. ReadString and ReadMap distinguish between missing/nil and wrong-type cases with different error messages.

For consistency, consider adding the same check:

Proposed fix
 func ReadStringSlice(params map[string]any, key string, required bool) ([]string, error) {
 	arr := maputil.StringSliceArg(params, key)
 	if arr != nil {
 		return arr, nil
 	}
 	if required {
+		if _, ok := params[key]; !ok || params[key] == nil {
+			return nil, fmt.Errorf("parameter %q is required", key)
+		}
+		return nil, fmt.Errorf("parameter %q must be a string array", key)
-		return nil, fmt.Errorf("parameter %q is required", key)
 	}
 	return nil, nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/agents/tools/params.go` around lines 60 - 70, ReadStringSlice currently
returns the generic "parameter %q is required" when required=true even if the
key exists but has the wrong type; update ReadStringSlice (and its use of
maputil.StringSliceArg) to mimic ReadString/ReadMap: first check params[key]
presence (v, ok := params[key]); if v is present but maputil.StringSliceArg
returns nil, return a type error such as fmt.Errorf("parameter %q must be an
array of strings", key); only return the "required" missing error when the key
is absent or nil and required==true, otherwise return nil,nil for optional
missing keys.
🤖 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/client.go`:
- Around line 648-670: updateBridgeMessageMetadata currently overwrites
existing.Metadata with an empty MessageMetadata{} instead of applying the
computed meta; change it to set the found row's Metadata to the passed-in meta
(or a defensive copy of meta) before calling
oc.UserLogin.Bridge.DB.Message.Update so the caller's metadata (including
GeneratedFiles overlay) is persisted; specifically update existing.Metadata =
meta (or clone meta) in the updateBridgeMessageMetadata function prior to the DB
update.

In `@bridges/ai/handlematrix.go`:
- Around line 384-390: The code is zeroing out the bridge message metadata by
assigning edit.EditTarget.Metadata = &MessageMetadata{} before writing back;
instead preserve or merge the edited transcript metadata into the bridge row so
the edited body/role/canonical fields are mirrored. Remove the unconditional
overwrite of edit.EditTarget.Metadata with an empty MessageMetadata and either
leave edit.EditTarget.Metadata as supplied or merge its non-nil fields into the
existing bridge metadata before calling
oc.UserLogin.Bridge.DB.Message.Update(ctx, edit.EditTarget), referencing
edit.EditTarget, MessageMetadata and oc.UserLogin.Bridge.DB.Message.Update to
locate the change.

In `@bridges/ai/internal_prompt_db.go`:
- Around line 118-120: When DecodeTurnData(raw) returns ok==false, we should log
a warning instead of silently continuing: add a warning log inside the loop that
includes the event identifiers (event_id and portal_id from the current
row/context) and any minimal context available (e.g., raw/canonical_turn_data or
a digest) so corrupted rows are debuggable; modify the block around
DecodeTurnData/raw/turnData to call the existing logger in scope (e.g., logger,
log, or s.log) with a message like "skipping malformed canonical_turn_data" plus
event_id and portal_id before continuing.

In `@bridges/openclaw/manager.go`:
- Around line 1804-1807: If loadOpenClawPortalState(ctx, portal,
m.client.UserLogin) returns an error you must not return early; instead fall
back to an empty/default state for rendering and still call ResolveExternal(...)
and clearApprovalHint(...) to drop the local approval entry. Concretely: call
loadOpenClawPortalState and if it errors, log or capture the error, set state to
an empty/default OpenClawPortalState value, then proceed to call the existing
ResolveExternal and clearApprovalHint code paths (using portal and
m.client.UserLogin as before) so the Matrix-side approval is always
resolved/dropped even when state loading fails.
- Around line 2365-2374: startRunRecovery currently captures the full
*openClawPortalState and passes it into a goroutine, which can cause stale state
to be persisted later by recoverRunPreview/saveOpenClawPortalState; instead,
change startRunRecovery to send only immutable identifiers (e.g., portal.MXID,
turnID, runID, agentID) into the goroutine and have
waitForRunCompletion/recoverRunPreview reload the latest openClawPortalState
from storage (by calling whatever load method is used for portal state)
immediately before any mutation or before calling saveOpenClawPortalState so you
always persist a fresh snapshot; apply the same pattern to the similar usage
referenced around recoverRunPreview (the other occurrence noted).

In `@pkg/agents/tools/params.go`:
- Around line 12-22: The current logic treats an intentionally provided empty
string as a non-string and emits "parameter %q must be a string"; update the
required-param handling so an empty string is recognized as a string but
rejected with a clear message. In the block using s := maputil.StringArg(params,
key) and variables params, key, required, after s != "" check, if required and
params contains the key but s == "" return fmt.Errorf("parameter %q is required
and cannot be empty"), otherwise keep the existing missing-key error and the
"must be a string" error for non-string cases.

In `@pkg/aidb/001-init.sql`:
- Around line 202-205: Several *_json columns (e.g., last_heartbeat_event_json,
model_cache_json, gravatar_json, file_annotation_cache_json and the other *_json
columns noted) use DEFAULT '' which is not valid JSON; change each DEFAULT '' to
a valid JSON literal that matches the column's expected shape (use DEFAULT '{}'
for JSON objects and DEFAULT '[]' for JSON arrays), update the column
definitions (the *_json columns in this file) accordingly, and adjust any
INSERT/seed SQL that relies on empty-string defaults to instead rely on the new
valid JSON defaults.

---

Duplicate comments:
In `@bridges/ai/bridge_db.go`:
- Around line 112-117: loginScopeForClient currently builds a scope from raw
loginDBContext values so whitespace-padded IDs can mismatch scopes created by
loginScopeForLogin or portalScopeForPortal; call strings.TrimSpace (or
equivalent trimming) on the bridgeID and loginID returned from loginDBContext,
validate the trimmed values (db != nil && bridgeID != "" && loginID != ""), and
construct the returned &loginScope using the trimmed bridgeID and loginID so IDs
are normalized consistent with loginScopeForLogin/portalScopeForPortal.

In `@bridges/ai/handleai.go`:
- Around line 317-319: Persist the one-shot flags before sending the message:
when setting currentMeta.AutoGreetingSent (and similarly WelcomeSent) call
oc.savePortalQuiet(bgCtx, current, "...") and if that save returns an error,
stop and return the error instead of proceeding to oc.dispatchInternalMessage;
ensure the code checks the error from savePortalQuiet and only calls
oc.dispatchInternalMessage when persistence succeeded so the flags in storage
always reflect sent status (apply same change in the AutoGreetingSent block
around dispatchInternalMessage and the WelcomeSent block at the 385-389 range).
- Around line 454-461: The code sets portal.Name and portal.NameSet and calls
oc.savePortalQuiet(portal, "room title") but never triggers the room-lifecycle
sync that propagates the rename to Matrix; after the save, call the existing
room-lifecycle sync mechanism (the same routine used elsewhere to push portal
changes to Matrix) so the generated title is sent to the Matrix room UI — locate
the block around portalMeta(portal) and oc.savePortalQuiet and add the post-save
call to the room sync routine to ensure DB and Matrix stay in sync.

In `@bridges/openclaw/manager.go`:
- Around line 1959-1964: The loop currently skips any normalized message that
lacks turn/run/idempotency markers, which prevents
shouldMirrorLatestUserMessageFromHistory from applying its 15-minute timestamp
fallback; remove the early continue so that
normalizeOpenClawLiveMessage(payload.TS, history.Messages[idx]) results are
always passed into shouldMirrorLatestUserMessageFromHistory and let that helper
decide whether to mirror marker-less messages (i.e., stop filtering by
openClawMessageTurnMarker/openClawMessageRunMarker/openClawMessageIdempotencyKey
in the loop and call shouldMirrorLatestUserMessageFromHistory for every
message).
- Around line 594-617: When ResolveSessionKey returns a real session for a
synthetic contact key, don’t only update the local sessionKey variable — persist
or synchronously remap the portal before sending so the first message isn’t
dropped. Modify the block that calls gateway.ResolveSessionKey (checking
state.OpenClawDMCreatedFromContact, state.OpenClawSessionID and
isOpenClawSyntheticDMSessionKey) to either (a) assign the resolvedKey back into
state.OpenClawSessionKey and persist that state using the existing contact/state
persistence routine before calling gateway.SendMessage, or (b) call
m.syncSessions synchronously (wait for it to complete) before
gateway.SendMessage so the portal is remapped; keep the ResolveSessionKey and
gateway.SendMessage calls but ensure state persistence or synchronous
m.syncSessions happens first.

---

Nitpick comments:
In `@pkg/agents/tools/params.go`:
- Around line 44-48: ReadInt currently casts the float returned by ReadNumber to
int which silently truncates decimals; update ReadInt(params map[string]any, key
string, required bool) to validate that the returned float64 from ReadNumber is
an integer (e.g., compare n to math.Trunc(n) or use math.Modf) and return a
clear error if it has a fractional part, only then convert to int and return;
keep using ReadNumber as the source of the numeric value and include the param
key in the error message for clarity.
- Around line 60-70: ReadStringSlice currently returns the generic "parameter %q
is required" when required=true even if the key exists but has the wrong type;
update ReadStringSlice (and its use of maputil.StringSliceArg) to mimic
ReadString/ReadMap: first check params[key] presence (v, ok := params[key]); if
v is present but maputil.StringSliceArg returns nil, return a type error such as
fmt.Errorf("parameter %q must be an array of strings", key); only return the
"required" missing error when the key is absent or nil and required==true,
otherwise return nil,nil for optional missing keys.
🪄 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: 977baf40-acc9-4e24-b111-8baed5fec90e

📥 Commits

Reviewing files that changed from the base of the PR and between 7fbc737 and 22ded85.

📒 Files selected for processing (31)
  • bridges/ai/bridge_db.go
  • bridges/ai/client.go
  • bridges/ai/delete_chat.go
  • bridges/ai/handleai.go
  • bridges/ai/handlematrix.go
  • bridges/ai/internal_dispatch.go
  • bridges/ai/internal_prompt_db.go
  • bridges/ai/message_parts.go
  • bridges/ai/portal_cleanup.go
  • bridges/ai/prompt_builder.go
  • bridges/ai/reaction_handling.go
  • bridges/ai/reactions.go
  • bridges/ai/streaming_persistence.go
  • bridges/ai/subagent_spawn.go
  • bridges/ai/transcript_db.go
  • bridges/openclaw/manager.go
  • bridges/opencode/opencode_manager.go
  • pkg/agents/store.go
  • pkg/agents/tools/boss.go
  • pkg/agents/tools/params.go
  • pkg/agents/tools/types.go
  • pkg/aidb/001-init.sql
  • pkg/integrations/cron/tool_exec.go
  • pkg/shared/maputil/bool_arg.go
  • pkg/shared/maputil/number_arg.go
  • pkg/shared/maputil/slice_arg.go
  • pkg/shared/maputil/string_arg.go
  • pkg/shared/toolspec/toolspec.go
  • sdk/approval_flow.go
  • sdk/approval_reaction_helpers.go
  • sdk/helpers.go
✅ Files skipped from review due to trivial changes (2)
  • pkg/agents/store.go
  • pkg/agents/tools/boss.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • bridges/ai/portal_cleanup.go
  • bridges/ai/internal_dispatch.go
  • bridges/ai/subagent_spawn.go
  • bridges/ai/reaction_handling.go
  • bridges/ai/reactions.go
  • bridges/ai/transcript_db.go
  • bridges/ai/prompt_builder.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). (6)
  • GitHub Check: build-agentremote-cli-docker (arm64)
  • GitHub Check: build-agentremote-cli-docker (amd64)
  • GitHub Check: Lint
  • GitHub Check: build-docker
  • GitHub Check: Lint
  • GitHub Check: build-docker
🧰 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/handleai.go
  • bridges/ai/client.go
🔇 Additional comments (25)
pkg/agents/tools/types.go (2)

10-17: Backwards-compatible type aliasing looks correct.

Using aliases here cleanly preserves existing tools.ToolType / tools.ToolInfo call sites while moving the canonical definitions to pkg/shared/toolspec.


19-23: Constant re-exports are aligned with the new shared source of truth.

The mapping keeps existing comparisons (e.g., plugin-type checks) intact while avoiding duplicated enum definitions.

bridges/opencode/opencode_manager.go (11)

17-18: LGTM!

The import changes correctly bring in the new SDK package and the stringutil helper, aligning with the migration from the prior github.com/beeper/agentremote module types.


27-38: LGTM!

The type changes for approvalFlow and Presentation fields correctly use the new SDK types while maintaining the generic parameter for permissionApprovalRef.


40-53: LGTM!

The function correctly uses SDK helpers (ApprovalDetail, ValueSummary, AppendDetailsFromMap, BuildApprovalPresentation) to construct the approval presentation. The logic flow for extracting permission, patterns, and metadata is sound.


60-111: LGTM!

The approval flow configuration correctly uses SDK types. The DeliverDecision callback properly handles nil refs by returning sdk.ErrApprovalUnknown, and uses sdk.DecisionToString with appropriate OpenCode-specific response strings ("once", "always", "reject").


263-263: LGTM!

Pre-initializing knownSessions during instance construction is a good practice. While rememberSession has a defensive nil check (per the context snippet), eager initialization here avoids potential race conditions during concurrent access.


496-496: LGTM!

Correctly tracks session IDs during sync using rememberSession with trimmed session ID.


642-642: LGTM!

Properly removes session from tracking during deletion using forgetSession with trimmed session ID.


803-814: LGTM!

The SendPrompt call correctly uses SDK param types. The 10-minute expiration matches the registration timeout set on line 773, ensuring consistency.


841-872: LGTM!

The resolution handling correctly parses the origin with a proper fallback chain (ResolvedBySourceApprovalResolutionOriginUser). The ResolveExternal call properly constructs the ApprovalDecisionPayload with approval state derived from the reply.


1219-1219: LGTM!

Narrowing the query to listInstanceChatPortals is a good improvement. While the subsequent filter on line 1228 would catch mismatches, querying only relevant portals is more efficient and makes the intent clearer.


1247-1253: LGTM!

Using stringutil.ShortHash simplifies the code by leveraging a shared utility. The function uses SHA-256 for deterministic hashing, taking 8 bytes and encoding as hex (16 characters), prefixed with "msg_mx_" to identify Matrix-originated messages. This provides reasonable uniqueness for session-scoped message IDs.

pkg/agents/tools/params.go (6)

25-28: LGTM!

Clean delegation to the shared helper.


30-42: LGTM!

Error handling properly distinguishes missing vs wrong-type cases.


50-53: LGTM!


55-58: LGTM!


72-75: LGTM!


77-90: LGTM!

Proper distinction between missing/nil and wrong-type errors. Good consistency with ReadString.

bridges/ai/delete_chat.go (1)

62-90: Verify deleted-portal login references are still cleaned up.

I don't see an equivalent to the old deleted-portal reference cleanup in this path anymore. If DefaultChatPortalID / LastActiveRoomByAgent still exist anywhere, deleting a chat can leave stale routing pointers behind.

Run this to confirm whether those fields still exist and whether any delete-chat cleanup still clears them:

#!/bin/bash
set -euo pipefail

printf '== remaining login-scoped portal reference fields ==\n'
rg -n -C2 'DefaultChatPortalID|LastActiveRoomByAgent' bridges/ai

printf '\n== delete-chat cleanup path ==\n'
sed -n '1,140p' bridges/ai/delete_chat.go

printf '\n== any remaining cleanup helpers for deleted portal references ==\n'
rg -n -C2 'forgetDeletedPortalReferences|DefaultChatPortalID.*=|LastActiveRoomByAgent.*=|clear.*portal.*reference' bridges/ai

If the first search still finds live fields/usages and the third search finds no cleanup, this flow still leaves stale references behind.

bridges/ai/streaming_persistence.go (1)

134-160: Nice separation of transport persistence vs. AI transcript persistence.

Keeping transcript persistence best-effort here is a good fit for the live send path, and the warning logs preserve visibility when the overlay write fails.

bridges/ai/message_parts.go (1)

15-54: Good defensive lookup helpers.

Returning (nil, nil) for missing prerequisites and missing rows keeps the callers simple and lets the reaction/edit flows degrade safely.

Also applies to: 56-97

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

70-73: content_hash session tracking looks correct and cleaner.

This segment is straightforward and consistent with the new state model.


183-197: aichats_internal_messages key/index design is solid.

Primary key and history index align well for portal-scoped dedupe + time-ordered retrieval.


238-251: New lookup indexes are well targeted for scoped reads.

These additions match the new (bridge_id, login_id, ...) access pattern and should help keep hot-path queries efficient.

Also applies to: 272-276, 291-292

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
bridges/ai/transcript_db.go (1)

175-176: Avoid failing the full history load on one malformed row.

A single bad metadata_json currently aborts all history retrieval for this portal. Prefer skipping corrupt rows (and logging upstream) to preserve partial results.

Proposed fix
-		if err = json.Unmarshal([]byte(metadataRaw), &meta); err != nil {
-			return nil, err
-		}
+		if err = json.Unmarshal([]byte(metadataRaw), &meta); err != nil {
+			// TODO: log malformed row context (messageID) via scoped logger.
+			continue
+		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/transcript_db.go` around lines 175 - 176, The json.Unmarshal error
for metadataRaw currently returns the error and aborts the whole history load;
instead, catch json.Unmarshal errors (on metadataRaw -> meta) inside the loop in
the function that reads rows (using variables metadataRaw and meta), log the
parsing error and the offending metadataRaw (or row id) via the existing logger,
skip that row (continue) and proceed to accumulate remaining results so one
malformed metadata_json doesn't fail the entire load.
🤖 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/transcript_db.go`:
- Around line 90-95: The UPSERT's DO UPDATE currently overwrites created_at_ms
on conflict which mutates original creation timestamps; modify the ON CONFLICT
... DO UPDATE SET clause in the transcript persistence SQL so it does not assign
created_at_ms (only update event_id, sender_id, metadata_json, updated_at_ms,
etc.), leaving created_at_ms immutable during updates to preserve original
chronology.
- Around line 82-85: The createdAt assignment incorrectly checks for zero by
comparing UnixMilli() == 0; instead detect zero-time using
msg.Timestamp.IsZero() before calling UnixMilli(). Change the logic around
createdAt and msg.Timestamp so you first if msg.Timestamp.IsZero() then set
createdAt = time.Now().UnixMilli(), otherwise set createdAt =
msg.Timestamp.UnixMilli(); update references to the createdAt variable
accordingly.

---

Nitpick comments:
In `@bridges/ai/transcript_db.go`:
- Around line 175-176: The json.Unmarshal error for metadataRaw currently
returns the error and aborts the whole history load; instead, catch
json.Unmarshal errors (on metadataRaw -> meta) inside the loop in the function
that reads rows (using variables metadataRaw and meta), log the parsing error
and the offending metadataRaw (or row id) via the existing logger, skip that row
(continue) and proceed to accumulate remaining results so one malformed
metadata_json doesn't fail the entire load.
🪄 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: 66a0c299-29bb-4294-b724-5289d380dc58

📥 Commits

Reviewing files that changed from the base of the PR and between 22ded85 and 57c5e9b.

📒 Files selected for processing (5)
  • bridges/ai/custom_agents_db.go
  • bridges/ai/login_loaders.go
  • bridges/ai/transcript_db.go
  • sdk/approval_flow.go
  • sdk/approval_reaction_helpers_test.go
✅ Files skipped from review due to trivial changes (1)
  • bridges/ai/custom_agents_db.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • bridges/ai/login_loaders.go
📜 Review details
🔇 Additional comments (1)
bridges/ai/transcript_db.go (1)

16-67: Clone helpers look robust and side-effect-safe.

The cloning path for canonical turn data, metadata, and message wrappers is well-defensive and reduces shared-state mutation risk in AI history flows.

batuhan added 7 commits April 12, 2026 17:25
Validate and report bridge connection state and improve room meta handling.

- AIClient: mark connecting, validate provider via ListModels with timeout, handle auth failures by emitting BadCredentials, and defer non-auth errors while logging; only set logged-in on success.
- OpenCodeClient: emit connecting state, restore connections asynchronously, emit transient disconnect when restores fail or no instances reachable, add hasReachableOpenCodeInstance helper, and set logged-in only after successful restore.
- Bridgev2 room meta handlers: validate incoming Matrix room name/topic/avatar messages, apply content to portal fields, add validateRoomMetaMessage helper, and update unit tests to supply content and assert in-memory changes.
- Connector: remove hardcoded GetCapabilities override for OpenCode.
- SDK: make GetCapabilities context-aware (accept ctx), call currentRoomFeatures with ctx; remove automatic Custom override in convertRoomFeatures; small doc/comment wording adjustments and minor refactors.

These changes improve error reporting, make connection logic more robust, and ensure bridgev2 room metadata is validated and persisted in memory before saving.
batuhan added 3 commits April 12, 2026 19:27
Multiple cleanups and safety improvements across bridges and sdk:

- bridges/ai: clear stale AI session routing when deleting sessions and add defensive ctx handling; use timestamp.IsZero() when persisting transcripts and remove an unused helper.
- bridges/codex: apply copy-on-write for portal state updates (nextState) to avoid persisting partial state on failure and adjust thread startup flow.
- bridges/openclaw: add legacy openClawLegacyLoginState and a migration helper to load/save legacy metadata into the new persisted state; stop trimming token strings when saving.
- pkg/integrations/memory: ensure StopManagersForLogin is invoked before DB checks to avoid leaving managers running.
- pkg/shared/toolspec: remove an unnecessary "remove": true constraint from the message schema.
- pkg/agents/tools: remove several redundant parameter helper functions (ReadIntDefault, ReadBool, ReadStringSlice).
- sdk: remove commands() accessors and an unused login helper to reduce API surface.

These changes improve robustness (prevent partial state changes, clear stale routing), add backward compatibility for OpenClaw metadata, and simplify the codebase by removing unused/helpers.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

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

⚠️ Outside diff range comments (3)
bridges/ai/sessions_tools.go (1)

572-581: ⚠️ Potential issue | 🟠 Major

Use messages[0] instead of messages[len(messages)-1].

The SQL query at bridges/ai/transcript_db.go:147 orders results by created_at_ms DESC, meaning getAIHistoryMessages returns messages with the most recent first. However, messages[len(messages)-1] accesses the last element, which is the oldest message. Since lastMessageTimestamp is used to filter sessions by recent activity (line 92 checks updatedAt < cutoff), this should return the most recent timestamp using messages[0].

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

In `@bridges/ai/sessions_tools.go` around lines 572 - 581, In
lastMessageTimestamp, getAIHistoryMessages returns messages ordered
newest-first, so replace the oldest-element access (messages[len(messages)-1])
with the first element (messages[0]) to return the most recent timestamp; update
the return expression in the AIClient.lastMessageTimestamp function to use
messages[0].Timestamp.UnixMilli() and keep the existing nil/empty checks around
getAIHistoryMessages.
bridges/ai/handleai.go (1)

217-249: ⚠️ Potential issue | 🟠 Major

Don't treat missing transcript rows as an empty chat.

This check now only consults AI transcript history plus internal prompts. Existing rooms that still have conversation history only in the bridge message table will look empty here, so auto-greetings can fire in chats that already contain user/assistant turns. Keep a bridge-history fallback when transcript history is empty, or gate this path behind transcript backfill.

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

In `@bridges/ai/handleai.go` around lines 217 - 249, The current
conversation-start check (involving oc.getAIHistoryMessages, MessageMetadata and
hasInternalPromptHistory) treats a missing AI transcript as empty and can
trigger auto-greetings despite existing bridge-only history; modify the logic to
fall back to checking bridge message history when getAIHistoryMessages returns
an empty slice (or no user/assistant turns) instead of immediately returning
false — e.g., after exhausting the transcript loop and before calling
hasInternalPromptHistory, call the bridge-history lookup (the same table used
for bridge events/messages) and only treat the chat as empty if both transcript
AND bridge-history checks show no user/assistant messages; keep the existing
metadata/role/exclude filters when inspecting the bridge messages so behavior
remains consistent.
bridges/ai/handlematrix.go (1)

428-455: ⚠️ Potential issue | 🟠 Major

Don't rely on transcript-only history for edit regeneration.

oc.getAIHistoryMessages only reads aichats_transcript_messages. In an upgraded room where older turns still exist only in the bridge message table, editedMessage can be found but the following assistant reply will not, so assistantResponse stays nil, the stale reply is never redacted, and regeneration posts a second answer instead of replacing the old one. Keep a bridge-history fallback here until transcript backfill is guaranteed.

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

In `@bridges/ai/handlematrix.go` around lines 428 - 455, The history lookup
currently relies only on oc.getAIHistoryMessages which reads
aichats_transcript_messages, so when the assistant reply lives only in the
bridge table the search for assistantResponse (using editedIdx and messageMeta)
can miss it; update the logic in the block around
oc.getAIHistoryMessages/editedIdx/assistantResponse to fall back to fetching
bridge history (e.g., via an existing bridge-history reader or a new
oc.getBridgeHistoryMessages function) when assistantResponse remains nil: merge
or search the bridge messages with the same newest-first ordering and the same
messageMeta checks to locate the assistant reply, handle errors from the
fallback fetch, and keep the rest of the redaction/regeneration flow unchanged.
♻️ Duplicate comments (2)
bridges/ai/transcript_db.go (1)

82-85: ⚠️ Potential issue | 🟠 Major

Use Timestamp.IsZero() before calling UnixMilli().

time.Time{} does not yield 0 here, so this fallback never runs for uninitialized timestamps and transcript chronology can be skewed badly.

In Go, what does time.Time{}.UnixMilli() return? Is it 0 or a negative value?
Suggested fix
-	createdAt := msg.Timestamp.UnixMilli()
-	if createdAt == 0 {
-		createdAt = time.Now().UnixMilli()
-	}
+	var createdAt int64
+	if msg.Timestamp.IsZero() {
+		createdAt = time.Now().UnixMilli()
+	} else {
+		createdAt = msg.Timestamp.UnixMilli()
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/transcript_db.go` around lines 82 - 85, The current logic uses
msg.Timestamp.UnixMilli() and checks for zero, but time.Time{} does not produce
0 via UnixMilli so the fallback never triggers; change the code to first test
msg.Timestamp.IsZero() and if true set createdAt = time.Now().UnixMilli(),
otherwise set createdAt = msg.Timestamp.UnixMilli(); update the block around
createdAt and msg.Timestamp in transcript_db.go (the createdAt variable
assignment) accordingly.
bridges/ai/login_state_db.go (1)

70-103: ⚠️ Potential issue | 🟠 Major

Seed the new row from legacy login metadata on first load.

On sql.ErrNoRows, this now returns a blank loginRuntimeState. For upgraded logins, that drops existing metadata-backed values like chat index / heartbeat seed / Gravatar state unless some separate migration has already populated aichats_login_state before the first read. Please hydrate from the legacy login metadata on the no-row path, then persist the converted state.

🤖 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 70 - 103, When
loadLoginRuntimeState hits sql.ErrNoRows, hydrate the returned loginRuntimeState
from the legacy login metadata before returning and persist it into
aiLoginStateTable so upgraded logins keep their existing chat
index/heartbeat/gravatar state; specifically, in loadLoginRuntimeState use
scope.loginID/scope.bridgeID to query the legacy metadata table (the same DB via
scope.db), map legacy fields into the state struct (NextChatIndex,
LastHeartbeatEventJSON, ModelCacheJSON, GravatarJSON, FileAnnotationJSON,
ConsecutiveErrors, LastErrorAt) and then INSERT (or UPSERT) the populated state
into aiLoginStateTable using scope.db so the converted row is stored for
subsequent reads. Ensure the logic runs only on the sql.ErrNoRows branch and
reuses the existing state variable before returning.
🧹 Nitpick comments (4)
bridges/ai/reaction_handling_test.go (1)

17-30: Add whitespace-trimming assertions to match the intended contract.

If portalRoomName is expected to trim values, this test should include padded inputs (and optionally whitespace-only portal.Name) to lock that behavior in.

🧪 Suggested test tightening
 func TestPortalRoomNamePrefersBridgeOwnedName(t *testing.T) {
 	portal := &bridgev2.Portal{
 		Portal: &database.Portal{
 			PortalKey: networkid.PortalKey{ID: networkid.PortalID("chat-1")},
 			MXID:      id.RoomID("!chat:example.com"),
-			Name:      "Bridge Name",
+			Name:      "  Bridge Name  ",
 			Metadata: &PortalMetadata{
-				Slug: "sidecar-slug",
+				Slug: "  sidecar-slug  ",
 			},
 		},
 	}

 	if got := portalRoomName(portal); got != "Bridge Name" {
 		t.Fatalf("expected bridge-owned room name, got %q", got)
 	}

-	portal.Name = ""
+	portal.Name = "   "
 	if got := portalRoomName(portal); got != "sidecar-slug" {
 		t.Fatalf("expected slug fallback when bridge name is empty, got %q", got)
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/reaction_handling_test.go` around lines 17 - 30, The test should
assert that portalRoomName trims whitespace: update the test for portalRoomName
to include cases where portal.Name contains leading/trailing spaces (e.g., " 
Bridge Name  ") and where portal.Name is whitespace-only (e.g., "   ") so the
function returns the trimmed name or falls back to PortalMetadata.Slug
respectively; specifically modify the existing assertions that check portal.Name
== "Bridge Name" and portal.Name == "" to instead use padded inputs and a
whitespace-only input and assert portalRoomName(portal) returns "Bridge Name"
and "sidecar-slug" (from PortalMetadata.Slug) accordingly, referencing
portalRoomName, portal.Name and PortalMetadata.Slug to locate the code.
bridges/ai/internal_prompt_db.go (1)

83-101: Dynamic SQL construction is safe but consider query readability.

The parameter index computation via strconv.Itoa(len(args)) is safe since the values are controlled integers. However, the query string concatenation across multiple conditionals makes the final SQL harder to reason about.

This is acceptable for now given the query's simplicity, but consider using a query builder or strings.Builder with explicit parameter tracking for more complex queries in the future.

🤖 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 83 - 101, The dynamic SQL is
built by concatenating strings and computing parameter indices with
strconv.Itoa(len(args)) which is safe but reduces readability; refactor the
construction in the function that builds the query (around
aiInternalMessagesTable, args, resetAt, opts.excludeMessageID and the
scope.db.Query call) to make the parameter tracking explicit—use a
strings.Builder or a local slice of condition strings and a separate paramIndex
counter so each conditional (resetAt, excludedEventID) appends a condition like
"AND created_at_ms >= $N" using the incremented counter and pushes the value to
args; keep the final ORDER BY/LIMIT assembly the same and ensure args order
matches the $N placeholders passed to scope.db.Query.
bridges/ai/streaming_persistence.go (1)

120-164: Consider extracting the duplicated SenderID computation.

The sender ID logic at lines 125-130 and 145-150 is identical. Extracting it to a local variable would reduce duplication.

Also note: time.UnixMilli(0) returns the Unix epoch (Jan 1, 1970), which is not zero but would be semantically invalid. The .IsZero() check at line 157 will be false for epoch time, so the fallback to time.Now() won't trigger for completedAtMs == 0 after the else branch sets it.

♻️ Suggested refactor
+	senderID := func() networkid.UserID {
+		if state.respondingGhostID != "" {
+			return networkid.UserID(state.respondingGhostID)
+		}
+		return modelUserID(oc.effectiveModel(meta))
+	}()
+
 	sdk.UpsertAssistantMessage(ctx, sdk.UpsertAssistantMessageParams{
 		Login:  oc.UserLogin,
 		Portal: portal,
-		SenderID: func() networkid.UserID {
-			if state.respondingGhostID != "" {
-				return networkid.UserID(state.respondingGhostID)
-			}
-			return modelUserID(oc.effectiveModel(meta))
-		}(),
+		SenderID:         senderID,
 		NetworkMessageID: networkMessageID,
 		...
 	})
 	...
 	if messageID != "" && portal != nil {
 		transcriptMsg := &database.Message{
 			ID:       messageID,
 			MXID:     initialEventID,
 			Room:     portal.PortalKey,
-			SenderID: func() networkid.UserID {
-				if state.respondingGhostID != "" {
-					return networkid.UserID(state.respondingGhostID)
-				}
-				return modelUserID(oc.effectiveModel(meta))
-			}(),
+			SenderID: senderID,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/streaming_persistence.go` around lines 120 - 164, Extract the
duplicated SenderID computation into a local variable (e.g., senderID := func()
networkid.UserID { if state.respondingGhostID != "" { return
networkid.UserID(state.respondingGhostID) } return
modelUserID(oc.effectiveModel(meta)) }()) and use senderID both in
sdk.UpsertAssistantMessage (SenderID) and in transcriptMsg.SenderID to remove
duplication. Also fix the timestamp logic around state.completedAtMs: set
transcriptMsg.Timestamp = time.Now() when state.completedAtMs == 0, otherwise
compute t := time.UnixMilli(state.completedAtMs) and if t.IsZero() fall back to
time.Now(); assign the resulting time to transcriptMsg.Timestamp to avoid
treating Unix epoch as valid when completedAtMs is zero.
bridges/ai/chat.go (1)

1165-1205: Consider using GetExistingPortalByKey to avoid side effects.

GetPortalByKey may create a portal if one doesn't exist for the given key. Since this function is listing existing chat portals from the AI state table, using GetExistingPortalByKey would be more semantically correct and avoid unintended portal creation.

♻️ Suggested change
-		portal, err := oc.UserLogin.Bridge.GetPortalByKey(ctx, networkid.PortalKey{
+		portal, err := oc.UserLogin.Bridge.GetExistingPortalByKey(ctx, networkid.PortalKey{
 			ID:       networkid.PortalID(portalID),
 			Receiver: oc.UserLogin.ID,
 		})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/chat.go` around lines 1165 - 1205, The listAllChatPortals method
uses oc.UserLogin.Bridge.GetPortalByKey which can create portals as a side
effect; replace that call with oc.UserLogin.Bridge.GetExistingPortalByKey so you
only fetch already-existing portals, and keep the current nil-check and error
handling (i.e., if GetExistingPortalByKey returns (nil, nil) just skip, and if
it returns an error propagate it). Update the reference at the call site in
AIClient.listAllChatPortals that currently constructs networkid.PortalKey from
portalID and oc.UserLogin.ID to call GetExistingPortalByKey instead.
🤖 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/handlematrix.go`:
- Around line 956-967: savePortal currently calls saveAIPortalState before
portal.Save which can leave AI flags persisted if portal.Save fails; either flip
the order or, preferably, perform both writes inside the same transactional
boundary so they succeed or roll back together. Modify savePortal to begin a
DB/transactional context, call portal.Save(ctx) and saveAIPortalState(ctx,
portal, meta) within that transaction (or call portal.Save first then
saveAIPortalState if transaction support is unavailable), and ensure rollback on
any error (or revert the earlier write) so callers (auto-greeting/title
generation) don't see inconsistent AI flags; refer to savePortal,
saveAIPortalState, and portal.Save when locating the change.

In `@bridges/ai/login_state_db.go`:
- Around line 200-205: The current ensureLoginStateLoaded code assigns a new
empty &loginRuntimeState{} to oc.loginState when loadLoginRuntimeState fails,
which causes later successful updates to persist that empty snapshot; instead,
when loadLoginRuntimeState(ctx, oc) returns an error simply log the error and
return a temporary empty state without memoizing it (do not set oc.loginState).
Change the error branch in ensureLoginStateLoaded (the loadLoginRuntimeState
call) to create a local temp := &loginRuntimeState{} and return it (or continue
using it) while only assigning oc.loginState = state when loadLoginRuntimeState
succeeded; keep existing logging behavior via oc.loggerForContext(ctx).

In `@bridges/ai/transcript_db.go`:
- Around line 207-211: The helper getAllAIHistoryMessages is unused and causing
a staticcheck U1000 failure; either remove the function entirely or replace its
references by calling oc.getAIHistoryMessages(ctx, portal, 0) where intended; if
you remove it, also remove any tests or comments that reference
getAllAIHistoryMessages; if you prefer to keep it, add a single call site (or an
explicit //nolint:staticcheck comment) to avoid the unused symbol—refer to the
getAllAIHistoryMessages and getAIHistoryMessages identifiers when making the
change.

In `@bridges/codex/constructors.go`:
- Around line 52-60: The code currently returns nil when cc.bridgeDB() is nil,
causing silent success and skipping aidb.EnsureSchema, cc.applyRuntimeDefaults,
and sdk.PrimeUserLoginCache; change that to fail fast by replacing the early
return with an explicit error (or remove the check and let aidb.EnsureSchema
report the nil DB error). Concretely, in the block using cc.bridgeDB(), ensure
that when db == nil you return a descriptive error (e.g., "AI DB not available")
so aidb.EnsureSchema(ctx, db), cc.applyRuntimeDefaults(), and
sdk.PrimeUserLoginCache(ctx, cc.br) always run only on successful
initialization.

In `@bridges/codex/portal_state_db.go`:
- Around line 146-149: The code silently ignores json.Unmarshal errors for
stateRaw which can drop portals; update the error handling around
json.Unmarshal([]byte(stateRaw), state) so failures are surfaced with context
(include the portal key/identifier and the raw error) instead of just continue —
e.g., log a warning or return an error using the function’s logger/err path
(reference symbols: stateRaw, state, json.Unmarshal) so bad state_json rows are
visible during backfill/list operations.
- Around line 78-92: The function loadCodexPortalState hides the "no state" case
by returning a non-nil empty *codexPortalState, breaking callers that rely on
nil checks; change the behavior so that when scope == nil or when aidb.Load
returns nil state you return (nil, nil) instead of &codexPortalState{}; update
the return paths in loadCodexPortalState (which calls codexDBScopeForPortal,
uses codexPortalStateBlob and aidb.Load[codexPortalState]) to propagate a nil
pointer for "no state" so callers can detect it via state == nil.
- Around line 159-172: parseCodexPortalKey currently splits on the first "/"
which is brittle if PortalKey.String() allows "/" inside components; replace the
manual parsing with the canonical networkid parser (use
networkid.ParsePortalKey) to guarantee round-trip safety: call
networkid.ParsePortalKey on the trimmed raw string, handle the returned
(PortalKey, error) similarly to the current behavior (return empty PortalKey +
false on empty input or parse error, otherwise return the parsed
networkid.PortalKey and true), and remove the manual
strings.Cut/PortalID/UserLoginID construction in parseCodexPortalKey.

In `@bridges/openclaw/identifiers.go`:
- Around line 34-42: The generated ghost ID can contain an empty encoded loginID
which parseOpenClawGhostID rejects; update openClawScopedGhostUserID to
canonicalize loginID the same way it does agentID (e.g., set a default like
"gateway" when loginID == ""), so the produced string matches what
parseOpenClawGhostID expects; modify openClawScopedGhostUserID (and its use of
loginID before base64 encoding) to substitute that placeholder for empty values
to ensure round‑trippable IDs.

In `@bridges/openclaw/login.go`:
- Around line 244-276: Create the login first without starting auto-connect,
persist the credentials, then kick off the load/connect step: replace the single
call to sdk.CreateAndCompleteLogin with a two-step flow—use the create-only API
to obtain the login (so CreateAndCompleteLogin does not start
LoadConnectAndCompleteLogin), call saveOpenClawLoginState(login,
&openClawPersistedLoginState{...}) to persist
GatewayToken/GatewayPassword/DeviceToken, and only after a successful save
invoke the completion/load routine (LoadConnectAndCompleteLogin or the SDK's
complete-login entry) to start auto-connect; update error handling and rollback
accordingly around those symbols (sdk.CreateAndCompleteLogin,
saveOpenClawLoginState, openClawPersistedLoginState,
LoadConnectAndCompleteLogin).

---

Outside diff comments:
In `@bridges/ai/handleai.go`:
- Around line 217-249: The current conversation-start check (involving
oc.getAIHistoryMessages, MessageMetadata and hasInternalPromptHistory) treats a
missing AI transcript as empty and can trigger auto-greetings despite existing
bridge-only history; modify the logic to fall back to checking bridge message
history when getAIHistoryMessages returns an empty slice (or no user/assistant
turns) instead of immediately returning false — e.g., after exhausting the
transcript loop and before calling hasInternalPromptHistory, call the
bridge-history lookup (the same table used for bridge events/messages) and only
treat the chat as empty if both transcript AND bridge-history checks show no
user/assistant messages; keep the existing metadata/role/exclude filters when
inspecting the bridge messages so behavior remains consistent.

In `@bridges/ai/handlematrix.go`:
- Around line 428-455: The history lookup currently relies only on
oc.getAIHistoryMessages which reads aichats_transcript_messages, so when the
assistant reply lives only in the bridge table the search for assistantResponse
(using editedIdx and messageMeta) can miss it; update the logic in the block
around oc.getAIHistoryMessages/editedIdx/assistantResponse to fall back to
fetching bridge history (e.g., via an existing bridge-history reader or a new
oc.getBridgeHistoryMessages function) when assistantResponse remains nil: merge
or search the bridge messages with the same newest-first ordering and the same
messageMeta checks to locate the assistant reply, handle errors from the
fallback fetch, and keep the rest of the redaction/regeneration flow unchanged.

In `@bridges/ai/sessions_tools.go`:
- Around line 572-581: In lastMessageTimestamp, getAIHistoryMessages returns
messages ordered newest-first, so replace the oldest-element access
(messages[len(messages)-1]) with the first element (messages[0]) to return the
most recent timestamp; update the return expression in the
AIClient.lastMessageTimestamp function to use messages[0].Timestamp.UnixMilli()
and keep the existing nil/empty checks around getAIHistoryMessages.

---

Duplicate comments:
In `@bridges/ai/login_state_db.go`:
- Around line 70-103: When loadLoginRuntimeState hits sql.ErrNoRows, hydrate the
returned loginRuntimeState from the legacy login metadata before returning and
persist it into aiLoginStateTable so upgraded logins keep their existing chat
index/heartbeat/gravatar state; specifically, in loadLoginRuntimeState use
scope.loginID/scope.bridgeID to query the legacy metadata table (the same DB via
scope.db), map legacy fields into the state struct (NextChatIndex,
LastHeartbeatEventJSON, ModelCacheJSON, GravatarJSON, FileAnnotationJSON,
ConsecutiveErrors, LastErrorAt) and then INSERT (or UPSERT) the populated state
into aiLoginStateTable using scope.db so the converted row is stored for
subsequent reads. Ensure the logic runs only on the sql.ErrNoRows branch and
reuses the existing state variable before returning.

In `@bridges/ai/transcript_db.go`:
- Around line 82-85: The current logic uses msg.Timestamp.UnixMilli() and checks
for zero, but time.Time{} does not produce 0 via UnixMilli so the fallback never
triggers; change the code to first test msg.Timestamp.IsZero() and if true set
createdAt = time.Now().UnixMilli(), otherwise set createdAt =
msg.Timestamp.UnixMilli(); update the block around createdAt and msg.Timestamp
in transcript_db.go (the createdAt variable assignment) accordingly.

---

Nitpick comments:
In `@bridges/ai/chat.go`:
- Around line 1165-1205: The listAllChatPortals method uses
oc.UserLogin.Bridge.GetPortalByKey which can create portals as a side effect;
replace that call with oc.UserLogin.Bridge.GetExistingPortalByKey so you only
fetch already-existing portals, and keep the current nil-check and error
handling (i.e., if GetExistingPortalByKey returns (nil, nil) just skip, and if
it returns an error propagate it). Update the reference at the call site in
AIClient.listAllChatPortals that currently constructs networkid.PortalKey from
portalID and oc.UserLogin.ID to call GetExistingPortalByKey instead.

In `@bridges/ai/internal_prompt_db.go`:
- Around line 83-101: The dynamic SQL is built by concatenating strings and
computing parameter indices with strconv.Itoa(len(args)) which is safe but
reduces readability; refactor the construction in the function that builds the
query (around aiInternalMessagesTable, args, resetAt, opts.excludeMessageID and
the scope.db.Query call) to make the parameter tracking explicit—use a
strings.Builder or a local slice of condition strings and a separate paramIndex
counter so each conditional (resetAt, excludedEventID) appends a condition like
"AND created_at_ms >= $N" using the incremented counter and pushes the value to
args; keep the final ORDER BY/LIMIT assembly the same and ensure args order
matches the $N placeholders passed to scope.db.Query.

In `@bridges/ai/reaction_handling_test.go`:
- Around line 17-30: The test should assert that portalRoomName trims
whitespace: update the test for portalRoomName to include cases where
portal.Name contains leading/trailing spaces (e.g., "  Bridge Name  ") and where
portal.Name is whitespace-only (e.g., "   ") so the function returns the trimmed
name or falls back to PortalMetadata.Slug respectively; specifically modify the
existing assertions that check portal.Name == "Bridge Name" and portal.Name ==
"" to instead use padded inputs and a whitespace-only input and assert
portalRoomName(portal) returns "Bridge Name" and "sidecar-slug" (from
PortalMetadata.Slug) accordingly, referencing portalRoomName, portal.Name and
PortalMetadata.Slug to locate the code.

In `@bridges/ai/streaming_persistence.go`:
- Around line 120-164: Extract the duplicated SenderID computation into a local
variable (e.g., senderID := func() networkid.UserID { if state.respondingGhostID
!= "" { return networkid.UserID(state.respondingGhostID) } return
modelUserID(oc.effectiveModel(meta)) }()) and use senderID both in
sdk.UpsertAssistantMessage (SenderID) and in transcriptMsg.SenderID to remove
duplication. Also fix the timestamp logic around state.completedAtMs: set
transcriptMsg.Timestamp = time.Now() when state.completedAtMs == 0, otherwise
compute t := time.UnixMilli(state.completedAtMs) and if t.IsZero() fall back to
time.Now(); assign the resulting time to transcriptMsg.Timestamp to avoid
treating Unix epoch as valid when completedAtMs is zero.
🪄 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: 32c700c9-ca86-44de-87aa-fe2887bfd192

📥 Commits

Reviewing files that changed from the base of the PR and between 22ded85 and e3bbefc.

📒 Files selected for processing (90)
  • bridges/ai/account_hints.go
  • bridges/ai/agentstore.go
  • bridges/ai/bridge_db.go
  • bridges/ai/chat.go
  • bridges/ai/chat_fork_test.go
  • bridges/ai/client.go
  • bridges/ai/command_registry.go
  • bridges/ai/custom_agents_db.go
  • bridges/ai/desktop_api_sessions.go
  • bridges/ai/desktop_api_sessions_test.go
  • bridges/ai/error_logging.go
  • bridges/ai/events.go
  • bridges/ai/events_test.go
  • bridges/ai/gravatar.go
  • bridges/ai/handleai.go
  • bridges/ai/handlematrix.go
  • bridges/ai/handler_interfaces.go
  • bridges/ai/integration_host.go
  • bridges/ai/internal_prompt_db.go
  • bridges/ai/login.go
  • bridges/ai/login_loaders.go
  • bridges/ai/login_loaders_test.go
  • bridges/ai/login_state_db.go
  • bridges/ai/logout_cleanup.go
  • bridges/ai/logout_cleanup_test.go
  • bridges/ai/media_understanding_runner.go
  • bridges/ai/media_understanding_runner_openai_test.go
  • bridges/ai/message_parts.go
  • bridges/ai/metadata.go
  • bridges/ai/metadata_test.go
  • bridges/ai/persistence_boundaries_test.go
  • bridges/ai/portal_materialize.go
  • bridges/ai/portal_state_db.go
  • bridges/ai/prompt_builder.go
  • bridges/ai/reaction_handling.go
  • bridges/ai/reaction_handling_test.go
  • bridges/ai/room_info.go
  • bridges/ai/room_meta_bridgev2_test.go
  • bridges/ai/scheduler_heartbeat_test.go
  • bridges/ai/scheduler_rooms.go
  • bridges/ai/sessions_tools.go
  • bridges/ai/sessions_visibility_test.go
  • bridges/ai/status_text.go
  • bridges/ai/streaming_output_handlers.go
  • bridges/ai/streaming_persistence.go
  • bridges/ai/subagent_spawn.go
  • bridges/ai/tool_approvals.go
  • bridges/ai/tool_approvals_rules.go
  • bridges/ai/tool_configured.go
  • bridges/ai/tools.go
  • bridges/ai/tools_message_actions.go
  • bridges/ai/tools_message_desktop.go
  • bridges/ai/transcript_db.go
  • bridges/codex/constructors.go
  • bridges/codex/directory_manager.go
  • bridges/codex/portal_state_db.go
  • bridges/openclaw/catalog.go
  • bridges/openclaw/identifiers.go
  • bridges/openclaw/login.go
  • bridges/openclaw/manager.go
  • bridges/openclaw/metadata.go
  • bridges/openclaw/provisioning.go
  • bridges/opencode/client.go
  • bridges/opencode/connector.go
  • bridges/opencode/opencode_instance_state.go
  • cmd/agentremote/main.go
  • cmd/agentremote/profile.go
  • cmd/agentremote/profile_test.go
  • cmd/agentremote/run_bridge.go
  • cmd/internal/bridgeentry/bridgeentry.go
  • pkg/agents/tools/params.go
  • pkg/aidb/001-init.sql
  • pkg/aidb/json_blob_table.go
  • pkg/integrations/memory/sessions.go
  • pkg/matrixevents/matrixevents.go
  • pkg/shared/toolspec/toolspec.go
  • sdk/approval_flow.go
  • sdk/approval_reaction_helpers.go
  • sdk/approval_reaction_helpers_test.go
  • sdk/client.go
  • sdk/commands.go
  • sdk/conversation.go
  • sdk/conversation_state.go
  • sdk/conversation_test.go
  • sdk/helpers.go
  • sdk/load_user_login.go
  • sdk/login_handle.go
  • sdk/matrix_actions.go
  • sdk/portal_lifecycle.go
  • sdk/room_features.go
💤 Files with no reviewable changes (5)
  • bridges/ai/error_logging.go
  • bridges/ai/chat_fork_test.go
  • bridges/ai/events_test.go
  • bridges/ai/events.go
  • bridges/ai/command_registry.go
✅ Files skipped from review due to trivial changes (4)
  • bridges/ai/media_understanding_runner.go
  • bridges/ai/media_understanding_runner_openai_test.go
  • bridges/ai/persistence_boundaries_test.go
  • bridges/ai/custom_agents_db.go
🚧 Files skipped from review as they are similar to previous changes (22)
  • bridges/ai/gravatar.go
  • bridges/ai/portal_materialize.go
  • bridges/ai/desktop_api_sessions_test.go
  • bridges/ai/tool_configured.go
  • bridges/ai/login_loaders_test.go
  • bridges/ai/scheduler_rooms.go
  • bridges/ai/scheduler_heartbeat_test.go
  • bridges/ai/desktop_api_sessions.go
  • bridges/ai/tool_approvals.go
  • bridges/openclaw/catalog.go
  • bridges/ai/message_parts.go
  • bridges/codex/directory_manager.go
  • bridges/opencode/connector.go
  • bridges/ai/reaction_handling.go
  • bridges/openclaw/metadata.go
  • bridges/ai/integration_host.go
  • bridges/openclaw/manager.go
  • bridges/ai/agentstore.go
  • bridges/ai/login_loaders.go
  • bridges/ai/prompt_builder.go
  • bridges/ai/login.go
  • bridges/openclaw/provisioning.go
📜 Review details
🧰 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/room_info.go
  • bridges/ai/reaction_handling_test.go
  • bridges/codex/constructors.go
  • bridges/ai/handleai.go
  • bridges/ai/client.go
🪛 GitHub Actions: Go
bridges/ai/transcript_db.go

[error] 207-207: go-staticcheck-repo-mod failed (staticcheck U1000): func (*AIClient).getAllAIHistoryMessages is unused

🪛 GitHub Check: Lint
bridges/ai/transcript_db.go

[failure] 207-207:
func (*AIClient).getAllAIHistoryMessages is unused (U1000)

🔇 Additional comments (84)
bridges/ai/account_hints.go (2)

37-37: Context-aware instance discovery is a good fix.

This keeps desktop instance resolution aligned with the caller’s cancellation/deadline semantics and the rest of this method’s context usage.


65-65: Per-instance config lookup now uses consistent context propagation.

Using the same ctx for config resolution and account listing avoids mixed-context behavior in hint generation.

bridges/ai/reaction_handling_test.go (1)

12-31: Good coverage for precedence + fallback behavior.

This test cleanly validates both branches (bridge-owned name first, slug fallback when name is empty). Nice targeted regression guard.

bridges/opencode/client.go (8)

12-12: LGTM! SDK package migration for imports and type definitions.

The import and type migrations from agentremote to sdk are consistent. The ClientBase, StreamTurnHost, Turn, and StreamPartState types are all properly sourced from the new sdk package.

Also applies to: 26-26, 31-31, 38-39


67-74: LGTM! Stream host initialization properly migrated to SDK types.

The NewStreamTurnHost and StreamTurnHostCallbacks correctly use the sdk package. The GetAborter callback properly returns sdk.Aborter and the s.turn (of type *sdk.Turn) should satisfy this interface.


88-117: LGTM! Improved connection state management.

The refactored Connect() method now properly:

  1. Sets LoggedIn(false) and emits StateConnecting before attempting connection
  2. Handles RestoreConnections failure by emitting TransientDisconnect instead of silently continuing
  3. Adds reachability validation via hasReachableOpenCodeInstance() before declaring connected state
  4. Only transitions to Connected when instances are actually reachable

The explicit return on line 113 correctly prevents fall-through to the synchronous connected state when the async path is taken.


137-151: LGTM! Reachability check logic is sound.

The hasReachableOpenCodeInstance() method correctly:

  • Returns true when no instances are configured (vacuously connected)
  • Returns false with proper nil guards for missing bridge/manager
  • Returns true if at least one configured instance is connected
  • Returns false only when instances exist but none are reachable

153-158: LGTM! Return type correctly migrated to sdk.ApprovalReactionHandler.

The return type change aligns with the ReactionTarget interface defined in sdk/base_reaction_handler.go, which expects GetApprovalHandler() to return ApprovalReactionHandler. This is consistent with the broader SDK migration.


200-214: LGTM! Room features construction migrated to SDK builders.

The use of sdk.BuildRoomFeatures and sdk.BuildMediaFileFeatureMap is consistent with the SDK migration pattern. The parameter structure is clear and the closure pattern for providing file features is appropriate.


231-236: LGTM! Client cache removal migrated to SDK helper.

The sdk.RemoveClientFromCache usage is consistent with the SDK migration. The nil checks for connector and UserLogin before the call provide appropriate guards.


238-247: LGTM! Chat info construction migrated to SDK builder.

The sdk.BuildChatInfoWithFallback provides a clean abstraction for building chat info with fallback values. The parameters (primary title, fallback name, service identifier, topic) are appropriately passed.

bridges/codex/constructors.go (1)

84-94: Nice capability declaration.

Explicitly advertising the provisioning surface here is clearer than relying on implicit behavior and should help bridgev2 clients gate features correctly.

bridges/codex/portal_state_db.go (1)

44-76: Scope construction guards are solid.

Nil checks plus trimming of bridge/login/portal identifiers are a good defensive boundary before DB access.

bridges/openclaw/identifiers.go (2)

52-91: Good parser hardening for v1 ghost IDs.

The versioned prefix + Base64URL split/decode flow is a solid fix for the prior delimiter ambiguity and keeps legacy parsing isolated.


19-19: Clarify that openClawGatewayID() is new code with no backwards compatibility concerns.

This function is newly introduced in bridges/openclaw/identifiers.go and has no prior implementation to break compatibility with. stringutil.ShortHash(key, 8) correctly returns 16 hex characters (2*8 bytes), as documented. Since this is new code, there are no existing persisted gateway IDs with a different format to orphan. The gateway ID is deterministically recomputed from stable gatewayURL and label inputs, ensuring consistent format going forward.

			> Likely an incorrect or invalid review comment.
bridges/ai/sessions_visibility_test.go (1)

25-27: LGTM!

The test fixture correctly uses Slug instead of Title, aligning with the broader migration to derive room display logic from PortalMetadata.Slug.

bridges/ai/tools_message_desktop.go (1)

14-18: LGTM!

The context threading through resolveDesktopInstance and all its call sites is correct. This enables proper cancellation and timeout propagation for desktop API operations.

bridges/ai/internal_prompt_db.go (1)

118-130: LGTM!

The malformed canonical_turn_data handling correctly logs a warning with contextual identifiers (event_id, portal_id) before skipping the row, as previously requested. This makes corrupted rows debuggable without failing the entire load operation.

bridges/ai/subagent_spawn.go (1)

325-335: LGTM!

The migration from direct database.Message creation to persistInternalPrompt correctly uses the new shared internal-prompt persistence system. The error handling appropriately logs and continues rather than failing the subagent spawn operation.

bridges/ai/metadata.go (3)

95-117: LGTM!

The simplified UserLoginMetadata (now only storing Provider) and the credential accessor functions operating on *aiLoginConfig correctly align with the migration of login state to AI-owned sidecar tables.


210-244: LGTM!

All persistent portal state fields are correctly marked with json:"-" to prevent bridgev2 metadata serialization. The runtime-only portalStateLoaded flag enables the load-once semantics implemented in portal_state_db.go.


309-312: LGTM!

The deep copy of TypingIntervalSeconds correctly dereferences and creates a new pointer, preventing aliasing between the original and cloned metadata.

bridges/ai/portal_state_db.go (2)

124-132: LGTM!

The error handling correctly separates sql.ErrNoRows (return nil, nil) from other database errors (return nil, err) and the empty string check. This addresses the previous review concern about swallowing database errors.


163-178: LGTM!

The load-once pattern with portalStateLoaded flag prevents redundant database queries. The error handling correctly resets the flag on failure, allowing retry on subsequent access, while logging the error for debugging.

bridges/ai/metadata_test.go (2)

49-68: LGTM!

Good test coverage for the JSON serialization behavior. This test ensures the json:"-" tags are working correctly and persistent state won't accidentally leak through bridgev2 metadata serialization.


70-122: LGTM!

Comprehensive round-trip test covering the key persistence fields including PDFConfig, SessionBootstrapByAgent, ModuleMeta, and TypingIntervalSeconds. This validates the persistedPortalStateFromMeta → marshal → unmarshal → applyPersistedPortalState flow.

bridges/ai/status_text.go (1)

203-220: LGTM!

The iteration order fix correctly finds the most recent assistant message with token usage. The forward iteration (index 0 first) works correctly with getAIHistoryMessages which returns messages in newest-first order, as confirmed by the past review analysis.

bridges/ai/logout_cleanup.go (4)

42-93: Fire-and-forget cleanup behavior retained.

The previous review flagged that each DELETE is still fire-and-forget with only warning logs on failure. This design choice prioritizes logout completion over strict cleanup guarantees.


12-19: LGTM!

Clear documentation of what purgeLoginData handles vs. what bridgev2 handles automatically.


94-99: LGTM!

The in-memory state cleanup correctly clears both loginState and loginConfig after the DB cleanup, preventing stale state from persisting in the running client.


102-112: LGTM!

The execDelete helper appropriately handles nil context by defaulting to context.Background() and logs warnings without failing the overall logout flow.

bridges/ai/streaming_output_handlers.go (4)

14-15: LGTM!

Import changes correctly migrate from the old agentremote package to the consolidated sdk package, and add the stringutil import for the new hash function.


18-21: LGTM!

The approval ID generation is simplified using stringutil.ShortHash with a length of 8 characters. The input construction (trimmed toolCallID, toolName, and input joined by newlines) maintains stable, deterministic IDs.


29-40: LGTM!

Return type change to sdk.ApprovalHandle and usage of sdk.ApprovalReasonAutoApproved correctly aligns with the SDK migration. The caller in streaming_actions.go expects this type for the handle field.


240-240: LGTM!

The ctx parameter is now correctly passed to isMcpAlwaysAllowed, matching the updated function signature in tool_approvals_rules.go that requires context as the first argument.

bridges/ai/room_info.go (1)

11-30: LGTM!

The helper correctly implements the two-path room name update pattern:

  1. For established rooms (MXID set): uses portal.UpdateInfo with ExcludeChangesFromTimeline: true
  2. For pre-creation: sets local portal.Name and portal.NameSet fields

This matches the SDK pattern shown in sdk/matrix_actions.go.

bridges/ai/sessions_tools.go (6)

119-126: LGTM!

Migration to getAIHistoryMessages centralizes history retrieval. The slicing logic openClawMessages[len(openClawMessages)-messageLimit:] correctly takes the last N messages after buildOpenClawSessionMessages processes them.


138-138: LGTM!

Context is now correctly threaded through to desktopAPIInstanceNames.


217-222: LGTM!

Context threading through resolveDesktopInstanceName and desktopAPIClient aligns with the updated function signatures.


270-270: LGTM!

Correctly migrated to use the centralized getAIHistoryMessages helper.


312-312: LGTM!

Context threading for desktop session/instance resolution is consistent with other callers.

Also applies to: 355-355


473-486: LGTM!

Session label and display name resolution now prefer meta.Slug over the previous meta.Title. The fallback chain remains correct: portal.Name → meta.Slug.

bridges/ai/logout_cleanup_test.go (1)

8-42: LGTM!

Good test coverage for the run-key table cleanup. The test properly:

  1. Sets up a DB-backed client
  2. Inserts test rows into both tables
  3. Calls purgeLoginData
  4. Verifies both tables are empty for the target login

Consider adding a test case that verifies data belonging to other logins remains untouched to prevent accidental over-deletion regressions.

bridges/ai/streaming_persistence.go (2)

39-41: LGTM!

SDK migration for GeneratedFileRefsFromParts is correct.


55-77: LGTM!

Migration to sdk.BuildAssistantBaseMetadata and sdk.AssistantMessageMetadata correctly uses the SDK types for metadata construction.

bridges/ai/bridge_db.go (4)

13-27: LGTM!

Centralizing table name constants provides a single source of truth and reduces the risk of typos across the codebase.


35-35: LGTM!

Logger label change from "agentremote" to "ai" aligns with the broader rename in this PR.


83-88: LGTM!

New helper provides portal-based DB access with proper nil-checking.


100-160: LGTM!

Well-designed scope abstractions that:

  1. Centralize ID normalization with strings.TrimSpace (addressing prior review feedback)
  2. Provide consistent nil-safety checks
  3. Enable code reuse across the various *_db.go files

The portalScope embedding *loginScope is clean and avoids field duplication.

bridges/ai/tools_message_actions.go (2)

27-39: Behavior change: explicit emoji now required.

The "remove all reactions" functionality (previously triggered by empty emoji) has been removed. This is an intentional behavior change per the summary, but note that removed: 1 is now hardcoded rather than reflecting actual removal count.

If removeReaction can fail silently for non-existent reactions, the removed: 1 may be misleading. Consider returning the actual result from the underlying call if available.


88-88: LGTM!

Context is now correctly passed to desktopAPIInstanceConfig, matching the updated function signature.

bridges/ai/chat.go (14)

13-13: LGTM!

Clean import migration from the aliased agentremote style to direct sdk package usage.


50-62: LGTM!

The agent enablement logic correctly reads from persisted aiLoginConfig with a safe nil-defaulting pattern (cfg.Agents == nil || *cfg.Agents treats unset as enabled).


127-132: LGTM!

API key resolution now correctly flows through the persisted login config via resolveProviderAPIKeyForConfig.


194-280: LGTM!

Clean type migration to *sdk.Agent for agent matching, contact response, and catalog ID resolution. The field accesses remain consistent with the SDK type's interface.


630-630: LGTM!

Using savePortalQuiet correctly persists agent metadata (including ResolvedTarget and RuntimeModelOverride) to the AI-owned state table while gracefully handling persistence failures.


674-683: LGTM!

The chat index allocation now correctly persists through the AI-local runtime state via updateLoginState, maintaining thread safety with the existing chatLock mutex.


693-705: LGTM!

Intentionally omitting Title from forked portal metadata clone allows forked chats to receive fresh titles rather than inheriting from the source.


741-764: LGTM!

Portal state is correctly persisted to the AI-owned state table before calling sdk.ConfigureDMPortal. The error propagation is appropriate for initialization failures.


949-956: LGTM!

Improved title resolution with proper trimming and a cleaner fallback chain (portal name → slug → model contact name).


990-999: LGTM!

Clean abstraction using sdk.BuildLoginDMChatInfo for DM chat info construction while still allowing model-specific member customization.


1066-1068: LGTM!

System notice sending now uses the SDK helper sdk.SendSystemMessage for consistency across the codebase.


1080-1092: LGTM!

Bootstrap simplified to focus on config-driven default chat creation, removing unnecessary persistence waits and counter sync operations.


1094-1163: LGTM!

The refactored ensureDefaultChat uses deterministic portal key lookup, cleanly separates portal existence checking from room materialization, and appropriately errors when the required "beep agent" is unavailable.


1235-1244: LGTM!

Appropriately simplified to delegate message cleanup to the bridgev2 framework, keeping only the debug log for observability.

bridges/ai/client.go (19)

4-4: LGTM!

Clean import additions: cmp package for comparison operations and direct sdk package import.

Also applies to: 32-32


264-346: LGTM!

Clean structural migration to SDK types with properly separated mutexes for login state and config, and type-safe generic approval flow.


382-462: LGTM!

Well-structured constructor with defensive config cloning, properly configured approval flow callbacks, and clean migration to DB-backed state loading.


469-471: LGTM!

Return type correctly updated to sdk.ApprovalReactionHandler interface.


486-519: LGTM!

Clean refactor with initProviderForLoginConfig enabling config-driven provider initialization while maintaining backward compatibility through the wrapper initProviderForLogin.


614-618: LGTM!

Correctly using sdk.MatrixMessageStatusEventInfo helper for consistent message status event construction.


621-645: LGTM!

Clean dual persistence pattern: the bridge DB gets a transport-safe message copy (with cleared metadata), while the AI-owned transcript table stores the full payload. Error handling appropriately logs auxiliary persistence failures without failing the primary operation.


852-897: LGTM!

Connect now proactively validates provider authentication before marking logged in, with proper bridge state updates for auth failures and graceful degradation for transient issues.


910-912: LGTM!

Properly stops the scheduler on disconnect with a nil guard.


966-978: LGTM!

Logout cleanup centralized through purgeLoginData for consistent AI-owned state purging.


992-995: LGTM!

Clean use of sdk.BuildChatInfoWithFallback with cmp.Or for concise fallback chain.


1259-1287: LGTM!

Profile prompt supplement correctly reads from persisted login config via loginConfigSnapshot.


1528-1532: LGTM!

Clean provider type check using the loginMetadata helper.


1648-1682: LGTM!

Model cache management correctly migrated to use loginStateSnapshot/updateLoginState with proper initialization and atomic updates.


1684-1695: LGTM!

Model info lookup correctly uses loginStateSnapshot for cache access.


1706-1747: LGTM!

Generated files metadata correctly persisted to the AI-owned transcript table with proper fallback handling when the transcript message doesn't exist yet.


1749-1754: LGTM!

Added limit field to historyLoadResult struct for tracking history load limits.


2046-2048: LGTM!

Logging helper correctly uses sdk.LoggerFromContext for consistent context-based logger resolution.


2124-2143: LGTM!

Debounced message handling correctly uses SDK helpers for ID/timestamp generation and the updated saveUserMessage for dual persistence.

batuhan added 2 commits April 12, 2026 20:44
Add reusable DB and JSON helpers and tidy desktop/OpenClaw integrations. Introduces pkg/aidb.BlobScope with scoped Load/Save/Delete helpers and unmarshalJSONField/unmarshalMapJSONField helpers in ai/bridge_db.go. Replace many per-bridge DB scope types with BlobScope usage in codex and openclaw, simplify JSON blob handling, and remove legacy metadata parsing. Add desktopAccountNetwork stub and centralize desktop-network handling, plus tryResolveDesktopTarget to deduplicate desktop message target resolution. Change execDelete to return errors and aggregate failures during purgeLoginData. Reorder savePortal to persist before AI-specific state save. Update OpenClaw login flow to use NewLogin then LoadConnectAndCompleteLogin and improve rollback handling. Bump github.com/beeper/desktop-api-go to v0.5.0 and apply related test adjustments and small fixes across AI and SDK tests.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 17

Caution

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

⚠️ Outside diff range comments (5)
bridges/ai/subagent_spawn.go (1)

333-349: ⚠️ Potential issue | 🟠 Major

Fail the spawn when the seed prompt can't be persisted.

After this refactor, the initial subagent task only gets stored through persistInternalPrompt. If that write fails, the run still starts and the child session loses the persisted seed state, so follow-up history/context can diverge after reloads or retries.

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

In `@bridges/ai/subagent_spawn.go` around lines 333 - 349, The code currently
continues to create and start a subagent run even if persistInternalPrompt
fails; change the flow in the block using persistInternalPrompt so that if
persistInternalPrompt(ctx, childPortal, eventID, promptContext, false,
"subagent", time.Now()) returns an error you abort the spawn: log the error
(oc.loggerForContext(ctx).Warn/Err already present) and return the error (or
otherwise propagate failure) instead of proceeding to create runID, constructing
subagentRun, calling oc.registerSubagentRun(run), and oc.startSubagentRun(...);
ensure no registration or start occurs when persistInternalPrompt fails so the
child session isn’t started without persisted seed state.
bridges/ai/handlematrix.go (1)

383-413: ⚠️ Potential issue | 🟠 Major

Don't continue the edit flow after a transcript upsert failure.

History and regeneration now read from aiTranscriptTable, so warning and continuing here can leave the bridge row updated while the transcript row stays stale. Subsequent history reads/regeneration will still use the old body.

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

In `@bridges/ai/handlematrix.go` around lines 383 - 413, The
persistAITranscriptMessage call's error is only logged and the edit flow
continues, which can leave bridge rows updated while aiTranscriptTable remains
stale; change the block that calls persistAITranscriptMessage(ctx, oc, portal,
transcriptMsg) so that on error you log the failure and immediately return the
error (or wrap it) to stop further processing (prevent oc.notifySessionMutation,
any metadata Update, and oc.regenerateFromEdit from running). Locate the
persistAITranscriptMessage call and its surrounding code (including references
to oc.loggerForContext, edit.EditTarget, oc.notifySessionMutation, and
oc.regenerateFromEdit) and implement an early return on error instead of merely
warning. Ensure the caller receives the error so upstream behavior can handle
it.
bridges/ai/sessions_tools.go (1)

572-580: ⚠️ Potential issue | 🟠 Major

Don't treat transcript read failures as "no activity".

Returning 0 on any error makes sessions_list silently drop active chats when activeMinutes is used, because callers can't distinguish "no messages" from "transcript query failed". Bubble the error up, or at least log it and skip the activity filter on failure.

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

In `@bridges/ai/sessions_tools.go` around lines 572 - 580, lastMessageTimestamp
currently swallows any getAIHistoryMessages error by returning 0 which makes
callers (e.g. sessions_list) treat transcript read failures as "no activity";
change the signature of lastMessageTimestamp(ctx context.Context, portal
*bridgev2.Portal) to return (int64, error), propagate and return the error from
oc.getAIHistoryMessages, and update callers (such as sessions_list) to handle
the error by logging it and skipping the activeMinutes filter (or otherwise
treating an error as "unknown activity") instead of treating it as timestamp 0;
keep the final successful path to return
messages[len(messages)-1].Timestamp.UnixMilli() with a nil error.
bridges/codex/directory_manager.go (1)

147-160: ⚠️ Potential issue | 🟠 Major

Roll back persisted welcome state if portal creation fails.

saveCodexPortalState happens before sdk.EnsurePortalLifecycle. If lifecycle creation errors, the AwaitingCwdSetup record is left behind, and the next welcomeCodexPortals pass can treat that half-created portal as the existing welcome room. Please defer the save or clear the state on the error path.

Minimal fix
 	created, err := sdk.EnsurePortalLifecycle(ctx, sdk.PortalLifecycleOptions{
 		Login:             cc.UserLogin,
 		Portal:            portal,
 		ChatInfo:          info,
 		SaveBeforeCreate:  true,
 		AIRoomKind:        sdk.AIRoomKindAgent,
 		ForceCapabilities: true,
 	})
 	if err != nil {
+		_ = clearCodexPortalState(ctx, portal)
 		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 147 - 160, The code
currently calls saveCodexPortalState before sdk.EnsurePortalLifecycle, leaving
an AwaitingCwdSetup record if EnsurePortalLifecycle fails; change this by
deferring the persistent save until after EnsurePortalLifecycle returns
successfully (i.e., call saveCodexPortalState only when
sdk.EnsurePortalLifecycle succeeds), or, if you must persist before creation,
ensure you roll back on error by calling the state-clearing function (e.g.,
clearCodexPortalState or equivalent) in the error path after
sdk.EnsurePortalLifecycle returns an error; update the block around
composeCodexChatInfo, saveCodexPortalState, and sdk.EnsurePortalLifecycle
accordingly.
bridges/ai/chat.go (1)

745-764: ⚠️ Potential issue | 🟠 Major

Save the AI sidecar only after the portal save succeeds.

listAllChatPortals() now reads aichats_portal_state directly. If this upsert succeeds and sdk.ConfigureDMPortal(... Save: true) fails, the failed chat still leaves a live sidecar row behind and can show up later as a stale session portal.

Suggested fix
 	portal.Metadata = pmeta
-	if err := saveAIPortalState(ctx, portal, pmeta); err != nil {
-		return nil, nil, fmt.Errorf("failed to save portal state: %w", err)
-	}
 
 	if err := sdk.ConfigureDMPortal(ctx, sdk.ConfigureDMPortalParams{
 		Portal:      portal,
 		Title:       title,
@@
 	}); err != nil {
 		return nil, nil, fmt.Errorf("failed to save portal: %w", err)
 	}
+	if err := saveAIPortalState(ctx, portal, pmeta); err != nil {
+		return nil, nil, fmt.Errorf("failed to save portal state: %w", err)
+	}
 	oc.ensureGhostDisplayName(ctx, modelID)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/chat.go` around lines 745 - 764, The current flow saves the AI
portal sidecar via saveAIPortalState before calling sdk.ConfigureDMPortal, which
can leave a stale aichats_portal_state row if ConfigureDMPortal fails; change
the order so that sdk.ConfigureDMPortal(...) with Save: true and its
MutatePortal closure is executed first and only after it returns successfully
call saveAIPortalState(ctx, portal, pmeta) (keep portal.Metadata = pmeta where
appropriate) so the sidecar upsert happens only when the portal save has
succeeded.
♻️ Duplicate comments (3)
bridges/openclaw/manager.go (1)

594-605: ⚠️ Potential issue | 🟠 Major

Keep the post-resolution resync after resolving a synthetic DM key.

After Line 604 rewrites state.OpenClawSessionKey to the real key, the guard at Line 621 can no longer match the synthetic-DM case, so syncSessions never runs. That leaves the portal keyed to the synthetic session until some later full sync, and early events on the real session can still miss resolvePortal().

Suggested fix
 	sessionKey := strings.TrimSpace(state.OpenClawSessionKey)
+	resolvedSyntheticDM := false
 	if state.OpenClawDMCreatedFromContact && state.OpenClawSessionID == "" && isOpenClawSyntheticDMSessionKey(state.OpenClawSessionKey) {
 		if resolvedKey, err := gateway.ResolveSessionKey(ctx, state.OpenClawSessionKey); err == nil {
 			resolvedKey = strings.TrimSpace(resolvedKey)
 			if resolvedKey != "" {
 				updated := *state
 				updated.OpenClawSessionKey = resolvedKey
 				if err := saveOpenClawPortalState(ctx, msg.Portal, m.client.UserLogin, &updated); err != nil {
 					return nil, err
 				}
 				state.OpenClawSessionKey = resolvedKey
 				sessionKey = resolvedKey
+				resolvedSyntheticDM = true
 			}
 		}
 	}
@@
-	if state.OpenClawDMCreatedFromContact && state.OpenClawSessionID == "" && isOpenClawSyntheticDMSessionKey(state.OpenClawSessionKey) {
+	if resolvedSyntheticDM {
 		go func() {
 			if err := m.syncSessions(m.client.BackgroundContext(ctx)); err != nil {
 				m.client.Log().Debug().Err(err).Str("session_key", sessionKey).Msg("Failed to refresh OpenClaw sessions after synthetic DM message")
 			}
 		}()
 	}

Also applies to: 621-627

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

In `@bridges/openclaw/manager.go` around lines 594 - 605, The code updates
state.OpenClawSessionKey with a resolved real key (via
gateway.ResolveSessionKey) and persists it (saveOpenClawPortalState) but then
returns without performing the synthetic-DM specific resync, so
syncSessions/resolvePortal() never runs for the newly-resolved real session;
after successfully saving the updated state (and before returning), explicitly
trigger the same resync path you would have taken for a synthetic DM key: call
the session synchronization routine (syncSessions) and/or resolvePortal() for
the portal using the updated OpenClawSessionKey and m.client.UserLogin so early
events on the real session are processed immediately even though
isOpenClawSyntheticDMSessionKey no longer matches. Ensure you use the updated
state variable when invoking these functions and handle any errors returned from
syncSessions/resolvePortal() before returning.
bridges/ai/logout_cleanup.go (1)

20-20: ⚠️ Potential issue | 🟠 Major

Cleanup failures are still non-fatal here.

purgeLoginData now aggregates delete errors, but it still only logs them and then clears in-memory state. Logout can therefore continue with partially retained rows, which makes retry/recovery much harder.

Also applies to: 98-105

🤖 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 20, The purgeLoginData function
currently swallows delete errors and proceeds to clear in-memory state; change
its signature to return an error (e.g., func purgeLoginData(ctx context.Context,
login *bridgev2.UserLogin) error), aggregate any deletion failures into a single
error and return it instead of only logging, and update its callers (the logout
flow that invokes purgeLoginData) to check the returned error and
abort/propagate the logout when purgeLoginData fails so rows are not cleared
from memory on partial DB failures; keep references to purgeLoginData and the
logout caller when making the changes.
bridges/ai/scheduler_rooms.go (1)

24-24: ⚠️ Potential issue | 🟠 Major

Propagate scheduler portal-save failures instead of swallowing them.

Both paths still use savePortalQuiet, so the scheduler can return success after OtherUserID or metadata/name updates failed to persist. Since these functions already return error, this is the right place to surface the failure.

Suggested fix
-	s.client.savePortalQuiet(ctx, portal, "scheduler room")
+	if err := s.client.savePortal(ctx, portal, "scheduler room"); err != nil {
+		return "", fmt.Errorf("failed to persist scheduler room: %w", err)
+	}
-		s.client.savePortalQuiet(ctx, portal, "scheduler metadata update")
+		if err := s.client.savePortal(ctx, portal, "scheduler metadata update"); err != nil {
+			return nil, fmt.Errorf("failed to persist scheduler metadata update: %w", err)
+		}

Also applies to: 90-91

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

In `@bridges/ai/scheduler_rooms.go` at line 24, The scheduler currently calls
s.client.savePortalQuiet(...) which swallows persistence errors; replace those
calls in scheduler_rooms.go (the call at the top and the similar calls around
lines 90-91) with the non-quiet saver (e.g. s.client.savePortal(ctx, portal,
"scheduler room")) and propagate any returned error up (return the error from
the scheduler function) instead of ignoring it so failures to persist
OtherUserID/metadata/name surface to callers.
🧹 Nitpick comments (1)
bridges/codex/portal_state_db.go (1)

127-140: Please lock this bespoke portal-key format down with a round-trip test.

This parser is intentionally the inverse of the key format emitted by this bridge, so the main risk here is future format drift. A focused test for PortalKey.String()parseCodexPortalKey on id, id/receiver, and empty-input cases would make this much safer to change later.

Based on learnings: parseCodexPortalKey is intentionally a bespoke inverse of the portal-key format emitted by this bridge, and the mautrix bridgev2/networkid version used here does not provide a canonical ParsePortalKey API; prefer documenting the invariant and covering it with round-trip tests.

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

In `@bridges/codex/portal_state_db.go` around lines 127 - 140, Add a focused
round-trip unit test that asserts PortalKey.String() and parseCodexPortalKey are
inverses: create PortalKey values for (1) only ID, (2) ID with Receiver, and (3)
the zero/empty case, call String() on each, parse the result with
parseCodexPortalKey, and assert equality (including trimmed whitespace and
empty-input handling). Name the test to reference parseCodexPortalKey and
PortalKey.String and include assertions for the boolean ok return value to
ensure the parser handles id, id/receiver, and empty-input cases exactly as
intended.
🤖 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/account_hints.go`:
- Around line 15-19: The desktopAccountNetwork function currently returns a
constant "unknown" which collapses all desktop accounts into one synthetic
network; replace this by extracting a stable network identifier from the
available beeperdesktopapi.Account metadata (e.g., Service/Provider/API host,
environment, or any other unique field on Account) inside desktopAccountNetwork,
or if those fields aren’t present compute a deterministic fallback (e.g., a
"desktop:<stable-hash-of-account-unique-fields>") so networks are distinct;
update any callers like formatDesktopAccountID to use the new network value (or
stop using network-derived IDs) so accounts from different desktop networks are
not deduplicated.

In `@bridges/ai/agentstore.go`:
- Around line 528-533: Normalize room.Name (trim whitespace and treat empty-only
names as unset) before assigning it to resp.PortalInfo.Name so it matches what
client.applyPortalRoomName(ctx, portal, room.Name) uses; call the same
normalization/trimming used by applyPortalRoomName on room.Name, and only set
resp.PortalInfo.Name = &normalizedName when normalizedName is non-empty
(otherwise leave resp.PortalInfo.Name nil) to avoid creating blank or mismatched
persisted room names.
- Around line 86-98: saveAgent and deleteAgent dereference s.client.UserLogin
unguarded and can panic for a nil/half-initialized AgentStoreAdapter; mirror the
safe-guarding used by loadCustomAgents/loadCustomAgent: first check that s !=
nil, s.client != nil, and s.client.UserLogin is non-empty (or whatever the read
helpers check) and return nil (or the same safe response) if any are missing,
then proceed to acquire s.mu and call
saveCustomAgentForLogin/deleteCustomAgentForLogin; update the functions
saveAgent and deleteAgent to perform that pre-check before locking to avoid
panics.

In `@bridges/ai/chat.go`:
- Line 630: The call to oc.savePortalQuiet(ctx, portal, "agent config") (and the
similar call at the other site) ignores the save result; change these to capture
the returned error and abort/return immediately on failure so
provisioning/materialization does not proceed with only in-memory identity
changes. Specifically, update the savePortalQuiet invocation(s) to: err :=
oc.savePortalQuiet(ctx, portal, "agent config") and if err != nil return err (or
otherwise stop the surrounding operation and propagate/log the error), ensuring
the surrounding function (the provisioning/materialization flow) does not
continue when persistence fails.

In `@bridges/ai/client.go`:
- Around line 623-644: The saveUserMessage flow currently swallows DB/transcript
persistence failures which causes the user turn to be dropped from history;
change AIClient.saveUserMessage to return an error (propagate errors from
oc.UserLogin.Bridge.DB.Message.Insert and persistAITranscriptMessage and from
GetPortalByKey when persistence is required) instead of just logging, and update
callers dispatchOrQueueCore and handleDebouncedMessages to check the returned
error and bail out (stop processing/queueing) when saveUserMessage returns an
error so transcript persistence is treated as part of message acceptance;
reference AIClient.saveUserMessage, persistAITranscriptMessage,
dispatchOrQueueCore, and handleDebouncedMessages when making these changes.

In `@bridges/ai/custom_agents_db.go`:
- Around line 63-78: The stored JSON still contains the original untrimmed
agent.ID while the DB row key uses the trimmed agentID; before marshaling (prior
to json.Marshal(agent)) assign the normalized agentID back to agent.ID so the
content_json matches the row key (i.e., set agent.ID = agentID), then proceed to
marshal and execute the upsert (referencing agentID, agent, json.Marshal(agent),
and scope.db.Exec against aiCustomAgentsTable).

In `@bridges/ai/desktop_api_native_test.go`:
- Around line 12-19: desktopAccountNetwork() is a stub returning "unknown",
breaking network-dependent logic in matchDesktopChatsByLabelAliases,
filterDesktopChatsByResolveOptions and canonical account ID handling; replace
the TODO stub in bridges/ai/account_hints.go with implementation that derives
network from Account.AccountID by parsing known encodings (e.g., if AccountID
contains '_' use the substring before the first '_' as network; otherwise, try
splitting by '-' and map common prefixes like "local", "wa", "ig", "telegram",
"whatsapp" to logical network names) and return a sensible default only if no
pattern matches, and update the test fixtures in
bridges/ai/desktop_api_native_test.go to use realistic AccountIDs that encode
network (e.g., "local-telegram_123..." or "wa_...") so
matchDesktopChatsByLabelAliases and filterDesktopChatsByResolveOptions can
operate correctly.

In `@bridges/ai/internal_prompt_db.go`:
- Around line 148-159: The helper hasInternalPromptHistory should not mask DB
errors; change its signature to return (bool, error) and make it propagate the
query error instead of returning false on failure: in hasInternalPromptHistory
(and where portalScopeForPortal and aiInternalMessagesTable are referenced)
return (true/false, err) where err is non-nil when
scope.db.QueryRow(...).Scan(&count) fails; update all callers to handle the
error (or, if immediate caller needs a conservative fallback, treat a non-nil
error as "true" only after logging the error). Ensure the new behavior logs DB
errors and avoids silently treating errors as "no history."
- Around line 119-122: The json.Unmarshal call in loadInternalPromptHistory
currently returns nil, err for invalid rawTurnData which aborts the entire
replay; change it to treat malformed JSON as a per-row corruption: catch the
unmarshal error, log or record the failure (including rawTurnData and the error)
and continue to the next row instead of returning. Locate the
json.Unmarshal([...], &raw) in loadInternalPromptHistory and replace the return
with a continue (and optional debug log); keep DecodeTurnData's existing per-row
handling intact so only the bad row is skipped.

In `@bridges/ai/login_loaders.go`:
- Around line 35-38: The code calls loginMetadata(existing.UserLogin) and
immediately dereferences .Provider which can panic if loginMetadata returns nil
for a cached login; update the logic around existing.UserLogin and loginMetadata
to nil-check the returned metadata before using .Provider (e.g., call m :=
loginMetadata(existing.UserLogin); if m != nil { existingProvider =
strings.TrimSpace(m.Provider) } else { existingProvider = "" } ), and ensure
similar defensive checks are applied when calling
loginCredentialBaseURL(existing.loginConfigSnapshot(context.Background())) if
that helper can return nil/invalid data so the loader can safely decide rebuild
vs reuse without panicking.
- Around line 96-103: The code assumes meta is non-nil after calling
loginMetadata(login) but loginMetadata can return nil; update the block around
loadAILoginConfig and the call to oc.resolveProviderAPIKeyForConfig so you first
ensure meta is non-nil (e.g., after "if meta == nil { meta =
loginMetadata(login) }" check again), and if still nil use a safe default
provider value (empty string) or return a clear error; then call
oc.resolveProviderAPIKeyForConfig with that guarded provider value (reference
symbols: meta, loginMetadata, loadAILoginConfig,
oc.resolveProviderAPIKeyForConfig) so the function no longer dereferences
meta.Provider when meta can be nil.

In `@bridges/ai/login.go`:
- Around line 256-261: The rollback call to login.Delete is using the original
request context (ctx) which may be canceled or timed out; change the cleanup to
create and use a fresh bounded background context (e.g., ctxCleanup :=
context.WithTimeout(context.Background(), <shortDuration>) and defer cancel())
when invoking login.Delete(ctxCleanup, status.BridgeState{},
bridgev2.DeleteOpts{...}) so the rollback runs reliably even if the original ctx
is expired; keep the same Delete call and options but ensure you cancel the
cleanup context after use.

In `@bridges/ai/tools_message_actions.go`:
- Around line 26-31: Trim whitespace from the emoji input before validation and
removal: call strings.TrimSpace on args["emoji"] when assigning to emoji so the
empty check and the call to btc.Client.removeReaction(ctx, btc.Portal,
targetEventID, emoji) use the normalized value; update the emoji variable in
this block (the one that currently does emoji, _ := args["emoji"].(string)) and
ensure the trimmed emoji is used in the error message and the removeReaction
call.

In `@bridges/codex/directory_manager.go`:
- Around line 251-256: The loop is currently ignoring errors from
clearCodexPortalState which can leave DB portal rows behind; change the logic in
the loop that iterates portals so you capture and handle the error returned by
clearCodexPortalState(ctx, portal) (e.g., if err != nil return or propagate the
error after logging) before calling cc.deletePortalOnly; ensure
clearCodexPortalState failures are not swallowed so listCodexPortalStateRecords
cannot return stale records—use the existing symbols loadCodexPortalState,
clearCodexPortalState, cc.cleanupImportedPortalState, and cc.deletePortalOnly to
locate and update the behavior.
- Around line 234-240: The code is incorrectly skipping DB-backed Codex rooms by
checking in-memory portal metadata; remove the portalMeta(portal) /
meta.IsCodexRoom check in the loop that calls
cc.UserLogin.Bridge.GetExistingPortalByKey so persisted Codex state records
(e.g., the variable record selected earlier) are treated as the source of
truth—delete the conditional that uses portalMeta(portal) and !meta.IsCodexRoom
so unbridge/forget logic relies only on the persisted record and existence of
portal from GetExistingPortalByKey.

In `@bridges/codex/portal_state_db.go`:
- Around line 108-110: When parseCodexPortalKey(portalKeyRaw) returns ok==false
in bridges/codex/portal_state_db.go, log the raw portalKeyRaw (and possibly an
identifying message) before the continue so malformed or drifted portal_key
values are recorded like the malformed-JSON branch; add a
processLogger/Logger.Errorf or equivalent call immediately before the continue
referencing portalKeyRaw and context (e.g., "invalid portal_key, raw value: %s")
to ensure bad rows leave a breadcrumb for backfill/discovery.

In `@bridges/openclaw/manager.go`:
- Around line 1752-1756: The code currently returns early when
loadOpenClawPortalState fails, dropping approval handling; instead, catch and
log the error, set state to a safe empty/default value (the same structure used
in handleApprovalResolved as a fallback), and continue to call
resolveOpenClawAgentID with that empty state so the approval is still registered
and prompted locally; update the loadOpenClawPortalState error path to log the
failure and assign the empty state before calling resolveOpenClawAgentID (refer
to loadOpenClawPortalState, resolveOpenClawAgentID, and handleApprovalResolved
to mirror the safe fallback behavior).

---

Outside diff comments:
In `@bridges/ai/chat.go`:
- Around line 745-764: The current flow saves the AI portal sidecar via
saveAIPortalState before calling sdk.ConfigureDMPortal, which can leave a stale
aichats_portal_state row if ConfigureDMPortal fails; change the order so that
sdk.ConfigureDMPortal(...) with Save: true and its MutatePortal closure is
executed first and only after it returns successfully call
saveAIPortalState(ctx, portal, pmeta) (keep portal.Metadata = pmeta where
appropriate) so the sidecar upsert happens only when the portal save has
succeeded.

In `@bridges/ai/handlematrix.go`:
- Around line 383-413: The persistAITranscriptMessage call's error is only
logged and the edit flow continues, which can leave bridge rows updated while
aiTranscriptTable remains stale; change the block that calls
persistAITranscriptMessage(ctx, oc, portal, transcriptMsg) so that on error you
log the failure and immediately return the error (or wrap it) to stop further
processing (prevent oc.notifySessionMutation, any metadata Update, and
oc.regenerateFromEdit from running). Locate the persistAITranscriptMessage call
and its surrounding code (including references to oc.loggerForContext,
edit.EditTarget, oc.notifySessionMutation, and oc.regenerateFromEdit) and
implement an early return on error instead of merely warning. Ensure the caller
receives the error so upstream behavior can handle it.

In `@bridges/ai/sessions_tools.go`:
- Around line 572-580: lastMessageTimestamp currently swallows any
getAIHistoryMessages error by returning 0 which makes callers (e.g.
sessions_list) treat transcript read failures as "no activity"; change the
signature of lastMessageTimestamp(ctx context.Context, portal *bridgev2.Portal)
to return (int64, error), propagate and return the error from
oc.getAIHistoryMessages, and update callers (such as sessions_list) to handle
the error by logging it and skipping the activeMinutes filter (or otherwise
treating an error as "unknown activity") instead of treating it as timestamp 0;
keep the final successful path to return
messages[len(messages)-1].Timestamp.UnixMilli() with a nil error.

In `@bridges/ai/subagent_spawn.go`:
- Around line 333-349: The code currently continues to create and start a
subagent run even if persistInternalPrompt fails; change the flow in the block
using persistInternalPrompt so that if persistInternalPrompt(ctx, childPortal,
eventID, promptContext, false, "subagent", time.Now()) returns an error you
abort the spawn: log the error (oc.loggerForContext(ctx).Warn/Err already
present) and return the error (or otherwise propagate failure) instead of
proceeding to create runID, constructing subagentRun, calling
oc.registerSubagentRun(run), and oc.startSubagentRun(...); ensure no
registration or start occurs when persistInternalPrompt fails so the child
session isn’t started without persisted seed state.

In `@bridges/codex/directory_manager.go`:
- Around line 147-160: The code currently calls saveCodexPortalState before
sdk.EnsurePortalLifecycle, leaving an AwaitingCwdSetup record if
EnsurePortalLifecycle fails; change this by deferring the persistent save until
after EnsurePortalLifecycle returns successfully (i.e., call
saveCodexPortalState only when sdk.EnsurePortalLifecycle succeeds), or, if you
must persist before creation, ensure you roll back on error by calling the
state-clearing function (e.g., clearCodexPortalState or equivalent) in the error
path after sdk.EnsurePortalLifecycle returns an error; update the block around
composeCodexChatInfo, saveCodexPortalState, and sdk.EnsurePortalLifecycle
accordingly.

---

Duplicate comments:
In `@bridges/ai/logout_cleanup.go`:
- Line 20: The purgeLoginData function currently swallows delete errors and
proceeds to clear in-memory state; change its signature to return an error
(e.g., func purgeLoginData(ctx context.Context, login *bridgev2.UserLogin)
error), aggregate any deletion failures into a single error and return it
instead of only logging, and update its callers (the logout flow that invokes
purgeLoginData) to check the returned error and abort/propagate the logout when
purgeLoginData fails so rows are not cleared from memory on partial DB failures;
keep references to purgeLoginData and the logout caller when making the changes.

In `@bridges/ai/scheduler_rooms.go`:
- Line 24: The scheduler currently calls s.client.savePortalQuiet(...) which
swallows persistence errors; replace those calls in scheduler_rooms.go (the call
at the top and the similar calls around lines 90-91) with the non-quiet saver
(e.g. s.client.savePortal(ctx, portal, "scheduler room")) and propagate any
returned error up (return the error from the scheduler function) instead of
ignoring it so failures to persist OtherUserID/metadata/name surface to callers.

In `@bridges/openclaw/manager.go`:
- Around line 594-605: The code updates state.OpenClawSessionKey with a resolved
real key (via gateway.ResolveSessionKey) and persists it
(saveOpenClawPortalState) but then returns without performing the synthetic-DM
specific resync, so syncSessions/resolvePortal() never runs for the
newly-resolved real session; after successfully saving the updated state (and
before returning), explicitly trigger the same resync path you would have taken
for a synthetic DM key: call the session synchronization routine (syncSessions)
and/or resolvePortal() for the portal using the updated OpenClawSessionKey and
m.client.UserLogin so early events on the real session are processed immediately
even though isOpenClawSyntheticDMSessionKey no longer matches. Ensure you use
the updated state variable when invoking these functions and handle any errors
returned from syncSessions/resolvePortal() before returning.

---

Nitpick comments:
In `@bridges/codex/portal_state_db.go`:
- Around line 127-140: Add a focused round-trip unit test that asserts
PortalKey.String() and parseCodexPortalKey are inverses: create PortalKey values
for (1) only ID, (2) ID with Receiver, and (3) the zero/empty case, call
String() on each, parse the result with parseCodexPortalKey, and assert equality
(including trimmed whitespace and empty-input handling). Name the test to
reference parseCodexPortalKey and PortalKey.String and include assertions for
the boolean ok return value to ensure the parser handles id, id/receiver, and
empty-input cases exactly as intended.
🪄 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: 5f44ccfc-3c44-4a02-a291-eaba4776a81f

📥 Commits

Reviewing files that changed from the base of the PR and between 22ded85 and e63c327.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (100)
  • bridges/ai/account_hints.go
  • bridges/ai/agentstore.go
  • bridges/ai/bridge_db.go
  • bridges/ai/chat.go
  • bridges/ai/chat_fork_test.go
  • bridges/ai/client.go
  • bridges/ai/command_registry.go
  • bridges/ai/custom_agents_db.go
  • bridges/ai/delete_chat.go
  • bridges/ai/desktop_api_native_test.go
  • bridges/ai/desktop_api_sessions.go
  • bridges/ai/desktop_api_sessions_test.go
  • bridges/ai/error_logging.go
  • bridges/ai/events.go
  • bridges/ai/events_test.go
  • bridges/ai/gravatar.go
  • bridges/ai/handleai.go
  • bridges/ai/handlematrix.go
  • bridges/ai/handler_interfaces.go
  • bridges/ai/integration_host.go
  • bridges/ai/internal_prompt_db.go
  • bridges/ai/login.go
  • bridges/ai/login_loaders.go
  • bridges/ai/login_loaders_test.go
  • bridges/ai/login_state_db.go
  • bridges/ai/logout_cleanup.go
  • bridges/ai/logout_cleanup_test.go
  • bridges/ai/media_understanding_runner.go
  • bridges/ai/media_understanding_runner_openai_test.go
  • bridges/ai/message_parts.go
  • bridges/ai/metadata.go
  • bridges/ai/metadata_test.go
  • bridges/ai/persistence_boundaries_test.go
  • bridges/ai/portal_materialize.go
  • bridges/ai/portal_state_db.go
  • bridges/ai/prompt_builder.go
  • bridges/ai/reaction_handling.go
  • bridges/ai/reaction_handling_test.go
  • bridges/ai/room_info.go
  • bridges/ai/room_meta_bridgev2_test.go
  • bridges/ai/scheduler_heartbeat_test.go
  • bridges/ai/scheduler_rooms.go
  • bridges/ai/sessions_tools.go
  • bridges/ai/sessions_visibility_test.go
  • bridges/ai/status_text.go
  • bridges/ai/streaming_output_handlers.go
  • bridges/ai/streaming_persistence.go
  • bridges/ai/subagent_spawn.go
  • bridges/ai/tool_approvals.go
  • bridges/ai/tool_approvals_rules.go
  • bridges/ai/tool_configured.go
  • bridges/ai/tools.go
  • bridges/ai/tools_message_actions.go
  • bridges/ai/tools_message_desktop.go
  • bridges/ai/transcript_db.go
  • bridges/codex/constructors.go
  • bridges/codex/directory_manager.go
  • bridges/codex/portal_state_db.go
  • bridges/openclaw/catalog.go
  • bridges/openclaw/identifiers.go
  • bridges/openclaw/login.go
  • bridges/openclaw/manager.go
  • bridges/openclaw/metadata.go
  • bridges/openclaw/provisioning.go
  • bridges/opencode/client.go
  • bridges/opencode/connector.go
  • bridges/opencode/opencode_instance_state.go
  • cmd/agentremote/main.go
  • cmd/agentremote/profile.go
  • cmd/agentremote/profile_test.go
  • cmd/agentremote/run_bridge.go
  • cmd/internal/bridgeentry/bridgeentry.go
  • go.mod
  • pkg/agents/tools/params.go
  • pkg/aidb/001-init.sql
  • pkg/aidb/json_blob_table.go
  • pkg/integrations/memory/integration.go
  • pkg/integrations/memory/sessions.go
  • pkg/matrixevents/matrixevents.go
  • pkg/shared/toolspec/toolspec.go
  • sdk/approval_flow.go
  • sdk/approval_flow_test.go
  • sdk/approval_reaction_helpers.go
  • sdk/approval_reaction_helpers_test.go
  • sdk/client.go
  • sdk/commands.go
  • sdk/connector_builder_test.go
  • sdk/connector_hooks_test.go
  • sdk/conversation.go
  • sdk/conversation_state.go
  • sdk/conversation_state_test.go
  • sdk/conversation_test.go
  • sdk/helpers.go
  • sdk/load_user_login.go
  • sdk/login_handle.go
  • sdk/login_helpers.go
  • sdk/matrix_actions.go
  • sdk/portal_lifecycle.go
  • sdk/room_features.go
  • sdk/runtime.go
💤 Files with no reviewable changes (5)
  • bridges/ai/chat_fork_test.go
  • bridges/ai/error_logging.go
  • bridges/ai/events_test.go
  • bridges/ai/events.go
  • bridges/ai/command_registry.go
✅ Files skipped from review due to trivial changes (5)
  • bridges/ai/sessions_visibility_test.go
  • bridges/ai/message_parts.go
  • bridges/ai/scheduler_heartbeat_test.go
  • bridges/ai/metadata_test.go
  • bridges/ai/portal_state_db.go
🚧 Files skipped from review as they are similar to previous changes (13)
  • bridges/ai/portal_materialize.go
  • bridges/ai/gravatar.go
  • bridges/ai/tool_configured.go
  • bridges/ai/desktop_api_sessions_test.go
  • bridges/ai/media_understanding_runner.go
  • bridges/ai/streaming_output_handlers.go
  • bridges/ai/reaction_handling.go
  • bridges/codex/constructors.go
  • bridges/openclaw/catalog.go
  • bridges/ai/prompt_builder.go
  • bridges/ai/delete_chat.go
  • bridges/opencode/client.go
  • bridges/ai/bridge_db.go
📜 Review details
🧰 Additional context used
🧠 Learnings (10)
📓 Common learnings
Learnt from: batuhan
Repo: beeper/agentremote PR: 103
File: bridges/codex/portal_state_db.go:127-140
Timestamp: 2026-04-12T18:51:08.539Z
Learning: Repo beeper/agentremote PR 103: In bridges/codex/portal_state_db.go, parseCodexPortalKey is intentionally a bespoke inverse of the portal-key format emitted by this bridge, and the mautrix bridgev2/networkid version used here does not provide a canonical ParsePortalKey API. Do not recommend switching to a non-existent parser; prefer documenting the invariant and covering it with round-trip tests.
Learnt from: batuhan
Repo: beeper/agentremote PR: 103
File: bridges/ai/metadata.go:95-100
Timestamp: 2026-04-12T17:47:00.858Z
Learning: Repo beeper/agentremote: For unreleased AI bridges (package ai and related), do not add or propose migration, legacy-compat, or dual-read/backfill code paths (e.g., metadata/portal-state fallbacks). Keep only the new sidecar/state approach with no legacy compatibility.
Learnt from: batuhan
Repo: beeper/agentremote PR: 103
File: bridges/codex/portal_state_db.go:78-92
Timestamp: 2026-04-12T17:55:54.825Z
Learning: Repo beeper/agentremote PR 103: In bridges/codex/portal_state_db.go, loadCodexPortalState(ctx, portal) intentionally implements a “LoadScopedOrNew” contract and must always return a non-nil *codexPortalState (empty zero state) when scope is missing or no row exists; callers already rely on state != nil.
Learnt from: batuhan
Repo: beeper/agentremote PR: 103
File: bridges/ai/login_state_db.go:0-0
Timestamp: 2026-04-12T17:47:17.903Z
Learning: For the unreleased AI bridge (package bridges/ai), do not include migration or legacy-compat behavior. Specifically, do not carry over legacy UserLoginMetadata fields (e.g., NextChatIndex, LastHeartbeatEvent) into the new login runtime state; treat prior metadata migration suggestions as not applicable.
📚 Learning: 2026-04-12T17:47:17.903Z
Learnt from: batuhan
Repo: beeper/agentremote PR: 103
File: bridges/ai/login_state_db.go:0-0
Timestamp: 2026-04-12T17:47:17.903Z
Learning: In the unreleased AI bridge implementation under bridges/ai, do not add migration logic or legacy-compat behavior. When building the new login runtime state, do not copy/carry over legacy UserLoginMetadata fields (e.g., NextChatIndex, LastHeartbeatEvent) from older schemas into the new state. Treat any prior migration/compat suggestions for UserLoginMetadata in this area as not applicable for the new login runtime state.

Applied to files:

  • bridges/ai/status_text.go
  • bridges/ai/media_understanding_runner_openai_test.go
  • bridges/ai/account_hints.go
  • bridges/ai/scheduler_rooms.go
  • bridges/ai/desktop_api_native_test.go
  • bridges/ai/subagent_spawn.go
  • bridges/ai/login_loaders_test.go
  • bridges/ai/logout_cleanup_test.go
  • bridges/ai/reaction_handling_test.go
  • bridges/ai/room_info.go
  • bridges/ai/sessions_tools.go
  • bridges/ai/persistence_boundaries_test.go
  • bridges/ai/streaming_persistence.go
  • bridges/ai/login_state_db.go
  • bridges/ai/tools_message_desktop.go
  • bridges/ai/tool_approvals.go
  • bridges/ai/tools_message_actions.go
  • bridges/ai/tool_approvals_rules.go
  • bridges/ai/metadata.go
  • bridges/ai/login_loaders.go
  • bridges/ai/agentstore.go
  • bridges/ai/desktop_api_sessions.go
  • bridges/ai/room_meta_bridgev2_test.go
  • bridges/ai/handleai.go
  • bridges/ai/login.go
  • bridges/ai/handlematrix.go
  • bridges/ai/internal_prompt_db.go
  • bridges/ai/transcript_db.go
  • bridges/ai/handler_interfaces.go
  • bridges/ai/logout_cleanup.go
  • bridges/ai/tools.go
  • bridges/ai/custom_agents_db.go
  • bridges/ai/integration_host.go
  • bridges/ai/client.go
  • bridges/ai/chat.go
📚 Learning: 2026-04-12T18:51:08.539Z
Learnt from: batuhan
Repo: beeper/agentremote PR: 103
File: bridges/codex/portal_state_db.go:127-140
Timestamp: 2026-04-12T18:51:08.539Z
Learning: Repo beeper/agentremote PR 103: In bridges/codex/portal_state_db.go, parseCodexPortalKey is intentionally a bespoke inverse of the portal-key format emitted by this bridge, and the mautrix bridgev2/networkid version used here does not provide a canonical ParsePortalKey API. Do not recommend switching to a non-existent parser; prefer documenting the invariant and covering it with round-trip tests.

Applied to files:

  • bridges/ai/status_text.go
  • bridges/ai/scheduler_rooms.go
  • bridges/ai/subagent_spawn.go
  • bridges/ai/login_loaders_test.go
  • bridges/ai/reaction_handling_test.go
  • bridges/ai/room_info.go
  • bridges/ai/sessions_tools.go
  • bridges/ai/persistence_boundaries_test.go
  • bridges/ai/streaming_persistence.go
  • bridges/ai/login_state_db.go
  • bridges/ai/tool_approvals.go
  • bridges/ai/metadata.go
  • bridges/ai/agentstore.go
  • bridges/ai/room_meta_bridgev2_test.go
  • bridges/ai/handleai.go
  • bridges/codex/portal_state_db.go
  • bridges/ai/login.go
  • bridges/openclaw/login.go
  • bridges/ai/handlematrix.go
  • bridges/openclaw/metadata.go
  • bridges/ai/internal_prompt_db.go
  • bridges/ai/transcript_db.go
  • bridges/ai/handler_interfaces.go
  • bridges/ai/logout_cleanup.go
  • bridges/ai/tools.go
  • bridges/openclaw/provisioning.go
  • bridges/ai/custom_agents_db.go
  • bridges/ai/integration_host.go
  • bridges/codex/directory_manager.go
  • bridges/ai/client.go
  • bridges/ai/chat.go
  • bridges/openclaw/manager.go
  • bridges/openclaw/identifiers.go
📚 Learning: 2026-04-12T17:55:54.825Z
Learnt from: batuhan
Repo: beeper/agentremote PR: 103
File: bridges/codex/portal_state_db.go:78-92
Timestamp: 2026-04-12T17:55:54.825Z
Learning: Repo beeper/agentremote PR 103: In bridges/codex/portal_state_db.go, loadCodexPortalState(ctx, portal) intentionally implements a “LoadScopedOrNew” contract and must always return a non-nil *codexPortalState (empty zero state) when scope is missing or no row exists; callers already rely on state != nil.

Applied to files:

  • bridges/ai/status_text.go
  • bridges/ai/scheduler_rooms.go
  • bridges/ai/subagent_spawn.go
  • bridges/ai/login_loaders_test.go
  • bridges/ai/reaction_handling_test.go
  • bridges/ai/room_info.go
  • bridges/ai/sessions_tools.go
  • bridges/ai/persistence_boundaries_test.go
  • bridges/ai/streaming_persistence.go
  • bridges/ai/login_state_db.go
  • bridges/ai/tool_approvals.go
  • bridges/ai/metadata.go
  • bridges/ai/agentstore.go
  • bridges/ai/desktop_api_sessions.go
  • bridges/ai/room_meta_bridgev2_test.go
  • bridges/ai/handleai.go
  • bridges/codex/portal_state_db.go
  • bridges/ai/login.go
  • bridges/openclaw/login.go
  • bridges/ai/handlematrix.go
  • bridges/openclaw/metadata.go
  • bridges/ai/internal_prompt_db.go
  • bridges/ai/transcript_db.go
  • bridges/ai/logout_cleanup.go
  • bridges/ai/tools.go
  • bridges/openclaw/provisioning.go
  • bridges/ai/custom_agents_db.go
  • bridges/ai/integration_host.go
  • bridges/codex/directory_manager.go
  • bridges/ai/client.go
  • bridges/ai/chat.go
  • bridges/openclaw/manager.go
  • bridges/openclaw/identifiers.go
📚 Learning: 2026-04-12T17:47:00.858Z
Learnt from: batuhan
Repo: beeper/agentremote PR: 103
File: bridges/ai/metadata.go:95-100
Timestamp: 2026-04-12T17:47:00.858Z
Learning: For the unreleased AI bridge code under bridges/ai (including the ai package and related files), do not add or propose migration paths, legacy-compatibility fallbacks, or dual-read/backfill logic (e.g., metadata/portal-state fallbacks). Review changes to ensure the implementation uses only the new sidecar/state approach and does not include any compatibility layers intended to support older data/behavior.

Applied to files:

  • bridges/ai/status_text.go
  • bridges/ai/media_understanding_runner_openai_test.go
  • bridges/ai/account_hints.go
  • bridges/ai/scheduler_rooms.go
  • bridges/ai/desktop_api_native_test.go
  • bridges/ai/subagent_spawn.go
  • bridges/ai/login_loaders_test.go
  • bridges/ai/logout_cleanup_test.go
  • bridges/ai/reaction_handling_test.go
  • bridges/ai/room_info.go
  • bridges/ai/sessions_tools.go
  • bridges/ai/persistence_boundaries_test.go
  • bridges/ai/streaming_persistence.go
  • bridges/ai/login_state_db.go
  • bridges/ai/tools_message_desktop.go
  • bridges/ai/tool_approvals.go
  • bridges/ai/tools_message_actions.go
  • bridges/ai/tool_approvals_rules.go
  • bridges/ai/metadata.go
  • bridges/ai/login_loaders.go
  • bridges/ai/agentstore.go
  • bridges/ai/desktop_api_sessions.go
  • bridges/ai/room_meta_bridgev2_test.go
  • bridges/ai/handleai.go
  • bridges/ai/login.go
  • bridges/ai/handlematrix.go
  • bridges/ai/internal_prompt_db.go
  • bridges/ai/transcript_db.go
  • bridges/ai/handler_interfaces.go
  • bridges/ai/logout_cleanup.go
  • bridges/ai/tools.go
  • bridges/ai/custom_agents_db.go
  • bridges/ai/integration_host.go
  • bridges/ai/client.go
  • bridges/ai/chat.go
📚 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/reaction_handling_test.go
  • bridges/ai/room_info.go
  • bridges/ai/tool_approvals.go
  • bridges/ai/metadata.go
  • bridges/ai/login_loaders.go
  • bridges/ai/handlematrix.go
  • bridges/ai/client.go
📚 Learning: 2026-04-12T17:46:44.941Z
Learnt from: batuhan
Repo: beeper/agentremote PR: 103
File: bridges/openclaw/metadata.go:310-337
Timestamp: 2026-04-12T17:46:44.941Z
Learning: For the OpenClaw bridge (bridges/openclaw/*), the project intentionally does not include migration or legacy‑compatibility code (e.g., no seeding from legacy login metadata in loadOpenClawLoginState). Avoid suggesting migration fallbacks for this unreleased bridge.

Applied to files:

  • bridges/ai/login_state_db.go
  • bridges/ai/metadata.go
  • bridges/codex/portal_state_db.go
  • bridges/ai/logout_cleanup.go
📚 Learning: 2026-04-12T17:47:00.858Z
Learnt from: batuhan
Repo: beeper/agentremote PR: 103
File: bridges/ai/metadata.go:95-100
Timestamp: 2026-04-12T17:47:00.858Z
Learning: Repo beeper/agentremote: For unreleased AI bridges (package ai and related), do not add or propose migration, legacy-compat, or dual-read/backfill code paths (e.g., metadata/portal-state fallbacks). Keep only the new sidecar/state approach with no legacy compatibility.

Applied to files:

  • bridges/codex/portal_state_db.go
  • bridges/openclaw/login.go
  • bridges/openclaw/metadata.go
  • bridges/openclaw/provisioning.go
  • bridges/codex/directory_manager.go
  • bridges/openclaw/manager.go
  • bridges/openclaw/identifiers.go
📚 Learning: 2026-04-12T17:47:17.903Z
Learnt from: batuhan
Repo: beeper/agentremote PR: 103
File: bridges/ai/login_state_db.go:0-0
Timestamp: 2026-04-12T17:47:17.903Z
Learning: For the unreleased AI bridge (package bridges/ai), do not include migration or legacy-compat behavior. Specifically, do not carry over legacy UserLoginMetadata fields (e.g., NextChatIndex, LastHeartbeatEvent) into the new login runtime state; treat prior metadata migration suggestions as not applicable.

Applied to files:

  • bridges/codex/portal_state_db.go
  • bridges/openclaw/login.go
  • bridges/openclaw/metadata.go
  • bridges/openclaw/manager.go
  • bridges/openclaw/identifiers.go
📚 Learning: 2026-04-12T17:46:44.941Z
Learnt from: batuhan
Repo: beeper/agentremote PR: 103
File: bridges/openclaw/metadata.go:310-337
Timestamp: 2026-04-12T17:46:44.941Z
Learning: For the unreleased OpenClaw bridge under bridges/openclaw/, the codebase intentionally omits migration/legacy-compatibility logic. When reviewing files in this directory (e.g., loading/saving OpenClaw login state), do not recommend adding migration fallbacks or legacy seeding logic (such as copying legacy login metadata) unless there is an explicit existing compatibility requirement elsewhere (e.g., via documented specs/CI gates).

Applied to files:

  • bridges/openclaw/login.go
  • bridges/openclaw/metadata.go
  • bridges/openclaw/provisioning.go
  • bridges/openclaw/manager.go
  • bridges/openclaw/identifiers.go
🔇 Additional comments (9)
bridges/ai/media_understanding_runner_openai_test.go (3)

12-25: Nice helper consolidation for test client wiring.

This keeps provider/config setup in one place and makes the test fixtures more consistent.


31-37: Good call-site cleanup for MagicProxy test setup.

Passing provider and login config directly into the helper removes repetitive inline login construction and keeps test intent clearer.

Also applies to: 44-50


59-63: Provider-explicit test setup looks correct.

Using the same helper path for OpenRouter config tests improves consistency across media-understanding test cases.

Also applies to: 108-109

bridges/ai/reaction_handling_test.go (1)

12-32: Good coverage for room-name precedence and fallback behavior.

This test cleanly validates both the preferred bridge-owned portal.Name path and the slug fallback path, which is exactly the risky behavior boundary.

bridges/openclaw/metadata.go (1)

209-221: No issue here. PostgreSQL explicitly supports casting booleans to/from integers (true→1, false→0) via implicit coercion, and both Go's database/sql and pgx explicitly handle scanning INTEGER columns into bool values. The code is correct as-is.

(Optional: Using BOOLEAN in the schema instead of INTEGER would make intent clearer, but this is a style preference, not a functional requirement.)

bridges/ai/login_state_db.go (1)

201-222: Nice fix on cache/DB consistency.

Cloning the loaded state, persisting the clone, and only then swapping oc.loginState avoids leaving the in-memory snapshot ahead of the database after a failed write.

bridges/ai/handlematrix.go (1)

956-969: Saving the portal row before AI state is the right fix.

This removes the earlier "AI state written, portal save failed" partial-commit case.

bridges/ai/persistence_boundaries_test.go (1)

16-118: Good persistence-boundary coverage.

These tests pin the new contract well: bridge rows stay transport-only, AI state stays in transcript/portal-state storage, and bridge-owned room names do not get overwritten.

bridges/ai/room_meta_bridgev2_test.go (1)

14-142: Good bridgev2 room-metadata coverage.

Exercising the handlers plus portal.Save is the right boundary here and should catch regressions in bridge-owned name/topic/avatar persistence.

Comment on lines +15 to +19
// desktopAccountNetwork returns the network identifier for a desktop API account.
// TODO: add Network field to desktop-api-go Account and remove this stub.
func desktopAccountNetwork(_ beeperdesktopapi.Account) string {
return "unknown"
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't collapse every desktop account into the same synthetic network.

Returning a constant "unknown" here makes all downstream bridge/account IDs look like the same network. That breaks network-based filtering and can silently dedupe distinct accounts when two networks reuse the same raw AccountID, because formatDesktopAccountID(...) now produces the same unknown_* key for both. Please derive a real network value here or avoid using network-derived IDs/filters until the value is actually available.

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

In `@bridges/ai/account_hints.go` around lines 15 - 19, The desktopAccountNetwork
function currently returns a constant "unknown" which collapses all desktop
accounts into one synthetic network; replace this by extracting a stable network
identifier from the available beeperdesktopapi.Account metadata (e.g.,
Service/Provider/API host, environment, or any other unique field on Account)
inside desktopAccountNetwork, or if those fields aren’t present compute a
deterministic fallback (e.g., a
"desktop:<stable-hash-of-account-unique-fields>") so networks are distinct;
update any callers like formatDesktopAccountID to use the new network value (or
stop using network-derived IDs) so accounts from different desktop networks are
not deduplicated.

Comment on lines +86 to +98
func (s *AgentStoreAdapter) saveAgent(ctx context.Context, agent *AgentDefinitionContent) error {
if agent == nil {
return nil
}
s.mu.Lock()
defer s.mu.Unlock()

meta := loginMetadata(s.client.UserLogin)
if meta.CustomAgents == nil {
meta.CustomAgents = map[string]*AgentDefinitionContent{}
}
meta.CustomAgents[agent.ID] = agent
return s.client.UserLogin.Save(ctx)
return saveCustomAgentForLogin(ctx, s.client.UserLogin, agent)
}

func (s *AgentStoreAdapter) deleteAgentFromMetadata(ctx context.Context, agentID string) error {
func (s *AgentStoreAdapter) deleteAgent(ctx context.Context, agentID string) error {
s.mu.Lock()
defer s.mu.Unlock()

meta := loginMetadata(s.client.UserLogin)
if meta.CustomAgents == nil {
return nil
}
if _, ok := meta.CustomAgents[agentID]; !ok {
return nil
}
delete(meta.CustomAgents, agentID)
return s.client.UserLogin.Save(ctx)
return deleteCustomAgentForLogin(ctx, s.client.UserLogin, agentID)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard the write helpers the same way as the read helpers.

loadCustomAgents and loadCustomAgent return safely when the adapter or login is missing, but saveAgent and deleteAgent dereference s.client.UserLogin unconditionally. A nil or half-initialized adapter will panic here instead of returning an 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)
 }
📝 Committable suggestion

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

Suggested change
func (s *AgentStoreAdapter) saveAgent(ctx context.Context, agent *AgentDefinitionContent) error {
if agent == nil {
return nil
}
s.mu.Lock()
defer s.mu.Unlock()
meta := loginMetadata(s.client.UserLogin)
if meta.CustomAgents == nil {
meta.CustomAgents = map[string]*AgentDefinitionContent{}
}
meta.CustomAgents[agent.ID] = agent
return s.client.UserLogin.Save(ctx)
return saveCustomAgentForLogin(ctx, s.client.UserLogin, agent)
}
func (s *AgentStoreAdapter) deleteAgentFromMetadata(ctx context.Context, agentID string) error {
func (s *AgentStoreAdapter) deleteAgent(ctx context.Context, agentID string) error {
s.mu.Lock()
defer s.mu.Unlock()
meta := loginMetadata(s.client.UserLogin)
if meta.CustomAgents == nil {
return nil
}
if _, ok := meta.CustomAgents[agentID]; !ok {
return nil
}
delete(meta.CustomAgents, agentID)
return s.client.UserLogin.Save(ctx)
return deleteCustomAgentForLogin(ctx, s.client.UserLogin, agentID)
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 86 - 98, saveAgent and deleteAgent
dereference s.client.UserLogin unguarded and can panic for a
nil/half-initialized AgentStoreAdapter; mirror the safe-guarding used by
loadCustomAgents/loadCustomAgent: first check that s != nil, s.client != nil,
and s.client.UserLogin is non-empty (or whatever the read helpers check) and
return nil (or the same safe response) if any are missing, then proceed to
acquire s.mu and call saveCustomAgentForLogin/deleteCustomAgentForLogin; update
the functions saveAgent and deleteAgent to perform that pre-check before locking
to avoid panics.

Comment on lines 528 to 533
// Apply custom room name if provided.
pm := portalMeta(portal)
originalName := portal.Name
originalNameSet := portal.NameSet
originalTitle := pm.Title
originalTitleGenerated := pm.TitleGenerated

if room.Name != "" {
pm.Title = room.Name
portal.Name = room.Name
portal.NameSet = true
b.client.applyPortalRoomName(ctx, portal, room.Name)
if resp.PortalInfo != nil {
resp.PortalInfo.Name = &room.Name
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Normalize room.Name before copying it into PortalInfo.

applyPortalRoomName trims and ignores whitespace-only names, but resp.PortalInfo.Name still receives the raw input. " " can therefore create a blank Matrix room name while the portal state stays unset, and " foo " leaves the created room and persisted name out of sync.

Suggested fix
-	if room.Name != "" {
-		b.client.applyPortalRoomName(ctx, portal, room.Name)
-		if resp.PortalInfo != nil {
-			resp.PortalInfo.Name = &room.Name
-		}
-	}
+	if trimmedName := strings.TrimSpace(room.Name); trimmedName != "" {
+		b.client.applyPortalRoomName(ctx, portal, trimmedName)
+		if resp.PortalInfo != nil {
+			resp.PortalInfo.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 528 - 533, Normalize room.Name (trim
whitespace and treat empty-only names as unset) before assigning it to
resp.PortalInfo.Name so it matches what client.applyPortalRoomName(ctx, portal,
room.Name) uses; call the same normalization/trimming used by
applyPortalRoomName on room.Name, and only set resp.PortalInfo.Name =
&normalizedName when normalizedName is non-empty (otherwise leave
resp.PortalInfo.Name nil) to avoid creating blank or mismatched persisted room
names.

if err := portal.Save(ctx); err != nil {
return nil, fmt.Errorf("failed to save portal with agent config: %w", err)
}
oc.savePortalQuiet(ctx, portal, "agent config")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't continue when agent portal persistence fails.

Both sites mutate persisted portal identity fields and then ignore the save result. If that write is dropped, provisioning/materialization still continues with only in-memory OtherUserID / ResolvedTarget / avatar state, so a reload can reopen the room as the wrong target.

Suggested fix
-	oc.savePortalQuiet(ctx, portal, "agent config")
+	if err := oc.savePortal(ctx, portal, "agent config"); err != nil {
+		return nil, fmt.Errorf("failed to persist agent config: %w", err)
+	}
-	oc.savePortalQuiet(ctx, portal, "default chat agent config")
+	if err := oc.savePortal(ctx, portal, "default chat agent config"); err != nil {
+		oc.loggerForContext(ctx).Err(err).Msg("Failed to persist default chat agent config")
+		return err
+	}

Also applies to: 1149-1149

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

In `@bridges/ai/chat.go` at line 630, The call to oc.savePortalQuiet(ctx, portal,
"agent config") (and the similar call at the other site) ignores the save
result; change these to capture the returned error and abort/return immediately
on failure so provisioning/materialization does not proceed with only in-memory
identity changes. Specifically, update the savePortalQuiet invocation(s) to: err
:= oc.savePortalQuiet(ctx, portal, "agent config") and if err != nil return err
(or otherwise stop the surrounding operation and propagate/log the error),
ensuring the surrounding function (the provisioning/materialization flow) does
not continue when persistence fails.

Comment on lines 623 to +644
func (oc *AIClient) saveUserMessage(ctx context.Context, evt *event.Event, msg *database.Message) {
if evt != nil {
msg.MXID = evt.ID
}
if _, err := oc.UserLogin.Bridge.GetGhostByID(ctx, msg.SenderID); err != nil {
oc.loggerForContext(ctx).Warn().Err(err).Msg("Failed to ensure user ghost before saving message")
}
if err := oc.UserLogin.Bridge.DB.Message.Insert(ctx, msg); err != nil {
transportMsg := *msg
transportMsg.Metadata = &MessageMetadata{}
if err := oc.UserLogin.Bridge.DB.Message.Insert(ctx, &transportMsg); err != nil {
oc.loggerForContext(ctx).Err(err).Msg("Failed to save message to database")
}
portal, err := oc.UserLogin.Bridge.GetPortalByKey(ctx, msg.Room)
if err != nil || portal == nil {
if err != nil {
oc.loggerForContext(ctx).Warn().Err(err).Msg("Failed to resolve portal for AI transcript persistence")
}
return
}
if err := persistAITranscriptMessage(ctx, oc, portal, msg); err != nil {
oc.loggerForContext(ctx).Warn().Err(err).Msg("Failed to persist AI transcript message")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Treat transcript persistence as part of message acceptance.

getAIHistoryMessages() now drives later context/session reads, so logging and continuing here drops the user turn from future history while the current request still runs. A transient DB error will desync the next prompt.

Suggested fix
-func (oc *AIClient) saveUserMessage(ctx context.Context, evt *event.Event, msg *database.Message) {
+func (oc *AIClient) saveUserMessage(ctx context.Context, evt *event.Event, msg *database.Message) error {
 	if evt != nil {
 		msg.MXID = evt.ID
 	}
@@
 	if err := persistAITranscriptMessage(ctx, oc, portal, msg); err != nil {
-		oc.loggerForContext(ctx).Warn().Err(err).Msg("Failed to persist AI transcript message")
+		return fmt.Errorf("persist AI transcript message: %w", err)
 	}
+	return nil
 }

Update the callers in dispatchOrQueueCore and handleDebouncedMessages to bail out on the returned error instead of continuing.

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

In `@bridges/ai/client.go` around lines 623 - 644, The saveUserMessage flow
currently swallows DB/transcript persistence failures which causes the user turn
to be dropped from history; change AIClient.saveUserMessage to return an error
(propagate errors from oc.UserLogin.Bridge.DB.Message.Insert and
persistAITranscriptMessage and from GetPortalByKey when persistence is required)
instead of just logging, and update callers dispatchOrQueueCore and
handleDebouncedMessages to check the returned error and bail out (stop
processing/queueing) when saveUserMessage returns an error so transcript
persistence is treated as part of message acceptance; reference
AIClient.saveUserMessage, persistAITranscriptMessage, dispatchOrQueueCore, and
handleDebouncedMessages when making these changes.

Comment on lines 26 to 31
emoji, _ := args["emoji"].(string)

removed, err := removeMatrixReactions(ctx, btc, targetEventID, emoji)
if err != nil {
if emoji == "" {
return "", errors.New("action=react with remove requires an explicit emoji")
}
if err := btc.Client.removeReaction(ctx, btc.Portal, targetEventID, emoji); err != nil {
return "", fmt.Errorf("failed to remove reactions: %w", err)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Trim the emoji before attempting removal.

The add path normalizes whitespace, but the remove path now sends the raw string through. Inputs like " 👍 " pass validation here and then fail to match the stored reaction key.

Suggested fix
-	emoji, _ := args["emoji"].(string)
+	emoji, _ := args["emoji"].(string)
+	emoji = strings.TrimSpace(emoji)
 	if emoji == "" {
 		return "", errors.New("action=react with remove requires an explicit emoji")
 	}
📝 Committable suggestion

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

Suggested change
emoji, _ := args["emoji"].(string)
removed, err := removeMatrixReactions(ctx, btc, targetEventID, emoji)
if err != nil {
if emoji == "" {
return "", errors.New("action=react with remove requires an explicit emoji")
}
if err := btc.Client.removeReaction(ctx, btc.Portal, targetEventID, emoji); err != nil {
return "", fmt.Errorf("failed to remove reactions: %w", err)
emoji, _ := args["emoji"].(string)
emoji = strings.TrimSpace(emoji)
if emoji == "" {
return "", errors.New("action=react with remove requires an explicit emoji")
}
if err := btc.Client.removeReaction(ctx, btc.Portal, targetEventID, emoji); err != nil {
return "", fmt.Errorf("failed to remove reactions: %w", err)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/tools_message_actions.go` around lines 26 - 31, Trim whitespace
from the emoji input before validation and removal: call strings.TrimSpace on
args["emoji"] when assigning to emoji so the empty check and the call to
btc.Client.removeReaction(ctx, btc.Portal, targetEventID, emoji) use the
normalized value; update the emoji variable in this block (the one that
currently does emoji, _ := args["emoji"].(string)) and ensure the trimmed emoji
is used in the error message and the removeReaction call.

Comment on lines +234 to 240
portal, err := cc.UserLogin.Bridge.GetExistingPortalByKey(ctx, record.PortalKey)
if err != nil || portal == nil {
continue
}
meta := portalMeta(portal)
if meta == nil || !meta.IsCodexRoom || !meta.ManagedImport || strings.TrimSpace(meta.CodexCwd) != path {
if meta := portalMeta(portal); meta == nil || !meta.IsCodexRoom {
continue
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't gate DB-backed Codex rooms on in-memory PortalMetadata.

This function already selects persisted Codex state records, but Line 238 re-checks portalMeta(portal).IsCodexRoom. That flag is only in-memory (bridges/codex/metadata.go:62-64), so after a restart imported rooms loaded by GetExistingPortalByKey can fail this check and !codex forget stops unbridging them. The persisted state record should be the source of truth here.

Suggested change
 		portal, err := cc.UserLogin.Bridge.GetExistingPortalByKey(ctx, record.PortalKey)
 		if err != nil || portal == nil {
 			continue
 		}
-		if meta := portalMeta(portal); meta == nil || !meta.IsCodexRoom {
-			continue
-		}
 		out = append(out, portal)
 	}
🤖 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 234 - 240, The code is
incorrectly skipping DB-backed Codex rooms by checking in-memory portal
metadata; remove the portalMeta(portal) / meta.IsCodexRoom check in the loop
that calls cc.UserLogin.Bridge.GetExistingPortalByKey so persisted Codex state
records (e.g., the variable record selected earlier) are treated as the source
of truth—delete the conditional that uses portalMeta(portal) and
!meta.IsCodexRoom so unbridge/forget logic relies only on the persisted record
and existence of portal from GetExistingPortalByKey.

Comment on lines 251 to 256
for _, portal := range portals {
meta := portalMeta(portal)
if meta != nil {
cc.cleanupImportedPortalState(meta.CodexThreadID)
if state, err := loadCodexPortalState(ctx, portal); err == nil && state != nil {
cc.cleanupImportedPortalState(state.CodexThreadID)
}
_ = clearCodexPortalState(ctx, portal)
cc.deletePortalOnly(ctx, portal, "codex directory forgotten")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Please don't ignore clearCodexPortalState failures during forget.

deletePortalOnly no longer removes the DB portal row, so this delete is now the only cleanup of persisted Codex tracking. Swallowing the error can leave forgotten rooms discoverable by listCodexPortalStateRecords while the command reports success.

🤖 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 251 - 256, The loop is
currently ignoring errors from clearCodexPortalState which can leave DB portal
rows behind; change the logic in the loop that iterates portals so you capture
and handle the error returned by clearCodexPortalState(ctx, portal) (e.g., if
err != nil return or propagate the error after logging) before calling
cc.deletePortalOnly; ensure clearCodexPortalState failures are not swallowed so
listCodexPortalStateRecords cannot return stale records—use the existing symbols
loadCodexPortalState, clearCodexPortalState, cc.cleanupImportedPortalState, and
cc.deletePortalOnly to locate and update the behavior.

Comment on lines +108 to +110
key, ok := parseCodexPortalKey(portalKeyRaw)
if !ok {
continue
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Warn before dropping rows with a bad portal_key.

Line 109 silently skips records when key parsing fails, so format drift or corrupted rows disappear from directory discovery/backfill with no breadcrumb. Please log the raw key before continue, like the malformed-JSON branch does.

Suggested change
 		key, ok := parseCodexPortalKey(portalKeyRaw)
 		if !ok {
+			zerolog.Ctx(ctx).Warn().Str("portal_key", portalKeyRaw).Msg("skipping malformed codex portal key")
 			continue
 		}
📝 Committable suggestion

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

Suggested change
key, ok := parseCodexPortalKey(portalKeyRaw)
if !ok {
continue
key, ok := parseCodexPortalKey(portalKeyRaw)
if !ok {
zerolog.Ctx(ctx).Warn().Str("portal_key", portalKeyRaw).Msg("skipping malformed codex portal key")
continue
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/codex/portal_state_db.go` around lines 108 - 110, When
parseCodexPortalKey(portalKeyRaw) returns ok==false in
bridges/codex/portal_state_db.go, log the raw portalKeyRaw (and possibly an
identifying message) before the continue so malformed or drifted portal_key
values are recorded like the malformed-JSON branch; add a
processLogger/Logger.Errorf or equivalent call immediately before the continue
referencing portalKeyRaw and context (e.g., "invalid portal_key, raw value: %s")
to ensure bad rows leave a breadcrumb for backfill/discovery.

Comment on lines +1752 to +1756
state, err := loadOpenClawPortalState(ctx, portal, m.client.UserLogin)
if err != nil {
return
}
agentID := resolveOpenClawAgentID(state, sessionKey, payload.Request)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't drop approval requests when portal state fails to load.

state is only used here to improve agent resolution. Returning early means the approval never gets registered or prompted locally, so it stays pending upstream until reconnect/rehydration. Falling back to an empty state is safer, like handleApprovalResolved already does.

Suggested fix
 	state, err := loadOpenClawPortalState(ctx, portal, m.client.UserLogin)
 	if err != nil {
-		return
+		m.client.Log().Warn().Err(err).Str("session_key", sessionKey).Msg("Failed to load OpenClaw portal state for approval request")
+		state = &openClawPortalState{}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/openclaw/manager.go` around lines 1752 - 1756, The code currently
returns early when loadOpenClawPortalState fails, dropping approval handling;
instead, catch and log the error, set state to a safe empty/default value (the
same structure used in handleApprovalResolved as a fallback), and continue to
call resolveOpenClawAgentID with that empty state so the approval is still
registered and prompted locally; update the loadOpenClawPortalState error path
to log the failure and assign the empty state before calling
resolveOpenClawAgentID (refer to loadOpenClawPortalState,
resolveOpenClawAgentID, and handleApprovalResolved to mirror the safe fallback
behavior).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant