Feature/iovalkey wrapper#235
Conversation
…d UI Capture-only wrapper around iovalkey that tees outbound commands to Monitor. Capture is off by default and gated by a Monitor-controlled window. Package (@betterdb/iovalkey-capture): - CaptureValkey extends Redis with sendCommand override - Bounded in-memory buffer with HTTP batching to Monitor - Capture window polling (active/inactive from Monitor) - Fire-and-forget: never blocks, never throws into user code - Stats() for debugging (capturedCount, droppedCount, etc) Backend (apps/api): - CommandCaptureSession entity with start/stop/expire lifecycle - User-facing endpoints: start, stop, status, session read - Wrapper-facing endpoints: poll (GET window), ingest (POST batch) - Instance authorization via ConnectionRegistry (matches MCP pattern) - Storage: full memory + sqlite + postgres implementations - Bulk write path for high-volume ingest - 3-day retention with inline prune throttled to 1/hour Frontend (apps/web): - CommandCaptureControl component on Monitor page - Idle: duration presets, manual input, optional command cap - Active: live countdown, command count, stop control - 5s polling via usePolling (matches monitor cadence) - Auto-reflects expiry without manual refresh
…orcement CommandCaptureModule now imports StorageModule and ConnectionsModule so STORAGE_CLIENT resolves at startup. Wrapper enforces window expiry per command via absolute expiresAt from the poll response, and ingest applies a 5s grace window plus command-cap rejection.
Rename the sidebar entry to "Monitor / Command Capture" and add a Server (MONITOR) / Client Library toggle so the client-capture control and the MONITOR session UI each get their own view.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 990ba0f. Configure here.
| * User-facing endpoints to start/stop command capture sessions. | ||
| * Uses the same auth pattern as the monitor controller. | ||
| */ | ||
| @Controller('api/command-capture') |
There was a problem hiding this comment.
Duplicate api route prefix
High Severity
The capture controllers register under api/... while other Nest routes use paths like monitor and rely on setGlobalPrefix('api') in production. That yields /api/api/... in prod (breaking CaptureValkey calls to /api/capture/...) and /api/command-capture/... in dev while the web client calls /command-capture/... without the extra segment.
Reviewed by Cursor Bugbot for commit 990ba0f. Configure here.
| const saved = await this.storage.saveCommandCaptureRecords(records); | ||
| await this.storage.updateCommandCaptureSession(active.id, { | ||
| commandCount: active.commandCount + saved, | ||
| }); |
There was a problem hiding this comment.
Ingest ignores command cap remainder
High Severity
ingestBatch only rejects when commandCount is already at the cap, then persists every command in the batch. A single large batch can push commandCount well above commandCap, contradicting server-side cap enforcement described in the PR.
Reviewed by Cursor Bugbot for commit 990ba0f. Configure here.
| const saved = await this.storage.saveCommandCaptureRecords(records); | ||
| await this.storage.updateCommandCaptureSession(active.id, { | ||
| commandCount: active.commandCount + saved, | ||
| }); |
There was a problem hiding this comment.
Concurrent ingest loses command count
Medium Severity
Each ingest reads commandCount, inserts records, then writes commandCount + saved from that stale snapshot. Overlapping batch requests can overwrite each other’s increments, so totals and cap checks diverge from rows actually stored.
Reviewed by Cursor Bugbot for commit 990ba0f. Configure here.
|
Ok
…On Thu, 11 Jun 2026, 1:21 am cursor[bot], ***@***.***> wrote:
***@***.***[bot]* commented on this pull request.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
[image: Fix All in Cursor]
<https://cursor.com/open?link=eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiQlVHQk9UX0ZJWF9BTExfSU5fQ1VSU09SIiwiZGF0YSI6eyJyZWRpc0tleSI6ImJ1Z2JvdC1tdWx0aTpiMWY3ZjM4My1iOTc2LTQyMjEtOTc0MC05ODI2N2VkNmJiMDQiLCJlbmNyeXB0aW9uS2V5IjoiaElidVE4QUpudFQwT1JwWmZ6dUNpbFVldUVrNkZYelFOS081Qk1MMGdJcyIsImJyYW5jaCI6ImZlYXR1cmUvaW92YWxrZXktd3JhcHBlciIsInJlcG9Pd25lciI6IkJldHRlckRCLWluYyIsInJlcG9OYW1lIjoibW9uaXRvciJ9fQ>
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud
agents, enable autofix in the Cursor dashboard
<https://www.cursor.com/dashboard/bugbot>.
Reviewed by Cursor Bugbot <https://cursor.com/bugbot> for commit 990ba0f
<990ba0f>.
Configure here <https://www.cursor.com/dashboard/bugbot>.
------------------------------
In apps/api/src/command-capture/command-capture.controller.ts
<#235 (comment)>:
> + @Param('instanceId') instanceId: string,
+ @Body() body: CaptureBatchRequest,
+ ): Promise<{ accepted: number; dropped: boolean }> {
+ this.assertInstanceExists(instanceId);
+ if (!body || !Array.isArray(body.commands)) {
+ throw new BadRequestException('Invalid batch: commands array required');
+ }
+ return this.captureService.ingestBatch(instanceId, body);
+ }
+}
+
+/**
+ * User-facing endpoints to start/stop command capture sessions.
+ * Uses the same auth pattern as the monitor controller.
+ */
***@***.***('api/command-capture')
Duplicate api route prefix
*High Severity*
The capture controllers register under api/... while other Nest routes
use paths like monitor and rely on setGlobalPrefix('api') in production.
That yields /api/api/... in prod (breaking CaptureValkey calls to
/api/capture/...) and /api/command-capture/... in dev while the web
client calls /command-capture/... without the extra segment.
[image: Fix in Cursor]
<https://cursor.com/open?link=eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiQlVHQk9UX0ZJWF9JTl9DVVJTT1IiLCJkYXRhIjp7InJlZGlzS2V5IjoiYnVnYm90OjYzMDIxN2M5LWNhMTItNGIzYi1iODdlLWUxYWMzMGUzNTI2ZSIsImVuY3J5cHRpb25LZXkiOiJzTm5ORXZLdEdfX3pYRmR5Y2lmTjlPOGlfS1N5Vk9fM1QtWTE4cXRIQ1NnIiwiYnJhbmNoIjoiZmVhdHVyZS9pb3ZhbGtleS13cmFwcGVyIiwicmVwb093bmVyIjoiQmV0dGVyREItaW5jIiwicmVwb05hbWUiOiJtb25pdG9yIn19>
[image: Fix in Web]
<https://cursor.com/agents?link=eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiQlVHQk9UX0ZJWF9JTl9XRUIiLCJkYXRhIjp7InJlZGlzS2V5IjoiYnVnYm90OjYzMDIxN2M5LWNhMTItNGIzYi1iODdlLWUxYWMzMGUzNTI2ZSIsImVuY3J5cHRpb25LZXkiOiJzTm5ORXZLdEdfX3pYRmR5Y2lmTjlPOGlfS1N5Vk9fM1QtWTE4cXRIQ1NnIiwiYnJhbmNoIjoiZmVhdHVyZS9pb3ZhbGtleS13cmFwcGVyIiwicmVwb093bmVyIjoiQmV0dGVyREItaW5jIiwicmVwb05hbWUiOiJtb25pdG9yIiwicHJOdW1iZXIiOjIzNSwiY29tbWl0U2hhIjoiOTkwYmEwZjYwYzQyZGI4YWMyMTkzM2IxZGVhOGU1YTg2MmExMzNjZiIsInByb3ZpZGVyIjoiZ2l0aHViIn19>
Reviewed by Cursor Bugbot <https://cursor.com/bugbot> for commit 990ba0f
<990ba0f>.
Configure here <https://www.cursor.com/dashboard/bugbot>.
------------------------------
In apps/api/src/command-capture/command-capture.service.ts
<#235 (comment)>:
> + return { accepted: 0, dropped: true };
+ }
+
+ const records: StoredCommandCaptureRecord[] = batch.commands.map((cmd) => ({
+ sessionId: active.id,
+ connectionId,
+ wrapperConnectionId: batch.connectionId,
+ name: cmd.name,
+ args: cmd.args,
+ ts: cmd.ts,
+ }));
+
+ const saved = await this.storage.saveCommandCaptureRecords(records);
+ await this.storage.updateCommandCaptureSession(active.id, {
+ commandCount: active.commandCount + saved,
+ });
Ingest ignores command cap remainder
*High Severity*
ingestBatch only rejects when commandCount is already at the cap, then
persists every command in the batch. A single large batch can push
commandCount well above commandCap, contradicting server-side cap
enforcement described in the PR.
[image: Fix in Cursor]
<https://cursor.com/open?link=eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiQlVHQk9UX0ZJWF9JTl9DVVJTT1IiLCJkYXRhIjp7InJlZGlzS2V5IjoiYnVnYm90OjNjYzFlYjRjLTM3YTItNGZjYy05MjE5LTUzNTcyYjI1NWIwNSIsImVuY3J5cHRpb25LZXkiOiJ4dkREcTZPYm5GazhVQVozdUNMc196RjZFMWJjT0h3ZFZSN3dHd1BjeF9FIiwiYnJhbmNoIjoiZmVhdHVyZS9pb3ZhbGtleS13cmFwcGVyIiwicmVwb093bmVyIjoiQmV0dGVyREItaW5jIiwicmVwb05hbWUiOiJtb25pdG9yIn19>
[image: Fix in Web]
<https://cursor.com/agents?link=eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiQlVHQk9UX0ZJWF9JTl9XRUIiLCJkYXRhIjp7InJlZGlzS2V5IjoiYnVnYm90OjNjYzFlYjRjLTM3YTItNGZjYy05MjE5LTUzNTcyYjI1NWIwNSIsImVuY3J5cHRpb25LZXkiOiJ4dkREcTZPYm5GazhVQVozdUNMc196RjZFMWJjT0h3ZFZSN3dHd1BjeF9FIiwiYnJhbmNoIjoiZmVhdHVyZS9pb3ZhbGtleS13cmFwcGVyIiwicmVwb093bmVyIjoiQmV0dGVyREItaW5jIiwicmVwb05hbWUiOiJtb25pdG9yIiwicHJOdW1iZXIiOjIzNSwiY29tbWl0U2hhIjoiOTkwYmEwZjYwYzQyZGI4YWMyMTkzM2IxZGVhOGU1YTg2MmExMzNjZiIsInByb3ZpZGVyIjoiZ2l0aHViIn19>
Reviewed by Cursor Bugbot <https://cursor.com/bugbot> for commit 990ba0f
<990ba0f>.
Configure here <https://www.cursor.com/dashboard/bugbot>.
------------------------------
In apps/api/src/command-capture/command-capture.service.ts
<#235 (comment)>:
> + return { accepted: 0, dropped: true };
+ }
+
+ const records: StoredCommandCaptureRecord[] = batch.commands.map((cmd) => ({
+ sessionId: active.id,
+ connectionId,
+ wrapperConnectionId: batch.connectionId,
+ name: cmd.name,
+ args: cmd.args,
+ ts: cmd.ts,
+ }));
+
+ const saved = await this.storage.saveCommandCaptureRecords(records);
+ await this.storage.updateCommandCaptureSession(active.id, {
+ commandCount: active.commandCount + saved,
+ });
Concurrent ingest loses command count
*Medium Severity*
Each ingest reads commandCount, inserts records, then writes commandCount
+ saved from that stale snapshot. Overlapping batch requests can
overwrite each other’s increments, so totals and cap checks diverge from
rows actually stored.
[image: Fix in Cursor]
<https://cursor.com/open?link=eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiQlVHQk9UX0ZJWF9JTl9DVVJTT1IiLCJkYXRhIjp7InJlZGlzS2V5IjoiYnVnYm90OmY4MTAyZmRjLTJmMWMtNGUyYi05NDBjLWVmMjg1YjM0NWE4NiIsImVuY3J5cHRpb25LZXkiOiJHX2VqZXZTd09HNU9LVUJIckhEaFFibFpyYnVEU3FPZVhMNWVWbUFsbUpVIiwiYnJhbmNoIjoiZmVhdHVyZS9pb3ZhbGtleS13cmFwcGVyIiwicmVwb093bmVyIjoiQmV0dGVyREItaW5jIiwicmVwb05hbWUiOiJtb25pdG9yIn19>
[image: Fix in Web]
<https://cursor.com/agents?link=eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiQlVHQk9UX0ZJWF9JTl9XRUIiLCJkYXRhIjp7InJlZGlzS2V5IjoiYnVnYm90OmY4MTAyZmRjLTJmMWMtNGUyYi05NDBjLWVmMjg1YjM0NWE4NiIsImVuY3J5cHRpb25LZXkiOiJHX2VqZXZTd09HNU9LVUJIckhEaFFibFpyYnVEU3FPZVhMNWVWbUFsbUpVIiwiYnJhbmNoIjoiZmVhdHVyZS9pb3ZhbGtleS13cmFwcGVyIiwicmVwb093bmVyIjoiQmV0dGVyREItaW5jIiwicmVwb05hbWUiOiJtb25pdG9yIiwicHJOdW1iZXIiOjIzNSwiY29tbWl0U2hhIjoiOTkwYmEwZjYwYzQyZGI4YWMyMTkzM2IxZGVhOGU1YTg2MmExMzNjZiIsInByb3ZpZGVyIjoiZ2l0aHViIn19>
Reviewed by Cursor Bugbot <https://cursor.com/bugbot> for commit 990ba0f
<990ba0f>.
Configure here <https://www.cursor.com/dashboard/bugbot>.
—
Reply to this email directly, view it on GitHub
<#235?email_source=notifications&email_token=B3U4DQYPHJ53PKQ5EJQPL6347G34BA5CNFSNUABKM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UKJSXM2LFO4XTINBXGEYTCMZVGAZ2M4TFMFZW63VKON2WE43DOJUWEZLEUVSXMZLOOSWGM33PORSXEX3DNRUWG2Y#pullrequestreview-4471113503>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/B3U4DQ2IIHI3UGM2USOKZD347G34BAVCNFSNUABGKJSXA33TNF2G64TZHMYTCMRUHAYDSNBVHA5US43TOVSTWNBWGM2DINRUGY2TBILWAI>
.
You are receiving this because you are subscribed to this thread.Message
ID: ***@***.***>
|


Summary
Adds client-side command capture: a new @betterdb/iovalkey-capture package that wraps iovalkey and tees commands from instrumented applications to Monitor, plus the backend session/ingest pipeline and UI to control capture windows. Unlike server-side MONITOR, this works on managed instances where the MONITOR command is disabled, and captures only the traffic of opted-in clients with bounded overhead.
Changes
Checklist
roborev review --branchor/roborev-review-branchin Claude Code (internal)Note
Medium Risk
New ingest path stores full Redis command names/args (may include secrets) and relies on agent tokens that are not instance-scoped; bounded caps and retention limit blast radius but the feature touches auth and sensitive workload data.
Overview
Adds client-side command capture alongside existing server MONITOR capture: a new
@betterdb/iovalkey-capturepackage (CaptureValkeysubclasses iovalkey, tees commands viasendCommand, polls for active windows, and POSTs batched commands to Monitor without affecting caller Redis behavior).The API gains
CommandCaptureModulewith session lifecycle (one active window per connection, optional command cap, lazy expiry/stop), wrapper routes underapi/capture/instance/:instanceId(window+batch,AgentTokenGuard+ registry check), and admin routes underapi/command-capturefor start/stop/status. Captured commands persist through newcommand_capture_sessions/command_capture_recordsstorage APIs on memory, SQLite, and Postgres, with ~3-day retention and throttled prune on ingest.Expiry and caps are enforced on both sides: the wrapper gates on
expiresAtand stops at cap; ingest uses a 5s post-expiry grace, drops when no session or at cap, and updatescommandCount.The web app renames the Monitor area to Monitor / Command Capture, adds a Server (MONITOR) vs Client Library tab, and
CommandCaptureControlto start/stop windows with duration presets, optional cap, and live countdown/command count. Shared types for command-capture sessions/records live in@betterdb/shared.Reviewed by Cursor Bugbot for commit 990ba0f. Bugbot is set up for automated code reviews on this repo. Configure here.