Skip to content

Cache proposal approve/reject/edit + apply dispatcher#137

Merged
jamby77 merged 9 commits into
feature/cache-mcp-readonly-toolsfrom
feature/cache-apply-logic-and-approval-api
Apr 29, 2026
Merged

Cache proposal approve/reject/edit + apply dispatcher#137
jamby77 merged 9 commits into
feature/cache-mcp-readonly-toolsfrom
feature/cache-apply-logic-and-approval-api

Conversation

@jamby77

@jamby77 jamby77 commented Apr 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements the cache proposal approval and apply pipeline for issue
179753285 (cache intelligence v1, advisory mode):

  • CacheProposalService: new approve, reject, editAndApprove,
    expireProposals, getProposalWithAudit methods, sharing logic between
    UI (HTTP) and MCP callers
  • CacheApplyService: idempotent transition approved → applied | failed,
    writes applied/failed audit
  • CacheApplyDispatcher: 4 handlers
    • semantic + threshold_adjust: HSET {cache_name}:__config threshold[:category] <value>
      (gated on threshold_adjust capability in discovery marker)
    • agent + tool_ttl_adjust: HSET {cache_name}:__tool_policies <tool> <json>
    • semantic + invalidate: FT.SEARCH + DEL
    • agent + invalidate: SCAN + DEL on tool/key_prefix/session patterns
  • CacheExpirationCron: 5-min setInterval, drives
    expireCacheProposalsBefore, writes actor_source='system' audit
  • HTTP controller under /cache-proposals: 6 endpoints (pending, history,
    get, approve, reject, edit-and-approve)
  • MCP controller: 5 new endpoints for the approval flow
    (actor_source='mcp')
  • Extracts shared mapCacheProposalErrorToHttp helper

Stacked on PR #136 (feature/cache-mcp-readonly-tools).

Test plan

  • Unit tests pass: 87 cache-related specs (18 new, 69 pre-existing)
  • tsc --noEmit clean
  • eslint clean
  • Manual e2e: propose → approve → verify Valkey HSET (left for the
    integration-tests spec)

Note

Medium Risk
Adds new write/delete operations against Valkey (including bulk invalidation) and new state transitions/auditing paths; mistakes could lead to unintended cache data loss or stuck proposals, though changes are guarded by approval flow and covered by unit tests.

Overview
Implements an end-to-end cache proposal review workflow: new API endpoints to list pending/history, fetch proposal+audit, and approve/reject/edit-and-approve, with shared error-to-HTTP mapping and controller input validation.

Adds an apply pipeline that transitions approved proposals to applied or failed with audit events, plus a CacheApplyDispatcher that executes Valkey-side changes (semantic threshold HSET, agent tool TTL policy HSET, semantic invalidate via FT.SEARCH+DEL, agent invalidate via SCAN+DEL) and a background cron that periodically expires overdue proposals.

Web UI gains a new Cache Proposals page (pending + history + detail/audit drawer), sidebar unread badge (localStorage last-seen), and hooks/API client for fetching and mutating proposals; comprehensive unit tests cover dispatcher behavior and proposal state transitions.

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

@jamby77 jamby77 changed the title Cache proposal approve/reject/edit + apply dispatcher (Day 5–6) Cache proposal approve/reject/edit + apply dispatcher Apr 27, 2026
Comment thread apps/api/src/cache-proposals/cache-apply.dispatcher.ts
Comment thread apps/api/src/cache-proposals/cache-proposal.controller.ts Outdated
Comment thread apps/api/src/cache-proposals/cache-apply.dispatcher.ts
@jamby77
jamby77 force-pushed the feature/cache-mcp-readonly-tools branch from 720786d to 54794d1 Compare April 27, 2026 13:08
@jamby77
jamby77 force-pushed the feature/cache-apply-logic-and-approval-api branch from eee5420 to db14ecd Compare April 27, 2026 13:09
@jamby77
jamby77 force-pushed the feature/cache-mcp-readonly-tools branch from 54794d1 to 85cd223 Compare April 27, 2026 13:25
@jamby77
jamby77 force-pushed the feature/cache-apply-logic-and-approval-api branch from fc837be to caaa3b7 Compare April 27, 2026 13:25
Comment thread apps/api/src/cache-proposals/cache-apply.dispatcher.ts
Comment thread apps/api/src/cache-proposals/cache-apply.dispatcher.ts Outdated
Comment thread apps/api/src/cache-proposals/cache-proposal.controller.ts
Comment thread apps/api/src/cache-proposals/cache-apply.dispatcher.ts
@jamby77
jamby77 force-pushed the feature/cache-mcp-readonly-tools branch from 85cd223 to af032f6 Compare April 27, 2026 13:46
jamby77 added 4 commits April 27, 2026 16:46
- Add approve/reject/editAndApprove/expireProposals on CacheProposalService
  sharing logic between HTTP controller (actor_source='ui') and MCP tools
  (actor_source='mcp')
- Add CacheApplyService that runs Valkey work and transitions
  approved -> applied|failed with idempotency on re-entry
- Add CacheApplyDispatcher with 4 handlers:
  - semantic threshold_adjust: HSET {cache_name}:__config (gated on
    threshold_adjust capability advertised in marker)
  - agent tool_ttl_adjust: HSET {cache_name}:__tool_policies
  - semantic invalidate: FT.SEARCH + DEL
  - agent invalidate: SCAN + DEL on tool/key_prefix/session pattern
- Add CacheExpirationCron (5 min cadence, setInterval pattern)
  marking past-due pending proposals as expired with system audit
- Add /cache-proposals HTTP controller with 6 endpoints
- Add 5 MCP approval endpoints in mcp.controller.ts
- Extract shared mapCacheProposalErrorToHttp helper used by both
  controllers and add typed errors for proposal lifecycle
- Tests: 18 new specs covering approve/reject/edit/expire flows and
  dispatcher behaviour
- HIGH: parseFtSearchKeys was iterating with i+=2, missing every other
  key when FT.SEARCH was called with RETURN 0 (which behaves like
  NOCONTENT and returns just [count, key1, key2, ...]). Step by 1.
- LOW: extract optionalString, optionalFiniteNumber, formatApprovalResult
  to controller-helpers.ts, share between cache-proposal.controller and
  mcp.controller
- Add regression test exercising RETURN 0 response shape
bugbot HIGH: applyAgentInvalidate's key_prefix branch built a SCAN
pattern as `<filter_value>*` without prepending `<cache.name>:`,
unlike the tool/session branches. This let a key_prefix invalidation
match keys belonging to other caches or unrelated application data on
the same Valkey instance. Scope the pattern to the cache namespace and
add a regression test.
…lob cache name

bugbot MEDIUM: applySemanticThresholdAdjust, applyAgentToolTtlAdjust,
applySemanticInvalidate were passing cache.name as the first arg to
ApplyFailedError, which the constructor labels as proposalId — that
wrong value was landing in applied_result.details. Plus, the constructor
spread '{ proposalId, ...details }' allowed a stale proposalId in
details to overwrite the explicit one. Pass proposalId through to all
dispatcher methods, attach cacheName separately in details, and put the
explicit proposalId last in the spread so it always wins.

bugbot LOW: tool and session SCAN patterns interpolated cache.name
without escaping while key_prefix did. Apply escapeGlob uniformly to
cache.name across all three branches.
@jamby77
jamby77 force-pushed the feature/cache-apply-logic-and-approval-api branch from c4ae2d2 to af5cf8b Compare April 27, 2026 13:46
Add /cache-proposals route with Pending and History tabs. Pending cards
render four (cache_type, proposal_type) variants with approve, reject,
and edit-and-approve flows. Edit hidden on invalidate cards. History
table filters by status/cache_name and opens a detail panel with full
reasoning, payload, and audit trail. Sidebar shows an unread badge for
new pending proposals.
@jamby77 jamby77 mentioned this pull request Apr 28, 2026
4 tasks
jamby77 added 4 commits April 28, 2026 11:20
Use a module-level subscription store with useSyncExternalStore so the
sidebar badge clears when CacheProposals.markAllRead() runs. Wraps
markAllRead in useCallback to avoid firing the page-level effect every
render. Drops the unused cacheProposalQueryKeys export.
- proposalSource now reads proposed_by only (was falling through to
  reviewed_by, mislabelling UI-proposed/MCP-reviewed entries as 'mcp').
- Track unread by last-seen timestamp instead of id, so when the
  marker proposal is approved/rejected/expired it doesn't inflate the
  unread count to all remaining (already-seen) pending proposals.
…sion

Math.max guards setLastSeenAt so a markAllRead call where the newest
pending proposal has a smaller timestamp (e.g. the previous newest was
approved by another user) cannot move the marker backward and resurrect
already-seen proposals as unread.
## Summary

- Add the **Cache Proposals** tab to Monitor (route `/cache-proposals`)
with Pending and History sub-views.
- **Pending view**: cards render the 4 `(cache_type, proposal_type)`
variants — semantic threshold adjust, agent tool TTL adjust, semantic
invalidate, agent invalidate. Approve is optimistic ("Applying…"),
Reject opens an inline reason input, Edit swaps the proposed value for
an inline numeric input that posts via `/edit-and-approve`. Edit is
hidden on invalidate cards. `estimated_affected > 10000` triggers a warn
visual.
- **History view**: table with status + cache_name filters, source
column, clickable rows opening a detail panel with reasoning, payload,
apply result, and full audit trail.
- **Unread indicator**: sidebar badge on the Cache Proposals nav item,
cleared when the user opens the Pending tab. Persisted in localStorage.
- New helpers: `lib/formatters.ts` (`formatTtlSeconds`, `formatTimeAgo`,
`formatExpiresIn`).
- Component tests (Vitest + RTL) cover all 4 card variants, edit hidden
on invalidate, warn visual at >10000, optimistic Applying state, reject
flow with/without reason, edit-and-approve, and direct approve.

Stacked on #137 — merge after the apply-dispatcher PR lands.

Closes the UI portion of issue-179753285 (Days 7–9). Spec:
`docs/plans/specs/spec-cache-proposals-ui.md`.

## Test plan

- [ ] `pnpm --filter @betterdb/web test` — all web tests pass (175
tests, 27 files).
- [ ] `pnpm --filter @betterdb/web build` — typecheck + Vite build
clean.
- [ ] Manual: visit `/cache-proposals`, verify all 4 card variants
render correctly, approve/reject/edit flows POST to the right endpoint,
history filters work, detail panel opens with audit trail, unread badge
clears on tab visit.
- [ ] Verify no hard-coded hex colours — all status colours use
`var(--chart-*)` / shadcn theme variables.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Medium risk because it introduces new approve/reject/edit-and-approve
mutation flows that trigger operational changes via new API endpoints,
plus polling/unread state persisted in localStorage.
> 
> **Overview**
> Introduces a new `/cache-proposals` page with **Pending** and
**History** views for reviewing agent-submitted cache proposals.
> 
> Pending proposals render as actionable cards (approve, reject with
optional reason, and edit+approve for supported proposal types) with
basic validation and “high impact” highlighting; history adds filtering
and a row-click detail sheet that shows payload, apply result, and audit
events.
> 
> Adds a `cacheProposalsApi` client plus React Query hooks (including
pending polling and query invalidation) and a sidebar **unread badge**
backed by `localStorage`, along with new time/TTL formatting utilities
and targeted component/unit tests.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
1bef37b. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
@jamby77
jamby77 merged commit 1f6442e into feature/cache-mcp-readonly-tools Apr 29, 2026
2 checks passed
@jamby77
jamby77 deleted the feature/cache-apply-logic-and-approval-api branch April 29, 2026 06:24
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 29, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant