Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .agent/tasks/gh-2429.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# GH-2429

**Created:** 2026-04-27

## Problem

GitHub Issue #2429: feat(memory): wire usage_events table — billing pipeline scaffolded but never called

## Problem

`usage_events` table is created on store init (`internal/memory/store.go:171-187`) and the `RecordUsageEvent` / `RecordExecutionUsage` writers exist in `internal/memory/metering.go`, but **no production code calls them**:

```bash
$ grep -rn "RecordUsageEvent\|RecordExecutionUsage" internal/ cmd/ --include="*.go" | grep -v "_test.go\|metering.go"
# (no results)
```

Result: `SELECT COUNT(*) FROM usage_events` → **0 rows**, ever. The billing/metering pipeline is dead code.

All cost data still lives in `executions.estimated_cost_usd` (legacy single-row-per-execution model), which:
- Doesn't separate task vs. token vs. compute vs. api_call event types (the whole reason `usage_events` was designed)
- Has no `user_id` (multi-tenancy can't aggregate per-user spend)
- Has no `metadata` field for per-event context (cache hit ratio, model variant, etc.)

## Suggested fix

In `internal/executor/dispatcher.go` worker loop (around line 669, where `SaveExecutionMetrics` is called):

```go
if result != nil && result.TokensTotal > 0 {
if err := w.store.RecordExecutionUsage(exec.ID, exec.UserID, exec.ProjectPath, result); err != nil {
w.log.Error("Failed to record usage event", slog.Any("error", err))
}
}
```

`RecordExecutionUsage` already exists at `metering.go:130-187` and emits 3 events per execution (task, token, compute). It just needs a caller.

## Why it matters

- Multi-user / team mode (per `team_members`, `teams` tables) needs per-user cost rollups. `usage_events.user_id` is the designed pivot.
- Future budget enforcement / billing export depends on this table.
- We currently can't answer "how much did GLM vs. Claude cost us this week" without parsing `executions.metadata`.

## Verification

After fix: run any task, then `SELECT * FROM usage_events ORDER BY timestamp DESC LIMIT 5` should show 3 rows per execution (task + token + compute event types).

## Scope

Small: ~1 call site in dispatcher + plumb `UserID` through `Execution` struct if missing. Tests already exist in `metering_test.go`.

## Refs

- `internal/memory/metering.go:110,130-187`
- `internal/memory/store.go:171-187`
- `internal/executor/dispatcher.go:669`

## Acceptance Criteria

4 changes: 2 additions & 2 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"hooks": [
{
"type": "command",
"command": "/var/folders/_8/nnsxrdhn46x08pb886hrggv00000gn/T/pilot-hooks-3723655428/pilot-bash-guard.sh"
"command": "/var/folders/_8/nnsxrdhn46x08pb886hrggv00000gn/T/pilot-hooks-4040757811/pilot-bash-guard.sh"
}
]
}
Expand All @@ -16,7 +16,7 @@
"hooks": [
{
"type": "command",
"command": "/var/folders/_8/nnsxrdhn46x08pb886hrggv00000gn/T/pilot-hooks-3723655428/pilot-stop-gate.sh"
"command": "/var/folders/_8/nnsxrdhn46x08pb886hrggv00000gn/T/pilot-hooks-4040757811/pilot-stop-gate.sh"
}
]
}
Expand Down
14 changes: 14 additions & 0 deletions internal/executor/dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,20 @@ func (w *ProjectWorker) processQueue(ctx context.Context) {
}); err != nil {
w.log.Error("Failed to save execution metrics", slog.Any("error", err))
}

// GH-2429: emit per-execution usage events (task + token + compute) so the
// `usage_events` table reflects real activity. UserID is single-tenant for
// now (empty); when multi-user lands, plumb the real ID through Execution.
if err := w.store.RecordTaskUsage(
exec.ID,
exec.UserID,
exec.ProjectPath,
duration.Milliseconds(),
result.TokensInput,
result.TokensOutput,
); err != nil {
w.log.Error("Failed to record usage event", slog.Any("error", err))
}
}

w.currentTaskID.Store("")
Expand Down
4 changes: 4 additions & 0 deletions internal/memory/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,10 @@ type Execution struct {
ID string
TaskID string
ProjectPath string
// UserID identifies the user/tenant that owns this execution.
// Empty in single-tenant deployments; populated when multi-user mode is enabled.
// Used as the pivot for `usage_events` aggregation (GH-2429).
UserID string
Status string
Output string
Error string
Expand Down
Loading