fix: more work on background tasks

This commit is contained in:
2026-07-24 11:43:11 +02:00
parent c187108f89
commit da13bac755
45 changed files with 2905 additions and 732 deletions

View File

@@ -44,9 +44,10 @@ surface GenerationRuntimeSurface {
-- "Generated" for a full render and "Rewrote" for a validation apply —
-- and advances the bar by the number of URLs written.
--
-- Before section work begins, generation counts its planned URLs. The
-- task reports 0/total immediately and advances once per rendered URL,
-- matching bDS2's route-based generation progress.
-- Route preparation and counting happen in background work before
-- section rendering. Validation totals count only prepared URLs
-- selected by the validation report, not the whole section. The task
-- reports 0/total immediately and advances once per rendered URL.
--
-- Full generation and validation apply share the same structuring: a
-- task group containing one task per section (Render Site Core, Render
@@ -310,6 +311,9 @@ rule ApplyValidation {
-- unchanged post route refreshes its tracked generation time so the
-- automatic follow-up validation does not report it stale again.
ensures: GenerateSiteRequested(plan_generation(project_id, sections))
-- Observable phases are prepare, affected render sections, optional
-- calendar regeneration, search indexing, and final validation. Failure or
-- cancellation stops every dependent phase.
}
-- Day-block grouping for archives

View File

@@ -262,7 +262,9 @@ invariant PanelTabFallback {
-- git_log unavailable when neither post nor media tab is active
}
-- Tasks tab: last 10 tasks, newest first, with progress/cancel.
-- Tasks tab: every active task plus the 10 newest finished tasks, with
-- progress/cancel. Active groups always include every child needed to show a
-- complete group even when this exceeds the finished-history limit.
-- Tasks with shared group_id are collapsible groups showing bDS2-style
-- average child progress; pending children contribute zero.
-- Output tab: log entries with copy-all button.
@@ -287,6 +289,12 @@ value StatusBarLeft {
running_task_overflow: Integer? -- "+N more" count when multiple running
}
invariant StatusBarTaskSelection {
-- The indicator selects the first running task, otherwise the first
-- pending task. Overflow counts every other running or pending task.
-- Status remains typed until its display label is localized.
}
value StatusBarRight {
-- In display order (left to right):
post_status: String? -- draft|published|archived dot, when post tab active

View File

@@ -135,6 +135,11 @@ surface ScriptRuntimeSurface {
-- sync_from_posts.
-- bds.tasks: get, status_snapshot, cancel, get_all, get_running,
-- clear_completed.
-- get and get_all inspect every retained task rather than the
-- status-snapshot history cap; get_all is newest first. get_running
-- contains only running tasks. cancel succeeds only for a pending or
-- running task. clear_completed removes completed tasks while keeping
-- failed and cancelled results.
-- bds.sync: check_availability, get_repo_state, get_status,
-- get_history, get_remote_state, fetch, pull, push, commit_all.
-- bds.publish: upload_site.

View File

@@ -6,6 +6,9 @@
entity Task {
name: String
status: pending | running | completed | failed | cancelled
source: local | remote
cancellable: Boolean
cancellation_requested: Boolean
progress: Decimal? -- 0.0..1.0
message: String?
group_id: String? -- Optional task grouping
@@ -28,6 +31,7 @@ surface TaskControlSurface {
SubmitTaskRequested(name, work)
CancelTaskRequested(task)
RegisterExternalTaskRequested(name)
TaskStateRequested()
}
surface TaskRuntimeSurface {
@@ -38,6 +42,8 @@ surface TaskRuntimeSurface {
TaskWorkFailed(task, error_message)
ProgressReported(task, value, message)
FinishedTaskEvictionDue()
WorkerCapacityAvailable()
TaskWorkStopped(task)
}
surface TaskSurface {
@@ -46,6 +52,7 @@ surface TaskSurface {
exposes:
task.name
task.status
task.source
task.progress when task.progress != null
task.message when task.message != null
task.group_id when task.group_id != null
@@ -74,13 +81,23 @@ invariant FifoQueue {
rule SubmitTask {
when: SubmitTaskRequested(name, work)
let running_tasks = Tasks where status = running
ensures:
let task = Task.created(name: name, status: pending)
let task = Task.created(
name: name,
status: pending,
source: local,
cancellable: true,
cancellation_requested: false
)
task.status = pending
if running_tasks.count < config.max_concurrent:
task.status = running
TaskStarted(task, work)
}
rule StartNextTask {
when: WorkerCapacityAvailable()
let pending_tasks = Tasks where status = pending
requires: pending_tasks.count > 0
ensures: pending_tasks.first.status = running
ensures: TaskStarted(pending_tasks.first)
}
rule CompleteTask {
@@ -99,8 +116,28 @@ rule FailTask {
rule CancelTask {
when: CancelTaskRequested(task)
requires: task.status = running or task.status = pending
-- Cancellation uses a runtime-specific cancellation mechanism
requires: task.status = pending
ensures: task.status = cancelled
ensures: NextQueuedTaskStarted()
}
rule RequestRunningTaskCancellation {
when: CancelTaskRequested(task)
requires: task.status = running
requires: task.cancellable
ensures: task.cancellation_requested = true
ensures: StopTaskWorkRequested(task)
@guidance
-- Long operations observe the request at phase or item boundaries.
-- Owned child processes are terminated when the platform permits it.
-- The task remains running and occupies its worker slot until work
-- actually stops; status surfaces expose cancellation_requested.
}
rule FinishRunningTaskCancellation {
when: TaskWorkStopped(task)
requires: task.status = running
requires: task.cancellation_requested
ensures: task.status = cancelled
ensures: NextQueuedTaskStarted()
}
@@ -110,6 +147,9 @@ rule ReportProgress {
-- Progress events throttled to 250ms
ensures: task.progress = value
ensures: task.message = message
@guidance
-- A report whose value is 1.0 is always accepted so the terminal
-- localized phase text cannot be dropped by throttling.
}
invariant ProgressThrottled {
@@ -139,11 +179,24 @@ rule EvictFinishedTasks {
ensures: not exists task
}
rule PruneFinishedTasksOnAccess {
when: TaskStateRequested()
for task in Tasks where status in {completed, failed, cancelled}:
if now - task.finished_at >= config.finished_task_ttl:
ensures: not exists task
}
-- External tasks: lifecycle controlled by caller (e.g., renderer-side scripts)
rule RegisterExternalTask {
when: RegisterExternalTaskRequested(name)
ensures:
let task = Task.created(name: name, status: running)
let task = Task.created(
name: name,
status: running,
source: remote,
cancellable: true,
cancellation_requested: false
)
task.status = running
@guidance
-- External tasks are not managed by the queue