logbook/Docs

Troubleshooting

Error catalog for logbook, organized by symptom. Each entry gives the exact error text (quoted from source), what it means, and the fix. Error strings are verbatim from lib/auth.ts (resolveMcpAuth), lib/billing.ts, lib/db/ queries.ts, app/api/mcp/route.ts, and app/api/webhooks/github/route.ts.

MCP auth errors come back as an HTTP response with a JSON body { "error": "<message>", …extra }. Tool-level domain errors come back inside a successful tool result as text prefixed with Error: .

Connection and auth (MCP)

401 — no bearer token

Unauthorized. Pass a logbook API token: Authorization: Bearer lgbk_… (generate one in Settings).

Means: the request had no Authorization: Bearer <token> header (or it did not match the Bearer <token> shape). Fix: send Authorization: Bearer lgbk_…. Generate a token in logbook Settings. See Connecting agents.

401 — token not recognized

Unauthorized. This logbook API token is not recognized — generate a fresh one in Settings.

Means: a bearer token was sent, but its SHA-256 hash matches no stored token. The token is wrong, was revoked, or is from another environment. Only the hash is stored, so a mistyped or truncated token lands here. Fix: generate a fresh token in Settings and reconnect. Copy the whole lgbk_… string — it is shown exactly once at creation.

401 — token has no scope (should never happen)

This token has no project or organization scope.

Means: a stored token row has neither project_id nor org_id set. Every mint sets exactly one, so this indicates a corrupted row. Fix: revoke that token and mint a new one; report it if it recurs.

400 — org key on the bare /api/mcp

This is an org key — it has no default project. Connect to /api/mcp/<project-slug>; this org has: <slug-a>, <slug-b>.

The JSON body also carries { "projects": ["<slug-a>", "<slug-b>"] } (or (no projects yet) in the message when the org has none).

Means: you connected an org key to the bare /api/mcp endpoint. Org keys carry only who may act; where comes from the project slug in the URL, so the bare endpoint cannot resolve a project. Fix: connect to /api/mcp/<project-slug>, choosing one of the listed slugs. See Connecting agents.

404 — no project with that slug (org key)

No project with slug "<slug>" in this token's organization.

Means: an org key was used at /api/mcp/<slug>, but no project in this token's organization has that slug. This is returned for both "slug does not exist anywhere" and "slug exists but belongs to another org" — the ambiguity is deliberate, so a token cannot probe for the existence of another tenant's project. Fix: verify the slug (check Settings / the org's project list). If you believe the project exists, confirm it belongs to this token's org — a slug in a different org yields the same 404 by design.

403 — legacy project token on a mismatched slug

This token is bound to the "<its-slug>" project and cannot act on "<slug>". Use /api/mcp/<its-slug> or the bare /api/mcp endpoint.

(When the token's project has been deleted, the message reads This token is bound to a deleted project and cannot act on "<slug>". Use /api/mcp/<its-project-slug> or the bare /api/mcp endpoint.)

Means: a legacy project token (fixed to one project) was used at /api/mcp/<slug> where <slug> is not its own project's slug. Fix: use the bare /api/mcp endpoint, or /api/mcp/<its-own-slug>. To act on a different project, mint an org key and target that project's slug.

Billing cap (upgrade_required)

This is the free-tier cap, not the lapsed-payment lock — for that see 402 below. Only create_item and create_items are capped. Reads, updates, notes, tasks, documents, and archiving are never capped — a capped org keeps full access to data it already has. The gate returns a structured tool-result JSON (not an HTTP error):

{
  "error": "upgrade_required",
  "message": "This org is on the free plan (50 logs; 50 used). Upgrade to keep logging: <app-url>/settings",
  "items_used": 50,
  "max_items": 50,
  "upgrade_url": "<app-url>/settings"
}

What counts toward the 50: FREE_MAX_ITEMS = 50 — the lifetime count of items created in the org, counted across all its projects (countOrgItems). Deleting/archiving does not reduce it; it is a lifetime creation count. For create_items the check is whole-batch and all-or-nothing: if items_used + batch_size > max_items, the entire batch is refused and nothing is created.

Plan semantics (effectivePlan / limitsFor):

  • free — maxItems = 50, maxProjects = 1.
  • pro — unlimited items and projects (Infinity). $20/month for the first 5 seats, $5/seat after; a seat is a Clerk org member.
  • founder — unlimited, and never lapses (no subscription; status always treated as active).
  • A lapsed payment no longer demotes. It used to be treated as free for limit purposes; it now triggers the lock below instead. During the grace window a lapsed pro org keeps full pro limits.

Billing fails open when unconfigured: with no STRIPE_SECRET_KEY, billingEnabled() is false and every entitlement check passes — no org is ever capped with no way to upgrade. The gate is also skipped when the project has no org (the migration/backfill window, orgId === null).

Fix: upgrade at the upgrade_url (Settings). Note the also-present project cap error (thrown by assertCanCreateProject, surfaced in the web UI): The <plan> plan is limited to <n> project(s). Upgrade to add more: <url>.

402 — payment required (locked)

{
  "error": "payment_required",
  "message": "This organization's payment failed and the 14-day grace period has ended. Logbook is locked until the subscription is current: <app-url>/settings",
  "status": "past_due",
  "upgrade_url": "<app-url>/settings"
}

Means: the org's subscription lapsed and its 14-day grace window has expired. Unlike the cap, this is an HTTP 402 and it applies to every tool — reads included. The check sits in handle() before the MCP handler is built, so no tool is exempt. On the web the same state redirects every data page to /locked.

This never happens to a free or founder org, or on a deployment with no STRIPE_SECRET_KEY.

Fix: settle the subscription in Settings or at the /locked paywall. The invoice.paid webhook clears the grace window and restores access immediately — there is nothing to re-provision. If a payment has succeeded and the org is still locked, the Stripe webhook is the thing to check: confirm STRIPE_WEBHOOK_SECRET is set and that invoice.paid is among the subscribed events, since without it the lock is never lifted.

create_item / create_items validation

These throw before anything is created; the message comes back as tool-result text prefixed with Error: . In create_items each is additionally prefixed item <i>: and suffixed — nothing was created (all-or-nothing).

Log without a size

`size` is required for logs (S/M/L/XL). If this decomposes into shippable pieces, pass kind:'epic' and omit size.

Means: kind was omitted or log, but no size was given. Every MCP-filed log must be sized so shape queries work. Fix: pass size (S/M/L/XL), or file it as kind:'epic' with no size.

Epic with a size

Epics carry no size — omit `size` for kind:'epic' (containers aren't sized).

Fix: remove size from the epic entry; size the child logs instead.

Epic with a parent

Epics cannot have a parent — `epic`/`epic_index` are for child logs only (no nesting in v1).

Means: an entry with kind:'epic' also passed epic or epic_index. No nesting in v1. Fix: drop epic/epic_index from the epic entry.

epic and epic_index both set

Set `epic` OR `epic_index`, not both.

Fix: choose one — epic (an existing epic ref) or epic_index (an epic entry's 0-based index in this batch).

epic_index errors (create_items)

item <i>: epic_index <idx> is out of range (0..<N-1>) — nothing was created.
item <i>: epic_index cannot point at itself — nothing was created.
item <i>: epic_index <idx> points at a non-epic entry — nothing was created.

Fix: point epic_index at a valid index whose entry is kind:'epic' and is not the child itself.

epic ref resolves to a log (create_items)

item <i>: LOG-<n> is a log, not an epic — nothing was created.

Fix: children attach only to epics. Point epic at an epic ref, or file the parent as an epic first.

Unknown label

create_item (via validateLabels):

Unknown label(s): <list>. Available: <list>. Create labels deliberately (Settings, or the create_label tool).

create_items (batch prevalidation):

Unknown label(s): <list> — nothing was created. Use list_labels / create_label first, then retry.

Means: a label was used that is not in the managed taxonomy. Labels are normalized (lowercased/slugified) before comparison. Fix: call list_labels, then create_label for any genuinely new label, then retry. Reuse existing labels over near-duplicates.

Unknown assignee email

No logbook user with email <email>. Known users: <comma-separated list>

(or Known users: (none).)

Means: assignee_emails / assignee_email contained an email not on this project's team roster (org members + anyone attributed in its items). Emails are lowercased and trimmed before matching. Fix: call list_team for valid emails; the user must be an org member or already appear in the project's items.

update_item / update_items rejections

Same Error: text convention. In update_items a failing entry does not stop the others — it appears as { ref, ok: false, error } in results.

kind is immutable

There is no kind parameter on update_item — kind cannot be changed after creation. Attempting to "convert" a log to an epic (or vice versa) is not possible; file a new item of the correct kind and re-parent/close the old one.

epic attach on an epic

LOG-<n> is an epic — epics can't be attached to a parent.

Means: you passed epic on an item that is itself an epic. Epics are containers, not children. Fix: only attach logs to epics. To reorganize, re-parent the child logs, not the epic.

epic ref is a log, not an epic

LOG-<n> is a log, not an epic — children only attach to epics.

Fix: point epic at an actual epic ref.

epic does not exist

That epic does not exist in this project.

Fix: verify the epic ref belongs to this project.

no such item

update_item returns No item LOG-<n>; update_items records { ref, ok: false, error: "no such item" }. Fix: confirm the number and that the item is in this project.

Webhook: items not crossing off

When a PR merges, logbook finds every item linking that PR and crosses it off. If that is not happening, check these in order (all from app/api/webhooks/github/route.ts):

Repo not mapped to a project

{ "ignored": "repo not mapped to a project", "repo": "<owner/name>" }

Means: no project has this repo registered (projectIdForRepo returned nothing), so the event is dropped before any signature check. Fix: run link_repo with owner/name in the target project (or add it in project setup). Repo matching is on repository.full_name.

Bad signature (secret mismatch)

{ "error": "Bad signature" }   // HTTP 401

Means: the HMAC-SHA256 of the raw body under this project's webhook secret did not match x-hub-signature-256. Each project has its own secret, resolved from the repo before verification — a secret copied from another project fails here. Fix: set the GitHub webhook secret to this project's webhook secret (shown in its setup prompt), content type application/json.

Project has no webhook secret

{ "error": "project has no webhook secret" }   // HTTP 503

Means: the repo mapped to a project, but that project has no webhookSecret configured. Fix: complete the project's webhook setup so a secret exists.

Event is not a pull_request / not a merge

  • Ping events: { "ok": true } (setup handshake — expected).
  • Other events: { "ignored": "<event>" } — only pull_request events are processed.
  • A pull_request event that is not a merge: { "recorded": "<action>", "state": "<open|draft|closed|merged>" } — the PR registry is updated, but nothing crosses off. Only action === "closed" with pull_request.merged === true crosses off linked items.

Fix: ensure the webhook is subscribed to Pull requests events, and note that only an actual merge (not close-without-merge, not open/draft) crosses items off.

PR linked but item not found

Cross-off matches items whose links jsonb contains "<owner>/<repo>/pull/<number>" (any stored variant — full URL, with or without scheme). A successful merge returns { "matched": <n>, "crossed_off": ["LOG-…", …] }.

Means: if matched is 0, no item links that PR by URL/number. Fix: attach the PR to the item as a pr link (via create_item/ update_item links/add_links), using a URL that contains <owner>/<repo>/pull/<number>.

Notes on merge behavior: an already-done item is not re-crossed but still gets a merge note. A written resolution is never overwritten — the auto-resolution stub is only filled when the item's resolution is empty. Changed-file enrichment (the file summary and item_files stamping) is best-effort: any failure (no GITHUB_TOKEN, GitHub error) leaves the cross-off behaving exactly as before enrichment existed and the webhook still returns 200.

Sign-in / account edge cases (human web app)

Humans sign in through Clerk; agents never do (agents use lgbk_… tokens).

Wrong email = a fresh, empty account

History adoption is by exact email. getCurrentUser resolves the Clerk user to the local users mirror and, on first sign-in, lazily provisions a mirror row from the Clerk email/name. If you sign in with a different email address than the one your history is under, you get a fresh, empty account — nothing is wrong with the data; it is simply keyed to the other email. Fix: sign in with the exact email your items and attribution live under. An item's attribution and assignee_email resolution both key on the exact, lowercased email.

Redirects after sign-in

requireProjectContext redirects: no user → /sign-in; signed in but no active org (e.g. you left your org) → /select-org; org with no project yet → /onboarding. These are expected routing, not errors.

Checking server / deployment status

A healthy deployment behaves like this:

  • GET / (unauthenticated) redirects to /sign-in — the app is up.
  • POST /api/mcp with no/invalid token returns HTTP 401 JSON ({ "error": "Unauthorized. Pass a logbook API token: …" }) — the MCP endpoint is up and reachable. A 401 here is the expected signal that the server is healthy but your request is unauthenticated, not that the server is down.

If instead you get connection refused, a 5xx, or an HTML error page, the problem is deployment/infrastructure, not auth — check the host and the Neon database (schema migrations must be applied before the matching code ships, or endpoints 500).