Skip to content

opencode.lines_of_code.count over-counts on multi-message sessions (adds cumulative totals instead of deltas) #43

@Jeevan1351

Description

@Jeevan1351

Summary

handleSessionDiff increments opencode.lines_of_code.count by fileDiff.additions/deletions on every session.diff event. But opencode publishes session.diff with the cumulative session diff (first snapshot → latest), not a per-event delta — so the counter accumulates cumulative totals and over-reports line counts on any session with more than one message.

Reproduction

  1. Install the plugin with OPENCODE_ENABLE_TELEMETRY=1 against a local OTLP collector (SigNoz, Prometheus, anything).
  2. Start an opencode TUI session.
  3. Ask the model to edit a file across multiple turns. Example: edit test.md six times in a row, ending with a state that differs from baseline by one line added and one line removed.
  4. Query the counter (e.g. in SigNoz ClickHouse):
    SELECT value FROM signoz_metrics.distributed_samples_v4
     WHERE metric_name='opencode.lines_of_code.count'
     ORDER BY unix_milli

Expected vs. actual

For the scenario above with only +1/-1 net change, opencode's session.diff fires after each message with cumulative totals like:

Event additions in cum diff counter .add() counter total Real session total
msg 1 1 +1 1 1
msg 2 1 +1 2 1
msg 3 1 +1 3 1
msg 4 0 (revert) +0 3 0
msg 5 1 +1 4 1
msg 6 1 +1 5 1

The counter ends at 5 but the real net is 1. Users summing the counter on dashboards see inflated numbers that grow with message count.

Root cause

src/handlers/activity.ts#L7-L40 treats each session.diff event as carrying a delta, but the event actually carries the cumulative diff of the whole session so far. packages/opencode/src/session/summary.ts in upstream opencode confirms this — it passes the same cumulative diffs array into bus.publish(Session.Event.Diff, { sessionID, diff: diffs }) every time it recomputes the summary.

Proposed fix

Track the last-seen cumulative per session and emit only the positive delta to the counter. This also enables a parallel Gauge that reports the current cumulative total (for "what has this session produced so far" panels).

export function handleSessionDiff(e: EventSessionDiff, ctx: HandlerContext) {
  let totalAdded = 0
  let totalRemoved = 0
  for (const d of e.properties.diff) {
    totalAdded += d.additions
    totalRemoved += d.deletions
  }
  const prev = ctx.sessionDiffTotals.get(e.properties.sessionID) ?? { additions: 0, deletions: 0 }
  ctx.sessionDiffTotals.set(e.properties.sessionID, { additions: totalAdded, deletions: totalRemoved })

  const delta = { added: totalAdded - prev.additions, removed: totalRemoved - prev.deletions }
  if (delta.added > 0)   ctx.instruments.linesCounter.add(delta.added,   { /*...*/, type: "added" })
  if (delta.removed > 0) ctx.instruments.linesCounter.add(delta.removed, { /*...*/, type: "removed" })

  // Optional companion gauge for "current cumulative"
  ctx.instruments.linesTotalGauge.record(totalAdded,   { /*...*/, type: "added" })
  ctx.instruments.linesTotalGauge.record(totalRemoved, { /*...*/, type: "removed" })
}

Cleanup on session.idle / session.error is symmetric to the existing sessionTotals map.

Backwards compatibility

This is a behaviour change for opencode.lines_of_code.count. Existing dashboards that sum() the counter will see smaller numbers (the correct numbers). Worth a BREAKING CHANGE: footer on the fix commit so release-please ticks a major version.

Known limitation

Even with this fix, the counter only captures churn that opencode actually publishes via session.diff. Rewrites that happen and then get reverted inside a single message don't generate events — opencode's summary recomputes against the session baseline, not step-by-step — so fine-grained "churn" would need to hook tool.execute.after instead. Not in scope for this bug.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions