The six Lifecycle/*Recovery hosted services now broadcast one
OperationProgress("startup-recovery", <phase>, current, total) message each
after they finish, instead of leaving the UI on a bare "connecting" text
during worker startup. IslandsShellViewModel subscribes and swaps in
"Recovering... (i/n)" (existing ops.worker.startupRecovery key, no locale
changes) while Worker.IsReconnecting is true, and clears it once actually
connected so a later transient reconnect doesn't replay stale text.
OperationProgress broadcasts to Clients.All with no replay-on-connect, so a
UI that hasn't finished its SignalR handshake yet can miss some or all of
these messages and simply keep showing "connecting" as before -- accepted
rather than adding a cached-state + reconnect-replay path (mirroring
RefreshExternalMergeConflictsAsync) for what is a fast, best-effort,
local-only startup sweep with no UI-visible failure mode beyond that.
Three behavioral changes around stuck planning subtasks:
- OrphanRecovery no longer clears ParentTaskId. Queued children of a
parent that is not in a planning phase are dequeued (Status: Queued
-> Idle, BlockedByTaskId cleared) but stay attached to the parent so
the historical lineage is preserved.
- DiscardPlanningAsync stops promoting terminal (Done/Failed/Cancelled)
children to top-level for the same reason - they remain ChildTasks of
the (now non-planning) parent.
- New PlanningLineageRecovery hosted service scans
~/.todo-app/planning-sessions/ and re-attaches a single, unambiguous
blocked-by chain to its original planning parent when the
parent_task_id links were lost. Refuses to guess when multiple
candidate chains exist.
UI now exposes ConnectionRestoredEvent on IWorkerClient, fired on first
connect and every reconnect. ListsIslandViewModel refreshes counters
and TasksIslandViewModel reloads the current list - so stale counts no
longer survive a worker restart.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three coordinated guards close the orphan-creation paths:
- CreateChildAsync refuses when the parent is not in a planning phase.
- DiscardPlanningAsync now returns a structured DiscardPlanningOutcome
and refuses when children are queued or running; callers can opt into
auto-dequeuing queued kids via dequeueQueuedChildren=true. Terminal
children (Done/Failed/Cancelled) are promoted to top-level instead of
becoming orphans when the parent's PlanningPhase is reset.
- OrphanRecovery hosted service clears ParentTaskId on any rows whose
parent is missing or no longer in a planning phase on worker startup,
mirroring the StaleTaskRecovery pattern.
UI surfaces the block reason: a confirm dialog offers to dequeue queued
children and retry; a running-children block is shown as a hard error
asking the user to cancel first.
WorkerClient now negotiates the JsonStringEnumConverter so the
DiscardPlanningResult enum round-trips correctly over SignalR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>