Skip to content

feat: add Pandora read-only action runner#135

Merged
besfeng23 merged 1 commit into
mainfrom
codex/upgrade-operator-action-center-to-read-only-runner
Jul 3, 2026
Merged

feat: add Pandora read-only action runner#135
besfeng23 merged 1 commit into
mainfrom
codex/upgrade-operator-action-center-to-read-only-runner

Conversation

@besfeng23

@besfeng23 besfeng23 commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Motivation

  • Upgrade the Operator Action Center into an approval-gated read-only execution runner so operators can approve and run safe, read-only verification workflows with full audit history.
  • Preserve strict Pandora safety boundaries: no model calls, no embeddings, no destructive memory mutations, and never trust client-supplied user_id — all identities come from server-derived sessions.
  • Provide clear, auditable lifecycle states and timelines so verification evidence and warnings are visible in the dashboard without changing core memory tables.

Description

  • Extend the operator action lifecycle and enforce valid transitions by adding statuses and validators for proposed, dry_ran, approved, executing, completed, blocked, failed, and cancelled, and implement assertTransition/TERMINAL protection in lib/services/pandora-operator-action-service.ts.
  • Add service functions approveOperatorAction, executeApprovedOperatorAction, getOperatorAction, and listOperatorActionEvents that validate ownership, action type, and mode; write audited event rows for every transition; and always return read-only execution envelopes including no_mutation_performed: true and execution metadata.
  • Add authenticated internal API routes that use server session identity and reject client user_id overrides: POST /api/pandora/operator-actions/[id]/approve, POST /api/pandora/operator-actions/[id]/execute, and GET /api/pandora/operator-actions/[id]/events, and harden existing dry-run and cancel routes to produce events and respect terminal states.
  • Update the dashboard loader to include recent actions, event previews, and counts by status (action-level event_preview, event_count, approved_at, completed_at, failed_at, and countsByStatus) without letting action-table read failures crash /pandora.
  • Add UI pieces and safe controls: OperatorActionControls (Prepare dry-run, Approve, Execute approved read-only action, Cancel), OperatorActionTimeline, and UI updates to OperatorActionEnvelope/OperatorActionList/OperatorActionCenterCard to show lifecycle, request_id, shortened idempotency, timestamps, warnings, and No mutation performed visibility.
  • Add an additive migration supabase/migrations/20260703010000_pandora_operator_action_runner_statuses.sql to expand the status check constraint safely; do not change or drop any core memory tables or RLS policies.
  • Update docs: docs/pandora-operator-action-center.md and add docs/pandora-readonly-action-runner.md describing lifecycle, allowed read-only executions, safety boundaries, and smoke-test checklist.

Testing

  • Ran npm run typecheck which passed successfully with no blocking errors.
  • Ran npm run lint which completed; project had existing lint warnings unrelated to these changes but no new lint errors from the runner changes.
  • Ran npm run test and the test suite passed (96 files, 615 tests passed overall); unit tests added/updated for operator actions include service and UI tests covering approve/execute/event ownership, transition guards, idempotency, and no-mutation envelopes.
  • Ran npm run build (Next.js build) which succeeded with existing runtime/warnings unrelated to safety, and ran npm run env:policy which passed Env Broker checks.

Codex Task

Summary by CodeRabbit

  • New Features

    • Added action approval, execution, cancellation, and event history views for Pandora operator actions.
    • Added clearer status summaries, lifecycle timestamps, and per-action timeline previews in the UI.
    • Updated the dashboard to show counts by action status.
  • Bug Fixes

    • Strengthened protections against client-side user overrides.
    • Improved error handling and status validation for action lifecycle changes.
    • Updated action flows to use the latest approved status states.

@vercel

vercel Bot commented Jul 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
memory Ready Ready Preview, Comment Jul 3, 2026 9:04pm

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR extends the Pandora Operator Action Center with an approval/execution lifecycle. It adds new approve, execute, and events API routes; hardens cancel/dry-run routes against client userId overrides; introduces approved/executing statuses with transition guards; updates dashboard data to include event previews and status counts; adds UI components for controls and timelines; adds a DB migration for the new status set; and updates docs and tests accordingly.

Changes

Operator Action Approval Workflow

