Skip to content

fix(control): close the captcha governance loop - #65

Open
1012839419a-alt wants to merge 1 commit into
2233admin:mainfrom
1012839419a-alt:fix/captcha-governance-loop
Open

fix(control): close the captcha governance loop#65
1012839419a-alt wants to merge 1 commit into
2233admin:mainfrom
1012839419a-alt:fix/captcha-governance-loop

Conversation

@1012839419a-alt

Copy link
Copy Markdown
Contributor

Summary

PR #63 added error_type='captcha_challenge' classification but nothing consumed it: is_retryable() treated it as permanent (unknown types default to permanent), so a Doubao captcha wall permanently failed the task with no recovery path — while the control layer's pause/review machinery (actuator, paused_until, review_required) had no scheduler consumer at all.

Changes

  • error_taxonomy: add CAPTCHA_CHALLENGE + is_captcha() — a third category (needs human), neither retryable nor permanent
  • error_kinds: map captcha_challengeErrorKind.CAPTCHA (control vocabulary)
  • actuator: add pause_source_for_captcha() — pause + require_review in one call; actuator stays the only mutator of DataSource
  • pipeline: on captcha_challenge collect failure, pause the source (TTL from control_pause_ttl_seconds) + flag review instead of failing permanently; best-effort so actuator/DB errors never mask the original collect error
  • scheduler: _get_enabled_schedules also skips sources with review_required=True — a paused/flagged source is never dispatched until a human clears it

Test Plan

  • taxonomy: is_captcha / retryability (captcha is not retryable)
  • error_kinds: captcha_challenge → ErrorKind.CAPTCHA
  • actuator: pause_source_for_captcha pauses + flags review, idempotent re-pause
  • pipeline: captcha failure pauses source; ordinary failures don't touch actuator
  • scheduler: review_required and disabled sources are excluded from dispatch

uv run pytest on affected suites: 96 passed. ruff on changed files: no new issues vs baseline (repo has pre-existing style debt).

Closes the gap recorded in G3/G5 of the opencli-admin gap PRD.

PR 2233admin#63 added error_type='captcha_challenge' classification but nothing
consumed it: is_retryable() treated it as permanent (unknown types default
to permanent), so a Doubao captcha wall permanently failed the task with no
recovery path, while the control layer's pause/review machinery (actuator,
paused_until, review_required) had no scheduler consumer at all.

- error_taxonomy: add CAPTCHA_CHALLENGE + is_captcha() — a third category
  (needs human), neither retryable nor permanent
- error_kinds: map captcha_challenge -> ErrorKind.CAPTCHA for the control
  vocabulary
- actuator: add pause_source_for_captcha() — pause + require_review in one
  call, keeping the actuator the only mutator of DataSource
- pipeline: on captcha_challenge collect failure, pause the source (TTL from
  control_pause_ttl_seconds) + flag review instead of failing permanently;
  best-effort so actuator/DB errors never mask the original collect error
- scheduler: _get_enabled_schedules now also skips sources with
  review_required=True, so a paused/flagged source is never dispatched until
  a human clears it (control loop writes the state, scheduler honors it)

Tests: taxonomy (is_captcha/retryability), error_kinds mapping, actuator
pause+review (incl. idempotent re-pause), pipeline wiring (captcha pauses,
ordinary failures don't), scheduler review/disabled gating.
@repowise-bot

repowise-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown

✅ Health of changed files: 7.2 → 7.3 (+0.1)
🚨 Change risk: high, riskier than 69% of this repo's commits.

📋 At a glance
5 hotspots touched · 1 new finding introduced · 5 files with recent fix history.

Files & modules (2)
  • backend (4 files)
    • backend/pipeline/pipeline.py
    • backend/control/error_kinds.py
    • backend/control/actuator.py
    • backend/pipeline/error_taxonomy.py
  • tests (3 files)
    • tests/unit/test_scheduler.py
    • .../pipeline/test_pipeline_errors.py
    • .../pipeline/test_error_taxonomy.py

✅ Health gate: passed

📌 Before you merge

  • Run .../channels/test_cli_channel_execution.py, .../channels/test_rss_channel_schema_drift.py, .../channels/test_rss_fetch.py, .../channels/test_cli_channel_allowlist.py (+5 more): they import the changed files
🔎 More signals (2)

🗺️ Change map

flowchart LR
  subgraph PR ["Changed in this PR (5 with dependents)"]
    f_backend_control_actuator_py["backend/control/actuator.py 🔥"]:::changed
    f_backend_control_error_kinds_py["backend/control/error_kinds.py 🔥"]:::changed
    f_backend_pipeline_pipeline_py["backend/pipeline/pipeline.py 🔥"]:::changed
    f_backend_pipeline_error_taxonomy_py["backend/pipeline/error_taxonomy.py"]:::changed
    f_backend_scheduler_py["backend/scheduler.py"]:::changed
  end
  f_backend_control_cycle_py["backend/control/cycle.py"]
  f_backend_control_actuator_py --> f_backend_control_cycle_py
  f_backend_control_recorder_py["backend/control/recorder.py"]
  f_backend_control_error_kinds_py --> f_backend_control_recorder_py
  f_backend_api_v1___init___py[".../v1/__init__.py"]
  f_backend_pipeline_pipeline_py --> f_backend_api_v1___init___py
  f_backend_channels_rss_channel_py["backend/channels/rss_channel.py"]
  f_backend_pipeline_pipeline_py --> f_backend_channels_rss_channel_py
  f_backend_channels_skill_channel_py["backend/channels/skill_channel.py"]
  f_backend_pipeline_pipeline_py --> f_backend_channels_skill_channel_py
  f_backend_config_py["backend/config.py"]
  f_backend_pipeline_pipeline_py --> f_backend_config_py
  f_backend_channels_api_channel_py["backend/channels/api_channel.py"]
  f_backend_pipeline_error_taxonomy_py --> f_backend_channels_api_channel_py
  f_backend_pipeline_error_taxonomy_py --> f_backend_channels_rss_channel_py
  f_backend_channels_web_scraper_channel_py["backend/channels/web_scraper_channel.py"]
  f_backend_pipeline_error_taxonomy_py --> f_backend_channels_web_scraper_channel_py
  f_backend_pipeline_odp_client_py["backend/pipeline/odp_client.py"]
  f_backend_pipeline_error_taxonomy_py --> f_backend_pipeline_odp_client_py
  f_backend_main_py["backend/main.py"]
  f_backend_scheduler_py --> f_backend_main_py
  more(["+2 more dependents"])
  PR --> more
  t_tests_unit_channels_test_cli_channel_execution_py(["✅ .../channels/test_cli_channel_execution.py"]):::guard
  t_tests_unit_channels_test_cli_channel_execution_py -.-> f_backend_control_error_kinds_py
  t_tests_unit_pipeline_test_legacy_db_sink_py(["✅ .../pipeline/test_legacy_db_sink.py"]):::guard
  t_tests_unit_pipeline_test_legacy_db_sink_py -.-> f_backend_pipeline_pipeline_py
  t_tests_unit_channels_test_cli_channel_allowlist_py(["✅ .../channels/test_cli_channel_allowlist.py"]):::guard
  t_tests_unit_channels_test_cli_channel_allowlist_py -.-> f_backend_pipeline_error_taxonomy_py
  classDef changed fill:#dbeafe,stroke:#1d4ed8,color:#1e3a5f
  classDef warn fill:#fef3c7,stroke:#b45309,color:#78350f
  classDef guard fill:#dcfce7,stroke:#15803d,color:#14532d
Loading

Solid arrows: code that imports the changed files (12 direct dependents, from the last indexed snapshot). Dashed: history/tests.

🔥 Hotspots touched (5)

  • backend/pipeline/pipeline.py: 18 commits/90d, 14 dependents · primary owner: Curry (64%)
  • tests/unit/test_scheduler.py: 2 commits/90d, 0 dependents · primary owner: Curry (59%)
  • backend/control/error_kinds.py: 2 commits/90d, 7 dependents · primary owner: Curry (100%)
2 more
  • backend/control/actuator.py: 1 commits/90d, 2 dependents · primary owner: Curry (100%)
  • .../pipeline/test_pipeline_errors.py: 3 commits/90d, 1 dependents · primary owner: xujinghua (51%)

👀 Suggested reviewers @2233admin


📊 See the full report for this PR
Your repo map with this PR's blast radius lit up, every caller of the contracts it changes, and health before and after. No sign-in. · ⭐ Star Repowise · 📥 Install bot · Silence on a single PR with [skip repowise] in the title · Per-repo toggle on repowise.dev/settings?tab=bot · Updated 2026-08-07 11:51 UTC

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Detects CAPTCHA challenges during data collection and pauses affected sources for a configurable period.
    • Flags paused sources for review and reports CAPTCHA-specific failure details.
    • Prevents scheduled runs for sources awaiting review.
    • Adds CAPTCHA-specific error classification.
  • Bug Fixes

    • Preserves existing handling for non-CAPTCHA failures without triggering source pauses.

Walkthrough

Captcha failures now receive a dedicated classification. The pipeline pauses affected sources, marks them for review, reports failed results, and excludes reviewed sources from enabled scheduling. Unit and database-backed tests cover the new behavior.

Changes

Captcha governance flow

Layer / File(s) Summary
Captcha error contracts
backend/pipeline/error_taxonomy.py, backend/control/error_kinds.py, tests/unit/pipeline/test_error_taxonomy.py, tests/unit/control/test_error_kinds.py
The code adds captcha error detection and maps "captcha_challenge" to ErrorKind.CAPTCHA. Tests cover classification and negative cases.
Captcha failure pause handling
backend/control/actuator.py, backend/pipeline/pipeline.py, tests/unit/control/test_actuator.py, tests/unit/pipeline/test_pipeline_errors.py
Captcha failures pause the source for the configured TTL, set review_required, emit applicable warnings, and return failed results with captcha_paused metadata. Tests cover captcha and ordinary failures.
Review-required schedule filtering
backend/scheduler.py, tests/unit/test_scheduler.py
Enabled schedule queries exclude sources that require review. Tests confirm eligible schedules remain enabled and disabled sources remain excluded.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CollectionPipeline
  participant CaptchaActuator
  participant SourceDatabase
  participant Scheduler
  CollectionPipeline->>CaptchaActuator: pause source for configured TTL
  CaptchaActuator->>SourceDatabase: disable source and set review_required
  SourceDatabase-->>CollectionPipeline: return pause details
  CollectionPipeline-->>CollectionPipeline: return failed result with captcha_paused
  Scheduler->>SourceDatabase: query enabled schedules
  SourceDatabase-->>Scheduler: exclude review-required sources
Loading

Suggested reviewers: 2233admin

Poem

I’m a rabbit who guards the gate,
Captcha pauses sources before they activate.
A review flag marks the careful trail,
Schedules skip the flagged detail.
Tests hop neatly, green and bright.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary change: completing captcha governance through source pausing and review enforcement.
Description check ✅ Passed The description directly explains the captcha taxonomy, pausing, review enforcement, scheduler changes, and related tests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai
coderabbitai Bot requested a review from 2233admin August 7, 2026 11:51

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/pipeline/pipeline.py`:
- Around line 270-299: In the captcha-handling flow, introduce a local
captcha_paused flag initialized to False and set it to True only after
pause_source_for_captcha and the subsequent session.commit() complete
successfully. Return this flag in PipelineResult.metadata so missing sources or
any pause/commit exception report captcha_paused as False.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c518f9f4-7b2d-4c5b-a72e-24df7d0ba48f

📥 Commits

Reviewing files that changed from the base of the PR and between 94ab53d and 67776aa.

📒 Files selected for processing (10)
  • backend/control/actuator.py
  • backend/control/error_kinds.py
  • backend/pipeline/error_taxonomy.py
  • backend/pipeline/pipeline.py
  • backend/scheduler.py
  • tests/unit/control/test_actuator.py
  • tests/unit/control/test_error_kinds.py
  • tests/unit/pipeline/test_error_taxonomy.py
  • tests/unit/pipeline/test_pipeline_errors.py
  • tests/unit/test_scheduler.py

Comment on lines +270 to +299
ttl = get_settings().control_pause_ttl_seconds
async with AsyncSessionLocal() as session:
src = await session.get(DataSource, source.id)
if src is not None:
await pause_source_for_captcha(
session,
source=src,
now=datetime.now(timezone.utc),
ttl_seconds=ttl,
)
await session.commit()
logger.warning(
"[task:%s] captcha wall | paused source=%s (ttl=%ss, review_required)",
task_id, source.id, ttl,
)
if run_id:
await events.emit(
run_id, "collect",
"验证码拦截:数据源已暂停,等待人工处理",
level="warning",
detail={"captcha_paused": True, "pause_ttl_seconds": ttl},
)
except Exception:
logger.exception("[task:%s] failed to pause source on captcha", task_id)
return PipelineResult(
success=False,
source_id=source.id,
error=channel_result.error,
metadata={"captcha_paused": True},
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Set captcha_paused only after the pause commits.

The result always returns {"captcha_paused": True}. This remains true if session.get() returns no source, pause_source_for_captcha() fails, or session.commit() fails. The caller can then report a pause that did not persist, while the scheduler can continue to dispatch the source.

Track a local captcha_paused = False. Set it to True only after await session.commit() succeeds.

Proposed fix
+            captcha_paused = False
             try:
                 from backend.config import get_settings
                 from backend.control.actuator import pause_source_for_captcha
                 from backend.database import AsyncSessionLocal

                 ttl = get_settings().control_pause_ttl_seconds
                 async with AsyncSessionLocal() as session:
                     src = await session.get(DataSource, source.id)
                     if src is not None:
                         await pause_source_for_captcha(
                             session,
                             source=src,
                             now=datetime.now(timezone.utc),
                             ttl_seconds=ttl,
                         )
                         await session.commit()
+                        captcha_paused = True
                         logger.warning(
                             "[task:%s] captcha wall | paused source=%s (ttl=%ss, review_required)",
                             task_id, source.id, ttl,
                         )
@@
             return PipelineResult(
                 success=False,
                 source_id=source.id,
                 error=channel_result.error,
-                metadata={"captcha_paused": True},
+                metadata={"captcha_paused": captcha_paused},
             )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ttl = get_settings().control_pause_ttl_seconds
async with AsyncSessionLocal() as session:
src = await session.get(DataSource, source.id)
if src is not None:
await pause_source_for_captcha(
session,
source=src,
now=datetime.now(timezone.utc),
ttl_seconds=ttl,
)
await session.commit()
logger.warning(
"[task:%s] captcha wall | paused source=%s (ttl=%ss, review_required)",
task_id, source.id, ttl,
)
if run_id:
await events.emit(
run_id, "collect",
"验证码拦截:数据源已暂停,等待人工处理",
level="warning",
detail={"captcha_paused": True, "pause_ttl_seconds": ttl},
)
except Exception:
logger.exception("[task:%s] failed to pause source on captcha", task_id)
return PipelineResult(
success=False,
source_id=source.id,
error=channel_result.error,
metadata={"captcha_paused": True},
)
captcha_paused = False
try:
from backend.config import get_settings
from backend.control.actuator import pause_source_for_captcha
from backend.database import AsyncSessionLocal
ttl = get_settings().control_pause_ttl_seconds
async with AsyncSessionLocal() as session:
src = await session.get(DataSource, source.id)
if src is not None:
await pause_source_for_captcha(
session,
source=src,
now=datetime.now(timezone.utc),
ttl_seconds=ttl,
)
await session.commit()
captcha_paused = True
logger.warning(
"[task:%s] captcha wall | paused source=%s (ttl=%ss, review_required)",
task_id, source.id, ttl,
)
if run_id:
await events.emit(
run_id, "collect",
"验证码拦截:数据源已暂停,等待人工处理",
level="warning",
detail={"captcha_paused": True, "pause_ttl_seconds": ttl},
)
except Exception:
logger.exception("[task:%s] failed to pause source on captcha", task_id)
return PipelineResult(
success=False,
source_id=source.id,
error=channel_result.error,
metadata={"captcha_paused": captcha_paused},
)
🧰 Tools
🪛 Ruff (0.16.1)

[warning] 288-288: String contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF001)


[warning] 288-288: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/pipeline/pipeline.py` around lines 270 - 299, In the captcha-handling
flow, introduce a local captcha_paused flag initialized to False and set it to
True only after pause_source_for_captcha and the subsequent session.commit()
complete successfully. Return this flag in PipelineResult.metadata so missing
sources or any pause/commit exception report captcha_paused as False.

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