# Troubleshooting

Diagnose common capability, executor, and execution-store failures.

Start with the first error you can reproduce, then apply the matching fix.

## `capability() requires a non-empty name.`

`capability({ name })` must be a non-empty string. This is thrown at
construction time, before the capability is ever executed.

## `capability() requires an authorize function.`

Every capability must declare `authorize`. There is no default policy —
Kaji does not decide who may run an action, so an omitted `authorize` fails
fast instead of defaulting to allow or deny.

## `ExecutionRequest.principalId must be a non-empty string.` / `...idempotencyKey must be a non-empty string.`

Both fields are required on every call to `kaji.execute()`. Kaji never
infers a principal or generates an idempotency key on the caller's behalf —
supply both explicitly from your request.

## A retried request returns a `failed` conflict instead of running again

```ts
error: new Error('Idempotency key "..." was already used for a different request.');
```

The idempotency key was already claimed with a different input,
capability, or principal. One idempotency key identifies exactly one
intended operation; reusing it for a materially different request is a
conflict, not a retry. Generate a new idempotency key for a genuinely new
operation.

## A request I expected to execute returns the previous result instead

This is expected, not a bug: a repeated execution with the same capability,
principal, idempotency key, and input fingerprint returns the recorded
outcome instead of running `execute` again. If you intended a new
operation, use a new idempotency key.

## `execute()` settles `rejected`

Approval was required (`approval` returned `true`) and was not granted.
Check, in order:

1. Is `approve` configured on `createKaji()` at all? An absent handler
   always settles `rejected`.
2. Does your `approve` handler return `{ approved: boolean }`? Any other
   shape is treated as an invalid decision and settles `rejected`.
3. Did your `approve` handler throw or reject? A handler error settles
   `rejected`, not `unknown` — approval failures are never ambiguous about
   whether the action ran, because it hasn't yet.

## `execute()` settles `denied`

`authorize` returned `false`, or it threw before returning. Kaji does not
provide an authorization system — the decision is entirely your
application's `authorize` function. Add logging inside `authorize` itself
if you need to know why a specific request was denied.

## `execute()` settles `unknown` and I want to retry

Read `unknown` as "Kaji cannot prove whether the side effect happened," not
as an ordinary failure. Do not retry it automatically. Instead:

1. Use `result.evidence` (`executionId`, `inputFingerprint`) to look up
   whatever your application or downstream provider can tell you about
   this specific operation.
2. If your capability's `execute` function can reconcile the ambiguity
   itself — for example, checking the provider for an existing committed
   effect by idempotency key — do that before deciding to retry.
3. Only call `kaji.execute()` again with the _same_ idempotency key once
   you're ready to accept its behavior: a repeated call with the same key
   and input still returns the recorded `unknown` result rather than
   re-running `execute`. To force a genuinely new attempt, use a new
   idempotency key, and only after you've confirmed the original operation
   did not commit.

## My capability's own errors are always `unknown`, never `failed`

Kaji treats every thrown or rejected `execute` call as `unknown` by
default — it has no way to know whether your side effect committed before
the error surfaced. If your application code can prove a specific failure
never committed a side effect (for example, a payment provider that
confirms a charge was declined before any charge occurred), throw
`knownFailure(cause)` for that specific case:

```ts
import { knownFailure } from "@irogane/kaji";

execute: async (input, context) => {
  try {
    return await provider.charge(input);
  } catch (cause) {
    if (isDefinitelyDeclined(cause)) throw knownFailure(cause);
    throw cause; // stays unknown: the side effect may have committed
  }
},
```

Kaji never infers this from an error's class, message, or status code —
only throw `knownFailure()` when your own code has proven no side effect
occurred.

## Cancellation didn't stop my side effect

Cancellation is cooperative. If your capability's `execute` function
doesn't check `context.signal` (or pass it to an operation that does), Kaji
cannot force it to stop. Once `execute` has been called, an aborted signal
can no longer prevent the action — it can only ask the capability to stop,
and the resulting outcome settles `unknown` rather than `cancelled`.

## `timeoutMs` elapsed but the remote operation still ran

A timeout means Kaji stopped waiting — it is not a rollback and is not
proof the operation didn't happen. The corresponding execution settles
`unknown`. If you need to know the real outcome, reconcile with the
downstream system using the execution's idempotency key, the same as any
other `unknown` result.

## My custom `ExecutionStore` throws during `claim()` or `record()`

A `claim()` failure causes `kaji.execute()` itself to throw — Kaji cannot
establish the execution boundary at all in that case, since no execution
record exists yet. A `record()` failure after execution instead settles the
returned result as `unknown`: Kaji cannot prove the outcome was durably
recorded, so it can't tell the caller the capability's real result.

`memoryStore()` is process-local and non-durable; it is meant for local
development, tests, and examples, not production. A production deployment
needs its own durable `ExecutionStore` implementation satisfying the same
claim/record contract.