Layer / File(s) Summary
Status types and data contracts
components/pandora/types.ts
OperatorActionStatus replaces "queued" with "approved"/"executing"; adds OperatorActionEventSummary type and extends OperatorActionSummary/OperatorActionCenterData with timestamps, event fields, and countsByStatus.
Service-layer state machine and lifecycle functions
lib/services/pandora-operator-action-service.ts
Adds assertTransition/TERMINAL guards, getOperatorAction/listOperatorActionEvents helpers, and reworks proposeOperatorAction, dryRunOperatorAction, approveOperatorAction, executeApprovedOperatorAction, and cancelOperatorAction.
Status check constraint migration
supabase/migrations/20260703010000_pandora_operator_action_runner_statuses.sql
Recreates the status CHECK constraint to allow the expanded set of eight statuses.
Approve/execute/events routes and cancel/dry-run hardening
app/api/pandora/operator-actions/[id]/approve/route.ts, .../execute/route.ts, .../events/route.ts, .../cancel/route.ts, .../dry-run/route.ts
Adds new approve, execute, and events endpoints; adds assertNoClientUserIdOverride checks and body parsing to cancel/dry-run; changes some error responses from 404 to 400.
Dashboard service event aggregation
lib/services/pandora-dashboard-service.ts
Fetches per-action events, builds eventsByAction, computes countsByStatus, and adds event_count/event_preview to returned actions.
UI components for status counts, timeline, and controls
components/pandora/OperatorActionCenterCard.tsx, OperatorActionControls.tsx, OperatorActionTimeline.tsx, OperatorActionEnvelope.tsx, OperatorActionList.tsx, mock-data.ts
Adds OperatorActionControls and OperatorActionTimeline components, a status-count mini-grid, lifecycle timestamp tiles, updated status color map, and mock fixture counts.
Documentation updates
docs/pandora-operator-action-center.md, docs/pandora-readonly-action-runner.md
Rewrites scope/lifecycle sections and adds a new read-only action runner doc.
Unit test coverage updates
tests/unit/pandora-operator-action-guards.test.ts, pandora-operator-action-service.test.ts, pandora-operator-action-ui.test.tsx
Expands guard, service, and UI tests for new statuses, transitions, and components.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ApproveRoute
  participant ExecuteRoute
  participant OperatorActionService
  participant Supabase

  Client->>ApproveRoute: POST /approve
  ApproveRoute->>OperatorActionService: approveOperatorAction(userId, actionId)
  OperatorActionService->>Supabase: assertTransition -> approved
  Supabase-->>OperatorActionService: updated row
  OperatorActionService-->>ApproveRoute: action
  ApproveRoute-->>Client: { ok: true, action }

  Client->>ExecuteRoute: POST /execute
  ExecuteRoute->>OperatorActionService: executeApprovedOperatorAction(userId, actionId)
  OperatorActionService->>Supabase: assertTransition approved -> executing
  OperatorActionService->>Supabase: persist completed/failed + timestamps
  Supabase-->>OperatorActionService: updated row
  OperatorActionService-->>ExecuteRoute: { action, result }
  ExecuteRoute-->>Client: { ok: true, action, result }
Loading
sequenceDiagram
  participant User
  participant OperatorActionControls
  participant APIRoute
  participant Browser

  User->>OperatorActionControls: click Approve/Execute/Cancel
  OperatorActionControls->>APIRoute: POST /api/pandora/operator-actions/{id}/{verb}
  APIRoute-->>OperatorActionControls: { ok, action }
  OperatorActionControls->>Browser: window.location.reload()
Loading

Related PRs: None identified.

Suggested labels: pandora, api, backend, frontend

Suggested reviewers: besfeng23

🐰 A dry-run once, now approved with care,
Executing actions read-only, no mutation to spare,
Timelines unfold, controls click and reload,
Statuses transition down a guarded road,
Hop by hop, the operator center grows.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description does not follow the repository template and omits the required checklist answers about env vars and secrets. Rewrite the description to answer each checklist item: env vars, Env Broker catalog, server-only secrets, and public-safe NEXT_PUBLIC values.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding the Pandora read-only action runner.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ 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 codex/upgrade-operator-action-center-to-read-only-runner

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.

@besfeng23 besfeng23 merged commit 87857c9 into main Jul 3, 2026
4 of 5 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 66f33ba157

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


alter table public.pandora_operator_actions
add constraint pandora_operator_actions_status_check
check (status in ('proposed','dry_ran','approved','executing','completed','blocked','failed','cancelled'));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve queued rows before tightening status checks

In any deployment that already has operator actions created with mode = 'queued_only', the previous service stored those rows with status = 'queued' and the original constraint allowed that value. This migration removes queued from the allowed set without updating existing rows first, so PostgreSQL will reject the new check constraint when such rows exist and the rollout will be blocked. Please migrate existing queued rows to a supported state or keep queued allowed until cleanup is complete.

Useful? React with 👍 / 👎.

if (existing) return existing;
const now = new Date().toISOString(); const requestId = randomUUID();
const row = { id: randomUUID(), user_id: input.userId, request_id: requestId, idempotency_key: idempotencyKey, action_type: input.actionType, namespace: input.namespace ?? null, mode, status: mode === "queued_only" ? "queued" : "proposed", title: titleFor(input.actionType), description: "Operator-proposed safe action. Initial implementation is dry-run or queued-only and cannot mutate core memory truth tables.", payload: input.payload ?? {}, result: {}, warnings: [], created_at: now, updated_at: now, approved_at: null, completed_at: null, failed_at: null };
const row = { id: randomUUID(), user_id: input.userId, request_id: requestId, idempotency_key: idempotencyKey, action_type: input.actionType, namespace: input.namespace ?? null, mode, status: "proposed", title: titleFor(input.actionType), description: "Operator-proposed safe action. Initial implementation is dry-run or queued-only and cannot mutate core memory truth tables.", payload: input.payload ?? {}, result: {}, warnings: [], created_at: now, updated_at: now, approved_at: null, completed_at: null, failed_at: null };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep queued-only proposals out of executable flow

When an API client still submits mode: "queued_only" (the route and assertMode continue to accept it), this now stores the action as proposed. The new transition table allows proposed -> approved -> executing, so a request that explicitly chose queued-only mode can be approved and executed by the new runner instead of remaining queue-only. Either reject queued_only for runner actions or preserve a non-executable queued state for that mode.

Useful? React with 👍 / 👎.

const result = envelope({ ...executing, status: "completed" }, built.result, built.warnings);
const completed = await single(client.from("pandora_operator_actions").update({ status: "completed", result, warnings: built.warnings, completed_at: completedAt, updated_at: completedAt }).eq("user_id", input.userId).eq("id", input.actionId).select("*").single());
await createActionEvent(client, { userId: input.userId, actionId: input.actionId, eventType: "completed", message: "Approved read-only execution completed. No core memory mutation was performed.", metadata: result });
return completed ?? { ...executing, status: "completed", result, warnings: built.warnings, completed_at: completedAt, updated_at: completedAt };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Surface failed completion writes instead of fabricating success

If the terminal update fails after the read-only work succeeds (for example because the migration/grants are missing or RLS rejects the update), single() returns null and this fallback returns a synthetic completed action anyway. The route then reports { ok: true } even though the database may still show the action as approved/executing and no durable completion row was written, which contradicts the runner's audit-backed completion semantics. Please propagate the update error instead of fabricating the terminal state.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (8)
components/pandora/OperatorActionEnvelope.tsx (1)

12-14: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

"Pending" is misleading for actions that will never reach that stage.

For a cancelled, failed, or blocked action that never got approved, approved_at ?? "Pending" implies approval is still forthcoming. Consider distinguishing "will not happen" (e.g., "N/A") from "not yet happened" (e.g., "Pending") based on action.status.

🤖 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 `@components/pandora/OperatorActionEnvelope.tsx` around lines 12 - 14, The
`OperatorActionEnvelope` status timestamps currently use `?? "Pending"` for
`approved_at`, `completed_at`, and `failed_at`, which can mislead users for
terminal states like cancelled, failed, or blocked. Update the render logic to
branch on `action.status` so `approved_at` shows a non-applicable label such as
“N/A” when approval will never happen, while still showing “Pending” only for
actions that are still in progress. Keep the change localized to the timestamp
display in `OperatorActionEnvelope` and use the existing `action.status` field
to decide the label.
lib/services/pandora-dashboard-service.ts (1)

44-53: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

N+1 event queries per dashboard load.

For every operator action returned (up to 10), a separate round trip to pandora_operator_action_events is issued. This is an N+1 query pattern on a dashboard read path; a single query filtering action_id via .in(...) (grouping/slicing the top-3-per-action client-side) would cut this to one round trip.

♻️ Suggested single-query approach
-  const actionEvents = await Promise.all(operatorActions.map(async (action) => {
-    try {
-      const result = await client.from("pandora_operator_action_events").select("*").eq("user_id", input.userId).eq("action_id", action.id).order("created_at", { ascending: false }).limit(3);
-      if (result.error) { actionWarnings.push(`operator action events unavailable for ${action.id}; showing action without timeline.`); return { actionId: action.id, events: [] as Row[] }; }
-      return { actionId: action.id, events: Array.isArray(result.data) ? result.data : [] };
-    } catch {
-      actionWarnings.push(`operator action events unavailable for ${action.id}; showing action without timeline.`);
-      return { actionId: action.id, events: [] as Row[] };
-    }
-  }));
-  const eventsByAction = new Map(actionEvents.map((item) => [item.actionId, item.events]));
+  const actionIds = operatorActions.map((action) => action.id);
+  const eventsByAction = new Map<string, Row[]>();
+  if (actionIds.length > 0) {
+    try {
+      const result = await client.from("pandora_operator_action_events").select("*").eq("user_id", input.userId).in("action_id", actionIds).order("created_at", { ascending: false });
+      if (result.error) {
+        actionWarnings.push("operator action events unavailable; showing actions without timeline.");
+      } else {
+        for (const row of (Array.isArray(result.data) ? result.data : [])) {
+          const list = eventsByAction.get(row.action_id) ?? [];
+          if (list.length < 3) list.push(row);
+          eventsByAction.set(row.action_id, list);
+        }
+      }
+    } catch {
+      actionWarnings.push("operator action events unavailable; showing actions without timeline.");
+    }
+  }
🤖 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 `@lib/services/pandora-dashboard-service.ts` around lines 44 - 53, The action
event fetch in pandoraDashboardService is using one query per operator action,
creating an N+1 read path. Refactor the Promise.all block around
client.from("pandora_operator_action_events") to fetch all needed events in a
single query using .in(...) on action ids, then group the results by actionId
and apply the top-3 slicing client-side before building the actionEvents array.
Keep the existing warning behavior in the actionWarnings path and preserve the
actionId/events shape returned from this section.
components/pandora/OperatorActionList.tsx (1)

6-6: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

blocked and executing share the same color.

Both map to "amber", making an in-progress execution visually indistinguishable from a blocked/problem state in the status pill — potentially confusing for an operator scanning action history for issues that need attention.

🤖 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 `@components/pandora/OperatorActionList.tsx` at line 6, The status color
mapping in OperatorActionList uses the same amber color for both blocked and
executing, which makes active work look like an error state. Update the colors
Record in OperatorActionList.tsx so executing has a distinct non-blocking color
while blocked keeps the warning/error color, and verify the status pill
rendering still clearly differentiates action states.
components/pandora/OperatorActionControls.tsx (1)

4-7: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No error handling on action transition requests.

postAction reloads the page regardless of the fetch outcome. If approve/execute/cancel/dry-run is rejected (e.g., invalid transition, ownership check failure), the operator gets no feedback — the page just reloads with the action unchanged and no indication of why.

♻️ Suggested improvement
 async function postAction(actionId: string, verb: "dry-run" | "approve" | "execute" | "cancel") {
-  await fetch(`/api/pandora/operator-actions/${actionId}/${verb}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({}) });
-  window.location.reload();
+  const response = await fetch(`/api/pandora/operator-actions/${actionId}/${verb}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({}) });
+  if (!response.ok) {
+    const body = await response.json().catch(() => ({}));
+    window.alert(body.error ?? `Failed to ${verb} action.`);
+    return;
+  }
+  window.location.reload();
 }
🤖 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 `@components/pandora/OperatorActionControls.tsx` around lines 4 - 7, The
postAction helper currently reloads the page even when the operator-action
request fails, so rejected transitions provide no feedback. Update postAction in
OperatorActionControls to check the fetch response before calling
window.location.reload, handle non-OK responses and network errors, and surface
a clear message to the user (e.g. through existing UI state/notification
handling) when approve, execute, cancel, or dry-run is rejected.
app/api/pandora/operator-actions/[id]/approve/route.ts (2)

10-11: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Catch-all maps every failure to HTTP 400, hiding not-found and server errors.

The catch funnels not-found (getOperatorAction throws), invalid-transition (assertTransition), and genuine infra failures (Supabase/network) into a single 400. That mislabels server faults as client errors, so 5xx-based alerting and client retry logic won't trigger, and the raw error.message is returned without any server-side log. Consider classifying: 404 for not-found, 409/400 for invalid state, 500 (logged) for the rest. This pattern is shared by execute/cancel/dry-run.

🤖 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 `@app/api/pandora/operator-actions/`[id]/approve/route.ts around lines 10 - 11,
The approve route’s catch-all in the approveOperatorAction flow is mapping
not-found, invalid-transition, and infrastructure failures to the same 400
response. Update the error handling around the route handler in approve/route.ts
so it distinguishes expected domain errors from unexpected failures, returning
404 for missing actions, 400/409 for invalid state transitions, and 500 for
Supabase/network or other unexpected errors; also add server-side logging for
the unexpected branch. Use the existing approveOperatorAction,
createSupabaseServerClient, and NextResponse.json path to keep the
classification close to the current handler logic, and apply the same pattern to
the matching execute/cancel/dry-run routes.

6-12: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Consider extracting the shared POST scaffolding.

The body-parse → assertNoClientUserIdOverrideresolvePandoraServerSession → try/catch envelope is duplicated near-verbatim across approve/execute/cancel/dry-run. A small wrapper (e.g. withOperatorActionSession(handler)) would centralize the auth contract and the error-mapping fix above, reducing drift risk.

🤖 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 `@app/api/pandora/operator-actions/`[id]/approve/route.ts around lines 6 - 12,
The POST handler scaffolding in approve/execute/cancel/dry-run is duplicated and
should be centralized to avoid drift. Extract the shared body parsing,
assertNoClientUserIdOverride, resolvePandoraServerSession, and
try/catch/error-mapping flow into a reusable wrapper such as
withOperatorActionSession, then have POST in approve/route.ts delegate only the
action-specific work (approveOperatorAction and id lookup) through that helper.
Keep the wrapper responsible for consistent blocker and error responses across
all operator action routes.
tests/unit/pandora-operator-action-service.test.ts (1)

18-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test title claims dry_ran coverage that the test body doesn't exercise.

The test is titled "approves proposed and dry_ran actions..." but only ever creates a proposed action and approves it directly; it never transitions the action to dry_ran before calling approveOperatorAction. If a regression breaks approving from dry_ran specifically, this test won't catch it despite implying it does.

♻️ Suggested addition
   it("approves proposed and dry_ran actions, then executes approved read-only action", async () => { const s=store(); const c=client(s); const a=await proposeOperatorAction(c,{userId:USER, actionType:"verify_pack_supersession", namespace:"real_life"}); const approved=await approveOperatorAction(c,{userId:USER, actionId:a.id}); expect(approved.status).toBe("approved"); const completed=await executeApprovedOperatorAction(c,{userId:USER, actionId:a.id}); expect(completed.status).toBe("completed"); expect(completed.result.no_mutation_performed).toBe(true); expect(JSON.stringify(completed.result)).toContain("pack_supersession"); expect(s.memory_context_packs).toHaveLength(2); });
+  it("approves a dry_ran action", async () => { const s=store(); const c=client(s); const a=await proposeOperatorAction(c,{userId:USER, actionType:"verify_pack_supersession", namespace:"real_life"}); await dryRunOperatorAction(c,{userId:USER, actionId:a.id}); const approved=await approveOperatorAction(c,{userId:USER, actionId:a.id}); expect(approved.status).toBe("approved"); });
🤖 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 `@tests/unit/pandora-operator-action-service.test.ts` at line 18, The test name
overstates coverage because `proposeOperatorAction`, `approveOperatorAction`,
and `executeApprovedOperatorAction` only exercise the proposed flow and never
the `dry_ran` state. Update this test in
`tests/unit/pandora-operator-action-service.test.ts` so it explicitly
transitions the created action through `dry_ran` before approval, or split it
into separate cases that cover proposed and dry_ran approval paths. Keep the
assertions on the completed read-only execution, but make sure the test body
matches the `dry_ran` behavior claimed by the title.
tests/unit/pandora-operator-action-ui.test.tsx (1)

12-12: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assertion is trivially satisfied by the always-rendered status-count grid, not the action list.

countsByStatus fixture includes all 8 status names with fixed values, and OperatorActionCenterCard unconditionally renders every countsByStatus key as text (<span>{status}</span>) regardless of the actions prop. So expect(html).toContain(s) for "approved"/"executing"/etc. passes purely from that grid — this test doesn't actually verify that per-action status rendering (e.g., in OperatorActionList) works correctly for the constructed actions array. A regression there would go undetected.

