Skip to content

fix: prevent user-input activities from leaking into pending approvals projection#2051

Merged
juliusmarminge merged 1 commit intopingdotgg:mainfrom
marcusgrando:fix/pending-approval-user-input-leak
Apr 16, 2026
Merged

fix: prevent user-input activities from leaking into pending approvals projection#2051
juliusmarminge merged 1 commit intopingdotgg:mainfrom
marcusgrando:fix/pending-approval-user-input-leak

Conversation

@marcusgrando
Copy link
Copy Markdown
Contributor

@marcusgrando marcusgrando commented Apr 16, 2026

Summary

Threads that used user-input prompts (structured questions to the user) permanently display "Pending Approval" in the sidebar even after all inputs have been answered and the turn has completed.

Root cause

applyPendingApprovalsProjection in ProjectionPipeline.ts does not filter by activity.kind before creating pending-approval rows. The logic:

  1. Extracts requestId from any activity payload via extractActivityRequestId
  2. Checks if the activity is approval.resolved → skip
  3. Checks if the activity is provider.approval.respond.failed → skip
  4. Default: creates a row with status = "pending" for any remaining activity with a requestId

Activities like user-input.requested and user-input.resolved carry a requestId in their payload, so they fall through to the default branch and create orphan pending-approval rows. When user-input.resolved arrives, it also falls through and overwrites the row with status = "pending" again (since it's not approval.resolved).

These orphan rows are never cleaned up because only approval.resolved can resolve them.

Contrast with backfill migration

The backfill migration 024_BackfillProjectionThreadShellSummary.ts correctly filters by kind = 'approval.requested' when seeding pending-approval rows (line 36). The live projection was missing this same guard.

Operation Migration 024 (backfill) Live projection (before fix)
Create pending row WHERE kind = 'approval.requested' Any activity with requestId
Resolve row WHERE kind = 'approval.resolved' kind === "approval.resolved"
Stale cleanup WHERE kind = 'provider.approval.respond.failed' Matching stale detail strings ✅

Fix

Add a kind !== "approval.requested" guard before the pending-row creation path, so only actual approval requests create entries in projection_pending_approvals. User-input activities already have their own accounting via derivePendingUserInputCountFromActivities.

Verified with real data

A thread with 11 answered user-input prompts had 11 orphan pending rows in projection_pending_approvals, all with status = "pending" and resolved_at = null, even though every single one had a matching user-input.resolved event in the event store. The thread's session was status = "ready" with active_turn_id = null.

Test plan

  • All 18 ProjectionPipeline.test.ts tests pass
  • All 82 related orchestration tests pass (ProjectionPipeline, ProviderRuntimeIngestion, ProviderCommandReactor, projector)
  • Verify sidebar no longer shows "Pending Approval" for threads with only answered user-input prompts

Note

Low Risk
Small, localized change to projection logic that narrows when pending-approval rows are created; risk is limited to approval sidebar/count behavior.

Overview
Fixes an issue where user-input.* activities that include a requestId could incorrectly create/overwrite projection_pending_approvals rows, leaving threads stuck showing Pending Approval.

applyPendingApprovalsProjection now only creates status="pending" rows for activity.kind === "approval.requested", while keeping existing resolve/stale-failure handling intact.

Reviewed by Cursor Bugbot for commit 51ea2b1. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Fix pending approvals projection to only process approval.requested activities

Previously, any activity event carrying a requestId could upsert a row into the pending-approval repository, causing user-input activities to leak into the pending approvals projection. A guard is added in ProjectionPipeline.ts that returns early unless event.payload.activity.kind === 'approval.requested'.

Macroscope summarized 51ea2b1.

…s projection

Activities with a requestId in their payload (e.g. user-input.requested,
user-input.resolved) were incorrectly creating rows in the
projection_pending_approvals table.  The fall-through default branch in
applyPendingApprovalsProjection only checked for approval.resolved and
provider.approval.respond.failed before unconditionally upserting a
"pending" row for any remaining activity carrying a requestId.

This caused threads with answered user-input prompts to permanently
display "Pending Approval" in the sidebar, since user-input.resolved
never clears the erroneous pending row — only approval.resolved does.

Add a kind guard (approval.requested) before the pending-row creation
path, consistent with the backfill migration 024 which already filters
by kind = 'approval.requested'.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 16, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f6d87d32-6693-4f14-8f64-612046babf6e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions github-actions bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XS 0-9 changed lines (additions + deletions). labels Apr 16, 2026
@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp bot commented Apr 16, 2026

Approvability

Verdict: Approved

This is a straightforward bug fix that adds a guard clause to filter out user-input activities from polluting the pending-approvals projection. The change is small (8 lines), well-documented with comments explaining the intent, and has clear, limited scope - it simply adds an early return for activity kinds that shouldn't create pending-approval rows.

You can customize Macroscope's approvability policy. Learn more.

@juliusmarminge juliusmarminge enabled auto-merge (squash) April 16, 2026 05:05
@juliusmarminge juliusmarminge merged commit d22c6f5 into pingdotgg:main Apr 16, 2026
12 checks passed
@marcusgrando marcusgrando deleted the fix/pending-approval-user-input-leak branch April 16, 2026 11:50
smraikai pushed a commit to smraikai/t3code that referenced this pull request Apr 16, 2026
aaditagrawal added a commit to aaditagrawal/t3code that referenced this pull request Apr 16, 2026
Integrates upstream/main (d22c6f5) into the fork while preserving all
multi-provider support (codex, claudeAgent, copilot, cursor, opencode,
geminiCli, amp, kilo) and fork UI/UX additions.

Highlights adopted from upstream:
- Nightly release channel + update channel selector (pingdotgg#2012, pingdotgg#2049, pingdotgg#1969)
- Filesystem browse API + command palette project picker (pingdotgg#2024)
- Launch Args setting for Claude provider (pingdotgg#1971)
- Kiro editor support in open picker (pingdotgg#1974)
- Claude plan events for TodoWrite during input streaming (pingdotgg#1541)
- Lost provider session recovery (pingdotgg#1938)
- Cache provider status and gate desktop startup (pingdotgg#1962)
- LegendList migration for chat scrolling and branch lists (pingdotgg#1953)
- Shell snapshot queries + backfill migration (pingdotgg#1973, pingdotgg#2004)
- PATH hydration + fallback detection (pingdotgg#1799)
- Warm sidebar thread subscriptions (pingdotgg#2001)
- Full thread title tooltip (pingdotgg#1994)
- Markdown file link UX (pingdotgg#1956), composer polish (pingdotgg#1944, pingdotgg#1992, pingdotgg#1985)
- Worktree/branch state + draft reuse fixes (pingdotgg#2005, pingdotgg#2003, pingdotgg#1995, pingdotgg#1936)
- Window controls overlay for Windows/Linux (pingdotgg#1969)
- Backend readiness timeout 10s→30s (pingdotgg#1979)
- Clear tracked RPCs on reconnect, live stream subscriptions (pingdotgg#2000, pingdotgg#1972)
- Various misc fixes (pingdotgg#2051, pingdotgg#2052, pingdotgg#2025, pingdotgg#2027, pingdotgg#2049, pingdotgg#1997, pingdotgg#1975)

Fork features preserved and reconciled:
- All 8 provider adapters + conformance tests
- Extended ProviderKind union across contracts/model/settings/provider
- appearance/accentColor/themeConfig/ProviderLogo UI system
- customModels + gitTextGeneration + providerModelOptions
- Migration IDs 23 (NormalizeLegacyProviderKinds) and 24
  (RepairProjectionThreadProposedPlanImplementationColumns) kept; new
  upstream migrations registered at IDs 25-26 to avoid breaking deployed
  fork databases
- DesktopBridge: log directory channels (LOG_DIR/LIST/READ/OPEN_DIR)
  retained; getWsUrl replaced by upstream's getAppBranding
- PROVIDER_CACHE_IDS extended to all 8 providers
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XS 0-9 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants