different-ai/openwork · warning · ApiError

approval_not_found

approval_not_found

Error message

Approval request not found

What it means

The POST /approvals/:id route resolves a pending approval request by id via ctx.approvals.respond and denies it by default unless body.reply === "allow". If no approval with that id exists (already responded, expired, wrong id, or routed to the wrong server), respond returns null and the route throws ApiError 404 code "approval_not_found".

Source

Thrown at apps/server/src/routes/operations.ts:67

      action: "engine.reload",
      target: workspace.baseUrl ?? "opencode",
      summary: "Reloaded workspace engine",
      timestamp: Date.now(),
    });

    return jsonResponse({ ok: true, reloadedAt: Date.now() });
  });

  addRoute(routes, "GET", "/approvals", "host", async (ctx) => {
    return jsonResponse({ items: ctx.approvals.list() });
  });

  addRoute(routes, "POST", "/approvals/:id", "host", async (ctx) => {
    const body = await readJsonBody(ctx.request);
    const reply = body.reply === "allow" ? "allow" : "deny";
    const result = ctx.approvals.respond(ctx.params.id, reply);
    if (!result) {
      throw new ApiError(404, "approval_not_found", "Approval request not found");
    }
    return jsonResponse({ ok: true, allowed: result.allowed });
  });
}

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Treat 404 as 'already handled': refresh the pending approvals list and proceed — the operation was allowed or denied by the earlier response.
  2. Guard the client against duplicate submissions (disable the button after the first POST, dedupe by approval id).
  3. Verify you are posting to the same server/session that issued the approval and that the id comes from the current pending set (e.g. via the approvals list/event stream).

Example fix

// before
await Promise.all([respond(id, "allow"), logApproval(id)]); // second POST may 404
// after
try {
  await respond(id, "allow");
} catch (e) {
  if (e.code === "approval_not_found") await refreshApprovals(); // already resolved
  else throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

const pending = await api.listApprovals();
if (!pending.some((a) => a.id === approvalId)) return; // already resolved or expired — skip POST

Type guard

function isPendingApproval(a) { return typeof a === "object" && a !== null && typeof a.id === "string" && a.status === "pending"; }

Try / catch

try {
  await api.respondToApproval(approvalId, "allow");
} catch (e) {
  if (e.status === 404 && e.code === "approval_not_found") {
    await refreshApprovals(); // idempotent: someone already answered
  } else throw e;
}

Prevention

When it happens

Trigger: Answering an approval twice (the first response consumes it); the approval timed out or the server restarted losing in-memory state; a typo'd/mismatched approval id; replying on a different host connection than the one that created the request.

Common situations: Double-click on an Approve/Deny button sends two POSTs; a long-running request expired before the user answered; the client cached an old approval list; multiple server instances with per-process approval stores.

Related errors


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/a97d651b05fa3fb3. Report an issue: GitHub.