Skip to content

delete emoji approvals, make approval handling shared pkg#114

Merged
batuhan merged 4 commits into
mainfrom
batuhan/delete-emoji-approvals
Jun 2, 2026
Merged

delete emoji approvals, make approval handling shared pkg#114
batuhan merged 4 commits into
mainfrom
batuhan/delete-emoji-approvals

Conversation

@batuhan
Copy link
Copy Markdown
Member

@batuhan batuhan commented Jun 2, 2026

No description provided.

batuhan added 2 commits June 2, 2026 02:13
Introduce a new ai-command package to centralize parsing of visible (`/...`) and hidden (`!ai ...`) commands with canonical names and aliases. Add an ApprovalCoordinator to pkg/ai-stream to normalize approval requests, parse/stamp approval commands, wait/resolve approvals, and publish lifecycle hooks; replace duplicated per-run approval logic with this coordinator. Integrate the registry and coordinator into the connector: use the command registry for parsing, use ApprovalCoordinator for request/resolve/publish flow, and simplify approval-related code paths. Update Matrix approval content rendering to use helper commands, and add/tests for command parsing and approval flows. Update README to reflect the new packages and behavior.
@indent
Copy link
Copy Markdown

indent Bot commented Jun 2, 2026

PR Summary

Centralizes slash-command parsing into a new pkg/ai-command package and lifts the per-run approval lifecycle into a shared ApprovalCoordinator in pkg/ai-stream. Emoji-reaction approvals are removed: users now respond with /approve <id> [approve|always|deny] (or hidden !ai approve …). Follow-up commits 5378cd24 / 68d47ea6 thread request-specific Choices through the parser, delete the pending entry before post-response hooks, restore the stream-anchor fast-path/deterministic fallback, ensure finalize/failOpen always release the active-stream record, always wait for anchor row persistence, emit an approval-event-linked follow-up so the AG-UI interrupt regains its approvalEventId, and default bare /approve <id> to the first pending choice.

  • pkg/ai-command: new package with Command, Registry, alias canonicalization, and MatrixCommandMsgType.
  • pkg/ai-stream/approval_command.go: ApprovalCoordinator + PublishRequested/BeforeResponded/PublishResponded hooks, NormalizeApprovalRequest, ParseApprovalCommand(arg, choices, now), StampApprovalResponse, ApprovalCommandForChoice/ForID, ApprovalCommandReply, and ResolveCommand(arg, now) that defaults to the first pending choice when no choice token is supplied.
  • pkg/ai-stream/run.go: new Writer.ApprovalEventLinked(approvalID, eventID) annotates the in-flight interrupt with approvalEventId and emits a com.beeper.ai.approval event-linked custom event.
  • pkg/connector/approvals.go: removes the per-run activeApproval map; PublishRequested now publishes the AG-UI interrupt first, queues the Matrix prompt, and (when queued) emits publishApprovalEventLink so the interrupt regains its approvalEventId. resolveApprovalCommand delegates to ResolveCommand.
  • pkg/connector/client.go: queueAssistantStreamAnchor retains its fast-path + deterministic-event-ID fallback; waitForAssistantAnchorMessage always waits for the bridgev2 message-store row before allowing finalize/error edits, only writing stream.eventID when previously empty. finalizeAssistant/failOpenAssistant always call deleteActiveStream, even when the anchor wait fails.
  • Matrix approval message renders choices via ApprovalCommandForChoice; CleanupApprovalReactions removed; new tests cover ResolveCommand with custom choices, the bare-/approve default-choice path, and the ApprovalEventLinked writer.

Issues

1 potential issue found:

  • aiCommandRegistry() rebuilds the definitions slice and both registry maps on every call (parsing, dispatch, canonicalization). The set is static — store it once in a package-level singleton (e.g. via sync.OnceValue) to avoid per-message allocations. → Autofix
7 issues already resolved
  • finalizeAssistant silently drops the final AI message and leaks the active-stream record when waitForAssistantAnchorMessage fails: it returns before queueAssistantFinal/deleteActiveStream run, so successful completions disappear and the run stays 'active' across restarts. (fixed by commit 5378cd2)
  • Reordering PublishRequested to call publishApprovalInterrupt before QueueRemoteEvent means the AG-UI interrupt is now published with an empty approvalEventIdrequest.ApprovalEventID is only assigned after the queue returns, but the interrupt was already emitted, and nothing republishes/annotates it (no SetApprovalInterruptEventID call, no follow-up event). Downstream AG-UI consumers lose the link between the interrupt and its Matrix approval message. (fixed by commit 68d47ea)
  • ParseApprovalCommand resolves the user-supplied choice against DefaultApprovalChoices() instead of the pending request's own Choices, so any approval that customizes choices (today possible via ApprovalRequest.Choices and already plumbed through ApprovalContext/ApprovalNotice) cannot actually be approved via /approve — the parser will reject the custom keys. (fixed by commit 5378cd2)
  • waitForAssistantAnchorMessage always re-queries the message store and overwrites stream.eventID even when setup already populated it; the only short-circuit (stream.portal == nil && stream.eventID != "") is effectively the test-only path. This wastes a 30s-bounded round-trip on every run termination and risks divergence if the store returns a different MXID than what was set at setup. (fixed by commit 5378cd2)
  • waitForAssistantAnchorMessage short-circuits whenever stream.eventID != "", but this defeats the helper's purpose: assistantFinalEditWithProjection / FinalMetadataEditWithContent look up the message row by messageID and silently drop the edit when existing is empty. stream.eventID can be set without the row being persisted (the restored fast-path in queueAssistantStreamAnchor returns result.EventID before any store write; the deterministic-fallback path never writes the row). The wait must still confirm persistence even when the MXID is already known. Retracts my earlier recommendation to add this short-circuit. (fixed by commit 68d47ea)
  • failOpenAssistant leaks active-stream rows on anchor-wait failure: when waitForAssistantAnchorMessage errors, the loop continues and skips cl.deleteActiveStream, so the persisted active-stream record is left behind and the run-error event is never published; old code always ran both. (fixed by commit 5378cd2)
  • queueAssistantStreamAnchor drops both the result.EventID fast-path and the GenerateDeterministicEventID fallback, so every anchor lookup now waits up to 30s on the message store and has no recovery path; combined with the new wait-gating in finalize/failOpen this turns previously-survivable lookup hiccups into dropped runs. This change isn't mentioned in the PR description or commit message. (fixed by commit 5378cd2)

CI Checks

Waiting for CI checks...


⚡ Autofix All Issues

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b30f140c-a460-4d62-af92-96f29c8af2bf

📥 Commits

Reviewing files that changed from the base of the PR and between 5378cd2 and 68d47ea.

📒 Files selected for processing (6)
  • pkg/ai-stream/approval_command.go
  • pkg/ai-stream/approval_command_test.go
  • pkg/ai-stream/run.go
  • pkg/ai-stream/stream_test.go
  • pkg/connector/approvals.go
  • pkg/connector/client.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • pkg/connector/client.go
  • pkg/ai-stream/approval_command_test.go
  • pkg/connector/approvals.go
  • pkg/ai-stream/approval_command.go
📜 Recent 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 comments (2)
pkg/ai-stream/run.go (1)

542-557: LGTM!

pkg/ai-stream/stream_test.go (1)

763-780: LGTM!

Also applies to: 816-832


📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Explicit slash-command approval flow (/approve ) with hidden command support, centralized approval coordinator, and approval-event linking.
  • Bug Fixes / Reliability

    • Improved assistant message anchoring/waiting to reduce race conditions for final edits and error handling.
  • Documentation

    • README clarifies the human approval workflow, programmatic approval schema, and message-edit/reaction capabilities.
  • Tests

    • Expanded tests for command parsing, approval coordinator behavior, timeouts, and approval event linking.

Walkthrough

Refactors approvals from reaction-based UX to a dedicated /approve command flow, adds a reusable ai-command registry, centralizes approval lifecycle in ApprovalCoordinator with lifecycle hooks, updates connector slash-command handling, and adds anchor-message waiting for assistant stream finalization.

Changes

Approval Command Workflow Refactor

Layer / File(s) Summary
Command Registry Infrastructure
pkg/ai-command/command.go, pkg/ai-command/command_test.go
New reusable command registry handles visible (/) and hidden (!ai ) prefix parsing with normalized names and alias support.
Approval Choice Data Model Simplification
pkg/ai-stream/approval.go
ApprovalChoice removes Alias; choice emission stops including alias and ResolveApprovalChoice uses internal normalization and key-based matching.
Approval Command Parser and Coordinator
pkg/ai-stream/approval_command.go, pkg/ai-stream/approval_command_test.go
Implements /approve command formatting/parsing, stamping, and an in-memory ApprovalCoordinator with Request/Resolve/ResolveCommand/Delete, lifecycle hooks, timeout handling, and tests.
Connector Approval Integration
pkg/connector/approvals.go
Delegates approval lifecycle to ApprovalCoordinator via hooks, removes local per-approval state and translation helpers, and constructs ApprovalContext with ApprovalCommandForID.
Slash Command Registry Integration
pkg/connector/slash_commands.go
Consolidates manual command parsing into registry-based parsing via aiCommandRegistry(), switching to Name/Arg fields and removing local alias logic.
Approve Command Handler
pkg/connector/slash_commands_approvals.go
runApproveCommand delegates parsing/validation to the approval coordinator utilities and replies with ApprovalCommandReply instead of hard-coded strings.
Approval Event Linking
pkg/ai-stream/run.go, pkg/ai-stream/stream_test.go
Adds Writer.ApprovalEventLinked to annotate interrupts with approvalEventId and emit an event-linked payload; test asserts metadata and emitted event.
Documentation and Content Updates
README.md, pkg/ai-stream/matrix/content.go
README documents the /approve workflow, updates room capabilities and quirks, package map includes pkg/ai-command; approval notices render commands via ApprovalCommandForChoice.

Stream Anchor Message Management

Layer / File(s) Summary
Stream State and Portal Reference
pkg/connector/client.go
activeAIRun.approvals retyped to ApprovalCoordinator pointer; assistantStreamState stores portal *bridgev2.Portal.
Stream Anchor Waiting Logic
pkg/connector/client.go
Adds Client.waitForAssistantAnchorMessage to validate stream/portal and wait for anchored assistant message event ID; finalize/fail paths use this helper.
Stream Anchor Test Update
pkg/connector/stream_test.go
Test changed to run stream.Result() in a goroutine and assert it blocks until anchor setup completes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • beeper/ai-bridge#112: Modifies approval-choice and resolve behavior overlapping pkg/ai-stream/approval.go.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive No pull request description was provided by the author, making it impossible to evaluate whether the description relates to the changeset. Add a pull request description explaining the motivation, changes, and impact of migrating from emoji approvals to command-based approval handling.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately captures the main changes: removing emoji-based approval reactions and consolidating approval handling into a shared package with a new command-based workflow.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

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

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread pkg/connector/client.go
Comment thread pkg/connector/client.go
Comment thread pkg/connector/client.go
Comment thread pkg/connector/client.go
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (2)
pkg/connector/stream_test.go (1)

118-126: ⚡ Quick win

Synchronize the Result() goroutine before asserting it is blocked.

This negative assertion currently depends on the goroutine being scheduled within 50ms. If it hasn't entered stream.Result() yet, the test can pass without actually proving that Result() is blocked by anchor setup. Add a small started channel and wait for it before the timeout-based check.

Suggested test hardening
 	resultDone := make(chan ai.Message, 1)
+	resultStarted := make(chan struct{})
 	go func() {
+		close(resultStarted)
 		resultDone <- stream.Result()
 	}()
