Skip to content

feat(ai): record AI resource consumption per backend call with Matrix sync#3403

Merged
matthiasn merged 5 commits into
mainfrom
feat/melious_impact_stats
Jul 5, 2026
Merged

feat(ai): record AI resource consumption per backend call with Matrix sync#3403
matthiasn merged 5 commits into
mainfrom
feat/melious_impact_stats

Conversation

@matthiasn

@matthiasn matthiasn commented Jul 4, 2026

Copy link
Copy Markdown
Owner

What

Adds a dedicated data layer that captures one immutable row per AI backend call — tokens, cost (credits), energy, CO₂, water, data center and renewable share — tagged with the owning task/category/entry and the causal parent call. This is the storage + capture + aggregation-query layer only; no dashboard UI is built here (only the queries a later dashboard consumes).

Impact values come from Melious (the default provider), which returns them per call. Verified against the reference project: Melious exposes environment_impact and billing_cost only on non-streaming responses, so measured Melious calls now post stream:false. Streaming is considered irrelevant for these use cases, so the loss of incremental display is a non-issue.

How

Storage & aggregation (new module lib/features/ai_consumption/)

  • ConsumptionDatabase on ai_consumption.sqlite — a separate DB so diagnostics data never bloats the existing databases. Blob-plus-projection: a serialized JSON sync source-of-truth plus denormalized typed columns + indexes purely for aggregation (mirrors AgentDatabase/agent_entities).
  • ConsumptionRepository, pure-Dart bucketing (reuses the insights epoch-day/day-start machinery), and aggregation models: per-task lifetime totals + per-category time-bucketed series.
  • AiConsumptionEvent freezed entity + response-type enum.

Matrix sync

  • New inline SyncMessage.consumptionEvent, appended consumptionEvent payload type (ordinal appended last — persisted), outbox enqueue/priority/apply wiring, inbound dominance handler, backfill resolvers, and DI wiring. Append-only and idempotent (insertOnConflictUpdate), so no concurrent-merge machinery is needed.

Melious non-streaming impact

  • Parses impact via an InferenceImpactCollector side-channel that mirrors the existing ThoughtSignatureCollector threading through the dispatch chain — no behavior change for other providers.

Capture wiring (all guarded on getIt.isRegistered<AiConsumptionRecorder>() so wakes carry zero overhead when tracking is off)

  • Per-turn agent consumption inside ConversationRepository.sendMessage, wired through every agent workflow (task, project, event, day, template/soul evolution).
  • Skill runner transcription, image analysis, image generation and prompt generation, plus the unified inference path.
  • AiConsumptionRecorder never throws, so a recorder failure can never fail an inference. Non-Melious providers still get a row (tokens set, impact null).

Testing

  • dart analyze lib test → zero issues; dart format clean.
  • All touched suites green (1207 tests): consumption/impact/sync-consumption, Melious/cloud-inference/interface/conversation, skill/unified/eval, task-agent workflow, event/project/soul/template/day workflows, and the sync sequence/processor/outbox/backfill/ui suites.

Notes

  • Architecture-first ai_consumption/README.md with Mermaid diagrams for capture flow, sync flow, and aggregation.
  • No user-visible strings beyond the localized syncPayloadConsumptionEvent payload label.
  • No CHANGELOG entry: the data layer is runtime-invisible and the streaming→non-streaming switch is a deliberate, non-user-noticeable change.

Summary by CodeRabbit

  • New Features
    • Added optional AI consumption tracking across conversation sends, agent/workflow runs, and skill/tool invocations, including per-turn token metrics and environmental/cost impact when available.
    • Introduced a persistent consumption event store with syncing, replay handling, and backfill support, plus a new “AI consumption” sync payload label.
  • Bug Fixes
    • Preserved AI consumption attribution during forced retry/update_report flows.
  • Documentation
    • Added a dedicated guide covering AI consumption tracking architecture and reporting.

… sync

Add a dedicated data layer that captures one immutable row per AI backend
call — tokens, cost (credits), energy, CO2, water, data center and
renewable share — tagged with the owning task/category/entry and the causal
parent call, so lifetime per-task and time-bucketed per-category aggregates
can be derived later.

Storage & aggregation (new feature module lib/features/ai_consumption/):
- ConsumptionDatabase on ai_consumption.sqlite (blob-plus-projection: a
  serialized JSON sync source-of-truth plus denormalized typed columns and
  indexes purely for aggregation), ConsumptionRepository, bucketing logic
  and aggregation models reusing the insights epoch-day/day-start machinery.
- AiConsumptionEvent freezed entity + response-type enum.

Matrix sync:
- New inline SyncMessage.consumptionEvent, appended consumptionEvent payload
  type, outbox enqueue/priority/apply wiring, inbound dominance handler,
  backfill resolvers and DI wiring. Append-only and idempotent
  (insertOnConflictUpdate), so no concurrent-merge machinery is needed.

Melious non-streaming impact:
- environment_impact and billing_cost are only present on non-streaming
  responses, so measured Melious calls now post stream:false and surface the
  parsed impact via an InferenceImpactCollector side-channel that mirrors the
  existing ThoughtSignatureCollector threading through the dispatch chain.

Capture wiring (all guarded on getIt.isRegistered<AiConsumptionRecorder>()
so wakes carry zero overhead when tracking is off):
- Per-turn agent consumption inside ConversationRepository.sendMessage, wired
  through every agent workflow (task, project, event, day, template/soul
  evolution).
- Skill runner transcription, image analysis, image generation and prompt
  generation, plus the unified inference path.

AiConsumptionRecorder never throws, so a recorder failure can never fail an
inference. Includes an architecture-first README with Mermaid diagrams and
tests for storage, aggregation, sync round-trip, impact parsing and capture.
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: de2e6a6c-6ab8-4a27-99e6-f521afdb2956

📥 Commits

Reviewing files that changed from the base of the PR and between e9d2244 and 9d8f792.

📒 Files selected for processing (1)
  • test/features/agents/workflow/event_agent_workflow_test.dart

📝 Walkthrough

Walkthrough

This PR adds AI consumption tracking end to end: new impact and event models, persistence and sync plumbing, inference impact collection, workflow attribution, and consumptionEvent handling across sync, backfill, outbox, UI, localization, and tests.

Changes

AI Consumption Tracking

Layer / File(s) Summary
Consumption model and aggregation types
lib/features/ai/model/ai_call_impact.dart, lib/features/ai/repository/ai_consumption_mapping.dart, lib/features/ai_consumption/model/*, lib/features/ai_consumption/logic/consumption_bucketing.dart, test/features/ai/model/ai_call_impact_test.dart, test/features/ai_consumption/*
Adds impact and consumption domain types, aggregation helpers, bucketization, and coverage for parsing, equality, and bucketing.
Consumption database, repository, and recorder
lib/features/ai_consumption/database/*, lib/features/ai_consumption/repository/consumption_repository.dart, lib/features/ai_consumption/sync/consumption_sync_service.dart, lib/features/ai_consumption/consumption/ai_consumption_recorder.dart, lib/features/ai_consumption/README.md, test/features/ai_consumption/database/*, test/features/ai_consumption/repository/*, test/features/ai_consumption/sync/*
Adds the consumption database, repository, sync service, recorder facade, documentation, and persistence/sync tests.
Inference impact collection
lib/features/ai/repository/*, lib/features/ai/services/skill_inference_runner.dart, test/features/ai/repository/*, test/features/ai/services/skill_inference_runner_test.dart, test/features/ai/eval/*
Threads InferenceImpactCollector through inference paths, captures Melious impact data, and records consumption at completion.
Workflow attribution
lib/features/ai/conversation/conversation_repository.dart, lib/features/agents/workflow/*, lib/features/daily_os_next/agents/workflow/day_agent_workflow.dart, test/features/agents/workflow/*, test/features/daily_os_next/agents/workflow/day_agent_workflow_test.dart
Passes consumption owner identifiers through conversation turns and agent workflows, with matching test updates.
Sync, backfill, outbox, and UI
lib/features/sync/model/*, lib/features/sync/matrix/*, lib/features/sync/outbox/*, lib/features/sync/backfill/*, lib/features/sync/sequence/sync_sequence_payload_type.dart, lib/features/sync/queue/queue_apply_adapter.dart, lib/features/sync/ui/view_models/outbox_list_item_view_model.dart, test/features/sync/*
Adds consumptionEvent sync plumbing, outbox handling, backfill support, matrix stats, queue classification, and UI labeling.
DI, localization, notifications, and mocks
lib/get_it.dart, lib/l10n/*, lib/services/db_notification.dart, test/mocks/mocks.dart, test/helpers/fallbacks.dart
Registers consumption services, adds localized strings and notification tokens, and wires test mocks/fallbacks.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: recording AI consumption per backend call with Matrix sync support.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/melious_impact_stats

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.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces the ai_consumption module, which records token usage, cost, and environmental impact (energy, CO2, water) for every AI backend call into a dedicated, append-only SQLite database. It integrates this tracking across legacy and modern AI call sites, including agent workflows and skill runners, and implements out-of-band impact capture for Melious non-streaming responses. Additionally, it integrates these events into the Matrix sync pipeline as inline payloads with vector-clock dominance checks and backfill support. A review comment identifies a potential runtime type error in outbox_enqueue_writer.dart where a nullable vectorClock is added to a list passed to VectorClock.mergeUniqueClocks, suggesting a collection-if guard to prevent null insertion.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread lib/features/sync/outbox/outbox_enqueue_writer.dart
@codecov

codecov Bot commented Jul 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.20%. Comparing base (ddb8e10) to head (9d8f792).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff            @@
##             main    #3403    +/-   ##
========================================
  Coverage   99.19%   99.20%            
========================================
  Files        1693     1704    +11     
  Lines      120257   120821   +564     
========================================
+ Hits       119293   119859   +566     
+ Misses        964      962     -2     
Flag Coverage Δ
glados 15.15% <22.25%> (+0.03%) ⬆️
standard 98.93% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
lib/features/ai/services/skill_inference_runner.dart (1)

731-766: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Consumption recording can be skipped after a costly call if the linked task disappears mid-flight.

generateImage (a billed/impactful call) runs at Line 733, but _recordConsumption isn't reached until Line 754 — after taskEntity is re-fetched at Line 743 and validated at Line 746. If the linked task is deleted between kicking off generation and this fetch, the StateError at Line 747-750 propagates and the consumption/cost event for the already-completed call is silently lost (unlike runTranscription/runImageAnalysis/runPromptGeneration, where the source entity is fetched before the inference call).

Consider recording consumption immediately after generateImage returns, before the post-generation task-existence check, so a downstream StateError doesn't erase billing/impact data for a call that already happened.

💡 Suggested reordering
-        // 6. Verify linked task still exists and get its category.
-        final taskEntity = await _journalRepository.getJournalEntityById(
-          linkedTaskId,
-        );
-        if (taskEntity is! Task) {
-          throw StateError(
-            'Linked task $linkedTaskId not found before cover art save',
-          );
-        }
-
-        // Image generation is a single request (no token stream), so tokens are
-        // null; impact comes from the collector for Melious.
-        await _recordConsumption(
-          entryId: entryId,
-          taskId: linkedTaskId,
-          categoryId: taskEntity.meta.categoryId,
-          skillId: skill.id,
-          provider: provider,
-          modelId: modelId,
-          responseType: skill.skillType.toResponseType,
-          usage: null,
-          impact: impactCollector.impact,
-          start: start,
-        );
+        // 6. Verify linked task still exists and get its category.
+        final taskEntity = await _journalRepository.getJournalEntityById(
+          linkedTaskId,
+        );
+
+        // Image generation is a single request (no token stream), so tokens
+        // are null; impact comes from the collector for Melious. Record
+        // regardless of task-lookup outcome so a deleted task doesn't drop
+        // an already-incurred cost/impact record.
+        await _recordConsumption(
+          entryId: entryId,
+          taskId: linkedTaskId,
+          categoryId: taskEntity is Task ? taskEntity.meta.categoryId : null,
+          skillId: skill.id,
+          provider: provider,
+          modelId: modelId,
+          responseType: skill.skillType.toResponseType,
+          usage: null,
+          impact: impactCollector.impact,
+          start: start,
+        );
+
+        if (taskEntity is! Task) {
+          throw StateError(
+            'Linked task $linkedTaskId not found before cover art save',
+          );
+        }
🤖 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 `@lib/features/ai/services/skill_inference_runner.dart` around lines 731 - 766,
Consumption for the image-generation path can be lost if the linked task
disappears after the billed call completes; in the branch that uses
generateImage in skill_inference_runner.dart, move the _recordConsumption call
to immediately after generateImage returns and before re-fetching taskEntity.
Keep the later task existence/category check for validation, but ensure the
consumption record is written first so a StateError from the post-call check
does not skip billing/impact tracking. Use the generateImage,
_recordConsumption, and taskEntity flow in this method to locate the reordering.
test/features/daily_os_next/agents/workflow/day_agent_workflow_test.dart (1)

4078-4087: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Capture the consumption IDs in _ConversationHarness.sendMessage.
sendMessageCalls still records only inferenceRepo, message, model, toolChoice, and tools, so the new consumptionAgentId / consumptionWakeRunKey / consumptionThreadId arguments can’t be asserted in this test harness. Add them to the recorded tuple so the day-agent wiring can be checked.

🤖 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 `@test/features/daily_os_next/agents/workflow/day_agent_workflow_test.dart`
around lines 4078 - 4087, The `_ConversationHarness.sendMessage` test recorder
is missing the new consumption identifiers, so the day-agent wiring cannot be
asserted. Update the `sendMessageCalls` tuple to include `consumptionAgentId`,
`consumptionWakeRunKey`, and `consumptionThreadId`, and make sure the harness
records these same values from `sendMessage` alongside `inferenceRepo`,
`message`, `model`, `toolChoice`, and `tools`.
🧹 Nitpick comments (14)
lib/features/ai/model/ai_call_impact.dart (1)

15-130: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider Freezed for consistency.

MeliousCallImpact hand-rolls ==/hashCode/toString while the sibling AiConsumptionEvent model in this PR uses @freezed. Not required, but converting would reduce boilerplate and match the pattern used elsewhere in this cohort.

🤖 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 `@lib/features/ai/model/ai_call_impact.dart` around lines 15 - 130,
MeliousCallImpact is hand-implementing value semantics while the sibling
AiConsumptionEvent uses Freezed, so align this model with that pattern for
consistency. Update MeliousCallImpact to use `@freezed` and move the current
constructor/factory parsing logic into the generated-style structure, keeping
fromResponseJson and the existing fields intact. Remove the manual ==, hashCode,
and toString overrides once Freezed covers them, and make sure the class remains
easy to locate by its MeliousCallImpact symbol and fromResponseJson factory.
lib/features/ai/repository/melious_inference_repository.dart (3)

1385-1533: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding a timeout-failure test for the non-streaming impact path.

Other network paths in this file (listModels, generateImage) have explicit timeout tests, but the new _postChatCompletion/_nonStreamingChat path (used whenever an impactCollector is supplied) doesn't. Given the increased _chatCompletionTimeout and its role in cost/impact tracking correctness, a Duration.zero timeout test mirroring the existing pattern would close the gap.

🤖 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 `@lib/features/ai/repository/melious_inference_repository.dart` around lines
1385 - 1533, Add a timeout-failure test for the non-streaming impact path in
melious_inference_repository.dart by covering the _postChatCompletion /
_nonStreamingChat flow used when an impactCollector is provided. Mirror the
existing timeout test patterns used for listModels and generateImage, and verify
that a Duration.zero timeout causes the expected failure through the
non-streaming chat completion path while respecting the _chatCompletionTimeout
behavior. Focus the test around the _postChatCompletion and _nonStreamingChat
symbols so the coverage stays aligned with the impact-tracking branch.

550-555: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

_asInt duplicates the pre-existing _integerValue helper.

Both are private static methods with identical bodies (int/num/String → nullable int coercion). Consolidate into one.

♻️ Proposed consolidation
-  static CompletionUsage? _parseUsage(Object? raw) {
+  static CompletionUsage? _parseUsage(Object? raw) {
     if (raw is! Map) return null;
     final map = raw.cast<String, dynamic>();
-    final prompt = _asInt(map['prompt_tokens']);
-    final completion = _asInt(map['completion_tokens']);
-    final cached = _asInt(map['cached_tokens']);
+    final prompt = _integerValue(map['prompt_tokens']);
+    final completion = _integerValue(map['completion_tokens']);
+    final cached = _integerValue(map['cached_tokens']);
     return CompletionUsage(
       promptTokens: prompt,
       completionTokens: completion,
       totalTokens:
-          _asInt(map['total_tokens']) ?? (prompt ?? 0) + (completion ?? 0),
+          _integerValue(map['total_tokens']) ?? (prompt ?? 0) + (completion ?? 0),
       promptTokensDetails: cached != null
           ? PromptTokensDetails(cachedTokens: cached)
           : null,
     );
   }

-  static int? _asInt(Object? value) {
-    if (value is int) return value;
-    if (value is num) return value.toInt();
-    if (value is String) return int.tryParse(value);
-    return null;
-  }

Also applies to: 948-953

🤖 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 `@lib/features/ai/repository/melious_inference_repository.dart` around lines
550 - 555, The private static helper _asInt duplicates the existing
_integerValue conversion logic in melious_inference_repository.dart. Remove the
duplicate by reusing the shared helper from the repository class, and update the
call sites in the affected inference parsing paths to use the single coercion
method so the int/num/String-to-nullable-int behavior lives in one place only.

43-43: 🩺 Stability & Availability | 🔵 Trivial

Non-streaming impact path suppresses progress updates for the full call duration (up to 300s).

_nonStreamingChat yields nothing until the whole response (or the 300s timeout) resolves, so onProgress in UnifiedAiInferenceRepository won't fire incrementally for measured Melious calls — a UX regression relative to the streaming path for potentially long-running responses (e.g. reasoning models). This is an accepted trade-off for capturing environment_impact/billing_cost (only available on non-streaming responses), but worth surfacing since it affects perceived responsiveness.

Also applies to: 374-433

🤖 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 `@lib/features/ai/repository/melious_inference_repository.dart` at line 43,
`_nonStreamingChat` in `MeliousInferenceRepository` blocks progress callbacks
for the full request, so make that behavior explicit where the non-streaming
path is chosen. Update the implementation/comments around
`_chatCompletionTimeout` and the
`_nonStreamingChat`/`UnifiedAiInferenceRepository` flow to clearly document that
`onProgress` will not fire incrementally for impact-aware Melious calls because
`environment_impact` and `billing_cost` require the non-streaming response. If
needed, surface this trade-off in the call path that selects streaming vs
non-streaming so future readers understand the responsiveness impact.
test/features/ai/repository/unified_ai_inference_repository_test.dart (1)

1286-1300: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move the new fallback registration out of the test body, matching this file's own convention.

Every other registerFallbackValue(...) call in this suite lives in setUpAll() (lines 106-124); this one is registered inline inside a single test. Consider moving registerFallbackValue(consumption.makeConsumptionEvent()) to setUpAll() (or centralizing it in test/helpers/fallbacks.dart per convention) for consistency.

As per path instructions: "Centralize fallback values in test/helpers/fallbacks.dart and register with registerFallbackValue(...)."

🤖 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 `@test/features/ai/repository/unified_ai_inference_repository_test.dart` around
lines 1286 - 1300, The fallback registration for the consumption event is
inconsistent with this suite’s setup and should be centralized instead of done
inside the individual test. Move the
`registerFallbackValue(consumption.makeConsumptionEvent())` call out of the test
body and into the shared setup used by this file, preferably `setUpAll()` in
`unified_ai_inference_repository_test.dart` or the common fallback helper in
`test/helpers/fallbacks.dart`, keeping the `MockAiConsumptionRecorder` test
logic unchanged.

Source: Path instructions

test/features/ai/repository/inference_repository_interface_test.dart (1)

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

Minor inconsistency: impactCollector isn't captured like the other parameters.

Every other parameter here is recorded into a lastX field (e.g. lastSignatureCollector), but impactCollector is accepted and dropped. Harmless today since no test asserts on it, but consider adding lastImpactCollector for parity if a future test needs to verify pass-through.

Also applies to: 47-72

🤖 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 `@test/features/ai/repository/inference_repository_interface_test.dart` at line
4, The InferenceRepositoryInterface test setup is inconsistent because
impactCollector is passed in but not stored like the other captured parameters.
Update the relevant test helper/mocks in InferenceRepositoryInterface so it also
records lastImpactCollector alongside the existing lastSignatureCollector-style
fields, ensuring the collector passed through the interface can be asserted
later if needed.
lib/features/ai/services/skill_inference_runner.dart (2)

215-238: 📐 Maintainability & Code Quality | 🔵 Trivial

Duplicated stream-collection + usage-capture logic across three methods.

The CompletionUsage? usage; await for (...) { if (chunk.usage != null) usage = chunk.usage; ... } block is copy-pasted identically in runTranscription, runImageAnalysis, and runPromptGeneration. Consider extracting a small helper (e.g. Future<(String, CompletionUsage?)> _collectStream(Stream<...> stream)) that returns the concatenated content and captured usage, to avoid triple maintenance if the accumulation logic ever needs to change (e.g. handling multiple usage chunks).

Also applies to: 384-407, 538-561

🤖 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 `@lib/features/ai/services/skill_inference_runner.dart` around lines 215 - 238,
The stream-consumption and usage-tracking logic is duplicated in
runTranscription, runImageAnalysis, and runPromptGeneration, making the same
accumulation block hard to maintain. Extract the repeated await for (...) loop
into a small helper on skill_inference_runner.dart, such as a private
_collectStream method that takes the response stream and returns both the
concatenated content and the last seen CompletionUsage. Update each of the three
methods to call that helper and keep the existing _recordConsumption call using
the returned usage and buffer content.

835-835: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Consider uuid.v4() instead of uuid.v1() for the consumption event id.

uuid.v1() embeds a timestamp and a node identifier that (per the uuid package's docs) is randomly generated once and then reused for subsequent v1 calls from the same instance — this can make consumption events linkable to the same originating session/device across synced records, and duplicates the timestamp already captured explicitly via createdAt: start. v4() (purely random) is the more conventional choice for an opaque record id and avoids any node-based correlation, especially since these events sync across devices via Matrix.

Please confirm how uuid (the shared instance referenced here) is configured elsewhere in the codebase — if a custom node option is ever supplied, verify it isn't derived from real hardware identifiers.

🤖 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 `@lib/features/ai/services/skill_inference_runner.dart` at line 835, The
consumption event id in skill inference should use an opaque random UUID rather
than a time/node-based one. Update the id assignment in the consumption event
creation path that currently uses the shared `uuid` instance’s `v1()` call to
`uuid.v4()`, and keep `createdAt: start` as the time source. Also verify any
shared `uuid` configuration elsewhere does not supply a custom `node` value
derived from real hardware identifiers.
lib/features/ai_consumption/database/consumption_database.dart (1)

52-74: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

OFFSET-based pagination degrades on large tables.

Each batch re-scans and discards all prior rows, so total cost across all pages grows roughly O(n²) as the table (and offset) grows. For a high-volume, append-only table intended to log every AI backend call, this stream could see meaningful degradation on larger installs or full backfills.

Consider keyset pagination instead (track the last-seen id and query WHERE id > ? ORDER BY id LIMIT ?), which scales linearly regardless of table size.

♻️ Suggested keyset-pagination approach
-    var offset = 0;
+    String? lastId;
     while (true) {
       final rows = await customSelect(
-        'SELECT id, serialized FROM consumption_events '
-        'ORDER BY id LIMIT ? OFFSET ?',
-        variables: [Variable(batchSize), Variable(offset)],
+        'SELECT id, serialized FROM consumption_events '
+        '${lastId != null ? 'WHERE id > ? ' : ''}'
+        'ORDER BY id LIMIT ?',
+        variables: [
+          if (lastId != null) Variable(lastId),
+          Variable(batchSize),
+        ],
       ).get();

       if (rows.isEmpty) break;

       yield rows
           .map(
             (row) => (
               id: row.read<String>('id'),
               vectorClock: _extractVectorClock(row.read<String>('serialized')),
             ),
           )
           .toList();
-      offset += batchSize;
+      lastId = rows.last.read<String>('id');
     }
🤖 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 `@lib/features/ai_consumption/database/consumption_database.dart` around lines
52 - 74, The streamConsumptionEventsWithVectorClock method currently uses OFFSET
pagination, which will get slower as consumption_events grows. Update this
method to use keyset pagination instead by tracking the last seen id and
querying with a WHERE id > ? clause plus ORDER BY id LIMIT ?; keep the batching
behavior and loop termination the same, but remove the offset-based scan so the
pagination scales linearly.
lib/features/ai_consumption/database/consumption_database.drift (1)

76-81: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Drop the unused vector-clock query or route callers through it.

getConsumptionEventVectorClockById isn’t referenced in source, while ConsumptionRepository.getVectorClock still uses the same json_extract(... '$.vectorClock') SQL inline. ConsumptionDatabase._extractVectorClock is a separate path for the serialized batch stream, so the only duplicated lookup here is between the inline query and the named query.

🤖 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 `@lib/features/ai_consumption/database/consumption_database.drift` around lines
76 - 81, The vector-clock lookup is duplicated:
`getConsumptionEventVectorClockById` in the Drift database schema is currently
unused, while `ConsumptionRepository.getVectorClock` still embeds the same
`json_extract(... '$.vectorClock')` query inline. Either remove the unused named
query from `consumption_database.drift` or update
`ConsumptionRepository.getVectorClock` to call the generated query method so
there is a single source of truth; leave
`ConsumptionDatabase._extractVectorClock` unchanged since it serves the
batch-stream path.
lib/features/ai/conversation/conversation_repository.dart (1)

280-284: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider clock.now() instead of raw DateTime.now() for turn timing.

turnStart (line 283) and the durationMs computation in _recordTurnConsumption (line 495) use raw DateTime.now(), while sibling workflow files in this same PR (e.g. event_agent_workflow.dart's final now = clock.now();) use package:clock for testability. Since durationMs/createdAt are now persisted, billed/audited consumption-tracking fields, using the injectable clock would keep this path consistent with the rest of the codebase and deterministically testable.

♻️ Proposed fix
-        final turnStart = DateTime.now();
+        final turnStart = clock.now();
-        durationMs: DateTime.now().difference(start).inMilliseconds,
+        durationMs: clock.now().difference(start).inMilliseconds,

Also applies to: 459-511

🤖 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 `@lib/features/ai/conversation/conversation_repository.dart` around lines 280 -
284, The turn timing in ConversationRepository is using raw DateTime.now() in
both the turn start capture and the duration calculation path, which should be
aligned with the injectable clock used elsewhere. Update the turn-start logic in
the conversation flow and the timing math in _recordTurnConsumption to use
clock.now() instead of DateTime.now(), keeping the timing source consistent and
testable through the same clock-based pattern used in sibling workflow code.
test/features/sync/matrix/sync_event_processor_consumption_handlers_test.dart (1)

13-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Centralize the consumption-event fallback value instead of registering it inline.

registerFallbackValue(makeConsumptionEvent()) is registered directly in this test's setUpAll, alongside the shared registerSyncProcessorFallbacks() helper. As per path instructions, fallback values should be centralized in test/helpers/fallbacks.dart rather than declared ad hoc per test file.

♻️ Suggested consolidation
-  setUpAll(() {
-    registerSyncProcessorFallbacks();
-    registerFallbackValue(makeConsumptionEvent());
-  });
+  setUpAll(registerSyncProcessorFallbacks);

And add the AiConsumptionEvent fallback registration inside registerSyncProcessorFallbacks() in test/helpers/fallbacks.dart (or the shared helper it delegates to).

🤖 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
`@test/features/sync/matrix/sync_event_processor_consumption_handlers_test.dart`
around lines 13 - 16, The consumption-event fallback is being registered ad hoc
in the test file’s setUpAll, which should be centralized instead. Remove the
inline registerFallbackValue(makeConsumptionEvent()) call from
sync_event_processor_consumption_handlers_test and add the AiConsumptionEvent
fallback registration inside registerSyncProcessorFallbacks() in
test/helpers/fallbacks.dart (or the shared helper it calls), so this test only
relies on the shared fallback setup.

Source: Path instructions

lib/features/agents/workflow/template_evolution_session.dart (1)

254-260: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Cache the isRegistered check once.

getIt.isRegistered<AiConsumptionRecorder>() is evaluated twice for the same two fields. day_agent_workflow.dart in this same PR caches this check into a local (recordConsumption) and reuses it — worth mirroring here (and in template_evolution_workflow.dart, which has the same duplication) for consistency.

♻️ Proposed fix
+      final recordConsumption = getIt.isRegistered<AiConsumptionRecorder>();
       await conversationRepository.sendMessage(
         ...
         // Evolution attributes consumption to the template being improved.
-        consumptionAgentId: getIt.isRegistered<AiConsumptionRecorder>()
-            ? templateId
-            : null,
-        consumptionThreadId: getIt.isRegistered<AiConsumptionRecorder>()
-            ? conversationId
-            : null,
+        consumptionAgentId: recordConsumption ? templateId : null,
+        consumptionThreadId: recordConsumption ? conversationId : null,
🤖 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 `@lib/features/agents/workflow/template_evolution_session.dart` around lines
254 - 260, Cache the result of getIt.isRegistered<AiConsumptionRecorder>() once
and reuse it for both consumptionAgentId and consumptionThreadId, instead of
evaluating it twice. Update the logic in the template evolution session flow by
introducing a local boolean like recordConsumption, mirroring the pattern used
in day_agent_workflow.dart, and apply the same cleanup in
template_evolution_workflow.dart where the duplication also exists.
test/features/agents/workflow/task_agent_workflow_test_helpers.dart (1)

34-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Forward the consumption IDs into sendMessageDelegate. MockConversationRepository.sendMessage accepts the new consumption fields, but the delegate type and call still drop them, so tests using this helper can’t assert the ownership IDs that TaskAgentWorkflow passes through.

🤖 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 `@test/features/agents/workflow/task_agent_workflow_test_helpers.dart` around
lines 34 - 46, Forward the new consumption ID fields through
MockConversationRepository.sendMessage by updating the sendMessageDelegate
signature in task_agent_workflow_test_helpers.dart to accept the
ownership/consumption IDs and passing them along from the sendMessage call site.
Make sure the delegate type stays aligned with
MockConversationRepository.sendMessage and TaskAgentWorkflow so tests can assert
the IDs are preserved end-to-end.
🤖 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.

Outside diff comments:
In `@lib/features/ai/services/skill_inference_runner.dart`:
- Around line 731-766: Consumption for the image-generation path can be lost if
the linked task disappears after the billed call completes; in the branch that
uses generateImage in skill_inference_runner.dart, move the _recordConsumption
call to immediately after generateImage returns and before re-fetching
taskEntity. Keep the later task existence/category check for validation, but
ensure the consumption record is written first so a StateError from the
post-call check does not skip billing/impact tracking. Use the generateImage,
_recordConsumption, and taskEntity flow in this method to locate the reordering.

In `@test/features/daily_os_next/agents/workflow/day_agent_workflow_test.dart`:
- Around line 4078-4087: The `_ConversationHarness.sendMessage` test recorder is
missing the new consumption identifiers, so the day-agent wiring cannot be
asserted. Update the `sendMessageCalls` tuple to include `consumptionAgentId`,
`consumptionWakeRunKey`, and `consumptionThreadId`, and make sure the harness
records these same values from `sendMessage` alongside `inferenceRepo`,
`message`, `model`, `toolChoice`, and `tools`.

---

Nitpick comments:
In `@lib/features/agents/workflow/template_evolution_session.dart`:
- Around line 254-260: Cache the result of
getIt.isRegistered<AiConsumptionRecorder>() once and reuse it for both
consumptionAgentId and consumptionThreadId, instead of evaluating it twice.
Update the logic in the template evolution session flow by introducing a local
boolean like recordConsumption, mirroring the pattern used in
day_agent_workflow.dart, and apply the same cleanup in
template_evolution_workflow.dart where the duplication also exists.

In `@lib/features/ai_consumption/database/consumption_database.dart`:
- Around line 52-74: The streamConsumptionEventsWithVectorClock method currently
uses OFFSET pagination, which will get slower as consumption_events grows.
Update this method to use keyset pagination instead by tracking the last seen id
and querying with a WHERE id > ? clause plus ORDER BY id LIMIT ?; keep the
batching behavior and loop termination the same, but remove the offset-based
scan so the pagination scales linearly.

In `@lib/features/ai_consumption/database/consumption_database.drift`:
- Around line 76-81: The vector-clock lookup is duplicated:
`getConsumptionEventVectorClockById` in the Drift database schema is currently
unused, while `ConsumptionRepository.getVectorClock` still embeds the same
`json_extract(... '$.vectorClock')` query inline. Either remove the unused named
query from `consumption_database.drift` or update
`ConsumptionRepository.getVectorClock` to call the generated query method so
there is a single source of truth; leave
`ConsumptionDatabase._extractVectorClock` unchanged since it serves the
batch-stream path.

In `@lib/features/ai/conversation/conversation_repository.dart`:
- Around line 280-284: The turn timing in ConversationRepository is using raw
DateTime.now() in both the turn start capture and the duration calculation path,
which should be aligned with the injectable clock used elsewhere. Update the
turn-start logic in the conversation flow and the timing math in
_recordTurnConsumption to use clock.now() instead of DateTime.now(), keeping the
timing source consistent and testable through the same clock-based pattern used
in sibling workflow code.

In `@lib/features/ai/model/ai_call_impact.dart`:
- Around line 15-130: MeliousCallImpact is hand-implementing value semantics
while the sibling AiConsumptionEvent uses Freezed, so align this model with that
pattern for consistency. Update MeliousCallImpact to use `@freezed` and move the
current constructor/factory parsing logic into the generated-style structure,
keeping fromResponseJson and the existing fields intact. Remove the manual ==,
hashCode, and toString overrides once Freezed covers them, and make sure the
class remains easy to locate by its MeliousCallImpact symbol and
fromResponseJson factory.

In `@lib/features/ai/repository/melious_inference_repository.dart`:
- Around line 1385-1533: Add a timeout-failure test for the non-streaming impact
path in melious_inference_repository.dart by covering the _postChatCompletion /
_nonStreamingChat flow used when an impactCollector is provided. Mirror the
existing timeout test patterns used for listModels and generateImage, and verify
that a Duration.zero timeout causes the expected failure through the
non-streaming chat completion path while respecting the _chatCompletionTimeout
behavior. Focus the test around the _postChatCompletion and _nonStreamingChat
symbols so the coverage stays aligned with the impact-tracking branch.
- Around line 550-555: The private static helper _asInt duplicates the existing
_integerValue conversion logic in melious_inference_repository.dart. Remove the
duplicate by reusing the shared helper from the repository class, and update the
call sites in the affected inference parsing paths to use the single coercion
method so the int/num/String-to-nullable-int behavior lives in one place only.
- Line 43: `_nonStreamingChat` in `MeliousInferenceRepository` blocks progress
callbacks for the full request, so make that behavior explicit where the
non-streaming path is chosen. Update the implementation/comments around
`_chatCompletionTimeout` and the
`_nonStreamingChat`/`UnifiedAiInferenceRepository` flow to clearly document that
`onProgress` will not fire incrementally for impact-aware Melious calls because
`environment_impact` and `billing_cost` require the non-streaming response. If
needed, surface this trade-off in the call path that selects streaming vs
non-streaming so future readers understand the responsiveness impact.

In `@lib/features/ai/services/skill_inference_runner.dart`:
- Around line 215-238: The stream-consumption and usage-tracking logic is
duplicated in runTranscription, runImageAnalysis, and runPromptGeneration,
making the same accumulation block hard to maintain. Extract the repeated await
for (...) loop into a small helper on skill_inference_runner.dart, such as a
private _collectStream method that takes the response stream and returns both
the concatenated content and the last seen CompletionUsage. Update each of the
three methods to call that helper and keep the existing _recordConsumption call
using the returned usage and buffer content.
- Line 835: The consumption event id in skill inference should use an opaque
random UUID rather than a time/node-based one. Update the id assignment in the
consumption event creation path that currently uses the shared `uuid` instance’s
`v1()` call to `uuid.v4()`, and keep `createdAt: start` as the time source. Also
verify any shared `uuid` configuration elsewhere does not supply a custom `node`
value derived from real hardware identifiers.

In `@test/features/agents/workflow/task_agent_workflow_test_helpers.dart`:
- Around line 34-46: Forward the new consumption ID fields through
MockConversationRepository.sendMessage by updating the sendMessageDelegate
signature in task_agent_workflow_test_helpers.dart to accept the
ownership/consumption IDs and passing them along from the sendMessage call site.
Make sure the delegate type stays aligned with
MockConversationRepository.sendMessage and TaskAgentWorkflow so tests can assert
the IDs are preserved end-to-end.

In `@test/features/ai/repository/inference_repository_interface_test.dart`:
- Line 4: The InferenceRepositoryInterface test setup is inconsistent because
impactCollector is passed in but not stored like the other captured parameters.
Update the relevant test helper/mocks in InferenceRepositoryInterface so it also
records lastImpactCollector alongside the existing lastSignatureCollector-style
fields, ensuring the collector passed through the interface can be asserted
later if needed.

In `@test/features/ai/repository/unified_ai_inference_repository_test.dart`:
- Around line 1286-1300: The fallback registration for the consumption event is
inconsistent with this suite’s setup and should be centralized instead of done
inside the individual test. Move the
`registerFallbackValue(consumption.makeConsumptionEvent())` call out of the test
body and into the shared setup used by this file, preferably `setUpAll()` in
`unified_ai_inference_repository_test.dart` or the common fallback helper in
`test/helpers/fallbacks.dart`, keeping the `MockAiConsumptionRecorder` test
logic unchanged.

In
`@test/features/sync/matrix/sync_event_processor_consumption_handlers_test.dart`:
- Around line 13-16: The consumption-event fallback is being registered ad hoc
in the test file’s setUpAll, which should be centralized instead. Remove the
inline registerFallbackValue(makeConsumptionEvent()) call from
sync_event_processor_consumption_handlers_test and add the AiConsumptionEvent
fallback registration inside registerSyncProcessorFallbacks() in
test/helpers/fallbacks.dart (or the shared helper it calls), so this test only
relies on the shared fallback setup.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0c9128d9-25f9-4d7b-887a-57609124e705

📥 Commits

Reviewing files that changed from the base of the PR and between ddb8e10 and 54b3e70.

📒 Files selected for processing (90)
  • lib/features/agents/workflow/event_agent_workflow.dart
  • lib/features/agents/workflow/project_agent_execute.dart
  • lib/features/agents/workflow/project_agent_workflow.dart
  • lib/features/agents/workflow/soul_evolution_workflow.dart
  • lib/features/agents/workflow/task_agent_execute.dart
  • lib/features/agents/workflow/task_agent_workflow.dart
  • lib/features/agents/workflow/template_evolution_session.dart
  • lib/features/agents/workflow/template_evolution_workflow.dart
  • lib/features/ai/conversation/conversation_repository.dart
  • lib/features/ai/model/ai_call_impact.dart
  • lib/features/ai/repository/ai_consumption_mapping.dart
  • lib/features/ai/repository/cloud_inference_generate.dart
  • lib/features/ai/repository/cloud_inference_generate_more.dart
  • lib/features/ai/repository/cloud_inference_repository.dart
  • lib/features/ai/repository/cloud_inference_wrapper.dart
  • lib/features/ai/repository/inference_repository_interface.dart
  • lib/features/ai/repository/melious_inference_repository.dart
  • lib/features/ai/repository/ollama_inference_repository.dart
  • lib/features/ai/repository/unified_ai_inference_repository.dart
  • lib/features/ai/services/skill_inference_runner.dart
  • lib/features/ai_consumption/README.md
  • lib/features/ai_consumption/consumption/ai_consumption_recorder.dart
  • lib/features/ai_consumption/database/consumption_database.dart
  • lib/features/ai_consumption/database/consumption_database.drift
  • lib/features/ai_consumption/database/consumption_database.g.dart
  • lib/features/ai_consumption/database/consumption_db_conversions.dart
  • lib/features/ai_consumption/logic/consumption_bucketing.dart
  • lib/features/ai_consumption/model/ai_consumption_enums.dart
  • lib/features/ai_consumption/model/ai_consumption_event.dart
  • lib/features/ai_consumption/model/ai_consumption_event.freezed.dart
  • lib/features/ai_consumption/model/ai_consumption_event.g.dart
  • lib/features/ai_consumption/model/consumption_aggregation_models.dart
  • lib/features/ai_consumption/repository/consumption_repository.dart
  • lib/features/ai_consumption/sync/consumption_sync_service.dart
  • lib/features/daily_os_next/agents/workflow/day_agent_workflow.dart
  • lib/features/sync/backfill/backfill_response_builders.dart
  • lib/features/sync/backfill/backfill_response_handler.dart
  • lib/features/sync/matrix/matrix_service.dart
  • lib/features/sync/matrix/sync_event_processor.dart
  • lib/features/sync/matrix/sync_event_processor_apply.dart
  • lib/features/sync/matrix/sync_event_processor_consumption_handlers.dart
  • lib/features/sync/model/sync_message.dart
  • lib/features/sync/model/sync_message.freezed.dart
  • lib/features/sync/model/sync_message.g.dart
  • lib/features/sync/outbox/outbox_enqueue_writer.dart
  • lib/features/sync/outbox/outbox_enqueue_writer_simple.dart
  • lib/features/sync/outbox/outbox_scheduling.dart
  • lib/features/sync/outbox/outbox_service.dart
  • lib/features/sync/queue/queue_apply_adapter.dart
  • lib/features/sync/sequence/sync_sequence_payload_type.dart
  • lib/features/sync/ui/view_models/outbox_list_item_view_model.dart
  • lib/get_it.dart
  • lib/l10n/app_cs.arb
  • lib/l10n/app_de.arb
  • lib/l10n/app_en.arb
  • lib/l10n/app_es.arb
  • lib/l10n/app_fr.arb
  • lib/l10n/app_localizations.dart
  • lib/l10n/app_localizations_cs.dart
  • lib/l10n/app_localizations_de.dart
  • lib/l10n/app_localizations_en.dart
  • lib/l10n/app_localizations_es.dart
  • lib/l10n/app_localizations_fr.dart
  • lib/l10n/app_localizations_ro.dart
  • lib/l10n/app_ro.arb
  • lib/services/db_notification.dart
  • test/features/agents/workflow/soul_evolution_workflow_test.dart
  • test/features/agents/workflow/task_agent_workflow_test_helpers.dart
  • test/features/agents/workflow/template_evolution_workflow_test.dart
  • test/features/ai/conversation/conversation_repository_test.dart
  • test/features/ai/eval/local_task_agent_inference_eval_test.dart
  • test/features/ai/eval/qwen_local_inference_eval_test.dart
  • test/features/ai/model/ai_call_impact_test.dart
  • test/features/ai/repository/cloud_inference_generate_more_test.dart
  • test/features/ai/repository/cloud_inference_generate_test.dart
  • test/features/ai/repository/inference_repository_interface_test.dart
  • test/features/ai/repository/melious_inference_repository_test.dart
  • test/features/ai/repository/unified_ai_inference_repository_test.dart
  • test/features/ai/services/skill_inference_runner_test.dart
  • test/features/ai_consumption/database/consumption_database_test.dart
  • test/features/ai_consumption/logic/consumption_bucketing_test.dart
  • test/features/ai_consumption/repository/consumption_repository_test.dart
  • test/features/ai_consumption/sync/consumption_sync_service_test.dart
  • test/features/ai_consumption/test_utils.dart
  • test/features/daily_os_next/agents/workflow/day_agent_workflow_test.dart
  • test/features/sync/backfill/backfill_response_handler_test.dart
  • test/features/sync/matrix/sync_event_processor_consumption_handlers_test.dart
  • test/features/sync/model/sync_message_consumption_test.dart
  • test/features/sync/sequence/sync_sequence_payload_type_test.dart
  • test/mocks/mocks.dart

matthiasn added 2 commits July 4, 2026 23:05
- Record image-generation consumption before the task-existence check so a
  task deleted mid-flight cannot erase the cost/impact record of an
  already-billed call
- Extract shared _collectStream helper for the triplicated stream-drain +
  usage-capture loop in the skill runner
- Use opaque uuid v4 for consumption event ids (no timestamp/node linkage;
  createdAt captures time explicitly)
- Use injectable clock.now() for turn timing in ConversationRepository
- Consolidate duplicate _asInt into _integerValue in the Melious repository
  and document the non-streaming no-incremental-progress trade-off
- Switch consumption VC-stream pagination from OFFSET to keyset so sequence
  seeding stays linear on large tables
- Route ConsumptionRepository.getVectorClock through the generated named
  drift query instead of duplicating the SQL inline
- Cache the recorder isRegistered check in evolution workflows
- Test infra: centralize the AiConsumptionEvent fallback, capture consumption
  ids in the task/day workflow test harnesses, record lastImpactCollector in
  the interface test double
…d sync paths

Bring the PR's new consumption-tracking code to (near-)full line coverage:

- consumption_aggregation_models + ai_call_impact: value semantics, hasData,
  per-field difference loops, and Glados property tests (additive identity,
  commutativity/associativity, parse round-trips)
- Melious non-streaming path to 100%: timeout failure (no real waiting),
  provider-error status codes, malformed bodies, tool-call/usage coercion
  edges, generateImage impact-presence branches
- ConversationRepository per-turn recording: full field capture incl. all
  impact fields, 1-based turnIndex across turns, parentId == wake run key,
  null-agent and no-recorder guards
- SkillInferenceRunner: transcription token capture (impact null by design)
  and image generation recording consumption even when the linked task
  disappears mid-flight
- Unified inference repository: token/impact field mapping with a populated
  collector via the Melious branch
- Sync arms to 100%: backfill request/response for consumption events,
  handler sequence-log recording and edge paths, outbox enqueue/prepare
  writers, scheduling priority, queue-apply classifier
- Workflow pass-through wiring asserted for task, event, project, day, and
  template/soul evolution agents (recorder registered vs absent)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
test/features/ai/conversation/conversation_repository_test.dart (1)

30-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract _registerConsumptionRecorder() to a shared test helper.

This helper is duplicated verbatim in test/features/ai/repository/unified_ai_inference_repository_test.dart. Since test/features/ai_consumption/test_utils.dart is already shared across several files in this PR (e.g., for makeConsumptionEvent()), moving this registration/teardown helper there too would avoid drift between the two copies.

♻️ Suggested consolidation
-MockAiConsumptionRecorder _registerConsumptionRecorder() {
-  final recorder = MockAiConsumptionRecorder();
-  when(() => recorder.record(any())).thenAnswer((_) async {});
-  if (getIt.isRegistered<AiConsumptionRecorder>()) {
-    getIt.unregister<AiConsumptionRecorder>();
-  }
-  getIt.registerSingleton<AiConsumptionRecorder>(recorder);
-  addTearDown(() {
-    if (getIt.isRegistered<AiConsumptionRecorder>()) {
-      getIt.unregister<AiConsumptionRecorder>();
-    }
-  });
-  return recorder;
-}
+// Moved to test/features/ai_consumption/test_utils.dart as
+// `registerConsumptionRecorder()`, imported here instead.
🤖 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 `@test/features/ai/conversation/conversation_repository_test.dart` around lines
30 - 46, The `_registerConsumptionRecorder()` helper is duplicated across test
suites, so move the registration/teardown logic into the shared test utilities
used by `makeConsumptionEvent()` in `test_utils.dart` and update both
`conversation_repository_test.dart` and
`unified_ai_inference_repository_test.dart` to call that shared helper. Keep the
behavior the same for `MockAiConsumptionRecorder`, `getIt`, and `addTearDown`,
but centralize the implementation so the stub registration stays consistent and
does not drift between tests.
test/features/agents/workflow/template_evolution_workflow_test.dart (1)

290-331: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the AiConsumptionRecorder registration/teardown boilerplate into a shared test helper.

The register-singleton + isRegistered-guarded addTearDown unregister block is now duplicated verbatim across at least four workflow test files (soul_evolution, task_agent, project_agent, template_evolution). A shared helper (e.g. withMockAiConsumptionRecorder() in a common workflow test-utils file) would remove this repetition.

Based on learnings, in workflow test files under test/features/agents/workflow/, local stateful test doubles are fine, but shared boilerplate should move to a helper once a third occurrence needs the same pattern — that threshold has now been passed.

🤖 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 `@test/features/agents/workflow/template_evolution_workflow_test.dart` around
lines 290 - 331, The AiConsumptionRecorder setup/teardown block in this test is
duplicated across multiple workflow test files, so extract it into a shared
helper instead of keeping the registerSingleton plus guarded addTearDown
unregister logic inline. Add a common test utility (for example, a helper around
getIt.registerSingleton<AiConsumptionRecorder> and the matching teardown) and
update this test to use that helper, keeping the local workflow-specific stubs
like _TestConversationRepository and buildSessionWorkflow unchanged.

Source: Learnings

🤖 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.

Nitpick comments:
In `@test/features/agents/workflow/template_evolution_workflow_test.dart`:
- Around line 290-331: The AiConsumptionRecorder setup/teardown block in this
test is duplicated across multiple workflow test files, so extract it into a
shared helper instead of keeping the registerSingleton plus guarded addTearDown
unregister logic inline. Add a common test utility (for example, a helper around
getIt.registerSingleton<AiConsumptionRecorder> and the matching teardown) and
update this test to use that helper, keeping the local workflow-specific stubs
like _TestConversationRepository and buildSessionWorkflow unchanged.

In `@test/features/ai/conversation/conversation_repository_test.dart`:
- Around line 30-46: The `_registerConsumptionRecorder()` helper is duplicated
across test suites, so move the registration/teardown logic into the shared test
utilities used by `makeConsumptionEvent()` in `test_utils.dart` and update both
`conversation_repository_test.dart` and
`unified_ai_inference_repository_test.dart` to call that shared helper. Keep the
behavior the same for `MockAiConsumptionRecorder`, `getIt`, and `addTearDown`,
but centralize the implementation so the stub registration stays consistent and
does not drift between tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 003bd150-eeb2-407f-99ef-ed7f30e3c9b7

📥 Commits

Reviewing files that changed from the base of the PR and between f723aa3 and cc2a5e8.

📒 Files selected for processing (18)
  • test/features/agents/workflow/event_agent_workflow_test.dart
  • test/features/agents/workflow/project_agent_workflow_test.dart
  • test/features/agents/workflow/soul_evolution_workflow_test.dart
  • test/features/agents/workflow/task_agent_workflow_test.dart
  • test/features/agents/workflow/template_evolution_workflow_test.dart
  • test/features/ai/conversation/conversation_repository_test.dart
  • test/features/ai/model/ai_call_impact_test.dart
  • test/features/ai/repository/melious_inference_repository_test.dart
  • test/features/ai/repository/unified_ai_inference_repository_test.dart
  • test/features/ai/services/skill_inference_runner_test.dart
  • test/features/ai_consumption/model/consumption_aggregation_models_test.dart
  • test/features/daily_os_next/agents/workflow/day_agent_workflow_test.dart
  • test/features/sync/backfill/backfill_response_handler_test.dart
  • test/features/sync/matrix/sync_event_processor_consumption_handlers_test.dart
  • test/features/sync/outbox/outbox_enqueue_writer_test.dart
  • test/features/sync/outbox/outbox_scheduling_test.dart
  • test/features/sync/outbox/outbox_service_test.dart
  • test/features/sync/queue/queue_apply_adapter_handlers_test.dart
🚧 Files skipped from review as they are similar to previous changes (2)
  • test/features/daily_os_next/agents/workflow/day_agent_workflow_test.dart
  • test/features/sync/backfill/backfill_response_handler_test.dart

- New unit tests for AiConsumptionRecorder (delegation + never-throws
  logging contract) and the AiResponseType→AiConsumptionResponseType
  mapping (exhaustive over every enum value)
- ConsumptionSyncService: getIt fallback resolution and the
  sequence-record/enqueue failure-swallow branches
- ConsumptionRepository: remaining edge branches
- One-line sync arms pinned: matrix_service consumptionEvent dispatch and
  the outbox list item's payload label
- Event agent workflow: forced update_report retry carries the consumption
  owner ids when a recorder is registered

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 (1)
test/features/agents/workflow/event_agent_workflow_test.dart (1)

950-957: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consumption-recorder registration/teardown boilerplate duplicated.

This exact getIt.registerSingleton<AiConsumptionRecorder>(MockAiConsumptionRecorder()); addTearDown(...) block is identical to the one at lines 414-421. Consider a small registerConsumptionRecorder() test helper to avoid the copy-paste as more consumption-attribution tests are added.

🤖 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 `@test/features/agents/workflow/event_agent_workflow_test.dart` around lines
950 - 957, The AiConsumptionRecorder test setup is duplicated, so extract the
repeated
getIt.registerSingleton<AiConsumptionRecorder>(MockAiConsumptionRecorder()) plus
addTearDown unregistration into a small reusable helper such as
registerConsumptionRecorder(). Update the affected event_agent_workflow_test
cases to call that helper instead of inlining the boilerplate, and keep the
helper responsible for registering and unregistering the mock through getIt.
🤖 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 `@test/features/agents/workflow/event_agent_workflow_test.dart`:
- Around line 938-1031: The forced-retry sendMessageDelegate stub is duplicated
across multiple tests, so extract the shared “first pass publishes nothing,
forced retry publishes update_report” behavior into a reusable helper. Move the
common mockConversationRepository.sendMessageDelegate setup out of the
individual tests in event_agent_workflow_test.dart and parameterize only the
small differences (such as whether the first pass publishes or whether the retry
throws) so the tests call a single helper by name, keeping the existing
assertions intact.

---

Nitpick comments:
In `@test/features/agents/workflow/event_agent_workflow_test.dart`:
- Around line 950-957: The AiConsumptionRecorder test setup is duplicated, so
extract the repeated
getIt.registerSingleton<AiConsumptionRecorder>(MockAiConsumptionRecorder()) plus
addTearDown unregistration into a small reusable helper such as
registerConsumptionRecorder(). Update the affected event_agent_workflow_test
cases to call that helper instead of inlining the boilerplate, and keep the
helper responsible for registering and unregistering the mock through getIt.
🪄 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

Run ID: 74771408-aa67-4749-b6f3-61ad6df3f6bf

📥 Commits

Reviewing files that changed from the base of the PR and between cc2a5e8 and e9d2244.

📒 Files selected for processing (8)
  • test/features/agents/workflow/event_agent_workflow_test.dart
  • test/features/ai/repository/ai_consumption_mapping_test.dart
  • test/features/ai_consumption/consumption/ai_consumption_recorder_test.dart
  • test/features/ai_consumption/repository/consumption_repository_test.dart
  • test/features/ai_consumption/sync/consumption_sync_service_test.dart
  • test/features/sync/matrix/matrix_service_test.dart
  • test/features/sync/ui/view_models/outbox_list_item_view_model_test.dart
  • test/mocks/mocks.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/mocks/mocks.dart

Comment thread test/features/agents/workflow/event_agent_workflow_test.dart
The 'first pass publishes nothing, forced update_report retry publishes
the recap' sendMessageDelegate was copy-pasted across four event-agent
tests; extract stubForcedRetryPublishing with the varying parts (usage
per pass, recap fields, throw-on-retry) as parameters.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant