fix(mail): stop Gmail refusing the search, and stop calling that "hittade inget" (#1521)

Pressing Leta produced mails=25, documents=0 on a real two-mailbox run.
Nothing was found because nothing was searched: every request came back
429 "Too many concurrent requests for user".

Two bugs, and the second is the one that matters.

The search fanned out with Promise.all over every message id at once, one
Gmail request per message, per connection. Gmail enforces a per-user
concurrency ceiling as well as a daily quota, and this sailed past it long
before any volume worth worrying about. It now runs through a pool of five
per connection, which is comfortably under and still finishes a page of
results in a couple of round trips.

The catch turned each refusal into an empty array, with a comment saying
one mailbox's failure must not become the company's. Right instinct, wrong
consequence: an empty array is also what an empty mailbox returns, and the
manual hunt loop stops on fetched === 0 because that is its signal for
"the mailboxes hold nothing more for what is open". So a rate-limited
search told the user their receipts do not exist, and stopped looking.

searchFailureCount() now separates "could not look" from "nothing there".
The run route reports it, and the loop treats a pass with failures as
failed rather than finished, so pressing again is the obvious next move
instead of a pointless one.

This is the failure this feature exists to catch, happening inside the
feature: silence that reads as an answer.

Restoring the unbounded fan-out fails one test; removing the failure
counter fails three.

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-08-11 14:23:58 +02:00
committed by GitHub
co-authored by Jakob Wennberg Claude Opus 5
parent 709c0c817a
commit f2d9e98af3
7 changed files with 222 additions and 6 deletions
+1
View File
@@ -863,3 +863,4 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
[2026-08-10] Staging DB reconcile (metjnjrhvujscngnpzdv): the tracker had skipped everything from 20260721101500 to 2026-08-10 (105 local-only versions) while 25 rows existed only remotely. Renamed 10 remote rows to their repo versions (same name, MCP apply-time version drift: sandbox-cleanup consolidation, shopify, tax-depreciation, JEL index), deleted 7 superseded sandbox-iteration rows with no local file, and left 8 rows from unmerged branches (white-label brands/teams, vacation columns, agent-atom product tier) untouched since their content is deliberately live for the byra rigs. Older seed_agent_atom_bodies files register version-only: each seed is a full idempotent upsert with a version guard, so only the newest seed's content needs to run.
[2026-08-11] suggest-booking derives the proposed kontering on demand rather than storing it on the inbox row or computing it in the receipt hunt: a stored proposal goes stale against a corrected amount, a re-matched transaction or a template the company taught itself since, and the nightly hunt is already at its 300 s ceiling for a proposal most rows never open. It composes the existing evaluateMappingRules -> buildTransactionEntryLines chain rather than a second one, so the shown lines cannot drift from the posted lines. It withholds the proposal entirely on a foreign-currency row that matched via the mapping_rules branch: mapping-engine.ts buildResult computes VAT from the transaction's own currency while every other line is SEK (its own NOTE tracks this), which understates ingaende moms by the exchange rate and still balances, so nothing downstream catches it. Guarding the surface was chosen over fixing buildResult in this PR because that changes posted VAT amounts across every caller; the counterparty and static-template paths already convert correctly and are not withheld.
[2026-08-11] Agent skills for the API ship as generated artifacts, not authored docs: skills/accounted-api/ is CI-checked output (apiskill:check) of scripts/api-skill/generate.ts, rendered from the same lib/api/v1 registry that serves the API and its OpenAPI spec, so the installable skill cannot drift from the server. Edit scripts/api-skill/overlays/ or the registry, never the output. The per-operation renderer is the portable tool inside skills/openapi-to-skill/ (the generic spec-to-skill generator): our own skill dogfoods it. Skills live in top-level skills/ because that is the directory `npx skills add erp-mafia/accounted` scans; the OpenAPI generator was extended to emit requestBody + path parameters (previously response-only) rather than teaching the skill generator to read Zod directly, so every spec consumer benefits, not just the skill.
[2026-08-11] Gmail search fans out with a bounded pool (5 per connection) instead of Promise.all over every message id: Gmail enforces a per-user CONCURRENCY ceiling, not just a daily quota, and answers 429 "Too many concurrent requests for user" well below this app's volume. A real two-connection run returned mails=25 documents=0 purely from 429s. The catch in searchOne turned every refusal into an empty result, which is indistinguishable from an empty mailbox, and the manual hunt loop stops on fetched===0 as its "nothing left to find" signal, so the user was told their receipts do not exist by a search that never ran. searchFailureCount() now separates "could not look" from "nothing there", the run route returns it, and the loop treats a pass with failures as failed rather than finished.