Skip to content

Deploy follow-ups: classify-call thinking pin + regression test, inbound runbook fix, publications backfill - #36

Merged
ahueb merged 7 commits into
copi-prodfrom
deploy-followups
Aug 14, 2026
Merged

Deploy follow-ups: classify-call thinking pin + regression test, inbound runbook fix, publications backfill#36
ahueb merged 7 commits into
copi-prodfrom
deploy-followups

Conversation

@ahueb

@ahueb ahueb commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #31 (email-fix): merge #31 (and #32) first — until then this PR's diff includes #31's commits. Only the three deploy-followups commits are new here. Targets copi-prod.

Executes the remediation-plan follow-up tracks from the 2026-08-14 deploy audit.

What this adds

  • fix(email): pin thinking={"type": "disabled"} on the inbound classify call — ports prod hotfix 0e2ed84 byte-identically into the email-fix line (so the merge with copi-prod stays trivial) and adds the regression test that was missing: without the pin, Sonnet 5's default thinking makes content[0] a thinking block, the .text read raises, and every inbound reply classifies as unparseable. The test was written first and failed on email-fix exactly as the latent bug predicted.
  • docs(inbound): the ENABLE_INBOUND_EMAIL flip must recreate app AND worker — the welcome email (built by the app) reads the same flag for its reply-vs-dashboard copy; the runbook previously recreated only the worker. Also documents why up -d and not docker restart (env_file resolves at container creation).
  • ops(publications): scripts/backfill_publications.py — the issue-29 rollout prerequisite for the 11 muted labs (badran, cravatt, good, kern, lotz, maillie, pwu, saez, schultz, williamson, wilson). Root cause, measured on prod 2026-08-14: their ORCID works are empty (profile jobs completed, cravatt ×3 / lotz ×4 retries, still zero publications) and their profiles contain zero DOIs, so the DB ∪ profile grounding union is empty and re-running the pipeline cannot fix it. The script takes a human-curated {agent_id: [pmid, ...]} JSON (PubMed author search can't disambiguate names like Wu or Wilson), fetches records via fetch_pubmed_records, canonicalizes DOIs, inserts idempotently, and is dry-run by default. Inserted rows go live on the next ~30s roster sync — no restart.

What this does NOT do

  • No curated PMID lists yet — curation is a human task; the script's dry run is the review checkpoint.
  • No migrations (alembic head stays 0024).

Testing

All code written test-first: the pin test failed red on email-fix, the backfill's five tests failed red against a stub. Full ./scripts/ci.sh gate green on this branch: 1681 passed / 120 skipped, 20 snapshots, migration round trip clean; also re-run by the pre-push hook on push.

🤖 Generated with Claude Code

ahueb and others added 7 commits August 11, 2026 08:58
…d in the prod investigation

Investigation of the dead reply-to-review flow (2026-08-11) found latent
defects that would break or undermine the pipeline even once its missing
AWS/DNS infrastructure is provisioned:

- The SEC-5 anti-spoofing gate merged verdicts across ALL
  Authentication-Results headers with "a pass wins", so a sender-forged
  pass header overrode SES's fail verdicts. Now only the topmost header
  (the one SES prepends on receipt) is trusted, and it must carry the
  amazonses.com authserv-id.
- HTML-only replies (no text/plain part) extracted an empty body and were
  silently dropped. Now fall back to tag-stripped HTML, with structural
  quote removal (blockquote/gmail_quote).
- Auto-submitted mail (RFC 3834, e.g. out-of-office) was processed and
  could be answered with a help email - a mail loop. Now ignored.
- MAX_REPLIES_PER_TOKEN_PER_HOUR was declared but never enforced. Now a
  sliding one-hour in-memory window per token.
- A poison S3 object was retried every poll forever. Now quarantined to
  failed/ after 3 attempts for manual inspection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Prod sent 129 review emails telling PIs to "reply to this email to rate
it" while ENABLE_INBOUND_EMAIL was off and the reply infrastructure (MX
record, S3 bucket, receipt rule) did not exist - every PI who replied got
silence plus an eventual bounce, which is the reported failure.

Gate the reply-soliciting copy and the Reply-To header on
settings.enable_inbound_email in the proposal-review reminder, the
new-proposal alert, and the welcome email. When the flag is off, all
three direct PIs to the web dashboard only, so outbound email can be
safely re-enabled before (or without) provisioning inbound.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 2026-08-11 investigation found every infrastructure layer of the
reply-to-review flow missing on prod: no MX record on reply.copi.science,
no copi-inbound-email S3 bucket, no SES receipt rule, send-only perms on
the copi-ec2-ses-role instance role, and ENABLE_INBOUND_EMAIL unset.

scripts/setup_inbound_email.py --check reports each layer;
--provision (admin creds) creates the bucket/policy/receipt rule and
prints the DNS records and the IAM policy that must be applied by hand.
docs/inbound-email.md is the architecture + bring-up runbook, including
the ordered re-enable steps and the end-to-end verification procedure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports prod hotfix 0e2ed84 byte-identically (the merge with copi-prod stays
trivial) and adds the regression test that was missing there: Sonnet 5 thinks
by default and max_tokens caps thinking + text together, so without the pin
content[0] is a thinking block, the .text read raises, and every inbound
reply classifies as unparseable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The welcome email (built by the app) reads ENABLE_INBOUND_EMAIL for its
reply-vs-dashboard copy, so recreating only the worker leaves new signups
with stale instructions. Also spell out why 'up -d' and not 'docker restart':
env_file is resolved at container creation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ons rows

Issue #29 rollout prerequisite. Eleven active labs have empty ORCID works, so
the profile pipeline (the only publications ingest path) persisted nothing and
the fail-closed authorship guard mutes their first-person paper claims. The
script takes a human-curated agent_id -> PMID JSON mapping (PubMed author
search cannot disambiguate names like Wu or Wilson), fetches records via
fetch_pubmed_records, canonicalizes DOIs, and inserts idempotently; dry-run
by default. Rows go live on the next ~30s roster sync, no restart.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ahueb
ahueb marked this pull request as ready for review August 14, 2026 15:01
@ahueb
ahueb merged commit 35ce7ea into copi-prod Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant