12 Commits

Author SHA1 Message Date
YYClaw
da4f278330 fix(usage): disambiguate model pricing by provider (MUL-3346)
Disambiguate client-side model pricing by provider: generic ids (e.g. `auto`) resolve ${provider}/${model} first, so they only price under their real provider instead of borrowing Cursor's rate. Provider is LOWER()-normalized on read and write so mixed-case historical rows merge.

Closes #4199. MUL-3346
2026-06-18 11:10:06 +08:00
LinYushen
5bacfd9742 MUL-2526 feat: add member(user_id, workspace_id) index + upgrade sqlc to v1.31.1 (#3046)
- Add migration 106: CREATE INDEX CONCURRENTLY on member(user_id, workspace_id)
- Rewrite ListWorkspaces to drive from member table with explicit fields
- Regenerate all sqlc code with v1.31.1 (intentional version upgrade)

Co-authored-by: multica-agent <github@multica.ai>
2026-05-22 12:26:56 +08:00
YYClaw
614dfae884 MUL-2488 feat(timezone): Scheduling / Viewing two-layer timezone architecture (#2968)
* docs(timezone): add scheduling/viewing timezone architecture RFC

* feat(db): replace daily rollups with task_usage_hourly, add user.timezone

Migrations 100-104: add "user".timezone (Viewing tz), build the UTC
hourly task_usage_hourly rollup with its pipeline, drop the legacy
task_usage_daily / task_usage_dashboard_daily pipelines, and drop the
agent_runtime.timezone column. Report queries now slice day boundaries
at read time by the caller-supplied @tz instead of materialising in a
fixed tz. Regenerate sqlc.

* feat(server): add task_usage_hourly backfill command

Replace the two legacy backfill commands (daily / dashboard_daily) with
a single backfill_task_usage_hourly that loads historical task_usage
into the new UTC hourly rollup, sliced per workspace.

* refactor(server): resolve viewing timezone in report handlers

Report handlers resolve the Viewing tz per request (?tz query param,
then user.timezone, then UTC) and pass it to the hourly-rollup queries.
Drop the UseDailyRollup feature flags and the old raw-scan/daily-rollup
dual paths, remove the /api/usage endpoints, and stop the daemon from
reporting and the runtime handler from accepting host timezone.

* refactor(core): switch report queries to viewing timezone

API client and dashboard/runtime queries send ?tz with each report
request, the user schema/types carry the new timezone field, and the
runtime timezone field/mutation is removed.

* feat(views): add viewing timezone preference and UI

Add the useViewingTimezone hook and a Timezone setting in Preferences;
report charts and the dashboard week boundary follow the viewer tz.
Remove the runtime detail timezone editor and its locale strings.

* fix(test): update fixtures and stabilize tests for timezone refactor

The timezone architecture refactor changed several types without
updating dependent test code:

- RuntimeDevice no longer has a timezone field — drop it from the
  create-agent-dialog runtime fixture.
- User now requires a timezone field — add it to the apps/web mockUser
  fixture.
- The PreferencesTab timezone tests asserted on the async save handler
  (PATCH then store update) with a bare expect, racing the mutation's
  settle callback, and timed out querying the Select's ~600-option IANA
  list on a loaded CI runner. Wrap the assertions in waitFor and extend
  the timeout for those three tests.

* docs(timezone): document self-host migration order and trigger invariant

Add a SELF-HOST UPGRADE ORDER runbook to the backfill command's package
comment: applying migrations 100-104 in a single migrate-up drops the
legacy daily rollups before the hourly backfill runs, leaving dashboards
empty until cron catches up.

Add an INVARIANT comment on trg_atq_dirty_hourly noting that agent_id
must be added to the trigger's OF list if it ever becomes mutable,
otherwise dirty buckets for the old agent_id are silently missed.

* style(runtimes): drop trailing blank line in runtime-detail
2026-05-21 15:33:47 +08:00
Multica Eve
d6349c16ec feat(runtime): per-runtime timezone for token-usage aggregation (MUL-1950) (#2394)
* feat: per-runtime timezone for token usage aggregation

The runtime token-usage charts (daily and hourly tabs on the
runtime-detail page) bucketed every event by the Postgres session
timezone, which is UTC in production. For an operator in UTC+8 that
meant a Tuesday afternoon's tasks landed in Tuesday early-morning's
bar — the chart was always one off.

Fix: store an IANA timezone on agent_runtime and aggregate under it.

* migrations 081 / 082 add agent_runtime.timezone (TEXT NOT NULL
  DEFAULT 'UTC') and rebuild the rollup pipeline (window function
  and both trigger functions) to compute bucket_date with
  AT TIME ZONE rt.timezone instead of bare DATE().
* No historical backfill — task_usage_daily rows already on disk
  keep their UTC bucket_date; only future writes / re-touches
  recompute under the new tz. (Product call from MUL-1950: 'guarantee
  future correctness'.)
* runtime_usage.sql gains a @tz parameter on ListRuntimeUsage and
  GetRuntimeUsageByHour and threads tz through GetRuntimeTaskHourly  Activity. ListRuntimeUsageDaily reads bucket_date as-is since the
  rollup already wrote it in tz.
* parseSinceParamInTZ replaces the raw N×24h cutoff with start-of-
  day-N in the runtime's tz so 'last 7 days' lines up with bucket
  boundaries.
* Daemon registration sends the host's IANA tz (TZ env, then
  time.Local), and UpsertAgentRuntime preserves any user override
  via a CASE-on-existing-value pattern so a daemon reconnect can't
  silently revert the operator's setting.
* New PATCH /api/runtimes/:id endpoint (UpdateAgentRuntime) lets
  the runtime detail page edit the tz; the editor seeds with the
  browser tz on first interaction.

Refs: MUL-1950

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix: harden runtime timezone rollups

Co-authored-by: multica-agent <github@multica.ai>

* fix: address runtime timezone review nits

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Eve <eve@multica.ai>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Eve <eve@multica-ai.local>
2026-05-11 14:39:35 +08:00
Multica Eve
eb067ff077 fix(server): aggregate task_usage into daily rollup table to cut DB load (#2256)
* fix(server): aggregate task_usage into daily rollup table to cut DB load

ListRuntimeUsage previously did a SUM(...) GROUP BY DATE(created_at), provider,
model over the raw task_usage stream once per runtime row on the runtimes
list and once per detail page load, scaling O(events) per call. This is the
hot read path responsible for sustained load on Postgres.

Switch the read path to a materialized daily rollup table maintained by a
pg_cron job:

- 072_task_usage_daily_rollup: schema for task_usage_daily +
  task_usage_rollup_state, plus rollup_task_usage_daily_window(p_from, p_to)
  (window primitive used by both cron and offline backfill, idempotent via
  ON CONFLICT DO UPDATE adding deltas) and rollup_task_usage_daily() (cron
  entry point — pg_try_advisory_lock(4242) for serialization, watermark
  advancement, 5-minute safety lag for late-visible inserts). Also adds
  idx_task_usage_created_at to help the two lazy endpoints
  (ListRuntimeUsageByAgent / GetRuntimeUsageByHour) that still hit the
  raw table.

- 073_task_usage_daily_pgcron: CREATE EXTENSION IF NOT EXISTS pg_cron in a
  DO/EXCEPTION block (mirrors the migration 032 pg_bigm pattern so envs
  without shared_preload_libraries=pg_cron skip gracefully) and schedules
  rollup_task_usage_daily() every 5 minutes when the extension is present.

- queries/runtime_usage.sql ListRuntimeUsage rewritten to read from
  task_usage_daily; sqlc regenerated. Other usage queries unchanged.

- cmd/backfill_task_usage_daily: one-shot Go command that walks
  task_usage in monthly slices through rollup_task_usage_daily_window,
  then stamps the watermark to now()-5m so the cron resumes cleanly.
  Run once after migrations have applied, before relying on the rollup.

- runtime_test.go: TestGetRuntimeUsage_BucketsByUsageTime now invokes
  rollup_task_usage_daily_window after fixture inserts so the handler
  sees the rolled-up rows. Synthetic daily rows cleaned up after each
  test.

- runtime_rollup_test.go: new tests covering aggregation correctness,
  idempotency contract of ON CONFLICT DO UPDATE, and the watermark
  advancing exactly to now()-5m via the cron entry point.

Deployment order: apply migrations → run backfill_task_usage_daily once
→ pg_cron picks up subsequent windows automatically. Today bucket may be
up to ~10 minutes stale (5 min cron + 5 min lag) by design.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(server): make task_usage_daily rollup safe to overlap, replay, and correct

Addresses 4 review blockers on the original PR:

1. Cron/backfill double-count race: the rollup function is now idempotent.
   Window calls find DIRTY KEYS via task_usage.updated_at, then RECOMPUTE
   each bucket from ground truth and REPLACE the daily row (no more
   additive ON CONFLICT). Cron and backfill can now overlap safely.

2. Silent pg_cron absence: the read path is gated behind a new
   USAGE_DAILY_ROLLUP_ENABLED feature flag (default off). The raw
   task_usage scan is preserved as the fallback. Operators flip the
   flag per-environment after backfill + cron are confirmed healthy
   (task_usage_rollup_lag_seconds() helper added for monitoring).

3. UpsertTaskUsage corrections invisible to rollup: added
   task_usage.updated_at column (default now(), backfilled from
   created_at), and bumped it on conflict. Corrections now mark the
   bucket dirty and the next window call recomputes it correctly.

4. CREATE INDEX blocking writes on hot table: split into separate
   single-statement migrations using CREATE INDEX CONCURRENTLY
   (074, 075), matching the 035/067 pattern.

Also: cron.schedule() removed from migrations entirely. Migration 076
only enables the extension (gracefully on unsupported envs); the actual
schedule is a documented operator runbook step that runs AFTER backfill.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(server): trigger-driven invalidation + online-safe migration for task_usage_daily

Round-2 review feedback on PR #2256:

1. Add explicit dirty-bucket queue (task_usage_daily_dirty) populated by
   triggers on agent_task_queue (UPDATE OF runtime_id, DELETE) and
   task_usage (DELETE). The rollup window function drains both this queue
   and the updated_at-based discovery, so runtime reassignment and
   issue-cascade deletes no longer leave the rollup divergent from the
   raw query.

   Triggers join via agent (not issue) to look up workspace_id, because
   when the cascade comes from issue, the issue row is already gone by
   the time atq's BEFORE DELETE fires; agent stays alive.

2. Make migration 072 online-safe: only ADD COLUMN updated_at TIMESTAMPTZ
   (nullable, no default → metadata-only ALTER, no row rewrite) and a
   separate ALTER for SET DEFAULT now() (also metadata-only). No bulk
   UPDATE on the hot task_usage table. The rollup window function's
   dirty_keys CTE handles legacy NULL rows via an OR branch, supported
   by partial index idx_task_usage_created_at_legacy.

3. Refresh stale documentation in cmd/backfill_task_usage_daily/main.go
   header to describe the current recompute/replace semantics, idempotent
   re-runnability, and the actual migration numbering (072..077).

Tests:
- TestRollupTaskUsageDaily_InvalidationOnReassign: verifies usage moves
  between runtime buckets after ReassignTasksToRuntime-style update.
- TestRollupTaskUsageDaily_InvalidationOnIssueDelete: verifies daily
  bucket is cleared after issue delete cascades through atq → task_usage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(server): close dirty-queue race + move legacy partial index to its own concurrent migration

Round-3 review feedback on PR #2256:

1. Blocker: dirty-queue invalidations could be silently lost under
   concurrency. ON CONFLICT DO NOTHING let a late trigger see the row
   already enqueued, no-op, and then the rollup drain (WHERE
   enqueued_at < p_to) would delete the original row — losing the
   late invalidation. Switched all three trigger enqueue paths to
   ON CONFLICT DO UPDATE SET enqueued_at = GREATEST(existing,
   EXCLUDED.enqueued_at), so any invalidation arriving during a
   rollup tick keeps enqueued_at > p_to (p_to = now() - 5min) and
   survives the post-tick drain.

2. High: idx_task_usage_created_at_legacy (partial index on hot
   task_usage table) was being created in the regular 077 migration
   without CONCURRENTLY. Moved to new migration 078 with
   CREATE INDEX CONCURRENTLY, matching the pattern of 074/075.
   077's down migration leaves the index alone (it is owned by 078).

3. Minor: gofmt -w on runtime_rollup_test.go and
   backfill_task_usage_daily/main.go (tabs were lost in the original
   heredoc append). PR description rewritten to describe the current
   recompute/replace + dirty queue + feature flag design and the
   072..078 migration ordering.

Tests still green: TestRollupTaskUsageDaily_* (including both new
invalidation regressions), TestGetRuntimeUsage_*, TestWorkspaceUsage_*.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(server): unify workspace_id source via agent in rollup window function

Round-4 review feedback (J) on PR #2256:

M1 (must-fix): The dirty queue triggers resolved workspace_id via
`agent.workspace_id`, but the window function's `dirty_from_updates`
discovery and `recomputed` recompute join used `issue.workspace_id`.
There is no schema-level FK guaranteeing
`agent.workspace_id == issue.workspace_id`. Any divergence (future
cross-workspace task scenarios, data repairs, migration bugs) would
cause:

  - dirty queue rows with workspace_id from agent
  - recompute join filtering by workspace_id from issue
  - 0 matches in recompute → bucket erroneously hits the
    deleted_empty branch and the daily row is silently dropped
  - dirty_from_updates path attributing usage to the wrong workspace

Replaced both CTEs to JOIN agent (not issue) so trigger / discovery /
recompute share one workspace_id source. Comment in 077 explains the
constraint.

N1: Refreshed two stale references in
cmd/backfill_task_usage_daily/main.go (header now says "072..078";
stampWatermark warning now mentions migration 073, where the rollup
state table is actually introduced).

Test: New TestRollupTaskUsageDaily_WorkspaceMismatch constructs an
atq with agent.workspace_id != issue.workspace_id, asserts the bucket
lands under agent's workspace (not issue's), and re-asserts after a
runtime reassign in the foreign workspace. Acts as a canary if the
schema invariant changes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Eve <eve@multica.ai>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Devv <devv@Devvs-Mac-mini.local>
2026-05-08 15:35:21 +08:00
Naiyuan Qing
21e3cfaa01 Agent runtime status redesign: split presence into availability + last-task (#1794)
* feat(agent-status): add workspace live-tasks endpoint and TaskFailureReason type

Lays the API + type contract for the front-end agent presence cache:

- New `GET /api/active-tasks` returns active (queued/dispatched/running)
  tasks plus failed tasks within the last 2 minutes for the current
  workspace. The 2-minute window powers a UI-side auto-clearing "Failed"
  agent state without back-end pollers.
- `agent_task_queue` has no workspace_id column, so the query JOINs agent;
  `SELECT atq.*` keeps `failure_reason` (migration 055) on the wire.
- Adds `TaskFailureReason` to `AgentTask` so the UI can map the 5 backend
  classifiers (agent_error / timeout / runtime_offline / runtime_recovery
  / manual) to copy without parsing free-text errors.
- New `api.getActiveTasksForWorkspace()` client method; workspace is
  resolved server-side from the X-Workspace-Slug header (no path param,
  matching /api/agents and /api/runtimes conventions).

Includes the joint engineering plan and designer brief that scope the
broader Agent / Runtime status redesign — Phase 0 is this contract plus
the front-end derivation layer landing in the next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(agent-status): derive presence/health states with WS sync and desktop IPC bridge

Adds the front-end derivation layer that turns raw server data into the
user-facing 5-state agent / 4-state runtime enums. UI files are
deliberately untouched in this commit — derivation lives behind hooks
(useAgentPresence, useRuntimeHealth) that any component can call with
zero additional network traffic.

Architecture:
- Derivation is pure functions in packages/core/{agents,runtimes}; the
  back-end stays free of UI translation. Agents algorithm: runtime
  offline > recent failed (2-min window) > running > queued > available.
  Runtimes algorithm: status + last_seen_at -> online / recently_lost /
  offline / about_to_gc.
- A single workspace-wide active-tasks query backs all per-agent
  presence reads, eliminating N+1 across hover cards, list rows, and
  pickers. 30-second tick re-renders the hooks so the failed window
  expires even when no underlying data changes.
- WS task lifecycle events (dispatch / completed / failed / cancelled)
  invalidate active-tasks via the prefix dispatcher. completed/failed
  were removed from specificEvents so they go through both the prefix
  invalidate and the existing chat ws.on() handlers. Reconnect refetch
  picks up active-tasks too.
- Desktop bridges window.daemonAPI.onStatusChange directly into the
  runtimes cache via setQueryData, giving the local daemon sub-second
  feedback (vs. 75s server sweep). Bridge is wsId-bound so workspace
  switches automatically rebind the subscription; daemon_id matching
  covers the same-daemon-multiple-providers case.

24 derivation unit tests cover all branches plus null/empty/boundary
inputs (FAILED_WINDOW_MS edges, null last_seen_at, missing
completed_at). Full core suite: 112 tests passing. Typecheck green
across all 8 workspace packages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(agent-status): redesign agent runtime status as two orthogonal dimensions

Splits the conflated 5-state agent presence into two independent axes:

- AgentAvailability (3-state): online / unstable / offline — drives the
  dot indicator everywhere a dot appears. Pure runtime reachability;
  never sticky-red because of a past task outcome.

- LastTaskState (5-state): running / completed / failed / cancelled /
  idle — surfaced as text + icon on focused surfaces (hover card,
  agent detail page, agents list, runtime detail). Never colours the dot.

Major changes:

* Domain layer: AgentPresence union → AgentAvailability + LastTaskState.
  derive-presence split into deriveAgentAvailability + deriveLastTaskState
  + deriveAgentPresenceDetail orchestrator. Tests reorganised into three
  groups (availability invariants, last-task invariants, composition).

* Visual config: presenceConfig (5 entries) → availabilityConfig (3) +
  taskStateConfig (5). availabilityOrder + lastTaskOrder for filter chips.

* Workspace-level presence prefetch: new useWorkspacePresencePrefetch
  hook + WorkspacePresencePrefetch mount component, wired into
  DashboardLayout (web) and WorkspaceRouteLayout (desktop). Hover cards
  render synchronously with no skeleton flash on first hover.

* ActorAvatar hover: flipped default — disableHoverCard removed,
  enableHoverCard added (default false). Opt-in at ~14 decision-moment
  surfaces; pickers / decoration sub-chips stay plain. Status dot
  decoupled (showStatusDot prop) so picker rows can show presence
  without nesting popovers.

* Hover cards: AgentProfileCard simplified — availability dot only,
  Detail link top-right (logs live on the detail page). New
  MemberProfileCard mirrors the structure: name + role + email +
  top-2 owned agents (sorted by 30d run count) with click-through to
  agent detail.

* Agents list: split Status into two columns — availability (3-color
  dot + label) and Last run (task icon + label, optional running
  counts). Two independent filter chip groups (Status + Last run);
  combination acts as intersection ("online + failed" finds broken-
  but-alive agents).

* Other UI surfaces (issue list/board/detail, comments, autopilots,
  projects, runtimes, mention autocomplete, subscribers picker)
  updated to the new dot semantics; status dot now strictly 3-color.

Server changes accompany the client redesign — workspace-wide
agent-task-snapshot endpoint, runtime usage queries, etc. — to feed
the derive layer with the data it needs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(agent-detail): drop last-task chip from detail header + inspector

The Recent work section on the agent detail page already shows the same
data (with task titles, timestamps, error context) — surfacing
"Completed" / "Failed" / etc. up in the header was redundant chrome.
Detail surfaces now show only the 3-state availability dot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(tables): handle narrow viewports across agents / skills / runtimes

Three table layouts were squeezing content into adjacent cells at
intermediate widths. Each fix is small and targeted:

* runtime-list: the Runtime cell's base name had `shrink-0`, so it
  refused to truncate when its grid column was narrowed under width
  pressure — the name visually overflowed into the Health column
  ("ClaudeOnline" etc). Removed shrink-0, added truncate. The Health
  column was also a fixed 9.5rem reservation for the worst-case
  "Recently lost · 2m 14s ago" copy; switched to minmax(0,1fr) so it
  competes fairly with Runtime.

* skills-page: had a single grid template with no responsive
  breakpoints — all 6 columns were rendered at any width and got
  visually jammed below md. Added a <md template that drops Source +
  Updated; the row markup hides those cells via `hidden md:block` /
  `md:contents`.

* agent-list-item: the new Last run column was reserved at minmax(8rem,
  max-content); on narrow md viewports the 8rem floor pushed the row
  past available width. Changed to minmax(0,max-content) so the cell
  shrinks under pressure (its content already truncates).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(agent-card): hover-only Detail + add Runtime row + breathing room

Three small polish tweaks to the agent hover card:

- Detail link gets `mr-1` + fades in only on card hover (group-hover).
  It was visually flush against the popover edge and competing for
  attention; now it stays out of the way during a quick glance and
  surfaces only when the user is dwelling on the card.

- Runtime row is back, in the meta block (cloud/local icon + runtime
  name). The earlier removal was over-aggressive — knowing where an
  agent runs is part of "who is this agent". The wifi badge stays
  dropped because the availability dot in the header already conveys
  reachability.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(runtime): wifi-style health icon (4-state) for runtime list + agent card

Replaces the 6px coloured dot with a wifi-shape icon that carries both
state (Wifi vs WifiOff) and severity (success/warning/muted/destructive).

Mapping:
- online        → Wifi (success)
- recently_lost → WifiHigh (warning) — transient hiccup, fewer bars
- offline       → WifiOff (muted)    — long unreachable
- about_to_gc   → WifiOff (destructive) — sweeper coming soon

Used in two places:

- Runtime list: replaces HealthDot in the dedicated leading-icon column.
  Bumped the column from 0.5rem (dot-sized) to 0.875rem (icon-sized).

- Agent profile card RuntimeRow: derives runtime health from runtime +
  clock (matching the 4-state semantics) and renders HealthIcon next
  to the runtime name. Cloud runtimes always read as online. The
  duplicate signal with the header availability dot is intentional —
  it confirms WHICH runtime is the one currently in the dot's state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:21:13 +08:00
Bohan Jiang
a36252ca99 refactor(runtime): derive runtime usage from task_usage only (#1167)
* refactor(runtime): derive runtime usage from task_usage only

The daemon used to scan each runtime's local CLI log directory every 5
minutes (Claude Code, Codex, OpenCode, OpenClaw, Hermes) and post daily
aggregates to /api/daemon/runtimes/{id}/usage. Those directories are
shared with the user's own local CLI sessions, so the user's personal
usage was being counted as Daemon-executed usage. Cursor and Gemini had
no scanner at all, so their runtime-level aggregates were always zero.

Switch GetRuntimeUsage to aggregate task_usage (already scoped to
Daemon-executed tasks) via agent_task_queue.runtime_id. Single source of
truth; Cursor/Gemini/Copilot get runtime usage for free; no reliance on
external CLI log formats.

Removes:
- server/internal/daemon/usage/ (all scanners)
- Daemon.usageScanLoop + providerToRuntimeMap
- Client.ReportUsage
- ReportRuntimeUsage handler + POST /api/daemon/runtimes/{id}/usage
- UpsertRuntimeUsage / GetRuntimeUsageSummary queries
- runtime_usage table (migration 046)

Refs: MUL-786

* fix(runtime): bucket daily usage by task_usage.created_at, not enqueue time

ListRuntimeUsage was aggregating by DATE(atq.created_at) and filtering
on atq.created_at. agent_task_queue.created_at is the enqueue timestamp,
which drifts from actual token-production time: a task queued at 23:58
and executed at 00:05 was attributed to yesterday; a task sitting in
the queue overnight was counted on the queue day.

The ?days=N cutoff also became a rolling window (now() - N) instead of
a calendar-day boundary, silently clipping the morning of the earliest
day returned.

Switch bucket + filter to task_usage.created_at (~= task completion /
usage-report time) and snap the since cutoff to start-of-day via
DATE_TRUNC.

Add a regression test covering both scenarios: cross-midnight task
attributes to the day tokens were reported, and the earliest day's
pre-cutoff rows are still included.
2026-04-16 18:54:12 +08:00
Jiayuan Zhang
ff5f6ac2ee fix(daemon): prevent duplicate runtime registration on profile switch (#906)
* fix(daemon): prevent duplicate runtime registration on profile switch

The daemon_id included a profile name suffix (e.g. "hostname-staging"),
so switching profiles created a new daemon_id that bypassed the UPSERT
dedup constraint, leaving orphaned runtime records in the database.

Three changes:
- Remove profile suffix from daemon_id — use stable hostname only.
  The unique constraint (workspace_id, daemon_id, provider) already
  prevents collisions within the same workspace.
- Auto-migrate agents from old offline runtimes to the newly registered
  runtime during DaemonRegister (same workspace/provider/owner).
- Add TTL-based GC in the runtime sweeper to delete offline runtimes
  with no active agents after 7 days.

Closes MUL-695

* fix(daemon): address code review issues on PR #906

1. Move gcRuntimes() to the main sweep loop — previously it was inside
   sweepStaleRuntimes() after an early return, so it only ran when new
   runtimes were marked stale. Now it runs every sweep cycle independently.

2. Fix DeleteStaleOfflineRuntimes to exclude runtimes with ANY agent
   reference (not just active ones). The FK agent.runtime_id is ON DELETE
   RESTRICT, so archived agents also block deletion.

3. Scope MigrateAgentsToRuntime to the same machine by matching
   daemon_id LIKE '<current_daemon_id>-%'. This prevents cross-machine
   agent migration when the same user has multiple devices.
2026-04-14 01:52:34 +08:00
Jiang Bohan
7312b5650c fix(server): fix ListRuntimeUsage to filter by date range instead of row count (#765)
Replace LIMIT $2 with AND date >= $2 in ListRuntimeUsage query. When a
runtime uses multiple models each day has multiple rows, so a row LIMIT
silently returns fewer days than requested.

Also fixes displayName warnings in issue-detail test mocks and adds
missing setOpen to useCallback deps in search-command.

Co-authored-by: jayavibhavnk <jaya11vibhav@gmail.com>
Closes #731
2026-04-12 22:46:07 +08:00
Jiayuan
36798b1d76 feat(runtimes): add usage charts, activity heatmap, and hourly distribution
Add comprehensive data visualization to the runtime detail page:
- Daily token usage stacked area chart and daily cost bar chart
- Model distribution donut chart with cost breakdown
- GitHub-style activity heatmap (13 weeks of daily token usage)
- Hourly task distribution bar chart with new backend endpoint
- Responsive 2-column grid layout for charts on wide screens

Backend: new GET /api/runtimes/{runtimeId}/activity endpoint
returning hourly task counts from agent_task_queue.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 15:44:10 +08:00
yushen
83111761db feat(workspace): add repos JSONB field for GitHub repository URLs
Add a `repos` JSONB column to the workspace table for storing
associated repository URLs and descriptions. This enables the daemon
to clone repos and set up git worktrees for agent task execution.

Structure: [{"url": "https://github.com/org/repo", "description": "..."}]

- Migration 014: adds repos column with default '[]'
- UpdateWorkspace query: supports updating repos
- Workspace API: returns repos in GET, accepts in PUT

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 15:49:30 +08:00
Jiayuan
903fbee55d feat(runtimes): add Runtimes tab with usage tracking and connection test
Add a new "Runtimes" sidebar tab to manage local agent runtimes with three
main capabilities: runtime status overview, token usage tracking (reading
Claude Code and Codex CLI local JSONL logs via daemon), and an interactive
connection test that sends a ping through the daemon to verify end-to-end
agent CLI connectivity.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 18:28:36 +08:00