♻️ Suggested strengthening
-  it("renders approved/executing/completed/failed/cancelled statuses", () => { const actions=["approved","executing","completed","failed","cancelled"].map((status,i)=>({...base,id:String(i),status: status as OperatorActionStatus})); const html=renderToStaticMarkup(<OperatorActionCenterCard data={{actions,warnings:[], countsByStatus}} />); for (const s of ["approved","executing","completed","failed","cancelled"]) expect(html).toContain(s); });
+  it("renders approved/executing/completed/failed/cancelled statuses", () => { const actions=["approved","executing","completed","failed","cancelled"].map((status,i)=>({...base,id:String(i),status: status as OperatorActionStatus})); const html=renderToStaticMarkup(<OperatorActionCenterCard data={{actions,warnings:[], countsByStatus}} />); for (const s of ["approved","executing","completed","failed","cancelled"]) expect((html.match(new RegExp(s,"g")) ?? []).length).toBeGreaterThan(1); });
🤖 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 `@tests/unit/pandora-operator-action-ui.test.tsx` at line 12, The
status-rendering test is too weak because it only matches the always-rendered
counts grid in OperatorActionCenterCard, not the per-action rows. Update the
test to assert against the action list rendering path, using OperatorActionList
or the action section inside OperatorActionCenterCard, and verify each
constructed action status is actually displayed from the actions array. Keep
countsByStatus from satisfying the assertion by narrowing the query to the
action item markup or by using a fixture that isolates the list output.
🤖 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 `@app/api/pandora/operator-actions/`[id]/events/route.ts:
- Line 9: The catch-all in the events route is masking real server failures by
always returning a 404, and it also disagrees with the other operator-action
handlers. Update the `catch` block in the `events` route so only the expected
not-found case maps to the same status used by sibling routes like the
approve/execute/cancel/dry-run handlers, and let unexpected errors from
`NextResponse.json` or the Supabase/client setup surface as a 500 instead. Keep
the response shape consistent while distinguishing missing actions from genuine
infrastructure errors.

In `@app/api/pandora/operator-actions/`[id]/execute/route.ts:
- Line 10: The execute route in the operator-actions handler always returns ok:
true from the NextResponse.json call even when executeApprovedOperatorAction
produces a failed action. Update the route logic around
executeApprovedOperatorAction and the returned action.result so ok is derived
from the terminal action.status, or return a non-200 response when the action
ends in failed, while keeping the success path unchanged.

In `@components/pandora/OperatorActionControls.tsx`:
- Around line 10-15: The Cancel button in OperatorActionControls is still shown
for blocked actions, even though cancelOperatorAction() cannot transition
blocked to cancelled. Update the terminal/status gating in
OperatorActionControls so blocked is treated like a non-cancelable state, and
only render the Cancel affordance for statuses that can actually be cancelled;
keep the logic aligned with postAction and cancelOperatorAction so the UI never
exposes a dead action.

In `@docs/pandora-operator-action-center.md`:
- Around line 31-39: The action-center transition table is missing the `blocked`
lifecycle state in the terminal-state row, which creates a mismatch with the
service behavior. Update the state transition documentation in the transition
table so `blocked` is listed alongside `completed`, `failed`, and `cancelled` as
terminal, using the existing action-state terminology in the table to keep the
lifecycle description accurate.

In `@lib/services/pandora-dashboard-service.ts`:
- Around line 55-56: The status bucket list is duplicated in pandora dashboard
logic and related UI fixtures, so update the counting code in
PandoraDashboardService to use a shared source of truth instead of a local
literal. Create or reuse a single OPERATOR_ACTION_STATUSES constant (for example
in types.ts or the service module) and import it in actionStatuses,
OperatorActionCenterCard, and mock-data so all counts/empty states stay aligned
when OperatorActionStatus changes.

In `@lib/services/pandora-operator-action-service.ts`:
- Around line 99-122: Guard the status transitions in
executeApprovedOperatorAction by making each database update conditional on the
expected current status, not just userId and actionId. Update the
approved-to-executing and executing-to-completed/failed writes to include the
current status in the filter, and treat a null return from single(...) as a lost
race so the action is not advanced twice or double-logged.

---

Nitpick comments:
In `@app/api/pandora/operator-actions/`[id]/approve/route.ts:
- Around line 10-11: The approve route’s catch-all in the approveOperatorAction
flow is mapping not-found, invalid-transition, and infrastructure failures to
the same 400 response. Update the error handling around the route handler in
approve/route.ts so it distinguishes expected domain errors from unexpected
failures, returning 404 for missing actions, 400/409 for invalid state
transitions, and 500 for Supabase/network or other unexpected errors; also add
server-side logging for the unexpected branch. Use the existing
approveOperatorAction, createSupabaseServerClient, and NextResponse.json path to
keep the classification close to the current handler logic, and apply the same
pattern to the matching execute/cancel/dry-run routes.
- Around line 6-12: The POST handler scaffolding in
approve/execute/cancel/dry-run is duplicated and should be centralized to avoid
drift. Extract the shared body parsing, assertNoClientUserIdOverride,
resolvePandoraServerSession, and try/catch/error-mapping flow into a reusable
wrapper such as withOperatorActionSession, then have POST in approve/route.ts
delegate only the action-specific work (approveOperatorAction and id lookup)
through that helper. Keep the wrapper responsible for consistent blocker and
error responses across all operator action routes.

In `@components/pandora/OperatorActionControls.tsx`:
- Around line 4-7: The postAction helper currently reloads the page even when
the operator-action request fails, so rejected transitions provide no feedback.
Update postAction in OperatorActionControls to check the fetch response before
calling window.location.reload, handle non-OK responses and network errors, and
surface a clear message to the user (e.g. through existing UI state/notification
handling) when approve, execute, cancel, or dry-run is rejected.

In `@components/pandora/OperatorActionEnvelope.tsx`:
- Around line 12-14: The `OperatorActionEnvelope` status timestamps currently
use `?? "Pending"` for `approved_at`, `completed_at`, and `failed_at`, which can
mislead users for terminal states like cancelled, failed, or blocked. Update the
render logic to branch on `action.status` so `approved_at` shows a
non-applicable label such as “N/A” when approval will never happen, while still
showing “Pending” only for actions that are still in progress. Keep the change
localized to the timestamp display in `OperatorActionEnvelope` and use the
existing `action.status` field to decide the label.

In `@components/pandora/OperatorActionList.tsx`:
- Line 6: The status color mapping in OperatorActionList uses the same amber
color for both blocked and executing, which makes active work look like an error
state. Update the colors Record in OperatorActionList.tsx so executing has a
distinct non-blocking color while blocked keeps the warning/error color, and
verify the status pill rendering still clearly differentiates action states.

In `@lib/services/pandora-dashboard-service.ts`:
- Around line 44-53: The action event fetch in pandoraDashboardService is using
one query per operator action, creating an N+1 read path. Refactor the
Promise.all block around client.from("pandora_operator_action_events") to fetch
all needed events in a single query using .in(...) on action ids, then group the
results by actionId and apply the top-3 slicing client-side before building the
actionEvents array. Keep the existing warning behavior in the actionWarnings
path and preserve the actionId/events shape returned from this section.

In `@tests/unit/pandora-operator-action-service.test.ts`:
- Line 18: The test name overstates coverage because `proposeOperatorAction`,
`approveOperatorAction`, and `executeApprovedOperatorAction` only exercise the
proposed flow and never the `dry_ran` state. Update this test in
`tests/unit/pandora-operator-action-service.test.ts` so it explicitly
transitions the created action through `dry_ran` before approval, or split it
into separate cases that cover proposed and dry_ran approval paths. Keep the
assertions on the completed read-only execution, but make sure the test body
matches the `dry_ran` behavior claimed by the title.

In `@tests/unit/pandora-operator-action-ui.test.tsx`:
- Line 12: The status-rendering test is too weak because it only matches the
always-rendered counts grid in OperatorActionCenterCard, not the per-action
rows. Update the test to assert against the action list rendering path, using
OperatorActionList or the action section inside OperatorActionCenterCard, and
verify each constructed action status is actually displayed from the actions
array. Keep countsByStatus from satisfying the assertion by narrowing the query
to the action item markup or by using a fixture that isolates the list output.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 48e43ef8-b346-4435-b8b7-32f3b9423bd7

📥 Commits

Reviewing files that changed from the base of the PR and between d5482a1 and 66f33ba.

📒 Files selected for processing (20)
  • app/api/pandora/operator-actions/[id]/approve/route.ts
  • app/api/pandora/operator-actions/[id]/cancel/route.ts
  • app/api/pandora/operator-actions/[id]/dry-run/route.ts
  • app/api/pandora/operator-actions/[id]/events/route.ts
  • app/api/pandora/operator-actions/[id]/execute/route.ts
  • components/pandora/OperatorActionCenterCard.tsx
  • components/pandora/OperatorActionControls.tsx
  • components/pandora/OperatorActionEnvelope.tsx
  • components/pandora/OperatorActionList.tsx
  • components/pandora/OperatorActionTimeline.tsx
  • components/pandora/mock-data.ts
  • components/pandora/types.ts
  • docs/pandora-operator-action-center.md
  • docs/pandora-readonly-action-runner.md
  • lib/services/pandora-dashboard-service.ts
  • lib/services/pandora-operator-action-service.ts
  • supabase/migrations/20260703010000_pandora_operator_action_runner_statuses.sql
  • tests/unit/pandora-operator-action-guards.test.ts
  • tests/unit/pandora-operator-action-service.test.ts
  • tests/unit/pandora-operator-action-ui.test.tsx

export async function GET(request: NextRequest, context: { params: Promise<{ id: string }> }) {
const session = await resolvePandoraServerSession({ request }); if (!session.ok) return NextResponse.json({ ok: false, blockers: session.blockers }, { status: 401 });
try { const { id } = await context.params; const supabase = await createSupabaseServerClient(); const events = await listOperatorActionEvents(supabase as unknown as OperatorActionDbClient, { userId: session.session.userId, actionId: id }); return NextResponse.json({ ok: true, events }); }
catch (error) { return NextResponse.json({ ok: false, error: error instanceof Error ? error.message : "Action not found" }, { status: 404 }); }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

404 catch-all also masks server errors and diverges from sibling routes.

Any failure here (including a Supabase/client construction fault) returns 404, while approve/execute/cancel/dry-run map the same not-found condition to 400. Beyond hiding 5xx faults, the inconsistent not-found status across the operator-action routes will complicate client handling. Align the not-found status and let genuine infra failures surface as 500.

🤖 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 `@app/api/pandora/operator-actions/`[id]/events/route.ts at line 9, The
catch-all in the events route is masking real server failures by always
returning a 404, and it also disagrees with the other operator-action handlers.
Update the `catch` block in the `events` route so only the expected not-found
case maps to the same status used by sibling routes like the
approve/execute/cancel/dry-run handlers, and let unexpected errors from
`NextResponse.json` or the Supabase/client setup surface as a 500 instead. Keep
the response shape consistent while distinguishing missing actions from genuine
infrastructure errors.

let body: unknown; try { body = await request.json(); } catch { body = {}; }
const rejected = await assertNoClientUserIdOverride(request, body); if (rejected) return NextResponse.json({ ok: false, blockers: rejected.blockers }, { status: 400 });
const session = await resolvePandoraServerSession({ request }); if (!session.ok) return NextResponse.json({ ok: false, blockers: session.blockers }, { status: 401 });
try { const { id } = await context.params; const supabase = await createSupabaseServerClient(); const action = await executeApprovedOperatorAction(supabase as unknown as OperatorActionDbClient, { userId: session.session.userId, actionId: id }); return NextResponse.json({ ok: true, action, result: action.result }); }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Return ok: false for failed executions

executeApprovedOperatorAction can return a failed action on internal errors, but this route still responds with HTTP 200 { ok: true, ... }. Callers will treat failures as success unless they inspect action.status; derive ok from the terminal status or map failed executions to a non-200 response.

🤖 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 `@app/api/pandora/operator-actions/`[id]/execute/route.ts at line 10, The
execute route in the operator-actions handler always returns ok: true from the
NextResponse.json call even when executeApprovedOperatorAction produces a failed
action. Update the route logic around executeApprovedOperatorAction and the
returned action.result so ok is derived from the terminal action.status, or
return a non-200 response when the action ends in failed, while keeping the
success path unchanged.

Comment on lines +10 to +15
const terminal = ["completed", "failed", "cancelled"].includes(action.status);
return <div className="pd-action-controls">
{action.status === "proposed" ? <button className="button-link" type="button" onClick={() => postAction(action.id, "dry-run")}>Prepare dry-run</button> : null}
{(action.status === "proposed" || action.status === "dry_ran") ? <button className="button-link button-link--primary" type="button" onClick={() => postAction(action.id, "approve")}>Approve</button> : null}
{action.status === "approved" ? <button className="button-link button-link--primary" type="button" onClick={() => postAction(action.id, "execute")}>Execute approved read-only action</button> : null}
{!terminal && action.status !== "executing" ? <button className="button-link" type="button" onClick={() => postAction(action.id, "cancel")}>Cancel</button> : null}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -B2 -A6 'TERMINAL' lib/services/pandora-operator-action-service.ts

Repository: besfeng23/Memory

Length of output: 2683


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the frontend component and any status handling around it.
ast-grep outline components/pandora/OperatorActionControls.tsx --view expanded
printf '\n---\n'
rg -n -A3 -B3 'postAction|blocked|dry_ran|executing|cancel' components/pandora/OperatorActionControls.tsx components/pandora -g '!**/node_modules/**'
printf '\n---\n'
# Inspect service-side action status handling and any relevant transitions.
sed -n '1,180p' lib/services/pandora-operator-action-service.ts
printf '\n---\n'
rg -n -A3 -B3 'Cannot cancel terminal Pandora operator action|Invalid Pandora operator action transition|blocked' lib/services/pandora-operator-action-service.ts lib/services -g '!**/node_modules/**'

Repository: besfeng23/Memory

Length of output: 50373


Exclude blocked from the Cancel affordance. cancelOperatorAction() rejects blocked via assertTransition(..., "cancelled"), but this UI still renders Cancel because terminal omits it. That leaves a dead button for blocked actions, and the fetch path doesn’t surface the failure.

🤖 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 `@components/pandora/OperatorActionControls.tsx` around lines 10 - 15, The
Cancel button in OperatorActionControls is still shown for blocked actions, even
though cancelOperatorAction() cannot transition blocked to cancelled. Update the
terminal/status gating in OperatorActionControls so blocked is treated like a
non-cancelable state, and only render the Cancel affordance for statuses that
can actually be cancelled; keep the logic aligned with postAction and
cancelOperatorAction so the UI never exposes a dead action.

Comment on lines +31 to +39
| From | To |
| --- | --- |
| `proposed` | `dry_ran`, `approved`, `cancelled` |
| `dry_ran` | `approved`, `cancelled` |
| `approved` | `executing`, `cancelled` |
| `executing` | `completed`, `failed` |
| `completed`, `failed`, `cancelled` | terminal |

## Why dry-run comes before live actions
Invalid transitions fail safely and do not create misleading completion states.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Include blocked in the terminal-state list.

The service treats blocked as terminal, but this doc only marks completed/failed/cancelled as terminal. That mismatch will confuse readers about the lifecycle.

Suggested fix
-| `completed`, `failed`, `cancelled` | terminal |
+| `completed`, `failed`, `cancelled`, `blocked` | terminal |
📝 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.

Suggested change
| From | To |
| --- | --- |
| `proposed` | `dry_ran`, `approved`, `cancelled` |
| `dry_ran` | `approved`, `cancelled` |
| `approved` | `executing`, `cancelled` |
| `executing` | `completed`, `failed` |
| `completed`, `failed`, `cancelled` | terminal |
## Why dry-run comes before live actions
Invalid transitions fail safely and do not create misleading completion states.
| From | To |
| --- | --- |
| `proposed` | `dry_ran`, `approved`, `cancelled` |
| `dry_ran` | `approved`, `cancelled` |
| `approved` | `executing`, `cancelled` |
| `executing` | `completed`, `failed` |
| `completed`, `failed`, `cancelled`, `blocked` | terminal |
Invalid transitions fail safely and do not create misleading completion states.
🤖 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 `@docs/pandora-operator-action-center.md` around lines 31 - 39, The
action-center transition table is missing the `blocked` lifecycle state in the
terminal-state row, which creates a mismatch with the service behavior. Update
the state transition documentation in the transition table so `blocked` is
listed alongside `completed`, `failed`, and `cancelled` as terminal, using the
existing action-state terminology in the table to keep the lifecycle description
accurate.

Comment on lines +55 to +56
const actionStatuses = ["proposed", "dry_ran", "approved", "executing", "completed", "blocked", "failed", "cancelled"] as const;
const countsByStatus = Object.fromEntries(actionStatuses.map((status) => [status, operatorActions.filter((action) => action.status === status).length])) as PandoraDashboardData["operatorActions"]["countsByStatus"];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Hardcoded status list duplicated across files.

actionStatuses duplicates the same 8-status literal also hardcoded in components/pandora/OperatorActionCenterCard.tsx (emptyCounts) and components/pandora/mock-data.ts (countsByStatus fixture). If OperatorActionStatus gains/loses a value, all three call sites must be updated in lockstep or the UI silently drops/omits a status bucket. Consider exporting a single OPERATOR_ACTION_STATUSES constant from components/pandora/types.ts (or this service) and importing it everywhere.

🤖 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 `@lib/services/pandora-dashboard-service.ts` around lines 55 - 56, The status
bucket list is duplicated in pandora dashboard logic and related UI fixtures, so
update the counting code in PandoraDashboardService to use a shared source of
truth instead of a local literal. Create or reuse a single
OPERATOR_ACTION_STATUSES constant (for example in types.ts or the service
module) and import it in actionStatuses, OperatorActionCenterCard, and mock-data
so all counts/empty states stay aligned when OperatorActionStatus changes.

Comment on lines +99 to +122
export async function executeApprovedOperatorAction(client: OperatorActionDbClient, input: { userId: string; actionId: string }): Promise<OperatorActionRow> {
const action = await getOperatorAction(client, input);
assertTransition(action.status, "executing");
assertAction(action.action_type);
assertMode(action.mode);
const executingAt = new Date().toISOString();
const executing = await single(client.from("pandora_operator_actions").update({ status: "executing", updated_at: executingAt }).eq("user_id", input.userId).eq("id", input.actionId).select("*").single()) ?? { ...action, status: "executing" as const, updated_at: executingAt };
await createActionEvent(client, { userId: input.userId, actionId: input.actionId, eventType: "executing", message: "Approved read-only verification execution started.", metadata: { no_mutation_performed: true } });
try {
const built = await buildDryRunResult(client, input.userId, executing);
const completedAt = new Date().toISOString();
const result = envelope({ ...executing, status: "completed" }, built.result, built.warnings);
const completed = await single(client.from("pandora_operator_actions").update({ status: "completed", result, warnings: built.warnings, completed_at: completedAt, updated_at: completedAt }).eq("user_id", input.userId).eq("id", input.actionId).select("*").single());
await createActionEvent(client, { userId: input.userId, actionId: input.actionId, eventType: "completed", message: "Approved read-only execution completed. No core memory mutation was performed.", metadata: result });
return completed ?? { ...executing, status: "completed", result, warnings: built.warnings, completed_at: completedAt, updated_at: completedAt };
} catch (error) {
const failedAt = new Date().toISOString();
const warnings = [error instanceof Error ? error.message : "Read-only execution failed"];
const result = envelope({ ...executing, status: "failed" }, { error: warnings[0] }, warnings);
const failed = await single(client.from("pandora_operator_actions").update({ status: "failed", result, warnings, failed_at: failedAt, updated_at: failedAt }).eq("user_id", input.userId).eq("id", input.actionId).select("*").single());
await createActionEvent(client, { userId: input.userId, actionId: input.actionId, eventType: "failed", message: "Approved read-only execution failed; no completion was fabricated.", metadata: result });
return failed ?? { ...executing, status: "failed", result, warnings, failed_at: failedAt, updated_at: failedAt };
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

ast-grep outline lib/services/pandora-operator-action-service.ts --view expanded

Repository: besfeng23/Memory

Length of output: 5931


🏁 Script executed:

rg -n "function single|const single|async function single|export .*single|\.eq\\(\"status\"" lib -S

Repository: besfeng23/Memory

Length of output: 16982


🏁 Script executed:

sed -n '80,150p' lib/services/pandora-operator-action-service.ts | cat -n

Repository: besfeng23/Memory

Length of output: 5641


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path
text = Path("lib/services/pandora-operator-action-service.ts").read_text()
for needle in [
    '.eq("user_id", input.userId).eq("id", input.actionId)',
    '.eq("status", "approved")',
    '.eq("status", "executing")',
    '.eq("status", "proposed")',
    '.eq("status", "dry_ran")',
]:
    print(needle, text.count(needle))
PY

Repository: besfeng23/Memory

Length of output: 322


Guard each transition update on the expected current status.

assertTransition only checks the row that was read. The update(...) calls for approved → executing, executing → completed/failed, and cancelled still match only user_id/id, so two concurrent requests can both advance the same action and emit duplicate audit events. Add the expected status to each update and treat a null result as a lost race.

🤖 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 `@lib/services/pandora-operator-action-service.ts` around lines 99 - 122, Guard
the status transitions in executeApprovedOperatorAction by making each database
update conditional on the expected current status, not just userId and actionId.
Update the approved-to-executing and executing-to-completed/failed writes to
include the current status in the filter, and treat a null return from
single(...) as a lost race so the action is not advanced twice or double-logged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant