feat(ai-missions): live phase + elapsed + cancel for running iterations

Closes the "iteration is running, no feedback" black hole. The user now
sees, per running iteration:

    Frage Planner · frage Planner an              ⏱ 23s
                                              [Abbrechen]

Phases (\`IterationPhase\`):
  resolving-inputs → calling-llm → parsing-response →
  staging-proposals → finalizing

The runner advances through these via \`setIterationPhase\` between each
await, writing currentPhase + phaseDetail + phaseStartedAt onto the
iteration. UI reads them via Dexie liveQuery — no polling.

Cancel:
- \`requestIterationCancel\` writes cancelRequested=true on the iteration
- runner polls \`isCancelRequested\` between every phase + per stage step
- cancellation finalises as \`failed\` with summary \`'cancelled by user'\`
- UI button is disabled + relabelled "Wird abgebrochen…" until the next
  poll picks it up

Hard timeout: 90 s wall-clock per iteration via Promise.race against a
CancelledError. Wedged backends (e.g. flaky mana-llm) fail fast with
"timeout after 90s" instead of sitting in \`running\` forever.

Elapsed counter is a \$state variable ticking once a second, scoped to
the ListView component — Dexie isn't touched. Auto-cleaned on
component destroy.

shared-ai re-exports \`IterationPhase\` so server-side mana-ai can
inspect the same phase enum (no consumer there yet, but the type is
ready for the run-status endpoint planned in HEALTH page).

77/77 webapp tests still green; svelte-check clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-15 14:15:48 +02:00
parent bb3da78d5c
commit ef47adb7d7
6 changed files with 357 additions and 44 deletions

View file

@ -11,6 +11,7 @@ export type { Actor } from './actor';
export { USER_ACTOR, isAiActor, isSystemActor } from './actor';
export type {
IterationPhase,
Mission,
MissionCadence,
MissionInputRef,

View file

@ -1,4 +1,5 @@
export type {
IterationPhase,
Mission,
MissionCadence,
MissionInputRef,

View file

@ -41,6 +41,18 @@ export interface PlanStep {
readonly status: 'planned' | 'staged' | 'approved' | 'rejected' | 'skipped' | 'failed';
}
/**
* Sub-state of a `running` iteration so the UI can show meaningful
* progress instead of a spinner with no context. Set by the runner as
* it advances; cleared when the iteration leaves `running`.
*/
export type IterationPhase =
| 'resolving-inputs'
| 'calling-llm'
| 'parsing-response'
| 'staging-proposals'
| 'finalizing';
export interface MissionIteration {
readonly id: string;
readonly startedAt: string;
@ -59,6 +71,20 @@ export interface MissionIteration {
* pre-server iterations.
*/
readonly source?: 'browser' | 'server';
/** Sub-status while `overallStatus === 'running'`. Undefined otherwise. */
readonly currentPhase?: IterationPhase;
/** When the runner advanced into the current phase — for elapsed-in-phase. */
readonly phaseStartedAt?: string;
/**
* Human-readable detail for the current phase (e.g. "staging 2/5").
* Pure UI hint; not load-bearing.
*/
readonly phaseDetail?: string;
/**
* Set to true by the user clicking "Abbrechen". The runner polls this
* between phases and exits early as `failed` with summary 'cancelled'.
*/
readonly cancelRequested?: boolean;
}
export interface Mission {