Merge branch 'main' into worktree-diff-side-by-side
# Conflicts: # src/ClaudeDo.Ui/CLAUDE.md
This commit is contained in:
@@ -32,7 +32,14 @@ session internals, or app-settings writes. Auth via an optional `X-ClaudeDo-Key`
|
||||
`ListTasksResult`/`BatchGetTaskResult`, where exactly one of the lean (`TaskRefDto`) and
|
||||
full (`TaskDto`, incl. Description/Result) fields is populated per the flag — keeps a
|
||||
list of verbosely-described tasks from blowing past the response size limit by default.
|
||||
3. `ExternalMcpExceptionFilter.Wrap` is registered as a call-tool filter so
|
||||
3. **Description style is documented in `McpToolDocs`** (same folder) and shared boilerplate
|
||||
lives there as `const` strings. Rules: the first sentence says what the tool does *and* when
|
||||
to reach for it (MCP clients rank tools by that text, so the trigger must not sit behind
|
||||
return-shape prose); parameters are documented with `[Description]` **on the parameter**, not
|
||||
in the tool description; result fields appear only where the caller must branch on them
|
||||
before calling (`isEmpty`, `truncated`, `conflicts`, `available`); no design rationale or
|
||||
"since this feature was introduced" history. Not test-enforced — review it in PRs.
|
||||
4. `ExternalMcpExceptionFilter.Wrap` is registered as a call-tool filter so
|
||||
`InvalidOperationException` / `ArgumentException` messages survive as `McpException` —
|
||||
otherwise the SDK's catch-all replaces any non-`McpException` with a generic
|
||||
*"An error occurred invoking 'X'."*
|
||||
@@ -42,8 +49,9 @@ session internals, or app-settings writes. Auth via an optional `X-ClaudeDo-Key`
|
||||
### `ExternalMcpService` — task CRUD, execution, git
|
||||
|
||||
Task: `ListTaskLists`, `ListTasks`, `GetTask`, `AddTask`, `AddSubtask`, `UpdateTask`,
|
||||
`UpdateTaskStatus`, `GetTaskStatusValues`, `ReviewTask`, `RunTaskNow`, `ContinueTask`,
|
||||
`CancelTask`, `DeleteTask`.
|
||||
`UpdateTaskStatus`, `ReviewTask`, `RunTaskNow`, `ContinueTask`, `CancelTask`, `DeleteTask`.
|
||||
(`GetTaskStatusValues` was removed — a whole tool entry for static reference text. `GetTask`'s
|
||||
description is now the canonical place for what each status means.)
|
||||
|
||||
Worktree/git: `GetTaskWorktree`, `GetTaskDiff`, `MergeTask`, `ContinueMerge`, `AbortMerge`,
|
||||
`PreviewMerge`, `PreviewMergeSet`, `RevertMerge`, `ListWorktrees`, `CleanupTaskWorktree`.
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# Usage monitoring, gate & throttle
|
||||
|
||||
> **Explore-note — verify before trusting.** Distilled map of a subsystem, not authoritative.
|
||||
> Last verified against commit `f6cb825` (2026-08-05).
|
||||
> Last verified against commit `f6cb825` (2026-08-05), plus the uncommitted per-bucket-throttle /
|
||||
> draggable-gauge change of 2026-08-06 (this note already describes that newer state).
|
||||
> Drift check: `git log --oneline f6cb825..HEAD -- src/ClaudeDo.Worker/Usage src/ClaudeDo.Worker/Queue src/ClaudeDo.Ui/ViewModels/UsagePillViewModel.cs`
|
||||
> Stable structure only (no line numbers). See docs/explore-notes/README.md.
|
||||
|
||||
@@ -52,21 +53,26 @@ on block / Info on resume) exactly **once per change**, not every tick.
|
||||
|
||||
## The throttle (staged parallelism)
|
||||
|
||||
`UsageThrottle.EffectiveSlots(configuredSlots, fiveHourPct, sevenDayPct, softPct, hardPct,
|
||||
gateFiveHourPct, gateSevenDayPct)` — pure static, no state.
|
||||
`UsageThrottle.EffectiveSlots(configuredSlots, fiveHourPct, fiveHourThresholds, sevenDayPct,
|
||||
sevenDayThresholds)` — pure static, no state. `UsageThresholds(SoftPct, HardPct, GatePct)` is the
|
||||
per-bucket triple (same file).
|
||||
|
||||
Thresholds `usage_throttle_soft_pct` / `usage_throttle_hard_pct` (defaults 50/65).
|
||||
Whichever of 5h/7d is **more utilized** decides the stage:
|
||||
Thresholds are **per bucket** (`usage_throttle_five_hour_{soft,hard}_pct` /
|
||||
`usage_throttle_seven_day_{soft,hard}_pct`, defaults 50/65 each) because the 5h and 7d windows fill
|
||||
at very different rates. Each bucket is staged independently and the **strictest** bucket wins —
|
||||
not "whichever is more utilized", so a bucket that is lower but tightly configured can be the one
|
||||
that throttles:
|
||||
|
||||
| Utilization | Effective slots |
|
||||
| Utilization (per bucket) | That bucket's slots |
|
||||
|---|---|
|
||||
| below soft | full configured `max_parallel_executions` |
|
||||
| `>= softPct` | capped at 2 |
|
||||
| `>= hardPct` | capped at 1 |
|
||||
| `>=` either gate threshold | 0 — same hard block as `UsageGate` |
|
||||
| `>= gatePct` | 0 — same hard block as `UsageGate` |
|
||||
|
||||
A threshold of `0` disables that stage. The `0` return is deliberately kept in sync with
|
||||
`UsageGate`'s hard block because both read the same gate thresholds — change one, change both.
|
||||
A threshold of `0` disables that stage for that bucket, and a bucket with no reading (null) never
|
||||
throttles. The `0` return is deliberately kept in sync with `UsageGate`'s hard block because both
|
||||
read the same gate thresholds — change one, change both.
|
||||
|
||||
Only **new** slot fills are affected; a run already occupying a slot when the stage tightens
|
||||
runs to completion. Same fail-open policy: no snapshot means no throttling.
|
||||
@@ -108,6 +114,12 @@ A missing/unreadable transcript leaves all four fields `null`; it never fails th
|
||||
Reads `~/.claude/projects/**/*.jsonl`, aggregating by date / model / scope (ClaudeDo vs
|
||||
Other), deduped by `requestId`, with a per-file length+mtime cache.
|
||||
|
||||
`ReadAsync` **skips any file whose mtime predates the window start minus one day** — it cannot hold
|
||||
a record inside the range, and the full history is large (measured 2026-08-06: 501 files / 230 MB /
|
||||
77k lines ≈ 1.7 s to parse cold; a 7-day range touches ~190 files / ~106 MB). The one-day slack
|
||||
absorbs local-vs-UTC skew between mtime and record timestamps. `ReadSessionTotalsAsync` is
|
||||
unaffected — it looks up a single `{sessionId}.jsonl`.
|
||||
|
||||
`<synthetic>`-model lines are skipped **everywhere** — they are not real API calls.
|
||||
|
||||
## UI surfaces
|
||||
@@ -117,6 +129,25 @@ Other), deduped by `requestId`, with a per-file length+mtime cache.
|
||||
`IWorkerClient.UsageUpdatedEvent`. Dot state priority is mutually exclusive:
|
||||
**blocked > stale > warn > normal**. `IsThrottled` (effective slots below configured, and
|
||||
not gate-blocked) adds a tooltip line naming effective/configured slots + decisive bucket.
|
||||
The pill's click handler (`IslandsShellViewModel.OpenUsageMonitor`) **shows the window before
|
||||
loading** (`BeginLoad`) — awaiting the load first made the pill feel like a dead click, because
|
||||
the first `GetModelUsage` per worker process scans the whole transcript history.
|
||||
- **Draggable stage markers** — each of the two real gauges carries three markers (soft/hard/gate).
|
||||
`UsageGaugeBar` (`Views/Controls`) draws them against its own width and does the pointer work;
|
||||
the math is a pure static, `UsageThresholdDrag` (in the modal VM's file), which keeps
|
||||
soft ≤ hard ≤ gate and treats a neighbour of `0` as off. Release fires the row's
|
||||
`CommitCommand` → read-modify-write via `GetAppSettings` + `UpdateAppSettings`, so only the
|
||||
dragged bucket's three fields change. Plan-dependent `weekly_scoped` gauges are read-only.
|
||||
- **Legend = numeric editor.** Under each adjustable bar sit three legend rows whose colour swatches
|
||||
match the markers (soft `TextDimBrush`, hard `StatusReviewBrush`, gate `StatusErrorBrush`), each
|
||||
with a `NumericUpDown`. `NumericUpDown` has no commit command, so the box's `Tag`
|
||||
(`soft`/`hard`/`gate`) plus two code-behind handlers (`LostFocus`, Enter) call the row's
|
||||
`CommitSoft`/`CommitHard`/`CommitGate` command. Those run the typed value through the **same**
|
||||
`UsageThresholdDrag.Apply` clamp as a drag, so a box can't invert the order and only the edited
|
||||
stage moves. ⚠️ The `KeepLastNumber` converter is mandatory on those bindings — see the
|
||||
`NumericUpDown` null gotcha in `src/ClaudeDo.Ui/CLAUDE.md`.
|
||||
Rows are updated **in place** on each snapshot (keyed by limit kind) so a poll landing mid-drag
|
||||
doesn't replace the bound instance.
|
||||
- **`UsageMonitorModalViewModel`** — opened from the pill. Renders one gauge **per row** in
|
||||
`UsageSnapshotDto.Limits` — deliberately **dynamic**, because the `seven_day_opus` /
|
||||
`seven_day_sonnet`-style buckets the raw API returns are plan-dependent and come back
|
||||
@@ -138,5 +169,13 @@ Other), deduped by `requestId`, with a per-file length+mtime cache.
|
||||
## Settings columns
|
||||
|
||||
`app_settings`: `usage_gate_five_hour_pct` / `usage_gate_seven_day_pct` (80/90),
|
||||
`usage_throttle_soft_pct` / `usage_throttle_hard_pct` (50/65). All four clamped 0..100 by
|
||||
`AppSettingsRepository.UpdateAsync`. Worker config: `usage_poll_interval_seconds`.
|
||||
`usage_throttle_five_hour_{soft,hard}_pct` / `usage_throttle_seven_day_{soft,hard}_pct` (50/65 per
|
||||
bucket). All six clamped 0..100 by `AppSettingsRepository.UpdateAsync`, which does **not** enforce
|
||||
soft ≤ hard ≤ gate — the ordering is a UI-side drag constraint, and an out-of-order stored config
|
||||
degrades instead of throwing. Worker config: `usage_poll_interval_active_seconds` /
|
||||
`usage_poll_interval_idle_seconds`.
|
||||
|
||||
The gate percentages are editable in **two** places that both write the same `app_settings` row:
|
||||
Settings → General (typed) and the usage-monitor gauges (dragged). The throttle stages are
|
||||
gauge-only — `SettingsModalViewModel` therefore carries them load→save verbatim so saving Settings
|
||||
can't reset a dragged value.
|
||||
|
||||
@@ -124,7 +124,7 @@ read-only "## Reference files" section.
|
||||
- `TaskMergeService` — conflict resolution for worktree merges.
|
||||
|
||||
**Hub/**
|
||||
- `HubBroadcaster` — single SignalR broadcast point (TaskStarted/TaskUpdated/TaskMessage/RunCreated…).
|
||||
- `HubBroadcaster` — single SignalR broadcast point (TaskStarted/TaskUpdated/TaskMessage/WorktreeUpdated…).
|
||||
- `WorkerHub` — SignalR hub + client methods.
|
||||
|
||||
**Agents/**
|
||||
|
||||
Reference in New Issue
Block a user