You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Verified defects in the outbound integrations: the Slack client wrapper, GrantBot + the funding regexes, and the ORCID/PubMed/Grants HTTP layer.
Originally verified at origin/main @ b7edcbc (2026-07-30). Re-verified 2026-08-11 against the open PR-stack tip (issue-29-authorship-grounding @ b1d54da = main + #30/#31/#32). The stack fixed the Slack retry/pagination cluster and the invite import (V10) — marked below; grantbot.py, foa_cache.py and funding_rules.py are untouched by it.
V7 remainder is two trivial items. V8 is Tier 3 funding-feature correctness — its sharpest edge is COR-26b, a crash loop (the failed run never marks complete, so the scheduler re-fires it every interval).
_call_with_retry raises UnboundLocalError on rate-limit exhaustion — fixed in stack (last_exc alias, :324-342; regression-pinned at tests/unit/test_slack_client_contract.py:146-169).
poll_channel_messages has no pagination — fixed in stack: cursor-paginated via _paginate (:344-398, :505-545) with MAX_PAGES=200; on an incomplete listing it returns [] and the sim's cursor only advances per processed message, so the >100-message-gap skip is closed at both ends.
list_channels is a single page — fixed in stack (:1006-1059; re-raises on incomplete rather than returning a subset that looks complete).
resolve_user_name reads top-level display_name (:662) — still present; Slack returns it at profile.display_name, so the first branch is dead and every call degrades to real_name/user_id.
int(Retry-After) (:331) — still present, and sharper than filed: a non-integer header (RFC 7231 permits an HTTP-date; proxies inject them) raises ValueErrorfrom inside the except SlackApiError block, so callers' except SlackApiError never sees it — the same type-substitution class the UnboundLocalError fix just closed. Also unclamped: an arbitrarily large value blocks in time.sleep. Tests only ever feed str(int).
PR V8 — GrantBot + FOA regex + funding detection (small-medium; untouched by the stack)
COR-26 (remainder) — all four still present: the selection-failure fallback posts unvetted FOAs (grantbot.py:344-346 returns up to max_select arbitrary keys on any exception; downstream caps volume, not relevance); a dict element in the LLM's JSON hits num in all_opps → TypeError: unhashable type (:538), uncaught in run_grantbot — and because _mark_run_complete() is then skipped, the scheduler re-fires the crash every check_interval and main() eventually dies; GrantBot falls back to SuBot's token (:615-620), so funding posts are authored under SuBot's identity; _load_researcher_profiles/_extract_list_section/_build_search_queries (:49-132) are 84 dead lines with no callers. (Already fixed earlier: two-phase _claim_foa/_release_foa.)Fix: hard-fail selection; type-check the parsed list; drop the dead helpers.
COR-27 — still present; both patterns unchanged and divergent in both directions:
input
foa_cache.FOA_PATTERN
funding_rules._FOA_NUMBER_RE
PAR-24-293
✗
✓
PA-24-293
✗
✓
PAS-24-293
✗
✓
NOT-OD-24-001
✓
✗
DE-FOA-0003456
✗
✗
RFA-AI-27-019
✓
✓
Neither is IGNORECASE. foa_cache.py:19-21 forces a -[A-Z]{2,4}- institute segment, rejecting the entire PA/PAR/PAS parent-announcement family — the docstring at :18 is falsified by its own regex on 2 of its 3 examples, and extract_foa_number returns None for those posts so cache/prompt keys never resolve. Fix: one correct shared pattern accepting PA/PAR/PAS/RFA/NOT/DE-FOA with 2- and 4-digit years, used by both call sites.
COR-28 — still present:
The apostrophe classes in _ANNOUNCEMENT_PHRASES are ASCII-only in both phrases (funding_rules.py:23 — the class is the ASCII apostrophe listed twice — and :25). Verified live: "I'll spin up a dedicated thread." → filtered; the U+2019 curly form → passes. LLM output and Slack smart-quotes routinely produce U+2019.
is_acknowledgment_only_funding_reply (:108-134) false-rejects substantive short replies whose vocabulary is outside the fixed 28-term marker list (verified: "Agreed, we can send the plasmids and the mice next week." → rejected). The soft-livelock is sharper than filed: funding_reject_count resets only on a successful post (simulation.py:1465) while has_pending_reply re-arms at three sites — after two rejections a thread is permanently in one-strike mode.
_TAG_RE (:154) lacks IGNORECASE — partially mitigated by [Bb] (@grantbot matches; @GRANTBOT/@SuBOT don't). Fix: fix both apostrophe classes; broaden the ack detector (or reset the counter on any turn); add IGNORECASE to _TAG_RE.
COR-29 (robustness slice) — still present, all three: none of orcid.py/pubmed.py/grants.py contains any retry/backoff (every call is a one-shot httpx + raise_for_status()); raise_for_status() runs before the pacing sleep (pubmed.py:93-95), so pacing is skipped exactly when NCBI is 429-ing; Semaphore(8) (pubmed.py:73) with the 0.12 s sleep inside the semaphore yields tens of req/s against the keyless 3/s limit — the api_key presence check exists (:87-88) but never feeds back into concurrency. PR Authorship grounding: fix issue #29 across emit, memory, tools, and prompts #32 touched _ncbi_get (per-policy tool/email params, :76-90) without addressing any of this. Fix: retry/backoff; sleep before raise_for_status; size the semaphore to the key/no-key limit. (Shared AsyncClient + gather parallelization remain deferred to the refactor pass.)
COR-30 — still present, unchanged by Authorship grounding: fix issue #29 across emit, memory, tools, and prompts #32 (its diff touches only the docstring and the authors/DOI formatting): tools.py:126-129/:135-138 debit the abstract/full-text budget before the awaited fetch, and the blanket except at :146 converts a failure into an error string with no refund — a failed retrieval still consumes budget. Fix: increment only on success.
PR V10 — Fix the broken invite import(fixed in stack)
invite.py:232-252 now imports token_for_agent_row (src/services/slack_tokens.py:46) + lookup_user_by_email_async, and the handler logs instead of pass. (Resolved per-agent rather than via get_any_bot_token — better: the lookup uses the inviting agent's own bot.) Residual nit for whoever touches the file next: a None token skips the block with no log line.
Definition of done: each PR ships a test that fails against the pre-fix code. The regex fixes should be table-driven over the cases above.
Verified defects in the outbound integrations: the Slack client wrapper, GrantBot + the funding regexes, and the ORCID/PubMed/Grants HTTP layer.
Originally verified at
origin/main@b7edcbc(2026-07-30). Re-verified 2026-08-11 against the open PR-stack tip (issue-29-authorship-grounding@b1d54da= main + #30/#31/#32). The stack fixed the Slack retry/pagination cluster and the invite import (V10) — marked below;grantbot.py,foa_cache.pyandfunding_rules.pyare untouched by it.Priority (triage 2026-08-11)
_ncbi_get(addedtool/email) without touching the rate math. Land with issue Profile pipeline & write integrity: parse crashes, blanking writes, version races (3 PRs) #22's V1 (V1 = parsing, V9 = transport; V1 first keeps the diffs disjoint).Suggested order: V9 → V7 remainder → V8. V10 is done.
PR V7 — Slack client correctness cluster (small; mostly fixed in stack)
All in
src/agent/slack_client.py:— fixed in stack (_call_with_retryraisesUnboundLocalErroron rate-limit exhaustionlast_excalias,:324-342; regression-pinned attests/unit/test_slack_client_contract.py:146-169).— fixed in stack: cursor-paginated viapoll_channel_messageshas no pagination_paginate(:344-398,:505-545) withMAX_PAGES=200; on an incomplete listing it returns[]and the sim's cursor only advances per processed message, so the >100-message-gap skip is closed at both ends.— fixed in stack (list_channelsis a single page:1006-1059; re-raises on incomplete rather than returning a subset that looks complete).resolve_user_namereads top-leveldisplay_name(:662) — still present; Slack returns it atprofile.display_name, so the first branch is dead and every call degrades toreal_name/user_id.int(Retry-After)(:331) — still present, and sharper than filed: a non-integer header (RFC 7231 permits an HTTP-date; proxies inject them) raisesValueErrorfrom inside theexcept SlackApiErrorblock, so callers'except SlackApiErrornever sees it — the same type-substitution class theUnboundLocalErrorfix just closed. Also unclamped: an arbitrarily large value blocks intime.sleep. Tests only ever feedstr(int).Fix (remainder): read
profile.display_name; guard + clampRetry-After.PR V8 — GrantBot + FOA regex + funding detection (small-medium; untouched by the stack)
COR-26 (remainder) — all four still present: the selection-failure fallback posts unvetted FOAs (
grantbot.py:344-346returns up tomax_selectarbitrary keys on any exception; downstream caps volume, not relevance); a dict element in the LLM's JSON hitsnum in all_opps→TypeError: unhashable type(:538), uncaught inrun_grantbot— and because_mark_run_complete()is then skipped, the scheduler re-fires the crash everycheck_intervalandmain()eventually dies; GrantBot falls back to SuBot's token (:615-620), so funding posts are authored under SuBot's identity;_load_researcher_profiles/_extract_list_section/_build_search_queries(:49-132) are 84 dead lines with no callers. (Already fixed earlier: two-phase_claim_foa/_release_foa.) Fix: hard-fail selection; type-check the parsed list; drop the dead helpers.COR-27 — still present; both patterns unchanged and divergent in both directions:
foa_cache.FOA_PATTERNfunding_rules._FOA_NUMBER_REPAR-24-293PA-24-293PAS-24-293NOT-OD-24-001DE-FOA-0003456RFA-AI-27-019Neither is
IGNORECASE.foa_cache.py:19-21forces a-[A-Z]{2,4}-institute segment, rejecting the entire PA/PAR/PAS parent-announcement family — the docstring at:18is falsified by its own regex on 2 of its 3 examples, andextract_foa_numberreturnsNonefor those posts so cache/prompt keys never resolve. Fix: one correct shared pattern accepting PA/PAR/PAS/RFA/NOT/DE-FOA with 2- and 4-digit years, used by both call sites.COR-28 — still present:
_ANNOUNCEMENT_PHRASESare ASCII-only in both phrases (funding_rules.py:23— the class is the ASCII apostrophe listed twice — and:25). Verified live:"I'll spin up a dedicated thread."→ filtered; the U+2019 curly form → passes. LLM output and Slack smart-quotes routinely produce U+2019.is_acknowledgment_only_funding_reply(:108-134) false-rejects substantive short replies whose vocabulary is outside the fixed 28-term marker list (verified:"Agreed, we can send the plasmids and the mice next week."→ rejected). The soft-livelock is sharper than filed:funding_reject_countresets only on a successful post (simulation.py:1465) whilehas_pending_replyre-arms at three sites — after two rejections a thread is permanently in one-strike mode._TAG_RE(:154) lacksIGNORECASE— partially mitigated by[Bb](@grantbotmatches;@GRANTBOT/@SuBOTdon't).Fix: fix both apostrophe classes; broaden the ack detector (or reset the counter on any turn); add
IGNORECASEto_TAG_RE._extract_tagged_agentfix in issue Agent engine: turn & thread state-machine correctness (7 PRs) #20. Land one shared helper or keep the two in sync.PR V9 — External-HTTP robustness + tool counter charging (small-medium)
orcid.py/pubmed.py/grants.pycontains any retry/backoff (every call is a one-shothttpx+raise_for_status());raise_for_status()runs before the pacing sleep (pubmed.py:93-95), so pacing is skipped exactly when NCBI is 429-ing;Semaphore(8)(pubmed.py:73) with the 0.12 s sleep inside the semaphore yields tens of req/s against the keyless 3/s limit — theapi_keypresence check exists (:87-88) but never feeds back into concurrency. PR Authorship grounding: fix issue #29 across emit, memory, tools, and prompts #32 touched_ncbi_get(per-policytool/emailparams,:76-90) without addressing any of this. Fix: retry/backoff; sleep beforeraise_for_status; size the semaphore to the key/no-key limit. (SharedAsyncClient+gatherparallelization remain deferred to the refactor pass.)tools.py:126-129/:135-138debit the abstract/full-text budget before the awaited fetch, and the blanketexceptat:146converts a failure into an error string with no refund — a failed retrieval still consumes budget. Fix: increment only on success.PR V10 — Fix the broken invite import(fixed in stack)invite.py:232-252now importstoken_for_agent_row(src/services/slack_tokens.py:46) +lookup_user_by_email_async, and the handler logs instead ofpass. (Resolved per-agent rather than viaget_any_bot_token— better: the lookup uses the inviting agent's own bot.) Residual nit for whoever touches the file next: aNonetoken skips the block with no log line.Definition of done: each PR ships a test that fails against the pre-fix code. The regex fixes should be table-driven over the cases above.