Conversation
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (29)
Disabled knowledge base sources:
📝 WalkthroughSummary by CodeRabbit
WalkthroughAdds a generic approval/prompt subsystem in package Changes
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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes ✨ Finishing Touches
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
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 | 🟡 MinorRemove unused
stringValuefunction.The
stringValuehelper 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 | 🟡 MinorRun
goimportson 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 | 🟡 MinorRun
goimportshere 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 | 🟠 MajorTreat 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-responseandResolveExternal, 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 | 🟠 MajorChannel-based approvals can still time out after a valid reaction.
On the
deliverDecision == nilpath, a successful reaction only writes intop.ch; the prompt stays registered until the waiter later callsFinishResolved. That leaves a window whereschedulePromptTimeoutcan finalize the same approval as"timeout", and near expiryWaitcan 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 | 🟠 MajorDon't re-enable
allow_alwayswhen custom options normalize to empty.If a caller passes malformed custom options while
Presentation.AllowAlwaysisfalse, this function falls back toDefaultApprovalOptions()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
nwhen 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 renamingTestIsHostAuthLogin_SkipsRemoteLogoutfor clarity.The test name suggests it's testing remote logout behavior, but it actually validates that the
isHostAuthLoginpredicate correctly distinguishes between host and managed auth sources. A more descriptive name would beTestIsHostAuthLogin_DistinguishesManagedFromHostor 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.
ParseCursoraccepts 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
toolNameandactionarguments- Empty/nil
argsorinputmaps- Non-map input types for
buildMCPApprovalPresentationThis 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 thatCursoris backward-only, or honor it in forward mode.
PaginateParamsexposes a cursor regardless of direction, butpaginateForwardnever 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
📒 Files selected for processing (62)
bridges/codex/approvals_test.gobridges/codex/backfill.gobridges/codex/backfill_test.gobridges/codex/client.gobridges/codex/config.gobridges/codex/connector.gobridges/codex/connector_test.gobridges/codex/login.gobridges/codex/metadata.gobridges/codex/metadata_test.gobridges/codex/portal_send.gobridges/codex/stream_events.gobridges/openclaw/approval_presentation_test.gobridges/openclaw/catalog.gobridges/openclaw/client.gobridges/openclaw/events.gobridges/openclaw/gateway_client.gobridges/openclaw/gateway_client_test.gobridges/openclaw/manager.gobridges/openclaw/manager_test.gobridges/openclaw/media.gobridges/openclaw/provisioning.gobridges/openclaw/stream.gobridges/opencode/opencodebridge/approval_presentation_test.gobridges/opencode/opencodebridge/backfill.gobridges/opencode/opencodebridge/opencode_manager.gobridges/opencode/opencodebridge/opencode_media.gobridges/opencode/opencodebridge/opencode_messages.godocs/matrix-ai-matrix-spec-v1.mdpkg/bridgeadapter/approval_flow.gopkg/bridgeadapter/approval_flow_test.gopkg/bridgeadapter/approval_prompt.gopkg/bridgeadapter/approval_prompt_test.gopkg/bridgeadapter/approval_reaction_helpers.gopkg/bridgeadapter/approval_reaction_helpers_test.gopkg/bridgeadapter/base_reaction_handler.gopkg/connector/approval_prompt_presentation.gopkg/connector/approval_prompt_presentation_test.gopkg/connector/canonical_history.gopkg/connector/canonical_history_test.gopkg/connector/identifiers.gopkg/connector/image_understanding.gopkg/connector/legacy_multimodal_adapter.gopkg/connector/login.gopkg/connector/managed_beeper.gopkg/connector/media_understanding_runner.gopkg/connector/reaction_handling.gopkg/connector/streaming_chat_completions.gopkg/connector/streaming_function_calls.gopkg/connector/streaming_output_handlers.gopkg/connector/streaming_responses_api.gopkg/connector/streaming_ui_tools.gopkg/connector/strict_cleanup_test.gopkg/connector/toast.gopkg/connector/tool_approvals.gopkg/shared/backfillutil/cursor.gopkg/shared/backfillutil/pagination.gopkg/shared/backfillutil/pagination_test.gopkg/shared/backfillutil/search.gopkg/shared/backfillutil/search_test.gopkg/shared/streamui/tools.gopkg/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
legacyUnifiedMessagesNeedChatAdaptercheck streamlines the logic. The type assertion safely routes OpenAI providers togenerateChatCompletionswhile preserving the genericGeneratefallback for other providers.Note: This introduces a difference from
analyzeImageWithModel(line 238), which usesoc.provider.Generateunconditionally. 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
...SupportsLegacyAndStructuredOriginto...ParsesStructuredOriginaccurately 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
nilwhencanonicalis empty) is a deliberate simplification. All callers handle the nil/empty return safely:
handlematrix.gochecksif len(bundle) > 0before using the result- Both
client.gocalls unpack the result with..., which safely handles nil slices in GoThe behavior change is correct as implemented.
pkg/connector/login.go (1)
103-105: The review comment is incorrect. There is nolegacyManagedBeeperLoginIDfunction in the codebase. The only managed Beeper login ID function ismanagedBeeperLoginID, 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.FallbackFilenameForMIMEremoves 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.FallbackFilenameForMIMEkeeps 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.StringsTrimDefaultshould 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
EnsureUIToolInputStarton minimal metadata here avoids pushing approval-specific payloads into the generic start event.
202-240: Good to see a singlepresentationpropagated 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.StringsTrimDefaultis 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.StringsTrimDefaultis 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
CodexHomeManagedboolean toCodexAuthSource: CodexAuthSourceManagedproperly 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.StringsTrimDefaultis 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
CodexAuthSourceHostandCodexAuthSourceManagedvalues.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.StringsTrimDefaultcentralizes 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
exceptIDexclusion logic- Ensures only Codex-managed logins are matched
The test helper
hostAuthLoginIDForTestcreates a new connector per call, which is fine for test purposes.pkg/connector/toast.go (1)
21-49: LGTM!The
presentationparameter is correctly added to the function signature and properly forwarded toApprovalPromptMessageParams. 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
AppendDetailsFromMapcaps at 8 fields to prevent UI overload- The MCP builder handles both map and non-map inputs gracefully via
ValueSummarybridges/opencode/opencodebridge/backfill.go (1)
64-83: LGTM!The refactor to use
backfillutil.Paginatecentralizes pagination logic and simplifies this code. The callbacks are correctly implemented:
findAnchorIndexhandles both message and part ID matchingIndexAtOrAfterleverages binary search for efficient timestamp-based positioningThe 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:
TestCodexTurnTextPairvalidates text extraction with deduplication (using last message per ID)TestCodexThreadBackfillEntriesensures correct entry generation with ordered timestamps and unique IDsTestCodexPaginateBackfillBackwardverifies pagination cursor mechanics and batch boundariesThe assertions are clear and test the expected invariants thoroughly.
bridges/codex/connector.go (1)
268-269: No issues found. There is only one definition ofMakeUserLoginIDin the codebase (pkg/bridgeadapter/identifier_helpers.go:27), and the code correctly callsbridgeadapter.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. IfcreatedAt/updatedAtare 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 totime.Now()makes the same imported history move on every fetch. Please normalize the unit explicitly and keep the fallback deterministic.
|
Note Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it. Fixes Applied SuccessfullyFixed 10 file(s) based on 12 unresolved review comments. A stacked PR containing fixes has been created.
Time taken: |
There was a problem hiding this comment.
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 | 🟡 MinorRun the repo import formatter before merging.
CI is currently failing on this import block (
go-imports-reporeformatted 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 | 🟡 MinorGuard
inputbefore writing to it.
AddOptionalDetailpanics 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 | 🟡 MinorReturn normalized options on response messages too.
This builder normalizes
optionsfor metadata, but the returnedApprovalPromptMessagestill leavesOptionsempty. 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 | 🟡 MinorPreserve the raw map key when reading values.
Trimming
keybefore the lookup means any entry whose original key has surrounding whitespace becomes unreachable, so the detail is silently dropped even though it exists invalues.💡 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 | 🟡 MinorPreserve cancellation when the wait aborts.
waitToolApprovalalready resolves cancelled contexts withApprovalReasonCancelled, but this branch always reportstimeout. That makes the streamedtool-approval-responsedisagree 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 | 🟠 MajorHandle duplicate approval IDs before re-entering the UI flow.
registerToolApprovalcan return an existing pending approval, but this path ignorescreatedand 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: Redundantstrings.TrimSpacecall.
openclawconv.StringsTrimDefaultalready trims whitespace from the returned value, so the subsequentstrings.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: Redundantstrings.TrimSpacewrapper.Same issue as above:
openclawconv.StringsTrimDefaultalready 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
📒 Files selected for processing (17)
bridges/codex/client.gobridges/openclaw/client.gobridges/openclaw/manager.gobridges/openclaw/media_test.gobridges/opencode/opencodebridge/backfill.gobridges/opencode/opencodebridge/backfill_test.gobridges/opencode/opencodebridge/bridge.gobridges/opencode/opencodebridge/opencode_manager.gobridges/opencode/opencodebridge/opencode_parts.gopkg/bridgeadapter/approval_decision.gopkg/bridgeadapter/approval_flow.gopkg/bridgeadapter/approval_prompt.gopkg/connector/streaming_ui_tools.gopkg/connector/toast.gopkg/connector/tool_approvals.gopkg/connector/tool_execution.gopkg/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
openclawconvpackage 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
StringsTrimDefaultcalls 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.ToolDisplayTitleis well-structured. The variable is correctly used elsewhere in theconnectorpackage (e.g., instreaming_output_handlers.goline 62).bridges/opencode/opencodebridge/bridge.go (3)
3-16: LGTM!The new imports are correctly organized and necessary for the added functionality:
stringsfor validation,timefor timestamp handling,simpleventfor creating resync events, andopencodefor session types.
171-179: LGTM!The timestamp extraction logic correctly prioritizes
UpdatedoverCreatedand safely handles the case where neither is set by returning a zerotime.Time. Theint64cast is appropriate sinceopencode.Timestampis defined asint64.
181-201: LGTM!The resync event construction is well-implemented:
- Correctly uses
OpenCodePortalKeyto build the portal key with proper URL escaping.LatestMessageTSmatches the event timestamp, which is appropriate for resync semantics.queueOpenCodeSessionResynchas 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.Paginateis 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 viaindexAtOrAfter.
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
continueafter 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/SessionIDfrom the parent message when missing- Uses
buildOpenCodeConvertedPartfor part conversion with proper error handling (ErrIgnoringRemoteEvent→ skip, other errors → propagate)- Creates distinct
BackfillMessageper part with unique IDs viaopencodePartMessageID- Maintains strictly increasing stream order via the
nextOrderclosurebridges/opencode/opencodebridge/opencode_manager.go (7)
38-39: LGTM!Adding
PresentationtopermissionApprovalRefenables structured approval prompts to be passed through the approval flow, consistent with the broader approval presentation ecosystem in the codebase.
57-61: GateAllowAlwaysbased on the upstream request.The
AllowAlwaysfield is unconditionally set totrue, butopencode.PermissionRequestincludes anAlways []stringfield indicating whether "always allow" is supported for this request. SettingAllowAlways: trueregardless allows the UI to offer analwaysdecision 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.DecisionToStringcentralizes 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
hadRoomcheck pattern assyncSessions, 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
ExpiresAtcorrectly matches the 10-minute registration timeout.
938-943: LGTM!Using
ResolveExternalwith a fullApprovalDecisionPayloadproperly reports the externally-resolved decision through the approval flow, enabling correct lifecycle management. The derivation ofApproved,Always, andReasonfrom 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
Updatedtimestamp to be used (since it's > 0)- Validates portal key construction with proper receiver
- Confirms expected defaults for
StreamOrder(0) andSender(empty)
15-62: Verifynilintent handling inbuildOpenCodeConvertedPart.The test passes
nilfor theintentparameter. While the test avoids triggering the code path that requires intent (no "file" type parts),buildOpenCodeFileContentexplicitly 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 tobuildOpenCodeUserBackfillMessagesor 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
streamOrderbehavior, 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-requestemission and theapprovalFlow.SendPromptconstruction 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 increasingStreamOrdershould 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.
There was a problem hiding this comment.
Actionable comments posted: 8
♻️ Duplicate comments (1)
docs/matrix-ai-matrix-spec-v1.md (1)
310-310:⚠️ Potential issue | 🟡 MinorUse
m.reactionterminology here.
m.annotationis the relation type on the reaction, not a separate event type. This should describe bridge-authored placeholderm.reactionevents that usem.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 == nilcheck is unreachable after mutex access.The check
oc == nilon line 310 comes afteroc.StreamMu.Lock()on line 301. Ifocwere 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
📒 Files selected for processing (34)
bridges/codex/approvals_test.gobridges/codex/backfill.gobridges/codex/backfill_test.gobridges/codex/client.gobridges/codex/portal_send.gobridges/codex/stream_mapping_test.gobridges/codex/stream_transport.gobridges/codex/streaming_support.gobridges/codex/streaming_test.gobridges/openclaw/client.gobridges/openclaw/events.gobridges/openclaw/manager.gobridges/openclaw/manager_test.gobridges/openclaw/stream.gobridges/openclaw/stream_test.gobridges/opencode/client.gobridges/opencode/host.gobridges/opencode/opencodebridge/bridge.gobridges/opencode/stream_canonical.gobridges/opencode/stream_canonical_test.godocs/matrix-ai-matrix-spec-v1.mdpkg/bridgeadapter/helpers.gopkg/bridgeadapter/remote_events.gopkg/bridgeadapter/remote_events_test.gopkg/connector/remote_message.gopkg/connector/stream_events.gopkg/connector/streaming_error_handling.gopkg/connector/streaming_error_handling_test.gopkg/connector/streaming_state.gopkg/matrixevents/matrixevents.gopkg/matrixevents/matrixevents_test.gopkg/shared/streamtransport/session.gopkg/shared/streamtransport/session_target_test.gopkg/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
TargetEventIDtoRelatesToEventIDimproves clarity by directly reflecting the Matrix protocol'sm.relates_tosemantics.
98-105: LGTM!Good validation pattern: requiring a non-empty
RelatesToEventIDensures stream events always have a proper parent reference. Them.relates_tostructure correctly usesRelReferenceas the relation type.pkg/matrixevents/matrixevents_test.go (2)
19-24: LGTM!Good addition of a dedicated test case for the new
RelatesToEventIDrequirement. 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_eventfield 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
openclawconvcreates incorrect grouping per Go import conventions.
81-84: LGTM!Good consolidation of trimming logic using the shared
openclawconv.StringsTrimDefaultutility. The pattern correctly provides a fallback from the session key when the existing value is empty/whitespace.
105-105: LGTM!Consistent use of
StringsTrimDefaultfor preview snippet with appropriate fallback.
124-124: LGTM!Consistent application of
StringsTrimDefaultwith a sensible "gateway" fallback.
164-199: LGTM!Good addition of
streamOrderfield with appropriate fallback logic. Using zero as the "unset" sentinel is idiomatic for ordering values, and falling back toUnixMilli()preserves backward compatibility. The interface assertion at line 176 ensures compile-time verification.
204-239: LGTM!Consistent implementation of
streamOrderfor edit events, mirroring theOpenClawRemoteMessagepattern. 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
TimestampandStreamOrderfields 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 totime.Now()for zero-value timestamps (line 63-67 inpkg/bridgeadapter/remote_events.go). This applies to all three event types (RemoteMessage,RemoteEdit,RemoteReaction), and is part of thebridgev2.RemoteEventWithTimestampinterface contract.Callers that omit the
Timestampparameter 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.StringsTrimDefaultmakes 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
streamOrdervalues 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
denyas an approval, so tokens liketimeout,expired,cancelled, ordelivery_errorwill be emitted as allowed here. LimitApproved=truetoallow-once/allow-always, keepAlwaystied toallow-always, and propagate every other decision string as the non-approval reason.
1344-1347: Don't bypass the fallback window whenrunIDis empty.Returning
truehere 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
StreamOrderwins” 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
sendViaPortalWithOrderingmaintains backward compatibility by havingsendViaPortaldelegate 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
senderForHumanhelper correctly mirrorssenderForPortalwith appropriateIsFromMe: trueand 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
GetTargetEventIDwithGetStreamTargetandResolveTargetEventIDenables proper context propagation and DB-backed resolution while preserving the caching pattern.
96-113: LGTM! Resolver correctly caches resolved event IDs.The
resolveStreamTargetEventIDmethod properly:
- Returns cached
initialEventIDearly if available- Guards against nil receivers before DB access
- 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
openCodeStreamEventTimestampandopenCodeNextStreamOrderfor consistent ordering, and correctly caches bothnetworkMessageIDandinitialEventIDafter a successful send.
196-207: LGTM! Stream target callbacks align with Codex pattern.The
GetStreamTargetandResolveTargetEventIDcallbacks follow the same pattern introduced inbridges/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 checkingcc.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
gotPartsandgotPartTypesprevents data races when the streamEventHook is called from goroutines.
153-244: LGTM! Comprehensive test for denial flow.
TestCodex_CommandApproval_DenyEmitsResponseThenOutputDeniedproperly validates:
- Denial returns
decision=decline- Both
tool-approval-responseandtool-output-deniedparts are emitted- The parts appear in correct order (response before denied)
289-292: LGTM! Updated test to use new presentation parameter.The
registerToolApprovalcall now includes theApprovalPromptPresentationstruct, 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:
- Generated in correct chronological order
- Have strictly increasing stream order
- 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.
writeCodexRolloutTestFileandcodexRolloutTestEventprovide 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
FetchMessagesmethod.
250-260: LGTM! Conditional upstream logout for managed auth only.The check
!isHostAuthLogin(meta)correctly ensures that only per-login managed auth triggers upstreamaccount/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 toFinishResolved. This addresses the previously flagged issue.
2150-2154: DuplicaterequestApprovalhandling still not addressed.
registerToolApprovalreturns 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
sendInitialStreamMessagenow usescodexStreamEventTimestampandcodexNextLiveStreamOrderfor consistent event ordering.
1920-1941: LGTM! emitUIToolApprovalRequest refactored with structured presentation.The method now properly integrates
ApprovalPromptPresentationand delegates toapprovalFlow.SendPromptwith complete parameters includingReplyToEventIDandExpiresAt.
2184-2222: LGTM! Approval handlers use new extractInput signature with presentation.Both
handleCommandApprovalRequestandhandleFileChangeApprovalRequestcleanly build input maps and structured presentations usingAddOptionalDetail, withAllowAlways: falsefor Codex approvals.
There was a problem hiding this comment.
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 | 🟡 MinorReadability check doesn't verify actual read permission for current process.
The check
info.Mode().Perm()&0o444 == 0only verifies that some read bit is set, not that the current process can read the file. A file with0o400(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.Openfor 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 | 🟠 MajorPermitting 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 | 🟠 MajorSSRF protection has DNS rebinding vulnerability and missing IPv6 ranges.
Two issues with the current implementation:
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.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.Transportwith aDialContextthat 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 | 🟠 MajorDon't stop the fallback chain on an unresolvable configured room.
These branches return
resolveHeartbeatDeliveryRoom(...)directly. Ifheartbeat.toorheartbeat.targetcontains a stale room ID, the helper returnsReason: "no-target"and heartbeat delivery stops here instead of falling through tolastActivePortal/defaultChatPortal, unlike the laterentry.LastTopath.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 | 🟠 MajorAvoid 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 | 🟡 MinorAdd proper optional field validation for
ReasoningTokens.
CompletionTokensDetailsis an embedded struct (not a pointer) in openai-go/v3, butReasoningTokensis 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 | 🟠 MajorDon't default generic
m.fileuploads to the text-file path.Line 600 routes every
application/octet-streamor MIME-less file intohandleTextFileMessage. 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. Checkmsg.Content.FileNameor 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 | 🟠 MajorThis short-circuits the audio fallback flow.
For
event.MsgAudio, Lines 734-737 return before the!supportsMediabranch below, sodispatchMediaUnderstandingFallback(...)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 | 🟡 MinorError from
EnsureSyntheticReactionSenderGhostis 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 | 🟠 MajorInclude ephemeral target check to prevent duplicate messages.
The current condition only checks
hasEditTarget(), but if an ephemeral target hasinitialEventIDset (while the stream target lacks edit capability), the function incorrectly returnsPreDeltaError. 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.Fprintfcall 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.Numbercase is unreachable.The JSON decoder on line 74 doesn't use
UseNumber(), so numeric values are decoded asfloat64, notjson.Number. Thejson.Numbercase 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
imageGenHTTPClientwith a hardcoded 120-second timeout works for production but makes unit testing difficult. Consider injecting the client or timeout via theBridgeToolContext.🤖 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.
resolveImageGenProviderhas high cyclomatic complexity with nested switch statements. The fallback chains withinProviderBeepercould be extracted into a helper likeselectBestAvailableProvider(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 coverDBandMessagefields for consistency.The defensive nil check on line 72 validates up to
oc.UserLogin.Bridge, but line 75 accessesoc.UserLogin.Bridge.DB.Message.GetPartByMXID. While most code in the codebase accesses these fields directly, similar helper functions such aslastAssistantMessageInfoinintegration_host.goexplicitly check bothDBandMessagefor 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.
resolveHeartbeatDeliveryRoomcallsportalByRoomIDbefore any nil/readiness check onoc. Today the current caller is pre-checked, but moving theoc == 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 thebridgeadapter→agentremoterename 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 theconnector→airename 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 theconnector→airename as a breaking import change.Downstream Go code importing
github.com/beeper/agentremote/bridges/aiand 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 thebridgeadapter→agentremoterename 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: Removemax64helper in favor of builtinmaxfunction.Go 1.21 introduced a builtin
maxfunction. The project targets Go 1.25.0, so the custommax64helper at lines 156–161 can be safely removed. Replace all usages ofmax64(a, b)withmax(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
agentremoteimport 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
agentremoteimports 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
FinishResolvedandResolveExternal, and 500ms is tight enough to flake on busy CI runners. A small helper with a ~1-2s default, or one derived fromt.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 satisfieshasEditTarget()will still pass and routestreamFailureErrordown 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.Resolvedirectly 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: Missingmax_output_tokensare safely defaulted, but reliance on implicit zero-value handling could be clearer.The catalog does omit
max_output_tokensfor 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 to0in the generatedbeeper_models_generated.go. However,effectiveMaxTokens()inclient.goexplicitly checksinfo.MaxOutputTokens > 0and falls back todefaultMaxTokenswhen 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
📒 Files selected for processing (300)
approval_decision.goapproval_flow.goapproval_flow_test.goapproval_manager.goapproval_prompt.goapproval_prompt_test.goapproval_reaction_helpers.goapproval_reaction_helpers_test.gobase_connector.gobase_login_process.gobase_reaction_handler.gobase_stream_state.gobridges/ai/abort_helpers.gobridges/ai/account_hints.gobridges/ai/account_hints_test.gobridges/ai/ack_reactions.gobridges/ai/active_room_state.gobridges/ai/agent_activity.gobridges/ai/agent_contact_identifiers_test.gobridges/ai/agent_display.gobridges/ai/agents_list_tool.gobridges/ai/agentstore.gobridges/ai/agentstore_capture_test.gobridges/ai/agentstore_room_lookup.gobridges/ai/approval_prompt_presentation.gobridges/ai/approval_prompt_presentation_test.gobridges/ai/audio_analysis.gobridges/ai/audio_generation.gobridges/ai/audio_mime.gobridges/ai/beeper_models.jsonbridges/ai/beeper_models_generated.gobridges/ai/beeper_models_manifest_test.gobridges/ai/bootstrap_context.gobridges/ai/bootstrap_context_test.gobridges/ai/bridge_db.gobridges/ai/bridge_info.gobridges/ai/bridge_info_test.gobridges/ai/broken_login_client.gobridges/ai/canonical_history.gobridges/ai/canonical_history_test.gobridges/ai/canonical_prompt_messages.gobridges/ai/canonical_user_messages.gobridges/ai/chat.gobridges/ai/chat_fork_test.gobridges/ai/chat_login_redirect_test.gobridges/ai/chat_search_test.gobridges/ai/client.gobridges/ai/client_capabilities_test.gobridges/ai/client_runtime_helpers.gobridges/ai/command_aliases.gobridges/ai/command_registry.gobridges/ai/commandregistry/registry.gobridges/ai/commands.gobridges/ai/commands_helpers.gobridges/ai/commands_login_selection_test.gobridges/ai/commands_mcp_test.gobridges/ai/commands_parity.gobridges/ai/compaction_summarization.gobridges/ai/compaction_summarization_test.gobridges/ai/config_test.gobridges/ai/connector.gobridges/ai/connector_validate_userid_test.gobridges/ai/constructors.gobridges/ai/context_overrides.gobridges/ai/context_pruning_test.gobridges/ai/context_value.gobridges/ai/debounce.gobridges/ai/debounce_test.gobridges/ai/dedupe.gobridges/ai/dedupe_test.gobridges/ai/default_chat_test.gobridges/ai/defaults_alignment_test.gobridges/ai/delivery_target.gobridges/ai/desktop_api_helpers.gobridges/ai/desktop_api_native_test.gobridges/ai/desktop_api_sessions.gobridges/ai/desktop_instance_resolver_test.gobridges/ai/desktop_networks.gobridges/ai/duration.gobridges/ai/envelope_test.gobridges/ai/error_logging.gobridges/ai/errors.gobridges/ai/errors_extended.gobridges/ai/errors_test.gobridges/ai/events.gobridges/ai/events_test.gobridges/ai/gravatar.gobridges/ai/group_activation.gobridges/ai/group_history.gobridges/ai/group_history_test.gobridges/ai/handleai.gobridges/ai/handleai_test.gobridges/ai/handlematrix.gobridges/ai/handler_interfaces.gobridges/ai/heartbeat_active_hours.gobridges/ai/heartbeat_config.gobridges/ai/heartbeat_config_test.gobridges/ai/heartbeat_context.gobridges/ai/heartbeat_delivery.gobridges/ai/heartbeat_events.gobridges/ai/heartbeat_execute.gobridges/ai/heartbeat_session.gobridges/ai/heartbeat_state.gobridges/ai/heartbeat_visibility.gobridges/ai/history_limit_test.gobridges/ai/identifiers.gobridges/ai/identifiers_test.gobridges/ai/identity_sync.gobridges/ai/image_analysis.gobridges/ai/image_generation.gobridges/ai/image_generation_tool.gobridges/ai/image_generation_tool_magic_proxy_test.gobridges/ai/image_understanding.gobridges/ai/inbound_debounce.gobridges/ai/inbound_prompt_runtime_test.gobridges/ai/inbound_runtime_context.gobridges/ai/integration_host.gobridges/ai/integrations.gobridges/ai/integrations_config.gobridges/ai/integrations_example-config.yamlbridges/ai/integrations_test.gobridges/ai/internal_dispatch.gobridges/ai/linkpreview.gobridges/ai/linkpreview_test.gobridges/ai/login.gobridges/ai/login_loaders.gobridges/ai/logout_cleanup.gobridges/ai/magic_proxy_test.gobridges/ai/managed_beeper.gobridges/ai/managed_beeper_test.gobridges/ai/matrix_coupling.gobridges/ai/matrix_helpers.gobridges/ai/matrix_payload.gobridges/ai/mcp_client.gobridges/ai/mcp_client_test.gobridges/ai/mcp_helpers.gobridges/ai/mcp_servers.gobridges/ai/mcp_servers_test.gobridges/ai/media_download.gobridges/ai/media_helpers.gobridges/ai/media_prompt.gobridges/ai/media_send.gobridges/ai/media_understanding_attachments.gobridges/ai/media_understanding_cli.gobridges/ai/media_understanding_defaults.gobridges/ai/media_understanding_format.gobridges/ai/media_understanding_providers.gobridges/ai/media_understanding_resolve.gobridges/ai/media_understanding_runner.gobridges/ai/media_understanding_runner_openai_test.gobridges/ai/media_understanding_scope.gobridges/ai/media_understanding_types.gobridges/ai/mentions.gobridges/ai/message_formatting.gobridges/ai/message_pins.gobridges/ai/message_results.gobridges/ai/message_send.gobridges/ai/message_status.gobridges/ai/messages.gobridges/ai/messages_responses_input_test.gobridges/ai/metadata.gobridges/ai/metadata_test.gobridges/ai/model_api.gobridges/ai/model_catalog.gobridges/ai/model_catalog_test.gobridges/ai/model_contacts.gobridges/ai/models.gobridges/ai/models_api.gobridges/ai/models_api_test.gobridges/ai/msgconv/to_matrix.gobridges/ai/msgconv/to_matrix_test.gobridges/ai/owner_allowlist.gobridges/ai/pending_queue.gobridges/ai/portal_cleanup.gobridges/ai/portal_send.gobridges/ai/portal_send_test.gobridges/ai/prompt_params.gobridges/ai/provider.gobridges/ai/provider_openai.gobridges/ai/provider_openai_chat.gobridges/ai/provider_openai_responses.gobridges/ai/provisioning.gobridges/ai/provisioning_test.gobridges/ai/queue_helpers.gobridges/ai/queue_policy_runtime_test.gobridges/ai/queue_resolution.gobridges/ai/queue_settings.gobridges/ai/queue_status_test.gobridges/ai/reaction_feedback.gobridges/ai/reaction_handling.gobridges/ai/reactions.gobridges/ai/remote_events.gobridges/ai/remote_message.gobridges/ai/reply_mentions.gobridges/ai/reply_policy.gobridges/ai/reply_policy_runtime_test.gobridges/ai/response_finalization.gobridges/ai/response_finalization_test.gobridges/ai/response_retry.gobridges/ai/response_retry_test.gobridges/ai/room_activity.gobridges/ai/room_capabilities.gobridges/ai/room_runs.gobridges/ai/runtime_compaction_adapter.gobridges/ai/runtime_defaults_test.gobridges/ai/scheduler.gobridges/ai/scheduler_cron.gobridges/ai/scheduler_db.gobridges/ai/scheduler_events.gobridges/ai/scheduler_heartbeat.gobridges/ai/scheduler_host.gobridges/ai/scheduler_rooms.gobridges/ai/scheduler_ticks.gobridges/ai/session_greeting.gobridges/ai/session_greeting_test.gobridges/ai/session_keys.gobridges/ai/session_store.gobridges/ai/session_transcript_openclaw.gobridges/ai/session_transcript_openclaw_test.gobridges/ai/sessions_tools.gobridges/ai/sessions_visibility_test.gobridges/ai/simple_mode_prompt.gobridges/ai/simple_mode_prompt_test.gobridges/ai/source_citations.gobridges/ai/source_citations_test.gobridges/ai/status_events_context.gobridges/ai/status_text.gobridges/ai/status_text_heartbeat_test.gobridges/ai/stream_events.gobridges/ai/stream_transport.gobridges/ai/streaming_chat_completions.gobridges/ai/streaming_continuation.gobridges/ai/streaming_error_handling.gobridges/ai/streaming_error_handling_test.gobridges/ai/streaming_finish_reason_test.gobridges/ai/streaming_function_calls.gobridges/ai/streaming_init.gobridges/ai/streaming_init_test.gobridges/ai/streaming_input_conversion.gobridges/ai/streaming_output_handlers.gobridges/ai/streaming_output_items.gobridges/ai/streaming_output_items_test.gobridges/ai/streaming_params.gobridges/ai/streaming_persistence.gobridges/ai/streaming_response_lifecycle.gobridges/ai/streaming_responses_api.gobridges/ai/streaming_responses_finalize.gobridges/ai/streaming_responses_input_test.gobridges/ai/streaming_state.gobridges/ai/streaming_text_deltas.gobridges/ai/streaming_tool_selection.gobridges/ai/streaming_tool_selection_test.gobridges/ai/streaming_ui_events.gobridges/ai/streaming_ui_finish.gobridges/ai/streaming_ui_helpers.gobridges/ai/streaming_ui_sources.gobridges/ai/streaming_ui_tools.gobridges/ai/strict_cleanup_test.gobridges/ai/subagent_announce.gobridges/ai/subagent_conversion.gobridges/ai/subagent_registry.gobridges/ai/subagent_spawn.gobridges/ai/system_ack.gobridges/ai/system_events.gobridges/ai/system_events_db.gobridges/ai/system_prompts.gobridges/ai/system_prompts_test.gobridges/ai/target_test_helpers_test.gobridges/ai/text_files.gobridges/ai/timezone.gobridges/ai/toast.gobridges/ai/toast_test.gobridges/ai/token_resolver.gobridges/ai/tokenizer.gobridges/ai/tokenizer_fallback_test.gobridges/ai/tool_approvals.gobridges/ai/tool_approvals_policy.gobridges/ai/tool_approvals_policy_test.gobridges/ai/tool_approvals_rules.gobridges/ai/tool_approvals_test.gobridges/ai/tool_availability_configured_test.gobridges/ai/tool_call_id.gobridges/ai/tool_call_id_test.gobridges/ai/tool_configured.gobridges/ai/tool_descriptions.gobridges/ai/tool_execution.gobridges/ai/tool_policy.gobridges/ai/tool_policy_apply_patch_test.gobridges/ai/tool_policy_chain.gobridges/ai/tool_policy_chain_test.gobridges/ai/tool_registry.gobridges/ai/tool_schema_sanitize.gobridges/ai/tool_schema_sanitize_test.gobridges/ai/tools.gobridges/ai/tools_analyze_image.gobridges/ai/tools_apply_patch.gobridges/ai/tools_beeper_docs.gobridges/ai/tools_beeper_feedback.gobridges/ai/tools_matrix_api.gobridges/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
There was a problem hiding this comment.
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
📒 Files selected for processing (9)
approval_manager.gobridges/ai/agentstore.gobridges/ai/chat.gobridges/ai/client.gobridges/ai/connector.gobridges/ai/identifiers.gobridges/ai/response_finalization.gobridges/ai/streaming_state.gobridges/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
aialigns with the file pathbridges/ai/connector.go, and the import ofgithub.com/beeper/agentremoteis consistent with the migration frombridgeadaptertoagentremotenamespace described in the PR.Also applies to: 19-19
61-61: API migration toagentremotepackage is consistent.All six call sites have been correctly migrated from
bridgeadapter.*toagentremote.*:
EnsureClientMap- proper mutex/map initializationStopClients- correct cleanup on shutdownPrimeUserLoginCache- context and bridge passed correctlyDefaultNetworkCapabilities- simple delegationDefaultBridgeInfoVersion- simple delegationBuildMetaTypes- factory functions passed as closuresThe 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
bridgeadapterto the rootagentremotepackage is clean and consistent with the broader API surface consolidation described in the PR.
913-922: LGTM!The migration to
agentremote.BuildDMChatInfowithagentremote.DMChatInfoParamsis clean. The parameter struct fields and subsequent member map override remain consistent with the previous implementation.
984-991: LGTM!The
agentremote.BuildSystemNoticemigration 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 oldbridgeadapterdependency with the top-levelagentremoteimport 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/streamtransporttoagentremote/turnsare 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
replyToEventIDbut doesn't pass it through.Consider extending the signature to accept an optional
ReplyTargetand pass it toBuildContinuationMessageso continuation events remain in the same thread/reply chain.
74-82: LGTM!The migration to
agentremote.NewMessageIDandagentremote.BaseMessageMetadatais correctly implemented and consistent with the type definitions inmessage_metadata.go.
600-606: Splittingrendered.Bodyloses markdown formatting.
rendered.Bodyis a fallback produced byformat.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
RenderMarkdownon each chunk.
642-658: LGTM!The edit target resolution and
agentremote.RemoteEditconstruction are correctly implemented with appropriate fallback logic.
667-672: Continuation loop inherits the same splitting issue.The
continuationBodyvariable originates from therendered.Bodysplit 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 inloginMetadatais valid.
173-179:agentremote.EnsurePortalMetadatais properly exported and handles nil cases.The generic function is correctly defined in agentremote and exported. It always returns a non-nil
*T(eithernew(T)for nil portals or the result ofensureMetadata[T]), so themeta != nilcheck in line 175 is always true and could be simplified to justif portal != nil, though the current defensive approach is harmless.
135-137: Function signature is correct. TheHumanUserIDcall properly passes a string prefix andnetworkid.UserLoginID, returningnetworkid.UserIDas expected.bridges/ai/client.go (5)
1-1: Consistent package/API surface migration.The
aipackage rename andagentremoteimport 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.ApprovalFlowsetup 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.BuildChatInfoWithFallbackkeeps the fallback logic centralized instead of re-encoding it here.
2338-2340: Logger context migration looks clean.Routing through
agentremote.LoggerFromContextkeeps logging behavior aligned with the shared package.
2461-2469: Shared metadata helpers are applied consistently.Using
agentremote.MatrixMessageIDandagentremote.BaseMessageMetadatahere keeps debounced-message persistence aligned with the moved core types.
There was a problem hiding this comment.
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 | 🟠 MajorClean up the approval when prompt delivery fails.
The prompt is already registered at Lines 461-473, but this early return skips both
bindPromptIDsLocked()andschedulePromptTimeout(). 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 | 🟡 MinorAdd nil check for
oc.approvalFlowbefore calling SendPrompt.While other prerequisites are validated at lines 33-44,
oc.approvalFlowcould potentially be nil if not initialized. TheAIClientstruct showsapprovalFlowas 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 == nilto 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 checkTitleandAllowAlways. Consider verifying theDetailsfield 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
📒 Files selected for processing (23)
approval_flow.goapproval_manager.goapproval_prompt.gobase_login_process.gobase_reaction_handler.gobridges/ai/agentstore.gobridges/ai/approval_prompt_presentation_test.gobridges/ai/chat.gobridges/ai/client.gobridges/ai/connector.gobridges/ai/constructors.gobridges/ai/identifiers.gobridges/ai/msgconv/to_matrix_test.gobridges/ai/remote_message_test.gobridges/ai/response_finalization.gobridges/ai/stream_events.gobridges/ai/streaming_error_handling.gobridges/ai/streaming_output_handlers.gobridges/ai/streaming_responses_api.gobridges/ai/streaming_state.gobridges/ai/streaming_ui_tools.gobridges/ai/subagent_spawn.gobridges/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
agentremotealigns 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
connectortoaiand the import migration frombridgeadaptertoagentremotealign with the PR's refactoring objectives.Also applies to: 16-16
343-350: No issues found. TheMessageMetadatastruct inbridges/ai/metadata.gocorrectly embedsagentremote.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:
- Add
t.Parallel()to tests that don't share state to improve test execution time.- Consider adding error case tests for
ConvertAIResponse(e.g., invalid inputs).- Table-driven tests could reduce repetition for functions like
RelatesToReplaceandRelatesToThreadif more edge cases are added later.bridges/ai/response_finalization.go (5)
1-1: LGTM!Package rename and import migration from
bridgeadapter/streamtransporttoagentremote/turnsis 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
buildReplyRelatesToand conditionally attaches the relation to the event. Migration toagentremotetypes is consistent.
610-684: LGTM - addresses past review concerns.The split logic now correctly operates on the original
markdownsource (line 614) before rendering, preserving formatting across split messages. Continuation chunks properly inheritstate.replyTarget(line 682) to maintain thread/reply context. Migration toagentremotetypes (MatrixMessageID,RemoteEdit) is consistent.
50-58: The concern about markdown formatting loss is unfounded. TheBuildContinuationMessagefunction inagentremote/helpers.gocallsformat.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 fromturns.SplitAtMarkdownBoundaryand 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
connectortoaiand the import migration frombridgeadaptertoagentremoteare consistent with the PR's refactoring objectives.
913-923: LGTM - API migration to agentremote.BuildDMChatInfo.The call to
agentremote.BuildDMChatInfowithagentremote.DMChatInfoParamscorrectly migrates from the previousbridgeadapterimplementation while preserving the same functional behavior.
988-991: LGTM - API migration to agentremote.BuildSystemNotice.The migration to
agentremote.BuildSystemNoticeis consistent with the broader refactoring pattern.bridges/ai/agentstore.go (2)
1-20: LGTM - Package rename and import migration.The package rename to
aiand import migration toagentremoteare consistent with the PR objectives.
385-414: LGTM - NewEventID migration to agentremote.The migration from
bridgeadapter.NewEventID("internal")toagentremote.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
aiand import migration toagentremoteare consistent with the broader refactoring.
135-137: LGTM - humanUserID migration.The migration to
agentremote.HumanUserIDprovides 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
aiand addition ofagentremoteimport 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:
- Assigns
agentremote.ApprovalReasonTimeoutwhen the approval wait times out without a specific reason- 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=falseand 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, andoc.UserLogin.UserMXIDbefore 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
AllowAlwaysis consistentlytrueacross all scenarios- Ensures fallback
Titleis 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
aiand import migration toagentremoteare 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
buildMCPApprovalPresentationto generate rich UI context for MCP tool approvals. ThePresentationfield is properly passed through to the approval registration.
237-261: LGTM - Robust error handling for approval delivery failures.Excellent defensive programming:
- Checks return value of
emitUIToolApprovalRequest- On failure, resolves the approval with
ApprovalReasonDeliveryError- Cleans up state (
delete(state.pendingMcpApprovalsSeen, approvalID))- Emits UI error and logs the failure
The auto-approval path at lines 252-260 correctly uses
agentremote.ApprovalDecisionPayloadwith proper error handling.bridges/ai/connector.go (5)
19-31: LGTM - Well-defined constants and interface assertions.Good practice to:
- Define named constants for default values rather than magic numbers
- Use compile-time interface assertions to ensure
OpenAIConnectorsatisfies required interfaces
51-63: LGTM - Sensible runtime defaults.The
applyRuntimeDefaultsmethod appropriately:
- Sets a 6-hour model cache duration if unspecified
- Defaults command prefix to
!ai- Applies pruning defaults using
airuntime.ApplyPruningDefaultsor creates default config
75-83: LGTM - Ghost ID validation logic.The
ValidateUserIDmethod correctly:
- Parses model ghost IDs and validates against the manifest
- Parses agent ghost IDs and validates with
isValidAgentID- Returns false for unrecognized formats
98-112: LGTM - Login flow creation with proper validation.The
createLoginmethod validates the flow ID against available flows before creating the login process, preventing invalid flow IDs from being accepted.
33-42: Theclientsmap is properly initialized viaagentremote.EnsureClientMap()in the connector's Init phase.The review comment incorrectly states that the
clientsmap is "declared but not initialized." However, line 30 ofconstructors.gocallsagentremote.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 inlogin_loaders.goare properly synchronized with theclientsMumutex, 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 persistednetworkMessageIDas 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.IDbeforeturns.ResolveTargetEventIDFromDB()preserves edit targeting for receiver-less portals.
There was a problem hiding this comment.
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 | 🟠 MajorReplacing a prompt leaves the old timeout armed.
SendPrompt()can overwrite the registration for an existingapprovalID, butschedulePromptTimeout()just starts another timer tied to that same approval. If the new prompt extendsExpiresAt, 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 | 🟠 MajorExternal resolutions still bypass
DeliverDecision.
ResolveExternal()now goes throughResolve(), butResolve()only enqueues onp.ch. In callback-based flows (DeliverDecision != nil), nothing consumes that channel, so the external decision is lost andFinishResolved()immediately removes the pending entry. It also finalizes even whenResolve()returns an error. Route this path through the same delivery logic asHandleReaction()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
📒 Files selected for processing (6)
approval_flow.goapproval_flow_test.gobase_login_process.gobridges/ai/remote_message_test.gobridges/ai/streaming_ui_tools.gobridges/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)
There was a problem hiding this comment.
🧹 Nitpick comments (1)
approval_flow.go (1)
278-288: Consider: Timeout timer race condition.When
timer.Cfires inWait, 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
📒 Files selected for processing (3)
approval_flow.goapproval_flow_test.goapproval_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
agentremoteand imports look correct. Theturnspackage import aligns with the new edit functionality.
57-92: LGTM!The
donechannel addition toPendingenables clean cancellation of timeout goroutines. Test hooks follow a reasonable pattern for dependency injection during testing.
140-148: LGTM!The
donechannel is properly initialized for new pending approvals.
197-215: LGTM!
ResolveExternalnow properly routes the decision throughResolve()before callingFinishResolved(), addressing the previous review concern about channel-based waiters not being notified.
235-257: LGTM!The
Resolvemethod correctly handles expiration by callingfinishTimedOutApprovaland cancels pending timeouts on successful delivery.
335-371: LGTM!The
PromptVersiontracking is a good approach to prevent stale timeout handlers from finalizing newer prompts. The copy semantics inpromptRegistrationensure thread safety.
459-535: LGTM!The
SendPromptflow properly preserves the previous prompt for restoration on failure, and correctly usespromptVersionto ensure timeout handlers are tied to specific prompt instances.
575-604: LGTM!The
resolvedflag correctly gates finalization, ensuringFinishResolvedis only called after successful decision delivery in both callback-based and channel-based flows.
691-716: LGTM!The timeout scheduling correctly captures
promptVersionto prevent stale handlers, and thedonechannel enables clean cancellation when approvals resolve before timeout.
819-881: LGTM!The
finalizeWithPromptVersionmethod correctly handles:
- Version validation to prevent stale timeouts from affecting newer prompts
- Double-close protection on the
donechannel- 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
mirrorRemoteDecisionReactionmethod 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
selectwithdefaultis 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
detailKeystruct 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
ApprovalOptionmethods provide clean accessors for decision reasons and reaction keys.
181-217: LGTM!The options builder correctly constructs the approval option set based on the
allowAlwaysflag.
219-281: LGTM!The body builder functions compose well to create structured approval prompt and response messages.
313-367: LGTM!The
BuildApprovalPromptMessagefunction correctly normalizes and returns options in the resulting message, addressing the previous review concern.
369-429: LGTM!The
BuildApprovalResponsePromptMessagefunction 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
variationselectorlibrary.
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.
There was a problem hiding this comment.
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 | 🟠 MajorAudio/video path is short-circuited before capability/fallback handling.
At Line 671, audio/video returns
UnsupportedMessageStatusbefore 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 | 🟡 MinorCapture and verify
QueueRemoteEventresult 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
setRoomNamecan 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, thesavebranch only logsportal.Savefailures, so callers likemaybeGenerateTitle()will think the rename succeeded whileTitleGeneratedwas 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 | 🔴 CriticalGuard 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 | 🟠 MajorAvoid starting
approvalFlowbefore provider init can fail.
agentremote.NewApprovalFlow()launches its reaper immediately. IfinitProviderForLogin()returns an error,newAIClient()exits without callingClose(), 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 | 🔴 CriticalRemove the nested
.PromptContextfield access.The code at line 373 incorrectly accesses
promptContext.PromptContext. ThePromptContextstruct (defined insdk/prompt_context.go:14) contains onlySystemPrompt,DeveloperPrompt, andMessagesfields—no.PromptContextfield exists. SincepromptContextis already of typePromptContext, 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.Headerswere 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. Ifmetadatais 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 foractiveTools.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
ParseContextLengthErrorfor 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, nilThe first is for
done=truecase, the second for the fallthrough. Ifdone=trueshould 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
convertToResponsesInputbut 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 redundantstate.completedAtMscheck.In
FinalizeAgentLoop, the conditionstate.completedAtMs != 0prevents re-finalization, butcompleteStreamingSuccessshould be idempotent. IfcompleteStreamingSuccessalready 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
📒 Files selected for processing (48)
approval_flow.gobridges/ai/agent_loop_chat_tools.gobridges/ai/agent_loop_chat_tools_test.gobridges/ai/agent_loop_request_builders.gobridges/ai/agent_loop_request_builders_test.gobridges/ai/agent_loop_routing_test.gobridges/ai/agent_loop_runtime.gobridges/ai/agent_loop_steering_test.gobridges/ai/agent_loop_test.gobridges/ai/canonical_history.gobridges/ai/canonical_prompt_messages.gobridges/ai/canonical_user_messages.gobridges/ai/client.gobridges/ai/client_find_model_info_test.gobridges/ai/constructors.gobridges/ai/constructors_test.gobridges/ai/handleai.gobridges/ai/handlematrix.gobridges/ai/heartbeat_execute.gobridges/ai/identifiers.gobridges/ai/internal_dispatch.gobridges/ai/login_loaders.gobridges/ai/login_loaders_test.gobridges/ai/media_understanding_runner.gobridges/ai/media_understanding_runner_openai_test.gobridges/ai/pending_queue.gobridges/ai/portal_materialize.gobridges/ai/provider_openai_responses.gobridges/ai/provider_openai_responses_test.gobridges/ai/response_retry.gobridges/ai/room_runs.gobridges/ai/session_transcript_openclaw.gobridges/ai/session_transcript_openclaw_test.gobridges/ai/streaming_chat_completions.gobridges/ai/streaming_continuation.gobridges/ai/streaming_executor.gobridges/ai/streaming_init.gobridges/ai/streaming_lifecycle_cluster_test.gobridges/ai/streaming_persistence.gobridges/ai/streaming_response_lifecycle.gobridges/ai/streaming_responses_api.gobridges/ai/streaming_state.gobridges/ai/streaming_ui_helpers.gobridges/ai/subagent_announce.gobridges/ai/subagent_spawn.gobridges/ai/tool_approvals.gobridges/ai/tool_approvals_helpers_test.gobridges/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)
| 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() | ||
| } |
There was a problem hiding this comment.
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.
| 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.
There was a problem hiding this comment.
@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.
There was a problem hiding this comment.
🧩 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.
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.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (5)
bridges/ai/login_loaders.go (2)
58-69:⚠️ Potential issue | 🟠 MajorCalling
Disconnect()while holdingclientsMurisks deadlock.At line 66,
cached.Disconnect()is invoked while holdingoc.clientsMu(deferred unlock at line 60). IfDisconnect()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 | 🟠 MajorSame issue:
Disconnect()called underclientsMulock.Lines 78 and 83 call
Disconnect()while holdingclientsMu. 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 | 🟡 MinorStoring typed-nil in interface field breaks nil checks.
When
metaisnil, assigning it directly toMetadata(an interface field) stores a typed-nil(*UserLoginMetadata)(nil)rather than a true nil interface. Code checkinglogin.Metadata == nilwill 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
ResolveExternalbypassesDeliverDecisioncallback mode.
ResolveExternalalways routes throughResolve(channel send) on Line 372. In callback-based mode (DeliverDecision), this skips the actual delivery path used inHandleReaction, 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
cancelPendingTimeoutcurrently has no effect on reaper timeouts.Line 1034 closes
done, but reaper expiry checks never consultdone(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 eachApprovalFlowin tests to avoid reaper goroutine leaks.Each
NewApprovalFlow(...)starts a background reaper. Addt.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". PreferApprovalReasonAllowAlwaysto 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
📒 Files selected for processing (15)
approval_flow.goapproval_flow_test.goapproval_prompt.gobridges/ai/chat_login_redirect_test.gobridges/ai/client.gobridges/ai/errors.gobridges/ai/errors_extended.gobridges/ai/errors_test.gobridges/ai/events.gobridges/ai/handleai.gobridges/ai/handleai_test.gobridges/ai/login_loaders.gobridges/ai/login_loaders_test.gobridges/ai/message_status.gobridges/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 aimigration 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
resolveModelIDFromManifestandparseModelFromGhostIDutilities, 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
containsAnyInFieldshelper 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
extractStructuredErrorMessagehelper provides a robust extraction path: first checking for typedopenai.Error, then falling back to JSON parsing with both nested and flat structures. The use ofsafeErrorStringguards 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.Errorwith status 403,IsPermissionDeniedErrorcheckspermissionFallbackPatterns(lines 15-19), but for plain errors, it only checkspermissionDeniedPatterns(line 24). This means:
- A plain error containing "forbidden" or "access denied": passes through
IsPermissionDeniedError(returns false) and is matched byIsAuthErrorat line 47- An
openai.Error403 with the same text: is caught byIsPermissionDeniedErrorand never reachesIsAuthErrorIf the intent is to classify "forbidden"/"access denied" consistently as permission-denied errors (not auth errors), then
IsPermissionDeniedErrorshould also checkpermissionFallbackPatternsfor non-openai.Errortypes.bridges/ai/message_status.go (1)
9-41: LGTM!The error classification updates are consistent:
IsPermissionDeniedErrorcorrectly maps toMessageStatusFailandMessageStatusNoPermission- Grouping
IsModelNotFoundwith context/image errors underUnsupportedaligns with the semantic meaning (capability/format mismatches rather than permission issues)bridges/ai/events.go (2)
15-26: LGTM!The
AIRoomInfoEventTyperegistration ininit()correctly associates the state event type with its content struct. The alias pattern keeps the type definition centralized inmatrixeventswhile exposing it conveniently in this package.
121-124: LGTM!The
AIRoomInfoContentstruct is appropriately minimal for its purpose of identifying AI room surfaces in Matrix state. The singleTypefield provides sufficient metadata for client-side rendering decisions.bridges/ai/login_loaders.go (1)
90-127: LGTM on the overallloadAIUserLoginflow.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:
TestAIClientNeedsRebuildcovers key, provider, and base URL change detectionTestLoadAIUserLoginMissingAPIKeyEvictsCacheAndSetsBrokenClientvalidates eviction behaviorTestReuseAIClientUpdatesClientBaseLoginconfirms client wiringTestAIRoomInfoEventTypeRegisteredvalidates event registrationbridges/ai/handleai.go (3)
77-121: LGTM onbridgeStateForErrorcentralization.The helper provides a clean mapping from error types to bridge states:
- Auth errors →
StateBadCredentialswith logout- Permission denied →
StateUnknownErrorwithout 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
bridgeStateForErroris correct: logout is triggered only for auth failures, and bridge state is always published when the error is recognized. Theokcheck ensures unrecognized errors don't trigger spurious state updates.
574-600: LGTM onsetRoomNameconsolidation.Adding the
saveparameter 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 != nilcheck prevents a panic path while preserving fallback behavior.
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.
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
bridges/ai/bootstrap_context_test.go (1)
116-123:⚠️ Potential issue | 🟡 MinorUnreachable 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 | 🟠 MajorAdd the same login guard before queuing synthetic reactions.
The new
BuildReactionEventpath dereferencesoc.UserLogin.ID, butsendReactionstill has no earlyUserLogin/Bridge/DBguard and already touchesDB.Messageon 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 | 🟠 MajorCheck
target.Portalwith a real cast before sending.
target.Portal == nilonly protects a nil interface. IfresolveCronDeliveryTargetreturns a typed-nil*bridgev2.Portal(where the dynamic type is non-nil but the pointer value is nil), this guard is bypassed andsendPlainAssistantMessagereceives a nil portal. Cast once, validate withok && 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 | 🔴 CriticalGuard
state.turnbefore reading turn ID.Line 391 dereferences
state.turndirectly. 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 | 🟠 MajorAdd nil guard for
metabefore provider switch.
initProviderForLogindereferencesmetaon 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
cancelPendingTimeoutstill allows reaper timeout races after decision delivery.Only closing
donedoes 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 withZeroLoggerand proper labeling, but the fallback paths inAIClient.bridgeDB()(line 37) andbridgeDBFromLogin()(line 52) useNoopLogger. 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:
persistTerminalAssistantTurncallssendFinalAssistantTurn,MessageMetadatacallsEmitUIMessageMetadata, andnoteStreamingPersistenceSideEffectscallssavePortalQuiet(which logs but doesn't propagate errors fromportal.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 thatresolveModelIDFromManifest()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.LoggerinpersistTerminalAssistantTurn.The
logparameter 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 forocinredactViaPortalfor consistency.Unlike
sendViaPortalWithTimingandsendEditViaPortalWithTimingwhich guard againstoc == nil,redactViaPortaldoes not. Whileocbeing 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:visibleStreamingTexthas redundant fallback paths.The function first checks
turn.VisibleText(), then falls back to parsing the UI message snapshot. Ifturn.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 thatturn.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 tobuildCanonicalTurnDatawon'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 givenagents.DefaultAgentIDis already "beeper".The
normalizeAgentID(agents.DefaultAgentID)call withDefaultAgentID = "beeper"(perpkg/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 toDefaultAgentID, 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
systemEventsScopefunction hardcodesagents.DefaultAgentIDrather than using a dynamic value. While the database schema includesagent_idin 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 (persistSystemEventsSnapshotandrestoreSystemEventsFromDB) 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 inSetDebouncedEditFuncis 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: CloseApprovalFlowin 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
📒 Files selected for processing (31)
README.mdapproval_flow.goapproval_flow_test.gobase_reaction_handler.gobridges/ai/agentstore.gobridges/ai/bootstrap_context_test.gobridges/ai/bridge_db.gobridges/ai/chat.gobridges/ai/chat_login_redirect_test.gobridges/ai/client.gobridges/ai/constructors.gobridges/ai/handlematrix.gobridges/ai/integrations_config.gobridges/ai/integrations_example-config.yamlbridges/ai/logout_cleanup.gobridges/ai/portal_send.gobridges/ai/reaction_handling.gobridges/ai/reactions.gobridges/ai/response_finalization.gobridges/ai/scheduler_cron.gobridges/ai/scheduler_db.gobridges/ai/session_store.gobridges/ai/streaming_init.gobridges/ai/streaming_lifecycle_cluster_test.gobridges/ai/streaming_response_lifecycle.gobridges/ai/streaming_responses_api.gobridges/ai/streaming_state.gobridges/ai/streaming_success.gobridges/ai/streaming_ui_helpers.gobridges/ai/subagent_spawn.gobridges/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
connectortoaiis 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,okidiom, and has appropriate fallback logic.bridges/ai/bootstrap_context_test.go (5)
1-18: Package rename and imports look correct.The package rename from
connectortoaialigns 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_filestoaichats_memory_filesis consistent with the DB table rename strategy (ai_* → aichats_*) documented in the PR.
50-79: Test logic is correct.The test properly verifies that
buildBootstrapContextFilesreturns 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/ListBossModelsare straightforward conversions and preserve existingAgentStoreAdapterbehavior without duplicating logic.
318-350: Embedding migration and client-path rewiring are consistent.The
BossStoreAdapterembedding 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.CloneSubagentConfigin 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
upgradeConfigcopy 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 toaiis consistent.No concerns on the file-level package migration.
16-18:agentremote/agentconfigimport 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
agentremotehelpers/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 (viapromptTail(promptContext, 1)extracting the current user message), not the full context. The full prompt context passed tosetCanonicalTurnDataFromPromptMessagesis used for agent dispatch viapromptContextToDispatchMessages, 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
AIClientinstances 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.completednow stays on the metadata path.Keeping completion in the lifecycle switch while still reserving
Error(...)forresponse.failedcleanly 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 foraichats_cron_jobs,aichats_managed_heartbeats,aichats_cron_job_run_keys, andaichats_managed_heartbeat_run_keysis already defined inpkg/aidb/001-init.sqlwithCREATE TABLE IF NOT EXISTSstatements, 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.SendViaPortalWithOptionswith proper timing parameters.bridges/ai/handlematrix.go (2)
267-277: LGTM!Message construction correctly uses the new
agentremotetypes (MatrixMessageID,BaseMessageMetadata,MatrixEventTimestamp) and properly sets canonical turn data from the prompt context.
1012-1023: Remove incorrect argument count reference.The
agentremote.BuildReactionEventfunction 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 onlastStreamOrder—the streaming flow is entirely synchronous.The code executes sequentially:
SetSendFunccallback runs once duringensureStarted()(guarded by a started flag), then finalization runs after streaming completes. All calls tonextMessageTiming()happen serially, not concurrently. No goroutines spawn that could cause concurrent access tolastStreamOrderorstartedAtMs.> 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
handleStreamStepErrorcorrectly:
- Identifies context-length errors
- Sets
finishReasonto "context-length"- Sets
completedAtMstimestamp- Wraps the error in
PreDeltaError
117-163: LGTM - Comprehensive response status test.This test verifies the full lifecycle from
in_progress→completedstatus transitions, checking bothstate.responseStatusand the UI message metadata. Good coverage of the status propagation path.
16-16: HelpernewTestStreamingStateWithTurnis 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 onstate.turnaccess - verify synchronization.The
FinalMetadataProvidercallback (lines 38-40) capturesstateand may be invoked from a different goroutine when the turn finalizes. Ifstate.turnor 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:prepareStreamingRunis well-structured with proper cleanup semantics.The function:
- Initializes state with source event context
- Creates the SDK turn with appropriate hooks
- Sets up typing controller with configurable modes
- 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
sendContinuationMessagefunction now acceptsreplyTargetandtimingparameters, and attachesm.relates_toto 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 splittingrendered.Bodywhich was the double-transformation issue flagged in the past review. Each chunk is then rendered independently viaformat.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 incrementingStreamOrdervalues, which is correct for ordering but worth noting.
| 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") |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
https://github.com/coderabbitai can you resolve this if it can be resolved safely?
There was a problem hiding this comment.
https://github.com/coderabbitai can you resolve this if it can be resolved safely?
There was a problem hiding this comment.
@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.
There was a problem hiding this comment.
🧩 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.
|
@coderabbitai can do a review again, everything you mentioned so far should be fixed. |
|
Oops, something went wrong! Please try again later. 🐰 💔 |
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (3)
bridges/ai/agentstore.go (1)
36-36:⚠️ Potential issue | 🔴 CriticalSynchronize
CustomAgentsreads with the metadata write lock.Line 36 still reaches read paths that access
meta.CustomAgentswithout locking, while writes are locked (Line 89, Line 101). This remains vulnerable toconcurrent map read and map writepanics.🔧 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 | 🟠 MajorAdd a nil guard for provider metadata before switch.
Line 484 dereferences
meta.Providerdirectly. Ifmetais 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 | 🟠 MajorDon’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 throughfinishTimedOutApproval(), 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) and0.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:normalizeAgentUpsertRequestalways returnsnilerror.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 eachApprovalFlowin these tests.
NewApprovalFlow()starts a reaper goroutine immediately. Withoutt.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
📒 Files selected for processing (24)
approval_flow.goapproval_flow_test.gobridges/ai/agent_loop_request_builders.gobridges/ai/agent_loop_request_builders_test.gobridges/ai/agent_loop_runtime.gobridges/ai/agent_loop_steering_test.gobridges/ai/agentstore.gobridges/ai/client.gobridges/ai/client_find_model_info_test.gobridges/ai/connector.gobridges/ai/constructors.gobridges/ai/defaults_alignment_test.gobridges/ai/events.gobridges/ai/login_loaders.gobridges/ai/login_loaders_test.gobridges/ai/media_understanding_runner.gobridges/ai/pending_queue.gobridges/ai/provider.gobridges/ai/provider_openai_chat.gobridges/ai/provider_openai_responses.gobridges/ai/provider_openai_responses_test.gobridges/ai/provisioning.gobridges/ai/streaming_continuation.gobridges/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 toaialigns with PR objectives.The rename from
connectortoaiis 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 registersAIRoomInfoEventTypewithAIRoomInfoContent{}, 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
AIRoomInfoEventTypeis correctly aliased frommatrixevents.AIRoomInfoEventTypewith an appropriate descriptive comment.
121-124: LGTM!The
AIRoomInfoContentstruct is appropriately minimal for storing lightweight room metadata. The singleTypefield with JSON serialization aligns with the stated purpose of identifying AI room surfaces.
138-138: Appropriate use of pointer for optional numeric field.Changing
Temperaturefromfloat64to*float64correctly distinguishes between "unset" (nil) and "explicitly set to zero" (0.0). This is semantically important since temperature0.0is 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
connectortoaiis 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
nilreturn value, and the error message safely dereferencesgotonly whengot != nil. This aligns with thefloat64→*float64API 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 (*float64pointing to0.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/StepFinishlifecycle 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
connectortoaiis consistent with the documented breaking API changes. Downstream consumers will need to update their imports.
17-17: LGTM!The
ptrimport fromgo.mau.fi/utilis appropriately added to support the nil-aware pointer cloning for theTemperaturefield.
205-221: Temperature pointer type correctly distinguishes unset vs explicit zero.The change from
float64to*float64enables 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
*float64instead offloat64.
253-253: Good use ofptr.Cloneto prevent pointer aliasing.Cloning the
Temperaturepointer prevents unintended mutation if the originalreq.Temperaturepointer 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 forTemperaturepointers.Using
ptr.Clonein 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.AgentDefinitionandtools.AgentData.Also applies to: 670-671
bridges/ai/media_understanding_runner.go (5)
1-21: LGTM!Package rename to
aiand newbridgesdkimport 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:
- Uses
bridgesdk.BuildDataURLandbridgesdk.UserPromptContextfor the new PromptContext-based flow- Always routes explicit OpenRouter entries through
generateWithOpenRouter(line 720 now only checksentryProvider == "openrouter")- Passes
capCfgandentryto 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/entrytogenerateWithOpenRouter.
932-961: Well-structured helper function.
resolveOpenRouterMediaConfigcleanly 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.PromptContextHasBlockTypefunction exists insdk/prompt_context.goand correctly acceptsPromptBlockAudioandPromptBlockVideoas validPromptBlockTypeconstants. The routing logic properly delegates togenerateChatCompletionswhen audio or video blocks are present.bridges/ai/streaming_continuation.go (1)
42-46: Steering persistence fix is correct.Lines 44-45 now persist
steerInputinto both the current request input andstate.baseInput, so steering survives later stateless continuations.bridges/ai/client.go (1)
1645-1649: Nil-safe model cache access looks good.The
meta != nilguard beforemeta.ModelCacheaccess 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.turnaccess 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
Metadataonly whenmeta != 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 != nilcorrectly 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
GenerateStreamandGenerate, 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 fromresponses.ReasoningEfforttoshared.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 > 0preventing explicit zero has been addressed. Now usingsettings.temperature != nilcorrectly allows explicit zero values to pass through while omitting temperature when unset.
125-129: No issues found. The type casting fromresponses.ReasoningEfforttoshared.ReasoningEffortat 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 onoc.Config.Bridge.Line 57 accesses
oc.Config.Bridge.CommandPrefixwithout checking ifBridgeis nil. IfapplyRuntimeDefaults()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: Theclientsmap is properly initialized in the constructor.Confirmed:
NewAIConnector()initializes theclientsfield withmake(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.
takeQueueSummaryat line 217 (and line 242) only reads the queue'sdroppedCountandsummaryLineswithout 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.Followupbefore any mutation (line 345), and only then callstakePendingQueueDispatchCandidate(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
QueueModeSteerqueues are not consumed bygetFollowUpMessages, which is critical for correct queue behavior separation.bridges/ai/login_loaders.go (3)
58-70: LGTM! Disconnect correctly called outside lock.The
evictCachedClientmethod now releases the mutex (line 66) before callingDisconnect()(line 68), addressing the prior deadlock concern.
72-94: LGTM! Client reuse logic correctly handles lock ordering.The
publishOrReuseClientmethod releases the mutex before callingDisconnect()on both thecreatedclient (line 80) andreplaceclient (line 91), preventing potential deadlocks.
96-134: LGTM! Robust login loading flow.The
loadAIUserLoginmethod 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:Temperatureas 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
GenerateParamscontract.
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.
batuhan
left a comment
There was a problem hiding this comment.
Submitting previously added thread replies.
| 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) | ||
| } | ||
| } |
There was a problem hiding this comment.
@coderabbitai can you resolve this if it can be resolved safely?
| 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") |
There was a problem hiding this comment.
https://github.com/coderabbitai can you resolve this if it can be resolved safely?
| 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") |
There was a problem hiding this comment.
https://github.com/coderabbitai can you resolve this if it can be resolved safely?
No description provided.