Skip to content

ADR 003 post-cutover follow-ups: delivery robustness and projection consistency #13

Description

@HCHogan

Tracking issue for the defects found in the post-cutover review (agents + manual verification, 2026-08-04). The legacy emote/card projection pollution was already repaired in production and is documented in docs/runbooks/adr003-cutover.md; everything below is a runtime fix deployable at normal cadence.

Ordered by priority:

  • 1. Retry attempt budget (ADR §7's one unimplemented promise). Deliveries that fail deterministically (QQ risk-control retcode≠0, reactions on deleted targets) retry forever as failed/AttemptRetryable and block the per-endpoint ordered lane behind them — retryDelay caps the interval at 300s but nothing caps attempts (src/Max/Platform/Delivery.hs retry paths, claim ordering predicate in src/Max/Platform/Store.hs). Convert over-budget attempts to suppressed (content) or outcome_unknown (undecidable) so the lane releases.
  • 2. LISTEN/NOTIFY listener can starve the pool into a deadlock. claimOrWait/waitForTimeline hold a listener connection and then request a second one for the recheck; with dispatch + delivery workers + a handful of admin timeline long-polls, all pool connections become listeners and every recheck blocks on acquiring a connection with no timeout (src/Max/DB/Notify.hs). Bound admin long-poll concurrency, or run the recheck on the pinned listener connection.
  • 3. Blob read failure poisons the whole message. loadDeliveryMedia treats an unreadable blob the same as an over-limit one: errorDeliveryPermanentlyFailed, taking the message's text down with it (src/Max/Platform/Delivery.hs). A missing/unreadable blob is semantically sourceless — ADR §2 says it must fold to the text tier with a lower note; only genuine size-limit violations should stay permanent failures.
  • 4. insertCanonical and verify disagree on the rendered_text source. Ingest renders from the unenriched 'Ingest body while maintenance verify/reproject recompute from the enriched canonical body + identity map, so verify false-positives (and dies) on post-cutover rows whose mention display got enriched. Render from the resolved body in insertCanonical (src/Max/Platform/Store.hs). Until fixed, do not run max-adr003-maintenance verify against the live database.
  • 5. Re-stamp compartment source_hash. Migration 062 changed both the hash function and its input set (055 rewrote content, 059 added event_kind, 060 changed occurred_at), so every stored conversation_compartments.source_hash is stale: admin integrity checks report all historical compartments as bad_source_ranges and episode expansion flags spurious source mismatches. One offline UPDATE … SET source_hash = conversation_source_hash(…) re-stamp.
  • 6. wechatpad deliveries never confirm. Self-message echoes are filtered before ingest (src/Max/Wechatpad.hs) so the content-match echo fallback never sees them, and listUnconfirmedDeliveries is only consumed by iMessage — every WeChat delivery parks at accepted_unconfirmed forever and the admin counter grows monotonically. The synthetic native id ("sent:" <> chat <> ":" <> take 32 body) also collides for repeated identical texts, which nativeEventAlreadyOwned then discards. Needs a real confirmation loop (or an honest terminal state) and a collision-free receipt id.
  • 7. QQ multi-image resend regressed. [image#id] referencing a multi-image message emits all stored images (messageImageNodes, src/Max/ReplySend.hs) but QQ's maxNativeMedia = 1 folds every image after the first into bare [图片] text with no attachable URL for blob refs. Either raise QQ's media budget (it can send several images per message) or split lowered media across chunks.

Lower priority, recorded from the same review:

  • Permanent failures and deliberate suppressions share status='suppressed', so poisoned messages are indistinguishable from quiet no-ops in /api/platforms/status monitoring without grepping last_error.
  • Matrix/iMessage blob-backed inbound media never reaches the vision pipeline (enqueueCanonicalMedia only picks remote URLs), so models see bare [image] for non-QQ media even though the bytes are already in BlobStore.
  • ensureConfiguredEndpoint resets endpoint_mode/caps on every boot, so a hand-promoted mirror reverts to standalone; no supported configuration lets a wechatpad room join a mirror conversation despite the code path existing.
  • Non-QQ conversations advertise the [@#<QQ号>] mention syntax and accept hallucinated numeric ids when no roster exists, minting fake principal identities on the destination account (src/Max/ReplySend.hs known-check, prompt advertisement in src/Max/Prompt/System.hs).
  • max_native_media > 1 on Matrix/iMessage endpoints permanently fails any multi-media chunk (ContractFailure): chunking never splits media, so the declared budget is unusable above 1.
  • Historical @全体成员 mentions were lifted as literal text; MentionAll semantics exist in the IR but the (now-squashed) backfill never produced them — only worth revisiting if mention-all lowering ever matters.
  • deliverContent's state machine (media fallback re-lower, empty-chunks suppression, toCompletion mapping) and the oneBot sendChunks failure matrix have no test coverage; same for the wechatpad outbound lifecycle.

Full analysis: the two review reports in the 2026-08-04 session; production repair record in docs/runbooks/adr003-cutover.md.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions