Skip to content

feat(telemetry): aggregate task completion telemetry with delta installments - #1071

Open
edelauna wants to merge 6 commits into
mainfrom
issue/830-3
Open

feat(telemetry): aggregate task completion telemetry with delta installments#1071
edelauna wants to merge 6 commits into
mainfrom
issue/830-3

Conversation

@edelauna

@edelauna edelauna commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Related GitHub Issue

Refs: #830 (task completion telemetry aggregation — split from #835; independent of #1069 and #1070)

Description

Replaces per-turn Conversation Message and Tool Used events with a single Task Completed installment per model-initiated attempt_completion call.

Delta invariants. Each installment carries only the delta in toolsUsed and messageCount since the prior installment for that task. Summing installments for a taskId reconstructs full-task totals without double-counting. An unchanged task produces no empty installment.

Completion reasons. completionReason is constrained to three values:

  • "attempt_completion" — the model called that tool (fires regardless of whether the user accepts, declines, or gives feedback)
  • "idle" — the task has been quiet for 30 minutes
  • "shutdown" — the task was disposed (panel closed, task switched, extension deactivated)

Idle and shutdown flushes. A 5-minute check interval fires an idle installment after 30 minutes of quiet. dispose() flushes any unreported activity as a shutdown installment. Neither path mutates task.toolUsage or task.messageCounts — those stay running totals for the public RooCodeEventName.TaskCompleted API event and the UI.

Stale replay suppression. When a user revisits a completed subtask from history, the handler runs again on a fresh Task instance with a zero baseline. The isStaleHistoryReplay flag (set when historyItem.status === "completed") prevents that replay from emitting a duplicate installment or a duplicate public TaskCompleted event.

Public API preserved. RooCodeEventName.TaskCompleted still represents genuine task completion or acceptance. The PostHog telemetry flush is independent of it and fires earlier (on every model-initiated attempt_completion).

Retry symmetry. messageCounts.user decrements when Task.ts pops a message on an empty assistant response, and restores exactly once on the next successful attempt via userMessageWasRemoved. The manual-retry branch now also sets userMessageWasRemoved: true (previously it did not, causing the message count to stay at zero on a user-approved retry). The decline-to-retry branch increments both messageCounts.user and messageCounts.assistant to match the messages it re-adds directly.

Scope

Included:

  • Task message and tool-usage counters for completion telemetry
  • Task Completed payload additions: completionReason, toolsUsed, messageCount
  • Telemetry installment on every model-initiated attempt_completion
  • Idle (30 min) and shutdown installments for abandoned/long-running tasks
  • Delta/baseline tracking so multiple installments do not double-count
  • Empty-assistant-response retry accounting fixes for accurate message counts
  • Stale completed-subtask replay suppression

Excluded (per scope):

Test Procedure

  • pnpm check-types passes repo-wide
  • pnpm lint passes with no suppression count increases
  • New and updated tests:
    • packages/telemetry/src/__tests__/TelemetryService.task-completed.test.tscaptureTaskCompleted signature (default reason, with summaries, idle reason)
    • src/core/task/__tests__/messageCounting.spec.tsshouldAddUserMessageToHistory predicate (7 cases)
    • src/core/task/__tests__/messageCounting.retrySymmetry.spec.ts — increment/decrement/re-increment symmetry across retry cycles
    • src/core/task/__tests__/Task.spec.tsflushTelemetryInstallment (first/second/no-op installments, failure deltas, idle timer, dispose flush, disposed-task timer stop)
    • src/core/tools/__tests__/attemptCompletionTool.spec.ts — attempt completion before acceptance, feedback-instead-of-acceptance, stale replay suppression, no duplicate public TaskCompleted on replay

Pre-Submission Checklist

  • Issue Linked: This PR is linked to an approved GitHub Issue (see "Related GitHub Issue" above).
  • Scope: My changes are focused on the linked issue (one major feature/fix per PR).
  • Self-Review: I have performed a thorough self-review of my code.
  • Testing: New and/or updated tests have been added to cover my changes.
  • Documentation Impact: No documentation updates required for this PR.
  • Contribution Guidelines: I have read and agree to the Contributor Guidelines.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The changes add incremental task telemetry. Tasks track message counts, report tool and message deltas during completion, idle periods, and shutdown, and preserve retry symmetry. Completion telemetry is separate from acceptance-dependent public completion events.

Changes

Task telemetry lifecycle

Layer / File(s) Summary
Completion telemetry contract
packages/telemetry/src/TelemetryService.ts, packages/telemetry/src/__tests__/TelemetryService.task-completed.test.ts
captureTaskCompleted now accepts tool usage, message-count deltas, and completion reasons. Tests cover default and explicit reasons and optional fields.
Message counting and task tracking
src/core/task/messageCounting.ts, src/core/task/Task.ts, src/core/task/__tests__/*
Tasks centralize user-message decisions, track message counts, preserve retry symmetry, and flush telemetry deltas on completion, idle, and shutdown.
Completion telemetry integration
src/core/tools/AttemptCompletionTool.ts, src/core/tools/__tests__/attemptCompletionTool.spec.ts
Validated live completions flush telemetry independently of user acceptance. Stale history replays avoid duplicate telemetry and public events.
Test fixture updates
src/__tests__/history-resume-delegation.spec.ts, src/__tests__/nested-delegation-resume.spec.ts, src/__tests__/task-run-dispatch.spec.ts
Task test doubles now provide the telemetry methods required by the updated task behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested labels: awaiting-review

Suggested reviewers: navedmerchant

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main telemetry aggregation and delta-installment change.
Description check ✅ Passed The description covers the linked issue, implementation details, scope, testing, and checklist, with appropriate omissions for non-UI changes.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue/830-3

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
src/core/tools/__tests__/attemptCompletionTool.spec.ts (1)

88-90: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider asserting on flushTelemetryInstallment instead of the payload this mock synthesizes.

The mock forwards mockTask.toolUsage and mockTask.messageCounts as-is, so the toolsUsed and messageCount arguments asserted throughout this file come from the mock, not from Task#flushTelemetryInstallment. Delta semantics are covered in Task.spec.ts. What this file needs to verify is that the tool calls the flush and passes "attempt_completion". Asserting expect(task.flushTelemetryInstallment).toHaveBeenCalledWith("attempt_completion") states that intent directly and stays correct if the payload shape changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/tools/__tests__/attemptCompletionTool.spec.ts` around lines 88 - 90,
Update the attempt-completion tests to assert directly on
task.flushTelemetryInstallment, verifying it is called with
"attempt_completion". Remove assertions on synthesized toolsUsed and
messageCount payloads from mockCaptureTaskCompleted, while preserving coverage
of the tool’s completion behavior.
src/core/task/Task.ts (1)

4777-4793: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The idle check re-fires every 5 minutes after the first idle flush, and lastTelemetryFlushAt never suppresses it.

lastMessageTs only advances on new activity. flushTelemetryInstallment updates lastTelemetryFlushAt, not lastMessageTs. After the first idle flush, idleForMs stays above IDLE_TELEMETRY_THRESHOLD_MS, so the interval calls flushTelemetryInstallment("idle") again every 5 minutes for the rest of the task's life. Each repeat no-ops on the empty-delta check, so no duplicate events are sent, but the comment at lines 4779-4783 states that this check "avoids waking up to do that check needlessly" — the condition never becomes false.

lastTelemetryFlushAt is also read only in the ?? fallback, so it has no effect whenever lastMessageTs is set. Compare against the later of the two timestamps so a completed flush actually resets the window.

♻️ Proposed fix
 	private startIdleTelemetryCheck(): void {
 		this.idleTelemetryCheckInterval = setInterval(() => {
-			// lastMessageTs only moves forward on activity, so comparing it against the
-			// last flush tells us whether anything happened since that flush -- if the
-			// task has been quiet since well before the last flush, there's nothing new
-			// to report and flushTelemetryInstallment's own empty-check would no-op anyway,
-			// but skipping here avoids waking up to do that check needlessly.
-			const idleForMs = Date.now() - (this.lastMessageTs ?? this.lastTelemetryFlushAt)
+			// Measure idleness from the later of the last activity and the last flush.
+			// Using lastMessageTs alone would keep this condition true forever after the
+			// first idle flush, re-running the empty-delta check on every interval tick.
+			const lastEventAt = Math.max(this.lastMessageTs ?? 0, this.lastTelemetryFlushAt)
+			const idleForMs = Date.now() - lastEventAt
 
 			if (idleForMs >= Task.IDLE_TELEMETRY_THRESHOLD_MS) {
 				this.flushTelemetryInstallment("idle")
 			}
 		}, Task.IDLE_TELEMETRY_CHECK_INTERVAL_MS)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/task/Task.ts` around lines 4777 - 4793, Update
startIdleTelemetryCheck to calculate idleForMs from the later of lastMessageTs
and lastTelemetryFlushAt, while preserving the fallback when neither timestamp
is set. Ensure a completed flush resets the idle threshold so the interval does
not repeatedly invoke flushTelemetryInstallment("idle") until new activity
occurs.
src/core/tools/AttemptCompletionTool.ts (1)

126-127: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

emitFinalTokenUsageUpdate() is already called inside flushTelemetryInstallment.

Task#flushTelemetryInstallment calls this.emitFinalTokenUsageUpdate() at Task.ts line 4769 before it captures the event. The explicit call here is redundant. The same duplication exists at lines 183-184. One difference: the internal call runs only when a delta exists, so removing the external calls also removes the forced token emit for a no-delta completion attempt. Decide which layer owns the responsibility and keep one call.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/tools/AttemptCompletionTool.ts` around lines 126 - 127, Remove the
redundant emitFinalTokenUsageUpdate call from the completion flow in
AttemptCompletionTool, including the duplicate occurrence near the other
reported location, and retain flushTelemetryInstallment("attempt_completion") as
the single ownership point. Verify the resulting behavior still matches the
intended no-delta token usage handling.
src/core/task/__tests__/Task.spec.ts (1)

3121-3124: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Dispose each created Task in afterEach so the 5-minute interval does not outlive its test.

createTask() starts a real setInterval in the Task constructor. Only the three dispose tests clear it. The other seven tests leave a live interval that holds the Task instance and can invoke the shared captureTaskCompletedSpy while a later test in this file is running. That makes the suite order-dependent.

♻️ Proposed fix
+	const createdTasks: Task[] = []
+
 	afterEach(() => {
+		for (const task of createdTasks) {
+			task.dispose()
+		}
+		createdTasks.length = 0
 		vi.useRealTimers()
 		captureTaskCompletedSpy.mockRestore()
 	})
 
 	function createTask() {
-		return new Task({
+		const task = new Task({
 			provider: mockProvider,
 			apiConfiguration: mockApiConfig,
 			task: "test task",
 			startTask: false,
 		})
+		createdTasks.push(task)
+		return task
 	}

dispose() is idempotent for the interval, so the three dispose tests remain correct.

As per path instructions, **/*.{test,spec}.{ts,tsx,js}: "Use package-local unit tests for pure logic, parsing, state transitions, ...".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/task/__tests__/Task.spec.ts` around lines 3121 - 3124, Update the
Task test cleanup in afterEach to dispose every Task instance created by
createTask before restoring timers and mocks. Track created tasks within the
test setup and call each task’s idempotent dispose method during teardown,
preserving the existing dispose-test behavior.

Source: Path instructions

src/core/task/__tests__/messageCounting.retrySymmetry.spec.ts (1)

28-41: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

These tests re-implement the Task.ts counter steps, so they cannot catch a regression in Task.ts.

simulatePopOnEmptyResponse and simulateDeclineRetry hard-code messageCounts.user-- and messageCounts.user++. Only the add decision comes from production code. If someone removes this.messageCounts.user-- at Task.ts line 3701, or the this.messageCounts.user++ at line 3769, these tests still pass. Consider driving the empty-response retry cycle through Task#recursivelyMakeClineRequests with a stubbed stream, or extract the counter mutations into messageCounting.ts so both sides share one implementation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/task/__tests__/messageCounting.retrySymmetry.spec.ts` around lines
28 - 41, Replace the hard-coded counter mutations in simulatePopOnEmptyResponse
and simulateDeclineRetry with tests that exercise the production
Task#recursivelyMakeClineRequests retry flow using a stubbed stream, or move the
shared counter mutations into messageCounting.ts and test that implementation
directly. Ensure the tests fail if Task removes either the user decrement for an
empty response or the user increment during declined retry.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/core/task/Task.ts`:
- Around line 3695-3701: Guard the decrement in the retry cleanup around
apiConversationHistory.pop() so messageCounts.user is reduced only when this
method previously incremented it for the removed message, preserving the
existing restoration behavior otherwise. Also update flushTelemetryInstallment
to clamp both user and assistant message-count deltas to zero or greater before
emitting telemetry.

---

Nitpick comments:
In `@src/core/task/__tests__/messageCounting.retrySymmetry.spec.ts`:
- Around line 28-41: Replace the hard-coded counter mutations in
simulatePopOnEmptyResponse and simulateDeclineRetry with tests that exercise the
production Task#recursivelyMakeClineRequests retry flow using a stubbed stream,
or move the shared counter mutations into messageCounting.ts and test that
implementation directly. Ensure the tests fail if Task removes either the user
decrement for an empty response or the user increment during declined retry.

In `@src/core/task/__tests__/Task.spec.ts`:
- Around line 3121-3124: Update the Task test cleanup in afterEach to dispose
every Task instance created by createTask before restoring timers and mocks.
Track created tasks within the test setup and call each task’s idempotent
dispose method during teardown, preserving the existing dispose-test behavior.

In `@src/core/task/Task.ts`:
- Around line 4777-4793: Update startIdleTelemetryCheck to calculate idleForMs
from the later of lastMessageTs and lastTelemetryFlushAt, while preserving the
fallback when neither timestamp is set. Ensure a completed flush resets the idle
threshold so the interval does not repeatedly invoke
flushTelemetryInstallment("idle") until new activity occurs.

In `@src/core/tools/__tests__/attemptCompletionTool.spec.ts`:
- Around line 88-90: Update the attempt-completion tests to assert directly on
task.flushTelemetryInstallment, verifying it is called with
"attempt_completion". Remove assertions on synthesized toolsUsed and
messageCount payloads from mockCaptureTaskCompleted, while preserving coverage
of the tool’s completion behavior.

In `@src/core/tools/AttemptCompletionTool.ts`:
- Around line 126-127: Remove the redundant emitFinalTokenUsageUpdate call from
the completion flow in AttemptCompletionTool, including the duplicate occurrence
near the other reported location, and retain
flushTelemetryInstallment("attempt_completion") as the single ownership point.
Verify the resulting behavior still matches the intended no-delta token usage
handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 610a96d6-9c0b-4461-b893-f1da90e47d3b

📥 Commits

Reviewing files that changed from the base of the PR and between cd29243 and a060887.

📒 Files selected for processing (9)
  • packages/telemetry/src/TelemetryService.ts
  • packages/telemetry/src/__tests__/TelemetryService.task-completed.test.ts
  • src/core/task/Task.ts
  • src/core/task/__tests__/Task.spec.ts
  • src/core/task/__tests__/messageCounting.retrySymmetry.spec.ts
  • src/core/task/__tests__/messageCounting.spec.ts
  • src/core/task/messageCounting.ts
  • src/core/tools/AttemptCompletionTool.ts
  • src/core/tools/__tests__/attemptCompletionTool.spec.ts

Comment thread src/core/task/Task.ts Outdated
@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.00000% with 14 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/core/task/Task.ts 73.07% 11 Missing and 3 partials ⚠️

📢 Thoughts on this report? Let us know!

@edelauna
edelauna marked this pull request as ready for review July 31, 2026 13:57

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/core/task/Task.ts`:
- Around line 3700-3707: Update the error-recovery flow around the history pop
and retry handling to track whether this iteration removed its own user message.
Only pop or restore the message, set the corresponding removal flag, and
increment messageCounts.user when shouldAddUserMessage indicates this iteration
added it; preserve prior persisted history for empty continuations such as
resumeAfterDelegation().
- Line 624: Move idle telemetry initialization from the current launch path into
a shared idempotent task-launch helper, preserving a single active
idleTelemetryCheckInterval. In src/core/task/Task.ts lines 624-624, update the
existing startup logic to use that helper; in src/core/task/Task.ts lines
1902-1902, invoke the same helper from start(), run(), and Task.create() before
startTask() or resumeTaskFromHistory().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b25a08e7-08dd-41f8-9bc5-920ac305f382

📥 Commits

Reviewing files that changed from the base of the PR and between a060887 and 39ab054.

📒 Files selected for processing (5)
  • src/__tests__/history-resume-delegation.spec.ts
  • src/__tests__/nested-delegation-resume.spec.ts
  • src/core/task/Task.ts
  • src/core/task/__tests__/Task.spec.ts
  • src/core/task/__tests__/messageCounting.retrySymmetry.spec.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/core/task/tests/Task.spec.ts
  • src/core/task/tests/messageCounting.retrySymmetry.spec.ts

Comment thread src/core/task/Task.ts
Comment thread src/core/task/Task.ts Outdated
@github-actions github-actions Bot added the awaiting-review PR changes are ready and waiting for maintainer re-review label Aug 1, 2026
@github-actions github-actions Bot removed the awaiting-review PR changes are ready and waiting for maintainer re-review label Aug 1, 2026
@edelauna edelauna changed the title feat(telemetry): aggregate task completion telemetry with delta insta… feat(telemetry): aggregate task completion telemetry with delta installments Aug 1, 2026
@github-actions github-actions Bot added the awaiting-review PR changes are ready and waiting for maintainer re-review label Aug 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting-review PR changes are ready and waiting for maintainer re-review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant