Skip to content

fixes#71

Merged
batuhan merged 202 commits intomainfrom
batuhan/of
Mar 16, 2026
Merged

fixes#71
batuhan merged 202 commits intomainfrom
batuhan/of

Conversation

@batuhan
Copy link
Member

@batuhan batuhan commented Mar 11, 2026

No description provided.

@coderabbitai
Copy link

coderabbitai bot commented Mar 11, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b7e552e0-08fc-4fc6-8ec4-1fbbeceb800a

📥 Commits

Reviewing files that changed from the base of the PR and between c25e20b and fd28c5e.

📒 Files selected for processing (29)
  • approval_flow.go
  • approval_flow_test.go
  • approval_prompt_test.go
  • base_reaction_handler.go
  • bridges/ai/agent_loop_steering_test.go
  • bridges/ai/agentstore.go
  • bridges/ai/bootstrap_context_test.go
  • bridges/ai/bridge_db.go
  • bridges/ai/chat.go
  • bridges/ai/chat_login_redirect_test.go
  • bridges/ai/client.go
  • bridges/ai/client_init_test.go
  • bridges/ai/defaults_alignment_test.go
  • bridges/ai/pending_queue.go
  • bridges/ai/portal_send.go
  • bridges/ai/provider_openai_responses_test.go
  • bridges/ai/provisioning.go
  • bridges/ai/provisioning_test.go
  • bridges/ai/reaction_handling.go
  • bridges/ai/reactions.go
  • bridges/ai/response_finalization.go
  • bridges/ai/scheduler_cron.go
  • bridges/ai/streaming_error_handling.go
  • bridges/ai/streaming_init.go
  • bridges/ai/streaming_responses_api.go
  • bridges/ai/streaming_success.go
  • bridges/ai/streaming_ui_helpers.go
  • bridges/ai/subagent_spawn.go
  • bridges/ai/system_events_db.go

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.


📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Interactive approval prompts with reaction support and reasons.
    • Rich, streaming AI responses with tool calls, metadata, and UI updates.
    • Tool approval workflow (including MCP/builtin) with timeouts and auto-approve.
    • Media understanding enhancements for images/audio/video and source attachments.
    • Portal auto-materialization with welcome messages.
  • Improvements

    • Clearer message statuses and error handling (auth/permission/model not found).
    • Agent and model discovery in search/contacts; better DM/chat setup.
    • Smarter follow-ups, queue handling, and typing indicators.
  • Documentation

    • Expanded README with AgentRemote SDK guides and examples.
    • New example integration config.
  • Chores

    • Release packaging via GoReleaser (binaries, Homebrew tap).

Walkthrough

Adds a generic approval/prompt subsystem in package agentremote (flow, prompts, decision reasons, reaction helpers) and integrates it into the AI bridge (bridges/ai): large package renames/refactors, streaming agent-loop adapters (Responses/chat), tool-approval lifecycle, many new tests, config/schema changes, and release tooling.

Changes

Cohort / File(s) Summary
Approval core (agentremote)
approval_flow.go, approval_prompt.go, approval_decision.go, approval_reaction_helpers.go
New ApprovalFlow generic manager, pending approvals, reaper/TTL handling, prompt renderers/presentation types, exported approval-reason constants, placeholder-reaction redaction, synthetic-reaction ghost creation, and APIs to register/resolve/finish approvals.
Approval tests (agentremote)
approval_flow_test.go, approval_prompt_test.go, approval_reaction_helpers_test.go
Extensive unit/integration tests for approval lifecycle, prompt construction, reaction handling/mirroring, placeholder redaction, and synthetic-sender DB insertion.
AI bridge: package rename & refactor
bridges/ai/... (many files; package connectorpackage ai)
Massive cross-cutting rename and refactor: replace bridgeadapter usage with agentremote/SDK, rewrite streaming/turn/message/metadata models, many API call-site updates, SQL table name adjustments, and removal/adjustment of legacy helpers.
Streaming architecture
bridges/ai/streaming_*.go, bridges/ai/streaming_responses_api.go, bridges/ai/streaming_chat_completions.go
New agent-loop streaming framework and adapters for Responses API and Chat Completions: per-turn adapters, streaming state, persistence, error handling, finalization, and UI emission via writer/semantic stream.
Tool approvals & streaming tool lifecycle (AI)
bridges/ai/tool_approvals.go, bridges/ai/streaming_output_handlers.go, bridges/ai/streaming_output_items.go, bridges/ai/streaming_ui_tools_test.go
Tool-approval subsystem (MCP/builtin) integrated with ApprovalFlow; streaming tool registry, upsert/wait/resolve flows, gating, and UI/stream signaling for tool inputs/results.
Agent loop tooling & registry
bridges/ai/agent_loop_*.go, bridges/ai/agent_loop_request_builders.go, bridges/ai/streaming_tool_registry.go
Tool selection and request builders, steering/follow-up handling, sequential tool-call execution, stream tool registry and helpers supporting agent loop runs.
AI connector & login lifecycle
bridges/ai/connector.go, bridges/ai/constructors.go, bridges/ai/login_loaders.go, bridges/ai/login*
New OpenAIConnector, login loaders, AIClient caching/reuse/eviction, broken-login handling, portal materialization helper and related login lifecycle plumbing.
Config, manifests & examples
bridges/ai/integrations_config.go, bridges/ai/beeper_models_generated.go, bridges/ai/integrations_example-config.yaml
Config schema normalization (snake_case), model manifest API updates, new example YAML including tool-approval/tool-policy entries.
Tests: broad expansion
bridges/ai/*_test.go (many new/updated tests)
Large addition of unit and integration tests across streaming, approvals, tool lifecycle, prompt/message construction, catalog, and connector behaviors.
Tooling & docs
.goreleaser.yml, README.md, agentremote.sh, .gitignore
Release packaging config, SDK README and bridge examples, run wrapper script, and minor .gitignore update.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Bridge/AI Client
    participant ApprovalFlow as agentremote.ApprovalFlow
    participant Portal as Matrix Portal
    participant DB as Database
    participant Reaper as Background Reaper

    Client->>ApprovalFlow: Register(approvalID, TTL, data, prompt)
    ApprovalFlow->>DB: Persist pending approval
    ApprovalFlow->>Portal: SendPrompt(build UI/Raw, bind IDs)
    Portal-->>ApprovalFlow: PromptSent(eventID, messageID)
    Note over Portal,ApprovalFlow: Users react to prompt (reactions)
    Portal->>ApprovalFlow: HandleReaction(reaction event)
    ApprovalFlow->>DB: Update pending decision / finalize
    ApprovalFlow->>Portal: Edit prompt to resolved state / mirror reaction
    ApprovalFlow->>Client: Deliver decision callback (optional)
    Reaper->>ApprovalFlow: PeriodicWake
    Reaper->>DB: FindExpired -> ApprovalFlow: FinalizeTimedOut
    ApprovalFlow->>Portal: Send timeout/expiry notice
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch batuhan/of

Copy link

@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: 14

Caution

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

⚠️ Outside diff range comments (6)
pkg/connector/canonical_history.go (1)

94-99: ⚠️ Potential issue | 🟡 Minor

Remove unused stringValue function.

The stringValue helper defined at line 94 is not called anywhere within this file and appears to be dead code from a prior refactoring. This function exists as duplicate implementations in other files (pkg/shared/streamui/recorder.go, bridges/openclaw/manager.go), so removing it from this file will not affect other parts of the codebase.

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

In `@pkg/connector/canonical_history.go` around lines 94 - 99, The function
stringValue is unused dead code in this file; remove the entire stringValue
helper declaration to eliminate duplication (the identical helpers live in
pkg/shared/streamui/recorder.go and bridges/openclaw/manager.go), ensuring no
other symbols in this file reference stringValue and that the file still builds
after deletion.
bridges/opencode/opencodebridge/opencode_messages.go (1)

3-20: ⚠️ Potential issue | 🟡 Minor

Run goimports on this file.

The Go workflow is already failing on this import block, so this still needs to be normalized before merge.

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

In `@bridges/opencode/opencodebridge/opencode_messages.go` around lines 3 - 20,
The import block in opencode_messages.go is not goimports-formatted; run
goimports (or `gofmt -w` with goimports) on this file to automatically group and
sort standard library vs third-party imports, remove unused imports (e.g., check
symbols like context, errors, fmt, os, filepath, strings, time, bridgev2,
networkid, simplevent, event, opencode, media, stringutil) and fix the import
order so the file compiles and the CI import checks pass.
bridges/openclaw/media.go (1)

3-22: ⚠️ Potential issue | 🟡 Minor

Run goimports here as well.

CI is still failing on this file's import block.

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

In `@bridges/openclaw/media.go` around lines 3 - 22, Run goimports on the import
block in this file to sort and tidy imports (remove unused imports and group
stdlib vs external packages) so CI stops failing; specifically update the import
block shown (context, encoding/base64, errors, fmt, net/http, net/url, path,
path/filepath, strings, mautrix/bridgev2, mautrix/event, mautrix/id, and the
local packages jsonutil, media, openclawconv, stringutil) so only actually used
packages remain and the imports are formatted by goimports, then save/commit the
resulting change.
bridges/openclaw/manager.go (1)

886-894: ⚠️ Potential issue | 🟠 Major

Treat unknown approval decisions as non-approved.

The default branch currently turns any unexpected gateway value into an approval. That feeds both the streamed tool-approval-response and ResolveExternal, so a typo or new server-side decision string will be mirrored into Matrix as an allow. Please handle only the known allow values explicitly and default everything else to a non-approved outcome.

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

In `@bridges/openclaw/manager.go` around lines 886 - 894, The function
openClawApprovalDecisionStatus currently treats unknown decisions as approved;
change its default to non-approved by returning false, "" so only explicitly
handled allow cases (e.g., the "allow-always" branch in
openClawApprovalDecisionStatus) result in approval; update the switch's default
branch to return false, "" (and ensure any other known allow strings are added
as explicit cases if needed).
pkg/bridgeadapter/approval_flow.go (1)

547-576: ⚠️ Potential issue | 🟠 Major

Channel-based approvals can still time out after a valid reaction.

On the deliverDecision == nil path, a successful reaction only writes into p.ch; the prompt stays registered until the waiter later calls FinishResolved. That leaves a window where schedulePromptTimeout can finalize the same approval as "timeout", and near expiry Wait can race its timer against the already-buffered decision. The result is nondeterministic timeout/decline behavior for approvals that were actually accepted. Finalize or cancel the timeout as part of the successful channel send instead of deferring it to the waiter.

Also applies to: 665-691

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

In `@pkg/bridgeadapter/approval_flow.go` around lines 547 - 576, The channel-path
race: when deliverDecision == nil the code only sends match.Decision into p.ch
and leaves the prompt registered so schedulePromptTimeout/Wait can still mark it
as "timeout"; to fix, treat a successful non-blocking send to p.ch as final
resolution — either call f.FinishResolved(approvalID, match.Decision)
immediately (or cancel the prompt timeout if you have a per-prompt cancel
function) inside the select case that does p.ch <- match.Decision, ensuring the
prompt is finalized when the decision is delivered; apply the same change to the
other channel-based block mentioned (around the 665-691 range) so both channel
paths cancel/finalize timeouts on successful send.
pkg/bridgeadapter/approval_prompt.go (1)

587-612: ⚠️ Potential issue | 🟠 Major

Don't re-enable allow_always when custom options normalize to empty.

If a caller passes malformed custom options while Presentation.AllowAlways is false, this function falls back to DefaultApprovalOptions() and silently reintroduces the "Always allow" action. The fallback here needs to come from the caller's policy-specific defaults, not the unconditional global default.

Suggested direction
-func normalizeApprovalOptions(options []ApprovalOption) []ApprovalOption {
-	if len(options) == 0 {
-		options = DefaultApprovalOptions()
+func normalizeApprovalOptions(options []ApprovalOption, fallback []ApprovalOption) []ApprovalOption {
+	if len(options) == 0 {
+		options = fallback
 	}
 	out := make([]ApprovalOption, 0, len(options))
 	for _, option := range options {
 		...
 	}
 	if len(out) == 0 {
-		return DefaultApprovalOptions()
+		return fallback
 	}
 	return out
 }

Then pass ApprovalPromptOptions(presentation.AllowAlways) from the builders as the fallback.

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

In `@pkg/bridgeadapter/approval_prompt.go` around lines 587 - 612,
normalizeApprovalOptions currently falls back to DefaultApprovalOptions() and
can re-enable "allow_always" even when Presentation.AllowAlways is false; change
normalizeApprovalOptions to accept a caller-provided fallback (e.g., a
[]ApprovalOption produced by ApprovalPromptOptions(presentation.AllowAlways) or
an explicit allowAlways-derived fallback) and return that fallback when
normalized out is empty instead of DefaultApprovalOptions(); update all
callers/builders to pass ApprovalPromptOptions(presentation.AllowAlways) (or the
appropriate fallback) into normalizeApprovalOptions so policy-specific defaults
are preserved.
🧹 Nitpick comments (5)
pkg/shared/backfillutil/search_test.go (1)

17-45: Add a no-match boundary test.

These cases cover the common hits, but the binary-search helper also returns n when the anchor is after the last timestamp. That edge case is easy to regress and is worth pinning down here as well.

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

In `@pkg/shared/backfillutil/search_test.go` around lines 17 - 45, Add a boundary
test that verifies IndexAtOrAfter returns n when the anchor is after the last
timestamp: create a new test (e.g., TestIndexAtOrAfterNoMatch) that builds a
small times slice, calls IndexAtOrAfter(len(times), func(i int) time.Time {
return times[i] }, anchorAfterLast) where anchorAfterLast is later than the
final element, and assert that the returned index equals len(times); place it
alongside the existing TestIndexAtOrAfter* tests to prevent regressions of the
no-match case.
bridges/codex/metadata_test.go (1)

19-29: Consider renaming TestIsHostAuthLogin_SkipsRemoteLogout for clarity.

The test name suggests it's testing remote logout behavior, but it actually validates that the isHostAuthLogin predicate correctly distinguishes between host and managed auth sources. A more descriptive name would be TestIsHostAuthLogin_DistinguishesManagedFromHost or similar.

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

In `@bridges/codex/metadata_test.go` around lines 19 - 29, The test name
TestIsHostAuthLogin_SkipsRemoteLogout is misleading; rename the test function to
something that reflects its actual intent (e.g.,
TestIsHostAuthLogin_DistinguishesManagedFromHost) so it clearly verifies the
predicate isHostAuthLogin using UserLoginMetadata with CodexAuthSourceHost and
CodexAuthSourceManaged; update the test function declaration accordingly to
match the new descriptive name.
pkg/shared/backfillutil/cursor.go (1)

9-18: Consider validating non-negative cursor values.

ParseCursor accepts negative integers, which could cause out-of-bounds access when used as array indices in pagination. Consider adding a bounds check:

🛡️ Proposed fix to validate non-negative indices
 func ParseCursor(cursor networkid.PaginationCursor) (int, bool) {
 	if cursor == "" {
 		return 0, false
 	}
 	idx, err := strconv.Atoi(string(cursor))
-	if err != nil {
+	if err != nil || idx < 0 {
 		return 0, false
 	}
 	return idx, true
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/shared/backfillutil/cursor.go` around lines 9 - 18, ParseCursor currently
accepts negative integers which can lead to out-of-bounds indexing; after
converting the cursor string to an int in ParseCursor (type
networkid.PaginationCursor), add a non-negative bounds check (ensure idx >= 0)
and return (0, false) for negative values (and keep returning false on parse
error) so callers only receive valid non-negative indices.
pkg/connector/approval_prompt_presentation_test.go (1)

5-34: Consider adding edge case tests for robustness.

The current tests validate the happy path, but consider adding tests for edge cases:

  • Empty toolName and action arguments
  • Empty/nil args or input maps
  • Non-map input types for buildMCPApprovalPresentation

This would ensure the builders handle edge cases gracefully.

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

In `@pkg/connector/approval_prompt_presentation_test.go` around lines 5 - 34, Add
edge-case unit tests for buildBuiltinApprovalPresentation and
buildMCPApprovalPresentation: create new tests (e.g.,
TestBuildBuiltinApprovalPresentation_EdgeCases and
TestBuildMCPApprovalPresentation_EdgeCases) that call
buildBuiltinApprovalPresentation and buildMCPApprovalPresentation with empty
toolName/action (""), with nil and empty map[string]any for args/input, and
verify the returned Presentation still has sane defaults (no panics, non-empty
Title or safe fallback, and predictable AllowAlways/Details behavior). For
buildMCPApprovalPresentation also include a case that simulates a non-map input
where possible (e.g., pass a typed any/interface{} value and assert the function
handles it or returns a controlled error) to ensure the builder handles
unexpected input types gracefully. Ensure assertions mirror existing style
(t.Fatalf on failures) and reference the same helper functions
buildBuiltinApprovalPresentation and buildMCPApprovalPresentation.
pkg/shared/backfillutil/pagination.go (1)

24-43: Clarify that Cursor is backward-only, or honor it in forward mode.

PaginateParams exposes a cursor regardless of direction, but paginateForward never reads it and never returns a continuation cursor. That makes the exported API look bidirectional even though only backward pagination is cursor-driven.

Also applies to: 45-71

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

In `@pkg/shared/backfillutil/pagination.go` around lines 24 - 43, The Paginate API
exposes PaginateParams.Cursor for both directions but only backward pagination
uses it; make this explicit by documenting that Cursor is backward-only and
ensure forward-mode ignores any provided cursor: update the comment above
Paginate and PaginateParams to state Cursor is only used when params.Forward ==
false, add a short guard in Paginate that if params.Forward and params.Cursor is
non-empty it is ignored/cleared before calling paginateForward, and ensure
paginateForward never returns a continuation cursor (empty cursor) so callers
cannot rely on cursor-driven forward pagination; reference symbols: Paginate,
PaginateParams, Cursor, paginateForward, paginateBackward.
🤖 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/codex/approvals_test.go`:
- Around line 95-99: Replace the fixed time.Sleep(50 * time.Millisecond) in the
approvals tests with a polling loop that waits until the handler has registered
the approval or the test context deadline expires: repeatedly call
cc.approvalFlow.Get("123") (and similarly check Resolve(...) where used) until
it returns a non-nil pending with non-nil Data, sleeping briefly (e.g., a few
milliseconds) between attempts and failing if the context's deadline is
exceeded; this removes the flakiness caused by assuming registration completes
within 50ms and directly references approvalFlow.Get and Resolve in the test.

In `@bridges/codex/client.go`:
- Around line 2161-2164: registerToolApproval returns whether the approval ID
was newly created, but the code ignores that and always calls
emitUIToolApprovalRequest which causes duplicate handlers to race; update the
block around registerToolApproval(approvalID, ...) to capture its returned
"created/new" boolean and, if it indicates a duplicate (not new), short-circuit:
do not call emitUIToolApprovalRequest and do not re-enter the UI/wait flow for
this approvalID (return early or otherwise reuse the existing approval result
path), so only the original requester emits the UI prompt and waits.
- Around line 2187-2190: When waitToolApproval(ctx, approvalID) returns ok ==
false, preserve context cancellation by checking ctx.Err(): if
errors.Is(ctx.Err(), context.Canceled) (or ctx.Err() == context.Canceled) then
call emitOutcome(false, "cancelled"); otherwise keep emitOutcome(false,
"timeout"). Update the branch that handles decision, ok :=
cc.waitToolApproval(ctx, approvalID) so cancelled contexts result in "cancelled"
rather than always "timeout".

In `@bridges/codex/metadata.go`:
- Around line 14-18: Add a field to preserve legacy "codex_home_managed"
metadata so old persisted logins still indicate managed auth: update the
UserLoginMetadata struct by adding a CodexHomeManaged bool
`json:"codex_home_managed,omitempty"` field (and add the same field to the
analogous struct referenced later around lines 97–110), so unmarshalling retains
the legacy flag and functions like isManagedAuthLogin/hasManagedCodexLogin can
detect managed logins during upgrade.

In `@bridges/codex/stream_events.go`:
- Around line 18-29: The helper codexThreadPortalKey must guard against empty
thread IDs: change its signature to return (networkid.PortalKey, error), trim
and escape the threadID (strings.TrimSpace then url.PathEscape) and if the
trimmed value is empty return a descriptive error (e.g., fmt.Errorf("empty
threadID")). When non-empty, construct and return the PortalKey as before;
update all callers to handle the error. Ensure the Receiver and ID formatting
stay identical to the existing fmt.Sprintf("codex:%s:thread:%s", ...) logic.

In `@bridges/openclaw/events.go`:
- Around line 12-14: The imports block in bridges/openclaw/events.go is
mis-grouped due to an extra blank line before
"github.com/beeper/agentremote/pkg/shared/openclawconv", causing go-imports-repo
formatting failures; remove the blank line and place openclawconv with the other
internal/project imports (keeping standard library imports in their own group
and third-party imports like "maunium.net/go/mautrix/bridgev2" in a separate
group) so the import groups follow standard Go grouping.

In `@bridges/opencode/opencodebridge/opencode_manager.go`:
- Around line 41-61: buildOpenCodeApprovalPresentation currently ignores
opencode.PermissionRequest.Always and unconditionally sets AllowAlways: true,
which exposes an "always" option even when the upstream request didn't advertise
it; update the function to read req.Always (e.g., treat non-empty slice as
allowed) and set AllowAlways accordingly so the
ApprovalPromptPresentation.AllowAlways mirrors the presence of requested
always-scopes from opencode.PermissionRequest.Always, ensuring DeliverDecision
only sees "always" when upstream permitted it.

In `@docs/matrix-ai-matrix-spec-v1.md`:
- Line 309: The sentence incorrectly treats `m.reaction` and `m.annotation` as
two event types; change it to require the bridge to send bridge-authored
placeholder `m.reaction` events (one per allowed option key) that use the
`m.annotation` relation type, e.g. "The bridge MUST send bridge-authored
placeholder `m.reaction` events on the notice, one for each allowed option key,
using `m.annotation` as the relation type." Ensure the wording references
`m.reaction` as the event type and `m.annotation` as the relation to match the
example.

In `@pkg/bridgeadapter/approval_prompt.go`:
- Around line 58-64: The code currently reassigns key = strings.TrimSpace(key)
before using it to look up values in the map, which loses original keys with
whitespace; instead preserve the original map key for lookup by not mutating
key—use a new variable (e.g., trimmed := strings.TrimSpace(key)) for
label/append operations and keep values[key] for lookup. Update the block that
builds keys (variables keys, values, key) and the similar block around lines
72-75 to use a separate trimmed variable rather than reassigning key.
- Around line 395-400: The normalized approval options are being stored in
metadata but not attached to the returned ApprovalPromptMessage, so callers see
nil; update BuildApprovalPromptMessage (the code paths around the options
normalization at the blocks handling params.Options and the similar block at
lines 420-425) to assign the normalized slice back onto the
ApprovalPromptMessage.Options field before returning (and keep the existing
metadata population). Ensure both normalization branches set message.Options =
options so returned messages include the normalized options.
- Around line 617-620: The AddOptionalDetail helper writes into the provided
input map without guarding against a nil map, which can panic; update
AddOptionalDetail to check if input == nil and if so initialize it with
make(map[string]any) before assigning input[key] = v, then proceed to append to
details and return the (possibly newly created) map and details so callers need
not preinitialize the map.

In `@pkg/bridgeadapter/base_reaction_handler.go`:
- Around line 37-38: EnsureSyntheticReactionSenderGhost is being called as a
best-effort FK guard but its error is ignored; change the call in
base_reaction_handler.go to capture the returned error and log it (using the
same logger or pattern used in pkg/connector/reaction_handling.go) instead of
discarding it so provisioning failures are visible and diagnosable;
specifically, replace the blind assignment "_ =
EnsureSyntheticReactionSenderGhost(ctx, login, msg.Event.Sender)" with an error
capture and a process-appropriate log call that includes the error and context
(e.g., sender ID/login).

In `@pkg/connector/tool_approvals.go`:
- Around line 112-115: The caller is converting any non-ok result into a timeout
even when waitToolApproval returns a cancellation reason; update the
FinishResolved call sites (e.g., where oc.approvalFlow.FinishResolved is invoked
after waitToolApproval in this file) to propagate the actual Reason string from
waitToolApproval instead of unconditionally mapping !ok to
"timed_out"/"timeout"—read the returned Reason from the approval result and pass
it into bridgeadapter.ApprovalDecisionPayload.Reason (and do the same change for
the other similar block that handles approval completion) so the persisted
approval state and emitted UI response match (preserve ApprovalID and Reason
from the waitToolApproval result).

In `@pkg/shared/backfillutil/pagination_test.go`:
- Around line 3-9: The import block in pagination_test.go is not formatted to
the repository's standard; run the project's import formatter (go-imports-repo /
the repo's provided import formatting tool) on this file to reorder and group
the imports (testing, time, and the mautrix packages) according to the repo
rules so CI stops failing; then re-run `gofmt`/formatter and commit the
rewritten file.

---

Outside diff comments:
In `@bridges/openclaw/manager.go`:
- Around line 886-894: The function openClawApprovalDecisionStatus currently
treats unknown decisions as approved; change its default to non-approved by
returning false, "" so only explicitly handled allow cases (e.g., the
"allow-always" branch in openClawApprovalDecisionStatus) result in approval;
update the switch's default branch to return false, "" (and ensure any other
known allow strings are added as explicit cases if needed).

In `@bridges/openclaw/media.go`:
- Around line 3-22: Run goimports on the import block in this file to sort and
tidy imports (remove unused imports and group stdlib vs external packages) so CI
stops failing; specifically update the import block shown (context,
encoding/base64, errors, fmt, net/http, net/url, path, path/filepath, strings,
mautrix/bridgev2, mautrix/event, mautrix/id, and the local packages jsonutil,
media, openclawconv, stringutil) so only actually used packages remain and the
imports are formatted by goimports, then save/commit the resulting change.

In `@bridges/opencode/opencodebridge/opencode_messages.go`:
- Around line 3-20: The import block in opencode_messages.go is not
goimports-formatted; run goimports (or `gofmt -w` with goimports) on this file
to automatically group and sort standard library vs third-party imports, remove
unused imports (e.g., check symbols like context, errors, fmt, os, filepath,
strings, time, bridgev2, networkid, simplevent, event, opencode, media,
stringutil) and fix the import order so the file compiles and the CI import
checks pass.

In `@pkg/bridgeadapter/approval_flow.go`:
- Around line 547-576: The channel-path race: when deliverDecision == nil the
code only sends match.Decision into p.ch and leaves the prompt registered so
schedulePromptTimeout/Wait can still mark it as "timeout"; to fix, treat a
successful non-blocking send to p.ch as final resolution — either call
f.FinishResolved(approvalID, match.Decision) immediately (or cancel the prompt
timeout if you have a per-prompt cancel function) inside the select case that
does p.ch <- match.Decision, ensuring the prompt is finalized when the decision
is delivered; apply the same change to the other channel-based block mentioned
(around the 665-691 range) so both channel paths cancel/finalize timeouts on
successful send.

In `@pkg/bridgeadapter/approval_prompt.go`:
- Around line 587-612: normalizeApprovalOptions currently falls back to
DefaultApprovalOptions() and can re-enable "allow_always" even when
Presentation.AllowAlways is false; change normalizeApprovalOptions to accept a
caller-provided fallback (e.g., a []ApprovalOption produced by
ApprovalPromptOptions(presentation.AllowAlways) or an explicit
allowAlways-derived fallback) and return that fallback when normalized out is
empty instead of DefaultApprovalOptions(); update all callers/builders to pass
ApprovalPromptOptions(presentation.AllowAlways) (or the appropriate fallback)
into normalizeApprovalOptions so policy-specific defaults are preserved.

In `@pkg/connector/canonical_history.go`:
- Around line 94-99: The function stringValue is unused dead code in this file;
remove the entire stringValue helper declaration to eliminate duplication (the
identical helpers live in pkg/shared/streamui/recorder.go and
bridges/openclaw/manager.go), ensuring no other symbols in this file reference
stringValue and that the file still builds after deletion.

---

Nitpick comments:
In `@bridges/codex/metadata_test.go`:
- Around line 19-29: The test name TestIsHostAuthLogin_SkipsRemoteLogout is
misleading; rename the test function to something that reflects its actual
intent (e.g., TestIsHostAuthLogin_DistinguishesManagedFromHost) so it clearly
verifies the predicate isHostAuthLogin using UserLoginMetadata with
CodexAuthSourceHost and CodexAuthSourceManaged; update the test function
declaration accordingly to match the new descriptive name.

In `@pkg/connector/approval_prompt_presentation_test.go`:
- Around line 5-34: Add edge-case unit tests for
buildBuiltinApprovalPresentation and buildMCPApprovalPresentation: create new
tests (e.g., TestBuildBuiltinApprovalPresentation_EdgeCases and
TestBuildMCPApprovalPresentation_EdgeCases) that call
buildBuiltinApprovalPresentation and buildMCPApprovalPresentation with empty
toolName/action (""), with nil and empty map[string]any for args/input, and
verify the returned Presentation still has sane defaults (no panics, non-empty
Title or safe fallback, and predictable AllowAlways/Details behavior). For
buildMCPApprovalPresentation also include a case that simulates a non-map input
where possible (e.g., pass a typed any/interface{} value and assert the function
handles it or returns a controlled error) to ensure the builder handles
unexpected input types gracefully. Ensure assertions mirror existing style
(t.Fatalf on failures) and reference the same helper functions
buildBuiltinApprovalPresentation and buildMCPApprovalPresentation.

In `@pkg/shared/backfillutil/cursor.go`:
- Around line 9-18: ParseCursor currently accepts negative integers which can
lead to out-of-bounds indexing; after converting the cursor string to an int in
ParseCursor (type networkid.PaginationCursor), add a non-negative bounds check
(ensure idx >= 0) and return (0, false) for negative values (and keep returning
false on parse error) so callers only receive valid non-negative indices.

In `@pkg/shared/backfillutil/pagination.go`:
- Around line 24-43: The Paginate API exposes PaginateParams.Cursor for both
directions but only backward pagination uses it; make this explicit by
documenting that Cursor is backward-only and ensure forward-mode ignores any
provided cursor: update the comment above Paginate and PaginateParams to state
Cursor is only used when params.Forward == false, add a short guard in Paginate
that if params.Forward and params.Cursor is non-empty it is ignored/cleared
before calling paginateForward, and ensure paginateForward never returns a
continuation cursor (empty cursor) so callers cannot rely on cursor-driven
forward pagination; reference symbols: Paginate, PaginateParams, Cursor,
paginateForward, paginateBackward.

In `@pkg/shared/backfillutil/search_test.go`:
- Around line 17-45: Add a boundary test that verifies IndexAtOrAfter returns n
when the anchor is after the last timestamp: create a new test (e.g.,
TestIndexAtOrAfterNoMatch) that builds a small times slice, calls
IndexAtOrAfter(len(times), func(i int) time.Time { return times[i] },
anchorAfterLast) where anchorAfterLast is later than the final element, and
assert that the returned index equals len(times); place it alongside the
existing TestIndexAtOrAfter* tests to prevent regressions of the no-match case.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8b4e4bab-0a69-4b20-810c-06f5e5221515

📥 Commits

Reviewing files that changed from the base of the PR and between f0e47c8 and 5fcf659.

📒 Files selected for processing (62)
  • bridges/codex/approvals_test.go
  • bridges/codex/backfill.go
  • bridges/codex/backfill_test.go
  • bridges/codex/client.go
  • bridges/codex/config.go
  • bridges/codex/connector.go
  • bridges/codex/connector_test.go
  • bridges/codex/login.go
  • bridges/codex/metadata.go
  • bridges/codex/metadata_test.go
  • bridges/codex/portal_send.go
  • bridges/codex/stream_events.go
  • bridges/openclaw/approval_presentation_test.go
  • bridges/openclaw/catalog.go
  • bridges/openclaw/client.go
  • bridges/openclaw/events.go
  • bridges/openclaw/gateway_client.go
  • bridges/openclaw/gateway_client_test.go
  • bridges/openclaw/manager.go
  • bridges/openclaw/manager_test.go
  • bridges/openclaw/media.go
  • bridges/openclaw/provisioning.go
  • bridges/openclaw/stream.go
  • bridges/opencode/opencodebridge/approval_presentation_test.go
  • bridges/opencode/opencodebridge/backfill.go
  • bridges/opencode/opencodebridge/opencode_manager.go
  • bridges/opencode/opencodebridge/opencode_media.go
  • bridges/opencode/opencodebridge/opencode_messages.go
  • docs/matrix-ai-matrix-spec-v1.md
  • pkg/bridgeadapter/approval_flow.go
  • pkg/bridgeadapter/approval_flow_test.go
  • pkg/bridgeadapter/approval_prompt.go
  • pkg/bridgeadapter/approval_prompt_test.go
  • pkg/bridgeadapter/approval_reaction_helpers.go
  • pkg/bridgeadapter/approval_reaction_helpers_test.go
  • pkg/bridgeadapter/base_reaction_handler.go
  • pkg/connector/approval_prompt_presentation.go
  • pkg/connector/approval_prompt_presentation_test.go
  • pkg/connector/canonical_history.go
  • pkg/connector/canonical_history_test.go
  • pkg/connector/identifiers.go
  • pkg/connector/image_understanding.go
  • pkg/connector/legacy_multimodal_adapter.go
  • pkg/connector/login.go
  • pkg/connector/managed_beeper.go
  • pkg/connector/media_understanding_runner.go
  • pkg/connector/reaction_handling.go
  • pkg/connector/streaming_chat_completions.go
  • pkg/connector/streaming_function_calls.go
  • pkg/connector/streaming_output_handlers.go
  • pkg/connector/streaming_responses_api.go
  • pkg/connector/streaming_ui_tools.go
  • pkg/connector/strict_cleanup_test.go
  • pkg/connector/toast.go
  • pkg/connector/tool_approvals.go
  • pkg/shared/backfillutil/cursor.go
  • pkg/shared/backfillutil/pagination.go
  • pkg/shared/backfillutil/pagination_test.go
  • pkg/shared/backfillutil/search.go
  • pkg/shared/backfillutil/search_test.go
  • pkg/shared/streamui/tools.go
  • pkg/shared/toolspec/message_schema_test.go
💤 Files with no reviewable changes (7)
  • bridges/codex/config.go
  • pkg/connector/legacy_multimodal_adapter.go
  • pkg/connector/identifiers.go
  • pkg/connector/strict_cleanup_test.go
  • pkg/connector/canonical_history_test.go
  • pkg/shared/toolspec/message_schema_test.go
  • pkg/connector/managed_beeper.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). (1)
  • GitHub Check: build-docker
🧰 Additional context used
🧬 Code graph analysis (29)
pkg/bridgeadapter/base_reaction_handler.go (1)
pkg/bridgeadapter/approval_reaction_helpers.go (1)
  • EnsureSyntheticReactionSenderGhost (26-52)
bridges/opencode/opencodebridge/opencode_messages.go (1)
pkg/shared/media/message_type.go (1)
  • FallbackFilenameForMIME (24-31)
bridges/codex/portal_send.go (1)
bridges/codex/client.go (1)
  • CodexClient (71-109)
bridges/codex/backfill_test.go (1)
bridges/opencode/opencode/types.go (1)
  • Timestamp (8-8)
pkg/connector/approval_prompt_presentation.go (1)
pkg/bridgeadapter/approval_prompt.go (4)
  • ApprovalPromptPresentation (46-50)
  • ApprovalDetail (41-44)
  • AppendDetailsFromMap (54-87)
  • ValueSummary (90-143)
bridges/codex/login.go (2)
bridges/codex/config.go (1)
  • ProviderCodex (11-11)
bridges/codex/metadata.go (1)
  • CodexAuthSourceManaged (25-25)
pkg/shared/backfillutil/search_test.go (1)
pkg/shared/backfillutil/search.go (1)
  • IndexAtOrAfter (10-17)
bridges/openclaw/events.go (1)
pkg/shared/openclawconv/content.go (1)
  • StringsTrimDefault (136-142)
bridges/codex/connector_test.go (3)
pkg/bridgeadapter/identifier_helpers.go (1)
  • MakeUserLoginID (27-34)
bridges/codex/metadata.go (3)
  • UserLoginMetadata (14-22)
  • CodexAuthSourceHost (26-26)
  • CodexAuthSourceManaged (25-25)
bridges/codex/config.go (1)
  • ProviderCodex (11-11)
bridges/opencode/opencodebridge/backfill.go (2)
pkg/shared/backfillutil/pagination.go (2)
  • Paginate (28-43)
  • PaginateParams (9-15)
pkg/shared/backfillutil/search.go (1)
  • IndexAtOrAfter (10-17)
bridges/opencode/opencodebridge/opencode_manager.go (3)
pkg/bridgeadapter/approval_prompt.go (5)
  • ApprovalPromptPresentation (46-50)
  • ApprovalDetail (41-44)
  • ValueSummary (90-143)
  • AppendDetailsFromMap (54-87)
  • DecisionToString (627-635)
bridges/opencode/opencode/types.go (1)
  • PermissionRequest (175-183)
pkg/bridgeadapter/approval_decision.go (1)
  • ApprovalDecisionPayload (9-14)
pkg/connector/streaming_ui_tools.go (1)
pkg/bridgeadapter/approval_prompt.go (1)
  • ApprovalPromptPresentation (46-50)
bridges/codex/metadata_test.go (1)
bridges/codex/metadata.go (3)
  • UserLoginMetadata (14-22)
  • CodexAuthSourceHost (26-26)
  • CodexAuthSourceManaged (25-25)
bridges/openclaw/stream.go (1)
pkg/shared/openclawconv/content.go (1)
  • StringsTrimDefault (136-142)
bridges/codex/approvals_test.go (3)
pkg/bridgeadapter/approval_decision.go (1)
  • ApprovalDecisionPayload (9-14)
bridges/codex/metadata.go (1)
  • PortalMetadata (29-37)
pkg/bridgeadapter/approval_prompt.go (1)
  • ApprovalPromptPresentation (46-50)
bridges/opencode/opencodebridge/approval_presentation_test.go (1)
bridges/opencode/opencode/types.go (1)
  • PermissionRequest (175-183)
pkg/connector/media_understanding_runner.go (1)
pkg/connector/messages.go (3)
  • UnifiedMessage (116-122)
  • ContentTypeAudio (29-29)
  • ContentTypeVideo (30-30)
pkg/connector/reaction_handling.go (1)
pkg/bridgeadapter/approval_reaction_helpers.go (1)
  • EnsureSyntheticReactionSenderGhost (26-52)
pkg/shared/streamui/tools.go (1)
pkg/shared/streamui/emitter.go (1)
  • Emitter (83-86)
pkg/bridgeadapter/approval_prompt_test.go (2)
pkg/bridgeadapter/approval_prompt.go (10)
  • BuildApprovalPromptMessage (311-365)
  • ApprovalPromptPresentation (46-50)
  • ApprovalDetail (41-44)
  • ApprovalReactionKeyAllowOnce (22-22)
  • ApprovalReactionKeyDeny (24-24)
  • ApprovalPromptOptions (175-207)
  • ApprovalReactionKeyAllowAlways (23-23)
  • BuildApprovalResponsePromptMessage (367-426)
  • ApprovalResponsePromptMessageParams (292-301)
  • ApprovalPromptStateResponded (20-20)
pkg/bridgeadapter/approval_decision.go (1)
  • ApprovalDecisionPayload (9-14)
pkg/connector/tool_approvals.go (3)
pkg/bridgeadapter/approval_prompt.go (1)
  • ApprovalPromptPresentation (46-50)
pkg/bridgeadapter/approval_decision.go (1)
  • ApprovalDecisionPayload (9-14)
pkg/runtime/types.go (2)
  • ToolApprovalDecision (190-196)
  • ToolApprovalTimedOut (185-185)
bridges/openclaw/manager_test.go (1)
bridges/opencode/opencode/types.go (1)
  • Message (47-60)
bridges/openclaw/manager.go (3)
pkg/bridgeadapter/approval_prompt.go (3)
  • ApprovalPromptPresentation (46-50)
  • DecisionToString (627-635)
  • ApprovalDetail (41-44)
pkg/shared/openclawconv/content.go (1)
  • StringsTrimDefault (136-142)
pkg/bridgeadapter/approval_decision.go (1)
  • ApprovalDecisionPayload (9-14)
bridges/openclaw/catalog.go (1)
pkg/shared/openclawconv/content.go (1)
  • StringsTrimDefault (136-142)
pkg/shared/backfillutil/pagination_test.go (1)
pkg/shared/backfillutil/pagination.go (2)
  • Paginate (28-43)
  • PaginateParams (9-15)
bridges/openclaw/media.go (3)
pkg/shared/media/message_type.go (1)
  • FallbackFilenameForMIME (24-31)
pkg/shared/openclawconv/content.go (1)
  • StringsTrimDefault (136-142)
pkg/shared/stringutil/normalize.go (1)
  • NormalizeMimeType (13-22)
bridges/codex/connector.go (3)
bridges/codex/codexrpc/client.go (2)
  • ProcessConfig (39-51)
  • Client (53-81)
bridges/codex/metadata.go (1)
  • UserLoginMetadata (14-22)
bridges/codex/config.go (2)
  • ProviderCodex (11-11)
  • Config (13-17)
pkg/bridgeadapter/approval_flow_test.go (6)
bridges/opencode/opencodebridge/bridge.go (1)
  • Bridge (67-70)
pkg/bridgeadapter/approval_flow.go (3)
  • NewApprovalFlow (94-114)
  • ApprovalFlowConfig (25-55)
  • Pending (58-62)
pkg/bridgeadapter/approval_decision.go (1)
  • ApprovalDecisionPayload (9-14)
pkg/bridgeadapter/approval_prompt.go (4)
  • ApprovalPromptRegistration (485-498)
  • DefaultApprovalOptions (209-211)
  • ApprovalReactionKeyAllowOnce (22-22)
  • ApprovalReactionKeyAllowAlways (23-23)
pkg/bridgeadapter/approval_reaction_helpers.go (1)
  • MatrixSenderID (16-21)
bridges/opencode/opencode/types.go (1)
  • Event (206-209)
pkg/bridgeadapter/approval_prompt.go (1)
pkg/bridgeadapter/approval_decision.go (1)
  • ApprovalDecisionPayload (9-14)
🪛 GitHub Actions: Go
bridges/opencode/opencodebridge/opencode_messages.go

[error] 1-1: go-imports-repo reformatted imports in bridges/opencode/opencodebridge/opencode_messages.go.

bridges/openclaw/events.go

[error] 1-1: go-imports-repo reformatted imports in bridges/openclaw/events.go.

pkg/shared/backfillutil/pagination_test.go

[error] 1-1: go-imports-repo reformatted imports in pkg/shared/backfillutil/pagination_test.go.

bridges/openclaw/media.go

[error] 1-1: go-imports-repo reformatted imports in bridges/openclaw/media.go.

bridges/codex/client.go

[error] 1-1: go-imports-repo reformatted imports in bridges/codex/client.go.

🔇 Additional comments (33)
pkg/connector/image_understanding.go (1)

303-307: LGTM - Simplified OpenAI audio handling.

The removal of the legacy legacyUnifiedMessagesNeedChatAdapter check streamlines the logic. The type assertion safely routes OpenAI providers to generateChatCompletions while preserving the generic Generate fallback for other providers.

Note: This introduces a difference from analyzeImageWithModel (line 238), which uses oc.provider.Generate unconditionally. If audio content parts require the chat completions endpoint specifically for OpenAI, this is correct.

pkg/connector/media_understanding_runner.go (1)

955-971: Nice simplification of the OpenRouter routing check.

The new helper makes the dispatch condition explicit and keeps the media-type decision local and easy to audit.

bridges/openclaw/gateway_client.go (1)

83-96: LGTM - Clean simplification of origin parsing.

The updated OriginString() correctly handles only structured JSON objects, returning "" for empty, null, or non-object origins. The re-marshaling step normalizes the JSON representation.

bridges/openclaw/gateway_client_test.go (1)

68-75: LGTM - Test name correctly reflects the updated behavior.

The rename from ...SupportsLegacyAndStructuredOrigin to ...ParsesStructuredOrigin accurately reflects that only structured JSON objects are now supported.

Consider adding edge case tests for completeness (empty origin, "null", non-object JSON like "legacy-string"), but this is optional given the straightforward logic.

pkg/connector/canonical_history.go (1)

18-27: Generated images are returned only when canonical messages exist—this appears intentional.

The refactored logic (line 27 returning nil when canonical is empty) is a deliberate simplification. All callers handle the nil/empty return safely:

  • handlematrix.go checks if len(bundle) > 0 before using the result
  • Both client.go calls unpack the result with ..., which safely handles nil slices in Go

The behavior change is correct as implemented.

pkg/connector/login.go (1)

103-105: The review comment is incorrect. There is no legacyManagedBeeperLoginID function in the codebase. The only managed Beeper login ID function is managedBeeperLoginID, and the guard at lines 103-105 correctly checks this function. The concern about a removed legacy check cannot be verified because no such legacy variant exists or has ever existed in the codebase.

			> Likely an incorrect or invalid review comment.
bridges/opencode/opencodebridge/opencode_messages.go (1)

206-208: Shared fallback filename handling looks good.

Reusing media.FallbackFilenameForMIME removes duplicate MIME-extension logic and keeps OpenCode aligned with the shared media behavior.

bridges/openclaw/manager_test.go (1)

8-93: Nice coverage on the history-mirroring decision tree.

These subtests hit the beeper-idempotency skip, matching/mismatched run markers, and the markerless fallback window.

pkg/connector/streaming_function_calls.go (1)

189-194: The invalid-JSON error path is aligned with the updated emitter API.

Dropping the obsolete trailing flag here keeps this call site in sync with EmitUIToolInputError.

bridges/openclaw/media.go (2)

44-47: Shared filename fallback is a good consolidation.

Using media.FallbackFilenameForMIME keeps attachment uploads consistent with the other bridges and removes duplicate MIME-extension logic.


117-165: Nice consolidation of trim/default parsing.

Routing both block and nested-source extraction through openclawconv.StringsTrimDefault should keep URL, data, MIME, and filename fallback behavior consistent.

Also applies to: 208-232

pkg/bridgeadapter/approval_flow_test.go (1)

19-112: Good coverage on the tricky approval paths.

This file exercises finish-resolved cleanup, placeholder filtering, delivery-error retention, and external mirroring—the brittle edges of the new flow.

Also applies to: 114-257

pkg/connector/streaming_output_handlers.go (2)

59-63: The start-event call is in sync with the slimmer UI API.

Keeping EnsureUIToolInputStart on minimal metadata here avoids pushing approval-specific payloads into the generic start event.


202-240: Good to see a single presentation propagated end-to-end.

Registering and emitting the same approval presentation should keep persisted prompt state and live UI requests consistent.

pkg/connector/reaction_handling.go (1)

27-29: Best-effort ghost provisioning is handled cleanly here.

Warning on failure preserves the reaction flow while still surfacing why the FK guard may not have been applied.

bridges/openclaw/catalog.go (1)

9-9: LGTM!

The migration to openclawconv.StringsTrimDefault is consistent with the broader refactoring effort to consolidate string trimming utilities across OpenClaw bridges. The logic remains unchanged.

Also applies to: 100-100

bridges/openclaw/approval_presentation_test.go (1)

5-21: LGTM!

Good basic test coverage for the approval presentation builder. The test validates that the essential fields (Title, AllowAlways, Details) are properly populated.

bridges/openclaw/events.go (1)

81-83: LGTM!

The migration to openclawconv.StringsTrimDefault is correct and maintains the same fallback logic. All usages follow the correct pattern of (value, fallback).

Also applies to: 105-105, 124-124

bridges/codex/login.go (2)

623-627: LGTM!

The duplicate login detection now correctly filters to only managed-auth logins, which aligns with the introduction of distinct auth sources (managed vs host). This prevents host-auth logins from being counted in the duplicate naming logic.


647-653: LGTM!

The transition from CodexHomeManaged boolean to CodexAuthSource: CodexAuthSourceManaged properly implements the new auth source classification model.

bridges/opencode/opencodebridge/approval_presentation_test.go (1)

9-26: LGTM!

Good test coverage for the OpenCode approval presentation builder. The test validates that the essential fields (Title, AllowAlways, Details) are properly populated for a filesystem write permission request.

bridges/openclaw/provisioning.go (1)

18-18: LGTM!

The migration to openclawconv.StringsTrimDefault is consistent across all usages:

  • Line 309: Agent display name with existing meta value as fallback
  • Line 474: Avatar URL with alternate avatar URL as fallback
  • Lines 551-564: Agent name with ID as fallback for sorting

All follow the correct (value, fallback) pattern.

Also applies to: 309-309, 474-474, 551-552, 563-564

bridges/codex/metadata_test.go (1)

5-17: LGTM!

Good test coverage for the auth source predicate functions. The tests validate the expected behavior for both CodexAuthSourceHost and CodexAuthSourceManaged values.

pkg/bridgeadapter/approval_reaction_helpers_test.go (2)

16-38: LGTM!

Well-structured test helper with proper resource cleanup. The in-memory SQLite setup with SetMaxOpenConns(1) is appropriate for SQLite's single-writer constraint.


40-63: LGTM!

Good test coverage for EnsureSyntheticReactionSenderGhost:

  • Tests idempotency by calling the function twice
  • Verifies the ghost row is created and retrievable
  • Validates the ghost ID matches the expected sender ID
bridges/openclaw/stream.go (1)

18-18: LGTM!

The refactor to use openclawconv.StringsTrimDefault centralizes string trimming logic and provides consistent default value handling. The fallback values ("gateway", "aborted", "assistant") are appropriate for their respective contexts.

Also applies to: 91-91, 126-126, 454-454, 476-476

bridges/codex/connector_test.go (1)

40-132: LGTM!

The tests provide comprehensive coverage for the host-auth login ID functionality:

  • Verifies the dedicated prefix is used for host-auth logins
  • Confirms managed Codex login detection ignores host-auth logins
  • Tests the exceptID exclusion logic
  • Ensures only Codex-managed logins are matched

The test helper hostAuthLoginIDForTest creates a new connector per call, which is fine for test purposes.

pkg/connector/toast.go (1)

21-49: LGTM!

The presentation parameter is correctly added to the function signature and properly forwarded to ApprovalPromptMessageParams. This aligns with the broader PR changes that add structured presentation data to approval prompts.

pkg/connector/approval_prompt_presentation.go (1)

9-55: LGTM!

The builder functions are well-structured:

  • Input trimming prevents whitespace issues
  • Dynamic titles provide context
  • Details are systematically populated with relevant fields
  • AppendDetailsFromMap caps at 8 fields to prevent UI overload
  • The MCP builder handles both map and non-map inputs gracefully via ValueSummary
bridges/opencode/opencodebridge/backfill.go (1)

64-83: LGTM!

The refactor to use backfillutil.Paginate centralizes pagination logic and simplifies this code. The callbacks are correctly implemented:

  • findAnchorIndex handles both message and part ID matching
  • IndexAtOrAfter leverages binary search for efficient timestamp-based positioning

The slicing on Line 81 correctly extracts the batch from the computed range.

bridges/codex/backfill_test.go (1)

10-112: LGTM!

The tests provide solid coverage for the Codex backfill functionality:

  • TestCodexTurnTextPair validates text extraction with deduplication (using last message per ID)
  • TestCodexThreadBackfillEntries ensures correct entry generation with ordered timestamps and unique IDs
  • TestCodexPaginateBackfillBackward verifies pagination cursor mechanics and batch boundaries

The assertions are clear and test the expected invariants thoroughly.

bridges/codex/connector.go (1)

268-269: No issues found. There is only one definition of MakeUserLoginID in the codebase (pkg/bridgeadapter/identifier_helpers.go:27), and the code correctly calls bridgeadapter.MakeUserLoginID(...) without any conflicts or compile breaks.

			> Likely an incorrect or invalid review comment.
bridges/codex/backfill.go (1)

390-397: Verify the epoch unit before converting thread timestamps.

time.Unix(baseUnix, 0) assumes seconds. If createdAt / updatedAt are coming from the Codex JS RPC in milliseconds, these backfilled messages land far in the future and pagination/order break immediately. Even when the fields are absent, falling back to time.Now() makes the same imported history move on every fetch. Please normalize the unit explicitly and keep the fallback deterministic.

@coderabbitai
Copy link

coderabbitai bot commented Mar 11, 2026

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 10 file(s) based on 12 unresolved review comments.

A stacked PR containing fixes has been created.

  • Stacked PR: #72
  • Files modified:
  • bridges/codex/approvals_test.go
  • bridges/codex/backfill.go
  • bridges/codex/client.go
  • bridges/codex/metadata.go
  • bridges/codex/stream_events.go
  • bridges/openclaw/events.go
  • bridges/opencode/opencodebridge/opencode_manager.go
  • docs/matrix-ai-matrix-spec-v1.md
  • pkg/bridgeadapter/approval_prompt.go
  • pkg/bridgeadapter/base_reaction_handler.go

Time taken: 7m 33s

Copy link

@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: 3

Caution

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

⚠️ Outside diff range comments (1)
bridges/opencode/opencodebridge/opencode_parts.go (1)

3-17: ⚠️ Potential issue | 🟡 Minor

Run the repo import formatter before merging.

CI is currently failing on this import block (go-imports-repo reformatted imports), so this needs a quick reformat/pre-commit pass before the PR can land.

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

In `@bridges/opencode/opencodebridge/opencode_parts.go` around lines 3 - 17, The
import block in opencode_parts.go is not formatted to the repository's
go-imports-repo style; run the repo formatter (e.g., goimports -w . or the
project's pre-commit formatter) on the file to reorder and group imports
correctly so they match CI expectations (the import block that contains
"context", "fmt", "strings", "maunium.net/go/mautrix/bridgev2", and the
github.com/... imports should be reordered/grouped by standard, external, and
internal groups). Ensure you stage and commit the resulting changes before
pushing.
♻️ Duplicate comments (5)
pkg/bridgeadapter/approval_prompt.go (3)

613-616: ⚠️ Potential issue | 🟡 Minor

Guard input before writing to it.

AddOptionalDetail panics if a caller passes a nil map. Since the helper is exported, it should initialize the map itself.

💡 Minimal fix
 func AddOptionalDetail(input map[string]any, details []ApprovalDetail, key, label string, ptr *string) (map[string]any, []ApprovalDetail) {
 	if v := ValueSummary(ptr); v != "" {
+		if input == nil {
+			input = map[string]any{}
+		}
 		input[key] = v
 		details = append(details, ApprovalDetail{Label: label, Value: v})
 	}
 	return input, details
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/bridgeadapter/approval_prompt.go` around lines 613 - 616, The helper
AddOptionalDetail can panic when callers pass a nil input map; update
AddOptionalDetail to guard and initialize input if nil (e.g., if input == nil {
input = make(map[string]any) }) before writing to it, then continue to call
ValueSummary(ptr), set input[key]=v and append to details as currently done;
ensure the function returns the (possibly newly created) input map and updated
details so callers receive the non-nil map.

391-421: ⚠️ Potential issue | 🟡 Minor

Return normalized options on response messages too.

This builder normalizes options for metadata, but the returned ApprovalPromptMessage still leaves Options empty. Any caller inspecting the response message directly gets a different view than the metadata carries.

💡 Minimal fix
 	return ApprovalPromptMessage{
 		Body:         body,
 		UIMessage:    uiMessage,
 		Raw:          raw,
 		Presentation: presentation,
+		Options:      options,
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/bridgeadapter/approval_prompt.go` around lines 391 - 421, The
ApprovalPromptMessage being returned does not include the normalized options
even though you computed them; update the return value so the
ApprovalPromptMessage.Options field is set to the normalized options variable
(options) you produced with normalizeApprovalOptions (used earlier), ensuring
callers see the same options as encoded in approvalMessageMetadata and the
UIMessage; modify the return struct in the function that builds the message to
include Options: options.

58-75: ⚠️ Potential issue | 🟡 Minor

Preserve the raw map key when reading values.

Trimming key before the lookup means any entry whose original key has surrounding whitespace becomes unreachable, so the detail is silently dropped even though it exists in values.

💡 Minimal fix
-	keys := make([]string, 0, len(values))
-	for key := range values {
-		key = strings.TrimSpace(key)
-		if key == "" {
+	type detailKey struct {
+		raw   string
+		label string
+	}
+	keys := make([]detailKey, 0, len(values))
+	for rawKey := range values {
+		label := strings.TrimSpace(rawKey)
+		if label == "" {
 			continue
 		}
-		keys = append(keys, key)
+		keys = append(keys, detailKey{raw: rawKey, label: label})
 	}
-	sort.Strings(keys)
+	sort.Slice(keys, func(i, j int) bool { return keys[i].label < keys[j].label })
 	count := 0
 	for _, key := range keys {
 		if count >= max {
 			break
 		}
-		if value := ValueSummary(values[key]); value != "" {
+		if value := ValueSummary(values[key.raw]); value != "" {
 			details = append(details, ApprovalDetail{
-				Label: fmt.Sprintf("%s %s", labelPrefix, key),
+				Label: fmt.Sprintf("%s %s", labelPrefix, key.label),
 				Value: value,
 			})
 			count++
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/bridgeadapter/approval_prompt.go` around lines 58 - 75, The code trims
the loop variable `key` before lookup which makes entries with whitespace in the
original map key unreachable; change the key collection to preserve the original
map key for lookups while using a trimmed form only for sorting/empty checks.
For example, when building the list replace mutating `key` with `originalKey :=
key` and `trimmed := strings.TrimSpace(originalKey)`; skip if `trimmed==""`,
store pairs (originalKey, trimmed) or two parallel slices, then sort by
`trimmed` and when iterating use the preserved `originalKey` for the lookup into
`values` and for constructing `ApprovalDetail` (refer to symbols `keys`,
`values`, `ValueSummary`, `ApprovalDetail`, and `labelPrefix`).
bridges/codex/client.go (2)

2174-2177: ⚠️ Potential issue | 🟡 Minor

Preserve cancellation when the wait aborts.

waitToolApproval already resolves cancelled contexts with ApprovalReasonCancelled, but this branch always reports timeout. That makes the streamed tool-approval-response disagree with the final prompt state.

💡 Minimal fix
 	decision, ok := cc.waitToolApproval(ctx, approvalID)
 	if !ok {
-		return emitOutcome(false, bridgeadapter.ApprovalReasonTimeout)
+		reason := bridgeadapter.ApprovalReasonTimeout
+		if ctx.Err() != nil {
+			reason = bridgeadapter.ApprovalReasonCancelled
+		}
+		return emitOutcome(false, reason)
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/codex/client.go` around lines 2174 - 2177, When waitToolApproval(ctx,
approvalID) returns !ok we currently always emit ApprovalReasonTimeout; instead
preserve cancellation by inspecting the returned decision: if decision != nil
and decision.Reason == bridgeadapter.ApprovalReasonCancelled then call
emitOutcome(false, bridgeadapter.ApprovalReasonCancelled), otherwise fall back
to emitOutcome(false, bridgeadapter.ApprovalReasonTimeout). This ensures the
final outcome matches waitToolApproval's cancelled result.

2148-2151: ⚠️ Potential issue | 🟠 Major

Handle duplicate approval IDs before re-entering the UI flow.

registerToolApproval can return an existing pending approval, but this path ignores created and always emits a fresh prompt + waiter. A retried JSON-RPC request ID will race the original handler: one caller gets the decision, the other times out and returns "decline".

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

In `@bridges/codex/client.go` around lines 2148 - 2151, registerToolApproval can
return an existing pending approval, so change this call site to inspect the
creation result and avoid re-entering the UI flow when an approval already
exists: call registerToolApproval and capture its returned approval object plus
a created boolean (or check an existing flag on the returned object), and if
created is false then do NOT call emitUIToolApprovalRequest again — instead
attach to or wait on the existing approval's waiter/response. Ensure you still
compute approvalTTL but only call emitUIToolApprovalRequest when the approval
was newly created to prevent duplicate prompts and racing JSON-RPC requests.
🧹 Nitpick comments (3)
bridges/openclaw/client.go (2)

544-546: Redundant strings.TrimSpace call.

openclawconv.StringsTrimDefault already trims whitespace from the returned value, so the subsequent strings.TrimSpace(preview) check is redundant.

♻️ Proposed simplification
-	if preview := openclawconv.StringsTrimDefault(meta.OpenClawPreviewSnippet, meta.OpenClawLastMessagePreview); strings.TrimSpace(preview) != "" {
-		appendPart("Recent: " + strings.TrimSpace(preview))
+	if preview := openclawconv.StringsTrimDefault(meta.OpenClawPreviewSnippet, meta.OpenClawLastMessagePreview); preview != "" {
+		appendPart("Recent: " + preview)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/openclaw/client.go` around lines 544 - 546, The code redundantly
calls strings.TrimSpace on preview after openclawconv.StringsTrimDefault already
returns a trimmed string; change the condition to check preview != "" (or
len(preview) > 0) and call appendPart("Recent: " + preview) directly. Update the
block using openclawconv.StringsTrimDefault(meta.OpenClawPreviewSnippet,
meta.OpenClawLastMessagePreview) and appendPart to remove the extra
strings.TrimSpace invocation.

659-659: Redundant strings.TrimSpace wrapper.

Same issue as above: openclawconv.StringsTrimDefault already returns a trimmed value.

♻️ Proposed simplification
-	provider := strings.TrimSpace(openclawconv.StringsTrimDefault(stringValue(structured["provider"]), stringValue(structured["source"])))
+	provider := openclawconv.StringsTrimDefault(stringValue(structured["provider"]), stringValue(structured["source"]))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/openclaw/client.go` at line 659, The code calls strings.TrimSpace
around openclawconv.StringsTrimDefault which already returns a trimmed string;
remove the redundant wrapper by assigning provider directly from
openclawconv.StringsTrimDefault(stringValue(structured["provider"]),
stringValue(structured["source"])) (keep the existing stringValue(...)
arguments), and then remove any now-unused strings import if applicable.
bridges/opencode/opencodebridge/opencode_parts.go (1)

198-199: Prefer a thin wrapper over a mutable package-global function variable.

Line 199 makes this helper rebindable across the whole package. To prevent accidental mutations and test leakage, wrap it in a function instead.

Suggested change
-// toolDisplayTitle is an alias for streamui.ToolDisplayTitle.
-var toolDisplayTitle = streamui.ToolDisplayTitle
+func toolDisplayTitle(toolName string) string {
+	return streamui.ToolDisplayTitle(toolName)
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/opencode/opencodebridge/opencode_parts.go` around lines 198 - 199,
Replace the mutable package-global alias var toolDisplayTitle =
streamui.ToolDisplayTitle with an unexported wrapper function named
toolDisplayTitle that forwards to streamui.ToolDisplayTitle (i.e., declare func
toolDisplayTitle(<same params>) <same results> { return
streamui.ToolDisplayTitle(<params>) }), so the binding is immutable and can't be
reassigned; update any call sites to use the new wrapper signature if necessary.
🤖 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/openclaw/manager.go`:
- Around line 1155-1161: The code currently treats any remote decision other
than "deny" as an approval; change openClawApprovalDecisionStatus/its usage so
that only explicit allow decisions map to Approved=true (e.g., "allow-once" or
"allow-always") and everything else (timeouts, expired, cancelled,
delivery_error, etc.) maps to Approved=false with Reason set to the exact
non-approval token. Update the call site around openClawApprovalDecisionStatus
and m.approvalFlow.ResolveExternal (using approvalID,
bridgeadapter.ApprovalDecisionPayload, and payload.Decision) to set Always only
when payload.Decision equals "allow-always", set Approved true only for allow-*
values, and propagate other decision strings as the Reason so non-approval
states are not treated as allow-once.
- Around line 1343-1346: The early return when runID is empty stops the later
timestamp-based guard from running; remove the `if runID == "" { return true }`
early return so the function continues and the subsequent timestamp-based
protections run even when `payload.RunID` is absent (ensure any later uses of
`runID` handle the empty string safely rather than relying on the early exit).

In `@pkg/bridgeadapter/approval_flow.go`:
- Around line 691-699: The current finishTimedOutApproval function returns early
when promptRegistration(approvalID) is missing, which leaves pending entries and
their done channels dangling when matchReaction removed prompts on expiry;
change finishTimedOutApproval so it does not bail out on missing registration:
attempt to call promptRegistration(approvalID) only to obtain any cleanup info
if present but always proceed to call f.FinishResolved(approvalID,
ApprovalDecisionPayload{ApprovalID: approvalID, Reason: ApprovalReasonTimeout});
optionally log when promptRegistration returned false so callers can see it, but
do not skip the FinishResolved path.

---

Outside diff comments:
In `@bridges/opencode/opencodebridge/opencode_parts.go`:
- Around line 3-17: The import block in opencode_parts.go is not formatted to
the repository's go-imports-repo style; run the repo formatter (e.g., goimports
-w . or the project's pre-commit formatter) on the file to reorder and group
imports correctly so they match CI expectations (the import block that contains
"context", "fmt", "strings", "maunium.net/go/mautrix/bridgev2", and the
github.com/... imports should be reordered/grouped by standard, external, and
internal groups). Ensure you stage and commit the resulting changes before
pushing.

---

Duplicate comments:
In `@bridges/codex/client.go`:
- Around line 2174-2177: When waitToolApproval(ctx, approvalID) returns !ok we
currently always emit ApprovalReasonTimeout; instead preserve cancellation by
inspecting the returned decision: if decision != nil and decision.Reason ==
bridgeadapter.ApprovalReasonCancelled then call emitOutcome(false,
bridgeadapter.ApprovalReasonCancelled), otherwise fall back to
emitOutcome(false, bridgeadapter.ApprovalReasonTimeout). This ensures the final
outcome matches waitToolApproval's cancelled result.
- Around line 2148-2151: registerToolApproval can return an existing pending
approval, so change this call site to inspect the creation result and avoid
re-entering the UI flow when an approval already exists: call
registerToolApproval and capture its returned approval object plus a created
boolean (or check an existing flag on the returned object), and if created is
false then do NOT call emitUIToolApprovalRequest again — instead attach to or
wait on the existing approval's waiter/response. Ensure you still compute
approvalTTL but only call emitUIToolApprovalRequest when the approval was newly
created to prevent duplicate prompts and racing JSON-RPC requests.

In `@pkg/bridgeadapter/approval_prompt.go`:
- Around line 613-616: The helper AddOptionalDetail can panic when callers pass
a nil input map; update AddOptionalDetail to guard and initialize input if nil
(e.g., if input == nil { input = make(map[string]any) }) before writing to it,
then continue to call ValueSummary(ptr), set input[key]=v and append to details
as currently done; ensure the function returns the (possibly newly created)
input map and updated details so callers receive the non-nil map.
- Around line 391-421: The ApprovalPromptMessage being returned does not include
the normalized options even though you computed them; update the return value so
the ApprovalPromptMessage.Options field is set to the normalized options
variable (options) you produced with normalizeApprovalOptions (used earlier),
ensuring callers see the same options as encoded in approvalMessageMetadata and
the UIMessage; modify the return struct in the function that builds the message
to include Options: options.
- Around line 58-75: The code trims the loop variable `key` before lookup which
makes entries with whitespace in the original map key unreachable; change the
key collection to preserve the original map key for lookups while using a
trimmed form only for sorting/empty checks. For example, when building the list
replace mutating `key` with `originalKey := key` and `trimmed :=
strings.TrimSpace(originalKey)`; skip if `trimmed==""`, store pairs
(originalKey, trimmed) or two parallel slices, then sort by `trimmed` and when
iterating use the preserved `originalKey` for the lookup into `values` and for
constructing `ApprovalDetail` (refer to symbols `keys`, `values`,
`ValueSummary`, `ApprovalDetail`, and `labelPrefix`).

---

Nitpick comments:
In `@bridges/openclaw/client.go`:
- Around line 544-546: The code redundantly calls strings.TrimSpace on preview
after openclawconv.StringsTrimDefault already returns a trimmed string; change
the condition to check preview != "" (or len(preview) > 0) and call
appendPart("Recent: " + preview) directly. Update the block using
openclawconv.StringsTrimDefault(meta.OpenClawPreviewSnippet,
meta.OpenClawLastMessagePreview) and appendPart to remove the extra
strings.TrimSpace invocation.
- Line 659: The code calls strings.TrimSpace around
openclawconv.StringsTrimDefault which already returns a trimmed string; remove
the redundant wrapper by assigning provider directly from
openclawconv.StringsTrimDefault(stringValue(structured["provider"]),
stringValue(structured["source"])) (keep the existing stringValue(...)
arguments), and then remove any now-unused strings import if applicable.

In `@bridges/opencode/opencodebridge/opencode_parts.go`:
- Around line 198-199: Replace the mutable package-global alias var
toolDisplayTitle = streamui.ToolDisplayTitle with an unexported wrapper function
named toolDisplayTitle that forwards to streamui.ToolDisplayTitle (i.e., declare
func toolDisplayTitle(<same params>) <same results> { return
streamui.ToolDisplayTitle(<params>) }), so the binding is immutable and can't be
reassigned; update any call sites to use the new wrapper signature if necessary.
🪄 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: 18e47874-a88f-4105-88e4-2604a8031239

📥 Commits

Reviewing files that changed from the base of the PR and between 5fcf659 and 613074b.

📒 Files selected for processing (17)
  • bridges/codex/client.go
  • bridges/openclaw/client.go
  • bridges/openclaw/manager.go
  • bridges/openclaw/media_test.go
  • bridges/opencode/opencodebridge/backfill.go
  • bridges/opencode/opencodebridge/backfill_test.go
  • bridges/opencode/opencodebridge/bridge.go
  • bridges/opencode/opencodebridge/opencode_manager.go
  • bridges/opencode/opencodebridge/opencode_parts.go
  • pkg/bridgeadapter/approval_decision.go
  • pkg/bridgeadapter/approval_flow.go
  • pkg/bridgeadapter/approval_prompt.go
  • pkg/connector/streaming_ui_tools.go
  • pkg/connector/toast.go
  • pkg/connector/tool_approvals.go
  • pkg/connector/tool_execution.go
  • pkg/shared/streamui/tools.go
💤 Files with no reviewable changes (1)
  • pkg/connector/toast.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/connector/tool_approvals.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). (2)
  • GitHub Check: build-docker
  • GitHub Check: build-docker
🧰 Additional context used
🧬 Code graph analysis (11)
pkg/connector/tool_execution.go (1)
pkg/shared/streamui/tools.go (1)
  • ToolDisplayTitle (224-233)
bridges/opencode/opencodebridge/backfill.go (4)
pkg/shared/backfillutil/pagination.go (2)
  • Paginate (28-43)
  • PaginateParams (9-15)
bridges/opencode/opencode/types.go (2)
  • Message (47-60)
  • MessageWithParts (165-168)
pkg/shared/backfillutil/search.go (1)
  • IndexAtOrAfter (10-17)
bridges/opencode/opencodebridge/bridge.go (1)
  • Bridge (71-74)
bridges/opencode/opencodebridge/opencode_manager.go (3)
pkg/bridgeadapter/approval_prompt.go (5)
  • ApprovalPromptPresentation (46-50)
  • ApprovalDetail (41-44)
  • ValueSummary (90-143)
  • AppendDetailsFromMap (54-87)
  • DecisionToString (623-631)
bridges/opencode/opencode/types.go (1)
  • PermissionRequest (175-183)
pkg/bridgeadapter/approval_decision.go (1)
  • ApprovalDecisionPayload (20-25)
bridges/opencode/opencodebridge/backfill_test.go (3)
bridges/opencode/opencodebridge/bridge.go (1)
  • Bridge (71-74)
bridges/opencode/opencode/types.go (6)
  • MessageWithParts (165-168)
  • Message (47-60)
  • Part (69-100)
  • Session (29-38)
  • SessionTime (41-44)
  • Timestamp (8-8)
bridges/opencode/opencodebridge/opencode_identifiers.go (1)
  • OpenCodePortalKey (58-68)
bridges/openclaw/client.go (1)
pkg/shared/openclawconv/content.go (1)
  • StringsTrimDefault (136-142)
bridges/openclaw/media_test.go (2)
bridges/openclaw/metadata.go (1)
  • PortalMetadata (25-76)
bridges/opencode/opencode/types.go (2)
  • Message (47-60)
  • Timestamp (8-8)
bridges/opencode/opencodebridge/bridge.go (2)
bridges/opencode/opencode/types.go (2)
  • Session (29-38)
  • Timestamp (8-8)
bridges/opencode/opencodebridge/opencode_identifiers.go (1)
  • OpenCodePortalKey (58-68)
bridges/openclaw/manager.go (6)
pkg/bridgeadapter/approval_prompt.go (4)
  • ApprovalPromptPresentation (46-50)
  • DecisionToString (623-631)
  • ApprovalDetail (41-44)
  • ApprovalPromptMessageParams (277-286)
pkg/shared/backfillutil/pagination.go (2)
  • Paginate (28-43)
  • PaginateParams (9-15)
pkg/shared/backfillutil/search.go (1)
  • IndexAtOrAfter (10-17)
pkg/shared/openclawconv/content.go (1)
  • StringsTrimDefault (136-142)
pkg/bridgeadapter/approval_flow.go (1)
  • SendPromptParams (435-439)
pkg/bridgeadapter/approval_decision.go (1)
  • ApprovalDecisionPayload (20-25)
bridges/opencode/opencodebridge/opencode_parts.go (1)
pkg/shared/streamui/tools.go (1)
  • ToolDisplayTitle (224-233)
pkg/shared/streamui/tools.go (2)
pkg/shared/streamui/emitter.go (1)
  • Emitter (83-86)
pkg/agents/tools/builtin.go (1)
  • GetTool (95-97)
pkg/bridgeadapter/approval_prompt.go (1)
pkg/bridgeadapter/approval_decision.go (5)
  • ApprovalDecisionPayload (20-25)
  • ApprovalReasonTimeout (13-13)
  • ApprovalReasonExpired (14-14)
  • ApprovalReasonDeliveryError (16-16)
  • ApprovalReasonCancelled (15-15)
🪛 GitHub Actions: Go
bridges/opencode/opencodebridge/opencode_parts.go

[error] 1-1: go-imports-repo: imports were reformatted by pre-commit.

bridges/codex/client.go

[error] 1-1: go-imports-repo: imports were reformatted by pre-commit.

🔇 Additional comments (29)
bridges/openclaw/client.go (4)

28-28: LGTM!

Import addition for the shared openclawconv package is appropriate for consolidating string normalization utilities.


351-351: LGTM!

Correctly uses the shared helper to extract the agent ID with an appropriate fallback to OpenClawAgentID.


663-677: LGTM!

The nested StringsTrimDefault calls correctly implement cascading fallback logic (e.g., workspace → space → team). The pattern is clear and appropriate for multi-level defaults.


738-738: LGTM!

Avatar ID construction correctly uses the metadata agent ID with a fallback to the function parameter.

pkg/connector/tool_execution.go (2)

15-15: LGTM!

The import is correctly added to support the delegation to streamui.ToolDisplayTitle.


53-54: Clean refactor to shared utility.

The delegation to streamui.ToolDisplayTitle is well-structured. The variable is correctly used elsewhere in the connector package (e.g., in streaming_output_handlers.go line 62).

bridges/opencode/opencodebridge/bridge.go (3)

3-16: LGTM!

The new imports are correctly organized and necessary for the added functionality: strings for validation, time for timestamp handling, simplevent for creating resync events, and opencode for session types.


171-179: LGTM!

The timestamp extraction logic correctly prioritizes Updated over Created and safely handles the case where neither is set by returning a zero time.Time. The int64 cast is appropriate since opencode.Timestamp is defined as int64.


181-201: LGTM!

The resync event construction is well-implemented:

  • Correctly uses OpenCodePortalKey to build the portal key with proper URL escaping.
  • LatestMessageTS matches the event timestamp, which is appropriate for resync semantics.
  • queueOpenCodeSessionResync has proper defensive checks for nil receiver, host, empty session ID, and nil login.
bridges/opencode/opencodebridge/backfill.go (5)

64-84: LGTM!

The pagination refactor using backfillutil.Paginate is a clean improvement. The closure-based anchor finding and index-at-or-after functions properly integrate with the centralized pagination logic, and the result extraction (result.Start:result.End, result.Cursor, result.HasMore) is straightforward.


104-125: LGTM!

The anchor index maps are efficiently constructed with O(1) lookup capability. The function correctly handles:

  • Message IDs mapped to entry indices
  • Part IDs mapped to entry indices
  • Nested attachment IDs from part.State.Attachments
  • Empty ID filtering to avoid spurious map entries

127-147: LGTM!

The anchor index resolution correctly handles the dual-namespace (part vs. message) lookup pattern. Returning (0, false) for unresolvable anchors allows the caller to fall back to time-based positioning via indexAtOrAfter.


196-226: LGTM!

The user message backfill path is cleanly separated from assistant messages:

  • User messages get per-part handling via buildOpenCodeUserBackfillMessages
  • Assistant messages continue to use the canonical snapshot-based approach
  • Early continue after user path prevents accidental fallthrough
  • Proper error propagation from the user backfill builder

247-290: LGTM!

The user backfill message builder correctly:

  • Iterates parts and skips empty IDs
  • Propagates MessageID/SessionID from the parent message when missing
  • Uses buildOpenCodeConvertedPart for part conversion with proper error handling (ErrIgnoringRemoteEvent → skip, other errors → propagate)
  • Creates distinct BackfillMessage per part with unique IDs via opencodePartMessageID
  • Maintains strictly increasing stream order via the nextOrder closure
bridges/opencode/opencodebridge/opencode_manager.go (7)

38-39: LGTM!

Adding Presentation to permissionApprovalRef enables structured approval prompts to be passed through the approval flow, consistent with the broader approval presentation ecosystem in the codebase.


57-61: Gate AllowAlways based on the upstream request.

The AllowAlways field is unconditionally set to true, but opencode.PermissionRequest includes an Always []string field indicating whether "always allow" is supported for this request. Setting AllowAlways: true regardless allows the UI to offer an always decision even when OpenCode didn't advertise it.

🔧 Suggested fix
 	return bridgeadapter.ApprovalPromptPresentation{
 		Title:       title,
 		Details:     details,
-		AllowAlways: true,
+		AllowAlways: len(req.Always) > 0,
 	}

100-100: LGTM!

Using bridgeadapter.DecisionToString centralizes the decision-to-string mapping, improving consistency across approval flows.


564-581: LGTM!

The session sync correctly triggers a resync only when the portal already existed (hadRoom), avoiding unnecessary resyncs for newly created session portals. The check-before-mutation, action-after-mutation pattern is sound.


698-715: LGTM!

The session event handler consistently applies the same hadRoom check pattern as syncSessions, ensuring session create/update events trigger backfill resyncs for existing portals.


852-894: LGTM!

The presentation is computed once and reused for both the approval reference storage and the prompt payload. The ExpiresAt correctly matches the 10-minute registration timeout.


938-943: LGTM!

Using ResolveExternal with a full ApprovalDecisionPayload properly reports the externally-resolved decision through the approval flow, enabling correct lifecycle management. The derivation of Approved, Always, and Reason from the reply string is correct.

bridges/opencode/opencodebridge/backfill_test.go (2)

64-95: LGTM!

The test comprehensively validates the resync event construction:

  • Correctly expects Updated timestamp to be used (since it's > 0)
  • Validates portal key construction with proper receiver
  • Confirms expected defaults for StreamOrder (0) and Sender (empty)

15-62: Verify nil intent handling in buildOpenCodeConvertedPart.

The test passes nil for the intent parameter. While the test avoids triggering the code path that requires intent (no "file" type parts), buildOpenCodeFileContent explicitly checks for nil intent and returns an error ("matrix API unavailable"). This test is brittle—if a file part were added to the test data, it would fail. Either pass a valid intent to buildOpenCodeUserBackfillMessages or add a comment documenting why nil is safe for this specific test case.

bridges/openclaw/media_test.go (1)

261-325: Good coverage for the new backfill invariants.

These assertions pin down both chronological batching and stable streamOrder behavior, which should make the new snapshot-pagination logic much safer to refactor.

pkg/connector/streaming_ui_tools.go (1)

20-53: Nice consolidation of the approval prompt path.

Keeping the ephemeral tool-approval-request emission and the approvalFlow.SendPrompt construction together reduces the chance of the stream event and the Matrix prompt drifting out of sync.

pkg/bridgeadapter/approval_decision.go (1)

8-17: LGTM.

Centralizing the canonical approval reasons here should help the new prompt/edit flow stay consistent across bridges.

pkg/shared/streamui/tools.go (1)

130-159: LGTM.

Adding a dedicated approval-response chunk and preferring annotated tool titles should make the live tool UI read a lot better.

Also applies to: 222-233

bridges/openclaw/manager.go (1)

555-646: Deterministic backfill ordering looks good.

Sorting by (timestamp, messageID) and then forcing strictly increasing StreamOrder should keep pagination and dedup stable even when multiple history items share a timestamp.

pkg/bridgeadapter/approval_flow.go (1)

775-884: The finalize/edit path is a nice cleanup.

Closing done, dropping prompt bindings, and then editing/redacting asynchronously makes the approval lifecycle much easier to follow than the previous scattered cleanup.

Copy link

@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: 8

♻️ Duplicate comments (1)
docs/matrix-ai-matrix-spec-v1.md (1)

310-310: ⚠️ Potential issue | 🟡 Minor

Use m.reaction terminology here.

m.annotation is the relation type on the reaction, not a separate event type. This should describe bridge-authored placeholder m.reaction events that use m.annotation.

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

In `@docs/matrix-ai-matrix-spec-v1.md` at line 310, Update the sentence to use the
correct Matrix terminology: replace "bridge-authored placeholder `m.reaction` /
`m.annotation` events" with "bridge-authored placeholder `m.reaction` events
that use `m.annotation` as the relation type", ensuring the text refers to
`m.reaction` as the event type and `m.annotation` as the relation key (e.g., in
the clause containing "The bridge MUST send bridge-authored placeholder ...").
🧹 Nitpick comments (1)
bridges/opencode/host.go (1)

295-322: Static analysis: oc == nil check is unreachable after mutex access.

The check oc == nil on line 310 comes after oc.StreamMu.Lock() on line 301. If oc were nil, the code would have panicked before reaching line 310. The nil check can be simplified to only check the fields:

♻️ Suggested simplification
 func (oc *OpenCodeClient) resolveStreamTargetEventID(
 	ctx context.Context,
 	portal *bridgev2.Portal,
 	turnID string,
 	target streamtransport.StreamTarget,
 ) (id.EventID, error) {
 	oc.StreamMu.Lock()
 	state := oc.streamStates[turnID]
 	if state != nil && state.initialEventID != "" {
 		eventID := state.initialEventID
 		oc.StreamMu.Unlock()
 		return eventID, nil
 	}
 	oc.StreamMu.Unlock()

-	if oc == nil || oc.UserLogin == nil || oc.UserLogin.Bridge == nil || portal == nil {
+	if oc.UserLogin == nil || oc.UserLogin.Bridge == nil || portal == nil {
 		return "", nil
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/opencode/host.go` around lines 295 - 322, The oc == nil check is
unreachable because StreamMu is accessed earlier; before locking (StreamMu.Lock)
ensure you validate oc, oc.UserLogin, oc.UserLogin.Bridge and portal are non-nil
and return early if any are nil, then proceed to lock and access
oc.streamStates; alternatively remove the redundant oc == nil check and only
check the fields after the lock—update the resolveStreamTargetEventID function
to perform the nil/field checks (oc, oc.UserLogin, oc.UserLogin.Bridge, portal)
prior to calling oc.StreamMu.Lock or drop the oc == nil clause and keep only
field checks while retaining correct locking around oc.streamStates and setting
state.initialEventID.
🤖 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/codex/streaming_support.go`:
- Around line 66-68: The helper function hasEphemeralTarget is unused and
causing a staticcheck U1000 failure; remove the unused function definition
(hasEphemeralTarget on type streamingState) from
bridges/codex/streaming_support.go, or if it was intended to be used, wire it up
by replacing ad-hoc checks of s != nil && s.initialEventID != "" with calls to
s.hasEphemeralTarget() in the relevant methods (ensure method name
streamingState.hasEphemeralTarget is referenced wherever that logic is needed)
so the symbol is actually used and the linter stops failing.

In `@bridges/openclaw/stream.go`:
- Around line 396-399: The DB lookup is using portal.Receiver directly but
persistStreamDBMetadata falls back to oc.UserLogin.ID when receiver is empty;
update the call to streamtransport.ResolveTargetEventIDFromDB to pass the same
fallback receiver (use oc.UserLogin.ID when portal.Receiver is blank) so it
queries with the correct receiver; locate the call to ResolveTargetEventIDFromDB
in stream.go (the block that checks oc/UserLogin/Bridge/portal) and mirror the
fallback logic used in persistStreamDBMetadata.
- Around line 381-397: In resolveStreamTargetEventID, move the nil checks for
oc, oc.UserLogin, oc.UserLogin.Bridge and portal to the very start of the
function before accessing oc.StreamMu; then only acquire oc.StreamMu and read
oc.streamStates[turnID] after those checks, keeping the existing behavior
(return "", nil on those nil cases) and ensuring StreamMu is unlocked on all
return paths (use early return before locking or defer Unlock after Lock).
Reference: function resolveStreamTargetEventID, field StreamMu and map
streamStates, and the UserLogin.Bridge check.

In `@docs/matrix-ai-matrix-spec-v1.md`:
- Line 347: The spec references a chunk type `tool-approval-response` without
defining it; update the chunk inventory or the text to be consistent by either
adding a formal definition for `tool-approval-response` (including its fields
and relationship to `tool-approval-request` and the canonical `approval notice`)
or by renaming the reference to the actual emitted chunk/event already defined
in the chunk set; ensure the change appears where the chunk inventory is
declared and update the sentence mentioning mirrored `tool-approval-request` /
`tool-approval-response` so both names match defined chunk types.
- Around line 213-220: The spec currently allows producers to emit
debounced/final timeline edits while the Matrix event ID is unavailable, but
also mandates those edits be sent as m.replace (which requires
m.relates_to.event_id), making the fallback path impossible; update the
lifecycle text so that producers MUST buffer until the placeholder's Matrix
event ID is resolved before emitting com.beeper.ai.stream_event chunks or the
final m.replace, or alternatively explicitly define a different fallback
mechanism that does not use m.replace (e.g., a separate event type) and document
it alongside com.beeper.ai.stream_event and m.relates_to.event_id; ensure
references to m.replace, m.relates_to.event_id, com.beeper.ai.stream_event and
the placeholder seed com.beeper.ai are used to locate and update the problematic
lines.

In `@pkg/connector/stream_events.go`:
- Around line 88-95: Normalize the portal receiver before calling
ResolveTargetEventIDFromDB: if portal.Receiver is empty or blank, use
oc.UserLogin.ID as the receiver value passed to
streamtransport.ResolveTargetEventIDFromDB so receiver-less portals resolve
correctly; keep the existing nil checks for oc, oc.UserLogin,
oc.UserLogin.Bridge and portal, call ResolveTargetEventIDFromDB with the
normalized receiver, and preserve the existing logic that sets
state.initialEventID when err==nil and eventID!=""

In `@pkg/connector/streaming_error_handling.go`:
- Around line 24-28: The check in streamFailureError currently only uses
state.hasEditTarget() so cases where an ephemeral edit target has initialEventID
set (but networkMessageID is still nil) are misclassified as PreDeltaError;
update streamFailureError to treat any state with hasEditTarget() OR a non-empty
state.initialEventID OR a non-nil state.networkMessageID as "output already
sent" and return &NonFallbackError{Err: err} in those cases (otherwise return
&PreDeltaError{Err: err}), referencing the streamingState fields initialEventID
and networkMessageID and the hasEditTarget(), NonFallbackError and PreDeltaError
symbols.

In `@pkg/shared/streamtransport/session.go`:
- Around line 274-300: resolveTargetEventID currently caches a single
session-wide resolvedTargetID (s.resolvedTargetID / s.targetResolutionOK) which
is incorrect because the cache must be keyed by the StreamTarget; change the
cache to map[StreamTarget]someID (e.g., resolvedTargetIDs
map[StreamTarget]EventID or map[string]EventID) on StreamSession, update
resolveTargetEventID to look up and set the entry under s.targetMu (check map
before calling s.params.ResolveTargetEventID, and store the resolved ID into the
map when successful), keep locking consistent and return the appropriate
resolved string or error as before to avoid cross-target stale resolutions.

---

Duplicate comments:
In `@docs/matrix-ai-matrix-spec-v1.md`:
- Line 310: Update the sentence to use the correct Matrix terminology: replace
"bridge-authored placeholder `m.reaction` / `m.annotation` events" with
"bridge-authored placeholder `m.reaction` events that use `m.annotation` as the
relation type", ensuring the text refers to `m.reaction` as the event type and
`m.annotation` as the relation key (e.g., in the clause containing "The bridge
MUST send bridge-authored placeholder ...").

---

Nitpick comments:
In `@bridges/opencode/host.go`:
- Around line 295-322: The oc == nil check is unreachable because StreamMu is
accessed earlier; before locking (StreamMu.Lock) ensure you validate oc,
oc.UserLogin, oc.UserLogin.Bridge and portal are non-nil and return early if any
are nil, then proceed to lock and access oc.streamStates; alternatively remove
the redundant oc == nil check and only check the fields after the lock—update
the resolveStreamTargetEventID function to perform the nil/field checks (oc,
oc.UserLogin, oc.UserLogin.Bridge, portal) prior to calling oc.StreamMu.Lock or
drop the oc == nil clause and keep only field checks while retaining correct
locking around oc.streamStates and setting state.initialEventID.
🪄 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: 4097e3f5-040f-4b84-aabe-237a6b474d00

📥 Commits

Reviewing files that changed from the base of the PR and between 613074b and 437e0dc.

📒 Files selected for processing (34)
  • bridges/codex/approvals_test.go
  • bridges/codex/backfill.go
  • bridges/codex/backfill_test.go
  • bridges/codex/client.go
  • bridges/codex/portal_send.go
  • bridges/codex/stream_mapping_test.go
  • bridges/codex/stream_transport.go
  • bridges/codex/streaming_support.go
  • bridges/codex/streaming_test.go
  • bridges/openclaw/client.go
  • bridges/openclaw/events.go
  • bridges/openclaw/manager.go
  • bridges/openclaw/manager_test.go
  • bridges/openclaw/stream.go
  • bridges/openclaw/stream_test.go
  • bridges/opencode/client.go
  • bridges/opencode/host.go
  • bridges/opencode/opencodebridge/bridge.go
  • bridges/opencode/stream_canonical.go
  • bridges/opencode/stream_canonical_test.go
  • docs/matrix-ai-matrix-spec-v1.md
  • pkg/bridgeadapter/helpers.go
  • pkg/bridgeadapter/remote_events.go
  • pkg/bridgeadapter/remote_events_test.go
  • pkg/connector/remote_message.go
  • pkg/connector/stream_events.go
  • pkg/connector/streaming_error_handling.go
  • pkg/connector/streaming_error_handling_test.go
  • pkg/connector/streaming_state.go
  • pkg/matrixevents/matrixevents.go
  • pkg/matrixevents/matrixevents_test.go
  • pkg/shared/streamtransport/session.go
  • pkg/shared/streamtransport/session_target_test.go
  • pkg/shared/streamtransport/target.go
💤 Files with no reviewable changes (1)
  • bridges/openclaw/stream_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • bridges/openclaw/client.go
  • bridges/opencode/opencodebridge/bridge.go
  • bridges/codex/backfill.go
  • bridges/openclaw/manager_test.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). (2)
  • GitHub Check: build-docker
  • GitHub Check: build-docker
🧰 Additional context used
🧬 Code graph analysis (16)
pkg/bridgeadapter/remote_events_test.go (2)
pkg/bridgeadapter/remote_events.go (2)
  • RemoteMessage (30-41)
  • RemoteEdit (92-103)
bridges/opencode/opencode/types.go (1)
  • Timestamp (8-8)
bridges/opencode/client.go (1)
pkg/shared/streamui/emitter.go (1)
  • UIState (14-36)
pkg/shared/streamtransport/session_target_test.go (2)
pkg/shared/streamtransport/session.go (2)
  • StreamSessionParams (43-59)
  • EndReasonFinish (38-38)
pkg/shared/streamtransport/target.go (1)
  • StreamTarget (15-18)
pkg/matrixevents/matrixevents.go (1)
pkg/connector/events.go (1)
  • RelReference (89-89)
bridges/opencode/stream_canonical.go (2)
bridges/opencode/remote_events.go (1)
  • OpenCodeRemoteEdit (11-11)
bridges/opencode/opencode/types.go (1)
  • Timestamp (8-8)
bridges/codex/approvals_test.go (3)
bridges/codex/metadata.go (1)
  • PortalMetadata (29-37)
pkg/bridgeadapter/approval_decision.go (1)
  • ApprovalDecisionPayload (20-25)
pkg/bridgeadapter/approval_prompt.go (1)
  • ApprovalPromptPresentation (46-50)
bridges/codex/stream_transport.go (1)
pkg/shared/streamtransport/target.go (2)
  • StreamTarget (15-18)
  • ResolveTargetEventIDFromDB (26-48)
bridges/codex/backfill_test.go (1)
bridges/opencode/opencode/types.go (1)
  • Timestamp (8-8)
pkg/bridgeadapter/helpers.go (1)
bridges/opencode/opencode/types.go (1)
  • Timestamp (8-8)
pkg/shared/streamtransport/session.go (2)
pkg/shared/streamtransport/target.go (2)
  • StreamTarget (15-18)
  • TargetEventResolver (24-24)
pkg/matrixevents/matrixevents.go (2)
  • BuildStreamEventEnvelope (80-111)
  • StreamEventOpts (74-77)
pkg/matrixevents/matrixevents_test.go (1)
pkg/matrixevents/matrixevents.go (2)
  • BuildStreamEventEnvelope (80-111)
  • StreamEventOpts (74-77)
pkg/connector/streaming_state.go (1)
pkg/shared/streamtransport/target.go (1)
  • StreamTarget (15-18)
bridges/codex/portal_send.go (2)
pkg/bridgeadapter/helpers.go (2)
  • SendViaPortal (171-198)
  • SendViaPortalParams (156-167)
bridges/opencode/opencode/types.go (1)
  • Timestamp (8-8)
bridges/openclaw/manager.go (7)
pkg/bridgeadapter/approval_prompt.go (4)
  • ApprovalPromptPresentation (46-50)
  • DecisionToString (623-631)
  • ApprovalDetail (41-44)
  • ApprovalPromptMessageParams (277-286)
bridges/openclaw/metadata.go (1)
  • PortalMetadata (25-76)
pkg/shared/backfillutil/pagination.go (2)
  • Paginate (28-43)
  • PaginateParams (9-15)
pkg/shared/backfillutil/search.go (1)
  • IndexAtOrAfter (10-17)
pkg/shared/openclawconv/content.go (1)
  • StringsTrimDefault (136-142)
pkg/bridgeadapter/approval_flow.go (1)
  • SendPromptParams (435-439)
pkg/bridgeadapter/approval_decision.go (1)
  • ApprovalDecisionPayload (20-25)
bridges/openclaw/stream.go (2)
pkg/shared/openclawconv/content.go (1)
  • StringsTrimDefault (136-142)
pkg/shared/streamtransport/target.go (2)
  • StreamTarget (15-18)
  • ResolveTargetEventIDFromDB (26-48)
bridges/codex/client.go (7)
pkg/bridgeadapter/helpers.go (2)
  • BuildChatInfoWithFallback (221-234)
  • ComputeApprovalExpiry (362-367)
pkg/shared/streamui/tools.go (1)
  • ToolDisplayTitle (224-233)
pkg/bridgeadapter/approval_prompt.go (4)
  • ApprovalPromptPresentation (46-50)
  • ApprovalPromptMessageParams (277-286)
  • ApprovalDetail (41-44)
  • AddOptionalDetail (613-619)
pkg/bridgeadapter/approval_flow.go (1)
  • SendPromptParams (435-439)
pkg/bridgeadapter/approval_decision.go (3)
  • ApprovalDecisionPayload (20-25)
  • ApprovalReasonTimeout (13-13)
  • ApprovalReasonCancelled (15-15)
pkg/shared/streamui/recorder.go (1)
  • RecordApprovalResponse (218-244)
pkg/shared/stringutil/elevated_level.go (1)
  • NormalizeElevatedLevel (8-21)
🪛 GitHub Actions: Go
bridges/codex/streaming_support.go

[error] 66-66: go-staticcheck: hasEphemeralTarget is unused. (U1000)

bridges/openclaw/events.go

[error] 1-1: pre-commit go-imports-repo hook failed: files were modified by this hook. Re-run pre-commit to apply fixes.

🪛 GitHub Check: Lint
bridges/codex/streaming_support.go

[failure] 66-66:
func (*streamingState).hasEphemeralTarget is unused (U1000)

bridges/opencode/host.go

[failure] 310-310:
this check suggests that the pointer can be nil


[failure] 308-308:
possible nil pointer dereference (SA5011)

bridges/openclaw/stream.go

[failure] 396-396:
this check suggests that the pointer can be nil


[failure] 394-394:
possible nil pointer dereference (SA5011)

🔇 Additional comments (43)
pkg/matrixevents/matrixevents.go (2)

74-77: LGTM!

The rename from TargetEventID to RelatesToEventID improves clarity by directly reflecting the Matrix protocol's m.relates_to semantics.


98-105: LGTM!

Good validation pattern: requiring a non-empty RelatesToEventID ensures stream events always have a proper parent reference. The m.relates_to structure correctly uses RelReference as the relation type.

pkg/matrixevents/matrixevents_test.go (2)

19-24: LGTM!

Good addition of a dedicated test case for the new RelatesToEventID requirement. This ensures the validation logic is properly exercised.


26-52: LGTM!

Comprehensive test coverage: validates the renamed field, envelope structure, and the assertion at lines 50-52 properly ensures the old target_event field is no longer emitted (good regression prevention for the migration).

bridges/openclaw/events.go (6)

12-13: Import formatting still causes pipeline failure.

The pipeline failure confirms this import grouping issue persists. The blank line before openclawconv creates incorrect grouping per Go import conventions.


81-84: LGTM!

Good consolidation of trimming logic using the shared openclawconv.StringsTrimDefault utility. The pattern correctly provides a fallback from the session key when the existing value is empty/whitespace.


105-105: LGTM!

Consistent use of StringsTrimDefault for preview snippet with appropriate fallback.


124-124: LGTM!

Consistent application of StringsTrimDefault with a sensible "gateway" fallback.


164-199: LGTM!

Good addition of streamOrder field with appropriate fallback logic. Using zero as the "unset" sentinel is idiomatic for ordering values, and falling back to UnixMilli() preserves backward compatibility. The interface assertion at line 176 ensures compile-time verification.


204-239: LGTM!

Consistent implementation of streamOrder for edit events, mirroring the OpenClawRemoteMessage pattern. The parallel structure ensures uniform ordering semantics across both message and edit event types.

pkg/bridgeadapter/helpers.go (2)

163-166: New fields for explicit timestamp and ordering look good.

The addition of Timestamp and StreamOrder fields provides callers control over event ordering, which aligns with the PR's goal of supporting stream ordering through the portal send flow.


182-188: No action required — zero-value timestamps are already handled.

The RemoteMessage.GetTimestamp() method already provides a fallback to time.Now() for zero-value timestamps (line 63-67 in pkg/bridgeadapter/remote_events.go). This applies to all three event types (RemoteMessage, RemoteEdit, RemoteReaction), and is part of the bridgev2.RemoteEventWithTimestamp interface contract.

Callers that omit the Timestamp parameter will not cause unexpected behavior—GetTimestamp() guarantees a valid timestamp during event processing. The centralized fallback in the message object is a better design than adding fallbacks at every call site.

			> Likely an incorrect or invalid review comment.
bridges/openclaw/stream.go (1)

91-91: Nice normalization cleanup.

Switching these call sites to openclawconv.StringsTrimDefault makes the defaulting behavior consistent across stream metadata and UI payloads.

Also applies to: 126-126, 485-507

bridges/openclaw/manager.go (3)

591-633: Nice deterministic backfill ordering.

Sorting first and then assigning monotonic streamOrder values should make backfill pagination much more stable across refetches.


1155-1160: Only explicit allow decisions should resolve as approved.

This path still treats any decision other than deny as an approval, so tokens like timeout, expired, cancelled, or delivery_error will be emitted as allowed here. Limit Approved=true to allow-once / allow-always, keep Always tied to allow-always, and propagate every other decision string as the non-approval reason.


1344-1347: Don't bypass the fallback window when runID is empty.

Returning true here makes the newest user message in the 8-message history slice eligible for mirroring even when it is stale or unrelated, because the timestamp guard below never runs.

pkg/bridgeadapter/remote_events_test.go (1)

8-26: Good regression coverage for explicit stream-order overrides.

These tests pin the new “explicit StreamOrder wins” behavior for both message and edit paths, which is the key contract change here.

bridges/opencode/stream_canonical.go (1)

114-143: Nice consolidation of stream timing and ordering rules.

Keeping timestamp selection and monotonic order generation in helpers should make the final-edit path much easier to keep consistent.

bridges/opencode/stream_canonical_test.go (1)

39-60: Nice coverage for the helper contracts.

These assertions lock in both the started/completed timestamp preference and the monotonic ordering guarantee.

bridges/codex/portal_send.go (2)

16-43: LGTM! Clean delegation pattern for ordering parameters.

The refactoring to introduce sendViaPortalWithOrdering maintains backward compatibility by having sendViaPortal delegate with default zero values while enabling explicit timestamp and stream order for callers that need deterministic ordering.


69-76: LGTM! Consistent pattern for human sender identity.

The senderForHuman helper correctly mirrors senderForPortal with appropriate IsFromMe: true and derives the sender from the human user ID.

bridges/codex/stream_transport.go (2)

40-45: LGTM! Improved target resolution with async/context-aware callbacks.

The replacement of GetTargetEventID with GetStreamTarget and ResolveTargetEventID enables proper context propagation and DB-backed resolution while preserving the caching pattern.


96-113: LGTM! Resolver correctly caches resolved event IDs.

The resolveStreamTargetEventID method properly:

  1. Returns cached initialEventID early if available
  2. Guards against nil receivers before DB access
  3. Caches successful resolutions back to state
bridges/opencode/host.go (2)

155-174: LGTM! Correct timestamp and stream order propagation for placeholder messages.

The placeholder creation now properly uses openCodeStreamEventTimestamp and openCodeNextStreamOrder for consistent ordering, and correctly caches both networkMessageID and initialEventID after a successful send.


196-207: LGTM! Stream target callbacks align with Codex pattern.

The GetStreamTarget and ResolveTargetEventID callbacks follow the same pattern introduced in bridges/codex/stream_transport.go, ensuring consistent behavior across bridges.

bridges/codex/approvals_test.go (4)

96-101: Fixed sleeps in approval tests remain potentially flaky.

The time.Sleep(50 * time.Millisecond) pattern before checking cc.approvalFlow.Get("123") was flagged in a previous review. Under slow CI or GC pauses, the handler may not have registered the approval yet. Consider polling with a timeout instead.


44-60: Good addition: Mutex protection for test event collection.

The mutex around gotParts and gotPartTypes prevents data races when the streamEventHook is called from goroutines.


153-244: LGTM! Comprehensive test for denial flow.

TestCodex_CommandApproval_DenyEmitsResponseThenOutputDenied properly validates:

  1. Denial returns decision=decline
  2. Both tool-approval-response and tool-output-denied parts are emitted
  3. The parts appear in correct order (response before denied)

289-292: LGTM! Updated test to use new presentation parameter.

The registerToolApproval call now includes the ApprovalPromptPresentation struct, aligning with the updated signature.

bridges/codex/backfill_test.go (5)

13-38: LGTM! Good test for turn text pair extraction.

The test correctly validates:

  • User text concatenation (text types only, mentions ignored)
  • Agent message deduplication by ID (same ID → latest text wins)
  • Multiple agent messages with different IDs are concatenated

40-83: LGTM! Comprehensive backfill entry validation.

The test ensures entries are:

  1. Generated in correct chronological order
  2. Have strictly increasing stream order
  3. Have unique, non-empty message IDs

85-115: LGTM! Pagination test validates backward batch semantics.

Good coverage of:

  • First batch returns most recent entries
  • Cursor enables continuation
  • Final batch correctly reports hasMore=false

117-213: LGTM! Rollout timing tests cover both happy path and fallback.

The tests validate:

  • Rollout file parsing extracts correct user/assistant timestamps per turn
  • When no rollout timings exist, synthetic timestamps are generated from thread creation time

215-245: LGTM! Clean test helpers.

writeCodexRolloutTestFile and codexRolloutTestEvent provide reusable scaffolding for rollout-based tests.

bridges/codex/client.go (9)

36-36: LGTM! BackfillingNetworkAPI assertion added.

This enables Codex portals with thread IDs to support backfill via the FetchMessages method.


250-260: LGTM! Conditional upstream logout for managed auth only.

The check !isHostAuthLogin(meta) correctly ensures that only per-login managed auth triggers upstream account/logout, while host-auth logins skip this step but still perform local cleanup.


362-373: LGTM! canBackfill derived from CodexThreadID presence.

The logic correctly enables backfill only when a Codex thread is associated with the portal.


1539-1553: LGTM! composeCodexChatInfo now accepts canBackfill parameter.

The updated signature properly propagates backfill capability to BuildDMChatInfo.


2102-2118: LGTM! waitToolApproval now distinguishes timeout vs cancelled.

The method correctly checks ctx.Err() to determine if the failure was due to cancellation vs timeout, and passes the appropriate reason to FinishResolved. This addresses the previously flagged issue.


2150-2154: Duplicate requestApproval handling still not addressed.

registerToolApproval returns a boolean indicating whether the approval was newly created, but this return value is ignored. If Codex retries the same JSON-RPC request ID, both handlers will emit prompts and wait on the same channel—one gets the decision, the other times out.

Consider short-circuiting when the approval already exists:

🔧 Suggested fix
-	cc.registerToolApproval(active.portal.MXID, approvalID, toolCallID, toolName, presentation, approvalTTL)
+	_, isNew := cc.registerToolApproval(active.portal.MXID, approvalID, toolCallID, toolName, presentation, approvalTTL)
+	if !isNew {
+		// Duplicate request—wait for existing approval without re-emitting UI prompt
+		decision, ok := cc.waitToolApproval(ctx, approvalID)
+		if !ok {
+			return emitOutcome(false, bridgeadapter.ApprovalReasonTimeout)
+		}
+		return emitOutcome(decision.Approved, decision.Reason)
+	}

 	cc.emitUIToolApprovalRequest(ctx, active.portal, active.state, approvalID, toolCallID, toolName, presentation, ttlSeconds)

1876-1878: LGTM! Initial stream message uses proper timestamp and ordering.

The sendInitialStreamMessage now uses codexStreamEventTimestamp and codexNextLiveStreamOrder for consistent event ordering.


1920-1941: LGTM! emitUIToolApprovalRequest refactored with structured presentation.

The method now properly integrates ApprovalPromptPresentation and delegates to approvalFlow.SendPrompt with complete parameters including ReplyToEventID and ExpiresAt.


2184-2222: LGTM! Approval handlers use new extractInput signature with presentation.

Both handleCommandApprovalRequest and handleFileChangeApprovalRequest cleanly build input maps and structured presentations using AddOptionalDetail, with AllowAlways: false for Codex approvals.

Copy link

@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: 3

Caution

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

⚠️ Outside diff range comments (8)
bridges/ai/image_generation_tool.go (3)

1187-1189: ⚠️ Potential issue | 🟡 Minor

Readability check doesn't verify actual read permission for current process.

The check info.Mode().Perm()&0o444 == 0 only verifies that some read bit is set, not that the current process can read the file. A file with 0o400 (owner-only) passes this check even if the process runs as a different user.

Consider removing this check and relying on the actual read operation to fail with a proper permission error, or use os.Open for a definitive check.

♻️ Alternative: attempt to open the file
-	if info.Mode().Perm()&0o444 == 0 {
-		return "", errors.New("local image path is not readable")
-	}
+	f, err := os.Open(resolved)
+	if err != nil {
+		return "", fmt.Errorf("local image path is not readable: %w", err)
+	}
+	f.Close()
🤖 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 1187 - 1189, The permission
check using info.Mode().Perm()&0o444 in the code path that handles local image
paths is unsafe because it only inspects permission bits and not whether the
current process can actually read the file; replace that check by attempting to
open the file (e.g., call os.Open) and handle any error from the open/read
operation, or simply remove the Perm() check and let the subsequent file read
fail and propagate its error; locate the check around info.Mode().Perm()&0o444
in the image path validation logic in image_generation_tool.go (the block that
currently returns errors.New("local image path is not readable")) and change it
to perform an actual open/read to verify readability and return the real error
when it fails.

1246-1255: ⚠️ Potential issue | 🟠 Major

Permitting the entire home directory may expose sensitive files.

Including the full home directory allows access to potentially sensitive locations like ~/.ssh/, ~/.aws/, ~/.gnupg/, or credential files. Consider restricting to specific subdirectories (e.g., ~/Pictures, ~/Downloads) or requiring explicit configuration.

🛡️ Suggested more restrictive approach
 func permittedImageInputDirs() []string {
 	var dirs []string
 	if tempDir := os.TempDir(); strings.TrimSpace(tempDir) != "" {
 		dirs = append(dirs, tempDir)
 	}
-	if homeDir, err := os.UserHomeDir(); err == nil && strings.TrimSpace(homeDir) != "" {
-		dirs = append(dirs, homeDir)
+	if homeDir, err := os.UserHomeDir(); err == nil && homeDir != "" {
+		// Only allow common user content directories
+		for _, subdir := range []string{"Pictures", "Downloads", "Documents", "Desktop"} {
+			dirs = append(dirs, filepath.Join(homeDir, subdir))
+		}
 	}
 	return dirs
 }
🤖 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 1246 - 1255, The function
permittedImageInputDirs currently adds the entire user home directory which can
expose sensitive files; update permittedImageInputDirs to only include
os.TempDir plus a whitelist of safe subdirectories under os.UserHomeDir such as
"Pictures" and "Downloads" (check existence and trim spaces before appending),
or read an explicit configurable list from an environment variable/config so
only allowed subpaths are permitted; ensure you reference and update the logic
around os.UserHomeDir, os.TempDir, and the permittedImageInputDirs function to
implement the safer whitelist/configurable approach.

1067-1136: ⚠️ Potential issue | 🟠 Major

SSRF protection has DNS rebinding vulnerability and missing IPv6 ranges.

Two issues with the current implementation:

  1. DNS Rebinding: The validation resolves the hostname at check time, but the HTTP client re-resolves it later. An attacker controlling DNS could return a safe IP during validation and an internal IP (e.g., 169.254.169.254) during the actual fetch.

  2. Missing IPv6 private ranges: The blocklist lacks IPv6 unique-local (fc00::/7) and link-local (fe80::/10) addresses.

🛡️ Suggested fix for IPv6 ranges
 var imageFetchBlockedCIDRs = []*net.IPNet{
 	mustParseCIDR("127.0.0.0/8"),
 	mustParseCIDR("10.0.0.0/8"),
 	mustParseCIDR("172.16.0.0/12"),
 	mustParseCIDR("192.168.0.0/16"),
 	mustParseCIDR("169.254.0.0/16"),
 	mustParseCIDR("::1/128"),
+	mustParseCIDR("fc00::/7"),  // IPv6 unique local
+	mustParseCIDR("fe80::/10"), // IPv6 link-local
 }

For DNS rebinding, consider using a custom http.Transport with a DialContext that validates the resolved IP before connecting, or pin the resolved IP for the request.

🤖 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 1067 - 1136, The current
SSRF checks in validateExternalImageURL and isDisallowedImageIP are vulnerable
to DNS rebinding and miss IPv6 private/link-local ranges; add fc00::/7 and
fe80::/10 to imageFetchBlockedCIDRs and ensure fetch-time DNS can't rebind by
pinning/validating the resolved IP in the HTTP transport. Change the fetch logic
(create a custom http.Transport with a DialContext) to first resolve the
hostname with net.DefaultResolver.LookupIPAddr, pick/lock the chosen IP(s), call
isDisallowedImageIP on each resolved IP (using imageFetchBlockedCIDRs and
imageFetchMetadataIP), and then dial the selected IP(s) directly (using
net.JoinHostPort) instead of letting the default resolver run later; keep
validateExternalImageURL to do early checks but enforce the final check in the
Transport/DialContext to prevent rebinding (referencing
validateExternalImageURL, isDisallowedImageIP, imageFetchBlockedCIDRs,
imageFetchMetadataIP, and the custom DialContext/http.Transport).
bridges/ai/heartbeat_delivery.go (1)

25-33: ⚠️ Potential issue | 🟠 Major

Don't stop the fallback chain on an unresolvable configured room.

These branches return resolveHeartbeatDeliveryRoom(...) directly. If heartbeat.to or heartbeat.target contains a stale room ID, the helper returns Reason: "no-target" and heartbeat delivery stops here instead of falling through to lastActivePortal / defaultChatPortal, unlike the later entry.LastTo path.

Suggested fix
 	if heartbeat != nil && heartbeat.To != nil && strings.TrimSpace(*heartbeat.To) != "" {
-		return oc.resolveHeartbeatDeliveryRoom(strings.TrimSpace(*heartbeat.To))
+		target := oc.resolveHeartbeatDeliveryRoom(strings.TrimSpace(*heartbeat.To))
+		if target.Reason != "no-target" {
+			return target
+		}
 	}

 	if heartbeat != nil && heartbeat.Target != nil {
 		trimmed := strings.TrimSpace(*heartbeat.Target)
 		if trimmed != "" && !strings.EqualFold(trimmed, "last") {
-			return oc.resolveHeartbeatDeliveryRoom(trimmed)
+			target := oc.resolveHeartbeatDeliveryRoom(trimmed)
+			if target.Reason != "no-target" {
+				return target
+			}
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/heartbeat_delivery.go` around lines 25 - 33, The current branches
return the result of resolveHeartbeatDeliveryRoom(...) immediately, which stops
the fallback chain if the configured room is unresolvable; instead call
resolveHeartbeatDeliveryRoom for heartbeat.To and heartbeat.Target but only
return its result if the resolution reason is not "no-target" (i.e., a valid
room was found); if the reason is "no-target" fall through to the existing
fallback logic that uses entry.LastTo, lastActivePortal and defaultChatPortal.
Update the branches that reference heartbeat.To and heartbeat.Target to check
the resolveHeartbeatDeliveryRoom return value's Reason before returning so stale
configured room IDs don't short-circuit the fallback chain.
bridges/ai/account_hints.go (1)

151-160: ⚠️ Potential issue | 🟠 Major

Avoid sending raw contact PII in the account-hint prompt.

These fields end up in model-visible prompt text, so every request now exposes full phone numbers, email addresses, and stable user IDs for all connected accounts. For disambiguation, keep the display to name/username/network and mask anything else.

🔐 Minimal fix
 func buildDesktopAccountDisplay(account beeperdesktopapi.Account) string {
 	return buildDesktopAccountDisplayFromView(desktopAccountView{
 		accountID:   account.AccountID,
 		network:     account.Network,
-		userID:      account.User.ID,
+		userID:      "",
 		fullName:    account.User.FullName,
 		username:    account.User.Username,
-		phoneNumber: account.User.PhoneNumber,
-		email:       account.User.Email,
+		phoneNumber: "",
+		email:       "",
 	})
 }
🤖 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 151 - 160, The
buildDesktopAccountDisplay function currently populates desktopAccountView with
sensitive PII (phoneNumber, email, userID) which will be included in
model-visible prompts; change buildDesktopAccountDisplay to only set non-PII
fields (fullName, username, network) and remove or replace phoneNumber, email,
and userID when constructing desktopAccountView (e.g., omit them or set masked
placeholders) so prompts do not contain raw contact info; update any consumers
of desktopAccountView if they rely on those fields to tolerate missing/masked
values.
bridges/ai/provider_openai_chat.go (1)

47-52: ⚠️ Potential issue | 🟡 Minor

Add proper optional field validation for ReasoningTokens.

CompletionTokensDetails is an embedded struct (not a pointer) in openai-go/v3, but ReasoningTokens is an optional field that may not be present in all API responses (e.g., non-reasoning models). The code should validate field presence using the .JSON.Valid() pattern before reading the value:

var reasoningTokens int
if resp.Usage.JSON.CompletionTokensDetails.Valid() {
    if resp.Usage.CompletionTokensDetails.JSON.ReasoningTokens.Valid() {
        reasoningTokens = int(resp.Usage.CompletionTokensDetails.ReasoningTokens)
    }
}

Currently, when the field is not present, it silently defaults to 0, which may not accurately reflect the API response.

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

In `@bridges/ai/provider_openai_chat.go` around lines 47 - 52, Update the optional
field handling for ReasoningTokens in the Usage population (in
bridges/ai/provider_openai_chat.go) so you check JSON validity before reading
nested optional fields: when constructing the UsageInfo from resp.Usage, test
resp.Usage.JSON.CompletionTokensDetails.Valid() and then
resp.Usage.CompletionTokensDetails.JSON.ReasoningTokens.Valid() before assigning
ReasoningTokens to avoid assuming a 0 value for absent fields; ensure you only
set UsageInfo.ReasoningTokens when both checks pass and otherwise leave it
unset/zero to accurately reflect the API response.
bridges/ai/handlematrix.go (2)

588-605: ⚠️ Potential issue | 🟠 Major

Don't default generic m.file uploads to the text-file path.

Line 600 routes every application/octet-stream or MIME-less file into handleTextFileMessage. That will misclassify PDFs and other binary uploads whose client omitted a MIME type, so they fail with the UTF-8 text-file error instead of taking the PDF or unsupported-file path. Check msg.Content.FileName or sniff the payload before assuming an unknown file is text.

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

In `@bridges/ai/handlematrix.go` around lines 588 - 605, The current switch branch
that treats mimeType == "" or "application/octet-stream" as text is incorrect;
update the branch in the MsgFile handling (the switch that currently calls
oc.handleTextFileMessage for empty/ octet-stream mime types) to first inspect
msg.Content.FileName (check file extension like .pdf) and/or sniff the media
payload to detect if it is PDF or plain text; if the filename or sniffing
indicates a PDF, set config = pdfConfig, isPDF = true, ok = true and proceed
with the PDF path, if it indicates text call oc.handleTextFileMessage, otherwise
return the unsupported/unsupported-file error (or let the unsupported-file path
run) instead of defaulting to text; keep references to mimeType,
msg.Content.FileName, oc.canUseMediaUnderstanding, and oc.handleTextFileMessage
when making the change.

730-737: ⚠️ Potential issue | 🟠 Major

This short-circuits the audio fallback flow.

For event.MsgAudio, Lines 734-737 return before the !supportsMedia branch below, so dispatchMediaUnderstandingFallback(...) at Lines 772-788 never runs. Rooms with a configured audio fallback/transcription model will still get the unsupported error instead of being transcribed.

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

In `@bridges/ai/handlematrix.go` around lines 730 - 737, The current early return
for event.MsgAudio/MsgVideo prevents the audio fallback flow from running;
change the logic so that for MsgAudio you only short-circuit to dispatchTextOnly
when understanding.Body is present, but do NOT immediately return the
UnsupportedMessageStatus error for audio—allow the path that checks
supportsMedia and calls dispatchMediaUnderstandingFallback(...) to run when no
transcript/understanding is available; keep the unsupported error for MsgVideo
(or for audio only when neither understanding nor a fallback/transcription model
is configured). Locate the branch that checks msgType, understanding, and calls
dispatchTextOnly and dispatchMediaUnderstandingFallback to adjust the control
flow accordingly.
♻️ Duplicate comments (2)
base_reaction_handler.go (1)

37-38: ⚠️ Potential issue | 🟡 Minor

Error from EnsureSyntheticReactionSenderGhost is still being silently discarded.

The error should be logged rather than discarded. If this call fails, the subsequent reaction handling may still encounter FK constraint violations, and without logging, these failures will be difficult to diagnose.

🛠️ Proposed fix to log the error
 	// Best-effort persistence guard for reaction.sender_id -> ghost.id FK.
-	_ = EnsureSyntheticReactionSenderGhost(ctx, login, msg.Event.Sender)
+	if err := EnsureSyntheticReactionSenderGhost(ctx, login, msg.Event.Sender); err != nil && login != nil {
+		login.Log.Warn().Err(err).Stringer("sender", msg.Event.Sender).Msg("Failed to ensure synthetic ghost for reaction sender")
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@base_reaction_handler.go` around lines 37 - 38, Ensure the error returned by
EnsureSyntheticReactionSenderGhost(ctx, login, msg.Event.Sender) is not ignored:
capture its return value and log it (using the logger available in scope) with
contextual fields (e.g., login and msg.Event.Sender) so failures are visible;
replace the discarded assignment "_ = EnsureSyntheticReactionSenderGhost(...)"
with code that checks err and calls the existing logger (or fmt.Errorf wrapper)
to emit a clear error message including the sender and login identifiers.
bridges/ai/streaming_error_handling.go (1)

24-28: ⚠️ Potential issue | 🟠 Major

Include ephemeral target check to prevent duplicate messages.

The current condition only checks hasEditTarget(), but if an ephemeral target has initialEventID set (while the stream target lacks edit capability), the function incorrectly returns PreDeltaError. This allows fallback retries even after partial output was already delivered, potentially duplicating the assistant turn.

🔧 Proposed fix
 func streamFailureError(state *streamingState, err error) error {
-	if state != nil && state.hasEditTarget() {
+	if state != nil && (state.hasEditTarget() || state.hasEphemeralTarget()) {
 		return &NonFallbackError{Err: err}
 	}
 	return &PreDeltaError{Err: err}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/streaming_error_handling.go` around lines 24 - 28,
streamFailureError currently returns PreDeltaError when hasEditTarget() is
false, but misses the case where an ephemeral target has initialEventID set and
partial output was already delivered; update streamFailureError (and related
streamingState usage) to treat that case like an edit-capable target by checking
for the ephemeral-target-with-initial-event condition (e.g., add or call a
method such as streamingState.hasEphemeralTargetWithInitialEvent() or check
state.ephemeralTarget != nil && state.ephemeralTarget.initialEventID != ""), and
return &NonFallbackError{Err: err} when either hasEditTarget() OR the
ephemeral-initial-event condition is true so we avoid fallback retries and
duplicate assistant messages.
🧹 Nitpick comments (19)
bridges/ai/canonical_history.go (1)

44-51: URL added to text before download verification.

The URL is written to the string builder at line 48 before verifying the download succeeds at line 49. If the download fails, the text block will reference an image URL that isn't actually included in the blocks. Consider moving the fmt.Fprintf call inside the success branch, or verify this is intentional for debugging purposes.

♻️ Optional fix to only include URL when download succeeds
 	for _, f := range files {
 		if !isImageMimeType(f.MimeType) || strings.TrimSpace(f.URL) == "" {
 			continue
 		}
-		fmt.Fprintf(&sb, "\n[media_url: %s]", f.URL)
 		if imgPart := oc.downloadHistoryImageBlock(ctx, f.URL, f.MimeType); imgPart != nil {
+			fmt.Fprintf(&sb, "\n[media_url: %s]", f.URL)
 			blocks = append(blocks, *imgPart)
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/canonical_history.go` around lines 44 - 51, The code writes the
image URL to the string builder before confirming the download; move the
fmt.Fprintf(&sb, "\n[media_url: %s]", f.URL) call inside the success branch
where oc.downloadHistoryImageBlock(ctx, f.URL, f.MimeType) returns non-nil so
the text only references images that were actually downloaded; update the loop
around isImageMimeType/f.URL and the blocks append logic so the URL is emitted
only when imgPart != nil (reference: files loop, isImageMimeType, f.URL, sb,
oc.downloadHistoryImageBlock, blocks).
bridges/ai/gravatar.go (1)

173-182: Nit: json.Number case is unreachable.

The JSON decoder on line 74 doesn't use UseNumber(), so numeric values are decoded as float64, not json.Number. The json.Number case here will never execute.

Either remove the dead case or configure the decoder with dec.UseNumber() if you want to preserve numeric precision.

Option 1: Remove dead code
 func formatGravatarScalar(value any) string {
 	switch v := value.(type) {
 	case string:
 		return v
-	case json.Number:
-		return v.String()
 	default:
 		return fmt.Sprint(v)
 	}
 }
Option 2: Use json.Number in decoder (lines 73-76)
-	var profile map[string]any
-	if err := json.NewDecoder(resp.Body).Decode(&profile); err != nil {
+	var profile map[string]any
+	dec := json.NewDecoder(resp.Body)
+	dec.UseNumber()
+	if err := dec.Decode(&profile); err != nil {
 		return nil, fmt.Errorf("failed to decode Gravatar profile: %w", err)
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/gravatar.go` around lines 173 - 182, The json.Number branch in
formatGravatarScalar is unreachable because the JSON decoder isn't configured to
use json.Number; either remove the json.Number case from formatGravatarScalar to
eliminate dead code, or enable number preservation by calling UseNumber() on the
json.Decoder instance used to parse the Gravatar response (so
formatGravatarScalar's json.Number case becomes reachable); update the decoder
creation to call dec.UseNumber() if you choose the latter and keep
formatGravatarScalar as-is.
bridges/ai/integrations_config.go (1)

65-105: YAML tag naming inconsistency.

The YAML tags here use camelCase (ttlSeconds, requireForMcp, requireForTools) while other configs in this file use snake_case (dedupe_ttl, base_url, api_key). Consider standardizing to one convention for a consistent user experience when editing config files.

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

In `@bridges/ai/integrations_config.go` around lines 65 - 105, The YAML tags on
ToolApprovalsRuntimeConfig are inconsistent with the rest of the file (camelCase
vs snake_case); update the struct field tags for TTLSeconds, RequireForMCP, and
RequireForTools to use snake_case (e.g., ttl_seconds, require_for_mcp,
require_for_tools) so the config aligns with other structs, then run go
vet/build to ensure no tag typos and keep WithDefaults, Enabled, TTLSeconds,
RequireForMCP and RequireForTools identifiers unchanged.
bridges/ai/image_generation_tool.go (3)

65-65: Consider making the HTTP client configurable for testing.

The global imageGenHTTPClient with a hardcoded 120-second timeout works for production but makes unit testing difficult. Consider injecting the client or timeout via the BridgeToolContext.

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

In `@bridges/ai/image_generation_tool.go` at line 65, Replace the package-level
hardcoded variable imageGenHTTPClient with a configurable HTTP client provided
via BridgeToolContext so tests can inject a custom client or timeout; update
BridgeToolContext to include an *http.Client (or timeout config) with a nil-safe
default that constructs the current &http.Client{Timeout:120*time.Second}, and
refactor all usages that reference imageGenHTTPClient to read from
ctx.ImageGenHTTPClient (or ctx.HTTPClient) so unit tests can pass a mock client
with controlled behavior and timeouts.

156-272: Consider extracting provider-specific fallback logic into helper functions.

resolveImageGenProvider has high cyclomatic complexity with nested switch statements. The fallback chains within ProviderBeeper could be extracted into a helper like selectBestAvailableProvider(openAI, gemini, openRouter bool, preferred imageGenProvider) to improve readability.

🤖 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 156 - 272, The
resolveImageGenProvider function has high cyclomatic complexity (especially in
the ProviderBeeper branch); refactor by extracting the fallback logic into a
helper (e.g., selectBestAvailableProvider(openAISupported, geminiSupported,
openRouterSupported imageGenProvider preferred) or similar) and replace the long
chain inside the ProviderBeeper case with calls to that helper and small
condition checks; use existing predicates (supportsOpenAIImageGen,
supportsGeminiImageGen, supportsOpenRouterImageGen, usesGeminiParams,
usesOpenAIParams, inferProviderFromModel) to build the helper’s inputs and
return values so the behavior is preserved while resolveImageGenProvider and the
ProviderBeeper branch become much simpler and easier to read.

603-631: Consider cancelling remaining requests on first error.

When an error occurs, the function returns immediately but other goroutines continue making API calls. While not a leak (the buffered channel prevents blocking), this wastes resources on requests that will be discarded.

♻️ Suggested improvement using cancellable context
+		ctx, cancel := context.WithCancel(ctx)
+		defer cancel()
 		concurrency := 3
 		if count < concurrency {
 			concurrency = count
 		}
 		sem := make(chan struct{}, concurrency)
 		results := make(chan genResult, count)
 		for i := 0; i < count; i++ {
 			sem <- struct{}{}
 			go func() {
 				defer func() { <-sem }()
 				out, err := callOpenRouterImageGenWithControls(ctx, btc, openRouterAPIKey, openRouterBaseURL, req, model)
 				results <- genResult{images: out, err: err}
 			}()
 		}
 		images := make([]string, 0, count)
+		var firstErr error
 		for i := 0; i < count; i++ {
 			r := <-results
 			if r.err != nil {
-				return nil, r.err
+				if firstErr == nil {
+					firstErr = r.err
+					cancel() // Signal remaining goroutines to abort
+				}
+				continue
 			}
-			images = append(images, r.images...)
+			if firstErr == nil {
+				images = append(images, r.images...)
+			}
 		}
+		if firstErr != nil {
+			return nil, firstErr
+		}
🤖 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 603 - 631, Wrap the
per-request goroutines with a cancellable context and cancel on first error:
create childCtx, cancel := context.WithCancel(ctx) and use childCtx when calling
callOpenRouterImageGenWithControls; when you receive a r.err != nil call
cancel() and then continue draining the remaining (count-i-1) results from the
results channel before returning the error so all goroutines can exit and/or
send their result into the buffered results channel; also defer cancel() after
creating it to ensure cancellation on normal completion. Ensure you update the
goroutine call to reference childCtx instead of ctx and keep existing symbols
genResult, sem, results, and callOpenRouterImageGenWithControls to locate the
code.
bridges/ai/reply_mentions.go (1)

71-84: Consider extending nil checks to cover DB and Message fields for consistency.

The defensive nil check on line 72 validates up to oc.UserLogin.Bridge, but line 75 accesses oc.UserLogin.Bridge.DB.Message.GetPartByMXID. While most code in the codebase accesses these fields directly, similar helper functions such as lastAssistantMessageInfo in integration_host.go explicitly check both DB and Message for nil before use. Adding these checks would align with the most defensive pattern found in the codebase and protect against potential nil pointer panics.

♻️ Proposed fix to extend nil checks
 func (oc *AIClient) isReplyToBot(ctx context.Context, portal *bridgev2.Portal, replyTo id.EventID) bool {
-	if oc == nil || portal == nil || replyTo == "" || oc.UserLogin == nil || oc.UserLogin.Bridge == nil {
+	if oc == nil || portal == nil || replyTo == "" || oc.UserLogin == nil || oc.UserLogin.Bridge == nil || oc.UserLogin.Bridge.DB == nil || oc.UserLogin.Bridge.DB.Message == nil {
 		return false
 	}
 	msg, err := oc.UserLogin.Bridge.DB.Message.GetPartByMXID(ctx, replyTo)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/reply_mentions.go` around lines 71 - 84, The function isReplyToBot
should extend its initial nil checks to include oc.UserLogin.Bridge.DB and
oc.UserLogin.Bridge.DB.Message to avoid nil dereference before calling
GetPartByMXID; update the guard in isReplyToBot to return false if oc, portal,
replyTo, oc.UserLogin, oc.UserLogin.Bridge, oc.UserLogin.Bridge.DB, or
oc.UserLogin.Bridge.DB.Message are nil, then proceed to call GetPartByMXID as
before.
bridges/ai/heartbeat_delivery.go (1)

68-84: Make the room resolver self-guarding before the lookup.

resolveHeartbeatDeliveryRoom calls portalByRoomID before any nil/readiness check on oc. Today the current caller is pre-checked, but moving the oc == nil / UserLogin == nil / !oc.IsLoggedIn() guards to the top makes this helper safe on its own and avoids the lookup when the bridge is disconnected.

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

In `@bridges/ai/heartbeat_delivery.go` around lines 68 - 84, In
resolveHeartbeatDeliveryRoom, make the method self-guarding by checking the
receiver's readiness before calling portalByRoomID: first return
deliveryTarget{Channel: "matrix", Reason: "channel-not-ready"} if oc == nil or
oc.UserLogin == nil or !oc.IsLoggedIn(); only then proceed to trim the raw
string and call portalByRoomID(id.RoomID(trimmed)). This avoids doing the portal
lookup when the bridge/client is nil or not logged in and keeps the same return
reasons used today.
base_login_process.go (1)

1-1: Call out the bridgeadapteragentremote rename in upgrade notes.

Because this package exports types like BaseLoginProcess, the rename is a public API break for downstream imports even if the implementation is unchanged. Please make the migration path explicit in changelog/docs.

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

In `@base_login_process.go` at line 1, Document the package rename from
bridgeadapter to agentremote in the upgrade notes and changelog, explicitly
calling out that exported types such as BaseLoginProcess (and any other public
surface) moved packages and showing the exact migration step (e.g., update
import paths from "…/bridgeadapter" to "…/agentremote"), include guidance on
updating go.mod imports or IDE refactors and add a [migration] entry referencing
BaseLoginProcess to help downstream maintainers locate and update usages; ensure
the note is added to the project's changelog/release notes and any README/docs
that list public API.
bridges/ai/sessions_tools.go (1)

1-1: Document the connectorai rename as a breaking package change.

This is fine in-repo, but it changes the default import name for any downstream code importing bridges/ai, so upgrades will require source changes. A short migration note / release note would make that break explicit.

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

In `@bridges/ai/sessions_tools.go` at line 1, Document the package rename from
connector to ai as a breaking change and add a short migration note in the
release notes/CHANGELOG/UPGRADING explaining that downstream imports must be
updated from "bridges/connector" (or package name connector) to "bridges/ai"
(package ai); include one or two example before/after import lines and call out
that the default import identifier has changed (connector → ai) so consumers
must update their source. Ensure the note references the package name change
(connector → ai) and the file(s) under bridges/ai to make the migration obvious
to downstream users.
bridges/ai/bridge_info.go (1)

1-1: Document the connectorai rename as a breaking import change.

Downstream Go code importing github.com/beeper/agentremote/bridges/ai and using the old default package name will need selector updates. Please call that out in upgrade notes or release notes.

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

In `@bridges/ai/bridge_info.go` at line 1, Document the package rename from
"connector" to "ai" as a breaking import change: update the release/upgrade
notes to state that code importing github.com/beeper/agentremote/bridges/ai
(package ai) used to be named "connector" and consumers must update any selector
uses (e.g., change connector.X to ai.X) and import comments/examples
accordingly; include an explicit migration example and call out this is a
breaking change for downstream users referencing the connector package name.
base_connector.go (1)

1-1: Call out the bridgeadapteragentremote rename in migration docs.

This is a source-level breaking change for downstream Go consumers that import the root package without an alias and refer to the old package identifier. Please include an explicit upgrade note mapping old selectors to agentremote.

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

In `@base_connector.go` at line 1, Add an explicit upgrade note to the migration
docs that calls out the package rename from bridgeadapter to agentremote; state
that any downstream code importing the root package without an alias and using
selectors from the old package identifier (e.g., bridgeadapter.SomeType,
bridgeadapter.SomeFunc) must be updated to agentremote.SomeType and
agentremote.SomeFunc, and include a short mapping table or examples showing old
→ new selectors to make the breaking change clear for consumers.
bridges/ai/scheduler_ticks.go (1)

156-161: Remove max64 helper in favor of builtin max function.

Go 1.21 introduced a builtin max function. The project targets Go 1.25.0, so the custom max64 helper at lines 156–161 can be safely removed. Replace all usages of max64(a, b) with max(a, b) across the codebase (scheduler_cron.go, scheduler_heartbeat.go, and scheduler_ticks.go).

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

In `@bridges/ai/scheduler_ticks.go` around lines 156 - 161, Remove the custom
helper max64 and replace its usages with the Go 1.21 builtin max: delete the
function max64(a, b int64) and update all calls of max64(a, b) to max(a, b)
(notably in scheduler_cron.go, scheduler_heartbeat.go, and scheduler_ticks.go);
ensure the arguments remain int64 (max supports ordered types) and run `go
vet`/`go test` to confirm no remaining references to max64.
bridges/ai/subagent_spawn.go (1)

16-19: Consider fixing import order.

The agentremote import is placed after internal package imports. Standard Go convention groups imports as: stdlib, external packages, then internal packages.

♻️ Suggested import order
 	"github.com/beeper/agentremote/pkg/agents"
 	"github.com/beeper/agentremote/pkg/agents/tools"
-	"github.com/beeper/agentremote"
+)
+
+import (
+	"github.com/beeper/agentremote"
 )

Or consolidate all agentremote imports together:

+	"github.com/beeper/agentremote"
 	"github.com/beeper/agentremote/pkg/agents"
 	"github.com/beeper/agentremote/pkg/agents/tools"
-	"github.com/beeper/agentremote"
 )
🤖 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 16 - 19, The import block in
subagent_spawn.go is misordered: "github.com/beeper/agentremote" appears after
internal package imports; reorder the imports to follow Go convention (stdlib
first, external packages next, then internal packages) and consolidate all
agentremote imports together (e.g., keep "github.com/beeper/agentremote" with
the other beeper/* imports like "github.com/beeper/agentremote/pkg/agents" and
"github.com/beeper/agentremote/pkg/agents/tools") so the import groupings are
correct.
bridges/ai/approval_prompt_presentation_test.go (1)

5-34: Assert the important rendered fields, not just “non-empty”.

These tests would still pass if Tool, Action, Server, or the scalar-input summary path disappeared, because they only check that something was produced. A few concrete label/value assertions would give the new presentation helpers real regression coverage without depending on full detail ordering.

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

In `@bridges/ai/approval_prompt_presentation_test.go` around lines 5 - 34, The
tests currently only assert non-empty fields; update
TestBuildBuiltinApprovalPresentation and TestBuildMCPApprovalPresentation to
assert specific rendered label/value pairs instead of just len>0: verify
presentation.AllowAlways and Title, then locate in presentation.Details the
entries with labels like "Tool", "Action" (for built-in: expect
"commandExecution" and "run"), and the scalar-input summary entries for input
keys ("command" -> "ls -la", "cwd" -> "/tmp"); for MCP test assert labels like
"Server"/"Tool" and "Action" (expect "filesystem" and "read_file") and that the
path scalar shows "/tmp/demo.txt". Use existence-based checks (search by
label/key) rather than relying on ordering so tests remain robust.
approval_flow_test.go (1)

95-110: Use a wider timeout budget for these async waits.

These assertions depend on goroutine scheduling inside FinishResolved and ResolveExternal, and 500ms is tight enough to flake on busy CI runners. A small helper with a ~1-2s default, or one derived from t.Deadline(), would make the new tests much more stable.

Also applies to: 249-255

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

In `@approval_flow_test.go` around lines 95 - 110, The 500ms timeouts for the
async selects waiting on cleanupCh and editCh are too tight and flakey; replace
those time.After(500 * time.Millisecond) calls with a wider, configurable
timeout (e.g., 2*time.Second) or better, add a small helper like waitFor(t, ch)
that uses a default 2s timeout (or derives from t.Deadline() when present) and
use it in the two selects shown (the blocks reading from cleanupCh and editCh)
and the similar block at 249-255; this ensures waits triggered by
FinishResolved/ResolveExternal have a more stable timeout budget.
bridges/ai/streaming_error_handling_test.go (1)

12-17: Lock in that event IDs are not edit targets.

The new split only checks the positive case for initialEventID. Add the inverse assertion too; otherwise a regression where an event ID also satisfies hasEditTarget() will still pass and route streamFailureError down the wrong branch.

Suggested test addition
 	t.Run("event-id", func(t *testing.T) {
 		state := &streamingState{initialEventID: id.EventID("$evt")}
 		if !state.hasEphemeralTarget() {
 			t.Fatalf("expected event-id target to be a valid ephemeral target")
 		}
+		if state.hasEditTarget() {
+			t.Fatalf("did not expect event-id alone to be a valid edit target")
+		}
 	})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/streaming_error_handling_test.go` around lines 12 - 17, The test
for event-id only asserts that streamingState{initialEventID:
id.EventID("$evt")} hasEphemeralTarget(); add the inverse assertion that this
state does NOT satisfy hasEditTarget() so regressions don't treat event IDs as
edit targets; update the "event-id" subtest to call state.hasEditTarget() and
t.Fatalf if it returns true (i.e., assert hasEditTarget() == false) to ensure
streamFailureError routing remains correct.
bridges/ai/tool_approvals_test.go (1)

55-58: Resolve through the same path the product uses.

Calling approvalFlow.Resolve directly skips the room/owner checks and prompt-finalization logic that reactions or external approvals go through, so this test can still pass even if the user-facing resolution path breaks. Consider routing it through the same helper used by those paths, or extracting that validation/dispatch into a unit-testable method.

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

In `@bridges/ai/tool_approvals_test.go` around lines 55 - 58, The test is calling
approvalFlow.Resolve directly (approvalFlow.Resolve with
agentremote.ApprovalDecisionPayload{ApprovalID, Approved:true}), which bypasses
room/owner checks and prompt-finalization; change the test to route the
resolution through the same helper used by reactions/external approvals (the
public resolver path that performs room/owner validation and dispatch) instead
of calling approvalFlow.Resolve directly, or extract the validation/dispatch
logic from that public helper into a unit-testable method and call that method
from the test so you still exercise the real room/owner checks and
prompt-finalization logic.
bridges/ai/beeper_models.json (1)

195-206: Missing max_output_tokens are safely defaulted, but reliance on implicit zero-value handling could be clearer.

The catalog does omit max_output_tokens for 13 models (deepseek-v3.1-terminus, minimax-m2.1, kimi-k2, kimi-k2-0905, gpt-oss-120b, gpt-oss-20b, qwen2.5-vl-32b-instruct, qwen3-coder, grok-3, grok-3-mini, grok-4, glm-4.7, glm-5), which deserialize to 0 in the generated beeper_models_generated.go. However, effectiveMaxTokens() in client.go explicitly checks info.MaxOutputTokens > 0 and falls back to defaultMaxTokens when zero, so the runtime safety is in place.

The pattern works but is implicit: missing JSON fields silently become 0, then get treated as "unknown" by the guard check. This is correct but fragile—consider either making the field explicitly optional in the data model or documenting this contract prominently in the generated code comment.

Also applies to: 456-467, 485-510, 768-793, 863-960, 1071-1096

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

In `@bridges/ai/beeper_models.json` around lines 195 - 206, The JSON omits
max_output_tokens for many models which deserializes to 0 and relies on
effectiveMaxTokens() in client.go to treat 0 as "unknown"; make this explicit by
changing the generated model type in beeper_models_generated.go to use a pointer
(e.g., MaxOutputTokens *int) and update effectiveMaxTokens() to check for nil
instead of >0 (falling back to defaultMaxTokens when nil), or alternatively add
explicit "max_output_tokens" values in the JSON entries for the listed models;
update any deserialization/usage sites accordingly so the presence/absence of
the field is unambiguous.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: be284f1a-c2ef-4a40-8cdc-1a26b7a3655f

📥 Commits

Reviewing files that changed from the base of the PR and between 437e0dc and f900e74.

📒 Files selected for processing (300)
  • approval_decision.go
  • approval_flow.go
  • approval_flow_test.go
  • approval_manager.go
  • approval_prompt.go
  • approval_prompt_test.go
  • approval_reaction_helpers.go
  • approval_reaction_helpers_test.go
  • base_connector.go
  • base_login_process.go
  • base_reaction_handler.go
  • base_stream_state.go
  • bridges/ai/abort_helpers.go
  • bridges/ai/account_hints.go
  • bridges/ai/account_hints_test.go
  • bridges/ai/ack_reactions.go
  • bridges/ai/active_room_state.go
  • bridges/ai/agent_activity.go
  • bridges/ai/agent_contact_identifiers_test.go
  • bridges/ai/agent_display.go
  • bridges/ai/agents_list_tool.go
  • bridges/ai/agentstore.go
  • bridges/ai/agentstore_capture_test.go
  • bridges/ai/agentstore_room_lookup.go
  • bridges/ai/approval_prompt_presentation.go
  • bridges/ai/approval_prompt_presentation_test.go
  • bridges/ai/audio_analysis.go
  • bridges/ai/audio_generation.go
  • bridges/ai/audio_mime.go
  • bridges/ai/beeper_models.json
  • bridges/ai/beeper_models_generated.go
  • bridges/ai/beeper_models_manifest_test.go
  • bridges/ai/bootstrap_context.go
  • bridges/ai/bootstrap_context_test.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/canonical_history.go
  • bridges/ai/canonical_history_test.go
  • bridges/ai/canonical_prompt_messages.go
  • bridges/ai/canonical_user_messages.go
  • bridges/ai/chat.go
  • bridges/ai/chat_fork_test.go
  • bridges/ai/chat_login_redirect_test.go
  • bridges/ai/chat_search_test.go
  • bridges/ai/client.go
  • bridges/ai/client_capabilities_test.go
  • bridges/ai/client_runtime_helpers.go
  • bridges/ai/command_aliases.go
  • bridges/ai/command_registry.go
  • bridges/ai/commandregistry/registry.go
  • bridges/ai/commands.go
  • bridges/ai/commands_helpers.go
  • bridges/ai/commands_login_selection_test.go
  • bridges/ai/commands_mcp_test.go
  • bridges/ai/commands_parity.go
  • bridges/ai/compaction_summarization.go
  • bridges/ai/compaction_summarization_test.go
  • bridges/ai/config_test.go
  • bridges/ai/connector.go
  • bridges/ai/connector_validate_userid_test.go
  • bridges/ai/constructors.go
  • bridges/ai/context_overrides.go
  • bridges/ai/context_pruning_test.go
  • bridges/ai/context_value.go
  • bridges/ai/debounce.go
  • bridges/ai/debounce_test.go
  • bridges/ai/dedupe.go
  • bridges/ai/dedupe_test.go
  • bridges/ai/default_chat_test.go
  • bridges/ai/defaults_alignment_test.go
  • bridges/ai/delivery_target.go
  • bridges/ai/desktop_api_helpers.go
  • bridges/ai/desktop_api_native_test.go
  • bridges/ai/desktop_api_sessions.go
  • bridges/ai/desktop_instance_resolver_test.go
  • bridges/ai/desktop_networks.go
  • bridges/ai/duration.go
  • bridges/ai/envelope_test.go
  • bridges/ai/error_logging.go
  • bridges/ai/errors.go
  • bridges/ai/errors_extended.go
  • bridges/ai/errors_test.go
  • bridges/ai/events.go
  • bridges/ai/events_test.go
  • bridges/ai/gravatar.go
  • bridges/ai/group_activation.go
  • bridges/ai/group_history.go
  • bridges/ai/group_history_test.go
  • bridges/ai/handleai.go
  • bridges/ai/handleai_test.go
  • bridges/ai/handlematrix.go
  • bridges/ai/handler_interfaces.go
  • bridges/ai/heartbeat_active_hours.go
  • bridges/ai/heartbeat_config.go
  • bridges/ai/heartbeat_config_test.go
  • bridges/ai/heartbeat_context.go
  • bridges/ai/heartbeat_delivery.go
  • bridges/ai/heartbeat_events.go
  • bridges/ai/heartbeat_execute.go
  • bridges/ai/heartbeat_session.go
  • bridges/ai/heartbeat_state.go
  • bridges/ai/heartbeat_visibility.go
  • bridges/ai/history_limit_test.go
  • bridges/ai/identifiers.go
  • bridges/ai/identifiers_test.go
  • bridges/ai/identity_sync.go
  • bridges/ai/image_analysis.go
  • bridges/ai/image_generation.go
  • bridges/ai/image_generation_tool.go
  • bridges/ai/image_generation_tool_magic_proxy_test.go
  • bridges/ai/image_understanding.go
  • bridges/ai/inbound_debounce.go
  • bridges/ai/inbound_prompt_runtime_test.go
  • bridges/ai/inbound_runtime_context.go
  • bridges/ai/integration_host.go
  • bridges/ai/integrations.go
  • bridges/ai/integrations_config.go
  • bridges/ai/integrations_example-config.yaml
  • bridges/ai/integrations_test.go
  • bridges/ai/internal_dispatch.go
  • bridges/ai/linkpreview.go
  • bridges/ai/linkpreview_test.go
  • bridges/ai/login.go
  • bridges/ai/login_loaders.go
  • bridges/ai/logout_cleanup.go
  • bridges/ai/magic_proxy_test.go
  • bridges/ai/managed_beeper.go
  • bridges/ai/managed_beeper_test.go
  • bridges/ai/matrix_coupling.go
  • bridges/ai/matrix_helpers.go
  • bridges/ai/matrix_payload.go
  • bridges/ai/mcp_client.go
  • bridges/ai/mcp_client_test.go
  • bridges/ai/mcp_helpers.go
  • bridges/ai/mcp_servers.go
  • bridges/ai/mcp_servers_test.go
  • bridges/ai/media_download.go
  • bridges/ai/media_helpers.go
  • bridges/ai/media_prompt.go
  • bridges/ai/media_send.go
  • bridges/ai/media_understanding_attachments.go
  • bridges/ai/media_understanding_cli.go
  • bridges/ai/media_understanding_defaults.go
  • bridges/ai/media_understanding_format.go
  • bridges/ai/media_understanding_providers.go
  • bridges/ai/media_understanding_resolve.go
  • bridges/ai/media_understanding_runner.go
  • bridges/ai/media_understanding_runner_openai_test.go
  • bridges/ai/media_understanding_scope.go
  • bridges/ai/media_understanding_types.go
  • bridges/ai/mentions.go
  • bridges/ai/message_formatting.go
  • bridges/ai/message_pins.go
  • bridges/ai/message_results.go
  • bridges/ai/message_send.go
  • bridges/ai/message_status.go
  • bridges/ai/messages.go
  • bridges/ai/messages_responses_input_test.go
  • bridges/ai/metadata.go
  • bridges/ai/metadata_test.go
  • bridges/ai/model_api.go
  • bridges/ai/model_catalog.go
  • bridges/ai/model_catalog_test.go
  • bridges/ai/model_contacts.go
  • bridges/ai/models.go
  • bridges/ai/models_api.go
  • bridges/ai/models_api_test.go
  • bridges/ai/msgconv/to_matrix.go
  • bridges/ai/msgconv/to_matrix_test.go
  • bridges/ai/owner_allowlist.go
  • bridges/ai/pending_queue.go
  • bridges/ai/portal_cleanup.go
  • bridges/ai/portal_send.go
  • bridges/ai/portal_send_test.go
  • bridges/ai/prompt_params.go
  • bridges/ai/provider.go
  • bridges/ai/provider_openai.go
  • bridges/ai/provider_openai_chat.go
  • bridges/ai/provider_openai_responses.go
  • bridges/ai/provisioning.go
  • bridges/ai/provisioning_test.go
  • bridges/ai/queue_helpers.go
  • bridges/ai/queue_policy_runtime_test.go
  • bridges/ai/queue_resolution.go
  • bridges/ai/queue_settings.go
  • bridges/ai/queue_status_test.go
  • bridges/ai/reaction_feedback.go
  • bridges/ai/reaction_handling.go
  • bridges/ai/reactions.go
  • bridges/ai/remote_events.go
  • bridges/ai/remote_message.go
  • bridges/ai/reply_mentions.go
  • bridges/ai/reply_policy.go
  • bridges/ai/reply_policy_runtime_test.go
  • bridges/ai/response_finalization.go
  • bridges/ai/response_finalization_test.go
  • bridges/ai/response_retry.go
  • bridges/ai/response_retry_test.go
  • bridges/ai/room_activity.go
  • bridges/ai/room_capabilities.go
  • bridges/ai/room_runs.go
  • bridges/ai/runtime_compaction_adapter.go
  • bridges/ai/runtime_defaults_test.go
  • bridges/ai/scheduler.go
  • bridges/ai/scheduler_cron.go
  • bridges/ai/scheduler_db.go
  • bridges/ai/scheduler_events.go
  • bridges/ai/scheduler_heartbeat.go
  • bridges/ai/scheduler_host.go
  • bridges/ai/scheduler_rooms.go
  • bridges/ai/scheduler_ticks.go
  • bridges/ai/session_greeting.go
  • bridges/ai/session_greeting_test.go
  • bridges/ai/session_keys.go
  • bridges/ai/session_store.go
  • bridges/ai/session_transcript_openclaw.go
  • bridges/ai/session_transcript_openclaw_test.go
  • bridges/ai/sessions_tools.go
  • bridges/ai/sessions_visibility_test.go
  • bridges/ai/simple_mode_prompt.go
  • bridges/ai/simple_mode_prompt_test.go
  • bridges/ai/source_citations.go
  • bridges/ai/source_citations_test.go
  • bridges/ai/status_events_context.go
  • bridges/ai/status_text.go
  • bridges/ai/status_text_heartbeat_test.go
  • bridges/ai/stream_events.go
  • bridges/ai/stream_transport.go
  • bridges/ai/streaming_chat_completions.go
  • bridges/ai/streaming_continuation.go
  • bridges/ai/streaming_error_handling.go
  • bridges/ai/streaming_error_handling_test.go
  • bridges/ai/streaming_finish_reason_test.go
  • bridges/ai/streaming_function_calls.go
  • bridges/ai/streaming_init.go
  • bridges/ai/streaming_init_test.go
  • bridges/ai/streaming_input_conversion.go
  • bridges/ai/streaming_output_handlers.go
  • bridges/ai/streaming_output_items.go
  • bridges/ai/streaming_output_items_test.go
  • bridges/ai/streaming_params.go
  • bridges/ai/streaming_persistence.go
  • bridges/ai/streaming_response_lifecycle.go
  • bridges/ai/streaming_responses_api.go
  • bridges/ai/streaming_responses_finalize.go
  • bridges/ai/streaming_responses_input_test.go
  • bridges/ai/streaming_state.go
  • bridges/ai/streaming_text_deltas.go
  • bridges/ai/streaming_tool_selection.go
  • bridges/ai/streaming_tool_selection_test.go
  • bridges/ai/streaming_ui_events.go
  • bridges/ai/streaming_ui_finish.go
  • bridges/ai/streaming_ui_helpers.go
  • bridges/ai/streaming_ui_sources.go
  • bridges/ai/streaming_ui_tools.go
  • bridges/ai/strict_cleanup_test.go
  • bridges/ai/subagent_announce.go
  • bridges/ai/subagent_conversion.go
  • bridges/ai/subagent_registry.go
  • bridges/ai/subagent_spawn.go
  • bridges/ai/system_ack.go
  • bridges/ai/system_events.go
  • bridges/ai/system_events_db.go
  • bridges/ai/system_prompts.go
  • bridges/ai/system_prompts_test.go
  • bridges/ai/target_test_helpers_test.go
  • bridges/ai/text_files.go
  • bridges/ai/timezone.go
  • bridges/ai/toast.go
  • bridges/ai/toast_test.go
  • bridges/ai/token_resolver.go
  • bridges/ai/tokenizer.go
  • bridges/ai/tokenizer_fallback_test.go
  • bridges/ai/tool_approvals.go
  • bridges/ai/tool_approvals_policy.go
  • bridges/ai/tool_approvals_policy_test.go
  • bridges/ai/tool_approvals_rules.go
  • bridges/ai/tool_approvals_test.go
  • bridges/ai/tool_availability_configured_test.go
  • bridges/ai/tool_call_id.go
  • bridges/ai/tool_call_id_test.go
  • bridges/ai/tool_configured.go
  • bridges/ai/tool_descriptions.go
  • bridges/ai/tool_execution.go
  • bridges/ai/tool_policy.go
  • bridges/ai/tool_policy_apply_patch_test.go
  • bridges/ai/tool_policy_chain.go
  • bridges/ai/tool_policy_chain_test.go
  • bridges/ai/tool_registry.go
  • bridges/ai/tool_schema_sanitize.go
  • bridges/ai/tool_schema_sanitize_test.go
  • bridges/ai/tools.go
  • bridges/ai/tools_analyze_image.go
  • bridges/ai/tools_apply_patch.go
  • bridges/ai/tools_beeper_docs.go
  • bridges/ai/tools_beeper_feedback.go
  • bridges/ai/tools_matrix_api.go
  • bridges/ai/tools_message_actions.go
✅ Files skipped from review due to trivial changes (16)
  • bridges/ai/tool_descriptions.go
  • bridges/ai/beeper_models_manifest_test.go
  • bridges/ai/canonical_history_test.go
  • bridges/ai/timezone.go
  • bridges/ai/heartbeat_config_test.go
  • bridges/ai/session_keys.go
  • bridges/ai/models.go
  • bridges/ai/group_history.go
  • bridges/ai/mcp_servers.go
  • bridges/ai/canonical_prompt_messages.go
  • bridges/ai/sessions_visibility_test.go
  • bridges/ai/streaming_finish_reason_test.go
  • bridges/ai/handleai_test.go
  • bridges/ai/runtime_defaults_test.go
  • bridges/ai/connector_validate_userid_test.go
  • bridges/ai/debounce_test.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). (2)
  • GitHub Check: build-docker
  • GitHub Check: build-docker

Copy link

@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: 1

🤖 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/streaming_state.go`:
- Around line 82-99: hasInitialMessageTarget currently only defers to
hasEditTarget(), causing it to return false when initialEventID is set but
networkMessageID is empty; update streamingState.hasInitialMessageTarget to
treat either an edit target or an ephemeral target as an initial message target
by checking both hasEditTarget() and hasEphemeralTarget() (i.e., return true
when s != nil and (s.hasEditTarget() || s.hasEphemeralTarget())), referencing
streamingState.hasInitialMessageTarget, hasEditTarget, hasEphemeralTarget,
streamTarget, networkMessageID and initialEventID to locate and fix the logic.
🪄 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: 2f9d7685-f667-4d7d-9a33-0692287a45e7

📥 Commits

Reviewing files that changed from the base of the PR and between f900e74 and 17bcacf.

📒 Files selected for processing (9)
  • approval_manager.go
  • bridges/ai/agentstore.go
  • bridges/ai/chat.go
  • bridges/ai/client.go
  • bridges/ai/connector.go
  • bridges/ai/identifiers.go
  • bridges/ai/response_finalization.go
  • bridges/ai/streaming_state.go
  • bridges/ai/subagent_spawn.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • approval_manager.go
  • bridges/ai/subagent_spawn.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). (2)
  • GitHub Check: build-docker
  • GitHub Check: build-docker
🧰 Additional context used
🧬 Code graph analysis (3)
bridges/ai/identifiers.go (2)
bridges/openclaw/metadata.go (2)
  • PortalMetadata (25-76)
  • UserLoginMetadata (13-23)
bridges/opencode/metadata.go (2)
  • PortalMetadata (15-26)
  • UserLoginMetadata (10-13)
bridges/ai/client.go (3)
bridges/opencode/api/types.go (1)
  • Event (206-209)
bridges/openclaw/metadata.go (1)
  • MessageMetadata (87-111)
message_metadata.go (1)
  • BaseMessageMetadata (7-26)
bridges/ai/response_finalization.go (2)
bridges/openclaw/metadata.go (1)
  • MessageMetadata (87-111)
message_metadata.go (1)
  • BaseMessageMetadata (7-26)
🔇 Additional comments (23)
bridges/ai/connector.go (2)

1-1: Package rename and import migration look correct.

The package name ai aligns with the file path bridges/ai/connector.go, and the import of github.com/beeper/agentremote is consistent with the migration from bridgeadapter to agentremote namespace described in the PR.

Also applies to: 19-19


61-61: API migration to agentremote package is consistent.

All six call sites have been correctly migrated from bridgeadapter.* to agentremote.*:

  • EnsureClientMap - proper mutex/map initialization
  • StopClients - correct cleanup on shutdown
  • PrimeUserLoginCache - context and bridge passed correctly
  • DefaultNetworkCapabilities - simple delegation
  • DefaultBridgeInfoVersion - simple delegation
  • BuildMetaTypes - factory functions passed as closures

The function signatures and parameter passing appear correct.

Also applies to: 65-65, 105-105, 133-133, 149-149, 174-179

bridges/ai/chat.go (3)

1-24: LGTM!

The package declaration and import migration from bridgeadapter to the root agentremote package is clean and consistent with the broader API surface consolidation described in the PR.


913-922: LGTM!

The migration to agentremote.BuildDMChatInfo with agentremote.DMChatInfoParams is clean. The parameter struct fields and subsequent member map override remain consistent with the previous implementation.


984-991: LGTM!

The agentremote.BuildSystemNotice migration is a clean namespace change with no semantic differences.

bridges/ai/agentstore.go (2)

1-20: Package and import migration looks clean.

This file reads consistently after the move to package ai, and replacing the old bridgeadapter dependency with the top-level agentremote import keeps the surface area simpler here.


382-394: Event ID construction is aligned with the package migration.

Switching this synthetic command event over to agentremote.NewEventID("internal") keeps the dispatch path on the new API without changing the surrounding command wiring.

bridges/ai/response_finalization.go (6)

1-21: LGTM!

Package rename and import migrations from bridgeadapter/streamtransport to agentremote/turns are consistent with the PR's refactoring objectives.


23-31: Continuation messages still lose reply/thread context.

The function signature doesn't accept reply target information, so overflow chunks from threaded/replied messages will spill into the room timeline as standalone events. The caller at Line 671 has access to replyToEventID but doesn't pass it through.

Consider extending the signature to accept an optional ReplyTarget and pass it to BuildContinuationMessage so continuation events remain in the same thread/reply chain.


74-82: LGTM!

The migration to agentremote.NewMessageID and agentremote.BaseMessageMetadata is correctly implemented and consistent with the type definitions in message_metadata.go.


600-606: Splitting rendered.Body loses markdown formatting.

rendered.Body is a fallback produced by format.RenderMarkdown's HTML-to-text conversion, not the original markdown source. Splitting this fallback and re-rendering on Line 605 causes a second transformation pass that strips markdown formatting from the first chunk and all continuation chunks.

The fix requires passing the original source text to this function and splitting it before calling RenderMarkdown on each chunk.


642-658: LGTM!

The edit target resolution and agentremote.RemoteEdit construction are correctly implemented with appropriate fallback logic.


667-672: Continuation loop inherits the same splitting issue.

The continuationBody variable originates from the rendered.Body split on Line 603, so all continuation chunks suffer from the same markdown-stripping problem noted above.

bridges/ai/identifiers.go (5)

1-1: LGTM!

Package name correctly matches the directory structure.


16-18: LGTM!

Import correctly added to support the migrated helper functions.


216-218: Generic function call is correct.

agentremote.EnsureLoginMetadata[T] is properly exported and the type parameter usage in loginMetadata is valid.


173-179: agentremote.EnsurePortalMetadata is properly exported and handles nil cases.

The generic function is correctly defined in agentremote and exported. It always returns a non-nil *T (either new(T) for nil portals or the result of ensureMetadata[T]), so the meta != nil check in line 175 is always true and could be simplified to just if portal != nil, though the current defensive approach is harmless.


135-137: Function signature is correct. The HumanUserID call properly passes a string prefix and networkid.UserLoginID, returning networkid.UserID as expected.

bridges/ai/client.go (5)

1-1: Consistent package/API surface migration.

The ai package rename and agentremote import swap are internally consistent with the rest of this file.

Also applies to: 28-28


338-338: Approval flow wiring looks consistent.

The new agentremote.ApprovalFlow setup preserves the same room, sender, notice, and background-context hooks, so this migration reads cleanly.

Also applies to: 407-424


1099-1102: Good helper consolidation for chat info.

Delegating to agentremote.BuildChatInfoWithFallback keeps the fallback logic centralized instead of re-encoding it here.


2338-2340: Logger context migration looks clean.

Routing through agentremote.LoggerFromContext keeps logging behavior aligned with the shared package.


2461-2469: Shared metadata helpers are applied consistently.

Using agentremote.MatrixMessageID and agentremote.BaseMessageMetadata here keeps debounced-message persistence aligned with the moved core types.

Copy link

@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: 1

Caution

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

⚠️ Outside diff range comments (1)
approval_flow.go (1)

498-500: ⚠️ Potential issue | 🟠 Major

Clean up the approval when prompt delivery fails.

The prompt is already registered at Lines 461-473, but this early return skips both bindPromptIDsLocked() and schedulePromptTimeout(). That leaves an orphaned approval that reactions can never resolve and callback-based flows may never finalize.

💡 Minimal fix
 eventID, msgID, err := f.send(ctx, portal, converted)
 if err != nil {
-	return
+	f.FinishResolved(strings.TrimSpace(params.ApprovalID), ApprovalDecisionPayload{
+		ApprovalID: strings.TrimSpace(params.ApprovalID),
+		Reason:     ApprovalReasonDeliveryError,
+	})
+	return
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@approval_flow.go` around lines 498 - 500, The early return on send failure
(eventID, msgID, err := f.send(...); if err != nil { return }) leaves the
prompt/approval registered but unbound and unscheduled; instead, when f.send
returns an error you must clean up the registration created earlier: call the
inverse/unregister routine (the code-path that removes the approval/prompt
registration or deletes the approval entry) before returning, and do not skip
bindPromptIDsLocked() / schedulePromptTimeout() semantics (i.e., either perform
proper binding/scheduling on success or call the cleanup function on failure).
Reference the send call and ensure you invoke the cleanup/unregister function
(or otherwise remove the registered prompt) so reactions/callback flows cannot
be left orphaned.
♻️ Duplicate comments (1)
bridges/ai/streaming_ui_tools.go (1)

53-66: ⚠️ Potential issue | 🟡 Minor

Add nil check for oc.approvalFlow before calling SendPrompt.

While other prerequisites are validated at lines 33-44, oc.approvalFlow could potentially be nil if not initialized. The AIClient struct shows approvalFlow as a pointer field that may not always be set.

🛡️ Proposed fix
 	if portal == nil || portal.MXID == "" || oc == nil || oc.UserLogin == nil || oc.UserLogin.UserMXID == "" {
+		return false
+	}
+	if oc.approvalFlow == nil {
 		if oc != nil {
-			log := oc.loggerForContext(ctx).Warn().
+			oc.loggerForContext(ctx).Warn().
 				Str("approval_id", approvalID).
-				Str("tool_call_id", toolCallID)
-			if portal != nil {
-				log = log.Stringer("room_id", portal.MXID)
-			}
-			log.Msg("Skipping tool approval prompt: missing portal or owner context")
+				Str("tool_call_id", toolCallID).
+				Msg("Skipping tool approval prompt: approval flow not initialized")
 		}
 		return false
 	}

Or add oc.approvalFlow == nil to the existing condition at line 33.

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

In `@bridges/ai/streaming_ui_tools.go` around lines 53 - 66, The call to
oc.approvalFlow.SendPrompt can panic if oc.approvalFlow is nil; add a nil check
for oc.approvalFlow before invoking SendPrompt (or include oc.approvalFlow ==
nil in the existing precondition at the earlier validation block) so that
SendPrompt is only called when oc.approvalFlow is non-nil; reference the
oc.approvalFlow field and the SendPrompt call in your change and ensure the
function returns false (or the same early-exit behavior) when approvalFlow is
missing.
🧹 Nitpick comments (3)
base_login_process.go (1)

17-18: Consider adding a doc comment for the type alias.

The alias works correctly, but a brief comment would clarify its purpose (e.g., backwards compatibility, deprecation intent, or preferred usage).

📝 Suggested documentation
+// LoginBase is an alias for BaseLoginProcess for backwards compatibility.
+// Deprecated: Use BaseLoginProcess directly.
 type LoginBase = BaseLoginProcess
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@base_login_process.go` around lines 17 - 18, Add a Go doc comment immediately
above the type alias LoginBase = BaseLoginProcess that explains why the alias
exists (for example: backwards compatibility, deprecation shim, or preferred
name), starting the comment with "LoginBase" so it follows GoDoc conventions;
update the comment if there are any migration steps or preferred usage notes to
help future maintainers locate and understand the alias's intent.
bridges/ai/remote_message_test.go (1)

50-71: Consider adding assertions on the converted Parts content.

The test verifies that Parts is non-empty and that metadata.Body is backfilled, but doesn't validate the actual content or type of the parts returned. Adding assertions for the expected part type and content (e.g., verifying the text content matches "hello world" or FormattedContent) would improve confidence in the conversion logic.

💡 Suggested enhancement
 	if converted == nil || len(converted.Parts) == 0 {
 		t.Fatalf("expected converted message parts, got %#v", converted)
 	}
+	// Consider adding assertions on part content, e.g.:
+	// textPart, ok := converted.Parts[0].(*bridgev2.ConvertedMessagePart)
+	// if !ok { t.Fatalf("expected text part type") }
+	// Verify text content matches expected value
 	if meta.Body != "hello world" {
 		t.Fatalf("expected metadata body to be backfilled from content, got %q", meta.Body)
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/remote_message_test.go` around lines 50 - 71, Add assertions in
TestOpenAIRemoteMessageConvertMessage to validate the actual part payload
returned by OpenAIRemoteMessage.ConvertMessage: after confirming converted.Parts
is non-empty, assert the first part's type (e.g., converted.Parts[0].Type) is
the expected text type and assert its content (e.g., converted.Parts[0].Content
or converted.Parts[0].Text) equals msg.FormattedContent when FormattedContent is
present, otherwise equals msg.Content; use MessageMetadata, OpenAIRemoteMessage,
and ConvertMessage to locate the logic to verify the part shape and values.
bridges/ai/approval_prompt_presentation_test.go (1)

36-55: Consider adding Details verification in edge case tests.

The happy path tests verify len(presentation.Details) > 0, but the edge case tests only check Title and AllowAlways. Consider verifying the Details field behavior for edge inputs to ensure consistent fallback handling.

♻️ Optional enhancement
 		t.Run(tc.name, func(t *testing.T) {
 			presentation := buildBuiltinApprovalPresentation("", "", tc.args)
 			if presentation.Title == "" {
 				t.Fatal("expected fallback title")
 			}
 			if !presentation.AllowAlways {
 				t.Fatal("expected allow-always to remain enabled")
 			}
+			// Details may be empty for edge cases, but shouldn't cause issues
+			_ = presentation.Details
 		})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/approval_prompt_presentation_test.go` around lines 36 - 55, The
edge-case test TestBuildBuiltinApprovalPresentation_EdgeCases currently only
asserts Title and AllowAlways; add a check that presentation.Details falls back
consistently by asserting len(presentation.Details) > 0 (or that Details equals
the expected fallback string/structure) for both nil and empty args; update the
subtests inside the t.Run loop that call buildBuiltinApprovalPresentation to
include this Details verification so edge inputs get the same Details behavior
as the happy-path tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@approval_flow.go`:
- Around line 196-210: ResolveExternal currently mirrors the remote reaction
then directly calls FinishResolved, skipping the internal delivery path that
notifies channel-based waiters; change ResolveExternal to route the decision
through the existing delivery/resolution flow (call DeliverDecision(ctx,
approvalID, decision) or Resolve(ctx, approvalID, decision) after
mirrorRemoteDecisionReaction) so waiters are notified, and avoid calling
FinishResolved directly if DeliverDecision/Resolve already finalizes the
approval; keep promptRegistration and mirrorRemoteDecisionReaction calls intact.

---

Outside diff comments:
In `@approval_flow.go`:
- Around line 498-500: The early return on send failure (eventID, msgID, err :=
f.send(...); if err != nil { return }) leaves the prompt/approval registered but
unbound and unscheduled; instead, when f.send returns an error you must clean up
the registration created earlier: call the inverse/unregister routine (the
code-path that removes the approval/prompt registration or deletes the approval
entry) before returning, and do not skip bindPromptIDsLocked() /
schedulePromptTimeout() semantics (i.e., either perform proper
binding/scheduling on success or call the cleanup function on failure).
Reference the send call and ensure you invoke the cleanup/unregister function
(or otherwise remove the registered prompt) so reactions/callback flows cannot
be left orphaned.

---

Duplicate comments:
In `@bridges/ai/streaming_ui_tools.go`:
- Around line 53-66: The call to oc.approvalFlow.SendPrompt can panic if
oc.approvalFlow is nil; add a nil check for oc.approvalFlow before invoking
SendPrompt (or include oc.approvalFlow == nil in the existing precondition at
the earlier validation block) so that SendPrompt is only called when
oc.approvalFlow is non-nil; reference the oc.approvalFlow field and the
SendPrompt call in your change and ensure the function returns false (or the
same early-exit behavior) when approvalFlow is missing.

---

Nitpick comments:
In `@base_login_process.go`:
- Around line 17-18: Add a Go doc comment immediately above the type alias
LoginBase = BaseLoginProcess that explains why the alias exists (for example:
backwards compatibility, deprecation shim, or preferred name), starting the
comment with "LoginBase" so it follows GoDoc conventions; update the comment if
there are any migration steps or preferred usage notes to help future
maintainers locate and understand the alias's intent.

In `@bridges/ai/approval_prompt_presentation_test.go`:
- Around line 36-55: The edge-case test
TestBuildBuiltinApprovalPresentation_EdgeCases currently only asserts Title and
AllowAlways; add a check that presentation.Details falls back consistently by
asserting len(presentation.Details) > 0 (or that Details equals the expected
fallback string/structure) for both nil and empty args; update the subtests
inside the t.Run loop that call buildBuiltinApprovalPresentation to include this
Details verification so edge inputs get the same Details behavior as the
happy-path tests.

In `@bridges/ai/remote_message_test.go`:
- Around line 50-71: Add assertions in TestOpenAIRemoteMessageConvertMessage to
validate the actual part payload returned by OpenAIRemoteMessage.ConvertMessage:
after confirming converted.Parts is non-empty, assert the first part's type
(e.g., converted.Parts[0].Type) is the expected text type and assert its content
(e.g., converted.Parts[0].Content or converted.Parts[0].Text) equals
msg.FormattedContent when FormattedContent is present, otherwise equals
msg.Content; use MessageMetadata, OpenAIRemoteMessage, and ConvertMessage to
locate the logic to verify the part shape and values.
🪄 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: 33a6a223-7812-428e-9a98-399c1574a200

📥 Commits

Reviewing files that changed from the base of the PR and between f900e74 and 3913e8f.

📒 Files selected for processing (23)
  • approval_flow.go
  • approval_manager.go
  • approval_prompt.go
  • base_login_process.go
  • base_reaction_handler.go
  • bridges/ai/agentstore.go
  • bridges/ai/approval_prompt_presentation_test.go
  • bridges/ai/chat.go
  • bridges/ai/client.go
  • bridges/ai/connector.go
  • bridges/ai/constructors.go
  • bridges/ai/identifiers.go
  • bridges/ai/msgconv/to_matrix_test.go
  • bridges/ai/remote_message_test.go
  • bridges/ai/response_finalization.go
  • bridges/ai/stream_events.go
  • bridges/ai/streaming_error_handling.go
  • bridges/ai/streaming_output_handlers.go
  • bridges/ai/streaming_responses_api.go
  • bridges/ai/streaming_state.go
  • bridges/ai/streaming_ui_tools.go
  • bridges/ai/subagent_spawn.go
  • bridges/ai/tool_approvals.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • approval_manager.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). (2)
  • GitHub Check: build-docker
  • GitHub Check: build-docker
🧰 Additional context used
🧬 Code graph analysis (9)
base_reaction_handler.go (2)
bridges/opencode/api/types.go (1)
  • Event (206-209)
bridges/opencode/bridge.go (1)
  • Bridge (71-74)
bridges/ai/subagent_spawn.go (2)
bridges/openclaw/metadata.go (1)
  • MessageMetadata (87-111)
message_metadata.go (1)
  • BaseMessageMetadata (7-26)
bridges/ai/streaming_state.go (1)
turns/session.go (1)
  • StreamSession (65-86)
bridges/ai/streaming_ui_tools.go (3)
bridges/ai/client.go (1)
  • AIClient (265-346)
approval_prompt.go (2)
  • ApprovalPromptPresentation (46-50)
  • ApprovalPromptMessageParams (283-292)
approval_flow.go (1)
  • SendPromptParams (436-440)
bridges/ai/connector.go (3)
connector_builder.go (1)
  • ConnectorBase (32-35)
bridges/opencode/bridge.go (1)
  • Bridge (71-74)
pkg/runtime/pruning.go (2)
  • DefaultPruningConfig (61-93)
  • ApplyPruningDefaults (252-326)
approval_prompt.go (1)
pkg/matrixevents/matrixevents.go (1)
  • BeeperAIKey (34-34)
bridges/ai/stream_events.go (1)
turns/session.go (4)
  • StreamSession (65-86)
  • NewStreamSession (88-107)
  • StreamSessionParams (43-59)
  • EmitStreamEventWithSession (135-152)
bridges/ai/constructors.go (1)
bridges/ai/connector.go (1)
  • OpenAIConnector (34-42)
approval_flow.go (3)
approval_prompt.go (4)
  • ApprovalPromptRegistration (488-501)
  • ApprovalOption (31-39)
  • BuildApprovalResponsePromptMessage (369-429)
  • ApprovalResponsePromptMessageParams (294-303)
bridges/opencode/bridge.go (1)
  • Bridge (71-74)
bridges/opencode/api/types.go (1)
  • Timestamp (8-8)
🔇 Additional comments (37)
base_login_process.go (1)

1-1: LGTM!

Package rename to agentremote aligns with the broader refactoring effort.

base_reaction_handler.go (1)

38-52: Nice fix on the ghost-provisioning warning path.

Capturing and logging the best-effort guard failure makes FK-related provisioning issues diagnosable without changing the existing reaction flow.

bridges/ai/remote_message_test.go (2)

1-11: LGTM!

The package declaration and imports are clean and appropriate for the test file. All imported packages are utilized in the tests.


13-48: LGTM!

The test thoroughly covers accessor methods, interface compliance (line 38), and fallback logic for GetStreamOrder(). The interface type assertion at line 38 is a good compile-time check for interface satisfaction.

bridges/ai/subagent_spawn.go (2)

1-1: LGTM on package rename and import update.

The package rename from connector to ai and the import migration from bridgeadapter to agentremote align with the PR's refactoring objectives.

Also applies to: 16-16


343-350: No issues found. The MessageMetadata struct in bridges/ai/metadata.go correctly embeds agentremote.BaseMessageMetadata, and the initialization pattern in the code at lines 348-349 properly references this embedded field.

bridges/ai/msgconv/to_matrix_test.go (1)

12-141: Good test coverage for message conversion utilities.

The tests cover the key functions with appropriate assertions. Consider these optional improvements for enhanced robustness:

  1. Add t.Parallel() to tests that don't share state to improve test execution time.
  2. Consider adding error case tests for ConvertAIResponse (e.g., invalid inputs).
  3. Table-driven tests could reduce repetition for functions like RelatesToReplace and RelatesToThread if more edge cases are added later.
bridges/ai/response_finalization.go (5)

1-1: LGTM!

Package rename and import migration from bridgeadapter/streamtransport to agentremote/turns is consistent throughout.

Also applies to: 15-20


23-43: LGTM!

Clean implementation of Matrix event relation building. The three-case logic correctly handles thread roots (with fallback), simple replies, and no-relation scenarios per the Matrix spec.


63-106: LGTM!

Reply target handling is properly integrated. The function correctly uses buildReplyRelatesTo and conditionally attaches the relation to the event. Migration to agentremote types is consistent.


610-684: LGTM - addresses past review concerns.

The split logic now correctly operates on the original markdown source (line 614) before rendering, preserving formatting across split messages. Continuation chunks properly inherit state.replyTarget (line 682) to maintain thread/reply context. Migration to agentremote types (MatrixMessageID, RemoteEdit) is consistent.


50-58: The concern about markdown formatting loss is unfounded. The BuildContinuationMessage function in agentremote/helpers.go calls format.RenderMarkdown(body, true, true) internally, ensuring that continuation chunks preserve markdown formatting like bold, code blocks, and other styled text. The implementation correctly handles the raw markdown body passed from turns.SplitAtMarkdownBoundary and renders it appropriately before sending.

			> Likely an incorrect or invalid review comment.
bridges/ai/chat.go (3)

1-24: LGTM - Package rename and import migration.

The package rename from connector to ai and the import migration from bridgeadapter to agentremote are consistent with the PR's refactoring objectives.


913-923: LGTM - API migration to agentremote.BuildDMChatInfo.

The call to agentremote.BuildDMChatInfo with agentremote.DMChatInfoParams correctly migrates from the previous bridgeadapter implementation while preserving the same functional behavior.


988-991: LGTM - API migration to agentremote.BuildSystemNotice.

The migration to agentremote.BuildSystemNotice is consistent with the broader refactoring pattern.

bridges/ai/agentstore.go (2)

1-20: LGTM - Package rename and import migration.

The package rename to ai and import migration to agentremote are consistent with the PR objectives.


385-414: LGTM - NewEventID migration to agentremote.

The migration from bridgeadapter.NewEventID("internal") to agentremote.NewEventID("internal") preserves the same functionality for generating internal event IDs used in the command execution context.

bridges/ai/identifiers.go (4)

1-18: LGTM - Package rename and import migration.

The package rename to ai and import migration to agentremote are consistent with the broader refactoring.


135-137: LGTM - humanUserID migration.

The migration to agentremote.HumanUserID provides a standardized way to construct human user IDs across the codebase.


173-179: LGTM - portalMeta migration to EnsurePortalMetadata.

Using the generic agentremote.EnsurePortalMetadata[PortalMetadata] helper provides type-safe metadata access with automatic initialization.


216-218: LGTM - loginMetadata migration to EnsureLoginMetadata.

The migration to agentremote.EnsureLoginMetadata[UserLoginMetadata] is consistent with the portal metadata pattern.

bridges/ai/streaming_responses_api.go (2)

1-22: LGTM - Package rename and import migration.

The package rename to ai and addition of agentremote import are consistent with the PR objectives.


513-533: LGTM - Improved timeout handling and UI emission ordering.

The changes enhance the approval flow in two ways:

  1. Assigns agentremote.ApprovalReasonTimeout when the approval wait times out without a specific reason
  2. Emits the UI approval response before recording it for proper real-time UI updates

The decision reassignment pattern at lines 516-518 correctly replaces the decision struct only when timeout conditions are met (wait returns ok=false and no reason was provided).

bridges/ai/streaming_ui_tools.go (1)

33-44: LGTM - Proper prerequisite validation.

Good defensive programming to validate portal, portal.MXID, oc.UserLogin, and oc.UserLogin.UserMXID before proceeding. The logging provides useful context when validation fails.

bridges/ai/approval_prompt_presentation_test.go (1)

1-77: LGTM - Good test coverage for approval presentation builders.

The tests cover both happy path and edge cases effectively:

  • Validates AllowAlways is consistently true across all scenarios
  • Ensures fallback Title is provided for empty/nil inputs
  • Tests both builtin and MCP presentation builders
bridges/ai/streaming_output_handlers.go (4)

1-18: LGTM - Package rename and import migration.

The package rename to ai and import migration to agentremote are consistent with the PR objectives.


62-63: LGTM - Updated EnsureUIToolInputStart call.

The updated call signature includes additional parameters for provider execution status, display title, and optional metadata.


204-223: LGTM - MCP approval presentation integration.

Good addition of buildMCPApprovalPresentation to generate rich UI context for MCP tool approvals. The Presentation field is properly passed through to the approval registration.


237-261: LGTM - Robust error handling for approval delivery failures.

Excellent defensive programming:

  1. Checks return value of emitUIToolApprovalRequest
  2. On failure, resolves the approval with ApprovalReasonDeliveryError
  3. Cleans up state (delete(state.pendingMcpApprovalsSeen, approvalID))
  4. Emits UI error and logs the failure

The auto-approval path at lines 252-260 correctly uses agentremote.ApprovalDecisionPayload with proper error handling.

bridges/ai/connector.go (5)

19-31: LGTM - Well-defined constants and interface assertions.

Good practice to:

  1. Define named constants for default values rather than magic numbers
  2. Use compile-time interface assertions to ensure OpenAIConnector satisfies required interfaces

51-63: LGTM - Sensible runtime defaults.

The applyRuntimeDefaults method appropriately:

  1. Sets a 6-hour model cache duration if unspecified
  2. Defaults command prefix to !ai
  3. Applies pruning defaults using airuntime.ApplyPruningDefaults or creates default config

75-83: LGTM - Ghost ID validation logic.

The ValidateUserID method correctly:

  1. Parses model ghost IDs and validates against the manifest
  2. Parses agent ghost IDs and validates with isValidAgentID
  3. Returns false for unrecognized formats

98-112: LGTM - Login flow creation with proper validation.

The createLogin method validates the flow ID against available flows before creating the login process, preventing invalid flow IDs from being accepted.


33-42: The clients map is properly initialized via agentremote.EnsureClientMap() in the connector's Init phase.

The review comment incorrectly states that the clients map is "declared but not initialized." However, line 30 of constructors.go calls agentremote.EnsureClientMap(&oc.clientsMu, &oc.clients) during the Init callback of the connector setup. This initializes the map before any methods can access it. Additionally, all accesses to the map in login_loaders.go are properly synchronized with the clientsMu mutex, preventing any nil map panics.

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

106-144: Nice lifecycle cleanup here.

Finishing the approval in both the timeout/cancel branch and the success branch keeps the prompt store and pending store aligned with the actual decision state.

bridges/ai/streaming_error_handling.go (1)

24-28: Good target classification here.

Treating edit targets, cached initialEventID, and persisted networkMessageID as non-fallback cases prevents duplicate assistant turns after partial output was already sent.

bridges/ai/stream_events.go (1)

91-99: Receiver normalization looks correct.

Falling back to oc.UserLogin.ID before turns.ResolveTargetEventIDFromDB() preserves edit targeting for receiver-less portals.

Copy link

@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.

Caution

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

⚠️ Outside diff range comments (1)
approval_flow.go (1)

463-523: ⚠️ Potential issue | 🟠 Major

Replacing a prompt leaves the old timeout armed.

SendPrompt() can overwrite the registration for an existing approvalID, but schedulePromptTimeout() just starts another timer tied to that same approval. If the new prompt extends ExpiresAt, the first timer still fires and times out the replacement at the old deadline. You need to cancel or version the previous timeout when rebinding a prompt.

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

In `@approval_flow.go` around lines 463 - 523, The new SendPrompt registration can
replace an existing prompt without cancelling its existing timeout; update the
prompt lifecycle to either store and cancel the previous timer or use a version
token so only the latest prompt's timeout takes effect. Specifically, when
replacing a prompt in registerPromptLocked (or right after detecting
hadPrevPrompt in SendPrompt), stop/clear the previous prompt's timer (if you
store a *time.Timer or cancel func on the ApprovalPromptRegistration) or
increment a prompt version on bindPromptIDsLocked and have schedulePromptTimeout
attach that version to the scheduled closure and ignore stale versions when the
timer fires; ensure dropPromptLocked and the timeout handler also cancel/ignore
timers consistently.
♻️ Duplicate comments (1)
approval_flow.go (1)

196-211: ⚠️ Potential issue | 🟠 Major

External resolutions still bypass DeliverDecision.

ResolveExternal() now goes through Resolve(), but Resolve() only enqueues on p.ch. In callback-based flows (DeliverDecision != nil), nothing consumes that channel, so the external decision is lost and FinishResolved() immediately removes the pending entry. It also finalizes even when Resolve() returns an error. Route this path through the same delivery logic as HandleReaction() and only finalize after that succeeds.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@approval_flow.go`:
- Around line 463-523: The new SendPrompt registration can replace an existing
prompt without cancelling its existing timeout; update the prompt lifecycle to
either store and cancel the previous timer or use a version token so only the
latest prompt's timeout takes effect. Specifically, when replacing a prompt in
registerPromptLocked (or right after detecting hadPrevPrompt in SendPrompt),
stop/clear the previous prompt's timer (if you store a *time.Timer or cancel
func on the ApprovalPromptRegistration) or increment a prompt version on
bindPromptIDsLocked and have schedulePromptTimeout attach that version to the
scheduled closure and ignore stale versions when the timer fires; ensure
dropPromptLocked and the timeout handler also cancel/ignore timers consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e8dd0b95-2405-4317-8a8e-f6f446f44338

📥 Commits

Reviewing files that changed from the base of the PR and between 3913e8f and 6ec022c.

📒 Files selected for processing (6)
  • approval_flow.go
  • approval_flow_test.go
  • base_login_process.go
  • bridges/ai/remote_message_test.go
  • bridges/ai/streaming_ui_tools.go
  • bridges/ai/streaming_ui_tools_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • bridges/ai/remote_message_test.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). (3)
  • GitHub Check: build-docker
  • GitHub Check: Lint
  • GitHub Check: build-docker
🧰 Additional context used
🧬 Code graph analysis (2)
bridges/ai/streaming_ui_tools_test.go (2)
bridges/ai/client.go (1)
  • AIClient (265-346)
approval_prompt.go (1)
  • ApprovalPromptPresentation (46-50)
bridges/ai/streaming_ui_tools.go (3)
bridges/ai/client.go (1)
  • AIClient (265-346)
approval_prompt.go (2)
  • ApprovalPromptPresentation (46-50)
  • ApprovalPromptMessageParams (283-292)
approval_flow.go (1)
  • SendPromptParams (437-441)

Copy link

@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.

🧹 Nitpick comments (1)
approval_flow.go (1)

278-288: Consider: Timeout timer race condition.

When timer.C fires in Wait, no finalization is triggered. The separately scheduled timeout goroutine (schedulePromptTimeout) will eventually handle it, but there's a window where the caller believes the approval timed out while it's still pending internally. This is likely acceptable since the scheduled timeout should fire nearly simultaneously, but worth noting.

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

In `@approval_flow.go` around lines 278 - 288, In Wait, when the local timer fires
(case <-timer.C) you must invoke the same finalization/timeout path used by
schedulePromptTimeout so the internal state and external return value stay
consistent; update the case <-timer.C branch in Wait to call the shared
timeout/finalize function (the same handler invoked by schedulePromptTimeout) or
send the same signal/message that schedulePromptTimeout uses, then return
zero,false—this ensures the caller’s timeout and the internal scheduled timeout
are synchronized; reference the Wait method, p.ch, timer.C, and
schedulePromptTimeout to locate and wire up the shared finalization path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@approval_flow.go`:
- Around line 278-288: In Wait, when the local timer fires (case <-timer.C) you
must invoke the same finalization/timeout path used by schedulePromptTimeout so
the internal state and external return value stay consistent; update the case
<-timer.C branch in Wait to call the shared timeout/finalize function (the same
handler invoked by schedulePromptTimeout) or send the same signal/message that
schedulePromptTimeout uses, then return zero,false—this ensures the caller’s
timeout and the internal scheduled timeout are synchronized; reference the Wait
method, p.ch, timer.C, and schedulePromptTimeout to locate and wire up the
shared finalization path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ffac9dea-fb48-48b9-bc80-58008f28d417

📥 Commits

Reviewing files that changed from the base of the PR and between 6ec022c and 9adcecf.

📒 Files selected for processing (3)
  • approval_flow.go
  • approval_flow_test.go
  • approval_prompt.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • approval_flow_test.go
📜 Review details
🧰 Additional context used
🧬 Code graph analysis (1)
approval_prompt.go (1)
pkg/matrixevents/matrixevents.go (1)
  • BeeperAIKey (34-34)
🔇 Additional comments (26)
approval_flow.go (12)

1-16: LGTM!

Package rename to agentremote and imports look correct. The turns package import aligns with the new edit functionality.


57-92: LGTM!

The done channel addition to Pending enables clean cancellation of timeout goroutines. Test hooks follow a reasonable pattern for dependency injection during testing.


140-148: LGTM!

The done channel is properly initialized for new pending approvals.


197-215: LGTM!

ResolveExternal now properly routes the decision through Resolve() before calling FinishResolved(), addressing the previous review concern about channel-based waiters not being notified.


235-257: LGTM!

The Resolve method correctly handles expiration by calling finishTimedOutApproval and cancels pending timeouts on successful delivery.


335-371: LGTM!

The PromptVersion tracking is a good approach to prevent stale timeout handlers from finalizing newer prompts. The copy semantics in promptRegistration ensure thread safety.


459-535: LGTM!

The SendPrompt flow properly preserves the previous prompt for restoration on failure, and correctly uses promptVersion to ensure timeout handlers are tied to specific prompt instances.


575-604: LGTM!

The resolved flag correctly gates finalization, ensuring FinishResolved is only called after successful decision delivery in both callback-based and channel-based flows.


691-716: LGTM!

The timeout scheduling correctly captures promptVersion to prevent stale handlers, and the done channel enables clean cancellation when approvals resolve before timeout.


819-881: LGTM!

The finalizeWithPromptVersion method correctly handles:

  • Version validation to prevent stale timeouts from affecting newer prompts
  • Double-close protection on the done channel
  • Background goroutine for I/O-bound cleanup operations

This addresses the previous review concern about timed-out approvals leaving dangling entries.


770-813: LGTM!

The mirrorRemoteDecisionReaction method correctly translates decisions to Matrix reactions, ensuring the owner ghost exists before queuing the reaction event.


725-739: LGTM!

The double-close protection pattern using select with default is correct for safely closing channels that may already be closed.

approval_prompt.go (14)

1-29: LGTM!

Package declaration, imports, and constants are well-defined. The reaction keys and reject reasons provide clear semantic meanings.


31-51: LGTM!

The data types are well-structured with appropriate JSON tags for serialization.


52-93: LGTM!

The detailKey struct approach correctly preserves the original map key for value lookup while using the trimmed key for display labels, addressing the previous review concern.


95-149: LGTM!

Comprehensive type handling for human-readable value summaries with appropriate truncation for long values.


151-179: LGTM!

The ApprovalOption methods provide clean accessors for decision reasons and reaction keys.


181-217: LGTM!

The options builder correctly constructs the approval option set based on the allowAlways flag.


219-281: LGTM!

The body builder functions compose well to create structured approval prompt and response messages.


313-367: LGTM!

The BuildApprovalPromptMessage function correctly normalizes and returns options in the resulting message, addressing the previous review concern.


369-429: LGTM!

The BuildApprovalResponsePromptMessage function correctly includes normalized options in the returned message, addressing the previous review concern.


431-464: LGTM!

Clean metadata construction with proper handling of optional fields.


488-511: LGTM!

The registration and match types are comprehensive for tracking prompt state and matching reactions.


566-623: LGTM!

Thorough normalization with proper fallbacks ensures valid options and presentation data.


625-636: LGTM!

The nil map guard is correctly implemented, addressing the previous review concern about potential panics.


638-656: LGTM!

The helper functions provide clean mappings for decision strings and reaction key normalization using the variationselector library.

batuhan added 7 commits March 15, 2026 13:39
Consolidate and refactor agent-loop tooling and steering logic: move tool-selection helpers into agent_loop_request_builders.go (add filterEnabledTools and selectedStreamingToolDescriptors), remove the separate streaming_request_tools.go file, and relocate steering message builders from agent_loop_steering.go into pending_queue.go and add streamingState helpers (addPendingSteeringPrompts, consumePendingSteeringPrompts). Simplify continuation checks by inlining pending-function/approval checks in streaming_responses_api.go and adjust ContinueAgentLoop to append incoming messages to the adapter state. Also remove unused helpers: agent_loop_steering.go, streaming_request_tools.go, store/scope.go, store/system_events.go; drop buildThreadSessionParams in codex client and closeRPCLocked in codex login. These changes consolidate related functionality, reduce indirection, and clean up dead code.
Multiple fixes and refactors across the AI bridge:

- Responses API: omit MaxOutputTokens when unset and map reasoning effort using reasoningEffortMap.
- Safety checks: guard findModelInfo against nil login metadata; ensure NewAIConnector initializes client cache map.
- Media understanding: extract resolveOpenRouterMediaConfig to centralize OpenRouter config resolution and use it from generateWithOpenRouter; add tests for config overrides and auth header handling.
- Provider validation: OpenAIProvider.GenerateStream now rejects unsupported Responses prompt context types.
- Portal/materialization: only send welcome if portal was created by EnsurePortalLifecycle.
- Streaming: centralize stream-step error handling; use turn ID consistently when creating streaming turns; make metadata/persistence robust when turn is nil.
- Streaming lifecycle: treat response.completed as a no-op state.
- Tool approvals: avoid nil derefs by checking approvalFlow, finish resolved decisions correctly on timeout, and ensure builtin tool checks fail closed if turn is missing; add tests for approval flows and cancellation.
- Tool execution: parseToolArgs preserves non-object JSON, pass raw JSON to integrations, enforce owner-only tool restrictions early, and ensure integrated handlers receive the correct arguments; add tests covering these behaviors.

Also adds multiple unit tests covering the above fixes and behaviors.
Call FinalizeAgentLoop on both normal and error exits while avoiding double-finalization when a turn is already completed. Added finalizeAgentLoopExit helper to centralize finalize logic and added a guard in streaming_chat_completions.FinalizeAgentLoop to return early if state is nil or already completed. Updated tests to expect finalize on error/context-length exits (renamed tests accordingly) and removed an obsolete responses-agent-loop params test. Also fixed a nil deref in media_understanding_runner by checking oc.UserLogin.User before reading MXID.
Replace legacy AI SDK UIMessage/canonical prompt schema with canonical TurnData snapshots across the codebase. Added sdk/turn_snapshot.go and BuildTurnSnapshot/SnapshotFromTurnData utilities, migrated metadata to use CanonicalTurnSchema/CanonicalTurnData, and removed old canonical UI message/prompt encoding paths. Renamed and simplified prompt helper APIs (canonicalPromptMessages -> promptMessagesFromMetadata, canonicalPromptTail -> promptTail, setCanonicalPromptMessages -> setCanonicalTurnDataFromPromptMessages), normalized turn part types (dynamic-tool -> tool), and updated bridges, openclaw/opencode/codex handlers, streaming persistence, and tests to build and consume TurnSnapshot/TurnData. This consolidates canonical representation and streamlines prompt/turn projections.
Drop the separate CanonicalTurnSchema field and related constant checks across the codebase, and treat the presence of CanonicalTurnData as the canonical indicator. Updated BaseMessageMetadata, AssistantMetadataParams and BuildAssistantBaseMetadata to remove the schema field; removed CanonicalTurnDataSchemaV1 constant from the SDK; adjusted logic in turn_data, streaming persistence, bridges (ai, codex, openclaw, opencode), and tests to no longer set or check CanonicalTurnSchema. Also applied minor formatting/import reordering. This simplifies canonical turn handling (presence of CanonicalTurnData is now authoritative); callers relying on the removed schema field should be updated.
Replace usages of SnapshotCanonicalUIMessage and UICanonicalMessage with SnapshotUIMessage and UIMessage across the codebase. Rename helper methods (e.g. currentCanonicalUIMessage -> currentUIMessage), update variable names and tests, and adjust callers in bridges (ai, codex, openclaw, opencode), pkg/shared/streamui, and sdk/turn. Also prune unused canonical-specific helpers/imports in canonical_extract.go. These changes simplify naming and consolidate the UI message projection API.
Ensure procCancel() is called in the error path after attempting to create the RPC. This cancels the spawned process/context when RPC initialization fails, preventing resource or goroutine leaks.
Copy link

@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 (6)
bridges/ai/handlematrix.go (2)

667-734: ⚠️ Potential issue | 🟠 Major

Audio/video path is short-circuited before capability/fallback handling.

At Line 671, audio/video returns UnsupportedMessageStatus before reaching the Line 677+ capability branch. This makes the audio fallback block at Lines 707-727 unreachable and can regress handling when understanding is empty/transiently unavailable.

Suggested fix
-	if msgType == event.MsgAudio || msgType == event.MsgVideo {
-		if understanding != nil && strings.TrimSpace(understanding.Body) != "" {
-			return dispatchTextOnly(understanding.Body)
-		}
-		return nil, agentremote.UnsupportedMessageStatus(fmt.Errorf(
-			"%s messages must be preprocessed into text before generation; configure media understanding or upload a transcript",
-			msgType,
-		))
-	}
+	if (msgType == event.MsgAudio || msgType == event.MsgVideo) &&
+		understanding != nil && strings.TrimSpace(understanding.Body) != "" {
+		return dispatchTextOnly(understanding.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 667 - 734, The audio/video branch
currently short-circuits before the supportsMedia fallback logic, making the
audio fallback in dispatchMediaUnderstandingFallback unreachable; move or modify
the initial msgType == event.MsgAudio || msgType == event.MsgVideo check so it
only short-circuits when the model actually supports media (e.g., add a
supportsMedia check to that if or relocate the block below the supportsMedia
handling), ensuring that when supportsMedia == false the code can reach the
dispatchMediaUnderstandingFallback logic (resolveVisionModelForImage,
resolveModelForCapability, dispatchMediaUnderstandingFallback,
analyzeAudioWithModel) and only return UnsupportedMessageStatus after those
fallback attempts (using oc.effectiveModel and config.capabilityName as
currently done).

1078-1089: ⚠️ Potential issue | 🟡 Minor

Capture and verify QueueRemoteEvent result before logging.

At line 1078, the result is ignored while lines 1086-1089 unconditionally log success. This diverges from the established pattern used just 66 lines earlier at line 1012 (same file) and elsewhere (helpers.go, portal_send.go) where the result is checked for success before proceeding. If queueing fails, the log message will be misleading.

Suggested fix
-	oc.UserLogin.QueueRemoteEvent(&agentremote.RemoteReactionRemove{
+	result := oc.UserLogin.QueueRemoteEvent(&agentremote.RemoteReactionRemove{
 		Portal:        portal.PortalKey,
 		Sender:        sender,
 		TargetMessage: entry.targetNetworkID,
 		EmojiID:       networkid.EmojiID(entry.emoji),
 		LogKey:        "ai_reaction_remove_target",
 	})
+	if !result.Success {
+		oc.loggerForContext(ctx).Warn().
+			Stringer("source_event", sourceEventID).
+			Str("emoji", entry.emoji).
+			Msg("Failed to queue ack reaction removal")
+		return
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/handlematrix.go` around lines 1078 - 1089, Capture the return
error from oc.UserLogin.QueueRemoteEvent when sending the
agentremote.RemoteReactionRemove and only log the "Queued ack reaction removal"
debug message via oc.loggerForContext(ctx) after verifying the call succeeded;
if it fails, log an error (including the err and relevant context like
sourceEventID, entry.emoji, portal.PortalKey, entry.targetNetworkID) and avoid
the success message. Ensure you reference QueueRemoteEvent,
RemoteReactionRemove, and loggerForContext(ctx) when locating and updating the
code.
bridges/ai/handleai.go (2)

566-573: ⚠️ Potential issue | 🔴 Critical

setRoomName can panic and still report success.

portalMeta(portal) is nullable elsewhere in this file (Line 349), but Lines 567-569 write through it unconditionally. Even when metadata exists, the save branch only logs portal.Save failures, so callers like maybeGenerateTitle() will think the rename succeeded while TitleGenerated was never persisted.

🔧 Suggested hardening
 	// Update portal metadata
 	meta := portalMeta(portal)
+	if meta == nil {
+		return errors.New("portal metadata is missing")
+	}
 	meta.Title = name
 	meta.TitleGenerated = true
 	if save {
 		if err := portal.Save(ctx); err != nil {
-			oc.loggerForContext(ctx).Warn().Err(err).Msg("Failed to save portal after setting room name")
+			return fmt.Errorf("failed to save portal after setting room name: %w", err)
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/handleai.go` around lines 566 - 573, The code writes through a
potentially nil portal metadata value (portalMeta(portal)) and then only logs
Save failures, which can leave Title/TitleGenerated unpersisted and cause
panics; modify setRoomName to nil-check the result of portalMeta(portal) (create
and assign a new metadata struct if nil), set meta.Title and
meta.TitleGenerated, and ensure portal.Save(ctx) error is returned (not just
logged) so callers like maybeGenerateTitle can detect persistence failures
instead of assuming success; update callers to handle the returned error if
necessary.

270-289: ⚠️ Potential issue | 🔴 Critical

Guard missing portal metadata in the auto-greeting loop.

portalMeta(current) is treated as nullable on Line 271, but Line 287 dereferences it unconditionally. A room whose metadata has not been initialized yet will panic here before the greeting is persisted or sent.

🔒 Minimal fix
 			currentMeta := portalMeta(current)
-			if currentMeta != nil && currentMeta.AutoGreetingSent {
+			if currentMeta == nil {
+				oc.Log().Debug().Stringer("room_id", roomID).Msg("auto-greeting loop exiting: missing portal metadata")
+				return
+			}
+			if currentMeta.AutoGreetingSent {
 				oc.Log().Debug().Stringer("room_id", roomID).Msg("auto-greeting loop exiting: already sent")
 				return
 			}
@@
 			currentMeta.AutoGreetingSent = true
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/handleai.go` around lines 270 - 289, The code assumes
portalMeta(current) returns non-nil later but dereferences currentMeta at the
assignment currentMeta.AutoGreetingSent = true; fix by guarding/initializing
metadata before that line: after currentMeta := portalMeta(current) and the
earlier nil checks, if currentMeta == nil then allocate and attach a new
metadata struct to current (e.g., create a PortalMeta with default values and
assign it to current's metadata field or call the existing initializer), then
set currentMeta.AutoGreetingSent = true and call current.Save(bgCtx) as before
so the updated metadata is persisted; update references to currentMeta (used in
autoGreetingBlockReason, hasPortalMessages, etc.) to use the initialized object.
bridges/ai/client.go (1)

407-444: ⚠️ Potential issue | 🟠 Major

Avoid starting approvalFlow before provider init can fail.

agentremote.NewApprovalFlow() launches its reaper immediately. If initProviderForLogin() returns an error, newAIClient() exits without calling Close(), so each failed client initialization leaks a background goroutine.

🧹 Minimal fix
 	provider, err := initProviderForLogin(key, meta, connector, login, log)
 	if err != nil {
+		if oc.approvalFlow != nil {
+			oc.approvalFlow.Close()
+		}
 		return nil, err
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/client.go` around lines 407 - 444, The approvalFlow is started by
agentremote.NewApprovalFlow (oc.approvalFlow) before initProviderForLogin runs,
which leaks its reaper goroutine on provider init failure; fix by delaying
creation of oc.approvalFlow until after provider,err :=
initProviderForLogin(...) succeeds (i.e., move the agentremote.NewApprovalFlow
block to after the provider error check), or alternatively ensure you call
oc.approvalFlow.Close() in the error path of newAIClient right after
initProviderForLogin fails; update newAIClient to either create approvalFlow
only after initProviderForLogin returns nil or explicitly close the approvalFlow
on error to avoid leaking the reaper.
bridges/ai/response_retry.go (1)

372-391: ⚠️ Potential issue | 🔴 Critical

Remove the nested .PromptContext field access.

The code at line 373 incorrectly accesses promptContext.PromptContext. The PromptContext struct (defined in sdk/prompt_context.go:14) contains only SystemPrompt, DeveloperPrompt, and Messages fields—no .PromptContext field exists. Since promptContext is already of type PromptContext, pass it directly to the function:

if bridgesdk.HasUnsupportedResponsesPromptContext(promptContext) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/response_retry.go` around lines 372 - 391, The call in
selectAgentLoopRunFunc incorrectly accesses a non-existent nested field
promptContext.PromptContext; change the call to pass the PromptContext value
directly to bridgesdk.HasUnsupportedResponsesPromptContext by using
promptContext (the PromptContext parameter) rather than
promptContext.PromptContext so selectAgentLoopRunFunc, the PromptContext
parameter, and bridgesdk.HasUnsupportedResponsesPromptContext all use the
correct type.
🧹 Nitpick comments (8)
bridges/ai/media_understanding_runner_openai_test.go (1)

66-119: Add immutability assertions for input header maps.

This test validates merge outputs well, but it doesn’t verify whether cfg.Headers / entry.Headers were mutated in-place. Adding that check would prevent subtle regression in shared-map callers.

✅ Suggested test hardening
 func TestResolveOpenRouterMediaConfigUsesEntryOverrides(t *testing.T) {
@@
 	apiKey, baseURL, headers, pdfEngine, _, err := client.resolveOpenRouterMediaConfig(cfg, entry)
@@
 	if pdfEngine != "native" {
 		t.Fatalf("expected configured PDF engine, got %q", pdfEngine)
 	}
+	if cfg.Headers["HTTP-Referer"] != "" {
+		t.Fatalf("cfg headers mutated unexpectedly: %#v", cfg.Headers)
+	}
+	if entry.Headers["X-Config"] != "" {
+		t.Fatalf("entry headers mutated unexpectedly: %#v", entry.Headers)
+	}
 }
🤖 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 66 - 119,
Add assertions to ensure cfg.Headers and entry.Headers are not mutated by
resolveOpenRouterMediaConfig: capture deep copies of cfg.Headers and
entry.Headers before calling client.resolveOpenRouterMediaConfig (in
TestResolveOpenRouterMediaConfigUsesEntryOverrides), call the function, then
assert the originals remain equal to the saved copies; reference the cfg and
entry variables and the resolveOpenRouterMediaConfig call to locate where to add
these immutability checks.
bridges/ai/streaming_lifecycle_cluster_test.go (1)

80-80: Consider checking the type assertion result.

The type assertion message["metadata"].(map[string]any) ignores the boolean return value. If metadata is not the expected type, this will panic instead of producing a clear test failure message.

🔧 Suggested improvement
-	metadata, _ := message["metadata"].(map[string]any)
+	metadata, ok := message["metadata"].(map[string]any)
+	if !ok {
+		t.Fatalf("expected metadata to be map[string]any, got %T", message["metadata"])
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/streaming_lifecycle_cluster_test.go` at line 80, The type
assertion message["metadata"].(map[string]any) can panic if the value isn't the
expected type; change it to capture the boolean result (e.g., metadata, ok :=
message["metadata"].(map[string]any)) and if !ok fail the test with a clear
message (use t.Fatalf or your test helper like require.Fatalf) including the
actual type or value from message["metadata"] so the failure is informative;
update the test in streaming_lifecycle_cluster_test.go wherever "message" and
"metadata" are used.
bridges/ai/handlematrix.go (1)

267-277: Consider extracting a shared helper for user-message construction.

The same database.Message + MessageMetadata + canonical turn-data pattern is repeated across three paths. A small helper would reduce drift risk in future schema/API changes.

Also applies to: 611-622, 902-913

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

In `@bridges/ai/handlematrix.go` around lines 267 - 277, The code repeats
construction of a user database.Message with MessageMetadata and the
setCanonicalTurnDataFromPromptMessages call across multiple locations (e.g., the
userMessage creation block, usage of MessageMetadata,
setCanonicalTurnDataFromPromptMessages, and promptTail). Extract a small helper
function (e.g., NewUserMessage or buildUserMessage) that accepts the common
inputs (eventID/MXID, portal key, sender ID, body, event timestamp, and
promptContext) and returns the fully populated *database.Message with canonical
turn data already set; replace the repeated construction sites (including the
current userMessage creation and the occurrences around lines 611–622 and
902–913) with calls to that helper to centralize schema setup and reduce drift
risk.
bridges/ai/agent_loop_chat_tools.go (1)

17-20: Add a defensive nil guard for activeTools.

If this helper is called with a nil registry, it panics on Lookup. A small guard keeps agent-loop fallback paths safer.

Suggested fix
 func executeChatToolCallsSequentially(
 	keys []string,
 	activeTools *streamToolRegistry,
 	executeTool func(tool *activeToolCall, toolName, argsJSON string),
 	getSteeringMessages func() []string,
 ) ([]openai.ChatCompletionMessageToolCallUnionParam, []string) {
+	if activeTools == nil {
+		return nil, nil
+	}
 	toolCallParams := make([]openai.ChatCompletionMessageToolCallUnionParam, 0, len(keys))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/agent_loop_chat_tools.go` around lines 17 - 20, The loop calls
activeTools.Lookup and can panic if activeTools is nil; add a defensive nil
check before using it (e.g., at the top of the helper or immediately before the
for-loop) to skip lookup when activeTools == nil so the function safely
returns/continues instead of calling activeTools.Lookup; update the code paths
that reference activeTools.Lookup to first check activeTools != nil (referencing
the activeTools symbol and its Lookup method) and handle the nil case by
skipping lookup or returning an empty result as appropriate.
bridges/ai/streaming_responses_api.go (2)

400-410: Context-length error detection could be more robust.

The CLE detection on lines 404-408 uses simple string matching ("context_length" or "token"). This may miss some provider-specific error formats or false-positive on unrelated messages containing "token".

Consider using ParseContextLengthError for consistency with other error paths, if the error format allows:

if !isContinuation {
    if cle := ParseContextLengthError(fmt.Errorf("%s", streamEvent.Message)); cle != nil {
        return true, cle, nil
    }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/streaming_responses_api.go` around lines 400 - 410, Replace the
fragile substring checks in the "error" streamEvent case by calling
ParseContextLengthError to parse streamEvent.Message (only when !isContinuation)
and return the resulting *ContextLengthError if non-nil; specifically update the
error-handling block that currently constructs apiErr and checks
strings.Contains to instead call ParseContextLengthError(fmt.Errorf("%s",
streamEvent.Message)) and, if it returns a non-nil cle, return true, cle, nil,
otherwise proceed to call oc.finishStreamingWithFailure with apiErr as before.

189-194: Consider consolidating duplicate return logic.

Lines 190 and 193 have identical return expressions:

return state != nil && (len(state.pendingFunctionOutputs) > 0 || len(state.pendingMcpApprovals) > 0), nil, nil

The first is for done=true case, the second for the fallthrough. If done=true should exit without continuation, consider:

♻️ Suggested simplification
 	if done {
-		return state != nil && (len(state.pendingFunctionOutputs) > 0 || len(state.pendingMcpApprovals) > 0), nil, nil
+		// Stream completed - check for pending actions requiring continuation
 	}

-	return state != nil && (len(state.pendingFunctionOutputs) > 0 || len(state.pendingMcpApprovals) > 0), nil, nil
+	hasPending := state != nil && (len(state.pendingFunctionOutputs) > 0 || len(state.pendingMcpApprovals) > 0)
+	return hasPending, nil, nil
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/streaming_responses_api.go` around lines 189 - 194, The two
identical return statements using done, state, pendingFunctionOutputs and
pendingMcpApprovals should be consolidated: remove the duplicate and return once
after computing hasPending := state != nil && (len(state.pendingFunctionOutputs)
> 0 || len(state.pendingMcpApprovals) > 0); if the intended semantics are that
done=true should not continue, change the done branch to return (false, nil,
nil) instead of the pending value, otherwise simply compute hasPending and
return it once for both paths; update the logic around the done variable and use
the hasPending local variable to avoid repetition.
bridges/ai/streaming_chat_completions.go (2)

215-217: Trailing comment appears orphaned.

Lines 215-217 contain a comment about convertToResponsesInput but no corresponding function definition in this file. This appears to be documentation for a function defined elsewhere. Consider removing or relocating this comment.

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

In `@bridges/ai/streaming_chat_completions.go` around lines 215 - 217, The
trailing orphaned comment referencing convertToResponsesInput should be removed
or relocated: if the function convertToResponsesInput exists in another file,
move this documentation block to that function's declaration (or its unit) so
the comment sits next to the implementation; if no such function exists in the
repo, delete the comment entirely to avoid confusion and stale docs. Ensure any
notes about supported multimodal types (ResponseInputImageParam,
ResponseInputFileParam) and the audio fallback remark are preserved only where
convertToResponsesInput is implemented or in a relevant higher-level API doc.

172-190: Consider removing redundant state.completedAtMs check.

In FinalizeAgentLoop, the condition state.completedAtMs != 0 prevents re-finalization, but completeStreamingSuccess should be idempotent. If completeStreamingSuccess already handles this internally, consider simplifying:

if state == nil {
    return
}

However, if this is intentional to avoid double-logging, the current implementation is acceptable.

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

In `@bridges/ai/streaming_chat_completions.go` around lines 172 - 190, The guard
in FinalizeAgentLoop redundantly checks state.completedAtMs before calling
oc.completeStreamingSuccess; if oc.completeStreamingSuccess is already
idempotent, simplify the early return to only check for nil by changing the
condition to `if state == nil { return }` and remove the `state.completedAtMs !=
0` check so we don't duplicate re-finalization logic; ensure FinalizeAgentLoop
still calls oc.completeStreamingSuccess(ctx, a.log, portal, state, meta) and
preserves the subsequent logging block.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@approval_flow.go`:
- Around line 880-889: cancelPendingTimeout currently only calls p.closeDone()
leaving the entry in f.pending so the reaper can still mark it timed out; update
cancelPendingTimeout to remove the pending entry and related prompt references
so resolved approvals are no longer considered by the reaper: after trimming
approvalID and locking f.mu, if p := f.pending[approvalID] != nil call
p.closeDone(), delete(f.pending, approvalID), and also remove any entries in
f.promptsByApproval for that approvalID (and clear any ExpiresAt on the pending
struct if present) so the reaper's checks (pending/ExpiresAt and
promptsByApproval) will no longer see this approval.

In `@bridges/ai/agent_loop_request_builders.go`:
- Around line 95-97: The code currently skips setting params.Temperature when
settings.temperature is 0 because it checks "if settings.temperature > 0";
change this conditional to allow zero (e.g., "if settings.temperature >= 0" or
an explicit presence flag) so that params.Temperature =
openai.Float(settings.temperature) is assigned when the user explicitly sets
temperature = 0; update the check around settings.temperature and the assignment
to params.Temperature accordingly.

In `@bridges/ai/agent_loop_runtime.go`:
- Around line 27-34: The current loop marks send success before processing each
chunk; move the call to oc.markMessageSendSuccess(ctx, portal, evt, state) so it
runs only after handleEvent(current) completes successfully. Specifically, in
the loop that calls stream.Next(), call handleEvent(current) first, check its
results (done, cle, err) and return immediately if any indicate termination or
error, and only if handleEvent returned no error and no ContextLengthError then
evaluate shouldMarkSuccess and invoke oc.markMessageSendSuccess. Keep the same
variables (stream.Current, shouldMarkSuccess, handleEvent,
oc.markMessageSendSuccess) and preserve existing return behavior.

In `@bridges/ai/client_find_model_info_test.go`:
- Around line 5-10: The test TestFindModelInfoWithNilLoginMetadataDoesNotPanic
only exercises the empty-ID fast path, so update it to exercise the
nil-login-metadata lookup: construct an AIClient with a non-nil Login whose
Metadata field is explicitly nil (e.g., client.Login = &Login{Metadata: nil})
and call client.findModelInfo with a non-empty unknown model id (not ""), or
alternatively rename the test to indicate it only tests the empty-id case;
ensure the call uses the AIClient.findModelInfo method and asserts no
panic/returns nil for the unknown id.

In `@bridges/ai/constructors.go`:
- Around line 28-30: The InitConnector implementation is mutating the global
bridgev2.PortalEventBuffer (in InitConnector) which creates process-wide state
and ordering dependencies; remove the assignment to bridgev2.PortalEventBuffer
from InitConnector and instead initialize buffering at bootstrap or attach a
per-bridge setting (e.g., add/use a field or config on the Bridge struct) so the
AI connector only mutates its own bridge instance (oc.br) and does not change
global state; update callers to set the global default once during application
bootstrap or refactor consumers to read a bridge-level value rather than
bridgev2.PortalEventBuffer.

In `@bridges/ai/login_loaders_test.go`:
- Around line 13-19: The helper testUserLoginWithMeta stores a typed-nil when
assigning the *UserLoginMetadata param directly to the interface-backed Metadata
field on bridgev2.UserLogin -> database.UserLogin; instead, construct the
UserLogin value first and only set login.UserLogin.Metadata = meta when meta !=
nil so that passing nil results in an unassigned (true nil) Metadata rather than
a typed-nil; update the function testUserLoginWithMeta to build the UserLogin
object first and conditionally assign Metadata when meta is non-nil.

In `@bridges/ai/login_loaders.go`:
- Around line 58-68: The evictCachedClient function is calling
AIClient.Disconnect() while holding oc.clientsMu which can deadlock; change the
flow in evictCachedClient (and similar spots) to look up and remove the client
under oc.clientsMu (use oc.clients and oc.clientsMu), store the removed
*AIClient in a local variable, release the lock, and only then call
cached.Disconnect(); ensure you still check the expected bridgev2.NetworkAPI
equality before removal so behavior is unchanged.

In `@bridges/ai/media_understanding_runner.go`:
- Around line 720-727: The code must always take the dedicated OpenRouter path
when the media entry explicitly specifies OpenRouter; change the conditional
around oc.generateWithOpenRouter so it triggers whenever entryProvider ==
"openrouter" (remove the extra normalization/login-provider check that causes a
fallback to oc.provider.Generate), ensuring the call still passes capCfg and
entry; make the identical change in the other occurrence referenced (the block
around the 869-876 range) so explicit OpenRouter entries always use
generateWithOpenRouter and its overrides.

In `@bridges/ai/pending_queue.go`:
- Around line 215-218: The summary state (snapshot.droppedCount and
summaryLines) must be consumed when you attach a synthetic summary candidate so
it cannot be reissued every round; update the logic that builds the summary
candidate (the oc.takeQueueSummary call and the code that sets summary := "") to
atomically clear the summary bookkeeping after creating the candidate (either by
changing takeQueueSummary to return-and-clear or calling a new
oc.clearQueueSummary helper) while holding the same mutex used to snapshot the
queue so droppedCount and summaryLines are reset to zero/empty when you enqueue
the summary candidate.
- Around line 340-346: You currently call
takePendingQueueDispatchCandidate(roomID, true) which mutates the queue before
checking the queue mode; instead obtain a non-destructive snapshot of the queue
first (e.g., call the existing snapshot/getter for the pending queue or add a
non-mutating peek method) and run airuntime.ResolveQueueBehavior(snapshot.mode)
on that snapshot; only after confirming behavior.Followup is true should you
call takePendingQueueDispatchCandidate(roomID, true) to dequeue and return the
candidate; ensure you reference snapshot.mode, ResolveQueueBehavior and
takePendingQueueDispatchCandidate in your changes so the code no longer removes
items for non-followup modes like QueueModeSteer.

In `@bridges/ai/streaming_continuation.go`:
- Around line 41-47: The steering prompts are only persisted into
state.baseInput when state.baseInput already has items, causing steering to be
lost for subsequent stateless continuations; update the logic around
steerPrompts -> oc.buildSteeringInputItems(...) so that when steerInput is
non-empty you always append steerInput to both input and state.baseInput (remove
the conditional that requires len(state.baseInput) > 0), ensuring steerInput is
persisted across continuations; reference steerPrompts, buildSteeringInputItems,
steerInput, input, and state.baseInput to locate and modify the code.

In `@bridges/ai/streaming_responses_api.go`:
- Around line 381-396: state.turn may be nil when extracting images from
streamEvent.Response.Output, so avoid a nil dereference by reading turn ID into
a local variable (e.g., turnID := "" then if state.turn != nil { turnID =
state.turn.ID() }) and use that when appending to state.pendingImages (created
via generatedImage with itemID imgOutput.ID and imageB64 imgOutput.Result);
update the logging call to use the safe turnID if needed and ensure all
references to state.turn.ID() in this block are replaced with the guarded
turnID.

---

Outside diff comments:
In `@bridges/ai/client.go`:
- Around line 407-444: The approvalFlow is started by
agentremote.NewApprovalFlow (oc.approvalFlow) before initProviderForLogin runs,
which leaks its reaper goroutine on provider init failure; fix by delaying
creation of oc.approvalFlow until after provider,err :=
initProviderForLogin(...) succeeds (i.e., move the agentremote.NewApprovalFlow
block to after the provider error check), or alternatively ensure you call
oc.approvalFlow.Close() in the error path of newAIClient right after
initProviderForLogin fails; update newAIClient to either create approvalFlow
only after initProviderForLogin returns nil or explicitly close the approvalFlow
on error to avoid leaking the reaper.

In `@bridges/ai/handleai.go`:
- Around line 566-573: The code writes through a potentially nil portal metadata
value (portalMeta(portal)) and then only logs Save failures, which can leave
Title/TitleGenerated unpersisted and cause panics; modify setRoomName to
nil-check the result of portalMeta(portal) (create and assign a new metadata
struct if nil), set meta.Title and meta.TitleGenerated, and ensure
portal.Save(ctx) error is returned (not just logged) so callers like
maybeGenerateTitle can detect persistence failures instead of assuming success;
update callers to handle the returned error if necessary.
- Around line 270-289: The code assumes portalMeta(current) returns non-nil
later but dereferences currentMeta at the assignment
currentMeta.AutoGreetingSent = true; fix by guarding/initializing metadata
before that line: after currentMeta := portalMeta(current) and the earlier nil
checks, if currentMeta == nil then allocate and attach a new metadata struct to
current (e.g., create a PortalMeta with default values and assign it to
current's metadata field or call the existing initializer), then set
currentMeta.AutoGreetingSent = true and call current.Save(bgCtx) as before so
the updated metadata is persisted; update references to currentMeta (used in
autoGreetingBlockReason, hasPortalMessages, etc.) to use the initialized object.

In `@bridges/ai/handlematrix.go`:
- Around line 667-734: The audio/video branch currently short-circuits before
the supportsMedia fallback logic, making the audio fallback in
dispatchMediaUnderstandingFallback unreachable; move or modify the initial
msgType == event.MsgAudio || msgType == event.MsgVideo check so it only
short-circuits when the model actually supports media (e.g., add a supportsMedia
check to that if or relocate the block below the supportsMedia handling),
ensuring that when supportsMedia == false the code can reach the
dispatchMediaUnderstandingFallback logic (resolveVisionModelForImage,
resolveModelForCapability, dispatchMediaUnderstandingFallback,
analyzeAudioWithModel) and only return UnsupportedMessageStatus after those
fallback attempts (using oc.effectiveModel and config.capabilityName as
currently done).
- Around line 1078-1089: Capture the return error from
oc.UserLogin.QueueRemoteEvent when sending the agentremote.RemoteReactionRemove
and only log the "Queued ack reaction removal" debug message via
oc.loggerForContext(ctx) after verifying the call succeeded; if it fails, log an
error (including the err and relevant context like sourceEventID, entry.emoji,
portal.PortalKey, entry.targetNetworkID) and avoid the success message. Ensure
you reference QueueRemoteEvent, RemoteReactionRemove, and loggerForContext(ctx)
when locating and updating the code.

In `@bridges/ai/response_retry.go`:
- Around line 372-391: The call in selectAgentLoopRunFunc incorrectly accesses a
non-existent nested field promptContext.PromptContext; change the call to pass
the PromptContext value directly to
bridgesdk.HasUnsupportedResponsesPromptContext by using promptContext (the
PromptContext parameter) rather than promptContext.PromptContext so
selectAgentLoopRunFunc, the PromptContext parameter, and
bridgesdk.HasUnsupportedResponsesPromptContext all use the correct type.

---

Nitpick comments:
In `@bridges/ai/agent_loop_chat_tools.go`:
- Around line 17-20: The loop calls activeTools.Lookup and can panic if
activeTools is nil; add a defensive nil check before using it (e.g., at the top
of the helper or immediately before the for-loop) to skip lookup when
activeTools == nil so the function safely returns/continues instead of calling
activeTools.Lookup; update the code paths that reference activeTools.Lookup to
first check activeTools != nil (referencing the activeTools symbol and its
Lookup method) and handle the nil case by skipping lookup or returning an empty
result as appropriate.

In `@bridges/ai/handlematrix.go`:
- Around line 267-277: The code repeats construction of a user database.Message
with MessageMetadata and the setCanonicalTurnDataFromPromptMessages call across
multiple locations (e.g., the userMessage creation block, usage of
MessageMetadata, setCanonicalTurnDataFromPromptMessages, and promptTail).
Extract a small helper function (e.g., NewUserMessage or buildUserMessage) that
accepts the common inputs (eventID/MXID, portal key, sender ID, body, event
timestamp, and promptContext) and returns the fully populated *database.Message
with canonical turn data already set; replace the repeated construction sites
(including the current userMessage creation and the occurrences around lines
611–622 and 902–913) with calls to that helper to centralize schema setup and
reduce drift risk.

In `@bridges/ai/media_understanding_runner_openai_test.go`:
- Around line 66-119: Add assertions to ensure cfg.Headers and entry.Headers are
not mutated by resolveOpenRouterMediaConfig: capture deep copies of cfg.Headers
and entry.Headers before calling client.resolveOpenRouterMediaConfig (in
TestResolveOpenRouterMediaConfigUsesEntryOverrides), call the function, then
assert the originals remain equal to the saved copies; reference the cfg and
entry variables and the resolveOpenRouterMediaConfig call to locate where to add
these immutability checks.

In `@bridges/ai/streaming_chat_completions.go`:
- Around line 215-217: The trailing orphaned comment referencing
convertToResponsesInput should be removed or relocated: if the function
convertToResponsesInput exists in another file, move this documentation block to
that function's declaration (or its unit) so the comment sits next to the
implementation; if no such function exists in the repo, delete the comment
entirely to avoid confusion and stale docs. Ensure any notes about supported
multimodal types (ResponseInputImageParam, ResponseInputFileParam) and the audio
fallback remark are preserved only where convertToResponsesInput is implemented
or in a relevant higher-level API doc.
- Around line 172-190: The guard in FinalizeAgentLoop redundantly checks
state.completedAtMs before calling oc.completeStreamingSuccess; if
oc.completeStreamingSuccess is already idempotent, simplify the early return to
only check for nil by changing the condition to `if state == nil { return }` and
remove the `state.completedAtMs != 0` check so we don't duplicate
re-finalization logic; ensure FinalizeAgentLoop still calls
oc.completeStreamingSuccess(ctx, a.log, portal, state, meta) and preserves the
subsequent logging block.

In `@bridges/ai/streaming_lifecycle_cluster_test.go`:
- Line 80: The type assertion message["metadata"].(map[string]any) can panic if
the value isn't the expected type; change it to capture the boolean result
(e.g., metadata, ok := message["metadata"].(map[string]any)) and if !ok fail the
test with a clear message (use t.Fatalf or your test helper like require.Fatalf)
including the actual type or value from message["metadata"] so the failure is
informative; update the test in streaming_lifecycle_cluster_test.go wherever
"message" and "metadata" are used.

In `@bridges/ai/streaming_responses_api.go`:
- Around line 400-410: Replace the fragile substring checks in the "error"
streamEvent case by calling ParseContextLengthError to parse streamEvent.Message
(only when !isContinuation) and return the resulting *ContextLengthError if
non-nil; specifically update the error-handling block that currently constructs
apiErr and checks strings.Contains to instead call
ParseContextLengthError(fmt.Errorf("%s", streamEvent.Message)) and, if it
returns a non-nil cle, return true, cle, nil, otherwise proceed to call
oc.finishStreamingWithFailure with apiErr as before.
- Around line 189-194: The two identical return statements using done, state,
pendingFunctionOutputs and pendingMcpApprovals should be consolidated: remove
the duplicate and return once after computing hasPending := state != nil &&
(len(state.pendingFunctionOutputs) > 0 || len(state.pendingMcpApprovals) > 0);
if the intended semantics are that done=true should not continue, change the
done branch to return (false, nil, nil) instead of the pending value, otherwise
simply compute hasPending and return it once for both paths; update the logic
around the done variable and use the hasPending local variable to avoid
repetition.
🪄 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: c3c90ef5-5358-48df-bb0b-35635626e904

📥 Commits

Reviewing files that changed from the base of the PR and between 5acb348 and 710fd85.

📒 Files selected for processing (48)
  • approval_flow.go
  • bridges/ai/agent_loop_chat_tools.go
  • bridges/ai/agent_loop_chat_tools_test.go
  • bridges/ai/agent_loop_request_builders.go
  • bridges/ai/agent_loop_request_builders_test.go
  • bridges/ai/agent_loop_routing_test.go
  • bridges/ai/agent_loop_runtime.go
  • bridges/ai/agent_loop_steering_test.go
  • bridges/ai/agent_loop_test.go
  • bridges/ai/canonical_history.go
  • bridges/ai/canonical_prompt_messages.go
  • bridges/ai/canonical_user_messages.go
  • bridges/ai/client.go
  • bridges/ai/client_find_model_info_test.go
  • bridges/ai/constructors.go
  • bridges/ai/constructors_test.go
  • bridges/ai/handleai.go
  • bridges/ai/handlematrix.go
  • bridges/ai/heartbeat_execute.go
  • bridges/ai/identifiers.go
  • bridges/ai/internal_dispatch.go
  • bridges/ai/login_loaders.go
  • bridges/ai/login_loaders_test.go
  • bridges/ai/media_understanding_runner.go
  • bridges/ai/media_understanding_runner_openai_test.go
  • bridges/ai/pending_queue.go
  • bridges/ai/portal_materialize.go
  • bridges/ai/provider_openai_responses.go
  • bridges/ai/provider_openai_responses_test.go
  • bridges/ai/response_retry.go
  • bridges/ai/room_runs.go
  • bridges/ai/session_transcript_openclaw.go
  • bridges/ai/session_transcript_openclaw_test.go
  • bridges/ai/streaming_chat_completions.go
  • bridges/ai/streaming_continuation.go
  • bridges/ai/streaming_executor.go
  • bridges/ai/streaming_init.go
  • bridges/ai/streaming_lifecycle_cluster_test.go
  • bridges/ai/streaming_persistence.go
  • bridges/ai/streaming_response_lifecycle.go
  • bridges/ai/streaming_responses_api.go
  • bridges/ai/streaming_state.go
  • bridges/ai/streaming_ui_helpers.go
  • bridges/ai/subagent_announce.go
  • bridges/ai/subagent_spawn.go
  • bridges/ai/tool_approvals.go
  • bridges/ai/tool_approvals_helpers_test.go
  • bridges/ai/tool_approvals_test.go
✅ Files skipped from review due to trivial changes (1)
  • bridges/ai/agent_loop_routing_test.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • bridges/ai/subagent_announce.go
  • bridges/ai/heartbeat_execute.go
  • bridges/ai/canonical_history.go
  • bridges/ai/streaming_executor.go
  • bridges/ai/constructors_test.go
  • bridges/ai/provider_openai_responses.go
  • bridges/ai/room_runs.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). (3)
  • GitHub Check: build-docker
  • GitHub Check: Lint
  • GitHub Check: build-docker
🧰 Additional context used
🧬 Code graph analysis (28)
bridges/ai/login_loaders_test.go (1)
bridges/codex/metadata.go (1)
  • UserLoginMetadata (13-23)
bridges/ai/agent_loop_runtime.go (2)
bridges/opencode/api/types.go (1)
  • Event (206-209)
bridges/ai/errors.go (1)
  • ContextLengthError (50-54)
bridges/ai/agent_loop_chat_tools.go (1)
bridges/ai/metadata.go (1)
  • NewCallID (357-359)
bridges/ai/client_find_model_info_test.go (1)
bridges/ai/client.go (1)
  • AIClient (264-343)
bridges/ai/agent_loop_request_builders_test.go (5)
pkg/agents/prompt.go (1)
  • DefaultSystemPrompt (90-90)
pkg/fetch/types.go (1)
  • Provider (6-9)
bridges/ai/login.go (1)
  • ProviderOpenRouter (22-22)
bridges/ai/metadata.go (1)
  • ModelCache (18-22)
bridges/ai/identifiers.go (2)
  • ResolvedTarget (145-150)
  • ResolvedTargetModel (141-141)
bridges/ai/streaming_response_lifecycle.go (1)
sdk/types.go (1)
  • Message (29-44)
bridges/ai/identifiers.go (2)
identifier_helpers.go (1)
  • HumanUserID (24-26)
metadata_helpers.go (2)
  • EnsurePortalMetadata (28-33)
  • EnsureLoginMetadata (21-26)
bridges/ai/pending_queue.go (4)
bridges/ai/client.go (1)
  • AIClient (264-343)
pkg/runtime/queue_policy.go (1)
  • ResolveQueueBehavior (39-52)
bridges/opencode/api/types.go (1)
  • Event (206-209)
pkg/runtime/types.go (1)
  • QueueDropSummarize (87-87)
bridges/ai/streaming_lifecycle_cluster_test.go (4)
bridges/ai/client.go (1)
  • AIClient (264-343)
bridges/ai/errors.go (1)
  • PreDeltaError (61-63)
pkg/shared/streamui/recorder.go (1)
  • SnapshotUIMessage (212-217)
pkg/shared/streamui/emitter.go (1)
  • UIState (14-36)
bridges/ai/tool_approvals_helpers_test.go (4)
sdk/types.go (1)
  • ApprovalRequest (101-108)
cmd/internal/cliutil/state.go (1)
  • Metadata (12-23)
bridges/ai/tool_approvals.go (1)
  • ToolApprovalKindBuiltin (21-21)
approval_decision.go (2)
  • ApprovalReasonTimeout (14-14)
  • ApprovalReasonCancelled (16-16)
bridges/ai/provider_openai_responses_test.go (3)
bridges/ai/provider_openai.go (1)
  • OpenAIProvider (24-28)
bridges/ai/provider.go (1)
  • GenerateParams (21-29)
sdk/prompt_context.go (1)
  • UserPromptContext (20-27)
bridges/ai/media_understanding_runner.go (5)
sdk/prompt_context.go (3)
  • BuildDataURL (236-238)
  • UserPromptContext (20-27)
  • PromptContextHasBlockType (29-45)
bridges/ai/models_api.go (1)
  • ResolveAlias (6-8)
bridges/ai/provider.go (2)
  • GenerateResponse (32-38)
  • GenerateParams (21-29)
bridges/ai/integrations_config.go (2)
  • MediaUnderstandingConfig (311-324)
  • MediaUnderstandingModelConfig (280-297)
bridges/ai/client.go (1)
  • AIClient (264-343)
bridges/ai/client.go (5)
client_base.go (1)
  • ClientBase (14-25)
bridges/ai/messages.go (1)
  • PromptContext (29-32)
helpers.go (1)
  • BuildChatInfoWithFallback (281-287)
sdk/prompt_context.go (2)
  • PromptContextToChatCompletionMessages (402-463)
  • BuildDataURL (236-238)
identifier_helpers.go (1)
  • MatrixMessageID (15-17)
bridges/ai/portal_materialize.go (3)
sdk/types.go (1)
  • ChatInfo (79-84)
sdk/portal_lifecycle.go (2)
  • EnsurePortalLifecycle (26-53)
  • PortalLifecycleOptions (13-22)
sdk/commands.go (1)
  • BroadcastCommandDescriptions (82-98)
bridges/ai/streaming_chat_completions.go (2)
bridges/ai/errors.go (3)
  • ContextLengthError (50-54)
  • ParseContextLengthError (131-184)
  • PreDeltaError (61-63)
bridges/ai/client.go (1)
  • AIClient (264-343)
bridges/ai/agent_loop_request_builders.go (4)
bridges/ai/client.go (1)
  • AIClient (264-343)
pkg/agents/tools/types.go (1)
  • Tool (12-18)
pkg/agents/boss.go (1)
  • IsBossAgent (26-28)
pkg/agents/tools/boss.go (2)
  • BossTools (339-349)
  • SessionTools (352-362)
bridges/ai/session_transcript_openclaw_test.go (3)
message_metadata.go (1)
  • BaseMessageMetadata (7-23)
sdk/turn_data.go (2)
  • TurnData (11-17)
  • TurnPart (21-39)
pkg/shared/jsonutil/jsonutil.go (1)
  • ToMap (10-27)
bridges/ai/agent_loop_test.go (2)
bridges/ai/errors.go (1)
  • ContextLengthError (50-54)
bridges/opencode/api/types.go (1)
  • Event (206-209)
bridges/ai/streaming_continuation.go (2)
bridges/ai/client.go (1)
  • AIClient (264-343)
bridges/codex/metadata.go (1)
  • PortalMetadata (30-38)
bridges/ai/streaming_state.go (5)
sdk/turn.go (1)
  • Turn (87-125)
sdk/writer.go (1)
  • Writer (33-41)
sdk/types.go (1)
  • ApprovalHandle (111-115)
pkg/runtime/streaming_directives.go (1)
  • NewStreamingDirectiveAccumulator (22-24)
pkg/runtime/types.go (1)
  • StreamingDirectiveResult (39-46)
bridges/ai/streaming_init.go (4)
sdk/turn.go (1)
  • Turn (87-125)
helpers.go (2)
  • SendDebouncedStreamEdit (65-95)
  • SendDebouncedStreamEditParams (51-62)
bridges/ai/typing_mode.go (3)
  • TypingSignaler (106-115)
  • TypingModeNever (13-13)
  • NewTypingSignaler (117-128)
bridges/ai/typing_controller.go (3)
  • TypingController (20-35)
  • NewTypingController (42-59)
  • TypingControllerOptions (37-40)
bridges/ai/login_loaders.go (3)
bridges/ai/client.go (1)
  • AIClient (264-343)
bridges/codex/metadata.go (1)
  • UserLoginMetadata (13-23)
bridges/ai/connector.go (1)
  • OpenAIConnector (36-45)
bridges/ai/subagent_spawn.go (4)
identifier_helpers.go (2)
  • NewEventID (20-22)
  • MatrixMessageID (15-17)
bridges/opencode/message_metadata.go (1)
  • MessageMetadata (10-22)
bridges/openclaw/metadata.go (1)
  • MessageMetadata (88-97)
message_metadata.go (1)
  • BaseMessageMetadata (7-23)
bridges/ai/response_retry.go (1)
sdk/prompt_context.go (2)
  • PromptContext (14-18)
  • HasUnsupportedResponsesPromptContext (555-557)
bridges/ai/agent_loop_steering_test.go (3)
bridges/ai/client.go (1)
  • AIClient (264-343)
bridges/ai/connector.go (1)
  • OpenAIConnector (36-45)
pkg/runtime/types.go (3)
  • QueueModeFollowup (76-76)
  • QueueModeCollect (77-77)
  • QueueDropSummarize (87-87)
bridges/ai/tool_approvals_test.go (3)
approval_flow.go (2)
  • NewApprovalFlow (109-133)
  • ApprovalFlowConfig (25-55)
approval_decision.go (1)
  • ApprovalDecisionPayload (21-26)
bridges/ai/client.go (1)
  • AIClient (264-343)
bridges/ai/streaming_responses_api.go (5)
bridges/ai/provider_openai.go (1)
  • WithPDFEngine (39-41)
bridges/opencode/api/types.go (1)
  • Event (206-209)
bridges/ai/errors.go (2)
  • ContextLengthError (50-54)
  • PreDeltaError (61-63)
bridges/ai/client.go (1)
  • AIClient (264-343)
pkg/matrixevents/matrixevents.go (1)
  • ResultStatusError (57-57)
bridges/ai/handleai.go (3)
bridges/ai/client.go (1)
  • AIClient (264-343)
bridges/opencode/api/types.go (1)
  • Event (206-209)
status_helpers.go (1)
  • SendMatrixMessageStatus (47-57)

Comment on lines +880 to +889
func (f *ApprovalFlow[D]) cancelPendingTimeout(approvalID string) {
approvalID = strings.TrimSpace(approvalID)
if approvalID == "" {
return
}
f.mu.Lock()
defer f.mu.Unlock()
if p := f.pending[approvalID]; p != nil {
p.closeDone()
}
Copy link

@coderabbitai coderabbitai bot Mar 15, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

cancelPendingTimeout() doesn't actually prevent a timeout.

Line 413 calls this after Resolve() succeeds, but Lines 887-889 only close done. The reaper still decides from pending/ExpiresAt and promptsByApproval in Lines 238-265, so a resolved approval can later be finalized as timeout unless something else removes it first.

💡 Minimal fix
 	f.mu.Lock()
 	defer f.mu.Unlock()
 	if p := f.pending[approvalID]; p != nil {
 		p.closeDone()
+		delete(f.pending, approvalID)
 	}
 }
📝 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 (f *ApprovalFlow[D]) cancelPendingTimeout(approvalID string) {
approvalID = strings.TrimSpace(approvalID)
if approvalID == "" {
return
}
f.mu.Lock()
defer f.mu.Unlock()
if p := f.pending[approvalID]; p != nil {
p.closeDone()
}
func (f *ApprovalFlow[D]) cancelPendingTimeout(approvalID string) {
approvalID = strings.TrimSpace(approvalID)
if approvalID == "" {
return
}
f.mu.Lock()
defer f.mu.Unlock()
if p := f.pending[approvalID]; p != nil {
p.closeDone()
delete(f.pending, approvalID)
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@approval_flow.go` around lines 880 - 889, cancelPendingTimeout currently only
calls p.closeDone() leaving the entry in f.pending so the reaper can still mark
it timed out; update cancelPendingTimeout to remove the pending entry and
related prompt references so resolved approvals are no longer considered by the
reaper: after trimming approvalID and locking f.mu, if p :=
f.pending[approvalID] != nil call p.closeDone(), delete(f.pending, approvalID),
and also remove any entries in f.promptsByApproval for that approvalID (and
clear any ExpiresAt on the pending struct if present) so the reaper's checks
(pending/ExpiresAt and promptsByApproval) will no longer see this approval.

Copy link
Member Author

Choose a reason for hiding this comment

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

@coderabbitai validated against current HEAD: I do not think this should be changed as written. cancelPendingTimeout() closes done, and the reaper explicitly skips approvals where approvalPendingResolved(p) is true, so a resolved approval is not eligible for timeout finalization through the reaper path. I would only change this with a failing test that shows a real repro.

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate approval_flow.go =="
fd -t f -a 'approval_flow.go' || true

echo
echo "== show cancelPendingTimeout =="
rg -nP 'func\s+\(f \*ApprovalFlow\[D any\]\)\s+cancelPendingTimeout\(' -n -C5 -- 'approval_flow.go' || true

echo
echo "== show schedulePromptTimeout and finishTimedOutApproval =="
rg -nP 'func\s+\(f \*ApprovalFlow\[D any\]\)\s+schedulePromptTimeout\(|func\s+\(f \*ApprovalFlow\[D any\]\)\s+finishTimedOutApproval\(' -n -C5 -- 'approval_flow.go' || true

echo
echo "== show reaper loop(s) and helpers =="
rg -nP 'reap|reaper|ensureReaperRunning|wakeReaper|nextReaperDelay' -n -C5 -- 'approval_flow.go' || true

echo
echo "== show approvalPendingResolved and closeDone =="
rg -nP 'approvalPendingResolved\(|closeDone\(' -n -C5 -- 'approval_flow.go' || true

echo
echo "== show promptsByApproval usage =="
rg -nP 'promptsByApproval' -n -C5 -- 'approval_flow.go' || true

echo
echo "== show finalizeWithPromptVersion =="
rg -nP 'finalizeWithPromptVersion\s*\(' -n -C8 -- 'approval_flow.go' || true

echo
echo "== callsites of cancelPendingTimeout =="
rg -n 'cancelPendingTimeout\(' -n -C3 || true

echo
echo "== timeout-related tests =="
fd -a 'approval_flow_test.go' || true
rg -n 'timeout|SchedulePromptTimeout|FinishTimedOut' -n -C4 -- 'approval_flow_test.go' || true

Length of output: 20753


@batuhan You’re right — I rechecked the current HEAD and the reaper explicitly skips resolved pendings.

Key points I verified:

  • nextReaperDelay() and reapExpired() both call approvalPendingResolved(p) and continue when it returns true, for both f.pending entries and promptsByApproval lookups. That means once cancelPendingTimeout() closes p.done, the reaper won’t consider that approval for timeout.
  • finalizeWithPromptVersion (used by both FinishResolved and finishTimedOutApproval) already deletes f.pending[approvalID] and cleans prompt state; we don’t need to delete in cancelPendingTimeout() for correctness.
  • cancelPendingTimeout() is invoked immediately after a successful Resolve enqueue (line ~506), so done is closed under lock before any subsequent reaper pass.

Conclusion:

  • My earlier suggestion to delete from f.pending in cancelPendingTimeout() isn’t necessary. The current implementation is correct given approvalPendingResolved(p) gating. I’m withdrawing the change request.

If you’d like, I can add:

  • A brief comment near cancelPendingTimeout() clarifying that closeDone() is sufficient because the reaper skips resolved items.
  • Or an extra test that resolves an approval right before its expiry and asserts no late timeout finalization occurs (to guard regressions).

Let me know if you want me to add either of those.

batuhan added 2 commits March 15, 2026 15:19
Makes several cross-cutting refactors and feature additions:

- AI client
  - Initialize embedded ClientBase in newAIClient and add SetUserLogin/GetApprovalHandler helpers so reused clients update their embedded ClientBase.
  - Replace previous Agents event type/content with a lightweight AIRoomInfoEventType/AIRoomInfoContent and register it for sync.
  - Add tests to verify client reuse updates ClientBase and that AIRoomInfo event type is registered.

- Event and room settings cleanup
  - Remove custom RoomCapabilities/RoomSettings event type plumbing from OpenCode/ Codex code paths and the helpers that populated power level overrides for those events.
  - Simplify DM/login chat info structs to no longer carry capability/settings event types or related power level overrides.
  - Update matrixevents package to only expose AIRoomInfoEventType (remove several previously-defined ai event constants).

- OpenCode bridge
  - Remove host interface methods and usages for RoomCapabilitiesEventType/RoomSettingsEventType.

- beeperauth login flow
  - Add HTTP helpers for login flow: normalizeEmail/normalizeLoginCode, sendLoginEmail, sendLoginCode, newJSONRequest, loginCodeResponse parsing and helpers, and an http.Client with timeout.
  - Use DomainForEnv earlier in cmd login to print the target domain.
  - Wire sendLoginEmail/sendLoginCode into Login and surface signup-related errors.
  - Add unit tests for normalization and loginCodeResponse behavior (new normalize_test.go).

These changes simplify event/state handling for AI rooms, centralize client base initialization for AI clients, and implement a more robust HTTP-based beeper auth flow with tests.
Add handling for reactions targeting the wrong event when answering approval prompts: introduce matchFallbackReaction and hasPendingApprovalForOwner to resolve/mirror decisions or send a Matrix message status notice when ambiguous, redact resolved reactions, and mirror remote decisions. Add isApprovalReactionKey, sendMessageStatus helper, and approvalWrongTargetMSSMessage constant, plus new flags on ApprovalPromptReactionMatch to control mirroring and redaction.

Enhance AI error classification and reporting: add IsPermissionDeniedError and extractStructuredErrorMessage, surface permission-denied errors in FormatUserFacingError, and consolidate bridge state selection into bridgeStateForError so permission vs auth vs billing/rate-limit cases are handled correctly (permission-denied does not force logout). Update message status mapping to include permission-denied cases.

Persist streaming checkpoints for approval requests: shouldPersistDebouncedCheckpoint forces debounced checkpoint persistence for tool-approval-request parts and ensure EmitPart triggers SendDebounced when appropriate; buildStreamUI now includes pending approval state.

Classify access_denied/feature-flag/subscription errors as provider-hard failures for fallback logic. Many unit tests added/updated across approval_flow, bridges/ai, streaming UI and runtime packages to cover the new behaviors.
Copy link

@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: 1

♻️ Duplicate comments (5)
bridges/ai/login_loaders.go (2)

58-69: ⚠️ Potential issue | 🟠 Major

Calling Disconnect() while holding clientsMu risks deadlock.

At line 66, cached.Disconnect() is invoked while holding oc.clientsMu (deferred unlock at line 60). If Disconnect() attempts to acquire the same lock or interacts with other lock-protected state, this can cause deadlock or stalls.

The fix is to capture the client reference, delete from the map, release the lock, and then call Disconnect().

Proposed fix
 func (oc *OpenAIConnector) evictCachedClient(loginID networkid.UserLoginID, expected bridgev2.NetworkAPI) {
 	oc.clientsMu.Lock()
-	defer oc.clientsMu.Unlock()
 	cachedAPI := oc.clients[loginID]
 	if expected != nil && cachedAPI != expected {
+		oc.clientsMu.Unlock()
 		return
 	}
-	if cached, ok := cachedAPI.(*AIClient); ok && cached != nil {
-		cached.Disconnect()
-	}
+	cached, _ := cachedAPI.(*AIClient)
 	delete(oc.clients, loginID)
+	oc.clientsMu.Unlock()
+	if cached != nil {
+		cached.Disconnect()
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/login_loaders.go` around lines 58 - 69, In evictCachedClient,
avoid calling cached.Disconnect() while holding oc.clientsMu to prevent
deadlock: under the oc.clientsMu lock (in evictCachedClient) load the cached
client into a local variable, check expected matching, delete the entry from
oc.clients, then release the lock and only after unlocking call the
AIClient.Disconnect() on that saved reference (use the oc.clientsMu, oc.clients
map and AIClient.Disconnect names to locate and change the code).

71-88: ⚠️ Potential issue | 🟠 Major

Same issue: Disconnect() called under clientsMu lock.

Lines 78 and 83 call Disconnect() while holding clientsMu. Apply the same pattern: capture references, update map, release lock, then disconnect.

Proposed fix
 func (oc *OpenAIConnector) publishOrReuseClient(login *bridgev2.UserLogin, created *AIClient, replace *AIClient) *AIClient {
 	if login == nil || created == nil {
 		return nil
 	}
 	oc.clientsMu.Lock()
-	defer oc.clientsMu.Unlock()
 	if cached, ok := oc.clients[login.ID].(*AIClient); ok && cached != nil && cached != replace {
-		created.Disconnect()
 		reuseAIClient(login, cached, false)
+		oc.clientsMu.Unlock()
+		created.Disconnect()
 		return cached
 	}
+	var toDisconnect *AIClient
 	if replace != nil && replace != created {
-		replace.Disconnect()
+		toDisconnect = replace
 	}
 	oc.clients[login.ID] = created
 	reuseAIClient(login, created, false)
+	oc.clientsMu.Unlock()
+	if toDisconnect != nil {
+		toDisconnect.Disconnect()
+	}
 	return created
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/login_loaders.go` around lines 71 - 88, publishOrReuseClient is
calling Disconnect() while holding oc.clientsMu; move any calls to Disconnect
(and any non-map work like reuseAIClient) outside the critical section. Inside
the locked section (oc.clientsMu.Lock/Unlock) only read/compare and assign
oc.clients[login.ID] and capture references to any clients that need to be
disconnected or reused (e.g., variables for cached, toDisconnect := replace or
created). After releasing the lock, call Disconnect() on the captured client(s)
and then call reuseAIClient(login, <cached|created>, false) as appropriate;
ensure you still return the correct *AIClient (cached or created).
bridges/ai/login_loaders_test.go (1)

15-22: ⚠️ Potential issue | 🟡 Minor

Storing typed-nil in interface field breaks nil checks.

When meta is nil, assigning it directly to Metadata (an interface field) stores a typed-nil (*UserLoginMetadata)(nil) rather than a true nil interface. Code checking login.Metadata == nil will return false unexpectedly.

Proposed fix
 func testUserLoginWithMeta(loginID networkid.UserLoginID, meta *UserLoginMetadata) *bridgev2.UserLogin {
-	return &bridgev2.UserLogin{
-		UserLogin: &database.UserLogin{
-			ID:       loginID,
-			Metadata: meta,
-		},
-	}
+	login := &bridgev2.UserLogin{
+		UserLogin: &database.UserLogin{
+			ID: loginID,
+		},
+	}
+	if meta != nil {
+		login.UserLogin.Metadata = meta
+	}
+	return login
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/login_loaders_test.go` around lines 15 - 22, The
testUserLoginWithMeta helper stores a typed nil in the
database.UserLogin.Metadata interface when meta is a nil *UserLoginMetadata,
which breaks downstream nil checks; update testUserLoginWithMeta so that before
constructing bridgev2.UserLogin it sets database.UserLogin.Metadata to nil if
meta == nil, otherwise assign meta (i.e., use a conditional: if meta == nil then
Metadata = nil else Metadata = meta) referencing the testUserLoginWithMeta
function and the Metadata field on database.UserLogin/UserLoginMetadata to
locate the change.
approval_flow.go (2)

363-379: ⚠️ Potential issue | 🟠 Major

ResolveExternal bypasses DeliverDecision callback mode.

ResolveExternal always routes through Resolve (channel send) on Line 372. In callback-based mode (DeliverDecision), this skips the actual delivery path used in HandleReaction, so external decisions can be finalized without invoking bridge delivery logic.

💡 Suggested direction
 func (f *ApprovalFlow[D]) ResolveExternal(ctx context.Context, approvalID string, decision ApprovalDecisionPayload) {
   ...
-  if err := f.Resolve(approvalID, decision); err != nil {
-    return
-  }
+  if f.deliverDecision != nil {
+    f.mu.Lock()
+    p := f.pending[approvalID]
+    f.mu.Unlock()
+    if p == nil {
+      return
+    }
+    if err := f.deliverDecision(ctx, nil, p, decision); err != nil {
+      return
+    }
+    f.cancelPendingTimeout(approvalID)
+  } else if err := f.Resolve(approvalID, decision); err != nil {
+    return
+  }
   ...
 }

Also applies to: 399-421, 877-888

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

In `@approval_flow.go` around lines 363 - 379, ResolveExternal currently always
calls Resolve (which uses the channel send path) and thus bypasses the
DeliverDecision callback path used by HandleReaction; update ResolveExternal to
detect when the flow is configured for callback delivery and, in that case,
invoke the DeliverDecision path instead of Resolve so external decisions go
through the same bridge delivery logic. Specifically, after normalizing the ID
(normalizeDecisionID) and obtaining the prompt via promptRegistration, branch on
the delivery mode used by this ApprovalFlow (the same condition used elsewhere
to decide between Resolve and DeliverDecision), call the DeliverDecision method
with the approvalID and decision when in callback mode, and only call Resolve
when channel delivery is appropriate; keep the mirrorRemoteDecisionReaction and
FinishResolved calls unchanged so remote mirroring and finalization still occur.

218-239: ⚠️ Potential issue | 🟠 Major

cancelPendingTimeout currently has no effect on reaper timeouts.

Line 1034 closes done, but reaper expiry checks never consult done (Lines 222-227 and 246-249). A resolved approval can still be finalized later as timeout unless dropped immediately elsewhere.

💡 Minimal fix
 func (f *ApprovalFlow[D]) nextReaperDelay() time.Duration {
   ...
   for _, p := range f.pending {
+    select {
+    case <-p.done:
+      continue
+    default:
+    }
     earliest = earliestExpiry(earliest, p.ExpiresAt)
   }
   ...
 }

 func (f *ApprovalFlow[D]) reapExpired() {
   ...
   for aid, p := range f.pending {
+    select {
+    case <-p.done:
+      continue
+    default:
+    }
     if !p.ExpiresAt.IsZero() && now.After(p.ExpiresAt) {
       expired = append(expired, aid)
     }
   }
   ...
 }

Also applies to: 241-249, 1026-1035

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

In `@approval_flow.go` around lines 218 - 239, nextReaperDelay currently never
observes the cancelation signal from cancelPendingTimeout, so closing f.done
doesn't wake the reaper and resolved approvals can still be finalized by
timeout; update nextReaperDelay to check the flow's cancelation (f.done or
context) under the same lock and, if closed, return a minimal delay (e.g.
time.Millisecond) so the reaper loop wakes and re-evaluates immediately;
reference f.done, nextReaperDelay, cancelPendingTimeout, f.pending and
f.promptsByApproval when making this change.
🧹 Nitpick comments (3)
bridges/ai/handleai_test.go (1)

120-132: Harden auth-403 coverage with an error-code assertion.

Line 129 verifies StateEvent, but this path can still regress on bridge error-code mapping without failing this test.

✅ Suggested test hardening
 func TestBridgeStateForError_Auth403(t *testing.T) {
 	err := testOpenAIError(403, "forbidden", "authentication_error", "invalid api key")
 	state, shouldMarkLoggedOut, ok := bridgeStateForError(err)
 	if !ok {
 		t.Fatal("expected bridge state for auth failure")
 	}
 	if !shouldMarkLoggedOut {
 		t.Fatal("expected auth failure to mark login inactive")
 	}
 	if state.StateEvent != status.StateBadCredentials {
 		t.Fatalf("expected bad credentials state, got %s", state.StateEvent)
 	}
+	if state.Error == "" {
+		t.Fatal("expected non-empty bridge error code for auth failure")
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/handleai_test.go` around lines 120 - 132, The test
TestBridgeStateForError_Auth403 currently asserts StateEvent but not the mapped
error code; update this test (which calls testOpenAIError and
bridgeStateForError) to also assert that the returned state's error-code field
(e.g. state.ErrorCode) equals "authentication_error" so regressions in the
bridgeStateForError error-code mapping are caught; add a concise check after
obtaining state to compare state.ErrorCode to "authentication_error" and fail
the test if it doesn't match.
approval_flow_test.go (1)

44-46: Close each ApprovalFlow in tests to avoid reaper goroutine leaks.

Each NewApprovalFlow(...) starts a background reaper. Add t.Cleanup(flow.Close) right after construction so test processes stay clean and deterministic.

♻️ Suggested pattern
 flow := NewApprovalFlow(ApprovalFlowConfig[*testApprovalFlowData]{
     Login: func() *bridgev2.UserLogin { return login },
 })
+t.Cleanup(flow.Close)

Also applies to: 142-147, 196-205, 251-254, 333-335, 423-425, 479-480, 508-510, 555-557, 616-620

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

In `@approval_flow_test.go` around lines 44 - 46, Each test that constructs an
ApprovalFlow via NewApprovalFlow (e.g., flow := NewApprovalFlow(...)) must call
t.Cleanup(flow.Close) immediately after construction to avoid leaking the
background reaper goroutine; update each occurrence of NewApprovalFlow
(including the instances around the ranges mentioned) to add
t.Cleanup(flow.Close) directly after the variable is assigned so the flow.Close
method is guaranteed to run when the test finishes.
approval_prompt.go (1)

612-615: Use the reason constant instead of a raw string literal.

Line 614 hardcodes "allow_always". Prefer ApprovalReasonAllowAlways to avoid drift.

🧹 Proposed tweak
-		if strings.TrimSpace(option.ID) == "allow_always" || option.Always {
+		if strings.TrimSpace(option.ID) == ApprovalReasonAllowAlways || option.Always {
 			return true
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@approval_prompt.go` around lines 612 - 615, In approvalOptionsAllowAlways,
replace the raw string literal "allow_always" with the reason constant
ApprovalReasonAllowAlways: update the condition inside
approvalOptionsAllowAlways (which iterates ApprovalOption items) to compare
strings.TrimSpace(option.ID) against ApprovalReasonAllowAlways (and keep the
existing option.Always check) so the function uses the canonical constant rather
than a hardcoded value.
🤖 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 482-484: initProviderForLogin currently dereferences meta.Provider
without checking meta for nil, which can panic; add a nil guard at the start of
initProviderForLogin (checking the meta parameter) and return a clear error
(matching the function's (*OpenAIProvider, error) signature) if meta is nil
before reaching the switch on meta.Provider, so downstream code like the
switch-case on meta.Provider and any meta.* accesses are safe.

---

Duplicate comments:
In `@approval_flow.go`:
- Around line 363-379: ResolveExternal currently always calls Resolve (which
uses the channel send path) and thus bypasses the DeliverDecision callback path
used by HandleReaction; update ResolveExternal to detect when the flow is
configured for callback delivery and, in that case, invoke the DeliverDecision
path instead of Resolve so external decisions go through the same bridge
delivery logic. Specifically, after normalizing the ID (normalizeDecisionID) and
obtaining the prompt via promptRegistration, branch on the delivery mode used by
this ApprovalFlow (the same condition used elsewhere to decide between Resolve
and DeliverDecision), call the DeliverDecision method with the approvalID and
decision when in callback mode, and only call Resolve when channel delivery is
appropriate; keep the mirrorRemoteDecisionReaction and FinishResolved calls
unchanged so remote mirroring and finalization still occur.
- Around line 218-239: nextReaperDelay currently never observes the cancelation
signal from cancelPendingTimeout, so closing f.done doesn't wake the reaper and
resolved approvals can still be finalized by timeout; update nextReaperDelay to
check the flow's cancelation (f.done or context) under the same lock and, if
closed, return a minimal delay (e.g. time.Millisecond) so the reaper loop wakes
and re-evaluates immediately; reference f.done, nextReaperDelay,
cancelPendingTimeout, f.pending and f.promptsByApproval when making this change.

In `@bridges/ai/login_loaders_test.go`:
- Around line 15-22: The testUserLoginWithMeta helper stores a typed nil in the
database.UserLogin.Metadata interface when meta is a nil *UserLoginMetadata,
which breaks downstream nil checks; update testUserLoginWithMeta so that before
constructing bridgev2.UserLogin it sets database.UserLogin.Metadata to nil if
meta == nil, otherwise assign meta (i.e., use a conditional: if meta == nil then
Metadata = nil else Metadata = meta) referencing the testUserLoginWithMeta
function and the Metadata field on database.UserLogin/UserLoginMetadata to
locate the change.

In `@bridges/ai/login_loaders.go`:
- Around line 58-69: In evictCachedClient, avoid calling cached.Disconnect()
while holding oc.clientsMu to prevent deadlock: under the oc.clientsMu lock (in
evictCachedClient) load the cached client into a local variable, check expected
matching, delete the entry from oc.clients, then release the lock and only after
unlocking call the AIClient.Disconnect() on that saved reference (use the
oc.clientsMu, oc.clients map and AIClient.Disconnect names to locate and change
the code).
- Around line 71-88: publishOrReuseClient is calling Disconnect() while holding
oc.clientsMu; move any calls to Disconnect (and any non-map work like
reuseAIClient) outside the critical section. Inside the locked section
(oc.clientsMu.Lock/Unlock) only read/compare and assign oc.clients[login.ID] and
capture references to any clients that need to be disconnected or reused (e.g.,
variables for cached, toDisconnect := replace or created). After releasing the
lock, call Disconnect() on the captured client(s) and then call
reuseAIClient(login, <cached|created>, false) as appropriate; ensure you still
return the correct *AIClient (cached or created).

---

Nitpick comments:
In `@approval_flow_test.go`:
- Around line 44-46: Each test that constructs an ApprovalFlow via
NewApprovalFlow (e.g., flow := NewApprovalFlow(...)) must call
t.Cleanup(flow.Close) immediately after construction to avoid leaking the
background reaper goroutine; update each occurrence of NewApprovalFlow
(including the instances around the ranges mentioned) to add
t.Cleanup(flow.Close) directly after the variable is assigned so the flow.Close
method is guaranteed to run when the test finishes.

In `@approval_prompt.go`:
- Around line 612-615: In approvalOptionsAllowAlways, replace the raw string
literal "allow_always" with the reason constant ApprovalReasonAllowAlways:
update the condition inside approvalOptionsAllowAlways (which iterates
ApprovalOption items) to compare strings.TrimSpace(option.ID) against
ApprovalReasonAllowAlways (and keep the existing option.Always check) so the
function uses the canonical constant rather than a hardcoded value.

In `@bridges/ai/handleai_test.go`:
- Around line 120-132: The test TestBridgeStateForError_Auth403 currently
asserts StateEvent but not the mapped error code; update this test (which calls
testOpenAIError and bridgeStateForError) to also assert that the returned
state's error-code field (e.g. state.ErrorCode) equals "authentication_error" so
regressions in the bridgeStateForError error-code mapping are caught; add a
concise check after obtaining state to compare state.ErrorCode to
"authentication_error" and fail the test if it doesn't match.
🪄 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: 97ed3e0b-5145-48ba-987b-fb21dd6e1ad9

📥 Commits

Reviewing files that changed from the base of the PR and between 710fd85 and 628d921.

📒 Files selected for processing (15)
  • approval_flow.go
  • approval_flow_test.go
  • approval_prompt.go
  • bridges/ai/chat_login_redirect_test.go
  • bridges/ai/client.go
  • bridges/ai/errors.go
  • bridges/ai/errors_extended.go
  • bridges/ai/errors_test.go
  • bridges/ai/events.go
  • bridges/ai/handleai.go
  • bridges/ai/handleai_test.go
  • bridges/ai/login_loaders.go
  • bridges/ai/login_loaders_test.go
  • bridges/ai/message_status.go
  • bridges/ai/streaming_ui_tools_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • bridges/ai/streaming_ui_tools_test.go
  • bridges/ai/errors_test.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). (3)
  • GitHub Check: build-docker
  • GitHub Check: Lint
  • GitHub Check: build-docker
🧰 Additional context used
🧬 Code graph analysis (10)
bridges/ai/errors_extended.go (1)
bridges/ai/errors.go (1)
  • IsPermissionDeniedError (250-272)
bridges/ai/login_loaders_test.go (4)
bridges/ai/connector.go (1)
  • OpenAIConnector (36-45)
bridges/ai/login.go (1)
  • ProviderOpenAI (21-21)
bridges/ai/events.go (2)
  • AIRoomInfoEventType (26-26)
  • AIRoomInfoContent (122-124)
pkg/matrixevents/matrixevents.go (1)
  • AIRoomInfoEventType (18-18)
bridges/ai/message_status.go (2)
bridges/ai/errors.go (3)
  • IsPermissionDeniedError (250-272)
  • IsAuthError (276-295)
  • IsModelNotFound (298-318)
bridges/ai/errors_extended.go (2)
  • IsBillingError (119-131)
  • IsImageError (162-170)
bridges/ai/login_loaders.go (3)
bridges/ai/client.go (1)
  • AIClient (264-343)
bridges/codex/metadata.go (1)
  • UserLoginMetadata (13-23)
bridges/ai/connector.go (1)
  • OpenAIConnector (36-45)
bridges/ai/handleai_test.go (1)
bridges/ai/errors.go (1)
  • AIProviderError (19-19)
bridges/ai/client.go (11)
client_base.go (1)
  • ClientBase (14-25)
bridges/openclaw/metadata.go (1)
  • MessageMetadata (88-97)
bridges/ai/provider_openai.go (1)
  • OpenAIProvider (24-28)
bridges/ai/login.go (1)
  • ProviderBeeper (20-20)
cmd/internal/beeperauth/auth.go (1)
  • Config (27-32)
bridges/ai/messages.go (1)
  • PromptContext (29-32)
bridges/opencode/api/types.go (1)
  • Event (206-209)
helpers.go (1)
  • BuildChatInfoWithFallback (269-275)
sdk/prompt_context.go (4)
  • PromptContextToChatCompletionMessages (402-463)
  • AppendChatMessagesToPromptContext (54-61)
  • AppendPromptText (81-91)
  • BuildDataURL (236-238)
identifier_helpers.go (1)
  • MatrixMessageID (15-17)
message_metadata.go (1)
  • BaseMessageMetadata (7-23)
bridges/ai/events.go (1)
pkg/matrixevents/matrixevents.go (1)
  • AIRoomInfoEventType (18-18)
approval_flow_test.go (4)
approval_flow.go (3)
  • ApprovalFlowConfig (27-57)
  • Pending (60-65)
  • SendPromptParams (717-721)
approval_decision.go (1)
  • ApprovalDecisionPayload (21-26)
approval_prompt.go (3)
  • ApprovalPromptRegistration (469-483)
  • ApprovalPromptMessageParams (258-267)
  • ApprovalPromptPresentation (46-50)
approval_reaction_helpers.go (1)
  • MatrixSenderID (16-21)
bridges/ai/handleai.go (3)
bridges/ai/errors.go (7)
  • IsAuthError (276-295)
  • AIAuthFailed (18-18)
  • IsPermissionDeniedError (250-272)
  • AIProviderError (19-19)
  • AIBillingError (20-20)
  • IsRateLimitError (187-202)
  • AIRateLimited (17-17)
bridges/ai/errors_extended.go (3)
  • FormatUserFacingError (317-430)
  • IsBillingError (119-131)
  • IsOverloadedError (134-143)
status_helpers.go (1)
  • SendMatrixMessageStatus (47-57)
approval_prompt.go (1)
approval_decision.go (8)
  • ApprovalReasonAllowOnce (10-10)
  • ApprovalReasonDeny (13-13)
  • ApprovalReasonAllowAlways (11-11)
  • ApprovalDecisionPayload (21-26)
  • ApprovalReasonTimeout (14-14)
  • ApprovalReasonExpired (15-15)
  • ApprovalReasonDeliveryError (17-17)
  • ApprovalReasonCancelled (16-16)
🔇 Additional comments (18)
bridges/ai/handleai_test.go (3)

1-10: Package rename and import updates look good.

No concerns in the package ai migration and supporting test imports.


100-118: Access-denied 403 bridge-state test is solid.

This adds good coverage for ok, logout behavior, state event, provider error code, and propagated message.


134-142: No-permission message-status mapping test looks correct.

Nice coverage for both status (fail) and reason (no_permission) on access-denied 403.

bridges/ai/chat_login_redirect_test.go (1)

56-75: LGTM!

The new tests provide good coverage for resolveModelIDFromManifest and parseModelFromGhostID utilities, including the malformed escaping edge case. The test structure follows the existing patterns in this file.

bridges/ai/errors_extended.go (3)

100-116: LGTM!

The containsAnyInFields helper correctly iterates over fields and patterns, short-circuiting on the first match. The empty-field skip and lowercase normalization are appropriate for pattern matching against structured error fields.


278-313: LGTM!

The extractStructuredErrorMessage helper provides a robust extraction path: first checking for typed openai.Error, then falling back to JSON parsing with both nested and flat structures. The use of safeErrorString guards against panics from malformed error implementations.


339-343: LGTM!

Good addition to surface provider-specific permission-denied messages when available. The fallthrough behavior allows other error classifiers to handle the case if no structured message is extracted.

bridges/ai/errors.go (2)

248-272: LGTM on the core IsPermissionDeniedError logic.

The function correctly handles the precedence: it excludes model-not-found errors first, then checks 403 status codes with appropriate pattern matching. The two-tier approach (primary patterns, then fallback patterns when not auth-related) is a reasonable heuristic for distinguishing permission from auth failures.


286-294: IsPermissionDeniedError does not check permissionFallbackPatterns for non-openai.Error types, creating an inconsistency with openai.Error handling.

For openai.Error with status 403, IsPermissionDeniedError checks permissionFallbackPatterns (lines 15-19), but for plain errors, it only checks permissionDeniedPatterns (line 24). This means:

  • A plain error containing "forbidden" or "access denied": passes through IsPermissionDeniedError (returns false) and is matched by IsAuthError at line 47
  • An openai.Error 403 with the same text: is caught by IsPermissionDeniedError and never reaches IsAuthError

If the intent is to classify "forbidden"/"access denied" consistently as permission-denied errors (not auth errors), then IsPermissionDeniedError should also check permissionFallbackPatterns for non-openai.Error types.

bridges/ai/message_status.go (1)

9-41: LGTM!

The error classification updates are consistent:

  • IsPermissionDeniedError correctly maps to MessageStatusFail and MessageStatusNoPermission
  • Grouping IsModelNotFound with context/image errors under Unsupported aligns with the semantic meaning (capability/format mismatches rather than permission issues)
bridges/ai/events.go (2)

15-26: LGTM!

The AIRoomInfoEventType registration in init() correctly associates the state event type with its content struct. The alias pattern keeps the type definition centralized in matrixevents while exposing it conveniently in this package.


121-124: LGTM!

The AIRoomInfoContent struct is appropriately minimal for its purpose of identifying AI room surfaces in Matrix state. The single Type field provides sufficient metadata for client-side rendering decisions.

bridges/ai/login_loaders.go (1)

90-127: LGTM on the overall loadAIUserLogin flow.

The function handles the key scenarios correctly:

  • No API key → evict cache and set broken client
  • Existing client still valid → reuse with bootstrap
  • Cached non-AIClient → evict before creating new
  • New client creation failure → preserve existing or set broken
  • Success → publish/reuse and schedule bootstrap

The logic is defensive and handles race conditions appropriately (double-check in publishOrReuseClient).

bridges/ai/login_loaders_test.go (1)

24-95: LGTM on the test coverage.

The tests provide good coverage for the login lifecycle:

  • TestAIClientNeedsRebuild covers key, provider, and base URL change detection
  • TestLoadAIUserLoginMissingAPIKeyEvictsCacheAndSetsBrokenClient validates eviction behavior
  • TestReuseAIClientUpdatesClientBaseLogin confirms client wiring
  • TestAIRoomInfoEventTypeRegistered validates event registration
bridges/ai/handleai.go (3)

77-121: LGTM on bridgeStateForError centralization.

The helper provides a clean mapping from error types to bridge states:

  • Auth errors → StateBadCredentials with logout
  • Permission denied → StateUnknownError without logout (appropriate since this isn't a credential issue)
  • Billing → StateTransientDisconnect (recoverable)
  • Rate limit/overload → StateTransientDisconnect (transient)

The three-value return (state, shouldLogout, handled) gives callers clear control flow.


36-42: LGTM on the error handling flow.

The integration with bridgeStateForError is correct: logout is triggered only for auth failures, and bridge state is always published when the error is recognized. The ok check ensures unrecognized errors don't trigger spurious state updates.


574-600: LGTM on setRoomName consolidation.

Adding the save parameter eliminates the need for separate wrapper functions. The conditional save at line 592-595 correctly gates the persistence operation, and metadata is always updated in-memory regardless.

bridges/ai/client.go (1)

1617-1621: Good nil-safety hardening in model cache lookup.

The meta != nil && meta.ModelCache != nil check prevents a panic path while preserving fallback behavior.

batuhan added 5 commits March 15, 2026 21:40
Include "tool-approval-response" in shouldPersistDebouncedCheckpoint so approval responses persist debounced checkpoints like approval requests. Add TestStreamSessionApprovalResponsePersistsCheckpointWithoutFallback to assert that an approval response triggers a forced debounced checkpoint edit (SendDebouncedEdit called with force=true), the stream hook is invoked, and the runtime fallback flag is not enabled.
Track and block post-resolution reaction changes for approval prompts. Added resolvedPrompt tracking (by event and message), rememberResolvedPromptLocked, and resolvedPrompt lookup to detect late reactions or redactions and respond with a MessageStatus fail plus redaction when applicable. Introduced an ApprovalReactionRemoveHandler interface and ApprovalFlow.HandleReactionRemove to reject removals of terminal approval choices; BaseReactionHandler and the AI bridge now call into this removal handler. Adjusted redactSingleReaction to pick a sender tied to the original Matrix user (reactionRedactionSender) and ensure a synthetic ghost is created when needed. Finalization now records resolved prompts, and editPromptToResolvedState resolves the target message if necessary. Also updated debounced edit logic to exclude approval request/response events and adapted related tests; added tests for reaction redaction sender and resolved-prompt behavior. Minor refactors and test additions to validate the new behavior.
Introduce event timing/stream-order support and propagate it through the AI streaming pipeline (sendViaPortalWithTiming, sendEditViaPortalWithTiming, nextMessageTiming, EventTiming usage). Canonicalize response status handling (canonicalResponseStatus), refactor response lifecycle updates (applyResponseLifecycleState), and add tests for streaming lifecycle and model ID resolution. Standardize remote reaction/remove events to use agentremote builders and remove the old remote_events implementation. Improve model contact resolution to accept URL-encoded IDs and hydrate ghosts. Add Codex directory management and per-path backfill syncs, adjust composeCodexChatInfo to include portal/topic handling, and various logging/robustness fixes across AI and Codex bridges.
Introduce parsing and handling for `!codex` commands (help, new, dirs, import, forget) in the Codex bridge: added parseCodexCommand, command help text, path resolution, management helpers (cleanup, deletePortalOnly, managedImportedPortalsForPath, forgetManagedDirectory) and a handler hook in HandleMatrixMessage. Add unit tests for command parsing and path resolution.

Refactor OpenClaw remote event plumbing to use simplevent builders: replace custom OpenClawSessionResyncEvent and OpenClawRemoteMessage implementations with buildOpenClawSessionResyncEvent and buildOpenClawRemoteMessage (and related helper functions). Update call sites to queue the new simplevent-based events and adjust session/chat info assembly to use the new signatures. Remove legacy RemoteMessage/RemoteReaction implementations from remote_events.go and update tests accordingly (including sendSystemNoticeViaPortal changes to use the builder). Also add/adjust imports and tests to reflect these changes.
Update naming, database tables, and config keys for the AI bridge surface: change user-facing strings from "AI" to "AI Chats"; rename SQLite tables and keys (ai_* -> aichats_* and ai_sessions -> agentremote_sessions) and adjust DB logging section. Normalize YAML config field names from camelCase to snake_case and extend the config upgrader to copy the new keys. Switch subagent types to use pkg/agents/agentconfig and remove the identity conversion file. Misc: replace several openclaw string helpers with stringutil, minor README wording updates, and other wiring fixes to match these refactors.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

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

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

116-123: ⚠️ Potential issue | 🟡 Minor

Unreachable code detected.

Lines 120-122 will never execute:

  • If strings.EqualFold(file.Path, agents.DefaultBootstrapFilename) is true on line 117, the test fails immediately at line 118.
  • If false on line 117, the same check on line 120 will also be false.

The second condition is dead code that should be removed.

🧹 Proposed fix to remove dead code
 	files := oc.buildBootstrapContextFiles(ctx, agentID, nil)
 	for _, file := range files {
 		if strings.EqualFold(file.Path, agents.DefaultBootstrapFilename) {
 			t.Fatalf("expected BOOTSTRAP.md to not be injected after auto-delete, but it was present")
 		}
-		if strings.EqualFold(file.Path, agents.DefaultBootstrapFilename) && strings.Contains(file.Content, "[MISSING]") {
-			t.Fatalf("expected no missing placeholder for BOOTSTRAP.md")
-		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/bootstrap_context_test.go` around lines 116 - 123, The second
conditional that checks strings.EqualFold(file.Path,
agents.DefaultBootstrapFilename) && strings.Contains(...) is dead code; remove
it and either combine the checks into a single branch or nest the content check
inside the presence check: locate the loop iterating over files and the usages
of agents.DefaultBootstrapFilename and strings.Contains, then replace the two
separate ifs with one that first tests strings.EqualFold(file.Path,
agents.DefaultBootstrapFilename) and inside that block call
strings.Contains(file.Content, "[MISSING]") to fail with a specific message if
present, otherwise fail for presence as before.
bridges/ai/reactions.go (1)

15-22: ⚠️ Potential issue | 🟠 Major

Add the same login guard before queuing synthetic reactions.

The new BuildReactionEvent path dereferences oc.UserLogin.ID, but sendReaction still has no early UserLogin / Bridge / DB guard and already touches DB.Message on Line 21. A disconnect race turns this helper into a panic instead of a no-op.

🛡️ Proposed guard
 func (oc *AIClient) sendReaction(ctx context.Context, portal *bridgev2.Portal, targetEventID id.EventID, emoji string) {
-	if portal == nil || portal.MXID == "" || targetEventID == "" || emoji == "" {
+	if oc == nil || oc.UserLogin == nil || oc.UserLogin.Bridge == nil || oc.UserLogin.Bridge.DB == nil ||
+		portal == nil || portal.MXID == "" || targetEventID == "" || emoji == "" {
 		return
 	}

Also applies to: 50-53

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

In `@bridges/ai/reactions.go` around lines 15 - 22, The sendReaction helper can
panic when the user disconnects because it dereferences oc.UserLogin /
oc.UserLogin.Bridge / oc.UserLogin.Bridge.DB and oc.UserLogin.ID without
checking them; add an early guard at the top of sendReaction that returns if
oc.UserLogin is nil, oc.UserLogin.ID is empty, oc.UserLogin.Bridge is nil, or
oc.UserLogin.Bridge.DB is nil (the same guard should be added before queuing any
BuildReactionEvent call path that uses oc.UserLogin.ID) so the function becomes
a no-op on disconnect instead of panicking.
bridges/ai/scheduler_cron.go (1)

366-372: ⚠️ Potential issue | 🟠 Major

Check target.Portal with a real cast before sending.

target.Portal == nil only protects a nil interface. If resolveCronDeliveryTarget returns a typed-nil *bridgev2.Portal (where the dynamic type is non-nil but the pointer value is nil), this guard is bypassed and sendPlainAssistantMessage receives a nil portal. Cast once, validate with ok && portal != nil, then send.

🛡️ Proposed safe cast
 	if record.Job.Delivery != nil && record.Job.Delivery.Mode == integrationcron.DeliveryAnnounce {
 		target := s.resolveCronDeliveryTarget(record.Job.AgentID, record.Job.Delivery)
-		if target.Portal == nil || strings.TrimSpace(target.RoomID) == "" {
+		portal, ok := target.Portal.(*bridgev2.Portal)
+		if !ok || portal == nil || strings.TrimSpace(target.RoomID) == "" {
 			return "skipped", "delivery target unavailable", preview
 		}
-		if err := s.client.sendPlainAssistantMessage(runCtx, target.Portal.(*bridgev2.Portal), body); err != nil {
+		if err := s.client.sendPlainAssistantMessage(runCtx, portal, body); err != nil {
 			return "error", err.Error(), preview
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/scheduler_cron.go` around lines 366 - 372, The current check uses
target.Portal == nil which doesn't catch a typed-nil *bridgev2.Portal; update
the block around resolveCronDeliveryTarget and sendPlainAssistantMessage to
perform a type assertion to *bridgev2.Portal (e.g., portal, ok :=
target.Portal.(*bridgev2.Portal)) and then ensure ok && portal != nil before
calling s.client.sendPlainAssistantMessage(runCtx, portal, body); keep the
existing RoomID check and return paths unchanged if the portal is invalid.
♻️ Duplicate comments (3)
bridges/ai/streaming_responses_api.go (1)

382-393: ⚠️ Potential issue | 🔴 Critical

Guard state.turn before reading turn ID.

Line 391 dereferences state.turn directly. If it is nil during completion handling, this will panic the stream processor.

🔧 Suggested fix
 		if !isContinuation {
 			// Extract any generated images from response output
+			turnID := ""
+			if state.turn != nil {
+				turnID = state.turn.ID()
+			}
 			for _, output := range streamEvent.Response.Output {
 				if output.Type == "image_generation_call" {
 					imgOutput := output.AsImageGenerationCall()
 					if imgOutput.Status == "completed" && imgOutput.Result != "" {
 						state.pendingImages = append(state.pendingImages, generatedImage{
 							itemID:   imgOutput.ID,
 							imageB64: imgOutput.Result,
-							turnID:   state.turn.ID(),
+							turnID:   turnID,
 						})
 						log.Debug().Str("item_id", imgOutput.ID).Msg("Captured generated image from response")
 					}
 				}
 			}
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/streaming_responses_api.go` around lines 382 - 393, The code
appends a generatedImage using state.turn.ID() without guarding against
state.turn being nil, which can panic; in the handler that processes
streamEvent.Response.Output (the block that checks output.Type ==
"image_generation_call" and uses imgOutput.ID/Result), add a nil-check for
state.turn before calling state.turn.ID() and handle the nil case (e.g., skip
appending, use a safe default turn ID, or log and continue) so
state.pendingImages is only populated with a valid turnID; update the logic
around generatedImage creation and the use of state.turn.ID() to avoid any
dereference when state.turn == nil.
bridges/ai/client.go (1)

483-485: ⚠️ Potential issue | 🟠 Major

Add nil guard for meta before provider switch.

initProviderForLogin dereferences meta on Line 484 without validation; nil metadata will panic client init.

🔧 Suggested fix
 func initProviderForLogin(key string, meta *UserLoginMetadata, connector *OpenAIConnector, login *bridgev2.UserLogin, log zerolog.Logger) (*OpenAIProvider, error) {
+	if meta == nil {
+		return nil, errors.New("missing user login metadata")
+	}
 	switch meta.Provider {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/client.go` around lines 483 - 485, initProviderForLogin currently
dereferences meta (meta.Provider) without a nil check which can panic; add a nil
guard at the start of initProviderForLogin to validate meta is not nil and
return a clear error if it is (e.g., from initProviderForLogin or using the
existing error types/logging), then proceed to the switch on meta.Provider only
after the nil check so functions like initProviderForLogin, and callers handling
*UserLoginMetadata are safe.
approval_flow.go (1)

1150-1159: ⚠️ Potential issue | 🟠 Major

cancelPendingTimeout still allows reaper timeout races after decision delivery.

Only closing done does not remove the approval from the reaper’s timeout candidates, so a resolved approval can still be finalized as timeout later.

🔧 Minimal fix
 func (f *ApprovalFlow[D]) cancelPendingTimeout(approvalID string) {
 	approvalID = strings.TrimSpace(approvalID)
 	if approvalID == "" {
 		return
 	}
 	f.mu.Lock()
 	defer f.mu.Unlock()
 	if p := f.pending[approvalID]; p != nil {
 		p.closeDone()
+		delete(f.pending, approvalID)
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@approval_flow.go` around lines 1150 - 1159, The cancelPendingTimeout function
currently only calls p.closeDone() but leaves the entry in f.pending so the
reaper can still time it out; update cancelPendingTimeout (the method on
ApprovalFlow and its use of f.pending and p.closeDone()) to, while holding f.mu,
call p.closeDone() and then remove the entry from the pending map (e.g.,
delete(f.pending, approvalID)) so resolved approvals are no longer candidates
for the reaper timeout.
🧹 Nitpick comments (12)
bridges/ai/bridge_db.go (1)

18-22: Logging inconsistency between DB acquisition paths.

OpenAIConnector.bridgeDB() creates the child DB with ZeroLogger and proper labeling, but the fallback paths in AIClient.bridgeDB() (line 37) and bridgeDBFromLogin() (line 52) use NoopLogger. This means DB operations will have no logging when accessed through those fallback paths.

If this is intentional (e.g., to avoid duplicate logging when logger context isn't available), consider adding a brief comment. Otherwise, consider propagating a logger consistently.

🤖 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 18 - 22, The DB child is created with a
labeled ZeroLogger in OpenAIConnector.bridgeDB() but the fallback paths
AIClient.bridgeDB() and bridgeDBFromLogin() use NoopLogger, causing
inconsistent/no logging; update those fallback paths to construct the child DB
using dbutil.ZeroLogger(oc.br.Log.With().Str("db_section",
"agentremote").Logger()) (or propagate the same logger/context used in
OpenAIConnector.bridgeDB()) so DB operations always get a consistent labeled
logger, or if NoopLogger is intentional add a brief comment in
AIClient.bridgeDB() and bridgeDBFromLogin() explaining why logging is
suppressed.
bridges/ai/streaming_success.go (1)

28-33: Consider error handling and execution flow in finalization operations.

The methods called here return void but invoke operations that can fail: persistTerminalAssistantTurn calls sendFinalAssistantTurn, MessageMetadata calls EmitUIMessageMetadata, and noteStreamingPersistenceSideEffects calls savePortalQuiet (which logs but doesn't propagate errors from portal.Save()). When underlying operations fail, execution continues regardless—for persistence operations like metadata saves, this could result in inconsistent state.

While errors are logged as warnings rather than completely silent, they don't halt subsequent operations. Consider whether some of these—especially persistence operations—should short-circuit on failure rather than continue to the next step.

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

In `@bridges/ai/streaming_success.go` around lines 28 - 33, The finalization
sequence in streaming_success.go currently calls persistTerminalAssistantTurn,
state.writer().MessageMetadata, and oc.noteStreamingPersistenceSideEffects but
ignores failures; change these APIs to surface errors and short-circuit on
failure: update persistTerminalAssistantTurn, MessageMetadata (or wrap its
EmitUIMessageMetadata call), and noteStreamingPersistenceSideEffects to return
an error (or return the underlying portal.Save error instead of swallowing it),
then in this finalization path check each returned error, log it with context,
and return early (do not proceed to maybeGenerateTitle or recordProviderSuccess)
when persistence/metadata/save operations fail to avoid inconsistent state.
Ensure recordProviderSuccess is only invoked after all critical persistence
steps succeed.
bridges/ai/subagent_spawn.go (1)

247-250: Optional cleanup: simplify typed nil initialization.

Line 247 can be simplified without behavior change.

Refactor suggestion
-	defaultSubagents := (*agentconfig.SubagentConfig)(nil)
+	var defaultSubagents *agentconfig.SubagentConfig
🤖 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 247 - 250, Replace the explicit
typed nil initialization for defaultSubagents with a simple pointer var
declaration: instead of assigning (*agentconfig.SubagentConfig)(nil) to
defaultSubagents, declare defaultSubagents as a nil *agentconfig.SubagentConfig
using a var statement; keep the subsequent conditional that assigns
oc.connector.Config.Agents.Defaults.Subagents unchanged (references:
defaultSubagents, agentconfig.SubagentConfig, oc.connector,
oc.connector.Config.Agents.Defaults.Subagents).
bridges/ai/chat_login_redirect_test.go (1)

64-73: Exercise the encoded model ID through the full candidate-resolution path.

This currently proves candidateModelLookupIDs() decodes and that resolveModelIDFromManifest() accepts the canonical ID, but it never asserts the combined encoded-input flow. A regression in the caller glue would still pass.

♻️ Proposed test tightening
 func TestResolveModelIDFromManifestAcceptsEncodedModelIDViaCandidates(t *testing.T) {
 	const encoded = "google%2Fgemini-2.0-flash-lite-001"
 	candidates := candidateModelLookupIDs(encoded)
 	const canonical = "google/gemini-2.0-flash-lite-001"
 	if !slices.Contains(candidates, canonical) {
 		t.Fatalf("expected decoded model candidate in %#v", candidates)
 	}
-	if got := resolveModelIDFromManifest(canonical); got != canonical {
-		t.Fatalf("expected canonical candidate %q to resolve via manifest, got %q", canonical, got)
+	resolved := ""
+	for _, candidate := range candidates {
+		if got := resolveModelIDFromManifest(candidate); got != "" {
+			resolved = got
+			break
+		}
+	}
+	if resolved != canonical {
+		t.Fatalf("expected encoded model ID %q to resolve via candidates to %q, got %q", encoded, canonical, resolved)
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/chat_login_redirect_test.go` around lines 64 - 73, The test
currently verifies candidateModelLookupIDs decodes the encoded string and that
resolveModelIDFromManifest accepts the canonical ID, but doesn't verify the
end-to-end flow; update
TestResolveModelIDFromManifestAcceptsEncodedModelIDViaCandidates to take the
encoded input, obtain candidates via candidateModelLookupIDs(encoded), then
exercise resolveModelIDFromManifest against those candidates (e.g., iterate
candidates and assert that one of their resolveModelIDFromManifest results
equals the canonical "google/gemini-2.0-flash-lite-001"), failing the test if
none of the candidate resolutions produce the canonical ID.
bridges/ai/response_finalization.go (1)

571-571: Unused parameter _ zerolog.Logger in persistTerminalAssistantTurn.

The log parameter is ignored (named _). If logging is not needed in this function, consider removing the parameter entirely to simplify the API, or document why it's preserved for interface compatibility.

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

In `@bridges/ai/response_finalization.go` at line 571, The
persistTerminalAssistantTurn function currently declares an unused parameter `_
zerolog.Logger`; remove this unused parameter from the function signature of
persistTerminalAssistantTurn and update all call sites (and any interface
declarations) that pass a logger to it so they no longer supply that argument,
or alternatively use the logger inside the function if logging is required—refer
to the function name persistTerminalAssistantTurn and callers to ensure the
signature change is applied consistently across the codebase.
bridges/ai/portal_send.go (1)

95-123: Add nil check for oc in redactViaPortal for consistency.

Unlike sendViaPortalWithTiming and sendEditViaPortalWithTiming which guard against oc == nil, redactViaPortal does not. While oc being nil is unlikely in practice, the inconsistency could mask issues during debugging.

♻️ Suggested fix for consistency
 func (oc *AIClient) redactViaPortal(
 	ctx context.Context,
 	portal *bridgev2.Portal,
 	targetMsgID networkid.MessageID,
 ) error {
+	if oc == nil || oc.UserLogin == nil {
+		return fmt.Errorf("client unavailable")
+	}
 	if portal == nil || portal.MXID == "" {
 		return fmt.Errorf("invalid portal")
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/portal_send.go` around lines 95 - 123, The function
redactViaPortal lacks the nil-receiver guard present in sendViaPortalWithTiming
and sendEditViaPortalWithTiming; add an early check at the top of
redactViaPortal that returns an error if oc == nil (e.g., "invalid AI client" or
matching existing message used elsewhere), then proceed to call
oc.senderForPortal and oc.UserLogin.QueueRemoteEvent as before to keep behavior
consistent and avoid panics when dereferencing oc.
bridges/ai/streaming_ui_helpers.go (2)

30-54: visibleStreamingText has redundant fallback paths.

The function first checks turn.VisibleText(), then falls back to parsing the UI message snapshot. If turn.VisibleText() is non-empty, it returns early. However, if it's empty and the snapshot also yields nothing, the function returns empty string. The snapshot-based path duplicates logic that turn.VisibleText() should already encapsulate. Consider verifying if the snapshot fallback is necessary.

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

In `@bridges/ai/streaming_ui_helpers.go` around lines 30 - 54, The function
visibleStreamingText contains a redundant fallback: it first uses
turn.VisibleText() and then re-parses the UI snapshot via
streamui.SnapshotUIMessage plus sdk.TurnDataFromUIMessage to recreate the same
visible text; remove the duplicative snapshot parsing path and rely on
turn.VisibleText() as the single source of truth (inside visibleStreamingText
and any callers using streamingState) or, if a fallback is truly required,
centralize that logic in streamingState.turn.VisibleText() so you only call
turn.VisibleText() here and drop streamui.SnapshotUIMessage,
sdk.TurnDataFromUIMessage, and the manual Parts aggregation.

69-82: Metadata pruning creates a new map with explicit key list.

When !includeUsage, a new map is created with only specific keys. This means any new metadata fields added to buildCanonicalTurnData won't appear in non-usage contexts unless this list is updated. Consider using a deny-list approach (removing usage-specific keys) rather than an allow-list to be more future-proof.

♻️ Alternative approach using deny-list
 	if !includeUsage && len(metadata) > 0 {
-		metadata = map[string]any{
-			"turn_id":           metadata["turn_id"],
-			"agent_id":          metadata["agent_id"],
-			"model":             metadata["model"],
-			"finish_reason":     metadata["finish_reason"],
-			"response_id":       metadata["response_id"],
-			"response_status":   metadata["response_status"],
-			"started_at_ms":     metadata["started_at_ms"],
-			"first_token_at_ms": metadata["first_token_at_ms"],
-			"completed_at_ms":   metadata["completed_at_ms"],
-		}
+		// Clone and remove usage-specific fields
+		pruned := make(map[string]any, len(metadata))
+		for k, v := range metadata {
+			pruned[k] = v
+		}
+		delete(pruned, "prompt_tokens")
+		delete(pruned, "completion_tokens")
+		delete(pruned, "reasoning_tokens")
+		delete(pruned, "total_tokens")
+		metadata = pruned
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/streaming_ui_helpers.go` around lines 69 - 82, The current pruning
in the function that returns metadata recreates a new map with an explicit
allow-list when !includeUsage, which will miss any future fields added by
buildCanonicalTurnData; change this to a deny-list approach by copying the
original metadata map (or iterating its keys) and removing only the
usage-specific keys (e.g., "usage", "total_tokens", etc.) instead of rebuilding
with the explicit keys shown in the block that references includeUsage and
metadata; update the logic around the metadata variable so new fields added by
buildCanonicalTurnData are preserved automatically in non-usage contexts.
bridges/ai/system_events_db.go (2)

31-34: Fallback to "beeper" is redundant given agents.DefaultAgentID is already "beeper".

The normalizeAgentID(agents.DefaultAgentID) call with DefaultAgentID = "beeper" (per pkg/agents/beeper.go:36) will always return a non-empty string, making the fallback on lines 32-34 unreachable. If the intent is to guard against future changes to DefaultAgentID, consider documenting that assumption; otherwise, this dead code can be removed.

♻️ Suggested simplification
-	agentID := normalizeAgentID(agents.DefaultAgentID)
-	if agentID == "" {
-		agentID = "beeper"
-	}
+	agentID := normalizeAgentID(agents.DefaultAgentID) // DefaultAgentID is "beeper"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/system_events_db.go` around lines 31 - 34, The conditional
fallback to "beeper" is dead code because
normalizeAgentID(agents.DefaultAgentID) is guaranteed non-empty; remove the if
block and directly assign agentID := normalizeAgentID(agents.DefaultAgentID) in
system_events_db.go (or, if you want to explicitly guard against future changes,
replace the fallback with a short comment referencing agents.DefaultAgentID and
normalizeAgentID to explain the invariant). Ensure edits target the code that
uses normalizeAgentID and agents.DefaultAgentID so no unreachable branches
remain.

26-41: Agent ID is hardcoded to a single value, preventing per-agent data isolation even though the database schema supports it.

The systemEventsScope function hardcodes agents.DefaultAgentID rather than using a dynamic value. While the database schema includes agent_id in its partitioning queries (DELETE, INSERT, SELECT), all records will use the same agent ID regardless of which agent initiated the operation. This means system events for all agents within a bridge/login pair share the same persistence partition. If multi-agent isolation becomes a requirement, the callers (persistSystemEventsSnapshot and restoreSystemEventsFromDB) would need to accept or derive agent-specific context to pass through to this function.

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

In `@bridges/ai/system_events_db.go` around lines 26 - 41, systemEventsScope
currently hardcodes agents.DefaultAgentID via
normalizeAgentID(agents.DefaultAgentID) which forces all system events into one
agent partition; change systemEventsScope to accept an agentID parameter (or
derive it from the AIClient if it carries per-agent context), use
normalizeAgentID(agentID) and fall back to "beeper" only if empty, and update
callers persistSystemEventsSnapshot and restoreSystemEventsFromDB to pass the
correct agent ID (or fetch from client) so the DB queries use the intended
agent-specific partitioning.
bridges/ai/streaming_init.go (1)

64-80: Nil check in SetDebouncedEditFunc is defensive but partially redundant.

Line 65 checks oc == nil || state == nil || portal == nil, but these are captured in the closure at the time of turn creation. If they were nil, the turn creation would have already failed. The check is harmless but suggests uncertainty about the callback's execution context.

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

In `@bridges/ai/streaming_init.go` around lines 64 - 80, The nil-check inside the
SetDebouncedEditFunc closure is redundant because oc, state and portal are
captured at turn creation and cannot be nil here; remove the "if oc == nil ||
state == nil || portal == nil { return nil }" guard from the closure in
streaming_init.go so the callback directly calls
agentremote.SendDebouncedStreamEdit with the existing parameters (preserving
Login, Portal, Sender, NetworkMessageID, SuppressSend, VisibleBody,
FallbackBody, LogKey, Force, UIMessage). If you prefer to keep a safety note,
replace the check with a short comment referencing SetDebouncedEditFunc and why
the values are guaranteed non-nil instead of returning early.
approval_flow_test.go (1)

44-47: Close ApprovalFlow in tests to avoid leaking reaper goroutines.

Each NewApprovalFlow(...) starts a background reaper; these tests should register cleanup to keep the test process stable.

✅ Pattern to apply
 	flow := NewApprovalFlow(ApprovalFlowConfig[*testApprovalFlowData]{
 		Login: func() *bridgev2.UserLogin { return login },
 	})
+	t.Cleanup(flow.Close)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@approval_flow_test.go` around lines 44 - 47, NewApprovalFlow starts a
background reaper that must be stopped in tests; after creating the ApprovalFlow
instance (NewApprovalFlow) register a cleanup to call its Close method (e.g.,
t.Cleanup or defer in the test) so the reaper goroutine is stopped and not
leaked — update the test that creates flow (and any other tests constructing
ApprovalFlow) to call flow.Close() via test cleanup.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@approval_flow.go`:
- Around line 459-475: The Wait implementation currently calls clearPending
(which locks f.mu and deletes f.pending[approvalID]) both on timer expiry and on
ctx.Done(), which incorrectly mutates shared approval state when callers cancel;
change the logic so clearPending (or the removal/finalize code that calls
p.closeDone and delete(f.pending, approvalID)) is invoked only from the timeout
branch (case <-timer.C) and not from the ctx.Done() branch, and ensure the
timeout branch still performs the finalization path (calling p.closeDone and
deleting f.pending[approvalID]) so terminal timeout handling/edit flows run as
intended; update the select to return immediately on ctx.Done() without touching
f.pending, referencing clearPending, f.pending, approvalID, timer.C, and
ctx.Done().

In `@base_reaction_handler.go`:
- Around line 53-56: The HandleMatrixReactionRemove path in BaseReactionHandler
should mirror the nil/validity guards used elsewhere: check that h.Target is
non-nil, that h.Target.GetApprovalHandler() is non-nil and implements
ApprovalReactionRemoveHandler, and that msg is non-nil before calling
handler.HandleReactionRemove; if any check fails return nil (or an appropriate
no-op error) so a zero-value BaseReactionHandler or partial MatrixReactionRemove
cannot be forwarded into bridge-specific code. Locate
BaseReactionHandler.HandleMatrixReactionRemove, the Target.GetApprovalHandler
call, the ApprovalReactionRemoveHandler type and the HandleReactionRemove method
to add these defensive guards.

In `@bridges/ai/agentstore.go`:
- Line 35: LoadAgents calls loadCustomAgentsFromMetadata which accesses
meta.CustomAgents without synchronization while
saveAgentToMetadata/deleteAgentFromMetadata use s.mu for writes; wrap the
metadata read path with the same mutex to prevent concurrent map read/write
panics. Specifically, in AgentStoreAdapter.LoadAgents (or inside
loadCustomAgentsFromMetadata) acquire s.mu.RLock() before reading
meta.CustomAgents and release it after, or alternatively call
loadCustomAgentsFromMetadata only while holding s.mu.RLock(); keep
saveAgentToMetadata/deleteAgentFromMetadata unchanged so all reads and writes
use s.mu for coordination.

In `@bridges/ai/chat.go`:
- Around line 355-362: catalogAgentID(catalogAgent) may be empty but you still
treat that as a DB-miss and return a false "not found"; change the flow to check
the ID right after computing agentID and, if agentID == "", skip the DB lookup
(NewAgentStoreAdapter(...).GetAgentByID) and directly call
oc.resolveAgentIdentifier with the catalog agent (or the appropriate
catalog-agent object) to produce the non-chat resolve response; otherwise
continue with the existing DB lookup and not-found error path. Ensure you
reference catalogAgentID(catalogAgent), NewAgentStoreAdapter(oc).GetAgentByID,
and oc.resolveAgentIdentifier in the patch.

In `@bridges/ai/integrations_config.go`:
- Around line 569-577: The upgrader is missing copies for newly introduced
snake_case keys so user values get dropped; update the upgradeConfig calls to
also copy the snake_case variants for AgentDefaults and media configs by adding
helper.Copy calls for "timeout_seconds", "user_timezone", "envelope_timezone",
"envelope_timestamp", "envelope_elapsed", "typing_mode",
"typing_interval_seconds", "subagents", and the nested "heartbeat"."every" using
the same types (configupgrade.Int/Str/Map) as the existing camelCase entries;
ensure the same additions are made in the other upgrade block that mirrors these
entries (the block near the other media/agent defaults) so both upgrade paths
preserve the renamed keys.

In `@bridges/ai/reaction_handling.go`:
- Around line 24-29: Guard oc.UserLogin for nil before dereferencing it for
agentremote.IsMatrixBotUser, agentremote.EnsureSyntheticReactionSenderGhost, and
any DB lookups (e.g., DB.Message.GetPartByID); add the same defensive login gate
used in base_reaction_handler.go so if oc.UserLogin is nil the handler returns
early and drops the reaction instead of proceeding, and apply the same nil-check
pattern to the other occurrence mentioned (around lines 60-85) to prevent panics
when teardown/login is in progress.

In `@bridges/ai/streaming_success.go`:
- Around line 29-30: The calls to state.writer().MessageMetadata(...) and
state.turn.End(...) can panic when state.turn is nil because writer() returns
nil; guard both calls: check the result of state.writer() (via state.writer())
is non-nil before calling MessageMetadata with oc.buildUIMessageMetadata(state,
meta, true), and check state.turn != nil before calling
state.turn.End(msgconv.MapFinishReason(state.finishReason)) so both
MessageMetadata and End are only invoked when their receivers are non-nil.

---

Outside diff comments:
In `@bridges/ai/bootstrap_context_test.go`:
- Around line 116-123: The second conditional that checks
strings.EqualFold(file.Path, agents.DefaultBootstrapFilename) &&
strings.Contains(...) is dead code; remove it and either combine the checks into
a single branch or nest the content check inside the presence check: locate the
loop iterating over files and the usages of agents.DefaultBootstrapFilename and
strings.Contains, then replace the two separate ifs with one that first tests
strings.EqualFold(file.Path, agents.DefaultBootstrapFilename) and inside that
block call strings.Contains(file.Content, "[MISSING]") to fail with a specific
message if present, otherwise fail for presence as before.

In `@bridges/ai/reactions.go`:
- Around line 15-22: The sendReaction helper can panic when the user disconnects
because it dereferences oc.UserLogin / oc.UserLogin.Bridge /
oc.UserLogin.Bridge.DB and oc.UserLogin.ID without checking them; add an early
guard at the top of sendReaction that returns if oc.UserLogin is nil,
oc.UserLogin.ID is empty, oc.UserLogin.Bridge is nil, or oc.UserLogin.Bridge.DB
is nil (the same guard should be added before queuing any BuildReactionEvent
call path that uses oc.UserLogin.ID) so the function becomes a no-op on
disconnect instead of panicking.

In `@bridges/ai/scheduler_cron.go`:
- Around line 366-372: The current check uses target.Portal == nil which doesn't
catch a typed-nil *bridgev2.Portal; update the block around
resolveCronDeliveryTarget and sendPlainAssistantMessage to perform a type
assertion to *bridgev2.Portal (e.g., portal, ok :=
target.Portal.(*bridgev2.Portal)) and then ensure ok && portal != nil before
calling s.client.sendPlainAssistantMessage(runCtx, portal, body); keep the
existing RoomID check and return paths unchanged if the portal is invalid.

---

Duplicate comments:
In `@approval_flow.go`:
- Around line 1150-1159: The cancelPendingTimeout function currently only calls
p.closeDone() but leaves the entry in f.pending so the reaper can still time it
out; update cancelPendingTimeout (the method on ApprovalFlow and its use of
f.pending and p.closeDone()) to, while holding f.mu, call p.closeDone() and then
remove the entry from the pending map (e.g., delete(f.pending, approvalID)) so
resolved approvals are no longer candidates for the reaper timeout.

In `@bridges/ai/client.go`:
- Around line 483-485: initProviderForLogin currently dereferences meta
(meta.Provider) without a nil check which can panic; add a nil guard at the
start of initProviderForLogin to validate meta is not nil and return a clear
error if it is (e.g., from initProviderForLogin or using the existing error
types/logging), then proceed to the switch on meta.Provider only after the nil
check so functions like initProviderForLogin, and callers handling
*UserLoginMetadata are safe.

In `@bridges/ai/streaming_responses_api.go`:
- Around line 382-393: The code appends a generatedImage using state.turn.ID()
without guarding against state.turn being nil, which can panic; in the handler
that processes streamEvent.Response.Output (the block that checks output.Type ==
"image_generation_call" and uses imgOutput.ID/Result), add a nil-check for
state.turn before calling state.turn.ID() and handle the nil case (e.g., skip
appending, use a safe default turn ID, or log and continue) so
state.pendingImages is only populated with a valid turnID; update the logic
around generatedImage creation and the use of state.turn.ID() to avoid any
dereference when state.turn == nil.

---

Nitpick comments:
In `@approval_flow_test.go`:
- Around line 44-47: NewApprovalFlow starts a background reaper that must be
stopped in tests; after creating the ApprovalFlow instance (NewApprovalFlow)
register a cleanup to call its Close method (e.g., t.Cleanup or defer in the
test) so the reaper goroutine is stopped and not leaked — update the test that
creates flow (and any other tests constructing ApprovalFlow) to call
flow.Close() via test cleanup.

In `@bridges/ai/bridge_db.go`:
- Around line 18-22: The DB child is created with a labeled ZeroLogger in
OpenAIConnector.bridgeDB() but the fallback paths AIClient.bridgeDB() and
bridgeDBFromLogin() use NoopLogger, causing inconsistent/no logging; update
those fallback paths to construct the child DB using
dbutil.ZeroLogger(oc.br.Log.With().Str("db_section", "agentremote").Logger())
(or propagate the same logger/context used in OpenAIConnector.bridgeDB()) so DB
operations always get a consistent labeled logger, or if NoopLogger is
intentional add a brief comment in AIClient.bridgeDB() and bridgeDBFromLogin()
explaining why logging is suppressed.

In `@bridges/ai/chat_login_redirect_test.go`:
- Around line 64-73: The test currently verifies candidateModelLookupIDs decodes
the encoded string and that resolveModelIDFromManifest accepts the canonical ID,
but doesn't verify the end-to-end flow; update
TestResolveModelIDFromManifestAcceptsEncodedModelIDViaCandidates to take the
encoded input, obtain candidates via candidateModelLookupIDs(encoded), then
exercise resolveModelIDFromManifest against those candidates (e.g., iterate
candidates and assert that one of their resolveModelIDFromManifest results
equals the canonical "google/gemini-2.0-flash-lite-001"), failing the test if
none of the candidate resolutions produce the canonical ID.

In `@bridges/ai/portal_send.go`:
- Around line 95-123: The function redactViaPortal lacks the nil-receiver guard
present in sendViaPortalWithTiming and sendEditViaPortalWithTiming; add an early
check at the top of redactViaPortal that returns an error if oc == nil (e.g.,
"invalid AI client" or matching existing message used elsewhere), then proceed
to call oc.senderForPortal and oc.UserLogin.QueueRemoteEvent as before to keep
behavior consistent and avoid panics when dereferencing oc.

In `@bridges/ai/response_finalization.go`:
- Line 571: The persistTerminalAssistantTurn function currently declares an
unused parameter `_ zerolog.Logger`; remove this unused parameter from the
function signature of persistTerminalAssistantTurn and update all call sites
(and any interface declarations) that pass a logger to it so they no longer
supply that argument, or alternatively use the logger inside the function if
logging is required—refer to the function name persistTerminalAssistantTurn and
callers to ensure the signature change is applied consistently across the
codebase.

In `@bridges/ai/streaming_init.go`:
- Around line 64-80: The nil-check inside the SetDebouncedEditFunc closure is
redundant because oc, state and portal are captured at turn creation and cannot
be nil here; remove the "if oc == nil || state == nil || portal == nil { return
nil }" guard from the closure in streaming_init.go so the callback directly
calls agentremote.SendDebouncedStreamEdit with the existing parameters
(preserving Login, Portal, Sender, NetworkMessageID, SuppressSend, VisibleBody,
FallbackBody, LogKey, Force, UIMessage). If you prefer to keep a safety note,
replace the check with a short comment referencing SetDebouncedEditFunc and why
the values are guaranteed non-nil instead of returning early.

In `@bridges/ai/streaming_success.go`:
- Around line 28-33: The finalization sequence in streaming_success.go currently
calls persistTerminalAssistantTurn, state.writer().MessageMetadata, and
oc.noteStreamingPersistenceSideEffects but ignores failures; change these APIs
to surface errors and short-circuit on failure: update
persistTerminalAssistantTurn, MessageMetadata (or wrap its EmitUIMessageMetadata
call), and noteStreamingPersistenceSideEffects to return an error (or return the
underlying portal.Save error instead of swallowing it), then in this
finalization path check each returned error, log it with context, and return
early (do not proceed to maybeGenerateTitle or recordProviderSuccess) when
persistence/metadata/save operations fail to avoid inconsistent state. Ensure
recordProviderSuccess is only invoked after all critical persistence steps
succeed.

In `@bridges/ai/streaming_ui_helpers.go`:
- Around line 30-54: The function visibleStreamingText contains a redundant
fallback: it first uses turn.VisibleText() and then re-parses the UI snapshot
via streamui.SnapshotUIMessage plus sdk.TurnDataFromUIMessage to recreate the
same visible text; remove the duplicative snapshot parsing path and rely on
turn.VisibleText() as the single source of truth (inside visibleStreamingText
and any callers using streamingState) or, if a fallback is truly required,
centralize that logic in streamingState.turn.VisibleText() so you only call
turn.VisibleText() here and drop streamui.SnapshotUIMessage,
sdk.TurnDataFromUIMessage, and the manual Parts aggregation.
- Around line 69-82: The current pruning in the function that returns metadata
recreates a new map with an explicit allow-list when !includeUsage, which will
miss any future fields added by buildCanonicalTurnData; change this to a
deny-list approach by copying the original metadata map (or iterating its keys)
and removing only the usage-specific keys (e.g., "usage", "total_tokens", etc.)
instead of rebuilding with the explicit keys shown in the block that references
includeUsage and metadata; update the logic around the metadata variable so new
fields added by buildCanonicalTurnData are preserved automatically in non-usage
contexts.

In `@bridges/ai/subagent_spawn.go`:
- Around line 247-250: Replace the explicit typed nil initialization for
defaultSubagents with a simple pointer var declaration: instead of assigning
(*agentconfig.SubagentConfig)(nil) to defaultSubagents, declare defaultSubagents
as a nil *agentconfig.SubagentConfig using a var statement; keep the subsequent
conditional that assigns oc.connector.Config.Agents.Defaults.Subagents unchanged
(references: defaultSubagents, agentconfig.SubagentConfig, oc.connector,
oc.connector.Config.Agents.Defaults.Subagents).

In `@bridges/ai/system_events_db.go`:
- Around line 31-34: The conditional fallback to "beeper" is dead code because
normalizeAgentID(agents.DefaultAgentID) is guaranteed non-empty; remove the if
block and directly assign agentID := normalizeAgentID(agents.DefaultAgentID) in
system_events_db.go (or, if you want to explicitly guard against future changes,
replace the fallback with a short comment referencing agents.DefaultAgentID and
normalizeAgentID to explain the invariant). Ensure edits target the code that
uses normalizeAgentID and agents.DefaultAgentID so no unreachable branches
remain.
- Around line 26-41: systemEventsScope currently hardcodes agents.DefaultAgentID
via normalizeAgentID(agents.DefaultAgentID) which forces all system events into
one agent partition; change systemEventsScope to accept an agentID parameter (or
derive it from the AIClient if it carries per-agent context), use
normalizeAgentID(agentID) and fall back to "beeper" only if empty, and update
callers persistSystemEventsSnapshot and restoreSystemEventsFromDB to pass the
correct agent ID (or fetch from client) so the DB queries use the intended
agent-specific partitioning.
🪄 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: 460b2245-c1ca-46ea-9975-95195f5dc7e5

📥 Commits

Reviewing files that changed from the base of the PR and between 628d921 and b85c071.

📒 Files selected for processing (31)
  • README.md
  • approval_flow.go
  • approval_flow_test.go
  • base_reaction_handler.go
  • bridges/ai/agentstore.go
  • bridges/ai/bootstrap_context_test.go
  • bridges/ai/bridge_db.go
  • bridges/ai/chat.go
  • bridges/ai/chat_login_redirect_test.go
  • bridges/ai/client.go
  • bridges/ai/constructors.go
  • bridges/ai/handlematrix.go
  • bridges/ai/integrations_config.go
  • bridges/ai/integrations_example-config.yaml
  • bridges/ai/logout_cleanup.go
  • bridges/ai/portal_send.go
  • bridges/ai/reaction_handling.go
  • bridges/ai/reactions.go
  • bridges/ai/response_finalization.go
  • bridges/ai/scheduler_cron.go
  • bridges/ai/scheduler_db.go
  • bridges/ai/session_store.go
  • bridges/ai/streaming_init.go
  • bridges/ai/streaming_lifecycle_cluster_test.go
  • bridges/ai/streaming_response_lifecycle.go
  • bridges/ai/streaming_responses_api.go
  • bridges/ai/streaming_state.go
  • bridges/ai/streaming_success.go
  • bridges/ai/streaming_ui_helpers.go
  • bridges/ai/subagent_spawn.go
  • bridges/ai/system_events_db.go
✅ Files skipped from review due to trivial changes (1)
  • bridges/ai/integrations_example-config.yaml
🚧 Files skipped from review as they are similar to previous changes (3)
  • bridges/ai/logout_cleanup.go
  • bridges/ai/constructors.go
  • bridges/ai/session_store.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). (4)
  • GitHub Check: Lint
  • GitHub Check: build-docker
  • GitHub Check: build-docker
  • GitHub Check: Lint
🧰 Additional context used
🧬 Code graph analysis (13)
approval_flow_test.go (3)
approval_flow.go (4)
  • NewApprovalFlow (125-151)
  • ApprovalFlowConfig (33-63)
  • Pending (66-71)
  • SendPromptParams (772-776)
approval_decision.go (2)
  • ApprovalDecisionPayload (21-26)
  • ApprovalReasonAllowOnce (10-10)
approval_prompt.go (3)
  • ApprovalPromptRegistration (469-483)
  • ApprovalPromptMessageParams (258-267)
  • ApprovalPromptPresentation (46-50)
bridges/ai/chat_login_redirect_test.go (1)
bridges/ai/client.go (1)
  • AIClient (265-344)
bridges/ai/reaction_handling.go (2)
approval_reaction_helpers.go (3)
  • PreHandleApprovalReaction (75-88)
  • EnsureSyntheticReactionSenderGhost (26-52)
  • ExtractReactionContext (97-112)
matrix_helpers.go (1)
  • IsMatrixBotUser (39-48)
bridges/ai/reactions.go (1)
reaction_helpers.go (1)
  • BuildReactionEvent (35-63)
bridges/ai/streaming_ui_helpers.go (2)
sdk/turn_data.go (2)
  • TurnDataFromUIMessage (95-147)
  • UIMessageFromTurnData (160-228)
pkg/shared/stringutil/coalesce.go (1)
  • StringValue (29-38)
bridges/ai/streaming_response_lifecycle.go (2)
bridges/codex/metadata.go (2)
  • PortalMetadata (32-41)
  • MessageMetadata (43-46)
sdk/types.go (1)
  • Message (29-44)
bridges/ai/system_events_db.go (1)
pkg/agents/beeper.go (1)
  • DefaultAgentID (37-37)
bridges/ai/agentstore.go (3)
pkg/agents/tools/boss.go (2)
  • AgentData (97-109)
  • ModelData (112-117)
bridges/ai/client.go (1)
  • AIClient (265-344)
pkg/agents/agentconfig/subagent.go (1)
  • CloneSubagentConfig (15-27)
bridges/ai/subagent_spawn.go (4)
pkg/agents/agentconfig/subagent.go (1)
  • SubagentConfig (8-12)
identifier_helpers.go (2)
  • NewEventID (20-22)
  • MatrixMessageID (15-17)
bridges/opencode/message_metadata.go (1)
  • MessageMetadata (10-22)
message_metadata.go (1)
  • BaseMessageMetadata (7-23)
bridges/ai/streaming_lifecycle_cluster_test.go (3)
bridges/ai/errors.go (1)
  • PreDeltaError (61-63)
pkg/shared/streamui/recorder.go (1)
  • SnapshotUIMessage (212-217)
pkg/shared/streamui/emitter.go (1)
  • UIState (14-36)
bridges/ai/portal_send.go (3)
bridges/ai/client.go (1)
  • AIClient (265-344)
bridges/opencode/bridge.go (1)
  • Bridge (74-79)
client_base.go (1)
  • ClientBase (14-25)
bridges/ai/streaming_responses_api.go (2)
bridges/opencode/api/types.go (1)
  • Event (206-209)
bridges/ai/errors.go (2)
  • ContextLengthError (50-54)
  • PreDeltaError (61-63)
bridges/ai/integrations_config.go (3)
pkg/agents/agentconfig/subagent.go (1)
  • SubagentConfig (8-12)
bridges/ai/typing_mode.go (1)
  • TypingMode (10-10)
pkg/runtime/types.go (1)
  • ReplyToMode (61-61)
🔇 Additional comments (36)
bridges/ai/bridge_db.go (3)

1-1: Package rename aligns with PR objectives.

The rename from connector to ai is part of the intentional API migration documented in this PR. Downstream callers will need to update their imports.


20-20: Label change from "ai_bridge" to "agentremote" is consistent with the rename.

This aligns with the broader naming normalization (bridgeadapter → agentremote) documented in the PR objectives.


42-55: Nil-safety and type assertion handling looks correct.

The function properly checks for nil login, handles the type assertion with the ,ok idiom, and has appropriate fallback logic.

bridges/ai/bootstrap_context_test.go (5)

1-18: Package rename and imports look correct.

The package rename from connector to ai aligns with the PR objectives. Imports are appropriate for the test functionality.


31-43: Table name change aligns with PR objectives.

The rename from ai_memory_files to aichats_memory_files is consistent with the DB table rename strategy (ai_* → aichats_*) documented in the PR.


50-79: Test logic is correct.

The test properly verifies that buildBootstrapContextFiles returns files including a valid SOUL.md without placeholder content.


130-158: Test correctly verifies bootstrap file injection on new workspaces.

The test has good coverage, checking for file presence, absence of placeholder markers, and non-empty content.


160-172: Good edge case test for template parsing.

The test properly verifies that optional placeholder hints don't incorrectly trigger bootstrap deletion logic.

bridges/ai/agentstore.go (3)

189-220: Boss-facing adapter API additions look clean.

LoadBossAgents/SaveBossAgent/ListBossModels are straightforward conversions and preserve existing AgentStoreAdapter behavior without duplicating logic.


318-350: Embedding migration and client-path rewiring are consistent.

The BossStoreAdapter embedding change and downstream call-path updates (b.client..., delegated adapter methods, and command execution wiring) are internally consistent and improve cohesion.

Also applies to: 362-403, 517-553, 590-612


650-650: Good defensive copy on subagent config mapping.

Using agentconfig.CloneSubagentConfig in both conversion directions avoids shared mutable state across store/tool boundaries.

Also applies to: 669-669

bridges/ai/streaming_success.go (1)

1-11: LGTM!

Package declaration and imports are appropriate for the method's functionality.

bridges/ai/integrations_config.go (1)

488-599: Nice migration coverage for most normalized keys.

The snake_case normalization and corresponding upgradeConfig copy rules are largely in sync across approvals/messages/channels/session, which makes config behavior more predictable.

bridges/ai/subagent_spawn.go (7)

1-1: Package rename to ai is consistent.

No concerns on the file-level package migration.


16-18: agentremote / agentconfig import migration is coherent.

These imports match the updated type usage in this file.


57-75: Subagent model/thinking fallback precedence remains correct.

Line 58/68 still prioritize per-agent subagent settings before defaults, preserving expected override behavior.


318-321: Room materialization consolidation looks good.

Using oc.materializePortalRoom(...) with explicit options improves lifecycle consistency and error handling in this path.


328-330: Non-fatal room rename failure handling is appropriate.

Line 328 keeps spawn flow resilient by logging rename failures instead of aborting the run.


333-350: Event ID and message metadata migration is internally consistent.

Line 333, Line 344, and Line 349 all use agentremote helpers/types, which keeps ID/metadata format aligned across the new API surface.


353-353: This concern is based on an architectural misunderstanding. Each message stores only its own turn's canonical data (via promptTail(promptContext, 1) extracting the current user message), not the full context. The full prompt context passed to setCanonicalTurnDataFromPromptMessages is used for agent dispatch via promptContextToDispatchMessages, but only the current turn is stored in message metadata. Replay/backfill reconstructs full context by reading accumulated message history from the database, not by attempting to reconstruct it from a single message's metadata. The tail-only canonicalization is the correct design pattern—confirmed by consistent usage across six call sites (handlematrix.go, subagent_spawn.go, internal_dispatch.go, client.go) and the test suite.

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

10-30: LGTM on the unauthenticated client coverage.

Zero-value AIClient instances are a good fit here and should keep the login-required fast-fail path from regressing.

bridges/ai/streaming_response_lifecycle.go (1)

19-34: LGTM: response.completed now stays on the metadata path.

Keeping completion in the lifecycle switch while still reserving Error(...) for response.failed cleanly preserves the final metadata/status emission.

Also applies to: 51-64

bridges/ai/scheduler_db.go (1)

49-50: No action required. The schema migration for aichats_cron_jobs, aichats_managed_heartbeats, aichats_cron_job_run_keys, and aichats_managed_heartbeat_run_keys is already defined in pkg/aidb/001-init.sql with CREATE TABLE IF NOT EXISTS statements, ensuring safe creation on both fresh installs and upgrades.

bridges/ai/portal_send.go (1)

45-62: LGTM!

The timing-enabled send path correctly validates prerequisites, separates sender resolution from the inline argument evaluation (addressing the past review concern), and delegates to ClientBase.SendViaPortalWithOptions with proper timing parameters.

bridges/ai/handlematrix.go (2)

267-277: LGTM!

Message construction correctly uses the new agentremote types (MatrixMessageID, BaseMessageMetadata, MatrixEventTimestamp) and properly sets canonical turn data from the prompt context.


1012-1023: Remove incorrect argument count reference.

The agentremote.BuildReactionEvent function takes 10 parameters (not 11), and the code call correctly passes all 10 arguments in the proper order: portalKey, sender, targetID, emoji, emojiID, timestamp, streamOrder, logKey, dbMeta (nil), and extraContent (nil). The parameter order matches the function signature exactly.

			> Likely an incorrect or invalid review comment.
bridges/ai/streaming_state.go (2)

86-96: LGTM - Past review issue addressed.

hasInitialMessageTarget() now correctly returns true when either an edit target or an ephemeral target exists, fixing the regression flagged in the previous review.


105-116: No data race on lastStreamOrder—the streaming flow is entirely synchronous.

The code executes sequentially: SetSendFunc callback runs once during ensureStarted() (guarded by a started flag), then finalization runs after streaming completes. All calls to nextMessageTiming() happen serially, not concurrently. No goroutines spawn that could cause concurrent access to lastStreamOrder or startedAtMs.

			> Likely an incorrect or invalid review comment.
bridges/ai/streaming_lifecycle_cluster_test.go (3)

15-45: Good test coverage for context-length error finalization.

The test verifies that handleStreamStepError correctly:

  1. Identifies context-length errors
  2. Sets finishReason to "context-length"
  3. Sets completedAtMs timestamp
  4. Wraps the error in PreDeltaError

117-163: LGTM - Comprehensive response status test.

This test verifies the full lifecycle from in_progresscompleted status transitions, checking both state.responseStatus and the UI message metadata. Good coverage of the status propagation path.


16-16: Helper newTestStreamingStateWithTurn is properly defined and initialized.

The helper is defined in bridges/ai/streaming_error_handling_test.go (line 15) and correctly initializes the streaming state with a mock turn by creating a conversation, starting a turn, and assigning it to the state object before returning.

bridges/ai/streaming_init.go (2)

38-43: Past review flagged potential race on state.turn access - verify synchronization.

The FinalMetadataProvider callback (lines 38-40) captures state and may be invoked from a different goroutine when the turn finalizes. If state.turn or other fields are accessed concurrently during finalization, this could cause a data race. The past review suggested adding synchronization. Verify if this has been addressed or if the access pattern guarantees single-threaded execution.

#!/bin/bash
# Check if streamingState has mutex or atomic protection
rg -n 'sync\.(Mutex|RWMutex)|atomic\.' bridges/ai/streaming_state.go
# Check FinalMetadataProvider invocation context
rg -n 'FinalMetadataProvider' bridges/ai/ -A 5

104-181: prepareStreamingRun is well-structured with proper cleanup semantics.

The function:

  1. Initializes state with source event context
  2. Creates the SDK turn with appropriate hooks
  3. Sets up typing controller with configurable modes
  4. Returns a cleanup function that must be deferred

The cleanup function properly marks the typing controller complete. Good separation of concerns.

bridges/ai/response_finalization.go (3)

45-59: LGTM - Continuation messages now preserve reply context.

The sendContinuationMessage function now accepts replyTarget and timing parameters, and attaches m.relates_to to overflow chunks. This addresses the past review concern about long replies spilling into the room timeline as standalone events.


583-588: LGTM - Markdown split now operates on original source.

The code now splits markdown (the original source text) at line 585 before rendering, rather than splitting rendered.Body which was the double-transformation issue flagged in the past review. Each chunk is then rendered independently via format.RenderMarkdown.


633-642: Each continuation chunk gets a unique timing.

state.nextMessageTiming() is called for the main edit (line 633) and again for each continuation chunk in the loop (line 656). This ensures proper stream ordering. However, if multiple continuation chunks exist, they'll all have incrementing StreamOrder values, which is correct for ordering but worth noting.

Comment on lines +569 to 577
helper.Copy(configupgrade.Int, "agents", "defaults", "timeout_seconds")
helper.Copy(configupgrade.Str, "agents", "defaults", "user_timezone")
helper.Copy(configupgrade.Str, "agents", "defaults", "envelope_timezone")
helper.Copy(configupgrade.Str, "agents", "defaults", "envelope_timestamp")
helper.Copy(configupgrade.Str, "agents", "defaults", "envelope_elapsed")
helper.Copy(configupgrade.Str, "agents", "defaults", "typing_mode")
helper.Copy(configupgrade.Int, "agents", "defaults", "typing_interval_seconds")
helper.Copy(configupgrade.Map, "agents", "defaults", "subagents")
helper.Copy(configupgrade.Str, "agents", "defaults", "heartbeat", "every")
Copy link

@coderabbitai coderabbitai bot Mar 16, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Config upgrader misses renamed keys, causing silent config-loss on upgrade.

AgentDefaultsConfig and media configs now expose snake_case keys, but upgradeConfig still omits several of them. Existing user values for these keys will be dropped during config upgrade.

🛠️ Proposed fix
 	// Agents heartbeat configuration
+	helper.Copy(configupgrade.Bool, "agents", "defaults", "skip_bootstrap")
+	helper.Copy(configupgrade.Int, "agents", "defaults", "bootstrap_max_chars")
+	helper.Copy(configupgrade.Map, "agents", "defaults", "soul_evil")
 	helper.Copy(configupgrade.Int, "agents", "defaults", "timeout_seconds")
 	helper.Copy(configupgrade.Str, "agents", "defaults", "user_timezone")
 	helper.Copy(configupgrade.Str, "agents", "defaults", "envelope_timezone")
@@
 	helper.Copy(configupgrade.Int, "tools", "media", "image", "max_bytes")
 	helper.Copy(configupgrade.Int, "tools", "media", "image", "max_chars")
 	helper.Copy(configupgrade.Int, "tools", "media", "image", "timeout_seconds")
 	helper.Copy(configupgrade.Int, "tools", "media", "audio", "max_bytes")
+	helper.Copy(configupgrade.Int, "tools", "media", "audio", "max_chars")
 	helper.Copy(configupgrade.Int, "tools", "media", "audio", "timeout_seconds")
 	helper.Copy(configupgrade.Int, "tools", "media", "video", "max_bytes")
+	helper.Copy(configupgrade.Int, "tools", "media", "video", "max_chars")
 	helper.Copy(configupgrade.Int, "tools", "media", "video", "timeout_seconds")

Also applies to: 627-633

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

In `@bridges/ai/integrations_config.go` around lines 569 - 577, The upgrader is
missing copies for newly introduced snake_case keys so user values get dropped;
update the upgradeConfig calls to also copy the snake_case variants for
AgentDefaults and media configs by adding helper.Copy calls for
"timeout_seconds", "user_timezone", "envelope_timezone", "envelope_timestamp",
"envelope_elapsed", "typing_mode", "typing_interval_seconds", "subagents", and
the nested "heartbeat"."every" using the same types (configupgrade.Int/Str/Map)
as the existing camelCase entries; ensure the same additions are made in the
other upgrade block that mirrors these entries (the block near the other
media/agent defaults) so both upgrade paths preserve the renamed keys.

Copy link
Member Author

Choose a reason for hiding this comment

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

https://github.com/coderabbitai can you resolve this if it can be resolved safely?

Copy link
Member Author

Choose a reason for hiding this comment

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

https://github.com/coderabbitai can you resolve this if it can be resolved safely?

Copy link

Choose a reason for hiding this comment

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

@batuhan Thanks — this can be resolved safely. The upgrader is missing a few snake_case copies; adding them is backward-compatible and prevents silent config loss during upgrades.

Here’s a minimal patch to bridges/ai/integrations_config.go that only augments upgradeConfig with the missing keys:

  • Add copies for:
    • agents.defaults.skip_bootstrap (Bool)
    • agents.defaults.bootstrap_max_chars (Int)
    • agents.defaults.soul_evil (Map)
    • tools.media.audio.max_chars (Int)
    • tools.media.video.max_chars (Int)

If you’d like, I can open a follow-up PR/commit with this patch.


🧠 Learnings used
Learnt from: batuhan
Repo: beeper/agentremote PR: 71
File: bridges/ai/connector.go:53-63
Timestamp: 2026-03-16T09:01:17.958Z
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.

Failed to handle agent chat message. Please try again.

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

@batuhan Thanks — I inspected the upgrader and this is safely fixable.

What I verified

  • Agents defaults: skip_bootstrap, bootstrap_max_chars, and soul_evil are not currently copied in upgradeConfig.
  • Media tools: image.max_chars is copied, but audio.max_chars and video.max_chars are missing.

Safe fix

  • Add copy rules for the 5 missing keys. This only preserves user values during upgrade; it doesn’t change runtime behavior for users who don’t set them.

Plan to patch

  • Add helper.Copy lines in bridges/ai/integrations_config.go within upgradeConfig:
    • agents.defaults.skip_bootstrap (Bool)
    • agents.defaults.bootstrap_max_chars (Int)
    • agents.defaults.soul_evil (Map)
    • tools.media.audio.max_chars (Int)
    • tools.media.video.max_chars (Int)

If you’d like, I can push this patch via a follow-up commit on this PR.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate upgradeConfig =="
rg -nP 'func\s+upgradeConfig\(' bridges/ai/integrations_config.go -n -C3 || true

echo
echo "== Check if agents.defaults.{skip_bootstrap,bootstrap_max_chars,soul_evil} already copied =="
rg -nP 'agents", "defaults", "(skip_bootstrap|bootstrap_max_chars|soul_evil)"' bridges/ai/integrations_config.go -n -C1 || true

echo
echo "== Check if tools.media.(audio|video).max_chars already copied =="
rg -nP '"tools", "media", "(audio|video)", "max_chars"' bridges/ai/integrations_config.go -n -C1 || true

echo
echo "== Show nearby existing media copy lines =="
rg -nP '"tools", "media", "(image|audio|video)"' bridges/ai/integrations_config.go -n -C2 || true

Length of output: 1307


@coderabbit-user Confirmed missing upgrader copies and it’s safe to fix. I verified there are no existing helper.Copy lines for:

  • agents.defaults.skip_bootstrap, bootstrap_max_chars, soul_evil
  • tools.media.audio.max_chars, tools.media.video.max_chars

The media section sits around Lines 627–633; we’ll extend it there. This only preserves user values during upgrade and does not affect runtime behavior otherwise.

  • Add missing helper.Copy entries in bridges/ai/integrations_config.go within upgradeConfig:
    • agents.defaults.skip_bootstrap (Bool)
    • agents.defaults.bootstrap_max_chars (Int)
    • agents.defaults.soul_evil (Map)
    • tools.media.audio.max_chars (Int)
    • tools.media.video.max_chars (Int)

If you’re good with this, I’ll proceed to apply the patch to this PR.


🧠 Learnings used
Learnt from: batuhan
Repo: beeper/agentremote PR: 71
File: bridges/ai/connector.go:53-63
Timestamp: 2026-03-16T09:01:17.958Z
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.

Failed to handle agent chat message. Please try again.

Change agent temperature from float64 to *float64 across the codebase so unset vs explicit-zero can be distinguished. Update data types, cloning (ptr.Clone), and consumers (providers, request builders, provisioning, tooling, and agent store) to handle pointer temperatures and preserve explicit zero values.

Other fixes and improvements included:
- ApprovalFlow: skip already-resolved pending approvals in reaper logic and Wait(), and add test to ensure resolving prevents later timeout.
- AI client/session management: fix client cache eviction/publish locking to avoid double-unlock and ensure proper Disconnect behavior when replacing clients; add test helpers for login metadata.
- Pending queue/steering: ensure non-followup queues remain untouched and preserve steering base input; adjust getFollowUpMessages to avoid mutating queue snapshot prematurely and add tests.
- Request/response builders and providers: honor explicit zero temperatures when building OpenAI Chat/Responses params and add tests.
- Media understanding: simplify provider selection for openrouter generation paths.
- Streaming: make generated-image turnID capture nil-safe.
- Init: set PortalEventBuffer for AI bridge startup paths.

Adds/uses go.mau.fi/util/ptr helper and several tests to validate new behaviors.
@batuhan
Copy link
Member Author

batuhan commented Mar 16, 2026

@coderabbitai can do a review again, everything you mentioned so far should be fixed.

@coderabbitai
Copy link

coderabbitai bot commented Mar 16, 2026

Oops, something went wrong! Please try again later. 🐰 💔

Copy link

@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: 3

♻️ Duplicate comments (3)
bridges/ai/agentstore.go (1)

36-36: ⚠️ Potential issue | 🔴 Critical

Synchronize CustomAgents reads with the metadata write lock.

Line 36 still reaches read paths that access meta.CustomAgents without locking, while writes are locked (Line 89, Line 101). This remains vulnerable to concurrent map read and map write panics.

🔧 Suggested fix
 type AgentStoreAdapter struct {
 	client *AIClient
-	mu     sync.Mutex // protects read-modify-write operations on custom agents
+	mu     sync.RWMutex // protects all access to custom agents metadata
 }

 func (s *AgentStoreAdapter) loadCustomAgentsFromMetadata() map[string]*AgentDefinitionContent {
+	s.mu.RLock()
+	defer s.mu.RUnlock()
+
 	meta := loginMetadata(s.client.UserLogin)
 	if meta == nil || len(meta.CustomAgents) == 0 {
 		return nil
 	}
 	result := make(map[string]*AgentDefinitionContent, len(meta.CustomAgents))
 	for id, agent := range meta.CustomAgents {
 		if agent == nil {
 			continue
 		}
 		result[id] = agent
 	}
 	return result
 }

 func (s *AgentStoreAdapter) loadCustomAgentFromMetadata(agentID string) *AgentDefinitionContent {
+	s.mu.RLock()
+	defer s.mu.RUnlock()
+
 	meta := loginMetadata(s.client.UserLogin)
 	if meta == nil || meta.CustomAgents == nil {
 		return nil
 	}
 	return meta.CustomAgents[agentID]
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/agentstore.go` at line 36, LoadAgents reads meta.CustomAgents
without synchronization while writers use the metadata write lock; wrap any read
accesses to meta.CustomAgents in the metadata's read lock (e.g., call the same
RWMutex's RLock() / RUnlock() used by writers) inside
AgentStoreAdapter.LoadAgents so reads and writes use the same lock; ensure you
obtain the read lock before referencing meta.CustomAgents and release it after
finishing the read.
bridges/ai/client.go (1)

483-485: ⚠️ Potential issue | 🟠 Major

Add a nil guard for provider metadata before switch.

Line 484 dereferences meta.Provider directly. If meta is nil, this panics during client init.

🔧 Minimal fix
 func initProviderForLogin(key string, meta *UserLoginMetadata, connector *OpenAIConnector, login *bridgev2.UserLogin, log zerolog.Logger) (*OpenAIProvider, error) {
+	if meta == nil {
+		return nil, errors.New("missing user login metadata")
+	}
 	switch meta.Provider {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/client.go` around lines 483 - 485, The function
initProviderForLogin dereferences meta.Provider without checking meta for nil;
add a nil guard at the start of initProviderForLogin (check if meta == nil) and
return a clear error (or fallback to a default Provider) instead of proceeding
to the switch. Locate initProviderForLogin and ensure you validate meta before
accessing meta.Provider so the function returns an error early when metadata is
missing and avoids a panic.
approval_flow.go (1)

488-504: ⚠️ Potential issue | 🟠 Major

Don’t let Wait() tear down shared approval state.

Line 503 deletes f.pending[approvalID] when any waiter’s context is canceled, so one aborted waiter can invalidate the approval for everyone else. The timeout branch has the same problem: it drops the pending entry without going through finishTimedOutApproval(), so the prompt never transitions to its timeout state.

Suggested change
-	clearPending := func() {
-		f.mu.Lock()
-		defer f.mu.Unlock()
-		if p := f.pending[approvalID]; p != nil {
-			p.closeDone()
-			delete(f.pending, approvalID)
-		}
-	}
 	select {
 	case d := <-p.ch:
 		return d, true
 	case <-timer.C:
-		clearPending()
+		f.finishTimedOutApproval(approvalID)
 		return zero, false
 	case <-ctx.Done():
-		clearPending()
 		return zero, false
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@approval_flow.go` around lines 488 - 504, The Wait() implementation is
deleting the shared pending entry (f.pending[approvalID]) from inside the select
branches (timer.C and ctx.Done()), which lets one waiter's timeout or
cancellation tear down approval state for all waiters and bypasses
finishTimedOutApproval(); instead, remove the deletion from clearPending and
ensure the timeout branch calls finishTimedOutApproval(approvalID) while leaving
f.pending[approvalID] intact for other waiters, and make the ctx.Done() branch
only unregister this specific waiter (or return without mutating f.pending) so
other waiters remain unaffected; update or remove clearPending accordingly and
keep any shared-state cleanup centralized inside the existing finish/cleanup
routines.
🧹 Nitpick comments (3)
bridges/ai/defaults_alignment_test.go (1)

18-42: Consider adding a test for non-zero temperature values.

The current tests cover nil (unset) and 0.0 (explicit zero). Adding a test with a non-zero temperature (e.g., 0.7) would provide additional confidence that the pointer value is correctly propagated, not just checked for nil-ness.

🧪 Example test case
func TestEffectiveTemperatureUsesExplicitNonZero(t *testing.T) {
	client := &AIClient{
		connector: &OpenAIConnector{},
		UserLogin: &bridgev2.UserLogin{UserLogin: &database.UserLogin{Metadata: &UserLoginMetadata{
			CustomAgents: map[string]*AgentDefinitionContent{
				"agent-1": {
					ID:          "agent-1",
					Name:        "Agent One",
					Model:       "openai/gpt-5.2",
					Temperature: ptr.Ptr(0.7),
				},
			},
		}}},
	}
	meta := &PortalMetadata{
		ResolvedTarget: &ResolvedTarget{
			Kind:    ResolvedTargetAgent,
			AgentID: "agent-1",
		},
	}

	got := client.effectiveTemperature(meta)
	if got == nil || *got != 0.7 {
		t.Fatalf("expected temperature 0.7, got %#v", got)
	}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bridges/ai/defaults_alignment_test.go` around lines 18 - 42, Add a test that
verifies non-zero temperatures are propagated: create a new test (e.g.,
TestEffectiveTemperatureUsesExplicitNonZero) that constructs an AIClient with a
UserLogin whose Metadata.CustomAgents contains an AgentDefinitionContent for
"agent-1" with Temperature set to a non-zero pointer (e.g., ptr.Ptr(0.7)), set
PortalMetadata.ResolvedTarget to that agent, call
client.effectiveTemperature(meta) and assert the returned pointer is non-nil and
equals 0.7; reference AIClient, effectiveTemperature, AgentDefinitionContent and
Temperature when locating where to mirror the existing zero-value test.
bridges/ai/provisioning.go (1)

236-262: Consider simplifying: normalizeAgentUpsertRequest always returns nil error.

The function signature returns error, but no code path can produce a non-nil error. The error return could be removed to simplify the API, or this may be intentional to allow future validation logic.

♻️ Optional: Remove unused error return
-func normalizeAgentUpsertRequest(req agentUpsertRequest, pathID string) (*agents.AgentDefinition, error) {
+func normalizeAgentUpsertRequest(req agentUpsertRequest, pathID string) *agents.AgentDefinition {
 	agentID := strings.TrimSpace(pathID)
 	// ... rest of function ...
-	return FromAgentDefinitionContent(content), nil
+	return FromAgentDefinitionContent(content)
 }

This would also require updating callers at lines 384, 416 to not check for error.

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

In `@bridges/ai/provisioning.go` around lines 236 - 262,
normalizeAgentUpsertRequest currently declares a second error return but never
returns a non-nil error; either remove the unused error from its signature or
add real validation that can produce an error. If you choose removal, change the
signature of normalizeAgentUpsertRequest to return (*agents.AgentDefinition)
only and update every caller that currently does "def, err :=
normalizeAgentUpsertRequest(...)" to accept the single return value and drop
error checks; alternatively, if you keep the error, add validation logic (e.g.,
validate Model/SystemPrompt/Name) inside normalizeAgentUpsertRequest and return
an appropriate error when validation fails so callers can handle it.
approval_flow_test.go (1)

44-46: Close each ApprovalFlow in these tests.

NewApprovalFlow() starts a reaper goroutine immediately. Without t.Cleanup(flow.Close), this file leaves one background goroutine per test behind, which can trip goleak-style checks and make the timing assertions noisier. Please apply the same cleanup to the other constructors below.

Suggested change
 flow := NewApprovalFlow(ApprovalFlowConfig[*testApprovalFlowData]{
 	Login: func() *bridgev2.UserLogin { return login },
 })
+t.Cleanup(flow.Close)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@approval_flow_test.go` around lines 44 - 46, The tests create ApprovalFlow
instances with NewApprovalFlow(...) which starts a background reaper goroutine;
update each test to register cleanup to close the flow (e.g., call
t.Cleanup(flow.Close)) immediately after constructing the flow so the goroutine
is stopped when the test finishes; locate all uses of NewApprovalFlow and
ApprovalFlow in this file (including other constructors mentioned) and add
t.Cleanup(flow.Close) after each creation to prevent leaked goroutines.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@approval_flow.go`:
- Around line 94-97: The code currently never removes entries from
resolvedByEventID/resolvedByMsgID (inserted in rememberResolvedPromptLocked),
causing unbounded memory growth; add expiry-based pruning tied to the prompt's
Prompt.ExpiresAt by storing that expiry timestamp in resolvedApprovalPrompt and
implementing a pruneExpiredResolvedPromptsLocked() helper that deletes entries
whose expiry is in the past, then call that helper from
rememberResolvedPromptLocked (and from any lookup path that checks resolved
maps) or run it periodically in a background goroutine; ensure you use the same
mutex as rememberResolvedPromptLocked when mutating
resolvedByEventID/resolvedByMsgID and update any code that relies on resolved
entries to handle missing (pruned) entries gracefully.
- Around line 274-307: The reaper collects approval IDs then unlocks, which can
let a new approval with the same ID be registered and incorrectly be finalized
by finishTimedOutApproval; modify reapExpired to capture a lightweight
generation token for each expired entry (e.g., the pending instance pointer or a
PromptVersion/expiry timestamp from pending[aid] and/or promptsByApproval[aid])
and store pairs (aid, generation), then after unlocking iterate those pairs and,
before calling finishTimedOutApproval, re-acquire the lock and verify the
current pending/promptsByApproval still match the saved generation (and still
appear expired); only call finishTimedOutApproval(aid) if the re-checked
generation matches, otherwise skip.

In `@bridges/ai/streaming_responses_api.go`:
- Around line 93-107: Replace the manual reconstruction of state.baseInput (the
loops over pendingOutputs and approvalInputs) with the already-built
continuation input used when sending continuation params (use the continuation
input returned by buildContinuationParams or the variable holding it) so the
order and flags match; if you must keep manual construction, ensure
approvalInputs are appended before pendingOutputs (approvals first) and stop
hardcoding the final flag in buildFunctionCallOutputItem—pass the actual
final/complete flag from the pending output (e.g., output.final or the
appropriate field) and preserve output.arguments handling as-is.

---

Duplicate comments:
In `@approval_flow.go`:
- Around line 488-504: The Wait() implementation is deleting the shared pending
entry (f.pending[approvalID]) from inside the select branches (timer.C and
ctx.Done()), which lets one waiter's timeout or cancellation tear down approval
state for all waiters and bypasses finishTimedOutApproval(); instead, remove the
deletion from clearPending and ensure the timeout branch calls
finishTimedOutApproval(approvalID) while leaving f.pending[approvalID] intact
for other waiters, and make the ctx.Done() branch only unregister this specific
waiter (or return without mutating f.pending) so other waiters remain
unaffected; update or remove clearPending accordingly and keep any shared-state
cleanup centralized inside the existing finish/cleanup routines.

In `@bridges/ai/agentstore.go`:
- Line 36: LoadAgents reads meta.CustomAgents without synchronization while
writers use the metadata write lock; wrap any read accesses to meta.CustomAgents
in the metadata's read lock (e.g., call the same RWMutex's RLock() / RUnlock()
used by writers) inside AgentStoreAdapter.LoadAgents so reads and writes use the
same lock; ensure you obtain the read lock before referencing meta.CustomAgents
and release it after finishing the read.

In `@bridges/ai/client.go`:
- Around line 483-485: The function initProviderForLogin dereferences
meta.Provider without checking meta for nil; add a nil guard at the start of
initProviderForLogin (check if meta == nil) and return a clear error (or
fallback to a default Provider) instead of proceeding to the switch. Locate
initProviderForLogin and ensure you validate meta before accessing meta.Provider
so the function returns an error early when metadata is missing and avoids a
panic.

---

Nitpick comments:
In `@approval_flow_test.go`:
- Around line 44-46: The tests create ApprovalFlow instances with
NewApprovalFlow(...) which starts a background reaper goroutine; update each
test to register cleanup to close the flow (e.g., call t.Cleanup(flow.Close))
immediately after constructing the flow so the goroutine is stopped when the
test finishes; locate all uses of NewApprovalFlow and ApprovalFlow in this file
(including other constructors mentioned) and add t.Cleanup(flow.Close) after
each creation to prevent leaked goroutines.

In `@bridges/ai/defaults_alignment_test.go`:
- Around line 18-42: Add a test that verifies non-zero temperatures are
propagated: create a new test (e.g.,
TestEffectiveTemperatureUsesExplicitNonZero) that constructs an AIClient with a
UserLogin whose Metadata.CustomAgents contains an AgentDefinitionContent for
"agent-1" with Temperature set to a non-zero pointer (e.g., ptr.Ptr(0.7)), set
PortalMetadata.ResolvedTarget to that agent, call
client.effectiveTemperature(meta) and assert the returned pointer is non-nil and
equals 0.7; reference AIClient, effectiveTemperature, AgentDefinitionContent and
Temperature when locating where to mirror the existing zero-value test.

In `@bridges/ai/provisioning.go`:
- Around line 236-262: normalizeAgentUpsertRequest currently declares a second
error return but never returns a non-nil error; either remove the unused error
from its signature or add real validation that can produce an error. If you
choose removal, change the signature of normalizeAgentUpsertRequest to return
(*agents.AgentDefinition) only and update every caller that currently does "def,
err := normalizeAgentUpsertRequest(...)" to accept the single return value and
drop error checks; alternatively, if you keep the error, add validation logic
(e.g., validate Model/SystemPrompt/Name) inside normalizeAgentUpsertRequest and
return an appropriate error when validation fails so callers can handle it.
🪄 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: 377c9cca-1d8c-43e1-9649-73eaf6f80fa0

📥 Commits

Reviewing files that changed from the base of the PR and between b85c071 and c25e20b.

📒 Files selected for processing (24)
  • approval_flow.go
  • approval_flow_test.go
  • bridges/ai/agent_loop_request_builders.go
  • bridges/ai/agent_loop_request_builders_test.go
  • bridges/ai/agent_loop_runtime.go
  • bridges/ai/agent_loop_steering_test.go
  • bridges/ai/agentstore.go
  • bridges/ai/client.go
  • bridges/ai/client_find_model_info_test.go
  • bridges/ai/connector.go
  • bridges/ai/constructors.go
  • bridges/ai/defaults_alignment_test.go
  • bridges/ai/events.go
  • bridges/ai/login_loaders.go
  • bridges/ai/login_loaders_test.go
  • bridges/ai/media_understanding_runner.go
  • bridges/ai/pending_queue.go
  • bridges/ai/provider.go
  • bridges/ai/provider_openai_chat.go
  • bridges/ai/provider_openai_responses.go
  • bridges/ai/provider_openai_responses_test.go
  • bridges/ai/provisioning.go
  • bridges/ai/streaming_continuation.go
  • bridges/ai/streaming_responses_api.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • bridges/ai/provider_openai_responses_test.go
  • bridges/ai/client_find_model_info_test.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). (4)
  • GitHub Check: build-docker
  • GitHub Check: Lint
  • GitHub Check: Lint
  • GitHub Check: build-docker
🧰 Additional context used
🧬 Code graph analysis (14)
bridges/ai/events.go (1)
pkg/matrixevents/matrixevents.go (1)
  • AIRoomInfoEventType (18-18)
bridges/ai/streaming_continuation.go (1)
bridges/codex/metadata.go (1)
  • PortalMetadata (32-41)
bridges/ai/agent_loop_request_builders_test.go (5)
bridges/ai/login.go (1)
  • ProviderOpenRouter (22-22)
bridges/ai/metadata.go (1)
  • ModelCache (18-22)
bridges/ai/events.go (2)
  • ModelInfo (102-119)
  • AgentDefinitionContent (128-147)
pkg/agents/types.go (1)
  • ModelInfo (148-165)
bridges/ai/identifiers.go (3)
  • ResolvedTarget (145-150)
  • ResolvedTargetModel (141-141)
  • ResolvedTargetAgent (142-142)
bridges/ai/connector.go (4)
connector_builder.go (1)
  • ConnectorBase (32-35)
sdk/types.go (1)
  • Config (219-301)
sdk/connector_helpers.go (1)
  • ApplyDefaultCommandPrefix (29-33)
bridges/ai/login.go (4)
  • ProviderBeeper (20-20)
  • ProviderMagicProxy (23-23)
  • FlowCustom (24-24)
  • OpenAILogin (43-48)
bridges/ai/provider_openai_responses.go (1)
sdk/prompt_context.go (3)
  • PromptContextToResponsesInput (241-384)
  • PromptContext (14-18)
  • HasUnsupportedResponsesPromptContext (555-557)
bridges/ai/pending_queue.go (4)
bridges/ai/client.go (1)
  • AIClient (265-344)
pkg/runtime/queue_policy.go (1)
  • ResolveQueueBehavior (39-52)
bridges/opencode/api/types.go (1)
  • Event (206-209)
pkg/runtime/types.go (1)
  • QueueDropSummarize (87-87)
approval_flow_test.go (4)
approval_flow.go (3)
  • ApprovalFlowConfig (33-63)
  • Pending (66-71)
  • SendPromptParams (801-805)
approval_decision.go (2)
  • ApprovalDecisionPayload (21-26)
  • ApprovalReasonAllowOnce (10-10)
approval_prompt.go (3)
  • ApprovalPromptRegistration (469-483)
  • ApprovalPromptMessageParams (258-267)
  • ApprovalPromptPresentation (46-50)
approval_reaction_helpers.go (1)
  • MatrixSenderID (16-21)
bridges/ai/agent_loop_request_builders.go (4)
bridges/ai/client.go (1)
  • AIClient (265-344)
pkg/agents/tools/types.go (1)
  • Tool (12-18)
pkg/agents/boss.go (1)
  • IsBossAgent (26-28)
pkg/agents/tools/boss.go (2)
  • BossTools (337-347)
  • SessionTools (350-360)
bridges/ai/agent_loop_runtime.go (3)
bridges/ai/client.go (1)
  • AIClient (265-344)
bridges/opencode/api/types.go (1)
  • Event (206-209)
bridges/ai/errors.go (1)
  • ContextLengthError (50-54)
bridges/ai/login_loaders_test.go (5)
bridges/ai/client.go (1)
  • AIClient (265-344)
bridges/ai/connector.go (1)
  • OpenAIConnector (35-44)
bridges/ai/login.go (1)
  • ProviderOpenAI (21-21)
bridges/ai/events.go (2)
  • AIRoomInfoEventType (26-26)
  • AIRoomInfoContent (122-124)
pkg/matrixevents/matrixevents.go (1)
  • AIRoomInfoEventType (18-18)
bridges/ai/client.go (6)
client_base.go (1)
  • ClientBase (14-25)
bridges/ai/provider_openai.go (1)
  • OpenAIProvider (24-28)
bridges/ai/messages.go (1)
  • PromptContext (29-32)
helpers.go (1)
  • BuildChatInfoWithFallback (282-288)
bridges/ai/identifiers.go (1)
  • ResolvedTarget (145-150)
sdk/prompt_context.go (4)
  • PromptContextToChatCompletionMessages (402-463)
  • AppendChatMessagesToPromptContext (54-61)
  • AppendPromptText (81-91)
  • BuildDataURL (236-238)
bridges/ai/agentstore.go (3)
pkg/agents/tools/boss.go (1)
  • AgentData (98-110)
identifier_helpers.go (1)
  • NewEventID (20-22)
pkg/agents/agentconfig/subagent.go (1)
  • CloneSubagentConfig (15-27)
approval_flow.go (6)
approval_decision.go (8)
  • ApprovalDecisionPayload (21-26)
  • ErrApprovalMissingID (30-30)
  • ErrApprovalUnknown (33-33)
  • ErrApprovalExpired (35-35)
  • ErrApprovalAlreadyHandled (36-36)
  • ApprovalErrorToastText (40-60)
  • ErrApprovalWrongRoom (34-34)
  • ErrApprovalOnlyOwner (32-32)
approval_prompt.go (3)
  • ApprovalPromptMessage (280-286)
  • ApprovalPromptRegistration (469-483)
  • ApprovalOption (31-39)
message_metadata.go (1)
  • BaseMessageMetadata (7-23)
helpers.go (2)
  • SendViaPortal (187-220)
  • SendViaPortalParams (172-183)
reaction_helpers.go (1)
  • BuildReactionEvent (35-63)
turns/debounced_edit.go (1)
  • BuildConvertedEdit (44-64)
bridges/ai/agent_loop_steering_test.go (2)
bridges/ai/client.go (1)
  • AIClient (265-344)
pkg/runtime/types.go (4)
  • QueueModeFollowup (76-76)
  • QueueModeCollect (77-77)
  • QueueDropSummarize (87-87)
  • QueueModeSteer (75-75)
🔇 Additional comments (45)
bridges/ai/events.go (5)

1-1: Package rename to ai aligns with PR objectives.

The rename from connector to ai is part of the broader API migration documented in the PR. This is a breaking change that will require downstream consumers to update their imports.


15-17: Event type registration is correctly updated.

The init() function properly registers AIRoomInfoEventType with AIRoomInfoContent{}, and both symbols are consistently defined later in the file. This ensures mautrix's TypeMap can parse the new event type during sync.


25-26: LGTM!

The AIRoomInfoEventType is correctly aliased from matrixevents.AIRoomInfoEventType with an appropriate descriptive comment.


121-124: LGTM!

The AIRoomInfoContent struct is appropriately minimal for storing lightweight room metadata. The single Type field with JSON serialization aligns with the stated purpose of identifying AI room surfaces.


138-138: Appropriate use of pointer for optional numeric field.

Changing Temperature from float64 to *float64 correctly distinguishes between "unset" (nil) and "explicitly set to zero" (0.0). This is semantically important since temperature 0.0 is a valid configuration value for deterministic model output.

bridges/ai/defaults_alignment_test.go (3)

1-1: Package rename aligns with PR objectives.

The change from connector to ai is consistent with the broader package migration mentioned in the PR description.


11-16: Test correctly validates nil return for unset temperature.

The updated assertion properly checks for nil return value, and the error message safely dereferences got only when got != nil. This aligns with the float64*float64 API change.


18-42: Good coverage for explicit zero temperature distinction.

This test validates the critical behavior change: distinguishing between an unset temperature (nil) and an explicitly set zero temperature (*float64 pointing to 0.0). The test setup is well-structured with proper agent definition and portal metadata wiring.

bridges/ai/agent_loop_runtime.go (1)

24-43: Good stream-step control flow and success-marking order.

StepStart/StepFinish lifecycle handling is clean, and the Line 29→Line 30 ordering correctly marks send success only after successful event handling. Error and context-length exits are also consistently propagated.

bridges/ai/provisioning.go (4)

1-1: Package rename aligns with PR objectives.

The rename from connector to ai is consistent with the documented breaking API changes. Downstream consumers will need to update their imports.


17-17: LGTM!

The ptr import from go.mau.fi/util is appropriately added to support the nil-aware pointer cloning for the Temperature field.


205-221: Temperature pointer type correctly distinguishes unset vs explicit zero.

The change from float64 to *float64 enables proper handling of explicit zero temperature values (for deterministic AI output) versus unset values. This aligns with the PR objective to honor explicit zero temperatures for OpenAI Chat/Responses params.

Note: This is a breaking change for API consumers who will need to pass *float64 instead of float64.


253-253: Good use of ptr.Clone to prevent pointer aliasing.

Cloning the Temperature pointer prevents unintended mutation if the original req.Temperature pointer is modified after this function returns. This is a defensive pattern for pointer fields in structs.

bridges/ai/agentstore.go (2)

262-262: Good defensive copy for Temperature pointers.

Using ptr.Clone in both conversion directions prevents shared-pointer aliasing across store/tool payload boundaries.

Also applies to: 295-295


651-652: Great call cloning subagent config and temperature in boss/tool adapters.

These clones avoid accidental cross-object mutation when data is passed between agents.AgentDefinition and tools.AgentData.

Also applies to: 670-671

bridges/ai/media_understanding_runner.go (5)

1-21: LGTM!

Package rename to ai and new bridgesdk import alias are consistent with the broader PR refactor. The import is properly utilized throughout the file.


705-727: LGTM! Past review concerns addressed.

The refactored code properly:

  1. Uses bridgesdk.BuildDataURL and bridgesdk.UserPromptContext for the new PromptContext-based flow
  2. Always routes explicit OpenRouter entries through generateWithOpenRouter (line 720 now only checks entryProvider == "openrouter")
  3. Passes capCfg and entry to preserve entry-level configuration overrides

856-876: LGTM!

Video understanding path correctly mirrors the image refactor: PromptContext construction with video blocks and proper forwarding of capCfg/entry to generateWithOpenRouter.


932-961: Well-structured helper function.

resolveOpenRouterMediaConfig cleanly encapsulates configuration resolution with proper nil safety (lines 936-938, 957-959) and correctly merges headers, resolves API keys with profile support, and handles base URL fallback. This addresses the prior review concern about preserving entry-level OpenRouter configuration.


926-929: API compatibility confirmed.

The bridgesdk.PromptContextHasBlockType function exists in sdk/prompt_context.go and correctly accepts PromptBlockAudio and PromptBlockVideo as valid PromptBlockType constants. The routing logic properly delegates to generateChatCompletions when audio or video blocks are present.

bridges/ai/streaming_continuation.go (1)

42-46: Steering persistence fix is correct.

Lines 44-45 now persist steerInput into both the current request input and state.baseInput, so steering survives later stateless continuations.

bridges/ai/client.go (1)

1645-1649: Nil-safe model cache access looks good.

The meta != nil guard before meta.ModelCache access removes a panic path in model lookup.

bridges/ai/streaming_responses_api.go (1)

384-387: Good nil-safe turn ID handling for generated images.

The guarded state.turn access avoids a nil dereference while still associating images to turn context when available.

bridges/ai/login_loaders_test.go (2)

15-25: LGTM! Typed-nil issue correctly addressed.

The helper now conditionally assigns Metadata only when meta != nil, avoiding the typed-nil interface problem flagged in a previous review.


27-98: Good test coverage for AI client lifecycle.

The tests comprehensively cover rebuild decisions, cache eviction on missing API key, client reuse semantics, and event type registration. The assertions are specific and validate expected behaviors clearly.

bridges/ai/provider_openai_responses.go (2)

34-36: Good: Temperature pointer pattern correctly implemented.

Using params.Temperature != nil correctly distinguishes between "unset" (nil) and "explicit zero" (0.0), allowing the API to use its default when temperature is unset while honoring explicit zero values.


63-65: LGTM! Unsupported prompt context validation now consistent across both paths.

The validation for unsupported prompt context types (audio/video) is now present in both GenerateStream and Generate, ensuring consistent fail-fast behavior regardless of streaming mode.

bridges/ai/agent_loop_request_builders_test.go (2)

65-107: Good test for explicit zero temperature preservation.

This test correctly validates that ptr.Ptr(0.0) propagates through both chat and responses builders, ensuring explicit zero is not confused with "unset". This aligns with the PR's temperature handling changes.


60-62: Type compatibility is correctly handled via explicit cast.

The cast at line 127 in agent_loop_request_builders.go (shared.ReasoningEffort(effort)) properly converts from responses.ReasoningEffort to shared.ReasoningEffort. Both types are from the same SDK (github.com/openai/openai-go/v3) but in different sub-packages. The test correctly validates that the cast produces the expected value.

bridges/ai/agent_loop_request_builders.go (2)

95-97: LGTM! Temperature pointer pattern correctly implemented.

The previous review concern about temperature > 0 preventing explicit zero has been addressed. Now using settings.temperature != nil correctly allows explicit zero values to pass through while omitting temperature when unset.


125-129: No issues found. The type casting from responses.ReasoningEffort to shared.ReasoningEffort at line 127 is compatible at runtime. Both are string type aliases from the same external openai-go library (v3.16.0), and existing tests confirm the casting works correctly.

bridges/ai/connector.go (3)

53-63: Potential nil dereference on oc.Config.Bridge.

Line 57 accesses oc.Config.Bridge.CommandPrefix without checking if Bridge is nil. If applyRuntimeDefaults() is called before config is fully loaded, this will panic.

🛡️ Proposed defensive guard
 func (oc *OpenAIConnector) applyRuntimeDefaults() {
 	if oc.Config.ModelCacheDuration == 0 {
 		oc.Config.ModelCacheDuration = 6 * time.Hour
 	}
-	bridgesdk.ApplyDefaultCommandPrefix(&oc.Config.Bridge.CommandPrefix, "!ai")
+	if oc.Config.Bridge != nil {
+		bridgesdk.ApplyDefaultCommandPrefix(&oc.Config.Bridge.CommandPrefix, "!ai")
+	}
 	if oc.Config.Pruning == nil {

75-83: LGTM! User ID validation logic.

The validation correctly handles both model and agent ghost IDs, with appropriate trimming and format checks.


42-44: The clients map is properly initialized in the constructor.

Confirmed: NewAIConnector() initializes the clients field with make(map[networkid.UserLoginID]bridgev2.NetworkAPI) at construction time. No action required.

bridges/ai/pending_queue.go (3)

215-218: Summary state not consumed after being attached to dispatch candidate.

takeQueueSummary at line 217 (and line 242) only reads the queue's droppedCount and summaryLines without clearing them. After the last real item is popped, the queue may stay alive and regenerate the same summary-only candidate on subsequent rounds.

#!/bin/bash
# Check if takeQueueSummary clears the summary state
rg -n "func.*takeQueueSummary" --type go -A 15

# Check if droppedCount or summaryLines are cleared anywhere
rg -n "droppedCount\s*=\s*0|summaryLines\s*=\s*nil" bridges/ai/pending_queue.go

336-360: LGTM! Follow-up message handling addresses prior concern.

The flow now correctly gets a snapshot first (line 340), checks behavior.Followup before any mutation (line 345), and only then calls takePendingQueueDispatchCandidate (line 348). This prevents dequeuing items for non-followup modes.


257-292: LGTM! Dispatch candidate preparation logic.

The function correctly handles both collect mode (aggregating ACK IDs and building combined prompts) and non-collect mode with synthetic summary support. The logic for falling back to message body when prompt is empty is consistent.

bridges/ai/agent_loop_steering_test.go (3)

12-53: Good test coverage for steering message filtering and draining.

The test validates key behaviors: preferring explicit prompts over message body, filtering non-text types, trimming whitespace, and ensuring the queue is drained after consumption.


90-111: Thorough test for collect mode follow-up.

The test correctly validates that multiple queued text messages are combined into a single follow-up with the expected format including headers and separators.


160-180: Important edge case test: non-followup queues remain untouched.

This test confirms that QueueModeSteer queues are not consumed by getFollowUpMessages, which is critical for correct queue behavior separation.

bridges/ai/login_loaders.go (3)

58-70: LGTM! Disconnect correctly called outside lock.

The evictCachedClient method now releases the mutex (line 66) before calling Disconnect() (line 68), addressing the prior deadlock concern.


72-94: LGTM! Client reuse logic correctly handles lock ordering.

The publishOrReuseClient method releases the mutex before calling Disconnect() on both the created client (line 80) and replace client (line 91), preventing potential deadlocks.


96-134: LGTM! Robust login loading flow.

The loadAIUserLogin method correctly handles:

  • Missing API key → evict cache and assign broken client
  • Existing client that doesn't need rebuild → reuse with bootstrap
  • Cached non-AIClient → evict before creating new
  • Client creation failure → preserve existing or assign broken client
  • Successful creation → publish/reuse and bootstrap
bridges/ai/provider.go (1)

21-29: Temperature as a pointer is a good API fit.

This cleanly preserves “unset” versus explicit 0, which the provider implementations can now honor.

bridges/ai/provider_openai_chat.go (1)

14-30: The migration is wired through cleanly here.

The prompt conversion path, strict-tool wiring, and nil-based temperature assignment all line up with the new GenerateParams contract.

batuhan added 6 commits March 16, 2026 09:25
Refactor cleanup of OpenCode-related code and test imports:

- Remove ReplyQuestion from the OpenCode API client.
- Remove OpenCodeManager.RemoveInstance and its helper cleanupInstancePortals, and drop the ErrInstanceNotFound constant.
- Remove StreamEventState struct from turns/session.go used for streaming session events.
- Reorder imports in provider_openai_responses_test.go.

These changes trim deprecated/unused instance-management and streaming scaffolding and tidy imports.
Refactor approval expiry handling and related tests; tighten AI client/provider initialization; adjust agent metadata locking; simplify provisioning agent normalization; and streamline streaming continuation input.

Highlights:
- approval_flow: add ExpiresAt to resolved prompts; rework reapExpired to collect candidates and finalize expiry checks under lock; add finalizeExpiredCandidate and support finishing timeouts with prompt version. Prune expired resolved prompts and ensure Wait finalizes timeouts instead of naively clearing state.
- approval_flow tests: add newTestApprovalFlow helper and multiple tests covering resolved prompt pruning, waiter cancellation, and wait-timeout prompt finalization; update existing tests to use the helper.
- bridges/ai: AgentStoreAdapter uses RWMutex and RLock for metadata reads to reduce contention and prevent races. initProviderForLogin now rejects nil metadata and a unit test was added. Add test for effective temperature from explicit agent settings.
- provisioning: normalizeAgentUpsertRequest no longer returns an error (simplified return type); callers adjusted to validate models afterwards and not treat normalization as fallible JSON validation.
- streaming_responses_api: use continuationParams.Input.OfInputItemList (cloned) to populate continuation base input instead of manual merging logic.

These changes fix expiry race conditions, improve correctness around prompt/version handling, add validation coverage, and simplify locking and request normalization logic.
Add defensive nil-checks and safety guards throughout the AI bridge code to avoid panics (handlers, reactions, streaming, portal send, reactions, sendReaction, turn data). Refactor bridge DB child creation into newBridgeChildDB to centralize logger handling. Simplify streaming UI helpers and metadata handling (use maps.Clone to drop usage fields, use VisibleText). Update streaming success/error flows to tolerate nil writers/turns and remove a log param from persistTerminalAssistantTurn. Rework system events persistence to include agent_id, normalize agent IDs, persist/restore per-agent snapshots, and add listing helper listPersistedSystemEventAgentIDs. Misc: small test cleanups (t.Cleanup calls) and minor config default handling.
Add consumeQueueSummary to build-and-clear a queue summary (clears droppedCount and summaryLines, deletes empty queue) and use it in pending queue dispatch paths so summaries are not reused. Update takePendingQueueDispatchCandidate to call consumeQueueSummary for both combined and synthetic summary cases. Add tests verifying collect and synthetic summaries are consumed and queue is drained after dispatch.
Copy link
Member Author

@batuhan batuhan left a comment

Choose a reason for hiding this comment

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

Submitting previously added thread replies.

Comment on lines +54 to +64
func (oc *OpenAIConnector) applyRuntimeDefaults() {
if oc.Config.ModelCacheDuration == 0 {
oc.Config.ModelCacheDuration = 6 * time.Hour
}
bridgesdk.ApplyDefaultCommandPrefix(&oc.Config.Bridge.CommandPrefix, "!ai")
if oc.Config.Pruning == nil {
oc.Config.Pruning = airuntime.DefaultPruningConfig()
} else {
oc.Config.Pruning = airuntime.ApplyPruningDefaults(oc.Config.Pruning)
}
}
Copy link
Member Author

Choose a reason for hiding this comment

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

@coderabbitai can you resolve this if it can be resolved safely?

Comment on lines +569 to 577
helper.Copy(configupgrade.Int, "agents", "defaults", "timeout_seconds")
helper.Copy(configupgrade.Str, "agents", "defaults", "user_timezone")
helper.Copy(configupgrade.Str, "agents", "defaults", "envelope_timezone")
helper.Copy(configupgrade.Str, "agents", "defaults", "envelope_timestamp")
helper.Copy(configupgrade.Str, "agents", "defaults", "envelope_elapsed")
helper.Copy(configupgrade.Str, "agents", "defaults", "typing_mode")
helper.Copy(configupgrade.Int, "agents", "defaults", "typing_interval_seconds")
helper.Copy(configupgrade.Map, "agents", "defaults", "subagents")
helper.Copy(configupgrade.Str, "agents", "defaults", "heartbeat", "every")
Copy link
Member Author

Choose a reason for hiding this comment

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

https://github.com/coderabbitai can you resolve this if it can be resolved safely?

Comment on lines +569 to 577
helper.Copy(configupgrade.Int, "agents", "defaults", "timeout_seconds")
helper.Copy(configupgrade.Str, "agents", "defaults", "user_timezone")
helper.Copy(configupgrade.Str, "agents", "defaults", "envelope_timezone")
helper.Copy(configupgrade.Str, "agents", "defaults", "envelope_timestamp")
helper.Copy(configupgrade.Str, "agents", "defaults", "envelope_elapsed")
helper.Copy(configupgrade.Str, "agents", "defaults", "typing_mode")
helper.Copy(configupgrade.Int, "agents", "defaults", "typing_interval_seconds")
helper.Copy(configupgrade.Map, "agents", "defaults", "subagents")
helper.Copy(configupgrade.Str, "agents", "defaults", "heartbeat", "every")
Copy link
Member Author

Choose a reason for hiding this comment

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

https://github.com/coderabbitai can you resolve this if it can be resolved safely?

@batuhan batuhan merged commit 4919017 into main Mar 16, 2026
4 of 6 checks passed
@batuhan batuhan deleted the batuhan/of branch March 16, 2026 09:18
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