経歴書ドラフトPDF生成の非同期タスク化(ADR-0020) - #469

Merged
yusuke0610 merged 2 commits into
mainfrom
feat/async-resume-draft
Jul 6, 2026
Merged

経歴書ドラフトPDF生成の非同期タスク化(ADR-0020)#469
yusuke0610 merged 2 commits into
mainfrom
feat/async-resume-draft

Conversation

@yusuke0610

@yusuke0610yusuke0610 commented Jul 5, 2026

Copy link
Copy Markdown
Owner

概要

GitHub 連携後の経歴書ドラフト PDF 生成(ADR-0018)を、連携とは別トリガーの独立した非同期タスクに改修する。従来は同期エンドポイントで LLM→PDF 生成→レスポンスで PDF をそのまま返しており、生成中はブラウザが待たされ、タブを閉じると生成が無駄になっていた。

連携ボタンとドラフト生成ボタンを分離し、ドラフト生成はバックグラウンドで完了・通知される(既存の非同期タスク基盤・ポーリングを再利用)。モデルはユーザーが選択中のものを使う。

主な変更

バックエンド

  • 新タスク種別 TaskType.RESUME_DRAFT + ハンドラ + run_task.py(連携データ取得→LLM→PDF レンダリング検証→課金→保存)
  • 新テーブル resume_draft_cache(マイグレーション 0047)に payload(JSON) のみ最小永続化resumes/Resume には一切触れない。ダウンロード時に build_resume_pdf で再レンダリング(DB にバイナリを持たない)
  • エンドポイント: 旧同期 POST /api/agent/resume-draft/pdf を廃止し、POST .../run(202) / GET .../status / GET .../pdf に置換
  • 課金をルーターからタスク側へ移設し、不変条件を維持: PDF レンダリング成功後のみ課金/LLM・パース失敗時は消費済みトークンを課金/PDF 失敗は課金なし/課金記録失敗は NonRetryableError で dead_letter 化(リトライ再課金を防止)

フロント

  • useResumeDraftPdf を enqueue→useTaskPolling→ダウンロードへ改修(返り値の形は不変のためダッシュボードは変更なし)。マウント時に進行中タスクを検知したらポーリング復帰。Blob URL リーク防止は維持

ドキュメント

  • ADR-0020 を追加し、ADR-0018 の「何も永続化しない」を最小永続化へ refine。索引・agent.md を更新

テスト

  • make ci green(backend lint〈lint-tdd・lint-adr-index 含む〉+ test 、web vitest 372 件、build)
  • backend: test_worker/test_resume_draft.py(成功=completed+課金/パース失敗=課金あり/PDF 失敗=課金なし/未整備=NonRetryable)、test_resume_draft_api.py を非同期契約へ全面改修(enqueue 202 / status / download / 402 / 409 / 422)
  • front: useResumeDraftPdf.test.ts を非同期フローへ改修、E2E に「連携→ドラフト生成→プレビュー」1 本追加

備考

  • 破壊的変更: 旧同期エンドポイント POST /api/agent/resume-draft/pdf を削除(GET 版に置換)。web/src/api/generated.ts を再生成・同梱
  • マイグレーション: 新テーブル追加(ADD TABLE のみ)。実 libSQL 適用は CI smoke-backend で検証
  • ADR は当初 0019 を採番していたが、main の TDD ADR-0019 と衝突するため 0020 に採番。ベース機能(GitHub連携データから経歴書ドラフトPDFを生成する機能を追加 #463)は squash マージ済みのため、本 PR は最新 origin/main 起点で非同期化分のみを対象にしている

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Resume draft generation now runs in the background, with progress/status polling and a separate PDF download step after completion.
    • Added clearer completion/failure notifications for resume draft jobs.
  • Bug Fixes

    • Improved handling for in-progress, failed, and unavailable draft states with clearer user-facing errors.
    • PDF preview/download now works from saved completed results and avoids regenerating on demand.
  • Tests

    • Expanded API, worker, and end-to-end coverage for the new async resume draft flow.

GitHub連携後のドラフトPDF生成を、連携とは別トリガーの独立した非同期
タスクにする。連携ボタンとドラフト生成ボタンを分離し、生成はバック
グラウンドで完了・通知される(既存の非同期タスク基盤・ポーリングを再利用)。
- 新TaskType RESUME_DRAFT / ハンドラ / run_task.py(LLM→PDF検証→課金→保存)
- 新テーブル resume_draft_cache に payload のみ最小永続化(resumes には触れない)
DL時に build_resume_pdf で再レンダリング(バイナリをDBに持たない)
- エンドポイント: 旧同期 POST /resume-draft/pdf を廃止し run(202)/status/pdf(GET) に置換
- 課金をタスク側へ移設(PDF成功後のみ課金/LLM・パース失敗は課金/PDF失敗は課金なし を維持)
- モデルはユーザー選択中のものを使用。フロントは enqueue→ポーリング→DL へ改修
- ADR-0020 追加(0018 の「何も永続化しない」を最小永続化へ refine)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation backend バックエンド web フロントエンド (web) test テスト追加・修正 agent DevForge Agent labels Jul 5, 2026
@coderabbitai

coderabbitaiBot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@yusuke0610, you've reached your PR review limit, so we couldn't start this review.

Next review available in:11 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 609cf2b6-f857-4050-a9b8-21f2bad096c4

📥 Commits

Reviewing files that changed from the base of the PR and between 301143a and a4147e3.

📒 Files selected for processing (3)
  • .claude/rules/backend/agent.md
  • backend/app/services/agent/resume_draft/run_task.py
  • backend/tests/test_worker/test_resume_draft.py
📝 Walkthrough

Walkthrough

This PR migrates resume draft PDF generation from a synchronous, single-request LLM call into an asynchronous task-based flow. It adds a resume_draft_cache table/model, a RESUME_DRAFT task type/handler, an async worker (run_resume_draft_task) with billing/error semantics, restructured router endpoints (run/status/pdf), frontend polling integration, tests, and an ADR documenting the design.

Changes

Async resume draft generation

Layer / File(s)Summary
Cache schema, migration, and model
backend/alembic_migrations/versions/0047_add_resume_draft_cache_table.py, backend/app/models/cache.py, backend/app/models/__init__.py, backend/app/repositories/resume_draft.py
Adds resume_draft_cache table (one row per user) and ResumeDraftCache ORM model with status/retry/timestamp tracking, plus ResumeDraftCacheRepository with get_by_user/get_or_create and concurrency handling.
Async task runner and handler wiring
backend/app/services/agent/resume_draft/run_task.py, backend/app/services/tasks/base.py, backend/app/services/tasks/handlers/resume_draft.py, backend/app/services/tasks/handlers/__init__.py
Adds run_resume_draft_task implementing LLM generation, PDF validation, and billing with NonRetryableError semantics; registers TaskType.RESUME_DRAFT and ResumeDraftHandler.
Router endpoints for run/status/pdf
backend/app/routers/agent.py, backend/app/messages.json
Replaces synchronous PDF endpoint with POST /run (202 enqueue), GET /status (polling), and GET /pdf (download from completed cache); adds new error/notification messages.
Backend integration and worker tests
backend/tests/test_resume_draft_api.py, backend/tests/test_worker/test_resume_draft.py
Rewrites API tests for the new async flow and adds worker unit tests for success, parse failure charging, PDF failure, and missing cache/source scenarios.
Frontend API client and paths
web/src/api/agent.ts, web/src/api/generated.ts, web/src/api/paths.ts
Adds startResumeDraft, getResumeDraftStatus, fetchResumeDraftPdfBlobUrl functions and updates generated OpenAPI types/paths for the new endpoints.
useResumeDraftPdf polling hook and UI
web/src/hooks/useResumeDraftPdf.ts, web/src/hooks/useResumeDraftPdf.test.ts, web/e2e/github-link.spec.ts, web/src/constants/messages.ts
Rewrites the hook to enqueue, poll, and fetch the completed PDF; updates its tests, adds e2e coverage, and updates hint/notice messages.
Design docs and ADR
docs/adr/0020-async-resume-draft-generation.md, docs/adr/README.md, .claude/rules/backend/agent.md
Adds ADR-0020 documenting the async migration and billing/error contracts, and updates the ADR index and backend design rules accordingly.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
participant Frontend as useResumeDraftPdf
participant Router as agent router
participant Cache as resume_draft_cache
participant Worker as run_resume_draft_task
participant LLM as LLM/PDF builder
participant Billing as credit_service
Frontend->>Router: POST /resume-draft/run
Router->>Router: ensure_can_use_model, build_draft_source
Router->>Cache: get_or_create + set status=pending
Router->>Worker: dispatch background task
Router-->>Frontend: 202 TaskAcceptedResponse
loop polling
Frontend->>Router: GET /resume-draft/status
Router->>Cache: read status
Router-->>Frontend: TaskStatusResponse
end
Worker->>Cache: set status=processing
Worker->>LLM: run_resume_draft (generate)
alt LLM/parse failure
Worker->>Billing: charge consumed tokens
Worker->>Cache: raise NonRetryableError (dead_letter)
else success
Worker->>LLM: build_resume_pdf (validate)
alt PDF failure
Worker->>Cache: raise NonRetryableError (no charge)
else PDF valid
Worker->>Billing: record_chat_usage (final charge)
Worker->>Cache: write result, status=completed
end
end
Frontend->>Router: GET /resume-draft/pdf
Router->>Cache: read result (completed)
Router-->>Frontend: streamed PDF
Loading

Possibly related PRs

  • yusuke0610/devforge#289: Introduces the OpenAPI/TypeScript contract foundation including TaskAcceptedResponse used by the new async resume-draft endpoints.
  • yusuke0610/devforge#446: Changes build_resume_pdf to raise RuntimeError on failure, directly affecting the worker's PDF-failure handling path.
  • yusuke0610/devforge#463: Adds the original synchronous resume draft generation service that this PR migrates to the async task flow.

Suggested labels:feature

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ Passedタイトルは変更の主眼である経歴書ドラフトPDF生成の非同期タスク化を簡潔に表している。
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/async-resume-draft

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.

@coderabbitaicoderabbitaiBot 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: 2

🧹 Nitpick comments (3)
backend/tests/test_resume_draft_api.py (1)

85-160: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Missing test: re-enqueue guard when a draft task is already in-progress.

The router's try_reset_to_pending short-circuits and returns the existing cache.status (not "pending") instead of re-dispatching when a task is already pending/processing. This idempotency path prevents duplicate dispatch/re-billing but isn't covered by any test in this file.

♻️ Suggested test addition
deftest_resume_draft_run_returns_existing_status_when_already_pending(client: TestClient) ->None:
"""既に pending/processing 中なら再ディスパッチせず現在のステータスを返す。"""headers=auth_header(client, github_id=1)
_seed_link_data(client._db_session)
user=client._db_session.query(User).filter_by(username="testuser").one()
client._db_session.add(ResumeDraftCache(user_id=user.id, status="processing"))
client._db_session.commit()
res=client.post("/api/agent/resume-draft/run", json={"model": "haiku"}, headers=headers)
assertres.status_code==202assertres.json()["status"] =="processing"
🤖 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/tests/test_resume_draft_api.py` around lines 85 - 160, Add a test for
the idempotent re-enqueue guard in the resume draft flow: when an existing
ResumeDraftCache is already in "pending" or "processing", the request should not
dispatch again and should return the current cache status instead of "pending".
Extend backend/tests/test_resume_draft_api.py with a case around
test_resume_draft_run_enqueues and try_reset_to_pending that seeds link data,
inserts a ResumeDraftCache in processing state, calls
/api/agent/resume-draft/run, and asserts the 202 response echoes "processing"
from the response payload.
web/src/hooks/useResumeDraftPdf.ts (1)

92-105: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Stale previewUrl not cleared when re-generating.

Unlike the prior synchronous flow, generate() no longer resets previewUrl before enqueueing a new task, so an old preview can remain visible/stale until the new one completes. Self-recovers once loadPreview runs, but could confuse users mid-regeneration.

🩹 Suggested fix
 const generate = async () => {
if (generating) return;
setGenerating(true);
setError(null);
+ updatePreviewUrl(null);
try {
await startResumeDraft(model);
startPolling();
🤖 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 `@web/src/hooks/useResumeDraftPdf.ts` around lines 92 - 105, The generate flow
in useResumeDraftPdf should clear any existing previewUrl before starting a new
draft generation, since the async enqueue/polling path can leave a stale preview
visible until loadPreview updates it. Update generate() to reset the preview
state alongside setGenerating/setError before calling startResumeDraft(model),
so the UI doesn’t show an outdated preview during regeneration.
web/src/api/agent.ts (1)

25-53: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

ADR-0020 に更新してください
この非同期ドラフトフローの JSDoc は ADR-0018 ではなく ADR-0020 を参照するように揃えてください。関連するソースコメントも同じ番号に合わせると、生成される API ドキュメントとのずれもなくなります。

🤖 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 `@web/src/api/agent.ts` around lines 25 - 53, The JSDoc references in the
resume draft async flow are still pointing to ADR-0018, so update the comments
on startResumeDraft, getResumeDraftStatus, and fetchResumeDraftPdfBlobUrl to
reference ADR-0020 instead. Keep the surrounding descriptions the same, and make
sure any related source comments for this flow use the same ADR number so the
generated API docs stay consistent.
🤖 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 @.claude/rules/backend/agent.md:
- Around line 54-74: `run_resume_draft_task()` の課金と `resume_draft_cache`
への結果保存が別トランザクションになっており、さらに rerun 時に `dead_letter` を `pending`
に戻すため、`record_chat_usage()`
後に失敗すると再実行で二重課金される可能性があります。`run_resume_draft_task()`, `record_chat_usage()`,
`resume_draft_cache`, `dead_letter`
の再投入経路を見直し、課金済みマーカーを永続化するか、課金記録と結果保存を同一の原子操作にまとめてから、この billing flow
を安全とみなすよう修正してください。
In `@backend/app/services/agent/resume_draft/run_task.py`:
- Around line 105-126: The post-billing write-back in run_task() is still
retryable, so failures after record_chat_usage() can rerun the task and risk
double-charging or inconsistent cache state. Update the second session_factory
block that loads ResumeDraftCacheRepository and writes result/status so any
exception there is wrapped as NonRetryableError, matching the billing failure
handling, and keep the cache update path non-retryable after the charge has been
committed.
---
Nitpick comments:
In `@backend/tests/test_resume_draft_api.py`:
- Around line 85-160: Add a test for the idempotent re-enqueue guard in the
resume draft flow: when an existing ResumeDraftCache is already in "pending" or
"processing", the request should not dispatch again and should return the
current cache status instead of "pending". Extend
backend/tests/test_resume_draft_api.py with a case around
test_resume_draft_run_enqueues and try_reset_to_pending that seeds link data,
inserts a ResumeDraftCache in processing state, calls
/api/agent/resume-draft/run, and asserts the 202 response echoes "processing"
from the response payload.
In `@web/src/api/agent.ts`:
- Around line 25-53: The JSDoc references in the resume draft async flow are
still pointing to ADR-0018, so update the comments on startResumeDraft,
getResumeDraftStatus, and fetchResumeDraftPdfBlobUrl to reference ADR-0020
instead. Keep the surrounding descriptions the same, and make sure any related
source comments for this flow use the same ADR number so the generated API docs
stay consistent.
In `@web/src/hooks/useResumeDraftPdf.ts`:
- Around line 92-105: The generate flow in useResumeDraftPdf should clear any
existing previewUrl before starting a new draft generation, since the async
enqueue/polling path can leave a stale preview visible until loadPreview updates
it. Update generate() to reset the preview state alongside
setGenerating/setError before calling startResumeDraft(model), so the UI doesn’t
show an outdated preview during regeneration.
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b1c56f8e-c984-477b-9471-66dabdae96ce

📥 Commits

Reviewing files that changed from the base of the PR and between f3258b7 and 301143a.

📒 Files selected for processing (22)
  • .claude/rules/backend/agent.md
  • backend/alembic_migrations/versions/0047_add_resume_draft_cache_table.py
  • backend/app/messages.json
  • backend/app/models/__init__.py
  • backend/app/models/cache.py
  • backend/app/repositories/resume_draft.py
  • backend/app/routers/agent.py
  • backend/app/services/agent/resume_draft/run_task.py
  • backend/app/services/tasks/base.py
  • backend/app/services/tasks/handlers/__init__.py
  • backend/app/services/tasks/handlers/resume_draft.py
  • backend/tests/test_resume_draft_api.py
  • backend/tests/test_worker/test_resume_draft.py
  • docs/adr/0020-async-resume-draft-generation.md
  • docs/adr/README.md
  • web/e2e/github-link.spec.ts
  • web/src/api/agent.ts
  • web/src/api/generated.ts
  • web/src/api/paths.ts
  • web/src/constants/messages.ts
  • web/src/hooks/useResumeDraftPdf.test.ts
  • web/src/hooks/useResumeDraftPdf.ts

Comment thread.claude/rules/backend/agent.md
Comment threadbackend/app/services/agent/resume_draft/run_task.py Outdated
課金(record_chat_usage)と結果保存を同一トランザクションで確定し、
「課金済みだが結果未保存」からの再実行による二重課金を構造的に防ぐ。
加えてフェーズA に completed 冪等ガードを追加し、原子 commit 後・ack 前の
クラッシュによる Cloud Tasks 再配信での再課金を防ぐ。回帰テストを追加。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVqj6qCo31rXwKASxB5V2Z
@yusuke0610
yusuke0610 merged commit 114f801 into mainJul 6, 2026
20 checks passed
@yusuke0610
yusuke0610 deleted the feat/async-resume-draft branch July 20, 2026 12:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agentDevForge AgentbackendバックエンドdocumentationImprovements or additions to documentationtestテスト追加・修正webフロントエンド (web)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@yusuke0610@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

経歴書ドラフトPDF生成の非同期タスク化(ADR-0020) - #469

Merged
yusuke0610 merged 2 commits into
mainfrom
feat/async-resume-draft
Jul 6, 2026
Merged

経歴書ドラフトPDF生成の非同期タスク化(ADR-0020)#469
yusuke0610 merged 2 commits into
mainfrom
feat/async-resume-draft

Conversation

@yusuke0610

@yusuke0610yusuke0610 commented Jul 5, 2026

Copy link
Copy Markdown
Owner

概要

GitHub 連携後の経歴書ドラフト PDF 生成(ADR-0018)を、連携とは別トリガーの独立した非同期タスクに改修する。従来は同期エンドポイントで LLM→PDF 生成→レスポンスで PDF をそのまま返しており、生成中はブラウザが待たされ、タブを閉じると生成が無駄になっていた。

連携ボタンとドラフト生成ボタンを分離し、ドラフト生成はバックグラウンドで完了・通知される(既存の非同期タスク基盤・ポーリングを再利用)。モデルはユーザーが選択中のものを使う。

主な変更

バックエンド

  • 新タスク種別 TaskType.RESUME_DRAFT + ハンドラ + run_task.py(連携データ取得→LLM→PDF レンダリング検証→課金→保存)
  • 新テーブル resume_draft_cache(マイグレーション 0047)に payload(JSON) のみ最小永続化resumes/Resume には一切触れない。ダウンロード時に build_resume_pdf で再レンダリング(DB にバイナリを持たない)
  • エンドポイント: 旧同期 POST /api/agent/resume-draft/pdf を廃止し、POST .../run(202) / GET .../status / GET .../pdf に置換
  • 課金をルーターからタスク側へ移設し、不変条件を維持: PDF レンダリング成功後のみ課金/LLM・パース失敗時は消費済みトークンを課金/PDF 失敗は課金なし/課金記録失敗は NonRetryableError で dead_letter 化(リトライ再課金を防止)

フロント

  • useResumeDraftPdf を enqueue→useTaskPolling→ダウンロードへ改修(返り値の形は不変のためダッシュボードは変更なし)。マウント時に進行中タスクを検知したらポーリング復帰。Blob URL リーク防止は維持

ドキュメント

  • ADR-0020 を追加し、ADR-0018 の「何も永続化しない」を最小永続化へ refine。索引・agent.md を更新

テスト

  • make ci green(backend lint〈lint-tdd・lint-adr-index 含む〉+ test 、web vitest 372 件、build)
  • backend: test_worker/test_resume_draft.py(成功=completed+課金/パース失敗=課金あり/PDF 失敗=課金なし/未整備=NonRetryable)、test_resume_draft_api.py を非同期契約へ全面改修(enqueue 202 / status / download / 402 / 409 / 422)
  • front: useResumeDraftPdf.test.ts を非同期フローへ改修、E2E に「連携→ドラフト生成→プレビュー」1 本追加

備考

  • 破壊的変更: 旧同期エンドポイント POST /api/agent/resume-draft/pdf を削除(GET 版に置換)。web/src/api/generated.ts を再生成・同梱
  • マイグレーション: 新テーブル追加(ADD TABLE のみ)。実 libSQL 適用は CI smoke-backend で検証
  • ADR は当初 0019 を採番していたが、main の TDD ADR-0019 と衝突するため 0020 に採番。ベース機能(GitHub連携データから経歴書ドラフトPDFを生成する機能を追加 #463)は squash マージ済みのため、本 PR は最新 origin/main 起点で非同期化分のみを対象にしている

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Resume draft generation now runs in the background, with progress/status polling and a separate PDF download step after completion.
    • Added clearer completion/failure notifications for resume draft jobs.
  • Bug Fixes

    • Improved handling for in-progress, failed, and unavailable draft states with clearer user-facing errors.
    • PDF preview/download now works from saved completed results and avoids regenerating on demand.
  • Tests

    • Expanded API, worker, and end-to-end coverage for the new async resume draft flow.

GitHub連携後のドラフトPDF生成を、連携とは別トリガーの独立した非同期
タスクにする。連携ボタンとドラフト生成ボタンを分離し、生成はバック
グラウンドで完了・通知される(既存の非同期タスク基盤・ポーリングを再利用)。
- 新TaskType RESUME_DRAFT / ハンドラ / run_task.py(LLM→PDF検証→課金→保存)
- 新テーブル resume_draft_cache に payload のみ最小永続化(resumes には触れない)
DL時に build_resume_pdf で再レンダリング(バイナリをDBに持たない)
- エンドポイント: 旧同期 POST /resume-draft/pdf を廃止し run(202)/status/pdf(GET) に置換
- 課金をタスク側へ移設(PDF成功後のみ課金/LLM・パース失敗は課金/PDF失敗は課金なし を維持)
- モデルはユーザー選択中のものを使用。フロントは enqueue→ポーリング→DL へ改修
- ADR-0020 追加(0018 の「何も永続化しない」を最小永続化へ refine)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation backend バックエンド web フロントエンド (web) test テスト追加・修正 agent DevForge Agent labels Jul 5, 2026
@coderabbitai

coderabbitaiBot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@yusuke0610, you've reached your PR review limit, so we couldn't start this review.

Next review available in:11 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 609cf2b6-f857-4050-a9b8-21f2bad096c4

📥 Commits

Reviewing files that changed from the base of the PR and between 301143a and a4147e3.

📒 Files selected for processing (3)
  • .claude/rules/backend/agent.md
  • backend/app/services/agent/resume_draft/run_task.py
  • backend/tests/test_worker/test_resume_draft.py
📝 Walkthrough

Walkthrough

This PR migrates resume draft PDF generation from a synchronous, single-request LLM call into an asynchronous task-based flow. It adds a resume_draft_cache table/model, a RESUME_DRAFT task type/handler, an async worker (run_resume_draft_task) with billing/error semantics, restructured router endpoints (run/status/pdf), frontend polling integration, tests, and an ADR documenting the design.

Changes

Async resume draft generation

Layer / File(s)Summary
Cache schema, migration, and model
backend/alembic_migrations/versions/0047_add_resume_draft_cache_table.py, backend/app/models/cache.py, backend/app/models/__init__.py, backend/app/repositories/resume_draft.py
Adds resume_draft_cache table (one row per user) and ResumeDraftCache ORM model with status/retry/timestamp tracking, plus ResumeDraftCacheRepository with get_by_user/get_or_create and concurrency handling.
Async task runner and handler wiring
backend/app/services/agent/resume_draft/run_task.py, backend/app/services/tasks/base.py, backend/app/services/tasks/handlers/resume_draft.py, backend/app/services/tasks/handlers/__init__.py
Adds run_resume_draft_task implementing LLM generation, PDF validation, and billing with NonRetryableError semantics; registers TaskType.RESUME_DRAFT and ResumeDraftHandler.
Router endpoints for run/status/pdf
backend/app/routers/agent.py, backend/app/messages.json
Replaces synchronous PDF endpoint with POST /run (202 enqueue), GET /status (polling), and GET /pdf (download from completed cache); adds new error/notification messages.
Backend integration and worker tests
backend/tests/test_resume_draft_api.py, backend/tests/test_worker/test_resume_draft.py
Rewrites API tests for the new async flow and adds worker unit tests for success, parse failure charging, PDF failure, and missing cache/source scenarios.
Frontend API client and paths
web/src/api/agent.ts, web/src/api/generated.ts, web/src/api/paths.ts
Adds startResumeDraft, getResumeDraftStatus, fetchResumeDraftPdfBlobUrl functions and updates generated OpenAPI types/paths for the new endpoints.
useResumeDraftPdf polling hook and UI
web/src/hooks/useResumeDraftPdf.ts, web/src/hooks/useResumeDraftPdf.test.ts, web/e2e/github-link.spec.ts, web/src/constants/messages.ts
Rewrites the hook to enqueue, poll, and fetch the completed PDF; updates its tests, adds e2e coverage, and updates hint/notice messages.
Design docs and ADR
docs/adr/0020-async-resume-draft-generation.md, docs/adr/README.md, .claude/rules/backend/agent.md
Adds ADR-0020 documenting the async migration and billing/error contracts, and updates the ADR index and backend design rules accordingly.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
participant Frontend as useResumeDraftPdf
participant Router as agent router
participant Cache as resume_draft_cache
participant Worker as run_resume_draft_task
participant LLM as LLM/PDF builder
participant Billing as credit_service
Frontend->>Router: POST /resume-draft/run
Router->>Router: ensure_can_use_model, build_draft_source
Router->>Cache: get_or_create + set status=pending
Router->>Worker: dispatch background task
Router-->>Frontend: 202 TaskAcceptedResponse
loop polling
Frontend->>Router: GET /resume-draft/status
Router->>Cache: read status
Router-->>Frontend: TaskStatusResponse
end
Worker->>Cache: set status=processing
Worker->>LLM: run_resume_draft (generate)
alt LLM/parse failure
Worker->>Billing: charge consumed tokens
Worker->>Cache: raise NonRetryableError (dead_letter)
else success
Worker->>LLM: build_resume_pdf (validate)
alt PDF failure
Worker->>Cache: raise NonRetryableError (no charge)
else PDF valid
Worker->>Billing: record_chat_usage (final charge)
Worker->>Cache: write result, status=completed
end
end
Frontend->>Router: GET /resume-draft/pdf
Router->>Cache: read result (completed)
Router-->>Frontend: streamed PDF
Loading

Possibly related PRs

  • yusuke0610/devforge#289: Introduces the OpenAPI/TypeScript contract foundation including TaskAcceptedResponse used by the new async resume-draft endpoints.
  • yusuke0610/devforge#446: Changes build_resume_pdf to raise RuntimeError on failure, directly affecting the worker's PDF-failure handling path.
  • yusuke0610/devforge#463: Adds the original synchronous resume draft generation service that this PR migrates to the async task flow.

Suggested labels:feature

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ Passedタイトルは変更の主眼である経歴書ドラフトPDF生成の非同期タスク化を簡潔に表している。
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/async-resume-draft

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.

@coderabbitaicoderabbitaiBot 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: 2

🧹 Nitpick comments (3)
backend/tests/test_resume_draft_api.py (1)

85-160: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Missing test: re-enqueue guard when a draft task is already in-progress.

The router's try_reset_to_pending short-circuits and returns the existing cache.status (not "pending") instead of re-dispatching when a task is already pending/processing. This idempotency path prevents duplicate dispatch/re-billing but isn't covered by any test in this file.

♻️ Suggested test addition
deftest_resume_draft_run_returns_existing_status_when_already_pending(client: TestClient) ->None:
"""既に pending/processing 中なら再ディスパッチせず現在のステータスを返す。"""headers=auth_header(client, github_id=1)
_seed_link_data(client._db_session)
user=client._db_session.query(User).filter_by(username="testuser").one()
client._db_session.add(ResumeDraftCache(user_id=user.id, status="processing"))
client._db_session.commit()
res=client.post("/api/agent/resume-draft/run", json={"model": "haiku"}, headers=headers)
assertres.status_code==202assertres.json()["status"] =="processing"
🤖 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/tests/test_resume_draft_api.py` around lines 85 - 160, Add a test for
the idempotent re-enqueue guard in the resume draft flow: when an existing
ResumeDraftCache is already in "pending" or "processing", the request should not
dispatch again and should return the current cache status instead of "pending".
Extend backend/tests/test_resume_draft_api.py with a case around
test_resume_draft_run_enqueues and try_reset_to_pending that seeds link data,
inserts a ResumeDraftCache in processing state, calls
/api/agent/resume-draft/run, and asserts the 202 response echoes "processing"
from the response payload.
web/src/hooks/useResumeDraftPdf.ts (1)

92-105: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Stale previewUrl not cleared when re-generating.

Unlike the prior synchronous flow, generate() no longer resets previewUrl before enqueueing a new task, so an old preview can remain visible/stale until the new one completes. Self-recovers once loadPreview runs, but could confuse users mid-regeneration.

🩹 Suggested fix
 const generate = async () => {
if (generating) return;
setGenerating(true);
setError(null);
+ updatePreviewUrl(null);
try {
await startResumeDraft(model);
startPolling();
🤖 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 `@web/src/hooks/useResumeDraftPdf.ts` around lines 92 - 105, The generate flow
in useResumeDraftPdf should clear any existing previewUrl before starting a new
draft generation, since the async enqueue/polling path can leave a stale preview
visible until loadPreview updates it. Update generate() to reset the preview
state alongside setGenerating/setError before calling startResumeDraft(model),
so the UI doesn’t show an outdated preview during regeneration.
web/src/api/agent.ts (1)

25-53: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

ADR-0020 に更新してください
この非同期ドラフトフローの JSDoc は ADR-0018 ではなく ADR-0020 を参照するように揃えてください。関連するソースコメントも同じ番号に合わせると、生成される API ドキュメントとのずれもなくなります。

🤖 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 `@web/src/api/agent.ts` around lines 25 - 53, The JSDoc references in the
resume draft async flow are still pointing to ADR-0018, so update the comments
on startResumeDraft, getResumeDraftStatus, and fetchResumeDraftPdfBlobUrl to
reference ADR-0020 instead. Keep the surrounding descriptions the same, and make
sure any related source comments for this flow use the same ADR number so the
generated API docs stay consistent.
🤖 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 @.claude/rules/backend/agent.md:
- Around line 54-74: `run_resume_draft_task()` の課金と `resume_draft_cache`
への結果保存が別トランザクションになっており、さらに rerun 時に `dead_letter` を `pending`
に戻すため、`record_chat_usage()`
後に失敗すると再実行で二重課金される可能性があります。`run_resume_draft_task()`, `record_chat_usage()`,
`resume_draft_cache`, `dead_letter`
の再投入経路を見直し、課金済みマーカーを永続化するか、課金記録と結果保存を同一の原子操作にまとめてから、この billing flow
を安全とみなすよう修正してください。
In `@backend/app/services/agent/resume_draft/run_task.py`:
- Around line 105-126: The post-billing write-back in run_task() is still
retryable, so failures after record_chat_usage() can rerun the task and risk
double-charging or inconsistent cache state. Update the second session_factory
block that loads ResumeDraftCacheRepository and writes result/status so any
exception there is wrapped as NonRetryableError, matching the billing failure
handling, and keep the cache update path non-retryable after the charge has been
committed.
---
Nitpick comments:
In `@backend/tests/test_resume_draft_api.py`:
- Around line 85-160: Add a test for the idempotent re-enqueue guard in the
resume draft flow: when an existing ResumeDraftCache is already in "pending" or
"processing", the request should not dispatch again and should return the
current cache status instead of "pending". Extend
backend/tests/test_resume_draft_api.py with a case around
test_resume_draft_run_enqueues and try_reset_to_pending that seeds link data,
inserts a ResumeDraftCache in processing state, calls
/api/agent/resume-draft/run, and asserts the 202 response echoes "processing"
from the response payload.
In `@web/src/api/agent.ts`:
- Around line 25-53: The JSDoc references in the resume draft async flow are
still pointing to ADR-0018, so update the comments on startResumeDraft,
getResumeDraftStatus, and fetchResumeDraftPdfBlobUrl to reference ADR-0020
instead. Keep the surrounding descriptions the same, and make sure any related
source comments for this flow use the same ADR number so the generated API docs
stay consistent.
In `@web/src/hooks/useResumeDraftPdf.ts`:
- Around line 92-105: The generate flow in useResumeDraftPdf should clear any
existing previewUrl before starting a new draft generation, since the async
enqueue/polling path can leave a stale preview visible until loadPreview updates
it. Update generate() to reset the preview state alongside
setGenerating/setError before calling startResumeDraft(model), so the UI doesn’t
show an outdated preview during regeneration.
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b1c56f8e-c984-477b-9471-66dabdae96ce

📥 Commits

Reviewing files that changed from the base of the PR and between f3258b7 and 301143a.

📒 Files selected for processing (22)
  • .claude/rules/backend/agent.md
  • backend/alembic_migrations/versions/0047_add_resume_draft_cache_table.py
  • backend/app/messages.json
  • backend/app/models/__init__.py
  • backend/app/models/cache.py
  • backend/app/repositories/resume_draft.py
  • backend/app/routers/agent.py
  • backend/app/services/agent/resume_draft/run_task.py
  • backend/app/services/tasks/base.py
  • backend/app/services/tasks/handlers/__init__.py
  • backend/app/services/tasks/handlers/resume_draft.py
  • backend/tests/test_resume_draft_api.py
  • backend/tests/test_worker/test_resume_draft.py
  • docs/adr/0020-async-resume-draft-generation.md
  • docs/adr/README.md
  • web/e2e/github-link.spec.ts
  • web/src/api/agent.ts
  • web/src/api/generated.ts
  • web/src/api/paths.ts
  • web/src/constants/messages.ts
  • web/src/hooks/useResumeDraftPdf.test.ts
  • web/src/hooks/useResumeDraftPdf.ts

Comment thread.claude/rules/backend/agent.md
Comment threadbackend/app/services/agent/resume_draft/run_task.py Outdated
課金(record_chat_usage)と結果保存を同一トランザクションで確定し、
「課金済みだが結果未保存」からの再実行による二重課金を構造的に防ぐ。
加えてフェーズA に completed 冪等ガードを追加し、原子 commit 後・ack 前の
クラッシュによる Cloud Tasks 再配信での再課金を防ぐ。回帰テストを追加。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVqj6qCo31rXwKASxB5V2Z
@yusuke0610
yusuke0610 merged commit 114f801 into mainJul 6, 2026
20 checks passed
@yusuke0610
yusuke0610 deleted the feat/async-resume-draft branch July 20, 2026 12:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agentDevForge AgentbackendバックエンドdocumentationImprovements or additions to documentationtestテスト追加・修正webフロントエンド (web)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@yusuke0610@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

経歴書ドラフトPDF生成の非同期タスク化(ADR-0020) - #469

Merged
yusuke0610 merged 2 commits into
mainfrom
feat/async-resume-draft
Jul 6, 2026
Merged

経歴書ドラフトPDF生成の非同期タスク化(ADR-0020)#469
yusuke0610 merged 2 commits into
mainfrom
feat/async-resume-draft

Conversation

@yusuke0610

@yusuke0610yusuke0610 commented Jul 5, 2026

Copy link
Copy Markdown
Owner

概要

GitHub 連携後の経歴書ドラフト PDF 生成(ADR-0018)を、連携とは別トリガーの独立した非同期タスクに改修する。従来は同期エンドポイントで LLM→PDF 生成→レスポンスで PDF をそのまま返しており、生成中はブラウザが待たされ、タブを閉じると生成が無駄になっていた。

連携ボタンとドラフト生成ボタンを分離し、ドラフト生成はバックグラウンドで完了・通知される(既存の非同期タスク基盤・ポーリングを再利用)。モデルはユーザーが選択中のものを使う。

主な変更

バックエンド

  • 新タスク種別 TaskType.RESUME_DRAFT + ハンドラ + run_task.py(連携データ取得→LLM→PDF レンダリング検証→課金→保存)
  • 新テーブル resume_draft_cache(マイグレーション 0047)に payload(JSON) のみ最小永続化resumes/Resume には一切触れない。ダウンロード時に build_resume_pdf で再レンダリング(DB にバイナリを持たない)
  • エンドポイント: 旧同期 POST /api/agent/resume-draft/pdf を廃止し、POST .../run(202) / GET .../status / GET .../pdf に置換
  • 課金をルーターからタスク側へ移設し、不変条件を維持: PDF レンダリング成功後のみ課金/LLM・パース失敗時は消費済みトークンを課金/PDF 失敗は課金なし/課金記録失敗は NonRetryableError で dead_letter 化(リトライ再課金を防止)

フロント

  • useResumeDraftPdf を enqueue→useTaskPolling→ダウンロードへ改修(返り値の形は不変のためダッシュボードは変更なし)。マウント時に進行中タスクを検知したらポーリング復帰。Blob URL リーク防止は維持

ドキュメント

  • ADR-0020 を追加し、ADR-0018 の「何も永続化しない」を最小永続化へ refine。索引・agent.md を更新

テスト

  • make ci green(backend lint〈lint-tdd・lint-adr-index 含む〉+ test 、web vitest 372 件、build)
  • backend: test_worker/test_resume_draft.py(成功=completed+課金/パース失敗=課金あり/PDF 失敗=課金なし/未整備=NonRetryable)、test_resume_draft_api.py を非同期契約へ全面改修(enqueue 202 / status / download / 402 / 409 / 422)
  • front: useResumeDraftPdf.test.ts を非同期フローへ改修、E2E に「連携→ドラフト生成→プレビュー」1 本追加

備考

  • 破壊的変更: 旧同期エンドポイント POST /api/agent/resume-draft/pdf を削除(GET 版に置換)。web/src/api/generated.ts を再生成・同梱
  • マイグレーション: 新テーブル追加(ADD TABLE のみ)。実 libSQL 適用は CI smoke-backend で検証
  • ADR は当初 0019 を採番していたが、main の TDD ADR-0019 と衝突するため 0020 に採番。ベース機能(GitHub連携データから経歴書ドラフトPDFを生成する機能を追加 #463)は squash マージ済みのため、本 PR は最新 origin/main 起点で非同期化分のみを対象にしている

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Resume draft generation now runs in the background, with progress/status polling and a separate PDF download step after completion.
    • Added clearer completion/failure notifications for resume draft jobs.
  • Bug Fixes

    • Improved handling for in-progress, failed, and unavailable draft states with clearer user-facing errors.
    • PDF preview/download now works from saved completed results and avoids regenerating on demand.
  • Tests

    • Expanded API, worker, and end-to-end coverage for the new async resume draft flow.

GitHub連携後のドラフトPDF生成を、連携とは別トリガーの独立した非同期
タスクにする。連携ボタンとドラフト生成ボタンを分離し、生成はバック
グラウンドで完了・通知される(既存の非同期タスク基盤・ポーリングを再利用)。
- 新TaskType RESUME_DRAFT / ハンドラ / run_task.py(LLM→PDF検証→課金→保存)
- 新テーブル resume_draft_cache に payload のみ最小永続化(resumes には触れない)
DL時に build_resume_pdf で再レンダリング(バイナリをDBに持たない)
- エンドポイント: 旧同期 POST /resume-draft/pdf を廃止し run(202)/status/pdf(GET) に置換
- 課金をタスク側へ移設(PDF成功後のみ課金/LLM・パース失敗は課金/PDF失敗は課金なし を維持)
- モデルはユーザー選択中のものを使用。フロントは enqueue→ポーリング→DL へ改修
- ADR-0020 追加(0018 の「何も永続化しない」を最小永続化へ refine)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation backend バックエンド web フロントエンド (web) test テスト追加・修正 agent DevForge Agent labels Jul 5, 2026
@coderabbitai

coderabbitaiBot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@yusuke0610, you've reached your PR review limit, so we couldn't start this review.

Next review available in:11 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 609cf2b6-f857-4050-a9b8-21f2bad096c4

📥 Commits

Reviewing files that changed from the base of the PR and between 301143a and a4147e3.

📒 Files selected for processing (3)
  • .claude/rules/backend/agent.md
  • backend/app/services/agent/resume_draft/run_task.py
  • backend/tests/test_worker/test_resume_draft.py
📝 Walkthrough

Walkthrough

This PR migrates resume draft PDF generation from a synchronous, single-request LLM call into an asynchronous task-based flow. It adds a resume_draft_cache table/model, a RESUME_DRAFT task type/handler, an async worker (run_resume_draft_task) with billing/error semantics, restructured router endpoints (run/status/pdf), frontend polling integration, tests, and an ADR documenting the design.

Changes

Async resume draft generation

Layer / File(s)Summary
Cache schema, migration, and model
backend/alembic_migrations/versions/0047_add_resume_draft_cache_table.py, backend/app/models/cache.py, backend/app/models/__init__.py, backend/app/repositories/resume_draft.py
Adds resume_draft_cache table (one row per user) and ResumeDraftCache ORM model with status/retry/timestamp tracking, plus ResumeDraftCacheRepository with get_by_user/get_or_create and concurrency handling.
Async task runner and handler wiring
backend/app/services/agent/resume_draft/run_task.py, backend/app/services/tasks/base.py, backend/app/services/tasks/handlers/resume_draft.py, backend/app/services/tasks/handlers/__init__.py
Adds run_resume_draft_task implementing LLM generation, PDF validation, and billing with NonRetryableError semantics; registers TaskType.RESUME_DRAFT and ResumeDraftHandler.
Router endpoints for run/status/pdf
backend/app/routers/agent.py, backend/app/messages.json
Replaces synchronous PDF endpoint with POST /run (202 enqueue), GET /status (polling), and GET /pdf (download from completed cache); adds new error/notification messages.
Backend integration and worker tests
backend/tests/test_resume_draft_api.py, backend/tests/test_worker/test_resume_draft.py
Rewrites API tests for the new async flow and adds worker unit tests for success, parse failure charging, PDF failure, and missing cache/source scenarios.
Frontend API client and paths
web/src/api/agent.ts, web/src/api/generated.ts, web/src/api/paths.ts
Adds startResumeDraft, getResumeDraftStatus, fetchResumeDraftPdfBlobUrl functions and updates generated OpenAPI types/paths for the new endpoints.
useResumeDraftPdf polling hook and UI
web/src/hooks/useResumeDraftPdf.ts, web/src/hooks/useResumeDraftPdf.test.ts, web/e2e/github-link.spec.ts, web/src/constants/messages.ts
Rewrites the hook to enqueue, poll, and fetch the completed PDF; updates its tests, adds e2e coverage, and updates hint/notice messages.
Design docs and ADR
docs/adr/0020-async-resume-draft-generation.md, docs/adr/README.md, .claude/rules/backend/agent.md
Adds ADR-0020 documenting the async migration and billing/error contracts, and updates the ADR index and backend design rules accordingly.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
participant Frontend as useResumeDraftPdf
participant Router as agent router
participant Cache as resume_draft_cache
participant Worker as run_resume_draft_task
participant LLM as LLM/PDF builder
participant Billing as credit_service
Frontend->>Router: POST /resume-draft/run
Router->>Router: ensure_can_use_model, build_draft_source
Router->>Cache: get_or_create + set status=pending
Router->>Worker: dispatch background task
Router-->>Frontend: 202 TaskAcceptedResponse
loop polling
Frontend->>Router: GET /resume-draft/status
Router->>Cache: read status
Router-->>Frontend: TaskStatusResponse
end
Worker->>Cache: set status=processing
Worker->>LLM: run_resume_draft (generate)
alt LLM/parse failure
Worker->>Billing: charge consumed tokens
Worker->>Cache: raise NonRetryableError (dead_letter)
else success
Worker->>LLM: build_resume_pdf (validate)
alt PDF failure
Worker->>Cache: raise NonRetryableError (no charge)
else PDF valid
Worker->>Billing: record_chat_usage (final charge)
Worker->>Cache: write result, status=completed
end
end
Frontend->>Router: GET /resume-draft/pdf
Router->>Cache: read result (completed)
Router-->>Frontend: streamed PDF
Loading

Possibly related PRs

  • yusuke0610/devforge#289: Introduces the OpenAPI/TypeScript contract foundation including TaskAcceptedResponse used by the new async resume-draft endpoints.
  • yusuke0610/devforge#446: Changes build_resume_pdf to raise RuntimeError on failure, directly affecting the worker's PDF-failure handling path.
  • yusuke0610/devforge#463: Adds the original synchronous resume draft generation service that this PR migrates to the async task flow.

Suggested labels:feature

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ Passedタイトルは変更の主眼である経歴書ドラフトPDF生成の非同期タスク化を簡潔に表している。
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/async-resume-draft

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.

@coderabbitaicoderabbitaiBot 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: 2

🧹 Nitpick comments (3)
backend/tests/test_resume_draft_api.py (1)

85-160: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Missing test: re-enqueue guard when a draft task is already in-progress.

The router's try_reset_to_pending short-circuits and returns the existing cache.status (not "pending") instead of re-dispatching when a task is already pending/processing. This idempotency path prevents duplicate dispatch/re-billing but isn't covered by any test in this file.

♻️ Suggested test addition
deftest_resume_draft_run_returns_existing_status_when_already_pending(client: TestClient) ->None:
"""既に pending/processing 中なら再ディスパッチせず現在のステータスを返す。"""headers=auth_header(client, github_id=1)
_seed_link_data(client._db_session)
user=client._db_session.query(User).filter_by(username="testuser").one()
client._db_session.add(ResumeDraftCache(user_id=user.id, status="processing"))
client._db_session.commit()
res=client.post("/api/agent/resume-draft/run", json={"model": "haiku"}, headers=headers)
assertres.status_code==202assertres.json()["status"] =="processing"
🤖 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/tests/test_resume_draft_api.py` around lines 85 - 160, Add a test for
the idempotent re-enqueue guard in the resume draft flow: when an existing
ResumeDraftCache is already in "pending" or "processing", the request should not
dispatch again and should return the current cache status instead of "pending".
Extend backend/tests/test_resume_draft_api.py with a case around
test_resume_draft_run_enqueues and try_reset_to_pending that seeds link data,
inserts a ResumeDraftCache in processing state, calls
/api/agent/resume-draft/run, and asserts the 202 response echoes "processing"
from the response payload.
web/src/hooks/useResumeDraftPdf.ts (1)

92-105: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Stale previewUrl not cleared when re-generating.

Unlike the prior synchronous flow, generate() no longer resets previewUrl before enqueueing a new task, so an old preview can remain visible/stale until the new one completes. Self-recovers once loadPreview runs, but could confuse users mid-regeneration.

🩹 Suggested fix
 const generate = async () => {
if (generating) return;
setGenerating(true);
setError(null);
+ updatePreviewUrl(null);
try {
await startResumeDraft(model);
startPolling();
🤖 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 `@web/src/hooks/useResumeDraftPdf.ts` around lines 92 - 105, The generate flow
in useResumeDraftPdf should clear any existing previewUrl before starting a new
draft generation, since the async enqueue/polling path can leave a stale preview
visible until loadPreview updates it. Update generate() to reset the preview
state alongside setGenerating/setError before calling startResumeDraft(model),
so the UI doesn’t show an outdated preview during regeneration.
web/src/api/agent.ts (1)

25-53: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

ADR-0020 に更新してください
この非同期ドラフトフローの JSDoc は ADR-0018 ではなく ADR-0020 を参照するように揃えてください。関連するソースコメントも同じ番号に合わせると、生成される API ドキュメントとのずれもなくなります。

🤖 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 `@web/src/api/agent.ts` around lines 25 - 53, The JSDoc references in the
resume draft async flow are still pointing to ADR-0018, so update the comments
on startResumeDraft, getResumeDraftStatus, and fetchResumeDraftPdfBlobUrl to
reference ADR-0020 instead. Keep the surrounding descriptions the same, and make
sure any related source comments for this flow use the same ADR number so the
generated API docs stay consistent.
🤖 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 @.claude/rules/backend/agent.md:
- Around line 54-74: `run_resume_draft_task()` の課金と `resume_draft_cache`
への結果保存が別トランザクションになっており、さらに rerun 時に `dead_letter` を `pending`
に戻すため、`record_chat_usage()`
後に失敗すると再実行で二重課金される可能性があります。`run_resume_draft_task()`, `record_chat_usage()`,
`resume_draft_cache`, `dead_letter`
の再投入経路を見直し、課金済みマーカーを永続化するか、課金記録と結果保存を同一の原子操作にまとめてから、この billing flow
を安全とみなすよう修正してください。
In `@backend/app/services/agent/resume_draft/run_task.py`:
- Around line 105-126: The post-billing write-back in run_task() is still
retryable, so failures after record_chat_usage() can rerun the task and risk
double-charging or inconsistent cache state. Update the second session_factory
block that loads ResumeDraftCacheRepository and writes result/status so any
exception there is wrapped as NonRetryableError, matching the billing failure
handling, and keep the cache update path non-retryable after the charge has been
committed.
---
Nitpick comments:
In `@backend/tests/test_resume_draft_api.py`:
- Around line 85-160: Add a test for the idempotent re-enqueue guard in the
resume draft flow: when an existing ResumeDraftCache is already in "pending" or
"processing", the request should not dispatch again and should return the
current cache status instead of "pending". Extend
backend/tests/test_resume_draft_api.py with a case around
test_resume_draft_run_enqueues and try_reset_to_pending that seeds link data,
inserts a ResumeDraftCache in processing state, calls
/api/agent/resume-draft/run, and asserts the 202 response echoes "processing"
from the response payload.
In `@web/src/api/agent.ts`:
- Around line 25-53: The JSDoc references in the resume draft async flow are
still pointing to ADR-0018, so update the comments on startResumeDraft,
getResumeDraftStatus, and fetchResumeDraftPdfBlobUrl to reference ADR-0020
instead. Keep the surrounding descriptions the same, and make sure any related
source comments for this flow use the same ADR number so the generated API docs
stay consistent.
In `@web/src/hooks/useResumeDraftPdf.ts`:
- Around line 92-105: The generate flow in useResumeDraftPdf should clear any
existing previewUrl before starting a new draft generation, since the async
enqueue/polling path can leave a stale preview visible until loadPreview updates
it. Update generate() to reset the preview state alongside
setGenerating/setError before calling startResumeDraft(model), so the UI doesn’t
show an outdated preview during regeneration.
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b1c56f8e-c984-477b-9471-66dabdae96ce

📥 Commits

Reviewing files that changed from the base of the PR and between f3258b7 and 301143a.

📒 Files selected for processing (22)
  • .claude/rules/backend/agent.md
  • backend/alembic_migrations/versions/0047_add_resume_draft_cache_table.py
  • backend/app/messages.json
  • backend/app/models/__init__.py
  • backend/app/models/cache.py
  • backend/app/repositories/resume_draft.py
  • backend/app/routers/agent.py
  • backend/app/services/agent/resume_draft/run_task.py
  • backend/app/services/tasks/base.py
  • backend/app/services/tasks/handlers/__init__.py
  • backend/app/services/tasks/handlers/resume_draft.py
  • backend/tests/test_resume_draft_api.py
  • backend/tests/test_worker/test_resume_draft.py
  • docs/adr/0020-async-resume-draft-generation.md
  • docs/adr/README.md
  • web/e2e/github-link.spec.ts
  • web/src/api/agent.ts
  • web/src/api/generated.ts
  • web/src/api/paths.ts
  • web/src/constants/messages.ts
  • web/src/hooks/useResumeDraftPdf.test.ts
  • web/src/hooks/useResumeDraftPdf.ts

Comment thread.claude/rules/backend/agent.md
Comment threadbackend/app/services/agent/resume_draft/run_task.py Outdated
課金(record_chat_usage)と結果保存を同一トランザクションで確定し、
「課金済みだが結果未保存」からの再実行による二重課金を構造的に防ぐ。
加えてフェーズA に completed 冪等ガードを追加し、原子 commit 後・ack 前の
クラッシュによる Cloud Tasks 再配信での再課金を防ぐ。回帰テストを追加。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVqj6qCo31rXwKASxB5V2Z
@yusuke0610
yusuke0610 merged commit 114f801 into mainJul 6, 2026
20 checks passed
@yusuke0610
yusuke0610 deleted the feat/async-resume-draft branch July 20, 2026 12:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agentDevForge AgentbackendバックエンドdocumentationImprovements or additions to documentationtestテスト追加・修正webフロントエンド (web)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@yusuke0610@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

経歴書ドラフトPDF生成の非同期タスク化(ADR-0020) - #469

Merged
yusuke0610 merged 2 commits into
mainfrom
feat/async-resume-draft
Jul 6, 2026
Merged

経歴書ドラフトPDF生成の非同期タスク化(ADR-0020)#469
yusuke0610 merged 2 commits into
mainfrom
feat/async-resume-draft

Conversation

@yusuke0610

@yusuke0610yusuke0610 commented Jul 5, 2026

Copy link
Copy Markdown
Owner

概要

GitHub 連携後の経歴書ドラフト PDF 生成(ADR-0018)を、連携とは別トリガーの独立した非同期タスクに改修する。従来は同期エンドポイントで LLM→PDF 生成→レスポンスで PDF をそのまま返しており、生成中はブラウザが待たされ、タブを閉じると生成が無駄になっていた。

連携ボタンとドラフト生成ボタンを分離し、ドラフト生成はバックグラウンドで完了・通知される(既存の非同期タスク基盤・ポーリングを再利用)。モデルはユーザーが選択中のものを使う。

主な変更

バックエンド

  • 新タスク種別 TaskType.RESUME_DRAFT + ハンドラ + run_task.py(連携データ取得→LLM→PDF レンダリング検証→課金→保存)
  • 新テーブル resume_draft_cache(マイグレーション 0047)に payload(JSON) のみ最小永続化resumes/Resume には一切触れない。ダウンロード時に build_resume_pdf で再レンダリング(DB にバイナリを持たない)
  • エンドポイント: 旧同期 POST /api/agent/resume-draft/pdf を廃止し、POST .../run(202) / GET .../status / GET .../pdf に置換
  • 課金をルーターからタスク側へ移設し、不変条件を維持: PDF レンダリング成功後のみ課金/LLM・パース失敗時は消費済みトークンを課金/PDF 失敗は課金なし/課金記録失敗は NonRetryableError で dead_letter 化(リトライ再課金を防止)

フロント

  • useResumeDraftPdf を enqueue→useTaskPolling→ダウンロードへ改修(返り値の形は不変のためダッシュボードは変更なし)。マウント時に進行中タスクを検知したらポーリング復帰。Blob URL リーク防止は維持

ドキュメント

  • ADR-0020 を追加し、ADR-0018 の「何も永続化しない」を最小永続化へ refine。索引・agent.md を更新

テスト

  • make ci green(backend lint〈lint-tdd・lint-adr-index 含む〉+ test 、web vitest 372 件、build)
  • backend: test_worker/test_resume_draft.py(成功=completed+課金/パース失敗=課金あり/PDF 失敗=課金なし/未整備=NonRetryable)、test_resume_draft_api.py を非同期契約へ全面改修(enqueue 202 / status / download / 402 / 409 / 422)
  • front: useResumeDraftPdf.test.ts を非同期フローへ改修、E2E に「連携→ドラフト生成→プレビュー」1 本追加

備考

  • 破壊的変更: 旧同期エンドポイント POST /api/agent/resume-draft/pdf を削除(GET 版に置換)。web/src/api/generated.ts を再生成・同梱
  • マイグレーション: 新テーブル追加(ADD TABLE のみ)。実 libSQL 適用は CI smoke-backend で検証
  • ADR は当初 0019 を採番していたが、main の TDD ADR-0019 と衝突するため 0020 に採番。ベース機能(GitHub連携データから経歴書ドラフトPDFを生成する機能を追加 #463)は squash マージ済みのため、本 PR は最新 origin/main 起点で非同期化分のみを対象にしている

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Resume draft generation now runs in the background, with progress/status polling and a separate PDF download step after completion.
    • Added clearer completion/failure notifications for resume draft jobs.
  • Bug Fixes

    • Improved handling for in-progress, failed, and unavailable draft states with clearer user-facing errors.
    • PDF preview/download now works from saved completed results and avoids regenerating on demand.
  • Tests

    • Expanded API, worker, and end-to-end coverage for the new async resume draft flow.

GitHub連携後のドラフトPDF生成を、連携とは別トリガーの独立した非同期
タスクにする。連携ボタンとドラフト生成ボタンを分離し、生成はバック
グラウンドで完了・通知される(既存の非同期タスク基盤・ポーリングを再利用)。
- 新TaskType RESUME_DRAFT / ハンドラ / run_task.py(LLM→PDF検証→課金→保存)
- 新テーブル resume_draft_cache に payload のみ最小永続化(resumes には触れない)
DL時に build_resume_pdf で再レンダリング(バイナリをDBに持たない)
- エンドポイント: 旧同期 POST /resume-draft/pdf を廃止し run(202)/status/pdf(GET) に置換
- 課金をタスク側へ移設(PDF成功後のみ課金/LLM・パース失敗は課金/PDF失敗は課金なし を維持)
- モデルはユーザー選択中のものを使用。フロントは enqueue→ポーリング→DL へ改修
- ADR-0020 追加(0018 の「何も永続化しない」を最小永続化へ refine)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation backend バックエンド web フロントエンド (web) test テスト追加・修正 agent DevForge Agent labels Jul 5, 2026
@coderabbitai

coderabbitaiBot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@yusuke0610, you've reached your PR review limit, so we couldn't start this review.

Next review available in:11 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 609cf2b6-f857-4050-a9b8-21f2bad096c4

📥 Commits

Reviewing files that changed from the base of the PR and between 301143a and a4147e3.

📒 Files selected for processing (3)
  • .claude/rules/backend/agent.md
  • backend/app/services/agent/resume_draft/run_task.py
  • backend/tests/test_worker/test_resume_draft.py
📝 Walkthrough

Walkthrough

This PR migrates resume draft PDF generation from a synchronous, single-request LLM call into an asynchronous task-based flow. It adds a resume_draft_cache table/model, a RESUME_DRAFT task type/handler, an async worker (run_resume_draft_task) with billing/error semantics, restructured router endpoints (run/status/pdf), frontend polling integration, tests, and an ADR documenting the design.

Changes

Async resume draft generation

Layer / File(s)Summary
Cache schema, migration, and model
backend/alembic_migrations/versions/0047_add_resume_draft_cache_table.py, backend/app/models/cache.py, backend/app/models/__init__.py, backend/app/repositories/resume_draft.py
Adds resume_draft_cache table (one row per user) and ResumeDraftCache ORM model with status/retry/timestamp tracking, plus ResumeDraftCacheRepository with get_by_user/get_or_create and concurrency handling.
Async task runner and handler wiring
backend/app/services/agent/resume_draft/run_task.py, backend/app/services/tasks/base.py, backend/app/services/tasks/handlers/resume_draft.py, backend/app/services/tasks/handlers/__init__.py
Adds run_resume_draft_task implementing LLM generation, PDF validation, and billing with NonRetryableError semantics; registers TaskType.RESUME_DRAFT and ResumeDraftHandler.
Router endpoints for run/status/pdf
backend/app/routers/agent.py, backend/app/messages.json
Replaces synchronous PDF endpoint with POST /run (202 enqueue), GET /status (polling), and GET /pdf (download from completed cache); adds new error/notification messages.
Backend integration and worker tests
backend/tests/test_resume_draft_api.py, backend/tests/test_worker/test_resume_draft.py
Rewrites API tests for the new async flow and adds worker unit tests for success, parse failure charging, PDF failure, and missing cache/source scenarios.
Frontend API client and paths
web/src/api/agent.ts, web/src/api/generated.ts, web/src/api/paths.ts
Adds startResumeDraft, getResumeDraftStatus, fetchResumeDraftPdfBlobUrl functions and updates generated OpenAPI types/paths for the new endpoints.
useResumeDraftPdf polling hook and UI
web/src/hooks/useResumeDraftPdf.ts, web/src/hooks/useResumeDraftPdf.test.ts, web/e2e/github-link.spec.ts, web/src/constants/messages.ts
Rewrites the hook to enqueue, poll, and fetch the completed PDF; updates its tests, adds e2e coverage, and updates hint/notice messages.
Design docs and ADR
docs/adr/0020-async-resume-draft-generation.md, docs/adr/README.md, .claude/rules/backend/agent.md
Adds ADR-0020 documenting the async migration and billing/error contracts, and updates the ADR index and backend design rules accordingly.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
participant Frontend as useResumeDraftPdf
participant Router as agent router
participant Cache as resume_draft_cache
participant Worker as run_resume_draft_task
participant LLM as LLM/PDF builder
participant Billing as credit_service
Frontend->>Router: POST /resume-draft/run
Router->>Router: ensure_can_use_model, build_draft_source
Router->>Cache: get_or_create + set status=pending
Router->>Worker: dispatch background task
Router-->>Frontend: 202 TaskAcceptedResponse
loop polling
Frontend->>Router: GET /resume-draft/status
Router->>Cache: read status
Router-->>Frontend: TaskStatusResponse
end
Worker->>Cache: set status=processing
Worker->>LLM: run_resume_draft (generate)
alt LLM/parse failure
Worker->>Billing: charge consumed tokens
Worker->>Cache: raise NonRetryableError (dead_letter)
else success
Worker->>LLM: build_resume_pdf (validate)
alt PDF failure
Worker->>Cache: raise NonRetryableError (no charge)
else PDF valid
Worker->>Billing: record_chat_usage (final charge)
Worker->>Cache: write result, status=completed
end
end
Frontend->>Router: GET /resume-draft/pdf
Router->>Cache: read result (completed)
Router-->>Frontend: streamed PDF
Loading

Possibly related PRs

  • yusuke0610/devforge#289: Introduces the OpenAPI/TypeScript contract foundation including TaskAcceptedResponse used by the new async resume-draft endpoints.
  • yusuke0610/devforge#446: Changes build_resume_pdf to raise RuntimeError on failure, directly affecting the worker's PDF-failure handling path.
  • yusuke0610/devforge#463: Adds the original synchronous resume draft generation service that this PR migrates to the async task flow.

Suggested labels:feature

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ Passedタイトルは変更の主眼である経歴書ドラフトPDF生成の非同期タスク化を簡潔に表している。
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/async-resume-draft

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.

@coderabbitaicoderabbitaiBot 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: 2

🧹 Nitpick comments (3)
backend/tests/test_resume_draft_api.py (1)

85-160: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Missing test: re-enqueue guard when a draft task is already in-progress.

The router's try_reset_to_pending short-circuits and returns the existing cache.status (not "pending") instead of re-dispatching when a task is already pending/processing. This idempotency path prevents duplicate dispatch/re-billing but isn't covered by any test in this file.

♻️ Suggested test addition
deftest_resume_draft_run_returns_existing_status_when_already_pending(client: TestClient) ->None:
"""既に pending/processing 中なら再ディスパッチせず現在のステータスを返す。"""headers=auth_header(client, github_id=1)
_seed_link_data(client._db_session)
user=client._db_session.query(User).filter_by(username="testuser").one()
client._db_session.add(ResumeDraftCache(user_id=user.id, status="processing"))
client._db_session.commit()
res=client.post("/api/agent/resume-draft/run", json={"model": "haiku"}, headers=headers)
assertres.status_code==202assertres.json()["status"] =="processing"
🤖 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/tests/test_resume_draft_api.py` around lines 85 - 160, Add a test for
the idempotent re-enqueue guard in the resume draft flow: when an existing
ResumeDraftCache is already in "pending" or "processing", the request should not
dispatch again and should return the current cache status instead of "pending".
Extend backend/tests/test_resume_draft_api.py with a case around
test_resume_draft_run_enqueues and try_reset_to_pending that seeds link data,
inserts a ResumeDraftCache in processing state, calls
/api/agent/resume-draft/run, and asserts the 202 response echoes "processing"
from the response payload.
web/src/hooks/useResumeDraftPdf.ts (1)

92-105: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Stale previewUrl not cleared when re-generating.

Unlike the prior synchronous flow, generate() no longer resets previewUrl before enqueueing a new task, so an old preview can remain visible/stale until the new one completes. Self-recovers once loadPreview runs, but could confuse users mid-regeneration.

🩹 Suggested fix
 const generate = async () => {
if (generating) return;
setGenerating(true);
setError(null);
+ updatePreviewUrl(null);
try {
await startResumeDraft(model);
startPolling();
🤖 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 `@web/src/hooks/useResumeDraftPdf.ts` around lines 92 - 105, The generate flow
in useResumeDraftPdf should clear any existing previewUrl before starting a new
draft generation, since the async enqueue/polling path can leave a stale preview
visible until loadPreview updates it. Update generate() to reset the preview
state alongside setGenerating/setError before calling startResumeDraft(model),
so the UI doesn’t show an outdated preview during regeneration.
web/src/api/agent.ts (1)

25-53: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

ADR-0020 に更新してください
この非同期ドラフトフローの JSDoc は ADR-0018 ではなく ADR-0020 を参照するように揃えてください。関連するソースコメントも同じ番号に合わせると、生成される API ドキュメントとのずれもなくなります。

🤖 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 `@web/src/api/agent.ts` around lines 25 - 53, The JSDoc references in the
resume draft async flow are still pointing to ADR-0018, so update the comments
on startResumeDraft, getResumeDraftStatus, and fetchResumeDraftPdfBlobUrl to
reference ADR-0020 instead. Keep the surrounding descriptions the same, and make
sure any related source comments for this flow use the same ADR number so the
generated API docs stay consistent.
🤖 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 @.claude/rules/backend/agent.md:
- Around line 54-74: `run_resume_draft_task()` の課金と `resume_draft_cache`
への結果保存が別トランザクションになっており、さらに rerun 時に `dead_letter` を `pending`
に戻すため、`record_chat_usage()`
後に失敗すると再実行で二重課金される可能性があります。`run_resume_draft_task()`, `record_chat_usage()`,
`resume_draft_cache`, `dead_letter`
の再投入経路を見直し、課金済みマーカーを永続化するか、課金記録と結果保存を同一の原子操作にまとめてから、この billing flow
を安全とみなすよう修正してください。
In `@backend/app/services/agent/resume_draft/run_task.py`:
- Around line 105-126: The post-billing write-back in run_task() is still
retryable, so failures after record_chat_usage() can rerun the task and risk
double-charging or inconsistent cache state. Update the second session_factory
block that loads ResumeDraftCacheRepository and writes result/status so any
exception there is wrapped as NonRetryableError, matching the billing failure
handling, and keep the cache update path non-retryable after the charge has been
committed.
---
Nitpick comments:
In `@backend/tests/test_resume_draft_api.py`:
- Around line 85-160: Add a test for the idempotent re-enqueue guard in the
resume draft flow: when an existing ResumeDraftCache is already in "pending" or
"processing", the request should not dispatch again and should return the
current cache status instead of "pending". Extend
backend/tests/test_resume_draft_api.py with a case around
test_resume_draft_run_enqueues and try_reset_to_pending that seeds link data,
inserts a ResumeDraftCache in processing state, calls
/api/agent/resume-draft/run, and asserts the 202 response echoes "processing"
from the response payload.
In `@web/src/api/agent.ts`:
- Around line 25-53: The JSDoc references in the resume draft async flow are
still pointing to ADR-0018, so update the comments on startResumeDraft,
getResumeDraftStatus, and fetchResumeDraftPdfBlobUrl to reference ADR-0020
instead. Keep the surrounding descriptions the same, and make sure any related
source comments for this flow use the same ADR number so the generated API docs
stay consistent.
In `@web/src/hooks/useResumeDraftPdf.ts`:
- Around line 92-105: The generate flow in useResumeDraftPdf should clear any
existing previewUrl before starting a new draft generation, since the async
enqueue/polling path can leave a stale preview visible until loadPreview updates
it. Update generate() to reset the preview state alongside
setGenerating/setError before calling startResumeDraft(model), so the UI doesn’t
show an outdated preview during regeneration.
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b1c56f8e-c984-477b-9471-66dabdae96ce

📥 Commits

Reviewing files that changed from the base of the PR and between f3258b7 and 301143a.

📒 Files selected for processing (22)
  • .claude/rules/backend/agent.md
  • backend/alembic_migrations/versions/0047_add_resume_draft_cache_table.py
  • backend/app/messages.json
  • backend/app/models/__init__.py
  • backend/app/models/cache.py
  • backend/app/repositories/resume_draft.py
  • backend/app/routers/agent.py
  • backend/app/services/agent/resume_draft/run_task.py
  • backend/app/services/tasks/base.py
  • backend/app/services/tasks/handlers/__init__.py
  • backend/app/services/tasks/handlers/resume_draft.py
  • backend/tests/test_resume_draft_api.py
  • backend/tests/test_worker/test_resume_draft.py
  • docs/adr/0020-async-resume-draft-generation.md
  • docs/adr/README.md
  • web/e2e/github-link.spec.ts
  • web/src/api/agent.ts
  • web/src/api/generated.ts
  • web/src/api/paths.ts
  • web/src/constants/messages.ts
  • web/src/hooks/useResumeDraftPdf.test.ts
  • web/src/hooks/useResumeDraftPdf.ts

Comment thread.claude/rules/backend/agent.md
Comment threadbackend/app/services/agent/resume_draft/run_task.py Outdated
課金(record_chat_usage)と結果保存を同一トランザクションで確定し、
「課金済みだが結果未保存」からの再実行による二重課金を構造的に防ぐ。
加えてフェーズA に completed 冪等ガードを追加し、原子 commit 後・ack 前の
クラッシュによる Cloud Tasks 再配信での再課金を防ぐ。回帰テストを追加。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVqj6qCo31rXwKASxB5V2Z
@yusuke0610
yusuke0610 merged commit 114f801 into mainJul 6, 2026
20 checks passed
@yusuke0610
yusuke0610 deleted the feat/async-resume-draft branch July 20, 2026 12:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agentDevForge AgentbackendバックエンドdocumentationImprovements or additions to documentationtestテスト追加・修正webフロントエンド (web)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@yusuke0610@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

経歴書ドラフトPDF生成の非同期タスク化(ADR-0020) - #469

Merged
yusuke0610 merged 2 commits into
mainfrom
feat/async-resume-draft
Jul 6, 2026
Merged

経歴書ドラフトPDF生成の非同期タスク化(ADR-0020)#469
yusuke0610 merged 2 commits into
mainfrom
feat/async-resume-draft

Conversation

@yusuke0610

@yusuke0610yusuke0610 commented Jul 5, 2026

Copy link
Copy Markdown
Owner

概要

GitHub 連携後の経歴書ドラフト PDF 生成(ADR-0018)を、連携とは別トリガーの独立した非同期タスクに改修する。従来は同期エンドポイントで LLM→PDF 生成→レスポンスで PDF をそのまま返しており、生成中はブラウザが待たされ、タブを閉じると生成が無駄になっていた。

連携ボタンとドラフト生成ボタンを分離し、ドラフト生成はバックグラウンドで完了・通知される(既存の非同期タスク基盤・ポーリングを再利用)。モデルはユーザーが選択中のものを使う。

主な変更

バックエンド

  • 新タスク種別 TaskType.RESUME_DRAFT + ハンドラ + run_task.py(連携データ取得→LLM→PDF レンダリング検証→課金→保存)
  • 新テーブル resume_draft_cache(マイグレーション 0047)に payload(JSON) のみ最小永続化resumes/Resume には一切触れない。ダウンロード時に build_resume_pdf で再レンダリング(DB にバイナリを持たない)
  • エンドポイント: 旧同期 POST /api/agent/resume-draft/pdf を廃止し、POST .../run(202) / GET .../status / GET .../pdf に置換
  • 課金をルーターからタスク側へ移設し、不変条件を維持: PDF レンダリング成功後のみ課金/LLM・パース失敗時は消費済みトークンを課金/PDF 失敗は課金なし/課金記録失敗は NonRetryableError で dead_letter 化(リトライ再課金を防止)

フロント

  • useResumeDraftPdf を enqueue→useTaskPolling→ダウンロードへ改修(返り値の形は不変のためダッシュボードは変更なし)。マウント時に進行中タスクを検知したらポーリング復帰。Blob URL リーク防止は維持

ドキュメント

  • ADR-0020 を追加し、ADR-0018 の「何も永続化しない」を最小永続化へ refine。索引・agent.md を更新

テスト

  • make ci green(backend lint〈lint-tdd・lint-adr-index 含む〉+ test 、web vitest 372 件、build)
  • backend: test_worker/test_resume_draft.py(成功=completed+課金/パース失敗=課金あり/PDF 失敗=課金なし/未整備=NonRetryable)、test_resume_draft_api.py を非同期契約へ全面改修(enqueue 202 / status / download / 402 / 409 / 422)
  • front: useResumeDraftPdf.test.ts を非同期フローへ改修、E2E に「連携→ドラフト生成→プレビュー」1 本追加

備考

  • 破壊的変更: 旧同期エンドポイント POST /api/agent/resume-draft/pdf を削除(GET 版に置換)。web/src/api/generated.ts を再生成・同梱
  • マイグレーション: 新テーブル追加(ADD TABLE のみ)。実 libSQL 適用は CI smoke-backend で検証
  • ADR は当初 0019 を採番していたが、main の TDD ADR-0019 と衝突するため 0020 に採番。ベース機能(GitHub連携データから経歴書ドラフトPDFを生成する機能を追加 #463)は squash マージ済みのため、本 PR は最新 origin/main 起点で非同期化分のみを対象にしている

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Resume draft generation now runs in the background, with progress/status polling and a separate PDF download step after completion.
    • Added clearer completion/failure notifications for resume draft jobs.
  • Bug Fixes

    • Improved handling for in-progress, failed, and unavailable draft states with clearer user-facing errors.
    • PDF preview/download now works from saved completed results and avoids regenerating on demand.
  • Tests

    • Expanded API, worker, and end-to-end coverage for the new async resume draft flow.

GitHub連携後のドラフトPDF生成を、連携とは別トリガーの独立した非同期
タスクにする。連携ボタンとドラフト生成ボタンを分離し、生成はバック
グラウンドで完了・通知される(既存の非同期タスク基盤・ポーリングを再利用)。
- 新TaskType RESUME_DRAFT / ハンドラ / run_task.py(LLM→PDF検証→課金→保存)
- 新テーブル resume_draft_cache に payload のみ最小永続化(resumes には触れない)
DL時に build_resume_pdf で再レンダリング(バイナリをDBに持たない)
- エンドポイント: 旧同期 POST /resume-draft/pdf を廃止し run(202)/status/pdf(GET) に置換
- 課金をタスク側へ移設(PDF成功後のみ課金/LLM・パース失敗は課金/PDF失敗は課金なし を維持)
- モデルはユーザー選択中のものを使用。フロントは enqueue→ポーリング→DL へ改修
- ADR-0020 追加(0018 の「何も永続化しない」を最小永続化へ refine)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation backend バックエンド web フロントエンド (web) test テスト追加・修正 agent DevForge Agent labels Jul 5, 2026
@coderabbitai

coderabbitaiBot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@yusuke0610, you've reached your PR review limit, so we couldn't start this review.

Next review available in:11 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 609cf2b6-f857-4050-a9b8-21f2bad096c4

📥 Commits

Reviewing files that changed from the base of the PR and between 301143a and a4147e3.

📒 Files selected for processing (3)
  • .claude/rules/backend/agent.md
  • backend/app/services/agent/resume_draft/run_task.py
  • backend/tests/test_worker/test_resume_draft.py
📝 Walkthrough

Walkthrough

This PR migrates resume draft PDF generation from a synchronous, single-request LLM call into an asynchronous task-based flow. It adds a resume_draft_cache table/model, a RESUME_DRAFT task type/handler, an async worker (run_resume_draft_task) with billing/error semantics, restructured router endpoints (run/status/pdf), frontend polling integration, tests, and an ADR documenting the design.

Changes

Async resume draft generation

Layer / File(s)Summary
Cache schema, migration, and model
backend/alembic_migrations/versions/0047_add_resume_draft_cache_table.py, backend/app/models/cache.py, backend/app/models/__init__.py, backend/app/repositories/resume_draft.py
Adds resume_draft_cache table (one row per user) and ResumeDraftCache ORM model with status/retry/timestamp tracking, plus ResumeDraftCacheRepository with get_by_user/get_or_create and concurrency handling.
Async task runner and handler wiring
backend/app/services/agent/resume_draft/run_task.py, backend/app/services/tasks/base.py, backend/app/services/tasks/handlers/resume_draft.py, backend/app/services/tasks/handlers/__init__.py
Adds run_resume_draft_task implementing LLM generation, PDF validation, and billing with NonRetryableError semantics; registers TaskType.RESUME_DRAFT and ResumeDraftHandler.
Router endpoints for run/status/pdf
backend/app/routers/agent.py, backend/app/messages.json
Replaces synchronous PDF endpoint with POST /run (202 enqueue), GET /status (polling), and GET /pdf (download from completed cache); adds new error/notification messages.
Backend integration and worker tests
backend/tests/test_resume_draft_api.py, backend/tests/test_worker/test_resume_draft.py
Rewrites API tests for the new async flow and adds worker unit tests for success, parse failure charging, PDF failure, and missing cache/source scenarios.
Frontend API client and paths
web/src/api/agent.ts, web/src/api/generated.ts, web/src/api/paths.ts
Adds startResumeDraft, getResumeDraftStatus, fetchResumeDraftPdfBlobUrl functions and updates generated OpenAPI types/paths for the new endpoints.
useResumeDraftPdf polling hook and UI
web/src/hooks/useResumeDraftPdf.ts, web/src/hooks/useResumeDraftPdf.test.ts, web/e2e/github-link.spec.ts, web/src/constants/messages.ts
Rewrites the hook to enqueue, poll, and fetch the completed PDF; updates its tests, adds e2e coverage, and updates hint/notice messages.
Design docs and ADR
docs/adr/0020-async-resume-draft-generation.md, docs/adr/README.md, .claude/rules/backend/agent.md
Adds ADR-0020 documenting the async migration and billing/error contracts, and updates the ADR index and backend design rules accordingly.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
participant Frontend as useResumeDraftPdf
participant Router as agent router
participant Cache as resume_draft_cache
participant Worker as run_resume_draft_task
participant LLM as LLM/PDF builder
participant Billing as credit_service
Frontend->>Router: POST /resume-draft/run
Router->>Router: ensure_can_use_model, build_draft_source
Router->>Cache: get_or_create + set status=pending
Router->>Worker: dispatch background task
Router-->>Frontend: 202 TaskAcceptedResponse
loop polling
Frontend->>Router: GET /resume-draft/status
Router->>Cache: read status
Router-->>Frontend: TaskStatusResponse
end
Worker->>Cache: set status=processing
Worker->>LLM: run_resume_draft (generate)
alt LLM/parse failure
Worker->>Billing: charge consumed tokens
Worker->>Cache: raise NonRetryableError (dead_letter)
else success
Worker->>LLM: build_resume_pdf (validate)
alt PDF failure
Worker->>Cache: raise NonRetryableError (no charge)
else PDF valid
Worker->>Billing: record_chat_usage (final charge)
Worker->>Cache: write result, status=completed
end
end
Frontend->>Router: GET /resume-draft/pdf
Router->>Cache: read result (completed)
Router-->>Frontend: streamed PDF
Loading

Possibly related PRs

  • yusuke0610/devforge#289: Introduces the OpenAPI/TypeScript contract foundation including TaskAcceptedResponse used by the new async resume-draft endpoints.
  • yusuke0610/devforge#446: Changes build_resume_pdf to raise RuntimeError on failure, directly affecting the worker's PDF-failure handling path.
  • yusuke0610/devforge#463: Adds the original synchronous resume draft generation service that this PR migrates to the async task flow.

Suggested labels:feature

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ Passedタイトルは変更の主眼である経歴書ドラフトPDF生成の非同期タスク化を簡潔に表している。
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/async-resume-draft

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.

@coderabbitaicoderabbitaiBot 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: 2

🧹 Nitpick comments (3)
backend/tests/test_resume_draft_api.py (1)

85-160: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Missing test: re-enqueue guard when a draft task is already in-progress.

The router's try_reset_to_pending short-circuits and returns the existing cache.status (not "pending") instead of re-dispatching when a task is already pending/processing. This idempotency path prevents duplicate dispatch/re-billing but isn't covered by any test in this file.

♻️ Suggested test addition
deftest_resume_draft_run_returns_existing_status_when_already_pending(client: TestClient) ->None:
"""既に pending/processing 中なら再ディスパッチせず現在のステータスを返す。"""headers=auth_header(client, github_id=1)
_seed_link_data(client._db_session)
user=client._db_session.query(User).filter_by(username="testuser").one()
client._db_session.add(ResumeDraftCache(user_id=user.id, status="processing"))
client._db_session.commit()
res=client.post("/api/agent/resume-draft/run", json={"model": "haiku"}, headers=headers)
assertres.status_code==202assertres.json()["status"] =="processing"
🤖 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/tests/test_resume_draft_api.py` around lines 85 - 160, Add a test for
the idempotent re-enqueue guard in the resume draft flow: when an existing
ResumeDraftCache is already in "pending" or "processing", the request should not
dispatch again and should return the current cache status instead of "pending".
Extend backend/tests/test_resume_draft_api.py with a case around
test_resume_draft_run_enqueues and try_reset_to_pending that seeds link data,
inserts a ResumeDraftCache in processing state, calls
/api/agent/resume-draft/run, and asserts the 202 response echoes "processing"
from the response payload.
web/src/hooks/useResumeDraftPdf.ts (1)

92-105: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Stale previewUrl not cleared when re-generating.

Unlike the prior synchronous flow, generate() no longer resets previewUrl before enqueueing a new task, so an old preview can remain visible/stale until the new one completes. Self-recovers once loadPreview runs, but could confuse users mid-regeneration.

🩹 Suggested fix
 const generate = async () => {
if (generating) return;
setGenerating(true);
setError(null);
+ updatePreviewUrl(null);
try {
await startResumeDraft(model);
startPolling();
🤖 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 `@web/src/hooks/useResumeDraftPdf.ts` around lines 92 - 105, The generate flow
in useResumeDraftPdf should clear any existing previewUrl before starting a new
draft generation, since the async enqueue/polling path can leave a stale preview
visible until loadPreview updates it. Update generate() to reset the preview
state alongside setGenerating/setError before calling startResumeDraft(model),
so the UI doesn’t show an outdated preview during regeneration.
web/src/api/agent.ts (1)

25-53: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

ADR-0020 に更新してください
この非同期ドラフトフローの JSDoc は ADR-0018 ではなく ADR-0020 を参照するように揃えてください。関連するソースコメントも同じ番号に合わせると、生成される API ドキュメントとのずれもなくなります。

🤖 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 `@web/src/api/agent.ts` around lines 25 - 53, The JSDoc references in the
resume draft async flow are still pointing to ADR-0018, so update the comments
on startResumeDraft, getResumeDraftStatus, and fetchResumeDraftPdfBlobUrl to
reference ADR-0020 instead. Keep the surrounding descriptions the same, and make
sure any related source comments for this flow use the same ADR number so the
generated API docs stay consistent.
🤖 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 @.claude/rules/backend/agent.md:
- Around line 54-74: `run_resume_draft_task()` の課金と `resume_draft_cache`
への結果保存が別トランザクションになっており、さらに rerun 時に `dead_letter` を `pending`
に戻すため、`record_chat_usage()`
後に失敗すると再実行で二重課金される可能性があります。`run_resume_draft_task()`, `record_chat_usage()`,
`resume_draft_cache`, `dead_letter`
の再投入経路を見直し、課金済みマーカーを永続化するか、課金記録と結果保存を同一の原子操作にまとめてから、この billing flow
を安全とみなすよう修正してください。
In `@backend/app/services/agent/resume_draft/run_task.py`:
- Around line 105-126: The post-billing write-back in run_task() is still
retryable, so failures after record_chat_usage() can rerun the task and risk
double-charging or inconsistent cache state. Update the second session_factory
block that loads ResumeDraftCacheRepository and writes result/status so any
exception there is wrapped as NonRetryableError, matching the billing failure
handling, and keep the cache update path non-retryable after the charge has been
committed.
---
Nitpick comments:
In `@backend/tests/test_resume_draft_api.py`:
- Around line 85-160: Add a test for the idempotent re-enqueue guard in the
resume draft flow: when an existing ResumeDraftCache is already in "pending" or
"processing", the request should not dispatch again and should return the
current cache status instead of "pending". Extend
backend/tests/test_resume_draft_api.py with a case around
test_resume_draft_run_enqueues and try_reset_to_pending that seeds link data,
inserts a ResumeDraftCache in processing state, calls
/api/agent/resume-draft/run, and asserts the 202 response echoes "processing"
from the response payload.
In `@web/src/api/agent.ts`:
- Around line 25-53: The JSDoc references in the resume draft async flow are
still pointing to ADR-0018, so update the comments on startResumeDraft,
getResumeDraftStatus, and fetchResumeDraftPdfBlobUrl to reference ADR-0020
instead. Keep the surrounding descriptions the same, and make sure any related
source comments for this flow use the same ADR number so the generated API docs
stay consistent.
In `@web/src/hooks/useResumeDraftPdf.ts`:
- Around line 92-105: The generate flow in useResumeDraftPdf should clear any
existing previewUrl before starting a new draft generation, since the async
enqueue/polling path can leave a stale preview visible until loadPreview updates
it. Update generate() to reset the preview state alongside
setGenerating/setError before calling startResumeDraft(model), so the UI doesn’t
show an outdated preview during regeneration.
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b1c56f8e-c984-477b-9471-66dabdae96ce

📥 Commits

Reviewing files that changed from the base of the PR and between f3258b7 and 301143a.

📒 Files selected for processing (22)
  • .claude/rules/backend/agent.md
  • backend/alembic_migrations/versions/0047_add_resume_draft_cache_table.py
  • backend/app/messages.json
  • backend/app/models/__init__.py
  • backend/app/models/cache.py
  • backend/app/repositories/resume_draft.py
  • backend/app/routers/agent.py
  • backend/app/services/agent/resume_draft/run_task.py
  • backend/app/services/tasks/base.py
  • backend/app/services/tasks/handlers/__init__.py
  • backend/app/services/tasks/handlers/resume_draft.py
  • backend/tests/test_resume_draft_api.py
  • backend/tests/test_worker/test_resume_draft.py
  • docs/adr/0020-async-resume-draft-generation.md
  • docs/adr/README.md
  • web/e2e/github-link.spec.ts
  • web/src/api/agent.ts
  • web/src/api/generated.ts
  • web/src/api/paths.ts
  • web/src/constants/messages.ts
  • web/src/hooks/useResumeDraftPdf.test.ts
  • web/src/hooks/useResumeDraftPdf.ts

Comment thread.claude/rules/backend/agent.md
Comment threadbackend/app/services/agent/resume_draft/run_task.py Outdated
課金(record_chat_usage)と結果保存を同一トランザクションで確定し、
「課金済みだが結果未保存」からの再実行による二重課金を構造的に防ぐ。
加えてフェーズA に completed 冪等ガードを追加し、原子 commit 後・ack 前の
クラッシュによる Cloud Tasks 再配信での再課金を防ぐ。回帰テストを追加。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVqj6qCo31rXwKASxB5V2Z
@yusuke0610
yusuke0610 merged commit 114f801 into mainJul 6, 2026
20 checks passed
@yusuke0610
yusuke0610 deleted the feat/async-resume-draft branch July 20, 2026 12:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agentDevForge AgentbackendバックエンドdocumentationImprovements or additions to documentationtestテスト追加・修正webフロントエンド (web)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@yusuke0610@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

経歴書ドラフトPDF生成の非同期タスク化(ADR-0020) - #469

Merged
yusuke0610 merged 2 commits into
mainfrom
feat/async-resume-draft
Jul 6, 2026
Merged

経歴書ドラフトPDF生成の非同期タスク化(ADR-0020)#469
yusuke0610 merged 2 commits into
mainfrom
feat/async-resume-draft

Conversation

@yusuke0610

@yusuke0610yusuke0610 commented Jul 5, 2026

Copy link
Copy Markdown
Owner

概要

GitHub 連携後の経歴書ドラフト PDF 生成(ADR-0018)を、連携とは別トリガーの独立した非同期タスクに改修する。従来は同期エンドポイントで LLM→PDF 生成→レスポンスで PDF をそのまま返しており、生成中はブラウザが待たされ、タブを閉じると生成が無駄になっていた。

連携ボタンとドラフト生成ボタンを分離し、ドラフト生成はバックグラウンドで完了・通知される(既存の非同期タスク基盤・ポーリングを再利用)。モデルはユーザーが選択中のものを使う。

主な変更

バックエンド

  • 新タスク種別 TaskType.RESUME_DRAFT + ハンドラ + run_task.py(連携データ取得→LLM→PDF レンダリング検証→課金→保存)
  • 新テーブル resume_draft_cache(マイグレーション 0047)に payload(JSON) のみ最小永続化resumes/Resume には一切触れない。ダウンロード時に build_resume_pdf で再レンダリング(DB にバイナリを持たない)
  • エンドポイント: 旧同期 POST /api/agent/resume-draft/pdf を廃止し、POST .../run(202) / GET .../status / GET .../pdf に置換
  • 課金をルーターからタスク側へ移設し、不変条件を維持: PDF レンダリング成功後のみ課金/LLM・パース失敗時は消費済みトークンを課金/PDF 失敗は課金なし/課金記録失敗は NonRetryableError で dead_letter 化(リトライ再課金を防止)

フロント

  • useResumeDraftPdf を enqueue→useTaskPolling→ダウンロードへ改修(返り値の形は不変のためダッシュボードは変更なし)。マウント時に進行中タスクを検知したらポーリング復帰。Blob URL リーク防止は維持

ドキュメント

  • ADR-0020 を追加し、ADR-0018 の「何も永続化しない」を最小永続化へ refine。索引・agent.md を更新

テスト

  • make ci green(backend lint〈lint-tdd・lint-adr-index 含む〉+ test 、web vitest 372 件、build)
  • backend: test_worker/test_resume_draft.py(成功=completed+課金/パース失敗=課金あり/PDF 失敗=課金なし/未整備=NonRetryable)、test_resume_draft_api.py を非同期契約へ全面改修(enqueue 202 / status / download / 402 / 409 / 422)
  • front: useResumeDraftPdf.test.ts を非同期フローへ改修、E2E に「連携→ドラフト生成→プレビュー」1 本追加

備考

  • 破壊的変更: 旧同期エンドポイント POST /api/agent/resume-draft/pdf を削除(GET 版に置換)。web/src/api/generated.ts を再生成・同梱
  • マイグレーション: 新テーブル追加(ADD TABLE のみ)。実 libSQL 適用は CI smoke-backend で検証
  • ADR は当初 0019 を採番していたが、main の TDD ADR-0019 と衝突するため 0020 に採番。ベース機能(GitHub連携データから経歴書ドラフトPDFを生成する機能を追加 #463)は squash マージ済みのため、本 PR は最新 origin/main 起点で非同期化分のみを対象にしている

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Resume draft generation now runs in the background, with progress/status polling and a separate PDF download step after completion.
    • Added clearer completion/failure notifications for resume draft jobs.
  • Bug Fixes

    • Improved handling for in-progress, failed, and unavailable draft states with clearer user-facing errors.
    • PDF preview/download now works from saved completed results and avoids regenerating on demand.
  • Tests

    • Expanded API, worker, and end-to-end coverage for the new async resume draft flow.

GitHub連携後のドラフトPDF生成を、連携とは別トリガーの独立した非同期
タスクにする。連携ボタンとドラフト生成ボタンを分離し、生成はバック
グラウンドで完了・通知される(既存の非同期タスク基盤・ポーリングを再利用)。
- 新TaskType RESUME_DRAFT / ハンドラ / run_task.py(LLM→PDF検証→課金→保存)
- 新テーブル resume_draft_cache に payload のみ最小永続化(resumes には触れない)
DL時に build_resume_pdf で再レンダリング(バイナリをDBに持たない)
- エンドポイント: 旧同期 POST /resume-draft/pdf を廃止し run(202)/status/pdf(GET) に置換
- 課金をタスク側へ移設(PDF成功後のみ課金/LLM・パース失敗は課金/PDF失敗は課金なし を維持)
- モデルはユーザー選択中のものを使用。フロントは enqueue→ポーリング→DL へ改修
- ADR-0020 追加(0018 の「何も永続化しない」を最小永続化へ refine)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation backend バックエンド web フロントエンド (web) test テスト追加・修正 agent DevForge Agent labels Jul 5, 2026
@coderabbitai

coderabbitaiBot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@yusuke0610, you've reached your PR review limit, so we couldn't start this review.

Next review available in:11 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 609cf2b6-f857-4050-a9b8-21f2bad096c4

📥 Commits

Reviewing files that changed from the base of the PR and between 301143a and a4147e3.

📒 Files selected for processing (3)
  • .claude/rules/backend/agent.md
  • backend/app/services/agent/resume_draft/run_task.py
  • backend/tests/test_worker/test_resume_draft.py
📝 Walkthrough

Walkthrough

This PR migrates resume draft PDF generation from a synchronous, single-request LLM call into an asynchronous task-based flow. It adds a resume_draft_cache table/model, a RESUME_DRAFT task type/handler, an async worker (run_resume_draft_task) with billing/error semantics, restructured router endpoints (run/status/pdf), frontend polling integration, tests, and an ADR documenting the design.

Changes

Async resume draft generation

Layer / File(s)Summary
Cache schema, migration, and model
backend/alembic_migrations/versions/0047_add_resume_draft_cache_table.py, backend/app/models/cache.py, backend/app/models/__init__.py, backend/app/repositories/resume_draft.py
Adds resume_draft_cache table (one row per user) and ResumeDraftCache ORM model with status/retry/timestamp tracking, plus ResumeDraftCacheRepository with get_by_user/get_or_create and concurrency handling.
Async task runner and handler wiring
backend/app/services/agent/resume_draft/run_task.py, backend/app/services/tasks/base.py, backend/app/services/tasks/handlers/resume_draft.py, backend/app/services/tasks/handlers/__init__.py
Adds run_resume_draft_task implementing LLM generation, PDF validation, and billing with NonRetryableError semantics; registers TaskType.RESUME_DRAFT and ResumeDraftHandler.
Router endpoints for run/status/pdf
backend/app/routers/agent.py, backend/app/messages.json
Replaces synchronous PDF endpoint with POST /run (202 enqueue), GET /status (polling), and GET /pdf (download from completed cache); adds new error/notification messages.
Backend integration and worker tests
backend/tests/test_resume_draft_api.py, backend/tests/test_worker/test_resume_draft.py
Rewrites API tests for the new async flow and adds worker unit tests for success, parse failure charging, PDF failure, and missing cache/source scenarios.
Frontend API client and paths
web/src/api/agent.ts, web/src/api/generated.ts, web/src/api/paths.ts
Adds startResumeDraft, getResumeDraftStatus, fetchResumeDraftPdfBlobUrl functions and updates generated OpenAPI types/paths for the new endpoints.
useResumeDraftPdf polling hook and UI
web/src/hooks/useResumeDraftPdf.ts, web/src/hooks/useResumeDraftPdf.test.ts, web/e2e/github-link.spec.ts, web/src/constants/messages.ts
Rewrites the hook to enqueue, poll, and fetch the completed PDF; updates its tests, adds e2e coverage, and updates hint/notice messages.
Design docs and ADR
docs/adr/0020-async-resume-draft-generation.md, docs/adr/README.md, .claude/rules/backend/agent.md
Adds ADR-0020 documenting the async migration and billing/error contracts, and updates the ADR index and backend design rules accordingly.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
participant Frontend as useResumeDraftPdf
participant Router as agent router
participant Cache as resume_draft_cache
participant Worker as run_resume_draft_task
participant LLM as LLM/PDF builder
participant Billing as credit_service
Frontend->>Router: POST /resume-draft/run
Router->>Router: ensure_can_use_model, build_draft_source
Router->>Cache: get_or_create + set status=pending
Router->>Worker: dispatch background task
Router-->>Frontend: 202 TaskAcceptedResponse
loop polling
Frontend->>Router: GET /resume-draft/status
Router->>Cache: read status
Router-->>Frontend: TaskStatusResponse
end
Worker->>Cache: set status=processing
Worker->>LLM: run_resume_draft (generate)
alt LLM/parse failure
Worker->>Billing: charge consumed tokens
Worker->>Cache: raise NonRetryableError (dead_letter)
else success
Worker->>LLM: build_resume_pdf (validate)
alt PDF failure
Worker->>Cache: raise NonRetryableError (no charge)
else PDF valid
Worker->>Billing: record_chat_usage (final charge)
Worker->>Cache: write result, status=completed
end
end
Frontend->>Router: GET /resume-draft/pdf
Router->>Cache: read result (completed)
Router-->>Frontend: streamed PDF
Loading

Possibly related PRs

  • yusuke0610/devforge#289: Introduces the OpenAPI/TypeScript contract foundation including TaskAcceptedResponse used by the new async resume-draft endpoints.
  • yusuke0610/devforge#446: Changes build_resume_pdf to raise RuntimeError on failure, directly affecting the worker's PDF-failure handling path.
  • yusuke0610/devforge#463: Adds the original synchronous resume draft generation service that this PR migrates to the async task flow.

Suggested labels:feature

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ Passedタイトルは変更の主眼である経歴書ドラフトPDF生成の非同期タスク化を簡潔に表している。
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/async-resume-draft

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.

@coderabbitaicoderabbitaiBot 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: 2

🧹 Nitpick comments (3)
backend/tests/test_resume_draft_api.py (1)

85-160: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Missing test: re-enqueue guard when a draft task is already in-progress.

The router's try_reset_to_pending short-circuits and returns the existing cache.status (not "pending") instead of re-dispatching when a task is already pending/processing. This idempotency path prevents duplicate dispatch/re-billing but isn't covered by any test in this file.

♻️ Suggested test addition
deftest_resume_draft_run_returns_existing_status_when_already_pending(client: TestClient) ->None:
"""既に pending/processing 中なら再ディスパッチせず現在のステータスを返す。"""headers=auth_header(client, github_id=1)
_seed_link_data(client._db_session)
user=client._db_session.query(User).filter_by(username="testuser").one()
client._db_session.add(ResumeDraftCache(user_id=user.id, status="processing"))
client._db_session.commit()
res=client.post("/api/agent/resume-draft/run", json={"model": "haiku"}, headers=headers)
assertres.status_code==202assertres.json()["status"] =="processing"
🤖 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/tests/test_resume_draft_api.py` around lines 85 - 160, Add a test for
the idempotent re-enqueue guard in the resume draft flow: when an existing
ResumeDraftCache is already in "pending" or "processing", the request should not
dispatch again and should return the current cache status instead of "pending".
Extend backend/tests/test_resume_draft_api.py with a case around
test_resume_draft_run_enqueues and try_reset_to_pending that seeds link data,
inserts a ResumeDraftCache in processing state, calls
/api/agent/resume-draft/run, and asserts the 202 response echoes "processing"
from the response payload.
web/src/hooks/useResumeDraftPdf.ts (1)

92-105: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Stale previewUrl not cleared when re-generating.

Unlike the prior synchronous flow, generate() no longer resets previewUrl before enqueueing a new task, so an old preview can remain visible/stale until the new one completes. Self-recovers once loadPreview runs, but could confuse users mid-regeneration.

🩹 Suggested fix
 const generate = async () => {
if (generating) return;
setGenerating(true);
setError(null);
+ updatePreviewUrl(null);
try {
await startResumeDraft(model);
startPolling();
🤖 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 `@web/src/hooks/useResumeDraftPdf.ts` around lines 92 - 105, The generate flow
in useResumeDraftPdf should clear any existing previewUrl before starting a new
draft generation, since the async enqueue/polling path can leave a stale preview
visible until loadPreview updates it. Update generate() to reset the preview
state alongside setGenerating/setError before calling startResumeDraft(model),
so the UI doesn’t show an outdated preview during regeneration.
web/src/api/agent.ts (1)

25-53: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

ADR-0020 に更新してください
この非同期ドラフトフローの JSDoc は ADR-0018 ではなく ADR-0020 を参照するように揃えてください。関連するソースコメントも同じ番号に合わせると、生成される API ドキュメントとのずれもなくなります。

🤖 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 `@web/src/api/agent.ts` around lines 25 - 53, The JSDoc references in the
resume draft async flow are still pointing to ADR-0018, so update the comments
on startResumeDraft, getResumeDraftStatus, and fetchResumeDraftPdfBlobUrl to
reference ADR-0020 instead. Keep the surrounding descriptions the same, and make
sure any related source comments for this flow use the same ADR number so the
generated API docs stay consistent.
🤖 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 @.claude/rules/backend/agent.md:
- Around line 54-74: `run_resume_draft_task()` の課金と `resume_draft_cache`
への結果保存が別トランザクションになっており、さらに rerun 時に `dead_letter` を `pending`
に戻すため、`record_chat_usage()`
後に失敗すると再実行で二重課金される可能性があります。`run_resume_draft_task()`, `record_chat_usage()`,
`resume_draft_cache`, `dead_letter`
の再投入経路を見直し、課金済みマーカーを永続化するか、課金記録と結果保存を同一の原子操作にまとめてから、この billing flow
を安全とみなすよう修正してください。
In `@backend/app/services/agent/resume_draft/run_task.py`:
- Around line 105-126: The post-billing write-back in run_task() is still
retryable, so failures after record_chat_usage() can rerun the task and risk
double-charging or inconsistent cache state. Update the second session_factory
block that loads ResumeDraftCacheRepository and writes result/status so any
exception there is wrapped as NonRetryableError, matching the billing failure
handling, and keep the cache update path non-retryable after the charge has been
committed.
---
Nitpick comments:
In `@backend/tests/test_resume_draft_api.py`:
- Around line 85-160: Add a test for the idempotent re-enqueue guard in the
resume draft flow: when an existing ResumeDraftCache is already in "pending" or
"processing", the request should not dispatch again and should return the
current cache status instead of "pending". Extend
backend/tests/test_resume_draft_api.py with a case around
test_resume_draft_run_enqueues and try_reset_to_pending that seeds link data,
inserts a ResumeDraftCache in processing state, calls
/api/agent/resume-draft/run, and asserts the 202 response echoes "processing"
from the response payload.
In `@web/src/api/agent.ts`:
- Around line 25-53: The JSDoc references in the resume draft async flow are
still pointing to ADR-0018, so update the comments on startResumeDraft,
getResumeDraftStatus, and fetchResumeDraftPdfBlobUrl to reference ADR-0020
instead. Keep the surrounding descriptions the same, and make sure any related
source comments for this flow use the same ADR number so the generated API docs
stay consistent.
In `@web/src/hooks/useResumeDraftPdf.ts`:
- Around line 92-105: The generate flow in useResumeDraftPdf should clear any
existing previewUrl before starting a new draft generation, since the async
enqueue/polling path can leave a stale preview visible until loadPreview updates
it. Update generate() to reset the preview state alongside
setGenerating/setError before calling startResumeDraft(model), so the UI doesn’t
show an outdated preview during regeneration.
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b1c56f8e-c984-477b-9471-66dabdae96ce

📥 Commits

Reviewing files that changed from the base of the PR and between f3258b7 and 301143a.

📒 Files selected for processing (22)
  • .claude/rules/backend/agent.md
  • backend/alembic_migrations/versions/0047_add_resume_draft_cache_table.py
  • backend/app/messages.json
  • backend/app/models/__init__.py
  • backend/app/models/cache.py
  • backend/app/repositories/resume_draft.py
  • backend/app/routers/agent.py
  • backend/app/services/agent/resume_draft/run_task.py
  • backend/app/services/tasks/base.py
  • backend/app/services/tasks/handlers/__init__.py
  • backend/app/services/tasks/handlers/resume_draft.py
  • backend/tests/test_resume_draft_api.py
  • backend/tests/test_worker/test_resume_draft.py
  • docs/adr/0020-async-resume-draft-generation.md
  • docs/adr/README.md
  • web/e2e/github-link.spec.ts
  • web/src/api/agent.ts
  • web/src/api/generated.ts
  • web/src/api/paths.ts
  • web/src/constants/messages.ts
  • web/src/hooks/useResumeDraftPdf.test.ts
  • web/src/hooks/useResumeDraftPdf.ts

Comment thread.claude/rules/backend/agent.md
Comment threadbackend/app/services/agent/resume_draft/run_task.py Outdated
課金(record_chat_usage)と結果保存を同一トランザクションで確定し、
「課金済みだが結果未保存」からの再実行による二重課金を構造的に防ぐ。
加えてフェーズA に completed 冪等ガードを追加し、原子 commit 後・ack 前の
クラッシュによる Cloud Tasks 再配信での再課金を防ぐ。回帰テストを追加。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVqj6qCo31rXwKASxB5V2Z
@yusuke0610
yusuke0610 merged commit 114f801 into mainJul 6, 2026
20 checks passed
@yusuke0610
yusuke0610 deleted the feat/async-resume-draft branch July 20, 2026 12:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agentDevForge AgentbackendバックエンドdocumentationImprovements or additions to documentationtestテスト追加・修正webフロントエンド (web)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@yusuke0610@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

経歴書ドラフトPDF生成の非同期タスク化(ADR-0020) - #469

Merged
yusuke0610 merged 2 commits into
mainfrom
feat/async-resume-draft
Jul 6, 2026
Merged

経歴書ドラフトPDF生成の非同期タスク化(ADR-0020)#469
yusuke0610 merged 2 commits into
mainfrom
feat/async-resume-draft

Conversation

@yusuke0610

@yusuke0610yusuke0610 commented Jul 5, 2026

Copy link
Copy Markdown
Owner

概要

GitHub 連携後の経歴書ドラフト PDF 生成(ADR-0018)を、連携とは別トリガーの独立した非同期タスクに改修する。従来は同期エンドポイントで LLM→PDF 生成→レスポンスで PDF をそのまま返しており、生成中はブラウザが待たされ、タブを閉じると生成が無駄になっていた。

連携ボタンとドラフト生成ボタンを分離し、ドラフト生成はバックグラウンドで完了・通知される(既存の非同期タスク基盤・ポーリングを再利用)。モデルはユーザーが選択中のものを使う。

主な変更

バックエンド

  • 新タスク種別 TaskType.RESUME_DRAFT + ハンドラ + run_task.py(連携データ取得→LLM→PDF レンダリング検証→課金→保存)
  • 新テーブル resume_draft_cache(マイグレーション 0047)に payload(JSON) のみ最小永続化resumes/Resume には一切触れない。ダウンロード時に build_resume_pdf で再レンダリング(DB にバイナリを持たない)
  • エンドポイント: 旧同期 POST /api/agent/resume-draft/pdf を廃止し、POST .../run(202) / GET .../status / GET .../pdf に置換
  • 課金をルーターからタスク側へ移設し、不変条件を維持: PDF レンダリング成功後のみ課金/LLM・パース失敗時は消費済みトークンを課金/PDF 失敗は課金なし/課金記録失敗は NonRetryableError で dead_letter 化(リトライ再課金を防止)

フロント

  • useResumeDraftPdf を enqueue→useTaskPolling→ダウンロードへ改修(返り値の形は不変のためダッシュボードは変更なし)。マウント時に進行中タスクを検知したらポーリング復帰。Blob URL リーク防止は維持

ドキュメント

  • ADR-0020 を追加し、ADR-0018 の「何も永続化しない」を最小永続化へ refine。索引・agent.md を更新

テスト

  • make ci green(backend lint〈lint-tdd・lint-adr-index 含む〉+ test 、web vitest 372 件、build)
  • backend: test_worker/test_resume_draft.py(成功=completed+課金/パース失敗=課金あり/PDF 失敗=課金なし/未整備=NonRetryable)、test_resume_draft_api.py を非同期契約へ全面改修(enqueue 202 / status / download / 402 / 409 / 422)
  • front: useResumeDraftPdf.test.ts を非同期フローへ改修、E2E に「連携→ドラフト生成→プレビュー」1 本追加

備考

  • 破壊的変更: 旧同期エンドポイント POST /api/agent/resume-draft/pdf を削除(GET 版に置換)。web/src/api/generated.ts を再生成・同梱
  • マイグレーション: 新テーブル追加(ADD TABLE のみ)。実 libSQL 適用は CI smoke-backend で検証
  • ADR は当初 0019 を採番していたが、main の TDD ADR-0019 と衝突するため 0020 に採番。ベース機能(GitHub連携データから経歴書ドラフトPDFを生成する機能を追加 #463)は squash マージ済みのため、本 PR は最新 origin/main 起点で非同期化分のみを対象にしている

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Resume draft generation now runs in the background, with progress/status polling and a separate PDF download step after completion.
    • Added clearer completion/failure notifications for resume draft jobs.
  • Bug Fixes

    • Improved handling for in-progress, failed, and unavailable draft states with clearer user-facing errors.
    • PDF preview/download now works from saved completed results and avoids regenerating on demand.
  • Tests

    • Expanded API, worker, and end-to-end coverage for the new async resume draft flow.

GitHub連携後のドラフトPDF生成を、連携とは別トリガーの独立した非同期
タスクにする。連携ボタンとドラフト生成ボタンを分離し、生成はバック
グラウンドで完了・通知される(既存の非同期タスク基盤・ポーリングを再利用)。
- 新TaskType RESUME_DRAFT / ハンドラ / run_task.py(LLM→PDF検証→課金→保存)
- 新テーブル resume_draft_cache に payload のみ最小永続化(resumes には触れない)
DL時に build_resume_pdf で再レンダリング(バイナリをDBに持たない)
- エンドポイント: 旧同期 POST /resume-draft/pdf を廃止し run(202)/status/pdf(GET) に置換
- 課金をタスク側へ移設(PDF成功後のみ課金/LLM・パース失敗は課金/PDF失敗は課金なし を維持)
- モデルはユーザー選択中のものを使用。フロントは enqueue→ポーリング→DL へ改修
- ADR-0020 追加(0018 の「何も永続化しない」を最小永続化へ refine)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation backend バックエンド web フロントエンド (web) test テスト追加・修正 agent DevForge Agent labels Jul 5, 2026
@coderabbitai

coderabbitaiBot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@yusuke0610, you've reached your PR review limit, so we couldn't start this review.

Next review available in:11 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 609cf2b6-f857-4050-a9b8-21f2bad096c4

📥 Commits

Reviewing files that changed from the base of the PR and between 301143a and a4147e3.

📒 Files selected for processing (3)
  • .claude/rules/backend/agent.md
  • backend/app/services/agent/resume_draft/run_task.py
  • backend/tests/test_worker/test_resume_draft.py
📝 Walkthrough

Walkthrough

This PR migrates resume draft PDF generation from a synchronous, single-request LLM call into an asynchronous task-based flow. It adds a resume_draft_cache table/model, a RESUME_DRAFT task type/handler, an async worker (run_resume_draft_task) with billing/error semantics, restructured router endpoints (run/status/pdf), frontend polling integration, tests, and an ADR documenting the design.

Changes

Async resume draft generation

Layer / File(s)Summary
Cache schema, migration, and model
backend/alembic_migrations/versions/0047_add_resume_draft_cache_table.py, backend/app/models/cache.py, backend/app/models/__init__.py, backend/app/repositories/resume_draft.py
Adds resume_draft_cache table (one row per user) and ResumeDraftCache ORM model with status/retry/timestamp tracking, plus ResumeDraftCacheRepository with get_by_user/get_or_create and concurrency handling.
Async task runner and handler wiring
backend/app/services/agent/resume_draft/run_task.py, backend/app/services/tasks/base.py, backend/app/services/tasks/handlers/resume_draft.py, backend/app/services/tasks/handlers/__init__.py
Adds run_resume_draft_task implementing LLM generation, PDF validation, and billing with NonRetryableError semantics; registers TaskType.RESUME_DRAFT and ResumeDraftHandler.
Router endpoints for run/status/pdf
backend/app/routers/agent.py, backend/app/messages.json
Replaces synchronous PDF endpoint with POST /run (202 enqueue), GET /status (polling), and GET /pdf (download from completed cache); adds new error/notification messages.
Backend integration and worker tests
backend/tests/test_resume_draft_api.py, backend/tests/test_worker/test_resume_draft.py
Rewrites API tests for the new async flow and adds worker unit tests for success, parse failure charging, PDF failure, and missing cache/source scenarios.
Frontend API client and paths
web/src/api/agent.ts, web/src/api/generated.ts, web/src/api/paths.ts
Adds startResumeDraft, getResumeDraftStatus, fetchResumeDraftPdfBlobUrl functions and updates generated OpenAPI types/paths for the new endpoints.
useResumeDraftPdf polling hook and UI
web/src/hooks/useResumeDraftPdf.ts, web/src/hooks/useResumeDraftPdf.test.ts, web/e2e/github-link.spec.ts, web/src/constants/messages.ts
Rewrites the hook to enqueue, poll, and fetch the completed PDF; updates its tests, adds e2e coverage, and updates hint/notice messages.
Design docs and ADR
docs/adr/0020-async-resume-draft-generation.md, docs/adr/README.md, .claude/rules/backend/agent.md
Adds ADR-0020 documenting the async migration and billing/error contracts, and updates the ADR index and backend design rules accordingly.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
participant Frontend as useResumeDraftPdf
participant Router as agent router
participant Cache as resume_draft_cache
participant Worker as run_resume_draft_task
participant LLM as LLM/PDF builder
participant Billing as credit_service
Frontend->>Router: POST /resume-draft/run
Router->>Router: ensure_can_use_model, build_draft_source
Router->>Cache: get_or_create + set status=pending
Router->>Worker: dispatch background task
Router-->>Frontend: 202 TaskAcceptedResponse
loop polling
Frontend->>Router: GET /resume-draft/status
Router->>Cache: read status
Router-->>Frontend: TaskStatusResponse
end
Worker->>Cache: set status=processing
Worker->>LLM: run_resume_draft (generate)
alt LLM/parse failure
Worker->>Billing: charge consumed tokens
Worker->>Cache: raise NonRetryableError (dead_letter)
else success
Worker->>LLM: build_resume_pdf (validate)
alt PDF failure
Worker->>Cache: raise NonRetryableError (no charge)
else PDF valid
Worker->>Billing: record_chat_usage (final charge)
Worker->>Cache: write result, status=completed
end
end
Frontend->>Router: GET /resume-draft/pdf
Router->>Cache: read result (completed)
Router-->>Frontend: streamed PDF
Loading

Possibly related PRs

  • yusuke0610/devforge#289: Introduces the OpenAPI/TypeScript contract foundation including TaskAcceptedResponse used by the new async resume-draft endpoints.
  • yusuke0610/devforge#446: Changes build_resume_pdf to raise RuntimeError on failure, directly affecting the worker's PDF-failure handling path.
  • yusuke0610/devforge#463: Adds the original synchronous resume draft generation service that this PR migrates to the async task flow.

Suggested labels:feature

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ Passedタイトルは変更の主眼である経歴書ドラフトPDF生成の非同期タスク化を簡潔に表している。
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/async-resume-draft

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.

@coderabbitaicoderabbitaiBot 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: 2

🧹 Nitpick comments (3)
backend/tests/test_resume_draft_api.py (1)

85-160: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Missing test: re-enqueue guard when a draft task is already in-progress.

The router's try_reset_to_pending short-circuits and returns the existing cache.status (not "pending") instead of re-dispatching when a task is already pending/processing. This idempotency path prevents duplicate dispatch/re-billing but isn't covered by any test in this file.

♻️ Suggested test addition
deftest_resume_draft_run_returns_existing_status_when_already_pending(client: TestClient) ->None:
"""既に pending/processing 中なら再ディスパッチせず現在のステータスを返す。"""headers=auth_header(client, github_id=1)
_seed_link_data(client._db_session)
user=client._db_session.query(User).filter_by(username="testuser").one()
client._db_session.add(ResumeDraftCache(user_id=user.id, status="processing"))
client._db_session.commit()
res=client.post("/api/agent/resume-draft/run", json={"model": "haiku"}, headers=headers)
assertres.status_code==202assertres.json()["status"] =="processing"
🤖 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/tests/test_resume_draft_api.py` around lines 85 - 160, Add a test for
the idempotent re-enqueue guard in the resume draft flow: when an existing
ResumeDraftCache is already in "pending" or "processing", the request should not
dispatch again and should return the current cache status instead of "pending".
Extend backend/tests/test_resume_draft_api.py with a case around
test_resume_draft_run_enqueues and try_reset_to_pending that seeds link data,
inserts a ResumeDraftCache in processing state, calls
/api/agent/resume-draft/run, and asserts the 202 response echoes "processing"
from the response payload.
web/src/hooks/useResumeDraftPdf.ts (1)

92-105: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Stale previewUrl not cleared when re-generating.

Unlike the prior synchronous flow, generate() no longer resets previewUrl before enqueueing a new task, so an old preview can remain visible/stale until the new one completes. Self-recovers once loadPreview runs, but could confuse users mid-regeneration.

🩹 Suggested fix
 const generate = async () => {
if (generating) return;
setGenerating(true);
setError(null);
+ updatePreviewUrl(null);
try {
await startResumeDraft(model);
startPolling();
🤖 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 `@web/src/hooks/useResumeDraftPdf.ts` around lines 92 - 105, The generate flow
in useResumeDraftPdf should clear any existing previewUrl before starting a new
draft generation, since the async enqueue/polling path can leave a stale preview
visible until loadPreview updates it. Update generate() to reset the preview
state alongside setGenerating/setError before calling startResumeDraft(model),
so the UI doesn’t show an outdated preview during regeneration.
web/src/api/agent.ts (1)

25-53: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

ADR-0020 に更新してください
この非同期ドラフトフローの JSDoc は ADR-0018 ではなく ADR-0020 を参照するように揃えてください。関連するソースコメントも同じ番号に合わせると、生成される API ドキュメントとのずれもなくなります。

🤖 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 `@web/src/api/agent.ts` around lines 25 - 53, The JSDoc references in the
resume draft async flow are still pointing to ADR-0018, so update the comments
on startResumeDraft, getResumeDraftStatus, and fetchResumeDraftPdfBlobUrl to
reference ADR-0020 instead. Keep the surrounding descriptions the same, and make
sure any related source comments for this flow use the same ADR number so the
generated API docs stay consistent.
🤖 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 @.claude/rules/backend/agent.md:
- Around line 54-74: `run_resume_draft_task()` の課金と `resume_draft_cache`
への結果保存が別トランザクションになっており、さらに rerun 時に `dead_letter` を `pending`
に戻すため、`record_chat_usage()`
後に失敗すると再実行で二重課金される可能性があります。`run_resume_draft_task()`, `record_chat_usage()`,
`resume_draft_cache`, `dead_letter`
の再投入経路を見直し、課金済みマーカーを永続化するか、課金記録と結果保存を同一の原子操作にまとめてから、この billing flow
を安全とみなすよう修正してください。
In `@backend/app/services/agent/resume_draft/run_task.py`:
- Around line 105-126: The post-billing write-back in run_task() is still
retryable, so failures after record_chat_usage() can rerun the task and risk
double-charging or inconsistent cache state. Update the second session_factory
block that loads ResumeDraftCacheRepository and writes result/status so any
exception there is wrapped as NonRetryableError, matching the billing failure
handling, and keep the cache update path non-retryable after the charge has been
committed.
---
Nitpick comments:
In `@backend/tests/test_resume_draft_api.py`:
- Around line 85-160: Add a test for the idempotent re-enqueue guard in the
resume draft flow: when an existing ResumeDraftCache is already in "pending" or
"processing", the request should not dispatch again and should return the
current cache status instead of "pending". Extend
backend/tests/test_resume_draft_api.py with a case around
test_resume_draft_run_enqueues and try_reset_to_pending that seeds link data,
inserts a ResumeDraftCache in processing state, calls
/api/agent/resume-draft/run, and asserts the 202 response echoes "processing"
from the response payload.
In `@web/src/api/agent.ts`:
- Around line 25-53: The JSDoc references in the resume draft async flow are
still pointing to ADR-0018, so update the comments on startResumeDraft,
getResumeDraftStatus, and fetchResumeDraftPdfBlobUrl to reference ADR-0020
instead. Keep the surrounding descriptions the same, and make sure any related
source comments for this flow use the same ADR number so the generated API docs
stay consistent.
In `@web/src/hooks/useResumeDraftPdf.ts`:
- Around line 92-105: The generate flow in useResumeDraftPdf should clear any
existing previewUrl before starting a new draft generation, since the async
enqueue/polling path can leave a stale preview visible until loadPreview updates
it. Update generate() to reset the preview state alongside
setGenerating/setError before calling startResumeDraft(model), so the UI doesn’t
show an outdated preview during regeneration.
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b1c56f8e-c984-477b-9471-66dabdae96ce

📥 Commits

Reviewing files that changed from the base of the PR and between f3258b7 and 301143a.

📒 Files selected for processing (22)
  • .claude/rules/backend/agent.md
  • backend/alembic_migrations/versions/0047_add_resume_draft_cache_table.py
  • backend/app/messages.json
  • backend/app/models/__init__.py
  • backend/app/models/cache.py
  • backend/app/repositories/resume_draft.py
  • backend/app/routers/agent.py
  • backend/app/services/agent/resume_draft/run_task.py
  • backend/app/services/tasks/base.py
  • backend/app/services/tasks/handlers/__init__.py
  • backend/app/services/tasks/handlers/resume_draft.py
  • backend/tests/test_resume_draft_api.py
  • backend/tests/test_worker/test_resume_draft.py
  • docs/adr/0020-async-resume-draft-generation.md
  • docs/adr/README.md
  • web/e2e/github-link.spec.ts
  • web/src/api/agent.ts
  • web/src/api/generated.ts
  • web/src/api/paths.ts
  • web/src/constants/messages.ts
  • web/src/hooks/useResumeDraftPdf.test.ts
  • web/src/hooks/useResumeDraftPdf.ts

Comment thread.claude/rules/backend/agent.md
Comment threadbackend/app/services/agent/resume_draft/run_task.py Outdated
課金(record_chat_usage)と結果保存を同一トランザクションで確定し、
「課金済みだが結果未保存」からの再実行による二重課金を構造的に防ぐ。
加えてフェーズA に completed 冪等ガードを追加し、原子 commit 後・ack 前の
クラッシュによる Cloud Tasks 再配信での再課金を防ぐ。回帰テストを追加。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVqj6qCo31rXwKASxB5V2Z
@yusuke0610
yusuke0610 merged commit 114f801 into mainJul 6, 2026
20 checks passed
@yusuke0610
yusuke0610 deleted the feat/async-resume-draft branch July 20, 2026 12:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agentDevForge AgentbackendバックエンドdocumentationImprovements or additions to documentationtestテスト追加・修正webフロントエンド (web)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@yusuke0610@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

経歴書ドラフトPDF生成の非同期タスク化(ADR-0020) - #469

Merged
yusuke0610 merged 2 commits into
mainfrom
feat/async-resume-draft
Jul 6, 2026
Merged

経歴書ドラフトPDF生成の非同期タスク化(ADR-0020)#469
yusuke0610 merged 2 commits into
mainfrom
feat/async-resume-draft

Conversation

@yusuke0610

@yusuke0610yusuke0610 commented Jul 5, 2026

Copy link
Copy Markdown
Owner

概要

GitHub 連携後の経歴書ドラフト PDF 生成(ADR-0018)を、連携とは別トリガーの独立した非同期タスクに改修する。従来は同期エンドポイントで LLM→PDF 生成→レスポンスで PDF をそのまま返しており、生成中はブラウザが待たされ、タブを閉じると生成が無駄になっていた。

連携ボタンとドラフト生成ボタンを分離し、ドラフト生成はバックグラウンドで完了・通知される(既存の非同期タスク基盤・ポーリングを再利用)。モデルはユーザーが選択中のものを使う。

主な変更

バックエンド

  • 新タスク種別 TaskType.RESUME_DRAFT + ハンドラ + run_task.py(連携データ取得→LLM→PDF レンダリング検証→課金→保存)
  • 新テーブル resume_draft_cache(マイグレーション 0047)に payload(JSON) のみ最小永続化resumes/Resume には一切触れない。ダウンロード時に build_resume_pdf で再レンダリング(DB にバイナリを持たない)
  • エンドポイント: 旧同期 POST /api/agent/resume-draft/pdf を廃止し、POST .../run(202) / GET .../status / GET .../pdf に置換
  • 課金をルーターからタスク側へ移設し、不変条件を維持: PDF レンダリング成功後のみ課金/LLM・パース失敗時は消費済みトークンを課金/PDF 失敗は課金なし/課金記録失敗は NonRetryableError で dead_letter 化(リトライ再課金を防止)

フロント

  • useResumeDraftPdf を enqueue→useTaskPolling→ダウンロードへ改修(返り値の形は不変のためダッシュボードは変更なし)。マウント時に進行中タスクを検知したらポーリング復帰。Blob URL リーク防止は維持

ドキュメント

  • ADR-0020 を追加し、ADR-0018 の「何も永続化しない」を最小永続化へ refine。索引・agent.md を更新

テスト

  • make ci green(backend lint〈lint-tdd・lint-adr-index 含む〉+ test 、web vitest 372 件、build)
  • backend: test_worker/test_resume_draft.py(成功=completed+課金/パース失敗=課金あり/PDF 失敗=課金なし/未整備=NonRetryable)、test_resume_draft_api.py を非同期契約へ全面改修(enqueue 202 / status / download / 402 / 409 / 422)
  • front: useResumeDraftPdf.test.ts を非同期フローへ改修、E2E に「連携→ドラフト生成→プレビュー」1 本追加

備考

  • 破壊的変更: 旧同期エンドポイント POST /api/agent/resume-draft/pdf を削除(GET 版に置換)。web/src/api/generated.ts を再生成・同梱
  • マイグレーション: 新テーブル追加(ADD TABLE のみ)。実 libSQL 適用は CI smoke-backend で検証
  • ADR は当初 0019 を採番していたが、main の TDD ADR-0019 と衝突するため 0020 に採番。ベース機能(GitHub連携データから経歴書ドラフトPDFを生成する機能を追加 #463)は squash マージ済みのため、本 PR は最新 origin/main 起点で非同期化分のみを対象にしている

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Resume draft generation now runs in the background, with progress/status polling and a separate PDF download step after completion.
    • Added clearer completion/failure notifications for resume draft jobs.
  • Bug Fixes

    • Improved handling for in-progress, failed, and unavailable draft states with clearer user-facing errors.
    • PDF preview/download now works from saved completed results and avoids regenerating on demand.
  • Tests

    • Expanded API, worker, and end-to-end coverage for the new async resume draft flow.

GitHub連携後のドラフトPDF生成を、連携とは別トリガーの独立した非同期
タスクにする。連携ボタンとドラフト生成ボタンを分離し、生成はバック
グラウンドで完了・通知される(既存の非同期タスク基盤・ポーリングを再利用)。
- 新TaskType RESUME_DRAFT / ハンドラ / run_task.py(LLM→PDF検証→課金→保存)
- 新テーブル resume_draft_cache に payload のみ最小永続化(resumes には触れない)
DL時に build_resume_pdf で再レンダリング(バイナリをDBに持たない)
- エンドポイント: 旧同期 POST /resume-draft/pdf を廃止し run(202)/status/pdf(GET) に置換
- 課金をタスク側へ移設(PDF成功後のみ課金/LLM・パース失敗は課金/PDF失敗は課金なし を維持)
- モデルはユーザー選択中のものを使用。フロントは enqueue→ポーリング→DL へ改修
- ADR-0020 追加(0018 の「何も永続化しない」を最小永続化へ refine)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation backend バックエンド web フロントエンド (web) test テスト追加・修正 agent DevForge Agent labels Jul 5, 2026
@coderabbitai

coderabbitaiBot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@yusuke0610, you've reached your PR review limit, so we couldn't start this review.

Next review available in:11 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 609cf2b6-f857-4050-a9b8-21f2bad096c4

📥 Commits

Reviewing files that changed from the base of the PR and between 301143a and a4147e3.

📒 Files selected for processing (3)
  • .claude/rules/backend/agent.md
  • backend/app/services/agent/resume_draft/run_task.py
  • backend/tests/test_worker/test_resume_draft.py
📝 Walkthrough

Walkthrough

This PR migrates resume draft PDF generation from a synchronous, single-request LLM call into an asynchronous task-based flow. It adds a resume_draft_cache table/model, a RESUME_DRAFT task type/handler, an async worker (run_resume_draft_task) with billing/error semantics, restructured router endpoints (run/status/pdf), frontend polling integration, tests, and an ADR documenting the design.

Changes

Async resume draft generation

Layer / File(s)Summary
Cache schema, migration, and model
backend/alembic_migrations/versions/0047_add_resume_draft_cache_table.py, backend/app/models/cache.py, backend/app/models/__init__.py, backend/app/repositories/resume_draft.py
Adds resume_draft_cache table (one row per user) and ResumeDraftCache ORM model with status/retry/timestamp tracking, plus ResumeDraftCacheRepository with get_by_user/get_or_create and concurrency handling.
Async task runner and handler wiring
backend/app/services/agent/resume_draft/run_task.py, backend/app/services/tasks/base.py, backend/app/services/tasks/handlers/resume_draft.py, backend/app/services/tasks/handlers/__init__.py
Adds run_resume_draft_task implementing LLM generation, PDF validation, and billing with NonRetryableError semantics; registers TaskType.RESUME_DRAFT and ResumeDraftHandler.
Router endpoints for run/status/pdf
backend/app/routers/agent.py, backend/app/messages.json
Replaces synchronous PDF endpoint with POST /run (202 enqueue), GET /status (polling), and GET /pdf (download from completed cache); adds new error/notification messages.
Backend integration and worker tests
backend/tests/test_resume_draft_api.py, backend/tests/test_worker/test_resume_draft.py
Rewrites API tests for the new async flow and adds worker unit tests for success, parse failure charging, PDF failure, and missing cache/source scenarios.
Frontend API client and paths
web/src/api/agent.ts, web/src/api/generated.ts, web/src/api/paths.ts
Adds startResumeDraft, getResumeDraftStatus, fetchResumeDraftPdfBlobUrl functions and updates generated OpenAPI types/paths for the new endpoints.
useResumeDraftPdf polling hook and UI
web/src/hooks/useResumeDraftPdf.ts, web/src/hooks/useResumeDraftPdf.test.ts, web/e2e/github-link.spec.ts, web/src/constants/messages.ts
Rewrites the hook to enqueue, poll, and fetch the completed PDF; updates its tests, adds e2e coverage, and updates hint/notice messages.
Design docs and ADR
docs/adr/0020-async-resume-draft-generation.md, docs/adr/README.md, .claude/rules/backend/agent.md
Adds ADR-0020 documenting the async migration and billing/error contracts, and updates the ADR index and backend design rules accordingly.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
participant Frontend as useResumeDraftPdf
participant Router as agent router
participant Cache as resume_draft_cache
participant Worker as run_resume_draft_task
participant LLM as LLM/PDF builder
participant Billing as credit_service
Frontend->>Router: POST /resume-draft/run
Router->>Router: ensure_can_use_model, build_draft_source
Router->>Cache: get_or_create + set status=pending
Router->>Worker: dispatch background task
Router-->>Frontend: 202 TaskAcceptedResponse
loop polling
Frontend->>Router: GET /resume-draft/status
Router->>Cache: read status
Router-->>Frontend: TaskStatusResponse
end
Worker->>Cache: set status=processing
Worker->>LLM: run_resume_draft (generate)
alt LLM/parse failure
Worker->>Billing: charge consumed tokens
Worker->>Cache: raise NonRetryableError (dead_letter)
else success
Worker->>LLM: build_resume_pdf (validate)
alt PDF failure
Worker->>Cache: raise NonRetryableError (no charge)
else PDF valid
Worker->>Billing: record_chat_usage (final charge)
Worker->>Cache: write result, status=completed
end
end
Frontend->>Router: GET /resume-draft/pdf
Router->>Cache: read result (completed)
Router-->>Frontend: streamed PDF
Loading

Possibly related PRs

  • yusuke0610/devforge#289: Introduces the OpenAPI/TypeScript contract foundation including TaskAcceptedResponse used by the new async resume-draft endpoints.
  • yusuke0610/devforge#446: Changes build_resume_pdf to raise RuntimeError on failure, directly affecting the worker's PDF-failure handling path.
  • yusuke0610/devforge#463: Adds the original synchronous resume draft generation service that this PR migrates to the async task flow.

Suggested labels:feature

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ Passedタイトルは変更の主眼である経歴書ドラフトPDF生成の非同期タスク化を簡潔に表している。
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/async-resume-draft

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.

@coderabbitaicoderabbitaiBot 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: 2

🧹 Nitpick comments (3)
backend/tests/test_resume_draft_api.py (1)

85-160: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Missing test: re-enqueue guard when a draft task is already in-progress.

The router's try_reset_to_pending short-circuits and returns the existing cache.status (not "pending") instead of re-dispatching when a task is already pending/processing. This idempotency path prevents duplicate dispatch/re-billing but isn't covered by any test in this file.

♻️ Suggested test addition
deftest_resume_draft_run_returns_existing_status_when_already_pending(client: TestClient) ->None:
"""既に pending/processing 中なら再ディスパッチせず現在のステータスを返す。"""headers=auth_header(client, github_id=1)
_seed_link_data(client._db_session)
user=client._db_session.query(User).filter_by(username="testuser").one()
client._db_session.add(ResumeDraftCache(user_id=user.id, status="processing"))
client._db_session.commit()
res=client.post("/api/agent/resume-draft/run", json={"model": "haiku"}, headers=headers)
assertres.status_code==202assertres.json()["status"] =="processing"
🤖 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/tests/test_resume_draft_api.py` around lines 85 - 160, Add a test for
the idempotent re-enqueue guard in the resume draft flow: when an existing
ResumeDraftCache is already in "pending" or "processing", the request should not
dispatch again and should return the current cache status instead of "pending".
Extend backend/tests/test_resume_draft_api.py with a case around
test_resume_draft_run_enqueues and try_reset_to_pending that seeds link data,
inserts a ResumeDraftCache in processing state, calls
/api/agent/resume-draft/run, and asserts the 202 response echoes "processing"
from the response payload.
web/src/hooks/useResumeDraftPdf.ts (1)

92-105: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Stale previewUrl not cleared when re-generating.

Unlike the prior synchronous flow, generate() no longer resets previewUrl before enqueueing a new task, so an old preview can remain visible/stale until the new one completes. Self-recovers once loadPreview runs, but could confuse users mid-regeneration.

🩹 Suggested fix
 const generate = async () => {
if (generating) return;
setGenerating(true);
setError(null);
+ updatePreviewUrl(null);
try {
await startResumeDraft(model);
startPolling();
🤖 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 `@web/src/hooks/useResumeDraftPdf.ts` around lines 92 - 105, The generate flow
in useResumeDraftPdf should clear any existing previewUrl before starting a new
draft generation, since the async enqueue/polling path can leave a stale preview
visible until loadPreview updates it. Update generate() to reset the preview
state alongside setGenerating/setError before calling startResumeDraft(model),
so the UI doesn’t show an outdated preview during regeneration.
web/src/api/agent.ts (1)

25-53: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

ADR-0020 に更新してください
この非同期ドラフトフローの JSDoc は ADR-0018 ではなく ADR-0020 を参照するように揃えてください。関連するソースコメントも同じ番号に合わせると、生成される API ドキュメントとのずれもなくなります。

🤖 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 `@web/src/api/agent.ts` around lines 25 - 53, The JSDoc references in the
resume draft async flow are still pointing to ADR-0018, so update the comments
on startResumeDraft, getResumeDraftStatus, and fetchResumeDraftPdfBlobUrl to
reference ADR-0020 instead. Keep the surrounding descriptions the same, and make
sure any related source comments for this flow use the same ADR number so the
generated API docs stay consistent.
🤖 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 @.claude/rules/backend/agent.md:
- Around line 54-74: `run_resume_draft_task()` の課金と `resume_draft_cache`
への結果保存が別トランザクションになっており、さらに rerun 時に `dead_letter` を `pending`
に戻すため、`record_chat_usage()`
後に失敗すると再実行で二重課金される可能性があります。`run_resume_draft_task()`, `record_chat_usage()`,
`resume_draft_cache`, `dead_letter`
の再投入経路を見直し、課金済みマーカーを永続化するか、課金記録と結果保存を同一の原子操作にまとめてから、この billing flow
を安全とみなすよう修正してください。
In `@backend/app/services/agent/resume_draft/run_task.py`:
- Around line 105-126: The post-billing write-back in run_task() is still
retryable, so failures after record_chat_usage() can rerun the task and risk
double-charging or inconsistent cache state. Update the second session_factory
block that loads ResumeDraftCacheRepository and writes result/status so any
exception there is wrapped as NonRetryableError, matching the billing failure
handling, and keep the cache update path non-retryable after the charge has been
committed.
---
Nitpick comments:
In `@backend/tests/test_resume_draft_api.py`:
- Around line 85-160: Add a test for the idempotent re-enqueue guard in the
resume draft flow: when an existing ResumeDraftCache is already in "pending" or
"processing", the request should not dispatch again and should return the
current cache status instead of "pending". Extend
backend/tests/test_resume_draft_api.py with a case around
test_resume_draft_run_enqueues and try_reset_to_pending that seeds link data,
inserts a ResumeDraftCache in processing state, calls
/api/agent/resume-draft/run, and asserts the 202 response echoes "processing"
from the response payload.
In `@web/src/api/agent.ts`:
- Around line 25-53: The JSDoc references in the resume draft async flow are
still pointing to ADR-0018, so update the comments on startResumeDraft,
getResumeDraftStatus, and fetchResumeDraftPdfBlobUrl to reference ADR-0020
instead. Keep the surrounding descriptions the same, and make sure any related
source comments for this flow use the same ADR number so the generated API docs
stay consistent.
In `@web/src/hooks/useResumeDraftPdf.ts`:
- Around line 92-105: The generate flow in useResumeDraftPdf should clear any
existing previewUrl before starting a new draft generation, since the async
enqueue/polling path can leave a stale preview visible until loadPreview updates
it. Update generate() to reset the preview state alongside
setGenerating/setError before calling startResumeDraft(model), so the UI doesn’t
show an outdated preview during regeneration.
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b1c56f8e-c984-477b-9471-66dabdae96ce

📥 Commits

Reviewing files that changed from the base of the PR and between f3258b7 and 301143a.

📒 Files selected for processing (22)
  • .claude/rules/backend/agent.md
  • backend/alembic_migrations/versions/0047_add_resume_draft_cache_table.py
  • backend/app/messages.json
  • backend/app/models/__init__.py
  • backend/app/models/cache.py
  • backend/app/repositories/resume_draft.py
  • backend/app/routers/agent.py
  • backend/app/services/agent/resume_draft/run_task.py
  • backend/app/services/tasks/base.py
  • backend/app/services/tasks/handlers/__init__.py
  • backend/app/services/tasks/handlers/resume_draft.py
  • backend/tests/test_resume_draft_api.py
  • backend/tests/test_worker/test_resume_draft.py
  • docs/adr/0020-async-resume-draft-generation.md
  • docs/adr/README.md
  • web/e2e/github-link.spec.ts
  • web/src/api/agent.ts
  • web/src/api/generated.ts
  • web/src/api/paths.ts
  • web/src/constants/messages.ts
  • web/src/hooks/useResumeDraftPdf.test.ts
  • web/src/hooks/useResumeDraftPdf.ts

Comment thread.claude/rules/backend/agent.md
Comment threadbackend/app/services/agent/resume_draft/run_task.py Outdated
課金(record_chat_usage)と結果保存を同一トランザクションで確定し、
「課金済みだが結果未保存」からの再実行による二重課金を構造的に防ぐ。
加えてフェーズA に completed 冪等ガードを追加し、原子 commit 後・ack 前の
クラッシュによる Cloud Tasks 再配信での再課金を防ぐ。回帰テストを追加。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVqj6qCo31rXwKASxB5V2Z
@yusuke0610
yusuke0610 merged commit 114f801 into mainJul 6, 2026
20 checks passed
@yusuke0610
yusuke0610 deleted the feat/async-resume-draft branch July 20, 2026 12:25
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agentDevForge AgentbackendバックエンドdocumentationImprovements or additions to documentationtestテスト追加・修正webフロントエンド (web)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@yusuke0610@claude