+	<-resultStarted
 	select {
 	case result := <-resultDone:
 		t.Fatalf("stream result completed before anchor setup: %#v", result)
 	case <-time.After(50 * time.Millisecond):
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/connector/stream_test.go` around lines 118 - 126, The test races on
whether the goroutine has entered stream.Result() before the 50ms timeout; fix
by adding a started channel and have the goroutine send a signal on started
immediately after it begins (before calling stream.Result()), then in the test
wait for started before performing the timeout-based read from resultDone to
assert Result() is blocked; update the goroutine that writes to resultDone to
also write to started, and add a receive on started in the main test flow prior
to the select that checks for blocking.
pkg/ai-stream/stream_test.go (1)

763-780: ⚡ Quick win

Lock in the removal of legacy approval aliases with a negative test.

This renamed test only proves the canonical command keys still work. Since this PR intentionally deletes emoji/reaction-style approvals, add a couple of assertions that legacy inputs are rejected so that behavior doesn’t quietly come back later.

Example addition
 func TestApprovalResolverMatchesCommandChoices(t *testing.T) {
 	choices := DefaultApprovalChoices()
 	choice, ok := ResolveApprovalChoice(choices, "approve")
@@
 	if !ok || response.Approved || response.Reason != "denied" {
 		t.Fatalf("expected denial, got %#v ok=%v", choice, ok)
 	}
+	if _, ok := ResolveApprovalChoice(choices, "✅"); ok {
+		t.Fatal("legacy emoji approval choice should not resolve")
+	}
+	if _, ok := ResolveApprovalChoice(choices, "yes"); ok {
+		t.Fatal("legacy alias approval choice should not resolve")
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/ai-stream/stream_test.go` around lines 763 - 780, Update
TestApprovalResolverMatchesCommandChoices to assert legacy/reaction-style
approval inputs are rejected: after creating choices via DefaultApprovalChoices
and existing positive checks, add negative assertions that
ResolveApprovalChoice(choices, "<legacy>") returns ok == false for a couple of
legacy strings (e.g. emoji or reaction aliases you removed), and do not call
ApprovalResponseForChoice when ok is false; reference ResolveApprovalChoice,
DefaultApprovalChoices, and ApprovalResponseForChoice to locate where to add
these assertions so the test will fail if legacy aliases are ever reintroduced.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/ai-stream/approval_command_test.go`:
- Around line 30-37: The test is timing-sensitive because
ApprovalRequest.ExpiresAt is set to the same 1s window as ctx's deadline and the
test blocks indefinitely on a bare `<-done`; to fix it, set ExpiresAt to a time
well past the context deadline (e.g., time.Now().Add(5 * time.Second)) so expiry
can't race the test, and replace the bare `<-done` with a select that waits for
either the done channel or a short test-specific timeout (using the existing ctx
or a new timeout) to fail the test instead of hanging; apply the same change to
the other instance of the test at the noted lines.

In `@pkg/ai-stream/approval_command.go`:
- Around line 57-67: ParseApprovalCommand currently validates choices against
DefaultApprovalChoices() which rejects request-specific choices; change it to
accept the pending request's choices (or remove validation here and let the
coordinator validate). Update ParseApprovalCommand signature to take the
request-specific choices (e.g., pass in the pending request's Choices) or return
a parsed approval payload without choice validation, then have the coordinator
call ResolveApprovalChoice with the request's Choices and perform validation
there; keep creation of the ApprovalResponse via ApprovalResponseForChoice and
stamping via StampApprovalResponse unchanged.
- Around line 101-127: The deferred c.Delete(request.ID) at the top leaves the
pending entry live while hooks run; move the deletion so the pending entry is
removed immediately after c.wait returns: remove the initial defer
c.Delete(request.ID), and call c.Delete(request.ID) right after response :=
c.wait(ctx, request, pending) (ensuring pending is cleared before executing
BeforeResponded/PublishResponded). Preserve deletion on early-exit/error paths
by adding short-lived defers or explicit c.Delete(request.ID) in error branches
where the function returns before the wait completes so we still clean up on
those paths.

In `@pkg/connector/approvals.go`:
- Around line 41-49: The current flow in PublishRequested (in
ApprovalCoordinator) queues the remote prompt via cl.UserLogin.QueueRemoteEvent
before calling publishApprovalInterrupt, which can leave an orphaned prompt if
publishApprovalInterrupt fails; to fix, make the side-effects atomic by calling
publishApprovalInterrupt(ctx, cl, publisher, portal.MXID, stream, request) first
and only on success call cl.UserLogin.QueueRemoteEvent(...) and then set
request.ApprovalEventID, or if you must keep the current order add a
compensating cleanup that cancels the queued prompt when
publishApprovalInterrupt returns an error (use the appropriate cancel/remove
method on cl.UserLogin with queued.EventID), ensuring request.ApprovalEventID is
only set after both operations succeed.

---

Nitpick comments:
In `@pkg/ai-stream/stream_test.go`:
- Around line 763-780: Update TestApprovalResolverMatchesCommandChoices to
assert legacy/reaction-style approval inputs are rejected: after creating
choices via DefaultApprovalChoices and existing positive checks, add negative
assertions that ResolveApprovalChoice(choices, "<legacy>") returns ok == false
for a couple of legacy strings (e.g. emoji or reaction aliases you removed), and
do not call ApprovalResponseForChoice when ok is false; reference
ResolveApprovalChoice, DefaultApprovalChoices, and ApprovalResponseForChoice to
locate where to add these assertions so the test will fail if legacy aliases are
ever reintroduced.

In `@pkg/connector/stream_test.go`:
- Around line 118-126: The test races on whether the goroutine has entered
stream.Result() before the 50ms timeout; fix by adding a started channel and
have the goroutine send a signal on started immediately after it begins (before
calling stream.Result()), then in the test wait for started before performing
the timeout-based read from resultDone to assert Result() is blocked; update the
goroutine that writes to resultDone to also write to started, and add a receive
on started in the main test flow prior to the select that checks for blocking.
🪄 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: da12fae8-04f9-4cb5-a4a0-14a1e492ff2f

📥 Commits

Reviewing files that changed from the base of the PR and between ab83be6 and 3bffda7.

📒 Files selected for processing (14)
  • README.md
  • pkg/ai-command/command.go
  • pkg/ai-command/command_test.go
  • pkg/ai-stream/approval.go
  • pkg/ai-stream/approval_command.go
  • pkg/ai-stream/approval_command_test.go
  • pkg/ai-stream/matrix/content.go
  • pkg/ai-stream/stream_test.go
  • pkg/connector/approvals.go
  • pkg/connector/client.go
  • pkg/connector/slash_commands.go
  • pkg/connector/slash_commands_approvals.go
  • pkg/connector/slash_commands_test.go
  • pkg/connector/stream_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). (1)
  • GitHub Check: build-docker
🔇 Additional comments (20)
README.md (7)

239-239: LGTM!


248-248: LGTM!


514-514: LGTM!


534-535: LGTM!


535-535: LGTM!


542-542: LGTM!


235-237: Confirm /approve arg options match the implementation
README lists /approve <approval-id> <approve|always|deny>, but the codebase search for ApprovalChoice{ Key: ... } / approval-related Key: "(approve|always|deny|always_approve)" returned no matches, so the choice-key mapping used by /approve still needs to be checked in the implementation.

pkg/ai-stream/matrix/content.go (1)

249-249: LGTM!

pkg/connector/client.go (6)

103-103: LGTM!


119-119: LGTM!


1114-1114: LGTM!


2587-2603: LGTM!


2620-2623: LGTM!


2637-2643: LGTM!

pkg/connector/approvals.go (1)

59-83: LGTM!

Also applies to: 103-123

pkg/connector/slash_commands.go (1)

15-18: LGTM!

Also applies to: 162-194, 207-250

pkg/connector/slash_commands_approvals.go (1)

14-22: LGTM!

pkg/ai-stream/approval_command_test.go (1)

10-25: LGTM!

Also applies to: 77-88

pkg/ai-stream/stream_test.go (1)

1049-1051: LGTM!

pkg/connector/slash_commands_test.go (1)

59-60: LGTM!

Also applies to: 75-75, 81-81, 87-87, 93-93, 134-145

Comment thread pkg/ai-stream/approval_command_test.go
Comment thread pkg/ai-stream/approval_command.go Outdated
Comment thread pkg/ai-stream/approval_command.go Outdated
Comment thread pkg/connector/approvals.go
Comment thread pkg/ai-stream/approval_command.go Outdated
Change ParseApprovalCommand to accept explicit ApprovalChoice lists and use those when resolving choices. Add ApprovalCoordinator.ResolveCommand to parse and resolve approval slash commands against pending requests. Adjust ApprovalCoordinator.Request to avoid double-delete of pending entries and ensure proper cleanup after wait. Update connector to call publishApprovalInterrupt earlier and use the new resolveApprovalCommand flow. Improve Client anchor handling: return queued EventID when available, fallback to deterministic event ID generation on timeout, and remove active streams when anchor confirmation fails. Update tests to reflect the new signatures and behaviors.
Comment thread pkg/connector/approvals.go
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (3)
pkg/connector/client.go (1)

2598-2613: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep waiting for anchor persistence even when the event ID is already known.

Line 2602 now returns as soon as stream.eventID is set, but finalizeAssistant/failOpenAssistant use this helper to ensure the anchor is actually editable. Knowing the MXID is not enough here: assistantFinalEditWithProjection drops the edit when the anchor row is still missing, so a fast completion can still lose the final/error edit if the store write lags behind the queued anchor.

Suggested fix
 func (cl *Client) waitForAssistantAnchorMessage(ctx context.Context, stream *assistantStreamState) error {
 	if stream == nil {
 		return fmt.Errorf("missing assistant stream")
 	}
-	if stream.eventID != "" {
-		return nil
-	}
 	if stream.portal == nil {
 		return fmt.Errorf("missing assistant stream portal")
 	}
 	eventID, err := cl.waitForMessageEventID(ctx, stream.portal, stream.messageID, aiid.PartID("text"), 30*time.Second)
 	if err != nil {
 		return err
 	}
-	stream.eventID = eventID
+	if stream.eventID == "" {
+		stream.eventID = eventID
+	}
 	return nil
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/connector/client.go` around lines 2598 - 2613, The early return in
waitForAssistantAnchorMessage when stream.eventID is non-empty skips waiting for
the anchor persistence and can cause dropped final/error edits; remove that
early return and always call waitForMessageEventID (using stream.portal and
stream.messageID with aiid.PartID("text") and the same timeout) so the function
ensures the anchor row is editable even if eventID is already known; keep
existing nil checks for stream and stream.portal and still set stream.eventID
from the returned eventID if it was previously empty.
pkg/ai-stream/approval_command.go (2)

108-115: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Synchronize access to pending.request.

Request updates pending.request after add() without holding c.mu, while ResolveCommand reads pending.request.Choices from another goroutine after releasing the mutex. A fast /approve can race with PublishRequested, which is undefined under the race detector and can validate against stale or partially updated request state.

🔒 Minimal locking fix
 	if hooks.PublishRequested != nil {
 		var err error
 		request, err = hooks.PublishRequested(ctx, request)
 		if err != nil {
 			return ToolApprovalResponse{}, err
 		}
-		pending.request = request
+		c.mu.Lock()
+		pending.request = request
+		c.mu.Unlock()
 	}
-	c.mu.Lock()
-	pending := c.pending[approvalID]
-	c.mu.Unlock()
+	c.mu.Lock()
+	pending := c.pending[approvalID]
+	var choices []ApprovalChoice
+	if pending != nil {
+		choices = append([]ApprovalChoice(nil), pending.request.Choices...)
+	}
+	c.mu.Unlock()
 	if pending == nil {
 		return ToolApprovalResponse{ID: approvalID}, false, nil
 	}
-	response, err := ParseApprovalCommand(arg, pending.request.Choices, now)
+	response, err := ParseApprovalCommand(arg, choices, now)

Also applies to: 147-153

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/ai-stream/approval_command.go` around lines 108 - 115, The update to
pending.request in the PublishRequested hook is not synchronized and can race
with ResolveCommand reading pending.request. Modify the code around
hooks.PublishRequested (and the similar block at lines 147-153) to acquire the
command-level mutex (c.mu) before writing pending.request (and keep it held
while assigning the new request) or otherwise perform the assignment inside the
same critical section that calls add()/sets up pending, ensuring pending.request
updates and reads (e.g., in ResolveCommand) are protected by the same c.mu lock;
update both the PublishRequested paths and any other places that mutate
pending.request to use c.mu consistently.

164-175: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep lookup and delivery atomic in Resolve.

Resolve drops c.mu before the channel send. If wait() times out or aborts and Request deletes the entry in that gap, this method can still send into the buffered channel and return true even though nobody is waiting anymore. The caller will then reply with a success message for an approval that already expired.

🛠️ Suggested fix
 func (c *ApprovalCoordinator) Resolve(response ToolApprovalResponse) bool {
 	if c == nil || response.ID == "" {
 		return false
 	}
 	c.mu.Lock()
-	pending := c.pending[response.ID]
-	c.mu.Unlock()
+	defer c.mu.Unlock()
+	pending := c.pending[response.ID]
 	if pending == nil {
 		return false
 	}
 	select {
 	case pending.response <- response:
 		return true
 	default:
 		return false
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/ai-stream/approval_command.go` around lines 164 - 175, Resolve currently
releases c.mu before sending to pending.response, allowing Request to delete the
entry in between; keep the lookup and delivery atomic by holding c.mu while
performing the non-blocking send: inside Resolve, lock c.mu, load pending :=
c.pending[response.ID], if pending == nil unlock and return false, then perform
the non-blocking send to pending.response (select { case pending.response <-
response: success = true default: success = false }), unlock c.mu and return
success; refer to Resolve, c.mu, c.pending, pending.response and Request/wait
interactions when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/ai-stream/approval_command.go`:
- Around line 141-145: The code currently treats a bare token like "/approve
approval-1" as missing the choice and sends it straight to ParseApprovalCommand
which expects a choice; change the branch in the approval parsing so that when
strings.Cut yields ok==false (i.e., only an approval ID was provided) you
synthesize a full command by appending the default choice token (use the
first/default entry from DefaultApprovalChoices()) to the approval ID and then
call ParseApprovalCommand with that synthesized string (keep the existing now
parameter and error handling). Reference symbols: strings.Cut usage around
approvalID, ParseApprovalCommand, DefaultApprovalChoices().

---

Outside diff comments:
In `@pkg/ai-stream/approval_command.go`:
- Around line 108-115: The update to pending.request in the PublishRequested
hook is not synchronized and can race with ResolveCommand reading
pending.request. Modify the code around hooks.PublishRequested (and the similar
block at lines 147-153) to acquire the command-level mutex (c.mu) before writing
pending.request (and keep it held while assigning the new request) or otherwise
perform the assignment inside the same critical section that calls add()/sets up
pending, ensuring pending.request updates and reads (e.g., in ResolveCommand)
are protected by the same c.mu lock; update both the PublishRequested paths and
any other places that mutate pending.request to use c.mu consistently.
- Around line 164-175: Resolve currently releases c.mu before sending to
pending.response, allowing Request to delete the entry in between; keep the
lookup and delivery atomic by holding c.mu while performing the non-blocking
send: inside Resolve, lock c.mu, load pending := c.pending[response.ID], if
pending == nil unlock and return false, then perform the non-blocking send to
pending.response (select { case pending.response <- response: success = true
default: success = false }), unlock c.mu and return success; refer to Resolve,
c.mu, c.pending, pending.response and Request/wait interactions when making the
change.

In `@pkg/connector/client.go`:
- Around line 2598-2613: The early return in waitForAssistantAnchorMessage when
stream.eventID is non-empty skips waiting for the anchor persistence and can
cause dropped final/error edits; remove that early return and always call
waitForMessageEventID (using stream.portal and stream.messageID with
aiid.PartID("text") and the same timeout) so the function ensures the anchor row
is editable even if eventID is already known; keep existing nil checks for
stream and stream.portal and still set stream.eventID from the returned eventID
if it was previously empty.
🪄 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: 4e86ff4b-bca4-4dd8-baf9-2de951b6139b

📥 Commits

Reviewing files that changed from the base of the PR and between 3bffda7 and 5378cd2.

📒 Files selected for processing (7)
  • pkg/ai-stream/approval_command.go
  • pkg/ai-stream/approval_command_test.go
  • pkg/connector/approvals.go
  • pkg/connector/client.go
  • pkg/connector/slash_commands_approvals.go
  • pkg/connector/slash_commands_test.go
  • pkg/connector/stream_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • pkg/ai-stream/approval_command_test.go
  • pkg/connector/stream_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

Comment thread pkg/ai-stream/approval_command.go Outdated
Comment thread pkg/connector/client.go Outdated
@batuhan batuhan merged commit 1f707fd into main Jun 2, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant