The usage monitor polled the undocumented OAuth usage endpoint every 60s and
earned 429s. It now polls every 5 min while any task is Running and every
15 min while idle (usage_poll_interval_active_seconds / _idle_seconds, both
clamped to >= 60; the old single usage_poll_interval_seconds key is gone).
A 429 comes back as UsageRateLimitedException carrying Retry-After and adds
exponential backoff on top, capped at 30 min and never shorter than the normal
cadence; the strike count resets on the first success. The schedule arithmetic
is the pure static UsagePollSchedule.NextDelay.
Since the idle cadence is slow on purpose, WorkerHub.RefreshUsage drives
UsageMonitorService.RefreshNowAsync behind a Refresh now button in the Usage
Monitor modal: an out-of-band poll that pushes the loop's next-due time out so
no double poll follows, with a 10s cooldown so click-spam can't earn a 429.
Staleness now measures against the slower (idle) interval so an idle worker
isn't flagged stale just for not polling.
Adds Usage/ with ClaudeOAuthUsageClient (reads the access token Claude
Code keeps fresh at ~/.claude/.credentials.json, calls the Anthropic
OAuth usage endpoint, defensively parses buckets/limits), UsageState
(threadsafe last-good-snapshot holder that never regresses on
failure), and UsageMonitorService (BackgroundService polling on the
new usage_poll_interval_seconds config, min 15s, one poll at startup,
warns at most once per distinct error).
The embedded ConPTY terminal replaced the in-app streaming interactive session, so
delete the dead stack: StreamingClaudeSession, InteractiveSessionService,
ProcessClaudeStreamTransport, IClaudeStreamTransport, ILiveSession, LiveSessionRegistry,
IdleSessionReaper (+ WorkerConfig.InteractiveIdleTimeoutMinutes), the WorkerHub
interactive methods + HubBroadcaster events, IWorkerClient interactive members, the
TaskMonitorViewModel composer + SessionTerminalView composer markup, and the old
'Run interactively' entry. AskUser/PendingQuestionRegistry, the autonomous path,
planning, ResumeTaskInTerminal, and all ConPTY code are kept. Localization pruned.
Interactive/streaming sessions are persistent claude.exe processes that
wait on stdin and never exit on their own. The only teardown was an
explicit StopInteractiveSession from the UI — there is no client-disconnect
or shutdown sweep — so an abandoned chat (UI closed, navigated away,
crashed) kept its claude.exe (+ conhost) alive for the worker's whole
lifetime. Under a long-running autostart worker these accumulate to dozens
of orphaned child processes.
LiveSessionRegistry now tracks per-session activity (Touch on every output
line and user action) and exposes ReapIdleAsync, which stops sessions idle
past a timeout while skipping any with a turn in flight. IdleSessionReaper
(BackgroundService) sweeps every 5 min; idle timeout defaults to 30 min,
configurable via interactive_idle_timeout_minutes (0 disables).
Hub: GetOnlineInboxState / SetOnlineInboxConfig / SetOnlineInboxAuth /
ClearOnlineInboxAuth. WorkerConfig.SaveOnlineInbox persists only the
online_inbox section. OnlineTokenStore + config registered always so hub
methods work when sync is disabled. IWorkerClient surface + all test fakes
synced. RedirectUri config (default http://localhost:8765/callback).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Optional, opt-in (online_inbox.enabled, default false → zero network).
Worker-side reconcile loop: pull web-created tasks down as Idle, push the
list catalog and the Idle backlog mirror up. Auth behind IOnlineAuthProvider
(StaticTokenAuthProvider default; ZitadelAuthProvider stubbed for Phase 2).
DPAPI refresh-token store. 35 tests, no real network/Zitadel/Claude.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A second WebApplication runs the external MCP server on its own port (default 47822) so it can expose a different tool set under different auth than the internal /mcp endpoint. Shared singletons (config, broadcaster, queue, db factory) are injected by instance so both apps share runtime state. ExternalMcpAuthMiddleware enforces an optional X-ClaudeDo-Key header; loopback-only trust when no key is configured.
Tools: ListTaskLists, ListTasks, GetTask, AddTask, UpdateTaskStatus, RunTaskNow, CancelTask.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Data layer: Paths helper with ~/%USERPROFILE% expansion, SqliteConnectionFactory
(WAL + foreign keys), SchemaInitializer that applies the embedded schema.sql, and
POCO entities for lists/tasks/tags/worktrees.
Worker: WorkerConfig loader (~/.todo-app/worker.config.json with defaults),
WorkerHub exposing Ping(), and Program.cs wiring Kestrel to 127.0.0.1:<port>,
SignalR at /hub, schema applied on startup.
Pins Microsoft.Data.Sqlite and Microsoft.Extensions.Hosting to 8.x for net8.0
compatibility.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>