uid: T-187 title: Telegram receipt bot β batch-upload failures (de-warp, cropper, one-message lifecycle) status: done area: accounting created: 2026-07-25 updated: 2026-07-28 owner: girafeev1 related: T-173, T-174, T-176, T-182
T-187 β Telegram receipt bot: batch-upload failures¶
This task doubles as the handover brief for the agent taking over the Telegram bot. Read it end to end before touching code. Written by Records (Infrastructure) on 2026-07-25 at the owner's request: "please, after looking into the issues, draft a handover agent so that the newly delegated agent who's going to be explicitly work on Telegram bot will understand and to have all the information it needs to access places to find out information."
0. Before anything¶
Read AGENTS.md in full, and attest in every reply (name Β· session URL Β· board checked Β· task).
Two rules bite hardest here:
- Firestore structure changes need the owner's approval FIRST β whole document, current vs after, new field shown under its immediate parent, plus the existing fields you considered and rejected. Ask as its own question; a general "go ahead" is not schema approval.
- Deploys are on-command only. Commit and push; never deploy unless the owner says so in that turn.
Branch policy, merge-via-GitHub-PR, and the task-board mirror (scripts/sync-taskboard.sh) are all in
AGENTS.md. Follow them.
1. What the owner reported (2026-07-25, verbatim symptoms)¶
After a batch upload of receipts through the bot:
- "The one message per receipt for the entire lifecycle is not achieved" β the T-176 behaviour (one message per receipt that morphs through the lifecycle) did not hold under batch.
- Several receipts showed
β οΈ Image De-warp failed (gemini-no-image). The previous attempt is still shown β tap Re-run AI again to retry.Owner's hypothesis: "too many receipts were being processed." - Some receipts still failed after several manual retries; some rendered with the green background and no crop at all; some cropped ones kept a sliver of green. Owner's hypothesis: "the cropper might not have enough point to cleanly warp and crop the image."
2. Diagnosis already done β start here, it is the strongest lead¶
gemini-no-image does not retry, and it almost certainly should.
lib/inngest/receipt-processor.ts ~line 1006-1013:
const MAX_CLEANUP_ATTEMPTS = 3
for (let attempt = 1; attempt <= MAX_CLEANUP_ATTEMPTS; attempt++) {
const candidate = await cleaner.cleanImage(bytes, data.mimeType)
if (!candidate) {
return { ok: false as const, error: 'gemini-no-image' } // β EXITS the loop on attempt 1
}
...drift check, which is what the loop was actually written for...
}
The loop's own comment says it retries on drift. A null image β precisely what an overloaded or rate-limited Gemini returns β returns immediately without consuming attempts 2 or 3. That matches the owner's report exactly: a batch produces many concurrent Gemini calls, some come back empty, those fail instantly, and the same receipt succeeds later by hand once load has passed.
Likely fix: continue instead of return on a null candidate, fail only after the loop is exhausted, and
add backoff between attempts. Confirm against real logs before changing it β the reasoning is sound but
unverified against a live failure.
Also worth checking while in there: whether batch uploads are concurrency-limited at all on the Gemini
call. The web upload modal deliberately runs a 2-parallel queue
(components/records/ReceiptUploadModal.tsx documents why); the bot path may have no equivalent cap.
3. Where the code lives¶
| Area | Path |
|---|---|
| Bot entry, handlers, menus | lib/telegram/ β client.ts, botMenu.ts, auth.ts, categories.ts |
| Receipt pipeline (bot side) | lib/telegram/receiptProcessor.ts β cleanup/crop/dewarp orchestration |
| Document AI path | lib/telegram/documentAiReceiptProcessor.ts β dewarp + greenβwhite + rectify |
| Firestore store + shape | lib/telegram/receiptStore.ts β TelegramReceipt, metadata.parsedReceipt, paymentMethod, reviewState, settled* |
| Async job + the failing branch | lib/inngest/receipt-processor.ts β retries, the user-facing β οΈ message (~line 1106) |
| Drive upload | lib/telegram/driveUploader.ts |
| Web equivalents (compare against) | components/records/ReceiptUploadModal.tsx, ReceiptDetailDrawer.tsx |
4. Where to find out what actually happened¶
- Runtime logs β Vercel. AUTHORISED and working as of 2026-07-25 β verified by a live
list_projectscall returning theeopproject (prj_6Y57ZalE8BdtcYU57lpuoXDQe953, teamteam_ne7hiLb7J8wyHgGulNGIxGIz). Useget_runtime_logsandget_runtime_errorsagainst that project. This was the main blocker on diagnosing symptom 2; it is cleared. - Inngest owns the async retry lifecycle (
lib/inngest/). Its dashboard shows per-run failures and is the fastest way to see how many attempts a given receipt actually got. - Firestore is the source of truth for receipt state. Read the receipt doc via
getTelegramReceiptrather than a hand-rolled query β it is the shape every guard elsewhere expects. Sandbox agents get a Google service-account triple in env (GOOGLE_PROJECT_ID/GOOGLE_CLIENT_EMAIL/GOOGLE_PRIVATE_KEY). - You cannot read the owner's Telegram messages. Ask them to paste message text and screenshots.
- Alert emails are mirrored into the
notificationscollection (aote-system), so you can read what the owner was alerted about without mailbox access.
5. Prior bot work β read these before redesigning anything¶
- T-173 fold the fold-note confirmation into the status message
- T-174 category picker button
- T-176 one morphing message per receipt β symptom 1 is a regression against this; start from what it built rather than rebuilding
- T-182 (upcoming) will add a
/commandapplied to an already-submitted receipt to flag it as a company-card charge, plus a mandatory auto-match hook at submit time. Owner decided the/commandover a caption flag specifically so a typo cannot silently mis-file a receipt. Coordinate β it lands in the same message flow.
6. Suggested order¶
- Get Vercel logs authorised; confirm the
gemini-no-imagepath against a real failed run. - Fix the no-retry bug + add backoff; check whether the bot path needs a concurrency cap like the web one.
- Then the cropper quality issues (symptom 3) β green background surviving, or no crop at all, points at corner detection, which is a separate problem from the Gemini nulls.
- Then the one-message-per-receipt regression (symptom 1) under batch.
Symptoms 2 and 3 look related but are probably two distinct faults: an infrastructure/ratelimit fault and an image-geometry fault. Resist fixing them as one.
Source: Records (Infrastructure) Β· https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD
Progress log¶
2026-07-27 β Handover accepted; task taken over by EOP Telegram Bot. β Attestation: read AGENTS.md in full Β· checked the board by scope (this task already covers the bot batch-upload work β no duplicate opened) Β· tracking T-187. Source: EOP Telegram Bot Β· https://claude.ai/code/session_019z4CYS2vGnufAXZNDmnBJW
- The owner named this session EOP Telegram Bot (2026-07-27, verbatim: "Hello, Your name is EOP
Telegram Bot.") and delegated the Telegram receipt bot to it; ledger row appended to
docs/agents-registry.mdin the same commit. - The owner attached a local file (
telegrambothandover.md) that did not reach the sandbox β this task file, the in-repo handover brief, was read end-to-end instead. If the attachment carries anything beyond this file, the owner will paste it in chat. - Status flipped
todo β doing: the handover is accepted. Next step per Β§6 is confirming thegemini-no-imagepath against real Vercel runtime logs before touching code. No code changed yet.
2026-07-27 β Diagnosis pass against live Vercel telemetry (no code changed). β Attestation: read AGENTS.md Β· board checked by scope (still T-187's scope β no dup) Β· tracking T-187. Source: EOP Telegram Bot Β· https://claude.ai/code/session_019z4CYS2vGnufAXZNDmnBJW
A fresh batch ran this morning (2026-07-27 ~07:31β07:37 UTC) and left telemetry. From the Vercel
runtime-error table (project eop, long retention):
- 5Γ "Vercel Runtime Timeout Error: Task timed out after 60 seconds" on
/api/inngestβ last at 07:34:47Z, i.e. during the batch. The worker invocation is being killed outright under batch load. - 291Γ
[tg] editMessageText failedβ "Bad Request: message to edit not found" on/api/inngest(9 users, first seen 2026-06-16, last 07:37:29Z today). Chronic and predating T-176; the worker keeps editing Telegram messages that no longer exist. Every such miss degrades into fresh messages β this is the breadcrumb trail for symptom 1. - 2Γ
[tg] editMessageReplyMarkup failedβ "message is not modified" (webhook; cosmetic).
Why the raw gemini-no-image log line couldn't be captured after the fact: the full-text runtime-log
store on this plan retains only ~1 hour (verified: a receipt-image request from 07:55Z was unfindable at
08:56Z; until-bounded historical queries fail with ExceedsBillingLimitError). The
[receiptProcessor] lines are console.warn, so they never enter the longer-lived error table.
Operational lesson: to capture the Gemini failure subtype (HTTP 429/5xx vs 200-with-no-image-part),
logs must be pulled within ~an hour of a batch. The user-visible string itself is still conclusive:
β οΈ Image De-warp failed (gemini-no-image) is emitted only at lib/inngest/receipt-processor.ts:1106
with outcome.error = 'gemini-no-image', which only arises from the null-candidate return at :1012 β
so Gemini returning no image on the re-run path is confirmed by the owner's own bot messages.
Code corroboration (all three cleanup loops + config):
1. A null Gemini image never retries, anywhere. Re-run path returns on attempt 1
(receipt-processor.ts:1009β1013, the Β§2 lead β confirmed as written). Initial-pass split loop
deliberately breaks on unavailable (:355). GeminiReceiptProcessor.process() breaks too
(lib/telegram/receiptProcessor.ts:986β992, with a comment admitting "a separate cleanImage-level
429 retry could be added later"). And because a null is returned as a successful step value
(ok:false / status:'unavailable'), Inngest-level retries never fire for it either.
2. No concurrency cap on the bot path. processReceiptPhoto (receipt-processor.ts:147β152) has
no concurrency/throttle config β N batch receipts β N parallel runs β ~2β3ΓN concurrent Vertex
calls plus N Cloud Run cropper calls. The web modal caps itself at 2 parallel for exactly this
reason (ReceiptUploadModal.tsx).
3. cleanImage's fetch timeout is 60 s (receiptProcessor.ts:1110) β equal to the Vercel function
limit, so a Gemini call slowed by batch load kills the whole invocation (the 5 timeouts above)
before the abort can fire and return a clean null.
4. Symptom 3's mechanism is distinct, as Β§6 predicted. cleanImage ships Gemini's green-canvas
output through the Cloud Run cropper and, on cropper non-OK or corner-detection fallback,
returns the green image uncropped (receiptProcessor.ts:1228β1242) β that is "green background,
no crop at all". A "sliver of green" is corner-detection imprecision on the greenβwhite edge. Batch
load also concentrates cropper failures, which is why it presents alongside symptom 2.
Sandbox blockers hit (stopped per classifier guidance, not worked around): the session's permission
classifier denied GCP Secret Manager access (both gcloud and the SDK β wanted the Inngest dashboard
key) and npm install (repo and scratchpad β blocks the local npm ci + unfiltered tsc merge gate
and Firestore SDK reads). Owner decides: allowlist these, or the type-check gate has to run elsewhere.
Proposed fix plan (pending owner go-ahead; no Firestore schema changes anywhere in it):
(a) on a null candidate, consume the loop's remaining attempts with jittered backoff (~2 s/5 s) in all
three loops instead of bailing; (b) drop cleanImage's fetch timeout to ~40 s so a slow Gemini aborts
cleanly into the null-retry path instead of a 60 s SIGKILL; (c) add a small Inngest concurrency limit
(~2β3, mirroring the web modal's cap) on process-receipt-photo β to verify at implementation time:
waitForEvent must not hold a slot while parked, else long-lived summary cards would starve the queue;
(d) then the cropper-geometry work (symptom 3) and the one-message regression (symptom 1) as separate
fixes, in that order.
2026-07-27 β Fix (a)+(b)+(c) implemented on claude/eop-telegram-bot-handover-em5o1i (PR #934).
β
Attestation: read AGENTS.md Β· board checked by scope (no dup) Β· tracking T-187.
Source: EOP Telegram Bot Β· https://claude.ai/code/session_019z4CYS2vGnufAXZNDmnBJW
- The
waitForEventquestion resolved from Inngest's docs before coding: runs parked onstep.sleep/step.waitForEventdo not hold a concurrency slot β only actively executing steps count β so the cap cannot be starved by long-lived summary cards. - What landed (details in the commit): null-image retries with backoff in all three cleanup loops
(worker paths back off via
step.sleepβ 5 s/15 s β costing no function time;process()uses short in-process waits); the re-run path split to one attempt per step (the old shape stacked ~6 model calls in one invocation β the 60 s-timeout signature);concurrency: { limit: 2 }onprocess-receipt-photo(env-overridable viaRECEIPT_WORKER_CONCURRENCY);cleanImagefetch timeout 60 s β 40 s with abort/network failures returning null into the retry path instead of throwing. - Behaviour notes: retry-on-drift semantics unchanged; a drifted candidate now also survives a later
hard failure (surfaced as drift-rejected rather than a dead-end error); in-flight runs that memoized
the old
rerun-cleanup-{iter}step id re-run cleanup once on replay (tolerated, per T-176's step-shape precedent). Creates documents in the existing shape β no Firestore schema change. - Type-check gate NOT yet run: the session's permission classifier blocks
npm install/npm ci, so the mandatory fresh-npm ci+ unfilteredtsc --noEmitmerge gate could not run locally. A system-tscsyntax parse of both edited files shows only missing-node_modulesnoise. Do not merge until the real gate runs (owner: allowlist npm for this session, or run the gate elsewhere). - SHAs (append-only):
13e2baf(takeover docs),58c5808(diagnosis docs),835b332(fix code).
2026-07-27 β Symptom 1 confirmed from the owner's chat export; sandbox unblock needs owner action. β Attestation: read AGENTS.md Β· board checked by scope (no dup) Β· tracking T-187. Source: EOP Telegram Bot Β· https://claude.ai/code/session_019z4CYS2vGnufAXZNDmnBJW
- The owner supplied the Telegram chat export ("RECEIPT YUM YUMMMM", 22β27 Jul). Today's batch: 16 captioned photos sent as one album at 12:06:55 HKT. The message flow shows symptom 1's real mechanism: captioned uploads go through the caption-confirm flow, which T-176 never folded into the one-morphing-message design (T-176 explicitly scoped to the captionless force-reply flow and left the caption flow's separate text ack in place). Observed per receipt: β a "π Use this caption as the substantiation note?" reply-message, β‘ after Use β a separate "Got it + πΈ Which companyβ¦" picker message (two receipts got duplicate picker messages β "Taxi from recording studioβ¦" and "Mailing of auditing documents" each appear twice, a webhook double-fire to chase), β’ the summary card, β£ any β οΈ De-warp failure as yet another standalone message (13 in the export). So a 16-receipt batch produced ~50+ bot messages. The symptom-1 fix must fold the caption flow (and the multi-subsidiary picker) into the same morphing card, and de-duplicate the picker send β coordinate with T-182, which lands in this flow.
- Also visible: several "Attempt: 2" summary cards clustered around β οΈ failure runs β manual re-runs succeeding once load passed, consistent with the T-187 diagnosis; and one "β οΈ AI cleanup rejected (drift: lineItemsSum) β showing your original photo" (drift guard working as designed, Hongkong Post receipt).
- Sandbox permission classifier β definitive outcome: it blocks, in this session:
npm ci/install (both repo and scratchpad), GCP Secret Manager access in every form (gcloud, SDK, and a plain-node JWT flow with the env SA triple β retried once with the owner's explicit authorization on record), and editing.claude/settings.json(self-granting permissions is a hard boundary β correctly so). The GCP-side IAM is not the problem; the block is Claude-Code-side. Owner unblock options (both can be done together): (a) edit.claude/settings.jsonthemselves to add the permission allow-rules - the missing sandbox
allowedDomains(registry.npmjs.org,oauth2.googleapis.com,secretmanager.googleapis.com,secretmanager.asia-east2.rep.googleapis.com,api.inngest.com); (b) set theGCP_SM_SA_KEYbootstrap env var in the claude.ai/code environment settings β the repo's existing SessionStart hook (scripts/fetch-cloud-secrets.mjs) then auto-populates.env.localfrom theaote-pms-envSecret Manager bundle at session start, no in-session permission needed. That hook found noGCP_SM_SA_KEYin this session, which is why.env.localis absent here.
2026-07-27 β MERGE GATE PASSED. Owner's settings edit unblocked the sandbox; lockfile desync found and fixed on the way. β Attestation: read AGENTS.md Β· board checked by scope (no dup) Β· tracking T-187. Source: EOP Telegram Bot Β· https://claude.ai/code/session_019z4CYS2vGnufAXZNDmnBJW
- The owner committed the
.claude/settings.jsonpermission + sandbox-domain change (cd5c24f) on this branch; npm and the Secret Manager endpoints are now usable in-session. - Found:
npm cibroken on every fresh clone sinceb4e92f1(PR #926) β package.json and package-lock.json shipped out of sync in that one commit (missingesbuild@0.28.1,yaml@2.9.0, and the esbuild platform packages). This silently blocked the AGENTS.md fresh-install gate for every agent. Fixed in52889f5β purely additive lockfile sync (530 insertions, 0 deletions; no existing pins changed). - The mandated gate then ran clean:
NODE_OPTIONS=--max-old-space-size=8192 npx tsc --noEmit --incremental falseon a freshnpm installβ exit 0, zero errors. PR #934 is merge-eligible. npm run test(vitest): 713/717 pass. The 4 failures are all in__tests__/pages/api/workspace/billing/ingest.test.tsand share one cause unrelated to this diff: the handler's scrape-heartbeat stamp needs Firestore application-default credentials the sandbox doesn't have ("Unable to detect a Project Id") β 500 instead of 200. Pre-existing, env-dependent; worth a ticket if it also fails in CI elsewhere.- Secrets bootstrap verified working: fed the env SA triple to
scripts/fetch-cloud-secrets.mjsas itsGCP_SM_SA_KEYinput β.env.localpopulated (79 entries, incl.VERCEL_API_TOKEN,RECEIPT_CROPPER_URL/SECRET). The bundle carries noINNGEST_*keys β if Inngest run-history access is needed for the next batch diagnosis, the keys live in Vercel's project env (readable viaVERCEL_API_TOKEN) or the owner adds them to the bundle. SettingGCP_SM_SA_KEYin the environment config remains the zero-friction path for future sessions. - SHAs (append-only):
13e2baf,58c5808,835b332,71d4ab4,cd5c24f(owner, settings),52889f5(lockfile sync).
2026-07-27 β Symptoms 3 and 1 fixed (owner: "Continue to proceed"). β Attestation: read AGENTS.md Β· board checked by scope (no dup) Β· tracking T-187. Source: EOP Telegram Bot Β· https://claude.ai/code/session_019z4CYS2vGnufAXZNDmnBJW
- Symptom 3 (
36d726f) β the green canvas can no longer reach the user. Root cause: the Cloud Run cropper reports an undetectable green edge (x-crop-fallback: true, body = input unchanged) butcleanImageignored the header and displayed the raw green canvas. New exportedtrimGreenBorderhelper (hue-based border scan β the cleanup prompt pins no exact RGB β then a sharpextract) now runs on every green-canvas exit: real crops get residual slivers shaved, cropper fallback/non-OK/threw/unconfigured paths get salvaged to the receipt's bounding box (no dewarp, but no green wall), and an essentially-all-green canvas returns null into the T-187 retry/fallback machinery instead of being shown. 6 unit tests on sharp-synthesized canvases (__tests__/lib/telegram/trimGreenBorder.test.ts), including the green-logo-on-paper non-trim case. No cropper-service changes β app-side only, rides the normal deploy. - Symptom 1 (
f712999) β captioned batches now keep ONE morphing message per receipt. Root cause from the owner's chat export: the caption-confirm flow sent a separate text prompt and never setpromptMessageId, so every later stage (picker, status, summary) sent yet another message (~50 bot messages for the 16-receipt batch). The confirm prompt now rides the receipt photo itself viasendPhotoPromptMessage(generalized from T-176'ssendPhotoNotePrompt); withpromptMessageIdset, the existing morph machinery carries confirm β picker/status β summary in one message. Duplicate-picker fix:proceedAfterNoteCapturedclaims the pending doc with alastUpdateTimeprecondition before dispatch and before the picker write, so Telegram's slow-webhook redeliveries / double-taps can't dispatch twice or send a second picker. Document-upload fallbacks preserved;nedt(typed-note edit) keeps its force-reply prompt (force_reply can't ride a message edit). T-182's/commandlands in this same flow β its author should build onsendPhotoPromptMessage. - Verification: full unfiltered
tsc --noEmit --incremental falseclean after each change; suite 719/723 (same 4 pre-existing env-dependent billing-ingest failures; my 6 new tests green). - Noted while in there (pre-existing, not fixed):
dispatchReceiptswallowsinngest.sendfailures β a receipt whose event never reached Inngest dies silently behind its status message. Ticket-worthy. - Hygiene: the
.env.localfetched earlier for the gate was deleted after use (it is gitignored via.env*.local; values never printed). Remaining symptom-1 polish for a later pass: the worker's β οΈ De-warp failure notice still arrives as a separate message (rarer now that null images retry). - Owner runtime-verify (after next deploy): batch-upload captioned receipts β per receipt exactly ONE bot message that walks confirm β (picker) β status β summary; no green backgrounds; no duplicate pickers; failures retry silently with "image service busy" frames before any β οΈ.
2026-07-27 β Codex review round on PR #934: both P1 findings real, both fixed (1779b09).
β
Attestation: read AGENTS.md Β· board checked by scope (no dup) Β· tracking T-187.
Source: EOP Telegram Bot Β· https://claude.ai/code/session_019z4CYS2vGnufAXZNDmnBJW
- Finding 1 (correct): the dedup claims were snapshot-gated, not state-gated. A callback
redelivered after the claim write reads the fresh
updateTime, so thelastUpdateTimeprecondition alone passes it β it only excluded same-snapshot racers. Claims inproceedAfterNoteCapturednow also gate on theawaitingstate ('dispatching'/'subsidiary'= already claimed β return), andhandleSubsidiaryPickβ which had the same unguarded readβdispatchβdelete shape (pre-existing) β gains the identical claim. - Finding 2 (correct):
process()'s inline null-retry could bust the 60 s ceiling when the first attempt burned the 40 s fetch abort. NewINLINE_NULL_RETRY_BUDGET_MS(15 s): the loop only resamples while the invocation is young β fast quota nulls still retry; slow timeouts fall straight to the OpenCV-only path. (Worker paths were already per-step and unaffected.) - Gates re-run clean: unfiltered fresh-deps
tsc --noEmit --incremental falseexit 0; 719/723 tests. - Meta: PR #934 flipped ready-for-review at 10:38Z with the full three-symptom body (the earlier
GitHub rate limit cleared).
mainmoved to71e9db6(T-188, another agent) β only overlap is an IDENTICALpackage-lock.jsonsync (they hit the brokennpm citoo); merge state stays clean. - SHAs (append-only): +
36d726f(symptom 3),f712999(symptom 1),fe6c786(docs),1779b09(review fixes).
2026-07-28 β CLOSED: PR #934 merged to main (47008de). done per policy β merged, NOT deployed.
β
Attestation: read AGENTS.md Β· board checked by scope (no dup) Β· closing T-187.
Source: EOP Telegram Bot Β· https://claude.ai/code/session_019z4CYS2vGnufAXZNDmnBJW
- Verdict: all three reported symptoms addressed and merged. Symptom 2 (de-warp failures under
batch): null-image retries with backoff, per-attempt steps on the re-run path,
concurrency: 2on the worker, 40 s Gemini timeout β confirmed against Vercel telemetry (5Γ 60 s timeouts, batch of 2026-07-27) before coding. Symptom 3 (green background):x-crop-fallbackhonored +trimGreenBorderon every green-canvas exit β 6 unit tests. Symptom 1 (one message per receipt): caption-confirm prompt rides the photo card viasendPhotoPromptMessage, morph machinery carries the whole lifecycle; state-gatedlastUpdateTimeclaims kill duplicate dispatch/pickers (hardened after two confirmed-real Codex P1s, both fixed in1779b09). Verified by: full unfiltered fresh-installtscexit 0 after each change; 719/723 vitest (4 pre-existing env-dependent failures); 6 new unit tests green. - Trigger for the merge: owner observed the second-message behaviour still live (2026-07-28) β
correct, since the fix was merged nowhere and deployed nowhere. Merge closes gate 1.
β οΈ DEPLOY STILL PENDING β production behaviour will NOT change until the owner explicitly orders
a deploy (owner policy: deploys are one-off, on-command). After deploy, runtime-verify per the
2026-07-27 checklist above. In-flight pendings created pre-deploy lack
promptMessageIdand keep the old text-prompt flow for their remainder β expected, not a regression. - Blast radius for other agents: Telegram webhook message flow (caption-confirm now a photo
card;
sendPhotoPromptMessageis the shared mechanic β T-182's/commandshould build on it); Inngest worker step layout (per-attemptrerun-cleanup-{iter}-aNsteps, newclean-image-backoff-Nsleeps,concurrency: 2);cleanImageoutput contract (never returns the green canvas; null on all-green); pending-docawaitinggains claim states (dispatching) β value is write-only today, but don't repurpose it.package-lock.jsonsynced (also landed independently via T-188). No Firestore schema changes anywhere. - Loose ends already ticketed in this file:
dispatchReceiptswallowsinngest.sendfailures (silent receipt loss β ticket-worthy); worker β οΈ De-warp notice still a separate message (polish); Gemini failure subtype capture needs a within-the-hour log pull after a batch; Inngest keys absent from the secrets bundle. - SHAs (append-only, final):
13e2baf,58c5808,835b332,71d4ab4,cd5c24f(owner),52889f5,36d726f,f712999,fe6c786,1779b09,c76b11a,47008de(merge), + the docs commit carrying this close-out.