163 lines
9.3 KiB
Markdown
163 lines
9.3 KiB
Markdown
---
|
|
status: draft
|
|
system: platform
|
|
requirements:
|
|
- REQ-PLATFORM-INTERACTIVE-READS-001
|
|
- REQ-PLATFORM-INTERACTIVE-READS-002
|
|
- REQ-PLATFORM-INTERACTIVE-READS-003
|
|
---
|
|
|
|
# Interactive Read Availability System Design
|
|
|
|
## Purpose and boundaries
|
|
|
|
Platform owns operational read availability. Analytics owns aggregate execution,
|
|
while workspace recovery follows its
|
|
[workspace design](../../workspaces/system-design/workspace-read-recovery.md).
|
|
The existing [required-store design](postgres-domain-store-parity.md#runtime-health)
|
|
and [persistence ADR](../../../decisions/2026-09-05-required-internal-persistence.md)
|
|
remain authoritative. This design adds workload control, not a health bypass.
|
|
|
|
## Requirement mapping
|
|
|
|
| Requirement | Design sections |
|
|
| --- | --- |
|
|
| REQ-PLATFORM-INTERACTIVE-READS-001 | Query shape and metric compatibility; Performance evidence |
|
|
| REQ-PLATFORM-INTERACTIVE-READS-002 | Admission and cancellation; Persistence and diagnostics |
|
|
| REQ-PLATFORM-INTERACTIVE-READS-003 | Section recovery and presentation |
|
|
|
|
## Query shape and metric compatibility
|
|
|
|
Change `internal/analytics/repository/sqlite/stats.go`. All runtime reads use
|
|
`Repository.ro`. The database layer supplies the selected SQLite or PostgreSQL
|
|
handle; continue using `dialect` helpers, `sqlx.In`, and `Rebind`.
|
|
|
|
For `GetTaskStats`, select the eligible workspace task page first, using the
|
|
existing task-created range, updated-time ordering, and caller's limit including
|
|
the extra pagination row. Aggregate sessions, turns, and messages independently
|
|
for those task IDs, then join aggregates. Session-start filtering remains distinct
|
|
from task-created filtering. Do not add a message timestamp filter where none
|
|
exists. Empty sessions still contribute a session count; messages without a turn
|
|
still contribute message counts. Preserve last completion, elapsed span, active
|
|
duration, and zero values. Add a task-ID tie-break only if existing tests/contracts
|
|
permit it; this repair does not require changing tied-row ordering.
|
|
|
|
For `GetRepositoryStats` and `buildRepositoryStatsQuery`, select eligible workspace
|
|
repositories before their child aggregates. Preserve the current distinctions:
|
|
task counts follow task-created range; session counts/messages/duration follow
|
|
session-start range; commit totals follow committed-at range. Task/session totals
|
|
are attributed through `task_repositories`; git totals use the session repository.
|
|
A task attached to two repositories contributes once to each eligible repository.
|
|
Do not sum raw turns after joining raw messages. Exclude soft-deleted repositories,
|
|
ephemeral tasks, and automation tasks as before.
|
|
|
|
For `GetDailyActivity`, constrain turn days to the requested inclusive UTC date
|
|
series before grouping. Count turns/tasks by day independently. Count messages
|
|
through distinct eligible (session, day) pairs, preserving current behavior: a
|
|
message contributes only when that session has a turn on the same day. Do not
|
|
silently redefine this as all messages created that day. Use portable timestamp
|
|
bounds/dialect normalization and cover midnight boundaries and naive UTC columns.
|
|
The date series still emits zero buckets. All-time heatmap remains 365 days.
|
|
|
|
`GetGlobalStats` already separates major aggregates. Preserve its clean-turn
|
|
outlier calculation. Keep completed activity, model usage, git totals, DTOs, top
|
|
model limits, completion definitions, and authorization unchanged. Add an index
|
|
only when query-plan evidence shows an unmet access path; replay it through the
|
|
existing `ensureStatsIndexes`, without a speculative index inventory.
|
|
|
|
## Admission and cancellation
|
|
|
|
Use one context-aware admission gate owned by the shared analytics repository
|
|
instance. Acquire once at each of its eight public read methods, including
|
|
`ListSessionCodeStats`; release after all rows close or any error. Do not acquire
|
|
again inside query helpers. Both HTTP Stats and plugin Host data reads receive the
|
|
same repository from `backendapp/storage.go`, so they share this budget.
|
|
|
|
Allow at most two concurrent analytics operations. The default SQLite pool has
|
|
four reader connections; queued analytics must not borrow another connection.
|
|
Keep the same analytics cap for PostgreSQL to avoid driver-specific request
|
|
behavior. It does not promise reserved capacity against unrelated workloads.
|
|
No new pool, broad scheduler, configurable knob, or persistent cache is needed.
|
|
|
|
Each operation has a ten-second total queue-plus-query deadline, capped by the
|
|
caller's earlier deadline. Waiting uses context cancellation. Check cancellation
|
|
again after admission and before SQL. Context-aware query/scan cleanup must return
|
|
capacity on cancellation, errors, and panic-safe deferred cleanup. Plugin calls
|
|
retain existing service error translation; no new plugin wire contract is added.
|
|
|
|
The Stats HTTP handler maps its own admission/query deadline to HTTP 503 with
|
|
`error_code: "analytics_busy"`, a sanitized message, and `Retry-After: 2`.
|
|
Client cancellation is not reported as a database defect. Other database errors
|
|
retain the existing error handling. Authorize before exposing analytics status.
|
|
Use `ApiError.errorCode` for the new error; persistence middleware instead uses
|
|
`body.code: "persistence_unavailable"`, so client classification must inspect that
|
|
existing field without changing the middleware contract.
|
|
|
|
The cap protects capacity; query optimization makes it practical. Never ship
|
|
serialization as the only performance fix. Tests hold admitted work at barriers,
|
|
then prove a normal read and a real health probe complete through the shared pool.
|
|
|
|
## Section recovery and presentation
|
|
|
|
`apps/web/app/stats/stats-data.tsx` retains independently loaded sections and adds
|
|
retry state/actions. Preserve its AbortController and workspace/range generation.
|
|
Keep successful section data during same-selection retries. A selection change
|
|
resets all sections and aborts every old timer/request. Copy remains gated by
|
|
`composeStatsResponse`.
|
|
|
|
Retry failed transient sections only: network failures, HTTP 429/502/503/504.
|
|
Allow two scheduled retries at 2 and 5 seconds after each preceding failure,
|
|
honoring a larger Retry-After. Cancel automatic retries while hidden; use
|
|
`useForegroundRefresh` to start one bounded recovery cycle on return. Manual
|
|
Retry starts a bounded cycle immediately. Timer, foreground, and manual triggers
|
|
coalesce into one in-flight request per section. No automatic retry for auth,
|
|
404, parse, or cancellation errors. Test using fake timers and deferred responses.
|
|
|
|
Render an inline failure and Retry in affected section cards using the existing
|
|
stats components. A same-selection refresh with retained data labels it stale;
|
|
an initial failure displays no fabricated zero. Show retry-in-progress status and
|
|
disable duplicate actions. Map temporary availability errors to translated copy
|
|
rather than exposing raw backend text. Update all five locale catalogs and generate
|
|
the Traditional Chinese pair using the repository script.
|
|
|
|
Desktop keeps its multi-column Stats layout. Phone enters through the existing
|
|
hamburger Stats destination and uses the existing single-column cards. Retry is
|
|
inside the failed card with at least a 44px touch area; ordinary desktop sizing
|
|
remains 28px. Keep the page's existing scroll owner, safe areas, focus behavior,
|
|
and no horizontal overflow. Workspace navigation remains its existing drawer.
|
|
|
|
## Performance evidence
|
|
|
|
Seed disposable data: 700 eligible tasks, 800 sessions, 5,000 turns, 650,000
|
|
messages, 2,200 commits, and five repositories in the measured workspace. Add a
|
|
second equally sized workspace to detect unnecessary cross-workspace scans.
|
|
Concentrate at least 200 turns and 10,000 messages in one session. Spread the
|
|
history over 400 days, including empty sessions and days without turns.
|
|
|
|
Use the same SQLite file shape, indexes, machine, and non-race binary before and
|
|
after optimization. Record CPU, memory, filesystem, driver, query plans, per-endpoint
|
|
latency, admission wait, and all-seven completion latency. Run ten measured warm
|
|
cycles after one warmup, for week/month/all ranges. The five-second p95 target
|
|
applies on a machine with at least four available CPU cores, 8 GiB RAM, and local
|
|
SSD storage, without unrelated load. Record cold results separately, without
|
|
claiming warm targets for cold caches. Also record a minimum fivefold improvement
|
|
for the heavy-join fixture; this is a delivery benchmark, not a fragile CI timer.
|
|
|
|
Normal tests assert numeric equivalence and concurrency bounds. Add opt-in Go
|
|
benchmarks with deterministic fixtures and report results in the work order.
|
|
Do not copy the user's database or transcripts into fixtures. A benchmark that
|
|
misses the target requires query work before the work order is done.
|
|
|
|
## Persistence and diagnostics
|
|
|
|
No new schema is required unless a measured index is necessary. Retain dual-engine
|
|
conformance. Actual missing tables and closed pools must still fail health and
|
|
recover through the existing probe rules. Do not increase health timeouts or
|
|
reader counts to hide overload. Pool exhaustion caused by other subsystems remains
|
|
outside this repair and must not be reported as solved.
|
|
|
|
Record bounded analytics operation names and elapsed admission/execution time in
|
|
existing structured logs on slow or failed calls. Do not log SQL, task titles,
|
|
workspace data, credentials, or query payloads. Request timings and health logs
|
|
provide the integration evidence; diagnostic bundles remain the collection path.
|