Skip to content

External clients: Slack SDK, GrantBot, FOA regexes, HTTP robustness (4 PRs) #23

Description

@ahueb

Four verified defects in the outbound integrations: the Slack client wrapper, GrantBot + the funding regexes, the ORCID/PubMed/Grants HTTP layer, and one broken import.

Line numbers current as of origin/main @ b7edcbc (2026-07-30).

Suggested order: V10 (trivial, and unblocks C1's invite.py site in issue #22) → V7 → V9 → V8.

PR V7 — Slack client correctness cluster (small)

All in src/agent/slack_client.py:

  • _call_with_retry raises UnboundLocalError on rate-limit exhaustion — except … as exc unbinds exc at block exit, and the post-loop raise SlackApiError(response=exc.response) (:167) then references it. Language semantics reproduced 2026-07-30: UnboundLocalError: cannot access local variable 'exc' where it is not associated with a value.
  • resolve_user_name reads top-level display_name (:339), but Slack returns it at profile.display_name → always falls through to real_name/user_id.
  • poll_channel_messages (:177-197, limit=100) has no pagination, and the sim advances the cursor past a >100-message gap.
  • list_channels is a single conversations_list(types=…, limit=200) (:598).
  • int(Retry-After) (:159) is unguarded.

(Already fixed upstream: get_full_channel_history/get_all_thread_replies are cursor-paginated.)
Fix: capture exc outside the loop; read profile.display_name; paginate the pollers and the channel list; guard Retry-After.

PR V8 — GrantBot + FOA regex + funding detection (small-medium)

  • COR-26 (remainder) — the selection-failure fallback still posts unvetted FOAs (grantbot.py:288-289 returns every key on any exception); a dict element in the LLM's JSON hits num in all_oppsTypeError: unhashable type, uncaught (:479); GrantBot falls back to SuBot's token (:534-537); _load_researcher_profiles (:48) and _build_search_queries (:98) are ~83 dead lines with no callers. (Already fixed: two-phase _claim_foa/_release_foa.) Fix: hard-fail selection; validate the drafted channel; drop the dead helpers.

  • COR-27foa_cache.py:19-21 forces a -[A-Z]{2,4}- segment. Re-tested live 2026-07-30:

    input foa_cache.FOA_PATTERN funding_rules._FOA_NUMBER_RE
    PAR-24-293
    PA-24-293
    PAS-24-293
    DE-FOA-0003456
    RFA-AI-27-019

    Two divergent patterns, neither IGNORECASE. Fix: one correct pattern accepting PA/PAR/PAS/RFA/NOT/DE-FOA with 2- and 4-digit years, used by both call sites.

  • COR-28 — the apostrophe classes in _ANNOUNCEMENT_PHRASES are ASCII-0x27-only (U+2019 absent, verified with od -c) in two phrases: \bi['']?ll (start|post|…) (funding_rules.py:23) and \bi'?m (going|about) to (…) (:25). Reproduced 2026-07-30: "I'll spin up a dedicated thread." → filtered; "I’ll spin up a dedicated thread." (curly) → passes the filter. Same for "I'm going to start a thread." vs the curly form. Also: is_acknowledgment_only_funding_reply false-rejects short marker-less replies (soft-livelock via funding_reject_count); _TAG_RE (:154) lacks IGNORECASE (_ANNOUNCEMENT_RE already has it). Fix: fix both apostrophe classes; broaden the ack detector; add IGNORECASE to _TAG_RE.

PR V9 — External-HTTP robustness + tool counter charging (small-medium)

  • COR-29 (robustness slice) — none of orcid.py/pubmed.py/grants.py contains any retry/backoff; raise_for_status() runs before the pacing sleep (pubmed.py:84-85); Semaphore(8) (pubmed.py:73) is sized above NCBI's keyless 3/s limit (ncbi_api_key=""). Fix: add retry/backoff; sleep before raise_for_status; size the semaphore to the key/no-key limit. (The "one shared AsyncClient" and "parallelize with gather" parts are deliberately deferred to the refactor pass.)
  • COR-30tools.py:116/125 increment the abstract/full-text budget before the awaited fetch, so a failed retrieval still consumes budget. Fix: increment only on success.

PR V10 — Fix the broken invite import (trivial)

invite.py:235 does from src.routers.agent_page import _get_bot_token — that name does not exist anywhere in src/. Confirmed 2026-07-30: the only two occurrences in the entire tree are the import and its call site at :236; the real helper is get_any_bot_token in src/services/slack_tokens.py:66. The ImportError is swallowed by except Exception: pass (:245), so the block that resolves a web-invited delegate's Slack ID (:238-244) is a permanent no-op. Fix: call get_any_bot_token(db) and stop swallowing ImportError.

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions