Abort and Retry Flows
docsfy handles abort and retry/regeneration as explicit state transitions for each variant (project/provider/model) and owner.
# src/docsfy/storage.py
VALID_STATUSES = frozenset({"generating", "ready", "error", "aborted"})
A generation task is keyed by owner + variant so duplicate in-flight runs are blocked:
# src/docsfy/main.py
gen_key = f"{owner}/{project_name}/{ai_provider}/{ai_model}"
async with _gen_lock:
if gen_key in _generating:
raise HTTPException(
status_code=409,
detail=f"Variant '{project_name}/{ai_provider}/{ai_model}' is already being generated",
)
Abort flow for active runs
Endpoints
| Endpoint | Purpose |
|---|---|
POST /api/projects/{name}/{provider}/{model}/abort |
Abort a specific variant (recommended) |
POST /api/projects/{name}/abort |
Legacy/backward-compatible abort by project name |
Note: The name-only abort endpoint is explicitly marked backward-compatible and aborts the first matching active run.
# src/docsfy/main.py
@app.post("/api/projects/{name}/abort")
async def abort_generation(request: Request, name: str) -> dict[str, str]:
"""Abort generation for any variant of the given project name.
Kept for backward compatibility. Finds the first active generation
matching the project name.
"""
What happens when abort is requested
- Write access is required (
adminoruserrole). - Ownership/access is verified.
- The task is cancelled with
task.cancel(). - Server waits up to 5 seconds for cancellation acknowledgment.
- Variant status is persisted as
abortedwith an error message.
# src/docsfy/main.py
task.cancel()
try:
await asyncio.wait_for(task, timeout=5.0)
except asyncio.CancelledError:
pass
except asyncio.TimeoutError as exc:
raise HTTPException(
status_code=409,
detail=f"Abort still in progress for '{gen_key}'. Please retry shortly.",
) from exc
await update_project_status(
name,
provider,
model,
status="aborted",
owner=key_owner,
error_message="Generation aborted by user",
current_stage=None,
)
Warning: Abort can return
409(Abort still in progress...) if cancellation has not completed within 5 seconds. Retrying abort shortly is expected behavior.
UI behavior during abort
On the status page and dashboard, running variants show an Abort button; the action uses a confirmation modal and calls the variant-specific abort API.
// src/docsfy/templates/status.html
fetch('/api/projects/' + encodeURIComponent(PROJECT_NAME) + '/' + encodeURIComponent(PROJECT_PROVIDER) + '/' + encodeURIComponent(PROJECT_MODEL) + '/abort', { method: 'POST', credentials: 'same-origin', redirect: 'manual' })
// src/docsfy/templates/_modal.html
function modalConfirm(title, body, danger) {
return new Promise(function(resolve) {
showModal({
title: title, body: body, danger: danger,
confirmText: danger ? 'Delete' : 'Confirm',
cancelText: 'Cancel',
onConfirm: function() { resolve(true); },
onCancel: function() { resolve(false); },
});
});
}
Retry/regeneration flow after error or abort
There is no dedicated /retry backend route. Retry/regeneration is implemented as a new POST /api/generate request, usually pre-filled from the failed/aborted variant.
// src/docsfy/templates/status.html
var payload = { repo_url: repoUrl };
if (providerSelect) payload.ai_provider = providerSelect.value;
if (modelInput) payload.ai_model = modelInput.value;
if (forceCheckbox && forceCheckbox.checked) payload.force = true;
fetch('/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
redirect: 'manual',
body: JSON.stringify(payload)
})
Retry controls are only shown for error or aborted states:
<!-- src/docsfy/templates/status.html -->
{% if project.status == 'error' or project.status == 'aborted' %}
<div class="regenerate-inline">
<select class="form-select" id="retry-provider" aria-label="Provider for regeneration">...</select>
<input type="text" class="form-input" id="retry-model" ...>
<div class="form-checkbox-group">
<input type="checkbox" id="retry-force">
<label for="retry-force">Force</label>
</div>
<button class="btn btn-primary" id="btn-retry">Regenerate</button>
</div>
{% endif %}
Note: If provider/model is changed during retry from the status page, the UI redirects to the new variant status URL.
Force vs non-force regeneration
force=true clears cached pages and resets page count before regeneration:
# src/docsfy/main.py
if force:
cache_dir = get_project_cache_dir(project_name, ai_provider, ai_model, owner)
if cache_dir.exists():
shutil.rmtree(cache_dir)
logger.info(f"[{project_name}] Cleared cache (force=True)")
await update_project_status(
project_name,
ai_provider,
ai_model,
status="generating",
owner=owner,
page_count=0,
)
Without force, docsfy can short-circuit to up-to-date if commit SHA is unchanged:
# src/docsfy/main.py
if old_sha == commit_sha:
await update_project_status(
project_name,
ai_provider,
ai_model,
status="ready",
owner=owner,
current_stage="up_to_date",
)
return
Status UI explicitly surfaces this case:
<!-- src/docsfy/templates/status.html -->
<span id="success-text">{% if project.current_stage == 'up_to_date' %}Documentation is already up to date — no changes since last generation.{% else %}Documentation generated successfully!{% endif %}</span>
Tip: Use
Forcewhen you need a full refresh and do not want reuse of existing cached pages.
Incremental regeneration behavior
If commit changed and previous plan exists, docsfy can ask the incremental planner which pages to regenerate.
# src/docsfy/generator.py
if not success:
logger.warning(f"[{project_name}] Incremental planner failed, regenerating all")
return ["all"]
result = parse_json_list_response(output)
if result is None or not isinstance(result, list):
return ["all"]
...
if not result:
return ["all"]
current_stage values used through generation include:
- cloning
- planning
- incremental_planning (when applicable)
- generating_pages
- rendering
- up_to_date (ready without rebuild)
// src/docsfy/templates/status.html
var STAGES = ['cloning', 'planning', 'generating_pages', 'rendering'];
Failure recovery and post-retry path
On startup, orphaned generating records are moved to error, which then enables regeneration controls.
# src/docsfy/storage.py
cursor = await db.execute(
"UPDATE projects SET status = 'error', error_message = 'Server restarted during generation', current_stage = NULL WHERE status = 'generating'"
)
Cancellation and hard failures during background generation are also persisted:
# src/docsfy/main.py
except asyncio.CancelledError:
await update_project_status(... status="aborted", error_message="Generation was cancelled", current_stage=None)
raise
except Exception as exc:
await update_project_status(... status="error", error_message=str(exc))
Access control for abort/retry
Abort and regenerate both require write access, and abort additionally enforces ownership/grant checks.
# src/docsfy/main.py
def _require_write_access(request: Request) -> None:
if request.state.role not in ("admin", "user"):
raise HTTPException(status_code=403, detail="Write access required.")
# src/docsfy/main.py
async def _check_ownership(...):
if request.state.is_admin:
return
...
access = await get_project_access(project_name, project_owner=project_owner)
if request.state.username in access:
return
raise HTTPException(status_code=404, detail="Not found")
Test coverage confirms viewer restriction:
# tests/test_auth.py
response = await ac.post("/api/generate", json={...})
assert response.status_code == 403
assert "Write access required" in response.json()["detail"]
Relevant configuration and automation
AI_CLI_TIMEOUT directly impacts failure timing (and therefore how often you hit retry/regeneration paths):
# .env.example
AI_PROVIDER=claude
AI_MODEL=claude-opus-4-6[1m]
AI_CLI_TIMEOUT=60
Automated tests are configured via tox:
# tox.toml
envlist = ["unittests"]
[env.unittests]
deps = ["uv"]
commands = [["uv", "run", "--extra", "dev", "pytest", "-n", "auto", "tests"]]
Abort/retry expectations are also documented in end-to-end UI checks:
# test-plans/e2e-ui-test-plan.md
- The status changes to `aborted`
- The error message shows "Generation aborted by user"
- The "Abort" button is replaced by regenerate controls (provider select, model input, force checkbox, and "Regenerate" button)
Warning: Retry UI currently submits
repo_urlpayloads.GenerateRequestaccepts eitherrepo_urlorrepo_path(not both), andrepo_urlis validated as a Git URL pattern. For local-path workflows, start a new generation withrepo_path(admin-only) rather than relying on URL-based retry payloads.